面向-Android-开发的-Java-学习手册-五-

81 阅读1小时+

面向 Android 开发的 Java 学习手册(五)

原文:Learn Java for Android development

协议:CC BY-NC-SA 4.0

十、探索其他工具 API

在第十章中,我继续探索 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 (见第八章),并提供了更高级别的构建模块(如锁定习惯用法)以使创建多线程应用更容易。它们被组织成执行器、同步器、并发集合、锁和原子变量类别。

遗嘱执行人

在第八章的中,我介绍了 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 >列表<未来invoke all(收藏<?扩展可调用的任务)执行 tasks 集合中的每个可调用任务,返回 Java . util . concurrent . future 实例的 java.util.List ,保存所有任务完成时的任务状态和结果——任务通过正常终止或抛出异常完成。未来的列表与任务迭代器返回的任务顺序相同。该方法在等待中被中断时抛出 InterruptedException ,在这种情况下未完成的任务被取消;当任务或其任意元素为空时,NullPointerException;当任务任务中的任何一个不能被调度执行时拒绝执行异常。
< T >列表<未来invoke all(收藏<?扩展可调用任务,长超时,时间单位单位)执行 tasks 集合中的每个可调用任务,并返回一个 Future 实例的列表,当所有任务完成时——一个任务通过正常终止或抛出异常完成——或者超时(以单位时间单位计量)到期时,该列表保存任务状态和结果。到期时未完成的任务将被取消。未来的列表与任务迭代器返回的任务顺序相同。该方法在等待过程中被中断时抛出 InterruptedException ,在这种情况下,未完成的任务被取消。当任务、其任意元素或者单元为空时,它还抛出 NullPointerException;并且当任务中的任何一个任务不能被调度执行时,抛出 rejected execution exception。
< T > T invokeAny(收藏<?扩展可调用的任务)执行给定的任务,返回已经成功完成的任意任务的结果(即没有抛出异常),如果有的话。在正常或异常返回时,未完成的任务将被取消。该方法在等待中被中断时抛出 InterruptedException ,当任务或其任何元素为 null 时抛出 NullPointerException ,当任务为空时抛出 Java . lang . illegalargumentexception,当没有任务成功完成时抛出 Java . util . concurrent . execution exception,拒绝 ExecutionException
< T > T invokeAny(收藏<?扩展可调用的任务,长超时,时间单位单位)执行给定的任务,返回成功完成的任意任务的结果(即,不抛出异常),如果在超时(以单位时间单位计量)到期之前有任何任务成功完成,则取消到期时未完成的任务。在正常或异常返回时,未完成的任务将被取消。该方法在等待过程中被中断时抛出 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 ,类属类型为 Future,提供了取消任务、返回任务值、判断任务是否完成的方法。表 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) 方法)。(查看维基百科“闭包(计算机科学)”词条[[en . Wikipedia . org/wiki/Closure _(计算机](en.wikipedia.org/wiki/Closur… 科学) ]了解闭包。)

例如,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.

第八章的 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)将格式化的输出写入指定的输出流——我在第十一章中讨论了输出流。您可以通过调用格式化程序的 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)构造函数。(你将在第十一章中了解输出流。)虽然从概念上来说,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)构造函数。(你将在第十一章中了解输入流。)虽然从概念上讲,从这个流中读取了 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 文件的相关问题。问题在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 以类似于 ZipOutputStream的方式被实例化。然而,初始化的清单实例也作为第二个参数传递给构造函数。

要使用这个应用,您至少需要一个带有 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 组成的单行输出。

练习

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

  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 的浏览。在第十一章中,我探索了 Java 的经典 I/O API:文件、随机访问文件、流和写/读器。*

十一、执行经典 I/O

应用经常输入数据进行处理,并输出处理结果。数据从文件或其他来源输入,然后输出到文件或其他目的地。Java 通过位于 java.io 包中的经典 I/O API 和位于 java.nio 和相关子包(以及 java.util.regex 中的新 I/O API 来支持 I/O。本章向您介绍了经典的 I/O API。

注意你已经在第一章的标准 I/O 覆盖、第八章的进程类和第十章的 ZIP 和 JAR APIs 的上下文中体验了经典 I/O。

使用文件 API

应用经常与一个文件系统 交互,该文件系统通常表示为从根目录开始的文件和目录的层次结构。

Android 和其他运行虚拟机的平台通常支持至少一个文件系统。例如,Unix/Linux(和基于 Linux 的 Android)平台将所有挂载的(附加和准备好的)磁盘组合成一个虚拟文件系统。相比之下,Windows 将单独的文件系统与每个活动磁盘驱动器相关联。

Java 通过其具体的 java.io.File 类提供对底层平台可用文件系统的访问。 File 声明了 File[] listRoots() 类方法来返回可用文件系统的根目录(root)作为一个数组 File 对象。

注意可用文件系统根的设置受到平台级操作的影响,例如插入或弹出可移动介质,以及断开或卸载物理或虚拟磁盘驱动器。

清单 11-1 展示了一个 DumpRoots 应用,它使用 listRoots() 获得一个可用文件系统根的数组,然后输出数组的内容。

清单 11-1 。将可用的文件系统根目录转储到标准输出

import java.io.File;

public class DumpRoots
{
   public static void main(String[] args)
   {
      File[] roots = File.listRoots();
      for (File root: roots)
         System.out.println(root);
   }
}

当我在 Windows 7 平台上运行这个应用时,我会收到以下输出,其中显示了四个可用的根:

C:\
D:\
E:\
F:\

如果我碰巧在 Unix 或 Linux 平台上运行了 DumpRoots ,我会收到一行输出,其中包含虚拟文件系统根( / )。

除了使用 listRoots() 之外,还可以通过调用 File(String pathname)等 File 构造函数,创建一个 File 实例,存储 pathname 字符串。下列赋值语句演示了此构造函数:

File file1 = new File("/x/y");
File file2 = new File("C:\\temp\\x.dat");

第一条语句假设一个 Unix/Linux 平台,以根目录符号 / 开始路径名,接着是目录名 x ,分隔符 / ,以及文件或目录名 y 。(它也适用于 Windows,Windows 假定该路径从当前驱动器的根目录开始。)

注意路径是一个目录层次结构,必须遍历它才能找到文件或目录。路径名是路径的字符串表示;与平台相关的分隔符(例如,Windows 反斜杠[ \ ]字符)出现在连续名称之间。

第二条语句假设使用 Windows 平台,以驱动器说明符 C: 开始路径名,然后是根目录符号 \ ,目录名 temp ,分隔符 \ ,文件名 x.dat (尽管 x.dat 可能指的是一个目录)。

注意字符串中出现的反斜杠字符一定要加双,尤其是在指定路径名的时候;否则,您将面临出现错误或编译器错误消息的风险。例如,我在第二条语句中使用了双倍的反斜杠字符来表示反斜杠而不是制表符( \t ),以避免编译器错误消息( \x 是非法的)。

每条语句的路径名都是一个绝对路径名 ,是一个以根目录符号开头的路径名;不需要其他信息来定位它所表示的文件/目录。相比之下,相对路径名 不是以根目录符号开始的;它是通过从其他路径名获取的信息来解释的。

注意Java . io 包的类默认解析当前用户(也称为工作)目录的相对路径名,该目录由系统属性 user.dir 标识,通常是虚拟机启动的目录。(第八章向您展示了如何通过 java.lang.System 的 getProperty() 方法读取系统属性。)

文件实例通过存储抽象路径名 来包含文件和目录路径名的抽象表示(这些文件或目录可能存在,也可能不存在于它们的文件系统中),这提供了独立于平台的分层路径名视图。相反,用户界面和操作系统使用依赖于平台的路径名字符串来命名文件和目录。

抽象路径名由一个可选的与平台相关的前缀字符串组成,例如磁盘驱动器说明符(Unix/Linux 根目录用“/”表示,Windows 通用命名约定(UNC)路径名用“\”表示)以及一系列零个或多个字符串名称。抽象路径名中的第一个名称可以是目录名,或者在 Windows UNC 路径名的情况下,可以是主机名。每个后续名称表示一个目录;姓氏可以表示目录或文件。空抽象路径名 没有前缀,名称序列为空。

路径名字符串与抽象路径名之间的转换本质上是平台相关的。当路径名字符串被转换为抽象路径名时,该字符串中的名称可以由默认名称分隔符或底层平台支持的任何其他名称分隔符分隔。当一个抽象路径名被转换成一个路径名字符串时,每个名称与下一个名称之间用一个默认的名称分隔符分开。

注意默认名称-分隔符由系统属性 file.separator 定义,并在 File 的 public static separator 和 separatorChar 字段中可用——第一个字段将字符存储在 java.lang.String 实例中,第二个字段将其存储为 char 值。

文件为实例化这个类提供了额外的构造函数。例如,下面的构造函数将父路径名和子路径名合并成组合路径名,存储在文件对象中:

  • File(String parent,String child) 从一个父路径名字符串和一个子路径名字符串创建一个新的文件实例。
  • File(File parent,String child) 从一个父路径名文件实例和一个子路径名字符串创建一个新的文件实例。

每个构造函数的父参数被传递一个父路径名,这是一个由除了姓氏之外的所有路径名组成的字符串,姓氏由子指定。下面的语句通过文件(String,String) 演示了这个概念:

File file3 = new File("prj/books/", "ljfad2");

构造器将父路径名 prj/books/ 与子路径名 ljfad2 合并成路径名 prj/books/ljfad2 。(如果我指定了 prj/books 作为父路径名,那么构造函数就会在 books 之后添加分隔符。)

提示因为文件(字符串路径名)、文件(字符串父级,字符串子级)和文件(文件父级,字符串子级)不检测无效的路径名参数(除了当路径名或子级为 null 时抛出 Java . lang . nullpointerexception),所以在指定路径名时一定要小心。您应该尽量只指定对应用运行的所有平台都有效的路径名。例如,不要在路径名中硬编码驱动器说明符(比如 C:),而是使用从 listRoots() 返回的根。更好的是,保持路径名相对于当前用户/工作目录(从 user.dir 系统属性返回)。

在获得一个文件对象后,您可以通过调用表 11-1 中描述的方法来询问它,以了解其存储的抽象路径名。

表 11-1。 文件学习方法关于存储的抽象路径名

方法描述
【getabsolutefile()文件 ??]返回这个文件对象的抽象路径名的绝对形式。该方法相当于新建文件(this.getAbsolutePath()) 。
字符串 getAbsolutePath()返回这个文件对象的抽象路径名的绝对路径名字符串。当它已经是绝对路径时,就像调用 getPath() 一样返回路径名字符串。当是空的抽象路径名时,返回当前用户目录的路径名字符串(通过 user.dir 标识)。否则,抽象路径名以依赖于平台的方式被解析。在 Unix/Linux 平台上,相对路径名通过根据当前用户目录进行解析而成为绝对路径名。在 Windows 平台上,通过根据路径名命名的驱动器的当前目录或当前用户目录(如果没有驱动器)解析路径名,使路径名成为绝对路径。
文件获取规范文件()返回这个文件对象的抽象路径名的规范(尽可能简单、绝对和唯一)形式。当发生 I/O 错误时,该方法抛出 java.io.IOException (创建规范路径名可能需要文件系统查询);它等同于新文件(this.getCanonicalPath()) 。
字串 get anoniclpath()返回这个文件对象的抽象路径名的规范路径名字符串。这个方法首先在必要的时候将这个路径名转换成绝对形式,就像通过调用 getAbsolutePath() 一样,然后以一种平台相关的方式将其映射到其唯一的形式。这样做通常需要删除多余的名称,如“.”还有“..”从路径名,解析符号链接(在 Unix/Linux 平台上),并将驱动器号转换为标准大小写(在 Windows 平台上)。当出现 I/O 错误时,该方法抛出 IOException (创建规范路径名可能需要文件系统查询)。
字串 getName()返回由这个文件对象的抽象路径名表示的文件名或目录名。此名称是路径名名称序列中的最后一个。当路径名的名称序列为空时,返回空字符串。
字串 get arent()返回这个文件对象的路径名的父路径名字符串,或者当这个路径名没有命名父目录时返回 null。
【getparentfile()档案返回一个文件对象,存储这个文件对象的抽象路径名的父抽象路径名;当父路径名不是目录时,返回 null。
字符串 getPath()将此文件对象的抽象路径名转换为路径名字符串,其中序列中的名称由存储在文件的分隔符字段中的字符分隔。返回结果路径名字符串。
boolean isAbsolute()当这个文件对象的抽象路径名是绝对路径名时返回 true 否则,当它是相对的时,返回 false。绝对路径名的定义取决于系统。在 Unix/Linux 平台上,当前缀为“ / 时,路径名是绝对的。在 Windows 平台上,如果路径名的前缀是驱动器说明符,后跟“ \ ”,或者前缀是“ \ ,则该路径名是绝对路径名。
字符串 toString()getPath() 的同义词。

表 11-1 引用了 IOException ,它是那些描述各种 I/O 错误的异常类的公共异常超类,如 Java . io . filenotfoundexception。

清单 11-2 用路径名命令行参数实例化文件,并调用表 11-1 中描述的一些文件方法来了解这个路径名。

清单 11-2 。获取抽象路径名信息

import java.io.File;
import java.io.IOException;

public class PathnameInfo
{
   public static void main(final String[] args) throws IOException
   {
      if (args.length != 1)
      {
         System.err.println("usage: java PathnameInfo pathname");
         return;
      }
      File file = new File(args[0]);
      System.out.println("Absolute path = " + file.getAbsolutePath());
      System.out.println("Canonical path = " + file.getCanonicalPath());
      System.out.println("Name = " + file.getName());
      System.out.println("Parent = " + file.getParent());
      System.out.println("Path = " + file.getPath());
      System.out.println("Is absolute = " + file.isAbsolute());
   }
}

比如当我指定 java PathnameInfo 的时候。(句点代表我的 Windows 7 平台上的当前目录),我观察到以下输出:

Absolute path = C:\prj\dev\ljfad2\ch11\code\PathnameInfo\.
Canonical path = C:\prj\dev\ljfad2\ch11\code\PathnameInfo
Name = .
Parent = null
Path = .
Is absolute = false

这个输出表明规范路径名不包含句点。它还显示没有父路径名,并且路径名是相对的。

接下来,我现在指定 Java pathname info c:\ reports \ 2012 ..\ 2011 \ 2 月。这一次,我观察到以下输出:

Absolute path = c:\reports\2012\..\2011\February
Canonical path = C:\reports\2011\February
Name = February
Parent = c:\reports\2012\..\2011
Path = c:\reports\2012\..\2011\February
Is absolute = true

这个输出表明规范路径名不包括 2012 。它还显示路径名是绝对的。

对于我的最后一个例子,假设我指定 java PathnameInfo "" 来获取空路径名的信息。作为响应,该应用生成以下输出:

Absolute path = C:\prj\dev\ljfad2\ch11\code\PathnameInfo
Canonical path = C:\prj\dev\ljfad2\ch11\code\PathnameInfo
Name =
Parent = null
Path =
Is absolute = false

输出显示 getName() 和 getPath() 返回空字符串( "" ),因为空路径名为空。

您可以通过调用表 11-2 中描述的方法来询问文件系统,以了解由文件对象的存储路径名表示的文件或目录。

表 11-2。 学习关于文件或目录的文件方法

方法描述
【boolean can execute()当这个文件对象的抽象路径名代表一个现有的可执行文件时,返回 true。
布尔 canRead()当这个文件对象的抽象路径名代表一个现有的可读文件时,返回 true。
布尔 canWrite()当这个文件对象的抽象路径名代表一个可以被修改的现有文件时,返回 true。
布尔存在()当且仅当由这个文件对象的抽象路径名表示的文件或目录存在时,返回 true。
布尔 isDirectory()当这个文件对象的抽象路径名指向一个现有目录时,返回 true。
boolean isFile()当这个文件对象的抽象路径名引用一个现有的普通文件时,返回 true。当一个文件不是一个目录并且满足其他平台相关的标准时,它就是正常的*:例如,它不是一个符号链接或者一个命名管道。由 Java 应用创建的任何非目录文件都保证是普通文件。*
布尔石田()当由这个文件对象的抽象路径名表示的文件被隐藏时,返回 true。隐藏的 ?? 的确切定义取决于平台。在 Unix/Linux 平台上,当文件名以句点字符开头时,文件将被隐藏。在 Windows 平台上,当文件在文件系统中被标记为隐藏时,该文件就是隐藏的。
long last modified()返回由这个文件对象的抽象路径名表示的文件最后被修改的时间,或者当文件不存在或者在这个方法调用期间发生 I/O 错误时返回 0。从 Unix 纪元(格林威治时间 1970 年 1 月 1 日 00:00:00)开始,返回值以毫秒为单位进行测量。
龙长()返回由这个文件对象的抽象路径名表示的文件长度。当路径名表示目录时,返回值是未指定的,当文件不存在时,返回值为 0。

清单 11-3 用其路径名命令行参数实例化文件,并调用表 11-2 中描述的所有文件方法来了解路径名的文件/目录。

清单 11-3 。获取文件/目录信息

import java.io.File;
import java.io.IOException;

import java.util.Date;

public class FileDirectoryInfo
{
   public static void main(final String[] args) throws IOException
   {
      if (args.length != 1)
      {
         System.err.println("usage: java FileDirectoryInfo pathname");
         return;
      }
      File file = new File(args[0]);
      System.out.println("About " + file + ":");
      System.out.println("Can execute = " + file.canExecute());
      System.out.println("Can read = " + file.canRead());
      System.out.println("Can write = " + file.canWrite());
      System.out.println("Exists = " + file.exists());
      System.out.println("Is directory = " + file.isDirectory());
      System.out.println("Is file = " + file.isFile());
      System.out.println("Is hidden = " + file.isHidden());
      System.out.println("Last modified = " + new Date(file.lastModified()));
      System.out.println("Length = " + file.length());
   }
}

例如,假设我有一个名为 x.dat 的 3 字节只读文件。当我指定 Java filedirectorinfo x . dat 时,我观察到以下输出:

About x.dat:
Can execute = true
Can read = true
Can write = false
Exists = true
Is directory = false
Is file = true
Is hidden = false
Last modified = Tue Nov 20 12:12:09 CST 2012
Length = 3

注意 Java 6 向文件添加了 long getFreeSpace() 、 long getTotalSpace() 和 long getUsableSpace() 方法,这些方法返回关于分区(文件系统的特定平台存储部分;例如 C:)由文件实例的路径名描述。Android 支持这些额外的方法。

File 声明了五个方法,这些方法返回位于由 File 对象的抽象路径名标识的目录中的文件和目录的名称。表 11-3 描述了这些方法。

表 11-3。 获取目录内容的文件方法

方法描述
字符串【列表】()返回一个可能为空的字符串数组,命名由这个文件对象的抽象路径名表示的目录中的文件和目录。如果路径名不表示目录,或者如果发生 I/O 错误,该方法返回 null。否则,它返回一个字符串数组,目录中的每个文件或目录一个字符串。表示目录本身和目录父目录的名称不包括在结果中。每个字符串都是文件名,而不是完整的路径。此外,也不能保证结果数组中的名称字符串会按字母顺序或任何其他顺序出现。
String[]list(filename filter filter)调用 list() 并只返回那些满足过滤器的字符串的便捷方法。
File[]list files()调用 list() ,将其数组字符串转换为数组文件并返回文件 s 数组的便捷方法。
File[]list files(FileFilter filter)一个调用 list() 的便捷方法,将其数组字符串转换为数组文件 s,但只针对那些满足 filter 的字符串,返回文件 s 数组。
File[]list files(filename filter 过滤器)一个调用 list() 的便捷方法,将其数组字符串转换为数组文件 s,但只针对那些满足 filter 的字符串,返回文件 s 数组。

重载的 list() 方法 返回表示文件和目录名的字符串的数组。第二种方法让您只返回那些感兴趣的名字(例如,只返回那些以扩展名结尾的名字)。txt )通过一个基于 java.io.FilenameFilter 的过滤器对象。

FilenameFilter 接口声明单个布尔 accept(文件目录,字符串名称)方法,该方法被调用用于位于由文件对象的路径名标识的目录中的每个文件/目录:

  • dir 标识路径名的父部分(目录路径)。
  • name 标识路径名的最终目录名或文件名部分。

accept() 方法 使用传递给这些参数的自变量来确定文件或目录是否满足其可接受的标准。当文件/目录名应该包含在返回的数组中时,它返回 true 否则,此方法返回 false。

清单 11-4 展示了一个 Dir (ectory)应用,它使用 list(filename filter)只获取那些以特定扩展名结尾的名字。

清单 11-4 。列出具体的名称

import java.io.File;
import java.io.FilenameFilter;

public class Dir
{
   public static void main(final String[] args)
   {
      if (args.length != 2)
      {
         System.err.println("usage: java Dir dirpath ext");
         return;
      }
      File file = new File(args[0]);
      FilenameFilter fnf = new FilenameFilter()
                           {
                              @Override
                              public boolean accept(File dir, String name)
                              {
                                 return name.endsWith(args[1]);
                              }
                           };
      String[] names = file.list(fnf);
      for (String name: names)
         System.out.println(name);
   }
}

例如,当我在我的 Windows 7 平台上指定 java Dir c:\windows exe 时, Dir 只输出那些带有的 \windows 目录文件名。exe 扩展名:

bfsvc.exe
explorer.exe
fveupdate.exe
HelpPane.exe
hh.exe
notepad.exe
regedit.exe
splwow64.exe
twunk_16.exe
twunk_32.exe
winhlp32.exe
write.exe

重载的 listFiles() 方法 返回数组文件。在很大程度上,它们与其对应的 list() 是对称的。然而, listFiles(FileFilter) 引入了不对称性。

java.io.FileFilter 接口声明了一个单独的布尔 accept(字符串路径名)】方法,该方法被调用用于位于由文件对象的路径名标识的目录中的每个文件/目录:传递给路径名的参数标识文件或目录的完整路径。

accept() 方法使用这个参数来确定文件或目录是否满足其可接受的标准。当文件/目录名应该包含在返回的数组中时,它返回 true 否则,此方法返回 false。

注意因为每个接口的 accept() 方法完成相同的任务,您可能想知道使用哪个接口。如果你喜欢一个分解成目录和名称组件的路径,使用 FilenameFilter 。但是,如果您喜欢完整的路径名,可以使用 FileFilter;你可以随时调用 getParent() 和 getName() 来获取这些组件。

File 还声明了几个创建文件和操作现有文件的方法。表 11-4 描述了这些方法。

表 11-4。 文件创建文件和操作现有文件的方法

方法描述
布尔 createNewFile()当且仅当具有此名称的文件尚不存在时,自动创建一个新的空文件,并以此文件对象的抽象路径名命名。检查文件是否存在以及在文件不存在时创建文件是一个单独的操作,相对于可能影响文件的所有其他文件系统活动来说,它是原子性的。当指定的文件不存在并且成功创建时,该方法返回 true,当指定的文件已经存在时,该方法返回 false。当一个 I/O 错误发生时,它抛出 IOException 。
静态文件 createTempFile (字符串前缀,字符串后缀)使用给定的前缀和后缀在默认的临时文件目录中创建一个空文件,并生成其名称。这个重载的类方法调用它的三参数变量,将前缀、后缀和 null 传递给这个其他方法,并返回其他方法的返回值。
静态文件 createTempFile (字符串前缀,字符串后缀,文件目录)使用给定的前缀和后缀在指定的目录中创建一个空文件,并生成其名称。名称以前缀指定的字符序列开始,以后缀指定的字符序列结束;。当后缀为 null 时,tmp 用作后缀。该方法成功时返回创建的文件的路径名。当前缀包含的字符少于三个时,抛出 Java . lang . illegalargumentexception,当文件无法创建时,抛出 IOException 。
布尔删除()删除由这个文件对象的路径名表示的文件或目录。成功时返回 true 否则,返回 false。如果路径名表示一个目录,则该目录必须为空才能删除。
void deleteonexis()当虚拟机终止时,请求删除由该文件对象的抽象路径名表示的文件或目录。在同一个文件对象上重新调用该方法没有效果。一旦请求删除,就不可能取消请求。因此,应谨慎使用这种方法。
布尔 mkdir()创建由这个文件对象的抽象路径名命名的目录。成功时返回 true 否则,返回 false。
布尔 mkdirs()创建由这个文件对象的抽象路径名命名的目录和任何必要的中间目录。成功时返回 true 否则,返回 false。
布尔重命名 (文件目标)将由这个文件对象的抽象路径名表示的文件重命名为目标。成功时返回 true 否则,返回 false。当 dest 为 null 时,该方法抛出 NullPointerException 。此方法行为的许多方面都依赖于平台。例如,重命名操作可能无法将文件从一个文件系统移动到另一个文件系统,该操作可能不是原子的,或者当具有目标路径名的文件已经存在时,该操作可能不会成功。应该始终检查返回值,以确保重命名操作成功。
布尔 setlastmedict(长时间)设置由该文件对象的抽象路径名命名的文件或目录的最后修改时间。成功时返回 true 否则,返回 false。当时间为负时,该方法抛出 IllegalArgumentException 。所有平台都支持精确到秒的文件修改时间,但有些平台提供的精度更高。时间值将被截断以适应支持的精度。如果操作成功并且没有对文件进行干预操作,那么下一次调用 lastModified() 将返回传递给该方法的(可能被截断的) time 值。
布尔 setReadOnly()标记由这个文件对象的抽象路径名表示的文件或目录,以便只允许读操作。调用此方法后,文件或目录在被删除或标记为允许写访问之前不会改变。只读文件或目录是否可以删除取决于文件系统。

假设您正在设计一个文本编辑器应用,用户将实现它来打开一个文本文件并对其内容进行更改。在用户将这些更改显式保存到文件之前,您希望文本文件保持不变。

因为用户不想在应用崩溃或计算机断电时丢失这些更改,所以您将应用设计为每隔几分钟将这些更改保存到一个临时文件中。这样,用户就有了更改的备份。

你可以使用重载的 createTempFile() 方法来创建临时文件。如果您没有指定存储该文件的目录,那么它将创建在由 java.io.tmpdir 系统属性标识的目录中。

在用户告诉应用保存或放弃更改后,您可能希望删除临时文件。 deleteOnExit() 方法让你注册一个临时文件用于删除;当虚拟机在没有崩溃/断电的情况下结束时,它将被删除。

清单 11-5 展示了一个 TempFileDemo 应用,用于试验 createTempFile() 和 deleteOnExit() 方法。

清单 11-5 。试用临时文件

import java.io.File;
import java.io.IOException;

public class TempFileDemo
{
   public static void main(String[] args) throws IOException
   {
      System.out.println(System.getProperty("java.io.tmpdir"));
      File temp = File.createTempFile("text", ".txt");
      System.out.println(temp);
      temp.deleteOnExit();
   }
}

在输出临时文件存储的位置后, TempFileDemo 创建一个临时文件,文件名以文本开头,以结尾。txt 分机。 TempFileDemo 接下来输出临时文件的名称,并注册临时文件,以便在应用成功终止时删除。

在运行 TempFileDemo 的过程中,我观察到以下输出(文件在退出时消失):

C:\Users\Owner\AppData\Local\Temp\
C:\Users\Owner\AppData\Local\Temp\text3173127870811188221.txt

Java 6 添加到文件新增布尔可执行(boolean executable) 、布尔可执行(boolean executable,boolean ownerOnly) 、布尔可读取(boolean readable) 、布尔可读取(boolean readable,boolean ownerOnly) 、布尔可写入(boolean writable) 和布尔可写入(boolean writable,boolean ownerOnly) Android 支持这些额外的方法。

最后,文件实现了 java.lang.Comparable 接口的 compareTo() 方法,并覆盖了 equals() 和 hashCode() 。表 11-5 描述了这些其他方法。

表 11-5 。文件的其他方法

方法描述
int compareTo (文件路径名)按字典顺序比较两个路径名。此方法定义的顺序取决于基础平台。在 Unix/Linux 平台上,比较路径名时字母大小写很重要;在 Windows 平台上,字母大小写无关紧要。当路径名的抽象路径名等于该文件对象的抽象路径名时返回零,当该文件对象的抽象路径名小于路径名时返回负值,否则返回正值。为了准确地比较两个文件对象,在每个文件对象上调用 getCanonicalFile() ,然后比较返回的文件对象。
布尔等于 (对象 obj)比较这个文件对象和对象是否相等。抽象路径名相等依赖于底层平台。在 Unix/Linux 平台上,比较路径名时字母大小写很重要;在 Windows 平台上,字母大小写无关紧要。当且仅当对象不是 null 并且是一个文件对象,其抽象路径名表示与这个文件对象的抽象路径名相同的文件/目录时,返回 true。
int hashCode()计算并返回该路径名的散列码。这种计算取决于底层平台。在 Unix/Linux 平台上,路径名的散列码等于其路径名字符串的散列码和十进制值 1234321 的异或。在 Windows 平台上,哈希代码是小写路径名字符串的哈希代码和十进制值 1234321 的异或。当路径名字符串小写时,不考虑当前的语言环境(地理、政治或文化区域)。

使用 RandomAccessFile API

可以为随机访问创建和/或打开文件,在随机访问中,可以混合进行写和读操作,直到文件被关闭。Java 通过其具体的 java.io.RandomAccessFile 类支持这种随机访问。

RandomAccessFile 声明了以下构造函数:

  • RandomAccessFile(File file,String mode ) 创建并打开一个不存在的新文件或打开一个已有的文件。该文件由文件的抽象路径名标识,并根据模式创建和/或打开。
  • RandomAccessFile(字符串路径名,字符串模式 ) 创建并打开一个不存在的新文件或打开一个现有文件。文件由路径名标识,并根据模式创建和/或打开。

构造函数的模式参数必须是【r】【rw】【rws】或【rwd】中的一个;否则,构造函数抛出 IllegalArgumentException。这些字符串文字具有以下含义:

  • 【r】通知构造器以只读方式打开一个已有的文件。任何写入文件的尝试都会导致抛出一个 IOException 类的实例。
  • 【rw】当文件不存在时,通知构造器创建并打开一个新文件进行读写,或者打开一个已有的文件进行读写。
  • 【rwd】当文件不存在时,通知构造器创建并打开一个新文件进行读写,或者打开一个已有的文件进行读写。此外,对文件内容的每次更新都必须同步写入底层存储设备。
  • 【rws】通知构造器创建并打开一个不存在的新文件进行读写,或者打开一个已有的文件进行读写。此外,对文件内容或元数据的每次更新都必须同步写入底层存储设备。

注意文件的元数据 是关于文件的数据,而不是实际的文件内容。元数据的例子包括文件的长度和文件最后修改的时间。

【rwd】和【rws】模式确保对位于本地存储设备上的文件的任何写入都被写入该设备,这保证了当操作系统崩溃时关键数据不会丢失。当文件不在本地设备上时,不做任何保证。

注意在【rwd】或【rws】模式下打开的随机存取文件的操作比在【rw】模式下打开的随机存取文件的操作慢。

当模式为【r】且路径名标识的文件无法打开(可能不存在,也可能是目录)或模式为【rw】且路径名为只读或目录时,这些构造函数抛出 FileNotFoundException 。

下面的示例演示了第二个构造函数,它试图通过 "r" 模式字符串打开一个现有文件进行读访问:

RandomAccessFile raf = new RandomAccessFile("employee.dat", "r");

随机存取文件与一个文件指针 相关联,该指针标识下一个要写入或读取的字节的位置。当打开一个现有文件时,文件指针被设置为它的第一个字节,偏移量为 0。创建文件时,文件指针也被设置为 0。

写入或读取操作从文件指针开始,并使其前进超过写入或读取的字节数。超过文件当前结尾的写入操作会导致文件被扩展。这些操作会一直持续到文件关闭。

RandomAccessFile 声明了各种各样的方法。我在表 11-6 中给出了这些方法的一个典型例子。

表 11-6 。RandomAccessFile 方法

方法描述
虚空关()关闭该文件并释放所有相关的平台资源。后续的写或读操作导致 IOException 。同样,不能用这个随机访问文件对象重新打开文件。当一个 I/O 错误发生时,这个方法抛出 IOException 。
文件描述符 getFD()返回文件的相关文件描述符对象。当发生 I/O 错误时,该方法抛出 IOException 。
长 getFilePointer()将文件指针的当前从零开始的字节偏移量返回到文件中。当发生 I/O 错误时,该方法抛出 IOException 。
龙长()返回文件的长度(以字节为单位)。当发生 I/O 错误时,该方法抛出 IOException 。
int read()读取并返回(作为 0 到 255 范围内的 int )文件的下一个字节,或者在到达文件末尾时返回 1。该方法在没有输入可用时阻塞,并在发生 I/O 错误时抛出 IOException 。
int read(byte[] b)将文件中最多 b.length 字节的数据读入字节数组 b 。此方法会一直阻塞,直到至少有 1 个字节的输入可用。它返回读入数组的字节数,或者在到达文件末尾时返回 1。当 b 为 null 时抛出 NullPointerException ,当发生 I/O 错误时抛出 IOException 。
char readChar()从文件中读取并返回一个字符。这个方法从当前文件指针开始从文件中读取 2 个字节。如果读取的字节依次为 b1 和 b2 ,其中 0 < = b1 , b2 < = 255,则结果等于(char)((B1<<8)| B2)。此方法会一直阻塞,直到读取了 2 个字节、检测到文件的结尾或者引发异常。在读取两个字节之前到达文件末尾时,它抛出 java.io.EOFException (是 IOException 的子类),在发生 I/O 错误时,抛出 IOException 。
int readInt()从文件中读取并返回一个 32 位整数。这个方法从当前文件指针开始从文件中读取 4 个字节。如果读取的字节依次为 b1 、 b2 、 b3 、 b4 ,其中 0 < = b1 、 b2 、 b3 、 b4 < = 255,则结果等于(B1<<24)|(B2<)此方法会一直阻塞,直到读取了 4 个字节、检测到文件的结尾或引发异常。在读取 4 个字节之前,当到达文件末尾时抛出 EOFException ,当发生 I/O 错误时抛出 IOException 。
void seek(长位置)将文件指针的当前偏移量设置为位置(从文件的开始以字节为单位测量)。如果设置的偏移量超出了文件的结尾,文件的长度不会改变。文件长度将仅在偏移被设置为超出文件结尾之后通过写入来改变。当位置中的值为负或发生 I/O 错误时,该方法抛出 IOException 。
void set length(long new length)??]设置文件的长度。如果由 length() 返回的当前长度大于 newLength ,则文件被截断。在这种情况下,如果 getFilePointer() 返回的文件偏移量大于 newLength ,那么在 setLength() 返回后,偏移量将等于 newLength 。如果当前长度小于 newLength ,则文件被扩展。在这种情况下,文件扩展部分的内容没有定义。当发生 I/O 错误时,该方法抛出 IOException 。
int skip bytes(int n)尝试跳过 n 个字节。当在跳过 n 字节之前到达文件末尾时,该方法跳过较少的字节(可能为零)。这种情况下不会抛出 EOFException 。如果 n 为负,则不跳过任何字节。返回跳过的实际字节数。当一个 I/O 错误发生时,这个方法抛出 IOException 。
void write(byte[] b)从当前文件指针位置开始,将字节数组 b 中的 b.length 字节写入文件。当一个 I/O 错误发生时,这个方法抛出 IOException 。
void write(int b)将 b 的低 8 位写入当前文件指针位置的文件。当一个 I/O 错误发生时,这个方法抛出 IOException 。
void writeChars(字符串 s)从当前文件指针位置开始,将字符串 s 作为字符序列写入文件。当一个 I/O 错误发生时,这个方法抛出 IOException 。
void writeInt(int i)从当前文件指针位置开始,将 32 位整数 i 写入文件。这 4 个字节首先写入高位字节。当一个 I/O 错误发生时,这个方法抛出 IOException 。

大多数表 11-6 的方法都是不言自明的。然而, getFD() 方法需要进一步的启发。

注意 RandomAccessFile 的 read 前缀方法和 skipBytes() 源自 java.io.DataInput 接口,这个类实现了这个接口。此外, RandomAccessFile 的 write 前缀方法源自 java.io.DataOutput 接口,该类也实现了该接口。

当打开一个文件时,底层平台创建一个依赖于平台的结构来表示该文件。该结构的句柄存储在 java.io.FileDescriptor 类的实例中,该类由 getFD() 返回。

注意句柄是一个标识符,在这种情况下,当 Java 需要底层平台执行文件操作时,它会传递给底层平台以标识特定的打开文件。

FileDescriptor 是一个小类,声明了三个 FileDescriptor 常量,分别命名为中的、中的和中的错误。这些常量让 System.in 、 System.out 和 System.err 提供对标准输入、标准输出和标准错误流的访问。

FileDescriptor 也声明了下面一对方法:

  • void sync() 告诉底层平台将打开文件的输出缓冲区的内容刷新(清空)到它们相关的本地磁盘设备。 sync() 在所有修改的数据和属性都写入相关设备后返回。当缓冲区无法刷新或因为平台无法保证所有缓冲区都已与物理介质同步时,它会抛出 Java . io . syncfailedexception。
  • 布尔有效() 确定这个文件描述符对象是否有效。当文件描述符对象代表一个打开的文件或其他活动的 I/O 连接时,它返回 true 否则,它返回 false。

写入打开文件的数据最终被存储在底层平台的输出缓冲区中。当缓冲区填满时,平台会将它们清空到磁盘。缓冲区可以提高性能,因为磁盘访问速度很慢。

但是,当您向通过模式“rwd”或“rws”打开的随机存取文件写入数据时,每次写入操作的数据都会直接写入磁盘。因此,写操作比在“rw”模式下打开随机存取文件时要慢。

假设您有这样一种情况,既通过输出缓冲区写入数据,又直接将数据写入磁盘。下面的例子通过以模式“rw”打开文件并选择性地调用文件描述符的 sync() 方法来解决这个混合场景。

RandomAccessFile raf = new RandomAccessFile("employee.dat", "rw");
FileDescriptor fd = raf.getFD();
// Perform a critical write operation.
raf.write(. . .);
// Synchronize with underlying disk by flushing platform's output buffers to disk.
fd.sync();
// Perform non-critical write operation where synchronization isn't necessary.
raf.write(. . .);
// Do other work.
// Close file, emptying output buffers to disk.
raf.close();

RandomAccessFile 对于创建一个平面文件数据库 很有用,一个组织成记录和字段的单个文件。记录存储单个条目(例如,零件数据库中的零件),而字段存储条目的单个属性(例如,零件号)。

注意术语字段也用来指一个类中声明的变量。为了避免与这种重载术语混淆,可以将字段变量想象成类似于记录的字段属性。

平面文件数据库通常将其内容组织成一系列固定长度的记录。每个记录被进一步组织成一个或多个固定长度的字段。图 11-1 说明了零件数据库中的这一概念。

9781430257226_Fig11-01.jpg

图 11-1T3。汽车零件的平面文件数据库分为记录和字段

根据图 11-1 ,每个字段都有一个名称(零件号、desc、数量和成本)。此外,每个记录被分配一个从 0 开始的数字。这个例子由五条记录组成,为了简洁起见,只显示了其中的三条。

为了向您展示如何根据 RandomAccessFile 实现平面文件数据库,我创建了一个简单的 PartsDB 类来模拟图 11-1 。查看清单 11-6 。

清单 11-6 。实现零件平面文件数据库

import java.io.IOException;
import java.io.RandomAccessFile;

public class PartsDB
{
   public final static int PNUMLEN = 20;
   public final static int DESCLEN = 30;
   public final static int QUANLEN = 4;
   public final static int COSTLEN = 4;

   private final static int RECLEN = 2 * PNUMLEN + 2 * DESCLEN + QUANLEN + COSTLEN;
   private RandomAccessFile raf;

   public PartsDB(String pathname) throws IOException
   {
      raf = new RandomAccessFile(pathname, "rw");
   }

   public void append(String partnum, String partdesc, int qty, int ucost)
      throws IOException
   {
      raf.seek(raf.length());
      write(partnum, partdesc, qty, ucost);
   }

   public void close()
   {
      try
      {
         raf.close();
      }
      catch (IOException ioe)
      {
         System.err.println(ioe);
      }
   }

   public int numRecs() throws IOException
   {
      return (int) raf.length() / RECLEN;
   }

   public Part select(int recno) throws IOException
   {
      if (recno < 0 || recno >= numRecs())
         throw new IllegalArgumentException(recno + " out of range");
      raf.seek(recno * RECLEN);
      return read();
   }

   public void update(int recno, String partnum, String partdesc, int qty,
                      int ucost) throws IOException
   {
      if (recno < 0 || recno >= numRecs())
         throw new IllegalArgumentException(recno + " out of range");
      raf.seek(recno * RECLEN);
      write(partnum, partdesc, qty, ucost);
   }

   private Part read() throws IOException
   {
      StringBuffer sb = new StringBuffer();
      for (int i = 0; i < PNUMLEN; i++)
         sb.append(raf.readChar());
      String partnum = sb.toString().trim();
      sb.setLength(0);
      for (int i = 0; i < DESCLEN; i++)
         sb.append(raf.readChar());
      String partdesc = sb.toString().trim();
      int qty = raf.readInt();
      int ucost = raf.readInt();
      return new Part(partnum, partdesc, qty, ucost);
   }

   private void write(String partnum, String partdesc, int qty, int ucost)
      throws IOException
   {
      StringBuffer sb = new StringBuffer(partnum);
      if (sb.length() > PNUMLEN)
         sb.setLength(PNUMLEN);
      else
      if (sb.length() < PNUMLEN)
      {
         int len = PNUMLEN - sb.length();
         for (int i = 0; i < len; i++)
            sb.append(" ");
      }
      raf.writeChars(sb.toString());
      sb = new StringBuffer(partdesc);
      if (sb.length() > DESCLEN)
         sb.setLength(DESCLEN);
      else
      if (sb.length() < DESCLEN)
      {
         int len = DESCLEN - sb.length();
         for (int i = 0; i < len; i++)
            sb.append(" ");
      }
      raf.writeChars(sb.toString());
      raf.writeInt(qty);
      raf.writeInt(ucost);
   }

   public static class Part
   {
      private String partnum;
      private String desc;
      private int qty;
      private int ucost;

      public Part(String partnum, String desc, int qty, int ucost)
      {
         this.partnum = partnum;
         this.desc = desc;
         this.qty = qty;
         this.ucost = ucost;
      }

      String getDesc()
      {
         return desc;
      }

      String getPartnum()
      {
         return partnum;
      }

      int getQty()
      {
         return qty;
      }

      int getUnitCost()
      {
         return ucost;
      }
   }
}

PartsDB 首先声明标识字符串和 32 位整数字段长度的常数。然后,它声明一个常数,以字节为单位计算记录长度。该计算考虑了一个字符在文件中占用 2 个字节的事实。

这些常数后面是一个名为 raf 的字段,该字段的类型为 RandomAccessFile 。这个字段在后续的构造函数中被赋予了一个 RandomAccessFile 类的实例,由于“rw”,这个类创建/打开一个新文件或者打开一个已有的文件。

PartsDB 接下来声明 append() 、 close() 、 numRecs() 、 select() 、 update() 。这些方法将记录追加到文件中,关闭文件,返回文件中的记录数,选择并返回特定记录,以及更新特定记录:

  • 追加()方法 首先调用长度()和查找()。这样做可以确保在调用私有的 write() 方法来写入包含该方法参数的记录之前,文件指针被定位到文件的末尾。
  • RandomAccessFile 的 close() 方法可以抛出 IOException 。因为这种情况很少发生,所以我选择在 PartDB 的 close() 方法中处理这个异常,这样可以保持该方法的签名简单。但是,当 IOException 发生时,我会打印一条消息。
  • 方法返回文件中记录的数量。这些记录从 0 开始编号,以 num RECs()–1 结束。每个 select() 和 update() 方法验证其 recno 参数是否在此范围内。
  • select() 方法调用私有的 read() 方法返回由 recno 标识的记录,作为嵌套的 Part 类的实例。 Part 的构造函数将 Part 对象初始化为记录的字段值,其 getter 方法返回这些值。
  • update() 方法同样简单。与 select() 一样,它首先将文件指针定位到由 recno 标识的记录的开头。与 append() 一样,它调用 write() 写出它的参数,但是替换一个记录而不是添加一个记录。

记录是用私有的 write() 方法写的。因为字段必须有精确的大小, write() 用右边的空格填充比字段大小短的基于字符串的值,并在需要时将这些值截断为字段大小。

通过私有的 read() 方法读取记录。 read() 在保存部分对象中基于字符串的字段值之前删除填充。

单独来看, PartsDB 是没用的。你需要一个应用让你试验这个类,而清单 11-7 满足了这个需求。

清单 11-7 。试验零件平面文件数据库

import java.io.IOException;

public class UsePartsDB
{
   public static void main(String[] args)
   {
      PartsDB pdb = null;
      try
      {
         pdb = new PartsDB("parts.db");
         if (pdb.numRecs() == 0)
         {
            // Populate the database with records.
            pdb.append("1-9009-3323-4x", "Wiper Blade Micro Edge", 30, 2468);
            pdb.append("1-3233-44923-7j", "Parking Brake Cable", 5, 1439);
            pdb.append("2-3399-6693-2m", "Halogen Bulb H4 55/60W", 22, 813);
            pdb.append("2-599-2029-6k", "Turbo Oil Line O-Ring ", 26, 155);
            pdb.append("3-1299-3299-9u", "Air Pump Electric", 9, 20200);
         }
         dumpRecords(pdb);
         pdb.update(1, "1-3233-44923-7j", "Parking Brake Cable", 5, 1995);
         dumpRecords(pdb);
      }
      catch (IOException ioe)
      {
         System.err.println(ioe);
      }
      finally
      {
         if (pdb != null)
            pdb.close();
      }
   }

   static void dumpRecords(PartsDB pdb) throws IOException
   {
      for (int i = 0; i < pdb.numRecs(); i++)
      {
         PartsDB.Part part = pdb.select(i);
         System.out.print(format(part.getPartnum(), PartsDB.PNUMLEN, true));
         System.out.print(" | ");
         System.out.print(format(part.getDesc(), PartsDB.DESCLEN, true));
         System.out.print(" | ");
         System.out.print(format("" + part.getQty(), 10, false));
         System.out.print(" | ");
         String s = part.getUnitCost() / 100 + "." + part.getUnitCost() % 100;
         if (s.charAt(s.length() - 2) == '.') s += "0";
         System.out.println(format(s, 10, false));
      }
      System.out.println("Number of records = " + pdb.numRecs());
      System.out.println();
   }

   static String format(String value, int maxWidth, boolean leftAlign)
   {
      StringBuffer sb = new StringBuffer();
      int len = value.length();
      if (len > maxWidth)
      {
         len = maxWidth;
         value = value.substring(0, len);
      }
      if (leftAlign)
      {
         sb.append(value);
         for (int i = 0; i < maxWidth-len; i++)
            sb.append(" ");
      }
      else
      {
         for (int i = 0; i < maxWidth-len; i++)
            sb.append(" ");
         sb.append(value);
      }
      return sb.toString();
   }
}

清单 11-7 的 main() 方法从实例化 PartsDB 开始,用 parts.db 作为数据库文件的名称。当这个文件没有记录时, numRecs() 返回 0,并且通过 append() 方法将几个记录追加到文件中。

main() 接下来将存储在 parts.db 中的五条记录转储到标准输出流中,更新编号为 1 的记录中的单位成本,再次将这些记录转储到标准输出流中以显示这一变化,并关闭数据库。

注意我将单位成本值存储为基于整数的便士数量。例如,我指定文字 1995 来表示 1995 年的便士,即 19.95 美元。如果我要使用 java.math.BigDecimal 对象来存储货币值,我将不得不重构 PartsDB 来利用对象序列化,但我现在还不准备这么做。(我将在本章后面讨论对象序列化。)

main() 依靠一个 dumpRecords() 助手方法来转储这些记录,而 dumpRecords() 依靠一个 format() 助手方法来格式化字段值,以便它们可以在正确对齐的列中显示——我本可以使用 java.util.Formatter (参见第十章)来代替。以下输出揭示了这种一致性:

1-9009-3323-4x       | Wiper Blade Micro Edge         |         30 |      24.68
1-3233-44923-7j      | Parking Brake Cable            |          5 |      14.39
2-3399-6693-2m       | Halogen Bulb H4 55/60W         |         22 |       8.13
2-599-2029-6k        | Turbo Oil Line O-Ring          |         26 |       1.55
3-1299-3299-9u       | Air Pump Electric              |          9 |     202.00
Number of records = 5

1-9009-3323-4x       | Wiper Blade Micro Edge         |         30 |      24.68
1-3233-44923-7j      | Parking Brake Cable            |          5 |      19.95
2-3399-6693-2m       | Halogen Bulb H4 55/60W         |         22 |       8.13
2-599-2029-6k        | Turbo Oil Line O-Ring          |         26 |       1.55
3-1299-3299-9u       | Air Pump Electric              |          9 |     202.00
Number of records = 5

这就是:一个简单的平面文件数据库。尽管缺乏对索引和事务管理等高级数据库特性的支持,平面文件数据库可能就是您的 Android 应用所需要的全部。

注意要了解更多关于平面文件数据库的信息,请查看维基百科的“平面文件数据库”条目(【en.wikipedia.org/wiki/Flat_f…

使用流

连同文件和随机访问文件,Java 使用流来执行 I/O 操作。是任意长度的有序字节序列。字节通过输出流 从应用流向目的地,并通过输入流 从源流向应用。图 11-2 说明了这些流程。

9781430257226_Fig11-02.jpg

图 11-2T3。将输出和输入流概念化为字节流

注意 Java 对这个词的使用类似于“水流”、“电子流”等等。

Java 识别各种流目的地;比如字节数组,文件,屏幕,套接字(网络端点),线程管道。Java 也能识别各种流源。例子包括字节数组、文件、键盘、套接字和线程管道。(我将在第十二章中讨论套接字。)

流类概述

java.io 包提供了几个输出流和输入流类,它们是抽象的输出流和输入流类的后代。图 11-3 揭示了输出流类的层次。

9781430257226_Fig11-03.jpg

图 11-3T3。除 PrintStream 之外的所有输出流类都由它们的 output stream 后缀表示

图 11-4 揭示了输入流类的层次。

9781430257226_Fig11-04.jpg

图 11-4T3。不推荐使用 LineNumberInputStream 和 StringBufferInputStream

LineNumberInputStream 和 StringBufferInputStream 已经被弃用,因为它们不支持不同的字符编码,我将在本章后面讨论这个主题。 LineNumberReader 和 StringReader 是它们的替代品。(我将在本章后面讨论读者。)

注意 PrintStream 是另一个不推荐使用的类,因为它不支持不同的字符编码;版画家是它的替代者。然而,令人怀疑的是 Oracle(和 Google)会反对这个类,因为 PrintStream 是 java.lang.System 类的 out 和 err 类字段的类型,太多的遗留代码依赖于这个事实。

其他 Java 包 提供了额外的输出流和输入流类。例如, java.util.zip 提供了四个将未压缩的数据压缩成各种格式的输出流类和四个匹配的输入流类 将压缩的数据从相同的格式解压缩:

  • 检控流体
  • 支票输入流
  • 放气输出流
  • gzip poutput stream
  • gzip putstream
  • 充气输入流
  • zip output stream
  • zipinput stream

另外, java.util.jar 包提供了一对流类,用于向 jar 文件写入内容和从 JAR 文件读取内容 ??:

  • jaroutput stream
  • JarInputStream

在接下来的几节中,我将带您浏览一下大多数的 java.io 的输出流和输入流类,从输出流和输入流开始。

输出流和输入流

Java 提供了用于执行流 I/O 的 OutputStream 和 InputStream 类, OutputStream 是所有 OutputStream 子类的超类。表 11-7 描述了输出流的方法。

表 11-7 。输出流方法

方法描述
虚空关()关闭此输出流,并释放与该流关联的任何平台资源。当发生 I/O 错误时,该方法抛出 IOException 。
虚空冲()通过将任何缓冲的输出字节写入目标来刷新该输出流。如果该输出流的预期目的地是由底层平台提供的抽象(例如,文件),则刷新该流仅保证先前写入该流的字节被传递到底层平台进行写入;它不能保证它们实际上被写入到物理设备,如磁盘驱动器。当发生 I/O 错误时,该方法抛出 IOException 。
void write(byte[] b)将字节数组 b 中的 b.length 字节写入该输出流。一般来说, write(b) 的行为就像您指定了 write(b,0,b.length) 一样。当 b 为 null 时,该方法抛出 NullPointerException ,当发生 I/O 错误时,该方法抛出 IOException 。
void write(byte[] b,int off,int len)从偏移量 off 开始,将字节数组 b 中的 len 字节写入该输出流。当 b 为 null 时,该方法抛出 NullPointerException;Java . lang . indexoutofboundsexception 当 off 为负, len 为负,或者 off + len 大于 b.length 时;以及发生 I/O 错误时的 IOException 。
void write(int b)将字节 b 写入该输出流。仅写入 8 个低阶位;24 个高位被忽略。当一个 I/O 错误发生时,这个方法抛出 IOException 。

在需要经常保存更改的长时间运行的应用中, flush() 方法非常有用,例如,前面提到的每隔几分钟就将更改保存到临时文件的文本编辑器应用。记住 flush() 只向平台刷新字节;这样做不一定会导致平台将这些字节刷新到磁盘。

注意close()方法自动刷新输出流。如果应用在调用 close() 之前结束,输出流将自动关闭,其数据将被刷新。

InputStream 是所有输入流子类的超类。表 11-8 描述了 InputStream 的方法。

表 11-8。 InputStream 方法

方法描述
int available()返回在不阻塞调用线程的情况下,通过下一个 read() 方法调用(或通过 skip() 跳过)可以从该输入流中读取的字节数的估计值。当一个 I/O 错误发生时,这个方法抛出 IOException 。使用这个方法的返回值来分配一个缓冲区来保存流的所有数据是不正确的,因为子类可能不会返回流的总大小。
虚空关()关闭此输入流,并释放与该流关联的任何平台资源。当发生 I/O 错误时,该方法抛出 IOException 。
void mark(int read limit)标记此输入流中的当前位置。对 reset() 的后续调用将该流重新定位到最后标记的位置,以便后续读取操作重新读取相同的字节。 readlimit 参数告诉这个输入流在使这个标记无效之前允许读取那么多字节(这样流就不能被重置到标记的位置)。
布尔 markSupported()当该输入流支持 mark() 和 reset() 时返回 true 否则,返回 false。
int read()读取并返回该输入流的下一个字节(作为 0 到 255 范围内的 int ),或者在到达流的末尾时返回 1。此方法会一直阻塞,直到输入可用、检测到流的结尾或引发异常。当一个 I/O 错误发生时,它抛出 IOException 。
int read(byte[] b)从这个输入流中读取一些字节,并将它们存储在字节数组 b 中。返回实际读取的字节数(可能小于 b 的长度,但绝不会超过这个长度),或者在到达流的末尾时返回 1(没有字节可供读取)。此方法会一直阻塞,直到输入可用、检测到流的结尾或引发异常。当 b 为 null 时抛出 NullPointerException ,当发生 I/O 错误时抛出 IOException 。
int read(byte[] b,int off,int len)从该输入流中读取不超过 len 个字节,并将它们存储在字节数组 b 中,从 off 指定的偏移量开始。返回实际读取的字节数(可能小于 len ,但绝不会大于 len ),或者在到达流的末尾时返回 1(没有可读取的字节)。此方法会一直阻塞,直到输入可用、检测到流的结尾或引发异常。当 b 为 null 时,抛出 NullPointerException;IndexOutOfBoundsException 当 off 为负, len 为负,或者 len 大于 b . length-off;以及发生 I/O 错误时的 IOException 。
void 复位()将此输入流重新定位到最后一次调用 mark() 时的位置。当这个输入流没有被标记或者标记已经失效时,这个方法抛出 IOException 。
长跳过(长 n)跳过并丢弃来自该输入流的 n 字节的数据。例如,当在跳过 n 字节之前到达文件末尾时,该方法可能会跳过一些较小的字节(可能为零)。返回跳过的实际字节数。当 n 为负时,没有字节被跳过。当这个输入流不支持跳转或者发生其他 I/O 错误时,这个方法抛出 IOException 。

InputStream 子类如 bytearrayiputstream 支持通过 mark() 方法标记输入流中的当前读取位置,稍后通过 reset() 方法返回到该位置。

注意不要忘记调用 markSupported() 来查明子类是否支持 mark() 和 reset() 。

ByteArrayOutputStream 和 ByteArrayInputStream

字节数组通常用作流的目的地和源。ByteArrayOutputStream 类允许您将字节流写入字节数组;bytearrayiputstream 类让你从一个字节数组中读取一个字节流。

ByteArrayOutputStream 声明了两个构造函数。每个构造函数用内部字节数组创建一个字节数组输出流;通过调用的的 byte[] toByteArray() 方法,可以返回该数组的副本:

  • ByteArrayOutputStream() 用初始大小为 32 字节的内部字节数组创建一个字节数组输出流。这个数组会根据需要增长。
  • ByteArrayOutputStream(int size)用内部字节数组创建一个字节数组输出流,其初始大小由 size 指定,并根据需要增长。当大小小于零时,该构造函数抛出 IllegalArgumentException 。

以下示例使用 ByteArrayOutputStream() 创建一个字节数组输出流,其内部字节数组设置为默认大小:

ByteArrayOutputStream baos = new ByteArrayOutputStream();

ByteArrayInputStream 也声明了一对构造函数。每个构造函数基于指定的字节数组创建一个字节数组输入流,并跟踪要从数组中读取的下一个字节以及要读取的字节数:

  • bytearrainputstream(byte[]ba)创建一个使用 ba 作为其字节数组的字节数组输入流( ba 直接使用;不会创建副本)。位置设置为 0,要读取的字节数设置为 ba.length 。
  • ByteArrayInputStream(byte[]ba,int offset,int count) 创建一个字节数组输入流,它使用 ba 作为它的字节数组(不进行复制)。位置被设置为偏移,要读取的字节数被设置为计数。

以下示例使用 bytearrayiputstream(byte[])创建一个字节数组输入流,其源是前一个字节数组输出流的字节数组的副本:

ByteArrayInputStream bais = new ByteArrayInputStream(baos.toByteArray());

ByteArrayOutputStream 和 ByteArrayInputStream 在您需要将图像转换为字节数组,以某种方式处理这些字节,然后将字节转换回图像的场景中非常有用。

例如,假设您正在编写一个基于 Android 的图像处理应用。您将包含图像的文件解码为特定于 Android 的 android.graphics.BitMap 实例,将该实例压缩为 ByteArrayOutputStream 实例,获取字节数组输出流的数组的副本,以某种方式处理该数组,将该数组转换为 bytearrayiputstream 实例,并使用字节数组输入流将这些字节解码为另一个 BitMap 实例,如下所示:

String pathname = . . . ; // Assume a legitimate pathname to an image.
Bitmap bm = BitmapFactory.decodeFile(pathname);
ByteArrayOutputStream baos = new ByteArrayOutputStream();
if (bm.compress(Bitmap.CompressFormat.PNG, 100, baos))
{
   byte[] imageBytes = baos.toByteArray();
   // Do something with imageBytes.
   bm = BitMapFactory.decodeStream(new ByteArrayInputStream(imageBytes));
}

这个例子获取一个图像文件的路径名,然后调用具体的 Android . graphics . Bitmap factory 类的 Bitmap decover(String pathname)类方法。该方法将由路径名标识的图像文件解码成位图,并返回代表该位图的位图实例。

在创建了一个 ByteArrayOutputStream 对象后,该示例使用返回的位图实例来调用位图的布尔压缩(BitMap。CompressFormat format,int quality,OutputStream stream) 将位图的压缩版本写入字节数组输出流的方法:

  • 格式标识压缩图像的格式。我选择使用流行的可移植网络图形(PNG)格式。
  • 质量提示压缩机需要多少压缩量。该值的范围从 0 到 100,其中 0 表示以牺牲质量为代价的最大压缩,100 表示以牺牲压缩为代价的最大质量。像 PNG 这样的格式忽略了质量,因为它们采用无损压缩。
  • stream 标识在其上写入压缩图像数据的流。

当 compress() 返回 true,这意味着它成功地将图像压缩到 PNG 格式的字节数组输出流中,调用 ByteArrayOutputStream 对象的 toByteArray() 方法创建并返回一个包含图像字节的字节数组。

继续,处理数组,创建一个 bytearrayiputstream 对象,将处理后的字节作为该流的源,调用 BitmapFactory 的 BitMap decode stream(InputStream is)类方法将字节数组输入流的字节源转换为 BitMap 实例。

FileOutputStream 和 FileInputStream

文件是常见的流目的地和源。具体的 FileOutputStream 类允许您将字节流写入文件;具体的 FileInputStream 类让你从文件中读取字节流。

FileOutputStream 子类 OutputStream 并声明了五个构造函数用于创建文件输出流。例如, FileOutputStream(字符串名称)创建一个文件输出流到由名称标识的现有文件。当文件不存在且无法创建时,该构造函数抛出 FileNotFoundException ,它是一个目录而不是一个普通文件,或者有其他原因导致文件无法打开输出。

以下示例使用 FileOutputStream(字符串路径名)创建一个以 employee.dat 为目标的文件输出流:

FileOutputStream fos = new FileOutputStream("employee.dat");

提示 FileOutputStream(字符串名)覆盖现有文件。要追加数据而不是覆盖现有内容,请调用一个 FileOutputStream 构造函数,该函数包含一个布尔追加参数,并将 true 传递给该参数。

FileInputStream 子类 InputStream 并声明了三个用于创建文件输入流的构造函数。例如, FileInputStream(字符串名称)从由名称标识的现有文件创建一个文件输入流。当文件不存在,它是一个目录而不是一个普通的文件,或者有其他原因导致文件不能打开输入时,这个构造函数抛出 FileNotFoundException 。

以下示例使用 file inputstream(String name)创建一个文件输入流,并将 employee.dat 作为其源:

FileInputStream fis = new FileInputStream("employee.dat");

文件输出流和文件输入流在文件复制上下文中很有用。清单 11-8 将源代码呈现给一个复制应用,该应用提供了一个演示。

清单 11-8 。将源文件复制到目标文件

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

public class Copy
{
   public static void main(String[] args)
   {
      if (args.length != 2)
      {
         System.err.println("usage: java Copy srcfile dstfile");
         return;
      }
      FileInputStream fis = null;
      FileOutputStream fos = null;
      try
      {
         fis = new FileInputStream(args[0]);
         fos = new FileOutputStream(args[1]);
         int b; // I chose b instead of byte because byte is a reserved word.
         while ((b = fis.read()) != −1)
            fos.write(b);
      }
      catch (FileNotFoundException fnfe)
      {
         System.err.println(args[0] + " could not be opened for input, or " +
                            args[1] + " could not be created for output");
      }
      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
            }

         if (fos != null)
            try
            {
               fos.close();
            }
            catch (IOException ioe)
            {
               assert false; // shouldn't happen in this context
            }
      }
   }
}

清单 11-8 的 main() 方法首先验证两个命令行参数,标识源文件和目标文件的名称,是否被指定。然后,它开始实例化 FileInputStream 和 FileOutputStream ,并进入一个 while 循环,该循环反复从文件输入流读取字节,并将它们写入文件输出流。

当然有些事情可能会出错。可能源文件不存在,或者可能无法创建目标文件(例如,可能存在同名的只读文件)。在任一场景中,都会抛出 FileNotFoundException ,并且必须对其进行处理。另一种可能是在复制操作过程中发生了 I/O 错误。这样的错误导致 IOException 。

不管是否抛出异常,输入和输出流都通过 finally 块关闭。在这样一个简单的应用中,我可以忽略 close() 方法调用,让应用终止。尽管此时 Java 会自动关闭打开的文件,但在退出时显式关闭文件是一种好的方式。

因为 close() 能够抛出被检查的 IOException 类的一个实例,所以对这个方法的调用被包装在一个 try 块中,其中有一个适当的 catch 块捕获这个异常。注意每个 try 块前面的 if 语句。如果 fis 或 fos 包含空引用,该语句对于避免抛出 NullPointerException 实例是必要的。

PipedOutputStream 和 PipedInputStream

线程必须经常通信。一种方法是使用共享变量。另一种方法是通过 PipedOutputStream 和 PipedInputStream 类使用管道流。 PipedOutputStream 类让发送线程将字节流写入 PipedInputStream 类的实例,接收线程随后使用该实例读取这些字节。

注意不建议试图从一个线程中使用一个 PipedOutputStream 对象和一个 PipedInputStream 对象,因为这可能会使线程死锁。

PipedOutputStream 声明了一对用于创建管道输出流的构造函数:

  • PipedOutputStream() 创建一个尚未连接到管道输入流的管道输出流。在使用之前,它必须由接收方或发送方连接到管道输入流。
  • PipedOutputStream(PipedInputStream dest)创建一个连接到管道输入流 dest 的管道输出流。写入管道输出流的字节可以从 dest 中读取。当一个 I/O 错误发生时,这个构造函数抛出 IOException 。

PipedOutputStream 声明了一个 void connect(PipedInputStream dest)方法,将这个管道输出流连接到 dest 。当这个管道输出流已经连接到另一个管道输入流时,该方法抛出 IOException 。

PipedInputStream 声明了四个用于创建管道输入流的构造函数:

  • PipedInputStream() 创建一个尚未连接到管道输出流的管道输入流。在使用之前,它必须连接到管道输出流。
  • PipedInputStream(int pipeSize)创建一个尚未连接到管道输出流的管道输入流,并使用 pipeSize 来调整管道输入流的缓冲区大小。在使用之前,它必须连接到管道输出流。当 pipeSize 小于或等于 0 时,该构造函数抛出 IllegalArgumentException 。
  • piped inputstream(piped outputstream src)创建一个连接到管道输出流 src 的管道输入流。写入 src 的字节可以从这个管道输入流中读取。当一个 I/O 错误发生时,这个构造函数抛出 IOException 。
  • PipedInputStream(PipedOutputStream src,int pipeSize) 创建一个连接到管道输出流 src 的管道输入流,并使用 pipeSize 来调整管道输入流的缓冲区大小。写入 src 的字节可以从这个管道输入流中读取。该构造函数在发生 I/O 错误时抛出 IOException ,在 pipeSize 小于或等于 0 时抛出 IllegalArgumentException。

PipedInputStream 声明了一个 void connect(piped outputstream src)方法,该方法将这个管道输入流连接到 src 。当这个管道输入流已经连接到另一个管道输出流时,该方法抛出 IOException 。

创建一对管道流最简单的方法是在同一个线程中以任意顺序创建。例如,您可以首先创建管道输出流:

PipedOutputStream pos = new PipedOutputStream();
PipedInputStream pis = new PipedInputStream(pos);

或者,您可以首先创建管道输入流:

PipedInputStream pis = new PipedInputStream();
PipedOutputStream pos = new PipedOutputStream(pis);

您可以让这两个流保持不连接,稍后使用适当的管道流的 connect() 方法将它们彼此连接起来,如下所示:

PipedOutputStream pos = new PipedOutputStream();
PipedInputStream pis = new PipedInputStream();
// . . .
pos.connect(pis);

清单 11-9 展示了一个 PipedStreamsDemo 应用,它的发送方线程将一个随机生成的字节整数序列传送给接收方线程,接收方线程输出这个序列。

清单 11-9 。将随机生成的字节从发送方线程输送到接收方线程

import java.io.IOException;
import java.io.PipedInputStream;
import java.io.PipedOutputStream;

public class PipedStreamsDemo
{
   public static void main(String[] args) throws IOException
   {
      final PipedOutputStream pos = new PipedOutputStream();
      final PipedInputStream pis = new PipedInputStream(pos);
      Runnable senderTask = new Runnable()
                            {
                               final static int LIMIT = 10;

                               @Override
                               public void run()
                               {
                                  try
                                  {
                                     for (int i = 0 ; i < LIMIT; i++)
                                        pos.write((byte) (Math.random() * 256));
                                  }
                                  catch (IOException ioe)
                                  {
                                     ioe.printStackTrace();
                                  }
                                  finally
                                  {
                                     try
                                     {
                                        pos.close();
                                     }
                                     catch (IOException ioe)
                                     {
                                        ioe.printStackTrace();
                                     }
                                  }
                               }
                            };
      Runnable receiverTask = new Runnable()
                              {
                                 @Override
                                 public void run()
                                 {
                                    try
                                    {
                                       int b;
                                       while ((b = pis.read()) != −1)
                                          System.out.println(b);
                                    }
                                    catch (IOException ioe)
                                    {
                                       ioe.printStackTrace();
                                    }
                                    finally
                                    {
                                       try
                                       {
                                          pis.close();
                                       }
                                       catch (IOException ioe)
                                       {
                                          ioe.printStackTrace();
                                       }
                                    }
                                 }
                              };
      Thread sender = new Thread(senderTask);
      Thread receiver = new Thread(receiverTask);
      sender.start();
      receiver.start();
   }
}

清单 11-9 的 main() 方法创建管道输出和管道输入流,它们将被 senderTask 线程用来传递随机生成的字节整数序列,并被 receiverTask 线程用来接收该序列。

发送者任务的 run() 方法在完成发送数据时显式关闭其管道流。如果不这样做,当接收器线程最后一次调用 read() 时,将抛出一个带有“写结束死亡”消息的 IOException 实例(否则将返回 1 以指示流结束)。关于这条信息的更多信息,请查看丹尼尔·费伯的“这是什么?IOException: Write end dead”博文(tech tavern . WordPress . com/2008/07/16/whats-this-io exception-Write-end-dead/)。

编译清单 11-9(Java PipedStreamsDemo.java)并运行这个应用( java PipedStreamsDemo )。您将发现类似于以下内容的输出:

93
23
125
50
126
131
210
29
150
91

filter utputstream 和 FilterInputStream

字节数组、文件和管道流将字节原封不动地传递到目的地。Java 还支持 filter streams ,在流到达目的地之前对其进行缓冲、压缩/解压缩、加密/解密或其他操作(输入到过滤器的)字节序列。

过滤器输出流获取传递给其 write() 方法(输入流)的数据,对其进行过滤,并将过滤后的数据写入底层输出流,该输出流可能是另一个过滤器输出流或目标输出流,如文件输出流。

过滤器输出流是从具体的 FilterOutputStream 类的子类创建的,一个 OutputStream 子类。 FilterOutputStream 声明了一个单独的 filter output stream(output stream out)构造函数,它创建了一个构建在基础输出流 out 之上的过滤器输出流。

清单 11-10 揭示了子类化 FilterOutputStream 很容易。至少,您声明一个构造函数,将它的输出流参数传递给 filter output 流的构造函数,并覆盖 filter output 流的 write(int) 方法。

清单 11-10 。加扰一个字节流

import java.io.FilterOutputStream;
import java.io.IOException;
import java.io.OutputStream;

public class ScrambledOutputStream extends FilterOutputStream
{
   private int[] map;

   public ScrambledOutputStream(OutputStream out, int[] map)
   {
      super(out);
      if (map == null)
         throw new NullPointerException("map is null");
      if (map.length != 256)
         throw new IllegalArgumentException("map.length != 256");
      this.map = map;
   }

   @Override
   public void write(int b) throws IOException
   {
      out.write(map[b]);
   }
}

清单 11-10 展示了一个 ScrambledOutputStream 类,它通过重新映射操作对输入流的字节进行加扰,从而对输入流执行简单的加密。此构造函数接受一对参数:

  • out 标识要写入加扰字节的输出流。
  • map 标识输入流字节映射到的 256 字节整数值的数组。

构造函数首先通过一个 super(out)将它的 out 参数传递给 FilterOutputStream 父类;通话。然后,在保存映射之前,它验证其映射参数的完整性(映射必须非空,并且长度为 256:一个字节流正好提供 256 个字节进行映射)。

write(int) 方法很简单:它用参数 b 映射到的字节调用底层输出流的 write(int) 方法。 FilterOutputStream 声明 out 被保护(为了性能),这就是为什么我可以直接访问这个字段。

注意只需要重写 write(int) ,因为 FilterOutputStream 的另外两个 write() 方法都是通过这个方法实现的。

清单 11-11 展示了一个加扰应用的源代码,该应用通过 ScrambledOutputStream 对源文件的字节进行加扰,并将这些加扰的字节写入目标文件。

清单 11-11 。打乱文件的字节

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

import java.util.Random;

public class Scramble
{
   public static void main(String[] args)
   {
      if (args.length != 2)
      {
         System.err.println("usage: java Scramble srcpath destpath");
         return;
      }
      FileInputStream fis = null;
      ScrambledOutputStream sos = null;
      try
      {
         fis = new FileInputStream(args[0]);
         FileOutputStream fos = new FileOutputStream(args[1]);
         sos = new ScrambledOutputStream(fos, makeMap());
         int b;
         while ((b = fis.read()) != −1)
            sos.write(b);
      }
      catch (IOException ioe)
      {
         ioe.printStackTrace();
      }
      finally
      {
         if (fis != null)
            try
            {
               fis.close();
            }
            catch (IOException ioe)
            {
               ioe.printStackTrace();
            }
         if (sos != null)
            try
            {
               sos.close();
            }
            catch (IOException ioe)
            {
               ioe.printStackTrace();
            }
      }
   }

   static int[] makeMap()
   {
      int[] map = new int[256];
      for (int i = 0; i < map.length; i++)
         map[i] = i;
      // Shuffle map.
      Random r = new Random(0);
      for (int i = 0; i < map.length; i++)
      {
         int n = r.nextInt(map.length);
         int temp = map[i];
         map[i] = map[n];
         map[n] = temp;
      }
      return map;
   }
}

Scramble 的 main() 方法首先验证命令行参数的个数:第一个参数标识包含未加扰内容的文件的源路径;第二个参数标识存储加密内容的文件的目标路径。

假设已经指定了两个命令行参数, main() 实例化 FileInputStream ,创建一个文件输入流,它连接到由 args[0] 标识的文件。

继续, main() 实例化 FileOutputStream ,创建一个文件输出流,它连接到由 args[1] 标识的文件。然后实例化 scrambled output stream 并将 FileOutputStream 实例传递给 ScrambledOutputStream 的构造函数。

注意当一个流实例被传递给另一个流类的构造函数时,两个流被链接在一起。例如,加扰输出流链接到文件输出流。

main() 现在进入一个循环,通过调用 ScrambledOutputStream 的 write(int) 方法,从文件输入流中读取字节,并将它们写入加扰的输出流。这个循环一直持续到 FileInputStream 的 read() 方法返回 1 (文件结束)。

finally 块通过调用它们的 close() 方法来关闭文件输入流和加密的输出流。它不调用文件输出流的 close() 方法,因为 FilterOutputStream 自动调用底层输出流的 close() 方法。

makeMap() 方法负责创建传递给 ScrambledOutputStream 的构造函数的映射数组。想法是用所有 256 字节整数值填充数组,以随机顺序存储它们。

注意在创建 java.util.Random 对象以返回一个可预测的随机数序列时,我将 0 作为种子参数传递。在解读应用中创建互补映射数组时,我需要使用相同的随机数序列,稍后我会介绍。没有相同的序列,解读将无法工作。

假设您有一个简单的 15 字节文件,名为 hello.txt ,其中包含“ Hello,World!(后跟回车和换行符)。如果你在 Windows 7 平台上执行 Java Scramble hello . txt hello . out,你会观察到图 11-5 的加扰输出。

9781430257226_Fig11-05.jpg

图 11-5T3。不同的字体会产生不同外观的杂乱输出

一个过滤器输入流从其底层输入流(可能是另一个过滤器输入流或一个源输入流,如文件输入流)获取数据,对其进行过滤,并通过其 read() 方法(输出流)使这些数据可用。

过滤器输入流是从具体的 FilterInputStream 类的子类创建的,一个 InputStream 子类。 FilterInputStream 声明了一个单独的 filter InputStream(InputStream in)构造函数,它创建了一个构建在基础输入流中之上的过滤器输入流。

清单 11-12 表明子类化 FilterInputStream 很容易。至少声明一个构造函数,将它的 InputStream 参数传递给 FilterInputStream 的构造函数,并覆盖 FilterInputStream 的 read() 和 read(byte[],int,int) 方法。

清单 11-12 。解读字节流

import java.io.FilterInputStream;
import java.io.InputStream;
import java.io.IOException;

public class ScrambledInputStream extends FilterInputStream
{
   private int[] map;

   public ScrambledInputStream(InputStream in, int[] map)
   {
      super(in);
      if (map == null)
         throw new NullPointerException("map is null");
      if (map.length != 256)
         throw new IllegalArgumentException("map.length != 256");
      this.map = map;
   }

   @Override
   public int read() throws IOException
   {
      int value = in.read();
      return (value == −1) ? -1 : map[value];
   }

   @Override
   public int read(byte[] b, int off, int len) throws IOException
   {
      int nBytes = in.read(b, off, len);
      if (nBytes <= 0)
         return nBytes;
      for (int i = 0; i < nBytes; i++)
         b[off + i] = (byte) map[off + i];
      return nBytes;
   }
}

清单 11-12 展示了一个 ScrambledInputStream 类,它通过重新映射操作对底层输入流的加扰字节进行解扰,从而对底层输入流执行简单的解密。

read() 方法首先从底层输入流中读取加扰的字节。如果返回值为 1(文件结束),则将该值返回给其调用者。否则,该字节将被映射到其未加扰的值,该值将被返回。

read(byte[],int,int) 方法类似于 read() ,但是将从底层输入流中读取的字节存储在一个字节数组中,并考虑了该数组中的偏移量和长度(要读取的字节数)。

同样,1 可能从底层的 read() 方法调用返回。如果是,则必须返回该值。否则,数组中的每个字节都被映射到它的未加扰值,并返回读取的字节数。

注意只需要重写 read() 和 read(byte[],int,int) ,因为 FilterInputStream 的 read(byte[]) 方法是通过后一种方法实现的。

清单 11-13 将源代码呈现给一个解扰应用,用于通过解扰源文件的字节并将这些解扰的字节写入目标文件来试验加扰输入流。

清单 11-13 。解读文件的字节

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

import java.util.Random;

public class Unscramble
{
   public static void main(String[] args)
   {
      if (args.length != 2)
      {
         System.err.println("usage: java Unscramble srcpath destpath");
         return;
      }
      ScrambledInputStream sis = null;
      FileOutputStream fos = null;
      try
      {
         FileInputStream fis = new FileInputStream(args[0]);
         sis = new ScrambledInputStream(fis, makeMap());
         fos = new FileOutputStream(args[1]);
         int b;
         while ((b = sis.read()) != −1)
            fos.write(b);
      }
      catch (IOException ioe)
      {
         ioe.printStackTrace();
      }
      finally
      {
         if (sis != null)
            try
            {
               sis.close();
            }
            catch (IOException ioe)
            {
               ioe.printStackTrace();
            }
         if (fos != null)
            try
            {
               fos.close();
            }
            catch (IOException ioe)
            {
               ioe.printStackTrace();
            }
      }
   }

   static int[] makeMap()
   {
      int[] map = new int[256];
      for (int i = 0; i < map.length; i++)
         map[i] = i;
      // Shuffle map.
      Random r = new Random(0);
      for (int i = 0; i < map.length; i++)
      {
         int n = r.nextInt(map.length);
         int temp = map[i];
         map[i] = map[n];
         map[n] = temp;
      }
      int[] temp = new int[256];
      for (int i = 0; i < temp.length; i++)
         temp[map[i]] = i;
      return temp;
   }
}

解读的 main() 方法首先验证命令行参数的个数:第一个参数标识被加扰内容的文件的源路径;第二个参数标识存储未加扰内容的文件的目标路径。

假设已经指定了两个命令行参数,main()实例化 FileInputStream ,创建一个文件输入流,它连接到由 args[1] 标识的文件。

继续, main() 实例化 FileInputStream ,创建一个连接到由 args[0] 标识的文件的文件输入流。然后实例化 ScrambledInputStream 并将 FileInputStream 实例传递给 ScrambledInputStream 的构造函数。

注意当一个流实例被传递给另一个流类的构造函数时,两个流被链接在一起。例如,加扰的输入流链接到文件输入流。

main() 现在进入一个循环,从加扰的输入流中读取字节,并将它们写入文件输出流。这个循环一直持续到 ScrambledInputStream 的 read() 方法返回 1(文件结束)。

finally 块通过调用它们的 close() 方法来关闭加扰的输入流和文件输出流。它不调用文件输入流的 close() 方法,因为 FilterOutputStream 自动调用底层输入流的 close() 方法。

makeMap() 方法负责创建传递给 ScrambledInputStream 的构造函数的映射数组。这个想法是复制清单 11-11 的映射数组,然后反转它,这样就可以执行解码了。

继续前面的 hello . txt/hello . out 例子,执行 java 解读 hello.out hello.bak ,你会在 hello.bak 中看到与 hello.txt 中相同的解读内容。

注意关于过滤器输出流及其补充过滤器输入流的另一个示例,请查看 Dobb 博士网站上的“扩展 Java 流以支持比特流”一文(【drdobbs.com/184410423】)… BitStreamOutputStream 和 BitStreamInputStream 类。然后,本文在 Lempel-Zif-Welch (LZW)数据压缩和解压缩算法的 Java 实现中演示了这些类。

buffer utputstream 和 buffer ediinput stream

文件输出流和文件输入流出现性能问题。每个文件输出流 write() 方法调用和文件输入流 read() 方法调用都会导致对底层平台的一个本机方法的调用,这些本机调用会降低 I/O 的速度

原生方法是 Java 通过 Java 原生接口(JNI) 连接到应用的底层平台 API 函数。Java 提供了保留字 native 来标识一个本地方法。例如, RandomAccessFile 类声明了一个私有 native void open(String name,int mode) 方法。当一个 RandomAccessFile 构造函数调用这个方法时,Java 要求底层平台(通过 JNI)代表 Java 以指定的模式打开指定的文件。

具体的 BufferedOutputStream 和 BufferedInputStream 过滤器流类通过最小化底层输出流 write() 和底层输入流 read() 方法调用来提高性能。相反,对 BufferedOutputStream 的 write() 和 BufferedInputStream 的 read() 方法的调用考虑了 Java 缓冲区:

  • 当写缓冲区已满时, write() 调用底层输出流 write() 方法来清空缓冲区。对 BufferedOutputStream 的 write() 方法的后续调用将字节存储在这个缓冲区中,直到它再次充满。
  • 当读取缓冲区为空时, read() 调用底层输入流 read() 方法来填充缓冲区。对 BufferedInputStream 的 read() 方法的后续调用从这个缓冲区返回字节,直到它再次为空。

BufferedOutputStream 声明了以下构造函数:

  • BufferedOutputStream(output stream out)创建一个缓冲输出流,将其输出传输到 out 。创建一个内部缓冲器来存储写入 out 的字节。
  • BufferedOutputStream(output stream out,int size) 创建一个缓冲的输出流,将其输出流传送到 out 。创建一个长度为大小为的内部缓冲区来存储写入 out 的字节。

以下示例将一个 BufferedOutputStream 实例链接到一个 FileOutputStream 实例。后续的 write() 方法调用 BufferedOutputStream 实例缓冲字节,偶尔会导致内部 write() 方法调用封装的 FileOutputStream 实例:

FileOutputStream fos = new FileOutputStream("employee.dat");
BufferedOutputStream bos = new BufferedOutputStream(fos); // Chain bos to fos.
bos.write(0); // Write to employee.dat through the buffer.
// Additional write() method calls.
bos.close(); // This method call internally calls fos's close() method.

BufferedInputStream 声明了以下构造函数:

  • BufferedInputStream(InputStream in)创建一个缓冲的输入流,它从中的输入。创建一个内部缓冲区来存储从中的读取的字节。
  • BufferedInputStream(InputStream in,int size) 创建一个缓冲的输入流,它从中的输入。创建一个长度为大小为的内部缓冲区来存储从中的读取的字节。

以下示例将一个 BufferedInputStream 实例链接到一个 FileInputStream 实例。后续的 read() 方法调用在 BufferedInputStream 实例上解缓冲字节,偶尔会导致内部 read() 方法调用在封装的 FileInputStream 实例上:

FileInputStream fis = new FileInputStream("employee.dat");
BufferedInputStream bis = new BufferedInputStream(fis); // Chain bis to fis.
int ch = bis.read(); // Read employee.dat through the buffer.
// Additional read() method calls.
bis.close(); // This method call internally calls fis's close() method.

DataOutputStream 和 DataInputStream

FileOutputStream 和 FileInputStream 对于写入和读取字节和字节数组很有用。但是,它们不支持读写基本类型值(例如整数)和字符串。

为此,Java 提供了具体的数据输出流和数据输入流过滤流类。每个类都通过提供以独立于平台的方式写入或读取基元类型值和字符串的方法来克服这一限制:

  • 整数值以大端格式写入和读取(最高有效字节在前)。查看维基百科的“字节序”条目(【http://en.wikipedia.org/wiki/Endianness】)来了解字节序的概念。
  • 浮点和双精度浮点值是根据 IEEE 754 标准读写的,该标准规定每个浮点值 4 个字节,每个双精度浮点值 8 个字节。
  • 字符串是根据 UTF-8 的修改版本写入和读取的,这是一种有效存储 2 字节 Unicode 字符的可变长度编码标准。查看维基百科的“UTF-8”条目(【en.wikipedia.org/wiki/Utf-8】… UTF-8 的信息。

DataOutputStream 声明单个 data output stream(output stream out)构造函数。因为这个类实现了 DataOutput 接口, DataOutputStream 也提供了对由 RandomAccessFile 提供的同名写方法的访问。

DataInputStream 声明单个 data InputStream(InputStream in)构造函数。因为这个类实现了 DataInput 接口, DataInputStream 也提供了对由 RandomAccessFile 提供的同名读取方法的访问。

清单 11-14 给出了一个数据流数据应用的源代码,该应用使用一个数据输出流实例将多字节值写入一个文件输出流实例,并使用一个数据输入流实例从一个文件输入流实例读取多字节值。

清单 11-14 。输出然后输入多字节值流

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

public class DataStreamsDemo
{
   final static String FILENAME = "values.dat";

   public static void main(String[] args)
   {
      DataOutputStream dos = null;
      DataInputStream dis = null;
      try
      {
         FileOutputStream fos = new FileOutputStream(FILENAME);
         dos = new DataOutputStream(fos);
         dos.writeInt(1995);
         dos.writeUTF("Saving this String in modified UTF-8 format!");
         dos.writeFloat(1.0F);
         dos.close(); // Close underlying file output stream.
         // The following null assignment prevents another close attempt on
         // dos (which is now closed) should IOException be thrown from
         // subsequent method calls.
         dos = null;
         FileInputStream fis = new FileInputStream(FILENAME);
         dis = new DataInputStream(fis);
         System.out.println(dis.readInt());
         System.out.println(dis.readUTF());
         System.out.println(dis.readFloat());
      }
      catch (IOException ioe)
      {
         System.err.println("I/O error: " + ioe.getMessage());
      }
      finally
      {
         if (dos != null)
            try
            {
               dos.close();
            }
            catch (IOException ioe2) // Cannot redeclare local variable ioe.
            {
               assert false; // shouldn't happen in this context
            }
         if (dis != null)
            try
            {
               dis.close();
            }
            catch (IOException ioe2) // Cannot redeclare local variable ioe.
            {
               assert false; // shouldn't happen in this context
            }
      }
   }
}

DataStreamsDemo 创建一个名为 values.dat 的文件;调用 DataOutputStream 方法将一个整数、一个字符串和一个浮点值写入该文件;并调用 DataInputStream 方法来读回这些值。不出所料,它会生成以下输出:

1995
Saving this String in modified UTF-8 format!
1.0

注意当读取由 DataOutputStream 方法调用序列写入的值文件时,确保使用相同的方法调用序列。否则,您一定会得到错误的数据,在使用 readUTF() 方法的情况下,会抛出 Java . io . utfdataformatexception 类的实例(是 IOException 的子类)。

对象序列化和反序列化

Java 提供了 DataOutputStream 和 DataInputStream 类来传输原始类型值和字符串对象。但是,您不能使用这些类来流式传输非字符串对象。相反,您必须使用对象序列化和反序列化来流式传输任意类型的对象。

对象序列化是一个虚拟机机制,用于对象状态序列化为字节流。它的反序列化对应物是一个虚拟机机制,用于从字节流中反序列化该状态。

注意一个对象的状态由存储原始类型值和/或对其他对象的引用的实例字段组成。当对象被序列化时,属于此状态的对象也会被序列化(除非您阻止它们被序列化)。此外,作为这些对象的状态的一部分的对象被序列化(除非您阻止这样做),等等。

Java 支持默认序列化和反序列化、自定义序列化和反序列化以及外部化。

默认序列化和反序列化

默认的序列化和反序列化是最容易使用的形式,但是对如何序列化和反序列化对象几乎没有控制。尽管 Java 代表您处理了大部分工作,但是有几项任务您必须执行。

您的第一个任务是让要序列化的对象的类实现 java.io.Serializable 接口,直接或通过类的超类间接实现。实现可序列化的基本原理是为了避免无限制的序列化。

注意 Serializable 是一个空的标记接口(没有要实现的方法),类实现它来告诉虚拟机可以序列化类的对象。当序列化机制遇到一个其类没有实现 Serializable 的对象时,它抛出一个 Java . io . notserializableexception 类的实例(一个 IOException 的间接子类)。

无限制序列化是序列化整个对象图 (从一个起始对象可达的所有对象)的过程。Java 不支持无限制的序列化,原因如下:

  • 安全 :如果 Java 自动序列化一个包含敏感信息(如密码或信用卡号)的对象,黑客很容易发现这些信息并大肆破坏。最好给开发者一个选择,防止这种情况发生。
  • 性能 :序列化利用了反射 API,这往往会降低应用的性能。无限制的序列化真的会损害应用的性能。
  • 不适合序列化的对象:有些对象只存在于正在运行的应用的上下文中,序列化它们是没有意义的。例如,反序列化的文件流对象不再表示与文件的连接。

清单 11-15 声明了一个雇员类,它实现了可序列化接口来告诉虚拟机可以序列化雇员对象。

清单 11-15 。实现序列化

import java.io.Serializable;

public class Employee implements Serializable
{
   private String name;
   private int age;

   public Employee(String name, int age)
   {
      this.name = name;
      this.age = age;
   }

   public String getName() { return name; }

   public int getAge() { return age; }
}

因为雇员实现了可序列化,序列化一个雇员对象时,序列化机制不会抛出一个 NotSerializableException 实例。不仅 Employee 实现了 Serializable ,而且 String 类也实现了这个接口。

您的第二个任务是使用 ObjectOutputStream 类及其 writeObject() 方法来序列化对象,使用 OutputInputStream 类及其 readObject() 方法来反序列化对象。

注意尽管 ObjectOutputStream 扩展了 OutputStream 而不是 FilterOutputStream ,尽管 ObjectInputStream 扩展了 InputStream 而不是 FilterInputStream ,但是这些类的行为就像过滤器流一样。

Java 提供了具体的 ObjectOutputStream 类来启动对象状态到对象输出流的序列化。这个类声明了一个 object output stream(output stream out)构造函数,将对象输出流链接到由 out 指定的输出流。

当您将对的输出流引用传递给 out 时,该构造函数会尝试将序列化头写入该输出流。当 out 为 null 时,它抛出 NullPointerException ,当 I/O 错误阻止它写入此标头时,它抛出 IOException 。

ObjectOutputStream 通过其 void writeObject(Object obj)方法序列化一个对象。该方法试图将关于 obj 的类的信息,后跟 obj 的实例字段的值写入底层输出流。

writeObject() 不序列化静态字段的内容。相比之下,它序列化所有没有明确以瞬态保留字为前缀的实例字段的内容。例如,考虑以下字段声明:

public transient char[] password;

这个声明指定了 transient 来避免序列化某个黑客遇到的密码。虚拟机的序列化机制忽略任何标记为瞬态的实例字段。

当出错时, writeObject() 抛出 IOException 或 IOException 子类的实例。例如,当这个方法遇到一个类没有实现 Serializable 的对象时,就会抛出 NotSerializableException。

注意因为 ObjectOutputStream 实现了 DataOutput ,所以它也声明了将原始类型值和字符串写入对象输出流的方法。

Java 提供了具体的 ObjectInputStream 类来启动对象输入流中对象状态的反序列化。这个类声明了一个 ObjectInputStream(InputStream in)构造函数,它将对象输入流链接到由中的指定的输入流。

当您将输入流引用传递给中的时,该构造函数试图从该输入流中读取序列化头。当中的为 null 时,它抛出 NullPointerException ,当 I/O 错误阻止它读取该头时,抛出 IOException ,当流头不正确时,抛出 Java . io . streamcorvertedexception(io exception 的间接子类)。

ObjectInputStream 通过其 Object readObject() 方法反序列化一个对象。该方法试图从底层输入流中读取关于 obj 的类的信息,后跟 obj 的实例字段的值。

readObject() 在出错时抛出 Java . lang . classnotfoundexception、 IOException ,或者一个 IOException 子类的实例。例如,当这个方法遇到原始类型的值而不是对象时,它会抛出 Java . io . optionaldataexception。

注意因为 ObjectInputStream 实现了 DataInput ,它还声明了从对象输入流中读取原始类型值和字符串的方法。

清单 11-16 展示了一个应用,它使用这些类来序列化和反序列化清单 11-15 的雇员类的实例到雇员. dat 文件。

清单 11-16 。序列化和反序列化一个雇员对象

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

public class SerializationDemo
{
   final static String FILENAME = "employee.dat";

   public static void main(String[] args)
   {
      ObjectOutputStream oos = null;
      ObjectInputStream ois = null;
      try
      {
         FileOutputStream fos = new FileOutputStream(FILENAME);
         oos = new ObjectOutputStream(fos);
         Employee emp = new Employee("John Doe", 36);
         oos.writeObject(emp);
         oos.close();
         oos = null;
         FileInputStream fis = new FileInputStream(FILENAME);
         ois = new ObjectInputStream(fis);
         emp = (Employee) ois.readObject(); // (Employee) cast is necessary.
         ois.close();
         System.out.println(emp.getName());
         System.out.println(emp.getAge());
      }
      catch (ClassNotFoundException cnfe)
      {
         System.err.println(cnfe.getMessage());
      }
      catch (IOException ioe)
      {
         System.err.println(ioe.getMessage());
      }
      finally
      {
         if (oos != null)
            try
            {
               oos.close();
            }
            catch (IOException ioe)
            {
               assert false; // shouldn't happen in this context
            }

         if (ois != null)
            try
            {
               ois.close();
            }
            catch (IOException ioe)
            {
               assert false; // shouldn't happen in this context
            }
      }
   }
}

清单 11-16 的 main() 方法首先实例化 Employee ,并通过 writeObject() 将此实例序列化为 employee.dat 。然后,它通过 readObject() 从这个文件中反序列化这个实例,并调用实例的 getName() 和 getAge() 方法。与 employee.dat 一起,当您运行这个应用时,您将发现以下输出:

John Doe
36

当序列化对象被反序列化时,不能保证相同的类会存在(可能实例字段已经被删除)。在反序列化期间,该机制导致 readObject() 在检测到反序列化的对象与其类之间的差异时抛出 Java . io . invalidclassexception—io exception 类的间接子类。

每个序列化对象都有一个标识符。反序列化机制将被反序列化的对象的标识符与其类的序列化标识符进行比较(所有可序列化的类都会自动获得唯一的标识符,除非它们显式指定了自己的标识符),并在检测到不匹配时引发 InvalidClassException。

也许您已经向类中添加了一个实例字段,并且您希望反序列化机制将实例字段设置为默认值,而不是让 readObject() 抛出一个 InvalidClassException 实例。(下次序列化对象时,将写出新字段的值。)

可以通过添加一个静态最终长 serialVersionUID = 长整型值 来避免抛出 InvalidClassException 实例;向全班同学宣言。长整数值必须是唯一的,被称为流唯一标识符(SUID)

在反序列化过程中,虚拟机会将反序列化对象的 SUID 与其类的 SUID 进行比较。如果匹配, readObject() 在遇到兼容的类变更 (如添加实例字段)时,不会抛出 InvalidClassException 。但是,当遇到不兼容的类变更 (例如,变更实例字段的名称或类型)时,它仍然会抛出这个异常。

注意每当你以某种方式改变一个类时,你必须计算一个新的 SUID 并把它分配给 serialVersionUID 。

JDK 为计算 SUID 提供了一个串行工具。例如,要为清单 11-15 的雇员类生成一个 SUID,切换到包含雇员.类的目录并执行串行雇员。作为响应, serialver 生成以下输出,您将其粘贴到 Employee.java 中(除了雇员:):

Employee:    static final long serialVersionUID = 1517331364702470316L;

Windows 版的 serialver 也提供了一个图形用户界面,你可能会发现使用起来更方便。要访问该界面,请指定串行显示。当 serialver 窗口出现时,将 Employee 输入完整的类名文本字段,点击显示按钮,如图图 11-6 所示。

9781430257226_Fig11-06.jpg

图 11-6T3。serialver 用户界面显示员工的 SUID

自定义序列化和反序列化

我之前的讨论集中在默认的序列化和反序列化上(除了标记实例字段 transient 以防止它在序列化期间被包含)。但是,有时您需要定制这些任务。

例如,假设您想要序列化一个没有实现 Serializable 的类的实例。作为一种变通方法,您将这个类分成子类,让子类实现 Serializable ,并将子类构造函数调用转发给超类。

尽管这种变通方法允许您序列化子类对象,但是当超类没有声明反序列化机制所要求的无参数构造函数时,您不能反序列化这些序列化的对象。清单 11-17 展示了这个问题。

清单 11-17 。有问题的反序列化

import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.Serializable;

class Employee
{
   private String name;

   Employee(String name)
   {
      this.name = name;
   }

   @Override
   public String toString()
   {
      return name;
   }
}

class SerEmployee extends Employee implements Serializable
{
   SerEmployee(String name)
   {
      super(name);
   }
}

public class SerializationDemo
{
   public static void main(String[] args)
   {
      ObjectOutputStream oos = null;
      ObjectInputStream ois = null;
      try
      {
         oos = new ObjectOutputStream(new FileOutputStream("employee.dat"));
         SerEmployee se = new SerEmployee("John Doe");
         System.out.println(se);
         oos.writeObject(se);
         oos.close();
         oos = null;
         System.out.println("se object written to file");
         ois = new ObjectInputStream(new FileInputStream("employee.dat"));
         se = (SerEmployee) ois.readObject();
         System.out.println("se object read from file");
         System.out.println(se);
      }
      catch (ClassNotFoundException cnfe)
      {
         cnfe.printStackTrace();
      }
      catch (IOException ioe)
      {
         ioe.printStackTrace();
      }
      finally
      {
         if (oos != null)
            try
            {
               oos.close();
            }
            catch (IOException ioe)
            {
               assert false; // shouldn't happen in this context
            }
         if (ois != null)
            try
            {
               ois.close();
            }
            catch (IOException ioe)
            {
               assert false; // shouldn't happen in this context
            }
      }
   }
}

清单 11-17 的 main() 方法用雇员姓名实例化 SerEmployee 。这个类的 SerEmployee(String) 构造函数将这个参数传递给其对应的 Employee 。

main() 接下来通过 System.out.println() 间接调用员工的 toString() 方法,获取这个名字,然后输出。

继续, main() 通过 writeObject() 将 SerEmployee 实例序列化为 employee.dat 文件。然后,它试图通过 readObject() 反序列化该对象,这就是问题所在,如下图所示:

John Doe
se object written to file
java.io.InvalidClassException: SerEmployee; no valid constructor
                at java.io.ObjectStreamClass$ExceptionInfo.newInvalidClassException(Unknown Source)
                at java.io.ObjectStreamClass.checkDeserialize(Unknown Source)
                at java.io.ObjectInputStream.readOrdinaryObject(Unknown Source)
                at java.io.ObjectInputStream.readObject0(Unknown Source)
                at java.io.ObjectInputStream.readObject(Unknown Source)
                at SerializationDemo.main(SerializationDemo.java:48)

这个输出揭示了一个抛出的 InvalidClassException 类的实例。这个异常对象在反序列化过程中被抛出,因为雇员没有无参数构造函数。

你可以通过利用我在第四章中介绍的包装类模式来解决这个问题。此外,您在子类中声明了一对私有方法,序列化和反序列化机制会查找并调用这些方法。

通常,序列化机制将类的实例字段写出到基础输出流中。但是,您可以通过在该类中声明一个私有的 void writeObject(object output stream OOS)方法来防止这种情况发生。

当序列化机制发现此方法时,它会调用方法,而不是自动输出实例字段值。唯一输出的值是通过方法显式输出的值。

相反,反序列化机制向从底层输入流中读取的类的实例字段赋值。但是,您可以通过声明一个私有的 void read object(ObjectInputStream ois)方法来防止这种情况发生。

当反序列化机制发现此方法时,它会调用方法,而不是自动为实例字段赋值。分配给实例字段的唯一值是通过方法显式分配的值。

因为 SerEmployee 不引入任何字段,并且因为 Employee 不提供对其内部字段的访问(假设您没有该类的源代码),序列化的 SerEmployee 对象将包含什么?

虽然不能序列化雇员的内部状态,但是可以序列化传递给其构造函数的参数,比如雇员姓名。

清单 11-18 展示了重构后的 SerEmployee 和 SerializationDemo 类。

清单 11-18 。解决有问题的反序列化

import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.Serializable;

class Employee
{
   private String name;

   Employee(String name)
   {
      this.name = name;
   }

   @Override
   public String toString()
   {
      return name;
   }
}

class SerEmployee implements Serializable
{
   private Employee emp;
   private String name;

   SerEmployee(String name)
   {
      this.name = name;
      emp = new Employee(name);
   }

   private void writeObject(ObjectOutputStream oos) throws IOException
   {
      oos.writeUTF(name);
   }

   private void readObject(ObjectInputStream ois)
      throws ClassNotFoundException, IOException
   {
      name = ois.readUTF();
      emp = new Employee(name);
   }

   @Override
   public String toString()
   {
      return name;
   }
}

public class SerializationDemo
{
   public static void main(String[] args)
   {
      ObjectOutputStream oos = null;
      ObjectInputStream ois = null;
      try
      {
         oos = new ObjectOutputStream(new FileOutputStream("employee.dat"));
         SerEmployee se = new SerEmployee("John Doe");
         System.out.println(se);
         oos.writeObject(se);
         oos.close();
         oos = null;
         System.out.println("se object written to file");
         ois = new ObjectInputStream(new FileInputStream("employee.dat"));
         se = (SerEmployee) ois.readObject();
         System.out.println("se object read from file");
         System.out.println(se);
      }
      catch (ClassNotFoundException cnfe)
      {
         cnfe.printStackTrace();
      }
      catch (IOException ioe)
      {
         ioe.printStackTrace();
      }
      finally
      {
         if (oos != null)
            try
            {
               oos.close();
            }
            catch (IOException ioe)
            {
               assert false; // shouldn't happen in this context
            }
         if (ois != null)
            try
            {
               ois.close();
            }
            catch (IOException ioe)
            {
               assert false; // shouldn't happen in this context
            }
      }
   }
}

SerEmployee 的 writeObject() 和 readObject() 方法依赖于 DataOutput 和 DataInput 方法:它们不需要调用 writeObject() 和 readObject() 来执行任务。

当您运行此应用时,它会生成以下输出:

John Doe
se object written to file
se object read from file
John Doe

writeObject() 和 readObject() 方法可用于序列化/反序列化超出正常状态的数据项(非瞬态实例字段),例如,序列化/反序列化静态字段的内容。

但是,在序列化或反序列化附加数据项之前,必须告诉序列化和反序列化机制序列化或反序列化对象的正常状态。以下方法有助于您完成这项任务:

  • ObjectOutputStream 的 defaultWriteObject() 方法输出对象的正常状态。您的 writeObject() 方法首先调用此方法来输出该状态,然后通过 ObjectOutputStream 方法(如 writeUTF() )输出附加数据项。
  • ObjectInputStream 的 defaultReadObject() 方法输入对象的正常状态。您的 readObject() 方法首先调用这个方法来输入那个状态,然后通过 ObjectInputStream 方法输入额外的数据项,比如 readUTF() 。

客观化

除了默认序列化/反序列化和自定义序列化/反序列化,Java 还支持外部化。与默认/自定义序列化/反序列化不同,外部化 提供了对序列化和反序列化任务的完全控制。

外部化通过让您完全控制序列化和反序列化哪些字段,帮助您提高基于反射的序列化和反序列化机制的性能。

Java 通过 java.io.Externalizable 支持外部化。这个接口声明了下面一对公共方法:

  • void write external(object output out)通过调用 out 对象上的各种方法来保存调用对象的内容。当一个 I/O 错误发生时,这个方法抛出 IOException 。( java.io.ObjectOutput 是 DataOutput 的子接口,由 ObjectOutputStream 实现。)
  • void read external(object input in)通过调用对象中上的各种方法来恢复调用对象的内容。当发生 I/O 错误时,该方法抛出 IOException ,当找不到正在恢复的对象的类时,抛出 ClassNotFoundException 。( java.io.ObjectInput 是 DataInput 的子接口,由 ObjectInputStream 实现。)

如果一个类实现了可外部化,它的 writeExternal() 方法负责保存所有要保存的字段值。此外,它的 readExternal() 方法负责恢复所有保存的字段值,并按照它们保存的顺序。

清单 11-19 展示了清单 11-15 的的雇员类的重构版本,向您展示如何利用外部化。

清单 11-19 。重构清单 11-15 的雇员类以支持外部化

import java.io.Externalizable;
import java.io.IOException;
import java.io.ObjectInput;
import java.io.ObjectOutput;

public class Employee implements Externalizable
{
   private String name;
   private int age;

   public Employee()
   {
      System.out.println("Employee() called");
   }

   public Employee(String name, int age)
   {
      this.name = name;
      this.age = age;
   }

   public String getName() { return name; }

   public int getAge() { return age; }

   @Override
   public void writeExternal(ObjectOutput out) throws IOException
   {
      System.out.println("writeExternal() called");
      out.writeUTF(name);
      out.writeInt(age);
   }

   @Override
   public void readExternal(ObjectInput in)
      throws IOException, ClassNotFoundException
   {
      System.out.println("readExternal() called");
      name = in.readUTF();
      age = in.readInt();
   }
}

Employee 声明了一个 public Employee() 构造函数,因为每个参与外部化的类都必须声明一个 public noargument 构造函数。反序列化机制调用此构造函数来实例化对象。

注意反序列化机制在没有检测到公共无参数构造函数时抛出 InvalidClassException 和“无有效构造函数”消息。

通过实例化 ObjectOutputStream 并调用其 writeObject(Object) 方法,或者通过实例化 ObjectInputStream 并调用其 readObject() 方法,启动外部化。

注意当将一个其类(直接/间接)实现了可外化的对象传递给 writeObject() 时, writeObject() 发起的序列化机制只将该对象的类的标识写入对象输出流。

假设你在同一个目录下编译了清单 11-16 的【SerializationDemo.java】的的源代码和清单 11-19 的的【Employee.java】的源代码。现在假设您执行了 java SerializationDemo 。作为响应,您会看到以下输出:

writeExternal() called
Employee() called
readExternal() called
John Doe
36

在序列化一个对象之前,序列化机制检查对象的类,看它是否实现了可外部化的。如果是,该机制调用 writeExternal() 。否则,它会查找私有的 writeObject(object output stream)方法,并在出现时调用该方法。当这个方法不存在时,这个机制执行默认序列化,它只包括非瞬态实例字段。

在反序列化一个对象之前,反序列化机制检查对象的类,看它是否实现了可外化的。如果是这样,该机制试图通过 public noargument 构造函数实例化该类。假设成功,它调用 readExternal() 。

当对象的类没有实现可外部化时,反序列化机制会寻找私有的 read object(ObjectInputStream)方法。当这个方法不存在时,这个机制执行默认的反序列化,它只包括非瞬态实例字段。

PrintStream

在所有的流类中, PrintStream 是一个古怪的类:为了与命名约定保持一致,它应该被命名为 PrintOutputStream 。此筛选器输出流类将输入数据项的字符串表示形式写入基础输出流。

注意 PrintStream 使用默认的字符编码将字符串的字符转换成字节。(在下一节向作者和读者介绍字符编码时,我会讨论字符编码。)因为 PrintStream 不支持不同的字符编码,所以你应该使用等价的 PrintWriter 类,而不是 PrintStream 。但是,在使用 System.out 和 System.err 时,您需要了解 PrintStream ,因为这些类字段属于类型 PrintStream 。

PrintStream 实例是打印流,其各种 print() 和 println() 方法将整数、浮点值和其他数据项的字符串表示打印到底层输出流。与 print() 方法不同, println() 方法在它们的输出中附加一个行结束符。

注意行结束符(也称为行分隔符)不一定是换行符(通常也称为换行)。相反,为了提高可移植性,行分隔符是由系统属性 line.separator 定义的字符序列。在 Windows 平台上,system . getproperty(" line . separator ")返回实际的回车码(13),用 \r 象征性表示,后面是实际的换行符/换行码(10),用 \n 象征性表示。相比之下,system . getproperty(" line . separator ")在 Unix 和 Linux 平台上只返回实际的换行符/换行符代码。

println() 方法调用它们对应的 print() 方法,然后调用等价的 void println() 方法,最终导致 line.separator 的值被输出。比如 void println(int x) 输出 x 的字符串表示,调用这个方法输出行分隔符。

注意永远不要将 \n 转义序列硬编码到将要通过 print() 或 println() 方法输出的字符串中。这样做是不可移植的。比如 Java 执行 system . out . print(" first line \ n ")时;后跟 System.out.println("第二行");,当在 Windows 命令行查看该输出时,您将在一行上看到第行,然后在下一行上看到第行。相比之下,当在 Windows 记事本应用中查看此输出时,您将看到第一行第二行(需要回车/换行序列来终止行)。当需要输出一个空行时,最简单的方法就是调用 system . out . println();这就是为什么你会在我的书的其他地方发现这个方法调用。我承认我并不总是遵循自己的建议,所以在本书的其他地方,您可能会发现文字字符串中的 \n 被传递给 System.out.print() 或 System.out.println() 的实例。

PrintStream 还提供了另外三个有用的功能:

  • 与其他输出流不同,打印流从不重新抛出从底层输出流抛出的 IOException 实例。相反,异常情况会设置一个内部标志,可以通过调用 PrintStream 的 boolean checkError() 方法进行测试,该方法返回 true 来指示问题。
  • PrintStream 可以创建对象来自动将它们的输出刷新到底层输出流。换句话说,在写入一个字节数组、调用一个 println() 方法或者写入一个换行符之后,会自动调用 flush() 方法。分配给 System.out 和 System.err 的 PrintStream 实例自动将其输出刷新到底层输出流。
  • PrintStream 声明一个 PrintStream 格式(字符串格式,对象...args) 实现格式化输出的方法。在幕后,这个方法与我在第十章的中介绍的格式化器类一起工作。 PrintStream 还声明了一个 printf(字符串格式,对象...args) 委托给 format() 方法的便利方法。例如,通过 out.printf(format,args) 调用 printf() 等同于调用 out.format(format,args) 。

与作者和读者合作

Java 的流类适用于字节流序列,但不适用于字符流序列,因为字节和字符是两种不同的东西:一个字节代表一个 8 位数据项,一个字符代表一个 16 位数据项。同样,Java 的 char 和 String 类型自然处理字符而不是字节。

更重要的是,字节流不知道字符集合 (整数值之间的映射集合,称为码点 和符号,如 Unicode)和它们的字符编码 (字符集成员和字节序列之间的映射,为提高效率对这些字符进行编码,如 UTF-8)。

如果您需要流式传输字符,您应该利用 Java 的 writer 和 reader 类,它们被设计为支持字符 I/O(它们使用 char 而不是 byte )。此外,writer 和 reader 类考虑了字符编码。

字符集和字符编码简史

早期的计算机和编程语言主要是由以英语为母语的国家的英语程序员创造的。他们开发了代码点 0 到 127 与英语中常用的 128 个字符(如 A-Z)之间的标准映射。由此产生的字符集/编码被命名为美国信息交换标准码(ASCII)

ASCII 的问题是它对于大多数非英语语言来说是不够的。例如,ASCII 不支持像法语中使用的 cedilla 这样的变音符号。由于一个字节最多可以表示 256 个不同的字符,世界各地的开发人员开始创建不同的字符集/编码,这些字符集/编码不仅编码 128 个 ASCII 字符,还编码额外的字符,以满足法语、希腊语或俄语等语言的需要。多年来,已经创建了许多遗留的(并且仍然重要的)数据文件,其字节表示由特定字符集/编码定义的字符。

国际标准化组织(ISO)和国际电工委员会(IEC)一直致力于在名为 ISO/IEC 8859 的联合总括标准下标准化这些 8 位字符集/编码。结果是一系列名为 ISO/IEC 8859-1、ISO/IEC 8859-2 等的子标准。例如,ISO/IEC 8859–1(也称为 Latin-1)定义了一个字符集/编码,它由 ASCII 和覆盖大多数西欧国家的字符组成。此外,ISO/IEC 8859-2(也称为 Latin-2)定义了一个涵盖中欧和东欧国家的类似字符集/编码。

尽管 ISO/IEC 尽了最大努力,过多的字符集/编码仍然是不够的。例如,大多数字符集/编码只允许您用英语和一种其他语言(或少量其他语言)的组合来创建文档。例如,您不能使用 ISO/IEC 字符集/编码来创建使用英语、法语、土耳其语、俄语和希腊语字符组合的文档。

这个问题和其他问题正在由一个国际组织努力解决,该组织已经创建并正在继续开发一种单一通用字符集 Unicode 。因为 Unicode 字符比 ISO/IEC 字符大,所以 Unicode 使用几种称为 Unicode 转换格式(UTF) 的可变长度编码方案之一来编码 Unicode 字符以提高效率。例如,UTF-8 将 Unicode 字符集中的每个字符编码为 1 到 4 个字节(并向后兼容 ASCII)。

术语字符集字符编码经常互换使用。在 ISO/IEC 字符集的上下文中,它们的意思是一样的,在 ISO/IEC 字符集中,代码点就是编码。但是,这些术语在 Unicode 的上下文中是不同的,在 Unicode 的上下文中,Unicode 是字符集,UTF-8 是 Unicode 字符的几种可能的字符编码之一。

作者和读者类概述

java.io 包提供了几个 writer 和 reader 类,它们是抽象的 Writer 和 Reader 类的后代。图 11-7 揭示了 writer 类的层次结构。

9781430257226_Fig11-07.jpg

图 11-7T3。与 FilterOutputStream 不同,FilterWriter 是抽象的

图 11-8 揭示了阅读器类的层次结构。

9781430257226_Fig11-08.jpg

图 11-8T3。与 FilterInputStream 不同,FilterReader 是抽象的

尽管 writer 和 reader 类的层次结构与它们的输出流和输入流相似,但还是有区别。例如, FilterWriter 和 FilterReader 是抽象的,而它们的 FilterOutputStream 和 FilterInputStream 等价物不是抽象的。另外, BufferedWriter 和 BufferedReader 不扩展 FilterWriter 和 FilterReader ,而 BufferedOutputStream 和 BufferedInputStream 扩展 FilterOutputStream 和 FilterInputStream 。

Java 1.0 中引入了输出流和输入流类。在它们发布后,设计问题出现了。例如, FilterOutputStream 和 FilterInputStream 应该是抽象的。但是,进行这些更改已经太晚了,因为这些类已经被使用了;进行这些更改会导致代码崩溃。Java 1.1 的 writer 和 reader 类的设计者花时间来纠正这些错误。

注意关于 BufferedWriter 和 BufferedReader 直接子类化 Writer 和 Reader 而不是 FilterWriter 和 FilterReader ,相信这个变化跟性能有关。对 BufferedOutputStream 的 write() 方法和 BufferedInputStream 的 read() 方法的调用导致对 FilterOutputStream 的 write() 方法和 FilterInputStream 的 read() 方法的调用。因为文件 I/O 活动(比如将一个文件复制到另一个文件)可能涉及许多 write()/read()方法调用,所以您希望获得尽可能好的性能。通过不子类化 FilterWriter 和 filter reader,buffered writer 和 BufferedReader 实现更好的性能。

为了简洁起见,我在本章中只关注 Writer 、 Reader 、 OutputStreamWriter 、 OutputStreamReader 、 FileWriter 和 FileReader 类。

作家和读者

Java 提供了用于执行字符 I/O 的 Writer 和 Reader 类。 Writer 是所有 Writer 子类的超类。下表列出了编写器和输出流 之间的区别:

  • Writer 声明了几个 append() 方法,用于向该 Writer 追加字符。这些方法之所以存在,是因为编写器实现了 java.lang.Appendable 接口,该接口与格式化程序类(在第十章中讨论)一起使用,以输出格式化的字符串。
  • 编写器声明了额外的 write() 方法,包括一个方便的 void write(String str) 方法,用于将 String 对象的字符写入该编写器。

Reader 是所有 Reader 子类的超类。下面列出了阅读器和输入流 之间的区别:

  • Reader 声明了 read(char[]) 和 read(char[],int,int) 方法,而不是 read(byte[]) 和 read(byte[],int,int) 方法。
  • 读者没有声明一个可用的()方法。
  • Reader 声明了一个 boolean ready() 方法,当下一个 read() 调用保证不会阻塞输入时,该方法返回 true。
  • Reader 声明了一个 int read(char buffer target)方法,用于从字符缓冲区读取字符。(我在第十三章的中讨论 CharBuffer 。)

OutputStreamWriter 和 InputStreamReader

具体的 OutputStreamWriter 类(一个 Writer 子类)是传入字符序列和传出字节流之间的桥梁。根据默认或指定的字符编码,写入此编写器的字符被编码为字节。

注意默认的字符编码可以通过 file.encoding 系统属性访问。

对 OutputStreamWriter 的 write() 方法之一的每次调用都会导致对给定字符调用编码器。结果字节在写入基础输出流之前在缓冲区中累积。传递给 write() 方法的字符没有被缓冲。

OutputStreamWriter 声明了四个构造函数,包括下面的一对:

  • output streamwriter(output stream out)在传入的字符序列(通过其 append() 和 write() 方法传递给 OutputStreamWriter )和底层输出流 out 之间创建一个桥梁。默认的字符编码用于将字符编码成字节。
  • output streamwriter(output stream out,String charsetName) 在传入的字符序列(通过其 append() 和 write() 方法传递给 OutputStreamWriter )和底层输出流 out 之间创建一个桥梁。 charsetName 标识用于将字符编码成字节的字符编码。当不支持命名字符编码时,该构造函数抛出 Java . io . unsupportedencodingexception。

注意 OutputStreamWriter 依赖抽象 java.nio.charset.Charset 和 Java . nio . charset . charset encoder 类来执行字符编码。

下面的示例使用第二个构造函数创建到基础文件输出流的桥,以便可以将波兰语文本写入 ISO/IEC 8859-2 编码的文件。

FileOutputStream fos = new FileOutputStream("polish.txt");
OutputStreamWriter osw = new OutputStreamWriter(fos, "8859_2");
char ch = '\u0323'; // Accented N.
osw.write(ch);

具体的 InputStreamReader 类(一个 Reader 子类)是传入字节流和传出字符序列之间的桥梁。从该读取器读取的字符根据默认或指定的字符编码从字节解码。

对 InputStreamReader 的 read() 方法之一的每次调用都可能导致从底层输入流中读取一个或多个字节。为了有效地将字节转换为字符,可以从基础流中提前读取比满足当前读取操作所需更多的字节。

InputStreamReader 声明了四个构造函数,包括下面的一对:

  • InputStreamReader(InputStream in)在中的基础输入流和输出字符序列(通过其 read() 方法从 InputStreamReader 返回)之间创建一座桥梁。默认的字符编码用于将字节解码成字符。
  • InputStreamReader(InputStream in,String charsetName) 在中的底层输入流和输出字符序列(通过其 read() 方法从 InputStreamReader 返回)之间创建一座桥梁。 charsetName 标识用于将字节解码为字符的字符编码。当不支持命名字符编码时,该构造函数抛出 UnsupportedEncodingException。

注意 InputStreamReader 依赖抽象 Charset 和 Java . nio . Charset . Charset decoder 类进行字符解码。

下面的示例使用第二个构造函数创建到基础文件输入流的桥,以便可以从 ISO/IEC 8859-2 编码的文件中读取波兰语文本。

FileInputStream fis = new FileInputStream("polish.txt");
InputStreamReader isr = new InputStreamReader(fis, "8859_2");
char ch = isr.read(ch);

注意 OutputStreamWriter 和 InputStreamReader 声明了一个 String getEncoding() 方法,该方法返回正在使用的字符编码的名称。如果编码有历史名称,则返回该名称;否则,将返回编码的规范名称。

文件写入器和文件读取器

FileWriter 是一个方便的类,用于向文件中写入字符。它子类化 OutputStreamWriter ,其构造函数调用 output streamwriter(output stream)。此类的一个实例等效于下面的代码片段:

FileOutputStream fos = new FileOutputStream(pathname);
OutputStreamWriter osw;
osw = new OutputStreamWriter(fos, System.getProperty("file.encoding"));

在第五章的中,我介绍了一个带有文件类的日志库,它没有包含文件写代码。清单 11-20 通过提供一个修改过的 File 类来解决这种情况,该类使用 FileWriter 将消息记录到一个文件中。

清单 11-20 。将消息记录到实际文件中

package logging;

import java.io.FileWriter;
import java.io.IOException;

class File implements Logger
{
   private final static String LINE_SEPARATOR = System.getProperty("line.separator");

   private String dstName;
   private FileWriter fw;

   File(String dstName)
   {
      this.dstName = dstName;
   }

   public boolean connect()
   {
      if (dstName == null)
         return false;
      try
      {
          fw = new FileWriter(dstName);
      }
      catch (IOException ioe)
      {
         return false;
      }
      return true;
   }

   public boolean disconnect()
   {
      if (fw == null)
         return false;
      try
      {
         fw.close();
      }
      catch (IOException ioe)
      {
         return false;
      }
      return true;
   }

   public boolean log(String msg)
   {
      if (fw == null)
          return false;
      try
      {
         fw.write(msg + LINE_SEPARATOR);
      }
      catch (IOException ioe)
      {
         return false;
      }
      return true;
   }
}

清单 11-20 重构第五章的文件类,通过对 connect() 、 disconnect() 和 log() 方法中的每一个进行修改来支持 FileWriter :

  • connect() 尝试实例化 FileWriter ,成功后其实例保存在 fw 中;否则, fw 继续存储其默认的空引用。
  • disconnect() 试图通过调用 FileWriter 的 close() 方法来关闭文件,但仅当 fw 不包含其默认的空引用时。
  • log() 试图通过调用 FileWriter 的 void write(String str) 方法将其字符串参数写入文件,但仅当 fw 不包含其默认空引用时。

connect() 的 catch 块指定了 IOException 而不是 FileNotFoundException ,因为 FileWriter 的构造函数在无法连接到现有的普通文件时会抛出 io exception; FileOutputStream 的构造函数抛出 FileNotFoundException 。

log()write(String)方法将 line.separator 值(为了方便起见,我将其赋给了一个常量)附加到输出的字符串中,而不是附加 \n ,这将违反可移植性。

FileReader 是一个从文件中读取字符的便利类。它子类化 InputStreamReader ,它的构造函数调用 InputStreamReader(InputStream)。此类的一个实例等效于下面的代码片段:

FileInputStream fis = new FileInputStream(pathname);
InputStreamReader isr;
isr = new InputStreamReader(fis, System.getProperty("file.encoding"));

通常需要在文本文件中搜索特定字符串的出现。尽管正则表达式(在第十三章中讨论过)对于这个任务来说是理想的,但是我还没有讨论它们。因此,清单 11-21 给出了正则表达式的更冗长的替代方案。

清单 11-21 。查找包含与搜索字符串匹配的内容的所有文件

import java.io.BufferedReader;
import java.io.File;
import java.io.FileReader;
import java.io.IOException;

public class FindAll
{
   public static void main(String[] args)
   {
      if (args.length != 2)
      {
         System.err.println("usage: java FindAll start search-string");
         return;
      }
      if (!findAll(new File(args[0]), args[1]))
         System.err.println("not a directory");
   }

   static boolean findAll(File file, String srchText)
   {
      File[] files = file.listFiles();
      if (files == null)
         return false;
      for (int i = 0; i < files.length; i++)
         if (files[i].isDirectory())
            findAll(files[i], srchText);
         else
         if (find(files[i].getPath(), srchText))
            System.out.println(files[i].getPath());
      return true;
   }

   static boolean find(String filename, String srchText)
   {
      BufferedReader br = null;
      try
      {
         br = new BufferedReader(new FileReader(filename));
         int ch;
         outer_loop:
         do
         {
            if ((ch = br.read()) == −1)
               return false;
            if (ch == srchText.charAt(0))
            {
               for (int i = 1; i < srchText.length(); i++)
               {
                  if ((ch = br.read()) == −1)
                     return false;
                  if (ch != srchText.charAt(i))
                     continue outer_loop;
               }
               return true;
            }
         }
         while (true);
      }
      catch (IOException ioe)
      {
         System.err.println("I/O error: " + ioe.getMessage());
      }
      finally
      {
         if (br != null)
            try
            {
               br.close();
            }
            catch (IOException ioe)
            {
               assert false; // shouldn't happen in this context
            }
      }
      return false;
   }
}

清单 11-21 的 FindAll 类声明了 main() 、 findAll() 和 find() 类方法。

main() 验证命令行参数的数量,必须是两个。第一个参数标识了搜索在文件系统中的起始位置,用于构造一个文件对象。第二个参数指定搜索文本。 main() 然后将文件对象和搜索文本传递给 findAll() 来搜索包含该文本的所有文件。

递归的 findAll() 方法首先调用传递给该方法的文件对象上的 listFiles() ,获取当前目录下所有文件的名称。如果 listFiles() 返回 null,意味着文件对象没有引用现有的目录,则 findAll() 返回 false,并输出适当的错误消息。

对于返回列表中的每个名字, findAll() 或者在名字代表一个目录时递归调用自身,或者调用 find() 方法在文件中搜索文本;当文件包含此文本时,将输出文件的路径名字符串。

find() 方法首先通过 FileReader 类打开由其第一个参数标识的文件,然后将 FileReader 实例传递给 BufferedReader 实例,以提高文件读取性能。然后,它进入一个循环,继续从文件中读取字符,直到到达文件的末尾。

如果当前读取的字符与搜索文本中的第一个字符匹配,则进入一个内部循环,从文件中读取后续字符,并将它们与搜索文本中的后续字符进行比较。当所有字符都匹配时, find() 返回 true。否则,标记的 continue 语句用于跳过内部循环的剩余迭代,并将执行转移到标记的外部循环。在读取了最后一个字符后,仍然没有匹配结果, find() 返回 false。

既然你已经知道了 FindAll 的工作原理,你可能会想尝试一下。以下示例向您展示了如何在我的 Windows 7 平台上使用该应用:

java FindAll \prj\dev RenderScript

该示例在我的默认驱动器(C:)上的 \prj\dev 目录中搜索包含单词 RenderScript (区分大小写)的所有文件,并生成以下输出:

\prj\dev\ar2\appb\ar\1-4302-4614-5_Friesen_AppB_Android_Tools_Overview.doc
\prj\dev\ar2\appb\ce\1-4302-4614-5_Friesen_AppB_Android_Tools_Overview.doc
\prj\dev\ar2\ch08\1-4302-4614-5_Friesen_Ch08_Working_with_Android_NDK_and_Renderscript.doc
\prj\dev\ar2\ch08\ar\1-4302-4614-5_Friesen_Ch08_Working_with_Android_NDK_and_Renderscript.doc
\prj\dev\ar2\ch08\ce\1-4302-4614-5_Friesen_Ch08_Working_with_Android_NDK_and_Renderscript.doc
\prj\dev\ar2\ch08\code\GrayScale\GrayScale.java
\prj\dev\ar2\ch08\code\WavyImage\WavyImage.java
\prj\dev\ar2\code\ch08\GrayScale\GrayScale.java
\prj\dev\ar2\code\ch08\WavyImage\WavyImage.java
\prj\dev\ar2\xtra\ndkrs.txt
\prj\dev\EmbossImage\src\ca\tutortutor\embossimage\EmbossImage.java
\prj\dev\GrayScale\src\ca\tutortutor\grayscale\GrayScale.java
\prj\dev\WavyImage\src\ca\tutortutor\wavyimage\WavyImage.java

如果我现在指定 Java find all \ prj \ dev " Jelly Bean ",我会观察到以下简短的输出:

\prj\dev\ar2\ch01\1-4302-4614-5_Friesen_Ch01_Getting_Started_with_Android.doc
\prj\dev\ar2\ch01\ar\1-4302-4614-5_Friesen_Ch01_Getting_Started_with_Android.doc
\prj\dev\ar2\ch01\ce\1-4302-4614-5_Friesen_Ch01_Getting_Started_with_Android.doc

练习

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

  1. 文件类的用途是什么?
  2. 文件类的实例包含什么?
  3. 文件的 listRoots() 方法完成了什么?
  4. 什么是路径,什么是路径名?
  5. 绝对路径名和相对路径名有什么区别?
  6. 如何获得当前用户(也称为工作)目录?
  7. 定义父路径名。
  8. 文件的构造函数规范化它们的路径名参数。正常化是什么意思?
  9. 如何获得默认的名称分隔符?
  10. 什么是规范路径名?
  11. 文件的 getParent() 和 getName() 方法有什么区别?
  12. 是非判断:文件的 exists() 方法只决定一个文件是否存在。
  13. 什么是正常文件?
  14. 文件的 lastModified() 方法返回什么?
  15. 是非判断: File 的 list() 方法返回一个字符串的数组,其中每个条目都是一个文件名,而不是一个完整的路径。
  16. FilenameFilter 和 FileFilter 接口有什么区别?
  17. 是非判断: File 的 createNewFile() 方法不检查文件是否存在,并在文件不存在时在一个操作中创建文件,该操作相对于所有其他可能影响文件的文件系统活动是原子的。
  18. File 的 createTempFile(String,String) 方法在默认的临时目录下创建一个临时文件。你如何能找到这个目录?
  19. 当应用退出后不再需要临时文件时,应该将其删除(以避免弄乱文件系统)。当虚拟机正常结束时,如何确保临时文件被删除(它不会崩溃,也不会断电)?
  20. 如何准确比较两个文件对象?
  21. RandomAccessFile 类的用途是什么?
  22. 【rwd】【rws】模式参数的目的是什么?
  23. 什么是文件指针?
  24. 是非判断:当您调用 RandomAccessFile 的 seek(long) 方法来设置文件指针的值时,当该值大于文件的长度时,文件的长度发生变化。
  25. 定义平面文件数据库。
  26. 什么是溪流?
  27. OutputStream 的 flush() 方法的目的是什么?
  28. 是非判断: OutputStream 的 close() 方法自动刷新输出流。
  29. InputStream 的 mark(int) 和 reset() 方法的用途是什么?
  30. 如何访问一个 ByteArrayOutputStream 实例的内部字节数组的副本?
  31. 是非判断: FileOutputStream 和 FileInputStream 提供内部缓冲区来提高读写操作的性能。
  32. 为什么要用 PipedOutputStream 和 PipedInputStream ?
  33. 定义过滤器流。
  34. 两个流链接在一起意味着什么?
  35. 如何提高文件输出流或文件输入流的性能?
  36. DataOutputStream 和 DataInputStream 支援 FileOutputStream 和 FileInputStream ?
  37. 什么是对象序列化和反序列化?
  38. Java 支持哪三种形式的序列化和反序列化?
  39. 可序列化接口的用途是什么?
  40. 当序列化机制遇到一个类没有实现 Serializable 的对象时,它会怎么做?
  41. 找出 Java 不支持无限序列化的三个原因。
  42. 如何启动序列化?如何启动反序列化?
  43. 是非判断:类字段是自动序列化的。
  44. 瞬态保留字的目的是什么?
  45. 当反序列化机制试图反序列化一个类已更改的对象时,它会做什么?
  46. 反序列化机制如何检测到序列化对象的类发生了变化?
  47. 如何将实例字段添加到类中,并在反序列化添加实例字段之前已序列化的对象时避免麻烦?您可以使用什么 JDK 工具来帮助完成这项任务?
  48. 如何在不使用外部化的情况下定制默认的序列化和反序列化机制?
  49. 在序列化或反序列化附加数据项之前,如何告诉序列化和反序列化机制序列化或反序列化对象的正常状态?
  50. 外部化与默认和自定义序列化和反序列化有什么不同?
  51. 一个类如何表明它支持外部化?
  52. 是非判断:在外部化过程中,当反序列化机制没有检测到公共无参数构造函数时,它会抛出 InvalidClassException 和“无有效构造函数”消息。
  53. PrintStream 的 print() 和 println() 的方法有什么区别?
  54. PrintStream 的无参数 void println() 方法是做什么的?
  55. 为什么 Java 的流类不擅长流字符?
  56. 就字符 I/O 而言,Java 提供了什么作为流类的首选替代方案?
  57. 是非判断:读取器声明了一个可用()方法。
  58. OutputStreamWriter 类的用途是什么? InputStreamReader 类的目的是什么?
  59. 如何识别默认的字符编码?
  60. FileWriter 类的用途是什么? FileReader 类的目的是什么?
  61. 创建一个名为 Touch 的 Java 应用,用于将文件或目录的时间戳设置为当前时间。这个应用有以下用法语法:Java Touchpathname
  62. 通过使用 BufferedInputStream 和 BufferedOutputStream 来改进清单 11-8 的副本应用(性能方面)。 Copy 应该从缓冲输入流中读取要复制的字节,并将这些字节写入缓冲输出流。
  63. 创建一个名为 Split 的 Java 应用,用于将一个大文件拆分成多个较小的部分 x 文件(其中 x 从 0 开始递增;例如 part0 、 part1 、 part2 等等)。每个部分 x 文件(可能最后一个部分 x 文件除外,它保存剩余的字节)将具有相同的大小。这个应用有以下用法语法:Java Splitpathname。此外,您的实现必须使用 BufferedInputStream 、 BufferedOutputStream 、 File 、 FileInputStream 和 FileOutputStream 类。
  64. 从标准输入中读取文本行通常很方便,而 InputStreamReader 和 BufferedReader 类使这项任务成为可能。创建一个名为 CircleInfo 的 Java 应用,在获得一个链接到标准输入的 BufferedReader 实例后,该应用呈现一个循环,提示用户输入半径,将输入的半径解析为一个 double 值,并输出一对消息,根据该半径报告圆的周长和面积。

摘要

应用经常输入数据进行处理,并输出处理结果。数据从文件或其他来源输入,然后输出到文件或其他目的地。Java 通过位于 java.io 包中的经典 I/O API 支持 I/O。

文件 I/O 活动经常与文件系统交互。Java 通过其具体的 File 类提供对底层平台可用文件系统的访问。文件实例包含文件和目录的路径名,这些路径名可能存在于它们的文件系统中,也可能不存在。

可以打开文件进行随机访问,在随机访问中,可以混合进行写和读操作,直到文件关闭。Java 通过提供具体的 RandomAccessFile 类来支持这种随机访问。

Java 使用流来执行 I/O 操作。流是任意长度的有序字节序列。字节通过输出流从应用流向目的地,并通过输入流从源流向应用。

java.io 包提供了几个输出流和输入流类,它们是抽象的输出流和输入流类的后代。 BufferedOutputStream 和 FileInputStream 就是例子。

Java 的流类适合于字节流序列,但不适合于字符流序列,因为字节和字符是两回事,而且字节流不了解字符集和编码。

如果您需要流式传输字符,您应该利用 Java 的 writer 和 reader 类,它们被设计为支持字符 I/O(它们使用 char 而不是 byte )。此外,writer 和 reader 类考虑了字符编码。

java.io 包提供了几个 writer 和 reader 类,它们是抽象的 Writer 和 Reader 类的后代。 FileWriter 和 FileReader 就是例子。这些便利类基于文件输出/输入流和 output streamwriter/InputStreamReader。

本章主要关注文件系统环境中的 I/O。但是,您也可以在网络环境中执行 I/O。第十二章向你介绍几个 Java 的面向网络的 API。