Java IO 第3篇:不能不懂的 IO 处理流

不能不懂的 IO 处理流

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

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

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

一、缓冲流

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();
  }

运行结果:

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

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();
  }

运行结果:


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

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

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

二、转换流

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();
  }

运行结果:


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

三、数据处理流

在 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类型输出到文件

四、对象处理流

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

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

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 160,333评论 4 364
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,812评论 1 298
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 110,016评论 0 246
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,293评论 0 214
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,650评论 3 288
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,788评论 1 222
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 32,003评论 2 315
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,741评论 0 204
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,462评论 1 246
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,681评论 2 249
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,168评论 1 262
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,528评论 3 258
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,169评论 3 238
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,119评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,902评论 0 198
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,846评论 2 283
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,719评论 2 274

推荐阅读更多精彩内容

  • tags:io categories:总结 date: 2017-03-28 22:49:50 不仅仅在JAVA领...
    行径行阅读 2,130评论 0 3
  • [TOC] IO流 IO流概述及其分类 IO概念 IO流用来处理设备之间的数据传输,Java对数据的操作是通过流的...
    wh_阅读 7,136评论 0 22
  • 1、IO流 1.1、概述 之前学习的File类它只能操作文件或文件夹,并不能去操作文件中的数据。真正保存数据的是文...
    Villain丶Cc阅读 2,540评论 0 5
  • Java中是通过流的方式对数据进行操作,用于操作流的类都在IO包中,IO流用来处理设备之间的数据传输。IO流按照流...
    阿Q说代码阅读 1,537评论 0 1
  • 本文对 Java 中的 IO 流的概念和操作进行了梳理总结,并给出了对中文乱码问题的解决方法。 1. 什么是流 J...
    Skye_kh阅读 737评论 0 2