JDK8 新特性

简介

JDK 8 是自JDK 5以来,Oracle对JDK做出的最重大的更新,这个版本中包含了语言、编译器、库、工具、JVM等多种新特性。针对我们平时常用的一些场景,接下来将介绍JDK 8中的新特性。

JAVA 语言的新特性

Lambda表达式和函数式接口

Lambda的设计者们为了让现有的功能与Lambda表达式良好兼容,考虑了很多方法,于是产生了函数接口这个概念,所以我们先讲函数式接口。

函数式接口

函数式接口(Functional Interface)就是一个有且仅有一个抽象方法,但是可以有多个非抽象方法的接口。

1
2
3
4
@FunctionalInterface
interface GreetingService {
void sayMessage(String message);
}

Java 8为函数式接口引入了一个新注解@FunctionalInterface,主要用于编译级错误检查,加上该注解,当你写的接口不符合函数式接口定义的时候,编译器会报错。

加不加 @FunctionalInterface 对于接口是不是函数式接口没有影响,该注解只是提醒编译器去检查该接口是否仅包含一个抽象方法。

函数式接口里允许定义默认方法

1
2
3
4
5
6
7
8
9
10
@FunctionalInterface
interface GreetingService{
void sayMessage(String message);
default void doSomeMoreWork1(){
// Method body
}
default void doSomeMoreWork2(){
// Method body
}
}

函数式接口里允许定义静态方法

1
2
3
4
5
6
7
@FunctionalInterface
interface GreetingService {
void sayMessage(String message);
static void printHello(){
System.out.println("Hello");
}
}

函数式接口里允许定义 java.lang.Object 里的 public 方法

1
2
3
4
5
6
@FunctionalInterface
interface GreetingService {
void sayMessage(String message);
@Override
boolean equals(Object obj);
}

默认方法和静态方法也是JDK 8的新特性,之后会详细的介绍。

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

Lambda表达式

Lambda表达式(也称为闭包)允许我们将函数当成参数传递给某个方法,或者把代码本身当作数据处理。之前的JDK中只能使用匿名内部类代替Lambda表达式。

之前的写法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
interface Person {
void eat();
}

public class Demo {
public static void main(String[] args) {
Person p = new Person() {
public void eat(String s) {
System.out.println("eat something" + s);
}
};
p.eat();
}
}

JDK 8的写法:

1
2
3
4
5
6
7
8
9
10
interface Person {
void eat();
}

public class Demo {
public static void main(String[] args) {
Person p = (s) -> System.out.println("eat something" + s);
p.eat();
}
}

这个操作就像是将(s) -> System.out.println("eat something" + s)这段代码赋值给了p这个实例。

我们对比一下:

1
2
3
4
5
public void eat(String s) {
System.out.println("eat something" + s);
}

p = (s) -> System.out.println("eat something" + s);

Lambda表达式移除了多余的 public void eat , 移除了传入参数的类型 String,一行代码所以可以移除大括号,在参数和函数之间加入了 ->符号。传统的Java 7必须要求你定义一个“污染环境”的接口实现InterfaceImpl或者使用匿名内部类,而相较之下Java 8的Lambda, 就显得干净很多。

Lambda表达式并非匿名内部类的语法糖

实际上,匿名内部类存在着影响应用性能的问题。

首先,编译器会为每一个匿名内部类创建一个类文件。创建出来的类文件的名称通常按照这样的规则 ClassName 符合和数字。生成如此多的文件就会带来问题,因为类在使用之前需要加载类文件并进行验证,这个过程则会影响应用的启动性能。类文件的加载很有可能是一个耗时的操作,这其中包含了磁盘 IO 和解压 JAR 文件。

假设 Lambda 表达式翻译成匿名内部类,那么每一个 Lambda 表达式都会有一个对应的类文件。随着匿名内部类进行加载,其必然要占用 JVM 中的元空间(从 Java 8 开始永久代的一种替代实现)。如果匿名内部类的方法被 JIT 编译成机器代码,则会存储到代码缓存中。同时,匿名内部类都需要实例化成独立的对象。以上关于匿名内部类的种种会使得应用的内存占用增加。因此我们有必要引入新的缓存机制减少过多的内存占用,这也就意味着我们需要引入某种抽象层。

总的来说,lambda的大致思路如下:

  1. lamdba表达式被编译生成当前类的一个私有静态方法
  2. 在原调用Lamdba方法的地方编译成了一个invokedynamic指令(java7 JVM中增加了一个新的指令)调用,同时呢也生成了一个对应的BootstrapMethod
  3. 当lamdba表达式被JVM执行,也就是碰到2中说到的invokedynamic指令,该指令引导调用LambdaMetafactory.metafactory方法,该方法返回一个CallSite实例
  4. 这个CallSite实例中的target对象,也就是直接引用到一个MethodHandle实例,而这个MethodHandle实例会调用到1中生成的静态方法,在上面的例子就是lambda$main$0这个方法,完成整个lamdba表达式的使用

查看原文:Java 8 Lambdas - A Peek Under the Hood

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

默认方法和抽象方法之间的区别在于抽象方法需要实现,而默认方法不需要。接口提供的默认方法会被接口的实现类继承或者覆写,例子代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
private interface Defaulable {
default String notRequired() {
return "Default implementation";
}
static Defaulable create( Supplier< Defaulable > supplier ) {
return supplier.get();
}
}

private static class DefaultableImpl implements Defaulable {
}

private static class OverridableImpl implements Defaulable {
@Override
public String notRequired() {
return "Overridden implementation";
}
}

方法引用

方法引用使得开发者可以直接引用现存的方法、Java类的构造方法或者实例对象,总的来说,一共有以下几种形式:

  • 静态方法引用:ClassName::methodName;
  • 实例上的实例方法引用:instanceName::methodName;
  • 超类上的实例方法引用:supper::methodName;
  • 类的实例方法引用:ClassName:methodName;
  • 构造方法引用Class:new;
  • 数组构造方法引用::TypeName[]::new

构造器引用,语法是Class::new,或者更一般的形式:Class::new

1
final Test test = Test::new;

静态方法引用,语法是Class::static_method

1
list.forEach(System.out::println);

重复注解

在Java 8中使用**@Repeatable**注解定义重复注解

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@Target( ElementType.TYPE )
@Retention( RetentionPolicy.RUNTIME )
public @interface Filters {
Filter[] value();
}

@Target( ElementType.TYPE )
@Retention( RetentionPolicy.RUNTIME )
@Repeatable( Filters.class )
public @interface Filter {
String value();
};

@Filter( "filter1" )
@Filter( "filter2" )
public interface Filterable {
}

Java官方库的新特性

Optional

Java应用中最常见的bug就是NullPointerException,JDK参考了Guava的Optionals类来解决NullPointerException,从而避免源码被各种null检查污染。

看一个简单样例:

1
2
3
4
5
Optional< String > firstName = Optional.of( "Tom" );
System.out.println( "First Name is set? " + firstName.isPresent() );
System.out.println( "First Name: " + firstName.orElseGet( () -> "[none]" ) );
System.out.println( firstName.map( s -> "Hey " + s + "!" ).orElse( "Hey Stranger!" ) );
System.out.println();

更直接的例子,使用JPA访问数据库(最新的Mybatis已经支持,但是代码生成工具还没有支持):

1
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
@Repository
public interface UserInfoRepository extends JpaRepository<UserInfo, String> {
UserInfo findByName(String name);
}

public void business() {
UserInfo user = userInfoRepository.findByName("dumdum");
if (Objects.isNull(user)) {
// throw exception
} else {
// do something
}
}

//----------------------------------------------------------------------------------
//使用Optional
@Repository
public interface UserInfoRepository extends JpaRepository<UserInfo, String> {
Optional<Userinfo> findByName(String name);
}

public void business() {
Optional<Userinfo> userOpt = userInfoRepository.findByName("dumdum");
UserInfo user = userOpt.orElseThrow(() ->
new Exception("查無 userInfo(dumdum) 資訊."));
// UserInfo user = userOpt.orElse(new UserInfo("Rocko")));
// do something
}

Streams

有了函数式编程之后,配合StreamAPI,极大得简化了集合操作(后面我们会看到不止是集合)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
private enum Status {
OPEN, CLOSED
};

Collection< Task > tasks = Arrays.asList(
new Task( Status.OPEN, 5 ),
new Task( Status.OPEN, 13 ),
new Task( Status.CLOSED, 8 )
);

//如何计算集合中每个任务的点数在集合中所占的比重
Collection< String > result = tasks
.stream() // Stream< String >
.mapToInt( Task::getPoints ) // IntStream
.asLongStream() // LongStream
.mapToDouble( points -> points / totalPoints ) // DoubleStream
.boxed() // Stream< Double >
.mapToLong( weigth -> ( long )( weigth * 100 ) ) // LongStream
.mapToObj( percentage -> percentage + "%" ) // Stream< String>
.collect( Collectors.toList() ); // List< String >

System.out.println( result );

传统的IO操作(从文件或者网络一行一行得读取数据)可以受益于steam处理, 配合try-with-resource写法

1
2
3
4
Path path = new File( filename ).toPath();
try( Stream< String > lines = Files.lines( path, StandardCharsets.UTF_8 ) ) {
lines.onClose( () -> System.out.println("Done!") ).forEach( System.out::println );
}

Date/Time API(JSR 310)

java.util.Date和后来的java.util.Calendar一直存在着诸多的问题,比如:

  1. 线程安全问题:java.util.Date是非线程安全的,所有的日期类都是可变的;

  2. 设计很差:在java.util和java.sql的包中都有日期类,此外,用于格式化和解析的类在java.text包中也有定义。而每个包将其合并在一起,也是不合理的;

  3. 时区处理麻烦:日期类不提供国际化,没有时区支持,因此Java中引入了java.util.Calendar和Java.util.TimeZone类;

从而催生了第三方库Joda-Time,Java 8中新的时间和日期管理API深受Joda-Time影响,并吸收了很多Joda-Time的精华。

在java.util.time包中常用的几个类有:

  • 它通过指定一个时区,然后就可以获取到当前的时刻,日期与时间。Clock可以替换System.currentTimeMillis()与TimeZone.getDefault()
  • Instant:一个instant对象表示时间轴上的一个时间点,Instant.now()方法会返回当前的瞬时点(格林威治时间);
  • Duration:用于表示两个瞬时点相差的时间量;
  • LocalDate:一个带有年份,月份和天数的日期,可以使用静态方法now或者of方法进行创建;
  • LocalTime:表示一天中的某个时间,同样可以使用now和of进行创建;
  • LocalDateTime:兼有日期和时间;
  • ZonedDateTime:通过设置时间的id来创建一个带时区的时间;
  • DateTimeFormatter:日期格式化类,提供了多种预定义的标准格式;

Spring 中 LocalDateTime格式处理

Controller接收LocalDateTime参数

@RequestParam @DateTimeFormat(pattern = “yyyy-MM-dd HH:mm:ss”) LocalDateTime date

ResponseBody格式化LocalDateTime

Spring默认使用jackson来进行json格式转换,我们只需要使用@Bean注解创建一个ObjectMapperbean,并将JavaTimeModule注册到ObjectMapper中即可,spring会使用该bean创建MappingJackson2HttpMessageConverter进行json格式转换。这里需要加入jacksonjsr310扩展包。

1
2
3
4
5
<dependency>
<groupId>com.fasterxml.jackson.datatype</groupId>
<artifactId>jackson-datatype-jsr310</artifactId>
<version>2.8.9</version>
</dependency>
1
2
3
4
5
6
7
8
9
10
@Bean(name = "mapperObject")
public ObjectMapper getObjectMapper() {
ObjectMapper om = new ObjectMapper();
JavaTimeModule javaTimeModule = new JavaTimeModule();
javaTimeModule.addSerializer(LocalDateTime.class, new LocalDateTimeSerializer(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")));
javaTimeModule.addSerializer(LocalDate.class, new LocalDateSerializer(DateTimeFormatter.ofPattern("yyyy-MM-dd")));
javaTimeModule.addSerializer(LocalTime.class, new LocalTimeSerializer(DateTimeFormatter.ofPattern("HH:mm:ss")));
om.registerModule(javaTimeModule);
return om;
}

或者使用(缺点是每一个变量都需要加这个注解)

1
@JsonSerialize(using = LocalDateTimeSerializer.class)

另外,如果持久层框架使用mybatis,同样需要加入mybatisjsr310 扩展包。

1
2
3
4
5
<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis-typehandlers-jsr310</artifactId>
<version>1.0.2</version>
</dependency>

Base64

对Base64编码的支持已经被加入到Java 8官方库中,这样不需要使用第三方库就可以进行Base64编码,例子代码如下:

1
2
3
4
5
6
7
8
9
String text = "Base64 finally in Java 8!";
String encoded = Base64.getEncoder()
.encodeToString( text.getBytes( StandardCharsets.UTF_8 ) );
System.out.println( encoded );
String decoded = new String(
Base64.getDecoder()
.decode( encoded ),StandardCharsets.UTF_8
);
System.out.println( decoded );