Lambda表达式你会用吗?

函数式编程

在正式学习Lambda以前,咱们先来了解一下什么是函数式编程java

咱们先看看什么是函数。函数是一种最基本的任务,一个大型程序就是一个顶层函数调用若干底层函数,这些被调用的函数又能够调用其余函数,即大任务被一层层拆解并执行。因此函数就是面向过程的程序设计的基本单元。git

Java不支持单独定义函数,但能够把静态方法视为独立的函数,把实例方法视为自带this参数的函数。而函数式编程(请注意多了一个“式”字)——Functional Programming,虽然也能够归结到面向过程的程序设计,但其思想更接近数学计算。程序员

咱们首先要搞明白计算机(Computer)和计算(Compute)的概念。在计算机的层次上,CPU执行的是加减乘除的指令代码,以及各类条件判断和跳转指令,因此,汇编语言是最贴近计算机的语言。而计算则指数学意义上的计算,越是抽象的计算,离计算机硬件越远。对应到编程语言,就是越低级的语言,越贴近计算机,抽象程度低,执行效率高,好比C语言;越高级的语言,越贴近计算,抽象程度高,执行效率低,好比Lisp语言。github

函数式编程就是一种抽象程度很高的编程范式,纯粹的函数式编程语言编写的函数没有变量,所以,任意一个函数,只要输入是肯定的,输出就是肯定的,这种纯函数咱们称之为没有反作用。而容许使用变量的程序设计语言,因为函数内部的变量状态不肯定,一样的输入,可能获得不一样的输出,所以,这种函数是有反作用的。express

函数式编程的一个特色就是,容许把函数自己做为参数传入另外一个函数,还容许返回一个函数!编程

函数式编程最先是数学家阿隆佐·邱奇研究的一套函数变换逻辑,又称Lambda Calculus(λ-Calculus),因此也常常把函数式编程称为Lambda计算。markdown

Java平台从Java 8开始,支持函数式编程。多线程

Lambda初体验

先从一个例子开始,让咱们来看一下Lambda能够用在什么地方。闭包

例一:建立线程

常见建立线程的方法(JDK1.8之前)app

//JDK1.7经过匿名内部类的方式建立线程
Thread thread = new Thread(new Runnable() {
    @Override
    public void run() { //实现run方法
        System.out.println("Thread Run...");
    }
});

thread.start();

经过匿名内部类的方式建立线程,省去了取名字的烦恼,可是还能不能再简化一些呢?

JDK1.8 Lambda表达式写法

Thread thread = new Thread(() -> System.out.println("Thread Run")); //一行搞定

thread.start();

咱们能够看到Lambda一行代码就完成了线程的建立,简直不要太方便。(至于Lambda表达式的语法,咱们下面章节再详细介绍)

若是你的逻辑不止一行代码,那么你还能够这么写

Thread thread = new Thread(() -> {
    System.out.println("Thread Run");
    System.out.println("Hello");
});

thread.start();

{}将代码块包裹起来

例二:自定义比较器

咱们先来看一下JDK1.7是如何实现自定义比较器的

List<String> list = Arrays.asList("Hi", "Life", "Hello~", "World");
Collections.sort(list, new Comparator<String>(){// 接口名
    @Override
    public int compare(String s1, String s2){// 方法名
        if(s1 == null)
            return -1;
        if(s2 == null)
            return 1;
        return s1.length()-s2.length();
    }
});

//输出排序好的List
for (String s : list) {
    System.out.println(s);
}

这里的sort方法传入了两个参数,一个是待排序的list,一个是比较器(排序规则),这里也是经过匿名内部类的方式实现的比较器。

下面咱们来看一下Lambda表达式如何实现比较器?

List<String> list = Arrays.asList("Hi", "Life", "Hello~", "World");
Collections.sort(list, (s1, s2) ->{// 省略了参数的类型,编译器会根据上下文信息自动推断出类型
    if(s1 == null)
        return -1;
    if(s2 == null)
        return 1;
    return s1.length()-s2.length();
});

//输出排序好的List
for (String s : list) {
    System.out.println(s);
}

咱们能够看到,Lambda表达式和匿名内部类的做用相同,可是省略了不少代码,能够大大加快开发速度

Lambda表达式语法

Lambda 表达式,也可称为闭包,它是推进 Java 8 发布的最重要新特性。Lambda 容许把函数做为一个方法的参数(函数做为参数传递进方法中)。

使用 Lambda 表达式可使代码变的更加简洁紧凑。上一章节咱们已经见识到了Lambda表达式的优势,那么Lambda表达式到底该怎么写呢?

语法

lambda 表达式的语法格式以下:

(parameters) -> expression   //一行代码
  或
(parameters) ->{ statements; }  //多行代码

lambda表达式的重要特征:

  • 可选类型声明:不须要声明参数类型,编译器能够统一识别参数值。
  • 可选的参数圆括号:一个参数无需定义圆括号,但多个参数须要定义圆括号。
  • 可选的大括号:若是主体包含了一个语句,就不须要使用大括号。
  • 可选的返回关键字:若是主体只有一个表达式返回值则编译器会自动返回值,大括号须要指定明表达式返回了一个数值。
// 1. 不须要参数,返回值为 5  
() -> 5  
  
// 2. 接收一个参数(数字类型),返回其2倍的值  
x -> 2 * x  
  
// 3. 接受2个参数(数字),并返回他们的差值  
(x, y) -> x – y  
  
// 4. 接收2个int型整数,返回他们的和  
(int x, int y) -> x + y  
  
// 5. 接受一个 string 对象,并在控制台打印,不返回任何值(看起来像是返回void)  
(String s) -> System.out.print(s)

函数接口

上面几个章节给你们介绍Lambda表达式的基本使用,那么是否是在任意地方均可以使用Lambda表达式呢?

其实Lambda表达式使用是有限制的。也许你已经想到了,可以使用Lambda的依据是必须有相应的函数接口。(函数接口,是指内部只有一个抽象方法的接口)

自定义函数接口

自定义函数接口很容易,只须要编写一个只有一个抽象方法的接口便可。

// 自定义函数接口
@FunctionalInterface
public interface PersonInterface<T>{
    void accept(T t);
}

上面代码中的@FunctionalInterface是可选的,但加上该标注编译器会帮你检查接口是否符合函数接口规范。就像加入@Override标注会检查是否重载了函数同样。

那么根据上面的自定义函数式接口,咱们就能够写出以下的Lambda表达式。

PersonInterface p = str -> System.out.println(str);

Lambda和匿名内部类

通过上面几部分的介绍,相信你们对Lambda表达式已经有了初步认识,学会了如何使用。但想必你们心中始终有一个疑问,Lambda表达式彷佛只是为了简化匿名内部类的写法,其余也没啥区别了。这看起来仅仅经过语法糖在编译阶段把全部的Lambda表达式替换成匿名内部类就能够了,事实真的如此吗?

public class Main {

    public static void main(String[] args) {
        new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println("Anonymous class");
            }
        }).start();
    }
}

匿名内部类也是一个类,只不过咱们不须要显示为他定义名称,可是编译器会自动为匿名内部类命名。Main编辑后的文件以下图

image-20201217164035157

咱们能够看到共有两个class文件,一个是Main.class,而另外一个则是编辑器为咱们命名的内部类。

下面咱们来看一下Lambda表达式会产生几个class文件

public class Main {
    public static void main(String[] args) {
        new Thread(() -> System.out.println("Lambda")).start();
    }
}

image-20201217164610350

Lambda表达式经过invokedynamic指令实现,书写Lambda表达式不会产生新的类

Lambda在集合中的运用

既然Lambda表达式这么方便,那么哪些地方可使用Lambda表达式呢?

咱们先从最熟悉的Java集合框架(Java Collections Framework, JCF)开始提及。

为引入Lambda表达式,Java8新增了java.util.funcion包,里面包含经常使用的函数接口,这是Lambda表达式的基础,Java集合框架也新增部分接口,以便与Lambda表达式对接。

首先回顾一下Java集合框架的接口继承结构:

JCF_Collection_Interfaces

上图中绿色标注的接口类,表示在Java8中加入了新的接口方法,固然因为继承关系,他们相应的子类也都会继承这些新方法。下表详细列举了这些方法。

接口名 Java8新加入的方法
Collection removeIf() spliterator() stream() parallelStream() forEach()
List replaceAll() sort()
Map getOrDefault() forEach() replaceAll() putIfAbsent() remove() replace() computeIfAbsent() computeIfPresent() compute() merge()

这些新加入的方法大部分要用到java.util.function包下的接口,这意味着这些方法大部分都跟Lambda表达式相关。咱们将逐一学习这些方法。

Collection中的新方法

如上所示,接口CollectionList新加入了一些方法,咱们以是List的子类ArrayList为例来讲明。了解Java7ArrayList实现原理,将有助于理解下文。

forEach()

该方法的签名为void forEach(Consumer<? super E> action),做用是对容器中的每一个元素执行action指定的动做,其中Consumer是个函数接口,里面只有一个待实现方法void accept(T t)(后面咱们会看到,这个方法叫什么根本不重要,你甚至不须要记忆它的名字)。

需求:假设有一个字符串列表,须要打印出其中全部长度大于3的字符串.

Java7及之前咱们能够用加强的for循环实现:

// 使用曾强for循环迭代
ArrayList<String> list = new ArrayList<>(Arrays.asList("I", "love", "you", "too"));
for(String str : list){
    if(str.length()>3)
        System.out.println(str);
}

如今使用forEach()方法结合匿名内部类,能够这样实现:

// 使用forEach()结合匿名内部类迭代
ArrayList<String> list = new ArrayList<>(Arrays.asList("I", "love", "you", "too"));
list.forEach(new Consumer<String>(){
    @Override
    public void accept(String str){
        if(str.length()>3)
            System.out.println(str);
    }
});

上述代码调用forEach()方法,并使用匿名内部类实现Comsumer接口。到目前为止咱们没看到这种设计有什么好处,可是不要忘记Lambda表达式,使用Lambda表达式实现以下:

// 使用forEach()结合Lambda表达式迭代
ArrayList<String> list = new ArrayList<>(Arrays.asList("I", "love", "you", "too"));
list.forEach( str -> {
        if(str.length()>3)
            System.out.println(str);
    });

上述代码给forEach()方法传入一个Lambda表达式,咱们不须要知道accept()方法,也不须要知道Consumer接口,类型推导帮咱们作了一切。

removeIf()

该方法签名为boolean removeIf(Predicate<? super E> filter),做用是删除容器中全部知足filter指定条件的元素,其中Predicate是一个函数接口,里面只有一个待实现方法boolean test(T t),一样的这个方法的名字根本不重要,由于用的时候不须要书写这个名字。

需求:假设有一个字符串列表,须要删除其中全部长度大于3的字符串。

咱们知道若是须要在迭代过程冲对容器进行删除操做必须使用迭代器,不然会抛出ConcurrentModificationException,因此上述任务传统的写法是:

// 使用迭代器删除列表元素
ArrayList<String> list = new ArrayList<>(Arrays.asList("I", "love", "you", "too"));
Iterator<String> it = list.iterator();
while(it.hasNext()){
    if(it.next().length()>3) // 删除长度大于3的元素
        it.remove();
}

如今使用removeIf()方法结合匿名内部类,咱们但是这样实现:

// 使用removeIf()结合匿名名内部类实现
ArrayList<String> list = new ArrayList<>(Arrays.asList("I", "love", "you", "too"));
list.removeIf(new Predicate<String>(){ // 删除长度大于3的元素
    @Override
    public boolean test(String str){
        return str.length()>3;
    }
});

上述代码使用removeIf()方法,并使用匿名内部类实现Precicate接口。相信你已经想到用Lambda表达式该怎么写了:

// 使用removeIf()结合Lambda表达式实现
ArrayList<String> list = new ArrayList<>(Arrays.asList("I", "love", "you", "too"));
list.removeIf(str -> str.length()>3); // 删除长度大于3的元素

使用Lambda表达式不须要记忆Predicate接口名,也不须要记忆test()方法名,只须要知道此处须要一个返回布尔类型的Lambda表达式就好了。

replaceAll()

该方法签名为void replaceAll(UnaryOperator<E> operator),做用是对每一个元素执行operator指定的操做,并用操做结果来替换原来的元素。其中UnaryOperator是一个函数接口,里面只有一个待实现函数T apply(T t)

需求:假设有一个字符串列表,将其中全部长度大于3的元素转换成大写,其他元素不变。

Java7及以前彷佛没有优雅的办法:

// 使用下标实现元素替换
ArrayList<String> list = new ArrayList<>(Arrays.asList("I", "love", "you", "too"));
for(int i=0; i<list.size(); i++){
    String str = list.get(i);
    if(str.length()>3)
        list.set(i, str.toUpperCase());
}

使用replaceAll()方法结合匿名内部类能够实现以下:

// 使用匿名内部类实现
ArrayList<String> list = new ArrayList<>(Arrays.asList("I", "love", "you", "too"));
list.replaceAll(new UnaryOperator<String>(){
    @Override
    public String apply(String str){
        if(str.length()>3)
            return str.toUpperCase();
        return str;
    }
});

上述代码调用replaceAll()方法,并使用匿名内部类实现UnaryOperator接口。咱们知道能够用更为简洁的Lambda表达式实现:

// 使用Lambda表达式实现
ArrayList<String> list = new ArrayList<>(Arrays.asList("I", "love", "you", "too"));
list.replaceAll(str -> {
    if(str.length()>3)
        return str.toUpperCase();
    return str;
});

sort()

该方法定义在List接口中,方法签名为void sort(Comparator<? super E> c),该方法根据c指定的比较规则对容器元素进行排序Comparator接口咱们并不陌生,其中有一个方法int compare(T o1, T o2)须要实现,显然该接口是个函数接口。

需求:假设有一个字符串列表,按照字符串长度增序对元素排序。

因为Java7以及以前sort()方法在Collections工具类中,因此代码要这样写:

// Collections.sort()方法
ArrayList<String> list = new ArrayList<>(Arrays.asList("I", "love", "you", "too"));
Collections.sort(list, new Comparator<String>(){
    @Override
    public int compare(String str1, String str2){
        return str1.length()-str2.length();
    }
});

如今能够直接使用List.sort()方法,结合Lambda表达式,能够这样写:

// List.sort()方法结合Lambda表达式
ArrayList<String> list = new ArrayList<>(Arrays.asList("I", "love", "you", "too"));
list.sort((str1, str2) -> str1.length()-str2.length());

spliterator()

方法签名为Spliterator<E> spliterator(),该方法返回容器的可拆分迭代器。从名字来看该方法跟iterator()方法有点像,咱们知道Iterator是用来迭代容器的,Spliterator也有相似做用,但两者有以下不一样:

  1. Spliterator既能够像Iterator那样逐个迭代,也能够批量迭代。批量迭代能够下降迭代的开销。
  2. Spliterator是可拆分的,一个Spliterator能够经过调用Spliterator<T> trySplit()方法来尝试分红两个。一个是this,另外一个是新返回的那个,这两个迭代器表明的元素没有重叠。

可经过(屡次)调用Spliterator.trySplit()方法来分解负载,以便多线程处理。

stream()和parallelStream()

stream()parallelStream()分别返回该容器的Stream视图表示,不一样之处在于parallelStream()返回并行的StreamStream是Java函数式编程的核心类,咱们会在后面章节中学习。

Map中的新方法

相比CollectionMap中加入了更多的方法,咱们以HashMap为例来逐一探秘。了解[Java7HashMap实现原理](https://github.com/CarpenterLee/JCFInternals/blob/master/markdown/6-HashSet and HashMap.md),将有助于理解下文。

forEach()

该方法签名为void forEach(BiConsumer<? super K,? super V> action),做用是Map中的每一个映射执行action指定的操做,其中BiConsumer是一个函数接口,里面有一个待实现方法void accept(T t, U u)BinConsumer接口名字和accept()方法名字都不重要,请不要记忆他们。

需求:假设有一个数字到对应英文单词的Map,请输出Map中的全部映射关系.

Java7以及以前经典的代码以下:

// Java7以及以前迭代Map
HashMap<Integer, String> map = new HashMap<>();
map.put(1, "one");
map.put(2, "two");
map.put(3, "three");
for(Map.Entry<Integer, String> entry : map.entrySet()){
    System.out.println(entry.getKey() + "=" + entry.getValue());
}

使用Map.forEach()方法,结合匿名内部类,代码以下:

// 使用forEach()结合匿名内部类迭代Map
HashMap<Integer, String> map = new HashMap<>();
map.put(1, "one");
map.put(2, "two");
map.put(3, "three");
map.forEach(new BiConsumer<Integer, String>(){
    @Override
    public void accept(Integer k, String v){
        System.out.println(k + "=" + v);
    }
});

上述代码调用forEach()方法,并使用匿名内部类实现BiConsumer接口。固然,实际场景中没人使用匿名内部类写法,由于有Lambda表达式:

// 使用forEach()结合Lambda表达式迭代Map
HashMap<Integer, String> map = new HashMap<>();
map.put(1, "one");
map.put(2, "two");
map.put(3, "three");
map.forEach((k, v) -> System.out.println(k + "=" + v));
}

getOrDefault()

该方法跟Lambda表达式不要紧,可是颇有用。方法签名为V getOrDefault(Object key, V defaultValue),做用是按照给定的key查询Map中对应的value,若是没有找到则返回defaultValue。使用该方法程序员能够省去查询指定键值是否存在的麻烦.

需求;假设有一个数字到对应英文单词的Map,输出4对应的英文单词,若是不存在则输出NoValue

// 查询Map中指定的值,不存在时使用默认值
HashMap<Integer, String> map = new HashMap<>();
map.put(1, "one");
map.put(2, "two");
map.put(3, "three");
// Java7以及以前作法
if(map.containsKey(4)){ // 1
    System.out.println(map.get(4));
}else{
    System.out.println("NoValue");
}
// Java8使用Map.getOrDefault()
System.out.println(map.getOrDefault(4, "NoValue")); // 2

putIfAbsent()

该方法跟Lambda表达式不要紧,可是颇有用。方法签名为V putIfAbsent(K key, V value),做用是只有在不存在key值的映射或映射值为null,才将value指定的值放入到Map中,不然不对Map作更改.该方法将条件判断和赋值合二为一,使用起来更加方便.

remove()

咱们都知道Map中有一个remove(Object key)方法,来根据指定key值删除Map中的映射关系;Java8新增了remove(Object key, Object value)方法,只有在当前Mapkey正好映射到value才删除该映射,不然什么也不作.

replace()

在Java7及之前,要想替换Map中的映射关系可经过put(K key, V value)方法实现,该方法老是会用新值替换原来的值.为了更精确的控制替换行为,Java8在Map中加入了两个replace()方法,分别以下:

  • replace(K key, V value),只有在当前Mapkey的映射存在时才用value去替换原来的值,不然什么也不作.
  • replace(K key, V oldValue, V newValue),只有在当前Mapkey的映射存在且等于oldValue才用newValue去替换原来的值,不然什么也不作.

replaceAll()

该方法签名为replaceAll(BiFunction<? super K,? super V,? extends V> function),做用是对Map中的每一个映射执行function指定的操做,并用function的执行结果替换原来的value,其中BiFunction是一个函数接口,里面有一个待实现方法R apply(T t, U u).不要被如此多的函数接口吓到,由于使用的时候根本不须要知道他们的名字.

需求:假设有一个数字到对应英文单词的Map,请将原来映射关系中的单词都转换成大写.

Java7以及以前经典的代码以下:

// Java7以及以前替换全部Map中全部映射关系
HashMap<Integer, String> map = new HashMap<>();
map.put(1, "one");
map.put(2, "two");
map.put(3, "three");
for(Map.Entry<Integer, String> entry : map.entrySet()){
    entry.setValue(entry.getValue().toUpperCase());
}

使用replaceAll()方法结合匿名内部类,实现以下:

// 使用replaceAll()结合匿名内部类实现
HashMap<Integer, String> map = new HashMap<>();
map.put(1, "one");
map.put(2, "two");
map.put(3, "three");
map.replaceAll(new BiFunction<Integer, String, String>(){
    @Override
    public String apply(Integer k, String v){
        return v.toUpperCase();
    }
});

上述代码调用replaceAll()方法,并使用匿名内部类实现BiFunction接口。更进一步的,使用Lambda表达式实现以下:

// 使用replaceAll()结合Lambda表达式实现
HashMap<Integer, String> map = new HashMap<>();
map.put(1, "one");
map.put(2, "two");
map.put(3, "three");
map.replaceAll((k, v) -> v.toUpperCase());

简洁到让人难以置信.

merge()

该方法签名为merge(K key, V value, BiFunction<? super V,? super V,? extends V> remappingFunction),做用是:

  1. 若是Mapkey对应的映射不存在或者为null,则将value(不能是null)关联到key上;
  2. 不然执行remappingFunction,若是执行结果非null则用该结果跟key关联,不然在Map中删除key的映射.

参数中BiFunction函数接口前面已经介绍过,里面有一个待实现方法R apply(T t, U u)

merge()方法虽然语义有些复杂,但该方法的用方式很明确,一个比较常见的场景是将新的错误信息拼接到原来的信息上,好比:

map.merge(key, newMsg, (v1, v2) -> v1+v2);

compute()

该方法签名为compute(K key, BiFunction<? super K,? super V,? extends V> remappingFunction),做用是把remappingFunction的计算结果关联到key上,若是计算结果为null,则在Map中删除key的映射.

要实现上述merge()方法中错误信息拼接的例子,使用compute()代码以下:

map.compute(key, (k,v) -> v==null ? newMsg : v.concat(newMsg));

computeIfAbsent()

该方法签名为V computeIfAbsent(K key, Function<? super K,? extends V> mappingFunction),做用是:只有在当前Map不存在key值的映射或映射值为null,才调用mappingFunction,并在mappingFunction执行结果非null时,将结果跟key关联.

Function是一个函数接口,里面有一个待实现方法R apply(T t)

computeIfAbsent()经常使用来对Map的某个key值创建初始化映射.好比咱们要实现一个多值映射,Map的定义多是Map<K,Set<V>>,要向Map中放入新值,可经过以下代码实现:

Map<Integer, Set<String>> map = new HashMap<>();
// Java7及之前的实现方式
if(map.containsKey(1)){
    map.get(1).add("one");
}else{
    Set<String> valueSet = new HashSet<String>();
    valueSet.add("one");
    map.put(1, valueSet);
}
// Java8的实现方式
map.computeIfAbsent(1, v -> new HashSet<String>()).add("yi");

使用computeIfAbsent()将条件判断和添加操做合二为一,使代码更加简洁.

computeIfPresent()

该方法签名为V computeIfPresent(K key, BiFunction<? super K,? super V,? extends V> remappingFunction),做用跟computeIfAbsent()相反,即,只有在当前Map存在key值的映射且非null,才调用remappingFunction,若是remappingFunction执行结果为null,则删除key的映射,不然使用该结果替换key原来的映射.

这个函数的功能跟以下代码是等效的:

// Java7及之前跟computeIfPresent()等效的代码
if (map.get(key) != null) {
    V oldValue = map.get(key);
    V newValue = remappingFunction.apply(key, oldValue);
    if (newValue != null)
        map.put(key, newValue);
    else
        map.remove(key);
    return newValue;
}
return null;
  1. Java8为容器新增一些有用的方法,这些方法有些是为完善原有功能,有些是为引入函数式编程,学习和使用这些方法有助于咱们写出更加简洁有效的代码.
  2. 函数接口虽然不少,但绝大多数时候咱们根本不须要知道它们的名字,书写Lambda表达式时类型推断帮咱们作了一切.

参考:https://github.com/CarpenterLee/JavaLambdaInternals

相关文章
相关标签/搜索