深入lambda表达式详细详细

    xiaoxiao2022-07-05  170

    lambda表达式:它没有名称,但有参数列表、函数体、返回类型,必要的时候还有抛出的异常的列表。

    下面来看看lambda的特征

    匿名:它没有名称,集中精力做正事。函数化:lambda表达式不像其他方法属于特定类,但是比方法要简单多,人恨话不多,直奔核心主题,执行代码块。传递:可以作为方法的参数也可以存贮在变量中。简洁:不需要像匿名类那样写模板,是不按套路走的兄弟。

    基本语法

    (parameters) -> expression (parameters) -> { statements; }

    可以看到lambda完全是纯爷们,一眼就看透了,没错它的语法就是那么简单。还想再详细可以看我java8:lambda新特性

    示例:

    根据上述语法规则,以下哪个不是有效的Lambda表达式?

    (1) () -> {} (2) () -> "Raoul" (3) () -> {return "Mario";} (4) (Integer i) -> return "Alan" + i; (5) (String s) -> {"IronMan";}

    答案:只有45是无效的Lambda

    (1) 这个Lambda没有参数,并返回void。它类似于主体为空的方法:public void run() {}

    (2) 这个Lambda没有参数,并返回String作为表达式。

    (3) 这个Lambda没有参数,并返回String(利用显式返回语句)。

    (4) return是一个控制流语句。要使此Lambda有效,需要使花括号,如下所示:

    (Integer i) -> {return "Alan" + i;}

    (5)Iron Man”是一个表达式,不是一个语句。要使此Lambda有效,你可以去除花括号

    和分号,如下所示:(String s) -> "Iron Man"。或者如果你喜欢,可以使用显式返回语

    句,如下所示:(String s)->{return "IronMan";}

     

    使用案例示例布尔表达式(List<String> list)->list.isEmpty()创建对象()->new IPhone()消费一个对象(Apple a)->{System.out.println(a.getApple);}从一个对象中选择/抽取(String s)->s.length()组合两个值(a,b)->a*b...... 

    在哪里以及如何使用lambda

    一般lambda表达式都式配合函数式接口一起使用的,如何使用如上图示例。

    函数式接口

    就是只定义了一个抽象方法的接口。注意是一个抽象方法,在java8后在接口中可以有默认的实现方法但不妨碍函数式接口的定义,一般都会在接口上加@FunctionInterface,其实只要只有一个抽象方法不加注解也是可以的。java8中添加了一个java.util.function的包里面全是函数式接口

    那么函数式接口可以干嘛?

    可以让lambda表达式直接以内联的形式为函数式接口的方式提供实现,并把整个表达式作为函数式接口的实例(跟创建一个对象差不多),比匿名类要简单的多。下面示例的代码时等效的。

    Runnable r1 = () -> System.out.println("Hello World 1"); Runnable r2 = new Runnable(){ public void run(){ System.out.println("Hello World 2"); } };

    使用函数式接口

                                                                                       java 8中常用函数式接口

    函数式接口函数描述符原始类型特化Predicate<T>T->booleanIntPredicate,LongPredicate,DoublePredicateConsumer<T>T->voidIntConsumer,LongConsumer,DoubleConsumerFunction<T,R>T->R

    IntFunction<R>,IntToDoubleFunction,IntToLongFunction

    LongFunction<R>,LongToIntFunction,LongToDoubleFunction

    DoubleFunction<R>,DoubleToIntFunction,DoubleToLongFunction

    ToIntFunction<T>,ToLongFunction<T>,ToDoubleFunction<T>

    Supplier<T>()->TBooleanSupplier,IntSupplier,LongSupplier,DoubleSupplierUnaryOperator<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>

    java8中的util包中提供很多的函数式接口,已经可以我满足绝大多数需求啦,如果还不行你还可以自己去定义。

    Lambda的类型检查、类型推断以及限制

    提到Lambda表达式时,说它可以为函数式接口生成一个实例。然而,Lambda表达式本身并不包含它在实现哪个函数函数式接口的信息。为了全面了解Lambda表达式,我们应该知道Lambda实际类型是什么。

    1.Lambda的类型是从使用Lambda的上下文推断出来的。上下文(比如,接收它传递的方法的参数吗,或接收它的值的局部变量)中Lambda表达式需要的类型称为目标类型

    List<Apple> heavierThan150g = filter(inventory, (Apple a) -> a.getWeight() > 150);//筛选大于150克的苹果

    类型检查分解如下:

    首先,找出filter方法的声明第二,要求它是Predicate<Apple>目标类型第三,Predicate<Apple>是一个函数式接口,它的抽象方法是test(Apple apple)第四,test的函数描述符(方法的签名),是接收一个Apple的类,并返回一个boolean最后,filter的实际参数必须匹配

    这段代码是有效的,因为我们所传递的Lambda表达式也同样接受Apple为参数,并返回一个 boolean。请注意,如果Lambda表达式抛出一个异常,那么抽象方法所声明的throws语句也必 须与之匹配。

    2.同样的Lambda,不同的函数式接口

    有了目标类型的概念,同一个Lambda表达式就可以与不同的函数式接口联系起来,只要他们的抽象想法签名能够兼容。

    示例

    @FunctionalInterface public interface Callable<V> { /** * Computes a result, or throws an exception if unable to do so. * * @return computed result * @throws Exception if unable to compute a result */ V call() throws Exception; } public interface PrivilegedAction<T> { /** * Performs the computation. This method will be called by * {@code AccessController.doPrivileged} after enabling privileges. * * @return a class-dependent value that may represent the results of the * computation. Each class that implements * {@code PrivilegedAction} * should document what (if anything) this value represents. * @see AccessController#doPrivileged(PrivilegedAction) * @see AccessController#doPrivileged(PrivilegedAction, * AccessControlContext) */ T run()

    下面两个赋值时是等效的

    Callable<Integer> c = () -> 42; PrivilegedAction<Integer> p = () -> 42;

    特别注意:如果一个Lambda主体语句是一个语句表达式,它就和一个返回void的函数描述符兼容(当然需要参数列表也兼容)。例如,一下两行都是合法的,尽管List的add方法返回了一个boolean,而不是Consumer上下文(T->void)

    // Predicate返回了一个boolean Predicate<String> p = s -> list.add(s); // Consumer返回了一个void Consumer<String> b = s -> list.add(s);

    3.类型推断

    我们还可以进一步简化代码,Java编译器会从上下文(目标类型)推断出用什么函数式接口来配合Lambda表达式,这意味着它也可以推断出适合Lambda表达式的签名,因为函数描述符可以通过目标类型来得到。这样做的好处在于,编译器可以了解Lambda表达式的参数类型,这样就可以在Lambda中省去标注参数类型

    4.使用局部变量

    Lambda表达式不只可以用主体里面的参数,还可以使用自由变量(外层作用域中定义的变量),就像匿名类一样。

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

    注意:Lambda可以没有限制的使用实例变量和静态变量,但局部变量必须式final类型,实际上java编译器会默认给变量声明为final,就像上例。

    为什么会对局部变量做这些限制

    第一,实例变量和局部变量背后的实现有一个关键不同,实例变量都存储在堆中,而局部变量都保存在栈上。如果Lambda可以直接访问局部变量,而且是在一个线程中使用,可能会在分配该变量的线程将这个变量回收后去访问这个变量(局部变量已经销毁),因此访问局部变量其实是访问它的副本,而不是原始变量,这其实涉及到一点JMM.如果局部变量仅仅访问一次,那么怎么访问都是没有区别的。

    进一步简化Lambda表达式:方法引用

    方法引用是让我可以重复的使用现有的方法定义,提高编码效率,实现代码重用。

    inventory.sort((Apple a1, Apple a2) -> a1.getWeight().compareTo(a2.getWeight()));//根据苹果的重量进行排序 inventory.sort(comparing(Apple::getWeight));//java.util.Comparator.comparing

    根据现有的方法直接进行方法的引用。

    方法引用其实可以看做是Lambda表达式的语法糖(在编译语言中添加某些语法,但不影响编译语言本身,使代码有更好的可读性)。

    构建方法引用

    (1)指向静态方法的方法引用,例如:Integer::parseInt

       (2)指向任意类型实例方法的引用,例如:String::length

       (3)指向现有对象的实例方法引用 , 例如 Person:;getName

    第二种和第三种方法的引用可能乍看起来有点晕,类似于String::length的第二种方法引用就是你在引用一个对象的方法,而这个对象本身是Lambda的一个参数。例如,Lambda表达式(String s )->s.toUpperCase()可以写作String::toUpperCase。但第三种方法引用指的是,你在Lambda中调用一个已经存在的外部对象的方法

    重构实例

    还有针对构造函数、数组构造函数和父类调用(super-all)的一些特殊形式的方法引用。比方说我们想要对一个字符串的List排序,忽略大小写。List的sort方法需要一个Comparator作为参数。Comparator描述了一个具有(T,T)->int签名的函数描述,String类中的compareToIgnoreCase方法来定义Lambda表达式。

    List<String> str = Arrays.asList("a","b","A","B"); str.sort((s1, s2) -> s1.compareToIgnoreCase(s2));

    Lambda表达式的签名与Comparator的函数描述兼容。所以可以改写为

    List<String> str = Arrays.asList("a","b","A","B"); str.sort(String::compareToIgnoreCase);

    注意编译器会进行一种与Lambda表达式类似检查过程,来确定对于给定的函数式接口,这个方法引用是否有效;方法的引用的签名必须和上下文文类型匹配。

    构造引用

    对于现有的构造函数,可以利用它的类名称和关键字new来创建它的一个引用:ClassName::new。它的功能与指向静态方法的引用类似。例如,假设有一个构造函数没有参数。它适合Supplier的签名()->Apple。

    Supplier<Apple> c1 = Apple::new; Apple a1 = c1.get();

    这等价于

    Supplier<Apple> c1 = () -> new Apple(); Apple a1 = c1.get();

    如果构造函数签名是Apple(Integer weight),那么它就适合Function接口的签名,于是可以这样写

    Function<Integer, Apple> c2 = Apple::new; Apple a2 = c2.apply(110);

    等价于

    Function<Integer, Apple> c2 = (weight) -> new Apple(weight); Apple a2 = c2.apply(110);

    在下面的代码中,一个由Integer构成的List中每个元素都通过我们前面定义的类似的map方法传递给了Apple的构造函数,得到了一个具体不同重量苹果的List:

    List<Integer> weights = Arrays.asList(7, 3, 4, 10); List<Apple> apples = map(weights, Apple::new); public static List<Apple> map(List<Integer> list, Function<Integer, Apple> f){ List<Apple> result = new ArrayList<>(); for(Integer e: list){ result.add(f.apply(e)); } return result; }

    如果具有两个参数的构造函数Apple(String color,Integer weight),那么它就适合BiFunction接口签名,就可以这样写:

    BiFunction<String, Integer, Apple> c3 = Apple::new; Apple c3 = c3.apply("green", 110);

    等价于

    BiFunction<String, Integer, Apple> c3 = (color, weight) -> new Apple(color, weight); Apple c3 = c3.apply("green", 110);

    不将构造函数实例化却能够引用它,这个功能有一些有趣的应用。例如可以将构造函数映射到字符串。可以创建一个giveMeFruit方法,给他一个String和一个Integer,他就可以创建出不同重量的各种水果。

    static Map<String, Function<Integer, Fruit>> map = new HashMap<>(); static { map.put("apple", Apple::new); map.put("orange", Orange::new); // etc... } public static Fruit giveMeFruit(String fruit, Integer weight){ return map.get(fruit.toLowerCase()) .apply(weight); }

    如果还需要更多的参数,可以自己创建相应的函数式接口。

     

    最新回复(0)