目录
一、Stream是什么
Java 8引入了一种新的编程模型,称为"流"(Stream)API,用于处理集合数据。Stream API支持声明式风格的编程,专注于数据的处理,而不是控制流。它可以用于读取、过滤、转换和聚合数据,而无需显式地操作集合对象。
Stream API的主要特点包括:
-
管道模式:每个Stream操作都会产生一个新的Stream,形成一条流水线。这条流水线可以由多个中间操作(intermediate operations)和一个终端操作(terminal operation)组成。中间操作不会立即执行,而是延迟到终端操作时才执行,这样可以优化性能并支持惰性求值。
-
内存效率:Stream API设计的目标之一是减少内存占用。例如,在处理大型数据集时,Stream可以在不需要存储整个集合于内存的情况下工作。
-
并行处理:Stream API支持并行流,可以利用多核处理器的优势进行并行计算,提高程序性能。
-
函数式编程:Stream API与函数式编程的概念紧密相关,如filter(), map(), reduce()等方法,它们接收Lambda表达式作为参数,使得代码更简洁、易读。
以下是一些Stream API的例子:
// 创建一个Stream
List<String> list = Arrays.asList("one", "two", "three");
Stream<String> stream = list.stream();
// 中间操作:筛选和转换
stream.filter(s -> s.startsWith("t"))
.map(String::toUpperCase)
.forEach(System.out::println); // 输出: TWO, THREE
// 终端操作:收集到新的列表
List<String> result = stream.filter(s -> s.length() > 3)
.collect(Collectors.toList());
在这个例子中,我们首先创建了一个Stream,然后通过filter
筛选出以"t"开头的字符串,再通过map
将其转换为大写,最后通过forEach
打印结果。第二个例子中,我们使用collect
终端操作将满足条件的元素收集到一个新的列表中。
二、创建Stream
例如,List.stream()
或 Arrays.stream(ints)
。
下面是创建stream流的几种方式:
/**
* 1. 创建一个stream流
*/
//1.1 从集合创建流
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);
System.out.println("集合numbers = " + numbers);
Stream<Integer> stream = numbers.stream();
System.out.println("stream = " + stream);
//1.2 从数组创建流
Integer[] arr = {1, 2, 3, 4, 5};
System.out.println("数组arr = " + arr);
Stream<Integer> stream1 = Arrays.stream(arr);
System.out.println("stream1 = " + stream1);
//1.3 使用Stream.of()创建Stream
Stream<String> stream2 = Stream.of("a", "b", "c");
System.out.println("使用Stream.of()创建stream2 = " + stream2);
运行程序可以看到下面结果:
java.util.stream.ReferencePipeline$Head@579bb367
是一种Java对象的默认字符串表示形式,通常称为"对象的哈希值表示"。这种表示方式在打印未重写toString()
方法的对象时出现,主要包括以下部分:
类名:
ReferencePipeline$Head
这代表的是对象所属的类。在这个例子中,它来自于java.util.stream
包,表示一个流管道的头节点。
@
符号:这个符号标志着后面的部分是对象的内存地址的十六进制表示。在某些JVM实现中,这不是真正的内存地址,而是对象的哈希码,用于标识对象的唯一性。十六进制数字:
579bb367
是上述提到的哈希码或内存地址的十六进制表示。每个Java对象都有唯一的哈希码,除非有特殊情况导致哈希冲突。
总的来说,当你打印一个没有重写toString()
方法的对象时,你会看到类似这样的输出,它提供了对象的类信息以及一个用于区分不同对象的哈希码。如果你想要更友好的输出,你应该覆盖toString()
方法,自定义你需要显示的信息。例如,在自定义类中,你可以将对象的关键属性组合成一个字符串返回。
这里的‘ReferencePipelineHead`是Stream管道的一个内部类,代表了未执行任何操作的原始流。由于这是一个中间对象,所以直接打印它不会有具体的结果,只有当你执行一个终端操作时,才会得到实际的数据或行为。
三、中间操作
Intermediate operations: 如 filter()
, map()
, distinct()
等,这些操作不会立即执行,而是构建一个新的流,等待最终操作触发。
3.1 filter()
filter()
方法用于根据给定的条件筛选流中的元素。以下是一个简单的代码示例:
import java.util.Arrays;
import java.util.List;
public class StreamFilterExample {
public static void main(String[] args) {
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9);
// 使用filter()筛选出偶数
List<Integer> evenNumbers = numbers.stream()
.filter(n -> n % 2 == 0)
.collect(Collectors.toList());
System.out.println("Even Numbers: " + evenNumbers); // 输出:[2, 4, 6, 8]
}
}
在这个例子中,我们创建了一个整数列表,然后使用stream()
方法将其转换为Stream。接着,我们调用filter()
方法,传入一个lambda表达式(n -> n % 2 == 0)
作为参数,这意味着我们只保留能被2整除的数字。最后,我们使用collect()
方法将筛选后的流收集到一个新的列表中。
当你运行这段代码,你会看到输出 [2, 4, 6, 8]
,这就是原始列表中所有的偶数。
请注意,filter()
和其他中间操作一样,不会立即执行。只有在遇到终端操作,如collect()
, forEach()
, count()
等时,才会开始执行流的所有操作。这种延迟执行的概念被称为"惰性求值"。
3.2 map()
用于将流中的每个元素应用一个函数得到一个新的结果,然后创建一个新的流,这个新流由应用函数后产生的结果组成。以下是一个简单的例子:
import java.util.Arrays;
import java.util.List;
public class Main {
public static void main(String[] args) {
List<String> names = Arrays.asList("Alice", "Bob", "Charlie", "David");
// 使用map()将字符串转换为长度
List<Integer> lengths = names.stream()
.map(Main::stringLength)
.collect(Collectors.toList());
lengths.forEach(System.out::println); // 输出:5, 3, 7, 5
}
public static int stringLength(String s) {
return s.length();
}
}
在这个例子中,我们有一个包含名字的列表,我们使用map()
方法将每个名字转换为其长度,最终得到了一个新的整数列表。
3.3 flatMap()
它用于处理流中的每个元素,并将其转换为另一个流,然后将所有这些流连接成一个新的单一流。这对于处理嵌套结构(例如,一个列表的每个元素都是另一个列表)特别有用。
以下是一个简单的flatMap()
用法的例子,假设我们有一个用户类,每个用户有一个列表的地址:
public class User {
private String name;
private List<Address> addresses;
// getters and setters...
}
public class Address {
private String city;
// getters and setters...
}
现在,我们有一个用户列表,并想获取所有城市的列表:
List<User> users = ...; // 初始化用户列表
List<String> allCities = users.stream()
.flatMap(user -> user.getAddresses().stream())
.map(Address::getCity)
.collect(Collectors.toList());
在这个例子中,flatMap()
将每个用户的地址列表展开成单独的流,然后map()
方法将每个地址的city
字段提取出来,最后通过collect()
方法收集所有的城市到一个新的列表中。
这个例子展示了如何使用flatMap()
来处理嵌套的数据结构并将其展平为单一层次的结果。
3.4 distinct()
用于去除流中的重复元素。这个方法不会立即执行,而是在流的终端操作时(例如 collect
, forEach
等)才执行。以下是一个简单的代码示例:
import java.util.Arrays;
import java.util.List;
import java.util.stream.Stream;
public class DistinctExample {
public static void main(String[] args) {
List<Integer> numbers = Arrays.asList(1, 2, 3, 2, 4, 3, 5);
// 使用 distinct() 去除重复元素
Stream<Integer> distinctNumbers = numbers.stream().distinct();
// 输出去重后的结果
distinctNumbers.forEach(System.out::println);
}
}
在这个例子中,我们首先创建了一个包含重复元素的列表 numbers
。然后,我们调用 stream()
方法将其转换为 Stream,接着调用 distinct()
进行去重操作。最后,使用 forEach
终端操作打印出去重后的数字。
输出将是:
1
2
3
4
5
这里没有重复的元素,因为 distinct()
操作已经将他们去除。需要注意的是,distinct()
是根据对象的 equals()
方法来判断是否重复的,所以如果你自定义了类并覆写了 equals()
方法,那么这里的去重规则也会相应改变。
3.5 limit()
用于限制流中元素的数量。它会返回一个新的流,其中只包含原始流的前n个元素。如果原始流的元素数量少于n,那么新流将包含所有元素。这个操作是非阻塞的,也就是说,直到终端操作执行时,实际的数据提取才会发生。
以下是一个简单的代码示例:
import java.util.Arrays;
import java.util.List;
public class LimitExample {
public static void main(String[] args) {
List<String> numbers = Arrays.asList("One", "Two", "Three", "Four", "Five");
// 使用 limit() 方法限制输出到前三项
numbers.stream()
.limit(3)
.forEach(System.out::println);
}
}
在这个例子中,numbers.stream().limit(3)
创建了一个新的流,该流只包含原列表的前三个元素。然后,forEach(System.out::println)
遍历并打印这些元素,所以输出将是:
One
Two
Three
请注意,limit()
操作不会改变原始集合的内容。它只是创建了一个新的、有限的视图来处理数据。
四、终端操作
Terminal operations: 如 collect()
, forEach()
, count()
,这些操作会触发前面所有的中间操作,并返回结果或完成流。
4.1 findAny(), 和 orElse()
- findAny(): 这是一个终端操作,用于找到流中的任意一个元素。对于无限流,它可能会立即返回,而对于有限流,它会在遍历完流后返回。如果流为空,它会返回一个Optional对象的空实例。例如:
Optional<Integer> firstEven = numbers.stream()
.filter(n -> n % 2 == 0)
.findAny();
if (firstEven.isPresent()) {
System.out.println("First even number is: " + firstEven.get());
}
- orElse(): 也是一个与Optional相关的终端操作。当Optional对象有值时,它会返回该值;如果没有值(即isPresent()返回false),则会返回传入
orElse()
方法的参数。例如:
Optional<String> optionalName = Optional.empty();
String name = optionalName.orElse("Default Name");
System.out.println(name); // 输出: Default Name
- 结合filter()操作,下面是个完整的例子:
List<String> names = Arrays.asList("Alice", "Bob", "Charlie", "David");
Optional<String> startsWithA = names.stream()
.filter(name -> name.startsWith("A"))
.findAny();
String firstNameStartingWithA = startsWithA.orElse("No one starts with 'A'");
System.out.println(firstNameStartingWithA); // 输出: Alice 或者 No one starts with 'A'
在这个例子中,我们首先创建了一个名字列表,然后通过filter找出以"A"开头的名字,如果存在这样的名字,就用findAny找到第一个,最后使用orElse确保即使没有找到名字,也会返回一个默认值。
4.2 sorted()
用于对流中的元素进行排序。这个操作会返回一个新的流,其元素按照自然排序或者是提供的Comparator进行排序。如果原始流是并行流,那么结果流也是并行流。
以下是一个简单的代码示例:
import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;
public class Main {
public static void main(String[] args) {
List<Integer> numbers = Arrays.asList(5, 2, 9, 1, 5, 6);
// 使用sorted()对列表进行排序
List<Integer> sortedNumbers = numbers.stream()
.sorted()
.collect(Collectors.toList());
System.out.println(sortedNumbers); // 输出:[1, 2, 5, 5, 6, 9]
}
}
在这个例子中,我们首先创建了一个未排序的整数列表。然后,我们调用stream()
方法将其转换为流,接着使用sorted()
进行排序,最后通过collect()
收集到一个新的已排序的列表。
如果你想要自定义排序规则,你可以传递一个Comparator
给sorted()
方法,如下所示:
List<String> words = Arrays.asList("zebra", "apple", "cat", "banana");
words.stream()
.sorted(Comparator.comparingInt(String::length))
.forEach(System.out::println);
这段代码会按字符串长度进行排序,输出结果可能是:"cat"
, "apple"
, "banana"
, "zebra"
。
4.3 forEach()
用于执行对每个流元素的操作。这个操作不会返回任何结果,而是纯粹为了副作用而存在,例如打印元素、更新UI等。
以下是一个简单的代码示例,展示如何使用forEach()
遍历并打印一个整数列表:
import java.util.Arrays;
import java.util.List;
public class Main {
public static void main(String[] args) {
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);
// 使用forEach打印列表中的每个元素
numbers.forEach(System.out::println);
}
}
在这个例子中,System.out::println
是一个方法引用,相当于传递了一个lambda表达式 (Integer num) -> System.out.println(num)
,即对于列表中的每个元素,执行System.out.println()
来打印它。
注意,forEach()
操作一旦执行,流就会被消费掉,不能再被重复使用。此外,由于它是终端操作,所以在其后不能接着放置其他中间操作。
4.4 count()
用于计算流中元素的数量。这个操作是累积性的,所以它可以有效地处理大量数据,即使是在不可见的并行流中。count()
返回的是一个 long 类型的值,表示流中元素的数量。
以下是一个简单的代码示例,展示了如何使用 count()
方法统计数组中偶数的数量:
import java.util.Arrays;
public class Main {
public static void main(String[] args) {
int[] numbers = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
long evenCount = Arrays.stream(numbers)
.filter(n -> n % 2 == 0)
.count();
System.out.println("Number of even numbers: " + evenCount);
}
}
在这个例子中,我们首先创建了一个整数数组 numbers
。然后,我们通过 Arrays.stream(numbers)
创建了一个 IntStream。接着,使用 filter(n -> n % 2 == 0)
过滤出所有的偶数。最后,count()
方法计算过滤后剩余的偶数个数,并将其存储在变量 evenCount
中。程序输出结果将是偶数的数量。
4.5 collect()
用于将流转换为其他形式,例如收集到 Collection、构建 Map 或者执行其他聚合操作。它可以与 Collectors
类提供的工厂方法一起使用,以实现各种常见的收集行为。
以下是一个简单的例子,展示了如何使用 collect()
方法将一个整数流收集到 ArrayList 中:
import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;
public class Main {
public static void main(String[] args) {
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);
// 创建一个新的列表,只包含偶数
List<Integer> evenNumbers = numbers.stream()
.filter(n -> n % 2 == 0)
.collect(Collectors.toList());
System.out.println(evenNumbers); // 输出: [2, 4]
}
}
在这个例子中,我们首先创建了一个数字列表,然后将其转换为流。接着,我们使用 filter()
过滤出偶数,最后调用 collect()
并传入 Collectors.toList()
来收集结果到一个新的 ArrayList 中。
collect()
可以配合不同的收集器完成更复杂的任务,例如分组、规约等。例如,如果你想统计列表中每个元素出现的次数,你可以这样使用:
Map<Integer, Long> countMap = numbers.stream()
.collect(Collectors.groupingBy(Function.identity(), Collectors.counting()));
System.out.println(countMap); // 输出: {1=1, 2=1, 3=1, 4=1, 5=1}
在这个例子中,groupingBy()
聚合操作用于按元素值分组,counting()
则计算每组的数量。结果是一个 Map,其中键是原始列表中的元素,值是它们的计数。
4.6 groupingBy()
groupingBy()
是Java 8 Stream API中的一个收集器,用于根据提供的函数将流中的元素分组到一个Map中。这个Map的键是由分组函数计算出来的,值则是一个列表,包含了具有相同键的所有元素。
以下是一个简单的例子,假设我们有一个Person对象的列表,每个Person有名字(name)和年龄(age),我们想要按照年龄将这些人分组:
import java.util.*;
import java.util.stream.*;
class Person {
String name;
int age;
public Person(String name, int age) {
this.name = name;
this.age = age;
}
// getters for name and age
}
List<Person> people = Arrays.asList(
new Person("Alice", 25),
new Person("Bob", 30),
new Person("Charlie", 25),
new Person("Dave", 30)
);
Map<Integer, List<Person>> groupedPeople =
people.stream()
.collect(Collectors.groupingBy(Person::getAge));
System.out.println(groupedPeople);
在这个例子中,groupingBy(Person::getAge)
创建了一个Map,其中键是年龄,值是对应年龄的人的列表。运行这段代码后,输出将会是一个Map,显示每个年龄段的人都有哪些:
{25=[Person@3e2c7a8f, Person@6d8761b3], 30=[Person@7059b6df, Person@3305f532]}
这里的键是年龄(25和30),值是包含相应年龄的Person对象的列表。注意,由于Person类没有重写equals和hashCode,所以Person对象的默认表示是内存地址,通常看起来像"Person@somehexnumber"。如果需要按照名字或其他属性比较Person对象,应该在Person类中实现这些方法。
4.7 average()
用于计算流中所有元素的平均值。这个操作要求流中的元素必须是可以转换为 Double
类型的。如果流为空,average()
将返回 OptionalDouble.empty()
;否则,它将返回一个表示平均值的 OptionalDouble
对象。
下面是一个简单的代码示例,展示了如何使用 average()
来计算一组数字的平均值:
import java.util.Arrays;
import java.util.DoubleSummaryStatistics;
import java.util.OptionalDouble;
import java.util.stream.Stream;
public class AverageExample {
public static void main(String[] args) {
double[] numbers = {1.5, 2.7, 3.3, 4.1, 5.0};
OptionalDouble average = Arrays.stream(numbers)
.average();
if (average.isPresent()) {
System.out.println("Average: " + average.getAsDouble());
} else {
System.out.println("No elements to calculate average.");
}
}
}
在这个例子中,我们首先创建了一个 double
数组 numbers
,然后将其转换为一个流(Arrays.stream(numbers)
)。接着调用 average()
方法计算平均值。由于流中有元素,average()
返回了一个非空的 OptionalDouble
,我们可以使用 getAsDouble()
获取实际的平均值并打印出来。如果流为空,average()
返回的将是空的 OptionalDouble
,可以通过 isPresent()
判断是否为空。
4.8 anyMatch()
用于检查流中的任何一个元素是否满足给定的 predicate(断言)。如果找到至少一个匹配的元素,它就返回 true,否则返回 false。这是一个短路操作,一旦找到匹配项,它就会立即停止处理剩余的元素。
以下是一个简单的代码示例,我们有一个整数列表,我们想检查是否存在任意一个偶数:
import java.util.Arrays;
import java.util.List;
public class Main {
public static void main(String[] args) {
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5, 6);
boolean hasEvenNumber = numbers.stream()
.anyMatch(number -> number % 2 == 0);
System.out.println("Does the list contain any even number? " + hasEvenNumber);
}
}
在这个例子中,anyMatch(number -> number % 2 == 0)
使用了一个 lambda 表达式作为 predicate,检查每个数字是否能被2整除。因为列表中有偶数(2、4、6),所以 anyMatch()
返回 true
。
参考