浅谈 collection.stream() 以及 collect() 方法

前言

stream()方法和collect()方法都是 java8 的新特性:

List<String> widgetIds = widgets.stream().map(Widget::getWidgetId).collect(Collectors.toList());
解释下一这行代码
widgets:一个实体类的集合,类型为List
Widget:实体类
getWidgetId:实体类中的get方法,为获取Widget的id

本来想要获得wiget的id集合,按照我的思路肯定是遍历widges,依次取得widgetIds,但是此行代码更加简洁,高效
stream()优点:

  1. 无存储。stream不是一种数据结构,它只是某种数据源的一个视图,数据源可以是一个数组,Java容器或I/O channel等。
  2. 为函数式编程而生。对stream的任何修改都不会修改背后的数据源,比如对stream执行过滤操作并不会删除被过滤的元素,而是会产生一个不包含被过滤元素的新stream。
  3. 惰式执行。stream上的操作并不会立即执行,只有等到用户真正需要结果的时候才会执行。
  4. 可消费性。stream只能被“消费”一次,一旦遍历过就会失效,就像容器的迭代器那样,想要再次遍历必须重新生成。
  5. Stream 就如同一个迭代器(Iterator),单向,不可往复,数据只能遍历一次,遍历过一次后即用尽了。

流(stream)的操作类型分为两种:

  • Intermediate:一个流可以后面跟随零个或多个 intermediate 操作。其目的主要是打开流,做出某种程度的数据映射/过滤,然后返回一个新的流,交给下一个操作使用。这类操作都是惰性化的(lazy),就是说,仅仅调用到这类方法,并没有真正开始流的遍历。

  • Terminal:一个流只能有一个 terminal 操作,当这个操作执行后,流就被使用“光”了,无法再被操作。所以这必定是流的最后一个操作。Terminal 操作的执行,才会真正开始流的遍历,并且会生成一个结果,或者一个 side effect。

流(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<String> 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);

流转换为其它数据结构

// 1. Array
String[] strArray1 = stream.toArray(String[]::new);
// 2. Collection
List<String> list1 = stream.collect(Collectors.toList());
List<String> 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().map()方法的使用示例:
List<String> list= Arrays.asList("a", "b", "c", "d");
 
List<String> collect =list.stream().map(String::toUpperCase).collect(Collectors.toList());
System.out.println(collect); //[A, B, C, D]
  
List<Integer> num = Arrays.asList(1,2,3,4,5);
List<Integer> collect1 = num.stream().map(n -> n * 2).collect(Collectors.toList());
System.out.println(collect1); //[2, 4, 6, 8, 10]

Lambda 之 Collection Stream

  • Collection.stream() 测试实体类:
class Demo {
    private Long id;
    private String name;   
}
  • // 实例化并组成一个List:
List<Demo> demos = Lists.newArrayList(new Demo(1L, "SF"), new Demo(2L, "AXE"));
  1. map + collect用法
  • 场景1.0:获取List列表的所有 id 之 No Use Lambda:
public static List<Long> getIds(List<Demo> demos){
        List<Long> ids = Lists.newArrayList();
        for (Demo d : demos) {
            ids.add(d.getId());
        }
        return ids;
}
  • 场景1.1:获取List列表的所有 id 之 Use Lambda:
List<Long> ids = demos.stream().map(Demo::getId).collect(toList());
//最后toList() 可以有更多的实现,比如:
Set<Long> ids = demos.stream().map(Demo::getId).collect(toSet());
//下面这段代码生成一个整数 list 的平方数 {1, 4, 9, 16}。
List<Integer> nums = Arrays.asList(1, 2, 3, 4);
List<Integer> squareNums = nums.stream().map(n -> n * n).collect(Collectors.toList());
  • 场景2.0:我们时常把 list 变成 map ,将多次 List 的 O(n) 遍历变成 Map 的 O(1) 查询,拿空间换时间。
    list 变成 map 之 No Use Lambda:
public static Map<Long, Demo> getIds(List<Demo> demos){
        Map<Long, Demo> map = Maps.newHashMap;
        for (Demo d : demos) {
            map.put(d.getId, d);
        }
        return map;
}
  • 场景2.1:list 变成 map 之 Use Lambda:
 Map<Long, Demo> map = demos.stream().collect(toMap(Demo::getId, o -> o));
  1. filter用法
  • 场景1.0:从List中找到name="SF"的 Demo 实例之No Use Lambda:
public static Demo getSpecifyDemo(String name, List<Demo> demos){
    Demo target = null;
    for (Demo demo : demos) {
        if (name.equals(demo.getName())) {
            target = demo;
        }
    }
    return target;
}
  • 场景1.1:从List中找到name="SF"的 Demo 实例之 Use Lambda:
String targetName = "SF";
Demo target = demos.stream().filter(d -> targetName.equals(d.getName())).findFirst().orElse(null);
//上面这种写法非常简单,但是调用链太长,一个lambda能够绕地球好几圈,最好写成以下格式,防止步子迈得太大:
Demo target = demos.stream()
            .filter(d -> targetName.equals.equals(d.getName()))
            .findFirst()
            .orElse(null);
//find()的结果是Optional,Optional号称NPE终结者,于是对于find()的结果你可以随意使用终结者携带的任何武器
//例如orElse(),ifPresent(),isPresent()…每个用起来都是那种哒哒哒冒蓝火的,更多姿势详见Optional的裸体源码。
  1. match用法
    match()是filter()的缩写版本,返回结果只有boolean类型,返回是否匹配。
  • 场景1.1:当前list中某个元素是否符合某个条件 这个例子,给出另一个用法Demo::getId 之 No Use Lambda
List<String> condition = new ArrayList<>();
condition.add("SF");

public static boolean isExist(List<String> condition, List<Demo> demos){
    boolean flag = false;
    for (Demo demo : demos) {
        if (condition.contains(demo.getName())) {
            flag = true;
            break;
        }
    }
    return flag;
}
  • 场景1.2:当前list中某个元素是否符合某个条件 这个例子,给出另一个用法Demo::getId 之 Use Lambda中 filter
boolean flag = demos.stream()
            .map(Demo::getName)
            .filter(condition::contains)
            .findAny()
            .isPresent();
  • 场景1.3:当前list中某个元素是否符合某个条件 这个例子,给出另一个用法Demo::getId 之 Use Lambda中 match
boolean flag = demos.stream()
            .map(Demo::getName)
            .anyMatch(condition::contains);

这里还有一个说法:
Stream 有三个 match 方法,从语义上说:

  • allMatch:Stream 中全部元素符合传入的 predicate,返回 true

  • anyMatch:Stream 中只要有一个元素符合传入的 predicate,返回 true

  • noneMatch:Stream 中没有一个元素符合传入的 predicate,返回 true
    它们都不是要遍历全部元素才能返回结果。例如 allMatch 只要一个元素不满足条件,就 skip 剩下的所有元素,返回 false。

//Match使用示例
List<Person> 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);
//结果
All are adult? false
Any child? true
  1. forEach用法
  • 场景1.1:打印所有男性姓名,roster为person集合类型为List<Pserson> 之 Use Lambda:
roster.stream().filter(p -> p.getGender() == Person.Sex.MALE).forEach(p -> System.out.println(p.getName()));
  • 场景1.2:打印所有男性姓名,roster为person集合类型为List <Person>之 No Use Lambda:
for (Person p : roster) {
    if (p.getGender() == Person.Sex.MALE) {
        System.out.println(p.getName());
    }
}

当需要为多核系统优化时,可以 parallelStream().forEach(),只是此时原有元素的次序没法保证,并行的情况下将改变串行时操作的行为,此时 forEach 本身的实现不需要调整,而 Java8 以前的 for 循环 code 可能需要加入额外的多线程逻辑。

另外一点需要注意,forEach 是 terminal 操作,因此它执行后,Stream 的元素就被“消费”掉了,你无法对一个 Stream 进行两次 terminal 运算。下面代码是错误的:

//错误代码示例,一个stream不可以使用两次
stream.forEach(element -> doOneThing(element));
stream.forEach(element -> doAnotherThing(element));
  1. findFirst用法
    这是一个 termimal 兼 short-circuiting 操作,它总是返回 Stream 的第一个元素,或者空。

这里比较重点的是它的返回值类型:Optional。这也是一个模仿 Scala 语言中的概念,作为一个容器,它可能含有某值,或者不包含。使用它的目的是尽可能避免 NullPointerException

String strA = " abcd ", strB = null;
print(strA);
print("");
print(strB);
getLength(strA);
getLength("");
getLength(strB);
//输出text不为null的值
public static void print(String text) {
    // Java 8 之 Use Lambda
     Optional.ofNullable(text).ifPresent(System.out::println);
    // Pre-Java 8 之 No Use Lambda
     if (text != null) {
        System.out.println(text);
     }
 }
//输出text的长度,避免空指针
public static int getLength(String text) {
    // Java 8 之 Use Lambda
    return Optional.ofNullable(text).map(String::length).orElse(-1);
    // Pre-Java 8 之 No Use Lambda
    return if (text != null) ? text.length() : -1;
}

在更复杂的 if (xx != null) 的情况中,使用 Optional 代码的可读性更好,而且它提供的是编译时检查,能极大的降低 NPE 这种 Runtime Exception 对程序的影响,或者迫使程序员更早的在编码阶段处理空值问题,而不是留到运行时再发现和调试。

Stream 中的 findAny、max/min、reduce 等方法等返回 Optional 值。还有例如 IntStream.average() 返回 OptionalDouble 等等。

  1. reduce用法
    这个方法的主要作用是把 Stream 元素组合起来。它提供一个起始值(种子),然后依照运算规则(BinaryOperator),和前面 Stream 的第一个、第二个、第 n 个元素组合。从这个意义上说,字符串拼接、数值的 sum、min、max、average 都是特殊的 reduce。也有没有起始值的情况,这时会把 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, 无起始值,返回Optional,所以有get()方法
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);
  1. limit/skip用法
    limit 返回 Stream 的前面 n 个元素;skip 则是扔掉前 n 个元素(它是由一个叫 subStream 的方法改名而来)。
// limit 和 skip 对运行次数的影响
public void testLimitAndSkip() {
     List<Person> persons = new ArrayList();
     for (int i = 1; i <= 10000; i++) {
         Person person = new Person(i, "name" + i);
         persons.add(person);
    }
    List<String> 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;
     }
}
//结果
name1
name2
name3
name4
name5
name6
name7
name8
name9
name10
[name4, name5, name6, name7, name8, name9, name10]
//这是一个有 10,000 个元素的 Stream,但在 short-circuiting 操作 limit 和 skip 的作用下,管道中 map 操作指定的 getName() 方法的执行次数为 limit 所限定的 10 次,而最终返回结果在跳过前 3 个元素后只有后面 7 个返回。

有一种情况是 limit/skip 无法达到 short-circuiting 目的的,就是把它们放在 Stream 的排序操作后,原因跟 sorted 这个 intermediate 操作有关:此时系统并不知道 Stream 排序后的次序如何,所以 sorted 中的操作看上去就像完全没有被 limit 或者 skip 一样。

 List<Person> persons = new ArrayList();
 for (int i = 1; i <= 5; i++) {
     Person person = new Person(i, "name" + i);
     persons.add(person);
 }
List<Person> 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.StreamDW$Person@816f27d, stream.StreamDW$Person@87aac27]
//虽然最后的返回元素数量是 2,但整个管道中的 sorted 表达式执行次数没有像前面例子相应减少。
  1. sorted用法
    对 Stream 的排序通过 sorted 进行,它比数组的排序更强之处在于你可以首先对 Stream 进行各类 map、filter、limit、skip 甚至 distinct 来减少元素数量后,再排序,这能帮助程序明显缩短执行时间。
List<Person> persons = new ArrayList();
 for (int i = 1; i <= 5; i++) {
     Person person = new Person(i, "name" + i);
     persons.add(person);
 }
List<Person> personList2 = persons.stream().limit(2).sorted((p1, p2) -> p1.getName().compareTo(p2.getName())).collect(Collectors.toList());
System.out.println(personList2);
//结果
name2
name1
[stream.StreamDW$Person@6ce253f1, stream.StreamDW$Person@53d8d10a]
  1. min/max/distinct用法
    min 和 max 的功能也可以通过对 Stream 元素先排序,再 findFirst 来实现,但前者的性能会更好,为 O(n),而 sorted 的成本是 O(n log n)。同时它们作为特殊的 reduce 方法被独立出来也是因为求最大最小值是很常见的操作。
//找出最长一行的长度
BufferedReader br = new BufferedReader(new FileReader("c:\\Service.log"));
int longest = br.lines().mapToInt(String::length).max().getAsInt();
br.close();
System.out.println(longest);
//找出全文的单词,转小写,并排序,使用 distinct 来找出不重复的单词。单词间只有空格
List<String> 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);

自己生成流

通过实现 Supplier 接口,你可以自己来控制流的生成。这种情形通常用于随机数、常量的 Stream,或者需要前后元素间维持着某种状态信息的 Stream。把 Supplier 实例传递给 Stream.generate() 生成的 Stream,默认是串行(相对 parallel 而言)但无序的(相对 ordered 而言)。由于它是无限的,在管道中,必须利用 limit 之类的操作限制 Stream 大小。

//生成10个随机数
Random seed = new Random();
Supplier<Integer> random = seed::nextInt;
Stream.generate(random).limit(10).forEach(System.out::println);
//Another way
IntStream.generate(() -> (int) (System.nanoTime() % 100)).
limit(10).forEach(System.out::println);

Stream.generate() 还接受自己实现的 Supplier。例如在构造海量测试数据的时候,用某种自动的规则给每一个变量赋值;或者依据公式计算 Stream 的每个元素值。这些都是维持状态信息的情形。

Stream.generate(new PersonSupplier()).
    limit(10).forEach(p -> System.out.println(p.getName() + ", " + p.getAge()));
private class PersonSupplier implements Supplier<Person> {
     private int index = 0;
     private Random random = new Random();
     @Override
     public Person get() {
        return new Person(index++, "StormTestUser" + index, random.nextInt(100));
     }
}
//结果
StormTestUser1, 9
StormTestUser2, 12
StormTestUser3, 88
StormTestUser4, 51
StormTestUser5, 22
StormTestUser6, 28
StormTestUser7, 81
StormTestUser8, 51
StormTestUser9, 4
StormTestUser10, 76
  1. Stream.iterate用法
    iterate 跟 reduce 操作很像,接受一个种子值,和一个 UnaryOperator(例如 f)。然后种子值成为 Stream 的第一个元素,f(seed) 为第二个,f(f(seed)) 第三个,以此类推
//生成等差数列
Stream.iterate(0, n -> n + 3).limit(10). forEach(x -> System.out.print(x + " "));
//结果
0 3 6 9 12 15 18 21 24 27

Stream.generate 相仿,在 iterate 时候管道必须有 limit 这样的操作来限制 Stream 大小。

用 Collectors 来进行 reduction 操作

java.util.stream.Collectors 类的主要作用就是辅助进行各类有用的 reduction 操作,例如转变输出为 Collection,把 Stream 元素进行归组。
11. groupingBy/partitioningBy 用法

//按照年龄归组
Map<Integer, List<Person>> personGroups = Stream.generate(new PersonSupplier())
    .limit(100).collect(Collectors.groupingBy(Person::getAge));
Iterator it = personGroups.entrySet().iterator();
while (it.hasNext()) {
     Map.Entry<Integer, List<Person>> persons = (Map.Entry) it.next();
     System.out.println("Age " + persons.getKey() + " = " + persons.getValue().size());
}
//上面的 code,首先生成 100 人的信息,然后按照年龄归组,相同年龄的人放到同一个 list 中,如下的输出:
Age 0 = 2
Age 1 = 2
Age 5 = 2
Age 8 = 1
Age 9 = 1
Age 11 = 2
……
Map<Boolean, List<Person>> children = Stream.generate(new PersonSupplier())
    .limit(100).collect(Collectors.partitioningBy(p -> p.getAge() < 18));
System.out.println("Children number: " + children.get(true).size());
System.out.println("Adult number: " + children.get(false).size());
//结果
Children number: 23 
Adult number: 77

总结

Stream 的特性可以归纳为:

  • 不是数据结构
  • 它没有内部存储,它只是用操作管道从 source(数据结构、数组、generator function、IO channel)抓取数据。
  • 它也绝不修改自己所封装的底层数据结构的数据。例如 Stream 的 filter 操作会产生一个不包含被过滤元素的新 Stream,而不是从 source 删除那些元素。
  • 所有 Stream 的操作必须以 lambda 表达式为参数
  • 不支持索引访问
  • 你可以请求第一个元素,但无法请求第二个,第三个,或最后一个。不过请参阅下一项。
  • 很容易生成数组或者 List
  • 惰性化
  • 很多 Stream 操作是向后延迟的,一直到它弄清楚了最后需要多少数据才会开始。
  • Intermediate 操作永远是惰性化的。
  • 并行能力
  • 当一个 Stream 是并行化的,就不需要再写多线程代码,所有对它的操作会自动并行进行的。
  • 可以是无限的
  • 集合有固定大小,Stream 则不必。limit(n) 和 findFirst() 这类的 short-circuiting 操作可以对无限的 Stream 进行运算并很快完成。

小主们,有用的点个赞再走呗,小海在这里谢过诸位啦!

  • 20
    点赞
  • 20
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
Java 中,`stream` 是一个用于处理集合数据的功能强大的工具。它引入了函数式编程的概念,可以让你以一种更简洁、更灵活的方式对集合进行操作。通过使用 `stream`,你可以对集合中的元素进行过滤、映射、排序等操作,以及执行聚合操作。 要使用 `stream`,你需要先将集合转换为一个流。可以通过调用集合对象的 `stream()` 方法来实现。例如,如果你有一个名为 `collection` 的集合,你可以使用以下代码将其转换为一个流: ```java Stream<T> stream = collection.stream(); ``` 其中的 `<T>` 是指集合中元素的类型。 一旦你获得了一个流,就可以使用流的各种方法进行操作。例如,你可以使用 `filter` 方法对流中的元素进行过滤,使用 `map` 方法对元素进行映射,使用 `sorted` 方法对元素进行排序等等。最后,你可以使用 `forEach` 方法来处理流中的每个元素。 以下是一个简单的示例,展示了如何使用 `stream` 进行操作: ```java List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5); List<Integer> evenNumbers = numbers.stream() .filter(n -> n % 2 == 0) .collect(Collectors.toList()); evenNumbers.forEach(System.out::println);``` 这个例子中,我们首先创建了一个包含一些整数的集合 `numbers`。然后,我们将其转换为一个流,并使用 `filter` 方法过滤出其中的偶数。最后,我们将过滤得到的偶数收集到一个新的集合 `evenNumbers` 中,并输出每个偶数。 希望这可以回答你的问题!如果还有其他问题,请随时提问。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值