继承
为什么需要继承
Java中使用类对现实世界中实体进行描述,类经过实例化之后的产物对象,则可以用来表示现实世界的实体,但是现实世界错综复杂,事物之间可能有一些关联,那再设计程序时就需要考虑。
比如:狗和猫,他们都是动物。(每个动物都有共性,可以抽取出来它们的共性)
使用java语言对狗和猫进行描述,设计出:
//创建一个狗类 public class Dog { String name; int age; float weight; public void eat(){ System.out.println(name + "正在吃饭"); } public void sleep(){ System.out.println(name + "正在睡觉"); } void bark(){ System.out.println(name + "汪汪汪"); } } //创建一个猫类 public class Cat { String name; int age; float weight; public void eat(){ System.out.println(name + "正在吃饭"); } public void sleep(){ System.out.println(name + "正在睡觉"); } void mew(){ System.out.println(name + "喵喵喵~~~"); } }
通过狗类和猫类,我们发现大量代码出现重复,如图所示:
那如何能实现共性抽取呢?面向对象的思想中提出了继承的概念,专门用来进行共性抽取,实现代码复用。 (继承可以看作is-a关系,比如Dog is a animal)
继承的概念
继承(inheritance)机制:是面向对象程序设计使代码可以复用的重要手段,它允许程序员在保持原有类的个性的基础上进行扩展,增加新功能,这样产生的新的类,称派生类。继承呈现了面向对象程序设计的层次结构,体现了由简单到复杂的认知过程。继承解决的主要问题是:共性抽取,实现代码复用
例如:狗和猫都是动物,那么我们就可以将共性进行抽取,然后采用继承的思想来达到共用。
上述图示中,Dog和Cat都继承了Animal类,其中:Animal类称为父类/基类/超类,Dog和Cat可以称为Animal的子类/派生类,继承之后,子类可以复用父类中的成员,子类在实现时只需关心自己新增加的成员即可。
从继承概念中可以看出继承的最大作用是:实现代码复用,后面也应用于多态。
继承的语法
在java中如果要表示类的继承,需要用到extends关键字,具体如下:
修饰符 class 子类名称 extends 父类名称 { //... }
对之前的Dog类和Cat类通过继承重新设计:
//Animal.java public class Animal { String name; int age; public void eat(){ System.out.println(name + "正在吃饭"); } public void sleep(){ System.out.println(name + "正在睡觉"); } } //Dog.java public class Dog extends Animal { void bark() { System.out.println(name + "汪汪汪"); } } //Cat.java public class Cat extends Animal { void mew() { System.out.println(name + "喵喵喵"); } }
public class TestExtend { public static void main(String[] args) { Dog dog = new Dog(); //dog类中并没有定义任何成员变量,name和age属性肯定是从父类Animal中继承下来的 System.out.println(dog.name); System.out.println(dog.age); //dog访问的eat和sleep方法也是从Animal中继承下来的 dog.eat(); dog.sleep(); //bark是子类新增加的方法 dog.bark(); } }
注意:
1.子类会将父类中的成员变量或者成员方法继承到子类中了
2.子类继承父类后,建议添加自己特有的成员,体现出与基类的不同,否则没有继承的必要
父类成员的访问
在继承体系中,子类将父类中的方法和字段继承下来了,那在子类中能否直接访问父类中继承下来的成员呢?
子类中访问父类的成员变量
1.子类和父类不存在同名的成员变量
class Base { int a; int b; } public class Derived extends Base { int c; public void method() { a = 10;//访问从父类继承下来的a b = 20;//访问从父类继承下来的b c = 30;//访问子类自己的c } }
2.子类和父类成员变量同名
class Base { int a; int b; int c; } public class Derived extends Base { int a;//与父类中成员a同名,而且类型相同 char b;//与父类中成员b同名,而且类型不同 public void method() { a = 100;//使用的是子类新增的a b = 101;//使用的是子类新增的b c = 102;//使用从父类继承下来的c } }
通过上述栗子,我们可以得出以下规律:
1.如果访问的成员变量子类中有,则优先访问自己的成员变量
2.如果访问的成员变量子类中没有,则访问从父类继承下来的,如果父类也没有,则报错
3.如果访问的成员变量与父类中的成员变量同名,则优先访问自己的
简:成员变量的访问遵循就近原则,自己有则优先访问自己的,如果没有则在父类中找
子类中访问父类的成员方法
1.成员方法名字不同
总结:成员方法没有同名时,在子类方法中或者通过子类对象访问方法时,则优先访问自己的,自己没有时再到父类中找,如果父类中也没有则报错
2.成员方法名字相同
class Base { public void methodA() { System.out.println("Base中的methodA()"); } public void methodB() { System.out.println("Base中的methodB()"); } } public class Derived extends Base { public void methodA(int a) { System.out.println("Derived 中的methodA(int) 方法"); } public void methodB() { System.out.println("Derived 中的methodB() 方法"); } public void methodC() { methodA();//没有传参,访问父类中的methodA() methodA(20);//有传参,访问子类中的methodA(int) methodB();//直接访问,则永远访问到的都是子类中的methodB(),永远无法访问到基类的 } public static void main(String[] args) { Derived d = new Derived(); d.methodC(); } }
总结:通过派生类对象访问父类与子类相同名的方法时,如果父类和子类同名方法的参数列表不同,根据调用方法时传递的参数选择合适的方法进行访问,如果没有则报错。
那么有的人会问,如果成员的访问遵循就近原则,那么如果想访问父类中同名的成员应该怎么办?
这就需要super关键字
super关键字
使用场景:子类和父类中可能存在相同名称的成员,需要在子类方法中访问与父类同名的成员,这时就需要super,该关键字的主要作用:在子类中访问父类的成员。
class Parent { int value = 10; public void methodA() { System.out.println("Parent中的methodA()"); } } class Child extends Parent { int value = 20; public void methodA() { System.out.println("Child中的methodA()"); } void printValues() { System.out.println("Child value: " + value); // 子类字段 System.out.println("Parent value: " + super.value); // 父类字段 methodA();//子类方法 super.methodA();//父类方法 } public static void main(String[] args) { Child c = new Child(); c.printValues(); } }
执行结果:
在子类方法中,如果想要明确访问父类中的成员时,借助super关键字即可。
注意事项:
1.只能在非静态的方法中使用
2.在子类方法中,调用父类的成员变量和方法
子类构造方法
父子父子,先有父再有子,即:子类对象构造时,需要先调用基类构造犯法,然后执行子类的构造方法。
class Base { public Base() { System.out.println("Base()"); } } public class Derived extends Base { public Derived() { //super();//注意子类构造方法中默认会调用基类的无参构造方法:super(); //用户没有写时,编译器会自动添加,而且super()必须是子类构造方法中的第一条语句 //并且只出现一次 System.out.println("Derived()"); } public static void main(String[] args) { Derived d = new Derived(); } }
执行结果:
在子类构造方法中,并没有写任何关于基类构造的代码,但是在构造子类对象时,先执行基类的构造方法,然后执行子类的构造方法,因为:子类对象中成员是由两部分组成的,基类继承下来的以及子类新增加的部分。父子父子必是先有父后有子,所以在构建子类对象的时候,先要调用基类的构造方法将从基类继承下来的成员构造完整,然后再调用子类自己的构造方法,将子类自己新增加的成员初始化完整。
注意:
1.若父类显式定义无参或者默认的构造方法,在子类构造方法第一行默认有隐含的super()调用,即调用基类的构造方法
2.如果父类构造方法是带有参数的,此时需要用户为子类显式定义的构造方法,并在子类构造方法中选择合适的父类构造方法调用,否则编译失败
3.在子类的构造方法中,super(..)调用父类构造时,必须是子类构造方法中的第一条语句
4.super(...)只能在子类构造中出现一次,并且不能和this同时出现
super和this
super和this都可以在成员方法中用来访问:成员变量和调用其他成员方法,都可以作为构造方法的第一条语句,那它们之间有什么区别呢?
相同点:
1.都是Java的关键字
2.只能在类的非静态方法中使用,用来访问非静态成员方法和字段
3.在构造方法中使用时,必须是构造方法中的第一条语句,并且不能同时存在
不同点:
1.this是当前对象的引用,当前对象即调用实例方法的对象,super相当于是子类对象中从父类继承下来的部分成员的引用
2.在非静态成员方法中,this用来访问本类的方法和属性,super用来访问父类的方法与属性
3.在构造方法中:this(...)用来调用本类的构造方法,super(...)用来调用父类的构造方法,两种调用不能同时在构造方法中出现
4.构造方法中一定会存在super(...)的调用,用户没有写编译器也会增加,而this(..)不写则没有
再谈初始化
还记得之前讲过的代码块吗?我们来简要回顾一下几个重要的代码块:实例代码块和静态代码块。
我们之前讲过在没有继承关系下的执行顺序。
1.静态代码块先执行,并且只执行一次,在类的加载阶段执行
2.当有对象创建时,才会执行实例代码块,实例代码块执行完成后,构造方法执行
那么如果现在有继承关系,那它们的执行顺序又是什么?让我们看看下面的代码:
class Person { public String name; public int age; public Person(String name, int age) { this.name = name; this.age = age; System.out.println("Person:构造方法执行"); } { System.out.println("Person:实例代码块执行"); } static { System.out.println("Person:静态代码块执行"); } } public class Student extends Person { public Student(String name, int age) { super(name, age); System.out.println("Student:构造方法执行"); } { System.out.println("Student:实例代码块执行"); } static { System.out.println("Student:静态代码块执行"); } } public class Test1 { public static void main(String[] args) { Student s1 = new Student("zhangsan",19); System.out.println("-----------------------------"); Student s2 = new Student("lisi",22); } }
执行结果:
通过分析结果,得出以下结论:
1.父类静态代码块优先于子类静态代码块执行,而且是最早执行
2.父类实例代码块和父类构造方法紧接着执行
3.子类实例代码块和子类构造方法紧接着执行
4.第二次实例化子类对象时,父类盒子类的静态代码块都将不会执行
protected关键字
- 成员可见性: 使用
protected
关键字修饰的成员(字段或方法)可以被同一个包内的其他类访问,以及继承自该类的子类访问。- 访问权限范围:
protected
修饰的成员在同一个包内是可见的,同时也对继承关系中的子类可见,即使子类位于不同的包内。- 使用举例:
package com.example; // 包名 public class Parent { protected int value; protected void printValue() { System.out.println("Value: " + value); } } package com.example; // 同一个包 public class Child extends Parent { void accessParent() { value = 10; // 访问父类字段 printValue(); // 调用父类方法 } } package otherpackage; // 不同包 import com.example.Parent; public class OtherChild extends Parent { void accessParent() { value = 20; // 访问父类字段 printValue(); // 调用父类方法 } }
继承方式
在Java中有以下几种继承方式:
注意:java不支持多继承
我们写的类是现实事物的抽象,而我们真正在公司中所遇到的项目往往业务比较复杂,也会涉及到一系列复杂的概念,都需要我们用代码表示,所以在实际项目中写的类比较多,类之间的关系也十分复杂
但是即使如此,我们并不希望类之间的继承层次太复杂,一般我们不希望超出三层的继承关系,如果继承层数过多,就考虑对代码进行重构了。
如果想从语法上限制继承,就可以使用final关键字
final关键字
final关键字可以用来修饰变量,成员方法和类。
1.修饰变量和字段,表示常量(即不能修改)
final int a = 10;
a = 20;//编译出错
2.修饰类,表示此类不能被继承
final public class Animal {
...
}
public class Bird extends Animal {
...
}
//编译出错
我们平时用的String字符串类,就是用final修饰的,不能被继承
3.修饰方法:表示该方法不能被重写(后面介绍)
继承与组合
和继承类似,组合也是一种表达类之间关系的方式,也是能够达到代码重用的效果。组合并没有涉及到特殊的语法(诸如extends关键字),仅仅是将一个类的实例作为另一个类的字段
继承表示对象之间是is-a的关系,比如:狗是动物,猫是动物。
组合表示对象之间是has-a的关系,比如:汽车有轮胎,汽车有发动机。
举个例子:汽车和其轮胎,发动机,方向盘,车载系统等的关系就应该是组合,因为汽车是由这些部件组成的。
//轮胎类 class Tire { //... } //发动机类 class Engine { //... } //车载系统类 class VehicleSystem { //... } class Car { private Tire tire;//可以复用轮胎中的属性和方法 private Engine engine;//可以复用发动机中的属性和方法 private VehicleSystem vs;//可以复用车载系统类中的属性和方法 //。。。 } public class Benz extends Car { //将汽车中的轮胎,发动机,车载系统全部继承下来 }
组合和继承都可以实现代码的复用,应该使用继承还是组合,需要根据应用场景来选择。一般建议:能用组合尽量用组合。