本文并非原创,是根据网上一些文字总结而来。
Lambda 表达式
Lambda语法
Lambda表达式在Java中引入了一个新的语法元素和操作符->,它将Lambda分成两个部分:
左侧:指定Lambda表达式需要的所有参数;
右侧:指定了Lambda体,即Lambda表达式要执行的功能。
语法格式一:无参,无返回值,Lambda体只需要一条语句
Runnable runnable = () -> System.out.println(“Hello World”);
runnable.run();
语法格式二:Lambda有一个参数,且无返回值
Consumer com = (x) -> System.out.println(x);
com.accept(“Hello World”);
语法格式三:Lambda有一个参数,参数的小括号可以省略
Consumer com = x -> System.out.println(x);
com.accept(“Hello World”);
语法格式四:Lambda有2个参数,并且有返回值,多条语句时必须加上{}
BinaryOperator bi = (x, y) -> {
System.out.println(“加法”);
return x + y;
};
Integer apply = bi.apply(23, 45);
System.out.println(apply);
语法格式五:当Lambda体只有一条语句时,return与大括号可以省略
语法格式六:Lambda的参数列表的类型可以不写,JVM编辑器可以通过上下文进行”类型推断“
BinaryOperator bi = (x, y) -> x + y;
System.out.println(bi.apply(20, 30));
函数式接口
只包含一个抽象方法的接口,称为函数式接口。
可以在任意函数式接口上使用@FunctionalInterface注解,这样可以检查此接口是否是一个函数式接口,同时javadoc也会包含一条声明,说明这是一个函数式接口。
@FunctionalInterface
public interface ICalculate {
//有且只有一个抽象函数
int calculate(int x, int y);
//可以有默认方法
default void print(int x, int y) {
System.out.println(String.format("[x:%d,y:%d]", x, y));
}
//可以有静态方法
static int addition(int x, int y) {
return x + y;
}
public static void main(String[] args) {
ICalculate c = (x, y) -> 100 * x + 10 * y;
System.out.println(c.calculate(5, 6));
}
}
##使用场所
1、替代匿名内部类
//jdk1.8之前
new Thread(new Runnable() {
@Override
public void run() {
System.out.println("jdk1.8 befare");
}
}).start();
System.out.println("=============================");
//jdk1.8
new Thread(() -> System.out.println("jdk1.8 lambda")).start();
2、对集合进行迭代
List<String> dict = Arrays.asList("java", "C#", "PHP", "C++");
//jdk1.8之前
for (String lang : dict){
System.out.println(lang);
}
System.out.println("=============================");
//jdk1.8
//dict.forEach(lang -> System.out.println(lang));
dict.forEach(System.out::println);
3、对集合进行过滤
jdk1.8之前
//定义过滤接口
public interface FilterProcessor {
boolean process(T t);
}
//实现filter函数
public class Filter {
List filter(Listlist, FilterProcessor fp){
List result = new ArrayList();
for (T t : list){
if (fp.process(t))
result.add(t);
}
return result;
}
}
//使用匿名内部类实现过滤
List cost = Arrays.asList(1, 3, 5, 6, 7, 10, 20, 100);
List<Integer> beferFilterCost = new Filter().filter(cost, new FilterProcessor<Integer>() {
@Override
public boolean process(Integer i) {
if (i > 5)
return true;
return false;
}
});
beferFilterCost.forEach(System.out::println);
jdk1.8
List cost = Arrays.asList(1, 3, 5, 6, 7, 10, 20, 100);
List<Integer> filterCost = cost.stream().filter(x -> x > 5).collect(Collectors.toList());
filterCost.forEach(System.out::println);
4、实现map
map函数可以说是函数式编程里最重要的一个方法了。map的作用是将一个对象变换为另外一个。
//将cost增加了0,05倍的大小然后输出
List cost = Arrays.asList(10.0, 20.0,30.0);
cost.stream().map(x -> x + x*0.05).forEach(x -> System.out.println(x));
5、实现reduce
reduce实现的是将所有值合并为一个
jdk1.8之前
List cost = Arrays.asList(10.0, 20.0,30.0);
double sum = 0;
for(double each:cost) {
each += each * 0.05;
sum += each;
}
System.out.println(sum);
jdk1.8
List cost = Arrays.asList(10.0, 20.0,30.0);
double allCost = cost.stream().map(x -> x+x*0.05).reduce((sum,x) -> sum + x).get();
System.out.println(allCost);
6、与函数式接口Predicate配合
除了在语言层面支持函数式编程风格,Java 8也添加了一个包,叫做java.util.function。它包含了很多类,用来支持Java的函数式编程。其中一个便是Predicate,使用 java.util.function.Predicate 函数式接口以及lambda表达式,可以向API方法添加逻辑,用更少的代码支持更多的动态行为。Predicate接口非常适用于做过滤。
public static void filterTest(List languages, Predicate condition) {
languages.stream().filter(x -> condition.test(x)).forEach(x -> System.out.println(x + " "));
}
public static void main(String[] args) {
List languages = Arrays.asList(“Java”,“Python”,“scala”,“Shell”,“R”);
System.out.println("Language starts with J: “);
filterTest(languages,x -> x.startsWith(“J”));
System.out.println(”\nLanguage ends with a: “);
filterTest(languages,x -> x.endsWith(“a”));
System.out.println(”\nAll languages: “);
filterTest(languages,x -> true);
System.out.println(”\nNo languages: “);
filterTest(languages,x -> false);
System.out.println(”\nLanguage length bigger three: ");
filterTest(languages,x -> x.length() > 4);
}
Stream
Stream定义
Java 8 中的 Stream 是对集合(Collection)对象功能的增强,它专注于对集合对象进行各种非常便利、高效的聚合操作(aggregate operation),或者大批量数据操作 (bulk data operation)。
Stream API 借助于同样新出现的 Lambda 表达式,极大的提高编程效率和程序可读性。
同时它提供串行和并行两种模式进行汇聚操作,并发模式能够充分利用多核处理器的优势,使用 fork/join 并行方式来拆分任务和加速处理过程。
stream总览
Stream 不是集合元素,它不是数据结构并不保存数据,它是有关算法和计算的,它更像一个高级版本的 Iterator。
Stream 就如同一个迭代器(Iterator),单向,不可往复,数据只能遍历一次,遍历过一次后即用尽了,就好比流水从面前流过,一去不复返。
而和迭代器又不同的是,Stream 可以并行化操作,迭代器只能命令式地、串行化操作。
Stream 的另外一大特点是,数据源本身可以是无限的。
stream的构成
当我们使用一个流的时候,通常包括三个基本步骤:
获取一个数据源(source)→ 数据转换→执行操作获取想要的结果,每次转换原有 Stream 对象不改变,返回一个新的 Stream 对象(可以有多次转换),这就允许对其操作可以像链条一样排列,变成一个管道。
image.png[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Rg8q6eR2-1586480815002)(stream Pipeline.png)]
有多种方式生成 Stream Source:
从Collection和数组
COllection.steam()
Collection.parallelStream()
Arrays.stream(T arrary) or Stream.of()
从BufferedReader
java.io.BuffereReader.lines()
静态工厂
java.util.stream.IntStream.range()
java.nio.file.Files.walk()
自己构建
java.util.Spliterator
其他
Random.ints()
BitSet.stream()
Pattern.splitAsStream(java.lang.CharSequence)
JarFile.stream()
stream操作类型分为两种:
Intermediate : 一个流可以后面跟随零个或多个intermediate 操作。其目的主要是打开流,做出某种程度的数据映射/过滤,然后返回一个新的流,交给下一个操作使用。这类操作都是惰性化的(lazy),就是说,仅仅调用到这类方法,并没有真正开始流的遍历。
Terminal : 一个流只能有一个 terminal 操作,当这个操作执行后,流就被使用“光”了,无法再被操作。所以这必定是流的最后一个操作。Terminal 操作的执行,才会真正开始流的遍历,并且会生成一个结果,或者一个 side effect。
还有一种操作被称为 short-circuiting。用以指:
对于一个 intermediate 操作,如果它接受的是一个无限大(infinite/unbounded)的 Stream,但返回一个有限的新 Stream。
对于一个terminal 操作,如果它接受的是一个无限大的 Stream,但能在有限的时间计算出结果。
stream操作示例
int sum = widgets.stream()
.filter(w -> w.getColor() == RED)
.mapToInt(w -> w.getWeight())
.sum();
stream() 获取当前小物件的 source,filter 和 mapToInt 为 intermediate 操作,进行数据筛选和转换,最后一个 sum() 为 terminal 操作,对符合条件的全部小物件作重量求和。
stream使用详解
简单说,对 Stream 的使用就是实现一个 filter-map-reduce 过程,产生一个最终结果,或者导致一个副作用(side effect)。
stream的构造与转换
构造stream的几种常见方法
// 1. Individual values
Stream stream = Stream.of(“a”, “b”, “c”);
// 2. Arrays
String [] strArray = new String[] {“a”, “b”, “c”};
stream = Stream.of(strArray);
stream = Arrays.stream(strArray);
// 3. Collections
List list = Arrays.asList(strArray);
stream = list.stream();
需要注意的是,对于基本数值型,目前有三种对应的包装类型 Stream:IntStream、LongStream、DoubleStream。
当然我们也可以用 Stream、Stream >、Stream,但是 boxing 和 unboxing 会很耗时,所以特别为这三种基本数值型提供了对应的 Stream。
数据流的构造
IntStream.of(new int[]{1, 2, 3}).forEach(System.out::println);
IntStream.range(1, 3).forEach(System.out::println);
IntStream.rangeClosed(1, 3).forEach(System.out::println);
stream转换为其它数据结构
// 1. Array
String[] strArray1 = stream.toArray(String[]::new);
// 2. Collection
List list1 = stream.collect(Collectors.toList());
List list2 = stream.collect(Collectors.toCollection(ArrayList::new));
Set set1 = stream.collect(Collectors.toSet());
Stack stack1 = stream.collect(Collectors.toCollection(Stack::new));
// 3. String
String str = stream.collect(Collectors.joining()).toString();
一个 Stream 只可以使用一次,上面的代码为了简洁而重复使用了数次。
stream操作
当把一个数据结构包装成 Stream 后,就要开始对里面的元素进行各类操作了。常见的操作可以归类如下。
Intermediate
map(mapToInt, flatMap 等)
filter
distinct
sorted
peek
limit
skip
parallel
sequential
unordered
Terminal
forEach
forEachOrdered
toArray
reduce
collect
min
max
count
anyMatch
allMatch
noneMatch
findFirst
findAny
iterator
Short-circuiting
anyMatch
allMatch
noneMtch
findFirst
findAny
limit
典型用法
转换大写
List output = wordList.stream().
map(String::toUpperCase).
collect(Collectors.toList());
平方数
List nums = Arrays.asList(1, 2, 3, 4);
List squareNums = nums.stream().
map(n -> n * n).
collect(Collectors.toList());
map生成的是个 1:1 映射,每个输入元素,都按照规则转换成为另外一个元素。
一对多
Stream<List> inputStream = Stream.of(
Arrays.asList(1),
Arrays.asList(2, 3),
Arrays.asList(4, 5, 6)
);
Stream outputStream = inputStream.flatMap((childList) -> childList.stream());
flatMap把 input Stream 中的层级结构扁平化,就是将最底层元素抽出来放到一起,最终 output 的新 Stream 里面已经没有 List 了,都是直接的数字。
filter
//取偶数
Integer[] sixNums = {1, 2, 3, 4, 5, 6};
Integer[] evens =
Stream.of(sixNums).filter(n -> n%2 == 0).toArray(Integer[]::new);
/**
- 挑单词
- 把每行的单词用 flatMap 整理到新的 Stream,然后保留长度不为 0 的,就是整篇文章中的全部单词了
*/
List output = reader.lines().
flatMap(line -> Stream.of(line.split(REGEXP))).
filter(word -> word.length() > 0).
collect(Collectors.toList());
forEach
// Java 8
roster.stream()
.filter(p -> p.getGender() == Person.Sex.MALE)
.forEach(p -> System.out.println(p.getName()));
// Pre-Java 8
for (Person p : roster) {
if (p.getGender() == Person.Sex.MALE) {
System.out.println(p.getName());
}
}
forEach是为 Lambda而设计的,保持了最紧凑的风格。
当需要为多核系统优化时,可以parallelStream().forEach(),只是此时原有元素的次序没法保证,并行的情况下将改变串行时操作的行为,此时 forEach本身的实现不需要调整,而 Java8 以前的 for 循环 code 可能需要加入额外的多线程逻辑。
另外一点需要注意,forEach是 terminal操作,因此它执行后,Stream 的元素就被“消费”掉了,你无法对一个 Stream 进行两次 terminal运算。下面的代码是错误的:
stream.forEach(element -> doOneThing(element));
stream.forEach(element -> doAnotherThing(element));
相反,具有相似功能的 intermediate 操作 peek 可以达到上述目的 。
//peek 对每个元素执行操作并返回一个新的 Stream
Stream.of(“one”, “two”, “three”, “four”)
.filter(e -> e.length() > 3)
.peek(e -> System.out.println("Filtered value: " + e))
.map(String::toUpperCase)
.peek(e -> System.out.println("Mapped value: " + e))
.collect(Collectors.toList());
forEach不能修改自己包含的本地变量值,也不能用 break/return 之类的关键字提前结束循环。
findFirst
这是一个 termimal兼 short-circuiting 操作,它总是返回 Stream 的第一个元素,或者空。
比较重点的是它的返回值类型:Optional。这也是一个模仿 Scala 语言中的概念,作为一个容器,它可能含有某值,或者不包含。使用它的目的是尽可能避免 NullPointerException。
Optional 的两个用例
String strA = " abcd “, strB = null;
print(strA);
print(”");
print(strB);
getLength(strA);
getLength("");
getLength(strB);
public static void print(String text) {
// Java 8
Optional.ofNullable(text).ifPresent(System.out::println);
// Pre-Java 8
if (text != null) {
System.out.println(text);
}
}
public static int getLength(String text) {
// Java 8
return Optional.ofNullable(text).map(String::length).orElse(-1);
// Pre-Java 8
// return if (text != null) ? text.length() : -1;
};
Stream 中的 findAny、max/min、reduce等方法等返回 Optional值。还有例如 IntStream.average() 返回 OptionalDouble等等。
reduce
这个方法的主要作用是把 Stream 元素组合起来。它提供一个起始值(种子),然后依照运算规则(BinaryOperator),和前面 Stream 的第一个、第二个、第 n 个元素组合。从这个意义上说,字符串拼接、数值的 sum、min、max、average都是特殊的 reduce。
例如 Stream 的 sum 就相当于
Integer sum = integers.reduce(0, (a, b) -> a+b);
//或
Integer sum = integers.reduce(0, Integer::sum);
也有没有起始值的情况,这时会把 Stream 的前面两个元素组合起来,返回的是 Optional。
reduce示例
// 字符串连接,concat = “ABCD”
String concat = Stream.of(“A”, “B”, “C”, “D”).reduce("", String::concat);
// 求最小值,minValue = -3.0
double minValue = Stream.of(-1.5, 1.0, -3.0, -2.0).reduce(Double.MAX_VALUE, Double::min);
// 求和,sumValue = 10, 有起始值
int sumValue = Stream.of(1, 2, 3, 4).reduce(0, Integer::sum);
// 求和,sumValue = 10, 无起始值
sumValue = Stream.of(1, 2, 3, 4).reduce(Integer::sum).get();
// 过滤,字符串连接,concat = “ace”
concat = Stream.of(“a”, “B”, “c”, “D”, “e”, “F”).
filter(x -> x.compareTo(“Z”) > 0).
reduce("", String::concat);
limit/skip
limit返回 Stream 的前面 n 个元素;skip则是扔掉前 n 个元素(它是由一个叫 subStream的方法改名而来)。
public void testLimitAndSkip() {
List persons = new ArrayList();
for (int i = 1; i <= 10000; i++) {
Person person = new Person(i, “name” + i);
persons.add(person);
}
List personList2 = persons.stream().
map(Person::getName).limit(10).skip(3).collect(Collectors.toList());
System.out.println(personList2);
}
private class Person {
public int no;
private String name;
public Person (int no, String name) {
this.no = no;
this.name = name;
}
public String getName() {
System.out.println(name);
return name;
}
}
有一种情况是 limit/skip 无法达到 short-circuiting目的的,就是把它们放在 Stream 的排序操作后,原因跟 sorted 这个 intermediate操作有关:此时系统并不知道 Stream 排序后的次序如何,所以 sorted中的操作看上去就像完全没有被 limit或者 skip一样。
List persons = new ArrayList();
for (int i = 1; i <= 5; i++) {
Person person = new Person(i, “name” + i);
persons.add(person);
}
List personList2 = persons.stream().sorted((p1, p2) ->
p1.getName().compareTo(p2.getName())).limit(2).collect(Collectors.toList());
System.out.println(personList2);
输出结果
name2
name1
name3
name2
name4
name3
name5
name4
[stream.StreamDWP e r s o n @ 816 f 27 d , s t r e a m . S t r e a m D W Person@816f27d, stream.StreamDWPerson@816f27d,stream.StreamDWPerson@87aac27]
有一点需要注意的是,对一个 parallel的 Steam 管道来说,如果其元素是有序的,那么 limit操作的成本会比较大,因为它的返回对象必须是前 n 个也有一样次序的元素。取而代之的策略是取消元素间的次序,或者不要用 parallel Stream。
sorted
对 Stream 的排序通过 sorted 进行,它比数组的排序更强之处在于你可以首先对 Stream 进行各类 map、filter、limit、skip 甚至 distinct 来减少元素数量后,再排序,这能帮助程序明显缩短执行时间。
//优化:排序前进行 limit 和 skip
List persons = new ArrayList();
for (int i = 1; i <= 5; i++) {
Person person = new Person(i, “name” + i);
persons.add(person);
}
List personList2 = persons.stream().limit(2).sorted((p1, p2) -> p1.getName().compareTo(p2.getName())).collect(Collectors.toList());
System.out.println(personList2);
这种优化是有 business logic 上的局限性的:即不要求排序后再取值。
min/max/distinct
min和 max的功能也可以通过对 Stream 元素先排序,再 findFirst来实现,但前者的性能会更好,为 O(n),而 sorted的成本是 O(n log n)。同时它们作为特殊的 reduce方法被独立出来也是因为求最大最小值是很常见的操作。
//找出最长一行的长度
BufferedReader br = new BufferedReader(new FileReader(“c:\SUService.log”));
int longest = br.lines().
mapToInt(String::length).
max().
getAsInt();
br.close();
System.out.println(longest);
//找出全文的单词,转小写,并排序
List words = br.lines().
flatMap(line -> Stream.of(line.split(" "))).
filter(word -> word.length() > 0).
map(String::toLowerCase).
distinct().
sorted().
collect(Collectors.toList());
br.close();
System.out.println(words);
Match
Stream 有三个 match 方法,从语义上说:
allMatch: Stream 中全部元素符合传入的 predicate,返回 true
anyMatch: Stream 中只要有一个元素符合传入的 predicate,返回 true
noneMatch: Stream 中没有一个元素符合传入的 predicate,返回 true
它们都不是要遍历全部元素才能返回结果。例如 allMatch只要一个元素不满足条件,就 skip剩下的所有元素,返回 false。
List persons = new ArrayList();
persons.add(new Person(1, “name” + 1, 10));
persons.add(new Person(2, “name” + 2, 21));
persons.add(new Person(3, “name” + 3, 34));
persons.add(new Person(4, “name” + 4, 6));
persons.add(new Person(5, “name” + 5, 55));
boolean isAllAdult = persons.stream().
allMatch(p -> p.getAge() > 18);
System.out.println("All are adult? " + isAllAdult);
boolean isThereAnyChild = persons.stream().
anyMatch(p -> p.getAge() < 12);
System.out.println("Any child? " + isThereAnyChild);
总结
Stream特性可归纳为:
不是数据结构
没有内部存储,只是用操作管道从source(数据结构、数组、generator function、IO channe)抓取数据
也绝不修改所封装的底层数据结构的数据。例如Stream的filter操作会产生一个不包含被过滤元素的新Stream,而不是从source删除那些元素
所有Stream的操作必须以lambda表达式为参数
不支持索引访问
可以请求第一个元素,但无法请求第二个、第三个或最后一个
很容易生成数据或List
惰性化
很多Stream操作是向后延迟的,一直到它弄清楚最后需要多少数据才会开始
Intermediate操作永远是惰性化的
并行能力(parallelStream)
当一个Stream是并行的,就不需要再写多线程代码,所有对它的操作会自动并行进行
可以是无限的
集合有固定大小,Stream则不必。limit(n)和findFirst()这类的short-circuiting操作可以对无限的Stream进行运算并很快完成