Java基础扩展提升总结:Java 8 新特性之(四)Stream API

 Sream API

1、什么是Stream API 及其作用

(1)简介

Stream是Java8中处理数组、集合的抽象概念,它可以指定你希望对集合进行的操作,可以执行非常复杂的查找、过滤和映射数据等操作。它与 java.io 包里的 InputStream 和 OutputStream 是完全不同的概念。使用Stream API对集合数据进行操作,就类似于使用SQL执行的数据库查询。Stream API 借助于同样新出现的 Lambda 表达式,极大的提高编程效率和程序可读性。同时它提供串行和并行两种模式进行汇聚操作,并发模式能够充分利用多核处理器的优势,使用 fork/join 并行方式来拆分任务和加速处理过程。

Stream 不是集合元素,它不是数据结构并不保存数据,它是有关算法和计算的,它更像一个高级版本的 Iterator。当我们使用一个流的时候,通常包括三个基本步骤:

获取一个数据源(source)→ 数据转换→执行操作获取想要的结果。

每次转换原有 Stream 对象不改变,返回一个新的 Stream 对象(可以有多次转换),这就允许对其操作可以像链条一样排列,变成一个管道,为我们提供链式编程(流水线式)编程的能力。

2、Stream API 的使用

(1)我先举个栗子:

我们有一个需求:统计一个字符串类型集合中,所有长度大于6的元素个数。

传统实现代码:

package basis.StuStream;

import java.util.ArrayList;
import java.util.List;

public class StuStream {
    public static void main(String[] args) {
        List<String> datas = new ArrayList<>();
        datas.add("hello");
        datas.add("hellooo");
        datas.add("helloooooo");
        datas.add("helloo");
        datas.add("hellooooo");

        int count = 0;
        for (String data : datas) {
            if (data.length()>6){
                count++;
            }
        }
        System.out.println(count);
    }
}

使用Stream API 实现:

package basis.StuStream;

        import java.util.ArrayList;
        import java.util.List;

public class StuStream {
    public static void main(String[] args) {
        List<String> datas = new ArrayList<>();
        datas.add("hello");
        datas.add("hellooo");
        datas.add("helloooooo");
        datas.add("helloo");
        datas.add("hellooooo");

        long count = datas.stream()
                .filter(data->data.length()>6)
                .count();
        System.out.println(count);
    }
}

从上面的代码中,我们可以看出,使用Stream API 大致分为三步,

  1. 创建一个Stream。 (创建)
  2. 在一个或多个步骤中,将初始Stream转化到另一个Stream的中间操作。 (中间操作)
  3. 使用一个终止操作来产生一个结果。该操作会强制他之前的延迟操作立即执行。在这之后,该Stream就不会在被使用了。(终止操作)

(2)如何获取Stream流

有多种方式生成 Stream Source:

1)从 Collection 和 数组中获取

  1. Collection.stream()
  2. Collection.parallelStream()
  3. Arrays.stream(T array) or Stream.of()

2)从BufferedReader中获取

  1. java.io.BufferedReader.lines()

3)静态工厂

  1. java.util.stream.IntStream.range()
  2. java.nio.file.Files.walk()

4)自己构建

  1. java.util.Spliterator

5)其它

  1. Random.ints()
  2. BitSet.stream()
  3. Pattern.splitAsStream(java.lang.CharSequence)
  4. JarFile.stream()

(3)流的操作

流的操作类型分为两种:

Intermediate(即中间操作)

一个流可以后面跟随零个或多个 intermediate 操作。其目的主要是打开流,做出某种程度的数据映射/过滤,然后返回一个新的流,交给下一个操作使用。这类操作都是惰性化的(lazy),就是说,仅仅调用到这类方法,并没有真正开始流的遍历,这被称为“惰性求值”

中间操作有:

map (mapToInt, flatMap 等)、 filter、distinct、sorted、peek、limit、skip、parallel、sequential、unordered。

Terminal(终止操作)

一个流只能有一个 terminal 操作,当这个操作执行后,流就被使用“光”了,无法再被操作。所以这必定是流的最后一个操作。Terminal 操作的执行,才会真正开始流的遍历,并且会生成一个结果,或者一个 side effect。

终止操作有:

forEach、forEachOrdered、toArray、reduce、collect、min、max、count、anyMatch、allMatch、noneMatch、findFirst、findAny、iterator。

需要注意的是,对于基本数值型,目前有三种对应的包装类型 Stream:

IntStream、LongStream、DoubleStream。当然我们也可以用 Stream<Integer>、Stream<Long> >、Stream<Double>,但是 boxing 和 unboxing 会很耗时,所以特别为这三种基本数值型提供了对应的 Stream。

Java 8 中还没有提供其它数值型 Stream,因为这将导致扩增的内容较多。而常规的数值型聚合运算可以通过上面三种 Stream 进行。

3、获取Stream流的方法示例

(1)Stream.of() 方法:

 //(1)Stream.of方法
Stream<String> stream = Stream.of("hello", "xxx", "yyy", "beijing", "shanghai");
//调用方法
long count = stream.filter(s -> s.length() > 5).count();
System.out.println(count);

Stream.of()接收一个可变的参数,该参数可以是任意类型,返回一个Stream。Stream.of()源码:

public static<T> Stream<T> of(T... values) {
    return Arrays.stream(values);
}

Stream.of()方法内调用的是Arrays的静态方法stream()。

(2)Arrays.stream()方法:

//(2)Arrays.stream方法
IntStream stream = Arrays.stream(new int[]{100, 200, 300, 400});
double avg = stream.average().getAsDouble();
System.out.println(avg);

Arrays中的静态方法stream()接收一个 int 类型数组作为参数,返回一个IntStream。Arrays.stream()源码:

public static IntStream stream(int[] array) {
    return stream(array, 0, array.length);
}

IntStream中:

  • average():用于计算数组所有值的平均数;
  • getAsDouble():将得到的数据以 double 类型返回;

(3)Collection.stream() 和 Collection.parallelStream()方法

Collection集合接口中有两个用于获取Stream的默认方法,一个是Collection.Stream()方法,用于获取串行的Stream;另一个是Collection.parallel()方法用于获取并行的Stream(),所有Collection集合的实现类都继承了这两个方法。

  • Collection.stream():
//(3)使用集合的方法(重点)
List<String> list=new ArrayList<>();
list.add("aaaa");
list.add("bbbb");
list.add("cccc");
list.add("dddd");
        
Stream<String> stringStream = list.stream();
long count2 = stringStream.count();
System.out.println(count2);

count():终止操作,统计执行了一系列中间操作后集合中元素的个数 

源码:

default Stream<E> stream() {
    return StreamSupport.stream(spliterator(), false);
}
  • Collection.parallelStream():
List<String> list=new ArrayList<>();
list.add("aaaa");
list.add("bbbb");
list.add("cccc");
list.add("dddd");

Stream<String> stream3 = list.parallelStream();
long count1 = stream3.count();
System.out.println(count1);

源码:

default Stream<E> parallelStream() {
    return StreamSupport.stream(spliterator(), true);
}

(4)Stream.iterate

Stream类的静态方法 iterate() 方法也返回一个Stream,可以把这个方法理解为 迭代。

System.out.println("----------迭代-----------");
Stream<Integer> iterate = Stream.iterate(0, x -> x + 2);
iterate.limit(10)
    .forEach(System.out::println);

上述代码中,我们使用Stream.iterate()生成十个连续的数,每个数在前一个数的基础上加2,第一个参数表示数应该从几开始,第二个参数是生成数的规则的Lambda表达式。

  • limit():中间方法,用于限制Stream处理数据的个数;
  • forEach():终止方法,遍历。

源码:

    public static<T> Stream<T> iterate(final T seed, final UnaryOperator<T> f) {
        Objects.requireNonNull(f);
        final Iterator<T> iterator = new Iterator<T>() {
            @SuppressWarnings("unchecked")
            T t = (T) Streams.NONE;

            @Override
            public boolean hasNext() {
                return true;
            }

            @Override
            public T next() {
                return t = (t == Streams.NONE) ? seed : f.apply(t);
            }
        };
        return StreamSupport.stream(Spliterators.spliteratorUnknownSize(
                iterator,
                Spliterator.ORDERED | Spliterator.IMMUTABLE), false);
    }

可以看出,iterate()方法中使用内部类的方式实现了一个Iterator迭代器。

(5)Stream.generate()

Stream类的静态方法 iterate() 方法也返回一个Stream,可以把这个方法理解为 生成。

System.out.println("--------生成----------");
Stream<Integer> generate = Stream.generate(() -> new Random().nextInt(100));
generate.limit(10)
    .forEach(System.out::println);

上述代码生成了10个0~100的随机数,Stream.generate()方法接受一个Supplier(供给型接口)类型的参数,用于定义生成规则。

源码:

    public static<T> Stream<T> generate(Supplier<T> s) {
        Objects.requireNonNull(s);
        return StreamSupport.stream(
                new StreamSpliterators.InfiniteSupplyingSpliterator.OfRef<>(Long.MAX_VALUE, s), false);
    }

4、Stream中的常用的中间操作

常用的中间操作:map (mapToInt, flatMap 等)、 filter、distinct、sorted、peek、limit、skip、parallel、sequential、unordered。

JavaBean:

package StuStream;

public class Employee {
    private String name;
    private int age;
    private double salary;
    private String gender;

    public Employee(String name, int age, double salary) {
        this.name = name;
        this.age = age;
        this.salary = salary;
    }

    public Employee(String name, int age, double salary, String gender) {
        this.name = name;
        this.age = age;
        this.salary = salary;
        this.gender = gender;
    }

    public Employee() {

    }

   //省略getter和setter

    @Override
    public String toString() {
        return "Employee{" +
                "name='" + name + '\'' +
                ", age=" + age +
                ", salary=" + salary +
                ", gender='" + gender + '\'' +
                '}';
    }
}

数据准备:

List<Employee> employees=new ArrayList<>();
employees.add(new Employee("xxx", 30, 10000));
employees.add(new Employee("yyy", 29, 8000));
employees.add(new Employee("zzz", 22, 12000));
employees.add(new Employee("张三", 21, 20000));
employees.add(new Employee("李四", 32, 22000));
employees.add(new Employee("李四", 32, 22000));

 (1)filter():过滤,从流中排除元素

 过滤出年龄大于25的并遍历输出。

System.out.println("------filter-----");
employees.stream()
    .filter(e->e.getAge()>25)
    .forEach(System.out::println);

(2)limit():限制。截断流,使其元素不超过给定数量

输出集合中前两个元素。

System.out.println("------limit-----");
employees.stream()
    .limit(2)
    .forEach(System.out::println);

(3)skip(n):跳过元素。返回一个扔掉了前 n 个元素的流。若流中元素不足 n 个,则返回一个空流

System.out.println("-------skip------");
employees.stream()
    .skip(2)
    .limit(2)
    .forEach(System.out::println);

(4)distinct():筛选,通过流所生成元素的 equals() 去除重复元素 

数据中的最后两个对象属于重复对象,我们要想过滤掉重复的元素,可以使用distinct()方法。

System.out.println("-----distinct------");
employees.stream()
    .distinct()
    .forEach(System.out::println);

运行结果:

-----distinct------
Employee{name='xxx', age=30, salary=10000.0, gender='null'}
Employee{name='yyy', age=29, salary=8000.0, gender='null'}
Employee{name='zzz', age=22, salary=12000.0, gender='null'}
Employee{name='张三', age=21, salary=20000.0, gender='null'}
Employee{name='李四', age=32, salary=22000.0, gender='null'}
Employee{name='李四', age=32, salary=22000.0, gender='null'}

为什么没有过滤掉的,原因是我们的JavaBean类Employee,没有重写hashcode() 和 equals() 方法,为Employee类添加如下代码:

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        Employee employee = (Employee) o;
        return age == employee.age &&
                Double.compare(employee.salary, salary) == 0 &&
                Objects.equals(name, employee.name);
    }

    @Override
    public int hashCode() {
        return Objects.hash(name, age, salary);
    }

再次运行:

-----distinct------
Employee{name='xxx', age=30, salary=10000.0, gender='null'}
Employee{name='yyy', age=29, salary=8000.0, gender='null'}
Employee{name='zzz', age=22, salary=12000.0, gender='null'}
Employee{name='张三', age=21, salary=20000.0, gender='null'}
Employee{name='李四', age=32, salary=22000.0, gender='null'}

此时,重复的元素被过滤掉了。

(5)map():转换或映射。将数据从一种格式转换成另一种格式。

将元素转换成其他形式或提取信息。接收一个函数作为参数,该函数会被应用到每个元素上,并将其映射成一个新的元素。

获取原集合中所有元素的name属性。

System.out.println("-------------map---------------");
employees.stream()
    .map(e->e.getName())
    .forEach(System.out::println);

结果:

-------------map---------------
xxx
yyy
zzz
张三
李四
李四

将集合中的String类型的元素全部转成大写。

List<String> strList = Arrays.asList("aaa", "bbb", "ccc", "ddd", "eee");
strList.stream()
    .map(s->s.toUpperCase())
    .forEach(System.out::println);

(6)sorted():对元素进行排序。

使用stored()方法排序有两种方法:

  • stored():默认使用自然序排序, 其中的元素必须实现Comparable接口
  • sorted(Comparator<? super T> comparator) :我们可以使用lambada 来创建一个Comparator实例。可以按照升序或着降序来排序元素。

1)自然排序

自然排序数据必须是是基本数据类型或String类型的,自然排序按字典顺序进行。

List<Employee> employees=new ArrayList<>();
employees.add(new Employee("5xxx", 30, 10000));
employees.add(new Employee("4yyy", 29, 8000));
employees.add(new Employee("1zzz", 22, 12000));
employees.add(new Employee("c张三", 21, 20000));
employees.add(new Employee("b李四", 32, 24000));
employees.add(new Employee("a李四", 32, 22000));

System.out.println("-----------------自然排序-------------------");
employees.stream()
    .map(Employee::getName)
    .sorted()
    .forEach(System.out::println);

自然排序结果:

1zzz
4yyy
5xxx
a李四
b李四
c张三

2)自定义排序

如果数据引用数据类型的,想要对数据进行排序,就需要我们自定义排序规则:实现方法有两种:

  • JavaBean(数据)实现Compareable
  • 使用sorted()方法时通过Lambda表达式传入自定义比较器

两种方式是等效的,只不过使用Comparator更加linghuo具体实现

  • 实现Compareable:

修改Employee类,让其实现Compareable接口,并重写comparTo方法:

@Override
public int compareTo(Object o) {
    int age = this.getAge()-((Employee)o).getAge();
    int salary =  Double.compare(this.getSalary(),((Employee)o).getSalary());
    return age==0?salary:age;
}

排序:

System.out.println("-------自定义排序排序(实现Compareable接口) ---------");
employees.stream()
    .sorted()
    .forEach(System.out::println);

结果:

Employee{name='c张三', age=21, salary=20000.0, gender='null'}
Employee{name='1zzz', age=22, salary=12000.0, gender='null'}
Employee{name='4yyy', age=29, salary=8000.0, gender='null'}
Employee{name='5xxx', age=30, salary=10000.0, gender='null'}
Employee{name='a李四', age=32, salary=22000.0, gender='null'}
Employee{name='b李四', age=32, salary=24000.0, gender='null'}
  • 传入Comparator比较器
employees.stream()
    .sorted((o1,o2)->{
        int n1=o1.getAge()-o2.getAge();
        int n2=Double.compare(o1.getSalary(), o2.getSalary());
        return n1==0?n2:n1; })
    .forEach(System.out::println);

结果跟Compareable一样,因为两种方式是等效的。

5、Stream中的常用的终止操作

数据准备:

List<Employee> employees=new ArrayList<>();
employees.add(new Employee("xxx", 30, 10000,"男"));
employees.add(new Employee("yyy", 29, 8000,"男"));
employees.add(new Employee("zzz", 22, 12000,"男"));
employees.add(new Employee("张三", 21, 20000,"男"));
employees.add(new Employee("李四", 32, 22000,"妖"));

(1)匹配:返回值均为boolean类型

  • allMatch():检查是否匹配所有元素
  • anyMatch():检查是否至少匹配一个元素
  • noneMatch():检查是否没有匹配的元素
boolean b=employees.stream()
    .allMatch(e->e.getGender().equals("男"));
System.out.println("结果:"+b);

boolean b2= employees.stream()
    .anyMatch(e -> e.getGender().equals("妖"));
System.out.println(b2);

boolean b3= employees.stream()
    .noneMatch(e -> e.getGender().equals("女"));
System.out.println(b3);

(2)返回流中的一个元素,返回类型均为Optional类型的

  • findFirst():返回第一个元素
  • findAny():返回当前流中的任意元素
Optional<Employee> first = employees.stream()
    .findFirst();
System.out.println(first.get().toString());

Optional<Employee> any = employees.stream()
    .findAny();
System.out.println(any.get().toString());

findAny()方法在串行和并行流中返回任意一个数据时,总是返回最快得到的那个元素。

Optional类:Java 8 中为了解决空指针异常而引入的类。

  • Optional 类是一个可以为null的容器对象。如果值存在则isPresent()方法会返回true,调用get()方法会返回该对象。
  • Optional 是个容器:它可以保存类型T的值,或者仅仅保存null。Optional提供很多有用的方法,这样我们就不用显式进行空值检测。
  • Optional 类的引入很好的解决空指针异常。

(3)最值:获取流中的最大值最小值,参数为Comparator类型

  • max():返回流中最大值
  • min():返回流中最小值

返回工资最高的员工:

Optional<Employee> max = employees.stream()
    .max((o1, o2) -> Double.compare(o1.getSalary(), o2.getSalary()));
System.out.println(max.get().toString());

返回工资最低的员工:

Optional<Employee> min = employees.stream()
    .min((o1, o2) -> Double.compare(o1.getSalary(), o2.getSalary()));
System.out.println(min.get().toString());

(4)reduce():规约

这个方法的主要作用是把 Stream 元素组合起来。它提供一个起始值(种子),然后依照运算规则(BinaryOperator),和前面 Stream 的第一个、第二个、第 n 个元素组合。从这个意义上说,字符串拼接、数值的 sum、min、max、average 都是特殊的 reduce。

//数值求和
List<Integer> list= Arrays.asList(10,20,30,40);
Integer sum = list.stream()
    .reduce(10, (x, y) -> x + y);
System.out.println(sum);

//字符串拼接
List<String> strings= Arrays.asList("aa","bb","cc","dd");
String str = strings.stream()
    .reduce("aa", (x, y) -> x + y);
System.out.println(str);

//计算所有员工工资总和
Double salay = employees.stream()
    .map(Employee::getSalary)
    .reduce(0.0, Double::sum);
System.out.println(salay);

(5)Collect:把数据按照指定的集合类型进行返回。

获取所有的员工姓名的List集合

List<String> collect = employees.stream()
    .map(Employee::getName)
    .collect(Collectors.toList());
System.out.println(collect);

获取所有的员工姓名的Set集合:Collectors.toSet()方法。

6、Collectors类

用 Collectors 来进行 reduction 操作

java.util.stream.Collectors 类的主要作用就是辅助进行各类有用的 reduction 操作,例如转变输出为 Collection,把 Stream 元素进行归组。

1、groupingBy():按照指定的条件分组

返回一个Map,键为分组条件,值为一个单列集合Collection,表示满足分组条件的元素

按年龄将员工分组:

package StuStream;

import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;

/*
 * wgy 2019/8/17 15:30
 */
public class Demo4 {

    public static void main(String[] args) {
        List<Employee> employees=new ArrayList<>();
        employees.add(new Employee("xxx", 30, 10000,"男"));
        employees.add(new Employee("yyy", 21, 8000,"男"));
        employees.add(new Employee("zzz", 22, 12000,"男"));
        employees.add(new Employee("张三", 21, 20000,"男"));
        employees.add(new Employee("李四", 22, 22000,"妖"));


        Map<Integer,List<Employee>> map = employees.stream()
                .collect(Collectors.groupingBy(e->e.getAge()));
        Set<Integer> key = map.keySet();
        for (Integer o : key) {
            System.out.println(o+"="+map.get(o));
        }

    }
}

结果:

21=[Employee{name='yyy', age=21, salary=8000.0, gender='男'}, Employee{name='张三', age=21, salary=20000.0, gender='男'}]
22=[Employee{name='zzz', age=22, salary=12000.0, gender='男'}, Employee{name='李四', age=22, salary=22000.0, gender='妖'}]
30=[Employee{name='xxx', age=30, salary=10000.0, gender='男'}]

2、partitioningBy():按照指定的条件分区

返回一个Map,键为分区条件,值为一个单列集合Collection,表示满足分区条件的元素

按工资是否大于12000将员工分为两部分:

Map<Boolean,List<Employee>> map1 = employees.stream()
    .collect(Collectors.partitioningBy(e->e.getSalary()>12000));

Set<Boolean> key1 = map1.keySet();
for (Boolean o : key1) {
    System.out.println(o+"="+map1.get(o));
}
false=[Employee{name='xxx', age=30, salary=10000.0, gender='男'}, Employee{name='yyy', age=21, salary=8000.0, gender='男'}, Employee{name='zzz', age=22, salary=12000.0, gender='男'}]
true=[Employee{name='张三', age=21, salary=20000.0, gender='男'}, Employee{name='李四', age=22, salary=22000.0, gender='妖'}]

 Collectors类还有很多强大的功能,后续博客可能会专门介绍(如果有时间的话)。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值