Java-Lambda表达式和“方法引用”的对比和详解

Lambda表达式

一、Lambda表达式简介

1.1什么是Lamdba表达式?

 Lambda表达式是Java 8 添加的一个新特性,可以认为,Lambda是一个匿名函数(相似于匿名内部类),作用是返回一个实现了接口的对象(这个观点非常重要,贯穿于Lambda表达式的整个使用过程)。

1.2为什么使用Lambada表达式?

 使用Lambda表达式对比于其他接口实现方式显得非常简洁。(详见3种接口实现的方法代码块CodeBlock-1)

1.3Lambda对接口的要求?

 虽然Lambda表达式对某些接口进行简单的实现,但是并不是所有的接口都可以使用Lambda表达式来实现,要求接口种定义的必须要实现的抽象方法只能是一个(注意:具体方法可以多个或者没有)。

在Java 8 中对接口增加了新特性:default,提供了一个默认的抽象方法,但是Lambda对此没有特殊的影响,方法可以按Lambda所表达的来。

1.4注意事项:

  1. @FunctionalInterface
     这个注解用于修饰函数式接口,即意味着接口中的抽象方法只能有一个,否则编译器会报错。
  2. 我们总是需要对象来实现接口,Lambda表达式就是帮助我们简化这个过程,而对象中的单独的方法在对象的创建接口对象的创建过程中并不会执行。4.2小节中构造方法在Lambda表达式中的调用,其更像一种工厂方法返回一个对象的引用,在创建实现接口的对象的时候工厂方法并不被执行。

1.5Lambda表达式的延迟执行原因

(以下来源于Java 核心技术 卷一)
 使用lambda表达式的重点是延迟执行(deffered execution)。毕竟,如果想要立即执行代码,完全可以直接执行,而无需将它包装到一个lambda表达式中。之所以希望以后再执行代码,这有很多原因,比如:

  • 在一个单独的线程中运行代码
  • 多次运行代码、在算法的适当位置运行代码(例如,排序的比较操作)
  • 发生某种情况时执行代码(如,点击了一个按钮,数据到达,等等)】
  • 只在必要时才运行代码

1.6接口实现的不同方式

 实现接口的对象创建方式有三种,如CodeBlock-1代码所示,分为:

  1. 使用接口实现类(接口对象指向已实现了接口的类对象
  2. 使用匿名内部类实现接口
  3. 使用lambda表达式来实现接口
    CodeBlock-1:三种不同方法来实现接口
public class Program {
    public static void main(String[] args) {
        /***
         * 1.使用接口实现类
         */

        Comparator comparator = new MyComparator();

        /**
         * 2.使用匿名内部类实现接口
         */

        Comparator comparator1 = new Comparator() {
            @Override
            public int compare(int a, int b) {
                return a - b;
            }
        };

        /**
         * 3.使用lambda表达式来实现接口
         *
         */
        Comparator comparator2 = (a, b) -> a - b;


        /**
         * 测试部分 若不出错,则显示三个-1
         */

        System.out.println(comparator.compare(1, 2));
        System.out.println(comparator1.compare(1, 2));
        System.out.println(comparator2.compare(1, 2));
    }
}


class MyComparator implements Comparator {
    @Override
    public int compare(int a, int b) {
        return a - b;
    }
}

@FunctionalInterface
interface Comparator {
    int compare(int a, int b);
}

二、Lambda表达式的基础语法

2.1基本语法注意事项

  1. Lambda表达式是一个匿名函数
  2. 关注重点:参数列表 方法体
  3. 小括号():=用来描述一个参数列表(形参)
  4. 大括号{} 来描述一个方法体
  5. ->:即Lambda运算符,读作goes to ,用于分割参数列表和方法体

2.2代码实例

 我们用Lambda表达式的多种形式,分为有无返回值的普通方法(构造方法后面再讲),无参、一参、多参方法,总共有6个方法。所以定义了多个拥有不同方法的接口。从接口的命名方式就可以知道其意味着的含义(如果不在同一包种,注意import)。
CodeBlock-2:6种不同的接口定义:

//1.无返回值的多参接口
@FunctionalInterface
public interface LambdaNoneReturnMultipleParameter {
    void test(int a, int b);
}
//2.无返回值的无参接口
@FunctionalInterface
public interface LambdaNoneReturnNoneParameter {
    void test();
}
//3.无返回值的一参接口
@FunctionalInterface
public interface LambdaNoneReturnSingleParameter {
    void test(int n );
}

//4.有返回值的多参接口
@FunctionalInterface
public interface LambdaSingleReturnMultipleParameter {
    int test(int a, int b);
}
//5.有返回值的无参接口
@FunctionalInterface
public interface LambdaSingleReturnNoneParameter {
    int test();
}
//6.有返回值的一参接口
@FunctionalInterface
public interface LambdaSingleReturnSingleParameter {
    int test(int n);
}

CodeBlock-3:6种不同的接口的Lambda表达式应用:

/**
 * Lambda表达式的基础语法
 */
public class Syntax1 {
    public static void main(String[] args) {
        /**
         * 1.无参无返回的Lambda表达式使用样例
         */
        LambdaNoneReturnNoneParameter lambda1 = () -> {
            System.out.println("lambda1:" + "Hello World!");
        };
        lambda1.test();

        /**
         * 2.无返回值的单参数的Lambda表达式使用样例
         */

        LambdaNoneReturnSingleParameter lambda2 = (int i) -> {
            System.out.println("lambda2:" + i);
        };
        lambda2.test(1024);


        /**
         * 3.无返回值的多参数的Lambda表达式使用样例
         */
        LambdaNoneReturnMultipleParameter lambda3 = (int a, int b) ->
        {
            System.out.println("lambda3:" + (a + b));
        };
        lambda3.test(1000, 24);

        /**
         * 4.有返回值的无参数的Lambda表达式使用样例
         */

        LambdaSingleReturnNoneParameter lambda4 = () -> {
            return 1024;
        };
        int res = lambda4.test();
        System.out.println("lambda4:" + res);

        /**
         * 5.有返回值,单个参数的Lambdad的表达式使用
         */

        LambdaSingleReturnSingleParameter lambda5 = (int a) -> {
            return a;
        };
        int res2 = lambda5.test(1024);
        System.out.println("lambda5:" + res2);

        /**
         * 6.有返回值,多个参数的Lambdad的表达式使用
         */
        LambdaSingleReturnMultipleParameter lambda6 = (int a, int b) -> {
            int sum = a + b;
            return sum;
        };
        int res3 = lambda6.test(1000, 24);
        System.out.println("lambda6:" + res3);


    }


}

三、Lambda表达式语法精简

 从Lambda表达式的基础语法样例中我们几乎没有看Lambda语法的优势,特别是和匿名内部类对比,更是没发现Lambda带来的代码的优雅和简化。但是,Lambda语法提供了合理的代码化简方式,我们在第三章中进行Lambda表达式的简化的学习。

3.1Lambda表达式精简的方式:

  1. 参数类型的精简:
    由于在接口中已经定义了参数,所以在Lambda表达式中参数的类型可以省略;
    备注:如果需要进行省略类型,那么所有参数的类型都必须都得省略,省略部分会报错;
    匿名内部类中省略参数类型是不可取的,这是Lambda表达式的优势;
  2. 小括号的精简:
    如果参数列表中,参数的个数有且只有一个(多了少了都不行),那么小括号可以省略,且仍然可以省略参数的类型
  3. 方法大括号的精简:
    类似于if,while语句,如果语句块只有一条语句,那么此时大括号可以省略
  4. return的省略:
    如果出现接口只有唯一方法且方法中只有唯一语句,且是返回语句,那么如果要省略,只能一起省略掉大括号以及return,不能省略其中之一,否则会报错。

3.2 代码实例

 此处所使用到的接口仍然是CodeBlock-2代码块所定义的。

CodeBlock-4:四种接口精简的方式代码案例:

/**
 * 此类用于语法精简的Lambda表达式演示
 */
public class Syntax2 {
    /**
     * 参数精简
     * 1.参数的精简
     * 由于在接口中已经定义了参数,所以在Lambda表达式中参数的类型可以省略
     * 备注:如果需要进行省略类型,但是所有参数的类型必须都得省略,省略部分会报错
     * 匿名内部类中省略参数类型是不可取的
     */

    LambdaNoneReturnMultipleParameter lambda1 = (a, b) -> {
        System.out.println(a + b);

    };
    /**
     * 2.精简参数小括号
     * 如果参数列表中,参数的个数有且只有一个(多了少了都不行),那么小括号可以省略
     * 且仍然可以省略参数的类型
     */
    LambdaNoneReturnSingleParameter lambda2 = a -> {
        System.out.println(a);
    };

    /**
     * 3.方法大括号的省略
     * 类似于if,while语句,如果语句块只有一条语句,那么此时大括号可以省略、
     * 前面的省略方式仍然成立
     */
    LambdaNoneReturnSingleParameter lambda3 = a ->
            System.out.println(a);
    /**
     * 4.如果接口的唯一方法只有唯一返回语句,那么可以省略大括号,但是在省略大号的同时必须省略return
     */
    LambdaSingleReturnNoneParameter lambda4 = () -> 10;

}


四、 Lambda表达式进阶之函数引用

方法引用的提出: 由于如果存在一种情况,我们新建了多个接口的实现对象,其方法都是相同的,但是如果方法需要修改,那么修改的复杂度就随着对象数量的上升而上升。
方法引用的定义: 快速将一个Lambda表达式的实现指向一个已经写好的方法
方法引用可以看作是lambda表达式的特殊形式,或者称之为语法糖。一般方法已经存在才可以使用方法引用,而方法若未存在,则只能使用lambda表达式。

 我们可以采用两种方式来在Lambda表达式中调用其他方法,第一种如一般的方法调用,第二种就是方法引用。
方法引用的语法说明:
 即:“方法的隶属者::方法名”。方法的隶属者,即静态方法隶属者为类,非静态方法的隶属者是对象(隶属者不是接口,而是定义引用方法的类或者对象)。
 注意事项:
 1.被引用的方法的参数数量以及类型一定要和接口中的方法参数数目一致;
 2.被引用的方法的返回值一定要和接口中的方法返回值一致,方法引用这个整体表达式可以返回函数式接口的实现对象,但其调用/引用的方法其返回类型绝不是接口实例对象;
 3.方法名的后面没有括号“()”;
 4.方法的引用是可以有多个参数入口的,虽然再::表达式中没有体现(由于没有小括号),但是接口中对其已有所规定了;

4.1 普通方法在Lambda表达式中的调用

CodeBlock-5:2种不同的普通方法调用的样例说明:

public class Syntax3 {
    public static void main(String[] args) {
/**
 *方法引用:可以快速将一个Lambda表达式的实现指向一个已经写好的方法
 *语法:方法的隶属者,静态方法隶属者为类,非静态方法的隶属者是对象
 * 即:“方法的隶属者:方法名”
 * 注意事项:
 * 1.被引用的方法的参数数量以及类型一定要和接口中的方法参数数目一致
 * 2.被引用的方法的返回值一定要和接口中的方法返回值一致
 *
 *
 * 假如我们在程序中对于某个接口方法需要调用许多次,那么用以下的方法创建对象,来调用方法就是不太好的
 * 缺点:如果将来要对方法进行改变,那么所有用Lambda表达式定义的对象都要更改,这在设计模式上就是有问题的;
 *      */
        LambdaSingleReturnSingleParameter lambda1 = a -> a * 2;
        LambdaSingleReturnSingleParameter lambda2 = a -> a * 2;

        /**
         * 我们一般是写一个通用的方法,并将其引用至Lambda表达式中
         *
         * */

        LambdaSingleReturnSingleParameter lambda3 = a -> change(a);//在Lambda表达式中使用一般方法的调用方式
        LambdaSingleReturnSingleParameter lambda4 = Syntax3::change;//在Lambda表达式种使用方法引用(方法隶属于类)
		System.out.println(lambda4.test(2));
		Syntax3 syntax3 = new Syntax3();//非静态方法需要对象才能被调用
        LambdaSingleReturnSingleParameter lambda5 = syntax3::change2;//在Lambda表达式种使用方法引用(方法隶属于对象)
		LambdaSingleReturnMultipleParameter lambda6 = syntax3::change3;//多参数的引用方法使用
    }

    private static int change(int a) {
        return a * 2;
    }

    private int change2(int a) {
        return a * 2;
    }
}
    private int change3(int a, int b) {
        return a * 2 + b * 3;
    }

4.2 构造方法在Lambda表达式中的调用

 Person类具有无参和有参构造方法。
CodeBlock-6:定义一个类,构造方法创建的对象

public class Person {
    public String name;
    public int age;

    public Person() {
        System.out.println("Person类的无参构造方法执行了");//语句用于判断无参构造器是否执行了
    }

    public Person(String name, int age) {
        this.name = name;
        this.age = age;
        System.out.println("方法的有参构造方法执行了");//语句用于判断有参构造器是否执行了
    }
}

CodeBlock-7:Lambda表达式中引用构造方法的样例:

public class Syntax4 {

    public static void main(String[] args) {

        PersonCreater person = () -> new Person();

        /**构造方法的引用
         * 有参和无参构造器的调用区别在于所定义的接口中构造方法的参数区别
         */

        PersonCreater creater1 = Person::new;
        //无参
        Person person1 = creater1.getPerson();
        //有参
        PersonCreater2 creater2=Person::new;
        Person person2 = creater2.getPerson("Fisherman",18 );

    }

}

//需求为:一个返回一个Person类的接口

interface PersonCreater {
    Person getPerson();

}

interface PersonCreater2 {
    Person getPerson(String name, int age);

}

数组的构造方法引用:
语法格式:TypeName[]::new等价于lambda表达式:x -> new int[x]

IntFunction<int[]> arrayMaker = int[]::new;//假设有一个返回int类型数组的函数式接口
int[] array = arrayMaker.apply(10) // 创建数组 int[10]

注意事项:

  1. 在Lambda表达式中,一个接口要么对应一个无参构造方法,要么含有一个有参构造方式,其在接口中所定义的抽象返回Person对象的方法已经决定有参还是无参了。
  2. 构造方法和静态方法一样都是隶属于类的方法
  3. 构造方法不同于一般静态方法“类名::方法名一样调用”,而是采用"类名::new"的方式来进行构造方法的调用。
    使用new关键字是为了明确地知道调用的是构造函数。而其并不需要入口参数的原因是因为编译器完全可以通过接口的定义推断出参数类型和个数。构造方法的方法引用和普通方法引用并没有本质的区别,比如在CodeBlock-5中用change(a)来实现原接口中定义的返回整形数据的test方法,而new关键字使用对应形参的构造器来实现接口中定义的返回Person对象的getPerson方法。
  4. ::在IDE(比如Intllij IDEA)中总是指向当前方法引用实现的函数式接口,以此可以方便地确定方法引用所实现的函数式接口为哪个。

4.3 方法引用的格式总结:

引用方法的类型 格式规定 方法的发出者 等价lambda表达式
引用静态方法 ClassName::staticMethodName (s) -> String.valueOf(s)
引用某个对象的实例(非静态)方法 ObjectName::instanceMethodName 当前对象 ObjectName.instanceMethodName
引用某个类型的任意对象的实例方法 ClassName::methodName 任意此类或子类的对象 (任意对象,s) -> methodName(任意对象,s)
引用构造方法 ClassName::new (s) -> new ClassName(s);

上述s代表形参,限于篇幅,只象征性地写了一个。其数目可以不为1,为0,2,3…都可以。

4.4 方法引用和Lambda表达式的对比:

 方法引用比Lambda表达式更加简洁,但同时也更难理解其语法,所以我们以下用做对比的方法来理解表达式。

4.4.1 静态方法引用

组成语法格式:ClassName::staticMethodName

  • 静态方法引用比较容易理解,和静态方法调用的lambda表达式相比,只是把 .换为 ::
  • 在目标类型兼容的任何地方,都可以使用静态方法引用。此时,类是静态方法动作的发起者。
  • 例子:
      String::valueOf等价于lambda表达式 (s) -> String.valueOf(s)
      Math::pow等价于lambda表达式 (x, y) -> Math.pow(x, y);

4.4.2 特定实例对象的方法引用

实例方法引用又分以下三种类型:

  1. 实例上的实例方法引用
    这种语法与用于静态方法的语法类似,只不过这里使用对象引用而不是类名。此时,对象是方法动作的发起者。
    语法格式:instanceReference::methodName
    例子:
    instanceReference::methodName相当于(无参或有参)->instanceReference.methodName(数目相同的参数)
    由于对象需要构造,故在下面给出代码示例。
public class Test {
   public static void main(String[] args) {
   
       Power powerObject = new Power();
       Function<Integer,Integer> function1 = a->powerObject.power(a);
       Function<Integer,Integer> function2 = powerObject::power;
   	/**
   	*不管哪种实现,方法的调用是相同的,都用接口的已实现抽象方法名调用。
   	*/
       System.out.println(function1.apply(2));
       System.out.println(function2.apply(3));
   }

}

class Power {
   public int power (int a ){
       return a*a;
   }
}
  1. 超类上的实例方法引用
    语法格式:super::methodName
    方法的名称由methodName指定,通过使用super,可以引用方法的超类版本。
    例子:
    还可以捕获this 指针,this :: equals 等价于lambda表达式 x -> this.equals(x);

  2. 类上的实例方法引用(特定类的任意对象的方法引用)
    语法格式:ClassName::methodName
    这里和类调用静态方法以及对象调用实例都不相同, ::前的类不在是实例方法的发出者,那发出者是谁呢?我们凭借此方法引用的格式也找不到究竟谁是动作的发起者?实际上真正的发起者是ClassName类锁创建的任意一个对象,只不过,在方法调用的时候需要将引用对象作为参数输入到方法中,并且规定,此对象一定要位于方法参数的第一个。
    代码案例:

注意:
 若类型的实例方法是泛型的,就需要在::分隔符前提供类型参数,或者(多数情况下)利用目标类型推导出其类型,并不需要加类型参数,但是遇到分隔符前使用了强制类型转换应当看得懂其用意。
例子:
String::toString等价于lambda表达式 (s) -> s.toString()而绝不等于String.toString(),因为非静态方法只允许对象调用,而不能是类。但是即使这样讲,你可能还是不明白,那我给以下浅显易懂的例子,你一定都能够掌握它。

 我们定义一个计算平方的方法,输入参数为”几“次方,而对于Power接口的不同实现,实现了求幂的底数规定,比如:PowerOfTwo类实现power(i)方法是求2的i次幂,PowerOfThree类实现power(i)方法是求3的i次幂。
类::实例方法的CodeBlock-1:

import java.net.InterfaceAddress;
import java.util.function.BiFunction;
import java.util.function.Function;

public interface Power {
    int power(int i);
}

class PowerOfTwo implements Power {
    public int power(int i) {
        return (int) Math.pow(2, i);

    }
}

class PowerOfThree implements Power {
    public int power(int i) {
        return (int) Math.pow(3, i);

    }
}


class Test {
    public static void main(String[] args) {
        /**
         * 之所以使用BiFunction作为函数式接口,是因为其为2输入参数,1个返回值。
         * 正好符合此例中,类名.实例方法的调用规则
         */
        Power powerObject1 = new PowerOfTwo();
        Power powerObject2 = new PowerOfThree();

        BiFunction<Power, Integer, Integer> function = Power::power;
        
        System.out.println(function.apply(powerObject1, 4));//输出"2"的4次方:16
        System.out.println(function.apply(powerObject2, 4));//输出"3"的4次方:81


    }

}

 只需要一个BiFunction接口的实现类,就能实现方法调用的多态(同一的方法名,由于对象名的不同而有不一样的操作)。这里多态的形成也是由于父类接口指向不同子类对象实现形成的。
 我们应当将BiFunction function = Power::power;以及function.apply(powerObject1, 4)这两个步骤结合起来看,而不是孤立的。前者规定了方法输入参数类型,返回值类型,且规定了方法第一个参数必须得是实际调用该方法的对象,实现了后者的apply方法(代码实现,并未执行)。后者则输入了对应参数,执行相关程序。
 如果你问我为什么需要这样的方法引用形式,那么最大的原因就其在具体方法调用时将对象作为参数输入到方法调用中呢,这增加了多态在方法引用中的便利性,如上述例子所示。如果采用其他方法引用方式,将产生多个接口的实例,而此方式只需要一个接口的实例。

 类上实例方法的引用的第二个例子:
类::实例方法的CodeBlock-2:

public class LowercaseToUppercase {

    public static void main(String[] args) {

        List<String> list = Arrays.asList("hello", "world", "hello world");
        
        list.stream().map(String::toUpperCase).forEach(System.out::println);
    }
}

 如果你还没学过流,不影响这里的理解。我们Crtl+左键+::,可以看到map类所需实现的是一个Function接口,即一个输入,一个输出。但是String::toUpperCase作为一个方法引用,返回值是确定的,因为toUpperCase方法显然会返回一个String类型对象:大写的String对象;但是输入参数是哪个呢,这似乎难以判断了,因为toUpperCase方法是一个无参的函数。但是JVM模型告诉我们:所有实例的实例方法,第一个参数都是一个隐式的this。map方法,对流的元素进行了映射(小写至大写),而方法中隐式的参数this即这每一个对象。
 分步骤结合JDK源码解释:

  1. map(String::toUpperCase),向map方法内传入一个方法引用所形成的接口实现对象
  2. Stream map(Function mapper);,map是一个返回Stream对象,输入为实现Function接口的对象即1中的方法引用
  3. R apply(T t);此是Function接口所要实现的方法,即使用toUpperCase()方法去实现它。
  4. public String toUpperCase() { return toUpperCase(Locale.getDefault());},这是toUpperCase()方法的源码,其是一个无参返回String类对象的非静态方法。
  5. 接口所需实现的方法的形参T t则对应任意一个String对象。

 如果你有关于“为何例子1中的apply方法调用中方法的第一个参数为执行操作对象的引用,而第二个例子中却从未输入过对象的引用”的疑惑,那么说明你理解到关键位置了。实际上后者是map调用Function接口对象返回给stream,最后被流中的迭代方法调用了apply方法,其最终也是将对象引用作为第一个输入参数调用了apply方法,所以两个例子通过同一种方式达成了类名::实例方法,只不过一个显式,一个隐式。

4.4.3 构造方法引用

 参看4.2小节内容。


5. 总结

总结: Lambda表达式和方法引用的目的都是使用具体的方法来代替接口中抽象的方法,但是在实际使用中,调用的是接口中被实现的方法名,lambda表达式和方法引用只应用于接口实例的方式实现了的构造过程,例如在CodeBlock-6/7中的代码:

//1.这是需要被实现的抽象方法,方法名:getPerson
interface PersonCreater {
    Person getPerson();

}

//2.这是使用方法引用实现了抽象方法的对象(返回的是一个被实现了抽象方法的接口的实例)
    PersonCreater creater1 = Person::new;

//3.这是调用接口实例的实现方法,返回一个Person对象,分为有参和无参。
	Person person1 = creater1.getPerson();

6.补充:常用函数式接口Java-Lambda表达式和“方法引用”的对比和详解_第1张图片

图源:Java核心技术卷1-第十版

你可能感兴趣的:(JAVA-语法)