【java基础(四十一)】lambda表达式(三)

构造器引用

构造器引用与方法引用很类似,只不过方法名为new。例如,Person:new是Person构造器的一个引用。哪一个构造器呢?这取决于上下文。假设你有一个字符串列表。可以把它转换为一个Person对象数组,为此要在各个字符串上调用构造器。如:

ArrayList<String> names = ...;
Stream<Person> stream = names.stream().map(Person:new);
List<Person> people = stream.collect(Collectors.toList());

这里,我们暂时先理解到map方法为各个列表元素调用Person(String)构造器。如果有多个Person构造器,编译器会选择有一个String参数的构造器。因为它从上下文推导出这是在对一个字符串调用构造器。

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

Java有一个限制,无法构造泛型类型T的数组。数组构造器引用对于克服这个限制很有用。表达式new T[n]会产生错误,因为这会改为new Object[n]。对于开发类库的人来说,这是一个问题。例如,假设我们需要一个Person对象数组。Stream接口有一个toArray方法可以返回Object数组:

Object[] people = stream.toArray();

不过,这并不让人满意。用户希望得到一个Person引用数组,而不是Object引用数组。流库利用构造器引用解决了问题。可以把Person[]::new传入toArray方法:

Person[] people = stream.toArray(Person[]::new);

toArray方法调用这个构造器来得到一个正确类型的数组。然后填充这个数组并返回。

变量作用域

通常,你可以希望能够在lambda表达式中访问外围方法或类中的变量。考虑下面这个例子:

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

来看这样一个调用:

repeatMessage("Hello", 1000);

现在来看lambda表达式中的变量text。注意这个变量并不是在这个lambda表达式中定义的。实际上,这是repeatMessage方法的一个参数变量。

如果再想想看,这里好像会有问题,尽管不那么明显。lambda表达式的代码可能会在repeatMessage调用返回很久以后才运行,而那时这个参数变量已经不存在了。如何保留text变量呢?

要了解到底会发生什么,下面来巩固我们对lambda的理解。lambda表达式有3个部分:

  • 一个代码块
  • 参数
  • 自由变量的值,这是指非参数而且不在代码中定义的变量

在我们的例子中,则个lambda表达式有1个自由变量text。表示lambda表达式的数据结构必须存储自由变量的值。在这里就是字符串“Hello”。我们说它被lambda表达式捕获(captured)。

可以看到,lambda表达式可以捕获外围作用域中变量的值。在Java中,要确保捕获的值是明确定义的,这里有一个重要的限制。在lambda表达式中,只能引用值不会改变的变量,如:下面的做法是不合法的:

public static void countDown(int start, int delay) {
	ActionListener listener = event -> {
		start--;	// 这里不合法
		System.out.println(start);
	}
	new Timer(delay, listener).start();
}

之所以有这个限制是有原因的。如果在lambda表达式中改变变量,并发执行多个动作时就会不安全。对于目前为止我们看到的动作不会发生这种情况,不过一般来讲,这确实是一个严重的问题。

另外,如果在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表达式中捕获的变量必须实际上是最终变量(effectively final)。实际上的最终变量是指,这个变量初始化之后就不会再为它赋新值。在这里,text总数指示同一个String对象,所以捕获这个变量是合法的。不过,i的值会改变,因此不能捕获i。

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

Path first = Paths.get("/usr/bin");
Comparator<String> comp = (first, second) -> first.length() - second.length();

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

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

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

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

捐赠

若你感觉读到这篇文章对你有启发,能引起你的思考。请不要吝啬你的钱包,你的任何打赏或者捐赠都是对我莫大的鼓励。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章