Java编程思想 第十八章:Java I/O

  • I/O源端与之通信的接收端:文件、控制台、网络链接等。
  • 通信方式:顺序、随机存取、缓冲、二进制、按字符、按行、按字等。

1. File类

File(文件)既能代表一个特定文件名称,又能代表一个目录下的一组文件的名称。如果是文件集,可以对此集合调用list()方法,返回一个字符数组。

1.1 目录列表器

public class DirList {
    public static void main(String[] args) {
        File path = new File("./src/io");
        String[] list;
        if (args.length == 0) {
            list = path.list();
        } else {
            list = path.list(new DirFilter(args[0]));
        }
        Arrays.sort(list, String.CASE_INSENSITIVE_ORDER);
        for (String dirItem : list) {
            System.out.println(dirItem);
        }
    }
}

class DirFilter implements FilenameFilter {
    private Pattern pattern;

    public DirFilter(String regex) {
        pattern = Pattern.compile(regex);
    }

    @Override
    public boolean accept(File dir, String name) {
        return pattern.matcher(name).matches();
    }
} /*
DirectoryDemo.java
DirList.java
DirList2.java
DirList3.java
*/

DirFilter这个类存在的唯一原因就是accept()方法。创建这个类的目的在于把accept()方法提供给list()使用,使list()可以回调accept(),进而以决定哪些文件包含在列表中。因此,这种结构也常常成为回调。
accept()会使用一个正则表达式的matcher对象,来查看此正则表达式regex是否匹配这个文件的名字。通过accept,list()方法会返回一个数组。

1.2 目录实用工具

public final class Directory {
    public Directory() {
    }

    public static File[] local(File dir, final String regex) {
        return dir.listFiles(new FilenameFilter() {
            private Pattern pattern = Pattern.compile(regex);

            public boolean accept(File dir, String name) {
                return this.pattern.matcher((new File(name)).getName()).matches();
            }
        });
    }

    public static File[] local(String path, String regex) {
        return local(new File(path), regex);
    }

    public static Directory.TreeInfo walk(String start, String regex) {
        return recurseDirs(new File(start), regex);
    }

    public static Directory.TreeInfo walk(File start, String regex) {
        return recurseDirs(start, regex);
    }

    public static Directory.TreeInfo walk(File start) {
        return recurseDirs(start, ".*");
    }

    public static Directory.TreeInfo walk(String start) {
        return recurseDirs(new File(start), ".*");
    }

    static Directory.TreeInfo recurseDirs(File startDir, String regex) {
        Directory.TreeInfo result = new Directory.TreeInfo();
        File[] var6;
        int var5 = (var6 = startDir.listFiles()).length;

        for(int var4 = 0; var4 < var5; ++var4) {
            File item = var6[var4];
            if(item.isDirectory()) {
                result.dirs.add(item);
                result.addAll(recurseDirs(item, regex));
            } else if(item.getName().matches(regex)) {
                result.files.add(item);
            }
        }

        return result;
    }

    public static void main(String[] args) {
        if(args.length == 0) {
            System.out.println(walk("."));
        } else {
            String[] var4 = args;
            int var3 = args.length;

            for(int var2 = 0; var2 < var3; ++var2) {
                String arg = var4[var2];
                System.out.println(walk(arg));
            }
        }

    }

    public static class TreeInfo implements Iterable<File> {
        public List<File> files = new ArrayList();
        public List<File> dirs = new ArrayList();

        public TreeInfo() {
        }

        public Iterator<File> iterator() {
            return this.files.iterator();
        }

        void addAll(Directory.TreeInfo other) {
            this.files.addAll(other.files);
            this.dirs.addAll(other.dirs);
        }

        public String toString() {
            return "dirs: " + PPrint.pformat(this.dirs) + "\n\nfiles: " + PPrint.pformat(this.files);
        }
    }
}
  1. local()方法使用被称为listFile()的File.list()的变体来产生File数组。
  2. walk()方法将开始目录的名字转换为File对象,然后调用recurseDirs(),该方法将递归地遍历目录,并在每次递归中都手机更多的信息。为了区分普通文件和目录,返回值实际上是一个对象元组——一个List持有所有普通文件,另一个持有目录。
  3. TreeInfo.toString()方法使用了一个灵巧打印机类,一个可以添加新行并缩排所有元素的工具:

1.3 目录的检查以及创建

目录的检查:

  • f.getAbsolutePath() 返回File的绝对路径
  • f.canRead() 判断File是否可读
  • f.canWrite() 判断File是否可写
  • f.getName() 获取文件的名字
  • f.getParent() 获取父目录文件路径
  • f.getPath() 获取File路径
  • f.length() 获取File长度
  • f.lastModified() 获取文件上次被修改时间
  • f.exists 判断文件是否存在

文件的创建

  • File file=new File() 创建文件对象
  • file.createNewFile() 创建文件
  • file.mkdir() 创建目录
  • file.delete() 删除目录

2. 输入和输出

编程语言的I/O库经常使用流,它代表任何有能力产生数据的数据源对象或者有能力接受数据的接收端对象

Java类库中的I/O类分成输入和输出两部分。通过继承,任何自InputstreamReader派生而来的类都含有名为read的基本方法,用于读取单个字节或者字节数组。同样的OutputStream或者Writer派生出来的类都含有名为write的基本方法,用于写单个字节或者字节数组。但是不会用到,它们之所以存在是因为别的类可以使用它们以便提供更有用的接口。因此,很少使用单一的类来创建流对象,而是通过叠合多个对象来提供所期望的功能。

2.1 InputStream类型

InputStream的作用是用来表示从不同数据源产生输出的类,这些数据源包括:

  1. 字节数组
  2. String对象
  3. 文件
  4. 管道,工作方式与实际管道相似,即,从一段输入,从另一端输出
  5. 一个由其他种类的流组成的序列,以便可以将它们收集合并到一个流内
  6. 其他数据源,如Internet链接等

每一种数据源都有相应的InputStream子类。另外,FilterInputStream也属于一种InputStream,为装饰器类提供的基类,其中,装饰器类可以把属性或有用的接口与输入流连接在一起。

在这里插入图片描述

2.2 OutputStream类型

该类别的类决定了输出所要去往的目标:字节数组、文件或管道

另外,FilterOutputStream为装饰器类提供了一个基类,装饰器类把属性或者有用的接口与输出流连接了起来:

在这里插入图片描述

3. 添加属性和有用的接口

  • Java I/O类库里存在filter(过滤器)类的原因所在抽象类filter是所有装饰器类的基类。
  • FilterInputStream和FilterOutputStream是用来提供装饰类器接口以及控制特定输入流和输出流的两个类。FilterInputStream和FilterOutputStream分别自I/O类库中的基类InputStream和OutputStream派生而来,这两个类是装饰器的必要条件。

装饰者设计模式:动态的将功能附加到对象上,在对象扩展方面,它比继承更加有弹性。

3.1 通过FilterInputStream从InputStream读取数据

FilterInputStream类能够完成完全不同的事情,其中,DateInputStream允许读取不同的基本类型数据以及String对象。

其他FilterInputStream类则在内部修改InputStream的行为方式:是否缓冲,是否保留它所读过的行(允许查询行数或设置行数),以及是否把单一字符推回输入流。

在这里插入图片描述

3.2 通过FilterOutputStream向OutputStream导入

在这里插入图片描述

4. Reader和Writer

InputStream和OutputStream在以面向字节形式的IO中可以提供极有价值的功能,Reader和Writer(Java 1.1对基础IO流类库进行了重大修改,可能会以为是用来替换InputStream和OutputStream的)则提供兼容Unicode和面向字符的IO功能。

  1. Java 1.1向InputStream和OutputStream继承层次中添加了一些新类,所以这两个类不会被取代
  2. 有时必须把来自于字节层次结构中的类和字符层次中的类结合起来。为了实现这个目的,要用到适配器类:InputStreamReader可以吧InputStream转换为Reader,而OutputStreamWriter可以吧OutputStream转换为Writer。

适配器设计模式:将一个类的接口转换成客户端希望的另一个接口。

设计Reader和Writer继承层次结构只要是为了国际化。老的IO流继承层次结构仅支持8位字节流,并且不能很好地处理16位的Unicode字符。所以Reader和Writer继承层次结构就是为了在所有IO操作中都支持Unicode。

4.1 数据的来源和去处

在这里插入图片描述

##4.2 更改流的行为
对于InputStream和OutputStream来说,有装饰器子类来修改流以满足需要。Reader和Writer的类继承层次结构继续沿用相同的思想——但不完全相同。

在这里插入图片描述

无论何时使用readLine(),都不应该使用DataInputStream,而应该使用BufferedReader。
为了更容易地过渡到使用PrintWriter,它提供了一个既接受Writer对象又能接受任何OutputStream对象的构造器。PrintWriter的格式化接口实际上与PrintStream相同。

5. 自我独立的类:RandomAccessFile

RandomAccessFile适用于由大小已知的记录组成的文件,所以可以使用seek()将记录从一处转移到另一处,然后读取或者修改记录。文件中记录的大小不一定都相同,只要能够确定那些记录有多大以及它们在文件中的位置即可。

RandomAccessFile实现了DataInput和DataOutput接口,它是一个完全独立的类,从头开始编写其所有的方法(大多数都是本地的)。这么做是因为RandomccessFile拥有和别的I/O类型本质不同的行为,因为可以在一个文件内向前和向后移动。在任何情况下,它都是自我独立的,直接从Object派生而来。

方法getFilePointer()用于查找当前所处的文件位置,seek()用于在文件内移至新的位置,length()用于判断文件的最大尺寸。另外,其构造器还需要第二个参数(和C中的fopen()相同)用来指示我们只是“随机读”®还是“既读又写”(rw)。

6. I/O流的典型使用方式

6.1 缓冲输入文件

使用以String或File对象作为文件名的FileInputReader。为了提高速度,对文件进行缓冲,将所产生的引用传给一个BufferedReader构造器。

public class BufferedInputFile {
    public static String read(String filename) throws IOException {
        BufferedReader in = new BufferedReader(new FileReader(filename));
        String s;
        StringBuilder sb = new StringBuilder();
        while ((s = in.readLine()) != null) {
            sb.append(s + "\n");
        }
        // 调用close()关闭文件
        in.close();
        return sb.toString();
    }

    public static void main(String[] args) throws IOException {
        System.out.print(read("BufferedInputFile.java"));
    }
}

BufferedReader in = new BufferedReader(new FileReader(filename));

6.2 从内存输入

从BufferedInputFile.read()读入的String结果被用来创建一个StringReader。然后调用read()每次读取一个字符,并发送到控制台。

public class MemoryInput {
    public static void main(String[] args) throws IOException {
        StringReader in = new StringReader(BufferedInputFile.read("MemoryInput.java"));
        int c;
        while ((c = in.read()) != -1) {
            System.out.print((char) c);
        }
    }
}

6.3 格式化的内存输入

要读取格式化数据,可以使用DataInputStream,它是面向字节的IO类,因此必须用InputStream而不是Reader。

ublic class FormattedMemoryInput {
    public static void main(String[] args) throws IOException {
        try {
            DataInputStream in = new DataInputStream(new ByteArrayInputStream(BufferedInputFile.read("E:\\JAVA\\IdeaProjects\\untitled\\src\\io\\FormattedMemoryInput.java").getBytes()));
            while (true) {
                System.out.print((char) in.readByte());
            }
        } catch (EOFException e) {
            System.err.println("End of stream");
        }
    }
}

DataInputStream in = new DataInputStream(new ByteArrayInputStream);

DataInputStream in = new DataInputStream(new BufferedInputStream(new FileInputStream(“TestEOF.java”)));

6.4 基本文件输出

FileWriter对象可以向文件写入数据。通常会用BufferedWriter将其包装起来用以缓冲输出。

ublic class BasicFileOutput {
    static String file = "BasicFileOutput.out";

    public static void main(String[] args) throws IOException {
        BufferedReader in = new BufferedReader(new StringReader(
                BufferedInputFile.read("BasicFileOutput.java")));
        PrintWriter out = new PrintWriter(new BufferedWriter(new FileWriter(file)));
        int lineCount = 1;
        String s;
        while ((s = in.readLine()) != null) {
            out.println(lineCount++ + ": " + s);
        }
        out.close();

        System.out.println(BufferedInputFile.read(file));
    }

PrintWriter out = new PrintWriter(new BufferedWriter(new FileWriter(file)));

6.5 存储和恢复数据

PrintWriter可以对数据进行格式化,以便阅读。但是为了输出可供另一个流恢复的数据,需要用DataOutputStream写入数据,并用DataInputStream恢复数据。当然,这些流可以使用任何形式,下面示例使用的是一个文件,并且对于读和写进行了缓冲处理。注意DataOutputStream和DataInputStream是面向字节的,因此要使用InputStream和OutputStream。

public class StoringAndRecoveringData {
  public static void main(String[] args) throws IOException {
    DataOutputStream out = new DataOutputStream(
      new BufferedOutputStream(
        new FileOutputStream("Data.txt")));
    out.writeDouble(3.14159);
    out.writeUTF("That was pi");
    out.writeDouble(1.41413);
    out.writeUTF("Square root of 2");
    out.close();
    DataInputStream in = new DataInputStream(
      new BufferedInputStream(
        new FileInputStream("Data.txt")));
    System.out.println(in.readDouble());
    System.out.println(in.readUTF());
    System.out.println(in.readDouble());
    System.out.println(in.readUTF());
  }
}

DataOutputStream out = new DataOutputStream(new BufferedOutputStream(new FileOutputStream(“Data.txt”)));
DataInputStream in = new DataInputStream(new BufferedInputStream(new FileInputStream(“Data.txt”)));

6.6 读写随机访问文件

public class UsingRandomAccessFile {
    static String file = "rtest.dat";

    static void display() throws IOException {
        RandomAccessFile rf = new RandomAccessFile(file, "r");
        for (int i = 0; i < 7; i++) {
            System.out.println("Value " + i + ": " + rf.readDouble());
        }
        System.out.println(rf.readUTF());
        rf.close();
    }

    public static void main(String[] args) throws IOException {
        RandomAccessFile rf = new RandomAccessFile(file, "rw");
        for (int i = 0; i < 7; i++) {
            rf.writeDouble(i * 1.414);
        }
        rf.writeUTF("The end of the file");
        rf.close();
        display();
        rf = new RandomAccessFile(file, "rw");
        rf.seek(5 * 8);
        rf.writeDouble(47.0001);
        rf.close();
        display();
    }
}

RandomAccessFile rf = new RandomAccessFile(file, “rw”);

7. 文件读写的使用工具

读取文件:

BufferedReader in = new BufferedReader(new FileReader((new File(fileName)).getAbsoluteFile()));

写入文件:

PrintWriter out = new PrintWriter((new File(fileName)).getAbsoluteFile());

8. 标准IO

标准IO源自于Unix的“程序所使用的单一信息流”这一概念。程序的所有输入都可以来自于标准输入,所有输出都可以发送到标准输出。

8.1 从标准输入中读取

  • 标准输入:System.in未加工的InputStream
  • 标准输出:System.out PrintStream对象
  • 标准错误:System.err PrintStream对象

通常会用readLine()一次一行读取输入,将System.in包装城BufferedReader来使用,这要求必须用InputStreamReader把Sytem.in转换成Reader。System,in通常应该对它进行缓冲。

8.2 将System.out转换成PrintWriter

public class ChangeSystemOut {
  public static void main(String[] args) {
    PrintWriter out = new PrintWriter(System.out, true);
    out.println("Hello, world");
  }
} /*
Hello, world
*/

8.3 标准IO重定向

Java的System类提供了静态方法嗲用,以允许对标准输入输出和错误IO流进行重定向:

  • setIn(InputStream)
  • setOut(PrintStream)
  • setErr(PrintStream)

9.进程控制

Java内部执行其他操作系统的程序,并且控制这些程序输入输出,Java类库提供了执行这些操作的类。

想运行一个程序,向OSException.command()传递一个command字符串,它与在控制台上运行该程序所键入的命令相同。这个命令被传递给java,lang.ProcessBuilder构造器,然后产生的ProcessBuilder对象被启动:

public class OSExecute {
    public OSExecute() {
    }

    public static void command(String command) {
        boolean err = false;

        try {
            Process e = (new ProcessBuilder(command.split(" "))).start();
            BufferedReader results = new BufferedReader(new InputStreamReader(e.getInputStream()));

            String s;
            while((s = results.readLine()) != null) {
                System.out.println(s);
            }

            for(BufferedReader errors = new BufferedReader(new InputStreamReader(e.getErrorStream())); (s = errors.readLine()) != null; err = true) {
                System.err.println(s);
            }
        } catch (Exception var6) {
            if(command.startsWith("CMD /C")) {
                throw new RuntimeException(var6);
            }

            command("CMD /C " + command);
        }

        if(err) {
            throw new OSExecuteException("Errors executing " + command);
        }
    }
}

10. 新I/O

JDK 1.4的java.nio.*包中引入了新的IO类库,其目的在于提高速度。实际上,旧的IO包已经使用nio重新实现过,以便充分利用这种速度提高。

速度提高源自于所使用的结构更接近于操作系统执行IO的方式:通道和缓冲器

唯一直接与通道交互的缓冲器是ByteBuffer,可以存储未加工字节的缓冲器。java.nio.ByteBuffer是相当基础的类:通过稿子分配多少存储空间来创建一个ByteBuffer对象,并且还有一个方法选择集,用于以原始的字节形式或基本数据类型输出和读取数据。但是,没办法输出或读取对象,即使是字符串对象也不行。这种处理虽然很低级,但却正好,因为这是大多数草走系统中更有效的映射方式。
旧IO类库有三个类被修改了,用以产生FileChannel。这三个被修改类是FileInputStream、FileOutputStream以及用于既读又写的RandomAccessFile。这些都是字节操作流,与底层nio性质一致。Reader和Writer这些字符模式类不能用于产生通道;但是java.nio.channels.Channels类提供了使用方法,用于在通道中产生Reader和Writer。

getChannel()将会产生一个FileChannel。通道是一种相当基础的:可以向它传送用于读写的ByteBuffer,并且可以锁定文件的某些区域用于独占式访问。
使用warp()方法将已存在的字节数组包装到ByteBuffer中。
data.txt文件用RandomAccessFile被再次打开。注意可以在文件内随处移动FileChanel;这里,把它移动到最后,以便附加其他写操作。
对于只读访问,必须显式地使用静态的allocate()方法来分配ByteBuffer。nio的目的就是快速移动大量数据,因此ByteBuffer的大小显得尤为重要——实际上,使用1K可能比通常使用的小一点(必须通过实际运行应用程序来找到最佳尺寸)。甚至叨叨更高速度,使用allocateDirect(),以产生一个与操作系统有更高耦合性的直接缓冲器(但分配的开支会更大)。
一旦调用read()来告知FileChannel向ByteBuffer存储字节,就必须调用缓冲器上的flip(),让它做好让别人读取字节的准备。如果打算使用缓冲器执行进一步read()操作,也必须得使用clear()来为每个read()做好准备。

10.1 转换数据

在GetChannel.java中,必须每次只读取一个字节的数据,然后将每个byte类型强制转换成char类型。而java.nio.CharBuffer有一个toString方法:返回一个包含缓冲器中所有字符的字符串。

10.2 获取基本类型

尽管ByteBuffer只能保存字节类型数据,但是它可以从其所容纳的字节中产生出各种不同的基本类型值的方法

  • bb.asCharBuffer();
  • bb.asShortBuffer();
  • bb.asIntBuffer();
  • bb.asLongBuffer();
  • bb.asFloatBuffer();
  • bb.asDoubleBuffer();

10.3 视图缓冲器

视图缓冲器(view buffer)可以让我们通过某个特定的基本数据类型的视窗查看其底层的ByteBuffer。对视图的任何修改都会映射成为对ByteBuffer中数据的修改。

先用重载后的put()方法存储一个整数数组。接着get()和put()方法调用直接访问底层ByteBuffer中的某个整数位置。注意,这些通过直接与ByteBuffer对话访问绝对位置的方式也同样适用于基本类型。

一旦底层的ByteBuffer通过视图缓冲器填满了整数或其他基本类型时,就可以直接被写到通道中。正像从通道中读取那样容易,然后使用视图缓冲器可以把任何数据都转化为某一特定的基本类型。

10.4 用缓冲器操纵数据

在这里插入图片描述

此图阐明了nio类之间的关系,便于理解怎么移动和转换数据。如果想把一个字节数组写到文件中去,那么就应该使用ByteBuffer.wrap()方法把字节数组包装起来,然后用getChannel()方法在FileOutputStream上打开一个通道,接着将来自于ByteBuffer的数据写到FileChannel。
注意:BytBuffer是将数据移进移出通道的唯一方式,并且只能创建一个独立的基本类型缓冲器,或者使用as方法从ByteBuffer中获得。也就是说,不能把基本类型的缓冲器转换成ByteBuffer。

10.5 缓冲器的细节

Buffer有数据和可以高效地访问及操作这些数据的四个索引组成,mark(标记)、position(位置)、limit(界限)和capacity(容量)。

在这里插入图片描述

10.6 内存映射文件(MemoryMappedFile)

内存映射文件允许创建和修改因为太大而不能放入内存的文件。可以假定整个文件都放在内存中,而且可以完全把它当作非常大的数组访问

public class LargeMappedFiles {
    static int length = 0x8FFFFFF; // 128 MB
    public static void main(String[] args) throws Exception {
        MappedByteBuffer out =
                new RandomAccessFile("test.dat", "rw").getChannel()
                        .map(FileChannel.MapMode.READ_WRITE, 0, length);
        for (int i = 0; i < length; i++) {
            out.put((byte) 'x');
        }
        print("Finished writing");
        for (int i = length / 2; i < length / 2 + 6; i++) {
            printnb((char) out.get(i));
        }
    }
}

使用map()产生MappedByteBuffer,一个特殊类型的直接缓冲器,必须指定映射文件初始位置和映射区域长度。

MappedByteBuffer由ByteBuffer继承而来,因此它具有ByteBuffer的所有方法。
创建了128MB,这可能比操作系统所允许一次载入内存的空间大。但似乎可以一次访问整个文件。因为只有一部分文件放入了内存,其他被交换了出去。
底层操作系统的文件映射工具是用来最大化地提高性能的。

10.6.1 性能

nio实现后性能有所提高,但是映射文件访问往往可以更加显著地加快速度。

10.7 文件加锁

JDK 1.4引入了文件加锁机制,允许同步访问某个做为共享资源的文件。文件锁对其他的操作系统进程是可见的,因为Java的文件加锁直接映射到本地操作系统的加锁工具。

public class FileLocking {
  public static void main(String[] args) throws Exception {
    FileOutputStream fos= new FileOutputStream("file.txt");
    FileLock fl = fos.getChannel().tryLock();
    if(fl != null) {
      System.out.println("Locked File");
      TimeUnit.MILLISECONDS.sleep(100);
      fl.release();
      System.out.println("Released Lock");
    }
    fos.close();
  }
} /*
Locked File
Released Lock
*/

通过对FileChannel调用tryLock()或lock(),就可以获得整个文件的FileLock。SocketChannel、DatagramChannel和ServerSocketChannel不需要加锁,因为它们是从单进程实体继承而来,通常不在两个进程之间共享socket。tryLock()是非阻塞式的,它设法获取锁,如果不能得到(当其他一些进程已经持有相同的锁,并且不共享时),它将直接从方法调用返回。lock()是阻塞式的,它要阻塞进程直至锁可以获得,或调用lock()的线程中断,或调用lock()的通道关闭。使用FileLock.release()可以释放锁。
也可以对文件部分上锁:

tryLock(long position, long size, boolean shared)或者lock(long position, long size, boolean shared) 

其中加锁区域由size-position决定,第三个参数指定是否共享锁。
尽管无参的加锁方法将根据文件尺寸变化而变化,但是具有固定尺寸的锁不随文件变化而变化。
对于独占锁或者共享锁的支持必须有底层的操作系统提供。如操作系统不支持共享锁并未每一个请求都创建锁,那么它就会使用独占锁。锁的类型可以通过FileLock.isShared()进行查询。

11. 压缩

Java IO类库中的类支持读写压缩格式的数据流。 Java IO类库中的类支持读写压缩格式的数据流。

在这里插入图片描述

11.1 用GZIP进行简单压缩

如果相对单一数据流进行压缩,GZIP接口是比较合适的选择:

public class GZIPcompress {
    public static void main(String[] args) throws IOException {
        if (args.length == 0) {
            System.out.println(
                    "Usage: \nGZIPcompress file\n" +
                            "\tUses GZIP compression to compress " +
                            "the file to test.gz");
            System.exit(1);
        }
        BufferedReader in = new BufferedReader(
                new FileReader(args[0]));
        BufferedOutputStream out = new BufferedOutputStream(
                new GZIPOutputStream(
                        new FileOutputStream("test.gz")));
        System.out.println("Writing file");
        int c;
        while ((c = in.read()) != -1) {
            out.write(c);
        }
        in.close();
        out.close();
        System.out.println("Reading file");
        BufferedReader in2 = new BufferedReader(
                new InputStreamReader(new GZIPInputStream(
                        new FileInputStream("test.gz"))));
        String s;
        while ((s = in2.readLine()) != null) {
            System.out.println(s);
        }
    }
}

压缩:BufferedOutputStream <- GZIPOutputStream <- FileOutputStream
解压:BufferedReader <- InputStreamReader <- GZIPInputStream <- FileInputStream

11.2 用Zip进行多文件保存

支持Zip格式的Java库更加全面,它显示了用Checksum类来计算和校验文件的校验和的方法。一共有两种Checksum类型:Adler32(快)和CRC32(慢,准确)。

public class ZipCompress {
    public static void main(String[] args)
            throws IOException {
        FileOutputStream f = new FileOutputStream("test.zip");
        CheckedOutputStream csum =
                new CheckedOutputStream(f, new Adler32());
        ZipOutputStream zos = new ZipOutputStream(csum);
        BufferedOutputStream out =
                new BufferedOutputStream(zos);
        zos.setComment("A test of Java Zipping");
        // No corresponding getComment(), though.
        for (String arg : args) {
            print("Writing file " + arg);
            BufferedReader in =
                    new BufferedReader(new FileReader(arg));
            zos.putNextEntry(new ZipEntry(arg));
            int c;
            while ((c = in.read()) != -1) {
                out.write(c);
            }
            in.close();
            out.flush();
        }
        out.close();
        // Checksum valid only after the file has been closed!
        print("Checksum: " + csum.getChecksum().getValue());
        // Now extract the files:
        print("Reading file");
        FileInputStream fi = new FileInputStream("test.zip");
        CheckedInputStream csumi =
                new CheckedInputStream(fi, new Adler32());
        ZipInputStream in2 = new ZipInputStream(csumi);
        BufferedInputStream bis = new BufferedInputStream(in2);
        ZipEntry ze;
        while ((ze = in2.getNextEntry()) != null) {
            print("Reading file " + ze);
            int x;
            while ((x = bis.read()) != -1) {
                System.out.write(x);
            }
        }
        if (args.length == 1) {
            print("Checksum: " + csumi.getChecksum().getValue());
        }
        bis.close();
        // Alternative way to open and read Zip files:
        ZipFile zf = new ZipFile("test.zip");
        Enumeration e = zf.entries();
        while (e.hasMoreElements()) {
            ZipEntry ze2 = (ZipEntry) e.nextElement();
            print("File: " + ze2);
            // ... and extract the data as before
        }
    /* if(args.length == 1) */
    }
}

压缩:BufferedOutputStream <- ZIPOutputStream <- CheckedOutputStream <- FileOutputStream
解压: BufferedReader <- InputStreamReader <- ZIPInputStream <- CheckedInputStream <- FileInputStream

对于每一个要加入压缩档案的文件,都必须调用putNextEntry(),并将其传递给一个ZipEntry对象。ZipEntry包含一个功能很广泛的接口,允许获取和设置Zip文件内该特定项上所有可利用的数据:名字、压缩的和未压缩的文件大小、日期、CRC检验和、额外字段数据、注释、压缩方法以及他是否是一个目录入口等等。

11.3 Java档案文件

ip格式也被应用于JAR(Java ARchive,Java档案文件)文件格式中。将一组文件压缩到单个压缩文件中。同Java中任何其他东西一样,JAR也是跨平台的。由于采用压缩技术,可以使传输时间更短,只需向服务器发送一次请求即可。

Sun的JDK自带jar程序,可根据选择自动压缩文件:

jar [options] destination [manifest] inputfile(s) 

其中options只是一个字母几个:

在这里插入图片描述

  1. 创建一个名为myJarFile.jar的JAR文件,包含当前目录下所有类文件,以及自动产生的清单文件:
    jar cf myJarFile.jar *.class
  2. 下面的命令与前例类似,但添加了一个名为myManifestFile.mf的用户自建清单文件:
    jar cmf myJarFile.jar myHanifestFile.nf *.class
  3. 下面的命令会产生myJarFile.jar内所有文件的一个目录表:
    jar tf myJarFile.jar
  4. 下面的命令添加“v”(详尽)标志,可以提供有关myJarFle.jar中的文件的更详细的信息:
    jar. tvf myJarfile.jar
  5. 假定audio、classes和image是子目录, 下面的命令将所有子目录合并到文件myApp.jar中,其中也包括了“v” 标志。当jar程序运行时,该标志可以提供更详细的信息:
    jar cvf myApp.jar audio classes image
  6. 如果用0 (零)选项创建一个JAR文件,那么该文件就可放入类路径变量(CLASSPATH) 中:
    CLASSPATH=”lib1.jar;lib2.jar:”
    然后Java就可以在1lib1.jar和lib2.jar中搜索目标类文件了。

12 对象序列化

Java的对象序列化将那些实现了Serializable接口的对象转换成一个字节序列,并能够在以后将这个字节序列完全恢复为原来的对象。这一过程甚至可通过网络进行;这意味着序列化机制能自动弥补不同操作系统之间的差异。也就是说,可以在运行Windows系统的计算机上创建一个对象,将其序列化,通过网络将它发送给一台运行Unix系统的计算机,然后在那里谁确地重新组装,而却不必担心数据在不同机器上的表示会不同,也不必关心字节的顺序或者其他任何细节。

利用序列化可以实现轻量级持久性(lightweight persistence)。“持久性”意味着一个对象的生存周期并不取决于程序是否正在执行;它可以生存于程序的调用之间。

对象序列化的概念加入到语言中是为了支持两种主要特性。

  1. 一是Java的远程方法调用(Remote Method Invocation, RMI),它使存活于其他计算机上的对象使用起来就像是存活于本机上一样。
  2. 对Java Beans来说,对象的序列化也是必需的。使用一个Bean时,一般情况下是在设计阶段对它的状态信息进行配置。这种状态信息必须保存下来,并在程序启动时进行后期恢复,这种具体工作就是由对象序列化完成的。

只要对象实现了Serializable接口(该接口仅是一个标记接口,不包括任何方法),对象的序列化处理就会非常简单。当序列化的概念被加入到语言中时,许多标准库类都发生了改变,以便具备序列化特性一其中包括所有基 本数据类型的封装器、所有容器类以及许多其他的东西。甚至Class对象也可以被序列化。

序列化一个对象的步骤:

  1. 首先要创建某些OutputStream对象
  2. 然后将其封装在一个ObjectOutputStream对象内。
  3. 这时,只需调用writeObject0即可将对象序列化,并将其发送给OutputStream(对象化序列是基于字节的,因要使用InputStream和OutputStream继承层次结构)。
  4. 要反向进行该过程(即将-一个序列还原为一个对象),需要将个InputStream封装在ObjectmputStream内,然后调用readObject()。和往常一样,我们最后获得的是一个引用,它指向一个向上转型的Object,所以必须向下转型才能直接设置它们。

下面我们来看一个例子:

class Data implements Serializable {
    private int n;

    public Data(int n) {
        this.n = n;
    }

    @Override
    public String toString() {
        return Integer.toString(n);
    }
}

public class Worm implements Serializable {
    private static Random rand = new Random(47);
    private Data[] d = {
            new Data(rand.nextInt(10)),
            new Data(rand.nextInt(10)),
            new Data(rand.nextInt(10))
    };
    private Worm next;
    private char c;

    // Value of i == number of segments
    public Worm(int i, char x) {
        print("Worm constructor: " + i);
        c = x;
        if (--i > 0) {
            next = new Worm(i, (char) (x + 1));
        }
    }

    public Worm() {
        print("Default constructor");
    }

    @Override
    public String toString() {
        StringBuilder result = new StringBuilder(":");
        result.append(c);
        result.append("(");
        for (Data dat : d) {
            result.append(dat);
        }
        result.append(")");
        if (next != null) {
            result.append(next);
        }
        return result.toString();
    }

    public static void main(String[] args)
            throws ClassNotFoundException, IOException {
        Worm w = new Worm(6, 'a');
        print("w = " + w);
        ObjectOutputStream out = new ObjectOutputStream(
                new FileOutputStream("worm.out"));
        out.writeObject("Worm storage\n");
        out.writeObject(w);
        out.close(); // Also flushes output
        ObjectInputStream in = new ObjectInputStream(
                new FileInputStream("worm.out"));
        String s = (String) in.readObject();
        Worm w2 = (Worm) in.readObject();
        print(s + "w2 = " + w2);
        ByteArrayOutputStream bout =
                new ByteArrayOutputStream();
        ObjectOutputStream out2 = new ObjectOutputStream(bout);
        out2.writeObject("Worm storage\n");
        out2.writeObject(w);
        out2.flush();
        ObjectInputStream in2 = new ObjectInputStream(
                new ByteArrayInputStream(bout.toByteArray()));
        s = (String) in2.readObject();
        Worm w3 = (Worm) in2.readObject();
        print(s + "w3 = " + w3);
    }
} /*
Worm constructor: 6
Worm constructor: 5
Worm constructor: 4
Worm constructor: 3
Worm constructor: 2
Worm constructor: 1
w = :a(853):b(119):c(802):d(788):e(199):f(881)
Worm storage
w2 = :a(853):b(119):c(802):d(788):e(199):f(881)
Worm storage
w3 = :a(853):b(119):c(802):d(788):e(199):f(881)
*/

从输出上看,被还原后的对象确实包含了原对象中的所有链接。 在对一个Serializable对象进行还原的过程中,没有调用任何构造器,包含默认的构造器。整个对象都是通过InputStream中取得数据恢复过来的。

12.1 寻找类

将一个对象从它的序列化状态中恢复出来,哪些工作是必须的?

  • 打开和读取mystery对象中的内容都需要Alien的Class对象;Java虚拟机找不到Alien.class(除非它正好在类路径Classpath内,而本例不在类路径之内)。这样就会得到一个名叫ClassNotFoundException的异常(同样,除非能够验证Alien存在,否则它等于消失)。必须保证Java虚拟机能够找到相关的.class文件。

12.2 序列化的控制

public class Blip3 implements Externalizable {
    private int i;
    private String s; // No initialization

    public Blip3() {
        print("Blip3 Constructor");
        // s, i not initialized
    }

    public Blip3(String x, int a) {
        print("Blip3(String x, int a)");
        s = x;
        i = a;
        // s & i initialized only in non-default constructor.
    }

    @Override
    public String toString() {
        return s + i;
    }

    @Override
    public void writeExternal(ObjectOutput out)
            throws IOException {
        print("Blip3.writeExternal");
        // You must do this:
        out.writeObject(s);
        out.writeInt(i);
    }

    @Override
    public void readExternal(ObjectInput in)
            throws IOException, ClassNotFoundException {
        print("Blip3.readExternal");
        // You must do this:
        s = (String) in.readObject();
        i = in.readInt();
    }

    public static void main(String[] args) throws IOException, ClassNotFoundException {
        print("Constructing objects:");
        Blip3 b3 = new Blip3("A String ", 47);
        print(b3);
        ObjectOutputStream o = new ObjectOutputStream(
                new FileOutputStream("Blip3.out"));
        print("Saving object:");
        o.writeObject(b3);
        o.close();
        // Now get it back:
        ObjectInputStream in = new ObjectInputStream(new FileInputStream("Blip3.out"));
        print("Recovering b3:");
        b3 = (Blip3) in.readObject();
        print(b3);
    }
} /*
Constructing objects:
Blip3(String x, int a)
A String 47
Saving object:
Blip3.writeExternal
Recovering b3:
Blip3 Constructor
Blip3.readExternal
A String 47
*/

默认序列化机制并不难操纵。如果希望部分序列化或子对象不必序列化。可通过Exterbalizable接口代替Serializable。Exterbalizable接口继承了Serializable,同时增添了两个方法:writeExternal()和readExternal()。这两个方法会在序列化和反序列化还原的过程中被自动调用。

如果从一个Externalizable对象继承,通常需要调用基类版本的writeExternal()和readExternal()来为基类组件提供恰当的存储和恢复功能。为正常运行,不仅需要在writeExternal()方法(没有任何默认行为来为Externalizable 对象写入任何成员对象)中将来自对象的重要信息写入,还必须在readExternal()方法中恢复数据。因为Externalizable对象的默认构造器行为使其看起来似乎像某种自动发生的存储与恢复操作。但实际上并非如此。

Serializable对象与Externalizable对象的区别:

对于Serializable对象来说,对象完全以它存储的二进制位为基础进行构造,而不调用构造器。
对于Externalizable对象,所有的普通的默认构造器都会被调用(包括字段定义时的初始化),然后调用readeExternal()。必须注意:所有的默认构造器都被调用之后,才能使Externalizable对象产生正确的行为。

transient(瞬时)关键字

特定子对象不想让java的序列化自动保存与恢复,可以使用transient逐个字段地关闭序列化。

当对象被恢复时,transient的password域就会变成null。虽然toString()是用重载后的+运算符来连接String对象,但是null引用会被自动转换成字符串null。

Externalizable的替代方法

可以实现Serializable接口,并添加(非覆盖或实现)名为writeObject()和readObject()方法。这样一旦对象被序列化或者被反序列化还原,就会自动地分别调用这个方法。也就是说,只要提供这两个方法,就会使用它们而不是默认的序列化机制。
方法特征签名:

private void writeObject(ObjectOutputStream stream)throws IOException;
private void readObject(ObjectInputStream stream) throws IOException, ClassNotFoundException;

实际上并不是从这个类的其他方法中调用它们,而是ObjectOutputStream和ObjectInputStream对象的writeObject()和readObject()方法调用了对象的writeObject()和readObject()方法(类型信息章节展示了如何在类的外部访问private方法)。

还有另一个技巧,可在自己的writeObject()内部,调用defaultWriteObject()来选择执行默认的writeObject().

13 XML

对象序列化的一个重要限制是它只是Java的解决方案:只有Java程序才能反序列化这种对象。一种更具有互操作性的解决方案是将数据转换为XML格式,这样可以使其被各种各样的平台和语言使用。

开源XOM类库,Person类有一个getXML()方法,使用XOM来产生被转换过的XML的Element对象的Person数据。还有一个构造器,接受Element并从中抽取恰当的Person数据:

14 Preferences

Preferences API与对象序列化相比,前者与对象持久性更密切。因为它可以自动存储和读取信息。不过,它只能用于小的受限的数据集合——只能存储基本数据和字符串,并且每个字符串的存储长度不能超过8K。顾名思义,Preferences API用于存储和读取用户的偏好preferences以及程序配置项的设置。

Preferences是一个键-值集合,存储在一个节点层次结构中。

public class PreferencesDemo {
    public static void main(String[] args) throws Exception {
        Preferences prefs = Preferences
                .userNodeForPackage(PreferencesDemo.class);
        prefs.put("Location", "Oz");
        prefs.put("Footwear", "Ruby Slippers");
        prefs.putInt("Companions", 4);
        prefs.putBoolean("Are there witches?", true);
        int usageCount = prefs.getInt("UsageCount", 0);
        usageCount++;
        prefs.putInt("UsageCount", usageCount);
        for (String key : prefs.keys()) {
            print(key + ": " + prefs.get(key, null));
        }
        // You must always provide a default value:
        print("How many companions does Dorothy have? " +
                prefs.getInt("Companions", 0));
    }
} /*
Location: Oz
Footwear: Ruby Slippers
Companions: 4
Are there witches?: true
UsageCount: 53
How many companions does Dorothy have? 4
*/

这里用的是userNodeForPackage(),但我们也可以选择用systemNodeForPackage();虽然可以任意选择,但最好将“user” 用于个别用户的偏好,将“system”用于通用的安装配置。

get()的第二个参数,如果某个关键字下没有任何条目,那么这个参数就是所产生的默认值。当在一个关键字集合内迭代时,我们总要确信条目是存在的,因此用null作为默认值是安全的,但是通常我们会获得一个具名的关键字,就像下面这条语句:
prefs.getInt(“Companions”. 0)):

这样,在我们第一次运行程序时,UsageCount的值是0, 但在随后引用中,它将会是非零值。
在我们运行PreferencesDemo.java时,会发现每次运行程序时,UsageCount的值都会增加1,但是数据存储到哪里了呢?在程序第一次运行之后,并没有出现任何本地文件。Preferences API利用合适的系统资源完成了这个任务,并且这些资源会随操作系统不同而不同。例如在Windows里,就使用注册表(因为它已经有“键值对”这样的节点对层次结构了)。但是最重要的一点是,它已经神奇般地为我们存储了信息,所以我们不必担心不同的操作系统是怎么运作的。

猜你喜欢

转载自blog.csdn.net/qq_21125183/article/details/85063052