lambda表达式详解

1、 lambda表达式

JVM内部是通过invokedynamic指令来实现Lambda表达式的
Lambda中允许将一个函数作为方法的参数,即函数作为参数传递进方法中
使用Lambda表达式可以使代码更加简洁

2、变量作用域

Lambda表达式只能引用标记了final的外层局部变量.即不能在Lambda表达式内部修改定义在作用域外的局部变量,否则会导致报错
Lambda表达式中可以直接访问外层的局部变量
Lambda表达式中外层局部变量可以不用声明为final, 但是必须不可被后面的代码修改,即隐性地具有final的语义
Lambda表达式中不允许声明一个与外层局部变量同名的参数或者局部变量

3、使用示例

匿名内部类
匿名内部类: 匿名内部类仍然是一个类,不需要指定类名,编译器会自动为该类取名
Java中的匿名内部类:

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

使用Lambda表达式实现匿名内部类:

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


4、带参函数

带参函数的简写:

List list = Arrays.asList("I", "love", "you", "too");
Collections.sort(list, new Comparator() { // 接口名
    @Override
    public int compare(String s1, String s2) { // 方法名
        if(s1 == null)
            return -1;
        if(s2 == null)
            return 1;
        return s1.length() - s2.length();           
    }
});

上述代码通过内部类重载了Comparator接口的compare() 方法来实现比较逻辑. 采用Lambda表达式可简写如下:

List list = Arrays.asList("I", "love", "you", "too");
Collections.sort(list, (s1, s2) -> { // 省略参数表类型
    if (s1 == null)
        return -1;
    if (s2 == null)
        return 1;
    return s1.length() - s2.length();
});


上述代码根内部类的作用一样
除了省略了接口名和方法名,代码中的参数类型也可以省略
因为javac的类型推断机制,编译器能够根据上下文信息推断出参数的类型

5、 Collection

5.1、forEach

增强型for循环:

ArrayList list = new ArrayList<>(Arrays.asList("I", "love", "you", "too"));
for(String str : list) {
    if (str.length() > 3)
        System.out.println(str);
}

使用forEach() 方法结合匿名内部类实现:

ArrayList list = new ArrayList<>(Arrays.asList("I", "love", "you", "too"));
list.forEach(new Consumer(){
    @Override
    public void accept(String str) {
        if (str.length() > 3) {
            System.out.println(str);
        }
    }
});

使用Lambda表达式实现如下:

// 使用forEach()结合Lambda表达式迭代

ArrayList list = new ArrayList<>(Arrays.asList("I", "love", "you", "too"));
list.forEach(str -> {
    if (str.length() > 3) {
        Systemm.out.println(str);
    }       
});

上述代码给forEach() 方法传入一个Lambda表达式,不需要知道accept() 方法,也不需要知道Consumer接口,类型推导已经完成了这些

5.2、removeIf

该方法签名: boolean removeIf(Predicate filter);
删除容器中所有满足filter指定条件的元素
Predicate是一个函数接口,里面有一个待实现的方法boolean test(T t)
如果需要在迭代过程中对容器进行删除操作必须使用迭代器, 否则会抛出ConcurrentModificationException.
使用迭代器删除列表元素:

ArrayList list = new ArrayList<>(Arrays.asList("I", "love", "you", "too"));
Iterator it = list.iterator();
while (it.hasNext()) {
    if (it.next().length > 3) {
        it.remove();
    }
}
ArrayList list = new ArrayList<>(Arrays.asList("I", "love", "you", "too"));
list.removeIf(new Predicate(){
    @Override
    public boolean test(String str) {
        return str.length() > 3;
    }
});

使用removeIf结合Lambda表达式实现:

Array list = new ArrayList<>(Arrays.asList("I", "love", "you", "too"));
list.removeIf(str -> str.length() > 3);

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

5.3、replaceAll

该方法签名: void replaceAll(UnaryOperator operator);
对每个元素执行operator指定的操作,并用操作结果来替换原来的元素
UnaryOperator是一个函数接口,里面有待实现的方法T apply(T t)
使用下标实现元素替换:

ArrayList 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 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表达式实现:

ArrayList list = new ArrayList<>(Arrays.asList("I", "love", "you", "too"));
list.replaceAll(str -> {
    if (str.length > 3) {
        return str.toUpperCase();
    }
    return str;
});

5.4、sort

该方法定义在List接口中,方法签名: void sort(Comparator c);
根据c指定的比较规则对容器进行排序
Comparator接口中需要实现接口int compare(T o1, T o2)
使用Collections的sort() 方法:

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

直接使用List.sort() 方法,结合Lambda表达式:

ArrayList list = new ArrayList<>(Arrays.asList("I", "love", "you", "too"));
list.sort((str1, str2) -> str1.length() - str2.length());


5.5 、spliterator

该方法签名: Spliterator spliterator();
Spliterator既可以像Iterator那样逐个迭代,也可以批量迭代,批量迭代可以降低迭代的开销
Spliterator是可拆分的,一个Spliterator可以通过调用Spliterator trySplit() 方法来尝试分成两个.一个是this, 一个是新返回的元素.这两个迭代器代表的元素没有重叠
可通过多次调用Spliterator.trySplit() 方法来分解负载,以便于多线程处理

5.6、stream和parallStream

Stream() 和parallStream() 分别返回该容器的Stream视图表示
parallStream() 返回并行的Stream
Stream是Java函数式编程的核心类

6、 Map

6.1、forEach

该方法签名: void forEach(BiConsumer action);
对Map中的每个映射执行action操作
BiConsumer是一个函数接口,里面有一个待实现方法 void accept(T t, U u);

使用Java 7之前的方式输出Map中所有的对应关系:

HashMap map = new HashMap<>();
map.put(1, “one”);
map.put(2, “two”);
map.put(3, “three”);
for (Map.Entry entry : map.entrySet()) {
system.out.println(entry.getKey() + “=” + entry.getValue());
}

使用Map的forEach() 方法,结合匿名内部类:

HashMap map = new HashMap<>();
map.put(1, “one”);
map.put(2, “two”);
map.put(3, “three”);
map.forEach(new BiConsumer() {
@Override
public void accept(Integer k, String v) {
System.out.println(k + “=” + v);
}
});

使用Lambda表达式:

HashMap map = new HashMap<>();
map.put(1, “one”);
map.put(2, “two”);
map.put(3, “three”);
map.forEach((k, v) -> System.out.println(k + “=” + v));

6.2、getOrDefault

该方法签名: V getOrDefault(Object key, V defaultValue);
按照给定的key查询Map中对应的value, 如果没有找到则返回defaultValue
查询Map中指定键所对应的值,如果不存在则返回NoValue:

HashMap map = new HashMap<>();
map.put(1, “one”);
map.put(2, “two”);
map.put(3, “three”);
System.out.println(map.getOrDefault(4,“NoValue”));

6.3、putIfAbsent

该方法签名: V putIfAbsent(K key, V value);
只有在不存在key值的映射或映射值为null时,才将value指定的值放入到Map中,否则不对Map做修改
该方法将判断和赋值合二为一,使用起来更加方便

6.4、remove

该方法签名: remove(Object key);
根据指定的key值删除Map中映射关系
该方法签名: remove(Object key, Object value);
只有在当前Map中key正好映射到value时才删除该映射

6.5、replace

该方法签名: replace(K key, V value);
只有在当前Map中key的映射存在时才用value去替换原来的值
该方法签名: replace(K key, V oldValue, V newValue);
只有在当前Map中key的映射存在且等于oldValue时,才用newValue去替换原来的值,否则不做任何操作

6.6、replaceAll

该方法签名: replaceAll(BiFunction function);
对Map中的每个映射执行function操作,并用function的执行结果替换原来的value
其中BiFunction是一个函数接口,里面有一个待实现的方法R apply(T t, U u)
使用Java 7以前的方式将Map中的映射关系的单词都转换成大写:

HashMap map = new HashMap<>();
map.put(1, “one”);
map.put(2, “two”);
map.put(3, “three”);
for (Map.Entry entry : map.entrySet()) {
entry.setValue(entry.getValue().toUpperCase());
}

使用replaceAll方法结合匿名内部类:

HashMap map = new HashMap<>();
map.put(1, “one”);
map.put(2, “two”);
map.put(3, “three”);
map.replaceAll(new BiFunction(){
@Override
public String apply(Integer k, String v) {
return v.toUpperCase();
}
});

使用Lambda表达式实现:

HashMap map = new HashMap<>();
map.put(1, “one”);
map.put(2, “two”);
map.put(3, “three”);
map.replaceAll( -> v.toUpperCase());

6.6、merge

该方法签名: merge(K key, V value, BiFunction remappingFunction);
如果Map中的key对应的映射不存在或者为null, 则将value, value不可能为null关联到key上
否则执行remappingFunction, 如果执行结果非null, 则用该结果与key关联,否则在Map中删除key的映射
其中BiFunction是一个函数接口,里面有一个待实现方法R apply(T t, U u)
merge()方法语义复杂,但使用的方式明确,经典的使用场景: 将新的错误信息拼接到原来的信息上:

7、 Stream API 高级

7.1、归约操作: reduction operation

又称作折叠操作fold
通过某个连接动作将所有元素汇总成一个汇总结果的过程
元素求和,求最大值最小值,求元素总个数,将所有元素转换成一个列表或集合,都属于归约操作
Stream类库中两个通用的规约操作:
reduce()
collect()
也有一些为了简化书写而设计的专用归约操作 : sum(), max(), min(), count() 等

7.2、reduce

实现从一组元素中生成一个值
sum(),max(),min(),count()等都是reduce操作,将这些单独设为函数是因为经常使用
reduce()的方法定义有三种重写形式:

Optional< T > reduce(BinaryOperator< T > accumulator);
T reduce(T identity, BinaryOperator< T > accumulator);
< U > U reduce(U identity, BiFunction< U, ? super T,U> accumulator, BinaryOperator< U > combiner);

虽然函数的定义越来越长,但是语义不变.多的参数是为了指明初始值(identity), 或者是指定并行执行时多个部分结果的合并方式(combiner)
从一组单词中找出最长的单词.这里"大"的含义就是"长":

/*
 * 找出最长的单词
 */
Stream stream = Stream.of("I", "love", "you", "too");
Optional longets = stream.reduce((s1, s2) -> s1.length() >= s2.length() ? s1 : s2);
// Optional longest = stream.max((s1, s2) -> s1.length() - s2.length());
System.out.println(longest.get());

选出最长的单词love
其中Optional是只有一个值的容器,使用Optional可以避免null值
求出一组单词长度之和. 这是个求和操作,操作对象输入类型是String,结果类型是Integer:

lambda表达式详解_第1张图片

/*
 * 求单词长度之和
 */
 Stream strean = Stream.of("I", "love", "you", "too");
 Integer lengthSum = stream.reduce(0,       // 初始值 (1)
                                  (sum, str) -> sum +str.length(),      // 累加器 (2)
                                  (a, b) -> a + b);     // 部分和拼接,并行执行时会用到 (3)
// int lengthSun = stream.mapToInt(str -> str.length()).sum();
System.out.println(lengthSum);

上述代码 (2) 处的累加器:
字符串映射成长度
并和当前累加和相加
使用reduce() 函数将这两步合二为一,更有助于提升性能
同样也可以使用map() 和sum() 组合也可以达到目的

7.3、collect

reduce() 的优点的是生成一个值,但是如果想要从Stream中生成一个集合或者Map等复杂对象时,就要用到collect()
示例: reduce() 的优点的是生成一个值,但是如果想要从Stream中生成一个集合或者Map等复杂对象时,就要用到collect()
示例:

/* 
 * 将Stream转换成容器或者Map
 */
Stream stream = Stream.of("I", "love", "you", "too");
List list = stream.collect(Collectors.toList());
Set set = stream.collect(Collectors.toSet());
// 将Stream转换成Map
Map map = stream.collect(Collectors.toMap(Function.identity(), String::length));

上述分别将Stream转换成List,Set,Map
需要注意的有:

Function.identity()
String::length
Collectors

7.4、接口的静态方法和默认方法


Function是一个接口 ,Function.identity() 含义有两个方面:
Java 8允许在接口中加入具体方法. 接口中的具体方法有两种:
static: 静态方法,identity()就是Function接口的一个静态方法
default: 默认方法
Function.identity(): 返回一个输出和输入一样的Lambda表达式对象,等价于 t -> t 形式的Lambda表达式

7.5、使用collect()生成Map

Stream依赖某种数据源,数据源可以是数组,容器等,但不能是Map
但是可以从Stream生成Map,要做的是确定好Map的key和value分别代表什么,这个在于要想清楚到底要干什么
通常在三种情况下collect()的结果会是Map:
使用Collectors.toMap() 生成的收集器: 用户需要指定如何生成Map的key和value
使用Collectors.partitioningBy() 生成的收集器: 对元素进行二分区操作时用到
使用Collectors.groupingBy() 生成的收集器: 对元素做group操作时用到

7.6、使用toMap()生成的收集器:

这个是和Collectors.toCollection() 并列的方法
示例: 将学生列表转换成由<学生, GPA>组成的Map

/*
 * 使用toMap()统计学生的GPA  如何生成key 如何生成value                
 */
Map studentToGPA = student.stream().collect(Collectors.toMap(Function.identity(), student -> computeGPA(student))); 

7.7、使用partitioningBy()生成的收集器:

适用于将Stream中的元素依据某个二值逻辑(Boolean 满足,不满足)分成互补相交的两部分
示例: 将学生分成成绩及格和不及格的两部分

/*
 * 将学生成绩分为及格不及格两部分
 */
 Map> passingFailing = students.stream().collect(Collectors.partitioningBy(s -> s.getGrade() >= PASS_THRESHOLD));

7.8、使用groupingBy()生成的收集器:

这是比较灵活的一种,与SQL中的group by语句类似
这里的groupingBy也是按照某个属性对数据进行分组,属性相同的元素会被对应到Map的同一个key上
示例: 将员工按照部门进行分组

/*
 * 将员工按照部门进行分组
 */
 Map> byDept = employees.stream().collect(Collectors.groupingBy(Employee :: getDepartment));

有时候仅仅分组是无法满足要求的.在SQL中使用group by是为了方便更高级的查询:
先将员工按照部门分组
然后统计每个部门员工的人数
增强版的groupingBy()能够满足这种需求:
增强版的groupingBy()允许先对元素分组之后再执行某种运算,比如求和,计数,平均值,类型转换等
这种先将元素分组的收集器叫作上游收集器
然后执行分组后的运算的收集器叫作下游收集器

/*
 * 使用下游收集器统计每个部门的人数
 */
 Map totalByDept = employees.stream()
                                               .collect(Collectors.groupingBy(Employee :: getDepartment,
                                                                              Collectors.counting()));

这个groupingBy和SQL相似,也是高度非结构化

下游收集器还可以包含更下游的收集器:
将员工按照部门分组
得到每个员工的名字字符串,而不是一个个Employee对象

/*
 * 按照部门对员工进行分组,并且只保留员工的名字
 */
 Map> byDept = employees.stream()
                                                 .collect(Collectors.groupingBy(Employee :: getDepartment,
                                                  Collectors.mapping(Employee :: getName,
                                                                    Collectors.toList())));

7.9、使用collect()做字符串join

字符串拼接时使用Collectors.joining() 生成的收集器,代替for循环拼接
Collectors.joining() 方法有三种重写形式,分别对应三种不同的拼接方式:

/*
 * 使用Collectors.joining()拼接字符串
 */
 Stream stream = Stream.of("I", "love", "you");

String joined = stream.collect(Collectors.joining());   // Iloveyou
String joined = stream.collect(Collectors.joining(","));    // I,love,you
String joined = stream.collect(Collectors.joining(",", "{", "}"));  // {I,love,you}

除了可以使用Collectors工具类已经封装好的收集器,还可以自定义收集器.或者直接调用collect(Supplier< R > supplier, BiConsumer accumulator, BiConsumer combiner) 方法,收集需要的任何形式的信息

8、Stream Pipelines

通过使用Stream API中引起的疑问:
如此强大的Stream API是如何实现的?
Pipeline是怎么执行的,每次调用都会迭代一次吗?
自动并行又是怎么做到的,线程个数是多少?
容器执行Lambda表达式的方式 - 以ArrayList.forEach()方法为例:

/*
 * ArrayList.forEach()
 */
 public void forEach(Consumer action) {
    ...
    for (int i = 0; modCount == expectedModCount && i < size; i ++) {
        // 回调方法
        action.accept(elementData[i]);
    }
    ...
 }

ArrayList.forEach() 方法的主要逻辑就是一个for循环,在该for循环里不断调用action.accept() 回调方法完成对元素的遍历
回调方法在Java GUI的监听器中广泛使用,Lambda表达式的作用就是相当于一个回调方法
Stream API中大量使用Lambda表达式作为回调方法. 但想要理解Stream, 关键的是:
流水线
自动并行

int longestStringLengthStaringWithA = strings.stream().filter(s -> s.startsWith("A"))
                                                      .mapToInt(String :: length)
                                                      .max();

Stream中的相关操作:
中间操作: Intermediate operations
无状态: Stateless

unordered()
filter()
map()
mapToInt()
mapToLong()
mapToDouble()
flatMap()
flatMapToInt()
flatMapToLong()
flatMapToDouble()
peek()
有状态: Stateful
distinct()
sorted()
limit()
skip()

结束操作: Terminal operations
短路操作: short-circuiting

anyMatch()
allMatch()
noneMatch()
findFirst()
findAny()
非短路操作:
forEach()
forEachOrdered()
toArray()
reduce()
collect()
max()
min()
count()

Stream上的所有操作分为两类: 因为Stream底层对每一种情况的处理方式不同,所以要进行精细的划分
中间操作: 中间操作只是一种标记
无状态: 指元素的处理不受前面元素的影响,处理完一个元素就能立即知道结果
有状态: 指元素的处理受到别的元素的影响,必须等到所有元素处理之后才能知道结果
结束操作: 只有结束操作才会触发实际的计算
短路操作: 指不用处理全部元素就可以返回结果
非短路操作: 指对所有的元素处理后才可以返回结果

8.1、Stream Pipeline实现方案

一种直白的Stream Pipeline实现方案:
lambda表达式详解_第2张图片

求最长字符串的长度:
一种直白的实现方式是为每一次函数调用都执行一次迭代,并将处理中间结果发明放到某种数据结构中,比如数组,容器等
就是调用filter() 方法后立即执行
选出所有以A开头的字符串并放到一个列表list1中
然后让list1传递给mapToInt() 方法并立即执行
生成的结果放到list2中
最后遍历list2, 找出最大的数字作为最终的结果
这种实现方法实现简单直观,但存在两个明显的缺陷:
迭代次数多: 迭代次数和函数的调用次数相等
频繁产生中间结果: 每次函数调用都产生一次中间结果,存储开销大
不使用Stream API在一次迭代中实现求最长字符串长度的方式:

int longest = 0;
for (String str : strings) {
    if (str.startsWith("A")) {  // 类似filter(),保留以A开头的字符串
        int len = str.length(); // 类似mapToInt(),得到字符串的长度
        longest = Math.max( );
    }
}

采用这种方法不但减少了迭代次数,也避免了存储中间结果,这就是Stream Pipeline.将三个操作放在了一次迭代中
只要事先知道意图,总是能够采取上述方式实现与Stream API等价的功能

你可能感兴趣的:(java-lamda表达式,java)