今天学习的内容是继承
如果想要复用某段功能代码,可以把它封装成方法。那么如果把变量和方法封装成类,复用这个类而不破坏现有的程序代码岂不更好?Java提供了两种方式来复用类。第一种方法非常直观:只需将要复用的类的对象引用放在新类中即可,这种方法称为组合。第二种方法则更加细致:它按照现有类的类型来创建新类,这种方法称为继承。实际上在进行程序设计时,应该优先考虑组合而不是继承,因为组合更灵活。只有当子类不仅要保留父类内容,而且还要扩展新功能时,才使用继承。之前许多例子都使用过组合,而今天要讲的继承则要复杂一些,继承的知识点如下:
- 继承的格式为:class 子类 extends 父类
- Java只支持单继承,多继承会造成致命方块问题(不知道调用哪个父类的方法)
- 继承建立了子类与父类的关系,为实现多态提供了条件
- 所有类都隐式地继承了java.lang.Object类,所以它们可以使用Object类的toString()等方法
- 子类不能继承父类的构造函数
- 子类会继承父类的所有成员,包括private成员,但private成员只能被父类对象访问(private变量可以通过public的getter访问)
- 如果子类中有与父类同名且同参数列表的方法,子类方法将会覆盖父类方法
- 子类方法无法覆盖父类的private方法,如果子类中定义一个与它同名同参数列表的方法,就相当于子类定义了一个新方法
- 静态方法无法被覆盖!
- 覆盖方法可以修改方法内容、扩大存取权限、改变返回值类型(必须是原返回值类型的子类)
- 如果子类中有与父类同名的变量,子类变量不会覆盖父类变量,它们存在于子类对象的不同区域(实际开发中不会出现同名变量的情况,会造成混淆,仅仅是面试要点),子类引用会默认访问同名的子类变量
- 如果想在子类访问父类方法和与子类变量同名的父类变量,可以使用super.方法和super.变量
- 创建子类对象的步骤:加载父类-->加载子类-->执行父类构造函数-->执行子类构造函数
- 当创建子类对象后,该对象包含了一个父类的子对象,这个对象与你用父类直接创建的对象是一样的,可以用super关键字获取其中的内容(但实际上你还是只创建了一个子类对象)
- 编译器会自动在子类构造函数的第一行添加super()来调用父类的无参构造函数,用来初始化父类的子对象。如果父类没有定义无参构造函数,必须在子类构造函数的第一行显式地添加super(参数)来调用父类的有参构造函数,进行父类子对象的初始化
- super关键字不能在静态方法里使用(与this同理)
- 面试要点:子类实例变量的默认初始化是在开辟内存空间分配内存地址时进行的,在调用父类构造函数进行父类子对象的初始化时,子类实例变量的指定初始化还没有进行,它的值为本类型默认值。只有当父类子对象初始化完毕,才会执行子类实例变量的指定初始化。然后执行的是构造代码块初始化,最后进行子类构造函数初始化。
示例程序及运行结果:
class Fu{
//父类的实例变量
private String s="fulei";//private变量虽然可以继承,但只能被父类对象直接访问
int a;
protected int b;
public int c;
//静态代码块,类加载时执行
static{
System.out.println("父类加载");
}
//父类构造函数
public Fu(int x){
aa();//这里是由子类对象的引用来调用父类的构造函数,this指向的是子类对象,所以调用的是子类中覆盖方法
System.out.println("父类的有参构造函数");
}
public void aa(){
System.out.println("父类的aa()方法");
}
void bb(){
System.out.println("父类的bb()方法");
}
public Fu cc(){
return new Fu(1);
}
@SuppressWarnings("unused")
private void siyouff(){
System.out.println("父类的私有方法");
}
public String getS() {
return s;
}
public void setS(String s) {
this.s = s;
}
}
public class Zi extends Fu{
//子类的实例变量
private String s="zilei";
private int a=1;
private int b=1;
private int c=1;
private int num=1;
//静态代码块,类加载时执行
static{
System.out.println("子类加载");
}
//构造代码块
{
System.out.println("使用构造代码块进行初始化时,num="+num);
num=10;
}
public Zi(){
super(10);//父类没有无参构造函数时,必须在子类构造函数的第一行显式地添加super(参数)来调用父类的有参构造函数
//初始化父类子对象时,子类实例变量并未指定初始化。等父类初始化完毕后,才执行子类的指定初始化
//指定初始化之后,进行构造代码块初始化
System.out.println("使用子类构造函数进行初始化时,num="+num);//此时num已经完成了指定初始化,值为1
}
public Zi(int x){
super(x);
System.out.println("子类有参构造函数,参数为"+x);
}
public static void main(String[] args){
System.out.println("main()方法");
Zi zilei=new Zi();
zilei.aa();//子类的aa()方法1 同名同参数列表的方法会被覆盖
System.out.println(zilei.s);//zilei 子类对象会默认访问同名的子类变量
System.out.println(zilei.getS());//fulei 父类的private变量会被继承,且没有被覆盖
System.out.println(zilei.b);//1 子类对象会默认访问同名的子类变量
System.out.println(zilei.a);//1 子类对象会默认访问同名的子类变量
System.out.println(zilei.c);//1 子类对象会默认访问同名的子类变量
zilei.bb();//父类中的b为0 同名变量不会被覆盖,并且使用super.变量可以访问父类的同名变量
//!super.aa(); super关键字不能在静态方法里使用
Zi zi=new Zi(99);
System.out.println(zi);//所有类都继承了Object类,所以可以使用Object类的toString()等方法
}
//本方法第一次调用时,是在父类构造函数中,此时num只被默认初始化为0,还没有进行指定初始化,必须等待父类子对象初始化完成
//本方法第二次调用时,父类初始化已经完成,num被指定初始化为1
public void aa(){
super.aa();//使用super.方法可以调用父类的同名方法
System.out.println("子类的aa()方法,此时num="+num);
}
//扩大存取权限
public void bb(){
System.out.println("父类中的b为"+super.b);
}
//改变返回值类型为原返回值类型的子类
public Zi cc(){
return new Zi();
}
public void siyouff(){} //子类无法覆盖父类的private方法,这里相当于定义了一个子类新方法
}
面试题:
1.继承的坏处?
答:实际上在进行程序设计时,应该优先考虑组合而不是继承,因为组合更灵活。只有当子类不仅要保留父类内容,而且还要扩展新功能时,才使用继承。
2.子类对象实例化过程(重要)
答:父类静态代码(静态代码块和静态变量,按顺序)--子类静态代码(静态代码块和静态变量,按顺序)--父类非静态代码(构造代码块,实例变量,按顺序)--父类构造函数--子类非静态代码(构造代码块,实例变量,按顺序)--子类构造函数