Java-3-接口与lambda表达式

接口是面向对象编程的重要概念,通过提供接口,我们可以在不提供具体实现的情况下规范程序的框架。对于只有一个抽象方法的接口,我们称之为 函数接口, 在 java 8 中,针对这一接口类型引入了 lambda 表达式。在 java 中引入了更加抽象的编程思想 - 函数式编程(functional program,比较有名的包括 Lisp语言,R 语言)。以上的内容,都在本节进行介绍。

A. 接口

接口的实质, 是服务提供者 和使用者(class 的对象)之间的一个协议。下面展示两组代码,分别是一个接口与它的实现类;另一个则是

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
//Demo1: 接口与它的实现类
public interface IntSequence{
boolean hasNext(); //判断下个数是否为整数
int next(); //下个数字的值
int ZERO = 0;
}
//接口的实现类
public class SquareSequence implements IntSequence{
private int i;
public boolean hasNext(){
return true;
}
public int next(){
i++;
return i * i;
}
}
//Demo2: 接口的继承
public interface Closeable{
void close();
}
public interface Channel extends Closeable{
boolean isOpen();
}
//实现多个接口
public class FileSequence implements IntSequence, Closeable{
...
}

接口定义使用 关键字 interface ,可以使用 extends 关键字继承其它的接口(Demo2)。

在接口中定义的 变量 默认都是 public static final 类型。Note:不推荐在 interface 中添加 instance 的变量,一般 interface 中只规定方法。

所有接口中的 方法 都是默认为 public 的,因此可以不写 public 权限说明。此外,接口中的方法默认为 abstract 方法(也就是没有函数内容的方法),不需要使用 abstract 关键字 进行修饰。

需要注意的是,对于早期的 java 版本,interface 内部只允许有 抽象方法, 但是在新的 java 版本中, 我们允许向 interface 中添加 3 种另外的方法。

a. 接口中的非抽象方法

从 Java 8 开始,我们被允许在 接口中添加3类方法的具体实现:staticdefault and private 方法。

Static 方法:可以配合工厂方法使用。如下.

1
2
3
4
5
6
public interface IntSequence{
...
Static IntSequence digitsOf(int n){
return new DigitSequence(n);
}
}

从而调用者不用关心接口的具体实现类。

Note:过去,静态方法通常放在所谓 companion class中,例如:Collection/Collections, Path/Paths, 但现在没有必要再如此如此处理了。

Default methods:在 接口中,我们可以通过 default 关键字,为接口中方法做一个 default 的实现,这个实现是基础的,在实现该接口的类中可以被 复写(override)。

1
2
3
default void test(int a){ 
//... content
}

这个功能对于软件的兼容性很有意义。例如我们向 Java 库的某个接口中添加了一个方法,老的程序编译就会报错。而提供一个 default 的实现,就不会出现前述问题。

Private methods: Java 9 允许 interface 中有 private methods,但同类中的private methods 一样,它只能供接口内部的方法使用。

b. 接口的实现

类可以实现一个接口,通过关键字,implements 实现。当我们编写一个类实现一个接口,则在类中需要重写 接口中所有的抽象方法。而如果实现类方法仅仅实现了接口的部分方法, 这个实现类需要添加修饰词 abstract

需要注意的是,不同于 interface 中 我们默认所有的方法为 public 的 从而不用添加 public 关键字进行说明,接口实现类中,我们必须在方法前面添加上 public 关键字,否则会报错。因为对于 class,默认的权限为 package 级,而接口需要 public 权限。(试想:我们可以将一个接口实现类的实例声明为接口类型,这时候,如果方法的权限不相同,会引发矛盾。

不同于一个类只能有一个超类,一个类可以继承多个接口。

c. 变量声明为接口类型

一个接口的实现类的实例 声明时可以声明类型为该接口,例如上面的 IntSequence squareSeq = new SquareSequence();. 这里 IntSequence 为超类型(superType),SquareSequence 为子类型(subType)。

Cast 用于类型转换:我们可以使用 强制类型转换将一个 超类型的对象 转为 子类型。在这之前,为了转换的安全,我们可以通过 instanceof operator 判断对象是否为所要转换类型的子类型。i.e. object instanceof Type, 该表达式返回 true 如果 后面的 Type 是 object 对应的类的类型或 superType。

d. Java类库中典型接口举例

为了更好的了解 Interface 的用处,在这里举一些常用的 interface 的例子。

Comparable interface:为了使得比较同一个类的任意两个对象成为可能,我们需要规定对象之间的比较方法。因此,Comparable 接口规定了对象之间比较的标准形式。

1
2
3
public interface Comparable<T>{
int comparaTo(T other);
}

对于一个特定的实现了 Comparable 接口的类,我们要在其中复写这个 comparaTo() 方法。可见,该方法返回一个整型值,我们可以将两个对象的某个 ID 值相减或者使用 Integer.compare or Double.compare 方法比较 ID 值,最终返回一个可以反映两个对象大小关系的整数值。

Java 类库中的许多类都实现了 Comparable 接口,例如 String 类。对于实现了该接口的方法,我们可以使用 Arrays.sort() 方法对他们进行排序。

Comparator interface: Comparable 接口是在排列对象所在的类中实现的,例如 String 类 就实现了该接口- 按照子母表顺序进行排列。但是如果我们需要自定义另一种 String 的排序方法,例如按照字符串长度 进行排序,如果仍要使用 Arrays.sort() 方法,就需要通过实现 Comparator 接口来实现。该接口定义如下:

1
2
3
public interface Comparator<T>{
int compare(T first, T second);
}

实现该接口的字符串比较类如下:

1
2
3
4
5
class LengthComparator implements Comparator<String>{
public int compare(String first, String second){
return first.length() - second.length();
}
}

最后,我们就可以通过调用 Arrays.sort(字符串数组, new LengthComparator()) 来实现字符串按长度比较排序。

Runnable Interface:对于多核系统,我们希望一些任务在一个分割开的线程中运行,这时,就需要用到 Runnable 接口,该接口仅有一个函数run()。我们使用一个类作为示例:

1
2
3
4
5
6
7
class HelloTask implements Runnable{
public void run(){
for(int i = 0;i < 1000; i++){
system.out.println("Hello World!");
}
}
}

如果想在一个新的线程中执行该 task,通过 Runnable 生成一个对象,然后通过线程类启动该对象,如下。

1
2
3
Runnable task = new HelloTask();
Thread thread = new Thread(task);
thread.start();

这样,run 方法可以在一个分离的线程中执行。从而可以实现多线程并行工作。

B. Lambda表达式

lambda表达式 实际上 是函数式编程的一种 表达式。关于 lambda 表达式,更多的可以参考:关于lambda表达式,看这一篇就够了

a. 语法

在前面的例子中,我们实现了 Comparator 接口,复写了其中的 compare() 方法。在这里,我们可以将那一部分以 lambda表达式 的形式展现出来。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
(String first, String second) -> first.length() - second.length()

/**
如果参数的类型可以推断出来,可以省略。
如果只有一个参数,并且类型可以推断,括号也可以省略
*/
Comparator<String> comp = (first, second) -> first.length() - second.length();

/**
如果函数功能有多行,我们使用 {} 包裹函数部分,
并使用 return specify 返回值。
*/
(String first, String second) ->{
int difference = first.length() - second.length();
if (difference < 0) return -1;
else if(difference > 0) return 1;
else return 0;
}

/**
如果函数无参数,直接使用 ()
*/
Runnable task = () -> {for (int i = 0; i < 1000; i++) doWork();}

b. 使用条件

我们可以看到,Lambda表达式 实际上是描述了一个函数,-> 前面为函数的 形参, -> 后面则是 函数的内容。 因此,在 Java 中,它可以放置在本应传入一个 functional interface(函数接口,只有一个抽象方法的接口) 对象 的地方。一个例子如下:

1
Arrays.sort(words, (first, second) -> first.length() - second.length());

原先,上述 Arrays.sort() 函数应该接收的参数是一个实现了 Comparator<String> 接口,复写了 compare 方法的的类的实例,使用 lambda表达式 可以省略书写接口名,方法名,生成显性实例的过程,而只要写出传入的参数和 compare 函数的内容。

相对来说,java 中的 lambda表达式的自由度相对较低,它只可以放在本应摆放 functional interface 实例的地方,从而编译器可以通过 lambda表达式 生成主类的一个私有方法,来实现需要传入接口的抽象方法。

Java 标准库中 提供了很多的函数接口,我们在需要使用 函数表达式的时候,优先考虑使用现有接口的形式,来构建我们的 lambda表达式。无法实现时,我们也可以自己写一个函数接口 来协助使用 lambda表达式。

常见的 函数接口包括:Runnable, Supplier<T>, Consumer<T>, Function<T,R>

常见的用于基本数据类型的 函数接口, BooleanSupplier, IntSupplier, IntConsumer, IntPredicate

c. Lambda表达式中的变量的作用范围

Lambda表达式 中的变量 与 嵌套代码块中变量的作用域一致,也就其作用域是在同一个类中。如下例子:

1
2
3
4
5
public class Application(){
public void doWork(){
Runnable runner = () -> {...; System.out.println(this.toString()); ...};
}
}

对于上面代码中, this.toString() this指代的是 Application 类的实例,而不是隐性构造的 Runnable 的实例。

Lambda表达式中的变量范围是 它所在的 enclosing scope。本质上讲,lambda表达式 等价于生成了一个类的实例,故其中的变量类似于一个类的内部的变量,只是对于 lambda表达式而言,这些变量是从外部获取的。对于这一特殊的机制,我们称为 捕获(capture)。

由于捕获的机制,lambda表达式中所使用的来自外部的变量 必须为 constant(声明为final 或者等效是final的量)。下面看两个例子说明:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
/**
会报错。因为 i 的值在不断变化,lambda表达式无法 capture 这个值
*/
for(int i = 0; i< n; i++){
new Thread(()-> System.out.println(i)).start();
// error, 无法捕获 i
}

/**
与上面例子类似,但是由于 升级型 for 语句的特性:
每一个循环都生成一个 变量 arg
不会报错。
*/
for(String arg : args){
new Thread(()-> System.out.println(arg)).start();
}

此外,还需要注意的是,在 lambda表达式中,这些被 捕获 下来的变量,不允许在表达式中进行变更,也很好理解,本来就要求这些量为 final(or: effectively final)。

d. 使用Lambda表达式的好处

表达更加简洁

上面的例子中可以发现,使用 lambda表达式 可以省略不写接口名,接口方法名。

延迟执行

使用 Lambda表达式 可以 延迟执行 (deferred exxcution) 部分不必要立即执行的代码,这样的延迟执行通常发生在下面的情况中:

  • 在分割的线程中执行程序;
  • 多次运行代码;
  • 在算法中特定的位置运行代码;
  • 某一动作发生后执行代码 - 例如 android 的 listener。

具体可以参考下面这篇博文中的例子。(Ref)

高阶函数

这里的 “高阶函数” 是指 处理函数 或者 返回值为函数 的函数。具体做法是将函数的返回值的类型 定为一个函数接口类型, 然后在 return 部分利用 lambda表达式 实现 返回的接口的抽象方法。参考下面的例子:

1
2
3
4
5
6
//函数1 - 可选择排序-正序or倒序
public static Comparator<String> compareInDirection(int Direction){
return (x,y) -> direction * x.compareTo(y);
}
//调用, 进行 倒序 排列
Arrays.sort(friends, compareInDirection(-1));

上面的例子的作用是直接将 函数 作为返回值返回给 Comparator<String> 来重写它的 compare 方法。

1
2
3
4
5
6
//函数2- 函数排序反向
public static Comparator<String> reverse(Comparator<String> comp){
return (x,y) -> comp.compare(y,x);
}
//调用
reverse(String::compareToIgnoreCase)

Comparator 接口的的默认的 reversed 方法就是类似的方法进行的实现。

Comparator 方法 中的 comparing 方法可以将一个不可比较的对象按照规定的函数返回值进行比较 (称为 “key extractor”, 从 object 中 extract 一个 key, 即将 type T 的对象转变为 可比较的 type)。看下面的例子:

1
2
3
4
//依据人的姓氏进行排序
Arrays.sort(people, Comparator.comparing(Person::getLastName));
//如果姓氏相同,依据名字排列
Arrays.sort(people, Comparator.comparing(Person::getLastName).thenComparing(Person::getFirstName));

C. 方法引用

(Method References)方法引用 同样也是 Java 8 中引入的新的机制,相比于 lambda表达式,方法引用的语法更加简洁,它进一步省略掉了 函数参数列表的书写,将这一部分的工作交由 java 编译器的推断机制处理。

例子1:一个对字符串进行忽略大小写的排序例子如下。

1
2
3
Arrays.sort(strings, (x,y) -> x.compareToIgnoreCase(y));
//下述方法等价于上面的 lambda表达式。下面的方法称为 “函数引用” 方法。
Arrays.sort(strings, String::compareToIngoreCase);

例子2: Objects 类中的 isNull() 函数,语法为: Objects.isNull(x), 返回 x == null
的值。对于移除列表中为 null 的元素的任务,使用函数引用可以表达为:

1
list.removeIf(Objects::isNull);

例子3:对于顺序打印列表中元素的任务,使用 lambda表达式 和函数引用 分别实现,如下:

1
2
3
list.forEach(x -> System.out.println(x));   //lambda表达式
list.forEach(System.out::println); //函数引用

总结起来,一共有如下三种形式:

1
2
3
Class::instanceMethod   //例子1
Class::staticMethod //例子2
object::instanceMethod //e.g. `this::equals`

Constructor References

Constructor reference 与method reference 非常类似,只是这里的函数式构造函数,从而会有一些需要特别注意的地方。构造函数引用格式为 Employee::new.

参看例子: Stream<Employee> stream = names.stream().map(Employee::new);.

上述语句构造了一个 职员列表,每个职员的名字新建一个对象。(stream 的内容后续介绍)

此外,还可以用 构造函数引用 新建数组,int[]::new,等价于 lambda 表达式 n -> new int[n]

D. 本地类与匿名内部类

在 Java 引入 lambda 表达式之前,Java 中也有类似的隐式的显示接口的方式,不同于 lambda 表达式,这些内容同样适用于非 functional 的interface。

Local Classes(本地类)在一个 方法中 定义一个类。使用的场景可以参考下面这个例子:

1
2
3
4
5
6
7
8
//该方法的作用是生成一个给定上下限值,而长度无限的随机数组。
public static IntSequence randomInts(int low, int high){
class RandomSequence implements Intsequence{
public int next(){return low + generator.nextInt(high - low + 1);}
public boolean hasNext(){return true;}
}
return new RandomSequence();
}

可以看到该方法要求返回一个 IntSequence 类型的对象,而 IntSequence 实际上是一个接口类型。因为对象必须为类的实例,而在这里,我们并不关心类的内容,而只需要该类实现了 IntSequence 接口。

对于这种不关心 具体接口实现类 的的情况,我们可以使用上述的本地类来实现。这样做的好处是,首先我们不再关心 local classes 的类名;其次,local classes 类的作用域为包裹它的类,可以直接使用其中的变量,和 lambda 表达式的捕获机制类似(因此,只能使用外部的 final 类型的变量)。

需要注意的是,本地类(也有叫方法内部类)只能在所在的方法的内部进行实例化。

Anonymous Classes(匿名类) 与 lambda表达式类似,语法为:new Interface() {methods}. 比 lambda 表达式需要多写一个 接口名。但是他可以用于非函数接口(也就是有多个抽象方法的接口)。