Java8新特性之Stream

168

本来以为Java8的新特性需要3篇文章才能写完的,但是发现其实Stream的接口并不想象的那么多,今天就可以完结啦!

流之初体验

首先先定义一个菜品类:

  • Dish
public class Dish {
    private final String name;
    private final boolean vegetarian;
    private final int calories;
    private final Type type;
    
    public boolean isVegetarian() {
        return vegetarian;
    }
    
    //省略set,get,toString方法
}

然后创建一个静态方法,并且设置成类的成员变量,以供测试。

public static List<Dish> getDishes() {
    return 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("pizza", true, 550, Dish.Type.OTHER),
        new Dish("prawns", false, 300, Dish.Type.FISH),
        new Dish("salmon", false, 450, Dish.Type.FISH)
    );
}

XNBqZ

好了,现在有个需求,找出菜品中所有小于400卡路里的食物,并且按照卡路里的大小进行排序。

java8之前,甚至有些人在java8之后,都会想着借助一个中间变量保符合要求的菜品,然后排序。

public static List<String> beforeJava8() {
    List<Dish> lowCaloricDishes = new ArrayList<>();
    for (Dish dish : dishes) {
        if (dish.getCalories() < 400) {
            lowCaloricDishes.add(dish);
        }
    }

    lowCaloricDishes.sort(Comparator.comparingInt(Dish::getCalories));
//    lowCaloricDishes.sort((d1, d2) -> Integer.compare(d1.getCalories(), d2.getCalories()));
    List<String> res = new ArrayList<>();

    for (Dish dish : lowCaloricDishes) {
        res.add(dish.getName());
    }
    return res;
}

由于前一篇文章讲过了方法引用,所以这里就直接用,不过下面一行也有普通的Lambda表达式的书写。

上述写法有什么问题吗,可以发现lowCaloricDishes 只使用了一次,真就一个临时变量。那能不能跳过创建变量的过程,你直接把数据给我,我经过过滤排序后得到想要的呢,就和流水线一样。

public static List<String> afterJava8() {
    return dishes.stream()
        .filter(dish -> dish.getCalories() < 400)
        .sorted(Comparator.comparing(Dish::getCalories))
        .map(Dish::getName)
        .collect(Collectors.toList());
}

img

这就是本篇要讲的流(Stream)了

流的定义

从支持数据处理操作的源生成的元素序列

流和集合有点类似,集合是数据结构,主要的目的是存储和访问元素,而流的主要目的是为了对元素进行一系列的操作。

通俗入门地来讲,集合就相当于你一部电影下载,流就相当于在线观看。其实只需要把流想成高级的集合即可。流有两个重要的特点:

  • 流水线: 很多流本身会返回一个流,这样多个流就能链接起来和流水线一般。
  • 内部迭代: 内部迭代也就是把迭代封装起来,如collect(Collectors.toList) ,与之相对应的外部迭代则是for-each

值得注意的是,和迭代器类似,流只能遍历一次 ,遍历完就可以说这个流消费掉了。

流的构建

流的构建

流常用的构建方式有4种,其实要么是借助Stream 类的静态方法,要么是借助别人的类的静态方法。

  • 由值创建流
  • 由数组创建流
  • 由文件生成流
  • 由函数生成流
public static void buildStream() throws IOException {
    Stream<String> byValue = Stream.of("java8", "c++", "go");

    Stream<Object> empty = Stream.empty();

    int[] nums = {1, 2, 3, 4, 5};
    IntStream byInts = Arrays.stream(nums);

    Stream<String> byFiles = Files.lines(Paths.get(""));

    Stream<Integer> byFunction1 = Stream.iterate(0, n -> n * 2);
    Stream<Double> byFunction2 = Stream.generate(Math::random);

    Stream<String> java = Stream.of("java");
}

流的操作

可以连接起来的流操作称为中间操作,关闭流的操作称为终端操作

通俗地讲,返回结果是流的操作称为中间操作,放回的不是流的操作称为终端操作。

image-20210414155605342

通过查找java8接口可以得知到哪些接口是中间操作,哪些接口时终端操作。由于那些接口描述得太过官方,估计我贴了也没啥人会仔细看,所以想看的直接去官方查阅即可。

流的使用

就按照官网上的java API顺序来讲述,小插一句,之前我一直没有学流是主要是因为感觉接口会很多,怎么可能记得了这么多,其实这几天看才发现真的很少,基本上不用记。

img

首先构建好一个数字列表:

List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5, 5, 5, 5, 6, 7);

中间操作

中间操作有去重、过滤、截断、查看、跳过、排序 ,这些相信大家都能够明白是什么意思。

public static void midOperation() {
    numbers.stream()
        .distinct()
        .forEach(System.out::println);

    List<Integer> filter = numbers.stream()
        .filter(n -> n % 2 == 0)
        .collect(Collectors.toList());

    numbers.stream()
        .limit(3)
        .forEach(System.out::println);

    numbers.stream()
        .peek(integer -> System.out.println("consume operation:" + integer))
        .forEach(System.out::println);

    List<Integer> skip = numbers.stream()
        .skip(2)
        .collect(Collectors.toList());

    numbers.stream()
        .sorted()
        .forEach(System.out::println);
}

中间操作之映射(map)

需要单独拎出来说的是映射(map)扁平化映射(flatMap) ,注意,这里的map并不是hashmap的那个map,而是说把什么映射或者说转化成了什么。

public static void midOperation() {
	List<String> map = numbers.stream()
                .map(Object::toString)			//这里就是把int映射成了string
                .collect(Collectors.toList());
}

而对于扁平化映射,现在又有一个需求,现在有个单词列表如{"hello", "world"},返回里面各不相同的字符,也就是要求返回List<String>

这还不简单,把单词映射成一个个字母,再去重就好了。

public static void flatMapDemoNoral() {
    List<String> words = Arrays.asList("hello", "world");
    List<String[]> normal = words.stream()
        .map(str -> str.split(""))
        .distinct()
        .collect(Collectors.toList());
}

img

虽然确实也能达到效果,但是注意映射所用的函数是split() ,返回的是String[] ,因此整个返回的是List<String[]>

那我映射完后再把每个String[] 数组映射成流

public static void flatMapDemoMap() {
    List<String> words = Arrays.asList("hello", "world");
    List<Stream<String>> usingMap = words.stream()
                .map(str -> str.split(""))
                .map(Arrays::stream)
                .distinct()
                .collect(Collectors.toList());
}

虽然摘掉了数组的帽子,但是返回的却是List<Stream<String>>

flatMap 正是为了解决这种情况的

public static void flatMapDemoFlatMap() {
    List<String> words = Arrays.asList("hello", "world");
    List<String> usingFlatMap = words.stream()
                .map(str -> str.split(""))
                .flatMap(Arrays::stream)
                .distinct()
                .collect(Collectors.toList());
}

可以简单的理解,map是把每个元素映射成了独立的流,而扁平化map是把元素保存了下来,最后映射成了一个流

查找与匹配

终端操作除了上述写例子的时候常用的collect()forEach() 还有查找和规约两种大的方向。

因为没啥好说的,直接上代码就完了:

public static void endOperationFindAndMatch() {
    if (dishes.stream().noneMatch(Dish::isVegetarian)) {
        System.out.println("所有的菜品都是非素食");
    }
    if (dishes.stream().allMatch(Dish::isVegetarian)) {
        System.out.println("所有的菜品都是素食");
    }
    if (dishes.stream().anyMatch(Dish::isVegetarian)) {
        System.out.println("菜品中至少有一道菜是素食");
    }

    Optional<Dish> any = dishes.stream()
        .filter(meal -> meal.getCalories() <= 1000)
        .findAny();
    Optional<Dish> first = dishes.stream()
        .filter(meal -> meal.getCalories() <= 1000)
        .findFirst();
}

归约(计算)

对流的规约操作的话,一般有普通操作也就是能直接调用接口的,还有一种就是借助reduce()

对于普通操作来说,像求和,最大值,最小值这些都是有接口对应的。

public static void endOperationCalculate() {
    long count = dishes.stream()
        .filter(meal -> meal.getCalories() <= 1000)
        .count();
    Optional<Dish> max = dishes.stream()
        .max(Comparator.comparingInt(Dish::getCalories));
    Optional<Dish> min = dishes.stream()
        .min(Comparator.comparing(Dish::getName));

}

但是如果说要求对元素求和,就要使用reduce()

image-20210417114209123

一般使用的是可以接受2个参数,一个是初始值,一个是BinaryOprator<T> 来将两个元素结合起来产生的新值。

public static void reduceDemo() {
    List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5, 5, 5, 5, 6, 7);
    Integer sum = numbers.stream().reduce(0, Integer::sum);
    
    //所有元素相乘也是比较简单
    Integer multi = numbers.stream().reduce(0, (a, b) -> a * b);
    
    //还有求最大值
    Optional<Integer> max = numbers.stream().reduce(Integer::max);
}

Optional类

上面一直出现有返回值是Optional<T> ,它是一个容器类代表一个值存在或者不存在,比如一开始的findAny() ,可能找不到符合条件的菜品。Java8引入的目的主要是/为了不要返回容易出现问题的null了。

就说几个比较常用的api就好了至于其它的可以上网看下官方API,今天说的API已经够多了

  • isPresent() 将在Optional 包含值的时候返回true,否则返回false
  • ifPresent(Consumer<T> block) 存在值的时候会执行给定的代码块
  • get() 存在值就返回值,否则抛出NoSuchElement异常
  • orElse() 存在值就返回,否则就返回一个默认值
public static void optionalDemo() {
    //ifPresent
    dishes.stream()
        .filter(Dish::isVegetarian)
        .findAny()
        .ifPresent(dish -> System.out.println(dish.getName()));

    //isPresent
    boolean isLowCalories= dishes.stream()
        .filter(dish -> dish.getCalories() <= 1000)
        .findAny()
        .isPresent();
	
    //get
    Optional<Dish> optional = dishes.stream()
        .filter(Dish::isVegetarian)
        .findAny();
    if (optional.isPresent()) {
        Dish dish = optional.get();
    }

    //orElse
    Dish dishNormal = dishes.stream()
        .filter(Dish::isVegetarian)
        .findAny()
        .orElse(new Dish("java", false, 10000, Dish.Type.OTHER));
}

总结

一样的,还是有几个小点没讲,比如并行流的部分,收集器部分,但是这些相对来说算是比较深入的。再经过两篇文章后,已经尽可能地全面地了解了Java8新特性。消化完后就可以去看《java8实战》,看书才是学习新知识的重点,最后就是应用了。