十、探索其他工具 API

第 10 章中,我继续探索 Java 的工具 API,介绍并发工具、日期类(用于表示时间)、格式化程序类(用于格式化数据项)、随机类(用于生成随机数)、扫描器类(用于将输入的字符流解析为整数、字符串和其他值),以及用于处理 ZIP 和 JAR 文件的 API。

探索并发工具

Java 5 引入了并发工具,其类和接口简化了并发(多线程)应用的开发,Java 6 对其进行了扩展。这些类型位于 java.util.concurrent 包及其 Java . util . concurrent . atomic 和 Java . util . concurrent . locks 子包中。

注意 Android 支持所有的 Java 5 和 Java 6 并发类型。在撰写本文时,它不支持 Java 7 新增功能。

这些工具在它们的实现中利用了低级线程 API (见第 8 章),并提供了更高级别的构建模块(如锁定习惯用法)以使创建多线程应用更容易。它们被组织成执行器、同步器、并发集合、锁和原子变量类别。

遗嘱执行人

在第 8 章的中,我介绍了 Threads API,它可以让你通过像 new Java . lang . thread(new runnable task())这样的表达式来执行可运行任务。start();。这些表达式将任务提交与任务的执行机制(在当前线程、新线程或从线程池【组】中任意选择的线程上运行)紧密耦合。

注意任务是一个对象,它的类实现了 java.lang.Runnable 接口(一个可运行的任务)或者 Java . util . concurrent . callable 接口(一个可调用的任务)。

面向并发的工具提供执行器作为执行可运行任务的低级线程 API 表达式的高级替代。一个执行器是一个对象,它的类直接或间接地实现了 Java . util . concurrent . executor 接口,该接口将任务提交与任务执行机制分离开来。

注意executor 框架使用接口将任务提交从任务执行机制中分离出来,类似于 Collections 框架使用核心接口将列表、集合、队列和映射从它们的实现中分离出来。解耦产生了更容易维护的灵活代码。

Executor 声明了一个单独的 void execute(Runnable Runnable)方法,该方法在未来的某个时刻执行名为 runnable 的可运行任务。 execute() 在 runnable 为 null 时抛出 Java . lang . nullpointerexception,在无法执行 runnable 时抛出 Java . util . concurrent . rejected execution exception。

注意rejected execution exception 当一个执行程序正在关闭并且不想接受新任务时会抛出。此外,当执行器没有足够的空间来存储任务时,也会抛出这个异常(也许执行器使用了一个有界的阻塞队列来存储任务,而队列已经满了——我将在本章后面讨论阻塞队列)。

下面的例子展示了前面提到的新线程的执行器(new runnable task())。start();表情:

Executor executor = ...; //  ... represents some executor creation
executor.execute(new RunnableTask());

虽然执行器很容易使用,但是这个接口在各方面都有限制:

  • 执行人专注于可运行。因为 Runnable 的 run() 方法不返回值,所以对于一个可运行的任务来说,没有方便的方法向它的调用者返回值。
  • Executor 没有提供一种方法来跟踪执行可运行任务的进度,取消正在执行的可运行任务,或者确定可运行任务何时完成执行。
  • 执行者不能执行可运行任务的集合。
  • Executor 没有为应用提供关闭执行器的方法(更不用说正确关闭执行器了)。

这些限制由 Java . util . concurrent . executorservice 接口解决,该接口扩展了执行器,其实现通常是一个线程池。表 10-1 描述了执行服务的方法 。

表 10-1。 执行员服务方法

方法 描述
布尔 awaitTermination(长超时,时间单位单位) 阻塞(等待)直到关闭请求后所有任务都已完成,超时(以单位时间单位测量)到期,或者当前线程被中断,无论哪种情况先发生。当这个执行器已经终止时返回 true,当在终止之前经过了超时时返回 false。该方法在被中断时抛出 Java . lang . interrupted exception。
< T >列表<未来T7】invoke all(收藏<?扩展可调用的T11】任务) 执行 tasks 集合中的每个可调用任务,返回 Java . util . concurrent . future 实例的 java.util.List ,保存所有任务完成时的任务状态和结果——任务通过正常终止或抛出异常完成。未来的列表与任务迭代器返回的任务顺序相同。该方法在等待中被中断时抛出 InterruptedException ,在这种情况下未完成的任务被取消;当任务或其任意元素为空时,NullPointerException;当任务任务中的任何一个不能被调度执行时拒绝执行异常。
< T >列表<未来T7】invoke all(收藏<?扩展可调用T11】任务,长超时,时间单位单位) 执行 tasks 集合中的每个可调用任务,并返回一个 Future 实例的列表,当所有任务完成时——一个任务通过正常终止或抛出异常完成——或者超时(以单位时间单位计量)到期时,该列表保存任务状态和结果。到期时未完成的任务将被取消。未来的列表与任务迭代器返回的任务顺序相同。该方法在等待过程中被中断时抛出 InterruptedException ,在这种情况下,未完成的任务被取消。当任务、其任意元素或者单元为空时,它还抛出 NullPointerException;并且当任务中的任何一个任务不能被调度执行时,抛出 rejected execution exception。
< T > T invokeAny(收藏<?扩展可调用的T7】任务) 执行给定的任务,返回已经成功完成的任意任务的结果(即没有抛出异常),如果有的话。在正常或异常返回时,未完成的任务将被取消。该方法在等待中被中断时抛出 InterruptedException ,当任务或其任何元素为 null 时抛出 NullPointerException ,当任务为空时抛出 Java . lang . illegalargumentexception,当没有任务成功完成时抛出 Java . util . concurrent . execution exception,拒绝 ExecutionException
< T > T invokeAny(收藏<?扩展可调用的T7】任务,长超时,时间单位单位) 执行给定的任务,返回成功完成的任意任务的结果(即,不抛出异常),如果在超时(以单位时间单位计量)到期之前有任何任务成功完成,则取消到期时未完成的任务。在正常或异常返回时,未完成的任务将被取消。该方法在等待过程中被中断时抛出 interrupted exception; NullPointerException 当任务时,其任意元素或者单元为 null;IllegalArgumentException 当任务为空时;Java . util . concurrent . time out exception 在任何任务成功完成之前超过超时时;没有任务成功完成时出现 execution exception;并且当没有任务可以被调度执行时拒绝执行异常。
boolean isShutdown() 当这个执行程序被关闭时返回 true 否则,返回 false。
布尔 I terminated() 关机后所有任务完成时返回 true 否则,返回 false。在调用 shutdown() 或 shutdownNow() 之前,该方法永远不会返回 true。
无效关机() 启动有序关机,执行以前提交的任务,但不接受新任务。执行程序关闭后,调用此方法没有任何效果。该方法不等待先前提交的任务完成执行。当需要等待时,使用 awaitTermination() 。
列表<可运行>关闭现() 尝试停止所有正在执行的任务,暂停正在等待的任务的处理,并返回正在等待执行的任务列表。除了尽最大努力停止处理正在执行的任务之外,没有任何保证。例如,典型的实现将通过 Thread.interrupt() 取消,因此任何未能响应中断的任务可能永远不会终止。
< T >未来< T >提交(可调用< T >任务) 提交一个可调用的任务来执行,并返回一个代表任务未决结果的未来实例。 Future 实例的 get() 方法返回任务成功完成的结果。当任务不能被调度执行时,该方法抛出 rejected execution exception,当任务为 null 时,该方法抛出 NullPointerException 。如果您想在等待任务完成时立即阻塞,可以使用形式为 result = exec . submit(a callable)的结构。get();。
未来<?>提交(可运行任务) 提交一个可运行的任务来执行,并返回一个代表任务未决结果的未来实例。 Future 实例的 get() 方法返回任务成功完成的结果。当任务不能被调度执行时,该方法抛出 rejected execution exception,当任务为 null 时,抛出 NullPointerException 。
< T >未来< T >提交(可运行任务,T 结果) 提交一个可运行的任务来执行,并返回一个未来实例,其 get() 方法在成功完成时返回结果。当任务不能被调度执行时,该方法抛出 rejected execution exception,当任务为 null 时,该方法抛出 NullPointerException 。

表 10-1 是指 Java . util . concurrent . time unit,一个以给定粒度单位表示持续时间的枚举:天、小时、微秒、毫秒、分钟、纳秒和秒。此外, TimeUnit 声明了用于跨单元转换(例如, long toHours(长持续时间))以及用于在这些单元中执行定时和延迟操作(例如, void sleep(长超时))的方法。

表 10-1 也指可调用任务,类似于可运行任务。与 Runnable 的 void run() 方法不能抛出被检查的异常不同, Callable < V > 声明了一个 V call() 方法,该方法返回一个值,并且可以抛出被检查的异常,因为 call() 是用一个 throws Exception 子句声明的。

最后,表 10-1 引用了未来接口,它代表了一个异步计算的结果。 Future ,类属类型为 FutureT7】,提供了取消任务、返回任务值、判断任务是否完成的方法。表 10-2 描述了未来的方法。

表 10-2。 未来战法

方法 描述
boolean cancel(boolean may interruptifying) 尝试取消此任务的执行,并在任务取消时返回 true 否则,返回 false(可能在调用此方法之前任务已正常完成)。当任务已完成、已被取消或由于其他原因无法取消时,取消尝试将失败。如果成功,并且调用 cancel() 时该任务尚未开始,则该任务不应运行。如果任务已经开始,那么 mayinterruptirunning 确定执行该任务的线程是否应该被中断以试图停止该任务。这个方法返回后,对 isDone() 的后续调用总是返回 true。当 cancel() 返回 true 时,对 isCancelled() 的后续调用总是返回 true。
V get() 如果需要,等待任务完成,然后返回结果。当任务在该方法被调用之前被取消时,该方法抛出 Java . util . concurrent . cancellation exception,当任务抛出异常时抛出 ExecutionException ,当当前线程在等待时被中断时抛出 InterruptedException 。
V get(长超时,TimeUnit 单位) 等待最多个超时单位(由单位指定)的任务完成,然后返回结果(如果可用)。当任务在该方法被调用之前被取消时,该方法抛出 CancellationException ,当任务抛出异常时抛出 ExecutionException ,当当前线程在等待时被中断时抛出 InterruptedException ,当该方法的超时值到期(等待超时)时抛出 TimeoutException 。
布尔 isCancelled() 当此任务在正常完成之前被取消时,返回 true 否则,返回 false。
布尔是多() 此任务完成时返回 true 否则,返回 false。完成可能是由于正常终止、异常或取消,在所有这些情况下,该方法都返回 true。

假设您打算编写一个应用,它的图形用户界面允许用户输入单词。用户输入单词后,应用将这个单词呈现给几个在线词典,并获得每个词典的条目。这些条目随后显示给用户。

因为在线访问可能很慢,而且用户界面应该保持响应(也许用户想要结束应用),所以您将“获取单词条目”任务卸载到一个在单独线程上运行该任务的执行器。下面的例子使用 ExecutorService 、 Callable 和 Future 来实现这个目标:

ExecutorService executor = ...; //  ... represents some executor creation
Future<String[]> taskFuture = executor.submit(new Callable<String[]>()
                                              {
                                                  @Override
                                                  public String[] call()
                                                  {
                                                     String[] entries = ...;
                                                     // Access online dictionaries
                                                     // with search word and populate
                                                     // entries with their resulting
                                                     // entries.
                                                     return entries;
                                                  }
                                              });
// Do stuff.
String entries = taskFuture.get();

在以某种方式获得一个执行程序后(您将很快了解如何获得),该示例的主线程向执行程序提交一个可调用的任务。 submit() 方法立即返回一个对 Future 对象的引用,用于控制任务执行和访问结果。主线程最终调用这个对象的 get() 方法来获得这些结果。

注意Java . util . concurrent . scheduledexecutorservice 接口扩展了 ExecutorService 并描述了一个执行器,它允许您调度任务运行一次或在给定延迟后定期执行。

虽然你可以创建自己的 Executor 、 ExecutorService 和 ScheduledExecutorService 实现(比如类 DirectExecutor 实现 Executor { @ Override public void execute(Runnable r){ r . run();} }—直接在调用线程上运行 executor),还有一个更简单的替代:Java . util . concurrent . executors。

提示如果您打算创建自己的 ExecutorService 实现,您会发现使用 Java . util . concurrent . abstract ExecutorService 和 Java . util . concurrent . future task 类会很有帮助。

Executors 工具类声明了几个类方法,这些方法返回各种 ExecutorService 和 ScheduledExecutorService 实现的实例(以及其他类型的实例)。这个类的静态方法完成以下任务:

  • 创建并返回一个用常用配置设置配置的 ExecutorService 实例。
  • 创建并返回一个使用常用配置设置配置的 ScheduledExecutorService 实例。
  • 创建并返回一个“包装的” ExecutorService 或 ScheduledExecutorService 实例,通过使特定于实现的方法不可访问来禁用执行器服务的重新配置。
  • 创建并返回一个用于创建新线程的 Java . util . concurrent . thread factory 实例(即实现 ThreadFactory 接口的类的实例)。
  • 从其他类似闭包的形式中创建并返回一个可调用的实例,这样它就可以用在需要可调用的参数的执行方法中(例如, ExecutorService 的 submit(Callable) 方法)。(查看维基百科“闭包(计算机科学)”词条[http://en . Wikipedia . org/wiki/Closure _(计算机 ]了解闭包。)

例如,static ExecutorService newFixedThreadPool(int nThreads)创建一个线程池,该线程池重用固定数量的在共享的无界队列外运行的线程。最多有个线程个线程在主动处理任务。如果在所有线程都处于活动状态时提交了额外的任务,它们将在队列中等待一个可用的线程。

如果在执行器关闭之前,任何线程由于执行过程中的故障而终止,那么在需要执行后续任务时,一个新的线程将取代它的位置。在明确关闭执行器之前,线程池中的线程将一直存在。当您将零或负值传递给 n 线程 时,该方法抛出 IllegalArgumentException。

注意线程池用于消除为每个提交的任务创建新线程的开销。线程创建并不便宜,而且创建许多线程会严重影响应用的性能。

您通常会在文件和网络输入/输出上下文中使用执行器、可运行程序、可调用程序和未来程序。执行冗长的计算提供了使用这些类型的另一个场景。例如,清单 10-1 在欧拉数 e(2.71828……)的计算上下文中使用了一个执行器、一个可调用变量和一个未来变量。

清单 10-1。计算欧拉数 e

import java.math.BigDecimal;
import java.math.MathContext;
import java.math.RoundingMode;

import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;

public class CalculateE
{
   final static int LASTITER = 17;

   public static void main(String[] args)
   {
      ExecutorService executor = Executors.newFixedThreadPool(1);
      Callable<BigDecimal> callable;
      callable = new Callable<BigDecimal>()
                 {
                    @Override
                    public BigDecimal call()
                    {
                       MathContext mc = new MathContext(100,
                                                        RoundingMode.HALF_UP);
                       BigDecimal result = BigDecimal.ZERO;
                       for (int i = 0; i <= LASTITER; i++)
                       {
                          BigDecimal factorial = factorial(new BigDecimal (i));
                          BigDecimal res = BigDecimal.ONE.divide(factorial, mc);
                          result = result.add(res);
                       }
                       return result;
                    }

                    public BigDecimal factorial(BigDecimal n)
                    {
                       if (n.equals(BigDecimal.ZERO))
                          return BigDecimal.ONE;
                       else
                          return n.multiply(factorial(n.subtract(BigDecimal.ONE)));
                    }
                 };
      Future<BigDecimal> taskFuture = executor.submit(callable);
      try
      {
         while (!taskFuture.isDone())
            System.out.println("waiting");
         System.out.println(taskFuture.get());
      }
      catch(ExecutionException ee)
      {
         System.err.println("task threw an exception");
         System.err.println(ee);
      }
      catch(InterruptedException ie)
      {
         System.err.println("interrupted while waiting");
      }
      executor.shutdownNow();
   }
}

执行清单 10-1 的 main() 方法的主线程首先通过调用 Executors'newFixedThreadPool()方法获得一个 executor。然后,它实例化一个匿名类,该匿名类实现了可调用的接口,并将该任务提交给执行器,作为响应接收一个未来的实例。

提交任务后,线程通常会做一些其他工作,直到它需要任务的结果。我选择通过让主线程重复输出等待消息来模拟这项工作,直到 Future 实例的 isDone() 方法返回 true。(在实际应用中,我会避免这种循环。)此时,主线程调用实例的 get() 方法获得结果,然后输出。

注意重要的是在执行程序完成后关闭它;否则,应用可能不会结束。执行者通过调用 shutdownNow() 来完成这个任务。

callable 的 call() 方法通过计算数学幂级数 e = 1 / 0 来计算 e!+ 1 / 1!+ 1 / 2!+ ….这个级数可以用求和 1 / n 来求值!,其中 n 的范围从 0 到无穷大。

call() 首先实例化 java.math.MathContext 封装一个精度(位数)和一个舍入方式。我选择了 100 作为 e 的精度上限,也选择了 HALF_UP 作为舍入方式。

提示增加精度以及 LASTITER 的值,使级数收敛到更长更精确的 e 的近似值

call() 接下来初始化一个 java.math.BigDecimal 名为 result 的局部变量为 BigDecimal。零。然后,它进入一个循环,计算阶乘,除以 BigDecimal。一个乘以阶乘,并将除法结果加到结果上。

divide() 方法将 MathContext 实例作为其第二个参数,以确保除法不会导致无终止的十进制扩展(除法的商结果无法精确表示,例如 0.333333……),这会抛出 Java . lang . arithmetecexception(以提醒调用方商无法精确表示的事实),而执行程序会将其作为 ExecutionException 重新抛出

当您运行此应用时,您应该观察到类似如下的输出:

waiting
waiting
waiting
waiting
waiting
2.718281828459045070516047795848605061178979635251032698900735004065225042504843314055887974344245741730039454062711

同步器

Threads API 提供了同步原语,用于同步线程对临界区的访问。因为很难正确地编写基于这些原语的同步代码,所以面向并发的工具包括了同步器,这些类促进了常见形式的同步。

四种常用的同步器是倒计时锁、循环屏障、交换器和信号量:

  • 一个倒计时锁存器 让一个或多个线程在一个“门”等待,直到另一个线程打开这个门,此时这些其他线程可以继续。Java . util . concurrent . countdownlatch 类实现了这个同步器。
  • 一个循环障碍 让一组线程互相等待到达一个共同的障碍点。Java . util . concurrent . cyclic barrier 类实现了这个同步器,并利用了 Java . util . concurrent . brokenbarriexception 类。CyclicBarrier 实例在涉及固定大小线程的应用中非常有用,这些线程有时必须相互等待。 CyclicBarrier 支持一个可选的 Runnable 称为 barrier action ,它在团队中的最后一个线程到达之后、任何线程被释放之前,在每个障碍点运行一次。这个屏障动作对于在任何一方继续之前更新共享状态是有用的。
  • 一个交换器 让一对线程在同步点交换对象。Java . util . concurrent . exchange 类实现了这个同步器。每个线程在进入交换器的 exchange() 方法时提供一些对象,与伙伴线程匹配,并在返回时接收其伙伴的对象。交换器可能在遗传算法(参见【http://en.wikipedia.org/wiki/Genetic_algorithm)和管道设计等应用中有用。
  • 一个信号量 维护一组许可证,用于限制可以访问有限资源的线程数量。Java . util . concurrent . semaphore 类实现了这个同步器。如果有必要,对一个信号量的 acquire() 方法的每个调用都会被阻塞,直到获得许可,然后获取它。对 release() 的每次调用都返回一个许可,可能会释放阻塞的收单方。然而,没有使用实际的许可对象;信号量实例只记录可用许可的数量,并相应地采取行动。信号量通常用于限制可以访问某些(物理或逻辑)资源的线程数量。

考虑一下 CountDownLatch 类。它的每个实例都被初始化为非零计数。一个线程调用 CountDownLatch 的 await() 方法之一来阻塞,直到计数达到零。另一个线程调用 CountDownLatch 的 countDown() 方法来递减计数。一旦计数达到零,等待线程就被允许继续。

注意等待线程被释放后,对的后续调用 await() 立即返回。此外,因为计数不能被重置,所以一个 CountDownLatch 实例只能被使用一次。当需要重复使用时,使用 CyclicBarrier 类代替。

您可以使用 CountDownLatch 来确保线程几乎同时开始工作。例如,查看清单 10-2

清单 10-2。使用倒计时锁存器触发协调启动

import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class CountDownLatchDemo
{
   final static int NTHREADS = 3;

   public static void main(String[] args)
   {
      final CountDownLatch startSignal = new CountDownLatch(1);
      final CountDownLatch doneSignal = new CountDownLatch(NTHREADS);
      Runnable r = new Runnable()
                   {
                      @Override
                      public void run()
                      {
                         try
                         {
                            report("entered run()");
                            startSignal.await(); // wait until told to proceed
                            report("doing work");
                            Thread.sleep((int) (Math.random() * 1000));
                            doneSignal.countDown(); // reduce count on which
                                                    // main thread is waiting
                         }
                         catch (InterruptedException ie)
                         {
                            System.err.println(ie);
                         }
                      }

                      void report(String s)
                      {
                         System.out.println(System.currentTimeMillis() + ": " +
                                            Thread.currentThread() + ": " + s);
                      }
                   };
      ExecutorService executor = Executors.newFixedThreadPool(NTHREADS);
      for (int i = 0; i < NTHREADS; i++)
         executor.execute(r);
      try
      {
         System.out.println("main thread doing something");
         Thread.sleep(1000); // sleep for 1 second
         startSignal.countDown(); // let all threads proceed
         System.out.println("main thread doing something else");
         doneSignal.await(); // wait for all threads to finish
         executor.shutdownNow();
      }
      catch (InterruptedException ie)
      {
         System.err.println(ie);
      }
   }
}

清单 10-2 的主线程首先创建一对倒计时闩锁。 startSignal 倒计时锁存器阻止任何工作线程继续运行,直到主线程准备好让它们继续运行。 doneSignal 倒计时锁存导致主线程等待,直到所有工作线程完成。

主线程接下来创建一个 runnable,它的 run() 方法由随后创建的工作线程执行。

run() 方法首先输出一个初始消息,然后调用 startSignal 的 await() 方法,等待这个倒计时锁存器的计数读到零,然后才能继续。一旦发生这种情况, run() 输出一条消息,指示工作正在进行,并休眠一段随机的时间(0 到 999 毫秒)来模拟这项工作。

此时, run() 调用 doneSignal 的 countDown() 方法来减少该锁存器的计数。一旦该计数达到零,等待该信号的主线程将继续,关闭执行器并终止应用。

创建 runnable 后,主线程获得一个基于线程池的执行程序,线程池由个线程 个线程组成,然后调用执行程序的 execute() 方法个线程次,将 runnable 传递给每个基于线程池的线程个线程。这个动作启动工作线程,进入 run() 。

接下来,主线程输出一条消息并休眠一秒钟以模拟执行额外的工作(让所有工作线程都有机会进入 run() 并调用 startSignal.await() ),调用 startSignal 的 countDown() 方法以使工作线程开始运行,输出一条消息以指示它正在执行其他工作,并调用 doneSignal 的 await() 方法以

当您运行此应用时,您将会看到类似如下的输出:

main thread doing something
1353265795934: Thread[pool-1-thread-3,5,main]: entered run()
1353265795934: Thread[pool-1-thread-2,5,main]: entered run()
1353265795934: Thread[pool-1-thread-1,5,main]: entered run()
main thread doing something else
1353265796948: Thread[pool-1-thread-1,5,main]: doing work
1353265796948: Thread[pool-1-thread-2,5,main]: doing work
1353265796948: Thread[pool-1-thread-3,5,main]: doing work

注意为了简洁,我避免了演示循环障碍、交换器和信号量的例子。相反,我建议您参考这些类的 Java 文档。每个类的文档都提供了一个示例,向您展示如何使用该类。

并发收款

java.util.concurrent 包包括几个面向并发的接口和类,它们是集合框架的扩展(参见第九章):

  • blocking queue 是 BlockingQueue 和 Java . util . dequee 的子接口,它也支持阻塞操作,在检索元素之前等待 dequee 变为非空,在存储元素之前等待 dequee 中的空间变得可用。linkedblockingeque 类实现了这个接口。
  • BlockingQueue 是 java.util.Queue 的子接口,它也支持阻塞操作,在检索元素之前等待队列变为非空,在存储元素之前等待队列中的空间变得可用。每个 ArrayBlockingQueue 、 DelayQueue 、LinkedBlockingQueue、 LinkedBlockingQueue 、 PriorityBlockingQueue 和 SynchronousQueue 类都实现了这个接口。
  • ConcurrentMap 是 java.util.Map 的子接口,它声明了附加的原子 putIfAbsent() 、 remove() 和 replace() 方法。 ConcurrentHashMap 类(与 java.util.HashMap 并发等价)和 ConcurrentSkipListMap 类实现了这个接口。
  • ConcurrentNavigableMap 是 ConcurrentMap 和 java.util.NavigableMap 的子接口。concurrentskiplismap 类实现了这个接口。
  • ConcurrentLinkedQueue 是队列接口的一个无界、线程安全的 FIFO 实现。
  • ConcurrentSkipListSet 是一个可伸缩的并发 NavigableSet 实现。
  • CopyOnWriteArrayList 是 java.util.ArrayList 的线程安全变体,其中所有可变(不可变)操作(添加、设置等)都是通过制作底层数组的新副本来实现的。
  • CopyOnWriteArraySet 是一个 java.util.Set 实现,它使用内部 CopyOnWriteArrayList 实例进行所有操作。

清单 10-3 使用阻塞队列和数组阻塞队列来替代清单 8-14 的生产者-消费者应用( PC )。

清单 10-3。阻塞队列相当于清单 8-14 的 PC 应用

import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class PC
{
   public static void main(String[] args)
   {
      final BlockingQueue<Character> bq;
      bq = new ArrayBlockingQueue<Character>(26);
      final ExecutorService executor = Executors.newFixedThreadPool(2);
      Runnable producer;
      producer = new Runnable()
                 {
                    @Override
                    public void run()
                    {
                       for (char ch = 'A'; ch <= 'Z'; ch++)
                       {
                          try
                          {
                             bq.put(ch);
                             System.out.println(ch + " produced by producer.");
                          }
                          catch (InterruptedException ie)
                          {
                             assert false;
                          }
                       }
                    }
                 };
      executor.execute(producer);
      Runnable consumer;
      consumer = new Runnable()
                 {
                    @Override
                    public void run()
                    {
                       char ch = '\0';
                       do
                       {
                          try
                          {
                             ch = bq.take();
                             System.out.println(ch + " consumed by consumer.");
                          }
                          catch (InterruptedException ie)
                          {
                             assert false;
                          }
                       }
                       while (ch != 'Z');
                       executor.shutdownNow();
                    }
                 };
      executor.execute(consumer);
   }
}

清单 10-3 使用 BlockingQueue 的 put() 和 take() 方法,分别将一个对象放入阻塞队列和从阻塞队列中移除一个对象。 put() 在没有空间放一个对象时阻塞; take() 当队列为空时阻塞。

虽然 BlockingQueue 确保了一个字符在产生之前不会被消耗掉,但是这个应用的输出可能会有不同的指示。例如,下面是一次运行的部分输出:

Y consumed by consumer.
Y produced by producer.
Z consumed by consumer.
Z produced by producer.

第 8 章的 PC 应用通过引入围绕 setSharedChar()/system . out . println()的额外同步层和围绕 getSharedChar()/system . out . println()的额外同步层,克服了这种不正确的输出顺序。在下一节中,我将向您展示一种锁形式的替代方案。

Java . util . concurrent . locks 包提供了接口和类,用于以不同于内置同步和监视器的方式锁定和等待条件。这个包最基本的锁接口是锁,它提供了比通过同步保留字所能实现的更广泛的锁操作。锁还通过关联的条件对象支持等待/通知机制。

注意锁对象相对于线程进入临界区时获得的隐式锁(通过同步保留字控制)的最大优势是它们能够退出获取锁的尝试。例如, tryLock() 方法在锁不立即可用时或者在超时过期之前(如果指定的话)退出。同样,当另一个线程在获取锁之前发送中断时,lock interruptible()方法 退出。

ReentrantLock 实现了锁,描述了一个可重入互斥锁的实现,其基本行为和语义与通过 synchronized 访问的隐式监控锁相同,但是具有扩展的功能。

清单 10-4 演示了清单 10-3 的一个版本中的锁和重入锁 ,确保输出永远不会以错误的顺序显示(消费的消息出现在产生的消息之前)。

清单 10-4。根据锁实现同步

import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

public class PC
{
   public static void main(String[] args)
   {
      final Lock lock = new ReentrantLock();
      final BlockingQueue<Character> bq;
      bq = new ArrayBlockingQueue<Character>(26);
      final ExecutorService executor = Executors.newFixedThreadPool(2);
      Runnable producer;
      producer = new Runnable()
                 {
                    @Override
                    public void run()
                    {
                       for (char ch = 'A'; ch <= 'Z'; ch++)
                       {
                          try
                          {
                             lock.lock();
                             try
                             {
                                while (!bq.offer(ch))
                                {
                                   lock.unlock();
                                   Thread.sleep(50);
                                   lock.lock();
                                }
                                System.out.println(ch + " produced by producer.");
                             }
                             catch (InterruptedException ie)
                             {
                                assert false;
                             }
                          }
                          finally
                          {
                             lock.unlock();
                          }
                       }
                    }
                 };
      executor.execute(producer);
      Runnable consumer;
      consumer = new Runnable()
                 {
                    @Override
                    public void run()
                    {
                       char ch = '\0';
                       do
                       {
                          try
                          {
                             lock.lock();
                             try
                             {
                                Character c;
                                while ((c = bq.poll()) == null)
                                {
                                   lock.unlock();
                                   Thread.sleep(50);
                                   lock.lock();
                                }
                                ch = c; // unboxing behind the scenes
                                System.out.println(ch + " consumed by consumer.");
                             }
                             catch (InterruptedException ie)
                             {
                                assert false;
                             }
                          }
                          finally
                          {
                             lock.unlock();
                          }
                       }
                       while (ch != 'Z');
                       executor.shutdownNow();
                    }
                 };
      executor.execute(consumer);
   }
}

清单 10-4 使用锁的锁()和解锁()方法来获取和释放锁。当线程调用 lock() 并且锁不可用时,线程被禁用(并且不能被调度),直到锁变得可用。

这个清单还使用 BlockingQueue 的 offer() 方法代替 put() 方法在阻塞队列中存储一个对象,并使用 poll() 方法代替 take() 方法从队列中检索一个对象。使用这些替代方法是因为它们不会阻塞。

如果我使用了 put() 和 take() ,这个应用就会在下面的场景中死锁:

  1. 消费者线程通过它的 lock.lock() 调用获得锁。
  2. 生产者线程试图通过其 lock.lock() 调用来获取锁,并且被禁用,因为消费者线程已经获取了锁。
  3. 消费者线程调用 take() 从队列中获取下一个 java.lang.Character 对象。
  4. 因为队列是空的,所以使用者线程必须等待。
  5. 消费者线程在等待之前没有放弃生产者线程需要的锁,所以生产者线程也继续等待。

注意如果我可以访问由 BlockingQueue 实现使用的私有锁,我就会使用 put() 和 take() ,并且还会在那个锁上调用 Lock 的 lock() 和 unlock() 方法。由此产生的应用将与清单 8-14 的 PC 应用相同(从锁的角度来看),该应用为生产者线程和消费者线程各使用了两次同步。

运行这个应用,你会发现,就像清单 8-14 的 PC 应用一样,它从来不在同一项目的生产消息之前输出消费消息。

原子变量

Java . util . concurrent . Atomic 包提供了以原子为前缀的类(比如原子,支持对单个变量进行无锁、线程安全的操作。每个类都声明了 get() 和 set() 等方法来读写这个变量,不需要外部同步。

清单 8-10 声明了一个名为 ID 的小工具类,用于通过 ID 的 getNextID() 类方法返回唯一的长整数标识符。因为这个方法不是同步的,所以多个线程可以获得相同的标识符。清单 10-5 通过在方法头中包含保留字 synchronized 解决了这个问题。

清单 10-5。通过同步以线程安全的方式返回唯一标识符

class ID
{
   private static long nextID = 0;
   static synchronized long getNextID()
   {
      return nextID++;
   }
}

虽然 synchronized 适合这个类,但是在更复杂的类中过度使用这个保留字会导致死锁、饥饿或其他问题。清单 10-6 向您展示了如何通过用原子变量替换 synchronized 来避免对并发应用的活性(及时执行的能力)的攻击。

清单 10-6。通过原子克隆以线程安全的方式返回唯一的 id

import java.util.concurrent.atomic.AtomicLong;

class ID
{
   private static AtomicLong nextID = new AtomicLong(0);
   static long getNextID()
   {
      return nextID.getAndIncrement();
   }
}

清单 10-6 中,我已经将 nextID 从 long 转换为 atomicloning 实例,并将该对象初始化为 0。我还重构了 getNextID() 方法来调用 AtomicLong 的 getAndIncrement() 方法 ,该方法将 AtomicLong 实例的内部长整型变量递增 1,并在一个不可分割的步骤中返回先前的值。

探索日期类

第八章中我向大家介绍了 java.lang.System 类的 long currentTimeMillis() 类方法 ,它返回自 1970 年 1 月 1 日 00:00:00 UTC 以来的毫秒数。因为 Unix 是在这一天正式发布的,所以它永远被称为 Unix 纪元

java.util.Date 类用这些长整数描述日期。尽管这个类的大部分已经被废弃,但是日期的部分仍然有用。表 10-3 描述了 Date 类的非废弃部分。

表 10-3。 日期构造函数和方法

方法 描述
日期() 分配一个 Date 对象,通过调用 system . current time millis()将其初始化为当前时间。
日期(长日期) 分配一个日期对象,并将其初始化为由日期毫秒表示的时间。负值表示纪元前的时间,0 表示纪元,正值表示纪元后的时间。
(日期日期)后的布尔值 当此日期发生在日期之后时,返回 true。当日期为 null 时,该方法抛出 NullPointerException 。
(日期日期)之前的布尔数 当此日期出现在日期之前时,返回 true。当日期为 null 时,该方法抛出 NullPointerException 。
对象克隆() 返回此对象的副本。
int compareTo(日期日期) 将该日期与日期进行比较。当该日期等于日期时返回 0,当该日期在日期之前时返回负值,当该日期在日期之后时返回正值。当日期为 null 时,该方法抛出 NullPointerException 。
布尔等于(对象对象) 将此日期与由 obj 表示的日期对象进行比较。当且仅当 obj 不是 null 并且是一个 Date 对象,表示与此日期相同的时间点(精确到毫秒)时,返回 true。
长宾语() 返回纪元前必须经过的毫秒数(负值)或纪元后必须经过的毫秒数(正值)。
int hashCode() 返回这个日期的散列码。结果是由 getTime() 返回的长整型值的两半的异或。即哈希码是表达式(int)(this . gettime()^(this . gettime()>>>32))的值。
void setTime(长时间) 将此日期设置为代表由时间毫秒指定的时间点(负值表示纪元之前;正值是指在纪元之后)。
字符串 toString() 返回一个 java.lang.String 对象,包含这个日期的表示形式为 dow mon dd hh:mm:ss zzz yyyy ,其中 dow 是一周中的某一天(星期日、星期一、星期二、星期三、星期四、Fri、星期六);周一是月份(一月、二月、三月、四月、五月、六月、七月、八月、九月、十月、十一月、十二月); dd 是一个月中的第几天(01 到 31); hh 是一天中的两个十进制数字小时(00 到 23); mm 是小时内的两位小数分钟(00 到 59); ss 是分钟内的两位十进制数字秒(00 到 61,其中 60 和 61 代表闰秒); zzz 是(可能是空的)时区(可能反映夏令时);并且 yyyy 是四位十进制数字的年份。

清单 10-7 提供了一个日期类的小演示。

清单 10-7。展示日期类

import java.util.Date;

public class DateDemo
{
   public static void main(String[] args)
   {
      Date now = new Date();
      System.out.println(now);
      Date later = new Date(now.getTime() + 86400);
      System.out.println(later);
      System.out.println(now.after(later));
      System.out.println(now.before(later));
   }
}

清单 10-7 的 main() 方法创建一对 Date 对象(现在和以后)并输出它们的日期,根据 Date 的隐式调用 toString() 方法格式化。 main() 然后论证了()之后的和()之前的,证明了现在在之后之前,也就是将来。

当您运行这个应用时,它会生成类似下面的输出:

Wed Nov 21 13:21:20 CST 2012
Wed Nov 21 13:22:46 CST 2012
false
true

探索格式化程序类

C 语言的标准库通过 printf() 和相关函数提供了强大的数据格式化功能。例如, printf("%05d %x ",2380,2830);将整数文字 2380 格式化为十进制字符序列,将整数文字 2830 格式化为十六进制字符序列。格式说明符 %05d 告诉 printf() 将 2380 的格式化结果放入一个五个字符的字段中,对于小于该宽度的值,前导零。格式说明符 %x 告诉 printf() 创建 2830 的十六进制等效值,并对十六进制数字 A-F 使用小写。产生的 02380 b0e 字符序列被输出到标准输出设备。

Java 5 引入了 java.util.Formatter 类作为 printf() 风格格式字符串的解释器。此类为布局对齐和对齐提供支持;数字、字符串和日期/时间数据的通用格式。以及更多。支持常用的 Java 类型(如字节和 BigDecimal )。此外,通过关联的 java.util.Formattable 接口和 Java . util . format table flags 类,为任意用户定义的类型提供有限的格式定制。

格式化程序声明了几个用于创建格式化程序对象的构造函数。这些构造函数让您有机会指定格式化输出的发送位置。例如, Formatter() 构造函数将格式化的输出写入内部 java.lang.StringBuilder 实例,而 Formatter(output stream OS)将格式化的输出写入指定的输出流——我在第 11 章中讨论了输出流。您可以通过调用格式化程序的 Appendable out() 方法 来访问目的地。

注意Java . lang . appendable 接口描述了一个对象,可以向该对象追加 char 值和字符序列。其实例将接收格式化输出(通过格式化器类)的类实现可追加的。它声明了一些方法,如 Appendable append(char c)—将 c 的字符追加到这个 Appendable 中。当发生 I/O 错误时,该方法抛出 java.io.IOException 。

在创建了一个格式化器对象之后,您可以调用一个 format() 方法来格式化不同数量的值。例如,格式化程序格式(字符串格式,对象。。。args) 根据传递给格式参数的格式说明符字符串格式化 args 数组。当格式字符串包含非法语法、与给定参数不兼容的格式说明符、给定格式字符串的参数不足或其他非法条件时,该方法抛出 Java . util . illegalformatexception。当这个格式化程序通过调用它的 void close() 方法被关闭时,它抛出 Java . util . formatterclosedexception。返回对这个格式化程序实例的引用,以便您可以将 format() 方法调用链接在一起。

清单 10-8 提供了一个简单的格式化程序的演示。

清单 10-8。用格式化程序类演示

import java.util.Formatter;

public class FormatterDemo
{
   public static void main(String[] args)
   {
      Formatter formatter = new Formatter();
      formatter.format("%05d %x", 2380, 2830);
      System.out.println(formatter.toString()); // Output: 02380 b0e
   }
}

清单 10-8 的 main() 方法首先创建一个格式化程序对象,其目的地是一个 StringBuilder 实例。然后,它调用 format() 根据第一个参数格式化第二个和第三个 format() 参数,并将格式化后的字符序列发送到 StringBuilderappendable。最后,它调用格式化程序的字符串 toString() 方法来返回这个可追加的内容,它随后输出该内容。(我本来可以指定 system . out . println(formatter);而是因为 System.out.print() 和 System.out.println() 方法自动调用一个对象的 toString() 方法返回该对象的字符串表示。)

关于格式化程序及其支持的格式说明符的更多信息,我建议你参考格式化程序的 Java 文档。您可能还想查看 Oracle 关于 Formattable 接口和 FormattableFlags 类的文档,以了解如何定制格式化程序。

探索随机类

第七章我正式向大家介绍了 java.lang.Math 类的 random() 方法。如果您从 Java 7 的角度来研究这个方法的源代码,您将会遇到下面的实现:

private static Random randomNumberGenerator;

private static synchronized Random initRNG() {
   Random rnd = randomNumberGenerator;
   return (rnd == null) ? (randomNumberGenerator = new Random()) : rnd;
}

public static double random() {
   Random rnd = randomNumberGenerator;
   if (rnd == null) rnd = initRNG();
   return rnd.nextDouble();
}

这段代码摘录向您展示了 Math 的 random() 方法是根据一个名为 Random 的类实现的,该类位于 java.util 包中。随机实例生成随机数序列,称为随机数生成器

注意这些数字不是真正随机的,因为它们是由数学算法生成的。因此,它们通常被称为。然而,去掉“伪”前缀并把它们称为随机数通常是很方便的。此外,延迟对象创建(例如, new Random() )直到第一次需要该对象,这被称为惰性初始化

Random 从一个被称为种子的特殊 48 位值开始生成随机数序列。该值随后通过数学算法进行修改,该算法被称为线性同余发生器* 。

注意查看维基百科的“线性同余生成器”条目()来了解这种生成随机数的算法。

Random 声明一对构造函数:

  • Random() 创建新的随机数生成器。此构造函数将随机数生成器的种子设置为一个值,该值很可能不同于对此构造函数的任何其他调用。
  • Random(long seed) 使用其种子参数创建一个新的随机数生成器。该参数是随机数发生器内部状态的初始值,由保护的 int next(int bits) 方法维护。

因为 Random() 不接受种子参数,所以产生的随机数生成器总是生成不同的随机数序列。这解释了为什么 Math.random() 每次应用开始运行时都会生成不同的序列。

提示 Random(长种子)让您有机会重用相同的种子值,允许生成相同的随机数序列。在调试包含随机数的错误应用时,您会发现这个功能非常有用。

Random(long seed) 调用 void setSeed(long seed) 方法将种子设置为指定值。如果在实例化 Random 后调用 setSeed() ,随机数发生器将重置为调用 Random(long seed) 后的状态。

前面的代码摘录演示了 Random 的 double nextDouble() 方法,该方法返回该随机数生成器序列中的下一个伪随机、均匀分布的双精度浮点值,介于 0.0 和 1.0 之间。

Random 还声明了以下方法,用于返回其他类型的值:

  • Boolean next Boolean()返回该随机数生成器序列中的下一个伪随机、均匀分布的布尔值。值 true 和 false 以(大约)相等的概率生成。
  • void next bytes(byte[]bytes)生成伪随机字节整数值,并存储在 bytes 数组中。生成的字节数等于字节数组的长度。
  • float nextFloat() 返回此随机数生成器序列中的下一个伪随机、均匀分布的浮点值,介于 0.0 和 1.0 之间。
  • double next Gaussian()返回下一个伪随机,高斯(“正态”)分布的双精度浮点值,在此随机数生成器的序列中均值为 0.0,标准差为 1.0。
  • int nextInt() 返回该随机数生成器序列中的下一个伪随机、均匀分布的整数值。所有 4,294,967,296 个可能的整数值都以(近似)相等的概率生成。
  • int nextInt(int n) 返回一个伪随机的、均匀分布的整数值,该值介于 0(包括 0)和从该随机数生成器的序列中提取的指定值(不包括 0)之间。所有 n 个可能的整数值都以(近似)相等的概率产生。
  • long nextLong() 返回该随机数生成器序列中的下一个伪随机、均匀分布的长整型值。因为 Random 使用一个只有 48 位的种子,所以这个方法不会返回所有可能的 64 位长整型值。

java.util.Collections 类声明了一对用于混排列表内容的 shuffle() 方法。相比之下, java.util.Arrays 类并没有声明一个用于混合数组内容的 shuffle() 方法。清单 10-9 解决了这个遗漏。

清单 10-9。改组整数数组

import java.util.Random;

public class Shuffler
{
   public static void main(String[] args)
   {
      Random r = new Random();
      int[] array = { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 };
      for (int i = 0; i < array.length; i++)
      {
         int n = r.nextInt(array.length);
         // swap array[i] with array[n]
         int temp = array[i];
         array[i] = array[n];
         array[n] = temp;
      }
      for (int i = 0; i < array.length; i++)
         System.out.print(array[i] + " ");
      System.out.println();
   }
}

清单 10-9 展示了一个简单的整数数组洗牌的方法——这个方法可以被推广。对于从数组开始到数组结束的每个数组条目,这个条目与另一个条目交换,另一个条目的索引由 int nextInt(int n) 选择。

当您运行这个应用时,您将看到一个打乱的整数序列,类似于我观察到的以下序列:

9 0 5 6 2 3 8 4 1 7

探索扫描仪类

C 语言的标准库提供了一个 scanf() 函数,用于将输入的字符流解析为整数、浮点值等等。不甘示弱的 Java 5 引入了 java.util.Scanner 类,借助正则表达式将这些字符解析为原始类型、字符串和大整数/小数(在第十三章中讨论)。

Scanner 声明了几个构造函数,用于扫描来自不同来源的内容。例如,Scanner(InputStream source)创建一个用于扫描指定输入流的扫描器,而 Scanner(String source) 创建一个用于扫描指定字符串的扫描器。

一个扫描器实例使用一个定界符模式 ,默认匹配空白,将其输入分解成离散值。创建这个实例后,您可以调用“ hasNext ”方法之一来验证预期的字符序列是否存在,以便进行扫描。例如,您可以调用 boolean hasNextDouble() 来确定下一个字符序列是否可以扫描成双精度浮点值。

当值存在时,您将调用适当的“ next 方法来扫描该值。例如,您可以调用 double nextDouble() 来扫描这个序列,并返回一个包含其值的 double 。

下面的示例说明如何创建一个扫描器,用于扫描标准输入中的值,然后扫描后跟双精度浮点值的整数:

Scanner sc = new Scanner(System.in);
if (sc.hasNextInt())
   i = sc.nextInt();
if (sc.hasNextDouble())
   d = sc.nextDouble();

清单 10-10 给出了一个更现实的(面向菜单的)例子。

清单 10-10。在菜单上下文中扫描输入

import java.util.Scanner;

public class ScannerDemo
{
   public static void main(String[] args)
   {
      Scanner scanner = new Scanner(System.in);
      while (true)
      {
         System.out.printf("%nMenu Options%n%n");
         System.out.println("1: Frequency Count");
         System.out.printf("2: Quit%n%n");
         System.out.print("Enter your selection (1 or 2): ");
         int selection = scanner.nextInt();
         scanner.nextLine();
         if (selection == 1)
         {
            System.out.printf("%nEnter sentence: ");
            String sentence = scanner.nextLine();
            System.out.print("Enter index: ");
            int index = scanner.nextInt();
            int count = 0;
            for (int i = 0; i < sentence.length(); i++)
               if (sentence.charAt(i) == sentence.charAt(index))
                  count++;
            System.out.printf("Count of [%c] in [%s]: %d%n",
                              sentence.charAt(index), sentence, count);
         }
         else
         if (selection == 2)
            break;
      }
   }
}

清单 10-10 的 main() 方法创建一个扫描器,它扫描来自标准输入流的输入,然后进入一个 while 循环。每次循环迭代都会显示一个双选项菜单,并提示用户选择其中一个选项。

选项选择是通过一个 scanner.nextInt() 方法调用进行的。因为 nextInt() 不消耗选择号后面的行结束符,所以调用 Scanner 的 void nextLine() 方法跳过行结束符,以便不影响句子输入(当选择选项 1 时)。

如果用户选择了选项 1,则提示用户输入句子以及句子字符之一的从零开始的索引。然后,语句被迭代,索引字符的所有出现被计数。该计数随后被输出。

如果用户选择了选项 2,则循环中断,应用结束。

编译清单 10-10(javac ScannerDemo.java)并运行这个应用( java ScannerDemo )。以下输出展示了该应用的一次运行:

Menu Options

1: Frequency Count
2: Quit

Enter your selection (1 or 2): 1

Enter sentence: This is a test.
Enter index: 2
Count of [i] in [This is a test.]: 2

Menu Options

1: Frequency Count
2: Quit

Enter your selection (1 or 2): 2

要了解更多关于扫描器的信息,请查看这个类的 Java 文档。

探索 ZIP 和 JAR APIs

您可能需要开发一个应用,该应用必须创建一个新的 ZIP 文件,并将文件存储在该文件中,或者从现有的 ZIP 文件中提取内容。也许您可能需要在 JAR 文件的上下文中执行任一任务,您可能认为 JAR 文件是一个带有的 ZIP 文件。jar 文件扩展名。本节向您介绍处理 ZIP 和 JAR 文件的 API。

探索 ZIP API

java.util.zip 包提供了处理 zip 文件的类,这些类也被称为 ZIP 存档。每个 ZIP 存档存储通常被压缩的文件,每个存储的文件被称为一个 ZIP 条目 。您可以使用这些类在标准 ZIP 和 GZIP (GNU ZIP)文件格式的 ZIP 存档中写入或读取 ZIP 条目,通过这些格式使用的 DEFLATE 压缩算法压缩和解压缩数据,以及计算任意输入流的 CRC-32 和 Adler-32 校验和。

参见维基百科的“循环冗余校验”(【http://en.wikipedia.org/wiki/CRC-32】)和“阿德勒-32”()词条了解 CRC-32 和阿德勒-32。

ZipEntry 类表示一个 ZIP 条目。您必须实例化该类,以便将新条目写入 ZIP 存档或从现有 ZIP 存档中读取条目。 ZipEntry 提供了两个构造函数:

  • ZipEntry(字符串名)用指定的名创建一个新的 ZIP 条目。当名称为空时,该构造函数抛出 NullPointerException ,当分配给名称的字符串长度超过 65535 字节时,抛出 IllegalArgumentException 。
  • ZipEntry(ZipEntry ze) 创建一个新的 ZIP 条目,其值取自现有的 ZIP 条目 ze 。

此外, ZipEntry 声明了几个方法,包括以下列表中列出的方法:

  • String getComment() 返回条目的注释字符串,如果没有注释字符串,则返回 null。一个评论提供了与一个条目相关联的用户特定信息。
  • long getCompressedSize() 返回条目压缩数据的大小,如果未指定,则返回 1。当条目数据未经压缩存储时,压缩的大小与未压缩的大小相同。
  • long getCrc() 返回条目未压缩数据的 CRC-32 校验和,如果未指定校验和,则返回 1。
  • int getMethod() 返回用于压缩条目数据的压缩方法。该值是 ZipEntry 的放气或存储的(未压缩)常量之一,或者在未指定压缩方法时为 1。
  • String getName() 返回条目的名称。
  • long getSize() 返回条目数据的未压缩大小,如果未指定大小,则返回 1。
  • boolean isDirectory() 当条目描述一个目录时返回 true 否则,此方法返回 false。
  • void set comment(String comment)将条目的注释字符串设置为注释。注释字符串是可选的。指定时,最大长度应为 65,535 字节;剩余字节被截断。
  • void setCompressedSize(long csize)将条目的压缩数据大小(以字节为单位)设置为 csize 。
  • void setCrc(long crc) 将条目未压缩数据的 CRC-32 校验和设置为 crc 。当 crc 的值小于 0 或大于 0xFFFFFFFF 时,该方法抛出 IllegalArgumentException 。
  • void setMethod(int method) 将压缩方法设置为方法。当除了 ZipEntry 之外的任何值时,该方法抛出 IllegalArgumentException 。压缩(在特定级别压缩数据文件)或 ZipEntry。存储的(不压缩)被传递给方法。
  • void setSize(long size) 将条目数据的未压缩大小设置为大小。当不支持“Zip 64”()http://en . Wikipedia . org/wiki/Zip _(file _ format)# Zip 64)时,当 size 的值小于 0 或值大于 0xFFFFFFFF 时,该方法抛出 IllegalArgumentException 。

你将很快学会如何使用这个类。

将文件写入 ZIP 存档

使用 ZipOutputStream 类将 ZIP 条目(压缩的和未压缩的)写入 ZIP 存档。

注意使用 GZIPOutputStream 类创建一个 GZIP 档案,并以 GZIP 格式将文件写入该档案。为了简洁起见,我不讨论这个类。

ZipOutputStream 声明了用于创建 ZIP 输出流的 ZIP output stream(output stream out)构造函数。(你将在第 11 章中了解输出流。)虽然从概念上来说,ZipEntry 实例被写入这个流,但实际上是这些实例描述的数据被写入。

以下示例用底层文件输出流实例化了 ZipOutputStream :

ZipOutputStream zos = new ZipOutputStream(new FileOutputStream("archive.zip"));

ZipOutputStream 也声明了几个方法,并从它的通缩输出流超类继承了额外的方法。您至少可以使用以下方法:

  • void close() 关闭 ZIP 输出流和底层输出流。
  • void closeEntry() 关闭当前 ZIP 条目,并定位流以写入下一个条目。
  • void putNextEntry(ZIP entry e)开始写入新的 ZIP 条目,并将流定位到条目数据的开始处。当前条目仍处于活动状态时被关闭(即,当前条目未被前一条目调用 closeEntry() )。
  • void write(byte[] b,int off,int len) 将从偏移量 off 开始的 len 字节从缓冲区 b 写入当前 ZIP 条目。该方法将一直阻塞,直到所有字节都被写入。

每个方法在发生一般性 I/O 错误时抛出 IOException ,在发生特定于 ZIP 的 I/O 错误时抛出 ZipException (它是 IOException 的子类)。

清单 10-11 展示了一个 ZipCreate 应用,它向您展示了如何最小限度地使用 ZipOutputStream 和 ZipEntry 将各种文件存储在一个新的 ZIP 存档中。

清单 10-11。创建一个 ZIP 存档,并将指定的文件存储在该存档中

import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;

import java.util.zip.ZipEntry;
import java.util.zip.ZipOutputStream;

public class ZipCreate
{
   public static void main(String[] args) throws IOException
   {
      if (args.length < 2)
      {
         System.err.println("usage: java ZipCreate ZIPfile infile1 "+
                            "infile2 ...");
         return;
      }
      ZipOutputStream zos = null;
      try
      {
         zos = new ZipOutputStream(new FileOutputStream(args[0]));
         byte[] buf = new byte[1024];
         for (String filename: args)
         {
            if (filename.equals(args[0]))
               continue;
            FileInputStream fis = null;
            try
            {
               fis = new FileInputStream(filename);
               zos.putNextEntry(new ZipEntry(filename));
               int len;
               while ((len = fis.read(buf)) > 0)
                  zos.write(buf, 0, len);
            }
            catch (IOException ioe)
            {
               System.err.println("I/O error: " + ioe.getMessage());
            }
            finally
            {
               if (fis != null)
                  try
                  {
                     fis.close();
                  }
                  catch (IOException ioe)
                  {
                     assert false; // shouldn't happen in this context
                  }
            }
            zos.closeEntry();
         }
      }
      catch (IOException ioe)
      {
         System.err.println("I/O error: " + ioe.getMessage());
      }
      finally
      {
         if (zos != null)
            try
            {
               zos.close();
            }
            catch (IOException ioe)
            {
               assert false; // shouldn't happen in this context
            }
      }
   }
}

清单 10-11 相当简单。它首先验证命令行参数的数量,必须至少为两个:第一个参数总是要创建的 ZIP 文件的名称。如果成功,该应用将创建一个 ZIP 输出流,并将底层文件输出流写入该文件,然后将由连续命令行参数标识的那些文件的内容写入 ZIP 输出流。

该源代码中唯一可能令人困惑的部分是 if(filename . equals(args[0]))continue;。该语句防止第一个命令行参数(恰好是 ZIP 存档的名称)被添加到存档中,由于其递归性质,这是没有意义的。如果允许这种可能性,就会抛出一个包含“重复条目”消息的 ZipException 实例。

编译清单 10-11(javac ZipCreate.java)并通过以下命令行运行该应用,这将创建一个名为 a.zip 的 ZIP 归档文件并将文件 ZipCreate.java 存储在该归档文件中——该应用不是递归的(它不会递归到目录中):

java ZipCreate a.zip ZipCreate.java

您应该不会观察到任何输出。相反,您应该在当前目录中看到一个名为 a.zip 的文件。此外,当你解压缩 a.zip 时,你应该会发现一个未归档的【ZipCreate.java】文件。

您不能在归档中存储重复的文件,因为这没有意义。例如,当您执行以下命令行时,将会看到一条关于重复条目的异常消息:

java ZipCreate a.zip ZipCreate.java ZipCreate.java

ZipOutputStream 提供了更多的功能。例如,您可以使用它的 void setLevel(int level) 方法来设置连续条目的压缩级别。指定一个从 0 到 9 的整数参数,其中 0 表示不压缩,9 表示最佳压缩,较好的压缩会降低性能。(谷歌将这些限制报告为 1 和 8。)或者,指定一个平减指数类的最佳 _ 压缩、最佳 _ 速度、默认 _ 压缩(默认为 setLevel() )和其他常量作为参数。

从 ZIP 存档中读取文件

使用 ZipInputStream 类从 ZIP 存档中读取 ZIP 条目(压缩的和未压缩的)。

注意使用 GZIPInputStream 类打开一个 GZIP 档案,并从这个档案中读取 GZIP 格式的文件。为了简洁起见,我不讨论这个类。

ZipInputStream 声明了用于创建 ZIP 输入流的 ZIP InputStream(InputStream in)构造函数。(你将在第 11 章中了解输入流。)虽然从概念上讲,从这个流中读取了 ZipEntry 实例,但是实际上读取的是这些实例描述的数据。

以下示例用底层文件输入流实例化了 ZipInputStream :

ZipInputStream zis = new ZipInputStream(new FileInputStream("archive.zip"));

ZipInputStream 也声明了几个方法,并从它的 inflate inputstream 超类继承了额外的方法。您至少可以使用以下方法:

  • void close() 关闭 ZIP 输入流和底层输入流。
  • void closeEntry() 关闭当前 ZIP 条目,并定位流以读取下一个条目。
  • ZipEntry getNextEntry() 读取下一个 ZIP 条目,并将流定位到条目数据的开头。当没有更多条目时,此方法返回 null。
  • int read(byte[] b,int off,int len) 从当前 ZIP 条目读取最多 len 字节到缓冲区 b 中,从偏移量 off 开始。该方法将一直阻塞,直到所有字节都被读取。

每个方法在发生一般性 I/O 错误时抛出 IOException ,在发生特定于 ZIP 的 I/O 错误时抛出(除了 close())ZIP exception。同样,当 b 为空时 read() 抛出 NullPointerException ,当 off 为负时 Java . lang . indexoutofboundsexception, len 为负,或者 len 大于 b.length - off 。

清单 10-12 展示了一个 ZipAccess 应用,它向您展示了如何最少地使用 ZipInputStream 和 ZipEntry 从现有的 ZIP 存档中提取各种文件。

清单 10-12。访问 ZIP 存档并从该存档中提取指定的文件

import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;

import java.util.zip.ZipEntry;
import java.util.zip.ZipInputStream;

public class ZipAccess
{
   public static void main(String[] args) throws IOException
   {
      if (args.length != 1)
      {
         System.err.println("usage: java ZipAccess zipfile");
         return;
      }
      ZipInputStream zis = null;
      try
      {
         zis = new ZipInputStream(new FileInputStream(args[0]));
         byte[] buffer = new byte[4096];
         ZipEntry ze;
         while ((ze = zis.getNextEntry()) != null)
         {
            System.out.println("Extracting: " + ze);
            FileOutputStream fos = null;
            try
            {
               fos = new FileOutputStream(ze.getName());
               int numBytes;
               while ((numBytes = zis.read(buffer, 0, buffer.length)) != -1)
                  fos.write(buffer, 0, numBytes);
            }
            catch (IOException ioe)
            {
               System.err.println("I/O error: " + ioe.getMessage());
            }
            finally
            {
               if (fos != null)
                  try
                  {
                     fos.close();
                  }
                  catch (IOException ioe)
                  {
                     assert false; // shouldn't happen in this context
                  }
            }
            zis.closeEntry();
         }
      }
      catch (IOException ioe)
      {
         System.err.println("I/O error: " + ioe.getMessage());
      }
      finally
      {
         if (zis != null)
            try
            {
               zis.close();
            }
            catch (IOException ioe)
            {
               assert false; // shouldn't happen in this context
            }
      }
   }
}

清单 10-12 相当简单。它首先验证命令行参数的数量,必须正好是一个:要访问的 ZIP 文件的名称。假设成功,它将创建一个 ZIP 输入流,其中包含该文件的底层文件输入流,然后读取存储在该归档中的各种文件的内容,在当前目录中创建这些文件。

编译清单 10-12(ZipAccess.java)并通过下面的命令行运行这个应用,它访问前面的 a.zip 档案并从这个档案中提取文件 ZipCreate.java:

java ZipAccess a.zip

您应该观察到"提取:ZipCreate.java"作为单行输出,并且还注意到在当前目录中出现了一个 ZipCreate.java 文件。

ZIPFILE 与 zipinput stream 的比较

java.util.zip 包包含一个 ZipFile 类,它似乎是 ZipInputStream 的别名。与 ZipInputStream 一样,您可以使用 ZipFile 来读取 ZIP 文件的条目。然而, ZipFile 有几个不同之处,值得考虑作为替代方案:

  • ZipFile 允许通过它的 ZipEntry getEntry(字符串名)方法随机访问 ZIP 条目。给定一个 ZipEntry 实例,您可以调用 ZipEntry 的 InputStream getInputStream(ZipEntry entry)方法来获得一个用于读取条目内容的输入流。 ZipInputStream 支持对 ZIP 条目的顺序访问。
  • 根据“使用 Java APIs 压缩和解压缩数据”一文(www . Oracle . com/tech network/articles/Java/compress-1565076 . html), ZipFile 内部缓存 ZIP 条目以提高性能。 ZipInputStream 不缓存条目。

您可能对一个声明类型为 int 的模式参数的 ZipFile 构造函数感到好奇。传递给模式的参数是 ZipFile。OPEN_READ 或 ZipFile。OPEN_READ | ZipFile。打开 _ 删除。后一个参数会导致基础文件在打开和关闭之间的某个时间被删除。

这个功能是由 Java 1.3 引入的,用于解决在长时间运行的服务器应用或远程方法调用的上下文中缓存下载的 JAR 文件的相关问题。问题在http://docs . Oracle . com/javase/7/docs/technotes/guides/lang/enhancements . html讨论。

探索 JAR API

java.util.jar 包提供了处理 jar 文件的类。因为 JAR 文件是一种 ZIP 文件,所以这个包提供了扩展它们的 java.util.zip 对应物的类就不足为奇了。比如 java.util.jar.JarEntry 扩展 java.util.zip.ZipEntry 。

java.util.jar 包还提供了没有 java.util.zip 对应的类,例如 Manifest 。这些类提供了对特定于 JAR 的功能的访问。例如, Manifest 允许您使用 JAR 文件的清单(稍后解释)。

清单 10-13 展示了一个 MakeRunnableJAR 应用,它向您展示了如何使用 java.util.jar 包中的一些类型来创建一个可运行的 JAR 文件。

清单 10-13。创建一个可运行的 JAR 文件

import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;

import java.util.jar.Attributes;
import java.util.jar.JarEntry;
import java.util.jar.JarOutputStream;
import java.util.jar.Manifest;

public class MakeRunnableJAR
{
   public static void main(String[] args) throws IOException
   {
      if (args.length < 2)
      {
         System.err.println("usage: java MakeRunnableJAR JARfile " +
                            "classfile1 classfile2 ...");
         return;
      }
      JarOutputStream jos = null;
      try
      {
         Manifest mf = new Manifest();
         Attributes attr = mf.getMainAttributes();
         attr.put(Attributes.Name.MANIFEST_VERSION, "1.0");
         attr.put(Attributes.Name.MAIN_CLASS,
                  args[1].substring(0, args[1].indexOf('.')));
         jos = new JarOutputStream(new FileOutputStream(args[0]), mf);
         byte[] buf = new byte[1024];
         for (String filename: args)
         {
            if (filename.equals(args[0]))
               continue;
            FileInputStream fis = null;
            try
            {
               fis = new FileInputStream(filename);
               jos.putNextEntry(new JarEntry(filename));
               int len;
               while ((len = fis.read(buf)) > 0)
                  jos.write(buf, 0, len);
            }
            catch (IOException ioe)
            {
               System.err.println("I/O error: " + ioe.getMessage());
            }
            finally
            {
               if (fis != null)
                  try
                  {
                     fis.close();
                  }
                  catch (IOException ioe)
                  {
                     assert false; // shouldn't happen in this context
                  }
            }
            jos.closeEntry();
         }
      }
      catch (IOException ioe)
      {
         System.err.println("I/O error: " + ioe.getMessage());
      }
      finally
      {
         if (jos != null)
            try
            {
               jos.close();
            }
            catch (IOException ioe)
            {
               assert false; // shouldn't happen in this context
            }
      }
   }
}

因为清单 10-13清单 10-11 非常相似,尽管用 java.util.jar 类替换了它们的 java.util.zip 类,我将只关注这个应用中创建清单的那部分。然而,您首先需要理解 JAR 文件清单的概念。

一个清单是一个名为清单的特殊文件。MF 存储关于 JAR 文件内容的信息。这个文件位于 JAR 文件的 META-INF 目录中。例如,对于包含 Hello 应用类的可执行文件 hello.jar JAR,清单如下所示:

Manifest-Version: 1.0
Main-Class: Hello

第一行表示清单的版本,必须存在。第二行标识执行 JAR 文件时要运行的应用类。一个。不得指定类文件扩展名。这样做意味着您想要运行 Hello 包中的类。

注意你必须在主类:你好后插入一个空行。否则,在尝试运行应用时,您将收到一条“没有主清单属性,在 hello.jar 中”的错误消息。

清单 10-13 区别于清单 10-11 的关键部分是下面的代码片段:

Manifest mf = new Manifest();
Attributes attr = mf.getMainAttributes();
attr.put(Attributes.Name.MANIFEST_VERSION, "1.0");
attr.put(Attributes.Name.MAIN_CLASS,
         args[1].substring(0, args[1].indexOf('.')));
jos = new JarOutputStream(new FileOutputStream(args[0]), mf);

首先实例化清单类(通过它的无参数构造函数)来描述即将创建的清单。然后调用它的 getMainAttributes() 方法返回一个 Attributes 实例,用于访问现有的清单属性或者创建新的清单属性(比如 Main-Class )。

Attributes 本质上是一个映射,提供了 Object put(Object key,Object value) 来存储属性名/值对。传递给键的值必须是一个属性。名称等常量属性。Name.MANIFEST_VERSION 或属性。名称. MAIN_CLASS 。

注意你必须存储清单 _ 版本;否则,您将在运行时观察到一个抛出的异常。

因为。当指定 classfile 的名称作为命令行参数时,必须指定 class 文件扩展名,表达式 args[1]。substring(0,args[1]。indexOf(“.”)))用于移除此扩展—您可以指定多个类文件名作为命令行参数;第一个名字被存储(没有它的)。class 扩展)在清单中。

最后, JarOutputStream 以类似于 ZipOutputStreamT5】的方式被实例化。然而,初始化的清单实例也作为第二个参数传递给构造函数。

要使用这个应用,您至少需要一个带有 public static void main(String[]args)方法的类。为了简单起见,考虑清单 10-14 中的。

清单 10-14。打招呼

public class Hello
{
   public static void main(String[] args)
   {
      System.out.println("Hello");
   }
}

清单 10-14 不是一个很好的应用,但是对于我们的目的来说已经足够了。编译清单 10-13清单 10-14 并执行以下命令:

java MakeRunnableJAR hello.jar Hello.class

如果一切顺利,您应该在当前目录中观察到一个 hello.jar 文件。执行以下命令来运行该文件:

java -jar hello.jar

假设成功,您应该观察到由 Hello 组成的单行输出。

练习

以下练习旨在测试您对第 10 章内容的理解:

  1. 定义任务。
  2. 定义执行者。
  3. 确定执行器接口的限制。
  4. 执行者的局限性是如何克服的?
  5. Runnable 的 run() 方法和 Callable 的 call() 方法有什么区别?
  6. 是非判断:您可以从 Runnable 的 run() 方法中抛出已检查和未检查的异常,但只能从 Callable 的 call() 方法中抛出未检查的异常。
  7. 定义未来。
  8. 描述 Executors 类的 newFixedThreadPool() 方法。
  9. 定义同步器。
  10. 识别并描述四种常用的同步器。
  11. 并发工具为集合框架提供了哪些面向并发的扩展?
  12. 定义锁。
  13. 锁对象持有线程进入临界区时获得的隐式锁(通过同步保留字控制)的最大优势是什么?
  14. 定义原子变量。
  15. 日期类描述的是什么?
  16. 格式化程序类的用途是什么?
  17. 随机类完成什么?
  18. 扫描器类的用途是什么?
  19. 在扫描一个字符序列之前,如何确定该序列代表的是整数还是其他类型的值?
  20. 找出 ZipFile 和 ZipInputStream 之间的两个区别。
  21. 创建一个类似于 ZipAccess 的 ZipList 应用,但是只输出关于存档的信息:它也不提取文件内容。要输出的信息是条目的名称、压缩和未压缩的大小以及上次修改时间。使用日期类将最后修改时间转换为可读的字符串。

摘要

Java 5 引入了并发工具来简化并发应用的开发。这些工具被组织成执行器、同步器、并发收集、锁和原子变量类别,并在其实现中利用低级线程 API。

执行器将任务提交从任务执行机制中分离出来,由执行器、执行器服务和调度执行器服务接口描述。同步器有助于常见形式的同步;倒计时锁、循环屏障、交换器和信号量是常用的同步器。

并发收集是收集框架的扩展。锁支持高级锁定,并且可以以不同于内置同步和监视器的方式与条件相关联。最后,原子变量封装了单个变量,并支持对该变量进行无锁、线程安全的操作。

Date 类根据相对于 Unix 纪元的长整型值来描述日期。尽管这个类的大部分已经被弃用,但是部分日期(例如, long getTime() 方法)仍然有用。

Java 5 引入了格式器类作为 printf() 风格格式字符串的解释器。此类为布局对齐和对齐提供支持;数字、字符串和日期/时间数据的通用格式。以及更多。

Math 类的 random() 方法是根据 Random 类实现的,其实例被称为随机数生成器。 Random 从一个特殊的 48 位种子开始生成一个随机数序列。该值随后通过称为线性同余发生器的数学算法进行修改。

Java 5 引入了 Scanner 类,用于借助正则表达式将输入的字符流解析为原始类型、字符串和大整数/小数。您调用一个“ hasNext 方法来验证要扫描的预期字符序列是否存在,并调用适当的“ next 方法来扫描值。

您可能需要开发一个应用,该应用必须创建一个新的 ZIP 文件,并将文件存储在该文件中,或者从现有的 ZIP 文件中提取内容。 java.util.zip 包提供了处理 zip 文件的类,也称为 ZIP 存档。每个 ZIP 存档存储通常被压缩的文件,并且每个存储的文件被称为一个 ZIP 条目。

也许您可能需要在 JAR 文件的上下文中执行任一任务,您可能认为 JAR 文件是一个带有的 ZIP 文件。jar 文件扩展名。 java.util.jar 包提供了处理 jar 文件的类。因为 JAR 文件是一种 ZIP 文件,所以这个包提供了扩展它们的 java.util.zip 对应物的类就不足为奇了。

这一章完成了我对 Java 工具 API 的浏览。在第 11 章中,我探索了 Java 的经典 I/O API:文件、随机访问文件、流和写/读器。*