由匿名内部类到Lambda
看看遍历一个List的不同方法
public class Main {
public static List<String> myList;
static {
myList = new ArrayList<String>();
myList.add("AAA");
myList.add("BBB");
myList.add("CCC");
myList.add("DDD");
}
public static void main(String[] args) {
//1.直接进行遍历
for (int i = 0; i < myList.size(); ++i) {
System.out.println("myList: " + myList.get(i));
}
for (String tmp : myList) {
System.out.println("myList: " + tmp);
}
/**************************************************************/
//2.使用匿名内部类
myList.forEach(new Consumer<String>() {
@Override
public void accept(String tmp) {
System.out.println("myList: " + tmp);
}
});
/**************************************************************/
//3.使用Lambda
//lambda完整版:(参数) -> {代码块}
myList.forEach((String s) -> System.out.println("myList: " + s));
//由于myList里面的参数肯定是String类型的,所以可以不用申明s为String类型的
myList.forEach((s) -> System.out.println("myList: " + s));
//由于只有一个参数,所以还可以不用加括号
myList.forEach(s -> System.out.println("myList: " + s));
//由于该参数其实是来自于myList,还可以这么写
myList.forEach(System.out::println);
}
}
首先,先看看forEach方法。
通过层层继承,List获得了Iterable接口中的forEach方法。
//List接口继承了Collection接口
public interface List<E> extends Collection<E> {
......
}
//Collection接口继承了Iterable接口
public interface Collection<E> extends Iterable<E> {
......
}
//在Iterable接口中定义了forEach的默认实现
public interface Iterable<T> {
......
default void forEach(Consumer<? super T> action) {
Objects.requireNonNull(action);
for (T t : this) {
action.accept(t);
}
}
}
分析forEach方法
default void forEach(Consumer<? super T> action) {
Objects.requireNonNull(action);
for (T t : this) {
action.accept(t);
}
}
forEach方法传入的是一个Consumer类型的参数,Consumer是一个接口,接口不能直接实例化,但是接口的引用可以指向实现它的某个子类
。也就是说,这里传入的参数实际上是一个继承了Consumer接口的子类的实例,并且这个子类一定实现了Consumer中的抽象方法
。
Consumer是Java1.8新增的一个接口,里面只有一个抽象方法accept()
以及一个拥有默认实现的方法。
@FunctionalInterface
public interface Consumer<T> {
void accept(T t);
default Consumer<T> andThen(Consumer<? super T> after) {
Objects.requireNonNull(after);
return (T t) -> { accept(t); after.accept(t); };
}
}
再回到forEach方法,在forEach方法中遍历List,并通过action对象来调用accept方法,action是一个实例,该实例对应的类实现了accept方法,而我们所要做的事情其实就是实现accept方法,在accept方法中指定在遍历List的过程中,我们究竟要做些什么操作。
for (T t : this) {
action.accept(t);
}
回顾一下,这三种遍历myList的不同方法。
-
方法一是直接进行遍历,在遍历中操作myList对象。相当于是在方法中直接操作对象。
-
方法二是使用匿名内部类创建一个Consumer类型的实例,然后在实例中实现accept方法,最后再将这个实例作为一个参数传入forEach方法中。
我们习惯的编程思想是:如果我们要操作某个对象,我们会在代码块里直接操作,然后将其封装成一个方法,然后调用该方法即可完成对该对象的操作
。
第二种遍历的方法则是一种截然不同的思路。如果我们要操作某个对象,首先会在对象内部预留一个“空的方法”,这个“空的方法”就像是一个占位符一样,在该方法内部并没有指定对象具体需要做什么,当我们想对该对象进行操作的时候,就可以将相应的代码块传入,填在这个"空的方法"里,然后对象调用这个方法就可以完成具体的操作了
。
可以对比这张图来区分这两种不同的编程思想。
在遍历myList的第二种方法里,我们使用了匿名内部类传入了一个对象,在该对象内实现了accept方法,accept方法内的代码就是我们希望在遍历myList的时候所要完成的操作
。但是实际上,我们根本不需要这个对象,我们所需要的仅仅只是这个对象里的方法
。但是Java是面向对象编程的,类才是第一等公民,任何操作都得依靠类来完成,我们不能直接调用方法,我们必须创建一个对象,然后通过Object.method()
的形式来调用方法,如果是静态方法,我们同样也需要类名.method()
的形式来进行调用。由于方法不能单独存在,方法必须封装在一个对象里,因此我们要想往forEach方法中传入代码块,就必须得将方法封装在一个对象里。尽管这样看起来有些臃肿,但是在Java里却不得不这么做。
针对这样的问题,我们再来看遍历myList的第三种方法,这便是Lambda,Lambda是一种编程思想——函数式编程。在函数式编程的语言中,函数才是第一等公民,函数可以单独存在,函数可以当作参数传递,也可以当作返回值。
//2.使用匿名内部类
myList.forEach(new Consumer<String>() {
@Override
public void accept(String tmp) {
System.out.println("myList: " + tmp);
}
});
//3.使用Lambda
//lambda完整版:(参数) -> {代码块}
myList.forEach(s -> System.out.println("myList: " + s));
对比使用匿名内部类和Lambda表达式,Lambda表达式显然更加简洁,我们不需要创建一个对象,然后将方法封装在对象里来进行传递,我们可以直接将要执行的代码块当作参数传入。
Lambda的使用
单个参数的情况
还是以myList的遍历为例
这里的s其实就是从myList中依次取出的值,由于我们定义myList的时候就指定了它里面所存储的数据类型:List<String> myList
。所以这个参数s就不需要定义它的类型,它一定和myList中存储的参数的数据类型一样。
String outside = "myList:";
myList.forEach((s) -> System.out.println(outside + s));
进一步,我们可以将 System.out.println("myList: " + s)
封装在一个方法里,假如我们封装在了public void process(String str)
这个方法里,且该方法与myList.forEach
位于同一个类里,则可以这么调用:
String outside = "myList:";
myList.forEach(s -> process(outside + s));
在上面的例子中,我们将外部的变量outside
传入了Lambda表达式中,如果我们的process方法中不需要外部的变量,所需要的参数仅仅来自于myList中。我们还可以这么简写
- 假设
public void process(String str)
位于LambdaTest
这个类中,则可以这么调用。
LambdaTest test = new LambdaTest();
myList.forEach(test::process);
- 若process是静态方法,还能这么调用
myList.forEach(LambdaTest::process);
两个参数的情况
Map<String, String> myMap = new HashMap<>();
myMap.put("k1", "v1");
myMap.put("k2", "v2");
myMap.put("k3", "v3");
// 两个参数也没问题,把参数用括号扩起来,用逗号分开
myMap.forEach((k, v) -> processTwo(k, v));
// 省略也没问题,这里假设processTwo为LambdaTest中的一个静态方法
myMap.forEach(LambdaTest::processTwo);
使用Stream进行流式处理
以上面的myList为例。
myList.stream().filter(s -> s.length() > 4).map(String::toUpperCase).forEach(System.out::println);
上面这段代码的意思就是:取出myList中的每一个元素,过滤掉length > 4的元素,然后将过滤后的元素转化为全大写,然后依次输出每一个。这一气呵成的处理便是Stream
,就像是水流动一样。
collect的作用则是将元素又收集起来,转化为一个Collection集合。
List<String> longgerStrList = myList.stream().filter(s -> s.length() > 4)
.map(String::toUpperCase).collect(Collectors.toList());
使用Lambda需要注意的点
- Lambda可以有返回值和异常,得具体看对应的接口中的抽象方法有没有返回值或者是有没有抛出异常。对应到myList.forEach,里面所需要实现的方法其实是
Consumer<T>
接口中的void accept(T t);
方法,该方法并没有返回值,因此Lambda的表达式中就不能有表达式。该方法的签名中也没抛出异常,因此不需要抛出异常。 - lambda 可以取代只有一个抽象方法的接口,因为在使用Lambda的时候,我们只管往里面传入要执行的代码,并没有指定这段代码是要覆盖接口中的哪个方法,如果接口中有多个抽象方法,则究竟覆盖谁就无法抉择了,因此只能在有一个抽象方法的接口中使用。