自主学习报告第二周F_Part

来源:互联网 发布:sql语句查询时的别名 编辑:程序博客网 时间:2024/05/15 23:46

自主学习报告第二周

什么是有状态转换和无状态转换?

在流的概念里,有状态和无状态区别在于转换过程是否依赖原始流的元素,依赖称为有状态转换,不依赖称为无状态转换。之前我们所介绍的流转换操作均为无状态转换,它们的转换不需要依赖之前的元素,接下来介绍几个有状态转换的方法。

distinct

distinct方法用于排除重复值,要排除重复值说明转换过程必须依赖原始元素进行,因此是有状态转换。

Stream.of("my name is na na".splitf(" ")).distinct().peek(x -> System.out.print(" ")).forEach(System.out::printf);

输出结果为 my name is na,会自动排除交集部分。

sorted

sorted用于对排序,它可以不接收参数,此时默认按照字典序排序,如果接收参数就接收一个Comparator接口,用于自定义排序规则。Comparator接口接收两个参数,返回一个布尔值。

//默认排序方法,字典序排序Stream.of("aya is a Pretty girl".split(" ")).sorted().peek(x -> System.out.printf(" ")).forEach(System.out::printf);//自定义,按字符串长度排序Stream.of("aya is a pretty girl".split(" ")).sorted(Comparator.comparing(String::length)).peeek(x -> System.out.prinf(" ")).forEach(System.out::printf);
Optional类型

Optional类型主要用于封装返回值对象,主要作用是简化代码。在没有Optional之前,我们在对某个操作返回对象进行非空判断的时候不得不使用大量的if语句,代码冗杂,而有了Optional类型对返回对象进行封装之后,我们仅仅需要一行ifPresent(方法体)就可以进行非空判断了。

创建Optional值

首先,我们下节即将要讲的流的聚合操作大部分就是返回Optional值,这样把数据转换成流操作过后就能很方便地处理结果。此外,创建Optional值还包括以下几个方法。

  • Optional.empty能创建一个空的Optional对象
  • Optional.of能将一个任意泛型封装成Optional对象
  • Optional.ofNullLable当参数为对象是将对象封装成Optional值,为空时返回Optional.empty
Optional<String> op1 = Optional.empty();Optional<String> op2 = Optional.of("Optional");//会报NPE错误//Optional<String> op3 = Optional.of(null);//返回Optional.emptyOptional<String> op4 = Optional.ofNullLable(null);
Optional的orElse,orElseGet,orElseThrow方法
  • orElse(str)方法会判断Optional是否为空,为空则输出str,不为空输出Optional的值。
  • orElseGet(supplier)方法同样会判断Optional是否为空,不为空输出Optional的值,为空则输出参数的值,参数是一个supplier接口,不接收返回值,返回一个结果,因此可以在supplier中自己构造返回值。
  • orElseThrow(supplier)同样判断是否为空,为空则构造一个异常输出。
String str = Optional.<String>ofNullable(null).ofElse("Optional");String str = Optional.<String>ofNullable(null).ofElseGet(() -> {return "Optional";})Exception e = Optional.<Exception>ofNullable(null).orElseThrow(IOException::new);

上面的例子有两点要讲的,第一是异常需要被捕获或者抛出,这一点不容置疑。第二点是类型推断,假如根据上下文能够推断类型编译器会自动给表达式的结果进行类型转换,假如根据上下文不能推断类型,则需要我们手动加上类型声明,就上上述的程序<>里面那样。

Optional的isPresent和ifPresent方法

isPresent用于判断Optional值是否为空,一般不怎么用到。最常用到的是ifPresent方法,该方法接收一个comsumer接口函数,当判断结果不为空时,将Optional的可选值作为参数传入comsumer函数进行进一步操作。

if(op.isPresent()) System.out.pirnt(op.get());//上述代码等价于op.ifPresent(System.out::print);
Optional的map方法和flatMap方法

上述的所有对Optional的操作都是对值进行操作,假如我们希望对值操作后返回一个结果,我们就应该考虑使用map和flatMap方法。事实上,Optional相当于一个集合,一个或存空值,或存任意一个泛型对象的值。这样map和flatMap的用法大致就清楚了。map是对Optional内的元素进行操作,之后返回一个结果的函数;而flatMap是对元素拆箱后操作,返回一个结果的函数。

Optional<String> op = Optional.of("Hello");if(op.isPresent()) System.out.println(op.map(x -> x + ",world!").get());

这里需要强调的是map是不会进行非空判断的,所以要我们手动进行非空判断。

Optional<String> op = Optional.of("Hello");Optional<Optional> op1 = Optional.of(op);String = op1.flatMap(x -> x).get();

向上述情况,Optional包含着Optional,这时候假如使用map操作的对象就是Optional对象而不是元素本身,通过flatMap可以对Optional进行一次自动拆箱,因为这里只包了一层因此一次拆箱就可以直接操作元素了。假如是n层就要调用n次flatMap进行n次拆箱。

聚合操作

聚合操作是对流的终止操作,通过聚合操作,或生成集合,或获得最值统计总数等。通过聚合操作产生的值一般都被封装为Optional对象。

统计相关的几个聚合操作
  • max返回流的最大值,min返回流的最小值
  • count返回元素个数总和
Stream<Integet> stream = Stream.of(4,1,6,2,5);Optional<Integer> max = stream.max(Integer::compare);Optional<Integer> min = stream.min(Integer::compare);long count = stream.count();

max/min接收的参数是Integer的静态方法,假如流是字符流,String也实现了对应的方法,同样的其他类型对象也实现了该静态方法。count方法返回的类型不是Optional类型,而直接是long型。

findFirst和findAny

findFirst会返回非空集的第一个元素,findAny会返回非空集的任一元素。这两个方法用法差不多,findAny主要用于并行流,它在并行流中匹配到第一个就会停止其他所有流的匹配。

Stream<String> stream = Stream.of("Hello World How Are You".split(" "));Optional<String> word = stream.filter(x -> x.startsWith("H")).findFirst();//Optional<String> word = stream.parallel().filter(x -> x.startsWith("H")).findAny();

上述代码运行时先过滤所有元素,将符合的元素过滤到新的流里,findFirst会返回流里第一个元素,而findAny在其中一个线程返回一个符合的元素后程序结束。

anyMatch、noneMatch和allMatch

这三个方法都是用于判断是否存在某元素,anyMatch是只要存在任意一个符合的元素就会返回TRUE,noneMarch是只有没有匹配元素时返回TRUE,allMatch时所有元素都符合条件时才返回TRUE。

Stream<String> stream = Stream.of("Hello How do you doing".split(" "));//true//stream.anyMatch(x -> x.startsWith("H"));//false//stream.noneMatch(x -> x.startsWith("H"));//falsestream.allMatch(x ->x.startsWith("H"));
reduce方法

reduce用于对流进行聚合操作,对于串行流而言,该方法可以用于所有的聚合操作,包括针对联合操作和非联合操作的情况(这里联合操作和非联合操作区分的关键是元素顺序对计算结构是否影响,比如加法和乘法存在交换律因此顺序对计算结构不影响,因此是联合操作;而减法除法不存在交换律,运算的顺序会对运算结果有影响,因此是非联合操作),而针对并行流而言,该方法只能应用于联合操作的情况,因为并行的执行时非顺序的。当然,假如你想让并行流按顺序执行也是可以的,不过这样就失去了并行的大部分优点了。

  • reduce 接收一个参数时候,必须接受一个binaryOperator接口,该接口接收两个相同类型参数,返回值跟接受值类型相同。
  • reduce接收两个参数,除了binaryOperator接口外还必须接收一个相同类型的identity值,主要用于避免输出null值。
  • reduce接收三个参数的目的是为了操作不同的类型的数据,比如计算某个对象的字符长度总和,计算字符长度总和的话必然要接收一个String类型,返回值必然是int类型,因此用上面的方法就变得不可靠了。我们可以通过接收三个参数,分别是identity,bigFunction和binaryOperator,其中bigFunction接收两个任意泛型的值,返回一个任意泛型的结果。
reduce并行执行过程简推
System.out.println(Stream.of("my name is chenweijie zhong guo zui qiang bufu biezhe meiren gen ni bai yan ".split(" ")).parallel().reduce(0, (t, w) -> {System.out.println("第一个参数的输出:t->"+t+"|w.length->"+w.length());return t+w.length();}, (a, b) -> {System.out.println("第二个参数的输出a->"+a+"|b->"+b);return a+b;}));

输出结果

执行结果

首先要明确上述代码的结构,上述代码对并行流进行计算字符长度并获得字符长度总和。reduce函数接收了一个identity值为0,作为每一条线程中的初始值,接受了第二个参数用于计算字符长度,接收第三个参数计算长度总和。这里我们约定第二参数和第三参数的函数体称为字符长度函数和统计函数。

从执行结果只能大概可以看出程序执行的情况:程序会为字符长度函数实例化多个线程,为统计函数分配一个线程,该线程很可能就是主线程。这几个线程互相抢夺CPU执行权。

每当字符长度函数获得线程执行权就从流中获取若干个字符,统计这几个字符串长度的总和,并将结果放进统计函数线程可享的集合容器中,直到流中没有未捕获的元素为止。

每当统计函数抢到线程的执行权就会取出集合容器中的两个元素,计算结果后放回回容器中,直到容器元素为1。

从结果上看我们还能大致推断统计函数线程的优先度小于字符长度函数线程。还能大致推断出总共的线程数,因为每次实例化一个线程就会为其赋identity的初始值,加上统计函数的线程,可推断大致有八个线程在执行。

        <Integer> arr = new ArrayList();         for(int i=0; i<50; i++){            arr.add(0);            Stream<Integer> streamInt = arr.stream();            System.out.print("当arr存在元素个数为" + (i + 1));            System.out.println("时,调用了" + streamInt.parallel().reduce(10, (x, y) -> x + y)/10 + "个线程");    }

当arr存在元素个数为1时,调用了1个线程
当arr存在元素个数为2时,调用了2个线程
当arr存在元素个数为3时,调用了3个线程
当arr存在元素个数为4时,调用了4个线程
当arr存在元素个数为5时,调用了5个线程
当arr存在元素个数为6时,调用了6个线程
当arr存在元素个数为7时,调用了7个线程
当arr存在元素个数为8时,调用了4个线程
当arr存在元素个数为9时,调用了5个线程
当arr存在元素个数为10时,调用了6个线程
当arr存在元素个数为11时,调用了7个线程
当arr存在元素个数为12时,调用了4个线程
当arr存在元素个数为13时,调用了5个线程
当arr存在元素个数为14时,调用了6个线程
当arr存在元素个数为15时,调用了7个线程
… …

从以上结果可知,包括主线程,reduce限制最大调用线程数为8。

究其根本,测试程序的PC环境正好是八核处理器,因此我们可以猜测底层操作很可能读取了本机电脑的核心数,然后根据核心数创建线程。

把流放进结果集

肠炎道:取之于民,用之于民。我们一般从集合中获得流,经过一系列转换之后希望把结果重新存进集合或者数组中。

将结果收集到数组中

String[] arr = Stream.of("hello world java8".split(" ")).toArray(String[]::new);

假如没有调用String数组的构造器,会一律生成Object数组对象。

将结果收集到链表中

List<String> list = Stream.of("hello world java7".split(" ")).collect(Collectors.toList());

将结果收集到集合中

//生成集合 Set<String> set = Stream.of("hello world java6".split(" ")).collect(Collectors.toSet());//生成TreeSetTreeSet<String> treeSet = Stream.of("hello world java5".split(" ")).collect(Collectors.toCollection(TreeSet::new));//生成HashSetHashSet<String> hashSet = Stream.of("hello world java5".split(" ")).collect(Collectors.toCollection(HashSet::new));

将结果拼接成字符串

将结果拼接成字符串我们有joining方法,该方法或不接收参数或接收一个分割符参数,或接收三个参数分别是分隔符,前缀和后缀。下面是该方法的用例。

//结果为helloString s1 = Stream.of("he", "llo").collect(Collectors.joining());//结果为hello,world!String s2 = Stream.of("hello", "world!").collect(Collectors.joining(","));//结果为hello, nice to meet you, see you!String s3 = Stream.of("nice", "to", "meet", "you").collect(Collectors.joining(" ", "hello,", ",see you!"));
使用结果生成summaryStatistics对象

summary意为概要,statistics意为统计,即该对象被封装了统计分析的函数。通过把数字流存进该对象能很方便地进行统计运算。针对整型和不同的浮点型都有对应的summarystatistics对象。

Stream<Integer> stream1 = Stream.of(1, 2, 3, 4);IntSummaryStatistics summary = stream1.collect(Collectors.summarizingInt(x -> x));//最大值int max = summary.getMax();//最小值int min = summary.getMin();//平均值double ave = summary.getAverage();//元素个数long count = summary.getCount();//总和long sum = summary.getSum();

这里生成summaryStatistics的summarizing方法接收一个toIntFunction接口函数,该函数规定接收一个任意型返回一个整型对象。

forEach方法

这个方法我们上述很多例子都有用到,在这里就不赘述了。需要提醒的一点是,并行流也能被顺序执行,通过forEachByOrder方法。

把结果收集到Map

有时候,我们获得一个对象流,希望将流存进Map对象方便我们之后进行索引,这时候toMap方法就可以大显神通了。

在写程序之前我们先写一个类封装索引值和内容,例如我们创建一个Person的实体类。

Person.java

private String id;private String name;public Person(){}public Person(String id, String name){    this.id = id;    this.name = name;}public void setId(String id){    this.id = id;}... ...

toMap(key, value)

Stream<Person> persons = Stream.of(new Person("1", "aa"), new Person("2", "bb"));//Map<String,String> map = persons.collect(Collectors.toMap(Person::getId, Person::getName));//该方法返回封装对象和索引id的map对象Map<Strin,Person> map = persons.collect(Collectors.toMap(Person::getId, Function.identity()));

上述方法能够将任意结果封装进map。但是当一个键有多个值的时候,上述方法就会报错,对于一对多的map关系,我们需要像下面这样使用toMap。

toMap

Stream<Person> persons = Stream.of(new Person("1", "aa"), new Person("1", "ee"), new Person("1", "ff"), new Person("2", "bb"), new Person("2", "cc"), new Person("2", "dd"));//这个变量只是用来监控merge函数体被调用了几次int[] count = {0};Map<String, Set<String>> map = persons.collect(Collections.toMap(Person::getId,p -> Collections.singleton(p.getName()),(a, b) -> {    System.out.println("第" + ++count[0] + "次调用");    System.out.println(a);    System.out.println(b);    Set<String> r = new HashSet<>(a);    r.addAll(b);    return r;}));System.out.println(map.get("2"));

上面函数要说明的是toMap第二个参数接收的Collections.singleton(T o)返回一个不可变的set集合,我不是很清楚这个要怎么理解,不过我自己尝试在Person里面自己封装成set,在这里调用是不可行的,在我理解singleton这个函数之前,我们暂且认为这个singleton是必不可少的吧。第三个参数是合并参数,当发现key值为重复的时候,合并集合。下面是程序运行的推断过程。

运行结果:

第1次调用[aa][ee]第2次调用[aa, ee][ff]第3次调用[bb][cc]第4次调用[bb, cc][dd][bb, cc, dd]

通过运行结果我们大致推断程序运行的过程:首先程序获得key值,检查结果集中是否有重复的key值,无则将该key和通过第二参数封装成set的value放进map中,有的话调用merge函数,合并旧set集和新set集。

分组和分片

通过之前介绍的toMap方法我们能够对对象按照键值分组,其中键为String类型,值是一个封装对象的集合,试着会想一想我们之前toMap方法的步骤,先是判断键是否存在map中,不存在则将值封装成集合,然后把键对应集合放进map中,存在的话就执行merge函数,将键索引的原始集合和现在的集合合并成一个新的集合并重新放进map中。这个过程相当复杂,实际上本人就在纠结toMap方法怎么用纠结了半天时间,直到现在还是不是很了解为何要用到Collections的singleton方法能生成不可变集合以及为何自定义生成集合的函数就会报错,只是模糊地觉得应该跟单例有关。如果有谁知道原因,也请在留言下做出回答,感谢您。

现在介绍一种能实现toMap方法同样效果的方法,并且远比toMap强大,就是分组grouping方法。

Stream<Person> persons = Stream.of(new Person("1", "aa"), new Person("2", "bb"), new Person("1", "cc"), new Person("2", "dd"));Map<String, List<Person>> map = persons.collect(Collectors.groupingBy(Person::getId));

通过这样就能根据id对Person进行分组,但是现在和我们最初的结果并不相符,我们想要的结果应该是生成一个键为id值为name的集合的map才对。完全无须担心,这一点强大的groupingBy也为我们提供了实现。

Map<String, Set<String>> map = person.collect(Collectors.groupingBy(Person::getId, Collectors.Mapping(Person::getName, Collectors.toSet())));

通过这样就能实现和我们最初用toMap一模一样效果了。

现在讲解一下上面函数的用法:

  • 针对groupingBy的第一个用法,它要求传入一个参数,这个参数是一个Function函数,用于接收一个任意泛型,返回一个结果,该结果作为分组的依据(即作为键)。

  • 针对groupingBy的第二个用法,它除了要就传入一个Function外,还可以传入一个downStream收集器(中文译为下流收集器),这个收集器必须是一个实现了Collector接口的方法,在这里我们传入collectors的mapping方法,这个方法会把第一个参数定义的函数应用到收集到的元素中,此外该方法也要求一个下流收集器。我们这里把收集到的Person对象取出name字段封装进Set中并返回出去作为键的值。

其他的downStream收集器

grouping默认的下流收集器是toList,此外假如你想让结果封装进set集合内,你可以考虑用toSet方法,假如你想指定set类型,你也可以使用toCollections传入Set类型的构造函数来构造。

Map<String, TreeSet<String>> map = perStream.collect(Collectors.groupingBy(Person::getId,Collectors.mapping(Person::getName, Collectors.toCollection(TreeSet::new))));

上面的代码看上去似乎有点复杂,但是Java8的代码有一个特点就是按照顺序读下来就对了。上面代码最终结果构造了一个以String为键,TreeSet为值得map,构造过程如下:

首先对读入的流按照id进行分组,分组之后的值是以Person类型的流存在,该流被应用到mapping定义的函数体中,获得存放name的字符串流,最后通过构造器构造TreeSet,并把流存进TreeSet中,将键值对应的id和TreeSet放进map中,直到原始流中的元素都遍历完了就返回map。

counting方法可以统计每个键下面对应的值的个数,它返回的对象是一个值为long的map对象。

summarizing方法可以返回一个summaryStatistics对象,该对象在之前介绍过,就不重复介绍了。

maxBy和minBy方法能获得最值。

下面是这几个方法的用例:

Map<String, Long> map = perStream.collect(Collectors.groupingBy(Person::getId, Collectors.counting()));Map<String, IntSummaryStatistics> map = perStream.collect(Collectors.groupingBy(Person::getId, Collectors.mapping(Person::getName, Collectors.summarizingInt(String::length))));

上面的代码也按照之前说的方法读下去,感觉也没什么需要特别强调的。maxBy和MinBy这个两个方法用法类似,但是用途不是特别广泛,主要用于统计键对应的集合中的数据,返回最高或最低的一个数据的map对象。而且事实上,简单的替代方法远多于此,上述的maxBy方法和minBy方法我感觉有诸多限制,还不如直接操作数字流或者操作summaryStatistics对象。这是没有什么实际项目经验的我的见解,事实上在项目中能被很好地应用我也不反驳。

上面介绍的是分组方法,这里再介绍一个分片方法。

分片方法PartitioningBy主要用于将流数据分成true和false 两片,该方法接收一个predicate参数,该参数接收一个任意泛型返回布尔值。用例如下:

Map<Boolean, List<Person>> map = perStream.collect(Collectors.partitioningBy(x -> x.getId().equals("1")));
0 0