前言
该部分为面向对象编程(中级部分)中:继承部分的知识
继承
为什么需要继承?
继承可以解决代码复用,让我们的编程更加接近人类思维。当多个类存在相同的属性(变量)和方法时,可以从这些类中抽象出父类,在父类中定义这些相同的属性和方法,所有的字类不需要重新定义这些属性和方法,只需要通过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); System.out.println("子类sub()构造器被调用"); } public Sub(String name){ super("Tom",30); System.out.println("子类Sub(String name)构造器被调用"); } }
public class ExtendsDetail{ public static void main(String[] args){ Sub sub = new 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(); System.out.println(son.name); System.out.println(son.age); System.out.println(son.hobby); System.out.println(son.getSalary()); } }
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"); System.out.println("b"); } public B(String name){ System.out.println("b name"); } }
|
结果是:
例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{ 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()"); cal(); this.cal(); super.cal(); } }
|
如果本类有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()"); cal(); this.cal(); super.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(){}
|
1 2 3 4 5 6
| void sayok(){}
public void sayok(){}
|
这里的主要原因依赖于:里氏代换原则
里氏代换原则(Liskov Substitution Principle LSP)面向对象设计的基本原则之一。 里氏代换原则中说,任何基类可以出现的地方,子类一定可以出现。 LSP是继承复用的基石,只有当衍生类可以替换掉基类,软件单位的功能不受到影响时,基类才能真正被复用,而衍生类也能够在基类的基础上增加新的行为。里氏代换原则是对“开-闭”原则的补充。实现“开-闭”原则的关键步骤就是抽象化。而基类与子类的继承关系就是抽象化的具体实现,所以里氏代换原则是对实现抽象化的具体步骤的规范。
把里氏代换原则解释得更完整一些:在一个软件系统中,子类应该可以替换任何基类能够出现的地方,并且经过替换以后,代码还能正常工作。
方法重写和重载区别
| 名称 |
发生范围 |
方法名 |
形参列表 |
返回类型 |
修饰符 |
| 重载(overload) |
本类 |
必须一样 |
类型、个数或者顺序至少有一个不同 |
无要求 |
无要求 |
| 重写(override) |
父子类 |
必须一样 |
必须相同 |
子类重写的方法,返回的类型和父类返回的类型一致,或者是其子类 |
子类不能缩小父类方法的访问范围 |