行为参数化:让方法接受多种行为作为参数,并在内部使用,来完成不同的行为。
行为参数化可让代码更好地适应不断变化的要求,减轻未来的工作量。
传递代码,就是将新行为作为参数传递给方法。但在Java 8之前这实现起来很啰嗦。为接口声明许多只用一次的实体类而造成的啰嗦代码(策略设计模式),在Java 8之前可以用匿名类来减少。
Lambda是一个匿名函数,我们可以把Lambda表达式理解为是一段可以传递的代码(将代码像数据一样进行传递)。
Lambda表达式本质上就是一个只有一个函数的匿名内部类,Lambda表达式提供了其简写方式,就是个语法糖。
可以把Lambda表达式看作匿名功能,它基本上就是没有声明名称的方法,但和匿名类一样,它也可以作为参数传递给一个方法。
可以把Lambda表达式理解为简洁地表示可传递的匿名函数的一种方式:它没有名称,但它有参数列表、函数主体、返回类型,可能还有一个可以抛出的异常列表。
MyFunction mf1=(int x)->{return x+1;};
MyFunction mf2=(x)->{return x+1;};
MyFunction mf3= x ->{return x+1;};
MyFunction mf4= x -> x+1;
//无参数,无返回值
()->System.out.println("hello");
//有一个参数,并且无返回值
(x)-> System.out.println(x);
//Lambda体中有多条语句,必须使用大括号
(x,y)->{
System.out.println("hello");
return Integer.compare(x,y);
};
//若Lambda体中只有一条语句,return和大括号都可以不写
(x,y)->Integer.compare(x,y);
函数式接口就是只定义一个抽象方法的接口。
只有在接受函数式接口的地方才可以使用Lambda表达式
接口现在还可以拥有默认方法(即在类没有对方法进行实现时,其主体为方法提供默认实现的方法)。哪怕有很多默认方法,只要接口只定义了一个抽象方法,它就仍然是一个函数式接口。
用函数式接口可以干什么呢?Lambda表达式允许你直接以内联的形式为函数式接口的抽象方法提供实现,并把整个表达式作为函数式接口的实例(具体来说,是函数式接口一个具体实现的实例)。你用匿名内部类也可以完成同样的的事情,只不过比较笨拙。
Lambda表达式可以被赋给一个变量,或传递给一个接受函数式接口作为参数的方法。当然这个Lambda表达式的签名要和函数式接口的抽象方法一样。
如果你用@FunctionalInterface定义了一个接口,而它却不是函数式接口的话,编译器将返回一个提示原因的错误。
Java 8的库设计师在java.util.function包中引入了几个新的函数式接口。
java.util.function.Predicate
接口定义了一个名叫test的抽象方法,它接受泛型T对象,并返回一个boolean。
java.util.function.Consumer
定义了一个名叫accept的抽象方法,它接受泛型T的对象,没有返回(void)。
java.util.function.Function
接口定义了一个叫做apply的方法,它接受一个泛型T的对象,并返回一个泛型R的对象。
Java类型要么是引用类型,要么是原始类型。但是泛型只能绑定到引用类型。在Java里有一个将原始类型转换为对应的引用类型的机制。这个机制叫做装箱。相反的操作,也就是将引用类型转换为对应的原始类型,叫做拆箱。
Java还有一个自动装箱机制来帮助程序员执行这一任务:装箱和拆箱操作时自动完成的。
但这在性能方面是要付出代价的。装箱后的值本质上就是把原始类型包裹起来,并保存在堆里。因此装箱后的值需要更多的内存,并需要额外的内存搜索来获取被包裹的原始值。
Java 8为我们前面所说的函数式接口带来了一个专门的版本,以便在输入和输出都是原始类型时避免自动装箱的操作。
public interface IntPredicate{
boolean test(int t);
}
一般来说,针对专门的输入参数类型的函数式接口的名称都要加上对应的原始类型前缀,比如DoublePredicate、IntConsumer、LongBinaryOperator、IntFunction等。
任何Java 8中自带的函数式接口都不允许抛出受检异常(checked exception)。如果你需要Lambda表达式来抛出异常,有两种办法:定义一个自己的函数式接口,并声明受检异常,或者把Lambda包在一个try/catch块中。
public interface BufferedReaderProcessor{
String process(BufferedReader b)throws IOException;
}
Function f=(BufferedReader b)->{
try{
return b.readLine();
}catch(IOException e){
throw new RuntimeException(e);
}
};
Lambda的类型是从使用Lambda的上下文推断出来的。上下文(比如,接受它传递的方法的参数,或接受它的值的局部变量)中Lambda表达式需要的类型称为目标类型。
如果Lambda表达式抛出一个异常,那么抽象方法所声明的throws语句也必须与之匹配。
同一个Lambda表达式可以与不同的函数式接口联系起来,只要它们的抽象方法前面能够兼容。
特殊的void兼容规则
如果一个Lambda的主体是一个语句表达式,它就和一个返回void的函数描述符兼容(当然需要参数列表也兼容)。
//尽管List的add方法返回了一个boolean,而不是Consumer上下文(T->void)所要求的void,也是合法的
Consumer b=s->list.add(s);
迄今为止所介绍的所有Lambda表达式都只用到了其主体里面的参数。但Lambda表达式也允许使用自由变量(不是参数,而是在外层作用域中定义的变量),就像匿名类一样。它们被称为捕获Lambda。例如,下面的Lambda捕获了portNumber变量。
int portNumber=1337;
Runnable r=()->System.out.println(portNumber);
Lambda可以没有限制的捕获(也就是在其主体中引用)实例变量和静态变量。但局部变量必须显式声明为final,或事实上是final。Lambda表达式只能捕获指派给它们的局部变量一次。(捕获实例变量可以被看作捕获最终局部变量this).
对局部变量的限制
为什么局部变量有这些限制。第一,实例变量和局部变量背后的实现有一个关键不同。实例变量都存储在堆中,而局部变量则保存在栈上。如果Lambda可以直接访问局部变量,而且Lambda是在一个线程中使用的,则使用Lambda的线程,可能会在分配该变量的线程将这个变量收回之后,去访问该变量。因此,Java在访问自由局部变量时,实际上是在访问它的副本,而不是访问原始变量。如果局部变量仅仅赋值一次那就没什么区别了—因此就有了这个限制。
第二,这一限制不鼓励你使用改变外部变量的典型命令式编程模式(这种模式会阻碍很容易做到的并行处理)。
闭包
闭包就是一个函数的实例,且它可以无限制地访问那个函数的非本地变量。例如,闭包可以作为参数传递给另一个函数。它也可以访问和修改其作用域之外的变量。
Java 8的Lambda和匿名类可以做类似于闭包的事情:它们可以作为参数传递给方法,并且可以访问其作用域之外的变量。但有一个限制:它们不能修改定义Lambda的方法的局部变量的内容。这些变量必须是隐式最终的。可以认为Lambda是对值封闭,而不是对变量封闭。这种限制存在的原因在于局部变量保存在栈上,并且隐式表示它们仅限于其所在线程。如果允许捕获可改变的局部变量,就会引发造成线程不安全的新的可能性,而这是我们不想看到的(实例变量可以,因为它们保存在堆中,而堆是在线程之间共享的)。