接口(interface)
学习《Java Core》ed.11 读书笔记
概念
Java语言中,接口不是一个类,而是对于要实现这个接口的类的一系列要求(requirements)。如果要实现这个接口,那么其中要求的所有方法都要实现。接口中所有的方法都自动改为public
的
接口可以定义常量(static final
),但是不能定义实例成员变量。JDK8之后允许接口中存在实现的方法,这些方法也没办法访问实例成员变量(不存在)。在实现类中,接口的方法要声明为public
,否则编译器会报错(方法提供权限限制--默认包内访问)
接口的必要性:如果一个服务需要接收一些实现了某个方法的对象,那么强制这些对象实现特定方法的机制就是接口
Comparable
接口的例子。如果父类实现了这个接口(泛型),那么子类和父类比较的时候可能会出现问题。解决问题有两种方式:
- 如果子类的比较含义和父类不同,那么在比较子类对象的时候必须要进行类型判断
- 如果比较子类有一个共有的算法,那么可以在父类中声明
compareTo
方法并声明为final
,在这个方法中把共有的逻辑写好(符合继承的意义)
注:Comparable
接口有泛型实现,建议使用
注:整数比较使用Integer.compare()
方法,浮点数使用Double.compare()
注:Comparable
接口文档中需要compareTo()
方法和equals()
方法兼容。BigDecimal
是一个例外
接口的属性
接口不是类,所以不能使用new
关键字来实例化对象。但是可以声明接口变量,并且这个变量只能引用实现了这个接口的类的对象。可以使用instanceof
来判断一个实例是否实现了某个接口
接口可以继承,这允许构建接口链,从范围最大的接口到最详细的接口
接口中不能定义实例变量,但是可以提供常量(public static final
修饰)
根据阿里开发手册以及Java官方建议,不在接口中的方法上加任何其它的修饰符
接口中的常量在实现类中可以直接使用而不需要使用类名.constant
的方式,但是这种方式和接口的意义不符,不建议使用(这种情况用常量类就好了)
类可以实现多个接口,这样使得类的行为的实现灵活度很高(没有多继承)
接口和抽象类
为什么不用抽象类而用接口,其原因还在于Java的单继承机制,如果用抽象类来表示一些多种类型泛用的方法,那么这些继承的子类就没办法再继承别的父类。接口的实现避免Java变成C++这种复杂的语言,也避免了Eiffel的低效率多继承实现
静态和私有方法
JDK8之后,在接口中可以添加静态方法。这在技术原因上没有不能实现的理由,只是简单地认为它违背了接口是描述抽象行为规范的意义。这里的意义是,当你实现自己的接口时,没必要再另外写一个类来实现一些工具方法,可以直接在接口中定义好。例如标准库中的(Paths
类和Path
接口)
JDK9之后,接口中可以定义private
方法,这些方法可以是static
的,主要作为接口中其它方法的辅助方法
Default方法
可以在接口中提供一些方法的默认实现,通过在方法签名的开头带上default
修饰符
方法的默认实现在那些实现类必须要实现的方法上意义不大,但是在某些场景是有意义的,以Iterator
为例
interface Iterator {
boolean hasNext();
E next();
// 默认实现
default remove() {throw new UnSurpportedOperationException();}
}
一般我们实现一个迭代器,如果让它是只读的,那么我们可以忽略这个remove
方法
默认方法中是可以调用接口中其他的方法的
使用接口最重要的场景是interface evolution
(接口进化),很久之前一个实现类实现了某个接口,如果在之后某个版本这个接口新添的方法不是默认方法,那么实现类将无法通过编译。添加非默认方法到接口的方式是源码不兼容的。如果不重新编译这个类,而使用包含这个类的老的jar包,那么没有问题(给接口中添加方法是binary compatible
),但是如果调用这个类的实例的新接口方法会报错。默认方法将上面两种情况都解决了,无论是否重新编译那个实现类,使用实现类的实例调用新方法都不会报错
解决默认方法冲突
如果在接口中和父类中有相同签名的方法存在,那么Java中解决冲突的方式是:
- 父类和接口中存在相同签名的方法,子类继承父类并实现接口,则接口中的方法被忽略
- 两个接口中存在相同签名的方法(default),编译器会报错,即子类必须重新实现这个方法
重新实现方法时可以通过InterfaceName.super.methodName()
来调用任意接口的默认实现
接口和回调
回调(callback)是一种常用的编程模式,即设置一个执行器,无论何时一个特殊事件发生这个执行器代码就开始执行
举例:在程序强制退出前,每过1s,控制台就打印一下当前的时间。Java中可以使用javax.swing
包中的Timer
对象来实现,只要给它的构造器中第二个参数传递一个实现了ActionListener
接口的对象即可,代码如下:
Timer timer = new Timer(1000, event -> {
System.out.println("Current time is : " + Instant.ofEpochMilli(event.getWhen()));
Toolkit.getDefaultToolkit().beep();
});
timer.start();
上面代码中的Timer会新开一个线程,所以需要把主线程阻塞这个定时器才会持续运行,否则主线程会很快结束,定时器也不会打印任何信息
Comparator
接口
在Arrays.sort(T[])
方法中,因为String实现了Comparator
接口(按字符字典顺序排序),如果我们希望用别的方式排序,在没办法改动String类的情况下,可以使用Arrays.sort(T[], Comparator)
方法,即自己写一个实现了Comparator
接口的类