Loading

Java 8 新特性

Java 8 (又称为 jdk 1.8) 是 Java 语言开发的一个主要版本。 Oracle 公司于 2014 年 3 月 18 日发布 Java 8 ,这个版本包含语言、编译器、库、工具和JVM等方面的十多个新特性。 下面就来介绍下语言方面的新特性。

语法相关新特性

默认接口方法

从 Java 8 开始,接口支持定义默认实现方法。所谓的默认方法,就是指接口中定义的抽象方法可以由接口本身提供默认实现,而不一定要实现类去实现这个抽象方法。

比如我定义了一个 Programmer 接口,在 Java 8 之前,我们必须在实现类中实现所有的抽象方法,比如下面的 JavaProgrammer 类。

package com.csx.feature.defaultm;
public interface Programmer {
    /**
     * 编程操作
     */
    void coding();

    /**
     * 介绍自己
     * @return
     */
    String introduce();
}

实现类:

package com.csx.feature.defaultm;
public class JavaProgrammer implements Programmer {
    @Override
    public void coding() {
        System.out.println("l am writing a bug...");
    }

    @Override
    public String introduce() {
        return "hi, l am a Java programmer";
    }
}

在 Java 8 中,我们可以使用 default 关键字来定义接口中的默认方法实现。比如下面我们将introduce方法定义成一个具有默认实现的方法。

package com.csx.feature.defaultm;
public interface Programmer {

    /**
     * 编程操作
     */
    void coding();

    /**
     * 介绍自己
     * @return
     */
    default String introduce(){
        return "hi, l am a C++ programmer";
    }
}

我们再定义一个实现类,就不需要再实现这个方法了(当然,实现类中还是可以实现这个方法的,实现的方法会将接口的默认方法覆盖)。

package com.csx.feature.defaultm;
public class CJJProgrammer implements Programmer {
    @Override
    public void coding() {
        System.out.println("l am writing a bug.......");
    }
}

上面的实现类 CJJProgrammer 就不再必须要实现 introduce 方法,因为这个方法在接口中已经有默认实现了。

使用接口默认方法的最主要目的是:修改接口后不需要大范围的修改以前老的实现类

比如说现在给 Programmer 接口新添加一个新的方法 reading():

package com.csx.feature.defaultm;
public interface Programmer {
    
    void reading();
       
    void coding();

    default String introduce(){
        return "hi, l am a C++ programmer";
    }

}

新添加这个方法后,每个实现类中必须也加上这个方法的实现,不然代码编译会报错。假如之前的实现类很多的话,那么修改的工作量将是非常大的。这时你就可以为这个方法提供默认实现:

public interface Programmer {
    
    default void reading(){
        System.out.println("reading 11.11 shopping list...");
    }
       
    void coding();

    default String introduce(){
        return "hi, l am a C++ programmer";
    }

}

多个默认方法

考虑这样的情况,一个类实现了多个接口,且这些接口有相同的默认方法。

public interface Vehicle {
   default void print(){
      System.out.println("我是一辆车!");
   }
}
 
public interface FourWheeler {
   default void print(){
      System.out.println("我是一辆四轮车!");
   }
}

第一个解决方案是创建自己的默认方法,来覆盖重写接口的默认方法:

public class Car implements Vehicle, FourWheeler {
   default void print(){
      System.out.println("我是一辆四轮汽车!");
   }
}

第二种解决方案可以使用 super 来调用指定接口的默认方法:

public class Car implements Vehicle, FourWheeler {
   public void print(){
      Vehicle.super.print();
   }
}

静态默认方法

Java 8 的另一个特性是接口可以声明(并且可以提供实现)静态方法。

package com.csx.feature.defaultm;
public interface Programmer {
    
    static final String  BLOG = "程序员自由之路";
   
    static String blogName(){
        return BLOG;
    }
    
    void coding();

    default String introduce(){
        return "hi, l am a C++ programmer";
    }
}

Lambda 表达式

Lambda 表达式,也可称为闭包,它是推动 Java 8 发布的最重要新特性。Lambda 允许把函数作为一个方法的参数(函数作为参数传递进方法中)。

使用 Lambda 表达式可以使代码变的更加简洁紧凑,其本质是一个Java语法糖,具体内容可以参考我的博客Java中的语法糖中关于Lambda的章节。

Lambda 表达式的语法如下:


(p1) -> exp;
或者
(p1,p2) -> {
    exp1;
    exp2;
}

以下是lambda表达式的重要特征:

  • 可选类型声明:不需要声明参数类型,编译器可以统一识别参数值。
  • 可选的参数圆括号:一个参数无需定义圆括号,但多个参数需要定义圆括号。
  • 可选的大括号:如果主体包含了一个语句,就不需要使用大括号。
  • 可选的返回关键字:如果主体只有一个表达式返回值则编译器会自动返回值,大括号需要指定明表达式返回了一个数值。

lambda 表达式的最大作用是简化了匿名内部类的使用,是的代码看起来更加简洁。(注意:Lambda 表达式并不能提升代码执行的效率)。
lambda 表达式还有一个作用就是推动了 Java 中的函数化编程,使得可以将一个函数作为参数传给方法。(之前必须传一个对象的引用给方法,然后再通过这个对象引用调用具体的方法)。

变量作用域

lambda 表达式只能引用不被修改的外层局部变量,否则会编译错误。

int num = 1;  
Converter<Integer, String> s = (param) -> System.out.println(String.valueOf(param + num));
s.convert(2);
// 这个 num 变量被 lambda 表达式引用,又被修改了,所以会编译报错。
num = 5;

其实如果lambda表达式引用了外层的局部变量,编译器会自动将这个变量设置成final修饰。

final int num = 1;  
Converter<Integer, String> s = (param) -> System.out.println(String.valueOf(param + num));
s.convert(2);
// final变量赋值后不能修改,所以会编译报错。
// num = 5;

函数式接口

函数式接口(Functional Interface)就是一个有且仅有一个抽象方法,但是可以有多个非抽象方法的接口。函数式接口可以被隐式转换为 lambda 表达式。

@FunctionalInterface
interface GreetingService {
    // 非抽象接口可以有多个
    static void sayBye(){
        System.out.println("bye");
    }

    void sayMessage(String message);
}

那么就可以使用Lambda表达式来表示该接口的一个实现(注:JAVA 8 之前一般是用匿名类实现的):

GreetingService greetService1 = message -> System.out.println("Hello " + message);

从 Java 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

同时,Java 8 也增加了很多新的函数式接口,主要在java.util.function这个包下面。常用的有:

  • Predicate:接受一个输入参数,返回一个布尔值结果。

  • Supplier:无参数,返回一个结果。

  • Consumer:代表了接受一个输入参数并且无返回的操作

在实践中,函数式接口非常脆弱:只要某个开发者在该接口中添加一个函数,则该接口就不再是函数式接口进而导致编译失败。为了克服这种代码层面的脆弱性,并显式说明某个接口是函数式接口,Java 8 提供了一个特殊的注解@FunctionalInterface(Java 库中的所有相关接口都已经带有这个注解了)。

不过有一点需要注意,默认方法和静态方法不会破坏函数式接口的定义,因此如下的代码是合法的。

@FunctionalInterface
interface GreetingService {
    // 非抽象接口可以有多个
    static void sayBye(){
        System.out.println("bye");   
    }
   
   default void sayHi() {            
        System.out.println("bye");   
    }        
  
    void sayMessage(String message);
}

方法引用

在学习lambda表达式之后,我们通常使用lambda表达式来创建匿名方法。然而,有时候我们仅仅是调用了一个已存在的方法。如下:

Arrays.sort(stringsArray,(s1,s2)->s1.compareToIgnoreCase(s2));

在Java8中,我们可以直接通过方法引用来简写lambda表达式中已经存在的方法。

Arrays.sort(stringsArray, String::compareToIgnoreCase);

这种特性就叫做方法引用(Method Reference)。

方法引用是用来直接访问类或者实例的已经存在的方法或者构造方法。方法引用提供了一种引用而不执行方法的方式,它需要由兼容的函数式接口构成的目标类型上下文。计算时,方法引用会创建函数式接口的一个实例。我们需要把握的重点是:函数引用只是简化Lambda表达式的一种手段而已。

当Lambda表达式中只是执行一个方法调用时,不用Lambda表达式,直接通过方法引用的形式可读性更高一些。方法引用是一种更简洁易懂的Lambda表达式。

注意方法引用是一个特殊的Lambda表达式,其中方法引用的操作符是双冒号"::"。

具体关于方法引用的内容,请参考这篇文章

下面就举个列子:

方法引用的标准形式是:类名::方法名。(注意:只需要写方法名,不需要写括号

有以下四种形式的方法引用:

类型 示例
引用静态方法 ContainingClass::staticMethodName
引用某个对象的实例方法 containingObject::instanceMethodName
引用某个类型的任意对象的实例方法 ContainingType::methodName
引用构造方法 ClassName::new

1、静态方法引用

组成语法格式:ClassName::staticMethodName

注意:

  • 静态方法引用比较容易理解,和静态方法调用相比,只是把 . 换为 ::
  • 在目标类型兼容的任何地方,都可以使用静态方法引用。

例子:

  String::valueOf 等价于lambda表达式 (s) -> String.valueOf(s);

  Math::pow 等价于lambda表达式 (x, y) -> Math.pow(x, y);

2、特定实例对象的方法引用

这种语法与用于静态方法的语法类似,只不过这里使用对象引用而不是类名。****实例方法引用又分以下三种类型:

  • 实例上的实例方法引用

    组成语法格式:instanceReference::methodName

  • 超类上的实例方法引用

    组成语法格式:super::methodName

    方法的名称由methodName指定,通过使用super,可以引用方法的超类版本。

  • 类型上的实例方法引用

    组成语法格式:ClassName::methodName (会先创建一个对象??)

3、任意对象(属于同一个类)的实例方法引用

如下示例,这里引用的是字符串数组中任意一个对象的compareToIgnoreCase方法。

String[] stringArray = { "Barbara", "James", "Mary", "John", "Patricia", "Robert", "Michael", "Linda" };
Arrays.sort(stringArray, String::compareToIgnoreCase);

4、构造方法引用

构造方法引用又分构造方法引用和数组构造方法引用。

a.构造方法引用(也可以称作构造器引用)

组成语法格式:Class::new

构造函数本质上是静态方法,只是方法名字比较特殊,使用的是new 关键字

例子:String::new, 等价于lambda表达式 () -> new String()

b.数组构造方法引用

组成语法格式:TypeName[]::new

例子:int[]::new 是一个含有一个参数的构造器引用,这个参数就是数组的长度。等价于lambda表达式 x -> new int[x]。

假想存在一个接收int参数的数组构造方法

IntFunction<int[]> arrayMaker = int[]::new;
int[] array = arrayMaker.apply(10) // 创建数组 int[10]

支持重复注解并拓宽注解的应用场景

自从Java 5中引入注解以来,这个特性开始变得非常流行,并在各个框架和项目中被广泛使用。不过,注解有一个很大的限制是:在同一个地方不能多次使用同一个注解。Java 8打破了这个限制,引入了重复注解的概念,允许在同一个地方多次使用同一个注解。

在Java 8中使用@Repeatable注解定义重复注解,实际上,这并不是语言层面的改进,而是编译器做的一个trick,底层的技术仍然相同。

Java 8拓宽了注解的应用场景。现在,注解几乎可以使用在任何元素上:局部变量、接口类型、超类和接口实现类,甚至可以用在函数的异常定义上。

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 语言也做了对应的改变,以识别这些新增的注解。

具体支持哪些使用场景,建议查看ElementType这个类。

工具相关新特性

Stream API

Java 8 API添加了一个新的抽象称为流Stream,可以让你以一种声明的方式处理数据。

Stream 使用一种类似用 SQL 语句从数据库查询数据的直观方式来提供一种对 Java 集合运算和表达的高阶抽象。

Stream API可以极大提高Java程序员的生产力,让程序员写出高效率、干净、简洁的代码。

这种风格将要处理的元素集合看作一种流, 流在管道中传输, 并且可以在管道的节点上进行处理, 比如筛选, 排序,聚合等。

元素流在管道中经过中间操作(intermediate operation)的处理,最后由最终操作(terminal operation)得到前面处理的结果。

关于 Stream API 的具体使用方式,我之前写过一篇文章详细介绍过。点击谈谈集合.Stream API前往阅读。

Optional 类

Java应用中最常见的bug就是空值异常。在Java 8之前,Google Guava引入了Optionals类来解决NullPointerException,从而避免源码被各种null检查污染,以便开发者写出更加整洁的代码。Java 8也将Optional加入了官方库。

Optional仅仅是一个容易:存放T类型的值或者null。它提供了一些有用的接口来避免显式的null检查,可以参考Java 8官方文档了解更多细节。

关于这个类的具体使用方式,可以参考我整理的这篇文章

另外,建议大家看看流行的开源框架中,这些新特性是怎么使用的。我们在没有熟练掌握这些新功能之前,不妨模仿下这些框架的用法,也不失为一种好的学习方法。

时间API

Java 8引入了新的Date-Time API(JSR 310)来改进时间、日期的处理。时间和日期的管理一直是最令Java开发者痛苦的问题。java.util.Date和后来的java.util.Calendar一直没有解决这个问题(甚至令开发者更加迷茫)。

因为上面这些原因,诞生了第三方库Joda-Time,可以替代Java的时间管理API。Java 8中新的时间和日期管理API深受Joda-Time影响,并吸收了很多Joda-Time的精华。新的java.time包包含了所有关于日期、时间、时区、Instant(跟日期类似但是精确到纳秒)、duration(持续时间)和时钟操作的类。新设计的API认真考虑了这些类的不变性(从java.util.Calendar吸取的教训),如果某个实例需要修改,则返回一个新的对象。

关于时间API的详细使用,可以参考我之前整理的文章

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: 2

Base64

对Base64编码的支持已经被加入到Java 8官方库中,这样不需要使用第三方库就可以进行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 );
    }
}

新的Base64API也支持URL和MINE的编码解码。
(Base64.getUrlEncoder() / Base64.getUrlDecoder(), Base64.getMimeEncoder() / Base64.getMimeDecoder())。

并行数组

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个元素。

并发相关新特性

基于新增的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

Java命令行工具相关

Nashorn引擎:jjs

jjs是一个基于标准Nashorn引擎的命令行工具,可以接受js源码并执行。例如,我们写一个func.js文件,内容如下:

function f() { 
     return 1; 
}; 
 
print( f() + 1 );

可以在命令行中执行这个命令:jjs func.js,控制台输出结果是:

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 Files\Java\jdk1.8.0\jre\lib\rt.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

更多的细节可以参考官方文档

编译器相关特性

1. 获取方法的参数名称

为了在运行时获得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>

JVM的新特性

使用Metaspace(JEP 122)代替持久代(PermGen space)。在JVM参数方面,使用-XX:MetaSpaceSize和-XX:MaxMetaspaceSize代替原来的-XX:PermSize和-XX:MaxPermSize。

參考

posted @ 2021-03-01 16:24  程序员自由之路  阅读(781)  评论(0编辑  收藏  举报