一. 面试题及剖析
1. 今日面试题
在之前的几个章节中,壹哥 带各位复习的知识点与Java版本没有特别大的关系,今天我们就来复习一道与Java特定版本有关系的面试题:
你了解哪些Java(JDK)的新特性?
Java8的新特性你知道哪些?
Java11的新特性你知道哪些?
......
2. 题目剖析
这道题目,其实回答起来并没有特别大的难度,主要是考察我们对Java新特性的了解程度。那么面试官为什么要考察我们对Java新特性的掌握程度呢?
其实之所以问这样的问题,一方面是因为现在大部分公司开发时,JDK已经逐步替换成JDK8版本了,这就要求Java程序员必须掌握JDK新的特性才能适应开发要求;另一方面,也是在考察我们的学习能力,是否做到了经常更新自己的技术,毕竟IT行业是一个需要不断学习的行业。而且任何一个公司招聘时,对学习能力都有很高的要求,如下图所示:
所以如果各位不能很好的回答这道题,那么肯定会被面试官贴上不爱学习、技术陈旧的标签,这是非常糟糕的。既然这样,那就请大家好好看看本文,跟着 壹哥 好好复习一下Java新特性吧。
二. JDK版本简介
1. JDK版本规划
我们知道,Java本来属于Sun这个公司,结果这公司经营不善黄摊了,后来就被Oracle(甲骨文)给收购了,所以现在是Oracle负责Java的开发维护。在挺长一段时间里,Java都处于一种不怎么更新变动的状态,感觉就是不温不火的样子。
后来有一天,也不知道是咋地了,Oracle公司突然顿悟了,觉得得把Java给重视起来,于是就制定了一个针对Java的长远规划。这个规划的工作量可以说是很宏大的,就是Oracle把JDK分成了两种维护情况,即短期支持版本和长期支持版本。对于短期支持版本(non-LTS)而言,Oracle只会提供6个月的支持维护;而对于长期支持版本(LTS),则提供8年的支持维护。根据这一规划,Oracle每隔6个月,就会发布一个大版本,每个季度发布一个中间特性版本。并且承诺新的JDK发布周期会严格遵循时间点,将于每年的3月份和9月份发布,中间不会跳票。当然,至于Oracle能不能做到,我们只能拭目以待。
所以现在正常情况下,每隔6个月就会有一个短期维护版本(non-LTS)发布出来,比如JDK 9、10、12、13、14、15、16;然后每隔3年,则会发布一款得到8年长期支持维护的JDK版本,比如JDK 8、11,还有即将于2021年底发布的JDK 17。其中JDK 8发布于2014年3月,JDK 11发布于2018年9月,JDK 17应该发布于2021年9月。我们来看看 Oracle官方发布的JDK发布支持路线图 吧:
| Oracle Java SE Support Roadmap*† | ||||
| Release | GA Date | Premier Support Until | Extended Support Until | Sustaining Support |
| 7 (LTS) | July 2011 | July 2019 | July 2022***** | Indefinite |
| 8 (LTS)** | March 2014 | March 2022 | December 2030***** | Indefinite |
| 9 (non‑LTS) | September 2017 | March 2018 | Not Available | Indefinite |
| 10 (non‑LTS) | March 2018 | September 2018 | Not Available | Indefinite |
| 11 (LTS) | September 2018 | September 2023 | September 2026 | Indefinite |
| 12 (non‑LTS) | March 2019 | September 2019 | Not Available | Indefinite |
| 13 (non‑LTS) | September 2019 | March 2020 | Not Available | Indefinite |
| 14 (non‑LTS) | March 2020 | September 2020 | Not Available | Indefinite |
| 15 (non‑LTS) | September 2020 | March 2021 | Not Available | Indefinite |
| 16 (non-LTS) | March 2021 | September 2021 | Not Available | Indefinite |
| 17 (LTS) | September 2021 | September 2026**** | September 2029**** | Indefinite |
| 18 (non-LTS)*** | March 2022 | September 2022 | Not Available | Indefinite |
| 19 (non-LTS)*** | September 2022 | March 2023 | Not Available | Indefinite |
| 20 (non-LTS)*** | March 2023 | September 2023 | Not Available | Indefinite |
| 21 (LTS)*** | September 2023 | September 2028 | September 2031 | Indefinite |
注:
non-LTS:6个月的短期维护版本,公司生产环境中绝不会采用的JDK版本;
LTS:8年长期维护版本,公司生产环境中重点采用的JDK版本。
根据上图,2021年9月应该发布JDK 17这个LTS版本,目前来看,这明显是跳票了,不知道今年能不能发布出来。咱们作为小码农,对Oracle这种巨头,也只能乖乖的看着,爱啥时候发布就发布吧,反正即使发布了JDK 17,5年内也不可能用于生产环境。
2. JDK版本选择
看了上面 壹哥 对JDK版本的介绍,有的小伙伴直接就懵了,JDK版本这么多,我们学习和开发时到底该选择哪一个呢?别慌,听我给你道来。
其实上面JDK的维护路线图中,虽然展示了很多版本的JDK,比如JDK7、8、9、10......,但是我们真正开发时,并不会每一个版本都要选择使用。在公司的生产环境中,只会选择长期维护版本(LTS),也就是只会选择JDK7、8、11、17、21这几个版本。如果有哪个公司选择了JDK9、10这样的non-LTS版本,只能说这样的公司根本就不配做Java项目。
所以你现在该选择学习哪个JDK版本就很容易知道了!JDK 17、21还没有发布,所以我们只能从JDK 7/8/11中选择了,当然个别古董公司还在使用JDK 6甚至更早的JDK 5,这不在 我们正常程序员的选择之列。
目前来看,截止到2018年,根据权威统计报告,79%的Java开发者都在使用JDK 8,部分追求性能喜欢尝鲜的互联网公司程序员在使用JDK 11。
当然,上面说的是79%以上的程序员在使用JDK 8,并不代表着有79%的项目在使用JDK 8。但是目前JDK 8绝对是企业项目开发的主流版本,而未来的5年内,JDK 11的使用则会逐步上升。
三. Java 8新特性
根据上面 壹哥 对JDK版本的介绍,你现在应该知道了,JDK 8是现在企业开发时选择的主流版本,JDK 11是未来的重点选择版本。所以我们作为一个Java程序员,既要学习Java的常规API,也要掌握Java的这些新特性内容,要不然你不就比别人low了吗?接下来我们就一起来了解一下,Java中都有哪些新特性吧,看看这些新特性,与之前固有的 “旧特性” 有什么不同。
1. Java 8简介
Java 8版本,可谓是自Java 5以来最具革命性的版本了,其在语言、编译器、类库、开发工具以及Java虚拟机等10个方面都带来了不少新特性。Java 8的新特性主要集中在以下几个方面:
Java语言的新特性;
Java编译器的新特性;
Java官方库的新特性;
JVM的新特性;
新的Java工具;
.....
接下来,壹哥 就从这几个方面,对Java 8的新特性展开介绍。
2. Java语言的新特性
2.1 Lambda表达式(重点)
Lambda表达式(也称为闭包) 是Java 8所有新特性中最大和最令人期待的语言改变。Lambda用于表示一个函数,所以它和函数一样,也有参数、返回值、函数体,但没有函数名,即Lambda表达式相当于一个匿名函数(闭包)。语法如下:
具体内容请参考官方教程:函数式开发者,以后 壹哥 会推出专门的Lambda教程,敬请关注哦!
其实JVM平台上的很多其他语言(Groovy、Scala等) ,从诞生时就支持Lambda表达式了,但是Java中一开始却没有提供,所以之前我们只能使用匿名内部类来代替Lambda表达式。
但因为本文不是专门的Lambda教程,所以这里 壹哥 只是简单地说一下Lambda的基本用法及要求,如下:
·可选的类型声明:不需要声明参数类型,编译器可以统一识别参数值;
·可选的参数圆括号:一个参数无需定义圆括号,但多个参数需要定义圆括号;
·可选的大括号:如果方法体中只包含一个语句,就不需要使用大括号;
·可选的返回关键字:如果方法体中只有一个表达式返回值,则编译器会自动返回值。
一个Lambda表达式可以由逗号分隔的参数列表、–>符号与函数体三部分表示,例如:
Arrays.asList( "a", "b", "d" ).forEach( e -> System.out.println( e ) );
简单一句话,就可以轻松实现对List集合的循环遍历。
上面这个代码中,参数e的类型是由编译器推理得出的,我们也可以显式地指定该参数的类型,例如:
Arrays.asList( "a", "b", "d" ).forEach( ( String e ) -> System.out.println( e ) );
如果Lambda表达式中需要更复杂的语句块,则可以使用花括号将该语句块括起来,类似于Java中的函数体,例如:
Arrays.asList( "a", "b", "d" ).forEach( e -> { System.out.print( e );
System.out.print( e ); } );
Lambda表达式可以引用类成员和局部变量(会将这些变量隐式地转换成final修饰),例如下列两个代码块的效果完全相同:
String separator = ","; Arrays.asList( "a", "b", "d" ).forEach(( String e ) -> System.out.print( e + separator ) );
和
final String separator = ","; Arrays.asList( "a", "b", "d" ).forEach(( String e ) -> System.out.print( e + separator ) );
如果Lambda表达式有返回值,则返回值的类型也可以由编译器推理得出。如果Lambda表达式中的语句块只有一行,则可以不用return语句,下列两个代码片段效果相同:
Arrays.asList( "a", "b", "d" ).sort((e1,e2) -> e1.compareTo(e2) );
和
Arrays.asList( "a", "b", "d" ).sort((e1,e2) -> {int result = e1.compareTo(e2);
2.2 函数式接口(重点)
Lambda的设计者为了让现有的功能与Lambda表达式良好兼容,考虑了很多方法,于是产生了 函数式接口 这个概念。函数式接口指的是只有一个方法的普通接口,这样的接口可以隐式转换为Lambda表达式。
java.lang.Runnable和java.util.concurrent.Callable是函数式接口的最佳例子。在实践中,函数式接口非常脆弱:只要某个开发者在该接口中添加一个函数,则该接口就不再是函数式接口进而导致编译失败。为了克服这种代码层面的脆弱性,并显式说明某个接口是函数式接口,Java 8 提供了一个特殊的注解@FunctionalInterface(Java 库中的所有相关接口都已经带有这个注解了),举个简单的函数式接口的定义:
@FunctionalInterface
public interface Functional {
void method();
}
不过有一点需要注意,默认方法和静态方法不会破坏函数式接口的定义,因此如下的代码是合法的。
@FunctionalInterface
public interface FunctionalDefaultMethods {
void method();
default void defaultMethod() { }
}
Lambda表达式作为Java 8的最大卖点,它有潜力吸引更多的开发者加入到JVM平台,并在纯Java编程中使用函数式编程的概念。如果你需要了解更多Lambda表达式的细节,可以参考官方文档。
2.3 接口的默认方法和静态方法(重点)
Java 8中对接口进行了拓展,新增了2个功能:默认方法和静态方法。默认方法使得开发者可以在 不破坏二进制兼容性的前提下,往现存接口中添加新的方法,即不强制那些实现了该接口的类也同时实现这个新加的方法。
我们可以在接口中使用default关键字来定义默认方法,并提供默认的实现。所有实现这个接口的类都会接受默认方法的实现,除非子类提供的自己的实现。所以默认方法和抽象方法之间的区别在于抽象方法需要实现,而默认方法不需要。接口提供的默认方法会被接口的实现类继承或者覆写,代码如下:
private interface Defaulable {
// Interfaces now allow default methods, the implementer may or
// may not implement (override) them.
default String notRequired() {
return "Default implementation";
}
}
private static class DefaultableImpl implements Defaulable {
}
private static class OverridableImpl implements Defaulable {
@Override
public String notRequired() {
return “Overridden implementation”;
}
}
Defaulable接口使用default关键字定义了一个默认方法notRequired()。DefaultableImpl类实现了这个接口,同时默认继承了这个接口中的默认方法;OverridableImpl类也实现了这个接口,但覆写了该接口的默认方法,并提供了一个不同的实现。
我们还可以在接口中定义静态方法,使用static关键字,也可以提供实现,代码如下:
private interface DefaulableFactory {
// Interfaces now allow static methods
static Defaulable create( Supplier< Defaulable > supplier ) {
return supplier.get();
}
}
下面的代码片段整合了默认方法和静态方法的使用场景:
public static void main( String[] args ) {
Defaulable defaulable = DefaulableFactory.create( DefaultableImpl::new );
System.out.println( defaulable.notRequired() );
defaulable = DefaulableFactory.create( OverridableImpl::new );
System.out.println( defaulable.notRequired() );
}
这段代码的输出结果如下:
Default implementation
Overridden implementation
由于JVM上的默认方法的实现在字节码层面提供了支持,因此效率非常高。默认方法允许在不打破现有继承体系的基础上改进接口,该特性在官方库中的应用是:给java.util.Collection接口添加新方法,如stream()、parallelStream()、forEach()和removeIf()等。
接口的默认方法和静态方法的引入,其实可以认为引入了C++中抽象类的理念,以后我们再也不用在每个实现类中都写重复的代码了。
尽管默认方法有这么多好处,但在实际开发中应该谨慎使用:在复杂的继承体系中,默认方法可能引起歧义和编译错误。如果你想了解更多细节,可以参考官方文档。
2.3 方法引用
方法引用使得开发者可以直接引用现存的静态方法、构造方法、类方法、实例方法等,当和Lambda表达式配合使用时,会使得Java代码看起来更紧凑简洁。方法引用有4种调用形式,如下:
- 构造器引用:语法是ClassName::new,或者是Class
::new,要求构造方法不带参数; - 静态方法引用:语法是ClassName::static_method,要求携带一个Class类型的参数;
- 特定类的任意对象方法引用:语法是ClassName::method,要求方法不带参数;
- 特定对象的方法引用:语法是instance::method,要求方法携带一个参数。该方式与语法3不同的地方在于,语法3是在类上调用方法,而语法4是利用某个对象调用方法。
- 超类上的实例方法引用:语法是super::methodName;
- 数组构造方法引用:语法是TypeName[]::new。
示例代码如下:
public static class Car {
public static Car create(final Supplier 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() );
}
}
构造器引用示例,注意:这个构造器没有参数。
final Car car = Car.create(Car::new); final List< Car > cars = Arrays.asList(car);
静态方法引用,注意:这个方法接受一个Car类型的参数。
cars.forEach(Car::collide);
某个类的成员方法的引用,注意,这个方法没有定义入参:
cars.forEach(Car::repair);
某个实例对象的成员方法的引用,注意:这个方法接受一个Car类型的参数:
final Car police = Car.create(Car::new); cars.forEach(police::follow);
运行上述例子,可以在控制台看到如下输出(Car实例可能不同):
Collided com.javacodegeeks.java8.method.references.MethodReferences$Car@7a81197d Repaired com.javacodegeeks.java8.method.references.MethodReferences$Car@7a81197d Following the com.javacodegeeks.java8.method.references.MethodReferences$Car@7a81197d
如果想了解和学习更详细的内容,可以参考官方文档
2.4 重复注解
自从Java 5中引入注解以来,这个特性开始变得非常流行,并在各个框架和项目中被广泛使用。不过在Java 5中使用注解有一个限制,即相同的注解在同一位置只能声明使用一次。Java 8引入了重复注解,这样相同的注解在同一地方也可以声明多次,重复注解机制本身需要用@Repeatable注解。Java 8在编译器层做了优化,相同注解会以集合的方式保存,因此底层的原理并没有变化。
示例代码如下:
package com.javacodegeeks.java8.repeatable.annotations;
import java.lang.annotation.ElementType;
import java.lang.annotation.Repeatable;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
public class RepeatingAnnotations {
@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 {
}
public static void main(String[] args) {
for( Filter filter: Filterable.class.getAnnotationsByType( Filter.class ) ) {
System.out.println( filter.value() );
}
}
}
正如我们所见,这里的Filter类使用@Repeatable(Filters.class)注解修饰,而Filters是存放Filter注解的容器,编译器尽量对开发者屏蔽这些细节。这样,Filterable接口可以用两个Filter注解注释(这里并没有提到任何关于Filters的信息)。
另外,反射API提供了一个新的方法:getAnnotationsByType(),可以返回某个类型的重复注解,例如Filterable.class.getAnnoation(Filters.class)将返回两个Filter实例,输出到控制台的内容如下所示:
filter1 filter2
如果你希望了解更多内容,可以参考官方文档。
2.5 更好的类型推断
Java 8编译器在类型推断方面有很大的提升,在很多场景下编译器可以推导出某个参数的数据类型,不需要太多的强制类型转换了,从而使得代码更为简洁。示例代码如下:
package com.javacodegeeks.java8.type.inference;
public class Value< T > {
public static< T > T defaultValue() {
return null;
}
public T getOrDefault( T value, T defaultValue ) {
return ( value != null ) ? value : defaultValue;
}
}
下列代码是Value
package com.javacodegeeks.java8.type.inference;
public class TypeInference {
public static void main(String[] args) {
final Value< String > value = new Value<>();
value.getOrDefault( "22", Value.defaultValue() );
}
}
参数Value.defaultValue()的类型由编译器推导得出,不需要显式指明。在Java 7中这段代码会有编译错误,除非使用Value.
2.6 拓宽注解的应用场景
Java 8拓宽了注解的应用场景。现在,注解几乎可以使用在任何元素上:局部变量、接口类型、超类和接口实现类,甚至可以用在函数的异常定义上。下面是一些例子:
package com.javacodegeeks.java8.annotations;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import java.util.ArrayList;
import java.util.Collection;
public class Annotations {
@Retention( RetentionPolicy.RUNTIME )
@Target( { ElementType.TYPE_USE, ElementType.TYPE_PARAMETER } )
public @interface NonEmpty {
}
public static class Holder< @NonEmpty T > extends @NonEmpty Object {
public void method() throws @NonEmpty Exception {
}
}
@SuppressWarnings( "unused" )
public static void main(String[] args) {
final Holder< String > holder = new @NonEmpty Holder< String >();
@NonEmpty Collection< @NonEmpty String > strings = new ArrayList<>();
}
}
ElementType.TYPE_USER和ElementType.TYPE_PARAMETER是Java 8新增的两个注解,用于描述注解的使用场景。Java 语言也做了对应的改变,以识别这些新增的注解。
3. Java编译器的新特性
3.1 参数名称
为了在运行时获得Java程序中方法的参数名称,之前可能需要使用第三方类库,例如Paranamer liberary。而在Java 8中则将方法的参数名直接加入到了字节码中,这样在运行时通过反射就能获取到参数名,我们只需要在编译时使用parameters参数,使用反射里的Parameter.getName()方法即可。
package com.javacodegeeks.java8.parameter.names;
import java.lang.reflect.Method;
import java.lang.reflect.Parameter;
public class ParameterNames {
public static void main(String[] args) throws Exception {
Method method = ParameterNames.class.getMethod( "main", String[].class );
for( final Parameter parameter: method.getParameters() ) {
System.out.println( "Parameter: " + parameter.getName() );
}
}
}
在Java 8中这个特性是默认关闭的,因此如果不带-parameters参数编译上述代码并运行,则会输出如下结果:
Parameter: arg0
如果带-parameters参数,则会输出如下结果(正确的结果):
Parameter: args
如果你使用Maven进行项目管理,则可以在maven-compiler-plugin编译器的配置项中配置-parameters参数:
org.apache.maven.plugins maven-compiler-plugin3.1 -parameters 1.8 1.8
4. Java官方库的新特性
Java 8增加了很多新的工具类(date/time类),并扩展了现存的工具类,以支持现代的并发编程、函数式编程等。
4.1 Optional(重点)
Java应用中最常见的bug就是空指针异常。在Java 8之前,Google Guava引入了Optionals类来解决NullPointerException,从而避免源码被各种null检查污染,以便我们写出更加整洁的代码。Java 8也将Optional加入了官方库来防止空指针异常。Optional类实际上是个容器:它可以保存类型T的值,或者保存null,它提供了一些有用的接口来避免显式的null检查,使用Optional类我们就不用显式地进行空指针检查了。请参考Java 8官方文档了解更多细节。
示例代码:
Optional< String > fullName = Optional.ofNullable( null ); System.out.println( "Full Name is set? " + fullName.isPresent() ); System.out.println( "Full Name: " + fullName.orElseGet( () -> "[none]" ) ); System.out.println( fullName.map( s -> "Hey " + s + "!" ).orElse( "Hey Stranger!" ) );
如果Optional实例持有一个非空值,则isPresent()方法返回true,否则返回false;orElseGet()方法,Optional实例持有null,则可以接受一个lambda表达式生成的默认值;map()方法可以将现有的Opetional实例的值转换成新的值;orElse()方法与orElseGet()方法类似,但是在持有null的时候返回传入的默认值。
上述代码的输出结果如下:
Full Name is set? false
Full Name: [none] Hey Stranger!
再看下另一个简单的例子:
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();
这个例子的输出是:
First Name is set? true First Name: Tom Hey Tom!
如果想了解更多的细节,请参考官方文档。
4.2 Streams(重点)
Stream API把真正的函数式编程风格引入到了Java中,其实简单来说可以把Stream理解为MapReduce,当然Google的MapReduce的灵感也是来自函数式编程。Stream其实是一连串支持连续、并行聚集操作的元素,从语法上看,很像linux的管道、或者链式编程,代码写起来简洁明了。这是目前为止对Java库最大的一次完善,可以使开发者能够写出更加有效、更加简洁紧凑的代码。
Steam API极大地简化了集合操作(后面我们会看到不止是集合),示例代码:
public class Streams {
private enum Status {
OPEN, CLOSED
};
private static final class Task {
private final Status status;
private final Integer points;
Task( final Status status, final Integer points ) {
this.status = status;
this.points = points;
}
public Integer getPoints() {
return points;
}
public Status getStatus() {
return status;
}
@Override
public String toString() {
return String.format( "[%s, %d]", status, points );
}
}
}
Task类有一个分数(或伪复杂度)的概念,另外还有两种状态:OPEN或者CLOSED。现在假设有一个task集合:
final Collection< Task > tasks = Arrays.asList(new Task(Status.OPEN, 5),
new Task( Status.OPEN, 13 ), new Task( Status.CLOSED, 8 ) );
首先看一个问题:在这个task集合中一共有多少个OPEN状态的点?在Java 8之前,要解决这个问题,则需要使用foreach循环遍历task集合;但是在Java 8中可以利用steams解决:包括一系列元素的列表,并且支持顺序和并行处理。
// Calculate total points of all active tasks using sum()
final long totalPointsOfOpenTasks = tasks
.stream()
.filter( task -> task.getStatus() == Status.OPEN )
.mapToInt( Task::getPoints )
.sum();
System.out.println( "Total points: " + totalPointsOfOpenTasks );
运行这个方法的控制台输出是:
Total points: 18
这里有很多知识点值得说。首先,tasks集合被转换成steam表示;其次,在steam上的filter操作会过滤掉所有CLOSED的task;第三,mapToInt操作基于每个task实例的Task::getPoints方法将task流转换成Integer集合;最后,通过sum方法计算总和,得出最后的结果。
在学习下一个例子之前,还需要记住一些steams(点此更多细节)的知识点。Steam之上的操作可分为中间操作和晚期操作。
中间操作会返回一个新的steam——执行一个中间操作(例如filter)并不会执行实际的过滤操作,而是创建一个新的steam,并将原steam中符合条件的元素放入新创建的steam。
晚期操作(例如forEach或者sum),会遍历steam并得出结果或者附带结果;在执行晚期操作之后,steam处理线已经处理完毕,就不能使用了。在几乎所有情况下,晚期操作都是立刻对steam进行遍历。
steam的另一个价值是创造性地支持并行处理(parallel processing)。对于上述的tasks集合,我们可以用下面的代码计算所有任务的点数之和:
// Calculate total points of all tasks final double totalPoints = tasks .stream() .parallel() .map( task -> task.getPoints() ) // or map( Task::getPoints ) .reduce( 0, Integer::sum ); System.out.println( "Total points (all tasks): " + totalPoints );
这里我们使用parallel方法并行处理所有的task,并使用reduce方法计算最终的结果。控制台输出如下:
Total points(all tasks): 26.0
对于一个集合,经常需要根据某些条件对其中的元素分组。利用steam提供的API可以很快完成这类任务,代码如下:
// Group tasks by their status
final Map< Status, List< Task > > map = tasks
.stream()
.collect( Collectors.groupingBy( Task::getStatus ) );
System.out.println( map );
控制台的输出如下:
{CLOSED=[[CLOSED, 8]], OPEN=[[OPEN, 5], [OPEN, 13]]}
最后一个关于tasks集合的例子问题是:如何计算集合中每个任务的点数在集合中所占的比重,具体处理的代码如下:
// Calculate the weight of each tasks (as percent of total points)
final 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 );
控制台输出结果如下:
[19%, 50%, 30%]
最后,正如之前所说,Steam API不仅可以作用于Java集合,传统的IO操作(从文件或者网络一行一行得读取数据)可以受益于steam处理,这里有一个小例子:
final 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 ); }
Stream的方法onClose 返回一个等价的有额外句柄的Stream,当Stream的close()方法被调用的时候这个句柄会被执行。Stream API、Lambda表达式还有接口默认方法和静态方法支持的方法引用,是Java 8对软件开发的现代范式的响应。
4.3 Date/Time API(JSR 310)
在Java 8之前的版本中,日期时间类API存在很多的问题,比如:
- 线程安全问题:java.util.Date是非线程安全的,所有的日期类都是可变的;
- 设计很差:在java.util和java.sql的包中都有日期类,此外,用于格式化和解析的类在java.text包中也有定义。而每个包都将其合并在一起,也是不合理的;
- 时区处理麻烦:日期类不提供国际化,没有时区支持,因此Java中引入了java.util.Calendar和Java.util.TimeZone类。
针对这些问题,Java 8中重新设计了日期时间类相关的API,Java 8通过发布新的Date-Time API (JSR 310)来进一步加强对日期与时间的处理。在java.util.time包中常用的几个类有:
- Clock: 它通过指定一个时区,然后就可以获取到当前的时刻,日期与时间。Clock可以替换System.currentTimeMillis()与TimeZone.getDefault();
- Instant: 一个Instant对象表示时间轴上的一个时间点,Instant.now()方法会返回当前的瞬时点(格林威治时间);
- Duration: 用于表示两个瞬时点相差的时间量;
- LocalDate: 一个带有年份,月份和天数的日期,可以使用静态方法now或者of方法进行创建;
- LocalTime: 表示一天中的某个时间,同样可以使用now和of进行创建;
- LocalDateTime:兼有日期和时间;
- ZonedDateTime:通过设置时间的id来创建一个带时区的时间;
- DateTimeFormatter:日期格式化类,提供了多种预定义的标准格式;
我们接下来看看java.time包中的关键类和各自的使用例子。
首先,Clock类使用时区来返回当前的纳秒时间和日期,可以替代System.currentTimeMillis()和TimeZone.getDefault()。
// Get the system clock as UTC offset final Clock clock = Clock.systemUTC(); System.out.println( clock.instant() ); System.out.println( clock.millis() );
这个例子的输出结果是:
2014-04-12T15:19:29.282Z 1397315969360
LocalDate和LocalTime类。LocalDate仅仅包含ISO-8601日历系统中的日期部分;LocalTime则仅仅包含该日历系统中的时间部分。这两个类的对象都可以使用Clock对象构建得到。
// Get the local date and local time final LocalDate date = LocalDate.now(); final LocalDate dateFromClock = LocalDate.now( clock ); System.out.println( date ); System.out.println( dateFromClock ); // Get the local date and local time final LocalTime time = LocalTime.now(); final LocalTime timeFromClock = LocalTime.now( clock ); System.out.println( time ); System.out.println( timeFromClock );
上述例子的输出结果如下:
2014-04-12 2014-04-12 11:25:54.568 15:25:54.568
LocalDateTime类包含了LocalDate和LocalTime的信息,但是不包含ISO-8601日历系统中的时区信息。这里有一些关于LocalDate和LocalTime的例子:
// Get the local date/time final LocalDateTime datetime = LocalDateTime.now(); final LocalDateTime datetimeFromClock = LocalDateTime.now( clock ); System.out.println( datetime ); System.out.println( datetimeFromClock );
上述这个例子的输出结果如下:
2014-04-12T11:37:52.309 2014-04-12T15:37:52.309
如果你需要特定时区的data/time信息,则可以使用ZoneDateTime,它保存有ISO-8601日期系统的日期和时间,而且有时区信息。下面是一些使用不同时区的例子:
// Get the zoned date/time final ZonedDateTime zonedDatetime = ZonedDateTime.now(); final ZonedDateTime zonedDatetimeFromClock = ZonedDateTime.now( clock ); final ZonedDateTime zonedDatetimeFromZone = ZonedDateTime.now( ZoneId.of( "America/Los_Angeles" ) ); System.out.println( zonedDatetime ); System.out.println( zonedDatetimeFromClock ); System.out.println( zonedDatetimeFromZone );
这个例子的输出结果是:
2014-04-12T11:47:01.017-04:00[America/New_York] 2014-04-12T15:47:01.017Z 2014-04-12T08:47:01.017-07:00[America/Los_Angeles]
最后看下Duration类,它持有的时间精确到秒和纳秒。这使得我们可以很容易得计算两个日期之间的不同,例子代码如下:
// Get duration between two dates final LocalDateTime from = LocalDateTime.of( 2014, Month.APRIL, 16, 0, 0, 0 ); final LocalDateTime to = LocalDateTime.of( 2015, Month.APRIL, 16, 23, 59, 59 ); final Duration duration = Duration.between( from, to ); System.out.println( "Duration in days: " + duration.toDays() ); System.out.println( "Duration in hours: " + duration.toHours() );
这个例子用于计算2014年4月16日和2015年4月16日之间的天数和小时数,输出结果如下:
Duration in days: 365 Duration in hours: 8783
对于Java 8的新日期时间的总体印象还是比较积极的,一部分是因为Joda-Time的积极影响,另一部分是因为官方终于听取了开发人员的需求。如果希望了解更多细节,可以参考官方文档。
4.4 Nashorn Javascript引擎
Java 8提供了新的Nashorn Javascript引擎,使得我们可以在JVM上开发和运行JS应用。Nashorn Javascript引擎是javax.script.scriptEngine的另一个实现版本,这类script引擎遵循相同的规则,允许Java和Javascript交互使用,例子代码如下:
scriptEngineManager manager = new scriptEngineManager();
scriptEngine engine = manager.getEngineByName( "Javascript" );
System.out.println( engine.getClass().getName() );
System.out.println( "Result:" + engine.eval( "function f() { return 1; }; f() + 1;" ) );
这个代码的输出结果如下:
jdk.nashorn.api.scripting.NashornscriptEngine Result:
4.5 base64
在Java 8中,base64编码成为了Java类库的标准,这样就不需要使用第三方库就可以进行base64编码。base64类同时还提供了对URL、MIME友好的编码器与解码器。示例代码如下:
package com.javacodegeeks.java8.base64;
import java.nio.charset.StandardCharsets;
import java.util.base64;
public class base64s {
public static void main(String[] args) {
final String text = "base64 finally in Java 8!";
final String encoded = base64
.getEncoder()
.encodeToString( text.getBytes( StandardCharsets.UTF_8 ) );
System.out.println( encoded );
final String decoded = new String(
base64.getDecoder().decode( encoded ),
StandardCharsets.UTF_8 );
System.out.println( decoded );
}
}
这个例子的输出结果如下:
QmFzZTY0IGZpbmFsbHkgaW4gSmF2YSA4IQ== base64 finally in Java 8!
新的base64API也支持URL和MINE的编码解码。
(base64.getUrlEncoder() / base64.getUrlDecoder(), base64.getMimeEncoder() / base64.getMimeDecoder())。
base64不是用来加密的,是base64编码后的字符串,全部都是由标准键盘上面的常规字符组成,这样编码后的字符串在网关之间传递不会产生UNICODE字符串不能识别或者丢失的现象。你再仔细研究下EMAIL就会发现其实EMAIL就是用base64编码过后再发送的。然后接收的时候再还原。
4.6 并行数组
Java8版本新增了很多新的方法,用于支持并行数组处理。最重要的方法是parallelSort(),可以显著加快多核机器上的数组排序。下面的例子论证了parallexXxx系列的方法:
package com.javacodegeeks.java8.parallel.arrays;
import java.util.Arrays;
import java.util.concurrent.ThreadLocalRandom;
public class ParallelArrays {
public static void main( String[] args ) {
long[] arrayOfLong = new long [ 20000 ];
Arrays.parallelSetAll( arrayOfLong,
index -> ThreadLocalRandom.current().nextInt( 1000000 ) );
Arrays.stream( arrayOfLong ).limit( 10 ).forEach(
i -> System.out.print( i + " " ) );
System.out.println();
Arrays.parallelSort( arrayOfLong );
Arrays.stream( arrayOfLong ).limit( 10 ).forEach(
i -> System.out.print( i + " " ) );
System.out.println();
}
}
上述这些代码使用parallelSetAll()方法生成20000个随机数,然后使用parallelSort()方法进行排序。这个程序会输出乱序数组和排序数组的前10个元素。上述例子的代码输出的结果是:
Unsorted: 591217 891976 443951 424479 766825 351964 242997 642839 119108 552378 Sorted: 39 220 263 268 325 607 655 678 723 793
4.7 并发性
基于新增的lambda表达式和steam特性,为Java 8中为java.util.concurrent.ConcurrentHashMap类添加了新的方法来支持聚焦操作;另外,也为java.util.concurrentForkJoinPool类添加了新的方法来支持通用线程池操作(更多内容可以参考我们的并发编程课程)。
Java 8还添加了新的java.util.concurrent.locks.StampedLock类,用于支持基于容量的锁——该锁有三个模型用于支持读写操作(可以把这个锁当做是java.util.concurrent.locks.ReadWriteLock的替代者)。
在java.util.concurrent.atomic包中也新增了不少工具类,列举如下:
- DoubleAccumulator
- DoubleAdder
- LongAccumulator
- LongAdder
5. JVM的新特性
使用metaspace(JEP 122) 代替持久代(PermGen Space)。在JVM参数方面,使用-XX:metaSpaceSize和-XX:MaxmetaspaceSize代替原来的-XX:PermSize和-XX:MaxPermSize。
6. 新的Java工具
Java 8提供了一些新的命令行工具,这部分会讲解一些对开发者最有用的工具。
6.1 Nashorn引擎:jjs
Nashorn允许在JVM上开发运行Javascript应用,允许Java与Javascript相互调用。jjs是一个基于标准Nashorn引擎的命令行工具,可以接受js源码并执行。例如,我们写一个func.js文件,内容如下:
function f() {
return 1;
};
print( f() + 1 );
可以在命令行中执行这个命令:jjs func.js,控制台输出结果是:
2
如果需要了解细节,可以参考官方文档。
5.2 类依赖分析器:jdeps
jdeps是一个相当棒的命令行工具,它可以展示包层级和类层级的Java类依赖关系,它以.class文件、目录或者Jar文件为输入,然后会把依赖关系输出到控制台。
我们可以利用jedps分析下Spring framework库,为了让结果少一点,仅仅分析一个JAR文件:org.springframework.core-3.0.5.RELEASE.jar。
jdeps org.springframework.core-3.0.5.RELEASE.jar
这个命令会输出很多结果,我们仅看下其中的一部分:依赖关系按照包分组,如果在classpath上找不到依赖,则显示"not found".
org.springframework.core-3.0.5.RELEASE.jar -> C:Program FilesJavajdk1.8.0jrelibrt.jar org.springframework.core (org.springframework.core-3.0.5.RELEASE.jar) -> java.io -> java.lang -> java.lang.annotation -> java.lang.ref -> java.lang.reflect -> java.util -> java.util.concurrent -> org.apache.commons.logging not found -> org.springframework.asm not found -> org.springframework.asm.commons not found org.springframework.core.annotation (org.springframework.core-3.0.5.RELEASE.jar) -> java.lang -> java.lang.annotation -> java.lang.reflect -> java.util
更多的细节可以参考官方文档。
四. Java 11新特性
2018年9月26号Java 11如期发布,这是Java 8之后的另一LTS版本,也就是Java 8之后,会在企业生产环境中正式使用的版本。Java 11中包含了Java9、Java10版本的全部功能,当然短期内还不可能投入到生产环境中使用。
1. 局部变量类型推断
所谓的局部变量类型推断,就是指左边的类型直接使用var来定义,而不用写具体的类型,编译器能根据右边的表达式内容自动推断出类型。
var str = "Hello 一一哥"; System.out.print(str); // Hello 一一哥
上面的str变量使用var来定义,编译器就能通过右边的 "Hello 一一哥" 自动推断出这是一个String类型的变量。另外我们需要注意,var并不是关键字!
2. String操作方法
Java 11 中增加了一系列的字符串处理方法,如以下所示。
// 判断字符串是否为空白 " ".isBlank(); // true // 去除首尾空格 " yyg ".strip(); // "Javastack" // 去除尾部空格 " yyg ".stripTrailing(); // " yyg" // 去除首部空格 " yyg ".stripLeading(); // "yyg " // 复制字符串,将字符串重复指定次数,如果传递的是负数,则会抛出异常,如果传递的是 0 ,那么就返回空字符串了 "SunYiYi".repeat(3);// "SunYiYiSunYiYiSunYiYi" // 行数统计 "AnBnC".lines().count(); // 3
3. 集合加强
自 Java 9 开始,JDK中就为集合(List/ Set/ Map)添加了 of 和 copyOf 方法,它们两个都用来创建不可变的集合。
var list = List.of("Java11", "Java10", "Java9");
var copy = List.copyOf(list);
System.out.println(list == copy); // true
注意:
使用 of 和 copyOf 创建的集合为不可变集合,不能进行添加、删除、替换、排序等操作,不然会产生java.lang.UnsupportedOperationException 异常!
4. Stream加强
Stream 是 Java 8 中的新特性,Java 9 开始对 Stream 增加了以下 4 个新方法。
4.1 增加ofNullable()方法,参数可为null
Stream.ofNullable(null).count(); // 0
4.2 增加 takeWhile()方法
从1开始计算,当 n < 3 时就截止。
Stream.of(1, 2, 3, 2, 1)
.takeWhile(n -> n < 3)
.collect(Collectors.toList()); // [1, 2]
4.3 增加dropWhile 方法
这个和上面的相反,一旦 n < 3 不成立就开始计算。
Stream.of(1, 2, 3, 2, 1)
.dropWhile(n -> n < 3)
.collect(Collectors.toList()); // [3, 2, 1]
4.4 增加iterate()重载方法
增加iterate()方法的新重载方法,可以让你提供一个 Predicate (判断条件) 来指定什么时候结束迭代。
5. Optional 加强
Opthonal 也增加了几个非常有用的方法,比如我们可以很方便的将一个 Optional 转换成一个 Stream, 或者当一个Optional内容 为空时给它一个替代的内容。
Optional.of("Java11").orElseThrow();//java11
Optional.of("Java11").stream().count();//1
Optional.ofNullable(null).or(() -> Optional.of("Java11")).get();// java11
6. InputStream 加强
InputStream新增了一个transferTo()方法,可以用来将数据直接传输到 OutputStream,这是在处理原始数据流时非常常见的一种用法。
var classLoader = ClassLoader.getSystemClassLoader();
var inputStream = classLoader.getResourceAsStream("Java11.txt");
var javastack = File.createTempFile("Java11.1", "txt");
try (var outputStream = new FileOutputStream(javastack)) {
inputStream.transferTo(outputStream);
}
7. HTTP Client API
这是 Java 9 开始引入的一个处理 HTTP 请求的的 API,该 API 支持同步和异步,而在 Java 11 中已经为正式可用状态,我们可以在 java.net 包中找到这个 API,示例如下。
var request = HttpRequest.newBuilder()
//设置请求地址
.uri(URI.create("https://yiyige.blog.csdn.net/"))
//默认为get请求,可以省略
.GET()
.build();
var client = HttpClient.newHttpClient();
//同步请求
HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofString());
System.out.println(response.body());
//异步请求
client.sendAsync(request, HttpResponse.BodyHandlers.ofString())
.thenApply(HttpResponse::body)
.thenAccept(System.out::println);
// 创建POST请求
HttpRequest postRequest = HttpRequest.newBuilder()
.uri(URI.create("https://postman-echo.com/post"))
.header("Content-Type", "application/json")
.POST(HttpRequest.BodyPublishers.ofString(JSON.toJSonString(Map.of("method", "POST"))))
.build();
System.out.println(client.send(postRequest, HttpResponse.BodyHandlers.ofString()).body());
整体来看,Java 11中并没有新增特别多的特性,主要还是在已有特性基础上进行了增强和优化。
五. 结语
至此,壹哥 就把Java 8 和Java 11中的新特性给大家简要介绍了一些,我们挑选几个重点常用的新特性,比如Stream流、Lambda表达式、Optional、Date、Time等内容重点给面试官介绍即可。
如果你能够把本题目回答的非常好,就会给面试官留下一个爱学习爱钻研的好印象,对我们程序员来说,这是非常重要的一个品质!记住,一个不爱学习的程序员,一定不是一个好程序员!如果你想在IT行业里长远发展,请保持时刻学习的状态,否则请慎入这个行业!



