Skip to content

Files

Latest commit

eb0ea53 · Sep 21, 2021

History

History
612 lines (459 loc) · 23 KB

File metadata and controls

612 lines (459 loc) · 23 KB

三、Java11 基础

在最后一章中,我们介绍了 Java9、10 和 11 引入的 Java 平台的一些令人印象深刻的新特性。我们关注 Javac、JDK 库和各种测试套件。内存管理的改进,包括堆空间效率、内存分配和改进的垃圾收集,代表了一组强大的 Java 平台增强功能。关于提高效率的汇编过程的变化是我们这一章的一部分。我们还介绍了一些重要的改进,例如有关编译过程、类型测试、注解和自动运行时编译器测试的改进。

本章介绍对 Java 平台的一些更改,这些更改会影响变量处理器、导入语句、对 Coin 项目的改进、局部变量类型推断、根证书、动态类文件常量等等。这些表示对 Java 语言本身的更改。

我们将在本章介绍的具体主题如下:

  • 变量处理器
  • import语句废弃警告
  • Coin 项目
  • import语句处理
  • 推断局部变量
  • 线程本地握手
  • 备用内存设备上的堆分配
  • 根证书
  • 动态类文件常量
  • 删除 JavaEE 和 CORBA 模块

技术要求

本章及后续章节主要介绍 Java11,Java 平台的标准版SE)可从 Oracle 官方网站下载。

IDE 包就足够了。来自 JetBrains 的 IntelliJ IDEA 用于与本章和后续章节相关的所有编码。IntelliJ IDEA 的社区版可从网站下载。

本章的源代码可以在 GitHub 上找到。

使用变量处理器

变量处理器是对变量的类型化引用,由java.lang.invoke.VarHandle抽象类控制。VarHandle方法的签名是多态的。这使得方法签名和返回类型都具有很大的可变性。下面是一个代码示例,演示如何使用VarHandle

. . .
class Example {
  int myInt;
  . . .
}
. . .
class Sample {
  static final VarHandle VH_MYINT;

  static { 
    try {
      VH_MYINT =
        MethodHandles.lookup().in(Example.class)
        .findVarHandle(Example.class, "myInt", int.class);
    }
    catch (Exception e) {
      throw new Error(e);
    }
  }
}
. . .

正如您在前面的代码片段中所看到的,VarHandle.lookup()执行的操作与由MethodHandle.lookup()方法执行的操作相同。

对 Java 平台的这一更改旨在标准化调用以下类的方法的方式:

  • java.util.concurrent.atomic
  • sun.misc.Unsafe

具体地说,是执行以下操作的方法:

  • 访问/修改对象字段
  • 数组的已访问/已修改元素

此外,这种变化导致了内存排序和对象可达性的两种栅栏操作。本着尽职尽责的精神,特别注意确保 JVM 的安全。确保这些更改不会导致内存错误非常重要。数据完整性、可用性,当然还有性能是上述尽职调查的关键组成部分,解释如下:

  • 安全:不能出现损坏的内存状态。

  • 数据完整性:必须确保对对象字段的访问使用相同的规则:

  • getfield字节码

  • putfield字节码

  • 可用性:可用性的基准是sun.misc.UnsafeAPI。目标是使新的 API 比基准更易于使用。

  • 性能:与sun.misc.UnsafeAPI 相比,性能无下降。目标是超越 API。

在 Java 中,栅栏操作是 Javac 以屏障指令的形式强制内存约束的操作。这些操作发生在屏障指令之前和之后,本质上是将它们封闭起来。

使用原子工具包

java.util.concurrent.atomic包是 12 个子类的集合,它们支持对线程安全和无锁的单个变量的操作。在此上下文中,线程安全是指访问或修改共享单个变量而不妨碍其他线程同时对该变量执行的代码。这个超类是在 Java7 中引入的。

下面是原子工具箱中 12 个子类的列表。如您所料,类名是自描述性的:

  • java.util.concurrent.atomic.AtomicBoolean
  • java.util.concurrent.atomic.AtomicInteger
  • java.util.concurrent.atomic.AtomicIntegerArray
  • java.util.concurrent.atomic.AtomicIntegerFieldUpdater<T>
  • java.util.concurrent.atomic.AtomicLong
  • java.util.concurrent.atomic.AtomicLongArray
  • java.util.concurrent.atomic.AtomicLongFieldUpdater<T>
  • java.util.concurrent.atomic.AtomicMarkableReference<V>
  • java.util.concurrent.atomic.AtomicReference<V>
  • java.util.concurrent.atomic.AtomicReferenceArray<E>
  • java.util.concurrent.atomic.AtomicReferenceFieldUpdater<T,V>
  • java.util.concurrent.atomic.AtomicStampedReference<V>

使用 AtoMIC 工具箱的关键是理解可变变量。可变变量、字段和数组元素可以由并发线程异步修改。

在 Java 中,volatile关键字用于通知 Javac 工具从主存中读取值、字段或数组元素,而不是缓存它们。

下面是一段代码片段,演示了对实例变量使用volatile关键字:

public class Sample {
  private static volatile Sample myVolatileVariable; // a volatile   
  //
instance     
  //variable

  // getter method
  public static Sample getVariable() { 
    if (myVolatileVariable != null) {
      return myVolatileVariable;
    }

    // this section executes if myVolatileVariable == null
    synchronized(Sample.class) {
      if (myVolatileVariable == null) {
        myVolatileVariable = new Sample();
      }
    }
    return null;
  }
}

使用sun.misc.Unsafe

sun.misc.Unsafe类和其他sun类一样,没有正式的文档记录或支持。它被用来规避 Java 的一些内置内存管理安全特性。虽然这可以看作是我们代码中实现更大控制和灵活性的窗口,但这是一种糟糕的编程实践。

该类只有一个私有构造器,因此无法轻松实例化该类的实例。所以,如果我们尝试用myUnsafe = new Unsafe()实例化一个实例,在大多数情况下都会抛出SecurityException。这个有些不可访问的类有 100 多个方法,允许对数组、类和对象进行操作。以下是这些方法的简单示例:

数组 对象
arrayBaseOffset defineAnonymousClass allocateInstance
arrayIndexScale defineClass objectFieldOffset
ensureClassInitialized
staticFieldOffset

以下是用于信息、内存和同步的sun.misc.Unsafe类方法的第二个分组:

信息 存储器 同步
addressSize allocateMemory compareAndSwapInt
pageSize copyMemory monitorEnter
freeMemory monitorExit
getAddress putOrderedEdit
getInt tryMonitorEnter
putInt

在 Java9 中,sun.misc.Unsafe类被指定要删除。实际上,编程行业对这一决定有一些反对意见。为了平息他们的担忧,这个阶级已经被贬低了,但不会被完全消除。

import语句废弃警告

通常,当我们编译程序时,会收到许多警告和错误。编译器错误必须被修复,因为它们在本质上是典型的语法错误。另一方面,应该对警告进行审查并适当处理。开发人员忽略了一些警告消息。

Java9 略微减少了我们收到的警告数量。特别是,不再生成由导入报表引起的废弃警告。在 Java9 之前,我们可以使用以下注解抑制不推荐使用的警告消息:

@SupressWarnings

现在,如果以下一种或多种情况为真,编译器将抑制废弃警告:

  • 如果使用@Deprecated注解
  • 如果使用@SuppressWarnings注解
  • 如果警告生成代码和声明在祖先类中使用
  • 如果警告生成代码在import语句中使用

Coin 项目

Coin 项目是 Java7 中引入的一组小改动的特性集。这些变化如下:

  • switch语句中的字符串
  • 二进制整数字面值
  • 在数字文本中使用下划线
  • 实现多重捕获
  • 允许更精确地重新触发异常
  • 泛型实例创建的改进
  • 带资源的try语句的添加
  • 调用varargs方法的改进

详细信息见以下 Oracle 演示

对于 Java9 版本,Coin 项目有五个改进。这些增强功能将在下面的部分中详细介绍。

使用@SafeVarargs注解

从 Java9 开始,我们可以将@SafeVarargs注解与私有实例方法结合使用。当我们使用这个注解时,我们断言这个方法不包含对作为参数传递给这个方法的varargs的任何有害操作。

使用的语法如下:

@SafeVarargs // this is the annotation
static void methodName(...) {

/*
  The contents of the method or constructor must not
  perform any unsafe or potentially unsafe operations
  on the varargs parameter or parameters.
*/
}

@SafeVarargs注解的使用仅限于以下内容:

  • 静态方法
  • 最终方法
  • 私有实例方法

带资源的try语句

带资源的try语句以前要求在使用final变量时为语句中的每个资源声明一个新变量。以下是 Java9 之前的带资源的try语句的语法(在 Java7 或 Java8 中):

try ( // open resources ) {
  // use resources
} catch (// error) { 
  // handle exceptions
}
// automatically close resources

以下是使用上述语法的代码段:

try ( Scanner xmlScanner = new Scanner(new File(xmlFile)); {
  while (xmlScanner.hasNext()) {
    // read the xml document and perform needed operations
    }
  xmlScanner.close();
  } catch (FileNotFoundException fnfe) {
  System.out.println("Your XML file was not found.");
}

自 Java9 以来,带资源的try语句可以管理final变量,而不需要新的变量声明。因此,我们现在可以重写 Java9、10 或 11 中的早期代码,如图所示:

Scanner xmlScanner = new Scanner(newFile(xmlFile));
try ( while (xmlScanner.hasNext()) {
  {
    // read the xml document and perform needed operations
  }
  xmlScanner.close();
} catch (FileNotFoundException fnfe) {
    System.out.println("Your XML file was not found.");
  }

如您所见,xmlScanner对象引用包含在带资源的try语句块中,它提供了自动资源管理。一旦退出带资源的try语句块,资源将自动关闭。

您也可以使用finally块作为带资源的try语句的一部分。

使用菱形运算符

Java9 中引入了菱形操作符,如果推断的数据类型是可表示的,那么菱形操作符可以用于匿名类。当推断出数据类型时,它表明 Java 编译器可以确定方法调用中的数据类型。这包括声明和其中包含的任何参数。

菱形运算符是小于和大于符号对(<>),它对 Java9 并不陌生,相反,匿名类的具体用法是。

菱形操作符是在 Java7 中引入的,它简化了泛型类的实例化。以下是 Java7 之前的一个示例:

ArrayList<Student> roster = new ArrayList<Student>();

然后,在 Java7 中,我们可以重写它:

ArrayList<Student> roster = new ArrayList<>();

问题是这个方法不能用于匿名类。下面是 Java8 中一个运行良好的示例:

public interface Example<T> {
  void aMethod() {
    // interface code goes here
  }
}

Example example = new Example<Integer>()
{
  @Override
  public void aMethod() {
    // code
  }
};

虽然前面的代码可以正常工作,但当我们将其更改为使用菱形运算符时(如图所示),将出现编译器错误:

public interface Example<T> {
  void aMethod() {
    // interface code goes here
  }
}

Example example = new Example<>() 
{
  @Override
  public void aMethod() { 
    // code
  }
};

该错误是由于对匿名内部类使用菱形运算符而导致的。Java9 救命!虽然前面的代码在 Java8 中会导致编译时错误,但在 Java9、10 和 11 中工作正常。

停止使用下划线

下划线字符(_)不能再用作合法的标识符名称。先前删除标识符名称中下划线的尝试是不完整的。使用下划线将产生错误和警告的组合。自 Java9 以来,警告现在是错误。考虑以下示例代码:

public class UnderscoreTest {
  public static void main(String[] args) {
    int _ = 319;
    if ( _ > 300 ) {
      System.out.println("Your value us greater than 300.");
    }
    else {
      System.out.println("Your value is not greater than 300.");
    }
  }
}

在 Java8 中,前面的代码将导致针对int _ = 319;if ( _ > 300语句的编译器警告。警告是:As of Java9, '_' is a keyword, and may not be used as an identifier。因此,在 Java9、10 或 11 中,不能单独使用下划线作为合法标识符。

使用非自描述性的标识符名称被认为是不好的编程实践。因此,将下划线字符本身用作标识符名称不应该是一个有问题的更改。

使用私有接口方法

Lambda 表达式是 Java8 版本的重要组成部分。作为这一改进的后续,接口中的私有方法现在是可行的。以前,我们不能在接口的非抽象方法之间共享数据。使用 Java9、10 和 11,这种数据共享是可能的。接口方法现在可以是私有的。让我们看一些示例代码

第一个代码片段是如何在 Java8 中编写接口的:

. . . 
public interface characterTravel { 
  public default void walk() { 
    Scanner scanner = new Scanner(System.in);
    System.out.println("Enter desired pacing: ");
    int p = scanner.nextInt();
    p = p +1;
  }
  public default void run() {
    Scanner scanner = new Scanner(System.in);
    System.out.println("Enter desired pacing: ");
    int p = scanner.nextInt();
    p = p +4;
  }
  public default void fastWalk() {
    Scanner scanner = new Scanner(System.in);
    System.out.println("Enter desired pacing: ");
    int p = scanner.nextInt();
    p = p +2;
  }
  public default void retreat() {
    Scanner scanner = new Scanner(System.in);
    System.out.println("Enter desired pacing: ");
    int p = scanner.nextInt();
    p = p - 1;
  }
  public default void fastRetreat() {
    Scanner scanner = new Scanner(System.in);
    System.out.println("Enter desired pacing: ");
    int p = scanner.nextInt();
    p = p - 4;
  }
}

从 Java9 开始,我们可以重写这段代码。正如您在下面的代码片段中所看到的,冗余代码已经被移动到一个名为characterTravel的私有方法中:

. . .
public interface characterTravel {
  public default void walk() {
    characterTravel("walk");
  }
  public default void run() {
    characterTravel("run");
  }
  public default void fastWalk() {
    characterTravel("fastWalk");
  }
  public default void retreat() {
    characterTravel("retreat");
  }
  public default void fastRetreat() {
    characterTravel("fastRetreat");
  }
  private default void characterTravel(String pace) {
    Scanner scanner = new Scanner(System.in);
    System.out.println("Enter desired pacing: ");
    int p = scanner.nextInt();
    if (pace.equals("walk")) {
      p = p +1;
    }
    else if (pace.equals("run")) {
      p = p + 4;
    }
    else if (pace.equals("fastWalk")) {
      p = p + 2;
    }
    else if (pace.equals("retreat")) {
      p = p - 1;
    }
    else if (pace.equals("fastRetreat"))
    {
      p = p - 4;
    }
    else
    {
      //
    }
  }
}

import语句处理

JDK 增强建议JEP)216 是针对 Javac 如何处理导入语句而发布的。在 Java9 之前,如果源代码是否被接受,导入语句的顺序会产生影响

当我们用 Java 开发应用时,我们通常会根据需要添加import语句,从而导致import语句的无序列表。IDE 在对未使用的导入语句进行颜色编码方面做得很好,还可以通知我们需要的导入语句,但这还没有包括在内。导入语句的顺序应该无关紧要;没有适用的层次结构

javac编译类有两个主要步骤。具体到导入语句的处理,步骤如下:

  • 类型解析:类型解析包括对抽象语法树的检查,以识别类和接口的声明
  • 成员解析:成员解析包括确定类的层次结构、单个类变量和成员

从 Java9 开始,我们在类和文件中列出import语句的顺序将不再影响编译过程。让我们看一个例子:

package samplePackage;

import static SamplePackage.OuterPackage.Nested.*;
import SamplePackage.Thing.*;

public class OuterPackage {
  public static class Nested implements Inner {
    // code
  }
}

package SamplePackage.Thing;

public interface Inner {
  // code
}

在前面的示例中,发生类型解析并导致以下实现:

  • SamplePackage.OuterPackage存在
  • SamplePackage.OuterPackage.Nested存在
  • SamplePackage.Thing.Innner存在

下一步是成员解析,这就是 Java9 之前存在的问题所在。下面是 Javac 将用于为我们的示例代码执行成员解析的连续步骤的概述:

  1. SamplePackage.OuterPackage的解析开始。
  2. 处理SamplePackage.OuterPackage.Nested导入。
  3. SamplePackage.Outer.Nested类的决议开始。
  4. 内部接口是经过类型检查的,但由于此时它不在范围内,因此无法解析内部接口。
  5. SamplePackage.Thing的解析开始。此步骤包括将SamplePackage.Thing的所有成员类型导入范围。

因此,在我们的示例中,出现错误是因为在尝试解析时,Inner超出了范围。如果把第 4 步和第 5 步互换,就不会有问题了。

这个问题的解决方案是在 Java9 中实现的,它将成员解析步骤分解为额外的子步骤。以下是这些步骤:

  1. 分析导入语句
  2. 创建层次结构(类和接口)
  3. 分析类头部和类型参数

推断局部变量

从 Java10 开始,局部变量的声明已经简化。开发人员不再需要包含局部变量类型的清单声明;相反,可以通过使用新的var标识符来推断声明

使用var标识符推断声明

我们可以使用新的var标识符来推断数据类型,如下例所示。因此,我们不必显式声明数据类型,而是可以推断它们:

var myList = new ArrayList<String>();

前面的代码推断出了ArrayList<String>,因此我们不再需要使用详细的ArrayList<String> myList = new ArrayList<String>();语法。

引入var标识符不应被解释为向 Java 语言中添加new关键字。var标识符在技术上是一个保留的类型名

使用new标识符有一些限制。例如,当存在以下任何一种情况时,不能使用它们:

  • 未使用初始化器
  • 声明多个变量
  • 使用数组维度括号
  • 使用对初始化变量的引用

如预期的那样,如果var使用不正确,javac将发出特定的错误消息。

Lambda 参数的局部变量语法

正如本章前面所讨论的,var标识符是在 Java10 中引入的。在最新版本 Java11 中,var可以用在隐式类型的 Lambda 表达式中。以下是两个等效 Java 语句的示例:

  • (object1, object2) -> object1.myMyethod(object2)
  • (var object1, var object2) -> object1.myMethod(object2)

在第一个语句中,不使用var标识符。在第二个语句中,使用了var。需要注意的是,如果在隐式类型的 Lambda 表达式中使用了var,则必须将其用于所有形式参数。

线程本地握手

版本 10 中添加到 Java 平台的一个特性是能够单独停止线程,而不必执行全局虚拟机安全点。拥有此功能的好处包括有偏差的锁撤销改进、虚拟机延迟减少、更安全的栈跟踪以及省略内存障碍。

这种变化在 x64 和 SPARC 系统中非常明显。如果我们想选择正常的安全点,我们将使用以下选项:

XX: ThreadLocalHandshakes

备用内存设备上的堆分配

从 Java10 开始,热点虚拟机支持非 DRAM 内存设备。我们可以使用以下选项在备用内存设备中分配 Java 对象堆:

XX:AllocateHeapAt=<file system path>

在使用备用设备文件系统分配内存时,解决位置冲突和安全问题非常重要。具体来说,请确保使用了正确的权限,并且在应用终止时清除堆。

根证书

从 Java10 的发布开始,JDK 中有一组默认的证书颁发机构CA)证书。Java10 之前的 JDK 的cacertskeystore不包含一组证书。在此 Java 版本之前,开发人员需要为cacertskeystore创建和配置一组根证书

现在,Java 平台在cacertskeystore中包含了一组由 Oracle 颁发的根证书。特定 CA 是 JavaSE 根 CA 程序的一部分。

从 Java10 开始,根证书中包括以下经 Oracle 验证的 CA:

  • Actalis S.p.A.
  • Buypass AS
  • Camerfirma
  • Certum
  • Chunghwa Telecom Co., Ltd.
  • Comodo CA Ltd.
  • Digicert Inc
  • DocuSign
  • D-TRUST GmbH
  • IdenTrust
  • Let's Encrypt
  • LuxTrust
  • QuoVadis Ltd
  • Secom Trust Systems
  • SwissSign AG
  • Tella
  • Trustwave

很可能在 Java 平台的每个后续版本中都会添加额外的 ca

动态类文件常量

在 Java11 中,Java 类文件的文件格式被扩展为支持CONSTANT_Dynamic,它将创建委托给自举方法。Java 平台中增加了一个新的常量形式CONSTANT_Dynamic,它包含两个组件:

  • CONSTANT_InvokeDynamic
  • CONSTANT_NameAndType

有关此功能增强的更多细节,请按照本章“进一步阅读”部分的链接找到。

删除 JavaEE 和 CORBA 模块

Java 企业版JavaEE)和公共对象请求代理架构CORBA)模块在 Java9 中被废弃,并从 Java11 开始从 Java 平台中移除。

以下 Java SE 模块包含 Java EE 和 CORBA 模块,已被删除:

  • 聚合器模块(java.se.ee
  • 常用注解(java.xml.ws.annotation
  • CORBA(java.corba
  • JAF(java.activation
  • JAX-WS(java.xml.ws
  • JAX-WS 工具(jdk.xml.ws
  • JAXB(java.xml.bind
  • JAXB 工具(jdk.xml.bind)
  • JTA(java.transaction

总结

在本章中,我们介绍了 Java 平台的一些变化,这些变化会影响变量处理器、导入语句、对 Coin 项目的改进、局部变量类型推断、根证书、动态类文件常量等等。我们还介绍了废弃警告,以及在特定情况下为什么现在禁止这些警告。最后,我们探讨了导入语句处理的改进。

在下一章中,我们将研究 Jigsaw 项目指定的 Java 模块的结构。我们将深入探讨 Jigsaw 项目是如何作为 Java 平台的一部分实现的。本章使用代码片段演示 Java 的模块化系统,还将讨论 Java 平台在模块化系统方面的内部变化。

问题

  1. 什么是栅栏操作?
  2. 什么是 Coin 计划?
  3. @SafeVarargs可以与什么类型的方法一起使用?
  4. import语句处理的变化有什么意义?
  5. Java 在哪里存储根证书?
  6. var不是关键字。这是怎么一回事?
  7. var是用来做什么的?
  8. Java 平台的下划线字符(_有什么变化?
  9. 原子包中有多少子类?
  10. 哪个类管理变量处理器?

进一步阅读

下面列出的链接将帮助您深入了解本章介绍的概念: