注:本文是《Effective Java》学习的笔记。
Lambda 和 Stream是 java8加进来的。真的特别好用。写出来的代码很漂亮。还增加了函数式接口和方法引用等。
用comparator排序器来举一个例子。下面是匿名类形式
package zy.service.effective.six;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
/**
* @Author zhangyong
* @Date 2019/7/12 12:32
*/
public class Lambda {
public static void compare(List extends Number> list){
Collections.sort(list, new Comparator() {
@Override
public int compare(Number o1, Number o2) {
if (o1 instanceof Double){
return Double.compare(o1.doubleValue(),o2.doubleValue());
}else if (o1 instanceof Integer){
return Integer.compare(o1.intValue(),o2.intValue());
}
return 0; //省略
}
});
}
public static void main(String[] args) {
List list = new ArrayList<>();
list.add(3.0);
list.add(2.0);
list.add(3.1);
compare(list);
list.forEach(System.out::println); //2.0 3.0 3.1
}
}
要是用lambda可以换成这个
Collections.sort(list, (o1, o2) -> {
if (o1 instanceof Double) {
return Double.compare(o1.doubleValue(), o2.doubleValue());
} else if (o1 instanceof Integer) {
return Integer.compare(o1.intValue(), o2.intValue());
}
return 0;
});
注意lambda的类型,参数类型以及返回值类型都没有出现在代码中,编译器利用一个称作类型推导的过程,进行上下文判断从而推断出这些类型。
建议:删除所有的lambda参数的类型吧,除非它们的存在能够使程序变得更加清晰。 泛型对lambda推导有很大的作用,所以不要写原生类型而不使用泛型。
lambda可以用在枚举中。下面写一个使用函数式声明的枚举demo
import java.util.function.IntBinaryOperator;
public enum Week {
MONDAY(1,(x,y)-> 1),FRIDAY(5, Integer::sum);
private int number;
private IntBinaryOperator op; //函数式声明接口,表示两个入参一个int返回类型。
Week(int number,IntBinaryOperator op){
this.number = number;
this.op = op;
}
public int getNumber(){
return number;
}
public int apply(int left,int right){ //实例调用这个方法也就是调用声明实例时的第二个入参函数。
return op.applyAsInt(left,right);
}
public static void main(String[] args) {
System.out.println(Week.MONDAY.apply(2, 4)); //与下行代码执行结果一致。
System.out.println(Week.MONDAY.op.applyAsInt(2, 4));
System.out.println(Week.FRIDAY.apply(2, 4));
}
}
Lambda没有名称和文档;如果一个计算本身不是自描述的,或者超出了几行,那就不要把它放在一个Lambda中。
Lambda越短越好。且不是完全替代匿名类,只是大部分场景替换匿名类。 Lambda无法获取自身引用,在Lambda中this是指向外围实例,而匿名类中的this指向的是匿名类实例。
尽可能不要序列化一个Lambda或者匿名类。
千万不要给函数对象使用匿名类,除非必须创建非函数接口的类型的实例。
实际上方法引用是Lambda的一个语法糖。 瞅着比lambda更简单了。
方法引用共分为四类:
1.类名::静态方法名
2.对象::实例方法名
3.类名::实例方法名
4.类名::new
下面给举个Demo来演示一下方法声明的使用。
package zy.service.effective.six;
import lombok.*;
import java.util.Arrays;
import java.util.List;
import java.util.function.Supplier;
/**
* @Author zhangyong
* @Date 2019/7/13 11:21
*/
@Getter
@Setter
@AllArgsConstructor
@NoArgsConstructor
@ToString
public class Glasses {
private int price;
/**
* 类名调用的方式。静态的,中规中举,可以写在别的类中。
*/
public static int compare(Glasses glasses1,Glasses glasses2){
return glasses1.getPrice() - glasses2.getPrice();
}
/**
* 由于使用到this作为lambda的调用方,而glasses对象才是比较。
*/
public int compareTwo(Glasses glasses){
return this.getPrice() - glasses.getPrice();
}
static class GlassesCompare{
/**
* 写在那个类中都行的
*/
public int compare(Glasses g1,Glasses g2){
return g1.getPrice() - g2.getPrice();
}
}
public static void main(String[] args) {
final Glasses g1 = new Glasses(100);
final Glasses g2 = new Glasses(200);
final Glasses g3 = new Glasses(50);
List list = Arrays.asList(g1, g2, g3);
list.sort(Glasses::compare); // 类名::静态方法名称
GlassesCompare g = new GlassesCompare();
list.sort(g::compare); // 实例::实例方法名称
list.sort(Glasses::compareTwo); // 类名::实例方法名称
Supplier supplier = Glasses::new; // 类名::new
Glasses glasses = supplier.get();
}
}
关于lambda表达式就是下面这种的。 左边参数 → 返回值
()->returnType
对于简单的表达,lambda比函数接口简单的就用lambda就行了。
函数式接口,有且仅有一个抽象方法,Object超类的public方法除外。并标注@FunctionalInterface 注解
Java.util.function包提供了大量标准的函数接口。只要标准的函数接口能够满足需求,通常应该优先考虑,而不是专门再构建一个新的函数接口。
其中有基础接口比较重要,其余接口可以被推导出来
Function
@FunctionalInterface
public interface Function {
R apply(T t);
Predicate接口代表带有一个参数并返回一个boolean的函数。
@FunctionalInterface
public interface Predicate {
boolean test(T t);
Consumer代表的是带有一个函数但是不返回任何值的函数,相当于消费掉了其参数。
@FunctionalInterface
public interface Consumer {
void accept(T t);
}
Supplier接口代表没有参数且返回一个值的函数。
@FunctionalInterface
public interface Supplier {
T get();
}
然后其他的函数接口以他们为基准,各种变化。前文叙述枚举时提到过 IntBinaryOperator 相应的还有long和double 这种作用于基本类型的函数接口。但是注意目前对于包装类是没有函数接口的。
Java8中增加了StreamAPI,简化了串行或并行的大批量操作。
这个API提供了两个关键抽象:Stream流代表数据元素有限或无限的顺序,Stream pipeline则代表这些元素的一个多级计算。
一个Stream pipeline包括一个源Stream ,接着是0个或者多个中间操作和一个终止操作。 所有的中间操作就是将一个流转换成为另外一个流。
Stream pipeline通常是lazy的,直到调用终止操作才会开始计算,对于完成终止操作不需要的数据元素,将永远都不会被计算。正是这种lazy的方式使得 无线Stream成为可能,因此千万不能忘记终止操作。
默认情况下Stream pipeline是顺序执行的。要使pipeline 并发执行,只需在该pipeline 的任何Stream 上调用parallel 并行流方法就行。但是通常不建议这么这做。
对于Stream 流的具体操作就不在叙述了。就是过滤筛选计算最后在聚集起来的一个过程。
建议:在没有显示类型的情况下,仔细命名Lambda参数,这对于Stream pipeline的可读性至关重要、
Stream 中的数据元素可以是对象引用,或者是基础类型值。 它支持 三种基础数据类型。 int long double 所以想在stream中处理char是不合理的。 下面是char在stream中使用时出现的问题,可以看到都变成了int值。当然强制类型转换可以解决这个错误,但是我们还是不要这么玩。
注:如果实在不确定用Stream还是用迭代比较好,那么就两种都试试,看看哪一种更好用吧。
1.foreach是Stream终止操作中比较无力的一个。 forEach操作应该只用于报告Stream计算的结果,而不是执行计算。
List list = Arrays.asList("c","b","c");
list.stream().sorted().forEach(System.out::println); // b c c
可以看到上面用forEach终止的。我使用Stream一般都是以 collect来收集元素结尾。 内部有几十种方法供你玩。
2.最简单的映射收集器是toMap(keyMapper,valueMapper) 第一个参数是映射成key,第二个参数是映射成value
需要注意的是toMap映射的时候,key有相同的会抛异常。IllegalStateException 重载的就不说了。
List list = Arrays.asList("c","b","e");
Map collect = list.stream().collect(Collectors.toMap(s -> s + "a", s -> s + "z"));
System.out.println(collect);
3.还有我用的比较多的分组 groupingBy
这个是以你的lambda或者啥条件作为分组条件,符合条件的分到一组。最终生成一个Map,key是你分组时返回值的类型,值是分过去的集合。 重载不说了。
List list = Arrays.asList("c","b","r","er","asd","rq");
Map> listMap = list.stream().collect(Collectors.groupingBy(s -> s.length()));
System.out.println(listMap); // {1=[c, b, r], 2=[er, rq], 3=[asd]}
4.压根没有理由使用 collect(Collectors.counting()); 因为 Stream.count();就有一样的功能。
还包括 summing, averaging, summarizing开头。reducing , filtering, mapping , flatMapping 和 collectingAndThen 方法。
上述方法可以直接 Stream. xx()使用,而不用 collect再使用 Collectors收集。
5.joining方法,这个方法是以分隔符连接起来,我们可以用Guava 的 splitter将字符串以一个字符分割。也可以通过joining来将多个字符串聚集起来。joining还有增加前缀和后缀的重载方法。
List list = Arrays.asList("c","b","r","er","asd","rq");
String collect = list.stream().collect(Collectors.joining("--","[","]"));
System.out.println(collect); //[c--b--r--er--asd--rq]
6.常见的聚集起来的方式还有 toList
List list = Arrays.asList("c","b","r","er","asd","rq");
List stringList = list.stream().filter(s -> s.length() > 1).collect(Collectors.toList());
System.out.println(stringList); //[er, asd, rq]
如上最重要的收集工厂是 toList 、toSet、 toMap 、 groupingBy、 joining
对于公共的、返回序列的方法,Collection或者适当的子类型通常是最佳的返回类型。
数组可以通过 Stream.of("a","b").collect(Collectors.toList()); 来变成Stream
一般返回的就是List 或者 Map
如果Stream的源头是来自Stream.iterate,或者使用了中间操作的limit,那么并行pipeline也不可能提升性能。而且会导致错误。
在Stream上通过并行获得的性能,最好是通过ArrayList、HashMap、HashSet、和concurrentHashMap实例,数组,int long等
并行Stream不仅可能降低性能,包括活性失败(CPU使用率不变了一动不动),还可能导致结果出错,以及难以预计的行为,注意并行是没有顺序可言的,所以对于最终结果很容易出现问题。
并行流最佳操作是做减法。 Stream的reduce min max count sum 等等。骤死性操作如 anyMatch, allMatch noneMatch可以用并行流。
在使用并行流时也要测试一下,虽然多线程跑,线程切换也是需要时间的,小心得不偿失。