lambda就是数学中的“λ”的读音,lambda表达式是基于λ演算而得名的,因为lambda抽象(lambda abstraction)表示一个匿名的函数,于是开发语言也将lambda表达式用来表示匿名函数,也就是没有函数名字的函数。C#、Python,甚至是C++都有lambda表达式语法。为了提高开发者的开发效率,并照顾“跨语言”开发者的开发习惯,Java语言也加入了lambda表达式。流处理是Java程序中一种重要的数据处理手段,它用少量的代码便可以执行复杂的数据过滤、映射、查找和收集等功能。
本章知识架构及重难点如下:
14.1 lambda表达式
14.1.1 lambda表达式简介
lambda表达式可以用非常少的代码实现抽象方法。lambda表达式不能独立执行,因此必须实现函数式接口,并且会返回一个函数式接口的对象。lambda表达式的语法非常特殊,语法格式如下:
() -> 结果表达式
参数 -> 结果表达式
(参数1,参数2....,参数n)-> 结果表达式
1行实现无参方法,方法体是操作符右侧代码块。
2行实现只有一个参数的方法,方法体是操作符右侧代码块。
3行实现多参数的方法,方法体是操作符右侧代码块。
lambda表达式的语法非常抽象,并且有着非常强大的自动化功能,如自动识别泛型、自动数据类型转换等,这会让初学者很难掌握。如果将lambda表达式的功能归纳总结,可以将lambda表达式语法用如下方式理解:
() (代码块》
这个方法 按照 这样的代码来实现
简单总结:操作符左侧的是方法参数,操作符右侧的是方法体。
误区警示
“->”符号是由英文状态下的“-”和“>”组成的,符号之间没有空格。
14.1.2 lambda表达式实现函数式接口
lambda表达式可以实现函数式接口,本节将讲解函数式接口概念以及用lambda表达式实现不同类型的函数式接口。
1.函数式接口
函数式接口指的是仅包含一个抽象方法的接口,接口中的方法简单明了地说明了接口的用途,如线程接口Runnable、动作事件监听接口ActionListener等。开发者可以创建自定义的函数式接口,例如:
interface MyInterface (
void method();
】
如果接口中包含一个以上的抽象方法,则不符合函数式接口的规范,这样的接口不能用lambda表达式创建匿名对象。本章内容中所有被lambda表达式实现的接口均为函数式接口。
2.lambda表达式实现无参抽象方法
很多函数式接口的抽放方法是无参数的,如线程接口Runnable接口只有一个run()方法,这样的无参抽象方法在lambda表达式中使用“( )”表示。
【例14.1】使用lambda表达式实现打招呼接口
创建函数式接口和测试类,接口抽象方法为无参方法并返回一个字符串。使用lambda表达式实现接口,让方法可以输出当前日期。
本实例直接在lambda表达式中创建SayHiInterface接口对象,并指定了一个字符串作为接口方法的返回值。最后在输出语句中,pi对象就是lambda表达式创建出的对象,当pi调用接口方法时就输出了lambda表达式指定的字符串。
3.lambda表达式实现有参抽象方法
抽象方法中有一个或多个参数的函数式接口也是很常见的,lambda表达式中可以用“(a1,a2,a3)”的方法表示有参抽象方法,圆括号里标识符对应抽象方法的参数。如果抽象方法中只有一个参数,lambda表达式则可以省略圆括号。
【例14.2】使用lambda表达式做加法计算
创建函数式接口和测试类,接口抽象方法有两个参数并返回一个int型结果。使用lambda表达式实现接口,让方法可以计算两个整数的和,具体代码如下:
在这个实例中,函数式接口的抽象方法有两个参数,lambda表达式的圆括号内也写了两个参数对应的抽象方法。这里有一个点要注意,lambda表达式中的参数不需要与抽象方法的参数名称相同,但顺序必须相同。
4.lambda表达式使用代码块
当函数式接口的抽象方法需要实现复杂逻辑而不是返回一个简单的表达式的话,就需要在lambda表达式中使用代码块。lambda表达式会自动判断返回值类型是否符合抽象方法的定义。
【例14.3】使用lambda表达式为考试成绩分类
创建函数式接口和测试类,接口抽象方法有一个整型参数表示成绩,输入成绩后,返回成绩的字符串评语。在lambda表达式中实现成绩判断。
14.1.3 lambda表达式调用外部变量
lambda表达式除了可以调用定义好的参数,还可以调用表达式以外的变量。但是,这些外部的变量有些可以被更改,有些则不能。例如,lambda表达式无法更改局部变量的值,但是却可以更改外部类的成员变量(也可以叫作类属性)的值。
1.lambda表达式无法更改局部变量
局部变量在lambda表达式中默认被定义为final(静态)的,也就是说,lambda表达式只能调用局部变量,却不能改变其值。
【例14.4】使用lambda表达式修改局部变量
创建函数式接口和测试类,在测试类的main()方法中创建局部变量和接口对象,接口对象使用lambda表达式实现,并在lambda表达式中尝试更改局部变量值。
2.lambda表达式可以更改类成员变量
类成员变量是在lambda表达式中不是被final修饰的,所以lambda表达式可以改变其值。
【例14.5】使用lambda表达式修改类成员变量
创建函数式接口和测试类,在测试类中创建成员属性value和成员方法action()。在action()方法中使用lambda表达式创建接口对象,并在lambda表达式中修改value的值。运行程序,查看value值是否发生变化。
从这个结果可以看出以下几点:
ambda表达式可以调用并修改类成员变量的值。
ambda表达式只是描述了抽象方法是如何实现的,在抽象方法没有被调用前,lambda表达式中的代码并没有被执行,所以运行抽象方法之前类成员变量的值不会发生变化。
要抽象方法被调用,就会执行lambda表达式中的代码,类成员变量的值就会被修改。
14.1.4 lambda表达式与异常处理
很多接口的抽象方法为了保证程序的安全性,会在定义时就抛出异常。但是lambda表达式中并没有抛出异常的语法,这是因为lambda表达式会默认抛出抽象方法原有的异常,当此方法被调用时则需要进行异常处理。
【例14.6】使用lambda表达式实现防沉迷接口
创建自定义异常UnderAgeException,当发现用户是未成年人时进入此异常处理。创建函数式接口,在抽象方法中抛出UnderAgeException异常,使用lambda表达式实现此接口,并让接口对象执行抽象方法。
从这个实例中可以看出,即使lambda表达式没有定义异常,原抽象方法抛出的异常仍然是存在的,当接口对象执行此方法时会被强制要求进行异常处理。
14.2.1 引用静态方法
引用静态方法的语法如下:
类名::静态方法名
这个语法中出现了一个新的操作符“::”,这是由两个英文冒号组成的操作符,冒号之间没有空格。这个操作符左边表示方法所属的类名,右边是方法名。需要注意的是,这个语法中方法名是没有圆括号的。
【例14.7】使用lambda表达式引用静态方法
创建函数式接口和测试类,在接口中定义抽象方法method(),在测试类中编写一个可以用来实现抽象方法的静态方法——add()方法。在main()方法中创建接口对象,并使用引用静态方法的语法让接口对象的抽象方法按照测试类的add()方法来实现。
从这个结果可以看出,接口方法得出的结果正是按照add()方法中的逻辑计算出来的。
14.2.2 引用成员方法
引用成员方法的语法如下:
对象名::成员方法名
与引用静态方法语法不同,这里操作符左侧的必须是一个对象名,而不是类名。这种语法也可以达到抽象方法按照类成员方法逻辑来实现的目的。
【例14.8】使用lambda表达式引用成员方法
创建函数式接口和测试类,在接口中定义抽象方法method(),在测试类中编写一个可以用来实现抽象方法的成员方法——format()方法。在main()方法中创建接口对象,并使用引用成员方法的语法让接口对象的抽象方法按照测试类的format()方法来实现。
从这个结果可以看出,抽象方法的结果是按照类成员方法的逻辑计算出来的。
14.2.3 引用带泛型的方法
泛型是Java开发经常使用到的功能,“::”操作符支持引用带泛型的方法。除方法外,“::”操作符也支持引用带泛型的类。
【例14.9】使用lambda表达式引用带泛型的方法
创建函数式接口和测试类,在接口定义时添加泛型T,并且在抽象方法参数中使用此泛型。在测试类中创建带有泛型的静态方法,同样在方法参数中使用此泛型。抽象方法和类静态方法参数类型保持一致。类静态方法会利用哈希集合不保存重复数据的原理,实现过滤数组中的重复数据。
注意
与其他使用泛型的场景一样,要保证代码前后泛型一致,否则会发生编译错误。
14.2.4 引用构造方法
lambda表达式有3种引用构造方法的语法,分别是引用无参构造方法、引用有参构造方法和引用数组构造方法,下面分别进行讲解。
1.引用无参构造方法
引用构造方法的语法如下:
类名::new
因为构造方法与类名相同,如果操作符左右都写类名,会让操作符误以为是在引用与类名相同的静态方法,这样会导致程序出现Bug,所以引用构造方法的语法使用了new关键字。操作符右侧的写new关键字,表示引用构造方法。
这个语法有一点要注意:new关键字之后没有圆括号,也没有参数的定义。如果类中既有无参构造方法,又有有参构造方法,使用引用构造方法语法后,究竟哪一个构造方法被引用了呢?引用哪个构造方法是由函数式接口决定的,“::”操作符会返回与抽象方法的参数结构相同的构造方法。如果找不到参数接口相同的构造方法,则会发生编译错误。
【例14.10】使用lambda表达式引用无参构造方法
创建函数式接口和测试类。测试类中创建一个无参构造方法和一个有参构造方法。接口抽象方法返回值为测试类对象,并且方法无参数。使用引用构造方法语法创建接口对象,调用接口对象方法创建测试类对象,查看输出结果。
从这个结果可以看出,如果接口方法没有参数,调用的就是无参的构造方法。
2.引用有参构造方法
引用有参构造方法的语法与引用无参构造方法一样。区别就是函数式接口的抽象方法是有参数的。
【例14.11】使用lambda表达式引用有参数的构造方法
创建函数式接口和测试类。测试类创建一个无参构造方法和一个有参构造方法。接口抽象方法返回值为测试类对象,并且方法参数结构要和测试类有参构造方法参数一致。使用引用构造方法语法创建接口对象,调用接口对象方法创建测试类对象,查看输出结果。
从这个结果可以看出,无参构造方法没有被调用,接口方法使用的就是有参数的构造方法。
3.引用数组构造方法
Java开发可能出现这样一种特殊场景:把数组类型当作泛型。如果方法返回值是泛型,在这种特殊场景下,方法就应该返回一个数组类型的结果。如果要求抽象方法既引用构造方法,又要返回数组类型结果,这种场景下抽象方法的参数就有了另外一个含义:数组个数。抽象方法的参数可以决定返回的数组长度,但数组中的元素并不是有值的,还需要再次赋值。引用数组构造方法的语法也会有所不同,语法如下:
类名[]::new
【例14.12】使用lambda表达式引用数组的构造方法
创建函数式接口和测试类。定义接口时创建一个泛型T,同时T作为抽象方法的返回值。抽象方法定义一个整型参数。创建接口对象时,将测试类数组作为泛型,并引用数组构造方法。通过接口方法创建测试类数组,再分别为每个数组元素赋值。
实例中不能给array[3]赋值,因为接口方法的参数是3,创建的数组只包含3个元素。
14.2.5 Fuction接口
在此之前的所有实例中,想要使用lambda表达式都需要先创建或调用已有的函数式接口,但java.util.function包已经提供了很多预定义函数式接口,就是没有实现任何功能,仅用来封装lambda表达式的对象。该包中最常用的接口是Function<T,R>接口,这个接口有以下两个泛型:
:被操作的类型,可以理解为方法参数类型。
:操作结果类型,可以理解为方法的返回类型。
Function接口是函数式接口,所以只有一个抽象方法,但是Function接口还提供了3个已实现的方法以方便开发者对函数逻辑进行更深层的处理。Function接口方法如表14.1所示。
表14.1 Function接口方法
【例14.13】使用lambda表达式拼接IP地址
创建Function接口对象,使用lambda表达式实现拼接IP地址的功能,具体代码如下:
流处理有点类似数据库的SQL语句,可以执行非常复杂的过滤、映射、查找和收集功能,并且代码量很少。唯一的缺点是代码可读性不高,如果开发者基础不好,可能会看不懂流API所表达的含义。
为了能让读者更好地理解流API的处理过程和结果,本节先创建一个公共类——Employee员工类。员工类包含员工的姓名、年龄、薪资、性别和部门属性,并给这些属性提供了getter方法。重写toString()可以方便查看员工对象的所有信息。公共类提供了一个静态方法getEmpList(),这个方法已经创建好了一些员工对象,然后将这些员工封装成一个集合并返回。本节将重点对这些员工数据进行流处理。
员工集合的详细数据如表14.2所示。
表14.2 公共类提供已定义好的员工集合数据
【例14.14】创建员工类,并按照表14.2创建初始化数据
创建Employee类,在类中创建姓名、年龄、工资、性别和部门属性,创建对应这些属性的构造方法和Getter方法。最后将初始化的员工数据放到一个ArrayList列表中。
14.3.1 Stream接口简介
流处理的接口都定义在java.uil.stream包下。BaseStream接口是最基础的接口,但最常用的是BaseStream接口的一个子接口——Stream接口,基本上绝大多数的流处理都是在Stream接口上实现的。
Stream接口是泛型接口,所以流中操作的元素可以是任何类的对象。Stream接口的常用方法如表14.3所示。
表14.3 Stream接口的常用方法
说明
表14.3中最后一列“类型”中有两个值:中间操作和终端操作。中间操作类型的方法会生成一个新的流对象,被操作的流对象仍然可以执行其他操作;终端操作会消费流,操作结束之后,被操作的流对象就不能再次执行其他操作了。这是两者的最大区别。
Collection接口新增两个可以获取流对象的方法。第一个方法最常用,可以获取集合的顺序流,方法如下:
Stream<E> stream();
第二个方法可以获取集合的并行流,方法如下:
Stream<E> parallelstream();
因为所有集合类都是Collection接口的子类,如ArrayList类、HashSet类等,所以这些类都可以进行流处理。例如:
List<Integer> list =new ArrayList<Integer>(); //创建集合
Stream<Integer> s = list.stream(); //获取集合流对象
14.3.2 Optional类
Optional类像是一个容器,可以保存任何对象,并且针对NullPointerException空指针异常做了优化,保证Optional类保存的值不会是null。因此,Optional类是针对“对象可能是null也可能不是null”的场景为开发者提供了优质的解决方案,减少了烦琐的异常处理。
Optional类是用final修饰的,所以不能有子类。Optional类是带有泛型的类,所以该类可以保存任何对象的值。
从Optional类的声明代码中就可以看出这些特性,JDK中的部分代码如下:
public final class Optional<T>(
private final T value;
//省略其他代码
Optional类中有一个叫作value的成员属性,这个属性就是用来保存具体值的。value是用泛型T修饰的,并且还用了final修饰,这表示一个Optional对象只能保存一个值。
Optional类提供了很多封装、校验和获取值的方法,这些方法如表14.4所示。
表14.4 Optional类提供的常用方法
说明
除Optional类外,还可以使用OptionalDouble类、OptionalInt类和OptionalLong类这3个类,开发者可以根据不同的应用场景灵活选择。
【例14.15】使用Optional类创建“空”对象
创建一个Optional对象,并赋予一个字符串类型的值,然后判断此对象的值是否为空;再使用empty()方法创建一个“空值”的Optional对象,然后判断此对象的值是否为空。
14.3.3 Collectors类
Collectors类为收集器类,该类实现了java.util.Collector接口,可以将Stream流对象进行各种各样的封装、归集、分组等操作。同时,Collectors类还提供了很多实用的数据加工方法,如数据统计计算等。Collectors类的常用方法如表14.5所示。
表14.5 Collectors类的常用方法
Collectors类的具体用法将在后面做重点讲解。
14.3.4 数据过滤
数据过滤就是在杂乱的数据中筛选出需要的数据,类似SQL语句中的WHERE关键字,给出一定的条件,将符合条件的数据过滤并展示出来。
1.filter()方法
filter()方法是Stream接口提供的过滤方法。该方法可以将lambda表达式作为参数,然后按照lambda表达式的逻辑过滤流中的元素。过滤出想要的流元素后,还需使用Stream提供的collect()方法按照指定方法重新封装。
【例14.16】输出1~10中的所有奇数
将1~10的数字放到一个ArrayList列表中,调用该列表的Stream对象的filter()方法,方法参数为过滤奇数的lambda表达式。查看方法执行完毕后Stream对象返回的结果。
这个实例把“获取流”“过滤流”“封装流”3个部分操作分开编写,是为了方便读者学习理解,通常为了代码简洁,3部分操作可以写在一行代码中,例如:
List<Integer> result = list.stream().filter(n -> n % 2 == 1).collect(Collectors.toList());
这种写法也可以避免终端操作造成的“流被消费掉”的问题,因为每次被操作的流都是从集合中重新获取的。
说明
代码在Eclipse中出现黄色警告,这是printeach(String message, List list)方法中list参数没有指定泛型引起的。但是,这也体现出lambda表达式可以自动识别数据类型的优点。读者可以忽略此警告。
例14.16中演示的集合元素是数字类型,集合能保存的不止是数字,下面这个实例将演示如何利用过滤器以对象属性为条件过滤元素。
【例14.17】找出年龄大于30的员工
本实例使用了例14.14定义的Employee类,在获取员工集合后,将年龄大于30的员工过滤出来。如果将员工集合返回的流对象泛型定义为<Employee>,就可以直接在lambda表达式中使用Employee类的方法。
通过这个结果可以看出,年龄没超过30的员工都被过滤掉了。通过类的一个属性,就可以将符合条件的类对象完整地获取到,员工的姓名、性别等属性都可以打印出来。
2.distinct()方法
distinct()方法是Stream接口提供的过滤方法。该方法可以去除流中的重复元素,效果与SQL语句中的DISTINCT关键字一样。
【例14.18】去除List集合中的重复数字
创建一个List集合,保存一些数字(包含重复数字),获取集合的流对象,使用distinct()方法将重复的数字去掉。
因为distinct()方法属于中间操作,所以可以配合filter()方法一起使用。
3.limit()方法
limit()方法是Stream接口提供的方法,该方法可以获取流中前N个元素。
【例14.19】找出所有员工列表中的前两位女员工
本实例使用了例14.14定义的Employee类,在获取员工集合后,取出所有员工中的前两位女员工,具体代码如下:
4.skip()方法
skip()方法是Stream接口提供的方法,该方法可以忽略流中的前N个元素。
【例14.20】取出所有男员工,并忽略前两位男员工
本实例使用了例14.14定义的Employee类,在获取员工集合后,取出所有男员工,并忽略前两位男员工。
14.3.5 数据映射
数据的映射和过滤概念不同:过滤是在流中找到符合条件的元素,映射是在流中获得具体的数据。
Stream接口提供了map()方法用来实现数据映射,map()方法会按照参数中的函数逻辑获取新的流对象,新的流对象中元素类型可能与旧流对象元素类型不相同。
【例14.21】获取开发部所有员工的名单
本实例使用了例14.14定义的Employee类,在获取员工集合后,先过滤出开发部的员工,再引用员工类的getName()方法作为map()方法的映射参数,这样就获取到开发部员工名单。
结果输出了开发部两位员工的名字,但没有输出这两位员工的其他信息,这个就是映射的结果。
除了可以映射出员工名单,还可以对映射数据进行加工处理。例如,统计销售部一个月的薪资总额。因为涉及数字计算,所以需要让Stream对象转为可以进行数学运算的数字流。因为薪资类型是double类型,所以应该调用mapToDouble()方法进行转换。
【例14.22】计算销售部一个月的薪资总额
本实例使用了例14.14定义的Employee类,在获取员工集合后,先过滤出销售部的员工,再引用员工类的getSalary()方法作为mapToDouble()的映射参数,获取到DoubleStream对象后,调用该对象的sum()方法,就可以计算出销售部的薪资总和。具体代码如下:
除DoubleStream类外,java.util.stream包还提供了IntStream类和LongStream类以应对不同的计算场景。
14.3.6 数据查找
本节所讲的数据查找并不是在流中获取数据(这属于数据过滤),而是判断流中是否有符合条件的数据,查找的结果是一个boolean值或一个Optional类的对象。本节将讲解allMatch()、anyMatch()、noneMatch()和findFirst()这4个方法。
1.allMatch()方法
allMatch()方法是Stream接口提供的方法,该方法会判断流中的元素是否全部符合某一条件,返回结果是boolean值。如果所有元素都符合条件则返回true,否则返回false。
【例14.23】检查所有员工是否都大于25岁(实例位置:资源包\TM\sl\14\23)
本例使用了例14.14定义的Employee类,在获取员工集合后,使用allMatch()方法检查公司所有员工的年龄是否都大于25岁,具体代码如下:
最后得出的结果是false,因为公司里的“小马”和“小王”的年龄都只有21岁,而“小刘”的年龄只有24岁,不满足“所有员工都大于25”这个条件。
2.anyMatch()方法
anyMatch()方法是Stream接口提供的方法,该方法会判断流中的元素是否有符合某一条件,只要有一个元素符合条件就返回true,如果没有元素符合条件才会返回false。
【例14.24】检查是否有年龄大于40岁的员工
本例使用了例14.14定义的Employee类,在获取员工集合后,使用anyMatch()方法检查公司里是否有年龄在40岁或40岁以上的员工,具体代码如下:
运行结果为true,因为公司里的“老张”正好40岁,符合“有年龄在40岁或以上的员工”的条件。
3.noneMatch()方法
noneMatch()方法是Stream接口提供的方法,该方法会判断流中的所有元素是否都不符合某一条件。这个方法的逻辑和allMatch()方法正好相反。
【例14.25】检查公司是否不存在薪资小于2000元的员工
本例使用了例14.14定义的Employee类,在获取员工集合后,使用noneMatch()方法检查公司里是否没有薪资小于2000的员工,具体代码如下:
公司最低薪资是3000,也就是说没有员工的薪资会小于2000,所以结果为true。
4.findFirst()方法
findFirst()方法是Stream接口提供的方法,这个方法会返回符合条件的第一个元素。
【例14.26】找出第一个年龄等于21岁的员工(实例位置:资源包\TM\sl\14\26)
本例使用了例14.14定义的Employee类,在获取员工集合后,首先将年龄为21岁的员工过滤出来,然后使用findFirst()方法获取第一个员工,具体代码如下:
公司里有两个21岁的员工,一个是“小马”,一个是“小王”。因为“小马”在集合中的位置靠前,所以findFirst()方法获取的是“小马”。
注意
这个方法的返回值不是boolean值,而是一个Optional对象。
14.3.7 数据收集
数据收集可以理解为高级的“数据过滤+数据映射”,是对数据的深加工。本节将讲解两种实用场景:数据统计和数据分组。
1.数据统计
数据统计不仅可以筛选出特殊元素,还可以对元素的属性进行统计计算。这种复杂的统计操作不是由Stream实现的,而是由Collectors收集器类实现的,收集器提供了非常丰富的API,有着强大的数据挖掘能力。
【例14.27】统计公司各项数据,打印成报表
本例使用了例14.14定义的Employee类,在获取员工集合后,不使用filter()方法对元素进行过滤,而直接使用Stream接口和Collectors类的方法对公司各项数据进行统计,具体代码如下:
这是一个复杂的例子,里面涉及人数统计、比较年龄取最大年龄和最小年龄员工、统计薪资总和、求平均薪资等。最后两项比较特殊,一个是获取数字统计类DoubleSummaryStatistics类,这个类本身就包含了个数、总和、均值、最大值、最小值这5个属性;另一个在获取员工映射名单时又加工了一下,将所有名字拼接成了一个字符串,并在名字之间用逗号分隔。
2.数据分组
数据分组就是将流中元素按照指定的条件分开保存,类似SQL语言中的“GROUP BY”关键字。分组之后的数据会按照不同的标签分别保存成一个集合,然后按照“键-值”关系封装在Map对象中。
数据分组有一级分组和多级分组两种场景,首先先来介绍一级分组。
一级分组,就是将所有数据按照一个条件进行归类。例如,学校有100个学生,这些学生分布在3个年级中。学生按照年级分成了3组,然后就不再细分了,这就属于一级分组。
Collectors类提供的groupingBy()方法就是用来进行分组的方法,方法参数是一个Function接口对象,收集器会按照指定的函数规则对数据进行分组。
【例14.28】将所有员工按照部门分组
本例使用了例14.14定义的Employee类,在获取员工集合后,创建Function接口对象f,f引用Employee员工类的getDept()方法获取部门名称。然后,流的收集器类按照f的规则进行分组,Stream对象将分组结果赋值给一个Map对象,Map对象将会以“key:部门,value:员工List”的方式保存数据。具体代码如下:
组规则是一个函数,这个函数是由Collectors收集器类调用的,而不是Stream流对象。
ap<K,List<T>>有两个泛型,第一个泛型是组的类型,第二个是组内的元素集合类型。实例中按照部门名称分组,所以K的类型是String类型;部门内的元素是员工集合,所以List集合泛型T的类型就应该是Employee类型。
介绍完一级分组后,再介绍一下复杂的多级分组。
一级分组是按照一个条件进行分组,那么多级分组就是按照多个条件进行分组。还是用学校举例,学校有100个学生,这些学生分布在3个年级中,这是一级分组,但每个年级还有若干个班级,学生们分到不同年级之后又分到不同的班,这就是二级分组。如果学生再按男女分组,就变成了三级分组。元素按照两个以上的条件进行分组,就是多级分组。
Collectors类提供的groupingBy()方法还提供了一个重载形式:
groupingBy(Function<? super T,? extends K> classifier, Collector<? super T,A,D> downstream)
【例14.29】将所有员工先按照部门分组,再按照性别分组
在一级分组实例的基础上,首先创建Function接口对象deptFunc用于引用获取部门的方法,再创建Function接口对象sexFunc用于引用获取性别的方法(这两个对象将作为一级分组和二级分组的函数规则),最后将按照性别分组的Collectors.groupingBy(sexFunc)方法作为另一个groupingBy()方法的参数,按照部门进行分组,这样就实现了二级分组,具体代码如下:
这个结果先按照部门进行了分组,然后又对部门中的男女进行了二级分组。这个实例也有两个难点:
例中两个groupingBy()方法的参数不一样,一个是groupingBy(性别分组规则),另一个是groupingBy(部门分组规则, groupingBy(性别分组规则) )。
得的Map对象中,还嵌套了Map对象,它的结构是这样的:
Map<部门, Map<性别, List<员工>>>
从左数,第一个Map对象做了一级分组,第二个Map对象做了二级分组。