Java8新特性(流API)

这些是自己在看Java书籍时从书上摘抄的,主要是为了加强自己对知识点的记忆,同时也是对这个知识点不太懂所以记录一下,就是自己的一个学习笔记。

1.1 流的基本认识

流的概念:流是数据的渠道。因此,流代表了一个对象的序列。流操作数据源,如数组或集合。流本身不存储数据,而只是移动数据,在移动过程中可能会对数据执行过滤、排序或其他操作。流操作本身不修改数据。例如,对流排序不会修改数据源的顺序,相反对流会创建一个新流,其中包含排序后的结果。
注意:这里使用的“流”与I/O类使用的“流”不同
流与集合的区别:
1.流并不存储其元素。这些元素可能存储在底层的集合中,或者按需生成的
2.流的操作不会修改其数据源。例如,filter方法不会从流中移除元素,而是会生成一个新的流,其中不包含被过滤掉的元素。
3.流的操作是尽可能惰性执行的。这意味着直至需要其结果时,操作才会执行。例如,如果我们只想查找5个长单词而不是所有长单词,那么filter方法就会在匹配到第五个单词后停止过滤。

1.2 流方法

流中的方法可以分为“终端操作”和“中间操作”。
终端操作会消费流。这种操作用于产生结果,例如找出流中最小值(min()方法),或者执行某种操作,比如forEach()方法。一个流被消费以后,就不能被重用。
中间操作会产生另一个流。因此,中间操作可以用来创建执行一系列动作的管道。另外一点:中间操作不是立即发生的。相反,当在中间操作创建的新流上执行完终端操作后,中间操作指定的操作才会发生。这种机制称为延迟行为,所以中间操作为延迟发生的。
流的另外一个关键点:一些中间操作是无状态的,另外一些是有状态的。在无状态操作中,独立于其他元素处理每一个元素。在有状态操作中,某个元素的处理可能依赖于其他元素。例如:排序(sorted()方法)是有序操作,因为元素的顺序依赖于其他元素的值。基于无状态谓词的元素过滤是无状态的,因为每个元素都是被单独处理的。因此,filter()方法是无状态的。当需要并行处理流时,无状态与有状态的区别尤为重要,因为有状态操作可能需要几次处理才能完成。

1.3 流的创建

1.使用Collection接口的stream方法将任何集合转换为一个流。

        ArrayList<Integer> list = new ArrayList<Integer>();
        for (int i = 0;i < 10;i++){
            list.add(i);
        }
        Stream<Integer> stream1 = list.stream();

2.使用静态的Stream.of方法

Integer[] integers = new Integer[]{1,2,3,4,5};
Stream<Integer> stream2 = Stream.of(1,2,3,4,5,6,7,8,9,10);
Stream<Integer> stream3 = Stream.of(integers);

Stream.of方法接受一个可变长的参数或者是一个数组
3.使用Arrays.stream方法

String[] strings = new String[]{"one","two","three"};
Stream<String> stream4 = Arrays.stream(strings);

Arrays.stream方法接受一个数组,该方法有几个重载形式,例如有的形式能够处理基本类型的数组,它们返回流的类型为IntStream、DoubleStream或LongStream
4.还有另外一些方式可以获得流,例如许多流操作会返回新流,而且通过对BufferedReader调用lines()方法,可以获得I/O源的流。

1.4 filter、map和flatMap方法

流的转换会产生一个新的流,它的元素派生自另一个流中的元素。
filter转换会产生一个新流,它的元素与某种条件相匹配。下面的例子是将一个字符串流转换成只包含长单词的另外一个流,每个元素的长度大于12:

String[] text = "I went to the countryside to get my internship".split(" ");
List<String> words = Arrays.asList(text);
Stream<String> longWords = words.stream().filter((w) -> w.length() > 12);

通常,我们想要按照某种方式来转换流中的值,此时,可以使用map方法并传递执行该转换的函数。下面例子将所有单词都转换为小写:

Stream<String> lowercaseWords = words.stream().map(String::toLowerCase);

这里我们使用的是方法引用的map,也可以使用lambda表达式来代替:

Stream<String> lowercaseWords2 = words.stream().map((s) -> s.toLowerCase());

可以使用map产生包含所有单词首字母的流

Stream<String> firstLetters = words.stream().map((s) -> s.substring(0,1));

使用map时会有一个函数应用到每一个元素上,并且其结果是包含了应用该函数后所产生的所有结果的流
flatMap方法可以将一个函数应用于当前流中所有元素所产生的结果连接到一起,并产生一个新流。使用该方法可以将包含流的流,转换为一个流。

1.5 抽取子流和组合流

调用stream.limit(n)会返回一个新流,它在n个元素之后结束(如果原来的流比n短,那么就会在该流结束时结束)。这个方法对裁剪无限流的尺寸特别有用。下面的例子会产生包含100个随机数的流

Stream<Double> randoms = Stream.generate(Math::random).limit(100);

调用stream.skip(n)方法正好与limit方法相反:它会丢弃前n个元素。下面的例子新流中不包含集合中的前两个元素:

Stream<String> skipwords = words.stream().skip(2);

我们可以使用Stream类的静态方法concat方法将两个流连接起来:

String[] text1 = "I went to the countryside to get my internship".split(" ");
String[] text2 = "Last year".split(" ");
Stream<String> newwords = Stream.concat(Arrays.stream(text2),Arrays.stream(text1));

注意:第一个流不应该时无限流,否则第二个流将永远得不到处理

1.6 其他的流转换

distinct方法会返回一个流,它的元素是从原有流中产生的,即原来的元素按照同样的顺序剔除重复元素后产生的,这些重复元素并不一定是毗邻的。

Stream<String> uniquewords = Stream.of("I went went went went to").distinct();

对于流的排序,有多种sorted方法的变体可用。其中一种用于操作Comparable元素的流,另一种可以接受一个Comparator。

Stream<String> sortedwords1 = words.stream().sorted();
Stream<String> sortedwords2 = words.stream().sorted((s1,s2) -> s1.length() - s2.length());

第一种是按照字典排序的,第二种是按照单词长度排序的。
最后,peek方法也会产生一个新流,它的元素与原来流中的元素相同,但是在每次获取一个元素时,都会调用一个函数,这对调试来说非常方便:
下面例子每次获取流中元素时都会实行peek里面的函数,打印单词:

Stream<String> peekwords = words.stream().peek((w) -> System.out.println(w));

1.7 简单简约

约简是一种终端操作,它们会将流约简为可以在程序使用的非流值。
count方法就是一种简单约简,count方法会返回流中的元素。max方法会返回流中最大值。min方法会返回流中最小值。这些方法的是一个类型Optional的值,它要么包装的答案,要么没有任何值。Optional类型是一种表示缺少返回值比较好的方式。
下面的例子获取流中的最大值和最小值:

Optional<Integer> maxInteger = stream2.max(Integer::compare);
Optional<Integer> minInteger = stream2.min(Integer::compare);

min方法和max方法的参数是Comparator,该比较器用于比较流中的两个元素,本例中使用Integer的compare方法的引用传递给了min和max方法
findFirst方法返回的是非空集合中的第一个值。它通常与filter组合使用时很有用。例如下面展示了如何找到第一个以字母w开头的单词,前提是存在这样的单词:

Optional<String> startWithw = words.stream().filter((w) -> w.startsWith("w")).findFirst();

如果不强调使用第一个匹配,而是使用任意的匹配都可以,那么就可以使用findAny方法。这个方法在并行处理流时很有效,因为流可以报告任何它找到的匹配而不是被限制为必须报告第一个匹配:

Optional<String> startWithwAny = words.stream().filter((w) -> w.startsWith("w")).findAny();

如果只想知道是否存在匹配,那么可以使用anyMatch,该方法返回一个Boolean值

boolean startWithwFlag = words.stream().anyMatch((w) -> w.startsWith("w"));

还有allMatch和noneMatch方法,它们分别在所有元素和没有任何元素匹配谓词的情况下返回true。这些方法也可以通过并行运行而获益。

1.8 Optional类型

Optional对象是一种包装器对象,要么包装了类型T的对象,要么没有包装任何对象。对于第一种情况,我们称这种值存在。Optional类型被当作一种更安全的方式,用来代替类型T的引用,这种引用要么引用某个对象,要么为null。
有效使用Optional的关键是要使用这样的方法:它的值不存在的情况下会产生一个可替代物,而只有在值存在的情况下才会使用这个值。
在没有任何匹配时,使用默认值,比如空字符串:

String result1 = startWithw.orElse("");

你还可以调用代码来计算默认值

String result2 = startWithw.orElseGet(() -> System.getProperty("mapp.default"));

或者可以在没有任何值时抛出异常:

String result3 = startWithw.orElseThrow(IllegalStateException::new);

ifPresent方法会接受一个函数。如果可选值存在,那么它会被传递给该函数。否则不会发生任何事。例如下面例子,如果该值存在的情况下想要将其添加到某个集中,那么就可以调用i发Present方法,这里使用两种形式:

startWithw.ifPresent(words::add);
startWithw.ifPresent((w) -> words.add(w));

如果想要在可选值存在时执行一种行为,在可选值不存在时执行另外一种行为,可以使用ifPresentOrElse:

startWithw.ifPresentOrElse((w) -> System.out.println(w),() -> System.out.println("没有该值"));

isPresent方法会报告某个Optional对象是否具有值,结果是一个Boolean值,存在值true,不存在false

1.9 收集结果

当处理玩流后,通常想要查看其结果。此时可以调用iterator方法,它会产生访问元素的旧时风格的迭代器,这和集合的iterator用法相似:

        Iterator<String> istr = words.iterator();
        while (istr.hasNext()){
            System.out.println(istr.next());
        }

或者,可以调用forEach方法,将某个函数应用于每个元素,下面的例子是打印流中的每一个元素到控制台:

sortedwords2.forEach((w) -> System.out.println(w));

在并行流上,forEach方法会以任意顺序遍历各个元素。如果想要按照流中的顺序来处理它们,可以调用forEachOrdered方法。当然,这个方法会丧失并行处理的部分甚至全部优势。
但是,更常见的情况是,我们想要将结果收集到数据结构中。此时,可以调用toArray方法获得由流中的元素构成的数组。因为无法在运行时创建泛型数组,所以表达式stream.toArray()会返回一个Object[]数组。如果想要让数组具有正确的类型,可以将其传递到数组构造器中:

String[] str = newwords.toArray(String[]::new);

collect方法可以将流中的元素收集到另外一个目标中,该方法会接受一个Collector接口实例。收集器是一种收集众多元素并产生单一结果的对象,Collector类提供了大量用于生成常见收集器的工厂方法。要想将流的元素收集到一个列表中,应该使用Collector.toList()方法产生的收集器:

List<String> strlist = newwords.collect(Collectors.toList());

下面的代码展示了如何将流的元素收集到一个集中:

Set<String> strset = newwords.collect(Collectors.toSet());

如果想要控制获得的集的种类,可以使用下面的调用:

TreeSet<String> strtreeset = newwords.collect(Collectors.toCollection(TreeSet::new));

假设想要通过连接操作来收集流中的所有字符串,可以调用:

String strcollect1 = newwords.collect(Collectors.joining());

如果想要在元素之间增加分隔符,可以将分隔符传递给joining方法:

String strcollect2 = newwords.collect(Collectors.joining(", "));

如果流中包含除字符串以外的其他对象,那么我们需要先将其转换为字符串,像下面例子一样:

String strcollect3 = newwords.map(Object::toString).collect(Collectors.joining(", "));

如果想要将流中的结果简约为总和、数量、平均值、最大值或最小值,可以使用summarizing(Int|Long|Double)方法中的一个。这些方法会接受一个将流对象映射为数值的函数,产生类型为(Int|Long|Double)summaryStatistics的结果,同时计算总和、数量、平均值、最大值和最小值。summaryStatistics对象包含了总和、数量、平均值、最大值和最小值。

IntSummaryStatistics summay = newwords.collect(Collectors.summarizingInt(String::length));
double averageWordLength = summay.getAverage();
double maxWordLength = summay.getMax();
double minWordLength = summay.getMin();
Long wordCount = summay.getCount();

1.10 收集到映射表中

假设我们由一个Stream,并且想要将其元素收集到一个映射表中,这样后续就可以通过它们的ID来查找人员了。Collectors.toMap方法有两个函数引元,它们用来产生映射表的键和值。例如:

Map<Integer,String> idToName = personStream.collect(Collectors.toMap(Person::getId,Person::getName));

通常情况下,值应该是实际的元素,因此第二个函数可以使用Function.identity():

Map<Integer,Person> idToPerson = personStream.collect(Collectors.toMap(Person::getId, Function.identity()));

如果有多个元素具有相同的键,就会存在冲突,收集器将会抛出一个IllegalStateException异常。可以通过提供第三个函数引元来覆盖这种行为,该函数会针对给定的已有值和新值来解决冲突以确定键对应的值,这个函数应该返回已有值、新值或者它们的组合:

Map<Integer,Person> idToPerson2 = 
        personStream.collect(
                Collectors.toMap(
                        Person::getId, 
                        Function.identity(),
                        (oldValue,newValue) -> {
                            Person person = new Person(oldValue.getId() + newValue.getId(),oldValue.getName());
                            return person;
                         }));

1.11 群组和分区

可以为每个映射表的值都生成单例集,然后指定如何将现有值与新值合并。将具有相同特征的值群聚成组是非常常见的,并且groupingBy方法直接支持它,下面例子是将person元素的流中的元素按照sex(性别)分组了:

Map<String,List<Person>> groupBysexPerson = personStream.collect(Collectors.groupingBy(Person::getSex));

当分类函数是断言函数(即返回boolean值的函数)时,流的元素可以分为两个列表:该函数返回true的元素和其他的元素,下面是将person元素的流中的元素分成了男的一组和不是男的一组,在这种情况下,使用partitioningBy比使用groupingBy更高效:

Map<Boolean,List<Person>> groupBysexPerson2 = personStream.collect(
                Collectors.partitioningBy(
                        (p) -> p.getSex().equals("男")));
List<Person> man = groupBysexPerson2.get(true);
List<Person> woman = groupBysexPerson2.get(false);

1.12 下游收集器

groupingBy方法会产生一个映射表,它的每个值都是一个列表。如果想要以某种方式来处理这些列表,就需要提供一个“下游收集器”。例如,如果想要获得集(Set)而不是列表(List),那么就可以使用Collectors.toSet收集器:

Map<String, Set<Person>> groupByToSet = personStream.collect(
        Collectors.groupingBy(
                Person::getSex,Collectors.toSet()));

Java提供了许多可以将收集到的元素简约为数字的收集器:
counting会产生收集到的元素个数。例如,可以每个性别的人数进行统计:

Map<String, Long> groupByToCounting = personStream.collect(
        Collectors.groupingBy(
                Person::getSex,Collectors.counting()));

summing(Int|Long|Double)会接受一个函数作为引元,将该函数应用到下游元素中,并产生它们的和。例如,计算城市流中每个州的人口总数:

Map<String, Long> stateToCityPopulation = cities.collect(
    Collectors.groupingBy(
        City::getState,summarizingInt(City::getPopulation)));

maxBy和minBy会接受一个比较器,并分别产生下游元素中的最大值和最小值,例如,产生每个州中最大的城市:

Map<String,Optional<City>> stateToLargestCity = cities.collect(
    Collectors.groupingBy(City::getState,
        Collectors.maxBy(Comparator.comparing(City::getPopulation))));

mapping收集器的做法正好相反,它会将一个函数应用于收集到的每一个元素,并将结果传递给下游收集器:

Map<Character,Set<Integer>> stringLengthByStartingLetter = strings.collect(
    Collectors.groupingBy((s) -> s.charAt(0),
        Collectors.mapping(String:length,Collectors.toSet())));

这里我们按照首字符对字符进行了分组。在每个组内部,我们会计算字符串的长度,然后将这些长度收集到一个集中。

1.13 约简操作

reduce方法是一种用于从流中计算某个值的通用机制,其最简单的形式将接受一个二元函数,并从前两个元素开始持续应用它。如果这个函数是求和函数或者是累乘函数,那么就很容易解释这种机制:

ArrayList<Integer> list = new ArrayList<Integer>();
for (int i = 0;i < 10;i++){
    list.add(i);
}
//累加
Optional<Integer> sum1 = list.stream().reduce((x,y) -> x + y);
//累乘
Optional<Integer> sum2 = list.stream().reduce((x,y) -> x * y);

上面的情况中,reduce方法会计算V0+V1+V2+V3…,其中Vi是流中的元素。
reduce方法定义了三个版本:

  1. Optional reduce(BinaryOperator accumulator);
  2. T reduce(T identity, BinaryOperator accumulator);
  3. U reduce(U identity,
    BiFunction<U, ? super T, U> accumulator,
    BinaryOperator combiner);

第一个版本返回Optional类型对象,该对象包含了结果。第二个版本返回T类型的对象(T类型是流中元素的类型)。在这三种形式中accumulator是一个操作两个值并得到结果的函数,在第二种形式总中,identity是这样一个值:对于涉及identity和流中任意元素的累积操作,得到的结果是本身,没有改变。例如,如果操作是加法,identity是0,如果是乘法identity是1,因为1*X是X。
BinaryOperator是一个函数式接口,定义了如下方法:
T apply(T val, T val2)
在用到reduce()中,val将包含前一个结果,val2将包含下一个元素。
第三种形式,当计算被并行化时,会有多个这种类型的计算,所以需要将它们的结果合并,因此需要提供第二个函数来执行此处理。

1.14 基本类型流

流库中具有专门的类型IntStream、LongStream和DoubleStream,用来直接存储基本类型值,而无须使用包装器。如果想要存储short、char、byte和boolean,可以使用IntStream;而对于float,可以使用DoubleStream。
为了创建IntStream,需要调用IntStream.of和Array.stream方法:

int[] values = new int[]{1,2,3,4,5};
IntStream stream = IntStream.of(1,2,3,4,5);
stream = Arrays.stream(values);
stream = Arrays.stream(values,0,3);

与对象流一样,我们还可以使用静态的generate和iterate方法。此外,IntStream和LongStream有静态方法range和rangeClosed,可以生成步长为1的整数范围:

//[0,100)不包括100包括0
IntStream zeroToNinetyNine = IntStream.range(0,100);
//[0,100]包括0和100
IntStream zeroToHundred = IntStream.rangeClosed(0,100);

CharSequence接口拥有codePoints和chars方法,可以生成由字符的Unicode码或由UTF-16编码机制的码元构成的IntStream。

String words = "I went to the countryside to get my internship";
IntStream codes = words.codePoints();

当你有一个对象流时,可以用mapToInt、mapToLong或mapToDouble将其转换为基本类型流。例如:你有一个字符串流,并想将其处理为整数,那么就可以在IntStream中实现此目的:

String[] words1 = "I went to the countryside to get my internship".split(" ");
Stream<String> stringStream = Stream.of(words1);
IntStream length = stringStream.mapToInt(String::length);

为了将基本类型流转换为对象流,需要使用boxed方法:

Stream<Integer> IntToIntegerStream = IntStream.range(0,100).boxed();

通常,基本类型流上的方法与对象流上的方法类似,主要有以下的差异:

  1. toArray方法会返回基本类型数组。
  2. 产生可选结果的方法会返回一个OptionalInt、OptionalLong或者OptionalDouble。这些类与Optional类类似,但是具有getAsInt、getAsLong和getAsDouble方法,而不是get方法。
  3. 具有分别返回总和、平均值、最大值和最小值的sum、average、max和min方法。对象流没有定义这些方法。IntStream stream = IntStream.of(1,2,3,4,5);stream.max();stream.min();stream.sum();stream.average();
  4. summaryStatistics方法会产生一个类型为IntSummaryStatistics、LongSummaryStatistics和DoubleSummaryStatistics的对象,它们可以同时报告流的总和、数量、平均值、最大值和最小值。

1.15 并行流

流使并行处理块操作变得很容易。这个过程几乎是自动的,但是需要遵守一些规则。首先,必须要有一个并行流。可以用Collection.parallelStream()方法从任何一个集合中获取一个并行流:

Stream<String> parallelWords2 = stringList.parallelStream();

而且,parallel方法可以将任意的顺序流转换为并行流:

Stream<String> parallelWords1 = stringStream2.parallel();

只要在终端方法执行时流处于并行模式,所有的中间操作都将被并行化。
当流操作并行运行时,其目标是让其返回与顺序执行时返回的结果相同。重要的是,这些操作是无状态的,并且可以以任意顺序执行
默认情况下,从有序集合(数组和列表)、范围、生成器和迭代器产生的流,或者通过调用Stream.sorted方法产生的流,都是有序的。它们的结果是按照原来元素的顺序累积的,因此是完全可预知的。如果运行相同的操作两次,将会得到完全相同的结果。
排序并不排斥高效的并行处理。例如,当计算stream.map(function)时,流可以被划分为n部分,它们被并行的处理。然后,结果将会按照顺序宠重新组装起来。
当放弃排序需求时,有些操作可以被更有效的并行化。通过在流上调用Stream.unordered方法,就可以明确表示我们对排序不感兴趣。Stream.distinct方法就是从这种方式中获益的一种操作。在有序的流中,distinct会保留所有相同元素的第一个,这对并行化是一种阻碍,因为处理每个部分的线程在其之前的所有部分都被处理完之前,并不知道应该丢弃哪些元素。如果可以接受保留唯一元素的任意一个的做法,那么所有部分就可以并行的处理(使用共享的集合来跟踪重复的元素)。
还可以通过放弃排序要求来提高limit方法的速度。如果只想从流中取出任意n个元素,而并不在意到底要获取哪些,那么可以调用:

Stream<String> sample = stringStream.parallel().unordered().limit(100);

不要指望通过将所有的流转换为并行流就能够加速操作,要牢记下面几条:

  1. 并行化会导致大量的开销,只有面对非常大的数据集才划算
  2. 只有在底层的数据源可以被有效的分割为多个部分时,将流并行化才有意义
  3. 并行流使用的线程池可能会因诸如文件I/O或网络访问这样的操作被阻塞而饿死。
    只有面对海量的内存数据和运算密集处理,并行流才会工作最佳。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值