Java继承详解

简介: Java继承详解

继承

为什么需要继承

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关键字

  1. 成员可见性: 使用 protected 关键字修饰的成员(字段或方法)可以被同一个包内的其他类访问,以及继承自该类的子类访问。
  2. 访问权限范围: protected 修饰的成员在同一个包内是可见的,同时也对继承关系中的子类可见,即使子类位于不同的包内。
  3. 使用举例:
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 {
    //将汽车中的轮胎,发动机,车载系统全部继承下来
}

组合和继承都可以实现代码的复用,应该使用继承还是组合,需要根据应用场景来选择。一般建议:能用组合尽量用组合

相关文章
|
16天前
|
Java
Java中的继承和多态是什么?请举例说明。
Java中,继承让子类继承父类属性和方法,如`class Child extends Parent`,子类可重写父类方法。多态允许父类引用指向子类对象,如`Animal a = new Dog()`,调用`a.makeSound()`会根据实际对象类型动态绑定相应实现,增强了代码灵活性和可扩展性。
10 0
|
23天前
|
搜索推荐 Java
Java的面向对象特性主要包括封装、继承和多态
【4月更文挑战第5天】Java的面向对象特性主要包括封装、继承和多态
15 3
|
1月前
|
Java 程序员 编译器
Java继承与多态知识点详解
本文主要讲解的是Java中继承与多态的知识点
|
1月前
|
Java C++ iOS开发
java为何不支持多继承
java为何不支持多继承
25 0
|
1月前
|
Java 程序员 编译器
【详识JAVA语言】面向对象程序三大特性之二:继承
【详识JAVA语言】面向对象程序三大特性之二:继承
47 2
|
3天前
|
安全 Java
Java基础&面向对象&继承&抽象类
Java基础&面向对象&继承&抽象类
|
3天前
|
Java
【Java基础】详解面向对象特性(诸如继承、重载、重写等等)
【Java基础】详解面向对象特性(诸如继承、重载、重写等等)
8 0
|
1月前
|
Java
java中的继承
java中的继承
10 1
|
1月前
|
Java
Java类的继承
Java类的继承
7 0
|
1月前
|
Java
JAVA类的继承
JAVA类的继承
15 1