14.1 lambda 表达式
14.1.1 lambda 表达式简介
lambda 表达式可以用非常少的代码实现抽象方法。lambda 表达式不能独立执行,因此必须实现函数式接口,并且会返回一个函数式接口的对象。ilnbhda 表达的语法非常特殊,语法格式下:
()-> 结果表达式
参数->结果表达式
(参数1,参数2,...,参数n)结果表达式
- 第1行实现无参方法,单独写一对圆括号表示方法无参数,操作符右侧的结果表达式表示方法的返回值。
- 第2 行实现只有一个参数的方法,参数可以写在圆括号里,或者不写圆括号。
- 第3行实现多参数的方法,所有参数按顺序写在圆括号里,且圆括号不可以省略。
lambda 表达式也可以实现复杂方法,将操作符右侧的结果表达式换成代码块即可,语法格式加下:
()->{代码块}
参数 ->{代码块}
(参数1,参数2,...,参数n)->{ 代码块 }
- 第1行实现无参方法,方法体是操作符右侧代码块。
- 第2行实现只有一个参数的方法,方法体是操作符右侧代码块
- 第3行实现多参数的方法,方法体是操作符右侧代码块。
lambda 表达式的语法非常抽象,并且有着非常强大的自动化功能,如自动识别泛型、自动数据类型转换等,这会让初学者很难掌握。如果将 lambda 表达式的功能归纳总结,可以将 lambda 表达式语法用如下方式理解:
() -> {代码块}
这个方法 按照 这样的代码来实现
简单总结:操作符左侧的是方法参数,操作符右侧的是方法体
14.1.2 lambda 表达式实现函数式接口
lambda 表达式可以实现函数式接口,本节将讲解函数式接口概念以及用ambda表达式实现不同类型的函数式接口。
1.函数式接口
函数式接口指的是仅包含一个抽象方法的接口,接口中的方法简单明了地说明了接口的用途,如线程接口 Runnable、动作事件监听接口ActionListener 等。开发者可以创建自定义的函数式接口,例如:
interface Mylnterface {
void method();
}
如果接口中包含一个以上的抽象方法,则不符合函数式接口的规范,这样的接口不能用 lambda 表达式创建匿名对象。
2.lambda表达式实现无参抽象方法
很多函数式接口的抽放方法是无参数的,如线程接口 Runnable 接口只有一个run0方法,这样的无参抽象方法在 lambda表达式中使用“()”表示。
[例 14.1]
package sx;
interface SayHi{
String say();
}
public class NoParamDemo {
public static void main(String[] args) {
//通过匿名内部类补全方法体
SayHi sh1 = new SayHi() {
public String say() {
return "这里是匿名内部类";
}
};
System.out.println(sh1.say());
//通过Lambda表达式补全方法体
SayHi sh2 = () ->{
return "这里是Lambda语法";
};
System.out.println(sh2.say());
}
}
运行结果:
3.lambda 表达式实现有参抽象方法
抽象方法中有一个或多个参数的函数式接口也是很常见的,lambda 表达式中可以用“(al,a2,a3)'的方法表示有参抽象方法,圆括号里标识符对应抽象方法的参数。如果抽象方法中只有一个参数,lambda 表达式则可以省略圆括号。
[例14.2]
package sx;
interface AdditionInterface{ //加法接口
int add(int a,int b); //加法的抽象方法
}
public class ParamterDemo { //测试类
public static void main(String[] args) {
//使用匿名内部类补全方法
AdditionInterface np1= new AdditionInterface() {
public int add(int a,int b) {
return a+b;
}
};
System.out.println("匿名内部类的相加结果:"+np1.add(3,5));
//使用lambda表达式补全方法
AdditionInterface np=(a,b)-> a+b;
System.out.println("lambda表达式的相加结果:"+np.add(15,26)); //输出相加结果
}
}
运行结果:
4.lambda表达式使用代码块
当函数式接口的抽象方法需要实现复杂逻辑而不是返回一个简单的表达式的话,最需要在lmbd
表达式中使用代码块。lambda表达式会自动判断返回值类型是否符合抽象方法的定义。
[例14.3]
package sx;
interface CheckGrade{
String check(int grade); //查询成绩结果
}
public class Check {
public static void main(String[] args) {
CheckGrade g = (n)->{ //lambda表达式实现代码块
if(n>90 && n<=100) { //如果成绩在90~100
return"成绩为优"; //输出成绩为优
}else if(n>=80&& n<90) { //如果成绩在80~89
return"成绩为良"; //输出成绩为良
}else if(n>=60&& n<80) { //如果成绩在60~79
return"成绩为中"; //输出成绩为中
}else if(n>=0&& n<60) { //如果成绩小于60
return"成绩为差"; //输出成绩为差
}else { //其他数字不是有效成绩
return " 成绩无效"; //输出成绩无效
}
}; //不要丢掉lambda语句后的分号
System.out.println(g.check(89)); //输出查询结果
}
}
运行结果:
14.1.3 lambda 表达式调用外部变量
lambda 表达式除了可以调用定义好的参数,还可以调用表达式以外的变量。但是,这些外部的变量有些可以被更改,有些则不能。例如,lambda 表达式无法更改局部变量的值,但是却可以更改外部类的成员变量(也可以叫作类属性)的值。
1.lambda表达式无法更改局部变量
局部变量在 lambda表达式中默认被定义为 final(静态)的,也就是说,ambda 表达式只能调用局部变量,却不能改变其值。
[例14.4]
package sx;
interface VariableInterface1{ //测试接口
void method(); //测试方法
}
public class Variable { //测试类
public static void main(String[] args) {
int value = 100; //创建局部变量
VariableInterface1 v = ()->{ //实现测试接口
int num = value-90; //使用局部变量赋值
value = 12; //更改局部变量,此处会报错,无法通过编译
};
}
}
运行结果:
2.lambda表达式可以更改类成员变量
类成员变量是在lambda表达式中不是被final修饰的,所以lambda 表达式可以改变其值。
[例14.5]
package sx;
interface VariableInterface2{ //测试接口
void method(); //测试方法
}
public class Variable2 { //测试类
int value = 100; //创建类成员变量
public void action() { //创建类成员方法
VariableInterface1 v= ()->{ //实现测试接口
value = -12; //更改成员变量,没提示任何错误
};
System.out.println("运行接口方法前value="+value); //运行接口方法前先输出成员变量值
v.method(); //运行换口方法
System.out.println("运行接口方法后value="+value); //运行接口方法后再输出成员变量值
}
public static void main(String[] args) {
Variable2 demo = new Variable2(); //创建测试类对象
demo.action(); //执行测试类方法
}
}
运行结果:
从这个结果可以看出以下几点;
- lambda 表达式可以调用并修改类成员变量的值。
- lambda 表达式只是描述了抽象方法是如何实现的,在抽象方法没有被调用前,lambda表达式中的代码并没有被执行,所以运行抽象方法之前类成员变量的值不会发生变化。
- 只要抽象方法被调用,就会执行 lambda 表达式中的代码,类成员变量的值就会被修改。
14.1.4 lambda 表达式与异常处理
很多接口的抽象方法为了保证程序的安全性,会在定义时就抛出异常。但是 lambda 表达式中并没有抛出异常的语法,这是因为 lambda 表达式会默认抛出的抽象方法原有的异常,当此方法被调用时则需要进行异常处理。
[例14.6]
package sx;
import java.util.Scanner;
interface Antiaddictlnterface{ //防沉迷接口
boolean check(int age)throws UnderAgeException; //抽象检查方法,抛出用户未成年异常
}
class UnderAgeException extends Exception{ //自定义未成年异常
public UnderAgeException(String message) { //有参构造方法
super (message); //调用原有父类构造方法
}
}
public class Variable2 { //测试类
public static void main(String[] args) { //主方法
// lambda表达式创建 Antiaddictlnterface对象,默认抛出原有异常
Antiaddictlnterface ai=(a)->{
if(a<18) { //如果年龄小于18岁
throw new UnderAgeException("未满18周岁,开启防沉迷模式"); //抛出异常
}else { //否则
return true; //验证通过
}
};
Scanner sc=new Scanner(System.in); //创建控制台扫描器
System.out.println("请输入年龄"); //控制台提示
int age=sc.nextInt(); //获取用户输入的年龄
try { //因为接口方法抛出异常,所以此处必须捕捉异常
if(ai.check(age)) { //验证年龄
System.out.println("欢迎进入XX世界");
}
}catch ( UnderAgeException e) {
System.err.println(e); //控制台打印异常警告
}
sc.close(); //关闭扫描器
}
}
运行结果:
年龄小于18 岁会捕获到 UnderAgeException 异常:
年龄大于18 岁,直接进入XX世界:
14.2 方法的引用
lambda 表达式还添加了一类新语法,用来引用方法,也就是说方法也可以作为一个对象被调用,根据不同的方法类型,方法的引用包括引用静态方法、引用成员方法和引用构造方法等。
14.2.1 引用静态方法
引用静态方法的语法如下:
类名::静态方法名
这个语法中出现了一个新的操作符“::”,这是由两个英文冒号组成的操作符,冒号之同没有空%。这个操作符左边表示方法所属的类名,右边是方法名。需要注意的是,这个语法中方法名是没有圆据号的。
[例14.7]
package sx;
interface StaticMethodlnterface{ //测试接口
int method(int a,int b); //抽象方法
}
public class Static {
static int add(int x,int y) { //静态方法,返回两个参数相加的结果
return x+y; //返回相加结果
}
public static void main(String[] args) {
StaticMethodlnterface sm = Static::add; //引用Static类的静态方法
int result = sm.method(15, 16); //直接调用接口方法获取结果
System.out.println("接口方法结果:"+result); //输出结果
}
}
运行结果:
14.2.2 引用成员方法
引用成员方法的语法如下:
对象名::成员方法名
与引用静态方法语法不同,这里操作符左侧的必须是一个对象名,而不是类名。这种语法也可以达到抽象方法按照类成员方法逻辑来实现的目的。
[例14.8]
package sx;
import java.text.SimpleDateFormat;
import java.util.Date;
interface InstanceMethodInterface{ //创建测试接口
String method(Date date); //带参数的抽象方法
}
public class InstanceMethod {
public String format(Date date) { //格式化方法
//创建日期格式化对象,并指定日期格式
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
return sdf.format(date); //返回格式化结果
}
public static void main(String[] args) {
InstanceMethod demo = new InstanceMethod(); //创建类对象
InstanceMethodInterface im = demo::format; //引用类对象的方法
Date date = new Date(); //创建日期对象
System.out.println("默认格式:"+date); //输出日期对象默认格式
System.out.println("接口输出的格式:"+ im.method(date)); //输出经过接口方法处理过的格式、
}
}
运行结果:
14.2.3 引用带泛型的方法
泛型是 Java 开发经常使用到的功能,“”操作符支持引用带泛型的方法。除方法外,“.:”操作符也支持引用带泛型的类。
[例14.9]
package sx;
import java.util.HashSet;
interface ParadigmInterface<T>{ //测试接口
int method(T[]t); //抽象方法
}
public class ParadigmDemo {
static public<T>int repeatCoount(T[]t){
int arrayLength = t.length; //记录数组长度
java.util.HashSet<T>set = new HashSet<>(); //创建哈希集合
for(T tmp:t) { //遍历数组
set.add(tmp); //将数组元素放入集合中
}
return arrayLength - set.size(); //返回数组长度与集合长度的差
}
public static void main(String[] args) {
Integer a[] = {1,1,2,3,1,5,6,1,8,8}; //整数数组
String s[] = {"王","李","赵","陈","李","孙","张"}; //字符串数组
//创建接口对象,Interger作为泛型,引入ParadigmDemo类的静态方法,方法名要定义泛型
ParadigmInterface<Integer> p1 = ParadigmDemo::<Integer> repeatCoount;
System.out.println("整数数组重复元素个数:"+p1.method(a)); //调用接口方法
//创建接口对象,String 作为泛型,引入ParadigmDemo类的静态方法
//方法名若不定义泛型,则默认使用接口包定义好的泛型
ParadigmInterface<String> p2 = ParadigmDemo::repeatCoount;
System.out.println("字符串数组重复元案个数:"+ p2.method(s)); //调用接口方法
}
}
运行结果:
14.2.4 引用构造方法
lambda 表达式有 3 种引用构造方法的语法,分别是引用无参构造方法、引用有参构造方法和引用数组构造方法。
1.引用无参构造方法
引用构造方法的语法如下:
类名::new
因为构造方法与类名相同,如果操作符左右都写类名,会让操作符误以为是在引用与类名相同的静态方法,这样会导致程序出现 Bug,所以引用构造方法的语法使用了 new 关键字。操作符右侧的写new关键字,表示引用构造方法。
这个语法有一点要注意: new 关键字之后没有圆括号,也没有参数的定义。如果类中既有无参构造方法,又有有参构造方法,使用引用构造方法语法后,引用哪个构造方法是由函数式接口决定的,“::”操作符会返回与抽象方法的参数结构相同的构造方法。如果找不到参数接口相同的构造方法,则会发生编译错误。
[例14.10]
package sx;
interface ConstructorsInterface{ //构造方法接口
ConstructorsDemo action(); //调用无参方法
}
public class ConstructorsDemo { //测试类
public ConstructorsDemo() { //无参构造方法
System.out.println("调用无参构造方法");
}
public ConstructorsDemo(int i) { //有参构造方法
System.out.println("调用有参构造方法");
}
public static void main(String[] args) {
ConstructorsInterface a = ConstructorsDemo::new; //引用ConstructorsDemo类的构造方法
ConstructorsDemo b = a.action(); //通过无参方法创建对象
}
}
运行结果:
2.引用有参构造方法
引用有参构造方法的语法与引用无参构造方法一样。区别就是函数式接口的抽象方法是有参数的。
[例14.11]
package sx;
interface ConstructorsInterface{ //构造方法接口
ConstructorsDemo action(int i); //调用有参方法
}
public class ConstructorsDemo { //测试类
public ConstructorsDemo() { //无参构造方法
System.out.println("调用无参构造方法");
}
public ConstructorsDemo(int i) { //有参构造方法
System.out.println("调用有参构造方法,参数为:"+i);
}
public static void main(String[] args) {
ConstructorsInterface a = ConstructorsDemo::new; //引用ConstructorsDemo类的构造方法
ConstructorsDemo b = a.action(123); //通过有参方法创建对象
}
}
运行结果:
3.引用数组构造方法
如果要求抽象方法既引用构造方法,又要返回数组类型结果,这种场景下抽象方法的参数就有了另外一个含义:数组个数。抽像方法的参数可以决定返回的数组长度,但数组中的元素并不是有值的,还需要再次赋值。引用数组构造方法的语法也会有所不同,语法如下:
类名[]::new
[例 14.12]
package sx;
interface ArraysConslnterface<T> { //构造方法接口
//抽象方法返回对象数组,方法参数决定数组个数
T action(int n);
}
public class Arrays {
public static void main(String[] args) {
//引用数组的构造方法
ArraysConslnterface<Arrays[]> a = Arrays[]::new;
Arrays array[] = a.action(3); //接口创建数组,并指定数组个数
array[0] = new Arrays(); //给数组元素实例化
array[1] = new Arrays();
array[2] = new Arrays();
//如果调用或给 array[3]赋值,代码则会抛出数组下标越界异常
//array[3] = new ArraysConsDemo();
}
}
实例中不能给 array[3]赋值,因为接口方法的参数是 3,创建的数组只包含 3 个元素。
14.2.5 Fuction 接口
最常用的接口是 Function<T,R>接口,这个接口有以下两个泛型:
- T:被操作的类型,可以理解为方法参数类型。
- R: 操作结果类型,可以理解为方法的返回类型。
Function 接口是函数式接口,所以只有一个抽象方法,但是 Function 接口还提供了 3 个已实现的方法以方便开发者对函数逻辑进行更深层的处理。Function 接口方法如表 14.1 所示。
[例 14.13]
package sx;
import java.util.function.Function;
public class Fun {
//创建 Function 接口对象,参数类型是 Integer,返回值类型是 String
Function<Integer[], String> function = (n)->{
StringBuilder str = new StringBuilder(); //创建字符序列
for (Integer num : n) { //遍历参数数组
str.append(num); //字符序列添加数组元素
str.append("."); //字符序列添加字符"."
}
str.deleteCharAt(str.length() - 1); //删除末尾的"."
return str.toString(); //返回字符串
};
public static void main(String[] args) {
Integer[] ip = {192, 168, 1, 1 }; //待处理的数组
Fun demo = new Fun();
System.out.println(demo.function.apply(ip)); //输出处理结果
}
}
运行结果:
14.3 流处理
流处理有点类似数据库的 SQL 语句,可以执行非常复杂的过滤、映射、查找和收集功能,并且代码量很少。唯一的缺点是代码可读性不高,如果开发者基础不好,可能会看不懂流 API所表达的含义。
Stream 接口介绍
流处理的接口都定义在 java.uil.stream 包下。BaseStream 接口是最基础的接口,但最常见的是BaseStream 接口的一个子接口——Stream接口,基本上绝大多数的流处理都是在Stream 接口实现的。Stream 接口是泛型接口,所有流中操作的元素可以是任何的对象。
因为所有集合类都是 Collection 接口的子类,如ArrayList 类、HashSet 类等,所以这些类都可以进行流处理。例如:
List<Integer> list = new ArrayList<Integer>(); //创建集合
Stream<Integer> s = list.stream(); //获取集合流对象
Optional类
Optional 类像是一个容器,可以保存任何对象,并且针对 NullPointerException 空指针异常做了化,保证 Optional 类保存的值不会是 null。
Optional 类是用 final 修饰的,所以不能有子类。Optional 类是带有泛型的类,所以该类可以保任何对象的值。
public final class Optional<T> [
private final T value;... //省略其他代码
}
Collectors 类
Collectors 类为收集器类,该类实现了javautil.Collector 接口,可以将 Stream 流对象进行各种各样的封装、归集、分组等操作。同时,Collectors 类还提供了很多实用的数据加工方法,如数据统计计算等。Collectors 类的常用方法如表 14.5 所示。
数据过滤
数据过滤就是在杂乱的数据中筛选出需要的数据,类似SOL 语句中的WHERE关键字,给出的条件,将符合条件的数据过滤并展示出来。
1.filter()方法
filter0方法是Stream接口提供的过滤方法。该方法可以将ambda表达式作为参数然后按照lambda表达式的逻辑过滤流中的元素。过滤出想要的流元素后,还需使用 Stream 提供的 collect0方法按照指定方法重新封装。
2.limit() 方法
limit() 方法是Stream 接口提供的方法,该方法可以忽略流中的前 N 个元素
实例:
可以用以下方法,就更方便快捷。
package sx;
import java.util.ArrayList;
import java.util.List;
public class Emp { //员工类
private String name; //姓名
private int age; //年龄
private double salary; //工资
private String sex; //性别
private String dept; //部门
//构造方法
public Emp(String name, int age, double salary, String sex, String dept) {
super();
this.name = name;
this.age = age;
this.salary = salary;
this.sex = sex;
this.dept = dept;
}
员工属性的getter方法:
package sx;
import java.util.ArrayList;
import java.util.List;
public class Emp { //员工类
private String name; //姓名
private int age; //年龄
private double salary; //工资
private String sex; //性别
private String dept; //部门
//构造方法
public Emp(String name, int age, double salary, String sex, String dept) {
super();
this.name = name;
this.age = age;
this.salary = salary;
this.sex = sex;
this.dept = dept;
}
//重写toString(),方便打印员工信息
public String toString() {
return "Emp [name=" + name + ", age=" + age + ", salary=" + salary + ", sex=" + sex + ", dept=" + dept + "]";
}
//以下是员工属性的getter方法
public String getName() {
return name;
}
public int getAge() {
return age;
}
public double getSalary() {
return salary;
}
public String getSex() {
return sex;
}
public String getDept() {
return dept;
}
static List<Emp>getEmpList(){ //提供数据初始化方法
List<Emp> list = new ArrayList<Emp>();
list.add(new Emp("老张",40,9000,"男","运营部")); //添加员工数据
list.add(new Emp("翠花",39,7000,"女","开发部"));
list.add(new Emp("小聂",20,5700,"女","销售部"));
list.add(new Emp("狗蛋",31,5000,"男","销售部"));
list.add(new Emp("小管",21,6000,"女","开发部"));
list.add(new Emp("小赵",25,8000,"男","人事部"));
list.add(new Emp("狗桑",20,8100,"女","人事部"));
return list;
}
}
package sx;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;
import java.util.stream.Stream;
public class Group {
public static void main(String[] args) {
List<Emp> list = Emp.getEmpList();
Stream<Emp> stream = list.stream();
Map<String, List<Emp>>map = stream.collect(Collectors.groupingBy(Emp::getDept));
Set<String> depts = map.keySet();
for(String dept : depts) {
System.out.println("");
List<Emp>temp = map.get(dept);
for(Emp e : temp) {
System.out.println(e);
}
System.out.println();
}
}
}
运行结果: