小言_互联网的博客

【Java 8 in Action】Lambda表达式

203人阅读  评论(0)

Lambda表达式

Lambda表达式的介绍

Lambda表达式是在java规范提案JSR 335中定义的,Java 8 中引入了Lambda表达式,并被认为是Java 8最大的新特性,Lambda表达式促进了函数式编程,简化了Java编程开发。它允许你通过表达式来代替功能接口。 lambda表达式就和方法一样,它提供了一个正常的参数列表和一个使用这些参数的主体 (body,可以是一个表达式或一个代码块)

不仅仅简化了Java编程开发,Lambda表达式还增强了集合库。 Java SE 8添加了2个对集合数据进行批量操作的包: java.util.function 包以及 java.util.stream 包。 流(stream)就如同迭代器(iterator), 但附加了许多额外的功能。

总的来说,lambda表达式和 stream 是自Java语言添加泛型(Generics)和注解(annotation)以来最大的变化。 本文的重点放在Lambda表达式上

扯点函数式编程思想:
在数学中,函数就是有输入量、输出量的一套计算方案,也就是“拿什么东西做什么事情”。相对而言,面向对象过强调“必须通过对象的形式来做事情”,而函数式思想则尽量忽略面向对象的复杂语法——强调做什么,而不是以什么形式做。


Lambda表达式的语法

在介绍语法之前我们可以体验一下:Lambda表达式

public class Main{
    public static void main(String[] args) {

        //实现多线程程序
        Runnable r = new Runnable() {
            @Override
            public void run() {
                System.out.println(Thread.currentThread().getName()+"新线程创建了");
            }
        };
        new Thread(r).start();

        //简化代码
        new Thread(new Runnable() {
            //使用匿名内部类创建多线程
            @Override
            public void run() {
                System.out.println(Thread.currentThread().getName()+"新线程创建了");
            }
        }).start();

        //使用Lambda表达式创建多线程
        new Thread( ( ) ->{
            System.out.println(Thread.currentThread().getName() + "新线程创建了");
             }
        ).start();

    }
}

结果:

很容易发现Lambda表达式是非常简洁的,使用起来其实也是比较的简单的,其实从本质上来说Lambda表达式就是一种语法糖。

Lambda完全没有面向对象复杂的束缚。但在使用的时候,要明确它的适用前提:

  1. 使用Lambda必须具有接口,且要求接口中有且仅有一个抽象方法。
    无论是JDK内置的 Runnable 、 Comparator 接口还是自定义的接口,只有当接口中的抽象方法存在且唯一时,才可以使用Lambda。
  2. 使用Lambda必须具有上下文推断。
    也就是方法的参数或局部变量类型必须为Lambda对应的接口类型,才能使用Lambda作为该接口的实例。

备注:有且仅有一个抽象方法的接口,称为“函数式接口”。

那我们看一下它的语法吧:

parameter -> expression body

主要组成:参数列表 + 箭头 + 表达式体,如 (int x, int y) -> x + y

其中,表达式体可以是一条表达式,也可以是一个语句块(多条代码语句);

Lambda表达式具有如下特征:
【可选】类型声明:参数的类型不需要声明,编译器可以根据参数值推断出其类型;
【可选】括号:单个参数的话,不需要用圆括号包围参数,当然,对于多个参数或无参数的话,括号是需要的;
【可选】花括号:如果表达式主体只有一条语句的话,不需要用花括号包围,当然,对于多条语句,花括号是需要的;
【可选】return关键字:如果表达式主体是单一表达式,return关键字可以不写,编译器可以自动返回该值,当然,如果写了return,则需要加上花括号;

演示:

public class LambdaTest
{
    public static void main(String args[])
    {
        LambdaTest tester = new LambdaTest();

        // 有参数类型
        MathOperation addition = (int a, int b) -> a + b;

        // 无参数类型
        MathOperation subtraction = (a, b) -> a - b;

        // 有花括号,有return关键字
        MathOperation multiplication = (int a, int b) -> {
            return a * b;
        };

        // 无花括号,无return关键字,单一表达式情况
        MathOperation division = (int a, int b) -> a / b;

        // MathOperation调用示例
        System.out.println("10 + 5 = " + tester.operate(10, 5, addition));
        System.out.println("10 - 5 = " + tester.operate(10, 5, subtraction));
        System.out.println("10 x 5 = " + tester.operate(10, 5, multiplication));
        System.out.println("10 / 5 = " + tester.operate(10, 5, division));

        // 有括号
        GreetingService greetService1 = message -> System.out.println("Hello " + message);

        // 无括号,单个参数情况
        GreetingService greetService2 = (message) -> System.out.println("Hello " + message);

        // GreetingService调用示例
        greetService1.sayMessage("Mahesh");
        greetService2.sayMessage("Suresh");
        
        //有括号, 无参情况
        Runnable runTest = () -> System.out.println("Running");
        //Runnable调用示例
        runTest.run();
    }

    // 内部接口
    interface MathOperation
    {
        int operation(int a, int b);
    }

    interface GreetingService
    {
        void sayMessage(String message);
    }

    interface Runnable
    {
        void run();
    }
    
    private int operate(int a, int b, MathOperation mathOperation)
    {
        return mathOperation.operation(a, b);
    }
}

Method References

Method References指的是方法引用,简单地说,就是一个Lambda表达式。在Java 8中,我们会使用Lambda表达式创建匿名方法,但是有时候,我们的Lambda表达式可能仅仅调用一个已存在的方法,而不做任何其它事,对于这种情况,通过一个方法名字来引用这个已存在的方法会更加清晰,Java 8的方法引用允许我们这样做。方法引用是一个更加紧凑,易读的Lambda表达式,注意方法引用是一个Lambda表达式,其中方法引用的操作符是双冒号"::"。

演示一下,你就明白了

import java.time.LocalDate;

public class Person
{
  	String name;
    LocalDate birthday;

    public Person(String name, LocalDate birthday)
    {
        this.name = name;
        this.birthday = birthday;
    }


    public LocalDate getBirthday()
    {
        return birthday;
    }

    public static int compareByAge(Person a, Person b)
    {
        return a.birthday.compareTo(b.birthday);
    }

    @Override
    public String toString()
    {
        return this.name;
    }
}

我们对Person类进行排序

import java.time.LocalDate;
import java.util.Arrays;
import java.util.Comparator;

public class Main
{

    static class PersonAgeComparator implements Comparator<Person> {
        public int compare(Person a, Person b) {
            return a.getBirthday().compareTo(b.getBirthday());
        }
    }
    
    public static void main(String[] args)
    {
        Person[] pArr = new Person[]{
            new Person("003", LocalDate.of(2016,9,1)),
            new Person("001", LocalDate.of(2016,2,1)),
            new Person("002", LocalDate.of(2016,3,1)),
            new Person("004", LocalDate.of(2016,12,1))};

        Arrays.sort(pArr, new PersonAgeComparator());
        
        System.out.println(Arrays.asList(pArr));
    }
}

结果:

其中,Arrays类的sort方法定义如下:

public static <T> void sort(T[] a, Comparator<? super T> c)

这里,我们首先要注意Comparator接口是一个函数式接口,因此我们可以使用Lambda表达式,而不需要定义一个实现Comparator接口的类,并创建它的实例对象,传给sort方法。

使用Lambda表达式,我们可以这样写:

😇 一,使用Lambda表达式,未调用已存在的方法

import java.time.LocalDate;
import java.util.Arrays;

public class Main
{

    public static void main(String[] args)
    {
        Person[] pArr = new Person[]{
            new Person("003", LocalDate.of(2016,9,1)),
            new Person("001", LocalDate.of(2016,2,1)),
            new Person("002", LocalDate.of(2016,3,1)),
            new Person("004", LocalDate.of(2016,12,1))};

        Arrays.sort(pArr, (Person a, Person b) -> {
            return a.getBirthday().compareTo(b.getBirthday());
        });
        
        System.out.println(Arrays.asList(pArr));
    }
}

然而,在以上代码中,关于两个人生日的比较方法在Person类中已经定义了,因此,我们可以直接使用已存在的Person.compareByAge方法。

😇 二、使用Lambda表达式,调用已存在的方法

import java.time.LocalDate;
import java.util.Arrays;

public class Main
{

    public static void main(String[] args)
    {
        Person[] pArr = new Person[]{
            new Person("003", LocalDate.of(2016,9,1)),
            new Person("001", LocalDate.of(2016,2,1)),
            new Person("002", LocalDate.of(2016,3,1)),
            new Person("004", LocalDate.of(2016,12,1))};

        Arrays.sort(pArr, (a, b) -> Person.compareByAge(a, b));
        
        System.out.println(Arrays.asList(pArr));
    }
}

因为这个Lambda表达式调用了一个已存在的方法,因此,我们可以直接使用方法引用来替代这个Lambda表达式,

😇 三,使用方法引用

import java.time.LocalDate;
import java.util.Arrays;

public class Main
{

    public static void main(String[] args)
    {
        Person[] pArr = new Person[]{
            new Person("003", LocalDate.of(2016,9,1)),
            new Person("001", LocalDate.of(2016,2,1)),
            new Person("002", LocalDate.of(2016,3,1)),
            new Person("004", LocalDate.of(2016,12,1))};

        Arrays.sort(pArr, Person::compareByAge);
        
        System.out.println(Arrays.asList(pArr));
    }
}

在以上代码中,方法引用Person::compareByAge在语义上与Lambda表达式 (a, b) -> Person.compareByAge(a, b) 是等同的,都有如下特性:

  1. 真实的参数是拷贝自Comparator<Person>.compare方法,即(Person, Person);
  2. 表达式体调用Person.compareByAge方法;

四种方法引用类型

静态方法引用
我们前面举的例子Person::compareByAge就是一个静态方法引用。

特定实例对象的方法引用
如下示例,引用的方法是myComparisonProvider 对象的compareByName方法;

class ComparisonProvider
        {
            public int compareByName(Person a, Person b)
            {
                return a.getName().compareTo(b.getName());
            }

            public int compareByAge(Person a, Person b)
            {
                return a.getBirthday().compareTo(b.getBirthday());
            }
        }
        ComparisonProvider myComparisonProvider = new ComparisonProvider();
        Arrays.sort(rosterAsArray, myComparisonProvider::compareByName);

任意对象(属于同一个类)的实例方法引用
如下示例,这里引用的是字符串数组中任意一个对象的compareToIgnoreCase方法。

String[] stringArray = { "Barbara", "James", "Mary", "John", "Patricia", "Robert", "Michael", "Linda" };
Arrays.sort(stringArray, String::compareToIgnoreCase);

构造方法引用
如下示例,这里使用了关键字new,创建了一个包含Person元素的集合。

Set<Person> rosterSet = transferElements(roster, HashSet<Person>::new);

transferElements方法的定义如下,功能为集合拷贝,

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;
}

适合使用方法引用的情况
当一个Lambda表达式调用了一个已存在的方法

不适合使用方法引用的情况
当我们需要往引用的方法传其它参数的时候,不适合,如下示例:

IsReferable demo = () -> ReferenceDemo.commonMethod("Argument in method.");

Default methods

Default methods 是默认方法的意思

简单的说,就是可以在接口中定义一个已实现方法,且该接口的实现类不需要实现该方法;

举个例子

	interface GOService  
    {
        void sayMessage(String message);
        
        //可以在接口中定义默认方法
        default void sayHello(){
            System.out.println("Hello");
        }
    }
    
    //实现类不需要实现接口中的默认方法
    class GoServiceImpl implements GoService{
        @Override
        public void sayMessage(String message)
        {

        }
    }

默认方法主要是为了方便扩展已有接口;如果没有默认方法,加入给JDK中的某个接口添加一个新的抽象方法,那么所有实现了该接口的类都得修改,影响将非常大。

使用默认方法,可以给已有接口添加新方法,而不用修改该接口的实现类。当然,接口中新添加的默认方法,所有实现类也会继承该方法。

举个例子,在Java 8的Iterable接口中,新增了一个默认方法forEach,也正因为forEach是默认方法,才不用修改所有Iterable接口的实现类。

Iterable接口新增的forEach方法如下(入参是一个函数式接口,因此支持Lambda表达式):

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

因为Collection接口继承了Iterable接口,所以我们可以在集合类中使用forEach方法,如下,这里使用了方法引用

     List<String> list = new ArrayList<String>();
        list.add("001");
        list.add("002");
        list.forEach(System.out::println);

可见,我们在未破坏Iterable接口实现类的前提下,给Iterable接口的所有实现类添加了一个新方法forEach,这在Java 8之前是不可能的。

关于重写 Override默认方法:
如果子类没有重写父接口默认方法的话,会直接继承父接口默认方法的实现;
如果子类重写父接口默认方法为普通方法,则与普通方法的重写类似;
如果子类(接口或抽象类)重写父接口默认方法为抽象方法,那么所有子类的子类需要实现该方法;

关于默认方法调用冲突
因为一个类是可以实现多个接口的,如果多个接口定义了同样的默认方法,那么子类如何调用父类的默认方法呢?

具体调用流程如下:
1、首先,如果子类覆盖了父类的默认方法,那么什么也不用想,直接使用调用子类覆盖后的方法;
2、其次,优先选择调用更加具体的接口默认方法,什么意思呢,举个例子,如果A1接口继承A接口,那么A1接口相对A接口就更加具体,当C类实现了A1接口的时候,就优先调用A1接口的默认方法;
3、最后,如果C类同时实现A1接口和A2接口,且A1和A2有同名的默认方法,那么选择哪个接口的默认方法呢?答案是编译器报错,提示定义了重名的方法,快速修复方式是覆盖其中的一个即可;

尽管有默认方法这一说,但是万不得已不建议使用!!!

Functional Interface

Functional Interface是函数式接口的意思

所谓的函数式接口,是在接口里面只能有一个抽象方法。这种类型的接口也称为SAM接口,即Single Abstract Method
interfaces。

它们主要用在Lambda表达式和方法引用(实际上也可认为是Lambda表达式)上。
比如说定义了一个函数式接口如下:

  @FunctionalInterface
    interface MakeFood{
        void doSomething();
    }

那么就可以使用Lambda表达式来表示该接口的一个实现(注:JAVA 8 之前一般是用匿名类实现的)

MakeFood makeFood = ()-> System.out.println("蒸包子");

你会注意到定义函数式接口的时候有一个注解 @FunctionalInterface

这是Java 8为函数式接口引入的新注解,主要用于编译级错误检查,加上该注解,当你写的接口不符合函数式接口定义的时候,编译器会报错。

比如说接口中包含了两个抽象方法,这就违反了函数式接口的定义,一般IDE会提示你。

😇 函数式接口里允许定义默认方法
函数式接口里是可以包含默认方法,因为默认方法不是抽象方法,其有一个默认实现,所以是符合函数式接口的定义的;
如下代码不会报错:

@FunctionalInterface
    interface GOService
    {
        void sayMessage(String message);

        default void doSomeMoreWork1()
        {
            // Method body
        }

        default void doSomeMoreWork2()
        {
            // Method body
        }
    }

😇 函数式接口里允许定义静态方法
函数式接口里是可以包含静态方法,因为静态方法不能是抽象方法,是一个已经实现了的方法,所以是符合函数式接口的定义的;

如下代码不会报错:

@FunctionalInterface
    interface GOService 
    {
        void sayMessage(String message);
        static void printHello(){
            System.out.println("Hello");
        }
    }

😇 函数式接口里允许定义 java. lang Object里的 public方法

函数式接口里是可以包含Object里的public方法,这些方法对于函数式接口来说,不被当成是抽象方法(虽然它们是抽象方法);因为任何一个函数式接口的实现,默认都继承了Object类,包含了来自java.lang.Object里对这些抽象方法的实现;

如下代码不会报错:

@FunctionalInterface
    interface GreetingService  
    {
        void sayMessage(String message);
        
        @Override
        boolean equals(Object obj);
    }

对接口底层不熟悉,可以参考这篇文章:【Java核心技术卷】深入理解Java的接口

浅析Lambda表达式的本质

前面也说了Lambda表达式本质是一种语法糖,那么我们反编译一下,看看编译器为我们做了哪些事情

原始代码

import java.util.ArrayList;
import java.util.List;
public class Lambda {
    public static void main(String[] args) {
        List<Integer> list = new ArrayList<>();
        for (int i = 0; i < 10; i++) {
            list.add(i);
        }
        list.forEach(System.out::println);
    }
}

编译后代码

import java.io.PrintStream;
import java.lang.invoke.LambdaMetafactory;
import java.util.ArrayList;
import java.util.function.Consumer;

public class Lambda {
    public static void main(String[] arrstring) {
        ArrayList<Integer> arrayList = new ArrayList<Integer>();
        for (int i = 0; i < 10; ++i) {
            arrayList.add(i);
        }
        
        PrintStream printStream = System.out;
        printStream.getClass();
        
        arrayList.forEach(
            (Consumer<Integer>)LambdaMetafactory.metafactory(null, null, null, (Ljava/lang/Object;)V, println(java.lang.Object ), (Ljava/lang/Integer;)V)((PrintStream)printStream));
    }

}

这里可以简单看出lambda是在一个类文件中,最多生成多个方法而已,这可以与内部类区分开,内部类是生成了多个类文件。当然编译器处理Lambda语法糖的方式差不多了,其他情况自己试试呗~,当然Lambda本质这一块还涉及到类型推断相关的内容,
类型推断的大致意思是Java编译器来查看每一个方法调用和相应的声明,以确定类型参数(或参数),使调用能够正常实现。推理算法确定参数类型,如果类型推断成功,那么方法返回的值就是那个类型的。最后,推理算法试图找到与所有的变量工作的最具体类型。这又是一大块内容了,想要更进一步了解的,搜搜呗~

如果你不了解内部类底层,可以参考我之前写的这篇文章:【Java核心技术卷】深入理解Java的内部类


本文是基于《Java 8实战》这本书的思考与学习的总结笔记,含少量内容的摘录。


在方法引用部分参考了 平凡希 – Java8之方法引用


转载:https://blog.csdn.net/qq_42322103/article/details/101681233
查看评论
* 以上用户言论只代表其个人观点,不代表本网站的观点或立场