本文为书籍《Java编程的逻辑》1和《剑指Java:核心原理与应用实践》2阅读笔记
很多时候,我们将对象看作属于某种数据类型,并按该类型进行操作,在一些情况下,并不能反映对象以及对对象操作的本质。为什么这么说呢?很多时候,我们实际上关心的,并不是对象的类型,而是对象的能力,只要能提供这个能力,类型并不重要。我们来看一些生活中的例子。比如要拍照,很多时候,只要能拍出符合需求的照片就行,至于是用手机拍,还是用Pad
拍,或者是用单反相机拍,并不重要,即关心的是对象是否有拍出照片的能力,而并不关心对象到底是什么类型,手机、Pad
或单反相机都可以。又如要计算一组数字,只要能计算出正确结果即可,至于是由人心算,用算盘算,用计算器算,用计算机软件算,并不重要,即关心的是对象是否有计算的能力,而并不关心对象到底是算盘还是计算器。再如要将冷水加热,只要能得到热水即可,至于是用电磁炉加热,用燃气灶加热,还是用电热水壶加热,并不重要,即重要的是对象是否有加热水的能力,而并不关心对象到底是什么类型。在这些情况中,类型并不重要,重要的是能力。那如何表示能力呢?接口。下面就来详细介绍接口,包括其概念、用法、一些细节。
接口这个概念在生活中并不陌生,比如USB
接口。计算机往往有多个USB
接口,可以插各种USB
设备,如键盘、鼠标、U
盘、摄像头、手机等。接口声明了一组能力,但它自己并没有实现这个能力,它只是一个约定。接口涉及交互两方对象,一方需要实现这个接口,另一方使用这个接口,但双方对象并不直接互相依赖,它们只是通过接口间接交互,如下图所示。
拿上面的USB
接口来说,USB
协议约定了USB
设备需要实现的能力,每个USB
设备都需要实现这些能力,计算机使用USB
协议与USB
设备交互,计算机和USB
设备互不依赖,但可以通过USB
接口相互交互。java
的接口就类似USB
协议,接口实现类类似于鼠标等USB
设备,而接口使用类类似于电脑。
接口扩展了继承,降低了耦合性。继承中类和子类之间是is-a
的关系,接口中类和接口是like-a
或has-a
的关系。
我们定义一个用于比较的接口,很多对象都可以比较,对于求最大值、求最小值、排序的程序而言,它们其实并不关心对象的类型是什么,只要对象可以比较就可以了,或者说,它们关心的是对象有没有可比较的能力。Java API
中提供了Comparable
接口,以表示可比较的能力,但它使用了泛型,本节我们定义一个MyComparable
接口,用于实现和Comparable
接口一样的功能。首先来定义这个接口,代码如下:
package com.ieening.learnInterface;
public interface MyComaprable {
int compareTo(Object other);
}
定义接口的代码解释如下:
Java
使用interface
这个关键字来声明接口,修饰符一般都是public
。interface
后面就是接口的名字MyComparable
。compareTo
,但没有定义方法体,Java 8
之前,接口内不能实现方法。接口方法不需要加修饰符,加与不加相当于都是public abstract
。再来解释compareTo
方法:
Object
类型的变量other
,表示另一个参与比较的对象。int
类型,-1
表示自己小于参数对象,0
表示相同,1
表示大于参数对象。接口与类不同,它的方法没有实现代码。定义一个接口本身并没有做什么,也没有太大的用处,它还需要至少两个参与者:一个需要实现接口,另一个使用接口。我们先来实现接口。
类可以实现接口,表示类的对象具有接口所表示的能力。以自定义Point
类举例。我们让Point
具备可以比较的能力,Point
之间怎么比较呢?我们假设按照与原点的距离进行比较,Point
类代码如下所示。
package com.ieening.learnInterface;
public class Point implements MyComaprable { // 注释1
private int x;
public int getX() {
return x;
}
public void setX(int x) {
this.x = x;
}
private int y;
public int getY() {
return y;
}
public void setY(int y) {
this.y = y;
}
public Point(int x, int y) {
this.x = x;
this.y = y;
}
public double distance() {
return Math.sqrt(x * x + y * y);
}
@Override
public int compareTo(Object other) { // 注释2
if (other instanceof Point) { // 注释3
Point otherPoint = (Point) other; // 注释4
double delta = distance() - otherPoint.distance();
if (delta < 0) {
return -1;
} else if (delta > 0) {
return 1;
} else {
return 0;
}
} else {
throw new IllegalArgumentException(other.toString() + " is not instanceof " + this.getClass().getName());
}
}
@Override
public String toString() {
return "Point [x=" + x + ", y=" + y + "]";
}
}
代码解释如下:
Java
使用implements
这个关键字表示实现接口,前面是类名,后面是接口名。Point
实现了compareTo
方法。再来解释Point
的compareTo
实现。
Point
不能与其他类型的对象进行比较,它首先检查要比较的对象是否是Point
类型,如果不是,使用throw
抛出一个异常。Point
类型,则使用强制类型转换将Object
类型的参数other
转换为Point
类型的参数otherPoint
。一个类可以实现多个接口,表明类的对象具备多种能力,各个接口之间以逗号分隔,语法如下所示:
public class Test implements Interface1, Interface2 {
// 主体代码
}
与类不同,接口不能新建new
,不能直接创建一个接口对象,对象只能通过类来创建。但可以声明接口类型的变量,引用实现了接口的类对象。比如,可以这样:
MyComparable p1 = new Point(2,3);
MyComparable p2 = new Point(1,2);
System.out.println(p1.compareTo(p2));
p1
和p2
是MyComparable
类型的变量,但引用了Point
类型的对象,之所以能赋值是因为Point
实现了MyComparable
接口。如果一个类型实现了多个接口,那么这种类型的对象就可以被赋值给任一接口类型的变量。p1
和p2
可以调用MyComparable
接口的方法,也只能调用MyComparable
接口的方法,实际执行时,执行的是具体实现类的代码。为什么Point
类型的对象非要赋值给MyComparable
类型的变量呢?在以上代码中,确实没必要。但在一些程序中,代码并不知道具体的类型,这才是接口发挥威力的地方。我们来看下面使用MyComparable
接口的例子,代码如下所示:
package com.ieening.learnInterface;
import java.util.Objects;
public class CompareUtil {
public static Object max(MyComaprable[] objs) {
if (Objects.isNull(objs) || objs.length == 0) {
return null;
}
MyComaprable max = objs[0];
for (int i = 1; i < objs.length; i++) {
if (max.compareTo(objs[i]) < 0) {
max = objs[i];
}
}
return max;
}
public static void sort(MyComaprable[] objs) {
if (!(Objects.isNull(objs) || objs.length == 0)) {
for (int i = 0; i < objs.length; i++) {
int min = i;
for (int j = i + 1; j < objs.length; j++) {
if (objs[j].compareTo(objs[min]) < 0) {
min = j;
}
}
if (min != i) {
MyComaprable temp = objs[i];
objs[i] = objs[min];
objs[min] = temp;
}
}
}
}
}
类CompUtil
提供了两个方法,max
获取传入数组中的最大值,sort
对数组升序排序,参数都是MyComparable
类型的数组,可以看出,这个类是针对MyComparable
接口编程,它并不知道具体的类型是什么,也并不关心,但却可以对任意实现了MyComparable
接口的类型进行操作。我们来看如何对Point
类型进行操作,代码如下:
package com.ieening;
import static org.junit.Assert.assertTrue;
import org.junit.Test;
import com.ieening.learnInterface.CompareUtil;
import com.ieening.learnInterface.MyComaprable;
import com.ieening.learnInterface.Point;
public class TestCompareUtil {
@Test
public void testMax() {
MyComaprable[] points = new MyComaprable[] { (MyComaprable) new Point(1, 2), (MyComaprable) new Point(2, 1),
(MyComaprable) new Point(1, 1), (MyComaprable) new Point(-1, -3) };
Point point = (Point) CompareUtil.max(points);
assertTrue(point.compareTo(new Point(-1, -3)) == 0);
}
@Test
public void testSort() {
MyComaprable[] points = new MyComaprable[] { (MyComaprable) new Point(1, 2), (MyComaprable) new Point(2, 1),
(MyComaprable) new Point(1, 1), (MyComaprable) new Point(-1, -3) };
CompareUtil.sort(points);
for (MyComaprable point : points) {
System.out.println((Point) point);
}
}
}
以上代码创建了一个Point
类型的数组points
,然后使用CompareUtil
的max
方法获取最大值,使用sort
排序,并输出结果。这里演示的是对Point
数组操作,实际上可以针对任何实现了MyComparable
接口的类型数组进行操作。这就是接口的魅力,可以说,针对接口而非具体类型进行编程,是计算机程序的一种重要思维方式。接口很多时候反映了对象以及对对象操作的本质。它的优点有很多,首先是代码复用,同一套代码可以处理多种不同类型的对象,只要这些对象都有相同的能力,如CompareUtil
。接口更重要的是降低了耦合,提高了灵活性。使用接口的代码依赖的是接口本身,而非实现接口的具体类型,程序可以根据情况替换接口的实现,而不影响接口使用者。解决复杂问题的关键是分而治之,将复杂的大问题分解为小问题,但小问题之间不可能一点关系没有,分解的核心就是要降低耦合,提高灵活性,接口为恰当分解提供了有力的工具。
前面介绍了接口的基本内容,接口还有一些细节,包括:
1、接口中的变量
接口中也可以定义变量,语法如下所示:
public interface Interface1 {
public static final int a = 0;
}
这里定义了一个变量int a
,修饰符是public static final
,但这个修饰符是可选的,即使不写,也是public static final
。这个变量可以通过接口名.变量名
的方式使用,如``Interface1.a`。
2、接口的继承
接口也可以继承,一个接口可以继承其他接口,继承的基本概念与类一样,但与类不同的是,接口可以有多个父接口,代码如下所示:
public interface IBase1 {
void method1();
}
public interface IBase2 {
void method2();
}
public interface IChild extends IBase1, IBase2 {
}
IChild
有IBase1
和IBase2
两个父类,接口的继承同样使用extends
关键字,多个父接口之间以逗号分隔。
3、类的继承与接口
类的继承与接口可以共存,换句话说,类可以在继承基类的情况下,同时实现一个或多个接口,语法如下所示:
public class Child extends Base implements IChild {
//主体代码
}
关键字extends
要放在implements
之前。
4、instanceof
instanceof
与类一样,接口也可以使用instanceof
关键字,用来判断一个对象是否实现了某接口,例如:
Point p = new Point(2,3);
if(p instanceof MyComparable){
System.out.println("comparable");
}
需要说明的是,前面介绍的都是Java 8
之前的接口概念,Java 8
和Java 9
对接口做了一些增强。在Java 8
之前,接口中的方法都是抽象方法,都没有实现体,Java 8
允许在接口中定义两类新方法:静态方法和默认方法,它们有实现体,比如:
public interface IDemo {
void hello();
public static void test() {
System.out.println("hello");
}
default void hi() {
System.out.println("hi");
}
}
test()
就是一个静态方法,可以通过IDemo.test()
调用。在接口不能定义静态方法之前,相关的静态方法往往定义在单独的类中,比如,Java API
中,Collection
接口有一个对应的单独的类Collections
,在Java 8
中,就可以直接写在接口中了,比如Comparator
接口就定义了多个静态方法。
hi()
是一个默认方法,用关键字default
表示。默认方法与抽象方法都是接口的方法,不同在于,默认方法有默认的实现,实现类可以改变它的实现,也可以不改变。引入默认方法主要是函数式数据处理的需求,是为了便于给接口增加功能。在没有默认方法之前,Java
是很难给接口增加功能的,比如List
接口,因为有太多非Java JDK
控制的代码实现了该接口,如果给接口增加一个方法,则那些接口的实现就无法在新版Java
上运行,必须改写代码,实现新的方法,这显然是无法接受的。函数式数据处理需要给一些接口增加一些新的方法,所以就有了默认方法的概念,接口增加了新方法,而接口现有的实现类也不需要必须实现。看一些例子,List
接口增加了sort
方法,其定义为:
default void sort(Comparator<? super E> c) {
Object[] a = this.toArray();
Arrays.sort(a, (Comparator) c);
ListIterator<E> i = this.listIterator();
for(Object e : a) {
i.next();
i.set((E) e);
}
}
Collection
接口增加了stream
方法,其定义为:
default Stream<E> stream() {
return StreamSupport.stream(spliterator(), false);
}
因为类可以继承以及实现接口,那么,当父类的方法和接口中默认方法有相同的函数签名时,那么实例对象调用该方法时,最终执行的是父类的,还是接口的?
答案:执行的是父类的,遵循父类优先原则。
B
类实现了多个接口。如果多个接口中出现了方法签名相同的默认方法,那么访问通过B
类的实例对象调用该方法,最终执行的是哪个接口?
答案:要求类B
必须重写该方法,否则产生编译错误。方法重写后调用方法时执行的一定是重写后的方法体。
在Java 8
中,静态方法和默认方法都必须是public
的,Java 9
去除了这个限制,它们都可以是private
的,引入private
方法主要是为了方便多个静态或默认方法复用代码,比如:这里,actionA
和actionB
两个默认方法共享了相同的common()
方法的代码。
public interface IDemoPrivate {
private void common() {
System.out.println("common");
}
default void actionA() {
common();
}
default void actionB() {
common();
}
}
马俊昌.Java编程的逻辑[M].北京:机械工业出版社,2018. ↩︎
尚硅谷教育.剑指Java:核心原理与应用实践[M].北京:电子工业出版社,2023. ↩︎