非常容易看懂
这篇书评可能有关键情节透露
Java8实战
2015年;基于Java8 ;
写的真好;清晰明了;
2016年印刷;
1)Java 8则是在2014年3月发布的。
inventory.sort(comparing(Apple::getWeight)); 直接利用weight字段来作为排序依据;
Java 5添加了工业级的构建模块,如线程池和并发集合。Java 7添加了分支/合并(fork/join)框架,使得并行变得更实用,但仍然很困难。
2)这样就可以避免用synchronized编写代码,这一代码不仅容易出错,而且在多核CPU上执行所需的成本也比你想象的要高。
把代码传递给方法的简洁方式(方法引用、Lambda)和接口中的默认方法。
3)在Java 8之前可以用匿名类实现行为参数化呀
利用Stream轻松实现并行,而不需要通过thread;
这些函数有时被称为“纯函数”或“无副作用函数”或“无状态函数”
4)在多个处理器内核之间使用synchronized,其代价往往比你预期的要大得多,因为同步迫使代码按照顺序执行,而这与并行处理的宗旨相悖。
File[] hiddenFiles = new File(".").listFiles(File::isHidden);
直接过滤出隐藏的文件;
5)通过谓词传递方法,例如Predicate<Apple> p,那么就是一个Apple的成员方法p;
filterApples(inventory, (Apple a) -> a.getWeight() > 150 );
6)内部迭代;外部迭代;
用集合的话,你得自己去做迭代的过程。你得用for-each循环一个个去迭代元素,然后再处理元素。我们把这种数据迭代的方法称为外部迭代
7)Java 8用一些限制来避免出现类似于C++中臭名昭著的菱形继承问题。
排序避免要求给每一个类都增加一个实现,通过默认实现的方式来提供;
8)在Java 8里有一个Optional<T>类,如果你能一致地使用它的话,就可以帮助你避免出现NullPointer异常
使用多态和方法重载可以代替if/else,不过也过于复杂了;
9)行为参数化就是可以帮助你处理频繁变更的需求的一种软件开发模式。变化的部分,以后再说。
10)Java 8中的Lambda解决了代码啰嗦的问题
我们把它称为谓词(即一个返回boolean值的函数)。让我们定义一个接口来对选择标准建模:
public interface ApplePredicate{
boolean test (Apple apple);
}
11)利用匿名类实现:
List<Apple> redApples = filterApples(inventory, new ApplePredicate() { ←─直接内联参数化filterapples方法的行为
public boolean test(Apple apple){
return "red".equals(apple.getColor());
}
});
12)在Java里,你可以使用Runnable接口表示一个要执行的代码块。
理论上来说,你在Java 8之前做不了的事情,Lambda也做不了。
(String s) -> s.length() ←─第一个Lambda表达式具有一个String类型的参数并返回一个int。Lambda没有return语句,因为已经隐含了return
(Apple a) -> a.getWeight() > 150 ←─第二个Lambda表达式有一个Apple 类型的参数并返回一个boolean(苹果的重量是否超过150克)
(int x, int y) -> {
System.out.println("Result:");
System.out.println(x+y); ←─第三个Lambda表达式具有两个int类型的参数而没有返回值(void返回)。注意Lambda表达式可以包含多行语句,这里是两行
}
() -> 42 ←─第四个Lambda表达式没有参数, 返回一个int
(Apple a1, Apple a2) -> a1.getWeight().compareTo(a2.getWeight()) ←─第五个Lambda表达式具有两个Apple类型的参数,返回一个int:比较两个Apple的重量
13)你可以在函数式接口上使用Lambda表达式。
函数式接口就是只定义一个抽象方法的接口
Runnable r1 = () -> System.out.println("Hello World 1"); ←─使用Lambda
实现一个可执行的传入:
public void process(Runnable r){
r.run();
}
process(() -> System.out.println("This is awesome!!"));
这个标注用于表示该接口会设计成一个函数式接口。如果你用@FunctionalInterface定义了一个接口,而它却不是函数式接口的话,编译器将返回一个提示原因的错误。
14)Java API中已经有了几个函数式接口,比如你在3.2节中见到的Comparable、Runnable和Callable。
15)我们接下来会介绍Predicate、Consumer和Function,更完整的列表可见本节结尾处的表3-2
16)泛型(比如Consumer<T>中的T)只能绑定到引用类型。不能是基础类型。
17)在Java里有一个将原始类型转换为对应的引用类型的机制。这个机制叫作装箱(boxing)。
装箱后的值本质上就是把原始类型包裹起来,并保存在堆里。
18)(T,U) -> R的表达方式展示了应当如何思考一个函数描述符。表的左侧代表了参数类型。这里它代表一个函数,具有两个参数,分别为泛型T和U,返回类型为R。
19)任何函数式接口都不允许抛出受检异常(checked exception)。
定义一个自己的函数式接口,并声明受检异常,或者把Lambda包在一个try/catch块中。
20)菱形运算符可以推断类型。
List<String> listOfStrings = new ArrayList<>();
21)但局部变量必须显式声明为final,或事实上是final。换句话说,Lambda表达式只能捕获指派给它们的局部变量一次。
22)Java在访问自由局部变量时,实际上是在访问它的副本,而不是访问原始变量。
23)闭包就是一个函数的实例,且它可以无限制地访问那个函数的非本地变量。
可以代替闭包,不过它们不能修改定义Lambda的方法的局部变量的内容。
24)可以认为Lambda是对值封闭,而不是对变量封闭。
25)实例变量可以,因为它们保存在堆中,而堆是在线程之间共享的,这种限制存在的原因在于局部变量保存在栈上,并且隐式表示它们仅限于其所在线程。如果允许捕获可改变的局部变量,就会引发造成线程不安全的新的可能性。
26)我们就可以将Lambda表达式重构为等价的方法引用
Supplier<Apple> c1 = Apple::new; ←─构造函数引用指向默认的Apple()构造函数
Apple a1 = c1.get(); ←─调用Supplier的get方法将产生一个新的Apple
Function<Integer, Apple> c2 = Apple::new; ←─指向Apple(Integer weight)的构造函数引用
Apple a2 = c2.apply(110); ←─调用该Function函数的apply方法,并给出要求的重量,将产生一个Apple
BiFunction<String, Integer, Apple> c3 = Apple::new; ←─指向Apple(Stringcolor,Integer weight)的构造函数引用
Apple c3 = c3.apply("green", 110); ←─调用该BiFunction函数的apply方法,并给出要求的颜色和重量,将产生一个新的Apple对象
public interface TriFunction<T, U, V, R>{
R apply(T t, U u, V v);
}
import static java.util.Comparator.comparing;
inventory.sort(comparing((a) -> a.getWeight()));
inventory.sort(comparing(Apple::getWeight)
.reversed() ←─按重量递减排序
.thenComparing(Apple::getCountry)); ←─两个苹果一样重时,进一步按国家排序
27)and和or方法是按照在表达式链中的位置,从左向右确定优先级的。因此,a.or(b).and(c)可以看作(a || b) && c。
Function<Integer, Integer> f = x -> x + 1;
Function<Integer, Integer> g = x -> x * 2;
Function<Integer, Integer> h = f.andThen(g); ←─数学上会写作g(f(x))或(g o f)(x)
int result = h.apply(1); ←─这将返回4
28)andThen是先后,compose是嵌套;执行顺序刚好相反;
29)Java 8自带一些常用的函数式接口,放在java.util.function包里,包括Predicate<T>、Function<T,R>、Supplier<T>、Consumer<T>和BinaryOperator<T>,如表3-2所述。
30)为了避免装箱操作,对Predicate<T>和Function<T, R>等通用函数式接口的原始类型特化:IntPredicate、IntToLongFunction等。
31)集合是一个内存中的数据结构,它包含数据结构中目前所有的值——集合中的每个元素都得先算出来才能添加到集合中。
32)流则是在概念上固定的数据结构(你不能添加或删除元素),其元素则是按需计算的。
33)流就像是一个延迟创建的集合:只有在消费者要求的时候才会计算值(用管理学的话说这就是需求驱动,甚至是实时制造)。
例如:如果想列出质数的集合,是不现实的;但是如果用流,则可以实现(就像流媒体电影一样);
34)和迭代器类似,流只能遍历一次。
如果还需要再来,则需要重新生成一个流。
35)使用Collection接口需要用户去做迭代(比如用for-each),这称为外部迭代。 相反,Streams库使用内部迭代
36)自己管理实际上意味着“某个良辰吉日我们会把它并行化”或“开始了关于任务和synchronized的漫长而艰苦的斗争”
类似于Collections,但没有迭代器;
37)两类操作:filter、map和limit可以连成一条流水线;collect触发流水线执行并关闭它
这是因为中间操作一般都可以合并起来,在终端操作时一次性全部处理。
第一,尽管很多菜的热量都高于300卡路里,但只选出了前三个!这是因为limit操作和一种称为短路的技巧
第二,尽管filter和map是两个独立的操作,但它们合并到同一次遍历中了(我们把这种技术叫作循环合并)。
38)构建器模式。1在构建器模式中有一个调用链用来设置一套配置(对流来说这就是一个中间操作链),接着是调用built方法(对流来说就是终端操作)。
limit(n),取前n个;
skip(n),取后n个;
limit(n)和skip(n)是互补
.flatMap(Arrays::stream) ←─将各个生成流扁平化为单个流
anyMatch方法可以回答“流中是否有一个元素能匹配给定的谓词”。
allMatch方法的工作原理和anyMatch类似,但它会看看流中的元素是否都能匹配给定的谓词。
和allMatch相对的是noneMatch。它可以确保流中没有任何元素与给定的谓词匹配。
39)不管表达式有多长,你只需找到一个表达式为false,就可以推断整个表达式将返回false,所以用不着计算整个表达式。这就是短路。
isPresent()将在Optional包含值的时候返回true, 否则返回false。ifPresent(Consumer<T> block)会在值存在的时候执行给定的代码块。我们在第3章介绍了Consumer函数式接口;它让你传递一个接收T类型参数,并返回void的Lambda表达式。T get()会在值存在时返回值,否则抛出一个NoSuchElement异常。T orElse(T other)会在值存在时返回值,否则返回一个默认值。
40)你可能想要找到第一个元素。为此有一个findFirst方法
41)为什么会同时有findFirst和findAny呢?答案是并行
42)此类查询需要将流中所有元素反复结合起来,得到一个值,比如一个Integer。这样的查询可以被归类为归约操作(将流归约成一个值)。
43)map和reduce的连接通常称为map-reduce模式,因Google用它来进行网络搜索而出名,因为它很容易并行化。
44)传递给reduce的Lambda不能更改状态(如实例变量),而且操作必须满足结合律才可以按任意顺序执行。
.reduce(Integer::max); ←─计算生成的流中的最大值
.min(comparing(Transaction::getValue));
45)装箱造成的复杂性——即类似int和Integer之间的效率差异。
.mapToInt(Dish::getCalories) ←─返回一个IntStream
.sum(); 没有装箱成本。
int max = maxCalories.orElse(1); ←─如果没有最大值的话,显式提供一个默认最大值
46)一个Optional原始类型特化版本:OptionalInt、OptionalDouble和OptionalLong。
改用IntStream.range(1, 100),则结果将会是49个偶数,因为range是不包含结束值的。rangeClosed包含。
Stream.iterate(new int[]{0, 1},
t -> new int[]{t[1],t[0] + t[1]})
.limit(10)
.map(t -> t[0])
.forEach(System.out::println);
IntSupplier fib = new IntSupplier(){
private int previous = 0;
private int current = 1;
public int getAsInt(){
int oldPrevious = this.previous;
int nextValue = this.previous + this.current;
this.previous = this.current;
this.current = nextValue;
return oldPrevious;
}
};
IntStream.generate(fib).limit(10).forEach(System.out::println);
groupingBy说的是“生成一个Map,它的键是(货币)桶,值则是桶中那些元素的列表”。
import static java.util.stream.Collectors.*;
这样你就可以写counting()而用不着写Collectors.counting()之类的了。
47)你给groupingBy方法传递了一个Function(以方法引用的形式),它提取了流中每一道Dish的Dish.Type。我们把这个Function叫作分类函数,因为它用来把流中的元素分成不同的组。
普通的单参数groupingBy(f)(其中f是分类函数)实际上是groupingBy(f, toList())的简便写法。
Collectors.collectingAndThen()的最后一个参数,是把值取出来;否则就会被放到Option中;
Collector<T, A, R>接口, T是流中的泛型,A是累加器,R是收集得到的对象;
48)可以定义自己的规约行为;5个方法;
49)开发自己的收集器,已获得更好的性能;
50)代码使用并行流时,如果是累加,注意采用原子方式;否则每次结果不一样;
51)自动装箱和拆箱操作会大大降低性能。
52)分支/合并框架的目的是以递归方式将可以并行的任务拆分成更小的任务,然后将每个子任务的结果合并起来生成整体结果。
53)请注意availableProcessors方法虽然看起来是处理器,但它实际上返回的是可用内核的数量,包括超线程生成的虚拟内核
54)分支/合并框架需要“预热”或者说要执行几遍才会被JIT编译器优化。
55)分支/合并框架工程用一种称为工作窃取(work stealing)的技术来解决这个问题。避免线程任务不均匀。
Spliterator是Java 8中加入的另一个新接口;这个名字代表“可分迭代器”(splitable iterator)。并行迭代。(没有仔细看,有点头大,以后再看看吧)。
56)分支/合并框架让你得以用递归方式将可以并行的任务拆分成更小的任务,在不同的线程上执行,然后将各个子任务的结果合并起来生成整体结果。Spliterator定义了并行流如何拆分它要遍历的数据。
57)我们假设你用与Runnable同样的签名声明了一个函数接口,我们称之为Task
自定义lambda方法;
logger.log(Level.FINER, () -> "Problem: " + generateDiagnostic());
public void log(Level level, Supplier<String> msgSupplier){
if(logger.isLoggable(level)){
log(level, msgSupplier.get()); ←─执行Lambda表达式
}
}
58)lambda和interface之间的差异是啥?
interface是一组方法,lambda是一个方法;当然lambda也可以自己套自己,是一个复杂的方法;
59)对设计经验的归纳总结被称为设计模式
使用Lambda表达式后,很多现存的略显臃肿的面向对象设计模式能够用更精简的方式实现了
1)策略模式:基于interface,以传递不同的实现;
2)观察者模式:记录一个List的观察者,当发生事件时,可以挨个通知他们执行同一个动作;
3)责任链模式:可以把两个操作函数andThen,然后apply执行;
4)工厂方法:需要提前预置每个id的构造方法,才能快速指定名称创建;不是太方便;
60)因此要对你代码中的Lambda函数进行测试实际上比较困难,因为你无法通过函数名的方式调用它们。
61)调试有问题的代码时,程序员的兵器库里有两大老式武器,分别是:查看栈,跟踪输出日志。非常难跟踪。
62)peek的设计初衷就是在流的每个元素恢复运行之前,插入执行一个动作。
63)默认方法是Java 8中引入的一个新特性,希望能借此以兼容的方式改进API。
64)默认方法由default修饰符修饰,并像类中声明的其他方法一样包含方法体。你可以减少无效的模板代码。
65)一个类只能继承一个抽象类,但是一个类可以实现多个接口。
66)可以使用代理有效地规避这种窘境,即创建一个方法通过该类的成员变量直接调用该类的方法。这就是为什么有的时候我们发现有些类被刻意地声明为final类型:声明为final的类不能被其他的类继承,避免发生这样的反模式,防止核心代码的功能被污染。
67)如果一个类使用相同的函数签名从多个地方(比如另一个类或接口)继承了方法,通过三条规则可以进行判断。
(1) 类中的方法优先级最高。类或父类中声明的方法的优先级高于任何声明为默认方法的优先级。
(2) 如果无法依据第一条进行判断,那么子接口的优先级更高:函数签名相同时,优先选择拥有最具体实现的默认方法的接口,即如果B继承了A,那么B就比A更加具体。
(3) 最后,如果还是无法判断,继承了多个接口的类必须通过显式覆盖和调用期望的方法,显式地选择使用哪一个默认方法的实现。
68)我们保证,这些就是你需要知道的全部
public class C implements B, A {
void hello(){
B.super.hello(); ←─显式地选择调用接口B中的方法
}
}
69)价值百万的失误;Tony Hoare的计算机可选家提出了null空引用的想法;
null安全的尝试,深层质疑(或者说逐层判空);NPE问题;NullPointerException问题;
过多的return退出,避免深层嵌套;但if并没有减少;
你会了解使用null来表示变量值的缺失是大错特错的。
70)Java一直试图避免让程序员意识到指针的存在,唯一的例外是:null指针。
def carInsuranceName = person?.car?.insurance?.name
安全导航操作符;Java并没有采用;
71)汲取Haskell和Scala的灵感,Java 8中引入了一个新的类java.util.Optional<T>
72)Java语言的架构师Brian Goetz曾经非常明确地陈述过,Optional的设计初衷仅仅是要支持能返回Optional对象的语法。
public String getCarInsuranceName(Optional<Person> person) {
return person.flatMap(Person::getCar)
.flatMap(Car::getInsurance)
.map(Insurance::getName)
.orElse("Unknown"); ←─如果Optional的结果值为空,设置默认值
}
73)filter方法接受一个谓词作为参数。如果Optional对象的值存在,并且它符合谓词的条件,filter方法就返回其值;否则它就返回一个空的Optional对象
74)我们不推荐大家使用基础类型的Optional,因为基础类型的Optional不支持map、flatMap以及filter方法,而这些却是Optional类最有用的方法
75)Future接口在Java 5中被引入,设计初衷是对将来某个时刻会发生的结果进行建模
76)在Future中触发那些潜在耗时的操作把调用线程解放出来,让它能继续执行其他有价值的工作,不再需要呆呆等待耗时的操作完成
77)我们还是推荐大家使用重载版本的get方法,它接受一个超时的参数
78)CompletableFuture具有一定的优势,因为它允许你对执行器(Executor)进行配置,尤其是线程池的大小,让它以更适合应用需求的方式进行配置,满足程序的要求,而这是并行流API无法提供的。
t.setDaemon(true); // 使用守护线程方式不会组织程序的关停;
79)Java程序无法终止或者退出一个正在运行中的线程,所以最后剩下的那个线程会由于一直等待无法发生的事件而引发问题。与此相反,如果将线程标记为守护进程,意味着程序退出时它也会被回收。
80)只要有任何非守护线程还在运行,程序就不会终止。
使用守护线程,就不会阻止程序的退出。
非守护线程退出之后,守护线程无事可做,也会退出。
守护线程创建的线程也是守护线程,用户线程创建的还是用户线程。
81)thenCompose方法允许你对两个异步操作进行流水线,第一个操作完成时,将其结果作为参数传递给第二个操作。我们选择thenCompose方法的原因是因为它更高效一些,因为少了很多线程切换的开销。
thenCombine,可以并行启动另外一个任务,以方便将两个任务最终合并;
thenCombileAsync,异步版本也用不到。
Executors.newCachedThreadTool
new ThreadPoolExecutor(), 这是我们业务里面使用的;
82)调用get或者join方法只会造成阻塞,直到CompletableFuture完成才能继续往下运行
thenAccept
thenAcceptAsync
83)Java 8之前的库对日期和时间的支持就非常不理想。然而,你也不用太担心:Java 8中引入全新的日期和时间API就是要解决这一问题。
84)月份依旧是从0开始计算
DateFormat方法也有它自己的问题。比如,它不是线程安全的。
85)用于以语言无关方式格式化和解析日期或时间的DateFormat方法就只在Date类里有。
LocalDate不包含时区信息;
86)LocalDate.with 可以用的智能函数:
firstDayOfMonth
lastDayOfMonth
firstDayOfNextMonth
firstDayOfYear
lastDayOfYear
firstDayOfNextYear
firstInMonth
lastInMonth
dayOfWeekInMonth
next
nextOrSame
previous
previousOrSame
DayOfWeek.MONDAY;
MONDAY
TUESDAY
WEDNESDAY
THURSDAY
FRIDAY
SATURDAY
SUNDAY
87)使用ISO8601的日期格式;或者其他;
88)用了synchronize的程序员,就直接开除;用了这个关键字代码的服务就直接拒绝;
89)局部函数式(partial function)可能更为妥当
90)递归因为栈帧的存在而更加消耗内存;
91)尾-调优化(tail-call optimization),递归调用发生在函数的末尾;
92)scala:这里Lambda表达式的语法和Java 8也非常类似,区别是箭头的表示用=>替换了->2。
93)Collections的成员:
UnmodifiableCollection
UnmodifiableSet 方法返回指定set的不可修改视图
UnmodifiableSortedSet
UnmodifiableNavigableSet
UnmodifiableList
UnmodifiableRandomAccessList
UnmodifiableMap
UnmodifiableSortedMap
UnmodifiableNavigableMap
SynchronizedCollection
SynchronizedSet
SynchronizedSortedSet
SynchronizedNavigableSet
SynchronizedList
SynchronizedRandomAccessList
SynchronizedMap
SynchronizedSortedMap
SynchronizedNavigableMap
CheckedCollection
CheckedQueue
CheckedSet
CheckedSortedSet
CheckedNavigableSet
CheckedList
CheckedRandomAccessList
CheckedMap
CheckedSortedMap
CheckedNavigableMap
EmptyIterator
EmptyListIterator
EmptyEnumeration
EmptySet
EmptyList
EmptyMap
SingletonSet
SingletonList
SingletonMap
CopiesList
ReverseComparator
ReverseComparator2
SetFromMap
AsLIFOQueue
94)Completable-Future对于Future的意义就像Stream之于Collection。
Function<T,R>
Supplier<R>
BiFunction<T,R>
TriFunction<T,R>
95)什么时候会装箱,什么时候不会?这个不是很明显;
96)Java1.1就有逃逸分析了;
Completable-Future对于Future的意义就像Stream之于Collection。
97)你可以声明一个新的注解,它包含了你希望重复的注解数组。而Java本身是不能注解重复的。
List<@NonNull Car> cars = new ArrayList<>();
保证元素不为空;
98)checker框架,增强类型检查;
99)ConcurrentHashMap,可以对value和key做非锁的遍历和计算。
100)方法Files.lines,通过该方法你可以以延迟方式读取文件的内容,并将其作为一个流。
101)StreamForker,复制流;