Lambda表达式
定义
可以把Lambda表达式理解为简洁地表示可传递的匿名函数的一种方式:它没有名称,但它有参数列表、函数主体、返回类型,可能还有一个可以抛出的异常列表。
先前:
Comparator<Apple> byWeight = new Comparator<Apple>() {
public int compare(Apple a1, Apple a2){
return a1.getWeight().compareTo(a2.getWeight());
}
};
之后(用了Lambda表达式):
Comparator<Apple> byWeight =
(Apple a1, Apple a2) -> a1.getWeight().compareTo(a2.getWeight());
Lambda表达式由参数、箭头和主体组成
|-箭头-|
(Apple a1, Apple a2) -> a1.getWeight().compareTo(a2.getWeight());
|- Lambda参数 -| |- Lambda主体 -|
示例
Java语言设计者选择这样的语法,是因为C#和Scala等语言中的类似功能广受欢迎。
类型检查
Lambda的类型是从使用Lambda的上下文推断出来的。上下文(比如,接受它传递的方法的参数,或接受它的值的局部变量)中Lambda表达式需要的类型称为目标类型。
为什么下面的代码不能编译呢?
答:
Lambda表达式的上下文是Object(目标类型)。但Object不是一个函数式接口。 为了解决这个问题,你可以把目标类型改成Runnable,它的函数描述符是() -> void:
Runnable r = () -> {System.out.println("Tricky example"); };
类型推断
Java编译器会从上下文(目标类型)推断出用什么函数式接口来配合Lambda表达式,这意味着它也可以推断出适合Lambda的签名,因为函数描述符可以通过目标类型来得到。这样就可以在Lambda语法中省去标注参数类型。
//显示参数类型,没有类型推断
Comparator<Apple> c =
(Apple a1, Apple a2) -> a1.getWeight().compareTo(a2.getWeight());
//省略参数类型,有类型推断
Comparator<Apple> c =
(a1, a2) -> a1.getWeight().compareTo(a2.getWeight());
局部变量
Lambda表达式也允许使用自由变量(不是参数,而是在外层作用域中定义的变量),就像匿名类一样。被称作捕获Lambda。例如,下面的Lambda捕获了portNumber变量:
int portNumber = 1337;
Runnable r = () -> System.out.println(portNumber);
但是Lambda表达式只能捕获指派给它们的局部变量一次。例如,下面的代码无法编译,因为portNumber变量被赋值两次:
为什么局部变量有这些限制
实例变量都存储在堆中,而局部变量则保存在栈上。如果Lambda可以直接访问局部变量,而且Lambda是在一个线程中使用的,则使用Lambda的线程,可能会在分配该变量的线程将这个变量收回之后,去访问该变量。因此,Java在访问自由局部变量时,实际上是在访问它的副本,而不是访问原始变量。
如果局部变量仅仅赋值一次那就没有什么区别了——因此局部变量必须显式声明为final。
方法引用
方法引用让你可以重复使用现有的方法定义,并像Lambda一样传递它们。方法引用可以被看作仅仅调用特定方法的Lambda的一种快捷写法。
例如,Apple::getWeight 就是引用了Apple类中定义的方法 getWeight。这里不需要括号,因为你没有实际调用这个方法。
list.sort((Apple a1, Apple a2)-> a1.getWeight().compareTo(a2.getWeight()));
//方法引用写法
list.sort(comparing(Apple::getWeight));
如果一个Lambda代表的只是“直接调用这个方法”,那最好还是用名称来调用它,而不是去描述如何调用它。事实上,方法引用就是让你根据已有的方法实现来创建Lambda表达式。
当你需要使用方法引用时,目标引用放在分隔符 : : 前,方法的名称放在后面。
方法引用主要有三类。
-
指向静态方法的方法引用(例如
Integer的parseInt方法,写作Integer::parseInt) -
指向任意类型实例方法 的方法引用(例如
String的length方法,写作String::length) -
指向现有对象的实例方法的方法引用(假设你有一个局部变量
expensiveTransaction用于存放Transaction类型的对象,它支持实例方法getValue,写作expensive-Transaction::getValue)
Stream 流
Java 8中的集合支持一个新的stream方法,它会返回一个流(接口定义在java.util.stream.Stream里)。
流是Java API的新成员,它允许你以声明性方式处理数据集合,你可以把它们看成遍历数据集的高级迭代器。
流与集合
集合与流之间的差异
集合是一个内存中的数据结构, 它包含数据结构中目前所有的值。每个元素都是放在内存里的,可以添加与删除元素。
流则是在概念上固定的数据结构(你不能添加或删除元素),其元素则是按需计算的。是一种生产者与消费者的关系。从另一个角度来说,流就像是一个延迟创建的集合:只有在消费者要求的时候才会计算值。
只能遍历一次
和迭代器类似,流只能消费一次。遍历完之后,我们就说这个流已经被消费掉了。
List<String> title = Arrays.asList("Java8", "Stream");
Stream<String> s = title.stream();
s.forEach(System.out::println);
s.forEach(System.out::println);
Exception in thread "main" java.lang.IllegalStateException: stream has already been operated upon or closed
at java.util.stream.AbstractPipeline.sourceStageSpliterator(AbstractPipeline.java:279)
at java.util.stream.ReferencePipeline$Head.forEach(ReferencePipeline.java:580)
外部迭代与内部迭代
用for-each,这称为外部迭代,Streams库使用内部迭代——它帮你把迭代做了。
外部迭代一个集合,显式地取出每个元素再处理
List<String> names = new ArrayList<>();
for(Dish d: menu){
names.add(d.getName());
}
//底层是迭代器做外部迭代
List<String> names = new ArrayList<>();
Iterator<String> iterator = menu.iterator();
while(iterator.hasNext()) {
Dish d = iterator.next();
names.add(d.getName());
}
内部迭代时,可以并行处理,或者用更优化的顺序进行处理
List names = menu.stream() .map(Dish::getName) .collect(toList());
流操作
流的使用一般包括三件事:
- 一个数据(如集合)来执行一个查询;
- 一个中间操作链,形成一条流的流水线;
- 一个终端操作,执行流水线,并能生成结果。
可以连接起来的流操作称为中间操作,关闭流的操作称为终端操作。
List<String> names = menu.stream() //获取流
.filter(d -> d.getCalories() > 300) //中间操作
.map(Dish::getName) //中间操作
.limit(3) //中间操作
.collect(toList()); //终端操作
filter和map等中间操作会返回一个流,并可以链接在一起。可以用它们来设置一条流水线,但并不会生成任何结果forEach和count等终端操作会返回一个非流的值,并处理流水线以返回结果
使用流
筛选
filter()方法
该操作会接受一个返回boolean的函数作为参数,并返回一个包括所有符合条件元素的流。
distinct()方法
返回一个元素各不相同(根据流所生成元素的hashCode和equals方法实现)的流
List<Integer> numbers = Arrays.asList(1, 2, 1, 3, 3, 2, 4);
numbers.stream()
.filter(i -> i % 2 == 0)
.distinct()
.forEach(System.out::println);
limit(n)方法
该方法会返回一个不超过给定长度的流。如果流是有序的,则最多会返回前n个元素
List dishes = menu.stream()
.filter(d -> d.getCalories() > 300)
.limit(3)
.collect(toList());
skip(n)方法
返回一个扔掉了前n个元素的流。如果流中元素不足n个,则返回一个空流。
List<Dish> dishes = menu.stream()
.filter(d -> d.getCalories() > 300)
.skip(2)
.collect(toList());
映射
map()方法
接受一个函数作为参数。这个函数会被应用到每个元素上,并将其映射成一个新的元素。例如,返回列表数字的平方
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);
List<Integer> squares =
numbers.stream()
.map(n -> n * n)
.collect(toList());
flatMap()方法
使用flatMap方法的效果是,各个数组并不是分别映射成一个流,而是映射成流的内容。所有使用map(Arrays::stream) 时生成的单个流都被合并起来,即扁平化ࣰ为一个流。例如:
//给定单词列表 ["Hello","World"],返回列表["H","e","l", "o","W","r","d"]。
List<String> words = Arrays.asList("Hello","World");
List<String> uniqueCharacters =
words.stream()
.map(w -> w.split(""))
.flatMap(Arrays::stream)
.distinct()
.collect(Collectors.toList());
一言以蔽之,
flatmap方法让你把一个流中的每个值都换成另一个流,然后把所有的流连接起来成为一个流
匹配
anyMatch()方法
流中是否有一个元素能匹配,anyMatch方法返回一个boolean,因此是一个终端操作
if(menu.stream().anyMatch(Dish::isVegetarian)){
System.out.println("The menu is (somewhat) vegetarian friendly!!");
}
allMatch()方法
allMatch方法的工作原理和anyMatch类似,但它会看看流中的元素是否都能匹配
boolean isHealthy = menu.stream()
.allMatch(d -> d.getCalories() < 1000);
noneMatch()方法
和allMatch相对的是noneMatch。它可以确保流中没有任何元素与给定的条件匹配
boolean isHealthy = menu.stream()
.noneMatch(d -> d.getCalories() >= 1000);
查找
findAny()方法
返回当前流中的任意元素。它可以与其他流操作结合使用。
Stream.of(10, 20, 30).findAny()
.ifPresent(System.out::println);
findFirst()方法
返回流中第一个元素
Stream.of(10, 20, 30).findFirst()
.ifPresent(System.out::println);
运算
reduce()方法
如何把一个流中的元素组合起来,使用reduce操作来表达更复杂的查询。此类查询需要将流中所有元素反复结合起来,得到一个值。比如求和操作。sum,min,max等底层都是reduce实现的
List<Integer> numbers = Arrays.asList(1,2,3,5,7);
//元素求和
int sum = numbers.stream().reduce(0, (a, b) -> a + b);
//元素求和,方法引用写法
int sum = numbers.stream().reduce(0, Integer::sum);
//元素求和,sum() 底层代码 就是 reduce(0, Integer::sum);
int sum = numbers.stream().mapToInt(n -> n).sum();
调试
peek()方法
你试图对流操作中的流水线进行调试,该从何入手呢?
peek方法主要用于调试,以便在元素流过管道中的某个点时查看它们。返回由该流的元素组成的流。
List<Integer> numbers = Arrays.asList(1,2,4);
List<Integer> result =
numbers.stream()
//输出来自数据源的当前元素值
.peek(x -> System.out.println("from stream: " + x))
.map(x -> x + 17)
//输出map操作的结果
.peek(x -> System.out.println("after map: " + x))
.filter(x -> x % 2 == 0)
//输出filter操作之后,剩下的元素个数
.peek(x -> System.out.println("after filter: " + x))
.limit(3)
//输出limit操作之后,剩下的元素个数
.peek(x -> System.out.println("after limit: " + x))
.collect(Collectors.toList());
小结
流转化
//这段代码的问题是,它有一个暗含的装箱成本。每个Integer都必须拆箱成一个原始类型, 再进行求和
int calories = menu.stream()
.map(Dish::getCalories)
.reduce(0, Integer::sum);
int calories = menu.stream()
.mapToInt(Dish::getCalories) //返回一个 IntStream
.sum();
//将数值装箱成为一个一般流时 boxed()方法
IntStream intStream = menu.stream().mapToInt(Dish::getCalories);
Stream<Integer> stream = intStream.boxed();
IntStream、DoubleStream和 LongStream,分别将流中的元素特化为int、long和double,从而避免了暗含的的装箱成本
创建流
由值创建流
可以使用静态方法Stream.of,通过显式值创建一个流。它可以接受任意数量的参数。比如:创建了一个字符串流。然后,你可以将字符串转换为大写,再 一个个打印出来
Stream<String> stream = Stream.of("Java 8 ", "Lambdas ", "In ", "Action");
stream.map(String::toUpperCase).forEach(System.out::println);
//你可以使用empty得到一个空流,如下所示:
Stream<String> emptyStream = Stream.empty();
由数组创建流
你可以使用静态方法Arrays.stream从数组创建一个流。它接受一个数组作为参数。比如:将一个原始类型int的数组转换成一个IntStream。
int[] numbers = {2, 3, 5, 7, 11, 13};
int sum = Arrays.stream(numbers).sum();
由文件生成流
java.nio.file.Files中的很多静态方法都会返回一个流。比如: Files.lines,它会返回一个由指定文件中的各行构成的字符串流
try(Stream<String> lines =
Files.lines(Paths.get("data.txt"), Charset.defaultCharset())){
}
catch(IOException e){
}
由函数生成流:创建无限流
Stream API提供了两个静态方法来从函数生成流:Stream.iterate和 Stream.generate。 这两个操作可以创建所谓的无限流:不像固定集合创建的流那样有固定大小的流。无限流会用给定的函数按需创建值,无穷无尽地计算下去!一般来说, 应该使用limit(n)来对这种流加以限制。
iterate()方法
接受一个初始值(在这里是0),依次应用在每个产生的新值上的Lambda,比如:我们使用,返回的是前一个元素加上2。流的第一个元素是初始值0。然后加上2来生成新的值2,再加上2来得到新的值4,以此类推
//结果 0,2,4,6
Stream.iterate(0, n -> n + 2)
.limit(10)
.forEach(System.out::println);
generate()方法
与iterate方法类似,generate方法也可让你按需生成一个无限流。但generate不是依次对每个新生成的值应用函数的。它接受一个Supplier类型的Lambda提供新的值
Stream.generate(Math::random)
.limit(5)
.forEach(System.out::println);
收集器collect
collect是一个终端操作,它接受的参数是将流中元素累积到汇总结果的各种方式(称为收集器)
统计
long howManyDishes = list.stream().collect(Collectors.counting());
//这还可以写得更为直接:
long howManyDishes = list.stream().count();
最大值和最小值
Collectors.maxBy和Collectors.minBy,来计算流中的最大或最小值。这两个收集器接收一个Comparator参数来比较流中的元素
List<Integer> list = Arrays.asList(1,2,3,5,78);
list.stream().collect(Collectors.maxBy(Integer::compareTo)) .ifPresent(System.out::println);
//这种写法也一样
list.stream().min(Integer::compareTo).ifPresent(System.out::println);
汇总
Collectors类专门为汇总提供了一个工厂方法:Collectors.summingInt。它可接受一个把对象映射为求和所需int的函数,并返回一个收集器;该收集器在传递给普通的collect方法后即执行我们需要的汇总操作。
还有Collectors.averagingInt,同对应的averagingLong和averagingDouble可以计算数值的ࣰ平均数
List<Integer> list = Arrays.asList(1,2,3,5,78);
Integer collect = list.stream().collect(Collectors.summingInt(i ->i));
连接字符串
joining工厂方法返回的收集器会把对流中每一个对象应用toString方法得到的所有字符连接成一个字符串。joining在内部使用了StringBuilder来把生成的字符串逐个累加起来
List<String> list = Arrays.asList("hello","world");
String collect = list.stream().collect(Collectors.joining());
运算
Collectors.reducing工厂方法,需要三个参数
- 第一个参数是起始值
- 第二个参数使用的函数
- 第三个参数是一个
BinaryOperator,将两个元素累积成一个同类型的值
int totalCalories = menu.stream().collect(reducing(0, //初始值
Dish::getCalories, //转化函数
Integer::sum));
Stream接口的collect和reduce方法有何不同,因为两种方法常会获得相同的结果
一个语义问题和一个实际问题。语义问题在于,reduce方法在把两个值结合成一个新值,它是一个不可变的。collect方法的设计就是要改变容器,从而累积要输出的结果
分组
根据一个或多个属性集合中元素进行分组,用Collectors.groupingBy工厂方法
Map<Dish.Type, List<Dish>> dishesByType =
menu.stream().collect(groupingBy(Dish::getType));
groupingBy方法传递了一个Function(以方法引用的形式),它提取了流中每 一道Dish的Dish.Type。我们把这个Function叫作分类函数,因为它用来把流中的元素分成不同的组
分区
分区是分组的特殊情况:由一个返回一个布尔值的函数作为分类函数,称为分区函数。分区函数返回一个布尔值,这意味着得到的分组Map的键类型是Boolean,于是它最多可以 分为两组——true是一组,false是一组。
Map<Boolean, List<Dish>> partitionedMenu =
menu.stream().collect(Collectors.partitioningBy(Dish::isVegetarian));
//返回结果{false=[pork, beef, chicken, prawns, salmon],
//true=[french fries, rice, season fruit, pizza]}
小结
并行流
Stream接口可以让你非常方便地处理它的元素:可以通过对收集源调parallelStream方法来把集合转换为并行流。
并行流就是一个把内容分成多个数据块,并用不同的线程分别处理每个数据块的流。
Long reduce = Stream.iterate(1L, i -> i + 1)
.limit(10)
.parallel() //将流转化为并行流
.reduce(0L, Long::sum);
Stream在内部分成了几块。因此可以对不同的块独立并行进行归纳操作。 最后,同一个归纳操作会将各个子流的部分归纳结果合并起来,得到整个原始流的归纳结果。
顺序流
流调用parallel方法并不意味着流本身有任何实际的变化。它在内部实际上就是设了一个boolean标志,表示你想让调用parallel之后进行的所有操作都并行执行。对并行流调用sequential方法就可以把它变成顺序流。
配置并行流使用的线程池
并行流内部使用了默认的 ForkJoinPool(Java并发——执行框架与线程池 - 掘金 (juejin.cn)) ,它默认的线程数量就是你的处理器数量。
Runtime.getRuntime().availableProcessors()
可以通过系统设置来改变线程池大小
System.setProperty("java.util.concurrent.ForkJoinPool.common.parallelism","12");
这是一个全局设置,因此它将影响代码中所有的并行流。一般不建议修改它
和Iterator一样,Spliterator也用于遍历集合的元素,但它是为了并行执行而设计的
Spliterator
和Iterator一样,Spliterator也用于遍历集合的元素,但它是为了并行执行而设计的
public interface Spliterator<T> {
boolean tryAdvance(Consumer<? super T> action);
Spliterator<T> trySplit();
long estimateSize();
int characteristics();
}
T是Spliterator遍历元素的类型。tryAdvance方法的行为类似于普通的 Iterator,因为它会按顺序一个一个使用Spliterator中的元素,并且如果还有其他元素要遍历就返回true。 trySplit是专为Spliterator接口设计的,可以把一些元素划出去分给第二个Spliterator(由该方法返回),让它们两个并行处理。
参考
Java 8 in Action