JAVA8之lambda表达式详解

一.问题

1.什么是lambda表达式?
2.lambda表达式用来干什么的?
3.lambda表达式的优缺点?
4.lambda表达式的使用场景?
5.lambda只是一个语法糖吗?

二.概念

lambda表达式是JAVA8中提供的一种新的特性,它支持JAVA也能进行简单的“函数式编程”。
它是一个匿名函数,Lambda表达式基于数学中的λ演算得名,直接对应于其中的lambda抽象(lambda abstraction),是一个匿名函数,即没有函数名的函数。

1 为什么需要 Stream

Stream 作为 Java 8 的一大亮点,它与 java.io 包里的 InputStream 和 OutputStream 是完全不同的概念。它也不同于 StAX 对 XML 解析的 Stream,也不是 Amazon Kinesis 对大数据实时处理的 Stream。Java 8 中的 Stream 是对集合(Collection)对象功能的增强,它专注于对集合对象进行各种非常便利、高效的聚合操作(aggregate operation),或者大批量数据操作 (bulk data operation)。Stream API 借助于同样新出现的 Lambda 表达式,极大的提高编程效率和程序可读性。同时它提供串行和并行两种模式进行汇聚操作,并发模式能够充分利用多核处理器的优势,使用 fork/join 并行方式来拆分任务和加速处理过程。通常编写并行代码很难而且容易出错, 但使用 Stream API 无需编写一行多线程的代码,就可以很方便地写出高性能的并发程序。所以说,Java 8 中首次出现的 java.util.stream 是一个函数式语言+多核时代综合影响的产物。

2 什么是聚合操作

3 什么是lambda表达式?

4 什么是流

Stream 不是集合元素,它不是数据结构并不保存数据,它是有关算法和计算的,它更像一个高级版本的 Iterator。原始版本的 Iterator,用户只能显式地一个一个遍历元素并对其执行某些操作;高级版本的 Stream,用户只要给出需要对其包含的元素执行什么操作,比如 “过滤掉长度大于 10 的字符串”、“获取每个字符串的首字母”等,Stream 会隐式地在内部进行遍历,做出相应的数据转换。

 

Stream 就如同一个迭代器(Iterator),单向,不可往复,数据只能遍历一次,遍历过一次后即用尽了,就好比流水从面前流过,一去不复返。

 

而和迭代器又不同的是,Stream 可以并行化操作,迭代器只能命令式地、串行化操作。顾名思义,当使用串行方式去遍历时,每个 item 读完后再读下一个 item。而使用并行去遍历时,数据会被分成多个段,其中每一个都在不同的线程中处理,然后将结果一起输出。Stream 的并行操作依赖于 Java7 中引入的 Fork/Join 框架(JSR166y)来拆分任务和加速处理过程。Java 的并行 API 演变历程基本如下:

 

1.0-1.4 中的 java.lang.Thread

5.0 中的 java.util.concurrent

6.0 中的 Phasers 等

7.0 中的 Fork/Join 框架

8.0 中的 Lambda

Stream 的另外一大特点是,数据源本身可以是无限的。

5 流的构成

三.示例

1.使用lambda表达式实现Runnable

package com.lambda;

/**
 * 使用lambda表达式替换Runnable匿名内部类
 * @author MingChenchen
 *
 */
public class RunableTest {
    /**
     * 普通的Runnable
     */
    public static void runSomeThing(){

        Runnable runnable = new Runnable() {

            @Override
            public void run() {
                System.out.println("I am running");
            }
        };
        new Thread(runnable).start();
    }

    /**
     * 使用lambda后的
     */
    public static void runSomeThingByLambda(){
        new Thread(() -> System.out.println("I am running")).start();
    }

    public static void main(String[] args) {
        runSomeThing();
//      runSomeThingByLambda();
    }
}

上述代码中:

() -> System.out.println("I am running")就是一个lambda表达式,

可以看出,它是替代了new Runnable(){}这个匿名内部类。

2.使用lambda表达式实现Comparator

package com.lambda;

import java.util.Arrays;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;

public class SortList {
    //给入一个List
    private static List<String> list = 
            Arrays.asList("my","name","is","uber","and","uc");

    /**
     * 对一个String的list进行排序 - 使用老方法
     */
    public static void oldSort(){
        //排序
        Collections.sort(list,new Comparator<String>() {
            //使用新的排序规则 根据第二个字符进行逆序排
            @Override
            public int compare(String a,String b){
                if (a.charAt(1) <= b.charAt(1)) {
                    return 1;
                }else{
                    return -1;
                }
            }
        });
    }

    /**
     * 新的排序方法 - 使用lambda表达式实现
     */
    public static void newSort(){
        //lambda会自动推断出 a,b 的类型
        Collections.sort(list, (a, b) -> a.charAt(1) < b.charAt(1) ? 1:-1);
    }

    public static void main(String[] args) {
//      oldSort();
        newSort();
    }
}

四.来由

好了,通过上述的几个例子,大家差不多也能明白了lambda是用来干什么以及好处了。

显而易见的,好处就是代码量大大减少了!程序逻辑也很清晰明了。

它的用处浅显来说就是替代“内部匿名类”、可以对集合或者数组进行循环操作。

 

以前:

面向对象式编程就应该纯粹的面向对象,于是经常看到这样的写法:

如果你想写一个方法,那么就必须把它放到一个类里面,然后new出来对象,对象调用这个方法。

匿名类型最大的问题就在于其冗余的语法。

有人戏称匿名类型导致了“高度问题”(height problem):

比如大多匿名内部类的多行代码中仅有一行在做实际工作。

 

因此JAVA8中就提供了这种“函数式编程”的方法 —— lambda表达式,供我们来更加简明扼要的实现内部匿名类的功能。

五.什么时候可以使用它?

先说一个名词的概念

 

函数式接口:Functional Interface.

定义的一个接口,接口里面必须 有且只有一个抽象方法 ,这样的接口就成为函数式接口。

在可以使用lambda表达式的地方,方法声明时必须包含一个函数式的接口。

(JAVA8的接口可以有多个default方法)

 

任何函数式接口都可以使用lambda表达式替换。

例如:ActionListener、Comparator、Runnable

 

lambda表达式只能出现在目标类型为函数式接口的上下文中。

 

注意:

此处是只能!!!

意味着如果我们提供的这个接口包含一个以上的Abstract Method,那么使用lambda表达式则会报错。

这点已经验证过了。

 

场景:

这种场景其实很常见:

你在某处就真的只需要一个能做一件事情的函数而已,连它叫什么名字都无关紧要。

Lambda 表达式就可以用来做这件事。

六.写法、规则

基本语法:
(parameters) -> expression 或 (parameters) ->{ statements; }
即: 参数 -> 带返回值的表达式/无返回值的陈述

七.几个特性

1. 类型推导

编译器负责推导lambda表达式的类型。它利用lambda表达式所在上下文所期待的类型进行推导,

这个被期待的类型被称为目标类型。就是说我们传入的参数可以无需写类型了!

 

2.变量捕获

在Java SE 7中,编译器对内部类中引用的外部变量(即捕获的变量)要求非常严格:

如果捕获的变量没有被声明为final就会产生一个编译错误。

我们现在放宽了这个限制——对于lambda表达式和内部类,

我们允许在其中捕获那些符合有效只读(Effectively final)的局部变量。

 

简单的说,如果一个局部变量在初始化后从未被修改过,那么它就符合有效只读的要求,

换句话说,加上final后也不会导致编译错误的局部变量就是有效只读变量。

 

注意:此处和final关键字一样,指的是引用不可改!(感觉没多大意义,还不是用的final)

3.方法引用

如果我们想要调用的方法拥有一个名字,我们就可以通过它的名字直接调用它。

Comparator byName = Comparator.comparing(Person::getName);

此处无需再传入参数,lambda会自动装配成Person类型进来然后执行getName()方法,而后返回getName()的String

 

方法引用有很多种,它们的语法如下:

 

静态方法引用:ClassName::methodName

实例上的实例方法引用:instanceReference::methodName

超类上的实例方法引用:super::methodName

类型上的实例方法引用:ClassName::methodName

构造方法引用:Class::new

数组构造方法引用:TypeName[]::new

 

4.JAVA提供给我们的SAM接口

Java SE 8中增加了一个新的包:java.util.function,它里面包含了常用的函数式接口,例如:

八更多的例子

1 forEach+Lambda 表达式遍历Map和List

一、遍历Map
============Java8之前的方式==========
Map<String, Integer> items = new HashMap<>();
items.put("A", 10);
items.put("B", 20);
items.put("C", 30);
for (Map.Entry<String, Integer> entry : items.entrySet()) {
    System.out.println("Item : " + entry.getKey() + " Count : " + entry.getValue());
}
============forEach + Lambda表达式==========
Map<String, Integer> items = new HashMap<>();
items.put("A", 10);
items.put("B", 20);
items.put("C", 30);
items.forEach((k,v)->System.out.println("Item : " + k + " Count : " + v));
items.forEach((k,v)->{
    System.out.println("Item : " + k + " Count : " + v);
    if("E".equals(k)){
        System.out.println("Hello E");
    }
});
二遍历List:
============Java8之前的方式==========

List<String> items = new ArrayList<>();
items.add("A");
items.add("B");
for(String item : items){
    System.out.println(item);
}
============forEach + Lambda表达式==========
List<String> items = new ArrayList<>();
items.add("A");
items.add("B");
items.add("C");
//输出:A,B,C,D,E
items.forEach(item->System.out.println(item));
//输出 : C
items.forEach(item->{
    if("C".equals(item)){
        System.out.println(item);
    }
});

2 使用方法引用( ClassName::Method,无括号)

package com.lambda.usebean;

/**
 * 实体类Person
 * @author MingChenchen
 *
 */
public class Person {
    private String name;      //姓名
    private String location;  //地址

    public String getName() {
        return name;
    }
    public void setName(String name) {
        this.name = name;
    }
    public String getLocation() {
        return location;
    }
    public void setLocation(String location) {
        this.location = location;
    }

    @Override
    public String toString() {
        // TODO Auto-generated method stub
        return "Person:" + name + "," + location;
    }
}

//使用String默认的排序规则,比较的是Person的name字段
Comparator<Person> byName = Comparator.comparing(p -> p.getName());
//不用写传入参数,传入的用Person来声明
Comparator<Person> byName2 = Comparator.comparing(Person::getName);

3 使用lambda表达式完成for-each循环操作

//原本的for-each循环写做法
List list = Arrays.asList(....);
for (int i = 0; i < list.size(); i++) {
    System.out.println(list.get(i));
}

//使用lambda表达式后的写法
list.forEach(str -> System.out.println(str));

list.forEach()是JAVA8的新方法,支持函数式编程,此处使用的参数就是JAVA提供给我们的函数式接口:Consumer< T>

interface List<E> extends Collection<E>
interface Collection<E> extends Iterable<E>

public interface Iterable<T> {  
    default void forEach(Consumer<? super T> action) {
        Objects.requireNonNull(action);
        for (T t : this) {
            action.accept(t);
        }
    }
}

4 一个完整的例子

//普通写法:
List<Person> people = ...
Collections.sort(people, new Comparator<Person>() {
  public int compare(Person x, Person y) {
    return x.getLastName().compareTo(y.getLastName());
  }
})

//使用lambda表达式写法:
people.sort(comparing(Person::getLastName));

化简流程:
第一步:去掉冗余的匿名类
Collections.sort(people,(Personx, Person y) -> x.getLastName().compareTo(y.getLastName()));

第二步:使用Comparator里的comparing方法
Collections.sort(people, Comparator.comparing((Person p) -> p.getLastName()));

第三步:类型推导和静态导入
Collections.sort(people, comparing(p -> p.getLastName()));

第四步:方法引用
Collections.sort(people, comparing(Person::getLastName));

第五步:使用List本身的sort更优
people.sort(comparing(Person::getLastName));;

九.优缺点

优点:

1.极大的简化代码。去除了很多无用的Java代码,使得代码更为简洁明了。

2.比匿名内部类更加高效(不确定)。编译器会生成专门的lambda方法,可以使用javap -p查看编译过后的代码

 

缺点:

1.可读性差。在代码简洁的情况下,另一方面又让大多程序员很难读懂。因为很少程序员接触使用它。

(不过这个缺点不是本身缺点,而是源于程序员较少使用)

十.它是一个语法糖吗?

lambda表达式不算是一个语法糖。

语法糖就是说只是帮助我们程序员轻松的少写一些代码,之后编译器帮我们把那部分代码生成出来。

但是从编译过后的结果来说,并不是自动帮我们生成一个内部匿名类,而是生成了一个lambda$X方法。

 

第二个就是lambda其实表达的是目前流行的“函数式编程”这种思维。区别于我们面向对象的思维方法。

这点我认为很有意义,即我们要从各种思维来对待事情。

 

但是论坛基本都认为这是一个语法糖,也没错。毕竟它提倡的只是一种思想,而且jdk底层为lambda生成了新的高效的代码这个事儿并不确定。

十一 stream概要

stream的方法里面大多都使用了lambda表达式

一.什么是stream?

官方解释:

A sequence of elements supporting sequential and parallel aggregate operations.

简单来讲,stream就是JAVA8提供给我们的对于元素集合统一、快速、并行操作的一种方式。
它能充分运用多核的优势,以及配合lambda表达式、链式结构对集合等进行许多有用的操作。

概念:

stream:可以支持顺序和并行对元素操作的元素集合。

作用:


提供了一种操作大数据接口,让数据操作更容易和更快
使用stream,我们能够对collection的元素进行过滤、映射、排序、去重等许多操作。

中间方法和终点方法:

它具有过滤、映射以及减少遍历数等方法,这些方法分两种:中间方法和终端方法,

“流”抽象天生就该是持续的,中间方法永远返回的是Stream,因此如果我们要获取最终结果的话,

必须使用终点操作才能收集流产生的最终结果。区分这两个方法是看他的返回值,

如果是Stream则是中间方法,否则是终点方法

二.如何使用stream?

1.通过Stream接口的静态工厂方法(注意:Java8里接口可以带静态方法);

2.通过Collection接口的默认方法(默认方法:Default method,也是Java8中的一个新特性,就是接口中的一个带有实现的方法)–stream(),把一个Collection对象转换成Stream

一般情况下,我们都使用Collection接口的 .stream()方法得到stream.

三.常见的几个中间方法

中间方法即是一些列对元素进行的操作。譬如过滤、去重、截断等

1 Filter(过滤)

2.Map(对元素进行操作)

3.limit(截断)

对一个Stream进行截断操作,获取其前N个元素,如果原Stream中包含的元素个数小于N,那就获取其所有的元素

4.distinct(去重)

对于Stream中包含的元素进行去重操作(去重逻辑依赖元素的equals方法),新生成的Stream中没有重复的元素

5Collectors函数

6 统计函数

7forEach

四.常用的终点方法

通过中间方法,我们对stream的元素进行了统一的操作,但是中间方法得到还是一个stream。要想把它转换为新的集合、或者是统计等。我们需要使用终点方法。

1 count(统计)

count方法是一个流的终点方法,可使流的结果最终统计,返回int,比如我们计算一下满足18岁的总人数

2Collect(收集流的结果)

collect方法也是一个流的终点方法,可收集最终的结果

五.顺序流和并行流

每个Stream都有两种模式:顺序执行和并行执行

DEMO

package com.lambda.stream;

import java.util.stream.IntStream;

public class TestPerformance {
    public static void main(String[] args) {
        long t0 = System.nanoTime();

        //初始化一个范围100万整数流,求能被2整除的数字,toArray()是终点方法

        int a[]=IntStream.range(0, 1_000_000).filter(p -> p % 2==0).toArray();

        long t1 = System.nanoTime();

        //和上面功能一样,这里是用并行流来计算

        int b[]=IntStream.range(0, 1_000_000).parallel().filter(p -> p % 2==0).toArray();

        long t2 = System.nanoTime();

        //我本机的结果是serial: 0.06s, parallel 0.02s,证明并行流确实比顺序流快

        System.out.printf("serial: %.2fs, parallel %.2fs%n", (t1 - t0) * 1e-9, (t2 - t1) * 1e-9);

    }
}

 

参考链接

https://blog.csdn.net/jinzhencs/article/details/50748202

http://zh.lucida.me/blog/java-8-lambdas-insideout-language-features/

http://ifeve.com/lambda/

http://ifeve.com/stream/

https://blog.csdn.net/a13662080711/article/details/84928181