0%

Java - Lambda表达式和内部类

学习Java中有关 Lambda 表达式的概念。

参考资料

《Java核心技术 卷1(第十一版)》

《Java基础核心总结》

菜鸟教程


lambda 表达式

Lambda 表达式的语法

在 Java 中,有时候需要将一个代码块传递到某个对象(例如一个计时器或者数组的 sort 方法)。然而,Java 中并不能直接传递一个代码块,必须通过定义一个类和类方法来包含代码块,并构造实例来调用。为了解决这个问题,引入了 lambda 表达式。

1
2
3
// 用 lambda 表达式表示代码块
var timer = new Timer(1000, event -> System.out.println(event));
timer.start();

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

lambda 表达式常见形式为“参数 + 箭头‘->’ + 表达式”;如果一个表达式不足以表示内容,可以像一般方法一样将内容写在 {} 内,并且要包含显式的 return 语句(如果表达式没有返回值就不写)。

lambda 表达式不需要标明返回值类型,返回类型可以通过上下文推导得到。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 简短的版本
(String first, String second) -> first.length() - second.length()

// 带上花括号
(String first, String second) ->
{
if (first.length() < second.length()) return -1;
else if (first.length() == second.length()) return 0;
else return 0;
}

// 表达式没有参数时,也需要写括号
() -> System.out.println("Hello World!");

// 表达式只有一个参数,并且参数类型可以推导得出,也可以不写括号
x -> System.out.println(x);

函数式接口

Java 中有很多封装了代码块的接口,例如 ActionListener 和 Comparator,lambda 表达式与这些接口是兼容的。

对于只有一个抽象方法的接口,需要这种接口的对象时,就可以提供一个 lambda 表达式。这种接口被称为函数式接口

以 Arrays.sort() 为例,该方法的一个重载包含两个参数:一个数组和一个实现了 Comparator 接口的类对象(比较器)。该接口中的 compare 方法为 sort 提供排序的依据,如果不使用 lambda 表达式,就需要声明一个类来存放比较规则方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// 假如要按长度来排序字符串

// 用类包装的办法
class LengthComparator implements Comparator
{
public int compare (String first, String second)
{
return first.length() - second.length();
}
}

// 需要构建实例
var comp1 = new LengthComparator();

// 排序
Arrays.sort(anStringArray, comp1);
/*-------------------------------------------------*/

// lambda 表达式只需要一句话
Arrays.sort(anStringArray, (first, second) -> first.length() - second.length())

虽然 lambda 表达式可以传递到函数式接口(或者说 lambda 表达式可以转换为函数式接口),但是最好还是把它看作是函数而不是一个类。另外,函数式接口类型的变量可以引用一个 lambda 表达式,有点像给表达式一个“函数名”。

实际上,在 Java 中,对 lambda 表达式所能做的也就只是转换为函数式接口,而不像其它一些语言,还可以声明函数类型、声明这些类型的变量,还可以使用变量保存函数表达式。

方法引用

对于一个定时器事件,以下写法效果是相同的。

1
2
3
// 每隔1000毫秒打印一次事件对象 event
var timer1 = new Timer (1000, event -> System.out.println(event));
var timer2 = new Timer (1000, System.out::println);

其中,表达式 System.out::println 是一个方法引用,它指示编译器生成一个函数式接口的实例,覆盖这个接口的抽象方法来调用给定的方法。在这个例子中,会生成一个 ActionListener,它的 actionPerformed(ActionEvent e) 方法要调用 System.out.println()。

对于有重载的方法,编译器会根据上下文确定参数列表最合适的方法进行调用。例如上例中的 println 有10个重载,而方法引用需要 println 转换为一个包含方法 void actionPerformed(ActionEvent e) 的 ActionListener 实例,所以会从重载中选择 println(Object x) 方法。

同理,如果想对字符串进行排序,排序规则为忽略大小写的字典排序,则可以这样写:

1
Arrays.sort(anStringArray, String::compareToIgnoreCase)

总结来说,方法引用主要有以下三种情况:

  1. object::instanceMethod :等价于向方法传递参数的 lambda 表达式,例如 System.out::println 等价为 x -> System.out.println(x);
  2. Class::instanceMethod :第一个参数会成为该引用方法的隐式参数,例如 String::compareToIgnoreCase 等价于 (x, y) -> x.compareToIgnoreCase(y);
  3. Class::staticMethod :所有参数都成为静态方法的参数,例如 Math::pow 等价于 (x, y) -> Math.pow(x, y)。

注:1. 只有当 lambda 表达式的体只调用一种方法而不做其他任何操作时,才能把 lambda 表达式重写为方法引用。

1
s -> s.length() == 0	// 这里除了一个方法调用,还有一个比较运算,所以不能重载为方法引用
  1. 方法引用和 lambda 表达式一样,不能独立存在,总是需要转换为函数式接口的实例。

  2. 可以在方法中使用 this 和 super。this::instanceMethod 调用本类中的方法,super::instanceMethod 则会调用以 this 为目标的方法的超类版本。

变量作用域

1
2
3
4
5
6
7
8
public static void repeatMessage (String text, int delay)
{
ActionListener listener = event ->
{
System.out.println(text);
};
new Timer(delay, listener).start();
}

lambda 表达式能够捕获外围作用域中的变量值。上面的表达式就获取了外围方法的参数 text。

在 lambda 表达式中,只能引用值不会改变的变量,即捕获的变量必须是事实最终变量。事实最终变量是指初始化后不会再赋新值的变量。

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

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

1
2
3
4
5
6
7
8
9
10
11
public class Application
{
public void init()
{
ActionListener listener ->
{
System.out.println(this.toString());
...
}
}
}

这里的 this.toString() 调用的是类 Application 中的方法。


内部类

内部类是定义在另一个类中的类。内部类的特点有:

  1. 可以对包中的其它类隐藏;
  2. 内部类方法可以访问定义这个类的作用域中的数据,包括原本 private 的数据。

使用内部类访问对象状态

以下面代码为例。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class TalkingClock
{
private int interval;
private boolean beep;

public TalkingClock(int interval, boolean beep) {...}
public void start() {...}

// 一个内部类
public class TimePrinter implements ActionListener
{
public void actionPerformed(ActionEvent event)
{
System.out.println(...);
if (beep) Toolkit.getDefaultToolkit().beep();
}
}
}

这里的 TimePrinter 类位于 TalkingClock 内部,但是并不是所有 TalkingClock 类实例都有一个 TimePrinter 实例字段,内部类实例由包含它的类的方法来构造。

内部类 TimePrinter 的方法可以访问外围类的私有字段 beep。即内部类方法不仅可以访问自身的数据字段,也可以访问外围类中的字段成员(即使是私有成员)。为此,内部类的对象总有一个隐式引用,指向创建它的外部类对象,不过这个引用在内部类的定义中是不可见的。

外围类的引用在内部类的构造器中设置,编译器会修改所有的内部类构造器,添加一个对应的外围类引用的参数。

可以把内部类声明为私有的,这样的话只有外围类的方法可以构造内部类对象。注意,只有内部类是可以私有的,常规类只有默认的包可见或者公共可见。

内部类的特殊语法规则

  • 使用 OuterClass.this 来表示对外围类的引用

  • 更明确地编写内部类对象的构造器:

    一般内部类:OuterObject.new InnerClasss(construction parameters)

    静态内部类:new OuterClass.InnerClasss(construction parameters)

    1
    ActionListener listener = this.new TimePrinter;
  • 在外围类的作用域之外,引用内部类的方法:OuterClass.InnerClass

  • 除了静态内部类之外的一般内部类,不允许包含静态字段或方法成员
    👉非静态内部类中能不能拥有静态方法或属性?

局部内部类

定义在一个方法中的类称为局部内部类。声明局部内部类的时候不能有访问修饰符(即 public 或 private 等),局部类的作用域被限定在声明这个局部类的块中。

局部类的优点在于自身只能被所属的方法访问,同时,局部类不仅能够访问外部类的所有字段,还能访问方法中的局部变量。但是,局部类可访问的局部变量必须是事实最终变量

匿名内部类

如果定义的局部类在代码中只需使用一次,可以不用给它指定类名,即匿名局部类。

匿名局部类的语法一般为

1
2
3
4
new SuperType (construction parameters)
{
inner class methods and data
}

其中 SuperType 可以是一个接口,也可以是类。是接口的话说明这个匿名类需要实现该接口,是类的话说明这个匿名类继承了这个超类。

由于构造器名字与类名相同,而匿名类没有类名,所以匿名类不能有构造方法,而括号里的参数实际上传递给了超类的构造方法;对于接口来说,这个小括号里就没有构造参数。虽然不能有构造方法,但是仍然可以为匿名类提供初始化块。

1
2
3
var a = new A();	// 构造了一个 A 类的对象 a

var b = new A() {...}; // 声明并构造了一个继承 A 类的匿名类对象 b

匿名内部类与 lambda 表达式类似,可以用来实现事件监听器或者其他回调。

静态内部类

如果不需要外围类的引用,可以把类声明为静态的,静态内部类又称为嵌套类。与常规内部类不同,静态内部类可以有静态字段和静态方法。

在接口中的内部类默认是 public 且 static。

-------------本文结束感谢您的阅读-------------