服务器之家:专注于服务器技术及软件下载分享
分类导航

PHP教程|ASP.NET教程|JAVA教程|ASP教程|

服务器之家 - 编程语言 - JAVA教程 - 详解Java的闭包

详解Java的闭包

2019-12-26 13:25goldensun JAVA教程

这篇文章主要介绍了详解Java的闭包,作者从Lambda和默认方法等重要特性深入讲解,极力推荐!需要的朋友可以参考下

 在2013年将发布的 JavaSE8 中将包含一个叫做 Lambda Project 的计划,在今年6月份的 JSR-335 草案中有描述。

 JSR-335 将闭包引入了 Java 。闭包在现在的很多流行的语言中都存在,例如 C++、C# 。闭包允许我们创建函数指针,并把它们作为参数传递。在这篇文章中,我们将粗略的看一遍Java8的特性,并介绍Lambda表达式。而且我将试着放一些样例程序来解释一些概念和语法。

Java 编程语言给我们提供了接口的概念,接口里可以定义抽象的方法。接口定义了 API,并希望用户或者供应商来实现这些方法。很多时候,我们并不为一些接口创建独立的实现类,我们通过写一个匿名内部类来写一个内联的接口实现。

匿名类使用的非常广泛。匿名内部类使用的最常见的场景就是事件处理器了。其次匿名内部类还常被用在多线程的程序中,我们通常写匿名内部类,而不是创建 Runnable/Callable 接口的实现类。

就像我们讨论的一样,一个匿名类就是一个内联的给定的接口的实现。通常我们将这个实现类的对象作为参数传递给一个方法,然后这个方法将在内部调用传递过来的实现类的方法。故这种接口叫做回调接口,这些方法叫做回调方法。


虽然匿名类到处都在使用,但是他们还是有很多问题。第一个主要问题是复杂。这些类让代码的层级看起来很乱很复杂,也称作 Vertical Problem 。第二,他们不能访问封装类的非 final 成员。this 这个关键字将变得很有迷惑性。如果一个匿名类有一个与其封装类相同的成员名称,内部变量将会覆盖外部的成员变量,在这种情况下,外部的成员在匿名类内部将是不可见的,甚至不能通过 this 关键字来访问。因为 this 关键字值得是匿名类对象本身而不是他的封装类的对象。
 

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
public void anonymousExample() {
  String nonFinalVariable = "Non Final Example";
  String variable = "Outer Method Variable";
  new Thread(new Runnable() {
    String variable = "Runnable Class Member";
    public void run() {
      String variable = "Run Method Variable";
      //Below line gives compilation error.
      //System.out.println("->" + nonFinalVariable);
      System.out.println("->" + variable);
      System.out.println("->" + this.variable);
    }
  }).start();
}

输出是:
 

?
1
2
->Run Method Variable
->Runnable Class Member

这个例子很好的说明了我上面所说的这个问题,而 Lambda 表达式几乎解决了匿名内部类带来的所有问题。在我们进一步探讨 lambda 表达式之前,让我们来看一看 Functional Interfaces。


Functional Interfaces

Functional Interfaces 是一个只有单个方法的接口,这代表了这个方法契约。

上面的定义中的只有一个实际上并没有那么简单。这段有些不懂,请读者查看原文(The ‘Single' method can exist in the form of multiple abstract methods that are inherited from superinterfaces. But in that case the inherited methods should logically represent a single method or it might redundantly declare a method that is provided by classes like Object, e.g. toString.)

下面的例子清楚的展示了怎样理解 Functional Interfaces 的概念。
 

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
interface Runnable { void run(); }
// Functional
interface Foo { boolean equals(Object obj); }
// Not functional; equals is already an implicit member
interface Bar extends Foo {int compare(String o1, String o2); }
// Functional; Bar has one abstract non-Object method
interface Comparator {
 boolean equals(Object obj);
 int compare(T o1, T o2);
}
// Functional; Comparator has one abstract non-Object method
interface Foo {int m();  Object clone(); }
// Not functional; method Object.clone is not public
interface X { int m(Iterable arg); }
interface Y { int m(Iterable arg); }
interface Z extends X, Y {}
// Functional: two methods, but they have the same signature

大多数回调接口都是 Functional Interfaces。例如 Runnable,Callable,Comparator 等等。以前被称作 SAM(Single Abstract Method)


Lambda 表达式

我们上边说过,匿名类的一个主要问题是是代码的层级看起来很乱,也就是 Vertical Problem 了,Lamdba 表达式实际上就是匿名类,只不过他们的结构更轻量,更短。Lambda 表达式看起来像方法。他们有一个正式的参数列表和这些参数的块体表达。
 

?
1
2
3
(String s)-> s.lengh;
() -> 43;
(int x, int y) -> x + y;

上面的例子的意思是,第一个表达式接收一个 String 变量作为参数,然后返回字符串的长度。第二个不带任何参数,并返回43。最后,第三个接受两个整数 x 和 y ,并返回其和。

在看了许多文字后,终于,我可以给出第一个 Lambda 表达式的例子了,这个例子运行在 JavaSE8 的预览版下:
 

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class FirstLambdaExpression {
  public String variable = "Class Level Variable";
  public static void main(String[] arg) {
    new FirstLambdaExpression().lambdaExpression();
  }
  public void lambdaExpression(){
    String variable = "Method Local Variable";
    String nonFinalVariable = "This is non final variable";
    new Thread (() -> {
      //Below line gives compilation error
      //String variable = "Run Method Variable"
      System.out.println("->" + variable);
      System.out.println("->" + this.variable);
    }).start();
  }
}

输出是:
 

?
1
2
->Method Local Variable
->Class Level Variable


你可以比较一些使用 Lambda 表达式和使用匿名内部类的区别。我们可以清楚的说,使用 Lambda 表达式的方式写匿名类解决了变量可见性的问题。你可以看一下代码中的注释, Lambda 表达式不允许创建覆盖变量。

通常的 Lambda 表达式的语法包括一个参数列表,箭头关键字"->"最后是主体。主体可以是表达式(单行语句)也可以是多行语句块。如果是表达式,将被计算后返回,如果是多行的语句块,就看起来跟方法的语句块很相似了,可以使用 return 来指定返回值。break 和 continue  只能用在循环内部。

为什么选择这个特殊的语法形式呢,因为目前 C# 和 Scala 中通常都是这种样式,也算是 Lambda 表达式的通用写法。这样的语法设计基本上解决了匿名类的复杂性。但是与此同时他也是非常灵活的,例如,如果方法体是单个表达式,大括号和 return 语句都是不需要的。表达式的结果就是作为他自己的返回值。这种灵活性可以保持代码简洁。

 Lambda 表达式用作匿名类,因此他们可以灵活运用在其他模块或在其他 Lambda 表达式(嵌套的 Lambda 表达式)。
 

?
1
2
3
4
5
6
7
8
//Lambda expression is enclosed within methods parameter block.
//Target interface type is the methods parameter type.
String user = doSomething(() -> list.getProperty(“propName”);
//Lambda expression is enclosed within a thread constructor
//target interface type is contructors paramter i.e. Runnable
new Thread (() -> {
  System.out.println("Running in different thread");
}).start();


如果你仔细看看 lambda 表达式,您将看到,目标接口类型不是一个表达式的一部分。编译器会帮助推断 lambda 表达式的类型与周围环境。

Lambda 表达式必须有一个目标类型,而他们可以适配任意可能的目标类型。当目标类型是一个接口的时候,下面的条件必须满足,才能编译正确:

  •     接口应该是一个 functional interface
  •     表达式的参数数量和类型必须与 functional interface 中声明的一致
  •     返回值类型必须兼容 functional interface 中方法的返回值类型
  •     抛出的异常表达式必须兼容 functional interface 中方法的抛出异常声明

由于编译器可以通过目标类型的声明中得知参数类型和个数,所以在 Lambda 表达式中,可以省略参数类型声明。
 

?
1
Comparator c = (s1, s2) -> s1.compareToIgnoreCase(s2);

而且,如果目标类型中声明的方法只接收一个参数(很多时候都是这样的),那么参数的小括号也是可以不写的,例如:
 

?
1
ActionListenr listenr = event -> event.getWhen();


一个很明显的问题来了,为什么 Lambda 表达式不需要一个指定的方法名呢?

答案是:Lambda 表达式只能用于 functional interface ,而 functional interface 只有一个方法。

当我们确定一个 functional interface 来创建 Lambda 表达式的时候,编译器可以感知 functional interface 中方法的签名,并且检查给定的表达式是否匹配。

这种灵活的语法帮助我们避免了使用匿名类的 Vertical Problem ,而且不会带来 Horizontal Problem(单行语句非常长)。

Lambda 表达式的语法是上下文相关的,但是这些并不是第一次出现。Java SE 7添加的diamond operators 也有这个概念,通过上下文推断类型。
 

?
1
2
3
4
void invoke(Runnable r) {r.run()}
void Future invoke(Callable r) {return c.compute()}
//above are two methods, both takes parameter of type functional interface
Future s = invoke(() ->"Done"); //Which invoke will be called?


上面问题的答案是调用接收Callable参数的方法。在这种情况下编译器会通过不同参数类型的重载解决。当有不止一个适用的重载方法,编译器也检查lambda表达式与相应的目标类型的兼容性。简单的说,上面的invoke方法期望一个返回,但是只有一个invoke方法具有返回值。

Lambda表达式可以显式的转换为指定的目标类型,只要跟对应的类型兼容。看一下下面的程序,我实现了三种Callable,而且都将其转换为Callable类型。
 

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public class FirstSightWithLambdaExpressions {
  public static void main(String[] args) {
    List list = Arrays.asList(
      (Callable)()->"callable 1",
      (Callable) ()->"callable 2",
      (Callable) ()->"callable 3");
    ExecutorService e = Executors.newFixedThreadPool(2);
    List futures = null;
    try {
      futures = e.invokeAll(list);
      new FirstSightWithLambdaExpressions().dumpList(futures);
    } catch (InterruptedException | ExecutionException e1) {
      e1.printStackTrace();
    }
    e.shutdown();
  }
  public void dumpList(List list) throws InterruptedException,
       ExecutionException {
    for (Future future : list) {
      System.out.println(future.get());
    }
  }
}

正如我们前面讨论的一样,匿名类不能访问周围环境中非final的变量。但是Lambda表达式里就没有这个限制。

目前,该定义的 functional interfaces 只适用于接口。我试着对一个只有一个抽象方法的抽象类创建一个 lambda 表达式,但出了一个编译错误。按照 jsr - 335,未来版本的 lambda 表达式可能支持 Functional Classes。


方法引用
方法引用被用作引用一个方法而不调用它。
Lambda 表达式允许我们定义一个匿名的方法,并将它作为 Functional interface 的一个实例。方法引用跟 Lambda 表达式很像,他们都需要一个目标类型,但是不同的是方法引用不提供方法的实现,他们引用一个已经存在的类或者对象的方法。
 

?
1
2
3
4
5
System::getProperty
"abc"::length
String::length
super::toString
ArrayList::new

上面的语句展示了方法和构造函数的引用的通用语法。这里我们看到引入了一个新的操作符“::'(双冒号)。我尚不清楚确切名称为这个操作符,但是 JSR 指它作为分隔符,维基百科页面是指它作为一个范围解析操作符。作为我们的参考,本教程的范围内,我们将简单地将它作为分隔符。
目标引用或者说接收者被放在提供者和分隔符的后面。这形成了一个表达式,它能够引用一个方法。在最后声明上述代码,该方法的名字是“new”。这个表达式引用的是 ArrayList 类的构造方法(下一节再讨论构造方法的引用)

再进一步了解这个之前,我想让你看一看方法引用的强大之处,我创建了一个简单的 Employee 数组的排序程序。
 

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
import java.util.Arrays;
import java.util.List;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Future;
public class MethodReference {
  public static void main (String[] ar){
    Employee[] employees = {new Employee("Nick"), new Employee("Robin"), new      Employee("Josh"), new Employee("Andy"), new Employee("Mark")};
    System.out.println("Before Sort:");
    dumpEmployee(employees);
    Arrays.sort(employees, Employee::myCompare);
    System.out.println("After Sort:");
    dumpEmployee(employees);
  }
  public static void dumpEmployee(Employee[] employees){
    for(Employee emp : Arrays.asList(employees)){
      System.out.print(emp.name+", ");
    }
    System.out.println();
  }
}
class Employee {
  String name;
  Employee(String name) {
   this.name = name;
  }
  public static int myCompare(Employee emp1, Employee emp2) {
    return emp1.name.compareTo(emp2.name);
  }
}

输出是:
 

?
1
2
Before Sort: Nick, Robin, Josh, Andy, Mark,
After Sort: Andy, Josh, Mark, Nick, Robin,

输出没什么特别,Employee 是一个非常简单的类,只有一个 name 属性。静态方法 myCompare 接收两个 Employee 对象,返回他们名字的比较。

在 main 方法中我创建了一个不同的 employee 的数组,并且将它连同一个方法引用表达式( Employee::myCompare )传递给了 Arrays.sort 方法。

等一下,如果我们看 Javadoc 你会发现 sort 方法的第二个参数是 Comparator 类型的,但是我们却传递了 Employee 的一个静态方法引用。重要的问题就在这了,我既没有让 Employee 实现 Comparable 接口,也没有写一个独立的 Comparator 类,但是输出确实没有任何问题。

让我们来看一看这是为什么。 Arrays.sort 方法期望一个 Comparator 的实例,而这个 Comparator 是一个 functional interface  ,这就意味着他只有一个方法,就是 compare 了。这里我们同样恶意传一个 Lambda 表达式,在这个表达式中提供 compare 方法的实现。但是在我们的里中,我们的 Employee 类已经有了一个自己的比较方法。只是他们的名字是不一样的,参数的类型、数量,返回值都是相同的,这里我们就可以创建一个方法引用,并将它传递给 sort 作为第二个参数。

当有多个相同的名称的方法的时候,编译器会根据目标类型选择最佳的匹配。为了搞明白,来看一个例子:
 

?
1
2
3
4
5
6
7
public static int myCompare(Employee emp1, Employee emp2) {
 return emp1.name.compareTo(emp2.name);
}
//Another method with the same name as of the above.
public static int myCompare(Integer int1, Integer int2) {
 return int1.compareTo(int2);
}

我创建了两个不同的数组,用作排序。
 

?
1
2
3
Employee[] employees = {new Employee("Nick"), new Employee("Robin"),
     new Employee("Josh"), new Employee("Andy"), new Employee("Mark")};
Integer[] ints = {1 , 4, 8, 2, 3, 8, 6};

现在,我执行下面的两行代码
 

?
1
2
Arrays.sort(employees, Employee::myCompare);
Arrays.sort(ints, Employee::myCompare);

这里,两行代码中的方法引用声明都是相同的(Employee::myCompare),唯一不同的是我们传入的数组,我们不需要传递一个含糊不清的标记用以知名那个方法作为方法引用,编译器会帮助我们检查第一个参数,并且智能的找到合适的方法。
不要被静态方法误导了哦,我们还可以创建实例方法的引用。对于静态方法我们使用类名::方法名来写方法引用,如果是实例方法的引用,则是对象::方法名。

上面的例子已经是相当不错的了,但是我们不必为整型的比较单独写一个方法,因为Integer已经实现了Comparable并且提供了实现方法compareTo。所以我们直接使用下面这一行就行了:
 

?
1
Arrays.sort(ints, Integer::compareTo);

看到这里,你是否觉得有点迷惑?没有?那我来让你迷惑一下
这里, Integer 是一个类名(而不是一个像 new Integer() 一样的实例),而 compareTo 方法却是 Integer 类的成员方法(非静态).如果你仔细看了我上面的描述就会知道,成员方法的方法引用::之前应该是对象,但是为什么这里的语句确实合法的。
答案是:这种类型的语句允许使用在一些特定的类型中。Integer是一个数据类型,而对于数据类型来说,这种语句是允许的。
如果我们将 Employee 的方法 myCompare 变成非静态的,然后这样使用:Employee::myCompare,就会出编译错误:No Suitable Method Found。

构造方法引用
构造方法引用被用作引用一个构造方法而不实例化指定的类。
构造方法引用是 JavaSE 8 的一个新的特性。我们可以构造一个构造方法的引用,并且将它作为参数传递给目标类型。
当我们使用方法引用的时候,我们引用一个已有的方法使用他们。同样的,在使用构造方法引用的时候,我们创建一个已有的构造方法的引用。
上一节中我们已经看到了构造方法引用的语法类名::new,这看起来很像方法引用。这种构造方法的引用可以分配给目标 functional interfaces 的实例。一个类可能有多个构造方法,在这种情况下,编译器会检查 functional interfaces 的类型,最终找到最好的匹配。
对我来说写出第一个构造方法引用的程序有些困难,虽然我理解了他的语法,但是我却不知道怎么使用它,以及它有什么用。最后,我花费了很久的努力,终于“啊,找到了...”,看看下面的程序吧。
 

?
1
2
3
4
5
6
7
8
9
10
11
12
public class ConstructorReference {
  public static void main(String[] ar){
    MyInterface in = MyClass::new;
    System.out.println("->"+in.getMeMyObject());
  }
}
interface MyInterface{
  MyClass getMeMyObject();
}
class MyClass{
  MyClass(){}
}

输出是:
 

?
1
->com.MyClass@34e5307e

这看起来有点神奇是吧,这个接口和这个类除了接口中声明的方法的返回值是 MyClass 类型的,没有任何关系。

这个例子又激起了我心中的另一个问题:怎样实例化一个带参数的构造方法引用?看看下面的程序:
 

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class ConstructorReference {
  public static void main(String[] ar){
    EmlpoyeeProvider provider = Employee::new;
    Employee emp = provider.getMeEmployee("John", 30);
    System.out.println("->Employee Name: "+emp.name);
    System.out.println("->Employee Age: "+emp.age);
  }
}
interface EmlpoyeeProvider{
  Employee getMeEmployee(String s, Integer i);
}
class Employee{
  String name;
  Integer age;
  Employee (String name, Integer age){
    this.name = name;
    this.age = age;
  }
}

输出是:
 

?
1
2
->Employee Name: John
->Employee Age: 30

在看完这篇文章之前,让我们再来看一看JavaSE8中的最酷的一个特性--默认方法(Default Methods)

默认方法(Default Methods)
JavaSE8 中将会引入一个叫做默认方法的概念。早起的 Java 版本的接口拥有非常严格的接口,接口包含了一些抽象方法的声明,所有非抽象的实现类必须要提供所有这些抽象方法的实现,甚至是这些方法没有用或者不合适出现在一些特殊的实现类中。在即将到来的Java 版本中,允许我们在接口中定义方法的默认实现。废话不多说,看下面:
 

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class DefaultMethods {
 public static void main(String[] ar){
 NormalInterface instance = new NormalInterfaceImpl();
 instance.myNormalMethod();
 instance.myDefaultMethod();
 }
}
interface NormalInterface{
 void myNormalMethod();
 void myDefaultMethod () default{
 System.out.println("-> myDefaultMethod");
 }
}
class NormalInterfaceImpl implements NormalInterface{
 @Override
 public void myNormalMethod() {
 System.out.println("-> myNormalMethod");
 }
}

输出是:
 

?
1
-> myDefaultMethod

上面的接口中声明了两个方法,但是这个接口的实现类只实现了其中一个,因为 myDefaultMethod 使用 default 修饰符标记了,而且提供了一个方法块用作默认实现。通用的重载规则在这里仍然生效。如果实现类实现了接口中的方法,调用的时候将是调用类中的方法,否则,默认实现将被调用。

集成父接口的接口可以增加、改变、移除父接口中的默认实现。
 

?
1
2
3
4
5
6
7
8
9
10
11
12
interface ParentInterface{
 void initiallyNormal();
 void initiallyDefault () default{
 System.out.println("-> myDefaultMethod");
 }
}
interface ChildInterface extends ParentInterface{
 void initiallyNormal() default{
 System.out.println("now default - > initiallyNormal");
 }
 void initiallyDefault (); //Now a normal method
}

在这个例子中,ParentInterface  定义了两个方法,一个是正常的,一个是有默认实现的,子接口只是简单的反了过来,给第一个方法添加了默认实现,给第二个方法移除了默认实现。
设想一个类继承了类 C ,实现了接口 I ,而且 C 有一个方法,而且跟I中的一个提供默认方法的方法是重载兼容的。在这种情况下,C中的方法会优先于I中的默认方法,甚至C中的方法是抽象的时候,仍然是优先的。
 

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class DefaultMethods {
 public static void main(String[] ar){
 Interfaxe impl = new NormalInterfaceImpl();
 impl.defaultMethod();
 }
}
class ParentClass{
 public void defaultMethod() {
 System.out.println("->ParentClass");
 }
}
interface Interfaxe{
 public void defaultMethod() default{
 System.out.println("->Interfaxe");
 }
}
class NormalInterfaceImpl extends ParentClass implements Interfaxe{}

输出是:
 

?
1
->ParentClass

第二个例子是,我的类实现了两个不同的接口,但是两个接口中都提供了相同的具有默认实现的方法的声明。在这种情况下,编译器将会搞不清楚怎么回事,实现类必须选择两个的其中一个实现。这可以通过如下的方式来使用 super 来搞定。
 

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public class DefaultMethods {
 public static void main(String[] ar){
 FirstInterface impl = new NormalInterfaceImpl();
 impl.defaultMethod();
 }
}
interface FirstInterface{
 public void defaultMethod() default{
 System.out.println("->FirstInterface");
 }
}
interface SecondInterface{
 public void defaultMethod() default{
 System.out.println("->SecondInterface");
 }
}
class NormalInterfaceImpl implements FirstInterface, SecondInterface{
 public void defaultMethod(){
 SecondInterface.super.defaultMethod();
 }
}

输出是:
 

?
1
->SecondInterface

现在,我们已经看完了 Java  闭包的介绍。这个文章中,我们接触到了 Functional Interfaces  和 Java Closure ,理解了 Java 的 Lambda 表达式,方法引用和构造方法引用。而且我们也写出了 Lambda 表达式的 Hello World 例子。
JavaSE8 很快就要到来了,我将很高兴的拥抱这些新特性,也许这些新特性还是有些迷惑不清,但是我相信,随着时间的推移,会变得越来越好。

延伸 · 阅读

精彩推荐