本文内容参考《Java核心技术 卷Ⅰ》

接口

接口的概念

接口不是类,而是对希望符合这个接口的类的一组需求。

接口中所有方法都自动是public方法,并且所有方法都绝对不会实现。接口中不会有实例字段,但可以包含常量,并且自动被设置为public static final

为了让类实现一个接口,需要完成下面两个步骤:

  1. 将类声明为实现给定的接口(使用implements关键字)。
  2. 对接口中的所有方法提供定义。

假设希望使用Arrays类的sort方法对Employee对象数组进行排序,Employee类就必须实现Comparable接口,假设希望根据员工工资进行比较

1
2
3
4
5
6
7
8
class Employee implements Comparable<Employee> {
double salary;

@Override
public int compareTo(Employee other) {
return Double.compare(salary, other.salary);
}
}

接口的属性

不能用new实例化一个接口

1
x = new Comparable(...);  // ERROR

但可以声明接口的变量,且必须引用实现了这个接口的类

1
Comparable x = new Employee();

可以利用instanceof检查一个对象是否实现了某个特定的接口

1
if (anObject instanceof Comparable) ...

默认方法

可以为接口方法提供一个默认实现,这必须使用default修饰符标记,比如Iterator接口中的remove方法

1
2
3
4
public interface Iterator<E> {
default void remove() { throw new UnsupportedOperationException("remove"); }
...
}

默认方法可以调用其他方法,例如Collection接口可以定义一个便利的方法

1
2
3
4
5
public interface Collection {
int size();
default boolean isEmpty() { return size() == 0; }
...
}

默认方法的一个重要用法是“接口演化”。假如一个类A实现了接口B。但是在后来的版本中,接口B添加了新方法,如果新方法不是默认方法,那么A类将不能编译,因为它没实现这个方法。所以,为接口增加一个非默认方法不能保证“源代码兼容”。

解决默认方法冲突

如果先在一个接口中将一个方法定义为默认方法,然后又在超类或另一个接口中定义同样的方法,规则如下

  1. 超类优先。如果超类提供了一个具体方法,同名而且有相同参数类型的默认方法会被忽略。

  2. 接口冲突。如果一个接口提供了一个默认方法,另一个接口提供了一个同名而且参数类型相同的方法,必须使用覆盖这个方法来解决冲突。

    考虑两个包含getName方法的接口

    1
    2
    3
    4
    5
    6
    7
    interface Person {
    default String getName() { return ""; }
    }

    interface Named {
    default String getName() { return getClass().getName() + "_" + hashCode(); }
    }

    如果有一个类同时实现这两个接口,Java编译器会报错。只需要在Student类中提供一个getName方法即可,也可以选择两个冲突方法中的一个

    1
    2
    3
    class Student implments Person, Named {
    public String getName() { return Person.super.getName(); }
    }

lambda表达式

lambda表达式的语法

java中的一种lambda表达形式:参数,箭头(->)以及一个表达式

1
2
3
4
String[] strings = new String[100];
Arrays.sort(strings, (String first, String second) -> {
return Integer.compare(first.length(), second.length());
});

即使lambda表达式没有参数,也要提供一个括号

1
2
3
4
5
() -> {
for (int i = 1; i <= 10; i++) {
System.out.println(i);
}
}

如果方法只有一个参数且这个参数类型能被推导出来,可以省略括号

1
ActionListener listener = event -> System.out.println("xxx");

变量作用域

lambda表达式可以捕获外围作用域中变量的值,但捕获的变量必须是事实最终变量。也就是说这个变量在初始化后就不会再为它赋新的值。

1
2
3
4
5
6
7
8
public static void repeat(String text, int count) {
for (int i = 1; i <= count; i++) {
ActionListener listener = event -> {
System.out.println(i + " " + text);
}
new Timer(1000, listener).start();
}
}

在上面的代码中,i的值是会变的,所以不能被捕获,而text总是指向同一个String对象,所以捕获它是合法的。

lambda表达式中使用this关键字时,是指创建这个lambda表达式的方法的this参数

1
2
3
4
5
6
7
public class Application {
public void init() {
ActionListener listener = event -> {
System.out.println(this.toString());
}
}
}

表达式中的this.toString()会调用Application对象的toString方法,而不是ActionListener实例的方法。

处理lambda表达式

使用lambda表达式的重点是延迟执行。有很多原因,如:

  • 在一个单线程中运行代码。
  • 多次运行代码。
  • 在算法的适当位置运行代码。
  • 发生某种情况时执行代码。
  • 只在必要时才运行代码。

假如想要重复一个动作n次,将这个动作和重复次数传递到另一个repeat方法

1
repeat(10, () -> System.out.println("Hello World!"));

要接受这个lambda表达式,需要选择一个函数式接口,我们在这里可以用Runnable接口

1
2
3
4
5
public static void repeat(int n, Runnable action) {
for (int i = 0; i < n; i++) {
action.run();
}
}

我们希望告诉这个动作出现在哪一次迭代中,我们需要选择一个合适的函数式接口,其中要包含一个方法,这个方法有一个int参数而且返回类型为void

1
2
3
public interface IntConsumer {
void accept(int value);
}

对刚刚的repeat方法改进

1
2
3
4
5
public static void repeat(int n, IntConsumer action) {
for (int i = 0; i < n; i++) {
action.accept(i);
}
}

可以如下调用

1
repeat(10, i -> System.out.println("Countdown: " + (9 - i)));

内部类

内部类是定义在另一个类中的类。使用内部类的主要原因:

  • 内部类可以对同一个包中其他类隐藏。
  • 内部类方法可以访问定义这个类的作用域中的数据,包括原本私有的数据。

局部内部类

可以在一个方法中局部地定义这个类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public void start() {
class TimePrinter implements ActionListener {
@Override
public void actionPerformed(ActionEvent event) {
System.out.println("At the tone, the time is " +
Instant.ofEpochMilli(event.getWhen()));
if (beep) {
Toolkit.getDefaultToolkit().beep();
}
}
}
TimePrinter listener = new TimePrinter();
Timer timer = new Timer(inteval, listener);
timer.start();
}

声明局部类时不能有访问说明符(publicprivate等)。局部类的作用域被限定在声明这个局部类的块中。

匿名内部类

使用局部内部类时,假如只想创建这个类的一个对象,甚至不需要为类指定名字。这样的一个类被称为匿名内部类。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public void start(int interval, boolean beep) {
ActionListener listener = new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
System.out.println("At the tone, the time is " +
Instant.ofEpochMilli(e.getWhen()));
if (beep) {
Toolkit.getDefaultToolkit().beep();
}
}
};
Timer timer = new Timer(interval, listener);
timer.start();
}

它的含义是:创建一个类的新对象,这个类实现了ActionListener接口,需要实现的方法actionPerformed在括号{}内定义。一般语法如下

1
2
3
new SuperType(construction parameters) {
inner class methods and data
}

其中,SuperType可以是接口,也可以是一个类。

由于匿名内部类没有类名,所以它不能有构造器。但匿名内部类可以提供一个对象初始化块

1
2
3
4
5
6
Person count = new Person("Dracula") {
{
initialization
}
...
}

静态内部类

如果使用内部类只是为了把一个类隐藏在另一个类的内部,并不需要内部类有外围类对象的一个引用,可以将内部类声明为static

一个典型的例子,考虑一个任务:计算数组中的最小值和最大值。

1
2
3
4
5
6
7
8
9
10
double min = Double.POSITIVE_INFINITY;
double max = Double.NEGATIVE_INFINITY;
for (double v : values) {
if (v < min) {
min = v;
}
if (v > max) {
max = v;
}
}

然而这个方法必须返回两个数,为此可以定义一个包含两个值的类Pair

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
class ArrayAlg {
public static class Pair {
private double first;
private double second;

public Pair(double f, double s) {
first = f;
second = s;
}

public double getFirst() {
return first;
}

public double getSecond() {
return second;
}
}

public static Pair minmax(double[] values) {
double min = Double.POSITIVE_INFINITY;
double max = Double.NEGATIVE_INFINITY;
for (double v : values) {
if (v < min) {
min = v;
}
if (v > max) {
max = v;
}
}
return new Pair(min, max);
}
}

Pair对象中不需要任何其他对象的引用。

在接口声明中的内部类自动是staticpublic