函数式接口

函数式接口(Functional Interface)就是一个只有一个抽象方法(可以包含多个默认方法或多个static方法)的普通接口,可以被隐式转换为lambda表达式,可以现有的函数友好地支持 lambda。

函数式接口:

  • java.lang.Runnable

  • java.util.concurrent.Callable

  • java.security.PrivilegedAction

  • java.util.Comparator

  • java.io.FileFilter

  • java.nio.file.PathMatcher

  • java.lang.reflect.InvocationHandler

  • java.beans.PropertyChangeListener

  • java.awt.event.ActionListener

  • javax.swing.event.ChangeListener

  • java.util.function

Lambda表达式入门

匿名内部类:

public class CommandTest2 
{
    public static void main(String[] args) 
    {
        ProcessArray pa = new ProcessArray();
        int[] target = {3, -4, 6, 4};
        //处理数组,具体处理行为取决于匿名内部类
        pa.process(target, new Command() 
        {
            public void process(int[] target) 
            {
                int sum = 0;
                for(int tmp :target)
                {
                    sum += tmp;
                }
                System.out.println("数组元素的总和是:"+ sum);
            }
        });
    }
}

Lambda表达式:

public class CommandTest3 
{
    public static void main(String[] args) 
    {
        ProcessArray pa = new ProcessArray();
        int[] array = {3, -4, 6, 4};
        //处理数组,具体处理行为取决于匿名内部类
        pa.process(array, (int[] target)->{
            int sum = 0;
            for(int tmp : target)
            {
                sum += tmp;
            }
            System.out.println("数组元素的总和是:"+ sum);
        });
    }
}

当使用Lambda表达式代替匿名内部类创建对象时,Lambda表达式的代码块将会代替实现抽象方法的方法体,Lambda表达式就相当一个匿名方法。

Lambda表达式的主要作用就是代替匿名内部类的烦琐语法。它由三部分组成。

  • 形参列表。形参列表允许省略形参类型。如果形参列表只有一个参数,甚至连形参列表的圆括号也可以省略。

  • 箭头(->)。必须通过英文中画线号和大于符号组成。

  • 代码块。如果代码块只包含一条语句,Lambda表达式允许省略代码块的花括号,那么这条语句就不用用花括号表达语句结束。Lambda代码块只有一条return语句,甚至可以省略return关键字。Lambda表达式需要返回值,而它的代码块中仅有一条省略了return的语句,Lambda表达式会自动返回这条语句的值。

interface Eatable
{
    void test();
}

interface Flyable
{
    void fly(String weather);
}
interface Addable
{
    int add(int a, int b);
}
public class LambdaQs 
{
    //调用该方法需要Eatable对象
    public void eat(Eatable e) 
    {
        System.out.println(e);
        e.test();
    }
    //调用该方法需要Flyable对象
    public void drive(Flyable f) 
    {
        System.out.println("老司机正在开:"+f);
        f.fly("亮瞎眼");
    }
    //调用该方法需要Addable对象
    public void test(Addable add) 
    {
        System.out.println("34与59的和为:"+add.add(34, 59));
    }
    public static void main(String[] args) 
    {
        LambdaQs lq = new LambdaQs();
        //Lamba表达式的代码块只有一条语句,可以省略花括号
        lq.eat(()->System.out.println("苹果很赞哦!"));
        //Lamba表达式的形参列表只有一个形参,可以省略圆括号
        lq.drive(weather ->
        {
            System.out.println("今天天气是:"+weather);
            System.out.println("直升机飞行平稳");
        });
        //Lambda表达式的代码块只有一条语句,可以省略花括号
        //代码块中只有一条语句,即使该表达式需要返回值,也可以省略return关键字
        lq.test((a, b) -> a + b);
    }
}

Lambda表达式与函数式接口

Lambda表达式的类型,也被称为“目标类型”,Lambda表达式的目标类型必须是“函数式接口”。函数式接口代表只包含一个抽象方法的接口。函数式接口可以包含多个默认方法、类方法,但只能声明一个抽象方法

如果采用匿名内部类语法来创建函数式接口的实例,则只需要实现一个抽象方法,在这种情况下即可采用Lambda表达式来创建对象,该表达式创建处理的对象的目标类型就是这个函数式接口。查询Java8的API文档,可以发现大量的函数式接口,例如:Runnable、ActionListener等接口都是函数式接口

@FunctionalInterface注解,该注解通常放在接口定义前面,该注解对程序功能没有任何作用,它用于告诉编译器执行更严格检查——检查该接口必须是函数式接口,否则编译器就会报错

Lambda表达式实现的是匿名方法——因此它只能实现特定函数式接口中的唯一方法。这意味着Lambda表达式有如下两个限制:

  • Lambda表达式的目标类型必须是明确的函数式接口

  • Lambda表达式只能为函数式接口创建对象。Lambda表达式只能实现一个方法,因此它只能为只有一个抽象方法的接口(函数式接口)创建对象

为了保证Lambda表达式的目标类型是一个明确的函数式接口,可以有如下三种常见方式:

  • 将Lambda表达式赋值给函数式接口类型的变量

  • 将Lambda表达式作为函数式接口类型的参数传给某个方法

  • 使用函数式接口对Lambda表达式进行强制类型转换

同样的Lambda表达式的目标类型完全可能是变化的——唯一的要求是,Lambda表达式实现的匿名方法与目标类型(函数式接口)中唯一的抽象方法有相同的形参列表。

Java8在java.util.function包下预定义了大量函数式接口,典型地包含如下4类接口。

  • XxxFunction:这类接口中通常包含一个apply()抽象方法,该方法对参数进行处理、转换(apply()方法的处理逻辑由Lambda表达式来实现),然后返回一个新的值。该函数式接口通常用于对指定数据进行转换处理。

  • XxxConsumer:这类接口中通常包含一个accept()抽象方法,该方法与XxxFunction接口中的apply()方法基本相似,也负责对参数进行处理,只是该方法不会返回处理结果。

  • XxxxPredicate:这类接口中通常包含一个test()抽象方法,该方法通常用来对参数进行某种判断(test()方法的判断逻辑由Lambda表达式来实现),然后返回一个boolean值。该接口通常用于判断参数是否满足特定条件,经常用于进行筛滤数据。

  • XxxSupplier:这类接口中通常包含一个getAsXxx()抽象方法,该方法不需要输入参数,该方法会按某种逻辑算法(getAsXxx()方法的逻辑算法由Lambda表达式来实现)返回一个数据。

方法引用与构造器引用

如果Lambda表达式的代码块只有一条代码,还可以在代码块中使用方法引用和构造器引用。

方法引用和构造器引用可以让Lambda表达式的代码块更加简洁。方法引用和构造器引用都需要使用两个英文冒号。

种类 示例 说明 对应的Lambda表达式
引用类方法 类名::类方法 函数式接口中被实现方法的全部参数传给该类方法作为参数 (a,b,...)->类名.类方法(a,b,...)
引用特定对象的实例方法 特定对象::实例方法 函数式接口中被实现方法的全部参数传给该类方法作为参数 (a,b,...)->特定对象.实例方法(a,b,...)
引用某类对象的实例方法 类名::实例方法 函数式接口中被实现方法的第一个参数作为调用者,后面的参数全部传给该方法作为参数 (a,b,...)->a.实例方法(b,...)
引用构造器 类名::new 函数式接口中被实现方法的全部参数传给该类方法作为参数 (a,b,...)->new 类名(a,b,...)

引用类方法

@FunctionalInterface
interface Converter{
    Integer convert(String from);
}

该函数式接口包含一个convert()抽象方法,该方法负责将String参数转换成Integer。下面代码使用Lambda表达式来创建Converter对象。

//下面代码使用Lambda表达式创建Converter对象
Converter converter1 = from -> Integer.valueOf(from);

上面Lambda表达式的代码块只有一条语句,因此程序省略了该代码块的花括号;而且由于表达式所实现的convert()方法需要返回值,因此Lambda表达式将会把这条代码的值作为返回值
调用convert1对象的convert()方法将字符串转换为整数了,例如如下代码

Integer val = converter1.convert("99");
System.out.println(val); // 输出整数99

上面Lambda表达式的代码块只有一行调用类方法的代码,因此可以使用如下方法引用进行替换

// 方法引用代替Lambda表达式:引用类方法。
// 函数式接口中被实现方法的全部参数传给该类方法作为参数。
Converter converter1 = Integer::valueOf;

对于上面的类方法引用,也就是调用Integer类的valueOf()类方法来实现Converter函数式接口中唯一的抽象方法,当调用Converter接口中唯一的抽象方法时,调用参数将会传给Integer类的valueOf()类方法。

引用特定对象的实例方法

// 下面代码使用Lambda表达式创建Converter对象
Converter converter2 = from -> "fkit.org".indexOf(from);

Integer value = converter2.convert("it");
System.out.println(value); // 输出2
// 方法引用代替Lambda表达式:引用特定对象的实例方法。
// 函数式接口中被实现方法的全部参数传给该方法作为参数。
Converter converter2 = "fkit.org"::indexOf;

对于上面的实例方法引用,也就是调用"fkit.org"对象的indexOf()实例方法来实现Converter函数式接口中唯一的抽象方法,当调用Converter接口中唯一的抽象方法时,调用参数将会传给"fkit.org"对象的indexOf()实例方法。

引用某类对象的实例方法

定义如下函数式接口

@FunctionalInterface
interface MyTest
{
    String test(String a , int b , int c);
}

使用Lambda表达式来创建一个MyTest对象

MyTest mt = (a , b , c) -> a.substring(b , c);
String str = mt.test("Java I Love you" , 2 , 9);
System.out.println(str); // 输出:va I Lo
// 方法引用代替Lambda表达式:引用某类对象的实例方法。
// 函数式接口中被实现方法的第一个参数作为调用者,
// 后面的参数全部传给该方法作为参数。
MyTest mt = String::substring;

引用构造器

@FunctionalInterface
interface YourTest
{
    JFrame win(String title);
}

下面代码使用Lambda表达式创建YourTest对象

YourTest yt = (String a) -> new JFrame(a);
JFrame jf = yt.win("我的窗口");
System.out.println(jf);
// 构造器引用代替Lambda表达式。
// 函数式接口中被实现方法的全部参数传给该构造器作为参数。
YourTest yt = JFrame::new;

对于上面的构造器引用,也就是调用某个JFrame类的构造器来实现YourTest函数式接口中唯一的抽象方法,当调用YourTest接口中的唯一的抽象方法时,调用参数将会传给JFrame构造器。调用YourTest对象的win()抽象方法时,实际只传入了一个String类型的参数,这个String类型的参数会被传给JFrame构造器——这就确定了调用JFrame类的、带一个String参数的构造器。

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

Lambda表达式是匿名内部类的一种简化,存在如下相同点

  • Lambda表达式与匿名内部类一样,都可以直接访问“effectively final”的局部变量,以及外部类的成员变量(包括实例变量和类变量)。

  • Lambda表达式创建的对象与匿名内部类生成的对象一样,都可以直接调用从接口中继承的默认方法。

@FunctionalInterface
interface Displayable
{
    //定义一个抽象方法和默认方法
    void display();
    default int add(int a, int b)
    {
        return a + b;
    }
}
public class LambdaAndInner 
{
    private int age = 24;
    private static String name = "简单点,说话的方式简单点" ;
    public void test() 
    {
        String sing = "演员";
        Displayable dis = () -> {
            //访问“effectively final”的局部变量
            System.out.println("sing局部变量为:"+ sing);
            //访问外部类的实例变量和类变量
            System.out.println("外部类的age实例变量为:"+ age);
            System.out.println("外部类的name类变量为:"+ name);
        };
        dis.display();
        //调用dis对象从接口中继承的add()方法
        System.out.println(dis.add(34, 59));
    }
    public static void main(String[] args) 
    {
        LambdaAndInner lambdaAndInner = new LambdaAndInner();
        lambdaAndInner.test();
    }
}

上面Lambda表达式创建了一个Display的对象,Lambda表达式的代码块中的三行粗体字代码分别示范了访问“effectively fianl”的局部变量、外部类的实例变量和类变量。从这点来看,Lambda表达式的代码块与匿名内部类的方法体是相同的。
与匿名内部类相似的是,由于Lambda表达式访问了sing局部变量,该局部变量相当于与一个隐式的final修饰,因此不允许对sing局部变量重新赋值。

Lambda表达式与匿名内部类主要存在如下区别

  • 匿名内部类可以为任意接口创建实例——不管接口包含多少个抽象方法,只有匿名内部类实现所有的抽象方法即可;但Lambda表达式只能为函数式接口创建实例。

  • 匿名内部类可以为抽象类甚至普通类创建实例;但Lambda表达式只能为函数式接口创建实例。

  • 匿名内部类实现的抽象方法的方法体允许调用接口中定义的默认方法;但Lambda表达式的代码块不允许调用默认方法。

使用Lambda表达式调用Arrays的类方法

Arrays类的有些方法需要Comparator、XxxOperator、XxxFunction等接口的实例,这些接口都是函数式接口,因此可以使用Lambda表达式来调用Arrays的方法

import java.util.Arrays;

import javax.management.openmbean.OpenDataException;

public class LambdaArrays 
{
    public static void main(String[] args) 
    {
        String arr1[] = new String[]{"皇家马德里", "巴塞罗那", "巴黎圣日耳曼","尤文图斯","切尔西"};
        Arrays.parallelSort(arr1, (o1, o2) -> o1.length() - o2.length());
        System.out.println(Arrays.toString(arr1));
        int[] arr2 = new int[]{4, 2, 1, 3, 5};
        //left代表数组中前一个索引处的元素,计算第一个元素时,left为1
        //right代表数组中当前索引处的元素
        Arrays.parallelPrefix(arr2, (left, right) -> left * right);
        System.out.println(Arrays.toString(arr2));
        long[] arr3 = new long[5];
        //operand代表正在计算的元素索引
        Arrays.parallelSetAll(arr3, operand -> operand * 5);
        System.out.println(Arrays.toString(arr3));
    }
}
  • (o1, o2) -> o1.length() - o2.length():目标类型是Comparator指定了判断字符串大小的标准:字符串越长,即可认为该字符串越大

  • (left, right) -> left * right:目标类型是IntBinaryOperator,该对象将会根据前后两个元素来计算当前元素的值

  • operand -> operand * 5::目标类型是IntToLongFunction,该对象将会根据元素的索引来计算当前元素的值

Lambda表达式可以让程序更加简洁。


布still
461 声望32 粉丝

数据挖掘、用户行为研究、用户画像