java 基础给自己编程语言爱好者

java8(二)lambda表达式手把手学习

2021-10-20  本文已影响0人  我犟不过你

Lambda可以让你简单的传递一个行为或者代码。可以把lambda看作是匿名函数,是没有声明名称的方法,但和匿名类一样,也可以作为参数传递给一个方法。

可以把Lambda表达式理解为:简洁地表示可传递的匿名函数的一种方式:它没有名称,但它有参数列表、函数主体、返回类型,可能还有一个可以抛出的异常列表。

一、在哪里使用lambda?

1.1 函数式接口

函数式接口就是只定义一个抽象方法的接口

Lambda表达式允许你直接以内联的形式为函数式接口的抽象方法提供实现,并把整个表达式作为函数式接口的实例。具体来说,Lambda表达式是函数是接口的具体实现的实例。

通过匿名内部类也可以完成同样的事情,但是比较笨拙:需要提供一个实现,然后在直接内联将它的实现实例化。如下面的例子所示,可以清楚地看到lambda的简洁:

public class TestLambdaAndAnonymity {
    /**
     * 执行方法
     * @param r
     */
    public static void process(Runnable r){
        r.run();
    }
    
    public static void main(String[] args) {
        // 使用匿名类
        Runnable r1 = new Runnable() {
            @Override
            public void run() {
                System.out.println("this is r1");
            }
        };

        // 使用Lambda
        Runnable r2 = ()-> System.out.println("this is r2");
        
        process(r1);
        process(r2);
        // 直接将lambda作为参数传递
        process(()-> System.out.println("this is r3"));
    }
}

1.2 函数描述符

函数式接口的抽象方法的签名基本上就是Lambda表达式的签名。我们将这种抽象方法叫作函数描述符。

举个例子:
Runnable 接口可以看作一个什么也不接受什么也不返回( void )的函数的签名,因为它只有一个叫作 run 的抽象方法,这个方法什么也不接受,什么也不返回( void )。

@FunctionalInterface
public interface Runnable {

    public abstract void run();
}

如上个小节的例子:“()-> System.out.println("this is r3")”, () -> void就是这个函数的签名, 代表了参数列表为空,且返回 void 的函数。这正是 Runnable 接口所代表的。

举另一个例子, (Apple,Apple) -> int 代表接受两个 Apple 作为参数且返回 int 的函数。

1.3 @FunctionalInterface 又是怎么回事?

如果你去看看新的Java API,会发现函数式接口带有 @FunctionalInterface 的标注。这个标注用于表示该接口会设计成一个函数式接口。如果你用 @FunctionalInterface 定义了一个接口,而它却不是函数式接口的话,编译器将返回一个提示原因的错误。如下:

    @FunctionalInterface
    private interface ITest{
        void test();

        void test1();
    }
image.png

表示该接口存在多个抽象方法。

@FunctionalInterface 不是必需的,但对于为此设计的接口而言,使用它是比较好的做法。

二、使用函数式接口

函数式接口定义且只定义了一个抽象方法。函数式接口很有用,因为抽象方法的签名可以描述Lambda表达式的签名。函数式接口的抽象方法的签名称为函数描述符。所以为了应用不同的Lambda表达式,你需要一套能够描述常见函数描述符的函数式接口。

Java 8的库设计师帮你在 java.util.function 包中引入了几个新的函数式接口。

2.1 Predicate

java.util.function.Predicate<T> 接口定义了一个名叫 test 的抽象方法,它接受泛型T 对象,并返回一个 boolean 。

@FunctionalInterface
public interface Predicate<T> {

    /**
     * Evaluates this predicate on the given argument.
     *
     * @param t the input argument
     * @return {@code true} if the input argument matches the predicate,
     * otherwise {@code false}
     */
    boolean test(T t);
}

如下筛选华为手机的例子:

    /**
     * 手机过滤方法,参数是手机list和Predicate<T>函数
     */
    public static List<Phone> phoneFilter(List<Phone> phones, Predicate<Phone> p) {
        List<Phone> results = new ArrayList<>();
        for (Phone phone : phones) {
            if (p.test(phone)) {
                results.add(phone);
            }
        }
        return results;
    }

    // 调用筛选方法
    List<Phone> huaweiPhones = phoneFilter(list, (Phone p) -> "华为".equals(p.brand));

关于其中的and、or等方法此小节先不考虑。

2.2 Consumer

java.util.function.Consumer<T> 定义了一个名叫 accept 的抽象方法,它接受泛型 T的对象,没有返回( void )。你如果需要访问类型 T 的对象,并对其执行某些操作,就可以使用这个接口。

@FunctionalInterface
public interface Consumer<T> {

    /**
     * Performs this operation on the given argument.
     *
     * @param t the input argument
     */
    void accept(T t);

如下例子,分别打印每个值:

public class TestConsumer {

    public static void main(String[] args) {
        forEach(Arrays.asList(1, 2, 3, 4, 5), (Integer i) -> System.out.println(i));
    }

    public static <T> void forEach(List<T> list, Consumer<T> c) {
        for (T t : list) {
            c.accept(t);
        }

    }
}

2.3 Function

java.util.function.Function<T, R> 接口定义了一个叫作 apply 的方法,它接受一个泛型 T 的对象,并返回一个泛型 R 的对象。如果你需要定义一个Lambda,将输入对象的信息映射到输出,就可以使用这个接口。

@FunctionalInterface
public interface Function<T, R> {

    /**
     * Applies this function to the given argument.
     *
     * @param t the function argument
     * @return the function result
     */
    R apply(T t);

如下所示,将String列表的长度映射到Integer列表:

    public static <T, R> List<R> map(List<T> list, Function<T, R> function) {
        List<R> results = new ArrayList<>();
        for (T t : list) {
            results.add(function.apply(t));
        }
        return results;
    }

    public static void main(String[] args) {
        List<Integer> map = map(Arrays.asList("Tom", "Jerry", "XiaoMing"), (String s) -> s.length());
        System.out.println(map);
    }

以上简单介绍了三种函数式接口的使用方式。

2.4 带有数据类型的函数式接口

Java类型要么是引用类型(比如 Byte 、 Integer 、 Object 、 List ),要么是原始类型(比如 int 、 double 、 byte 、 char )。但是泛型(比如 Consumer<T> 中的 T )只能绑定到引用类型。这是由泛型内部的实现方式造成的。

因此,在Java里有一个将原始类型转换为对应的引用类型的机制。这个机制叫作装箱(boxing)。相反的操作,也就是将引用类型转换为对应的原始类型,叫作拆箱(unboxing)。Java还有一个自动装箱机制来帮助程序员执行这一任务:装箱和拆箱操作是自动完成的。

如下所示就是一个装箱过程,将int类型装箱成Integer类型:

List<Integer> list = new ArrayList<>();
for (int i = 300; i < 400; i++){
    list.add(i);
}

但这在性能方面是要付出代价的。装箱后的值本质上就是把原始类型包裹起来,并保存在堆里。因此,装箱后的值需要更多的内存,并需要额外的内存搜索来获取被包裹的原始值。

减少装箱拆箱是有必要的!

java8为了减少这样的问题,针对不同的接口函数提供专门的版本,用来避免自动装箱操作。

如下举例1000万次的循环,分别比较装箱和不装箱的耗时有多大:

    public static void main(String[] args) throws InterruptedException {
        Long current = System.currentTimeMillis();

        // 非装箱
        IntPredicate intPredicate = (int i) -> i % 3 == 0;
        CompletableFuture.runAsync(() -> {
            for (int i = 0; i < 10000000; i++) {
                intPredicate.test(RandomUtil.randomInt(100000));
            }
            System.out.println("线程名称:IntPredicate" + Thread.currentThread().getName()+"耗时:"+ (System.currentTimeMillis() - current));
        });

        // 装箱
        Predicate<Integer> predicate = (Integer i) -> i % 3 == 0;
        CompletableFuture.runAsync(() -> {
            for (int i = 0; i < 10000000; i++) {
                predicate.test(RandomUtil.randomInt(100000));
            }
            System.out.println("线程名称:Predicate" + Thread.currentThread().getName()+"耗时:"+ (System.currentTimeMillis() - current));
        });

        //等待线程执行完成
        Thread.sleep(1000);
    }

结果显示,使用IntPredicate比Predicate耗时差距在50ms左右:

线程名称:IntPredicateForkJoinPool.commonPool-worker-1耗时:143
线程名称:PredicateForkJoinPool.commonPool-worker-2耗时:194

一般来说,针对特殊类型的函数式接口的名称都需要加上对应的原始类型的前缀。

下面归纳常用函数式接口:

函数式接口 函数式描述符 原始类型特化
Predicate<T> T->boolean IntPredicate
LongPredicate
DoublePredicate
Consumer<T> T->void IntConsumer
LongConsumer
DoubleConsumer
Function<T,R> T->R IntFunction<R>
IntToDoubleFunction
IntToLongFunction
LongFunction<R>
LongToDoubleFunction
LongToIntFunction
DoubleFunction<R>
ToIntFunction<T>
ToDoubleFunction<T>
ToLongFunction<T>
Supplier<T> ()-T BooleanSupplier,IntSupplier
LongSupplier
DoubleSupplier
UnaryOperator<T> T->T IntUnaryOperator
LongUnaryOperator
DoubleUnaryOperator
BinaryOperator<T> (T,T)->T IntBinaryOperator
LongBinaryOperator
DoubleBinaryOperator
BiPredicate<L,R> (L,R)->boolean
BiConsumer<T,U> (T,U)->void ObjIntConsumer<T>
ObjLongConsumer<T>
ObjDoubleConsumer<T>
BiFunction<T,U,R> (T,U)->R ToIntBiFunction<T,U>
ToLongBiFunction<T,U>
ToDoubleBiFunction<T,U>

三、使用局部变量

Lambda可以没有限
制地捕获(也就是在其主体中引用)实例变量和静态变量。但局部变量必须显式声明为 final ,或事实上是 final 。换句话说,Lambda表达式只能捕获指派给它们的局部变量一次。(注:捕获实例变量可以被看作捕获最终局部变量 this 。)

如下代码将会出现变异错误,因为portBynber变了两次:

        int portNumber = 1337;
        Runnable r = () -> System.out.println(portNumber);
        portNumber = 31337;
image.png

3.1 限制局部变量的原因?

1)实例变量和局部变量背后的实现有一个关键不同。实例变量都存储在堆中,而局部变量则保存在栈上。

如果Lambda可以直接访问局部变量,而且Lambda是在一个线程中使用的,则使用Lambda的线程,可能会在分配该变量的线程将这个变量收回之后,去访问该变量。

因此,Java在访问自由局部变量时,实际上是在访问它的副本,而不是访问原始变量。

如果局部变量仅仅赋值一次那就没有什么区别了。

2)这一限制不鼓励你使用改变外部变量的典型命令式编程模式。

这种模式会阻碍很容易做到的并行处理,在java8当中使用的函数式编程可以很轻易的做到并行处理,而传统的命令式编程却需要自己去实现多线程与数据的合并。

四、方法引用

4.1 方法引用简介

方法引用是某些Lambda的快捷写法。让你可以重复使用现有的方法定义,并像Lambda一样传递它们。

当你需要使用方法引用时,目标引用放在分隔符 :: 前,方法的名称放在后面。如下面的例子:

Apple::getWeight

Apple::getWeight 就是引用了 Apple 类中定义的方法 getWeight,方法引用就是Lambda表达式 (Apple a) -> a.getWeight() 的快捷写法。

你可以把方法引用看作针对仅仅涉及单一方法的Lambda的语法糖,因为你表达同样的事情时要写的代码更少了。

4.2 构建方法引用

方法引用主要有三类:
1)指向静态方法的方法引用
如下所示,指向静态方法parseInt。

    public static void main(String[] args) {
        //静态方法引用,parseInt
        Function<String, Integer> function = Integer::parseInt;
        Integer apply = function.apply("100");
        System.out.println(apply);
    }

2)指向任意类型实例方法的方法引用
如下所示,指向String的length方法

        //指向任意实例类型方法
        Function<String,Integer> function1 = String::length;
        Integer hello_world = function1.apply("hello world");
        System.out.println(hello_world);

可以这样理解这句话:你在引用一个对象的方法,而这个对象本身是Lambda的一个参数。

3)指向现有实例对象的方法的方法引用
如下所示:

    @Data
    static class Student{
        private String name;
        private Integer age;

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

        //指向现有对象的方法
        Student student = new Student("Jack",10);
        Supplier<String> supplier = student::getName;
        String s = supplier.get();
        System.out.println(s);

与第二条不同,这个方法引用的对象时外部已经存在的对象,如上述例子中的student。

4.3 构造函数引用

对于一个现有构造函数,你可以利用它的名称和关键字 new 来创建它的一个引用:ClassName::new 。它的功能与指向静态方法的引用类似。

有如下实体类:

    @Data
    static class Student {
        private String name;
        private Integer age;
        private String address;

        public Student() {
        }

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

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

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

实例:

    /**
     * description: 自定义三个参数的接口

     * @return:
     * @author: weirx
     * @time: 2021/10/19 18:03
     */
    interface ThreeParamsFunction<T, U, P, R> {
        R apply(T t, U u, P p);
    }

    public static void main(String[] args) {
        // 无构造参数 ()->new Student() 转化成 Student::new
        Supplier<Student> supplier = Student::new;
        Student student = supplier.get();
        // 一个构造参数 (name)->new Student(name) 转化成 Student::new
        Function<String, Student> function = Student::new;
        function.apply("JACK");
        // 两个构造参数 (name,age)->new Student(name,age) 转化成 Student::new
        BiFunction<String, Integer, Student> biFunction = Student::new;
        biFunction.apply("JACK", 10);
        // 三个构造参数,没有提供三个构造参数的,需要自己写个接口
        // (name,age,address)->new Student(name,age,address) 转化成 Student::new
        ThreeParamsFunction<String, Integer, String, Student> threeParamsFunction = Student::new;
        threeParamsFunction.apply("JACK", 10, "Haerbin");
    }

五、实战

使用List的sort方法对苹果进行排序:

void sort(Comparator<? super E> c)

5.1 传递代码

根据上面的sort方法构成,我们需要一个Comparator对象对苹果进行比较。在没有java8之前,我们需要这样做,传递代码的方式

import lombok.Data;

import java.util.Arrays;
import java.util.Comparator;
import java.util.List;

/**
 * @description: 使用List的sort方法,对苹果进行排序
 * @author:weirx
 * @date:2021/10/20 9:44
 * @version:3.0
 */
public class TestLambda {

    @Data
    static class Apple {
        private Integer weight;

        public Apple(Integer weight) {
            this.weight = weight;
        }
    }

    /**
     * 自定义一个比较类,实现Comparator接口
     */
    public static class CompareApple implements Comparator<Apple> {

        @Override
        public int compare(Apple o1, Apple o2) {
            return o1.getWeight().compareTo(o2.getWeight());
        }
    }

    public static void main(String[] args) {
        List<Apple> apples = Arrays.asList(
                new Apple(10), new Apple(19), new Apple(9), new Apple(22));
        apples.sort(new CompareApple());
    }
}

5.2 匿名类

匿名类实现接口,重写compare方法,不需要单独写实现类了

import lombok.Data;

import java.util.Arrays;
import java.util.Comparator;
import java.util.List;

/**
 * @description: 使用List的sort方法,对苹果进行排序
 * @author:weirx
 * @date:2021/10/20 9:44
 * @version:3.0
 */
public class TestLambda {

    @Data
    static class Apple {
        private Integer weight;

        public Apple(Integer weight) {
            this.weight = weight;
        }
    }
    
    public static void main(String[] args) {
        List<Apple> apples = Arrays.asList(
                new Apple(10), new Apple(19), new Apple(9), new Apple(22));
        // 匿名类实现接口,重写compare方法
        apples.sort(new Comparator<Apple>() {
            @Override
            public int compare(Apple o1, Apple o2) {
                return o1.getWeight().compareTo(o2.getWeight());
            }
        });
    }
}

5.3 使用lambda表达式

这一步将上面的匿名类转换成了lambda表达式,下面不写多余代码了:

    public static void main(String[] args) {
        List<Apple> apples = Arrays.asList(
                new Apple(10), new Apple(19), new Apple(9), new Apple(22));
        // lambda表达式,替换匿名类
        apples.sort((a1, a2) -> a1.getWeight().compareTo(a2.getWeight()));
    }

在Comparator中有一个静态辅助方法comparing,其参数是传递一个Funtion<T>,在方法内部进行了比较:

    public static <T, U extends Comparable<? super U>> Comparator<T> comparing(
            Function<? super T, ? extends U> keyExtractor)
    {
        Objects.requireNonNull(keyExtractor);
        return (Comparator<T> & Serializable)
            (c1, c2) -> keyExtractor.apply(c1).compareTo(keyExtractor.apply(c2));
    }

所以最终会变成如下的使用方式:

    public static void main(String[] args) {
        List<Apple> apples = Arrays.asList(
                new Apple(10), new Apple(19), new Apple(9), new Apple(22));
        // lambda表达式,替换匿名类
        apples.sort(Comparator.comparing((Apple a) -> a.getWeight()));
    }

5.4 方法引用

最简洁的实现方式如下所示:

import lombok.Data;

import java.util.Arrays;
import java.util.Comparator;
import java.util.List;

/**
 * @description: 使用List的sort方法,对苹果进行排序
 * @author:weirx
 * @date:2021/10/20 9:44
 * @version:3.0
 */
public class TestLambda {

    @Data
    static class Apple {
        private Integer weight;

        public Apple(Integer weight) {
            this.weight = weight;
        }
    }

    public static void main(String[] args) {
        List<Apple> apples = Arrays.asList(
                new Apple(10), new Apple(19), new Apple(9), new Apple(22));
        // 方法引用,替换lambda
        apples.sort(Comparator.comparing((Apple::getWeight)));
    }
上一篇下一篇

猜你喜欢

热点阅读