(1) Java 8 实战第二版——基础知识阅读笔记
1、基础知识
第 1 章 Java 8、9、10以及11的变化
方法引用
Lmbada表达式
static List<Apple> filterApples(List<Apple> inventory,Predicate<Apple> p) {
←---- 方法作为Predicate参数p传递进去(见附注栏“什么是谓词?”)
List<Apple> result = new ArrayList<>();
for (Apple apple: inventory){
if (p.test(apple)) { ←---- 苹果符合p所代表的条件吗
result.add(apple);
}
}
return result;
}
Collection主要是为了存储和访问数据,Stream则主要用于描述对数据的计算
第 2 章 通过行为参数化传递代码
行为参数化就是可以帮助你处理频繁变更的需求的一种软件开发模式。
它意味着拿出一个代码块,把它准备好却不去执行它。这个代码块以后可以被你程序的其他部分调用
DRY(Don't Repeat Yourself,不要重复自己)——软件工程原则
定义一族算法,把它们封装起来(称为“策略”)💯
标准看作filter方法的不同行为。 和“策略设计模式”相关。
在这里,算法族就是ApplePredicate,不同的策略就是AppleHeavyWeightPredicate和AppleGreenColorPredicate。
:::info
行为参数化:让方法接受多种行为(策略)作为参数,并在内部使用,来完成不同的行为
:::
参数化filterApples的行为并传递不同的筛选策略
多种行为,一个参数
行为参数化的好处在于你可以把迭代要筛选的集合的逻辑与对集合中每个元素应用的行为区分开来。这样可以重复使用同一个方法,给它不同的行为来达到不同的目的
这就是行为参数化是一个有用的概念的原因。
参数化filterApples的行为并传递不同的筛选策略
第3章
Comparator<Apple> byWeight = new Comparator<Apple>() {
public int compare(Apple a1, Apple a2){
return a1.getWeight().compareTo(a2.getWeight());
}
};
//之后(用了Lambda表达式):
Comparator<Apple> byWeight =
(Apple a1, Apple a2) -> a1.getWeight().compareTo(a2.getWeight());
Lambda三个部分
Lambda表达式由参数、箭头和主体组成
- 参数列表——这里它采用了Comparator中compare方法的参数,两个Apple。
- 箭头——箭头->把参数列表与Lambda主体分隔开。
- Lambda主体——比较两个Apple的重量。表达式就是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的重量
:::info
Lambda语法根据上述语法规
(1) ()-> {}
(2) () -> "Raoul"
(3) () -> {return "Mario";}
(4) (Integer i) -> {return "Alan" + i;}
(5) (String s) -> "Iron Man" 等价于 (String s) -> {return "Iron Man";}
:::
使用案例 | Lambda示例 |
---|---|
布尔表达式 | (List<pString> list) -> list.isEmpty() |
创建对象 | () -> new Apple(10) |
消费一个对象 | (Apple a) -> { System.out.println(a.getWeight());} |
从一个对象中选择/抽取 | (String s) -> s.length() |
组合两个值 | (int a, int b) -> a * b |
比较两个对象 | (Apple a1, Apple a2) -> a1.getWeight().compareTo(a2.getWeight()) |
你可以在函数式接口上使用Lambda表达式。函数式接口就是只定义一个抽象方法的接口
public interface Adder {
int add(int a, int b);
}
public interface SmartAdder extends Adder {
int add(double a, double b);
}
public interface Nothing {
}
:::info
答案:只有Adder是函数式接口。
SmartAdder不是函数式接口,因为它定义了两个叫作add的抽象方法(其中一个是从Adder那里继承来的)。
Nothing也不是函数式接口,因为它没有声明抽象方法
:::
Lambda:环绕执行模式
public class ExecuteAround {
private static final String FILE = ExecuteAround.class.getResource("./data.txt").getFile();
public static void main(String... args) throws IOException {
// method we want to refactor to make more flexible
String result = processFileLimited();
System.out.println(result);
System.out.println("---");
String oneLine = processFile((BufferedReader b) -> b.readLine());
System.out.println(oneLine);
String twoLines = processFile((BufferedReader b) -> b.readLine() + b.readLine());
System.out.println(twoLines);
}
public static String processFileLimited() throws IOException {
try (BufferedReader br = new BufferedReader(new FileReader(FILE))) {
return br.readLine();
}
}
public static String processFile(BufferedReaderProcessor p) throws IOException {
try (BufferedReader br = new BufferedReader(new FileReader(FILE))) {
return p.process(br);
}
}
public interface BufferedReaderProcessor {
String process(BufferedReader b) throws IOException;
}
}
三个泛型函数式接口:Predicate、Consumer和Function<T,R>
函数式接口 | Predicate | Consumer |
---|---|---|
Predicate | T -> boolean | IntPredicate,LongPredicate,DoublePredicate |
Consumer | T -> void | IntConsumer,LongConsumer,DoubleConsumer |
Function<T, R> | T -> R | IntFunction,IntToDoubleFunction,IntToLongFunction,LongFunction, |
LongToDoubleFunction,LongToIntFunction,DoubleFunction, | ||
DoubleToIntFunction,DoubleToLongFunction,ToIntFunction, | ||
ToDoubleFunction,ToLongFunction | ||
Supplier | () -> T | BooleanSupplier, IntSupplier,LongSupplier, DoubleSupplier |
UnaryOperator | T -> T | IntUnaryOperator,LongUnaryOperator,DoubleUnaryOperator |
BinaryOperator | (T, T) -> T | IntBinaryOperator,LongBinaryOperator,DoubleBinaryOperator |
BiPredicate<T, U> | (T, U) -> boolean | |
BiConsumer<T, U> | (T, U) -> void | ObjIntConsumer,ObjLongConsumer,ObjDoubleConsumer |
BiFunction<T, U, R> | (T, U) -> R | ToIntBiFunction<T, U>,ToLongBiFunction<T, U>,ToDoubleBiFunction<T, U> |
Lambda及函数式接口的例子
使用案例 | Lambda的例子 | 对应的函数式接口 |
---|---|---|
布尔表达式 | (List list) -> list.isEmpty() | Predicate |
创建对象 | () -> new Apple(10) | Supplier |
消费一个对象 | (Apple a) -> System.out.println(a.getWeight()) | Consumer |
从一个对象中选择/提取 | (String s) -> s.length() | Function<String, Integer> or ToIntFunction |
合并两个值 | (int a, int b) -> a * b | IntBinaryOperator |
比较两个对象 | (Apple a1, Apple a2) -> a1.getWeight().compareTo(a2.getWeight()) | Comparator or BiFunction<Apple, Apple, Integer> or ToIntBiFunction<Apple, Apple> |
异常、Lambda,还有函数式接口
函数式接口中的任何一个都不允许抛出受检异常(checked exception)。如果你需要Lambda表达式来抛出异常,有两种办法:定义一个自己的函数式接口,并声明受检异常,或者把Lambda包在一个try/catch块中。
@FunctionalInterface
public interface BufferedReaderProcessor {
String process(BufferedReader b) throws IOException;
}
BufferedReaderProcessor p = (BufferedReader br) -> br.readLine();
但是你可能是在使用一个接受函数式接口的API,比如Function<T, R>,没有办法自己创建一个(你会在下一章看到,Stream API中大量使用了表3-2中的函数式接口)。这种情况下
Function<BufferedReader, String> f =
(BufferedReader b) -> {
try {
return b.readLine();
}
catch(IOException e) {
throw new RuntimeException(e);
}
};
类型检查、类型推断以及限制
List<Apple> heavierThan150g =
filter(inventory, (Apple apple) -> apple.getWeight() > 150);
类型检查过程分解如下。
- 第一,你要找出
filter
方法的声明。 - 第二,要求它是
Predicate<Apple>
(目标类型)对象的第二个正式参数。 - 第三,
Predicate<Apple>
是一个函数式接口,定义了一个叫作test
的抽象方法。 - 第四,
test
方法描述了一个函数描述符,它可以接受一个Apple
,并返回一个boolean
。 - 第五,
filter
的任何实际参数都必须匹配这个要求。
类型检查——为什么下面的代码不能编译呢?
Object o = () -> { System.out.println("Tricky example"); };
//答案:Lambda表达式的上下文是Object(目标类型)。
//但Object不是一个函数式接口。为了解决这个问题,你可以把目标类型改成Runnable,
//它的函数描述符是() -> void:
Runnable r = () -> { System.out.println("Tricky example"); };
// 还可以通过强制类型转换将Lambda表达式转换成Runnable,
// 显式地生成一个目标类型,以这种方式来修复这个问题:
Lambda表达式只能捕获指派给它们的局部变量一次。(注:捕获实例变量可以被看作捕获最终局部变量this。) 例如,下面的代码无法编译,因为portNumber变量被赋值两次
int portNumber = 1337;
Runnable r = () -> System.out.println(portNumber); ←---- 错误:Lambda表达式引用的局部变量必须是最终的(final)或事实上最终的portNumber = 31337;
第一,实例变量和局部变量背后的实现有一个关键不同。
实例变量都存储在堆中,局部变量则保存在栈上。
如果Lambda可以直接访问局部变量,而且Lambda是在一个线程中使用的,则使用Lambda的线程,可能会在分配该变量的线程将这个变量收回之后,去访问该变量。
因此,Java在访问自由局部变量时,实际上是在访问它的副本,而不是访问基本变量。如果局部变量仅仅赋值一次那就没有什么区别了——因此就有了这个限制。第二,这一限制不鼓励你使用改变外部变量的典型命令式编程模式(我们会在以后的各章中解释,这种模式会阻碍很容易做到的并行处理)。
方法引用:实际调用这个方法,只是引用了它的名称
方法引用返回的是接口的实现,方法调用返回的是方法的返回值
private static <T> Iterable<T> itToIterable(Stream<T> stream) {
return stream::iterator; //编译通过此处返回的是Iterable
}
private static <T> Iterable<T> itToIterablePro(Stream<T> stream) {
return stream.iterator(); //编译失败此处返回的是iterator
}
Lambda | 等效的方法引用 |
---|---|
(Apple apple) -> apple.getWeight() | Apple::getWeight |
() -> Thread.currentThread().dumpStack() | Thread.currentThread()::dumpStack |
(str, i) -> str.substring(i) | String::substring |
(String s) -> System.out.println(s) (String s) -> this.isValidName(s) | System.out::println this::isValidName |
Lambda表达式重构为等价方法引用的简易速查表
谓词、函数复合
谓词接口包括三个方法:negate、and和or,让你可以重用已有的Predicate来创建更复杂的谓词
函数复合:Function接口为此配了andThen和compose两个默认方法
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
Function<Integer, Integer> f = x -> x + 1;
Function<Integer, Integer> g = x -> x * 2;
Function<Integer, Integer> h = f.compose(g);
←---- 数学上会写作f(g(x))或(f o g)(x)
int result = h.apply(1); ←---- 这将返回3
compose的话,它将意味着f(g(x)),andThen则意味着g(f(x))
函数:
\(f(x)=x+10\)
积分:
\(\int_{3}^{7} (x)dx,或\int_{3}^{7} (x+10)dx\)
=1/2 × ((3 + 10) + (7 + 10)) × (7 – 3) = 60
integrate(f, 3, 7)
//请注意,你不能简单地写:
integrate(x + 10, 3, 7)
原因有两个。第一,x的作用域不清楚;
第二,这将把x + 10的值而不是函数f传给积分。
数学上{\rm d}x的秘密作用就是说“以x为自变量、结果是x+10的那个函数。
//可以写
integrate((double x) -> x + 10, 3, 7)
//或
integrate((double x) -> f(x), 3, 7)
//或引用
integrate(C::f, 3, 7)
这里C是包含静态方法f的一个类。理念就是把f背后的代码传给integrate方法。
//使用DoubleFunction比Function更高效,因为它避免了结果的装箱操作。
public double integrate(DoubleFunction<Double> f, double a, double b) {
return (f.apply(a) + f.apply(b)) * (b - a) / 2.0;
}
//或者用DoubleUnaryOperator,这样也可以避免对结果进行装箱:
public double integrate(DoubleUnaryOperator f, double a, double b) {
return (f.applyAsDouble(a) + f.applyAsDouble(b)) * (b - a) / 2.0;
}
//必须写f.apply(a),而不是像数学里面写f(a),
//但Java无法摆脱“一切都是对象”的思想——它不能让函数完全独立!
总结:
- Lambda表达式可以理解为一种匿名函数:它没有名称,但有参数列表、函数主体、返回类型,可能还有一个可以抛出的异常的列表。
- Lambda表达式让你可以简洁地传递代码。
- 函数式接口就是仅仅声明了一个抽象方法的接口。
- 只有在接受函数式接口的地方才可以使用Lambda表达式。
- Lambda表达式允许你直接内联,为函数式接口的抽象方法提供实现,并且将整个表达式作为函数式接口的一个实例。
- Java 8自带一些常用的函数式接口,放在
java.util.function
包里,包括Predicate <T>
、Function<T, R>
、Supplier<T>
、Consumer<T>
和BinaryOperator<T>
- 为了避免装箱操作,对
Predicate<T>
和Function<T, R>
等通用函数式接口的基本类型特化:IntPredicate
、IntToLongFunction
等。 - 环绕执行模式(即在方法所必需的代码中间,你需要执行点儿什么操作,比如资源分配和清理)可以配合Lambda提高灵活性和可重用性。
- Lambda表达式所需要代表的类型称为目标类型。
- 方法引用让你重复使用现有的方法实现并直接传递它们。
Comparator
、Predicate
和Function
等函数式接口都有几个可以用来结合Lambda表达式的默认方法。
本文来自博客园,作者:李好秀,转载请注明原文链接:https://www.cnblogs.com/lehoso/p/17650022.html