Java_常用API_1_IO流

IO 流

java.io 包下的 API 。

介绍与补充

IO 流简介

  • IO 流的概念:

    • IO 指的是输入/输出(Input/Output),而“流”是一种抽象概念,是对数据传输的总称,数据在设备间的传输便称为流;

      近似地,可以认为一个流就是一个方向的数据通道,可以打开,可以传输,可以关闭。并且,这个通道用完必须关闭,即所有和 IO 相关的最后都必须释放资源(运行 close() 方法)。

    • 字节流就是最本质的数据流,Java 中按不同的使用方式和应用场景,对底层流进行包装,得到各种各样的流,实际上最终直接与系统底层接触的是还是文件字节流。包装的方向如:

      • 用它实现实际的流传输,在包装中优化方法,如缓冲机制等等;

      • 用它实现自己特定的场景的应用;

  • 使用 IO 流的基本步骤:

    1. 创建流对象;

    2. 调用流对象的读/写数据方法;

    3. 释放资源。

  • 小专题说明:

    Java IO API 中的 void write(int i)int read() 问题——最小单元数据要么单字节 byte 类型,要么双字节 char 类型,而这里都用四字节 int 的原因:

    • 首先,对于底层实际存储的组合状态/二进制表示,按数值理解的时候,按补码读取解析成数值,把数值存入底层的时候也是存它的补码;
    • 然后,read() 方法的设计是返回 -1 代表读取结束,为了防止出现数据本身当数值理解时就是 -1 从而没读完就误结束了,所以不管是 byte 类型还是 char 类型,Java 底层代码都是在前面补 0 ,增加字节到 4 字节,拓展成 int ,这样,高位被拓展都是 0 ,int 数值就肯定不可能成 -1 了。
    • 对于“复制”,在 write(int i) 的时候,Java 底层代码会字节相应地按位去除上述拓展出的字节,仅保留它应有的原来的字节,从而对外部而言,实际数据没变。
    // 对于这点,可有如下小试验:
    public static void main(String[] args) throws IOException {
        // 汉字 “你” 的 Unicode 代码点为 U+4F60 转为 10 进制就是 20320 。
        FileOutputStream fos = new FileOutputStream("test.txt");
        fos.write(228);
        fos.write(189);
        fos.write(160);
        fos.close();
    
        FileInputStream fis = new FileInputStream("test.txt");
        byte[] bs = new byte[3];
        fis.read(bs);
        System.out.println(Arrays.toString(bs)); // 控制台输出: [-28, -67, -96]
        fis.close();
    
        InputStreamReader isr = new InputStreamReader(new FileInputStream("test.txt"), "UTF-8");
        int i = isr.read();
        isr.close();
        System.out.println(i); // 控制台输出: 20320
        System.out.println((char) i); // 控制台输出: 你
    }
    

补充

底层数据存储
  1. 字节的概念:

    • 字节是计算机的底层存储的基本单元,每个字节包含 8 个比特位,每个比特位的状态是 01 ,每个字节可以表示 256 种状态。

    • 每个字节中的 8 个比特位的组合状态可以用无符号二进制或十六进制数来指代,注意也仅仅是指代而已。

      如: 一个字节上的电平状态 高高低低高低高低 可用二进制 11001010 或十六进制 CA 来指代。

  2. 应用程序对底层存储组合状态的使用举例:

    • 文件被文本编辑器打开读取显示时,底层存储的组合状态,会被按一定规则映射到字符,编辑存储时,也会按一定的规则将字符映射成底层实际存储的组合状态写入底层。
    • 在 Java 中进行字节流操作时,get/read byte 操作会把底层存储的组合状态按数值补码的形式理解,从而得到一个数值数据,write byte 操作又会把数值数据的补码形式对应的组合状态写入底层。对于如复制等操作,这样的正反映射最终对底层的实际存储状态的复制并没有影响。Java 中的按位的操作,也是操作底层实际的状态。
Unicode 编码
  1. UCS 与 Unicode 对照标准:

    • UCS 是 Universal Character Set 的简称,即“通用字符集”。它有两种编码方式,分别为 UCS-2 和 UCS-4,前者代表使用 2 个字节表示一个字符,后者代表使用 4 个字节表示一个字符。

      UCS-4 的划分范围标准:

      1. 第 1 个字节使用 7 位,从而总体划分为 128 个组(group)
      2. 第 2 个字节使用 8 位,从而对于每个组又划分为 256 个平面(plane)
      3. 第 3 个字节使用 8 位,从而对于每个平面又划分为 256 行(row)
      4. 第 4 个字节使用 8 位,从而对于每个行又划分为 256 个码位(cell)
    • Unicode 就是一个基于 USC 划分标准的字符对照表,它计划使用第 0 组的 17 个平面,Unicode 编码的范围是 0 ~ 0x10FFFF(从而最多使用1114112 个码位)。任何文字在 Unicode 中都对应一个值,这个值称为代码点(Code Point)。

      其中:

      • Unicode 将第 0 组的第 0 个平面称为基本多文种平面,英文缩写 BMP,其余 16 个平面称为辅助平面;
      • 一个平面最多表示 65536 个字符,超过 BMP 范围的字符放到了辅助平面进行定义;
      • 并不是每个代码点都被使用对应成字符的,在 BMP 中有些被保留为专用区,有些被保留为代理区,以用作特定的用途,各个辅助平面也都是没有定义满。
  2. Unicode 的 3 种编码方式:

    每个字符的 Unicode 代码点是确定的,但在实际传输过程中,由于不同系统平台的设计不一定一致,以及出于节省空间的目的,对 Unicode 编码的实现方式是不同的。Unicode 的实现方式称为 Unicode Translation Format,简称 UTF 。常见编码方式如下:

    所谓大小头( BE、LE )问题,就是先存高位字节还是先存低位字节的存放顺序问题。

    • UTF-32 :

      完全对应于 UCS-4,把 UCS-4 规定的代码点直接用 4 个字节保存下来。

      很明显,这太浪费空间了,有太多的 0 。

    • UTF-16 :

      • 在 BMP 中,对于除了代理区外的代码点,都直接按 UCS-2 标准,将代码点直接用 2 个字节保存下来;
      • 对于超出 BMP 范围的字符,则需要使用 4 字节。具体的,在 BMP 中,U+D800~U+DFFF 是其代理区,这些代码点没有被单独对照成字符,当使用 UTF-16 编码时,会将两个代理区代码点构成的代理对进行计算得出一个辅助平面中的代码点,由此映射到辅助平面中的一个字符。

      历史遗留问题:

      • 提及 unicode 编码习惯性地指的是 UTF-16 。
    • UTF-8 :

      UTF-8 编码方式会占用 1~4 个字节,Unicode 代码点与具体存储内容的对应规则如下:

      Unicode 代码点Unicode 代码点二进制第 1 字节第 2 字节第 3 字节第 4 字节
      U+0000~U+007F0xxxxxxx0xxxxxxx
      U+0080~U+07FF00000xxx xxyyyyyy110xxxxx10yyyyyy
      U+0800~U+FFFFxxxxyyyy yyzzzzzz1110xxxx10yyyyyy10zzzzzz
      U+10000~U+10FFFF00000000 000wwwxx xxxxyyyy yyzzzzzz11110www10xxxxxx10yyyyyy10zzzzzz

      有如下规律:

      • 对于编码后会占 1 个字节的字符,字节的第 1 位设 0 ,后面 7 位为这个符号的 Unicode 代码点;
      • 对于编码后会占 n 个字节的符号(其中 n > 1),第 1 个字节的前 n 位都设为 1 ,第 n + 1 位设为 0,其余各字节的前 2 位一律都设为 10 ,最后将这个字符对应的 Unicode 代码点二进制,从低字节开始填充到刚刚没有提及的位上,不足补 0
File 类
  • File 类概述:

    它将路径封装成对象,通过用此对象调用方法的方式来对此路径所表示的文件或目录进行操作。

    关于文件的 tip :同一目录下不能有重名的文件/目录,文件和目录同名也不行。

  • 常用的构造方法:

    方法名说明
    File(String pathname)通过将给定的路径名字符串转换为抽象路径名来创建新的 File 实例
    File(String parent, String child)从父路径名字符串和子路径名字符串创建新的 File 实例
    File(File parent, String child)从父抽象路径名和子路径名字符串创建新的 File 实例
  • 常用方法:

    • 创建文件/目录:

      方法名说明
      boolean createNewFile()创建由此抽象路径名命名的新空文件(要求最后一级的父级目录必须已存在)
      boolean mkdir()创建由此抽象路径名命名的目录(要求最后一级的父级目录必须已存在)
      boolean mkdirs()创建由此抽象路径名命名的目录(可创建多级还不存在的目录)

      这几个方法的共同点:

      • 若路径指代的文件/目录还不存在,则创建,并返回 true
      • 若路径指代的文件/目录已存在,则不执行任何操作,并返回 false
    • 删除文件/目录:

      方法名说明
      boolean delete()删除由此抽象路径名表示的文件或目录

      注意: 此方法在该目录中有内容的情况下,不会执行删除操作,并会返回 false

    • 查询文件/目录:

      方法名说明
      boolean isDirectory()测试此抽象路径名表示的File是否为目录
      boolean isFile()测试此抽象路径名表示的File是否为文件
      boolean exists()测试此抽象路径名表示的File是否存在
      String getAbsolutePath()返回此抽象路径名的绝对路径名字符串
      String getPath()将此抽象路径名转换为路径名字符串
      String getName()返回由此抽象路径名表示的最后一级的文件或目录的名称
      String[] list()返回此抽象路径名表示的目录中的文件和目录的名称组成的字符串数组
      File[] listFiles()返回此抽象路径名表示的目录中的文件和目录的组成的File对象数组
  • 补充:用递归算法遍历一个文件夹下的所有文件,如:

    import java.io.File;
    
    public class JavaSE {
    
        public static void main(String[] args) {
            goThrough(new File("test"));
        }
    
        static void goThrough(File file) {
            if (file.isDirectory()) {
                File[] files = file.listFiles();
                for (File f : files) {
                    goThrough(f);
                }
            } else if (file.isFile()) {
                System.out.println(file.getAbsolutePath());
            }
        }
    }
    
    

    注意:递归必须要有出口。

字节流和字符流

字节流

简化体系及说明
实现
实现
继承
继承
继承
继承
继承
实现
继承
继承
继承
继承
«接口»
Closeable
close()
«接口»
Flushable
flush()
«抽象类»
OutputStream
write()
FileOutputStream
ObjectOutputStream
writeObject()
FilterOutputStream
BufferedOutputStream
PrintStream
print()
println()
«抽象类»
InputStream
read()
FileInputStream
ObjectInputStream
readObject()
FilterInputStream
BufferedInputStream
  • 常用读写方法:

    方向方法说明
    输出void write(int b)将单个字节数据写入输出流,传入值为前 3 字节补 0 拓展为 int 的结果数值
    void write(byte[] b)将数组 b 中的字节数据写入输出流
    void write(byte[] b, int off, int len)仅使用数组 b 的一部分位置:off 为起始索引,len 为使用长度
    输入int read()从输入流读取单个字节数据,返回值为此字节数据前 3 字节补 0 拓展为 int 的结果数值,读完则返 -1
    int read(byte[] b)从输入流读取 b.length 个字节数据到数组 b,返回实际读到的字节个数,读完则返 -1
    int read(byte[] b, int off, int len)仅使用数组 b 的一部分位置:off 为起始索引,len 为使用长度
  • 常用资源方法:

    方法说明
    flush()对于缓冲输出流,执行此方法会立即将当前缓冲区中的内容写入底层输出流
    close()释放系统资源。此外,对于缓冲输出流,还会包含在 close 前 flush 一次的操作
  • 说明:

    • 对于输入和输出,优化的方法是使用字节数组作为缓冲,从而降低单独调用系统底层功能和占用资源的次数,速度也快很多。此外,约定俗成的,字节数组长度用 1024 的整数倍(这样就是整数个“kB”了)。

    • 对于“复制”,要注意的是,若输入流的长度不能被接受数组的长度整除,那么最后一次把数据写进数组的时候,数组会有些后面的位置没有被覆盖数据。应该使用例如如下伪代码的思路:

      int len = 0;
      byte[] bs = new byte[1024];
      while ((len = in.read(bs)) != -1) {
          out.write(bs, 0, len);
      }
      
常用类
  1. 基本字节流之:FileOutputStreamFileInputStream

    方向构造方法说明
    输出FileOutputStream(String path)由文件系统中的路径名 path 创建一个文件输出流(覆盖)
    FileOutputStream(String path,boolean append)若第二个参数传 true 则流将写入文件的末尾而不是开头(追加)
    输入FileInputStream(String path)由文件系统中的路径名 path 创建一个文件输入流

    说明:

    • FileOutputStreamFileInputStream 属于基本字节流,调用方法操作会直接和底层系统接触,调用底层系统功能、占用底层系统资源;

    • 构造方法传入的路径名,用于在内部构造一个 File 对象,也正因如此,在使用文件输出流的时候,若传入的路径名所指向的目标不存在,则会创建一个文件。

  2. 缓冲字节流之:BufferOutputStreamBufferedInputStream

    方向构造方法说明
    输出BufferedOutputStream(OutputStream out)创建字节缓冲输出流对象,默认用于缓冲的字节数组长度为 8192
    BufferedOutputStream(OutputStream out, int size)创建指定缓冲区大小的字节缓冲输出流对象
    输入BufferedInputStream(InputStream in)创建字节缓冲输入流对象,默认用于缓冲的字节数组长度为 8192
    BufferedInputStream(InputStream in, int size)创建指定缓冲区大小的字节缓冲输入流对象

    说明:

    • BufferOutputStreamBufferedInputStream 属于字节缓冲流,也就是通过建立缓冲的机制,减少对系统底层的调用次数。调用方法会与其内部缓冲区的数据打交道,而其内部缓冲区和底层打交道又由它们自行使用封装在内部的底层流进行。
    • 字节缓冲流对象的构造需要传入字节流对象,是因为字节缓冲流对象本身仅仅提供缓冲区,而真正与底层的数据读写还得靠基本的字节流对象进行操作。

字符流

  • 字符流是对字节流的一个转换流,在代码与底层实际存储数据之间多了一层字节与字符的转换。它也处于比较底层的位置,后续的很多包装流既提供基于字节流构造,也提供基于字符流构造的方法,所以字符流也可当作底层的 IO 流之一吧。

  • 所有的字符流(xxxWriter),包括基本字符流,都内置了缓冲区,因此必须直接或间接调用 flush() 方法才能把数据传给底层流。

Java 内码问题
  • Java 运行时,字符在运行内存中都是使用 char 类型数据来保存和使用的,其中, char 类型数据是采用 UTF-16 编码方式与实际字符进行映射的。

    因此,超过 BMP 范围的单字符,按 UTF-16 的编码方式,需要 4 个字节,从而在 Java 内存中会占 2 个 char

  • Java 运行内存中的字符和底层存储的实际字节之间进行的编码或解码,不妨可认为分为 2 步:

    输入S1: 按指定字符集规则映射
    输入S2: 按 UTF-16 映射
    输出S1: 按 UTF-16 映射
    输出S2: 按指定字符集规则映射
    底层存储的实际字节
    Unicode 代码点字符
    运行内存中的 char 数据
简化体系及说明
实现
实现
继承
继承
继承
继承
实现
继承
继承
继承
«接口»
Closeable
close()
«接口»
Flushable
flush()
«抽象类»
Writer
write()
OutputStreamWriter
BufferedWriter
newLine()
FileWriter
PrintWriter
print()
println()
«抽象类»
Reader
read()
InputStreamReader
BufferedReader
readLine()
FileReader
  • 常用读写方法:

    方向方法说明
    输出void write(int c)写入一个字符,传入值为内存中 char 数据前 2 字节补 0 拓展为 int 的结果数值
    void write(char[] cbuf)写入一个字符数组
    void write(char[] cbuf, int off, int len)写入字符数组的一部分
    void write(String str)写入一个字符串
    void write(String str, int off, int len)写入一个字符串的一部分
    输入int read()往下读取一个字符,返回值为内存中 char 数据前 2 字节补 0 拓展为 int 的结果数值,读完了则返回 -1
    int read(char[] cbuf)往下读取一个字符数组
  • 常用资源方法:

    方法说明
    flush()将当前缓冲区中的内容立即写入底层输出流,注意字符流都包含缓冲机制
    close()释放系统资源。此外,对于缓冲输出流,包含 flush 操作。
常用类
  1. 基本字符流之:OutputStreamWriterInputStreamWriter

    • 概述:

      • OutputStreamWriter :是从字符流到字节流的桥梁,它使用指定的编码将写入的字符编码为字节;

      • InputStreamReader :是从字节流到字符流的桥梁,它读取字节,并使用指定的编码将其解码为字符;

      • 它们使用的字符集可以由名称指定,也可以被明确指定,或者可以接受平台的默认字符集。

    • 常用构造方法:

      方向构造方法说明
      输出OutputStreamWriter(OutputStream out)使用默认字符编码创建 OutputStreamWriter 对象
      OutputStreamWriter(OutputStream out, String charset)使用指定的字符编码创建 OutputStreamWriter 对象
      输入InputStreamReader(InputStream in)使用默认字符编码创建 InputStreamReader 对象
      InputStreamReader(InputStream in, String chatset)使用指定的字符编码创建 InputStreamReader 对象
  2. 包装字符流之:FileWriterFileReader

    • 概述:

      它们分别包装了上面的传入 FileOutputStream 对象构造 OutputStreamWriter 对象、传入 FileInputStream 对象构造 InputStreamWriter 对象的过程,简化了代码的写法。

    • 常用构造方法:

      方向构造方法说明
      输出FileWriter(String path)由路径名 path 构造文件字符输出流,使用平台默认字符集
      FileWriter(String path, boolean append)上一条的“追加版”
      FileWriter(String path, Charset cst)由路径名 path 构造文件字符输出流,使用指定字符集
      FileWriter(String path, Charset cst, boolean append)上一条的“追加版”
      输入FileReader(String path)由路径名 path 构造文件字符输入流,使用平台默认字符集
      FileReader(String path, Charset cst)由路径名 path 构造文件字符输入流,使用指定字符集
  3. 缓冲字符流之:BufferedWriterBufferedReader

    • 概述:

      对于字符流体系,Java 也提供了缓冲版本的对象,缓冲区有默认大小,也可自己在构造方法中传入设置。

    • 常用构造方法:

      方向构造方法说明
      输出BufferedWriter(Writer out)传入基本字符流对象以构造一个缓冲字符流对象,默认缓冲区大小
      BufferedWriter(Writer out, int size)相比上一条,指定缓冲区大小
      输入BufferedReader(Reader in)传入基本字符流对象以构造一个缓冲字符流对象,默认缓冲区大小
      BufferedReader(Reader in, int size)相比上一条,指定缓冲区大小
    • 除前层级接口规定外,特有的常用方法:

      方法说明
      BufferedWritervoid newLine()写入一个换行符(会自动根据所在操作系统适配)
      BufferedReaderString readLine()读取一行字符(不含换行符),读完返回 null

IO 流若干上层应用

打印流

这一类的包装类只负责输出数据,不负责读取数据,包装基础的 IO 流类的方法来提供了各种打印方法,不会抛出 IOException

  • 字节打印流 PrintStream

    继承自 OutputStream 体系,内部使用字节输出流作为底层流对象。

    构造方法说明
    PrintStream(String fileName)使用指定的文件名创建一个新的 PrintStream 对象
    常用方法说明
    write() 系列方法继承基础字节流按字节写入
    print() 系列方法能把代码中的量原样打印出
  • 字符打印流 PrintWriter

    构造方法说明
    PrintWriter(String fileName)使用指定的文件名创建一个新的 PrintWriter 对象
    PrintWriter(Writer out, boolean autoFlush)相比上条,若第二个参数传 true,则调用 print、format 方法会刷新输出缓冲区
    常用方法说明
    write() 系列方法继承基础字符流按字符写入
    print() 系列方法能把代码中的量原样打印出

标准输入输出流

Java 在 System 类中的成员变量 System.inSystem.out ,它们的类型的底层当然也是 IO 流,调用它们则会打开用户与系统的输入输出流(通道)。

  1. 标准输入流:

    • 通常该流对应于键盘输入或由主机环境或用户指定的另一个输入;

    • 在 Java 中,使用 System.in 来调用标准输入流( public static final InputStream in ),可见,它是基本字节流 InputStream 类型对象, Java 提供了 Scanner 类来包装这种键盘录入的输入流:

      常用构造方法说明
      Scanner(InputStream source)基于一个字节输入流构造一个 Scanner 对象
      常用方法说明
      xxx nextXxx()将键盘录入的数据转为相应类型的数据返回
  2. 标准输出流:

    • 通常该流对应于显示输出或由主机环境或用户指定的另一个输出目标;
    • 在 Java 中,使用 System.out 来调用标准输出流,它是一个打印流的对象( PrintStream extends OutputStream ),可以直接调用各种 print() 方法。

对象序列化流

  • 对象序列化:

    • 所谓的对象序列化,就是将对象保存到磁盘中或者在网络中传输对象。

    • 这种机制就是使用一个字节序列表示一个对象,该字节序列包含对象的类型、对象的数据和对象中存储的属性等信息。

    • 字节序列写到文件之后,相当于文件中持久保存了一个对象的信息;反之,该字节序列还可以从文件中读取回来,重构对象,对它进行反序列化;

  • 在 Java 中,要实现序列化和反序列化就要使用对象序列化流和对象反序列化流,同时要被序列化的对象,它所属类必须要实现 Serializable 接口。

    • Serializable 是一个标记接口,实现该接口,不需要重写任何方法。
    • 在序列化的时候,都按 Object 类型处理,在反序列化的时候,再自己将它向下转型为具体类型。
    • 不想参与序列化过程的成员,使用 transient 修饰符修饰。
    • serialVersionUID
      • 默认情况下由 Java 自动生成。
      • 当运行中的类与反序列化读取出的类对象版本不符合时,就会抛出 InvalidClassException 异常;
      • 为防止出现上面问题,可以手动添加该成员并锁定其值,如: private static final long serialVersionUID = 42L
  • 对象序列化流:ObjectOutputStream

    使用 ObjectOutputStream 对象可以将 Java 对象的原始数据类型和图形写入 OutputStream 。 可以通过使用流的文件来实现对象的持久存储。 如果流是网络套接字流,则可以在另一个主机上或另一个进程中重构对象。

    构造方法说明
    ObjectOutputStream(OutputStream out)传入一个 OutputStream 对象以构造 ObjectOutputStream 对象
    常用方法说明
    void writeObject(Object obj)将指定的对象写入 ObjectOutputStream
  • 对象反序列化流:ObjectInputStream

    使用 ObjectInputStream 对象反序列化先前使用 ObjectOutputStream 编写的原始数据和对象。

    构造方法说明
    ObjectInputStream(InputStream in)传入一个 InputStream 对象以构造一个 ObjectInputStream 对象
    常用方法说明
    Object readObject()从 ObjectInputStream 读取一个对象

Propoties 类

  • 概述:

    • 它本身是一个 Map 体系的集合类,存储的是字符串键值对。

    • 同时也提供了方便 IO 流操作的方法,存储的数据可以保存到流中或从流中加载。

    • 构造它的对象就是直接 new 类名() 就行,注意并没有泛型信息。

  • 相比基本 Map 和 IO 流,它的特有的常用方法:

    方法说明
    Object setProperty(String key, String value)设置集合的键和值,都是 String 类型
    String getProperty(String key)使用此属性列表中指定的键搜索属性
    Set<String> stringPropertyNames()从该属性列表中返回一个键 Set 集合,其中键及其对应的值是字符串
    void load(InputStream inStream)从输入字节流读取属性列表(键和元素对)
    void load(Reader reader)从输入字符流读取属性列表(键和元素对)
    void store(OutputStream out, String comments)将此属性列表(键和元素对)写入此 Properties 表中
    void store(Writer writer, String comments)将此属性列表(键和元素对)写入此 Properties 表中
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值