深入lambda表达式详细详细

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,DoublePredicate
Consumer<T>T->voidIntConsumer,LongConsumer,DoubleConsumer
Function<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,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>

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

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

 

  • 2
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值