本文对 Java 中的 IO 流的概念和操作进行了梳理总结,并给出了对中文乱码问题的解决方法。
1. 什么是流
Java 中的流是对字节序列的抽象,我们可以想象有一个水管,只不过现在流动在水管中的不再是水,而是字节序列。和水流一样,Java 中的流也具有一个 “流动的方向”,通常可以从中读入一个字节序列的对象被称为输入流;能够向其写入一个字节序列的对象被称为输出流。
以下是 IO 相关类的总结图
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 的内容:
从上图可以看出,我们写入的 “demo” 被编码为了“64 65 6D 6F”,但是我们并没有在上面的代码中显式指定编码方式,实际上,在我们没有指定时使用的是操作系统的默认字符编码方式来对我们要写入的字符进行编码。
由于字符流在输出前实际上是要完成 Unicode 码元序列到相应编码方式的字节序列的转换,所以它会使用内存缓冲区来存放转换后得到的字节序列,等待都转换完毕再一同写入磁盘文件中。
4. 字符流与字节流的区别
经过以上的描述,我们可以知道字节流与字符流之间主要的区别体现在以下几个方面:
- 字节流操作的基本单元为字节;字符流操作的基本单元为 Unicode 码元。
- 字节流通常用于处理二进制数据,实际上它可以处理任意类型的数据,但它不支持直接写入或读取 Unicode 码元;字符流通常处理文本数据,它支持写入及读取 Unicode 码元。
5. 缓冲流
缓冲流是处理流的一种, 它依赖于原始的输入输出流, 它令输入输出流具有1个缓冲区, 显著减少与外部设备的IO次数, 而且提供一些额外的方法.
可见, 缓冲流最大的特点就是具有1个缓冲区! 而我们使用缓冲流无非两个目的:
1. 减少IO次数(提升performance)
2. 使用一些缓冲流的额外的方法.
缓冲字节流:BufferedInputStream
,BufferedOutputStream
缓冲字符流:BufferedReader
,BufferedWriter
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了一个新的字符串,你说乱码不乱码?
解决办法
- 判断读取的字节是否是中文字节,这种方式比较麻烦
- 将字节流转换成字符流,并指定编码,详见下文
6. 字节流和字符流的选择
操作对象
- 字符流操作对象
- 纯文本
2.需要查指定的编码表,默认为GBK
- 纯文本
- 字节流操作对象
- 图像,音频等多媒体文件
- 无需查询指定编码表
如何选择合适的流
- 先明确源头和目的:源头使用的是输入流,InputStream或者Reader。目的使用的是输出流,OutputStream或者Writer
- 确定操作的对象是那些:纯文本用字符流,否则用字节流
- 当明确后,再确定使用哪一个具体的对象:内存,硬盘(比如操作文件的话用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/
使用方法可以参照博客:
- Java如何获取文件编码格式
- 如何使用Java代码获取文件、文件流或字符串的编码方式