Java 继承+final关键字

提示:文本为作者学习笔记,详略不均,大家可以根据目录调转到感兴趣部分进行学习!


一、继承

1.引入

想象我们养宠物,常见的有猫和狗。

狗的属性有名字、颜色、年龄…行为有吃、睡、狗叫…于是我们写出了如下一个Dog类:

class Dog {
    String name;
    int age;
    String color;
    
    public void eat() {
        System.out.println(this.name+"正在吃...");
    }
    
    public void sleep() {
        System.out.println(this.name+"正在睡...");
    }
    
    public void bark() {
        System.out.println(this.name+"正在汪汪叫...");
    }
}

猫的属性有名字、颜色、年龄…行为有吃、睡、喵喵叫…于是我们写出了如下一个Cat类:

class Cat {
    String name;
    int age;
    String color;

    public void eat() {
        System.out.println(this.name+"正在吃...");
    }

    public void sleep() {
        System.out.println(this.name+"正在睡...");
    }

    public void Miao() {
        System.out.println(this.name+"正在喵喵叫...");
    }
}

可以发现Dog类和Cat类的属性以及部分行为是完全相同的,对这些共性进行抽取,我们可以抽象出来一个Animal类:

class Animal {
    String name;
    int age;
    String color;

    public void eat() {
        System.out.println(this.name+"正在吃...");
    }

    public void sleep() {
        System.out.println(this.name+"正在睡...");
    }
}

这个Animal类包含了Dog类和Cat类的所有共性,或者说是Dog类和Cat类继承自Animal类的,除了这些共性,两者分别有其特性bark()方法和Miao()方法。在Java中提出了继承的概念来表示这种关系。

2.概念

继承机制:是面向对象程序设计使代码可以复用的最重要的手段,它允许程序员在保持原有类特性的基础上进行扩展,增加新功能,这样产生的新类,被称为派生类。继承主要解决的问题是:共性的抽取,实现代码的复用

在这里插入图片描述

在上面例子中,Animal类成为父类/基类/超类,Dog和Cat可以成为Animal的子类/派生类,继承之后,子类可以复用父类中的成员,子类在实现时只需要加上自己特有的成员即可。

3.语法

在Java中,用extends关键字表示继承关系:

class ParentClass {
    // 父类的成员变量和方法
}

class ChildClass extends ParentClass {
    // 子类特有的成员变量和方法
    // 同时继承了父类的非私有成员
}

那么对于上面猫和狗的例子,代码就可以改为:

class Animal {
    String name;
    int age;
    String color;

    public void eat() {
        System.out.println(this.name+"正在吃...");
    }

    public void sleep() {
        System.out.println(this.name+"正在睡...");
    }
}


class Dog extends Animal {
    public void bark() {
        System.out.println(this.name + "正在汪汪叫...");
    }
}

class Cat extends Animal {
    public void Miao() {
        System.out.println(this.name + "正在喵喵叫...");
    }
}

public class Inheritance {
    public static void main(String[] args) {
        Dog dog1=new Dog();
        dog1.name="小白";
        dog1.eat();
        dog1.bark();

        Cat cat1=new Cat();
        cat1.name="小花";
        cat1.sleep();
        cat1.Miao();
    }
}

可以看到,子类可以调用从父类继承过来的变量和方法。

4.如何访问父类成员

在继承关系中,子类继承了来自父类的字段和方法,那么还能访问到父类中被继承的成员吗?

(1)访问父类成员变量

假设我们有如下一个classA类,还有一个classB类继承自classA:

class ClassA {
    int a=1;
    int c=10;
}

class ClassB extends ClassA{
    int a=2;
    int b=3;
}

父类中有成员变量a和c,子类中有变量a和b。

猜测一下:对于子类自身没有而是继承自父类的变量c,若我用子类去访问c,那么拿到的一定是父类的c,值为10 ,对吧?

在这里插入图片描述

当然,结果完全符合我们的猜测。

那么对于父类有的,子类也有的同名变量a,访问结果是子类的还是父类的?

在这里插入图片描述

结果表示,它访问的是子类中的a,而非父类中的a。

[!TIP]

于是我们可以总结出通过子类对象访问父类成员时:

  • 如果访问的成员变量子类中有,那么就近优先访问自己的成员变量。
  • 如果访问的成员变量子类中没有,那么去父类中找,找到了就用父类的,没找到就报错。

(2)super关键字

在上一个小点,我们介绍了通过子类对象访问父类成员变量的情况。

在特定场景下,可能需要在子类方法中访问父类同名成员,直接访问是做不到的,这时应该怎么办?

Java提供了super关键字,super关键字代表父类对象的引用。主要作用作用就是在子类方法中能够访问父类的成员

示例:

class ClassA {
    int a=1;
    int c=10;
}

class ClassB extends ClassA{
    char a='z';
    int b=3;

    public void show() {
        System.out.println(this.a);
        System.out.println(super.a);
        System.out.println(this.b);
        System.out.println(super.c);
    }
}


public class Test {
    public static void main(String[] args) {
        ClassA classA=new ClassA();
        ClassB classB=new ClassB();
//        System.out.println(classB.a);
//        System.out.println(classB.c);
        classB.show();
    }
}

这里我们再利用这个例子细致地探讨一下this和super的关系:

在这里插入图片描述

this关键字在Java中代表当前对象的引用,它可以访问的范围包括子类自己定义的成员变量,还有从父类继承过来的那些,它都能访问得到,只是在子类中有同名变量时会优先选择使用子类的,不使用父类的。当子类没有但是父类中有时,this会访问到父类的并使用。

super关键字代表父类对象的引用,它只能访问到父类的变量,当你用super访问父类没有但是子类中有的变量时,会报错(如上图所示)。

图示:

在这里插入图片描述

[!IMPORTANT]

现阶段,我们还需要知道super的另一个作用:

在前面某篇博客中,讲解了构造方法,我们知道了构造方法是可以重载的,在一个构造方法中可以用this()来调用其他构造方法。

super()也可以调用其他方法,不过它是用于在子类构造方法中调用父类的构造方法。

与this()类似,super()中传几个参数,就调用有那几个参数的父类构造方法,在本文后面讲解子类构造方法时中会用到。

(3)访问父类成员方法

规律和访问父类成员变量大同小异:

都是优先在子类中找,在子类中找到方法名和参数列表都一致的就直接用,没找到(方法名和参数列表至少一个不符合)就去父类中找,找到了就用,还是没找到的话就报错。

class ClassA {
    public void methodA(){
        System.out.println("ClassA methodA");
    }
    public void methodA(int a){  //方法重载
        System.out.println("ClassA methodA int");
    }
}

class ClassB extends ClassA{
    @Override
    public void methodA(){      //方法重写
        System.out.println("ClassB methodA");
    }
    public void methodB(){
        System.out.println("ClassB methodB");
    }

    public void func() {
        this.methodA();
        super.methodA();
        this.methodA(1);
        this.methodB();
    }
}

public class Test {
    public static void main(String[] args) {
        ClassB classB=new ClassB();
        classB.func();
    }
}

运行结果如下:

在这里插入图片描述

5.如何进行子类构造

让我们回到最初的猫和狗的那个例子,现在我要为Dog和Cat类提供构造方法了:

class Dog extends Animal {
    public Dog(String name, int age, String color) {
        this.name = name;
        this.age = age;
        this.color = color;
    }

    public void bark() {
        System.out.println(this.name + "正在汪汪叫...");
    }
    
}

class Cat extends Animal {
    public Cat(String name, int age, String color) {
        this.name = name;
        this.age = age;
        this.color = color;
    }
    
    public void Miao() {
        System.out.println(this.name + "正在喵喵叫...");
    }
}

我为Dog类和Cat类各自都添加了前一个带有三个参数的构造方法,到目前为止一切正常,使用也很正常:

在这里插入图片描述

那么这有什么好讲的呢?平平无奇嘛…

别急,当我给父类Animal类加上有参构造方法时:
在这里插入图片描述

你就发现:两个子类的构造方法就莫名其妙报红了??那删掉,把子类的构造方法删掉呢?

在这里插入图片描述

怎么还是报错啊?把鼠标放上去看看原因:它说什么,Animal类中没有无参的构造器可用…解决方案是创建与父类匹配的构造器,这到底是什么原理?

  1. 首先,我先解释为什么一开始没有给父类添加任何构造方法时,完全不影响子类的构造。

还记得之前讲解构造方法时,我们知道当你没有手动添加任何构造方法时,编译器会自动提供一个无参的构造器,没有任何参数,方法体里没有任何操作。这时的Animal类中就存在这么一个无参构造方法。

这种情况下Java编译器会自动在子类构造方法的第一行插入一个隐式的super()调用,即调用父类的无参构造方法。

这一步是必要的,Java规定在子类对象构造时,需要先调用其基类构造方法,然后再执行子类的构造方法。毕竟父子父子,现有父再有子。如何调用基类的构造方法?通过super()。

所以你之前以为的岁月静好(没有报错),只不过是编译器在替你“负重前行”罢了hh。

  1. 当你一旦手动给父类加上了含参构造方法,原来的那个无参的构造方法就不见了,你就需要根据这个含参的父类构造方法的参数情况,在子类构造方法的第一行显式地用super(…)来进行调用。这时子类的构造方法你是写也得写,不写也得写了。

[!NOTE]

那有些人会有疑问了,这么麻烦,那我不给父类加构造方法不就好了,不仅少了工作量,还不用担心什么super不super的。

但是,你要明白的是,给父类加构造方法有很重要的使用场景:

  • 强制初始化父类的关键属性

    如果父类的某些字段(如 nameage)必须在创建对象时初始化,而无参构造方法无法保证这些字段被正确赋值,那么就需要自定义构造方法来强制要求子类传入必要的参数。

  • 有时候,父类可能需要多种初始化方式,比如:

    • 允许只传 nameage 使用默认值。
    • 允许传 nameage
    • 允许传 nameagecolor

    这时就需要手动定义多个构造方法(构造方法重载)。

等等各种原因…所以,为了不处理更麻烦的后果,还是干了眼前的这碗代码吧!

当你把super语句换到后面时,你会发现它报错了,这是因为super()与this()语句类似,只能放在构造方法的第一行。自然,super()和this()是不能碰面的。此外,super()语句只能在构造方法中出现一次。

说了这么多,最后记住几条精炼的规则:

  1. 若父类显式定义无参或者默认的构造方法,在子类构造方法第一行默认有隐含的super()调用,即调用基类构造方法

  2. 如果父类构造方法是带有参数的,此时需要用户为子类显式定义构造方法,并在子类构造方法中选择合适的父类构造方法调用,否则编译失败

  3. 对于子类父类的构造方法参数的个数关系没有硬性要求,子类构造方法可以小于父类的,可以等于,也可以大于,但是无论是什么关系,子类构造方法必须保证能提供父类构造方法所需的所有参数(通过super调用或默认值)

  4. 在子类构造方法中,super(…)调用父类构造时,必须是子类构造函数中第一条语句

  5. super(…)只能在子类构造方法中出现一次,并且不能和this同时出现

6.再谈代码块

上篇文章讲解的代码块大家还记得吧,先回顾一下几个重要代码块的执行顺序:静态代码块-》实例/构造代码块-》构造方法,其中静态代码块只在类加载时执行一次,而实例代码块在每次创建对象时都会执行,构造方法则是排在实例代码块后执行。

接下来讲解一下存在继承关系时,代码块的执行顺序:

class Parent {
    {
        System.out.println("父类实例代码块");
    }
    static {
        System.out.println("父类静态代码块");
    }
    public Parent() {
        System.out.println("父类构造方法");
    }
}

class Child extends Parent{
    {
        System.out.println("子类实例代码块");
    }
    static {
        System.out.println("子类静态代码块");
    }
    public Child() {
        System.out.println("子类构造方法");
    }
}

public class Inheritance {
    public static void main(String[] args) {
        Child child1= new Child();
        System.out.println("=========");
        Child child2= new Child();
    }
}

代码执行结果为:

在这里插入图片描述

结论:

  1. 父类静态代码块优先于子类静态代码块执行,且是最早执行的。
  2. 父类的实例代码块和父类的构造方法紧接着执行
  3. 子类的实例代码块和子类的构造方法接下来执行
  4. 静态代码块只执行一次

7.再谈访问控制修饰符

No.范围privatedefaultprotectedpublic
1同一包中的同一类
2同一包中的不同类
3不同包中的子类
4不同包中的非子类

private和default在讲解包时讲解过了,private访问权限最小,仅在同一个包的同一个类内可以访问;default是包访问权限,无修饰符,只要在同一个包,便可以访问该权限的类。

学了继承,就可以透彻理解protected权限了:

例如我们在两个包com.example.animals和com.example.pets中分别定义了父类Animal类和子类Lion类:

package com.example.animals;

public class Animal {
    String name;

    void displayName() {
        System.out.println("Animal name: " +this.name);
    }

    public Animal(String name) {
        this.name = name;
    }
}
package com.example.pets;

import com.example.animals.Animal;

public class Lion extends Animal {
    public Lion(String name) {
        super(name);
    }

    public void showDetails() {
        System.out.println("Lion's name: " + this.name);
        displayName();  
    }
}

写完后,你会发现,子类中访问父类成员的代码出现了标红,这是因为父类的访问权限是包访问权限,这个权限仅限于在同一个包中访问。

[!TIP]

子类可以继承父类的成员,但是能否访问取决于访问权限:

  • 若子类父类在一个包里,可以访问
  • 如果不在同一个包里,子类不能直接访问父类的包访问权限成员

当我们把父类的成员name和displayName()权限改成protected时,就可以正常访问了。

新创建一个包com.example.test,写入main方法:

package com.example.test;

import com.example.animals.Animal;
import com.example.pets.Lion;

public class Main {
    public static void main(String[] args) {
        Animal animal=new Animal("General animal");
        animal.name="Test";//报错:不能访问不同包的protected变量
        animal.displayName();//报错:不能访问不同包的protected方法

        Lion lion=new Lion("Simba");
        lion.showDetails();//正确,因为showDetails()方法的访问权限是是public
        lion.name="Simba";//报错:不能访问不同包的protected成员
        lion.displayName();//报错:不能访问不同包的protected方法
    }
}

Animal的成员变量name和方法displayName()方法都是包访问权限,main方法位于不同的包,无法访问。

lion.showDetails()调用是允许的,因为showDetails()是public。
lion.name="Simba"会报错,因为:

  • Main类不是Animal的子类

  • Main类与Animal不在同一个包中

lion.displayName()会报错,原因同上。

总结protected访问规则:

  1. 同包中的任何类都可以访问protected成员
  2. 不同包中的子类可以继承protected成员,并可以在子类内部使用
  3. 不同包中的非子类不能直接访问protected成员(无论是通过父类实例还是子类实例)

[!WARNING]

在Java中,protected只能用来修饰类的成员(字段、方法、内部类),不能用来修饰类。

外部类的访问修饰符只能是public或默认。

8.继承方式

在现实生活中,事物之间的关系是十分复杂的,比如:

在这里插入图片描述

但在Java中只支持以下几种继承方式:

img

继承的注意事项

  1. Java不支持类的多继承(一个类不能同时继承多个类)
  2. 构造方法不能被继承
  3. 私有成员(private)不能被继承
  4. 使用final修饰的类不能被继承
  5. 子类可以添加自己的新方法和属性

二、final关键字

final是Java中的一个重要关键字,可用于修饰类、方法、变量,表示“不可修改的”。下面我将详细介绍final的三种用法

  1. final变量

当final修饰变量时,表示该变量一旦被初始化就不能再修改了,例如:

基本类型变量:

final int MAX_VALUE = 100;
// MAX_VALUE = 200;  // 编译错误,不能修改final变量

引用类型变量:

final List<String> names = new ArrayList<>();
names.add("Alice");  // 可以修改对象内容
// names = new ArrayList<>();  // 编译错误,不能重新赋值
  1. final类

final类不能被继承

final class ImmutableClass {
    // 类实现
}

// 编译错误,不能继承final类
// class ExtendedClass extends ImmutableClass {}
  1. final方法

final方法不能被子类重写(后面文章会讲解)

class Parent {
    public final void show() {
        System.out.println("Parent's show");
    }
}

class Child extends Parent {
    // 编译错误,不能重写final方法
    // public void show() { 
    //     System.out.println("Child's show");
    // }
}

三、总结

以上就是我对面向对象三大特性之一的继承的知识点梳理,觉得有帮助的伙伴们可以点个关注!
在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值