频道栏目
首页 > 资讯 > Java > 正文

精通Java8新特性Lambdas、Streams、Interface default methods

16-10-10        来源:[db:作者]  
收藏   我要投稿

Java SE 8 是有史以来对 Java 语言和库改变最大的一次,其新特性增加了函数式编程风格的Lambda表达式。虽然一开始 lambda 表达式似乎只是“另一个语言特性”而已,但实际上,它们会改变你思考编程的方式。Java中的继承和泛型在很大程度上是关于数据抽象的。而Lambda表达式则提供了用于对行为进行抽象的更棒的工具来弥补这一点.

引入Lambdas动机是需要更好的编程模型以及让 Java 开始为多核处理器(目前4核8核16核等都很普遍了)提供支持。
将新特性Stream集成到现有的 Java 平台库中,需要对已有的集合接口进行演化,而之前由于兼容性问题这一点是没法实现的,所以通过接口的默认方法的引入来解决这些问题。

1.Lambda表达式 Lambda Expression

eg1. 对集合中每个 Point 沿着 x 与 y 轴各平移 1 个单位的距离。
Java8之前最常见的迭代实现方式(外部迭代):

List pointList = Arrays.asList(new Point(1, 2), newPoint(2, 3));
for (Point p : pointList) {
    p.translate(1, 1);
}
//或者  Java5之前
for (Iterator pointItr = pointList.iterator();pointItr.hasNext(); ) {
    ((Point) pointItr.next()).translate(1, 1);
}

而在Java8中可以用lambda表达式实现(内部迭代):

List pointList = Arrays.asList(new Point(1, 2), newPoint(2, 3));
pointList.forEach(p ->p.translate(1, 1));

符号 -> 左边部分是参数列表,右边是简单的表达式体或更复杂的lambda体(花括号包围)。
如果从未使用过 lambda 表达式, 你会很难理解其中的p是啥。p是由编译器推断出来为Point类型。
List的foreach方法在Java8中加入的,它是定义在接口Iterable中的默认方法(接口继承关系是List继承Collection,Collection继承Iterable)。Iterable的源码如下:

public interface Iterable {
    Iterator iterator();
    default void forEach(Consumer action) {
        Objects.requireNonNull(action);
        for (T t : this) {
            action.accept(t);
        }
    }
    default Spliterator spliterator() {
        return Spliterators.spliteratorUnknownSize(iterator(), 0);
    }
}

相对于外部迭代,可以看到foreach方法是在Iterable内部迭代的,称为内部迭代
从外部迭代到内部迭代的变化看起来很小,只不过是迭代工作跨越了客户端-库的边界。不过,其结果却并不是那么简单。我们所需要的并行工作现在可以定义在集合类中,不必重复写在每一个要迭代集合的客户端方法中。此外,实现上可以自由使用其他技术,比如说延迟加载、乱序执行或是其他方法,从而更快地
获得结果。

foreach方法接收的参数是Consumer接口,所以Consumer接口与Lambda表达式是有关联的,编译器将lambda表达式转换成对应的函数式接口Consumer,那它是如何转化的呢?接下来先看看Consumer接口的完整代码:

@FunctionalInterface
public interface Consumer {
    void accept(T t);
    default Consumer andThen(Consumer after) {
        Objects.requireNonNull(after);
        return (T t) -> { accept(t); after.accept(t); };
    }
}

该接口是非常简单的,除默认方法default外,它只定义了一个方法accept(T t)。
这种只声明了一个抽象方法的接口成为函数式接口。使用@FunctionalInterface 来注解自定义的函数式接口声明可以让编译器检查是否合法。
函数式接口又分为很多种, java.util.function 中包含 4 种基本的函数式接口类型,而定义的40多个奇怪的类型都是通过 3 种原生类型将类型参数替换掉的各种组合由这 4 种基本类型演化而来的:
这里写图片描述
从名字可以看出该函数式接口的行为:

interface LongFunction { R apply(long value); }//函数 参数long,返回R 
interface ToIntFunction { int applyAsInt(T value); }//参数T,返回int
interface LongToIntFunction { int applyAsInt(long value); }//参数long,返回int

interface BiConsumer { void accept(T t, U u); }//二元消耗(参数T,U返回void)
interface BiFunction { R apply(T t,U u); } //二元函数(参数T,U返回R)
interface ToIntBiFunction { int apply(T t, U u); }//返回为int的二元函数

interface UnaryOperator extends Function { ... }//一元操作函数(参数T,返回T)
interface BinaryOperator extends BiFunction { ... }//二元操作函数(参数T,T返回T)
interface IntBinaryOperator { int applyAsInt(int left, int
right); }//返回为int的二元操作函数

回到问题,编译器是如何将lambda表达式转换成对应的函数式接口类型呢?

pointList.forEach(p ->p.translate(1, 1));
//这段代码中的lambda表达式是如何转换成对应的Consumer的呢?
//转换成如下形式
pointList.forEach(new Consumer(){
    @Override
    public void accept(Point p){
        p.translate(1, 1));
    }
});

这个形式应该很熟悉吧,没错,这就是匿名内部类(实际上lambda与匿名内部类是有区别的,请先暂时认为是这样转换的)。以前编写某个按钮点击的响应事件时通常会采用这种匿名内部类的方式。那么编译器是如何知道表达式 p ->p.translate(1, 1)对应的是Consumer呢,而且又怎么知道是执行Consumer的accept方法呢?
首先编译器知道该表达式接收一个参数,然后执行的方法没有返回值,再根据上下文pointList的元素类型Point,所以根据那表中4种基本函数式接口可以推断出对应的是Consumer。另外函数式接口只声明了一个抽象方法,所以编译器可以直接推断执行的是这个唯一的方法。如果接口中有多个抽象方法,那编译器是无法推断出来要执行哪个方法的,这也就是为什么函数式接口仅仅只定义一个抽象方法的原因。

到此为止,应该对lambda表达式不再恐惧了吧。原来它可以转换成匿名内部类,只不过要使用它,必须得掌握各种函数式接口的定义。它的这种简洁性,完全是由编译器帮我们完成的。

那么如此的简洁,总会有一些复杂的表达式是编译器难以推断出来的吧?确实有,它是通过 类型检查(Type Checking)以及重载解析(Overload Resolution)来解决这些问题的。这两个以及
变量捕获(Variable Capture)请看电子书。

1.1 lambda 的语法 The Syntax of Lambdas

Java 中的 lambda 表达式包含一个参数列表和一个 lambda 体,二者之间通过一个函数箭头“->”分隔。包含了单个参数:

p ->p.translate(1, 1)
i -> new Point(i, i + 1)

不过与方法声明类似,lambda 可以接收任意数量的参数。除了像之前那样接收单个参数的 lambda 外, 参数列表必须使用圆括号包围起来:

(x, y) -> x + y
() -> 23

到目前为止,我们在声明参数时并没有显式指定类型,因为不指定类型时 lambda 的可读性通常会更好一些。不过,我们总是可以提供参数类型,有时这也是必要的,因为编译器可能无法从上下文中推断出其类型。如果显式提供了类型,那就必须为所有参数都提供类型,而且参数列表必须包围在圆括号中:

(int x, int y) -> x + y

可以像方法参数那样修改这种显式类型的参数,例如,可以将其声明为 final,也可以添加注解。
函数箭头右侧的 lambda 体可以是表达式, 到目前为止所有示例都是这样的(注意,方法调用是表达式,包括那些返回 void 的方法)。诸如此类的 lambda 有时也称为“表达式 lambda” 。更为一般的形式则是“语句 lambda” ,其中的 lambda 体是一个块,也就是说,是由花括号包围的一系列语句:

(Thread t) ->{ t.start(); }
() ->{ System.gc(); return 0; }

表达式 lambda:
args -> expr
可以看成相应的语句 lambda 的简写形式:
args -> { return expr; }

在块体中到底使用还是省略 return 关键字的原则与普通的方法体是一致的,也就是说,如果 lambda 体中的表达式有返回值,那就需要使用 return,也可以后跟一个参数来立刻终止 lambda 体的执行。如果 lambda 返回 void,那就可以省略 return,也可以使用它,但后面不带参数。

lambda 表达式不需要也不允许使用 throws 语句来声明它们可能会抛出的异常。

1.2 方法与构造器引用 Method and Constructor References

一般来说,任何 lambda 表达式都可以看作声明在函数式接口中的单个抽象方法的实现。不过,lambda 表达式只是调用现有类中的具名方法的一种方式时, 编写 lambda 的更好方式则是使用已有的名字。例如,考虑如下代码,它会向控制台输出列表中的每个元素:

pointList.forEach(s -> System.out.print(s));

这里的 lambda 表达式只是将参数传递给 print 调用。诸如此类的 lambda(其唯一目的就是将参数提供给一个具体方法)完全是由该方法类型定义的。因此,假如可以通过某种方式确定出类型,那么只包含方法名的简短形式所提供的信息就与完整的 lambda表达式一样,但可读性会更好。相比于上述代码,我们可以这样编写:

pointList.forEach(System.out::print);

它表示相同的含义。这种对现有类的具体方法的操作写法称为方法引用。有 4 种类型的方法引用,如表 2-2 所示。
这里写图片描述

1.2.1 静态方法引用 Static Method References

静态方法引用的语法只需要类与静态方法名,中间通过两个
冒号分隔。例如:
String::valueOf
Integer::compare
对数组 integerArray 排序就可以调用:

Arrays.sort(integerArray, (x,y) -> Integer.compareUnsigned(x,
y));

这是合法的,不过这么做要比相应的静态方法引用冗长和
重复:

Arrays.sort(integerArray, Integer::compareUnsigned);

事实上,该方法是在 Java 8 中引入的,并且就是期望按照这种方式使用。未来,API 设计的一个要素就是希望方法签名要适合于函数式接口转换。

1.2.2 实例方法引用 Instance Method References

有两种方式可以引用实例方法。绑定方法引用类似于静态引用, 只不过是通ObjectReference::Identifier 替换 ReferenceType::Identifier。之前的示例就是绑定方法引用:forEach方法用于将集合中的每个元素传递给 PrintStream 对象 System.out 的实例方法print 进行处理,如下 lambda 表达式:

pointList.forEach(p ->System.out.print(p));

可以替换为绑定方法引用:

pointList.forEach(System.out::print);

之所以称为绑定引用,是因为接收者已经确定为方法引用的一部分。 对方法引用 System.out::print 的每次调用都会有相同的接收者:System.out。不过,你常常会在调用方法引用时带上方法接收者及其参(来自于方法引用的参数)。要想做到这一点,你需要一个未绑定的方法引用,之所以起这个名字,是因为接收者是不确定的;方法引用的第一个参数被用作接收者。在只有一个参数的情况下,未绑定方法引用是最容易理解的;例如,要想通过工厂方法 comparing创建一个 Comparator, 我们可以将如下 lambda
表达式:

Comparator personComp = Comparator.comparing(p ->p.getLastName());

替换为未绑定方法引用:

Comparator personComp = Comparator.comparing(Person::getLastName);

未绑定方法引用可以通过其语法识别出来:与静态方法引用一样,我们也使用格式 ReferenceType::Identifier,不过这里的Identifier 指的是实例方法而非静态方法。要想探寻绑定与未绑定
方法引用之间的差别,考虑调用方法 Map.replaceAll 并提供绑定与未绑定的实例方法引用:
其方法声明为 public void replaceAll(BiFunction );
Map.replaceAll 的 效 果 是 对 map 中 的 每 个 键 值 对 应 用 其BiFunction参数,并使用结果替换键值对中的值部分。如果变量map指向的是一个TreeMap,并且其字符串表示如下:

{alpha=X, bravo=Y, charlie=Z}

那么像下面这样通过绑定方法引用来调用 replaceAll:

String str = "alpha-bravo-charlie";
map.replaceAll(str::replace)

效果就相当于三次应用 str.replace,即:

str.replace("alpha","X")
str.replace("bravo","Y")
str.replace("charlie","Z")

每次调用的结果都会替换相应的值, 执行完毕后 map 将包含:

{alpha=X-bravo-charlie, bravo=alpha-Y-charlie,charlie=alpha-bravo-Z}

现 在 使 用 map 的 初 始 值 来 重 新 执 行 该 例 , 再 次 调 用replaceAll, 这次使用未绑定方法引用 String::concat, 这是对 String实例方法的引用,接收单个参数。使用单个参数的实例方法作BiFunction 看起来有些奇怪,不过事实上它是 BiFunction 的方法引用:它会传递两个参数(键值对)并将第一个参数作为接收者,因此方法本身会像下面这样调用:key.concat(value)
方法引用的第 1 个参数移到了接收者的位置, 第 2 个参数(以及随后的参数,如果存在的话)会向左移动一个位置。因此,如下调用:map.replaceAll(String::concat)的结果是:

{alpha=alphaX, bravo=bravoY, charlie=charlieZ}

1.2.3 构造器的引用 Constructor References

方法引用是对现有方法的句柄,与之类似,构造器引用是对现有构造器的句柄。构造器引用的创建语法类似于方法引用,只不过使用关键字 new 替换方法名。例如:
ArrayList::new
File::new
与方法引用一样,对于重载构造器的选择是通过上下文的目标类型实现的。例如,在如下代码中,map 参数的目标类型是类型为 String -> File 的函数;为了与之匹配,编译器会选择带有单
个 String 参数的 File 构造器。

Stream stringStream = Stream.of("a.txt", "b.txt","c.txt");
Stream fileStream = stringStream.map(File::new);

1.2.4 lambda 与匿名内部类 Lambdas vs. Anonymous Inner Classes

事实上, lambda 表达式有时被错误地称为匿名内部类的 “语法糖” ,这说的是二者之间只存在简单的语法上的变化。但实际上,二者之间存在很多显著差异,其中有两点对于程序员来说非常重要:
● 内部类创建表达式会确保创建一个拥有唯一标识的新对象,而 lambda 表达式的计算结果可能有,也可能没有唯一标识,这取决于具体实现。相对于对应的内部类来说,这种灵活性可以让平台使用更为高效的实现策略。
● 内部类的声明会创建出一个新的命名作用域, 在这个作用域中,this与super指的是内部类本身的当前实例;相反,lambda表达式并不会引入任何新的命名环境。 这样就避免了内部类名称查找的复杂性,名称查找会导致很多小错误,例如想要调用外围实例的方法时却错误地调用了内部类实例的Object方法。

1.2.4.1 无标识性问题 No Identity Crisis

到目前为止,Java 程序的行为总是与对象相关联,以标识、状态和行为为特征。lambda 则违背了该规则;虽然它们会共享对象的一些属性,但其唯一的用处是表示行为。由于没有状态,因此标识问题就不重要了。语言规范显式表示其是未确定的,唯一的要求就是 lambda 必须计算出实现了恰当函数接口的类实例。这么做的意图是赋予平台足够的灵活性来进行优化,如果每个 lambda 表达式都要拥有唯一标识, 那么这种灵活性无法实现。

1.2.4.2 lambda 的作用域规则 Scoping Rules for Lambdas

就像大多数内部类一样, 匿名内部类的作用域规则非常复杂,这是因为它可以引用从父类型继承下来的名字,以及声明在外部类中的名字。lambda 表达式则要简单得多,因为它们并不会从父类型中继承名字 2。除了参数以外,用在 lambda 表达式体中的名字的含义与体外面是一样的。例如,像下面这样在lambda 中再次声明一个局部变量就是非法的:

void foo() { final int i = 2; Runnable r = () -> { int i = 3;}}//非法

参数就像局部声明一样,因为它们可以引入新的名称:

IntUnaryOperator iuo = i ->{ int j = 3; return i + j; };

lambda 参数与 lambda 体局部声明可以隐藏字段名(也就是说,字段名可能会临时被重新声明为参数或局部变量名)。

class Foo {
    Object i, j;
    IntUnaryOperator iuo = i -> { int j = 3; return i + j; }
}

由于 lambda 声明就像简单的块一样, 因此关键字 this 与 super与外围环境的含义一样:也就是说,它们分别指的是外围对象及其父类对象。例如,如下程序会向控制台打印出两条“Hello,world!”消息

public class Hello {
    Runnable r1 = () -> { System.out.println(this); };
    Runnable r2 = () -> { System.out.println(toString()); };
    public String toString() { return "Hello, world!"; }
    public static void main(String... args) {
    new Hello().r1.run();
    new Hello().r2.run();
    }
}

如果使用匿名内部类而非 lambda 表达式来编写同样的程序,那么它会打印出在内部类的对象上调用 toString 方法的结果。对于匿名内部类来说,更为常见的访问外围对象当前实例的用法要使用笨拙的语法 OuterClass.this,而这对于 lambda 来说是非常直接的。

关于如何解释 this:常常会有这样一个问题:lambda 能否引用自身呢?如果名字在作用域中,那么 lambda 就可以引用自身,不过初始化器中的前向引用限制规则(对于局部变量与实例变量均如此)导致 lambda 变量无法初始化。我们还是可以声明一个递归定义的 lambda:

public class Factorial {
    IntUnaryOperator fact;
    public Factorial() {
    fact = i -> i == 0 ? 1 : i * fact.applyAsInt(i - 1);
    }
}

需要递归的 lambda 定义的场合并不太多, 这种做法完全可以胜任该任务。

2.流与管道的介绍 Introduction to Streams and Pipelines

引入lambda的两个动机是更好的编程代码以及更容易的并行化。在Stream处理集合的介绍中将这两者结合了起来。

2.1 流的基本原理 Stream Fundamentals

在操作方面,流不同于集合之处在于流不存储值。它们的目标是处理它们。例如,有一个将集合作为它的输入源的流:创建它的时候没有数据流;当中间操作需要值的时候,流才从集合中拉取它们来供给;最后,当集合中所有的值被流拉取出来供给后,那流就被消耗掉了并且将来不能再使用。但是这和空的流不一样;流在任何点上都不保留值。以非集合类作为源的流表现非常类似:例如,生成并打印前10个的2的幂次方可以用如下代码:

IntStream.iterate(1, i->i*2).limit(10).forEachOrdered(System.out::println);
//输出结果是1 2 4 8 16 32 64 128 256 512

尽管这个方法iterate生成一个无限流,但是这个lambda代表的函数只会在下游处理(本例是打印)需要值计算时才会被调用。 IntStream.iterate(int seed, IntUnaryOperator f) ,
该iterate方法产生一个无线的流:seed,f(seed),f(f(seed),f(f(f(seed)) ……
流背后的中心思想是延迟计算(lazy evaluation) :直到值需要时才会被计算。
在Java中,创建一个Itearator并不会导致任何的值处理发生,只有当调用它的next方法时才会实际上从它的集合中返回值。流在概念上与Itearator很类似,但是有重要的改进:
? 流以一种更加与客户端友好的方式处理消耗完。Iterators则是通过从hasNext方法返回false时才认为消耗完,因此客户端每次取一个元素时需要测试它。这个交互式内在的缺陷,因为在调用hasNext和next之间的时间差正好是线程干扰的时机之窗。而且,它强制让元素以顺序的方式处理,由一个复杂的且通常低效的端/库之间的交互来实现。
? 流还有一些方法能接受转换流的行为参数(中间操作)且返回转换后的流。这就可以使流链接在一起形成管道,不仅提供了一种流畅的编程风格,而且还有机会获得性能上的提升。
? 流还保持关于源的属性的信息—例如,源的值是否有序,是否数量已知等等,这样就允许在值处理方式上可以进行优化,但这对于Itearator则不可以,因为它除了值之外没有保持其他任何信息。

延迟计算的一个大的优点可以在流的‘search’方法上体现:findFirst, findAny, anyMatch, allMatch以及noneMatch。这些操作被称为‘短路’操作,因为他们通常没有必要处理流中的所有元素。

延迟计算的另一个主要的优点:它允许多个合法的操作混合进一个单一的数据通道。
eg2:首先生成了一个 Integer 实例的集合,接下来通过转换生成了一个 Point 实例的集合,最后寻找到距离原点最远的点到原点的距离.
通常可能会写如下冗长而又低效的代码来实现:

ListintList = Arrays.asList(1, 2, 3, 4, 5);
ListpointList = new ArrayList<>();
for (Integer i : intList) {
    pointList.add(new Point(i % 3, i / 1));
}
doublemaxDistance = Double.MIN_VALUE;
for (Point p : pointList) {
    maxDistance = Math.max(p.distance(0, 0), maxDistance);
}

而流则可以将几个中间操作(流的转换)串在一起,值最后才被计算:

OptionalDouble maxDistance =intList.stream()
    .map(i -> new Point(i % 3, i / 3))
    .mapToDouble(p ->p.distance(0, 0))
    .max();
//Integer流 -> Point流 -> Double流,最后在进行求max操作

将流组合为数据管道

如果需要并行处理(如果是4核处理器,理论上只需要四分之一的之前的时间)但则只需要改一处代码(stream改为parallelStream)即可实现:

OptionalDoublemaxDistance =intList.parallelStream()
    .map(i -> new Point(i % 3, i / 3))
    .mapToDouble(p ->p.distance(0, 0))
    .max();

可以看到对于大量的集合处理这是一种非常不同的处理模式。

2.1.1 面向并行的代码 Parallel-Ready Code

Lazy value sequences 在编程上是一个非常古老的概念。如何区分它们的实现在java上是通过概念上的扩展来区分的,包括并行处理。尽管顺序处理仍然是一种非常重要的计算模式,但它已不再是唯一的参考模式: 因为并行处理已经如此重要以至于我们需要重新思考我们的计算模型要面向选择一个代码到底如何执行都不可知的处理模式,是否是顺序还是并行。那样的话,我们的代码以及更重要的是我们的编码风格需要迫切改变,当将来优势的平衡倾斜于并行执行的时候。而Stream API就鼓励这么做。

2.1.2 原生流 Primitive Streams

Java5引入的自动装箱与拆箱让程序员可以忽略原生的值以及其包裹类之间的区别。编译器通常能检测出来进行自动装箱和拆箱操作。但这会带来一个很高的性能花费问题。例如:

//自动装箱/拆箱 性能问题 每次计算i+1前要拆箱计算完后再装箱
Optional max = Arrays.asList(1,2,3,4,5)
    .stream().map(i>i+1).max(Integer::compareTo);

而为了避免自动装箱拆箱带来的性能问题,本例中可以可以使用原生流 IntStream,则代码可以重写为:

OptionalInt max = IntStream.rangeClosed(1, 5).map(i->i+1).max();

这样就不会有装箱拆箱问题,在处理大量数据时,性能上可以提高几个量级。
原生流类型有三种IntStream、LongStream、DoubleStream。float可以嵌入到DoubleStream,char、short、byte则可以嵌入到IntStream中。它们十分类似于引用流类型Stream。流类型可以互相转变:
? IntStream和LongStream 可以通过asDoubleStream转换成DoubleStream,对于IntStream 有asLongStream方法可以转换成LongStream;

DoubleStream ds = IntStream.rangeClosed(1, 10).asDoubleStream();

? 对于装箱操作,每个原生流类型都有一个boxed方法,返回对应的包裹类的流:

Stream is = IntStream.rangeClosed(1, 10).boxed();

? 对于拆箱操作,包裹类的流可以转换成对应的原生流,通过调用对应的map转换操作,用合适的拆箱方法作为参数。如下将Stream转换成一个IntStream:

Stream integerStream = Stream.of(1, 2);
IntStream intStream = integerStream.mapToInt(Integer::intValue);

2.2 剖析管道 Anatomy of a Pipeline

流所有的功能由我们创建的管道通过将它们组合在一起来实现。前面很多例子都展示了管道的各个阶段:它的起点是流的源,它的后续的 转换操作是通过中间操作完成的,最后它的终点以一个终止操作来结束。

2.2.1 开始管道 Starting Pipelines

由于流应用于处理大量的数据集上具有巨大的优势,所以平台库中的可以产生大量数据的很多类现在都可以创建流来处理数据。Collections有两个Stream的工厂方法:
? java.util.Collection:这个接口的两个默认方法将是最通用的生成流的方式。
Collection
注意parallelStream这个方法返回的可能是个并行流,集合只是负责并行的呈现它的数据且并不所有的集合都能做到这点。
为了Collection接口可以引入Stream API ,采用了默认方法的方式。Java8在语法上作如此大的变化是为了解决向后兼容性问题。
? java.util.stream.Stream:这个接口暴露了几个静态工厂方法。原生流有类似的方法。
Stream
Stream
IntStream
Stream
Arrays
BufferedReader
BufferedReader.jpg
Files2.jpg
Pattern
Random.jpg

2.2.2 转换管道 Transforming Pipelines

随着流的创建,管道的下一个阶段是由一系列中间操作组成。中间操作是延迟计算的:它们只会在终止管道时迫切需要时才会计算。
中间操作分为如下一些:
? Filtering 过滤 Stream filter(Predicate)
? Mapping 映射 ,将每个流中的元素T分别使用Fucntion转换成R:Stream map(Function)
例如:Stream bookTitles = library.stream().map(Book::getPubDate);
当然它有映射的原生类型的方法:
这里写图片描述
例如:

int totalAuthorships = library.stream().mapToInt(b->b.getAuthors().size()).sum();

? One-to-Many Mapping 一对多映射 Stream flatMap(Function<>>)
例如流中每个元素book,都有多个作者:

Stream authorStream =   library.stream()
    .flatMap(b->b.getAuthors().stream());

通用它也有对应的映射为原生类型的方法:flatMapToInt, flatMapToLong, and flatMapToDouble。
? Debugging 调试 peek操作的主要目的是用于查看中间操作的结果,可以用于调试。peek接收一个Consumer行为参数。
? Sorting and Deduplicating 排序和去重
sort
例如自然排序:

Stream sortedTitles = library.stream()
    .map(Book::getTitles())
    .sorted();

自定义排序:

Stream booksSortedByTitle = library.stream()
    .sorted(Comparator.comparing(Book::getTitle));

distinct去重操作。
?Truncating 截断
skip
skip丢弃前n个元素,保留其他剩余的元素,而limit则保留前n个元素,丢弃剩余的元素。

2.2.3 非侵入性

Stream API 给程序员提供了并行操作的功能。并行处理就会涉及线程安全的问题。在设计面向并行的代码时记住一个原则:
behavioral parameters should be stateless(行为参数应该是状态无关的)。
面向并行的代码的一个同等重要的要求是管道中在执行终止操作期间要避免改变它们的源。

2.2.4 终止管道

分为3类:
? Search operations搜素操作,用于检测流的元素满足某种条件,所以不用完全处理整个流就就可以完成操作。
Search

boolean withinShelfHeight = libaray.stream()
    .filter(b->b.getTopic() == HISTORY)
    .allMatch(b->b.getHeight()<19);

这里写图片描述

Optional anyBook = library.stream()
    .filter(b->b.getAuthors().contains("Herman MV"))
        .findAny();

瞅瞅Optional 这个类
这里写图片描述

? Reductions 汇聚,以某种归纳流中元素值的方式返回一个单一的值。例如count、max、collectors.
IntStream

IntSummaryStatistics pageCountStatistics = library.stream()
    .mapToInt(b->IntStream.of(b.getPageCounts()).sum())
    .summaryStatistics();

Stream

Optional oldest = library.stream()
    .min(Comparator.comparing(Book::getPubDate));

Stream

这里写图片描述

Set titles = library.stream()
    .map(Book::getTitle)
    .collect(Collectors.toSet());

这里写图片描述

Map titleToPubdate = library.stream()
    .collect(toMap(Book::getTitle,Book::getPubDate));
Map titleToPubdate = library.stream()
    .collect(toMap(Book::getTitle,Book::getPubDate,(x,y)->x.isAfter(y)?x:y));

? Side-effecting operations 附加操作,这类操作只包含两个方法,forEach和forEachOrder.

对于终止流:收集与汇聚Ending Streams: Collection and Reduction是一个大的话题,这里不作详细讨论

相关TAG标签
上一篇:多进程编程之进程间通信-共享内存,信号量和套接字
下一篇:Flask Web 开发 测试
相关文章
图文推荐

关于我们 | 联系我们 | 广告服务 | 投资合作 | 版权申明 | 在线帮助 | 网站地图 | 作品发布 | Vip技术培训 | 举报中心

版权所有: 红黑联盟--致力于做实用的IT技术学习网站