18 java I/O 系统

流的类继承结构

我们首先看看流的类继承结构,有助于理解下个标签的内容

InputStream

OutputStream

Reader

Writer

File类

File类技能表示一个特定文件的名称,又能代表一个目录下的一组文件的名称。如果它指的是一个文件集,我们就可以对此集合调用list()方法,这个方法会返回一个字符数组。如果我们想取得不同的目录列表,只需要再创建一个不同的File对象就可以了。实际上,FilePath(文件路径)对这个类来说是个更好的名字。

目录列表器

若我们调用不带参的list()方法,便得到File对象包含的全部列表。若我们想获得一个受限列表,如得到所有扩展名为.java的文件,就要用到“目录过滤器”。

public class DirList {
    public static void main(String[] args) {
        File path = new File(".");
        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);
    }

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

DirFilter类实现了FilenameFilter接口:

public interface FilenameFilter {
    boolean accept(File dir, String name);
}

DirFilter这个类存在的原因就是accept()方法。创建这个类的目的在于把accept()方法提供给list()使用,使list()可以回调accept(),进而决定哪些文件包含在列表中。这种结构常常被称为回调。更具体说,这是一个策略模式的例子。list()在提供服务时实现了基本功能,而FilenameFilter提供了这个策略,以便完善list()在提供服务时所需的算法。因为list()接受FilenameFilter对象作为参数,这意味着我们可以传递实现了FilenameFilter接口的任何类的对象,用以选择(甚至在运行时)list()方法的行为方式。策略的目的就是提供了代码行为的灵活性。

list()会为此目录下的每个文件名调用accept(),来判断该文件是否包含在内;判断结果由accept()返回的布尔值表示。

匿名内部类

上面的例子很适合用匿名内部类改进。首先创建一个filter()方法,返回一个纸箱FilenameFilter的引用:

public class DirList2 {
    public static FilenameFilter filter(final String regex) {
        // Creation of anonymous inner class:
        return new FilenameFilter() {
            private Pattern pattern = Pattern.compile(regex);

            public boolean accept(File dir, String name) {
                return pattern.matcher(name).matches();
            }
        }; // End of anonymous inner class
    }

    public static void main(String[] args) {
        File path = new File(".");
        String[] list;
        if (args.length == 0)
            list = path.list();
        else
            list = path.list(filter(args[0]));
        Arrays.sort(list, String.CASE_INSENSITIVE_ORDER);
        for (String dirItem : list)
            System.out.println(dirItem);
    }
} /* Output:
DirectoryDemo.java
DirList.java
DirList2.java
DirList3.java
*///:~

注意,传向filter()的参数必须是final的。这在匿名内部类中是必须的,这样它才能够使用来自该类范围之外的对象。

我们可以使程序变得更小一点,定义一个list()参数的匿名内部类:

public class DirList3 {
    public static void main(final String[] args) {
        File path = new File(".");
        String[] list;
        if (args.length == 0)
            list = path.list();
        else
            list = path.list(new FilenameFilter() {
                private Pattern pattern = Pattern.compile(args[0]);

                public boolean accept(File dir, String name) {
                    return pattern.matcher(name).matches();
                }
            });
        Arrays.sort(list, String.CASE_INSENSITIVE_ORDER);
        for (String dirItem : list)
            System.out.println(dirItem);
    }
}

上面的代码展示了匿名内部类怎样通过创建特定的、一次性的类解决问题。优点是将解决特定问题的代码聚拢在一起,缺点是不易阅读,应该慎用。

下面一个工具类即可以取得一个目录下的所有文件,也可以通过正则表达式筛选文件,挺有用:

public class SortedDirList {
    private File file;

    public SortedDirList(String dirPath) {
        file = new File(dirPath);
    }

    public List<String> list() {
        String[] list;
        if (null != file) {
            list = file.list();
            Arrays.sort(list, String.CASE_INSENSITIVE_ORDER);
            return Arrays.asList(list);
        }
        return new ArrayList<>();
    }

    /**
     * 筛选文件名符合正则表达式的文件
     * @param reg 正则表达式
     * @return
     */
    public List<String> list(final String reg) {
        String[] list;
        if (null != file) {
            list = file.list(
                new FilenameFilter() {
                    private Pattern pattern = Pattern.compile(reg);

                    @Override
                    public boolean accept(File dir, String name) {
                        return pattern.matcher(name).matches();
                    }
                }
            );
            Arrays.sort(list, String.CASE_INSENSITIVE_ORDER);
            return Arrays.asList(list);
        }
        return new ArrayList<>();
    }

    public static void main(String[] args) {
        SortedDirList sortedDirList = new SortedDirList(".");	//.代表当前目录
        System.out.println(sortedDirList.list());
        System.out.println(sortedDirList.list(".*\.txt"));
    }
}

目录实用工具

程序设计中一项常见任务就是在文件集上执行操作,这些文件要么在本地目录中,要么遍布于整个目录树中。下面的工具类通过local()方法残生由本地目录中的文件构成的File对象数组,或通过walk()方法产生给定目录下的由整个目录树中所有文件构成的List<File>

public final class 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 pattern.matcher(
                        new File(name).getName()).matches();
            }
        });
    }

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

    // A two-tuple for returning a pair of objects:
    public static class TreeInfo implements Iterable<File> {
        public List<File> files = new ArrayList<File>();
        public List<File> dirs = new ArrayList<File>();

        // The default iterable element is the file list:
        public Iterator<File> iterator() {
            return files.iterator();
        }

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

        public String toString() {
            return "dirs: " + PPrint.pformat(dirs) +
                    "

files: " + PPrint.pformat(files);
        }
    }

    public static TreeInfo
    walk(String start, String regex) { // Begin recursion
        return recurseDirs(new File(start), regex);
    }

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

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

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

    static TreeInfo recurseDirs(File startDir, String regex) {
        TreeInfo result = new TreeInfo();
        for (File item : startDir.listFiles()) {
            if (item.isDirectory()) {
                result.dirs.add(item);
                result.addAll(recurseDirs(item, regex));
            } else // Regular file
                if (item.getName().matches(regex))
                    result.files.add(item);
        }
        return result;
    }

    // Simple validation test:
    public static void main(String[] args) {
        if (args.length == 0)
            System.out.println(walk("."));
        else
            for (String arg : args)
                System.out.println(walk(arg));
    }
} ///:~

local()方法使用被称为listFile()的File.list()的变体来残生File数组。

walk()方法将开始目录的名字转换为File对象,然后调用recurseDirs(),该方法将递归地遍历目录,并在每次递归中都收集更多的信息。为了区分普通文件盒目录,返回值实际上是一个对象“元组”——一个List持有所有普通文件,另一个持有目录。这里,所有的域都被有意识地设置成了public,因为TreeInfo的使命只是将对象收集起来。

TreeInfo.toString()是为了更容易浏览结果。代码可以在网上下载:https://download.csdn.net/download/m0_37293461/10683933

public class PPrint {
    public static String pformat(Collection<?> c) {
        if (c.size() == 0) return "[]";
        StringBuilder result = new StringBuilder("[");
        for (Object elem : c) {
            if (c.size() != 1)
                result.append("
  ");
            result.append(elem);
        }
        if (c.size() != 1)
            result.append("
");
        result.append("]");
        return result.toString();
    }

    public static void pprint(Collection<?> c) {
        System.out.println(pformat(c));
    }

    public static void pprint(Object[] c) {
        System.out.println(pformat(Arrays.asList(c)));
    }
}

我们可以更进一步,创建一个工具,它可以在目录中穿行,通过Strategy对象来处理这些目录中的文件(这是策略设计模式的另一个示例):

public class ProcessFiles {
    public interface Strategy {
        void process(File file);
    }

    private Strategy strategy;
    private String ext;

    public ProcessFiles(Strategy strategy, String ext) {
        this.strategy = strategy;
        this.ext = ext;
    }

    public void start(String[] args) {
        try {
            if (args.length == 0)
                processDirectoryTree(new File("."));
            else
                for (String arg : args) {
                    File fileArg = new File(arg);
                    if (fileArg.isDirectory())
                        processDirectoryTree(fileArg);
                    else {
                        // Allow user to leave off extension:
                        if (!arg.endsWith("." + ext))
                            arg += "." + ext;
                        strategy.process(
                                new File(arg).getCanonicalFile());
                    }
                }
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }

    public void
    processDirectoryTree(File root) throws IOException {
        for (File file : Directory.walk(
                root.getAbsolutePath(), ".*\." + ext))
            strategy.process(file.getCanonicalFile());
    }

    // Demonstration of how to use it:
    public static void main(String[] args) {
        new ProcessFiles(new ProcessFiles.Strategy() {
            public void process(File file) {
                System.out.println(file);
            }
        }, "java").start(args);
    }
} 

Strategy接口内嵌在ProcessFiles中,使得如果你希望实现它,就必须实现ProcessFiles.Strategy。

目录的检查与创建

File不仅仅只代表存在的文件或目录。可以用来创建新目录或不存在的整个目录路径。还可以检查文件特性(大小,最后修改日期,读/写),检查整个File对象是一个文件还是一个目录,并可以删除文件。下面展示了一些File类的方法:

private static void fileData(File f) {
    System.out.println(
            "Absolute path: " + f.getAbsolutePath() +
                    "
 Can read: " + f.canRead() +
                    "
 Can write: " + f.canWrite() +
                    "
 getName: " + f.getName() +
                    "
 getParent: " + f.getParent() +
                    "
 getPath: " + f.getPath() +
                    "
 length: " + f.length() +
                    "
 lastModified: " + f.lastModified());
    if (f.isFile())
        System.out.println("It's a file");
    else if (f.isDirectory())
        System.out.println("It's a directory");
}

输入与输出

编程语言的I/O类库中常使用这个抽象概念,“流”屏蔽了实际的I/O设备中处理数据的细节。

java类库中的I/O类分为输入和输出两部分,可以在JDK文档里的类层次结构中看到。

InputStream类型

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

  • 字节数组。
  • String对象。
  • 文件。
  • “管道”,工作方式与实际管道相似,即,从一端输入,从另一端输出
  • 一个由其他种类的流组成的序列,以便我们可以将它们手机和并到一个流内
  • 其他数据源,如Internet连接等
功能 构造器参数/如何使用
ByteArrayInputStream 允许内存的缓冲区当作InputStream使用 缓冲区,字节将从中取出/ 作为一种数据源:将其与FilterInputStream对象相连以提供有用接口
StringBufferInputStream 将String转换成InputStream 字符串,底层实现实际使用StringBuffer/ 作为一种数据源:将其与FilterInputStream对象相连以提供有用接口
FileInputStream 用于从文件中读取信息 字符串,表示文件名、文件或FileDescriptor对象/ 作为一种数据源:将其与FilterInputStream对象相连以提供有用接口
PipedInputStream 产生用于写入相关PipedOutputStream的数据,实现“管道化”概念 作为多线程中数据源:将其与FilterInput-Stream对象相连以提供有用接口
SequenceInputStream 将两个或多个InputStream对象转换成单一InputStream 两个InputStream对象或一个容纳InputStream对象的容器Enumeration/ 作为一种数据源:将其与FilterInputStream对象相连以提供有用接口

OutputStream类型

改类别的类决定了输出所要去往的目标:字节数组()

添加属性和有用的接口

装饰器在第15章引入。java I/O类库需要多种不同功能的组合,这正是使用装饰器模式的理由。

装饰器提供了相当多的灵活性(我们可以很容易地混合和匹配属性),但是它同时增加了代码的复杂性。java I/O类库操作不便的原因在于:我们必须创建许多类——“核心”I/O类型加上所有的装饰器,才能得到我们所希望的单个I/O对象。

FilterInputStream和FilterOutputStream是用来提供装饰器类接口以控制特定输入流和输出流的两个类,它们分别由InputStream和OutputStream派生而来,这两个类是装饰器的必要条件。

Reader和Writer

InputStream和OutputStreamt在面向字节形式的I/O中提供极有价值的功能,Reader和Writer则提供兼容Unicode与面向字符的I/O功能。

有时我们必须把来自“字节”层次结构中的类和“字符”层次结构中的类结合起来使用,就要用到“适配器”类:InputStreamReader可以把InputStream转换为Reader,而OutputStreamWriter可以把OutputStream转换为Writer。

数据的来源和去处

几乎所有原始的java I/O流都有相应的Reader和Writer来提供Unicode操作。但也有需要InputStream和OutputStream的情况,比如java.util.zip类库就是面向字节的。我们可以先尝试使用面向字符的,不行再用面向字节的。

更改流的行为

对于InputStream和OutputStream来说,我们会使用FilterInputStream和FilterOutputStream的修饰器子类来修改“流”以满足特殊需要。Reader和Writer的类继承层次结构继承沿用相同的思想——但是并不完全相同。

无论我们何时使用readLine(),都不应该使用DataInputStream(会遭到编译器强烈反对),而应该使用BufferedReader。除了这一点DataInputStream仍是I/O类库的首选成员。

自我独立的类:RandomAccessFile

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

除了实现DataInput和DataOutput接口,它和InputStream或OutputStream没有任何关联,它是一个完全独立的类。这么做是因为RandomAccessFile拥有和别的I/O类型本质不同的行为,因为我们可以在一个文件内向前或向后移动。

I/O流的典型使用方式

尽管可以通过不同的方式组合I/O流类,但我们可能也就只能用到其中的几种组合。下面的例子可以作为经典的I/O用法的基本参考。

缓冲输入文件(BufferedReader)

如果想要打开一个文件用于字符输入,可以使用以String或File对象作为文件名的FileInputReader,为了提高速度。为了提高速度,我们洗完归队那个文件进行缓冲,那么我们将所产生的引用传给一个BufferedReader构造器。由于BufferedReader也提供readLine()方法,所以这是我们的最终对象和进行读取的接口。当readLine()将返回null时,你就达到了文件的末尾。

public class BufferedInputFile {
    // Throw exceptions to console:
    public static String
    read(String filename) throws IOException {
        // Reading input by lines:
        BufferedReader in = new BufferedReader(
                new FileReader(filename));
        String s;
        StringBuilder sb = new StringBuilder();
        while ((s = in.readLine()) != null)
            sb.append(s + "
");
        in.close();
        return sb.toString();
    }

    public static void main(String[] args)
            throws IOException {
        System.out.print(read("D:\workspace\thinking-In-Java-master\src\main\java\io\BufferedInputFile.java"));
    }
} 

字符串sb用来积累文件的全部内容(包括必须添加的换行符,因为readLine()已将它们删掉了)。最后,调用close()关闭文件。

从内存输入

下面的示例中,从BufferedInputFile.read()读入的String结果被用来创建一个StringReader。然后调用read()每次读取一个字符。

public class MemoryInput {
    public static void main(String[] args)
            throws IOException {
        StringReader in = new StringReader(
                BufferedInputFile.read("D:\workspace\thinking-In-Java-master\src\main\java\io\MemoryInput.java"));
        int c;
        while ((c = in.read()) != -1)
            System.out.print((char) c);
    }
}

格式化的内存输入

要读取格式化数据,可以使用DataInputStream,它是一个面向字节的I/O类(不是面向字符的)。因此我们必须使用InputStream类而不是Reader类。

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

必须为ByteArrayInputStream提供字节数组,为了产生改数组String包含了一个可以实现此项工作的getBytes()方法。

如果我们从DataInputStream用readByte()一次一个字节地读取字符,那么任何字节的值都是合法的结果,因此返回值不能用来检测输入是否结束。我们可以用arailable()方法查看还有多少克供存取的字符。下面例子演示了如何一次一个字节读取文件。

public class TestEOF {
    public static void main(String[] args)
            throws IOException {
        DataInputStream in = new DataInputStream(
                new BufferedInputStream(
                        new FileInputStream("D:\workspace\thinking-In-Java-master\src\main\java\io\TestEOF.java")));
        while (in.available() != 0)
            System.out.print((char) in.readByte());
    }
} /* (Execute to see output) *///:~

基本的文件输出

FileWriter对象可以向文件写入数据。实际上,我们通常会用BufferedWriter将其包装起来用以缓冲输出(尝试移除此包装来感受对性能的印象——缓冲旺旺能显著增加I/O性能)

public class BasicFileOutput {
    static String file = "D:\BasicFileOutput.out";

    public static void main(String[] args)
            throws IOException {
        BufferedReader in = new BufferedReader(
                new StringReader(
                        BufferedInputFile.read("D:\workspace\thinking-In-Java-master\src\main\java\io\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();	//1
        // Show the stored file:
        System.out.println(BufferedInputFile.read(file));
    }
} 

如果不为所有的输出文件调用close(),就会发现缓冲区内容不会被刷新清空。

文本文件输出的快捷方式

java se5在PrintWriter中添加了一个辅助构造器,使得你不必在每次希望创建文本文件并向其中写入时,都去执行所有的装饰工作。

public class FileOutputShortcut {
    static String file = "FileOutputShortcut.out";

    public static void main(String[] args)
            throws IOException {
        BufferedReader in = new BufferedReader(
                new StringReader(
                        BufferedInputFile.read("FileOutputShortcut.java")));
        // Here's the shortcut:
        PrintWriter out = new PrintWriter(file);
        int lineCount = 1;
        String s;
        while ((s = in.readLine()) != null)
            out.println(lineCount++ + ": " + s);
        out.close();
        // Show the stored file:
        System.out.println(BufferedInputFile.read(file));
    }
} /* (Execute to see output) *///:~

存储和恢复数据(DataOutputStream/DataInputStream)

PrintWriter可以对数据进行格式化,以便人们阅读。但是为了输出可供另一个“流”恢复数据,我们需要用DataOutputStream写入数据,并用DataInputStream恢复数据,它们都是面向字节的。

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());
        // Only readUTF() will recover the
        // Java-UTF String properly:
        System.out.println(in.readUTF());
        System.out.println(in.readDouble());
        System.out.println(in.readUTF());
    }
} /* Output:
3.14159
That was pi
1.41413
Square root of 2
*///:~

如果我们使用DataOutputStream写入数据,java保证我们可以使用用DataInputStream准确地读取数据——无论读与写的平台多么不同。这很重要!

读写随机访问文件

使用RandomAccessFile,类似于组合使用了DataInputStream和DataOutputStream(因为它是实现了相同的接口:DataInput和DataOutput)。另外我们可以看到,利用seek()可以在文件中到处移动,并修改文件中的某个值。

在使用RandomAccessFile,必须知道排版,才能正确操作它。

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);		//double是8字节长度,查找第5个双精度,所以5*8
        rf.writeDouble(47.0001);
        rf.close();
        display();
    }
} 

display()方法打开了一个文件,并以double值的形式显示了其中的七个元素。因为double总是8字节长度,为了用seek()查找第五个双精度值,需用5*8来产生查找位置。

管道流

属于多线程,在21章再讲。

文件读写的实用工具

一个很常见的任务化程序是读取文件到内存。修改,然后再写出。而java I/O缺少这些常用操作的功能。

public class TextFile extends ArrayList<String> {
    // Read a file as a single string:
    public static String read(String fileName) {
        StringBuilder sb = new StringBuilder();
        try {
            BufferedReader in = new BufferedReader(new FileReader(
                    new File(fileName).getAbsoluteFile()));
            try {
                String s;
                while ((s = in.readLine()) != null) {	//readLine()会将换行符去掉,所以读出来的需要重新加上
                    sb.append(s);
                    sb.append("
");
                }
            } finally {
                in.close();
            }
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
        return sb.toString();
    }

    // Write a single file in one method call:
    public static void write(String fileName, String text) {
        try {
            PrintWriter out = new PrintWriter(
                    new File(fileName).getAbsoluteFile());
            try {
                out.print(text);
            } finally {
                out.close();
            }
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }

    // Read a file, split by any regular expression:
    public TextFile(String fileName, String splitter) {
        super(Arrays.asList(read(fileName).split(splitter)));
        // Regular expression split() often leaves an empty
        // String at the first position:
        if (get(0).equals("")) remove(0);
    }

    // Normally read by lines:
    public TextFile(String fileName) {
        this(fileName, "
");
    }

    public void write(String fileName) {
        try {
            PrintWriter out = new PrintWriter(
                    new File(fileName).getAbsoluteFile());
            try {
                for (String item : this)
                    out.println(item);
            } finally {
                out.close();
            }
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }

    // Simple test:
    public static void main(String[] args) {
        String file = read("TextFile.java");
        write("test.txt", file);
        TextFile text = new TextFile("test.txt");
        text.write("test2.txt");
        // Break into unique sorted list of words:
        TreeSet<String> words = new TreeSet<String>(
                new TextFile("TextFile.java", "\W+"));
        // Display the capitalized words:
        System.out.println(words.headSet("a"));
    }
}

read()将每行添加到StringBuffer,并且为每行加上换行符,因为在读的过程中换行符会被去除掉。接着返回一个包含整个文件的字符串。write()打开文本并将其写入文件。

注意,在任何打开文件的代码中的finally子句中,都要添加对文件的close()调用。

因为这个类希望将读取和写入文件的过程简单化,因此所有的IOException都被转型为RuntimeException(可不被捕获)。

读取二进制文件

这个工具与TextFile类似,它简化了读取二进制文件的过程:

public class BinaryFile {
    public static byte[] read(File bFile) throws IOException {
        BufferedInputStream bf = new BufferedInputStream(
                new FileInputStream(bFile));
        try {
            byte[] data = new byte[bf.available()];
            bf.read(data);
            return data;
        } finally {
            bf.close();
        }
    }

    public static byte[]
    read(String bFile) throws IOException {
        return read(new File(bFile).getAbsoluteFile());
    }
} ///:~

标准I/O

从标准输入中读取

在标准I/O模型中,java提供了System.in、System.out和System.err。System.out和System.err都事先包装成了printStream对象,这样我们可以立即使用System.out和System.err。但是System.in并未对其包装。

System.in和大多数流一样,通常应对其进行缓冲:

public class Echo {
    public static void main(String[] args)
            throws IOException {
        BufferedReader stdin = new BufferedReader(
                new InputStreamReader(System.in));
        String s;
        while ((s = stdin.readLine()) != null && s.length() != 0)
            System.out.println(s);
        // An empty line or Ctrl-Z terminates the program
    }
} ///:~

将System.out转换成PrintWriter

System.out是一个PrintStream,而PrintStream是一个OutputStream。PrintWriter有一个可以接受OutputStream作为参数的构造器。

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

标准I/O重定向

java的System类提供了一些简单静态方法调用,以允许我们对标准输入、输出和错误I/O流进行重定向:

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

如果我们突然开始在显示器上创建大量输出,而这些输出滚得太快导致无法阅读时,重定向输出就显得极为有用。对于我们想重复测试某个特定用户的输入序列的命令程序来说,重定向输入就很有价值:

public class Redirecting {
    public static void main(String[] args)
            throws IOException {
        PrintStream console = System.out;
        BufferedInputStream in = new BufferedInputStream(
                new FileInputStream("D:\workspace\thinking-In-Java-master\src\main\java\io\Redirecting.java"));
        PrintStream out = new PrintStream(
                new BufferedOutputStream(
                        new FileOutputStream("test.out")));
      
      	//1 注意,这里重定向到自定义的in、out。
        System.setIn(in);
        System.setOut(out);		
        System.setErr(out);
        BufferedReader br = new BufferedReader(
                new InputStreamReader(System.in));
        String s;
        while ((s = br.readLine()) != null)
            System.out.println(s);
        out.close(); // Remember this!
        System.setOut(console);
    }
}

在1中,System.in和System.out都重定向为自定义的in、out。I/O重定向操纵的是字节流,而不是字符流。

新I/O

所使用的结构更接近于操作系统执行I/O的方式:通道和缓冲器

唯一直接与通道交互的缓冲器是ByteBuffer——也就是说,可以存储未加工字节的缓冲器。

在旧I/O类库中有三个类被修改了,用以产生FileChannel:FileInputStream、FileOutputStream、RandomAccessFile(既读又写),这些都是字节操作的。Reader和Writer这种字符模式类不能用于产生通道,但Channels类提供了实用方法,用来在通道中产生Reader和Writer。

下面例子演示了三种流,产生可读、可读可写及可读的通道。

public class GetChannel {
    private static final int BSIZE = 1024;

    public static void main(String[] args) throws Exception {
        // Write a file:
        FileChannel fc =
                new FileOutputStream("data.txt").getChannel();	//1
        fc.write(ByteBuffer.wrap("Some text ".getBytes()));
        fc.close();
        // Add to the end of the file:
        fc = new RandomAccessFile("data.txt", "rw").getChannel();	//2
        fc.position(fc.size()); 	// 移动到文件最后位置
        fc.write(ByteBuffer.wrap("Some more".getBytes()));
        fc.close();
        // Read the file:
        fc = new FileInputStream("data.txt").getChannel();	//3
        ByteBuffer buff = ByteBuffer.allocate(BSIZE);
        fc.read(buff);
        buff.flip();
        while (buff.hasRemaining())
            System.out.print((char) buff.get());
    }
} /* Output:
Some text Some more
*///:~

对于这里所展示的任何流类,getChannel都会产生一个FileChannel(通道)。一旦调用read()来告知FileChannel向ByteBuffer存储字节,就必须调用缓冲器上的flip(),让它做好让别人读取字节的准备。

public class ChannelCopy {
    private static final int BSIZE = 1024;

    public static void main(String[] args) throws Exception {
        if (args.length != 2) {
            System.out.println("arguments: sourcefile destfile");
            System.exit(1);
        }
        FileChannel
                in = new FileInputStream(args[0]).getChannel(),
                out = new FileOutputStream(args[1]).getChannel();
        ByteBuffer buffer = ByteBuffer.allocate(BSIZE);
        while (in.read(buffer) != -1) {
            buffer.flip(); // Prepare for writing
            out.write(buffer);
            buffer.clear();  // Prepare for reading
        }
    }
}

转换数据

回头看GetChannel.java这个程序会发现,为了输出文件中的信息,我们必须每次只读取一个字节的数据,然后将每个byte类型强制转换成char类型。缓冲器容纳的是普通的字节,为了把它们转换成字符,要么在将其从缓冲器输出时对它们进行解码。可以使用java.nio.charset.Charset类实现这些功能,该类提供了把数据编码成多种不同类型的字符集工具:

获取基本类型

尽管ByteBuffer只能保存字节类型的数据,但是它具有可以从其所容纳的字节中产生出各种不同基本类型值的方法。rewind()方法是为了返回数据开始部分。

public class GetData {
    private static final int BSIZE = 1024;

    public static void main(String[] args) {
        ByteBuffer bb = ByteBuffer.allocate(BSIZE);
        // Allocation automatically zeroes the ByteBuffer:
        int i = 0;
        while (i++ < bb.limit())
            if (bb.get() != 0)
                print("nonzero");
        print("i = " + i);
        bb.rewind();
        // Store and read a char array:
        bb.asCharBuffer().put("Howdy!");
        char c;
        while ((c = bb.getChar()) != 0)
            printnb(c + " ");
        print();
        bb.rewind();
        // Store and read a short:
        bb.asShortBuffer().put((short) 471142);
        print(bb.getShort());
        bb.rewind();
        // Store and read an int:
        bb.asIntBuffer().put(99471142);
        print(bb.getInt());
        bb.rewind();
        // Store and read a long:
        bb.asLongBuffer().put(99471142);
        print(bb.getLong());
        bb.rewind();
        // Store and read a float:
        bb.asFloatBuffer().put(99471142);
        print(bb.getFloat());
        bb.rewind();
        // Store and read a double:
        bb.asDoubleBuffer().put(99471142);
        print(bb.getDouble());
        bb.rewind();
    }
} 

在分配一个ByteBuffer之后,可以通过检查它的值来查看缓冲器的分配方式是否将其内容自动置0——它确实这样做了。这里一共检测了1024个值(由缓冲器的limit()决定),并且所有的值都是0.

向ByteBuffer插入基本类型数据最简单的方法是:利用asCharBuffer()、asShortBuffer()获得该缓冲器上的视图,然后再用视图的put()方法。只有asShortBuffer()需要转换类型使用put,其余都可直接put。

视图缓冲器

视图缓冲器可以让我们通过某个特定的基本数据类型的视窗查看其底层的ByteBuffer。ByteBuffer是实际存储数据的地方,因此,对视图的任何修改都会映射称为对ByteBuffer中数据的修改。视图允许我们从ByteBuffer一次一个地或成批(放入数组中)读取基本类型值。下面是IntBuffer操纵ByteBuffer中的int型数据:

public class IntBufferDemo {
    private static final int BSIZE = 1024;

    public static void main(String[] args) {
        ByteBuffer bb = ByteBuffer.allocate(BSIZE);
        IntBuffer ib = bb.asIntBuffer();
        // Store an array of int:
        ib.put(new int[]{11, 42, 47, 99, 143, 811, 1016});
        // Absolute location read and write:
        System.out.println(ib.get(3));
        ib.put(3, 1811);
        // Setting a new limit before rewinding the buffer.
        ib.flip();
        while (ib.hasRemaining()) {
            int i = ib.get();
            System.out.println(i);
        }
    }
} 

用put()方法存储一整个数组,接着get()和put()调用直接访问底层ByteBuffer中的某个整数位置。

public class ViewBuffers {
    public static void main(String[] args) {
        ByteBuffer bb = ByteBuffer.wrap(
                new byte[]{0, 0, 0, 0, 0, 0, 0, 'a'});
        bb.rewind();
        printnb("Byte Buffer ");
        while (bb.hasRemaining())
            printnb(bb.position() + " -> " + bb.get() + ", ");
        print();
        CharBuffer cb =
                ((ByteBuffer) bb.rewind()).asCharBuffer();
        printnb("Char Buffer ");
        while (cb.hasRemaining())
            printnb(cb.position() + " -> " + cb.get() + ", ");
        print();
        FloatBuffer fb =
                ((ByteBuffer) bb.rewind()).asFloatBuffer();
        printnb("Float Buffer ");
        while (fb.hasRemaining())
            printnb(fb.position() + " -> " + fb.get() + ", ");
        print();
        IntBuffer ib =
                ((ByteBuffer) bb.rewind()).asIntBuffer();
        printnb("Int Buffer ");
        while (ib.hasRemaining())
            printnb(ib.position() + " -> " + ib.get() + ", ");
        print();
        LongBuffer lb =
                ((ByteBuffer) bb.rewind()).asLongBuffer();
        printnb("Long Buffer ");
        while (lb.hasRemaining())
            printnb(lb.position() + " -> " + lb.get() + ", ");
        print();
        ShortBuffer sb =
                ((ByteBuffer) bb.rewind()).asShortBuffer();
        printnb("Short Buffer ");
        while (sb.hasRemaining())
            printnb(sb.position() + " -> " + sb.get() + ", ");
        print();
        DoubleBuffer db =
                ((ByteBuffer) bb.rewind()).asDoubleBuffer();
        printnb("Double Buffer ");
        while (db.hasRemaining())
            printnb(db.position() + " -> " + db.get() + ", ");
    }
} 

ByteBuffer通过一个被“包装”过的8字节数组产生,然后通过各种不同的基本类型的视图缓冲器显示出来。我们可以从下图看到不同类型的缓冲器读取时,数据的显示方式:

字节存放次序

不同的机器可能会使用不同的字节排序方式来存储数据。

  • 高位优先:将最重要的字节存放在地址最低的存储器单元。
  • 低位优先:将最重要的字节放在地址最高的存储器单元。

当字节大于一个字节时,就要考虑字节的顺序问题。ByteBuffer是以高位优先的形式存储数据的。可以使用带参数的ByteOrder.BIG_ENDIAN或ByteOrder.LITTLE_ENDIAN的order()方法改变ByteBuffer的字节排序方式。

用缓冲器操纵数据

下面的图阐明了nio类之间的关系,便于我们理解怎么移动和转换数据。

注意:ByteBuffer是将数据移进移出融到的唯一方式,并且我们只能创建一个独立的基本类型的缓冲器,或者使用“as”从ByteBuffer中获得。

缓冲器的细节

Buffer由数据和可以高效地访问及操纵这些数据的四个索引组成,四个索引:mark(标记),position(位置),limit(界限)和capacity(容量)。下面是操作四个索引的方法:

下面是一个简单使用缓冲器的示例:

public class UsingBuffers {
    private static void symmetricScramble(CharBuffer buffer) {
        while (buffer.hasRemaining()) {
            buffer.mark();
            char c1 = buffer.get();
            char c2 = buffer.get();
            buffer.reset();
            buffer.put(c2).put(c1);
        }
    }

    public static void main(String[] args) {
        char[] data = "UsingBuffers".toCharArray();
        ByteBuffer bb = ByteBuffer.allocate(data.length * 2);
        CharBuffer cb = bb.asCharBuffer();
        cb.put(data);
        print(cb.rewind());
        symmetricScramble(cb);
        print(cb.rewind());
        symmetricScramble(cb);
        print(cb.rewind());
    }
} /* Output:
UsingBuffers
sUniBgfuefsr
UsingBuffers
*///:~

在本例中使用的是底层的ByteBuffer,下面是symmetricScramble方法中缓冲器的样子:

position指向缓冲器的第一个元素,capacity和limit指向最后一个元素。

在symmetricScramble方法中,迭代执行while循环知道position等于limit,当调用缓冲器相对的get()或put()函数,position指针就会随之相应改变。也可以调用带有参数的get()和put()方法,带参数的get()和put()函数不会改变缓冲器的position指针。

当操纵while循环时,使用mark()调用来设置mark的值:

两个相对的get()调用后,缓冲器如下:

为了实现交换,可以使用绝对的(带参的)put()来实现,在程序中使用reset()将position的值设置为mark的值,然后两个put()先写c2,再写c1实现交换。

在下一次循环迭代期间,将mark设置成position的当前值:

当再一次调用symmetricScramble()功能时,会对CharBuffer进行同样的处理,并将其恢复到初始状态。

内存映射文件

内存映射文件允许我们创建和修改那些因为太大而不能放入内存的文件。有了内存映射文件,我们就可以假定整个文件都放在内存中,而且可以完全把它当作非常大的数组访问。这种方法极大地简化了用于修改文件的代码。例子:

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);	//1
        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));
    }
}

为了能读能写,我们使用RandomAccessFile来获取文件通道,然后调用map()产生映射。我们必须制定初始位置和映射区域长度。这意味我们可以映射某个文件的较小部分。

MappedByBuffer由ByteBuffer继承而来,因此它具有ByteBuffer的所有方法。

性能

下面程序进行了简单的性能比较:

public class MappedIO {
    private static int numOfInts = 4000000;
    private static int numOfUbuffInts = 200000;

    private abstract static class Tester {
        private String name;

        public Tester(String name) {
            this.name = name;
        }

        public void runTest() {
            System.out.print(name + ": ");
            try {
                long start = System.nanoTime();
                test();
                double duration = System.nanoTime() - start;
                System.out.format("%.2f
", duration / 1.0e9);
            } catch (IOException e) {
                throw new RuntimeException(e);
            }
        }

        public abstract void test() throws IOException;
    }

    private static Tester[] tests = {
            new Tester("Stream Write") {
                public void test() throws IOException {
                    DataOutputStream dos = new DataOutputStream(
                            new BufferedOutputStream(
                                    new FileOutputStream(new File("temp.tmp"))));
                    for (int i = 0; i < numOfInts; i++)
                        dos.writeInt(i);
                    dos.close();
                }
            },
            new Tester("Mapped Write") {
                public void test() throws IOException {
                    FileChannel fc =
                            new RandomAccessFile("temp.tmp", "rw")
                                    .getChannel();
                    IntBuffer ib = fc.map(
                            FileChannel.MapMode.READ_WRITE, 0, fc.size())
                            .asIntBuffer();
                    for (int i = 0; i < numOfInts; i++)
                        ib.put(i);
                    fc.close();
                }
            },
            new Tester("Stream Read") {
                public void test() throws IOException {
                    DataInputStream dis = new DataInputStream(
                            new BufferedInputStream(
                                    new FileInputStream("temp.tmp")));
                    for (int i = 0; i < numOfInts; i++)
                        dis.readInt();
                    dis.close();
                }
            },
            new Tester("Mapped Read") {
                public void test() throws IOException {
                    FileChannel fc = new FileInputStream(
                            new File("temp.tmp")).getChannel();
                    IntBuffer ib = fc.map(
                            FileChannel.MapMode.READ_ONLY, 0, fc.size())
                            .asIntBuffer();
                    while (ib.hasRemaining())
                        ib.get();
                    fc.close();
                }
            },
            new Tester("Stream Read/Write") {
                public void test() throws IOException {
                    RandomAccessFile raf = new RandomAccessFile(
                            new File("temp.tmp"), "rw");
                    raf.writeInt(1);
                    for (int i = 0; i < numOfUbuffInts; i++) {
                        raf.seek(raf.length() - 4);
                        raf.writeInt(raf.readInt());
                    }
                    raf.close();
                }
            },
            new Tester("Mapped Read/Write") {
                public void test() throws IOException {
                    FileChannel fc = new RandomAccessFile(
                            new File("temp.tmp"), "rw").getChannel();
                    IntBuffer ib = fc.map(
                            FileChannel.MapMode.READ_WRITE, 0, fc.size())
                            .asIntBuffer();
                    ib.put(0);
                    for (int i = 1; i < numOfUbuffInts; i++)
                        ib.put(ib.get(i - 1));
                    fc.close();
                }
            }
    };

    public static void main(String[] args) {
        for (Tester test : tests)
            test.runTest();
    }
} /* Output: (90% match)
Stream Write: 0.56
Mapped Write: 0.12
Stream Read: 0.80
Mapped Read: 0.07
Stream Read/Write: 5.32
Mapped Read/Write: 0.02
*///:~

上面的测试中,runTest()是一种模板方法。测试的结果显然说明,即使建立映射文件的花费很大,但是整体受益比起I/O流来说还是显著的。

文件加锁

jdk1.4引入了文件加锁机制,它允许我们同步访问某个作为共享资源的文件。竞争同一文件 的两个线程可能在不同的java虚拟机上,也可能一个是java线程,另一个是操作系统的本地线程。文件锁对其他的操作系统进程是可见的,因为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();
    }
} 

通过对FileChannel调用tryLock()或lock(),就可以获得整个文件的FileLock。(Socket-Channel、DatagramChannel和ServerSocketChannel不需要加锁,因为它们是从单进程实体继承而来;我们通常不在两个进程之间共享网络socket)。

tryLock()是非阻塞式的,它设法获取锁,但是如果不能获得(当其他一些进程已经持有相同的锁,并且不共享时),它将直接从方法调用返回。

lock()则是阻塞的,它要阻塞进程直至锁可以获得,或调用lock()线程中断,或调用lock()通道关闭。使用FileLock.release()可以释放锁。

可以对文件的一部分上锁:

public abstract FileLock tryLock(long position, long size, boolean shared)
    throws IOException;

或:

public abstract FileLock lock(long position, long size, boolean shared)
    throws IOException;

其中,加锁的区域由size-position决定。第三个参数指定是否共享锁。

无参的锁根据文件尺寸变化而变化,而固定尺寸的锁不随文件尺寸变化而变化。

对独占锁或共享锁的支持必须由底层的操作系统提供。若操作系统不支持共享锁并为每个请求都创建一个锁,那么它就会使用独占锁。锁的类型(共享或独占)可通过FileLock.isShared()进行查询。

对映射文件的部分加锁

如前所述,我们可能需要对巨大的文件的一部分加锁,以便其他进程可修改文件未加锁的部分。

下面例子中有两个线程,分别加锁不同部分:

public class LockingMappedFiles {
    static final int LENGTH = 0x8FFFFFF; // 128 MB
    static FileChannel fc;

    public static void main(String[] args) throws Exception {
        fc =
                new RandomAccessFile("test.dat", "rw").getChannel();
        MappedByteBuffer out =
                fc.map(FileChannel.MapMode.READ_WRITE, 0, LENGTH);
        for (int i = 0; i < LENGTH; i++)
            out.put((byte) 'x');
        new LockAndModify(out, 0, 0 + LENGTH / 3);
        new LockAndModify(out, LENGTH / 2, LENGTH / 2 + LENGTH / 4);
    }

    private static class LockAndModify extends Thread {
        private ByteBuffer buff;
        private int start, end;

        LockAndModify(ByteBuffer mbb, int start, int end) {
            this.start = start;
            this.end = end;
            mbb.limit(end);
            mbb.position(start);
            buff = mbb.slice();	//1`
            start();
        }

        public void run() {
            try {
                // Exclusive lock with no overlap:
                FileLock fl = fc.lock(start, end, false);
                System.out.println("Locked: " + start + " to " + end);
                // Perform modification:
                while (buff.position() < buff.limit() - 1)
                    buff.put((byte) (buff.get() + 1));
                fl.release();
                System.out.println("Released: " + start + " to " + end);
            } catch (IOException e) {
                throw new RuntimeException(e);
            }
        }
    }
}

在1`处,LockAndModify创建了缓冲区和用于修改的slice(),然后在run()中,获得文件通道上的锁(不能获得缓冲器上的锁,只能获取通道的锁)。lock()使部分文件具有独占访问权。

如果有java虚拟机,它会自动释放锁,或者关闭加锁的通道。不过也可以显示地FileLock调用release()来释放锁。

压缩

java I/O类库支持压缩格式的数据流,可以用它们对其他I/O类进行封装,以提供压缩功能。

这些类属于InputStream和OutputStream继承层次结构的一部分(属字节操作)。有时我们可能被迫要混合使用两种类型的数据流

尽管有许多压缩算法,但是最Zip和GZIP是最常用的。

用GZIP进行简单压缩

GZIP接口非常简单,适合对单个数据流(而不是一系列互异数据)进行压缩,那么它可能是比较适合的选择。

public class GZIPcompress {
    public static void main(String[] args)
            throws IOException {
        if (args.length == 0) {
            System.out.println(
                    "Usage: 
GZIPcompress file
" +
                            "	Uses 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")));	//1
        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"))));	//2
        String s;
        while ((s = in2.readLine()) != null)
            System.out.println(s);
    }
} /* (Execute to see output) *///:~

压缩类的使用非常直观——直接将输出流封装成GZIPOutputStream或ZipOutputStream,将输入流封装成GZIPInputStream或ZipInputStream即可(例如1、2处)。其他操作就是通常的I/O读写。这个例子把面向字符与面向字节的流混合了起来,输入(in)用Reader类,而GZIPOutputStream的构造器只能接受OutputStream对象,不能接受Writer对象,在打开文件时,GZIPInputStream可以转换成Reader。

用Zip进行多文件保存

支持Zip格式的java库更全面。利用该库可以保存多个文件,它甚至有一个独立的类,使得读取Zip文件更方便。可以用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));    //1
            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());	//3
        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");	//2
        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) */
    }
} 

对于每一个要加入压缩档案的文件,都必须调用putNextEntry(),并将其传递给一个ZipEntry对象。ZipEntry对象包含了一个功能很广泛的接口,允许你获取和设置Zip文件内特定项上所有可利用的数据:名称、压缩的和未压缩的文件大小、日期、CRC校验和、额外字段数据、注释、压缩方法以及它是否是一个目录入口等等。但是,java的Zip不支持设置密码、java的ZipEntry只支持CRC的校验和、而限制了不能使用速度更快的Adler32。

为了能够解压缩文件,ZipInputStream提供了一个getNextEntry()方法返回下一个ZipEntry(如果存在的话)。解压缩文件有一个更简便的方法(如2所示)——利用ZipFile对象读取文件。该对象有一个entries()方法用来向ZipEntries返回一个Enumeration(枚举)。

为了读取校验和,在第3处,必须拥有对与之相关联的Checksum对象的访问权限。

对象序列化

当你创建对象时,只要需要,它就会一直存在,但是在程序终止时,无论如何它都不会继续存在。如果对象能在程序不运行的情况下仍能存在并保留其信息,那将非常有用。这样,在下一次运行程序时,该对象将被重建并且拥有的信息与在程序上次运行时它所拥有的信息相同。这样使对象声明为“持久性”的比存入数据库的方式要方便得多。

java的对象序列化将那些实现了Serializable接口的对象转换成一个字节序列,并能够在以后将这个字节序列完全恢复到原来的对象。这一过程甚至可通过网络进行,不必担心系统间的差异(如win可发送到Unix上重新组装)。

可实现“轻量级持久性”。“持久性”意味着一个对象的生命周期不取决于程序是否执行,它可以生存于程序的调用之间。通过将一个序列化对象写入磁盘,然后再重新调用程序时恢复该对象,就能够实现持久性的效果。

对象序列化的概念加入到语言中是为了支持两种主要特性。一是:java的远程方法调用,它使存活于其他计算机上的对象使用起来就像是存活于本机上一样。二是:对Java Beans来说,对象的序列化是必须的。一般情况下是在设计阶段对它的状态信息进行配置。这种状态信息必须保存下来,并在程序启动时进行后期恢复;这种具体工作就是由对象序列化完成的。

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

对象序列化不仅保存了对象的“全景图”,而且能追踪对象内所包含的所有引用。

class Data implements Serializable {
    private int n;

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

    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");
    }

    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
");	//1
        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
");
        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);
    }
} 

如上例所示,真正的序列化过程非常简单。一旦从另外某个流创建了ObjectOutputStream,writeObject()就会将对象序列化。也可以为一个String调用writeObject()(如1)。

从输出中可以看出,被还原后的对象确实包含了原对象中的所有连接。

注意到对一个Serializable对象进行还原的过程中,没有调用任何构造器,包括默认的构造器。整个对象都是通过InputStream中取得数据恢复而来的。

通俗一点:持久化的作用就是在对象存储后(无论是存储在文件还是磁盘上),即使注释掉下例中的部分,单单in的部分也能从文件或磁盘中把对象还原成原来的样子:

public class TestSerializable {
    public static void main(String[] args) throws IOException, ClassNotFoundException {
//        SeriaB seriaB = new SeriaB(1);
//        SeriaA seriaA = new SeriaA(seriaB);
//
//        //文件
//        ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("test.out"));
//        out.writeObject(seriaA);
//        out.close();
//        System.out.println("seriaA = " + seriaA);

        ObjectInputStream in = new ObjectInputStream(new FileInputStream("test.out"));
        SeriaA a = (SeriaA)in.readObject();
        System.out.println("a = " + a);
        System.out.println("b = " + a.getSeriaB().getI());
    }
}

class SeriaA implements Serializable {
    private SeriaB seriaB;

    public SeriaA(SeriaB seriaB) {
        this.seriaB = seriaB;
    }

    public SeriaB getSeriaB() {
        return seriaB;
    }
}

class SeriaB implements Serializable {
    private int i;

    public SeriaB(int i) {
        this.i = i;
    }

    public int getI() {
        return i;
    }
}

寻找类

有哪些工作是必须用到序列化的呢?

public class Alien implements Serializable {} ///:~
public class FreezeAlien {
    public static void main(String[] args) throws Exception {
        ObjectOutput out = new ObjectOutputStream(
                new FileOutputStream("X.file"));
        Alien quellek = new Alien();
        out.writeObject(quellek);
    }
} ///:~
public class ThawAlien {
    public static void main(String[] args) throws Exception {
        ObjectInputStream in = new ObjectInputStream(
                new FileInputStream(new File("X.file")));
        Alien mystery = (Alien)in.readObject();
        System.out.println(mystery);
    }
} /* Output:
class Alien
*///:~

打开文件盒读取mystery对象中的内容都需要Alien的Class对象;若没有Alien对象,则会报类型转换异常的错误。必须保证能在java虚拟机找到相关的.class文件。

序列化的控制——Externalizable

默认的序列化不难操纵,但是如果有特殊需求呢?如,也许要考虑安全问题,不希望对象的某一部分被序列化;或者一个对象被还原以后,某子对象需要重新创建,从而不必讲该对象序列化。

解决这些特殊情况,可通过实现Externalizable——代替实现Serializable接口来对序列化过程进行控制。Externalizable接口继承了Serializable,同时增加了两个方法:writeExternal()和readExternal()。这两个方法会在序列化和反序列化还原过程中自动调用,以便执行一些特殊操作。

class Blip1 implements Externalizable {
    public Blip1() {
        print("Blip1 Constructor");
    }

    public void writeExternal(ObjectOutput out)
            throws IOException {
        print("Blip1.writeExternal");
    }

    public void readExternal(ObjectInput in)
            throws IOException, ClassNotFoundException {
        print("Blip1.readExternal");
    }
}

class Blip2 implements Externalizable {
    Blip2() {
        print("Blip2 Constructor");
    }

    public void writeExternal(ObjectOutput out)
            throws IOException {
        print("Blip2.writeExternal");
    }

    public void readExternal(ObjectInput in)
            throws IOException, ClassNotFoundException {
        print("Blip2.readExternal");
    }
}

public class Blips {
    public static void main(String[] args)
            throws IOException, ClassNotFoundException {
        print("Constructing objects:");
        Blip1 b1 = new Blip1();
        Blip2 b2 = new Blip2();
        ObjectOutputStream o = new ObjectOutputStream(
                new FileOutputStream("Blips.out"));
        print("Saving objects:");
        o.writeObject(b1);
        o.writeObject(b2);
        o.close();
        // Now get them back:
        ObjectInputStream in = new ObjectInputStream(
                new FileInputStream("Blips.out"));
        print("Recovering b1:");
        b1 = (Blip1) in.readObject();
        // OOPS! Throws an exception:
//! print("Recovering b2:");
//! b2 = (Blip2)in.readObject();
    }
}

Blip1的构造器是public的,但Blip2却不是,必须将Blip2的构造器变成public的,注释部分才不会出现异常,因为Serializable不调用构造器(完全以它存储的二进制为基础来构造的),Externalizable会调用所有普通的默认构造器(包括字段定义时的初始化)。然后调用readExternal()。

下面的例子示范了如何完整保存和恢复一个Externalizable对象:

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.
    }

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

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

    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);
    }
} 

其中,字段s和i只在第二个构造器中初始化,而不是在默认的构造器中初始化。这意味着假如不在readExternal()中初始化s和i,s就会为null,i就会为零。

我们如果从一个Externalizable对象继承,通常需要调用基类版本的writeExternal()和readExternal()来为基类组件提供恰当的存储和恢复功能。

transient(瞬时)关键字

在序列化控制中,可能某个特定子对象不想让java序列化机制自动保存和恢复。如果子对象表示的是我们不希望将其序列化的敏感信息(如密码),通常就会面临这些情况。即使是私有的,经序列化,也可以通过读取文件或拦截网络传输得到它。

有两个办法解决:

  • 将类实现为Externalizable
  • 若操作的是一个Serializable,可以用transient关键字逐个关闭序列化,它的意思是“不用麻烦你保存或回复数据——我自己会处理”

若某个Login对象保存某个特定登陆会话信息。登陆的合法性通过校验后,想把数据保存起来,但不包括密码,最简单是实现Serializable,并将密码字段标记为transient:

public class Logon implements Serializable {
    private Date date = new Date();
    private String username;
    private transient String password;  //1

    public Logon(String name, String pwd) {
        username = name;
        password = pwd;
    }

    public String toString() {
        return "logon info: 
   username: " + username +
                "
   date: " + date + "
   password: " + password;
    }

    public static void main(String[] args) throws Exception {
        Logon a = new Logon("Hulk", "myLittlePony");
        print("logon a = " + a);
        ObjectOutputStream o = new ObjectOutputStream(
                new FileOutputStream("Logon.out"));
        o.writeObject(a);
        o.close();
        TimeUnit.SECONDS.sleep(1); // Delay
        // Now get them back:
        ObjectInputStream in = new ObjectInputStream(
                new FileInputStream("Logon.out"));
        print("Recovering object at " + new Date());
        a = (Logon) in.readObject();
        print("logon a = " + a);
    }
}

从上例中可以看出:password声明为transient,所以不会被持久化。Externalizable对象在默认情况下不保存它们的任何字段,因此transient只能和Serializable对象一起使用。

Externalizable的替代方法

替代方法:可以实现Serializable接口,并添加(而非“覆盖”或者“实现”)名为writeObject()和readObject()的方法。这样一旦对象被序列化或者被反序列化还原,就会自动分别调用两个方法。

这些方法必须具有准确的方法特征签名:

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

注意上面的方法是private的,它们不是Serializable接口的一部分,而是会检查所传递的Serializable对象,看看是否实现了对象自己的writeObject()或readObject(),若有,则跳过正常的序列化执行它自己的writeObject或readObject,还有一个技巧,在你的writeObject()内部,调用defaultWriteObject来执行默认的writeObject。

public class SerialCtl implements Serializable {
    private String a;
    private transient String b;

    public SerialCtl(String aa, String bb) {
        a = "Not Transient: " + aa;
        b = "Transient: " + bb;
    }

    public String toString() {
        return a + "
" + b;
    }

    private void writeObject(ObjectOutputStream stream)
            throws IOException {
        System.out.println("in the writeObject..........");
        stream.defaultWriteObject();
        stream.writeObject(b);
    }

    private void readObject(ObjectInputStream stream)
            throws IOException, ClassNotFoundException {
        stream.defaultReadObject();		//1
        b = (String) stream.readObject();	//2
    }

    public static void main(String[] args)
            throws IOException, ClassNotFoundException {
        SerialCtl sc = new SerialCtl("Test1", "Test2");
        System.out.println("Before:
" + sc);
        ByteArrayOutputStream buf = new ByteArrayOutputStream();
        ObjectOutputStream o = new ObjectOutputStream(buf);
        o.writeObject(sc);
        // Now get it back:
        ObjectInputStream in = new ObjectInputStream(
                new ByteArrayInputStream(buf.toByteArray()));
        SerialCtl sc2 = (SerialCtl) in.readObject();
        System.out.println("After:
" + sc2);
    }
}

在1处,调用defaultReadObject执行more的writeObject,a被写入,而2处将transient修饰的b读出来,所以a和b都有值。

使用“持久性”

class House implements Serializable {
}

class Animal implements Serializable {
    private String name;
    private House preferredHouse;

    Animal(String nm, House h) {
        name = nm;
        preferredHouse = h;
    }

    public String toString() {
        return name + "[" + super.toString() +
                "], " + preferredHouse + "
";
    }
}

public class MyWorld {
    public static void main(String[] args)
            throws IOException, ClassNotFoundException {
        House house = new House();
        List<Animal> animals = new ArrayList<Animal>();
        animals.add(new Animal("Bosco the dog", house));
        animals.add(new Animal("Ralph the hamster", house));
        animals.add(new Animal("Molly the cat", house));
        print("animals: " + animals);
        ByteArrayOutputStream buf1 =
                new ByteArrayOutputStream();
        ObjectOutputStream o1 = new ObjectOutputStream(buf1);
        o1.writeObject(animals);
        o1.writeObject(animals); // Write a 2nd set
        // Write to a different stream:
        ByteArrayOutputStream buf2 =
                new ByteArrayOutputStream();
        ObjectOutputStream o2 = new ObjectOutputStream(buf2);
        o2.writeObject(animals);
        // Now get them back:
        ObjectInputStream in1 = new ObjectInputStream(
                new ByteArrayInputStream(buf1.toByteArray()));
        ObjectInputStream in2 = new ObjectInputStream(
                new ByteArrayInputStream(buf2.toByteArray()));
        List
                animals1 = (List) in1.readObject(),
                animals2 = (List) in1.readObject(),
                animals3 = (List) in2.readObject();
        print("animals1: " + animals1);
        print("animals2: " + animals2);
        print("animals3: " + animals3);
    }
} /* Output: (Sample)
animals: [Bosco the dog[Animal@addbf1], House@42e816
, Ralph the hamster[Animal@9304b1], House@42e816
, Molly the cat[Animal@190d11], House@42e816
]
animals1: [Bosco the dog[Animal@de6f34], House@156ee8e
, Ralph the hamster[Animal@47b480], House@156ee8e
, Molly the cat[Animal@19b49e6], House@156ee8e
]
animals2: [Bosco the dog[Animal@de6f34], House@156ee8e
, Ralph the hamster[Animal@47b480], House@156ee8e
, Molly the cat[Animal@19b49e6], House@156ee8e
]
animals3: [Bosco the dog[Animal@10d448], House@e0e1c6
, Ralph the hamster[Animal@6ca1c], House@e0e1c6
, Molly the cat[Animal@1bf216a], House@e0e1c6
]
*///:~

我们可以通过一个字节数组来使用对象序列化,从而实现对任何可Serializable对象的“深度复制”—深度复制意味着我们复制的是整个对象网。从结果可以看到,animals1和animals2出现了相同的地址,包括二者共享的那个指向House对象的引用。而animals3,系统无法知道另一个流内的对象是第一个流内的对象的别名,因此它会产生出完全不同的对象网。

static修饰的数据不会被序列化,若想要序列化,需要手动实现:

abstract class Shape implements Serializable {
    public static final int RED = 1, BLUE = 2, GREEN = 3;
}
class Line extends Shape {
    private static int color = RED;
    serializeStaticState(ObjectOutputStream os)		//1
            throws IOException {
        os.writeInt(color);
    }
    public static void
    deserializeStaticState(ObjectInputStream os)	//2
            throws IOException {
        color = os.readInt();
    }
 
}
public static void main(String[] args) throws Exception {
     
 		ObjectOutputStream out = new ObjectOutputStream(
                new FileOutputStream("CADState.out"));
        Line.serializeStaticState(out);		//3
        out.writeObject(shapes);
}

使static序列化如上例那样实现。

xml

对象序列化只能在java中运用,一种更适合各种平台的解决方案是xml。

jdk发布了javax.xml.*类库,用来产生和修改xml。

下面的示例将Person对象序列化到XML中。下面的Person类有一个getXML()方法,它使用XOM来产生被转换为XML的Element对象的Person数据;还有一个构造器,接受Element并从中抽取恰当的Person数据:

写入xml

package xml; /* Added by Eclipse.py */
// Use the XOM library to write and read XML
// {Requires: nu.xom.Node; You must install
// the XOM library from http://www.xom.nu }
import nu.xom.*;
import java.io.*;
import java.util.*;
public class Person {
    private String first, last;

    public Person(String first, String last) {
        this.first = first;
        this.last = last;
    }

    // Produce an XML Element from this Person object:
    public Element getXML() {
        Element person = new Element("person");
        Element firstName = new Element("first");
        firstName.appendChild(first);
        Element lastName = new Element("last");
        lastName.appendChild(last);
        person.appendChild(firstName);
        person.appendChild(lastName);
        return person;
    }

    // Constructor to restore a Person from an XML Element:
    public Person(Element person) {
        first = person.getFirstChildElement("first").getValue();
        last = person.getFirstChildElement("last").getValue();
    }

    public String toString() {
        return first + " " + last;
    }

    // Make it human-readable:
    public static void
    format(OutputStream os, Document doc) throws Exception {
        Serializer serializer = new Serializer(os, "ISO-8859-1");
        serializer.setIndent(4);
        serializer.setMaxLength(60);
        serializer.write(doc);
        serializer.flush();
    }

    public static void main(String[] args) throws Exception {
        List<Person> people = Arrays.asList(
                new Person("Dr. Bunsen", "Honeydew"),
                new Person("Gonzo", "The Great"),
                new Person("Phillip J.", "Fry"));
        System.out.println(people);
        Element root = new Element("people");
        for (Person p : people)
            root.appendChild(p.getXML());
        Document doc = new Document(root);
        format(System.out, doc);
        format(new BufferedOutputStream(new FileOutputStream(
                "People.xml")), doc);
    }
}

读取xml

public class People extends ArrayList<Person> {
  public People(String fileName) throws Exception  {
    Document doc = new Builder().build(fileName);
    Elements elements =
      doc.getRootElement().getChildElements();
    for(int i = 0; i < elements.size(); i++)
      add(new Person(elements.get(i)));
  }
  public static void main(String[] args) throws Exception {
    People p = new People("People.xml");
    System.out.println(p);
  }
} 
原文地址:https://www.cnblogs.com/sean-zeng/p/11319958.html