IO

102 阅读10分钟

开启掘金成长之旅!这是我参与「掘金日新计划 · 2 月更文挑战」的第 3 天,点击查看活动详情

File类

  • java.io.File类:文件和文件目录路径的抽象表示形式
  • File不能访问文件内容本身。如果需要访问文件内容本身,则需要使用输入/输出流
  • 想要在Java中表示一个真实存在的文件或目录,那么必须有一个File对象,但是Java程序中的一个File对象,可能没有一个真实存在的文件或目录

路径分隔符:

  • windows和DOS系统默认使用“ \ ”来表示
  • UNIX和URL使用“ / ”来表示

构造方法

  • public File(String pathname) :可以是绝对路径或者相对路径

    • main()方法是相对于工程
    • 测试方法则是相对于当前module
  • public File(String parent,String child):以parent为父路径,child为子路径创建File对象

  • public File(File parent,String child):根据一个父File对象和子文件路径创建File对象

常用方法

获取:

  • public String getAbsolutePath():获取绝对路径
  • public String getPath() :获取路径
  • public String getName() :获取名称
  • public String getParent():获取上层文件目录路径。若无,返回null
  • public long length() :获取文件长度(即:字节数)。不能获取目录的长度
  • public long lastModified() :获取最后一次的修改时间,毫秒值
  • public String[] list() :获取指定目录下的所有文件或者文件目录的名称数组
  • public File[] listFiles() :获取指定目录下的所有文件或者文件目录的File数组(绝对路径

重命名:

public boolean renameTo(File dest):把文件重命名为指定的文件路径,dest不能在硬盘中

判断:

  • public boolean isDirectory():判断是否是文件目录
  • public boolean isFile() :判断是否是文件
  • public boolean exists() :判断是否在硬盘中存在
  • public boolean canRead() :判断是否可读
  • public boolean canWrite() :判断是否可写
  • public boolean isHidden() :判断是否隐藏

创建:

  • public boolean createNewFile():创建文件。若文件存在,则不创建,返回false
  • public boolean mkdir():创建文件目录。如果此文件目录存在,就不创建了。如果此文件目录的上层目录不存在,也不创建
  • public boolean mkdirs():创建文件目录。如果上层文件目录不存在,一并创建

删除:

  • public boolean delete():删除文件或者文件夹,要删除一个文件目录,该文件目录内不能包含文件或者文件目录

知识加油站

字符集和字符编码

字符集是很多个字符的集合,例如 GB2312 是简体中文的字符集,它收录了六千多个常用的简体汉字及一些符号,数字,拼音等字符。


字符编码是字符集的一种实现方式,把字符集中的字符映射为特定的字节或字节序列,它是一种规则。

比如:Unicode 只是字符集,UTF-8、UTF-16、UTF-32 才是真正的字符编码规则

Unicode

Unicode 是国际标准字符集,它将世界各种语言的每个字符定义一个唯一的编码,以满足跨语言、跨平台的文本信息转换。

Unicode 字符集的编码范围是 0x0000 - 0x10FFFF , 可以容纳一百多万个字符, 每个字符都有一个独一无二的编码,也即每个字符都有一个二进制数值和它对应,这里的二进制数值也叫码点 , 比如:汉字 "中" 的 码点是 0x4E2D, 大写字母 A 的码点是 0x41, 具体字符对应的 Unicode 编码可以查询 Unicode字符编码表。

Unicode 字符存储

Unicode 是一个符号集, 它只规定了每个符号的二进制值,但是符号具体如何存储它并没有规定。

由于编码范围是 0x0000 - 0x10FFFF,因此需要 1 到 3 个字节来表示。

那么,对于三个字节的 Unicode字符,计算机怎么知道它表示的是一个字符而不是三个字符呢 ?

如果所有字符都用三个字节表示,那么对于那些一个字节就能表示的字符来说,有两个字节是无意义的,对于存储来说,这是极大的浪费。

因此,Unicode 出现了多种存储方式,常见的有 UTF-8、UTF-16、UTF-32,它们分别用不同的二进制格式来表示 Unicode 字符。UTF-8、UTF-16、UTF-32 中的 "UTF" 是 "Unicode Transformation Format" 的缩写,意思是"Unicode 转换格式",后面的数字表明至少使用多少个bit来存储字符, 比如:UTF-8 最少需要8个bit也就是一个字节来存储,对应的, UTF-16 和 UTF-32 分别需要最少2个字节和4个字节来存储。

UTF-8

UTF-8: 是一种变长字符编码,将码点编码为 1 至 4 个字节,具体取决于码点数值中有效二进制位的数量

编码规则

  • 对于单字节的符号,字节的第一位设为 0,后面 7 位为这个符号的 Unicode 码。因此对于英语字母,UTF-8 编码和 ASCII 码是相同的, 所以 UTF-8 能兼容 ASCII 编码,这也是互联网普遍采用 UTF-8 的原因之一。

  • 对于 n 字节的符号( n > 1),第一个字节的前 n 位都设为 1,第 n + 1 位设为 0,后面字节的前两位一律设为 10 。剩下的没有提及的二进制位(x),全部为这个符号的 Unicode 码

    下表是Unicode编码对应UTF-8需要的字节数量以及编码格式:

    image.png

例如:查询 "中" 的UTF-8编码

  1. 先查询 "中" 字的 Unicode 码 0x4E2D,对应范围的第三种,需要三个字节
  2. 0x4E2D对应的二进制为 0100 1110 0010 1101
  3. 然后从二进制的最后一位开始,从后向前依次填入格式中的x
  4. 得到 ”中“的UTF-8编码为11100100 10111000 10101101

更多见“编码”文章。。。。。。

IO流

流的分类:

  • 按操作数据单位不同分为:字节流(8 bit),字符流(16 bit)

    • 对于文本文件(.txt,.java,.c),一般使用字符流处理
    • 对于非文本文件(.doc,.jpg,.mp3),使用字节流处理
  • 按数据流的流向不同分为:输入流,输出流

  • 按流的角色的不同分为:节点流(文件流),处理流

image.png

IO流体系:

image.png

操作步骤:

  1. 实例化File类
  2. 实例化流
  3. 读入 / 写出操作(以下就是针对不同的操作进行说明)
  4. 流的关闭

节点流

FileReader & FileWriter(字符流)

  • Java中字符是采用Unicode字符集

  • 非文本文件不能用字符流,原因如下:

    • java通过字符流读写文件,默认是按照UTF-8读写的
    • UTF-8的编码格式有4种,当文件中的字节格式不符合时(详情见知识加油站),字符流会将其转换为FFFD (unicode码点),然后再将FFFD转换为UTF-8,导致文件读取的有问题。
    • 简单来说就是字符流想读取一个字符,然后发现不认识这个字符,就会转为FFFD。
  • 程序中打开的文件 IO 资源不属于内存里的资源,垃圾回收机制无法回收该资源,所以要显式关闭文件IO资源

常用方法:

  • int read():读取单个字符。返回范围在 0 到 65535 之间,如果已到达流的末尾,则返回 -1
  • int read(char[] cbuf):读取多个字符,并将字符覆盖cbuf数组。如果已到达流的末尾,则返回 -1。否则返回本次读取的字符数

输出操作,对应的File可以不存在。

  • 如果不存在,会自动创建

  • 如果存在:

    • FileWriter(file, false) / FileWriter(file):覆盖原有的文件
    • FileWriter(file, ture):在原有的文件基础上追加内容

FileInputStream & FileOutputStream(字节流)

  • 通常作用于非文本
  • 可以复制文本,但不能在程序中读取文本,可能会出现乱码。

    • 原因:程序中读取文本可能出现中文,而中文占多个字节,导致被切割(即read())) 成乱码。

常用方法:

  • int read():从输入流中读取数据的下一个字节。返回 0 到 255 。如果到达流末尾而没有可用的字节,则返回值 -1
  • int read(byte[] b):同FileReader
  • void write(byte[] b, int off, int len):将byte 数组中从偏移量 off 开始的 len 个字节写入此输出流

处理流

  • 在节点流外面包了一层流,套在相应的节点流之上。
  • 只要关闭最外层流即可,关闭最外层流也会相应关闭内层节点流。

缓冲流

  • 提高读写的速度,因为内部提供了一个缓冲区。

    • 当读取数据时,数据先读入缓冲区,其后的读操作则直接访问缓冲区

    • 当使用BufferedInputStream读取字节文件时,BufferedInputStream会一次性从文件中读取8192个(8Kb),存在缓冲区中,直到缓冲区装满了,才重新从文件中读取下一个8192个字节数组。

    • 向流中写入字节时,不会直接写到文件,先写到缓冲区中直到缓冲区写满,BufferedOutputStream才会把缓冲区中的数据一次性写到文件里。使用方法flush()可以手动将缓冲区的内容全部写入输出流

      image.png

转换流(属于字符流)

  • 转换流提供了在字节流和字符流之间的转换

    • InputStreamReader:将一个字节的输入流转换为字符的输入流。解码
    • OutputStreamWriter:将一个字符的输出流转换为字节的输出流。编码
  • 使用转换流来处理文件乱码问题。实现编码和解码的功能

    • public InputSreamReader(InputStream in,String charsetName):charsetName:指定字节输入流的编码规则
    • public OutputSreamWriter(OutputStream out,String charsetName):charsetName:指定字符输出流的编码规则

对象流

ObjectInputStream和OjbectOutputSteam,用于存储和读取基本数据类型数据或对象的处理流,可以把Java中的对象写入到数据源中,也能把对象从数据源中还原回来。

writeObject(obj):输出可序列化对象,输出一次,操作flush()一次

readObject():读取流中的对象

序列化和反序列化

对象序列化机制允许把内存中的Java对象转换二进制流,从而允许把这种二进制流持久地保存在磁盘上,或通过网络将这种二进制流传输到另一个网络节点。当其它程序获取了这种二进制流,就可以恢复成原来的Java对象


对象支持序列化前提:

  • 实现Serializable接口 或 Externalizable接口(不常用)

  • 在类中声明serialVersionUID静态常量,表示序列化对象版本。不加的话由Java运行时自动生成,如果修改类的成员,serialVersionUID也会跟着变化,从而导致反序列化异常。

    • 在进行反序列化时,JVM会把传来的字节流中的serialVersionUID与本地相应实体类的serialVersionUID进行比较,如果相同

      就认为是一致的,可以进行反序列化,否则就会出现序列化版本不一致的异常。(InvalidCastException)

  • 类的属性也必须是可序列化的(默认情况基本数据类型可序列化)

  • ObjectOutputStream和ObjectInputStream不能序列化statictransient修饰的成员