Java-3-接口与lambda表达式
接口是面向对象编程的重要概念,通过提供接口,我们可以在不提供具体实现的情况下规范程序的框架。对于只有一个抽象方法的接口,我们称之为 函数接口, 在 java 8 中,针对这一接口类型引入了 lambda 表达式。在 java 中引入了更加抽象的编程思想 - 函数式编程(functional program,比较有名的包括 Lisp语言,R 语言)。以上的内容,都在本节进行介绍。
A. 接口
接口的实质, 是服务提供者 和使用者(class 的对象)之间的一个协议。下面展示两组代码,分别是一个接口与它的实现类;另一个则是
1 | //Demo1: 接口与它的实现类 |
接口定义使用 关键字 interface
,可以使用 extends
关键字继承其它的接口(Demo2)。
在接口中定义的 变量 默认都是 public static final
类型。Note:不推荐在 interface 中添加 instance 的变量,一般 interface 中只规定方法。
所有接口中的 方法 都是默认为 public 的,因此可以不写 public
权限说明。此外,接口中的方法默认为 abstract
方法(也就是没有函数内容的方法),不需要使用 abstract
关键字 进行修饰。
需要注意的是,对于早期的 java 版本,interface 内部只允许有 抽象方法, 但是在新的 java 版本中, 我们允许向 interface
中添加 3 种另外的方法。
a. 接口中的非抽象方法
从 Java 8 开始,我们被允许在 接口中添加3类方法的具体实现:static
,default
and private
方法。
Static 方法:可以配合工厂方法使用。如下.
1 | public interface IntSequence{ |
从而调用者不用关心接口的具体实现类。
Note:过去,静态方法通常放在所谓 companion class中,例如:Collection/Collections, Path/Paths, 但现在没有必要再如此如此处理了。
Default methods:在 接口中,我们可以通过 default
关键字,为接口中方法做一个 default 的实现,这个实现是基础的,在实现该接口的类中可以被 复写(override)。
1 | default void test(int a){ |
这个功能对于软件的兼容性很有意义。例如我们向 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 | public interface Comparable<T>{ |
对于一个特定的实现了 Comparable
接口的类,我们要在其中复写这个 comparaTo()
方法。可见,该方法返回一个整型值,我们可以将两个对象的某个 ID 值相减或者使用 Integer.compare
or Double.compare
方法比较 ID 值,最终返回一个可以反映两个对象大小关系的整数值。
Java 类库中的许多类都实现了 Comparable 接口,例如 String 类。对于实现了该接口的方法,我们可以使用 Arrays.sort()
方法对他们进行排序。
Comparator interface: Comparable
接口是在排列对象所在的类中实现的,例如 String
类 就实现了该接口- 按照子母表顺序进行排列。但是如果我们需要自定义另一种 String
的排序方法,例如按照字符串长度 进行排序,如果仍要使用 Arrays.sort()
方法,就需要通过实现 Comparator
接口来实现。该接口定义如下:
1 | public interface Comparator<T>{ |
实现该接口的字符串比较类如下:
1 | class LengthComparator implements Comparator<String>{ |
最后,我们就可以通过调用 Arrays.sort(字符串数组, new LengthComparator())
来实现字符串按长度比较排序。
Runnable Interface:对于多核系统,我们希望一些任务在一个分割开的线程中运行,这时,就需要用到 Runnable
接口,该接口仅有一个函数run()
。我们使用一个类作为示例:
1 | class HelloTask implements Runnable{ |
如果想在一个新的线程中执行该 task,通过 Runnable 生成一个对象,然后通过线程类启动该对象,如下。
1 | Runnable task = new HelloTask(); |
这样,run
方法可以在一个分离的线程中执行。从而可以实现多线程并行工作。
B. Lambda表达式
lambda表达式 实际上 是函数式编程的一种 表达式。关于 lambda 表达式,更多的可以参考:关于lambda表达式,看这一篇就够了
a. 语法
在前面的例子中,我们实现了 Comparator
接口,复写了其中的 compare()
方法。在这里,我们可以将那一部分以 lambda表达式 的形式展现出来。
1 | (String first, String second) -> first.length() - second.length() |
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 | public class Application(){ |
对于上面代码中, this.toString()
this指代的是 Application
类的实例,而不是隐性构造的 Runnable
的实例。
Lambda表达式中的变量范围是 它所在的 enclosing scope。本质上讲,lambda表达式 等价于生成了一个类的实例,故其中的变量类似于一个类的内部的变量,只是对于 lambda表达式而言,这些变量是从外部获取的。对于这一特殊的机制,我们称为 捕获(capture)。
由于捕获的机制,lambda表达式中所使用的来自外部的变量 必须为 constant(声明为final 或者等效是final的量)。下面看两个例子说明:
1 | /** |
此外,还需要注意的是,在 lambda表达式中,这些被 捕获 下来的变量,不允许在表达式中进行变更,也很好理解,本来就要求这些量为 final(or: effectively final)。
d. 使用Lambda表达式的好处
表达更加简洁
上面的例子中可以发现,使用 lambda表达式 可以省略不写接口名,接口方法名。
延迟执行
使用 Lambda表达式 可以 延迟执行 (deferred exxcution) 部分不必要立即执行的代码,这样的延迟执行通常发生在下面的情况中:
- 在分割的线程中执行程序;
- 多次运行代码;
- 在算法中特定的位置运行代码;
- 某一动作发生后执行代码 - 例如 android 的 listener。
- …
具体可以参考下面这篇博文中的例子。(Ref)
高阶函数
这里的 “高阶函数” 是指 处理函数 或者 返回值为函数 的函数。具体做法是将函数的返回值的类型 定为一个函数接口类型, 然后在 return 部分利用 lambda表达式 实现 返回的接口的抽象方法。参考下面的例子:
1 | //函数1 - 可选择排序-正序or倒序 |
上面的例子的作用是直接将 函数 作为返回值返回给 Comparator<String>
来重写它的 compare
方法。
1 | //函数2- 函数排序反向 |
Comparator 接口的的默认的 reversed 方法就是类似的方法进行的实现。
Comparator 方法 中的 comparing
方法可以将一个不可比较的对象按照规定的函数返回值进行比较 (称为 “key extractor”, 从 object 中 extract 一个 key, 即将 type T 的对象转变为 可比较的 type)。看下面的例子:
1 | //依据人的姓氏进行排序 |
C. 方法引用
(Method References)方法引用 同样也是 Java 8 中引入的新的机制,相比于 lambda表达式,方法引用的语法更加简洁,它进一步省略掉了 函数参数列表的书写,将这一部分的工作交由 java 编译器的推断机制处理。
例子1:一个对字符串进行忽略大小写的排序例子如下。
1 | Arrays.sort(strings, (x,y) -> x.compareToIgnoreCase(y)); |
例子2: Objects
类中的 isNull()
函数,语法为: Objects.isNull(x)
, 返回 x == null
的值。对于移除列表中为 null 的元素的任务,使用函数引用可以表达为:
1 | list.removeIf(Objects::isNull); |
例子3:对于顺序打印列表中元素的任务,使用 lambda表达式 和函数引用 分别实现,如下:
1 | list.forEach(x -> System.out.println(x)); //lambda表达式 |
总结起来,一共有如下三种形式:
1 | Class::instanceMethod //例子1 |
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 | //该方法的作用是生成一个给定上下限值,而长度无限的随机数组。 |
可以看到该方法要求返回一个 IntSequence 类型的对象,而 IntSequence 实际上是一个接口类型。因为对象必须为类的实例,而在这里,我们并不关心类的内容,而只需要该类实现了 IntSequence 接口。
对于这种不关心 具体接口实现类 的的情况,我们可以使用上述的本地类来实现。这样做的好处是,首先我们不再关心 local classes 的类名;其次,local classes 类的作用域为包裹它的类,可以直接使用其中的变量,和 lambda 表达式的捕获机制类似(因此,只能使用外部的 final 类型的变量)。
需要注意的是,本地类(也有叫方法内部类)只能在所在的方法的内部进行实例化。
Anonymous Classes(匿名类) 与 lambda表达式类似,语法为:new Interface() {methods}
. 比 lambda 表达式需要多写一个 接口名。但是他可以用于非函数接口(也就是有多个抽象方法的接口)。