文章目录
- 《C++转Java快速入手系列》继承与多态篇
-
- 1、继承
-
- 1.1继承的概念
- 1.2继承的语法
- 1.3父类的成员访问
-
- [1.3.1 子类中访问父类的成员变量](#1.3.1 子类中访问父类的成员变量)
- [1.3.2 子类中访问父类的成员方法](#1.3.2 子类中访问父类的成员方法)
- [1.4 super关键字](#1.4 super关键字)
- 1.5、子类构造方法
- 1.6、再谈初始化
- 1.7、继承方式
- 1.8、final关键字
- 1.9、继承与组合
- 2、多态
《C++转Java快速入手系列》继承与多态篇
1、继承
1.1继承的概念
与C++一样,Java中的继承与多态也是面向对象编程中的重要的代码服用手段,它能够在保持原有类特性的基础上进行扩展,增加新功能。继承新功能的类被叫做派生类 、子类 ,被继承的类叫做父类 、基类 或超类,继承之后,子类可以复用父类中的成员,子类在实现时只需关心自信新增加的成员即可
1.2继承的语法
在Java中如果要表示类之间的继承关系,使用的时extend关键字:
修饰符 class 子类 extend 父类 {
//......
}
- 注意:在子类继承父类后,必须在此基础上添加自己特有的成员。否则没有必要进行继承
1.3父类的成员访问
1.3.1 子类中访问父类的成员变量
- 子类与父类的成员变量不同名 时,就近原则,优先在子类查找,找到则访问,否则在父类中查找,找到则访问,否则编译报错
- 子类与父类的成员变量同名时 ,直接使用默认就近原则 ,此时指的是子类 中的成员变量,这也被称为变量隐藏 如果想强行访问父类中的成员变量要加super 关键字来访问
1.3.2 子类中访问父类的成员方法
- 子类与父类的成员方法不同名 时,优先在子类查找,找到则访问,否则在父类中查找,找到则访问,否则编译报错
- 子类与父类的成员方法同名 时,如果父类和子类同名方法的参数不同(构成重载),则根据传递的参数来选择合适的方法访问,如果没有合适的方法会报错
1.4 super关键字
当子类和父类存在相同名称的成员时,我们想要在子类中访问父类中的该同名成员,我们就需要用到super关键字
- super关键字在子类中使用,就会调用当前子类对象的父类对象 ,来访问父类的非静态成员
1.5、子类构造方法
在子类对象构造时,需要先调用父类的构造方法,才能执行子类的构造方法
java
public class Base {
public Base(){
System.out.println("Base()");
}
}
public class Derived extends Base{
public Derived(){
// super(); // 注意子类构造方法中默认会调用基类的无参构造方法:super(),
// 用户没有写时,编译器会自动添加,而且super()必须是子类构造方法中第一条语句,
// 并且只能出现一次
System.out.println("Derived()");
}
}
public class Test {
public static void main(String[] args) {
Derived d = new Derived();
}
}
按照先执行父类构造再执行子类构造 的顺序,这里的父类构造执行的时候分两种情况判断是否能省略显示调用:
- 如果父类的构造方法 是无参数或默认构造 ,则子类的第一行有隐藏的
super() 调用,此时就可以省略显示调用父类构造 - 如果父类的构造方法 是带有参数的,则子类的第一行必须要手动显示调用父类的构造函数,否则会报错
- super只能在子类构造方法中出现一次 ,并且不能和this同时出现。
1.6、再谈初始化
前一节博客,我清晰的剖析了类对象初始化的所有情况,我们在此基础上,加入了父类与子类的关系,会对初始化的顺序有什么影响呢?
- 父类的静态代码块优先级高于子类静态代码块执行,属于最早执行的一个板块
- 父类的实例代码块和构造方法优先级高于子类的实例代码块和构造方法
- 第二次实例化子类对象时,父类和子类的静态代码块都将不会再执行
所以优先级变成了以下情况:
第一梯队(最高):(在类加载时进行)父类的静态代码块,父类的静态人员初始化
第二梯队:(在类加载时进行)子类的静态代码块,子类的静态成员初始化
第三梯队: 父类的就地初始化,父类的构造代码块(实例代码块)
第四梯队: 父类的构造方法初始化
第五梯队:子类的就地初始化,父类的构造代码块(实例代码块)
第六梯队:子类的构造方法初始化
1.7、继承方式
在Java中,继承的方式与C++有所差异,不支持多继承,因此避免了菱形继承的复杂情况
在开发中,一般我们不希望出现超过三层的继承关系
1.8、final关键字
java中的final关键字一定程度上可以理解为C++中const的用法,但也有些许不同
1.8.1、final修饰类
- 当用final修饰类时,表示当前类不可被继承
java
final class ImmutableClass {
// 类内容
}
1.8.2、final修饰方法
- 当方法被声明为final时,不可被子类重写
java
class Parent {
public final void criticalMethod() {
System.out.println("不可重写的方法");
}
}
class Child extends Parent {
// @Override public void criticalMethod() {} // 编译错误
}
1.8.3、final修饰变量
- 修饰基本类型变量:变量初始化后值不可被修改
java
final int MAX_VALUE = 100;
// MAX_VALUE = 200; // 编译错误
- 引用类型变量:引用不可指向新对象,但对象的内部状态可以修改
java
final List<String> list = new ArrayList<>();
list.add("A"); // 允许修改对象内容
// list = new ArrayList<>(); // 编译错误(不可更改引用)
1.8.4、final修饰成员变量
- 修饰实例变量:必须在声明时或初始化块中赋值
java
class Example {
final int a; // 未初始化
final int b = 10; // 声明时初始化
Example() {
a = 20; // 构造方法初始化
}
}
- 修饰静态变量:必须在声明时或静态初始化块中赋值
java
class Constants {
static final double PI = 3.14; // 声明时初始化
static final int MAX;
static { MAX = 100; } // 静态块初始化
}
1.8.5、final修饰参数
- 方法参数不可在方法内修改
java
void process(final int input) {
// input = 5; // 编译错误
System.out.println(input);
}
1.8.6、final与匿名内部类
匿名内部类中访问的外部局部变量必须是被final修饰的,或者等效不可变(Java 8+ 的 effectively final)
java
void outerMethod() {
final int localVar = 10; // 必须是final
new Thread(() -> {
System.out.println(localVar); // 访问外部变量
}).start();
}
1.9、继承与组合
和继承类似, 组合也是一种表达类之间关系的方式, 也是能够达到代码重用的效果。组合并没有涉及到特殊的语法
(诸如 extends 这样的关键字), 仅仅是将一个类的实例作为另外一个类的字段。
-
继承 表示对象之间是is-a的关系,比如:狗是动物,猫是动物
-
组合 表示对象之间是has-a的关系,比如:汽车和其轮胎、发动机、方向盘、车载系统等的关系就应该是组合
-
在设计类时,一般建议能用组合就用组合
2、多态
java中多态与C++中的意义并无区别,通俗来讲就是许多种 有继承关系 的一些对象去实现某个行为 ,当不同种对象 去实现该行为 时会用不同的方式 去实现
因此我们重点放在使用的不同上
2.1、多态的实现方式
众所周知,C++中的多态实现 是通过继承 和虚函数 的机制来实现多态,在重写 的方面,方法默认是非虚的 ,必须显示声明为virtual才能重写,运行时多态使用虚表机制
- 而Java中多态的实现方式是使用继承 和接口(interface) ,方法默认是虚的(可重写),除非用final或static修饰,运行时多态通过方法重写实现
2.2、多态的实现条件
java中实现的多态必须要满足如下几个条件,缺一不可
- 必须在继承的前提下
- 子类必须对父类中的方法进行重写
- 通过父类类型的引用调用重写的方法
2.3、重写
重写(override):也称为覆盖。重写是子类对父类非静态、非private修饰,非final修饰,非构造方法等的实现过程进行重新编写, 返回值和形参都不能改变。即外壳不变,核心重写
方法重写的规则:
- 子类在重写方法时,一般必须与父类方法原型一致:返回值类型,方法名,参数列表要完全一致
- 被重写的方法返回值类型可以不同,但是必须是具有父子关系的
- 子类 重写的方法的访问权限不能 比父类 中被重写的方法的访问权限更严格
- 父类被static、private修饰的方法、构造方法都不能被重写
- 重写的方法, 可以使用 @Override 注解来显式指定. 有了这个注解能帮我们进行一些合法性校验
2.4、绑定
在java中,绑定指的是方法调用与具体方法实现的关联过程,他决定了当程序调用一个方法时JVM如何选择要执行的代码。绑定分为静态绑定和动态绑定,以下详细介绍这两个概念
- 静态绑定
静态绑定发生在编译 时,编译器在编译代码时就能确定方法调用的具体实现。这个机制基于引用变量的声明类型,而不是对象的实际类型
应用场景:
- private 方法:因为私有方法不能被继承或重写
- final方法:final方法不能被重写,编译时即可固定
- static 方法:静态方法属于类,而非对象
- 变量(包括实例变量和静态变量):变量的访问总是静态访问的
优点:效率高,因为编译时解析,减少了运行时的开销
java
class Parent {
static void display() {
System.out.println("Parent's static method");
}
}
class Child extends Parent {
static void display() {
System.out.println("Child's static method");
}
}
public class Main {
public static void main(String[] args) {
Parent obj = new Child();
obj.display(); // 输出: Parent's static method,因为display是static方法,编译时绑定到Parent类
}
}
在这个例子中,obj.display() 调用的是Parent类的display方法,因为该方法时静态方法 ,所以绑定在编译时 即通过引用类型确定 ,而非对象的实际类型
2、动态绑定
动态绑定发生在运行 时,JVM根据对象的实际类型 来决定方法的调用,可以说动态绑定就是为了多态的重写而生,只要成功满足多态的条件,在父类类型引用 子类类型的具体对象时可以成功执行子类的方法
应用场景:主要用于重写方法,当方法时非静态、非私有、非final,动态绑定即可生效
优点:灵活性高,允许程序在运行时适应不同的子类对象
java
class Animal {
void sound() {
System.out.println("Animal makes a sound");
}
}
class Dog extends Animal {
@Override
void sound() {
System.out.println("Dog barks");
}
}
public class Main {
public static void main(String[] args) {
Animal myAnimal = new Dog();
myAnimal.sound(); // 输出: Dog barks,因为sound()是重写方法,运行时绑定到Dog的实际类型
}
}
2.5、向上转型和向下转型
这个概念特指在多态 中的引用类型 和实际对象类型 的不同组合的适配情况
2.5.1、向上转型
这里的 "上" 指的是父类,所以通俗来理解就是子类当成父类来使用
- 在多态的使用环节,我们就需要这个向上转型的功能,用父类 引用 子类对象来调用重写的方法
这里的使用场景有以下三部分,这部分非常重要,这三种场景意味着三种父类引用接收,意味着三种场景来实现多态,能让我们的多态设计的更加灵活
java
// 父类 Animal
abstract class Animal {
public abstract void speak(); // 抽象方法,子类必须重写
}
// 子类 Dog
class Dog extends Animal {
@Override
public void speak() {
System.out.println("汪汪!"); // Dog 的实现
}
}
// 子类 Cat
class Cat extends Animal {
@Override
public void speak() {
System.out.println("喵喵!"); // Cat 的实现
}
}
// 主类用于演示
public class PolymorphismDemo {
public static void main(String[] args) {
// 场景将在主方法中演示
}
}
- 直接赋值
java
public static void main(String[] args) {
// 直接赋值:父类引用指向 Dog 对象
Animal animal1 = new Dog();
animal1.speak(); // 输出:汪汪!
// 直接赋值:父类引用指向 Cat 对象
Animal animal2 = new Cat();
animal2.speak(); // 输出:喵喵!
}
- 方法传参
java
// 定义一个方法,参数为 Animal 类型
public static void makeAnimalSpeak(Animal animal) {
animal.speak(); // 调用传入对象的 speak 方法
}
public static void main(String[] args) {
Dog dog = new Dog();
Cat cat = new Cat();
// 方法传参:传入 Dog 对象
makeAnimalSpeak(dog); // 输出:汪汪!
// 方法传参:传入 Cat 对象
makeAnimalSpeak(cat); // 输出:喵喵!
}
- 方法返回
java
// 定义一个方法,返回 Animal 类型对象
public static Animal getAnimal(String type) {
if ("dog".equalsIgnoreCase(type)) {
return new Dog(); // 返回 Dog 对象(子类)
} else if ("cat".equalsIgnoreCase(type)) {
return new Cat(); // 返回 Cat 对象(子类)
}
return null; // 简化示例,实际中可处理异常
}
public static void main(String[] args) {
// 方法返回:接收父类引用
Animal animal3 = getAnimal("dog");
animal3.speak(); // 输出:汪汪!
Animal animal4 = getAnimal("cat");
animal4.speak(); // 输出:喵喵!
}
向上转型的优点:让代码实现更加的灵活
向上转型的缺点:不能调用到子类的特定方法
2.5.2、向下转型
向下转型就和上面的相反,用子类引用父类对象,这个操作比较危险,因为由上到下如果实际多出来一些东西是不兼容的
- 在使用向下转型的时候必须在运行时 确保所引用的对象 实际就是目标子类的实例,否则会抛出异常,以下有三种情况
- 向下转型不能像向上转型一样直接赋值,要用 () 来进行强制类型转换
- 对象实际是目标子类实例时直接向下转型
这是最安全的场景,对象类型在编译时已确定,无需额外检查
java
class Vehicle {
public void start() {
System.out.println("Vehicle started");
}
}
class Car extends Vehicle {
public void honk() {
System.out.println("Car honking");
}
}
public class DirectDowncastDemo {
public static void main(String[] args) {
Vehicle vehicle = new Car(); // 父类引用指向子类对象
Car car = (Car) vehicle; // 直接向下转型
car.honk(); // 输出: Car honking
}
}
- 对象实际不是目标子类实例(是其他子类实例)时,使用instanceof来进行安全向下转型
当父类引用可能指向不同子类对象时,需使用instanceof操作符检查类型后再转型,以避免ClassCastException。适用于多态场景,如方法参数或集合元素。
java
class Shape {
public void draw() {
System.out.println("Drawing shape");
}
}
class Circle extends Shape {
public void rotate() {
System.out.println("Circle rotating");
}
}
class Square extends Shape {
public void resize() {
System.out.println("Square resizing");
}
}
public class SafeDowncastDemo {
public static void main(String[] args) {
Shape shape = new Circle(); // 父类引用指向子类对象(可能是Circle或Square)
if (shape instanceof Circle) {
Circle circle = (Circle) shape; // 安全向下转型
circle.rotate(); // 输出: Circle rotating
} else if (shape instanceof Square) {
Square square = (Square) shape; // 安全向下转型
square.resize();
} else {
System.out.println("Unknown shape type");
}
}
}
- 在集合框架中处理向下转型
当集合中 储存着父类引用,但元素实际是不同类型的子类对象时,遍历集合instanceof检查后向下转型,以调用子类的特定方法
java
import java.util.ArrayList;
import java.util.List;
class Employee {
public void work() {
System.out.println("Employee working");
}
}
class Developer extends Employee {
public void code() {
System.out.println("Developer coding");
}
}
class Manager extends Employee {
public void manage() {
System.out.println("Manager managing");
}
}
public class CollectionDowncastDemo {
public static void main(String[] args) {
List<Employee> employees = new ArrayList<>();
employees.add(new Developer());
employees.add(new Manager());
for (Employee emp : employees) {
if (emp instanceof Developer) {
Developer dev = (Developer) emp; // 向下转型
dev.code(); // 输出: Developer coding
} else if (emp instanceof Manager) {
Manager mgr = (Manager) emp; // 向下转型
mgr.manage(); // 输出: Manager managing
}
}
}
}
2.6、多态的优缺点
- 使用多态的好处:
- 能够降低代码的 "圈复杂度", 避免使用大量的 if - else
- 可扩展能力更强 ,多态的方式改动代码成本比较低
- 多态的缺陷:
- 代码的运行效率降低