面向对象三原则

面向对象有下面三个特征:

  • 封装
  • 继承
  • 多态

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

Inheritance

IS-A Relationship

继承关系就是[is-A/isKindOf]的关系。 比如:

class

  • 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定义的扩展内容时不能访问的。

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();
    }

loop

练习:类型的赋值

 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++来源的小故事

wherecppfrom

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();
    }
}

上面代码中,内存中的**虚函数表**构图大概如下:

vptr

上图中,作为父类的变量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 那样,在运行时刻动态的“查找”所要调用的方法。

this