目录
Rule47 Stream要优先用Collection作为返回类型
Rule42 Lambda优先于匿名类
匿名类不太了解的请先看 Java中四种嵌套类
// Sorting with function objects (Pages 193-4)
public class SortFourWays {
public static void main(String[] args) {
List<String> words = Arrays.asList(args);
// List<String> words = Arrays.asList("51", "12345", "2345", "711");
// Anonymous class instance as a function object - obsolete! (Page 193)
// 1:匿名内部类方式
// idea提示:Anonymous new Comparator<String>() can be replaced with lambda
Collections.sort(words, new Comparator<String>() {
public int compare(String s1, String s2) {
return Integer.compare(s1.length(), s2.length());
}
});
System.out.println(words);
Collections.shuffle(words);
// Lambda expression as function object (replaces anonymous class) (Page 194)
// 2:Lambda带入参方式
// idea提示:Can be replaced with Comparator.comparingInt
Collections.sort(words,
(s1, s2) -> Integer.compare(s1.length(), s2.length()));
System.out.println(words);
Collections.shuffle(words);
// Comparator construction method (with method reference) in place of lambda (Page 194)
// 3:Lambda 使用Comparator.comparingInt方式
Collections.sort(words, comparingInt(String::length));
System.out.println(words);
Collections.shuffle(words);
// Default method List.sort in conjunction with comparator construction method (Page 194)
// 4:直接使用List对象的sort方法
words.sort(comparingInt(String::length));
System.out.println(words);
}
}
如这段代码示例,从原先的匿名内部类5行代码最终直接缩减为1行,同时在不影响理解的基础上,当行代码量也在不断的缩减。需要注意的是,就算是按2的Lambda方式来实现,也不用指定参数类型,因为编译器利用类型推导的过程,根据上下文可以推导出当前类型(java10引入了var关键字、但是有一定的限制条件),同时泛型的作用就体现出来了,只有在极少数情况下需要指定Lambda的参数类型。
Lambda可以大幅缩减代码量,但是如果运用Lambda的方式实现时,如果一个功能不是自描述的、或者代码量超出了几行,那就可以考虑换一种方式来完成,因为Lambda没有名称和文档,短短几行内,大篇幅的Lambda代码可能反而会对阅读性带来一些负面影响。
同时,书中指出,尽可能不要序列化一个Lambda(或者匿名内实例),如果对此不明白的,可以去参照java嵌套类那篇里面的介绍。
// Enum with function object fields & constant-specific behavior (Page 195)
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);
}
// Main method from Item 34 (Page 163)
public static void main(String[] args) {
double x = Double.parseDouble("1");
double y = Double.parseDouble("2");
for (Operation op : Operation.values())
System.out.printf("%f %s %f = %f%n",
x, op, y, op.apply(x, y));
}
}
Rule34用枚举代替int常量 中的代码示例可以改造成上面。其中需要注意的是java.util.function.DoubleBinaryOperator,在Java8中,java.util.function这个包下面提供了很多Function接口,仔细观察就会发现,这些接口都只有一个待实现方法,里面提供的其他方法都是带有默认实现的default方法。这与Java8,“带有单个抽象方法的接口是特殊的,值得特殊对待”观念呼应。
Rule43 方法引用优先于Lambda
首先一个问题什么是方法引用?
用Rule42中的例子来说,words.sort(comparingInt(String::length));,其中String::length就是方法引用,期初我看这种写法时,都会当成是Lambda表达式。看了这部分才知道,这种写法虽然一般都是搭配Lambda表达式来使用,但其本身是有专门的称呼的,就是方法引用(虽然常常会被叫做Lambda写法)。
方法引用类型 | 范例 | Lambda等式 |
---|---|---|
静态 | Integer::parseInt | str -> Integer.parseInt(str) |
有限制 | Instant.now()::isAfter | Instant then = Instant.now(); t -> then.isAfter(t) |
无限制 | String::toUpperCase | str -> str.toUpperCase() |
类构造器 | TreeMap<K, V>::new | () -> new TreeMap<K, V> |
数组构造器 | int[]::new | len -> new int[len] |
相比之下,方法引用常常会比Lambda表达式显得更加简练。但是有时候也有例外的情况,比如自定义了一个类GoshThisClassNameIsHumongous
- service.execute(GoshThisClassNameIsHumongous::action);
- service.execute(() -> action());
相比之下,Lambda表达式会显得更加简练,所以我们日常开发只要遵循一个原则就好,如果方法引用会比较简练,那我们就用方法引用,否则我们就使用Lambda表达式。
Rule44 坚持使用标准的函数接口
- 函数接口:即这个接口主要目的是为了提供给函数式编程所用,即只包含一个抽象方法的接口。可以用注解@FunctionalInterface来进行标明。
- 标准函数接口:java.util.function下面所提供的已经定义好了的函数接口。如果标准函数接口能够满足需求,那么我们就不应该自己再定义一套。
@FunctionalInterface有什么作用
- 告诉大家这个接口是针对Lambda设计的。
- 这个接口只允许有一个抽象方法,不然无法编译。
- 不要提供重载方法,有可能会引起客户端的歧义。
目前java8的java.util.function下面,接口有几十个,但是我们只要记住6个基础的接口,其他的接口可以通过这6个基础的进行类推。本质上就是执行那唯一一个抽象方法的实现,只不过由于参数类型、返回类型,无参、一个参数、两个参数,有无返回值的区别,做了基础的区分而已。
接口 | 函数签名 | 范例 | 备注 |
---|---|---|---|
一元运算符 UnaryOperator<T> | T apply(T t) | String::toUpperCase | Unary代表一个入参、Operator代表返回结果与参数一致的函数 ※继承自Function<T, R>接口,统一了泛型类型 |
二元运算符 BinaryOperator<T> | T apply(T t1, T t2) | BigInteger::add | Binary代表两个入参、Operator代表返回结果与参数一致的函数 ※继承自BiFunction<T, U, R>接口,统一了泛型类型 |
谓词 Predicate<T> | boolean test(T t) | Collection::isEmpty | Predicate代表有一个入参,并返回布尔值的函数 |
函数 Function<T, R> | R apply(T t) | Arrays::asList | Function代表返回类型与入参类型不一致的函数 |
提供者 Supplier<T> | T get() | Instant::now | Supplier代表无参数,返回一个值的函数 |
消费者 Consumer<T> | void accept(T t) | System.out::println | Consumer代表无返回值,有一个参数的函数 |
Rule45 谨慎使用Stream
关于Stream流的基础介绍,请看这篇《Java8 流相关分享》
流的写法虽然可以大大简化代码量,但是运用的不好的话反而会产生负面效应。
// Overuse of streams - don't do this! (page 205)
public class StreamAnagrams {
public static void main(String[] args) throws IOException {
Path dictionary = Paths.get(args[0]);
int minGroupSize = Integer.parseInt(args[1]);
try (Stream<String> words = Files.lines(dictionary)) {
words.collect(
groupingBy(word -> word.chars().sorted()
.collect(StringBuilder::new,
(sb, c) -> sb.append((char) c),
StringBuilder::append).toString()))
.values().stream()
.filter(group -> group.size() >= minGroupSize)
.map(group -> group.size() + ": " + group)
.forEach(System.out::println);
}
}
}
就比如这段代码,很难快速理解它的意图是什么。适当的做一些方法抽出,反而会提高代码的阅读性。
// Tasteful use of streams enhances clarity and conciseness (Page 205)
public class HybridAnagrams {
public static void main(String[] args) throws IOException {
Path dictionary = Paths.get(args[0]);
int minGroupSize = Integer.parseInt(args[1]);
try (Stream<String> 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));
}
}
private static String alphabetize(String s) {
char[] a = s.toCharArray();
Arrays.sort(a);
return new String(a);
}
}
有一些工作适合用Lambda表达式来完成,但是Lambda表达式并不是万能的,比如下面的一些情况:
- 代码块中,可以读取和修改范围内,任意局部变量;Lambda只能读取final变量,而且无法改值
- 代码块中,可以return、break、continue,或者抛出异常;Lambda都不可以
反之,下面的情况适合运用Lambda完成:
- 统一转换元素的序列
- 过滤元素的序列
- 利用单个操作(如添加、连接或者计算其最小值)合并元素顺序
- 将元素的序列存放到一个集合中,根据某些公共属性进行分组
- 搜索满足某些条件的元素的序列
Rule46 优先选择Stream中无副作用的函数
Stream副作用在另一篇Stream中间也有过介绍。理想中的Stream侧重的是把一系列的操作尽可能的成为纯函数,不依赖任何可变状态、也不更新任何状态。函数的结果只取决于输入的函数,尽可能的选用无状态的、无副作用的方法。
// Frequency table examples showing improper and proper use of stream (Page 210-11)
public class Freq {
public static void main(String[] args) throws FileNotFoundException {
File file = new File(args[0]);
// Uses the streams API but not the paradigm--Don't do this!
Map<String, Long> freq1 = new HashMap<>();
try (Stream<String> words = new Scanner(file).tokens()) {
words.forEach(word -> {
freq1.merge(word.toLowerCase(), 1L, Long::sum);
});
}
// Proper use of streams to initialize a frequency table (
Map<String, Long> freq2;
try (Stream<String> words = new Scanner(file).tokens()) {
freq2 = words
.collect(groupingBy(String::toLowerCase, counting()));
}
}
}
这两种实现方式,freq1只是看起来是Stream操作,但是其实本质还是迭代器操作,而freq2才是纯Stream操作。forEach操作应该只用于报告Stream操作的结果,而不是执行计算。
Rule47 Stream要优先用Collection作为返回类型
三大类、Collection集合、数组、迭代器。一般仅关注循环,推荐用迭代器;如果返回基本类型、同时对性能有要求,推荐用数组;其他情况一般都推荐用集合返回。
※遗憾的是,关于书中这部分4页的篇幅,我没太理解到核心想要表达什么内容。
Rule48 谨慎使用Stream并行
Java5引入了JUC、Java7引入了Fork-Join(处理并行分解的高性能框架)、Java8引入了Stream(只需要parallel就可以实现并行处理、内部实现是默认的ForkJoinPool)。编写并发编程越来越容易、但是还是需要关注并发编程的安全性和活性失败问题。
※活性失败:A修改了共享变量,此时线程B感知不到此共享变量的改变,就叫活性失败。常见的对策就是两个线程对此贡献变量拥有happens-before关系,最常见的就是volatile 或 加锁。(关于并发编程后面会陆续完善整理)
// Parallel stream-based program to generate the first 20 Mersenne primes - HANGS!!! (Page 222)
public class ParallelMersennePrimes {
public static void main(String[] args) {
primes().map(p -> TWO.pow(p.intValueExact()).subtract(ONE))
.parallel()
.filter(mersenne -> mersenne.isProbablePrime(50))
.limit(20)
.forEach(System.out::println);
}
static Stream<BigInteger> primes() {
return Stream.iterate(TWO, BigInteger::nextProbablePrime);
}
}
这段代码用了parallel()做并行,但是由于源头是Stream.iterate、或者用了limit()有状态中间操作,最终的效果与预期会完全相反,甚至引起活性失败,导致一致没有进度。什么场合适合用parallel(),数据源可以被切分成精准的、轻松地分成任意大小的子范围。例如:ArrayList、HashMap、ConcurrentHashMap、HashSet、数组、int范围、long范围等。
这些数据结构还拥有另一个特性,优异的引用局部性。序列化元素的引用一起被保存在内存中。被那些引用访问到的对象在内存中可能并不是一个挨着一个的,这种不连续性会降低引用局部性。在并行执行的时候,线程需要等待将对象从内存获取到处理器的缓存中,就容易出现线程等待。引用局部性最好的数据结构是基本类型数组,因为他们本身就是相邻的保存在内存之中。
如果大量的操作逻辑都是放在终端方法中的,而且还是固有顺序的执行,也会降低并行时的效率。并行操作最理想的终端操作是做减法,用一个reduce方法,将所有pipeline结果合并起来。或者利用短路(short-circuiting)方法也可以并行。
由Stream的collect执行的操作,并不是最好的并行方法,因为本身集合的合并就成本比较高。
如果想要使用并行操作,请先确保需要这样做,同时最好在真实环境中进行性能测试。
本文技术菜鸟个人学习使用,如有不正欢迎指出修正。xuweijsnj