JVM内部是通过invokedynamic指令来实现Lambda表达式的
Lambda中允许将一个函数作为方法的参数,即函数作为参数传递进方法中
使用Lambda表达式可以使代码更加简洁
Lambda表达式只能引用标记了final的外层局部变量.即不能在Lambda表达式内部修改定义在作用域外的局部变量,否则会导致报错
Lambda表达式中可以直接访问外层的局部变量
Lambda表达式中外层局部变量可以不用声明为final, 但是必须不可被后面的代码修改,即隐性地具有final的语义
Lambda表达式中不允许声明一个与外层局部变量同名的参数或者局部变量
匿名内部类
匿名内部类: 匿名内部类仍然是一个类,不需要指定类名,编译器会自动为该类取名
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();;
}
}
带参函数的简写:
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的类型推断机制,编译器能够根据上下文信息推断出参数的类型
增强型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接口,类型推导已经完成了这些
该方法签名: boolean removeIf(Predicate super E> 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表达式
该方法签名: 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;
});
该方法定义在List接口中,方法签名: void sort(Comparator super E> 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());
该方法签名: Spliterator spliterator();
Spliterator既可以像Iterator那样逐个迭代,也可以批量迭代,批量迭代可以降低迭代的开销
Spliterator是可拆分的,一个Spliterator可以通过调用Spliterator trySplit() 方法来尝试分成两个.一个是this, 一个是新返回的元素.这两个迭代器代表的元素没有重叠
可通过多次调用Spliterator.trySplit() 方法来分解负载,以便于多线程处理
Stream() 和parallStream() 分别返回该容器的Stream视图表示
parallStream() 返回并行的Stream
Stream是Java函数式编程的核心类
该方法签名: void forEach(BiConsumer super K,? super V> 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));
该方法签名: 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”));
该方法签名: V putIfAbsent(K key, V value);
只有在不存在key值的映射或映射值为null时,才将value指定的值放入到Map中,否则不对Map做修改
该方法将判断和赋值合二为一,使用起来更加方便
该方法签名: remove(Object key);
根据指定的key值删除Map中映射关系
该方法签名: remove(Object key, Object value);
只有在当前Map中key正好映射到value时才删除该映射
该方法签名: replace(K key, V value);
只有在当前Map中key的映射存在时才用value去替换原来的值
该方法签名: replace(K key, V oldValue, V newValue);
只有在当前Map中key的映射存在且等于oldValue时,才用newValue去替换原来的值,否则不做任何操作
该方法签名: replaceAll(BiFunction super K, ? super V, ? extends V> 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());
该方法签名: merge(K key, V value, BiFunction super V, ? super V, ? extends V> remappingFunction);
如果Map中的key对应的映射不存在或者为null, 则将value, value不可能为null关联到key上
否则执行remappingFunction, 如果执行结果非null, 则用该结果与key关联,否则在Map中删除key的映射
其中BiFunction是一个函数接口,里面有一个待实现方法R apply(T t, U u)
merge()方法语义复杂,但使用的方式明确,经典的使用场景: 将新的错误信息拼接到原来的信息上:
又称作折叠操作fold
通过某个连接动作将所有元素汇总成一个汇总结果的过程
元素求和,求最大值最小值,求元素总个数,将所有元素转换成一个列表或集合,都属于归约操作
Stream类库中两个通用的规约操作:
reduce()
collect()
也有一些为了简化书写而设计的专用归约操作 : sum(), max(), min(), count() 等
实现从一组元素中生成一个值
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:
/*
* 求单词长度之和
*/
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() 组合也可以达到目的
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
Function是一个接口 ,Function.identity() 含义有两个方面:
Java 8允许在接口中加入具体方法. 接口中的具体方法有两种:
static: 静态方法,identity()就是Function接口的一个静态方法
default: 默认方法
Function.identity(): 返回一个输出和输入一样的Lambda表达式对象,等价于 t -> t 形式的Lambda表达式
Stream依赖某种数据源,数据源可以是数组,容器等,但不能是Map
但是可以从Stream生成Map,要做的是确定好Map的key和value分别代表什么,这个在于要想清楚到底要干什么
通常在三种情况下collect()的结果会是Map:
使用Collectors.toMap() 生成的收集器: 用户需要指定如何生成Map的key和value
使用Collectors.partitioningBy() 生成的收集器: 对元素进行二分区操作时用到
使用Collectors.groupingBy() 生成的收集器: 对元素做group操作时用到
这个是和Collectors.toCollection() 并列的方法
示例: 将学生列表转换成由<学生, GPA>组成的Map
/*
* 使用toMap()统计学生的GPA 如何生成key 如何生成value
*/
Map studentToGPA = student.stream().collect(Collectors.toMap(Function.identity(), student -> computeGPA(student)));
适用于将Stream中的元素依据某个二值逻辑(Boolean 满足,不满足)分成互补相交的两部分
示例: 将学生分成成绩及格和不及格的两部分
/*
* 将学生成绩分为及格不及格两部分
*/
Map> passingFailing = students.stream().collect(Collectors.partitioningBy(s -> s.getGrade() >= PASS_THRESHOLD));
这是比较灵活的一种,与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())));
字符串拼接时使用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
通过使用Stream API中引起的疑问:
如此强大的Stream API是如何实现的?
Pipeline是怎么执行的,每次调用都会迭代一次吗?
自动并行又是怎么做到的,线程个数是多少?
容器执行Lambda表达式的方式 - 以ArrayList.forEach()方法为例:
/*
* ArrayList.forEach()
*/
public void forEach(Consumer super E> 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底层对每一种情况的处理方式不同,所以要进行精细的划分
中间操作: 中间操作只是一种标记
无状态: 指元素的处理不受前面元素的影响,处理完一个元素就能立即知道结果
有状态: 指元素的处理受到别的元素的影响,必须等到所有元素处理之后才能知道结果
结束操作: 只有结束操作才会触发实际的计算
短路操作: 指不用处理全部元素就可以返回结果
非短路操作: 指对所有的元素处理后才可以返回结果
求最长字符串的长度:
一种直白的实现方式是为每一次函数调用都执行一次迭代,并将处理中间结果发明放到某种数据结构中,比如数组,容器等
就是调用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等价的功能