在数学上常常用来代表一个类似函数的东西,它接受一个参数值,并返回true或false。你在后面会看到,Java 8允许写Function
//传递方法
@Data
@AllArgsConstructor
public class Apple {
private String color;
private Integer weight;
public static boolean isGreenApple(Apple apple) {
return "green".equals(apple.getColor());
}
public static boolean isHeavyApple(Apple apple) {
return apple.getWeight() > 150;
}
}
public class Test1 {
public static void main(String[] args) {
ArrayList<Apple> apples = new ArrayList<>();
Apple aa = new Apple("aa", 20);
Apple bb = new Apple("bb", 30);
Apple green = new Apple("green", 160);
Apple green1 = new Apple("green", 60);
apples.add(aa);
apples.add(bb);
apples.add(green);
apples.add(green1);
List<Apple> apples1 = filterApples(apples, Apple::isGreenApple);
System.out.println(apples1);
List<Apple> apples2 = filterApples(apples, Apple::isHeavyApple);
System.out.println(apples2);
}
public static List<Apple> filterApples(List<Apple> inventory, Predicate<Apple> p) {
ArrayList<Apple> apples = new ArrayList<>();
for (Apple apple : inventory) {
if (p.test(apple)) {
apples.add(apple);
}
}
return apples;
}
}
不需要为只用一次的方法写定义;代码更干净、更清晰,用不着去找自己到底传递了什么代码。
public class Test2 {
public static void main(String[] args) {
ArrayList<Apple> apples = new ArrayList<>();
Apple aa = new Apple("aa", 20);
Apple bb = new Apple("bb", 30);
Apple green = new Apple("green", 160);
Apple green1 = new Apple("green", 60);
apples.add(aa);
apples.add(bb);
apples.add(green);
apples.add(green1);
List<Apple> apples1 = filterApples(apples, (Apple apple) -> "green".equals(apple.getColor()));
System.out.println(apples1);
List<Apple> apples2 = filterApples(apples, (Apple apple) -> apple.getWeight() > 60);
System.out.println(apples2);
}
public static List<Apple> filterApples(List<Apple> inventory, Predicate<Apple> p) {
ArrayList<Apple> apples = new ArrayList<>();
for (Apple apple : inventory) {
if (p.test(apple)) {
apples.add(apple);
}
}
return apples;
}
}
但要是Lambda的长度多于几行(它的行为也不是一目了然)的话,应该用方法引用来指向一个有描述性名称的方法,而不是使用匿名的Lambda。以代码的清晰度为准绳。
Java里面并行很难,而且和synchronized相关的都容易出问题。在Java 8里面,首先,库会负责分块,即把大的流分成几个小的流,以便并行处理。其次,流提供的这个几乎免费的并行,只有在传递给filter之类的库方法的方法不会互动(比方说有可变的共享对象)时才能工作。举个例子,Apple::isGreenApple就是这样。确实,虽然函数式编程中的函数的主要意思是“把函数作为一等值”,不过它也常常隐含着第二层意思,即“执行时在元素之间无互动”。
Java 8中加入默认方法主要是为了支持库设计师,让他们能够写出更容易改进的接口。在Java 8之前,List并没有stream或parallelStream方法,它实现的Collection接口也没有,因为当初还没有想到这些方法嘛!可没有这些方法,这些代码就不能编译。换作你自己的接口的话,最简单的解决方案就是让Java 8的设计者把stream方法加入Collection接口,并加入ArrayList类的实现。可要是这样做,对用户来说就是噩梦了。有很多的替代集合框架都用Collection API实现了接口。但给接口加入一个新方法,意味着所有的实体类都必须为其提供一个实现。语言设计者没法控制Collections所有现有的实现,这下你就进退两难了:你如何改变已发布的接口而不破坏已有的实现呢?
Java 8的解决方法就是打破最后一环——接口如今可以包含实现类没有提供实现的方法签名了!那谁来实现它呢?缺失的方法主体随接口提供了(因此就有了默认实现),而不是由实现类提供。
这就给接口设计者提供了一个扩充接口的方式,而不会破坏现有的代码。Java 8在接口声明中使用新的default关键字来表示这一点
例如,在Java8里,你现在可以直接对List调用sort方法。它是用Java 8 List接口中如下所示的默认方法实现的,它会调用Arrays.sort静态方法:
@SuppressWarnings({"unchecked", "rawtypes"})
default void sort(Comparator<? super E> c) {
Object[] a = this.toArray();
Arrays.sort(a, (Comparator) c);
ListIterator<E> i = this.listIterator();
for (Object e : a) {
i.next();
i.set((E) e);
}
}
这意味着List的任何实体类都不需要显式实现sort,而在以前的Java版本中,除非提供了sort的实现,否则这些实体类在重新编译时都会失败
如果在好几个接口里有多个默认实现,是否意味着Java中有了某种形式的多重继承?是的,在某种程度上是这样。Java 8用一些限制来避免出现类似于C++中臭名昭著的菱形继承问题。
在C++中允许多继承,D类继承自B、C,而B、C有同一个父类A。那么这个时候调用say方法是否成功?答案是不能,编译器并不能判断这个say来自哪个父类
public class DiamondExtendProblem {
private interface A {
default void test() {
System.out.println("A");
}
}
private interface B1 extends A {
@Override
default void test() {
System.out.println("B1");
}
}
private interface B2 extends A {
@Override
default void test() {
System.out.println("B2");
}
}
private static class C implements B1, B2 {
}
public static void main(String[] args) {
C c = new C();
c.test();//菱形继承问题,Error:(32, 20) java: 类C从类型B1和B2中继承了test()的不相关默认值
}
}
C++的解决办法有两个:一是指定域,使用::
,二是虚继承
目前Java8解决这种冲突一般遵循三个原则:
//显示覆盖
public class DiamondExtendProblemSolve {
private interface A {
default void test() {
System.out.println("A");
}
}
private interface B1 extends A {
@Override
default void test() {
System.out.println("B1");
}
}
private interface B2 extends A {
@Override
default void test() {
System.out.println("B2");
}
}
private static class C implements B1, B2 {
@Override
public void test() {
B1.super.test();//显示调用B1的test方法
}
}
public static void main(String[] args) {
C c = new C();
c.test();
}
}
一等值
称作java的一等公民,即java可以操作的值,狭义上来说,是可以作为参数传递给方法的值)
二等值
有助于表示值的结构,但在程序执行期间不能传递的结构,通俗意义上来说就是不能作为参数传递的结构
在Java 8里有一个Optional类,如果你能一致地使用它的话,就可以帮助你避免出现NullPointer异常。它是一个容器对象,可以包含,也可以不包含一个值。Optional中有方法来明确处理值不存在的情况,这样就可以避免NullPointer异常了。换句话说,它使用类型系统,允许你表明我们知道一个变量可能会没有值。
(结构)模式匹配
f(0) = 1
f(n) = n*f(n-1) otherwise
在Java中,你可以在这里写一个if-then-else语句或一个switch语句。其他语言表明,对于更复杂的数据类型,模式匹配可以比if-then-else更简明地表达编程思想。对于这种数据类型,你也可以使用多态和方法重载来替代if-then-else,但对于哪种方式更合适,就语言设计而言仍有一些争论。两者都是有用的工具,你都应该掌握。不幸的是,Java 8对模式匹配的支持并不完全。
为什么Java中的switch语句应该限于原始类型值和Strings呢?函数式语言倾向于允许switch用在更多的数据类型上,包括允许模式匹配(在Scala代码中是通过match操作实现的)。在面向对象设计中,常用的访客模式可以用来遍历一组类(如汽车的不同组件:车轮、发动机、底盘等),并对每个访问的对象执行操作。模式匹配的一个优点是编译器可以报告常见错误,如:“Brakes类属于用来表示Car类的组件的一族类。你忘记了要显式处理它。”
函数式编程、及如何在Java 8中编写函数式风格、Java 8的功能并与Scala进行比较
行为参数化就是可以帮助你处理频繁变更的需求的一种软件开发模式
例如,你可以将代码块作为参数传递给另一个方法,稍后再去执行它。这样,这个方法的行为就基于那块代码被参数化了。
DRY(Don’t Repeat Yourself,不要重复自己)软件工程原则
把所有属性结合起来的笨拙尝试,糟糕透了!
public static List<Apple> filterApples(List<Apple> inventory, String color,
int weight, boolean flag) {
List<Apple> result = new ArrayList<Apple>();
for (Apple apple: inventory){
if ( (flag && apple.getColor().equals(color)) ||
(!flag && apple.getWeight() > weight) ){
result.add(apple);
}
}
return result;
}
List<Apple> greenApples = filterApples(inventory, "green", 0, true);
List<Apple> heavyApples = filterApples(inventory, "", 150, false);
我们需要一种比添加很多参数更好的方法来应对变化的需求。让我们后退一步来看看更高层次的抽象。一种可能的解决方案是对你的选择标准建模:你考虑的是苹果,需要根据Apple的某些属性(比如它是绿色的吗?重量超过150克吗?)来返回一个boolean值。我们把它称为谓词(即一个返回boolean值的函数)。让我们定义一个接口来对选择标准建模:
public interface ApplePredicate{
boolean test (Apple apple);
}
现在你就可以用ApplePredicate的多个实现代表不同的选择标准了
public class AppleHeavyWeightPredicate implements ApplePredicate{
public boolean test(Apple apple){
return apple.getWeight() > 150;
}
}
public class AppleGreenColorPredicate implements ApplePredicate{
public boolean test(Apple apple){
return "green".equals(apple.getColor());
}
}
你可以把这些标准看作filter方法的不同行为。你刚做的这些和“策略设计模式”相关,它让你定义一族算法,把它们封装起来(称为“策略”),然后在运行时选择一个算法。在这里,算法族就是ApplePredicate,不同的策略就是AppleHeavyWeightPredicate和AppleGreenColorPredicate。但是,该怎么利用ApplePredicate的不同实现呢?你需要filterApples方法接受ApplePredicate对象,对Apple做条件测试。这就是行为参数化:让方法接受多种行为(或战略)作为参数,并在内部使用,来完成不同的行为。
public static List<Apple> filterApples(List<Apple> inventory,
ApplePredicate p){
List<Apple> result = new ArrayList<>();
for(Apple apple: inventory){
if(p.test(apple)){
result.add(apple);
}
}
return result;
}
List<Apple> redAndHeavyApples = filterApples(inventory, new AppleRedAndHeavyPredicate());
但令人遗憾的是,由于该filterApples方法只能接受对象,所以你必须把代码包裹在ApplePredicate对象里。你的做法就类似于在内联“传递代码”,因为你是通过一个实现了test方法的对象来传递布尔表达式的。通过使用Lambda,你可以直接把表达式"red".equals(apple.getColor()) &&apple.getWeight() > 150传递给filterApples方法,而无需定义多个ApplePredicate类,从而去掉不必要的代码。
行为参数化的好处在于你可以把迭代要筛选的集合的逻辑与对集合中每个元素应用的行为区分开来。这样你可以重复使用同一个方法,给它不同的行为来达到不同的目的。
目前,当要把新的行为传递给filterApples方法的时候,你不得不声明好几个实现ApplePredicate接口的类,然后实例化好几个只会提到一次的ApplePredicate对象。这真是很啰嗦,很费时间!
匿名类
List<Apple> redApples = filterApples(inventory, new ApplePredicate() {
public boolean test(Apple apple){
return "red".equals(apple.getColor());
}
});
但匿名类还是不够好。第一,它往往很笨重,因为它占用了很多空间。在只需要传递一段简单的代码时(例如表示选择标准的boolean表达式),你还是要创建一个对象,明确地实现一个方法来定义一个新的行为(例如Predicate中的test方法)
Lambda
List<Apple> result = filterApples(inventory, (Apple apple) -> "red".equals(apple.getColor()));
public interface Predicate<T>{
boolean test(T t);
}
public static <T> List<T> filter(List<T> list, Predicate<T> p){
List<T> result = new ArrayList<>();
for(T e: list){
if(p.test(e)){
result.add(e);
}
}
return result;
}
可以把Lambda表达式理解为简洁地表示可传递的匿名函数的一种方式:它没有名称,但它有参数列表、函数主体、返回类型,可能还有一个可以抛出的异常列表
函数式接口
函数式接口就是只定义一个抽象方法的接口。接口现在还可以拥有默认方法(即在类没有对方法进行实现时,其主体为方法提供默认实现的方法)。哪怕有很多默认方法,只要接口只定义了一个抽象方法,它就仍然是一个函数式接口。
用函数式接口可以干什么呢?Lambda表达式允许你直接以内联的形式为函数式接口的抽象方法提供实现,并把整个表达式作为函数式接口的实例(具体说来,是函数式接口一个具体实现的实例)。你用匿名内部类也可以完成同样的事情,只不过比较笨拙:需要提供一个实现,然后再直接内联将它实例化。
函数式接口的抽象方法的签名基本上就是Lambda表达式的签名。我们将这种抽象方法叫作函数描述符。
例如,Runnable接口可以看作一个什么也不接受什么也不返回(void)的函数的签名,因为它只有一个叫作run的抽象方法,这个方法什么也不接受,什么也不返回(void)。在这里使用了一个特殊表示法来描述Lambda和函数式接口的签名,() -> void代表了参数列表为空,且返回void的函数,这正是Runnable接口所代表的。
Lambda表达式是怎么做类型检查的???
现在,只要知道Lambda表达式可以被赋给一个变量,或传递给一个接受函数式接口作为参数的方法就好了,当然这个Lambda表达式的签名要和函数式接口的抽象方法一样
@FunctionalInterface又是怎么回事?
在新的Java API中,会发现函数式接口带有@FunctionalInterface的标注。这个标注用于表示该接口会设计成一个函数式接口。如果你用@FunctionalInterface定义了一个接口,而它却不是函数式接口的话,编译器将返回一个提示原因的错误。例如,错误消息可能是“Multiple non-overriding abstract methods found in interface Foo”,表明存在多个抽象方法。请注意,@FunctionalInterface不是必需的,但对于为此设计的接口而言,使用它是比较好的做法。它就像是@Override标注表示方法被重写了。
在java.util.function包中引入了几个新的函数式接口
Predicate
java.util.function.Predicate接口定义了一个名叫test的抽象方法,它接受泛型T对象,并返回一个boolean.如果你去查Predicate接口的Javadoc说明,可能会注意到诸如and和or等其他方法。
@FunctionalInterface
public interface Predicate<T>{
boolean test(T t);
}
public static <T> List<T> filter(List<T> list, Predicate<T> p) {
List<T> results = new ArrayList<>();
for(T s: list){
if(p.test(s)){
results.add(s);
}
}
return results;
}
Predicate<String> nonEmptyStringPredicate = (String s) -> !s.isEmpty();
List<String> nonEmpty = filter(listOfStrings, nonEmptyStringPredicate);
Consumer
java.util.function.Consumer定义了一个名叫accept的抽象方法,它接受泛型T的对象,没有返回(void)。你如果需要访问类型T的对象,并对其执行某些操作,就可以使用这个接口。比如,你可以用它来创建一个forEach方法,接受一个Integers的列表,并对其中每个元素执行操作。在下面的代码中,你就可以使用这个forEach方法,并配合Lambda来打印列表中的所有元素。
public class TestConsumer {
public static void main(String[] args) {
forEach(Arrays.asList(1, 2, 3), System.out::println);
}
public static <T> void forEach(List<T> list, Consumer<T> consumer) {
list.forEach(consumer);
}
}
Function
java.util.function.Function
public class TestFunction {
public static void main(String[] args) {
List<Integer> result = map(Arrays.asList("12", "3423", "ff543"), String::length);
System.out.println(result);
}
public static <T, R> List<R> map(List<T> list, Function<T, R> function) {
List<R> result = new ArrayList<>();
list.forEach(v -> result.add(function.apply(v)));
return result;
}
}
有些函数式接口专为某些类型而设计
Java类型要么是引用类型(比如Byte、Integer、Object、List),要么是原始类型(比如int、double、byte、char)。但是泛型(比如Consumer中的T)只能绑定到引用类型。这是由泛型内部的实现方式造成的。
在Java里有一个将原始类型转换为对应的引用类型的机制。这个机制叫作装箱(boxing)。相反的操作,也就是将引用类型转换为对应的原始类型,叫作拆箱(unboxing)。
但这在性能方面是要付出代价的。装箱后的值本质上就是把原始类型包裹起来,并保存在堆里。因此,装箱后的值需要更多的内存,并需要额外的内存搜索来获取被包裹的原始值。
Java 8为我们前面所说的函数式接口带来了一个专门的版本,以便在输入和输出都是原始类型时避免自动装箱的操作。
public class TestIntegerPredicate {
public static void main(String[] args) {
IntPredicate evenNumber = (int v) -> v % 2 == 0;
Scanner sc = new Scanner(System.in);
boolean test = evenNumber.test(sc.nextInt());
System.out.println(test);
}
}
一般来说,针对专门的输入参数类型的函数式接口的名称都要加上对应的原始类型前缀,比如DoublePredicate、IntConsumer、LongBinaryOperator、IntFunction等。Function接口还有针对输出参数类型的变种:ToIntFunction、IntToDoubleFunction等。
函数式接口 | 函数描述符 | 原始类型特化 |
---|---|---|
Predicate | T->boolean | IntPredicate,LongPredicate, DoublePredicate |
Consumer | T->void | IntConsumer,LongConsumer, DoubleConsumer |
Function |
T->R | IntFunction, IntToDoubleFunction, IntToLongFunction, LongFunction, LongToDoubleFunction, LongToIntFunction, DoubleFunction, ToIntFunction, ToDoubleFunction, ToLongFunction |
Supplier | ()->T | BooleanSupplier,IntSupplier, LongSupplier, DoubleSupplier |
UnaryOperator | T->T | IntUnaryOperator, LongUnaryOperator, DoubleUnaryOperator |
BinaryOperator | (T,T)->T | IntBinaryOperator, LongBinaryOperator, DoubleBinaryOperator |
BiPredicate |
(L,R)->boolean | |
BiConsumer |
(T,U)->void | ObjIntConsumer, ObjLongConsumer, ObjDoubleConsumer |
BiFunction |
(T,U)->R | ToIntBiFunction ToLongBiFunction ToDoubleBiFunction |
使用案例 | Lambda的例子 | 对应的函数式接口 |
---|---|---|
布尔表达式 | (List list)->list.isEmpty() | Predicate |
创建对象 | () -> new Apple(10) | Supplier |
消费一个对象 | (Apple a) -> System.out.println(a.getWeight()) | Consumer |
从一个对象中选择/提取 | (String s) -> s.length() | Function |
合并两个值 | (int a, int b) -> a * b | IntBinaryOperator |
比较两个对象 | (Apple a1, Apple a2) -> a1.getWeight().compareTo(a2.getWeight()) | Comparator或BiFunction |
请注意,任何函数式接口都不允许抛出受检异常(checked exception)。如果你需要Lambda表达式来抛出异常,有两种办法:定义一个自己的函数式接口,并声明受检异常,或者把Lambda包在一个try/catch块中。
@FunctionalInterface
public interface BufferedReaderProcessor {
String process(BufferedReader b) throws IOException;
}
BufferedReaderProcessor p = (BufferedReader br) -> br.readLine();
但是你可能是在使用一个接受函数式接口的API,比如Function
Function<BufferedReader, String> f = (BufferedReader b) -> {
try {
return b.readLine();
}
catch(IOException e) {
throw new RuntimeException(e);
}
};
Lambda的类型是从使用Lambda的上下文推断出来的。上下文(比如,接受它传递的方法的参数,或接受它的值的局部变量)中Lambda表达式需要的类型称为目标类型。
List<Apple> heavierThan150g = filter(inventory, (Apple a) -> a.getWeight() > 150);
从上面看,使用Lambda的上下文是什么呢?该Lambda是作为filter的参数,所以我们进入filter方法中
filter(List<Apple> inventory,Predicate<Apple> p)
从中我们可以很清晰地看到,上下文就是filter方法中的Predicate p参数,那么目标类型就是Predicate,T绑定到Apple,Predicate是一个函数式接口。
boolean test(Apple apple)
这是Predicate的抽象方法,该方法描述了一个函数描述符,Apple->boolean
最后函数描述符Apple->boolean匹配Lambda的签名,返回一个boolean,因此代码类型检查无误。
类型检查过程可以分解为如下所示。
请注意,如果Lambda表达式抛出一个异常,那么抽象方法所声明的throws语句也必须与之匹配。
有了目标类型的概念,同一个Lambda表达式就可以与不同的函数式接口联系起来,只要它们的抽象方法签名能够兼容。
Comparator<Apple> c1 = (Apple a1, Apple a2) -> a1.getWeight().compareTo(a2.getWeight());
ToIntBiFunction<Apple, Apple> c2 = (Apple a1, Apple a2) -> a1.getWeight().compareTo(a2.getWeight());
BiFunction<Apple, Apple, Integer> c3 = (Apple a1, Apple a2) -> a1.getWeight().compareTo(a2.getWeight());
菱形运算符
那些熟悉Java的演变的人会记得,Java 7中已经引入了菱形运算符(<>),利用泛型推断从上下文推断类型的思想(这一思想甚至可以追溯到更早的泛型方法)。一个类实例表达式可以出现在两个或更多不同的上下文中,并会像下面这样推断出适当的类型参数:
List<String> listOfStrings = new ArrayList<>();
List<Integer> listOfIntegers = new ArrayList<>();
特殊的void兼容规则
如果一个Lambda的主体是一个语句表达式,它就和一个返回void的函数描述符兼容(当然需要参数列表也兼容)。例如,以下两行都是合法的,尽管List的add方法返回了一个boolean,而不是Consumer上下文(T -> void)所要求的void:
//Predicate返回了一个boolean
Predicate<String> p = s -> list.add(s);
//Consumer返回了一个void
Consumer<String> b = s -> list.add(s);
用Lambda表达式可以从赋值的上下文、方法调用的上下文(参数和返回值),以及类型转换的上下文中获得目标类型。
进一步简化代码。Java编译器会从上下文(目标类型)推断出用什么函数式接口来配合Lambda表达式,这意味着它也可以推断出适合Lambda的签名,因为函数描述符可以通过目标类型来得到。这样做的好处在于,编译器可以了解Lambda表达式的参数类型,这样就可以在Lambda语法中省去标注参数类型。换句话说,Java编译器会像下面这样推断Lambda的参数类型:
List<Apple> greenApples = filter(inventory, a -> "green".equals(a.getColor()));
参数a没有显示类型,当Lambda仅有一个类型需要推断的参数时,参数名称两边的括号也可以省略
Comparator<Apple> c =
(Apple a1, Apple a2) -> a1.getWeight().compareTo(a2.getWeight());
Comparator<Apple> c =
(a1, a2) -> a1.getWeight().compareTo(a2.getWeight());
请注意,有时候显式写出类型更易读,有时候去掉它们更易读。没有什么法则说哪种更好;对于如何让代码更易读,程序员必须做出自己的选择。
Lambda表达式也允许使用自由变量(不是参数,而是在外层作用域中定义的变量),就像匿名类一样。 它们被称作捕获Lambda。
int portNumber = 1337;
Runnable r = () -> System.out.println(portNumber);
Lambda可以没有限制地捕获(也就是在其主体中引用)实例变量和静态变量。但局部变量必须显式声明为final,或事实上是final。换句话说,Lambda表达式只能捕获指派给它们的局部变量一次。(注:捕获实例变量可以被看作捕获最终局部变量this。) 例如,下面的代码无法编译,因为portNumber变量被赋值两次:
int portNumber = 1337;
Runnable r = () -> System.out.println(portNumber);
portNumber = 31337;
为什么局部变量有这些限制?
第一,实例变量和局部变量背后的实现有一个关键不同。实例变量都存储在堆中,而局部变量则保存在栈上。如果Lambda可以直接访问局部变量,而且Lambda是在一个线程中使用的,则使用Lambda的线程,可能会在分配该变量的线程将这个变量收回之后,去访问该变量。因此,Java在访问自由局部变量时,实际上是在访问它的副本,而不是访问原始变量
第二,这一限制不鼓励你使用改变外部变量的典型命令式编程模式(这种模式会阻碍很容易做到的并行处理)
用科学的说法来说,闭包就是一个函数的实例,且它可以无限制地访问那个函数的非本地变量。例如,闭包可以作为参数传递给另一个函数。它也可以访问和修改其作用域之外的变量。现在,Java 8的Lambda和匿名类可以做类似于闭包的事情:它们可以作为参数传递给方法,并且可以访问其作用域之外的变量。但有一个限制:它们不能修改定义Lambda的方法的局部变量的内容。这些变量必须是隐式最终的。可以认为Lambda是对值封闭,而不是对变量封闭。如前所述,这种限制存在的原因在于局部变量保存在栈上,并且隐式表示它们仅限于其所在线程。如果允许捕获可改变的局部变量,就会引发造成线程不安全的新的可能性,而这是我们不想看到的(实例变量可以,因为它们保存在堆中,而堆是在线程之间共享的)。
方法引用让你可以重复使用现有的方法定义,并像Lambda一样传递它们。在一些情况下,比起使用Lambda表达式,它们似乎更易读,感觉也更自然。
inventory.sort(comparing(Apple::getWeight));
方法引用可以被看作仅仅调用特定方法的Lambda的一种快捷写法
它的基本思想是,如果一个Lambda代表的只是“直接调用这个方法”,那最好还是用名称来调用它,而不是去描述如何调用它。事实上,方法引用就是让你根据已有的方法实现来创建Lambda表达式。但是,显式地指明方法的名称,你的代码的可读性会更好。
当你需要使用方法引用时,目标引用放在分隔符::前,方法的名称放在后面。例如,Apple::getWeight就是引用了Apple类中定义的方法getWeight。请记住,不需要括号,因为你没有实际调用这个方法。方法引用就是Lambda表达式(Apple a) -> a.getWeight()的快捷写法。
Lambda | 等效的方法引用 |
---|---|
(Apple a) -> a.getWeight() | Apple::getWeight |
() -> Thread.currentThread().dumpStack() | Thread.currentThread()::dumpStack |
(str, i) -> str.substring(i) | String::substring |
(String s) -> System.out.println(s) | System.out::println |
可以把方法引用看作针对仅仅涉及单一方法的Lambda的语法糖,因为你表达同样的事情时要写的代码更少了
指向静态方法的方法引用(例如Integer的parseInt方法,写作Integer::parseInt)。
指 向 任意类型实例方法 的方法引用(例如 String 的 length 方法,写作String::length)。
你在引用一个对象的方法,而这个对象本身是Lambda的一个参数。例如,Lambda表达式(String s) -> s.toUppeCase()可以写作String::toUpperCase
指向现有对象的实例方法的方法引用(假设你有一个局部变量expensiveTransaction用于存放Transaction类型的对象,它支持实例方法getValue,那么你就可以写expensiveTransaction::getValue)
你在Lambda中调用一个已经存在的外部对象中的方法。例如,Lambda表达式()->expensiveTransaction.getValue()可以写作expensiveTransaction::getValue。
还有针对构造函数、数组构造函数和父类调用(super-call)的一些特殊形式的方法引用,方法引用的签名必须和上下文类型匹配
对于一个现有构造函数,你可以利用它的名称和关键字new来创建它的一个引用:ClassName::new。它的功能与指向静态方法的引用类似。
假设有一个构造函数没有参数。它适合Supplier的签名() -> Apple。
//构造函数引用指向默认的Student()构造函数
Supplier<Student> supplier = Student::new;
//调用Supplier的get方法将产生一个新的Student
Student student = supplier.get();
Student student1 = supplier.get();
//false
System.out.println(student == student1);
构造函数的签名是Student(String name),那么它就适合Function接口的签名
Function<String, Student> function = Student::new;
Student apply = function.apply("小明");
Student apply1 = function.apply("小明");
System.out.println(apply);
//false
System.out.println(apply == apply1);
两个参数的构造函数,适合BiFuction接口的签名
//BiFunction
BiFunction<String, Integer, Student> biFunction = Student::new;
Student student2 = biFunction.apply("小王", 20);
Student student3 = biFunction.apply("小王1", 21);
System.out.println(student2);
System.out.println(student3);
//false
System.out.println(student2==student3);
多个参数,自定义接口
Comparator具有一个叫作comparing的静态辅助方法,它可以接受一个Function来提取Comparable键值,并生成一个Comparator对象。
inventory.sort(comparing(Apple::getWeight));
public static <T, U extends Comparable<? super U>> Comparator<T> comparing(
Function<? super T, ? extends U> keyExtractor)
{
Objects.requireNonNull(keyExtractor);
return (Comparator<T> & Serializable)
(c1, c2) -> keyExtractor.apply(c1).compareTo(keyExtractor.apply(c2));
}
Java 8的好几个函数式接口都有为方便而设计的方法。具体而言,许多函数式接口,比如用于传递Lambda表达式的Comparator、Function和Predicate都提供了允许你进行复合的方法。
你可以把多个简单的Lambda复合成复杂的表达式。比如,你可以让两个谓词之间做一个or操作,组合成一个更大的谓词。而且,你还可以让一个函数的结果成为另一个函数的输入。
使用静态方法Comparator.comparing,根据提取用于比较的键值的Function来返回一个Comparator
Comparator<Apple> c = Comparator.comparing(Apple::getWeight);
逆序
对苹果按重量递减排序
inventory.sort(comparing(Apple::getWeight).reversed());
比较器链
在按重量比较两个苹果之后,按原产国排序
inventory.sort(comparing(Apple::getWeight)
.reversed()
.thenComparing(Apple::getCountry));
谓词接口包括三个方法:negate、and和or,可以重用已有的Predicate来创建更复杂的谓词
使用negate方法来返回一个Predicate的非,比如苹果不是红的
Predicate<Apple> notRedApple = redApple.negate();
请注意,and和or方法是按照在表达式链中的位置,从左向右确定优先级的。因此,a.or(b).and©可以看作(a || b) && c。
可以把Function接口所代表的Lambda表达式复合起来。Function接口为此配了andThen和compose两个默认方法,它们都会返回Function的一个实例
andThen方法会返回一个函数,它先对输入应用一个给定函数,再对输出应用另一个函数。
假设有一个函数f给数字加1 (x -> x + 1),另一个函数g给数字乘2,你可以将它们组合成一个函数h,先给数字加1,再给结果乘2
Function<Integer, Integer> f = x -> x + 1;
Function<Integer, Integer> g = x -> x * 2;
Function<Integer, Integer> h = f.andThen(g);
int result = h.apply(1);//4
使用compose方法,先把给定的函数用作compose的参数里面给的那个函数,然后再把函数本身用于结果。比如在上一个例子里用compose的话,它将意味着f(g(x)),而andThen则意味着g(f(x))
Function<Integer, Integer> f = x -> x + 1;
Function<Integer, Integer> g = x -> x * 2;
Function<Integer, Integer> h = f.compose(g);
int result = h.apply(1);
是Java API的新成员,它允许你以声明性方式处理数据集合(通过查询语句来表达,而不是临时编写一个实现)。就现在来说,你可以把它们看成遍历数据集的高级迭代器。此外,流还可以透明地并行处理,你无需写任何多线程代码了!
其他库:Guava、Apache和lambdaj
为了给Java程序员提供更好的库操作集合,前人已经做过了很多尝试。比如,Guava就是谷歌创建的一个很流行的库。它提供了multimaps和multisets等额外的容器类。Apache Commons Collections库也提供了类似的功能。最后,本书作者Mario Fusco编写的lambdaj受到函数式编程的启发,也提供了很多声明性操作集合的工具。
如今Java 8自带了官方库,可以以更加声明性的方式操作集合了很多模式,如筛选、切片、查找、匹配、映射和归约。
流简单定义:从支持数据处理操作的源生成的元素序列
元素序列
像集合一样,流也提供了一个接口,可以访问特定元素类型的一组有序值。集合讲的是数据,流讲的是计算
源
流会使用一个提供数据的源,如集合、数组或输入/输出资源。 请注意,从有序集合生成流时会保留原有的顺序。由列表生成的流,其元素顺序与列表一致。
数据处理操作
流的数据处理功能支持类似于数据库的操作,以及函数式编程语言中的常用操作,如filter、map、reduce、find、match、sort等。流操作可以顺序执行,也可并行执行
流水线
很多流操作本身会返回一个流,这样多个操作就可以链接起来,形成一个大的流水线。优化如延迟和短路。流水线的操作可以看作对数据源进行数据库式查询
内部迭代
与使用迭代器显式迭代的集合不同,流的迭代操作是在背后进行的
//使用流来筛选菜单,找出三个高热量菜肴的名字
List<String> threeHighCaloricDishNames =
menu.stream()
.filter(d -> d.getCalories() > 300)
.map(Dish::getName)
.limit(3)
.collect(toList());
System.out.println(threeHighCaloricDishNames);
//filter——接受Lambda,从流中排除某些元素,通过传递lambda d -> d.getCalories() > 300,选择出热量超过300卡路里的菜肴
//map——接受一个Lambda,将元素转换成其他形式或提取信息,通过传递方法引用Dish::getName,相当于Lambda d -> d.getName(),提取了每道菜的菜名
//limit——截断流,使其元素不超过给定数量
//collect——将流转换为其他形式。
粗略地说,集合与流之间的差异就在于什么时候进行计算。集合是一个内存中的数据结构,它包含数据结构中目前所有的值——集合中的每个元素都得先算出来才能添加到集合中。(你可以往集合里加东西或者删东西,但是不管什么时候,集合中的每个元素都是放在内存里的,元素都得先算出来才能成为集合的一部分。)
相比之下,流则是在概念上固定的数据结构(你不能添加或删除元素),其元素则是按需计算的。
构建一个质数流(2, 3, 5, 7, 11, …)有多简单,尽管质数有无穷多个。这个思想就是用户仅仅从流中提取需要的值,而这些值——在用户看不见的地方——只会按需生成。这是一种生产者-消费者的关系。从另一个角度来说,流就像是一个延迟创建的集合:只有在消费者要求的时候才会计算值(用管理学的话说这就是需求驱动,甚至是实时制造)
与此相反,集合则是急切创建的(供应商驱动:先把仓库装满,再开始卖,就像那些昙花一现的圣诞新玩意儿一样)。以质数为例,要是想创建一个包含所有质数的集合,那这个程序算起来就没完没了了,因为总有新的质数要算,然后把它加到集合里面。当然这个集合是永远也创建不完的,消费者这辈子都见不着了。
请注意,和迭代器类似,流只能遍历一次。遍历完之后,我们就说这个流已经被消费掉了。你可以从原始数据源那里再获得一个新的流来重新遍历一遍,就像迭代器一样(这里假设它是集合之类的可重复的源,如果是I/O通道就没戏了)。例如,以下代码会抛出一个异常(java.lang.IllegalStateException: stream has already been operated upon or closed),说流已被消费掉了。
List<String> title = Arrays.asList("Java8", "In", "Action");
Stream<String> s = title.stream();
s.forEach(System.out::println);
s.forEach(System.out::println);
集合和流的另一个关键区别在于它们遍历数据的方式
使用Collection接口需要用户去做迭代(比如用for-each),这称为外部迭代。 相反,Streams库使用内部迭代——它帮你把迭代做了,还把得到的流值存在了某个地方,你只要给出一个函数说要干什么就可以了。
用for-each循环外部迭代,for-each结构是一个语法糖,它背后的东西用Iterator对象表达出来更要丑陋得多。
可以连接起来的流操作称为中间操作,关闭流的操作称为终端操作
操作 | 类型 | 返回类型 | 操作参数 | 函数描述符 |
---|---|---|---|---|
filter | 中间 | Stream | Predicate | T -> boolean |
map | 中间 | Stream | Function |
T -> R |
limit | 中间 | Stream | ||
sorted | 中间 | Stream | Comparator | (T, T) -> int |
distinct | 中间 | Stream |
操作 | 类型 | 目的 |
---|---|---|
forEach | 终端 | 消费流中的每个元素并对其应用 Lambda。这一操作返回 void |
count | 终端 | 返回流中元素的个数。这一操作返回 long |
collect | 终端 | 把流归约成一个集合,比如 List、Map 甚至是 Integer。 |
filter筛选很好理解,就传一个Predicate,里面有一个distinct方法,可以用来去重的筛选方法,通过调用equals和hashCode方法来去重。
limit(n):返回一个不超过n的长度的流。如果流有序,则最多返回前n个元素;如果流无序,limit结果不会以任何顺序排列
skip(n):返回一个扔掉前n个元素的流,如果不足n个,则返回一个空流
流支持map方法,它会接受一个函数作为参数。这个函数会被应用到每个元素上,并将其映射成一个新的元素(使用映射一词,是因为它和转换类似,但其中的细微差别在于它是“创建一个新版本”而不是去“修改”)。
List<String> dishNames = menu.stream().map(Dish::getName).collect(toList());
流的扁平化
需求
对于一张单词表,如何返回一张列表,列出里面 各不相同的字符 呢?例如,给定单词列表[“Hello”,“World”],你想要返回列表[“H”,“e”,“l”, “o”,“W”,“r”,“d”]
解决方案
将每个单词转换为由其字母构成的数组,再将各个生成流扁平化为单个流
List<String> uniqueCharacters =
words.stream() //转换为流
.map(w -> w.split("")) //将每个元素映射为字符串数组的流
.flatMap(Arrays::stream) //将单独的流合并,扁平化为单个流
.distinct() //去重
.collect(Collectors.toList());//流关闭
值得注意的是:Arrays::stream会接受一个数组并产生一个流
String[] arrayOfWords = {"Goodbye", "World"};
Stream<String> streamOfwords = Arrays.stream(arrayOfWords);
public class TestFlatMap {
public static void main(String[] args) {
List<String> strings = Arrays.asList("hello", "world", "lina");
Stream<String> stream = strings.stream();
Stream<String[]> stream1 = stream.map(s -> s.split(""));
//Stream> streamStream = stream1.map(Arrays::stream);
Stream<String> stringStream = stream1.flatMap(Arrays::stream);
List<String> collect = stringStream.distinct().collect(Collectors.toList());
System.out.println(collect);
}
}
测验
public class Example1 {
public static void main(String[] args) {
List<Integer> integers = Arrays.asList(1, 2, 3, 4, 5);
List<Integer> collect = integers.stream().map(v -> v * v).collect(Collectors.toList());
System.out.println(collect);
}
}
给定两个数字列表,如何返回所有的数对呢?例如,给定列表[1, 2, 3]和列表[3, 4],应该返回[(1, 3), (1, 4), (2, 3), (2, 4), (3, 3), (3, 4)]。为简单起见,你可以用有两个元素的数组来代表数对
public class Example2 {
public static void main(String[] args) {
List list1 = Arrays.asList(1, 2, 3);
List list2 = Arrays.asList(3, 4);
List collect = list1.stream().flatMap(v -> list2.stream().map(k -> new Integer[]{v, k})).collect(Collectors.toList());
collect.forEach(i -> System.out.println(i[0] + "," + i[1]));
}
}
如何扩展前一个例子,只返回总和能被3整除的数对呢?例如(2, 4)和(3, 3)是可以的
public class Example3 {
public static void main(String[] args) {
List<Integer> list1 = Arrays.asList(1, 2, 3);
List<Integer> list2 = Arrays.asList(3, 4);
List<Integer[]> collect = list1.stream().flatMap(v -> list2.stream().filter(k -> (k + v) % 3 == 0).map(k -> new Integer[]{v, k})).collect(Collectors.toList());
collect.forEach(k -> System.out.println(k[0] + "," + k[1]));
}
}
检查谓词是否至少匹配一个元素
anyMatch方法,流中是否有一个元素能匹配给定的谓词
检查谓词是否匹配所有元素
allMatch方法,流中的元素是否都能匹配给定的谓词
noneMatch方法,流中没有任何元素与给定的谓词匹配
anyMatch、allMatch和noneMatch这三个操作都用到了短路,Java中&&和||运算符短路在流中的版本
短路求值
有些操作不需要处理整个流就能得到结果。例如,假设你需要对一个用and连起来的大布尔表达式求值。不管表达式有多长,你只需找到一个表达式为false,就可以推断整个表达式将返回false,所以用不着计算整个表达式。这就是短路。
对于流而言,某些操作(例如allMatch、anyMatch、noneMatch、findFirst和findAny)不用处理整个流就能得到结果。只要找到一个元素,就可以有结果了。
同样,limit也是一个短路操作:它只需要创建一个给定大小的流,而用不着处理流中所有的元素。在碰到无限大小的流的时候,这种操作就有用了:它们可以把无限流变成有限流。
findAny方法将返回当前流中的任意元素。流水线将在后台进行优化使其只需走一遍,并在利用短路找到结果时立即结束
Optional<Dish> dish =
menu.stream()
.filter(Dish::isVegetarian)
.findAny();
其中Optional类(java.util.Optional)是一个容器类,代表一个值存在或不存在,可以用来避免容易出问题的null。其中有以下常用的方法:
findFirst
何时使用findFirst和findAny
为什么会同时有findFirst和findAny呢?答案是并行。找到第一个元素在并行上限制更多。如果你不关心返回的元素是哪个,请使用findAny,因为它在使用并行流时限制较少。
把一个流中的元素组合起来,使用reduce操作来表达更复杂的查询,此类查询需要将流中所有元素反复结合起来,得到一个值,比如一个Integer。这样的查询可以被归类为归约操作(将流归约成一个值)。用函数式编程语言的术语来说,这称为折叠(fold),因为你可以将这个操作看成把一张长长的纸(你的流)反复折叠成一个小方块,而这就是折叠操作的结果。
1.元素求和
int[] numbers={4,5,3,9};
int sum = numbers.stream().reduce(0, (a, b) -> a + b);
T reduce(T identity, BinaryOperator<T> accumulator);//源码
reduce接受两个参数:
reduce操作是如何对一个数字流求和的?
首先,0作为Lambda(a)的第一个参数,从流中获得4作为第二个参数(b)。0 + 4得到4,它成了新的累积值。然后再用累积值和流中下一个元素5调用Lambda,产生新的累积值9。接下来,再用累积值和下一个元素3调用Lambda,得到12。最后,用12和流中最后一个元素9调用Lambda,得到最终结果21。
如果流中无数据,返回初始值。
可以使用方法引用让这段代码更简洁。在Java 8中,Integer类现在有了一个静态的sum方法来对两个数求和,用不着反复用Lambda写同一段代码了。
int[] numbers={4,5,3,9};
int sum = numbers.stream().reduce(0, Integer::sum);
reduce还有一个重载的变体,它不接受初始值,但是会返回一个Optional对象
Optional<Integer> sum = numbers.stream().reduce((a, b) -> (a + b));
Optional<T> reduce(BinaryOperator<T> accumulator);
为什么它返回一个Optional呢?考虑流中没有任何元素的情况。reduce操作无法返回其和,因为它没有初始值。这就是为什么结果被包裹在一个Optional对象里,以表明和可能不存在。如果有一个还是可以计算的
reduce接受三个参数:这个理解不了?存在问题。后续再研究
<U> U reduce(U identity,//返回实例u,传递你要返回的U类型对象的初始化实例u
BiFunction<U, ? super T, U> accumulator,//参数累加器accumulator
BinaryOperator<U> combiner);//参数组合器combiner,会将不同线程计算的结果调用combiner做汇总后返回,而这两个值必须与第二个函数参数相兼容,也就是说它们所得的结果类型是一样的。
2.最大值和最小值
Optional<Integer> max = numbers.stream().reduce(Integer::max);
测验
怎样用map和reduce方法数一数流中有多少个菜呢?
int count = vagetables.stream().map(v->1).reduce(0,(a,b)->a+b);
long count = menu.stream().count(); //使用count也可以计算
map和reduce的连接通常称为map-reduce模式,因Google用它来进行网络搜索而出名,因为它很容易并行化。
相比于逐步迭代求和,使用reduce的好处在于,这里的迭代被内部迭代抽象掉了,这让内部实现得以选择并行执行reduce操作。而迭代式求和例子要更新共享变量sum,这不是那么容易并行化的。如果你加入了同步,很可能会发现线程竞争抵消了并行本应带来的性能提升!这种计算的并行化需要另一种办法:将输入分块,分块求和,最后再合并起来。但这样的话代码看起来就完全不一样了,可以使用分支/合并框架来做。但要并行执行这段代码也要付一定代价:传递给reduce的Lambda不能更改状态(如实例变量),而且操作必须满足结合律才可以按任意顺序执行。
诸如map或filter等操作会从输入流中获取每一个元素,并在输出流中得到0或1个结果。这些操作一般都是无状态的:它们没有内部状态(假设用户提供的Lambda或方法引用没有内部可变状态)。
诸如reduce、sum、max等操作需要内部状态来累积结果。在上面的情况下,内部状态很小。在我们的例子里就是一个int或double。不管流中有多少元素要处理,内部状态都是有界的。
相反,诸如sort或distinct等操作一开始都和filter和map差不多——都是接受一个流,再生成一个流(中间操作),但有一个关键的区别。从流中排序和删除重复项时都需要知道先前的历史。例如,排序要求所有元素都放入缓冲区后才能给输出流加入一个项目,这一操
作的存储要求是无界的。要是流比较大或是无限的,就可能会有问题(把质数流倒序会做什么呢?它应当返回最大的质数,但数学告诉我们它不存在)。我们把这些操作叫作有状态操作。
操 作 | 类 型 | 返回类型 | 使用的类型/函数式接口 | 函数描述符 |
---|---|---|---|---|
filter | 中间 | Stream | Predicate | T -> boolean |
distinct | 中间(有状态-无界) | Stream | ||
skip | 中间(有状态-有界) | Stream | long | |
limit | 中间(有状态-有界) | Stream | long | |
map | 中间 | Stream | Function |
T -> R |
flatMap | 中间 | Stream | Function |
T -> Stream |
sorted | 中间(有状态-无界) | Stream | Comparator | (T, T) -> int |
anyMatch | 终端 | boolean | Predicate | T -> boolean |
noneMatch | 终端 | boolean | Predicate | T -> boolean |
allMatch | 终端 | boolean | Predicate | T -> boolean |
findAny | 终端 | Optional | ||
findFirst | 终端 | Optional | ||
forEach | 终端 | void | Consumer | T -> void |
collect | 终端 | R | Collector |
|
reduce | 终端(有状态-有界) Optional | BinaryOperator | (T, T) -> T | |
count | 终端 | long |
问题描述:
使用reduce方法计算流中元素的总和,有一个暗含的装箱成本。每个Integer都必须拆箱成一个原始类型,再进行求和。
int calories = menu.stream()
.map(Dish::getCalories)
.reduce(0, Integer::sum);
解决方案:
Java 8引入了三个原始类型特化流接口来解决这个问题:IntStream、DoubleStream和LongStream,分别将流中的元素特化为int、long和double,从而避免了暗含的装箱成本。每个接口都带来了进行常用数值归约的新方法,比如对数值流求和的sum,找到最大元素的max。?如果特化为int,long,double需要考虑数值精确度问题吗
此外还有在必要时再把它们转换回对象流的方法。要记住的是,这些特化的原因并不在于流的复杂性,而是装箱造成的复杂性——即类似int和Integer之间的效率差异。
将流转换为特化版本的常用方法是mapToInt、mapToDouble和mapToLong。这些方法和前面说的map方法的工作方式一样,只是它们返回的是一个特化流,而不是Stream。
int calories = menu.stream()
.mapToInt(Dish::getCalories) //返回一个IntStream
.sum();
IntStream mapToInt(ToIntFunction<? super T> mapper);
@FunctionalInterface
//函数描述符 T->int
public interface ToIntFunction<T> {
/**
* Applies this function to the given argument.
*
* @param value the function argument
* @return the function result
*/
int applyAsInt(T value);
}
这里,mapToInt会从每道菜中提取热量(用一个Integer表示),并返回一个IntStream(而不是一个Stream)。然后你就可以调用IntStream接口中定义的sum方法,对卡路里求和了!请注意,如果流是空的,sum默认返回0。IntStream还支持其他的方便方法,如max、min、average等。
要把原始流转换成一般流(每个int都会装箱成一个Integer),可以使用boxed方法
IntStream intStream = menu.stream().mapToInt(Dish::getCalories);
Stream<Integer> stream = intStream.boxed();
Optional可以用Integer、String等参考类型来参数化。对于三种原始流特化,也分别有一个Optional原始类型特化版本:OptionalInt、OptionalDouble和OptionalLong。
例如,要找到IntStream中的最大元素,可以调用max方法,它会返回一个OptionalInt:
OptionalInt maxCalories = menu.stream()
.mapToInt(Dish::getCalories)
.max();
int max = maxCalories.orElse(1); //如果没有最大值的话,显式提供一个默认最大值
Java 8引入了两个可以用于IntStream和LongStream的静态方法,帮助生成这种范围:range和rangeClosed。这两个方法都是第一个参数接受起始值,第二个参数接受结束值。但range是不包含结束值的,而rangeClosed则包含结束值。
IntStream evenNumbers = IntStream.rangeClosed(1, 100)
.filter(n -> n % 2 == 0); //一个从1到100的偶数流
System.out.println(evenNumbers.count()); //从1到100有50个偶数
使用静态方法Stream.of,通过显式值创建一个流。它可以接受任意数量的参数
Stream<String> stream = Stream.of("Java 8 ", "Lambdas ", "In ", "Action"); //创建一个字符串流
stream.map(String::toUpperCase).forEach(System.out::println);
使用empty得到一个空流
Stream<String> emptyStream = Stream.empty();
可以使用静态方法Arrays.stream从数组创建一个流。它接受一个数组作为参数。
例如,你可以将一个原始类型int的数组转换成一个IntStream
int[] numbers = {2, 3, 5, 7, 11, 13};
int sum = Arrays.stream(numbers).sum();
Java中用于处理文件等I/O操作的NIO API(非阻塞 I/O)已更新,以便利用Stream API。java.nio.file.Files中的很多静态方法都会返回一个流。
例如,一个很有用的方法是Files.lines,它会返回一个由指定文件中的各行构成的字符串流。
看一个文件中有多少各不相同的词
long uniqueWords = 0;
try(Stream<String> lines =
Files.lines(Paths.get("data.txt"), Charset.defaultCharset())){
uniqueWords = lines.flatMap(line -> Arrays.stream(line.split(" ")))
.distinct()
.count();
}
catch(IOException e){
}
Stream API提供了两个静态方法来从函数生成流:Stream.iterate和Stream.generate。这两个操作可以创建所谓的无限流:不像从固定集合创建的流那样有固定大小的流。由iterate和generate产生的流会用给定的函数按需创建值,因此可以无穷无尽地计算下去!一般来说,应该使用limit(n)来对这种流加以限制,以避免打印无穷多个值。
迭代
Stream.iterate(0, n -> n + 2)
.limit(10)
.forEach(System.out::println);
iterate方法接受一个初始值(在这里是0),还有一个依次应用在每个产生的新值上的Lambda(UnaryOperator类型)。
生成
与iterate方法类似,generate方法也可让你按需生成一个无限流。但generate不是依次对每个新生成的值应用函数的。它接受一个Supplier类型的Lambda提供新的值。
//生成5个0到1之间的随机双精度数(不包含1)
Stream.generate(Math::random)
.limit(5)
.forEach(System.out::println);
//转换为DoubleStream
Stream.generate(Math::random).mapToDouble(v->v).limit(5).forEach(System.out::println);
generate方法还有什么用途?
我们使用的供应源(指向Math.random的方法引用)是无状态的:它不会在任何地方记录任何值,以备以后计算使用。但供应源不一定是无状态的。可以创建存储状态的供应源,它可以修改状态,并在为流生成下一个值时使用。但很重要的一点是,在并行代码中使用有状态的供应源是不安全的。
//生成斐波纳契数列,不推荐这种用法,因为存在可变状态
IntStream.generate(new IntSupplier() {
private int previous = 0;
private int current = 1;
@Override
public int getAsInt() {
int oldPrevious = this.previous;
int nextValue = this.previous + this.current;
this.previous = this.current;
this.current = nextValue;
return oldPrevious;
}
}).limit(20).forEach(System.out::println);
这里的匿名类通过字段定义状态,状态可以通过getAsInt方法修改,有副作用,不推荐使用,在并行代码时不安全。相比之下,使用iterate的方法则是纯粹不变的:它没有修改现有状态,但在每次迭代时会创建新的元组。始终采用不变的方法,以便并行处理流,并保持结果正确。
collect是一个归约操作,就像reduce一样可以接受各种做法作为参数,将流中的元素累积成一个汇总结果。具体的做法是通过定义新的
Collector接口来定义的,因此区分Collection、Collector和collect是很重要的。
收集器非常有用,因为用它可以简洁而灵活地定义collect用来生成结果集合的标准。更具体地说,对流调用collect方法将对流中的元素触发一个归约操作(由Collector来参数化)。
一般来说,Collector会对元素应用一个转换函数(很多时候是不体现任何效果的恒等转换,例如toList),并将结果累积在一个数据结构中,从而产生这一过程的最终输出。
可以从Collectors类提供的工厂方法(例如groupingBy)创建的收集器,主要提供三大功能:
案例:利用counting工厂方法返回的收集器,数一数菜单里有多少种菜
long howManyDishes = menu.stream().collect(Collectors.counting());
long howManyDishes = menu.stream().count(); //写成这种更直观
如果已导入了Collectors类的所有静态工厂方法,就可以写counting()而用不着写Collectors.counting()
import static java.util.stream.Collectors.*;
可以使用两个收集器,Collectors.maxBy和Collectors.minBy,来计算流中的最大或最小值。这两个收集器接收一个Comparator参数来
比较流中的元素。
Comparator<Dish> dishCaloriesComparator =
Comparator.comparingInt(Dish::getCalories);
Optional<Dish> mostCalorieDish =
menu.stream()
.collect(maxBy(dishCaloriesComparator));
Collectors类专门为汇总提供了一个工厂方法:Collectors.summingInt。它可接受一个把对象映射为求和所需int的函数,并返回一个收集器;该收集器在传递给普通的collect方法后即执行我们需要的汇总操作。
//求出菜单列表的总热量
int totalCalories = menu.stream().collect(summingInt(Dish::getCalories));
但汇总不仅仅是求和;还有Collectors.averagingInt,连同对应的averagingLong和averagingDouble可以计算数值的平均数
double avgCalories =
menu.stream().collect(averagingInt(Dish::getCalories));
很多时候,你可能想要得到两个或更多这样的结果,而且你希望只需一次操作就可以完成。在这种情况下,你可以使用summarizingInt工厂方法返回的收集器。
例如,通过一次summarizing操作你可以就数出菜单中元素的个数,并得到菜肴热量总和、平均值、最大值和最小值。
IntSummaryStatistics menuStatistics =
menu.stream().collect(summarizingInt(Dish::getCalories));
这个收集器会把所有这些信息收集到一个叫作IntSummaryStatistics的类里,它提供了方便的取值(getter)方法来访问结果。
IntSummaryStatistics{count=9, sum=4300, min=120,
average=477.777778, max=800}
同样,相应的summarizingLong和summarizingDouble工厂方法有相关的LongSummaryStatistics和DoubleSummaryStatistics类型,适用于收集的属性是原始类型long或double的情况。
joining工厂方法返回的收集器会把对流中每一个对象应用toString方法得到的所有字符串连接成一个字符串。
String shortMenu = menu.stream().map(Dish::getName).collect(joining());//把菜单中所有菜肴的名称连接起来
请注意,**joining在内部使用了StringBuilder**来把生成的字符串逐个追加起来。此外还要注意,如果Dish类有一个toString方法来返回菜肴的名称,那你无需用提取每一道菜名称的函数来对原流做映射就能够得到相同的结果:
String shortMenu = menu.stream().collect(joining());//有一个toString方法返回菜肴的名称
joining工厂方法有一个重载版本可以接受元素之间的分界符
public static Collector<CharSequence, ?, String> joining(CharSequence delimiter) {
return joining(delimiter, "", "");
}
//prefix为前缀,suffix为后缀(针对于joining的整个字符串)
public static Collector<CharSequence, ?, String> joining(CharSequence delimiter,
CharSequence prefix,
CharSequence suffix) {
return new CollectorImpl<>(
() -> new StringJoiner(delimiter, prefix, suffix),
StringJoiner::add, StringJoiner::merge,
StringJoiner::toString, CH_NOID);
}
上面的所有收集器,都是一个可以用reducing工厂方法定义的归约过程的特殊情况,Collectors.reducing工厂方法是所有这些特殊情况的一般化。
案例:用reducing方法创建的收集器来计算你菜单的总热量
int totalCalories = menu.stream().collect(reducing(
0, Dish::getCalories, (i, j) -> i + j));
需要三个参数:
同样,你可以使用下面这样单参数形式的reducing来找到热量最高的菜。
Optional<Dish> mostCalorieDish =
menu.stream().collect(reducing(
(d1, d2) -> d1.getCalories() > d2.getCalories() ? d1 : d2));
你可以把单参数reducing工厂方法创建的收集器看作三参数方法的特殊情况,它把流中的第一个项目作为起点,把恒等函数(即一个函数仅仅是返回其输入参数)作为一个转换函数。这也意味着,要是把单参数reducing收集器传递给空流的collect方法,收集器就没有起点,因此会返回一个Optional对象。
收集与规约的区别
Stream接口的collect和reduce方法有何不同,因为两种方法通常会获得相同的结果。
reduce方法来实现toListCollector所做的工作:
Stream stream = Arrays.asList(1, 2, 3, 4, 5, 6).stream();
List numbers = stream.reduce(
new ArrayList(),
(List l, Integer e) -> {
l.add(e);
return l; },
(List l1, List l2) -> {
l1.addAll(l2);
return l1; });这个解决方案有两个问题:一个语义问题和一个实际问题。语义问题在于,reduce方法旨在把两个值结合起来生成一个新值,它是一个不可变的归约。与此相反,collect方法的设计就是要改变容器,从而累积要输出的结果。这意味着,上面的代码片段是在滥用reduce方法,因为它在原地改变了作为累加器的List。
收集框架的灵活性:以不同的方法执行同样的操作
之前提到的counting收集器也是类似地利用三参数reducing工厂方法实现的。它把流中的每个元素都转换成一个值为1的Long型对象,然后再把它们相加:
public static <T> Collector<T, ?, Long> counting() {
return reducing(0L, e -> 1L, Long::sum);
}
使用泛型?通配符
在这段代码中,你可能已经注意到了?通配符,它用作counting工厂方法返回的收集器签名中的第二个泛型类型。对这种记法你应该已经很熟悉了,特别是如果你经常使用Java的集合框架的话。在这里,它仅仅意味着收集器的累加器类型未知,换句话说,累加器本身可以是任何类型。
根据情况选择最佳解决方案
收集器在某种程度上比Stream接口上直接提供的方法用起来更复杂,但好处在于它们能提供更高水平的抽象和概括,也更容易重用和自定义。尽可能为手头的问题探索不同的解决方案,但在通用的方案里面,始终选择最专门化的一个。无论是从可读性还是性能上看,这一般都是最好的决定。例如,要计菜单的总热量,我们更倾向于最后一个解决方案(使用IntStream),因为它最简明,也很可能最易读。同时,它也是性能最好的一个,因为IntStream可以让我们避免自动拆箱操作,也就是从Integer到int的隐式转换,它在这里毫无用处。
测验:
用reducing连接字符串,以下哪一种reducing收集器的用法能够合法地替代joining收集器
String shortMenu = menu.stream().map(Dish::getName).collect(joining());
(1) String shortMenu = menu.stream().map(Dish::getName)
.collect( reducing ( (s1, s2) -> s1 + s2 ) ).get();
(2) String shortMenu = menu.stream()
.collect( reducing( (d1, d2) -> d1.getName() + d2.getName() ) ).get();
(3) String shortMenu = menu.stream()
.collect( reducing( "",Dish::getName, (s1, s2) -> s1 + s2 ) );
答案:语句1和语句3是有效的,语句2无法编译。
(1) 这会将每道菜转换为菜名,就像原先使用joining收集器的语句一样。然后用一个String作为累加器归约得到的字符串流,并将菜名逐个连接在它后面。
(2) 这无法编译,因为reducing接受的参数是一个BinaryOperator,也就是一个BiFunction
(3) 这会把一个空字符串作为累加器来进行归约,在遍历菜肴流时,它会把每道菜转换成菜名,并追加到累加器上。请注意,我们前面讲过,reducing要返回一个Optional并不需要三个参数,因为如果是空流的话,它的返回值更有意义——也就是作为累加器初始值的空字
符串。
请注意,虽然语句1和语句3都能够合法地替代joining收集器。然而就实际应用而言,不管是从可读性还是性能方面考虑,我们始终建议使用joining收集器。
Map<Dish.Type, List<Dish>> dishesByType =
menu.stream().collect(groupingBy(Dish::getType));
你给groupingBy方法传递了一个Function(以方法引用的形式),它提取了流中每一道Dish的Dish.Type。我们把这个Function叫作分类函数,因为它用来把流中的元素分成不同的组。
但是,分类函数不一定像方法引用那样可用,因为你想用的分类条件可能比简单的属性访问器要复杂。例如,你可能想把热量不到400卡路里的菜划分为“低热量”(diet),热量400到700卡路里的菜划为“普通”(normal),高于700卡路里的划为“高热量”(fat)。由于Dish类的作者没有把这个操作写成一个方法,你无法使用方法引用,但你可以把这个逻辑写成Lambda表达式。
public enum CaloricLevel { DIET, NORMAL, FAT }
Map<CaloricLevel, List<Dish>> dishesByCaloricLevel = menu.stream().collect(
groupingBy(dish -> {
if (dish.getCalories() <= 400) return CaloricLevel.DIET;
else if (dish.getCalories() <= 700) return
CaloricLevel.NORMAL;
else return CaloricLevel.FAT;
} ));
目前就只根据一个标准进行分类,如果要根据多个标准进行分类,就需要使用到多级分组
可以使用一个由双参数版本的Collectors.groupingBy工厂方法创建的收集器,它除了普通的分类函数之外,还可以接受collector类型的第二个参数。要进行二级分组的话,我们可以把一个内层groupingBy传递给外层groupingBy,并定义一个为流中项目分类的二级标准。
Map<Dish.Type, Map<CaloricLevel, List<Dish>>> dishesByTypeCaloricLevel =
menu.stream().collect(
groupingBy(Dish::getType,
groupingBy(dish -> {
if (dish.getCalories() <= 400) return CaloricLevel.DIET;
else if (dish.getCalories() <= 700) return CaloricLevel.NORMAL;
else return CaloricLevel.FAT;
} )
)
);
这种多级分组操作可以扩展至任意层级,n级分组就会得到一个代表n级树形结构的n级Map
把第二个groupingBy收集器传递给外层收集器来实现多级分组。但进一步说,传递给第一个groupingBy的第二个收集器可以是任何类型,而不一定是另一个groupingBy。例如,要数一数菜单中每类菜有多少个,可以传递counting收集器作为groupingBy收集器的第二个参数:
Map<Dish.Type, Long> typesCount = menu.stream().collect(
groupingBy(Dish::getType, counting()));
还要注意,普通的单参数groupingBy(f)(其中f是分类函数)实际上是groupingBy(f, toList())的简便写法
public static <T, K> Collector<T, ?, Map<K, List<T>>>
groupingBy(Function<? super T, ? extends K> classifier) {
return groupingBy(classifier, toList());
}
public static <T, K, A, D>
Collector<T, ?, Map<K, D>> groupingBy(Function<? super T, ? extends K> classifier,
Collector<? super T, A, D> downstream) {
return groupingBy(classifier, HashMap::new, downstream);
}
public static <T, K, D, A, M extends Map<K, D>>
Collector<T, ?, M> groupingBy(Function<? super T, ? extends K> classifier,
Supplier<M> mapFactory,
Collector<? super T, A, D> downstream) {
Supplier<A> downstreamSupplier = downstream.supplier();
BiConsumer<A, ? super T> downstreamAccumulator = downstream.accumulator();
BiConsumer<Map<K, A>, T> accumulator = (m, t) -> {
K key = Objects.requireNonNull(classifier.apply(t), "element cannot be mapped to a null key");
A container = m.computeIfAbsent(key, k -> downstreamSupplier.get());
downstreamAccumulator.accept(container, t);
};
BinaryOperator<Map<K, A>> merger = Collectors.<K, A, Map<K, A>>mapMerger(downstream.combiner());
@SuppressWarnings("unchecked")
Supplier<Map<K, A>> mangledFactory = (Supplier<Map<K, A>>) mapFactory;
if (downstream.characteristics().contains(Collector.Characteristics.IDENTITY_FINISH)) {
return new CollectorImpl<>(mangledFactory, accumulator, merger, CH_ID);
}
else {
@SuppressWarnings("unchecked")
Function<A, A> downstreamFinisher = (Function<A, A>) downstream.finisher();
Function<Map<K, A>, M> finisher = intermediate -> {
intermediate.replaceAll((k, v) -> downstreamFinisher.apply(v));
@SuppressWarnings("unchecked")
M castResult = (M) intermediate;
return castResult;
};
return new CollectorImpl<>(mangledFactory, accumulator, merger, finisher, CH_NOID);
}
}
把收集器的结果转换为另一种类型
把收集器返回的结果转换为另一种类型,可以使用Collectors.collectingAndThen工厂方法返回的收集器
Map<Dish.Type, Dish> mostCaloricByType =
menu.stream()
.collect(groupingBy(Dish::getType,
collectingAndThen(
maxBy(comparingInt(Dish::getCalories)),
Optional::get)));
这个工厂方法接受两个参数——要转换的收集器以及转换函数,并返回另一个收集器。这个收集器相当于旧收集器的一个包装,collect操作的最后一步就是将返回值用转换函数做一个映射。
在这里,被包起来的收集器就是用maxBy建立的那个,而转换函数Optional::get则把返回的Optional中的值提取出来。这个操作放在这里是安全的,因为reducing收集器永远都不会返回Optional.empty()。实际上,如果菜单中没有某一类型的Dish,这个类型就不会对应一个Optional. empty()值,而且根本不会出现在Map的键中。groupingBy收集器只有在应用分组条件后,第一次在流中找到某个键对应的元素时才会把键加入分组Map中。这意味着Optional包装器在这里不是很有用,因为它不会仅仅因为它是归约收集器的返回类型而表达一个最终可能不存在却意外存在的值。
与groupingBy联合使用的其他收集器的例子
一般来说,通过groupingBy工厂方法的第二个参数传递的收集器将会对分到同一组中的所有流元素执行进一步归约操作。例如,你还重用求出所有菜肴热量总和的收集器,不过这次是对每一组Dish求和:
Map<Dish.Type, Integer> totalCaloriesByType =
menu.stream().collect(groupingBy(Dish::getType,
summingInt(Dish::getCalories)));
然而常常和groupingBy联合使用的另一个收集器是mapping方法生成的。这个方法接受两个参数:一个函数对流中的元素做变换,另一个则将变换的结果对象收集起来。其目的是在累加之前对每个输入元素应用一个映射函数,这样就可以让接受特定类型元素的收集器适应不同类型的对象。
//每种类型的Dish,菜单中都有哪些CaloricLevel
Map<Dish.Type, Set<CaloricLevel>> caloricLevelsByType =
menu.stream().collect(
groupingBy(Dish::getType, mapping(
dish -> { if (dish.getCalories() <= 400) return CaloricLevel.DIET;
else if (dish.getCalories() <= 700) return CaloricLevel.NORMAL;
else return CaloricLevel.FAT; },
toSet() )));
对于返回的Set是什么类型并没有任何保证。但通过使用toCollection,你就可以有更多的控制。例如,你可以给它传递一个构造函数引用来要求HashSet
Map<Dish.Type, Set<CaloricLevel>> caloricLevelsByType =
menu.stream().collect(
groupingBy(Dish::getType, mapping(
dish -> { if (dish.getCalories() <= 400) return CaloricLevel.DIET;
else if (dish.getCalories() <= 700) return CaloricLevel.NORMAL;
else return CaloricLevel.FAT; },
toCollection(HashSet::new) )));
分区是分组的特殊情况:由一个谓词(返回一个布尔值的函数)作为分类函数,它称分区函数。分区函数返回一个布尔值,这意味着得到的分组Map的键类型是Boolean,于是它最多可以分为两组——true是一组,false是一组。
例如,如果你是素食者或是请了一位素食的朋友来共进晚餐,可能会想要把菜单按照素食和非素食分开:
Map<Boolean, List<Dish>> partitionedMenu =
menu.stream().collect(partitioningBy(Dish::isVegetarian));
分区的好处在于保留了分区函数返回true或false的两套流元素列表。
要得到非素食Dish的List,你可以使用两个筛选操作来访问partitionedMenu这个Map中false键的值:一个利用谓词,一个利用该谓词的非。而且就像你在分组中看到的,partitioningBy工厂方法有一个重载版本,可以像下面这样传递第二个收集器:
//对于分区产生的素食和非素食子流,分别按类型对菜肴分组
Map<Boolean, Map<Dish.Type, List<Dish>>> vegetarianDishesByType =
menu.stream().collect(
partitioningBy(Dish::isVegetarian,
groupingBy(Dish::getType)));
工厂方法 | 返回类型 | 用 于 | 使用示例 |
---|---|---|---|
toList | List | 把流中所有项目收集到一个 List | List dishes = menuStream.collect(toList()); |
toSet | Set | 把流中所有项目收集到一个 Set,删除重复项 | Set dishes = menuStream.collect(toSet()); |
toCollection | Collection | 把流中所有项目收集到给定的供应源创建的集合 | Collection dishes = menuStream.collect(toCollection(),ArrayList::new); |
counting | Long | 计算流中元素的个数 | long howManyDishes = menuStream.collect(counting()); |
summingInt | Integer | 对流中项目的一个整数属性求和 | int totalCalories = menuStream.collect(summingInt(Dish::getCalories)); |
averagingInt | Double | 计算流中项目 Integer 属性的平均值 | double avgCalories = menuStream.collect(averagingInt(Dish::getCalories)); |
summarizingInt | IntSummaryStatistics | 收集关于流中项目 Integer 属性的统计值,例如最大、最小、总和与平均值 | IntSummaryStatistics menuStatistics = menuStream.collect(summarizingInt(Dish::getCalories)); |
joining | String | 连接对流中每个项目调用 toString 方法所生成的字符串 | String shortMenu = menuStream.map(Dish::getName).collect(joining(", ")); |
maxBy | Optional | 一个包裹了流中按照给定比较器选出的最大元素的 Optional,或如果流为空则为 Optional.empty() | Optional fattest = menuStream.collect(maxBy(comparingInt(Dish::getCalories))); |
minBy | Optional | 一个包裹了流中按照给定比较器选出的最小元素的 Optional,或如果流为空则为 Optional.empty() | Optional lightest = menuStream.collect(minBy(comparingInt(Dish::getCalories))); |
reducing | 归约操作产生的类型 | 从一个作为累加器的初始值开始,利用 BinaryOperator 与流中的元素逐个结合,从而将流归约为单个值 | int totalCalories = menuStream.collect(reducing(0, Dish::getCalories, Integer::sum)); |
collectingAndThen | 转换函数返回的类型 | 包裹另一个收集器,对其结果应用转换函数 | int howManyDishes = menuStream.collect(collectingAndThen(toList(), List::size)); |
groupingBy | Map |
根据项目的一个属性的值对流中的项目作问组,并将属性值作为结果 Map 的键 | Map |
partitioningBy | Map |
根据对流中每个项目应用谓词的结果来对项目进行分区 | Map |
Collector接口包含了一系列方法,为实现具体的归约操作(即收集器)提供了范本
public interface Collector<T, A, R> {
Supplier<A> supplier();
BiConsumer<A, T> accumulator();
Function<A, R> finisher();
BinaryOperator<A> combiner();
Set<Characteristics> characteristics();
}
你可以实现一个ToListCollector类,将Stream中的所有元素收集到一个List里
public class ToListCollector<T> implements Collector<T, List<T>, List<T>>
上面提到的前四个方法都会返回一个会被collect方法调用的函数,而第五个方法characteristics则提供了一系列特征,也就是一个提示列表,告诉collect方法在执行归约操作的时候可以应用哪些优化(比如并行化)。
建立新的结果容器:supplier方法
supplier方法必须返回一个结果为空的Supplier,也就是一个无参数函数,在调用时它会创建一个空的累加器实例,供数据收集过程使用。例如:
public Supplier<List<T>> supplier() {
return ArrayList::new;
}
将元素添加到结果容器:accumulator方法
accumulator方法会返回执行归约操作的函数。当遍历到流中第n个元素时,这个函数执行时会有两个参数:保存归约结果的累加器(已收集了流中的前 n-1 个项目),还有第n个元素本身。该函数将返回void,因为累加器是原位更新,即函数的执行改变了它的内部状态以体现遍历的元素的效果。例如:
public BiConsumer<List<T>, T> accumulator() {
return List::add;
}
对结果容器应用最终转换:finisher方法
在遍历完流后,finisher方法必须返回在累积过程的最后要调用的一个函数,以便将累加器对象转换为整个集合操作的最终结果。例如:
public Function<List<T>, List<T>> finisher() {
return Function.identity();
}
这三个方法已经足以对流进行顺序归约,实践中的实现细节可能还要复杂一点,一方面是因为流的延迟性质,可能在collect操作之前还需要完成其他中间操作的流水线,另一方面则是理论上可能要进行并行归约。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-xZNpCixz-1636709787895)(C:\Users\80311062\Desktop\study\java8实战\image\顺序规约.jpg)]
合并两个结果容器:combiner方法
四个方法中的最后一个——combiner方法会返回一个供归约操作使用的函数,它定义了对流的各个子部分进行并行处理时,各个子部分归约所得的累加器要如何合并。例如:
public BinaryOperator<List<T>> combiner() {
return (list1, list2) -> {
list1.addAll(list2);
return list1; }
}
有了这第四个方法,就可以对流进行并行归约了。它会用到Java 7中引入的分支/合并框架和Spliterator抽象。其并行化规约过程如下:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-UbxgJthw-1636709787897)(C:\Users\80311062\Desktop\study\java8实战\image\combiner合并工作流程.jpg)]
具体步骤如下:
characteristics方法
最后一个方法——characteristics会返回一个不可变的Characteristics集合,它定义了收集器的行为——尤其是关于流是否可以并行归约,以及可以使用哪些优化的提示。Characteristics是一个包含三个项目的枚举。
public class MyToListCollector<T> implements Collector<T, List<T>, List<T>> {
@Override
public Supplier<List<T>> supplier() {
return ArrayList::new;
}
@Override
public BiConsumer<List<T>, T> accumulator() {
return List::add;
}
@Override
public BinaryOperator<List<T>> combiner() {
return (list1, list2) -> {
list1.addAll(list2);
return list1;
};
}
@Override
public Function<List<T>, List<T>> finisher() {
return Function.identity();
}
@Override
public Set<Characteristics> characteristics() {
return Collections.unmodifiableSet(EnumSet.of(IDENTITY_FINISH, CONCURRENT));
}
}
public class TestMyToListCollector {
public static void main(String[] args) {
List<Integer> list = Arrays.asList(1, 2, 3, 4, 5);
List<Integer> collect = list.stream().collect(new MyToListCollector<Integer>());
System.out.println(collect);
//unordered
}
}
这个实现和标准的List dishes = menuStream.collect(toList()); 构造之间的其他差异在于toList是一个工厂,而ToListCollector必须用new来实例化。
对于IDENTITY_FINISH的收集操作,还有一种方法可以得到同样的结果而无需从头实现新的Collectors接口。Stream有一个重载的collect方法可以接受另外三个函数——supplier、accumulator和combiner,其语义和Collector接口的相应方法返回的函数完全相同。
List<Integer> collect = list.stream().collect(
ArrayList::new,
List::add,
List::addAll);
第二种形式虽然比前一个写法更为紧凑和简洁,却不那么易读。此外,以恰当的类来实现自己的自定义收集器有助于重用并可避免代码重复。另外值得注意的是,这第二个collect方法不能传递任何Characteristics,所以它永远都是一个IDENTITY_FINISH和CONCURRENT但并非UNORDERED的收集器。
案例:将前n个自然数按质数和非质数分区
1.通过IntStream.rangeClosed(2, (int) Math.sqrt((double) candidate))来生成小于被除数平方根的整数,但有一个问题,因为我们只判断是否为质数,所以除数不是质数就不需要考虑,但生成流中并没有这个方法;如果使用filter需要处理整个流才能返回结果,如果流很大就存在问题,所以我们可以自定义收集器来生成这些质数
public class PrimeNumbersCollector
implements Collector<Integer,
Map<Boolean, List<Integer>>,
Map<Boolean, List<Integer>>> {
@Override
public Supplier<Map<Boolean, List<Integer>>> supplier() {
return () -> new HashMap<Boolean, List<Integer>>() {{
put(true, new ArrayList<Integer>());
put(false, new ArrayList<Integer>());
}};
}
@Override
public BiConsumer<Map<Boolean, List<Integer>>, Integer> accumulator() {
return (Map<Boolean, List<Integer>> acc, Integer candidate) -> {
acc.get( isPrime( acc.get(true),
candidate) )
.add(candidate);
};
}
/*
*实际上这个收集器是不能并行使用的,因为该算法本身是顺序的。这意味着永远都
*不会调用combiner方法,你可以把它的实现留空(更好的做法是抛出一个UnsupportedOperationException异常)。
*/
@Override
public BinaryOperator<Map<Boolean, List<Integer>>> combiner() {
return (Map<Boolean, List<Integer>> map1,
Map<Boolean, List<Integer>> map2) -> {
map1.get(true).addAll(map2.get(true));
map1.get(false).addAll(map2.get(false));
return map1;
};
}
@Override
public Function<Map<Boolean, List<Integer>>,
Map<Boolean, List<Integer>>> finisher() {
return Function.identity();
}
@Override
public Set<Characteristics> characteristics() {
return Collections.unmodifiableSet(EnumSet.of(IDENTITY_FINISH));
}
}
并行流就是一个把内容分成多个数据块,并用不同的线程分别处理每个数据块的流。
案例:求从1到n的和
public static long parallelSum(long n) {
return Stream.iterate(1L, i -> i + 1)
.limit(n)
.parallel()
.reduce(0L, Long::sum);
}
Stream在内部分成了几块,对不同的块独立并行进行归纳操作,最后,同一个归纳操作将子流的部分归纳结果合并起来。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-KZg5ifsc-1636709787897)(C:\Users\80311062\AppData\Roaming\Typora\typora-user-images\image-20211112130634844.png)]
注意:对顺序流调用parallel方法对流本身没有任何实际的变化,只是在内部设了一个boolean的标志,表示想让调用parallel之后进行的所有操作都并行执行,同样可以对并行流调用sequential方法就可以变成顺序流。但是这并不意味着你可以通过这两个方法来控制流的并行和顺序操作,例如:
stream.parallel()
.filter(...)
.sequential()
.map(...)
.parallel()
.reduce();
只有在最后一次parallel或sequential调用才会影响整个流水线,所以,该上例只会并行执行
配置并行流使用的线程池
并行流内部使用了默认的ForkJoinPool,线程数量就是你的处理器数量( Runtime.getRuntime().availableProcessors())。
可以通过系统属性System.setProperty(“java.util.concurrent.ForkJoinPool.common.parallelism”,“12”); 来改变线程池大小,这是全局设置,将影响代码中的所有的并行流,目前还无法为某个并行流指定,让ForkJoinPool的大小等于处理器数量比较好。
public class TestMeasureStreamPerformance03 {
public static void main(String[] args) {
long n = 1000000;
testSumPerformanceWithForEach(n);
testSumPerformanceWithSequence(n, Long::sum);
testSumPerformanceWithParallel(n, Long::sum);
}
private static void testSumPerformanceWithForEach(long n) {
long fastest = Long.MAX_VALUE;
long startTime = System.nanoTime();
long result = 0;
for (long i = 1; i <= n; i++) {
result += i;
}
long endTime = System.nanoTime();
long time = (endTime - startTime) / 1000000;
if (time > fastest) {
time = fastest;
}
System.out.println(MessageFormat.format("the sum:{0},foreach time:{1}", result, time));
}
private static void testSumPerformanceWithSequence(long n, BinaryOperator<Long> opr) {
long fastest = Long.MAX_VALUE;
long startTime = System.nanoTime();
Long result = Stream.iterate(1L, i -> i + 1).limit(n).reduce(0L, opr);
long endTime = System.nanoTime();
long time = (endTime - startTime) / 1000000;
if (time > fastest) {
time = fastest;
}
System.out.println(MessageFormat.format("the sum:{0},sequence time:{1}", result, time));
}
public static void testSumPerformanceWithParallel(long n, BinaryOperator<Long> opr) {
long fastest = Long.MAX_VALUE;
long startTime = System.nanoTime();
Long result = Stream.iterate(1L, i -> i + 1).limit(n).parallel().reduce(0L, opr);
long endTime = System.nanoTime();
long time = (endTime - startTime) / 1000000;
if (time > fastest) {
time = fastest;
}
System.out.println(MessageFormat.format("the sum:{0},parallel time:{1}", result, time));
}
}
并发速度并不一定块,要看装箱和拆箱,以及并发的开销等一系列因素
iterate生成的是装箱的对象,必须拆箱成数字才能求和。我们很难把iterate分成多个独立块来并行执行,因为每次应用这个函数都要依赖前一次应用的结果,,因而无法有效地把流划分为小块来并行处理
void main(String[] args) {
long n = 1000000;
testSumPerformanceWithForEach(n);
testSumPerformanceWithSequence(n, Long::sum);
testSumPerformanceWithParallel(n, Long::sum);
}
private static void testSumPerformanceWithForEach(long n) {
long fastest = Long.MAX_VALUE;
long startTime = System.nanoTime();
long result = 0;
for (long i = 1; i <= n; i++) {
result += i;
}
long endTime = System.nanoTime();
long time = (endTime - startTime) / 1000000;
if (time > fastest) {
time = fastest;
}
System.out.println(MessageFormat.format("the sum:{0},foreach time:{1}", result, time));
}
private static void testSumPerformanceWithSequence(long n, BinaryOperator<Long> opr) {
long fastest = Long.MAX_VALUE;
long startTime = System.nanoTime();
Long result = Stream.iterate(1L, i -> i + 1).limit(n).reduce(0L, opr);
long endTime = System.nanoTime();
long time = (endTime - startTime) / 1000000;
if (time > fastest) {
time = fastest;
}
System.out.println(MessageFormat.format("the sum:{0},sequence time:{1}", result, time));
}
public static void testSumPerformanceWithParallel(long n, BinaryOperator<Long> opr) {
long fastest = Long.MAX_VALUE;
long startTime = System.nanoTime();
Long result = Stream.iterate(1L, i -> i + 1).limit(n).parallel().reduce(0L, opr);
long endTime = System.nanoTime();
long time = (endTime - startTime) / 1000000;
if (time > fastest) {
time = fastest;
}
System.out.println(MessageFormat.format("the sum:{0},parallel time:{1}", result, time));
}
}
并发速度并不一定块,要看装箱和拆箱,以及并发的开销等一系列因素
iterate生成的是装箱的对象,必须拆箱成数字才能求和。我们很难把iterate分成多个独立块来并行执行,因为每次应用这个函数都要依赖前一次应用的结果,,因而无法有效地把流划分为小块来并行处理
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-St2rgOnt-1636709787898)(C:\Users\80311062\AppData\Roaming\Typora\typora-user-images\image-20211112161033138.png)]
针对上述iterate存在的问题,可采用LongStream.rangeClosed方法:1.直接生成原始类型的long数字,没有装箱拆箱的开销。2.会生成数字范围,很容易拆分为独立的小块。
public static void testMeasureStreamPerformanceWithRangeClosed(long n, LongBinaryOperator sum) {
long fastest = Long.MAX_VALUE;
long startTime = System.nanoTime();
long result = LongStream.rangeClosed(0L, n).parallel().reduce(0L, sum);
long endTime = System.nanoTime();
long time = (endTime - startTime) / 1000000;
if (time > fastest) {
time = fastest;
}
System.out.println(MessageFormat.format("the sum with rangeClosed:{0},parallel time:{1}", result, time));
}
并行化并不是没有代价的,并行化过程本身需要对流做递归划分,把每个子流的归纳操作分配到不同的线程,然后把这些操作的结果合并成一个值。但在多个内核之间移动数据的代价也可能比你想的要大,所以很重要的一点是要保证在内核中并行执行工作的时间比在内核之间传输数据的时间长总而言之,很多情况下不可能或不方便并行化。然而,在使用并行Stream加速代码之前,你必须确保用得对;如果结果错了,算得快就毫无意义了
错用并行流而产生错误的首要原因,就是使用的算法改变了某些共享状态。
public static long sideEffectParallelSum(long n) {
Accumulator accumulator = new Accumulator();
LongStream.rangeClosed(1, n).parallel().forEach(accumulator::add);
return accumulator.total;
}
public class Accumulator {
public long total = 0;
public void add(long value) { total += value; }
}
total作为共享变量,在并发时存在竞争
源 | 可分解性 |
---|---|
ArrayList | 极佳 |
LinkedList | 差 |
IntStream.range | 极佳 |
Stream.iterate | 差 |
HashSet | 好 |
TreeSet | 好 |
调并行流背后使用的基础架构是Java 7中引入的分支/合并框架
分支/合并框架的目的是以递归方式将可以并行的任务拆分成更小的任务,然后将每个子任务的结果合并起来生成整体结果。它是ExecutorService接口的一个实现,它把子任务分配给线程池(称为ForkJoinPool)中的工作线程。
要把任务提交到这个池,必须创建RecursiveTask的一个子类,其中R是并行化任务(以及所有子任务)产生的结果类型,或者如果任务不返回结果,则是RecursiveAction类型(当然它可能会更新其他非局部机构)。要定义RecursiveTask,只需实现它唯一的抽象方法compute:
这个方法同时定义了将任务拆分成子任务的逻辑,以及无法再拆分或不方便再拆分时,生成单个子任务结果的逻辑。
if (任务足够小或不可分) {
顺序计算该任务
} else {
将任务分成两个子任务
递归调用本方法,拆分每个子任务,等待所有子任务完成
合并每个子任务的结果
}
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-iJubYi6S-1637311316113)(C:\Users\80311062\Desktop\study\java8实战\image\image-20211116115100652.png)]
/**
* 计算求和
*
* @modifier:
* @date:2021/11/17 14:21
* @modifierUser: 修改人 modifierDate: 修改时间
*/
public class ForkJoinPoolSumRecursiveTask extends RecursiveTask<Long> {
private int start;
private int end;
private long[] data;
private final int minDivision = 3;
public ForkJoinPoolSumRecursiveTask(int start, int end, long[] data) {
this.start = start;
this.end = end;
this.data = data;
}
@Override
protected Long compute() {
int length = end - start;
if (length < minDivision) {
long result = 0;
for (int i = start; i < end; i++) {
result += data[i];
}
return result;
}
ForkJoinPoolSumRecursiveTask left = new ForkJoinPoolSumRecursiveTask(start, start + length / 2, data);
left.fork();
ForkJoinPoolSumRecursiveTask right = new ForkJoinPoolSumRecursiveTask(start + length / 2, end, data);
Long rightSum = right.compute();
Long leftSum = left.join();
return leftSum + rightSum;
}
}
请注意在实际应用时,使用多个ForkJoinPool是没有什么意义的。正是出于这个原因,一般来说把它实例化一次,然后把实例保存在静态字段中,使之成为单例,这样就可以在软件中任何部分方便地重用了。
分出大量的小任务一般来说都是一个好的选择。这是因为,理想情况下,划分并行任务时, 应该让每个任务都用完全相同的时间完成,让所有的CPU内核都同样繁忙。不幸的是,实际中,每 个子任务所花的时间可能天差地别,要么是因为划分策略效率低,要么是有不可预知的原因,比如 磁盘访问慢,或是需要和外部服务协调执行。
分支/合并框架工程用一种称为工作窃取(work stealing)的技术来解决这个问题。在实际应 用中,这意味着这些任务差不多被平均分配到ForkJoinPool中的所有线程上 。每个线程都为分 配给它的任务保存一个双向链式队列,每完成一个任务,就会从队列头上取出下一个任务开始执 行。基于前面所述的原因,某个线程可能早早完成了分配给它的所有任务,也就是它的队列已经空了,而其他的线程还很忙。这时,这个线程并没有闲下来,而是随机选了一个别的线程,从队列的尾巴上“偷走”一个任务。这个过程一直继续下去,直到所有的任务都执行完毕,所有的队列都清空。这就是为什么要划成许多小任务而不是少数几个大任务,这有助于更好地在工作线程 之间平衡负载。
一般来说,这种工作窃取算法用于在池中的工作线程之间重新分配和平衡任务。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-KM09iNn8-1637811439393)(C:\Users\MSI\AppData\Roaming\Typora\typora-user-images\image-20211121223752544.png)]
Spliterator是Java 8中加入的另一个新接口;这个名字代表“可分迭代器”(splitable iterator)。和Iterator一样,Spliterator也用于遍历数据源中的元素,但它是为了并行执行 而设计的。
Java 8已经为集合框架中包含的所有数据结构提供了一个 默认的Spliterator实现。集合实现了Spliterator接口,接口提供了一个spliterator方法。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-urMHgqwU-1637811439396)(C:\Users\MSI\AppData\Roaming\Typora\typora-user-images\image-20211121224428143.png)]
tryAdvance方法的行为类似于普通的 Iterator,因为它会按顺序一个一个使用Spliterator中的元素,并且如果还有其他元素要遍 历就返回true。但trySplit是专为Spliterator接口设计的,因为它可以把一些元素划出去分 给第二个Spliterator(由该方法返回),让它们两个并行处理。Spliterator还可通过 estimateSize方法估计还剩下多少元素要遍历,因为即使不那么确切,能快速算出来是一个值 也有助于让拆分均匀一点。
将Stream拆分成多个部分的算法是一个递归过程。
第一步是对第一个 Spliterator调用trySplit,生成第二个Spliterator。第二步对这两个Spliterator调用 trysplit,这样总共就有了四个Spliterator。这个框架不断对Spliterator调用trySplit 直到它返回null,表明它处理的数据结构不能再分割,如第三步所示。最后,这个递归拆分过 程到第四步就终止了,这时所有的Spliterator在调用trySplit时都返回了null。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-pRt1rOQj-1637811439398)(C:\Users\MSI\AppData\Roaming\Typora\typora-user-images\image-20211121224958348.png)]
这个拆分过程也受Spliterator本身的特性影响,而特性是通过characteristics方法声明的。
Spliterator接口声明的最后一个抽象方法是characteristics,它将返回一个int,代 表Spliterator本身特性集的编码。使用Spliterator的客户可以用这些特性来更好地控制和 优化它的使用。下表总结了这些特性。(不幸的是,虽然它们在概念上与收集器的特性有重叠, 编码却不一样。)
特 性 | 含 义 |
---|---|
ORDERED | 元素有既定的顺序(例如List),因此Spliterator在遍历和划分时也会遵循这一顺序 |
DISTINCT | 对于任意一对遍历过的元素x和y,x.equals(y)返回false |
SORTED | 遍历的元素按照一个预定义的顺序排序 |
SIZED | 该Spliterator由一个已知大小的源建立(例如Set),因此estimatedSize()返回的是准确值 |
NONNULL | 保证遍历的元素不会为null |
IMMUTABLE | Spliterator的数据源不能修改。这意味着在遍历时不能添加、删除或修改任何元素 |
CONCURRENT | 该Spliterator的数据源可以被其他线程同时修改而无需同步 |
SUBSIZED | 该Spliterator和所有从它拆分出来的Spliterator都是SIZED |
public class TestWordCounterSpliterator {
public static void main(String[] args) {
String s = "this is a big count is very interesting this is a big count is very interesting ";
Spliterator<Character> spliterator = new WordCounterSpliterator(s);
WordCounter reduce = StreamSupport.stream(spliterator, true)//true会声明后面操作为并行
.reduce(new WordCounter(0, true), WordCounter::accumulate, WordCounter::combine);
System.out.println(reduce.getCount());
}
}
public class WordCounterSpliterator implements Spliterator<Character> {
private final String sentence;//需要判断的句子
private int currentIndex = 0;//标识句子的当前位置
private final int endCondition = 10;//拆分的结束条件
public WordCounterSpliterator(String sentence) {
this.sentence = sentence;//初始化
}
/**
*
*/
@Override
public boolean tryAdvance(Consumer<? super Character> action) {//action用来处理
action.accept(sentence.charAt(currentIndex++));
return currentIndex < sentence.length();
}
@Override
public Spliterator<Character> trySplit() {
int currentSize = sentence.length() - currentIndex;
if (currentSize < endCondition) {
return null;
}
for (int i = currentIndex + currentSize / 2; i < sentence.length(); i++) {
if (Character.isWhitespace(sentence.charAt(i))) {
WordCounterSpliterator wordCounterSpliterator = new WordCounterSpliterator(sentence.substring(currentIndex, i));
currentIndex = i;
return wordCounterSpliterator;
}
}
return null;
}
@Override
public long estimateSize() {
return sentence.length() - currentIndex;
}
@Override
public int characteristics() {
return Spliterator.ORDERED + SIZED + SUBSIZED + NONNULL + IMMUTABLE;
}
}
Lambda表达式可以帮助我们用更紧凑的方式描述程序的行为。采用Lambda表达式之后,你的代码会变得更加灵活,因为Lambda表达式鼓励使用行为参数化的方式。在这种方式下,应对需求的变化时,你的代码可以依据传入的参数动态选择和执行相应的行为。
三种简单的重构
但是某些情况下,将匿名类转换为Lambda表达式可能是一个比较复杂的过程。首先**,匿名类和Lambda表达式中的this和super的含义是不同的。在匿名类中,this代表的是类自身,但是在Lambda中,它代表的是包含类**。其次,匿名类可以屏蔽包含类的变量,而Lambda表达式不能(它们会导致编译错误)。
int a = 10;
Runnable r1 = () -> {
int a = 2; //编译错误
System.out.println(a);
};
Runnable r2 = new Runnable(){
public void run(){
int a = 2; //正常
System.out.println(a);
}
};
在涉及重载的上下文里,将匿名类转换为Lambda表达式可能导致最终的代码更加晦涩。实际上,匿名类的类型是在初始化时确定的,而Lambda的类型取决于它的上下文。
public class TestLambdaOverload {
public static void main(String[] args) {
print((TaskLambdaOverload) () -> System.out.println("aaaaa"));//满足两个,不知道调用哪个,但是可以显示指定
}
public static void print(Runnable runnable) {
runnable.run();
}
public static void print(TaskLambdaOverload taskLambdaOverload) {//重载
taskLambdaOverload.print();
}
}
public interface TaskLambdaOverload {
/**
* 打印
*/
void print();
}
Lambda表达式非常适用于需要传递代码片段的场景。不过,为了改善代码的可读性,也请尽量使用方法引用。因为方法名往往能更直观地表达代码的意图。
如果你发现你需要频繁地从客户端代码去查询一个对象的状态(比如前文例子中的日志器的状态),只是为了传递参数、调用该对象的一个方法(比如输出一条日志),那么可以考虑实现一个新的方法,以Lambda或者方法表达式作为参数,新方法在检查完该对象的状态之后才调用原来的方法。你的代码会因此而变得更易读(结构更清晰),封装性更好(对象的状态也不会暴露给客户端代码了)。
如果你发现虽然你的业务代码千差万别,但是它们拥有同样的准备和清理阶段,这时,你完全可以将这部分代码用Lambda实现。这种方式的好处是可以重用准备和清理阶段的逻辑,减少重复冗余的代码。
例如:打开和关闭文件时使用了同样的逻辑,但在处理文件时可以使用不同的Lambda进行参数化。
String oneLine = processFile((BufferedReader b) -> b.readLine());
String twoLines = processFile((BufferedReader b) -> b.readLine() + b.readLine());
public static String processFile(BufferedReaderProcessor p) throws IOException {
try(BufferedReader br = new BufferedReader(new FileReader("java8inaction/chap8/data.txt"))){
return p.process(br);
}
}
public interface BufferedReaderProcessor{
String process(BufferedReader b) throws IOException;
}
策略模式
策略模式代表了解决一类算法的通用解决方案,你可以在运行时选择使用哪种方案。
策略模式包含三部分内容
一个代表某个算法的接口(它是策略模式的接口)
一个或多个该接口的具体实现,它们代表了算法的多种实现
一个或多个使用策略对象的客户
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-fIjkp9lo-1638961655486)(C:\Users\80311062\Desktop\study\java8实战\image\image-20211206153349421.png)]
代码实现:
public class Validator{
private final ValidationStrategy strategy;
public Validator(ValidationStrategy v){
this.strategy = v;
}
public boolean validate(String s){
return strategy.execute(s);
}
}
Validator numericValidator = new Validator(new IsNumeric());
boolean b1 = numericValidator.validate("aaaa");
Validator lowerCaseValidator = new Validator(new IsAllLowerCase ());
boolean b2 = lowerCaseValidator.validate("bbbb");
public interface ValidationStrategy {
boolean execute(String s);
}
public class IsAllLowerCase implements ValidationStrategy {
public boolean execute(String s){
return s.matches("[a-z]+");
}
}
public class IsNumeric implements ValidationStrategy {
public boolean execute(String s){
return s.matches("\\d+");
}
}
使用Lambda表达式
Validator numericValidator =
new Validator((String s) -> s.matches("[a-z]+"));
boolean b1 = numericValidator.validate("aaaa");
Validator lowerCaseValidator =
new Validator((String s) -> s.matches("\\d+"));
boolean b2 = lowerCaseValidator.validate("bbbb");
模板方法
如果你需要采用某个算法的框架,同时又希望有一定的灵活度,能对它的某些部分进行改进,那么采用模板方法设计模式是比较通用的方案。希望使用这个算法,但是需要对其中的某些行进行改进,才能达到希望的效果。
案例:
通常,用户需要输入一个用户账户,之后应用才能从银行的数据库中得到用户的详细信息,最终完成一些让用户满意的操作。不同分行的在线银行应用让客户满意的方式可能还略有不同,比如给客户的账户发放红利,或者仅仅是少发送一些推广文件。你可能通过下面的抽象类方式来实现在线银行应用。
代码实现:
abstract class OnlineBanking {
public void processCustomer(int id){
Customer c = Database.getCustomerWithId(id);
makeCustomerHappy(c);
}
abstract void makeCustomerHappy(Customer c);
}
processCustomer方法搭建了在线银行算法的框架:获取客户提供的ID,然后提供服务让用户满意。不同的支行可以通过继承OnlineBanking类,对该方法提供差异化的实现。
Lambda实现
public void processCustomer(int id, Consumer<Customer> makeCustomerHappy){
Customer c = Database.getCustomerWithId(id);
makeCustomerHappy.accept(c);
}
new OnlineBankingLambda().processCustomer(1337, (Customer c) ->
System.out.println("Hello " + c.getName());
现在,可以很方便地通过传递Lambda表达式,直接插入不同的行为,不再需要继承OnlineBanking类了。
观察者模式
观察者模式是一种比较常见的方案,某些事件发生时(比如状态转变),如果一个对象(通常我们称之为主题)需要自动地通知其他多个对象(称为观察者),就会采用该方案。创建图形用户界面(GUI)程序时,你经常会使用该设计模式。这种情况下,你会在图形用户界面组件(比如按钮)上注册一系列的观察者。如果点击按钮,观察者就会收到通知,并随即执行某个特定的行为。 但是观察者模式并不局限于图形用户界面。比如,观察者设计模式也适用于股票交易的情形,多个券商可能都希望对某一支股票价格(主题)的变动做出响应。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-1ppn1dDl-1640088062903)(C:\Users\80311062\Desktop\study\java8实战\image\image-20211206170442092.png)]
案例:
好几家报纸机构,比如《纽约时报》《卫报》以及《世界报》都订阅了新闻,他们希望当接收的新闻中包含他们感兴趣的关键字时,能得到特别通知。
interface Observer { //观察者接口,它将不同的观察者聚合在一起,仅有一个名为notify的方法,一旦接收到一条新的新闻,该方法就会被调用
void notify(String tweet);
}
定义不同的观察者:
class NYTimes implements Observer{
public void notify(String tweet) {
if(tweet != null && tweet.contains("money")){
System.out.println("Breaking news in NY! " + tweet);
}
}
}
class Guardian implements Observer{
public void notify(String tweet) {
if(tweet != null && tweet.contains("queen")){
System.out.println("Yet another news in London... " + tweet);
}
}
}
class LeMonde implements Observer{
public void notify(String tweet) {
if(tweet != null && tweet.contains("wine")){
System.out.println("Today cheese, wine and news! " + tweet);
}
}
}
主题注册观察者和通知
interface Subject{
void registerObserver(Observer o);
void notifyObservers(String tweet);
}
class Feed implements Subject{
private final List<Observer> observers = new ArrayList<>();
public void registerObserver(Observer o) {
this.observers.add(o);
}
public void notifyObservers(String tweet) {
observers.forEach(o -> o.notify(tweet));
}
}
使用Lambda表达式:使用Lambda表达式后,无需显式地实例化三个观察者对象,直接传递Lambda表达式表示需要执行的行为
f.registerObserver((String tweet) -> {
if(tweet != null && tweet.contains("money")){
System.out.println("Breaking news in NY! " + tweet);
}
});
f.registerObserver((String tweet) -> {
if(tweet != null && tweet.contains("queen")){
System.out.println("Yet another news in London... " + tweet);
}
});
注意:
并不是所有情况都要使用Lambda表达式,例如如果这里的观察者的逻辑有可能十分复杂,它们可能还持有状态,抑或定义了多个方法,诸如此类。在这些情形下,还是应该继续使用类的方式。
责任链模式
责任链模式是一种创建处理对象序列(比如操作序列)的通用方案。一个处理对象可能需要在完成一些工作之后,将结果传递给另一个对象,这个对象接着做一些工作,再转交给下一个处理对象,以此类推。通常,这种模式是通过定义一个代表处理对象的抽象类来实现的,在抽象类中会定义一个字段来记录后续对象。一旦对象完成它的工作,处理对象就会将它的工作转交给它的后继。
工厂模式
使用工厂模式,你无需向客户暴露实例化的逻辑就能完成对象的创建。
非常复杂的Lambda表达式,包含大量的业务逻辑,比如需要处理复杂情况的定价算法。你无法在测试程序中引用Lambda表达式,这种情况该如何处理呢?一种策略是将Lambda表达式转换为方法引用(这时往往需要声明一个新的常规方法)。
如果一个方法接受Lambda表达式作为参数,可以采用的一个方案是使用不同的Lambda表达式对它进行测试。
错误发生在Lambda表达式内部。由于Lambda表达式没有名字,所以编译器只能为它们指定一个名字。
如果方法引用指向的是同一个类中声明的方法,那么它的名称是可以在栈跟踪中显示的
List<Integer> numbers = Arrays.asList(2, 3, 4, 5);
numbers.stream()
.map(x -> x + 17)
.filter(x -> x % 2 == 0)
.limit(3)
.forEach(System.out::println);
一旦调用forEach,整个流就会恢复运行。到底哪种方式能更有效地帮助我们理解Stream流水线中的每个操作(比如map、filter、limit)产生的输出?流操作方法peek,peek的设计初衷就是在流的每个元素恢复运行之前,插入执行一个动作。但是它不像forEach那样恢复整个流的运行,而是在一个元素上完成操作之后。它只会将操作顺承到流水线中的下一个操作。流提供的peek方法在分析Stream流水线时,能将中间变量的值输出到日志中,是非常有用的工具。
List<Integer> result =
numbers.stream()
.peek(x -> System.out.println("from stream: " + x))//输出来自数据源的当前元素值
.map(x -> x + 17)
.peek(x -> System.out.println("after map: " + x))//输出map操作的结果
.filter(x -> x % 2 == 0)
.peek(x -> System.out.println("after filter: " + x))//输出经过filter操作之后,当前元素值
.limit(3)
.peek(x -> System.out.println("after limit: " + x))//输出经过limit操作之后,当前元素值
.collect(toList());
通过peek操作我们能清楚地了解流水线操作中每一步的输出结果:
from stream: 2
after map: 19
from stream: 3
after map: 20
after filter: 20
after limit: 20
from stream: 4
after map: 21
from stream: 5
after map: 22
after filter: 22
after limit: 22
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-oJ57uMTe-1640088062906)(C:\Users\80311062\Desktop\study\java8实战\image\image-20211221165431042.png)]
一,Java 8允许在接口内声明静态方法。
二,Java 8引入了一个新功能,叫默认方法,通过默认方法可以指定接口方法的默认实现。换句话说,接口能提供方法的具体实现。
变更对Java程序的影响大体可以分成三种类型的兼容性,分别是:二进制级的兼容、源代码级的兼容,以及函数行为的兼容。刚才我们看到,向接口添加新方法是二进制级的兼容,但最终编译实现接口的类时却会发生编译错误。了解不同类型兼容性的特性是非常有益的,下面我们会深入介绍这部分的内容。
二进制级的兼容性表示**现有的二进制执行文件能无缝持续链接(包括验证、准备和解析)和运行。**比如,为接口添加一个方法就是二进制级的兼容,这种方式下,如果新添加的方法不被调用,接口已经实现的方法可以继续运行,不会出现错误,编译实现接口的类时会发生编译错误。
简单地说,源代码级的兼容性表示引入变化之后,现有的程序依然能成功编译通过。比如,向接口添加新的方法就不是源码级的兼容,因为遗留代码并没有实现新引入的方法,所以它们无法顺利通过编译。
最后,函数行为的兼容性表示变更发生之后,程序接受同样的输入能得到同样的结果。比如,为接口添加新的方法就是函数行为兼容的,因为新添加的方法在程序中并未被调用(抑或该接口在实现中被覆盖了)。
类实现了接口,不过却刻意地将一些方法的实现留白。例如Iterator接口定义了hasNext、next,还定义了remove方法。Java 8之前,由于用户通常不会使用该方法,remove方法常被忽略。因此,实现Interator接口的类通常会为remove方法放置一个空的实现,这些都是些毫无用处的模板代码。
采用默认方法之后,你可以为这种类型的方法提供一个默认的实现,这样实体类就无需在自己的实现中显式地提供一个空方法。比如,在Java 8中,Iterator接口就为remove方法提供了一个默认实现,如下所示:
interface Iterator<T> {
boolean hasNext();
T next();
default void remove() {
throw new UnsupportedOperationException();
}
}
通过这种方式,你可以减少无效的模板代码。实现Iterator接口的每一个类都不需要再声明一个空的remove方法了,因为它现在已经有一个默认的实现。
默认方法让之前无法想象的事儿以一种优雅的方式得以实现,即行为的多继承。这是一种让类从多个来源重用代码的能力。
public class ArrayList<E> extends AbstractList<E>
implements List<E>, RandomAccess, Cloneable,
Serializable, Iterable<E>, Collection<E> {
}
类型的多继承
这个例子中ArrayList继承了一个类,实现了六个接口。因此ArrayList实际是七个类型的直接子类,分别是:AbstractList、List、RandomAccess、Cloneable、Serializable、Iterable和Collection。所以,在某种程度上,我们早就有了类型的多继承。
由于Java 8中接口方法可以包含实现,类可以从多个接口中继承它们的行为(即实现的代码)。
让我们从一个例子入手,看看如何充分利用这种能力来为我们服务。保持接口的精致性和正交性能帮助你在现有的代码基上最大程度地实现代码复用和行为组合。
利用正交方法的精简接口
假设你需要为你正在创建的游戏定义多个具有不同特质的形状。有的形状需要调整大小,但是不需要有旋转的功能;有的需要能旋转和移动,但是不需要调整大小。这种情况下,你怎么设计才能尽可能地重用代码?
你可以定义一个单独的Rotatable接口,并提供两个抽象方法setRotationAngle和getRotationAngle,如下所示:
public interface Rotatable {
void setRotationAngle(int angleInDegrees);
int getRotationAngle();
default void rotateBy(int angleInDegrees){
setRotationAngle((getRotationAngle () + angle) % 360);
}
}
这种方式和模板设计模式有些相似,都是以其他方法需要实现的方法定义好框架算法。
现在,实现了Rotatable的所有类都需要提供setRotationAngle和getRotationAngle的实现,但与此同时它们也会天然地继rotateBy的默认实现。类似地,你可以定义之前看到的两个接口Moveable和Resizable。它们都包含了默认实现。下面是Moveable的代码:
public interface Moveable {
int getX();
int getY();
void setX(int x);
void setY(int y);
default void moveHorizontally(int distance){
setX(getX() + distance);
}
default void moveVertically(int distance){
setY(getY() + distance);
}
}
组合接口
通过组合这些接口,你现在可以为你的游戏创建不同的实体类。比如,Monster可以移动、旋转和缩放。
public class Monster implements Rotatable, Moveable, Resizable {
}
继承不应该成为你一谈到代码复用就试图倚靠的万精油。比如,从一个拥有100个方法及字段的类进行继承就不是个好主意,因为这其实会引入不必要的复杂性。你完全可以使用代理有效地规避这种窘境,即创建一个方法通过该类的成员变量直接调用该类的方法。这就是为什么有的时候我们发现有些类被刻意地声明为final类型:声明为final的类不能被其他的类继承,避免发生这样的反模式,防止核心代码的功能被污染。注意,有的时候声明为final的类都会有其不同的原因,比如,String类被声明为final,因为我们不希望有人对这样的核心
功能产生干扰。这种思想同样也适用于使用默认方法的接口。通过精简的接口,你能获得最有效的组合,因为你可以只选择你需要的实现。
如果一个类使用相同的函数签名从多个地方(比如另一个类或接口)继承了方法,通过三条规则可以进行判断。
(1) 类中的方法优先级最高。类或父类中声明的方法的优先级高于任何声明为默认方法的优先级。
(2) 如果无法依据第一条进行判断,那么子接口的优先级更高:函数签名相同时,优先选择拥有最具体实现的默认方法的接口,即如果B继承了A,那么B就比A更加具体。
(3) 最后,如果还是无法判断,继承了多个接口的类必须通过显式覆盖和调用期望的方法显式地选择使用哪一个默认方法的实现
public interface A {
default void hello() {
System.out.println("Hello from A");
}
}
public interface B extends A {
default void hello() {
System.out.println("Hello from B");
}
}
public class C implements B, A {
public static void main(String... args) {
new C().hello(); //Hello from B
}
}
public class D implements A{ }
public class C extends D implements B, A {
public static void main(String... args) {
new C().hello();
}
}
依据规则(1),类中声明的方法具有更高的优先级。D并未覆盖hello方法,可是它实现了接口A。所以它就拥有了接口A的默认方法。规则(2)说如果类或者父类没有对应的方法,那么就应该选择提供了最具体实现的接口中的方法。因此,编译器会在接口A和接口B的hello方法之间做选择。由于B更加具体,所以程序会再次打印输出“Hello from B”。
public interface A {
void hello() {
System.out.println("Hello from A");
}
}
public interface B {
void hello() {
System.out.println("Hello from B");
}
}
public class C implements B, A { }
A接口和B接口的hello方法都是有效的选项。所以,Java编译器这时就会抛出一个编译错误,因为它无法判断哪一个方法更合适:“Error: class C inherits unrelated defaults for hello() from types B and A."
public class C implements B, A {
void hello(){
B.super.hello();
}
}
Java 8中引入了一种新的语法X.super.m(…),其中X是你希望调用的m方法所在的父接口
public interface A{
default void hello(){
System.out.println("Hello from A");
}
}
public interface B extends A { }
public interface C extends A { }
public class D implements B, C {
public static void main(String... args) {
new D().hello();
}
}
这种情况下类D中的默认方法到底继承自什么地方 ——源自B的默认方法,还是源自C的默认方法?实际上只有一个方法声明可以选择。只有A声明了一个默认方法。由于这个接口是D的父接口,代码会打印输出“Hello from A”。
如果你在C接口中添加一个抽象的hello方法(这次添加的不是一个默认方法)
public interface C extends A {
void hello();
}
这个新添加到C接口中的抽象方法hello比由接口A继承而来的hello方法拥有更高的优先级,因为C接口更加具体。因此,类D现在需要为hello显式地添加实现,否则该程序无法通过编译。