深入理解IO流


什么是流

文件在程序中是以流的形式来操作的

流 : 数据在数据源(文件)和程序(内存)之间经历的路径

以Java程序(内存)为参考点

  • 输入流 : 数据从数据源到程序
  • 输出流 : 数据从程序到数据源

文件基础知识

🎪 具体操作查阅文档 ~

  1. 创建文件 createNewFile()
  2. 获取文件信息 getName length isFile getAbsolutePath getParent 等等
  3. 目录操作 mkdirs delete 等等

IO流原理及分类

  • I/O是input/output的缩写, 用于处理数据传输. 如读写文件, 网络通讯等
  • Java程序中, 对于数据的输入/输出操作以流的方式进行
  • 按操作数据单位不同分为 : 字节流(8 bit)和字符流
  • 按数据流的流向不同分为 : 输入流和输出流
  • 按流的角色的不同分为 : 节点流和处理流/包装流

Java的IO流共涉及到40多个类, 都是从4个抽象基类派生出来的分别是

抽象基类 字节流 字符流
输入流 InputStream Reader
输出流 OutputStream Writer

四个抽象基类的常用子类

FileInputStream

文件输入流-读取

让我们来实现一个小案例, 读取D盘下demo目录中的hello.txt文件

//使用多个字符读取方法
package io_test;

import com.sun.org.apache.xml.internal.utils.StringToIntTable;
import java.io.FileInputStream;
import java.io.IOException;

public class FileInPutStream_01 {
    public static void main(String[] args) {
    //要读取文件的路径
    String filePath = "d:\\demo\\hello.txt";
    //创建一个byte数组, 用来多个字符读取
    byte[] buf = new byte[8];//每次读取8个字节
    int readLen = 0;
    FileInputStream fileInputStream = null;
        try {
            //创建FileInputStream对象,用于读取文件
            fileInputStream = new FileInputStream(filePath);
            //从该输入流读取最多buf.length字节的数据, 如果没有输入可用, 该方法将被阻止
            //如果返回-1, 表示读取完毕
            //如果读取正常,返回实际读取的字节数
            while ((readLen = fileInputStream.read(buf)) != -1){
                //public String(byte[] bytes,要解码为字符的字节
                //              int offset,要解码的第一个字节的索引
                //              int length)要解码的字节数
                System.out.print(new String(buf,0,readLen));//显示
            }
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            //关闭流
            try {
                fileInputStream.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
}

注意 : 我们使用的是字节输入流的子类, 也就是说我们并不能读取中文. 因为会造成乱码.

FileOutputStream

文件输出流

让我们来实现一个小案例, 将数据写入D盘下的demo下的a.txt, 如果文件不存在, 则创建文件

//使用多个字符写入方法
package io_test;

import java.io.FileOutputStream;
import java.io.IOException;

public class FileOutPutStream_01 {
    public static void main(String[] args) {
        //要写入文件的路径
        String filePath = "d:\\demo\\a.txt";
        //创建FileOutputStream对象
        FileOutputStream fileOutPutStream = null;
        String str = "hello,world";
        try {
            //得到FileOutPutStream对象
            fileOutPutStream = new FileOutputStream(filePath);
            //写入字符串
            fileOutPutStream.write(str.getBytes());//把字符串转成byte数组
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            //关闭流
            try {
                fileOutPutStream.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
}

注意 : 如果是 new FileOutputStream(filePath) 创建方式, 当写入内容时, 会覆盖掉原来的内容, 但是有时我们并不想这样做, 而是想要在末尾添加.

那么我们可以使用 new FileOutputStream(filePath, true) 创建方式

FileReader

字符输入流-读取

常用方法 :

  • new FileReader(File/String)
  • read : 每次读取单个字符, 返回该字符, 如果到文件末尾返回-1
  • read(char[]) : 批量读取多个字符到数组, 返回读取到的字符数, 如果到文件末尾返回-1

相关API

​ String类

  • new String(char[]) : 将char[]转换成String
  • new String(char[],off,len) : 将char[]的指定部分转换成String

FileWriter

字符输出流

常用方法 :

  • new FileWriter(File/String) : 覆盖模式, 相当于流的指针在首端
  • new FileWriter(File/String, true) : 追加模式, 相当于流的指针在尾端
  • write(int) : 写入单个字符
  • write(char[]) : 写入指定数组
  • write(char[],off,len) : 写入指定数组的指定部分
  • write(string) : 写入整个字符串
  • write(string,off,len) : 写入字符串的指定部分

相关API

​ String类

  • toCharArray : 将String转换成char[]

BufferedReader

缓冲字符输入流, 属于处理流

  1. 节点流可以从一个特定的数据源读写数据, 也就是专门操作文件的
  2. 处理流(也叫包装流) 在已存在的流(节点流或处理流) 之上, 为程序提供更为强大的读写功能. 既可以消除不同节点流的实现差异, 也可以提供更方便的方法来完成输入输出
  3. 关闭处理流时, 只需关闭外层流就行了, 因为底层真正在操作的, 还是节点流

BufferedReader类中, 有属性Reader, 即可以封装一个节点流, 该节点流可以是任意的. 只要是Reader子类

让我们来实现一个小案例, 使用BufferedReader读取文本文件

package io_test;

import java.io.BufferedReader;
import java.io.FileReader;
import java.io.IOException;

public class BufferedReader_01 {
    public static void main(String[] args) throws IOException {
        String filePath = "d:\\demo\\a.java";
        //创建BufferedReader
        BufferedReader bufferedReader = new BufferedReader(new FileReader(filePath));
        //读取
        String line;//按行读取
        //当返回一个null时, 表示读取完毕
        while ((line = bufferedReader.readLine()) != null ){
            System.out.println(line);
        }
        //关闭流, 只需要关闭外层的处理流就行, java底层会自动去关闭节点流
        bufferedReader.close();

    }
}

BufferedWriter

缓冲字符输出流, 属于处理流

让我们来实现一个小案例, 把上一个案例读取到的数据写入到b.java中

package io_test;

import java.io.*;

public class BufferedWriter_01 {
    public static void main(String[] args) throws IOException {
        //源文件
        String srcFilePath = "d:\\demo\\a.java";
        //目标文件
        String destFilePath = "d:\\demo\\b.java";
        //按行读取
        String line;
        //创建BufferedReader
        BufferedReader bufferedReader = new BufferedReader(new FileReader(srcFilePath));
        //创建BufferedWriter
        BufferedWriter bufferedWriter = new BufferedWriter(new FileWriter(destFilePath));
        //边读边写
        while ((line = bufferedReader.readLine()) != null){
            bufferedWriter.write(line);
            //插入一个换行
            bufferedWriter.newLine();
        }
        //关闭流
        bufferedReader.close();
        bufferedWriter.close();
    }
}

注意 : BufferedReader 和 BufferedWriter 是按照字符操作的, 不要去操作二进制文件, 可能造成文件损坏

BufferedInputStream与BufferedOutputStream

字节处理流

让我们来实现一个小案例, 通过字节处理流完成图片的拷贝

package io_test;

import pc.B;

import java.io.*;

public class BufferedOutputStream_01 {
    public static void main(String[] args) throws IOException {
        //源文件
        String srcFilePath = "d:\\demo\\demo1\\drb.png";
        //目标文件
        String destFilePath = "d:\\demo\\drb1.png";
        //每次读取1024个字节
        byte [] buf = new byte[1024];
        int len;//实际读取的字节
        //创建BufferedInputStream
        BufferedInputStream bufferedInputStream = new BufferedInputStream(new FileInputStream(srcFilePath));
        //创建BufferedOutputStream
        BufferedOutputStream bufferedOutputStream = new BufferedOutputStream(new FileOutputStream(destFilePath));
        //边读边写
        while ((len = bufferedInputStream.read(buf)) != -1){
            bufferedOutputStream.write(buf, 0, len);
        }
        //关闭流
        bufferedInputStream.close();
        bufferedOutputStream.close();
    }
}

对象流

什么是对象流?

让我们首先看两个需求 :

  1. int num = 100; 这个int数据保存到文件中, 注意不是100数字, 而是int 100, 并且, 能够从文件中直接恢复int 100
  2. Dog dog = new Dog("小黄",3); 这个dog对象保存到文件中, 并且能够从文件恢复

上面的要求就是在进行序列化和反序列化操作.

那么什么是序列化和反序列化呢? 序列化就是在保存数据时, 保存数据的值和数据类型. 反序列化就是在恢复数据时, 恢复数据的值和数据类型.

为了让某个对象支持序列化机制, 必须实现两个接口之一(Serializable/Externalizable), 使其类是可序列化的.

ObjectOutputStream

对象字节输出流

//main中
String filePath = "e:\\data.dat";
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream(filePath));

//序列化数据到 e:\data.dat
oos.writeInt(100);// int -> Integer(实现了 Serializable)
oos.writeBoolean(true);// boolean -> Boolean(实现了 Serializable)
oos.writeChar("a");// char -> Character(实现了 Serializable)
oos.writeUTF("jack");//String
//保存一个Dog对象
oos.writeObject(new Dog("旺财",10));

oos.close();
System.out.println("数据保存完毕(序列化形式)");

//如果需要序列化某个类,需要接口Serializable(标记接口,没有方法)
class Dog implements Serializable{
    private String name;
    private int age;
    public Dog(String name,int age){
        this.name = name;
    }
}

ObjectInputStream

对象字节输入流

读取指定文件, 并进行反序列化恢复数据

//指定反序列化的文件
String filePath = "e:\\data.dat";
ObjectInputStream ois = new ObjectInputStream(new FileInputStream(filePath));
//读取
//读取的顺序需要和保存数据的顺序一致, 否则会出现异常
System.out.println(ois.readIne());
System.out.println(ois.readBoolean());
System.out.println(ois.readChar());
System.out.println(ois.readDouble());
System.out.println(ois.readUTF());
Object dog = ois.readObject();
System.out.println("运行类型=" + dog.getClass());
System.out.println("dog信息=" + dog);//底层 Object->Dog
//关闭流
ois.close();

对象处理流使用细节

  • 要求实现Serializable或者Externalizable接口, 一般而言, 我们使用的是Serializable. 因为它是一个标记接口, 而Externalizable需要我们实现其方法, 才能够使用.
  • 读写顺序要一致
  • 序列化对象时, 要求里面属性的类型也需要实现序列化接口
  • 序列化具备可继承性, 也就是如果某类已经实现了序列化, 则它的所有子类也已经默认实现了序列化
  • 序列化对象时, 默认将里面所有属性都进行序列化, 但除了static或transient修饰的成员

模拟修饰器设计模式

在前文我们提到的处理流就是使用修饰器设计模式, 不会直接与数据源相连.

下面让我们来模拟实现这种设计模式的源码

public abstract class Reader_{//抽象类   模拟为Reader   
    public void readFile(){}
    public void readString(){}
}

public class FileReader_ extends Reader_{//节点流
    public void readFile(){
        System.out.println("对文件进行读取...");
    }
}

public class StringReader_ extends Reader_{//节点流
    public void readString(){
        System.out.println("读取字符串...");
    }
}

public class BufferedReader_ extends Reader_{//处理流
    private Reader_ reader_;//属性
    //构造器
    public BufferedReader_(Reader_ reader_){
        this.reader_ = reader_;
    }
    
    //扩展
    //让方法更加灵活,多次读取文件
    public void readFiles(int num){
        for(int i =0;i < num;i++){
            reader_.readFile();
        }
    }
    //扩展 readString,批量处理字符串数据
    public void readStrings(int num){
         for(int i =0;i < num;i++){
            reader_.readString();
        }
    }
}

//main中
BufferedReader_ bufferedReader_ = new BufferedReader_(new FileReader_());
bufferedReader_.readFiles(10);//读十次文件

BufferedReader_ bufferedReader_ = new BufferedReader_(new StringReader_());
bufferedReader_.readStrings(5);//读取5次字符串

如何解决乱码问题?

默认情况下, 我们使用UTF-8编码, 但是如果编码出现了问题, 不是使用UTF-8而是其他编码, 例如ANsI, 我们会出现中文乱码的问题. 如何解决? 我们可以使用转换流来避免出现乱码问题.

转换流 : 可以把字节流转换成字符流.

当处理纯文本数据时, 如果使用字符流效率更高, 并且可以有效解决中文乱码问题, 所以建议将字节流转换成字符流, 可以在使用时指定编码格式

InputStreamReader

Reader的子类, 可以将InputStream(字节流)转换成Reader(字符流)

将字节流 FileInputStream 转成字符流 InputStreamReader, 指定编码 UTF-8

package io_test;

import java.io.BufferedReader;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStreamReader;

public class InputStreamReader_ {
    public static void main(String[] args) throws IOException {
        String filePath = "e:\\a.txt";
        //把FileInputStream 转成 InputStreamReader, 指定了utf-8码
        InputStreamReader inputStreamReader = new InputStreamReader(new FileInputStream(filePath), "utf-8");
        //把InputStreamReader 传入 BufferedReader ,使用BufferedReader读取(效率高)
        BufferedReader bufferedReader = new BufferedReader(inputStreamReader);
        //读取
        String s = bufferedReader.readLine();
        System.out.println("读取的内容" + s);
        //关闭流
        bufferedReader.close();
    }
}

OutputStreamWriter

Writer的子类, 可以将OutputStream(字节流)转换成Writer(字符流)

将FileOutputStream(字节流) 转换成 OutputStreamWriter(字符流) 指定编码形式gbk

package io_test;

import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStreamWriter;

public class OutputStreamWriter_ {
    public static void main(String[] args) throws IOException {
        String filePath = "e:\\a.txt";
        OutputStreamWriter outputStreamWriter = new OutputStreamWriter(new FileOutputStream(filePath), "gbk");
        outputStreamWriter.write("hi,哒哒");
        outputStreamWriter.close();
    }
}

配置文件properties

//读取
//使用Properties类来读取mysql.properties文件
//1.创建Properties对象
Properties properties = new Properties();
//2.加载指定配置文件
properties.load(new FileReader("src\\mysql.properties"));
//3.把k-v显示到控制台
properties.list(System.out);
//4.根据key获取对应的值
String user = properties.getProperty("user");

//修改
//使用Properties类添加key-val到新文件mysql.properties中
//1.如果该文件没有key就是创建
//2.如果该文件有key就是修改
Properties properties = new Properties();
properties.setProperty("charset","utf8");
properties.setProperty("user","汤姆");
//将 k-v存储到文件中
properties.store(new FileOutputStream("src:\\mysql2.properties"),null);//注释这里为null

文章作者: 冬瓜冬瓜排骨汤
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 冬瓜冬瓜排骨汤 !
  目录