【Java8】Java 中的 Stream 流和基本操作

目录

1、什么是流?

2、Stream流的基本操作

(1)筛选和切片

(2)映射:map/flatMap

(3)查找和匹配

(4)归约

(5)数值流

3、Stream流练习


1、什么是流?

        简单定义:从支持数据处理操作生成的元素序列

        怎么理解元素序列?

        就像集合一样,流也提供了一个接口,可以访问特定元素类型的一组有序值。因为集合是数据结构,所以它的主要目的是以特定的时间/空间复杂度存储和访问元素(如ArrayList 与 LinkedList)。但流的目的在于表达计算,比如 filter、sorted map集合讲的是数据,流讲的是计算//由一系列有序计算步骤组成的步骤集

        怎么理解源?

        流需要使用一个提供数据的源,如集合、数组或输入/输出资源。这个源就是数据流,从有序集合生成流时会保留原有的顺序。由列表生成的流,其元素顺序与列表一致。

        需要注意的是,在 Java 中 null 不能作为数据源,但空集合可以,例如下边的代码:

//空集合
List<String> list1 = new ArrayList<>();
list1.stream().forEach(System.out::println);
//正常执行程序

//null
List<String> list2 = null;
list2.stream().forEach(System.out::println);
//执行异常:Cannot invoke "java.util.List.stream()" because "list2" is null

        这主要是因为,在Java中,空集合是一个有意义的数据,持有正常的引用,而 null 经常被用来表明一个变量或者对象引用当前没有任何有效的值(被垃圾回收器回收),换句话说,null 代表的是未知,不存在,无效值,因此也就无法生成数据流了。//在生成流时需要考虑空指针问题

        怎么理解数据处理操作?

        流的数据处理功能支持类似于数据库的操作,以及函数式编程语言中的常用操作,如 filter、map、reduce、find、match、sort 等。流操作可以顺序执行,也可并行执行。//常规操作有:过滤,映射,归约,筛选、匹配、排序等

        流的两个重要特点:

  1. 流水线——很多流操作本身会返回一个流,这样多个操作就可以链接起来,形成一个大的流水线。
  2. 内部迭代——与使用迭代器显式迭代的集合不同,流的迭代操作是在背后进行的。//这个特点很重要,内部迭代时,流可以进行自行优化和并行处理

2、Stream流的基本操作

        为了便于演示,接下来的操作都会用到以下特定的对象:

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;
    }

    public enum Type {MEAT, FISH, OTHER}

    @Override
    public String toString() {
        return name;
    }

    public static final List<Dish> menu =
            Arrays.asList(new Dish("pork", false, 800, Type.MEAT),
                    new Dish("beef", false, 700, Type.MEAT),
                    new Dish("chicken", false, 400, Type.MEAT),
                    new Dish("french fries", true, 530, Type.OTHER),
                    new Dish("rice", true, 350, Type.OTHER),
                    new Dish("season fruit", true, 120, Type.OTHER),
                    new Dish("pizza", true, 550, Type.OTHER),
                    new Dish("prawns", false, 400, Type.FISH),
                    new Dish("salmon", false, 450, Type.FISH));
}

(1)筛选和切片

        如何从流中选择元素?用谓词筛选,筛选出各不相同的元素,忽略流中的头几个元素,或将流截短至指定长度。示例程序如下:

    public static void main(String[] args) {
        List<Dish> result = menu.stream().filter(Dish::isVegetarian) //筛选
                .limit(3) //切片
                .collect(Collectors.toList());
        System.out.println(result);
    }
//输出
[french fries, rice, season fruit]

        上述示例中,演示了使用 filter 执行筛选操作,使用 limit 执行截流操作,获取满足过滤条件的前3个数据。

        有时候,除了过滤和节流,可能还会需要其他额外的操作,比如去重,或者跳过一些元素,示例代码如下:

    public static void main(String[] args) {
        List<Dish> result = menu.stream().filter(Dish::isVegetarian) //筛选
                .limit(3)      //获取前3个
                .skip(1)       //跳过第一个
                .distinct()    //去重
                .collect(Collectors.toList());
        System.out.println(result);
    }
//输出:
[rice, season fruit]

(2)映射:map/flatMap

        流支持map方法,它会接受一个函数作为参数。这个函数会被应用到每个元素上,并将其映射成一个新的元素。//接收参数,然后返回结果,中间可以存在一系列处理过程

    public static void main(String[] args) {
        List<Integer> result = menu.stream()
                .map(Dish::getName)    //映射: Dish -> Name(String)
                .map(String::length)   //映射:String -> Integer
                .collect(Collectors.toList());
        System.out.println(result);
    }
//输出:
[4, 4, 7, 12, 4, 12, 5, 6, 6]

        流的扁平化:使用 flatMap 可以把多个流合并成一个流。比如,将一个String类型的数组:["Hello","World"] 转变成 [H, e, l, l, o, W, o, r, l, d],就可以使用 flatMap 进行流合并

    public static void main(String[] args) {
        String[] worlds = {"Hello","World"};

        //collect0是一个String[]集合
        List<String[]> collect0 = Arrays.stream(worlds).map(r -> r.split("")).collect(Collectors.toList());

        //collect1是一个Stream<String>集合
        List<Stream<String>> collect1 = Arrays.stream(worlds).map(r -> r.split(""))
                .map(r -> Arrays.stream(r))     //将String[]映射成->Stream,也可以写成Arrays::stream
                .collect(Collectors.toList());

        List<String> collect3 = Arrays.stream(worlds).map(r -> r.split(""))
                .flatMap(Arrays::stream)        //合并多个流
                .collect(Collectors.toList());
        System.out.println(collect3);
    }
//输出:
[H, e, l, l, o, W, o, r, l, d]

        上述示例中,演示了使用 mapflatMap 映射的区别,map 只是在一个流中进行映射,flatMap 可以在映射完后,将所有映射后的流合并成一个流。

(3)查找和匹配

        StreamAPI 通过 allMatch、anyMatch、noneMatch、findFirst findAny 等方法提供了对数据的匹配和查找操作。

    public static void main(String[] args) {
        //匹配操作,使用短路求值:是一个终端操作
        boolean b = menu.stream().anyMatch(r -> r.isVegetarian());

        //查找操作:得到一个Optional容器类
        Optional<Dish> optional = menu.stream().findAny();
        optional.ifPresent(System.out::println); //如果容器中有值,那么就打印此值
    }

        匹配操作是一个终端操作,采用短路求值的方式获取匹配结果。所谓短路求值,就是获取到结果就返回,而不需要处理完整个流,类似于 && 和 || 运算符

        查找操作会返回一个Optional容器类,Optional<T>类代表一个值存在或不存在。在上面的代码中,findAny 可能什么元素都没找到,所以利用 optional,可以避免和 null 检查相关的 bug。

        不知你有没有发现,为什么会同时有 findrirst 和 findAny 呢?

        答案是并行。找到第一个元素在并行上限制更多。如果你不关心返回的元素是哪个,请使用findAny,因为它在使用并行流时限制较少。

(4)归约

        规约操作,就是将流归约成一个值。比如对一组数字进行求和://所谓的归约,就是要生成一个结果的操作,属于流水线的终端操作

    public static void main(String[] args) {
        List<Integer> numbers = new ArrayList<>(Arrays.asList(2, 5, 3, 10, 8, 12));
        Integer reduce = numbers.stream().reduce(0, (a, b) -> a + b);
        System.out.println(reduce);
    }

        上述示例中,reduce 接收了两个参数:

  • 一个初始值,这里是0;
  • 一个 Binary0perator<T> 函数,将两个元素结合起来产生一个新值,这里我们用的是 lambda (a,b) ->a + b,也可以是 (a,b) ->a * b 等。

        如果 reduce 不定义一个初始值,那么它会返回一个 Optional,表明归约值可能不存在:

Optional<Integer> reduce = numbers.stream().reduce((a, b) -> a + b);

        此外,还可以利用 reduce 来求最大值最小值

Optional<Integer> max = numbers.stream().reduce(Integer::max);
Optional<Integer> min = numbers.stream().reduce(Integer::min);

        最后总结一下,流的操作一般分为中间操作终端操作,以上的程序的操作示例中,每种操作对应的类型如下:

(5)数值流

        为什么需要数值流?

        主要是为了避免暗含的装箱成本。对比下边的求和方式,使用 reduce 进行求和时,会暗含装箱成本:

//引用类型
menu.stream().map(Dish::getCalories).reduce(0, Integer::sum); //需要拆箱
//原始类型
menu.stream().mapToInt(Dish::getCalories).sum();              //不需要拆箱

        所以,Java8引人了三个原始类型特化流接口来解决这个问题:Intstream、DoubleStream和LongStream,分别将流中的元素特化为 int、long 和 double,从而避免了暗含的装箱成本。

        有关装箱成本的详细内容,可以参考这篇文章《Java中的自动装箱和拆箱

        怎么理解特化流呢?

        对比下 map 和 mapToInt,就可以知道两种方式产生的流是不一样的,特化流是一种专门的流类型

//生成普通Stream
Stream<Integer> stream1 = menu.stream().map(Dish::getCalories);
//生成特化流IntStream
IntStream intStream = menu.stream().mapToInt(Dish::getCalories);

//特化流转化为普通流
Stream<Integer> stream2 = intStream.boxed();

        除此之外,每一个原始类型的特化流还对应有一个专门的默认值 Optionalxxx,这是一个表示值存在或不存在的容器:

OptionalInt optionalInt = menu.stream().mapToInt(Dish::getCalories).max();

        在特化流中,使用方法 range,rangeClosed 就可以根据数值范围生成一些数值,它们的区别在于 range 不包含区间结束值,rangeClosed 包含区间结束值,示例程序如下:

//不包含结束值:123456789
IntStream.range(1,10).forEach(System.out::print);
//包含结束值:  12345678910       
IntStream.rangeClosed(1,10).forEach(System.out::print);

3、Stream流练习

        了解了上边介绍的知识,马上练习一下吧,本次练习会使用到以下代码:

        Trader.java:

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

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

	public String getName() {
		return this.name;
	}

	public String getCity() {
		return this.city;
	}

	public void setCity(String newCity) {
		this.city = newCity;
	}

	public String toString() {
		return "Trader:" + this.name + " in " + this.city;
	}
}

        Transaction.java:

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

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

	public Trader getTrader() {
		return this.trader;
	}

	public int getYear() {
		return this.year;
	}

	public int getValue() {
		return this.value;
	}

	public String toString() {
		return "{" + this.trader + ", " + "year: " + this.year + ", " + "value:" + this.value + "}";
	}
}

        Test.java://数据

public class Test {
    public static void main(String[] args) {
        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<Transaction> 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, 2012, 700), new Transaction(alan, 2012, 950));
}

        练习要求://看答案前一定要自己先敲一遍代码

 * (1)找出2011年发生的所有交易,并按交易额排序(从低到高)。
 * (2)交易员都在哪些不同的城市工作过?
 * (3)查找所有来自于剑桥的交易员,并按姓名排序。
 * (4)返回所有交易员的姓名字符串,按字母顺序排序。
 * (5)有没有交易员是在米兰工作的?
 * (6)打印生活在剑桥的交易员的所有交易额。
 * (7)所有交易中,最高的交易额是多少?
 * (8)找到交易额最小的交易。

        练习答案:

import java.util.Arrays;
import java.util.Comparator;
import java.util.List;
import java.util.Optional;
import java.util.stream.Collectors;

public class Test {
    public static void main(String[] args) {
        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<Transaction> 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, 2012, 700),
                new Transaction(alan, 2012, 950));

        /**
         * (1)找出2011年发生的所有交易,并按交易额排序(从低到高)。
         */
        List<Transaction> collect0 = transactions.stream()
                .filter(r -> r.getYear() == 2011)
                .sorted(Comparator.comparing(Transaction::getValue))
                .collect(Collectors.toList());

        /**
         * (2)交易员都在哪些不同的城市工作过?
         */
        List<String> collect1 = transactions.stream()
                .map(r -> r.getTrader().getCity())
                .distinct()
                .collect(Collectors.toList());  //Collectors.toSet()去重

        /**
         * (3)查找所有来自于剑桥的交易员,并按姓名排序。
         */
        List<Trader> collect2 = transactions.stream()
                .map(r -> r.getTrader())
                .distinct()
                .filter(r -> r.getCity().equals("Cambridge"))
                .sorted(Comparator.comparing(Trader::getName))
                .collect(Collectors.toList());

        /**
         * (4)返回所有交易员的姓名字符串,按字母顺序排序。
         */
        String nameStr = transactions.stream()
                .map(r -> r.getTrader().getName())
                .distinct()
                .sorted()            
                .collect(Collectors.joining()); //.reduce("",(s1,s2)-> s1 + s2)

        /**
         * (5)有没有交易员是在米兰工作的?
         */
        boolean b = transactions.stream()
                .anyMatch(r -> r.getTrader().getCity().equals("Milan"));

        /**
         * (6)打印生活在剑桥的交易员的所有交易额。
         */
        transactions.stream()
                .filter(r -> r.getTrader().getCity().equals("Cambridge"))
                .map(r -> r.getValue())
                .forEach(System.out::println);

        /**
         * (7)所有交易中,最高的交易额是多少?
         */
        Optional<Integer> reduce = transactions.stream()
                .map(Transaction::getValue)
                .reduce(Integer::max);

        /**
         * (8)找到交易额最小的交易。
         */
        Optional<Transaction> min1 = transactions.stream()
                .reduce((v1, v2) -> v1.getValue() < v2.getValue() ? v1 : v2);
        //或者
        Optional<Transaction> min2 = transactions.stream()
                .min(Comparator.comparing(Transaction::getValue));
    }
}

        如果想对流操作有更多的了解,请参考我的这篇文章:《Java中的流收集器

        对于API的熟练,最主要的还是要多练习。至此,全文结束。

  • 0
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

swadian2008

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值