Chapter 1. Java 8: why should you care?
1.1 Java 怎么还在变
某些语言只是更适合某些方面。比如,C和C++仍然是构建操作系统和各种嵌入式系统的流行工具,因为它们编出的程序尽管安全性不佳,但运行时占用资源少。Java和C#等安全型语言在诸多运行资源不太紧张的应用中已经取代了前者。
1.1.1 Java 在编程语言生态系统中的位置
Java虚拟机(JVM)及其字节码 可能会变得比Java语言本身更重要,而且对于某些应用来说, Java可能会被同样运行在JVM上的 竞争对手语言(如Scala或Groovy)取代。
Java是怎么进入通用编程市场的?
封装原则使得其软件工程问题比C少,“一次编写,随处运行”
程序员越来越多地要处理所谓的大数据,并希望利用多核计算机或计算集群来有效地处理。这意味着需要使用并行处理——Java以前对此并不支持。
1.1.2 流处理
cat file1 file2 | tr "[A-Z]" "[a-z]" | sort | tail -3
sort把一个行流作为输入,产生了另一个行流(进行排序)作为输出。请注意在Unix中,命令( cat、 tr、sort和tail)是同时执行的,这样sort就可以在cat或tr完成前先处理头几行。
好处:
思路变成了把这样的流变成那样的流(就像写数据库查询语句时的那种思路),而不是一次只处理一个项目。
把输入的不相关部分拿到几个CPU内核上去分别执行你的Stream操作流水线
1.1.3 用行为参数化把代码传递给方法
让sort方法利用自定义的顺序进行比较。你可以写一个compareUsingCustomerId来比较两张发票的代码,而非写一个新Comparator对象
1.1.4 并行与共享的可变数据
并行:同时对不同的输入安全地执行。一般情况下这就意味着,你写代码时不能访问共享的可变数据。但如果要写入的是一个共享变量或对象,这就行不通了
这两个要点(1.1.4没有共享的可变数据,1.1.3将方法和函数即代码传递给其他方法的能力)是函数式编程范式的基石
1.1.5 Java 需要演变
1.2 Java 中的函数
值是Java中的一等公民,但其他很多Java概念(如方法和类等)则是二等公民。用方法来定义类很不错,类还可以实例化来产生值,但方法和类本身都不是值。在运行时传递方法能将方法变成一等公民,这在编程中非常有用。函数式编程中的函数的主要意思是“把函数作为一等值”,不过它也常常隐含着第二层意思,即“执行时在元素之间无互动”
1.2.1 方法和 Lambda 作为一等公民
旧:把isHidden
包在一个FileFilter对象里,然后传递给File.listFiles方法
File[] hiddenFiles = new File(".").listFiles(new FileFilter() {
public boolean accept(File file) {
return file.isHidden();
}
});
新
File[] hiddenFiles = new File(".").listFiles(File::isHidden);
(int x) -> x + 1
1.2.2 传递代码:一个例子
public static boolean isGreenApple(Apple apple) {
return "green".equals(apple.getColor());
}
public static boolean isHeavyApple(Apple apple) {
return apple.getWeight() > 150;
}
public interface Predicate<T>{
boolean test(T t);
} //平常只要从java.util.function导入就可以了
static List<Apple> filterApples(List<Apple> inventory, Predicate<Apple> p) {
List<Apple> result = new ArrayList<>();
for (Apple apple: inventory){
if (p.test(apple)) {
result.add(apple);
}
}
return result;
}
调用时filterApples(inventory, Apple::isGreenApple);
1.2.3 从传递方法到 Lambda
甚至都不需要为只用一次的方法写定义
filterApples(inventory, (Apple a) -> "green".equals(a.getColor()) );
filterApples(inventory, (Apple a) -> a.getWeight() < 80 || "brown".equals(a.getColor()) );
1.3 流
import static java.util.stream.Collectors.toList;
Map<Currency, List<Transaction>> transactionsByCurrencies =
transactions.stream()
.filter((Transaction t) -> t.getPrice() > 1000)
.collect(groupingBy(Transaction::getCurrency));
多线程并非易事: 线程可能会同时访问并更新共享变量。因此,如果没有协调好,数据可能会被意外改变。Java 8也用Stream API(java.util.stream)解决了这两个问题:集合处理时的套路和晦涩,以及难以利用多核。
Collection主要是为了存储和访问数据,而Stream则主要用于描述对数据的计算。
顺序处理:
import static java.util.stream.Collectors.toList;
List<Apple> heavyApples =
inventory.stream().filter((Apple a) -> a.getWeight() > 150)
.collect(toList());
并行处理:
import static java.util.stream.Collectors.toList;
List<Apple> heavyApples =
inventory.parallelStream().filter((Apple a) -> a.getWeight() > 150)
.collect(toList());
1.4 默认方法
主要是为了支持库设计师,让他们能够写出更容易改进的接口。
List调用sort方法。它是用Java 8 List接口中如下所示的默认方法实现的,它会调用Collections.sort静态方法:
default void sort(Comparator<? super E> c) {
Collections.sort(this, c); //this指Collections的具体变量
}
1.5 来自函数式编程的其他好思想
Java中从函数式编程中引入的两个核心思想:将方法和Lambda作为一等值,以及在没有可变共享状态时,函数或方法可以有效、安全地并行执行。
其他:
Optional<T>
类,如果你能一致地使用它的话,就可以帮助你避免出现NullPointer异常。
(结构)模式匹配,Java 8对模式匹配的支持并不完全
Chapter 2. Passing code with behavior parameterization
行为参数化,就是把方法或者方法内部的行为作为参数传入另一方法中。
2.1 应对不断变化的需求
背景:有一“列”苹果,需要根据不同标准(颜色,重量等)来筛选。当标准不断变化,如何处理?
下面是错误示范
public static List<Apple> filterGreenApples(List<Apple> inventory) {
List<Apple> result = new ArrayList<Apple>();
for(Apple apple: inventory){
if( "green".equals(apple.getColor() ) {//然后根据需求改这一行,或许有必要在上面增加引入的变量,如要判断color是否为red
result.add(apple);
}
}
return result;
}
//使用(符合更多标准时)
List<Apple> greenApples = filterApples(inventory, "green", 0, true);
这种方法实现不同需求,要写大量重复代码(只有评论和参数引入不同)
2.2 行为参数化
进一步,写接口,每个标准写一个并实现该接口的方法,最后再写实现需求的方法。之后就可以使用了....
这种方法灵活,但依旧啰嗦
//必写(当然,有的可以引用),可抽象为Predicate<T>
public interface ApplePredicate{
boolean test (Apple apple);
}
//可省
public class AppleHeavyWeightPredicate implements ApplePredicate{
public boolean test(Apple apple){
return apple.getWeight() > 150;
}
}
//必写,写法可能不一样(看2.4),另外可抽象为public static <T> List<T> filter(List<T> list, Predicate<T> p)
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;
}
//可简,使用(记得要new)
List<Apple> redAndHeavyApples = filterApples(inventory, new AppleRedAndHeavyPredicate());
2.3 简化
1.匿名类(简化一小步)
和上面一样,先写好接口和实现需求的方法,然后就可以用了。在用时再写上判断标准...
List<Apple> redApples = filterApples(inventory, new ApplePredicate() {//接口名同样记得new,下面实现接口。如果实现中有this,则指该实现ApplePredicate
public boolean test(Apple apple){
return "red".equals(apple.getColor());
}
})
2.Lambda 表达式(一大步,Java 8特征)
在接口处直接写所需参数,通过->
后写行为
List<Apple> result = filterApples(inventory, (Apple apple) -> "red".equals(apple.getColor()));
2.4 真实的例子(暂时不需深究)
Java API包含很多可以用不同行为进行参数化的方法,如下面三种
1.用 Comparator 来排序
public interface Comparator<T> {
public int compare(T o1, T o2);
}
inventory.sort(new Comparator<Apple>() {
public int compare(Apple a1, Apple a2){
return a1.getWeight().compareTo(a2.getWeight());
}
});
inventory.sort((Apple a1, Apple a2) -> a1.getWeight().compareTo(a2.getWeight()));
2.用 Runnable 执行代码块
public interface Runnable{
public void run();
}
Thread t = new Thread(new Runnable() {
public void run(){
System.out.println("Hello world");
}
});
Thread t = new Thread(() -> System.out.println("Hello world"));
3.GUI 事件处理
Button button = new Button("Send");
button.setOnAction(new EventHandler<ActionEvent>() {
public void handle(ActionEvent event) {
label.setText("Sent!!");
}
});
button.setOnAction((ActionEvent event) -> label.setText("Sent!!"));
Chapter 3. Lambda expressions
3.1 Lambda in a nutshell
根据chapter2中匿名方式和Lambda的比较,我们可以的出Lambda表达式实际是表示可传递的匿名函数的一种方式:它没有名称,但它有参数列表、函数主体、返回类型。(可能还有一个可以抛出的异常列表)
理论上来说,你在Java 8之前做不了的事情, Lambda也做不了。
基本语法
(parameters) -> expression
一个string也是表达式
(parameters) -> { statements; }
加上花括号,里面就要写语句,返回要return
一些例子
//1
(String s) -> s.length()
//2
(Apple a) -> a.getWeight() > 150
//3
(int x, int y) -> {
System.out.println("Result:");
System.out.println(x+y);
}
//4
() -> 42
//5
(Apple a1, Apple a2) -> a1.getWeight().compareTo(a2.getWeight())
//6
() -> {}
//7
() -> new Apple(10)
3.2 在哪里以及如何使用 Lambda
1.函数式接口
在函数式接口上使用Lambda表达式。函数式接口就是只定义一个抽象方法的接口,如:
public interface Predicate<T>{
boolean test (T t);
}
当某个方法用Predicate<T>
作为参数时,就可以用Lambda了
Lambda表达式可以被赋给一个变量,或传递给一个接受函数式接口作为参数的方法,如
Runnable r1 = () -> System.out.println("Hello World 1");
process(() -> System.out.println("Hello World 3"));
process接受Runnable参数
tip:如果你用@FunctionalInterface定义了一个接口,而它却不是函数式接口的话,编译器将返回一个提示原因的错误。
2.函数描述符
函数式接口的抽象方法的签名基本上就是Lambda表达式的签名。我们将这种函数式接口的抽象方法(如下面的() -> void)的签名叫作函数描述符。
//Lambda签名为() -> void,和Runnable的相同
execute(() -> {});
public void execute(Runnable r){
r.run();
}
//可以在定义实践方法(指实现要求的主体方法)时直接用Lambda,签名为() -> String,和fetch相同
public Callable<String> fetch() {
return () -> "Tricky example ;-)";
}
//签名为(Apple) -> Intege,Predicate不同;在实际当中其实不需要=及左部分
Predicate<Apple> p = (Apple a) -> a.getWeight();
3.3 实战
环绕执行( execute around) 模式: 资源处理(例如处理文件或数据库)时一个常见的模式就是打开一个资源,做一些处理,然后关闭资源。这个设置和清理阶段总是很类似,并且会围绕着执行处理的那些重要代码。
下面代码只能读文件的第一行。如果你想要返回头两行,甚至是返回使用最频繁的词,该怎么办呢
第一步,适合一种行为的方法:
public static String processFile() throws IOException {
try (BufferedReader br = new BufferedReader(new FileReader("data.txt"))) {
return br.readLine();
}
}//Java 7中的带资源的try语句,它已经简化了代码,因为你不需要显式地关闭资源了
第二步,思考一个接口,返回什么,这个返回又是通过什么得到的。下面返回头两行,需要有br,即BufferedReader
String result = processFile((BufferedReader br) -> br.readLine() + br.readLine());
写接口
@FunctionalInterface
public interface BufferedReaderProcessor {
String process(BufferedReader b) throws IOException;//和上面签名一致,接受BufferedReader -> String,还可以抛出IOException异常
}
第三步,修改实践方法
public static String processFile(BufferedReaderProcessor p) throws IOException {
try (BufferedReader br =
new BufferedReader(new FileReader("data.txt"))) {
return p.process(br);
}
}
第四步,运用
String twoLines = processFile((BufferedReader br) -> br.readLine() + br.readLine());
3.4 预设的函数式接口
//1.Predicate接受泛型T对象,并返回一个boolean,方法为test
//需求:表示一个涉及类型T的布尔表达式
//生成的Predicate<T>还能调用.and(), .or, .negate方法
@FunctionalInterface
public interface Predicate<T>{
boolean test(T t);
}
//实际当中,引用上面就可以直接用了
Predicate<String> nonEmptyStringPredicate = (String s) -> !s.isEmpty();//与Predicate.isEmpty(s)相同?即使s为空也能工作。
//2.Consumer接受泛型T对象,没有返回(void),方法为accept。
//需求:访问类型T的对象,并对其执行某些操作
//其他方法andThen
@FunctionalInterface
public interface Consumer<T>{
void accept(T t);
}
//3.Function接受一个泛型T的对象,并返回一个泛型R的对象,方法为apply
//需求:将输入对象的信息映射到输出
//其他方法andThen, compose, identity
@FunctionalInterface
public interface Function<T, R>{
R apply(T t);
}
public static <T, R> List<R> map(List<T> list, Function<T, R> f) {
List<R> result = new ArrayList<>();
for(T s: list){
result.add(f.apply(s));
}
return result;
}
// 将["lambdas","in","action"]转化为[7, 2, 6]
List<Integer> l = map(
Arrays.asList("lambdas","in","action"),
(String s) -> s.length()
);
//其他接口(详情看java.util.function,或core_java_for_the_impatient 3.6)
Runable ()-> void .run
Comparator<T,T,Integer> (T, T) -> int
Supplier<T> ()->T .get
UnaryOperator<T> T->T
BinaryOperator<T> (T,T)->T .apply
BiPredicate<L,R> (L,R)->boolean
BiConsumer<T,U> (T,U)->void
BiFunction<T,U,R> (T,U)->R
2.原始类型特化
boxing: Java里将原始类型转换为对应的引用类型的机制。装箱后的值本质上就是把原始类型包裹起来,并保存在堆里。因此,装箱后的值需要更多的内存,并需要额外的内存搜索来获取被包裹的原始值。
用Predicate<Integer>
就会把参数1000装箱到一个Integer对象中,用IntPredicate
可避免。一般来说,针对专门的输入参数类型的函数式接口的名称都要加上对应的原始类型前缀,比如DoublePredicate、 IntConsumer、 LongBinaryOperator、 IntFunction等。当然,输出也有。
不同的接口可是实现相同的Lambda,但效率可能不一样
3.关于异常
当没有办法自己创建一个接口时,比如Function<T, R>
,可用下面的显式catch
Function<BufferedReader, String> f = (BufferedReader b) -> {
try {
return b.readLine();
}
catch(IOException e) {
throw new RuntimeException(e);
}
};
3.5 类型检查、类型推断以及限制
1.类型检查
实践方法所需参数,参数中的接口(目标类型Predicate<Apple>
),接口中的抽象方法,接受什么和返回什么,与Lambda比较是否匹配
如果Lambda表达式抛出一个异常,那么抽象方法所声明的throws语句也必须与之匹配
2.同样的 Lambda,不同的函数式接口
如果一个Lambda的主体是一个语句表达式, 它就和一个返回void的函数描述符兼容(当然需要参数列表也兼容)。
// 合法,尽管Consumer返回了一个void
Consumer<String> b = s -> list.add(s);
3.类型推断
有时候显式写出类型更易读,有时候去掉它们更易读。
4.使用局部变量(类的方法中的变量)
int portNumber = 1337;//必须显式声明为final,或事实上是final
Runnable r = () -> System.out.println(portNumber);
portNumber = 31337;//再加段就无法捕捉。这一限制不鼓励你使用改变外部变量的典型命令式编程模式(会阻碍很容易做到的并行处理
3.6 方法引用
inventory.sort((Apple a1, Apple a2)
-> a1.getWeight().compareTo(a2.getWeight()));
//使用方法引用和java.util.Comparator.comparing)
inventory.sort(comparing(Apple::getWeight));
1.In a nutshell
如果一个Lambda代表的只是“直接调用这个方法”,那最好还是用名称来调用它,而不是去描述如何调用它。如Apple::getWeight
就是引用了Apple类中定义的方法getWeight。
实际是针对仅仅涉及单一方法的Lambda的语法糖
方法引用主要有三类
//1.指向静态方法(Integer::parseInt)
(String s) -> Integer.parseInt(s)//Integer::parseInt
//2.指向实例方法
//第一个参数为方法接收者
(x, y) ->
x.compareToIgnoreCase(y) //String::compareToIgnoreCase
//3.指向现有对象的实例方法
//调用一个已经存在的外部对象中的方法
(args) -> expr.instanceMethod(args)//expr::instanceMethod
// 例子
() -> Thread.currentThread().dumpStack() //Thread.currentThread()::dumpStack
(String s) -> System.out.println(s) //System.out::println
2.构造函数引用
//默认构造
Supplier<Apple> c1 = Apple::new; //() -> new Apple()
Apple a1 = c1.get();
//其他构造
Function<Integer, Apple> c2 = Apple::new;//(weight) -> new Apple(weight);
Apple a2 = c2.apply(110);
//对于三个及以上参数来构造的,要自己写一个interface
public interface TriFunction<T, U, V, R>{
R apply(T t, U u, V v);
}
注意不同接口,相同的引用。只有在调用接口方法时才不同
//上文的map可以如下引用,让map根据一个list来构造不同的object
public static List<Apple> map(List<Integer> list,
Function<Integer, Apple> f){
List<Apple> result = new ArrayList<>();
for(Integer e: list){
result.add(f.apply(e));
}
return result;
}
List<Integer> weights = Arrays.asList(7, 3, 4, 10);
List<Apple> apples = map(weights, Apple::new);
一个有趣的实现:使用Map来将构造函数(Function<Integer, Fruit>
)映射到字符串值。这样就可以通过string和构造所需参数获得相应的构造出来的对象
static Map<String, Function<Integer, Fruit>> map = new HashMap<>();
static {
map.put("apple", Apple::new);
map.put("orange", Orange::new);
// etc...
}
public static Fruit giveMeFruit(String fruit, Integer weight){
return map.get(fruit.toLowerCase())
.apply(weight);
3.7 Lambda 和方法引用实战
用不同的排序策略给一个Apple列表排序
1.传递代码
void sort(Comparator<? super E> c)
它需要一个Comparator对象来比较两个Apple!这就是在Java中传递策略的方式:它们必须包裹在一个对象里。
//普通方法
public class AppleComparator implements Comparator<Apple> {
public int compare(Apple a1, Apple a2){
return a1.getWeight().compareTo(a2.getWeight());
}
}
inventory.sort(new AppleComparator());
//匿名方法
inventory.sort(new Comparator<Apple>() {
public int compare(Apple a1, Apple a2){
return a1.getWeight().compareTo(a2.getWeight());
}
});
//Lambda(根据Comparator的函数描述符(T, T) -> int)
inventory.sort((Apple a1, Apple a2)//不写Apple也可
-> a1.getWeight().compareTo(a2.getWeight())
);
//更进一步
//Comparator具有一个叫作comparing的静态辅助方法,它可以接受一个能提取Comparable键值的Function,并生成一个Comparator对象
Comparator<Apple> c = Comparator.comparing((Apple a) -> a.getWeight());//注意你现在传递的Lambda只有一个参数: Lambda说明了如何从苹果中提取需要比较的键值
//或
import static java.util.Comparator.comparing;
inventory.sort(comparing((a) -> a.getWeight()));
//最终
inventory.sort(comparing(Apple::getWeight));
逆向思路:对List<Apple>
排序,由于Apple不是Comparator,所以需要用Comparator包裹,而产生Comparator的方式可以是1.comparing(提取Comparable键值的function),恰好Apple本身就有.getWeight,而且只需一个function,所以为comparing(Apple::getWeight)
3.8 复合 Lambda 表达式的有用方法
1.比较器复合
Comparator接口的一些默认方法:
逆序reversed
comparing(Apple::getWeight).reversed()
比较器链thenComparing
inventory.sort(comparing(Apple::getWeight)
.reversed()
.thenComparing(Apple::getCountry));
2.谓词(一个返回boolean的函数)复合
Predicate接口的一些默认方法:
非negate
Predicate<Apple> notRedApple = redApple.negate();
链(从左向右确定优先级的)
Predicate<Apple> redAndHeavyAppleOrGreen =
redApple.and(a -> a.getWeight() > 150)
.or(a -> "green".equals(a.getColor()));
3.函数复合
Function接口的一些默认方法:
andThen:从左往右f —> g
compose:从右往左f <— g
Function<Integer, Integer> f = x -> x + 1;
Function<Integer, Integer> g = x -> x * 2;
Function<Integer, Integer> h = f.andThen(g);//g(f(x))
int result = h.apply(1);//返回4
Function<Integer, Integer> h = f.compose(g);//f(g(x))
int result = h.apply(1);//返回3
// 另一个例子
Function<String, String> addHeader = Letter::addHeader;
Function<String, String> transformationPipeline=
addHeader.andThen(Letter::checkSpelling)
.andThen(Letter::addFooter);