高级集合类和收集器

目录

1.方法引用

2.收集器

2.1 转换成其他集合

2.2 转换成值

2.3 数据分块

2.4 数据分组

2.5 字符串

2.6 组合收集器

3.Map

4.总结


本章会介绍集合类的一些更高级的主题,比如流中元素的顺序,以及一些有用的API。

1.方法引用

Lambda表达式有一个常见的用法是经常调用参数,比如我们想得到学生的名字,Lambda的表达式如下:

student -> student.getName()

这种用法如此普遍,因此Java 8为其提供了一个简写的语法,叫作方法引用,帮助程序员重用已有的方法,代码如下:

Student::getName

标准语法为Classname::methodName,虽然这是一个方法,但是不需要再后面增加括号,因为这里并不是调用该方法。我们只是提供了和Lambda表达式等价的一种结构,在需要时才会调用。凡是使用Lambda表达式的地方,就可以使用方法引用。构造函数也有同样的缩写形式,如果我们想使用Lambda表达式创建一个Student对象,可以使用如下代码:

(name,age) -> new Student(name,age)

使用方法引用,上面的代码可以简写成:

Student::new

Student::new 立刻告诉程序员这是在创建一个Student对象,程序员无需看完整行代码就能明白代码的意图。需要注意的是方法引用不需要指定参数,因为它会自动支持多个参数,前提是选对了正确的函数接口。还可以使用下面的方式创建数组,例如下面的代码创建了一个字符串型的数组:

String[]::new

2.收集器

我们使用过collect(toList()),在流中生成列表,但是有时人们还是希望从流中生成其他值,比如Map或Set,甚至生成一个自定义的类。收集器就是一种通用的、从流生成复杂值的结构,只要将它传给collect方法,所有的流就都可以使用它了。

2.1 转换成其他集合

通常情况下,创建集合时需要调用适当的构造函数指明集合的具体类型,比如:

List<Student> students = new ArrayList();

但是调用toList或者toSet方法时,不需要指定具体的类型,Stream类库在背后自动为你挑选出了合适的类型。可能还会有这样的情况,我们希望使用一个特定的集合收集值。比如,我们希望使用TreeSet,而不是由框架在背后自动为你指定一种类型的Set。此时就可以使用toCollection,它接受一个函数作为参数,来创建集合。

stream.collect(toCollection(TreeSet::new))

2.2 转换成值

我们还可以利用收集器让流生成一个值,maxBy和minBy允许用户按某种特定的顺序生成一个值(最大值和最小值)。我们定义班级类:

package com.martin.learn.java8.domain;

import java.util.List;
import java.util.stream.Stream;


public class ClassDTO {
    private int num;
    private List<StudentDTO> studentDTOS;

    public int getNum() {
        return num;
    }

    public void setNum(int num) {
        this.num = num;
    }

    public Stream<StudentDTO> getStudentDTOS() {
        return studentDTOS.stream();
    }

    public List<StudentDTO> getStudentList() {
        return studentDTOS;
    }

    public void setStudentDTOS(List<StudentDTO> studentDTOS) {
        this.studentDTOS = studentDTOS;
    }
}

求班级人数的最大值、最小值和平均值的实现代码如下:

package com.martin.learn.java8;

import com.martin.learn.java8.domain.ClassDTO;
import com.martin.learn.java8.domain.StudentDTO;

import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
import java.util.function.Function;
import java.util.stream.Stream;

import static java.util.Comparator.comparing;
import static java.util.stream.Collectors.averagingInt;
import static java.util.stream.Collectors.maxBy;
import static java.util.stream.Collectors.minBy;

/**
 * @date: 2019/1/27 20:43
 * @description:
 */
public class Test {
    private static List<ClassDTO> classDTOS = new ArrayList<>();

    static {
        ClassDTO clazz1 = new ClassDTO();
        clazz1.setNum(1);
        List<StudentDTO> studentDTOS = new ArrayList<>();
        clazz1.setStudentDTOS(studentDTOS);
        studentDTOS.add(new StudentDTO(1L, "张三", 11));
        studentDTOS.add(new StudentDTO(2L, "李四", 18));
        studentDTOS.add(new StudentDTO(3L, "王五", 20));

        ClassDTO clazz2 = new ClassDTO();
        clazz2.setNum(2);
        List<StudentDTO> students = new ArrayList<>();
        clazz2.setStudentDTOS(students);
        students.add(new StudentDTO(4L, "刘六", 14));
        students.add(new StudentDTO(5L, "陈七", 16));

        classDTOS.add(clazz1);
        classDTOS.add(clazz2);
    }

    /**
     * 人数最多的班级
     *
     * @param classDTOStream
     * @return
     */
    public Optional<ClassDTO> biggestClass(Stream<ClassDTO> classDTOStream) {
        Function<ClassDTO, Long> getCount = classDTO -> classDTO.getStudentDTOS().count();
        return classDTOStream.collect(maxBy(comparing(getCount)));
    }

    /**
     * 人数最少的班级
     *
     * @param classDTOStream
     * @return
     */
    public Optional<ClassDTO> smallestClass(Stream<ClassDTO> classDTOStream) {
        Function<ClassDTO, Long> getCount = classDTO -> classDTO.getStudentDTOS().count();
        return classDTOStream.collect(minBy(comparing(getCount)));
    }

    /**
     * 平均人数
     *
     * @param classDTOS
     * @return
     */
    public double avgStudent(List<ClassDTO> classDTOS) {
        return classDTOS.stream().collect(averagingInt(classDTO -> classDTO.getStudentList().size()));
    }

    public static void main(String[] args) {
        Test test = new Test();
        ClassDTO biggestClass = test.biggestClass(classDTOS.stream()).get();
        System.out.println(biggestClass.getNum());

        ClassDTO smallestClass = test.smallestClass(classDTOS.stream()).get();
        System.out.println(smallestClass.getNum());

        double avgCount = test.avgStudent(classDTOS);
        System.out.println(avgCount);
    }
}

2.3 数据分块

另外一个常用的流操作是将其分解成两个集合。假设有一个学生组成的流,你可能希望将其组成两个部分,一部分是年龄大于15岁的,一部分是年龄小于15岁的,我们可能使用两次过滤操作分别过滤出以上的两种学生类型。但是这样操作起来有问题,为了执行两次过滤操作需要有两个流,代码也会变得冗余复杂。

收集器partitioningBy接受一个流,将其分成两部分,它使用Predicate对象判断一个元素应该属于哪个部分,并根据布尔值的返回一个Map到列表。因此,对于true List中的元素,Predicate返回true;对其他List中的元素,Predicate返回false。

实现的代码如下:

    /**
     * 将学生组成的流分成年龄大于15和小于15的两个部分
     *
     * @param studentDTOs
     * @return
     */
    public Map<Boolean, List<StudentDTO>> classifyStudent(Stream<StudentDTO> studentDTOs) {
        return studentDTOs.collect(partitioningBy(studentDTO -> studentDTO.getAge() >= 15));

    }

2.4 数据分组

数据分组是一种更自然的分隔数据操作,与将数据分成true和false两部分不同,可以使用任意值对数据进行分组。

   /**
     * 根据地域对学生进行分割
     *
     * @param studentDTOs
     * @return
     */
    public Map<String, List<StudentDTO>> classifyByArea(Stream<StudentDTO> studentDTOs) {
        return studentDTOs.collect(groupingBy(student -> student.getArea()));
    }

实现的示意图如下:

2.5 字符串

很多时候,收集流中的数据都是为了在最后生成一个字符串,比如我们希望获取所有学生的名字,可以使用如下的操作:

 /**
     * 获得所有学生的名字
     *
     * @param studentDTOs
     * @return ["张三","李四"....]
     */
    public String getNames(Stream<StudentDTO> studentDTOs) {
        return studentDTOs.map(StudentDTO::getName).collect(Collectors.joining(",", "[", "]"));
    }

这里使用map操作提取出全部的学生的名字,然后使用Collectors.joining收集流中的值,该方法可以方便的从一个流得到一个字符串,允许用户使用分隔符、前缀和后缀。

2.6 组合收集器

虽然现在的收集器已经很强大了,但是如果将他们组合起来,会变得更加强大。比如我们要计算各个地区的学生数,可以先将学生按照地区分组,然后使用counting分别计算学生数。

   /**
     * 按照地区计算学生数
     *
     * @param stream
     * @return
     */
    public Map<String, Long> numberOfArea(Stream<StudentDTO> stream) {
        return stream.collect(groupingBy(StudentDTO::getArea, counting()));
    }

或者我们还可能计算出各个地区的学生的姓名,这时我们可以使用mapping收集器,mapping允许在收集器的容器上执行类似map的操作,但是需要指明使用什么样的集合类存储结果,比如toList。


    /**
     * 计算每个地区的学生的姓名
     *
     * @param stream
     * @return
     */
    public Map<String, List<String>> namesOfArea(Stream<StudentDTO> stream) {
        return stream.collect(groupingBy(StudentDTO::getArea, mapping(StudentDTO::getName, toList())));
    }

3.Map

Lambda表达式的引入也推动了一些新的方法加入集合类,我们来看看Map类的一些变化。假设我们使用Map<String,Student> studentCache定义缓存,我们需要使用费时的数据库操作查询学生信息。代码实现如下:

    public StudentDTO getStudent(String name) {
        StudentDTO student = maps.get(name);
        if (student == null) {
            student = readStudentFromDB(name);
            maps.put(name, studentDTO);
        }
        return student;
    }

Java 8引入了一个新方法computeIfAbsent(compute),该方法接受一个Lambda表达式,值不存在时候就使用该Lambda表达式计算新值。实现的代码如下:

  public StudentDTO getStudentByLambda(String name) {
      return maps.computeIfAbsent(name, value -> readStudentFromDB(value));
  }

在工作中,我们可能尝试对Map进行迭代,过去是使用value方法返回一个值的集合,然后在集合上迭代,这样的代码不宜读。Java 8为Map接口新增了一个forEach方法,该方法接受一个BiConsumer对象为参数,通过内部迭代编写出易于阅读的代码。

  private void getAllValues() {
        maps.forEach((key, value) -> {
            System.out.println(key + ":" + value);
        });
    }

4.总结

  • 方法引用是一种引用方法的轻量级语法,形如:ClassName::methodName
  • 收集器可用来计算流的最终值,是reduce方法的模拟
  • Java 8提供了收集多种容器类型的方式,同时允许用户自定义收集器

 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值