彻底弄懂@FunctionalInterface、Lambda表达式和方法引用

文章目录

      • 1. @FunctionalInterface与“函数类型”
      • 2. JDK提供的“函数类型”
      • 3. Lambda表达式
      • 4. 四种方法引用
      • 5. andThen链式表达
      • 6. 最后

Java语言从JDK1.8开始引入了函数式编程。

函数式编程的核心特点是,函数作为一段功能代码,可以像变量一样进行引用和传递,以便在有需要的时候进行调用。

1. @FunctionalInterface与“函数类型”

Java对函数式编程的支持,本质是通过接口机制来实现的。首先定义一个仅声明一个方法的接口,然后对接口冠以@FunctionalInterface注解,那么这个接口就可以作为“函数类型”,可以接收一段以Lambda表达式,或者方法引用予以承载的逻辑代码。例如:

@FunctionalInterface
interface IntAdder {
    int add(int x, int y);
}

IntAdder adder = (x, y) -> x + y;

IntAdder 就可以看成是一个“函数类型”。
Lambda表达式和方法引用的介绍见后文。

概念如此,需要思考的有几点:

  1. 为什么必须是只声明一个方法的接口?

显然这个方法就是用来代表“函数类型”所能执行的功能,一个函数一旦定义好,它能执行的功能是确定的,就是调用和不调用的区别。接口中声明的方法就是和函数体定义一一对应的。
事实上,@FunctionalInterface下只能声明一个方法,多一个、少一个都不能编译通过 。覆写Object中toString/equals的方法不受此个数限制。 比如Comparator接口就声明了2个方法:

// Comparator.java
@FunctionalInterface
public interface Comparator<T> {
    int compare(T o1, T o2);
    boolean equals(Object obj);
    //...
}
  1. 严格地说,@FunctionalInterface下只能声明一个未实现的方法,default方法和static方法因为带有实现体,所有不受此限制。
   @FunctionalInterface
   public interface IAdd<T, R> {
   	R add(T t1, T t2);
   	
   	default R test1(T t1, T t2) {//可以额外定义default方法
   		return null;
   	}
   	
   	static <T,R> R test2(T t1, T t2) {//可以额外定义static方法
   		return null;
   	}
   }

关于interface中声明default/static方法有疑虑的话,可以查阅博主另一篇文章:java接口里面可以有成员变量么?

  1. @FunctionalInterface注解不是必须的,不加这个注解的接口(前提是只包含一个方法)一样可以作为函数类型。不过,显而易见的是,加了这个注解表意更明确、更直观,是更被推荐的做法。
  2. 要定义清楚一个函数类型,除了函数名称,必须明确规定函数的参数个数和类型返回值类型,这些信息都是包含于接口中声明的方法。

2. JDK提供的“函数类型”

java.util.function包下预定义了常用的函数类型,包括:

@FunctionalInterface
public interface Consumer<T> {
    void accept(T t); //接收一个类型为T(泛型)的参数,无返回值;所以叫消费者
}
@FunctionalInterface
public interface BiConsumer<T, U> {
    void accept(T t, U u);//接收2个参数,无返回值
}
@FunctionalInterface
public interface Supplier<T> {
    T get();//无参数,有返回值(所以叫提供者)
}
//注意没有BiSupplier,因为返回值只能有1个,不会有2个
@FunctionalInterface
public interface Function<T, R> {
    R apply(T t);//一个输入(参数),一个输出(返回值)
}
@FunctionalInterface
public interface BiFunction<T, U, R> {
    R apply(T t, U u);//两个输入T和U,一个输出R
}
@FunctionalInterface
public interface UnaryOperator<T> extends Function<T, T> {
    static <T> UnaryOperator<T> identity() {//一元操作,输入原样返回给输出
        return t -> t;
    }
}
@FunctionalInterface
public interface BinaryOperator<T> extends BiFunction<T,T,T> {//二元操作,输入输出类型相同
    public static <T> BinaryOperator<T> minBy(Comparator<? super T> comparator) {
        Objects.requireNonNull(comparator);
        return (a, b) -> comparator.compare(a, b) <= 0 ? a : b;//传入比较器,返回较小者
    }
    public static <T> BinaryOperator<T> maxBy(Comparator<? super T> comparator) {
        Objects.requireNonNull(comparator);
        return (a, b) -> comparator.compare(a, b) >= 0 ? a : b;//传入比较器,返回较大者
    }
}

这些个定义,都是在参数个数(0,1,2)和有无返回值上做文章。另外还有一些将泛型类型具体化的衍生接口,比如PredicateLongSupplier等等。

@FunctionalInterface
public interface Predicate<T> {
    boolean test(T t);//输入1个参数,返回boolean,就好比是预言家,预言你这个输入是真还是假
}
@FunctionalInterface
public interface LongSupplier {
    long getAsLong();//没有输入,输出long类型(long类型的提供者)
}

3. Lambda表达式

上面弄清楚了函数类型@FunctionalInterface,那么函数类型能接收怎么样的函数实现体呢?怎么接收呢?该Lambda出场了。

Lambda表达式用来定义函数实现体。有很多种写法(都是为了简化书写),但核心是通过->连接参数和实现代码:

(入参)->{实现代码}

//无返回值的时候
(int x)->{System.out.println(x);}
(x)->{System.out.println(x);}//参数类型自动推断
x->{System.out.println(x);}//只有一个参数的时候,可以省略小括号
x->System.out.println(x);//实现体只有一个表达式可以省略大括号,System.out.println本身无返回值
//有返回值的情况
(int x)->{return x*x;}
(x)->{return x*x;}
//x->return x*x; //错误,不能这么写!!
x->x*x;

说了这么多,来实操一把:

IntConsumer ic = x->System.out.println(x);
IntFunction<Integer> ifi1 = x->{return x*x;};
IntFunction<Integer> ifi2 = x->x*x;
ic.accept(100);//100
System.out.println(ifi1.apply(5));//25
System.out.println(ifi2.apply(5));//25

好了,函数类型–>Lambda表达式说明白了,再来看看方法引用是怎么回事。

4. 四种方法引用

文章开头说过了,函数类型可以接收一段Lambda表达式,或者对方法的引用。方法引用就是对一个类中已经存在的方法加以引用,分4中类型:(以Test类为例)

  1. 对类构造方法的引用,如Test::new
  2. 对类静态方法的引用,如Test::staticMethodName
  3. 对对象实例方法的引用,如:new Test()::instanceMethod
  4. 是2和3的结合,如Test::instanceMethod2,但要求函数类型声明和函数调用的时候,其第一个参数必须是Test类的实例。

第4种比较难以说清楚,看看下面的例子吧:

public class Test {
	private String name = "";
	
	public Test() {
		System.out.println("构造方法:无参数");
	}

	public Test(String name) {
		this.name = name;
		System.out.println("构造方法:参数="+name);
	}
	
	public static void staticMethod(String str) {
		System.out.println("static method: input=" + str);
	}
	
	public void instanceMethod(String str) {
		System.out.println("instance method: input=" + str);
	}
	
	public static void main(String[] args) {
		Supplier<Test> s1 = Test::new;//对无参构造器的引用,无参构造器其实就是一个对象的Supplier(提供者)
        s1.get();//调用构造方法:无参数
        
		Function<String, Test> f1 = Test::new;//引用有一个String参数的构造器
        f1.apply("Test");//调用构造方法:参数=Test
        
		Consumer<String> c1 = Test::staticMethod;//对静态方法引用
        c1.accept("1");//static method: input=1
        
		Consumer<String> c2 = new Test()::instanceMethod;//对实例方法的引用
        c2.accept("2");//instance method: input=2
        
		//第4种
		BiConsumer<Test, String> bc1 = Test::instanceMethod;
		bc1.accept(new Test(), "3");//instance method: input=3
	}
} 

第4中方法引用,本质上是对实例方法的引用,只不过是在调用的时候才传入那个实例对象

5. andThen链式表达

JDK中很多函数类型,都实现了default的andThen方法,可以将多个函数体(Lambda表达式、方法引用)串起来,方便进行链式调用。
调用链上的任何一个抛出异常,整个调用链会提前结束,异常由调用者处理。

/**
 * 通过andThen()进行链式操作
 */
@Test
public void testLinkConsumer() {
    IntConsumer action = x-> System.out.print(x);
    action = action.andThen(x->System.out.print("--tail1"))
            .andThen(x->System.out.print("--tail2"));
    //100--tail1--tail2
    action.accept(100);
}

6. 最后

小插曲:Callable和Runnable到底什么区别?

//java.util.concurrent.Callable
@FunctionalInterface
public interface Callable<V> {
    V call() throws Exception;
}
//java.lang.Runnable
@FunctionalInterface
public interface Runnable {
    public abstract void run();
}
  1. 这两者也都是JDK预定义的函数接口,两者都不接收参数,主要用于多线程编程。

  2. Runnable无返回值,一般用于new一个新线程的时候,在新线程中执行代码。

  3. Callable一样一般用于在新线程中执行,只不过执行成功后有返回值,如果执行失败还会抛异常。

最后,一起分析:

Callable<Integer> c1 = ()->1;
Callable<Integer> c2 = ()->c1.call();

c1引用了一个Lambda表达式;

c2引用了一个新的Lambda表达式,表示式的实现代码中调用了c1提供的call()方法,并将call()方法的返回值返回。

Callable<Integer> c1 = ()->1;
Callable<Integer> c2 = ()->c1.call();
Callable<Integer> c3 = ()->{
    System.out.println("c3 call c1");
    return c1.call();
};

try {
    System.out.println(c1.call());//1
    System.out.println(c2.call());//1
    System.out.println(c3.call());//c3 call c1
    //1
} catch (Exception e) {
    e.printStackTrace();
}

注意,c3和c2本质上是一样的,只不过方法实现上,多加了一行打印代码。


原文链接:https://blog.csdn.net/linysuccess/article/details/104751843

你可能感兴趣的:(Java功底,lambda,方法引用,函数接口,函数式编程,Java,Lambda)