MK
摩柯社区 - 一个极简的技术知识社区
AI 面试

Java抽象类的常见误区

2023-10-194.8k 阅读

一、对抽象类定义的误解

  1. 认为抽象类不能有构造函数
    • 常见误区阐述:在Java中,有一种常见的误解是认为抽象类不能有构造函数。部分开发者觉得抽象类不能被实例化,构造函数是用于实例化对象的,所以抽象类不应有构造函数。但实际上,抽象类是可以有构造函数的。
    • 本质原理分析:虽然抽象类不能被直接实例化,但其构造函数并非毫无用处。当一个具体子类继承自抽象类时,在子类实例化过程中,会首先调用抽象类的构造函数。这遵循了Java对象实例化的顺序,先初始化父类部分,再初始化子类部分。抽象类的构造函数可以用于初始化一些公共的成员变量或执行一些通用的初始化逻辑,这些逻辑对于所有子类都是必要的。
    • 代码示例
abstract class AbstractClassWithConstructor {
    private int num;
    public AbstractClassWithConstructor(int num) {
        this.num = num;
    }
    public int getNum() {
        return num;
    }
}
class ConcreteClass extends AbstractClassWithConstructor {
    public ConcreteClass(int num) {
        super(num);
    }
}
public class Test {
    public static void main(String[] args) {
        ConcreteClass cc = new ConcreteClass(10);
        System.out.println(cc.getNum());
    }
}
  • 示例解释:在上述代码中,AbstractClassWithConstructor是一个抽象类,它有一个构造函数,用于初始化num成员变量。ConcreteClass继承自该抽象类,在其构造函数中通过super(num)调用了抽象类的构造函数。当在main方法中创建ConcreteClass的实例时,会先调用抽象类的构造函数,从而正确初始化num变量并输出其值。
  1. 混淆抽象类与接口的定义目的
    • 常见误区阐述:一些开发者容易混淆抽象类和接口的定义目的。他们可能会觉得抽象类和接口都用于定义一些未实现的方法,所以在设计时不知道该选择哪一个。有时会错误地将应该用接口实现的功能放到抽象类中,或者反之。
    • 本质原理分析:抽象类主要用于抽取一些具有共性的属性和行为,它可以包含具体的方法和成员变量,子类继承抽象类后可以复用这些共性部分。而接口则侧重于定义一种行为规范,它的所有方法默认都是抽象的(Java 8 及以后接口可以有默认方法,但本质上还是强调行为规范),且不能有成员变量(除了public static final类型的常量)。一个类只能继承一个抽象类,但可以实现多个接口。
    • 代码示例
// 抽象类示例
abstract class Animal {
    protected String name;
    public Animal(String name) {
        this.name = name;
    }
    public abstract void makeSound();
    public void eat() {
        System.out.println(name + " is eating.");
    }
}
class Dog extends Animal {
    public Dog(String name) {
        super(name);
    }
    @Override
    public void makeSound() {
        System.out.println("Woof!");
    }
}
// 接口示例
interface Flyable {
    void fly();
}
class Bird extends Animal implements Flyable {
    public Bird(String name) {
        super(name);
    }
    @Override
    public void makeSound() {
        System.out.println("Chirp!");
    }
    @Override
    public void fly() {
        System.out.println(name + " is flying.");
    }
}
  • 示例解释:在这个例子中,Animal抽象类抽取了动物的共性,如名字和进食行为。Dog类继承自Animal并实现了makeSound方法。而Flyable接口定义了飞行的行为规范,Bird类继承自Animal并实现了Flyable接口,这样Bird既拥有动物的共性,又具备飞行的能力。如果将飞行行为放到抽象类中,那么其他不具备飞行能力的动物子类也会被迫继承这个不适合它们的飞行相关代码,这就体现了抽象类和接口定义目的的不同。

二、抽象类使用中的误区

  1. 过度依赖抽象类继承导致代码僵化
    • 常见误区阐述:有些开发者在使用抽象类时,过度依赖继承关系,将大量的功能都集中在抽象类及其子类的继承体系中。这可能导致代码的可维护性和扩展性变差,因为一旦抽象类的结构发生变化,所有子类都可能受到影响。
    • 本质原理分析:继承是一种强耦合关系。当我们通过继承来复用抽象类的代码时,子类与抽象类紧密相连。如果抽象类的实现细节改变,比如增加或修改了一个方法的参数列表,那么所有子类都需要相应地进行调整。此外,过多的继承层次也会使代码结构变得复杂,难以理解和维护。
    • 代码示例
abstract class Shape {
    public abstract double calculateArea();
}
class Circle extends Shape {
    private double radius;
    public Circle(double radius) {
        this.radius = radius;
    }
    @Override
    public double calculateArea() {
        return Math.PI * radius * radius;
    }
}
class Rectangle extends Shape {
    private double width;
    private double height;
    public Rectangle(double width, double height) {
        this.width = width;
        this.height = height;
    }
    @Override
    public double calculateArea() {
        return width * height;
    }
}
// 假设现在抽象类Shape需要添加一个新的功能,比如计算周长
abstract class ShapeNew {
    public abstract double calculateArea();
    public abstract double calculatePerimeter();
}
// 子类Circle和Rectangle都需要修改以适应新的抽象类
class CircleNew extends ShapeNew {
    private double radius;
    public CircleNew(double radius) {
        this.radius = radius;
    }
    @Override
    public double calculateArea() {
        return Math.PI * radius * radius;
    }
    @Override
    public double calculatePerimeter() {
        return 2 * Math.PI * radius;
    }
}
class RectangleNew extends ShapeNew {
    private double width;
    private double height;
    public RectangleNew(double width, double height) {
        this.width = width;
        this.height = height;
    }
    @Override
    public double calculateArea() {
        return width * height;
    }
    @Override
    public double calculatePerimeter() {
        return 2 * (width + height);
    }
}
  • 示例解释:最初,Shape抽象类及其子类CircleRectangle用于计算面积。当需要添加计算周长的功能时,抽象类Shape变为ShapeNew,这就导致CircleRectangle子类都需要进行修改。如果项目中有很多这样的子类,修改的工作量会很大,而且容易引入错误,这体现了过度依赖继承导致的代码僵化问题。
  1. 在抽象类中滥用具体方法
    • 常见误区阐述:部分开发者在抽象类中定义了过多的具体方法,使得抽象类承担了过多的职责。这样可能导致抽象类变得臃肿,并且不符合单一职责原则,同时也给子类带来不必要的负担,因为子类可能并不需要所有这些具体方法。
    • 本质原理分析:抽象类的主要目的是抽取共性和定义抽象行为。过多的具体方法会使抽象类的关注点变得模糊,难以理解其核心抽象概念。此外,子类可能会继承一些它们根本用不到的方法,这不仅增加了代码的冗余,还可能导致代码在运行时出现不必要的行为或错误。
    • 代码示例
abstract class Vehicle {
    private String brand;
    public Vehicle(String brand) {
        this.brand = brand;
    }
    public abstract void move();
    // 过多的具体方法
    public void printBrand() {
        System.out.println("Brand: " + brand);
    }
    public void startEngine() {
        System.out.println("Engine started.");
    }
    public void stopEngine() {
        System.out.println("Engine stopped.");
    }
}
class Car extends Vehicle {
    public Car(String brand) {
        super(brand);
    }
    @Override
    public void move() {
        System.out.println("Car is moving.");
    }
}
class Bicycle extends Vehicle {
    public Bicycle(String brand) {
        super(brand);
    }
    @Override
    public void move() {
        System.out.println("Bicycle is moving.");
    }
    // Bicycle类并不需要startEngine和stopEngine方法,但因为继承自Vehicle抽象类而不得不继承这些方法
}
  • 示例解释:在上述代码中,Vehicle抽象类不仅定义了抽象的move方法,还定义了printBrandstartEnginestopEngine等具体方法。对于Bicycle类来说,它并不需要startEnginestopEngine方法,但由于继承关系,它被迫继承了这些方法,这就造成了代码的不合理性。

三、抽象类与多态相关的误区

  1. 错误理解抽象类方法重写规则
    • 常见误区阐述:一些开发者在重写抽象类中的抽象方法时,可能会错误地理解重写规则。比如,改变方法的访问修饰符、返回类型等,导致重写不符合要求,运行时出现错误。
    • 本质原理分析:当子类重写抽象类的抽象方法时,方法的签名(方法名、参数列表)必须完全相同,并且访问修饰符不能比抽象类中抽象方法的访问修饰符更严格。如果抽象类中的抽象方法是protected,子类重写时不能改为private。此外,返回类型在Java 5.0及以后支持协变返回类型,即子类重写方法的返回类型可以是抽象类中抽象方法返回类型的子类。
    • 代码示例
abstract class AbstractParent {
    public abstract Number getNumber();
}
class Child extends AbstractParent {
    // 正确的重写,返回类型是Number的子类
    @Override
    public Integer getNumber() {
        return 10;
    }
}
// 错误的重写示例1:改变访问修饰符
class IncorrectChild1 extends AbstractParent {
    // 这里将public改为private,不符合重写规则
    private Integer getNumber() {
        return 10;
    }
}
// 错误的重写示例2:改变方法签名
class IncorrectChild2 extends AbstractParent {
    // 参数列表不同,不符合重写规则
    public Integer getNumber(int num) {
        return num;
    }
}
  • 示例解释:在上述代码中,Child类正确地重写了AbstractParent中的getNumber方法,返回类型IntegerNumber的子类。而IncorrectChild1类将方法的访问修饰符从public改为private,这是错误的。IncorrectChild2类改变了方法的参数列表,也不符合重写规则。
  1. 认为抽象类不能参与多态的动态绑定
    • 常见误区阐述:部分开发者认为抽象类不能很好地参与多态的动态绑定,因为抽象类不能被实例化。他们觉得只有具体类才能在多态中实现动态绑定,这种理解是片面的。
    • 本质原理分析:虽然抽象类不能被直接实例化,但通过其子类的实例化,抽象类可以很好地参与多态。当一个抽象类有多个子类,并且这些子类重写了抽象类的抽象方法时,通过抽象类的引用指向不同子类的实例,在运行时就会根据实际指向的子类对象来调用相应的重写方法,这就是多态的动态绑定。
    • 代码示例
abstract class ShapeAbstract {
    public abstract double calculateArea();
}
class CircleAbstract extends ShapeAbstract {
    private double radius;
    public CircleAbstract(double radius) {
        this.radius = radius;
    }
    @Override
    public double calculateArea() {
        return Math.PI * radius * radius;
    }
}
class RectangleAbstract extends ShapeAbstract {
    private double width;
    private double height;
    public RectangleAbstract(double width, double height) {
        this.width = width;
        this.height = height;
    }
    @Override
    public double calculateArea() {
        return width * height;
    }
}
public class PolymorphismTest {
    public static void main(String[] args) {
        ShapeAbstract shape1 = new CircleAbstract(5);
        ShapeAbstract shape2 = new RectangleAbstract(4, 5);
        System.out.println("Circle area: " + shape1.calculateArea());
        System.out.println("Rectangle area: " + shape2.calculateArea());
    }
}
  • 示例解释:在上述代码中,ShapeAbstract是抽象类,CircleAbstractRectangleAbstract是它的子类。通过ShapeAbstract的引用shape1shape2分别指向CircleAbstractRectangleAbstract的实例,在调用calculateArea方法时,会根据实际指向的子类对象动态调用相应的重写方法,这体现了抽象类在多态中的动态绑定。

四、抽象类与其他Java特性结合的误区

  1. 抽象类与泛型结合时的类型参数误解
    • 常见误区阐述:当抽象类与泛型结合使用时,一些开发者可能会对类型参数的使用和作用范围产生误解。比如,错误地在抽象类的不同方法中使用不一致的类型参数,或者不清楚类型参数在子类中的继承和使用规则。
    • 本质原理分析:抽象类中的泛型类型参数定义了一个占位符类型,在具体使用时会被实际类型替换。这个类型参数在整个抽象类及其子类中都有一定的作用范围。子类继承抽象类时,如果不指定具体类型,那么子类也会继承泛型类型参数。如果子类指定了具体类型,那么在子类中抽象类的泛型方法将使用指定的具体类型。
    • 代码示例
abstract class GenericAbstractClass<T> {
    public abstract T process(T data);
}
class GenericConcreteClass extends GenericAbstractClass<Integer> {
    @Override
    public Integer process(Integer data) {
        return data * 2;
    }
}
class AnotherGenericConcreteClass<T> extends GenericAbstractClass<T> {
    @Override
    public T process(T data) {
        return data;
    }
}
public class GenericTest {
    public static void main(String[] args) {
        GenericConcreteClass gcc = new GenericConcreteClass();
        System.out.println(gcc.process(5));
        AnotherGenericConcreteClass<String> agcc = new AnotherGenericConcreteClass<>();
        System.out.println(agcc.process("Hello"));
    }
}
  • 示例解释:在上述代码中,GenericAbstractClass是一个泛型抽象类,类型参数为TGenericConcreteClass继承自GenericAbstractClass并指定了类型参数为Integer,所以process方法处理的是Integer类型的数据。AnotherGenericConcreteClass没有指定具体类型,仍然使用泛型类型参数T,在main方法中创建AnotherGenericConcreteClass<String>实例时,process方法处理的就是String类型的数据。如果在子类中错误地使用与抽象类不一致的类型参数,就会导致编译错误。
  1. 抽象类与异常处理结合时的错误处理方式
    • 常见误区阐述:在抽象类及其子类中进行异常处理时,开发者可能会出现一些错误。比如,在抽象类的抽象方法中声明了异常,但子类重写时没有正确处理或声明异常,或者在抽象类的具体方法中处理异常的方式不恰当,影响了子类的正常使用。
    • 本质原理分析:当抽象类的抽象方法声明了异常时,子类重写该方法时要么声明相同类型或其子类型的异常,要么处理该异常。如果抽象类的具体方法抛出了异常,子类在调用该方法时需要根据实际情况进行处理。此外,异常处理应该遵循合理的逻辑,不能过度捕获或忽略异常,以免掩盖程序中的错误。
    • 代码示例
abstract class ExceptionAbstractClass {
    public abstract void performAction() throws IOException;
    public void commonMethod() throws IOException {
        // 模拟一些操作,可能抛出IOException
        throw new IOException();
    }
}
class ExceptionConcreteClass extends ExceptionAbstractClass {
    @Override
    public void performAction() throws FileNotFoundException {
        // 这里子类声明了IOException的子类型异常,符合规则
    }
    public void useCommonMethod() {
        try {
            commonMethod();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}
// 错误的子类示例
class IncorrectExceptionConcreteClass extends ExceptionAbstractClass {
    @Override
    public void performAction() {
        // 这里没有声明异常,不符合规则,因为抽象类的performAction方法声明了IOException
    }
}
  • 示例解释:在上述代码中,ExceptionAbstractClassperformAction方法声明了IOExceptionExceptionConcreteClass重写该方法时声明了FileNotFoundException,这是IOException的子类型,符合规则。ExceptionConcreteClassuseCommonMethod中正确处理了commonMethod可能抛出的IOException。而IncorrectExceptionConcreteClass重写performAction方法时没有声明异常,这是不符合规则的,会导致编译错误。

五、抽象类在设计模式应用中的误区

  1. 在模板方法模式中错误使用抽象类
    • 常见误区阐述:在使用模板方法模式时,一些开发者可能会错误地设计抽象类。比如,没有合理地定义抽象方法和具体方法,导致模板方法的流程不清晰,或者在子类中过度重写具体方法,破坏了模板方法模式的初衷。
    • 本质原理分析:模板方法模式通过在抽象类中定义一个模板方法,该方法定义了算法的骨架,其中包含一些抽象方法和具体方法。抽象方法由子类实现,具体方法提供一些通用的逻辑。子类在继承抽象类后,只需实现抽象方法,而不应该随意重写具体方法,除非有特殊需求。这样可以保证算法的整体流程在抽象类中得到控制,子类只需关注具体的实现细节。
    • 代码示例
abstract class CookingTemplate {
    // 模板方法
    public final void cook() {
        prepareIngredients();
        cookFood();
        serveFood();
    }
    protected abstract void prepareIngredients();
    protected abstract void cookFood();
    protected void serveFood() {
        System.out.println("Serving food.");
    }
}
class PizzaCooking extends CookingTemplate {
    @Override
    protected void prepareIngredients() {
        System.out.println("Preparing pizza ingredients.");
    }
    @Override
    protected void cookFood() {
        System.out.println("Cooking pizza.");
    }
}
class PastaCooking extends CookingTemplate {
    @Override
    protected void prepareIngredients() {
        System.out.println("Preparing pasta ingredients.");
    }
    @Override
    protected void cookFood() {
        System.out.println("Cooking pasta.");
    }
    // 错误示例:不应该随意重写serveFood方法,除非有特殊需求
    @Override
    protected void serveFood() {
        System.out.println("Custom serving for pasta.");
    }
}
  • 示例解释:在上述代码中,CookingTemplate是模板方法模式中的抽象类,cook方法是模板方法,定义了烹饪的整体流程。prepareIngredientscookFood是抽象方法,由子类实现。serveFood是具体方法,提供了通用的服务逻辑。PizzaCooking类正确地实现了抽象方法。而PastaCooking类重写了serveFood方法,虽然在某些情况下可能有合理需求,但一般情况下不应该随意重写,因为这破坏了模板方法模式中抽象类对整体流程的控制。
  1. 在策略模式中对抽象类角色的错误定位
    • 常见误区阐述:在策略模式中,一些开发者可能会错误地将抽象类定位为具体的策略实现,而不是作为策略的抽象定义。这会导致策略模式的结构混乱,不符合其设计原则。
    • 本质原理分析:在策略模式中,通常会有一个抽象类或接口来定义策略的抽象方法,具体的策略实现类继承该抽象类或实现该接口。抽象类在这里的作用是提供一个统一的抽象定义,使得不同的策略实现类可以按照相同的规范来实现具体的策略方法。如果将抽象类作为具体的策略实现,就无法体现策略模式的灵活性和可扩展性。
    • 代码示例
// 正确的策略模式抽象定义
abstract class SortingStrategy {
    public abstract void sort(int[] array);
}
class QuickSortStrategy extends SortingStrategy {
    @Override
    public void sort(int[] array) {
        // 快速排序实现
        System.out.println("Performing quick sort.");
    }
}
class MergeSortStrategy extends SortingStrategy {
    @Override
    public void sort(int[] array) {
        // 归并排序实现
        System.out.println("Performing merge sort.");
    }
}
// 错误的示例:将抽象类作为具体策略实现
abstract class IncorrectSortingStrategy {
    public void sort(int[] array) {
        // 这里直接在抽象类中实现了排序,不符合策略模式原则
        System.out.println("Performing incorrect sort.");
    }
}
class AnotherIncorrectSortingStrategy extends IncorrectSortingStrategy {
    // 子类没有实际的策略变化,因为抽象类已经实现了具体方法
}
  • 示例解释:在上述代码中,SortingStrategy是正确的策略模式抽象定义,QuickSortStrategyMergeSortStrategy是具体的策略实现类。而IncorrectSortingStrategy错误地在抽象类中实现了具体的排序方法,AnotherIncorrectSortingStrategy作为子类没有实际的策略变化,不符合策略模式的设计原则,使得策略模式失去了灵活性和扩展性。