目录
本章会介绍集合类的一些更高级的主题,比如流中元素的顺序,以及一些有用的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提供了收集多种容器类型的方式,同时允许用户自定义收集器