Java8的Lambda理解
函数接口
为了理解Lambda,首先应该了解一下java8新出的函数接口,因为这是Lambda表达式的类型。它的定义是:一个接口,如果只有一个显式声明的抽象方法,那么它就是一个函数接口。一般用@FunctionalInterface标注出来(也可以不标)。![](https://img.haomeiwen.com/i14249104/b800a473c70bcec7.png)
如上图所示,1.8后,Runnable接口被标注成函数接口
Lambda的理解
什么是Lambda,本质上还是一个匿名函数,
public int add(int x, int y) {
return x + y;
}
(x, y) -> x+y;
(x, y) -> { return x + y; } //显式指明返回值
三者是等价的
便于理解,假如我们有这么一个接口:
public interface StringFunction{
public String apply(String s);
}
还有这么一个函数
public String run(StringFunction f){
return f.apply("Hello world");
}
现在就可以使用Lambda来将匿名函数充当参数了
run(s -> s.toUpperCase());
其实也就是
run(new StringFunction(){
public String apply(String s){
return s.toUpperCase();
}
})
你可以用一个λ表达式为一个函数接口赋值:
Runnable r1 = () -> {System.out.println("Hello Lambda!");};
然后再赋值给一个Object:
Object obj = r1;
但却不能这样干:
Object obj = () -> {System.out.println("Hello Lambda!");}; // ERROR! Object is not a functional interface!
必须显式的转型成一个函数接口才可以:
Object o = (Runnable) () -> { System.out.println("hi"); }; // correct
一个λ表达式只有在转型成一个函数接口后才能被当做Object使用。所以下面这句也不能编译:
System.out.println( () -> {} ); //错误! 目标类型不明
必须先转型:
System.out.println( (Runnable)() -> {} ); // 正确
假设你自己写了一个函数接口,长的跟Runnable一模一样:
@FunctionalInterface
public interface MyRunnable {
public void run();
}
那么
Runnable r1 = () -> {System.out.println("Hello Lambda!");};
MyRunnable2 r2 = () -> {System.out.println("Hello Lambda!");};
都是正确的写法。这说明一个λ表达式可以有多个目标类型(函数接口),只要函数匹配成功即可。
但需注意一个λ表达式必须至少有一个目标类型。
JDK预定义了很多函数接口以避免用户重复定义。
@FunctionalInterface
public interface Function<T, R> {
R apply(T t);
}
@FunctionalInterface
public interface Consumer<T> {
void accept(T t);
}
@FunctionalInterface
public interface Predicate<T> {
boolean test(T t);
}
λ表达式的使用
之前创建一个新的线程:
Thread oldSchool = new Thread(new Runnable() {
@Override
public void run() {
System.out.println("This is from an anonymous class.");
}
});
利用Lambda之后:
Thread oldSchool = new Thread(() -> {
System.out.println("This is from an anonymous class.");
});
除了可以简化代码,Lambda还可以和集合类很好地结合。java8引入了流的概念,可以将集合转换成流,对流的一次操作会返回另一个流,最终调用一个结束方法来终结流,就好像StringBuild中append和toString的概念类似。
int []nums = new int[]{1,2,3,4,5,6};
Arrays.stream(nums)
.boxed()
.filter(item -> item % 2 == 0)
.forEach(item -> System.out.println(item));
// 输出结果2 4 6
filter方法的参数是Predicate类型,forEach方法的参数是Consumer类型,它们都是函数接口,所以可以使用λ表达式。
再看一个方法
public static void test(String... nums){
List<String> l = Arrays.asList(nums);
List<Integer> r = l.stream()
.map(Integer::parseInt)
.distinct()
.collect(Collectors.toList());
System.out.println(r);
}
利用map将元素转为Integer,去重后,利用collect收集
其中有用到map方法,需要和flatMap作对比
/* 输出
[1]
[2, 3]
[4, 5, 6]
*/
Stream.of(Arrays.asList(1),
Arrays.asList(2, 3), Arrays.asList(4, 5, 6))
.map(item -> item)
.forEach(item -> System.out.println(item));
/*输出
1
2
3
4
5
6
*/
Stream.of(Arrays.asList(1),
Arrays.asList(2, 3), Arrays.asList(4, 5, 6))
.flatMap(item -> item.stream())
.forEach(item -> System.out.println(item));
可以看到flatMap将元素打平了
collect也很有必要说一下,这是一个流的终结方法,
<R, A> R collect(Collector<? super T, A, R> collector);
// Collector参数解释
// 创建一个容器
Supplier<A> supplier();
// 将元素添加到容器中
BiConsumer<A, T> accumulator();
// 将两个容器合并为一个结果容器
BinaryOperator<A> combiner();
// 对结果容器作相应的变换
Function<A, R> finisher();
// 对上述过程做优化和控制
Set<Characteristics> characteristics();
可以看到,collect方法要求传入一个Collector接口的实例对象,Collector可以看做是用来处理流的工具,在Collectors里面封装了很多Collector工具。
了解了参数的意义,就可以理解Collectors工具类中的几个常用方法
public static <T>
Collector<T, ?, List<T>> toList() {
return new CollectorImpl<>((Supplier<List<T>>) ArrayList::new, List::add,
(left, right) -> { left.addAll(right); return left; },
CH_ID);
}
![](https://img.haomeiwen.com/i14249104/a1d635bc7c44ab13.png)
上面显示了Collectors.toList()的整个流程:
1.创建一个空器 :ArrayList::new
2.将流中的元素添加到容器中: List::add
3.将两个容器合并:left.addAll(right)
4.将结果较换成想要的格式:此例子中该方法不执行
事实上,对流的操作并没有使元素被循环一次,实际上流操作分为Lazy和eager两种,Lazy没有遇到eager时,是不会执行的,除此之外,每个元素都是顺着流进行执行的,也就是全在一个循环中执行,并不会产生循环上的效率问题
// Map转List
Map<String,String> map = new HashMap<>();
map.put("k1","v1");
System.out.println(map.entrySet().stream().map(item -> item.getValue()).collect(Collectors.toList()));
// List转Map
List<String> list = new ArrayList<>();
Collections.addAll(list,"1","2");
System.out.println(list.stream().map(Integer::parseInt).collect(Collectors.toMap(item -> item + 1, item -> item)));
reduce: // 对所有元素进行
Optional<T> reduce(BinaryOperator<T> accumulator);
T reduce(T identity, BinaryOperator<T> accumulator);
<U> U reduce(U identity,
BiFunction<U, ? super T, U> accumulator,
BinaryOperator<U> combiner);
第一个重载方法
List<Integer> numList = Arrays.asList(1,2,3,4,5);
int result = numList.stream().reduce((a,b) -> a + b ).get();
System.out.println(result);
lambada表达式的a参数是表达式的执行结果的缓存,也就是表达式这一次的执行结果会被作为下一次执行的参数,而第二个参数b则是依次为stream中每个元素。如果表达式是第一次被执行,a则是stream中的第一个元素。
第二个重载方法与第一个相比,仅仅是多个一个初始值
List<Integer> numList = Arrays.asList(1,2,3,4,5);
int result = numList.stream().reduce(0,(a,b) -> a + b );
System.out.println(result);
第三个参数只有并行流中才会执行(关于并行流,可以参考https://www.jianshu.com/p/ac2bcf2f9d48):
private static void testMultiReduce() {
ArrayList<List<String>> strings = new ArrayList<>();
strings.add(Arrays.asList("1", "2", "3", "4"));
strings.add(Arrays.asList("2", "3", "4", "5"));
strings.add(Arrays.asList("3", "4", "5", "6"));
// 非并行流
Integer reduce1 = strings.stream().flatMap(e -> e.stream()).reduce(0,
(acc, e) -> acc + Integer.valueOf(e), (u, t) -> {
// 非并行流,不会执行第三个参数
System.out.println("u----:" + u);
// 这里的返回值并没有影响返回结果
return null;
});
System.out.println("reduce1:" + reduce1);
// 并行流
Integer reduce2 = strings.parallelStream().flatMap(e -> e.stream()).reduce(0,
(acc, e) -> acc + Integer.valueOf(e), (u, t) -> {
// u,t分别为并行流每个子任务的结果
System.out.println("u----:" + u);
System.out.println("t----:" + t);
return u + t;
});
System.out.println("reduce2:" + reduce2);
}
其他概念
外部变量
Lambda在使用外部变量上和内部类相似,只是Lambda如果不使用外部变量,则不含有外部类的this指针,而在java8之前,内部类想要用外部类的变量,必须声明为final,这是为了保证数据的一致性,在java8以后,可以不显示声明final,但是使用上来讲,还是要为final,否则会报错,Lambda也是相同的用法
默认方法
java8除了新增加了Lambda,还增加了接口方法的默认方法,在stream概念出来以后,需要像Collection中添加新的方法,但是事实上不可以更改这个接口,因为有很多人已经实现了这个接口,所以不可以随便更改。于是就提出了默认方法这一概念,是一种折中的办法。