Java8学习笔记之Lambda表达式

Lambda表达式是Java8中一项重要的新特性,其本质是一个”语法糖“,由编译器推断并帮你转换包装为常规的代码,因此你可以使用更少的代码来实现同样的功能。

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

lambda表达式允许你通过表达式来代替功能接口。它和方法一样,它提供了一个正常的参数列表和一个使用这些参数的主体(body可以是一个表达式或一个代码块)。此外它还增强了集合库。 Java8添加了2个对集合数据进行批量操作的包:java.util.function和java.util.stream包。

Lambda表达式的特点:

1)匿名:写得少而想得多。

2)函数:不像方法那样属于某个特定的类。但和方法一样,有参数列表、函数主体、返回类型,还可能有可以抛出的异常列表。

3)传递:可作为参数传递给方法或存储在变量中。

4)简洁:无需像匿名类那样写很多模板代码。

Lambda表达式的语法:

(parameters) -> expression

(parameters) ->{ statements; }

1、有效的Lambda表达式

() ->5  //无参数,返回值为5

x ->2* x  //接受一个参数(数字),返回其2倍的值

(x, y) -> x – y  //接受2个参数(数字),并返回它们的差

(int x,int y) -> x + y  //接收2个int型整数,返回它们的和

(String s) -> System.out.print(s)  //接收一个string对象,并在控制台打印,不返回任何值(类似void)

(String s) -> s.length()  ///接收一个String类型的参数并返回一个int。Lambda没有return语句,因为已经隐含了return;

(Apple a) -> a.getWeight() > 150  //接收一个Apple类型的参数并返回一个boolean(苹果的重量是否超过150克)

(int x, int y) -> {

    System.out.println("Result:");

    System.out.println(x+y);

}    //接收两个int类型的参数而没有返回值。Lambda表达式可以包含多行语句,这里是两行;

(Apple a1, Apple a2) -> a1.getWeight().compareTo(a2.getWeight())  //接收两个Apple类型的参数,返回一个int结果:比较两个Apple的重量;

2、函数式接口及函数描述符

函数式接口就是只定义一个抽象方法的接口,如下所示:

public interface Comparator<T> { int compare(T o1, T o2); }

public interface Runnable{ void run(); }

public interface ActionListener extends EventListener{ void actionPerformed(ActionEvent e); }

public interface Callable<V>{ V call(); }

public interface PrivilegedAction<V>{ V run(); }

注:Java8中接口还可以拥有默认方法(即在类没有对方法进行实现时, 其主体为方法提供默认实现的方法)。哪怕有很多默认方法,只要接口只定义了一个抽象方法,它仍然是一个函数式接口。

Lambda表达式允许你直接以内联的形式为函数式接口的抽象方法提供实现,并把整个表达式作为函数式接口的实例。用匿名内部类也可以完成同样的事情,只不过比较笨拙:需要提供一个实现,然后再直接内联将它实例化。代码如下:

Runnable r1 = () -> System.out.println("Hello World 1"); //使用Lambda

Runnable r2 = new Runnable(){ //使用匿名类

    public void run(){

        System.out.println("Hello World 2");

    }

};

public static void process(Runnable r){ r.run(); }

process(r1); //打印“Hello World 1”

process(r2); //打印“Hello World 2”

process(() -> System.out.println("Hello World 3")); //利用传递的Lambda打印“Hello World 3”

函数描述符

函数式接口的抽象方法的签名称为函数描述符。例如,Runnable接口可以看作是一个无参且不返回(void)的函数签名,因为它只有一个叫作run的抽象方法,这个方法什么也不接受,什么也不返回(void)。

() -> void 代表参数列表为空,且返回void的函数。

(Apple, Apple) -> int 代表接受两个Apple对象作为参数且返回int的函数。

3、使用函数式接口

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

1)Predicate

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

@FunctionalInterface

public interface Predicate<T>{boolean test(T t);}

使用示例:

public static <T> List<T> filter(List<T> list, Predicate<T> p) {

    List<T> results = new ArrayList<>();

    for(T s: list){

        if(p.test(s))  results.add(s);

    }

    return results;

}

Predicate<String> nonEmptyStringPredicate = (String s) -> !s.isEmpty();

List nonEmpty = filter(listOfStrings, nonEmptyStringPredicate);

2)Consumer

java.util.function.Consumer<T>定义了一个名叫accept的抽象方法,它接受泛型T的对象,没有返回值。如果需要访问类型T的对象,并对其执行某些操作,可以使用这个接口。比如,可以用它来创建一个forEach方法,接受一个整数列表,并对其中每个元素执行操作。

@FunctionalInterface

public interface Consumer<T>{void accept(T t);}

使用示例:

public static <T> void forEach(List<T> list, Consumer<T> c){

    for(T i: list){

        c.accept(i);

    }

}

forEach(Arrays.asList(1,2,3,4,5),(Integer i) -> System.out.println(i));  //遍历list并打印元素

3)Function

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

@FunctionalInterface

public interface Function<T, R>{R apply(T t);}

使用示例:创建一个map方法,将一个String列表映射到包含每个String长度的Integer列表

public static <T, R> List<R> map(List<T> list, Function<T, R> f) {

    List<R> result = new ArrayList<>();

    for(T s: list){

        result.add(f.apply(s));

    }

    return result;

}

List<Integer> list = map(Arrays.asList("lambdas","in","action"), (String s) -> s.length()); //输出[7, 2, 6]

原始类型特化

Java中的数据类型包含原始类型(如int、double、byte、char)和引用类型(如Byte、Integer、Object、List)。泛型(比如Consumer<T>中的T)只能绑定到引用类型(原因是由泛型内部的实现方式造成的)。将原始类型转换为对应的引用类型的机制叫装箱(boxing),反之将引用类型转换为对应的原始类型叫作拆箱(unboxing)。Java中的装箱和拆箱操作是自动完成的。装箱后的值本质上就是把原始类型包裹起来,并保存在堆里。因此,装箱后的值需要更多的内存,并需要额外的内存搜索来获取被包裹的原始值,在性能方面是要花费更高的代价。

Java 8为函数式接口带来了一个专门的版本,以便在输入和输出都是原始类型时避免自动装箱的操作。比如,使用IntPredicate就避免了对值1000进行装箱操作,但要是用Predicate<Integer>就会把参数1000装箱到一个Integer对象中:

public interface IntPredicate{ boolean test(int t); }

IntPredicate evenNumbers = (int i) -> i % 2 == 0;

evenNumbers.test(1000); //true(无装箱)

Predicate<Integer> oddNumbers = (Integer i) -> i % 2 == 1; 

oddNumbers.test(1000); //false(装箱)

一般针对专门的输入参数类型的函数式接口的名称都要加上对应的原始类型,比如DoublePredicate、IntConsumer、LongBinaryOperator、IntFunction等。Function接口还有针对输出参数类型的变种:ToIntFunction<T>、IntToDoubleFunction等。

以下是常用的函数式接口及其函数描述符:

常用的函数式接口


常用的函数式接口
Lambdas及函数式接口的例子 

4、类型检查、类型推断及限制

Lambda可以为函数式接口生成一个实例,但是Lambda表达式本身并不包含它在实现哪个函数式接口的信息。

1)类型检查

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

List<Apple> list = filter(inventory, (Apple a) -> a.getWeight() > 150);

类型检查过程分解:

1>找出filter方法的声明

2>要求它是Predicate<Apple>(目标类型)对象的第二个正式参数。

3>Predicate<Apple>是一个函数式接口,定义了一个test抽象方法。

4>test方法描述了一个函数描述符,它可以接受一个Apple,并返回一个boolean。

5>filter的任何实际参数都必须匹配这个要求。

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

同一个Lambda表达式可以与不同的函数式接口联系起来,只要它们的抽象方法签名能够兼容。

Callable<Integer> c = () -> 1;

PrivilegedAction<Integer> p = () -> 1;

Comparator<Apple> c1 = (Apple a1, Apple a2) -> a1.getWeight().compareTo(a2.getWeight());

ToIntBiFunction<Apple, Apple> c2 = (Apple a1, Apple a2) ->     a1.getWeight().compareTo(a2.getWeight());

BiFunction<Apple, Apple, Integer> c3 = (Apple a1, Apple a2) ->     a1.getWeight().compareTo(a2.getWeight());

3)类型推断

Java编译器会从上下文(目标类型)推断出用什么函数式接口来配合Lambda表达式,它也可以推断出适合Lambda的签名,因为函数描述符可以通过目标类型来得到。

List<Apple> greenApples = filter(list, a -> "green".equals(a.getColor())); //参数a没有显式类型

Comparator<Apple> c = (Apple a1, Apple a2) -> a1.getWeight().compareTo(a2.getWeight()); //没有类型推断

Comparator<Apple> c = (a1, a2) -> a1.getWeight().compareTo(a2.getWeight()); //有类型推断

注意:当Lambda仅有一个类型需要推断的参数时,参数名称两边的括号可以省略。

4)使用局部变量

Lambda表达式也允许使用自由变量(在外层作用域中定义的变量),就像匿名类一样,被称作捕获Lambda。

int n = 10;

Runnable r = () -> System.out.println(n); //表示Lambda捕获了n变量

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

int n = 10;

Runnable r = () -> System.out.println(n); //编译出错,Lambda引用的局部变量必须是最终的(final)或事实上最终的

n = 11;

对局部变量的限制原因:

第一:实例变量存在堆中,而局部变量则存在栈上。如果Lambda可以直接访问局部变量,而且Lambda是在一个线程中使用的,则使用Lambda的线程,可能会在分配该变量的线程将这个变量收回之后,去访问该变量。因此,Java在访问自由局部变量时,实际上是在访问它的副本,而不是访问原始变量。若局部变量仅赋值一次则相当于是final的。

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

闭包(closure):

闭包就是一个函数的实例,且它可以无限制地访问那个函数的非本地变量。

闭包可以作为参数传递给另一个函数,也可以访问和修改其作用域之外的变量。

Lambda和匿名类可以做类似于闭包的事情:

它们可以作为参数传递给方法,并且可以访问其作用域之外的变量。但有一个限制:它们不能修改定义Lambda的方法的局部变量的内容,这些变量必须是隐式最终的。可以认为Lambda是对值封闭,而不是对变量封闭。这种限制存在的原因在于局部变量保存在栈上,并且隐式表示它们仅限于其所在线程。如果允许捕获可改变的局部变量,就会引发造成线程不安全的可能性。实例变量可以,因为它们存在堆中,而堆是在线程之间共享的。

5、方法引用

方法引用可以重复使用现有的方法定义,并像Lambda一样传递它们。方法引用可以看作仅仅调用特定方法的Lambda的一种快捷写法。

基本思想:如果一个Lambda代表的只是“直接调用这个方法”,最好用名称来调用它,而不是去描述如何调用它。可以把方法引用看作是针对仅仅涉及单一方法的Lambda的语法糖,因为在表达同样的事情时代码更少了。

格式:目标引用放在分隔符::前,方法的名称放在后面。

例如: Apple::getWeight 就是引用了Apple类中定义的getWeight方法,等效于(Apple a) -> a.getWeight()。

inventory.sort((Apple a1, Apple a2) -> a1.getWeight().compareTo(a2.getWeight()));

可以改写为:

inventory.sort(comparing(Apple::getWeight));

注意:不需要括号,因为并没有实际调用这个方法。

方法引用主要有三类:

(1) 指向静态方法的方法引用(如Integer.parseInt方法,写作Integer::parseInt)。

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

(3) 指向现有对象的实例方法的方法引用(假设有一个局部变量expensiveTransaction用于存放Transaction类型的对象,它支持实例方法getValue,写成expensiveTransaction::getValue)。

示例:对一个字符串的List排序,忽略大小写。

List<String> list = Arrays.asList("a","b","A","B");

list.sort((s1, s2) -> s1.compareToIgnoreCase(s2));

可以改成:

list.sort(String::compareToIgnoreCase);

构造函数引用:

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

如果构造函数没有参数,适合Supplier的签名:

Supplier<Apple> c1 = Apple::new; //构造函数引用指向默认的Apple()构造函数

Apple a1 = c1.get(); //调用Supplier的get方法将产生一个新的Apple

等价于:

Supplier<Apple> c1 = () -> new Apple(); //利用默认构造函数创建Apple的Lambda表达式

Apple a1 = c1.get();

如果构造函数的签名是Apple(Integer weight),那么它适合Function接口的签名:

Function<Integer, Apple> c2 = Apple::new; //指向Apple(Integer weight) 的构造函数引用

Apple a2 = c2.apply(100); //调用该Function函数的apply方法,并给出要求的重量,将产生一个Apple

等价于:

Function<Integer, Apple> c2 = (weight) -> new Apple(weight); //用要求的重量创建一个Apple的Lambda表达式

Apple a2 = c2.apply(100);

完整示例:筛选指定重量集合中的苹果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", 100);

等价于:

BiFunction<String, Integer, Apple> c3 = (color, weight) -> new Apple(color, weight); 

 Apple c3 = c3.apply("green", 100);

如何构建具有三个参数的构造函数,如Color(int, int, int),使用构造函数引用呢?

public interface TriFunction<T, U, V, R>{

    R apply(T t, U u, V v);

}

可以这样使用构造函数引用:

TriFunction<Integer, Integer, Integer, Color> colorFactory = Color::new;

6、Lambda和方法引用实战

如何实现对库存进行排序,然后比较苹果的重量?

方式1:使用传递代码

Java 8的API已经提供了一个List可用的sort方法;

void sort(Comparator<? super E> c)

public class AppleComparator implements Comparator<Apple> {

    public int compare(Apple a1, Apple a2){

        return a1.getWeight().compareTo(a2.getWeight());

    }

}

inventory.sort(new AppleComparator());

方式2:使用匿名类

inventory.sort(new Comparator<Apple>() {

    public int compare(Apple a1, Apple a2){

        return a1.getWeight().compareTo(a2.getWeight());

    }

});

方式3:使用Lambda表达式

inventory.sort((Apple a1, Apple a2)  -> a1.getWeight().compareTo(a2.getWeight()));

inventory.sort((a1, a2) -> a1.getWeight().compareTo(a2.getWeight()));

Comparator<Apple> c = Comparator.comparing((Apple a) -> a.getWeight());

inventory.sort(Comparator.comparing((a) -> a.getWeight()));

方式4:使用方法引用

inventory.sort(Comparator.comparing(Apple::getWeight));

7、复合Lambda表达式的有用方法

1)比较器复合

Comparator<Apple> c = Comparator.comparing(Apple::getWeight);

inventory.sort(comparing(Apple::getWeight).reversed()); //按重量递减排序

inventory.sort(comparing(Apple::getWeight).reversed()  //按重量递减排序

    .thenComparing(Apple::getCountry)); //两个苹果一样重时,进一步按国家排序

2)谓词复合

谓词接口包括三个方法:negate、and和or,可以重用已有的Predicate来创建更复杂的谓词。

Predicate<Apple> notRedApple = redApple.negate(); //产生现有Predicate对象redApple的非

Predicate<Apple> redAndHeavyApple = 

    redApple.and(a -> a.getWeight() > 150); //链接两个谓词来生成另一个Predicate对象

Predicate<Apple> redAndHeavyAppleOrGreen = 

    redApple.and(a -> a.getWeight() > 150)

        .or(a -> "green".equals(a.getColor())); //链接Predicate的方法来构造更复杂Predicate对象

执行优先级顺序:

and和or方法是按照在表达式链中的位置,从左向右确定优先级的。a.or(b).and(c)可以看作(a || b) && c。

3)函数复合

Function接口包含andThen和compose两个默认方法,它们都会返回Function的一个实例。

andThen方法会返回一个函数,它先对输入应用一个给定函数,再对输出应用另一个函数。

Function<Integer, Integer> f = x -> x + 1;

Function<Integer, Integer> g = x -> x * 2;

Function<Integer, Integer> h = f.andThen(g); //数学上会写作g(f(x))或(g o f)(x)

int result = h.apply(1); //返回4

compose方法先把给定的函数用作compose的参数里面给的那个函数,然后再把函数本身用于结果。

Function<Integer, Integer> f = x -> x + 1;

Function<Integer, Integer> g = x -> x * 2;

Function<Integer, Integer> h = f.compose(g);  //数学上会写作f(g(x))或(f o g)(x)

int result = h.apply(1); //返回3

andThen和compose的区别

总结:

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

2)Lambda表达式让你可以简洁地传递代码。

3)函数式接口就是仅仅声明了一个抽象方法的接口。

4)只有在接受函数式接口的地方才可以使用Lambda表达式。

5)Lambda表达式允许你直接内联,为函数式接口的抽象方法提供实现,并且将整个表达式作为函数式接口的一个实例。

6)Java 8自带一些常用的函数式接口,放在java.util.function包里,包括Predicate <T>、Function<T,R>、Supplier<T>、Consumer<T>和BinaryOperator<T>。

7)为了避免装箱操作,对Predicate<T>和Function<T, R>等通用函数式接口的原始类型特化:IntPredicate、IntToLongFunction等。

8)环绕执行模式可以配合Lambda提高灵活性和可重用性。

9)Lambda表达式所需要代表的类型称为目标类型。

10)方法引用让你重复使用现有的方法实现并直接传递它们。

11)Comparator、Predicate和Function等函数式接口都有可以用来结合Lambda表达式的默认方法。

                                                                                        --示例摘自《Java8实战》

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 216,039评论 6 498
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,223评论 3 392
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 161,916评论 0 351
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,009评论 1 291
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,030评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,011评论 1 295
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,934评论 3 416
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,754评论 0 271
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,202评论 1 309
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,433评论 2 331
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,590评论 1 346
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,321评论 5 342
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,917评论 3 325
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,568评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,738评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,583评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,482评论 2 352

推荐阅读更多精彩内容