JDK8之Stream

本文介绍java 8 Stream API提供的诸多操作,利用这些操作可以让你快速的完成复制的数据查询,如筛选、切片、映射、查找、匹配和规约。包括一些特殊流的用法:数值流、文件流、数组流,无限流。

筛选和切片

用谓词筛选

Stream接口提供了filter方法,该方法接收一个谓词作为参数,并返回一个包含所有符合谓词的元素的流。

比如,筛选出所有的素菜:

List dishes = menu.stream().filter(Dish::isVegetarian).collect(toList());

元素去重

Stream还支持distinct方法,它会返回不重复的元素的流(根据元素的hashCode和equals方法)。
例如,筛选出下面列表中的所有不重复的偶数:

List numbers = Arrays.asList(1,2,5,2,6,3,3,2,4);
numbers.stream()
    .filter(i -> i % 2 == 0)
    .distinct() // 根据元素的hashcode和equals方法过滤重复元素

截断流

Stream还支持limit方法,返回一个不超过给定长度的流。所需的长度作为参数传递给limit。如果流是有序的,则最多会返回前n个元素。
比如,筛选出热量超过300卡路里的头三道菜:

List highCaloriesTop3 = menu.stream() // 由菜单得到一个流
    .filter(dish ->  dish.getCalories() > 300) // 接收Lambda,从流中排除某些元素
    .limit(3) // 截断流,使元素不超过指定数量
    .collect(Collectors.toList()); // 将流转换为其他形式

跳过元素

Stream还支持skip方法,返回一个扔掉了前n个元素的流。如果流中元素不足n个,则返回空流。
比如,找出超过300卡路里的菜,但忽略前2道。

List highCalories = menu.stream()
    .filter(dish -> dish.getCalories() > 300)
    .skip(2)
    .collect(Collectors.toList());

映射

对流中的每个元素应用函数

Stream支持map方法,接收一个函数作为参数,会对流中的每个元素应用该函数,并映射为一个新的元素。
比如,获取每道菜的名称:

List dishNames = menu.stream().map(Dish::getName).collect(toList());

流的扁平化

流支持flatMap方法,可以将流中的每个值都转换为一个流,然后把所有的流连接起来形成一个流。

例1:
给定如下单词列表[‘Hello’,’World’],如何得到不重复的字符?

List strings = Arrays.asList("Hello", "World");
// Arrays::stream将一个数组转换为一个流
List words = strings.stream()
    .map(s -> s.split("")) // 得到字符数组
    .flatMap(Arrays::stream) // 将字符数组中的每个元素都换成另外一个流,然后把所有的流连接起来形成一个新的流
    .distinct()
    .collect(Collectors.toList());

上面如果只是使用map方法,只能得到2个字符数组,而使用flatMap就可以将数组中的每个元素映射为一个流,然后把所有的流合起来形成一个新的流。

例2:
给定2个数字列表,[1,2,3],[4,5],如何返回所有的数对?[(1,4),(1,5),(2,4),(2,5),(3,4),(3,5)]

List numList1 = Arrays.asList(1, 2, 3);
List numList2 = Arrays.asList(4, 5);
List<int[]> nums = numList1.stream()
    .flatMap(i -> numList2.stream().map(j -> new int[]{i, j}))
    .collect(Collectors.toList());

查找和匹配

查找谓词是否至少匹配一个元素

使用anyMatch()方法。
比如,菜单中是否有素菜?

boolean isExistVegetarian = menu.stream().anyMatch(d -> d.isVegetarian());

检查谓词是否匹配所有元素

使用allMatch()方法。

比如,是否所有的菜卡路由都小于1000?

boolean allLess1000 = menu.stream().allMatch(d -> d.getCalories() < 1000);

检查谓词是否券都不匹配所有元素

使用noneMatch,与allMatch对应。

比如,是否是否所欲的菜卡路由都是小于1000?

boolean allLess1000 = menu.stream().noneMatch(d -> d.getCalories() >= 1000);

查找元素

findAny()方法返回当前流中的任意元素,可以和其他流操作结合使用。

比如,找一道素菜(随便哪一道):

Optional dish = menu.stream().filter(Dish::isVegetarian).findAny();

注意:findAny()返回的是一个Optional。它是一个容器类,代表一个值存在或不存在。用来解决null值问题。

常用方法如下:
* isPresent
包含值的时候返回true,否则返回false;
* ifPresent(Consumer block)
在存在值时执行给定的代码块。
* get
在存在值时返回值,否则抛出一个NoSuchElement异常
* orElse(T other)
在存在值时返回值,否则返回一个默认值。

比如,打印一道素菜的名称:

menu.stream().filter(Dish::isVegetarian).findAny().ifPresent(d -> System.out.println(d.getName()));

规约

元素求和

使用for-each求和

List numbers = Arrays.asList(1,2,3,9);
int sum = 0;
for (int x:numbers) {
    sum += x;
}

使用reduce()方法

int sum = numbers.stream().reduct(0,(a,b) -> a+b);

第1个参数:初始值
第2个参数:一个BinaryOperator ,将2个元素结合起来产生一个新值。

求和过程:
首先,0作为Lambda的第1个参数(a),从流中获取到1作为第2个参数,0+1=1,它成为了新的累积值。
然后,在用累积值和下一个元素调用Lambda,产生新的累积值3。

综合示例
有如下交易和交易员,计算下面8个问题。
交易员

public class Trader {
    private final String name;
    private final String city;

    public Trader(String name, String city) {
        this.name = name;
        this.city = city;
    }

    public String getName() {
        return name;
    }

    public String getCity() {
        return city;
    }

    @Override
    public String toString() {
        return "Trader{" +
                "name='" + name + '\'' +
                ", city='" + city + '\'' +
                '}';
    }
}

交易

public class Transaction
{
    private final Trader trader;
    private final int year;
    private final int value;

    public Transaction(Trader trader, int year,int value) {
        this.value = value;
        this.year = year;
        this.trader = trader;
    }

    public Trader getTrader() {
        return trader;
    }

    public int getYear() {
        return year;
    }

    public int getValue() {
        return value;
    }

    @Override
    public String toString() {
        return "Transaction{" +
                "trader=" + trader +
                ", year=" + year +
                ", value=" + value +
                '}';
    }
}
Trader raoul = new Trader("Raoul","Cambridge");
Trader mario = new Trader("Mario","Milan");
Trader alan = new Trader("Alan","Cambridge");
Trader brian = new Trader("Brian","Cambridge");

List transactions = Arrays.asList(
    new Transaction(brian,2011,300),
    new Transaction(raoul,2012,1000),
    new Transaction(raoul,2011,400),
    new Transaction(mario,2012,710),
    new Transaction(mario,2011,700),
    new Transaction(alan,2012,950)
);

// 找出2011年发生的所有交易,并按交易额排序(从低到高)
transactions.stream().filter(t -> t.getYear() == 2011).sorted(Comparator.comparing(Transaction::getValue)).forEach(System.out::println);
System.out.println("1.=======================");
// 交易员都在哪些不同的城市工作过
transactions.stream().map(t -> t.getTrader().getCity()).distinct().forEach(System.out::println);
System.out.println("2.=======================");
// 查找所有来自剑桥的交易员,并按姓名排序
transactions.stream().filter(t -> "Cambridge".equals(t.getTrader().getCity())).map(t -> t.getTrader().getName()).distinct().sorted().forEach(System.out::println);
// 或者
transactions.stream().map(t -> t.getTrader()).filter(t -> t.getCity().equals("Cambridge")).distinct().sorted(Comparator.comparing(Trader::getName)).forEach(System.out::println);
System.out.println("3.=======================");
// 返回所有交易员的姓名字符串,按字母顺序排序
transactions.stream().map(t -> t.getTrader().getName()).distinct().sorted().forEach(System.out::println);
System.out.println("4.=======================");
// 有没有交易员是在米兰工作的
transactions.stream().filter(t -> "Milan".equals(t.getTrader().getCity())).findAny().ifPresent(System.out::println);
System.out.println("5.=======================");
// 打印生活在剑桥的交易员的所有交易额
transactions.stream().filter(t -> "Cambridge".equals(t.getTrader().getCity())).map(t -> t.getValue()).reduce(Integer::sum).ifPresent(System.out::println);
//或 mapToInt转化为int流,然后用IntStream的sum求和
transactions.stream().filter(t -> "Cambridge".equals(t.getTrader().getCity())).mapToInt(Transaction::getValue).sum();
System.out.println("6.=======================");
// 所有交易额中,最高的交易额是多少
transactions.stream().map(t -> t.getValue()).reduce(Integer::max).ifPresent(System.out::println);
System.out.println("7.=======================");
// 找到交易额最少的交易
transactions.stream().filter(t -> t.getValue() == transactions.stream().map(a -> a.getValue()).reduce(Integer::min).get()).forEach(System.out::println);
// 或
transactions.stream().reduce((t1,t2) -> t1.getValue() < t2.getValue() ? t1 : t2).ifPresent(System.out::println);
// 或
transactions.stream().min(Comparator.comparing(Transaction::getValue)).ifPresent(System.out::println);

数值流

原始类型流特化

在java 8中,引入了3个原始类型特化流接口:IntStream,DoubleStream和LongStream,分别将流中的元素特化为int,double和long,从而避免暗含的装箱(int->Integer)成本。
每个接口都带有常用数值规约的方法,比如对数值就和的sum,找到最大元素的max。
还有在必要时将它们转换为流对象的方法。

映射到数值流
将流转换为特化版本的常用方法是mapToInt,mapToDouble和mapToLong。这些方法和前面说的流工作方式一样,只是返回的是一个特化流,而不是Stream。
例如,使用mapToInt对菜单中所有菜肴的卡路里求和。

int sum = menu.stream()
    .mapToInt(Dish::getCalories) // 转换为IntStream
    .sum(); // 使用IntStream提供的sum方法求和。

转换到对象流
有了数值流,你还可以将数值流转换回对象流。将原始流转换为对象流,可以使用boxed方法。

IntStream is = menu.stream().mapToInt(Dish::getCalories);
Stream s = is.boxed();

OptionalInt
IntStream/DoubleStream/LongStream对max、min等计算返回的是OptionalInt/OptionalDouble/OptionalLong。之前说过Optional的用法,这些OptionalXXX用法是一样的。
比如,求最大值:

// OptionalInt
OptionalInt max = transactions.stream().mapToInt(Transaction::getValue).max();
max.ifPresent(System.out::println);

数值范围

在java 8中,IntStream和LongStream提供了range()和rangeClosed()用于生成一定范围内的数字。range不包括结束值,rangeClosed包括结束值。
比如,求1到100有多少个偶数?

IntStream.rangeClosed(1,100).filter(i -> i % 2 == 0).count();

数值流应用:勾股数
勾股数满足:a*a+b*b=c*c,a,b,c均为正数。比如(3,4,5)即为一组有效的勾股数。
下面使用IntStream来计算100内的勾股数。

IntStream.rangeClosed(1,100)
    .boxed()
    .flatMap(a -> IntStream.rangeClosed(a,100) // 从a开始,避免出现重复的数据,比如[3,4,5]和[4,3,5]
                           .filter(b -> Math.sqrt(a*a+b*b) % 1 == 0) // a的平方+b的平方开平方根是整数
                           .mapToObj(b -> new int[]{a,b,(int)Math.sqrt(a*a+b*b)})) // 返回符合条件的int数组
    .forEach(i -> System.out.println(i[0] + "," + i[1] + "," + i[2]));

// 上面的方案导致计算了2次平方根,方案2先生成a*a+b*b开平方后的结果,然后再过滤掉不符合要求的数
System.out.println("========方案2===========");
IntStream.rangeClosed(1,100)
    .boxed()
    .flatMap(a -> IntStream.rangeClosed(a,100)
        .mapToObj(b -> new double[] {a,b,Math.sqrt(a*a+b*b)}))
    .filter(a -> a[2] % 1 == 0)
    .forEach(a -> System.out.println(a[0] + "," + a[1] + "," + a[2]));

构建流

由值创建流

可以使用静态方法Stream.of,来创建一个流,它可以接收任意数量的参数。
比如,创建一个字符串流。

Stream s = Stream.of("Java 8" ,"Lambda","In","Action");

由数组创建流

可以使用静态方法Arrays.stream来从数组创建一个流,它接收一个数组作为参数。
比如:

int[] numbers = {1,3,4,6,3,9};
int sum = Arrays.stream(numbers).sum(); // 对数组元素求和

由文件创建流

java中用于处理文件等I/O操作的NIO API已更新,以便使用Stream API。java.nio.file.Files中很多静态方法都会返回一个流。
比如一个很有用的方法Files.lines,它返回一个由指定文件中各行构成的字符串流。
比如:计算文件中有多少个不重复的单词。

// 由文件生成流
String path = TraderDemo.class.getClassLoader().getResource("").getPath() + "data.txt";
// 统计文件中不重复的单词数量
try (Stream lines = Files.lines(Paths.get(path.substring(1)), Charset.forName("utf-8"))){
    long count = lines.flatMap(line -> Arrays.stream(line.split(" "))).distinct().count();
    System.out.println("distinct words count:" + count);
} catch (IOException e) {
    e.printStackTrace();
}

由函数生成流:创建无限流

Stream API提供了2个静态方法从函数生成流:iterate和generate,这2个操作可以创建无限流。一般来说,应该使用limit(n)来加以限制,否则会无穷无尽的计算下去。

iterate
比如:

// 从0开始,生成10个数,每个数是前面的数加2
Stream.iterate(0,n -> n+2).limit(10).forEach(System.out::println); // 0,2,4,6,8...

iterate方法接收一个初始值(在这里是0),还有一个依次应用在每个产生的新值上的Lambda。

利用iterate实现斐波那契数列

// 斐波那契数列(0,1,1,2,3,5,8,13,21,34,55...)除最开始的2个数字0和1外,后续的每个数字是前2个数字之和。
// 生成前20个元素(0,1),(1,1),(1,2),(2,3),(3,5),(5,8)...
Stream.iterate(new int[]{0,1},a -> new int[]{a[1],a[0] + a[1]}).limit(20).forEach(a -> System.out.println("(" + a[0] + "," + a[1] + ")"));
// 只打印数字
Stream.iterate(new int[]{0,1},a->new int[]{a[1],a[0]+a[1]}).limit(20).map(a -> a[0]).forEach(System.out::println);

generate
与iterate类似,可以生成无限流。但它不是依次对每个新生成的值应用Lambda.

例如,创建5个随机的0到1的数

Stream.generate(Math::random).limit(5).forEach(System.out::println);

如果用generate来实现斐波那契数列,则要这么写

// 使用generate上下行斐波那契数列,不推荐,只是演示
IntSupplier fib = new IntSupplier() { // 使用匿名类保存状态(前一项和当前项)
    int prev = 0;
    int curr = 1;
    @Override
    public int getAsInt() {
        int oldPrev = prev;
        int nextVal = prev + curr;
        prev = curr;
        curr = nextVal;
        return oldPrev;
    }
};
IntStream.generate(fib).limit(10).forEach(System.out::println);

参考《Java8实战》

你可能感兴趣的:(JDK8之Stream)