前言
#继承(Inheritance) 是面向对象编程(OOP)中的一个重要概念,它允许一个类(称为子类或派生类)可以从另一个类(称为父类、基类或超类)继承属性(数据)和方法(行为)。简单来说继承主要用于实现共性的抽取,达到代码的复用。
1 继承
1.1 什么是继承
Java中使用类对实体对象进行描述,而不同的事物间根据不同的业务场景可能存在一些特定联系。因此面向对象编程中,为我们提供了继承(inheritance)
机制,它允许操作者在保持原有类的基础上进行扩展、增加新功能,这样产生的类称为派生类/子类。
1.2 为什么需要继承
我们发现很多事物间存在着联系,也就是所谓的共性,那么我们能否将这些共同的特性进行抽取,从而达到代码的复用呢?
拿猫和狗进行举例,猫和狗都是动物
//Dog.java
public class Dog {
//成员变量
public String name;
int age;
//成员方法
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.java
public class Cat {
//成员变量
public String name;
int age;
//成员方法
public void eat() {
System.out.println(this.name + "正在吃饭...");
}
public void sleep() {
System.out.println(this.name + "正在睡觉...");
}
public void meow() {
System.out.println(this.name + "正在喵喵叫...");
}
}
从上面的两个Java文件中,我们可以发现猫和狗类中存在着相同的共性
面对这种情况,面向对象思想中提出了继承概念,用于共性的抽取,实现代码的复用
因此我们可以定义一个Animal类用于共性的抽取:
Dog和Cat都继承于Animal类,Animal类称为父类/基类/超类,Dog和Cat称为子类/派生类,继承之后子类可以复用父类中的成员,从而实现代码的复用。
1.3 继承的语法
在Java中表示类的继承关系需要借助extends
关键字
修饰符 class 子类 extends 父类 {
//.....
}
那么我们可以借助继承的语法将上述类组装起来:
//Animal.java
public class Animal {
//成员变量
public String name;
int age;
//成员方法
public void eat() {
System.out.println(this.name + "正在吃饭...");
}
public void sleep() {
System.out.println(this.name + "正在睡觉...");
}
}
//Dog.java
public class Dog extends Animal{
public void bark() {
System.out.println(this.name + "正在汪汪叫...");
}
}
//Cat.java
public class Cat extends Animal{
public void meow() {
System.out.println(this.name + "正在喵喵叫...");
}
}
在组装起来之后,我们可以通过在Test类中实例化相应类来进行代码的理解
//Test.java
public class Test {
public static void main(String[] args) {
Dog dog = new Dog();
System.out.println(dog.name);
System.out.println(dog.age);
//我们发现dog类可以访问从Animal中继承来的name和age属性,
dog.eat();
dog.sleep();
//dog类中也可以访问从Animal中继承下来的方法
}
}
1.4 父类成员的访问
通过子类对象访问成员时:
- 如果访问的成员变量子类中有,优先访问自己的成员变量。
- 如果访问的成员变量子类中无,则访问父类继承下来的;如果父类中也没有定义,则编译报错
- 如果访问的成员变量与父类中成员变量同名,则优先访问自己的
- 通过子类对象访问父类与子类不同名方法时,优先在子类中找
- 通过子类对象访问父类与子类同名方法时,如果子类和父类同名方法的参数列表不同(方法重载),根据传递的参数选择合适的方法访问,如果没有则报错
总的来说,成员变量访问遵循就近原则
,自己有优先访问自己的,如果没有则向父类中寻找。
//Base.java
public class Base {
//成员变量
public int a;
public int b;
public int c;
//成员方法
public void method() {
System.out.println("Base中的method()");
}
public void methodA() {
System.out.println("Base中的methodA()");
}
public void methodB() {
System.out.println("Base中的methodB()");
}
}
//Derived.java
public class Derived extends Base{
//成员变量
public int a;//与父类成员a同名,相同类型
public char b;//与父类成员b同名,不同类型
//成员方法
public void methodA(int a) {
System.out.println("Derived中的methodA()");
}
public void methodB() {
System.out.println("Derived中的methodB()");
}
//在methodC()中探讨成员变量的继承问题
public void methodC() {
a = 10;//访问子类自己的a
b = 100;//访问子类自己的b
c = 1000;//子类中没有c,访问父类继承下来的c
}
//在methodD()中探讨成员变量的继承问题
public void methodD() {
method();//访问从父类继承的method()方法
methodA();//没有传递参数,根据参数列表访问父类的
methodA(10);//传递参数int,访问子类中的methodA()方法
methodB();//直接访问则永远访问的是子类的methodB()方法
methodC();//访问子类自己的methodC()方法
}
}
在继承关系中,我们可以将子类对象中存在的成员方法和变量看成两部分构成,一部分是从父类继承来的,一部分是子类自己的:
#方法重写(Override) 当子类继承父类并且子类中定义了与父类同名、参数列表相同的方法时,子类的方法会覆盖(取代)父类中的方法。这意味着,通过子类对象调用这个方法时,会执行子类中的方法实现,而不会执行父类中的方法。
1.5 super
和this
关键字
通过上述演示,我们知道子类和父类中存在同名成员时,成员的访问中遵循着就近原则
,那么当我们想访问父类中的同名成员时,应该怎么操作呢?
super
关键字:在子类方法中访问父类成员
也就是说在上述的methodC()方法中,我们可以通过super.a来访问父类中的a;public void methodC() { super.a = 10;//访问父类中的a a = 100;//访问子类中的a }
在methodD()方法中
public void methodD() { //如果想要在子类中访问重写的父类方法,则需要借助 //`super`关键字 methodB();//直接访问则永远访问的是子类的methodB()方法 super.methodB();//访问父类的methodD(); }
1.5.1 再谈this
关键字
this
关键字是当前对象的引用,super
相当于是子类对象从父类继承下来的成员的引用- 在非静态方法中,
this
一般用来对本类的方法和属性访问,super
用来访问父类继承下来的方法和属性 - 在构造方法中:
this(...)
用来调用本类的构造方法,super(...)
用来调用父类的构造方法 - 当父类存在无参的构造方法时,子类的构造方法中一定会存在
super()
的调用,用户没有写编译器也会隐式为你增加,但用户不写则没有this()
1.6子类构造方法和初始化
1.6.1 构造方法
子类对象中成员是由两部分组成的,父类继承下来的和子类新增的部分,父子父子,如字面意思,肯定是先有父再有子,因此在构造子类对象的时候,我们会==先调用父类的构造方法,将从父类继承下来的成员构造完整,然后再调用子类自己的构造方法==,将子类自己新增的成员初始化完整。
//Base.java
public class Base {
public Base() {
System.out.println("Base()");
}
}
//Derived.java
public class Derived extends Base {
public Derived() {
//super();子类构造方法默认调用父类无参构造方法
//用户没写时编译器隐式添加,且super()是子类构造第一条语句
System.out.println("Derived()");
}
}
//Main.java
public class Main {
public static void main(String[] args) {
Derived derived = new Derived();
}
}
结果打印:
Base();
Derived();
注意:
- 父类显示定义无参或者默认无参构造方法时,子类构造方法第一行默认隐含Super();
- 当父类构造方法带有参数时,此时需要子类显示调用父类的构造方法,否则编译失败
- 在构造方法中,super(…)和this(…)都只能出现在第一行,所以在构造方法中不能同时存在
- 最后
super
和this
关键字都依赖于对象,所以只能在类的非静态方法中直接使用
1.6.2 初始化
一般在类中我们存在着几个重要的代码快:实例代码块和静态代码块
//Base.java
public class Base {
//被static修饰的代码快叫静态代码块
static {
System.out.println("Base静态代码块执行了");
}
//没有被修饰的代码快叫实例代码块代码块
{
System.out.println("Base实例代码块执行了");
}
public Base() {
System.out.println("Base()构造方法执行了");
}
}
//Derived.java
public class Derived extends Base {
static {
System.out.println("Derived静态代码块执行了");
}
//.....
{
System.out.println("Derived实例代码块执行了");
}
public Derived() {
//super();子类构造方法默认调用父类无参构造方法
//用户没写时编译器隐式添加,且super()是子类构造第一条语句
System.out.println("Derived()构造方法执行了");
}
}
//Main.java
public class Main {
public static void main(String[] args) {
//代码段1
//Base base1 = new Base();
//System.out.println("= = = = = = = = = =");
//Base base2 = new Base();
//代码段2
//Derived derived1 = new Derived();
//System.out.println("= = = = = = = = = =");
//Derived derived2 = new Derived();
}
}
执行结果:
执行代码块1
Base静态代码块执行了
Base实例代码块执行了
Base()构造方法执行了
= = = = = = = = = =
Base实例代码块执行了
Base()构造方法执行了
观察发现:
- 静态代码块先执行,且只在类加载阶段执行
- 当有对象创建时,才会执行实例代码块,最后执行构造方法
执行代码块2
Derived静态代码块执行了
Derived实例代码块执行了
Derived()构造方法执行了
= = = = = = = = = =
Derived实例代码块执行了
Derived()构造方法执行了
通过分析执行结果,我们发现:
- 父类静态代码块优于子类静态代码块执行,且最早执行
- 父类实例代码块和父类构造方法紧接着执行
- 子类实例化代码块和子类构造方法再紧接着执行
- 静态代码块只在类加载的时候执行,在第二次实例子类对象时,父类和子类的静态代码块将不在执行
#projected关键字 为了进一步实现封装特性,Java中引入了访问权限修饰符,限定:类或者类中成员能否在类外或者其他包中被访问。projected
关键字修饰的权限只能在同一个包下或者不同包的子类中访问。