【JAVA】接口和抽象类有什么区别?

本文深入探讨Java中的接口与抽象类,阐述它们如何促进API分离、代码重用,以及Java8后接口的增强特性。讲解了接口、抽象类的区别,SOLID原则的应用,以及OOP实践中的一些权衡。

概述

接口和抽象类是 Java 面向对象设计的两个基础机制。

接口是对行为的抽象,它是抽象方法的集合,利用接口可以达到 API 定义和实现分离的目的。接口,不能实例化;不能包含任何非常量成员,任何 field 都是隐含着 public static final 的意义;同时,没有非静态方法实现,也就是说要么是抽象方法,要么是静态方法。Java 标准类库中,定义了非常多的接口,比如 java.util.List。

抽象类是不能实例化的类,用 abstract 关键字修饰 class,其目的主要是代码重用。除了不能实例化,形式上和一般的 Java 类并没有太大区别,可以有一个或者多个抽象方法,也可以没有抽象方法。抽象类大多用于抽取相关 Java 类的共用方法实现或者是共同成员变量,然后通过继承的方式达到代码复用的目的。Java 标准库中,比如 collection 框架,很多通用部分就被抽取成为抽象类,例如 java.util.AbstractList。

Java 类实现 interface 使用 implements 关键词,继承 abstract class 则是使 用 extends 关键词,我们可以参考 Java 标准库中的 ArrayList。

public class ArrayList<E> extends AbstractList<E>
        implements List<E>, RandomAccess, Cloneable, java.io.Serializable
{
    //...
}
复制代码

正文

Java 相比于其他面向对象语言,如 C++,设计上有一些基本区别,比如 Java 不支持多继承。这种限制,在规范了代码实现的同时,也产生了一些局限性,影响着程序设计结构。Java 类可以实现多个接口,因为接口是抽象方法的集合,所以这是声明性的,但不能通过扩展多个抽象类来重用逻辑。

在一些情况下存在特定场景,需要抽象出与具体实现、实例化无关的通用逻辑,或者纯调用关系的逻辑,但是使用传统的抽象类会陷入到单继承的窘境。以往常见的做法是,实现由静态方法组成的工具类(Utils),比如 java.util.Collections。

设想,为接口添加任何抽象方法,相应的所有实现了这个接口的类,也必须实现新增方法,否则会出现编译错误。对于抽象类,如果我们添加非抽象方法,其子类只会享受到能力扩展,而不用担心编译出问题。

接口的职责也不仅仅限于抽象方法的集合,其实有各种不同的实践。有一类没有任何方法的接口,通常叫作 Marker Interface,顾名思义,它的目的就是为了声明某些东西,比如我们熟知的 Cloneable、Serializable 等。这种用法,也存在于业界其他的 Java 产品代码中。

从表面看,这似乎和 Annotation 异曲同工,也确实如此,它的好处是简单直接。对于 Annotation,因为可以指定参数和值,在表达能力上要更强大一些,所以更多人选择使用 Annotation。

Java 8 增加了函数式编程的支持,所以又增加了一类定义,即所谓 functional interface,简单说就是只有一个抽象方法的接口,通常建议使用 @FunctionalInterface Annotation 来标记。Lambda 表达式本身可以看作是一类 functional interface,某种程度上这和面向对象可以算是两码事。我们熟知的 Runnable、Callable 之类,都是 functional interface。

还有一点可能让人感到意外,严格说,Java 8 以后,接口也是可以有方法实现的!

从 Java 8 开始,interface 增加了对 default method 的支持。Java 9 以后,甚至可以定义 private default method。Default method 提供了一种二进制兼容的扩展已有接口的办法。比如,我们熟知的 java.util.Collection,它是 collection 体系的 root interface,在 Java 8 中添加了一系列 default method,主要是增加 Lambda、Stream 相关的功能。我在专栏前面提到的类似 Collections 之类的工具类,很多方法都适合作为 default method 实现在基础接口里面。

你可以参考下面代码片段:

public interface Collection<E> extends Iterable<E> {
     /**
     * Returns a sequential Stream with this collection as its source 
     * ...
     **/
     default Stream<E> stream() {
         return StreamSupport.stream(spliterator(), false);
     }
  }
复制代码

面向对象设计

我们一定要清楚面向对象的基本要素:封装、继承、多态。

封装的目的是隐藏事务内部的实现细节,以便提高安全性和简化编程。封装提供了合理的边界,避免外部调用者接触到内部的细节。我们在日常开发中,因为无意间暴露了细节导致的难缠 bug 太多了,比如在多线程环境暴露内部状态,导致的并发修改问题。从另外一个角度看,封装这种隐藏,也提供了简化的界面,避免太多无意义的细节浪费调用者的精力。

继承是代码复用的基础机制,类似于我们对于马、白马、黑马的归纳总结。但要注意,继承可以看作是非常紧耦合的一种关系,父类代码修改,子类行为也会变动。在实践中,过度滥用继承,可能会起到反效果。

多态,你可能立即会想到重写(override)和重载(overload)、向上转型。简单说,重写是父子类中相同名字和参数的方法,不同的实现;重载则是相同名字的方法,但是不同的参数,本质上这些方法签名是不一样的,为了更好说明,请参考下面的样例代码:

public int doSomething() {
    return 0;
}
// 输入参数不同,意味着方法签名不同,重载的体现
public int doSomething(List<String> strs) {
    return 0;
}
// return类型不一样,编译不能通过
public short doSomething() {
    return 0;
}
复制代码

这里你可以思考一个小问题,方法名称和参数一致,但是返回值不同,这种情况在 Java 代码中算是有效的重载吗? 答案是不是的,编译都会出错的。

进行面向对象编程,掌握基本的设计原则是必须的,这里介绍最通用的部分,也就是所谓的 S.O.L.I.D 原则。

  • 单一职责(Single Responsibility),类或者对象最好是只有单一职责,在程序设计中如果发现某个类承担着多种义务,可以考虑进行拆分。
  • 开关原则(Open-Close, Open for extension, close for modification),设计要对扩展开放,对修改关闭。换句话说,程序设计应保证平滑的扩展性,尽量避免因为新增同类功能而修改已有实现,这样可以少产出些回归(regression)问题。
  • 里氏替换(Liskov Substitution),这是面向对象的基本要素之一,进行继承关系抽象时,凡是可以用父类或者基类的地方,都可以用子类替换。
  • 接口分离(Interface Segregation),我们在进行类和接口设计时,如果在一个接口里定义了太多方法,其子类很可能面临两难,就是只有部分方法对它是有意义的,这就破坏了程序的内聚性。对于这种情况,可以通过拆分成功能单一的多个接口,将行为进行解耦。在未来维护中,如果某个接口设计有变,不会对使用其他接口的子类构成影响。
  • 依赖反转(Dependency Inversion),实体应该依赖于抽象而不是实现。也就是说高层次模块,不应该依赖于低层次模块,而是应该基于抽象。实践这一原则是保证产品代码之间适当耦合度的法宝。  

OOP 原则实践中的取舍

值得注意的是,现代语言的发展,很多时候并不是完全遵守前面的原则的,比如,Java 10 中引入了本地方法类型推断和 var 类型。按照,里氏替换原则,我们通常这样定义变量:

List<String> list = new ArrayList<>();
复制代码

如果使用 var 类型,可以简化为

var list = new ArrayList<String>();
复制代码

但是,list 实际会被推断为 ArrayList<String>

ArrayList<String> list = new ArrayList<String>();
复制代码

理论上,这种语法上的便利,其实是增强了程序对实现的依赖,但是微小的类型泄漏却带来了书写的便利和代码可读性的提高,所以,实践中我们还是要按照得失利弊进行选择,而不是一味得遵循原则。  

Java 编程中,接口抽象类在结构用法上有显著区别,这些区别使得它们在不同的情境下适用,具体如下: ### 语法差异 - **定义方式**:抽象类使用 `abstract` 关键字声明类,接口使用 `interface` 关键字声明[^2]。 ```java // 抽象类 abstract class AbstractExample { // 可以包含抽象方法具体方法 } // 接口 interface InterfaceExample { // 方法默认是抽象的 } ``` - **抽象方法**:抽象类可以包含抽象方法(`abstract` 修饰)具体方法;接口所有方法默认是抽象的(隐式 `public abstract`)[^2]。 ```java // 抽象类中的抽象方法具体方法 abstract class AbstractClass { abstract void abstractMethod(); void concreteMethod() { System.out.println("This is a concrete method."); } } // 接口中的抽象方法 interface Interface { void method(); // 隐式 public abstract } ``` - **非抽象方法**:抽象类可以有具体实现的方法(包括 `static` `default`);Java 8+ 接口支持 `default` `static` 方法(需写方法体)[^2]。 ```java // 抽象类的具体方法 abstract class AbstractWithConcrete { static void staticMethod() { System.out.println("Static method in abstract class"); } default void defaultMethod() { System.out.println("Default method in abstract class"); } } // 接口的 default static 方法 interface InterfaceWithMethods { static void staticMethod() { System.out.println("Static method in interface"); } default void defaultMethod() { System.out.println("Default method in interface"); } } ``` - **成员变量**:抽象类可以有普通成员变量(实例变量或静态变量);接口只能是 `public static final` 常量(必须显式初始化)[^2]。 ```java // 抽象类的成员变量 abstract class AbstractWithVariables { int instanceVariable; static int staticVariable = 10; } // 接口的常量 interface InterfaceWithConstants { public static final int CONSTANT = 20; // 必须显式初始化 } ``` - **构造方法**:抽象类可以有构造方法(用于子类初始化父类状态);接口不能有构造方法[^2]。 ```java // 抽象类的构造方法 abstract class AbstractWithConstructor { AbstractWithConstructor() { System.out.println("Abstract class constructor"); } } // 接口不能有构造方法 // 以下代码会报错 // interface InterfaceWithConstructor { // InterfaceWithConstructor() {} // 错误 // } ``` - **访问修饰符**:抽象类的抽象方法可以是 `protected` 或 `public`(默认 `public`);接口方法默认是 `public`,不能使用其他修饰符[^2]。 ```java // 抽象类的访问修饰符 abstract class AbstractWithAccess { protected abstract void protectedAbstractMethod(); public abstract void publicAbstractMethod(); } // 接口的访问修饰符 interface InterfaceWithAccess { void method(); // 默认 public // 不能使用其他修饰符,以下代码会报错 // private void privateMethod(); } ``` - **多重继承限制**:一个类只能继承一个抽象类;一个类可以实现多个接口[^2]。 ```java // 抽象类的单继承 abstract class AbstractBase {} class SubClass extends AbstractBase {} // 接口的多实现 interface Interface1 {} interface Interface2 {} class ImplementingClass implements Interface1, Interface2 {} ``` ### 设计目的差异 - **抽象类**:作为模板或基类,封装多个子类的公共状态行为(例如 `Animal` 类包含 `sleep()` 方法抽象的 `makeSound()`),强制子类实现抽象方法,但也提供默认行为(具体方法)[^2]。 ```java abstract class Animal { void sleep() { System.out.println("Animal is sleeping"); } abstract void makeSound(); } class Dog extends Animal { @Override void makeSound() { System.out.println("Woof!"); } } ``` - **接口**:定义一组行为规范(契约),不关心实现细节(例如 `Runnable` 接口的 `run()` 方法),实现解耦:允许无关的类实现相同接口(如 `File` `URL` 都实现 `Serializable`)[^2]。 ```java interface Runnable { void run(); } class MyThread implements Runnable { @Override public void run() { System.out.println("Thread is running"); } } ```
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值