人生倒计时
- 今日已经过去小时
- 这周已经过去天
- 本月已经过去天
- 今年已经过去个月
在介绍表达式之前,我们先来看看只有一个方法的(通常我们称之为回调接口):
public interface OnClickListener { void onClick(View v); }
我们这样使用它:
button.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { v.setText("lalala"); } });
这种回调模式在各种框架中都很流行,但是像上面这样的匿名内部类并不是一个好的选择,因为:
开心的是Java8给我们带来的,让我们看看如何使用上面的功能:
button.setOnClickListener(v -> v.setText("lalala"));
怎么样? !一行五行代码就可以搞定! ! !
这里增加一个概念功能界面;上面提到的接口只有一个方法,Java中大部分回调接口都有这个特点:比如and;我们只用一个方法功能接口来调用这些接口。
1.表达式
匿名内部类的最大问题是它们的冗余语法。比如前面五行代码中只有一行是在执行任务。表达式是匿名方法java lambda 方法引用,我们之前已经看到,它们以极其轻量级的语法解决了这个问题。
以下是一些表达式示例:
(int x, int y) -> x + y //接收x和y两个整形参数并返回他们的和 () -> 66 //不接收任何参数直接返回66 (String name) -> {System.out.println(name);} //接收一个字符串然后打印出来 (View view) -> {view.setText("lalala");} //接收一个View对象并调用setText方法
表达式语法由参数列表、-> 和函数体组成。函数体可以是表达式或代码块。
2.目标类型
从前面的例子中,我们可以看到表达式没有名字,那么我们怎么知道它的类型呢?答案来自上下文。例如下面表达式的类型是
OnClickListener listener = (View v) -> {v.setText("lalala");};
这意味着相同的表达式在不同的上下文中具有不同的类型
Runnable runnable = () -> doSomething(); //这个表达式是Runnable类型的 Callback callback = () -> doSomething(); //这个表达式是Callback类型的
编译器使用表达式所在的上下文所期望的类型来推断表达式的类型。这种预期类型称为目标类型。表达式只能出现在目标类型是函数式接口的上下文中。
表达式的类型和目标类型的方法签名必须相同。编译器会检查这个。要将表达式分配给目标类型 T,它必须满足以下所有条件:
由于目标类型是已知表达式的参数类型,所以不需要重复已知类型。也就是说,表达式的参数类型可以从目标类型中获取:
//编译器可以推导出s1和s2是String类型 Comparatorc = (s1, s2) -> s1.compareTo(s2); //当表达式的参数只有一个时括号也是可以省略的 button.setOnClickListener(v -> v.setText("lalala"));
ps:Java7中的泛型方法和构造函数也是通过目标类型进行类型推导的,如:
ListintList = Collections.emptyList>(); List strList = new ArrayList<>();
3.范围
在内部类中使用变量名和 this 非常容易出错。通过继承获得的内部类的成员变量(包括外部类的)可能会覆盖外部类的成员变量,无限制的this引用将指向内部类本身而不是外部类。
而且表达式的语义非常简单:它不从父类继承任何变量,也不引入新的作用域。函数体中表达式和变量的参数与其外部环境中的变量具有相同的语义(this关键字也是如此)。
让我们来个栗子吧!
public class HelloLambda { Runnable r1 = () -> System.out.println(this); Runnable r2 = () -> System.out.println(toString()); @Override public String toString() { return "Hello, lambda!"; } public static void main(String[] args) { new HelloLambda().r1.run(); new HelloLambda().r2.run(); } }
上面的代码最终会打印两个 Hello, ! ,并且类似的内部类会打印出意外的字符串,例如 $1@ 和 $1@。
总结:基于词法作用域的概念,表达式不能屏蔽其上下文中的任何局部变量。
4.变量捕获
在 Java 7 中,编译器对内部类中引用的外部变量(即捕获的变量)非常严格:如果捕获的变量未声明为 final,则会产生编译错误。但是这个限制在 Java 8 中已经放宽了——对于表达式和内部类,允许捕获符合有效只读条件的局部变量(如果局部变量在初始化后从未被修改,则它实际上是只读的)。
Runnable getRunnable(String name){ String hello = "hello"; return () -> System.out.println(hello+","+name); }
对 this 的引用以及对非限定字段的引用和通过 this 调用非限定方法本质上是最终局部变量。包含此类引用的表达式等效于捕获 this 实例。在其他情况下,对象不保留 this 的任何应用。
此功能非常适合内存管理:请注意java lambda 方法引用,在 java 中,默认情况下非静态内部类将持有对外部类实例的强引用,这通常会导致内存泄漏。在表达式中,如果未捕获外部类成员,则不保留对外部类实例的引用。
虽然Java 8放宽了对捕获变量的语法限制,但是禁止尝试修改捕获变量,比如下面的例子是非法的:
int sum = 0; list.forEach(i -> {sum += i;});
为什么禁止这种行为?因为这样的表达方式很容易引起竞态
另一个原因
表达式不支持修改捕获的变量是我们可以使用更好的方法来达到同样的效果:使用()。 java.util. 提供了各种协议操作,我们将在下一章介绍Java8中的API。
5.方法参考
表达式允许我们定义一个匿名方法并将其用作函数式接口。 Java 8 可以在现有方法上实现相同的功能。
方法引用和表达式具有相同的属性(它们都需要目标类型,并且都需要转换为函数式接口的实例),但是我们不需要为方法引用提供方法体,我们可以直接通过方法名引用有方法。
以下面的代码为例,假设我们要排序
class User{ private String userName; public String getUserName() { return userName; } ... } Listusers = new ArrayList<>(); Comparator comparator = Comparator.comparing(u -> u.getUserName()); Collections.sort(users, comparator);
我们可以用方法引用替换上面的表达式
Comparatorcomparator = Comparator.comparing(User::getUserName);
User:: here 被视为表达式的简写。虽然方法引用不一定会使代码更紧凑,但它具有更明确的语义——如果我们要调用的方法有名称,那么我们可以通过方法名称来调用它。
方法引用有很多种,它们的语法如下: