Java 简洁代码利器 —— Lambda 表达式

1,671 阅读15分钟

Java 8 特性(Lambda 表达式 with Android)

[TOC]

参考资料

http://www.importnew.com/16436.html

http://www.infoq.com/cn/articles/Java-8-Lambdas-A-Peek-Under-the-Hood

https://github.com/bazelbuild/bazel/blob/master/src/tools/android/java/com/google/devtools/build/android/desugar

《Mastering Lambdas- Java Programming in a Multicore World》

http://www.lambdafaq.org/

Lambdas in Java: An In-Depth Analysis

http://www.javac.info/

浅谈Java 7的闭包与Lambda表达式之优劣

https://martinfowler.com/bliki/Lambda.html

A Definition of Closures

The Original 'Lambda Papers' by Guy Steele and Gerald Sussman

http://viralpatel.net/blogs/Lambda-expressions-java-tutorial/

https://stackoverflow.com/questions/21887358/reflection-type-inference-on-java-8-lambdas

历史

-2. 1997 年,内部类的加入

-1. 2006 年 8 月,Closures for Java,初步提出想法

-0.5. 2009年 4 月,Oracle 收购 Sun 公司

  1. 2009 年 12 月,正式提出想法: Project Lambda: The Straw-Man Proposal
  2. 2010 年 1 月, 0.1 ver
  3. 2010 年 2 月, 0.15 ver
  4. 2010 年 4 月,Neal Gafter 提出:Lambda 会赶不上 JDK 7 的发布(主要是不能达到发布的标准),于是延迟到 Java 8
  5. 2010 年 11 月, JSR 335 出世,Lambda Expressions for the JavaTM Programming Language
  6. 2014 年 5 月, JSR 335 发布最终版本,Java 终于迎来了 Lambda

基本定义

λ演算(英语:lambda calculus,λ-calculus)是一套从数学逻辑中发展,以变量绑定和替换的规则,来研究函数如何抽象化定义、函数如何被应用以及递归形式系统

虽然 Java 已经使用泛型继承来对数据进行抽象,但还没有合适的手段来抽象数据处理过程,因此 Lambda 表达式就是为了解决这个问题而引入的。

Lambda 在 编程语言上主要有两大特性:匿名函数和闭包。

匿名函数

在计算机中,Lambda 一般是匿名函数,而匿名函数本质上就是一个函数,它所抽象出来的东西是一组运算

比如说我们想给某个数组都加1,就能非常简洁的实现。

Arrays.asList(1,2,3,4)
        .stream()
        .map(i -> i+1// 列表全+1,但注意其实这里没有改变原来的列表,而是重新生成新列表                          // (无副作用)
        .forEach(i -> System.out.println(i));

闭包

Java 不完全直接支持函数闭包(针对局部变量),但你可以通过类或者数组来模拟闭包。

  1. 直接使用函数闭包
public static Map<String, Supplier> createCounter(int initValue) { 
    // the enclosing scope
    int count = initValue;   // this variable is effectively final
    Map<String, Supplier> map = new HashMap<>();
    map.put("val", () -> count);
    map.put("inc", () -> count++);  // error,can't compile
    return map;
}
  1. Java 通过类来实现闭包
private static class MyClosure {
    public int value;
    public MyClosure(int initValue) { this.value = initValue; }
}

public static Map<String, Supplier> createCounterWithClosure(int initValue) {
    MyClosure closure = new MyClosure(initValue);  // ugly
    Map<String, Supplier> counter = new HashMap<>();
    counter.put("val", () -> closure.value);
    counter.put("inc", () -> closure.value++); 
    return counter;
}
// also can represent this way
 public static Map<String, Supplier> createCounter(int initValue) { 
        final int[] count = {initValue}; // use an array wrapper
        Map<String, Supplier> map = new HashMap<>();
        map.put("val", () -> count[0]);
        map.put("inc", () -> count[0]++);  
        return map;
    }

Java 没有像 JavaScript 一样,从语法上支持这种特性,主要有几点原因:

1.**多线程.**如果允许修改局部变量,那么在多线程情况下又得加多考虑,比如说同步问题。

比如说你的方法是在主线程执行的,而 Lambda 表达式又是在子线程创建的,那么如果在

Lambda 表达式中修改局部变量,主线程的同步应该怎么处理?而且,这样就要把变量分配

在堆上面,还是有相当一部分 Java 程序员对这个不爽的(没有使用 new 关键字居然你

要在堆上面分配内存??)。

还可能引发内存泄漏的情况(主线程跑完了,结果子线程 hold 住了引用)。

2.**性能问题.**这是由上一个问题带来的,如果需要同步,加锁、volatile、CAS都有一定的性能损耗

3.**没必要.**在函数式的思维中,在 Lambda 中修改变量其实就代表着 副作用。消除副作用是通过

​ 局部变量交给下一个方法去处理,这还能提高并发程序的容错性,所以最好不要使用之前说的

​ 技巧来实现这种特性。

int sum = 0;
integerList.forEach(e -> { sum += e; };  //wrong
// should do this way
int sum = integerList.stream()   // java 8 way
                    .mapToInt(Integer::intValue)  // 减少装箱带来的成本,提高性能
                    .sum();

Java Lambda

表达式语法

(params) -> expression

(params) -> statement

(params) -> { statements }

list.stream().map(i -> i+1);  // (params) -> expression

run(() -> System.out.println(233));  // (params) -> statement

run(() ->{   //(params) -> { statements }
    System.out.println(233);
    System.out.println(123);
});

private static void run(Runnable r){
   r.run();
}

// some other lambda sample
1. (int x, int y) -> x + y                          
// takes two integers and returns their sum
2. (x, y) -> x - y                                  
// takes two numbers and returns their difference
3. () -> 42                                         
// takes no values and returns the secret of the universe 
4. (String s) -> System.out.println(s)              
// takes a string, prints its value to the console, and returns nothing 
5. x -> 2 * x                                       
// takes a number and returns the result of doubling it
6. c -> { int s = c.size(); c.clear(); return s; } 
// takes a collection, clears it, and returns its previous size

特点:

1.可以看到参数类型可以显示声明出来(比如说 String ),也可以让编译器自动推导出来。

2.如果是 statements 的话,需要显式地去 return。

3.如果只有一个参数,可以省略括号;但如果是零个参数或者多个参数,则需要括号。

Tips:

通常都会把lambda表达式内部变量的名字起得短一些。这样能使代码更简短,放在同一行。所以,变量名选用a、b或者x、y会比even、odd要好。

Why Lambda ?

于 Java 而言, Lambda 表达式最大的作用就可以可以结合 Java 8 的 Stream 特性,更容易去并发地操作集合。

之前如果想并发地去处理list,需要开发者去维护这些代码(用锁或者CAS等同步机制),但在 1.8 中,可以交给类库去处理这些事情,开发者只需要写好业务代码即可。

然而,为了让开发者来享受这些好处的话, 需要提供一个收集方法的函数。 但我们知道 Java 中函数并不是一等公民,传参并不能传函数,如果使用匿名内部类的话就太笨拙了,因此 Lambda 就此诞生。

如果项目中使用了 RxJava 的话,如果不使用 Lambda 的话,其实整个代码依旧还是没有那么优雅的。

此外,使用 Lambda 并不像 匿名内部类一样引入了一个新的命名空间,如果使用匿名内部类:

public class Test {
  public static void main(String args[]){
      Test t = new Test();
      t.run();
  }
  
  public void run(){
        new EnClosing(){
            @Override
            public void run() {
                System.out.println(toString()); // "EnClosing"
                // use outclass's toString(awkward)
                System.out.println(Test.this.toString()); //"Test"
            }
        };
    }
  @Override
  public String toString() {
      return "Test";
  }
  public class EnClosing{
          public void run(){
  
          }
          @Override
          public String toString() {
              return "EnClosing";
          }
      }
}

但是其实这一点也是被吐槽之后再改的,在第一版的 Java7 Lambda 中,其实并没有解决以上问题。

Lambda 实战

一般来说,jdk中可用 Lambda 替换的接口都标注了@FunctionalInterface

这个接口的作用就是检查接口是否是SAM(Single Abstract Method 单个抽象方法)的作用,如果注解不是在接口上,或者这个接口有多个未实现的接口,那么IDE就会报错。

mark

那既然这个接口只有一个抽象方法的话,为何在调用的时候还这么麻烦,例如:

default void forEach(Consumer<T> action) {
    for (T t : this) {
        action.accept(t);  // why not just use `action(t)`?
        //action(t);
    }
}

大概原因是这样的:因为方法的命名空间与变量的命名空间是没有冲突的,那么有时候可能会造成困惑,而且要单独为 Lambda 表达式创造新的语法可能会让很多 Java 开发者造成误解(笑)。

感兴趣的话可以看看: 名词王国 Java

void action(T t) { ... }

default void forEach(Consumer<T> action) {
    for (T t : this) {
        action(t);  // who use use it? the local variable or the method?
    }
}

事件绑定

// before
mPMChatBottomView.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                LogUtil.d(TAG, "ClickView: " + v);
            }
        });

// after
mPMChatBottomView.setOnClickListener((v) -> LogUtil.d(TAG, "ClickView: " + v));

排序

List<Integer> list = Arrays.asList(4,3,2,1,7,8,9,5,10);
// 偶数排序在前,奇数在后
// 2 4 8 10 1 3 5 7 9
Collections.sort(list,(a,b) -> {
    boolean remainder1 = (a % 2 == 0);
    boolean remainder2 = (b % 2 == 0);
    return remainder1?(remainder2?a-b:-1):(remainder2?1:a-b);
});

handler post

mainHandler.post(() -> tv.setText("test"));

JDK's Lambda

在 java.util.function 中,包含了很多接口:

mark

这边举一个例子,有一个接口叫做 Predicate,一般可以用来做过滤:

public interface Predicate<T> {
    // 校验参数是否符合要求
    boolean test(T t);
	// 两个条件共同检查
    default Predicate<T> and(Predicate<? super T> other) {
        Objects.requireNonNull(other);
        return (t) -> test(t) && other.test(t);
    }
    // 取反
    default Predicate<T> negate() {
        return (t) -> !test(t);
    }
    // 类似于 || 
    default Predicate<T> or(Predicate<? super T> other) {
        Objects.requireNonNull(other);
        return (t) -> test(t) || other.test(t);
    }
    // 生产出一个Predicate,用来检查两个对象是否相等,在列表操作中很给力
    static <T> Predicate<T> isEqual(Object targetRef) {
        return (null == targetRef)
                ? Objects::isNull
                : object -> targetRef.equals(object);
    }
}

使用案例:

public static void main(String args[]){
    List<String> languages = Arrays.asList("Java", "Scala", "C++", "Haskell", "Lisp");

    System.out.println("Languages which starts with J :");
    filter(languages, (str)->str.startsWith("J"));

    System.out.println("Languages which ends with a ");
    filter(languages, (str)->str.endsWith("a"));

    System.out.println("Print all languages :");
    filter(languages, (str)->true);

    System.out.println("Print no language : ");
    filter(languages, (str)->false);

    System.out.println("Print language whose length greater than 4:");
    filter(languages, (str)->str.length() > 4);

    System.out.println("Print Only Lisp!");
    filter(languages, Predicate.isEqual("Lisp"));

}


public static <T> void filter(List<T> names, Predicate<T> condition) {
    for(T name: names)  {
        if(condition.test(name)) {
            System.out.println(name + " ");
        }
    }
    // can also present this simple way
    names.stream().filter(condition);
}

结果:

Languages which starts with J : Java Languages which ends with a Java Scala Print all languages : Java Scala C++ Haskell Lisp Print no language : Print language whose length greater than 4: Scala Haskell Print Only Lisp! Lisp

利用 or 和 and:

Predicate<String> startsWithJ = (n) -> n.startsWith("J");
Predicate<String> fourLetterLong = (n) -> n.length() == 4;
languages.stream()
        .filter(startsWithJ.and(fourLetterLong))
        .forEach((n) -> System.out.println("nName, which starts with 'J' and four letter long is : " + n));

languages.stream()
        .filter(startsWithJ.or(fourLetterLong))
        .forEach((n) -> System.out.println("nName, which starts with 'J' or four letter long is : " + n));

结果:

nName, which starts with 'J' and four letter long is : Java nName, which starts with 'J' or four letter long is : Java nName, which starts with 'J' or four letter long is : Lisp

使用Stream:

// 将字符串换成大写并用逗号链接起来
List<String> G7 = Arrays.asList("USA", "Japan", "France", "Germany", "Italy", "U.K.","Canada");
String G7Countries = G7.stream().map(x -> x.toUpperCase()).collect(Collectors.joining(", "));
System.out.println(G7Countries);

结果:

USA, JAPAN, FRANCE, GERMANY, ITALY, U.K., CANADA

基本的接口如下,他们都是为了 Stream 类库服务的(这些操作符的详细在此不做介绍):

mark

如果有用过 RxJava 的,对以上这些接口肯定非常熟悉,因为 RxJava 是基本复用了这些名称,而且做到了 Java 6 的兼容。

但要注意到 Stream 和 Observable 的区别在于, Java 8 的 Stream 只能使用一次,pull-base,而 Observable 可以被订阅多次,而且是 push-base。

Lambda表达式与异常

Lambda 表达式对于 Exception 的处理还是很挫的,主要的问题就是 checked Exception。

List<File> files  = getFileList();
files.stream().map((File a) -> a.getCanonicalPath());  // can't compile
// you can't do like this
try{
    files.stream().map((File a) -> a.getCanonicalPath());
} catch (IOException e){
    e.printStackTrace();
}
// you need do this (garbage code)
files.stream().map((File a) -> {
    try {
        return a.getCanonicalPath();
    } catch (IOException e) {
        e.printStackTrace();
    }
    return null;
});

不过最好在函数式编程中,Exception 意味着副作用,所以 Exception 和 lambda 表达式并不协调。

在平时编程中,尽可能少用 Checked Exception。

Mehtod References(方法引用)

mark

用于更加简化 Lambda 方法,可以与 Lambda 表达式相互翻译:

// static method
Arrays.sort(integerArray, Integer::compareUnsigned);
Arrays.sort(integerArray, (x,y) -> Integer.compareUnsigned(x, y));
// Bound Instance,固定实例调用
list.forEach(System.out::println);
list.forEach(x -> System.out.println(x));
// Unbound Instance,不同实例调用
list.forEach(Age::printAge);
list.forEach(age -> age.printAge());
// 构造函数
List<String> strList = Arrays.asList("1","2","3");
List<Integer> intList = strList.stream().map(Integer::new).collect(Collectors.toList());
List<Integer> intList = 
strList.stream().map(s -> new Integer(s)).collect(Collectors.toList());

Default methods

Default methods 引入的原因就是为了扩展集合类的接口,但同时又不影响开发者(因为每在接口中新引入一个方法就要求实现类必须全部实现)。

例如List.foreach

default void forEach(Consumer<? super T> action) {
    Objects.requireNonNull(action);
    for (T t : this) {
        action.accept(t);
    }
}

感觉引入了这些新特性之后,接口反而慢慢向着抽象类的层面在发展了,这可能也是因为 Java 单继承所需要付出的一些妥协吧。

但如果 Default 方法与父类方法有冲突怎么办?

这里有一套原则:

  1. Classes always win. 父类中的方法优先级永远大于 default 方法
  2. 否则,如果多个接口中都有一样的 Default 接口,那么更具体的接口方法优先。
    public interface A {
        default void hello() { System.out.println("Hello World from A"); }
    }
    public interface B extends A {
        default void hello() { System.out.println("Hello World from B"); }
    }
    public class C implements B, A {
        public static void main(String... args) {
            new C().hello();  // print Hello World from B
        }
    } 

但如果 A 和 B 之间没有任何关系呢? 那么这就会引发一个冲突,具体可以参见:

http://www.lambdafaq.org/how-are-conflicting-method-declarations-resolved/

关于 Java 集合框架的未来设想

随着硬件成本的降低,多核高性能服务器已经越来越普遍,那么并行并发已经成为一个老大难问题了,Java 类库的成员们就想着可以来通过 函数式编程 的一些特点来简化并发编程,从 Java 7 的 Fork-Join 开始,到 Java 8 的 Stream 库,Java Library 都想让开发者尽可能地减少开发工作量。

但目前 Java 8 的 Stream 在转换过程中就产生许多中间变量,这对于 GC 类型的语言而言不是一件好事,因此这套类库将来一定会慢慢优化效率(底层并发更加灵活和简单)和内存(类似于 rxJava -> rxJava2),对于开发者而言却不用关系,因为 API 依旧放在那里,如果将来想提高效率的话,很简单,直接 switch Java 8 到 Java 9 就 OK 了。

Stream API 的设计哲学起源于 Unix 系统中的 pipeline

Lambda 与泛型

虽然看起来 Lambda 可以完全替代内部类,但要注意其实它也是有坑:无法获取泛型信息。

public class Erasure {
    static class RetainedFunction implements Function<Integer,String> {
        public String apply(Integer t) {
            return String.valueOf(t);
        }
    }

    public static void main(String[] args) throws Exception {
        Function<Integer,String> f0 = new RetainedFunction();
        Function<Integer,String> f1 = new Function<Integer,String>() {
            public String apply(Integer t) {
                return String.valueOf(t);
            }
        };
        Function<Integer,String> f2 = String::valueOf;
        Function<Integer,String> f3 = i -> String.valueOf(i);

        for (Function<Integer,String> f : Arrays.asList(f0, f1, f2, f3)) {
            try {
                System.out.println(f.getClass().getMethod("apply", Integer.class).toString());
            } catch (NoSuchMethodException e) {
                System.out.println(f.getClass().getMethod("apply", Object.class).toString());
            }
            System.out.println(Arrays.toString(f.getClass().getGenericInterfaces()));
        }
    }
}

结果:

public java.lang.String com.company.Erasure$RetainedFunction.apply(java.lang.Integer) [java.util.function.Function<java.lang.Integer, java.lang.String>]

public java.lang.String com.company.Erasure$1.apply(java.lang.Integer) [java.util.function.Function<java.lang.Integer, java.lang.String>]

public java.lang.Object com.company.Erasure?Lambda$1/1096979270.apply(java.lang.Object) [interface java.util.function.Function]

public java.lang.Object com.company.Erasure?Lambda$2/1747585824.apply(java.lang.Object) [interface java.util.function.Function]

可以看到 Lambda 表达式并没有泛型信息,所以在某些需要获取泛型信息的地方,不要使用 Lambda表达式。

例如我们项目中的事件总线,如果使用 Lambda 表达式,就会拿不到泛型:

public interface OnEvent<T> {
    void onRecv(T event);
}

public <T> Eventor addOnEvent(OnEvent<T> onEvent){
    int code = parseCode(onEvent); // 根据泛型信息获取 hashcode
    if(code == -1) {
        throw new RuntimeException("addOnEvent ERROR!!!");
    }
    cbMap.put(code, onEvent);
    EventImpl.getInstance().register(onEvent);
    return this;
}

int parseCode(OnEvent cb){
    if(cb != null){
        Type[] genericType = cb.getClass().getGenericInterfaces();
        if(genericType[0] instanceof ParameterizedType){
            ParameterizedType p = (ParameterizedType)genericType[0];
            Type[] args = p.getActualTypeArguments();
            int code = args[0].hashCode();
            return code;
        }
    }
    return -1;
}

虽然某些项目可以利用 字符串常量池 来获取到 Lambda 的类型信息,但兼容性等方案不太好,因此可以考虑放弃。

具体实现

绝大多数的翻译策略都是翻译为内部类(继承自Object),但这里为了帮助理解,用方法的角度呈现。

主要分为两种情况考虑(捕获变量和不捕获变量):

1、不捕获变量:

类似于生成这样的方法:

private static java.lang.Object lambda$0(java.lang.String);

需要注意的是,这里的$0并不是代表内部类,这里仅仅是为了展示编译后的代码而已。

由于这种方法没有捕获外部变量,所以 Lambda 工厂会将其缓存起来,达到复用的目的。

匿名内部类通常需要我们手动的进行优化(static 匿名内部类 + 单例)来避免额外对象生成,而对于不进行变量捕获的Lambda表达式,JVM已经为我们做好了优化(单例缓存)。

2、捕获变量:

例如:

int offset = 100;
Function<String, Integer> f = s -> Integer.parseInt(s) + offset; 

对于捕获变量的Lambda表达式情况有点复杂,同前面一样 Lambda 表达式依然会被提取到一个静态方法中,不同的是被捕获的变量同正常的参数一样传入到这个方法中,因此会生成这样的实现:

static Integer lambda$1(int offset, String s) {    
  return Integer.parseInt(s) + offset;
}

这也解释了为什么我们无法修改引用的外部变量,因为它是作为参数引入的,和非 static 的匿名内部类操作基本相同。

但如果Lambda表达式用到了类的实例的属性,其对应生成的方法可以是实例方法,而不是静态方法,这样可以避免传入多余的参数。

函数式对于并发的支持做的比命令式要好的多,如果可能的话,尽量减少捕获外部变量。

因为捕获变量不仅仅可能会隐式生成桥接方法(为了访问外部 private 变量而生成的 accesss$num 方法),而且 Lambda 表达式也不能达到重用的目的(单例缓存),在多线程环境下还可能出现同步问题。

拥抱函数式,消除副作用

Why Lambda use invokedynamic?

Java 8的Lambda表达式为什么要基于invokedynamic?

Java 8 中的Lambda表达式是基于 invokedynamic 来在运行时动态生成匿名类字节码。

这样很明显会带来额外的运行时开销,所以为何这么设计呢?

假设 Lambda 表达式直接在编译过程中生成字节码(在解语法糖阶段)。

那么将来在优化的时候就很难进行了(源于之前踩过的坑)。

所以好处1:可以将来动态进行优化(目前是使用内部类,将来可以使用优化后的 MethodHandle 【这个暂时会有反射惩罚】)。

好处2:减少了编译期生成的字节码大小,不同 JVM 语言的实现上可以不一样。

Android 中的 Lambda

Android Studio 3.0 使用 Lambda 的秘密

由于 DVM 和 ART 已经在跑了,所以也不可能说修改版本去支持 Java 8 特性。

具体来说(sdk < 24 equals Java 7)、(sdk >= 24 equals Java 8)

所以怎么才能让开发者既能使用 Lambda,又能兼容老的系统呢?

那 Android team 团队决定使用 Desugar.java 来解语法糖了。

mark

具体我大概看了一下,

InvokeDynamicLambdaMethodCollector 先使用访问者模式访问所有使用了 InvokeDynamic 的字节码,来收集项目中所有的 Lambda 表达式,然后交给 LambdaDesugaring 去解语法糖。

LambdaDusugaring中,使用了内部类 InvokedynamicRewriter 去为每个 Lambda 表达式都生成一个 class 名称,具体名称格式为:

internalName + "?Lambda$" + (lambdaCount++)

举例来说:LogInActivity?Lambda$1

然后将 Lambda 类名和需要生成的桥接方法等信息,传给交给 LambdaClassMaker 去生成具体的匿名内部类(通过 java.lang.invoke.MethodHandle 去生成)。

生成类结束后,如果是没有捕获外部变量的 Lambda 表达式(stateless lambda classes),会在之前的内部类上 生成一个单例对象

// 生成单例对象
super.visitFieldInsn(
              Opcodes.GETSTATIC,
              lambdaClassName,
              LambdaClassFixer.SINGLETON_FIELD_NAME,
              desc.substring("()".length()));

使用 Lambda 表达式会相对而言增加一些编译时间,具体时间应该和类的数量有关系。

不过我看到作者的一个 todo :

TODO(kmb): Scan constant pool instead of visiting the class to find bootstrap methods etc.

如果是扫描常量池的话,应该扫描时间会降低不少。

Android 使用 Lambda 的注意事项

如果你是开发一个 Library 给别人使用,Ok,暂时不要使用 Java 1.8

如果你是多 Module 的项目,一定要在主 Module 中设置 使用 Java 1.8

具体原因请参照:https://developer.android.com/studio/write/java8-support.html