教你如何理解JAVA的I/O类库
花括号MC(huakuohao-mc):关注JAVA基础编程及大数据,注重经验分享及个人成长。
Java
的 I/O
流,说简单也简单,说复杂也复杂。复杂是因为进行一次常规的文件 I/O
操作通常要用到很多 I/O
基础类,这很容易让新手产生困惑。简单是因为它的设计思想其实很简单。只要理解了它的设计思想就很容易知道如何使用它们。
Java
的 I/O
类库最基本的思想就是流的概念。你可以把它类比成水流,我们把数据从一个地方搬运到另一个地方就是一次 I/O
操作。比如读取文件 'A' 的内容,存入文件 'B' 就是一次文件 I/O
操作; 通过监听键盘输入,然后将内容存储到磁盘上也是一次 I/O
操作; 当然通过网络带宽把服务器 'A' 的内容发送到 'B' 服务也叫做一次 I/O
操作。
Java
的 I/O
流分为 输入流 和 输出流 。跟读相关的都叫输入流,跟写相关的都叫输出流。 Java
的 I/O
不光分为输入流跟输出流,按照流的性质还分为 字节流 和 字符流 。所有继承自抽象类 InputStream
和 OutPutStream
的流都是字节流,所有继承自抽象类 Reader
和 Writer
的流都是字符流。这两种不同性质的流都能进行 I/O
操作。
为什么要分为字节流跟字符流。
所谓的字节流,就是里面流淌的都是字节,也就 byte
。字符流呢,里面流淌的就是字符,也就是 char
。字符是由字节通过特定的编码组成的,比如 GBK
编码, ASCII
编码等等。同样的字节,使用不同的编码得到的内容可能不一样,这也就是编程中经常遇到的乱码问题。关于字符编码问题,这里不做深入展开,后续有机会可以单独写一下。
JDK最开始支持的都是字节流,因为计算机就认字节。但是计算机是为人服务的,我们更喜欢看字符,比如‘A’,‘B’,‘C’,‘中’,‘国’,这些都叫字符。如果我们每次按字节 I/O
读取内容,然后在展示出来,是需要做编码转化的,所以为了方便程序员使用,JDK增加了字符流的相关类库,JDK底层帮我们完成转换工作。
新的字符类库继承体系相比于老的字节流继承体系,有如下增强,支持国际化也就是支持16位的 Unicode
字符,老的I/O流仅支持8位的的字节流,而且在性能上也会有一定提升。在进行 I/O
操作的时候,应该优先使用字符流,当字符流不能满足我们的时候,才转化到字节流,比如进行GZIP操作的时候,就需要使用字节流。
字节流I/O
先来认识一下字节流的 I/O
类库。
所有的跟输入相关的字节流都继承自抽象类 InputStream
。它有几个比较常用的直接子类,每个直接子类代表了一个数据源,比如 ByteArrayInputStream
表示将内存缓冲区中的字节数组当作输入源。 StringBufferInputStream
表示将 String
对象作为输入源,现在已经不推荐使用了,推荐使用 StringReader
读取 String
对象。后面介绍字符流的时候还会说到这个类。 FileInputStream
表示将文件作为数据源,这也是大家平时用的比较多的。
这些类的继承关系大概是这个样子
其中标红的直接子类 FilterInputStream
比较特殊。要想理解这个类存在的意义,需要对 装饰者模式 有一定了解。
俗话说的好,光说不练假把式,下面来看个小例子,看看如何进行字节流的文件 I/O
操作。
1try {
2 //打开输入流
3 InputStream in = new FileInputStream(new File("test-io.tmp"));
4 //打开输入流
5 OutputStream out = new FileOutputStream(new File("test-io.md"));
6 int length;
7 byte[] buffer = new byte[1024];
8 //输入流不断的读取,每次读取1024字节,直到文件末尾。
9 while ((length = in.read(buffer)) != -1) {
10 out.write(buffer,0,length);
11 }
12 in.close();
13 out.close();
14 } catch (FileNotFoundException e) {
15 e.printStackTrace();
16 } catch (IOException e) {
17 e.printStackTrace();
18 }
在真正的项目中很少看到这样的 I/O
操作,因为效率比较低。都2020年了,要是还进行非缓冲的 I/O
操作,估计会被同行狠狠的鄙视。我们针对上面的例子做一个小小的改动,使其变成带缓冲功能的 I/O
操作。主要的类就是 BufferedInputStream
.
看代码
1try {
2 InputStream in = new BufferedInputStream(new FileInputStream(new File("test-io.tmp")));
3 OutputStream out = new BufferedOutputStream(new FileOutputStream(new File("test-buffer-io.md")));
4 int lenght;
5 byte[] buffer = new byte[1024];
6 while ((lenght = in.read(buffer)) != -1){
7 out.write(buffer,0,lenght);
8 }
9 in.close();
10 out.close();
11 } catch (FileNotFoundException e) {
12 e.printStackTrace();
13 } catch (IOException e) {
14 e.printStackTrace();
15 }
装饰者模式的应用
上面这段带缓存功能的 I/O
操作跟不带缓存的 I/O
操作唯一的区别就是把 FileInputStream
用 BufferedInputStream
给包装(装饰)了一下。下面看一下 BufferedInputStream
的继承关系。
FileterInputStream
:JDK官方文档是这样介绍这个类的。
把一个真正的带有数据源的输入流包装进来,并为其提供格外的功能,这个额外的功能要其子类去实现。
这是典型的装饰者模式。
大家可能会问,为什么不能直接让 BufferedInputStream
继承自 FileInputStream
,而偏偏引入个装饰者模式呢。
我们假设让 BufferedInputStream
继承自 FileInputStream
,那么当需要对 ByteArrayInputStream
进行缓存读取的时候自然也需要在实现一个 ByteBufferedInputStram
,这样类库就会变得异常复杂跟臃肿。这也是装饰者模式比继承的方式更灵活的地方。如果你还是一头雾水的话,建议去深入的理解一下装饰者模式吧。
字节流的 I/O
操作基本就这样了,下面看一下字符流。
字符流I/O
所有跟输入相关的字符流都继承自抽象类 Reader
,跟输出相关的都继承自抽象类 Writer
。
我们先看个小例子,假设不使用字符流而是使用字节流读取文件内容,然后将内容输出到控制台,看看应该怎样操作。
1public static void readFileStreamToChar(){
2 /**
3 * UTF-8编码英文字符占一个字节,
4 * 数字占一个字节,
5 * 有的汉字占三个字节,有的占用四个字节。
6 * 拉丁文,希腊文占两个字节。
7 */
8
9 try {
10 InputStream in = new FileInputStream(new File("test-io.tmp"));
11 int length;
12 byte[] buffer = new byte[4];
13 while ((length = in.read(buffer)) != -1) {
14 System.out.println("the length of bytes is " + length);
15 //编码转化
16 String content = new String(buffer, 0, length, Charset.forName("utf-8"));
17 System.out.println("the content is " + content);
18 }
19 in.close();
20 } catch (FileNotFoundException e) {
21 e.printStackTrace();
22 } catch (IOException e) {
23 e.printStackTrace();
24 }
25 }
输出结果
1the length of bytes is 4
2the content is word
3the length of bytes is 4
4the content is
5世
6the length of bytes is 3
7the content is 界
说明: test-io.tmp
里面的内容为两行,第一行内容是'word' (没拼错,故意找的这个单词),第二行内容是'世界'
从代码中可以看到,将内容输出到控制台的时候必须进行编码转换。大家可以尝试把 buffer
的大小调整为2个字节看一下效果。懒得尝试的同学,就别试了,结论是中文字符会乱码,原因我已经在代码注释中说了,一个中文字符占三个字节。
假设JDK只提供基于字节流操作,那么每次都要进行字节到字符的转换操作,很麻烦,所以JDK就直接提供了字符流的操作。
下面来具体看看字符流类库的设计,我们还是以输入流举例,因为输出流跟输入流是一一对应关系。
抽象类 Reader
同样有几个比较常用的直接子类。
BufferedReader
:带缓冲功能的字符流,用的比较多。
CharArrayReader
:跟 ByteArrayInputStream
对应,读取内存中的字符数组。
StringReader
:跟 StringBufferInputStream
对应,读取 String
对象,这是读取 String
对象的推荐用法。
Reader
有个比较特殊的直接子类,就是 InputStreamReader
。这是个适配器,可以把字节流转换为字符流。 FileReader
就继承自该类, FileReader
跟字节流的 FileInputStream
对应,表示从文件读取字符。
下面来看简单看一下类的继承关系。
了解完类的继承关系之后,我们看个小例子,看看如何使用字符流的方式读取文件内容。注意我注释掉的代码。
1try {
2
3 //通过 InputStreamReader 适配器,将字节流FileInputStream按照制定的编码方式转换为字符流
4 // BufferedReader in = new BufferedReader(
5 // new InputStreamReader(
6 // new FileInputStream(
7 // new File("test-io.tmp")), "utf-8"));
8
9 //不直接使用适配器,使用FileReader,将使用系统默认的编码
10 BufferedReader in = new BufferedReader(new FileReader(new File("test-io.tmp")));
11 String s;
12 StringBuilder sb = new StringBuilder();
13
14 //按字符读
15 int value;
16 while ((value = in.read()) != -1) {
17 char c = (char) value;
18 System.out.println(c);
19 }
20
21 //按行读取
22 //while ((s = in.readLine()) != null){
23 // sb.append(s);
24 //}
25 //System.out.println(sb.toString());
26 in.close();
27 } catch (FileNotFoundException e) {
28 e.printStackTrace();
29 } catch (IOException e) {
30 e.printStackTrace();
31 }
32}
Java的普通 I/O
就介绍到这里了。Java的 I/O
类库用到了两个经典设计模式,装饰器跟适配器。如果想更深入的理解Java I/O
类库除了多读源码外(感兴趣的读者可以去看看底层源码,JDK如何实现字节到字符的转换的)还需要深入的理解设计模式。
希望这篇文章对你理解Java I/O
有一定帮助,下一篇打算写一下Java的NIO。
教你如何理解JAVA的I/O类库的更多相关文章
- [转]深入理解Java 8 Lambda(类库篇——Streams API,Collectors和并行)
以下内容转自: 作者:Lucida 微博:@peng_gong 豆瓣:@figure9 原文链接:http://zh.lucida.me/blog/java-8-lambdas-insideout-l ...
- 深入理解Java 8 Lambda(类库篇——Streams API,Collectors和并行)
转载:http://zh.lucida.me/blog/java-8-lambdas-inside-out-library-features/ 关于 深入理解 Java 8 Lambda(语言篇——l ...
- 深入理解Java 8 Lambda(类库篇)
背景(Background) 自从lambda表达式成为Java语言的一部分之后,Java集合(Collections)API就面临着大幅变化.而 JSR 355(规定了 Java lambda 表达 ...
- 深入理解Java 8 Lambda(语言篇——lambda,方法引用,目标类型和默认方法)
作者:Lucida 微博:@peng_gong 豆瓣:@figure9 原文链接:http://zh.lucida.me/blog/java-8-lambdas-insideout-language- ...
- [转载]深入理解Java 8 Lambda
原文链接:http://zh.lucida.me/blog/java-8-lambdas-insideout-language-features/ 深入理解Java 8 Lambda(语言篇——lam ...
- 万字长文深入理解java中的集合-附PDF下载
目录 1. 前言 2. List 2.1 fail-safe fail-fast知多少 2.1.1 Fail-fast Iterator 2.1.2 Fail-fast 的原理 2.1.3 Fail- ...
- 《深入理解Java虚拟机》虚拟机性能监控与故障处理工具
上节学习回顾 从课本章节划分,<垃圾收集器>和<内存分配策略>这两篇随笔同属一章节,主要是从理论+实验的手段来讲解JVM的内存处理机制.好让我们对JVM运行机制有一个良好的概念 ...
- java笔记--理解java类加载器以及ClassLoader类
类加载器概述: java类的加载是由虚拟机来完成的,虚拟机把描述类的Class文件加载到内存,并对数据进行校验,解析和初始化,最终形成能被java虚拟机直接使用的java类型,这就是虚拟机的类加载机制 ...
- 《深入理解Java内存模型》读书总结
概要 文章是<深入理解Java内容模型>读书笔记,该书总共包括了3部分的知识. 第1部分,基本概念 包括"并发.同步.主内存.本地内存.重排序.内存屏障.happens befo ...
随机推荐
- map实现单词转换程序的例子
代码来源于c++ primer 10.3 功能:已知一个一一对应的词典,求一小段文档对应的“翻译” 词典如下: A a B b C c D d E e 输入: D D E 代码: //需要两个文件,一 ...
- 模块化系列教程 | 阿里JarsLink1.0模块化场景实战Demo
场景实战Demo使用指引 Quickstart 快速开始 Demo说明 模块说明 使用说明 情景一环境部署 工作原理 情景二环境部署 工作原理 场景实战Demo使用指引 个人之前学习过程中对JarsL ...
- idea个人配置记录
idea.properties # Use ${idea.home.path} macro to specify location relative to IDE installation home. ...
- mod3 如何用硬件实现
今天接到Qualcom 的电话面试,表现很一般.Qualcom 不愧是一流的IC 设计公司,问得问题非常基础,但是非常深入! 其中问了一个如何实现模3 的问题.没有回答上来. 后来想了一下,其实非常简 ...
- python dict 中的中文处理
dict1 = {'中':'国 '} print dict1 ##{'\xc3\xa4\xc2\xb8\xc2\xad': '\xc3\xa5\xc2\x9b\xc2\xbd'} import jso ...
- HTML5与HTML4的区别-----文档结构
HTML5在结构和语法上做了大量的简化.当然,也提供了语义化的标签 结构上区别: 1.简化了文档声明语句 HTML5仅规定了一种: <!DOCTYPE html> 2. ...
- ESXi以及WorkStation缩减thin provision模式Linux虚拟机磁盘的方法
1. 公司的服务器采用ESXi 进行管理. 有时候为了灵活性,需要将虚拟机从ESXi服务器上面导出来. 放置到不同的客户机器上面去. 2. 但是发现,比如我在linux里面安装了Oracle数据库, ...
- 全网最全小白搭建Hexo+Gitee/Coding
全网最全小白搭建Hexo+Gitee/Coding 本站内容已全部转移到https://www.myyuns.ltd,具体请移步到www.myyuns.ltd查看
- Pycrypto与RSA密码技术
密码与通信 密码技术是一门历史悠久的技术.信息传播离不开加密与解密.密码技术的用途主要源于两个方面,加密/解密和签名/验签. pip install pycrypto RSA 密码算法与 ...
- CentOS7时区和时间设置
[root@saltstack-master ~]# timedatectl set-timezone Asia/Shanghai [root@saltstack-master ~]# ln -sf ...