美文网首页Java
Java8的Lambda理解

Java8的Lambda理解

作者: 愤怒的老照 | 来源:发表于2020-03-06 11:04 被阅读0次

    函数接口

    为了理解Lambda,首先应该了解一下java8新出的函数接口,因为这是Lambda表达式的类型。它的定义是:一个接口,如果只有一个显式声明的抽象方法,那么它就是一个函数接口。一般用@FunctionalInterface标注出来(也可以不标)。 3.png

    如上图所示,1.8后,Runnable接口被标注成函数接口

    Lambda的理解

    什么是Lambda,本质上还是一个匿名函数,

        public int add(int x, int y) {
            return x + y;
        }
    
        (x, y) -> x+y;
    
        (x, y) -> { return x + y; } //显式指明返回值
    

    三者是等价的

    便于理解,假如我们有这么一个接口:

    public interface StringFunction{
        public String apply(String s);
    }
    
    

    还有这么一个函数

    public String run(StringFunction f){
        return f.apply("Hello world");
    }
    

    现在就可以使用Lambda来将匿名函数充当参数了

    run(s -> s.toUpperCase());
    

    其实也就是

    run(new StringFunction(){
        public String apply(String s){
           return s.toUpperCase();
       }
    })
    

    你可以用一个λ表达式为一个函数接口赋值:

    Runnable r1 = () -> {System.out.println("Hello Lambda!");};  
    

    然后再赋值给一个Object:

    
        Object obj = r1;
    

    但却不能这样干:

        Object obj = () -> {System.out.println("Hello Lambda!");}; // ERROR! Object is not a functional interface!
    

    必须显式的转型成一个函数接口才可以:

        Object o = (Runnable) () -> { System.out.println("hi"); }; // correct
    

    一个λ表达式只有在转型成一个函数接口后才能被当做Object使用。所以下面这句也不能编译:

        System.out.println( () -> {} ); //错误! 目标类型不明
    

    必须先转型:

        System.out.println( (Runnable)() -> {} ); // 正确
    

    假设你自己写了一个函数接口,长的跟Runnable一模一样:

        @FunctionalInterface
        public interface MyRunnable {
            public void run();
        }
    

    那么

        Runnable r1 =    () -> {System.out.println("Hello Lambda!");};
        MyRunnable2 r2 = () -> {System.out.println("Hello Lambda!");};
    

    都是正确的写法。这说明一个λ表达式可以有多个目标类型(函数接口),只要函数匹配成功即可。
    但需注意一个λ表达式必须至少有一个目标类型。

    JDK预定义了很多函数接口以避免用户重复定义。

        @FunctionalInterface
        public interface Function<T, R> {  
            R apply(T t);
        }
    
        @FunctionalInterface
        public interface Consumer<T> {
            void accept(T t);
        }
    
        @FunctionalInterface
        public interface Predicate<T> {
            boolean test(T t);
        }
    

    λ表达式的使用

    之前创建一个新的线程:

    Thread oldSchool = new Thread(new Runnable() {
                @Override
                public void run() {
                    System.out.println("This is from an anonymous class.");
                }
            });
    
    

    利用Lambda之后:

     Thread oldSchool = new Thread(() -> {
                System.out.println("This is from an anonymous class.");
            });
    

    除了可以简化代码,Lambda还可以和集合类很好地结合。java8引入了流的概念,可以将集合转换成流,对流的一次操作会返回另一个流,最终调用一个结束方法来终结流,就好像StringBuild中append和toString的概念类似。

    int []nums = new int[]{1,2,3,4,5,6};
    Arrays.stream(nums)
                    .boxed()
                    .filter(item -> item % 2 == 0)
                    .forEach(item -> System.out.println(item));
    // 输出结果2 4 6
    

    filter方法的参数是Predicate类型,forEach方法的参数是Consumer类型,它们都是函数接口,所以可以使用λ表达式。

    再看一个方法

    public static void test(String... nums){
            List<String> l = Arrays.asList(nums);
            List<Integer> r = l.stream()
                    .map(Integer::parseInt)
                    .distinct()
                    .collect(Collectors.toList());
    
            System.out.println(r);
        }
    

    利用map将元素转为Integer,去重后,利用collect收集

    其中有用到map方法,需要和flatMap作对比

    /* 输出
    [1]
    [2, 3]
    [4, 5, 6]
    */
    Stream.of(Arrays.asList(1),
                    Arrays.asList(2, 3), Arrays.asList(4, 5, 6))
                    .map(item -> item)
                    .forEach(item -> System.out.println(item));
    /*输出
    1
    2
    3
    4
    5
    6
    */
            Stream.of(Arrays.asList(1),
                    Arrays.asList(2, 3), Arrays.asList(4, 5, 6))
                    .flatMap(item -> item.stream())
                    .forEach(item -> System.out.println(item));
    

    可以看到flatMap将元素打平了

    collect也很有必要说一下,这是一个流的终结方法,

    <R, A> R collect(Collector<? super T, A, R> collector);
    // Collector参数解释
    // 创建一个容器
    Supplier<A> supplier();
    // 将元素添加到容器中
    BiConsumer<A, T> accumulator();
    // 将两个容器合并为一个结果容器
    BinaryOperator<A> combiner();
    // 对结果容器作相应的变换
    Function<A, R> finisher();
    // 对上述过程做优化和控制
    Set<Characteristics> characteristics();
    

    可以看到,collect方法要求传入一个Collector接口的实例对象,Collector可以看做是用来处理流的工具,在Collectors里面封装了很多Collector工具。

    了解了参数的意义,就可以理解Collectors工具类中的几个常用方法

        public static <T>
        Collector<T, ?, List<T>> toList() {
            return new CollectorImpl<>((Supplier<List<T>>) ArrayList::new, List::add,
                                       (left, right) -> { left.addAll(right); return left; },
                                       CH_ID);
        }
    
    
    4.png

    上面显示了Collectors.toList()的整个流程:

    1.创建一个空器 :ArrayList::new
    2.将流中的元素添加到容器中: List::add
    3.将两个容器合并:left.addAll(right)
    4.将结果较换成想要的格式:此例子中该方法不执行

    事实上,对流的操作并没有使元素被循环一次,实际上流操作分为Lazy和eager两种,Lazy没有遇到eager时,是不会执行的,除此之外,每个元素都是顺着流进行执行的,也就是全在一个循环中执行,并不会产生循环上的效率问题

    // Map转List
    Map<String,String> map = new HashMap<>();
    map.put("k1","v1");
    
    System.out.println(map.entrySet().stream().map(item -> item.getValue()).collect(Collectors.toList()));
    
    // List转Map
    List<String> list = new ArrayList<>();
    Collections.addAll(list,"1","2");
    
    System.out.println(list.stream().map(Integer::parseInt).collect(Collectors.toMap(item -> item + 1, item -> item)));
    
    

    reduce: // 对所有元素进行

    Optional<T> reduce(BinaryOperator<T> accumulator);
    T reduce(T identity, BinaryOperator<T> accumulator);
    <U> U reduce(U identity,
                     BiFunction<U, ? super T, U> accumulator,
                     BinaryOperator<U> combiner);
    

    第一个重载方法

    List<Integer> numList = Arrays.asList(1,2,3,4,5);
    int result = numList.stream().reduce((a,b) -> a + b ).get();
    System.out.println(result);
    

    lambada表达式的a参数是表达式的执行结果的缓存,也就是表达式这一次的执行结果会被作为下一次执行的参数,而第二个参数b则是依次为stream中每个元素。如果表达式是第一次被执行,a则是stream中的第一个元素。

    第二个重载方法与第一个相比,仅仅是多个一个初始值

    List<Integer> numList = Arrays.asList(1,2,3,4,5);
    int result = numList.stream().reduce(0,(a,b) ->  a + b );
    System.out.println(result);
    

    第三个参数只有并行流中才会执行(关于并行流,可以参考https://www.jianshu.com/p/ac2bcf2f9d48):

     private static void testMultiReduce() {
            ArrayList<List<String>> strings = new ArrayList<>();
            strings.add(Arrays.asList("1", "2", "3", "4"));
            strings.add(Arrays.asList("2", "3", "4", "5"));
            strings.add(Arrays.asList("3", "4", "5", "6"));
     
            // 非并行流
            Integer reduce1 = strings.stream().flatMap(e -> e.stream()).reduce(0, 
                                (acc, e) -> acc + Integer.valueOf(e), (u, t) -> {
                // 非并行流,不会执行第三个参数
                System.out.println("u----:" + u);
                // 这里的返回值并没有影响返回结果
                return null;
            });
            System.out.println("reduce1:" + reduce1);
     
            // 并行流
            Integer reduce2 = strings.parallelStream().flatMap(e -> e.stream()).reduce(0, 
                                (acc, e) -> acc + Integer.valueOf(e), (u, t) -> {
                // u,t分别为并行流每个子任务的结果
                System.out.println("u----:" + u);
                System.out.println("t----:" + t);
                return u + t;
            });
            System.out.println("reduce2:" + reduce2);
        }
    

    其他概念

    外部变量

    Lambda在使用外部变量上和内部类相似,只是Lambda如果不使用外部变量,则不含有外部类的this指针,而在java8之前,内部类想要用外部类的变量,必须声明为final,这是为了保证数据的一致性,在java8以后,可以不显示声明final,但是使用上来讲,还是要为final,否则会报错,Lambda也是相同的用法

    默认方法

    java8除了新增加了Lambda,还增加了接口方法的默认方法,在stream概念出来以后,需要像Collection中添加新的方法,但是事实上不可以更改这个接口,因为有很多人已经实现了这个接口,所以不可以随便更改。于是就提出了默认方法这一概念,是一种折中的办法。

    相关文章

      网友评论

        本文标题:Java8的Lambda理解

        本文链接:https://www.haomeiwen.com/subject/xlivrhtx.html