在《 Java8 实战》中第三章主要讲的是Lambda 表达式
,在上一章节的笔记中咱们利用了行为参数化来因对不断变化的需求,最后咱们也使用到了 Lambda,经过表达式为咱们简化了不少代码从而极大地提升了咱们的效率。那咱们就来更深刻的了解一下如何使用 Lambda 表达式,让咱们的代码更加具备简洁性和易读性。java
什么是 Lambda 表达式?简单的来讲,Lambda 表达式是一个匿名函数,Lambda 表达式基于数学中的λ演算得名,直接对应其中的 Lambda 抽象( lambda abstraction ),是一个匿名函数,既没有函数名的函数。Lambda 表达式能够表示闭包(注意和数学传统意义的不一样)。你也能够理解为,简洁的表示可传递的匿名函数的一种方式:它没有名称,但它有参数列表、函数主体、返回类型,可能还有一个能够抛出异常的列表。git
有时候,咱们为了简化代码而去使用匿名类,虽然匿名类能简化一部分代码,可是看起来很啰嗦。为了更好的的提升开发的效率以及代码的简洁性和可读性,Java8 推出了一个核心的新特性之一:Lambda 表达式。程序员
Java8 以前,使用匿名类给苹果排序的代码:github
apples.sort(new Comparator<Apple>() { @Override public int compare(Apple o1, Apple o2) { return o1.getWeight().compareTo(o2.getWeight()); } });
是的,这段代码看上去并非那么的清晰明了,使用 Lambda 表达式改进后:express
Comparator<Apple> byWeight = (Apple a1, Apple a2) -> a1.getWeight().compareTo(a2.getWeight());
或者是:闭包
Comparator<Apple> byWeight = Comparator.comparing(Apple::getWeight);
不得不认可,代码看起来跟清晰了。要是你以为 Lambda 表达式看起来一头雾水的话也不要紧,咱们慢慢的来了解它。app
如今,咱们来看看几个 Java8 中有效的 Lambda 表达式加深对 Lambda 表达式的理解:ide
// 这个表达式具备一个 String 类型的参数并返回一个 int,Lambda 并无 return 语句,由于已经隐含了 return。 (String s) -> s.length() // 这个表达式有一个 Apple 类型的参数并返回一个 boolean (苹果重来是否大于 150 克) (Apple a) -> a.getWeight() > 150 // 这个表达式具备两个 int 类型二的参数而且没有返回值。Lambda 表达式能够包含多行代码,不仅是这两行。 (int x, int y) -> { System.out.println("Result:"); System.out.println(x + y); } // 这个表达式没有参数类型,返回一个 int。 () -> 250 // 显式的指定为 Apple 类型,并对重量进行比较返回 int (Apple a2, Apple a2) -> a1.getWeight.compareTo(a2.getWeight())
Java 语言设计者选选择了这样的语法,是由于 C#和 Scala 等语言中的相似功能广受欢迎。Lambda 的基本语法是:函数
(parameters) -> expression
或者(请注意花括号):性能
(parameters) -> {statements;}
是的,Lambda 表达式的语法看起来就是那么简单。那咱们继续看几个例子,看看如下哪几个是有效的:
( 1 ) () -> {} ( 2 ) () -> "Jack" ( 3 ) () -> {return "Jack"} ( 4 ) (Interge i) -> return "Alan" + i; ( 5 ) (String s) -> {"IronMan";}
正确答案是:(1)、(2)、(3)
缘由:
(1) 是一个无参而且无返回的,相似与 private void run() {}.
(2) 是一个无参而且返回的是一个字符串。
(3) 是一个无参,而且返回的是一个字符串,不过里面还能够继续写一些其余的代码(利用显式返回语句)。
(4) 它没有使用使用显式返回语句,因此它不能算是一个表达式。想要有效就必须加一对花括号, (Interge i) -> {return "Alan" + i}
(5) "IronMan"很显然是一个表达式,不是一个语句,去掉这一对花括号或者使用显式返回语句便可有效。
咱们刚刚已经看了不少关于 Lambda 表达式的语法例子,可能你还不太清楚这个 Lambda 表达式到底如何使用。
还记得在上一章的读书笔记中,实现的 filter 方法中,咱们使用的就是 Lambda:
List<Apple> heavyApples = filter(apples, (Apple apple) -> apple.getWeight() > 150);
咱们能够在函数式接口上使用 Lambda 表达式,函数式接口听起来很抽象,可是不用太担忧接下来就会解释函数式接口是什么。
还记得第二章中的读书笔记,为了参数化 filter 方法的行为使用的 Predicate<t>接口吗?它就是一个函数式接口。什么是函数式接口?一言蔽之,函数式接口就是只定义了一个抽象方法的接口。例如 JavaAPI 中的:Comparator、Runnable、Callable:</t>
public interface Comparable<T> { public int compareTo(T o); } public interface Runnable { public abstract void run(); } public interface Callable<V> { V call() throws Exception; }
固然,不仅是它们,还有不少一些其余的函数式接口。
函数式接口到底能够用来干什么? Lambda 表达式容许你直接之内联的形式为函数式接口的抽象方法提供实例,并把整个表达式做为函数式接口的实例(具体来讲,是函数式接口一个具体实现的实例)。你也可使用匿名类实现,只不过看来并非那么的一目了然。使用匿名类你须要提供一个实例,而后在直接内联将它实例化。
经过下面的代码,你能够来比较一下使用函数式接口和使用匿名类的区别:
// 使用 Lambda 表达式 Runnable r1 = () -> System.out.println("HelloWorld 1"); // 使用匿名类 Runnable r2 = new Runnable() { @Override public void run() { System.out.println("HelloWorld 2"); } }; // 运行结果 System.out.println("Runnable 运行结果:"); // HelloWorld 1 process(r1); // HelloWorld 2 process(r2); // HelloWorld 3 process(() -> System.out.println("HelloWorld 3")); private static void process(Runnable r) { r.run(); }
酷,从上面的代码能够看出使用 Lambda 表达式你能够减小不少代码同时也提升了代码的可读性而使用匿名类却要四五行左右的代码。
函数接口的抽象方法的前面基本上就是 Lambda 表达式的签名。咱们将这种抽象方法叫作函数描述符。例如,Runnable 接口能够看做一个什么也不接受什么也不返回的函数签名,由于它只有一个叫作 run 的抽象方法,这个方法没有参数而且是无返回的。
函数式接口颇有用,由于抽象方法的签名能够描述 Lambda 表达式的签名。函数式接口的抽象方法的签名称为函数描述符。
在第一章的读书笔记中,有提到过 Predicate 这个接口,如今咱们来详细的了解一下它。
java.util.function.Predicate<t>接口定义了一个名字叫 test 的抽象方法,它接受泛型 T 对象,并返回一个 boolean 值。以前咱们是建立了一个 Predicate<t>这样的一个接口,如今咱们所说到的这个接口和以前建立的同样,如今咱们不须要再去建立一个这样的接口就直接可使用了。在你须要表示一个涉及类型 T 的布尔表达式时,就可使用这个接口。好比,你能够定义一个接受 String 对象的 Lambda 表达式:</t></t>
@FunctionalInterface public interface Predicate<T> { boolean test(T t); } private static <T> List<T> filter(List<T> list, Predicate<T> predicate) { List<T> result = new ArrayList<>(); for (T t : list) { if (predicate.test(t)) { result.add(t); } } return result; } List<String> strings = Arrays.asList("Hello", "", "Java8", "", "In", "Action"); Predicate<String> nonEmptyStringPredicate = (String s) -> !s.isEmpty(); List<String> stringList = filter(strings, nonEmptyStringPredicate); // [Hello, Java8, In, Action] System.out.println(stringList);
若是,你去查看 Predicate 这个接口的源码你会发现有一些 and 或者 or 等等一些其余的方法,而且这个方法还有方法体,不过你目前无需关注这样的方法,之后的文章将会介绍到为何在接口中能定义有方法体的方法。
java.util.function.Consumer<t>定义了一个叫作 accept 的抽象方法,它接受泛型 T 的对象,而且是一个无返回的方法。你若是须要访问类型 T 的对象,并对其执行某些操做,就可使用这个接口。好比,你能够用它来建立一个 foreach 方法,并配合 Lambda 来打印列表中的全部元素.</t>
@FunctionalInterface public interface Consumer<T> { void accept(T t); } private static <T> void forEach(List<T> list, Consumer<T> consumer) { for (T i : list) { consumer.accept(i); } } // 使用 Consumer forEach(Arrays.asList("Object", "Not", "Found"), (String str) -> System.out.println(str)); forEach(Arrays.asList(1, 2, 3, 4, 5, 6), System.out::println);
java.util.function.Function<T, R>接口定义了一个叫作 apply 的方法,它接受一个泛型 T 的对象,并返回一个泛型 R 的对象。若是你须要定义一个 Lambda,将输入对象的信息映射到输出,就可使用这个接口(好比提取苹果的重量,把字符串映射为它的长度)。在下面的代码中,咱们来看看如何利用它来建立一个 map 方法,将以一个 String 列表映射到包含每一个 String 长度的 Integer 列表。
@FunctionalInterface public interface Function<T, R> { R apply(T t); } private static <T, R> List<R> map(List<T> list, Function<T, R> function) { List<R> result = new ArrayList<>(); for (T s : list) { result.add(function.apply(s)); } return result; } List<Integer> map = map(Arrays.asList("lambdas", "in", "action"), (String s) -> s.length()); // [7, 2, 6] System.out.println(map);
原始类型特化
咱们刚刚了解了三个泛型函数式接口:Predicate<t>、Consumer<t>和 Function<T, R>。还有些函数式接口专为某些类而设计。</t></t>
回顾一下:Java 类型要么用引用类型(好比:Byte、Integer、Object、List ),要么是原始类型(好比:int、double、byte、char )。可是泛型(好比 Consumer<t>中的 T )只能绑定到引用类型。这是由泛型接口内部实现方式形成的。所以,在 Java 里面有一个将原始类型转为对应的引用类型的机制。这个机制叫做装箱( boxing )。相反的操做,也就是将引用类型转为对应的原始类型,叫做拆箱( unboxing )。Java 还有一个自动装箱机制来帮助程序员执行这一任务:装箱和拆箱操做都是自动完成的。好比,这就是为何下面的代码是有效的(一个 int 被装箱成为 Integer ):</t>
List<Integer> list = new ArrayList<>; for (int i = 0; i < 100; i++) { list.add(i); }
可是像这种自动装箱和拆箱的操做,性能方面是要付出一些代价的。装箱的本质就是将原来的原始类型包起来,并保存在堆里。所以,装箱后的值须要更多的内存,并须要额外的内存搜索来获取被包裹的原始值。
Java8 为咱们前面所说的函数式接口带来了一个专门的版本,以便在输入和输出都是原始类型时,避免自动装箱的操做。好比,在下面的代码中,使用 IntPredicate 就避免了对值 1000 进行装箱操做,但要使用 Predicate<integer>就会把参数 1000 装箱到一个 Integer 对象中:</integer>
@FunctionalInterface public interface IntPredicate { boolean test(int value); } IntPredicate evenNumbers = (int i) -> i % 2 == 0; // 无装箱 evenNumbers.test(1000); Predicate<Integer> oddNumbers = (Integer i) -> i % 2 == 1; // 装箱 oddNumbers.test(1000);
通常来讲,针对专门的输入参数类型的函数式接口的名称都要加上对应的原始类型前缀,好比 DoublePredicate、IntConsumer、LongBinaryOperator、IntFunction 等。Function 接口还有针对输出参数类型变种:ToIntFunction<t>、IntToDoubleFunction 等。</t>
Java8 中还有不少经常使用的函数式接口,若是你有兴趣能够去查找一些相关的资料,了解了这些经常使用的函数接口以后,会对你之后了解 Stream 的知识有很大的帮助。