Java语言十五讲(第十讲 Lambda 表达式)

我们要从匿名类开始讲起,一点点引出Lambda表达式。我比较喜欢Lambda这个词,显得比较有学问似的,一笑。
我讲过,技术点不是孤立的,它们之间是有关联的,按照某种层次结构关联在一起就构成一个体系。我们在学习某个技术的时候,要了解它的来龙去脉,把某个技术点放在整体中学习会更有收获。事实上,学术论文一般都要求开头一段讲学术史,这是有道理的。当然,有个别天才能在不引用任何参考文献的情况下提出划时代的理论,横空出世。最著名的就是爱因斯坦在1905年写的《论动体的电动力学》。
以前提到过Inner Class内部类中有一种是可以不用起名字的,称之为匿名类。使用匿名类是因为我们有些场景中不需要知道名字,也不关心它的名字,而是关心它里面的方法,这在事件响应模型中会常用。
先看一个例子吧,先用普通写法,代码如下:

interface Adder { 
    int add(int x, int y); 
} 
class MyAdder implements Adder { 
    @Override
    public int add(int x, int y) { 
        return (x+y); 
    } 
} 
class AdderDemo { 
    public static void main(String[] args) { 
        MyAdder adder = new MyAdder(); 
    System.out.println(adder.add(21,37)); 
    } 
} 

程序简单,定义了一个interface,一个MyAdder class实现它,用AdderDemo测试。运行无误。我们看,其实概念上可以不显示声明一个MyAdder类的,我们可以在使用的时候直接弄一个,这样程序更加简洁。
看下面这个匿名类版本,代码如下:

interface Adder { 
    int add(int x, int y); 
} 
class AdderDemo { 
    public static void main(String[] args) { 
        Adder adder = new Adder() { 
               @Override
               public int add(int x, int y) { 
                   return (x+y); 
               } 
           }; 
        System.out.println(adder.add(21,37)); 
    } 
} 

这次,我们没有显示声明一个MyAdder类,而是在使用的时候临时创建了一个对象,而这个对象依赖的类是没有名字的,临时定义的。这种写法比原始版本要简洁了。
我们再仔细看这个接口,它就只有一个add方法,我们用这个接口其实是想用这个方法。这种情况,我们叫它functional interface函数式接口。有一种更加简洁的写法,见下面的代码(Test1.java):

public class Test1 {
    public static void main(String[] argv) {
        engine((int x, int y)-> {return x + y;});
        engine((x, y)-> {return x + y;});
        engine((x,y)-> x + y);
    }
    private static void engine(Calculator calculator){
        int x = 4, y = 2;
        int result = calculator.calculate(x,y);
        System.out.println(result);
    }
}
@FunctionalInterface
interface Calculator{
  int calculate(int x, int y);
}

上面的代码中,我们定义了一个Calculator接口,里面就一个calculate方法,这是典型的函数式接口。使用的程序中,我们提供一个engine方法,它以Calculator为参数,调用接口中的calculate方法。
再看对engine方法的调用,仔细看一看,现在不是生成一个匿名类的写法了,而是写成了(int x, int y)-> {return x + y;},比较一下匿名类里面的写法,似乎就是把方法体拷贝过来了,省略了接口名和方法名。概念上这么理解是对的。这里就用到了Lambda表达式,只显示定义参数和方法体。实际上还可以进一步简化成engine((x, y)-> {return x + y;});以及engine((x,y)-> x + y);。
这种写法是把实现方法的函数体当成参数使用了,跟传一个普通变量一样。我们要把上面的程序略加增强就能更加清晰地看出来了,把上面main()中的engine((x,y)-> x + y);扩充成四个语句:

engine((x,y)-> x + y);
engine((x,y)-> x - y);
engine((x,y)-> x * y);
engine((x,y)-> x / y);

你们运行一下看结果。我们把+-/操作当成了方法参数传进去并运行。所以这种机制的作用是干这个,并不只是简单的简化代码的编写。有一个术语叫“code as data”就是说的这件事情。 Lambda表达式就是让你能够实现上面的code as data机制而规定的一种写法。Lambda 表达式给Java中引入了操作符-> ,表达式分成两部分: (n) -> nn
左边的就是表达式的参数,如果没有参数,就用一个空括号。右边的就是操作语句,操作语句不仅仅只有一个数学表达式,它可以是任意的程序语句。如:

    RevString revStr = (str) -> {
        String result = "";

        for(int i = str.length()-1; i >= 0; i--)
            result += str.charAt(i);

        return result;
    };

Lambda表达式不是Java的发明,实际上大家千呼万唤,才姗姗来迟纳入到Java8中。自然,延迟主要的原因是因为Java命运坎坷,被Oracle收购,中间折腾了好几年,早就计划好的事情迟迟不能实现。
Lambda表达式是一个匿名函数,Lambda表达式基于数学中的λ演算得名,直接对应于其中的lambda抽象(lambda abstraction),即没有函数名的函数。在数学和工程领域使用的比较多,是科技工作者的心头好,最主要的是简洁显得有geek范。在别的许多语言中,早就支持了Lambda表达式,如C#和Javascript。
函数式语言提供了一种强大的功能:闭包。闭包是一个可调用的对象,它使用和处理一些信息,这些信息来自于创建它的作用域。Java 现在提供的最接近闭包的概念便是 Lambda 表达式。Java里面没有独立函数,因此,为了实现函数式编程,就用了函数式接口进行包装,一个有且仅有一个方法的接口。通过这种退化的方式模拟独立函数。有人却嘲讽Java是以名词为中心的,也确实是这样的,其实所有面向对象的语言都是以名词为中心的,RESTful风格的接口也是名词为中心的。从技术哲学角度,这个好不好,见仁见智,我个人觉得要以名词为中心辅助以动词。Java里面名词中心主义色彩确实太重了,正如这个物理世界,除了实体,还有实体之间的相互作用就是力,Java里面缺乏这个。AOP的思想有一点这个意思,能够穿透实体,加上装饰(Docoration/Advice),统一做某些事情。但是这是架构层面实现的,语言本身的层面没有提出来。比较起人类自己用的科学语言来,计算机语言还有很大的不足。虽然我们现在没有办法用一个方程描述世界,但是几大支柱概念都已经建立起来了,particle粒子,filed场,force力,dimension维度,symmetry对称性。

再看一个在list中使用Lambda表达式的例子,代码如下(Test.java):

public class Test{
    public static void main(String[] argv) {
        ArrayList<Integer> al = new ArrayList<Integer>(); 
        al.add(1); 
        al.add(2); 
        al.add(3); 

        al.forEach(n -> System.out.println(n)); 
    }
}

上面代码对arraylist中的每一个元素,执行打印动作。
如果我们的好奇心再强一点,看看编译之后的结果,Java究竟怎么处理的上面的Lambda表达式,我们可以试着反编译一下看结果,我们会看到编译器自动生成了一个私有方法,然后使用Lambda表达式的地方都会改成动态调用这个私有方法,形如:

  private static void Test$main$0(Integer n) {
      System.out.println(n);
  }

你们可以自己试一下,不过不同的编译器结果可能会有不同。

把Lambda表达式当成方法引用的时候,还可以进一步用::双冒号简化代码:

al.forEach(System.out::println);

jdk8中使用了::的用法。就是把方法当做参数,每个元素都传入到该方法里面执行一下。代码进一步简化,生人也更难理解了。科技就是这样,符号用得越多越怪就越有学问。

Lambda表达式有一个限制,它不能是Generic泛型的。但是我们结合一下泛型接口有可以实现不同类型的操作。举一个例子,代码如下(Test2.java):

public class Test2 {
  public static void main(String[] argv) {
    Adder adder = (int x, int y)-> {return x + y;};
    System.out.println(adder.add(12,34));
  }
}
@FunctionalInterface
interface Adder{
  int add(int x, int y);
}

这个例子中用到了一个add方法,是用于把两个整型相加的。如果我们想把两个字符串相加就做不到了,Lambda表达式不支持泛型。我们来看怎么用泛型改造上面的代码:

public class Test2 {
  public static void main(String[] argv) {
    Adder<Integer> adder = (Integer x, Integer y)-> {return x + y;};
    System.out.println(adder.add(12,34));

    Adder<String> adder2 = (String x, String y)-> {return x.concat(y);};
    System.out.println(adder2.add("12","34"));
  }
}
@FunctionalInterface
interface Adder<T>{
  T add(T x, T y);
}

先把接口改成泛型,再在用Lambda表达式的时候指定不同类型。两者结合在一起就可以了。

Lambda 表达式改进了Collection库,更容易遍历,过滤。比如Comparator类是用来给list排序的,以前这么写:

Collections.sort(studentList, new Comparator<Student>(){
    public int compare(Student p1, Student p2){
        return p1.getName().compareTo(p2.getName());
    }
});

用Lambda表达式这么写,简化不少:

Collections.sort(studentList, (Student p1, Student p2) -> p1.getName().compareTo(p2.getName()));

数据过滤也是,我们现在能把数据集进行一系列处理了。看一个例子。
先定义一个Student类,代码如下(Student.java):

public class Student {
       private String name;
       private int score;

       public Student(String name, int score) {
          this.name = name;
          this.score = score;
       }   
       public String getName() {
          return name;
       }
       public int getScore() {
         return score;
       }
}

再写一个使用的类,是Student列表,进行过滤排序等操作。代码如下(StudentTest.java):

public class StudentTest {
    public static void main(String args[]) {
        Student s1 = new Student("Alice", 87);
        Student s2 = new Student("Bob", 57);
        Student s3 = new Student("Chris", 102);
        Student s4 = new Student("Donald", 110);
        Student s5 = new Student("Cohn", 108);

        List<Student> students = Arrays.asList(s1, s2, s3, s4, s5);

        students.stream()
        .filter(s -> s.getScore() > 100)
        .sorted((p1, p2) -> p1.getName().compareTo(p2.getName()))
        .collect(Collectors.toList())
        .forEach(System.out::println);
    }
}

上面的核心代码就是最后一句,我们用了一个Stream流水式写法,将列表先过滤再排序最后打印,通过.符号一步步流水执行。代码非常简洁,难怪成为数学家工程师们的最爱。
filter里面,我们用的Lambda表达式是s -> s.getScore() > 100,过滤分数超过100的数据。这是简单表达式,也可以用复杂点的,如s -> s.getScore() > 100 && s.getName().startsWith("C"),过滤分数超过100并且名字以C开头的数据。
sorted里面,我们用的Lambda表达式是(p1, p2) -> p1.getName().compareTo(p2.getName()),用getName拿到的名字字符串进行比较排序,因为String类里面提供了compareTo方法,我们直接使用。要是不指定这个表达式,写成sorted()会有什么效果?我们试一下,编译是不会出错的,但是运行结果有错(如果数据为空或者只有一条数据不会出错,因为这种情况下其实没有进行比较排序):

java.lang.ClassCastException: Student cannot be cast to java.lang.Comparable
    at java.util.Comparators$NaturalOrderComparator.compare(Unknown Source)

原因是sorted没有参数,系统拿着这个student不知道该怎么比较,这个时候,我们可以实现Comparable提供一个compareTo方法就可以了:

       public int compareTo(Student s2) {
              return this.getName().compareTo(s2.getName());
       }

说到重写,要再提一句,上面最后执行System.out::println的时候,或许出现的是类似这个结果:

Student@7ba4f24f
Student@3b9a45b3
Student@7699a589

那是因为println其实是调用的对象的toString()方法,我们没有自己给Student提供这个方法,就出这么一个结果,自己提供一个就可以了。综合上面的考虑,最后Student类代码变成这个样子,代码如下(Student.java):

public class Student implements Comparable<Student>{
       private String name;
       private int score;

       public Student(String name, int score) {
          this.name = name;
          this.score = score;
       }

       public String getName() {
          return name;
       }
       public int getScore() {
         return score;
       }
       public String toString() {
          return name + " " + score;
       }
       public int compareTo(Student s2) {
              return this.getName().compareTo(s2.getName());
       }
}

上面看了过滤和排序,还可以把list里面的数据做一个平方转换,示例如下:

List<Integer> list = Arrays.asList(1,2,3,4,5,6,7);
for(Integer n : list) {
    int x = n * n;
    System.out.println(x);
}

现在用Lambda表达式,简写成这样:

List<Integer> list = Arrays.asList(1,2,3,4,5,6,7);
list.stream().map((x) -> x*x).forEach(System.out::println);

以上就是Lambda的基本内容。大家自己多练习。

我们说过,用函数式编程,我们可以把函数本身当成参数,也就可以有返回函数的函数,我们把这种叫高阶函数,还可以用一种级联式写法串在一起。
看一段代码:

public stat int filterSum(List<Integer> values, Predicate<Integer> filter) {
   return values.stream()
    .filter(filter)
    .reduce(0, Integer::sum);  
}

这个函数filterSum接收另一个函数filter作为参数,并执行两部操作,一是执行filter,然后执行sum。如果是要求选择所有偶数,就这么用:filterSum(numbers, e -> e % 2 == 0);
函数可以接收函数、lambda 表达式或方法引用作为参数。同样地,函数也可以返回 lambda 表达式或方法引用。在此情况下,返回类型将是函数接口。
再看一段代码:

public static Predicate<Integer> isOdd() {
  Predicate<Integer> result = (Integer n) -> n% 2 != 0;
  return result ;
}

还可以更加简洁:return n-> n% 2 != 0;
这是返回一个判断数是否为奇数的函数。使用的时候这么用:

Predicate<Integer> check = isOdd();
check.test(4);

我们接着往下面看,怎么真的通过级联的方式把一个Lambda表达式写成一个函数调用。我们的例子就是筛选出被2整除,被3整除,被4整除,被5整除的数据。
自然我们可以这么写:

List<Integer> divide2 = nlist.stream().filter(n % 2 == 0).collect(toList());
List<Integer> divide3 = nlist.stream().filter(n % 3 == 0).collect(toList());
List<Integer> divide4 = nlist.stream().filter(n % 4 == 0).collect(toList());
List<Integer> divide5 = nlist.stream().filter(n % 5 == 0).collect(toList());

我们用了四个Lambda表达式,只是参数略有不同。我们知道内部其实是四个私有方法,我们不想这样,想用一个。
我们做一个函数:

      Function<Integer, Predicate<Integer>> divide = (Integer d) -> {
          Predicate<Integer> dividen = (Integer n) -> {
              return n % d == 0;
          };
          return dividen;
      };

如上,我们定义了一个叫divide的函数,参数是d,即除数(2,3,4,5)。内部是又定义了一个dividen,这个dividen就是一个Lambda表达式判断是否能整除。这里就涉及到两层级联。
整体代码如下(Test4.java):

public class Test4 {
  public static void main(String[] argv) {
      ArrayList<Integer> al = new ArrayList<Integer>(); 
      al.add(2); 
      al.add(3); 
      al.add(4); 
      al.add(5); 
      al.add(6); 
      al.add(7); 

      Function<Integer, Predicate<Integer>> divide = (Integer d) -> {
          Predicate<Integer> dividen = (Integer n) -> {
              return n % d == 0;
          };
          return dividen;
      };

      List<Integer> values = al.stream().filter(divide.apply(3)).collect(Collectors.toList());
      values.forEach(System.out::println);     
}
}

运行结果是把被3整除的数据打印出来了。
我们招收简化divide定义。明显的是参数的类型不需要重复写了,然后是内部的dividen其实是不需要显示定义的。这样简化成了:

      Function<Integer, Predicate<Integer>> divide = (d) -> {
          return (n) -> {
              return n % d == 0;
          }; 
      };

上面代码还可以简化,你看最里面这个return只有一行,所以可以省掉return。变成:

      Function<Integer, Predicate<Integer>> divide = (d) -> {
          return (n) -> n % d == 0;
      }; 

留下的这个Lambda表达式还是只有一行,所以再次简化成级联表示:

Function<Integer, Predicate<Integer>> divide = d -> n -> n % d == 0;

上面我们演示了怎么一步一步简化,洋葱一层层耐心地剥,代码一点点简化,头脑越来越明朗。自然,用到的不熟悉的写法和符号也会越来越多,刚才说过这样越来越显得有学问。虽然这么说是开玩笑,但是也不是完全无理。我们看看数学物理的发展史,不断引入新的符号演算,越来越看不懂。为什么会是这样?是为了显得高深而故弄玄虚吗?是科学家为了保饭碗故意这样吗?肯定不是这样。我个人的观点是这是抽象带来的必然结果。人类认识的进程是越来越抽象,不是为了玄虚,而是为了普适,用一条或者几条简单的规则来描述世界。最开头,人是没有抽象能力的,见到的东西都是不同的具体的,后来抽象出类别来,动的东西,不动的东西,固体液体气体,开始对各种相互作用也是分开描述,后来抽象出力这个概念,后来统一成几种力,天上地下一起描述。每一次抽象都会带来新的术语与算符,初看起来就越来越难以弄懂了,其实都是为了简化问题。我们学技术的人,要顺着这个路子往前走,做的程序不光是应对眼前的具体场景,要有普适性,通过抽象来简化问题。
Shakespeare莎士比亚在《Hamlet》写到:Brevity is the soul of wit(简洁是智慧的灵魂)。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值