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

Java抽象类的重写与重载

2023-08-194.7k 阅读

Java抽象类的重写

在Java中,抽象类是一种不能被实例化的类,它通常包含抽象方法。抽象方法是只有声明而没有实现的方法。当一个子类继承一个抽象类时,它必须重写(override)抽象类中的抽象方法,除非该子类本身也是抽象类。

重写的规则

  1. 方法签名必须相同:重写方法的方法名、参数列表和返回类型必须与被重写方法完全相同。在Java 5.0及以上版本,如果被重写方法的返回类型是一个类,重写方法的返回类型可以是该类的子类,这被称为协变返回类型。例如:
abstract class Animal {
    public abstract Animal createInstance();
}

class Dog extends Animal {
    @Override
    public Dog createInstance() {
        return new Dog();
    }
}
  1. 访问修饰符:重写方法的访问修饰符不能比被重写方法的访问修饰符更严格。例如,如果抽象类中的抽象方法是protected,子类重写该方法时不能使用private,但可以使用publicprotected
abstract class Parent {
    protected abstract void abstractMethod();
}

class Child extends Parent {
    @Override
    public void abstractMethod() {
        // 实现代码
    }
}
  1. 异常声明:重写方法不能抛出比被重写方法更多的异常,或者不能抛出比被重写方法声明的异常更宽泛的异常类型。例如,如果抽象类中的抽象方法声明抛出IOException,子类重写该方法时不能抛出Exception,但可以抛出FileNotFoundException,因为FileNotFoundExceptionIOException的子类。
import java.io.IOException;

abstract class IOExceptionClass {
    protected abstract void ioMethod() throws IOException;
}

class SubIOExceptionClass extends IOExceptionClass {
    @Override
    protected void ioMethod() throws java.io.FileNotFoundException {
        // 实现代码
    }
}

重写的本质

重写的本质在于实现多态性。多态性允许我们通过父类的引用调用子类中重写的方法,从而根据对象的实际类型来执行不同的行为。这使得代码更加灵活和可维护。当一个对象调用重写方法时,Java虚拟机(JVM)会根据对象的实际类型来确定调用哪个版本的方法,而不是根据引用的类型。

例如:

abstract class Shape {
    public abstract double getArea();
}

class Circle extends Shape {
    private double radius;

    public Circle(double radius) {
        this.radius = radius;
    }

    @Override
    public double getArea() {
        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 getArea() {
        return width * height;
    }
}

public class Main {
    public static void main(String[] args) {
        Shape circle = new Circle(5);
        Shape rectangle = new Rectangle(4, 6);

        System.out.println("Circle area: " + circle.getArea());
        System.out.println("Rectangle area: " + rectangle.getArea());
    }
}

在上述代码中,circlerectangle都是Shape类型的引用,但它们实际指向的是CircleRectangle对象。当调用getArea方法时,JVM会根据对象的实际类型来调用相应的重写方法,从而实现了多态性。

重写与抽象类的关系

抽象类为子类提供了一个通用的框架,子类通过重写抽象类中的抽象方法来实现具体的行为。这种机制使得代码具有更好的层次性和结构性。例如,在一个图形绘制的应用中,可以定义一个抽象类GraphicObject,其中包含抽象方法draw。然后,不同的图形类如CircleRectangle等继承GraphicObject并重写draw方法来实现各自的绘制逻辑。

abstract class GraphicObject {
    public abstract void draw();
}

class Circle extends GraphicObject {
    @Override
    public void draw() {
        System.out.println("Drawing a circle");
    }
}

class Rectangle extends GraphicObject {
    @Override
    public void draw() {
        System.out.println("Drawing a rectangle");
    }
}

Java抽象类的重载

重载(overload)是指在同一个类中定义多个方法,这些方法具有相同的方法名,但参数列表不同(参数个数、参数类型或参数顺序不同)。在抽象类中也可以进行方法重载。

重载的规则

  1. 参数列表必须不同:方法名相同的情况下,参数的个数、类型或顺序必须至少有一个不同。例如:
abstract class MathUtils {
    public abstract int add(int a, int b);
    public abstract double add(double a, double b);
    public abstract int add(int a, int b, int c);
}

class MathUtilsImpl extends MathUtils {
    @Override
    public int add(int a, int b) {
        return a + b;
    }

    @Override
    public double add(double a, double b) {
        return a + b;
    }

    @Override
    public int add(int a, int b, int c) {
        return a + b + c;
    }
}
  1. 返回类型:返回类型与方法重载无关。即使两个方法的返回类型不同,但参数列表相同,它们也不是重载方法,这种情况下会导致编译错误。例如:
abstract class WrongOverload {
    // 编译错误,参数列表相同
    public abstract int method(int a);
    public abstract double method(int a);
}
  1. 访问修饰符:重载方法的访问修饰符可以不同,只要参数列表满足重载的要求即可。例如:
abstract class AccessOverload {
    public abstract void method(int a);
    protected abstract void method(String s);
}

重载的本质

重载的本质是为了提供一组功能相似但参数使用方式不同的方法,以方便调用者根据实际情况选择最合适的方法。它增加了代码的灵活性和可读性。例如,在一个字符串处理的抽象类中,可以重载一个方法来实现不同类型数据转换为字符串的功能。

abstract class StringConverter {
    public abstract String convertToString(int num);
    public abstract String convertToString(double num);
    public abstract String convertToString(Object obj);
}

class StringConverterImpl extends StringConverter {
    @Override
    public String convertToString(int num) {
        return String.valueOf(num);
    }

    @Override
    public String convertToString(double num) {
        return String.valueOf(num);
    }

    @Override
    public String convertToString(Object obj) {
        return obj.toString();
    }
}

重载与重写的区别

  1. 定义位置:重写发生在子类与父类(包括抽象类)之间,而重载发生在同一个类中。
  2. 方法签名要求:重写要求方法签名(除了返回类型可以是协变返回类型外)必须与被重写方法完全相同,而重载要求方法签名(参数列表)必须不同。
  3. 多态性体现:重写是实现运行时多态性的基础,通过父类引用调用子类重写方法实现动态绑定;而重载是编译时多态性,在编译阶段根据参数列表确定调用哪个方法。
  4. 访问修饰符与异常声明:重写方法的访问修饰符不能比被重写方法更严格,异常声明不能比被重写方法更宽泛;重载方法在访问修饰符和异常声明方面没有这样的限制,只要满足参数列表不同即可。

抽象类中重写与重载的综合应用

在实际开发中,常常会在抽象类及其子类中同时使用重写和重载。例如,在一个游戏开发中,有一个抽象类Character代表游戏角色,其中有抽象方法attack。不同的角色子类如WarriorMage等会重写attack方法来实现各自的攻击方式。同时,Character类中可以重载attack方法,以提供不同参数形式的攻击行为。

abstract class Character {
    // 抽象方法
    public abstract void attack();
    // 重载方法
    public abstract void attack(int power);
}

class Warrior extends Character {
    @Override
    public void attack() {
        System.out.println("Warrior attacks with sword");
    }

    @Override
    public void attack(int power) {
        System.out.println("Warrior attacks with sword with power " + power);
    }
}

class Mage extends Character {
    @Override
    public void attack() {
        System.out.println("Mage casts a spell");
    }

    @Override
    public void attack(int power) {
        System.out.println("Mage casts a spell with power " + power);
    }
}

这样,通过重写实现了不同角色的差异化攻击行为,通过重载提供了更灵活的攻击方式调用。

重写和重载的常见错误及注意事项

重写的常见错误

  1. 方法签名不匹配:忘记将重写方法的参数列表与被重写方法保持一致,或者返回类型不符合协变返回类型规则。例如:
abstract class ParentClass {
    public abstract void method(int a);
}

class ChildClass extends ParentClass {
    // 编译错误,参数列表不同
    @Override
    public void method(long a) {
    }
}
  1. 访问修饰符错误:使用了比被重写方法更严格的访问修饰符。例如:
abstract class ParentAccess {
    protected abstract void method();
}

class ChildAccess extends ParentAccess {
    // 编译错误,private比protected更严格
    @Override
    private void method() {
    }
}
  1. 异常声明错误:重写方法抛出了比被重写方法更宽泛的异常。例如:
import java.io.IOException;

abstract class ParentException {
    protected abstract void ioMethod() throws IOException;
}

class ChildException extends ParentException {
    // 编译错误,Exception比IOException更宽泛
    @Override
    protected void ioMethod() throws Exception {
    }
}

重载的常见错误

  1. 参数列表不唯一:定义了两个方法名相同且参数列表也相同的方法,即使返回类型不同,这也会导致编译错误。例如:
abstract class DuplicateOverload {
    // 编译错误,参数列表相同
    public abstract int method(int a);
    public abstract double method(int a);
}
  1. 忽略参数类型的细微差别:有时候,参数类型看起来不同,但在某些情况下会导致歧义。例如,intInteger虽然是不同类型,但在自动装箱和拆箱的情况下,可能会导致调用不明确。
abstract class AmbiguousOverload {
    public abstract void method(int a);
    public abstract void method(Integer a);
}

class AmbiguousOverloadCall {
    public static void main(String[] args) {
        AmbiguousOverload obj = null;
        int num = 5;
        // 编译错误,调用不明确
        obj.method(num);
    }
}

注意事项

  1. 重写时的@OverRide注解:在重写方法时,建议使用@Override注解。这个注解不是必需的,但它可以帮助编译器检查是否正确重写了方法。如果使用了该注解但方法签名与被重写方法不匹配,编译器会报错。
abstract class ParentAnnotation {
    public abstract void method();
}

class ChildAnnotation extends ParentAnnotation {
    @Override
    public void method() {
    }
}
  1. 重载方法的可读性:虽然重载可以提供灵活的方法调用,但过多的重载可能会降低代码的可读性。在设计重载方法时,应确保参数列表的差异是有意义的,并且方法名能够准确反映方法的功能。
  2. 继承体系中的重载与重写:在复杂的继承体系中,要注意重载和重写的相互影响。当子类重写了父类的方法后,子类中重载的方法可能会与父类中的方法产生混淆。此时,需要仔细设计和测试,确保方法调用的正确性。

重写和重载在设计模式中的应用

重写在设计模式中的应用

  1. 策略模式:策略模式定义了一系列算法,将每个算法封装起来,并使它们可以相互替换。这通常通过抽象类和重写来实现。例如,有一个抽象类PaymentStrategy,其中包含抽象方法pay。不同的支付方式如CreditCardPaymentPayPalPayment等继承PaymentStrategy并重写pay方法来实现具体的支付逻辑。
abstract class PaymentStrategy {
    public abstract void pay(double amount);
}

class CreditCardPayment extends PaymentStrategy {
    @Override
    public void pay(double amount) {
        System.out.println("Paying " + amount + " with credit card");
    }
}

class PayPalPayment extends PaymentStrategy {
    @Override
    public void pay(double amount) {
        System.out.println("Paying " + amount + " with PayPal");
    }
}
  1. 模板方法模式:模板方法模式在一个方法中定义一个算法的骨架,而将一些步骤延迟到子类中实现。抽象类中定义模板方法,其中包含调用抽象方法的逻辑,子类通过重写抽象方法来提供具体实现。例如,有一个抽象类AbstractGame,其中有模板方法playGame和抽象方法initializestartGameendGame。不同的游戏子类如ChessGameCardGame等继承AbstractGame并重写这些抽象方法。
abstract class AbstractGame {
    public final void playGame() {
        initialize();
        startGame();
        endGame();
    }

    protected abstract void initialize();
    protected abstract void startGame();
    protected abstract void endGame();
}

class ChessGame extends AbstractGame {
    @Override
    protected void initialize() {
        System.out.println("Initializing chess game");
    }

    @Override
    protected void startGame() {
        System.out.println("Starting chess game");
    }

    @Override
    protected void endGame() {
        System.out.println("Ending chess game");
    }
}

class CardGame extends AbstractGame {
    @Override
    protected void initialize() {
        System.out.println("Initializing card game");
    }

    @Override
    protected void startGame() {
        System.out.println("Starting card game");
    }

    @Override
    protected void endGame() {
        System.out.println("Ending card game");
    }
}

重载在设计模式中的应用

  1. 建造者模式:建造者模式将一个复杂对象的构建与它的表示分离,使得同样的构建过程可以创建不同的表示。在建造者类中,通常会重载一些方法来提供不同的构建步骤参数。例如,有一个CarBuilder抽象类,其中有重载的buildEngine方法来根据不同需求构建汽车发动机。
abstract class CarBuilder {
    public abstract void buildEngine();
    public abstract void buildEngine(int horsepower);
    // 其他构建方法
}

class SportsCarBuilder extends CarBuilder {
    @Override
    public void buildEngine() {
        System.out.println("Building a standard sports car engine");
    }

    @Override
    public void buildEngine(int horsepower) {
        System.out.println("Building a high - performance sports car engine with " + horsepower + " horsepower");
    }
}
  1. 工厂模式:在工厂模式中,工厂类可能会重载创建对象的方法。例如,一个ShapeFactory类可能会有重载的createShape方法,根据不同的参数创建不同类型的形状对象。
abstract class ShapeFactory {
    public abstract Shape createShape();
    public abstract Shape createShape(String shapeType);
}

class CircleFactory extends ShapeFactory {
    @Override
    public Shape createShape() {
        return new Circle();
    }

    @Override
    public Shape createShape(String shapeType) {
        if ("circle".equalsIgnoreCase(shapeType)) {
            return new Circle();
        }
        return null;
    }
}

总结

重写和重载是Java中非常重要的特性,它们在抽象类及其子类的设计和实现中发挥着关键作用。重写通过实现抽象类中的抽象方法,实现了运行时多态性,使得代码可以根据对象的实际类型执行不同的行为。重载则通过在同一个类中提供多个具有相同方法名但参数列表不同的方法,增加了代码的灵活性和可读性。

在实际开发中,正确理解和运用重写与重载,可以提高代码的质量、可维护性和可扩展性。同时,注意避免重写和重载过程中常见的错误,如方法签名不匹配、访问修饰符错误、异常声明错误等。

此外,重写和重载在各种设计模式中也有广泛应用,它们为构建灵活、可复用的软件系统提供了强大的支持。无论是策略模式、模板方法模式,还是建造者模式、工厂模式等,都离不开重写和重载这两个重要的特性。通过深入理解和掌握重写与重载,Java开发者能够更好地设计和实现复杂的软件系统。