Java

Java8的Lambda理解

2020-03-06  本文已影响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中添加新的方法,但是事实上不可以更改这个接口,因为有很多人已经实现了这个接口,所以不可以随便更改。于是就提出了默认方法这一概念,是一种折中的办法。

上一篇 下一篇

猜你喜欢

热点阅读