精通IO流:文件处理的必备技能!

我们在掌握了 File 类、字节流、字符流,学会了 IO 操作的套路之后,IO 操作基本上就能处理日常工作中80%的常用问题了。
今天再给大家介绍一下处理流,学会处理流之后,日常工作中的文件操作就都可以应对了,掌握了下面的处理流,你将如虎添翼。

我们知道从 IO 流的功能来划分,IO 流分为:节点流和处理流。其中,节点流是用来包装数据源(File)的,它直接和数据源连接,表示从一个节点读取数据或者把数据写入到一个节点;处理流是用来包装节点流的,它是对一个已经存在的节点流进行连接,处理流通过增加缓存的方式来提高输入输出操作的性能。

处理流按照功能划分,可以分为:缓冲流、转换流、数据处理流、对象处理流。缓冲流是为了提高处理性能的,转换流是字节流转换为字符流用于处理乱码的(解码与编码的字符集问题),数据处理流就是对 8个基本类型和字符串类型数据的直接处理,对象数据处理流就是经常说的序列化与反序列化操作。

1

缓冲流

84ca0ea2ae2f70b7acdf2959a659803a.png

1、认识字节缓冲流

字节缓冲流就是用缓冲流包裹字节流,也就是说在创建缓冲流对象的时候,需要传入一个字节流的对象,同时会创建一个默认 8KB 的字节数组的缓冲区,通过这个缓冲区进行读写操作,以减少 IO 的次数,从而提高字节流的处理性能。

字节缓冲流分为:字节输入缓冲流 BufferedInputStream 和字节输出缓冲流 BufferedOutputStream,以下代码是字符缓冲流的源码:

// 字节输入缓冲流的构造方法
 private static int DEFAULT_BUFFER_SIZE = 8192;  // 默认8KB
 public BufferedInputStream(InputStream in) {
        this(in, DEFAULT_BUFFER_SIZE);
}
 public BufferedInputStream(InputStream in, int size) {
        super(in);
        if (size <= 0) {
            throw new IllegalArgumentException("Buffer size <= 0");
        }
        buf = new byte[size];  // 建立缓冲区
}
 // 节输出缓冲流的构造方法
 public BufferedOutputStream(OutputStream out) {
        this(out, 8192); // 默认8KB
}
 public BufferedOutputStream(OutputStream out, int size) {
        super(out);
        if (size <= 0) {
            throw new IllegalArgumentException("Buffer size <= 0");
        }
        buf = new byte[size]; // 建立缓冲区
}

2、用字节缓冲流实现文件拷贝功能

@Test
  public void testCopy() throws IOException {
    // 1、使用File类与文件建立联系
    File src = new File("D:/file/image/tomcat.png");
    File dest = new File("D:/file/image/tomcat2.jpg");
    // 2、选择对应的输入流或者输出流
    InputStream is = new BufferedInputStream(new FileInputStream(src)); // 用缓冲流包裹节点流
    OutputStream os = new BufferedOutputStream(new FileOutputStream(dest)); // 用缓冲流包裹节点流
    // 3、进行读写操作
    byte[] b = new byte[1024];
    int len = 0;
    while ((len = is.read(b)) != -1) {
      os.write(b, 0, len);
    }
    os.flush();
    // 4、关闭资源
    os.close();
    is.close();
  }

运行结果:

db8372f34af219ac7b123db15a9c615c.png

3、认识字符缓存流

字符缓冲流就是用缓冲流包裹字符流,也就是说在创建缓冲流对象的时候,需要传入一个字符流的对象,同时会创建一个默认 8KB 的字符数组的缓冲区,通过这个缓冲区进行读写操作,以减少 IO 的次数,从而提高字符流的处理性能。

字符缓冲流分为:字符输入缓冲流 BufferedReader 和字符输出缓冲流 BufferedWriter,以下代码是字符缓冲流的源码:

// 字符输入缓冲流的构造方法
private static int defaultCharBufferSize = 8192; // 默认8KB
public BufferedReader(Reader in) {
        this(in, defaultCharBufferSize);
}
public BufferedReader(Reader in, int sz) {
        super(in);
        if (sz <= 0)
            throw new IllegalArgumentException("Buffer size <= 0");
        this.in = in;
        cb = new char[sz]; // 建立缓冲区
        nextChar = nChars = 0;
}
// 字符输出缓冲流的构造方法
private static int defaultCharBufferSize = 8192; // 默认8KB
public BufferedWriter(Writer out) {
        this(out, defaultCharBufferSize);
}
public BufferedWriter(Writer out, int sz) {
        super(out);
        if (sz <= 0)
            throw new IllegalArgumentException("Buffer size <= 0");
        this.out = out;
        cb = new char[sz];  // 建立缓冲区
        nChars = sz;
        nextChar = 0;
        lineSeparator = java.security.AccessController.doPrivileged(
            new sun.security.action.GetPropertyAction("line.separator"));
}

4、用字符缓冲流实现文件拷贝功能

/**
   * 字符缓冲流只能处理纯文本的copy
   */
  @Test
  public void testCopy1() throws IOException {
    // 1、使用File类与文件建立联系
    File src = new File("D:/file/txt/output_char.txt");
    File dest = new File("D:/file/image/output_char_coppy.txt");
    // 2、选择对应的输入流或者输出流
    // 想使用新增的readLine方法(不能发生多态)
    BufferedReader reader = new BufferedReader(new FileReader(src));// 用缓冲流包裹节点流
    BufferedWriter writer = new BufferedWriter(new FileWriter(dest, true));// 用缓冲流包裹节点流
    // 3、进行读或写操作
    String line = null;
    while ((line = reader.readLine()) != null) {
      writer.write(line);
      writer.newLine(); // 类似于writer.append("\r\n");
    }
    writer.flush(); // 强制刷出
    // 4、关闭资源
    writer.close();
    reader.close();
  }

运行结果:

ea39647e87eec9de2ebf486764ac10c5.png

因为缓冲流可以提高文件操作的性能,所以在以后的开发中,大家尽量要用缓冲流对节点流进行包装,不要直接使用字节流和字符流去操作文件。

通过以上的代码大家再来体会一下 IO 流操作的套路,是不是套路在手,操作不愁啊!

2

转换流

73c86bc48b61dc89a3f90ba64e1577d9.png

1、乱码产生的原因

我们首先来看两个概念,什么是编码,什么是解码?要区分这两个概念的话,也比较好理解:我们从码的角度出发来认识它们,码就是计算机能看懂的东西,也就是“二进制”,等同于字节,人类能看懂的语言是“字符或者字符串”。

如果是人类能看懂的变为计算机能看懂的就叫编码,也就是说“字符或者字符串”变为字节就是编码,反过来,如果是计算机能看懂的变为人类能看懂的就叫解码,也就是说字节变为“字符或者字符串”就是解码。

大家可以通过加密和解密来对比理解,人看不懂就是加密,人能看到就是解密。

乱码产生的原因有两个:

1、编码与解码的字符集不相同,导致乱码;

2、字节缺少或者长度丢失,导致乱码;

/**
   * 乱码的原因
   */
  @Test
  public void test() throws UnsupportedEncodingException {
    // 默认字符集“utf-8”
    System.out.println("默认字符集:" + System.getProperty("file.encoding"));
    String info = "北京欢迎您!"; // 解码
    byte[] data = info.getBytes(); // 编码:char--->byte,字符或者字符串到字节
    // 编码与解码字符集统一,都使用工作空间默认的字符集
    System.out.println(new String(data)); // 解码:byte--->char,字节到字符或者字符串
    // 不统一则出现乱码
    System.out.println(new String(data, "GBK"));


    // 编码与解码的字符集必须相同,否则乱码
    byte[] data2 = "JPM,你好!".getBytes("GBK");// 编码
    String info2 = new String(data2, "GBK");// 解码
    System.out.println(info2);


    // 乱码的原因之二,字节缺少,长度丢失
    String str = "北京";
    byte[] data3 = str.getBytes();
    System.out.println(data3.length); // 6
    System.out.println(new String(data3, 0, 5)); // 字节数不完整导致乱码
  }

运行结果:

默认字符集:UTF-8
北京欢迎您!
鍖椾含娆㈣繋鎮紒
JPM,你好!
6
北�

2、认识转换流

在 Java IO 中除了字节流和字符流外,还有一组字节流转换位字符流的类,用于处理乱码问题。

字节输入流 InputStreamReader:作用是将输入的字节流变为字符流。

字节输出流 OutputStreamWriter:作用是将输出的字节流变为字符流。

转换流只能是把字节流转为字符流,从而完成它的使命,那是因为字符流不能设置字符集,只能是把字符流变为字节流才能进行字符集的设置,因为字节流才有设置字符集的方法。

// 输入流 InputStreamReader 解码
public InputStreamReader(InputStream in, String charsetName)
        throws UnsupportedEncodingException
    {
        super(in);
        if (charsetName == null)
            throw new NullPointerException("charsetName");
        sd = StreamDecoder.forInputStreamReader(in, this, charsetName);
    }
    
//输出流 OutputStreamWriter 编码
public OutputStreamWriter(OutputStream out, String charsetName)
        throws UnsupportedEncodingException
    {
        super(out);
        if (charsetName == null)
            throw new NullPointerException("charsetName");
        se = StreamEncoder.forOutputStreamWriter(out, this, charsetName);
    }

3、转换流的文件拷贝demo,仔细体会注释的文字

/**
   * 转换流:字节转为字符<br>
   * 1、输入流 InputStreamReader 解码<br>
   * 2、输出流 OutputStreamWriter 编码<br>
   * 仔细体会注释的文字
   */
  @Test
  public void test2() throws IOException {
    String srcPath = "D:/file/txt/output_char.txt";
    String destPath = "D:/file/txt/output_char_convert.txt";
    // FileReader(字符流)不能解码,FileInputStream(字节流)才能解码
//    BufferedReader br = new BufferedReader(new FileReader(new File(srcPath)));  
    // 字符流FileReader要换成字节流FileInputStream,但是字节流与字符流不能直接操作,需要通过转换流InputStreamReader来实现
    BufferedReader br = new BufferedReader(new InputStreamReader(new FileInputStream(new File(srcPath)), "UTF-8")); // 指定解码字符集
    // FileWriter(字符流)不能编码,FileOutputStream(字节流)才能编码
//    BufferedWriter writer = new BufferedWriter(new FileWriter(new File(destPath))); 
    // 字符流FileWriter要换成字节流FileOutputStream,但是字节流与字符流不能直接操作,需要通过转换流OutputStreamWriter来实现
    BufferedWriter wr = new BufferedWriter(
        new OutputStreamWriter(new FileOutputStream(new File(destPath)), "UTF-8"));// 指定编码字符集
    // 读取并写出
    String line = null;
    while ((line = br.readLine()) != null) {
      wr.write(line);
      wr.newLine();
    }
    wr.flush();
    wr.close();
    br.close();
  }

运行结果:

a86c581dfb69be9454512ced4b391ec3.png

3

数据处理

324e23292ea4096349434adada39869e.png

在 Java IO 中,提供了两个数据(基本数据类型+String)操作流 ,分别是数据输入流 DataInputStream 和数据输出流 DataOutputStream。

下面直接通过一个例子来演示数据处理流的用法:

@Test
  public void test() throws IOException {
    write("D:/file/txt/data.txt"); // 写到文件
    read("D:/file/txt/data.txt"); // 从文件读取
  }
  /**
   * 基本数据类型+String类型输出到文件
   */
  public static void write(String destPath) throws IOException {
    int intNum = 100;
    long longNum = 999L;
    float floatNum = 3.14f;
    double doubleNum = 5.50;
    String str = "基本数据类型+String类型输出到文件";
    File dest = new File(destPath);
    DataOutputStream dos = new DataOutputStream(new BufferedOutputStream(new FileOutputStream(dest)));
    // 操作:注意写出的顺序,读取要和写出的顺序一致
    dos.writeInt(intNum);
    dos.writeLong(longNum);
    dos.writeFloat(floatNum);
    dos.writeDouble(doubleNum);
    dos.writeUTF(str);
    dos.flush();
    dos.close();
  }
  /**
   * 从文件里读取基本数据类型+String类型
   */
  public static void read(String srcPath) throws IOException {
    File src = new File(srcPath);
    DataInputStream dis = new DataInputStream(new BufferedInputStream(new FileInputStream(src)));
    int intNum = dis.readInt();
    long longNum = dis.readLong();
    float floatNum = dis.readFloat();
    double doubleNum = dis.readDouble();
    String str = dis.readUTF();
    dis.close();
    // 100---->999---->3.14---->5.5---->基本数据类型+String类型输出到文件
    System.out.println(intNum + "---->" + longNum + "---->" + floatNum + "---->" + doubleNum + "---->" + str);
  }

运行结果:

100---->999---->3.14---->5.5---->基本数据类型+String类型输出到文件

4

对象处理

0d792c37b9e128670b8ab5bc7c1ddd11.png

对象处理流的操作就是我们经常说的序列化与反序列化操作。序列化就是把一个对象变为二进制流的一种方法,通过对象序列化可以方便地实现对象的传输和存储,反过来,如果把一个对象读入到程序的过程及时反序列化。序列化操作需要使用输出流 ObjectOutputStream 进行输出,反序列化操作需要使用输出流 ObjectInputStream 进行读取对象数据。

如果一个类的对象想被序列化,这个类必须实现 Serializable 接口,同时要注意这个类对象的版本兼容问题,一般我们再要进行序列化的类中设置一个固定的 serialVersionUID 常量,这个值只要不修改,序列化和反序列化操作就不会发生版本兼容问题。

为了减少保存对象的使用空间,可以把一个类的某个属性设置为不被序列化,当实现 Serializable 接口实现序列化的时候,可以使用  transient 关键字进行声明。

下面直接通过一个例子来演示对象处理流的用法:

/**
   * 对象的序列化以及反序列化操作
   */
  @Test
  public void test() throws FileNotFoundException, IOException, ClassNotFoundException {
    String filePath = "D:/file/txt/object.txt";
    serializa(filePath);
    Object object = UnSerializa(filePath);
    if (object instanceof User) {
      object = (User) object;
    }
    // User [name=JPM, age=18, address=null],因为address属性被transient修饰,没有被序列化,所以为null
    System.out.println(object.toString());
  }


  /**
   * 对象序列化:对象变为二进制流的方法
   */
  public static void serializa(String destPath) throws FileNotFoundException, IOException {
    File dest = new File(destPath);
    ObjectOutputStream oos = new ObjectOutputStream(new BufferedOutputStream(new FileOutputStream(dest)));
    User user = new User("JPM", 18, "中国,北京!");
    oos.writeObject(user);
    oos.flush();
    oos.close();
  }


  /**
   * 对象反序列化:使用对象输入流读取对象数据
   */
  public static Object UnSerializa(String srcPath) throws FileNotFoundException, IOException, ClassNotFoundException {
    Object object = null;
    File scr = new File(srcPath);
    ObjectInputStream ois = new ObjectInputStream(new BufferedInputStream(new FileInputStream(scr)));
    object = ois.readObject();
    ois.close();
    return object;
  }
  
/**
 * 序列化与反序列化的对象,必须实现Serializable接口
 */
public class User implements Serializable {


  private static final long serialVersionUID = -6954786920974801199L;


  private String name;
  private int age;
  // transient修饰的属性不会被序列化
  private transient String address; 


  public User() {
    super();
  }


  public User(String name, int age, String address) {
    super();
    this.name = name;
    this.age = age;
    this.address = address;
  }


  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 String getAddress() {
    return address;
  }


  public void setAddress(String address) {
    this.address = address;
  }


  @Override
  public String toString() {
    return "User [name=" + name + ", age=" + age + ", address=" + address + "]";
  }


}

运行结果:

User [name=JPM, age=18, address=null]

f9288d8fbe4e496881d7c62bdbc3d1b7.png

这是 Java IO 操作的第三篇文章,文章有点长,坚持看下来的小伙伴们也非常不容易,但是我想说,能坚持看完这三篇 IO 文章的同学,你一定掌握了 Java IO 处理的套路,面对未来开发中涉及到的 IO 操作,一定会更加从容自如,如果你能把所有的示例代码手动敲一遍,那你的感觉就会更加美好,不信你试试。

回顾一下:

IO 第一篇:带你认识 File 类 我们对 File 的使用做了详细介绍;

IO 第二篇:IO 流,掌控一切 我们对 IO 流的操作套路有了深刻认识;

IO 第三篇:就是本文,我们对不能不懂的 IO 处理流进行了总结;

Java IO 系列第四篇:IO 操作大结局,敬请期待。

  • 7
    点赞
  • 27
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值