文章目录
- 1. 继承
- 1.1 继承概述(理解)
- 1.2 如何继承(掌握)
- 1.2.1 继承的语法格式
- 1.2.2 具体举例
- 1.3 继承的相关特性(掌握)
- 1.4 对继承自Object类的方法的测试(理解)
- 1.5 难点解惑
- 1.5.1 掌握类和类继承之后的代码执行顺序
- 1.5.2 子类继承父类之后,能使用子类对象调用父类方法吗?
- 1.6 练习
- 1.6.1 第1题
- 1.6.2 第2题
- 2. 方法覆盖和多态(Polymorphism)
- 2.1 方法覆盖Override
- 2.1.1 什么时候需要方法覆盖(理解)
- 2.1.2 怎么进行方法覆盖(掌握)
- 2.1.3 方法覆盖的条件及注意事项(掌握)
- 2.1.4 方法覆盖的例子
- 2.1.5 方法覆盖总结
- 2.1.6 方法重载和方法覆盖(重写)的区别
- 2.2 多态
- 2.2.1 多态基础语法(掌握)
- 2.2.2 向下转型的风险以及`instanceof`
- 2.2.3 多态存在的条件以及静态方法为何不谈方法覆盖?
- 2.2.4 私有方法不能覆盖
- 2.2.5 多态在开发中的作用&开闭(OCP)原则(理解)
- 2.3 难点解惑
- 2.3.1 有了多态之后,方法覆盖的返回值是否可以不一样呢?
- 2.3.2 多态机制的理解
- 2.4 练习
- 2.4.1 实现愤怒的小鸟
- 2.4.2 计算不同类型的员工薪资
- 2.4.3 计算汽车租金
- 3.super
- 3.1 super概述(理解)
- 3.1.1 super不能单独使用
- 3.1.2 super不能使用在静态方法中
- 3.1.3 super使用在构造方法中(掌握)
- 3.1.3.1 super()的默认调用
- 3.1.3.2 父类的构造方法必被子类构造方法调用
- 3.1.3.3 一个 java 对象在创建过程中比较完整的内存图变化
- 3.1.3.4 super()作用的总结
- 3.1.4 super使用在实例方法中(掌握)
- 3.2 难点解惑
- 3.3 练习
1. 继承
1.1 继承概述(理解)
继承是面向对象三大特征之一,封装居首位,封装之后形成了独立体,独立体 A和独立体B 之间可能存在继承关系。其实程序中的继承灵感来自于现实生活,在现实生活中继承处处可见,如下:
继承时子类继承父类的特征和行为,使得子类对象(实例)具有父类的属性,或子类从父类继承方法,使得子类具有与父类相同的行为。 兔子和羊属于食草动物类,狮子和豹属于食肉动物类。食草动物和食肉动物又是属于动物类。所以继承需要符合的关系是:is-a(Rabbit is-a Animal)【符合这种关系的,就可以使用继承】,父类更通用,子类更具体。 虽然食草动物和食肉动物都是属于动物,但是两者的属性和行为上有差别,所以子类会具有父类的一般特性也会具有自身的特性。
为什么要使用继承机制?在不同的类中也可能会有共同的特征和动作,可以把这些共同的特征和动作放在一个类中,让其它类共享。因此可以定义一个通用类,然后将其扩展为其它多个特定类,这些特定类继承通用类中的特征和动作。继承是 Java 中实现软件重用的重要手段,避免重复,易于维护。
1.2 如何继承(掌握)
1.2.1 继承的语法格式
class 类名 extends 父类名{
类体;
}
1.2.2 具体举例
接下来用以下例子来说明一下为什么需要继承:
public class Account { //银行账户类
//账号
private String actno;
//余额
private double balance;
//账号和余额的 set 和 get 方法
public String getActno() {
return actno;
}
public void setActno(String actno) {
this.actno = actno;
}
public double getBalance() {
return balance;
}
public void setBalance(double balance) {
this.balance = balance;
}
}
public class CreditAccount { //信用账户类
//账号
private String actno;
//余额
private double balance;
//账号和余额的 set 和 get 方法
public String getActno() {
return actno;
}
public void setActno(String actno) {
this.actno = actno;
}
public double getBalance() {
return balance;
}
public void setBalance(double balance) {
this.balance = balance;
}
//信誉度(特有属性)
private double credit;
//信誉度的 set 和 get 方法
public double getCredit() {
return credit;
}
public void setCredit(double credit) {
this.credit = credit;
}
}
以上两个类分别描述了“银行账户类”和“信用账户类”,信用账户类除了具有银行账户类的特征之外,还有自己的特性,按照以上代码的编写方式,程序将会非常的臃肿,可以修改“信用账户类”继承“银行账户类”:
public class CreditAccount extends Account{ //信用账户类
//信誉度(特有属性)
private double credit;
//信誉度的 set 和 get 方法
public double getCredit() {
return credit;
}
public void setCredit(double credit) {
this.credit = credit;
}
}
public class AccountTest {
public static void main(String[] args) {
CreditAccount act = new CreditAccount();
act.setActno("111111111");
act.setBalance(9000.0);
act.setCredit(100.0);
System.out.println(act.getActno() + "信用账户,余额"
+ act.getBalance() + "元,"
+ "信誉度为" + act.getCredit());
}
}
运行结果:
通过以上的代码,可以看到继承可以解决代码臃肿的问题。换句话说,继承解决了代码复用的问题(代码复用就是代码的重复利用),这是继承机制最基本的作用。
除此之外,继承还有非常重要的两个作用,那就是有了继承之后才会衍生出方法的覆盖和多态机制。(后续学习)
1.3 继承的相关特性(掌握)
继承需要理解和记忆的特性:
- B类继承 A类,则称 A类为超类(superclass)、父类、基类,B类则称为子类(subclass)、派生类、扩展类。
- java 中的继承只支持单继承,不支持多继承,C++中支持多继承,这也是 java 体现简单性的一点,换句话说,java 中不允许这样写代码:class B extends A,C{ }。
- 虽然 java 中不支持多继承,但有的时候会产生间接继承的效果,例如:class C extends B,class B extends A,也就是说,C 直接继承 B,其实 C 还间接继承 A。
- java 中规定,子类继承父类,除构造方法不能继承、private修饰的数据无法在子类中直接访问外,剩下都可以继承。
- java 中的类没有显示的继承任何类,则默认继承 Object 类,Object 类是 java 语言提供的根类(老祖宗类,其没有父类),也就是说,一个对象与生俱来就有 Object 类型中所有的特征。
- 继承也存在一些缺点,例如:CreditAccount 类继承 Account 类会导致它们之间的耦合度非常高,Account 类发生改变之后会马上影响到CreditAccount 类。
1.4 对继承自Object类的方法的测试(理解)
查看一下Object类的部分源代码:
测试一下从Object类中继承过来的toString()
方法:
public class ExtendsTest{
public static void main(String[] args) {
ExtendsTest et = new ExtendsTest();
String s = et.toString();
System.out.println(s);
}
}
运行结果:
可以看到toString()
方法确实被 ExtendsTest 类继承过来了。
1.5 难点解惑
1.5.1 掌握类和类继承之后的代码执行顺序
请看如下代码:
public class Test {
public static void main(String[] args) {
new H2();
}
}
class H1{
public H1(){
System.out.println("父类构造");
}
static{
System.out.println("父类静态代码块");
}
{
System.out.println("父类代码块");
}
}
class H2 extends H1{
static{
System.out.println("子类静态代码块");
}
{
System.out.println("子类代码块");
}
public H2(){
System.out.println("子类构造");
}
}
运行结果:
分析:
子类 H2 继承 H1,new H2()执行的时候,会先进行类加载,先加载 H2 的父类 H1,所以 H1 当中的静态代码块先执行,然后再执行 H2 中的静态代码块;静态代码块执行结束之后,不会马上执行构造方法,代码块会先执行,Java 中有一条规则:子类构造方法执行前先执行父类的构造方法(学习 super 之后就知道了),所以父类 H1 的代码块先执行,再执行 H1 的构造方法,然后再执行 H2 的代码块,最后执行 H2 的构造方法。
1.5.2 子类继承父类之后,能使用子类对象调用父类方法吗?
答:这种问法本身就存在问题,本质上,子类继承父类之后,是相当于将从父类继承的方法自己复刻一份,归自己所有,实际上其调用的就是子类自己的方法,不是父类的。
1.6 练习
1.6.1 第1题
定义猴子类,猴子有名字和性别等属性,并且定义猴子说话的方法,定义人类,人有名字和性别等属性,并且定义人说话的方法。使用继承,让代码具有复用性。
代码:
class Monkey{
public String name;
public char sex;
public Monkey(){}
public Monkey(String name, char sex) {
this.name = name;
this.sex = sex;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public char getSex() {
return sex;
}
public void setSex(char sex) {
this.sex = sex;
}
public void talk(){
System.out.println(this.name + "在吼叫~");
}
}
class People01 extends Monkey{
public People01(String name, char sex) {
this.name = name;
this.sex = sex;
}
public void talk(){
System.out.println(this.name + "咿咿呀呀~");
}
}
public class InheritTest01 {
public static void main(String[] args) {
Monkey monkey = new Monkey("Dai", '母');
monkey.talk();
People01 people = new People01("Li", '女');
people.talk();
}
}
运行结果:
1.6.2 第2题
定义动物类,动物有名字属性,并且定义动物移动的方法,定义鱼类,鱼有名字属性,有颜色属性,并且定义鱼移动的方法。使用继承,让代码具有复用性。
代码:
class Animal{
public String name;
public Animal(){}
public Animal(String name) {
this.name = name;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public void move(){
System.out.println(this.name +"在移动~");
}
}
class Fish extends Animal{
public String color;
public Fish(String name, String color){
this.name = name;
this.color = color;
}
public void move(){
System.out.println(this.name +"在游动~");
}
}
public class InheritTest02 {
public static void main(String[] args) {
Animal animal = new Animal("蚂蚁");
animal.move();
Fish fish = new Fish("金鱼","橙色");
fish.move();
}
}
运行结果:
(以上父类空的构造方法是必须要的,否则会编译报错,这部分后续再学习)
2. 方法覆盖和多态(Polymorphism)
2.1 方法覆盖Override
2.1.1 什么时候需要方法覆盖(理解)
首先,先来回顾一下方法重载(overload):
- 什么情况下考虑使用方法重载呢?
在同一个类当中,如果功能相似,尽可能将方法名定义的相同,这样方便调用的同时代码也会美观。 - 代码满足什么条件的时候能够构成方法重载呢?
只要在同一个类当中,方法名相同,参数列表不同(类型、个数、顺序),即构成方法重载。
带着同样的疑问去学习方法覆盖,什么是方法覆盖?什么情况下考虑方法覆盖?代码怎么写的时候就构成了方法覆盖呢?接下来看一段代码:
public class People {
private String name;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public void speakHi(){
System.out.println(this.name + "和别人打招呼!");
}
}
public class ChinaPeople extends People {
//中国人
}
public class AmericaPeople extends People {
//美国人
}
public class PeopleTest {
public static void main(String[] args) {
ChinaPeople cp = new ChinaPeople();
cp.setName("张三");
cp.speakHi();
AmericaPeople ap = new AmericaPeople();
ap.setName("jackson");
ap.speakHi();
}
}
运行结果:
“中国人”调用 speakHi()方法希望输出的结果是“你好,我叫张三,很高兴见到你!”,而“美国人”调用 speakHi()方法更希望输出的结果是“Hi,My name is jackson,Nice to meet you!”,可见 ChinaPeople 和 AmericaPeople 从父类中继承过来的 speakHi()方法已经不够子类使用了,此时就需要使用方法覆盖机制了。
2.1.2 怎么进行方法覆盖(掌握)
接上情景,如何进行方法覆盖,如下代码【保持People类,PeopleTest测试类不变】,修改如下:
public class ChinaPeople extends People {
public void speakHi(){
System.out.println("你好,我叫"+this.getName()+",很高兴认识你!");
}
}
public class AmericaPeople extends People {
public void speakHi(){
System.out.println("Hi,My name is "+this.getName()+",Nice to meet you!");
}
}
修改后的运行结果:
以上程序中 ChinaPeople 和 AmericaPeople 将从 People 类中继承过来的 speakHi()方法进行了覆盖,我们也看到了当 speakHi()方法发生覆盖之后,子类对象会调用覆盖之后的方法,不会再去调用之前从父类中继承过来的方法。
在什么情况下我们会考虑使用方法覆盖呢?只有当从父类中继承过来的方法无法满足当前子类业务需求的时候,需要将父类中继承过来的方法进行覆盖。换句话说,父类中继承过来的方法已经不够用了,子类有必要将这个方法重新再写一遍,所以方法覆盖又被称为方法重写。当该方法被重写之后,子类对象一定会调用重写之后的方法。
2.1.3 方法覆盖的条件及注意事项(掌握)
当程序具备哪些条件的时候,就能构成方法覆盖呢?【记住】
① 首要条件:方法覆盖发生在具有继承关系的父子类之间;
② 覆盖之后的方法与原方法具有相同的返回值类型、相同的方法名、相同的形式参数列表;
另外,在使用方法覆盖的时候,需要有哪些注意事项呢?
① 由于覆盖之后的方法与原方法一模一样,建议在开发的时候采用复制粘贴的方式,不建议手写,因为手写的时候非常容易出错,比如在 Object 类当中有 toString()方法,该方法中的 S 是大写的,在手写的时候很容易写成小写 tostring(),这个时候你会认为 toString()方法已经被覆盖了,但由于方法名不一致,导致最终没有覆盖,这样就尴尬了;
② 私有的方法不能被继承,所以不能被覆盖;
③ 构造方法不能被继承,所以也不能被覆盖;
④ 覆盖之后的方法不能比原方法拥有更低的访问权限,可以更高(学习了访问控制权限修饰符之后就明白了);
⑤ 覆盖之后的方法不能比原方法抛出更多的异常,可以相同或更少(学习了异常之后就明白了);
⑥ 方法覆盖只是和方法有关,和属性无关;
⑦ 静态方法不存在覆盖(不是静态方法不能覆盖,是静态方法覆盖意义不大,学习了多态机制之后就明白了);
2.1.4 方法覆盖的例子
业务需求:定义一个动物类,所有动物都有移动的行为,其中猫类型的对象在移动的时候输出“猫在走猫步!”,鸟儿类型的对象在移动的时候输出“鸟儿在飞翔!”,但是猫类型的对象具有一个特殊的行为,抓老鼠,这个行为不是所有动物对象都有的,是猫类型对象特有的。
public class Animal {
public void move(){
System.out.println("动物在移动!");
}
}
public class Bird extends Animal {
//方法覆盖
public void move() {
System.out.println("鸟儿在飞翔!");
}
}
public class Cat extends Animal{
//方法覆盖
public void move() {
System.out.println("猫在走猫步!");
}
//子类特有
public void catchMouse(){
System.out.println("猫在抓老鼠!");
}
}
public class AnimalTest {
public static void main(String[] args) {
Cat cat = new Cat();
cat.move();
cat.catchMouse();
Bird bird = new Bird();
bird.move();
}
}
运行结果:
2.1.5 方法覆盖总结
- 当父类中继承过来的方法无法满足当前子类业务需求的时候,子类有必要将父类中继承过来的方法进行覆盖/重写。
- 方法覆盖发生在具有继承关系的父子类之间。
- 方法覆盖的时候要求相同的返回值类型、相同的方法名、相同的形式参数列表。方法覆盖之后子类对象在调用的时候一定会执行覆盖之后的方法。
- 纠正一个错误观念: 后续会学到的一个注解
@override
可用于标注于子类覆盖的方法之上,但是该注解并不是必须的(只是为了增强可读性),如上,没有该注解,只要满足了方法覆盖的条件,就仍然是方法覆盖。
2.1.6 方法重载和方法覆盖(重写)的区别
- 方法重载是发生在同一个类中;而方法覆盖发生在具有继承关系的父子类之间。
- 方法重载是要求方法名相同,参数列表不同;而方法覆盖要求重写的方法必须和父类的方法一致:方法名一致、参数列表一致、返回值类型一致。
2.2 多态
2.2.1 多态基础语法(掌握)
多态(Polymorphism)属于面向对象三大特征之一,它的前提是封装形成独立体,独立体之间存在继承关系,从而产生多态机制。多态是同一个行为具有多个不同表现形式或形态的能力。
比如我们按下 F1 键这个动作:
- 如果当前在 Flash 界面下弹出的就是 AS 3 的帮助文档;
- 如果当前在 Word 下弹出的就是 Word 帮助;
- 如果当前在 Windows 下弹出的就是 Windows 帮助和支持。
多态就是“同一个行为”发生在“不同的对象上”会产生不同的效果。
在 java 中允许这样的两种语法出现,一种是向上转型(Upcasting),一种是向下转型(Downcasting),向上转型是指子类型转换为父类型,又被称为自动类型转换,向下转型是指父类型转换为子类型,又被称为强制类型转换。
记住: 在 java 中,无论是向上转型还是向下转型,两种类型之间必须要有继承关系,没有继承关系情况下进行向上转型或向下转型的时候编译器都会报错。
看如下代码:
public class Animal {
public void move(){
System.out.println("Animal move!");
}
}
public class Cat extends Animal{
//方法覆盖
public void move(){
System.out.println("猫在走猫步!");
}
//子类特有
public void catchMouse(){
System.out.println("猫在抓老鼠!");
}
}
public class Bird extends Animal{
//方法覆盖
public void move(){
System.out.println("鸟儿在飞翔!");
}
//子类特有
public void sing(){
System.out.println("鸟儿在歌唱!");
}
}
public class Test01 {
public static void main(String[] args) {
//创建 Animal 对象
Animal a = new Animal();
a.move();
//创建 Cat 对象
Cat c = new Cat();
c.move();
//创建鸟儿对象
Bird b = new Bird();
b.move();
}
}
运行结果:
java中还允许这样写代码:
public class AnimalTest02 {
public static void main(String[] args) {
Animal a1 = new Cat();
a1.move();
Animal a2 = new Bird();
a2.move();
}
}
运行结果:
以上程序演示的就是多态,多态就是“同一个行为(move)”作用在“不同的对象上”会有不同的表现结果。
java 中之所以有多态机制,是因为 java 允许一个父类型的引用指向一个子类型的对象。也就是说允许这种写法:Animal a2 = new Bird()
,因为 Bird is a Animal 是能够说通的。其中Animal a1 = new Cat()
或者 Animal a2 = new Bird()
都是父类型引用指向了子类型对象,都属于向上转型(Upcasting),或者叫做自动类型转换。
分析以上代码片段中的Animal a1 = new Cat();a1.move();
:
java 程序包括编译和运行两个阶段,分析 java 程序一定要先分析编译阶段,然后再分析运行阶段。
- 在编译阶段,编译器只知道
a1
变量的数据类型是Animal
,那么此时编译器会去 Animal.class字节码中查找move()
方法,发现 Animal.class 字节码中存在move()
方法,然后将该move()
方法绑定到a1
引用上,编译通过了,这个过程我们可以理解为“静态绑定”阶段完成了。 - 紧接着程序开始运行,进入运行阶段,在运行的时候实际上在堆内存中new的对象是
Cat
类型,也就是说真正在 move 移动的时候,是Cat
猫对象在移动,所以运行的时候就会自动执行Cat
类当中的move()
方法,这个过程可以称为“动态绑定”。但无论是什么时候,必须先“静态绑定”成功之后才能进入“动态绑定”阶段。
简单来说,多态就是程序在编译时一种形态,在运行时又是另一种形态。
再看一下以下代码:
public class AnimalTest03 {
public static void main(String[] args) {
Animal a = new Cat();
a.catchMouse();
}
}
编译报错:
分析:因为Animal a = new Cat();
在编译的时候,编译器只知道 a
变量的数据类型是 Animal
,也就是说它只会去Animal.class 字节码中查找 catchMouse()
方法,结果没找到,自然“静态绑定”就失败了,编译没有通过。就像以上述的错误信息一样:在类型为 Animal
的变量 a
中找不到方法catchMouse()
。
可以修改如下:
public class AnimalTest04 {
public static void main(String[] args) {
//向上转型
Animal a = new Cat();
//向下转型:为了调用子类对象特有的方法
Cat c = (Cat)a;
c.catchMouse();
}
}
运行结果:
可以看到直接使用 a
引用是无法调用 catchMouse()
方法的,因为这个方法属于子类 Cat
中特有的行为,不是所有 Animal
动物都可以抓老鼠的,要想让它去抓老鼠,就必须做向下转型(Downcasting),也就是使用强制类型转换将 Animal
类型的 a
引用转换成 Cat
类型的引用c
(Cat c = (Cat)a;),使用 Cat
类型的 c
引用调用 catchMouse()
方法。
结论:只有在访问子类型中特有数据的时候,需要先进行向下转型。
2.2.2 向下转型的风险以及instanceof
向下转型存在的风险:
public class AnimalTest05 {
public static void main(String[] args) {
Animal a = new Bird();
Cat c = (Cat)a;
}
}
以上编译可以通过,但是运行报错:
分析:因为编译器只知道 a
变量是Animal
类型,Animal
类和Cat
类之间存在继承关系,所以可以进行向下转型(前面提到过,只要两种类型之间存在继承关系,就可以进行向上或向下转型),语法上没有错误,所以编译通过了。但是运行的时候会出问题吗,因为毕竟a
引用指向的真实对象是一只小鸟。【即,是运行阶段的“动态绑定”出现了问题!产生的ClassCastException
是运行时异常(类型转换异常),需要记住这个异常。】
为了避免这种异常的发生,建议在进行向下转型之前进行运行期类型判断,这就需要我们学习一个运算符了,它就是 instanceof
,其语法格式如下:
(引用 instanceof 类型)
举例:
public class Test05 {
public static void main(String[] args) {
Animal a = new Bird();
if(a instanceof Cat){
Cat c = (Cat)a;
c.catchMouse();
}else if(a instanceof Bird){
Bird b = (Bird)a;
b.sing();
}
}
}
运行结果:
在实际开发中,java 中有这样一条默认的规范需要大家记住:在进行任何向下转型的操作之前,要使用 instanceof
进行判断,请保持这个良好的编程习惯。
2.2.3 多态存在的条件以及静态方法为何不谈方法覆盖?
多态存在的三个必要条件:
① 继承
② 方法覆盖
③ 父类型引用指向子类型对象
多态显然是离不开方法覆盖机制的,多态就是因为编译阶段绑定父类当中的方法,程序运行阶段自动调用子类对象上的方法,如果子类对象上的方法没有进行重写,这个时候创建子类对象就没有意义了,自然多态也就没有意义了,只有子类将方法重写之后调用到子类对象上的方法产生不同效果时,多态就形成了。实际上方法覆盖机制和多态机制是捆绑的,谁也离不开谁,多态离不开方法覆盖,方法覆盖离开了多态也就没有意义了。
再看看为何有之前的:方法覆盖主要是说实例方法,静态方法为何不谈方法覆盖?
先看看如下的例子:
public class OverrideTest {
public static void main(String[] args) {
Math.sum();
MathSubClass.sum();
}
}
class Math{
public static void sum(){
System.out.println("Math's sum execute!");
}
}
class MathSubClass extends Math{
//尝试覆盖从父类中继承过来的静态方法
public static void sum(){
System.out.println("MathSubClass's sum execute!");
}
}
运行结果:
貌似上面的代码也发生了覆盖,在程序运行的时候确实也调用了“子类MathSubClass”的sum方法,但这种“覆盖”有意义吗?其实前面已经学习过:方法覆盖和多态机制联合起来才有意义,我们来看看这种“覆盖”是否能够达到“多态”的效果,请看代码:
public class OverrideTest {
public static void main(String[] args) {
Math m = new MathSubClass();
m.sum();
m = null;
m.sum();
}
}
运行结果:
通过以上的代码,我们发现虽然创建了子类型对象new MathSubClass()
,但是程序在运行的时候仍然调用的是 Math 类当中的 sum 方法,甚至 m = null
的时候再去调用 m.sum()
也没有出现空指针异常,这说明静态方法的执行压根和对象无关,既然和对象无关那就表示和多态无关,既然和多态无关,也就是说静态方法的“覆盖”是没有意义的,所以通常我们不谈静态方法的覆盖。
2.2.4 私有方法不能覆盖
举例如下:
public class OverrideTest02 {
// 私有方法
private void doSome(){
System.out.println("OverrideTest02's private method doSome execute! ");
}
public static void main(String[] args) {
//多态
OverrideTest02 ot = new T();
ot.doSome();
}
}
class T extends OverrideTest02{
//尝试重写父类中的doSome()方法
//访问权限不能更低,可以更高
public void doSome(){
System.out.println("T's doSome execute!");
}
}
运行结果:
2.2.5 多态在开发中的作用&开闭(OCP)原则(理解)
先来了解一个业务背景:请设计一个系统,描述主人喂养宠物的场景,首先在这个场景当中应该有“宠物对象”,宠物对象应该有一个吃的行为,另外还需要一个“主人对象”,主人对象应该有一个喂的行为,请看代码:
//宠物狗
public class Dog {
String name;
public Dog(String name){
this.name = name;
}
//吃的行为
public void eat(){
System.out.println(this.name + "在啃肉骨头!");
}
}
//主人
public class Master {
//喂养行为
public void feed(Dog dog){
//主人喂养宠物,宠物就吃
System.out.println("主人开始喂食儿");
dog.eat();
System.out.println("主人喂食儿完毕");
}
}
public class Test {
public static void main(String[] args) {
//创建狗对象
Dog dog = new Dog("二哈");
//创建主人对象
Master master = new Master();
//喂养
master.feed(dog);
}
}
运行结果:
以上程序编译和运行都很正常,输出结果也是对的,那么存在什么问题吗?假设后期用户出了新的需求,软件可能面临着功能扩展,这个扩展会很方便吗?假设现在主人家里又来了一个宠物猫,那该怎么办呢?新增了一个 Cat 类,来表示宠物猫,请看代码:
//宠物猫
public class Cat {
String name;
public Cat(String name){
this.name = name;
}
//吃的行为
public void eat(){
System.out.println(this.name + "在吃鱼!");
}
}
除了增加一个 Cat 类之外,我们还需要“修改”Master 主人类的源代码,这件事儿
是我们程序员无法容忍的,因为修改之前写好的源代码就面临着重新编译、重新全方位的测试,这是一个巨大的工作,维护成本很高,也很麻烦:
//主人
public class Master {
//喂养行为
public void feed(Dog dog){
//主人喂养宠物,宠物就吃
System.out.println("主人开始喂食儿");
dog.eat();
System.out.println("主人喂食儿完毕");
}
//喂养行为
public void feed(Cat cat){
//主人喂养宠物,宠物就吃
System.out.println("主人开始喂食儿");
cat.eat();
System.out.println("主人喂食儿完毕");
}
}
public class Test {
public static void main(String[] args) {
//创建狗对象
Dog dog = new Dog("二哈");
//创建主人对象
Master master = new Master();
//喂养
master.feed(dog);
//创建猫对象
Cat cat = new Cat("汤姆");
//喂养
master.feed(cat);
}
}
运行结果:
在软件开发过程中,有这样的一个开发原则:开闭原则。开闭原则(OCP)是面向对象设计中“可复用设计”的基石,是面向对象设计中最重要的原则之一,其它很多的设计原则都是实现开闭原则的一种手段。它的原文是这样:“Software entities should be open for extension,but closed for modification”。翻译过来就是:“软件实体应当对扩展开放,对修改关闭”。把它讲得更通俗一点,也就是:软件系统中包含的各种组件,例如模块(Modules)、类(Classes)以及功能(Functions)等等,应该在不修改现有代码的基础上,引入新功能。 开闭原则中“开”,是指对于组件功能的扩展是开放的,是允许对其进行功能扩展的;开闭原则中“闭”,是指对于原有代码的修改是封闭的,即修改原有的代码对外部的使用是透明的。
以上程序在扩展的过程当中就违背了 OCP 原则,因为在扩展的过程当中修改了已经写好的 Master 类,怎样可以解决这个问题呢?多态可以解决,请看代码:
//宠物类
public class Pet {
String name;
//吃的行为
public void eat(){
}
}
//宠物猫
public class Cat extends Pet{
public Cat(String name){
this.name = name;
}
//吃的行为
public void eat(){
System.out.println(this.name + "在吃鱼!");
}
}
//宠物狗
public class Dog extends Pet{
public Dog(String name){
this.name = name;
}
//吃的行为
public void eat(){
System.out.println(this.name + "在啃肉骨头!");
}
}
//主人
public class Master {
//喂养行为
public void feed(Pet pet){
//主人喂养宠物,宠物就吃
System.out.println("主人开始喂食儿");
pet.eat();
System.out.println("主人喂食儿完毕");
}
}
public class Test02 {
public static void main(String[] args) {
//创建狗对象
Dog dog = new Dog("二哈");
//创建主人对象
Master master = new Master();
//喂养
master.feed(dog);
//创建猫对象
Cat cat = new Cat("汤姆");
//喂养
master.feed(cat);
}
}
运行结果:
如上使用到多态的部分:
在以上程序中,Master 类中的方法 feed(Pet pet)的参数类型定义为更加抽象的 Pet 类型,而不是具体 Dog 宠物,或者 Cat 宠物,显然 Master 类和具体的 Dog、Cat 类解耦合了,依赖性弱了,这就是我们通常所说的面向抽象编程,尽量不要面向具体编程,面向抽象编程会让你的代码耦合度降低,扩展能力增强,从而符合 OCP 的开发原则 。假如说这会再来一个新的宠物鸟呢,我们只需要这样做,新增加一个“宠物鸟类”,然后宠物鸟类 Bird 继承宠物类 Pet,并重写 eat()方法,然后修改一下测试类就行了,整个过程我们是不需要修改 Master 类的,只是额外增加了一个新的类。
总结一下多态的作用:
多态在开发中联合方法覆盖一起使用,可以降低程序的耦合度,提高程序的扩展力。在开发中尽可能面向抽象编程,不要面向具体编程。
好比电脑主板和内存条的关系一样,主板和内存条件之间有一个抽象的符合某个规范的插槽,不同品牌的内存条都可以插到主板上使用,2 个 G 的内存条和 4 个 G 的内存条都可以插上,但最终的表现结果是不同的,2 个 G 的内存条处理速度慢一些,4 个 G 的快一些,这就是多态,所谓多态就是同一个行为作用到不同的对象上,最终的表现结果是不同的,主要的要求就是对象是可以进行灵活切换的,灵活切换的前提就是解耦合,解耦合依赖多态机制。
2.3 难点解惑
2.3.1 有了多态之后,方法覆盖的返回值是否可以不一样呢?
经过测试,结果如下:
- 对于返回值是基本数据类型的,子类覆盖的方法的返回值必须与父类方法一致;
- 对于返回值是引用数据类型的,子类覆盖的方法的返回值可以更小(比如换成其子类本身),但是意义不大,在开发中一般不会修改。
2.3.2 多态机制的理解
多态的代码表现是父类型引用指向子类型对象,对于多态的理解一定要分为编译阶段和运行阶段来进行分析:
- 编译阶段只是看父类型中是否存在要调用的方法,如果父类中不存在,则编译器会报错;
- 编译阶段和具体 new 的对象无关。但是在运行阶段就要看底层具体 new 的是哪个类型的子对象了,new 的这个子类型对象可以看做“真实对象”,自然在运行阶段就会调用真实对象的相关方法。
例如代码:
Animal a = new Cat();
a.move();
在编译阶段编译器只能检测到a的类型是Animal,所以一定会去Animal类中找move()方法,如果 Animal 中没有 move()方法,则编译器会报错,即使 Cat 中有 move()方法,也会报错,因为编译器只知道 a 的类型是 Animal 类,只有在运行的时候,实际创建的真实对象是 Cat,那么在运行的时候就会自动调用 Cat 对象的 move()方法。这样就可以达到多种形态,也就是说编译阶段一种形态,运行阶段的时候是另一种形态。这也就是多态的含义。
2.4 练习
2.4.1 实现愤怒的小鸟
我们有很多种小鸟,每种小鸟都有飞的行为,还有一个弹弓,弹弓有一个弹射的行为,弹射时把小鸟弹出去,之后小鸟使用自己飞行的行为飞向小猪(不要求编写小猪的代码)。
不同种类的小鸟有不同飞行的方式:
1)红火:红色小鸟,飞行方式:正常;
2)蓝冰:蓝色小鸟,飞行方式:分成 3 个;
3)黄风:黄色小鸟,飞行方式:加速。
代码如下:
public class Bird {
public void fly(){
System.out.println("小鸟在飞翔!");
}
}
public class RedBird extends Bird{
public void fly(){
System.out.println("红色小鸟正常飞翔!");
}
}
public class BlueBird extends Bird {
public void fly(){
System.out.println("蓝色小鸟分成三个飞翔!");
}
}
public class YellowBird extends Bird{
public void fly(){
System.out.println("黄色小鸟加速飞翔!");
}
}
public class Slingshot {
public void shot(Bird bird){
bird.fly();
}
}
public class Test {
public static void main(String[] args) {
Slingshot slingshot = new Slingshot();
Bird redBird = new RedBird();
Bird blueBird = new BlueBird();
Bird yellowBird = new YellowBird();
slingshot.shot(redBird);
slingshot.shot(blueBird);
slingshot.shot(yellowBird);
}
}
运行结果:
2.4.2 计算不同类型的员工薪资
1)定义员工类 Employee,员工包含姓名 name、出生月份 birthMonth 两个属性,员工有获取指定月份工资的方法(getSalary(int month)),如果该月员工生日,公司补助 250 元。
2)定义有固定工资的员工类 SalariedEmployee,有月薪 monthlySalary 属性。
3)定义小时工类 HourlyEmployee,包含工作小时数 hours 和每小时的工资hourlySalary 属性,如果每月工作超过 160 小时,超过的部分按 1.5 倍工资发放。
4)定义销售人员类 SalesEmployee,包含月销售额 sales 和提成比例 comm 属性。
代码如下:
/**
* 员工类
*/
public class Employee {
String name; //姓名
int birthMonth; // 出生月份
public Employee(String name, int birthMonth) {
this.name = name;
this.birthMonth = birthMonth;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getBirthMonth() {
return birthMonth;
}
public void setBirthMonth(int birthMonth) {
this.birthMonth = birthMonth;
}
public double getSalary(int month){
return 0;
}
}
/**
* 固定工资的员工类
*/
public class SalariedEmployee extends Employee{
double monthlySalary; //月薪
public SalariedEmployee(String name, int birthMonth, double monthlySalary) {
super(name, birthMonth);
this.monthlySalary = monthlySalary;
}
public double getMonthlySalary() {
return monthlySalary;
}
public void setMonthlySalary(double monthlySalary) {
this.monthlySalary = monthlySalary;
}
@Override
public double getSalary(int month){
if(birthMonth == month){
return getMonthlySalary() + 250;
}
return getMonthlySalary();
}
}
/**
* 小时工类
*/
public class HourlyEmployee extends Employee{
double hours; //工作小时数
double hourlyWage; //每小时工资
public HourlyEmployee(String name, int birthMonth, double hours, double hourlyWage) {
super(name, birthMonth);
this.hours = hours;
this.hourlyWage = hourlyWage;
}
public double getHours() {
return hours;
}
public void setHours(double hours) {
this.hours = hours;
}
public double getHourlyWage() {
return hourlyWage;
}
public void setHourlyWage(double hourlyWage) {
this.hourlyWage = hourlyWage;
}
@Override
public double getSalary(int month) {
if(birthMonth == month && hours > 160){
return hourlyWage*160 + hourlyWage*1.5*(hours-160)+250;
}else if(birthMonth == month){
return hourlyWage*hours + 250;
}else if(hours > 160){
return hourlyWage*160 + hourlyWage*1.5*(hours-160);
}
return hourlyWage*hours;
}
}
/**
* 销售人员类
*/
public class SalesEmployee extends Employee {
int sales; //月销售额
double comm; //提成比例
public SalesEmployee(String name, int birthMonth, int sales, double comm) {
super(name, birthMonth);
this.sales = sales;
this.comm = comm;
}
public int getSales() {
return sales;
}
public void setSales(int sales) {
this.sales = sales;
}
public double getComm() {
return comm;
}
public void setComm(double comm) {
this.comm = comm;
}
@Override
public double getSalary(int month){
if(birthMonth == month){
return comm*sales + 250;
}
return comm*sales;
}
}
public class Test {
public static void main(String[] args) {
Employee salariedEmployee = new SalariedEmployee("杏子", 2, 2500);
Employee hourlyEmployee = new HourlyEmployee("栗子", 3, 240, 15);
Employee salesEmplyee = new SalesEmployee("李子", 4, 20, 200);
double salariedSalary = salariedEmployee.getSalary(2);
double hourlySalary = hourlyEmployee.getSalary(2);
double saleSalary = salesEmplyee.getSalary(2);
System.out.println(salariedEmployee.getName() + "的月工资:" + salariedSalary);
System.out.println(hourlyEmployee.getName() + "的月工资:" + hourlySalary);
System.out.println(salesEmplyee.getName() + "的月工资:" + saleSalary);
}
}
运行结果:
2.4.3 计算汽车租金
某汽车租赁公司有多种汽车可以出租,计算汽车租金:
1)Vehicle 是所有车的父类,属性:品牌、车牌号,有返回总租金的方法:public double getSumRent(int days){}
2)小轿车类 Car 是 Vehicle 的子类,属性:车型(两厢、三厢、越野),两厢每天 300,三厢每天 350,越野每天 500。
3)多座汽车类 Bus 是 Vehicle 的子类,属性:座位数,座位数<=16 的每天 400,座位数>16的每天 600。
4)编写测试类,根据用户选择不同的汽车,计算总租金。
代码如下:
/**
* 车类
*/
public class Vehicle {
String band; //品牌
int carNumber ; //车牌号
public Vehicle(String band, int carNumber) {
this.band = band;
this.carNumber = carNumber;
}
public String getBand() {
return band;
}
public void setBand(String band) {
this.band = band;
}
public int getCarNumber() {
return carNumber;
}
public void setCarNumber(int carNumber) {
this.carNumber = carNumber;
}
public double getSumRent(int days){
return 0.0;
}
}
/**
* 小轿车类
*/
public class Car extends Vehicle {
String type; //车型
public Car(String band, int carNumber, String type) {
super(band, carNumber);
this.type = type;
}
public String getType() {
return type;
}
public void setType(String type) {
this.type = type;
}
@Override
public double getSumRent(int days) {
if(getType() == "两厢"){
return 300*days;
}else if(getType() == "三厢"){
return 350*days;
}
return 500*days; //越野
}
}
/**
* 多座汽车类
*/
public class Bus extends Vehicle{
int seatsNumber; //座位数
public Bus(String band, int carNumber, int seatsNumber) {
super(band, carNumber);
this.seatsNumber = seatsNumber;
}
public int getSeatsNumber() {
return seatsNumber;
}
public void setSeatsNumber(int seatsNumber) {
this.seatsNumber = seatsNumber;
}
@Override
public double getSumRent(int days) {
if(getSeatsNumber() <= 16){
return 400*days;
}
return 600*days;
}
}
public class Test {
public static void main(String[] args) {
Vehicle car = new Car("奔驰", 232423,"两厢");
Vehicle bus = new Bus("大众", 343525, 17);
int rentDays = 20;
System.out.println(car.getBand() + car.getCarNumber() + "租" + rentDays + "天的总租金" + car.getSumRent(rentDays));
System.out.println(bus.getBand() + bus.getCarNumber() + "租" + rentDays + "天的总租金" + bus.getSumRent(rentDays));
}
}
运行结果:
3.super
3.1 super概述(理解)
this 和 super 对比:
- this
- this 是一个引用,保存内存地址指向自己。
- this 出现在实例方法中,谁调用这个实例方法,this 就代表谁,this 代表当前正在执行这个动作的对象。
- this 不能出现在静态方法中。
- this 大部分情况下可以省略,在方法中区分实例变量和局部变量的时候不能省略。
- “this(实际参数列表)”出现在构造方法第一行,通过当前的构造方法去调用
本类当中其它的构造方法。
- super
- 严格来说,super 其实并不是一个引用,它只是一个关键字,super 代表了当前对象中从父类继承过来的那部分特征。 this 指向一个独立的对象,super 并不是指向某个“独立”的对象,假设张大明是父亲,张小明是儿子,有这样一句话:大家都说张小明的眼睛、鼻子和父亲的很像。那么也就是说儿子继承了父亲的眼睛和鼻子特征,那么眼睛和鼻子肯定最终还是长在儿子的身上。假设this指向张小明,那么 super 就代表张小明身上的眼睛和鼻子。换句话说 super 其实是 this 的一部分。如下图所示:张大明和张小明其实是两个独立的对象,两个对象内存方面没有联系,super 只是代表张小明对象身上的眼睛和鼻子,因为这个是从父类中继承过来的,在内存方面使用了 super 关键字进行了标记,对于下图来说“this.眼睛”和“super.眼睛”都是访问的同一块内存空间。
- super 和 this 都可以使用在实例方法当中。
- super 不能使用在静态方法当中,因为 super 代表了当前对象上的父类型特征,静态方法中没有 this,肯定也是不能使用 super 的。
- super 也有这种用法:“super(实际参数列表);”,这种用法是通过当前的构造
方法调用父类的构造方法。
3.1.1 super不能单独使用
如下代码:
public class SuperTest01 extends Object{
//实例方法
public void doSome(){
System.out.println(this);
System.out.println(super);
}
}
编译报错:
通过以上的测试,可以看出 this
是可以单独使用的引用,但 super
无法输出,编译器提示super 要使用必须是super.xxx
,显然 super
并不指向独立的对象,并不是保存某个对象的内存地址。
3.1.2 super不能使用在静态方法中
如下代码:
public class SuperTest01 extends Object{
//静态方法
public static void doSome(){
System.out.println(this);
System.out.println(super.toString());
}
}
编译报错:
通过以上的测试,可以看出 this
和 super
都是无法使用在静态方法当中的。
3.1.3 super使用在构造方法中(掌握)
super 使用在构造方法中,语法格式为:super(实际参数列表)
,这行代码和this(实际参数列表)
都是只允许出现在构造方法第一行 【记住!】 ,所以这两行代码是无法共存的 。
super(实际参数列表)
表示子类构造方法执行过程中调用父类的构造方法。
如下代码:
public class People {
String idCard;
String name;
boolean sex;
public People(){
}
public People(String idCard,String name,boolean sex){
this.idCard = idCard;
this.name = name;
this.sex = sex;
}
}
public class Student extends People {
//学号是子类特有的
int sno;
public Student(){
}
public Student(String idCard,String name,boolean sex,int sno){
this.idCard = idCard;
this.name = name;
this.sex = sex;
this.sno = sno;
}
}
public class StudentTest {
public static void main(String[] args) {
Student s = new Student("12345x","jack",true,100);
System.out.println("身份证号" + s.idCard);
System.out.println("姓名" + s.name);
System.out.println("性别" + s.sex);
System.out.println("学号" + s.sno);
}
}
运行结果:
观察以上子类和父类的有参构造方法:
可以发现子类的构造方法前三行代码和父类构造方法中的代码是一样的,接下来把子类的构造方法修改如下,然后再运行测试程序运行结果与上一致:
public Student(String idCard,String name,boolean sex,int sno){
super(idCard, name, sex);
this.sno = sno;
}
注意: 若此处父类People中三个属性均为私有的,则子类直接使用super.属性名
调用是不行的,私有属性只能在本类中访问,即使是子类也不能直接访问。
这时,使用super(idCard,name,sex)
就非常有必要了,解决了这一问题。
总结:通过以上学习,super(实际参数列表);
语法表示调用父类的构造方法,代码复用性增强了,另外一方面也是模拟现实世界当中的“要想有儿子,必须先有父亲”的道理。不过这里的super(实际参数列表)
在调用父类构造方法的时候,从本质上来说并不是创建一个“独立的父类对象”,而是为了完成当前对象的父类型特征的初始化操作。(或者说通过子类的构造方法调用父类的构造方法,是为了让张小明身上长出具有他父亲特点的鼻子和眼睛,鼻子和眼睛初始化完毕之后,具有父亲的特点,但最终还是长在张小明的身上)
3.1.3.1 super()的默认调用
如下代码:
public class A {
public A(){
System.out.println("A 类的无参数构造方法执行");
}
}
public class B extends A {
public B(){
System.out.println("B 类的无参数构造方法执行");
}
}
public class C extends B{
public C(){
System.out.println("C 类的无参数构造方法执行");
}
}
public class Test {
public static void main(String[] args) {
new C();
}
}
运行结果:
等效代码:
public class A {
public A(){
//这里调用的是 Object 类中的无参数构造方法
//因为 A类的父类是 Object
super();
System.out.println("A 类的无参数构造方法执行");
}
}
public class B extends A {
public B(){
super();
System.out.println("B 类的无参数构造方法执行");
}
}
public class C extends B {
public C(){
super();
System.out.println("C 类的无参数构造方法执行");
}
}
结论:当一个构造方法第一行没有显示的调用super(实际参数列表)
的话,系统默认调用父类的无参数构造方法super()
。当然前提是this(实际参数列表)
也没有显示的去调用(因为 super()
和 this()
都只能出现在构造方法第一行,所以不能并存)。
再测试一下如下代码:
public class A {
//有参数构造方法定义之后
//系统则不再ᨀ供无参数构造方法
public A(String s){
}
}
public class B extends A {
public B(){
}
}
编译报错:
原因:B 类的构造方法第一行默认会调用super()
,而super()
会调用父类 A 的无参数构造方法,但由于父类 A 中提供了有参数构造方法,导致无参数构造方法不存在,从而编译报错了。所以在实际开发中还是建议程序员将无参数构造方法显示的定义出来,这样就可以避免对象的创建失败了。
另外,通过以上内容的学习,还有如下结论:在 java 语言当中无论是创建哪个 java对象,老祖宗 Object 类中的无参数构造方法是必然执行的。
一个重要结论:
当一个构造方法中的第一行既没有this()
又没有super()
的话,默认会有一个super()
表示通过当前子类的构造方法调用父类的午餐构造方法,所以必须保证父类的无参构造方法是存在的。
3.1.3.2 父类的构造方法必被子类构造方法调用
如下代码:
public class People {
String name;
String sex;
public People(String name, String sex){
this.name = name;
this.sex = sex;
}
}
public class Student extends People{
String id;
public Student(String name, String sex) {
this.name = name;
this.sex = sex;
}
public Student(String id, String name, String sex){
this(name,sex);
this.id = id;
}
}
编译报错:
结论:无论如何,父类的构造方法一定会执行,如果不在子类构造方法中显式调用父类有参构造,即使是在子类中调用本类中有参构造,该有参构造内也默认会调用父类的无参构造。
3.1.3.3 一个 java 对象在创建过程中比较完整的内存图变化
如下代码:
public class People {
String name;
boolean sex;
public People(String name, boolean sex){
this.name = name;
this.sex = sex;
}
}
public class Worker extends People{
double salary;
public Worker(String name, boolean sex, double salary){
super(name,sex);
this.salary = salary;
}
}
public class WorkerTest {
public static void main(String[] args) {
Worker worker = new Worker("lili",true, 20000);
System.out.println("姓名:" + worker.name);
System.out.println("性别:" + worker.sex);
System.out.println("工资:" + worker.salary);
}
}
运行结果:
以上程序Worker对象创建时构造方法的执行顺序:
- 先执行 Object 类的无参数构造方法;
- 再执行 People 类的构造方法;
- 最后执行 Worker 类的构造方法;
注意:虽然执行了三个构造方法,但是对象实际上只创建了一个 Worker。
以上程序的内存结构图变化如下:
3.1.3.4 super()作用的总结
- 调用父类的构造方法,使用这个构造方法来给当前子类对象初始化父类型特征;
- 代码复用。
3.1.4 super使用在实例方法中(掌握)
如下代码:
//书
public class Book {
//书名
String name;
//构造方法
public Book(){
super();
}
public Book(String name){
super();
this.name = name;
}
}
//纸质书
public class PaperBook extends Book {
//构造方法
public PaperBook(){
super();
}
public PaperBook(String name){
super();
this.name = name;
}
//打印书名
public void printName(){
System.out.println("this.name->书名 : " + this.name);
System.out.println("super.name->书名 : " + super.name);
}
}
public class BookTest {
public static void main(String[] args) {
PaperBook book1 = new PaperBook("零基础学 Java 卷 I");
book1.printName();
}
}
运行结果:
由以上代码发现:printName()
方法中的 super.name
和 this.name
最终输出结果是一样的,以上程序执行的内存图如下:
1)父类构造方法执行结束后的内存图
2)子类构造方法执行结束后的内存图
通过以上内存结构图发现 this.name
和 super.name
实际上是同一块内存空间,所以它们的输出结果是完全一样的。
修改一下PaperBook
类:
//纸质书
public class PaperBook extends Book {
String name; //在子类中也定义了一个 name 属性
//构造方法
public PaperBook(){
super();
}
public PaperBook(String name){
super();
this.name = name;//这里的 this.name 代表子类的 name
}
//打印书名
public void printName() {
System.out.println("this.name->书名 : " + this.name);
System.out.println("super.name->书名 : " + super.name);
}
}
运行结果:
再看一下以上程序的内存图:
1)父类构造方法执行结束后的内存图
2)子类构造方法执行结束后的内存图
可以发现:父类Book
的构造方法在执行时给 super.name
赋值null
,子类PaperBook
的构造方法在执行的时候给 this.name
赋值“零基础学 Java 卷 I”,由于在子类 PaperBook
中定义了重名的变量 name
导致在当前对象中有两个name
,一个是从父类中继承过来的,一个是自己的,如果此时想访问父类中继承过来的 name
则必须使用 super.name
,当直接访问 name
或者 this.name
都表示访问当前对象自己的 name
。
结论:当父类中有该实例变量,子类中又重新定义了同名的实例变量,如果想在子类中访问父类的实例变量,super 不能省略。
再测试一下实例方法:
package SuperTest.superTest07;
public class Vip {
//Vip 默认继承 Object
//重写从 Object 类中继承过来的 toString()方法
public String toString(){
return "我是超级会员";
}
public void test() {
System.out.println(super.toString());
System.out.println(this.toString());
System.out.println(toString());
}
}
package SuperTest.superTest07;
public class VipTest {
public static void main(String[] args) {
Vip vip = new Vip();
vip.test();
}
}
运行结果:
由上代码发现在实例方法中也是如此。
最终结论:父类和子类中有同名实例变量或者有同名的实例方法,想在子类中访问父类中的实例变量或实例方法,则super 是不能省略的,其它情况都可以省略。
3.2 难点解惑
Java 中 super 存储的是一个父对象的内存地址吗?this 保存了内存地址指向了当前对象,那么 super也是保存了内存地址指向了当前对象的父对象吗?
这个理解是错误的,在 Java 程序中创建 Java 对象的时候会调用构造方法,在构造方法执行之前会先调用父类的构造方法,在这里说明一下,调用父类的构造方法实际上并不是创建父类对象,只是为了完成初始化当前子类对象的父类型特征。所以严格意义上来说 super 并不指向任何对象,super 只是代表了当前对象中的那部分父类型特征,单独输出 super,例如System.out.println(super);
是无法编译的。
3.3 练习
public class Text {
public static int k = 0;
public static Text t1 = new Text("t1");
public static Text t2 = new Text("t2");
public static int i = print("i");
public static int n = 99;
public int j = print("j");
static {
print("静态块");
}
public Text(String str) {
System.out.println((++k) + ":" + str + " i=" + i + " n=" + n);
++i;
++n;
}
public static int print(String str) {
System.out.println((++k) + ":" + str + " i=" + i + " n=" + n);
++n;
return ++i;
}
public static void main(String args[]) {
new Text("init");
}
}
程序加载过程总结:
- 方法区进行类加载,加载所需类的.class文件;
- 根据静态变量和静态代码块出现的顺序:若静态代码块在前,执行静态代码块中的内容,后统一声明静态变量并给静态变量赋默认值,再统一进行显式赋值;若为静态变量在前,先统一声明静态变量并给静态变量赋默认值,再统一进行显式赋值,然后执行静态代码块中的内容;
- 若有new 对象,每new一次,以下过程走一遍:
3.1. 声明实例变量并进行默认初始化;
3.2. 实例变量的显示赋值;
3.3. 执行构造代码块;
3.4. 执行构造方法;
如果遇到extends,要记住,先初始化父类数据【父类也是按以上过程初始化】,然后初始化子类数据。如下图:
根据以上分析:其实以上程序等价于
package SuperTest;
public class Text {
public static int k ;
public static Text t1;
public static Text t2;
public static int i;
public static int n;
public int j = print("j");
static {
k = 0;
t1 = new Text("t1");
t2 = new Text("t2");
i = print("i");
n = 99;
print("静态块");
}
public Text(String str) {
System.out.println((++k) + ":" + str + " i=" + i + " n=" + n);
++i;
++n;
}
public static int print(String str) {
System.out.println((++k) + ":" + str + " i=" + i + " n=" + n);
++n;
return ++i;
}
public static void main(String args[]) {
new Text("init");
}
}
则代码运行过程详解如下:
- 首先进行类加载,需要将Text类、Object类、String类等要使用的类由JVM加载到其方法区中,JVM加载Text类根据顺序需要先声明静态变量,此时,k=0,t1=null,t2=null,i=0,n=0;
- 执行静态代码块中的代码,k=0,而后触发t1的实例化【声明实例变量j,并为其赋初值j=0,执行构造方法:进入print(“j”),输出:1:j i=0 n=0,后k=1,n=1,i=1,返回j=1,而后输出2:t1 i=1 n=1,后k=2,i=2,n=2,t1实例化完成】
- 触发t2的实例化【声明实例变量j,并为其赋初值j=0,执行构造方法:进入print(“j”),输出:3:j i=2 n=2,后k=3,n=3,i=3,返回j=3,而后输出4:t2 i=3 n=3,后k=4,i=4,n=4,t2实例化完成】
- 为i显示赋值,进入print(“i”),输出:5:i i=4 n=4,而后k=5,n=5,i=5,返回i=5
- 为n赋值99
- 执行print(“静态块”),输出:6:静态块 i=5 n=99,而后k=6,n=100,i=6
- 进行new Text(“init”)【声明实例变量j,并为其赋初值j=0,执行构造方法:进入print(“j”),输出:7:j i=6 n=100,后k=7,n=101,i=7,而后输出8:init i=7 n=101,后k=8,i=8,n=102,实例化完成】
- main执行结束。
所以,代码最终运行结果为: