Java IO 流总结

本文对 Java 中的 IO 流的概念和操作进行了梳理总结,并给出了对中文乱码问题的解决方法。

1. 什么是流

Java 中的流是对字节序列的抽象,我们可以想象有一个水管,只不过现在流动在水管中的不再是水,而是字节序列。和水流一样,Java 中的流也具有一个 “流动的方向”,通常可以从中读入一个字节序列的对象被称为输入流;能够向其写入一个字节序列的对象被称为输出流。

以下是 IO 相关类的总结图

image

2. 字节流

Java 中的字节流处理的最基本单位为单个字节,它通常用来处理二进制数据。Java 中最基本的两个字节流类是 InputStream 和 OutputStream,它们分别代表了组基本的输入字节流和输出字节流。InputStream 类与 OutputStream 类均为抽象类,我们在实际使用中通常使用 Java 类库中提供的它们的一系列子类。下面我们以 InputStream 类为例,来介绍下 Java 中的字节流。

InputStream 类中定义了一个基本的用于从字节流中读取字节的方法 read,这个方法的定义如下:

public abstract int read() throws IOException;

这是一个抽象方法,也就是说任何派生自 InputStream 的输入字节流类都需要实现这一方法,这一方法的功能是从字节流中读取一个字节,若到了末尾则返回 - 1,否则返回读入的字节。关于这个方法我们需要注意的是,它会一直阻塞知道返回一个读取到的字节或是 - 1。另外,字节流在默认情况下是不支持缓存的,这意味着每调用一次 read 方法都会请求操作系统来读取一个字节,这往往会伴随着一次磁盘 IO,因此效率会比较低。有的小伙伴可能认为 InputStream 类中 read 的以字节数组为参数的重载方法,能够一次读入多个字节而不用频繁的进行磁盘 IO。那么究竟是不是这样呢?我们来看一下这个方法的源码:

public int read(byte b[]) throws IOException {
    return read(b, 0, b.length);
}
它调用了另一个版本的 read 重载方法,那我们就接着往下追:

     public int read(byte b[], int off, int len) throws IOException {
        if (b == null) {
            throw new NullPointerException();
        } else if (off < 0 || len < 0 || len > b.length - off) {
            throw new IndexOutOfBoundsException();
        } else if (len == 0) {
            return 0;
        }

        int c = read();
        if (c == -1) {
            return -1;
        }
        b[off] = (byte)c;

        int i = 1;
        try {
            for (; i < len ; i++) {
                c = read();
                if (c == -1) {
                    break;
                }
                b[off + i] = (byte)c;
            }
        } catch (IOException ee) {
        }
        return i;
    }

从以上的代码我们可以看到,实际上 read(byte[])方法内部也是通过循环调用 read()方法来实现 “一次” 读入一个字节数组的,因此本质来说这个方法也未使用内存缓冲区。要使用内存缓冲区以提高读取的效率,我们应该使用 BufferedInputStream。

3. 字符流

Java 中的字符流处理的最基本的单元是 Unicode 码元(大小 2 字节),它通常用来处理文本数据。所谓 Unicode 码元,也就是一个 Unicode 代码单元,范围是 0x0000~0xFFFF。在以上范围内的每个数字都与一个字符相对应,Java 中的 String 类型默认就把字符以 Unicode 规则编码而后存储在内存中。然而与存储在内存中不同,存储在磁盘上的数据通常有着各种各样的编码方式。使用不同的编码方式,相同的字符会有不同的二进制表示。实际上字符流是这样工作的:

  • 输出字符流:把要写入文件的字符序列(实际上是 Unicode 码元序列)转为指定编码方式下的字节序列,然后再写入到文件中;
  • 输入字符流:把要读取的字节序列按指定编码方式解码为相应字符序列(实际上是 Unicode 码元序列从)从而可以存在内存中。

我们通过一个 demo 来加深对这一过程的理解,示例代码如下:

import java.io.FileWriter;
import java.io.IOException;

public class FileWriterDemo {
    public static void main(String[] args) {
        FileWriter fileWriter = null;
        try {
            try {
                fileWriter = new FileWriter("demo.txt");
                fileWriter.write("demo");
            } finally {
                fileWriter.close();
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

以上代码中,我们使用 FileWriter 向 demo.txt 中写入了 “demo” 这四个字符,我们用十六进制编辑器 WinHex 查看下 demo.txt 的内容:

image

从上图可以看出,我们写入的 “demo” 被编码为了“64 65 6D 6F”,但是我们并没有在上面的代码中显式指定编码方式,实际上,在我们没有指定时使用的是操作系统的默认字符编码方式来对我们要写入的字符进行编码。

由于字符流在输出前实际上是要完成 Unicode 码元序列到相应编码方式的字节序列的转换,所以它会使用内存缓冲区来存放转换后得到的字节序列,等待都转换完毕再一同写入磁盘文件中。

4. 字符流与字节流的区别

经过以上的描述,我们可以知道字节流与字符流之间主要的区别体现在以下几个方面:

  • 字节流操作的基本单元为字节;字符流操作的基本单元为 Unicode 码元。
  • 字节流通常用于处理二进制数据,实际上它可以处理任意类型的数据,但它不支持直接写入或读取 Unicode 码元;字符流通常处理文本数据,它支持写入及读取 Unicode 码元。

5. 缓冲流

缓冲流是处理流的一种, 它依赖于原始的输入输出流, 它令输入输出流具有1个缓冲区, 显著减少与外部设备的IO次数, 而且提供一些额外的方法.

可见, 缓冲流最大的特点就是具有1个缓冲区! 而我们使用缓冲流无非两个目的:

  1. 减少IO次数(提升performance)
  2. 使用一些缓冲流的额外的方法.

缓冲字节流:BufferedInputStreamBufferedOutputStream
缓冲字符流:BufferedReaderBufferedWriter

5. 字节流中文乱码问题

在 Java 中 不同编码方式中文所占字节数不同,详见 https://www.cnblogs.com/lslk89/p/6898526.html

例如:"abc中国"

当我们以每四个字节读取文件时,此时会读到 "abc" + "中"的首字节,此时就会产生乱码。

byte[] b = new btye[4];
inputStream.read(b);//出现乱码

又如如下代码,读取字节流

  /*
 * 读取文件字节流
 */
 
public String readerFile(File f) {
    String str = "";
    FileInputStream fis = null;
    try {
        fis = new FileInputStream(f);
        byte[] b = new byte[512];
        int n;
        while ((n = fis.read(b)) != -1) {
            str = str + new String(b, 0, n);
            b = new byte[512];
        }
    } catch (Exception e) {
        e.printStackTrace();
    } finally {
        try {
            fis.close();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
    return str;
}

这个方法就是通过传进的File对象,读取里面的内容,返回一个字符串,如果你把这方法copy去读取含有中文的文件,无意外的话就会出现中文乱码,如果出现中文乱码,该如何解决呢?其实String类有提供方法解决,只要把str = str + new String(b, 0, n);改成str = str + new String(b, 0, n, "gbk");就可以解决了。

上面的方法在一般情况下是可以读取中文了,但是,仔细想想,毕竟上面的方法是以字节为单位的,而一个中文占多个字节,细心的同学应该已经想到了,上面的方法是一次读取512个字节,如果,一个中文刚好就占了第512个字节和第513个字节,你一次读512个字节,狠狠得把他们拆散了,重新new了一个新的字符串,你说乱码不乱码?

解决办法

  1. 判断读取的字节是否是中文字节,这种方式比较麻烦
  2. 将字节流转换成字符流,并指定编码,详见下文

6. 字节流和字符流的选择

操作对象

  • 字符流操作对象
    1. 纯文本
      2.需要查指定的编码表,默认为GBK
  • 字节流操作对象
    1. 图像,音频等多媒体文件
    2. 无需查询指定编码表

如何选择合适的流

  1. 先明确源头和目的:源头使用的是输入流,InputStream或者Reader。目的使用的是输出流,OutputStream或者Writer
  2. 确定操作的对象是那些:纯文本用字符流,否则用字节流
  3. 当明确后,再确定使用哪一个具体的对象:内存,硬盘(比如操作文件的话用FileWriter/FileReader,或者FileInputStream/FileOutputStream),控制台(System)

7. 字节流和字符流的相互转换

从字符流到字节流

可以从字符流中获取char[]数组,转换为String,然后调用String的API函数getBytes() 获取到byte[],然后就可以通过ByteArrayInputStream、ByteArrayOutputStream来实现到字节流的转换。

函数:new String(byte[] data, String encoding);

这个方法通常与String.getBytes(String encoding)一起使用.

用法:tring str = new String(formMsg.getBytes("ISO-8859-1"),"utf-8");

从字节流到字符流

如下,是一个字节流上传文件到 hadoop hdfs 的工具方法。此处为了避免中文乱码的,将字节流指定编码转换为字符流,然后再用 getBytes("UTF-8") 方法获取相应编码的字节,实现字节流输出。

/**
     * 文件流上传文件
     *
     * @param iStream 输入流
     * @param pathStr HDFS 路径  'test/out/' 最后要有 /
     * @param fileName 文件名
     * @return
     */
    public static boolean upLoadFileToHdfs(InputStream iStream, String pathStr, String fileName) {
        //FileSystem fs = FileSystem.get(conf);
        Path path = new Path(pathStr + fileName);
        //FSDataOutputStream outputStream = fs.create(path);
        FileSystem fs = null;
        FSDataOutputStream outputStream = null;
        //InputStreamReader是字节流和字符流之间的桥梁,转化时需要指定字符集,否则按照系统字符集转换
        InputStreamReader reader = null;
        BufferedReader br = null;

        try {
            reader = new InputStreamReader(iStream,"UTF-8");
            //创建缓冲字符输入流
            br = new BufferedReader(reader);
            fs = FileSystem.get(conf);
            outputStream = fs.create(path);
            String line;
            while ((line = br.readLine()) != null) {
                outputStream.write(line.getBytes("UTF-8"));
                outputStream.write("\r\n".getBytes("UTF-8"));
            }
            //IOUtils.copyBytes(, outputStream, 4096);
            return true;
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            try {
                outputStream.hsync();
                outputStream.close();
                br.close();
                reader.close();
                iStream.close();
                fs.close();
            } catch (IOException e) {
                e.printStackTrace();
            }

        }
        return false;

    }

8. Java如何获取文件编码格式

若我们想知道一个文件的编码格式,我们可以使用 cpdetector 这个开源的jar包可以自动判断当前文件的内容编码,从而在读取的时候选择正确的编码读取,避免乱码问题。
地址: http://cpdetector.sourceforge.net/

使用方法可以参照博客:

参考

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

推荐阅读更多精彩内容

  • tags:io categories:总结 date: 2017-03-28 22:49:50 不仅仅在JAVA领...
    行径行阅读 2,178评论 0 3
  • 1. Java基础部分 基础部分的顺序:基本语法,类相关的语法,内部类的语法,继承相关的语法,异常的语法,线程的语...
    子非鱼_t_阅读 31,631评论 18 399
  • 引言 概念:"流"是一组有顺序的,有起点,有终点的字节集合.是对数据传输的总称或抽象概念."流"它既是数据在...
    batesli阅读 442评论 0 0
  • 见卿不念卿,请卿入梦来。 君不见卿来,夜夜把梦还。 遥遥星汉梦,不知谁与共? 晨钟惊人醒,原是鹊桥仙。
    聆听声音的人阅读 322评论 2 1
  • 一直听周围朋友说到巴厘岛,正好今年孩子小学毕业,特地和老同学各自带俩小子去了一趟慕名已久的巴厘岛! 开始入关还...
    淡定的吃货阅读 356评论 0 0