JAVA8 新特性
JAVA8 新特性
学习核心
- 掌握JAVA8核心特性(结合实际应用阐述)
- lambda表达式
- Lambda表达式有什么好处?场景应用?
- 方法引用
- 方法引用是什么?有什么好处?
- stream API
- JAVA中的stream流?有什么功能?
- stream应用场景是什么?
- Optional
- Optional是什么?应用场景是什么?
- lambda表达式
学习资料
Lambda表达式和函数式接口
1.Lambda表达式基础
Lambda表达式格式
// 1.不带参数
()-> System.out.println("x");
// 2.带参数
(val)-> System.out.println(val);
// 3.带单个参数(小括号可省略)
val -> System.out.println(val);
// 4.带多个参数(小括号不能省略)
(teacher,student)-> System.out.println(teacher.getName() + student.getName());
// 5.方法体
(x,y)->{...方法实现...}
// 6.lambda表达式的返回值
(x,y)->{
... 方法实现 ...
return "xxx";
}
// 如果 Lambda 表达式所做的只是计算返回值并返回它,甚至可以省略 return 语句
(a1, a2) -> { return a1 > a2; }
(a1, a2) -> { a1 > a2; }
在某些情况下,Lambda 表达式能够访问在 Lambda 函数体之外声明的变量。 Lambda 可以访问以下类型的变量:
- 局部变量
- 实例变量
- 静态变量
函数式接口
只有一个抽象方法的接口被称为函数是式接口,从 Java 8 开始,Java 接口中可以包含默认方法和静态方法。默认方法和静态方法都有直接在接口声明中定义的实现。这意味着,Java lambda 表达式可以实现拥有多个方法的接口——只要接口中只有一个未实现的抽象方法就行。
参考案例
interface MyInterface{
// 抽象方法
void doSth(String value);
// 默认方法
default void print(){
System.out.println("hello default print");
}
// 静态方法
static void show(){
System.out.println("hello static show");
}
}
/**
* Lambda表达式案例
*/
public class LambdaDemo {
public static void main(String[] args) {
// 1.传统方式实现接口
new MyInterface() {
@Override
public void doSth( String value) {
System.out.println("传统方式 doSth:" + value);
}
}.doSth("冲冲冲");
// 2.Lambda表达式
MyInterface myInterface = (val) -> {
System.out.println("Lambda doSth:" + val);
};
myInterface.doSth("hello");
}
}
Lambda表达式 VS 匿名类
匿名类可以指定自己的内部状态(成员变量),例如此处定义一个eventState用于记录consume方法被调用的次数
但Lambda不能定义成员变量
// 接口定义
interface MyEventConsumer{
public void consume(Object event);
}
/**
* Lambda表达式 VS 匿名类
*/
public class LambdaAnonymousDemo {
public static void main(String[] args) {
// 匿名类实现
MyEventConsumer c1 = new MyEventConsumer() {
// 定义内部状态
private int eventState = 0;
@Override
public void consume(Object event) {
System.out.println("匿名类实现" + event.toString() + "-" + (eventState++));
}
};
c1.consume("hh");
c1.consume("kk");
// Lambda表达式实现
MyEventConsumer c2 = (event)->{
System.out.println("Lambda表达式实现" + event.toString());
};
c2.consume("xx");
}
}
方法引用
方法引用通过方法的名字来指向一个方法,它可以使语言的构造更紧凑简洁,减少冗余代码。
使用规则:一对冒号 ::
import java.util.function.Supplier;
public class Car {
// Supplier是jdk1.8的接口,这里和lambda一起使用
public static Car create(final Supplier<Car> supplier) {
return supplier.get();
}
public static void collide(final Car car) {
System.out.println("Collided " + car.toString());
}
public void follow(final Car another) {
System.out.println("Following the " + another.toString());
}
public void repair() {
System.out.println("Repaired " + this.toString());
}
}
public class CarTest {
public static void main(String[] args) {
// 方式1:构造器引用:Class::new(或者一般的Class< T >::new)
final Car car = Car.create( Car::new );
final List< Car > cars = Arrays.asList( car );
// 方式2:静态方法引用:Class::static_method
cars.forEach( Car::collide );
// 方式3:特定类的任意对象的方法引用:Class::method
cars.forEach( Car::repair );
// 方式4:特定对象的方法引用:instance::method
final Car police = Car.create( Car::new );
cars.forEach( police::follow );
}
}
2.Lambda表达式应用
事件监听
在 Java 中的事件侦听器通常被定义为具有单个方法的 Java 接口。以实现事件Listener为例
// 定义监听接口
interface StateChangeListener {
public void onStateChange(State oldState, State newState);
}
// 定义事件监听器(可注册状态)
public class StateOwner {
public void addStateListener(StateChangeListener listener) { ... }
}
传统方式:可使用匿名类实现 StateChangeListener 接口,然后为 StateOwner 实例添加侦听器
StateOwner stateOwner = new StateOwner();
stateOwner.addStateListener(new StateChangeListener() {
public void onStateChange(State oldState, State newState) {
// do something with the old and new state.
System.out.println("State changed")
}
});
Lambda表达式:Java8引入Lambda之后,可以使用 Lambda 表达式实现 StateChangeListener 接口
StateOwner stateOwner = new StateOwner();
stateOwner.addStateListener(
(oldState, newState) -> System.out.println("State changed")
);
Stream API
Stream的引入
现在很多大数据量系统中都存在分表分库的情况。
例如,电商系统中的订单表,常常使用用户 ID 的 Hash 值来实现分表分库,这样是为了减少单个表的数据量,优化用户查询订单的速度。
但在后台管理员审核订单时,需要将各个数据源的数据查询到应用层之后进行合并操作。
例如,当需要查询出过滤条件下的所有订单,并按照订单的某个条件进行排序,单个数据源查询出来的数据是可以按照某个条件进行排序的,但多个数据源查询出来已经排序好的数据,并不代表合并后是正确的排序,所以需要在应用层对合并数据集合重新进行排序。
在 Java8 之前,通常是通过 for 循环或者 Iterator 迭代来重新排序合并数据,又或者通过重新定义 Collections.sorts 的 Comparator 方法来实现,这两种方式对于大数据量系统来说,效率并不是很理想。
Java8 中添加了一个新的接口类 Stream,他和字节流概念不太一样,Java8 集合中的 Stream 相当于高级版的 Iterator,可以通过 Lambda 表达式对集合进行各种非常便利、高效的聚合操作(Aggregate Operation),或者大批量数据操作 (Bulk Data Operation)。
Stream 的聚合操作与数据库 SQL 的聚合操作 sorted、filter、map 等类似。在应用层就可以高效地实现类似数据库 SQL 的聚合操作了,而在数据操作方面,Stream 不仅可以通过串行的方式实现数据操作,还可以通过并行的方式处理大批量数据,提高数据的处理效率。
1.Stream API基础概念
Java 的 Stream API 提供了一种处理对象集合的函数式方法。 Stream 是和 Lambda 表达式等其他几个函数式编程特性一起在 Java 8 被引入的。这个篇教程将解释 Stream API 提供的这些函数式方法是如何工作的,以及怎么使用它们。
注意,Java 的 Stream API 与 Java IO 的 InputStream 和 OutputStream 没有任何关系,不要因为名字类似造成误解。 InputStream 和 OutputStream 是与字节流有关,而 Java 的 Stream API 用于处理对象流。
Stream定义
Java 的 Stream 是一个能够对其元素进行内部迭代的组件,这意味着它可以自己迭代其元素。相反地,当使用 Collection 的迭代功能,例如,从 Collection 获取Iterator 或者使用 Iterable 接口 的 forEach 方法这些方式进行迭代时,必须自己实现集合元素的迭代逻辑
流处理
可以将 Listener 方法或者叫处理器方法附加到 Stream 上。当 Stream 在内部迭代元素时,将以元素为参数调用这些处理器。Stream 会为流中的每个元素调用一次处理器。所以每个处理器方法都可以处理 Stream 中的每个元素,称为流处理。
流的多个处理器方法可以形成一个调用链。链上的前一个处理器处理流中的元素,返回的新元素会作为参数传给链中的下一个处理器处理。当然,处理器可以返回相同的元素或新元素,具体取决于处理器的目的和用途
流处理的构成
一般有很多方法获取Stream,最常见的是从Collection对象中获取Stream。集合对象都实现了 Collection 接口,所以通过接口里定义的 stream 方法获救获取到由集合元素构成的 Steam
Stream<String> stream = items.stream();
在对流进行处理时,不同的流操作以级联的方式形成处理链。一个流的处理链由一个源(source),0 到多个中间操作(intermediate operation)和一个终结操作(terminal operation)完成。
- 源:源代表 Stream 中元素的来源,比如我们上面看到的集合对象。
- 中间操作:中间操作,在一个流上添加的处理器方法,他们的返回结果是一个新的流。这些操作是延迟执行的,在终结操作启动后才会开始执行。
- 终结操作:终结流操作是启动元素内部迭代、调用所有处理器方法并最终返回结果的操作
public class StreamDemo {
public static void main(String[] args) {
List<String> stringList = new ArrayList<>();
stringList.add("ONE");
stringList.add("TWO");
stringList.add("THREE");
// 获取流
Stream<String> stream = stringList.stream();
// 流处理
long count = stream
.map((value) -> value.toLowerCase())
.count();
System.out.println("count = " + count);
}
}
map() 方法的调用是一个中间操作。它只是在流上设置一个 Lambda 表达式,将每个元素转换为小写形式。而对 count() 方法的调用是一个终结操作。此调用会在内部启动迭代,开始流处理,这将导致每个元素都转换为小写然后计数。
将元素转换为小写实际上并不影响元素的计数。转换部分只是作为 map() 是一个中间操作的示例
流的中间操作(调用链)
Stream API 的中间(非终结)流操作是转换或者过滤流中元素的操作。当把中间操作添加到流上时,会得到一个新的流作为结果。如果需要将多个操作链接在一起,则只能将第二个操作应用于第一个操作产生的 Stream 实例上
List<String> stringList = new ArrayList<>();
stringList.add("ONE");
stringList.add("TWO");
stringList.add("THREE");
Stream<String> stream = stringList.stream();
// 流的中间操作(返回一个新的流)
Stream<String> stringStream = stream.map((value) -> value.toLowerCase());
// 多个操作链(stringStream1基于stream构建,stringStream2基于stringStream1构建)
Stream<String> stringStream1 = stream.map((value) -> value.toLowerCase());
Stream<String> stringStream2 = stringStream1.map((value) -> value.toUpperCase());
// 一般将Stream上所有的中间操作串联成一个调用链
Stream<String> stream1 = stream
.map((value) -> value.toLowerCase())
.map((value) -> value.toUpperCase())
.map((value) -> value.substring(0,3));
2.常用方法
map
map() 方法将一个元素转换(或者叫映射)到另一个对象。例如,一个字符串列表,map() 可以将每个字符串转换为小写、大写或原始字符串的子字符串,或完全不同的东西
List<String> list = new ArrayList<String>();
Stream<String> stream = list.stream();
Stream<String> streamMapped = stream.map((value) -> value.toUpperCase());
filter
filter() 用于从 Stream 中过滤掉元素。 filter 方法接受一个 Predicate (也是一个函数式接口),filter() 为流中的每个元素调用 Predicate。如果元素要包含在 filter() 返回结果的流中,则 Predicate 应返回 true。如果不应包含该元素,则 Predicate 应返回 false
Stream<String> longStringsStream = stream.filter((value) -> {
// 元素长度大于等于3,返回true,会被保留在 filter 产生的新流中。
return value.length() >= 3;
});
flatMap
flatMap方法接受一个 Lambda 表达式, Lambda 的返回值必须也是一个stream类型,flatMap方法最终会把所有返回的stream合并。map 与 flatMap 方法很像,都是以某种方式转换流中的元素。如果需要将每个元素转换为一个值,则使用 map 方法,如果需要将每个元素转换为多个值组成的流,且最终把所有元素的流合并成一个流,则需要使用 flatMap 方法。 在效果上看是把原来流中的每个元素进行了“展平”
public class StreamDemo {
public static void main(String[] args) {
List<String> stringList = new ArrayList<String>();
stringList.add("One flew over the cuckoo's nest");
stringList.add("To kill a muckingbird");
stringList.add("Gone with the wind");
Stream<String> stream = stringList.stream();
stream.flatMap((value) -> {
String[] split = value.split(" ");
return Arrays.asList(split).stream();
}).forEach((value) -> System.out.println(value));
}
}
在上面的例子中,每个字符串元素被拆分成单词,变成一个 List,然后从这个 List 中获取并返回流,flatMap 方法最终会把这些流合并成一个,所以最后用流终结操作 forEach 方法,遍历并输出了每个单词
distinct
distinct() 会返回一个仅包含原始流中不同元素的新 Stream 实例,任何重复的元素都将会被去掉
List<String> stringList = new ArrayList<String>();
stringList.add("one");
stringList.add("two");
stringList.add("three");
stringList.add("one");
Stream<String> stream = stringList.stream();
List<String> distinctStrings = stream
.distinct()
.collect(Collectors.toList());
System.out.println(distinctStrings);
// output
[one, two, three]
limit
limit 操作会截断原始流,返回最多只包含给定数量个元素的新流
List<String> stringList = new ArrayList<String>();
stringList.add("one");
stringList.add("two");
stringList.add("three");
stringList.add("one");
Stream<String> stream = stringList.stream();
stream.limit(2)
.forEach( element -> System.out.println(element));
// output
one
two
peek
peek() 方法是一个以 Consumer (java.util.function.Consumer,Consumer 代表的是消费元素但不返回任何值的方法) 作为参数的中间操作,它返回的流与原始流相同。当原始流中的元素开始迭代时,会调用 peek 方法中指定的 Consumer 实现对元素进行处理。
正如 peek 操作名称的含义一样,peek() 方法的目的是查看流中的元素,而不是转换它们。跟其他中间操作的方法一样,peek() 方法不会启动流中元素的内部迭代,流需要一个终结操作才能开始内部元素的迭代。
peek() 方法在流处理的 DEBUG 上的应用甚广,比如我们可以利用 peek() 方法输出流的中间值,方便我们的调试。
Stream.of("one", "two", "three","four").filter(e -> e.length() > 3)
.peek(e -> System.out.println("Filtered value: " + e))
.map(String::toUpperCase)
.peek(e -> System.out.println("Mapped value: " + e))
.collect(Collectors.toList());
// output: 输出调试信息
Filtered value: three
Mapped value: THREE
Filtered value: four
Mapped value: FOUR
3.流的终结操作
Stream 的终结操作通常会返回单个值,一旦一个 Stream 实例上的终结操作被调用,流内部元素的迭代以及流处理调用链上的中间操作就会开始执行,当迭代结束后,终结操作的返回值将作为整个流处理的返回值被返回。
案例分析
long count = stream
.map((value) -> value.toLowerCase())
.map((value) -> value.toUpperCase())
.map((value) -> value.substring(0,3))
.count();
// Stream 的终结操作 count() 被调用后整个流处理开始执行,最后将 count() 的返回值作为结果返回,结束流操作的执行。这也是为什么把他们命名成流的终结操作的原因
anyMatch
anyMatch() 方法以一个 Predicate (java.util.function.Predicate 接口,它代表一个接收单个参数并返回参数是否匹配的函数)作为参数,启动 Stream 的内部迭代,并将 Predicate 参数应用于每个元素。如果 Predicate 对任何元素返回了 true(表示满足匹配),则 anyMatch() 方法的结果返回 true。如果没有元素匹配 Predicate,anyMatch() 将返回 false
public class StreamAnyMatchExample {
public static void main(String[] args) {
List<String> stringList = new ArrayList<String>();
stringList.add("One flew over the cuckoo's nest");
stringList.add("To kill a muckingbird");
stringList.add("Gone with the wind");
Stream<String> stream = stringList.stream();
boolean anyMatch = stream.anyMatch((value) -> value.startsWith("One"));
System.out.println(anyMatch);
}
}
// output
true
allMatch
allMatch() 方法同样以一个 Predicate 作为参数,启动 Stream 中元素的内部迭代,并将 Predicate 参数应用于每个元素。如果 Predicate 为 Stream 中的所有元素都返回 true,则 allMatch() 的返回结果为 true。如果不是所有元素都与 Predicate 匹配,则 allMatch() 方法返回 false
public class StreamAllMatchExample {
public static void main(String[] args) {
List<String> stringList = new ArrayList<String>();
stringList.add("One flew over the cuckoo's nest");
stringList.add("To kill a muckingbird");
stringList.add("Gone with the wind");
Stream<String> stream = stringList.stream();
boolean allMatch = stream.allMatch((value) -> value.startsWith("One"));
System.out.println(allMatch);
}
}
// output
false
noneMatch
Match 系列里还有一个 noneMatch 方法,顾名思义,如果流中的所有元素都与作为 noneMatch 方法参数的 Predicate 不匹配,则方法会返回 true,否则返回 false
public class StreamNoneExample {
public static void main(String[] args) {
List<String> stringList = new ArrayList<>();
stringList.add("abc");
stringList.add("def");
Stream<String> stream = stringList.stream();
boolean noneMatch = stream.noneMatch((element) -> {
return "xyz".equals(element);
});
System.out.println("noneMatch = " + noneMatch); //输出 noneMatch = true
}
}
collect
collect() 方法被调用后,会启动元素的内部迭代,并将流中的元素收集到集合或对象中
public class StreamCollectExample {
public static void main(String[] args) {
List<String> stringList = new ArrayList<>();
stringList.add("One flew over the cuckoo's nest");
stringList.add("To kill a muckingbird");
stringList.add("Gone with the wind");
Stream<String> stream = stringList.stream();
List<String> stringsAsUppercaseList = stream
.map(value -> value.toUpperCase())
.collect(Collectors.toList());
System.out.println(stringsAsUppercaseList);
}
}
// output
[ONE FLEW OVER THE CUCKOO'S NEST, TO KILL A MUCKINGBIRD, GONE WITH THE WIND]
collect() 方法将收集器 -- Collector (java.util.stream.Collector) 作为参数。在上面的示例中,使用的是 Collectors.toList() 返回的 Collector 实现。这个收集器把流中的所有元素收集到一个 List 中去
count
count() 方法调用后,会启动 Stream 中元素的迭代,并对元素进行计数
public class StreamCountExample {
public static void main(String[] args) {
List<String> stringList = new ArrayList<String>();
stringList.add("One flew over the cuckoo's nest");
stringList.add("To kill a muckingbird");
stringList.add("Gone with the wind");
Stream<String> stream = stringList.stream();
long count = stream.flatMap((value) -> {
String[] split = value.split(" ");
return Arrays.asList(split).stream();
}).count();
System.out.println("count = " + count); // count = 14
}
}
// output
count = 14
首先创建一个字符串 List ,然后获取该 List 的 Stream,为其添加了 flatMap() 和 count() 操作。 count() 方法调用后,流处理将开始迭代 Stream 中的元素,处理过程中字符串元素在 flatMap() 操作中被拆分为单词、合并成一个由单词组成的 Stream,然后在 count() 中进行计数。所以最终打印出的结果是 count = 14
findAny
findAny() 方法可以从 Stream 中找到单个元素。找到的元素可以来自 Stream 中的任何位置。且它不提供从流中的哪个位置获取元素的保证
public class StreamFindAnyExample {
public static void main(String[] args) {
List<String> stringList = new ArrayList<>();
stringList.add("one");
stringList.add("two");
stringList.add("three");
stringList.add("one");
Stream<String> stream = stringList.stream();
Optional<String> anyElement = stream.findAny();
if (anyElement.isPresent()) {
System.out.println(anyElement.get());
} else {
System.out.println("not found");
}
}
}
// output
one
findAny() 方法会返回一个 Optional,意味着 Stream 可能为空,因此没有返回任何元素。我们可以通过 Optional 的 isPresent() 方法检查是否找到了元素(Optional 类是一个可以为 null 的容器对象。如果值存在则 isPresent() 方法会返回true,调用get()方法会返回容器中的对象,否则抛出异常:NoSuchElementException)
findFirst
findFirst() 方法将查找 Stream 中的第一个元素,跟 findAny() 方法一样,也是返回一个 Optional,我们可以通过 Optional 的 isPresent() 方法检查是否找到了元素
public class StreamFindFirstExample {
public static void main(String[] args) {
List<String> stringList = new ArrayList<>();
stringList.add("one");
stringList.add("two");
stringList.add("three");
stringList.add("one");
Stream<String> stream = stringList.stream();
Optional<String> anyElement = stream.findFirst();
if (anyElement.isPresent()) {
System.out.println(anyElement.get());
} else {
System.out.println("not found");
}
}
}
// output
one
forEach
forEach() 方法我们在介绍 Collection 的迭代时介绍过,当时主要是拿它来迭代 List 的元素。它会启动 Stream 中元素的内部迭代,并将 Consumer (java.util.function.Consumer, 一个函数式接口,上面介绍过) 应用于 Stream 中的每个元素。 注意 forEach() 方法的返回值是 void
public class StreamForEachExample {
public static void main(String[] args) {
List<String> stringList = new ArrayList<String>();
stringList.add("one");
stringList.add("two");
stringList.add("three");
Stream<String> stream = stringList.stream();
stream.forEach(System.out::println);
}
}
// output
one
two
three
min
min() 方法返回 Stream 中的最小元素。哪个元素最小是由传递给 min() 方法的 Comparator 接口实现来确定的
public class StreamMinExample {
public static void main(String[] args) {
List<String> stringList = new ArrayList<>();
stringList.add("abc");
stringList.add("def");
Stream<String> stream = stringList.stream();
// 作为 min 方法参数的Lambda 表达式可以简写成 String::compareTo
// Optional<String> min = stream.min(String::compareTo);
Optional<String> min = stream.min((val1, val2) -> {
return val1.compareTo(val2);
});
String minString = min.get();
System.out.println(minString); // abc
}
}
// output
abc
min() 方法返回的是一个 Optional ,也就是它可能不包含结果。如果为空,直接调用 Optional 的 get() 方法将抛出 异常--NoSuchElementException。比如我们把上面的 List 添加元素的两行代码注释掉后,运行程序就会报
Exception in thread "main" java.util.NoSuchElementException: No value present
at java.util.Optional.get(Optional.java:135)
at com.example.StreamMinExample.main(StreamMinExample.java:21)
所以最好先用 Optional 的 ifPresent() 判断一下是否包含结果,再调用 get() 获取结果
max
与 min() 方法相对应,max() 方法会返回 Stream 中的最大元素,max() 方法的参数和返回值跟 min() 方法的也都一样,只需要把上面求最小值的方法替换成求最大值的方法 max() 即可
Optional<String> min = stream.max(String::compareTo);
reduce
reduce() 方法,是 Stream 的一个聚合方法,它可以把一个 Stream 的所有元素按照聚合函数聚合成一个结果。reduce()方法接收一个函数式接口 BinaryOperator 的实现,它定义的一个apply()方法,负责把上次累加的结果和本次的元素进行运算,并返回累加的结果
public class StreamReduceExample {
public static void main(String[] args) {
List<String> stringList = new ArrayList<>();
stringList.add("One flew over the cuckoo's nest");
stringList.add("To kill a muckingbird");
stringList.add("Gone with the wind");
Stream<String> stream = stringList.stream();
Optional<String> reduced = stream.reduce((value, combinedValue) -> combinedValue + " + " + value);
// 写程序的时候记得别忘了 reduced.ifPresent() 检查结果里是否有值
System.out.println(reduced.get());
}
}
// output
Gone with the wind + To kill a muckingbird + One flew over the cuckoo's nest
reduce() 方法的返回值同样是一个 Optional 类的对象,所以在获取值前别忘了使用 ifPresent() 进行检查。
streadm 实现了多个版本的reduce() 方法,还有可以直接返回元素类型的版本,比如使用 reduce 实现整型Stream的元素的求和
public class IntegerStreamReduceSum {
public static void main(String[] args) {
List<Integer> intList = new ArrayList<>();
intList.add(10);
intList.add(9);
intList.add(8);
intList.add(7);
Integer sum = intList.stream().reduce(0, Integer::sum);
System.out.printf("List 求和,总和为%s\n", sum);
}
}
// output
List 求和,总和为34
toArray
toArray() 方法是一个流的终结操作,它会启动流中元素的内部迭代,并返回一个包含所有元素的 Object 数组
List<String> stringList = new ArrayList<String>();
stringList.add("One flew over the cuckoo's nest");
stringList.add("To kill a muckingbird");
stringList.add("Gone with the wind");
Stream<String> stream = stringList.stream();
// 方法1
Object[] objects = stream.toArray();
// 方法2(允许传入指定类型数组的构造方法,比如用 toArray 把流中的元素收集到字符串数组中)
String[] strArray = stream.toArray(String[]::new);
4.流的拼接
Java 的Stream 接口包含一个名为 concat() 的静态方法,它可以将两个流连接成一个
import java.util.ArrayList;
import java.util.List;
import java.util.stream.Collectors;
import java.util.stream.Stream;
public class StreamConcatExample {
public static void main(String[] args) {
List<String> stringList = new ArrayList<>();
stringList.add("One flew over the cuckoo's nest");
stringList.add("To kill a muckingbird");
stringList.add("Gone with the wind");
Stream<String> stream1 = stringList.stream();
List<String> stringList2 = new ArrayList<>();
stringList2.add("Lord of the Rings");
stringList2.add("Planet of the Rats");
stringList2.add("Phantom Menace");
Stream<String> stream2 = stringList2.stream();
Stream<String> concatStream = Stream.concat(stream1, stream2);
List<String> stringsAsUppercaseList = concatStream
.collect(Collectors.toList());
System.out.println(stringsAsUppercaseList);
}
}
从数组创建流
上面关于 Stream 的例子都是从 Collection 实例的 stream() 方法获取的集合包含的所有元素的流,除了这种方法之外,Java 的 Stream 接口中提供了一个名为 of 的静态方法,能支持从单个,多个对象或者数组对象快速创建流
import java.util.stream.Stream;
public class StreamExamples {
public static void main(String[] args) {
Stream<String> stream1 = Stream.of("one", "two", "three");
Stream<String> stream2 = Stream.of(new String[]{"one", "two"});
System.out.println(stream1.count()); // 输出3
System.out.println(stream2.count()); // 输出2
}
}
5.项目中高频使用的StreamAPI操作
场景1:求两个对象List的交集/差集
// 定义对象
class Person {
String id;
String nickName;
public Person(String id, String nickName) {
this.id = id;
this.nickName = nickName;
}
@Override
public String toString() {
return "A{" +
"id='" + id + '\'' +
", nickName='" + nickName + '\'' +
'}';
}
public String getId() {
return id;
}
public String getNickName() {
return nickName;
}
}
求交集
/**
* 场景1:求两个对象List的交集/差集
*/
public class GetObjectListIntersection {
public static void main(String[] args) {
List<Person> aList = new ArrayList<>(Arrays.asList(
new Person("1", "张三"),
new Person("2", "李四"),
new Person("3", "王五")
));
List<Person> bList = new ArrayList<>(Arrays.asList(
new Person("2", "李四"),
new Person("3", "王五"),
new Person("4", "赵六")
));
// aList 与 bList 的交集 (在两个集合中都存在的元素)
List<Person> intersections = aList
.stream() //获取第一个集合的Stream1
.filter( //取出Stream1中符合条件的元素组成新的Stream2,lambda表达式1返回值为true时为符合条件
a -> //lambda表达式1,a为lambda表达式1的参数,是Stream1中的每个元素
bList.stream() //获取第二个集合的Stream3
.map(Person::getId) //将第二个集合每个元素的id属性取出来,映射成新的一个Stream4
.anyMatch( //返回值(boolean):Stream4中是否至少有一个元素使lambda表达式2返回值为true
id -> //lambda表达式2,id为lambda表达式2的参数,是Stream4中的每个元素
Objects.equals(a.getId(), id) //判断id的值是否相等
)
)
.collect(Collectors.toList()); //将Stream2转换为List
System.out.println("----------aList 与 bList 的交集为:");
System.out.println(intersections);
}
}
// output
----------aList 与 bList 的交集为:
[A{id='2', nickName='李四'}, A{id='3', nickName='王五'}]
求差集
List<Person> differences = bList.
stream().
filter(
b ->
aList.stream()
.map(Person::getId)
.noneMatch(
id ->
Objects.equals(b.getId(), id)
)
).collect(Collectors.toList());
System.out.println("----------bList 与 aList 的差集为:");
System.out.println(differences);
// 求差集(优化高效版)
Map<String, A> aMap = aList.stream().collect(Collectors.toMap(A::getId, Function.identity())) ;
List<A> diffEffective = bList.stream().filter(b -> !aMap.containsKey(b.getId())).collect(Collectors.toList());
System.out.println("----------bList 与 aList 的差集为:");
System.out.println(diffEffective);
场景2:Stream中排序对象集合
class User {
private Long userId;
private String name;
private Integer age;
private Double salary;
public Long getUserId() {
return userId;
}
public void setUserId(Long userId) {
this.userId = userId;
}
public User(Long userId, String name, Integer age, Double salary) {
this.userId = userId;
this.name = name;
this.age = age;
this.salary = salary;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
public double getSalary() {
return salary;
}
public void setSalary(double salary) {
this.salary = salary;
}
}
正序排序
Stream API 里有一个 sorted 方法,通过该方法就能Stream 元素按照对象的指定的字段进行排序
public class GetSortObjDemo {
public static void main(String[] args) {
List<User> userList = Arrays.asList(new User(1000L, "First", 25, 30000D),
new User(2000L, "Second", 30, 45000D),
new User(3000L, "Third", 35, 25000D));
// 使用字段包装类型的 compareTo 进行排序, 这个方式可以简化成下面的使用 Comparator.comparing 的例子
userList.stream().sorted(
(p1, p2) ->
(p1.getUserId().compareTo(p2.getUserId()))
)
.forEach(user -> System.out.println(user.getName()));
}
}
Stream 的 sorted 方法内部,这个例子使用的是Person
对象personId
字段的包装类型实现的compareTo
方法进行的比较。 这个方式可以进一步简化成下面的使用Comparator.comparing
方法的方式
personList.stream()
.sorted(
Comparator.comparing(Person::getPersonId)
).forEach(person -> System.out.println(person.getName()));
Comparator.comparing()
该方法是一个泛型方法,会根据参数的类型,内部调用相应类型实现的CompareTo
方法进行两个参数的比较。
上面的例子是把Stream
里的元素按照Person
对象personId
的正序进行排序,运行例程后会看到下面的输出,按照personId
的正序输出了每个Person
对象的名字
倒序排序
如果需要按照倒序对Stream
中的元素进行排列,可以在Comparator.comparing()
后面追加reversed()
调用
personList.stream().sorted(
Comparator.comparing(
Person::getPersonId).reversed()
).forEach(person -> System.out.println(person.getName()));
多个字段排序
在Stream
的sorted
步骤里使用Comparator.comparing()
的好处是,除了倒序排序非常方便,我们还可以根据多字段进行排--比如先按Person
对象的personId
字段,如果再按Person
对象的年龄age
字段进行排序。这个时候可以在Comparator.comparaing
调用后,再接上thenComparing
调用,实现元素按照多字段进行排序的功能。
public static void main(String[] args) {
List<Person> personList = Arrays.asList(new Person(1000L, "First", 25, 30000D),
new Person(1000L, "Second", 22, 45000D),
new Person(3000L, "Third", 35, 25000D));
// 先按personId 排序,再按 age 排序
personList.stream().sorted(
Comparator.comparing(Person::getPersonId)
.thenComparing(Person::getAge)
).forEach(person -> System.out.println(person.getName()));
场景3:在Stream 迭代中使用元素索引
在 Stream 操作中没办法像 For 循环那样拿到当前迭代的元素的自然索引,虽然 Java 11 里有办法可以当前迭代元素的自然索引,因为国内大部分公司的 JDK 版本还是 8 ,所以只能另外用其他办法。
有另外一个办法,程序可以借助AtomicInteger
的获取并自增方法getAndIncrement
方法实现在 Stream 迭代中拿到当前迭代元素的自然索引的效果,看下面这个示例程序
import java.util.Arrays;
import java.util.concurrent.atomic.AtomicInteger;
// Java8 里没办法直接访问 stream 里的元素,不过可以用下面这种方式实现
public class AtomicIntegerAsStreamIndices {
public static void main(String[] args)
{
String[] array = { "A", "B", "C", "D" };
AtomicInteger index = new AtomicInteger();
Arrays.stream(array)
.map(str -> index.getAndIncrement() + " -> " + str)
.forEach(System.out::println);
}
}
// output
0 -> A
1 -> B
2 -> C
3 -> D
场景4:把对象 List 转换为对象 Map
一般为了提高程序效率的时候,我们会把对象 List 转换成以对象 Id 为 Key 的对象 Map。用 Stream 我们能简单便捷的把 List 转换成 Map ,免去了在程序里写两层循环的窘境。
class Animal {
private int id;
private String name;
public Animal(int id, String name) {
this.id = id;
this.name = name;
}
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
public class ListStreamToMap {
public static void main(String[] args) {
List<Animal> aList = new ArrayList<>();
aList.add(new Animal(1, "Elephant"));
aList.add(new Animal(2, "Bear"));
Map<Integer, Animal> map = aList.stream()
.collect(Collectors.toMap(Animal::getId, Function.identity()));
map.forEach((integer, animal) -> {
System.out.println(animal.getName());
});
}
}
Optional类
NPE(NullPointerException)
场景分析
在对一个对象进行非空判断,对比传统方式和Optional
// 定义会员类
class Member {
private String name;
Member(String name) {this.name = name;}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
class WithoutOptionalDemo {
// 定义一个方法模拟获取会员信息
public static Member getMemberByIdFromDB() {
// 当前 ID 的会员不存在
return null;
}
public static void main(String[] args) {
// 传统方式NPE判断
Member mem = getMemberByIdFromDB();
if (mem != null) {
System.out.println(mem.getName());
}
}
}
如果代码中没有对member对象做null校验,则在访问该对象的时候就会报NPE,而在程序设计过程中比较让人头痛的就是这个问题。Java8提供的Optional则是为了解决NPE应运而生
class WithOptionalDemo {
public static Optional<Member> getMemberByIdFromDB(boolean hasName) {
if (hasName) {
return Optional.of(new Member("noob"));
}
return Optional.empty();
}
public static void main(String[] args) {
// 通过Optional处理NPE
Optional<Member> member = getMemberByIdFromDB(false);
member.ifPresent((m)->{System.out.println("会员姓名:" + m.getName());});
}
}
从上面的代码可以看到,通过控制hasName参数来模拟会员是否存在,hasName为true会员存在则默认返回一个会员信息。如果hasName为false,则会员信息不存在,此时返回一个Optional.empty()。
而在调用的时候则是通过Optional去接收对象,随后通过Optional提供的ifPresent方法来进一步对对象进行操作。
1.Optional语法规则
创建Optional对象
// 1.使用静态方法 empty() 创建一个空的 Optional 对象
Optional<String> emptyObj = Optional.empty();
System.out.println(emptyObj);
// 2.使用静态方法 of() 创建一个非空的 Optional 对象
Optional<Member> member = Optional.of(new Member("noob"));
System.out.println(member);
// 如果使用of()方法,传递的参数必须非空(如果传递参数为null还是会抛出NPE)
// Optional<Member> nullMember = Optional.of(null);// 错误-抛出NPE
// 3.使用ofNullable() 创建一个即可空又可非空的 Optional 对象
String name = null;
Optional<String> optOrNull = Optional.ofNullable(name);
System.out.println(optOrNull); // 输出:Optional.empty
判断值是否存在
通过方法 isPresent()
判断一个 Optional 对象是否存在,如果存在,该方法返回 true,否则返回 false——取代了 obj != null
的判断
// 判断值是否存在
Optional<String> opt = Optional.of("noob");
System.out.println(opt.isPresent()); // 输出:true
Optional<String> optOrNullObj = Optional.ofNullable(null);
System.out.println(optOrNullObj.isPresent()); // 输出:false
Java11之后可以通过方法 isEmpty()
判断与 isPresent()
相反的结果
System.out.println(opt.isEmpty());
非空表达式
Optional 类有一个非常现代化的方法——ifPresent()
,允许开发者使用函数式编程的方式执行一些代码,称为非空表达式。如果没有该方法的话,我们通常需要先通过 isPresent()
方法对 Optional 对象进行判空后再执行相应的代码
// 传统方式:判断对象是否存在,然后执行操作
Optional<String> optOrNull = Optional.ofNullable(null);
if (optOrNull.isPresent()) {
System.out.println(optOrNull.get().length());
}
// 引入非空表达式(上面的实现可以简化为下述形式)
Optional<String> optOrNull = Optional.ofNullable(null);
optOrNull.ifPresent(str -> System.out.println(str.length()));
// Java9之后通过方法 ifPresentOrElse(action, emptyAction) 执行两种结果,非空时执行 action,空时执行 emptyAction
Optional<String> opt = Optional.of("noob");
opt.ifPresentOrElse(str -> System.out.println(str.length()), () -> System.out.println("为空"));
设置(获取)默认值
有时候,在创建(获取) Optional 对象的时候,需要一个默认值,orElse()
和 orElseGet()
方法就派上用场了。orElse()
方法用于返回包裹在 Optional 对象中的值,如果该值不为 null,则返回;否则返回默认值。该方法的参数类型和值的类型一致。
String nullName = null;
String name = Optional.ofNullable(nullName).orElse("noob");
System.out.println(name); // 输出:noob
orElseGet()
方法与 orElse()
方法类似,但参数类型不同。如果 Optional 对象中的值为 null,则执行参数中的函数。
String nullName = null;
String name = Optional.ofNullable(nullName).orElseGet(()->"noob");
System.out.println(name); // 输出:noob
获取值
直观从语义上来看,get()
方法才是最正宗的获取 Optional 对象值的方法,但很遗憾,该方法是有缺陷的,因为假如 Optional 对象的值为 null,该方法会抛出 NoSuchElementException 异常。这完全与我们使用 Optional 类的初衷相悖
public class GetOptionalDemo {
public static void main(String[] args) {
String name = null;
Optional<String> optOrNull = Optional.ofNullable(name);
System.out.println(optOrNull.get());
}
}
Exception in thread "main" java.util.NoSuchElementException: No value present
at java.base/java.util.Optional.get(Optional.java:141)
at com.cmower.dzone.optional.GetOptionalDemo.main(GetOptionalDemo.java:9)
尽管抛出的异常是 NoSuchElementException 而不是 NPE,但在我们看来,显然是在“五十步笑百步”。建议 orElseGet()
方法获取 Optional 对象的值
过滤值
filter方法用于过滤
public class FilterOptionalDemo {
public static void main(String[] args) {
String password = "12345";
Optional<String> opt = Optional.ofNullable(password);
System.out.println(opt.filter(pwd -> pwd.length() > 6).isPresent());
}
}
filter()
方法的参数类型为 Predicate(Java 8 新增的一个函数式接口),也就是说可以将一个 Lambda 表达式传递给该方法作为条件,如果表达式的结果为 false,则返回一个 EMPTY 的 Optional 对象,否则返回过滤后的 Optional 对象。
在上例中,由于 password 的长度为 5 ,所以程序输出的结果为 false。假设密码的长度要求在 6 到 10 位之间,那么还可以再追加一个条件。
Predicate<String> len6 = pwd -> pwd.length() > 6;
Predicate<String> len10 = pwd -> pwd.length() < 10;
password = "1234567";
opt = Optional.ofNullable(password);
boolean result = opt.filter(len6.and(len10)).isPresent();
System.out.println(result);
转化值
map()
方法:可以按照一定的规则将原有 Optional 对象转换为一个新的 Optional 对象,原有的 Optional 对象不会更改。
public class OptionalMapDemo {
public static void main(String[] args) {
String name = "noob";
Optional<String> nameOptional = Optional.of(name);
Optional<Integer> intOpt = nameOptional
.map(String::length);
System.out.println( intOpt.orElse(0));
}
}
在上面这个例子中,map()
方法的参数 String::length
,意味着要 将原有的字符串类型的 Optional 按照字符串长度重新生成一个新的 Optional 对象,类型为 Integer。
把 map()
方法与 filter()
方法结合起来用,前者用于将密码转化为小写,后者用于判断长度以及是否是“password”。
public class OptionalMapFilterDemo {
public static void main(String[] args) {
String password = "password";
Optional<String> opt = Optional.ofNullable(password);
Predicate<String> len6 = pwd -> pwd.length() > 6;
Predicate<String> len10 = pwd -> pwd.length() < 10;
Predicate<String> eq = pwd -> pwd.equals("password");
boolean result = opt.map(String::toLowerCase).filter(len6.and(len10 ).and(eq)).isPresent();
System.out.println(result);
}
}