6 接口、lambda表达式与内部类
接口是用来描述类应该做什么(行为),而不指定应该如何做(具体行为)。
lamda 表达式,是一种很简洁的方法,用来创建可以在将来某个时间点执行的代码块。通过使用它,可以用一种精巧而简洁的方式来表示使用回调或可变行为的代码。
内部类,定义在另外一个类内部,它们的使用方法可以访问包含它们的外部类字段。内部类在设计具有相互协作关系的类集合时很有用。
6.1 接口
特点:
- 接口中所有方法都自动是 public 的。因此,在接口中声明方法时,无需提供关键字 public
- 接口中可以定义常量,接口中的字段总是 public static final。
- 接口中绝没有实例字段,在java8以前,接口中绝不会实现方法
可以将接口看做是没有实例字段的 抽象类。因为实例字段和方法实现的任务应该由实现接口的那个类来完成。
接口的属性
接口不是类,故而
- a 不能使用 new 运算符实例化一个接口
- b 可以声明接口的变量,不过该接口变量必须引用实现了这个接口的类对象
- c 可以扩展接口,如同建立类的继承层次一样。允许有多条接口链,从通用性较高的接口扩展到专用性较高的接口。
b 示例代码
Comparable x;
x = new Employee(...)// Employee类实现了Comparable 接口
c 示例代码(假设有一个叫 Moveable的接口)
public interface Moveable {
void move(double x, double y);
}
//假设一个名叫 Power 的接口扩展了 Moveable 接口
public interface Power extends Moveable {
double milesPerGallon();
}
//虽然接口中不能包含实例字段,但可包含常量
public interface Power extends Moveable {
double milesPerGallon();
double SPEED_LIMIT = 95;// a public static final constant
}
注意:
- 与接口中方法都被自动设置为 public 一样,接口中的字段总是 public static final。
- 可以将接口的方法标记为 public ,将字段标记为 public static final. 但是Java 语言规范建议不要提供多余的关键字。
虽然每个类只能有一个超类,但可以实现多个接口。为定义类的行为提供了极大的灵活性。
例如,java 中有一个非常重要的内置接口,名叫 cloneable.
如果每个类实现了这个 cloneable 接口,Object 类中的 clone 方法就可以创建你的类对象的一个副本。
如果希望自己设计的类具有克隆和比较的能力,只要实现这个接口即可。用逗号将想要实现的接口分隔开。
class Employee implements Cloneable, Comparable
instanceof 作用:检查一个对象是否属于某个特定类,检查一个对象是否实现了某个特定的接口。 示例如下:
if (anObject instanceof Comparable) { ... }
抽象类与接口
Java 为何要引入接口概念,不将 Comparable 直接设计成一个抽象类呢?
- 使用抽象类表示通用属性存在一个严重的问题。每个类只能扩展一个类。
- 有些程序设计语言(尤其 C++)允许一个类有多个超类-------多重继承特性。而 Java 的设计者选择了不支持多重继承,主要原因是多重继承会让语言变得非常复杂,或者效率会降低
- 另外,接口可以提供多重继承的大多数好处,同时还能避免多重继承的复杂性和低效性。
静态和私有方法
Java8 中,允许在接口中增加静态方法。这并非不合法,只是有违将接口作为抽象规范的初衷
通常做法都是将静态方法放在伴随类中。在标准库中,你会看到成对出现的接口和实用工具类,如 Collection/Collections 或 Path/Paths.
可以通过一个 URI 或者字符串序列构成一个文件或者目录的路径,如 Paths.get(“jdk-11”, “conf”, “security”)。在java 11 中,Path 接口提供了等价的方法:
public interface Path {
public static Path of(URI uri) {...}
public static Path of(String first, String... more){...}
...
}
因此,Path 类就不再是必要的了。
类似地,实现你自己的接口时,不需要再为实用工具方法另外提供一个伴随类。
在 Java9 中,接口中的方法可以是 private。private 方法可以是静态方法或实例方法。
由于私有方法只能在接口本身的方法中使用,因此它们的用法很有限,只能作为接口中其他方法的辅助方法。
默认方法
作用总结:
- 限制调用某种方法,若调用某方法即报错。
- 可以调用其他方法
- 接口演化
可以将接口方法提供一个默认实现。使用 default 修饰符标记
public interface Comparable<T> {
default int compareTo(T other) {return 0;}
// by default, all elements are the same
}
这样用处不大,因为 Comparable 的每一个具体实现都会覆盖这个方法。不过有些情况下,默认方法可能很有用。例如:Iterator 接口,用于访问一个数据结构中的元素。这个接口声明了一个 remove 方法,如下:
public interface Iterator<E> {
boolean hasNext();
E next();
default void remove() {
throw new UnsuppportedOperationException("remove");
}
}
如果你要实现一个迭代器,就需要提供 hasNext 和 next 方法。这些方法没有默认实现-----他们依赖于你要遍历访问的数据结构。
不过,如果你的迭代器是只读的,就不用操心实现 remove 方法
默认方法可以调用其他方法。 例如,Collection 接口定义一个便利方法:
public interface Collection {
int size();// an abstract method
default boolean isEmpty() {return size() == 0}
}
解决默认方法冲突
如果先在一个接口中将一个方法定义为默认方法,然后又在一个超类或者另一个接口中定义同样的方法。会引发冲突。
java 解决的规则:
- 超类优先 。如果超类提供了一个具体方法,同名而且有相同参数类型的默认方法会被忽略。
- 接口冲突。如果一个接口提供了一个默认方法,另一个接口提供了一个同名而且参数类型(不论是否默认参数)相同的方法,必须覆盖这个方法来解决冲突。
下面看第二个规则。考虑两个包含 getName 方法的接口:
interface Person {
default String getName() { return "";}
}
interface Named {
default String getName() {
return getClass().getName() + "_" + hasCode();
}
}
此时,如果一个类同时实现了这两个接口会怎么样?
class Student implements Person, Named { … }
类会继承 Person 和 Named 接口提供的两个不一致的 getName 方法。并不是从中选择一个, Java 编译器会报告一个错误,让程序员来解决二义性问题。只需要在 Student 类中提供一个 getName() 方法即可。在这个方法中,可以选择两个冲突方法中的一个,如下:
class Student implements Person, Named {
public String getName() { return Person.super.getName();}
...
}
现在假设 Named 接口没有为 getName 提供默认实现:
interface Named {
String getName();
}
Student 类会从 Person 接口继承默认方法吗?这好像挺合理,不过,Java 设计者更强调一致性。两个接口如何冲突并不重要。如果至少有一个接口提供了一个实现,编译器就会报告错误,程序员就必须解决这个二义性。
注释:
当然,如果两个接口都没有为共享方法提供默认实现,那么就与 Java 8 之前的情况一样,这里不存在冲突。
实现类可以有两个选择:实现这个方法,或者干脆不实现。 如果是后一种情况,这个类本身就是抽象的。
前面讨论了两个接口命名冲突问题,现在来考虑另一种情况:一个类扩展了一个超类,同时实现了一个接口,并从超类和接口继承了相同的方法。例如,假设 Person 是一个类, Student 定义为:
class Student extends Person implements Named {...}
在这种情况下,只会考虑超类方法,接口的所有默认方法都会被忽略。这其实是 “类优先” 规则。
**“类优先” 规则可以确保与 Java 7 的兼容性。**如果为一个接口增加默认方法,这对于有这些默认方法之前能正常工作的代码不会有任何影响。
接口与回调
回调(callback)是一种常见的程序设计模式。在这种模式中,可以指定某个特定事件发生时应该采取的动作。
比如在 java.swing 包中有一个 Timer 类,如果希望经过一定时间间隔就得到通知, Timer 类就很有用。例如,假如程序中有一个时钟,可以请求每秒通知一次,以便更新时钟的表盘。
构造定时器时,需设置一个时间间隔,并告诉定时器经过这个时间间隔需要做什么。
如何告诉定时器做什么呢?在很多程序设计语言中,可以提供一个函数名,定时器要定期地调用这个函数。但是,Java 标准类库中的类采用的是面向对象方法。 先向定时器传入某个类的对象,然后,定时器调用这个对象的方法。由于对象可以携带一些附加的信息,因此传递一个对象比传递一个函数要灵活的多。
当然,定时器需要知道调用哪一个方法,并要求传递的对象所属的类实现了 java.awt.event 包的 ActionListener 接口。如下
public interface ActionListener {
void actionPerformed(ActionEvent event);
}
当到达指定的时间间隔时,定时器就调用 actionPerformed 方法。
假设希望每隔 1 秒钟打印一条消息“At the tone, the time is …”, 然后响一声,那么可以定义一个实现 ActionListener 接口的类,然后将想要执行的语句放在 actionPerformed 方法中。
class Timeer implement ActionListener {
public void actionPerformed(ActionEvent event) {
System.out.println("At the tone, the time is ..."
+ Instant.ofEpochMilli(event.getWhen()));
Toolkit.getDefaultToolkit().beep();
}
}
注意:
actionPerformed 方法的 ActionEvent 参数。这个参数提供了事件的相关信息,例如,发生这个事件的时间。
event.getWhen() 调用会返回这个事件的时间,表示为“纪元”(197.年1月1日)以来的毫秒数。
如果把它传入静态方法 Instant.ofEpochMilli,可以得到一个更可读的描述。
接下来,构造这个类的对象,并将它传递给 Timer 构造器
var listener = new TimePrinter();
Timer t = new Timer(1000, listener);
Timer 构造器的第一个参数是一个时间间隔(单位是毫秒),即经过多长时间通知一次。这里是每隔1秒钟通知一次。第二个参数是监听器对象。
最后启动定时器:
t.start();
每隔一秒就会显示下面的消息,然后响一声铃
At the tone, the time is 2017-12-16T05:01:49.550Z
Comparator 接口
针对 希望按长度递增的顺序对字符串进行排序,而不是按字典顺序进行排序的情况。 可以使用 Arrays.sort 方法的第二个版本,有一个数组和一个比较器(comparator)作为参数,比较器是实现了 Comparator 接口的类的实例。
public interface Comparator<T> {
int compare(T first, T second);
}
要按长度进行比较字符串,可以如下定义一个实现 Comparator 的类:
class LengthComparator implements Comparator<String> {
public int compare(String first, String second) {
return first.length() - second.length();
}
}
具体完成比较时,需要建立一个实例:
var comp = new LengthComparator();
if (comp.compare(words[i] - words[j]) > 0) ...
将这个调用与 words[i].compareTo(words[j]) 进行比较。
- 这个 compare 方法要在比较器对象上调用,而不是字符串本身调用。
要对一个数组排序,需要为 Arrays.sort 方法传入一个 LengthComparator 对象:
String[] friends = {"Peter", "Paul", "Mary"};
Arrays.sort(friends, new LengthComparator());
对象克隆
要了解克隆的具体含义,得先回忆为一个包含对象引用的变量建立副本时会发生什么。
- 原变量和副本都是同一个对象的引用。说明任何一个变量改变都会影响另一个变量。
var original = new Emplyee("John Public", 50000);
Employee copy = original;
copy.raiseSalary(10);// 也会改变 original
如果希望 copy 是一个新对象,它的初始状态与 original 相同,但是之后他们各自会有自己不同的状态,这种情况则要使用 clone 方法。
Employee copy = original.clone();
copy.raiseSalary(10);// 不会改变 original
不过并没那么简单。 clone 方法是 Object 的一个 protected 方法,这说明你的代码不能直接调用这个方法。只有 Employee 类可以克隆 Employ 对象。
这个限制是有原因的:
想想看 Object 类如何实现 clone 。它对这个对象一无所知,所以只能逐个字段地进行拷贝。如果对象中的所有数据字段都是数值或其他基本类型,拷贝这些字段没有问题。但是如果对象包含子对象的引用,拷贝字段就会得到相同子对象的另一个引用,这样一来,原对象和克隆的对象仍然会共享一些信息。
默认的克隆操作是“浅拷贝”,并没有克隆对象中引用的其他对象。