引言
在Java编程中,接口(Interface)是一种非常重要的概念,它不仅是面向对象编程(OOP)的基石之一,也是实现高内聚、低耦合设计原则的关键工具。接口定义了一组方法,但不提供这些方法的实现细节,而是由实现接口的类来具体实现。这种机制使得Java程序更加灵活、易于扩展和维护。
定义接口
基本语法
接口在Java中通过 interface
关键字来定义。一个接口可以包含常量(默认是 public static final
的)、方法(默认是 public abstract
的)以及从Java 8开始引入的默认方法和静态方法。接口不能包含实例变量(但可以包含静态常量)和构造方法。
public interface MyInterface {
// 常量
int MAX_SIZE = 100;
// 方法声明
void doSomething();
// Java 8 默认方法
default void defaultMethod() {
// 默认实现
}
// Java 8 静态方法
static void staticMethod() {
// 静态方法实现
}
}
详情解释
- 接口中定义的变量会在编译的时候自动加上
public static final
修饰符(注意看一下反编译后的字节码)
Java 官方文档上有这样的声明:
Every field declaration in the body of an interface is implicitly public, static, and final.
换句话说,接口可以用来作为常量类使用,还能省略掉 public static final
,看似不错,但这种选择并不可取。因为接口的本意是对方法进行抽象,而常量接口会对子类中的变量造成命名空间上的“污染”。
-
没有使用
private
、default
或者static
关键字修饰的方法是隐式抽象的,在编译的时候会自动加上public abstract
修饰符。 -
从 Java 8 开始,接口中允许有静态方法
静态方法无法由(实现了该接口的)类的对象调用,它只能通过接口名来调用,比如说
Electronic.isEnergyEfficient("LED")
。接口中定义静态方法的目的是为了提供一种简单的机制,使我们不必创建对象就能调用方法,从而提高接口的竞争力。
-
默认方法为实现该接口而不覆盖该方法的类提供默认实现。既然要提供默认实现,就要有方法体,后面不能直接使用“;”号来结束——编译器会报错。
假如我们需要在所有的实现类中追加某个具体的方法, default
方法就很有帮助。
- 接口不允许直接实例化,否则编译器会报错。
- 接口可以是空的,既可以不定义变量,也可以不定义方法。最典型的例子就是 Serializable 接口,在
java.io
包下。
public interface Serializable { }
Serializable 接口用来为序列化的具体实现提供一个标记,也就是说,只要某个类实现了 Serializable 接口,那么它就可以用来序列化了。
- 不要在定义接口的时候使用 final 关键字,否则会报编译错误,因为接口就是为了让子类实现的,而 final 阻止了这种行为。
- 接口的抽象方法不能是 private、protected 或者 final,否则编译器都会报错。
- 接口的变量是隐式
public static final
(常量),所以其值无法改变。
实现接口
类通过 implements
关键字来实现接口。一个类可以实现多个接口,但必须用逗号分隔接口名。实现接口的类必须提供接口中所有方法的实现(除非该类是抽象类)。
public class MyClass implements MyInterface {
@Override
public void doSomething() {
// 方法实现
}
// 对于默认方法,可以选择覆盖或不覆盖
@Override
public void defaultMethod() {
// 覆盖默认方法实现
}
// 静态方法不能被覆盖,但可以直接通过接口名调用
}
默认方法和静态方法
从Java 8开始,接口中可以包含默认方法和静态方法。默认方法提供了一种在不破坏现有实现的情况下向接口添加新方法的方式。静态方法则类似于工具方法,可以直接通过接口名调用。
Lambda表达式与函数式接口
Lambda 表达式
是Java 8引入的一种简洁的匿名函数实现方式。函数式接口是只包含一个抽象方法的接口(可以包含多个默认方法或静态方法)。Lambda 表达式
可以用于实现函数式接口,从而以更简洁的方式编写代码。
接口的作用
-
使某些实现类具有需要的功能,比如说,实现了
Cloneable
接口的类具有拷贝的功能,实现了Comparable
或者Comparator
的类具有比较功能。 -
Java 原则上只支持单一继承,但通过接口可以实现多重继承的目的。
如果有两个类共同继承(extends)一个父类,那么父类的方法就会被两个子类重写。然后,如果有一个新类同时继承了这两个子类,那么在调用重写方法的时候,编译器就不能识别要调用哪个类的方法了。这也正是著名的菱形问题
Java通过以下方式解决了这个问题:
-
默认方法:从Java 8开始,接口可以包含具有默认实现的方法,称为默认方法。这样,即使多个接口中有相同的方法签名,实现这些接口的类也可以直接使用其中一个接口中的默认实现,或者覆盖该方法以提供自己的实现。
-
静态方法:接口还可以包含静态方法,但这些方法不会被实现类继承。
-
显式覆盖:如果多个接口中存在相同的方法签名,并且每个接口提供了不同的默认实现,那么实现这些接口的类必须明确地覆盖这个方法并提供自己的实现。
interface InterfaceA {
default void method() {
System.out.println("InterfaceA's method");
}
}
interface InterfaceB extends InterfaceA {
default void method() {
System.out.println("InterfaceB's method");
}
}
class MyClass implements InterfaceB {
// 必须覆盖method()方法,因为InterfaceA和InterfaceB中都有相同的默认实现
@Override
public void method() {
System.out.println("MyClass's method");
}
}
public class Main {
public static void main(String[] args) {
MyClass obj = new MyClass();
obj.method(); // 输出: MyClass's method
}
}
在这个例子中:
InterfaceA
和InterfaceB
都声明了一个默认方法method()
。InterfaceB
继承了InterfaceA
并重写了method()
方法。MyClass
实现了InterfaceB
,并且必须明确地覆盖method()
方法,因为它同时继承了来自InterfaceA
和InterfaceB
的不同实现。
接口的用途
定义规范
接口为类提供了一种定义规范的方式。任何实现接口的类都必须实现接口中声明的所有方法(除非该类是抽象类)。这有助于确保类的行为一致性,并使得代码更加易于理解和维护。
假设我们需要为一组形状定义通用的行为,比如计算面积和周长。我们可以创建一个接口 Shape
,其中定义了这两个方法。
public interface Shape {
double area();
double perimeter();
}
然后,我们可以创建几个类来实现这个接口,例如 Circle
和 Rectangle
。
public class Circle implements Shape {
private double radius;
public Circle(double radius) {
this.radius = radius;
}
@Override
public double area() {
return Math.PI * radius * radius;
}
@Override
public double perimeter() {
return 2 * Math.PI * radius;
}
}
public class Rectangle implements Shape {
private double width;
private double height;
public Rectangle(double width, double height) {
this.width = width;
this.height = height;
}
@Override
public double area() {
return width * height;
}
@Override
public double perimeter() {
return 2 * (width + height);
}
}
多态
接口是实现多态性的重要手段。通过接口引用,我们可以指向实现了该接口的任何对象,并在运行时调用其实现的方法。这种机制提高了代码的灵活性和可扩展性。
现在我们可以通过 Shape
接口引用来指向 Circle
或 Rectangle
的实例,并调用它们的方法。
public class Main {
public static void main(String[] args) {
Shape circle = new Circle(5.0);
Shape rectangle = new Rectangle(4.0, 6.0);
System.out.println("Circle Area: " + circle.area());
System.out.println("Rectangle Perimeter: " + rectangle.perimeter());
}
}
解耦
接口有助于实现模块之间的解耦。通过定义接口,我们可以将模块之间的依赖关系从具体的实现类中分离出来,从而降低了模块之间的耦合度。这使得在修改或替换某个模块的实现时,不需要修改其他模块的代码。
如果我们需要改变 Circle
或 Rectangle
类的具体实现,只要它们仍然遵循 Shape
接口的定义,那么上面的 Main
类就不需要做任何改动。
框架设计
在 Java 框架设计中,接口扮演着核心角色。许多框架都通过定义接口来提供扩展点,允许开发者通过实现这些接口来定制框架的行为。例如,Spring
框架中的大量接口用于实现依赖注入、AOP等功能。
在框架设计中,接口可以作为扩展点。例如,我们可以创建一个 ShapeFactory
接口,允许用户通过实现该接口来定制创建形状的方式。
public interface ShapeFactory {
Shape createShape(String type);
}
// 具体实现
public class SimpleShapeFactory implements ShapeFactory {
@Override
public Shape createShape(String type) {
if ("circle".equals(type)) {
return new Circle(1.0); // 默认半径
} else if ("rectangle".equals(type)) {
return new Rectangle(1.0, 1.0); // 默认尺寸
}
throw new IllegalArgumentException("Unsupported shape type");
}
}
// 使用
public class Main {
public static void main(String[] args) {
ShapeFactory factory = new SimpleShapeFactory();
Shape shape = factory.createShape("circle");
System.out.println("Shape Area: " + shape.area());
}
}
通过这种方式,我们可以轻松地扩展 ShapeFactory
的实现,而不必修改客户端代码。
接口和抽象类的区别
接口(英文:Interface),在 Java 中是一个抽象类型,是抽象方法的集合;接口通过关键字 interface
来定义。接口与抽象类的不同之处在于:
- 抽象类可以有方法体的方法,但接口没有(Java 8 以前)。
- 接口中的成员变量隐式为
static final
,但抽象类不是的。 - 一个类可以实现多个接口,但只能继承一个抽象类。
- 接口和接口中的每个方法都是隐式抽象的,不需要使用
abstract
语法层面
-
抽象类可以提供成员方法的实现细节,而接口中只能存在
public abstract
方法; -
抽象类中的成员变量可以是各种类型的,而接口中的成员变量只能是
public static final
类型的; -
接口中不能含有静态代码块,而抽象类可以有静态代码块;
-
一个类只能继承一个抽象类,而一个类却可以实现多个接口。
设计层面
抽象类是对一种事物的抽象,即对类抽象,继承抽象类的子类和抽象类本身是一种 is-a
的关系。抽象类是对整个类整体进行抽象,包括属性、行为。
但是接口却是对类局部(行为)进行抽象。接口和类之间并没有很强的关联关系,举个例子来说,所有的类都可以实现 [[序列化与反序列化#序列化接口 Serializable|Serializable]],从而具有序列化的功能,但不能说所有的类和 Serializable 之间是 is-a
的关系。
继承是一个 "是不是"的关系,而 接口 实现则是 "有没有"的关系。
如果一个类继承了某个抽象类,则子类必定是抽象类的种类,而接口实现则是有没有、具备不具备的关系,比如鸟是否能飞(或者是否具备飞行这个特点),能飞行则可以实现这个接口,不能飞行就不实现这个接口。
抽象类作为很多子类的父类,它是一种模板式设计。而接口是一种行为规范,它是一种辐射式设计
接口的三种模式
在编程领域,好的设计模式能够让我们的代码事半功倍。在使用接口的时候,经常会用到三种模式,分别是策略模式、适配器模式和工厂模式。
策略模式
策略模式的思想是,针对一组算法,将每一种算法封装到具有共同接口的实现类中,接口的设计者可以在不影响调用者的情况下对算法做出改变,根据所传递的参数对象的不同而产生不同的行为。
适配器模式
适配器模式的思想是,针对调用者的需求对原有的接口进行转接。生活当中最常见的适配器就是HDMI(英语:High Definition Multimedia Interface
,中文:高清多媒体接口)线,可以同时发送音频和视频信号。
例如,Coach 接口中定义了两个方法(
defend()
和attack()
),如果类直接实现该接口的话,就需要对两个方法进行实现。
如果我们只需要对其中一个方法进行实现的话,就可以使用一个抽象类作为中间件,即适配器(AdapterCoach),用这个抽象类实现接口,并对抽象类中的方法置空(方法体只有一对花括号)
这时候,新类就可以绕过接口,继承抽象类,我们就可以只对需要的方法进行覆盖,而不是接口中的所有方法。
工厂模式
将工厂抽象成一个接口,由具体的工厂类来实现这个接口,并创建具体的产品对象。即,一个工厂接口可以创建多种不同类型的对象,每种类型的产品对象由相应的具体工厂类来创建。
接口工厂模式的基本结构通常包括以下角色:
-
抽象产品接口(Product):定义产品的标准接口规范;
-
具体产品类(ConcreteProduct):实现抽象产品接口规范;
-
抽象工厂接口(Factory):定义工厂的标准接口规范;
-
具体工厂类(ConcreteFactory):实现抽象工厂接口规范,并通过实现工厂接口的方法来创建具体的产品对象。