Java学习之IO相关

文章目录
  1. Java IO学习小结
    1. I. IO分类
    2. II. IO流的基本知识点
      1. 1. 基本使用
      2. 2. IO流使用姿势分析
      3. 3. 常见IO类
        1. a. 基本介质流
        2. b. 字节字符转换
        3. c. 装饰流
    3. III. 序列化
      1. 1. 背景
      2. 2. 实现
    4. IV. 小结
      1. 1. 流分类:
      2. 2. 从设计角度分类
      3. 3. 序列化和反序列化
      4. 4. IO基本使用姿势
    5. IV. 其他
      1. 参考:
      2. 个人博客: Z+|blog
      3. 声明
      4. 扫描关注

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

# IO, Java

评论

Your browser is out-of-date!

Update your browser to view this website correctly. Update my browser now

×