闭包
关于闭包的概念,我引用wiki的术语,大家先试着理解一下:
闭包,又称词法闭包,函数闭包,是引用了自由变量的函数。这个被引用的自由变量和函数一同存在,即使已经离开了创造它的环境也不例外。所以有另一种说法认为闭包是由函数和其相关的引用环境组成的实体。闭包在运行时可以有多个实例,不同的引用环境和相同的函数组合可以产生不同的示例。
在一些语言中,在函数中可以(嵌套)定义另一个函数时,如果内部的函数引用了外部的函数的变量,则可能产生闭包。运行时,一旦外部的函数被执行,一个闭包就形成了,闭包中包含了内部函数的代码,以及所需外部函数中的变量的引用。其中所引用的变量称作上值(upvalue)。
太过术语化的东西,初学者很难理解,通病。
我对闭包的理解主要包括两点:
- 闭包就是将函数内部和函数外部连接起来的一座桥梁,让外界可以读取到函数内部的变量,获取引用。
- 可以将函数内部的变量值<自由变量>一直保持在内存中。
这两点也是闭包的主要用途。更通俗易懂的讲,闭包就是一个函数,它了解周围都发生了什么,直到它没用了,也可以说闭包=函数+引用环境。
闭包在函数式编程语言中被大量使用,接下来,我们来讨论JavaScript中的闭包。
Javascript中的闭包
官方定义:
闭包是一个拥有许多变量和绑定了这些变量的环境的表达式(通常是一个函数),因而这些变量也是该表达式的一部分
参考下面这段js代码:
function a() {
var i = 0;
function b() {
alert(++i);
}
return b;
}
var c = a();
c();
这样在执行完var c=a()后,变量c实际上是指向了函数b,b中用到了变量i,再执行c()后就会弹出一个窗口显示i的值(第一次为1)。这段代码其实就创建了一个闭包,为什么?因为函数a外的变量c引用了函数a内的函数b,就是说:
当函数a的内部函数b被函数a外的一个变量引用的时候,就创建了一个我们通常所谓的“闭包”。
上面这段代码形象的体现了上面提到的闭包的两个主要用途:
- 内部函数b让函数a外界可以读取到函数a的局部变量。将函数内部和函数外部连接起来。
- 执行完var c = a()后,c实际指向了函数b,而b又依赖函数a中的变量i,这就使得垃圾回收机制不会回收a所占用的资源。**将函数内部的变量值一直保持在内存中。**如果b没有返回给a的外界,只是被a所引用,这时候a和b互相引用但又不被外界打扰,这时候垃圾回收机制就会回收。
使用闭包的注意点:
- 由于闭包会使得函数中的变量都被保存在内存中,内存消耗很大,所以不能滥用闭包,否则会造成网页的性能问题,在IE中可能导致内存泄露。解决方法是,在退出函数之前,将不使用的局部变量全部删除。
- 闭包会在父函数外部,改变父函数内部变量的值。所以,如果你把父函数当作对象(object)使用,把闭包当作它的公用方法(Public Method),把内部变量当作它的私有属性(private value),这时一定要小心,不要随便改变父函数内部变量的值。
虽然闭包在函数编程语言中被大量使用,下面我们讨论java中如何实现闭包。
行为参数化(behavior parameterization)
行为参数化是一种软件开发模式,可以帮助你处理频繁变更的软件需求。可以将一段代码块当做参数传递给另一个方法,这样便可以使得方法的行为由传入的代码块决定。
下面,我们借用网上一个例子来感知行为参数化的明显优势:
农场主有一个挑选农场中的水果的需求。
转载:[转载](https://yemengying.com/2016/02/20/Java-8-%E8%A1%8C%E4%B8%BA%E5%8F%82%E6%95%B0%E5%8C%96-behavior-parameterization/)
Version1:挑选出绿色苹果:
实现如下:
public static List<Apple> filterGreenApples(List<Apple> inventory) {
List<Apple> result = new ArrayList<>(); // An accumulator list for apples
for (Apple apple : inventory) {
if ( "green".equals(apple.getColor())) {
// Select only green apples
result.add(apple);
}
}
return result;
}
Version2:农场主又想要挑选其他颜色的苹果。
这还不简单,把上面的方法复制一遍,把green改成其他的颜色。但是颜色有很多种,没办法都复制,这时候我们就要试着抽象一下:将颜色作为参数。
实现如下:
public static List<Apple> filterApplesByColor(List<Apple> inventory, String color) {
List<Apple> result = new ArrayList<>();
for (Apple apple: inventory) {
if (apple.getColor().equals(color)) {
result.add(apple);
}
}
return result;
}
Version3:农场主又想按照重量挑选苹果
这也简单呀,把上面的按照颜色过滤的方法复制一遍,把color改成weight,再调整一下if条件就ok了,
实现如下:
public static List<Apple> filterApplesByWeight(List<Apple> inventory, int weight) {
List<Apple> result = new ArrayList<>();
for (Apple apple: inventory) {
if (apple.getWeight()>weight) {
result.add(apple);
}
}
return result;
}
但是,如果农场主还要换按照其他属性进行筛选,如果还复制的话那就违背了DRY(don’t repeat yourself))原则,再次试着抽象:
Version4:我们将过滤的标准抽象出来。
首先,定义一个接口作为抽象的标准选择。
public interface ApplePredicate {
boolean test(Apple apple);
}
然后,定义多个ApplePredicate接口的实现类代表不同的过滤策略:
//select only heavy apple
public class AppleHeavyWeightPredicate implements ApplePredicate {
public boolean test(Apple apple) {
return apple.getWeight() > 150;
}
}
//select only green apple
public class AppleGreenColorPredicate implements ApplePredicate {
public boolean test(Apple apple) {
return "green".equals(apple.getColor);
}
}
上面每一个实现了ApplePredicate接口的类都代表了一种筛选策略。在此基础上,我们可以将筛选方法修改成下面的样子,将ApplePredicate作为参数传入:
public static List<Apple> filterApples(List<Apple> inventory, ApplePredicate p) {
List<Apple> result = new ArrayList<>();
for (Apple apple : inventory) {
if (p.test(apple)) {
result.add(apple);
}
}
return result;
}
在这种方式中,行为参数化的本质是通过传递对象来传递方法。
这样似乎方便了很多,但是每增加一个过滤策略都要增加一个实现类,我们继续来优化:
Version5:使用匿名内部类:
List<Apple> redApples = filterApples(inventory, new ApplePredicate() {
public boolean test(Apple apple) {
return "red".equals(apple.getColor());
}
});
匿名内部类提供了一个临时的实现,解决了为一个接口实现多个实现类的问题,但是匿名类的代码看起来可读性差,难理解,在下面我们会讨论java8的新特性Lambda表达式,用Lambda表达式会让代码看起来更简单,易读。
Lambda表达式
这里,请读者先跟着笔者的思路走一下,我们先看如何使用Lambda表达式,然后再讨论Java里面为什么要引入Lambda表达式。
语法
Lambda表达式通常使用(argument) -> (body)语法书写。
(arg1, arg2...) -> { body }
(type1 arg1, type2 arg2...) -> { body }
结构
- 一个 Lambda 表达式可以有零个或多个参数。
- 参数的类型既可以明确声明,也可以根据上下文来推断。例如:(int a)与(a)效果相同。
- 所有参数需包含在圆括号内,参数之间用逗号相隔。例如:(a, b) 或 (int a, int b) 或 (String a, int b, float c)。
- 空圆括号代表参数集为空。例如:() -> 42。
- 当只有一个参数,且其类型可推导时,圆括号()可省略。例如:a -> return a*a。
- Lambda 表达式的主体可包含零条或多条语句。
- 如果 Lambda 表达式的主体只有一条语句,花括号{}可省略。匿名函数的返回类型与该主体表达式一致。
- 如果 Lambda 表达式的主体包含一条以上语句,则表达式必须包含在花括号{}中(形成代码块)。匿名函数的返回类型与代码块的返回类型一致,若没有返回则为空。
举例
线程可以通过以下方法初始化:
//旧方法:
new Thread(new Runnable() {
@Override
public void run() {
System.out.println("Hello from thread");
}
}).start();
//新方法:
new Thread(
() -> System.out.println("Hello from thread")
).start();
在java中,Lambda表达式是对象,所以他必须依附于一类特别的对象类型,函数式接口。接下来,我们来讨论什么是函数式接口。
函数式接口
有且只有一个抽象方法的接口被称为函数式接口,函数式接口适用于函数式编程的场景,Lambda表达式就是Java中函数式编程的体现,可以使用Lambda表达式创建一个函数式接口的对象,一定要确保接口中有且只有一个抽象方法,这样Lambda才能顺利的进行推导。
@FunctionInterface注解
与@Override 注解的作用类似,Java8中专门为函数式接口引入了一个新的注解:@FunctionalInterface 。该注解可用于一个接口的定义上,一旦使用该注解来定义接口,编译器将会强制检查该接口是否确实有且仅有一个抽象方法,否则将会报错。但是这个注解不是必须的,只要符合函数式接口的定义,那么这个接口就是函数式接口。
static方法
java8中为接口新增了一项功能,定义一个或者多个静态方法。用法和普通的static方法一样。注意:实现接口的类或者子接口不会继承接口中的静态方法。
default方法
java8在接口中新增default方法,是为了在现有的类库中中新增功能而不影响他们的实现类,试想一下,如果不增加默认实现的话,接口的所有实现类都要实现一遍这个方法,这会出现兼容性问题,如果定义了默认实现的话,那么实现类直接调用就可以了,并不需要实现这个方法。注意:如果接口中的默认方法不能满足某个实现类需要,那么实现类可以覆盖默认方法。不用加default关键字,
函数式接口的用途
- 函数式接口是为了满足java8函数式编程的特性而诞生的产物。
- 函数式接口中增加了静态方法和默认方法的实现,让开发者可以直接调用,让编程变的更加简单
在上面提到的,在函数式编程语言中,函数至高无上,Lambda表达式是函数,而在java中,对象至高无上,所以Lambda表达式是对象,所以它必须依附于函数式接口这一特殊的对象类型,Lambda表达式就是函数式接口的实例,也就是说,只要一个对象是函数式接口的实例,那么该对象就可以用Lambda表达式来表示。
总结:
为了引入函数式编程的特点,java8提供了两个东西:函数式接口和Lambda表达式,这两者结合起来近似的在java中实现了闭包。
匿名内部类和闭包
在上篇文章内部类详解中,我们知道匿名内部类中如果希望使用外部定义的对象,那么编译器要求其参数引用是final的。
不知道大家在这里是否有疑问呢?
这便是java中实现的不完全闭包。匿名内部类有权限访问其外围类的所有元素,这是因为当外围类创建了一个内部类时,内部类会隐式的持有一个对外围类对象的引用,他是通过这个引用来访问外围类的成员的。而匿名内部类又是如何访问到其包裹着它的方法的final变量呢,这正是java编译器将外部环境的final变量复制了一份到内部类里面,用final修饰使得内部类和他的依赖环境的局部变量保持不变。
Lambda表达式使得匿名内部类的书写更为简单。
Lambda和匿名类
区别
- 匿名内部类可以为任意接口创建实例,可以为抽象类甚至普通类创建实例,但是Lambda表达式只能为函数接口创建实例。
- 匿名内部类实现的抽象方法可以调用接口中定义的默认方法,但lambda表达式不允许调用接口的默认方法。
- 在匿名类中,使用this关键字指的是该匿名类,但是在lambda表达式中使用this关键字与在lambda表达式之外使用没有区别,因为lambda表达式中,指的是lambda表达式的外围类。
最后,我们再用Lambda表达式改进一下行为化参数提到的例子,使得代码更简单易读:
实现如下:
List<Apple> redApples = filterApples(inventory, new ApplePredicate() {
public boolean test(Apple apple) {
return "red".equals(apple.getColor());
}
});
完结
我们以闭包为引线,讨论了java8新增加的几个特性,行为化参数,Lambda表达式,函数式接口,Lambda表达式和函数式接口结合起来在java里面实现了不完整的闭包。下篇文章中,我们趁热打铁,继续讨论Stream流,将上面的例子实现最终版本。