提示:文本为作者学习笔记,详略不均,大家可以根据目录调转到感兴趣部分进行学习!
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类中没有无参的构造器可用…解决方案是创建与父类匹配的构造器,这到底是什么原理?
- 首先,我先解释为什么一开始没有给父类添加任何构造方法时,完全不影响子类的构造。
还记得之前讲解构造方法时,我们知道当你没有手动添加任何构造方法时,编译器会自动提供一个无参的构造器,没有任何参数,方法体里没有任何操作。这时的Animal类中就存在这么一个无参构造方法。
这种情况下Java编译器会自动在子类构造方法的第一行插入一个隐式的super()调用,即调用父类的无参构造方法。
这一步是必要的,Java规定在子类对象构造时,需要先调用其基类构造方法,然后再执行子类的构造方法。毕竟父子父子,现有父再有子。如何调用基类的构造方法?通过super()。
所以你之前以为的岁月静好(没有报错),只不过是编译器在替你“负重前行”罢了hh。
- 当你一旦手动给父类加上了含参构造方法,原来的那个无参的构造方法就不见了,你就需要根据这个含参的父类构造方法的参数情况,在子类构造方法的第一行显式地用super(…)来进行调用。这时子类的构造方法你是写也得写,不写也得写了。
[!NOTE]
那有些人会有疑问了,这么麻烦,那我不给父类加构造方法不就好了,不仅少了工作量,还不用担心什么super不super的。
但是,你要明白的是,给父类加构造方法有很重要的使用场景:
强制初始化父类的关键属性
如果父类的某些字段(如
name
、age
)必须在创建对象时初始化,而无参构造方法无法保证这些字段被正确赋值,那么就需要自定义构造方法来强制要求子类传入必要的参数。有时候,父类可能需要多种初始化方式,比如:
- 允许只传
name
,age
使用默认值。- 允许传
name
和age
。- 允许传
name
、age
和color
。这时就需要手动定义多个构造方法(构造方法重载)。
等等各种原因…所以,为了不处理更麻烦的后果,还是干了眼前的这碗代码吧!
当你把super语句换到后面时,你会发现它报错了,这是因为super()与this()语句类似,只能放在构造方法的第一行。自然,super()和this()是不能碰面的。此外,super()语句只能在构造方法中出现一次。
说了这么多,最后记住几条精炼的规则:
-
若父类显式定义无参或者默认的构造方法,在子类构造方法第一行默认有隐含的super()调用,即调用基类构造方法
-
如果父类构造方法是带有参数的,此时需要用户为子类显式定义构造方法,并在子类构造方法中选择合适的父类构造方法调用,否则编译失败。
-
对于子类父类的构造方法参数的个数关系没有硬性要求,子类构造方法可以小于父类的,可以等于,也可以大于,但是无论是什么关系,子类构造方法必须保证能提供父类构造方法所需的所有参数(通过super调用或默认值)。
-
在子类构造方法中,super(…)调用父类构造时,必须是子类构造函数中第一条语句。
-
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();
}
}
代码执行结果为:
结论:
- 父类静态代码块优先于子类静态代码块执行,且是最早执行的。
- 父类的实例代码块和父类的构造方法紧接着执行
- 子类的实例代码块和子类的构造方法接下来执行
- 静态代码块只执行一次
7.再谈访问控制修饰符
No. | 范围 | private | default | protected | public |
---|---|---|---|---|---|
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访问规则:
- 同包中的任何类都可以访问protected成员
- 不同包中的子类可以继承protected成员,并可以在子类内部使用
- 不同包中的非子类不能直接访问protected成员(无论是通过父类实例还是子类实例)
[!WARNING]
在Java中,protected只能用来修饰类的成员(字段、方法、内部类),不能用来修饰类。
外部类的访问修饰符只能是public或默认。
8.继承方式
在现实生活中,事物之间的关系是十分复杂的,比如:
但在Java中只支持以下几种继承方式:
继承的注意事项
- Java不支持类的多继承(一个类不能同时继承多个类)
- 构造方法不能被继承
- 私有成员(private)不能被继承
- 使用final修饰的类不能被继承
- 子类可以添加自己的新方法和属性
二、final关键字
final是Java中的一个重要关键字,可用于修饰类、方法、变量,表示“不可修改的”。下面我将详细介绍final的三种用法
- final变量
当final修饰变量时,表示该变量一旦被初始化就不能再修改了,例如:
基本类型变量:
final int MAX_VALUE = 100;
// MAX_VALUE = 200; // 编译错误,不能修改final变量
引用类型变量:
final List<String> names = new ArrayList<>();
names.add("Alice"); // 可以修改对象内容
// names = new ArrayList<>(); // 编译错误,不能重新赋值
- final类
final类不能被继承
final class ImmutableClass {
// 类实现
}
// 编译错误,不能继承final类
// class ExtendedClass extends ImmutableClass {}
- 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");
// }
}
三、总结
以上就是我对面向对象三大特性之一的继承的知识点梳理,觉得有帮助的伙伴们可以点个关注!