Java学习笔记(五)——继承

前言

该部分为面向对象编程(中级部分)中:继承部分的知识

继承

为什么需要继承?

继承可以解决代码复用,让我们的编程更加接近人类思维。当多个类存在相同的属性(变量)和方法时,可以从这些类中抽象出父类,在父类中定义这些相同的属性和方法,所有的字类不需要重新定义这些属性和方法,只需要通过extends来声明继承父类即可。

  • 代码的复用性提高
  • 代码的扩展性和维护性提高

继承的基本语法

1
2
3
4
5
class 子类 extends 父类{
1)子类就会自动拥有父类定义的属性和方法
2)父类又叫超类、基类
3)子类又叫派生类
}

继承的细节

  • 1、子类继承了所有的属性和方法,非私有的属性和方法可以在子类直接访问,但是私有属性和方法不能在子类直接访问直接访问,要通过父类提供的公共方法去访问。(注意,如果是默认的属性和方法,如果在同包下也可访问,具体可以看访问修饰符的范围表)
  • 2、子类必须调用父类的构造器,完成父类的初始化。

​ 也就是说,当子类在创建对象的时候,会默认调用父类的无参构造器。这是因为子类的无参构造器中会隐藏一个super动作:

1
2
3
4
5
6
public class Sub extends Base{
public Sub(){
super();//默认隐藏,默认调用父类的无参构造器
System.out.println("子类sub()构造器被调用...");
}
}
  • 3、当创建子类对象时,不管使用子类的哪个构造器,默认情况下总会去调用父类的无参构造器,如果父类没有提供无参构造器,则必须在子类的每一个构造器中都用super去指定父类的哪个构造器完成对父类的初始化工作,否则编译不会通过。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
//父类
public class Base{
public int age;
public String name;
//父类构造器
public Base(String name,int age){//有参构造器,这里写了有参构造器,默认的无参构造器就会被覆盖,也就是不存在无参构造器了
System.out.println("父类Base(String name, int age)构造器被调用");
}
}

//子类
public class Sub extends Base{
public Sub(){
super("Smith",10);//这里必须指定一个super
System.out.println("子类sub()构造器被调用");
}

public Sub(String name){
super("Tom",30);//这里必须指定一个super
System.out.println("子类Sub(String name)构造器被调用");
}
}

//主函数
public class ExtendsDetail{
public static void main(String[] args){
Sub sub = new Sub();//创建了子类对象sub
System.out.println("===第二个对象===");
Sub sub2 = new Sub("Jack");创建了第二个子类对象sub2
}
}
  • 4、如果希望指定去调用父类的某个构造器,则显示的调用一下:super(参数列表)
  • 5、super在使用时,只能在构造器中使用,且必须放在构造器的第一行 。(因为必须先有父类,才有子类)
  • 6、super()和this()都只能放在构造器的第一行,因此这两个方法不能共存在一个构造器中。注意,这里this调用指的是不能用this来调用其他构造器,以下情况是可以的:
1
2
3
4
public Pc(String cpu,int memory,int disk,String brand){
super(cpu,memory,disk);//父类的构造器完成父类属性的初始化
this.brand = brand;//子类构造器完成子类属性的初始化
}

这里也体现了:继承涉及的基本思想,父类的构造器完成父类属性的初始化,子类构造器完成子类属性的初始化。

  • 7、Java所有类都是Object的子类,Object是所有类的基类
  • 8、父类构造器的调用不限于直接父类!将一直往上追溯直到Object类(顶级父类)。
  • 9、子类最多只能继承一个父类(指直接继承),即Java中是单继承机制

​ 思考如何让A类继承B类和C类?——让B类继承C类。

  • 10、不能滥用继承,子类和父类之间必须满足is-a的逻辑关系。

继承的本质

当子类继承父类,创建子类对象的时候,内存中到底发生了什么?——当子类对象创建好后,建立查找的关系

构造过程:

  • 1、先根据继承关系加载我们的类,顺序从顶级父类到子类
  • 2、加载完后会堆里面分配空间
  • 3、如果有字符串则会指向常量池的某一个地址,基础数据类型直接存放在堆中
  • 4、把son在堆中的地址返回给主方法的对象引用

访问过程:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
package com.learn.extend_;

import javax.xml.transform.stream.StreamSource;
import java.sql.SQLOutput;

public class ExtendsTheory {
public static void main(String[] args) {
Son son = new Son();//内存的布局?
//->需要按照查找关系来返回信息
//(1)首先看子类是否有该属性
//(2)如果子类有这个属性,并且可以访问,则返回信息
//(3)如果子类没有这个属性,就看父类有没有这个属性(如果父类有该属性,并且可以访问,则返回信息)
//(4)如果父类没有,就按照(3)的规则(就近原则)继续找上一级父类,知道Object...
System.out.println(son.name);//返回就是儿子
System.out.println(son.age);//子类虽然没有,但父类有->30
System.out.println(son.hobby);//子类和父类虽然没有,但父类的父类有,可以访问
//System.out.println(son.salary);//能返回薪水吗?不可以,因为子类不能访问private类型的属性
//这里有个问题:salary属性是private的,那么在new一个son的对象之后,在内存里面到底有没有这个salary呢?
//当然有,但不能直接访问,只能让father提供一个公共的方法来访问
System.out.println(son.getSalary());
//相应还有一个问题:GrandPa类里面有一个salary是public类型的,父类中的salary是private类型的,那么son中能否直接访问salary呢?
//答案是不可以!因为son在往上找的时候,在father里面已经找到了salary,但是这个salary无法访问,所以直接报错,而不会再去上一级查看是否有
//如果有一级被堵住了,直接结束。总结:不能跨级查找。
}
}

class GrandPa{
String name = "爷爷";
String hobby = "旅游";
}
class Father extends GrandPa{
String name = "爸爸";
int age = 30;
private double salary = 40000;
public double getSalary(){
return salary;
}
}
class Son extends Father{
String name = "儿子";
}

练习例子

下面代码输出为何?

例1

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
package com.learn.extend_;

import java.sql.SQLSyntaxErrorException;

public class Extends02 {
public static void main(String[] args) {
B b = new B();
}
}

class A{
public A(){
System.out.println("a");
}
public A(String name){
System.out.println("a name");
}
}
class B extends A{
public B(){
this("abc");//由于有了this,默认的super被覆盖,先调用B(String name)构造器
System.out.println("b");
}
public B(String name){//默认有一个super(),调用父类的无参构造器
System.out.println("b name");
}
}

结果是:

1
2
3
a
b name
b

例2

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
package com.learn.extend_;

public class Extends03 {
public static void main(String[] args) {
C c = new C();
}
}

class A{
public A(){
System.out.println("我是A类");
}
}

class B extends A{
public B(){
System.out.println("我是B类的无参构造");
}
public B(String name){
System.out.println(name+"我是B类的有参构造");
}
}
class C extends B{
public C(){
this("hello");
System.out.println("我是C类的无参构造");
}
public C(String name){
super("hahaha");
System.out.println("我是C类的有参构造");
}
}

结果是:

1
2
3
4
我是A类
hahaha我是B类的有参构造
我是C类的有参构造
我是C类的无参构造

super的基本语法

  • 1、访问父类的属性,但不能访问父类的private属性 【案例】super.属性名

  • 2、访问父类的方法,但不能访问父类的private方法【案例】super.方法名(参数列表)

  • 3、访问父类的构造器:super(参数列表);只能放在构造器的第一句,且只能出现一句。

super的使用细节

super给编程带来的便利:

  • 1、调用父类的构造器的好处(分工明确,父类属性由父类初始化,子类属性由子类初始化)
  • 2、当子类中有和父类中成员(属性和方法)重名时,为了访问父类的成员,必须通过super。如果没有重名,使用super、this、直接访问是一样的效果。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
public class A{
//4个属性
public int n1 = 100;
protected int n2 = 200;
int n3 = 300;
private int n4 = 400;

public A(){}
public A(String name){}
public A(String name , int age){}

public void call(){
System.out.println("A类的cal()方法...")
}

public void test100(){
}

protected void test200(){
}

void test 300(){
}

private void test400(){
}

}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class B extends A{
public void sum(){
System.out.println("B类的sum()");
//这里希望调用父类-A 的cal方法
//这时,因为子类B没有cal方法,因此我可以使用下面三种方式
//找cal方法时,顺序是:
//(1)先找本类,如果有,则调用
//(2)如果没有则找父类(如果有,并可以调用,则调用)
//(3)如果父类没有,则继续找父类的父类,规则一样,直到object
//提示:如果查找方法的过程中,找到了,但是不能访问,则报错
// 如果查找方法的过程中,没有找到,则提示方法不存在
cal();
this.cal();//等价cal()
super.cal();//没有查找被类的过程,直接进入到第(2)步,直接找父类,但其他规则是一样的
}
}

如果本类有cal()函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class B extends A{
public void cal(){
System.out.println("B类的cal()");
}
public void sum(){
System.out.println("B类的sum()");
//这里希望调用父类-A 的cal方法
//这时,因为子类B没有cal方法,因此我可以使用下面三种方式
//找cal方法时,顺序是:
//(1)先找本类,如果有,则调用
//(2)如果没有则找父类(如果有,并可以调用,则调用)
//(3)如果父类没有,则继续找父类的父类,规则一样,直到object
//提示:如果查找方法的过程中,找到了,但是不能访问,则报错
// 如果查找方法的过程中,没有找到,则提示方法不存在
cal();//走的是本类的
this.cal();//等价cal()
super.cal();//走的是A的cal()
}
}
  • 3、super的访问不限于直接父类,如果爷爷类和本类中有同名的成员,也可以使用super去访问爷爷类的成员;如果多个基类中都有同名的成员,使用super访问遵循就近原则。当然也需要遵守访问权限的相关规则。

super和this的比较

no. 区别点 this super
1 访问属性 访问本类中的属性,如果本类中没有此属性则从父类中继续查找 从父类开始查找属性
2 调用方法 访问本类中的方法,如果本类中没有此方法则从父类中继续查找 从父类开始查找方法
3 调用构造器 调用本类构造器,必须放在构造器的首行 调用父类构造器,必须放在构造器的首行
4 特殊 表示当前对象 子类中访问父类对象

方法重写

方法覆盖(重写)就是子类有一个方法,和父类的某个方法的名称、返回类型、参数一样,那么我们就说子类的这个方法覆盖了父类的那个方法。

方法重写的细节

方法重写也叫方法覆盖,需要满足以下条件:

  • 1、子类的方法的参数、方法名称,要和父类方法完全一样。
  • 2、子类方法的返回类型和父类方法返回类型一样,或者是父类返回类型的子类(反过来不行)。比如父类返回类型是Object,子类方法返回类型是String,例如以下两个也是重写:
1
2
3
4
//父类方法
public Object getInfo(){}
//子类方法:
public String getInfo(){}
  • 3、子类方法不能缩小父类方法的访问权限。意思就是
1
2
3
4
5
6
//父类方法:
void sayok(){}
//子类方法:
public void sayok(){}

//可行,因为访问权限:public > protected > 默认 > private

这里的主要原因依赖于:里氏代换原则

里氏代换原则(Liskov Substitution Principle LSP)面向对象设计的基本原则之一。 里氏代换原则中说,任何基类可以出现的地方,子类一定可以出现。 LSP是继承复用的基石,只有当衍生类可以替换掉基类,软件单位的功能不受到影响时,基类才能真正被复用,而衍生类也能够在基类的基础上增加新的行为。里氏代换原则是对“开-闭”原则的补充。实现“开-闭”原则的关键步骤就是抽象化。而基类与子类的继承关系就是抽象化的具体实现,所以里氏代换原则是对实现抽象化的具体步骤的规范。

把里氏代换原则解释得更完整一些:在一个软件系统中,子类应该可以替换任何基类能够出现的地方,并且经过替换以后,代码还能正常工作。

方法重写和重载区别

名称 发生范围 方法名 形参列表 返回类型 修饰符
重载(overload) 本类 必须一样 类型、个数或者顺序至少有一个不同 无要求 无要求
重写(override) 父子类 必须一样 必须相同 子类重写的方法,返回的类型和父类返回的类型一致,或者是其子类 子类不能缩小父类方法的访问范围