1. 继承

1.1 背景

代码中创建的类,主要是为了抽象现实中的一些事物(包含属性和方法)。客观事物之间存在一些关联关系,那么在表示为类和对象的时候也会存在一定的关联。

继承(inheritance)机制:是面向对象程序设计使代码可以复用的最重要的手段。 它允许程序员在保持原有的类的特性的基础上进行扩展,增加功能。这样产生新的类,称派生类。继承呈现了面向对象程序设计的层次结构, 体现了由简单到复杂的认知过程。

继承主要解决的问题是:共性的抽取,将 “类” 进行代码重用。

继承的核心概念:
(1)父类(被继承) 超类 基类
(2)子类(继承的) 子类 派生类

例如: 鸟和猫都属于动物,那么就可以抽取出一些共性的内容。

鸟类如果继承了动物类,那么就不需要在鸟类当中再定义 “姓名” 这个属性和 “吃” 的方法了。也就是说子类可以拥有父类的内容,子类也可以拥有属于自己独有的内容。

设计一个类表示动物
注意: 可以给每个类创建一个单独的 java 文件,类名必须和 .java 文件名匹配(大小写敏感)。

// Animal.java
public class Animal {public String name;public Animal(String name) {this.name = name;}public void eat(String food) {System.out.println(this.name + "正在吃" + food);}
}// Cat.java
class Cat {public String name;public Cat(String name) {this.name = name;}public void eat(String food) {System.out.println(this.name + "正在吃" + food);}
}// Bird.java
class Bird {public String name;public Bird(String name) {this.name = name;}public void eat(String food) {System.out.println(this.name + "正在吃" + food);}public void fly() {System.out.println(this.name + "正在飞");}
}

这个代码可以发现存在了大量的冗余代码。仔细分析, Animal 和 Cat 以及 Bird 这几个类中存在一定的关联关系:
(1)这三个类都具备一个相同的 eat 方法,而且行为是完全一样的;
(2)这三个类都具备一个相同的 name 属性,而且意义是完全一样的;
(3)从逻辑上讲,Cat 和 Bird 都是一种 Animal。
此时就可以让 Cat 和 Bird 分别继承 Animal 类,来达到代码重用的效果。

1.2 语法规则

基本语法

class 子类 extends 父类 {}

注意事项:
(1)使用 extends 指定父类;
(2)Java 中一个子类只能继承一个父类(C++/Python 等语言支持多继承);
(3)子类会继承父类的所有 public 的字段和方法;
(4)对于父类的 private 的字段和方法,子类中是无法访问的;
(5)子类的实例中,也包含着父类的实例,可以使用 super 关键字得到父类实例的引用。

对于上面的代码,可以使用继承进行改进。此时让 Cat 和 Bird 继承自 Animal 类,在定义的时候就不必再写 name 字段和 eat 方法。

class Animal {public String name;public Animal(String name) {this.name = name;}public void eat(String food) {System.out.println(this.name + "正在吃" + food);}
}class Cat extends Animal {public Cat(String name) {// 使用 super 调用父类的构造方法super(name);}
}class Bird extends Animal {public Bird(String name) {// 使用 super 调用父类的构造方法super(name);}public void fly() {System.out.println(this.name + "正在飞");}
}public class Test{public static void main(String[] args) {Cat cat1 = new Cat("小黑");cat1.eat("鱼");Bird bird1 = new Bird("小红");bird1.eat("虫子");bird1.fly();}
}

注意事项:
(1)当 Cat 类或者 Bird 类继承了 Animal 类之后,就会把父类的 name 继承过来。所以在子类当中,可以通过 this 关键字访问到 name 属性。
(2)创建子类实例继承父类,会先构造父类对象(执行父类构造方法的逻辑),再构造子类对象(执行子类构造方法的逻辑)。所以在子类的构造函数当中,要先通过 super() 显式调用父类的构造方法。

extends 英文原意指 “扩展”,而我们所写的类的继承,也可以理解成基于父类进行代码上的 “扩展”。例如 Bird 类,就是在 Animal 的基础上扩展出了 fly 方法。

总结:继承的作用
(1)代码复用的一种手段;
(2)用来实现多态。

1.3 对象的内存布局

当子类继承了父类之后,此时子类的内存布局应该是什么样子的?例如以下代码:

class Base {public int m;
}class Derieve extends Base {public int n;
}public class Test{public static void main(String[] args) {Base base1 = new Base();//语句1Derieve derieve = new Derieve();//语句2Base base2 = new Derieve();//语句3 向上转型}
}

注意: base1、derieve、base2 均为引用。

回顾引用:
从本质上讲,引用就是一个变量。比如:Base base1 = new Base(); 这句代码,base1 就是一个引用变量,它指向了一个 Base 对象。也就是说:base1 引用了一个 Base 对象,通过操作 base1 来操作 Base 对象,base1 中存储的是 Base 对象地址的哈希码。

当父类和子类都有同名的数据成员:

class Base {public int m;
}class Derieve extends Base {public int m;public int n;
}public class Test{public static void main(String[] args) {Base base = new Derieve();}
}

子类当中如何访问父类中的相同的数据成员?

class Base {public int m = 10;
}class Derieve extends Base {public int m = 11;public int n;public void func() {System.out.println("访问 Derieve 类新增的数据成员m:" + m);System.out.println("通过 super 访问父类的数据成员:" + super.m);}
}public class Test{public static void main(String[] args) {Base base = new Derieve();System.out.println(base.m);Derieve derieve = new Derieve();System.out.println(derieve.m);System.out.println("=====子类当中如何访问,父类中的数据成员======");derieve.func();}
}

1.4 protected 关键字

我们知道,如果把字段设为 private,子类不能访问。但是设成 public,又违背了 “封装” 的初衷,两全其美的办法就是 protected 关键字
对于类的调用者来说,protected 修饰的字段和方法是不能访问的。
对于类的子类同一个包的其他类来说,protected 修饰的字段和方法是可以访问的。

// Animal.java
public class Animal {protected String name;public Animal(String name) {this.name = name;}public void eat(String food) {System.out.println(this.name + "正在吃" + food);}
}// Bird.java
public class Bird extends Animal {public Bird(String name) {// 使用 super 调用父类的构造方法super(name);}public void fly(){// 对于父类的 protected 字段,子类可以正确访问System.out.println(this.name + "正在飞");}
}// Test.java 和 Animal.java 不在同一个包之中
public class Test{public static void main(String[] args) {Animal animal = new Animal("小动物");// 成功运行Bird bird = new Bird("小鸟");bird.fly();// 此时编译出错, 无法访问 name// System.out.println(animal.name);}
}

小结: Java 中对于字段和方法共有四种访问权限。
public:类的内部和类的调用者都能访问。
protected:类内部能访问,子类和同一个包中的类可以访问,其他类不能访问。
daufault:默认(也叫包访问权限),类内部能访问,同一个包中的类可以访问,其他类不能访问。
private:只能在类的内部访问,类外部不能访问。

访问权限的一般原则:
类尽量要做到 “封装”,即隐藏内部实现细节,只暴露出必要的信息给类的调用者。因此在使用时应该尽可能的使用比较严格的访问权限。例如一个成员,能设定为 private,就尽量设置成 private。 另外,还有一种简单粗暴的做法:将所有的字段设为 private,将所有的方法设为 public。不过这种方式属于是对访问权限的滥用,还是更希望在写代码时认真思考,该类提供的字段方法到底给 “谁” 使用(是类内部自己用,还是类的调用者使用,还是子类使用)。

1.5 更复杂的继承关系

之前的例子中,只涉及到 Animal、Cat 和 Bird 三种类。但是如果情况更复杂一些呢?
针对 Cat ,可能还需要表示更多种类的猫。

这时候使用继承方式来表示,就会涉及到更复杂的体系。

// Animal.java
public Animal {...
}// Cat.java
public Cat extends Animal {...
}// ChineseGardenCat.java
public ChineseGardenCat extends Cat {...
}
// OrangeCat.java
public Orange extends ChineseGardenCat {...
}......

如上面这样的继承方式称为多层继承,即子类还可以进一步地再派生出新的子类。

时刻牢记,我们编写的类是现实事物的抽象。而真正在公司中所遇到的项目往往业务比较复杂,可能会涉及到一系列复杂的概念,都需要使用代码来表示,所以真实项目中所写的类也会有很多,类之间的关系也会更加复杂。
但是即使如此,我们并不希望类之间的继承层次太复杂。一般不希望出现超过三层的继承关系。如果继承层次太多,就需要考虑对代码进行重构了。
如果想从语法上进行限制继承,就可以使用 final 关键字

1.6 final 关键字

曾经学习过 final 关键字,修饰一个变量或者字段的时候,表示常量(不能修改)。

final int a = 10;
a = 20; // 编译出错

final 关键字也能修饰类,此时表示被修饰的类不能被继承。

final public class Animal {...
}public class Bird extends Animal {...
}// 编译出错 Error:(3, 27) java: 无法从最终 Animal 进行继承

final 关键字的功能是限制类被继承。
“限制” 这件事情意味着 “不灵活”。在编程中,灵活往往不见得是一件好事。灵活可能意味着更容易出错。
使用 final 修饰的类被继承时,就会编译报错,此时就可以提示我们这样的继承是有悖这个类设计的初衷。


平时使用的 String 字符串类,就是用 final 修饰的,不能被继承。

2. 组合

与继承类似,组合也是一种表达类之间关系的方式,也能够达到代码重用的效果。

一个类的成员也可以是其他的类。

// Student.java
public class Student {...
}// SchoolMaster.java
public class SchoolMaster {...
}// ClassRoom.java
public class ClassRoom {}// School.java
public class School {public SchoolMaster schoolMaster = new SchoolMaster();public Student student1 = new Student();public Student student2 = new Student();public Student student3 = new Student();public Student student4 = new Student();public ClassRoom classRoom1 = new ClassRoom();public ClassRoom classRoom2 = new ClassRoom();public ClassRoom classRoom3 = new ClassRoom();
}

组合并没有涉及到特殊的语法(诸如 extends 这样的关键字),仅仅是将一个类的实例作为另外一个类的字段。这是我们设计类的常用方式之一。

组合表示 has - a 语义
在刚才的例子中,可以理解成一个学校中 “包含” 若干学生、教室和校长。
继承表示 is - a 语义
在之前的 “动物和猫/鸟” 的例子中,可以理解成一只猫/鸟 “是” 一种动物。

3. 多态

上面已经学过了继承,继承其实就是实现多态的一个前提。在正式开始学习多态之前,首先来看看什么是向上转型。

3.1 向上转型

在刚才的例子中,写了形如下面的代码:

Bird bird = new Bird("圆圆");

这个代码也可以写成如下这样:

Bird bird = new Bird("圆圆");
Animal animal = null;animal = (Animal)bird;
animal = bird; // 向上转型可以省略强制类型转换// 将以上几个代码合并起来
Animal animal = new Bird("圆圆");

此时 animal 是一个父类(Animal)的引用,指向一个子类(Bird)的实例。这种写法称为 向上转型

即向上转型是使用一个父类的引用指向一个子类的实例。

向上转型这样的写法可以结合 is - a 语义 来理解。
例如,我让同学去喂圆圆,就可以说:“同学你喂下小鸟。” 或者 “同学你喂下小动物。”
因为圆圆确实是一只小鸟,也确实是一只小动物。

为什么叫 “向上转型” ?
在面向对象程序设计中,针对一些复杂的场景(很多类,很复杂的继承关系),程序员会用画 UML 图的方式来表示类之间的关系,此时父类通常画在子类的上方。所以就称为 “向上转型”,表示往父类的方向转型。

向上转型发生的时机:
(1)直接赋值
(2)方法传参
(3)方法返回

直接赋值的方式如上面代码,另外两种方式和直接赋值没有本质区别。

方法传参:本质上也是在进行 “赋值” 操作。

// Animal.java
public class Animal {protected String name;public Animal(String name) {this.name = name;}public void eat(String food) {System.out.println(this.name + "正在吃" + food);}
}// Bird.java
public class Bird extends Animal {public Bird(String name) {// 使用 super 调用父类的构造方法super(name);}
}// Test.java
public class Test{public static void main(String[] args) {Bird bird = new Bird("小鸟");feed(bird);}public static void feed(Animal animal){animal.eat("谷子");}
}


此时形参 animal 的类型是 Animal,实际上对应到 Bird 的实例。

方法返回

// Animal.java
public class Animal {protected String name;public Animal(String name) {this.name = name;}public void eat(String food) {System.out.println(this.name + "正在吃" + food);}
}// Bird.java
public class Bird extends Animal {public Bird(String name) {// 使用 super 调用父类的构造方法super(name);}
}// Test.java
public class Test{public static void main(String[] args) {Animal animal = findMyAnimal();}public static Animal findMyAnimal() {Bird bird = new Bird("圆圆");return bird;}
}

此时方法 findMyAnimal 返回的是一个 Animal 类型的引用,但是实际上对应的是 Bird 的实例。

3.2 动态绑定

如果父类中包含的方法,在子类中有对应的同名同参数的方法,就会进行 动态绑定

// Animal.java
public class Animal {protected String name;public Animal(String name) {this.name = name;}public void eat(String food) {System.out.println("我是一只小动物");System.out.println(this.name + "正在吃" + food);}
}// Bird.java
public class Bird extends Animal {public Bird(String name) {super(name);}public void eat(String food) {System.out.println("我是一只小鸟");System.out.println(this.name + "正在吃" + food);}
}// Test.java
public class Test{public static void main(String[] args) {Animal animal1 = new Animal("圆圆");animal1.eat("谷子");Animal animal2 = new Bird("扁扁");animal2.eat("谷子");}
}

此时可以发现:
animal1 和 animal2 虽然都是 Animal 类型的引用,但是 animal1 指向 Animal 类型的实例,animal2 指向 Bird 类型的实例。
针对 animal1 和 animal2 分别调用 eat 方法,发现 animal1.eat() 实际调用了父类的方法,而 animal2.eat() 实际调用了子类的方法。

因此,在 Java 中,调用某个类的方法,究竟执行了哪段代码(是父类方法的代码还是子类方法的代码),要看这个引用究竟指向的是父类对象还是子类对象。这个过程是程序运行时决定的,而不是在编译期决定,因此称为动态绑定。(一般 “动态” / “静态” 分别指的是 “编译期” / “运行时”,与 “static” 无关)。

如果某 同名方法 在父类和子类中都包含,但是 参数不相同,此时不涉及动态绑定

3.3 方法重写

子类实现父类的同名方法,并且参数的类型和个数完全相同,这种情况称为 覆写/重写/覆盖(Override)

注意事项:
(1)重写和重载完全不一样,不要混淆。
(2)普通方法可以重写,static 修饰的静态方法不能重写。
(3)重写中子类的方法的访问权限不能低于父类的方法访问权限。
(4)重写的方法返回值类型不一定和父类的方法相同(但是建议最好写成相同,特殊情况除外)。

针对需要重写的方法,可以使用 @Override 注解 来显式指定。告诉编译器,当前这个子类的方法是重写了父类的方法。其实没有这个注解,代码也能运行,但是加上注解后,编译器就能帮我们进行一些合法性校验和检查。例如不小心将方法名字拼写错了(比如 eat 写成 aet),那么编译器发现父类中没有 aet 方法,就会编译报错,提示无法构成重写。
因此推荐在代码中重写方法时显式加上 @Override 注解。

// Animal.java
public class Animal {protected String name;public Animal(String name) {this.name = name;}public void eat(String food) {System.out.println("我是一只小动物");System.out.println(this.name + "正在吃" + food);}
}// Bird.java
public class Bird extends Animal {public Bird(String name) {super(name);}@Overridepublic void eat(String food) {System.out.println("我是一只小鸟");System.out.println(this.name + "正在吃" + food);}
}

3.4 重写的设计原则

对于已经投入使用的类,尽量不要进行修改。最好的方式是:重新定义一个新的类,来重复利用其中共性的内容,并且添加或者改动新的内容。

例如:若干年前的手机,只能打电话、发短信,来电显示只能显示号码。而今天的手在来电显示时,不仅仅可以显示号码,还可以显示头像,地区等。在这个过程当中,不应该在原来的类上进行修改,因为原来的类,可能还有用户在使用。正确做法是:新建一个新手机的类,对来电显示这个方法进行重写,这样就达到了现在的需求

3.5 重载和重写的区别

动态绑定和方法重写
上面讲的动态绑定和方法重写采用的是相同的代码示例。
重写:父类和子类中,都存在同名方法,且参数相同,此时通过父类引用调用该方法,就会触发重写,具体执行哪个版本的方法由动态绑定规则决定。
因此,方法重写是 Java 语法层次上的规则,而动态绑定是方法重写这个语法规则的底层实现。二者本质上描述的是相同的事情,只是侧重点不同。

3.6 理解多态

多态(polypeptide) 是一种程序设计的思想,具体的语法体现为向上转型、方法重写、动态绑定等。在学习过向上转型、动态绑定、方法重写等内容后,就可以使用多态的思想来设计程序了。

多态直观的理解:一个引用,对应到多种形态(不同类型的实例)。

示例: 打印多种形状。

// Shape.java
public class Shape {public void draw(){// 将当前的形状打印出来}
}// Circle.java
public class Circle extends Shape{@Overridepublic void draw() {// 圆形System.out.println("○");}
}// Circle.java
public class Rect extends Shape{@Overridepublic void draw() {// 矩形System.out.println("[]");}
}// Flower.java
public class Flower extends Shape{@Overridepublic void draw() {// 花System.out.println("❀");}
}/我是分割线//// Test.java
public class Test{public static void main(String[] args) {// 这里体现多态,先创建几个子类的实例Shape shape1 = new Rect();Shape shape2 = new Circle();Shape shape3 = new Flower();drawShape(shape1);drawShape(shape2);drawShape(shape3);}// 打印单个图形public static void drawShape(Shape shape){shape.draw();}
}


在代码中,分割线上方的代码是 类的实现者 编写的,分割线下方的代码是 类的调用者 编写的。

当类的调用者在编写 drawShape 方法时,参数类型为 Shape(父类),此时在 drawShape 方法内部 并不知道,也不关注 当前的 shape 引用指向的是哪个类型(哪个子类)的实例,只要知道这个类有一个 draw 方法就可以了。此时 shape 这个引用调用 draw 方法可能会有多种不同的表现(和 shape 对应的实例相关),这种行为就称为 多态

使用多态的优点?
(1)类的调用者对类的信息了解地更少,使用成本进一步降低。
封装是让类的调用者不需要知道类的实现细节;
多态能让类的调用者连这个类的类型是什么都不必知道,只需要知道这个对象具有某个方法即可。
因此,多态可以理解成是封装的更进一步,让类的调用者对类的使用成本进一步降低。

(2)方便扩展
如果未来要新增一种新的形状,使用多态,代码改动成本也比较低。
创建一个新的子类即可,并让子类也去重写 draw 方法。
类的调用者只需要修改少量代码,创建一个新的类的实例即可。

// 创建一个新的 Triangle(三角形)子类
class Triangle extends Shape {@Overridepublic void draw() {System.out.println("△");}
}

(3)降低代码的 “圈复杂度”,避免使用大量的 if - else
例如现在需要打印的不是一个形状,而是多个形状。如果不基于多态,实现代码如下:

public static void drawShapes() {Rect rect = new Rect();Cycle cycle = new Cycle();Flower flower = new Flower();String[] shapes = {"cycle", "rect", "cycle", "rect", "flower"};for (String shape : shapes) {if (shape.equals("cycle")) {cycle.draw();} else if (shape.equals("rect")) {rect.draw();} else if (shape.equals("flower")) {flower.draw();}}
}

如果使用多态,则代码更简单:

public static void drawShapes() {// 创建一个 Shape 对象的数组Shape[] shapes = {new Cycle(), new Rect(), new Cycle(), new Rect(), new Flower()};for (Shape shape : shapes) {shape.draw();}
}

3.7 向下转型

向上转型是子类对象转成父类对象,向下转型就是父类对象转成子类对象。相比于向上转型来说,向下转型没那么常见,在一些特定场景下才会用到。

向下转型的应用场景:
有些方法只在子类中存在,但是在父类中不存在。此时使用多态的方式就无法执行到对应的子类的方法了。就必须 把父类引用先转回成子类的引用 ,然后再调用对应的方法。

向下转型必须得保证操作是合理的,否则可能会存在问题。

// Animal.java
public class Animal {public String name;public Animal(String name) {this.name = name;}public void eat(String food) {System.out.println("我是一只小动物");System.out.println(this.name + "正在吃" + food);}
}// Bird.java
public class Bird extends Animal {public Bird(String name) {super(name);}@Overridepublic void eat(String food) {System.out.println("我是一只小鸟");System.out.println(this.name + "正在吃" + food);}
}// Cat.java
public class Cat extends Animal {public Cat(String name) {// 使用 super 调用父类的构造方法super(name);}
}// Test.java
public class Test{public static void main(String[] args) {// 向上转型Animal animal = new Cat("tom");// 向下转型// 必须要确定 animal 指向的是一个 Cat 类型的实例才可以进行转换,否则转换可能会失效Cat cat = (Cat) animal;Animal animal2 = new Bird("小鸟");// 操作非法,运行时会报错Cat cat2 = (Cat) animal2;Animal animal3 = new Animal("小动物");// 操作非法,运行时会报错Cat cat3 = (Cat) animal3;}
}


为了让向下转型更安全,可以先判定当前父类的引用是不是指向了该子类,如果不是就不进行向下转型。( instanceof 可以判定一个引用是否是某个类的实例,如果是则返回 true)。

public class Test{public static void main(String[] args) {Animal animal2 = new Bird("小鱼");if (animal2 instanceof Cat){Cat cat2 = (Cat) animal2;}}
}

3.8 super 关键字

如果需要在子类内部调用父类方法,可以使用 super 关键字。

super 表示获取到父类实例的引用。
(1)使用 super 调用父类的构造方法

public class Bird extends Animal {public Bird(String name) {super(name);}
}

(2)使用 super 调用父类的普通方法

// Animal.java
public class Animal {public String name;public Animal(String name) {this.name = name;}public void eat(String food) {System.out.println("我是一只小动物");System.out.println(this.name + "正在吃" + food);}
}// Bird.java
public class Bird extends Animal {public Bird(String name) {super(name);}@Overridepublic void eat(String food) {// 修改代码, 让子调用父类的接口super.eat(food);System.out.println("我是一只小鸟");System.out.println(this.name + "正在吃" + food);}
}// Test.java
public class Test{public static void main(String[] args) {Bird bird = new Bird("小黑");bird.eat("谷子");}
}


在代码中,如果在子类的 eat 方法中直接调用 eat(不加super),那么就认为是在调用子类自己的 eat 方法(即递归),而加上 super 关键字,才是调用父类的方法。

super 和 this 的区别

3.9 在构造方法中调用重写的方法(一段有坑的代码)

创建两个类,B 是父类,D 是子类。D 中重写 func 方法,并且在 B 的构造方法中调用 func。

class A {// this 的类型是 A 类型,实际对应的是整个子类的实例public A() {// do nothingthis.func();}public void func() {System.out.println("A.func()");}
}class B extends A {private int num = 1;@Overridepublic void func() {System.out.println("B.func() " + num);}
}public class Test {public static void main(String[] args) {B b = new B();}
}


(1)A 是 B 的父类,构造 B 的实例时,就需要先构造 A 的实例。
(2)构造 A 的实例,就会调用 A 的构造方法,此时会调用到 this.func(); ,此时的 this 是指向子类的实例,会触发动态绑定,调用到 B 中的 func()
(3)此时 B 的实例自身还没有构造,初始化代码(包括就地初始化、代码块和构造方法)都没有执行,num 处在未初始化的状态,值为 0。

结论:
“用尽量简单的方式使对象进入可工作状态”。在实现构造方法时,最好只进行简单的赋值操作,不要在构造方法中调用其他的方法(如果这个方法被子类重写,就会触发动态绑定,但是此时子类对象还没构造完成)。可能会出现一些隐藏的,但是又极难发现的问题。

4. 执行顺序

几个重要的代码块:实例代码块和静态代码块,在没有继承关系时的执行顺序。

// Person.java
public class Person {public int age;public String name;public Person(String name, int age){this.name = name;this.age = age;System.out.println("构造方法执行");}// 实例代码块{System.out.println("实例代码块执行");}static {System.out.println("静态代码块执行");}
}// Test.java
public class Test {public static void main(String[] args) {Person person1 = new Person("tom", 20);System.out.println("=================");Person person2 = new Person("bob", 25);}
}


(1)静态代码块只执行一次;
(2)静态代码块先执行,实例代码块接着执行,最后构造方法执行。

继承关系中的执行顺序

// Person.java
public class Person {public int age;public String name;public Person(String name, int age){this.name = name;this.age = age;System.out.println("Person:构造方法执行");}// 实例代码块{System.out.println("Person:实例代码块执行");}static {System.out.println("Person:静态代码块执行");}
}// Student.java
public class Student extends Person{public Student(String name, int age){super(name, age);System.out.println("Student:构造方法执行");}// 实例代码块{System.out.println("Student:实例代码块执行");}static {System.out.println("Student:静态代码块执行");}
}// Test.java
public class Test {public static void main(String[] args) {Student student1 = new Student("tom", 20);System.out.println("=================");Student student2 = new Student("bob", 25);}
}


结论:
(1)父类静态代码块优先于子类静态代码块执行,且是最早执行;
(2)父类实例代码块和父类构造方法紧接着执行;
(3)子类的实例代码块和子类构造方法紧接着再执行;
(4)第二次实例化子类对象时,父类和子类的静态代码块都将不会再执行。

5. 抽象类、接口搭配多态使用

5.1 抽象类

之前写过的代码如下,Shape 本身包含的 draw 方法,该方法没有实质的内容,存在的目的只是为了让其他的子类进行重写。Shape 本身也不需要创建实例,Shape 存在的目的就是为了创建 Shape 的子类,搭配多态进行使用。

// Shape.java
public class Shape {public void draw(){}
}// Circle.java
public class Circle extends Shape{@Overridepublic void draw() {// 圆形System.out.println("○");}
}// Test.java
public class Test {public static void main(String[] args) {Shape shape = new Circle();drawShape(shape);}// 打印图形public static void drawShape(Shape shape){shape.draw();}
}

像这样不需要实例化的类,就可以将这个类作为一个 “抽象类”;像这种本身没有方法体,只是为了被子类重写的方法,可以作为一个 “抽象方法”

Java 中使用 abstract 关键字 描述抽象类和抽象方法。

抽象类:
给作为抽象类的类的代码前加上 abstract 关键字,就构成一个 抽象类
如果尝试创建抽象类的实例,会编译报错。
抽象类除了不能实例化外,其他的语法规则都和普通的类一样。
(1)抽象类中可以有普通的属性和方法。
(2)抽象类中可以有静态的属性和方法。
(3)抽象类中可以继承其他的类,也可以被其他的类继承。

抽象方法:
给方法前面加上 abstract 关键字,就构成一个 抽象方法
抽象方法不需要方法体。
抽象方法只能存在于抽象类中(也可以存在于接口中),不能存在于普通的类中。
抽象方法存在的意义就是为了让子类进行重写(抽象方法不能是 private)。

// Shape.java
// 抽象类
abstract public class Shape {// 抽象方法,不需要方法体abstract public void draw();
}// Circle.java
public class Circle extends Shape{@Overridepublic void draw() {// 圆形System.out.println("○");}
}// Test.java
public class Test {public static void main(String[] args) {Shape shape = new Circle();drawShape(shape);// 尝试创建抽象类的实例,会编译报错Shape shape2 = new Shape();}// 打印图形public static void drawShape(Shape shape){shape.draw();}
}

5.2 接口

接口是抽象类的更进一步。
抽象类只是不能实例化,但是其他方面和普通的类区别不大。
接口则更加抽象,不仅不能实例化,同时也不具备类的各种特性。
接口中可以放抽象方法,并且抽象方法不必写 abstract 关键字(写或不写,都是抽象方法)。即接口中不能放普通的方法。
接口中不能放普通的属性,只能放 public、static、final 修饰的属性。
接口不能继承自其他的类,但是可以继承自其他的接口。
接口不能被类继承,而是被其他的类 “实现”(某个类实现了接口,implements 关键字)。

// Shape.java
public interface Shape {// 虽然 draw() 没写 abstract 和 public,但是也表示是一个抽象的公有的方法void draw();// 看起来是普通的属性,实际上是 public、static、final 修饰int num = 10;
}// Circle.java
public class Circle extends Shape{@Overridepublic void draw() {// 圆形System.out.println("○");}
}// Test.java
public class Test {public static void main(String[] args) {Shape shape = new Circle();drawShape(shape);// 尝试创建抽象类的实例,会编译报错Shape shape2 = new Shape();}// 打印图形public static void drawShape(Shape shape){shape.draw();}
}

抽象类与接口的对比:
(1)抽象类与普通的类类似,只是不能实例化。接口则和普通的类相去甚远(包含的属性、方法、与其他类的关系)。
(2)一个类只能继承自一个抽象类,但是一个类可以同时实现多个接口。

接口的作用
解决 Java 中不能多继承的问题。Java 的继承是单继承,而多继承在有些场景下是有用的。Java 中可以通过 继承一个类,实现多个接口 的方式来完成类似于多继承的效果。
接口存在的意义是既能够实现类似于多继承的效果,同时又能规避多继承带来的问题。

代码示例:

// Animal.java
abstract public class Animal {protected String name;public Animal(String name) {this.name = name;}
}// IRunning.java
// 接口的命名,一般使用 I 作为前缀,并且使用形容词词性的单词进行命名
// 接口表示的语义,一个类具有 XXX 特性
public interface IRunning {void run();
}// ISwimming.java
public interface ISwimming {void swim();
}// IFlying.java
public interface IFlying {void fly();
}// IAmphibious.java
public interface IAmphibious extends IRunning, ISwimming{// 接口可以继承自其他的接口// 此时该接口就同时包含了 IRunning, ISwimming 中的抽象方法}// Cat.java
public class Cat extends Animal implements IRunning {public Cat(String name) {super(name);}@Overridepublic void run(){System.out.println(name + "四条腿跑");}
}// Duck.java
public class Duck extends Animal implements IRunning, IFlying, ISwimming{public Duck(String name){super(name);}@Overridepublic void fly() {System.out.println(name + "正在飞");}@Overridepublic void run() {System.out.println(name + "一跳一跳地跑");}@Overridepublic void swim() {System.out.println(name + "正在游泳");}
}// Bird.java
public class Bird extends Animal implements IRunning, IFlying {public Bird(String name) {super(name);}@Overridepublic void run() {System.out.println(name + "一跳一跳地跑");}@Overridepublic void fly() {System.out.println(name + "正在飞");}
}// Frog.java
public class Frog extends Animal implements IAmphibious{public Frog(String name) {super(name);}@Overridepublic void run() {System.out.println(name + "一跳一跳地跑");}@Overridepublic void swim() {System.out.println(name + "正在游泳");}
}

010 面向对象编程相关推荐

  1. 【面向对象编程】(4) 类的继承,重构父类中的方法

    各位同学好,今天和大家分享一下面向对象编程中,类的三大特征之继承.主要介绍:子类继承父类的基本方法:重写父类的类方法:重构父类的初始化方法:super() 方法.本节主要是单继承,多继承在下一节中介绍 ...

  2. 【面向对象编程】(3) 类之间的交互,依赖关系,关联关系

    各位同学好,今天和大家分享一下面向对象编程中,类之间的交互,类之间的依赖关系和关联关系.有不明白的可见前一章节:https://blog.csdn.net/dgvv4/article/details/ ...

  3. 【面向对象编程】(1) 类实例化的基本方法

    各位同学好,本章节和大家分享一下面向对象编程的一些方法,通过一些案例带大家由浅入深掌握面向对象的编程. 1. 最基本的类实例化 创建类的方法是 class 变量名: ,实例化方法是 类名() ,分配属 ...

  4. C#编程概念系列(一):面向对象编程

    系列文章索引目录:http://www.cnblogs.com/loner/archive/2013/05/09/3068211.html 引子: 面向对象编程:这个在当下已不是什么时髦的概念,但通过 ...

  5. JavaScript面向对象编程

    自从有了Ajax这个概念,JavaScript作为Ajax的利器,其作用一路飙升.JavaScript最基本的使用,以及语法.浏览器对象等等东东在这里就不累赘了.把主要篇幅放在如何实现JavaScri ...

  6. python面向对象的优点_Python面向对象编程——总结面向对象的优点

    Python面向对象编程--总结面向对象的优点 一.从代码级别看面向对象 1.在没有学习类这个概念时,数据与功能是分离的 def exc1(host,port,db,charset): conn=co ...

  7. 转载知乎上的一篇:“ 面向对象编程的弊端是什么?”

    2019独角兽企业重金招聘Python工程师标准>>> 弊端是,没有人还记得面向对象原本要解决的问题是什么. 1.面向对象原本要解决什么(或者说有什么优良特性) 似乎很简单,但实际又 ...

  8. c语言面向对象编程中的类_C ++中的面向对象编程

    c语言面向对象编程中的类 Object oriented programming, OOP for short, aims to implement real world entities like ...

  9. ruby 新建对象_Ruby面向对象编程的简介

    ruby 新建对象 by Saul Costa 由Saul Costa Object-oriented programming (OOP) is a programming paradigm orga ...

最新文章

  1. CentOS中安装WiFi图形管理工具
  2. https nginx phpstudy_让phpStudy2018 Nginx 支持WordPress自定义链接
  3. 光流 | 基于光流法检测跟踪视频中的汽车
  4. Spring MVC控制器JUnit测试
  5. 第十三期:你所了解的javascript?
  6. java第七章jdbc课后简答题_javaEE简答题答案
  7. 【模拟】Ingenious Lottery Tickets
  8. eureka注册中心HA集群搭建
  9. Skeljs – 用于构建响应式网站的前端开发框架
  10. 在win7下安装VC6.0
  11. 数据结构: 树形结构+思维导图
  12. 游侠更新仙剑全系列免CD补丁(支持WIN7 SP1)【转载】
  13. 《多收了三五斗》大学毕业版 (转)
  14. win764位loadrunner安装问题:提示:少了Microsoft Visual c++2005 sp1运行时组件,安装时会提示命令行选项语法错误,键入“命令/?”可获取帮肋信息,无法正常安装;
  15. OpenGL一维纹理映射练习
  16. Mysql免安装版下载以及配置
  17. 触手可及的人工智能,加速改变生产生活
  18. 关于SQL注入靶场搭建及过关教程
  19. 机器人操作系统ROS学习实战篇之------让小乌龟画矩形
  20. 前端之PWA使用总结

热门文章

  1. 微软project下载安装及激活教程
  2. 在 Linux 中查找用户帐户和登录详细信息的 11 种方法
  3. 电源芯片选择DC/DC还是LDO?及怎样选择LDO芯片
  4. Vue引入并使用Element-UI组件库的两种方式
  5. VC浏览器相关的学习(五)(在BHO中建立对话框)--解决CreateDialog的1813错误
  6. HTML CSS 布局
  7. 【全栈接口测试进阶系列教程】精通api接口测试,接口分类,接口架构,http,webservice,dubbo接口协议,接口流程,接口工具,cookie,session,token接口鉴权原理以及实战
  8. MATLAB 2018a安装教程(迅雷)
  9. 表格进阶03—出纳日报表(表格,再次练习)
  10. C# 获取微信二维码