天天看点

JAVA Lambda 表达式

一、 Lambda简介

简单的来说,java8引入Lambda的目的是为了简化代码,允许把函数作为一个方法的参数传递进方法中。如果有JavaScript的编程经验,马上会想到这不就是闭包吗。是的,Lambda表达式也可以称作Java中的闭包。

Java8以前,如果想把某个接口的实现类作为参数传递给一个方法会怎么做?要么创建一个类实现该接口,然后new出一个对象,在调用方法时传递进去,要么使用匿名类,可以精简一些代码。以创建一个线程并打印一行日志为例,使用匿名函数写法如下:

new Thread(new Runnable() { @Override public void run() { System.out.println("开启一个线程执行任务"); }
}).start();      

再来看看使用Lambda表达式,上面的代码会变成什么样子。

new Thread(() -> System.out.println("开启一个线程执行任务")).start();      

Java 中的 Lambda 表达式通常使用语法是

(argument) -> (body)

,比如:

(arg1, arg2...) -> { body }
(type1 arg1, type2 arg2...) -> { body }      

二、lambda表达式的基本使用

1、基本语法

在上面的例子中我们使用了这样一行() -> System.out.println("使用Lambda表达式");下面我们对lambda的格式进行一个介绍:

(1)左边括号:lambda的形参列表,就好比是我们定义一个接口,里面有一个抽象方法,这个抽象方法的形参列表。

(2)箭头:lambda的操作符,所以你看见这个箭头心中知道这是一个lambda表达式就可以了。

(3)右边lambda体:就好比是我们实现了接口中的抽象方法。

2、基本特点

  • Lambda 表达式可以具有零个,一个或多个参数。
  • 可以显式声明参数的类型,也可以由编译器自动从上下文推断参数的类型。例如

    (int a)

    与刚才相同

    (a)

  • 参数用小括号括起来,用逗号分隔。例如

    (a, b)

    (int a, int b)

    (String a, int b, float c)

  • 空括号用于表示一组空的参数。例如

    () -> 42

  • 当有且仅有一个参数时,如果不显式指明类型,则不必使用小括号。例如

    a -> return a*a

  • Lambda 表达式的正文可以包含零条,一条或多条语句。
  • 如果 Lambda 表达式的正文只有一条语句,则大括号可不用写,且表达式的返回值类型要与匿名函数的返回类型相同。
  • 如果 Lambda 表达式的正文有一条以上的语句必须包含在大括号(代码块)中,且表达式的返回值类型要与匿名函数的返回类型相同。

3、基本使用

3.1 无参无返回值

//这是一种最简单的情况
//此时如果方法体比较复杂好几行代码,那么这个{}是不能省略的
Runnable runnable1 = () -> {
            System.out.println("第一行代码");
            System.out.println("第二行代码");
};      

3.2 有参数无返回值

@Test
    public void ConsumerTest() {
        //第一种:没有使用lambda表达式
        Consumer<String> consumer = new Consumer<String>() {
            @Override
            public void accept(String s) {
                System.out.println(s);
            }
        };
        consumer.accept("hello,world");
        //第二种:使用lambda表达式
        Consumer<String> consumer1 = (String s)->
            //此时只有一行输出代码,因此可以省去外部的{}
            System.out.println(s);
        consumer.accept("hello,world");
    }      

3.3 有参数有返回值

@Test
    public void test() {
        //第一种:没有使用lambda表达式
        Comparator<Integer> comparator = new Comparator<Integer>() {
            @Override
            public int compare(Integer o1, Integer o2) {
                return o1.compareTo(o2);
            }
        };
        System.out.println(comparator.compare(1,2));
        System.out.println("======================");
        //第二种:使用lambda表达式
        Comparator<Integer> comparator2 = (o1,o2)-> o1.compareTo(o2);
        System.out.println(comparator2.compare(1,2));
    }      

三、FunctionalInterface介绍

在 Java 中,功能接口(Functional interface)指只有一个抽象方法的接口。比如我们的Runnable就是一个函数式接口,我们可以到源码中看看:

@FunctionalInterface
public interface Runnable {
    /**
     * When an object implementing interface <code>Runnable</code> is used
     * to create a thread, starting the thread causes the object's
     * <code>run</code> method to be called in that separately executing
     * thread.
     * <p>
     * The general contract of the method <code>run</code> is that it may
     * take any action whatsoever.
     *
     * @see     java.lang.Thread#run()
     */
    public abstract void run();
}      

1、函数式接口特点

(1)含有@FunctionalInterface注解

(2)只有一个抽象方法

2、函数式接口作用

函数式接口能够接受匿名内部类的实例化对象,换句话说,我们可以使用匿名内部类来实例化函数式接口的对象,而Lambda表达式能够代替内部类实现代码的进一步简化。并且java为我们提供了四个比较重要的函数式接口:

  1. 消费型接口:Consumer< T> void accept(T t)有参数,无返回值的抽象方法;
  2. 供给型接口:Supplier < T> T get() 无参有返回值的抽象方法;
  3. 断定型接口: Predicate< T> boolean test(T t):有参,但是返回值类型是固定的boolean
  4. 函数型接口: Function< T,R> R apply(T t)有参有返回值的抽象方法;

3、自定义函数式接口

@FunctionalInterface
public interface WorkerInterface {

    public void doSomeWork();
}      

功能接口只能有一个抽象方法。如果我们尝试在其中添加一个抽象方法,则会抛出编译时错误。

class InterfaceTest {

    public static void main(String[] args) {
        // 通过匿名内部类调用
        WorkerInterface work = new WorkerInterface() {
            @Override
            public void doWork() {
                System.out.println("通过匿名内部类调用");
            }
        };
        work.doWork();
        
        // 通过 Lambda 表达式调用
        // Lambda 表达式实际上是一个对象。
        // 我们可以将 Lambda 表达式赋值给一个变量,就可像其它对象一样调用。
        work = ()-> System.out.println("通过 Lambda 表达式调用");
        work.doWork();
    }
}      

四、方法引用

1、方法引用简介

方法引用是lambda表达式的一种特殊形式,如果正好有某个方法满足一个lambda表达式的形式,那就可以将这个lambda表达式用方法引用的方式表示,但是如果这个lambda表达式比较复杂就不能用方法引用进行替换。实际上方法引用是lambda表达式的一种语法糖。

2、方法引用分类

为了演示以下代码,我们先自定义一个User类,有两个属性,name(String),age(Integer)

public class User {
    private String name;
    private Integer age;

    public Student(){

    }

    public User(String name,Integer age){
        this.name = name;
        this.age = age;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public int getAge() {
        return age;
    }

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

    public static int compareUserByAge(User u1,User u2){
        return u1.getAge() - u2.getAge();
    }

    public static int compareUserByName(User u1,User u2){
        return u1.getName().compareToIgnoreCase(u2.getName());
    }
}      

2.1、类名::静态方法名

User类有两个属性name和age并提供了初始化name和age的构造方法,并且在最下方提供了两个静态方法分别按age和name进行比较先后顺序。

接下来的需求是,按着分数由小到大排列并输出,在使用方法引用前,我们先使用lambda表达式的方式进行处理

User u1 = new User("zhangsan",60);
User u2 = new User("lisi",70);
User u3 = new User("wangwu",80);
User u4 = new User("zhaoliu",90);
List<User> userList = Arrays.asList(u1,u2,u3,u4);
userList.sort((o1, o2) -> o1.getAge() - o2.getAge());
userList.forEach(u -> System.out.println(u.getAge()));      

使用类名::静态方法名 方法引用替换lambda表达式

userList.sort(User::compareUserByAge);
userList.forEach(u -> System.out.println(u.getAge()));      

2.2、对象::实例方法名

我们再自定义一个用于比较User元素的类

public class ComparatorUser {
    public int compareUserByAge(User u1,User u2){
        return u2.getAge() - u1.getAge();
    }
}      

ComparatorUser中定义了一个非静态的,实例方法compareUserByAge,同样该方法的定义满足Comparator接口的compare方法定义,所以这里可以直接使用 对象::实例方法名 的方式使用方法引用来替换lambda表达式

ComparatorUser comparatorUser = new ComparatorUser();
userList.sort(comparatorUser::compareUserByAge);
userList.forEach(u -> System.out.println(u.getAge()));      

2.3、类名::实例方法名

这种方法引用的方式较之前两种稍微有一些不好理解,因为无论是通过类名调用静态方法还是通过对象调用实例方法这都是符合Java的语法,使用起来也比较清晰明了。那我们带着这个疑问来了解一下这个比较特殊的方法引用。

现在再看一下Student类中静态方法的定义

public static int compareUserByAge(User u1,User u2){

    return u1.getAge() - u2.getAge();

}

虽然这个方法在语法上没有任何问题,可以作为一个工具正常使用,但是有没有觉得其在设计上是不合适的或者是错误的。这样的方法定义放在任何一个类中都可以正常使用,而不只是从属于User这个类,那如果要定义一个只能从属于User类的比较方法下面这个实例方法更合适一些

public int compareByAge(User u){

    return this.getAge() - u.getAge();

接收一个User对象和当前调用该方法的User对象的分数进行比较即可。现在我们就可以使用 类名::实例方法名 这种方式的方法引用替换lambda表达式了

userList.sort(User::compareByAge);

userList.forEach(u-> System.out.println(u.getAge()));

这里非常奇怪,sort方法接收的lambda表达式不应该是两个参数么,为什么这个实例方法只有一个参数也满足了lambda表达式的定义(想想这个方法是谁来调用的)。这就是 类名::实例方法名 这种方法引用的特殊之处:当使用 类名::实例方法名 方法引用时,一定是lambda表达式所接收的第一个参数来调用实例方法,如果lambda表达式接收多个参数,其余的参数作为方法的参数传递进去。

结合本例来看,最初的lambda表达式是这样的

userList.sort((u1, u2) -> u1.getAge() - u2.getAge());

那使用 类名::实例方法名 方法引用时,一定是u1来调用了compareByAge实例方法,并将u2作为参数传递进来进行比较。是不是就符合了compareByAge的方法定义。

2.4、类名::new

也称构造方法引用,和前两种类似只要符合lambda表达式的定义即可,回想下Supplier函数式接口的get方法,不接收参数有返回值,正好符合无参构造方法的定义

@FunctionalInterface
public interface Supplier<T> {

/**
 * Gets a result.
 *
 * @return a result
 */
T get();
}      
Supplier<User> supplier = User::new;      

上面就是使用了User类构造方法引用创建了supplier实例,以后通过supplier.get()就可以获取一个User类型的对象,前提是User类中存在无参构造方法。