2019年1月20日星期日

Effective Java 3rd edition 读书笔记


最近把去年出版的 Effective Java 3rd Edition 的新章节读完了,把笔记统一整理一下。

Lambdas and Streams

Item 42: Prefer lambdas to anonymous class

Java 8 以前,模板方法、函数方法基本是用匿名类实现:
// before JDK1.8  use anonymous object
// Anonymous class instance as a function object - obsolete! 
Collections.sort(words, new Comparator<String>() { 
    public int compare(String s1, String s2) { 
        return Integer.compare(s1.length(), s2.length()); 
    } 
}); 
Java 8 开始,可以使用lambda表达式:
// before JDK1.8  use anonymous object
// Anonymous class instance as a function object - obsolete! 
Collections.sort(words, new Comparator<String>() { 
    public int compare(String s1, String s2) { 
        return Integer.compare(s1.length(), s2.length()); 
    } 
}); 
Java 8 中的 lambda 表达式时基于函数式接口(Function Interface),它就是一个有且仅有一个抽象方法,但是可以有多个非抽象方法的接口。函数式接口可以被隐式转换为 lambda 表达式。
比如 上面Collections.sort中的 Comparator 变量,它的函数式接口定义为:
 */
@FunctionalInterface
public interface Comparator<T> {
    public static <T> void sort(List<T> list, Comparator<? super T> c) {
        list.sort(c);
    }
// other method …
}
所以从Java 8 开始,不要创建一个匿名内部类作为函数对象,除非函数式方法入参没有定义函数式接口。

Item 43: Prefer method references to lambdas

Java 8 开始提供一种比 lambda 更简洁的方式作为函数对象:方法引用(method references)
方法引用和 lambda 表达式对比:
// method reference
map.merge(key, 1, Integer::sum);
// lambda
map.merge(key, 1, (count, incr) -> count + incr); 
目前方法引用唯一用途是用来简化 lambda 表达式,用方法名来代替 lambda。
方法引用的几种形式:
引用静态方法
ContainingClass::staticMethodName
例子: String::valueOf,对应的Lambda:(s) -> String.valueOf(s)
比较容易理解,和静态方法调用相比,只是把.换为::
引用特定对象的实例方法
containingObject::instanceMethodName
例子: x::toString,对应的Lambda:() -> this.toString()
与引用静态方法相比,都换为实例的而已。
引用特定类型的任意对象的实例方法
ContainingType::methodName
例子: String::toString,对应的Lambda:(s) -> s.toString()
太难以理解了。难以理解的东西,也难以维护。建议还是不要用该种方法引用。
实例方法要通过对象来调用,方法引用对应Lambda,Lambda的第一个参数会成为调用实例方法的对象。
引用构造函数
ClassName::new
例子: String::new,对应的Lambda:() -> new String()
构造函数本质上是静态方法,只是方法名字比较特殊。
有些情况下,使用方法引用的代码 会比 lambda 更长
比如这个在 GoshThisClassNameIsHumongous 类中的方法:
用方法引用
service.execute(GoshThisClassNameIsHumongous::action); 
等价的 lambda 是:
service.execute(() -> action());
下面是各种方法引用的例子:

Method ref TypeExampleLambda Equivalent
StaticInteger::parseIntstr -> Inter.parseInt(str)
BoundInstant.now()::isAfterInstant then = Instant.now(); t -> then.isAfter(t)
UnboundString::toLowerCasestr -> str.toLowerCase()
Class ConstructorTreeMap<K, V> :: new() -> new TreeMap<K, V>
Array Constructorint[] :: newlen -> new int[len]
: Bound unbound reference method 用法语义比较模糊,感觉用 Lambda 描述更清楚。
总而言之,当方法引用更简单明了时,就用它,否则就用 lambda。

Item 44: Favor the use of standard functional interfaces

JDK 包中已经默认提供了多种函数式接口,所以尽量使用自带的而不是自己写函数式接口。
比如下面的例子,我们自定义了一个函数式接口,需要让重写的类有一一个 remove 的默认方法,入参是 map自己的引用和eledest元素,使用fuction interface可以先定义一个接口:
// Unnecessary functional interface; use a standard one instead. 
@FunctionalInterface interface EldestEntryRemovalFunction<K,V>{ 
    boolean remove(Map<K,V> map, Map.Entry<K,V> eldest); 
} 
其实这个接口,JDK已经有默认实现,就是BiPredicate 接口。

标准函数式接口分类

Java 自带的函数式接口分为几类:Operator,Predicate,Function,Supplier,Consumer。
  1. Operator 接口表示入参和返回值是同一种类型的函数,比如下面代表一元、二元运算的接口:
    @FunctionalInterface
    public interface UnaryOperator<T> extends Function<T, T> {
        static <T> UnaryOperator<T> identity() {
            return t -> t;
    }
    @FunctionalInterface
    public interface BinaryOperator<T> extends BiFunction<T,T,T> {
        public static <T> BinaryOperator<T> minBy(Comparator<? super T> comparator) {
            Objects.requireNonNull(comparator);
            return (a, b) -> comparator.compare(a, b) <= 0 ? a : b;
        }
    
        public static <T> BinaryOperator<T> maxBy(Comparator<? super T> comparator) {
            Objects.requireNonNull(comparator);
            return (a, b) -> comparator.compare(a, b) >= 0 ? a : b;
        }
    }
  2. Predicate 有些地方叫做谓词函数,用来表示返回值是boolean 的函数,如:
    @FunctionalInterface
    public interface Predicate<T> {
        boolean test(T t);
        ...
    }
  3. Function 类型的函数接口表示入参和返回值不同类型的函数,意为将参数T传给一个函数,返回R。即 R = Function(T),比如:
    @FunctionalInterface
    public interface BiFunction<T, U, R> {
        R apply(T t, U u);
    }
  4. Supplier 类型的接口,表示一个没有入参,有返回值的函数,例如:
    @FunctionalInterface
    public interface Supplier<T> {
        T get();
    }
    Supplier 常用于 Stream计算中new 对象。
  5. Consumer 类型的函数接口表示接受一个泛型参数,但是不返回数据,如:
    @FunctionalInterface
    public interface Consumer<T> {
        void accept(T t);
        ...
    }

@FunctionalInterface 注解

@FunctionalInterface 有点像 @Override 注解,注释告诉编译器这是个函数式接口,没有任何功能上的作用。只有一个抽象方法的接口,不使用这个注解也是函数式接口。
需要函数式接口时,先看一下Java 提供的标准接口能否满足,不满足再自己实现,实现时最好遵循标准函数式接口的定义风格。

Item 45: Use streams judiciously

一些 stream API 的概念

stream API 里有两种抽象概念:
  1. the stream: 表示Java 中的各种集合
  2. stream pipe-line: 表示对于这些集合的多重计算操作,有0个或多个 intermediate operations 一个 terminal operation 组成。 intermediate operations 从一个stream传到另一个stream,terminal operation 对接最后一个 intermediate operations,对stream做最后一次操作(一般是排序、打印、转换集合结果等操作)。
Stream pipelines 是懒式(lazily)执行的,只有当碰到 terminal operations 时,前面所有的 intermediate operations 才会执行。不带 terminal operations的stream操作是静默的,永远不会执行。
stream api 是流式的。
默认情况下stream pipleline 是串行(squentially)执行的

过度使用 stream 让程序难以阅读和维护

在没有显示类型的情况下,合适的参数命名是保持stream 流有良好可读性的关键。
避免使用 stream去处理字符(char)数据
例如:
"Hello world!".chars().forEach(System.out::print);"Hello world!".chars()返回的是int[]数组的stream,所以输出的不是 Hello world!而是 7210110810811132119i11111410810033。因为stream API中只支持 int long double三种primitive ype 的stream,没有char的stream。
只有在值得这么做的情况下,才需要使用sream重构已存在的代码或在新代码中使用sream。否则容易引入新问题。

几种适合使用stream api的场景

  1. 统一转换元素序列(如集合中的元素类型)。
  2. 过滤元素序列。
  3. 对序列元素进行单一聚合操作(如计算和,最小值,连接等)
  4. 将序列元素转化为集合,或者以特定条件为它们分组。
  5. 以指定条件搜索序列元素。
当不知道是否需要使用sream时,两种方案都尝试,看哪个更合适。

Item 46: Prefer side-effect-free functions in streams (没有副作用)

尽量把foreach 操作用于输出stream结果,而不是计算逻辑。
总的来说,stream 的流式编程是没有副作用(side-effect-free)的函数对象。
为了正确使用 stream,必须知道 collectors 操作,它用于产生输出集合。几个重要的collectors 工厂是:toList, toSet, toMap, groupingBy, joining.

Item 47: Prefer Collection to Stream as a return type

Collection 和它的子类最适合作为返回序列数据的方法的返回值类型。Stream 没有继承或实现 Interable 接口,所以它不能使用 for-each 遍历,所以当某个方法需要返回序列时,优先使用 Collection 返回而不是 Stream。因为Collection 接口不仅可以被for-each遍历,它还有一个sream方法。你的方法的调用方可能需要返回的序列结果进行sream运算,或者仅仅只要遍历访问它。

Item 48: Use caution when making streams parallel

在并发编程中,违反安全性和活跃度(liveness)的情况是没法避免的,stream 的 parallel 也不例外。
看下面这段代码:

// Stream-based program to generate the first 20 Mersenne primes 
// if change to parallel invoke, it will let cpu usage to 90 percent
public static void main(String[] args) {
    primes().map(p -> TWO.pow(p.intValueExact()).subtract(ONE))
        .filter(mersenne -> mersenne.isProbablePrime(50)).limit(20)
        .forEach(System.out::println); 
} 
static Stream<BigInteger> primes() {
    return Stream.iterate(TWO, BigInteger::nextProbablePrime); 
} 
假如源代码是Stream.iterate,或者有使用limit(),再使用 parallel 并行化一个stream pipeline 不大可能提高性能。所以不要随意地使用 parallel 来并行化 stream 计算。
作为一个规则,在以下类型数据上使用stream parallel 比较容易获得性能收益:ArrayList, HashMap, HashSet, and ConcurrentHashMap instances; arrays; int ranges; and long ranges. 因为这些数据结构易于拆分子任务,且有较好的存储局部性(locality of reference),所以能在 stream 的并行任务上获得较好效果。
局部性原理可以参考:
如果自己定义了 Stream,Iterable,Collection 接口的接口实现,想要在使用parallel时实现好的性能,需要重写 spliterator 方法。
在正确使用的前提下,使用 parallel 处理stream 流,可以得到近似处理器核心数的线性性能提升。某些领域,比如机器学习和数据处理,特别适合这种性能提升。
总的来说,不要尝试使用parallel处理stream,除非你有足够的理由去保证这么做能大幅提高性能并保证程序正确性。确保你的代码在使用parallel后仍然正确。

Optionals

Java 8 中新增的 Optional 容器类,它封装了可能为null的对象,强制使用方在使用时进行检查,防止NPE问题。
容器类型(collections, maps, streams, arrays, and optionals )不应该被包换到Optional对象中。
当你的方法可能没有返回值时,你应该使用Optional<T>作为返回值类型,这样调用方必须处理没有返回值的情况,避免了NPE问题。
不可以用 Optional 去包装原始类型(Boolean, Byte, Character, Short, and Float),因为 Optional 会对其进行两次装箱(boxing)。这种情况应该直接使用 OptionalInt, OptionalLong, and OptionalDouble
不要使用Optional对象最为Map的key或者数组的元素。
因为涉及的装箱拆箱操作,对于性能要求严苛的方法,还是使用返回null的方式处理控制比较合适。

Default methods in interfaces

Java 8中引入默认方法是为了让老接口支持lambda表达式。在老接口中添加默认方法,这些接口的实现类不会在编译器报错。

try-with-resources

Item 9: Prefer try-with-resources to try-finally

Java 7 以后,应该总是使用 try-with-resources 方式而不是 try-finally 方式处理资源操作。
自动关闭资源的语法:
// try-with-resources - the the best way to close resources! 
static String firstLineOfFile(String path) throws IOException { 
    try (BufferedReader br = new BufferedReader( 
    new FileReader(path))) { 
    return br.readLine(); 
    } 
} 
要使用 try-with-resources 语法,资源类必须实现 AutoCloseable 接口。

@safeVarags

混用泛型和可变参数时,可能存在安全问题:

// Mixing generics and varargs can violate type safety! 
static void dangerous(List<String>... stringLists) {
    List<Integer> intList = List.of(42);
    Object[] objects = stringLists;
    objects[0] = intList;
    // Heap pollution
    String s = stringLists[0].get(0); // ClassCastException 
} 
保存一个值到泛型的可变参数数组中是不安全的,编译器也会提出警告:
Warning look like this:
warning: [unchecked] Possible heap pollution from 
    parameterized vararg type List<String> 
@SafeVarargs 提供了一种声明,表示该方法的作者保证该方法是类型安全的,编译器会忽略安全检查,不显示警告。
只要有混用泛型和可变参数的方法,都要声明@SafeVarargs ,同时作者必须保证方法内部不会出现上面例子的Heap pollution 的类型安全问题。
以下情况的可变参数方法是安全的:
  1. 不在可变参数数组中保存任何数据
  2. 让可变参数数组的数据对非信任代码不可见。
@SafeVarargs 只在费重载方法中有效,且java 9之前只能用静态方法。java9中添加了私有方法的使用。

Effective Java 3rd edition 读书笔记

最近把去年出版的 Effective Java 3rd Edition 的新章节读完了,把笔记统一整理一下。 Lambdas and Streams Item 42: Prefer lambdas to anonymous class Java ...