在之前的几个章节中,壹哥 带各位复习的知识点与Java版本没有特别大的关系,今天我们就来复习一道与Java特定版本有关系的面试题:
2. 题目剖析你了解哪些Java(JDK)的新特性?
Java8的新特性你知道哪些?
Java11的新特性你知道哪些?
......
这道题目,其实回答起来并没有特别大的难度,主要是考察我们对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 Suppliersupplier) { 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参数:
4. Java官方库的新特性org.apache.maven.plugins maven-compiler-plugin3.1 -parameters 1.8 1.8
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包中也新增了不少工具类,列举如下:
5. JVM的新特性
- DoubleAccumulator
- DoubleAdder
- LongAccumulator
- LongAdder
使用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(); // 33. 集合加强
自 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();// java116. 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(); //同步请求 HttpResponseresponse = 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行业里长远发展,请保持时刻学习的状态,否则请慎入这个行业!
欢迎分享,转载请注明来源:内存溢出
评论列表(0条)