java8新特性之Stream

概述

Stream API与InputStream和OutputStream是完全不同的概念,Stream API是对Java中集合操作的增强,可以利用它进行各种过滤、排序、分组、聚合等操作。Stream API配合Lambda表达式可以加大的提高代码可读性和编码效率,Stream API也支持并行操作。

流不是集合,它不关心数据的存放,只关注如何处理数据

Stream API主要用于处理集合操作,不过它的处理方式与传统的方式不同,称为“数据流处理”。流(Stream)类似于关系数据库的查询操作,是一种声明式操作。比如要从数据库中获取所有年龄大于20岁的用户的名称,并按照用户的创建时间进行排序,用一条SQL语句就可以搞定,不过使用Java程序实现就会显得有些繁琐,这时候可以使用流:

List userNames = users.stream()
    .filter(user -> user.getAge() > 20)
    .sorted(comparing(User::getCreationDate))
    .map(User::getUserName)
    .collect(toList());

在Java中,集合是一种数据结构,或者说是一种容器,用于存放数据,流不是容器,它不关心数据的存放,只关注如何处理。可以把流当做是Java中的Iterator,不过它比Iterator强大多了。

流使用内部迭代方式处理数据

流与集合另一个区别在于他们的遍历方式,遍历集合通常使用for-each方式,这种方式称为外部迭代,而流使用内部迭代方式,也就是说它帮你把迭代的工作做了,你只需要给出一个函数来告诉它接下来要干什么:

// 外部迭代
List list = Arrays.asList("A", "B", "C", "D");
for (String str : list) {
    System.out.println(str);
}

// 内部迭代
list.stream().forEach(System.out::println);

外部迭代更像是作文题,我们不仅要控制元素的迭代方式,还需要定义怎么操作元素;内部迭代更像是填空题,我们只用关注如何操作元素就可以了。

流只能遍历一次

流只能遍历一次,遍历结束后,这个流就被关闭掉了。如果要重新遍历,可以从数据源(集合)中重新获取一个流。如果你对一个流遍历两次,就会抛出java.lang.IllegalStateException异常:

List list = Arrays.asList("A", "B", "C", "D");
Stream stream = list.stream();
stream.forEach(System.out::println);
stream.forEach(System.out::println); // 这里会抛出java.lang.IllegalStateException异常,因为流已经被关闭

流通常由三部分构成:

  1. 数据源:数据源一般用于流的获取,比如本文开头那个过滤用户的例子中users.stream()方法。
  2. 中间处理:中间处理包括对流中元素的一系列处理,如:过滤(filter()),映射(map()),排序(sorted())。
  3. 终端处理:终端处理会生成结果,结果可以是任何不是流值,如List;也可以不返回结果,如stream.forEach(System.out::println)就是将结果打印到控制台中,并没有返回。

创建流

由值创建流

使用静态方法Stream.of()创建流,该方法接收一个变长参数:

Stream stream = Stream.of("A", "B", "C", "D");
//也可以使用静态方法Stream.empty()创建一个空的流:
Stream stream = Stream.empty();

由数组、集合 创建流

使用静态方法Arrays.stream()从数组创建一个流,该方法接收一个数组参数:

String[] strs = {"A", "B", "C", "D"};
Stream stream = Arrays.stream(strs);

通过文件生成流

使用java.nio.file.Files类中的很多静态方法都可以获取流,比如Files.lines()方法,该方法接收一个java.nio.file.Path对象,返回一个由文件行构成的字符串流:

Stream stream = Files.lines(Paths.get("text.txt"), Charset.defaultCharset());

通过函数创建流

java.util.stream.Stream中有两个静态方法用于从函数生成流,他们分别是Stream.generate()和Stream.iterate():

// iteartor 打印100以内的所有偶数
Stream.iterate(0, n -> n + 2).limit(51).forEach(System.out::println);

// generate 打印10个Hello Man!
Stream.generate(() -> "Hello Man!").limit(10).forEach(System.out::println);

值得注意的是,这两个方法生成的流都是无限流,没有固定大小,可以无穷的计算下去,可以使用limit()来限制边界。

//一般来说,iterate()用于生成一系列值,比如生成以当前时间开始之后的10天的日期:
Stream.iterate(LocalDate.now(), date -> date.plusDays(1)).limit(10).forEach(System.out::println);
//generate()方法用于生成一些随机数,比如生成10个UUID:
Stream.generate(() -> UUID.randomUUID().toString()).limit(10).forEach(System.out::println);

collect(toList())将流中的值转换为集合

//of从初识值生成新的stream,再通过collect(toList())生成新的集合
List collect = Stream.of("aa", "bb", "cc", "aa", "bb").collect(Collectors.toList());

of是惰性求值,不生成新的集合,通过collect(toList())这种及早求值才生成新的集合。返回值是stream就是惰性求值,返回值为另一个值或者空就是及早求值。

惰性求值与及早求值的区别

//如下加count就是及早求值,会sout输出strs。不加count,程序不输出任何信息
strs.stream().filter(str -> {
    System.out.println(str);
    return str.startsWith("a");
}
).count();

过滤+计数,只循环了一次。只执行fileter会返回一个stream,他不是一个新集合,仅仅是创建新集合的配方。

流常用方法

过滤和排序

Stream.of(1, 8, 5, 2, 1, 0, 9, 2, 0, 4, 8)
    .filter(n -> n > 2)     // 对元素过滤,保留大于2的元素
    .distinct()             // 去重,类似于SQL语句中的DISTINCT
    .skip(1)                // 跳过前面1个元素
    .limit(2)               // 返回开头2个元素,类似于SQL语句中的SELECT TOP
    .sorted()               // 对结果排序
    .forEach(System.out::println);

filter 对集合的值进行过滤,必须返回true或者false

查找和匹配

//检查流中的任意元素是否包含字符串"PHP"
boolean match1 = getStream().anyMatch(s -> s.equals("PHP"));
System.out.println(match1);
boolean match2 = getStream().collect(Collectors.toList()).contains("PHP");
System.out.println(match2);

// 检查流中的所有元素是否都包含字符串"#"
boolean hasAllMatch = getStream().allMatch(s -> s.contains("#"));

// 检查流中的任意元素是否没有以"C"开头的字符串
boolean hasNoneMatch = getStream().noneMatch(s -> s.startsWith("C"));

// 查找元素
Optional element = getStream().ilter(s -> s.contains("C"))
    // .findFirst()     // 查找第一个元素
    .findAny();         // 查找任意元素
 
//获取初始流   
private static Stream getStream() {
    return Stream.of("Java", "C#", "PHP", "C++", "Python");
}
  1. 每次都需要重新getStream()来获取新的流,因为之前的流已经使用并且关闭了。可以理解为若返回值不是stream,则表示已经结束了流操作
  2. findAny的返回类型是一个Optional类(java.util.Optional),它一个容器类,代表一个值存在或不存在,用来避免控制正异常。
  3. findFirst()和findAny()返回的都是第一个元素,通过查看javadoc描述,大致意思是findAny()是为了提高并行操作时的性能。数据大时可findany。

归约

归约操作就是将流中的元素进行合并,形成一个新的值,常见的归约操作包括求和,求最大值或最小值。归约操作一般使用reduce()方法,与map()方法搭配使用,可以处理一些很复杂的归约操作。

// 获取流
List books = Arrays.asList(
   new Book("Java编程思想", "Bruce Eckel", "机械工业出版社", 108.00D),
   new Book("Java 8实战", "Mario Fusco", "人民邮电出版社", 79.00D),
   new Book("MongoDB权威指南(第2版)", "Kristina Chodorow", "人民邮电出版社", 69.00D)
);

// 计算所有图书的总价
Optional totalPrice = books.stream()
       .map(Book::getPrice)
       .reduce((n, m) -> n + m);

// 价格最高的图书
Optional expensive = books.stream().max(Comparator.comparing(Book::getPrice));

// 价格最低的图书
Optional cheapest = books.stream().min(Comparator.comparing(Book::getPrice));

// 计算总数
long count = books.stream().count()

reduce从一组值中生成一个值,如累加。

//0初始值,total总值,index循环的每一个值。
Integer num = Stream.of(1, 2, 3).reduce(0, (total, index) -> total + index);

max(min) 传入comparator进行大小比较,返回Optional对象

//Optional对象(代表一个可能存在也可能不存在的值,NPE方案),通过get拿到Optional对象中的值
Integer integer = Stream.of(1, 2, 3).min(Comparator.comparing(num -> num)).get();

map 将流中的值转换为新值

//将list小写处理成大写
Stream.of("aa", "bb", "cc", "aa", "bb").map(str -> str.toUpperCase()).collect(Collectors.toList());

flatmap 把多个stream合并成一个stream并返回

与map()方法类似的还有一个flatMap(),flatMap()方法让你把一个流中的每个值都换成另一个流,然后把所有的流连接起来成为一个新的流。

//传统for循环
List names0 = new ArrayList<>();
for (Company company : data) {
    List users = company.getUesrs();
    for (User user : users) {
        if (user.getAge() > 30) {
            names0.add(user.getName());
        }
    }
}
//lambda方式1
List names1 = data.stream()
    .flatMap(company -> company.getUesrs().stream())
    .filter(user -> user.getAge() > 30)
    .map(user -> user.getName())
    .collect(Collectors.toList());
//lambda方式2
List names2 = data.stream()
    .map(Company::getUesrs)
    .flatMap(Collection::stream)
    .filter(user -> user.getAge() > 30)
    .map(User::getName)
    .collect(Collectors.toList());

使用flatMap()方法的效果是,各个数组并不是分别映射成一个流,而是映射成流的内容。所有使用map(Arrays::stream)时生成的单个流都被合并起来,也就是对流扁平化操作。

peek 用于记录中间值(输出流中的值),不会改变流。一般用作日志输出,断点调试。

List nums = Stream.of(1, 2, 3, 4, 5).collect(Collectors.toList());
nums.stream().filter(num -> num > 3).peek(num -> {
    System.out.println(num);
}).collect(Collectors.toList());

数据收集

前面总结了如何创建流以及流处理,接下来总结下数据收集。数据收集是流式数据处理的终端处理,与中间处理不同的是,终端处理会消耗流,也就是说,终端处理之后,这个流就会被关闭,如果再进行中间处理,就会抛出异常。数据收集主要使用collect方法,该方法也属于归约操作,像reduce()方法那样可以接收各种做法作为参数,将流中的元素累积成一个汇总结果,具体的做法是通过定义新的Collector接口来定义的。

可以静态导入Collectors和Comparator两个类,这样就不用再去写Collectors.counting()和Comparator.comparing()。

import static java.util.stream.Collectors.*;
import static java.util.Comparator.*;

// 求和
long count = books.stream().collect(counting());
// 价格最高的图书
Optional expensive = books.stream().collect(maxBy(comparing(Book::getPrice)));
// 价格最低的图书
Optional cheapest = books.stream().collect(minBy(comparing(Book::getPrice)));

收集顺序

进有序,出有序;进无序,出无序。

特定收集器

stream.collect(toCollection(TreeSet::new));

字符串操作joining

//将流中的字符串通过逗号连接成一个新的字符串
String str = Stream.of("A", "B", "C", "D").collect(joining(","));
List data = Stream.of(1,2,3,4,5).collect(toList());
List integers = Arrays.asList(1, 2, 3, 4, 5);

//前后缀拼接
//传统for循环
StringBuilder sb = new StringBuilder("[");
for (Integer num : integers) {
    sb.append(num).append(", ");
}
sb.append("]");
System.out.println(sb);
//lambda循环(map是为了将int转为string)
String str = data.stream().map(String::valueOf).collect(joining(",", "[", "]"));
System.out.println(str);

数据分块(parttitioningBy,将流分解成两个集合),传入一个pridicate对象来判断属于哪部分。

//数据分块:将数据分成true和false两部分
Map> res = users.stream().collect(partitioningBy(user -> user.getName().length() > 3));

数据分组(groupingBy)

//数据分组:将数据以某个key值分成多部分
Map> result = users.stream().collect(groupingBy(user -> user.getAddress()));
Map> result = users.stream().collect(groupingBy(User::getAddress));

组合收集器(下游收集器),在主收集器中应用下级收集器,对结果进行再次封装。

//将数据分组后,不是返回每组的数据,而是统计每组的个数返回。
Map collect1 = users.stream().collect(groupingBy(User::getName, counting()));

//通过mapping可以进行其他二次收集。
Map> collect2 = users.stream().collect(groupingBy(User::getAddress, mapping(User::getName, toList()) ));

函数拆分示例:collect( groupingBy(key1,  mapping(key2,  value)  )  )

进阶集合操作

map的foreach循环

Map cache = new HashMap<>();
Map> users = new HashMap>();

//传统for循环
for (Map.Entry> entry : users.entrySet()) {
    String key = entry.getKey();
    List value = entry.getValue();
    cache.put(key, value.size());
}
//lambda方式
users.forEach((key, value) -> {
    cache.put(key, value.size());
});

computeIfAbsent

根据key获取某个值,若值不存在,丛数据库中取

//传统方式
public List getUserByName(String name) {
    List user = users.get(name);
    if (user == null) {
        user = readFromDB(name);
        users.put(name, user);
    }
    return user;
}
//lambda方式
public List getUser1ByName(String name) {
    return users.computeIfAbsent(name, this::readFromDB);
}

private List readFromDB(String name) {
    return new ArrayList<>();
}

你可能感兴趣的:(java)