Java 8的常用特性、API
# 简介
早在19年的时候,Oracle就宣布JDK的许可证将发生改变。从那个时候就有了两种许可证,分别是BCL(2019年4月16日前的JDK)和OTN,前者是完全免费的,后者是商用付费的。为此,Oracle的解决方案是提供了两种JDK,一个是Oracle OpenJDK releases(GPL V2),另一个是Oracle Java SE product releases(BCL/OTN)。简单来说,如果想使用较新版本的JDK只能选择Oracle OpenJDK (opens new window)。还有一个选择就是自己获取OpenJDK源码自行编译。另外,使用阿里等其他公司维护的第三方OpenJDK也不失为为一个方案。这里附上阿里维护的Alibaba Dragonwell JDK (opens new window)。如果非得选择Oracle Java SE下的JDK只能选择在2019年4月16日前发布的JDK,建议使用前确认一下Oracle JDK FAQ https://www.oracle.com/java/technologies/javase/jdk-faqs.html (opens new window)。
废话不多说,直接开始介绍Java1.8几个常用的特性。
# Lambda表达式
Lambda 表达式(lambda expression)是Java 8的一个大亮点,Lambda表达式基于数学中的λ演算 (opens new window)得名,直接对应于其中的lambda抽象(lambda abstraction). Lambda 允许把函数作为一个方法的参数(函数作为参数传递到方法中),这里的函数是一个匿名函数,即没有函数名的函数。Lambda表达式可以表示闭包 (opens new window)(注意和数学传统意义上的不同)。使用Lambda表达式可以使代码变得十分简洁。
lambda表达式的重要特征
- 可选类型声明: 不需要声明参数类型,编译器可以统一识别参数值。
- 可选的参数圆括号: 一个参数无需定义圆括号,但多个参数需要定义圆括号。
- 可选的大括号: 如果主体包含了一个语句,就不需要使用大括号。
- 可选的返回关键字: 如果主体只有一个表达式返回值则编译器会自动返回值,大括号需要指定明表达式返回了一个数值。
举个例子
// 没有声明e的类型 一个参数(省略圆括号) 一个语句(省略大括号) 一个表达式自动返回 e.getMessage()的结果
e -> e.getMessage()
2
这种用法是最为常见的。
lambda 表达式只能引用标记了 final 的外层局部变量,这就是说不能在 lambda 内部修改定义在域外的局部变量,否则会编译错误。
lambda 表达式的局部变量可以不用声明为 final,但是必须不可被后面的代码修改(即隐性的具有 final 的语义)
Lambda 表达式当中不允许声明一个与局部变量同名的参数或者局部变量。
# 方法引用
方法引用通过使用::
来引用一个方法,eg. Class/instance::method
。
- 构造器引用(
User::new
) - 静态方法引用
- 特定类的任意对象的方法引用
- 特定对象的方法引用
编写一个简单的例子
new ArrayList<String>(){{add("1");add("2");add("3");}}.forEach(System.out::println);
# 函数式接口
函数式接口指的是该接口有且仅有一个抽象方法
函数式接口基本上用于配合lambda表达式
使用@FunctionalInterface
声明该接口为函数式接口
@FunctionalInterface
public interface Runnable {
/**
* When an object implementing interface <code>Runnable</code> is used
* to create a thread, starting the thread causes the object's
* <code>run</code> method to be called in that separately executing
* thread.
* <p>
* The general contract of the method <code>run</code> is that it may
* take any action whatsoever.
*
* @see java.lang.Thread#run()
*/
public abstract void run();
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
可以看到Runnable接口就是函数式接口
好处就在于可以直接使用lambda表达式实现该接口,不再需要通过匿名内部类
匿名内部类是这样的
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
}
});
2
3
4
5
6
lambda expression方式
Thread thread = new Thread(()->{
});
2
3
下面通过一个例子来看如果不是FunctionalInterface的话会发生什么
interface Phone {
void brand(String brand);
void name(String name);
}
class Human{
Human(Phone phone){
System.out.println("play phone...");
}
}
Human human = new Human(new Phone() {
@Override
public void brand(String brand) {
}
@Override
public void name(String name) {
}
});
// 写成这种将会编译错误
// Multiple non-overriding abstract methods found in interface xxx.xxx.xxxx.Class.Phone
// Human human = new Human(()->{});
// 而如果删掉Phone接口的一个方法,使其满足有且仅有一个抽象方法,则可以直接使用lambda expression
// Human human = new Human(brand->{});
// @FunctionalInterface不是必需的。声明该注解的作用在于如果该接口中出现两个或以上抽象方法时会产生编译错误
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
# Stream API
相信Stream API已经并不少见了,写本文的出发点也是因为Stream API,因为经常看到别人使用Stream API初始化容器。
那么问题就来了,什么时stream?
stream是一个非常抽象的概念。可以先简单理解成来自数据源的元素队列并支持聚合操作
了解字节流的概念的话应该不难理解
两个基础特征
- Pipelining: 中间操作都会返回流对象本身。 这样多个操作可以串联成一个管道, 如同流式风格(fluent style)。 这样做可以对操作进行优化, 比如延迟执行(laziness)和短路( short-circuiting)。
- 内部迭代: 以前对集合遍历都是通过Iterator或者For-Each的方式, 显式的在集合外部进行迭代, 这叫做外部迭代。 Stream提供了内部迭代的方式, 通过访问者模式(Visitor)实现。
# 流的生成
在Java 8 中,集合对象可以通过 stream() 或者 parallelStream 为集合创建流,区别在于前者是串行流,后者是并行流。
List<String> filtered = Arrays.asList("abc", "", "bc", "efg", "abcd","", "jkl").stream().filter(string -> !string.isEmpty()).collect(Collectors.toList());
通过stream+lambda,我们竟可以如此优雅的实现List的筛选!
# forEach
Stream 提供了新的方法 forEach
来迭代流中的每个数据。以下代码片段使用 forEach 输出了10个随机数:
new Random().ints().limit(10).forEach(System.err::println);
# map
map 方法用于映射每个元素到对应的结果,以下代码片段使用 map 输出了元素对应的平方数:
// 获取对应的平方数 distinct:去重
List<Integer> squaresList = Arrays.asList(3, 2, 2, 3, 7, 3, 5).stream().map( i -> i*i).distinct().collect(Collectors.toList());
2
# filter
filter 方法用于通过设置的条件过滤出元素。
// 获取空字符串的数量
Arrays.asList("abc", "", "bc", "efg", "abcd","", "jkl").stream().filter(string -> string.isEmpty()).count();
2
# limit
limit 方法用于获取指定数量的流。例子之前foreach已经展示过了
# sorted
sorted 方法用于对流进行排序。
new Random().ints().limit(10).sorted().forEach(System.err::println);
sort list
// sort by desc
new ArrayList<Integer>(){{
add(1);
add(2);
add(3);
add(4);
}}.stream().sorted(Comparator.reverseOrder()).forEach(System.out::println);
2
3
4
5
6
7
sort map
// sort by key
map.entrySet().stream().sorted(Map.Entry.comparingByKey()).forEach(e -> System.out.println(e.getKey() + " " + e.getValue()));
// sort by key desc
map.entrySet().stream().sorted(Map.Entry.comparingByKey(Comparator.reverseOrder())).forEach(e -> System.out.println(e.getKey() + " " + e.getValue()));
// sort by value
map.entrySet().stream().sorted(Map.Entry.comparingByValue()).forEach(e -> System.out.println(e.getKey() + " " + e.getValue()));
// sort by value desc
map.entrySet().stream().sorted(Map.Entry.comparingByValue(Comparator.reverseOrder())).forEach(e -> System.out.println(e.getKey() + " " + e.getVal
2
3
4
5
6
7
8
# Collectors
Collectors 类实现了很多归约操作,例如将流转换成集合和聚合元素。Collectors 可用于返回列表或字符串。
List<String>strings = Arrays.asList("abc", "", "bc", "efg", "abcd","", "jkl");
// 转成集合
List<String> filtered = strings.stream().filter(string -> !string.isEmpty()).collect(Collectors.toList());
// 转成字符串
String mergedString = strings.stream().filter(string -> !string.isEmpty()).collect(Collectors.joining(", "));
2
3
4
5
# 统计
一些产生统计结果的收集器也非常有用。它们主要用于int、double、long等基本类型上,它们可以用来产生类似如下的统计结果。
List<Integer> numbers = Arrays.asList(3, 2, 2, 3, 7, 3, 5);
IntSummaryStatistics stats = numbers.stream().mapToInt((x) -> x).summaryStatistics();
System.out.println("列表中最大的数 : " + stats.getMax());
System.out.println("列表中最小的数 : " + stats.getMin());
System.out.println("所有数之和 : " + stats.getSum());
System.out.println("平均数 : " + stats.getAverage());
2
3
4
5
6
7
8
用过的都说好!
# Date Time API
Java 8通过发布新的Date Time API (JSR 310)来进一步加强对日期与时间的处理。
在旧版的 Java 中,日期时间 API 存在诸多问题,其中有:
- 非线程安全 − java.util.Date 是非线程安全的,所有的日期类都是可变的,这是Java日期类最大的问题之一。
- 设计很差 − Java的日期/时间类的定义并不一致,在java.util和java.sql的包中都有日期类,此外用于格式化和解析的类在java.text包中定义。java.util.Date同时包含日期和时间,而java.sql.Date仅包含日期,将其纳入java.sql包并不合理。另外这两个类都有相同的名字,这本身就是一个非常糟糕的设计。
- 时区处理麻烦 − 日期类并不提供国际化,没有时区支持,因此Java引入了java.util.Calendar和java.util.TimeZone类,但他们同样存在上述所有的问题。
Java 8 在 java.time 包下提供了很多新的 API。以下为两个比较重要的 API:
- Local(本地) − 简化了日期时间的处理,没有时区的问题。
- Zoned(时区) − 通过制定的时区处理日期时间。
新的java.time包涵盖了所有处理日期,时间,日期/时间,时区,时刻(instants),过程(during)与时钟(clock)的操作。
# 本地化日期时间 API
不需要考虑时区的情况。
LocalDateTime currentTime = LocalDateTime.now();
System.out.println(currentTime); // 2021-03-29T21:31:25.006189300
LocalDate localDate = currentTime.toLocalDate();
System.out.println(localDate); // 2021-03-29
LocalTime localTime = currentTime.toLocalTime();
System.out.println(localTime); // 21:31:25.006189300
// 类似获取年月日都可以猜到怎么调了
// 解析字符串 其他依此类推
LocalTime parseTime = LocalTime.parse("21:31:25");
// 21 小时 31 分钟
LocalTime date4 = LocalTime.of(21, 31);
// 29 March 2021
LocalDate date3 = LocalDate.of(2021, Month.MARCH, 29);
2
3
4
5
6
7
8
9
10
11
12
13
# 使用时区的日期时间API
如果我们需要考虑到时区,就可以使用时区的日期时间API。
// 获取当前时间日期
ZonedDateTime now = ZonedDateTime.now();
System.out.println(now); // 2021-03-29T21:40:22.618917600+08:00[Asia/Shanghai]
// 解析
ZonedDateTime zonedDateTime = ZonedDateTime.parse(now.toString());
// 时区ID
ZoneId zoneId = now.getZone();
ZoneId zoneId2 = ZoneId.of("Europe/Paris");
// 系统时区
ZoneId currentZoneId = ZoneId.systemDefault();
System.out.println(currentZoneId); // Asia/Shanghai
// 转换
ZonedDateTime.of(LocalDateTime.now(),ZoneId.systemDefault());
2
3
4
5
6
7
8
9
10
11
12
13
# Optional 类
主要用来处理一些可以预见到可能为null的情况,避免出现错误,注意Optionnal本身不为null,它应当永远指向Optional的实例。
在Optional的实例上使用对标识敏感的操作(包括引用相等(=)、标识哈希码或同步)可能会产生不可预知的结果,应该避免。
常见用法
// Optional.ofNullable - 允许传递为 null 参数
Optional<Integer> o1 = Optional.ofNullable(null);
// 不允许 会抛NullPointerException
// Optional<Object> o2 = Optional.of(null);
// isPresent 判断是否为null
if(o1.isPresent()){ // 此处为false
// 获取值,必须值存在
o1.get();
}
// 存在直接返回,不存在返回指定值
o1.orElse(0);
2
3
4
5
6
7
8
9
10
11
常见的就以上这些,还有一些其他api,但是见的比较少,所以没有列举。
值得一提的是,Optional类也支持Stream API。
# 最后
JDK 8的新特性其实远不止此,更多新特性可以查看 What is new in JDK 8 (opens new window)