继承
概述
多个类中存在相同属性和行为时,将这些内容抽取到单独一个类中,那么多个类无需再定义这些属性和行为,只要继承该类即可
单独的这个类称为父类、基类或者超类,这多个类可以称为子类或者派生类
有了继承之后,我们定义一个类的时候,可以在父类的基础上,定义自己新的成员方法和成员变量
方式
通过extends关键字可以实现类与类的继承
格式:class 子类名 extends 父类名{ };
好处与弊端
- 好处
- 提高了代码的复用性
- 提高了代码的维护性
- 让类与类之间产生了关系,是多态的前提
- 弊端
- 使类的耦合性增强了,这样某个类的改变就会影响其他和该类相关的类
- 打破了封装性
开发的原则:低耦合,高内聚
耦合:类与类之间的关系
内聚:独立完成某事的能力
特点
- Java只支持单继承,不支持多继承
- Java支持多层继承(继承体系)
注意事项
- 子类只能继承父类所有非私有的成员(成员方法和成员变量)
- 子类不能继承父类的构造方法,但可以通过super关键字访问父类构造方法
- 不要为了部分功能而去继承
- 使用继承的情况:
如果有两个类A,B,只有他们符合A是B的一种,或者B是A的一种,就可以考虑使用继承
继承体现的是一种"is a"的关系。
成员关系
成员变量
- 子类中的成员变量和父类中的成员变量名称不一样时,调用对应的变量即可
class Father{
public int num = 10;
}
class Son extends Father{
public int num2 = 20;
public void show(){
System.out.println(num); //10
System.out.println(num2); //20
}
}
- 子类中的成员变量和父类中的成员变量名称一样时,就近原则
在子类方法中访问一个变量的查找顺序(找到就使用):
子类的局部范围 -> 子类的成员范围 -> 父类的成员范围 -> 报错
问题:子类中怎么明确变量名称一样的不同变量呢?
解决方法:引入this 和 super 关键字
this 和 super 的定义:
this:代表本类对应对象的引用
super:代表父类存储空间的标识(可以理解为父类引用)
例子:
class Father{
public int num = 10;
}
public class Son extends Father{
public int num = 20;
public void show() {
int num = 30;
System.out.println(num); //30
System.out.println(this.num); //20
System.out.println(super.num); //10
}
}
构造方法
子类中所有的构造方法默认都会访问父类中的无参构造方法
原因:子类会继承父类中的数据,可能还会使用父类的数据,所以子类初始化前一定要先完成父类数据的初始化
子类每一个构造方法的第一条语句默认都是:super();
问题:如果父类没有无参构造方法(自己只定义一个有参构造方法,即可去除默认的无参构造方法),那么子类的构造方法会报错。如何解决?
解决方法:
1、在父类中加一个无参构造器
2、通过使用super关键字去显示调用父类的带参构造方法
3、子类通过this关键字去调用本类的其他构造方法,间接访问父类构造方法
class Father{
String name;
//带参构造器会取消默认的无参构造器
public Father(String name){
this.name = name;
System.out.println("Father 带参构造");
}
}
class Son extends Father{
public Son(String name){
super(name);
//如果没有上面那句话,系统会默认调用super(); ,但由于父类中没有无参构造,会报错
System.out.println("Son 有参构造");
}
}
总之:子类中的构造方法一定访问父类中的一个构造方法,否则父类数据就没有初始化
另外,需注意的是:super(…)或this(…)必须出现在第一条语句上,否则,就会出现父类数据多次初始化,报错("…"表示需传入的参数)
例子:
class Father{
public Father() {
System.out.println("Father 无参构造方法");
}
private void Fathe(String name) {
System.out.println("Father 带参构造方法");
}
public int num = 10;
}
public class Son extends Father{
public int num = 20;
public Son() {
System.out.println("Son 无参构造方法");
}
public Son(String name) {
System.out.println("Son 带参构造方法");
}
public void show() {
int num = 30;
System.out.println(num);
System.out.println(this.num);
System.out.println(super.num);
}
}
class Test{
public static void main(String[] args) {
Son son = new Son();
son.show();
System.out.println("------------------");
Son son2 = new Son("AAA");
}
}
运行结果:
成员方法
- 子类中的方法和父类中的方法声明不一样,就调用对应的方法
- 子类中的方法和父类中的方法声明一样,最终使用的是子类自己的方法
通过子类调用方法的查找顺序(找到就使用):
子类 -> 父类 -> 报错
其中涉及到了方法重写。
方法重写和方法重载的区别:
方法重写(Override):子类中出现了和父类中方法声明一模一样的方法
方法重载(Overload):本类中出现的方法名一样,参数列表不同的方法,与返回值无关
方法重写的应用:
当子类需要父类的功能,而功能主体子类有自己特有的内容时,可以重写父类中的方法。
这样,既沿袭了父类的功能,又定义了子类特有的内容
例子:
class Phone{
public void call(String name) {
System.out.println("call " + name);
}
}
class NewPhone extends Phone{
@Override
public void call(String name) { //方法重写
super.call(name);
System.out.println("呼叫过程中可以听到彩铃");
}
public void call() { //方法重载
System.out.println("打电话");
}
}
public class OverrideTest {
public static void main(String[] args) {
NewPhone np = new NewPhone();
np.call();
np.call("Jay");
}
}
运行结果:
- 方法重写的注意事项:
- 父类中私有方法不能被重写, 因为父类私有成员根本无法被继承
- 子类重写父类方法时,访问权限不能更低, 最好一致
- 父类静态方法,子类也必须通过静态方法重写(本质上不是方法重写)
子类重写父类方法时,最好声明一模一样
this 和 super 的使用
使用 | this | super |
---|---|---|
成员变量 | 调用本类的成员变量 | 调用父类的成员变量 |
构造方法 | 调用本类的构造方法 | 调用父类的构造方法 |
成员方法 | 调用本类的成员方法 | 调用父类的成员方法 |
具体的用法可参考:https://www.cnblogs.com/hasse/p/5023392.html
final 关键字
概述
由于继承中方法有一个现象:方法重写。所以,父类的功能,会被子类给覆盖掉。有些时候,我们不想让子类去覆盖父类的功能。这个时候,针对这种情况,Java提供了一个关键字:final。
final 可以修饰类,方法,变量。
特点
- final 可以修饰类,该类不能被继承
- final 可以修饰方法,该方法不能被覆盖重写
- final 可以修饰变量,该变量不能被重新赋值(这个变量其实是常量)
常量:
- 字面值常量
“hello” ,10 ,true- 自定义常量
final int x = 10;
注意事项
- final 修饰局部变量的问题:
- 基本类型:值不能发生改变
- 引用类型:地址值不能发生改变,但是,该对象堆内存中的内容可以修改
例子
class Student{
int age = 10;
}
class FinalTest{
public static void main(String[] args){
int x = 10;
x =100;
System.out.println(x);
final int y = 10 ;
//无法为最终变量赋值
//y = 100;
System.out.println(y);
Student s = new Student();
System.out.println(s.age);
s.age = 100;
System.out.println(s.age);
final Student ss = new Student();
System.out.println(ss.age);
ss.age = 100; //正确
System.out.println(ss.age);
//ss = new Student();//错误
}
}
- final 修饰变量的初始化问题:
- 被final修饰的变量只能赋值一次(不包括系统默认初始值)
- 对于非静态的常量,尽量在构造方法完毕前赋值。
面试题
1、看程序写结果:
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");
}
}
public class ExtendsTest {
public static void main(String[] args) {
Zi zi = new Zi();
}
}
结果:
分析要点:
A:一个类的静态代码块,构造代码块,构造方法的执行顺序:
静态代码块>构造代码块>构造方法
B:静态的内容随着类的加载而加载
静态代码块的内容会优先执行
C:子类初始化之前会先进行父类的初始化
从main()方法入口,执行Zi zi = new Zi();
时,程序加载Zi类,发现Zi类继承自Fu类,便把Fu类也加载了(实际的加载顺序是先父类后子类)。类加载后便会优先执行静态代码块。子类初始化之前会先进行父类的初始化,所以会先执行父类的构造代码块和构造方法,之后再执行子类的构造代码块和构造方法。
2、看程序写结果
class X {
Y b = new Y();
X(){
System.out.println("X");
}
}
class Y{
Y(){
System.out.println("Y");
}
}
public class Z extends X{
Y y = new Y();
Z() {
System.out.println("Z");
}
public static void main(String[] args) {
new Z();
}
}
结果:
分析要点:
A:成员变量的问题
int x = 10; //成员变量是基本类型
Student s = new Student(); //成员变量是引用类型
B:一个类中成员变量的初始化过程
默认初始化 -> 显示初始化 -> 构造方法的初始化
C:子类的初始化(分层初始化)
先进行父类初始化,后进行子类初始化
从main()方法入口,执行new Z();
时,加载类X和类Z,先执行类X的成员变量初始化,Y b = new Y();
创建类Y的对象b,调用构造方法,打印出“Y”,再执行类X的构造方法,输出“X”;类X初始化完毕后,执行类Z的初始化,Y y = new Y();
创建类Y的对象y,调用构造方法,打印出“Y”,再执行类Z的构造方法,输出“Z”。
关于Java中父类和子类的加载和初始化顺序,可参考:
https://blog.csdn.net/sunroyfcb/article/details/81637565
关于Java中初始化的本质,可参考:
https://blog.csdn.net/sunroyfcb/article/details/81637565