Java IO学习小结

IO操作算是java的一个基本知识点了,比如我们常见的网络IO,文件读写等,而且这一块基本上大家并不会频繁的来操作,大多会用一些封装得好用的工具来代替,某些时候真的需要做的时候,基本上也很难一下子很顺利的写完

本篇将主要集中在:

  • 几种IO分类
  • 字节IO和字符IO的转换
  • 装饰类IO是什么
  • 序列化的实现机制

I. IO分类

Java中的IO操作,一般都是基于流进行,以输入输出流进行分类可以分为

  • 字节流:InputStream, OutputStream

  • 字符流:Reader, Writer

从数据源进行分类,又可以区分为:

  • 文件读写: FileInputStream, FileOutputStream,FileReader
  • 字符串流: StringBufferInputStream, StringReader
  • 数组流: ByteArrayInputStream
  • 网络: Socket

从去向分析,就是输入流和输出流:

  • 输入流: xxxInputStream, xxxReader
  • 输出流: xxxOutputStream, xxxWriter

II. IO流的基本知识点

IO操作,最主要的一点就是需要清晰如何使用了,一般来讲,网络或文件读写,都是基于字节进行交互的,但实际上为了能友好的读取或写入信息,一般都是字符方式,由字符到字节之间则需要一个编码规则的映射

所有,很简单就可以知晓,这里至少有三种不同应用场景的类和一种设计模式

  • 字节流
  • 字符流
  • 字节映射字符流 (适配器模式)

1. 基本使用

以常见的文件读写为例进行说明,一般的读写操作是啥样的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@Test
public void testPrintFile() throws IOException {
String fileName = "/tmp/test.d";
File file = new File(fileName);
InputStream input = new FileInputStream(file);
InputStreamReader reader = new InputStreamReader(input, Charset.forName("utf-8"));
BufferedReader bufferedReader = new BufferedReader(reader);

String ans = bufferedReader.readLine();
while (ans !=null ) {
System.out.println(ans);
ans = bufferedReader.readLine();
}

bufferedReader.close();
reader.close();
input.close();
}

上面的流程基本上就下面五步:

  • 创建一个File对象
  • 包装为IO流: new FileInputStream(new File("test.d"))
  • 字节流转换为字符流: new InputStreamReader(input, Charset.forName("utf-8"))
  • 字符流使用缓冲修饰,支持读一行
  • 关闭流

基本上上面这个套路是比较适合常见的IO操作的,那么为什么是这么个流程呢?这个流程中又有些什么有意思的东西呢?

2. IO流使用姿势分析

声明:下面这一段纯属个人理解,如有不误,请不吝指正

对操作系统而言(网络传输也一样),他们关心的是一个字节一个字节的行为,所以与它们打交道,就需要遵循他们的规则来办事,使用字节来操作,所以最开始我们都是采用字节流来定义与数据源的交互规则

然而字节流虽好,但是所有的数据最终都是为人服务的,而由于客观原因,不同的国家有不同的语言,为了面向人类的友好,出现了字符这个东西,所以一般我们的操作也更多的是基于字符进行操作

上面这两个出现之后,一个自然而然的东西–>InputStreamReader就出现了,作为字节和字符转换的桥梁

上面这三个可算是一个基本的操作流程了,可以满足我们的输入输出需求,但依然不是特别友好,比如一个一个字符的操作不友好啊,比如我希望过滤某些东西,或者做其他的一些辅助操作之类的,因此就出现了各种装饰流,主要就是提供一些服务方法,增强接口的易用性

简单的使用姿势流图:

180318_IO1.png

1
2
3
4
5
6
graph TD
A(数据源)-->B(字节流InputStream/OutputStream)
B-->C[InputStreamReader/OutputStreamWriter]
C-->D[Reader/Writer]
D-->E[装饰流]
E-->F(关闭)

3. 常见IO类

a. 基本介质流

与提供读写数据的数据源打交道的流

  • FileInputStream : 数据源为文件
  • ByteArrayInputStream: 数据源为byte数组
  • StringBufferInputStream: StringBuffer作为数据源,已废弃
  • PipedInputStream:管道,多线程协作使用

使用姿势

1
2
3
4
5
6
7
8
9
10
11
12
// 数组
byte[] bytes = new byte[]{'a', 'b', 'c', '1', '2'};
ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(bytes);
int a = 0;
while((a = byteArrayInputStream.read()) != -1) {
System.out.print(a + " ");
}
byteArrayInputStream.close();
// 输出: 97 98 99 49 50


// StringBufferInputStream 已经被废弃

b. 字节字符转换

两个:

  • InputStreamReader
  • OutputStreamWriter

使用姿势也比较简单,标准的适配器模式,用构造方法传参即可,下面给出一个demo,结合上面的,实现将数组流的数据写入的文件(说明,下面的实现更多的是为了演示这种用法,实际编码中有较大的优化空间)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
byte[] bytes = new byte[]{'a', 'b', 'c', '1', '2'};
ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(bytes);

File file = new File("/tmp/test.d");
OutputStream out = new FileOutputStream(file);
Writer writer = new OutputStreamWriter(out);

int a = 0;
while((a = byteArrayInputStream.read()) != -1) {
writer.write(a);
}

writer.flush();
writer.close();
byteArrayInputStream.close();

c. 装饰流

主要是基于装饰模式,对IO流,增强一些操作方法,最明显的是特征是他们继承自 FilterInputStream,比如我们最常用的BufferedInputStreamDataInputStream

  • 基本数据类型读写流: DataInputStream
    • 8中基本类型的读入写出流,没什么好说的
  • 缓存流: BufferedInputStream
    • 为了提升性能引入,避免频繁的磁盘读写
  • 对象流: ObjectInputStream
    • 虽然没有继承自FilterInputStream,依然把它作为装饰流,后面单独说

这里面有必要好好的说到以下 BufferedInputStream,缓冲流的底层原理是什么,又有什么好处,为什么会引入这个?

API解释:在创建 BufferedInputStream时,会创建一个内部缓冲区数组。在读取流中的字节时,可根据需要从包含的输入流再次填充该内部缓冲区,一次填充多个字节。

也就是说,Buffered类初始化时会创建一个较大的byte数组,一次性从底层输入流中读取多个字节来填充byte数组,当程序读取一个或多个字节时,可直接从byte数组中获取,当内存中的byte读取完后,会再次用底层输入流填充缓冲区数组。

以文件读写为例,在实际的落盘和读取过程,这个是加锁阻塞的操作,如果每次都只读1个字节,在大量的读写情况下,这个性能就很脆了

加上这个缓冲之后呢?在一次加锁的过程中,尽量多的读取数据,放在本地内存,这样即便是在使用的地方一个一个的获取,也不会与其他的任务产生竞争,所有有效的提高了效率

III. 序列化

在平常的工作中,基本上离不开序列化了,比如web应用中,最常见的基于JSON的数据结构交互,就算是一种JSON序列化方式;当然我们这里谈的序列化主要是JDK原生的方式

1. 背景

出现序列化需求的背景比较清晰,我们希望某些对象可以更方便的共享,如即便程序over了,它们可以以某种方式存在(比如写在一个临时文件中),如RPC中传参和返回等

使用时注意事项

  • 一个类需要序列化,需要实现 Serializable 这个空接口,会告知编译器对它进行特殊处理
  • 一个友好的习惯是,在可序列化的类中,定义一个 static final long serialVersionUID
  • transient变量可标识出来不被序列化的字段

2. 实现

给出一个使用case

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
public static class Demo implements Serializable {

private String name;
private Integer age;
private boolean isBoy;
private transient String ignore;

public Demo(String name, Integer age, boolean isBoy, String ignore) {
this.name = name;
this.age = age;
this.isBoy = isBoy;
this.ignore = ignore;
}

@Override
public String toString() {
return "Demo{" +
"name='" + name + '\'' +
", age=" + age +
", isBoy=" + isBoy +
", ignore='" + ignore + '\'' +
'}';
}
}

@Test
public void testObjStream() throws IOException, ClassNotFoundException {
Demo demo = new Demo("测试", 123, true, "忽略的一段文本");

// 将对象写入到文件中
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("/tmp/out.t"));
oos.writeObject(demo);
oos.flush();
oos.close();

System.out.println("-------- over ----------");
// 从文件读取
ObjectInputStream ois = new ObjectInputStream(new FileInputStream("/tmp/out.t"));
Object obj = ois.readObject();
System.out.println("反序列化: " + obj + " \n类型:" + obj.getClass().getSimpleName());
}

输出结果

1
2
3
-------- over ----------
反序列化: Demo{name='测试', age=123, isBoy=true, ignore='null'}
类型:Demo

对应的文本内容

180318_IO2.jpg

IV. 小结

io可以说是java中最基本的操作方式了,jdk本身设计是比较优雅的,从上面简单的学习就看到了两种设计模式:适配器+装饰器

提到IO,就不能跳过NIO,特别是在实际的工作中,用得非常多的网络交互,现在基本上是Netty占据主流,这个里面又是有不少东西可以学习的,放在下一篇,下面简单回顾下IO流的认知与使用

1. 流分类:

  • 字节流: InputStream , OutputStream
  • 字符流: Reader, Writer

2. 从设计角度分类

  • 介质流:直接与数据源打交道 FileInputStream, StringBufferInputStream(已经不用,改StringBufferReader), ByteArrayInputStream (网络传输的二进制流,基本就是这个)
  • 转换: 字节流和字符流的转换 InputStreamReader, OutputStreamWriter
  • 装饰流: BufferedInputStream, DataInputStream, ObjectInputStream

3. 序列化和反序列化

序列化是指将对象输出为二进制流的过程,反序列化则是指将二进制流反序列化为对象的过程

一般序列化的对象需要实现Serializable接口,内部不需要序列化的对象,用transient关键字进行声明

4. IO基本使用姿势

介质流与数据源进行交互 –> 转换流包装为字符流 –> 装饰流进行实际操作 –> 关闭流

以文件读写为例:

1
2
3
4
5
BufferedReader reader = new BufferedReader(new InputStreamReader(new FileInputStream("test.txt"), Chaset.forName("utf-8")));

String ans = reader.readLine();

reader.close();

IV. 其他

参考:

个人博客: Z+|blog

基于hexo + github pages搭建的个人博客,记录所有学习和工作中的博文,欢迎大家前去逛逛

声明

尽信书则不如,已上内容,纯属一家之言,因本人能力一般,见识有限,如发现bug或者有更好的建议,随时欢迎批评指正

扫描关注

QrCode