1.前言
2019年9月19日java13已正式发布,感叹java社区强大,经久不衰。由于国内偏保守,新东西总要放一放,让其他人踩踩坑,等稳定了才会去用。并且企业目的还是赚钱,更不会因为一个新特性去重构代码,再开发一套程序出来。甚者国内大多传统企业还在用java4 、5、6…
2.java8 的新特性
Java 8 (又称为 jdk 1.8) 是 Java 语言开发的一个主要版本。Oracle 公司于 2014 年 3 月 18 日发布 Java 8 ,它支持函数式编程,新的日期 API,新的Stream API 等。是Java5之后一个大的版本升级,让Java语言和库仿佛获得了新生,核心新特性包含:
-
Java8 函数式接口− 函数式接口(Functional Interface)就是一个有且仅有一个抽象方法,但是可以有多个非抽象方法的接口。函数式接口可以被隐式转换为 lambda 表达式。
-
Lambda 表达式 − Lambda 允许把函数作为一个方法的参数(函数作为参数传递到方法中)。
-
Stream API −新添加的Stream API(java.util.stream) 把真正的函数式编程风格引入到Java中。
-
方法引用 − 方法引用提供了非常有用的语法,可以直接引用已有Java类或对象(实例)的方法或构造器。与lambda联合使用,方法引用可以使语言的构造更紧凑简洁,减少冗余代码。
-
Date Time API − 加强对日期与时间的处理。
-
Optional 类 − Optional 类已经成为 Java 8 类库的一部分,用来解决空指针异常。
3.新特性详细介绍
函数式接口
函数式接口(Functional Interface)就是一个有且仅有一个抽象方法,但是可以有多个非抽象方法的接口。
函数式接口可以被隐式转换为 lambda 表达式。
JDK 1.8 之前已有的函数式接口:
java.lang.Runnable
java.util.concurrent.Callable
java.security.PrivilegedAction
java.util.Comparator
java.io.FileFilter
java.nio.file.PathMatcher
java.lang.reflect.InvocationHandler
java.beans.PropertyChangeListener
java.awt.event.ActionListener
javax.swing.event.ChangeListener
JDK 1.8 新增加的函数接口:
java.util.function
这里说一下@FunctionalInterface
注解,这个注解是java8新出得一个注解。
我们常用的一些接口Callable、Runnable、Comparator等在JDK8中都添加@FunctionalInterface注解。
追踪源码查看@FunctionalInterface注解javadoc
javadoc,可以知道这个注解有以下特点:
该注解只能标记在”有且仅有一个抽象方法”的接口上。
JDK8接口中的静态方法和默认方法,都不算是抽象方法。
接口默认继承Java.lang.Object,所以如果接口显示声明覆盖了Object中方法,那么也不算抽象方法。
该注解不是必须的,如果一个接口符合”函数式接口”定义,那么加不加该注解都没有影响。加上该注解能够更好地让编译器进行检查。如果编写的不是函数式接口,但是加上了@FunctionInterface,那么编译器会报错。
@FunctionalInterface标记在接口上,“函数式接口”是指仅仅只包含一个抽象方法的接口。
package lynda.demo;
@FunctionalInterface
public interface TestInterface {
//抽象方法
public void test();
//java.lang.Object中的方法不是抽象方法
public boolean equals(Object obj);
//default不是抽象方法
public default void defaultMethod(){
}
//static方法不是抽象方法
public static void staticMethod(){
}
}
默认方法
简单说,默认方法就是接口可以有实现方法,而且不需要实现类去实现其方法。
我们只需在方法名前面加个 default 关键字即可实现默认方法。
为什么要有这个特性?
首先,之前的接口是个双刃剑,好处是面向抽象而不是面向具体编程,缺陷是,当需要修改接口时候,需要修改全部实现该接口的类,目前的 java 8 之前的集合框架没有 foreach 方法,通常能想到的解决办法是在JDK里给相关的接口添加新的方法及实现。然而,对于已经发布的版本,是没法在给接口添加新方法的同时不影响已有的实现。所以引进的默认方法。他们的目的是为了解决接口的修改与现有的实现不兼容的问题。
这里值得注意的是,我们知道java中一个类可以实现多个接口 如果多个接口中有同名同参同返回值得默认方法需要我们在实现类重写该方法,否则会编译报错。
lambda表达式
这是java8的一大重要特性,我们知道java是面向对象语言,把行为封装成一个对象是我们根深蒂固的java编程思想,但是lambda正好反其道而行之,是一种面向过程的编程思想。
不在赘述更多模糊,概念的含义,大家现在有这样的一个认识就可以了。下面我会用例子带入大家。
能够接收Lambda表达式的参数类型,是一个有且仅有一个抽象方法,但是可以有多个非抽象方法的接口。“函数接口”。可以顶替匿名内部类。
语法:
(parameters) -> expression或(parameters) ->{ statements; }
以下是lambda表达式的重要特征:
- 可选类型声明:不需要声明参数类型,编译器可以统一识别参数值。
- 可选的参数圆括号:一个参数无需定义圆括号,但多个参数需要定义圆括号。
- 可选的大括号:如果主体包含了一个语句,就不需要使用大括号。
- 可选的返回关键字:如果主体只有一个表达式返回值则编译器会自动返回值,大括号需要指定明表达式返回了一个数值。
比如:老版本Java中排列字符串
Collections.sort(names, new Comparator<String>() {
@Override
public int compare(String a, String b) {
return b.compareTo(a);
}
});
只需要给静态方法 Collections.sort 传入一个List对象以及一个比较器来按指定顺序排列。通常做法都是创建一个匿名的比较器对象然后将其传递给sort方法。
在Java 8 中你就没必要使用这种传统的匿名对象的方式了,直接上lambda
:
Collections.sort(names, (String a, String b) -> {
return b.compareTo(a);
});
看到了吧,代码变得更段且更具有可读性,但是实际上还可以写得更短:
Collections.sort(names, (String a, String b) -> b.compareTo(a));
对于函数体只有一行代码的,你可以去掉大括号{}以及return关键字,但是你还可以写得更短点:
Collections.sort(names, (a, b) -> b.compareTo(a));
据官方文档中介绍Java编译器可以自动推导出参数类型,可以不写,但是这里我还是建议大家写出参数类型,方便代码被其他人阅读时候的可读性。自己反过来查看代码也有帮助。
总结:
缺点 : 学习成本稍高,刚开始接触不容易理解,并需要反复练习。
优点 : lambda表达式让我们可以把一个方法当成参数传递进另一个方法,顶替匿名内部类消除了样板式代码。并让我们的代码看起来更加简洁、干净。
并且lambda表达式可以在结合很多地方使用。下面涉及我会再分析。总的来说lambda表达式还是值得我们学习的。
stream流
Java 8 API添加了一个新的抽象称为流Stream,可以让你以一种声明的方式处理数据。
Stream 使用一种类似用 SQL 语句从数据库查询数据的直观方式来提供一种对 Java 集合运算和表达的高阶抽象。
Stream API可以极大提高Java程序员的生产力,让程序员写出高效率、干净、简洁的代码。
这种风格将要处理的元素集合看作一种流, 流在管道中传输, 并且可以在管道的节点上进行处理, 比如筛选, 排序,聚合等。
元素流在管道中经过中间操作(intermediate operation)的处理,最后由最终操作(terminal operation)得到前面处理的结果。
什么是stream?
Stream(流)是一个来自数据源的元素队列并支持聚合操作。
元素是特定类型的对象,形成一个队列。Java中的Stream并不会存储元素,而是按需计算。
数据源 流的来源,可以是集合,数组,I/O channel, 产生器generator 等。
聚合操作 类似SQL语句一样的操作, 比如filter, map, reduce, find, match, sorted等。
和以前的Collection操作不同, Stream操作还有两个基础的特征:
Pipelining: 中间操作都会返回流对象本身。这样多个操作可以串联成一个管道, 如同流式风格(fluent style)。这样做可以对操作进行优化, 比如延迟执行(laziness)和短路( short-circuiting)。
内部迭代:以前对集合遍历都是通过Iterator或者For-Each的方式, 显式的在集合外部进行迭代, 这叫做外部迭代。Stream提供了内部迭代的方式, 通过访问者模式(Visitor)实现。
在 Java 8 中, 集合接口有两个方法来生成流:
stream() − 为集合创建串行流。
parallelStream() − 为集合创建并行流。
API的使用
forEach() :Stream 提供了新的方法 ‘forEach’ 来迭代流中的每个数据,以下代码片段使用 forEach 输出了list中的元素:
public void foreachDemo(List<String> strList) {
List<String> strList = Arrays.asList("ab","bc","de","fg","gh");
strList.forEach(e -> System.out.println("元素:" + e));
}
limit() 方法用于获取指定数量的流,以下代码片段使用 limit 方法打印出5 条数据:
public void limitDemo() {
List<String> strList = Arrays.asList("1","2","3","4","5","6","7","8");
strList.stream().limit(5).forEach(e -> System.out.println("元素:" + e));
}
执行结果
map() 方法用于映射每个元素到对应的结果, 以下代码片段使用 map 输出了元素对应的平方数:
public void mapDemo() {
List<Integer> list = Arrays.asList(1,2,2,3,4,5,5);
List<Integer> resultList = list.stream().map(i->i*i).collect(Collectors.toList());
System.out.println(resultList);
}
输出结果
distinct() 去重 需要实现hascCode和equase方法
public void distinctDemo() {
List<Integer> list = Arrays.asList(1,2,2,3,4,5,5);
List<Integer> resultList = list.stream().map(i->i*i).distinct().collect(Collectors.toList());
System.out.println(resultList);
}
输出结果
filter() 方法用于通过设置的条件过滤出元素。以下代码片段使用 filter 方法过滤出空字符串:
public List<String> filterDemo(List<String> strList) {
List<String> filteredList = strList.stream().filter(s -> !s.isEmpty()).collect(Collectors.toList());
System.out.println("filtered list is:" + filteredList);
return filteredList;
}
输出结果
sorted 方法用于对流进行排序。以下代码片段使用 sorted 方法对输出的 10 个数进行排序:
public void sortedDemo() {
List<Integer> list = Arrays.asList(1,6,2,3,4,9,5);
List<Integer> resultList = list.stream().sorted((a,b)->b.compareTo(a)).collect(Collectors.toList());
System.out.println(resultList);
}
输出结果
并行(parallel)程序
parallelStream 是流并行处理程序的代替方法。以下实例我们使用 parallelStream 来输出空字符串的数量:
public void parallelDemo() {
List<Integer> list = Arrays.asList(1, 6, 2, 3, 4, 9, 5);
list.parallelStream().forEach(System.out::println);
}
Collectors 结合 collect()方法后使用 Collectors.joining(String 分隔符)方法变为分隔符分隔的字符串, Collectors.toList()方法变为集合,Collectors.groupingBy(xxx)按照xxx字段值分组;
public void cellectGroupByDemo() {
User user1 = new User("1","A","user1");
User user2 = new User("2","A","user2");
User user3 = new User("3","B","user3");
User user4 = new User("4","B","user4");
List<User> userList = new ArrayList<>();
userList.add(user1);
userList.add(user2);
userList.add(user3);
userList.add(user4);
Map<String,List<User>> userMap = userList.stream().collect(Collectors.groupingBy(User::getUserType));
System.out.println(userMap);;
}
分组结果
方法引用:通过方法的名字来指向一个方法。
方法引用可以使语言的构造更紧凑简洁,减少冗余代码。方法引用使用一对冒号 :: 。
- 构造器引用:它的语法是Class::new,或者更一般的Class< T >::new。
final Car car=Car.create(Car::new);
final List<Car>cars=Arrays.asList(car);
- 静态方法引用:它的语法是Class::static_method。
cars.forEach(Car::collide);
- 特定类的任意对象的方法引用:它的语法是Class::method。
cars.forEach(Car::repair);
- 特定对象的方法引用:它的语法是instance::method。
final Car police = Car.create(Car::new);
cars.forEach(police::follow);
日期时间 API
Java 8通过发布新的Date-Time API (JSR 310)来进一步加强对日期与时间的处理。
因为Java的Date,Calendar类型使用起来并不是很方便,而且Date类(据说)有着线程不安全等诸多弊端。同时若不进行封装,会在每次使用时特别麻烦。于是Java8推出了线程安全、简易、高可靠的时间包。并且数据库中也支持LocalDateTime类型,在数据存储时候使时间变得简单。Java8这次新推出的包括三个相关的时间类型:LocalDateTime年月日时分秒;LocalDate日期;LocalTime时间;三个包的方法都差不多。
//获取当前时间的LocalDateTime对象
//LocalDateTime.now();
//根据年月日构建LocalDateTime
//LocalDateTime.of();
//比较日期先后
LocalDateTime.now().isBefore();
LocalDateTime.now().isAfter();
Optional 类
Optional不是对null关键字的一种替代,而是对于null判定提供了一种更加优雅的实现。
NullPointException可以说是所有java程序员都遇到过的一个异常,虽然java从设计之初就力图让程序员脱离指针的苦海,但是指针确实是实际存在的,而java设计者也只能是让指针在java语言中变得更加简单、易用,而不能完全的将其剔除,所以才有了我们日常所见到的关键字null。
空指针异常是一个运行时异常,对于这一类异常,如果没有明确的处理策略,那么最佳实践在于让程序早点挂掉,但是很多场景下,不是开发人员没有具体的处理策略,而是根本没有意识到空指针异常的存在。
当异常真的发生的时候,处理策略也很简单,在存在异常的地方添加一个if语句判定即可,但是这样的应对策略会让我们的程序出现越来越多的null判定,我们知道一个良好的程序设计,应该让代码中尽量少出现null关键字,而java8所提供的Optional类则在减少NullPointException的同时,也提升了代码的美观度。但首先我们需要明确的是,它并 不是对null关键字的一种替代,而是对于null判定提供了一种更加优雅的实现,从而避免NullPointException。
使用:
假设我们需要返回一个字符串的长度,如果不借助第三方工具类,我们需要调用str.length()方法:
if(null == str) { // 空指针判定
return 0;
}
return str.length();
如果采用Optional类,实现如下:
int len = Optional.ofNullable(str).map(String::length).orElse(0);
Optional的代码相对更加简洁,当代码量较大时,我们很容易忘记进行null判定,但是使用Optional类则会避免这类问题。
//创建了一个空的Optional对象型
Optional<String> optStr = Optional.empty();
// 创建对象:不允许为空,当str为null的时候,将抛出NullPointException
str = "abc";
optStr = Optional.of(str);
System.out.println("optStr=" + optStr);
// 创建对象:允许为空:如果str是null,则创建一个空对象
str = null;
optStr = Optional.ofNullable(str)
流式处理:Optional也提供了两个基本的流式处理---映射和过滤。
为方便演示,创建如下的类
public class User {
private String id;
private String userType;
private String userName;
//手机和邮箱不是一个人的必须有的,所以我们利用Optional定义。
private Optional<Long> phone;
private Optional<String> email;
public User(String id, String userType, String userName,Optional<Long> phone,Optional<String> email) {
this.id = id;
this.userType = userType;
this.userName = userName;
this.phone = phone;
this.email = email;
}
public String getId() {
return id;
}
public void setId(String newId) {
id = newId;
}
public String getUserType() {
return userType;
}
public void setUserType(String newUserType) {
userType = newUserType;
}
public String getUserName() {
return userName;
}
public void setUserName(String newUserName) {
userName = newUserName;
}
public Optional<Long> getPhone() {
return phone;
}
public void setPhone(Optional<Long> phone) {
this.phone = phone;
}
public Optional<String> getEmail() {
return email;
}
public void setEmail(Optional<String> email) {
this.email = email;
}
}
映射是将输入转换成另外一种形式的输出的操作。比如前面例子中,我们输入字符串,而输出的是字符串的长度,这就是一种映射,我们利用方法map()得以实现。假设我们希望获得一个人的姓名,那么我们可以如下实现:
String name = Optional.ofNullable(user).map(User::getUserName).orElse("no name");
这样当入参user不为空的时候则返回其name,否则返回no name。
如果我们希望通过上面方式得到phone或email,利用上面的方式则行不通了,因为map之后返回的是Optional,我们把这种称为Optional嵌套,我们必须再map一次才能拿到我们想要的结果:
long phone = Optional.ofNullable(user).map(User::getPhone).map(Optional::get).orElse(-1L);
其实这个时候,更好的方式是利用flatMap,一步拿到我们想要的结果:
phone = Optional.ofNullable(user).flatMap(User::getPhone).orElse(-1L);
fliter,顾名思义是过滤的操作,我们可以将过滤操作做为参数传递给该方法,从而实现过滤目的。如我们希望筛选18周岁以上的成年人,则可以实现如下:
optUser.filter(u -> u.getAge() >= 18).ifPresent(u -> System.out.println("Adult:" + u));
默认行为是当Optional为不满足条件时所执行的操作,比如在上面的例子中我们使用的orElse()就是一个默认操作,用于在Optional对象为空时执行特定操作,当然也有一些默认操作是当满足条件的对象存在时执行的操作。
get()
get用于获取变量的值,但是当变量不存在时则会抛出NoSuchElementException,所以如果不确定变量是否存在,则不建议使用。
orElse(Tother)
当Optional的变量不满足给定条件时,则执行orElse,比如前面当str为null时,返回0。
orElseGet(Supplier<? extends X> expectionSupplier)
如果条件不成立时,需要执行相对复杂的逻辑,而不是简单的返回操作,则可以使用orElseGet实现:
long phone = optUser.map(User::getPhone).map(Optional::get).orElseGet(() -> {return -1L;});
orElseThrow(Supplier<? extends X> expectionSupplier)
与get()方法类似,都是在不满足条件时返回异常,不过这里我们可以指定返回的异常类型。
ifPresent(Consumer<? super T>)
当满足条件时执行传入的参数化操作。
注意事项
Optional是一个final类,未实现任何接口,所以当我们在利用该类包装定义类的属性的时候,如果我们定义的类有序列化的需求,那么因为Optional没有实现Serializable接口,这个时候执行序列化操作就会有问题:
public class User implements Serializable{
private long id;
private String name;
private int age;
private Optional<Long> phone; // 不能序列化
private Optional<String> email; // 不能序列化
不过我们可以采用如下替换策略:
private long phone;
public Optional<Long> getPhone() {
return Optional.ofNullable(this.phone);
}