1、File类

  File类是java.io包下代表与平台无关的文件和目录,也就是说, 如果希望在程序中操作文件和目录, 都可以通过File类来完成。 值得 指出的是, 不管是文件还是目录都是使用File来操作的, File能新 建、删除、重命名文件和目录, File不能访问文件内容本身。 如果需 要访问文件内容本身,则需要使用输入/输出流。

1.1、访问文件和目录

  File类可以使用文件路径字符串来创建File实例, 该文件路径字 符串既可以是绝对路径,也可以是相对路径。在默认情况下,系统总 是依据用户的工作路径来解释相对路径, 这个路径由系统属性 “user.dir”指定,通常也就是运行Java虚拟机时所在的路径。
  一旦创建了File对象后, 就可以调用File对象的方法来访问, File类提供了很多方法来操作文件和目录, 下面列出一些比较常用的方法。
  1.访问文件名相关方法
    ➢ String getName():返回此File对象所表示的文件名或路径名 (如果是路径,则返回最后一级子路径名)。
    ➢ String getPath():返回此File对象所对应的路径名。
    ➢ File getAbsoluteFile():返回此File对象的绝对路径。
    ➢ String getAbsolutePath():返回此File对象所对应的绝对路径名。
    ➢ String getParent():返回此File对象所对应目录(最后一级子目录)的父目录名。
    ➢ boolean renameTo(File newName):重命名此File对象所对应的文件或目录, 如果重命名成功, 则返回true;否则返回false。

  2.文件检测相关方法
    ➢ boolean exists():判断File对象所对应的文件或目录是否存 在。
    ➢ boolean canWrite():判断File对象所对应的文件和目录是否 可写。
    ➢ boolean canRead():判断File对象所对应的文件和目录是否 可读。
    ➢ boolean isFile():判断File对象所对应的是否是文件,而不 是目录。
    ➢ boolean isDirectory():判断File对象所对应的是否是目录,而不是文件。
    ➢ boolean isAbsolute():判断File对象所对应的文件或目录是 否是绝对路径。 该方法消除了不同平台的差异, 可以直接判断 File对象是否为绝对路径。 在UNIX/Linux/BSD等系统上, 如果 路径名开头是一条斜线(/),则表明该File对象对应一个绝对 路径;在Windows等系统上,如果路径开头是盘符,则说明它是 一个绝对路径。

  3.获取常规文件信息
    ➢ long lastModified():返回文件的最后修改时间。
    ➢ long length():返回文件内容的长度。

  4.文件操作相关方法
    ➢ boolean createNewFile():当此File对象所对应的文件不存 在时, 该方法将新建一个该File对象所指定的新文件, 如果创 建成功则返回true;否则返回false。
    ➢ boolean delete():删除File对象所对应的文件或路径。
    ➢ static File createTempFile(String prefix, String suffix):在默认的临时文件目录中创建一个临时的空文件,使 用给定前缀、系统生成的随机数和给定后缀作为文件名。 这是 一个静态方法, 可以直接通过File类来调用。 prefix参数必须 至少是3字节长。建议前缀使用一个短的、有意义的字符串,比 如 “hjb” 或 “mail” 。 suffix参数可以为null, 在这种情况 下,将使用默认的后缀“.tmp”。
    ➢static File createTempFile(String prefix, String suffix, File directory):在directory所指定的目录中创建 一个临时的空文件, 使用给定前缀、系统生成的随机数和给定 后缀作为文件名。 这是一个静态方法, 可以直接通过File类来 调用。
    ➢ void deleteOnExit():注册一个删除钩子, 指定当Java虚拟 机退出时,删除File对象所对应的文件和目录。

  5.目录操作相关方法
    ➢ boolean mkdir():试图创建一个File对象所对应的目录, 如 果创建成功,则返回true;否则返回false。调用该方法时File 对象必须对应一个路径,而不是一个文件。
    ➢ String[] list():列出File对象的所有子文件名和路径名, 返回String数组。
    ➢ File[] listFiles():列出File对象的所有子文件和路径,返 回File数组。
    ➢ static File[] listRoots():列出系统所有的根路径。 这是 一个静态方法,可以直接通过File类来调用。

  上面详细列出了File类的常用方法,下面程序以几个简单方法来 测试一下File类的功能。

public class FileTest {public static void main(String[] args)throws IOException {// 以当前路径来创建一个File对象File file = new File(".");// 直接获取文件名,输出一点System.out.println(file.getName());// 获取相对路径的父路径可能出错,下面代码输出nullSystem.out.println(file.getParent());// 获取绝对路径System.out.println(file.getAbsoluteFile());// 获取上一级路径System.out.println(file.getAbsoluteFile().getParent());// 在当前路径下创建一个临时文件File tmpFile = File.createTempFile("aaa", ".txt", file);// 指定当JVM退出时删除该文件tmpFile.deleteOnExit();// 以系统当前时间作为新文件名来创建新文件File newFile = new File(System.currentTimeMillis() + "");System.out.println("newFile对象是否存在:" + newFile.exists());// 以指定newFile对象来创建一个文件newFile.createNewFile();// 以newFile对象来创建一个目录,因为newFile已经存在,// 所以下面方法返回false,即无法创建该目录newFile.mkdir();// 使用list()方法来列出当前路径下的所有文件和路径String[] fileList = file.list();System.out.println("====当前路径下所有文件和路径如下====");assert fileList != null;for (String fileName : fileList) {System.out.println(fileName);}// listRoots()静态方法列出所有的磁盘根路径。File[] roots = File.listRoots();System.out.println("====系统所有根路径如下====");for (File root : roots) {System.out.println(root);}}
}

  运行上面程序, 可以看到程序列出当前路径的所有文件和路径时,列出了程序创建的临时文件,但程序运行结束后,aaa.txt临时文 件并不存在,因为程序指定虚拟机退出时自动删除该文件。
  上面程序还有一点需要注意, 当使用System.out.println(file.getParent());相对路径的File对象来获取父路径时可能引起错误, 因为该方法返回将File对象所对应的目录 名、文件名里最后一个子目录名、子文件名删除后的结果。

1.2、文件过滤器

  在File类的list()方法中可以接收一个FilenameFilter参数,通过该参数可以只列出符合条件的文件。这里的FilenameFilter接口和 javax.swing.filechooser包下的FileFilter抽象类的功能非常相似,可以把FileFilter当成FilenameFilter的实现类。
  FilenameFilter 接 口 里 包 含 了 一 个 accept(File dir, String name)方法, 该方法将依次对指定File的所有子目录或者文件进行迭代,如果该方法返回true,则list()方法会列出该子目录或者文件。

public class FilenameFilterTest {public static void main(String[] args) {File file = new File(".");// FilenameFilter接口内只有一个抽象方法, 因此该接口也是一 个函数式接口,可使用Lambda表达式创建实现该接口的对象。// 使用Lambda表达式(目标类型为FilenameFilter)实现文件过滤器。// 如果文件名以.java结尾,或者文件对应一个路径,返回trueString[] nameList = file.list((dir, name) -> name.endsWith(".java") || new File(name).isDirectory());assert nameList != null;for (String name : nameList) {System.out.println(name);}}
}

  运行上面程序, 将看到当前路径下所有的*.java文件以及文件夹被列出。

2、理解Java的IO流

  Java的IO流是实现输入/输出的基础,它可以方便地实现数据的输 入/输出操作,在Java中把不同的输入/输出源(键盘、文件、网络连 接等)抽象表述为“流”(stream), 通过流的方式允许Java程序使 用相同的方式来访问不同的输入/输出源。stream是从起源(source) 到接收(sink)的有序数据。
  Java把所有传统的流类型(类或抽象类)都放在java.io包中,用 以实现输入/输出功能。

2.1、流的分类

  按照不同的分类方式, 可以将流分为不同的类型,下面从不同的 角度来对流进行分类,它们在概念上可能存在重叠的地方。
  1.输入流和输出流
  按照流的流向来分,可以分为输入流和输出流。
    ➢ 输入流:只能从中读取数据,而不能向其写入数据。
    ➢ 输出流:只能向其写入数据,而不能从中读取数据。
  Java的输入流主要由InputStream和Reader作为基类,而输出流则 主要OutputStream和Writer作为基类。 它们都是一些抽象基类, 无法直接创建实例。

  2.字节流和字符流
  字节流和字符流的用法几乎完全一样, 区别在于字节流和字符流所操作的数据单元不同——字节流操作的数据单元是8位的字节,而 符流操作的数据单元是16位的字符。
  字节流主要由InputStream和OutputStream作为基类,而字符流则主要Reader和Writer作为基类。

  3.节点流和处理流
  按照流的角色来分,可以分为节点流和处理流。
  可以从/向一个特定的IO设备(如磁盘、网络)读/写数据的流, 称为节点流, 节点流也被称为低级流(Low Level Stream)。 下图显示了节点流示意图。

  从上图中可以看出,当使用节点流进行输入/输出时,程序直接 连接到实际的数据源,和实际的输入/输出节点连接。
  处理流则用于对一个已存在的流进行连接或封装, 通过封装后的 流来实现数据读/写功能。处理流也被称为高级流。下图显示了处理流示意图。

  从上图中可以看出,当使用处理流进行输入/输出时,程序并不 会直接连接到实际的数据源,没有和实际的输入/输出节点连接。使用 处理流的一个明显好处是,只要使用相同的处理流,程序就可以采用 完全相同的输入/输出代码来访问不同的数据源,随着处理流所包装节 点流的变化,程序实际所访问的数据源也相应地发生变化。
  实际上,Java使用处理流来包装节点流是一种典型的装饰器设 计模式,通过使用处理流来包装不同的节点流,既可以消除不同节 点流的实现差异,也可以提供更方便的方法来完成输入/输出功能。 因此处理流也被称为包装流。

2.2、流的概念模型

  Java把所有设备里的有序数据抽象成流模型,简化了输入/输出处 理,理解了流的概念模型也就了解了Java IO。
  Java的IO流共涉及40多个类,这些类看上去芜杂而凌乱,但实际 上非常规则, 而且彼此之间存在非常紧密的联系。 Java的IO流的40多 个类都是从如下4个抽象基类派生的。
  ➢ InputStream/Reader:所有输入流的基类, 前者是字节输入 流,后者是字符输入流。
  ➢ OutputStream/Writer:所有输出流的基类, 前者是字节输出 流,后者是字符输出流。
  对于InputStream和Reader而言,它们把输入设备抽象成一个“水 管”,这个水管里的每个“水滴”依次排列,如下图所示。

  从上图中可以看出, 字节流和字符流的处理方式其实非常相似,只是它们处理的输入/输出单位不同而已。输入流使用隐式的记录 指针来表示当前正准备从哪个“水滴”开始读取, 每当程序从 InputStream或Reader里取出一个或多个“水滴”后,记录指针自动向后移动;除此之外,InputStream和Reader里都提供一些方法来控制记 录指针的移动。
  对于OutputStream和Writer而言, 它们同样把输出设备抽象成一 个“水管”,只是这个水管里没有任何水滴,如下图所示。

  正如上图所示,当执行输出时,程序相当于依次把“水滴”放入到输出流的水管中,输出流同样采用隐式的记录指针来标识当前水滴即将放入的位置, 每当程序向OutputStream或Writer里输出一个或多个水滴后,记录指针自动向后移动。
  除了Java IO流的基本概念模型之外, 还有Java处理流,Java的处理流模型则体现了Java输入/输出流设计的灵活性。处理流的功能主要体现在以下两个方面。
  ➢ 性能的提高:主要以增加缓冲的方式来提高输入/输出的效率。
  ➢ 操作的便捷:处理流可能提供了一系列便捷的方法来一次输入/输出大批量的内容, 而不是输入/输出一个或多个“水滴”。
  处理流可以“嫁接”在任何已存在的流的基础之上, 这就允许 Java应用程序采用相同的代码、透明的方式来访问不同的输入/输出设 备的数据流。下图显示了处理流的模型。

  通过使用处理流,Java程序无须理会输入/输出节点是磁盘、网络 还是其他的输入/输出设备,程序只要将这些节点流包装成处理流,就 可以使用相同的输入/输出代码来读写不同的输入/输出设备的数据。

3、字节流和字符流

3.1、InputStream和Reader

  InputStream和Reader是所有输入流的抽象基类,本身并不能创建实例来执行输入,但它们将成为所有输入流的模板,所以它们的方法是所有输入流都可使用的方法。
  在InputStream里包含如下三个方法。
    ➢ int read():从输入流中读取单个字节(相当于从水管中取出一滴水),返回所读取的字节数据(字节数据可 直接转换为int类型)。
    ➢ int read(byte[] b):从输入流中最多读取b.length个字节的 数据,并将其存储在字节数组b中,返回实际读取的字节数。
    ➢ int read(byte[] b, int off, int len):从输入流中最多读 取len个字节的数据,并将其存储在数组b中,放入数组b中时, 并不是从数组起点开始,而是从off位置开始,返回实际读取的 字节数。 在Reader里包含如下三个方法。

  在Reader里包含如下三个方法。
    ➢ int read():从输入流中读取单个字符(相当于从水管中取出一滴水),返回所读取的字符数据(字符数据可 直接转换为int类型)。
    ➢ int read(char[] cbuf):从输入流中最多读取cbuf.length个 字符的数据, 并将其存储在字符数组cbuf中, 返回实际读取的 字符数。
    ➢ int read(char[] cbuf, int off, int len):从输入流中最多读取len个字符的数据,并将其存储在字符数组cbuf中,放入 数组cbuf中时,并不是从数组起点开始,而是从off位置开始,返回实际读取的字符数。

  对比InputStream和Reader所提供的方法,就不难发现这两个基类的功能基本是一样的。InputStream和Reader都是将输入数据抽象成如之前上图所示的水管, 所以程序既可以通过read()方法每次读取一个 “水滴”,也可以通过read(char[] cbuf)或read(byte[] b)方法来读 取多个“水滴”。 当使用数组作为read()方法的参数时, 可以理解为 使用一个“竹筒”到如图15.5所示的水管中取水, 如图下所示。 read(char[]cbuf)方法中的数组可理解成一个“竹筒”, 程序每次调用输入流的read(char[] cbuf)或read(byte[] b)方法, 就相当于用 “竹筒”从输入流中取出一筒“水滴”,程序得到“竹筒”里的“水 滴”后,转换成相应的数据即可;程序多次重复这个“取水”过程, 直到最后。程序如何判断取水取到了最后呢?直到read(char[] cbuf) 或read(byte[] b)方法返回−1,即表明到了输入流的结束点。

  正如前面提到的,InputStream和Reader都是抽象类,本身不能创 建实例,但它们分别有一个用于读取文件的输入流:FileInputStream 和FileReader, 它们都是节点流——会直接和指定文件关联。 下面程 序示范了使用FileInputStream来读取自身的效果。

public class FileInputStreamTest {public static void main(String[] args) throws IOException {// 创建字节输入流InputStream fis = new FileInputStream("./java-base/src/main/java/com/yt/base/test/FileInputStreamTest.java");// 创建一个长度为1024的“竹筒”byte[] bbuf = new byte[1024];// 用于保存实际读取的字节数int hasRead = 0;// 使用循环来重复“取水”过程while ((hasRead = fis.read(bbuf)) > 0) {// 取出“竹筒”中水滴(字节),将字节数组转换成字符串输入!System.out.print(new String(bbuf, 0, hasRead));}// 关闭文件输入流,放在finally块里更安全fis.close();}
}

  上面程序创建了一个长度为1024的字节数组来读取该文件,实 际上该Java源文件的长度还不到1024字节,也就是说,程序只需要 执行一次read()方法即可读取全部内容。但如果创建较小长度的字 节数组,程序运行时在输出中文注释时就可能出现乱码—这是因为 本文件保存时采用的是UTF-8编码方式,在这种方式下,一个中文等于三个字节,如果read()方法读取时只读到了半个中文字符,这将导致乱码。
  上面程序最后使用了fis.close()来关闭该文件输入流,与JDBC编程一样,程序里打开的文件IO资源不属于内存里的资源,垃圾回收机 制无法回收该资源, 所以应该显式关闭文件IO资源。 Java 7改写了所 有的IO资源类,它们都实现了AutoCloseable接口,因此都可通过自动关闭资源的try语句来关闭这些IO流。下面程序使用FileReader来读取 文件本身。

public class FileReaderTest {public static void main(String[] args) {// 创建字符输入流try (Reader fr = new FileReader("FileReaderTest.java")) {// 创建一个长度为32的“竹筒”char[] cbuf = new char[32];// 用于保存实际读取的字符数int hasRead = 0;// 使用循环来重复“取水”过程while ((hasRead = fr.read(cbuf)) > 0) {// 取出“竹筒”中水滴(字符),将字符数组转换成字符串输入!System.out.print(new String(cbuf, 0, hasRead));}} catch (IOException ex) {ex.printStackTrace();}}
}

  上面的FileReaderTest.java程序与前面的FileInputStreamTest.java并没有太大的不同, 程序只是将字符数组的长度改为32, 这意味着程序需要多次调用read()方法才可以完全读取输入流的全部数据。程序最后使用了自动关闭资源的try语句来关闭文件输入流,这样可以保证输入流一定会被关闭。
  除此之外,InputStream和Reader还支持如下几个方法来移动记录指针。
    ➢ void mark(int readAheadLimit):在记录指针当前位置记录一个标记(mark)。
    ➢ boolean markSupported():判断此输入流是否支持mark()操作,即是否支持记录标记。
    ➢ void reset():将此流的记录指针重新定位到上一次记录标记(mark)的位置。
    ➢ long skip(long n):记录指针向前移动n个字节/字符。

3.2、OutputStream和Writer

  OutputStream和Writer也非常相似,两个流都提供了如下三个方法。
    ➢ void write(int c):将指定的字节/字符输出到输出流中,其中c既可以代表字节,也可以代表字符。
    ➢ void write(byte[]/char[] buf):将字节数组/字符数组中的 数据输出到指定输出流中。
    ➢ void write(byte[]/char[] buf, int off, int len):将字 节数组/字符数组中从off位置开始, 长度为len的字节/字符输 出到输出流中。

  因为字符流直接以字符作为操作单位, 所以Writer可以用字符串来代替字符数组, 即以String对象作为参数。 Writer里还包含如下两个方法。
    ➢ void write(String str):将str字符串里包含的字符输出到指定输出流中。
    ➢ void write(String str, int off, int len):将str字符串里从off位置开始,长度为len的字符输出到指定输出流中。

  下面程序使用FileInputStream来执行输入 ,并使用FileOutputStream来执行输 出 ,用以实现复制FileOutputStreamTest.java文件的功能。

public class FileOutputStreamTest {public static void main(String[] args) {try (// 创建字节输入流InputStream fis = new FileInputStream("./java-base/src/main/java/com/yt/base/test/FileOutputStreamTest.java");// 创建字节输出流OutputStream fos = new FileOutputStream("./java-base/src/main/java/com/yt/base/test/newFile.txt")) {byte[] bbuf = new byte[32];int hasRead = 0;// 循环从输入流中取出数据while ((hasRead = fis.read(bbuf)) > 0) {// 每读取一次,即写入文件输出流,读了多少,就写多少。fos.write(bbuf, 0, hasRead);}} catch (IOException ioe) {ioe.printStackTrace();}}
}

  运行上面程序,将看到系统当前路径下多了一个文件:newFile.txt,该文件的内容和FileOutputStreamTest.java文件的内容完全相同。
  使用Java的IO流执行输出时,不要忘记关闭输出流, 关闭输出 流除可以保证流的物理资源被回收之外,可能还可以将输出流缓冲 区中的数据flush到物理节点里(因为在执行close()方法之前, 自 动执行输出流的flush()方法)。Java的很多输出流默认都提供了缓 冲功能, 其实没有必要刻意去记忆哪些流有缓冲功能、哪些流没 有,只要正常关闭所有的输出流即可保证程序正常。
  如果希望直接输出字符串内容, 则使用Writer会有更好的效果, 如下程序所示。

public class FileWriterTest {public static void main(String[] args) {try (Writer fw = new FileWriter("./java-base/src/main/java/com/yt/base/test/poem.txt")) {fw.write("锦瑟 - 李商隐\r\n");fw.write("锦瑟无端五十弦,一弦一柱思华年。\r\n");fw.write("庄生晓梦迷蝴蝶,望帝春心托杜鹃。\r\n");fw.write("沧海月明珠有泪,蓝田日暖玉生烟。\r\n");fw.write("此情可待成追忆,只是当时已惘然。\r\n");} catch (IOException ioe) {ioe.printStackTrace();}}
}

  运行上面程序, 将会在当前目录下输出一个poem.txt文件, 文件 内容就是程序中输出的内容。
  上面程序在输出字符串内容时, 字符串内容的最后是\r\n, 这是Windows平台的换行符,通过这种方式就可以让输出内容换行;如 果是UNIX/Linux/BSD等平台,则使用\n就作为换行符。

4、输入/输出流体系

4.1、处理流的用法

  处理流可以隐藏底层设备上节点流的差 异,并对外提供更加方便的输入/输出方法,让程序员只需关心高级流的操作。
  使用处理流时的典型思路是, 使用处理流来包装节点流, 程序通 过处理流来执行输入/输出功能,让节点流与底层的I/O设备、文件交 互。
  实际识别处理流非常简单, 只要流的构造器参数不是一个物理节 点,而是已经存在的流,那么这种流就一定是处理流;而所有节点流 都是直接以物理IO节点作为构造器参数的。
  关于使用处理流的优势,归纳起来就是两点:①对开发人员来 说,使用处理流进行输入/输出操作更简单;②使用处理流的执行效率更高。
  下面程序使用PrintStream处理流来包装OutputStream,使用处理 流后的输出流在输出时将更加方便。

public class PrintStreamTest {public static void main(String[] args) {try (OutputStream fos = new FileOutputStream("./java-base/src/main/java/com/yt/base/test/test.txt");PrintStream ps = new PrintStream(fos)) {// 使用PrintStream执行输出ps.println("普通字符串");// 直接使用PrintStream输出对象ps.println(new PrintStreamTest());} catch (IOException ioe) {ioe.printStackTrace();}}
}

  上面程序中的代码先定义了一个节点输出流 FileOutputStream, 然后程序使用Print Stream包装了该节点输出流, 最后使用PrintStream输出字符串、输出对象……PrintStream的 输出功能非常强大, 前面程序中一直使用的标准输出System.out的类 型就是PrintStream。PrintStream类的输出功能非常强大,通常如果需要输出文本内容,都应该将输出流包装成PrintStream后进行输出。
  从上面的代码可以看出, 程序使用处理流非常简单, 通常只需要在创建处理流时传入一个节点流作为构造器参数即可,这样创建的处 理流就是包装了该节点流的处理流。
  在使用处理流包装了底层节点流之后, 关闭输入/输出流资源时,只要关闭最上层的处理流即可。关闭最上层的处理流时,系统会自动关闭被该处理流包装的节点流。还需要注意关闭顺序为:先使用的后关闭。

4.2、输入/输出流体系

  Java的输入/输出流体系提供了近40个类,这些类看上去杂乱而没 有规律,但如果将其按功能进行分类,则不难发现其是非常规律的。 如下图表显示了Java输入/输出流体系中常用的流分类。

  上表中的粗体字标出的类代表节点流,必须直接与指定的物理节点关联;斜体字标出的类代表抽象基类,无法直接创建实例。
  上表中列出了4个访问管道的流:PipedInputStream 、PipedOutputStream、PipedReader、PipedWriter,它们都是用于实现进程之间通信功能的,分别是字节输入流、字节输出流、字符输入流和字符输出流。
  上表中的4个缓冲流则增加了缓冲功能,增加缓冲功能可以提高输入、输出的效率,增加缓冲功能后需要使用flush()才可以将缓冲区的内容写入实际的物理节点。
  上表中的对象流主要用于实现对象的序列化。

  通常来说, 字节流的功能比字符流的功能强大, 因为计算机里所 有的数据都是二进制的, 而字节流可以处理所有的二进制文件——但 问题是,如果使用字节流来处理文本文件,则需要使用合适的方式把这些字节转换成字符,这就增加了编程的复杂度。所以通常有一个规则:如果进行输入/输出的内容是文本内容,则应该考虑使用字符流; 如果进行输入/输出的内容是二进制内容,则应该考虑使用字节流。
  上表仅仅总结了输入/输出流体系中位于java.io包下的流,还有一些诸AudioInputStream 、 CipherInputStream 、 DeflaterInputStream、ZipInputStream等具有访问音频文件、加密/ 解密、压缩/解压等功能的字节流,它们具有特殊的功能,位于JDK的 其他包下。
  上表中还列出了一种以数组为物理节点的节点流,字节流以字节数组为节点,字符流以字符数组为节点;这种以数组为物理节点的 节点流除在创建节点流对象时需要传入一个字节数组或者字符数组之 外,用法上与文件节点流完全相似。与此类似的是,字符流还可以使 用字符串作为物理节点,用于实现从字符串读取内容,或将内容写入 字符串(用StringBuffer充当字符串)的功能。 下面程序示范了使用字符串作为物理节点的字符输入/输出流的用法。

public class StringNodeTest {public static void main(String[] args) {String src = "从明天起,做一个幸福的人\n"+ "喂马,劈柴,周游世界\n"+ "从明天起,关心粮食和蔬菜\n"+ "我有一所房子,面朝大海,春暖花开\n"+ "从明天起,和每一个亲人通信\n"+ "告诉他们我的幸福\n";char[] buffer = new char[32];int hasRead = 0;try (Reader sr = new StringReader(src)) {// 采用循环读取的访问读取字符串while ((hasRead = sr.read(buffer)) > 0) {System.out.print(new String(buffer, 0, hasRead));}} catch (IOException ioe) {ioe.printStackTrace();}try (// 创建StringWriter时,实际上以一个StringBuffer作为输出节点// 下面指定的20就是StringBuffer的初始长度Writer sw = new StringWriter(20)) {// 调用StringWriter的方法执行输出sw.write("有一个美丽的新世界,\n");sw.write("她在远方等我,\n");sw.write("那里有天真的孩子,\n");sw.write("还有姑娘的酒窝\n");System.out.println("----下面是sw的字符串节点里的内容----");// 使用toString()方法返回StringWriter的字符串节点的内容System.out.println(sw.toString());} catch (IOException ex) {ex.printStackTrace();}}
}

  上面程序与前面使用FileReader和FileWriter的程序基本相似, 只是在创建StringReader和StringWriter对象时传入的是字符串节 点, 而不是文件节点。 由于String是不可变的字符串对象, 所以StringWriter使用StringBuffer作为输出节点。

4.3、转换流

  输入/输出流体系中还提供了两个转换流,这两个转换流用于实现 将字节流转换成字符流,其中InputStreamReader将字节输入流转换成 字符输入流,OutputStreamWriter将字节输出流转换成字符输出流。
  下面以获取键盘输入为例来介绍转换流的用法 。 Java 使用System.in 代表标 准输入 ,即键盘输入 ,但这个标准输入流是InputStream类的实例,使用不太方便,而且键盘输入内容都是文本内 容,所以可以使用InputStreamReader将其转换成字符输入流,普通的Reader读取输入内容时依然不太方便, 可以将普通的Reader再次包装BufferedReader, 利用BufferedReader的readLine()方法可以一次 读取一行内容。如下程序所示。

public class KeyinTest {public static void main(String[] args) {try (// 将Sytem.in对象转换成Reader对象InputStreamReader reader = new InputStreamReader(System.in);// 将普通Reader包装成BufferedReaderBufferedReader br = new BufferedReader(reader)) {String line = null;// 采用循环方式来一行一行的读取while ((line = br.readLine()) != null) {// 如果读取的字符串为"exit",程序退出if (line.equals("exit")) {System.exit(1);}// 打印读取的内容System.out.println("输入内容为:" + line);}} catch (IOException ioe) {ioe.printStackTrace();}}
}

4.4、推回输入流

  在输入/输出流体系中,有两个特殊的流与众不同,就是PushbackInputStream和PushbackReader, 它们都提供了如下三个方法。
    ➢ void unread(byte[]/char[] buf):将一个字节/字符数组内 容推回到推回缓冲区里,从而允许重复读取刚刚读取的内容。
    ➢ void unread(byte[]/char[] b, int off, int len):将一个 字节/字符数组里从off开始, 长度为len字节/字符的内容推回 到推回缓冲区里,从而允许重复读取刚刚读取的内容。
    ➢ void unread(int b):将一个字节/字符推回到推回缓冲区 里,从而允许重复读取刚刚读取的内容。
  细心的读者可能已经发现了这三个方法与InputStream和Reader中 的三个read() 方法一一对 应 ,没错 ,这三个方法就是PushbackInputStream和PushbackReader的奥秘所在。
  这两个推回输入流都带有一个推回缓冲区, 当程序调用这两个推 回输入流的unread()方法时, 系统将会把指定数组的内容推回到该缓 冲区里, 而推回输入流每次调用read()方法时总是先从推回缓冲区读 取, 只有完全读取了推回缓冲区的内容后, 但还没有装满read()所需 的数组时才会从原输入流中读取。下图显示了这种推回输入流的处 理示意图。

  根据上面的介绍可以知道,当程序创建一个PushbackInputStream 和PushbackReader时需要指定推回缓冲区的大小, 默认的推回缓冲区 的长度为1。如果程序中推回到推回缓冲区的内容超出了推回缓冲区的 大小,将会引发Pushback buffer overflow的IOException异常。
  下面程序试图找出程序中的"new PushbackReader" 字符串,当找到该字符串后,程序只是打印出目标字符串之前的内容。

public class PushbackTest {public static void main(String[] args) {try (// 创建一个PushbackReader对象,指定推回缓冲区的长度为64PushbackReader pr = new PushbackReader(new FileReader("./java-base/src/main/java/com/yt/base/test/PushbackTest.java"), 64)) {char[] buf = new char[32];// 用以保存上次读取的字符串内容String lastContent = "";int hasRead = 0;// 循环读取文件内容while ((hasRead = pr.read(buf)) > 0) {// 将读取的内容转换成字符串String content = new String(buf, 0, hasRead);int targetIndex = 0;// 将上次读取的字符串和本次读取的字符串拼起来,// 查看是否包含目标字符串, 如果包含目标字符串if ((targetIndex = (lastContent + content).indexOf("new PushbackReader")) > 0) {// 将本次内容和上次内容一起推回缓冲区pr.unread((lastContent + content).toCharArray());// 重新定义一个长度为targetIndex的char数组if (targetIndex > 32) {buf = new char[targetIndex];}// 再次读取指定长度的内容(就是目标字符串之前的内容)pr.read(buf, 0, targetIndex);// 打印读取的内容System.out.print(new String(buf, 0, targetIndex));System.exit(0);} else {// 打印上次读取的内容System.out.print(lastContent);// 将本次内容设为上次读取的内容lastContent = content;}}} catch (IOException ioe) {ioe.printStackTrace();}}

  上面程序中代码实现了将指定内容推回到推回缓冲区,于是当程序再次调用read()方法时,实际上只是读取了推回缓冲区的部分内容,从而实现了只打印目标字符串前面内容的功能。

5、重定向标准输入/输出

  在System类里提供了如下三个重定向标准输入/输出的方法。
    ➢ static void setErr(PrintStream err):重定向 “标准”错 误输出流。
    ➢ static void setIn(InputStream in):重定向“标准”输入 流。
    ➢ static void setOut(PrintStream out):重定向 “标准”输 出流。
  下面程序通过重定向标准输出流,将System.out的输出重定向到 文件输出,而不是在屏幕上输出。

public class RedirectOut {public static void main(String[] args) {try (// 一次性创建PrintStream输出流PrintStream ps = new PrintStream(new FileOutputStream("./java-base/src/main/java/com/yt/base/test/out.txt"))) {// 将标准输出重定向到ps输出流System.setOut(ps);// 向标准输出输出一个字符串System.out.println("普通字符串");// 向标准输出输出一个对象System.out.println(new RedirectOut());} catch (IOException ex) {ex.printStackTrace();}}
}

  上面程序代码创建了一个PrintStream输出流,并将系统的标准输出重定向到该Print Stream输出流。 运行上面程序时将看 不到任何输出——这意味着标准输出不再输出到屏幕, 而是输出到 out.txt文件。
  下面程序重定向标准输入,从而可以将System.in重定向到指定文 件,而不是键盘输入。

public class RedirectIn {public static void main(String[] args) {try (FileInputStream fis = new FileInputStream("./java-base/src/main/java/com/yt/base/test/RedirectIn.java")) {// 将标准输入重定向到fis输入流System.setIn(fis);// 使用System.in创建Scanner对象,用于获取标准输入Scanner sc = new Scanner(System.in);// 增加下面一行将只把回车作为分隔符sc.useDelimiter("\n");// 判断是否还有下一个输入项while (sc.hasNext()) {// 输出输入项System.out.println("键盘输入的内容是:" + sc.next());}} catch (IOException ex) {ex.printStackTrace();}}
}

  上面程序中的代码创建了一个FileInputStream输入流,并使用System的setIn()方法将系统标准输入重定向到该文件输入流。运 行上面程序,程序不会等待用户输入,而是直接输出了 RedirectIn.java文件的内容, 这表明程序不再使用键盘作为标准输 入,而是使用RedirectIn.java文件作为标准输入源。

5、RandomAccessFile

  RandomAccessFile是Java输入/输出流体系中功能最丰富的文件内 容访问类,它提供了众多的方法来访问文件内容,它既可以读取文件 内容, 也可以向文件输出数据。 与普通的输入/输出流不同的是, RandomAccessFile支持“随机访问”的方式, 程序可以直接跳转到文 件的任意地方来读写数据。
  由于RandomAccessFile可以自由访问文件的任意位置, 所以如果 只需要访问文件部分内容,而不是把文件从头读到尾,使用 RandomAccessFile将是更好的选择。
  与OutputStream、Writer等输出流不同的是, RandomAccessFile 允许自由定位文件记录指针, RandomAccessFile可以不从开始的地方 开始输出, 因此RandomAccessFile可以向已存在的文件后追加内容。 如果程序需要向已存在的文件后追加内容,则应该使用 RandomAccessFile。
  RandomAccessFile的方法虽然多, 但它有一个最大的局限, 就是 只能读写文件,不能读写其他IO节点。
  RandomAccessFile对象也包含了一个记录指针, 用以标识当前读 写处的位置, 当程序新创建一个RandomAccessFile对象时, 该对象的 文件记录指针位于文件头(也就是0处),当读/写了 个字节后,文件 记录指针将会向后移动 个字节。除此之外,RandomAccessFile可以自 由移动该记录指针,既可以向前移动,也可以向后移动。 RandomAccessFile包含了如下两个方法来操作文件记录指针。
    ➢ long getFilePointer():返回文件记录指针的当前位置。
    ➢ void seek(long pos):将文件记录指针定位到pos位置。
  RandomAccessFile既可以读文件, 也可以写, 所以它既包含了完全类似于InputStream的三个read()方法, 其用法和InputStream的三 个read()方法完全一样;也包含了完全类似于OutputStream的三个 write()方法, 其用法和OutputStream的三个write()方法完全一样。 除 此 之 外 , RandomAccessFile 还 包 含 了 一 系 列 的 readXxx() 和writeXxx()方法来完成输入、输出。
  RandomAccessFile类有两个构造器, 其实这两个构造器基本相 同, 只是指定文件的形式不同而已—一个使用String参数来指定文件 名 , 一 个 使 用 File 参 数 来 指 定 文 件 本 身 。 除 此 之 外 , 创 建 RandomAccessFile 对 象 时 还需 要 指 定 一个 mode参 数 , 该 参 数指 定 RandomAccessFile的访问模式,该参数有如下4个值。
    ➢ “r” : 以 只 读 方 式 打 开 指 定 文 件 。 如 果 试 图 对 该 RandomAccessFile执行写入方法,都将抛出IOException异常。
    ➢ “rw”:以读、写方式打开指定文件。如果该文件尚不存在,则 尝试创建该文件。
    ➢ “rws” :以读、写方式打开指定文件。 相对于"rw" 模式, 还要 求对文件的内容或元数据的每个更新都同步写入到底层存储设 备。
    ➢ “rwd” :以读、写方式打开指定文件。 相对于"rw" 模式, 还要 求对文件内容的每个更新都同步写入到底层存储设备。
  下面程序使用了RandomAccessFile来访问指定的中间部分数据。

public class RandomAccessFileTest {public static void main(String[] args) {try (RandomAccessFile raf = new RandomAccessFile("./java-base/src/main/java/com/yt/base/test/RandomAccessFileTest.java", "r")) {// 获取RandomAccessFile对象文件指针的位置,初始位置是0System.out.println("RandomAccessFile的文件指针的初始位置:" + raf.getFilePointer());// 2-移动raf的文件记录指针的位置raf.seek(300);byte[] bbuf = new byte[1024];// 用于保存实际读取的字节数int hasRead = 0;// 使用循环来重复“取水”过程while ((hasRead = raf.read(bbuf)) > 0) {// 取出“竹筒”中水滴(字节),将字节数组转换成字符串输入!System.out.print(new String(bbuf, 0, hasRead));}} catch (IOException ex) {ex.printStackTrace();}}
}

  上面程序中的第一行代码创建了一个RandomAccessFile对 象,该对象以只读方式打开了RandomAccessFileTest.java文件,这意 味着该RandomAccessFile对象只能读取文件内容,不能执行写入。
  程序中第2处字代码将文件记录指针定位到300处, 也就是说, 程序将从300字节处开始读、写, 程序接下来的部分与使用 InputStream读取并没有太大的区别。运行上面程序,将看到程序只读 取后面部分的效果。
  下面程序示范了如何向指定文件后追加内容, 为了追加内容, 程 序应该先将记录指针移动到文件最后,然后开始向文件中输出内容。

public class AppendContent {public static void main(String[] args) {try (// 1——以读、写方式打开一个RandomAccessFile对象RandomAccessFile raf = new RandomAccessFile("./java-base/src/main/java/com/yt/base/test/out.txt", "rw")) {// 2——将记录指针移动到out.txt文件的最后raf.seek(raf.length());raf.write("追加的内容!\r\n".getBytes());} catch (IOException ex) {ex.printStackTrace();}}
}

  上面程序中的第1处代码先以读、写方式创建了一个 RandomAccessFile对象, 第2处码将RandomAccessFile对象的记录指针移动到最后;接下来使用RandomAccessFile执行输出,与使用OutputStream或Writer执行输出并没有太大区别。
  每运行上面程序一次,都可以看到out.txt文件中多一行“追加的 内容!”字符串,程序在该字符串后使用“\r\n”是为了控制换行。
  RandomAccessFile依然不能向文件的指定位置插入内容, 如果直接将文件记录指针移动到中间某位置后开始输出,则新输出的内容会覆盖文件中原有的内容。如果需要向指定位置插入内容,程序需要先把插入点后面的内容读入缓冲区,等把需要插入的数据写入 文件后,再将缓冲区的内容追加到文件后面。
  下面程序示例实现了向指定文件、指定位置插入内容的功能。

public class InsertContent {public static void insert(String fileName, long pos, String insertContent) throws IOException {// 创建临时文件File tmp = File.createTempFile("tmp", null);tmp.deleteOnExit();try (RandomAccessFile raf = new RandomAccessFile(fileName, "rw");// 使用临时文件来保存插入点后的数据FileOutputStream tmpOut = new FileOutputStream(tmp);FileInputStream tmpIn = new FileInputStream(tmp)) {raf.seek(pos);// ------下面代码将插入点后的内容读入临时文件中保存------byte[] bbuf = new byte[64];// 用于保存实际读取的字节数int hasRead = 0;// 使用循环方式读取插入点后的数据while ((hasRead = raf.read(bbuf)) > 0) {// 将读取的数据写入临时文件tmpOut.write(bbuf, 0, hasRead);}// ----------下面代码插入内容----------// 把文件记录指针重新定位到pos位置raf.seek(pos);// 追加需要插入的内容raf.write(insertContent.getBytes());// 追加临时文件中的内容while ((hasRead = tmpIn.read(bbuf)) > 0) {raf.write(bbuf, 0, hasRead);}}}public static void main(String[] args) throws IOException {insert("./java-base/src/main/java/com/yt/base/test/InsertContent.java", 45, "插入的内容\r\n");}
}

  多线程断点的网络下载工具( 如 FlashGet 等 )就可通过RandomAccessFile类来实现, 所有的下载工具在下载开始时都会建 立两个文件:一个是与被下载文件大小相同的空文件,一个是记录 文件指针的位置文件,下载工具用多条线程启动输入流来读取网络 数据, 并使用RandomAccessFile将从网络上读取的数据写入前面建 立的空文件中,每写一些数据后,记录文件指针的文件就分别记下 每个RandomAccessFile当前的文件指针位置——网络断开后, 再次 开始下载时, 每个RandomAccessFile都根据记录文件指针的文件中 记录的位置继续向下写数据。

6、对象序列化

  对象序列化的目标是将对象保存到磁盘中, 或允许在网络中直接传输对象。 对象序列化机制允许把内存中的Java对象转换成平台无关的二进制流,从而允许把这种二进制流持久地保存在磁盘上,通过网络将这种二进制流传输到另一个网络节点。其他程序一旦获得了这种 二进制流(无论是从磁盘中获取的,还是通过网络获取的),都可以将这种二进制流恢复成原来的Java对象。

6.1、序列化的含义和意义

  序列化机制允许将实现序列化的Java对象转换成字节序列, 这些 字节序列可以保存在磁盘上,或通过网络传输,以备以后重新恢复成 原来的对象。序列化机制使得对象可以脱离程序的运行而独立存在。
  对象的序列化(Serialize)指将一个Java对象写入IO流中,与此对应的是,对象的反序列化(Deserialize)则指从IO流中恢复该Java 对象。
  Java 9增强了对象序列化机制, 它允许对读入的序列化数据进行 过滤,这种过滤可在反序列化之前对数据执行校验,从而提高安全性 和健壮性。
  如果需要让某个对象支持序列化机制, 则必须让它的类是可序列 化的(serializable)。 为了让某个类是可序列化的, 该类必须实现 如下两个接口之一。
    ➢ Serializable
    ➢ Externalizable
  Java的很多类已经实现了Serializable, 该接口是一个标记接 口,实现该接口无须实现任何方法,它只是表明该类的实例是可序列 化的。
  所有可能在网络上传输的对象的类都应该是可序列化的, 否则程 序将会出现异常, 比如RMI(Remote Method Invoke, 即远程方法调 用,是Java EE的基础)过程中的参数和返回值;所有需要保存到磁盘里的对象的类都必须可序列化 ,比如Web应用中需 要保存到HttpSession或ServletContext属性的Java对象。
  因为序列化是RMI过程的参数和返回值都必须实现的机制, 而RMI又是Java EE技术的基础——所有的分布式应用常常需要跨平台、跨网 络,所以要求所有传递的参数、返回值必须实现序列化。因此序列化 机制是Java EE平台的基础。通常建议:程序创建的每个JavaBean类都实现Serializable。

6.2、使用对象流实现序列化

  下面程序定义了一个Person类, 这个Person类就是一个普通的Java类, 只是实现了Serializable接口, 该接口标识该类的对象是可序列化的。

public class Person implements Serializable {private String name;private int age;// 注意此处没有提供无参数的构造器!public Person(String name, int age) {System.out.println("有参数的构造器");this.name = name;this.age = age;}// name的setter和getter方法public void setName(String name) {this.name = name;}public String getName() {return this.name;}// age的setter和getter方法public void setAge(int age) {this.age = age;}public int getAge() {return this.age;}
}

  下面程序使用ObjectOutputStream将一个Person对象写入磁盘文件。

public class WriteObject {public static void main(String[] args) {try (// 创建一个ObjectOutputStream输出流ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("./java-base/src/main/java/com/yt/base/test/serializable/object.txt"))) {Person per = new Person("孙悟空", 500);// 将per对象写入输出流oos.writeObject(per);} catch (IOException ex) {ex.printStackTrace();}}
}

  上面程序中先创建了一个ObjectOutputStream 输出流, 这个ObjectOutputStream输出流建立在一个文件输出流的基础之上;然后使用writeObject()方法将一个Person 对象写入输出流。 运行上面程序,将会看到生成了一个object.txt文 件,该文件的内容就是Person对象。
  如果希望从二进制流中恢复Java对象, 则需要使用反序列化。 反序列化的步骤如下。下面程序示范了从刚刚生成的object.txt文件中读取Person对象的步骤。

public class ReadObject {public static void main(String[] args) {try (// 创建一个ObjectInputStream输入流ObjectInputStream ois = new ObjectInputStream(new FileInputStream("./java-base/src/main/java/com/yt/base/test/serializable/object.txt"))) {// 从输入流中读取一个Java对象,并将其强制类型转换为Person类Person p = (Person) ois.readObject();System.out.println("名字为:" + p.getName() + "\n年龄为:" + p.getAge());} catch (Exception ex) {ex.printStackTrace();}}
}

  上面程序中将一个文件输入流包装成 ObjectInputStream输入流,然后使用readObject()读取了文件中的Java对象,这就完成了反序列化过程。
  必须指出的是, 反序列化读取的仅仅是Java对象的数据,而不是Java类, 因此采用反序列化恢复Java对象时, 必须提供该Java对象所 属类的class文件,否则将会引发ClassNotFoundException异常。
  还有一点需要指出:Person类只有一个有参数的构造器,没有无参数的构造器,而且该构造器内有一个普通的打印语句。当反序列化 读取Java对象时,并没有看到程序调用该构造器, 这表明反序列化机制无须通过构造器来初始化Java对象。
  如果使用序列化机制向文件中写入了多个Java对象, 使用反序列 化机制恢复对象时必须按实际写入的顺序读取。
  当一个可序列化类有多个父类时(包括直接父类和间接父类), 这些父类要么有无参数的构造器, 要么也是可序列化的——否则反序 列化时将抛出InvalidClassException异常。 如果父类是不可序列化 的,只是带有无参数的构造器,则该父类中定义的成员变量值不会序 列化到二进制流中。

6.3、对象引用的序列化

  前面介绍的Person类的两个成员变量分别是String类型和int类 型, 如果某个类的成员变量的类型不是基本类型或String类型, 而是 另一个引用类型,那么这个引用类必须是可序列化的,否则拥有该类 型成员变量的类也是不可序列化的。
  如下Teacher类持有一个Person类的引用,只有Person类是可序列化的,Teacher类才是可序列化的。如果Person类不可序列化,则无论 Teacher类是否实现Serilizable、Externalizable接口,则Teacher类都是不可序列化的。

public class Teacher implements Serializable {private String name;private Person student;public Teacher(String name, Person student) {this.name = name;this.student = student;}// 此处省略了name和student的setter和getter方法// name的setter和getter方法public void setName(String name) {this.name = name;}public String getName() {return this.name;}// student的setter和getter方法public void setStudent(Person student) {this.student = student;}public Person getStudent() {return this.student;}
}

  现在假设有如下一种特殊情形:程序中有两个Teacher对象,它们 的student实例变量都引用到同一个Person对象,而且该Person对象还 有一个引用变量引用它。代码如下:

        Person per = new Person("孙悟空", 500);Teacher t1 = new Teacher("唐僧", per);Teacher t2 = new Teacher("菩提祖师", per);

  上面代码创建了两个Teacher对象和一个Person对象,这三个对象 在内存中的存储示意图如下图所示。

  这里产生了一个问题——如果先序列化t1对象, 则系统将该t1对 象所引用的Person对象一起序列化;如果程序再序列化t2对象, 系统将一样会序列化该t2对象,并且将再次序列化该t2对象所引用的 Person对象;如果程序再显式序列化per对象,系统将再次序列化该 Person对象。这个过程似乎会向输出流中输出三个Person对象。
  如果系统向输出流中写入了三个Person对象, 那么后果是当程序从输入流中反序列化这些对象时,将会得到三个Person对象,从而引 起t1和t2所引用的Person对象不是同一个对象,这显然与上图所示 的效果不一致——这也就违背了Java序列化机制的初衷。
  所以,Java序列化机制采用了一种特殊的序列化算法,其算法内容如下。
    ➢ 所有保存到磁盘中的对象都有一个序列化编号。
    ➢ 当程序试图序列化一个对象时,程序将先检查该对象是否已经被序列化过, 只有该对象从未(在本次虚拟机中)被序列化过,系统才会将该对象转换成字节序列并输出。
    ➢ 如果某个对象已经序列化过,程序将只是直接输出一个序列化编号,而不是再次重新序列化该对象。
  根据上面的序列化算法, 可以得到一个结论——当第二次、第三 次序列化Person对象时, 程序不会再次将Person对象转换成字节序列 并输出,而是仅仅输出一个序列化编号。假设有如下顺序的序列化代码:

 oos.writeObject(t1);oos.writeObject(t2);oos.writeObject(per);

  上面代码依次序列化了t1、t2和per对象,序列化后磁盘文件的存 储示意图如下图所示。

  通过上图可以很好地理解Java序列化的底层机制,通过该机制不难看出,当多次调用writeObject()方法输出同一个对象时,只有第一次调用writeObject()方法时才会将该对象转换成字节序列并输出。
  下面程序序列化了两个Teacher对象,两个Teacher对象都持有一个引用到同一个Person对象的引用,而且程序两次调用writeObject() 方法输出同一个Teacher对象。

public class WriteTeacher {public static void main(String[] args) {try (// 创建一个ObjectOutputStream输出流ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("./java-base/src/main/java/com/yt/base/test/serializable/teacher.txt"))) {Person per = new Person("孙悟空", 500);Teacher t1 = new Teacher("唐僧", per);Teacher t2 = new Teacher("菩提祖师", per);// 依次将四个对象写入输出流oos.writeObject(t1);oos.writeObject(t2);oos.writeObject(per);oos.writeObject(t2);} catch (IOException ex) {ex.printStackTrace();}}
}

  上面程序中的代码4次调用了writeObject()方法来输出对 象, 实际上只序列化了三个对象, 而且序列的两个Teacher对象的 student引用实际是同一个Person对象。下面程序读取序列化文件中的 对象即可证明这一点。

public class ReadTeacher {public static void main(String[] args) {try (// 创建一个ObjectInputStream输出流ObjectInputStream ois = new ObjectInputStream(new FileInputStream("./java-base/src/main/java/com/yt/base/test/serializable/teacher.txt"))) {// 依次读取ObjectInputStream输入流中的四个对象Teacher t1 = (Teacher) ois.readObject();Teacher t2 = (Teacher) ois.readObject();Person p = (Person) ois.readObject();Teacher t3 = (Teacher) ois.readObject();// 输出trueSystem.out.println("t1的student引用和p是否相同:" + (t1.getStudent() == p));// 输出trueSystem.out.println("t2的student引用和p是否相同:" + (t2.getStudent() == p));// 输出trueSystem.out.println("t2和t3是否是同一个对象:" + (t2 == t3));} catch (Exception ex) {ex.printStackTrace();}}
}

  上面程序中的代码依次读取了序列化文件中的4个Java对象,但通过后面比较判断, 不难发现t2和t3是同一个Java对象, t1的 student引用的、t2的student引用的和p引用变量引用的也是同一个 Java对象。
  由于Java序列化机制使然:如果多次序列化同一个Java对象时,只有第一次序列化时才会把该Java对象转换成字节序列并输出, 这样 可能引起一个潜在的问题——当程序序列化一个可变对象时, 只有第 一次使用writeObject()方法输出时才会将该对象转换成字节序列并输 出,当程序再次调用writeObject()方法时,程序只是输出前面的序列 化编号,即使后面该对象的实例变量值已被改变,改变的实例变量值 也不会被输出。程序如下。

public class SerializeMutable {public static void main(String[] args) {try (// 创建一个ObjectOutputStream输入流ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("./java-base/src/main/java/com/yt/base/test/serializable/mutable.txt"));// 创建一个ObjectInputStream输入流ObjectInputStream ois = new ObjectInputStream(new FileInputStream("./java-base/src/main/java/com/yt/base/test/serializable/mutable.txt"))) {Person per = new Person("孙悟空", 500);// 系统会per对象转换字节序列并输出oos.writeObject(per);// 改变per对象的name实例变量per.setName("猪八戒");// 系统只是输出序列化编号,所以改变后的name不会被序列化oos.writeObject(per);Person p1 = (Person) ois.readObject();    // ①Person p2 = (Person) ois.readObject();    // ②// 下面输出true,即反序列化后p1等于p2System.out.println(p1 == p2);// 下面依然看到输出"孙悟空",即改变后的实例变量没有被序列化System.out.println(p2.getName());} catch (Exception ex) {ex.printStackTrace();}}
}

  程序中第一段代码先使用writeObject()方法写入了一个 Person对象, 接着程序改变了Person对象的name实例变量值, 然后程 序再次输出Person对象, 但这次的输出已经不会将Person对象转换成 字节序列并输出了,而是仅仅输出了一个序列化编号。   程序中①②号代码两次调用readObject()方法读取了序列化文件中的Java对象, 比较两次读取的Java对象将完全相同, 程序输 出第二次读取的Person对象的name实例变量的值依然是“孙悟空”, 表明改变后的Person对象并没有被写入——这与Java序列化机制相符。
  当使用Java序列化机制序列化可变对象时一定要注意, 只有第 一次调用wirteObject()方法来输出对象时才会将对象转换成字节序 列, 并写入到ObjectOutputStream;在后面程序中即使该对象的实 例变量发生了改变,再次调用writeObject()方法输出该对象时,改变后的实例变量也不会被输出。

6.4、Java 9增加的过滤功能

  Java 9 为 ObjectInputStream 增 加 了 setObjectInputFilter() 、 getObjectInputFilter()两个方法, 其中第一个方法用于为对象输入 流设置过滤器。当程序通过ObjectInputStream反序列化对象时,过滤 器的checkInput()方法会被自动激发, 用于检查序列化数据是否有效。
  使用checkInput()方法检查序列化数据时有3种返回值。
    ➢ Status.REJECTED:拒绝恢复。
    ➢ Status.ALLOWED:允许恢复。
    ➢ Status.UNDECIDED:未决定状态,程序继续执行检查。
  ObjectInputStream将会根据ObjectInputFilter的检查结果来决定是否执行反序列化,如果checkInput()方法返回Status.REJECTED, 反序列化将会被阻止;如果checkInput()方法返回Status.ALLOWED, 程序将可执行反序列化。
  下面程序对前的ReadObject.java程序进行改进,该程序将会在反 序列化之前对数据执行检查。

public class FilterTest {public static void main(String[] args) {try (// 创建一个ObjectInputStream输入流ObjectInputStream ois = new ObjectInputStream(new FileInputStream("./java-base/src/main/java/com/yt/base/test/serializable/object.txt"))) {ois.setObjectInputFilter((info) -> {System.out.println("===执行数据过滤===");ObjectInputFilter serialFilter = ObjectInputFilter.Config.getSerialFilter();if (serialFilter != null) {// 首先使用ObjectInputFilter执行默认的检查ObjectInputFilter.Status status = serialFilter.checkInput(info);// 如果默认检查的结果不是Status.UNDECIDEDif (status != ObjectInputFilter.Status.UNDECIDED) {// 直接返回检查结果return status;}}// 如果要恢复的对象不是1个if (info.references() != 1) {// 不允许恢复对象return ObjectInputFilter.Status.REJECTED;}if (info.serialClass() != null &&// 如果恢复的不是Person类info.serialClass() != Person.class) {// 不允许恢复对象return ObjectInputFilter.Status.REJECTED;}return ObjectInputFilter.Status.UNDECIDED;});// 从输入流中读取一个Java对象,并将其强制类型转换为Person类Person p = (Person) ois.readObject();System.out.println("名字为:" + p.getName()+ "\n年龄为:" + p.getAge());} catch (Exception ex) {ex.printStackTrace();}}
}

  上 面 程 序 中 的 粗 体 字 代 码 为 ObjectInputStream 设 置 了 ObjectInputFilter过滤器(程序使用Lambda表达式创建过滤器),程 序重写了checkInput()方法。
  重写checkInput()方法时先使用默认的ObjectInputFilter执行检 查, 如果检查结果不是Status.UNDECIDED, 程序直接返回检查结果。 接下来程序通过FilterInfo检验序列化数据, 如果序列化数据中的对 象不唯一(数据已被污染),程序拒绝执行反序列化;如果序列化数 据中的对象不是Person对象(数据被污染), 程序拒绝执行反序列 化。 通过这种检查, 程序可以保证反序列化出来的是唯一的Person对 象,这样就让反序列化更加安全、健壮。

6.5、自定义序列化

  在一些特殊的场景下, 如果一个类里包含的某些实例变量是敏感 信息,例如银行账户信息等,这时不希望系统将该实例变量值进行序 列化;或者某个实例变量的类型是不可序列化的,因此不希望对该实例变量进行递归序列化,以避免引发 java.io.NotSerializableException异常。
  当对某个对象进行序列化时,系统会自动把该对象的所有实例 变量依次进行序列化,如果某个实例变量引用到另一个对象,则被 引用的对象也会被序列化;如果被引用的对象的实例变量也引用了 其他对象,则被引用的对象也会被序列化,这种情况被称为递归序列化。
  通过在实例变量前面使用transient关键字修饰,可以指定Java序 列化时无须理会该实例变量。 如下Person类与前面的Person类几乎完 全一样,只是它的age使用了transient关键字修饰。

public class Person implements Serializable {private String name;private transient int age; // 1// 注意此处没有提供无参数的构造器!public Person(String name, int age) {System.out.println("有参数的构造器");this.name = name;this.age = age;}// name的setter和getter方法public void setName(String name) {this.name = name;}public String getName() {return this.name;}// age的setter和getter方法public void setAge(int age) {this.age = age;}public int getAge() {return this.age;}
}

  下面程序先序列化一个Person对象, 然后再反序列化该Person对 象,得到反序列化的Person对象后程序输出该对象的age实例变量值。

public class TransientTest {public static void main(String[] args) {try (// 创建一个ObjectOutputStream输出流ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("./java-base/src/main/java/com/yt/base/test/serializable/transient.txt"));// 创建一个ObjectInputStream输入流ObjectInputStream ois = new ObjectInputStream(new FileInputStream("./java-base/src/main/java/com/yt/base/test/serializable/transient.txt"))) {Person per = new Person("孙悟空", 500);// 系统会per对象转换字节序列并输出oos.writeObject(per);Person p = (Person) ois.readObject();System.out.println(p.getAge());} catch (Exception ex) {ex.printStackTrace();}}
}

  使 用 transient 关 键 字 修 饰 实 例 变 量 虽 然 简 单 、 方 便 , 但 被 transient修饰的实例变量将被完全隔离在序列化机制之外,这样导致 在反序列化恢复Java对象时无法取得该实例变量值。 Java还提供了一 种自定义序列化机制,通过这种自定义序列化机制可以让程序控制如 何序列化各实例变量, 甚至完全不序列化某些实例变量(与使用 transient关键字的效果相同)。
  在序列化和反序列化过程中需要特殊处理的类应该提供如下特殊 签名的方法,这些特殊的方法用以实现自定义序列化。
    ➢ private void writeObject(java.io.ObjectOutputStream out) throws IOException
    ➢ private void readObject(java.io.ObjectInputStream in) throws IOException,ClassNotFoundException;
    ➢ private void readObjectNoData() throws ObjectStreamException;

  writeObject()方法负责写入特定类的实例状态, 以便相应的 readObject()方法可以恢复它。 通过重写该方法, 程序员可以完全获 得对序列化机制的控制,可以自主决定哪些实例变量需要序列化,需 要 怎 样 序 列 化 。 在 默 认 情 况 下 , 该 方 法 会 调 用 out.defaultWriteObject来保存Java对象的各实例变量,从而可以实现序列化Java对象状态的目的。
  readObject()方法负责从流中读取并恢复对象实例变量, 通过重 写该方法,程序员可以完全获得对反序列化机制的控制,可以自主决 定需要反序列化哪些实例变量,以及如何进行反序列化。在默认情况 下, 该方法会调用in.defaultReadObject来恢复Java对象的非瞬态实 例变量。 在通常情况下, readObject()方法与writeObject()方法对 应, 如果writeObject()方法中对Java对象的实例变量进行了一些处 理, 则应该在readObject()方法中对其实例变量进行相应的反处理, 以便正确恢复该对象。
  当序列化流不完整时, readObjectNoData()方法可以用来正确地 初始化反序列化的对象。例如,接收方使用的反序列化类的版本不同 于发送方,或者接收方版本扩展的类不是发送方版本扩展的类,或者 序列化流被篡改时, 系统都会调用readObjectNoData()方法来初始化反序列化的对象。

  下面的Person类提供了writeObject()和readObject()两个方法, 其中writeObject()方法在保存Person对象时将其name实例变量包装成 StringBuffer, 并将其字符序列反转后写入;在readObject()方法中 处 理 name 的 策 略 与 此 对 应 —— 先 将 读 取 的 数 据 强 制 类 型 转 换 成 StringBuffer,再将其反转后赋给name实例变量。

public class Person implements Serializable {private String name;private int age;// 注意此处没有提供无参数的构造器!public Person(String name, int age) {System.out.println("有参数的构造器");this.name = name;this.age = age;}// name的setter和getter方法public void setName(String name) {this.name = name;}public String getName() {return this.name;}// age的setter和getter方法public void setAge(int age) {this.age = age;}public int getAge() {return this.age;}private void writeObject(java.io.ObjectOutputStream out)throws IOException {// 将name实例变量的值反转后写入二进制流out.writeObject(new StringBuffer(name).reverse());out.writeInt(age);}private void readObject(java.io.ObjectInputStream in)throws IOException, ClassNotFoundException {// 将读取的字符串反转后赋给name实例变量this.name = ((StringBuffer) in.readObject()).reverse().toString();this.age = in.readInt();}
}

  上面程序中writeObject和readObject方法用以实现自定义序列化, 对于这个Person类而言, 序列化、反序列化Person实例并没有任何区别—区别在于序列化后的对象流,即使有Cracker截获到Person对象流,他看到的name也是加密后的name值,这样就提高了序列化的安全性。
  需要注意writeObject()方法存储实例变量的顺序应该和readObject()方 法中恢复实例变量的顺序一致,否则将不能正常恢复该Java对象。

  序列化就简单介绍到这,还有很多其它序列化方法和细节,可自行了解学习。

6.5、版本

  根据前面的介绍可以知道, 反序列化Java对象时必须提供该对象 的class文件, 现在的问题是, 随着项目的升级, 系统的class文件也 会升级,Java如何保证两个class文件的兼容性?
  Java序列化机制允许为序列化类提供一个private static final 的serialVersionUID值, 该类变量的值用于标识该Java类的序列化版 本, 也就是说, 如果一个类升级后, 只要它的serialVersionUID类变 量值保持不变,序列化机制也会把它们当成同一个序列化版本。
  分配serialVersionUID类变量的值非常简单, 例如下面代码片段:

public class Test implements Serializable {private static final long serialVersionUID = -5933396828971614107L;
}

  为了在反序列化时确保序列化版本的兼容性, 最好在每个要序列 化的类中加入private static final long serialVersionUID这个类 变量,具体数值自己定义。这样,即使在某个对象被序列化之后,它 所对应的类被修改了,该对象也依然可以被正确地反序列化。
  如果不显式定义serialVersionUID类变量的值, 该类变量的值将 由JVM根据类的相关信息计算,而修改后的类的计算结果与修改前的类 的计算结果往往不同,从而造成对象的反序列化因为类版本不兼容而失败。
  如果类的修改确实会导致该类反序列化失败, 则应该为该类的 serialVersionUID类变量重新分配值。 那么对类的哪些修改可能导致 该类实例的反序列化失败呢?下面分三种情况来具体讨论。
    ➢ 如果修改类时仅仅修改了方法,则反序列化不受任何影响,类 定义无须修改serialVersionUID类变量的值。
    ➢ 如果修改类时仅仅修改了静态变量或瞬态实例变量,则反序列 化不受任何影响, 类定义无须修改serialVersionUID类变量的 值。
    ➢ 如果修改类时修改了非瞬态的实例变量,则可能导致序列化版 本不兼容。 如果对象流中的对象和新类中包含同名的实例变 量, 而实例变量类型不同, 则反序列化失败, 类定义应该更新 serialVersionUID类变量的值。

java基础—输入/输出相关推荐

  1. Java的输入/输出

    在Java中,有输入和输出,也就是IO,然后给大家介绍一下: IO(输入/输出)是比较抽象的,看不到明显的运行效果,但输入和输出是所有程序都必需的部分.使用输入机制,允许程序读取外部(包括来自磁盘.光 ...

  2. Java基础 println 输出常量的示例

        JDK :OpenJDK-11      OS :CentOS 7.6.1810      IDE :Eclipse 2019‑03 typesetting :Markdown   code ...

  3. java基础输入_java基础之标准输入

    管与java的标准输入.就是System.in. 怎么读取标准输入里的内容呢....推荐使用Scanner和inputStream. 1:Scanner. 读取单个字符串. String s = sc ...

  4. java基础操作—输出语句,基本要求

    java新手必知 输出语句 class Student123{public static void main(String[] args){System.out.print();//不进行换行Syst ...

  5. java重定向输入/输出

    实现流水线时,需要将代码的输入输出改为从文件中读入写出,因为原代码中有大量输出函数,为了避免大幅度改动,便采用重定向. 头文件 import java.io.FileInputStream; impo ...

  6. Java基础练习题~输出100以内能同时被3和5整除的数.

    public class day5 {public static void main(String[] args){System.out.println("依次输出100以内能同时被3和5整 ...

  7. JAVA订单的输入输出_Java的输入/输出操作

    Java的输入\输出机制 计算机的基本功能就是通过输入输出设备与外部其他设备尽心数据的交互,从其他设备读入数据叫做输入操作,将计算机内的数据写入到其他设备叫做输出操作.可以向计算机发送数据.又可以接受 ...

  8. java数组循环动态赋值_04、Java基础语法(循环嵌套、break、continue、方法、方法的重载、数组动态初始化)...

    复习: 流程控制语句 - 顺序结构 - 分支结构 - if-else - switch - 循环结构 - for - while - do...while ctrl + alt + L 格式化代码 & ...

  9. Java基础知识小杂库

    大二上半学期:面向对象程序设计(Java) 一.简述JDK与JRE的区别 JRE(Java运行时环境),它相当于操作系统部分,提供了Java程序运行时所需要的基本条件和许多Java基础类,例如:IO类 ...

  10. 《Java基础入门第2版》--黑马程序员 课后答案及其详解 第7章 I/O流

    文章目录 一.填空题 二.判断题 三.选择题 四.简答题 五.编程题 六.原题及其解析 一.填空题 1. 字节流.字符流 2. Channel.Buffer 3. InputStreamReader. ...

最新文章

  1. linux socket通信tcp,基于TCP协议的socket通信
  2. 实现对文本的简单one-hot编码
  3. Java与.NET 的Web Services相互调用
  4. CCIE理论-第六篇-SD-WAN网络(一)
  5. PolarDB-X 云原生分布式数据库 > API参考 > API参考(2017版本) > 数据库管理类 API > 删除 DRDS 数据库
  6. Ext.form.DateField简单用法及日期范围控制
  7. Discuz! 的编码规范
  8. Cesium:加载OSGB倾斜摄影三维模型
  9. HBase二级索引实现方案
  10. 2.语音增强短时谱估计算法——幅度谱减法
  11. MacBook Pro App Store无法下载和更新软件解决方案
  12. 全球及中国游戏耳机市场运营模式分析及需求前景预测报告2022年版
  13. 操作系统习题(有一个具有两道作业的批处理系统)
  14. Ubuntu 21.04 如何进入命令行的登录界面
  15. Android RTL 及小语种 适配
  16. 小明的调查作业java_小明的调查作业
  17. 中小学计算机课程标准及解读,小学信息技术课课程标准及解读
  18. 2020前端开发者的面试(2),不定时更新
  19. 1.oracle的dump理解一 BH buffer header
  20. python爬虫抓取动态网页数据_python网络爬虫抓取ajax动态网页数据:以抓取KFC门店地址为例...

热门文章

  1. php whois查询,php whois查询API制作方法
  2. (附源码)Python音乐分类系统 毕业设计 250858
  3. 用Python把20年的GDP、人口以及房价数据进行了可视化
  4. 当联想失去“联想”(4)- PC+换汤必须换药
  5. linux malloc内存申请相关参数设置
  6. 水果店的售价应该怎么来定,水果店怎样确定价格
  7. python出现syntaxerror_Python SyntaxError语法错误原因及解决
  8. matlab图片测量尺寸_基于MATLAB的不规则面积图像测量
  9. 中国城市轨道交通与设备产业十四五建设规划与运营模式咨询报告2022-2028年
  10. python影评_用Python分析18万条《八佰》影评,看看观众怎么说?