Java8—Lambda表达式方法引用默认方法的详细介绍一万字

Posted 刘Java

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Java8—Lambda表达式方法引用默认方法的详细介绍一万字相关的知识,希望对你有一定的参考价值。

基于Java8详细介绍了lambda表达式的语法与使用,以及方法引用、函数式接口、lambda复合等Java8的新特性!

文章目录

1 Lambda的概述

面向对象的语言强调“必须通过对象的形式来做事情”,做一件事情,找一个能解决这个事情的对象,调用对象的方法,完成事情。

无论什么情况,当我们在一个方法中需要调用另一个方法的时候,传递的参数必须是一个含有该方法的对象,对象作为一等公民!对于某些可以独立的单个方法(行为),比如比较的方法,在面向对象的程序设计中,同样必须使用一个对象来进行封装,比如Comparable、Comparator,虽然Java中已经使用接口这种更加抽象的类型来封装“比较”这种方法(行为),但是在一个方法中调用比较的方法的时候,我们仍然需要传递一个接口的实现类的对象,然后再方法中调用这个对象的方法,就会很麻烦!因为实际上我们只需要进行比较的这个方法(行为),它却必须要传递一个对象进来!

/**
 * 比较对象的方法
 *
 * @param comparator 比较器
 * @param i          i
 * @param j          j
 */
public static int cmp(Comparator comparator, int i, int j) 
    return comparator.compare(i, j);



@Test
public void testJava() 
    //传递一个对象
    int cmp = cmp(new Comparator<Integer>() 
        @Override
        public int compare(Integer o1, Integer o2) 
            return o1 - o2;
        
    , 1, 2);
    System.out.println(cmp);

如上图,我们有一个比较对象的方法cmp,在使用传统Java代码编程时,即使最简单的方式,也需要传递一个匿名内部类对象进去,但是我们实际上需要的只是比较的行为而已,并不需要对象!

面向对象的编程思想自有它的好处,比如封装性,可重用性,多态性。但是程序设计的世界里,想要依靠一种方法打遍天下并且还是最优的解,那几乎是不可能的!Java 8开始,支持lambda表达式,就是为了解决面向对象编程思想在某些时候(比如单个方法的调用)显得很笨重而又啰嗦!

lambda表达式是一种函数式的编程思想,尽量忽略面向对象的复杂语法。函数式的编程思想中,函数作为一等公民,这里的函数可以类比Java中的方法,描述的是一种行为!当一个方法(函数)中调用另一个方法时,直接将该方法(行为)作为参数传递即可。这样相比于面向对象的编程思想来说,可以让编程更加简单!
对于上面的代码,我们使用lambda表达式改造之后,如下所示:

@Test
public void testLambda() 
    int cmp = cmp((Comparator<Integer>) (o1, o2) -> o1 - o2, 1, 2);
    System.out.println(cmp);

可以看到,lambda表达式的应用让代码编程非常简单明了,我们直接将比较的行为作为参数传递给了cmp方法,连匿名对象都没有了!

2 函数式接口

简单的说,函数式接口(Functional Interface)就是只定义一个抽象方法的接口,并且只有在函数式接口中才能使用lambda表达式。为此,Java 8的时候新增加了一个@FunctionalInterface注解,用来表明某个接口是函数式接口。注意一个函数式接口可以选择加上该注解也可以不加上该注解,这个注解简单的说可以作为一种检验!

lambda表达式实际上就是对函数式接口的唯一抽象方法起作用的,即相当于可以把抽象方法的实现作为函数式接口的具体实现的实例来当作参数传递!这类似于匿名内部类!

Java8之前就有许多函数式接口,比如Runnable、Callable、Comparator、Comparable……,在Java8的时候,为了更好的支持lambda,新增了一个java.util.function包,这个包下面的有很多的接口,这些接口全部都是函数式接口,它们都用于描述某个行为,方便lambda的使用!

下面我们将介绍常用的四种接口:Consumer消费型接口、Supplier供给型接口、Function函数型接口、Predicate断言型接口,最后会附上大部分函数式接口的不同行为和功能!可能某些案例的lambda表达式看不太懂,不过没关系,下一节将会讲解lambda的语法!

2.1 Consumer消费型接口

@FunctionalInterface
public interface Consumer<T> 

    /**
     * @param t 输入参数
     */
    void accept(T t);

    //……

Consumer接口中有一个accept抽象方法,它用于接收一个泛型参数T,然而并没有返回值,顾名思义,就是对传递的参数进行“消费”,没有输出,就像消费者一样!

我们可以将其应用在对某些输入数据的处理但是不需要输出的情况中!下面的案例中,我们需要对集合中的所有int元素进行+1然后输出的操作:

/**
 * @author lx
 */
public class ConsumerTest 

    public static void main(String[] args) 
        //一个初始化集合
        List<Integer> objects = new ArrayList<>();
        objects.add(1);
        objects.add(2);
        objects.add(3);

        //对集合数据进行  加1然后输出的操作
        consume(objects, i -> System.out.println(i + 1));
    

    /**
     * 使用Consumer对集合元素进行操作的方法
     *
     * @param list     需要操作的集合
     * @param consumer 对元素的具体的操作,在调用的时候传递某个动作就行了
     */
    private static <T> void consume(List<T> list, Consumer<T> consumer) 
        for (T t : list) 
            consumer.accept(t);
        
    

2.2 Supplier供给型接口

@FunctionalInterface
public interface Supplier<T> 

    /**
     * @return 获取一个返回结果
     */
    T get();

Supplier接口中有一个get抽象方法,它不接收任何参数,但是返回一个T类型的结果,顾名思义,就是没有输入,只有输出,就像生产者一样!

我们可以将其应用在创建某些对象、获取数据数据的情况中。下面的案例中,我们需要用集合收集10个随机数:

/**
 * @author lx
 */
public class SupplierTest 

    public static void main(String[] args) 
        //一个初始化集合
        List<Integer> objects = new ArrayList<>();
        //我们需要填充10个随机数,Supplier是一个获取随机数的动作
        Random random = new Random();
        supplier(objects, 10, () -> random.nextInt(10));
        //输出集合数据
        System.out.println(objects);
    

    /**
     * 填充集合数据的方法
     *
     * @param list     需要填充的集合
     * @param count    需要填充的数量
     * @param supplier 获取数据的的操作,在调用的时候传递某个动作就行了
     */
    private static <T> void supplier(List<T> list, int count, Supplier<T> supplier) 
        for (int i = 0; i < count; i++) 
            list.add(supplier.get());
        
    

2.3 Function< T, R >函数型接口

@FunctionalInterface
public interface Function<T, R> 

    /**
     * 将指定函数应用于给定的参数。
     *
     * @param t 函数参数
     * @return 函数结果
     */
    R apply(T t);

Function接口中有一个apply抽象方法,它接收T类型的参数,返回一个R类型的结果,顾名思义,就是一个参数T到R的映射操作,就像一个函数一样!

我们可以将其应用在对某个输入对象进行变换、操作然后输出另一个对象(也可以是自己)的情况中。下面的案例中,我们需要对集合中的所有int元素进行自增1的操作:

/**
 * @author lx
 */
public class FunctionTest 

    public static void main(String[] args) 
        //一个初始化集合
        List<Integer> objects = new ArrayList<>();
        objects.add(1);
        objects.add(2);
        objects.add(3);

        //对集合中的数据进行 自增1的操作
        function(objects, i -> ++i);
        //输出集合数据
        System.out.println(objects);
    


    /**
     * 使用Function对集合元素进行操作的方法
     *
     * @param list     需要操作的集合
     * @param function 对元素的具体的函数操作,在调用的时候传递某个动作就行了
     */
    private static <T> void function(List<T> list, Function<T, T> function) 
        for (int i = 0; i < list.size(); i++) 
            //将通过传入的函数操作获取的结果替换原来的集合对应的数据
            list.set(i, function.apply(list.get(i)));
        
    

2.4 Predicate断言型接口

@FunctionalInterface
public interface Predicate<T> 

    /**
     * 对给定的参数进行断言的方法
     *
     * @param t 输入参数
     * @return 如果参数符合规则,那么返回true,否则返回false
     */
    boolean test(T t);

2.5 其他接口以及功能

java.util.function包中的大多数其他函数式接口都是一个特性化的接口,即它们的功能和上面的四大接口都差不多,区别可能是参数数量和类型以及返回值类型!

函数描述符:用来描述函数的参数以及返回值的类型,()表示无参,void表示无返回值,中间使用->连接。

函数式接口函数描述符特性化接口
Predicate< T >T->booleanIntPredicate,LongPredicate, DoublePredicate
Consumer< T >T->voidIntConsumer,LongConsumer, DoubleConsumer
Function< T,R >T->RIntFunction< R >,IntToDoubleFunction,IntToLongFunction,LongFunction< R >,LongToDoubleFunction,LongToIntFunction,DoubleFunction< R >,ToIntFunction< T >,ToDoubleFunction< T >,ToLongFunction< T >
Supplier< T >()->TBooleanSupplier,IntSupplier, LongSupplier
UnaryOperator< T >T->TIntUnaryOperator,LongUnaryOperator,DoubleUnaryOperator
BinaryOperator< T >(T,T)->TIntBinaryOperator,LongBinaryOperator,DoubleBinaryOperator
BiPredicate< L,R >(L,R)->boolean
BiConsumer< T,U >(T,U)->voidObjIntConsumer< T >,ObjLongConsumer< T >,ObjDoubleConsumer< T >
BiFunction< T,U,R >(T,U)->RToIntBiFunction< T,U >,ToLongBiFunction< T,U >,ToDoubleBiFunction< T,U >

前四个都介绍了,后面的其实都差不多,只是参数数量和类型以及返回值类型有差异:

  1. UnaryOperator:一元操作器,一个参数一个返回值,类似于Function,不过参数和返回值类型一致。
  2. BinaryOperator:二元操作器,两个参数一个返回值,类型一致。
  3. BiPredicate:二元断言,传递两个可以不同类型的参数,返回一个boolean类型。
  4. BiConsumer:二元消费,传递两个可以不同类型的参数,无返回值。
  5. BiFunction:二元函数,传递两个可以不同类型的参数,一个返回值可以是不同类型。

下面我们正式学习lambda的语法!

3 Lambda的语法

3.1 具体格式

lambda表达式的标准格式为

(参数类型 参数名称, 参数类型 参数名称) ‐> 代码语句

小括号内的语法与传统方法参数列表一致:无参数则留空;多个参数则用逗号分隔。-> 是新引入的语法格式,代表指向动作。大括号内的语法与传统方法体要求基本一致。

比如对Comparator接口使用匿名内部类对象和lambda表达式:

//使用传统匿名内部类
Comparator<Integer> comparable1 = new Comparator<Integer>() 
    @Override
    public int compare(Integer o1, Integer o2) 
        return o1 - o2;
    
;

//使用lambda表达式标准语法
Comparator<Integer> comparable2 = (Integer o1, Integer o2) -> 
    return o1 - o2;
;

当然,如果使用idea,那么可能会提示你这个lambda表达式还有更精简的写法。

//使用lambda表达式优化语法
Comparator<Integer> comparable3 = (o1, o2) -> o1 - o2;

可以看到,此时我们的lambda表达式更加精简了,同时也更加通俗易懂,那就是通过比较两个数的差值来比较大小!

在lambda标准格式的基础上,使用省略写法的规则为:

  1. 小括号内参数的类型可以省略;
  2. 如果小括号内有且仅有一个参,则小括号可以省略;
  3. 如果大括号内有且仅有一个语句,则无论是否有返回值,都可以省略大括号和return关键字及语句的分号,即无论有没有返回值,都可以省略return。

由此,我们可以知道,同一个lambda表达式可以对应不同的实际目标类型,比如下面的例子,同样的lambda表达式,却可以赋值给不同的类型!

//对应Comparator目标类型
Comparator<Integer> comparable3 = (o1, o2) -> o1 - o2;

//对应BinaryOperator目标类型
BinaryOperator<Integer> binaryOperator = (o1, o2) -> o1 - o2;

实际上,lambda表达式也有自己的类型,但是它的类型是通过上下文(参数类型、返回值类型、包括泛型类型)推断得来的。在上面的Comparator的精简写法中,参数类型被省略了,因为可以通过返回值的泛型类型Integer推断出来,参数的类型也一定是Integer类型,另外通过返回的类型可以推断出这个lambda一定是Comparator类型。

在下面的lambda表达式中,参数类型同样被省略了,因为可以通过cmp方法的第一个参数可以推断出,参数类型一定是Byte类型,并且根据第一个参数的目标类型可以推断出这个lambda表达式一定是Comparator类型!

@Test
public void test1() 
    cmp((o1, o2) -> (o1 - o2), (byte) 1, (byte) 1);


public int cmp(Comparator<Byte> comparator, byte l1, byte l2) 
    return comparator.compare(l1, l2);

这里的上下文推断就类似于JDK1.7出现的针对集合的类型推断<>符号:

//JDK1.7开始,右侧构造器可以使用<>当作泛型推断
List<String> strings = new ArrayList<>();

只不过Java8的时候对类型推断做了进一步增强,使用上下文推断可以在使用Lambda表达式时用来推断合法的Lambda表达式的类型的上下文,而不必在代码中强制转型或者注明类型!可推导即可省略!

3.2 使用要求

Lambda的语法非常简洁,完全没有面向对象复杂的束缚。但是使用时有几个问题需要特别注意:

  1. 使用Lambda必须具有接口,且要求接口中有且仅有一个抽象方法。
  2. 使用Lambda必须具有上下文推断。也就是方法的参数或局部变量类型必须为Lambda对应的接口类型,才能使用Lambda作为该接口的实例。
  3. 在 Lambda 表达式中不允许声明一个与局部变量同名的参数或者局部变量。
  4. 在 Lambda 表达式中,允许引用最终变量、静态变量、局部变量,但是只允许修改静态变量,以及对象类型的局部变量的属性(这要求后面的代码不会修改这个局部变量的引用指向),对于局部变量本身的引用指向以及基本类型的变量则不允许修改
  5. 对应第四条的另一种解释,lambda表达式的局部变量可以不用声明为final,但是实际上是具有隐式的final的的语义,即必须不可被后面的代码修改,否则会编译错误。

为什么会对局部变量有这些限制呢?主要是因为对象类型局部变量的引用以及基本类型的局部变量都保存栈上,存在某一个线程之中,如果Lambda可以直接访问并修改栈上的变量,并且Lambda是在另一个线程中使用的,那么使用Lambda的线程可能会在分配该变量的线程将这个变量收回之后,继续去访问该变量。因此,Java在访问栈上的局部变量时,实际上是在访问它的副本,而不是访问原始变量,从而造成线程不安全的可能,特别是并行运算的时候。但是如果局部变量仅仅被最开始赋值一次,以后不会再次变动,那就没有这种隐患了——因此就有了这个限制,即局部变量除了最开始的赋值之后都是读操作,而没有写操作,那么可以读取这个局部变量,相当于final的语义了。

由于对局部变量的限制,Lambda表达式在 Java 中又称为闭包或匿名函数。它们可以作为参数传递给方法,并且可以访问其作用域之外的变量。但是它们不能修改定义Lambda的方法的局部变量的内容,这些变量必须是隐式最终的。因此可以认为Lambda是对值封闭,而不是对变量封闭,因为可以访问局部变量,但不可修改值。为什么对象类型的额局部变量的属性可以修改呢?因为它们保存在堆中,而堆是在线程之间共享的!因此我们如果需要在lambda中修改某个基本变量,那么可以使用该变量的包装类。然后再修改属性值即可。

关于变量的测试案例如下:

//静态全局变量
static int k = 1;
static AtomicInteger stinteger = new AtomicInteger(1);

@Test
public void test3() 
    int i = 1;
    Object o = new Object();
    AtomicInteger integer = new AtomicInteger(1);
    //使用lambda表达式标准语法
    Comparator<Integer> comparable = (Integer o1, Integer o2) -> 
        //在后面的语句中不会修改这个局部变量的值时,可以在lambda中访问基本局部变量,但是不可操作值
        int b = i;
        System.out.println(i);

        //在后面的语句中不会修改这个对象局部变量的引用指向时,可以操作或者访问这个对象的属性
        integer.addAndGet(1);
        integer.get();
        integer.set(10);

        //静态变量的引用指向可以修改
        stinteger = new AtomicInteger(2);
        //静态变量的值可以修改
        k = 2;
        return o1 - o2;
    ;

    //在后面的语句中改变基本局部变量的值之后,lambda中对该变量的任何访问操作都将编译不通过
    // i = 2;

    //在后面的语句中改变对象局部变量的引用指向之后,lambda中对该变量的任何访问操作都将编译不通过
    //integer= new AtomicInteger(1);

    //在后面的语句中可以操作或者访问这个对象的属性
    integer.set(15);

    //静态变量的引用指向可以修改
    stinteger = new AtomicInteger(3);
    //静态变量的值可以修改
    k = 3;


static class Run implements Runnable 

    @Override
    public void run() 

    

4 方法引用

到此之前,我们已经会使用Lambda表达式创建匿名方法,自己实现方法体,但是有时候,我们的Lambda表达式可能仅仅调用一个已存在的方法,而不做任何其它事,对于这种情况,通过一个方法名字来引用这个已存在的方法会更加清晰,Java 8的方法引用允许我们这样做。方法引用简单的格式通过引用一个已经存在的方法,同时实现了代码的复用,进一步简化了lambda的复杂度。

方法引用的目标很明显,因为方法可以看作一个已经存在的定义好的函数,当我们要传递的函数已经被某个方法实现了的时候,那么则可以通过双冒号"::"操作符来引用该方法作为 Lambda 的替代者。

/**
 1. @author lx
 */
public class User 
    private int age;

    public int getAge() 
        return age;
    

    public void setAge(int age) 
        this.age = age;
    


    public static void main(String[] args) 
        //获取User的age
        //lambda标准格式的优化写法,内部只是引用了一个方法
        Function<User, Integer> userIntegerFunction1 = o -> o.getAge();

        //获取User的age
        //使用方法引用之后的写法,更加精简
        Function<User, Integer> userIntegerFunction2 = User::getAge;
    

方法引用同样可以使用传递的参数类型和参数个数进行推导。比如上面的lambda表达式可知道参数o的类型为User,同时它调用了getAge方法,返回一个Inteer。因此可以直接使用方法引用User::getAge,简化了方法的调用与参数的传递。这些都是可以推倒的,lambda遵循” 可推导即可省略”原则,或者说方法引用可以看作针对lambda的语法糖!

怎么才能将lambda表达式转换为方法引用呢?或者说什么情况才能使用方法引用呢?

  1. 要求Lambda 表达式的方法体中只有一句话,并且这句话就是调用另一个方法,此时就可能使用方法引用代替手动调用该方法。
  2. 特殊情况下,如果抽象方法的第一个参数就是内部调用该方法的实例,那么被调用的方法与函数式接口中的抽象方法的参数个数可以不相同,但是要求后面的参数和方法参数的顺序一致,类型相同(或者兼容)。如果不是这种特殊情况,那么还要求被调用的方法与函数式接口中的抽象方法的参数个数和顺序一致,类型相同(或者兼容)。
  3. 被调用的方法与函数式接口中的抽象方法返回值类型相同(或者兼容),与方法名无关。

方法引用有很多种,它们的语法如下,都需要遵循上面的原则:

  1. 类型上的静态方法引用:ClassName::methodName
  2. 实例上的实例方法引用:instanceReference::methodName
  3. 类型上的实例方法引用:ClassName::methodName
  4. 超类实例上的实例方法引用:super::methodName
  5. 构造方法引用:ClassName::new
  6. 数组构造方法引用:TypeClassName [ ]::new

方法引用案例如下:

/**
 * @author lx
 */
public class Person 
    private int age;

    public int getAge() 
        return age;
    

    public void setAge(int age) 
        this.age = age;
    


    public static void print() 
        System.out.println("静态方法");
    

    public static Person instance() 
        return new Person();
    


    public Integer gett(Object str, Integer integer) 
        setAge(integer);
        return getAge();
    


    public Integer gett(int integer, Object str) 
        setAge(integer);
        return getAge();
    

    @Test
    public void test() 
        //一个user实例
        Person user = new Person();

        //类型上的实例方法引用:ClassName::methodName
        Function<Person, Integer> userIntegerFunction2 = Person::getAge;

        //类型上的实例方法引用:ClassName::methodName
        Consumer<Person> userConsumer2 = System.out::println;



        //实例上的实例方法引用:instanceReference::methodName
        Supplier<Integer> supplier = user::getAge;



        //类型上的静态方法引用:ClassName::methodName
        Supplier<Person> userSupplier = Person::instance;

        //类型上的静态方法引用:ClassName::methodName
        Print print = Person::print;



        //超类实例上的实例方法引用:super::methodName
        Supplier<Class> SupSupplier = super::getClass;




        

以上是关于Java8—Lambda表达式方法引用默认方法的详细介绍一万字的主要内容,如果未能解决你的问题,请参考以下文章

Java8 之 lambda 表达式方法引用函数式接口默认方式静态方法

Java8 lambda表达式函数式接口方法引用

Java8新特性:Lambda表达式函数式接口以及方法引用

Java8函数式编程

Java8函数式编程

Java8函数式编程