第42条 Lambda优先于匿名类
函数对象:只带有单个抽象方法的接口或者抽象类,其实例称之为函数对象。
- 删除所有lambda参数的类型,除非其存在能够使程序更加清晰。
- 与方法和类不同,Lambda没有名称和文档,如果一个计算本身不是不言自明的,或者该计算超出了三行的限制,那么就不要把它放入一个Lambda中。
- 尽可能不要序列化一个Lambda或者匿名内部类实例。
- 不要为函数式接口或者函数式抽象类去创建匿名内部类对象。
Java中增加了Lambda后,许多地方可以使用函数对象了,如下第34条的Operation枚举类型,可以从旧版本优化为新版本:
// Enum type with constant-specific class bodies & data (Item 34)
public enum Operation {
PLUS("+") {
public double apply(double x, double y) {
return x + y;
}
},
MINUS("-") {
public double apply(double x, double y) {
return x - y;
}
},
TIMES("*") {
public double apply(double x, double y) {
return x * y;
}
},
DIVIDE("/") {
public double apply(double x, double y) {
return x / y;
}
};
private final String symbol;
Operation(String symbol) {
this.symbol = symbol;
}
@Override
public String toString() {
return symbol;
}
public abstract double apply(double x, double y);
}
使用Lambda后的版本
// Enum with function object fields & constant-specific behavior
public enum Operation {
PLUS("+", (x, y) -> x + y),
MINUS("-", (x, y) -> x - y),
TIMES("*", (x, y) -> x * y),
DIVIDE("/", (x, y) -> x / y);
private final String symbol;
private final DoubleBinaryOperator op;
Operation(String symbol, DoubleBinaryOperator op) {
this.symbol = symbol;
this.op = op;
}
@Override
public String toString() {
return symbol;
}
public double apply(double x, double y) {
return op.applyAsDouble(x, y);
}
}
第43条 方法引用优先于Lambda
- 当方法引用更简洁时,使用方法引用;反之,还是使用Lambda。
方法引用类型 | 范例 | Lambda等式 |
---|---|---|
静态引用 | Integer::parseInt |
str -> Integer.parseInt(str) |
有限实例引用 | Instant.now()::isAfter |
Instant then = Instant.now();t -> then.isAfter(t) |
无限实例引用 | String::toLowerCase |
str -> str.toLowerCase() |
类构造器 | TreeMap |
() -> new TreeMap |
数组构造器 | int[]::new |
len -> new int[len] |
- 有限方法引用和静态引用相似,都是所提供的方法引用与函数式接口的参数列表以及返回值类型一致,有限实例方法引用其实不需要实例对象。
- 无限方法引用,则需要一个特定的实例对象,该实例对象加上引用方法中的参数列表,才与函数式接口的参数列表一致。这个实例对象也就是原书上所说的
an additional parameter
。
注:书中的receiving object
其实指的是根据方法引用所创建的函数对象。
附上Java官网中的分类及其示例:
Kind | Example |
---|---|
Reference to a static method | ContainingClass::staticMethodName |
Reference to an instance method of a particular object | containingObject::instanceMethodName |
Reference to an instance method of an arbitrary object of a particular type | ContainingType::methodName |
Reference to a constructor | ClassName::new |
静态方法引用
public class Person {
public enum Sex {
MALE, FEMALE
}
String name;
LocalDate birthday;
Sex gender;
String emailAddress;
public int getAge() {
// ...
}
public LocalDate getBirthday() {
return birthday;
}
public static int compareByAge(Person a, Person b) {
return a.birthday.compareTo(b.birthday);
}
}
//Person::compareByAge就是静态方法引用
有限实例方法引用
String[] stringArray = { "Barbara", "James", "Mary", "John",
"Patricia", "Robert", "Michael", "Linda" };
Arrays.sort(stringArray, String::compareToIgnoreCase);
无限实例方法引用
class ComparisonProvider {
public int compareByName(Person a, Person b) {
return a.getName().compareTo(b.getName());
}
public int compareByAge(Person a, Person b) {
return a.getBirthday().compareTo(b.getBirthday());
}
}
ComparisonProvider myComparisonProvider = new ComparisonProvider();
Arrays.sort(rosterAsArray, myComparisonProvider::compareByName);
构造器方法引用
public static , DEST extends Collection>
DEST transferElements(
SOURCE sourceCollection,
Supplier collectionFactory) {
DEST result = collectionFactory.get();
for (T t : sourceCollection) {
result.add(t);
}
return result;
}
//类构造器方法引用
Set rosterSet = transferElements(roster, HashSet::new);
//等价于
Set rosterSet = transferElements(roster, HashSet::new);
第44条 坚持使用标准的函数接口
- 只要标准的函数接口能够满足需求,通常应优先考虑吗,而不是再专门构建一个新的函数接口。
- 千万不要用带包装类型的基础函数接口来代替基础类型的函数接口
- 必须始终用@FunctionalInterface注解对自己编写的函数接口进行标准
- 在Java9中,java.util.Function一共有43个接口,其中有6个基础接口:
- 参数类型和返回类型一致的有2个,分别是1个参数和两个参数的;
- 返回类型为boolean的有1个;
- 参数类型和返回类型不一致的有1个;
- 无返回值和无形参的各一个,共2个;
接口 | 函数签名 | 范例 |
---|---|---|
UnaryOperator |
T apply(T t) |
String::toLowerCase |
BinaryOperator |
T apply(T t1, T t2) |
BigInteger::add |
Predicate |
boolean test(T t) |
Collection::isEmpty |
Function |
R apply(T t) |
Arrays::asList |
Supplier |
T get() |
Instant::now |
Consumer |
void accept(T t) |
System.out::println |
- 其中每个基础接口各自有3种变体,分别作用于将T的类型替换为基本类型int、long和double,共6*3种。
接口 | Int变种 | Long变种 | Double变种 |
---|---|---|---|
UnaryOperator |
IntUnaryOperator |
LongUnaryOperator |
DoubleUnaryOperator |
BinaryOperator |
IntBinaryOperator |
LongBinaryOperator |
DoubleBinaryOperator |
Predicate |
IntPredicate |
LongPredicate |
DoublePredicate |
Function |
IntFunction |
LongFunction |
DoubleFunction |
Supplier |
IntSupplier |
LongSupplier |
DoubleSupplier |
Consumer |
IntConsumer |
LongConsumer |
DoubleConsumer |
- 针对于
Function
还有9种变体,其中6种用于源类型和结果类型都是基础类型,但又互不相同的情况,还有三种是将源类型为基础类型,结果类型是引用类型的(即返回了个对象):
Int |
Long |
Double |
Obj |
|
---|---|---|---|---|
Int |
- | IntToLongFunction |
IntToDoubleFunction |
IntFunction |
Long |
LongToIntFunction |
- | LongToDoubleFunction |
LongFunction |
Double |
DoubleToIntFunction |
DoubleToLongFunction |
- | DoubleFunction |
Predicate
Function
Consumer
还有各自有一个带有两个参数的版本,共3个;BiFunction
还有返回相关的三个基础类型的变种共3个;Consumer
接口还有带有1个引用类型参数和1个基础类型参数的变种,针对三种基础类型,共3个。
源类型 | 变种1 | 变种2 | 变种3 |
---|---|---|---|
Bi |
BiPredicate |
BiFunction |
BiConsumer |
BiFunction |
ToIntBiFunction |
ToLongBiFunction |
ToDoubleBiFunction |
Consumer |
ObjDoubleConsumer |
ObjIntConsumer |
ObjLongConsumer |
- 最后有
Supplier
的一个变种BooleanSupplier
,如下:
@FunctionalInterface
public interface BooleanSupplier {
/**
* Gets a result.
*
* @return a result
*/
boolean getAsBoolean();
}
第45条 谨慎使用Stream
- 滥用Stream会使程序代码难以读懂和维护
- 在没有显式类型的情况下,仔细命名Lambda参数,这对Stream pipeline的可读性至关重要
- 为了可读性,需要时在Stream Pipline中使用helper方法
- 避免利用Stream处理char值
- 重构现有代码来使用Stream,且只在必要的时候才在新代码中使用
- 不确定迭代和Stream哪种比较好的情况下,两种都试下比较下
stream的定义及抽象原理
在Java8中新增了Stream API,用于简化串行或者并行的大批量操作。这个API提供了两个关键的抽象:
- stream(流),表达了一个有限或者无限的数据元素序列。
- stream pipeline(流管道),表达了这些数据元素的多级计算。
stream的来源包括集合、数组、文件、正则表达式模式匹配器、伪随机数生成器以及其他stream。其中的数据元素类型可以是引用类型和基础类型(int、long、double)。一个stream pipeline中包含了一个源stream ,接着是零至多个中间操作和一个终止操作。每个中间操作都会以某种方式对接收到的stream转换为另一个stream,终止操作会在最后一个中间操作产生的stream上执行一个最终的计算。
stream pipeline通常是lazy的,直到调用终止操作时才会开始计算,对于完成终止操作不需要的数据元素,将永远不会计算。这种计算使得无限stream变为可能。而没有终止操作的stream pipeline将是一个静默的无操作指令,千万不能忘记终止操作。
在默认情况下,stream pipeline是按照顺序执行的,要使其并发执行,只需在该pipeline的任何stream上调用parallel方法即可。但通常不建议这么做。
补充说明
helper方法指的是对pipeline中的复杂计算流程,可以单独封装出来作为一个函数。
public class Anagrams {
public static void main(String[] args) throws IOException {
Path dictionary = Paths.get(args[0]);
int minGroupSize = Integer.parseInt(args[1]);
try (Stream words = Files.lines(dictionary)) {
words.collect(groupingBy(word -> alphabetize(word)))
.values().stream()
.filter(group -> group.size() >= minGroupSize)
.forEach(g -> System.out.println(g.size() + ": " + g));
}
}
//helper方法
private static String alphabetize(String word) {
String s = word.chars().sorted()
.collect(StringBuilder::new,
(sb, c) -> sb.append((char) c),
StringBuilder::append).toString();
return s;
}
}
中间操作flatMap可以将Stream中的每个元素都映射产生一个Stream,然后将这些新的Stream全部合并到一个Stream中。
// Iterative Cartesian product computation
private static List newDeck() {
List result = new ArrayList<>();
for (Suit suit : Suit.values())
for (Rank rank : Rank.values())
result.add(new Card(suit, rank));
return result;
}