拥抱变化,面向Java17,Java8-18全系列特性详解

Posted 阿提说说

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了拥抱变化,面向Java17,Java8-18全系列特性详解相关的知识,希望对你有一定的参考价值。

文章目录:

  1. Java 8 新特性
  2. Java 9 新特性
  3. Java 10 新特性
  4. Java 11 新特性
  5. Java 12 新特性
  6. Java 13 新特性
  7. Java 14 新特性
  8. Java 15 新特性
  9. Java 16 新特性
  10. Java 17 新特性
  11. Java 18 新特性

💡 文章较长,建议点赞、收藏、评论后慢慢看,合理利用 “只看目录功能

前言

当我们大部分Javaer还沉浸在Java 8 的特性中时,Java 19 预计在2022年9月20号发布,现在半年发布一次的节奏真让人应接不暇,况且Spring Boot 3.0开始最低版本为Java 17,Spring Security、KafKa等也都宣布在后期版本最低需要Java 17 ,所以我们恶补一下Java 8-18的特性很有必要。

Java 8 新特性

Java 8 带来了大量的新特性。主要分为以下几个方面:语言、它的编译器、库、工具和 JVM(Java 虚拟机)。
这个教程包含Java开发者经常面对的几类问题:

  • 语言
  • 编译器
  • 工具
  • 运行时(JVM)

感兴趣的话,可以看下官网的描述:
https://docs.oracle.com/en/java/javase/index.html

语言新特性

Lambda表达式和函数式接口

一个概念:函数式接口:只有一个方法的接口。
Lambda表达式(也称为闭包)是Java 8中最大和最令人期待的语言改变。
它允许我们将函数当成参数传递给某个方法,或者把代码本身当作数据处理:函数式开发者非常熟悉这些概念。很多JVM平台上的语言(Groovy、Scala等)从诞生之日就支持Lambda表达式,但是Java开发者没有选择,只能使用匿名内部类代替Lambda表达式。
Lambda的设计耗费了很多时间和很大的社区力量,最终找到一种折中的实现方案,可以实现简洁而紧凑的语言结构。最简单的Lambda表达式可由逗号分隔的参数列表、-> 符号和语句块组成。

示例1:

Arrays.asList( "a", "b", "d" ).forEach( e -> System.out.println( e ) );

在上面这个代码中的参数e的类型是由编译器推理得出的,你也可以显式指定该参数的类型,例如:

Arrays.asList( "a", "b", "d" ).forEach( ( String e ) -> System.out.println( e ) );

示例2:
如果Lambda表达式需要更复杂的语句块,则可以使用花括号将该语句块括起来,类似于Java中的函数体,例如

Arrays.asList( "a", "b", "d" ).forEach( e -> 
    System.out.print( e );
    System.out.print( e );
 );

示例3:
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 ) );

示例4:
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 );
    return result;
 );

Lambda的设计者们为了让现有的功能与Lambda表达式良好兼容,考虑了很多方法,于是产生了 函数接口 这个概念。
函数接口指的是只有一个函数的接口,这样的接口可以隐式转换为Lambda表达式。java.lang.Runnablejava.util.concurrent.Callable是函数式接口的最佳例子。
在实践中,函数式接口非常脆弱:只要某个开发者在该接口中添加一个函数,则该接口就不再是函数式接口进而导致编译失败。
为了克服这种代码层面的脆弱性,并显式说明某个接口是函数式接口,Java 8 提供了一个特殊的注解 @FunctionalInterface(Java 库中的所有相关接口都已经带有这个注解了),举个简单的函数式接口的定义:

@FunctionalInterface
public interface Functional 
    void method();

Lambda表达式作为Java 8的最大卖点,它有潜力吸引更多的开发者加入到JVM平台,并在纯Java编程中使用函数式编程的概念。如果你需要了解更多Lambda表达式的细节,可以参考官方文档

接口默认方法和静态方法

Java 8使用两个新概念扩展了接口的含义:默认方法和静态方法。
接口中可用定义默认方法
默认方法使得接口有点类似traits,不过要实现的目标不一样。默认方法使得开发者可以在不破坏二进制兼容性的前提下,往现存接口中添加新的方法,即不强制那些实现了该接口的类也同时实现这个新加的方法。
默认方法和抽象方法之间的区别在于抽象方法需要实现,而默认方法不需要。接口提供的默认方法会被接口的实现类继承或者覆写,例子代码如下:

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类也实现了这个接口,但覆写了该接口的默认方法,并提供了一个不同的实现。

接口中可用定义静态方法
Java 8带来的另一个有趣的特性是在接口中可以定义静态方法,例子代码如下:

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()**等等。
尽管默认方法有这么多好处,但在实际开发中应该谨慎使用:在复杂的继承体系中,默认方法可能引起歧义和编译错误。如果你想了解更多细节,可以参考官方文档

方法引用

方法引用使得开发者可以直接引用现存的方法、Java类的构造方法或者实例对象。方法引用和Lambda表达式配合使用,使得java类的构造方法看起来紧凑而简洁,没有很多复杂的模板代码。
下面的例子中,Car类是不同方法引用的例子,可以帮助读者区分四种类型的方法引用。

public static class Car 
    public static Car create( final Supplier<Car> 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() );
    


第一种方法引用的类型是构造器引用,语法是Class::new,或者更一般的形式:Class< T>::new。注意:这个构造器没有参数。

final Car car = Car.create( Car::new );
final List<Car> cars = Arrays.asList( car );

第二种方法引用的类型是静态方法引用,语法是Class::static_method。注意:这个方法接受一个Car类型的参数。

cars.forEach( Car::collide );

第三种方法引用的类型是某个类的成员方法的引用,语法是Class::method,注意,这个方法没有定义入参:

cars.forEach( Car::repair );

第四种方法引用的类型是某个实例对象的成员方法的引用,语法是instance::method。注意:这个方法接受一个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

重复注解

自从Java 5中引入注解以来,这个特性开始变得非常流行,并在各个框架和项目中被广泛使用。不过,注解有一个很大的限制是:在同一个地方不能多次使用同一个注解。Java 8打破了这个限制,引入了重复注解的概念,允许在同一个地方多次使用同一个注解。
在Java 8中使用**@Repeatable**注解定义重复注解,实际上,这并不是语言层面的改进,而是编译器做的一个trick,底层的技术仍然相同。可以利用下面的代码说明:

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

如果你希望了解更多内容,可以参考官方文档

更好的类型推断

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< String> 类型的应用:

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.defaultValue()。

拓宽注解的应用场景

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_USERElementType.TYPE_PARAMETER 是Java 8新增的两个注解,用于描述注解的使用场景。Java 语言也做了对应的改变,以识别这些新增的注解。

编译器的新特性

参数名称

在运行时获得Java程序中方法的参数名称
为了在运行时获得Java程序中方法的参数名称,老一辈的Java程序员必须使用不同方法,例如Paranamer liberary。Java 8终于将这个特性规范化,在语言层面(使用反射API和 Parameter.getName()方法 )和字节码层面(使用新的javac编译器以及 -parameters 参数)提供支持。

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**参数:

<plugin>
    <groupId>org.apache.maven.plugins</groupId>
    <artifactId>maven-compiler-plugin</artifactId>
    <version>3.1</version>
    <configuration>
        <compilerArgument>-parameters</compilerArgument>
        <source>1.8</source>
        <target>1.8</target>
    </configuration>
</plugin>

官方库的新特性

Optional

Java应用中最常见的bug就是空值异常。在Java 8之前,Google Guava引入了Optionals类来解决NullPointerException,从而避免源码被各种null检查污染,以便开发者写出更加整洁的代码。
Java 8也将Optional加入了官方库。
Optional仅仅是一个功能:存放T类型的值或者null。它提供了一些有用的接口来避免显式的null检查,可以参考Java 8官方文档了解更多细节。
接下来看一点使用Optional的例子:可能为空的值或者某个类型的值:

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( 程序员要拥抱变化,聊聊Android即将支持的Java 8

程序员要拥抱变化,聊聊Android即将支持的Java 8

iOS 7系列译文:忘记NSURLConnection,拥抱NSURLSession吧!

拥抱函数式编程吧,用得非常爽!

Java8:18个日期处理的实践

生还是不生? SpringBoot3 版本有起飞前兆,最小依赖Java17!