Lambda表达式和Function 接口

一、什么是Lambda表达式?

Lambda表达式,也可称为闭包,是JDK8的新特性。Lambda 允许把函数作为一个方法的参数(函数作为参数传递进方法中),可以使代码变的更加简洁紧凑。Lambda表达式是一个可传递的代码块,可以在以后执行一次或多次。

名字起源是以前还没有计算机时,逻辑学家Alonzo Church想要形式化的表示能有效计算的数学函数,使用了希腊字母lambda(λ )来标记参数,从那以后,带参数变量的表达式就被称为lambda表达式。

lambda表达式本质是一个匿名函数,比如以下函数

public int add(int x, int y) {
    return x + y;
}

 

可以转换为:

(int x, int y) -> x + y;

总结:

  1. Lambda 表达式,也可称为闭包,它是推动 Java 8 发布的最重要新特性。
  2. Lambda 允许把函数作为一个方法的参数
  3. 使用 Lambda 表达式可以使代码变的更加简洁紧凑。
  4. Lambda 表达式本质是一个匿名函数,是对匿名函数的简写形式。

二、为什么要引入Lambda表达式?

Lambda表达式可以让我们写出更简洁、更灵活的代码,能够让我们编程更加高效。

作为一种更紧凑的代码风格,使Java的语言表达能力得到了提升。

比如当我们使用最基本的代码去创建一个子线程,代码如下:

package cn.imyjs.java;

/**
 * @author YJS
 * @ClassName TestLamdba
 * @Description TODO
 * @webSite www.imyjs.cn
 * @date 2023/3/11 17:32
 */
public class TestLambda {
    public static void main(String[] args) {
        Thread thread = new Thread(new MyRunnable());
        thread.start();
    }
}

class MyRunnable implements Runnable{
    @Override
    public void run() {
        System.out.println("Hello~");
    }
}

 

为了使这段代码变得更加简洁,可以使用匿名内部类重构一下,代码如下:

public class TestLambda {
    public static void main(String[] args) {
        new Thread(new Runnable() {
            // 把一个重写的run()方法传入了一个构造函数中。
            @Override
            public void run() {
                System.out.println("Hello~");
            }
        }).start();
    }
}

 

而上面的这段代码,不是最简单的,还可以进一步简化,也就是使用Lambda表达式来简化。

public class TestLambda {
    public static void main(String[] args) {
        new Thread(() -> {System.out.println("Hello~");}).start();
    }
}

 

根据Lambda表达式简化的规则,终极简化代码如下:

public class TestLambda {
    public static void main(String[] args) {
        new Thread(() -> System.out.println("Hello~")).start();
    }
}

 

通过上面一段基础代码的显示,相信你已经知道了为什么需要引入Lambda表达式了。

三、Lambda表达式的语法

JDK1.8之后引入的一种语法,他的写法是使用一个->符号,箭头将Lambda表达式分为左右两部分,左边写的是实现的这个接口中的抽象方法中的形参列表,右边就是对抽象方法的处理;

([Lambda参数列表,即形参列表]) -> {Lambda体,即方法体}

拷贝小括号,写死右箭头,落地大括号,大括号中写上业务逻辑

以下是lambda表达式的重要特征:

  • 可选类型声明:不需要声明参数类型,编译器可以统一识别参数值。
  • 可选的参数圆括号:一个参数无需定义圆括号,但多个参数需要定义圆括号。
  • 可选的大括号:如果主体包含了一个语句,就不需要使用大括号。
  • 可选的返回关键字:如果主体只有一个表达式返回值则编译器会自动返回值,大括号需要指定表达式返回了一个数值。

Lambda 表达式的简单例子:

// 1. 不需要参数,返回值为 5 
() -> 5  
  
// 2. 接收一个参数(数字类型),返回其2倍的值  
x -> 2 * x  
  
// 3. 接受2个参数(数字),并返回他们的差值
(x, y) -> x – y  
  
// 4. 接收2个int型整数,返回他们的和  
(int x, int y) -> x + y  
  
// 5. 接受一个 string 对象,并在控制台打印,不返回任何值(看起来像是返回void)  
(String s) -> System.out.print(s)

总结:

  • 一个参数,小括号可省略
  • 无参数或大于一个参数,小括号不可省略
  • 能确定参数类型,可以省略类型关键字
  • 方法体只有一句代码,可以省略大括号和return以及分号;

原则:可推导可省略

 

四、Lambda表达式的分类

因为Lambda表达式的核心就是实现这个接口中抽象方法中的形参列表 -> 抽象方法的处理,因此根据形参列表与返回值的不同,Lambda表达式的具体写法也不相同;

特点:使用 "->"将参数和实现逻辑分离;( ) 中的部分是需要传入Lambda体中的参数;{ } 中部分,接收来自 ( ) 中的参数,完成一定的功能。

有参无返回值

自定义接口:

package cn.imyjs.java.type1;

@FunctionalInterface
public interface MyInterface {
    void show(int a, int b);
}

 

基础写法:

package cn.imyjs.java.type1;

public class Demo {
    public static void main(String[] args) {
        MyInterface myInterface = new MyInterface(){
            @Override
            public void show(int a, int b) {
                System.out.println(a + b);
            }
        };
        myInterface.show(10, 20); // 30
    }
}

 

Lambda 表达式简写:

@Test
public void testLambda(){
    // 简写1:方法名可以自己推断出来
    MyInterface myInterface = (int a, int b) -> {
        System.out.println(a + b);
    };
    myInterface.show(10, 20);

    // 简写2:可以省略形参列表中的形参类型
    MyInterface myInterface2 = (a, b) -> {
        System.out.println(a + b);
    };
    myInterface2.show(10, 20);

    // 简写3:如果抽象方法中只有一行代码,可以省略方法体的大括号,当然,如果不止一行,就不能省略
    MyInterface myInterface3 = (a, b) -> System.out.println(a + b);
    myInterface3.show(10, 20);
}

 

示例:

@Test
public void testForEach(){
    List<Integer> list = Arrays.asList(1, 2, 3, 4, 5);

    list.forEach(t -> System.out.println(t));
    // 方法引用更简单
    list.forEach(System.out::println);
}
// forEach() 功能等同与增强型for循环 这个方法来自于Iterable接口,Collection接口继承了这个接口,List又继承了
// Collection接口,而ArrayList是List的实现类;forEach函数,指明该函数需要传入一个函数,而且是有参数没有返回值的函数,
// 而Consumer接口中正好有且仅有一个这样的有参无返回值的抽象方法。

  • 可以省略方法名,IDEA会帮你自动检测方法名;
  • 可以省略方法中的形参类型;
  • 如果对抽象方法的实现逻辑只有一行,可以省略方法体的大括号,当然如果不止一行,就不能省略了;

有参有返回值

自定义接口

package cn.imyjs.java.type2;

public interface MyInterface {
    int add(double a, double b);
}

 

基础写法:

package cn.imyjs.java.type2;

public class Demo {
    public static void main(String[] args) {
        MyInterface myInterface = new MyInterface() {
            @Override
            public double add(double a, double b) {
                return a + b;
            }
        };
        System.out.println(myInterface.add(2.0, 6.0));
    }
}

 

Lambda 表达式简写:

@Test
public void testLambda(){
    // 简写一
    MyInterface myInterface = (double a, double b) -> {
        return a + b;
    };
    System.out.println(myInterface.add(2.0, 6.0));

    // 简写二
    MyInterface myInterface2 = (a, b) -> {
        return a + b;
    };
    System.out.println(myInterface2.add(2.0, 6.0));

    // 简写3:这个有返回值的方法,不能直接去掉大括号,还需要去掉return关键字
    MyInterface myInterface3 = (a, b) -> a + b;
    System.out.println(myInterface3.add(2.0, 6.0));
}

 

示例:

@Test
public void test(){
    // Collator是一个抽象类,但是提供了获取该类实例的方法getInstance()
    Collator collator = Collator.getInstance();
    TreeSet<Student> students =
        new TreeSet<>((s1, s2) ->collator.compare(s1.getName(), s2.getName()));
    students.add(new Student("小米", 80.0));
    students.add(new Student("小红", 88.0));
    students.add(new Student("小妹", 92.0));
    students.forEach(System.out::println);
}

  • 有返回值的方法,如果要去掉大括号,还需要去掉return关键字;

无参无返回值

自定义接口:

package cn.imyjs.java.type3;

public interface MyInterface {
    void print();
}

 

基础写法:

package cn.imyjs.java.type3;

public class Demo {
    public static void main(String[] args) {
        MyInterface myInterface = new MyInterface() {
            @Override
            public void print() {
                System.out.println("Hello");
            }
        };
        myInterface.print();
    }
}

 

Lambda 表达式简写:

@Test
public void testLambda(){
    MyInterface myInterface = () -> System.out.println("Hello!");
    myInterface.print();
}

 

示例:

public static void main(String[] args) {
    new Thread(() -> System.out.println("Hello~"));
}
()中无参数,也不能省略;{}中只有一句话,建议省略。

无参有返回值

自定义接口:

package cn.imyjs.java.type4;

public interface MyInterface {
    String getName();
}

 

基础写法:

package cn.imyjs.java.type4;

public class Demo {
    public static void main(String[] args) {
        MyInterface myInterface = new MyInterface(){
            @Override
            public String getName() {
                try {
                    Class<?> clazz = Class.forName("cn.imyjs.java.type4.Demo");
                    return clazz.getSimpleName();
                } catch (ClassNotFoundException e) {
                    throw new RuntimeException(e);
                }
            }
        };
        System.out.println(myInterface.getName()); // Demo
    }
}

 

Lambda 表达式简写:

@Test
    public void testLambda(){
        MyInterface myInterface = ()->{
            try {
                Class<?> clazz = Class.forName("cn.imyjs.java.type4.Demo");
                return clazz.getSimpleName();
            } catch (ClassNotFoundException e) {
                throw new RuntimeException(e);
            }
        };
        System.out.println(myInterface.getName());
    }

 

示例:

@Test
public void test(){
    //该方法将会不断的打印100以内的正整数。
    Random random = new Random();
    Stream<Integer> stream = Stream.generate(() ->random.nextInt(100));
    stream.forEach(t -> System.out.println(t));
    // Stream.generate()方法创建无限流,该方法要求传入一个无参有返回值的方法。
}

使用 Lambda 表达式需要注意以下两点:

  • Lambda 表达式主要用来定义行内执行的方法类型接口(例如,一个简单方法接口)。
  • Lambda 表达式免去了使用匿名方法的麻烦,并且给予 Java 简单但是强大的函数化的编程能力。

补充:变量作用域

Lambda 表达式只能引用标记了 final 的外层局部变量,这就是说不能在 Lambda内部修改定义在域外的局部变量,否则会编译错误。

public class Java8Tester {
 
   final static String salutation = "Hello! ";
   
   public static void main(String args[]){
      GreetingService greetService1 = message -> 
      System.out.println(salutation + message);
      greetService1.sayMessage("Runoob");
      // Hello! Runoob
   }
    
   interface GreetingService {
      void sayMessage(String message);
   }
}

 

我们也可以直接在 lambda 表达式中访问外层的局部变量:

public class Java8Tester {
    public static void main(String args[]) {
        final int num = 1;
        Converter<Integer, String> s = (param) -> System.out.println(String.valueOf(param + num));
        s.convert(2);  // 输出结果为 3
    }
 
    public interface Converter<T1, T2> {
        void convert(int i);
    }
}

 

lambda 表达式的局部变量可以不用声明为 final,但是必须不可被后面的代码修改(即隐性的具有 final 的语义)

int num = 1;  
Converter<Integer, String> s = (param) -> System.out.println(String.valueOf(param + num));
s.convert(2);
num = 5;  
//报错信息:Local variable num defined in an enclosing scope must be final or effectively final

 

在 Lambda 表达式当中不允许声明一个与局部变量同名的参数或者局部变量。

String first = "";  
Comparator<String> comparator = (first, second) -> Integer.compare(first.length(), second.length());  
//编译会出错 

Lambda 表达式的局部变量可以不用声明为 final,但是必须不可被后面的代码修改(即隐性的具有 final 的语义)

在 Lambda 表达式当中不允许声明一个与局部变量同名的参数或者局部变量。

 

五、什么是函数式接口?

即SAM(Single Abstract Method )接口,有且只有一个抽象方法的接口(可以有默认方法或者是静态方法和从Object继承来的方法,但是抽象方法有且只能有一个)。 JDK1.8之后,添加@FunctionalInterface表示这个接口是是一个函数式接口,因为有了@functionalInterface标记,也称这样的接口为Mark(标记)类型的接口。举例子:

@FunctionalInterface
public interface Runnable {
	  public abstract void run();
}

其实我们的Lambda表达式就是对函数式接口的一种简写方式,所以只有是函数式接口,我们才能用Lambda表达式;再换句话说,Lambda表达式需要函数式接口的支持,那函数式接口我们可以自己定义,当然JDK1.8也给我们提供了一些现成的函数式接口;

函数式接口(Functional Interface)就是一个有且仅有一个抽象方法,但是可以有多个非抽象方法的接口。

函数式接口可以被隐式转换为 Lambda 表达式。

可以通过 Lambda 表达式来创建该接口的对象,我们可以在任意函数式接口上使用@FunctionalInterface 注解,这样做可以检查它是否是一个函数式接口;

@FunctionalInterface注解写在接口声明上面,如果报错,就不是函数式接口;

六、函数式接口有什么作用?

可以通过 Lambda 表达式来创建该接口的对象,我们可以在任意函数式接口上使用@FunctionalInterface 注解,这样做可以检查它是否是一个函数式接口;

@FunctionalInterface注解写在接口声明上面,如果报错,就不是函数式接口;

上文中提到的例子:

new Thread(() -> System.out.println("hello")).start();

编译器会认为Thread()中传入的是一个Runnable的对象,而我们利用IDEA的智能感知,鼠标指向“->”或“()”的时候,会发现这是一个Runnable类型,实际上编译器会自动将Lambda表达式赋值给函数式接口,在本例中就是Runnable接口。本例中Lambda表达式将打印方法传递给了Runnable接口中的run()方法,从而形成真正的方法体。

而且,参数与返回值是一一对应的,即如果函数式接口中的抽象方法是有返回值,有参数的,那么要求Lambda表达式也是有返回值,有参数的。

七、自定义一个函数式接口

package cn.imyjs.java.type1;

@FunctionalInterface
public interface MyInterface {
    void show(int a, int b);
}

 

八、四大函数式接口

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

除了这四个之外,在java.util.function包下还有很多函数式接口可供使用。

九、函数式接口

JDK 1.8 之前已有的函数式接口:

  • 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

JDK 1.8 新增加的函数接口:

  • java.util.function

java.util.function 它包含了很多类,用来支持 Java的 函数式编程,该包中的函数式接口有:

序号 接口 & 描述
1 BiConsumer<T,U>代表了一个接受两个输入参数的操作,并且不返回任何结果
2 BiFunction<T,U,R>代表了一个接受两个输入参数的方法,并且返回一个结果
3 BinaryOperator代表了一个作用于于两个同类型操作符的操作,并且返回了操作符同类型的结果
4 BiPredicate<T,U>代表了一个两个参数的boolean值方法
5 BooleanSupplier代表了boolean值结果的提供方
6 Consumer代表了接受一个输入参数并且无返回的操作
7 DoubleBinaryOperator代表了作用于两个double值操作符的操作,并且返回了一个double值的结果。
8 DoubleConsumer代表一个接受double值参数的操作,并且不返回结果。
9 DoubleFunction代表接受一个double值参数的方法,并且返回结果
10 DoublePredicate代表一个拥有double值参数的boolean值方法
11 DoubleSupplier代表一个double值结构的提供方
12 DoubleToIntFunction接受一个double类型输入,返回一个int类型结果。
13 DoubleToLongFunction接受一个double类型输入,返回一个long类型结果
14 DoubleUnaryOperator接受一个参数同为类型double,返回值类型也为double 。
15 Function<T,R>接受一个输入参数,返回一个结果。
16 IntBinaryOperator接受两个参数同为类型int,返回值类型也为int 。
17 IntConsumer接受一个int类型的输入参数,无返回值 。
18 IntFunction接受一个int类型输入参数,返回一个结果 。
19 IntPredicate:接受一个int输入参数,返回一个布尔值的结果。
20 IntSupplier无参数,返回一个int类型结果。
21 IntToDoubleFunction接受一个int类型输入,返回一个double类型结果 。
22 IntToLongFunction接受一个int类型输入,返回一个long类型结果。
23 IntUnaryOperator接受一个参数同为类型int,返回值类型也为int 。
24 LongBinaryOperator接受两个参数同为类型long,返回值类型也为long。
25 LongConsumer接受一个long类型的输入参数,无返回值。
26 LongFunction接受一个long类型输入参数,返回一个结果。
27 LongPredicateR接受一个long输入参数,返回一个布尔值类型结果。
28 LongSupplier无参数,返回一个结果long类型的值。
29 LongToDoubleFunction接受一个long类型输入,返回一个double类型结果。
30 LongToIntFunction接受一个long类型输入,返回一个int类型结果。
31 LongUnaryOperator接受一个参数同为类型long,返回值类型也为long。
32 ObjDoubleConsumer接受一个object类型和一个double类型的输入参数,无返回值。
33 ObjIntConsumer接受一个object类型和一个int类型的输入参数,无返回值。
34 ObjLongConsumer接受一个object类型和一个long类型的输入参数,无返回值。
35 Predicate接受一个输入参数,返回一个布尔值结果。
36 Supplier无参数,返回一个结果。
37 ToDoubleBiFunction<T,U>接受两个输入参数,返回一个double类型结果
38 ToDoubleFunction接受一个输入参数,返回一个double类型结果
39 ToIntBiFunction<T,U>接受两个输入参数,返回一个int类型结果。
40 ToIntFunction接受一个输入参数,返回一个int类型结果。
41 ToLongBiFunction<T,U>接受两个输入参数,返回一个long类型结果。
42 ToLongFunction接受一个输入参数,返回一个long类型结果。
43 UnaryOperator接受一个参数为类型T,返回值类型也为T。

 

微信关注

编程那点事儿

编程那点事儿

阅读剩余
THE END