定义
流是Java API新成员,使我们对集合的操作有了更多的控制,更加高效。
示例
将List<Integer>
中的所有元素加1输出:
List<Integer> list = Arrays.asList(1, 2, 3, 4, 5, 6, 7);
//之前的写法:
List<Integer> result = new ArrayList<>();
for (Integer value:list) {
result.add(value+1);
}
result.forEach(System.out::println);
//使用Stream的写法:
list.stream().map(x -> x+1).forEach(System.out::println);
使用流
流的使用一般包含三个元素:
一个数据源(如集合)来执行查询
一个中间操作链,形成一条流的流水线
一个终端操作,执行流水线,并生成结果
根据前面的示例说明:数据源是list;中间操作链是map;终端操作是forEach。图示说明:
其他的中间操作有:filter、limit、sorted、distinct、skip、map、flatMap等。
其他的终端操作有:count、collect、reduce、count、anyMatch、noneMatch、findFirst、findAny、allMatch等。
它们的详细描述可参考下图:
中间操作都返回一个流,所以它们可以组合使用,形成操作链。终端操作会返回一个结果。
Stream API支持许多操作,能让你快速完成复杂的数据查询,如筛选、映射、查找、匹配和规约。
数据源
创建Dish类:
public class Dish {
private final String name;
private final boolean vegetarian;
private final int calories;
private final Type type;
public Dish(String name,boolean vegetarian,int calories,Type type){
this.name = name;
this.vegetarian = vegetarian;
this.calories = calories;
this.type = type;
}
public String getName() {
return name;
}
public boolean isVegetarian() {
return vegetarian;
}
public int getCalories() {
return calories;
}
public Type getType() {
return type;
}
@Override
public String toString() {
return name;
}
public enum Type{
MEAT,
FISH,
OTHER
}
}
创建Dish集合:
List<Dish> menu = Arrays.asList(
new Dish("pork",false,800,Dish.Type.MEAT),
new Dish("beef",false,700,Dish.Type.MEAT),
new Dish("chicken",false,400,Dish.Type.MEAT),
new Dish("french fries",true,530,Dish.Type.OTHER),
new Dish("rice",true,350,Dish.Type.OTHER),
new Dish("season fruit",true,120,Dish.Type.OTHER),
new Dish("pizza",true,550,Dish.Type.OTHER),
new Dish("prawns",false,300,Dish.Type.FISH),
new Dish("salmon",false,450,Dish.Type.FISH)
);
筛选
- 筛选出所有素食
List<Dish> vegetarianMenu = menu.stream().filter(Dish::isVegetarian).collect(toList());
- 筛选出各异的元素
List<Integer> numbers = Arrays.asList(1,2,1,3,3,2,4); // 集合中的偶数有三个:2,2,4
numbers.stream().filter(i -> i%2 == 0).distinct().forEach(System.out::println); //打印结果:2,4
- 筛选出前三个元素
List<Dish> dishes = menu.stream().filter(d -> d.getCalories() > 300).limit(3).collect(toList());
- 跳过元素
List<Dish> dishes = menu.stream().filter(d -> d.getCalories() > 300).skip(2).collect(toList());
映射
一个非常常见的数据处理操作就是从某些对象中选择信息。比如在SQL里,可以从表中选择一列。Stream API也通过map和flatMap方法提供了类似的工具。映射和类型转换不同,它是创建一个新版本,而不是去修改。
- 获取所有Dish的名称
List<String> dishNames = menu.stream().map(Dish::getName).collect(toList());
- 获取所有Dish的名称的字符长度
List<Integer> dishNameLengths = menu.stream()
.map(Dish::getName).map(String::length).collect(toList());
查找和匹配
另一个常见的操作就是看看数据集中的某些元素是否匹配一个给定的属性。Stream API通过allMatch、anyMatch、noneMatch、findFirst和findAny方法提供了这样的工具。
- 判断menu中是否有素食
boolean result = menu.stream().anyMatch(Dish::isVegetarian) ; //true
- 判断menu中是否所有菜品的热量都小于1000卡路里
boolean isHealthy = menu.stream().allMatch(d -> d.getCalories() < 1000); //true
noneMatch和allMatch是相反的,不再赘述。
anyMatch、allMatch、noneMatch这三个操作都用到了短路,和Java中的&&运算符类似。
- 查找元素。找到任一道素食。符合短路原则,找到结果时立即结束。
Optional<Dish> dish = menu.stream().filter(Dish::isVegetarian).findAny();
- 查找第一个元素
List<Integer> someNumbers = Arrays.asList(1,2,3,4,5);
Optional<Integer> firstSquareDivisibleByThree = someNumbers.stream()
.map(x -> x*x)
.filter(x -> x%2 ==0)
.findFirst(); //4
你可能会想,为什么会同时有findFirst和findAny呢?答案是并行。找到第一个元素在并行上限制更多。如果你不关心返回的元素是哪个,请使用findAny,因为它在使用并行流时限制较少。
归约
到目前为止,你见到过的终端操作都是返回一个boolean(allMatch等)、void(forEach)或Optional对象(findAny等)。你也见过了使用collect来将流中的所有元素组合成一个List。在本节中,你将看到如何把一个流中的元素组合起来,使用reduce操作来表达更复杂的查询,比如:计算菜单中的总卡路里。此类查询需要将流中所有元素反复结合起来,得到一个值,比如一个Integer。这样的查询被归类为归约操作。
- 元素求和
//1.有初始值
List<Integer> numbers = Arrays.asList(1,2,3,4);
int sum = numbers.stream().reduce(0,(a,b) -> a + b ); // 10
//1.1另一种方式
int sum = numbers.stream().reduce(0,Integer::sum);
//2.无初始值
Optional<Integer> sum = numbers.stream().reduce( (a,b) -> a + b ); //考虑到了流中没有值的情况
- 求最大值和最小值
Optional<Integer> maxNumber = numbers.stream().reduce( Integer::max );
Optional<Integer> minNumber = numbers.stream().reduce( Integer::min );
数值流
中间操作reduce可以计算流中元素的总和。现在,我们来计算菜单中菜的总热量:
int calories = menu.stream().map(Dish::getCalories).reduce(0,Integer::sum);
我们知道,在Stream里元素都是对象,那么,当我们操作一个数字流的时候就不得不考虑一个问题,装箱。getCalories返回值为int类型,所以需装箱为Integer类型才能放到流中。而一下次执行求和时又需要拆箱成int类型以参与运算。为了提高性能,Java8引入了3个原始类型特化流接口:IntStream、DoubleStream和LongSteam,分别将流中的元素特化为int、long和double,就是流中的元素允许以原始类型的身份存在,从而避免了暗含的装箱成本。每个接口都带有进行常用数值归约的新方法,如:sum、max等。
- 将流特化
IntStream result = menu.stream().mapToInt(Dish::getCalories); //mapToDouble、mapToLong
int sum = result.sum(); //如果流是空的,sum = 0;IntStream还支持其他方法,如:max,min,average等
- 转换回对象流
IntStream intStream = menu.stream().mapToInt(Dish::getCalories);
Stream<Integer> stream = intStream.boxed();
- 默认值OptionalInt。求和的例子很容易,因为它有一个默认值:0 。但是,如果你要计算IntStream中的最大值,就得换个办法了,因为0是错误的结果。如何区分没有元素的流和最大值真的为0的流呢?前面我们介绍了Optional类,这是一个可以表示值存在或不存在的容器。Optional可以用Integer、String等参考类型来参数化。对于三种原始流特化,也分别有一个Optional原始类型特化版本:OptionalInt、OptionalDouble、OptionalLong。例如:要找到IntStream中的最大值,可以调用max方法,它会返回一个OptionalInt:
OptionalInt maxCalories = menu.stream().mapToInt(Dish::getCalories).max();
//现在,如果没有最大值的话,你就可以显示处理OptionalInt去定义一个默认值了:
int max = maxCalories.orElse(1);
- 数值范围。IntStream和LongStream中都有两个静态方法:range和rangeClosed,来生成指定范围的所有值。这两个方法都是第一个参数接受起始值,第二个参数接受结束值。但range不包含结束值,rangeClosed包含结束值。
IntStream evenNumbers = IntStream.range(1,100).filter( n -> n%2 == 0 );
System.out.println(evenNumbers.count()); //49
创建
- 由值创建流。使用静态方法Stream.of,通过显式值创建一个流。
Stream<String> stream = Stream.of("java","python","c");
stream.map(String::toUpperCase).forEach(System.out::println);
//创建一个空流
Stream<String> emptyStream = Stream.empty();
- 由数组创建流。你可以使用静态方法Arrays.stream,从数组创建一个流。
int[] numbers = {2,5,8,9,7};
IntStream sum = Arrays.stream(numbers);
- 由函数生成流:创建无限流。Stream API提供了两个静态方法来从函数生成流:Stream.iterate和Stream.generate。这两个操作可以创建无限流。一般来说,应该使用limit(n)来对这种流加以限制,以避免打印无穷多个值。
//这是一个正偶数的流,用limit限制个数,forEach消费流
Stream.iterate( 0, n -> n+2 ).limit(10).forEach(System.out::println);
//generate方法接受一个Supplier<T>类型的Lambda提供新的值
Stream.generate(Math::random).limit(5).forEach(System.out::println);
//使用IntStream来避免装箱。生成一个全是1的无限流
IntStream ones = IntStream.generate( () -> 1 );
集合与流
集合保存值,可以迭代多次。
流按需生成,只可消费一次。
优点
Stream API可以让你写出这样的代码:
声明性。更简洁,更易读。
可复合。更灵活。
可并行。性能更好。
结语
流只能消费一次。
流是内部迭代。不用显式实现。
流中前一个元素遍历完整个操作链,下一个元素才开始执行。
参考资料:《Java8实战》