乐趣区

《java 8 实战》读书笔记 -第五章 使用流

一、筛选和切片
1. 用谓词筛选
Streams 接口支持 filter 方法(你现在应该很熟悉了)。该操作会接受一个谓词(一个返回 boolean 的函数)作为参数,并返回一个包括所有符合谓词的元素的流。例如筛选出所有素菜,创建一张素食菜单:
List<Dish> vegetarianMenu = menu.stream()
.filter(Dish::isVegetarian)
.collect(toList());
2. 筛选各异的元素
流还支持一个叫作 distinct 的方法,它会返回一个元素各异(根据流所生成元素的 hashCode 和 equals 方法实现)的流。例如,以下代码会筛选出列表中所有的偶数,并确保没有重复。
List<Integer> numbers = Arrays.asList(1, 2, 1, 3, 3, 2, 4);
numbers.stream()
.filter(i -> i % 2 == 0)
.distinct()
.forEach(System.out::println);

hashcode() 和 equals()1.Java 中的 hashCode() 的作用 hashCode() 的作用是为了提高在散列结构存储中查找的效率,在线性表中没有作用;只有每个对象的 hash 码尽可能不同才能保证散列的存取性能,事实上 Object 类提供的默认实现确实保证每个对象的 hash 码不同(在对象的内存地址基础上经过特定算法返回一个 hash 码)。在 Java 有些集合类(HashSet)中要想保证元素不重复可以在每增加一个元素就通过对象的 equals 方法比较一次,那么当元素很多时后添加到集合中的元素比较的次数就非常多了,也就是说如果集合中现在已经有 3000 个元素则第 3001 个元素加入集合时就要调用 3000 次 equals 方法,这显然会大大降低效率,于是 Java 采用了哈希表的原理,这样当集合要添加新的元素时会先调用这个元素的 hashCode 方法就一下子能定位到它应该放置的物理位置上(实际可能并不是),如果这个位置上没有元素则它就可以直接存储在这个位置上而不用再进行任何比较了,如果这个位置上已经有元素了则就调用它的 equals 方法与新元素进行比较,相同的话就不存,不相同就散列其它的地址,这样一来实际调用 equals 方法的次数就大大降低了,几乎只需要一两次,而 hashCode 的值对于每个对象实例来说是一个固定值。2.Java 中重写 equals() 方法时尽量要重写 hashCode() 方法的原因当 equals 方法被重写时通常有必要重写 hashCode 方法来维护 hashCode 方法的常规协定,该协定声明相等对象必须具有相等的哈希码,如果不这样做的话就会违反 hashCode 方法的常规约定,从而导致该类无法结合所有基于散列的集合一起正常运作,这样的集合包括 HashMap、HashSet、Hashtable 等引申:HashMap 实现原理及源码分析注意:哈希冲突的解决方案有多种: 开放定址法(发生冲突,继续寻找下一块未被占用的存储地址),再散列函数法,链地址法,而 HashMap 即是采用了链地址法,也就是数组 + 链表的方式

3. 截短流
流支持 limit(n) 方法,该方法会返回一个不超过给定长度的流。所需的长度作为参数传递给 limit。如果流是有序的,则最多会返回前 n 个元素。比如,你可以建立一个 List,选出热量超过 300 卡路里的头三道菜:
List<Dish> dishes = menu.stream()
.filter(d -> d.getCalories() > 300)
.limit(3)
.collect(toList());
请注意 limit 也可以用在无序流上,比如源是一个 Set。这种情况下,limit 的结果不会以任何顺序排列。
4. 跳过元素
流还支持 skip(n) 方法,返回一个扔掉了前 n 个元素的流。如果流中元素不足 n 个,则返回一个空流。
List<Dish> dishes = menu.stream()
.filter(d -> d.getCalories() > 300)
.skip(2)
.collect(toList());
二、映射
1. 对流中每一个元素应用函数
提取流中菜肴的名称:
List<String> dishNames = menu.stream()
.map(Dish::getName)
.collect(toList());
2. 流的扁平化
List<String> uniqueCharacters =
words.stream()
.map(w -> w.split(“”))
.flatMap(Arrays::stream)
.distinct()
.collect(Collectors.toList());
使用 flatMap 方法的效果是,各个数组并不是分别映射成一个流,而是映射成流的内容。所有使用 map(Arrays::stream) 时生成的单个流都被合并起来,即扁平化为一个流。一言以蔽之,flatmap 方法让你把一个流中的每个值都换成另一个流,然后把所有的流连接起来成为一个流。
三、查找和匹配
1. 检查谓词是否至少匹配一个元素
anyMatch 方法可以回答“流中是否有一个元素能匹配给定的谓词”。比如,你可以用它来看看菜单里面是否有素食可选择:
if(menu.stream().anyMatch(Dish::isVegetarian)){
System.out.println(“The menu is (somewhat) vegetarian friendly!!”);
}
anyMatch 方法返回一个 boolean, 是一个终端操作
2. 检查谓词是否匹配所有元素
allMatch 方法的工作原理和 anyMatch 类似,但它会看看流中的元素是否都能匹配给定的谓词
boolean isHealthy = menu.stream()
.allMatch(d -> d.getCalories() < 1000);
noneMatch 和 allMatch 相对的是 noneMatch。它可以确保流中没有任何元素与给定的谓词匹配。比如,你可以用 noneMatch 重写前面的例子:
boolean isHealthy = menu.stream()
.noneMatch(d -> d.getCalories() >= 1000);
anyMatch、allMatch 和 noneMatch 这三个操作都用到了我们所谓的短路
3. 查找元素
findAny 方法将返回当前流中的任意元素。它可以与其他流操作结合使用。比如,你可能想找到一道素食菜肴。你可以结合使用 filter 和 findAny 方法来实现这个查询:
Optional<Dish> dish =
menu.stream()
.filter(Dish::isVegetarian)
.findAny();

Optional 简介 Optional<T> 类(java.util.Optional)是一个容器类,代表一个值存在或不存在。在上面的代码中,findAny 可能什么元素都没找到。Java 8 的库设计人员引入了 Optional<T>,这样就不用返回众所周知容易出问题的 null 了。Optional 里面几种可以迫使你显式地检查值是否存在或处理值不存在的情形的方法也不错。

isPresent() 将在 Optional 包含值的时候返回 true, 否则返回 false。
ifPresent(Consumer<T> block) 会在值存在的时候执行给定的代码块。
T get() 会在值存在时返回值,否则抛出一个 NoSuchElement 异常。
T orElse(T other) 会在值存在时返回值,否则返回一个默认值。

例如,在前面的代码中你需要显式地检查 Optional 对象中是否存在一道菜可以访问其名称:
menu.stream()
.filter(Dish::isVegetarian)
.findAny()
.ifPresent(d -> System.out.println(d.getName());

4. 查找第一个元素
使用 findFirst() 方法
List<Integer> someNumbers = Arrays.asList(1, 2, 3, 4, 5);
Optional<Integer> firstSquareDivisibleByThree =
someNumbers.stream()
.map(x -> x * x)
.filter(x -> x % 3 == 0)
.findFirst(); // 9
四、归约
1. 元素求和(多归一)

有初始值
int sum = numbers.stream().reduce(0, (a, b) -> a + b);
reduce 接受两个参数:(1) 一个初始值,这里是 0;(2) 一个 BinaryOperator<T> 来将两个元素结合起来产生一个新值,这里我们用的是 lambda (a, b) -> a + b。

无初始值 reduce 还有一个重载的变体,它不接受初始值,但是会返回一个 Optional 对象:
Optional<Integer> sum = numbers.stream().reduce((a, b) -> (a + b));

为什么它返回一个 Optional<Integer> 呢?考虑流中没有任何元素的情况。reduce 操作无返回其和,因为它没有初始值。这就是为什么结果被包裹在一个 Optional 对象里,以表明和可能不存在。

2. 最大值和最小值

reduce 接受两个参数:

一个初始值;
一个 Lambda 来把两个流元素结合起来并产生一个新值

eg:
Optional<Integer> max = numbers.stream().reduce(Integer::max);// 最大值
Optional<Integer> min = numbers.stream().reduce(Integer::min);// 最小值

map 和 reduce 的连接通常称为 map-reduce 模式,因 Google 用它来进行网络搜索而出名,因为它很容易并行化。
诸如 map 或 filter 等操作会从输入流中获取每一个元素,并在输出流中得到 0 或 1 个结果, 这些操作一般都是无状态的; 但诸如 reduce、sum、max 等操作需要内部状态来累积结果, 我们把这些操作叫作有状态操作。
五、数值流
1. 原始类型流特化
1.1 映射到数值流
Java 8 引入了三个原始类型特化流接口来解决这个问题:IntStream、DoubleStream 和 LongStream,分别将流中的元素特化为 int、long 和 double,从而避免了暗含的装箱成本。个接口都带来了进行常用数值归约的新方法,比如对数值流求和的 sum,找到最大元素的 max,以及 min、average 等。可以像下面这样用 mapToInt 对 menu 中的卡路里求和:
int calories = menu.stream()
.mapToInt(Dish::getCalories)
.sum();
请注意,如果流是空的,sum 默认返回 0。
1.2 转换回对象流
要把原始流转换成一般流(每个 int 都会装箱成一个 Integer),可以使用 boxed 方法,如下所示:
IntStream intStream = menu.stream().mapToInt(Dish::getCalories);
Stream<Integer> stream = intStream.boxed();
1.3 默认值 OptionalInt
求和的那个例子很容易,因为它有一个默认值:0。但是,如果你要计算 IntStream 中的最大元素,就得换个法子了,因为 0 是错误的结果。对于三种原始流特化,分别有一个 Optional 原始类型特化版本:OptionalInt、OptionalDouble 和 OptionalLong。
例如,要找到 IntStream 中的最大元素,可以调用 max 方法,它会返回一个 OptionalInt:
OptionalInt maxCalories = menu.stream()
.mapToInt(Dish::getCalories)
.max();
现在,如果没有最大值的话,你就可以显式处理 OptionalInt 去定义一个默认值了:
int max = maxCalories.orElse(1);
2. 数值范围
假设你想要生成 1 和 100 之间的所有数字。Java 8 引入了两个可以用于 IntStream 和 LongStream 的静态方法,帮助生成这种范围:range 和 rangeClosed。这两个方法都是第一个参数接受起始值,第二个参数接受结束值。但 range 是不包含结束值的,而 rangeClosed 则包含结束值。
IntStream evenNumbers = IntStream.rangeClosed(1, 100)
.filter(n -> n % 2 == 0);
System.out.println(evenNumbers.count());
六、构建流
1. 由值创建流
你可以使用静态方法 Stream.of,通过显式值创建一个流。它可以接受任意数量的参数。例如,以下代码直接使用 Stream.of 创建了一个字符串流。然后,你可以将字符串转换为大写,再一个个打印出来:
Stream<String> stream = Stream.of(“Java 8 “, “Lambdas “, “In “, “Action”);
stream.map(String::toUpperCase).forEach(System.out::println);
你可以使用 empty 得到一个空流,如下所示:
Stream<String> emptyStream = Stream.empty();
2. 由数组创建流
int[] numbers = {2, 3, 5, 7, 11, 13};
int sum = Arrays.stream(numbers).sum();
3. 由文件生成流
Files.lines,它会返回一个由指定文件中的各行构成的字符串流。
用这个方法看看一个文件中有多少各不相同的词:
long uniqueWords = 0;
try(Stream<String> lines =
Files.lines(Paths.get(“data.txt”), Charset.defaultCharset())){
uniqueWords = lines.flatMap(line -> Arrays.stream(line.split(” “)))
.distinct()
.count();
} catch(IOException e){
// 如果打开文件时出现异常则加以处理
}
4. 由函数生成流:创建无限流
Stream API 提供了两个静态方法来从函数生成流:Stream.iterate 和 Stream.generate。一般来说,应该使用 limit(n) 来对这种流加以限制,以避免打印无穷多个值。
4.1 迭代
Stream.iterate(0, n -> n + 2)
.limit(10)
.forEach(System.out::println);

iterate 方法接受一个初始值(在这里是 0),还有一个依次应用在每个产生的新值上的 Lambda(UnaryOperator<t> 类型)。这里,我们使用 Lambda n -> n + 2,返回的是前一个元素加上 2。
4.2 生成
与 iterate 方法类似,generate 方法也可让你按需生成一个无限流。但 generate 不是依次对每个新生成的值应用函数的。它接受一 Supplier<T> 类型的 Lambda 提供新的值。我们先来看一个简单的用法:
Stream.generate(Math::random)
.limit(5)
.forEach(System.out::println);
这段代码将生成一个流,其中有五个 0 到 1 之间的随机双精度数。

退出移动版