Thinking In Java—— I/O系统读书笔记及摘录

File类

目录列表器

假设我们想查看一个目录列表,可以使用两种方法来使用File对象。如果我们调用不带参数的list()方法,便可以获得此File对象包含的全部列表。然而,如果我们想获得一个受限列表,假如,想获得所有扩展名为.java的文件,那么我们就要用到“目录过滤器”,这个类会告诉我们怎样显示符合条件的File对象。

DirFilter类实现了FilenameFilter接口。而DirFilter这个类存在的唯一原因是将accept()实现,并提供给list()使用,使得list()可以回调accpect()。此类结构被称为回调。

list()实现了基本的功能,而按照FilenameFilter的形式提供给了这个策略,以便完善list()在提供服务时所需的算法。因为list()接受FilenameFilter对象作为参数,这意味着我们可以传递实现了FilenameFilter接口的任何类的对象,用以选择list()方法的行为方式。

输入和输出

编程语言的I/O类库中常使用流这个概念,他代表了任何有能力产出数据的数据源对象或者是有能力接收数据的接收端对象。“流”屏蔽了实际的I/O设备中处理数据的细节。

Java类库中的I/O类分成输入和输出两部分。

任何自InputStream或Reader派生而来的类都含有名为read()的基本方法,用于读取单个字节或者字节数组。

任何自OutputStram或Writer派生而来的类都含有名为writer()的基本方法,用于写单个字节或者字节数组。

InputStream类

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

数据源包括:

  1. 字节数组;
  2. String对象;
  3. 文件;
  4. “管道”,工作方式与实际管道类似,一端进,一端出;
  5. 一个由其它种类的流组成的序列,以便我们可以将它们收集合并到一个流内;
  6. 其它数据源,如互联网,等;

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

功能 如何使用
ByteArrayInputStream 允许将内存的缓冲区当做InputStream使用; 缓冲区,字节将从中取出并作为一种数据源:将其与FilterInputStream对象相连以提供有用接口;
StringBufferInputStream 将String转换成InputStream; 字符串;底层实现实际使用StringBuffer作为一种数据源:将其与FilterInputStream对象相连以提供有用接口;
FileInputStream 用于从文件中读取信息; 字符串,表示文件名、文件或者FileDescriptor对象作为数据源:将其与FilterInputStream对象相连以提供有用接口;
PipedInputStream 产生用于写入相关PipedOutputStram的数据,实现“管道化”概念; PipedOutputStream作为多线程中的数据源:将其与FilterInputStream对象相连以提供有用接口;
SequenceInputStream 将两个或多个InputStream对象转换成单一InputStream; 两个InputStream对象或一个容纳InputStream对象的容器作为数据源:将其与FilterInputStream对象相连以提供有用接口;
FilterInputStream 抽象类,作为“装饰器”的接口。其中,“装饰器”为其它的InputStream类提供有用的功能;

OutputStream类

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

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

功能 如何使用
ByteArrayOutputStream 在内存中创建缓冲区;所有送往“流”的数据都要存放在此缓冲区; 缓冲区初始化尺寸,用于指定数组的目的地:将其与FilterOutputStream对象相连以提供有用接口;
FileOutputStream 用于将信息写至文件; 字符串,表示文件名、文件或FileDescriptor对象,指定数据的目的地:将其与FIlterOutputStream对象相连以提供有用接口;
PipedOutputStream 任何写入其中的信息都会自动作为相关PipedInputStream的输出;实现“管道化”的概念; PipedInputStream指定用于多线程的数据的目的地:将其与FIlterOutputStream对象相连以提供有用接口;
FilterOutputStream 抽象类,作为“装饰类”的接口;其中,“装饰器”为其它OutputStream提供有用接口;

添加属性和有用的接口

Java I/O类库需要多种不同功能的组合,这正是使用装饰器模式的理由所在。这也是Java I/O类库里存在filter(过滤器)类的原因所在,抽象类filter是所有装饰器类的基类。装饰器必须具有和它所装饰的对象相同的接口,但它也可以扩展接口,而这种情况只发生在个别filter类中。

装饰器模式也有一个缺点:在编写程序的时候,它给我们提供了相当多的灵活性,但是它同时也增加了代码的复杂度。Java I/O类库操作不便的原因在于:我们必须创建许多类——"核心"I/O类型加上所有的装饰器,才能得到我们想要的单个I/O对象。

FilterInputStream和FilterOutputStream是用来提供装饰器类接口以控制特定输入流和输出流的两个类,但是它们的名字不是很直观。FilterInputStream和FilterOutputStream分别来自I/O类库中的基类InputStream和OutputStream来生而来,这两个类是装饰器的必要条件,以便能为所有正在被修饰的对象提供通用接口。

通过FilterInputStream从InputStream读取数据

FilterInputStream类能够完成两件完全不一样的事情。其中,DataInputStream允许我们读取不同的基本类型数据以及String对象。搭配对应的DataOutputStream,我们就可以通过数据“流”将基本类型的数据从一个地方迁移到另一个地方。

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

功能 如何使用
DataInputStream 与DataOutputStream搭配使用,因此我们可以按照可移植方式从流读取基本数据类型; InputStream包含用于读取基本类型数据的全部接口;
BufferedInputStream 使用它可以防止每次读取时都得进行实际写操作,代表“使用缓冲区”; InputStream,可以指定缓冲区大小;本质上不提供接口,只不过是向进程中添加缓冲区所必需的;与接口对象搭配;
LineNumberInputStream 跟踪输入流中的行号;可调用getLineNumber()和setLineNumber(int); InputStream仅增加了行号,因此可能要与接口对象搭配使用;
PushbackInputStream 具有“能弹出一个字节的缓冲区”;因此可以将读到的最后一个字符回退; InputStream通常为编译器的扫描器,之所以包含在内是因为Java编译器的需要,我们可能永远不会用到;

通过FilterOutputStream向OutputStream写入

与DataInputStream对应的是DataOutputStream,它可以将各种基本数据类型以及String对象格式化输出到“流”中去;这样以来,任何机器上的任何DataInputStream都能够读取它们。所有的方法都以“write”开头;

功能 如何使用
DataOutputStream 与DataInputStream搭配使用,因此可以按照可移植方式向流中写入基本类型数据; OutputStream包含用于写入基本类型数据的全部接口;
PrintStream 用于产生格式化输出;其中DataOutputStream处理数据的存储,PrintStream处理显示; OutputStream,可以用boolean值指示是否在每次换行时清空缓存区(可选的),应该是对OutputStream对象的“final”封装;可能会经常使用到它;
BufferedOutpurStream 使用它以避免每次发送数据时都要进行实际的写操作;代表"使用缓存区";可以调用flush()清空缓存区; OutputStream,可以指定缓存区大小;本质上不提供接口,只不过时向进程中添加缓冲区所必须的;与接口对象搭配;

Reader和Writer

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

数据的来源和去处

面向字节的InputStream和OutputStream才是正确的解决方案;特别是,java.util.zip类库就是面向字节的而不是面向字符的。因此,最明智的做法就是尽量使用Reader和Writer,一旦程序无法成功编译,我们就会发现自己不得不使用面向字节的类库。

更改流的行为

对于InputStream和OutputStream来说,我们会使用FilterInputStream和FilterOutputStream的装饰器子类来修饰“流”以满足特殊需要。

I/O流的典型使用方式

缓冲输入文件

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

在读取时,可以使用字符串来累计文件内容,然后在readLine()之后添加必要的换行符号,在使用完毕后调用close()来关闭文件。

File类获取目录路径的方法:

		//获取当前目录
        File de = new File(".");
        //获取上一级目录
        File dea = new File("..");
        //获取相对路径
        System.out.println("CPath:" + dea.getCanonicalPath());
        //获取绝对路径
        System.out.println("APath:" + dea.getAbsolutePath());

从内存输入

使用BufferedReader、FileReader以及StringBuilder读入的String结果被用来创建一个StringReader。然后调用read()每次读取一个字符,并将其发送到控制台。

值得注意的是read()是以int形式返回下一个字节,因此必须类型转换为char才能正确打印。

格式化的内存输入

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

必须为DataInputStream提供字节数组,为了产生该数组String包含了一个可以实现此项工作的getBytes()方法。所产生的ByteArrayInputStream是一个适合传递给DataInputStream的InputStream。

如果我们从DataInputStream用readByte()一次一个字节地读取字符,那么任何字节的值都是合法结果,因此返回值不能用来检测输入是否是结束。相反,我们可以使用available()方法查看还有多少可供存取的字符。

此方法读取的UTF-8中文乱码

值的注意的是,available()的工作方式会随着读取的媒介类型的不同而有所不同;字面意思就是“在没有阻塞的清苦啊表格内下所能读取的字节数”。对于文件,这意味着整个问文件;但是对于不同类型的流,可能就不是这样的,因此要谨慎使用。

可以通过捕获来检测输入的末尾,但是,使用异常来进行流控制,通常被认为是对异常特性的错误使用。

基本的文件输出

FIleWriter对象可以向文件写入数据。

需要创建一个与指定文件连接的FileWriter。实际上,通常会用BufferedWriter将其包装起来用以缓冲输出。

缓冲往往能显著地增加I/O操作的性能

在绝大多数情况下,将零散的IO合并为大块的IO,以降低IO请求个数,是可以提高性能的,因为外设每秒处理的IO个数是有限的。

如果在使用PrintWriter.readLine()读取文件时,返回null后,没有显式的调用close()方法,那么位于缓冲区中的内容将无法被清空,导致读取的数据不完整。

关于BufferedReader和BufferedWriter

将从文件中读取的内容放入缓冲区,对字符进行批量的读写操作,以提高IO效率。

存储和恢复数据

PrintWriter可以对数据进行格式化,以便人们的阅读。但是为了输出可供另一个“流”恢复的数据,则需要使用DataOutputStream写入数据,并使用DataInputStream来恢复数据。这里的“流”可以是任何形式。

值得注意的是DataOutputStream和DataInputStream是面向字节的,因此要使用InputStream和OutputStream。

使用writeUTF()和readUTF()时,此处使用的UTF-8是适合于Java的UTF-8变体,如果直接用win的记事本中的UTF-8打开部分数据会乱码。所以如果是使用writeUTF()写入文件,则必须使用特殊的代码才可以正确的读取,比如,使用readUTF()。

读写随机访问文件

使用RandomAccessFile,类似于组合使用了DataInputStream和DataOutputStream。

另外,也可以利用seek()在文件中到处移动,并修改文件中的某个值。

关于浮点运算精度失真的问题

在Java中,例如,5 * 1.414 这样的问题 正确结果应该是7.07 ,但是程序跑出来则是7.069999999999999,原因是浮点运算是有精确计算范围的,如果超过范围则会造成数据失真等问题。

总而言之,浮点运算是不精确的,如果想要精确运算则需要使用String、BigDecimal或者Long转换。

管道流

PipedInputStream、PipedOutputStream、PipedReader以及PipedWriter适用于多线程间的通讯。

PipedReader/PipedWirter提供了线程间使用输入输出流进行通讯的方式,相比System.in.read()当前使用的PipedReader.read()是可被interrupt()打断的,这样省去了释放资源才能中断任务的步骤。

文件读写的实用工具

Java IO类库的最大问题就是:需要编写很多的代码才可以去执行一些常用操作——没有任何基本的帮助功能可以为我们做这些。更糟糕的是,装饰器会使得要记住如何打开文件变成一件相当困难的事情。

值得注意的是,在任何打开文件的代码的finally子句中,都添加了作为防卫措施的close()调用,以保证文件将会被正确的关闭。

标准I/O

程序的所有输入都可以来自于标准输入,它的所有输出也都可以发送到标准输出。以及所有的错误信息都可以发送到标准错误。标准I/O的意义在于:我们可以很容易地把程序串联起来,一个程序的标准输出可以成为另一程序的标准输入。

从标准输入中读取

按照标准I/O模型,Java提供了System.in、System.out和System.err。其中System.out已经事先被包装成了printStream对象。System.err同样也是PrintStream,但System.in却是一个没有被包装过的未经加工的InputStream。

将System.out转换成PrintWriter

System.out是一个PrintStream,而PrintStream是一个OutputStream。PrintWriter有一个可以接收OutputStream作为参数的构造器。因此,只要需要,就可以使用那个构造器把System.out转换成PrintWriter。

在使用PrintWriter来转换System.out时,构造器有连个参数,第二个参数是是否要自动清空缓存,如果置为false,则会有可能看不到输出的结果。

标准I/O重定向

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

​ 重定向System.in,即,在调用System.setIn(InputStream)之后,再次调用System.in时,即可获取到重定向之后标准流中的数据;

​ setIn(InputStream);

​ setOut(PrintStream);

​ setErr(PrintStream);

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

值得注意的是,I/O重定向操纵的是字节流,而不是字符流,因此我们使用的是InputStream和OutputStream,而不是Reader和Writer。

进程控制

在执行命令的过程中可能会出现两种错误:一种是,普通的已成导致的错误——对于这类错误我们只需要重新抛出一个运行时错误即可;第二种是在进程自身的执行过程中产生的错误。

当使用进程控制时,通常要写一个单独的Exception来抛出在进程执行过程中的错误。

示例代码:

public class OSExecute {

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

        try{

            Process process = new ProcessBuilder(
                    command.split(" ")
            ).start();

            BufferedReader result = new BufferedReader(
                    new InputStreamReader(
                            process.getInputStream()
                    )
            );

            String s;

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

            BufferedReader errors = new BufferedReader(
                    new InputStreamReader(
                            process.getErrorStream()
                    )
            );

            while ((s = errors.readLine()) != null){
                System.err.println(s);
                err = true;
            }
        } catch (IOException e) {
            if (!command.startsWith("CMD /C")){
                command("CMD /C " + command);
            }else {
                throw new  RuntimeException();
            }
        }
        if (err){
            throw new OSExecuteException("Errors executing " + command);
        }
    }
}

关键的是使用Process process = new ProcessBuilder(command.split(" ")).start()来启动一个进程,并在进程启动时附上其要执行的命令。

在进程启动之后便可以通过Process对象中的getInputStream()方法来获取InputStream之类的。

新I/O

引入新的Java I/O类库的目的在于提高速度。

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

通道类比于存有煤矿的矿层,而缓冲器类比于从矿层驶出来装满煤炭的卡车,而我们仅仅是从卡车上来获取煤炭,而我们要做的仅仅是从车上卸煤以及将卡车送至煤层。

也就是说,直接与我们交互的是缓冲器,而我们要做的只是从缓冲器上获得数据,或是通过缓冲器发送数据。

Reader和Writer这种字符模式类不能用于产生通道。

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

通道是一个相当基础的东西:可以向它传递用于读写的ByteBuffer,并且可以锁定文件的某些区域用于独占式访问。

实例代码:

public class GetChannel {

    private static final int BSIZE = 1024;

    public static void main(String[] args) throws IOException {

        FileChannel fc = new FileOutputStream("data.txt").getChannel();
        fc.write(ByteBuffer.wrap("Test Data...".getBytes()));
        fc.close();

        fc = new RandomAccessFile("data.txt", "rw").getChannel();
        fc.position(fc.size());
        fc.write(ByteBuffer.wrap("More Data...".getBytes()));
        fc.close();

        fc = new FileInputStream("data.txt").getChannel();
        ByteBuffer buff = ByteBuffer.allocate(BSIZE);
        fc.read(buff);
        buff.flip();
        while (buff.hasRemaining()){
            System.out.println((char)buff.get());
        }
    }

}

将字节存放于ByteBuffer的方法之一是:使用一种“put”方法直接对它进行填充,填入一个或多个字节,或者基本数据类型的数据。

对于只读访问,我们必须显式地调用静态方法allocate()方法来分配ByteBuffer。nio的目标就是快速移动大量数据,因此ByteBuffer的大小显得尤为重要。

如果使用allocateDirect()来产生一个与操作系统有更高耦合性的“直接”缓冲器,虽然速度会更快,但是支出会变大而且支出随操作系统的不同而不同,因此必须要再次实际运行一下才开一查看直接缓冲是否可以使速度提高。

值得注意的是,一旦调用read()来告知FileChannel向ByteBuffer存储字节,就必须调用缓冲器上的filp(),以便告知FileChannel做好让别人读取数据的准备。如果打算使用缓冲器执行进一步的read()操作,必须使用clear()来为每一个read()做好准备。

ByteBuffer被分配了空间,当FileChannel.read()返回-1时,表示已经到了输入的末尾。每次read()操作之后,将会将数据输入到缓冲器中,filp()则是准备缓冲器以便它的信息可以由write()提取。write()操作之后,数据仍在缓冲器中,接着clear()操作则对所有的内存指针重新安排,以便缓冲器在另一个read()操作期间能够做好数据接收的准备。

示例代码:

public class ChannelCopy {

    private static final int BSIZE = 8;

    public static void main(String[] args) throws IOException {

        FileChannel in = new FileInputStream("data.txt").getChannel();
        FileChannel out = new FileOutputStream("dataOut.txt").getChannel();

        ByteBuffer buffer = ByteBuffer.allocate(BSIZE);

        boolean flag = true;
        while (in.read(buffer) != -1){
            System.out.println("read...");
            //每次进行写操作时要进行准备工作
            /*if (flag){
                System.out.println("buffer.flip()...");
                buffer.flip();
                flag = false;
            }*/
            buffer.flip();
            out.write(buffer);
            //读写操作完成后需要为下一次读做准备
            buffer.clear();
        }
    }

}

关于Buffer.filp()方法的解释

在JDK 1.8 API的解释为:

原文:

Flips this buffer. The limit is set to the current position and then the position is set to zero. If the mark is defined then it is discarded.
After a sequence of channel-read or put operations, invoke this method to prepare for a sequence of channel-write or relative get operations. 

译文:

翻转这个缓冲区。 该限制设置为当前位置,然后将该位置设置为零。 
如果标记被定义,则它被丢弃。 
在通道读取或放置操作的序列之后,调用此方法来准备一系列通道写入或相对获取操作。

个人认为是,调整缓冲器中指针的位置,使之归零然后准备下次操作。

在示例代码中如果将clear()方法注释掉,而只调用一次filp(),可以从输出文件中看到,输出的数据是不完整且重复的,所以认为存在覆盖现象,而之所以一直没有读到结尾,也正是因为在覆盖原有数据,而非置空。

关于Buffer.clear()方法的解释

在JDK 1.8 API中的解释为:

原文:

Clears this buffer. The position is set to zero, the limit is set to the capacity, and the mark is discarded.
Invoke this method before using a sequence of channel-read or put operations to fill this buffer.

译文:

翻转这个缓冲区。 该限制设置为当前位置,然后将该位置设置为零。 如果标记被定义,则它被丢弃。 
在通道读取或放置操作的序列之后,调用此方法来准备一系列通道写入或相对获取操作。

与filp()不同的是,filp()并没有清空这个缓冲区,而clear()则是清空了。

在示例中声明了,一卡车(BSIZE)可以装多少煤,也已知了煤层有多少(文件大小),而filp()则是指定从卡车车斗的哪里开始装车或者取货(此处的取货是复制,而非剪切),clear()则是在完成装车或者卸载之后将整个车斗完全倒出将杂质或者废料全部清空。(个人认为)

而示例中的Buffer则是充当了将两个管道串联起来的角色。

如果使用发出端.transferTo()或者接收端.transferFrom()的方法会更加简便。

数据转换

在使用CharBuffer.asCharBuffer()方法时必须要设置字符集,否则读出来的就是乱码;

First asCharBuffer:卯浥⁤慴愠⸮
Decoded using UTF-8: Some data ...
Second asCharBuffer: Some text more...
Third asCharBuffer:Some Text more...

示例代码:

public class BufferToText {

    private static final int BSIZE = 1024;

    public static void main(String[] args) throws IOException {

        FileChannel fc = new FileOutputStream("data2.txt").getChannel();
        fc.write(ByteBuffer.wrap("Some data ...".getBytes()));
        fc.close();

        fc = new FileInputStream("data2.txt").getChannel();
        ByteBuffer buff = ByteBuffer.allocate(BSIZE);
        fc.read(buff);
        buff.flip();
        //没有作用
        System.out.println("First asCharBuffer:" + buff.asCharBuffer());
        //返回数据开始的地方
        buff.rewind();
        //需要设置字符集
        String enconding = System.getProperty("file.encoding");
        //对Buffer设置对应的字符集
        System.out.println("Decoded using " + enconding + ": " + Charset.forName(enconding).decode(buff));

        //第一种方式
        fc = new FileOutputStream("data2.txt").getChannel();
        fc.write(ByteBuffer.wrap("Some text more...".getBytes(StandardCharsets.UTF_16BE)));
        fc.close();

        fc = new FileInputStream("data2.txt").getChannel();
        buff.clear();
        fc.read(buff);
        buff.flip();
        System.out.println("Second asCharBuffer: " + buff.asCharBuffer());

        //第二种方式
        fc = new FileOutputStream("data2.txt").getChannel();
        buff = ByteBuffer.allocate(BSIZE);
        buff.asCharBuffer().put("Some Text more...");
        fc.write(buff);
        fc.close();

        fc = new FileInputStream("data2.txt").getChannel();
        buff.clear();
        fc.read(buff);
        buff.flip();
        System.out.println("Third asCharBuffer:" + buff.asCharBuffer());
    }

}

获取基本类型

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

示例代码:

public class GetData {

    private static final int BSIZE = 1024;

    public static void main(String[] args) {
        ByteBuffer bb = ByteBuffer.allocate(BSIZE);
        int i = 0;
        while (i++ < bb.limit()){
            if (bb.get() != 0){
                System.out.println("non zero");
            }
        }
        System.out.println("i=" + i);
        bb.rewind();

        bb.asCharBuffer().put("Howdy");
        char c;
        while ((c = bb.getChar()) != 0){
            System.out.print(c + " ");
        }
        System.out.println();
        bb.rewind();

        bb.asShortBuffer().put((short) 471142);
        System.out.println("short:" + bb.getShort());
        bb.rewind();

        bb.asIntBuffer().put(99471142);
        System.out.println("int:" + bb.getInt());
        bb.rewind();

        bb.asLongBuffer().put(99471142);
        System.out.println("long:" + bb.getLong());
        bb.rewind();

        bb.asFloatBuffer().put(99471142);
        System.out.println("float:" + bb.getFloat());
        bb.rewind();

        bb.asDoubleBuffer().put(99471142);
        System.out.println("double:" + bb.getDouble());
        bb.rewind();
    }

}

在分配一个ByteBuffer之后,缓冲器将其内容全部置为0。

对于ByteBuffer,可以调用asXXXXBuffer()方法来获取基本类型。

视图缓冲器

试图缓冲器可以让我们通过基本某个特定的基本类型数据的视窗查看其底层的ByteBuffer。ByteBuffer依然是实际存储数据的地方,“支持”着视图,因此,对视图的任何修改都会映射为对ByteBuffer中数据的修改,这使得我们可以很方便地向ByteBuffer插入数据。视图还允许我们从ByteBuffer一次一个地或者成批地读取基本类型数据。

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

示例代码中只演示了转换为Char类型的,其他基本类型相似:

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();
        System.out.println("Byte Buffer:");
        while (bb.hasRemaining()){
            System.out.print(bb.position() + "->" + bb.get() + ", ");
        }
        System.out.println();

        CharBuffer cb = ((ByteBuffer)bb.rewind()).asCharBuffer();
        System.out.println("Char Buffer:");
        while (cb.hasRemaining()){
            System.out.print(cb.position() + "->" + cb.get() + ", ");
        }
        System.out.println();
    }

}

ByteBuffer通过一个被“包装”过的8字节数组产生,然后通过各种不同的基本类型的视图缓冲器显示了出来。

与上示例代码对应的,当从不同类型的缓冲器读取时,数据显示的方式也不同:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-OhYEc7yy-1580269006756)(C:\Users\12118\AppData\Roaming\Typora\typora-user-images\1578790447826.png)]

字节存放次序

不同的机器可能会使用不同的字节排序方法来存储数据。“big endian”(高位优先)将最重要的字节存放在地址最低的存储器单元。而“little endian”(低位优先)则是将最重要的字节放在地最高的存储器单元。当存储量大于一个字节时,像int、float等,就要考虑字节的顺向问题了。

ByteBuffer是以高位优先的形式存储数据的。

示例代码:

public class Endians {

    public static void main(String[] args) {
        ByteBuffer bb = ByteBuffer.wrap(new byte[12]);
        bb.asCharBuffer().put("abcdef");
        System.out.println(Arrays.toString(bb.array()));
        bb.rewind();

        bb.order(ByteOrder.BIG_ENDIAN);
        bb.asCharBuffer().put("abcdef");
        System.out.println(Arrays.toString(bb.array()));
        bb.rewind();

        bb.order(ByteOrder.LITTLE_ENDIAN);
        bb.asCharBuffer().put("abcdef");
        System.out.println(Arrays.toString(bb.array()));
    }

}

ByteBuffer有足够的空间,以储存作为外部缓冲器的charArray中的所有字节,因此可以调用array()方法显示视图底层的字节。array()方法是“可选的”,并且我们只能对由数组支持的缓冲器调用此方法,否则,会抛出UnsupportedOperationException。

用缓冲器操纵数据

如果想把一个字节数组写到文件中去,那么就应该使用ByteBuffer.warp()方法把字节数组包装起来,然后用getChannel()方法在FileOutputStream上打开一个通道,接着将来自于ByteBuffer的数据写到FileChannel中。

值得注意的是,ByteBuffer是将数据移进移出通道的唯一方式,并且我们只能创建一个独立的基本类型缓冲器,或者使用“as”方法从ByteBuffer中获得。也就是说,我们不能把基本类型的缓冲器转换成ByteBuffer。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ejqHcxQ2-1580269006759)(E:\宝库\markdown笔记\Thinking in Java 笔记\ABE91DF44B5A98DD481E93AB94B1CEC4.png)]

缓冲器的细节

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

方法名 作用
capacity() 返回缓冲区容量。
clear() 清空缓冲区,将position置为0,limit置为容量。我们可以调用此方法用来覆写缓冲区。
filp() 将limit置为position,position置为0。此方法用于准备从缓冲区读取已经写入的数据。
limit() 返回limit值。
limit(int lim) 设置limit值。
mark() 将mark设置为position。
position() 返回position值。
position(int pos) 设置position值。
remaining() 返回(limit - position)。
hasRemaining() 若有介于position和limit之间的元素,则返回true。

关于四个索引的说明

索引名 说明
Capacity 容量,当前可容纳的最大数据量,在缓冲区创建时被设定且不能被改变。
Limit 表示缓冲区当前的终点,不能对超过缓冲区极限的位置进行读写操作,但是极限位置是可修改的。
Position 下一个被读或写的元素的索引,每次读写缓冲区数据时都会改变Position值,为下次读写操作准备。
Mark 调用mark()时来设置mark=position,再调用reset()可以让position恢复到标记的位置,主要是用来记录上一次的position值。

关于reset()、rewind()方法的说明

关于reset()方法的作用,在 jdk 1.8 API(中文版)中的说明为:

将此缓冲区的位置重置为先前标记的位置。 
调用此方法既不会更改也不丢弃该标记的值。

关于rewind()方法的作用,在 jdk 1.8 API(中文版)中的说明为:

倒带这个缓冲区。 位置设置为零,标记被丢弃。 
在通道写入或获取操作的序列之前调用此方法,假设已经设置了相应的限制。

示例代码:

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(c1).put(c2);
        }
    }

    public static void main(String[] args) {
        char[] data = "UsingBuffers".toCharArray();
        ByteBuffer bb = ByteBuffer.allocate(data.length * 2);
        CharBuffer cb = bb.asCharBuffer();
        cb.put(data);
        System.out.println(cb.rewind());
        symmetricScramble(cb);
        System.out.println(cb.rewind());
        symmetricScramble(cb);
        System.out.println(cb.rewind());
    }

}

尽管可以通过对某个char数组调用wrap()方法来直接产生一个CharBuffer,但是在示例代码中

取而代之的是分配一个底层的ByteBuffer,产生CharBuffer只是ByteBuffer上的一个视图而已。

内存映射文件

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

示例代码:

public class LargeMappedFiles {

    static int length = 0x8FFFFFF;

    public static void main(String[] args) throws IOException {
        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');
        }

        System.out.println("Finished Writing...");

        for (int i=length/2;i<length/2 + 6;i++){
            System.out.println((char)out.get(i));
        }
    }

}

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

性能

虽然nio在性能上有所改善,但是“映射文件访问”往往有更快的速度。

示例代码:

public class MappedIO {

    private static int numOfInts = 4 * 100 * 10000;
    private static int numOfUbufferInts = 20 * 10000;

    private abstract static class Tester{
        private String name;
        public Tester(String name){
            this.name = name;
        }

        public void runTest(){
            System.out.println("name:" + name);
            try {
                long starts = System.nanoTime();
                test();
                double duration = System.nanoTime() - starts;
                System.out.printf("cost:%.2f\n", duration/1.0e9);
            } catch (IOException e) {
                e.printStackTrace();
            }
        }

        public abstract void test() throws IOException;
    }

    private static Tester[] tests = {
      //普通流写入
      new Tester("Stream Write") {
          @Override
          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") {
          @Override
          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") {
          @Override
          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") {
          @Override
          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") {
          @Override
          public void test() throws IOException {
              RandomAccessFile raf = new RandomAccessFile(
                      new File("temp.tmp"), "rw"
              );
              raf.write(1);
              for (int i = 0; i < numOfUbufferInts; i++) {
                  raf.seek(raf.length() - 4);
                  raf.writeInt(raf.readInt());
              }
              raf.close();
          }
      },
      //内存映射文件读取操作
     new Tester("Mapped Read/Write") {
         @Override
         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 < numOfUbufferInts; i++) {
                 ib.put(ib.get( i - 1));
             }
             fc.close();
         }
     }

    };

    public static void main(String[] args) throws IOException {
        for (Tester coursor : tests){
            coursor.runTest();
        }
    }

}
/*
out:
name:Stream Write
cost:0.08
name:Mapped Write
cost:0.02
name:Stream Read
cost:0.24
name:Mapped Read
cost:0.01
name:Stream Read/Write
cost:5.21
name:Mapped Read/Write
cost:0.01
 */

文件加锁

文件锁机制允许我们同步访问某个作为共享资源的文件。不过,竞争同一个文件的两个线程可能不在同一个JVM上;或者一个是Java线程,另一个是操作系统中其他的某个本地线程。文件锁对其他的操作系统进程是可见的,因为Java的文件加锁直接映射到本地操作系统的加锁工具。

以下是一个文件加锁访问的简单实例:

public class FileLocking {

    public static void main(String[] args) throws IOException, InterruptedException {
        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();
    }

}

tryLock()是非阻塞式的,它设法获取锁,但是如果不能获得,它将直接从方法调用返回。lock()则是阻塞式的,它要阻塞进程直至锁可以获得,或者调用lock()的线程中断,或调用lock()的通道关闭。使用FileLock.release()可以释放锁。

也可以使用如下方法对文件部分区域加锁:

  • tryLock(long position, long size, boolean shared)
  • lock(long position, long size, boolean shared)

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

尽管无参的加锁方法将根据文件尺寸的变化而变化,但是具有固定尺寸的锁不随文件尺寸的变化而变化。如果获得了某一区域(从position到position + size)上的锁,当文件增到超出position + size时,那么在position + size之外的部分是不会被锁定的。无参的加锁方式会对整个文件进行加锁,甚至文件变大后也是如此。

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

关于共享锁和独占锁的解释

独占锁又叫排他锁,是指该锁一次只能被一个线程所持有。如果线程T对数据A加上排它锁后,则其他线程就不能再对A加任何类型的锁。获得排他锁的线程既能读数据也能修改数据。

共享锁是指该锁可被多个线程持有。如果线程T对数据A加上共享锁之后,则其他线程只能对A再加共享锁,不能加排他锁。获得共享锁的线程只能读取数据,不能修改数据。

对映射文件的部分加锁

示例代码:

public class LockingMappedFiles {

    static final int LENTH = 0x8FFFFFF;
    static FileChannel fc;

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

        public LockAndModify(ByteBuffer buffer, int start, int end) {
            this.start = start;
            this.end = end;
            buffer.limit(end);
            buffer.position(start);
            this.buffer = buffer.slice();
            start();
        }

        @Override
        public void run(){
            try {
                FileLock f1 = fc.lock(start, end, false);
                System.out.println("Locked:" + start + " to " + end);
                while (buffer.position() < buffer.limit() - 1){
                    buffer.put((byte)(buffer.get() + 1));
                }
                f1.release();
                System.out.println("Released:" + start + " to " + end);
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }

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

}
/*
out:
Locked:75497471 to 113246206
Locked:0 to 50331647
Released:75497471 to 113246206
Released:0 to 50331647
 */

在示例中LockAndModify创建了缓冲区和用于修改的slice(),然后在run()中,获得文件通道上的锁。值的注意得是,我们获取的是通道上的锁而不是缓冲器上的锁。

关于ByteBuffer.slice()方法的说明

在JDK 1.8 API(中文版)中的解释如下:

创建一个新的字节缓冲区,其内容是此缓冲区内容的共享子序列。 
新缓冲区的内容将从此缓冲区的当前位置开始。 对这个缓冲区内容的更改将在新的缓冲区中可见,反之亦然; 两个缓冲区的位置,极限和标记值将是独立的。 

新缓冲区的位置将为零,其容量和限制将是此缓冲区中剩余的字节数,其标记将不定义。 如果只有这个缓冲区是直接的,并且只有当这个缓冲区是只读的时,这个缓冲区将是只读的

压缩

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

这些类并不是从Reader和Writer类派生来的,而是属于InputStream和OutputStream继承层次结构的一部分。这样做是因为压缩类库是按字节方式而不是字符方式处理的。

压缩类 功能
CheckedInputStream GetCheckSum()为任何InputStream产生校验和(不仅仅是解压缩)
CheckOutputStream GetCheckSum()为任何OutputStream产生校验和(不仅仅是压缩)
DeflaterOutputStream 压缩类的基类
ZipOutputStream 一个DeflaterOutputStream,用于将数据压缩成Zip文件格式
GZIPOutputStream 一个DeflaterOutputStream,用于将数据压缩成GZip文件格式
InflaterInputStream 解压缩的基类
ZipInputStream 一个InflaterInputStream,用于解压缩Zip文件格式的数据
GZipInputStream 一个InflaterInputStream,用于解压缩GZip文件格式的数据

使用GZIP进行简单压缩

GZIp接口非常简单,因此如果我们只想对单个数据流(而不是一系列互异数据)进行压缩,那么GZIP可能就是比较合适的选择。

示例代码:

public class GZIPcompress {

    public static void main(String[] args) throws IOException {
        BufferedReader in = new BufferedReader(
                new FileReader("test.gz")
        );

        BufferedOutputStream out = new BufferedOutputStream(
                new GZIPOutputStream(
                        new FileOutputStream("test.gz")
                )
        );

        System.out.println("Starting Writing File...");

        int c;
        while ((c = in.read()) != -1){
            out.write(c);
        }
        in.close();
        out.close();

        System.out.println("Starting 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);
        }
    }

}

压缩类的使用非常直观——直接将输出流封装成GZIPOutputStream或ZipOutputStream,并将输入流封装成GZIPInputStream或ZipInputStream即可。其它全部操作都是通常的I/O操作。

上述示例把面向字符的流和面向字节的流混合了起来;输入(in)用Reader,而GZIPOutputStream的构造器只能接受OutputStream,不能接受Writer对象。在打开文件时,GZIPInputStream就会被转换成Reader。

对象序列化

Java的序列化将那些实现了Serializable接口的对象转换成一个字节序列,并能都在以后将这个字节序列完全恢复为原来的对象。这一过程可以通过网络进行;这意味着序列化机制能够自动弥补不同操作系统之间的差异。

就其本身而言,对象的序列化是非常有趣的,因为利用它可以实现轻量级持久性,“持久性”意味着一个对象的生命周期并不取决于程序是否正在执行;它可以生存于程序的调用之间。通过将一个序列化的对象写入硬盘,然后在重新调用程序时恢复该对象,就能够实现持久性的效果。之所以称之为“轻量级”,是因为不能用某种“persistent”关键字来简单的定义一个对象,并让系统自动维护其它细节问题。

对象必须在程序中显式的序列化和反序列化还原。

对象序列化的概念加入到语言中是为了支持两种特性。一是Java的远程方法调用,它使存活于其它计算机上的对象使用起来就像是存活于本机上一样。当远程对象发送消息时,需要通过对象序列化来传输参数和返回值。

再者,对于Java Bean来说,对象的序列化也是必需的。使用一个Bean时,一般情况下是在设计阶段对它的状态信息进行配置。这种状态信息必需保存下来,并在程序启动时进行后期恢复;这种具体工作就是由对象序列化完成的。

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

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

对象序列化特别“聪明”的一个地方是它不仅保存了对象的“全景图”,而且能够追踪对象内所包含的所有引用,并保存那些对象;接着又能对对象内包含的每一个这样的引用进行追踪;这种情况有时被称为“对象网”,单个对象可与之建立连接,而且它还包含了对象的引用数组以及成员对象。如果必须保持一套自己的对象序列化机制,那么维护那些可以追踪到所有链接的代码可能会显得非常麻烦。使用时,让它用优化的算法自动维护整个对象网,不要自己动手去做什么。

以下示例代码中,通过对链接的对象生成一个worm对序列化机制进行测试。每一个对象都与worm中的下一段链接,同时又与属于不同类(Data)的对象引用数组连接:

public class Data implements Serializable {

    private int n;

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

    @Override
    public String toString() {
        return "Data{" +
                "n=" + 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;

    public Worm(int i,char c) {
        System.out.println("Worm constructor:" + i);
        this.c = c;
        if (--i > 0){
            next = new Worm(i, (char)(c + 1));
        }
    }

    public Worm() {
        System.out.println("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 IOException, ClassNotFoundException {
        //写入文件
        Worm w = new Worm(6, 'a');
        ObjectOutputStream out = new ObjectOutputStream(
                new FileOutputStream("worm.out")
        );
        out.writeObject("Worm storage\n");
        out.writeObject(w);
        out.close();
        //读取文件
        ObjectInputStream in = new ObjectInputStream(
                new FileInputStream("worm.out")
        );
        String s = (String)in.readObject();
        Worm w2 = (Worm)in.readObject();
        System.out.println(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();
        System.out.println(s + "w3 = " + w3);
    }
}

Worm内的Data对象数组使用随机数初始化,这样就不用怀疑编译器保留了某种原始信息。每一个Worm段都用一个char加以标记。该char是在递归生成链接的Worm列表时自动生成的。要创建一个Worm,必须要告诉构造器你所希望的长度。在产生下一个引用时,要调用Worm构造器,并将长度减一,以此类推。最后一个next引用,则为null,表示已到达Worm的尾部。

以上这些操作都使得事情变得更加复杂,从而加大了对象序列化的难度。然而,真正的序列化过程却是非常简单的。一旦从另外某个流创建了ObjectOutputStream,writeObject()就会将对象序列化。注意也可以为一个String调用writeObject()。也可以用与DataOutputStream相同的方法写入所有基本类型。

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

寻找类

现在有一个问题是,将一个对象从它的序列话状态中恢复出来,有哪些工作是必须的呢?例如,我们要将以一个对象序列化,并通过网络将其作为文件传送给里另一台电脑,那么,另一台电脑上的程序可以只利用该文件的内容来还原对象吗?

以下为示例代码:

package IOSystem.ObjectSerialization.FindClass;

import java.io.Serializable;

public class Alien implements Serializable {
}
public class FreezeAlien {

    public static void main(String[] args) throws IOException {
        ObjectOutputStream out = new ObjectOutputStream(
                new FileOutputStream("X.file")
        );
        Alien quellek = new Alien();
        out.writeObject(quellek);
    }

}
public class ThawAlien {

    public static void main(String[] args) throws IOException, ClassNotFoundException {
        ObjectInputStream in = new ObjectInputStream(
                new FileInputStream(
                        new File("X.file")
                )
        );

        Object mystery = in.readObject();
        System.out.println(mystery.getClass());
    }

}
/*
out:
class IOSystem.ObjectSerialization.FindClass.Alien
*/

在输出中可以看到是可以输出类的,这是因为JVM可以找到与之相关的.class文件,如果找不到相关的.class文件则会抛出ClassNotFoundExpection的异常。

序列化的控制

如果对序列化有特殊的需求应该怎么办?例如,出于特殊的安全考虑,而且不希望对象的某一部分被序列化;或者对象被还原后,某子对象需要重新创建,从而不必将该子对象序列化。

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

示例代码:

public class Blips {

    public static void main(String[] args) throws IOException, ClassNotFoundException {
        System.out.println("Constructing Objects:");
        Blips1 b1 = new Blips1();
        Blips2 b2 = new Blips2();
        ObjectOutputStream o = new ObjectOutputStream(
                new FileOutputStream("Blips.out")
        );
        System.out.println("Saving Objects:");
        o.writeObject(b1);
        o.writeObject(b2);
        o.close();

        ObjectInputStream in = new ObjectInputStream(
                new FileInputStream("Blips.out")
        );
        System.out.println("Recovering b1:");
        b1 = (Blips1)in.readObject();
        try {
            System.out.println("Recovering b2:");
            b2 = (Blips2)in.readObject();
        }catch (Exception e){
            e.printStackTrace();
        }
    }

}
/*
Constructing Objects:
Blips1 Constructor...
Blips2 Constructor...
Saving Objects:
Blips1 writeExternal...
Blips2 writeExternal...
Recovering b1:
Blips1 Constructor...
Blips1 readExternal...
Recovering b2:
java.io.InvalidClassException: IOSystem.ObjectSerialization.SerializeControl.Blips2; no valid constructor
	at java.base/java.io.ObjectStreamClass$ExceptionInfo.newInvalidClassException(ObjectStreamClass.java:159)
	at java.base/java.io.ObjectStreamClass.checkDeserialize(ObjectStreamClass.java:864)
	at java.base/java.io.ObjectInputStream.readOrdinaryObject(ObjectInputStream.java:2061)
	at java.base/java.io.ObjectInputStream.readObject0(ObjectInputStream.java:1594)
	at java.base/java.io.ObjectInputStream.readObject(ObjectInputStream.java:430)
	at IOSystem.ObjectSerialization.SerializeControl.Blips.main(Blips.java:32)
*/

可以看到,在恢复第二个对象的时候发生了错误,是因为这与恢复Serializable对象不同。对于Serializable对象,对象完全以它存储的二进制为基础来构造,而不用调用构造器。而对于一个Externalizable对象,所有普通的默认构造器都会被调用(包括在字段定义时的初始化),然后调用readExternal()。必须注意一点——所有默认的构造器都会被调用,才能使Externalizable对象产生正确的行为。

public class Blips3 implements Externalizable {
    private int i;
    private String s;

    public Blips3() {
        System.out.println("Blips3()");
    }

    public Blips3(int i, String s) {
        System.out.println("Blips3(int i, String s)");
        this.i = i;
        this.s = s;
    }

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

    @Override
    public void writeExternal(ObjectOutput out) throws IOException {
        System.out.println("Blips3.writeExternal...");
        //类型要对应
        out.writeObject(s);
        out.writeInt(i);
    }

    @Override
    public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException {
        System.out.println("Blips3.readExternal...");
        //类型要对应
        s = (String)in.readObject();
        i = in.readInt();
    }

    public static void main(String[] args) throws IOException, ClassNotFoundException {
        System.out.println("Constructing objects:");
        Blips3 b3 = new Blips3(47, "A String");
        System.out.println(b3);
        System.out.println("Saving objects:");
        ObjectOutputStream o = new ObjectOutputStream(
                new FileOutputStream("Blips3.out")
        );
        o.writeObject(b3);
        o.close();

        ObjectInputStream in = new ObjectInputStream(
                new FileInputStream("Blips3.out")
        );
        System.out.println("Recovering b3:");
        b3 = (Blips3)in.readObject();
        System.out.println(b3);
    }
}
/*
out:
Constructing objects:
Blips3(int i, String s)
A String47
Saving objects:
Blips3.writeExternal...
Recovering b3:
Blips3()
Blips3.readExternal...
A String47
 */

值得注意的是,在上示例代码中的字段s和i只在第二个构造器中被初始化,而不是在默认构造器中初始化。这也就是说如果不在readExternal()中初始化s和i,那么s会为null,而i则会为0。

因此,为了正常运行,我们不仅需要在writeExternal()方法中将来自对象的重要信息写入,还必须在writeExternal()方法中恢复数据。

transient(瞬时)关键字

当我们对序列化进行控制时,可能某个特定子对象不想让Java的序列化机制自动保存与恢复。如果子对象表示的是我们不希望将其序列化的敏感信息,通常就会面临这种情况。即使对象中的这些信息是private属性,一经序列化处理,人们就可以通过读取文件或者拦截网络传输的方式来访问它。

对于以上问题,第一种解决方式是实现Externalizable接口。

第二种方式是使用transient关键字,如果当前正在操作的是一个Serializable对象,那么所有序列化操作都会自动进行。为了能够予以控制,可以使用transient关键字逐个字段地关闭序列化。

示例代码:

public class Logon implements Serializable {

    private Date date = new Date();
    private String username;
    private transient String password;

    public Logon(String username, String password) {
        this.username = username;
        this.password = password;
    }

    @Override
    public String toString() {
        return "Logon{" +
                "date=" + date +
                ", username='" + username + '\'' +
                ", password='" + password + '\'' +
                '}';
    }

    public static void main(String[] args) throws IOException, InterruptedException, ClassNotFoundException {
        Logon a = new Logon("Zcd", "123123");
        System.out.println("logon a = " + a);
        ObjectOutputStream o = new ObjectOutputStream(
                new FileOutputStream("Logon.out")
        );
        o.writeObject(a);
        o.close();

        TimeUnit.SECONDS.sleep(1);

        ObjectInputStream in = new ObjectInputStream(
                new FileInputStream("Logon.out")
        );

        System.out.println("Recovering Object at :" + new Date());
        a = (Logon)in.readObject();
        System.out.println("Logon a = " + a);
    }
}
/*
out:
logon a = Logon{date=Tue Jan 28 22:08:45 CST 2020, username='Zcd', password='123123'}
Recovering Object at :Tue Jan 28 22:08:46 CST 2020
Logon a = Logon{date=Tue Jan 28 22:08:45 CST 2020, username='Zcd', password='null'}
 */

可以在输出的结果中看到,Logon对象中的date字段和username字段都是一般的,所以被自动序列化。而password是被transient关键字修饰的,所以不会自动保存到磁盘中;另外,自动序列化机制也不会尝试去恢复它。当对象被恢复时,password会变成null。值得注意的是,虽然toString()是用,重载后的+运算符来连接String对象,但是null引用会被自动转换成字符串null。

由于Externalizable对象在默认情况下不保存它们的任何字段,所以transient关键字只能和Serializable对象一起使用。

Externalizable的替代方案

如果不是特别坚持实现Externalizable接口,还有一种办法。首先,我们要实现Serializable接口,并添加(并非“覆盖”或是“实现”)名为writeObject()和readObject()的方法。这样一旦对象被序列化或者被反序列化还原,就会自动地分别调用这两个方法。也就是说,只要我们提供了writeObject()和readObject()这两个方法,就会使用它们而不是默认的序列化机制。

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

private void writeObject(ObjectOutputStream stream) throws IOExpection;
private void readObject(ObjectInputStream stream) throws IOExpection;

在调用ObjectOutputStream.writeObject()时,会检查所传递的Serializable对象,检查是否实现了它自己的writeObject()方法。如果是这样,就跳过正常的序列化过程并调用它的writeObject()。readObject()类似。

此外,还可以在我们的writeObject()内部,调用defaultWriteObject()来选择执行默认的writeObject()。类似的,在readObject()中也可以调用,defaultReadObject()。

示例代码:

public class SerialCtl implements Serializable {

    private String a;
    private transient String b;

    public SerialCtl(String a, String b) {
        this.a = "Not Transient = " + a;
        this.b = "Transient = " + b;
    }

    @Override
    public String toString() {
        return "SerialCtl{" +
                "a='" + a + '\'' +
                ", b='" + b + '\'' +
                '}';
    }

    private void writeObject(ObjectOutputStream stream)
        throws IOException{
        stream.defaultWriteObject();
        //此处选择是否要将transient修饰对象序列化
        stream.writeObject(b);
    }

    private void readObject(ObjectInputStream stream)
        throws IOException, ClassNotFoundException{
        stream.defaultReadObject();
        //此处选择是否恢复transient修饰对象
        b = (String)stream.readObject();
    }

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

        ObjectInputStream in = new ObjectInputStream(
                new ByteArrayInputStream(buf.toByteArray())
        );

        SerialCtl sc2 = (SerialCtl)in.readObject();
        System.out.println("After: " + sc2);
    }
}
/*
out:
Before:SerialCtl{a='Not Transient = Test1', b='Transient = Test2'}
After: SerialCtl{a='Not Transient = Test1', b='Transient = Test2'}
 */

在示例中,有一个String字段是普通字段,而另一个是transient字段,用来证明transient字段由defaultWriteObject()方法,而transient字段必须在程序中明确保存和恢复。字段是在构造器内而不是在定义处进行初始化的,以此可以证明它们在反序列化还原期间没有被一些自动化机制初始化。

版本控制

在Java序列化中版本控制主要是通过如下代码实现的:

private static final long serialVersionUID = 1L;

在进行对象的序列化时读取该属性保存到二进制文件当中,反序列化时取得该对象的serialVersionUID属性之后,与当前JVM中保存的Class中的该属性进行一致性判定,如果不一致,则说明Class属性被更改过(当然,在更改类–如添加删除某个属性时,同时需要重新生成该属性的具体值,以保证每一次更改的版本与serialVersionUID的对应)。

使用“持久性”

一个对序列化技术的想法:存储程序的一些状态,以便我们随后可以很容易地将程序恢复到当前状态。但是在我们能够这样做之前,必须要回答几个问题,如果我们将两个对象——它们都具有指向第三个对象的引用——进行序列化,会发生什么情况?当我们从它们的序列化状态恢复这两个对象时,第三个对象会只出现一次吗?如果将这个两个对象序列化成独立的文件,然后在代码不同部分对它们进行反序列化还原,又会怎样呢?

示例代码:

public class House  implements Serializable {
}
public class Animal implements Serializable {

    private String name;
    private House preferredHouse;

    Animal(String name, House preferredHouse) {
        this.name = name;
        this.preferredHouse = preferredHouse;
    }

    @Override
    public String toString() {
        return "Animal{" +
                "name='" + name + '\'' +
                ", preferredHouse=" + preferredHouse +
                '}';
    }
}
public class MyWorld {

    public static void main(String[] args) throws IOException, ClassNotFoundException {
        House house = new House();
        List<Animal> animals = new ArrayList<>();
        animals.add(
                new Animal("Bosco the dog", house)
        );
        animals.add(
                new Animal("Ralph the hamster", house)
        );
        animals.add(
                new Animal("Molly the cat", house)
        );
        System.out.println("Animals : " + animals);

        ByteArrayOutputStream buf1 = new ByteArrayOutputStream();
        ObjectOutputStream o1 = new ObjectOutputStream(buf1);
        o1.writeObject(animals);
        o1.writeObject(animals);

        ByteArrayOutputStream buf2 = new ByteArrayOutputStream();
        ObjectOutputStream o2 = new ObjectOutputStream(buf2);
        o2.writeObject(animals);

        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();
        System.out.println("animals1 = " + animals1);
        System.out.println("animals2 = " + animals2);
        System.out.println("animals3 = " + animals3);
    }

}
/*
out:
Animals : [Animal{name='Bosco the dog', preferredHouse=IOSystem.ObjectSerialization.UsePersistence.House@2f4d3709}, Animal{name='Ralph the hamster', preferredHouse=IOSystem.ObjectSerialization.UsePersistence.House@2f4d3709}, Animal{name='Molly the cat', preferredHouse=IOSystem.ObjectSerialization.UsePersistence.House@2f4d3709}]
animals1 = [Animal{name='Bosco the dog', preferredHouse=IOSystem.ObjectSerialization.UsePersistence.House@e2144e4}, Animal{name='Ralph the hamster', preferredHouse=IOSystem.ObjectSerialization.UsePersistence.House@e2144e4}, Animal{name='Molly the cat', preferredHouse=IOSystem.ObjectSerialization.UsePersistence.House@e2144e4}]
animals2 = [Animal{name='Bosco the dog', preferredHouse=IOSystem.ObjectSerialization.UsePersistence.House@e2144e4}, Animal{name='Ralph the hamster', preferredHouse=IOSystem.ObjectSerialization.UsePersistence.House@e2144e4}, Animal{name='Molly the cat', preferredHouse=IOSystem.ObjectSerialization.UsePersistence.House@e2144e4}]
animals3 = [Animal{name='Bosco the dog', preferredHouse=IOSystem.ObjectSerialization.UsePersistence.House@6477463f}, Animal{name='Ralph the hamster', preferredHouse=IOSystem.ObjectSerialization.UsePersistence.House@6477463f}, Animal{name='Molly the cat', preferredHouse=IOSystem.ObjectSerialization.UsePersistence.House@6477463f}]
 */

这里有件有趣的事情:我们可以通过一个字节数组来使用对象序列化,从而实现对任何可Serializable对象的“深度赋值”——深度复制意味着我们复制的是整个对象网,而不仅仅是基本对对象及其引用。

只要将任何对象序列化到单一流中,就可以恢复出与我们写出时一样的对象网,并且没有任何意外重复复制出的对象。当然,我们可以在写出第一个对象和写出最后一个对象期间改变这些对象的状态,但是这是我们自己的事情;无论对象在序列化时处于什么状态(无论它们和其它对象有什么样的连接关系),它们都可以被写出来。

如果我们想保存系统状态,最安全的做法是将其作为“原子”操作进行序列化。如果我们序列化了某些东西,再去做其他一些工作,再来序列化更多的东西,如此等等,那么将无法安全地保存系统状态。取而代之的是,将构成系统状态的所有对象都置入单一容器内,并在一个操作中将该容器直接写出,然后同样的只需调用一次方法,就可以将其恢复。

示例代码:

public class StoreCADState {

    public static void main(String[] args) throws IOException {
        List<Class<? extends Shape>> shapeTypes = new ArrayList<>();
        //添加引用
        shapeTypes.add(Circle.class);
        shapeTypes.add(Square.class);
        shapeTypes.add(Line.class);
        //实现一些对象
        List<Shape> shapes = new ArrayList<>();
        for (int i = 0; i < 10; i++) {
            shapes.add(
                    Shape.randomFactory()
            );
        }
        //改变颜色
        for (int i = 0; i < 10; i++) {
            shapes.get(i).setColor(Shape.GREEN);
        }
        //保存状态
        ObjectOutputStream out = new ObjectOutputStream(
                new FileOutputStream("CADState.out")
        );
        out.writeObject(shapeTypes);
        //必须显式调用序列化static字段
        Line.serializeStaticState(out);
        out.writeObject(shapes);
        //输出保存的结果
        System.out.println(shapes);
    }

}
/*
out:
[Shape{color = 3, xPos=56, yPos=19, dimension=55},
 Shape{color = 3, xPos=37, yPos=27, dimension=64},
 Shape{color = 3, xPos=56, yPos=99, dimension=3},
 Shape{color = 3, xPos=60, yPos=70, dimension=99},
 Shape{color = 3, xPos=29, yPos=12, dimension=93},
 Shape{color = 3, xPos=45, yPos=53, dimension=96},
 Shape{color = 3, xPos=31, yPos=51, dimension=36},
 Shape{color = 3, xPos=19, yPos=3, dimension=1},
 Shape{color = 3, xPos=83, yPos=27, dimension=57},
 Shape{color = 3, xPos=21, yPos=49, dimension=75}]
 */
public class RecoverCADState {

    @SuppressWarnings("unchecked")
    public static void main(String[] args) throws IOException, ClassNotFoundException {
        ObjectInputStream in = new ObjectInputStream(
                new FileInputStream("CADState.out")
        );

        List<Class<? extends Shape>> shapeTypes =
                (List<Class<? extends Shape>>)in.readObject();
        //必须显式调用还原static字段
        Line.deserializeStaticState(in);
        List<Shape> shapes = (List<Shape>) in.readObject();
        System.out.println(shapes);
    }

}
/*
out:
[Shape{color = 1, xPos=56, yPos=19, dimension=55},
 Shape{color = 0, xPos=37, yPos=27, dimension=64},
 Shape{color = 3, xPos=56, yPos=99, dimension=3},
 Shape{color = 1, xPos=60, yPos=70, dimension=99},
 Shape{color = 0, xPos=29, yPos=12, dimension=93},
 Shape{color = 3, xPos=45, yPos=53, dimension=96},
 Shape{color = 1, xPos=31, yPos=51, dimension=36},
 Shape{color = 0, xPos=19, yPos=3, dimension=1},
 Shape{color = 3, xPos=83, yPos=27, dimension=57},
 Shape{color = 1, xPos=21, yPos=49, dimension=75}]
 */

在上述输出中可以看到xPos、yPos和dimension字段都被恢复了,但是对static信息的读取却出现了问题。所有的color字段应该是3,但是输出却并非如此。如果想要序列化static字段,必须自己去手动实现。

XML

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

XML和JSON

  1. 什么是XML?

    xml用于数据存储和传输,后缀为.xml;

    XML全称Extensible Markup Language,是标记语言;

    XML设计用来传送及携带数据信息,不用来表现或展示数据,所以XML用途的焦点是它说明数据是什么,以及携带数据信息;

  2. 什么是JSON?

    JSON全称JavaScript Object Notation,是一种轻量级的数据交换格式;

    JSON基于JavaScript的子集;

    JSON数据格式简单,易于读写,占用带宽小;

  3. 区别是什么?

    传输同样格式的数据,xml需要使用更多的字符进行描述;

    流行的是基于json的数据传输;

    xml的层次结构比json更清晰;

    xml是重量级的,json是轻量级的;

示例代码:

public class Person {

    private String first, last;

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

    /**
     * 将Person对象转换为XML元素
     * @return
     */
    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;
    }

    public Person(Element person){
        first = person.getFirstChildElement("first").getValue();
        last = person.getFirstChildElement("last").getValue();
    }

    @Override
    public String toString() {
        return "Person{" +
                "first='" + first + '\'' +
                ", last='" + last + '\'' +
                '}';
    }

    public static void format(OutputStream os, Document doc) throws IOException {
        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 IOException {
        List<Person> people = Arrays.asList(
                new Person("z","zz"),
                new Person("c","cc"),
                new Person("d","dd")
        );
        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);
    }
}
/*
out:
[Person{first='z', last='zz'}, Person{first='c', last='cc'}, Person{first='d', last='dd'}]
<?xml version="1.0" encoding="ISO-8859-1"?>
<people>
    <person>
        <first>z</first>
        <last>zz</last>
    </person>
    <person>
        <first>c</first>
        <last>cc</last>
    </person>
    <person>
        <first>d</first>
        <last>dd</last>
    </person>
</people>
 */

关于Serializer.setIndent()、Serializer.setMaxLength()、Serializer.write()、Serializer.flush()的解释

  • setIndent()

    API原文:

    Sets the number of additional spaces to add to each successive level in the hierarchy. Use 0 for no extra indenting. The maximum indentation is in limited to approximately half the maximum line length. The serializer will not indent further than that no matter how many levels deep the hierarchy is.
    
    When this variable is set to a value greater than 0, the serializer does not preserve white space. Spaces, tabs, carriage returns, and line feeds can all be interchanged at the serializer's discretion, and additional white space may be added before and after tags. Carriage returns, line feeds, and tabs will not be escaped with numeric character references.
    
    Inside elements with an xml:space="preserve" attribute, white space is preserved and no indenting takes place, regardless of the setting of the indent property, unless, of course, an xml:space="default" attribute overrides the xml:space="preserve" attribute.
    
    The default value for indent is 0; that is, the default is not to add or subtract any white space from the source document.
    
    Parameters:
    indent - the number of spaces to indent each successive level of the hierarchy
    Throws:
    IllegalArgumentException - if indent is less than zero
    

    译文:

    设置要添加到层次结构中每个连续级别的额外空间数。使用0表示没有额外缩进。最大压痕仅限于最大线长度的大约一半。无论层次结构有多深,序列化程序都不会进一步缩进。
    
    当此变量设置为大于0的值时,序列化程序不保留空白。空格、制表符、回车符和换行符都可以根据序列化程序的判断进行交换,并且可以在标记前后添加额外的空格。回车、换行和制表符将不会用数字字符引用转义。
    
    在具有xml:space=“preserve”属性的元素内部,不管indent属性的设置如何,都会保留空白并且不会进行缩进,除非xml:space=“default”属性覆盖了xml:space=“preserve”属性。
    
    缩进的默认值是0;也就是说,默认值是不从源文档中添加或减去任何空格。
    
    参数:
    
    缩进-缩进层次结构的每个连续级别的空格数
    
    抛出:
    
    IllegalArgumentException-如果缩进小于零
    
  • setMaxLength()

    API原文:

    Sets the suggested maximum line length for this serializer. Setting this to 0 indicates that no automatic wrapping is to be performed. When a line approaches this length, the serializer begins looking for opportunities to break the line. Generally it will break on any ASCII white space character (tab, carriage return, linefeed, and space). In some circumstances the serializer may not be able to break the line before the maximum length is reached. For instance, if an element name is longer than the maximum line length the only way to correctly serialize it is to exceed the maximum line length. In this case, the serializer will exceed the maximum line length.
    
    The default value for maximum line length is 0, which is interpreted as no maximum line length. Setting this to a negative value just sets it to 0.
    
    When this variable is set to a value greater than 0, the serializer does not preserve white space. Spaces, tabs, carriage returns, and line feeds can all be interchanged at the serializer's discretion. Carriage returns, line feeds, and tabs will not be escaped with numeric character references.
    
    Inside elements with an xml:space="preserve" attribute, the maximum line length is not enforced, regardless of the setting of the this property, unless, of course, an xml:space="default" attribute overrides the xml:space="preserve" attribute.
    
    Parameters:
    maxLength - the preferred maximum line length
    

    译文:

    设置此串行化器的建议的最大行长度。将此设置为0表示不执行自动包装。当一行接近此长度时,序列化程序开始寻找打断该行的机会。通常,它会在任何ASCII空白字符(制表符、回车符、换行符和空格)上中断。在某些情况下,串行器可能无法在达到最大长度之前中断该行。例如,如果元素名称比最大行长度长,正确序列化它的唯一方法是超过最大行长度。在这种情况下,串行化器将超过最大行长度。
    
    最大行长度的默认值为0,这被解释为没有最大行长度。将其设置为负值只会将其设置为0。
    
    当此变量设置为大于0的值时,序列化程序不保留空白。空格、制表符、回车符和换行符都可以由序列化程序自行决定进行交换。回车、换行和制表符将不会用数字字符引用转义。
    
    内部元素有一个XML:空间=“保存”属性,最大行长度不会被强制执行,不管这个属性的设置是什么,除非XML:空间=“默认”属性覆盖XML:空间=“保存”属性。
    
    参数:
    最大长度-优选的最大线长度
    
  • write()

    API原文:

    Writes a DocType object onto the output stream using the current options.
    

    译文:

    使用当前选项将DocType对象写入输出流。
    
  • flush()

    API原文:

    Flushes the data onto the output stream. It is not enough to flush the output stream. You must flush the serializer object itself because it uses some internal buffering. The serializer will flush the underlying output stream.
    

    译文:

    将数据刷新到输出流。仅刷新输出流是不够的。必须刷新序列化程序对象本身,因为它使用一些内部缓冲。序列化程序将刷新基础输出流。
    

在XOM中包含一个Serializer类,这个类在format()中被用来讲XML转换为更具可读性的格式。如果调用toXML(),那么所有的东西都会混在一起,因此Serializer是一种便利工具。

示例代码:

public class People extends ArrayList<Person> {

    public People(String fileName) throws ParsingException, IOException {
        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 ParsingException, IOException {
        People p = new People("People.xml");
        System.out.println(p);
    }

}
/*
out:
[Person{first='z', last='zz'}, Person{first='c', last='cc'}, Person{first='d', last='dd'}]
 */

Preferences

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

示例代码:

public class PreferencesDemo {

    public static void main(String[] args) throws BackingStoreException {
        Preferences prefs = Preferences
                .userNodeForPackage(PreferencesDemo.class);
        prefs.put("Location", "sd");
        prefs.put("zz", "cd");
        prefs.putInt("age", 21);
        prefs.putBoolean("Are you Robot?", false);

        int usageCount = prefs.getInt("UsageCount", 0);
        usageCount++;
        for (String key :
                prefs.keys()) {
            System.out.println("Key:" + key + ",value:" + prefs.get(key, null));
        }

        System.out.println("How old are you?" + prefs.get("age", String.valueOf(0)));
    }

}
/*
out:
Key:Location,value:sd
Key:zz,value:cd
Key:age,value:21
Key:Are you Robot?,value:false
How old are you?21
 */

示例中使用的是userNodeForPackage(),但是也可用systemNodeForPackage(),虽然可以任意选择,但是最好将“user”用于个别用户的偏好,将“system”用于通用的安装配置。因为main()是静态的,因此PreferencesDemo.class可以用来表示节点;但是在非静态方法内部,我们通常使用getClass()。尽管我们不一定非要把当前的类作为节点标识符,但是这仍不失为一种很有用的方法。

一旦我们创建了节点,就可以用来加载或者读取数据了。

关于UsageCount存储的位置,Preferences API利用合适的系统资源完成了这个任务,并且这些资源会随操作系统的不同而不同。Windows会保存在注册表中。

发布了24 篇原创文章 · 获赞 8 · 访问量 1858

猜你喜欢

转载自blog.csdn.net/qq_40462579/article/details/104105409