JDK 8新特性-Lambda表达式

  |   0 评论   |   0 浏览

JDK 8新特性-Lambda表达式

1.Lambda表达式

1.Lambda表达式介绍

1.目标

了解使用匿名内部类存在的问题体验Lambda

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

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

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

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

3.代码分析:

对于Runnable 的匿名内部类用法,可以分析出几点内容:

Thread 类需要Runnable 接口作为参数,其中的抽象run 方法是用来指定线程任务内容的核心为了指定run 的方法体,不得不需要Runnable 接口的实现类

为了省去定义一个Runnable 实现类的麻烦,不得不使用匿名内部类

必须覆盖重写抽象run 方法,所以方法名称、方法参数、方法返回值不得不再写一遍,且不能写错而实际上,似乎只有方法体才是关键所在

4.Lambda体验

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

借助Java 8的全新语法,上述Runnable 接口的匿名内部类写法可以通过更简单的Lambda表达式达到相同的效果

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

这段代码和刚才的执行效果是完全一样的,可以在JDK 8或更高的编译级别下通过。从代码的语义中可以看出:我们启动了一个线程,而线程任务的内容以一种更加简洁的形式被指定。

我们只需要将要执行的代码放到一个Lambda表达式中,不需要定义类,不需要创建对象。

5.Lambda的优点

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

6.小结

了解了匿名内部类语法冗余,体验了Lambda表达式的使用,发现Lmabda是简化匿名内部类的简写

2.Lambda的标准格式

1.目标

掌握Lambda的标准格式

练习无参数无返回值的Lambda 练习有参数有返回值的Lambda

2.Lambda的标准格式

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

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

3.格式说明:

(参数类型 参数名称):参数列表

{代码体;}:方法体

-> :箭头,分隔参数列表和方法体

Lambda与方法的对比

匿名内部类

public void run() {
    System.out.println("aa");
}

Lambda

() -> System.out.println("bb!")

4.练习无参数无返回值的Lambda

掌握了Lambda的语法,我们来通过一个案例熟悉Lambda的使用。

interface Swimmable { 
    public abstract void swimming(); 
}
public class Demo02LambdaUse {
    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 swimmable) {
        swimmable.swimming();
    }
}

5.练习有参数有返回值的Lambda

下面举例演示java.util.Comparator<T> 接口的使用场景代码,其中的抽象方法定义为:public abstract int compare(T o1, T o2);

当需要对一个对象集合进行排序时, Collections.sort 方法需要一个Comparator 接口实例来指定排序的规则。

传统写法

如果使用传统的代码对ArrayList 集合进行排序,写法如下:

public class Demo03LambdaUse {
    public static void main(String[] args) {
        List<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();
            }
        });
        /**
         * Person(name=黎明, age=53, height=178)
         * Person(name=刘德华, age=54, height=171)
         * Person(name=刘德华, age=58, height=174)
         * Person(name=张学友, age=58, height=176)
         */
        for (Person person : persons) {
            System.out.println(person);
        }
    }
}

这种做法在面向对象的思想中,似乎也是"理所当然"的。其中Comparator 接口的实例(使用了匿名内部类)代表了"按照年龄从小到大"的排序规则。

Lambda写法

public class Demo03LambdaUse {
    public static void main(String[] args) {
        List<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, (p1,p2)-> p1.getAge()-p2.getAge());
    }
}

6.小结

首先学习了Lambda表达式的标准格式

(参数列表) -> { 
    方法体;
}

以后我们调用方法时,看到参数是接口就可以考虑使用Lambda表达式,Lambda表达式相当于是对接口中抽象方法的重写

3.了解Lambda的实现原理

1.目标

了解Lambda的实现原理

我们现在已经会使用Lambda表达式了。现在同学们肯定很好奇Lambda是如何实现的,现在我们就来探究Lambda 表达式的底层实现原理。

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

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

我们可以看到匿名内部类会在编译后产生一个类:Demo04LambdaImpl$1.class

使用XJad反编译这个类,得到如下代码:

import java.io.PrintStream; 
// Referenced classes of package com.llp.demo01lambda: // Swimmable, Demo04LambdaImpl 
static class Demo04LambdaImpl$1 implements Swimmable { 		public void swimming() { 
    System.out.println("使用匿名内部类实现游泳"); }
                                                      	Demo04LambdaImpl$1() { 
  }
}

我们再来看看Lambda的效果,修改代码如下:

public class Demo04LambdaImpl {
    public static void main(String[] args) {
        goSwimming(() -> {
            System.out.println("Lambda游泳");
        });
    }

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

运行程序,控制台可以得到预期的结果,但是并没有出现一个新的类,也就是说Lambda并没有在编译的时候产生一 个新的类。使用XJad对这个类进行反编译,发现XJad报错。使用了Lambda后XJad反编译工具无法反编译。我们使用JDK自带的一个工具: javap ,对字节码进行反汇编,查看字节码指令。

在DOS命令行输入:

javap -c -p 文件名.class
-c:表示对代码进行反汇编 
-p:显示所有类和成员

反汇编后效果如下:

C:\Users\>javap -c -p Demo04LambdaImpl.class Compiled from "Demo04LambdaImpl.java" public class com.llp.demo01lambda.Demo04LambdaImpl { 
    public com.llp.demo01lambda.Demo04LambdaImpl(); 
    Code:0: aload_0 
        1: invokespecial #1 // Method java/lang/Object."<init>":()V 
        4: return 
            public static void main(java.lang.String[]); 
    Code:
    	0: invokedynamic #2, 0 // InvokeDynamic #0:swimming: ()Lcom/llp/demo01lambda/Swimmable; 
        5: invokestatic #3 // Method goSwimming: (Lcom/llp/demo01lambda/Swimmable;)V 
        8: return 
                public static void goSwimming(com.llp.demo01lambda.Swimmable); 
    Code:
    	0: aload_0 
        1: invokeinterface #4, 1 // InterfaceMethod com/llp/demo01lambda/Swimmable.swimming:()V 
        6: return 
                private static void lambda$main$0(); 
    Code:
    	0: getstatic #5 // Field java/lang/System.out:Ljava/io/PrintStream; 
        3: ldc #6 // String Lambda游泳 
        5: invokevirtual #7 // Method java/io/PrintStream.println: (Ljava/lang/String;)V 
        8: return 
}

可以看到在类中多出了一个私有的静态方法lambda$main$0 。这个方法里面放的是什么内容呢?我们通过断点调试来看看:

可以确认lambda$main$0 里面放的就是Lambda中的内容,我们可以这么理解lambda$main$0 方法:

public class Demo04LambdaImpl {
    public static void main(String[] args) {
        ...
    }

    private static void lambda$main$0() {
        System.out.println("Lambda游泳");
    }
}

关于这个方法lambda$main$0 的命名:以lambda开头,因为是在main()函数里使用了lambda表达式,所以带有 $main表示,因为是第一个,所以$0。

如何调用这个方法呢?其实Lambda在运行的时候会生成一个内部类,为了验证是否生成内部类,可以在运行时加上-Djdk.internal.lambda.dumpProxyClasses ,加上这个参数后,运行时会将生成的内部类class码输出到一个文件中。使用java命令如下:

java -Djdk.internal.lambda.dumpProxyClasses 要运行的包名.类名

根据上面的格式,在命令行输入以下命令:

C:\Users\>java -Djdk.internal.lambda.dumpProxyClasses com.llp.demo01lambda.Demo04LambdaImpl Lambda游泳

执行完毕,可以看到生成一个新的类,效果如下:

image-20220919210916935

反编译Demo04LambdaImpl$$Lambda$1.class这个字节码文件,内容如下:

// Referenced classes of package com.llp.demo01lambda: // Swimmable, Demo04LambdaImpl 
final class Demo04LambdaImpl$$Lambda$1 implements Swimmable { 
     public void swimming() { 		              
        Demo04LambdaImpl.lambda$main$0(); 
     }
	 private Demo04LambdaImpl$$Lambda$1() {}
}

可以看到这个匿名内部类实现了Swimmable 接口,并且重写了swimming 方法, swimming 方法调用

Demo04LambdaImpl.lambda$main$0() ,也就是调用Lambda中的内容。最后可以将Lambda理解为:

2.小结

匿名内部类在编译的时候会一个class文件

Lambda在程序运行的时候形成一个类

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

4.Lambda省略格式

目标

掌握Lambda省略格式

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

  1. 小括号内参数的类型可以省略
  2. 如果小括号内有且仅有一个参数,则小括号可以省略
  3. 如果大括号内有且仅有一个语句,可以同时省略大括号、return关键字及语句分号
(int a) -> { return new Person(); }

省略后

a -> new Person()

5.Lambda的前提条件

1.目标

掌握Lambda的前提条件

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

  1. 方法的参数或局部变量类型必须为接口才能使用Lambda
  2. 接口中有且仅有一个抽象方法
public interface Flyable { 
    public abstract void flying();
}
public class Demo05LambdaCondition {
    public static void main(String[] args) {
        test01(() -> {
        });
        Flyable s = new Flyable() {
            @Override
            public void flying() {
            }
        };
        Flyable s2 = () -> {
        };
    }

    public static void test01(Flyable fly) {
        fly.flying();
    }
}

2.小结

Lambda表达式的前提条件:

  1. 方法的参数或变量的类型是接口
  2. 这个接口中只能有一个抽象方法

6.函数式接口

函数式接口在Java中是指:有且仅有一个抽象方法的接口

函数式接口,即适用于函数式编程场景的接口。而Java中的函数式编程体现就是Lambda,所以函数式接口就是可以 适用于Lambda使用的接口。只有确保接口中有且仅有一个抽象方法,Java中的Lambda才能顺利地进行推导。

@FunctionalInterface注解

与@Override 注解的作用类似,Java 8中专门为函数式接口引入了一个新的注解: @FunctionalInterface 。该注解可用于一个接口的定义上:

@FunctionalInterface
public interface Operator {
    void myMethod();
}

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

7.Lambda和匿名内部类对比

1.目标

了解Lambda和匿名内部类在使用上的区别

  1. 所需的类型不一样

匿名内部类,需要的类型可以是类,抽象类,接口

Lambda表达式,需要的类型必须是接口

  1. 抽象方法的数量不一样

匿名内部类所需的接口中抽象方法的数量随意

Lambda表达式所需的接口只能有一个抽象方法

  1. 实现原理不同

匿名内部类是在编译后会形成class

Lambda表达式是在程序运行的时候动态生成class

2.小结

当接口中只有一个抽象方法时,建议使用Lambda表达式,其他其他情况还是需要使用匿名内部类

2.JDK 8接口新增的两个方法

目标

了解JDK 8接口新增的两个方法

掌握接口默认方法的使用

掌握接口静态方法的使用

1.JDK 8接口增强介绍

JDK 8以前的接口:

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

JDK 8对接口的增强,接口还可以有默认方法静态方法

JDK 8的接口:

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

2.接口引入默认方法的背景

在JDK 8以前接口中只能有抽象方法。存在以下问题:

如果给接口新增抽象方法,所有实现类都必须重写这个抽象方法。不利于接口的扩展。

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

    // 接口新增抽象方法,所有实现类都需要去重写这个方法
    @Override
    public void test2() {
        System.out.println("BB test2");
    }
}

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

    // 接口新增抽象方法,所有实现类都需要去重写这个方法
    @Override
    public void test2() {
        System.out.println("CC test2");
    }
}

例如,JDK 8 时在Map接口中增加了 forEach 方法:

public interface Map<K, V> { 
    ...
    abstract void forEach(BiConsumer<? super K, ? super V> action);
}

通过API可以查询到Map接口的实现类如:

image-20220919212749708

如果在Map接口中增加一个抽象方法,所有的实现类都需要去实现这个方法,那么工程量时巨大的。 因此,在JDK 8时为接口新增了默认方法,效果如下:

public interface Map<K, V> { 
    	... 
    	default void forEach(BiConsumer<? super K, ? super V> action) { 
        ... 
	}
}

接口中的默认方法实现类不必重写,可以直接使用,实现类也可以根据需要重写。这样就方便接口的扩展。

3.接口默认方法的定义格式

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

4.接口默认方法的使用

方式一:实现类直接调用接口默认方法

方式二:实现类重写接口默认方法

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

interface AA {
    public abstract void test1();

    public 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实现类重写接口默认方法");
    }
}

5.接口静态方法

为了方便接口扩展,JDK 8为接口新增了静态方法。

1.接口静态方法的定义格式

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

接口静态方法的使用

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

代码

public class Demo04UseStaticFunction {
    public static void main(String[] args) {
        // 直接使用接口名调用即可:接口名.静态方法名();
        AAA.test01();
    }
}

interface AAA {
    public static void test01() {
        System.out.println("AAA 接口的静态方法");
    }
}

class BBB implements AAA {
    /* @Override 静态方法不能重写 
    public static void test01() {
     System.out.println("AAA 接口的静态方法"); 
     }
     */
}

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

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

3.小结

接口中新增的两种方法: 默认方法和静态方法

如何选择呢?如果这个方法需要被实现类继承或重写,使用默认方法,如果接口中的方法不需要被继承就使用静态方法

3.常用内置函数式接口

目标

了解内置函数式接口由来了解常用内置函数式接口

1.内置函数式接口来由来

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

public class Demo01UserFunctionalInterface {
    public static void main(String[] args) {
        // 调用函数式接口中的方法 
        method((arr) -> {
            int sum = 0;
            for (int n : arr) {
                sum += n;
            }
            return sum;
        });
    }

    // 使用自定义的函数式接口作为方法参数 
    public static void method(Operator op) {
        int[] arr = {1, 2, 3, 4};
        int sum = op.getSum(arr);
        System.out.println("sum = " + sum);
    }
}

@FunctionalInterface
interface Operator {
    public abstract int getSum(int[] arr);
}

2.常用内置函数式接口介绍

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

  1. Supplier接口

    @FunctionalInterface
    public interface Supplier<T> {
        public abstract T get();
    }
    
  2. Consumer接口

    @FunctionalInterface
    public interface Consumer<T> {
        public abstract void accept(T t);
    }
    
  3. Function接口

    @FunctionalInterface
    public interface Function<T, R> {
        public abstract R apply(T t);
    }
    
  4. Predicate接口

    @FunctionalInterface
    public interface Predicate<T> {
        public abstract boolean test(T t);
    }
    Predicate接口用于做判断,返回boolean类型的值
    

4.Supplier接口

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

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

供给型接口,通过Supplier接口中的get方法可以得到一个值,无参有返回的接口。使用Lambda表达式返回数组元素最大值

使用Supplier 接口作为方法参数类型,通过Lambda表达式求出int数组中的最大值。提示:接口的泛型请使用java.lang.Integer类。

代码示例:

public class Demo05Supplier {
    public static void main(String[] args) {
        printMax(() -> {
            int[] arr = {10, 20, 100, 30, 40, 50};
            // 先排序,最后就是最大的 
            Arrays.sort(arr);
            return arr[arr.length - 1];
            // 最后就是最大的 
        });
    }

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

5.Consumer接口

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

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

使用Lambda表达式将一个字符串转成大写和小写的字符串

Consumer消费型接口,可以拿到accept方法参数传递过来的数据进行处理, 有参无返回的接口。基本使用如:

public class Demo06Consumer {
    public static void main(String[] args) {
        // Lambda表达式 
        test((String s) -> {
            System.out.println(s.toLowerCase());
        });
    }

    public static void test(Consumer<String> consumer) {
        consumer.accept("HelloWorld");
    }
}

默认方法:andThen

如果一个方法的参数和返回值全都是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语句和抛出空指针异常的麻烦。

要想实现组合,需要两个或多个Lambda表达式即可,而andThen 的语义正是"一步接一步"操作。例如两个步骤组合的情况:

public class Demo07ConsumerAndThen {
    public static void main(String[] args) {
        // Lambda表达式
        test((String s) -> {
            System.out.println(s.toLowerCase());
        }, (String s) -> {
            System.out.println(s.toUpperCase());
        });
        // Lambda表达式简写
        test(s -> System.out.println(s.toLowerCase()), s -> System.out.println(s.toUpperCase()));
    }

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

运行结果将会首先打印完全大写的HELLO,然后打印完全小写的hello。当然,通过链式写法可以实现更多步骤的组 合。

6.Function接口

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

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

使用Lambda表达式将字符串转成数字

Function转换型接口,对apply方法传入的T类型数据进行处理,返回R类型的结果,有参有返回的接口。使用的场景 例如:将String 类型转换为Integer 类型。

public class Demo08Function {
    public static void main(String[] args) {
        // Lambda表达式
        test((String s) -> {
            return Integer.parseInt(s);// 10
        });
    }

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

默认方法:andThen

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));
}

该方法同样用于"先做什么,再做什么"的场景,和Consumer 中的andThen 差不多:

public class Demo09FunctionAndThen {
    public static void main(String[] args) {
        // Lambda表达式
        test((String s) -> {
            return Integer.parseInt(s);
        }, (Integer i) -> {
            return i * 10;
        });
    }

    public static void test(Function<String, Integer> f1, Function<Integer, Integer> f2) {
        Integer in = f1.apply("66");// 将字符串解析成为int数字
        Integer in2 = f2.apply(in);// 将上一步的int数字乘以10
        Integer in3 = f1.andThen(f2).apply("66");
        System.out.println("in3: " + in3); // 660
    }
}

第一个操作是将字符串解析成为int数字,第二个操作是乘以10。两个操作通过 andThen 按照前后顺序组合到了一起。

请注意,Function的前置条件泛型和后置条件泛型可以相同。

7.Predicate接口

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

@FunctionalInterface 
public interface Predicate<T> {
    public abstract boolean test(T t); 
}
Predicate接口用于做判断,返回boolean类型的值

使用Lambda判断一个人名如果超过3个字就认为是很长的名字

对test方法的参数T进行判断,返回boolean类型的结果。用于条件判断的场景:

public class Demo10Predicate { 
    public static void main(String[] args) { 
        test(s -> s.length() > 3, "迪丽热巴"); }
    private static void test(Predicate<String> predicate, String str) { 
        boolean veryLong = predicate.test(str); 
        System.out.println("名字很长吗:" + veryLong); 
    } 
}

条件判断的标准是传入的Lambda表达式逻辑,只要名称长度大于3则认为很长。

1.默认方法:and

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

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

使用Lambda表达式判断一个字符串中即包含W,也包含H

使用Lambda表达式判断一个字符串中包含W或者包含H

使用Lambda表达式判断一个字符串中即不包含W

如果要判断一个字符串既要包含大写"H",又要包含大写"W",那么:

public class Demo10Predicate_And_Or_Negate {
    public static void main(String[] args) {
        // Lambda表达式
        test((String s) -> {
            return s.contains("H");
        }, (String s) -> {
            return s.contains("W");
        });
    }

    public static void test(Predicate<String> p1, Predicate<String> p2) {
        String str = "HelloWorld";
        boolean b1 = p1.test(str);
        // 判断包含大写“H” boolean b2 = p2.test(str); 
        // 判断包含大写“W” 
        if (b1 && b2) {
            System.out.println("即包含W,也包含H");
        }
        boolean bb = p1.and(p2).test(str);
        if (bb) {
            System.out.println("即包含W,也包含H");
        }
    }
}

2.默认方法: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);
}

如果希望实现逻辑"字符串包含大写H或者包含大写W",那么代码只需要将"and"修改为"or"名称即可,其他都不变:

public class Demo10Predicate_And_Or_Negate {
    public static void main(String[] args) {
        // Lambda表达式
        test((String s) -> {
            return s.contains("H");
        }, (String s) -> {
            return s.contains("W");
        });
    }

    public static void test(Predicate<String> p1, Predicate<String> p2) {
        String str = "HelloWorld";
        boolean b1 = p1.test(str); // 判断包含大写“H”
        boolean b2 = p2.test(str); // 判断包含大写“W”
        //if (b1 || b2) {
          //  System.out.println("有H,或者W");
        //}
        boolean bbb = p1.or(p2).test(str);
        if (bbb) {
            System.out.println("有H,或者W");
        }
    }
}

3.默认方法:negate

使用Lambda表达式判断一个字符串中即不包含W

"与"、"或"已经了解了,剩下的"非"(取反)也会简单。默认方法negate 的JDK源代码为:

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

从实现中很容易看出,它是执行了test方法之后,对结果boolean值进行"!"取反而已。一定要在 test 方法调用之前调用negate 方法,正如and 和or 方法一样:

public class Demo10Predicate_And_Or_Negate {
    public static void main(String[] args) {
        // Lambda表达式
        test((String s) -> {
            return s.contains("H");
        }, (String s) -> {
            return s.contains("W");
        });
    }

    public static void test(Predicate<String> p1, Predicate<String> p2) {
        String str = "HelloWorld";
        boolean b1 = p1.test(str); // 判断包含大写“H” 
        boolean b2 = p2.test(str); // 判断包含大写“W” 
        // 没有H,就打印 
        if (!b1) {
            System.out.println("没有H");
        }
        boolean test = p1.negate().test(str);
        if (test) {
            System.out.println("没有H");
        }
    }
}

8.小结

  1. Supplier接口
  2. Consumer接口
  3. Function接口
  4. Predicate接口

4.方法引用

目标

了解Lambda的冗余场景掌握方法引用的格式

了解常见的方法引用方式

1.Lambda的冗余场景

使用Lambda表达式求一个数组的和

public class Demo11MethodRefIntro {
    public static void getMax(int[] arr) {
        int sum = 0;
        for (int n : arr) {
            sum += n;
        }
        System.out.println(sum);
    }

    public static void main(String[] args) {
        printMax((int[] arr) -> {
            int sum = 0;
            for (int n : arr) {
                sum += n;
            }
            System.out.println(sum);
        });
    }

    private static void printMax(Consumer<int[]> consumer) {
        int[] arr = {10, 20, 30, 40, 50};
        consumer.accept(arr);
    }
}

体验方法引用简化Lambda

如果我们在Lambda中所指定的功能,已经有其他方法存在相同方案,那是否还有必要再写重复逻辑?可以直接"引 用"过去就好了:

public class DemoPrintRef {
    public static void getMax(int[] arr) {
        int sum = 0;
        for (int n : arr) {
            sum += n;
        }
        System.out.println(sum);
    }

    public static void main(String[] args) {
        printMax(Demo11MethodRefIntro::getMax);
    }

    private static void printMax(Consumer<int[]> consumer) {
        int[] arr = {10, 20, 30, 40, 50};
        consumer.accept(arr);
    }
}

请注意其中的双冒号:: 写法,这被称为"方法引用",是一种新的语法。

2.方法引用的格式

1.符号表示 ::

符号说明 : 双冒号为方法引用运算符,而它所在的表达式被称为方法引用

应用场景 : 如果Lambda所要实现的方案 , 已经有其他方法存在相同方案,那么则可以使用方法引用。

3.常见引用方式

方法引用在JDK 8中使用方式相当灵活,有以下几种形式:

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

4.小结

首先了解Lambda表达式的冗余情况,体验了方法引用,了解常见的方法引用方式

5.对象名::引用成员方法

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

// 对象::实例方法 
@Test
public void test01() {
    Date now = new Date();
    Supplier<Long> supp = () -> {
        return now.getTime();
    };
    System.out.println(supp.get());
    Supplier<Long> supp2 = now::getTime;
    System.out.println(supp2.get());
}

方法引用的注意事项

  1. 被引用的方法,参数要和接口中抽象方法的参数一样
  2. 当接口抽象方法有返回值时,被引用的方法也必须有返回值

6.类名::引用静态方法

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

// 类名::静态方法
@Test
public void test02() {
    Supplier<Long> supp = () -> {
        return System.currentTimeMillis();
    };
    System.out.println(supp.get());
    Supplier<Long> supp2 = System::currentTimeMillis;
    System.out.println(supp2.get());
}

7.类名::引用实例方法

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

// 类名::实例方法
@Test
public void test03() {
    Function<String, Integer> f1 = (s) -> {
        return s.length();
    };
    System.out.println(f1.apply("abc"));
    Function<String, Integer> f2 = String::length;
    System.out.println(f2.apply("abc"));
    BiFunction<String, Integer, String> bif = String::substring;
    String hello = bif.apply("hello", 2);
    System.out.println("hello = " + hello);
}

8.类名::new引用构造器

由于构造器的名称与类名完全一样。所以构造器引用使用类名称::new 的格式表示。首先是一个简单的Person 类:

public class Person {
    private String name;

    public Person(String name) {
        this.name = name;
    }

    public String getName() {
        return name;
    }
}
// 类名::new
@Test
public void test04() {
    Supplier<Person> sup = () -> {
        return new Person();
    };
    System.out.println(sup.get());
    Supplier<Person> sup2 = Person::new;
    System.out.println(sup2.get());
    BiFunction<String, Integer, Person> fun2 = Person::new;
    System.out.println(fun2.apply("张三", 18));
}

要使用这个函数式接口,可以通过方法引用传递:

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

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

// 类型[]::new
@Test
public void test05() {
    Function<Integer, String[]> fun = (len) -> {
        return new String[len];
    };
    String[] arr1 = fun.apply(10);
    //[Ljava.lang.String;@78e67e0a, 10
    System.out.println(arr1 + ", " + arr1.length);
    Function<Integer, String[]> fun2 = String[]::new;
    String[] arr2 = fun2.apply(5);
    //[Ljava.lang.String;@80503, 5
    System.out.println(arr2 + ", " + arr2.length);
}

10.小结

方法引用是对Lambda表达式符合特定情况下的一种缩写,它使得我们的Lambda表达式更加的精简,也可以理解为Lambda表达式的缩写形式 , 不过要注意的是方法引用只能"引用"已经存在的方法!


标题:JDK 8新特性-Lambda表达式
作者:llp
地址:https://llinp.cn/articles/2022/09/20/1663635438973.html