Bootstrap

Java基础理论之Lambda表达式,方法引用

函数式编程思想,让编码偏向于过程,让我们书写代码是关注关键所在而不在于固定形式。

函数式编程思想概述

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

做什么,而不是怎么做

传递一段代码——这才是我们真正的目的。而创建对象只是受限于面向对象语法而不得不采取的一种手段方式。那,有没有更加简单的办法?如果我们将关注点从“怎么做”回归到“做什么”的本质上,就会发现只要能够更好地达到目的,过程与形式其实并不重要。

2.2 Lambda的优化

Runnable接口简化:
1. () -> System.out.println("多线程任务执行!")
Comparator接口简化:
2. Arrays.sort(array, (a, b) -> a.getAge() - b.getAge());   a-b 是升序 ,b -a 是降序

本着“一切皆对象”的思想,这种做法是无可厚非的

  • 而实际上,有时候似乎只有方法体才是关键所在

2.3 Lambda的格式

标准格式:

Lambda省去面向对象的条条框框,格式由3个部分组成:

  • 一些参数
  • 一个箭头
  • 一段代码

Lambda表达式的标准格式为:

(参数类型 参数名称) -> { 代码语句 }

格式说明:

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

即制定了一种做事情的方案(其实就是一个方法):

  • 无参数:不需要任何条件即可执行该方案。
  • 无返回值:该方案不产生任何结果。
  • 代码块(方法体):该方案的具体执行步骤。

同样的语义体现在Lambda语法中,要更加简单:

  • 前面的一对小括号代表不需要任何条件;
  • 中间的一个箭头代表将前面的参数传递给后面的代码;
  • 后面的输出语句即业务逻辑代码。

参数和返回值:

Comparator接口简化:

  • 为了排序,Arrays.sort方法需要排序规则,即Comparator接口的实例,抽象方法compare是关键;
  • 为了指定compare的方法体,不得不需要Comparator接口的实现类;
  • 为了省去定义一个ComparatorImpl实现类的麻烦,不得不使用匿名内部类;
  • 必须覆盖重写抽象compare方法,所以方法名称、方法参数、方法返回值不得不再写一遍,且不能写错;
  • 实际上,只有参数和方法体才是关键

省略格式:

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

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

备注:掌握这些省略规则后,请对应地回顾本章开头的多线程案例。

可推导即可省略

Lambda强调的是“做什么”而不是“怎么做”,所以凡是可以根据上下文推导得知的信息,都可以省略。

Lambda的前提条件

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

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

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

函数式接口

概述

函数式接口在Java中是指:有且仅有一个抽象方法的接口

函数式接口,即适用于函数式编程场景的接口。而Java中的函数式编程体现就是Lambda,所以函数式接口就是可以适用于Lambda使用的接口。只有确保接口中有且仅有一个抽象方法,Java中的Lambda才能顺利地进行推导。

备注:从应用层面来讲,Java中的Lambda可以看做是匿名内部类的简化格式,但是二者在原理上不同。

格式

只要确保接口中有且仅有一个抽象方法即可:

修饰符 interface 接口名称 {
    public abstract 返回值类型 方法名称(可选参数信息);
    // 其他非抽象方法内容
}

自定义函数式接口

对于刚刚定义好的MyFunctionalInterface函数式接口,典型使用场景就是作为方法的参数:

public class FunctionalInterface {	
	// 使用自定义的函数式接口作为方法参数
	private static void doSomething(MyFunctionalInterface inter) {
		inter.myMethod(); // 调用自定义的函数式接口方法
	}
	
	public static void main(String[] args) {
		// 调用使用函数式接口的方法
		doSomething(() -> System.out.println("Lambda执行啦!"));
	}
}

FunctionalInterface注解

@Override注解的作用类似,Java 8中专门为函数式接口引入了一个新的注解:@FunctionalInterface。该注解可用于一个接口的定义上:

@FunctionalInterface
public interface MyFunctionalInterface {
	void myMethod();
}

一旦使用该注解来定义接口,编译器将会强制检查该接口是否确实有且仅有一个抽象方法,否则将会报错。不过,即使不使用该注解,只要满足函数式接口的定义,这仍然是一个函数式接口,使用起来都一样。

调用过程

常用函数式接口

JDK提供了大量常用的函数式接口以丰富Lambda的典型使用场景,它们主要在java.util.function包中被提供。前文的MySupplier接口就是在模拟一个函数式接口:java.util.function.Supplier<T>。其实还有很多,下面是最简单的几个接口及使用示例。

Supplier接口

java.util.function.Supplier<T>接口,它意味着"供给" , 对应的Lambda表达式需要“对外提供”一个符合泛型类型的对象数据。

抽象方法 : get

仅包含一个无参的方法:T get()。用来获取一个泛型参数指定类型的对象数据。

Consumer接口

java.util.function.Consumer<T>接口则正好相反,它不是生产一个数据,而是消费一个数据,其数据类型由泛型参数决定。

抽象方法:accept

Consumer接口中包含抽象方法void accept(T t),意为消费一个指定泛型的数据。

默认方法:andThen

如果一个方法的参数和返回值全都是Consumer类型,那么就可以实现效果:消费一个数据的时候,首先做一个操作,然后再做一个操作,实现组合。而这个方法就是Consumer接口中的default方法andThen。下面是JDK的源代码:

default Consumer<T> andThen(Consumer<? super T> after) {
    Objects.requireNonNull(after);
    return (T t) -> { accept(t); after.accept(t); };
}

备注:java.util.ObjectsrequireNonNull静态方法将会在参数为null时主动抛出NullPointerException异常。这省去了重复编写if语句和抛出空指针异常的麻烦。

要想实现组合,需要两个或多个Lambda表达式即可,而andThen的语义正是“一步接一步”操作。例如两个步骤组合的情况:

Function接口

java.util.function.Function<T,R>接口用来根据一个类型的数据得到另一个类型的数据,前者称为前置条件,后者称为后置条件。有进有出,所以称为“函数Function”。

抽象方法:apply

Function接口中最主要的抽象方法为:R apply(T t),根据类型T的参数获取类型R的结果。

默认方法:andThen

Function接口中有一个默认的andThen方法,用来进行组合操作。JDK源代码如:

default <V> Function<T, V> andThen(Function<? super R, ? extends V> after) {
    Objects.requireNonNull(after);
    return (T t) -> after.apply(apply(t));
}

该方法同样用于“先做什么,再做什么”的场景,和Consumer中的andThen差不多:

`

请注意,Function的前置条件泛型和后置条件泛型可以相同。

Predicate接口

有时候我们需要对某种类型的数据进行判断,从而得到一个boolean值结果。这时可以使用java.util.function.Predicate<T>接口。

抽象方法:test

Predicate接口中包含一个抽象方法:boolean test(T t)。用于条件判断的场景:

条件判断的标准是传入的Lambda表达式逻辑,只要字符串长度大于5则认为很长。

默认方法:and

既然是条件判断,就会存在与、或、非三种常见的逻辑关系。其中将两个Predicate条件使用“与”逻辑连接起来实现“并且”的效果时,可以使用default方法and。其JDK源码为:

default Predicate<T> and(Predicate<? super T> other) {
    Objects.requireNonNull(other);
    return (t) -> test(t) && other.test(t);
}
 // 去重
    private <T> Predicate<T> distinctByKey(Function<? super T, Object> keyExtractor) {
        Map<Object, Boolean> seen = new ConcurrentHashMap<>();
        return t -> seen.putIfAbsent(keyExtractor.apply(t), Boolean.TRUE) == null;
    }

List<SeededAccountManagerDto> SeededAccountManagerDtoList = initList.stream().filter(distinctByKey((p) -> (p.getPhoneNum()))).collect(Collectors.toList());

默认方法:or

and的“与”类似,默认方法or实现逻辑关系中的“”。JDK源码为:

default Predicate<T> or(Predicate<? super T> other) {
    Objects.requireNonNull(other);
    return (t) -> test(t) || other.test(t);
}

如果希望实现逻辑“字符串包含大写H或者包含大写W”,那么代码只需要将“and”修改为“or”名称即可

默认方法:negate

“与”、“或”已经了解了,剩下的“非”(取反)也会简单。默认方法negate的JDK源代码为:

default Predicate<T> negate() {
    return (t) -> !test(t);
}

从实现中很容易看出,它是执行了test方法之后,对结果boolean值进行“!”取反而已。

举个栗子

package com.youngchan.practice;


import java.util.function.Consumer;
import java.util.function.Function;
import java.util.function.Predicate;
import java.util.function.Supplier;

/**
 * @author youngchan
 * @version V1.0
 * @Package com.youngchan.practice
 * @date 2020/4/27 19:34
 */
public class LambdaPractice {


    public static void practiceLambda() {
        myStaticMethod(() -> System.out.println("无参无返回值方法"));
        String superlier = "superlier   interface 有返回值无参方法";
        System.out.println(myStaticSupplierMethod(() -> superlier.toUpperCase()));
        String consumer = "consumer  interface 有参数没有返回值方法";
        myStaticConsumerMethod(consumerTemp -> System.out.println(consumerTemp), consumer);
        String function = "function interface 有参有返回值方法";
        String functionAdd = "加工返回值";
        System.out.println(myStaticFunctionMethod(functionTemp -> functionTemp + functionAdd, function));
        System.out.println(myStaticFunctionMethod(functionTemp -> {
            return functionTemp + functionAdd;
        }, function));

        String predicate = "predicate interface 有参返回值为布尔值的方法";
        System.out.println(myStaticPredicateMethod(predicateTemp -> predicate.length() > 10, predicate));
    }

    /**
     * 无参无返回值 方法实现
     * @param inter
     */
    private static void myStaticMethod(MyInterface inter) {
        inter.myMethod();
    }

    /**
     * 有返回值无参 方法实现
     * @param interfaceSupplier
     * @return
     */
    private static String myStaticSupplierMethod(MyInterfaceSupplier interfaceSupplier) {
        return interfaceSupplier.myMethodSupplier();
    }


    /**
     * 有参无返回值方法实现
     * @param interfaceConsumer
     * @param str
     */
    private static void myStaticConsumerMethod(MyInterfaceConsumer<String> interfaceConsumer, String str) {
        interfaceConsumer.myMethodConsumer(str);
    }


    /**
     * 有参有返回值方法实现
     * @param interfaceFunction
     * @param function
     * @return
     */
    private static String myStaticFunctionMethod(MyInterfaceFunction<String, String> interfaceFunction, String function) {
        return interfaceFunction.myMethodFunction(function);
    }


    /**
     * 有参有布尔返回值方法实现
     * @param interfacePredicate
     * @param predicate
     * @return
     */
    private static Boolean myStaticPredicateMethod(MyInterfacePredicate<String> interfacePredicate, String predicate) {
        return interfacePredicate.myMethodPredicate(predicate);
    }



    /**
     * 无参无返回值接口
     */
    @FunctionalInterface
    interface MyInterface {
        void myMethod();
    }


    /**
     * 有返回值无参Supplier
     */
    @FunctionalInterface
    interface MyInterfaceSupplier {
        String myMethodSupplier();
    }

    interface MyInterfaceSupplier2 extends Supplier<String> {
        @Override
        String get();
    }

    /**
     * 有参无返回值Consumer
     */
    @FunctionalInterface
    interface MyInterfaceConsumer<T> {
        void myMethodConsumer(T t);
    }

    interface MyInterfaceConsumer2<T> extends Consumer<String> {
        @Override
        void accept(String s);

        @Override
        default Consumer<String> andThen(Consumer<? super String> after) {
            return null;
        }
    }

    /**
     * 有参有返回值Function
     */
    @FunctionalInterface
    interface MyInterfaceFunction<T, R> {
        R myMethodFunction(T t);
    }

    interface MyInterfaceFunction2<T, R> extends Function<String, String> {

        @Override
        String apply(String s);

        @Override
        default <V> Function<V, String> compose(Function<? super V, ? extends String> before) {
            return null;
        }

        @Override
        default <V> Function<String, V> andThen(Function<? super String, ? extends V> after) {
            return null;
        }
    }
    /**
     * 有参布尔返回值Function
     */
    @FunctionalInterface
    interface MyInterfacePredicate<T> {
        Boolean myMethodPredicate(T t);
    }

    interface MyInterfacePredicate2<T> extends Predicate<String> {
        @Override
        boolean test(String s);

        @Override
        default Predicate<String> and(Predicate<? super String> other) {
            return null;
        }

        @Override
        default Predicate<String> negate() {
            return null;
        }

        @Override
        default Predicate<String> or(Predicate<? super String> other) {
            return null;
        }
    }

}

Lambda表达式和匿名内部类的区别

 

方法引用

概述

在使用Lambda表达式的时候,我们实际上传递进去的代码就是一种解决方案:拿什么参数做什么操作。那么考虑一种情况:如果我们在Lambda中所指定的操作方案,已经有地方存在相同方案,那是否还有必要再写重复逻辑?

冗余的Lambda场景优化

来看一个简单的函数式接口以应用Lambda表达式 , 那么通过Lambda来使用它的代码很简单:

	// 不使用方法引用
      	printString(s -> System.out.println(s), "Hello World");
	// 使用方法引用
        printString(System.out::println, "HelloWorld"); 

请注意其中的双冒号::写法,这被称为“方法引用”,而双冒号是一种新的语法。

方法引用符

符号表示 : ::

符号说明 : 双冒号为方法引用运算符,而它所在的表达式被称为方法引用

**应用场景 : **如果Lambda要表达的函数方案 , 已经存在于某个方法的实现中,那么则可以使用方法引用。

如上例中,System.out对象中有个println(String)方法 , 恰好就是我们所需要的 , 那么对于Consumer接口作为参数,对比下面两种写法,完全等效:

  • Lambda表达式写法:s -> System.out.println(s);
    拿到参数之后经Lambda之手,继而传递给System.out.println方法去处理。
  • 方法引用写法:System.out::println
    直接让System.out中的println方法来取代Lambda。

**推导与省略 : ** 如果使用Lambda,那么根据“可推导就是可省略”的原则,无需指定参数类型,也无需指定的重载形式——它们都将被自动推导。而如果使用方法引用,也是同样可以根据上下文进行推导。函数式接口是Lambda的基础,而方法引用是Lambda的简化形式。

常见的方法引用

对象名–引用成员方法

这是最常见的一种用法,与上例相同。如果一个类中已经存在了一个成员方法,则可以通过对象名引用成员方法,代码为:

public class DemoOne {
     public static void main(String[] args) {
        String str = "hello";
        printUP(str::toUpperCase);
    }

    public static void printUP(Supplier< String> sup ){
        String apply =sup.get();
        System.out.println(apply);
    }
}

类名–引用静态方法

由于在java.lang.Math类中已经存在了静态方法random,所以当我们需要通过Lambda来调用该方法时,可以使用方法引用 , 写法是:

public class DeomTwo{
  public static void main(String[] args) {
        printRanNum(Math::random);
    }

    public static void printRanNum(Supplier<Double> sup ){
        Double apply =sup.get();
        System.out.println(apply);
    }
}

在这个例子中,下面两种写法是等效的:

  • Lambda表达式:n -> Math.abs(n)
  • 方法引用:Math::abs

类–构造引用

由于构造器的名称与类名完全一样,并不固定。所以构造器引用使用类名称::new的格式表示。首先是一个简单的Person类:

public class Person {
    private String name;
    public Person(String name) {
      	this.name = name;
    }
    public String getName() {
      	return name;
    }
}

要使用这个函数式接口,可以通过方法引用传递:

public class DemoThree {
    public static void main(String[] args) {
		String name = "tom";
        Person person = createPerson(Person::new, name);
        System.out.println(person);
        
    }

    public static Person createPerson(Function<String, Person> fun , String name){
        Person p = fun.apply(name);
        return p;

    }
}

在这个例子中,下面两种写法是等效的:

  • Lambda表达式:name -> new Person(name)
  • 方法引用:Person::new

数组–构造引用

数组也是Object的子类对象,所以同样具有构造器,只是语法稍有不同。如果对应到Lambda的使用场景中时,需要一个函数式接口:

在应用该接口的时候,可以通过方法引用传递:

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

        int[] array = createArray(int[]::new, 3);
        System.out.println(array.length);

    }

    public static int[] createArray(Function<Integer , int[]> fun , int n){
        int[] p = fun.apply(n);
        return p;

    }
}

在这个例子中,下面两种写法是等效的:

  • Lambda表达式:length -> new int[length]
  • 方法引用:int[]::new

注意 :
方法引用是对Lambda表达式符合特定情况下的一种缩写,它使得我们的Lambda表达式更加的精简,也可以理解为Lambda表达式的缩写形式 , 同学们可以尝试着 , 将之前使用lambda的地方 , 改写成方法引用的形式 ,不过要注意的是方法引用只能"引用"已经存在的方法!

;