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

Java接口与抽象类的区别与联系

2021-05-204.6k 阅读

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

在Java编程中,接口(Interface)和抽象类(Abstract Class)是两个重要的概念,它们都为代码的抽象和复用提供了有力的支持,但它们有着不同的设计目的和使用方式。

抽象类

抽象类是一种不能被实例化的类,它通常用于定义一些具有共性的属性和方法,为其他子类提供一个通用的框架。抽象类可以包含抽象方法(只有声明,没有实现)和具体方法(有实现代码)。如果一个类包含至少一个抽象方法,那么这个类必须被声明为抽象类。

以下是一个简单的抽象类示例:

abstract class Animal {
    // 抽象类中的属性
    protected String name;

    // 抽象类中的具体方法
    public void eat() {
        System.out.println(name + "正在进食。");
    }

    // 抽象方法,由子类去实现
    public abstract void makeSound();
}

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

    @Override
    public void makeSound() {
        System.out.println(name + "汪汪叫。");
    }
}

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

    @Override
    public void makeSound() {
        System.out.println(name + "喵喵叫。");
    }
}

在上述代码中,Animal 类是一个抽象类,它有一个具体方法 eat() 和一个抽象方法 makeSound()DogCat 类继承自 Animal 类,并实现了 makeSound() 方法。

接口

接口是一种特殊的抽象类型,它只包含常量和抽象方法的定义,没有具体的实现代码。一个类可以实现多个接口,这使得Java具备了多继承的特性(从接口实现角度)。接口中的所有方法默认都是 publicabstract 的,所有属性默认都是 publicstaticfinal 的。

以下是一个简单的接口示例:

interface Flyable {
    // 接口中的常量
    double MAX_HEIGHT = 10000;

    // 接口中的抽象方法
    void fly();
}

class Bird implements Flyable {
    private String name;

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

    @Override
    public void fly() {
        System.out.println(name + "在天空中飞翔。");
    }
}

class Plane implements Flyable {
    private String name;

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

    @Override
    public void fly() {
        System.out.println(name + "在天空中飞行。");
    }
}

在上述代码中,Flyable 是一个接口,BirdPlane 类实现了 Flyable 接口,并实现了 fly() 方法。

语法层面的区别

定义方式

  • 抽象类:使用 abstract 关键字来定义抽象类,例如 abstract class Animal。抽象类中可以有构造函数,用于初始化子类共有的属性。
abstract class AbstractClassExample {
    private int value;

    // 抽象类的构造函数
    public AbstractClassExample(int value) {
        this.value = value;
    }

    // 抽象方法
    public abstract void abstractMethod();

    // 具体方法
    public void concreteMethod() {
        System.out.println("这是抽象类中的具体方法,value: " + value);
    }
}
  • 接口:使用 interface 关键字来定义接口,例如 interface Flyable。接口中不能有构造函数,因为接口主要用于定义行为规范,而不是创建对象。
interface InterfaceExample {
    // 接口中的常量
    int CONSTANT_VALUE = 10;

    // 抽象方法
    void interfaceMethod();
}

成员变量

  • 抽象类:可以包含各种类型的成员变量,包括普通变量、静态变量等。成员变量可以有不同的访问修饰符,如 privateprotectedpublic 等。
abstract class AbstractWithVariables {
    private int privateVar;
    protected String protectedVar;
    public static double staticVar;

    public AbstractWithVariables(int privateVar, String protectedVar) {
        this.privateVar = privateVar;
        this.protectedVar = protectedVar;
    }

    public void printVariables() {
        System.out.println("Private Var: " + privateVar);
        System.out.println("Protected Var: " + protectedVar);
        System.out.println("Static Var: " + staticVar);
    }
}
  • 接口:只能包含 publicstaticfinal 修饰的常量,并且这些修饰符可以省略不写,系统会默认加上。接口中的常量必须在定义时初始化。
interface InterfaceWithConstants {
    // 省略public static final修饰符
    int VALUE_1 = 1;
    double VALUE_2 = 2.5;
    String VALUE_3 = "接口常量";
}

方法

  • 抽象类:可以包含抽象方法和具体方法。抽象方法只有方法声明,没有方法体,必须由子类实现;具体方法有完整的方法体,可以被子类继承和重写(如果有必要)。抽象类中的方法可以有不同的访问修饰符,除了 private 修饰的方法不能被子类访问外,其他修饰符(protectedpublic)都可以使用。
abstract class AbstractWithMethods {
    // 抽象方法
    public abstract void abstractMethod();

    // 具体方法
    protected void concreteMethod() {
        System.out.println("这是抽象类中的具体方法。");
    }
}
  • 接口:只能包含抽象方法(Java 8 之前),在Java 8 及以后,接口可以包含默认方法(使用 default 关键字修饰)和静态方法。接口中的抽象方法默认是 publicabstract 的,即使不写这两个修饰符,系统也会默认加上。默认方法和静态方法都有方法体,默认方法可以被实现接口的类重写,静态方法只能通过接口名调用。
interface InterfaceWithMethods {
    // 抽象方法
    void interfaceAbstractMethod();

    // 默认方法
    default void defaultMethod() {
        System.out.println("这是接口中的默认方法。");
    }

    // 静态方法
    static void staticMethod() {
        System.out.println("这是接口中的静态方法。");
    }
}

继承与实现

  • 抽象类:使用 extends 关键字来继承抽象类,一个类只能继承一个抽象类。继承抽象类的子类必须实现抽象类中的所有抽象方法,除非子类本身也是抽象类。
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;
    }
}
  • 接口:使用 implements 关键字来实现接口,一个类可以实现多个接口。实现接口的类必须实现接口中的所有抽象方法,除非该类是抽象类。
interface Drawable {
    void draw();
}

interface Fillable {
    void fill();
}

class Rectangle implements Drawable, Fillable {
    @Override
    public void draw() {
        System.out.println("绘制矩形。");
    }

    @Override
    public void fill() {
        System.out.println("填充矩形。");
    }
}

设计目的与使用场景的区别

抽象类的设计目的与场景

  • 设计目的:抽象类主要用于抽取具有共性的属性和行为,为子类提供一个通用的框架。它强调的是一种 “is - a” 的关系,即子类是抽象类的一种具体实现。例如,DogAnimal 的一种,CircleShape 的一种。
  • 使用场景
    • 代码复用:当多个子类有一些共同的属性和方法时,可以将这些共性抽取到抽象类中,避免在子类中重复编写代码。例如,上述 Animal 类中的 eat() 方法,DogCat 类都继承了这个方法,不需要再各自实现。
    • 定义模板方法:抽象类中的抽象方法定义了子类必须实现的行为,而具体方法可以提供一些通用的实现逻辑。例如,在一个图形绘制的框架中,抽象类 Shape 可以定义 draw() 抽象方法,让子类(如 CircleRectangle 等)去具体实现如何绘制,同时抽象类可以提供一些通用的方法,如获取图形的颜色等。
abstract class AbstractTemplate {
    // 模板方法
    public final void templateMethod() {
        step1();
        step2();
        step3();
    }

    protected void step1() {
        System.out.println("执行步骤1。");
    }

    protected abstract void step2();

    protected void step3() {
        System.out.println("执行步骤3。");
    }
}

class ConcreteClass extends AbstractTemplate {
    @Override
    protected void step2() {
        System.out.println("执行步骤2(具体实现)。");
    }
}

在上述代码中,AbstractTemplate 类定义了一个模板方法 templateMethod(),其中包含了通用的步骤执行逻辑,step2() 方法由子类 ConcreteClass 具体实现。

接口的设计目的与场景

  • 设计目的:接口主要用于定义一组行为规范,不关心实现细节。它强调的是一种 “can - do” 的关系,即实现接口的类表示具备接口所定义的行为能力。例如,BirdPlane 都实现了 Flyable 接口,表示它们都具备飞行的能力。
  • 使用场景
    • 多继承特性模拟:由于Java不支持类的多继承,通过实现多个接口可以让一个类具备多种不同的行为。例如,一个 Robot 类可以同时实现 Moveable 接口和 Workable 接口,表示它既可以移动又可以工作。
    • 解耦与可扩展性:接口可以将接口定义和实现分离,使得系统具有更好的解耦性和可扩展性。例如,在一个电商系统中,Payment 接口定义了支付的行为规范,具体的支付实现类(如 AlipayPaymentWechatPayment 等)实现该接口。这样,当需要添加新的支付方式时,只需要创建一个新的实现类并实现 Payment 接口即可,而不需要修改原有的代码。
interface Payment {
    void pay(double amount);
}

class AlipayPayment implements Payment {
    @Override
    public void pay(double amount) {
        System.out.println("使用支付宝支付" + amount + "元。");
    }
}

class WechatPayment implements Payment {
    @Override
    public void pay(double amount) {
        System.out.println("使用微信支付" + amount + "元。");
    }
}

在上述代码中,Payment 接口定义了支付行为,AlipayPaymentWechatPayment 类实现了该接口,实现了不同的支付方式,系统可以根据需要灵活地选择使用哪种支付方式。

从面向对象原则角度分析

抽象类与单一职责原则

单一职责原则(SRP)要求一个类应该只有一个引起它变化的原因。抽象类在一定程度上遵循这个原则,因为它将具有共性的属性和行为抽取出来,使得子类可以专注于自身特有的行为实现。例如,Animal 抽象类将动物共有的进食行为等抽取出来,而 DogCat 子类专注于自己独特的叫声等行为实现。但如果抽象类中包含了过多不相关的属性和方法,就可能违背单一职责原则。比如,在 Animal 类中如果既包含动物的基本行为,又包含一些与动物栖息地管理相关的方法,就可能导致该抽象类承担了过多的职责。

接口与依赖倒置原则

依赖倒置原则(DIP)要求高层模块不应该依赖低层模块,二者都应该依赖其抽象;抽象不应该依赖细节,细节应该依赖抽象。接口在这方面表现得非常出色。以电商系统的支付模块为例,高层模块(如订单处理模块)只依赖 Payment 接口,而不依赖具体的支付实现类(如 AlipayPaymentWechatPayment)。这样,当需要更换支付方式时,只需要创建新的实现 Payment 接口的类,而不需要修改高层模块的代码,实现了高层模块与低层模块的解耦,符合依赖倒置原则。

性能与内存占用方面的考虑

抽象类的性能与内存占用

抽象类在编译时,其字节码文件包含了抽象类本身的结构信息、属性和方法(包括抽象方法和具体方法)的定义等。当子类继承抽象类并实例化时,子类对象在内存中除了包含自身特有的属性外,还会包含从抽象类继承过来的属性。对于抽象类中的具体方法,在子类对象调用时,由于方法的绑定机制(静态绑定或动态绑定,取决于方法是否被重写等情况),会有一定的性能开销。但如果抽象类中的具体方法被频繁调用,由于代码复用,相对于每个子类都单独实现该方法,在内存占用上可能会有优势。

接口的性能与内存占用

接口在编译时,字节码文件只包含接口的定义信息,如常量和抽象方法的声明。当类实现接口时,类的字节码文件会记录实现的接口信息。在运行时,接口的方法调用涉及到动态绑定(Java 8 之前接口只有抽象方法),这会带来一定的性能开销。从内存占用角度看,接口本身不占用对象的实例空间,因为接口不能被实例化。但实现接口的类需要为接口方法的实现分配内存空间,并且由于一个类可以实现多个接口,可能会在一定程度上增加类的字节码文件大小和内存占用。

从代码维护与扩展角度分析

抽象类的维护与扩展

当对抽象类进行维护和扩展时,如果需要在抽象类中添加新的属性或方法,可能会影响到所有的子类。例如,如果在 Animal 抽象类中添加一个新的抽象方法,所有继承自 Animal 的子类都必须实现这个新方法,这可能会导致大量的代码修改。但如果是在抽象类中添加一个具体方法,且该方法不会影响到子类的核心逻辑,那么对现有子类的影响相对较小。在扩展方面,如果需要增加新的子类,只需要让新子类继承抽象类并实现抽象方法即可,相对比较简单。

接口的维护与扩展

接口的维护和扩展相对较为灵活。在Java 8 之前,如果在接口中添加新的抽象方法,所有实现该接口的类都必须实现这个新方法,这可能会导致大量代码修改。但在Java 8 引入默认方法和静态方法后,在接口中添加默认方法和静态方法不会影响到现有的实现类,除非实现类选择重写默认方法。在扩展方面,添加新的实现接口的类非常方便,只需要实现接口中的抽象方法即可,不会对其他实现类产生影响。

总结二者的区别与联系

区别总结

  1. 定义方式:抽象类使用 abstract class 定义,接口使用 interface 定义。抽象类可以有构造函数,接口不能有构造函数。
  2. 成员变量:抽象类可以有各种类型的成员变量,接口只能有 publicstaticfinal 修饰的常量。
  3. 方法:抽象类可以有抽象方法和具体方法,方法有多种访问修饰符;接口在Java 8 之前只有抽象方法(默认 publicabstract),Java 8 及以后有默认方法和静态方法。
  4. 继承与实现:类只能继承一个抽象类,而可以实现多个接口。
  5. 设计目的:抽象类强调 “is - a” 关系,用于抽取共性;接口强调 “can - do” 关系,用于定义行为规范。
  6. 使用场景:抽象类用于代码复用和定义模板方法,接口用于模拟多继承、解耦与可扩展性。
  7. 从面向对象原则角度:抽象类在一定程度上遵循单一职责原则,接口更符合依赖倒置原则。
  8. 性能与内存占用:抽象类的具体方法调用有一定绑定开销,子类继承抽象类会包含其属性;接口方法调用是动态绑定,接口本身不占对象实例空间,但实现类可能因实现多个接口增加内存占用。
  9. 代码维护与扩展:抽象类添加新抽象方法影响所有子类,添加具体方法影响相对小;接口在Java 8 后添加默认和静态方法不影响现有实现类,添加新实现类方便。

联系总结

  1. 都是抽象类型:抽象类和接口都不能被实例化,它们都为Java的抽象编程提供了支持。
  2. 都用于抽象和复用:抽象类通过抽取共性来实现代码复用,接口通过定义行为规范来实现不同类之间的复用和交互。
  3. 都与多态相关:抽象类的子类和实现接口的类都可以通过向上转型实现多态,使得代码更加灵活和可扩展。例如,Animal 类型的变量可以指向 DogCat 对象,Flyable 类型的变量可以指向 BirdPlane 对象,在运行时根据实际对象类型调用相应的方法,实现多态行为。

在实际的Java编程中,需要根据具体的需求和场景来选择使用抽象类还是接口。如果需要抽取共性并提供一些通用的实现,同时强调继承关系,那么抽象类是一个较好的选择;如果需要定义行为规范,实现多继承特性,或者希望系统具有更好的解耦性和可扩展性,那么接口会更合适。理解它们的区别与联系,能够帮助开发者编写出更加优雅、高效和可维护的Java代码。