在正式介绍Lambda表达式之前有一些概念是我们需要了解的。
函数式接口:仅仅声明了一个抽象方法的接口。
说明:函数式接口中除了抽象方法外,还可以有其他方法,但是必须被static或者default中修饰,且要有方法体。
Java API中常见的一些函数式接口
public interface Comparator<T> {
// 抽象方法
int compare(T o1, T o2);
// 除了抽象,默认或者静态方法外,函数式接口中还可以有Object类中public修饰的方法
boolean equals(Object obj);
// 默认方法,含有方法体
default Comparator<T> reversed() {
return Collections.reverseOrder(this);
}
// ...
}
public interface Runnable{
void run();
}
public interface Callable<V>{
V call();
}
Java8自带了一些常用的函数式接口,放在 java.util.function包里。
为了避免装箱操作,装箱拆箱都有一定的性能损耗。对Predicate< T>和Function< T, R>等通用函数式接口的原始类型特化:IntPredicate、IntToLongFunction等。
函数描述符是什么?
函数式接口的抽象方法的签名基本上就是Lambda表达式的签名。我们将这种抽象方法叫作函数描述符。
例如,Runnable接口可以看作一个什么也不接受什么也不返回(void)的函数的签名,因为它只有一个叫作run的抽象方法,这个方法什么也不接受,什么也不返回(void)。
() -> void 代表了参数列表为空,且返回void的函数
(Apple,Apple) -> int 代表接受两个Apple作为参数且返回int的函数。
函数式接口可以干什么?
Lambda表达式允许你直接以内联的形式为函数式接口的抽象方法提供实现,并把整个表达式作为函数式接口的实例,具体来说就是函数式接口的一个具体实现的实例。
当然匿名内部类也可以完成同样的事情,只不过比较笨拙。
举例:
// 1.使用Lambda
Runnable r1 = () -> System.out.println("Hello World 1");
new Thread(r1).start();
// 2.使用匿名内部类
Runnable r2 = new Runnable(){
public void run(){
System.out.println("Hello World 2");
}
};
new Thread(r2).start();
函数式接口和Lamdba表达式的关系?
只有在接受函数式接口的地方才可以使用Lamdba表达式。
Lambda表达式允许你直接内联,为函数式接口的抽象方法提供实现,并且将整个表达式作为函数式接口的一个实例。
使用函数式接口
函数式接口定义且只定义了一个抽象方法。函数式接口很有用,因为抽象方法的签名可以描述Lambda表达式的签名。函数式接口的抽象方法的签名称为函数描述符。所以为了应用不同的Lambda表达式,你需要一套能够描述常见函数描述符的函数式接口。
Java API中已经有了几个函数式接口,比如之前见到的Comparator、Runnable和Callable。
1. Predicate
java.util.function.Predicate< T > 接口定义了一个名叫test的抽象方法,它接受泛型T对象,并返回一个boolean。
函数描述符:(T)-> boolean
。
在你需要表示一个涉及类型T的布尔表达式时,就可以使用这个接口。比如,你可以定义一个接受String对象的Lambda表达式,如下所示。
@FunctionalInterface
public interface Predicate<T>{
boolean test(T t);
}
public static < T > List< T > filter(List< T> list, Predicate< T> p) {
List< T> results = new ArrayList<>();
for(T s: list){
if(p.test(s)){
results.add(s);
}
}
return results;
}
Predicate< String> nonEmptyStringPredicate = (String s) -> !s.isEmpty();
List< String> nonEmpty = filter(listOfStrings, nonEmptyStringPredicate);
2. Predicate
java.util.function.Consumer< T>定义了一个名叫accept的抽象方法,它接受泛型T的对象,没有返回(void)。
函数描述符: (T)-> void
你如果需要访问类型T的对象,并对其执行某些操作,就可以使用这个接口。比如,你可以用它来创建一个forEach方法,接受一个Integers的列表,并对其中每个元素执行操作。在下面的代码中,你就可以使用这个forEach方法,并配合Lambda来打印列表中的所有元素。
@FunctionalInterface
public interface Consumer<T>{
void accept(T t);
}
public static <T> void forEach(List<T> list, Consumer<T> c){
for(T i: list){
c.accept(i);
}
}
forEach(Arrays.asList(1,2,3,4,5),(Integer i) -> System.out.println(i));
3.Function
java.util.function.Function< T, R>接口定义了一个叫作apply的方法,它接受一个泛型T的对象,并返回一个泛型R的对象。
函数描述符:(T) -> R
如果你需要定义一个Lambda,将输入对象的信息映射到输出,就可以使用这个接口(比如提取苹果的重量,或把字符串映射为它的长度)。在下面的代码中,我们向你展示如何利用它来创建一个map方法,以将一个String列表映射到包含每个String长度的Integer列表。
@FunctionalInterface
public interface Function<T, R>{
R apply(T t);
}
public static <T, R> List<R> map(List<T> list,Function<T, R> f) {
List<R> result = new ArrayList<>();
for(T s: list){
result.add(f.apply(s));
}
return result;
}
// [7, 2, 6]
List<Integer> l = map(Arrays.asList("lambdas","in","action"),(String s) -> s.length());
关于接口中default和static方法的一些延伸:
问题:Java8中为什么要加入defalut方法呢?
答案:举例,在Java8之前,List接口中并没有stream()或者 parallelStream()方法,它实现的collection接口也没有,因为当初设计接口的时候还没有想到这些方法,最简单的方案就是Java8的设计者把steam方法加入collection中。可是如果这样做的话对于用户来说就是噩梦了,因为给接口加入一个方法,意味所有实体类都必须为其提供一个实现。
Java8中引入了支持defalut修饰的方法,这样方法就有了默认实现而不用实体类去实现这个方法。