前言:
在掌握了继承与多态的基础后,本章我们将深入探讨Java中另一个至关重要的面向对象特性------接口。
在编程实践中,我们经常会遇到这样的场景:两种完全不相关的类却具有相同的方法。一种可能的解决办法是为这两个类定义一个抽象的父类,把相同的方法定义为abstract 抽象方法 。但这种做法存在潜在问题:由于Java不支持多父类继承,如果这两个类本身都需要从某个父类继承,就不能再继承这个仅包含抽象方法的父类了。为了解决这个问题,Java提供了接口技术:通过接口可以定义不相关类的相同行为,也可以指明类必须要实现的方法。在Java编程实践中,接口的应用非常丰富。
以数学中的两类重要数据为例。复数可以表示为"a + b*i"的形式(其中a、b为实数,i为虚数单位且i² = -1),复数支持加法、减法运算,还可以增大或缩小一个倍数。n维向量是具有n个元素的一列或一行实数,一般表示为"(a1, a2, a3, ......, an)",向量同样支持加法、减法运算,也可以增大或缩小一个倍数。++复数和向量没有共性属性,是完全不相关的两个类,没有理由为它们定义共同的父类,可它们又支持相同的方法++------这正是接口擅长解决的问题。本章就以复数和向量为例来介绍接口。
为了编写和测试本章的例子,请在IDEA中新建一个名称为ch06-01的工程,并在该工程中新建名为com.ttt.math的包。
1 接口及其应用
1.1 定义接口
为了给复数类和向量类定义共有的方法,可以定义一个名称为ICompute的接口。注意:在工程规范和最佳实践上,接口名称的命名一般采用"I+动词"的形式。在IDEA中,右击com.ttt.math包 → New → Java Class,选择Interface,输入ICompute即可创建。


ICompute接口的完整代码如下:
java
package com.ttt.math;
public interface ICompute {
public abstract void add(float... args);
public abstract void subtract(float... args);
public abstract void enlarge(int factor);
public abstract void shrink(int factor);
}
由于接口中的方法默认都是由public和abstract修饰的,因此在接口定义中可以省略方法前面的public和abstract关键字,ICompute接口可以简写为:
java
package com.ttt.math;
public interface ICompute {
void add(float... args);
void subtract(float... args);
void enlarge(int factor);
void shrink(int factor);
}
Java中,定义接口的一般形式是:
java
public interface 接口名称 {
//接口的属性;
//接口的方法;
}
1.2 实现接口
复数类和向量类虽然是两个完全不同的类,但它们都具有ICompute接口中定义的方法,因此可以在定义这两个类时,使用implements关键字指明它们必须实现ICompute接口。Complex类的完整代码如下:
java
package com.ttt.math;
public class Complex implements ICompute {
private float a, b;
public Complex(float a, float b) {
this.a = a;
this.b = b;
}
@Override
public String toString() {
return "Complex{" +
"a=" + a +
", b=" + b +
'}';
}
@Override
public void add(float... args) {
if (args.length != 2) return;
a = a + args[0];
b = b + args[1];
}
@Override
public void subtract(float... args) {
if (args.length != 2) return;
a = a - args[0];
b = b - args[1];
}
@Override
public void enlarge(int factor) {
a = a*factor;
b = b*factor;
}
@Override
public void shrink(int factor) {
a = a/factor;
b = b/factor;
}
}
Vector类的完整代码如下:
java
package com.ttt.math;
import java.util.Arrays;
public class Vector implements ICompute {
private float[] element;
public Vector(float... element) {
this.element = element;
}
@Override
public String toString() {
return "Vector{" +
"element=" + Arrays.toString(element) +
'}';
}
@Override
public void add(float... args) {
if (element.length != args.length) return;
for(int i=0; i<element.length; i++) {
element[i] += args[i];
}
}
@Override
public void subtract(float... args) {
if (element.length != args.length) return;
for(int i=0; i<element.length; i++) {
element[i] -= args[i];
}
}
@Override
public void enlarge(int factor) {
for(int i=0; i<element.length; i++) {
element[i] *= factor;
}
}
@Override
public void shrink(int factor) {
for(int i=0; i<element.length; i++) {
element[i] /= factor;
}
}
}
1.3 使用接口及instanceof关键字在接口中的应用
现在可以测试ICompute接口、Complex类和Vector类的正确性。直接创建Complex类及Vector类的对象,进行加法、减法、倍增、倍缩操作,然后显示结果。Main类代码如下:
java
package org.example;
import com.ttt.math.Complex;
import com.ttt.math.Vector;
public class Main {
public static void main(String[] args) {
Complex c = new Complex(10, 20);
c.add(100, 200);
System.out.println(c.toString());
Vector v = new Vector(10, 20, 30, 40);
v.shrink(10);
v.add(100, 200, 300, 400);
System.out.println(v.toString());
}
}
输出结果:

在Java中,不能创建接口对象,但是可以定义接口变量并使接口变量引用接口实现类的对象。修改Main类,定义ICompute接口变量compute,使其分别引用Complex类和Vector类的对象,并使用instanceof关键字判断接口变量引用的对象是否属于某个类。修改后的Main类代码如下:
java
package org.example;
import com.ttt.math.Complex;
import com.ttt.math.ICompute;
import com.ttt.math.Vector;
public class Main {
public static void main(String[] args) {
ICompute compute;
compute = new Complex(10, 20);
compute.subtract(10, 10);
System.out.println(compute instanceof Complex);
System.out.println(compute instanceof Vector);
compute = new Vector(10, 20, 30, 40, 50);
compute.enlarge(2);
System.out.println(compute instanceof Complex);
System.out.println(compute instanceof Vector);
}
}
输出结果:

当compute引用Complex对象时,compute instanceof Complex输出true,compute instanceof Vector输出false;当compute改引用Vector对象后结果恰好相反。
1.4 接口的继承
接口是可以被继承的,可以基于已经存在的接口作为父接口进一步定义新的子接口。接口的继承使用与类的继承一致的关键字extends。定义一个新接口ICompute2,使其继承ICompute接口,代码如下:
java
package com.ttt.math;
public interface ICompute2 extends ICompute {
public abstract boolean compare(float... args);
}
接口继承的一般形式是:
java
访问限定符 interface 新接口名称 extends 父接口名称 {
//接口属性;
//接口方法;
}
1.5 练习
作为练习,请自行定义Complex类和Vector类实现ICompute2接口。有两种方法完成这个练习:其一,改写Complex类和Vector类,使之实现ICompute2接口;其二,分别定义Complex类和Vector类的子类,在新定义的子类中实现ICompute2接口。
2 接口的默认方法、静态方法和私有方法
++在接口中定义的所有属性默认都有 public static final 修饰符修饰;接口中的所有方法默认都有 public abstract 修饰符修饰++。随着Java语言的进步,Java允许在接口中定义默认方法、静态方法和私有方法。
考虑这样一个应用场景:如果接口中定义了多个方法,其中某个方法对所有实现了这个接口的类来说实现逻辑都是一致的,那么就没有必要在每个实现类中都重复实现这个方法,而是可以将该方法的实现直接放置在接口定义中。实现了这个接口的任何类将自动继承这个默认方法,同时实现类还可以根据需要重写接口的默认方法。
2.1 默认方法(default)
定义接口默认方法的一般形式是:
java
访问限定符 interface 接口名 {
//接口的属性;
//接口的抽象方法;
public default 返回值 方法名(参数) {
//默认方法的实现代码
}
}
默认方法使用 default 关键字修饰并带有方法体,实现类可以直接继承使用,也可以按需重写。
2.2 静态方法(static)
接口的静态方法与接口的默认方法类似,只是将定义默认方法的 default 关键字换成 static 关键字即可。与类的静态方法类似,接口的静态方法属于接口本身,++实现包含静态方法的接口的类不可重写接口的静态方法++,并且接口的静态方法也不会被实现类继承。定义接口静态方法的一般形式是:
java
访问限定符 interface 接口名 {
//接口的属性;
//接口的方法;
public static 返回值 方法名(参数) {
//静态方法的实现代码
}
}
访问接口静态方法的一般形式是:接口名称.接口静态方法(参数);
2.3 私有方法(private)
接口的私有方法,顾名思义,就是在接口中定义的、只能被接口的默认方法使用的、被 private 访问限定符修饰的方法。其一般使用场景是将接口默认方法中的共性功能抽取出来,定义为接口的私有方法供默认方法调用。定义接口私有方法的一般形式是:
java
访问限定符 interface 接口名 {
//接口的属性;
//接口的抽象方法;
private 返回值 方法名(参数) {
//私有方法的实现代码
}
}
2.4 综合示例:IMyInterface与MyExample
下面举例说明接口的默认方法、静态方法和私有方法的定义和使用。定义一个名称为 IMyInterface 的接口,其中包含默认方法、静态方法和私有方法,然后定义一个实现该接口的 MyExample 类。IMyInterface 接口的代码如下:
java
package com.ttt.notice;
public interface IMyInterface {
public abstract void ordinary();
public static void display() {
System.out.println("这是静态方法的输出");
}
public default void info() {
System.out.println("这是默认方法的输出");
show();
}
private void show() {
System.out.println("这是私有方法的输出");
}
}
MyExample 类的代码如下:
java
package com.ttt.notice;
public class MyExample implements IMyInterface {
@Override
public void ordinary() {
System.out.println("这是普通接口方法的输出");
}
@Override
public void info() {
System.out.println("可以重写接口的默认方法" +
"也可以不重写而直接使用接口中的默认方法");
System.out.println("这是重写后的默认方法info()方法的输出");
}
}
Main 类的代码如下:
java
package org.example;
import com.ttt.notice.IMyInterface;
import com.ttt.notice.MyExample;
public class Main {
public static void main(String[] args) {
MyExample me = new MyExample();
me.ordinary();
me.info();
IMyInterface.display();
}
}
输出结果:

在上述程序中,me.ordinary() 输出普通接口方法的内容;me.info() 执行的是重写后的默认方法;IMyInterface.display() 则直接通过接口名调用接口的静态方法。
接口中四类方法的对比总结如下:
|------|----------------------|--------|------------|-----------------|
| 方法类型 | 修饰符 | 是否有方法体 | 实现类能否重写 | 调用方式 |
| 抽象方法 | public abstract(可省略) | 无 | 必须实现 | 通过接口变量或实现类对象调用 |
| 默认方法 | default | 有 | 可选重写 | 通过实现类对象调用,默认被继承 |
| 静态方法 | static | 有 | 不可重写 | 通过"接口名.方法名()"调用 |
| 私有方法 | private | 有 | 不适用,接口外不可见 | 仅供接口内部的默认方法调用 |
3 函数式接口和lambda表达式
在Java的编程实践中,有一类接口非常有用也非常常见,++这就是有且只有一个抽象方法的接口++。这里所说的"有且只有一个抽象方法",并不是说这类接口只有抽象方法,而是指在有且只有一个抽象方法的同时,还可以包含其他类型的方法,例如接口的默认方法、静态方法或私有方法。
3.1 函数式接口
有且只有一个抽象方法的接口,Java为其赋予了特定的名称,叫做"函数式接口"。为了明确地告知Java编译器某个接口是函数式接口,Java提供了专门的注解 @FunctionalInterface,该注解用于接口的定义上。

新建一个名称为 com.ttt.function 的程序包,在该包下新建一个名为 IMyFunction 的函数式接口,代码如下:
java
package com.ttt.function;
@FunctionalInterface
public interface IMyFunction {
public abstract double sinAddTan(double angle);
}
在Java中定义函数式接口的一般形式是:
java
@FunctionalInterface
访问限定符 interface 接口名称 {
//接口的属性
//接口的静态方法、默认方法、私有方法定义
//接口的抽象方法
}
下面的 MyFunctionImpl 类实现了 IMyFunction 接口:
java
package com.ttt.function;
public class MyFunctionImpl implements IMyFunction {
@Override
public double sinAddTan(double angle) {
double x = Math.sin(angle) + Math.tan(angle);
System.out.println("这个幅度的sin值+tan值是:" + x);
return x;
}
}
Main 类的代码如下:
java
package org.example;
import com.ttt.function.MyFunctionImpl;
public class Main {
public static void main(String[] args) {
MyFunctionImpl mfi = new MyFunctionImpl();
double t = mfi.sinAddTan(20);
System.out.println(t);
}
}
3.2 使用匿名内部类实现接口
匿名内部类是Java内部类的一种,适用于仅使用一个局部类一次的场景。修改 Main 类为如下代码,可以实现与上一节同样的功能:
java
package org.example;
import com.ttt.function.IMyFunction;
public class Main {
public static void main(String[] args) {
//下面的代码定义了实现IMyFunction接口的匿名内部类并创建了这个类的对象
IMyFunction im = new IMyFunction() {
@Override
public double sinAddTan(double angle) {
double x = Math.sin(angle) + Math.tan(angle);
System.out.println("这个幅度的sin值+tan值是:" + x);
return x;
}
};
double t = im.sinAddTan(20);
System.out.println(t);
}
}
输出结果:
3.3 lambda入门:使用lambda表达式实现函数式接口
lambda表达式是实现函数式接口最为优雅和最为专业的方式。直接使用lambda表达式实现 IMyFunction 接口,修改后的 Main 类代码如下:
java
package org.example;
import com.ttt.function.IMyFunction;
public class Main {
public static void main(String[] args) {
IMyFunction im = (angle)-> {
double x = Math.sin(angle) + Math.tan(angle);
System.out.println("这个幅度的sin值+tan值是:" + x);
return x;
};
double t = im.sinAddTan(20);
System.out.println(t);
}
}
3.4 lambda表达式基本语法
lambda表达式的一般形式是:
java
(参数列表) -> {
代码块;
}
lambda表达式由以下三个核心部分组成:
- 参数列表:对应函数式接口中抽象方法的参数。当只有一个参数时可以省略圆括号;参数类型可以明确声明,也可以省略(由Java根据接口中的方法定义自动推断),注意如果要省略,每个参数的类型都要省略。
- ->:lambda表达式的符号,可以理解为"函数式接口中抽象方法的具体实现代码由如下代码块定义"。
- 代码块:由一系列程序语句组成,可以是表达式也可以是代码块,等同于方法的方法体。如果代码块中只有一条语句,大括号 { } 可以省略;如果直接是一条表达式语句,在省略大括号的同时还可以直接将表达式写在 -> 后面,不需要使用 return 语句。
3.5 接口方法引用
++使用接口方法引用可以使函数式接口变量直接访问某个类或者实例中已经存在的方法。++主要应用场景:如果某个类已经定义了满足函数式接口的方法,那么可以使函数式接口变量直接指向这个类的实现方法,而不需要自己重新编写函数式接口方法的代码。
在 com.ttt.function 包下新建函数式接口 IMyFunction2,其中定义抽象方法 doSomeMath(double a),代码如下:
java
package com.ttt.function;
@FunctionalInterface
public interface IMyFunction2 {
public abstract double doSomeMath(double num);
}
再定义一个普通的Java类 AnOrdinary,它无需实现任何接口,代码如下:
java
package com.ttt.function;
public class AnOrdinary {
private String currency;
private double rooms;
public AnOrdinary(String currency, int rooms) {
this.currency = currency;
this.rooms = rooms;
}
@Override
public String toString() {
return "AnOrdinary{" +
"currency='" + currency + '\'' +
", rooms=" + rooms +
'}';
}
public double method(double a) {
rooms = rooms * a;
return rooms;
}
}
Main 类的代码如下:
java
package org.example;
import com.ttt.function.AnOrdinary;
import com.ttt.function.IMyFunction2;
public class Main {
public static double addSomeThing(double a) {
System.out.println("Main类的静态方法");
return 100+a;
}
public static void main(String[] args) {
IMyFunction2 im21 = Main::addSomeThing;
double d1 = im21.doSomeMath(200);
System.out.println(d1);
System.out.println("-------------------------");
AnOrdinary aor = new AnOrdinary("RMB", 213);
IMyFunction2 im22 = aor::method;
double d2 = im22.doSomeMath(10);
System.out.println(d2);
}
}
输出结果:

在上述代码中,Main::addSomeThing 是引用 Main 类静态方法的写法,aor::method 是引用实例对象方法的写法,程序分别输出 300.0 和 2130.0。
4 接口、匿名内部类和lambda表达式应用最佳实践
引入接口 技术机制的初衷在于规范类的行为。尽管类所封装的对象可能完全不同,但它们可以具备相似的行为,接口正是用来统一这种行为的规范。通过让不同的类实现相同的接口,可以使这些类具备相似的行为特征,这也是Java实现模块化程序设计的重要支撑。在系统设计时,通常先定义好系统应具备的行为(即一组接口),再由编码人员通过类来实现这些接口,从而完成系统的编码。
一旦定义了接口,可以采用多种机制对其进行实现:既可以使用独立的Java类,也可以使用匿名内部类,对于函数式接口 还可以使用lambda表达式。选择实现机制的基本原则是:如果接口比较简单,特别是函数式接口,建议使用匿名内部类或lambda表达式;如果接口比较复杂,则建议使用独立类的方式。具体选择建议如下表所示:
|-----------|--------------------------|
| 实现方式 | 适用场景 |
| 独立类实现 | 接口较复杂,或实现逻辑需要在多处复用 |
| 匿名内部类 | 接口较简单,且仅需在局部使用一次 |
| lambda表达式 | 函数式接口(有且仅有一个抽象方法),实现逻辑简洁 |
5 案例:按价格排序不同产品
下面通过一个综合案例来结束本章内容的介绍。该案例对不同类型的食品按照其价格进行排序并显示,涉及类、类的继承、接口、将接口作为方法参数,以及将lambda表达式作为实参进行传递。
5.1 案例任务
编写Java代码,可以比较任何食品的价格高低并进行排序。程序要求首先定义一个函数式接口IComparator,该接口包含一个抽象方法compare(),可对食品的价格高低进行比较,并根据两个食品的价格高低返回-1、0和1。然后定义多个食品类,包括父类Eating类及其子类Cookie、Bread和Cake,并创建包含这些子类对象的一个数组,再定义一个可以对数组中的食品对象按价格进行排序的方法。最后,编写一个测试类,用于测试代码的正确性。
5.2 任务分析
分析案例任务要求,从中提取出要定义的接口、类及其相应功能要求。基于分析结果可知,需要定义如下接口和类:
- IComparator函数式接口;
- 食品父类Eating及其子类Cookie、Bread、Cake;
- Using类:创建食品数组,定义排序方法;
- Main类:测试程序的正确性。
5.3 任务实施
说明:在ch06-01工程下新建com.ttt.food包,在其下依次新建IComparator接口、Eating类、Cake类、Bread类、Cookie类、Using类。

IComparator接口是一个函数式接口:
java
package com.ttt.food;
@FunctionalInterface
public interface IComparator {
public abstract float compare(Eating t1, Eating t2);
}
Eating类的代码如下:
java
package com.ttt.food;
public class Eating {
private String name;
private float price;
private String producer;
public Eating(String name, float price, String producer) {
this.name = name;
this.price = price;
this.producer = producer;
}
public String getName() {
return name;
}
public float getPrice() {
return price;
}
public String getProducer() {
return producer;
}
}
Cake类的代码如下:
java
package com.ttt.food;
public class Cake extends Eating {
private String ingredient; //蛋糕的主要成分
private int calorie; //能量大小
public Cake(String name, float price, String producer, String ingredient, int calorie) {
super(name, price, producer);
this.ingredient = ingredient;
this.calorie = calorie;
}
public String getIngredient() {
return ingredient;
}
public int getCalorie() {
return calorie;
}
@Override
public String toString() {
return "Cake{" +
"name=" + super.getName() +
", price=" + super.getPrice() +
", producer=" + super.getProducer() +
", ingredient='" + ingredient + '\'' +
", calorie=" + calorie +
'}';
}
}
Bread类的代码如下:
java
package com.ttt.food;
public class Bread extends Eating {
private String date; //面包的生产日期
public Bread(String name, float price, String producer, String date) {
super(name, price, producer);
this.date = date;
}
public String getDate() {
return date;
}
@Override
public String toString() {
return "Bread{" +
"name=" + super.getName() +
", price=" + super.getPrice() +
", producer=" + super.getProducer() +
", date='" + date + '\'' +
'}';
}
}
Cookie类的代码如下:
java
package com.ttt.food;
public class Cookie extends Eating {
private String shape; //饼干的外形
public Cookie(String name, float price, String producer, String shape) {
super(name, price, producer);
this.shape = shape;
}
public String getShape() {
return shape;
}
@Override
public String toString() {
return "Cookie{" +
"name=" + super.getName() +
", price=" + super.getPrice() +
", producer=" + super.getProducer() +
", shape=" + shape +
'}';
}
}
Using类的代码如下:
java
package com.ttt.food;
import java.util.Random;
public class Using {
private Eating[] foods;
public Using(int n) {
Random random = new Random();
foods = new Eating[n];
for(int i=0; i<n; i++) {
foods[i] = switch (random.nextInt(3)) {
case 0 -> new Cake("Cake"+i, random.nextFloat(50),
"P"+i, "Rice", random.nextInt(100));
case 1 -> new Bread("Bread"+i, random.nextFloat(20),
"P"+i, "2023年09月20日");
case 2 -> new Cookie("Cookie"+i, random.nextFloat(30),
"P"+i, "圆形");
default -> null;
};
}
}
public void sort(IComparator comparator) {
for(int i=0; i<foods.length-1; i++) {
for(int j=i+1; j<foods.length; j++) {
float r = comparator.compare(foods[i], foods[j]);
if (r > 0) {
Eating e = foods[i];
foods[i] = foods[j];
foods[j] = e;
}
}
}
}
public void show() {
for(Eating e : foods)
System.out.println(e.toString());
}
}
Main类的代码如下:
java
package org.example;
import com.ttt.food.IComparator;
import com.ttt.food.Using;
public class Main {
public static void main(String[] args) {
Using u = new Using(5);
IComparator lambda = (e1, e2) -> (e1.getPrice() - e2.getPrice());
u.sort(lambda);
u.show();
}
}
程序运行后的一种输出结果如下:
Bread{name=Bread0, price=2.331642, producer=P0, date='2023年09月20日'}
Cookie{name=Cookie2, price=5.1906557, producer=P2, shape=圆形}
Cake{name=Cake1, price=10.386335, producer=P1, ingredient='Rice', calorie=54}
Bread{name=Bread4, price=15.738033, producer=P4, date='2023年09月20日'}
Cake{name=Cake3, price=41.081627, producer=P3, ingredient='Rice', calorie=22}
6 练习:计算空间中两点的距离
编写一个Java程序,计算空间中任意两点之间的距离。程序要求首先定义一个接口IDistance,该接口定义了计算空间中任意两点之间距离的方法,以及一个显示空间任意点对象信息的默认方法;然后定义三种类型的空间点类:二维空间点、三维空间点、四维空间点,这些类都要实现IDistance接口;最后编写一个测试类,用于测试代码的正确性。
总结
本章围绕Java面向对象编程中的接口展开了全面讲解,核心要点归纳如下:
- 接口使用interface关键字定义,用于规范不相关类的相同行为;
- 类通过implements关键字实现接口,并且必须重写接口中的所有抽象方法;
- 接口变量可以引用实现类对象,也可以使用instanceof关键字判断对象类型;
- 接口之间可以通过extends关键字实现继承;
- 接口中的属性默认是public static final,方法默认是public abstract,此外还支持默认方法(default)、静态方法(static)和私有方法(private);
- 有且只有一个抽象方法的接口称为函数式接口,可使用@FunctionalInterface注解进行标注;
- 函数式接口可以通过独立类、匿名内部类或lambda表达式来实现,lambda表达式的语法格式为(参数列表) -> { 代码块 };
- 方法引用可以让函数式接口变量直接指向已有方法,进一步简化代码;
- 接口是Java实现模块化程序设计的重要支撑。
掌握接口是迈向Java高级编程的重要一步,希望大家多加练习、融会贯通。
