疯狂Java讲义 第十五章 输入和输出(IO)

jefxff 153,583 2020-04-25

1. File 类

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

1.1 访问文件和目录

  1. File 类可以使用文件路径字符串来创建File实例, 该路径可以是绝对路径, 也可以是相对路径
  2. 一旦创建了 File 对象后, 就可以调用 File 对象的方法来访问

访问文件名相关的方法:

  1. String getName(): 返回此 File 对象所表示的文件名或路径名(如果是路径,则返回最后一级子路径名)
  2. String getPath(): 返回此 File 对象所对应的路径名
  3. File getAbsolutePath(): 返回此 File 对象所对应的绝对路径名
  4. String getParent(): 返回此 File 对象所对应目录(最后一级子目录)的父目录名
  5. boolean renameTo(File newName): 重命名此 File 对象所对应的文件或目录, 如果重命名成功, 返回true, 否则返回 false

文件检测相关的方法:

  1. boolean exists(): 判断 File 对象所对应的文件或目录是否存在
  2. boolean canWrite():判断 File 对象所对应的文件和目录是否可写
  3. boolean canRead(): 判断 File 对象所对应的文件和目录是否可读
  4. boolean isFile(): 判断 File 对象所对应的是否是文件, 而不是目录
  5. boolean isDirectory(): 判断 File 对象所对应的是否是目录, 而不是文件
  6. boolean isAbsolute(): 判断 File 对象所对应的文件或目录是否是绝对路径
  7. boolean isFile() : 判断是否是一个文件
  8. boolean isHidden() : 是否是隐藏文件

获取常规文件信息:

  1. long lastModified(): 返回文件最后修改时间
  2. long length(): 返回文件内容的长度

文件操作相关的方法:

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

目录操作相关的方法:

  1. boolean mkdir(): 试图创建一个 File 对象所对应的目录, 如果创建成功, 则返回 true, 否则返回false; 调用该方法时 File 对象必须对应一个路径, 而不是一个文件
  2. String[] list(): 列出File对象所有子文件和路径名, 返回 String[] 数据
  3. File[] listFiles(): 列出File对象的所有子文件和路径, 返回File数组
  4. static File[] listRoots(): 列出系统所有的根路径;

示例代码

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


1.2 文件过滤器

  1. 在 File 类的 list() 方法中可以接收一个 FilenameFilter 参数, 通过该参数可以列出只符合条件的文件
  2. FilenameFilter 接口里包含了一个 accept(File dir, String name) 方法, 该方法将依次对指定 File 的所有子目录或者文件进行迭代, 如果该方法返回true, 则 list() 方法会列出该子目录或者文件

示例代码

    import java.io.File;
    //
    public class FilenameFilterTest {
        public static void main(String[] args) {
            File file = new File("D:\\File\\Java\\ShoppingMall\\mall\\src\\jeffmall");
    //        使用Lambda表达式(目标类型为 FilenameFilter) 实现文件过滤器
    //        如果文件名以 .java 结尾, 或者文件对应一个路径, 则返回 true
            String[] nameList = file.list((dir, name) -> name.endsWith(".java")
                || new File(name).isDirectory());
            for (String name : nameList) {
                System.out.print(name + ", ");
            }
        }
    }

2. 理解Java的 IO 流

  1. 在Java中把不同的输入/输出源(键盘, 文件, 网络连接等) 抽象表述为 "流"(stream), 通过流的方式允许Java程序使用相同的方式来访问不同的输入/输出源
  2. stream 是从起源(source)到接收(sink)的有序数据
  3. 输入和输出的对象指的是程序, 输入指的是从程序外部读取数据, 输出指的是向程序外部输出数据

2.1 流的分类

输入流和输出流(按照流的流向来分):

  1. 输入流: 只能从中读取数据, 而不能向其写入数据; 输入流主要由InputStream 和 Reader 作为基类
  2. 输出流: 只能向其写入数据, 而不能从中读取数据; 输出流主要由 OutputStream 和 Writer 作为基类

字节流和字符流:

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

节点流和处理流(按流的角色):

  1. 节点流: 可以从/向一个特定的 IO 设备(如网络, 磁盘等) 读/写数据的流; 节点流也被称之为低级流(Low Level Stream)当使用节点流进行输入/输出时, 程序直接连接到实际的数据源, 和实际的输入/输出节点连接
  2. 处理流: 则用于对一个已存在的流进行连接或封装, 通过封装后的流来实现数据读/写功能, 处理流也被称为高级流当使用处理流进行输入/输出时, 程序并不是直接连接到实际的数据源, 没有和实际的输入/输出节点连接; 实际上Java使用处理流来包装节点流是一种典型的装饰器设计模式, 通过使用处理流来包装不同的节点流, 既可以消除不同的节点流的实现差异, 也可以提供更方便的方法来完成输入/输出功能

2.2 流的概念模型

  1. 流: Java把所有设备里的有序数据抽象成流模型
  2. Java IO 流 4个抽象的基类:
    • InputStream/Reader : 所有输入流的基类, 前者是字节输入流, 后者在字符输入流
    • OutputStream/Writer : 所有输出流的基类, 前者是字节输入流, 后者在字符输入流

3. 字节流和字符流

3.1 InputStream 和 Reader

  1. InputStream/Reader 是所有输入流的抽象基类, 本身不能创建实例来执行输入; 但分别有一个用于读取文件的输入流:FileInputStream 和FileReader, 都是节点流--会直接和指定的文件关联

  2. 输入流的结束标识是: read(char[] cbuf) read(byte[] b) 方法返回 -1

  3. InputStream 三个常用方法:

    • int read(): 从输入流中读取单个字节, 返回所读取的字节数据(字节数据可直接转换为 int 类型)
    • int read(byte[] b): 从输入流中最多读取b.length个字节的数据,并将其存储在字节数组b中, 返回实际读取的字节数
    • int read(byte[] b, int off, int len): 从输入流中最多读取len个字节的数据, 并将其存储在数组b中, 放入数组b中时, 并不是从数组起点开始, 而是从off的位置开始, 返回实际读取的字节数
  4. Reader 中的三个常用方法:

    • int read(): 从输入流中读取单个字符, 返回所读取的字符数据(字符数据可直接转换为int类型)
    • int read(char[] cbuf): 从输入流中最多读取 cbuf.length 个字符的数据, 并将其存储在字符数组 cbuf 中, 返回实际读取的字符数
    • int read(char[] cbuf, int off, int len): 从输入流中最多读取len个字符的数据, 并将其存储在字符数组cbuf中,放入数组cbuf中时, 并不是从数组的起点开始, 而是从 off 的位置开始, 返回实际读取的字符数
  5. InputStream 和 Reader 移动指针的方法:

    • void mark(int readAheadLimit): 在记录指针当前位置记录一个标记(mark)
    • boolean markSupported(): 判断此输入流是否支持mark()操作, 即是否支持记录标记
    • void reset(): 将此流的记录指针重新定位到上一次记录标记(mark)的位置
    • long skip(long n): 记录指针向前移动n个字节/字符

代码示例

        import java.io.FileInputStream;
        import java.io.IOException;
        //
        public class FileInputStreamTest {
            public static void main(String[] args) throws IOException {
        //        创建字节输入流
                FileInputStream fis = new FileInputStream(
                        "D:\\File\\Java\\ShoppingMall\\mall\\src\\jeffmall\\Mall.java");
        //        创建一个长度为1024的数组("竹筒")
                byte[] bbuf = new byte[1024];
        //        用于保存实际读取的字节数
                int hasRead = 0;
        //        使用循环来重复"取水"过程
                while ((hasRead = fis.read(bbuf)) > 0) {
                    System.out.println(new String(bbuf, 0, hasRead));
                }
                fis.close();
            }
        }

3.2 OutputStream 和 Writer

  1. OutputStream 和 Writer都提供的三个常用方法:

    • void write(int c): 将指定的字节/字符输出到输出流中, 其中c既可以代表字节, 也可以代表字符
    • void write(byte[]/char[] buf): 将字节数组/字符数组中的数据输出到指定输出流中
    • void write(byte[]/char[] buf, int off, int len): 将字节数组/字符数组中从off位置开始, 长度为len的字符输出到指定输出流中
  2. Writer 特有的方法:

    • void write(String str): 将 str 字符串里包含的字符输出到指定输出流中
    • void write(String str, int off, int len): 将 str 字符串里从 off 位置开始, 长度为 len 的字符输出到指定输出流中

代码示例

    import java.io.FileInputStream;
    import java.io.FileOutputStream;
    import java.io.IOException;
    //
    public class FileOutputStreamTest {
        public static void main(String[] args) {
            try (
                // 创建字节输入流
                FileInputStream fis = new FileInputStream(
                "D:\\File\\Java\\ShoppingMall\\mall\\src\\jeffmall\\Mall.java");
                FileOutputStream fos = new FileOutputStream(
                "D:\\File\\Java\\ShoppingMall\\mall\\src\\jeffmall\\newMall.txt")){
                byte[] bbuf  = new byte[32];
                int hasRead = 0;
                // 循环从输入流取出数据
                while ((hasRead = fis.read(bbuf)) > 0) {
                    // 每读取一次, 即写入文件输出流, 读了多少, 就写入多少
                    fos.write(bbuf, 0, hasRead);
                }
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }

代码示例

    import java.io.FileWriter;
    import java.io.IOException;
    //
    public class FileWriteTest {
        public static void main(String[] args) {
            try(
                FileWriter fw = new FileWriter("pome.txt")){
                // "\r\n" 是Windows平台的换行符
                fw.write("\t\t锦瑟 - 李商隐\r\n");
                fw.write("锦瑟无端五十弦, 一弦一柱思华年. \r\n");
                fw.write("庄生晓梦迷蝴蝶, 望帝春心托杜鹃. \r\n");
                fw.write("沧海月明珠有泪, 蓝田日暖玉生烟. \r\n");
                fw.write("此情可待成追忆, 只是当时已惘然. \r\n");
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }

4. 输入/输出流体系

4.1 处理流的用法

  1. 处理流的优势: 它可以隐藏底层设备节点流的差异, 并对外提供更加方便的输入/输出方法
  2. 处理流的思路: 使用处理流来包装节点流, 程序通过处理流来执行输入/输出功能, 让节点流与底层的I/O设备,文件交互
  3. 处理流的构造器参数不是一个物理节点, 而是已经存在的流, 也就是装饰流, 它需要直接以物理IO节点作为构造器的参数
  4. 在使用处理流包装了底层节点流之后, 关闭输入/输出流资源时, 只要关闭最外层的处理流即可

常用的输入底层流:

  • ByteArrayInputStream - 输入字节数组
  • StringBufferInputStream - 输入字符串
  • FileInputStream - 从文件输入,即打开一个文件
  • PipedInputStream - 管道输入流,构造器接受一个管道输出流
  • FileReader - 读取文件
  • StringReader - 读取字符串
  • CharArrayReader - 读取字符数组
  • PipedReader - 读取管道数据

常用的输入处理流:

  • DataInputStream: 从流中读取基本类型数据
  • BufferedInputStream: 采用缓冲区方式读写
  • LineNumberInputStream: 来跟踪输入流中的行号
  • BufferedReader - 通用的带缓冲读取字符
  • LineNumberReader - 带有行号API的读取

常用的输出底层流:

  • ByteArrayOutputStream: 字节数组输出,实际上是在内存中创建一个缓冲区输出
  • FileOutputStream: 输出到文件
  • PipedOutputStream: 管道输出流,构造器接受一个管道输入流
  • FileWriter - 写文件
  • StringWriter - 写字符串
  • CharArrayWriter - 写字符数组
  • PipedWriter - 向管道中写入字符

常用的输出处理流:

  • DataOutputStream - 向流中写入基本类型数据
  • PrintStream - 产生格式化输出,有print()和println()两个方法
  • BufferedOutputStream - 带缓冲区的写操作,带有flush()方法用于清空缓冲区并实际执行写操作
  • PrintWriter - 格式化输出,带有print()和println()方法

代码示例

    import java.io.FileOutputStream;
    import java.io.IOException;
    import java.io.PrintStream;
    //
    public class PrintStreamTest {
        public static void main(String[] args) {
            try (
            // PrintStream 是一个处理流, 它接收一个节点流作为构造器参数
                PrintStream ps = new PrintStream(new FileOutputStream("poem.txt"))) {
                // 直接使用PrintStream执行输出
                ps.println("普通字符串");
                ps.println(new PrintStreamTest());
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
    import java.io.*;
    // 
    // 从ASCII编码的文件 test.txt 中读出所有字符
    DataInputStream in = new DataInputStream(new FileInputStream("test.txt"));
    //
    // 从一个二进制文件 data.out 中采用缓冲的方式读取基本类型数据,已经知道这个文件按照顺序存放了一个int类型,一个long类型,四个ASCII字符
    DataInputStream in = new DataInputStream(new BufferedInputStream(new FileInputStream("data.out")));
    // 
    // 想从一个文本文件 story.txt 中按行读取字符
    BufferedReader in = new BufferedReader(new FileReader("story.txt"));
    // 
    // 向一个文件 test.data 中带缓冲写字符串,写完之后自动换行
    PrintWriter out = new PrintWriter(new BufferedWriter(new FileWriter("test.data")));

4.2 输入/输出流体系

  1. 如果输入/输出的内容是文本内容, 则应该考虑使用字符流; 如果输入/输出的内容是二进制内容, 则应该考虑字节流
  2. 以数组为物理节点的节点流, 字节流以字节数组为节点, 字符流以字符数组为节点, 这种以数组为物理节点的节点流处理在创建节点流对象时需要传入一个字节数组或字符数组之外, 用法上与文件节点流完全相似
  3. 访问管道的流: PipedInputStream, PipedOutputStream, PipedReader, PipedWriter这些流都是用于实现进程之间通信功能的, 分别代表字节输入流, 字节输出流, 字符输入流, 字符输出流
  4. 缓冲流: 缓冲流增加了缓冲的功能, 增加缓冲的功能可以提高输入, 输出的效率, 增加缓冲功能后需要使用flush()才可以将缓冲区的内容写入实际的物理节点
  5. 对象流主要用于实现对的序列化

代码示例

    import java.io.IOException;
    import java.io.StringReader;
    import java.io.StringWriter;
    //
    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(StringReader sr = new StringReader(src)) {
                // 循环读取的方式读取字符串
                while ((hasRead = sr.read(buffer)) != -1) {
                    System.out.println(new String(buffer, 0, hasRead));
                }
            }catch (IOException e) {
                e.printStackTrace();
            }
            try (
                // 创建StringWrite 时, 实际上以一个 StringBuffer 作为输出节点
                // 下面指定的20就是StringBuffer的初始长度
                StringWriter sw = new StringWriter()) {
                // 调用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 e) {
                e.printStackTrace();
            }
        }
    }

4.3 转换流

  1. 转换流用于实现将字节流转换为字符流

    • InputStreamReader: 将字节输入流转换成字符输入流
    • OutputStreamWriter: 将字节输出流转换成字符输出流
  2. 转换流的应用

    • Java使用System.in代表标准输入, 即键盘输入, 但这个标准输入流是 InputStream 类的实例, 使用不方便, 而且键盘输入的都是文本内容, 所以可以使用 InputStreamReader 将其转换成字符输入流
    • 普通的Reader读取内容时, 可以将普通的Reader再次包装成 BufferedReader, 再利用BufferedReader的 readLine()方法可以一次读取一行内容

示例代码

    import java.io.BufferedReader;
    import java.io.IOException;
    import java.io.InputStreamReader;
    //
    public class KeyinTest {
        public static void main(String[] args) {
            try (
                // 将 System.in对象转换为 InputStreamReader 对象
                InputStreamReader reader = new InputStreamReader(System.in);
                // 将普通的 Reader 包装成 BufferedReader
                BufferedReader 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 e) {
                e.printStackTrace();
            }
        }
    }

4.4 推回输入流

  1. PushbackInputStream 和 PushbackReader; 这两个推回输入流都带有一个推回缓冲区, 当程序调用这两个推回输入流的 unread() 方法时, 系统会把指定数组的内容推回到该缓冲区里, 而推回输入流每次调用read()方法时总是先从推回缓冲区读取, 只有完全读取了推回缓冲区的内容后, 但还没有装满read()所需的数组时才会从原输入流中读取

  2. 当程序创建一个 PushbackInputStream 或 PushbackReader 时需要指定推回缓冲区的大小, 默认的推回缓冲区的长度为1, 如果程序中推回到推回缓冲区的内容长度超出了推回缓冲区的大小, 将引发 Pushback buffer Overflow的 IOException 异常

  3. 都提供如下三个方法:

    • void unread(byte[]/char[] buf): 将一个字节/字符数组内容推回到推回缓冲区里, 从而允许重复读取刚刚读取的内容
    • void unread(byte[]/char[] b, int off, int len): 将一个字节/字符数组里从off开始, 长度为len字节/字符的内容推回到推回缓冲区里, 从而允许重复读取刚刚读取的内容
    • void unread(int b): 将一个字节/字符推回到推回缓冲区里, 从而允许允许重复读取刚刚读取的内容

示例代码

    import java.io.FileReader;
    import java.io.IOException;
    import java.io.PushbackReader;
    //
    public class PushbackTest {
        public static void main(String[] args) {
            // 再 try 括号中打开 PushbackReader 可以自动关闭
            try (
                // 创建一个 PushbackReader 对象, 指定推回缓冲区的长度为 06
                PushbackReader pr = new PushbackReader(new FileReader(
                        "D:\\File\\Java\\JavaBase\\src\\xyz\\xmcs\\CrazyJava\\IO\\PushbackTest.java")
                        , 64)) {
                char[] buf = new char[32];
                // 用于保存上次读取的字符串内容
                String lastContnt = "";
                int hasRead = 0;
                // 循环读取文件内容
                while ((hasRead = pr.read(buf)) > 0) {
                    // 将读取的内容转换为字符串
                    String content = new String(buf, 0, hasRead);
                    int targetIndex = 0;
                    // 将上次读取的字符串和本次读取的字符串拼接起来
                    // 查看是否包含目标字符串, 如果包含目标字符串
                    if ((targetIndex =(lastContnt + content).indexOf("new PushbackReader")) > 0) {
                        // 将本次内容和上次内容一起推回缓冲区
                        pr.unread((lastContnt + content).toCharArray());
                        // 重新定义一个长度为targetIndex 的 char数组
                        if (targetIndex > 32) {
                            buf = new char[targetIndex];
                        }
                        // 再次读取指定长度的内容(就是目标字符串之前的内容)
                        pr.read(buf, 0, targetIndex);
                        // 打印读取的内容
                        System.out.println(new String(buf, 0, targetIndex));
                        System.exit(0);
                    } else {
                        // 打印上次读取的内容
                        System.out.println(lastContnt);
                        lastContnt = content;
                    }
                }
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }

5 重定向标准输入/输出

  1. Java的标准输入/输出分别通过System.in和System.out来代表, 默认情况下他们分别代表键盘和显示器, 当程序通过 System.in 来获取输入时, 实际上是从键盘读取输入, 当程序视图通过System.out 执行输出时, 程序总是输出到屏幕; 重定向就是改变默认的输入输出情况, 例如输入重定向到从文本读取, 输出重定向为write到文本...

  2. System 类里面提供下面三个重定向的标准输入/输出的方法:

    • static void setErr(PrintStream err): 重定向 "标准" 错误输出流
    • static void setIn(InputStream in): 重定向 "标准" 输入流
    • static void setOut(PrintStream out): 重定向 "标准" 输出流

代码示例

    // 将System.out的输出重定向到文件输出, 而不是在屏幕上输出
    import java.io.FileOutputStream;
    import java.io.IOException;
    import java.io.PrintStream;
    //
    public class RedirectOut {
        public static void main(String[] args) {
            try (
                // 一次性创建 PrintStream 输出流
                PrintStream ps = new PrintStream(new FileOutputStream("out.txt"))
            ) {
                // 将标准输出重定向到ps输出流
                System.setOut(ps);
                // 向标准输出输出一个字符串
                System.out.println("普通字符串");
                // 向标准输出输出一个对象
                System.out.println(new RedirectOut());
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }

代码示例

    // 将System.in重定向到指定文件, 而不是键盘输入
    import java.io.FileInputStream;
    import java.io.IOException;
    import java.util.Scanner;
    //
    public class RedirectIn {
        public static void main(String[] args) {
            try(
                // 获取输入流对象 fis
                FileInputStream fis = new FileInputStream(
                    "D:\\File\\Java\\JavaBase\\src\\xyz\\xmcs\\CrazyJava\\IO\\RedirectIn.java")) {
                // 将标准输入重定向到 fis 输入流, 即获取的输入项是 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 e) {
                e.printStackTrace();
            }
        }
    }

6. Java虚拟机读写其他进行的数据

  1. 使用 Runtime 对象的 exec() 方法可以运行平台上的其他程序, 该方法产生一个 Process 对象, Process 对象代表由该Java程序启动的子进程

  2. Process 用于让程序和其子进程进行通信的方法:

    • InputStream getErrStream(): 获取子进程的错误流
    • InputStream getInputStream(): 获取子进程的输入流
    • OutputStream getOutputStream(): 获取子进程的输出流
  3. 要注意这里的输入输出流, 如果要让子进程读取程序中的数据, 对于Java程序来说这是输出流

代码示例

    import java.io.BufferedReader;
    import java.io.IOException;
    import java.io.InputStreamReader;
    public class ReadFromProcess {
        public static void main(String[] args) throws IOException {
            // 运行 javac 命令, 返回运行该命令的子进程
            Process p = Runtime.getRuntime().exec("javac");
            try (
               // 以p进程的错误流创建 BufferedReader 对象
               // 这个错误流对本程序是输入流, 对p则是输出流
               BufferedReader br = new BufferedReader(new
                       InputStreamReader(p.getErrorStream()))) {
                String buff = null;
                // 采取循环方式来读取p进程的错误输出
                while ((buff = br.readLine()) != null) {
                    System.out.println(buff);
                }
            }
        }
    }

7. RandomAccessFile (任意访问文件)

  1. RandomAccessFile 它既可以读取文件内容, 也可以向文件输出数据; 它支持 "随机访问" 的方式, 程序可以直接跳转到文件的任意地方来读写数据

  2. RandomAccessFile 允许自由定位文件记录指针, RandomAccessFile 可以不从开始的地方输出, 因此RandomAccessFile可以向已存在的文件后追加内容; 如果程序需要向已存在的文件后追加内容, 则应该使用 RandomAccessFile

  3. RandomAccessFile 对象包含了一个记录指针, 用于标识当前读写处的位置, 当程序新创建一个 RandomAccessFile对象时, 该对象的文件记录指针位于文件头(也就是0处), 当读/写了n个字节后, 文件记录指针将会向后移动n个字节

  4. RandomAccessFile 操作文件记录指针的方法:

    • long getFilePointer(): 返回文件记录指针的当前位置
    • void seek(long pos): 将文件记录指针定位到pos位置
  5. RandomAccessFile 既可以读取文件, 也可以写, 所以它既包含了完全类似于 InputStream 的三个 read() 方法,其用法和 InputStream 的三个 read() 方法完全一样; 也包含了完全类似于 OutputStream 的三个 write() 方法,其用法和 OutputStream 的三个 write() 方法完全一样; 另外 RandomAccessFile 还包含了一些列 readXxx()和 writeXxx() 方法来完成输入, 输出

  6. RandomAccessFile 类有两个构造器, 一个使用 String 参数来指定文件名, 另一个使用 File 参数来指定文件本身除此之外, 创建 RandomAccessFile 对象时还需要指定一个 mode 参数, 该参数指定 RandomAccessFile 的访问模式

    • "r": 只读方式打开指定文件, 如果试图对该 RandomAccessFile 执行写入方法, 将抛出 IOException 异常
    • "rw": 以读, 写方式打开指定文件, 如果文件尚不存在, 则尝试创建文件
    • "rws": 以读, 写方式打开指定文件, 相对于 "rw" 模式, 还要求对文件的内容或元数据的每个更新都同步写入到底层存储设备
    • "rwd": 以读, 写方式打开指定文件, 相对于 "rw" 模式, 还要求对文件内容的每个更新都同步写入到底层存储设备

示例代码

    // 使用 RandomAccessFile 来访问文件的指定的中间部分数据
    import java.io.RandomAccessFile;
    //
    public class RandomAccessFileTest {
        public static void main(String[] args) {
            try (
                RandomAccessFile raf = new RandomAccessFile(
                    "D:\\File\\Java\\JavaBase\\src\\xyz\\xmcs\\CrazyJava\\IO\\RandomAccessFileTest.java", "r")){
                // 获取 RandomAccessFile 对象文件指针的位置, 初始位置为0
                System.out.println("RandomAccessFileTest的文件指针的初始位置: "
                    + raf.getFilePointer());
                // 移动 raf 的文件记录指针的位置
                raf.seek(300);
                byte[] bbuf = new byte[1024];
                // 用于保存实际读取的字节数
                int hasRead = 0;
                // 使用循环来重复 "取水" 过程
                while ((hasRead = raf.read(bbuf)) > 0) {
                    // 取出 "竹筒" 中的水滴(字节), 将字节数组转换成字符串输入
                    System.out.println(new String(bbuf, 0, hasRead));
                }
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }

示例代码

    // 在文件的末尾追加内容
    import java.io.IOException;
    import java.io.RandomAccessFile;
    //
    public class AppendContent {
        public static void main(String[] args) {
            try (
                // 以读写的方式打开RandomAccessFile对象
                RandomAccessFile raf = new RandomAccessFile(
                "D:\\File\\Java\\JavaBase\\src\\xyz\\xmcs\\Main.java", "rw")) {
                // 将记录指针移动到 out.txt 文件的最后
                raf.seek(raf.length());
                raf.write("// 这是使用RandomFileAccess来追加的内容! \r\n".getBytes());
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }

示例代码

    // 在文件的指定位置插入文本
    // 实现方法是: 找到要插入的文件位置pos, 创建临时文件, 读取pos到文件尾的数据, 输出到临时文件中
    //            接着移动文件指针到pos位置, 插入指定的数据, 在读取临时文件的内容, 写入到文件中
    import java.io.*;
    //
    public class InsertContent {
        /**
         * @param fileName  需要追加的文件的名字, 绝对路径
         * @param pos   需要追加的位置
         * @param insertContent  需要追加的内容
         * @throws IOException  可能存在文件打开异常
         */
        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)) != -1) {
                    // 将读取的数据写入临时文件
                    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("D:\\File\\Java\\JavaBase\\src\\xyz\\xmcs\\Main.java",
                19, "// --- 这是插入的内容 --- // \r\n");
        }
    }

8. 对象序列化

8.1 序列化的含义和意义

  1. 对象序列化的目标就是将对象保存到磁盘中, 或允许在网络中直接传输对象; 对象序列化机制允许把内存中的 java 对象转换成平台无关的二进制流, 从而允许把这种二进制流持久地保存在磁盘上, 通过网络将这种二进制流传输到另一个网络节点; 其他程序一但获得了这种二进制流(无论网络还是磁盘), 都可以将这种二进制流恢复成原来的Java对象
  2. 对象的序列化(Serialize)指将一个 Java 对象写入 IO 流中; 对象的反序列化(Deserialize) 则指的是从 IO 流中恢复该 Java 对象
  3. 如果要让某个对象支持序列化机制, 则必须让它的类是可序列化的(serializable); 这个类必须实现 Serializable和 Externalizable 两个接口
    • Java的很多类都实现了 Serializable 接口, 该接口是一个标记接口, 无须是实现给接口的任何方法, 只是表明该类的实例是可序列化的
    • 所有的 网络传输对象以及需要保存到磁盘的对象都应该是可序列化的, 所以:程序创建的每个 JavaBean 类都实现Serializable

8.2 使用对象流实现序列化

序列化的主要步骤:

  1. 创建一个 ObjectOutputStream 输出流, 必须建立在其他节点流的基础上ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("object.txt"))
  2. 调用 ObjectOutputStream 对象的 writeObject() 方法输出可序列化对象 oos.writeObject(per);

代码示例

    // 先创建一个普通的person类
    package xyz.xmcs.CrazyJava.IO;
    //
    public class Person implements java.io.Serializable {
        private String name;
        private int age;
        public String getName() {
            return name;
        }
        public void setName(String name) {
            this.name = name;
        }
        public int getAge() {
            return age;
        }
        public void setAge(int age) {
            this.age = age;
        }
        public Person (String name, int age) {
            System.out.println("有参数的构造器!");
            this.name = name;
            this.age = age;
        }
    }
    // 在创建一个 WriteObject 程序, 调用 ObjectOutputStream 将一个 Person 对象写入磁盘文件
    import java.io.FileOutputStream;
    import java.io.IOException;
    import java.io.ObjectOutputStream;
    //
    public class WriteObject {
        public static void main(String[] args) {
            try (
                // 创建一个 ObjectOutputStream 输出流
                ObjectOutputStream oos = new ObjectOutputStream(
                        new FileOutputStream("object.txt"))) {
                Person per = new Person("孙悟空", 500);
                // 将 per 对象写入输出流
                oos.writeObject(per);
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }

反序列化的主要步骤:

  1. 先创建一个 ObjectInputStream 输入流, 这个输入流是一个处理流, 需要建立在其他节点流的基础上ObjectInputStream ois = new ObjectInputStream(new FileInputStream("object.txt"))
  2. 调用 ObjectInputStream 对象的 readObject() 方法读取流中的对象, 该方法返回一个 Object 类型的Java对象, 如果程序知道该Java对象的类型, 则可以将该对象强制类型转换成其真实的类型
  3. 注意点:
    • 反序列化读取的仅仅是Java对象的数据, 而不是Java类, 因此反序列化恢复Java对象时, 必须提供该Java对象所属类的 class 文件, 否则引发 ClassNotFoundException 异常
    • 反序列化机制无须通过构造器来初始化Java对象

代码示例

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

8.3 对象引用的序列化

  1. 如果某个类的成员变量是引用类型, 那么这个引用类型必须是可序列化的, 否则拥有该类型成员变量的类也是不可序列化的
  2. 如果 Teacher 类持有一个 Person 类的引用, 只有 Person 类是可序列化的, Teacher 类才是可序列化的; 如果Person 类不可序列化, 无论 Teacher 类是否实现 Serializable, Externalizable 接口, 则 Teacher 类都是不可序列化的
  3. Java序列化机制采用的序列化算法:ila
    • 所有保存到磁盘中的对象都有一个序列化编号
    • 当程序试图序列化一个对象时, 程序先检查该对象是否已经被序列化过, 只有该对象从未(在本次虚拟机中)被序列化过, 系统才会将该对象转换成字节序列并输出
    • 如果某个对象已经被序列化过, 程序将只是直接输出一个序列化编号, 而不是再次重新序列化该对象
  4. 所以Java序列化的底层机制是, 当多次调用 writeObject()方法输出同一个对象时, 只有第一次调用 writeObject()方法时才会将该对象转换成字节序列并写入到 ObjectOutputStream; 当程序再次调用 writeObject()方法时, 程序只是输出前面的序列化编号, 即使后面该对象的实例变量值已被改变, 改变的实例变量指也不会被输出

引用类型序列化:

代码示例

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

引用类型反序列化:

代码示例

    import java.io.FileInputStream;
    import java.io.ObjectInputStream;
    //
    public class ReadTeacher {
        public static void main(String[] args) {
            try (
                // 创建一个 ObjectInputStream 输入流
                ObjectInputStream ois = new ObjectInputStream(
                    new FileInputStream("teacher.txt"))) {
                // 依次读取 ObjectInputStream 输入流的4个对象
                Teacher t1 = (Teacher)ois.readObject();
                Teacher t2 = (Teacher)ois.readObject();
                Person p = (Person)ois.readObject();
                Teacher t3 = (Teacher)ois.readObject();
                System.out.println("t1 的student引用和p是否相同: " + (t1.getStudent() == p));
                System.out.println("t2 的student引用和p是否相同: " + (t2.getStudent() == p));
                System.out.println("t3 的student引用和t2是否相同: " + (t2 == t3));
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }

8.4 自定义序列化

  1. 递归序列化: 当某个对象进行序列化时, 系统会自动把该对象的所有实例变量依次进行序列化, 如果某个实例变量引用到另一个对象, 则被引用的对象也会被序列化, 如果被引用的对象的实例变量也引用了其他对象, 则被引用的其他对象也会被序列化
  2. 可以通过在实例变量前面使用 transient 关键字修饰, 指定Java序列化时无须理会该实例变量; transient 只能用于修饰实例变量, 不可修饰Java程序中的其他成分
  3. 使用 transient 关键字修饰实例变量虽然简单, 方便, 但被 transient 修饰的实例变量将被完全隔离在序列化机制之外, 这样导致在反序列化恢复Java对象时无法取得该实例变量值
  4. Java还提供了一种自定义序列化机制, 通过这种自定义序列化机制可以让程序控制如何序列化各实例变量, 甚至完全不序列化某些实例变量

9. NIO

9.1 Java 新 IO 概述

  1. 新IO和传统的IO具有相同的目的, 都是用于进行输入/输出, 但新IO使用了不同的方式来处理输入/输出, 新IO采用了内存映射文件的方式来处理输入/输出, 新IO将文件或文件的一段区域映射到内存中, 这样就可以像访问内存一样来访问文件了, 通过这种方式来进行输入/输出比传统的输入/输出要快

  2. Channel(通道)和Buffer(缓冲)是新IO中的两个核心对象, Channel 是对传统的输入/输出系统的模拟, 在新IO系统中所有的数据都需要通过通道传输; Channel 与传统的 InputStream, OutputStream最大的区别在于它提供了一个map() 方法, 通过该 map() 方法可以直接将 "一块数据" 映射到内存中; 传统的输入/输出是面向流的处理, 新IO则是面向块的处理

  3. Buffer 可以被理解成一个容器, 他的本质是一个数组, 发送到 Channel 中的所有对象都必须首先放到Buffer中, 而从Channel 中读取的数据也必须先放到Buffer中; 此处的Buffer类似于前面介绍的 "竹筒", 但该Buffer既可以像"竹筒" 那样一次次去Channel中取水, 也允许使用Channel直接将文件的某块数据映射成Buffer

  4. Java中新的IO相关包:

    • java.nio 包: 主要包含各种与Buffer(缓冲)相关的类
    • java.nio.channels 包: 主要包含与Channel(通道)和Selector相关的类
    • java.nio.charset 包: 主要包含与字符集相关的类
    • java.nio.channels.spi 包: 主要包含与 Channel 相关的服务提供者编程jiekou
    • java.nio.charset.spi 包: 包含与字符集相关的服务提供者编程接口

9.2 使用 Buffer

  1. Buffer的内部结构就是一个数组, 它可以保存多个类型相同的数据; Buffer是一个抽象类, 最常用的子类是 ByteBuffer,它可以在底层字节数组上进行 get/set 操作; 除此之外还有其他基本数据类型(boolean除外): CharBuffer, ShortBuffer,IntBuffer, LongBuffer, FloatBuffer, DoubleBuffer

  2. 上述这些Buffer类都没有构造器, 只能通过如下的方法来获取Buffer对象:

    • static XxxBuffer allocate(int capacity): 创建一个容量为 capacity 的 XxxBuffer 对象; 通过allocate()方法创建的Buffer对象是普通Buffer, ByteBuffer还提供了一个 allocateDirect()方法来创建直接Buffer. 直接Buffer的创建成本比普通Buffer的创建成本高, 但直接Buffer的读取效率更高
  3. 在Buffer中三个重要概念: 容量(capacity), 界限(limit), 位置(position)

    • 容量(capacity): 缓冲区的容量(capacity)表示该Buffer的最大数据容量, 即最多可以存储多少数据, 缓冲区的容量不可为负值, 创建后不能改变
    • 界限(limit): 第一个不应该被读出或者写入的缓冲区位置索引, 也就是说, 位于limit后的数据既不可读, 也不可写
    • 位置(position): 用于指明下一个可以被读出的或者写入的缓冲区位置索引(类似于IO流中的记录指针), 当使用Buffer从 Channel 中读取数据时, position的值恰好等于已经读到了多少数据, 当刚刚新建了一个Buffer对象时, 其position为0; 如果从 Channel 中读取了2个数据到Buffer时, 则 position为2, 指向Buffer中的第2个(第一个位置索引为0)位置
  4. Buffer的主要功能就是装入数据, 然后输出数据, 开始时Buffer的position为0, limit为capacity, 程序可通过put()方法向Buffer中放入一些数据(或者从Channel中获取一些数据), 每放入一些数据, Buffer的position相应的向后移动一些位置当Buffer装入数据结束后, 调用Buffer的flip()方法, 该方法将limit设置为position所在位置, 并将position设为0, 这就使得Buffer的读写指针又移到了开始位置, 也就是说, Buffer调用flip()方法之后, Buffer为输出数据做好了准备; 当Buffer输出数据结束后, Buffer调用clear()方法, clear()方法不是清空Buffer的数据, 它仅仅将position置为0, 将limit置为capacity, 这样为再次向Buffer中装入数据做好准备

  5. mark: 可选的标记, Buffer允许直接将position定位到该mark处; 且: 0 <= mark <= position <= limit <= capacity

Buffer中的一些常用的方法:

  • int capacity(): 返回Buffer的capacity大小
  • boolean hasRemaining(): 判断当前位置(position)和界限(limit)之间是否还有元素可供处理
  • int limit(): 返回Buffer的界限(limit)的位置
  • Buffer limit(int newLt): 重新设置界限(limit)的值, 并返回一个具有新的limit的缓冲区对象
  • Buffer mark(): 设置Buffer的mark位置, 它只能在0和位置(position)之间做mark
  • int position(): 返回Buffer中的position值
  • Buffer position(int newPos): 设置Buffer的position, 并返回position被修改后的Buffer对象
  • int remaining(): 返回当前位置和界限之间的元素个数
  • Buffer reset(): 将位置(position)转到mark所在的位置
  • Buffer rewind(): 将位置(position)设置为0, 取消设置的mark
  • flip(): (将position设为0)为从Buffer中取出数据做好准备
  • clear(): (将position设为0)为再次向Buffer中装入数据做好准备
  • put(): 向Buffer中放入数据
  • get(): 从Buffer中取出数据

相对和绝对的通过put和get来访问Buffer中的数据:

  • 相对(Relative): 从Buffer的当前position处开始读取或写入数据, 然后将位置(position)的值按处理元素的个数增加
  • 绝对(Absolute): 直接根据索引向Buffer中读取或写入数据, 使用绝对方式访问Buffer里的数据时, 并不会影响位置的值

代码示例

    import java.nio.CharBuffer;
    //
    public class BufferTest {
        public static void main(String[] args) {
            // 通过Buffer的allocate方法创建容量(capacity)为8的Buffer
            CharBuffer buff = CharBuffer.allocate(8);
            System.out.println("capacity = " + buff.capacity());
            System.out.println("limit = " + buff.limit());
            System.out.println("position = " + buff.position());
            // 放入元素
            buff.put('a');
            buff.put('b');
            buff.put('c');
            System.out.println("加入三个元素后, position = " + buff.position());
            // 调用flip()方法
            buff.flip();
            System.out.println("执行flip()方法后, limit = " + buff.limit());
            System.out.println("执行flip()方法后, position = " + buff.position());
            // 取出第一个元素
            System.out.println("取出第一个元素(position = 0): " + buff.get());
            System.out.println("取出第一个元素后, position = " + buff.position());
            // 调用clear()方法
            buff.clear();
            System.out.println("执行clear()后, limit = " + buff.limit());
            System.out.println("执行clear()方法后, position = " + buff.position());
            System.out.println("执行clear()后, 缓冲区内容并没有被清除: "
                    + "第三个元素为: " + buff.get(2));
            System.out.println("执行绝对读取后, position = " + buff.position());
        }
    }

9.3 使用 Channel

Channel 和传统的流对象的两个区别:

  1. Channel可以直接将指定文件的部分或全部直接映射成Buffer
  2. 程序不能直接访问 Channel中的数据, 包括读取, 写入都不行, Channel 只能和 Buffer 交互; 如果要从 Channel 中取得数据, 必须先用 Buffer 从 Channel 中取出一些数据, 然后让程序从Buffer中取出这些数据; 如果要将程序中的数据写入 Channel, 也要先让程序将数据放入 Buffer 中, 程序再将 Buffer 里的数据写入 Channel 中

Channel 接口的实现类:

  • Pipe.SinkChannel, Pipe.SourceChannel: 这两个实现类用于支持线程之间通信的管道 Channel
  • ServerSocketChannel, SocketChannel: 是用于支持TCP网络通信的Channel
  • DatagramChannel: 是用于支持UDP网络通信的 Channel
  • FileChannel: 是用于文件访问的 Channel

创建 Channel:

  1. 所有的 Channel 通过传统的节点 InputStream, OutputStream 的 getChannel() 方法来返回对应的 channel; 不同的节点流获得的 Channel 不一样
    • FileInputStream, FileOutputStream 的 getChannel() 方法返回 FileChannel
    • PipedInputStream, PipedOutputStream 的 getChannel() 方法返回 Piped.SinkChannel, Pipe.SoureChannel
  2. Channel 中最常用的三类方法是 map(), read(), write()
    • map() 用于将 Channel 对应的部分或全部数据映射成ByteBuffer
    • read() 用于从Buffer中读取数据
    • write() 向Buffer中写入数据
  3. map() 方法的方法签名为: MappedByteBuffer map(FileChannel.MapMode mode, long position, long size): 第一个参数执行映射时的模式, 分别有只读, 读写模式; 第二个参数, 第三个参数用于控制将 Channel 的那些数据映射成ByteBuffer
  4. RandomAccessFile 中也包含可一个getChannel()方法, 该方法是只读还是读写模式, 则取决于RandomAccessFile 打开文件的模式

代码示例

    import java.io.File;
    import java.io.FileInputStream;
    import java.io.FileOutputStream;
    import java.io.IOException;
    import java.nio.CharBuffer;
    import java.nio.MappedByteBuffer;
    import java.nio.channels.FileChannel;
    import java.nio.charset.Charset;
    import java.nio.charset.CharsetDecoder;
    //
    public class FileChannelTest {
        /**
         *
         * @param oldFilePath 源文件的绝对路径
         * @param newFilePath 目标文件的绝对路径
         */
        public static void CopyFile(String oldFilePath, String newFilePath) {
            File f = new File(oldFilePath);
            try (
                // 创建 FileInputStream, 以该文件输入流创建 FileChannel
                FileChannel inChannel = new FileInputStream(f).getChannel();
                // 以文件输出流创建 FileChannel, 用以控制输出
                FileChannel outChannel = new FileOutputStream(newFilePath).getChannel()) {
                // FileChannel 里的全部数据映射成 ByteBuffer
                MappedByteBuffer buffer = inChannel.map(FileChannel.MapMode.READ_ONLY, 0, f.length());
                // 使用 "UTF-8" 的字符集来创建解码器
                Charset charset = Charset.forName("UTF-8");
                // 直接将 buffer 里的数据全部输出
                outChannel.write(buffer);
                // 再次调用Buffer的clear()方法, 复原limit, position 的位置
                buffer.clear();
                // 创建解码器(CharsetDecoder)对象
                CharsetDecoder decoder = charset.newDecoder();
                // 使用解码器将 ByteBuffer 转换成 CharBuffer
                CharBuffer charBuffer = decoder.decode(buffer);
                System.out.println(charBuffer);
            } catch(IOException e) {
                e.printStackTrace();
            }
        }
        public static void main(String[] args) {
            String oldFilePath = "D:\\File\\Java\\JavaBase\\src\\xyz\\xmcs\\CrazyJava\\IO\\第十五章 输入_输出.txt";
            String newFilePath = "D:\\File\\Java\\JavaBase\\src\\xyz\\xmcs\\CrazyJava\\IO\\a.txt";
            CopyFile(oldFilePath, newFilePath);
        }
    }

代码示例

    // 使用RandomAccessFile 创建FileChannel 对象
    import java.io.File;
    import java.io.IOException;
    import java.io.RandomAccessFile;
    import java.nio.ByteBuffer;
    import java.nio.channels.FileChannel;
    //
    public class FandomFileChannelTest {
        public static void main(String[] args) throws IOException {
            File f = new File("D:\\File\\Java\\JavaBase\\src\\xyz\\xmcs\\CrazyJava\\IO\\a.txt");
            try (
                // 创建一个RandomAccessFile对象, 模式是 rw
                RandomAccessFile raf = new RandomAccessFile(f, "rw");
                // 获取 RandomAccessFile 对应的 Channel
                FileChannel randomChannel = raf.getChannel()) {
                // 将 Channel 中的所有数据映射成 ByteBuffer
                ByteBuffer buffer = randomChannel.map(FileChannel.MapMode.READ_ONLY, 0, f.length());
                // 把Channel的记录指针移动到最后
                randomChannel.position(f.length());
                // 将buffer中的所有数据输出
                randomChannel.write(buffer);
            }
        }
    }

代码示例

    // Channel 通过 "少拿勤跑" 的方式映射数据
    import java.io.FileInputStream;
    import java.io.IOException;
    import java.nio.ByteBuffer;
    import java.nio.CharBuffer;
    import java.nio.channels.FileChannel;
    import java.nio.charset.Charset;
    import java.nio.charset.CharsetDecoder;
    //
    public class ReadFile {
        public static void main(String[] args) throws IOException {
            try (
                // 创建文件输入流
                FileInputStream fis = new FileInputStream(
                    "D:\\File\\Java\\JavaBase\\src\\xyz\\xmcs\\CrazyJava\\IO\\ReadFile.java");
                // 通过文件输入流创建一个FileChannel对象
                FileChannel fc = fis.getChannel()) {
                // 定义一个ByteBuffer对象, 用于重复取水
                ByteBuffer bbuff = ByteBuffer.allocate(256);
                // 将 FileChannel 中的数据放入 ByteBuffer 中
                while (fc.read(bbuff) != -1) {
                    // 锁定Buffer的空白区
                    bbuff.flip();
                    // 创建 Charset 对象
                    Charset charset = Charset.forName("UTF-8");
                    // 创建解码器 (CharsetDecoder) 对象
                    CharsetDecoder decoder = charset.newDecoder();
                    // 将 ByteBuffer 的内容转码
                    CharBuffer cbuff = decoder.decode(bbuff);
                    System.out.print(cbuff);
                    // 将 Buffer 初始化, 为下一次读取数据做好准备
                    bbuff.clear();
                }
            }
        }
    }

9.4 字符集和 Channel

  1. 计算机所有的文件在底层存储的都是二进制的字节码, 之所以我们能在显示器上看到文件视频等, 正式因为存在Encode(编码), 和 Decode(解码); 编码 Encode 就是将明文的字符序列转换为计算机能够理解的二进制序列;解码 Decode 就是将计算机的二进制序列转换为人能看懂的明文字符串序列
  2. Java默认使用的是 Unicode 字符集, JDK 1.4 提供了 Charset 来处理字节序列和字符序列(字符串)之间的转换关系该类包含了可用于创建编码器和解码器的方法, 还提供了Charset所支持ide字符集的方法, Charset是不可变类
  3. Charset类提供了 availableCharsets() 静态方法来获取当前JDK所支持的所有字符集
  4. 获取了 Charset 对象后, 如果仅仅需要进行简单的编码, 解码操作, 可以无须创建 CharsetEncoder 和 CharsetDecoder对象, 直接调用 Charset 的 encode() 和 decode() 方法进行编解码即可
    • CharBuffer decode(ByteBuffer bb): 将 ByteBuffer 中的字节序列转换成字符序列的便捷方法
    • ByteBuffer encode(CharBuffer cb): 将 CharBuffer 中的字符序列转换成字节序列的便捷方法
    • ByteBuffer encode(String str): 将String中的字符序列转换成字节序列的便捷方法

代码示例

    // 获取Java支持的所有字符集
    import java.nio.charset.Charset;
    import java.util.SortedMap;
    //
    public class CharsetTest {
        public static void main(String[] args) {
            // 获取Ja有字符集
            SortedMap<String, Charset> map = Charset.availableCharsets();
            for (String alias : map.keySet()) {
                // 输出字符集的别名和对应的Charset对象
                System.out.println(alias + " ----> " + map.get(alias));
            }
        }
    }

常用的字符集:

  • GBK: 简体中文字符集
  • BIG5: 繁体中文字符集
  • ISO-8859-1: ISO 拉丁字母表 No.1, 也叫做ISO-LATIN-1
  • UTF-8: 8位UCS转换格式
  • UTF-16BE: 16位UCS转转格式, Big-endian(最低地址存放高位字节)字节顺序
  • UTF-16LE: 16位UCS转转格式, Little-endian(最高地址存放低位字节)字节顺序
  • UTF-16: 16位UCS转换格式, 字节顺序由可选的字节顺序标记来标识

设置字符集的方法:

  1. 调用 Charset 的 forName("字符集别名") 方法来创建对应的 Charset 对象
  2. 通过获得 Charset 对象, 调用该对象的 newDecoder(), newEncoder() 这两个方法分别返回 CharsetDecoder 和CharsetEncoder 对象, 代表该 Charset 的解码器和编码器;
  3. 在通过调用 CharsetDecoder 的 decode() 方法就可以将 ByteBuffer(字节序列)转换成 CharBuffer(字符序列) 或通过调用 CharsetEncoder 的 encode() 方法就可以将 CharBuffer 或 String (字符序列) 转换成 ByteBuffer

代码示例

    // CharBuffer 和 ByteBuffer 互转
    import java.nio.ByteBuffer;
    import java.nio.CharBuffer;
    import java.nio.charset.Charset;
    import java.nio.charset.CharsetDecoder;
    import java.nio.charset.CharsetEncoder;
    //
    public class CharsetTransform {
        public static void main(String[] args) throws Exception {
            // 创建简体中文对应的Charset
            Charset cn = Charset.forName("GBK");
            // 获取 cn 对象对应的编码器和解码器
            CharsetEncoder cnEncoder = cn.newEncoder();
            CharsetDecoder cnDecoder = cn.newDecoder();
            // 创建一个 CharBuffer 对象
            CharBuffer cbuff = CharBuffer.allocate(8);
            cbuff.put('李');
            cbuff.put('建');
            cbuff.put('锋');
            cbuff.flip();
            // 将 CharBuffer 中的字符序列转换成字节序列
            ByteBuffer bbuff = cnEncoder.encode(cbuff);
            // 循环访问 ByteBuffer 中的每个字节
            for (int i = 0; i < bbuff.capacity(); i++) {
                System.out.print(bbuff.get(i) + " ");
            }
            // 将 ByteBuffer 的数据解码成字符序列
            System.out.println("\n" + cnDecoder.decode(bbuff));
        }
    }

9.6 文件锁

  1. 如果对各运行的程序需要并发修改同一文件时, 程序之间需要某种机制来进行通信, 使用文件锁可以有效的阻止多个进程并发修改同一文件

  2. 文件锁控制文件的全部或部分字节的访问; NIO中, Java提供了 FileLockl哎支持文件锁定功能, 在 FileChannel中提供的lock()/tryLock()方法可以获取文件锁 FileLock 对象, 从而锁定文件; 直接使用 lock() 或 tryLock() 方法获取的文件锁是排他锁; 处理完文件之后通过 FileLock 的 release()方法释放文件锁

  3. lock() 和 tryLock() 方法存在区别: 当 lock() 试图锁定某个文件时, 如果无法得到文件锁, 程序将一直阻塞; 而 tryLock()是尝试锁定文件, 它将直接返回而不是阻塞, 如果得到了文件锁, 该方法则返回该文件锁, 否则将返回null

  4. FileChannel 锁定部分文件的方法:

    • lock(long position, long size, boolean shared): 对文件从 position 开始, 长度位 size 的内容加锁, 该方法是阻塞式的, 当参数 shared 为 true 时, 表明该锁是一个共享锁, 它将允许多个进行来读取该文件, 但阻止其他进程获得对该文件的排他锁; 当 shared 为 false 时, 表明该锁是一个排他锁, 他将锁住对该文件的读写; 程序可以通过FileLock 的 isShared 来判断它获得的锁是否为共享锁
    • tryLock(long position, long size, boolean shared): 非阻塞式的加锁方式, 参数含义同上

代码示例

    import java.io.FileOutputStream;
    import java.nio.channels.FileChannel;
    import java.nio.channels.FileLock;
    //
    public class FileLockTest {
        public static void main(String[] args) throws Exception {
            try (
                // 使用 FileOutPutStream 获取 FileChannel
                FileChannel channel = new FileOutputStream(
                    "D:\\File\\Java\\JavaBase\\src\\xyz\\xmcs\\CrazyJava\\IO\\a.txt").getChannel()) {
                // 使用非阻塞方式对指定文件加锁
                FileLock lock = channel.tryLock();
                // 程序暂停10S
                Thread.sleep(10000);
                // 释放锁
                lock.release();
            }
        }
    }

10. Java 7 的 NIO.2

10.1 Path, Paths 和 Files 核心 API

  1. Path 接口代表一个平台无关的平台操作; Files 包含大量静态的工具方法来操作文件; Paths 包含两个返回 Path 的 静态工厂方法

代码示例

    // Path 接口的功能和方法
    import java.nio.file.Path;
    import java.nio.file.Paths;
    //
    public class PathTest {
        public static void main(String[] args) throws Exception {
            // 以当前路径来创建Path对象
            Path path = Paths.get(".");
            System.out.println("path 里包含的路径数量: " + path.getNameCount());
            // 获取path对应的绝对路径
            Path absolutePath = path.toAbsolutePath();
            System.out.println(absolutePath);
            // 获取绝对路径的根路径
            System.out.println("absolutePath 的根路径: " + absolutePath.getRoot());
            // 获取绝对路径所包含的路径数量
            System.out.println("absolutePath 里包含的路径数量: " + absolutePath.getNameCount());
            System.out.println(absolutePath.getName(0));
            // 以多个String来构建Path对象
            Path path2 = Paths.get("D:", "Books", "Java");
            System.out.println(path2);
        }
    }

代码示例

    // Files 操作文件工具类
    import java.io.FileOutputStream;
    import java.nio.charset.Charset;
    import java.nio.file.FileStore;
    import java.nio.file.Files;
    import java.nio.file.Paths;
    import java.util.ArrayList;
    import java.util.List;
    //
    public class FilesTest {
        public static void main(String[] args) throws Exception {
            String FileTestPath = "D:\\File\\Java\\JavaBase\\src\\xyz\\xmcs\\CrazyJava\\IO\\FilesTest.java";
            // 复制文件
            Files.copy(Paths.get(FileTestPath), new FileOutputStream("a.txt"));
            // 判断 FilesTest.java 文件是否为隐藏文件
            System.out.println("FilesTest.java 是否为隐藏文件: "
                + Files.isHidden(Paths.get(FileTestPath)));
            // 一次性读取 FilesTest.j所有行
            List<String> lines = Files.readAllLines(Paths.get(FileTestPath), Charset.forName("UTf-8"));
            System.out.println(lines);
            // 判断指定文件的大小
            System.out.println("FilesTest.Java 的大小为: "
                + Files.size(Paths.get(FileTestPath)));
            List<String> poem = new ArrayList<>();
            poem.add("水晶潭底银鱼跃");
            poem.add("清风徐来碧竿横");
            // 直接将多个字符串内容写入指定文件中
            Files.write(Paths.get("poem.txt"), poem, Charset.forName("UTF-8"));
            // 使用Java8新增的 Stream API 列出当前目录下所有文件和子目录
            Files.list(Paths.get(".")).forEach(path -> System.out.println(path));
            // 使用Java8 新增的API读取文件内容
            Files.lines(Paths.get(FileTestPath), Charset.forName("UTF-8")).forEach(line -> System.out.println(line));
            FileStore cStore = Files.getFileStore(Paths.get("C:"));
            // 判断 C盘的总空间, 可用空间
            System.out.println("C盘的共有空间: " + cStore.getTotalSpace());
            System.out.println("C盘的可用空间: " + cStore.getUsableSpace());
        }
    }

10.2 使用 FileVisitor 便利文件和目录

  1. FileVisitor 代表一个文件访问器, walkFileTree() 方法会自动遍历 start 路径下的所有文件和子目录, 遍历文件和子目录都会 "触发" FileVisitor 中想相应的方法

  2. Files 类来遍历文件和子目录的方法:

    • walkFileTree(Path start, FileVisitor<? super Path> visitor): 遍历 start 路径下的所有文件和子目录
    • walkFileTree(Path start, set options, int maxDepth, FileVisitor<? super Path> visitor)该方法最多遍历maxDepth深度的文件
  3. FileVisitor 中定义的4个方法:

    • FileVisitorResult postVisitDirectory(T dir, IOException exc): 访问子目录之后触发该方法
    • FileVisitorResult preVisitDirectory(T dir, BasicFileAttributes attrs): 访问子目录之前触发该方法
    • FileVisitorResult visitFile(T file, BasicFileAttributes attrs): 访问file文件时触发该方法
    • FileVisitorResult visitFileFailed(T file, IOException exc): 访问 file 文件失败是触发该方法
    • 注意: 以上四个方法都返回一个 FileVisitorResult 对象, 它是一个枚举类, 代表了访问之后的后续行为
  4. FileVisitorResult 定义的几种后续行为:

    • CONTINUE: 代表了 "继续访问" 的后续行为
    • SKIP_SIBLINGS: 代表 "继续访问" 的后续行为, 但不访问该文件或目录的兄弟文件或目录
    • SKIP_SUBTREE: 代表 "继续访问" 的后续行为, 但不访问该文件或目录的子目录树
    • TERMINATE: 代表 "中止访问" 的后续行为
  5. 实际编程时没必要为FileVisitor的4个方法都提供实现, 可以通过继承 SimpleFileVisitor(FileVisitor实现类) 来实现自己的 "文件访问器", 这样就根据需要, 选择性地重写指定方法了

代码示例

    // 使用 FileVisitor 来遍历文件和子目录
    import java.io.IOException;
    import java.nio.file.*;
    import java.nio.file.attribute.BasicFileAttributes;
    //
    public class FileVisitorTest {
        public static void main(String[] args) throws Exception {
            // 遍历 D:\Books\Java目录下的所有文件和子目录
            Files.walkFileTree(Paths.get("D:", "Books", "Java")
                , new SimpleFileVisitor<Path>(){
                // 访问文件时触发该方法
                @Override
                public FileVisitResult visitFile(Path file , BasicFileAttributes attrs) throws IOException {
                    System.out.println("正在访问: " + file + " 文件");
                    // 找到了 FileVisitorTest.java 文件
                    if (file.endsWith("On Java 8.pdf")) {
                        System.out.println("---已经找到目标文件---");
                        return FileVisitResult.TERMINATE;
                    }
                    return FileVisitResult.CONTINUE;
                }
                // 开始访问目录时触发该方法
                @Override
                public FileVisitResult preVisitDirectory(Path dir
                    , BasicFileAttributes attrs) throws IOException {
                    System.out.println("正在访问: " + dir + " 路径");
                    return FileVisitResult.CONTINUE;
                }
            });
        }
    }

10.3 使用 WatchService 监控文件变化

  1. 以前版本Java监控文件的方法: 单启一条后台线程, 这条后台线程每隔一段时间就去 "遍历" 一次指定目录的文件,如果发现此次遍历结果与上次遍历结果不同, 则认为文件发生了变化. (繁琐, 性能不好)

  2. NIO.2 的 Path 类提供了如下一个方法来监控文件系统的变化:

    • register(WatchService watcher, WatchEvent.Kind<?>...events): 用Watch监听该Path代表的目录下的文件变化events参数指定要监听的那些类型的事件
  3. 在 NIO.2 的Path 类提供的这个方法中WatchService 代表一个文件系统监听服务, 它负责监听 Path 代表的目录下的文件变化, 一旦使用register()方法完成注册之后, 加下来就可调用 WatchService的下面三个方法来获取被监听目录的文件变化事件:

    • WatchKey poll(): 获取下一个 WatchKey, 如果没有 WatchKey 发生就立即返回null
    • WatchKey Poll(long timeout, timeUnit unit): 尝试等待 timeout 事件去获取下一个 WatchKey
    • WatchKey take(): 获取下一个WatchKey, 如果没有 WatchKey 发生就一直等待

代码示例

    // 通过 WatchService 来监控 D:\\财务部 下面的文件
    import java.nio.file.*;
    //
    public class WatchServiceTest {
        public static void main(String[] args) throws Exception {
            // 获取文件系统的 WatchService 对象
            WatchService watchService = FileSystems.getDefault().newWatchService();
            // 为 C: 盘根路径注册监听
            Paths.get("D:\\财务部").register(watchService
                , StandardWatchEventKinds.ENTRY_CREATE
                , StandardWatchEventKinds.ENTRY_MODIFY
                , StandardWatchEventKinds.ENTRY_DELETE);
            while (true) {
                // 获取下一个文件变化事件
                WatchKey key = watchService.take();
                for (WatchEvent<?> event : key.pollEvents()) {
                    System.out.println(event.context() + " 文件发生了 "
                        + event.kind() + " 事件!");
                }
                // 重设 WatchKey
                Boolean valid = key.reset();
                // 如果重设失败, 退出监听
                if(!valid) {
                    break;
                }
            }
        }
    }

10.4 访问文件属性

  1. Java7的 NIO.2 在 java.nio.file.attribute 报下提供了大量的工具类, 可以非常简单, 方便的读取, 修改文件的属性

  2. 这些工具类分类:

    • XxxAttributeView: 代表某种文件属性的 "视图"
    • XxxAttributes: 代表某种文件属性的 "集合", 程序一般通过 XxxAttributeView 对象来获取 XxxAttributes
  3. XxxAttributeView

    • AclFileAttributeView: 可以让开发者为特定文件设置 ACL(Access Control List)及文件所有者属性; 它的 getAcl()方法返回 List对象, 该返回值代表了该文件的权限集; 可以通过 setAcl(List)方法修改该文件的ACL
    • BasicFileAttributeView: 可以获取或修改文件的基本属性, 包括文件的最后修改时间, 最后访问时间, 创建时间, 大小是否为目录, 是否为符号链接等; readAttributes() 方法返回一个 BasicFileAttributes 对象, 对文件夹基本属性的修改是通过 BasicFileAttributes 对象完成的
    • DosFileAttributeView: 用于获取或修改文件的DOS相关属性, 比如文件是否为只读, 是否为隐藏, 是否为系统文件, 是否为存档文件等, 它的 readAttributes() 方法返回一个 DosFileAttributes 对象, 这些属性的修改是由 DosAttributes对象来完成的
    • FileOwnerAttributeView: 用于获取或修改文件的所有者, 通过 getOwner() 方法返回一个 UserPrincipal 对象来代表文件所有者; 通过调用 setOwner(UserPrincipal owner) 方法来改变文件的所有者
    • PosixFileAttributeView: 用于获取或修改POSIX(Portable Operating System Interface of INIX)属性, readAttributes()方法返回一个 PosixFileAttributes 对象, 该对象可用于获取或修改文件的所有者, 组所有者, 访问权限信息, 只用于Linux系统
    • UserDefinedFileAttributeView: 为文件设置自定义属性

代码示例

    import java.nio.ByteBuffer;
    import java.nio.charset.Charset;
    import java.nio.file.FileSystems;
    import java.nio.file.Files;
    import java.nio.file.Path;
    import java.nio.file.Paths;
    import java.nio.file.attribute.*;
    import java.util.Date;
    import java.util.List;
    //
    public class AttributeViewTest {
        public static void main(String[] args) throws Exception {
            // 获取将要操作的文件
            Path testPath = Paths.get("D:\\File\\aaa.txt");
            // 获取访问基本属性的 BasicFileAttributeView
            BasicFileAttributeView basicView = Files.getFileAttributeView(
                    testPath, BasicFileAttributeView.class);
            // 获取访问基本属性的 BasicFileAttributes
            BasicFileAttributes basicAttribs = basicView.readAttributes();
            // 访问文件的基本属性
            System.out.println("创建时间: " + new Date(basicAttribs.creationTime().toMillis()));
            System.out.println("最后访问时间: " + new Date(basicAttribs.lastAccessTime().toMillis()));
            System.out.println("最后修改时间: " + new Date(basicAttribs.lastModifiedTime().toMillis()));
            System.out.println("文件的大小: " + basicAttribs.size());
            // 获取访问文件属性主信息的 FileOwnerAttributeView
            FileOwnerAttributeView ownerView = Files.getFileAttributeView(
                    testPath, FileOwnerAttributeView.class);
            // 获取文件所属的用户
            System.out.println(ownerView.getOwner());
            // 获取系统中 guest 对应的用户
            UserPrincipal user = FileSystems.getDefault().getUserPrincipalLookupService().lookupPrincipalByName("guest");
            // 修改用户
            ownerView.setOwner(user);
            // 获取访问自定义属性的 FileOwnerAttributeView
            UserDefinedFileAttributeView userView = Files.getFileAttributeView(
                    testPath, UserDefinedFileAttributeView.class);
            List<String> attrNames = userView.list();
            // 遍历所有的自定义属性
            for(String name : attrNames) {
                ByteBuffer buf = ByteBuffer.allocate(userView.size(name));
                userView.read(name, buf);
                buf.flip();
                String value = Charset.defaultCharset().decode(buf).toString();
                System.out.println(name + " ----> " + value);
            }
            // 添加一个自定义属性
            userView.write("发行者", Charset.defaultCharset().encode("JavaBase"));
            // 获取访问DOS属性的DosFileAttributeView
            DosFileAttributeView dosView = Files.getFileAttributeView(
                    testPath, DosFileAttributeView.class);
            // 将文件设置隐藏, 只读
            dosView.setHidden(true);
            dosView.setReadOnly(true);
        }
    }

# Java