继承是什么?
面向对象三大巨头知识点----继承
有一个大黄猫类:
属性有:名字,年龄,颜色
方法有:吃鱼,跳,跑
还有一个大白猫类:
属性有:名字,年龄,颜色
方法有:吃猫罐头,跳,跑
可以看到除了吃的方法不一样,其他的几乎都是一样的,那么如果使用代码写出来,就会重复的工作,代码耦合度较高。两只猫或许浪费的时间不多,但如果是成百上千个这种猫类,就比较繁琐。
因此,为了缓解代码的耦合度,可以使用继承这个特性来解决。
继承的概念
继承可以解决代码复用,让编程更接近人类思维,当多个类存在相同的属性和方法时,可以从这些类中抽象出父类,在父类中定义相同的属性和方法,所有的子类不需要重新定义这些属性和方法,只需要通过extends关键字来声明继承符类即可
- 当子类继承父类时,就会自动拥有父类定义的属性和方法
- 父类又叫 超类 基类
- 子类又叫派生类
继承的基本语法
class 子类名 extends 父类名{
}
继承示意图
开头说的那个案例:我们可以将耦合度高的属性和方法抽象出来,重新定义一个父类Cat类,后面所有的猫类不管是黄猫 白猫 小猫 大猫,都继承Cat类。这样就不用再次定义重复的属性和方法了
- 当子类继承父类后也可以再定义自己的特有的属性和方法。
- 当子类既有父类的属性和方法,也定义了自己特有的属性和方法再继承给别的类时,别的类就会同时有顶级父类和继承父类的所有属性和方法
C可以再往下继承,属性方法继续叠加。注意不同分支继承下去的,属性不会继承(例如P类不会有C类的属性)
继承入门案例
以开头说到的猫类问题,使用继承解决代码复用性高的问题
有一个大黄猫类:
属性有:名字,年龄,颜色
方法有:吃鱼,跳,跑
还有一个大白猫类:
属性有:名字,年龄,颜色
方法有:吃猫罐头,跳,跑
根据以上信息发现可抽象出来的属性有:名字,年龄,颜色 方法有跳,跑
因此可以定义一个父类 Cat类 然后创建大黄猫类和大白猫类继承Cat类
//父类
public class Cat {
String name;
int age;
String color;
public void run(){
System.out.println("正在跑~~");
}
public void jump(){
System.out.println("正在跳~~");
}
}
//子类
public class WhiteCat extends Cat{
public void eat(){
System.out.println("吃罐头");
}
}
//子类2
public class YellowCat extends Cat{
public void eat(){
System.out.println("吃鱼");
}
}
//测试类
public class TestExtends {
public static void main(String[] args) {
WhiteCat xiaobai = new WhiteCat();
xiaobai.name = "大白猫";
xiaobai.age = 4;
xiaobai.color = "白色";
System.out.println("白猫信息如下:"+xiaobai.name+xiaobai.color+xiaobai.age+"岁");
xiaobai.eat();
xiaobai.run();
YellowCat xiaohuang = new YellowCat();
xiaohuang.name = "大黄猫";
xiaohuang.age = 5;
xiaohuang.color = "黄色";
System.out.println("黄猫信息如下:"+xiaobai.name+xiaobai.color+xiaobai.age+"岁");
xiaohuang.eat();
xiaohuang.run();
}
}
输出结果:
可以看到,虽然两只猫类没有定义属性,还是可以输出赋值,这就是因为继承了Cat的属性和方法。同时也定义了自己的方法eat。
- 注意点:子类继承父类属性是copy的形式,连同父类赋的值一起copy。需要自己修改成自己想要的值。
假设将父类Cat类的name属性赋值cat112。然后在测试类中实例化,先不修改自己的name直接输出看看,然后修改再输出看看
public class TestExtends {
public static void main(String[] args) {
WhiteCat xiaobai = new WhiteCat();
YellowCat xiaohuang = new YellowCat();
System.out.println(xiaobai.name);
xiaobai.name = "白色";
System.out.println(xiaobai.name);
System.out.println(xiaohuang.name);
}
}
输出结果
可以看到没有自己修改前,输出的值是父类自己的值。修改后才变成了自己的。没有修改的黄猫的name还是父类的值
因此得出结论:子类继承父类的属性连同赋的值会随着变量一起copy到对象中,需要自己修改。
继承细节
继承细节一:父类的不同修饰符,子类该如何访问。以及父类子类不同包的解决方案
- 子类继承了父类的所有属性和方法,权限内的属性和方法可以直接在子类访问。权限外的需要通过父类提供public修饰的方法去访问。
列如 父类中分别定义了:
public int n1;
protected int n2;
int n3;
private int n4
那么其中n1和n2,不管子类和父类同不同包,都可以直接访问。
n3因为是默认修饰符,只有父类和子类在同一个包内,才可以访问。不在同包,子类不可以直接访问,需要通过父类通过的公开方法才能访问。
n4 不管同包还是不同包,子类都只能通过父类提供的公开方法访问
对于父类的的私有属性,或者是子类没有权限访问的属性。都可以通过跟封装一样的set/get方法来获取和进行修改。
继承细节二:子类继承父类时,构造器的细节
重点要素:子类继承父类时,必须先调用一次父类的构造器(默认调用父类的无参构造器)
- 子类继承父类时,首先会调用父类的构造器,然后再调用自己的构造器。
案例演示:
//父类
public class Boos {
public Boos(){//父类的无参构造器
System.out.println("父类的无参构造器被调用");
}
}
//子类
public class Ceo extends Boos{
public Ceo(){
System.out.println("子类Ceo的无参构造器被调用");
}
}
//测试类
public class Test02 {
public static void main(String[] args) {
Ceo ceo = new Ceo();
}
}
输出效果
- 因为子类继承父类时会默认调用父类的无参构造器,但是又因为当在类中自己手写了有参构造器,无参构造器就会消失这个特性。所以会导致当父类有有参构造器而无参构造器没有写出来时,子类继承父类时就会报错:找不到父类的无参构造器。
所以为了解决这个问题有两种方法:
- 将父类的无参构造器再写出来
- 在子类的构造器中使用super关键字代入对应的父类有参构造器的参数,以达到让编译器使用父类的有参构造器的切换。
案例演示:
//父类
public class Boos {
public String name;
public Boos(String name){//父类的有参构造器
this.name = name;
System.out.println("父类的有参构造器被调用");
}
}
//子类
public class Ceo extends Boos{
public Ceo(){
super("张三");
System.out.println("子类Ceo的无参构造器被调用");
}
}
//测试类
public class Test02 {
public static void main(String[] args) {
Ceo ceo = new Ceo();
}
}
因为子类继承父类时,通过super触发了父类的有参构造器。所以父类的name被赋值了张三,然后再传给子类Ceo所有属性和方法,因此ceo的name也是张三
- 当父类和子类都有相同参数的有参构造器,实例化子类对象时会发生什么
- 例如一个父类Boss类,手写出他的有参构造器用于构造器修改属性
//父类
public class Boos {
public String name;
public Boos(String name){//父类的有参构造器
this.name = name;
System.out.println("父类的有参构造器被调用");
}
2.子类同时也写出子类的有参构造器用于构造器修改属性
//子类
public class Ceo extends Boss{
public Ceo(String name){
super("张三");
System.out.println("子类此时的name是:"+this.name);
this.name = name;
System.out.println("子类Ceo的有参构造器被调用");
}
}
- 然后在测试类中实例化子类,并输出子类的name属性
public class Tesst03 {
public static void main(String[] args) {
Ceo c1 = new Ceo("李四");
System.out.println(c1.name);
}
}
输出结果:
通过输出结果和debug可以看到,通过实例化子类,首先调用的是父类的构造器,将参数穿入父类的构造器,变成张三,然后再调用子类的构造器传入参数变成李四。
通过以上案例测试可以清晰的发现,实例化子类传入的实参( Ceo c1 = new Ceo(“李四”);)是传入子类的构造器的,而父类实际的实参需要在子类的构造器中由super定义。不通用
- 因此单想要指定调用调用父类的某个构造器时,需在子类的构造器中显示调用:super(参数列表);
super关键字语句只能写在构造器中,且只能是第一条语句
因为super的这个特性和this关键字在构造器中调用另一条构造器的特性相同,都只能是第一条语句,因此this();和super();只能存在一个,否则会冲突。
那么 可以实验以下super关键字不显写出来,而是让子类默认的去调用父类无参构造器,然后再写this看看会不会冲突。
//父类
public class Boos {
public String name;
public Boos(){//父类的有参构造
System.out.println("父类的无参构造器被调用");
}
}
//子类
public class Ceo extends Boos{
public Ceo(){
this("张三");
System.out.println("子类Ceo的无参构造器被调用");
}
public Ceo(String name){
this.name = name;
System.out.println("子类Ceo的有参构造器被调用");
}
}
//测试类
public class Test02 {
public static void main(String[] args) {
Ceo ceo = new Ceo();
System.out.println(ceo.name);
}
}
输出结果
可以看到没有编译错误,也就是说如果要在子类构造器中使用this调用其他构造器,那么就不能写super指定调用父类构造器,只能让编译器默认调用父类的无参构造器。
java所有的类都是Object类的子类,Object类是所有类的基类
- 父类构造器的调用不限于直接父类,它会一直向上追溯直到Object类(顶级父类),然后从Object的构造器开始,依次往下进行调用构造器
举例说明:
例如 上面案例的Boos类和它的子类Ceo类,Boos类也是继承于Object类。当实例化子类Ceo时,因为子类中有默认调用的父类构造器,所以会找到Boos的构造器,同理Boos的构造器中同样也有对Object的默认调用构造器。然后从object–>Boos–>Ceo依次进行构造器的调用
继承细节三:子类最多只能直接继承一个父类,java是单继承机制
现在有三个类 A类 B类 C类
要求让 B类同时有C类A类的属性和方法
根据java中单继承的规定,不可以直接B类继承C类的同时又继承A类,所以我们可以A类继承C类,然后B类继承C类。这样就实现了要求
ps:虽然java不允许多继承,但是可以实现多个接口
继承不可以滥用,必须满足is a逻辑关系
例如 Person is a Muisc 人是一个音乐,显然是错误的 Cat is a Animal 猫是一个动物,就满足了is a逻辑 Cat类就可以继承Animal类
继承的本质分析
当实例化一个子类的时候内存发生了什么?
先看以下代码
Son - extends-Father-extends-GrandPa
//爷爷类
public class GrandPa {
String name = "大头爷爷";
String hobby = "旅游";
}
//父类
public class Father extends GrandPa{
String name = "大头爸爸";
int age = 31;
}
//子类
public class Son extends Father{
String name = "大头儿子";
}
//测试类实例化子类
Son son = new Son();
此时在内存中依次执行了哪些操作:
- 在方法区依次加载:Object类 GrandPa类 Father类 Son类
- 在堆中开辟空间,并在此空间内划分不同区域依次存放各个继承类的属性
- 将此空间的地址给到Son
图示:
从示意图可以看到在堆中开辟的只有一个空间,而在这个空间中有不同的区域划分,以防止同名的属性冲突。
- 那么问题就随之而来了如果在测试类实例化son类后,输出son.name。到底输出的是哪个的name呢。
Son son = new Son();
System.out.println(son.name);
输出结果为 大头儿子。
在继承中,查找每个属性是按照查找关系来返回信息的
步骤会如下:
- 首先查看子类自己有没有这个属性
- 如果子类有,且可以访问,则就返回信息
- 如果子类没有这个属性,就看父类(Father类)有没有也
- 如果父类(Father类)也没有,就继续向上查找GrandPa类,再没有就Object类。
也就是依向上查找,并且一旦找到之后如果可以访问就直接返回,如果是不可以访问的修饰符,也不会向上查找另一个了,就直接就地报错没有权限。
- 就像如果要输出子类son的age,最终会输出 31
查找 hobby
如果再查找过程中发现查找的属性 找到了 但是修饰符是private无法访问,就会报错,只能通过父类提供的公开访问方法访问
继承练习
练习一:
以下代码会输出什么:
class A{
A(){
System.out.println("a");
}
}
class B extends A{
B(){
this("abc");
System.out.println("b");
}
B(String name){
System.out.println("b name");
}
}
//测试类
B b = new B();
a
b name
b
练习二:
练习当子类中有this调用其他构造器时super的位置
看下面代码会输出什么
class A{
A(){
System.out.println("A类的无参构造");
}
}
class B extends A{
B(){
System.out.println("B类的无参构造");
}
B(String name){
System.out.println("B类的有参构造");
}
}
class C extends B{
C(){
this("hha");
System.out.println("C类的无参构造器");
}
C(String name){
super("554");
System.out.println("C类的有参构造器");
}
}
//测试类
C c = new C();
执行步骤分析
1.进入c的构造器,此时发现this调用的另一个构造器中有super指向B类的有参构造器。
2.跟着this到C的有参构造器,通过super到B类的有参构造器在执行B类的有参构造之前再跟随默认的super到A类的无参构造器
3.到A类的无参构造器 输出—A类的无参构造
4.再返回到B类的有参构造器 输出 ----B类的有参构造
5.再返回到C类的有参构造器输出—C类的有参构造器
6.this执行完毕返回到C类的无参构造器输出—C类的无参构造器
所以结果为:
A类的无参构造
B类的有参构造
C类的有参构造
C类的无参构造器
总之:实例化子类时,依次从顶级父类开始依次往下执行构造器,当父类重载了构造器时,根据子类构造器中的super参数匹配执行具体的构造器,子类没有传入参数,默认执行无参构造器。
练习三:
编写Computer类,包含Cpu 内存,硬盘等属性,创建getDetails方法用于输出Computer的详细信息
编写EtPc子类继承Computer类,添加特有属性-品牌brand
编写RcPc子类继承Computer类,添加特有属性-颜色color
编写Test04类在main方法中创建EtPC和RcP对象,分别给对象中特有的属性赋值,且给Computer类继承来的属性赋值,并使用方法打印输出信息。
//Computer类
public class Computer {
String Cpu;
String Ram;
String Disk;
Computer(String Cpu,String Ram,String Disk){
this.Cpu = Cpu;
this.Ram = Ram;
this.Disk = Disk;
}
public String getDetails(){
return "CPU="+Cpu+"内存="+Ram+"硬盘="+Disk;
}
}
//EtPc子类
public class EtPC extends Computer{
private String brand;
EtPC(String Cpu,String Ram,String Disk,String brand){
super(Cpu,Ram,Disk);
setBrand(brand);
}
public String printInfo(){
return getDetails()+"品牌="+brand;
}
public void setBrand(String brand) {
this.brand = brand;
}
}
//RcPc子类
public class RcPc extends Computer{
private String color;
RcPc(String Cpu,String Ram,String Disk,String color){
super(Cpu,Ram,Disk);
setColor(color);
}
public String printInfo(){
return getDetails()+"颜色=" + color;
}
public void setColor(String color) {
this.color = color;
}
}
//测试类
public class Test04 {
public static void main(String[] args) {
EtPC e1 = new EtPC("i9-12600K","32g","1t","联想");
System.out.println(e1.printInfo());
RcPc r1 = new RcPc("i5-12600k","16g","500g","黑色");
System.out.println(r1.printInfo());
}
}