java8的Stream特性

java8

Java8

1. 引言

写这个文档的主要原因是自己使用java8时间并不长,对java的认知还停留在java6的基础上,很多新功能是:然也,知之不详;所以然也,不知。 所有平时就抽一些时间出来,做些记录和总结,就形成了这个文档,权当学习笔记之用。

2. Lambda

(1)什么是Lambda?

1
Lambda是从数学中的lambda运算引申出来的,在计算机中主要指匿名函数,也是函数式编程的核心。你可能会问什么是函数式编程,它与命令式编程不同在哪里? 笼统的来说,函数式编程关心的数据映射的问题,命令式编程更关心的是解决问题的步骤。

(2)Lambda基本语法

lambda表达式主要包括三部分:Argument List, Arrow, Body

对应的比如:

1
2
3
(int x, int y) -> x+y
() -> 42
(String s) -> {System.out.println(s);}

(a) 简单示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class RunnableTest {

public static void main (String[] args) {
Runnable r1 = new Runnable() {
@Override
public void run ( ) {
System.out.println("hello runnable");
}
};

Runnable r2 = ()->System.out.println("hello lambda runner");
r1.run();
r2.run();
}
}

上面代码反应了两个情况,一是lambda确实简洁,之前要6行代码做的事情,lambda一行就搞定了;二是其实lambda并没有引入新东西,lambda做的事情,用普通的java也能做,只是效率没那么高而已。

当然还有别的原因,在集合的处理上,lambda表达式能简化多线程或者多核的处理,这也是java8打动人的原因。

(b) Comparator

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
public class Car {
private String color;
private int price;
private int age;
....
}

public class ComparatorTest {
public static void main (String[] args) {

List <Car> carList = LambdaTest.buildCarList();
System.out.println("\n=== Sorted with normal java ASC ===");
Collections.sort(carList, new Comparator <Car>() {
@Override
public int compare (Car o1, Car o2) {
return o1.getPrice() - o2.getPrice();
}
});

for (Car car : carList) {
System.out.print(car.getPrice() + " ");
}

System.out.println("\n=== Sorted with lambda ASC ===");
Collections.sort(carList, (Car car1, Car car2) -> (car1.getPrice() - car2.getPrice()));
for (Car car : carList) {
System.out.print(car.getPrice() + " ");
}

System.out.println("\n=== Sorted with lambda DESC ===");
Collections.sort(carList, (car1, car2) -> (car2.getPrice() - car1.getPrice()));
carList.forEach(c -> System.out.print(c.getPrice() + " "));
}
}
}

上面例子中也明显可以看出,使用Lambda做一些自定义的排序,更为灵活简便。升序和降序只用改变一下位置就可以。注意降序排列跟升序排的写法并不一致,

Collections.sort(carList, (car1, car2) -> (car2.getPrice() - car1.getPrice()));

lambda会自动做参数类型推导,所以写的时候省略也是参数类型可以的!

遍历list用了forEach

carList.forEach(c -> System.out.print(c.getPrice() + " "));

(c) ::的使用

使用::有三种方式

  • object::instanceMethod
  • Class::staticMethod
  • Class::instanceMethod
  1. 第一种方式
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class Greeter {
public void greet ( ) {
System.out.println("Hello, world!");
}
}

public class ConcurrentGreeter extends Greeter {
public void greet ( ) {
Thread t = new Thread(super::greet);
t.start();
}

public static void main (String[] args) {
new ConcurrentGreeter().greet();
}
}

上面代码运行后,会调用父类Greeter的greet方法

  1. 第二种方式
1
list.forEach(System.out::println);

上述代码也等价与list.forEach(l -> System.out.println(l));

  1. 第三种方式
1
2
3
List<String> labels = ...;
Stream<Button> stream = labels.stream().map(Button::new);
List<Button> buttons = stream.collect(Collectors.toList());

stream也是java8中的核心内容之一,第二章仔细介绍类stream的用法。

Lambda最佳实践

待补充

2. Stream(流)

(a)简介

stream也叫流,大概是java8最受追捧和欢迎的特性,stream最大的特点有两个:

  • 代码简介,函数式编程的的写法让代码更易读的同时,长度也缩短了很多
  • 多核友好, 程序员几乎不用关心串并行的事儿,吃着火锅,唱着歌就能写出高效率的代码。

(b) stream是如何工作的

1
2
3
4
5
6
7
8
9
List <String> myList =
Arrays.asList("a1", "a2", "b1", "c2", "c1", "c5", "c3");

myList
.stream()
.filter(s -> s.startsWith("c"))
.map(String::toUpperCase)
.sorted()
.forEach(System.out::println);

流函数有两种类型,一种是中间类型,一种是终止类型,中间类型返回一个stream对象方便大家做优雅的链式调用,终止类型或者说终端类型是返回一个对象或者是吗都不返回,比如List或者map等等。总而言之,stream到这个函数就终止了,所以叫终止类型。

上述例子中, filter, map, sorted就是中间类型, forEach就是终止类型。

大部分stream函数是接受lambda作为参数的, 其实也容易理解,要用lambda定义这个流函数具体要做什么,filter是做过滤,但具体怎么做,过滤哪些,留下哪些?都是要lambda来定义,当然可以用java函数代替,但明显没有lambda优雅,可读性高,这也是为什么要用labmda的原因之一。

注意,不要在流函数中,改变集合,比如增加或者删除元素。

(c) stream类型介绍

流可以从串并行的缴费可以分为stream和parallelStream,即串行流和并行流,但其实一种stream.从类型上有可以分几类,创建方式如下:

  1. 第一种也是最常见的一种是通过List,Set等对象的stream()方法,获取stream
1
2
3
4
Arrays.asList("a1", "a2", "a3")
.stream()
.findFirst()
.ifPresent(System.out::println);
  1. 通过Stream.of()函数创建流
1
2
3
Stream.of("a1", "a2", "a3")
.findFirst()
.ifPresent(System.out::println);
  1. java8也定义了几种基础类型的流, 比如IntStream, LongStream, DoubleStream
1
2
IntStream.range(1, 4)
.forEach(System.out::println);

基础类型的流跟对象流的用法相同又有所不同,相同之处是他们都继承自BaseStream。 不同之处是部分函数参数有所不同, 比如IntStream使用IntFunction而不是Function, 用IntPredicate而不是Predicate; 基础类型的流支持一些聚合流函数,比如sum, average等。

1
2
3
4
Arrays.stream(new int[] {1, 2, 3})
.map(n -> 2 * n + 1)
.average()
.ifPresent(System.out::println);

不同的流类型之间也可以做转换, 比如IntStream可以转为DoubleStream

1
IntStream.range(1,10).mapToDouble(Double::new).forEach(System.out::println);

对象流可以转换为基础类型流

1
2
3
4
5
Stream.of("a12", "a23", "a34")
.map(s -> s.substring(1))
.mapToInt(Integer::parseInt)
.max()
.ifPresent(System.out::println);

基础类型流可以转换为对象流

1
2
3
IntStream.range(1, 4)
.mapToObj(i -> "a" + i)
.forEach(System.out::println);

360度前空翻转体两周半流类型转换

1
2
3
4
Stream.of(1.0, 2.0, 3.0)
.mapToInt(Double::intValue)
.mapToObj(i -> "a" + i)
.forEach(System.out::println);

(d) 流函数执行顺序

流的本质跟sql有些类似,即使是为了获取相同的数据,不同的写法效率也大大不同。

先看一段代码

1
2
3
4
5
6
7
8
9
10
Stream.of("d2", "a2", "b1", "b3", "c")
.map(s -> {
System.out.println("map: " + s);
return s.toUpperCase();
})
.filter(s -> {
System.out.println("filter: " + s);
return s.startsWith("A");
})
.forEach(s -> System.out.println("forEach: " + s));

这个的输出结果是

1
2
3
4
5
6
7
8
9
10
11
map: d2
filter: D2
map: a2
filter: A2
forEach: A2
map: b1
filter: B1
map: b3
filter: B3
map: c
filter: C

如果改变下顺序呢?

1
2
3
4
5
6
7
8
9
10
Stream.of("d2", "a2", "b1", "b3", "c")
.filter(s -> {
System.out.println("filter: " + s);
return s.startsWith("a");
})
.map(s -> {
System.out.println("map: " + s);
return s.toUpperCase();
})
.forEach(s -> System.out.println("forEach: " + s));

运行结果:

1
2
3
4
5
6
7
filter: d2
filter: a2
map: a2
forEach: A2
filter: b1
filter: b3
filter: c

map跟forEach仅执行了一次

(e) stream复用

正常情况下stream是不能被复用的,运行下面代码会报错

1
2
3
4
5
6
7
8
9
10
11
12
13
Stream<String> stream =
Stream.of("d2", "a2", "b1", "b3", "c")
.filter(s -> s.startsWith("a"));

stream.anyMatch(s -> true);
stream.noneMatch(s -> true);

output:
java.lang.IllegalStateException: stream has already been operated upon or closed
at java.util.stream.AbstractPipeline.evaluate(AbstractPipeline.java:229)
at java.util.stream.ReferencePipeline.noneMatch(ReferencePipeline.java:459)
at com.winterbe.java8.Streams5.test7(Streams5.java:38)
at com.winterbe.java8.Streams5.main(Streams5.java:28)

复用的办法是使用supplier每次都构建一个新的流

1
2
3
4
5
6
Supplier<Stream<String>> streamSupplier =
() -> Stream.of("d2", "a2", "b1", "b3", "c")
.filter(s -> s.startsWith("a"));

streamSupplier.get().anyMatch(s -> true);
streamSupplier.get().noneMatch(s -> true);

(f) stream进阶

除了上面例子中用到的filer,map, forEach等还有一些也是会用到的流函数,先构造一个对象List

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class Person {
String name;
int age;

Person(String name, int age) {
this.name = name;
this.age = age;
}

@Override
public String toString() {
return name;
}
}

List<Person> persons =
Arrays.asList(
new Person("Paomo", 18),
new Person("Linzhi", 23),
new Person("Wenhe", 23),
new Person("Shenlv", 12));

collect

collect是比较常用的流终止函数,与Collectors类搭配使用,奇妙无穷。

获取Person中name以”P”开头的人:

1
2
3
4
5
6
7
List<Person> filtered =
persons
.stream()
.filter(p -> p.name.startsWith("P"))
.collect(Collectors.toList());

System.out.println(filtered); // Paomo

当然也可以获取Set,使用Collectors.toSet()即可。

1
2
3
4
5
6
Map<Integer, List<Person>> personsByAge = persons
.stream()
.collect(Collectors.groupingBy(p -> p.age));

personsByAge
.forEach((age, p) -> System.out.format("age %s: %s\n", age, p));

collect好像还好,但groupingBy一开始看有点晕,看下函数源码知道传入的lambda,是作为classfier把item映射成key, collect是会返回Person.age为key, 如果Person年纪相同,会放入同一个list当中。
最后得到结果

1
2
3
age 18: [Paomo]
age 23: [Linzhi, Wenhe]
age 12: [Shenlv]

Collectors非常灵活,如果你想计算这些Person的平均年龄:

1
2
3
4
5
Double averageAge = persons
.stream()
.collect(Collectors.averagingInt(p -> p.age));

System.out.println(averageAge);

获取这些人的一些统计数据:

1
2
3
4
5
6
7
IntSummaryStatistics ageSummary =
persons
.stream()
.collect(Collectors.summarizingInt(p -> p.age));

System.out.println(ageSummary);
//IntSummaryStatistics{count=4, sum=76, min=12, average=19.000000, max=23}

所有人拼接成一个字符串输出

1
2
3
4
5
6
7
8
String phrase = persons
.stream()
.filter(p -> p.age >= 18)
.map(p -> p.name)
.collect(Collectors.joining(" and ", "In China,", " are of legal age."));

System.out.println(phrase);
//output: In China, Paomo and Linzhi and Wenhe are of legal age.

joining的三个参数,第一个为分隔符,第二个为前缀,第三个为后缀。

Person转换为map

1
2
3
4
5
6
7
8
Map<Integer, String> map = persons
.stream()
.collect(Collectors.toMap(
p -> p.age,
p -> p.name,
(name1, name2) -> name1 + ";" + name2));

System.out.println(map);

toMap传入的第三个参数是mergeFunction, 即当key相同时,把value合并的函数。可以自定Collector,完成一些定制化的转换

1
2
3
4
5
6
7
8
9
10
11
Collector<Person, StringJoiner, String> personNameCollector =
Collector.of(
() -> new StringJoiner(" | "),
(j, p) -> j.add(p.name.toUpperCase()),
(j1, j2) -> j1.merge(j2),
StringJoiner::toString);

String names = persons
.stream()
.collect(personNameCollector);
//output: PAOMO | LINZHI | WENHE | SHENLV

collector里面要传4个参数,分别的supplier, accumulator, combiner和finisher, 关于这几个函数具体含义后面会有更详细的介绍。但是可以看出collect使用起来非常非常灵活。

reduce

reduce会将stream中的所有元素combine到一块儿,形成一个最终结果。reduce函数有三种重载形式:

1
2
3
4
5
6
7
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);

BinaryOperator,BinaryOperator其实是继承自BiFunction,主要是两种相同类型的参数,经过运算产生一个相同类型的结果的运算符;BiFunction定义类似,是接收两个参数,产生一个结果的函数。

第一种方式,获取年纪最大的人,代码如下:

1
2
3
4
persons
.stream()
.reduce((p1, p2) -> p1.age > p2.age ? p1 : p2)
.ifPresent(System.out::println);

第二种调用方式:

1
2
3
4
5
6
7
8
9
10
Person result =
persons
.stream()
.reduce(new Person("", 0), (p1, p2) -> {
p1.age += p2.age;
p1.name += p2.name;
return p1;
});
//output:
//name=PaomoLinzhiWenheShenlv; age=76

上面方法返回的是一个年纪为list中所有person的age之和,name为所有人名字的拼接结果

第三种调用方式:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
Integer ageSum = persons
.stream()
.reduce(0,
(sum, p) -> {
System.out.format("accumulator: sum=%s; person=%s\n", sum, p);
return sum += p.age;
},
(sum1, sum2) -> {
System.out.format("combiner: sum1=%s; sum2=%s\n", sum1, sum2);
return sum1 + sum2;
});
//output:
//accumulator: sum=0; person=Paomo
//accumulator: sum=18; person=Linzhi
//accumulator: sum=41; person=Wenhe
//accumulator: sum=64; person=Shenlv

啊? reduce只调用了accumulator没调用combiner? 其实这是因为combiner是在串行流中才会用到。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
Integer ageSum = persons
.parallelStream()
.reduce(0,
(sum, p) -> {
System.out.format("accumulator: sum=%s; person=%s\n", sum, p);
return sum += p.age;
},
(sum1, sum2) -> {
System.out.format("combiner: sum1=%s; sum2=%s\n", sum1, sum2);
return sum1 + sum2;
});
//output:
//accumulator: sum=0; person=Wenhe
//accumulator: sum=0; person=Paomo
//accumulator: sum=0; person=Linzhi
//accumulator: sum=0; person=Shenlv
//combiner: sum1=23; sum2=12
//combiner: sum1=18; sum2=23
//combiner: sum1=41; sum2=35

accumulator是并行被调用所以需要combiner把accumalotr中的值归并累加。

(g) Parallel Streams(并行流)

面对数据量比较大的情况,可以使用parallelStream,以充分利用系统的多核和性能,可以使用如下代码获取到当前的并行线程:

1
2
3
ForkJoinPool commonPool = ForkJoinPool.commonPool();
System.out.println(commonPool.getParallelism());
//output: 7

针对上面那个例子,我们可以看下,到底每个操作是在哪个线程中执行的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
persons
.parallelStream()
.reduce(0,
(sum, p) -> {
System.out.format("accumulator: sum=%s; person=%s [%s]\n",
sum, p, Thread.currentThread().getName());
return sum += p.age;
},
(sum1, sum2) -> {
System.out.format("combiner: sum1=%s; sum2=%s [%s]\n",
sum1, sum2, Thread.currentThread().getName());
return sum1 + sum2;
});
//output:
//accumulator: sum=0; person=Wenhe [main]
//accumulator: sum=0; person=Shenlv [ForkJoinPool.commonPool-worker-6]
//accumulator: sum=0; person=Linzhi [ForkJoinPool.commonPool-worker-5]
//accumulator: sum=0; person=Paomo [ForkJoinPool.commonPool-worker-2]
//combiner: sum1=23; sum2=12 [ForkJoinPool.commonPool-worker-6]
//combiner: sum1=18; sum2=23 [ForkJoinPool.commonPool-worker-2]
//combiner: sum1=41; sum2=35 [ForkJoinPool.commonPool-worker-2]

可以看出accumulator和combiner都是并行执行的。

3. 新增API

(1) 时间处理

原来时间处理函数存在的问题:

  • 线程安全: Date和Calendar不是线程安全的,你需要编写额外的代码处理线程安全问题
  • API设计和易用性: 由于Date和Calendar的设计不当你无法完成日常的日期操作
  • ZonedDate和Time: 你必须编写额外的逻辑处理时区和那些旧的逻辑

(a) 获取当前日期

1
System.out.println("localDate: " + LocalDate.now());

(b) 判断是否为闰年

1
2
//leap year
System.out.println("is leap year: LocalDate.now()" + LocalDate.now().isLeapYear());

(c) 日期比较

1
2
3
4
5
6
// time comparison
boolean isBefore = LocalDate.parse("2018-02-20")
.isBefore(LocalDate.parse("2018-01-22"));
System.out.println("isBefore: " + isBefore);
boolean isAfter = LocalDate.parse("2018-02-20").isAfter(LocalDate.parse("2018-01-22"));
System.out.println("isAfter: " + isAfter);
1
2
3
4
// first day or last day of the month
LocalDate lastDayOfMonth = LocalDate.parse("2018-02-20")
.with(TemporalAdjusters.lastDayOfMonth());
System.out.println("Last Day of 2018/07: " + lastDayOfMonth);

(d) 日期格式化

string format容易被忽视,但是也很重要

yyyy是指当天所在的年份, YYYY是指当前周所在的年份。

如果不注意跨年可能出现bug(https://www.atatech.org/articles/97733)

MM是月份, mm是分钟

1
2
3
4
5
// format date
LocalDateTime now = LocalDateTime.now();
DateTimeFormatter dateTimeFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
System.out.println("default time format: " + now);
System.out.println("custom time format: " + now.format(dateTimeFormatter));

(e) 计算时间间隔

1
2
3
LocalDateTime finalDate = LocalDateTime.now().plus(Period.ofDays(10));
Long between = ChronoUnit.MINUTES.between(now, finalDate);
System.out.println("minutes between now and ten days after: " + between);

(2)interface的default函数

java8还是支持的接口可以设置default方法

1
2
3
4
5
6
7
interface MathTool {
double calculate(int a);

default double sqrt(int a) {
return Math.sqrt(a);
}
}

5. 奇技淫巧

(1)捕获多个Exception

1
2
3
4
5
6
7
8
try {
Thread.sleep(20000);
FileInputStream fis = new FileInputStream("/a/b.txt");
} catch (InterruptedException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}

这种每个都捕获的写法并不优雅,其实jdk7就提供了一种相对优雅的方式:

1
2
3
4
5
6
try {
Thread.sleep(20000);
FileInputStream fis = new FileInputStream("/a/b.txt");
} catch (InterruptedException | IOException e) {
e.printStackTrace();
}

(2) 字符串拼接

1
String str = String.join(",", "a", "b", "c");

6.引用

  1. https://stackoverflow.com/questions/16501/what-is-a-lambda-function
  2. http://winterbe.com/posts/2014/07/31/java8-stream-tutorial-examples/
坚持原创技术分享,您的支持将鼓励我继续创作!