Stream实战

1、Stream(流)是什么?

  • Stream即流,在Java 8中引入,它与java.io包里的InputStream与OutputStream是完全不同的概念。简单来讲流是一个包含着一系列数据项的集合,但它又不是一个存储容器,更像是一个与数据项相关的算法容器。
  • Scala和Groovy证明了,函数作为一等公民可以大大扩充程序员的工具库。在Stream中也使用了函数式编程的思想,极大地提高和解放了生产力。熟练使用Stream强大的功能,你能写出更加简洁、表现力更强的代码,从此告别996。

2、 Stream 的构造

    1. 通过集合的stream()或者parallelStream();
    1. 流的静态方法,如Stream.of();
    1. 随机数流Random.ints();
    1. 包装类型在Stream中常用,但是其实基本类型也能使用Stream。如定义基本类型的Stream,可以使用IntStream,LongStream,DoubleStream的接口静态方法of,range,empty
    1. 从文件等输入流中构造
//1.集合中构造
	Arrays.asList(1,2,3,4,5).stream()...;

//2.静态构造
	Stream.of(1,2,3,4,5)...
 
//3.随机数流
	//IntStream
 	new Random(100).ints();
	IntStream.of(1,2,3);
  IntStream.range(1,100);
	//LongStream
  new Random(100).longs();
	//DoubleStream
	new Random(100).doubles();

//4.IntStream/LongStream/DoubleStream
//也可以使用Stream<Integer>、Stream<Long>、Stream<Double>构造
	IntStream.of(new int[]{1,2,3});
	
//5.文件输入构造
//通常Stream不需要关闭,仅仅是需要关闭在IO通道上运行的流

	try(final Stream<String> lines=Files.lines(Paths.get("somePath"))){
    lines.forEach(System.out::println);
	}
复制代码

3、 Stream工作流程

  • Stream类似于一个迭代器,可以对流中每个元素迭代处理。串行化的处理与迭代器(Itrerator)类似,但是Stream的功能远不止迭代这么简单。

  • 其中的中间操作、终端操作。通常小伙伴在stream处理中一顿操作后,发现IDE爆红,经常不太明白什么原因,很多情况下是不明白中间操作与终端操作。

  • 简单来讲,中间操作执行后会返回一个流,类似于builder设计模式中build()后通常会有return this这么一个操作,中间操作返回了一个处理流从而提供了链式调用语法。而终端操作就是一个收尾操作,一般返回void或者非stream结果。如我们常用的toList()、toSet()、toMap()、toArray就是非stream结果,只有副作用的 foreach() 也是void。

  • map、flatmap、filter、peek、limit、skip、distinct、sorted...都是中间操作,foreach、forEachOrdered、collect、findFirst、min、max则是终端操作。

3.1、中间操作

3.1.1、map

  • 作用于stream中的每个元素
//下面两种等价的方式,完成将字符串转大写并排序
//1.函数式接口方式
	()->stream.of("apple","banana","orange","grapes", "melon","blueberry","blackberry")
	          .map(String::toUpperCase).sorted();

//2.Lambda表达式方式
	()->stream.of("apple","banana","orange","grapes", "melon","blueberry","blackberry")
	          .map(v->v.toUpperCase()).sorted();
复制代码

3.1.2、flatmap

  • 与map类似,都是一个函数作用于stream中的每个元素。

  • 从函数签名可以看出:map返回值是一个对象,对象形成了一个新的Stream。而flatmap返回的是一个Stream。flatmap不会在创建一个新Stream,而是将原来元素转换为Stream。通常用于流的扁平化处理

    //map()签名
    	<R> Stream<R> map(Function<? super T, ? extends R> mapper);
    
    //flatmap()签名
    	<R> Stream<R> flatMap(Function<? super T, ? extends Stream<? extends R>> mapper);
    
    //flatmap返回Stream
    	Stream.of(1,22,33).flatMap(v->Stream.of(v*v)).collect(Collectors.toList());
    
    //map返回对象
    	Stream.of(1,22,33).map(v->v*v).collect(Collectors.toList());
    
    //flatMap的扁平化处理
    
      List<Map<String, String>> list = new ArrayList<>();
      Map<String,String> map1 = new HashMap();
      map1.put("1", "one");
      map1.put("2", "two");
    
      Map<String,String> map2 = new HashMap();
      map2.put("3", "three");
      map2.put("4", "four");
      list.add(map1);
      list.add(map2);
    
      Set<String> output= list.stream()  //  Stream<Map<String, String>>
          .map(Map::values)              // Stream<List<String>>
          .flatMap(Collection::stream)   // Stream<String>
          .collect(Collectors.toSet());  //Set<String>
      [one, two, three,four]
    
    复制代码

3.1.3、peek

  • peek也是对流中的每一个元素进行操作,除了生成一个包含原所有元素的新Stream,还提供一个Consumer消费函数。

  • 与map对比可看出peek在流处理中,可以做一些输出、外部处理、副作用等无返回值。生成一个包含原Stream的所有元素的新Stream,新Stream每个元素在被消费之前都会执行peek给定的消费函数;

    //对每一个元素进行一些副作用
    	List<Integer> list = new ArrayList();
    	List<Integer> result = Stream.of(1, 2, 3, 4)
           .peek(x -> list.add(x))
           .map(x -> x * 2)
           .collect(Collectors.toList());
    
    //1
    //2
    //3
    //[1, 2, 3]
     System.out.println(list);
    
    
    //map()签名,返回值R
    	<R> Stream<R> map(Function<? super T, ? extends R> mapper);
    //peek()签名,返回值void
    	Stream<T> peek(Consumer<? super T> action); 
    复制代码

3.1.4、filter

  • 设置过滤条件,满足filter的元素汇总生成一个新的stream
//筛选出 >0 的元素
	Arrays.asList(1,2,3,4,5)
    		.stream()
    		.filter(v-> v>0)
    		.toArray(Integer[]::new);

//筛选出字母A开头的字符串
	Stream.of("apple","banana","orange","grapes", "melon","blueberry","blackberry")
			  .filter(s->s.startWith("A"))
  		  .forEach(System.out::println)
复制代码

3.1.5、limit/skip

  • limit返回Stream前面几个元素,skip扔掉前面几个元素。

3.1.6、distinct

  • 简单stream的去重

    //去重
    Stream.of(1,2,3,3,3,2,4,5,6)
      .distinct()
      .collect(Collectors.toSet());
    复制代码

3.2、终端操作

3.2.1、findFirst

  • 它总是返回 Stream 的第一个元素,或者空。注意返回值是Optional。

  • Optional可能含有值,也可能不含,主要是尽可能避免NPE。

	Optional<String> ops = Stream.of("apple","banana","orange","blueberry","blackberry")
															 .filter(s->s.startsWith("b"))
		       										 .findFirst();
//banana
	ops.orElse("apple");

	Optional<String> ops = Stream.of("apple","banana","orange","blueberry","blackberry")
															 .filter(s->s.startsWith("c"))
															 .findFirst();
//apple
	ops.orElse("apple");
复制代码

3.2.2、强大的collect

也许很多人经常搞不清Collector、Collection、Collections、Collectors。
1.Collection是Java集合祖先接口;
2.Collections是java.util包下的一个工具,内含有各种处理集合的静态方法。
3.java.util.stream.Stream#collect(java.util.stream.Collector)是Stream的一个函数,负责收集流。
4.java.util.stream.Collector是一个收集函数的接口,声明一个收集器功能。
5.java.util.Collectors是一收集器的工具类,内置了一系列常用收集器的实现,如Collectors.toList()/toSet(),作为上述第3条的collect()参数。
  • toList/toMap

    //toList
    	//方式1
    	List<String> list = Stream.of("I","love","you","too")
      								.collect(ArrayList::new,ArrayList::add,ArrayList::addAll);
    	//方式2
    	List<String> list = stream.collect(Collections.toList())
             
    //toMap   
      Map<Integer, Integer> collect1 = Stream.of(1, 3, 4)
    															    .collect(Collectors.toMap(x -> x, x -> x + 1));
        
    复制代码
  • 数组转List

    List list = new ArrayList<>(Arrays.asList("a", "b", "c"));
    Integer [] myArray = { 1, 2, 3 };
    List myList = Arrays.stream(myArray).collect(Collectors.toList());
    //基本类型也可以实现转换(依赖boxed的装箱操作)
    int [] myArray2 = { 1, 2, 3 };
    List myList = Arrays.stream(myArray2).boxed().collect(Collectors.toList());
    复制代码
  • 也许collect是我们最常用的一个终端操作,toList、toSet、toMap一气呵成。但是如果看collect函数签名会发现,这个collect不简单。

//collect1
	<R> R collect(
            Supplier<R> supplier,
            BiConsumer<R,? super T> accumulator,
            BiConsumer<R,R> combiner)

//collect2
/**
    @param1: supplier为结果存放容器
    @param2: accumulator为结果如何添加到容器的操作
    @param3: combiner则为多个容器的聚合策略
*/
<R,A> R collect(collector<? super T,A,R> collector);

复制代码
  • 从函数签名中可看出其实我们只用到了collect的第二个方法,即使用jdk提供的Collector的toList、toSet、toMap。正在因为这些操作常用,故jdk中直接提供了这些collector。

  • 我们也可以实现自己的Collector

    /*
    
    	T:流中要收集的对象的泛型
    	A:累加器的类型,累加器是在收集过程中用于累加部分结果的对象
    	R:收集操作得到的对象(通常但不一定是集合)的类型。
    */
    public interface Collector<T,A,R> {
    
    	//结果容器
    		Supplier<A> supplier();
    
    	//累加器执行累加的具体实现
        BiConsumer<A, T> accumulator();
    
        //合并2个结果的容器
        BinaryOperator<A> combiner();
    
        //对结果容器应用最终转换finisher
        Function<A, R> finisher();
    
        //characteristics
        Set<Characteristics> characteristics();
    }
    
    
    //自定义Collector
    	//1.建立新的结果容器supplier(),返回值必须是一个空Supplier,供数据收集过程使用
    
        //toList返回一个空List<>,toSet、toMap类似
          @Override
          public Supplier<List<T>> supplier() {
              return ArrayList::new;
          }
    
    	//2.累加器执行累加的具体实现accumulator()
    		//从BiConsumer看出返回值void,接受2个参数第一个是累计值,第二个是当期处理的第n个元素
          @Override
        	public BiConsumer<List<T>, T> accumulator() {
            //可以看出是个拼接list的操作
            return List::add;
        	}
    	
    	//3.转换最终结果容器的finisher()
    		//流遍历完成后,有时需要对结果处理,可以借助finisher。finisher()须返回累加过程中的最后一个调用			函数,用于将累加器对象转换为集合。	
    		//接收2个参数,第一个参数是累加值,第二个参数是返回值,返回值就是我们最终要的东西。
    
    		@Override
    			public Function<List<T>, List<T>> finisher() {
            //原样输出不额外处理
        		return (i) -> i;
    			}
    		//其实就是Function.identity()
    
    	//4.合并容器的combiner()
    		//Stream支持并行操作,但并行的子部分处理规约如何处理?combiner()就是指明了各个子任务如何合并。
    		@Override
    			public BinaryOperator<List<T>> combiner() {	
        	
            //每个子任务是一个List,2个子任务结果合并累加到第一个子任务上
            return (list1, list2) -> {
            			list1.addAll(list2);
    	        return list1;
        		};
    			}
    
    	//5.characteristics()
    	
    复制代码
  • 字符串的toList的Collector实现,核心是理解T、A、R

public class MyCollector<String> implements Collector<String, List<String>, List<String>> {
    @Override
    public Supplier<List<String>> supplier() {
      return ArrayList::new;
    }

    @Override
    public BiConsumer<List<String>, String> accumulator() {
      return (List<String> l, String s) -> {
        l.add(s);
      };
    }

    @Override
    public BinaryOperator<List<String>> combiner() {
      return (List<String> l, List<String> r) -> {
        List<String> list = new ArrayList<>(l);
        list.addAll(r);
        return list;
      };
    }

    @Override
    public Function<List<String>, List<String>> finisher() {
      return Function.identity();
    }

    @Override
    public Set<Characteristics> characteristics() {
      return Collections.unmodifiableSet(EnumSet.of(Characteristics.IDENTITY_FINISH));
    }
  }



  Stream<String> apple = Stream
        .of("apple","banana", "orange", "grapes", "melon", "blueberry", "blackberry");

  System.out.println(apple.collect(new MyCollector<>()));



//字符串的拼接concat

String concat = Stream
        .of("apple", "apple","banana", "orange", "grapes", "melon", "berry", 	"blary")
  			.collect(
  							StringBuilder::new,
  							StringBuilder::append,
  							StringBuilder::append)
  			.toString();
//等价于上面,这样看起来应该更加清晰
String concat = stringStream.collect(() -> new StringBuilder(),(l, x) -> l.append(x), (r1, r2) -> r1.append(r2)).toString();

复制代码

3.2.3、reduce操作

  • 这个方法的主要作用是把stream元素组合起来。它提供一个初始值(种子),然后将运算规则(BinaryOperator),和前面Stream的第一个、第二个、第n个元素组合。从这个意义上来讲,字符串拼接、数组的sum、min、max、average都是特殊的reduce。
Integer sum = integers.reduce(0,(a,b)->a+b);
或者
Integer sum = integers.reduce(0,Integer::sum);

复制代码
  • 没有起始值的情况下,会把Stream前面2个元素组合起来,返回的是Optional( 由于没有起始的reduce(),故可能没有足够的元素,因此设计返回的是Optional)
//原生操作
final Integer[] integers = Lists.newArrayList(1, 2, 3, 4, 5)
        .stream()
        .collect(() -> new Integer[]{0}, (a, x) -> a[0] += x, (a1, a2) -> a1[0] += a2[0]);
//reducing操作
final Integer collect = Lists.newArrayList(1, 2, 3, 4, 5)
        .stream()
        .collect(Collectors.reducing(0, Integer::sum));    
//当然Stream也提供了reduce操作
final Integer collect = Lists.newArrayList(1, 2, 3, 4, 5)
        .stream().reduce(0, Integer::sum)


复制代码
  • reduce拼接字符串
String concat = Stream.of("A","B","C","D")
  							.reduce("",String::concat);
复制代码
  • reduce求最小值
double minValue = Stream.of(-1.5,1.0,-3.0,-2.0)
  								.reduce(Double.MAX_VALUE,Double::min);
复制代码
  • reduce求和
//有起始值
int sumValue = Stream.of(1,2,3,4)
  						.reduce(0,Integer::sum);

//无起始值
int sumValue = Stream.of(1,2,3,4)
  						.reduce(Integer::sum).get();
复制代码

4、开发中常用的

4.1、 自己生成流

  • 通过实现Supplier接口,可以自己控制流的生成。
  • 把 Supplier 实例传递给 Stream.generate() 生成的 Stream,默认是串行(相对 parallel 而言)但无序的(相对 ordered 而言)。由于它是无限的,在管道中,必须利用 limit 之类的操作限制 Stream 大小。
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);

复制代码

4.2 、生成等差数列

//步长为3
Stream.iterate(0, n -> n + 3)
  .limit(10)
  .forEach(x -> System.out.print(x + " "));
复制代码

4.3、 reduce、collect实现filter功能

 1.collect实现方式更简单,效率也更高。
 2.reduce每次需要new ArrayList是因为reduce规定第二个参数:
    BiFunction accumulator表达式不能改变其自身参数acc原有值,所以每次都要new ArrayList(acc),再返回新的list。
//reduce 方式
public static <T> List<T> filter(Stream<T> stream,Predicate<T> predicate){
    
    return stream.reduce(new ArrayList<T>(),(acc,t)->{
        if(predicate.test(t)){
            List<T> lists = new ArrayList<T>(acc);
            lists.add(t);
            return lists;
        }
        return acc;
    }, (List<T> left,List<T> right)->{
        List<T> lists= new ArrayList<T>(left);
        lists.addAll(right);
        return lists;
    }
}


//collect

public static <T> List<T> filter(Stream<T> stream, Predicate<T> predicate) {
   return stream.collect(ArrayList::new, (acc, t) -> {
       if (predicate.test(t))
           acc.add(t);
   }, ArrayList::addAll);
}

复制代码

4.4、 返回规约类型

//使用toCollection()指定规约容器的类型
ArrayList<String> arrayList = stream.collect(Collectors.toCollection(ArrayList::new));// (3)
HashSet<String> hashSet = stream.collect(Collectors.toCollection(HashSet::new));// (4)

复制代码

4.5、 groupingBy上下游收集器

  • 使用partitioningBy()生成的收集器,适用于将Stream中的元素依据某个二值逻辑(满足、不满足)分成互补相交的两部分,如性吧,及格与否
  • groupingBy()按照某个属性对数据分组,属性相同的元素会被对应到Map的一个key上
//partitioningBy()
Map<Boolean, List<Student>> passingFailing = students.stream()
         .collect(Collectors.partitioningBy(s -> s.getGrade() >= PASS_THRESHOLD));
         
//groupingBy
Map<Department, List<Employee>> byDept = employees.stream()
            .collect(Collectors.groupingBy(Employee::getDepartment));       


//mapping的下游
//按照部门对员工进行分组,并且只保留员工的名字
Map<Department,List<String>> byDept=employees.stream()
            .collect(Collectors.groupingBy(Employee::getDepartment,
                Collectors.mapping(Employee::getName,   //下游收集器
                    Collectors.toList())));             //更下游收集器

复制代码

猜你喜欢

转载自juejin.im/post/5e061194518825122b0f994c