继承
概述
- 继承是面向对象思想的三大特性之一,使类与类之间产生
特殊 - 一般
的关系,即is-a
关系。 - 继承是
从已有类中派生出新的类
,新的类能吸收已有类的属性和方法
,并且能拓展新的属性和行为
。 - 在Java中使用
extends
关键字表示继承,语法表示为:class 子类 extends 父类{}
- 子类被称为
派生类
,父类又被称为超类
。 - 子类继承父类,表名
子类是一种特殊的父类
,子类拥有父类的属性和方法,并且子类可以拓展具有父类所没有的一些属性和方法。 - 子类即是不扩展父类,也能维持拥有父类的操作。
优缺点
- 继承的好处
- 提高了代码的复用性
- 提高了代码的维护性
- 让类与类之间产生了关系,是多态的前提
- 缺点
- 增加了耦合性
- OOP思想开发原则:高内聚,低耦合
- 耦合:类与类之间的关系
- 内聚:自身完成事情的能力
Java继承特点
Java
只支持单继承
,不支持多重继承
操作(extends A,B,C..)class A {} class B {} class C extends A,B {} // 错误的,一个子类继承了两个父类,Java中不允许
为什么只支持单继承?
多继承
会存在安全隐患
,因为当继承的多个类都存在相同的属性
或方法名相同方法体不同的方法
,子类进行调用时,就会产生不知道该调用哪一个类中的方法的情况。
Java支持
多层继承
(继承体系)class A {} class B extends A {} class C extends B {}
如果想用这个继承体系的所有功能,那么就实用对底层的子类创建的对象
如果想看这个体系的共性功能,那么就看最顶层的类的功能
super
- super 是 Java提供的一个
关键字
- super用于
限定该对象调用它从父类继承得到的实例变量或方法
。 - super和this相同,都
不能出现在静态方法中
,因为静态方法属于类的,调用静态方法的可能是个类,而不是对象,而super和this都是限定对象调用。 - super同样也可以在子类中
调用父类中被子类隐藏和覆盖的同名实例变量和同名方法
。 - 在
构造器
中使用super,则super
会用于限定于该构造器初始化的是该对象从父类继承得到的实例变量
,而不是该类自己定义的实例变量。意思就是调用父类的构造器。 super限定
:super紧跟一个点
- super.name; super.walk();
super调用
:super紧跟圆括号
- super(参数)
继承注意点
成员变量和方法
- 子类
只能
继承父类的所有非私有
的成员变量和方法。可以
继承public protected
修饰的成员,不可以
继承private
修饰的。 - 但是可以通过父类中提供的
public 的setter和getter方法
进行间接
的访问和操作
private 的属性 - 对于子类可以继承父类中的成员变量和成员方法,如果
子类
中出现
了和父类同名的成员变量和成员方法
时,父类
的成员变量会被隐藏
,父类的成员方法会被覆盖
。需要使用父类的成员变量和方法时,就需要使用super
关键字来进行引用
。
- 隐藏是针对成员变量和静态方法,覆盖是针对普通方法。
- 当创建一个子类对象时,
不仅会为该类的实例变量分配内存,也会为它从父类继承得到的所有实例变量分配内存
,即使子类定义了与父类中同名的实例变量。 即依然会为父类中定义的、被隐藏的变量分配内存。 - 如果
子类中的实例变量被私有
了 ,其父类中的同名实例变量没有被私有
,那么子类对象就无法直接调用该变量,但可以通过先将对象变量强制向上转型为父类型
,在通过该对象引用变量来访问那个实例变量,就会得到的是父类中的
那个实例变量。
- 子类
构造器
子类
不能继承获得
父类的构造方法
,但是可以通过super
关键字来访问
父类构造方法。在一个构造器中
调用另一个重载构造器
使用this
调用完成,在子类构造器中调用父类构造器
使用super
调用来完成。super 和 this 的调用都
必须是在第一句
,否则会产生编译错误,this和super只能存在一个
。不能进行
递归构造器调用
,即多个构造器之间互相循环调用。如果
父类有无参构造
时,所有
构造方法(包含任意有参构造)自动默认
都会访问父类中的空参构造方法
。(自带super();
)- 因为继承的目的是子类获取和使用父类的属性和行为,所以子类初始化之前,一定要先完成父类数据的初始化。
- 在Java中,每个类都会
默认继承Object超类
,所以每一个构造方法的第一条默认语句都是super()
如果
父类没有无参构造
,反而有其他的有参构造方法
时,子类继承父类后,子类必须显式的创建构造器,不论子类的构造器是否和父类构造器中参数类型是否一致,都必须在子类的构造器中显式的通过super关键字调用和父类构造器相应参数的构造方法
。否则编译都通不过。代码示例如下:class Person { public Person(int age){ System.out.println(age); } } class Student extends Person{ public Student(int age) { super(age); } public Student(){ super(10); //必须调用父类的有参构造 System.out.println("子类可以创建其他类型构造器,但是必须显式的用super调用父类构造器") } }
也可以使用
this
先调用子类中的构造方法,再间接调用父类中的有参构造方法,实例如下:public class ExtendTest1 { public static void main(String[] args) { new Student(); } } class Person { public Person(int age){ System.out.println("父类有参构造"); } } class Student extends Person{ public Student(int age) { super(age); System.out.println("子类有参构造"); } public Student(){ this(10); //可以使用this先调用子类中的有参构造,从而间接调用父类中的有参构造 System.out.println("子类无参构造"); } }
使用this,执行顺序结果为:先调用了子类中无参构造,此无参构造会接着调用子类中的有参构造,又接着调用父类中的有参构造,此时首先执行完毕了父类有参构造,接着子类有参构造执行完毕,最后子类无参构造才执行完毕。
父类有参构造 子类有参构造 子类无参构造
以下这种是错误的:(因为当父类中没有无参构造器时,父类中没有这种类型的构造方法)
class Student extends Person{ public Student(String name){ super(); } //错误的,因为当父类中没有无参构造器时,父类中没有这种类型的构造方法 public Student(int age) { super(age); } } class Person { public Person(String name ,int age){ System.out.println(name+age); } public Person(int age){ System.out.println(age); } }
以下这种正确:(因为当父类中没有无参构造器时,子类中的构造方法的类型在父类中有)
class Student extends Person{ //因为当父类中没有无参构造器时,子类中的构造方法的类型在父类中有 public Student(int age) { super(age); } } class Person { public Person(String name ,int age){ System.out.println(name+age); } public Person(int age){ System.out.println(age); } }
class Student extends Person{ //因为当父类中没有无参构造器时,子类中的构造方法的类型在父类中有 public Student(String name ,int age){ super(name,age); } public Student(int age) { super(age); } } class Person { public Person(String name ,int age){ System.out.println(name+age); } public Person(int age){ System.out.println(age); } }
结论:当父类中没有无参构造器时,子类继承父类,子类中的构造器方法类型可以和父类中的构造器不同,但是必须每个构造器都显式的使用super关键字调用父类中的某个有参构造器,也可以使用this调用子类中的某个有参构造器,但这个有参构造器必须通过super访问父类中的有参构造器。
继承的执行顺序问题
继承体系中的
构造器执行顺序
- 当调用子类构造器实例化子类对象时,
父类构造器总是在子类构造器之前执行
。 - 创建任何对象总是从该类所在
继承树最顶层类的构造器开始执行
,然后依次向下
执行,最后才执行本类的构造器。如果父类通过this调用了同类中的重载构造器
,就会依次执行此父类的多个构造器
。
- 当调用子类构造器实例化子类对象时,
继承体系中的
静态域
执行顺序- 当调用子类构造器实例化子类对象时,
父类优先于子类进行加载到内存
,所以会先执行父类中的静态域
- 从该类所在继承树最顶层类开始加载,并执行其静态域,依次向下执行,最后执行本类。
- 静态域优先于main方法,优先于构造器执行
- 当调用子类构造器实例化子类对象时,
父类和子类中
都有静态代码块和构造代码块
,示例如下:class Test2_Extends { static { System.out.println("主类静态块"); } public static void main(String[] args) { Zi z = new Zi(); } } class Fu { static { System.out.println("静态代码块Fu"); } { System.out.println("构造代码块Fu"); } public Fu() { System.out.println("构造方法Fu"); } } class Zi extends Fu { static { System.out.println("静态代码块Zi"); } { System.out.println("构造代码块Zi"); } public Zi() { System.out.println("构造方法Zi"); } }
此时的执行结果:
主类静态块 静态代码块Fu 静态代码块Zi 构造代码块Fu 构造方法Fu 构造代码块Zi 构造方法Zi
执行顺序分析:
- 主类Test2_Extends先加载到内存,静态域优先于main方法执行,先输出了主类静态块,其中的main方法入栈执行,main方法中创建了子类对象
- 子类对象创建过程中,父类和子类都加载到内存中,并且Fu.class优先于Zi.class加载,父类中的静态域先执行后,再执行子类中的静态域,此时会第一个输出:静态代码块Fu,第二个输出:静态代码块Zi
- 创建对象时进入子类的构造器,因为Java是分层初始化的,所以会先初始化父类再初始化子类,子类构造器会自动默认先执行父类的构造器,因为构造代码块优先于构造方法执行,所以此时就会先执行父类的构造代码块后,再执行父类的构造方法。所以第三个输出:构造代码块Fu,第四个输出:构造方法Fu
- Fu类初始化结束后,子类初始化,第五个输出的是:构造代码块Zi,第六个输出:构造方法Zi
方法重写
- 重写:子父类出现一模一样的方法,但
返回值类型
可以是子父类
。 - 方法重写的应用:
- 当子类需要父类的功能,而功能主体
子类有自己的特有内容
时,可以重写父类中的方法。即沿用了父类的功能,又定义了子类特有的内容
- 当子类需要父类的功能,而功能主体
方法重写规则
- 重写遵循“
两同两小一大
”规则:
- 两同:
方法名
、形参列表
相同 - 两小:
- 子类方法
返回值类型
应比父类方法返回值类型更小或相等
- 子类方法
声明抛出的异常类
应比父类方法声明抛出的异常类更小或相等
- 一大:
- 子类方法的
访问权限
应比父类方法访问权限更大或相等
- 两同:
重写注意点
- 父类中的
私有方法不能被重写
,该方法对于子类是隐藏
的,因此其子类无法访问
该方法,也无法重写 - 父类
静态方法
,子类也必须
通过静态方法进行覆盖,即静态只能覆盖静态
- 子类重写父类方法时,
最好
声明得一模一样 - 如果
子类中定义
了一个与父类private方法
具有相同的方法名、相同的形参列表、相同的返回值类型的方
法,依然不是
重写,只是在子类中重新定义了一个新的方法
,所以该新方法不会受父类方法的任何限制
。
重写和重载
Override
和Overload
的区别?Overload
能改变返回值类型吗?
Override是重写
,Overload是重载
。重载可以改变返回值类型
,它是方法名相同,参数列表不同,与返回值类型无关
。
- 方法
重写
:子类
中出现和父类
中方法声明一模一样的方法
。返回值类型相同(或者是子父类,多态),方法名和参数列表一模一样
。主要发生在子类和父类的同名方法之间。 - 方法
重载
:本类
中出现方法名相同
,参数列表不同
的方法,和返回值类型无关,可以改变
。主要发生同一类的多个同名方法之间。 - 子类对象调用方法时,
先找子类
本身的方法,再找父类
中的方法。
继承带来的问题
- 继承严重
破坏了父类的封装性
,每个类都应该它内部信息和实现细节,而只暴露必要的方法给其它类使用
。但在继承关系中
,子类可以直接访问父类的成员变量(内部信息)和方法
, 从而造成子类和父类的严重耦合。 - 父类的实现细节对其子类
不再透明
,从而导致子类可以恶意篡改父类的方法
子类继承的改进
为了保证父类
有良好的封装性
,不会被子类随意改变,设计父类通常应该遵循如下规则:尽量隐藏父类的内部数据。
尽量把
父类的所有成员变量都设置成private访问类型
,不要让子类直接访问父类的成员变量。不要让子类随意访问、修改父类的方法。
父类
中那些仅为辅助其他的工具方法
,应该使用private
修饰,让子类无法访问方法;- 如果父类中的方法
需要被外部类调用
,则必须以public
修饰,但又不想让子类重写
,就可以使用final
修饰符。 如果
希望父类的某个方法被子类重写
,但不希望被其他类自由访问
,则可以使用protected
来修饰方法。尽量不要在父类构造器中调用将要被子类重写的方法。
查看下面例子说明在父类构造器中调用被子类重写的方法引发的错误:
package extend; class Base { public Base() { System.out.println("父类构造器"); test(); } public void test() // ①号test()方法 { System.out.println("将被子类重写的方法"); } } public class Sub extends Base { public Sub(){ System.out.println("子类构造器"); } private String name="aa"; public void test() // ②号test()方法 { System.out.println("子类test"); System.out.println("子类重写父类的方法," + "其name字符串长度" + name.length()); } public static void main(String[] args) { // 下面代码会引发空指针异常 Sub s = new Sub(); } }
执行结果:
父类构造器 子类test Exception in thread "main" java.lang.NullPointerException
分析:
- 当创建Sub对象时,
先执行其父类构造器
,如果父类构造器调用了被子类重写覆盖的方法,就会调用被子类重写后的②号test()方法
,子类的test方法调用了子类的实例变量name
,父类直接调用的子类的test方法,此时子类还未初始化,还未调用子类构造器,实例变量name还未被指定初始值,仍然为默认值null
,所以引发了空指针异常。
何时适合用继承
- 子类需要额外增加属性,而不仅仅是属性值的改变。
- 子类需要增加自己独有的行为方式(包括增加新的方法或重写父类的方法)。