JavaSE-03笔记【继承~super】

文章目录

  • 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 继承的相关特性(掌握)

继承需要理解和记忆的特性:

  1. B类继承 A类,则称 A类为超类(superclass)、父类、基类,B类则称为子类(subclass)、派生类、扩展类。
  2. java 中的继承只支持单继承,不支持多继承,C++中支持多继承,这也是 java 体现简单性的一点,换句话说,java 中不允许这样写代码:class B extends A,C{ }。
  3. 虽然 java 中不支持多继承,但有的时候会产生间接继承的效果,例如:class C extends B,class B extends A,也就是说,C 直接继承 B,其实 C 还间接继承 A。
  4. java 中规定,子类继承父类,除构造方法不能继承、private修饰的数据无法在子类中直接访问外,剩下都可以继承。
  5. java 中的类没有显示的继承任何类,则默认继承 Object 类,Object 类是 java 语言提供的根类(老祖宗类,其没有父类),也就是说,一个对象与生俱来就有 Object 类型中所有的特征。
  6. 继承也存在一些缺点,例如: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):

  1. 什么情况下考虑使用方法重载呢?
    在同一个类当中,如果功能相似,尽可能将方法名定义的相同,这样方便调用的同时代码也会美观。
  2. 代码满足什么条件的时候能够构成方法重载呢?
    只要在同一个类当中,方法名相同,参数列表不同(类型、个数、顺序),即构成方法重载。

带着同样的疑问去学习方法覆盖,什么是方法覆盖?什么情况下考虑方法覆盖?代码怎么写的时候就构成了方法覆盖呢?接下来看一段代码:

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 方法覆盖总结

  1. 当父类中继承过来的方法无法满足当前子类业务需求的时候,子类有必要将父类中继承过来的方法进行覆盖/重写。
  2. 方法覆盖发生在具有继承关系的父子类之间。
  3. 方法覆盖的时候要求相同的返回值类型、相同的方法名、相同的形式参数列表。方法覆盖之后子类对象在调用的时候一定会执行覆盖之后的方法。
  4. 纠正一个错误观念: 后续会学到的一个注解@override可用于标注于子类覆盖的方法之上,但是该注解并不是必须的(只是为了增强可读性),如上,没有该注解,只要满足了方法覆盖的条件,就仍然是方法覆盖。

2.1.6 方法重载和方法覆盖(重写)的区别

  1. 方法重载是发生在同一个类中;而方法覆盖发生在具有继承关系的父子类之间。
  2. 方法重载是要求方法名相同,参数列表不同;而方法覆盖要求重写的方法必须和父类的方法一致:方法名一致、参数列表一致、返回值类型一致。

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
  1. this 是一个引用,保存内存地址指向自己。
  2. this 出现在实例方法中,谁调用这个实例方法,this 就代表谁,this 代表当前正在执行这个动作的对象。
  3. this 不能出现在静态方法中。
  4. this 大部分情况下可以省略,在方法中区分实例变量和局部变量的时候不能省略。
  5. “this(实际参数列表)”出现在构造方法第一行,通过当前的构造方法去调用
    本类当中其它的构造方法。
  • super
  1. 严格来说,super 其实并不是一个引用,它只是一个关键字,super 代表了当前对象中从父类继承过来的那部分特征。 this 指向一个独立的对象,super 并不是指向某个“独立”的对象,假设张大明是父亲,张小明是儿子,有这样一句话:大家都说张小明的眼睛、鼻子和父亲的很像。那么也就是说儿子继承了父亲的眼睛和鼻子特征,那么眼睛和鼻子肯定最终还是长在儿子的身上。假设this指向张小明,那么 super 就代表张小明身上的眼睛和鼻子。换句话说 super 其实是 this 的一部分。如下图所示:张大明和张小明其实是两个独立的对象,两个对象内存方面没有联系,super 只是代表张小明对象身上的眼睛和鼻子,因为这个是从父类中继承过来的,在内存方面使用了 super 关键字进行了标记,对于下图来说“this.眼睛”和“super.眼睛”都是访问的同一块内存空间。
    在这里插入图片描述
  2. super 和 this 都可以使用在实例方法当中。
  3. super 不能使用在静态方法当中,因为 super 代表了当前对象上的父类型特征,静态方法中没有 this,肯定也是不能使用 super 的。
  4. 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());
    }
}

编译报错:
在这里插入图片描述
通过以上的测试,可以看出 thissuper 都是无法使用在静态方法当中的。

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对象创建时构造方法的执行顺序:

  1. 先执行 Object 类的无参数构造方法;
  2. 再执行 People 类的构造方法;
  3. 最后执行 Worker 类的构造方法;
    注意:虽然执行了三个构造方法,但是对象实际上只创建了一个 Worker。
    以上程序的内存结构图变化如下:
    在这里插入图片描述
3.1.3.4 super()作用的总结
  1. 调用父类的构造方法,使用这个构造方法来给当前子类对象初始化父类型特征;
  2. 代码复用。

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.namethis.name最终输出结果是一样的,以上程序执行的内存图如下:

1)父类构造方法执行结束后的内存图
在这里插入图片描述

2)子类构造方法执行结束后的内存图
在这里插入图片描述
通过以上内存结构图发现 this.namesuper.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");
    }
}

程序加载过程总结:

  1. 方法区进行类加载,加载所需类的.class文件;
  2. 根据静态变量和静态代码块出现的顺序:若静态代码块在前,执行静态代码块中的内容,后统一声明静态变量并给静态变量赋默认值,再统一进行显式赋值;若为静态变量在前,先统一声明静态变量并给静态变量赋默认值,再统一进行显式赋值,然后执行静态代码块中的内容;
  3. 若有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");
    }
}

则代码运行过程详解如下:

  1. 首先进行类加载,需要将Text类、Object类、String类等要使用的类由JVM加载到其方法区中,JVM加载Text类根据顺序需要先声明静态变量,此时,k=0,t1=null,t2=null,i=0,n=0;
  2. 执行静态代码块中的代码,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实例化完成】
  3. 触发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实例化完成】
  4. 为i显示赋值,进入print(“i”),输出:5:i i=4 n=4,而后k=5,n=5,i=5,返回i=5
  5. 为n赋值99
  6. 执行print(“静态块”),输出:6:静态块 i=5 n=99,而后k=6,n=100,i=6
  7. 进行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,实例化完成】
  8. main执行结束。

所以,代码最终运行结果为:

在这里插入图片描述

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mfbz.cn/a/390489.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

07-k8s中secret资源02-玩转secret

一、回顾secret资源的简单实用 第一步&#xff1a;将想要的数据信息【key&#xff1a;value】中的value值&#xff0c;使用base64编码后&#xff0c;写入secret资源清单中&#xff1b; 第二步&#xff1a;创建secret资源&#xff1b; 第三步&#xff1a;pod资源引用secret资源&…

【Linux网络编程六】服务器守护进程化Daemon

【Linux网络编程六】服务器守护进程化Daemon 一.背景知识&#xff1a;前台与后台二.相关操作三.Linux的进程间关系四.自成会话五.守护进程四步骤六.服务器守护进程化 一.背景知识&#xff1a;前台与后台 核心知识就是一个用户在启动Linux时&#xff0c;都会给一个session会话&a…

最小生成树(Kruskal算法及相关例题)

1.Kruskal算法概念以及基本思路 &#xff08;1&#xff09;概念&#xff1a; 克鲁斯卡尔算法是求连通网的最小生成树的另一种方法。它的时间复杂度为O&#xff08;ElogE&#xff09;(E是图G的边的总数)&#xff0c;适合于求边稀疏的网的最小生成树 。 其基本思想是&#xff…

JDBC访问数据库

目录 加载Driver驱动配置驱动地址 获取数据库连接创建会话-SQL命令发送器通过Statement发送SQL命令并得到结果处理结果关闭数据库资源测试 加载Driver驱动 1.在模块JDBC中新建一个lib目录文件 2. 将mysql-connector-j-8.2.0包粘贴至lib目录中。 配置驱动地址 // 加载…

Nvidia 携手 RTX 推出的本地运行 AI 聊天机器人

每周跟踪AI热点新闻动向和震撼发展 想要探索生成式人工智能的前沿进展吗&#xff1f;订阅我们的简报&#xff0c;深入解析最新的技术突破、实际应用案例和未来的趋势。与全球数同行一同&#xff0c;从行业内部的深度分析和实用指南中受益。不要错过这个机会&#xff0c;成为AI领…

人工智能学习与实训笔记(三):神经网络之目标检测问题

目录 五、目标检测问题 5.1 目标检测基础概念 5.1.1 边界框&#xff08;bounding box&#xff09; 5.1.2 锚框&#xff08;Anchor box&#xff09; 5.1.3 交并比 5.2 单阶段目标检测模型YOLOv3 5.2.1 YOLOv3模型设计思想 5.2.2 YOLOv3模型训练过程 5.2.3 如何建立输出…

uni-app 经验分享,从入门到离职(二)—— tabBar 底部导航栏实战基础篇

文章目录 &#x1f4cb;前言⏬关于专栏 &#x1f3af;关于小程序 tabbar 的一些知识&#x1f3af;创建一个基本的 tabBar&#x1f4dd;最后 &#x1f4cb;前言 这篇文章的内容主题是关于小程序的 tabBar 底部导航栏的入门使用和实战技巧。通过上一篇文章的基础&#xff0c;我们…

【sgCreateTableColumn】自定义小工具:敏捷开发→自动化生成表格列html代码(表格列生成工具)[基于el-table-column]

源码 <template><!-- 前往https://blog.csdn.net/qq_37860634/article/details/136126479 查看使用说明 --><div :class"$options.name"><div class"sg-head">表格列生成工具</div><div class"sg-container"…

C++,stl,常用排序算法,常用拷贝和替换算法

目录 1.常用排序算法 sort random_shuffle merge reverse 2.常用拷贝和替换算法 copy replace replace_if swap 1.常用排序算法 sort 默认从小到大排序 #include<bits/stdc.h> using namespace std;int main() {vector<int> v;v.push_back(1);v.push_ba…

RabbitMQ如何保证可靠

0. RabbitMQ不可靠原因 消息从生产者到消费者的每一步都可能导致消息丢失&#xff1a; 发送消息时丢失&#xff1a; 生产者发送消息时连接MQ失败生产者发送消息到达MQ后未找到Exchange生产者发送消息到达MQ的Exchange后&#xff0c;未找到合适的Queue消息到达MQ后&#xff0c;…

idea里微服务依赖在maven能install但不能启动

场景&#xff1a;多个微服务相互依赖&#xff0c;install都没问题&#xff0c;jar包都是正常的&#xff0c;就连jar都能启动&#xff0c;为什么在idea里面项目就是不能启动呢&#xff0c;我是懵逼的 所以解决办法就是&#xff1a; 在设置的编译器里面虚拟机选项添加 -Djps.tr…

第五节 zookeeper集群与分布式锁_2

1.分布式锁概述 1.1 什么是分布式锁 1&#xff09;要介绍分布式锁&#xff0c;首先要提到与分布式锁相对应的是线程锁。 线程锁&#xff1a;主要用来给方法、代码块加锁。当某个方法或代码使用锁&#xff0c;在同一时刻仅有一个线程执行该方法或该代码段。 线程锁只在同一J…

LEETCODE 164. 破解闯关密码

class Solution { public:string crackPassword(vector<int>& password) {vector<string> password_str;for(int i0;i<password.size();i){password_str.push_back(to_string(password[i]));}//希尔排序int gappassword.size()/2;while(gap>0){for(int i…

命令执行讲解和函数

命令执行漏洞简介 命令执行漏洞产生原因 应用未对用户输入做严格得检查过滤&#xff0c;导致用户输入得参数被当成命令来执行 命令执行漏洞的危害 1.继承Web服务程序的权限去执行系统命会或读写文件 2.反弹shell&#xff0c;获得目标服务器的权限 3.进一步内网渗透 远程代…

python----输入输出算数运算

1.格式化输出 如果我们直接打印输出&#xff0c;就是输出变量的值&#xff0c;例如&#xff1a; 如果我们想打印a10就需要格式化字符串&#xff0c;就是使用f进行格式化&#xff0c;如图所示&#xff1b; 2.控制台输入 input执行的时候&#xff0c;就会等待用户进行输入&…

Qlik Sense : 条形图

条形图 “条形图适合比较多个值。维度轴显示所比较的类别条目&#xff0c;度量轴显示每个类别条目的值。” Qlik Sense中的条形图是一种数据可视化工具&#xff0c;用于展示不同类别或维度之间的比较。它通过水平或垂直的条形表示数据&#xff0c;并根据数值的大小进行排序。…

RK3568平台开发系列讲解(存储篇)文件描述符相关系统调用实现

🚀返回专栏总目录 文章目录 一、open 系统调用二、close 系统调用沉淀、分享、成长,让自己和他人都能有所收获!😄 一、open 系统调用 open()系统调用会分配新的文件句柄(file description),用来维护与打开文件相关的元信息(如偏移量、路径、操作方法等),并会给进程…

微信小程序框架阐述

目录 一、框架 响应的数据绑定 页面管理 基础组件 丰富的 API 二、逻辑层 App Service 小程序的生命周期 注册页面 使用 Page 构造器注册页面 在页面中使用 behaviors 使用 Component 构造器构造页面 页面的生命周期 页面路由 页面栈 路由方式 注意事项 模块化…

Git 初学

目录 一、需求的产生 二、版本控制系统理解 1. 认识版本控制系统 2. 版本控制系统分类 &#xff08;1&#xff09;集中式版本控制系统 缺点&#xff1a; &#xff08;2&#xff09;分布式版本控制系统 三、初识 git 四、git 的使用 例&#xff1a;将 “ OLED文件夹 ”…

单部10层电梯控制系列之UDT数据类型的建立(SCL代码)

这篇博客开始介绍单部10层电梯的完整控制程序编写过程&#xff0c;编程语言&#xff1a;SCL&#xff0c;控制器型号&#xff1a;S7-1200PLC。开篇博客我们介绍电梯控制用到的所有UDT数据类型。在学习本篇博客之前大家可以参考下面文章&#xff0c;了解博途PLC里的UDT数据类型是…
最新文章