Java7-新特性秘籍(五)

76 阅读32分钟

Java7 新特性秘籍(五)

原文:zh.annas-archive.org/md5/5FB42CDAFBC18FB5D8DD681ECE2B0206

译者:飞龙

协议:CC BY-NC-SA 4.0

第十章:并发处理

在本章中,我们将涵盖以下内容:

  • 在 Java 7 中使用 join/fork 框架

  • 使用可重用的同步障碍 Phaser

  • 在多个线程中安全地使用 ConcurrentLinkedDeque 类

  • 使用 LinkedTransferQueue 类

  • 使用 ThreadLocalRandom 类支持多个线程

介绍

Java 7 中改进了并发应用程序的支持。引入了几个新类,支持任务的并行执行。ForkJoinPool类用于使用分而治之技术解决问题的应用程序。每个子问题都被分叉(分割)为一个单独的线程,然后在必要时合并以提供解决方案。该类使用的线程通常是java.util.concurrent.ForkJoinTask类的子类,是轻量级线程。在 Java 中使用 join/fork 框架示例中说明了这种方法的使用。

此外,引入了java.util.concurrent.Phaser类,以支持一系列阶段中线程集合的执行。一组线程被同步,以便它们都执行然后等待其他线程的完成。一旦它们都完成了,它们可以重新执行第二阶段或后续阶段。使用可重用的同步障碍 Phaser示例说明了在游戏引擎设置中使用此类的情况。

使用 java.util.concurrent.ConcurrentLinkedDeque 类安全地与多个线程一起使用使用 java.util.concurrent.LinkedTransferQueue 类示例介绍了两个设计用于安全地与多个线程一起工作的新类。展示了它们在支持生产者/消费者框架的使用示例。

java.util.concurrent.ThreadLocalRandom类是新的,并提供更好地支持在多个线程之间使用的随机数生成。在使用 ThreadLocalRandom 类支持多个线程示例中进行了讨论。

java.util.ConcurrentModificationException类中添加了两个新的构造函数。它们都接受一个Throwable对象,用于指定异常的原因。其中一个构造函数还接受一个提供有关异常的详细信息的字符串。

Java 7 通过修改锁定机制改进了类加载器的使用,以避免死锁。在 Java 7 之前的多线程自定义类加载器中,某些自定义类加载器在使用循环委托模型时容易发生死锁。

考虑以下情景。Thread1 尝试使用 ClassLoader1(锁定 ClassLoader1)加载 class1。然后将加载 class2 的委托给 ClassLoader2。与此同时,Thread2 使用 ClassLoader2(锁定 ClassLoader2)加载 class3,然后将加载 class4 的委托给 ClassLoader1。由于两个类加载器都被锁定,而两个线程都需要这两个加载器,因此发生死锁情况。

并发类加载器的期望行为是从同一实例的类加载器并发加载不同的类。这需要以更细粒度的级别进行锁定,例如通过正在加载的类的名称锁定类加载器。

同步不应该在类加载器级别进行。相反,应该在类级别上进行锁定,其中类加载器只允许由该类加载器一次加载一个类的单个实例。

一些类加载器能够并发加载类。这种类型的类加载器称为并行可用的类加载器。它们在初始化过程中需要使用registerAsParallelCapable方法进行注册。

如果自定义类加载器使用无环层次委托模型,则在 Java 中不需要进行任何更改。在层次委托模型中,首先委托给其父类加载器。不使用层次委托模型的类加载器应该在 Java 中构造为并行可用的类加载器。

为自定义类加载器避免死锁:

  • 在类初始化序列中使用registerAsParallelCapable方法。这表示类加载器的所有实例都是多线程安全的。

  • 确保类加载器代码是多线程安全的。这包括:

  • 使用内部锁定方案,例如java.lang.ClassLoader使用的类名锁定方案

  • 删除类加载器锁上的任何同步

  • 确保关键部分是多线程安全的

  • 建议类加载器覆盖findClass(String)方法

  • 如果defineClass方法被覆盖,则确保每个类名只调用一次

有关此问题的更多详细信息,请访问openjdk.java.net/groups/core-libs/ClassLoaderProposal.html

在 Java 中使用 join/fork 框架

join/fork框架是一种支持将问题分解为更小的部分,以并行方式解决它们,然后将结果合并的方法。新的java.util.concurrent.ForkJoinPool类支持这种方法。它旨在与多核系统一起工作,理想情况下有数十个或数百个处理器。目前,很少有桌面平台支持这种并发性,但未来的机器将会支持。少于四个处理器时,性能改进将很小。

ForkJoinPool类源自java.util.concurrent.AbstractExecutorService,使其成为ExecutorService。它旨在与ForkJoinTasks一起工作,尽管它也可以与普通线程一起使用。ForkJoinPool类与其他执行程序不同,其线程尝试查找并执行其他当前运行任务创建的子任务。这称为工作窃取

ForkJoinPool类可用于计算子问题上的计算要么被修改,要么返回一个值。当返回一个值时,使用java.util.concurrent.RecursiveTask派生类。否则,使用java.util.concurrent.RecursiveAction类。在本教程中,我们将说明使用RecursiveTask派生类的用法。

准备工作

要为返回每个子任务结果的任务使用分支/合并框架:

  1. 创建一个实现所需计算的RecursiveTask的子类。

  2. 创建ForkJoinPool类的实例。

  3. 使用ForkJoinPool类的invoke方法与RecursiveTask类的子类的实例。

如何做...

该应用程序并非旨在以最有效的方式实现,而是用于说明分支/合并任务。因此,在处理器数量较少的系统上,可能几乎没有性能改进。

  1. 创建一个新的控制台应用程序。我们将使用一个派生自RecursiveTask的静态内部类来计算numbers数组中整数的平方和。首先,声明numbers数组如下:
private static int numbers[] = new int[100000];

  1. 如下添加SumOfSquaresTask类。它创建数组元素的子范围,并使用迭代循环计算它们的平方和,或者根据阈值大小将数组分成更小的部分:
private static class SumOfSquaresTask extends RecursiveTask<Long> {
private final int thresholdTHRESHOLD = 1000;
private int from;
private int to;
public SumOfSquaresTask(int from, int to) {
this.from = from;
this.to = to;
}
@Override
protected Long compute() {
long sum = 0L;
int mid = (to + from) >>> 1;
if ((to - from) < thresholdTHRESHOLD) {
for (int i = from; i < to; i++) {
sum += numbers[i] * numbers[i];
}
return sum;
}
else {
List<RecursiveTask<Long>> forks = new ArrayList<>();
SumOfSquaresTask task1 =
new SumOfSquaresTask(from, mid);
SumOfSquaresTask task2 =
new SumOfSquaresTask(mid, to);
forks.add(task1);
task1.fork();
forks.add(task2);
task2.fork();
for (RecursiveTask<Long> task : forks) {
sum += task.join();
}
return sum;
}
}
}

  1. 添加以下main方法。为了比较,使用 for 循环计算平方和,然后使用ForkJoinPool类。执行时间如下计算并显示:
public static void main(String[] args) {
for (int i = 0; i < numbers.length; i++) {
numbers[i] = i;
}
long startTime;
long stopTime;
long sum = 0L;
startTime = System.currentTimeMillis();
for (int i = 0; i < numbers.length; i++) {
sum += numbers[i] * numbers[i];
}
System.out.println("Sum of squares: " + sum);
stopTime = System.currentTimeMillis();
System.out.println("Iterative solution time: " + (stopTime - startTime));
ForkJoinPool forkJoinPool = new ForkJoinPool();
startTime = System.currentTimeMillis();
long result = forkJoinPool.invoke(new SumOfSquaresTask(0, numbers.length));
System.out.println("forkJoinPool: " + forkJoinPool.toString());
stopTime = System.currentTimeMillis();
System.out.println("Sum of squares: " + result);
System.out.println("Fork/join solution time: " + (stopTime - startTime));
}

  1. 执行应用程序。您的输出应该类似于以下内容。但是,根据您的硬件配置,您应该观察到不同的执行时间:

平方和:18103503627376

迭代解决方案时间:5

平方和:18103503627376

分支/合并解决方案时间:23

请注意,迭代解决方案比使用分支/合并策略的解决方案更快。如前所述,除非有大量处理器,否则这种方法并不总是更有效。

重复运行应用程序将导致不同的结果。更积极的测试方法是在可能不同的处理器负载条件下重复执行解决方案,然后取结果的平均值。阈值的大小也会影响其性能。

工作原理...

numbers数组声明为一个包含 100,000 个元素的整数数组。SumOfSquaresTask类是从RecursiveTask类派生的,使用了泛型类型Long。设置了阈值为 1000。任何小于此阈值的子数组都使用迭代解决。否则,该段被分成两半,并创建了两个新任务,每个任务处理一半。

ArrayList用于保存两个子任务。这严格来说是不需要的,实际上会减慢计算速度。但是,如果我们决定将数组分成两个以上的段,这将是有用的。它提供了一个方便的方法,在子任务加入时重新组合元素。

fork方法用于拆分子任务。它们进入线程池,最终将被执行。join方法在子任务完成时返回结果。然后将子任务的总和相加并返回。

main方法中,第一个代码段使用for循环计算了平方的和。开始和结束时间基于以毫秒为单位测量的当前时间。第二段创建了ForkJoinPool类的一个实例,然后使用其invoke方法与SumOfSquaresTask对象的新实例。传递给SumOfSquaresTask构造函数的参数指示它从数组的第一个元素开始,直到最后一个元素。完成后,显示执行时间。

还有更多...

ForkJoinPool类有几种报告池状态的方法,包括:

  • getPoolSize:该方法返回已启动但尚未完成的线程数

  • getRunningThreadCount:该方法返回未被阻塞但正在等待加入其他任务的线程数的估计值

  • getActiveThreadCount:该方法返回执行任务的线程数的估计值

ForkJoinPool类的toString方法返回池的几个方面。在invoke方法执行后立即添加以下语句:

out.println("forkJoinPool: " + forkJoinPool);

当程序执行时,将获得类似以下的输出:

forkJoinPool: java.util.concurrent.ForkJoinPool@18fb53f6[Running, parallelism = 4, size = 55, active = 0, running = 0, steals = 171, tasks = 0, submissions = 0]

另请参阅

*使用可重用同步障碍Phaser*的方法提供了执行多个线程的不同方法。

使用可重用的同步障碍Phaser

java.util.concurrent.Phaser类涉及协调一起工作的线程在循环类型阶段中的同步。线程将执行,然后等待组中其他线程的完成。当所有线程都完成时,一个阶段就完成了。然后可以使用Phaser来协调再次执行相同一组线程。

java.util.concurrent.CountdownLatch类提供了一种方法来做到这一点,但需要固定数量的线程,并且默认情况下只执行一次。java.util.concurrent.CyclicBarrier,它是在 Java 5 中引入的,也使用了固定数量的线程,但是可重用。但是,不可能进入下一个阶段。当问题以一系列基于某些标准的步骤/阶段进行推进时,这是有用的。

随着 Java 7 中Phaser类的引入,我们现在有了一个结合了CountDownLatchCyclicBarrier功能并支持动态线程数量的并发抽象。术语“phase”指的是线程可以协调执行不同阶段或步骤的想法。所有线程将执行,然后等待其他线程完成。一旦它们完成,它们将重新开始并完成第二个或后续阶段的操作。

屏障是一种阻止任务继续进行的类型的块,直到满足某些条件。一个常见的条件是当所有相关线程都已完成时。

Phaser类提供了几个功能,使其非常有用:

  • 可以动态地向线程池中添加和删除参与者

  • 每个阶段都有一个唯一的阶段号。

  • Phaser可以被终止,导致任何等待的线程立即返回

  • 发生的异常不会影响屏障的状态

register方法增加了参与的方数量。当内部计数达到零或根据其他条件确定时,屏障终止。

准备好了

我们将开发一个模拟游戏引擎操作的应用程序。第一个版本将创建一系列代表游戏中参与者的任务。我们将使用Phaser类来协调它们的交互。

使用Phaser类来同步一组任务的开始:

  1. 创建一个将参与PhaserRunnable对象集合。

  2. 创建Phaser类的一个实例。

  3. 对于每个参与者:

  • 注册参与者

  • 使用参与者的Runnable对象创建一个新线程

  • 使用arriveAndAwaitAdvance方法等待其他任务的创建

  • 执行线程

  1. 使用Phaser对象的arriveAndDeregister来启动参与者的执行。

如何做...

  1. 创建一个名为GamePhaserExample的新控制台应用程序类。我们将创建一系列内部类的简单层次结构,这些类代表游戏中的参与者。将Entity类添加为基本抽象类,定义如下。虽然不是绝对必要的,但我们将使用继承来简化这些类型应用程序的开发:
private static abstract class Entity implements Runnable {
public abstract void run();
}

  1. 接下来,我们将创建两个派生类:PlayerZombie。这些类实现run方法和toString方法。run方法使用sleep方法来模拟执行的工作。预期地,僵尸比人类慢:
private static class Player extends Entity {
private final static AtomicInteger idSource = new AtomicInteger();
private final int id = idSource.incrementAndGet();
public void run() {
System.out.println(toString() + " started");
try {
Thread.currentThread().sleep(
ThreadLocalRandom.current().nextInt(200, 600));
}
catch (InterruptedException ex) {
ex.printStackTrace();
}
System.out.println(toString() + " stopped");
}
@Override
public String toString() {
return "Player #" + id;
}
}
private static class Zombie extends Entity {
private final static AtomicInteger idSource = new AtomicInteger();
private final int id = idSource.incrementAndGet();
public void run() {
System.out.println(toString() + " started");
try {
Thread.currentThread().sleep(
ThreadLocalRandom.current().nextInt(400, 800));
}
catch (InterruptedException ex) {
ex.printStackTrace();
}
System.out.println(toString() + " stopped");
}
@Override
public String toString() {
return "Zombie #" + id;
}
}

  1. 为了使示例更清晰,将以下main方法添加到GamePhaserExample类中:
public static void main(String[] args) {
new GamePhaserExample().execute();
}

  1. 接下来,添加以下execute方法,我们在其中创建参与者列表,然后调用gameEngine方法:
private void execute() {
List<Entity> entities = new ArrayList<>();
entities = new ArrayList<>();
entities.add(new Player());
entities.add(new Zombie());
entities.add(new Zombie());
entities.add(new Zombie());
gameEngine(entities);
}

  1. 接下来是gameEngine方法。for each循环为每个参与者创建一个线程:
private void gameEngine(List<Entity> entities) {
final Phaser phaser = new Phaser(1);
for (final Entity entity : entities) {
synchronization barrier Phaserusingfinal String member = entity.toString();
System.out.println(member + " joined the game");
phaser.register();
new Thread() {
@Override
public void run() {
System.out.println(member +
" waiting for the remaining participants");
phaser.arriveAndAwaitAdvance(); // wait for remaining entities
System.out.println(member + " starting run");
entity.run();
}
}.start();
}
phaser.arriveAndDeregister(); //Deregister and continue
System.out.println("Phaser continuing");
}

  1. 执行应用程序。输出是不确定的,但应该类似于以下内容:

玩家#1 加入游戏

僵尸#1 加入游戏

僵尸#2 加入游戏

玩家#1 等待剩余参与者

僵尸#1 等待剩余参与者

僵尸#3 加入游戏

Phaser 继续

僵尸#3 等待剩余参与者

僵尸#2 等待剩余参与者

僵尸#1 开始奔跑

僵尸#1 开始

僵尸#3 开始奔跑

僵尸#3 开始

僵尸#2 开始奔跑

僵尸#2 开始

玩家#1 开始奔跑

玩家#1 开始

玩家#1 停止

僵尸#1 停止

僵尸#3 停止

僵尸#2 停止

注意Phaser对象会等待直到所有参与者都加入游戏。

它是如何工作的...

sleep 方法用于模拟实体所涉及的工作。请注意 ThreadLocalRandom 类的使用。其 nextInt 方法返回其参数中指定的值之间的随机数。在使用并发线程时,这是生成随机数的首选方式,如使用 ThreadLocalRandom 类支持多个线程配方中所述。

AtomicInteger 类的一个实例用于为每个创建的对象分配唯一的 ID。这是在线程中生成数字的安全方式。toString 方法返回实体的简单字符串表示形式。

execute 方法中,我们创建了一个 ArrayList 来保存参与者。请注意在创建 ArrayList 时使用了菱形操作符。这是 Java 7 语言改进,在第一章的使用菱形操作符进行构造类型推断配方中有解释,Java 语言改进。添加了一个玩家和三个僵尸。僵尸似乎总是比人类多。然后调用了 gameEngine 方法。

使用参数为一的 Phaser 对象创建了一个代表第一个参与者的对象。它不是一个实体,只是作为帮助控制阶段器的机制。

在每个循环中,使用 register 方法将阶段器中的方的数量增加一。使用匿名内部类创建了一个新线程。在其 run 方法中,直到所有参与者到达之前,实体才会开始。arriveAndAwaitAdvance 方法导致通知参与者已到达,并且该方法在所有参与者到达并且阶段完成之前不返回。

while循环的每次迭代开始时,注册参与者的数量比已到达的参与者数量多一个。register 方法将内部计数增加一。然后内部计数比已到达的数量多两个。当执行 arriveAndAwaitAdvance 方法时,现在等待的参与者数量将比已注册的多一个。

循环结束后,仍然有一个比已到达的参与者多的注册方。但是,当执行 arriveAndDeregister 方法时,已到达的参与者数量的内部计数与参与者数量匹配,并且线程开始。此外,注册方的数量减少了一个。当所有线程终止时,应用程序终止。

还有更多...

可以使用 bulkRegister 方法注册一组方。此方法接受一个整数参数,指定要注册的方的数量。

在某些情况下,可能希望强制终止阶段器。forceTermination 方法用于此目的。

在执行阶段器时,有几种方法可以返回有关阶段器状态的信息,如下表所述。如果阶段器已终止,则这些方法将不起作用:

方法描述
getRoot返回根阶段器。与阶段器树一起使用
getParent返回阶段器的父级
getPhase返回当前阶段编号
getArrivedParties已到达当前阶段的方的数量
getRegisteredParties注册方的数量
getUnarrivedParties尚未到达当前阶段的方的数量

可以构建阶段器树,其中阶段器作为任务的分支创建。在这种情况下,getRoot 方法非常有用。阶段器构造在www.cs.rice.edu/~vs3/PDF/SPSS08-phasers.pdf中讨论。

使用阶段器重复一系列任务

我们还可以使用Phaser类来支持一系列阶段,其中执行任务,执行可能的中间操作,然后再次重复一系列任务。

为了支持这种行为,我们将修改gameEngine方法。修改将包括:

  • 添加一个iterations变量

  • 覆盖Phaser类的onAdvance方法

  • 在每个任务的run方法中使用while循环,由isTerminated方法控制

添加一个名为iterations的变量,并将其初始化为3。这用于指定我们将使用多少个阶段。还要重写如下所示的onAdvance方法:

final int iterations = 3;
final Phaser phaser = new Phaser(1) {
protected boolean onAdvance(int phase, int registeredParties) {
System.out.println("Phase number " + phase + " completed\n")
return phase >= iterations-1 || registeredParties == 0;
}
};

每个阶段都有唯一的编号,从零开始。调用onAdvance传递当前阶段编号和注册到 phaser 的当前参与方数量。当注册方数量变为零时,此方法的默认实现返回true。这将导致 phaser 被终止。

该方法的实现导致仅当阶段编号超过iterations值(即减 1)或没有使用 phaser 的注册方时,该方法才返回true

根据以下代码中突出显示的内容修改run方法:

for (final Entity entity : entities) {
final String member = entity.toString();
System.out.println(member + " joined the game");
phaser.register();
new Thread() {
@Override
public void run() {
do {
System.out.println(member + " starting run");
entity.run();
System.out.println(member +
" waiting for the remaining participants during phase " +
phaser.getPhase());
phaser.arriveAndAwaitAdvance(); // wait for remaining entities
}
while (!phaser.isTerminated());
}
}.start();
}

实体被允许先运行,然后等待其他参与者完成和到达。只要通过isTerminated方法确定的 phaser 尚未终止,当每个人准备好时,下一阶段将被执行。

最后一步是使用arriveAndAwaitAdvance方法将 phaser 推进到下一个阶段。同样,只要 phaser 尚未终止,当每个参与者到达时,phaser 将推进到下一个阶段。使用以下代码序列来完成此操作:

while (!phaser.isTerminated()) {
phaser.arriveAndAwaitAdvance();
}
System.out.println("Phaser continuing");

仅使用一个玩家和一个僵尸执行程序。这将减少输出量,并且应与以下内容类似:

玩家#1 加入游戏

僵尸#1 加入游戏

玩家#1 开始运行

玩家#1 开始

僵尸#1 开始运行

僵尸#1 开始

玩家#1 停止

玩家#1 在第 0 阶段等待剩余参与者

僵尸#1 停止

僵尸#1 在第 0 阶段等待剩余参与者

第 0 阶段完成

玩家#1 开始运行

玩家#1 开始

僵尸#1 开始运行

僵尸#1 开始

玩家#1 停止

玩家#1 在第 1 阶段等待剩余参与者

僵尸#1 停止

僵尸#1 在第 1 阶段等待剩余参与者

第 1 阶段完成

僵尸#1 开始运行

玩家#1 开始运行

僵尸#1 开始

玩家#1 开始

玩家#1 停止

玩家#1 在第 2 阶段等待剩余参与者

僵尸#1 停止

僵尸#1 在第 2 阶段等待剩余参与者

第 2 阶段完成

Phaser 继续

另请参阅

有关为多个线程生成随机数的更多信息,请参阅使用当前线程隔离的随机数生成器

安全地使用新的ConcurrentLinkedDeque与多个线程

java.util.concurrent.ConcurrentLinkedDeque类是 Java 集合框架的成员,它允许多个线程安全地同时访问相同的数据集合。该类实现了一个双端队列,称为deque,并允许从 deque 的两端插入和删除元素。它也被称为头尾链接列表,并且与其他并发集合一样,不允许使用空元素。

在本示例中,我们将演示ConcurrentLinkedDeque类的基本实现,并说明一些最常用方法的使用。

准备好了

在生产者/消费者框架中使用ConcurrentLinkedDeque

  1. 创建ConcurrentLinkedDeque的实例。

  2. 定义要放入双端队列的元素。

  3. 实现一个生产者线程来生成要放入双端队列中的元素。

  4. 实现一个消费者线程来从双端队列中删除元素。

如何做...

  1. 创建一个新的控制台应用程序。使用Item的泛型类型声明一个私有静态实例的ConcurrentLinkedDequeItem类被声明为内部类。包括获取方法和构造函数,如下面的代码所示,使用两个属性descriptionitemId
private static ConcurrentLinkedDeque<Item> deque = new ConcurrentLinkedDeque<>();
static class Item {
privateublic final String description;
privateublic final int itemId;
public Item() {
"this(Default Item";, 0)
}
public Item(String description, int itemId) {
this.description = description;
this.itemId = itemId;
}
}

  1. 然后创建一个生产者类来生成Item类型的元素。为了这个示例的目的,我们只会生成七个项目,然后打印出一个声明来证明该项目已添加到双端队列中。我们使用ConcurrentLinkedDeque类的add方法来添加元素。每次添加后,线程会短暂休眠:
static class ItemProducer implements Runnable {
@Override
public void run() {
String itemName = "";
int itemId = 0;
try {
for (int x = 1; x < 8; x++) {
itemName = "Item" + x;
itemId = x;
deque.add(new Item(itemName, itemId));
System.out.println("New Item Added:" + itemName + " " + itemId);
Thread.currentThread().sleep(250);
}
}
catch (InterruptedException ex) {
ex.printStackTrace();
}
}
}

  1. 接下来,创建一个消费者类。为了确保在消费者线程尝试访问它之前,双端队列中将有元素,我们让线程在检索元素之前睡眠一秒钟。然后我们使用pollFirst方法来检索双端队列中的第一个元素。如果元素不为空,那么我们将元素传递给generateOrder方法。在这个方法中,我们打印有关该项目的信息:
static class ItemConsumer implements Runnable {
@Override
public void run() {
try {
Thread.currentThread().sleep(1000);
}
catch (InterruptedException ex) {
ex.printStackTrace();
}
Item item;
while ((item = deque.pollFirst()) != null) {
{
generateOrder(item);
}
}
private void generateOrder(Item item) {
System.out.println("Part Order");
System.out.println("Item description: " + item.getDescriptiond());
System.out.println("Item ID # " + item.getItemIdi());
System.out.println();
try {
Thread.currentThread().sleep(1000);
}
catch (InterruptedException ex) {
ex.printStackTrace();
}
}
}

  1. 最后,在我们的main方法中,启动两个线程:
public static void main(String[] args) {
new Thread(new ItemProducer());.start()
new Thread(new ItemConsumer());.start()
}

  1. 当您执行程序时,您应该看到类似以下的输出:

新项目已添加:Item1 1

新项目已添加:Item2 2

新项目已添加:Item3 3

新项目已添加:Item4 4

零件订单

项目描述:Item1

项目 ID#1

新项目已添加:Item5 5

新项目已添加:Item6 6

新项目已添加:Item7 7

零件订单

项目描述:Item2

项目 ID#2

零件订单

项目描述:Item3

项目 ID#3

零件订单

项目描述:Item4

项目 ID#4

零件订单

项目描述:Item5

项目 ID#5

零件订单

项目描述:Item6

项目 ID#6

零件订单para

项目描述:Item7

项目 ID#7

它是如何工作的...

当我们启动两个线程时,我们让生产者线程提前一点时间来填充我们的双端队列。一秒钟后,消费者线程开始检索元素。使用ConcurrentLinkedDeque类允许两个线程同时安全地访问双端队列的元素。

在我们的示例中,我们使用了addpollFirst方法来添加和删除双端队列的元素。有许多可用的方法,其中许多方法基本上以相同的方式运行。*还有更多...*部分提供了有关访问双端队列元素的各种选项的更多详细信息。

还有更多...

我们将涵盖几个主题,包括:

  • 异步并发线程存在问题

  • 向双端队列添加元素

  • 从双端队列中检索元素

  • 访问双端队列的特定元素

异步并发线程存在问题

由于多个线程可能在任何给定时刻访问集合,因此size方法并不总是会返回准确的结果。当使用iteratordescendingIterator方法时,情况也是如此。此外,任何批量数据操作,例如addAllremoveAll,也不总是会达到预期的结果。如果一个线程正在访问集合中的一个项目,而另一个线程尝试拉取所有项目,则批量操作不能保证以原子方式运行。

有两种toArray方法可用于检索双端队列的所有元素并将它们存储在数组中。第一个返回表示双端队列所有元素的对象数组,并且可以转换为适当的数据类型。当双端队列的元素是不同的数据类型时,这是有用的。以下是如何使用toArray方法的第一种形式的示例,使用我们之前的线程示例:

Item[] items = (Item[]) deque.toArray();

另一个toArray方法需要一个特定数据类型的初始化数组作为参数,并返回该数据类型的元素数组。

Item[] items = deque.toArray(new Item[0]);

向双端队列添加元素

以下表格列出了一些可用于向双端队列中添加元素的方法。在下表中分组在一起的方法本质上执行相同的功能。这种类似方法的多样性是ConcurrentLinkedDeque类实现略有不同接口的结果:

方法名添加元素到
add(Element e)``offer(Element e)``offerLast(Element e)``addLast(Element e)双端队列的末尾
addFirst(Element e)``offerFirst(Element e)``push(Element e)双端队列的前端

从双端队列中检索元素

以下是一些用于从双端队列中检索元素的方法:

方法名错误操作功能
element()如果双端队列为空则抛出异常检索但不移除双端队列的第一个元素
getFirst()  
getLast()  
peek()如果双端队列为空则返回 null 
peekFirst()  
peekLast()  
pop()如果双端队列为空则抛出异常检索并移除双端队列的第一个元素
removeFirst()  
poll()如果双端队列为空则返回 null 
pollFirst()  
removeLast()如果双端队列为空则抛出异常检索并移除双端队列的最后一个元素
pollLast()如果双端队列为空则返回 null 

访问双端队列的特定元素

以下是一些用于访问双端队列特定元素的方法:

方法名功能注释
contains(Element e)如果双端队列包含至少一个等于Element e的元素则返回true 
remove(Element e)``removeFirstOccurrence(Element e)移除双端队列中第一个等于Element e的元素如果元素在双端队列中不存在,则双端队列保持不变。如果e为 null 则抛出异常
removeLastOccurrence(Element e)移除双端队列中最后一个等于Element e的元素 

使用新的 LinkedTransferQueue 类

java.util.concurrent.LinkedTransferQueue类实现了java.util.concurrent.TransferQueue接口,是一个无界队列,遵循先进先出模型。该类提供了用于检索元素的阻塞方法和非阻塞方法,并且适合于多个线程的并发访问。在本示例中,我们将创建一个LinkedTransferQueue的简单实现,并探索该类中的一些可用方法。

准备工作

要在生产者/消费者框架中使用LinkedTransferQueue

  1. 创建一个LinkedTransferQueue的实例。

  2. 定义要放入队列的元素类型。

  3. 实现一个生产者线程来生成要放入队列的元素。

  4. 实现一个消费者线程来从队列中移除元素。

如何做...

  1. 创建一个新的控制台应用程序。使用Item的泛型类型声明一个LinkedTransferQueue的私有静态实例。然后创建内部类Item,并包括如下代码所示的 get 方法和构造函数,使用descriptionitemId这两个属性:
private static LinkedTransferQueue<Item>
linkTransQ = new LinkedTransferQueue<>();
static class Item {
public final String description;
public final int itemId;
public Item() {
this("Default Item", 0) ;
}
public Item(String description, int itemId) {
this.description = description;
this.itemId = itemId;
}
}

  1. 接下来,创建一个生产者类来生成Item类型的元素。为了本示例的目的,我们只会生成七个项目,然后打印一条语句来演示该项目已被添加到队列中。我们将使用LinkedTransferQueue类的offer方法来添加元素。在每次添加后,线程会短暂休眠,然后我们打印出添加的项目的名称。然后我们使用hasWaitingConsumer方法来确定是否有任何消费者线程正在等待可用的项目:
static class ItemProducer implements Runnable {
@Override
public void run() {
try {
for (int x = 1; x < 8; x++) {
String itemName = "Item" + x;
int itemId = x;
linkTransQ.offer(new Item(itemName, itemId));
System.out.println("New Item Added:" + itemName + " " + itemId);
Thread.currentThread().sleep(250);
if (linkTransQ.hasWaitingConsumer()) {
System.out.println("Hurry up!");
}
}
}
catch (InterruptedException ex) {
ex.printStackTrace();
}
}
}

  1. 接下来,创建一个消费者类。为了演示hasWaitingConsumer方法的功能,我们让线程在检索元素之前睡眠一秒钟,以确保一开始没有等待的消费者。然后,在while循环内,我们使用take方法来移除列表中的第一个项目。我们选择了take方法,因为它是一个阻塞方法,会等待直到队列有可用的元素。一旦消费者线程能够取出一个元素,我们将元素传递给generateOrder方法,该方法打印有关项目的信息:
static class ItemConsumer implements Runnable {
@Override
public void run() {
try {
Thread.currentThread().sleep(1000);
}
catch (InterruptedException ex) {
ex.printStackTrace();
}
while (true) {
try {
generateOrder(linkTransQ.take());
}
catch (InterruptedException ex) {
ex.printStackTrace();
}
}
}
private void generateOrder(Item item) {
System.out.println();
System.out.println("Part Order");
System.out.println("Item description: " + item.description());
System.out.println("Item ID # " + item.itemId());
}
}

  1. 最后,在我们的main方法中,我们启动了两个线程:
public static void main(String[] args) {
new Thread(new ItemProducer()).start();
new Thread(new ItemConsumer()).start();
}

  1. 当您执行程序时,您应该看到类似以下的输出:

新添加的项目:Item1 1

新添加的项目:Item2 2

新添加的项目:Item3 3

新添加的项目:Item4 4

零件订单

项目描述:Item1

项目编号#1

零件订单

项目描述:Item2

项目编号#2

零件订单

项目描述:Item3

项目编号#3

零件订单

项目描述:Item4

项目编号#4

快点!

新添加的项目:Item5 5

零件订单

项目描述:Item5

项目编号#5

快点!

零件订单

项目描述:Item6

项目编号#6

新添加的项目:Item6 6

快点!

零件订单

项目描述:Item7

项目编号#7

新添加的项目:Item7 7

快点!

它是如何工作的...

当我们启动了两个线程时,我们让生产者线程有一个领先,通过在ItemConsumer类中睡眠一秒钟来填充我们的队列。请注意,hasWaitingConsumer方法最初返回false,因为消费者线程尚未执行take方法。一秒钟后,消费者线程开始检索元素。在每次检索时,generateOrder方法打印有关检索到的元素的信息。在检索队列中的所有元素之后,请注意最后的*快点!*语句,表示仍有消费者在等待。在这个例子中,因为消费者在while循环中使用了一个阻塞方法,线程永远不会终止。在现实生活中,线程应该以更优雅的方式终止,比如向消费者线程发送终止消息。

在我们的例子中,我们使用了offertake方法来添加和移除队列的元素。还有其他可用的方法,这些方法在*还有更多..*部分中讨论。

还有更多...

在这里,我们将讨论以下内容:

  • 异步并发线程的问题

  • 向队列添加元素

  • 从双端队列中检索元素

异步并发线程的问题

由于多个线程可能在任何给定时刻访问集合,因此size方法不总是会返回准确的结果。此外,任何批量数据操作,如addAllremoveAll,也不总能达到期望的结果。如果一个线程正在访问集合中的一个项目,另一个线程尝试拉取所有项目,则不保证批量操作会以原子方式运行。

向队列添加元素

以下是一些可用于向队列添加元素的方法:

方法名称添加元素到评论
add(Element e)队列末尾队列是无界的,因此该方法永远不会返回false或抛出异常
offer(Element e)队列是无界的,因此该方法永远不会返回false
put(Element e)队列是无界的,因此该方法永远不会阻塞
offer(Element``e, Long t,TimeUnit u)队列末尾等待 t 个时间单位的类型 u 然后放弃队列是无界的,因此该方法将始终返回true

从双端队列中检索元素

以下是一些可用于从双端队列中检索元素的方法:

方法名称功能评论
peek()检索队列的第一个元素,但不移除如果队列为空,则返回 null
poll()移除队列的第一个元素如果队列为空,则返回 null
poll(Long t, TimeUnit u)从队列前面移除元素,在时间 t(以单位 u 计)之前放弃如果时间限制在元素可用之前到期,则返回 null
remove(Object e)从队列中移除等于Object e的元素如果找到并移除元素,则返回true
take()移除队列的第一个元素如果在阻塞时被中断,则抛出异常
transfer(Element e)将元素传输给消费者线程,必要时等待将元素插入队列末尾,并等待消费者线程检索它
tryTransfer(Element e)立即将元素传输给消费者如果消费者不可用,则返回false
tryTransfer(Element e, Time t, TimeUnit u)立即将元素传输给消费者,或在 t(以单位 u 计)指定的时间内如果消费者在时间限制到期时不可用,则返回false

使用 ThreadLocalRandom 类支持多个线程

java.util.concurrent包中有一个新的类ThreadLocalRandom,它支持类似于Random类的功能。然而,使用这个新类与多个线程将导致较少的争用和更好的性能,与Random类相比。当多个线程需要使用随机数时,应该使用ThreadLocalRandom类。随机数生成器是局部的。本食谱将介绍如何使用这个类。

准备就绪

使用这个类的推荐方法是:

  1. 使用静态的current方法返回ThreadLocalRandom类的一个实例。

  2. 使用该对象的方法。

如何做...

  1. 创建一个新的控制台应用程序。将以下代码添加到main方法中:
System.out.println("Five random integers");
for(int i = 0; i<5; i++) {
System.out.println(ThreadLocalRandom.current(). nextInt());
}
System.out.println();
System.out.println("Random double number between 0.0 and 35.0");
System.out.println(ThreadLocalRandom.current().nextDouble(35.0));
System.out.println();
System.out.println("Five random Long numbers between 1234567 and 7654321");
for(int i = 0; i<5; i++) {
System.out.println(
ThreadLocalRandom.current().nextLong(1234567L, 7654321L));
}

  1. 执行程序。您的输出应该类似于以下内容:

五个随机整数

0

4232237

178803790

758674372

1565954732

0.0 和 35.0 之间的随机双精度数

3.196571144914888

1234567 和 7654321 之间的五个随机长整数

7525440

2545475

1320305

1240628

1728476

它是如何工作的...

nextInt方法被执行了五次,其返回值被显示出来。注意该方法最初返回 0。ThreadLocalRandom类扩展了Random类。然而,不支持setSeed方法。如果尝试使用它,将抛出UnsupportedOperationException

然后执行了nextDouble方法。这个重载方法返回了一个介于 0.0 和 35.0 之间的数字。使用两个参数执行了五次nextLong方法,指定了其起始(包括)和结束(不包括)的范围值。

还有更多...

该类的方法返回均匀分布的数字。以下表总结了它的方法:

提示

当指定范围时,起始值是包含的,结束值是不包含的。

方法参数返回
current线程的当前实例
next代表返回值位数的整数值位数范围内的整数
nextDoubledoubledouble, double0.0 和其参数之间的双精度数 0.0 和其参数之间的双精度数
nextIntint, int其参数之间的整数
nextLonglonglong, long0 和其参数之间的长整数 0 和其参数之间的长整数
setSeedlong抛出 UnsupportedOperationException

另请参阅

使用可重用同步障碍 Phaser食谱中找到了它的用法示例。

第十一章:杂项

在本章中,我们将涵盖以下内容:

  • 在 Java 7 中处理周

  • 在 Java 7 中使用货币

  • 使用 NumericShaper.Range 枚举支持数字显示

  • Java 7 中的 JavaBean 改进

  • 在 Java 7 中处理区域设置和 Locale.Builder 类

  • 处理空引用

  • 在 Java 7 中使用新的 BitSet 方法

介绍

本章将介绍 Java 7 中许多不适合前几章的新内容。其中许多增强功能具有潜在的广泛应用,例如在在 Java 7 中处理区域设置和 Locale.Builder 类中讨论的java.lang.Objects类和java.util.Locale类的改进。其他更专业,例如对java.util.BitSet类的改进,这在在 Java 7 中使用新的 BitSet 方法中有所涉及。

在处理周和货币方面进行了许多改进。当前周数和每年的周数计算受区域设置的影响。此外,现在可以确定平台上可用的货币。这些问题在在 Java 7 中处理周在 Java 7 中使用货币中有所说明。

添加了一个新的枚举,以便在不同语言中显示数字。讨论了使用java.awt.font.NumericShaper类来支持此工作的使用 NumericShaper.Range 枚举支持数字显示配方。在 JavaBeans 的支持方面也有改进,这在Java 7 中的 JavaBean 改进配方中有所讨论。

还有许多增强功能,不值得单独列为配方。本介绍的其余部分都致力于这些主题。

Unicode 6.0

Unicode 6.0是 Unicode 标准的最新修订版。Java 7 通过添加数千个更多的字符和许多新方法来支持此版本。此外,正则表达式模式匹配使用**\u\x**转义序列支持 Unicode 6.0。

Character.UnicodeBlock类中添加了许多新的字符块。Java 7 中添加了Character.UnicodeScript枚举,用于表示Unicode 标准附录#24:脚本名称中定义的字符脚本。

注意

有关 Unicode 标准附录#24:脚本名称的更多信息,请访问download.oracle.com/javase/7/docs/api/index.html

Character类中添加了几种方法,以支持 Unicode 操作。以下是它们在字符串朝鲜圆上的使用示例,这是基于区域设置的朝鲜圆的中文显示名称,以及在中国大陆使用的简化脚本。将以下代码序列添加到新应用程序中:

int codePoint = Character.codePointAt("朝鲜圆", 0);
System.out.println("isBmpCodePoint: " + Character.isBmpCodePoint(codePoint));
System.out.println("isSurrogate: " + Character.isSurrogate('朝'));
System.out.println("highSurrogate: " + (int)Character.highSurrogate(codePoint));
System.out.println("lowSurrogate: " + (int)Character.lowSurrogate(codePoint));
System.out.println("isAlphabetic: " + Character.isAlphabetic(codePoint));
System.out.println("isIdeographic: " + Character.isIdeographic(codePoint));
System.out.println("getName: " + Character.getName(codePoint));

执行时,您的输出应如下所示:

isBmpCodePoint: true

isSurrogate: false

highSurrogate: 55257

lowSurrogate: 57117

isAlphabetic: true

isIdeographic: true

getName: CJK UNIFIED IDEOGRAPHS 671D

由于字符不是 Unicode 代理代码,因此highSurrogatelowSurrogate方法的结果是无用的。

注意

有关 Unicode 6.0 的更多信息,请访问www.unicode.org/versions/Unicode6.0.0/

原始类型和比较方法

Java 7 引入了用于比较原始数据类型Boolean, byte, long, shortint的新静态方法。每个包装类现在都有一个compare方法,它接受两个数据类型的实例作为参数,并返回表示比较结果的整数。例如,您以前需要使用compareTo方法来比较两个布尔变量 x 和 y,如下所示:

Boolean.valueOf(x).compareTo(Boolean.valueOf(y))

现在可以使用compare方法如下:

Boolean.compare(x,y);

虽然这对于布尔数据类型是 Java 的新功能,但compare方法以前已经适用于doublefloat。此外,在 7 中,parse, valueofdecode方法用于将字符串转换为数值,将接受Byte, Short, Integer, LongBigInteger的前导加号(+)标记,以及Float, DoubleBigDecimal,这些类型以前接受该标记。

全局记录器

java.util.logging.Logger类有一个新方法getGlobal,用于检索名为GLOBAL_LOGGER_NAME的全局记录器对象。Logger类的静态字段globalLogger类与LogManager类一起使用时容易发生死锁,因为两个类都会等待对方完成初始化。getGlobal方法是访问全局记录器对象的首选方式,以防止这种死锁。

JavaDocs 改进

从结构上讲,JavaDocs 在 Java 7 中有了重大改进。现在,通过使用HTMLTree类来创建文档树来生成 HTML 页面,从而实现了更准确的 HTML 生成和更少的无效页面。

JavaDocs 的外部变化也有一些,其中一些是为了符合新的第五百零八部分可访问性指南。这些指南旨在确保屏幕阅读器能够准确地将 HTML 页面翻译成可听的输出。主要结果是在表格上添加了更多的标题和标题。JavaDocs 现在还使用 CSS 样式表来简化页面外观的更改。

JVM 性能增强

Java HotSpotTM 虚拟机的性能已经得到了改进。这些改进大多数不在开发人员的控制范围之内,而且具有专业性质。感兴趣的读者可以在docs.oracle.com/javase/7/docs/technotes/guides/vm/performance-enhancements-7.html找到有关这些增强的更多详细信息。

在 Java 7 中处理周

一些应用程序关心一年中的周数和本年的当前周数。众所周知,一年有 52 周,但 52 周乘以每周 7 天等于每年 364 天,而不是实际的 365 天。周数用于指代一年中的周。但是如何计算呢?Java 7 引入了几种方法来支持确定一年中的周。在本教程中,我们将检查这些方法,并看看如何计算与周相关的值。ISO 8601标准提供了表示日期和时间的方法。java.util.GregorianCalendar类支持此标准,除了以下部分中描述的内容。

准备工作

使用这些基于周的方法,我们需要:

  1. 创建Calendar类的实例。

  2. 根据需要使用其方法。

如何做...

某些抽象java.util.Calendar类的实现不支持周计算。要确定Calendar实现是否支持周计算,我们需要执行isWeekDateSupported方法。如果提供支持,则返回true。要返回当前日历年的周数,请使用getWeeksInWeekYear方法。要确定当前日期的周,请使用get方法,并将WEEK_OF_YEAR作为其参数。

  1. 创建一个新的控制台应用程序。将以下代码添加到main方法:
Calendar calendar = Calendar.getInstance();
if(calendar.isWeekDateSupported()) {
System.out.println("Number of weeks in this year: " + calendar.getWeeksInWeekYear());
System.out.println("Current week number: " + calendar.get(Calendar.WEEK_OF_YEAR));
}

  1. 执行应用程序。您的输出应如下所示,但值将取决于应用程序执行的日期:

今年的周数:53

当前周数:48

工作原理...

创建了Calendar类的一个实例。这通常是GregorianCalendar类的一个实例。if语句由isWeekDateSupported方法控制。它返回true,导致执行getWeeksInWeekYearget方法。get方法传入了字段WEEK_OF_YEAR,返回当前的周数。

还有更多...

可以使用setWeekDate方法设置日期。此方法有三个参数,指定年、周和日。它提供了一种根据周设置日期的便捷技术。以下是通过将年份设置为 2012 年,将周设置为该年的第 16 周,将日期设置为该周的第三天来说明此过程:

calendar.setWeekDate(2012, 16, 3);
System.out.println(DateFormat.getDateTimeInstance(
DateFormat.LONG, DateFormat.LONG).format(calendar.getTime()));

执行此代码时,我们得到以下输出:

2012 年 4 月 17 日下午 12:00:08 CDT

一年中第一周和最后一周的计算方式取决于区域设置。GregorianCalendar类的WEEK_OF_YEAR字段范围从 1 到 53,其中 53 代表闰周。一年中的第一周是:

  • 最早的七天周期

  • 从一周的第一天开始(getFirstDayOfWeek

  • 其中至少包含一周的最小天数(getMinimalDaysInFirstWeek

getFirstDayOfWeekgetMinimalDaysInFirstWeek方法是与区域设置相关的。例如,getFirstDayOfWeek方法返回一个整数,表示该区域设置的一周的第一天。在美国,它是星期日,但在法国是星期一。

一年中的第一周和最后一周可能有不同的日历年。考虑以下代码序列。日历设置为 2022 年第一周的第一天:

calendar.setWeekDate(2022, 1, 1);
System.out.println(DateFormat.getDateTimeInstance(
DateFormat.LONG, DateFormat.LONG).format(calendar.getTime()));

执行时,我们得到以下输出:

2021 年 12 月 26 日下午 12:15:39 CST

这表明这周实际上是从上一年开始的。

此外,TimeZoneSimpleTimeZone类有一个observesDaylightTime方法,如果时区遵守夏令时,则返回true。以下代码序列创建了一个SimpleTimeZone类的实例,然后确定是否支持夏令时。使用的时区是中央标准时间CST):

SimpleTimeZone simpleTimeZone = new SimpleTimeZone(
-21600000,
"CST",
Calendar.MARCH, 1, -Calendar.SUNDAY,
7200000,
Calendar.NOVEMBER, -1, Calendar.SUNDAY,
7200000,
3600000);
System.out.println(simpleTimeZone.getDisplayName() + " - " +
simpleTimeZone.observesDaylightTime());

执行此序列时,您应该获得以下输出:

中央标准时间-真

在 Java 7 中使用 Currency 类

java.util.Currency类引入了四种检索有关可用货币及其属性的信息的新方法。本示例说明了以下方法的使用:

  • getAvailableCurrencies:此方法返回一组可用的货币

  • getNumericCode:此方法返回货币的 ISO 4217 数字代码

  • getDisplayName:此重载方法返回表示货币显示名称的字符串。一个方法传递了一个Locale对象。返回的字符串是特定于该区域设置的。

准备就绪

getAvailableCurrencies方法是静态的,因此应该针对类名执行。其他方法针对Currency类的实例执行。

如何做...

  1. 创建一个新的控制台应用程序。将以下代码添加到main方法中:
Set<Currency> currencies = Currency.getAvailableCurrencies();
for (Currency currency : currencies) {
System.out.printf("%s - %s - %s\n", currency.getDisplayName(),
currency.getDisplayName(Locale.GERMAN),
currency.getNumericCode());
}

  1. 执行应用程序时,您应该获得类似以下内容的输出。但是,每个的第一部分可能会有所不同,这取决于当前的区域设置。

朝鲜元 - 朝鲜元 - 408

欧元 - 欧元 - 978

荷兰盾 - 荷兰盾 - 528

福克兰群岛镑 - 福克兰-镑 - 238

丹麦克朗 - 丹麦克朗 - 208

伯利兹元 - 伯利兹元 - 84

它是如何工作的...

代码序列从生成代表当前系统配置的Currency对象的Set开始。对每个集合元素执行了重载的getDisplayName方法。使用了Locale.GERMAN参数来说明此方法的使用。显示的最后一个值是货币的数字代码。

使用 NumericShaper.Range 枚举来支持数字的显示

在本示例中,我们将演示使用java.awt.font.NumericShaper.Range枚举来支持使用java.awt.font.NumericShaper类显示数字。有时希望使用不同于当前使用的语言显示数字。例如,在关于蒙古语的英语教程中,我们可能希望用英语解释数字系统,但使用蒙古数字显示数字。NumericShaper类提供了这种支持。新的NumericShaper.Range枚举简化了这种支持。

准备工作

使用NumericShaper.Range枚举来显示数字:

  1. 创建一个HashMap来保存显示属性信息。

  2. 创建一个Font对象来定义要使用的字体。

  3. 指定要显示文本的 Unicode 字符范围。

  4. 创建一个FontRenderContext对象来保存有关如何测量要显示的文本的信息。

  5. 创建一个TextLayout的实例,并在paintComponent方法中使用它来渲染文本。

操作步骤...

我们将演示使用NumericShaper.Range枚举来显示蒙古数字。这是在download.oracle.com/javase/tutorial/i18n/text/shapedDigits.html中找到的示例的简化版本。

  1. 创建一个扩展JFrame类的应用程序,如下所示。我们将在NumericShaperPanel类中演示NumericShaper类的使用:
public class NumericShaperExample extends JFrame {
public NumericShaperExample() {
Container container = this.getContentPane();
container.add("Center", new NumericShaperPanel());
this.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
this.setTitle("NumericShaper Example");
this.setSize(250, 120);
}
public static void main(String[] args) {
new NumericShaperExample();.setVisible(true)
}
NumericShaper.Range enumeration using, for digit display}

  1. 接下来,将NumericShaperPanel类添加到项目中,如下所示:
public class NumericShaperPanel extends JPanel {
private TextLayout layout;
public NumericShaperPanel() {
String text = "0 1 2 3 4 5 6 7 8 9";
HashMap map = new HashMap();
Font font = new Font("Mongolian Baiti", Font.PLAIN, 32);
map.put(TextAttribute.FONT, font);
map.put(TextAttribute.NUMERIC_SHAPING,
NumericShaper.getShaper(NumericShaper.Range. MONGOLIAN));
FontRenderContext fontRenderContext =
new FontRenderContext(null, false, false);
layout = new TextLayout(text, map, fontRenderContext);
}
public void paintComponent(Graphics g) {
Graphics2D g2d = (Graphics2D) g;
layout.draw(g2d, 10, 50);
}
}

  1. 执行应用程序。您的输出应该如下所示:

操作步骤...

工作原理...

main方法中,创建了NumericShaperExample类的一个实例。在其构造函数中,创建了NumericShaperPanel类的一个实例,并将其添加到窗口的中心。设置了窗口的标题、默认关闭操作和大小。接下来,窗口被显示出来。

NumericShaperPanel类的构造函数中,创建了一个文本字符串以及一个HashMap来保存显示的基本特性。将此映射用作TextLayout构造函数的参数,以及要显示的字符串和映射。使用蒙古 Baiti 字体和 MONGOLIAN 范围显示蒙古文。我们使用这种字体来演示NumericShaper类的新方法。

NumericShaper类已添加了新方法,使得在不同语言中显示数字值更加容易。getShaper方法被重载,其中一个版本接受一个NumericShaper.Range枚举值。该值指定要使用的语言。NumericShaper.Range枚举已添加以表示给定语言中数字的 Unicode 字符范围。

paintComponent方法中,使用Graphics2D对象作为draw方法的参数来将字符串渲染到窗口中。

还有更多...

getContextualShaper方法用于控制在与不同脚本一起使用时如何显示数字。这意味着如果在数字之前使用日语脚本,则会显示日语数字。该方法接受一组NumericShaper.Range枚举值。

shape方法还使用范围来指定要在数组中的起始和结束索引处使用的脚本。getRangeSet方法返回NumericShaper实例使用的一组NumericShaper.Range

Java 7 中的 JavaBean 增强功能

JavaBean是构建 Java 应用程序可重用组件的一种方式。它们是遵循特定命名约定的 Java 类。在 Java 7 中添加了几个 JavaBean 增强功能。在这里,我们将重点关注java.beans.Expression类,它在执行方法时非常有用。execute方法已经添加以实现这一功能。

准备工作

使用Expression类来执行方法:

  1. 为方法创建参数数组,如果需要的话。

  2. 创建Expression类的一个实例,指定要执行方法的对象、方法名称和任何需要的参数。

  3. 针对表达式调用execute方法。

  4. 如有必要,使用getValue方法获取方法执行的结果。

如何做...

  1. 创建一个新的控制台应用程序。创建两个类:JavaBeanExample,其中包含main方法和Person类。Person类包含一个用于名称的单个字段,以及构造函数、getter 方法和 setter 方法:
public class Person {
private String name;
public Person() {
this("Jane", 23);
}
public Person(String name, int age) {
this.name = name;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}

  1. JavaBeanExample类的main方法中,我们将创建Person类的一个实例,并使用Expression类来执行其getNamesetName方法:
public static void main(String[] args) throws Exception {
Person person = new Person();
String arguments[] = {"Peter"};
Expression expression = new Expression(null, person, "setName", arguments);
System.out.println("Name: " + person.getName());
expression.execute();
System.out.println("Name: " + person.getName());
System.out.println();
expression = new Expression(null, person, "getName", null);
System.out.println("Name: " + person.getName());
expression.execute();
System.out.println("getValue: " + expression.getValue());
}

  1. 执行应用程序。其输出应如下所示:

名称:Jane

名称:Peter

名称:Peter

getValue:Peter

它是如何工作的...

Person类使用了一个名为 name 的字段。getNamesetName方法是从main方法中使用的,其中创建了一个Person实例。Expression类的构造函数有四个参数。第一个参数在本例中没有使用,但可以用来定义方法执行的返回值。第二个参数是方法将被执行的对象。第三个参数是包含方法名称的字符串,最后一个参数是包含方法使用的参数的数组。

在第一个序列中,使用Peter作为参数执行了setName方法。应用程序的输出显示名称最初为Jane,但在执行execute方法后更改为Peter

在第二个序列中,执行了getName方法。getValue方法返回方法执行的结果。输出显示getName方法返回了Peter

还有更多...

java.bean包的类还有其他增强。例如,FeatureDescriptorPropertyChangeEvent类中的toString方法已被重写,以提供更有意义的描述。

Introspector类提供了一种了解 Java Bean 的属性、方法和事件的方式,而不使用可能很繁琐的反射 API。该类已添加了一个getBeanInfo方法,该方法使用Inspector类的控制标志来影响返回的BeanInfo对象。

Transient注解已添加以控制包含什么。属性的true值意味着应忽略带注解的特性。

XMLDecoder类中添加了一个新的构造函数,接受一个InputSource对象。此外,添加了createHandler方法,返回一个DefaultHandler对象。此处理程序用于解析XMLEncoder类创建的 XML 存档。

XMLEncoder类中添加了一个新的构造函数。这允许使用特定的字符集和特定的缩进将 JavaBeans 写入OutputStream

在 Java 7 中处理区域设置和Locale.Builder

java.util.Locale.Builder类已添加到 Java 7 中,并提供了一种简单的创建区域设置的方法。Locale.Category枚举也是新的,使得在显示和格式化目的上使用不同的区域设置变得容易。我们首先将看一下Locale.Builder类的使用,然后检查其他区域设置的改进以及在*还有更多..*部分中使用Locale.Category枚举。

准备工作

构建和使用新的Locale对象:

  1. 创建Builder类的一个实例。

  2. 使用类的相关方法设置所需的属性。

  3. 根据需要使用Locale对象。

如何做...

  1. 创建一个新的控制台应用程序。在main方法中,添加以下代码。我们将创建一个基于东亚美尼亚语的区域设置,使用意大利的拉丁文。通过使用setWeekDate方法,演示了该区域设置,显示了 2012 年第 16 周的第三天的日期。这种方法在Java 7 中处理周中有更详细的讨论:
Calendar calendar = Calendar.getInstance();
calendar.setWeekDate(2012, 16, 3);
Builder builder = new Builder();
builder.setLanguage("hy");
builder.setScript("Latn");
builder.setRegion("IT");
builder.setVariant("arevela");
Locale locale = builder.build();
Locale.setDefault(locale);
System.out.println(DateFormat.getDateTimeInstance(
DateFormat.LONG, DateFormat.LONG).format(calendar.getTime()));
System.out.println("" + locale.getDisplayLanguage());

  1. 第二个示例构建了一个基于中国语言的区域设置,使用了在中国大陆使用的简体字:
builder.setLanguage("zh");
builder.setScript("Hans");
builder.setRegion("CN");
locale = builder.build();
Locale.setDefault(locale);
System.out.println(DateFormat.getDateTimeInstance(
DateFormat.LONG, DateFormat.LONG).format(calendar.getTime()));
System.out.println("" + locale.getDisplayLanguage());

  1. 执行时,输出应如下所示:

April 17, 2012 7:25:42 PM CDT

亚美尼亚语

2012 年 4 月 17 日 下午 07 时 25 分 42 秒

中文

工作原理...

创建了Builder对象。使用该对象,我们应用了方法来设置区域设置的语言、脚本和地区。然后执行了build方法,并返回了一个Locale对象。我们使用这个区域设置来显示日期和区域设置的显示语言。这是两次执行的。首先是亚美尼亚语,然后是中文。

还有更多...

能够标记一条信息以指示所使用的语言是很重要的。为此目的使用了一个标签。一组标准标签由IETF BCP 47标准定义。Java 7 符合这一标准,并添加了几种方法来处理标签。

该标准支持对标签的扩展概念。这些扩展可用于提供有关区域设置的更多信息。有两种类型:

  • Unicode 区域设置扩展

  • 私有使用扩展

Unicode 区域设置扩展由Unicode 通用区域设置数据存储库CLDR)(cldr.unicode.org/)定义。这些扩展涉及非语言信息,如货币和日期。CLDR 维护了一个区域设置信息的标准存储库。私有使用扩展用于指定特定于平台的信息,例如与操作系统或编程语言相关的信息。

注意

有关 IETF BCP 47 标准的更多信息,请访问tools.ietf.org/rfc/bcp/bcp47.txt

扩展由键/值对组成。键是一个单个字符,值遵循以下格式:

SUBTAG ('-' SUBTAG)*

SUBTAG由一系列字母数字字符组成。对于 Unicode 区域设置扩展,值必须至少为两个字符,但不超过 8 个字符的长度。对于私有使用扩展,允许 1 到 8 个字符。所有扩展字符串不区分大小写。

Unicode 区域设置扩展的键为u,私有使用扩展的键为x。这些扩展可以添加到区域设置中,以提供额外的信息,例如要使用的日历编号类型。

可以使用的键列在下表中:

键代码描述
ca用于确定日期的日历算法
co整理—语言中使用的排序
ka整理参数—用于指定排序
cu货币类型信息
nu编号系统
va常见变体类型

键和类型的示例列在下表中:

键/类型含义
nu-armnlow亚美尼亚小写数字
ca-indian印度日历

已添加了几种方法来使用这些扩展。getExtensionKeys方法返回一个包含区域设置中使用的所有键的Character对象集。同样,getUnicodeLocaleAttributesgetUnicodeLocaleKeys方法返回一个列出属性和可用的 Unicode 键的字符串集。如果没有可用的扩展,这些方法将返回一个空集。如果已知键,则getExtension方法或getUnicodeLocaleType方法将返回一个包含该键值的字符串。

对于给定的区域设置,getScript, getDisplayScripttoLanguageTag方法分别返回脚本、脚本的可显示名称和区域设置的格式良好的BCP 47标签。getDisplayScript方法还将返回给定区域设置的脚本的可显示名称。

接下来的部分讨论了使用setDefault方法同时控制使用两种不同区域设置显示信息的方法。

使用Locale.Category枚举来使用两种不同的区域设置显示信息

Locale.Category枚举已添加到 Java 7。它有两个值,DISPLAYFORMAT。这允许为格式类型资源(日期、数字和货币)和显示资源(应用程序的 GUI 方面)设置默认区域设置。例如,应用程序的一部分可以将格式设置为适应一个区域设置,比如JAPANESE,同时在另一个区域设置中显示相关信息,比如GERMAN

考虑以下示例:

Locale locale = Locale.getDefault();
Calendar calendar = Calendar.getInstance();
calendar.setWeekDate(2012, 16, 3);
System.out.println(DateFormat.getDateTimeInstance(
DateFormat.LONG, DateFormat.LONG).format(calendar.getTime()));
System.out.println(ocale.getDisplayLanguage());
Locale.setDefault(Locale.Category.FORMAT, Locale.JAPANESE);
Locale.setDefault(Locale.Category.DISPLAY, Locale.GERMAN);
System.out.println(DateFormat.getDateTimeInstance(
DateFormat.LONG, DateFormat.LONG).format(calendar.getTime()));
System.out.println(locale.getDisplayLanguage());

当执行此代码序列时,您应该会得到类似以下的输出。初始日期和显示语言可能会因默认区域设置而有所不同。

2012 年 4 月 17 日下午 7:15:14 CDT

英语

2012/04/17 19:15:14 CDT

英语

已检索默认区域设置,并使用setWeekDate方法设置了一个日期。这个方法在在 Java 7 中使用星期示例中有更详细的讨论。接下来,打印日期和显示语言。显示被重复,只是使用setDefault方法更改了默认区域设置。显示资源已更改为使用Locale.JAPANESE,格式类型资源已更改为Locale.GERMAN。输出反映了这一变化。

处理 null 引用

java.lang.NullPointerException是一个相当常见的异常。当尝试对包含 null 值的引用变量执行方法时,就会发生这种情况。在这个示例中,我们将研究各种可用的技术来解决这种类型的异常。

java.util.Objects类已被引入,并提供了许多静态方法来处理需要处理 null 值的情况。使用这个类简化了对 null 值的测试。

*还有更多..*部分讨论了使用空列表的情况,这可以用来代替返回 null。java.util.Collections类有三个返回空列表的方法。

准备就绪

使用Objects类来覆盖equalshashCode方法:

  1. 覆盖目标类中的方法。

  2. 使用Objects类的equals方法来避免在equals方法中检查 null 值的显式代码。

  3. 使用Objects类的hashCode方法来避免在hashCode方法中检查 null 值的显式代码。

如何做...

  1. 创建一个新的控制台应用程序。我们将创建一个Item类来演示Objects类的使用。在Item类中,我们将覆盖equalshashCode方法。这些方法是由 NetBeans 的插入代码命令生成的。我们使用这些方法,因为它们说明了Objects类的方法并且结构良好。首先按以下方式创建类:
public class Item {
private String name;
private int partNumber;
public Item() {
this("Widget", 0);
}
public Item(String name, int partNumber) {
this.name = Objects.requireNonNull(name);
this.partNumber = partNumber;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = Objects.requireNonNull(name);
}
public int getPartNumber() {
return partNumber;
null referenceshandling}
public void setPartNumber(int partNumber) {
this.partNumber = partNumber;
}
}

  1. 接下来,按以下方式覆盖equalshashCode方法。它们提供了检查 null 值的代码:
@Override
public boolean equals(Object obj){
if (obj == null) {
return false;
}
if (getClass() != obj.getClass()) {
return false;
}
final Item other = (Item) obj;
if (!Objects.equals(this.name, other.name)) {
return false;
}
if (this.partNumber != other.partNumber) {
return false;
}
return true;
}
@Override
public int hashCode() {
int hash = 7;
hash = 47 * hash + Objects.hashCode(this.name);
hash = 47 * hash + this.partNumber;
return hash;
}

  1. 通过添加toString方法完成类:
@Override
public String toString() {
return name + " - " + partNumber;
}

  1. 接下来,在main方法中添加以下内容:
Item item1 = new Item("Eraser", 2200);
Item item2 = new Item("Eraser", 2200);
Item item3 = new Item("Pencil", 1100);
Item item4 = null;
System.out.println("item1 equals item1: " + item1.equals(item1));
System.out.println("item1 equals item2: " + item1.equals(item2));
System.out.println("item1 equals item3: " + item1.equals(item3));
System.out.println("item1 equals item4: " + item1.equals(item4));
item2.setName(null);
System.out.println("item1 equals item2: " + item1.equals(item2));

  1. 执行应用程序。您的输出应如下所示:

item1 等于 item1:true

item1 等于 item2:true

item1 等于 item3:false

item1 等于 item4:false

线程"main"中的异常 java.lang.NullPointerException

在 java.util.Objects.requireNonNull(Objects.java:201)

在 packt.Item.setName(Item.java:23)

在 packt.NullReferenceExamples.main(NullReferenceExamples.java:71)

正如我们将很快看到的,NullPointerException是尝试将 null 值分配给 Item 的名称字段的结果。

它是如何工作的...

equals方法中,首先进行了一个测试,以确定传递的对象是否为 null。如果是,则返回false。进行了一个测试,以确保类是相同类型的。然后使用equals方法来查看两个名称字段是否相等。

Objects类的equals方法的行为如下表所示。相等性的含义由第一个参数的equals方法确定:

第一个参数第二个参数返回
非 null非 null如果它们是相同的对象,则为true,否则为false
非 nullnullfalse
null非 nullfalse
nullnulltrue

最后的测试比较了两个整数partNumber字段的相等性。

Item类的hashCode方法中,Objects类的hashCode方法被应用于名称字段。如果其参数为 null,则该方法将返回 0,否则返回参数的哈希码。然后使用partNumber来计算哈希码的最终值。

注意在两个参数构造函数和setName方法中使用了requireNonNull方法。该方法检查非空参数。如果参数为 null,则抛出NullPointerException。这有效地在应用程序中更早地捕获潜在的错误。

requireNonNull方法有两个版本,第二个版本接受第二个字符串参数。当发生异常时,此参数会改变生成的消息。用以下代码替换setName方法的主体:

this.name = Objects.requireNonNull(name, "The name field requires a non-null value");

重新执行应用程序。异常消息现在将显示如下:

Exception in thread "main" java.lang.NullPointerException: The name field requires a non-null value

还有更多...

有几个其他Objects类的方法可能会引起兴趣。此外,第二部分将讨论使用空迭代器来避免空指针异常。

其他Objects类方法

Objects类的hashCode方法是重载的。第二个版本接受可变数量的对象作为参数。该方法将使用这些对象的序列生成哈希码。例如,Item类的hashCode方法可以这样编写:

@Override
public int hashCode() {
return Objects.hash(name,partNumber);
}

deepEquals方法深度比较两个对象。这意味着它比较的不仅仅是引用值。两个 null 参数被认为是深度相等的。如果两个参数都是数组,则调用Arrays.deepEqual方法。对象的相等性由第一个参数的equals方法确定。

compare方法用于比较前两个参数,根据参数之间的关系返回负值、零或正值。通常,返回 0 表示参数相同。负值表示第一个参数小于第二个参数。正值表示第一个参数大于第二个参数。

如果其参数相同,或者两个参数都为 null,则该方法将返回零。否则,返回值将使用Comparator接口的compare方法确定。

Objects类的toString方法用于确保即使对象为 null 也返回字符串。以下序列说明了这个重载方法的使用:

Item item4 = null;
System.out.println("toString: " + Objects.toString(item4));
System.out.println("toString: " + Objects.toString(item4, "Item is null"));

当执行时,该方法的第一次使用将显示单词null。在第二个版本中,字符串参数显示如下:

toString: null

toString: Item is null

使用空迭代器来避免空指针异常

避免NullPointerException的一种方法是在无法创建列表时返回非空值。返回空的Iterator可能是有益的。

在 Java 7 中,Collections类添加了三种新方法,返回一个Iterator、一个ListIterator或一个Enumeration,它们都是空的。通过返回空,它们可以在不引发空指针异常的情况下使用。

演示使用空列表迭代器,创建一个新的方法,返回一个通用的ListIterator<String>,如下所示。使用if语句来返回ListIterator或空的ListIterator

public static ListIterator<String> returnEmptyListIterator() {
boolean someConditionMet = false;
if(someConditionMet) {
ArrayList<String> list = new ArrayList<>();
// Add elements
ListIterator<String> listIterator = list.listIterator();
return listIterator;
}
else {
return Collections.emptyListIterator();
}
}

使用以下main方法来测试迭代器的行为:

public static void main(String[] args) {
ListIterator<String> list = returnEmptyListIterator();
while(())String item: list {
System.out.println(item);
}
}

执行时,不应有输出。这表示迭代器是空的。如果我们返回 null,我们将收到NullPointerException

Collections类的静态emptyListIterator方法返回一个ListIterator,其方法如下表所列:

方法行为
hasNext``hasPrevious总是返回false
next``Previous总是抛出NoSuchElementException
remove``set总是抛出IllegalStateException
add总是抛出UnsupportedOperationException
nextIndex总是返回 0
previousIndex总是返回-1

emptyIterator方法将返回一个具有以下行为的空迭代器:

方法行为
hasNext总是返回false
next总是抛出NoSuchElementException
remove总是抛出IllegalStateException

emptyEnumeration方法返回一个空枚举。它的hasMoreElements将始终返回false,它的nextElement将始终抛出NoSuchElementException异常。

在 Java 7 中使用新的 BitSet 方法

java.util.BitSet类在最新的 Java 版本中增加了几种新方法。这些方法旨在简化大量位的操作,并提供更容易访问有关位位置的信息。位集可用于优先级队列或压缩数据结构。本示例演示了一些新方法。

准备工作

要使用新的BitSet方法:

  1. 创建一个BitSet的实例。

  2. 根据需要对BitSet对象执行方法。

如何做...

  1. 创建一个新的控制台应用程序。在main方法中,创建一个BitSet对象的实例。然后声明一个长数字的数组,并使用静态的valueOf方法将我们的BitSet对象设置为这个长数组的值。添加一个println语句,这样我们就可以看到我们的长数字在BitSet中的表示方式:
BitSet bitSet = new BitSet();
long[] array = {1, 21, 3};
bitSet = BitSet.valueOf(array);
System.out.println(bitSet);

  1. 然后,使用toLongArray方法将BitSet转换回长数字的数组。使用 for 循环打印数组中的值:
long[] tmp = bitSet.toLongArray();
for (long number : tmp) {
System.out.println(number);
}

  1. 执行应用程序。您应该看到以下输出:

{0, 64, 66, 68, 128, 129}

1

21

3

它是如何工作的...

创建BitSet对象后,我们创建了一个包含三个long数字的数组,这些数字用作我们在BitSet中希望使用的位序列的表示。valueOf方法接受这个表示并将其转换为位序列。

当我们打印出BitSet时,我们看到了序列{0, 64, 66, 68, 128, 129}。这个BitSet中的每个数字代表了在我们的位序列中设置的位的索引。例如,0 代表数组中的long数字 1,因为用于表示 1 的位的索引在位置 0。同样,位 64、66 和 68 被设置为表示我们的long数字 21。序列中的第 128 和 129 位被设置为表示我们的long数字 3。在下一节中,我们使用toLongArray方法将BitSet返回到其原始形式。

在我们的示例中,我们使用了一个long数字的数组。类似的valueOf方法也适用于byte, LongBufferByteBuffer数组。当使用LongBufferByteBuffer数组时,缓冲区不会被valueOf方法修改,并且BitSet不能被转换回缓冲区。相反,必须使用toLongArray方法或类似的toByteArray方法将BitSet转换为字节数组。

还有更多...

有两种有用的方法用于定位BitSet中的设置或清除位。方法previousSetBit以整数表示特定索引作为其参数,并返回表示BitSet中最接近的设置位的整数。例如,将以下代码序列添加到我们的先前示例中(使用由长数字{1, 21, 3}表示的BitSet):

System.out.println(bitSet.previousSetBit(1));

这将导致输出整数 0。这是因为我们将索引 1 的参数传递给previousSetBit方法,而我们的BitSet中最接近的前一个设置位是在索引 0 处。

previousClearBit方法以类似的方式运行。如果我们在上一个示例中执行以下代码:

System.out.println(bitSet.previousClearBit(66));

我们将得到整数 65 的输出。位于索引 65 的位是最接近我们的参数 66 的最近的清除位。如果在BitSet中不存在这样的位,则两种方法都将返回-1。