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

Java抽象类与接口的异同点

2023-01-082.4k 阅读

Java抽象类与接口的基本概念

在Java编程语言中,抽象类和接口是两个重要的概念,它们在构建灵活、可扩展的软件系统中发挥着关键作用。

抽象类

抽象类是一种不能被实例化的类,它通常包含一个或多个抽象方法。抽象方法是只有声明而没有实现的方法,其具体实现由子类来完成。抽象类的存在意义在于为一系列相关的子类提供一个通用的基类,定义一些子类共有的属性和行为框架,同时将某些具体实现延迟到子类中。例如,我们定义一个抽象类Shape

abstract class Shape {
    // 抽象方法,计算面积
    abstract double calculateArea();
    // 普通方法
    void display() {
        System.out.println("This is a shape.");
    }
}

在上述代码中,Shape类是抽象类,其中calculateArea方法是抽象方法,它没有方法体,由具体的形状子类(如圆形、矩形等)去实现该方法来计算各自的面积。而display方法是普通方法,在抽象类中已经有了具体的实现。

接口

接口是一种特殊的抽象类型,它只包含常量和抽象方法的定义,没有变量和具体方法的实现。接口用于定义一组相关的行为规范,实现接口的类必须实现接口中定义的所有抽象方法。例如,定义一个Drawable接口:

interface Drawable {
    void draw();
}

任何实现Drawable接口的类都必须提供draw方法的具体实现。接口体现了一种“has - a”关系,即实现接口的类拥有接口定义的行为。例如,一个Rectangle类实现Drawable接口:

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

这里Rectangle类通过implements关键字实现了Drawable接口,并实现了draw方法。

Java抽象类与接口的相同点

都用于实现多态

多态是Java面向对象编程的重要特性之一,抽象类和接口都为实现多态提供了有力的支持。

  • 抽象类实现多态:通过定义抽象类和抽象方法,不同的子类可以根据自身的特点来实现这些抽象方法,从而实现多态行为。例如,我们定义CircleRectangle类继承自Shape抽象类:
class Circle extends Shape {
    private double radius;

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

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

在使用时,可以通过Shape类型的引用指向不同的子类对象,从而实现多态调用:

Shape circle = new Circle(5);
Shape rectangle = new Rectangle(4, 6);
System.out.println(circle.calculateArea());
System.out.println(rectangle.calculateArea());

上述代码中,circlerectangle都是Shape类型的引用,但实际指向的是不同的子类对象,调用calculateArea方法时会根据对象的实际类型执行相应子类的实现,这就是多态的体现。

  • 接口实现多态:同样,接口也能实现多态。不同的类实现同一个接口,通过接口类型的引用可以调用不同实现类的方法。例如,假设有SquareTriangle类都实现Drawable接口:
class Square implements Drawable {
    @Override
    public void draw() {
        System.out.println("Drawing a square.");
    }
}

class Triangle implements Drawable {
    @Override
    public void draw() {
        System.out.println("Drawing a triangle.");
    }
}

使用时:

Drawable square = new Square();
Drawable triangle = new Triangle();
square.draw();
triangle.draw();

这里通过Drawable接口类型的引用调用draw方法,实现了不同类的多态行为。

都具有抽象性

抽象类和接口都具有抽象的特性,它们都不能直接被实例化。

  • 抽象类的抽象性:抽象类包含抽象方法,这些抽象方法需要子类去具体实现,这体现了抽象类的抽象性。例如前面的Shape抽象类,由于存在calculateArea抽象方法,所以不能直接创建Shape对象:
// 以下代码会报错
// Shape shape = new Shape();
  • 接口的抽象性:接口中所有的方法默认都是抽象的(JDK 8 之前),并且接口不能被实例化。例如,不能创建Drawable接口的实例:
// 以下代码会报错
// Drawable drawable = new Drawable();

这种抽象性使得它们专注于定义规范和行为框架,而具体的实现由具体的类来完成,从而提高了代码的可扩展性和灵活性。

都可作为类型使用

在Java中,抽象类和接口都可以作为一种类型来使用。

  • 抽象类作为类型:当一个类继承自抽象类时,它可以被当作抽象类的类型来处理。例如,前面的CircleRectangle类继承自Shape抽象类,它们的对象可以赋值给Shape类型的变量:
Shape circle = new Circle(5);

这里circle变量的类型是Shape,实际指向的是Circle对象。在方法参数传递等场景中,也可以使用抽象类类型作为参数类型,实现更通用的编程。例如:

void printArea(Shape shape) {
    System.out.println("The area is: " + shape.calculateArea());
}

上述方法接受一个Shape类型的参数,无论是Circle还是Rectangle对象都可以作为参数传递进来,这体现了抽象类作为类型的灵活性。

  • 接口作为类型:实现接口的类的对象可以被当作接口类型来处理。例如,前面的Rectangle类实现了Drawable接口,Rectangle对象可以赋值给Drawable类型的变量:
Drawable rectangle = new Rectangle();

同样,在方法参数传递等场景中,接口类型也能发挥类似的作用。例如:

void drawObject(Drawable drawable) {
    drawable.draw();
}

该方法接受一个Drawable类型的参数,任何实现了Drawable接口的类的对象都可以作为参数传递进来,这体现了接口作为类型在编程中的通用性和灵活性。

Java抽象类与接口的不同点

定义和实现方式

  • 抽象类的定义与实现:抽象类使用abstract关键字来定义,它可以包含变量、常量、构造函数、普通方法和抽象方法。抽象类中的普通方法可以有具体的实现,而抽象方法需要子类去实现。例如:
abstract class Animal {
    private String name;
    public static final String TYPE = "Mammal";

    public Animal(String name) {
        this.name = name;
    }

    public String getName() {
        return name;
    }

    abstract void makeSound();
}

class Dog extends Animal {
    public Dog(String name) {
        super(name);
    }

    @Override
    void makeSound() {
        System.out.println("Woof!");
    }
}

在上述代码中,Animal抽象类定义了变量name、常量TYPE、构造函数、普通方法getName和抽象方法makeSoundDog类继承自Animal抽象类,并实现了makeSound抽象方法。

  • 接口的定义与实现:接口使用interface关键字定义,在JDK 8之前,接口只能包含抽象方法和常量,所有方法默认都是publicabstract的,所有常量默认都是publicstaticfinal的。从JDK 8开始,接口可以包含默认方法(带有方法体)和静态方法。实现接口使用implements关键字,实现类必须实现接口中所有的抽象方法(除非实现类也是抽象类)。例如:
interface Flyable {
    void fly();

    // JDK 8 新增的默认方法
    default void land() {
        System.out.println("Landing.");
    }

    // JDK 8 新增的静态方法
    static void takeOff() {
        System.out.println("Taking off.");
    }
}

class Bird implements Flyable {
    @Override
    public void fly() {
        System.out.println("The bird is flying.");
    }
}

这里Flyable接口定义了抽象方法fly,以及JDK 8新增的默认方法land和静态方法takeOffBird类实现了Flyable接口,并实现了fly方法。

继承与实现关系

  • 抽象类的继承关系:Java中类只能单继承,一个类只能继承一个抽象类。例如,Dog类继承自Animal抽象类,不能再继承其他抽象类:
// 以下代码会报错,Java不支持多重继承
// class Dog extends Animal, AnotherAbstractClass {
// }

这种单继承的限制避免了多重继承带来的一些复杂性,如菱形继承问题。同时,通过继承抽象类,子类可以复用抽象类中已有的实现,并且根据自身需求对抽象方法进行具体实现。

  • 接口的实现关系:一个类可以实现多个接口,这使得Java能够实现类似于多重继承的功能。例如,一个Robot类可以同时实现FlyableDrawable接口:
class Robot implements Flyable, Drawable {
    @Override
    public void fly() {
        System.out.println("The robot is flying.");
    }

    @Override
    public void draw() {
        System.out.println("The robot is drawing.");
    }
}

通过实现多个接口,Robot类具备了多个接口定义的行为,提高了类的功能复用性和灵活性。

成员变量和方法特性

  • 抽象类的成员变量和方法:抽象类可以有各种访问修饰符的成员变量,包括privateprotectedpublic等。成员变量可以是实例变量或静态变量,并且抽象类中的普通方法可以访问这些成员变量。例如:
abstract class Vehicle {
    private int wheels;
    protected String color;
    public static int totalVehicles = 0;

    public Vehicle(int wheels, String color) {
        this.wheels = wheels;
        this.color = color;
        totalVehicles++;
    }

    public int getWheels() {
        return wheels;
    }

    abstract void move();
}

class Car extends Vehicle {
    public Car(int wheels, String color) {
        super(wheels, color);
    }

    @Override
    void move() {
        System.out.println("The car is moving.");
    }
}

在上述代码中,Vehicle抽象类有privatewheels变量、protectedcolor变量和public statictotalVehicles变量。Car类继承自Vehicle抽象类,可以访问protected修饰的color变量,并实现了move抽象方法。

抽象类中的抽象方法默认是publicabstract的,也可以显式声明为public abstract,但不能声明为privateprotected,因为抽象方法需要子类来实现,如果是privateprotected,子类将无法访问和实现该方法。

  • 接口的成员变量和方法:在JDK 8之前,接口中的变量只能是public static final的常量,不能有实例变量。例如:
interface ShapeConstants {
    double PI = 3.14159;
}

这里PI是一个public static final的常量。从JDK 8开始,接口可以有默认方法和静态方法。默认方法使用default关键字修饰,提供了一个默认的实现,实现类可以选择重写该方法。静态方法使用static关键字修饰,只能通过接口名调用,不能通过实现类的实例调用。例如:

interface MathUtils {
    static int add(int a, int b) {
        return a + b;
    }

    default int multiply(int a, int b) {
        return a * b;
    }
}

class Calculator implements MathUtils {
    // 可以选择重写默认方法
    @Override
    public int multiply(int a, int b) {
        return a * b * 2;
    }
}

在上述代码中,Calculator类实现了MathUtils接口,add是接口的静态方法,multiply是接口的默认方法,Calculator类重写了multiply默认方法。

设计目的与应用场景

  • 抽象类的设计目的与场景:抽象类主要用于抽取一组相关类的共性,为它们提供一个通用的基类。当一组类有一些共同的属性和行为,并且这些行为中有部分需要根据具体情况进行不同实现时,适合使用抽象类。例如,在图形绘制的场景中,Shape抽象类定义了图形的通用属性和行为(如计算面积、显示等),而具体的CircleRectangle等子类根据自身形状特点实现计算面积的方法。

抽象类也常用于框架设计中,提供一些基础的实现,让子类可以根据实际需求进行扩展和定制。例如,在Java的Swing框架中,JComponent类是一个抽象类,它为各种组件(如按钮、文本框等)提供了通用的属性和行为,具体的组件类继承自JComponent并进行定制化实现。

  • 接口的设计目的与场景:接口主要用于定义一组行为规范,强调的是“做什么”,而不关心“怎么做”。当需要不同的类具有相同的行为,但这些类之间没有直接的继承关系时,适合使用接口。例如,Drawable接口可以被RectangleCircleTriangle等各种形状类实现,也可以被Robot等与形状无关的类实现,只要这些类需要具备绘制的行为。

接口在构建可插拔式的软件架构中非常有用。例如,在一个游戏开发框架中,可以定义Character接口,不同类型的游戏角色(如战士、法师、刺客等)实现该接口,这样游戏开发者可以方便地添加新的角色类型,而不需要修改框架的核心代码,只需要实现Character接口的相关方法即可。

综上所述,抽象类和接口在Java编程中各有其独特的作用和应用场景,合理使用它们可以构建出更加灵活、可扩展和健壮的软件系统。在实际开发中,需要根据具体的需求和设计目标来选择使用抽象类还是接口,或者两者结合使用,以达到最佳的编程效果。