stream流常用方法_JDK8Stream流

526bfe0f171c89fa711455d125c25517.png

引入

  代码中循环遍历过滤等操作是经常见到的,在说Stream流之前,先看一个例子。

  有一个包含三国时期人物名字的集合,需要做以下操作:

  • 筛选出名字小于三个字的人物。
  • 筛选出曹姓的人物。
  • 打印人物名字。

  在JDK8之前我们可能这样写:

public class StreamIntroduction {

    private List names;@BeforeEachpublic void init() {
        names = new ArrayList<>();
        names.add("曹操");
        names.add("郭嘉");
        names.add("夏侯惇");
        names.add("曹丕");
        names.add("夏侯霸");
        names.add("曹安民");
        names.add("司马师");
        names.add("曹爽");
    }@Testpublic void beforeJdk8() {// 找出名字小于三个字的
        List nameLess = new ArrayList<>();for (String name : names) {if (name.length() 3) {
                nameLess.add(name);
            }
        }
        List nameCao = new ArrayList<>();// 找出出曹姓的人物for (String name : nameLess) {if (name.startsWith("曹")) {
                nameCao.add(name);
            }
        }// 遍历打印for (String name : nameCao) {
            System.out.println(name); // 曹操 曹丕 曹爽
        }
    }
}

  可以很明显的看到,JDK8之前的写法有点臃肿,虽然有点故意夸张了。但是,我们看一下JDK8的Stream流的写法。

@Test
public void dealWithJdk8() {
    names.stream()
        .filter(name -> name.length() 3)
        .filter(name -> name.startsWith("曹"))
        .forEach(System.out::println); // 曹操 曹丕 曹爽
}

  是不是简单明了很多了,直接链式调用达到我们的需求。

  JDK8新引入的Stream流,可以让你以一种声明的方式处理数据。操作集合中的数据可以看成将集合中的数据放在工厂的流水线上,我们对这些数据进行加工,筛选等操作。

Stream流和集合

  Java中的集合专注于如何存储数据元素以进行有效访问。而Stream流专注于对数据源中数据元素的聚合,筛选等操作

  集合是存储所有元素的内存中数据结构。Stream流没有存储空间,流按需从数据源中提取元素,并将其传递到操作管道以进行处理。

  Stream流是单向的,不能重复使用。假如一个流已经被使用了,再次使用的话就会抛出异常。

  以前对集合遍历都是通过Iterator或者For-Each的方式, 显式的在集合外部进行迭代, 这叫做外部迭代。Stream提供了内部迭代的方式, 通过访问者模式(Visitor)实现。

Stream流和IO流没有任何关系。

Stream流的创建

Collection方式

  获得Stream流的方式有很多,其中最常用的就是通过Collection集合来获得。

另外获取并行流的方式有两种:

  • 调用parallelStream方法直接获取并行流。
  • 先获得串行流,再使用parallel方法转换为并行流。
/**
 * Collection获得
 */
@Test
public void getStream06() {
    // List获得流
    List nameList = Arrays.asList("司马昭", "诸葛恪", "张角");
    Stream streamList = nameList.stream();
    streamList.forEach(System.out::println); // [司马昭,诸葛恪,张角]// 1.直接通过Collection获得并行流
    Stream parallelStream1 = nameList.parallelStream();// 2.先获得普通流,再调用parallel方法转换成并行流
    Stream parallelStream2 = nameList.stream().parallel();// Set获得流
    Set nameSet = new HashSet<>(nameList);
    Stream streamSet = nameSet.stream();
    streamSet.forEach(System.out::println); // [司马昭,诸葛恪,张角]
}

Arrays方式

  Arrays工具类有个静态方法可以直接获得Stream流,传入一个数组即可。

  需要注意的是假如传入的是int,double,long这三个基本数据类型,会直接帮我们转成IntStream,DoubleStream,LongStream。

public static  Stream stream(T[] array)

eg.

/**
 * 通过数组获得流 Arrays
 */
@Test
public void getStream08() {
    String[] names = {"司马昭", "诸葛恪", "张角"};
    Stream stringStream = Arrays.stream(names);// 假如我们是普通数据类型呢?只支持int,double,long这三个// 直接帮我们转换成IntStream了int[] idss = {1, 3, 4, 5, 6};
    IntStream intStream = Arrays.stream(idss);
}

Stream的of方法

  Stream的of有两个重载方法。

public static Stream of(T t)public static Stream of(T... values)

eg.

/**
 * 通过Stream的of方法获得
 */
@Test
public void getStream01() {
    Stream stream1 = Stream.of("曹植");
    Stream stream2 = Stream.of("司马昭", "诸葛恪", "张角");// 由于Stream的of方法的参数是可变参,所以也可以传入数组
    String[] names = {"司马昭", "诸葛恪", "张角"};
    Stream stream3 = Stream.of(names);
}

基本类型流的range方法

  包装基本类型的流IntStream和LongStream有两个range方法可以创建流。

  以IntStream的方法为例,range方法和rangeClosed方法的区别是后者的范围包含后面的数字

IntStream range(int startInclusive, int endExclusive)
IntStream rangeClosed(int startInclusive, int endInclusive)

eg.

/**
 * 通过IntStream,LongStream
 */
@Test
public void getStream02() {
    IntStream intStream1 = IntStream.range(1, 6);
    intStream1.forEach(System.out::print); // [1,2,3,4,5]

    IntStream intStream2 = IntStream.rangeClosed(1, 6);
    intStream2.forEach(System.out::print); // [1,2,3,4,5,6]

    LongStream longStream1 = LongStream.range(1, 6);
    longStream1.forEach(System.out::print); // [1,2,3,4,5]

    LongStream longStream2 = LongStream.rangeClosed(1, 6);
    longStream2.forEach(System.out::print); // [1,2,3,4,5,6]
}

Stream的empty方法

  Stream的empty方法可以创建一个空的Stream流

/**
 * IntStream,LongStream,DoubleStream,Stream的empty方法
 */
@Test
public void getStream03() {
    Stream stream = Stream.empty();
    stream.forEach(System.out::print); // []
    IntStream intStream = IntStream.empty();
    LongStream longStream = LongStream.empty();
    DoubleStream doubleStream = DoubleStream.empty();
}

Stream的builder方法

/**
 * IntStream,LongStream,DoubleStream,Stream的builder方法
 */
@Test
public void getStream04() {
    Stream stream = Stream.builder()
        .add("司马师")
        .add("潘凤")
        .add("郭嘉")
        .build();
    stream.forEach(System.out::print); // [司马师,潘凤,郭嘉]
}

创建无限连续的流

  Stream类两个方法可以创建无限连续的流,它们分别是下面两种:

Stream iterate(final T seed, final UnaryOperator f)

  iterate方法可以创建无限连续有序流。它的调用方式其实是seed,f(seed),f(f(seed))f(f(f(seed)))....

  其中iterate方法的UnaryOperator类型的参数其实就是一个转换的函数式接口,继承Function的。

@FunctionalInterface
public interface UnaryOperator<T> extends Function<T, T> {
    
    static  UnaryOperator identity() {return t -> t;
    }
}
Stream generate(Supplier s)

  generate方法可以创建无限连续的无序流,传入一个Supplier对象后续用limit函数限制个数即可。

例子:

/**
 * iterate 创建无限连续有序流
 * generate 创建无限连续的无序流
 */
@Test
public void getStream05() {
    Stream iterateStream =
        Stream.iterate(1, count -> ++count)
          .limit(5);
    iterateStream.forEach(System.out::print); // [1,2,3,4,5]
    Stream generateStream =
        Stream.generate(UUID::randomUUID).limit(2);/*
     * 4391d6c8-faaf-435e-a2ae-9f8cf0f70097
     * 555c7e14-2a2c-4173-acfe-a25505fe2d45
     */
    generateStream.forEach(System.out::println);
}

获得Map相关的Stream流

  获得Map的K-V/Entry的Stream流。

/**
 * Map获得key,value,entry的流
 */
@Test
public void getStream07() {
    Map map = new HashMap();

    Stream keyStream = map.keySet().stream();
    Stream valueStream = map.values().stream();
    Stream entryStream = map.entrySet().stream();
}

通过字符串获得

  String有个方法可以获得字符串每个字符的Stream流

public default IntStream chars()

  正则相关的Pattern类,有个方法可以按照正则表达式从字符串中获得Stream流

public Stream splitAsStream(final CharSequence input)

eg.

/**
 * String
 */
@Test
public void getStream09() {
    String charStr = "ABCDabcd";
    IntStream chars = charStr.chars();

    String namesStr = "司马昭,诸葛恪,张角";
    Stream stringStream = Pattern.compile(",").splitAsStream(namesStr);
}

通过文件获得

  Java 8中的java.io和java.nio.file软件包添加了许多方法来支持使用流的I / O操作。

  我们可以从文件中读取文本作为字符串流,流中的每个元素代表一行文本。

/**
 * files 通过文件获得
 */
@Test
public void getStream10() {
    Path path = Paths.get("src/main/resources/unHappy.txt");
    try(Stream lineStream = Files.lines(path)){
        lineStream.forEach(System.out::println);
    }catch (Exception e){
        e.printStackTrace();
    }
}

  unHappy.txt文件如下,控制台打印的也是下面的样子,因为流中的每个元素代表一行文本。

我不
想加班
又没钱

Stream流的基本操作

Stream类的方法可以分为两类:

  • 非终结方法:返回值是Stream类型的方法,支持链式调用。
  • 终结方法:返回值不是Stream类型的方法,不支持链式调用。如forEach方法。

Stream的注意事项:

  • Stream流只能操作一次,重复使用会报错。
  • Stream方法返回的是新的流。
  • 惰式执行。stream上的操作并不会立即执行,只有等到用户真正需要结果的时候才会执行。
  • Stream不调用终结方法,中间的操作不会执行

forEach

  终结方法forEach:消费方法,传入一个Consumer类型的对象消费Stream流中的元素

void forEach(Consumer super T> action);

eg.

@Test
public void forEachDemo(){
    List names = Arrays.asList("程昱", "荀彧", "郭嘉");
    names.stream()
      .forEach(System.out::println);
}

控制台:

程昱
荀彧
郭嘉

forEachOrder

  终结方法forEachOrdered,该方法和上面的forEach使用方式都一样。

void forEachOrdered(Consumer super T> action);

  其实两者完成的功能类似,主要区别在并行处理上,forEach是并行处理的,forEachOrder是按顺序处理的,显然前者速度更快。下面给个例子:

@Test
public void forEachOrderedDemo(){
    List names = Arrays.asList("程昱", "荀彧", "郭嘉");
    names.parallelStream()
        .forEach(System.out::println);
    names.parallelStream()
        .forEachOrdered(System.out::println);
}
  • forEachOrdered输出的"荀彧,郭嘉,程昱"按顺序来的。
  • forEach的输出顺序并不是确定的,是随机的。

count

  终结方法count,返回流中元素个数

@Test
public void countDemo() {
    List names = Arrays.asList("程昱", "荀彧", "郭嘉");long count = names.stream()
        .count();
    System.out.println("流中元素个数为:" + count);
}

控制台

流中元素个数为:3

filter

  filter过滤方法,通过Predicate对象对Stream流中的元素进行过滤。

Stream filter(Predicate super T> predicate)

eg.

@Test
public void filterDemo() {
    List names = Arrays.asList("程昱", "荀彧", "郭嘉","郭敬明");
    names.stream()
        .filter(name -> name.startsWith("郭"))
        .forEach(System.out::println);
}

limit

  limit方法,需要传入一个整数类型的值,索引大于此值的元素直接丢弃

Stream limit(long maxSize);

eg.

@Test
public void limitDemo() {
    List names = Arrays.asList("程昱", "荀彧", "郭嘉","郭敬明");
    names.stream()
        .limit(3)
        .forEach(System.out::println);
}

控制台:

程昱
荀彧
郭嘉

skip

  skip方法,需要传入一个整数类型的值,跳过前面指定个数的元素

Stream skip(long n);

eg.

@Test
public void skipDemo() {
    List names = Arrays.asList("程昱", "荀彧", "郭嘉","郭敬明");
    names.stream()
        .skip(3)
        .forEach(System.out::println);
}

sorted

  Stream的sorted方法有两个重载的方法,一个是按照自然循序排序,一个可以根据我们传入的Compartor接口来排序

Stream sorted();
Stream sorted(Comparator super T> comparator);

eg.

/**
 * sorted方法
 * 可以根据元素的自然顺序排序
 * 也可以根据比较器指定的规则排序
 */
@Test
public void sortedDemo() {
    List scores = Arrays.asList(50,90,40,65,35);
    scores.stream()
        .sorted() // 根据自然顺序排序
        .forEach(System.out::println);
 System.out.println("========分隔=======");
    List persons = new ArrayList<>();
    persons.add(new Person("汪胖子",190));
    persons.add(new Person("狗东",100));
    persons.add(new Person("郭帅",120));// 根据Person类的体重排序
    persons.stream()
        .sorted(Comparator.comparingInt(Person::getWeight))
        .forEach(System.out::println);
}

控制台

35
40
50
65
90
========分隔=======
Person{name='狗东', weight=100}
Person{name='郭帅', weight=120}
Person{name='汪胖子', weight=190}

distinct

  Stream的distinct方法可以对流中的元素进行去重操作,在对自定义类去重时需要重写hashcode和equals方法。

@Test
public void distinctDemo(){
    List scores = Arrays.asList(50,40,40,60,35);
    scores.stream()
        .distinct()
        .forEach(System.out::println);
    System.out.println("========分隔=======");// 自定义对象去重,需要重写hashcode和equals方法
    List persons = new ArrayList<>();
    persons.add(new Person("汪胖子",190));
    persons.add(new Person("狗东",100));
    persons.add(new Person("郭帅",120));
    persons.add(new Person("郭帅",120));
    persons.stream()
        .distinct()
        .forEach(System.out::println);
}

控制台

50
40
60
35
========分隔=======
Person{name='汪胖子', weight=190}
Person{name='狗东', weight=100}
Person{name='郭帅', weight=120}

match

  针对流中所有数据是否满足某个条件

// 所有元素都满足条件才返回true
boolean allMatch(Predicate super T> predicate);
// 所有的元素都不满足条件才返回true
boolean noneMatch(Predicate super T> predicate);
// 只要有一个元素满足就返回true
boolean anyMatch(Predicate super T> predicate);

eg.

@Test
public void matchDemo(){
    List nums = Arrays.asList(3, 3, 6, 1, 7);// allMatch:需要所有元素都满足才返回trueboolean allMatchBoolean = nums.stream()
        .allMatch(num -> num > 4); // false// anyMatch:只要有一个元素满足就返回trueboolean anyMatchBoolean = nums.stream()
        .anyMatch(num -> num > 4); // true// noneMatch:需要所有的元素都不满足条件才返回trueboolean noneMatchBoolean = nums.stream()
        .noneMatch(num -> num > 4); // false
}

find

  在流中找到一个元素,可以配合条件过滤。Stream流中提供两个方法

// 找到流中的第一个元素
Optional findFirst();
// 获取流中的一个元素
Optional findAny();

  第二个方法和findFirst()的区别是,在数据量较少,串行的情况下,一般会返回第一个结果,如果是并行的情况,那就不能确保是第一个,会随机返回一个元素,这是为了提高并行操作的性能的。

@Test
public void findDemo() {
    List nums = Arrays.asList(9, 3, 6, 1, 7);
    Optional first = nums.stream()
        .filter(num -> num 5)
        .findFirst(); // findFirst方法
    Integer num = first.get();
    System.out.println("第一个小于5的元素为:" + num); // 3// findAny方法在数据量较少,串行地情况下,一般会返回第一个结果// 如果是并行的情况,那就不能确保是第一个
    Optional one = nums.stream()
        .filter(num1 -> num1 > 5)
        .findAny(); // findAny方法
    Integer num1 = one.get();
    System.out.println("普通流第一个大于5的元素为:" + num1); // 9
    Optional parallelOne = nums.parallelStream()
        .filter(num2 -> num2 > 5)
        .findAny(); // findAny方法
    Integer num2 = parallelOne.get();
    System.out.println("并行流第一个大于5的元素为:" + num2); // 6
}

控制台:

第一个小于5的元素为:3
普通流第一个大于5的元素为:9
并行流第一个大于5的元素为:6

max/min

  Stream提供了获取流中最大值和最小值的方法。

Optional max(Comparator super T> comparator);
Optional min(Comparator super T> comparator);

eg.

@Test
public void maxMinDemo() {
    List nums = Arrays.asList(9, 3, 6, 1, 7);
    Optional max = 
        nums.stream().max(Comparator.comparingInt(num -> num));
    System.out.println("流中的最大值为:" + max.get());
    Optional min = 
        nums.stream().min(Comparator.comparingInt(num -> num));
    System.out.println("流中的最小值为:" + min.get());
}

控制台:

流中的最大值为:9
流中的最小值为:1

reduce

  此reduce方法可以对流中的所有数据进行归纳操作,归纳为一个数

重载一

T reduce(T identity, BinaryOperator accumulator);

  这个方法其实和下面的代码等价

T result = identity;
for (T element : this stream)
    result = accumulator.apply(result, element);
return result;

  例如,我们可以做求和运算。

@Test
public void reduceDemo01(){
    // 可以使用reduce方法进行求和运算
    List nums = Arrays.asList(9, 3, 6, 1, 7);
    Integer sum = nums.stream()
        .reduce(0, (a, b) -> a + b);
    System.out.println(sum); // 26
}

重载二

Optional reduce(BinaryOperator accumulator);

  这个方法和下面的代码等价,很简单,和上面的reduce方法的区别就是起始值是第一个元素

boolean foundAny = false;
T result = null;
for (T element : this stream) {
    if (!foundAny) {
        foundAny = true;
        result = element;
    }
    else
        result = accumulator.apply(result, element);
}
return foundAny ? Optional.of(result) : Optional.empty();

重载三

 U reduce(U identity,
             BiFunctionsuper T, U> accumulator,
             BinaryOperator combiner);

分析三个参数:

  1. 第一个参数identity:一个初始化的值这个初始化的值其类型是泛型U,与Reduce方法返回的类型一致,此时Stream中元素的类型是T,与U可以不一样也可以一样。
  2. 第二个参数accumulator:BiFunction类型,输入是U与T两个类型的数据,而返回的是U类型。
  3. 第三个参数combiner:使用在并行计算的场景下,如果Stream是串行流时,第三个参数实际上是不生效的。也就是说在串行的情况下随便写都可以,主要是为了编译通过。

  这个reduce方法,提供一个不同于Stream中数据类型的初始值,通过累加器规则迭代计算Stream中的数据,最终得到一个同初始值同类型的结果。当然U类型和T类型也可以是一样的。等价于:

U result = identity;
for (T element : this stream)
    result = accumulator.apply(result, element);
return result;

看一个例子:

@Test
public void reduceDemo02() {
    // 例子: 计算所有人名字的总长度
    List names = Arrays.asList("张辽", "徐晃", "司马师");// 我初始值给的是0,是整数类型,并不是集合的String类型// 但是最终返回的是整数类型
    Integer sum = names.stream()
        .reduce(0,
               (length, name) -> length + name.length(),
               (a, b) -> null);
    System.out.println("名字的总长度为6:" + sum);
}

  需要注意的是,在上面的例子中的Stream流是串行流,并不是并行流,所以第三个参数其实是无效的,上面第三个参数combiner写的是(a, b) -> null,只是为了通过编译。

  并行流的reduce操作是并发进行的,为了避免竞争 ,每个reduce线程都会有独立的计算结果,combiner的作用在于合并每个线程的结果得到最终结果。并行流运行时内部使用了fork-join框架,可以自己去看看。

  好,那就看看第三个参数有什么作用。继续看一个例子:

使串行流

@Test
public void reduceDemo03() {
    List nums = Arrays.asList(5, 4, 3, 2, 1);
    Integer calc = nums.stream()
        .reduce(1,
                (a, b) -> {
                    System.out.println("BiFunction a = " + a
                                       + ", b = " + b
                                       + " , a + b = " + (a + b));return a + b;
                },
                (a, b) -> {
                    System.out.println("BinaryOperator a = " + a
                                       + ", b = " + b
                                       + " , a + b = " + (a + b));return a + b;
                });
    System.out.println("计算结果为:" + calc);
}

  计算结果是20,打印下计算流程:其实就是(1+5)+4+3+2+1)=16

BiFunction a = 1, b = 5 , a + b = 6
BiFunction a = 6, b = 4 , a + b = 10
BiFunction a = 10, b = 3 , a + b = 13
BiFunction a = 13, b = 2 , a + b = 15
BiFunction a = 15, b = 1 , a + b = 16
计算结果为:16

使用并行流

@Test
public void reduceDemo04() {
    List nums = Arrays.asList(5, 4, 3, 2, 1);
    Integer calc = nums.parallelStream()
        .reduce(1,
                (a, b) -> {
                    System.out.println("BiFunction a = " + a
                                       + ", b = " + b
                                       + " , a + b = " + (a + b));return a + b;
                },
                (a, b) -> {
                    System.out.println("BinaryOperator a = " + a
                                       + ", b = " + b
                                       + " , a + b = " + (a + b));return a + b;
                });
    System.out.println("计算结果为:" + calc);
}

  计算结果是20,打印下计算流程:其实就是(1+5)+(1+4)+(1+3)+(1+2)+(1+1)=20

BiFunction a = 1, b = 3 , a + b = 4
BiFunction a = 1, b = 4 , a + b = 5
BiFunction a = 1, b = 1 , a + b = 2
BiFunction a = 1, b = 5 , a + b = 6
BiFunction a = 1, b = 2 , a + b = 3
BinaryOperator a = 6, b = 5 , a + b = 11
BinaryOperator a = 3, b = 2 , a + b = 5
BinaryOperator a = 4, b = 5 , a + b = 9
BinaryOperator a = 11, b = 9 , a + b = 20
计算结果为:20

  那么问题来了,并行和普通流的计算结果不同,是不是这个设计就没意义了呢?设计是没问题的,是我们使用方式的问题。

  API中有下面的话,

/*
 *

The {@code identity} value must be an identity for the combiner
 * function.  This means that for all {@code u}, {@code combiner(identity, u)}
 * is equal to {@code u}.  Additionally, the {@code combiner} function
 * must be compatible with the {@code accumulator} function; for all
 * {@code u} and {@code t}, the following must hold:
 * 

{@code
 *     combiner.apply(u, accumulator.apply(identity, t)) == accumulator.apply(u, t)
 * }

 */

  对于所有的ucombiner(identity, u)一定相等,即identity 的值等于合并运算combiner(identity, u),对于上面的例子,combiner方法的方法体是identity+u,要使identity + u == u ,我们只需要将identity设置为0即可。

  而且,对于combiner方法和accumulator方法,需要满足下面条件

combiner.apply(u, accumulator.apply(identity, t)) == accumulator.apply(u, t)

  这到底是什么意思呢?假如流中有三个元素需要进行reduce操作,假如前面两个元素已经计算完毕,即将对第三个元素进行计算。这里分串行和并行进行分析。

串行时

65095c0a61e31f39ea38bfdf45c60c2c.png

r ed uce串 行时

并行时:

faba65fed4842493e12ee67faf7bfa54.png

reduce并行时

  在串行时,最终的结果是通过 accumulator.(apply(r, t))算出。

  在并行时,是通过combiner.apply(u, accumlator.apply(identity, t))

  正是因为这样,需要保证串行和并行的时候计算的值相同,则需要使得上面两个式子等效,即

combiner.apply(u, accumulator.apply(identity, t)) == accumulator.apply(r, t)

map

  map方法通过传入一个Function转换函数,返回一个新得流。

 Stream map(Function super T, ? extends R> mapper);

eg.

@Test
public void mapDemo(){
    // 将persons里的Person对象的体重转换出一个新的流
    List persons = new ArrayList<>();
    persons.add(new Person("汪胖子", 190));
    persons.add(new Person("狗东", 100));
    persons.add(new Person("郭帅", 120));
    persons.stream()
        .map(person -> person.getWeight())
        .forEach(System.out::println);
}

  map还可以和reduce方法结合灵活使用

@Test
public void mapDemo02() {
    // 计算出所有人体重的和
    List persons = new ArrayList<>();
    persons.add(new Person("汪胖子", 190));
    persons.add(new Person("狗东", 100));
    persons.add(new Person("郭帅", 120));
    Integer sum = persons.stream()
        .map(person -> person.getWeight())
        .reduce(0, (x, y) -> x + y);
    System.out.println("所有人体重之和为:" + sum); // 410
}

  还有其他的map操作,可以将Integer等类型的流转换成基本数据类型的流。这样可以节省内存,减少自动装箱和自动拆箱。

IntStream mapToInt(ToIntFunction super T> mapper);
LongStream mapToLong(ToLongFunction super T> mapper);
DoubleStream mapToDouble(ToDoubleFunction super T> mapper);

flatMap

  flatMap方法可以将一个流中的数据转换成更细的流,再将这些流合并成一个流。方法声明:

 Stream flatMap(Function super T, ? extends Stream extends R>> mapper);

eg.

@Test
public void flatMapDemo(){
    List words = Arrays.asList("hello,world", "hola,mundo", "你好,世界");
    words.stream()
        .flatMap(word -> Arrays.stream(word.split(",")))
        .forEach(System.out::println);
}

控制台:

hello
world
hola
mundo
你好
世界

类似map方法,flatMap也有转换基本数据类型的方法

IntStream flatMapToInt(Function super T, ? extends IntStream> mapper);
LongStream flatMapToLong(Function super T, ? extends LongStream> mapper);
DoubleStream flatMapToDouble(Function super T, ? extends DoubleStream> mapper);

map和flatMap配合使用

map方法声明

 Stream map(Function super T, ? extends R> mapper);

  可以很明显的看到flatMap方法和map方法不同的地方就是参数的泛型不同。

  map方法的Function函数的返回值是R类型,map只是提取属性放入流中。

  而flatMap方法的Function函数的返回值是Stream extends R>类型。flatMap 先提取属性放入一个比较小的流,然后再将所有的流合并为一个流。叠罗汉。

map和flatMap结合使用:需要计算学校中每个人的体重总和

@Test
public void flatMapAndMapDemo() {
    // 班级1
    List classOne = new ArrayList<>();
    classOne.add(new Person("玉泽", 120));
    classOne.add(new Person("海露", 95));// 班级2
    List classTwo = new ArrayList<>();
    classTwo.add(new Person("今译", 130));
    classTwo.add(new Person("导读", 110));// 学校
    List> school = new ArrayList<>();
    school.add(classOne);
    school.add(classTwo);// 数据准备好了,现在获取所有人的体重之和
    Integer sum = school.stream()
        .flatMap(className -> className.stream())
        .map(Person::getWeight)
        .reduce(0, Integer::sum);
    System.out.println("学校所有人的体重和为:" + sum); // 455
}

concat

  concat方法的 作用就是将两个流合并成一个流。它是一个静态方法。

static  Stream concat(Stream extends T> a,
                            Stream extends T> b)

eg.

@Test
public void concatDemo(){
    Stream streamA = Stream.of("田七");
    Stream streamB = Stream.of("王八");// 合并流
    Stream concat = Stream.concat(streamA, streamB);
}

peek

  peek方法可以用来调试Stream流,可以看到每次操作后流中的数据

Stream peek(Consumer super T> action);

eg.

@Test
public void peekDemo() {
    Stream stream = Stream.of("太史慈","陆逊","吕蒙","诸葛子瑜","甘宁");long count = stream.filter(name -> name.length() > 2)
        .peek(System.out::println) // 打印名字长度大于2的名字
        .map(String::length)
        .peek(System.out::println) // 打印名字长度
        .map(length -> length * length)
        .peek(System.out::println) // 打印名字长度的平方
        .count();
}

控制台:

太史慈
3
9
诸葛子瑜
4
16

Stream流的收集操作

Stream流基本收集操作

  Stream流中的元素可以收集到指定的集合中,我们可以直接调用方法收集到指定的List、Set等集合中,进而继续后面的业务操作。

  **API中已经给我们提供了直接收集到List、Set、Map和ConcurrentHashMap的方法,也可以指定其他的集合类型。**具体可以去源码查看方法声明。

收集为List集合

@Test
public void collect01() {
    Stream stream = Stream.of("初", "九", "潜", "龙", "勿", "用");
    List stringList = stream.collect(Collectors.toList());
    System.out.println("收集后的list集合:" + stringList);
}

收集为Map集合

@Test
public void collect03() {
    Stream stream = Stream.of("九二", "现龙在田","利见大人");// 转换为Map集合,以字符串为key,字符串长度为value
    Map map = 
        stream.collect(Collectors.toMap(str -> str, String::length));
    System.out.println("收集后的map集合:" + map);
}

收集为自定义集合

@Test
public void collect04() {
    Stream stream = Stream.of("九三", "君子终日乾乾","夕惕若","厉无咎");
    LinkedHashSet stringSet =
        stream.collect(Collectors.toCollection(LinkedHashSet::new));
    System.out.println("收集后的LinkedHashSet集合:" + stringSet);
}

收集为数组

  收集为数组的时候,可以指定数组的类型

/**
 * 收集为数组 没有指定类型
 */
@Test
public void collect05() {
    Stream stream = Stream.of("九四", "或跃在渊", "无咎");
    Object[] objects = stream.toArray();
    System.out.println("收集后的Object类型数组:" + Arrays.toString(objects));
}/**
 * 收集为数组 转换指定类型数组
 */@Testpublic void collect06() {
    Stream stream = Stream.of("九五", "飞龙在天", "利见大人");
    String[] strings = stream.toArray(String[]::new);
    System.out.println("收集后的String类型数组:" + Arrays.toString(strings));
}

Stream流的聚合操作

  我们可以对Stream流中的某些字段像MySQL一样进行聚合操作,例如取最值,平均值等。

  在对Stream流操作完后调用collect方法进行聚合操作,需要传入Collector对象,我们可以通过Collectors类的静态方法完成聚合操作。方法如下:

// 统计元素个数
public static  Collector counting()// 最小值public static  Collector> minBy(Comparator super T> comparator)// 最大值public static  Collector> maxBy(Comparator super T> comparator)// 求和public static  Collector summingInt(ToIntFunction super T> mapper) public static  Collector summingLong(ToLongFunction super T> mapper) public static  Collector summingDouble(ToDoubleFunction super T> mapper) // 平均值public static  Collector averagingInt(ToIntFunction super T> mapper) public static  Collector averagingLong(ToLongFunction super T> mapper) public static  Collector averagingDouble(ToDoubleFunction super T> mapper) 

  先初始化一个流,Person是一个对象,属性为名字和体重。

private Stream stream;/**
 * 初始化
 */@BeforeEachpublic void init() {
    stream = Stream.of(new Person("汪汪汪", 210),new Person("狗东", 105),new Person("杨文浡", 120)
    );
}

最值

/**
 * 聚合操作-最大
 */
@Test
public void aggregationMax() {
    // 获取体重最大的对象
    Optional maxWeightPerson =
        stream.collect(Collectors.maxBy(Comparator.comparingInt(Person::getWeight)));
    System.out.println("最大:" + maxWeightPerson.get());
}/**
 * 聚合操作-最小
 */@Testpublic void aggregationMin() {// 获取体重最小的对象
    Optional minWeightPerson =
        stream.collect(Collectors.minBy(Comparator.comparingInt(Person::getWeight)));
    System.out.println("最小:" + minWeightPerson.get());
}

求和

/**
 * 聚合操作-总和
 */
@Test
public void aggregationSum() {
    // 获取体重总和
    Integer sumWeight = stream.collect(Collectors.summingInt(Person::getWeight));
    System.out.println("总值:" + sumWeight);
}

统计元素个数

/**
 * 聚合操作-统计个数
 */
@Test
public void aggregationCount() {
    // 获取流中元素个数
    Long count = stream.collect(Collectors.counting());
    System.out.println("元素个数:" + count);
}

求平均值

/**
 * 聚合操作-平均值
 */
@Test
public void aggregationAvg() {
    // 获取体重平均值
    Double avgWeight = stream.collect(Collectors.averagingInt(Person::getWeight));
    System.out.println("平均值:" + avgWeight);
}a

Stream流的分组操作

  同样的Stream流也有分组操作。方法声明如下:

// 传入传入的Function进行分组
public static  Collector>>
    groupingBy(Function super T, ? extends K> classifier);// 传入传入的Function和Collector可以进行多级分组public static  Collector> 
    groupingBy(Function super T, ? extends K> classifier,
               Collector super T, A, D> downstream); // 可以通过Supplier函数指定返回的类型public static > Collector  
    groupingBy(Function super T, ? extends K> classifier,
           Supplier mapFactory,
           Collector super T, A, D> downstream);

  初始化数据:

private Stream stream;/**
 * 初始化
 */@BeforeEachpublic void init() {
    stream = Stream.of(new Person("汪汪汪", 210),new Person("狗东", 210),new Person("杨文浡", 120),new Person("阿文", 150)
    );
}

分组

  通过体重来进行分组操作。

/**
 * 通过字段分组
 */
@Test
public void groupByField01() {
    Map> map =
        stream.collect(Collectors.groupingBy(Person::getWeight));
    map.forEach((k, v) -> System.out.println(k + ":" + v));
}

控制台:

210:[Person{name='汪汪汪', weight=210}, Person{name='狗东', weight=210}]
150:[Person{name='阿文', weight=150}]
120:[Person{name='杨文浡', weight=120}]

多级分组

  先通过名字长度进行分组,再通过体重进行多级分组。

/**
 * 通过字段分组 多级分组
 */
@Test
public void groupByField03() {
    // 先通过名字的长度进行分组,再用体重进行分组
    Map>> map = stream.collect(
        Collectors.groupingBy(person -> person.getName().length(),
            Collectors.groupingBy(person -> {if (person.getWeight() > 200) {return "肥胖";
                } else {return "正常";
                }
            })));
    map.forEach((dk, dv) ->{
        System.out.println(dk);
        dv.forEach( (k,v) -> {
            System.out.println("\t" + k +" : "+v);
        });
    });
}

控制台:

2
 肥胖 : [Person{name='狗东', weight=210}]
 正常 : [Person{name='阿文', weight=150}]
3
 肥胖 : [Person{name='汪汪汪', weight=210}]
 正常 : [Person{name='杨文浡', weight=120}]

分组返回指定对象

/**
 * 返回指定对象,即用指定类型接收
 */
@Test
public void groupReturn() {
    TreeMap> treeMap = stream.collect(Collectors.groupingBy(
        Person::getWeight,
        TreeMap::new,
        Collectors.toList()));
    treeMap.forEach((dk, dv) -> {
        System.out.println(dk + ":");
        dv.forEach(v -> System.out.println("\t" + v));
    });
}

Stream流的分区操作

  可以使用Collectors.partitioningBy方法来给Stream中的数据进行分区,根据传入的Predicate对象过滤,将true和false的数据分区。

方法声明:

// 可以通过Predicate作为条件进行分区
public static  Collector>> 
    partitioningBy(Predicate super T> predicate);// 可以通过Predicate作为条件进行分区// 第二个参数传入一个收集器Collector对象可以对分区后的数据再次处理public static  Collector> 
    partitioningBy(Predicate super T> predicate,
                   Collector super T, A, D> downstream)

初始化数据:

private Stream stream;/**
 * 初始化
 */@BeforeEachpublic void init() {
    stream = Stream.of(new Person("汪汪汪", 210),new Person("狗东", 210),new Person("杨文浡", 120),new Person("阿文", 150)
    );
}

普通分区

@Test
public void partition() {
    // 通过体重来进行分区
    Map> partitionMap =
            stream.collect(Collectors.partitioningBy(person ->
                person.getWeight() >= 180
            ));// 遍历打印
    partitionMap.forEach((k, v) -> {
        System.out.println(k + "==" + v);
    });
}

控制台:

false==[Person{name='杨文浡', weight=120}, Person{name='阿文', weight=150}]
true==[Person{name='汪汪汪', weight=210}, Person{name='狗东', weight=210}]

高级分区

  可以通过第二个参数传入一个收集器Collector对象进行高级分区。

@Test
public void partition02() {
    // 通过体重来进行分区
    Map>> partitionMap =
        stream.collect(Collectors.partitioningBy(person -> person.getWeight() >= 180,
                Collectors.groupingBy(person -> {int length = person.getName().length();if (length > 2) {return "大于2个字";
                    }return "小于等于2个字";
                })));                                         // 遍历打印
    partitionMap.forEach((dk, dv) ->{
        System.out.println(dk);
        dv.forEach( (k,v) -> {
            System.out.println("\t" + k +" : "+v);
        });
    });
}

控制台:

false
 小于等于2个字 : [Person{name='阿文', weight=150}]
 大于2个字 : [Person{name='杨文浡', weight=120}]
true
 小于等于2个字 : [Person{name='狗东', weight=210}]
 大于2个字 : [Person{name='汪汪汪', weight=210}]

Stream流的拼接操作

方法声明:

// 直接拼接 没有分隔符
public static Collector joining() // 指定分隔符 拼接public static Collector joining(CharSequence delimiter) // 指定分隔符 带前缀和后缀 拼接public static Collector joining(CharSequence delimiter,
                                                         CharSequence prefix,
                                                         CharSequence suffix) 

初始化数据:

private Stream stream;/**
 * 初始化
 */@BeforeEachpublic void init() {
    stream = Stream.of(new Person("汪汪汪", 210),new Person("狗东", 210),new Person("杨文浡", 120),new Person("阿文", 150)
    );
}

拼接并以分隔符分割

@Test
public void joining01(){
    // 拼接流中元素,以分隔符分割
    String joinStr = stream.map(Person::getName)
        .collect(Collectors.joining(","));
    System.out.println(joinStr);
}

控制台:

汪汪汪,狗东,杨文浡,阿文

拼接带前后缀

@Test
public void joining02(){
    // 拼接流中元素,以分隔符分割,有前缀和后缀
    String joinStr = stream.map(Person::getName)
        .collect(Collectors.joining(",","qAq","bAb"));
    System.out.println(joinStr);
}

控制台:

qAq汪汪汪,狗东,杨文浡,阿文bAb

性能比较

  关于串行流、并行流和传统for循环的性能对比,可以查看下面这篇文章。

  Stream Performance

https://github.com/CarpenterLee/JavaLambdaInternals/blob/master/8-Stream%20Performance.md

  嗯,强者搞的。

18b75707e769b17fdee038d67c03d4bb.png

小结

  此处只是讲了一些Stream流的API和一些简单的操作,还有未涉及到的地方,大家可以直接去查看Stream流的API。

        在适当的情况下使用Stream流操作效果非常好,但是由于Stream流的设计原因,在简单的场景下,传统的for循环可能更加适合,而且并行流在不了解原理的情况下尽量不要使用,避免出现问题。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值