JDK8的Lambda表达式和方法引用

Lambda表达式介绍

使用匿名内部类存在的问题

当需要启动一个线程去完成任务时,通常会通过 Runnable 接口来定义任务内容,并使用 Thread 类来启动该线程。
传统写法,代码如下:

public static void main(String[] args) {
    new Thread(new Runnable() {
        @Override
        public void run() {
            System.out.println("线程任务执行");
        }
    }).start();
}

由于面向对象的语法要求,首先创建一个 Runnable 接口的匿名内部类对象来指定线程要执行的任务内容,再将其交给一个线程来启动。
代码分析:
对于 Runnable 的匿名内部类用法,可以分析出几点内容:

  • Thread 类需要 Runnable 接口作为参数,其中的抽象 run 方法是用来指定线程任务内容的核心
  • 为了指定 run 的方法体,不得不需要 Runnable 接口的实现类
  • 为了省去定义一个 Runnable 实现类的麻烦,不得不使用匿名内部类
  • 必须覆盖重写抽象 run 方法,所以方法名称、方法参数、方法返回值不得不再写一遍,且不能写错
  • 而实际上,似乎只有方法体才是关键所在。

Lambda体验

Lambda 是一个匿名函数,可以理解为一段可以传递的代码。
Lambda 表达式写法,代码如下:

public static void main(String[] args) {
    new Thread(()->{
        System.out.println("线程任务执行");
    }).start();
}

这段代码和刚才的执行效果是完全一样的,可以在 JDK8 或更高的编译级别下通过。从代码的语义中可以看出:我们启动了一个线程,而线程任务的内容以一种更加简洁的形式被指定。
我们只需要将要执行的代码放到一个Lambda表达式中,不需要定义类,不需要创建对象。

Lambda的优点

简化匿名内部类的使用,语法更加简单。

Lambda的标准格式

Lambda省去面向对象的条条框框,Lambda的标准格式格式由3个部分组成:

(参数类型 参数名称) -> {
    代码体;
}

格式说明:

  • (参数类型 参数名称):参数列表
  • {代码体;}:方法体
  • 箭头:分隔参数列表和方法体

练习无参数无返回值的Lambda

public class Test {
    public static void main(String[] args) {
        goSwimming(new Swimmable() {
            @Override
            public void swimming() {
                System.out.println("匿名内部类游泳");
            }
        });
        goSwimming(() -> {
            System.out.println("Lambda游泳");
        });
    }

    public static void goSwimming(Swimmable s) {
        s.swimming();
    }
}

interface Swimmable {
    void swimming();
}

练习无参数无返回值的Lambda

下面举例演示 java.util.Comparator 接口的使用场景代码,其中的抽象方法定义为:public abstract int compare(T o1, T o2);
当需要对一个对象集合进行排序时,Collections.sort 方法需要一个 Comparator 接口实例来指定排序的规则。

public class Person {
    private String name;
    private int age;
    private int height;
    ...
}

public class Test {
    public static void main(String[] args) {
        ArrayList<Person> persons = new ArrayList<>();
        persons.add(new Person("刘德华", 58, 174));
        persons.add(new Person("张学友", 58, 176));
        persons.add(new Person("刘德华", 54, 171));
        persons.add(new Person("黎明", 53, 178));

        Collections.sort(persons, new Comparator<Person>() {
            @Override
            public int compare(Person o1, Person o2) {
                return o1.getAge() - o2.getAge(); //升序
            }
        });

        Collections.sort(persons, (Person o1, Person o2) -> {
            return o2.getAge() - o1.getAge(); //降序
        });

        for (Person person : persons) {
            System.out.println(person);
        }
    }
}

了解Lambda的实现原理

匿名内部类在编译的时候会一个class文件;
Lambda在程序运行的时候形成一个类:

  • 在类中新增一个方法,这个方法的方法体就是Lambda表达式中的代码
  • 还会形成一个匿名内部类,实现接口,重写抽象方法
  • 在接口的重写方法中会调用新生成的方法

Lambda省略格式

在Lambda标准格式的基础上,使用省略写法的规则为:

  1. 小括号内参数的类型可以省略
  2. 如果小括号内有且仅有一个参数,则小括号可以省略
  3. 如果大括号内有且仅有一个语句,可以同时省略大括号、return关键字及语句分号

Lambda的前提条件

Lambda的语法非常简洁,但是Lambda表达式不是随便使用的,使用时有几个条件要特别注意:

  1. 方法的参数或局部变量类型必须为接口才能使用Lambda
  2. 接口中有且仅有一个抽象方法

函数式接口

函数式接口在Java中是指:有且仅有一个抽象方法的接口
函数式接口,即适用于函数式编程场景的接口。而Java中的函数式编程体现就是Lambda,所以函数式接口就是可以适用于Lambda使用的接口。只有确保接口中有且仅有一个抽象方法,Java中的Lambda才能顺利地进行推导。
@FunctionalInterface注解与 @Override注解的作用类似,Java8 中专门为函数式接口引入了一个新的注解: @FunctionalInterface 。该注解可用于一个接口的定义上

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

一旦使用该注解来定义接口,编译器将会强制检查该接口是否确实有且仅有一个抽象方法,否则将会报错。不过,即使不使用该注解,只要满足函数式接口的定义,这仍然是一个函数式接口,使用起来都一样。

Lambda和匿名内部类对比

  1. 所需的类型不一样
    • 匿名内部类,需要的类型可以是类,抽象类,接口
    • Lambda表达式,需要的类型必须是接口
  2. 抽象方法的数量不一样
    • 匿名内部类所需的接口中抽象方法的数量随意
    • Lambda表达式所需的接口只能有一个抽象方法
  3. 实现原理不同
    • 匿名内部类是在编译后会形成class
    • Lambda表达式是在程序运行的时候动态生成class
      建议:当接口中只有一个抽象方法时,建议使用Lambda表达式,其他情况还是需要使用匿名内部类

JDK8 接口新增的两个方法

interface 接口名{
	静态常量;
	抽象方法;
	默认方法;
	静态方法;
}

接口默认方法

interface 接口名 {
	修饰符 default 返回值类型 方法名() {
		代码;
	}
}

接口默认方法的使用

  1. 实现类直接调用接口默认方法
  2. 实现类重写接口默认方法
public class Demo {
    public static void main(String[] args) {
        BB b = new BB();
        // 方式一:实现类直接调用接口默认方法
        b.test02();
        CC c = new CC();
        // 调用实现类重写接口默认方法
        c.test02();
    }
}

interface AA {
    void test1();

    default void test02() {
        System.out.println("AA test02");
    }
}

class BB implements AA {
    @Override
    public void test1() {
        System.out.println("BB test1");
    }
}

class CC implements AA {
    @Override
    public void test1() {
        System.out.println("CC test1");
    }

    // 方式二:实现类重写接口默认方法
    @Override
    public void test02() {
        System.out.println("CC实现类重写接口默认方法");
    }
}

接口静态方法

interface 接口名 {
	修饰符 static 返回值类型 方法名() {
		代码;
	}
}

直接使用接口名调用即可:接口名.静态方法名();

接口默认方法和静态方法的区别

  1. 默认方法通过实例调用,静态方法通过接口名调用。
  2. 默认方法可以被继承,实现类可以直接使用接口默认方法,也可以重写接口默认方法。
  3. 静态方法不能被继承,实现类不能重写接口静态方法,只能使用接口名调用

常用内置函数式接口

我们知道使用Lambda表达式的前提是需要有函数式接口。而Lambda使用时不关心接口名,抽象方法名,只关心抽象方法的参数列表和返回值类型。因此为了让我们使用Lambda方便,JDK提供了大量常用的函数式接口。

常用内置函数式接口介绍

它们主要在 java.util.function 包中。下面是最常用的几个接口。

@FunctionalInterface
public interface Supplier<T> {
    T get();
}

@FunctionalInterface
public interface Consumer<T> {
    void accept(T t);
}

@FunctionalInterface
public interface Function<T, R> {
    R apply(T t);
}

@FunctionalInterface
public interface Predicate<T> {
    boolean test(T t);
}

Supplier接口

java.util.function.Supplier 接口,它意味着"供给",对应的Lambda表达式需要“对外提供”一个符合泛型类型的对象数据。

public class Demo {
    // 使用Lambda表达式返回数组元素最大值
    public static void main(String[] args) {
        printMax(() -> {
            int[] arr = {11, 99, 88, 77, 22};
            Arrays.sort(arr); // 升序排序
            return arr[arr.length - 1];
        });
    }

    public static void printMax(Supplier<Integer> supplier) {
        int max = supplier.get();
        System.out.println("max = " + max);
    }
}

Consumer接口

java.util.function.Consumer 接口则正好相反,它不是生产一个数据,而是消费一个数据,其数据类型由泛型参数决定。

public class Demo {
    // 使用Lambda表达式将一个字符串转成大写的字符串
    public static void main(String[] args) {
        printUpperString((String str) -> {
            System.out.println(str.toUpperCase());
        });
    }

    public static void printUpperString(Consumer<String> consumer) {
        consumer.accept("Hello World");
    }
}

如果一个方法的参数和返回值全都是 Consumer 类型,那么就可以实现效果:消费一个数据的时候,首先做一个操作,然后再做一个操作,实现组合。而这个方法就是 Consumer 接口中的default方法andThen 。下面是JDK的源代码:

default Consumer<T> andThen(Consumer<? super T> after) {
	Objects.requireNonNull(after);
	return (T t) -> { accept(t); after.accept(t); };
}
//备注:java.util.Objects 的 requireNonNull 静态方法将会在参数为null时主动抛出NullPointerException 异常。这省去了重复编写if语句和抛出空指针异常的麻烦。
public class Demo {
    // 使用Lambda表达式先将一个字符串转成小写的字符串,再转成大写
    public static void main(String[] args) {
        printString((String str) -> {
            System.out.println(str.toLowerCase());
        }, (String str) -> {
            System.out.println(str.toUpperCase());
        });
    }

    public static void printString(Consumer<String> c1, Consumer<String> c2) {
        String str = "Hello World";
        // c1.accept(str);
        // c2.accept(str);
        c1.andThen(c2).accept(str);
    }
}

Function接口

java.util.function.Function<T,R> 接口用来根据一个类型的数据得到另一个类型的数据,前者称为前置条件,后者称为后置条件。有参数有返回值。

public class Demo {
    // 使用Lambda表达式将字符串转成数字
    public static void main(String[] args) {
        getNumber((String str) -> {
            return Integer.parseInt(str);
        });
    }

    public static void getNumber(Function<String, Integer> function) {
        Integer num = function.apply("10");
        System.out.println("num = " + num);
    }
}

Function 接口中有一个默认的 andThen 方法,用来进行组合操作。JDK源代码如:

default <V> Function<T, V> andThen(Function<? super R, ? extends V> after) {
	Objects.requireNonNull(after);
	return (T t) -> after.apply(apply(t));
}
public class Demo {
    // 使用Lambda表达式将字符串转成数字, 第二个操作将这个数字乘以5
    public static void main(String[] args) {
        getNumber((String str) -> {
            return Integer.parseInt(str);
        }, (Integer i) -> {
            return i * 5;
        });
    }

    public static void getNumber(Function<String, Integer> f1, Function<Integer, Integer> f2) {
        // Integer num = f1.apply("6");
        // Integer num2 = f2.apply(num);
        Integer num2 = f1.andThen(f2).apply("6");
        System.out.println("num2 = " + num2);
    }
}

Predicate接口

有时候我们需要对某种类型的数据进行判断,从而得到一个boolean值结果。这时可以使用java.util.function.Predicate 接口。

public class Demo {
    // 使用Lambda判断一个人名如果超过3个字就认为是很长的名字
    public static void main(String[] args) {
        isLongName((String name) -> {
            return name.length() > 3;
        });
    }

    public static void isLongName(Predicate<String> predicate) {
        boolean isLong = predicate.test("tom");
        System.out.println("是否是长名字: " + isLong);
    }
}

默认方法:and。既然是条件判断,就会存在与、或、非三种常见的逻辑关系。其中将两个 Predicate条件使用“与”逻辑连接起来实现“并且”的效果时,可以使用default方法 and 。其JDK源码为:

default Predicate<T> and(Predicate<? super T> other) {
	Objects.requireNonNull(other);
	return (t) -> test(t) && other.test(t);
}

默认方法:or。使用Lambda表达式判断一个字符串中包含W或者包含H与 and 的“与”类似,默认方法or 实现逻辑关系中的“或”。JDK源码为:

default Predicate<T> or(Predicate<? super T> other) {
	Objects.requireNonNull(other);
	return (t) -> test(t) || other.test(t);
}

默认方法:negate。使用Lambda表达式判断一个字符串中即不包含W“与”、“或”已经了解了,剩下的“非”(取反)也会简单。默认方法 negate 的JDK源代码为:

default Predicate<T> negate() {
	return (t) -> !test(t);
}

案例:

public class Demo {
    public static void main(String[] args) {
        test((String str) -> {
            // 判断是否包含W
            return str.contains("W");
        }, (String str) -> {
            // 判断是否包含H
            return str.contains("H");
        });
    }

    public static void test(Predicate<String> p1, Predicate<String> p2) {
        // 使用Lambda表达式判断一个字符串中即包含W,也包含H
        String str = "Hello World";
        boolean b = p1.and(p2).test(str);
        if (b) {
            System.out.println("即包含W,也包含H");
        }
        // 使用Lambda表达式判断一个字符串中包含W或者包含H
        boolean b1 = p1.or(p2).test(str);
        if (b1) {
            System.out.println("包含W或者包含H");
        }
        // 使用Lambda表达式判断一个字符串中不包含W
        boolean b2 = p1.negate().test("Hello W");
        // negate相当于取反 !boolean
        if (b2) {
            System.out.println("不包含W");
        }
    }
}

方法引用

符号说明 : 双冒号为方法引用运算符,而它所在的表达式被称为方法引用。
应用场景 : 如果Lambda所要实现的方案 , 已经有其他方法存在相同方案,那么则可以使用方法引用。
方法引用在JDK 8中使用方式相当灵活,有以下几种形式:

  1. instanceName::methodName 对象::方法名
  2. ClassName::staticMethodName 类名::静态方法
  3. ClassName::methodName 类名::普通方法
  4. ClassName::new 类名::new 调用的构造器
  5. TypeName[]::new String[]::new 调用数组的构造器

对象名::引用成员方法

这是最常见的一种用法。如果一个类中已经存在了一个成员方法,则可以通过对象名引用成员方法,代码为:

// 对象::实例方法
public class Demo {
    public static void main(String[] args) {
        Date now = new Date();
        /*Supplier<Long> su1 = () -> {
        return now.getTime();
        };*/
        // 使用方法引用
        Supplier<Long> su1 = now::getTime;
        Long aLong = su1.get();
        System.out.println("aLong = " + aLong);
        // 注意:方法引用有两个注意事项
        // 1.被引用的方法,参数要和接口中抽象方法的参数一样
        // 2.当接口抽象方法有返回值时,被引用的方法也必须有返回值
    }
}

类名::引用静态方法

由于在 java.lang.System 类中已经存在了静态方法 currentTimeMillis ,所以当我们需要通过Lambda来调用该方法时,可以使用方法引用 , 写法是:

// 类名::静态方法
public class Demo {
    public static void main(String[] args) {
        /*Supplier<Long> su = () -> {
            return System.currentTimeMillis();
        };*/
        Supplier<Long> su = System::currentTimeMillis;
        Long time = su.get();
        System.out.println("time = " + time);
    }
}

类名::引用实例方法

java面向对象中,类名只能调用静态方法,类名引用实例方法是有前提的,实际上是拿第一个参数作为方法的调用者。

// 类名::引用实例方法
public class Demo {
    public static void main(String[] args) {
        /*Function<String, Integer> f1 = (String str) -> {
            return str.length();
        };*/
        // 类名::实例方法(注意:类名::实例方法实际上会将第一个参数作为方法的调用者)
        Function<String, Integer> f1 = String::length;
        int length = f1.apply("hello");
        System.out.println("length = " + length);
        // BiFunction<String, Integer, String> f2 = String::substring;
        // 相当于这样的Lambda
        BiFunction<String, Integer, String> f2 = (String str, Integer index) -> {
            return str.substring(index);
        };
        String str2 = f2.apply("helloworld", 3);
        System.out.println("str2 = " + str2); // loworld
    }
}

类名::new引用构造器

由于构造器的名称与类名完全一样。所以构造器引用使用 类名称::new 的格式表示。

// 类名::new引用类的构造器
public class Demo {
    public static void main(String[] args) {
        /*Supplier<Person> su1 = () -> {
            return new Person();
        };*/
        Supplier<Person> su1 = Person::new;
        Person person = su1.get();
        System.out.println("person = " + person);
        /*BiFunction<String, Integer, Person> bif = (String name, Integer age) -> {
            return new Person(name, age);
        };*/
        BiFunction<String, Integer, Person> bif = Person::new;
        Person p2 = bif.apply("凤姐", 18);
        System.out.println("p2 = " + p2);
    }
}

数组::new 引用数组构造器

数组也是 Object 的子类对象,所以同样具有构造器,只是语法稍有不同。

// 类型[]::new
public class Demo {
    public static void main(String[] args) {
       /*Function<Integer, int[]> f1 = (Integer length) -> {
            return new int[length];
        };*/
        Function<Integer, int[]> f1 = int[]::new;
        int[] arr1 = f1.apply(10);
        System.out.println(Arrays.toString(arr1));
    }
}
posted @ 2023-01-12 11:06  wandoubaguo  阅读(46)  评论(0编辑  收藏  举报