JDK8–Lambda表达式

原文链接:https://github.com/Snailclimb/JavaGuide

JDK8--Lambda表达式

1.什么是Lambda表达式

Lambda表达式实质上是一个可传递的代码块,Lambda又称为闭包或者匿名函数,是函数式编程语法,让方法可以像普通参数一样传递

2.Lambda表达式语法

(参数列表) -> {执行代码块}

参数列表可以为空()->{}

可以加类型声明比如(String para1, int para2) -> {return para1 + para2;}我们可以看到,lambda同样可以有返回值.

在编译器可以推断出类型的时候,可以将类型声明省略,比如(para1, para2) -> {return para1 + para2;}

(lambda有点像动态类型语言语法。lambda在字节码层面是用invokedynamic实现的,而这条指令就是为了让JVM更好的支持运行在其上的动态类型语言)

3.函数式接口

在了解Lambda表达式之前,有必要先了解什么是函数式接口(@FunctionalInterface)

函数式接口指的是有且只有一个抽象(abstract)方法的接口

当需要一个函数式接口的对象时,就可以用Lambda表达式来实现,举个常用的例子:

  Thread thread = new Thread(() -> {
      System.out.println("This is JDK8's Lambda!");
  });

这段代码和函数式接口有啥关系?我们回忆一下,Thread类的构造函数里是不是有一个以Runnable接口为参数的?

public Thread(Runnable target) {...}

/**
 * Runnable Interface
 */
@FunctionalInterface
public interface Runnable {  
    public abstract void run();
}

到这里大家可能已经明白了,Lambda表达式相当于一个匿名类或者说是一个匿名方法。上面Thread的例子相当于

  Thread thread = new Thread(new Runnable() {
      @Override
      public void run() {
          System.out.println("Anonymous class");
      }
  });

也就是说,上面的lambda表达式相当于实现了这个run()方法,然后当做参数传入(个人感觉可以这么理解,lambda表达式就是一个函数,只不过它的返回值、参数列表都
由编译器帮我们推断,因此可以减少很多代码量)。

Lambda也可以这样用 :

  Runnable runnable = () -> {...};

其实这和上面的用法没有什么本质上的区别。

至此大家应该明白什么是函数式接口以及函数式接口和lambda表达式之间的关系了。在JDK8中修改了接口的规范,
目的是为了在给接口添加新的功能时保持向前兼容(个人理解),比如一个已经定义了的函数式接口,某天我们想给它添加新功能,那么就不能保持向前兼容了,
因为在旧的接口规范下,添加新功能必定会破坏这个函数式接口(JDK8中接口规范)


除了上面说的Runnable接口之外,JDK中已经存在了很多函数式接口
比如(当然不止这些):

  • java.util.concurrent.Callable

  • java.util.Comparator

  • java.io.FileFilter

    关于JDK中的预定义的函数式接口

  • JDK在java.util.function下预定义了很多函数式接口

    • Function<T, R> {R apply(T t);} 接受一个T对象,然后返回一个R对象,就像普通的函数。
    • Consumer<T> {void accept(T t);} 消费者 接受一个T对象,没有返回值。
    • Predicate<T> {boolean test(T t);} 判断,接受一个T对象,返回一个布尔值。
    • Supplier<T> {T get();} 提供者(工厂) 返回一个T对象。
    • 其他的跟上面的相似,大家可以看一下function包下的具体接口。

4.变量作用域

public class VaraibleHide {
    @FunctionalInterface
    interface IInner {
        void printInt(int x);
    }
    public static void main(String[] args) {
        int x = 20;
        IInner inner = new IInner() {
            int x = 10;
            @Override
            public void printInt(int x) {
                System.out.println(x);
            }
        };
        inner.printInt(30);
        
        inner = (s) -> {
            //Variable used in lambda expression should be final or effectively final
            //!int x = 10;
            //!x= 50; error
            System.out.print(x);
        };
        inner.printInt(30);
    }
}
输出 :
30
20

对于lambda表达式java inner = (s) -> {System.out.print(x);};,变量x并不是在lambda表达式中定义的,像这样并不是在lambda中定义或者通过lambda的参数列表()获取的变量成为自由变量,它是被lambda表达式捕获的。

lambda表达式和内部类一样,对外部自由变量捕获时,外部自由变量必须为final或者是最终变量(effectively final)的,也就是说这个变量初始化后就不能为它赋新值,
同时lambda不像内部类/匿名类,lambda表达式与外围嵌套块有着相同的作用域,因此对变量命名的有关规则对lambda同样适用。大家阅读上面的代码对这些概念应该
不难理解。

5.方法引用

只需要提供方法的名字,具体的调用过程由Lambda和函数式接口来确定,这样的方法调用成为方法引用。

下面的例子会打印list中的每个元素:

List<Integer> list = new ArrayList<>();
        for (int i = 0; i < 10; ++i) {
            list.add(i);
        }
        list.forEach(System.out::println);

其中System.out::println这个就是一个方法引用,等价于Lambda表达式 (para)->{System.out.println(para);}

我们看一下List#forEach方法 default void forEach(Consumer<? super T> action)可以看到它的参数是一个Consumer接口,该接口是一个函数式接口

@FunctionalInterface
public interface Consumer<T> {
    void accept(T t);

大家能发现这个函数接口的方法和System.out::println有什么相似的么?没错,它们有着相似的参数列表和返回值。

我们自己定义一个方法,看看能不能像标准输出的打印函数一样被调用

public class MethodReference {
    public static void main(String[] args) {
        List<Integer> list = new ArrayList<>();
        for (int i = 0; i < 10; ++i) {
            list.add(i);
        }
        list.forEach(MethodReference::myPrint);
    }

    static void myPrint(int i) {
        System.out.print(i + ", ");
    }
}

输出: 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 

可以看到,我们自己定义的方法也可以当做方法引用。

到这里大家多少对方法引用有了一定的了解,我们再来说一下方法引用的形式。

  • 方法引用
    • 类名::静态方法名
    • 类名::实例方法名
    • 类名::new (构造方法引用)
    • 实例名::实例方法名
      可以看出,方法引用是通过(方法归属名)::(方法名)来调用的。通过上面的例子已经讲解了一个类名::静态方法名的使用方法了,下面再依次介绍其余的几种
      方法引用的使用方法。

      类名::实例方法名

      先来看一段代码
  String[] strings = new String[10];
  Arrays.sort(strings, String::compareToIgnoreCase);

上面的String::compareToIgnoreCase等价于(x, y) -> {return x.compareToIgnoreCase(y);}

我们看一下Arrays#sort方法public static <T> void sort(T[] a, Comparator<? super T> c),
可以看到第二个参数是一个Comparator接口,该接口也是一个函数式接口,其中的抽象方法是int compare(T o1, T o2);,再看一下
String#compareToIgnoreCase方法,public int compareToIgnoreCase(String str),这个方法好像和上面讲方法引用中类名::静态方法名不大一样啊,它
的参数列表和函数式接口的参数列表不一样啊,虽然它的返回值一样?

是的,确实不一样但是别忘了,String类的这个方法是个实例方法,而不是静态方法,也就是说,这个方法是需要有一个接收者的。所谓接收者就是
instance.method(x)中的instance,
它是某个类的实例,有的朋友可能已经明白了。上面函数式接口的compare(T o1, T o2)中的第一个参数作为了实例方法的接收者,而第二个参数作为了实例方法的
参数。我们再举一个自己实现的例子:

public class MethodReference {
    static Random random = new Random(47);
    public static void main(String[] args) {
        MethodReference[] methodReferences = new MethodReference[10];
        Arrays.sort(methodReferences, MethodReference::myCompare);
    }
    int myCompare(MethodReference o) {
        return random.nextInt(2) - 1;
    }
}

上面的例子可以在IDE里通过编译,大家有兴趣的可以模仿上面的例子自己写一个程序,打印出排序后的结果。

构造器引用

构造器引用仍然需要与特定的函数式接口配合使用,并不能像下面这样直接使用。IDE会提示String不是一个函数式接口

  //compile error : String is not a functional interface
  String str = String::new;

下面是一个使用构造器引用的例子,可以看出构造器引用可以和这种工厂型的函数式接口一起使用的。

  interface IFunctional<T> {
    T func();
}

public class ConstructorReference {

    public ConstructorReference() {
    }

    public static void main(String[] args) {
        Supplier<ConstructorReference> supplier0 = () -> new ConstructorReference();
        Supplier<ConstructorReference> supplier1 = ConstructorReference::new;
        IFunctional<ConstructorReference> functional = () -> new ConstructorReference();
        IFunctional<ConstructorReference> functional1 = ConstructorReference::new;
    }
}

下面是一个JDK官方的例子

  public static <T, SOURCE extends Collection<T>, DEST extends Collection<T>>
    DEST transferElements(
        SOURCE sourceCollection,
        Supplier<DEST> collectionFactory) {

        DEST result = collectionFactory.get();
        for (T t : sourceCollection) {
            result.add(t);
        }
        return result;
    }
    
    ...
    
    Set<Person> rosterSet = transferElements(
            roster, HashSet::new);

实例::实例方法


其实开始那个例子就是一个实例::实例方法的引用

List<Integer> list = new ArrayList<>();
        for (int i = 0; i < 10; ++i) {
            list.add(i);
        }
        list.forEach(System.out::println);

其中System.out就是一个实例,println是一个实例方法。相信不用再给大家做解释了。

总结

Lambda表达式是JDK8引入Java的函数式编程语法,使用Lambda需要直接或者间接的与函数式接口配合,在开发中使用Lambda可以减少代码量,
但是并不是说必须要使用Lambda(虽然它是一个很酷的东西)。有些情况下使用Lambda会使代码的可读性急剧下降,并且也节省不了多少代码,
所以在实际开发中还是需要仔细斟酌是否要使用Lambda。和Lambda相似的还有JDK10中加入的var类型推断,同样对于这个特性需要斟酌使用。

# Java 

评论

Your browser is out-of-date!

Update your browser to view this website correctly. Update my browser now

×