面向对象的三大特征:封装性、继承性、多态性。
继承是多态的前提,如果没有继承,就没有多态。
继承是什么
- 这里说一个师傅和徒弟的例子。师傅把自己所有会的技艺都教给了徒弟,那从徒弟的角度来看,我们就可以说徒弟继承了师傅的技艺。在面向对象中,师傅叫做父类、基类、超类,徒弟就叫做子类、派生类。这里父类和子类,并不能对应于现实生活中的儿子继承家产的例子。儿子继承家产,儿子拿了钱,父亲就没钱了。在面向对象中,继承强调的是一种行为和属性的复制。如果硬要举父子的例子,你可以说父亲不秃顶,儿子也不秃顶,继承了他优秀的基因。父亲能运动吃饭,儿子也能运动吃饭,这个例子就比较合适。
继承解决了什么问题
- 还是说上面那个师傅和徒弟的例子。一方面,师傅一个人学习了技艺,他就可以把这种能力传授给很多徒弟。表现在面向对象中,就是父类完成了共性抽取,子类通过继承父类就可以获得这种共性。另一方面,我们知道徒弟不仅仅获得了师傅的技艺,他还有可能拥有自己独特的技艺。注意,这里独特的技艺并不是直接通过其它师傅得到的,java 中类与类之间只允许单继承,不允许有多个直接父类,也就是说,徒弟不能直接拜多个师傅。但是可以间接有多个师傅,比如
徒弟->师傅b->师傅a
,徒弟继承师傅a,师傅a继承师傅b。再回到徒弟独特的技艺上说,子类通过继承父类,不仅仅拥有了父类的成员变量和成员方法,还可以拥有自己独特的成员变量和方法,这样就完成了对父类的扩展。
继承的格式
public class 子类名 extends 父类名称 {//类内容}
public class Fu {
int numFu;//父类成员变量
public void methodFu() {
System.out.println("这里是父类成员方法");
}
}
public class Zi extends Fu {
}
public class Demo {
public static void main(String[] args) {
Zi zi = new Zi();
zi.numFu = 1;
zi.methodFu();
}
}
- 在上面的 demo 中我们可以看到,子类继承父类后就可以访问 父类的成员变量和成员方法。
继承中成员方法的访问特点
public class Fu {
public void method() {//父子都有
System.out.println("这里是父类method方法");
}
public void methodFu() {//父类独有,被继承后子类也会拥有
System.out.println("这里是父类methodFu方法");
}
}
public class Zi extends Fu {
public void method() {//父子都有
System.out.println("这里是子类method方法");
}
public void methodZi() {//子类独有
System.out.println("这里是子类methodZi方法");
}
}
- 上面的代码展示了成员方法存在的三种情况。
访问规则: 创建的对象是谁,就优先用谁中的方法。没有则向上找。
- 父类和子类有重名的方法。(这里涉及到一个重写(Override)的概念,下面会讲)
Fu fu = new Fu();
fu.method();//输出:"这里是父类method方法"
Zi zi = new Zi();
zi.method();//输出:"这里是子类method方法"。优先用Zi类中的method方法
- 父类拥有独有的方法,此时子类继承后也会拥有。
Fu fu = new Fu();
fu.methodFu();//输出:"这里是父类methodFu方法"。父亲调用自己的成员方法,当然可以。
Zi zi = new Zi();
zi.methodFu();//输出:"这里是父类methodFu方法" 。Zi类中没有methodFu方法,则向上找。
- 子类拥有独有的方法。
Fu fu = new Fu();
fu.methodZi();//错误,父类不会向下找子类中的方法
Zi zi = new Zi();
zi.methodZi();//输出:"这里是子类methodZi方法"
继承中成员变量的访问特点
- 直接通过 对象名.成员变量名 访问。
规则: new的时候等号左边是谁,就优先去谁中找相应的成员变量。没有则向父类找。
public class Fu {
int num;
int numFu;
}
public class Zi extends Fu {
int num;
}
public class Demo {
public static void main(String[] args) {
Fu fu = new Fu();
fu.num;//访问父类的num
Zi zi = new Zi();
zi.num;//访问子类的num
zi.numFu;//先去Zi类中找numFu,没有则向上找父类中的numFu。
}
}
2. 通过成员方法访问成员变量
规则: 方法属于谁,就优先去谁中找相应的成员变量。没有则向父类找。
public class Fu {
int num = 10;
public int methodFu(){
return num;
}
}
public class Zi extends Fu {
int num = 20;
public int methodZi(){
return num;
}
}
public class Demo {
public static void main(String[] args) {
Zi fu = new Zi();
zi.methodZi();//返回20,methodZi属于Zi类,所以会输出Zi中的num
zi.methodFu();//返回10,methodFu属于Fu类,所以会输出Fu中的num
}
}
方法重写
- 如果子类对父类继承过来的方法不满意,想要增加逻辑或者自己编写逻辑,就需要重写。
public class Fu {
public void method() {
System.out.println("这是父类method方法");
}
}
public class Zi extends Fu {
@Override//这个注解可以帮助我们检查父类是否有这个方法,同时表明这是一个覆写的方法。
public void method() {
System.out.println("这是子类method方法");
}
}
public class Demo {
public static void main(String[] args) {
Zi zi = new Zi();
zi.method();//输出:"这是子类method方法"
}
}
- 重写和重载的区别:
- 重写(Override):方法的名称一样,参数列表也一样。
- 重载(Overload):方法的名称一样,参数列表不一样。
- 方法重写的注意点:
- 子类必须保证重写的方法的名称和参数列表与父类的相同。
- 子类重写的方法的返回值必须小于等于父类方法的返回值范围。
public class Fu {
public Object method() {
return null;
}
}
public class Zi extends Fu {
@Override
public String method() {//这样写是可以的,如果父类是返回String,这里返回Object就不行了。
return null;
}
}
- 子类重写的方法的权限必须大于等于父类方法的权限修饰符。不过当父类方法的权限修饰符为 private 时,无法被重写。
权限修饰符 :public > protected > default(代表什么都不写) > private
public class Fu {
String method() {
return null;
}
}
public class Zi extends Fu {
@Override
public String method() {//这里权限修饰符不写,写protected,public都行
return null;
}
}
this 和 super 关键字
- this 代表本类对象,super 代表父类对象。
- 从成员变量的使用上来说:
public class Fu {
int num;
}
public class Zi extends Fu {
int num;
private void method(int num) {
System.out.println(num);//输出method方法上的num参数
System.out.println(this.num);//输出Zi类的成员变量num
System.out.println(super.num);//输出Fu类的成员变量num
}
}
- 从成员方法的使用来说:
public class Fu {
public void method() {}
}
public class Zi extends Fu {
public void method() {
super.method();//调用父类的method方法
this.methodZi();//调用本类的methodZi方法
}
public void methodZi() {}
}
- 从构造方法上来说:
public class Fu {
//父类无参构造
public Fu() { }
}
public class Zi extends Fu {
public Zi() {
super();//调用父类的无参构造
}
public Zi(int num) {
this(num,null);//调用下面的构造方法
}
public Zi(int num,Object obj) {
}
}
- this 和 super 关键字内存图解
讲到super关键字,在这就可以补充一个小知识点:为什么 java 类与类只允许单继承?假设一个类继承了两个父类,父类a有一个成员变量obj,父类b也有一个成员变量obj。那么在子类中调用super.obj
的时候就会出问题,我调用的到底是父类a的obj还是父类b的obj那?很明显,我们不清楚。所以java不允许多继承。
继承中构造函数的访问特点
明确一点:new 一个子类对象,会先去调用父类的构造方法,再调用子类的构造方法。没有子类,哪来父类。所以必须先有父类。所以在子类的构造方法中,一定要调用父类构造方法。
- 情形一:父类没写构造函数(没写会默认赠送一个无参构造)或者只写了一个无参构造。那么在子类的构造方法必须调用父类的无参构造。如果子类也没写构造函数:没写会默认赠送一个无参构造,并且在其中调用父类的无参构造。如果子类显式写了无参构造,那就必须调用父类的无参构造。如果子类写了有参构造,那就必须调用父类的无参构造。
public class Fu {
public Fu() {}//不写会赠送一个无参构造,如果写了有参构造就不会赠送了
}
public class Zi extends Fu {
public Zi() {//不写会赠送一个无参构造,如果写了有参构造就不会赠送了
super();//这行代码不写也行,会默认赠送一个
}
//这两个构造函数都行,明确我们的目的:构造出一个父类对象
public Zi(Object obj) {
super();//这行代码不写也行,会默认赠送一个
}
}
- 情形二:父类只有有参构造。此时不会给父类赠送一个无参构造了。所以在我们的子类构造函数中,必须调用父类的有参构造。
public class Fu {
public Fu(Object obj) {}
}
public class Zi extends Fu {
public Zi() {
super(null);
}
//这两种构造函数都行
public Zi(Object obj) {
super(obj);
}
}
- 情形三:父类显式写出无参构造,同时还有有参构造。子类此时没写构造函数,没写会默认赠送一个无参构造,并且在其中调用父类的无参构造。子类此时写了构造函数,可以在构造函数中调用父类的无参构造,也可以调用父类的有参构造。
public class Fu {
public Fu() {}
public Fu(Object obj) {}
}
public class Zi extends Fu {}//不会报错
public class Zi extends Fu {
public Zi() {}//会赠送一个super()
}
public class Zi extends Fu {
public Zi() {
super(null);
}
//这两个构造函数都行
public Zi(Object obj) {
super(obj);
}
}