Effective Java 6
Item 42 比起匿名类优先选择lambdas
1、在Java8中有一些只有一个抽象方法的接口值得被特殊对待。这些接口被称为函数接口。可以创建的这些接口的实例称为lambda表达式。
2、例子:
// 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()); } });
// Lambda expression as function object (replaces anonymous class) Collections.sort(words,(s1, s2) -> Integer.compare(s1.length(), s2.length()));
注意这个lamda的类型(Comparator<String>)、参数(s1,s2都是String)和返回类型(int)都没有呈现在代码中。编译器会自行从上下文推断,只有当编译器无法推断类型而提示错误时,才需要手动写明类型。
3、更进一步简化的例子:
Collections.sort(words, comparingInt(String::length));
words.sort(comparingInt(String::length));
4、之前操作码例子的简化:
// 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); } }
这里使用实现了了DoubleBinaryOperator接口的lambda代表了枚举常量的行为。它是java.util.function中众多函数接口中的一个。
5、lambdas缺少名字和文档,如果一个计算是不能自解释的,或者需要很多行,不要把它放在lambda中。
6、Lambda由于受限于函数接口,如果想要创造一个抽象类的实例。应该使用匿名类。
7、Lambda无法获得自己的引用,它的this引用将获得一个外围实例。因此如果想要获得一个本身的引用应该使用匿名类。
8、Lambda和匿名类一样,无法可靠点的序列化和反序列化,如果想要序列化使用一个私有静态内部类的实例。
Item 43比起lambda优先选择方法引用
1、一个显然的原因是方法引用更加简洁,例子:
map.merge(key, 1, (count, incr) -> count + incr);
map.merge(key, 1, Integer::sum);
2、然后在某些lambda中参数名字提供了了更好的可读性。
3、所有方法引用所能做到的lambda都能做到。
4、你可以从一个复杂的lambda中抽取代码,使它变成一个方法,并用这个方法的引用代替原来的lambda.
5、当所用的方法和lambda在同一个类时,lambda表达式往往会更加简洁:
service.execute(GoshThisClassNameIsHumongous::action);
service.execute(() -> action());
6、五种方法引用。
Item 44优先使用标准函数接口。
1、如果标准函数借口可以提供所需要的功能,通常就应该去优先使用它。
2、六大基础函数接口
3、不要试图在基础函数接口中使用包装器类型代替基本类型。
4、当需要自己为了某个目的要创建一个函数接口时思考以下问题:
1)是否它经常被使用并且可以从它的可描述性名字中获益。
2)它是否有一个强烈的约定。
3)是否可以从默认函数中收益。
5、@FunctionalInterface注解起到了两个目的:
1) 它告诉类和文档的读者这个接口被设计可以使用lambda调用。
2) 使你记住这个类只能拥有1个抽象方法,以防被别人添加新的这会导致编译错误。
6、因此总是对自己写的函数接口进行这样的注解。
7、不要让你一个有多个重载的方法在同一个参数位置上使用不同的参数接口。
Item 45谨慎地使用Stream
1、Streams API 提供了两种主要的抽象:stream,代表了一系列有限或者无限的数据元素。Stream pipeline 代表了这些元素的多阶段计算。Streams中的元素可以来自任何地方。
2、数据元素可以是对象引用或者三种基本类型int,long和double。
3、一个stream pipline有一个经过0个或多个中间操作以及一个最终操作的source stream组成。
4、Steam pipeline 是饿汉式的。
5、默认情况下,stream pipeline是按顺序执行的,如果想要并行执行可以很简单的调用 parallel方法但是很少使用。
6、一个例子:
// Prints all large anagram groups in a dictionary iteratively public class Anagrams { public static void main(String[] args) throws IOException { File dictionary = new File(args[0]); int minGroupSize = Integer.parseInt(args[1]); Map<String, Set<String>> groups = new HashMap<>(); try (Scanner s = new Scanner(dictionary)) { while (s.hasNext()) { String word = s.next(); groups.computeIfAbsent(alphabetize(word),(unused) -> new TreeSet<>()).add(word); } } for (Set<String> group : groups.values()) if (group.size() >= minGroupSize) System.out.println(group.size() + ": " + group); } private static String alphabetize(String s) { char[] a = s.toCharArray(); Arrays.sort(a); return new String(a); } }
// Overuse of streams - don't do this! 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<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 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<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)); } } // alphabetize method is the same as in original version }
7、注意在缺少具体类型的时候,小心的命名lambda的参数名字,以使它更有可读性。
8、在stream中使用帮助方法代替许多迭代代码块,使程序更有可读性。
9、stream不支持char类型,虽然可以通过强制转化类型使用,但不应该这样做。
10、不要重构或者创建所有的循环成stream,除非它真的有必要。
11、以下是代码块可以实现而函数对象或是lambda无法实现的:
1)在代码块中可以读取或者修改局部变量,而在lambda只能读取final或者实际上是fianl的变量。
2)在代码块中可以从外围方法返回,在外围循环使用continue和break,或者抛出任意受检查的异常,而lambda不能。
12、以下操作使用stream很容易进行:
1)一系列元素的同一转化。
2)一系列元素的过滤。
3)使用单个操作符对一系列元素的结合。
4)将一系列元素进行结合,例如groupby,放入list。
5)搜索满足特定条件的元素。
13、同时操作多级运算是很困难的。
14、当想要访问先前的值可以颠倒mapping。
15、例子:
static Stream<BigInteger> primes() { return Stream.iterate(TWO, BigInteger::nextProbablePrime); }
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);
//.forEach(mp -> System.out.println(mp.bitLength() + ": " + mp)); }
16、笛卡尔积的例子:
// Iterative Cartesian product computation private static List<Card> newDeck() { List<Card> result = new ArrayList<>(); for (Suit suit : Suit.values()) for (Rank rank : Rank.values()) result.add(new Card(suit, rank)); return result; }
// Stream-based Cartesian product computation private static List<Card> newDeck() { return Stream.of(Suit.values()).flatMap(suit ->Stream.of(Rank.values()).map(rank -> new Card(suit, rank))).collect(toList()); }
17、没有强制的规定该使用循环还是stream这只取决于哪个表现的更好。
Item 46 在streams中使用没有副作用(side-effect-free)的函数
1、stream不仅仅是一个API,它是一个基于函数编程的范例。
2、Stream范例最重要的部分是组织代码为一系列的转换,每个阶段的的结果尽可能都是纯函数。所谓纯函数就是输出只由输入决定也就是side-effect-free.
3、错误的例子:
// Uses the streams API but not the paradigm--Don't do this! Map<String, Long> freq = new HashMap<>(); try (Stream<String> words = new Scanner(file).tokens()) { words.forEach(word -> {freq.merge(word.toLowerCase(), 1L, Long::sum);}); }
这个代码的问题在于在最终的forEach操作中做了所有的事,使用lambda改变了外部的值。
正确的例子:
// Proper use of streams to initialize a frequency table Map<String, Long> freq; try (Stream<String> words = new Scanner(file).tokens()) { freq = words.collect(groupingBy(String::toLowerCase, counting())); }
ForEach 仅仅应该用在最后的输出结果上,而不是作为计算。
更进一步的代码使用collector,其中最重要的有toList,toSet,toMap,groupingby和joing。
Item 47 比起Stream优先使用Collection作为返回类型
1、当要返回一系列元素时,往往使用 collection接口。如果方法单独的存在能够使用foeach循环或者返回的类型没有被实现一些Collection方法例如(contains(Object)),这时候应该使用Iterable接口。如果返回的是基本类型或者有严格的性能要求,使用数组。
2、不能用foreach循环去迭代一个stream的原因是 stream没有扩展iterable接口。
3、例如:
// Won't compile, due to limitations on Java's type inference for (ProcessHandle ph : ProcessHandle.allProcesses()::iterator) { // Process the process }
需要强制转换类型:
// Hideous workaround to iterate over a stream for (ProcessHandle ph : (Iterable<ProcessHandle>)ProcessHandle.allProcesses()::iterator)
更好的方式:
// Adapter from Stream<E> to Iterable<E> public static <E> Iterable<E> iterableOf(Stream<E> stream) { return stream::iterator; } for (ProcessHandle p : iterableOf(ProcessHandle.allProcesses())) { // Process the process }
反过来的适配器:
// Adapter from Iterable<E> to Stream<E> public static <E> Stream<E> streamOf(Iterable<E> iterable) { return StreamSupport.stream(iterable.spliterator(), false); }
4、File.lines方法优于scanner 因为当它在读取文件遇到异常时会吞掉。
5、如果知道你写的方法的返回值将被用于stream pipeline,那应该让它返回一个stream;如果返回被用于迭代应该返回 Iterable;如果写一个公开的API,应该两种方式都提供。
6、Collection接口是Iterable接口的子类并且有一个stream方法,所以它既能访问iteration,也能访问stream。因此Collection或它的子类通常是一个公共返回一连串元素的方法的最好返回类型。同样Arrays也是,使用Arrays.asList 和Stream.of方法。
7、如果返回的将是一个占用很大内存的序列,那就不应该使用普通的collection。
8、如果返回的将是一个占用很大内存的序列但是可以被简洁的表示,应该构造特殊的collection。
9、例子:
// Returns the power set of an input set as custom collection public class PowerSet { public static final <E> Collection<Set<E>> of(Set<E> s) { List<E> src = new ArrayList<>(s); if (src.size() > 30) throw new IllegalArgumentException("Set too big " + s); return new AbstractList<Set<E>>() { @Override public int size() { return 1 << src.size(); // 2 to the power srcSize } @Override public boolean contains(Object o) { return o instanceof Set && src.containsAll((Set)o); } @Override public Set<E> get(int index) { Set<E> result = new HashSet<>(); for (int i = 0; index != 0; i++, index >>= 1) if ((index & 1) == 1) result.add(src.get(i)); return result; } }; } }
其他例子见书。
Item 48 小心地使Stream并行
1、如果stream source 是Stream.iterate或者中间操作有limit,使一个stream pipline并行化很可能无法提高性能。
2、并行化stream用以提高其性能最好用在 ArrayList,HashMap,HashSet ConcurrentHashMap实例 ;arrays;int范围;long范围。他们可以都被准确而简单的分成任意大小的子范围。其基于的 是 Stream或Iterable 的spliterator方法返回的spliterator。另一方面是在顺序处理时他们可以提供性能优异的局部引用。
3、pipeline最终操作的本质也会影响并行化Stream的性能,最好的操作是"减少"的操作。例如min,cout,anyMatch或者Stream reduce中的方法。collect方法不被认为是一个好的候选。
4、并行化可能带来很严重的后果。
5、并行化stream只是一个性能优化。
6、一个正确的例子:
// Prime-counting stream pipeline - benefits from parallelization static long pi(long n) { return LongStream.rangeClosed(2, n).mapToObj(BigInteger::valueOf).filter(i -> i.isProbablePrime(50)).count(); }
// Prime-counting stream pipeline - parallel version static long pi(long n) { return LongStream.rangeClosed(2, n).parallel().mapToObj(BigInteger::valueOf).filter(i -> i.isProbablePrime(50)).count(); }
7、如果并行化中需要使用ThreadLocalRandom,使用SplittableRandom代替。