因为第三章的内容比较多,并且为了让你们更好的了解Lambda表达式的使用,也写了一些相关的实例,能够在Github或者码云上拉取读书笔记的代码进行参考。java
当咱们第一次提到Lambda表达式时,说它能够为函数式接口生成一个实例。然而,Lambda表达式自己并不包含它在实现哪一个函数式接口的信息。为了全面了解Lambda表达式,你应该知道Lambda的实际类型是什么。git
Lambda的类型是从使用Lambda上下文推断出来的。上下文(好比,接受它传递的方法的参数,或者接受它的值得局部变量)中Lambda表达式须要类型称为目标类型。github
有了目标类型的概念,同一个Lambda表达式就能够与不一样的函数接口关联起来,只要它们的抽象方法可以兼容。好比,前面提到的Callable,这个接口表明着什么也不接受且返回一个泛型T的函数。bash
同一个Lambda可用于多个不一样的函数式接口:app
Comparator<Apple> c1 = (Apple a1, Apple a2) -> a1.getWeight().compareTo(a2.getWeight());
ToIntBiFunction<Apple, Apple> c2 = (Apple a1, Apple a2) -> a1.getWeight().compareTo(a2.getWeight());
BiFunction<Apple, Apple, Integer> c3 = (Apple a1, Apple a2) -> a1.getWeight().compareTo(a2.getWeight());;
复制代码
是的,ToIntFunction和BiFunction都是属于函数式接口。还有不少相似的函数式接口,有兴趣的能够去看相关的源码。ide
到目前为止,你应该可以很好的理解在何时以及在哪里使用Lambda表达式了。它们能够从赋值的上下文、方法调用(参数和返回值),以及类型转换的上下文中得到目标类型。为了更好的了解Lambda表达的时候方式,咱们来看看下面的例子,为何不能编译:函数
Object o = () -> {System.out.println("Tricky example");};
复制代码
答案:很简单,咱们都知道Object这个类并非一个函数式接口,因此它不支持这样写。为了解决这个问题,咱们能够把Object改成Runnable,Runnable是一个函数式接口,由于它只有一个抽象方法,在上一节的读书笔记中咱们有提到过它。ui
Runnable r = () -> {System.out.println("Tricky example");};
复制代码
你已经见过如何利用目标类型来检查一个Lambda是否能够用于某个特定的上下文。其实,它也能够用来作一些略有不一样的事情:tuiduanLambda参数的类型。this
咱们还能够进一步的简化代码。Java编译器会从上下文(目标类型)推断出用什么函数式接口来匹配Lambda表达式,这意味着它也能够推断出适合Lambda的签名,由于函数描述符能够经过目标类型来获得。这样作的好处在于,编译器能够了解Lambda表达式的参数类型,这样就能够在Lambda与法中省去标注参数类型。换句话说,Java编译器会向下面这样推断Lambda的参数类型:spa
// 参数a没有显示的指定类型
List<Apple> greenApples = filter(apples, a -> "green".equals(a.getColor()));
复制代码
Lambda表达式有多个参数,代码可独行的好处就更为明显。例如,你能够在这用来建立一个Comparator对象:
// 没有类型推断,显示的指定了类型
Comparator<Apple> cApple1 = (Apple a1, Apple a2) -> a1.getWeight().compareTo(a2.getWeight());
// 有类型推断,没有现实的指定类型
Comparator<Apple> cApple2 = (a1, a2) -> a1.getWeight().compareTo(a2.getWeight());
复制代码
有时候,指定类型的状况下代码更易读,有时候去掉它们也更易读。并无说哪一个就必定比哪一个好,须要根据自身状况来选择。
咱们迄今为止所介绍的全部Lambda表达式都只用到了其主体里的参数。但Lambda表达式也容许用外部变量,就像匿名类同样。他们被称做捕获Lambda。例如:下面的Lambda捕获了portNumber变量:
int portNumber = 6666;
Runnable r3 = () -> System.out.println(portNumber);
复制代码
尽管如此,还有一点点小麻烦:关于能对这些变量作什么有一些限制。Lambda能够没有限制地捕获(也就是在主体中引用)实例变量和静态变量。但局部变量必须显示的声明final,或实际上就算final。换句话说,Lambda表达式只能捕获指派给它们的局部变量一次。(注:捕获实例变量能够被看做捕获最终局部变量this)。例如,下面的代码没法编译。
int portNumber = 6666;
Runnable r3 = () -> System.out.println(portNumber);
portNumber = 7777;
复制代码
portNumber是一个final变量,尽管咱们没有显示的去指定它。可是,在代码编译的时候,编译器会自动给这个变量加了一个final,起码我看反编译后的代码是有一个final的。
对于局部变量的限制
你可能会有一个疑问,为何局部变量会有这些限制。第一个,实例变量和局部变量背后的实现有一个关键不一样。实例变量都存储在堆中,而局部变量则保存在栈上。若是Lambda能够直接访问局部变量,并且Lambda是在一个线程中使用,则使用Lambda的线程,可能会在分配该变量的线程将这个变量回收以后,去访问该变量。所以,Java在访问自由局部变量是,其实是在访问它的副本,而不是访问原始变量。若是局部变量仅仅复制一次那就没什么区别了,所以就有了这个限制。
如今,咱们来了解你会在Java8代码中看到的另外一个功能:方法引用。能够把它们视为某些Lambda的快捷方式。
方法引用让你能够重复使用现有的方法,并像Lambda同样传递它们。在一些状况下,比起用Lambda表达式还要易读,感受也更天然。下面就是咱们借助Java8 API,用法引用写的一个排序例子:
// 以前
apples.sort((Apple a1, Apple a2) -> a1.getWeight().compareTo(a2.getWeight()));
// 以后,方法引用
apples.sort(Comparator.comparing(Apple::getWeight));
复制代码
酷,使用::的代码看起来更加简洁。在此以前,咱们也有使用到过,它的确看起来很简洁。
方法引用能够被看做仅仅调用特定方法的Lambda的一种快捷写法。它的基本思想是,若是一个Lambda表明的只是:“直接调用这个方法”,那最好仍是用名称来调用它,而不是去描述如何调用它。事实上,方法引用就是让你根据已有的方法实现来建立Lambda表达式。可是,显示地指明方法的名称,你的代码可读性会更好。它是如何工做的?当你须要使用方法引用是,目标引用放在分隔符::前,方法的名称放在后面。 例如,Apple::getWeight就是引用了Apple类中定义的getWeight方法。请记住,不须要括号,由于你没有实际调用这个方法。方法引用就是用Lambda表达式(Apple a) -> a.getWeight()的快捷写法。
咱们接着来看看关于Lambda与方法引用等效的一些例子:
Lambda:(Apple a) -> a.getWeight()
方法引用:Apple::getWeight
Lambda:() -> Thread.currentThread().dumpStack()
方法引用:Thread.currentThread()::dumpStack
Lambda:(str, i) -> str.substring(i)
方法引用:String::substring
Lambda:(String s) -> System.out.println(s)
方法引用:System.out::println
复制代码
你能够把方法引用看做是Java8中个一个语法糖,由于它简化了一部分代码。
对于一个现有的构造函数,你能够利用它的名称和关键字new来建立它的一个引用:ClassName::new。若是,一个构造函数没有参数,那么可使用Supplier来建立一个对象。你能够这样作:
Supplier<Apple> c1 = Apple::new;
Apple apple = c1.get();
复制代码
这样作等价于
Supplier<Apple> c1 = () -> new Apple();
Apple apple = c1.get();
复制代码
若是,你的构造函数的签名是Apple(Integer weight),那么可使用Function接口的签名,能够这样写:
Function<Integer, Apple> c2 = Apple::new;
Apple a2 = c2.apply(120);
复制代码
这样作等价于
Function<Integer, Apple> c2 = (weight) -> new Apple(weight);
Apple a2 = c2.apply(120);
复制代码
若是有两个参数Apple(weight, color),那么咱们可使用BiFunction:
BiFunction<Integer, String, Apple> c3 = Apple::new;
Apple a3 = c3.apply(120, "red");
复制代码
这样作等价于
BiFunction<Integer, String, Apple> c3 =(weight, color) -> new Apple(weight, color);
Apple a3 = c3.apply(120, "red");
复制代码
到目前为止,咱们了解到了不少新内容:Lambda、函数式接口和方法引用,接下来咱们将把这一切付诸实践。
为了更好的熟悉Lambda和方法引用的使用,咱们继续研究开始的那个问题,用不一样的排序策略给一个Apple列表排序,并须要展现如何把一个圆使出报的解决方案变得更为简明。这会用到咱们目前了解到的全部概念和功能:行为参数化、匿名类、Lambda表达式和方法引用。咱们想要实现的最终解决方案是这样的:
apples.sort(comparing(Apple::getWeight));
复制代码
很幸运,Java8的Api已经提供了一个List可用的sort方法,咱们能够不用本身再去实现它。那么最困难部分已经搞定了!可是,若是把排序策略传递给sort方法呢?你看,sort方法签名是这样的:
void sort(Comparator<? super E> c)
复制代码
它须要一个Comparator对象来比较两个Apple!这就是在Java中传递策略的方式:它们必须包裹在一个对象利。咱们说sort的行为被参数化了了:传递给他的排序策略不一样,其行为也会不一样。
可能,你的第一个解决方案是这样的:
public class AppleComparator implements Comparator<Apple> {
@Override
public int compare(Apple o1, Apple o2) {
return o1.getWeight().compareTo(o2.getWeight());
}
}
apples.sort(new AppleComparator());
复制代码
它确实能实现排序,可是还须要去实现一个接口,而且排序的规则也不复杂,或许它还能够简化一下。
或许你已经想到了一个简化代码的办法,就是使用匿名类并且每次使用只须要实例化一次就能够了:
apples.sort(new Comparator<Apple>() {
@Override
public int compare(Apple o1, Apple o2) {
return o1.getWeight().compareTo(o2.getWeight());
}
});
复制代码
看上去确实简化一些,但感受仍是有些啰嗦,咱们接着继续简化:
咱们可使用Lambda表达式来替代匿名类,这样能够提升代码的简洁性和开发效率:
apples.sort((o1, o2) -> o1.getWeight().compareTo(o2.getWeight()));
复制代码
太棒了!这样的代码看起来很简洁,原来四五行的代码只须要一行就能够搞定了!可是,咱们还可使这行代码更加的简洁!
使用Lambda表达式的代码确实简洁了很多,那你还记得咱们前面说的方法引用吗?它是Lambda表达式的一种快捷写法,至关因而一种语法糖,那么咱们来试试糖的滋味如何:
apples.sort(Comparator.comparing(Apple::getWeight));
复制代码
恭喜你,这就是你的最终解决方案!这样的代码比真的很简洁,这比Java8以前的代码好了不少。这样的代码比较简短,它的意思也很明显,而且代码读起来和问题描述的差很少:“对库存进行排序,比较苹果的重量”。
Java8的好几个函数式接口都有为方便而设计的的方法。具体而言,许多函数式接口,好比用于传递Lambda表达式的Comparator、Function和Predicate都提供了容许你进行复合的方法。这是什么意思呢?在实践中,这意味着你能够把多个简单的Lambda复合成复杂的表达式。好比,你可让两个谓词之间作一个or操做,组合成一个更大的谓词。并且,你还可让一个函数的结果成为另外一个函数的输入。你可能会想,函数式接口中怎么可能有更多的方法?(毕竟,这违背了函数式接口的定义,只能有一个抽象方法)还记得咱们上一节笔记中提到默认方法吗?它们不是抽象方法。关于默认方法,咱们之后在进行详细的了解吧。
还记刚刚咱们对苹果的排序吗?它只是一个从小到大的一个排序,如今咱们须要让它进行逆序。看看刚刚方法引用的代码,你会发现它貌似没法进行逆序啊!不过不用担忧,咱们可让它进行逆序,并且很简单。
1.逆序
想要实现逆序其实很简单,须要使用一个reversed()方法就能够完成咱们想要的逆序排序:
apples.sort(Comparator.comparing(Apple::getWeight).reversed());
复制代码
按重量递减排序,就这样完成了。这个方法颇有用,并且用起来很简单。
2.比较器链
上面的代码很简单,可是你仔细想一想,若是存在两个同样重的苹果谁前谁后呢?你可能须要再提供一个Comparator来进一步定义这个比较。好比,再按重量比较了两个苹果以后,你可能还想要按原产国进行排序。thenComparing方法就是作这个用的。它接受一个函数做为参数(就像comparing方法同样),若是两个对象用第一个Comparator比较以后仍是同样,就提供第二个Comparator。咱们又能够优雅的解决这个问题了:
apples.sort(Comparator.comparing(Apple::getWeight).reversed()
.thenComparing(Apple::getCountry));
复制代码
谓词接口包括了三个方法: negate、and和or,让你能够重用已有的Predicate来建立更复杂的谓词。好比,negate方法返回一个Predicate的非,好比苹果不是红的:
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<Apple> apples = Arrays.asList(new Apple(150, "red"), new Apple(110, "green"), new Apple(100, "green"));
// 只要红苹果
Predicate<Apple> apple = a -> "red".equals(a.getColor());
// 只要红苹果的非
Predicate<Apple> notRedApple = apple.negate();
// 筛选
List<Apple> appleList = filter(apples, notRedApple);
// 遍历打印
appleList.forEach(System.out::println);
复制代码
你可能还想要把Lambda用and方法组合起来,好比一个苹果便是红色的又比较重:
Predicate<Apple> redAndHeavyApple = apple.and(a -> a.getWeight() >= 150);
复制代码
你还能够进一步组合谓词,表达要么是重的红苹果,要么是绿苹果:
Predicate<Apple> redAndHeavyAppleOrGreen =
apple.and(a -> a.getWeight() >= 150)
.or(a -> "green".equals(a.getColor()));
复制代码
这一点为何很好呢?从简单的Lambda表达式出发,你能够构建更复杂的表达式,但读起来仍然和问题陈述的差很少!请注意,and和or方法是按照表达式链中的位置,从左向右肯定优先级的。所以,a.or(b).and(c)能够看做(a || b) && c。
最后,你还能够把Function接口所表明的Lambda表达式复合起来。Function接口为此匹配了andThen和compose两个默认方法,它们都会返回Function的一个实例。
andThen方法会返回一个函数,它先对输入应用一个给定函数,再对输出应用另外一个函数。假设,有一个函数f给数字加1(x -> x + 1),另一个函数g给数字乘2,你能够将它们组合成一个函数h:
Function<Integer, Integer> f = x -> x + 1;
Function<Integer, Integer> g = x -> x * 2;
Function<Integer, Integer> h = f.andThen(g);
// result = 4
int result = h.apply(1);
复制代码
你也能够相似地使用compose方法,先把给定的函数左右compose的参数里面给的那个函数,而后再把函数自己用于结果。好比在上一个例子用compose的化,它将意味着f(g(x)),而andThen则意味着g(f(x)):
Function<Integer, Integer> f1 = x -> x + 1;
Function<Integer, Integer> g1 = x -> x * 2;
Function<Integer, Integer> h1 = f1.compose(g1);
// result1 = 3
int result1 = h1.apply(1);
复制代码
它们的关系以下图所示:
compose和andThen的不一样之处就是函数执行的顺序不一样。compose函数限制先参数,而后执行调用者,而andThen限制先调用者,而后再执行参数。
在《Java8实战》第三章中,咱们了解到了不少概念关键的念。
第三章的内容确实不少,并且这一章的内容也很重要,若是你有兴趣那么请慢慢的看,最好本身能动手写写代码不然过不了多久就会忘记了。
Github: chap3
Gitee: chap3
若是,你对Java8中的新特性很感兴趣,你能够关注个人公众号或者当前的技术社区的帐号,利用空闲的时间看看个人文章,很是感谢!