【文件IO】文件内容操作

读文件、写文件,都是操作系统提供了 API,在 Java 中也进行了封装,叫“文件流”/“IO流

Stream
流,形象比喻,水流/气流


水流的特点:我要通过水龙头,接 1000ml 水

  1. 直接一口气,把 1000ml 接完
  2. 一次接 500ml,分两次接完
  3. 一次接 100ml,分十次接完

IO 流的特点:我要从文件读取 100 字节文件

  1. 直接一口气,把 100 字节读完
  2. 一次读 50 字节,分两次读
  3. 一次读 10 字节,分十次

操作系统本身提供的文件读写 API 就是流式
Java 实现 IO 流,类有很多,主要分为两个大类:

字节流和字符流

  • 字节流:二进制文件使用
    • 读写数据的基本单位,就是字节
    • 一次读的 bit 不可少于 8 个,因为一个字节 8 个 bit,至少得读一个字节

表示字节流的类

  • InputStream,用来输入的

  • OutputStream,用来输出的

  • 字符流:文本文件使用

    • 一个字符不确定有几个字节,取决于实际的编码方式(GBK—一个汉字两个字节、UTF 8—一个汉字三个字节,一个字母一个字节
    • 内部做的工作更多,会自动的查询码表,把二进制数据转换成对应字符

表示字符流的类

  • Reader,输入
  • Writer,输出

比如,就像读取某个文件中的前 10 个汉字
使用字符流就可以非常方方便的实现

  • 直接读取 10 个字符
  • 字符流自动判定文件是哪种编码方式,再将字节分割好
  • 再读取对数量字节就得到 10 个汉字了

理解清楚“输入/输出”的方向(人为定义的)
把内存中的数据,放到硬盘上,视为输入还是输出呢?

  • 如果站在内存视角,就是输出
  • 如果站在硬盘视角,就是输入

后面但凡谈到输入输出,都是以 CPU 的视角来谈的,内存离 CPU 比硬盘离 CPU 更近

  • 数据远离 CPU,就是输出,将内存中的数据写到硬盘里
  • 数据靠近 CPU,就是输入,硬盘文件中的数据拿到内存里

上面四个输入输出的类,都是“抽象类
实际上真正干活的,并非这四个类
另外,Java 中,提供了很多很多类,实现上述的这四个抽象类
因为类太多了,就使得我们对于 IO 流的理解就非常费劲

  • 但虽然种类多,但其实大家的用法都差不多
  • 但凡类的名字是以“Read/Writer”结尾的,就是实现了 ReadWriter 的字符流对象
  • 但凡类的名字是以“InputStream/OutputStream”结尾的,就是实现了 InputStreamOutputStream 的字节流对象
import java.io.FileInputStream;  
import java.io.IOException;  
import java.io.InputStream;  
  
public class Demo8 {  
    //这个异常是IOException的子类,是其特殊的情况,可以直接写成 IOException    public static void main(String[] args) throws IOException {  
        //因为他是一个抽象类,所以不能直接new  
        //只能new一个实现了它的子类  
        InputStream inputStream = new FileInputStream("./text.txt");  
        //可以指定绝对路径,也可以指定相对路径,也可以指定 File 对象  
        inputStream.close();  
    }
}
  • FileNotFoundException 这个异常是 IOException 的子类,是他的一种特殊情况,可以就 throws 这个父类异常
  • 抽象类不能直接被 new,只能 new 一个实现了它的子类
    • 在这里还隐藏了一个操作,“打开文件”,针对文件进行读写,务必需要先打开(操作系统的基本要求)
  • 指定路径的时候,可以指定绝对路径,也可以指定相对路径,也可以指定 File 对象
  • 这个代码中,虽然要求文件使用完毕之后要关闭,但是局限于本代码,不写 close 也行。因为 close 之后,紧接着就是进程结束了
    • close 是释放“文件描述符表”里的元素,进程结束,意味着 PCB 就销毁了,PCB 上面的文件描述符表就整个释放了

文件资源泄露

打开文件之后,还需要关闭文件

打开文件,其实是在该进程的文件描述符表中,创建了一个新的表项

  • 进程 => PCB(进程控制块)=> 文件描述表
  • 这个表描述了该进程都需要操作哪些文件
  • 可以认为它是一个数组,数组的每个元素就是一个 struct file 对象(Linux 内核)
  • 每个结构体就描述了对应操作的文件的信息
  • 数组的下标,就称为“文件描述符

每次打开一个文件,就相当于在数组上占用一个位置,而在系统内核中,文件描述附表数组是固定长度&不可扩容的。除非主动调用 close 关闭文件,此时才会释放空间。如果代码里一直打开,不去关闭,就会使这里的资源越来越少,把数组填满了,后续再打开文件就会打开失败

这样的问题,不容易被发现,泄露不是一瞬间就泄露完耳朵,这是一个持续的过程。整个问题直到所有的资源泄露完毕,这一刻才会集中的爆发出来

import java.io.FileInputStream;  
import java.io.IOException;  
import java.io.InputStream;  
  
public class Demo8 {  
    public static void main(String[] args) {  
        try(InputStream inputStream = new FileInputStream("./text.txt")){  
                    }catch(IOException e){  
            e.printStackTrace();  
        }    
    }
}
  • close 的时候,问了防止因为一些特殊原因代码执行不到 close,有一种特殊的 try 方法——try with sources
  • 这里() 中的创建的资源可以是多个
  • try{}执行完毕,最终都会自动执行这里的 close
    • 不过想在() 里面写,必须是实现了 Closable 接口的类
      `

字节流

1. 读文件

在文件打开之后,就需要读文件了

image.png

import java.io.FileInputStream;  
import java.io.IOException;  
import java.io.InputStream;  
  
public class Demo8 {  
    public static void main(String[] args) {  
        try(InputStream inputStream = new FileInputStream("./text")){  
            while (true) {  
                int b = inputStream.read();  
                if(b == -1){  
                    //读取完毕  
                    break;  
                }                
                System.out.printf("0x%x\n",b);  
            }        
        }catch(IOException e){  
            e.printStackTrace();  
        }    
    }
}
//运行结果(text文件内容:hello)
0x68
0x65
0x6c
0x6c
0x6f
//(text文件内容:你好)
0xe4
0xbd
0xa0
0xe5
0xa5
0xbd
  • 当读到最后一个字节,就返回 -1
  • 打印字节的时候,一般都用十六进制进行表示,方便随时换算成二进制
  • hello,可在 ASCII 码表中找到对应单词;“你好”因为是六个字节,所以可以确定是 UTF8 编码方式,就可以在 UTF8 码表中对应打印出的内容拼出“你好”

频繁读取多次硬盘,当前硬盘的 IO 就耗时比较大,希望能减少 IO 的次数

byte[] buffer = new byte[1024];  
int n = inputStream.read(buffer);
  • 这个操作就会把硬盘中读取到的对应的数据,填充到 buffer 内存的字节数组中,并且尽可能填满(只需要一次 IO
    • 此处是把 buffer 形参当成了“输出型参数”
    • 平时写代码,方法的参数一般是“输入型参数”,使用返回值表示输出结果
  • 虽然是一次读的内容多了,但也比一次读 1 个字节,分很多次读效率高不少
  • 返回的 n 代表实际读到的字节数

  • 这个过程也非常类似于“去食堂打饭”
    • 拿空盘递给阿姨打饭(空 bufferread
    • 阿姨打满后,再把盘给你(read 把读完的内容装进 buffer

import java.io.FileInputStream;  
import java.io.IOException;  
import java.io.InputStream;  
  
public class Demo9 {  
    public static void main(String[] args) {  
        try(InputStream inputStream = new FileInputStream("./text")){  
            byte[] buffer = new byte[1024];  
            int n = inputStream.read(buffer);  
            for (int i = 0; i < n; i++) {  
                System.out.printf("0x%x\n",buffer[i]);  
            }        
        }catch (IOException e){  
            e.printStackTrace();  
        }    
    }
}

和 Scanner 结合:

import java.io.FileInputStream;  
import java.io.IOException;  
import java.io.InputStream;  
import java.util.Scanner;  
  
public class Demo14 {  
    public static void main(String[] args) throws IOException{  
        try(InputStream inputStream = new FileInputStream("./text")){  
            Scanner scanner = new Scanner(inputStream);  
            while(scanner.hasNextInt()){  
                System.out.println(scanner.nextInt());  
            }        
        }catch (IOException e){  
            e.printStackTrace();  
        }    
    }
}
  • 这样也可以完成文件内容的读取

2. 写文件

image.png|449

在文件中写入“你好

import java.io.*;  
  
public class Demo10 {  
    public static void main(String[] args) throws IOException {  
        try(OutputStream outputStream = new FileOutputStream("./text");){  
            outputStream.write(0xe4);  
			outputStream.write(0xbd);  
			outputStream.write(0xa0);  
			outputStream.write(0xe5);  
			outputStream.write(0xa5);  
			outputStream.write(0xbd);
        }catch (IOException e){  
            e.printStackTrace();  
        }    
    }
}
  • 这里是按照一个字节一个字节的方式进行写入的
  • 每次执行写操作的时候,都会先把之前的内容清空
    • 只要使用 OunputStream 打开文件,文件里面的内容就没了
    • 这样的操作,可能就把文件内容搞没了,并且找不回来了

还有一种“追加写”的方式,保持原内容不变,在末尾写入新内容

try(OutputStream outputStream = new FileOutputStream("./text",true);)
  • 在最后加上一个参数 true,代表开启“追加写”的方式

一次把整个字节数组都写入:

import java.io.*;  
  
public class Demo10 {  
    public static void main(String[] args) throws IOException {  
        try(OutputStream outputStream = new FileOutputStream("./text",true);){  
            byte[] buffer = new byte[] {(byte)0xe4,(byte)0xbd,(byte)0xa0,(byte)0xe5,(byte)0xa5,(byte)0xbd};  
            outputStream.write(buffer);  
        }catch (IOException e){  
            e.printStackTrace();  
        }  
    }  
}

InputStream / OutputStream 读写数据就是按照字节来操作的。如果要读写字符的话(中文),此时就绪要靠程序员手动来区分出哪几个字节是一个字符,再确保把这几个字节作为整体来写入

字符流

1. 读文件

为了方便处理字符,引入字符流
image.png|518

一次读一个字符:

import java.io.FileReader;  
import java.io.IOException;  
import java.io.Reader;  
  
public class Demo11 {  
    public static void main(String[] args) throws IOException {  
        try(Reader reader = new FileReader("./text")){  
            while (true) {  
                int c = reader.read();  
                if (c == -1) return;  
                char ch = (char) c;  
                System.out.println(ch);  
            }        }catch (IOException e){  
            e.printStackTrace();  
        }    
    }
}
  • 每次 read 读到的就是一个汉字
  • 最初按照字节来读的时候,是每个汉字三个字节,但在 Java 中一个 char 是两个字节,怎么用两个字节表示出了一个汉字?
    • 当使用 char 表示这里的汉字的时候,不再使用 UTF8,而是使用 unicode 编码方式
    • unicode 中,一个汉字就是两个字节
  • 使用字符流读取数据的过程,Java 标准库内部就自动针对数据的编码进行转码了

用字符数组一次读若干字符:

import java.io.FileReader;  
import java.io.IOException;  
import java.io.Reader;  
  
public class Demo12 {  
    public static void main(String[] args) throws IOException {  
        try(Reader reader = new FileReader("./text")){  
            char[] buffer = new char[1024];  
            int n = reader.read(buffer);  
            System.out.println(n);  
            for (int i = 0; i < n; i++) {  
                System.out.println(buffer[i]);  
            }        
        }catch (IOException e){  
            e.printStackTrace();  
        }    
    }
}

2. 写文件

import java.io.FileWriter;  
import java.io.IOException;  
import java.io.Writer;  
import java.nio.channels.WritableByteChannel;  
  
public class Demo13 {  
    public static void main(String[] args) throws IOException {  
        try(Writer writer = new FileWriter("./text")){  
            writer.write("你好世界");  
        }catch (IOException e){  
            e.printStackTrace();  
        }    
    }
}
  • 直接写入一个 String 到文件中

小结:
当前设计的这八个类,虽然数目不少,但用法都很相似
基本流程:打开 —> 读写 —> 关闭

评论 8
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值