面向对象三原则¶
面向对象有下面三个特征:
- 封装
- 继承
- 多态
1.封装¶
在通常的类里, 定义了实例所拥有的数据和这个实例能做的操作。 就是:数据(属性/字段) + 行为。
一般情况下, 私有化类所拥有的数据(状态), 使它只能从内部访问。
如果外部想操作它的数据, 必须通过它的set/get方法来操作
对象在它的状态和行为与外部世界之间保持一条界线。
类作为语言的功能,Java提供了下面的限制关键字:
| 关键字 | 类自身 | 同一个包下 | 子类 | 别的包 | 说明 |
|---|---|---|---|---|---|
| public | 〇 | 〇 | 〇 | 〇 | 在类定义的外面都可以访问,不设限 |
| protected | 〇 | 〇 | 〇 | ✖ | 同一个包下,或者子类可以访问 |
| friendly/ default (package public) |
〇 | 〇 | ✖ | ✖ | 同一个包下 |
| private | 〇 | ✖ | ✖ | ✖ | 类内部访问,任何类以外的地方都不可以访问。 |
1 2 3 4 5 6 7 8 9 10 11 | public class Shape { private int color; public void setColor(int color) { this.color = color; } public void display() { //display } } |
2.继承¶
继承: 一个类基于别的类的基础之上, 来构建自己, 来达到代码重用的目的。
被继承的基础类,就是父类(Super)或基类,与之对应的就是子类。
通过从基类派生子类来加强代码重用
在结构化编程中, 常常会复制一个结构, 为它提供一个新名称, 然后添加或修改属性, 使新实体( 比如一个 Account记录) 不同于它的原始来源。 随着时间的推移, 此方法会生成大量重复代码, 这可能带来维护问题。
OOP 引入了继承 的概念, 使得子类**无需额外的代码**就可以**复制**父类的属性和行为。 如果其中一些属性或行为需要更改, 子类可覆盖它们。 您更改的唯一的源代码是创建子类所需的代码。
Types of Inheritance¶

IS-A Relationship¶
继承关系就是[is-A/isKindOf]的关系。 比如:
- Circle是Ellipse的一种,或者说Circle是一个Ellipse
- Ellipse是Shape的一种,或者说,Ellipse是一个Shape
- Square是Rectangle的一种,或者说,Square是一个Rectangle
- Rectangle也是Shape的一种,或者说,Rectangle是一个Shape
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 | public class Shape { private int color; public void setColor(int color) { this.color = color; } public void display() { // 显示形状 } } public class Triangle extends Shape { @Override public void display() { // 显示三角形的形状 } } public class Rect extends Shape { @Override public void display() { // 显示矩形的形状 } } public class Square extends Rect { @Override public void display() { // 显示正方形的形状 } } public class Ellipse extends Shape { @Override public void display() { // 显示椭圆的形状 } } public class Circle extends Ellipse { @Override public void display() { // 显示圆形的形状 } } |
继承关系,就是「is-A」的关系,父类是A,子类是B是,【B is a A】B就是A。
这样就可以把B当做A来处理了。
这种类之间的关系,可以比较抽象化和通用化处理,可以提高对象的独立性。
这是面向对象设计的最最重要的一个因素。
(接口的实现也有同样的效果)
子类的实例可以赋值给父类的变量。如:
1 2 3 4 5 6 | Shape shape; shape = new Rect(); shape = new Ellipse(); shape = new Triangle(); shape = new Square(); shape = new Circle(); |
上面的例子中,
shape变量访问各个子类的实例时,只能访问Shape自身定义的数据和逻辑处理的部分。
子类追加的扩展功能是不可见的。
以最后的Circle为例:
1 | Shape shape = new Circle(); |
把Circle实例的变量赋值给shape变量进行处理,
shape能够访问的范围仅仅是Shape类定义的部分。Circle定义的扩展内容时不能访问的。

- 再来看一个例子:
1 2 3 4 5 6 7 8 | public class Animal { } public class Mammal extends Animal { } public class Reptile extends Animal { } public class Dog extends Mammal { } |
- Mammal IS-A Animal
- Reptile IS-A Animal
- Dog IS-A Mammal
-
Dog IS-A Animal as well
-
画对象的构造时内存的分配图
通用化(泛化)处理:
由于类之间的关联性,实际使用时不使用子类(或叫实际指向的类)而使用父类,这样可以降低类之间的耦合度,达到通用化的处理的目的。同样也可以提高类(对象)的独立性。
Override???
上面的例子中,定义了一个shape数组,并把各个子类的实例填进去,那么在需要遍历他们的时候,只要一个循环就可以了。如:循环画出各种图形(其实就是调用各个之类的display())。
1 2 3 | for(int i = 0 ; i < n ; i++ ){ shape[i].display(); } |
练习:类型的赋值¶
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 | class A{ private int data1; private int data2; public void func1(){ //A.func1.. } public void func2(){ //A.func2.. } public void func3(){ //A.func3.. } } class B : A{ private int data1; private int data3; public void func2(){ //B.func2.. } public void func4(){ //B.func4.. } } class C : B{ private int data5; public func2(){ //C.func2.. } public func3(){ //C.func3.. } } A a = new A(); A a2 = new B(); A a3 = new C(); B b = new B(); B b2 = new A(); B b3 = new C(); B b4 = a2; B b5 = a3; C c = new C(); C c2 = new A(); C c3 = new B(); C c4 = a3; C c5 = b3; a .func1() a .func2() a .func3() a .func4() a2.func1() a2.func2() a2.func3() a2.func4() a3.func1() a3.func2() a3.func3() a3.func4() b .func1() b .func2() b .func3() b .func4() b2.func1() b2.func2() b2.func3() b2.func4() b3.func1() b3.func2() b3.func3() b3.func4() b4.func1() b4.func2() b4.func3() b4.func4() b5.func1() b5.func2() b5.func3() b5.func4() c .func1() c .func2() c .func3() c .func4() c2.func1() c2.func2() c2.func3() c2.func4() c3.func1() c3.func2() c3.func3() c3.func4() c4.func1() c4.func2() c4.func3() c4.func4() c5.func1() c5.func2() c5.func3() c5.func4() |
目标变量必须是其自身及其父类的实例,否则编译器会抛出错误。
总结:
- 子类的实例赋给父类的变量:OK
- 父类的实例赋给子类的变量:NO
3.多态¶
多态是面向对象编程语言的重要特性,它允许基类的指针或引用指向派生类的对象,而在具体访问时实现方法的**动态绑定**。
C++来源的小故事¶
https://en.wikipedia.org/wiki/Cfront
**我们的目标:**C++/java程序可以用C的代码写出来,那多态的理解就OK了。
继续上面例子中,同样是shape变量,却能访问不同子类的实例的图形显示函数。那它的内部是如何实现的呢?还有,子类如果不重写父类的显示方法,那又会执行那个函数呢?
Virtual Methods¶
虚函数是面向对象程序设计中的一个重要的概念。只能适用于指针和参考的计算机工程运算。当从父类中继承的时候,虚函数和被继承的函数具有相同的签名。但是在运行过程中,运行系统将根据对象的类型,自动地选择适当的具体实现运行。虚函数是面向对象编程实现多态的基本手段。
Java 对于方法调用动态绑定的实现主要依赖于方法表(虚函数表)
Java 的方法调用有两类,动态方法调用与静态方法调用。
- 静态方法调用:是指对于类的静态方法的调用方式,是静态绑定的;
- 动态方法调用:需要有方法调用所作用的对象,是动态绑定的。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 | class ClassA { private int data1; private int data2; public void func1() { } public void func2() { } public void func3() { } } class ClassB extends ClassA { private int data3; @Override public void func2() { super.func2(); } } class ClassC extends ClassB { private int data4; @Override public void func3() { super.func3(); } public void func4() { } } class Program { public static void main() { ClassA a = new ClassA(); a.func1(); a.func2(); a.func3(); a = new ClassB(); a.func1(); a.func2(); a.func3(); a = new ClassC(); a.func1(); a.func2(); a.func3(); a.func4(); } } |
上面代码中,内存中的**虚函数表**构图大概如下:
上图中,作为父类的变量a来说,子类ClassB的实例赋给它,并调用a.func2()时,实际上调用的事ClassB的func2函数。
如果这是想访问ClassA的功能,需要调用「super.func2();」
FAQ¶
继承与多态的区别¶
继承与多态的区别
网上看到一个有趣的说法是:继承是子类使用父类的方法,而多态则是父类使用子类的方法。
- 继承:子类继承父类中所有的属性和方法,但是对于private的属性和方法,由于这个是父类的隐私,所以子类虽然是继承了,但是没有可以访问这些属性和方法的权利(引用),所以相当于没有继承到。很多时候,可以理解为,没有继承。
- 多态:就是父类引用可以持有子类对象。这时候只能调用父类中的方法,而子类中特有方法是无法访问的,因为这个时候(编译时)你把他看作父类对象的原因,但是到了运行的时候,编译器就会发现这个父类引用中原来是一个子类的对像,所以如果父类和子类中有相同的方法时,调用的会是子类中的方法,而不是父类的。可以这么说:编译时看父类,运行时看子类。
C# vs Java¶
C# vs Java
- Java:所有方法默认都是virtual method。是可以实现多态的。但如果不想让某个方法具有多态性,在方法前加入final关键字。
- C#:需要在父类方法前加virtual,子类继承的方法前加override才可以实现多态,两者少一个都不可以,就说就算你父类加了virtual,子类如果没有override或者是个new是不能实现多态的,如果父类没有virtual子类没有override关键字那就更不可以了
重写Override vs 重载Overload¶
重写Override vs 重载Overload
- 重写Override:是继承后重新实现父类的方法。
- 重载Overload:是在一个类里一系列参数不同名字相同的方法。
多态在 Java 和 C++ 中的实现比较¶
多态在 Java 和 C++ 中的实现比较
- 单继承情况下,两者实现在本质上相同,都是使用方法表,通过方法表的偏移量来调用具体的方法。
- Java 的方法表中包含 Java 类所定义的所有实例方法,而 C++ 的方法表则只包含需要动态绑定的方法 (virtual 修饰的方法 )。这样,在 Java 下所有的实例方法都要通过方法表调用,而 C++ 中的非虚方法则是静态绑定的。
- 任意 Java 对象只 “指向”一个方法表,而 C++ 在多重继承下则可能指向多个方法表,编译器保证这多个方法表的正确初始化。
- 多层继承中 C++ 面临的主要问题是 this 指针的调整,设计更精巧更复杂;而 Java 在接口调用时完全采用搜索的方式,实现更直观,但调用效率比实例方法调用要慢许多。
可以看到,两者之间既有相似之处,也有不同的地方。对于单继承的实现本质上是一样的,但也有细微的差别(如方法表);差别最大的是对于多重继承(多重接口)的支持。实际上,由于 C++ 是静态编译型语言,它无法像 Java 那样,在运行时刻动态的“查找”所要调用的方法。