java-IO流补坑

前言

之前学习Java的时候并没有学到io这一块,然后之后用到的时候都是随手网上找一段封装好的代码直接使用,有心去了解过一下,但是看到java.io包下那庞大的类,想想暂时先放下了;这次趁着春节疫情假期延长的时间来好好复习一下,最后发现还是蛮简单的。

各种语言的IO

这里我选取了C、Python和Java这三种语言来作为例子,看看它们三者之间的io之间的区别。

C语言

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#include <stdio.h>

int main() {
FILE *fp = NULL;
char buff[255];

fp = fopen("/tmp/read.txt", "r");
fscanf(fp, "%s", buff);
fgets(buff, 255, fp);
printf("%s", buff);
fclose(fp);

fp = fopen("/tmp/write.txt", "w+");
fprintf(fp, "This is testing for fprintf...\n");
fputs("This is testing for fputs...\n", fp);
fclose(fp);
}

C语言的操作很简单,首先由一个指针指向一个文件,然后使用特定的函数对其进行读写,最后对文件进行关闭即可。

python

1
2
3
4
5
6
7
8
9
#!/usr/bin/env python
# -*- coding:utf-8 -*-

if __name__ == '__main__':
with open("/tmp/read.txt", "r") as f:
print(f.read())

with open("/tmp/write.txt", "w") as f:
print(f.write("test info"))

显然Python更简单…..指定好文件,然后进行操作就行了。

Java

读取文件内容:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public static void main(String[] args) {
InputStream is = null;
File file = new File("/tmp/read.txt");
try {
is = new FileInputStream(file);
byte[] buf = new byte[1024];
int len;
while (-1 != (len = is.read(buf))) {
System.out.print(new String(buf, 0, len));
}
} catch (IOException e) {
e.printStackTrace();
} finally {
if (null!= is){
try {
is.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}

写出文件的内容:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public static void main(String[] args) {
OutputStream os = null;
File file = new File("/tmp/write.txt");
try {
os = new FileOutputStream(file);
os.write("测试信息".getBytes());
} catch (IOException e) {
e.printStackTrace();
} finally {
if(null!=os){
try {
os.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}

看上去比上面的两位都复杂一点,但是不要忘了,主要是因为Java加了不少的异常处理机制,使得代码更加健壮。

一点小总结

从写代码的效率来说,Python必然是最优选择,而C语言和Java在写代码方面我个人觉得差距不大(因为C这里没有异常处理机制)。但是对于初学者来说,可能还是C和Python这两种语言更符合大家的常理:打开文件,对文件进行处理,最后关闭文件。Java看上去似乎很奇怪,并不是直接对文件进行操作,而是用了Stream来进行处理。这里有个问题,为什么Java要这么做?emmmmm其实我到现在还是无法理解,我个人觉得一种可能的原因是,Java使用了虚拟机,导致它无法直接对文件进行操作,所以用到了流来间接进行处理。

###字符和字节

这里还是有必要来介绍一下这两者的区别。

一个字节(byte)是八个比特,所以其本质上,是一个数字,一个从0到255的数字。在Java中有一个基本的数据类型就是byte,但是由于Java不支持无符号数据类型,所以在Java中,byte这个数据类型所能表达的数字范围是-128~127(这里多嘴提一句,byte数据一般也用来表示ip地址,所以看到负数不要惊讶)下面的一张图很能说明问题:

image-20200217182247007

而字符,在Java中是用char来进行表示的,一共是2个字节。显然能够表示65536个字符。然后在《Thinking in java》这本书里,居然说char能够表示所有的中文字符。这句话显然是错误的,光是汉字就不止六万多字,更别说还需要包括别的语言的字符了。当然如果只是常用字符,确实char是完全能够表示的。

###编码和解码

定义

维基百科对其的定义是:

编码信息从一种形式格式转换为另一种形式的过程;解码则是编码的逆过程。

也就是说,其实两者是等价的。但是习惯上,我们把从字符到字节成为编码(encode),从字节到字符称为解码(decode)。

编码和解码其实特别简单,只要我们遵守编码和解码采用相同的字符集,就不会有问题。假设现在有这么一套编码,它会把编码成 0100 0001(十进制的65),把编码成0100 0010(十进制的66)到电脑中保存。然后如果程序读取的时候,它按照了ASCII进行解码,就会解读出AB,也就是”乱码”了——即你明明写了你好在文件里,但是程序读出来却成了另外的内容(AB)。

字符集

所以关键其实在于这个字符集的选取,字符集那就首先从ASCII讲起。上世纪六十年代,美国制定了一套字符编码,对英语字符与二进制位之间的关系,做了统一规定。这被称为 ASCII 码,一直沿用至今。而这套编码正好规定了128个字符,也就是说只用7个比特即可表示(0000 0000 - 0111 1111),而之前也提到Java中最高位代表的是符号位,所以ASCII用byte的正数表示刚刚好。

由于这套编码最多也只能由256个字符,对于中文这样的文字来说肯定是不够的,所以经过一系列的发展,有了Unicode这个字符集,囊括了世界上所有的文字(甚至连emoji都包括其中),这样相当于计算机王国有了自己的普通话标准,只要大家都使用这本大字典,就不会有问题。

看似问题是不是解决了呢?现在有了完全不会冲突的字符集,那我只需要按照字符集里每个字符所对应的二进制代码去操作,那不是完全不会有乱码了吗?没错,理论上完全没有问题。但是实际中并不能这么操作。因为为了能够表达所有的字符,其实Unicode对大部分字符的编码长度是2~4个字节(为了方便说明,我们姑且认为全是3个字节吧),而如果我仅仅为了传输一份纯英文的文档,或者说掺杂了少数中文的文档,那么开销会非常大。于是,我们就有了utf-8。这里需要注意的是:UTF-8 是 Unicode 的实现方式之一。而utf-8也是采用了所谓的变长编码方式以充分利用空间,具体实现过程这里不做赘述。所以如果无视硬盘空间和网络流量,那么其实unicode编码是最舒服最容易理解的(实际上可并没有这种编码方式哦)。

各种语言的编码和解码demo

C

根据你所定义的类型来决定。引自知乎:

char c[] = u8”I’m a UTF-8 string.”; // utf8编码 (假如是C++,那么C++11起为char,C++20起改为char8_t类型)

char16_t d[] = u”This is a UTF-16 string.” // UTF-16编码

char32_t e[] = U”This is a UTF-32 string.” // UTF-32编码

python
1
2
byte = "字符".encode("utf-8")
print(byte.decode("utf-8"))
java
1
2
3
String s = "中文";
byte[] bytes = s.getBytes(StandardCharsets.UTF_8);
System.out.println(new String(bytes, StandardCharsets.UTF_8));

总结

想要不乱码?很简单,编码和解码采用同一种编码方式即可。而往往实际中困扰我们的是,xxx编码格式兼容xxx编码格式,xxx编码格式部分兼容xxx编码格式。还有就是回到java的char这里,char是一个字符,我们如果使用数字来对char进行赋值,在Java内部是会发生解码的(从字节到字符),而我查阅了一下数据,java默认用的就是utf-8。可以通过下面的代码查看:

1
System.out.println("Default Charset=" + Charset.defaultCharset());

流处理

前面用了不少篇幅来讲述字符和字节、编码和解码相关的信息,这是因为这些其实在IO中还是很重要的(而且我之前也不是非常了解2333333),从这里正式介绍Java中的各种流。

InputStream和OutputStream

首先用Stream结尾的一定是字节流,而从它们俩的名字来也能一眼看出它们的作用。这里需要注意的是,这两个类几乎是所有处理流的父类,所以这里用多态非常容易。

InputStream可以读取单个字节,读到最后一个字节的时候会返回-1,由此来判断。当然更多的时候,是我们自己创建一个字节数组,然后读取字节到字节数组中,会返回实际读取到的字节数,来进行处理。

OutputStream需要注意的是,记得使用flush进行数据的强制写入硬盘(文件),否则可能会看不到结果。

FileInputStream和FileOutputStream

上面的两个的实现类而已,如果我们需要读取/写入文件,记得来操作这两个类。

Reader和Writer

专门用来操作字符的抽象类,会比字节流方便一丢丢。

FileReader和FileWriter

具体的实现类。

Buffered相关

装饰类,所以只需要把需要装饰的丢入即可。这里注意的是装饰之后新增了一个readLine()方法。

DataInputStream和DataOutputStream

主要是为了针对基本数据类型。

ObjectInputStream和ObjectOutputStream

序列化和反序列化操作对象。记得实现一个空接口。

Commons IO

实际一般使用这个第三方库。这里介绍下IDEA如何使用这个库:

  1. 自己下载好jar包。下载地址:http://commons.apache.org/proper/commons-io/ 解压一下。然后回到IDEA,使用File->Project structure,然后选择libraries,导入即可。
  2. 当然你可以让IDEA在maven上帮你下载,只需要在选择libraries的时候选择从maven下载即可(虽然这速度慢的可以)