Effective Java 读书笔记(五):Lambda和Stream
1 Lamdba优于匿名内部类
(1)DEMO1
- 匿名内部类:过时
Collections.sort(words, new Comparator<String>() {
public int compare(String s1, String s2) {
return Integer.compare(s1.length(), s2.length());
}
});
上述使用了策略模式,Comparator接口为排序的抽象策略,匿名内部类为具体实现策略,但是匿名内部类的实现过于冗长。
在java8中,如果一个接口只有一个方法,那么这个接口可以看作一个函数接口,功能接口的实现类可以通过lambda来实现,lambda与匿名内部类类似,但是更加简洁。
lamdba:常规
Collections.sort(words,(s1, s2) -> Integer.compare(s1.length(), s2.length()));
参数为String类型,返回值为int类型,编译器是如何知道的呢?
编译器使用称为类型推断的过程从上下文中推导出这些类型,但是编译器不是万能的,有时候仍然需要显式设定。
lamdba:方法引用
Collections.sort(words, comparingInt(String::length));
words.sort(comparingInt(String::length));
(2)DEMO2
- 常量类:enum with function object
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
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);
}
}
- lambda中的不要超过三行。
- lambda中无法访问枚举的实例成员。
- lambda无法创建抽象类的实例,但匿名内部类可以。
- lambda无法获取到对自身的引用。
- 如果需要反序列化一个函数接口,如:Comparator,我们需要使用私有静态内部类。
2 lambda中优先使用方法引用
(1)DEMO
// lambda代码块
map.merge(key, 1, (count, incr) -> count + incr);
// 方法引用
map.merge(key, 1, Integer::sum);
(2)类型
方法引用类型 | 例子 | Lambda等效方案 |
---|---|---|
Static | Integer::parseInt | str -> Integer.parseInt(str) |
Bound | Instant.now()::isAfter | Instant then = Instant.now(); t -> then.isAfter(t) |
Unbound | String::toLowerCase | str -> str.toLowerCase() |
Class Constructor | TreeMap::new | () -> new TreeMap |
Array Constructor | int[]::new | len -> new int[len] |
- 如果方法引用更加简洁和清晰,请使用方法引用,反之使用Lambda表达式。
3 优先使用标准功能接口
(1)模板方法模式
- 由于Lambda的存在,通过子类重写基本方法以专门化超类的行为的方式有点过时。
- 替代方案:提供一个静态工厂或者构造器,它们接收一个函数对象来实现相同的效果。
- 一般来说,我们将编写更多以函数对象作为参数的构造函数和方法。
// 模板方法
abstract class A {
public void print() {
System.out.println("A");
doSubThing();
}
abstract void doSubThing();
}
class B extends A {
@Override
void doSubThing() {
System.out.println("B");
}
}
// lambda
class A {
private Supplier<String> supplier;
public A(Supplier<String> supplier) {
this.supplier = supplier;
}
public void print() {
System.out.println("A");
System.out.println(supplier.get());
}
}
public static void main(String[] args) {
A a = new A(() -> "B");
a.print();
}
(2)标准函数接口
接口 | 函数签名 | 例子 |
---|---|---|
UnaryOperator<T> | T apply(T t) | String::toLowerCase |
BinaryOperator<T> | T apply(T t1, T t2) | BigInteger::add |
Predicate<T> | boolean test(T t) | Collection::isEmpty |
Function<T> | R apply(T t) | Arrays::asList |
Supplier<T> | T get() | Instant::now |
Consumer<T> | void accept(T t) | System.out::println |
- 优先使用标准函数接口,这能够缩小概念表面积,从而降低学习成本。
- 但是如果所有标准函数接口都不能很好表示时,请
- 上述的六种接口拥有许多变种,如:int、long和double,甚至是int->long等等。
- 其实大多数变种都使用了基础类型,而不是包装类型,基础类型的运算更快,节省内存空间,不要使用包装类去替换它们。
- 其实我们并不会去记忆所有变种,变种随着JDK升级可能会增加或减少,只需要在使用时去翻翻java.util.function包是否有需要的接口即可。
具体请参考:JAVA8的java.util.function包
(3)Comparator接口
- Comparator接口与ToIntBiFunction接口的结构相同,但是仍然不要用ToIntBiFunction去替代Comparator
- 因为Comparator的名称含义十分清晰,它在jdk中已经广泛使用了,而且Comparator提供了许多有用默认方法。
(4)@FunctionalInterface
- @FunctionalInterface注解能够帮助开发者检查这个接口是否只有一个抽象方法,如果不只一个将无法编译。
- @FunctionalInterface目的:将某个接口标志为函数接口且提供编译时检查。
4 明智地使用Stream
4.1 概念
流:无限或有限的数据元素序列。
管道:对流中的元素进行多级计算。
流的源:集合、数组、文件、正则表达式或模式匹配器、伪随机数生成器或其他流。
管道操作:由源流后跟着零个或多个中间操作和一个终止操作。
中间操作:某种转换流的方式,如:元素映射或元素过滤等。
终止操作:执行最终计算,如:流装入容器中或是消费掉。
4.2 补充
- 流管道只包含中间操作时是惰性的:当一个流没有最终操作时,流管道是什么都不做的。
- 流管道的API被设计成链式编码风格。
4.3 流的使用时机
(1)不要滥用流
// 普通方式
// 读取文件中的单词,检查单词的字母,相同字母的单词收集在一起
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);
}
}
// 过度使用流:虽然很简洁,但是对流不了解的开发人员可能无法理解。
// 打个比方,有些动漫是只有死宅才看的:永生之酒。
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);
}
}
// 合适使用流方式
// 有的动漫是大家都看的:龙珠。对动漫不需要太了解也能够接收。
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(group -> System.out.println(group.size() + ": " + group));
}
}
- 字母排序方法抽取出来增加程序的可读性。
- lambda中参数的命名尤为重要,好的命名能够提升可读性。
- 也许大家都希望使用lambda来消灭循环,但实际是不可取的(元素少时lambda存在性能问题)。
(2)代码块与lambda
- Stream的缺点
- 代码块能够读取或修改范围内的局部变量,lambda只能操作final变量和当前范围的局部变量。
- 代码块中能够return、抛出异常、跳出循环或是跳过循环,lambda中都无法做到。
- Stream的优势
- map:统一转换元素类型
- filter:过滤序列
- min、compute:计算最小值、合并序列等
- reduce:累计序列
- grouping:分组
(3)流无法做到同时在多级阶段访问相应的元素
- 通过操作反转来获取上一个流元素
public static void main(String[] args) {
primes().map(p -> TWO.pow(p.intValueExact()).subtract(ONE))
// (1-1/50)=98%代表isProbablePrime只有当98%几率为素数才返回true。
.filter(mersenne -> mersenne.isProbablePrime(50))
.limit(20)
// mp.bitLength等于p值,反向运算来获取上一个流的值。
.forEach(mp -> System.out.println(mp.bitLength() + ": " + mp));
}
static Stream<BigInteger> primes() {
return Stream.iterate(TWO, BigInteger::nextProbablePrime);
}
(4)笛卡尔积
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;
}
// flatMap 用于展平一个序列,如:List<String> -> String.
private static List<Card> newDeck() {
return Stream.of(Suit.values())
.flatMap(suit ->
Stream.of(Rank.values())
.map(rank -> new Card(suit, rank)))
.collect(toList());
}
5 优先选择流中无副作用的功能
(1)概述
- 为了得到stream的表现力、速度和并行度,我们必须遵守范式和使用API。
- stream范式最重要的部分:计算 -> 转换 ,每个转换(中间或终止操作)都是纯函数。
- 纯函数应该都是无副作用的(不依赖任何可变状态,不更新任何状态)。
(2)Collectors的基本方法
名称 | 作用 |
---|---|
toCollection toList toSet | 流转换为集合 |
toMap | 流转换为Map |
partitioningBy groupingBy groupingByConcurrent | 分组 |
minBy maxBy | 最值 |
counting | 计数 |
summingInt averagingInt | 和 平均值 |
joining mapping | 合并 映射 |
(3)示例
// 不遵守范式,forEach应该只用于呈现流执行的计算结果
Map<String, Long> freq = new HashMap<>();
try (Stream<String> words = new Scanner(file).tokens()) {
words.forEach(word -> freq.merge(word.toLowerCase(), 1L, Long::sum));
}
// 正确地使用流来初始化频率表
Map<String, Long> freq;
try (Stream<String> words = new Scanner(file).tokens()) {
freq = words.collect(groupingBy(String::toLowerCase, counting()));
}
// 按照频次获取前十个元素
List<String> topTen = freq.keySet().stream()
.sorted(comparing(freq::get).reversed())
.limit(10)
.collect(toList());
// groupingByConcurrent返回并发Map
ConcurrentHashMap<String, Long> freq;
try (Stream<String> words = new Scanner(file).tokens()) {
freq = words.collect(groupingByConcurrent(String::toLowerCase, counting()));
}
(4)分组
- Collector<T, ?, Map<Boolean, List>> partitioningBy(Predicate<? super T> predicate):true和false分成两组。
- Collector<T, ?, Map<K, List>> groupingBy(Function<? super T, ? extends K> classifier):按照key值分组。
List<String> words = new ArrayList<>();
words.add("1");
words.add("1");
words.add("2");
words.add("3");
Map<Boolean, List<String>> map = words.stream().collect(
partitioningBy(s -> s.equals("1"))
);
System.out.println(map); // {false=[2, 3], true=[1, 1]}
Map<String, List<String>> map = words.stream().collect(
groupingBy(String::toLowerCase)
);
System.out.println(map); // {1=[1, 1], 2=[2], 3=[3]}
(5)List转Map
// List转Map的正确实现
Map<Integer, Data> collect = words.stream().collect(toMap(Data::getId, e -> e));
// key值重复时,获取销量最大的Album
Map<Artist, Album> topHits = albums.collect(
toMap(Album::artist, a->a,maxBy(comparing(Album::sales)))
);
// 后访问的覆盖先访问的
Map<Artist, Album> topHits = albums.collect(
toMap(Album::artist, a->a,(v1, v2) -> v2)
);
// 指定返回Map的类型
HashMap<Artist, Album> topHits = albums.collect(
toMap(Album::artist, a->a,(v1, v2) -> v2,HashMap::new)
);
(6)List->字符串
// joining
List<String> words = new ArrayList<>();
words.add("2");
words.add("1");
words.add("1");
words.add("3");
String join1 = words.stream().collect(joining());
String join2 = words.stream().collect(joining(","));
String join3 = words.stream().collect(joining(",","[","]"));
System.out.println(join1); // 2113
System.out.println(join2); // 2,1,1,3
System.out.println(join3); //[2,1,1,3]
// mapping和map类似
List<String> words = new ArrayList<>();
words.add("2");
words.add("1");
words.add("1");
words.add("3");
List<Integer> list1 = words.stream().collect(mapping(e -> Integer.valueOf(e), toList()));
List<Integer> list2 = words.stream().map(e -> Integer.valueOf(e)).collect(toList());
System.out.println(list1); // [2, 1, 1, 3]
System.out.println(list2); // [2, 1, 1, 3]
(7)计算
List<String> words = new ArrayList<>();
words.add("2");
words.add("1");
words.add("3");
// 求和
Integer sum1 = words.stream().collect(summingInt(value -> Integer.valueOf(value)));
Integer sum2 = words.stream().mapToInt(value -> Integer.valueOf(value)).sum();
// 平均值
Double avg = words.stream().collect(averagingInt(value -> Integer.valueOf(value)));
// 最大值
String max1 = words.stream().max(comparing(Integer::valueOf)).get();
String max2 = words.stream().collect(maxBy(comparing(Integer::valueOf))).get();
// 总结值
IntSummaryStatistics summary = words.stream().collect(summarizingInt(Integer::valueOf));
System.out.println(summary.getAverage());
System.out.println(summary.getSum());
System.out.println(summary.getCount());
System.out.println(summary.getMax());
System.out.println(summary.getMin());
6 优先选择集合作为返回值
(1)stream的iterator
// need to cast
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
}
(2)spliterator
// spliterator用于并行迭代
public static <E> Stream<E> streamOf(Iterable<E> iterable) {
return StreamSupport.stream(iterable.spliterator(), false);
}
// 例子:并行计算1+2+...+10000
public static void main(String[] args) throws InterruptedException {
List<String> words = new ArrayList<>();
for (int i = 1; i <= 10000; i++) {
words.add(i + "");
}
final AtomicInteger atomicInteger = new AtomicInteger(0);
int count = 10;
CountDownLatch latch = new CountDownLatch(count);
final List<Spliterator<String>> splitList = split(words, count);
for (int i = 0; i < count; i++) {
int finalI = i;
new Thread(() -> {
try {
splitList.get(finalI)
.forEachRemaining(s -> atomicInteger.getAndAdd(Integer.valueOf(s)));
} finally {
latch.countDown();
}
}, "Thread:" + i).start();
}
latch.await();
System.out.println(atomicInteger.get());
}
public static <T> List<Spliterator<T>> split(List<T> list, int size) {
List<Spliterator<T>> returnList = new ArrayList<>();
returnList.add(list.spliterator());
if (size > 1) spliterator(returnList, 2, size);
return returnList;
}
private static <T> void spliterator(List<Spliterator<T>> returnList, int i, int size) {
int j = i / 2 - 1;
returnList.add(returnList.get(j).trySplit());
if (size == i) return;
spliterator(returnList, i + 1, size);
}
(3)原则
- Collection是Iterable的子类型,具有stream方法,因此提供迭代和流访问,因此Collection或适当的子类型通常是返回方法的最佳返回类型。
- 如果返回的序列小到足够放到内存中,则最好返回一个标准集合实现。
(4)DEMO
- 幂集
// {a,b,c}的幂集为{{},{a},{b},{c},{a,b},{a,c},{b,c},{a,b ,c}}
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() {
// 如果size > 31将导致溢出int的范围
return 1 << src.size(); // 2^size
}
@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;
}
};
}
}
- 前缀子集、后缀子集
public class SubLists {
public static <E> Stream<List<E>> of(List<E> list) {
return Stream.concat(Stream.of(Collections.emptyList()),
prefixes(list).flatMap(SubLists::suffixes));
}
// (a,b,c) => ((a),(a,b),(a,b,c))
private static <E> Stream<List<E>> prefixes(List<E> list) {
return IntStream.rangeClosed(1, list.size())
.mapToObj(end -> list.subList(0, end));
}
// (a,b,c) => ((a,b,c),(b,c),(c))
private static <E> Stream<List<E>> suffixes(List<E> list) {
return IntStream.range(0, list.size())
.mapToObj(start -> list.subList(start, list.size()));
}
}
- 所有子列表
// [1,3,2] => [[1], [1, 3], [1, 3, 2], [3], [3, 2], [2]]
public static <E> Stream<List<E>> of(List<E> list) {
return IntStream.range(0, list.size())
.mapToObj(start -> IntStream.rangeClosed(start + 1, list.size())
.mapToObj(end -> list.subList(start, end)))// subList使用闭区间
.flatMap(x -> x);
}
7 谨慎使用并行流
(1)原则
ArrayList、HashMap、HsahSet、CouncurrentHashMap、数组、int范围流和long范围流的并行性性能效益最佳。
它们的范围可以确定,而执行任务的抽象为spliterator。
数组存储的元素在内存中相近,数据定位更快。而上面涉及的数据结构基本都基于数组实现。
流的终止操作会影响并行执行的有效性。而流的reduce操作或预先打包(min、max、count和sum)是并行流的最佳实践。
流的中间操作(anyMatch、allMatch和noneMatch)也适合并行操作。
流的collect操作则不适合。
自己实现Stream、Iterable或Collection且希望有良好的并行性能,则需要覆盖spliterator方法。
并行流是基于fork-join池实现的。
当无法写出正确的并行流,将导致异常或者错误的数据。
注:程序的安全性、正确性比性能更重要。
(2)DEMO
// 串行,10^8需要30秒
static long pi(long n) {
return LongStream.rangeClosed(2, n)
.mapToObj(BigInteger::valueOf)
.filter(i -> i.isProbablePrime(50))
.count();
}
// 并行,10^8需要9秒
static long pi(long n) {
return LongStream.rangeClosed(2, n)
.parallel()
.mapToObj(BigInteger::valueOf)
.filter(i -> i.isProbablePrime(50))
.count();
}
Effective Java 读书笔记(五):Lambda和Stream的更多相关文章
- Effective java读书笔记
2015年进步很小,看的书也不是很多,感觉自己都要废了,2016是沉淀的一年,在这一年中要不断学习.看书,努力提升自己 计在16年要看12本书,主要涉及java基础.Spring研究.java并发.J ...
- Effective Java读书笔记完结啦
Effective Java是一本经典的书, 很实用的Java进阶读物, 提供了各个方面的best practices. 最近终于做完了Effective Java的读书笔记, 发布出来与大家共享. ...
- Effective Java 读书笔记(一):使用静态工厂方法代替构造器
这是Effective Java第2章提出的第一条建议: 考虑用静态工厂方法代替构造器 此处的静态工厂方法并不是设计模式,主要指static修饰的静态方法,关于static的说明可以参考之前的博文&l ...
- Effective Java 读书笔记之九 并发
一.访问共享的可变数据时要同步 1.synchronized关键字既然保证访问的可见性也能保证原子性.而volatile修饰符只能保证变量的线程可见性. 2.增量操作符等不是原子性,多线程操作时可能导 ...
- Effective Java 读书笔记之七 通用程序设计
一.将局部变量的作用域最小化 1.在第一次使用变量的地方声明 2.几乎每个变量的声明都应该包含一个初始化表达式:try-catch语句是一个例外 3.使方法小而集中是一个好的策略 二.for-each ...
- Effective Java 读书笔记之一 创建和销毁对象
一.考虑用静态工厂方法代替构造器 这里的静态工厂方法是指类中使用public static 修饰的方法,和设计模式的工厂方法模式没有任何关系.相对于使用共有的构造器来创建对象,静态工厂方法有几大优势: ...
- Effective Java 读书笔记(三):类与接口
1 最小化类和成员的可访问性 (1)封装 封装对组成系统的组件进行解耦,从而允许这些组件独立开发,测试,优化,使用,理解和修改. 封装提高了软件的复用性,因为组件间的耦合度低使得它们不仅在开发环境,而 ...
- Effective Java 读书笔记之十 序列化
一.谨慎地实现Serializable接口 1.一旦一个类被发布,就大大地降低了“改变这个类的实现”的灵活性. 2.仔细设计类的序列化形式而不是接受类的默认虚拟化形式. 3.反序列化机制是一个“隐藏的 ...
- Effective Java 读书笔记之八 异常
一.只针对异常的情况才使用异常 1.类具有状态相关的方法时,可采用状态测试方法和可识别的返回值两个策略. 二.对可恢复的情况使用受检异常,对编程错误使用运行时异常 1.期望调用者能够适当恢复的情况,应 ...
随机推荐
- pdf 中内容的坐标系
PDF Page Coordinates (page size, field placement, etc.) AcroForm, Basics, Automation Page coordinate ...
- kafka笔记博客
大数据数据流组件选择: https://www.cnblogs.com/yinzhengjie/articles/11155051.html 初识Apache Kafka 核心概念: https:// ...
- 如何贡献补丁到uboot社区?
答: 首次贡献分为两步: 1. 首先需要订阅一下,地址在此https://lists.denx.de/listinfo/u-boot,使邮箱地址对应有一个成员名称,才能向uboot社区发送补丁,否则会 ...
- Flutter 中的路由
Flutter 中的路由通俗的讲就是页面跳转.在 Flutter 中通过 Navigator 组件管理路由导航. 并提供了管理堆栈的方法.如:Navigator.push 和 Navigator.po ...
- Django 引用{% url "name"%} 避免链接硬编码
前提条件:为每个url指定name且name值要唯一.比如: 项目中的url.py文件: urlpatterns = patterns('', url(r'^$',TemplateView.as_vi ...
- Python - Django - ORM 外键操作
models.py: from django.db import models # 出版社 class Publisher(models.Model): id = models.AutoField(p ...
- 【mysql】reset Password
https://www.cnblogs.com/josn1984/p/8550419.html https://blog.csdn.net/l1028386804/article/details/92 ...
- 网络编程之Reactor 模式
基本的架构是 epoll+线程池. 这篇博文主要从以下几个方面进行阐述: (1)reactor模式的一个介绍:(只要是我的理解) (2)关于线程池的说明. (3)如何将epoll + 池结合起来实现一 ...
- web端自动化——Python的smtplib发送电子邮件
SMTP (Simple Mail Transfer Protocol)是简单邮件传输协议,它是一组用于由源地址到目的地址传送邮件的规则,由它来控制信件的中转方式. Python的smtplib模块提 ...
- mycat搭建环境
macos完全卸载mysql: https://blog.csdn.net/u012721519/article/details/55002626 踩过的坑: mycat1.6不支持单库分表; 最少要 ...