详解Java的闭包
在2013年将发布的JavaSE8中将包含一个叫做LambdaProject的计划,在今年6月份的JSR-335草案中有描述。
JSR-335将闭包引入了Java。闭包在现在的很多流行的语言中都存在,例如C++、C#。闭包允许我们创建函数指针,并把它们作为参数传递。在这篇文章中,我们将粗略的看一遍Java8的特性,并介绍Lambda表达式。而且我将试着放一些样例程序来解释一些概念和语法。
Java编程语言给我们提供了接口的概念,接口里可以定义抽象的方法。接口定义了API,并希望用户或者供应商来实现这些方法。很多时候,我们并不为一些接口创建独立的实现类,我们通过写一个匿名内部类来写一个内联的接口实现。
匿名类使用的非常广泛。匿名内部类使用的最常见的场景就是事件处理器了。其次匿名内部类还常被用在多线程的程序中,我们通常写匿名内部类,而不是创建Runnable/Callable接口的实现类。
就像我们讨论的一样,一个匿名类就是一个内联的给定的接口的实现。通常我们将这个实现类的对象作为参数传递给一个方法,然后这个方法将在内部调用传递过来的实现类的方法。故这种接口叫做回调接口,这些方法叫做回调方法。
虽然匿名类到处都在使用,但是他们还是有很多问题。第一个主要问题是复杂。这些类让代码的层级看起来很乱很复杂,也称作VerticalProblem。第二,他们不能访问封装类的非final成员。this这个关键字将变得很有迷惑性。如果一个匿名类有一个与其封装类相同的成员名称,内部变量将会覆盖外部的成员变量,在这种情况下,外部的成员在匿名类内部将是不可见的,甚至不能通过this关键字来访问。因为this关键字值得是匿名类对象本身而不是他的封装类的对象。
publicvoidanonymousExample(){ StringnonFinalVariable="NonFinalExample"; Stringvariable="OuterMethodVariable"; newThread(newRunnable(){ Stringvariable="RunnableClassMember"; publicvoidrun(){ Stringvariable="RunMethodVariable"; //Belowlinegivescompilationerror. //System.out.println("->"+nonFinalVariable); System.out.println("->"+variable); System.out.println("->"+this.variable); } }).start(); }
输出是:
->RunMethodVariable ->RunnableClassMember
这个例子很好的说明了我上面所说的这个问题,而Lambda表达式几乎解决了匿名内部类带来的所有问题。在我们进一步探讨lambda表达式之前,让我们来看一看FunctionalInterfaces。
FunctionalInterfaces
FunctionalInterfaces是一个只有单个方法的接口,这代表了这个方法契约。
上面的定义中的只有一个实际上并没有那么简单。这段有些不懂,请读者查看原文(The‘Single'methodcanexistintheformofmultipleabstractmethodsthatareinheritedfromsuperinterfaces.ButinthatcasetheinheritedmethodsshouldlogicallyrepresentasinglemethodoritmightredundantlydeclareamethodthatisprovidedbyclasseslikeObject,e.g.toString.)
下面的例子清楚的展示了怎样理解FunctionalInterfaces的概念。
interfaceRunnable{voidrun();} //Functional interfaceFoo{booleanequals(Objectobj);} //Notfunctional;equalsisalreadyanimplicitmember interfaceBarextendsFoo{intcompare(Stringo1,Stringo2);} //Functional;Barhasoneabstractnon-Objectmethod interfaceComparator{ booleanequals(Objectobj); intcompare(To1,To2); } //Functional;Comparatorhasoneabstractnon-Objectmethod interfaceFoo{intm();Objectclone();} //Notfunctional;methodObject.cloneisnotpublic interfaceX{intm(Iterablearg);} interfaceY{intm(Iterablearg);} interfaceZextendsX,Y{} //Functional:twomethods,buttheyhavethesamesignature
大多数回调接口都是FunctionalInterfaces。例如Runnable,Callable,Comparator等等。以前被称作SAM(SingleAbstractMethod)
Lambda表达式
我们上边说过,匿名类的一个主要问题是是代码的层级看起来很乱,也就是VerticalProblem了,Lamdba表达式实际上就是匿名类,只不过他们的结构更轻量,更短。Lambda表达式看起来像方法。他们有一个正式的参数列表和这些参数的块体表达。
(Strings)->s.lengh; ()->43; (intx,inty)->x+y;
上面的例子的意思是,第一个表达式接收一个String变量作为参数,然后返回字符串的长度。第二个不带任何参数,并返回43。最后,第三个接受两个整数x和y,并返回其和。
在看了许多文字后,终于,我可以给出第一个Lambda表达式的例子了,这个例子运行在JavaSE8的预览版下:
publicclassFirstLambdaExpression{ publicStringvariable="ClassLevelVariable"; publicstaticvoidmain(String[]arg){ newFirstLambdaExpression().lambdaExpression(); } publicvoidlambdaExpression(){ Stringvariable="MethodLocalVariable"; StringnonFinalVariable="Thisisnonfinalvariable"; newThread(()->{ //Belowlinegivescompilationerror //Stringvariable="RunMethodVariable" System.out.println("->"+variable); System.out.println("->"+this.variable); }).start(); } }
输出是:
->MethodLocalVariable ->ClassLevelVariable
你可以比较一些使用Lambda表达式和使用匿名内部类的区别。我们可以清楚的说,使用Lambda表达式的方式写匿名类解决了变量可见性的问题。你可以看一下代码中的注释,Lambda表达式不允许创建覆盖变量。
通常的Lambda表达式的语法包括一个参数列表,箭头关键字"->"最后是主体。主体可以是表达式(单行语句)也可以是多行语句块。如果是表达式,将被计算后返回,如果是多行的语句块,就看起来跟方法的语句块很相似了,可以使用return来指定返回值。break和continue 只能用在循环内部。
为什么选择这个特殊的语法形式呢,因为目前C#和Scala中通常都是这种样式,也算是Lambda表达式的通用写法。这样的语法设计基本上解决了匿名类的复杂性。但是与此同时他也是非常灵活的,例如,如果方法体是单个表达式,大括号和return语句都是不需要的。表达式的结果就是作为他自己的返回值。这种灵活性可以保持代码简洁。
Lambda表达式用作匿名类,因此他们可以灵活运用在其他模块或在其他Lambda表达式(嵌套的Lambda表达式)。
//Lambdaexpressionisenclosedwithinmethodsparameterblock. //Targetinterfacetypeisthemethodsparametertype. Stringuser=doSomething(()->list.getProperty(“propName”); //Lambdaexpressionisenclosedwithinathreadconstructor //targetinterfacetypeiscontructorsparamteri.e.Runnable newThread(()->{ System.out.println("Runningindifferentthread"); }).start();
如果你仔细看看lambda表达式,您将看到,目标接口类型不是一个表达式的一部分。编译器会帮助推断lambda表达式的类型与周围环境。
Lambda表达式必须有一个目标类型,而他们可以适配任意可能的目标类型。当目标类型是一个接口的时候,下面的条件必须满足,才能编译正确:
- 接口应该是一个functionalinterface
- 表达式的参数数量和类型必须与functionalinterface中声明的一致
- 返回值类型必须兼容functionalinterface中方法的返回值类型
- 抛出的异常表达式必须兼容functionalinterface中方法的抛出异常声明
由于编译器可以通过目标类型的声明中得知参数类型和个数,所以在Lambda表达式中,可以省略参数类型声明。
Comparatorc=(s1,s2)->s1.compareToIgnoreCase(s2);
而且,如果目标类型中声明的方法只接收一个参数(很多时候都是这样的),那么参数的小括号也是可以不写的,例如:
ActionListenrlistenr=event->event.getWhen();
一个很明显的问题来了,为什么Lambda表达式不需要一个指定的方法名呢?
答案是:Lambda表达式只能用于functionalinterface,而functionalinterface只有一个方法。
当我们确定一个functionalinterface来创建Lambda表达式的时候,编译器可以感知functionalinterface中方法的签名,并且检查给定的表达式是否匹配。
这种灵活的语法帮助我们避免了使用匿名类的VerticalProblem,而且不会带来HorizontalProblem(单行语句非常长)。
Lambda表达式的语法是上下文相关的,但是这些并不是第一次出现。JavaSE7添加的diamondoperators也有这个概念,通过上下文推断类型。
voidinvoke(Runnabler){r.run()} voidFutureinvoke(Callabler){returnc.compute()} //abovearetwomethods,bothtakesparameteroftypefunctionalinterface Futures=invoke(()->"Done");//Whichinvokewillbecalled?
上面问题的答案是调用接收Callable参数的方法。在这种情况下编译器会通过不同参数类型的重载解决。当有不止一个适用的重载方法,编译器也检查lambda表达式与相应的目标类型的兼容性。简单的说,上面的invoke方法期望一个返回,但是只有一个invoke方法具有返回值。
Lambda表达式可以显式的转换为指定的目标类型,只要跟对应的类型兼容。看一下下面的程序,我实现了三种Callable,而且都将其转换为Callable类型。
publicclassFirstSightWithLambdaExpressions{ publicstaticvoidmain(String[]args){ Listlist=Arrays.asList( (Callable)()->"callable1", (Callable)()->"callable2", (Callable)()->"callable3"); ExecutorServicee=Executors.newFixedThreadPool(2); Listfutures=null; try{ futures=e.invokeAll(list); newFirstSightWithLambdaExpressions().dumpList(futures); }catch(InterruptedException|ExecutionExceptione1){ e1.printStackTrace(); } e.shutdown(); } publicvoiddumpList(Listlist)throwsInterruptedException, ExecutionException{ for(Futurefuture:list){ System.out.println(future.get()); } } }
正如我们前面讨论的一样,匿名类不能访问周围环境中非final的变量。但是Lambda表达式里就没有这个限制。
目前,该定义的functionalinterfaces只适用于接口。我试着对一个只有一个抽象方法的抽象类创建一个lambda表达式,但出了一个编译错误。按照jsr-335,未来版本的lambda表达式可能支持FunctionalClasses。
方法引用
方法引用被用作引用一个方法而不调用它。
Lambda表达式允许我们定义一个匿名的方法,并将它作为Functionalinterface的一个实例。方法引用跟Lambda表达式很像,他们都需要一个目标类型,但是不同的是方法引用不提供方法的实现,他们引用一个已经存在的类或者对象的方法。
System::getProperty "abc"::length String::length super::toString ArrayList::new
上面的语句展示了方法和构造函数的引用的通用语法。这里我们看到引入了一个新的操作符“::'(双冒号)。我尚不清楚确切名称为这个操作符,但是JSR指它作为分隔符,维基百科页面是指它作为一个范围解析操作符。作为我们的参考,本教程的范围内,我们将简单地将它作为分隔符。
目标引用或者说接收者被放在提供者和分隔符的后面。这形成了一个表达式,它能够引用一个方法。在最后声明上述代码,该方法的名字是“new”。这个表达式引用的是ArrayList类的构造方法(下一节再讨论构造方法的引用)
再进一步了解这个之前,我想让你看一看方法引用的强大之处,我创建了一个简单的Employee数组的排序程序。
importjava.util.Arrays; importjava.util.List; importjava.util.concurrent.ExecutionException; importjava.util.concurrent.Future; publicclassMethodReference{ publicstaticvoidmain(String[]ar){ Employee[]employees={newEmployee("Nick"),newEmployee("Robin"),newEmployee("Josh"),newEmployee("Andy"),newEmployee("Mark")}; System.out.println("BeforeSort:"); dumpEmployee(employees); Arrays.sort(employees,Employee::myCompare); System.out.println("AfterSort:"); dumpEmployee(employees); } publicstaticvoiddumpEmployee(Employee[]employees){ for(Employeeemp:Arrays.asList(employees)){ System.out.print(emp.name+","); } System.out.println(); } } classEmployee{ Stringname; Employee(Stringname){ this.name=name; } publicstaticintmyCompare(Employeeemp1,Employeeemp2){ returnemp1.name.compareTo(emp2.name); } }
输出是:
BeforeSort:Nick,Robin,Josh,Andy,Mark, AfterSort: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是一个functionalinterface ,这就意味着他只有一个方法,就是compare了。这里我们同样恶意传一个Lambda表达式,在这个表达式中提供compare方法的实现。但是在我们的里中,我们的Employee类已经有了一个自己的比较方法。只是他们的名字是不一样的,参数的类型、数量,返回值都是相同的,这里我们就可以创建一个方法引用,并将它传递给sort作为第二个参数。
当有多个相同的名称的方法的时候,编译器会根据目标类型选择最佳的匹配。为了搞明白,来看一个例子:
publicstaticintmyCompare(Employeeemp1,Employeeemp2){ returnemp1.name.compareTo(emp2.name); } //Anothermethodwiththesamenameasoftheabove. publicstaticintmyCompare(Integerint1,Integerint2){ returnint1.compareTo(int2); }
我创建了两个不同的数组,用作排序。
Employee[]employees={newEmployee("Nick"),newEmployee("Robin"), newEmployee("Josh"),newEmployee("Andy"),newEmployee("Mark")}; Integer[]ints={1,4,8,2,3,8,6};
现在,我执行下面的两行代码
Arrays.sort(employees,Employee::myCompare); Arrays.sort(ints,Employee::myCompare);
这里,两行代码中的方法引用声明都是相同的(Employee::myCompare),唯一不同的是我们传入的数组,我们不需要传递一个含糊不清的标记用以知名那个方法作为方法引用,编译器会帮助我们检查第一个参数,并且智能的找到合适的方法。
不要被静态方法误导了哦,我们还可以创建实例方法的引用。对于静态方法我们使用类名::方法名来写方法引用,如果是实例方法的引用,则是对象::方法名。
上面的例子已经是相当不错的了,但是我们不必为整型的比较单独写一个方法,因为Integer已经实现了Comparable并且提供了实现方法compareTo。所以我们直接使用下面这一行就行了:
Arrays.sort(ints,Integer::compareTo);
看到这里,你是否觉得有点迷惑?没有?那我来让你迷惑一下
这里,Integer是一个类名(而不是一个像newInteger()一样的实例),而compareTo方法却是Integer类的成员方法(非静态).如果你仔细看了我上面的描述就会知道,成员方法的方法引用::之前应该是对象,但是为什么这里的语句确实合法的。
答案是:这种类型的语句允许使用在一些特定的类型中。Integer是一个数据类型,而对于数据类型来说,这种语句是允许的。
如果我们将Employee的方法myCompare变成非静态的,然后这样使用:Employee::myCompare,就会出编译错误:NoSuitableMethodFound。
构造方法引用
构造方法引用被用作引用一个构造方法而不实例化指定的类。
构造方法引用是JavaSE8的一个新的特性。我们可以构造一个构造方法的引用,并且将它作为参数传递给目标类型。
当我们使用方法引用的时候,我们引用一个已有的方法使用他们。同样的,在使用构造方法引用的时候,我们创建一个已有的构造方法的引用。
上一节中我们已经看到了构造方法引用的语法类名::new,这看起来很像方法引用。这种构造方法的引用可以分配给目标functionalinterfaces的实例。一个类可能有多个构造方法,在这种情况下,编译器会检查functionalinterfaces的类型,最终找到最好的匹配。
对我来说写出第一个构造方法引用的程序有些困难,虽然我理解了他的语法,但是我却不知道怎么使用它,以及它有什么用。最后,我花费了很久的努力,终于“啊,找到了...”,看看下面的程序吧。
publicclassConstructorReference{ publicstaticvoidmain(String[]ar){ MyInterfacein=MyClass::new; System.out.println("->"+in.getMeMyObject()); } } interfaceMyInterface{ MyClassgetMeMyObject(); } classMyClass{ MyClass(){} }
输出是:
->com.MyClass@34e5307e
这看起来有点神奇是吧,这个接口和这个类除了接口中声明的方法的返回值是MyClass类型的,没有任何关系。
这个例子又激起了我心中的另一个问题:怎样实例化一个带参数的构造方法引用?看看下面的程序:
publicclassConstructorReference{ publicstaticvoidmain(String[]ar){ EmlpoyeeProviderprovider=Employee::new; Employeeemp=provider.getMeEmployee("John",30); System.out.println("->EmployeeName:"+emp.name); System.out.println("->EmployeeAge:"+emp.age); } } interfaceEmlpoyeeProvider{ EmployeegetMeEmployee(Strings,Integeri); } classEmployee{ Stringname; Integerage; Employee(Stringname,Integerage){ this.name=name; this.age=age; } }
输出是:
->EmployeeName:John ->EmployeeAge:30
在看完这篇文章之前,让我们再来看一看JavaSE8中的最酷的一个特性--默认方法(DefaultMethods)
默认方法(DefaultMethods)
JavaSE8中将会引入一个叫做默认方法的概念。早起的Java版本的接口拥有非常严格的接口,接口包含了一些抽象方法的声明,所有非抽象的实现类必须要提供所有这些抽象方法的实现,甚至是这些方法没有用或者不合适出现在一些特殊的实现类中。在即将到来的Java版本中,允许我们在接口中定义方法的默认实现。废话不多说,看下面:
publicclassDefaultMethods{ publicstaticvoidmain(String[]ar){ NormalInterfaceinstance=newNormalInterfaceImpl(); instance.myNormalMethod(); instance.myDefaultMethod(); } } interfaceNormalInterface{ voidmyNormalMethod(); voidmyDefaultMethod()default{ System.out.println("->myDefaultMethod"); } } classNormalInterfaceImplimplementsNormalInterface{ @Override publicvoidmyNormalMethod(){ System.out.println("->myNormalMethod"); } }
输出是:
->myDefaultMethod
上面的接口中声明了两个方法,但是这个接口的实现类只实现了其中一个,因为myDefaultMethod使用default修饰符标记了,而且提供了一个方法块用作默认实现。通用的重载规则在这里仍然生效。如果实现类实现了接口中的方法,调用的时候将是调用类中的方法,否则,默认实现将被调用。
集成父接口的接口可以增加、改变、移除父接口中的默认实现。
interfaceParentInterface{ voidinitiallyNormal(); voidinitiallyDefault()default{ System.out.println("->myDefaultMethod"); } } interfaceChildInterfaceextendsParentInterface{ voidinitiallyNormal()default{ System.out.println("nowdefault->initiallyNormal"); } voidinitiallyDefault();//Nowanormalmethod }
在这个例子中,ParentInterface 定义了两个方法,一个是正常的,一个是有默认实现的,子接口只是简单的反了过来,给第一个方法添加了默认实现,给第二个方法移除了默认实现。
设想一个类继承了类C,实现了接口I,而且C有一个方法,而且跟I中的一个提供默认方法的方法是重载兼容的。在这种情况下,C中的方法会优先于I中的默认方法,甚至C中的方法是抽象的时候,仍然是优先的。
publicclassDefaultMethods{ publicstaticvoidmain(String[]ar){ Interfaxeimpl=newNormalInterfaceImpl(); impl.defaultMethod(); } } classParentClass{ publicvoiddefaultMethod(){ System.out.println("->ParentClass"); } } interfaceInterfaxe{ publicvoiddefaultMethod()default{ System.out.println("->Interfaxe"); } } classNormalInterfaceImplextendsParentClassimplementsInterfaxe{}
输出是:
->ParentClass
第二个例子是,我的类实现了两个不同的接口,但是两个接口中都提供了相同的具有默认实现的方法的声明。在这种情况下,编译器将会搞不清楚怎么回事,实现类必须选择两个的其中一个实现。这可以通过如下的方式来使用super来搞定。
publicclassDefaultMethods{ publicstaticvoidmain(String[]ar){ FirstInterfaceimpl=newNormalInterfaceImpl(); impl.defaultMethod(); } } interfaceFirstInterface{ publicvoiddefaultMethod()default{ System.out.println("->FirstInterface"); } } interfaceSecondInterface{ publicvoiddefaultMethod()default{ System.out.println("->SecondInterface"); } } classNormalInterfaceImplimplementsFirstInterface,SecondInterface{ publicvoiddefaultMethod(){ SecondInterface.super.defaultMethod(); } }
输出是:
->SecondInterface
现在,我们已经看完了Java 闭包的介绍。这个文章中,我们接触到了FunctionalInterfaces 和JavaClosure,理解了Java的Lambda表达式,方法引用和构造方法引用。而且我们也写出了Lambda表达式的HelloWorld例子。
JavaSE8很快就要到来了,我将很高兴的拥抱这些新特性,也许这些新特性还是有些迷惑不清,但是我相信,随着时间的推移,会变得越来越好。