《Java核心技术 卷I》学习笔记25:lambda表达式


lambda表达式是一个可传递的代码块,可以在以后执行一次或多次。

1 lambda表达式的语法

lambda表达式的语法:参数+箭头+表达式。参数就像方法参数一样,指定参数类型和名称,放在小括号中,多个参数之间用逗号分隔。例如:

(String first, String second) -> first.length() - second.length()

无须指定lambda表达式的返回类型,其返回类型由上下文推导得出。

如果代码要完成的计算无法放在一个表达式中,就要把这些代码放在大括号中,并包含显式的return语句。例如:

(String first, String second) ->
{
	if (first.length() < second.length()) return -1;
	else if (first.length() > second.length()) return 1;
	else return 0;
}

如果一个lambda表达式只在某些分支返回一个值,而另一些分支不返回值,这是不合法的。例如:

(int x) -> { if (x >= 0) return 1; } // 不合法

即使lambda表达式没有参数,仍然要提供空括号。例如:

() -> { for (int i = 100; i >= 0; i--) System.out.println(i); }

如果可以推导出lambda表达式的参数类型,就可以省略其类型。例如:

Comparator<String> comp
	= (first, second) -> first.length() - second.length();

如果只有一个参数,而且这个参数的类型可以推导得出,就可以省略小括号和类型。例如:

ActionListener listener = event ->
	System.out.println("The time is " + Instant.ofEpochMilli(event.getWhen()));

2 函数式接口

对于只有一个抽象方法的接口,需要这种接口的对象时,可以提供一个lambda表达式,用于实现抽象方法。这种接口称为函数式接口。例如,Comparator接口就是只有一个抽象方法的接口,可以向它提供一个lambda表达式:

Arrays.sort(words,
	(first, second) -> first.length() - second.length());

Arrays.sort方法会接收实现了Comparator<String>的某个类的对象,在这个对象上调用compare方法会执行这个lambda表达式的体。

Java API在java.util.function包中定义了很多非常通用的函数式接口。其中Predicate接口的定义如下:

public interface Predicate<T>
{
	boolean test(T t);
	// 其他默认和静态方法
}

ArrayList类有一个removeIf方法,它的签名为:

boolean removeIf(Predicate<? super E> filter)
	// 删除列表中满足条件的所有元素。条件由 filter 指定
	// 如果有元素被删除,返回 true,否则返回 false

使用removeIf方法时,可以向参数传递一个lambda表达式作为筛选条件,例如:

list.removeIf(e -> e == null); // 删除列表中所有 null 值

3 方法引用

方法引用可以用来代替lambda表达式。方法引用的语法主要有3种情况:

  1. object::instanceMethod。前面是对象,后面是实例方法名,中间用双冒号分隔。等价于向方法传递参数的lambda表达式。例如System.out::printlnSystem.out是对象,println是方法名,它等价于x -> System.out.println(x)
  2. Class::instanceMethod。前面是类名,后面是实例方法名,中间用双冒号分隔。等价于lambda表达式的第一个参数作为隐式参数,其他参数作为显式参数,传递给方法。例如String::compareToIgnoreCase等价于(x, y) -> x.compareToIgnoreCase(y)
  3. Class::staticMethod。前面是类名,后面是静态方法名,中间用双冒号分隔。等价于lambda表达式的所有参数都传递到静态方法。例如Math::pow等价于(x, y) -> Math.pow(x, y)

下表提供了更多示例:

方法引用等价的lambda表达式说明
separator::equalsx -> separator.equals(x)第一种情况
String::trimx -> x.trim()第二种情况
String::concat(x, y) -> x.concat(y)第二种情况
Integer::valueOfx -> Integer.valueOf(x)第三种情况
Integer::sum(x, y) -> Integer.sum(x, y)第三种情况

方法引用指示编译器生成一个函数式接口的实例,覆盖这个接口的抽象方法来调用给定的方法。类似于lambda表达式,方法引用本身不是对象,但为函数式接口变量赋值时会生成一个对象。例如,下面两条语句是等价的:

Timer timer = new Timer(1000, event -> System.out.println(event));
Timer timer = new Timer(1000, System.out::println);

只有lambda表达式的体只调用一个方法而不做其他操作时,才能把lambda表达式重写为方法引用。例如下面的lambda表达式不能重写为方法引用:

s -> s.length() == 0; // 除了方法调用,还有一个比较

如果有多个同名的重载方法,编译器就会尝试从上下文中找出你指的是哪一个方法。例如,Math.max方法有两个版本,一个用于整数,另一个用于double值。选择哪个版本取决于Math::max转换为哪个函数式接口的方法参数。类似于lambda表达式,方法引用不能独立存在,总是会转换为函数式接口的实例。

包含对象的方法引用与等价的lambda表达式还有一个细微的差别。例如separator::equals,如果separatornull,构造separator::equals时就会立即抛出一个NullPointerException异常,而lambda表达式x -> separator.equals(x)只在调用时才会抛出NullPointerException

可以在方法引用中使用this参数。例如,this::equals等价于x -> this.equals(x),属于第一种情况。

使用super也是合法的,super::instanceMethod使用this作为隐式参数,调用给定方法的超类版本。

4 构造器引用

构造器引用与方法引用很类似,语法是Class::new,前面是类名,后面用new作为方法名,中间用双冒号分隔。如果有多个重载的构造器,将根据上下文决定使用哪个构造器。

可以用数组类型建立构造器引用。例如,int[]::new是一个构造器引用,它有一个参数,用于指定数组长度。这个构造器引用等价于lambda表达式x -> new int[x]

5 变量作用域

lambda表达式有3个部分:代码块、参数、自由变量。这里的自由变量是指非参数并且不在代码块中定义的变量。例如:

public static void repeatMessage(String text, int delay)
{
	ActionListener listener = event ->
		{
			System.out.println(text); // text 是自由变量
			Toolkit.getDefaultToolkit().beep();
		};
	new Timer(delay, listener).start();
}

lambda表达式中的变量text既不是参数,也不是在lambda表达式的代码块中定义的,它是外围方法的一个参数变量,因此text就是自由变量。表示lambda表达式的数据结构必须存储自由变量的值,称自由变量的值被lambda表达式捕获。

lambda表达式可以捕获外围作用域中变量的值,要确保所捕获的值是明确定义的。在lambda表达式中,只能引用不会改变的变量,不能在lambda表达式内部修改自由变量。例如,下面的做法是不合法的:

public static void countDown(int start, int delay)
{
	ActionListener listener = event ->
		{
			start--; // 错误
			System.out.println(start);
		};
	new Timer(delay, listener).start();
}

如果在lambda表达式中引用一个变量,而这个变量可能在外部改变,这也是不合法的。例如:

public static void repeat(String text, int count)
{
	for (int i = 1; i <= count; i++)
	{
		ActionListener listener = event -> System.out.println(i + ": " + text); // 错误
		new Timer(1000, listener).start();
	}
}

lambda表达式中捕获的变量必须是事实最终变量,即这个变量初始化之后就不会再为它赋新值。在上面的例子中,texti都是自由变量。由于字符串是不可变的,而且text总是引用同一个String对象,所以捕获text是合法的。不过,i的值会改变,因此不能捕获i

lambda表达式的体与嵌套块有相同的作用域。这里同样适用命名冲突和遮蔽的有关规则。在lambda表达式中声明一个与局部变量同名的参数或局部变量是不合法的。

在一个方法中不能有两个同名的局部变量,lambda表达式中同样也不能有同名的局部变量。

在一个lambda表达式中使用this关键字时,是指创建这个lambda表达式的方法的this参数。例如:

public class Application
{
	public void init()
	{
		ActionListener listener = event ->
			{
				System.out.println(this.toString());
				//...
			}
		//...
	}
}

表达式this.toString会调用Application对象的toString方法。在lambda表达式中,this的使用并没有任何特殊之处。lambda表达式的作用域嵌套在init方法中,与出现在这个方法中的其他位置一样,lambda表达式中this的含义并没有变化。

6 处理lambda表达式

下表列出了Java API提供的最重要的函数式接口:

函数式接口参数类型返回类型抽象方法名描述其他方法
Runnablevoidrun作为无参数或返回值的动作运行
Supplier<T>Tget提供一个T类型的值
Consumer<T>Tvoidaccept处理一个T类型的值andThen
BiConsumer<T, U>T,Uvoidaccept处理T和U类型的值andThen
Function<T, R>TRapply有一个T类型参数的函数composeandThenidentity
BiFunction<T, U, R>T,URapply有T和U类型参数的函数andThen
UnaryOperator<T>TTapply类型T上的一元操作符composeandThenidentity
BinaryOperator<T>T,TTapply类型T上的二元操作符andThenmaxByminBy
Predicate<T>Tbooleantest布尔值函数andornegateisEqual
BiPredicate<T, U>T,Ubooleantest有两个参数的布尔值函数andornegate

下表列出了基本类型intlongdouble的34个可用的特殊化接口:

函数式接口参数类型返回类型抽象方法名
BooleanSupplierbooleangetAsBoolean
IntSupplierintgetAsInt
LongSupplierlonggetAsLong
DoubleSupplierdoublegetAsDouble
IntConsumerintvoidaccept
LongConsumerlongvoidaccept
DoubleConsumerdoublevoidaccept
ObjIntConsumer<T>T,intvoidaccept
ObjLongConsumer<T>T,longvoidaccept
ObjDoubleConsumer<T>T,doublevoidaccept
IntFunction<T>intTapply
LongFunction<T>longTapply
DoubleFunction<T>doubleTapply
IntToLongFunctionintlongapplyAsLong
IntToDoubleFunctionintdoubleapplyAsDouble
LongToIntFunctionlongintapplyAsInt
LongToDoubleFunctionlongdoubleapplyAsDouble
DoubleToIntFunctiondoubleintapplyAsInt
DoubleToLongFunctiondoublelongapplyAsLong
ToIntFunction<T>TintapplyAsInt
ToLongFunction<T>TlongapplyAsLong
ToDoubleFunction<T>TdoubleapplyAsDouble
ToIntBiFunction<T, U>T,UintapplyAsInt
ToLongBiFunction<T, U>T,UlongapplyAsLong
ToDoubleBiFunction<T, U>T,UdoubleapplyAsDouble
IntUnaryOperatorintintapplyAsInt
LongUnaryOperatorlonglongapplyAsLong
DoubleUnaryOperatordoubledoubleapplyAsDouble
IntBinaryOperatorintintintapplyAsInt
LongBinaryOperatorlonglonglongapplyAsLong
DoubleBinaryOperatordoubledoubledoubleapplyAsDouble
IntPredicateintbooleantest
LongPredicatelongbooleantest
DoublePredicatedoublebooleantest

在自定义方法中使用lambda表达式时,要选择合适的函数式接口。

7 Comparator接口

Comparator接口包含很多方便的静态方法来创建比较器,这些方法可以用于lambda表达式或方法引用。

静态comparing方法取一个“键提取器”函数,它将类型T映射为一个可比较的类型。对要比较的对象应用这个函数,然后对返回的键完成比较。它的签名为:

static <T, U extends Comparable<? super U>> Comparator<T> comparing(Function<? super T, ?extends U> keyExtractor)
	// T 为要比较的元素的类型,U 为键的类型
	// 从 T 类型提取可比较的键,并根据键值进行比较

例如,有一个Person对象数组,要按照名字对这些对象进行排序,可以如下实现:

Arrays.sort(people, Comparator.comparing(Person::getName));
	// 从 Person 类提取名字作为键,按照名字进行排序

comparing方法有一个重载版本,允许通过第二个参数指定排序方式。它的签名为:

static <T, U> Comparator<T> comparing(Function<? super T, ?extends U> keyExtractor,
									  Comparator<? super U> keyComparator)

例如,要对上面的Person对象数组按照人名长度进行排序,可以如下实现:

Arrays.sort(people, Comparator.comparing(Person::getName,
	(s, t) -> Integer.compare(s.length(), t.length())));

为了避免intlongdouble值的自动装箱,可以使用comparingIntcomparingLongcomparingDouble方法。它们只有一个参数,用法与一个参数的comparing方法类似。例如,要对上面的Person对象数组按照人名长度进行排序,也可以如下实现:

Arrays.sort(people, Comparator.comparingInt(p -> p.getName().length()));

thenComparing方法用于指定第二排序键,当第一排序键相同时,按照第二排序键确定顺序。它的用法与comparing方法类似,也有intlongdouble值的变体。例如:

Arrays.sort(people, Comparator.comparing(Person::getLastName).thenComparing(Person::getFirstName));
	// 先按照 last name 排序,如果 last name 相同,再按照 first name 排序

可以连续使用多个thenComparing方法,指定多个排序键。

如果键值可能为null,就要用到nullsFirstnullsLast方法。这两个方法会修改现有的比较器,从而在遇到null时不会抛出异常。它们的签名为:

static <T> Comparator<T> nullsFirst(Comparator<? super T> comparator) // null 值小于正常值,将 null 值排在前面
static <T> Comparator<T> nullsLast(Comparator<? super T> comparator) // null 值大于正常值,将 null 值排在后面
// 如果键值都是 null,二者相等。如果键值都是正常值,则按照参数指定的比较器确定顺序
// 如果参数指定的比较器为 null,则所有正常值都相同

例如,按照人的middle name排序,当一个人没有middle name时,getMiddleName会返回null,此时就可以使用nullsFirstnullsLast。这两个方法需要一个比较器作为参数,指定排序方式。例如,要按照middle name的长度排序,可以实现如下:

Arrays.sort(people, Comparator.comparing(Person::getMiddleName,
	Comparator.nullsFirst(Comparator.comparingInt(p -> p.getName().length()))));

naturalOrder静态方法可以为任何实现了Comparable接口的类建立一个比较器,按照自然顺序进行排序。reverseOrder静态方法会提供自然顺序的逆序。例如:

Arrays.sort(people, Comparator.comparing(Person::getName, Comparator.reverseOrder()));
	// 按照姓名进行逆序排序

reversed方法将当前比较器转为逆序,例如:

Arrays.sort(people, Comparator.comparing(Person::getName).reversed());
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值