- 掌握InputStream、OutputStream、Reader、Writer的继承体系。
- 掌握字节流(FileInputStream、DataInputStream、BufferedInputStream、FileOutputSteam、DataOutputStream、BufferedOutputStream)和 字符流(BufferedReader、InputStreamReader、FileReader、BufferedWriter、OutputStreamWriter、PrintWriter、FileWriter),并熟练运用。
- 掌握NIO实现原理及使用方法。
Java IO包括:
本文介绍的是Java IO的基础部分, 也叫作BIO.
Java IO 继承关系
java.io包中包含了一系列的Java IO相关的类和接口:

- File 文件类
- FileDescriptor 文件描述类
- InputStream 字节流输入抽象类
- OutputStream 字节流输出抽象类
- Reader 字符流输入抽象类
- Writer 字符流输出抽象类
- RandomAccessFile随机访问文件类 不是InputStream或者OutputStream继承层次的一部分,除了实现了DataInput和DataOutput接口(DataInputStream和DataOutputStream也实现这两个接口)外, 他和这两个继承层次没任何关系。甚至不使用InputStream和OutputStream类中的任何功能。完全是独立的类,RandomAccessFile拥有和别的IO类型本质完全不同的行为,可以在一个文件中向前和向后移动,因此重新编写了所有的方法。任何情况下都是相互独立的,直接从Object类派生而来。
- Console类,该类提供了用于读取密码的方法,可以禁止控制台回显并返回char数组,这两个特性对保证安全有作用,平时用的不多,了解就行。
- StreamTokenizer 类,这个类非常有用,它可以把输入流解析为标记(token), StreamTokenizer 并非派生自InputStream或者OutputStream,而是归类于io库中,因为StreamTokenizer只处理InputStream对象。
流,是编程语言的IO类库常用的概念, 所谓流是指任何有能力产出数据的数据源和任何有能力接收数据的接收端对象。”流”屏蔽了实际的IO设备中处理数据的细节。
Java中的流,根据处理数据类型的不同分为:字符流和字节流; 根据数据流向不同分为:输入流和输出流。可以通过类名的结尾来判断。
字节流
java中的压缩都是面向字节的 (java.util.zip)。
InputStream
字节输入流都是继承于InputStream并且以InputStream结尾,InputStream表示从不同的数据源产生输入的类,这些数据源包括字节数组、String对象(StringBufferInputStream已经不再使用,需要转换为字节数组来实现)、文件、“管道”、其他种类的流组成的序列及其他数据源。每种数据源都有相应的 InputStream 子类,FilterInputStream也属于一种 InputStream, 为装饰器类提供基类,其中,“装饰器”类可以把属性或有用的接口与输入流连接在一起。装饰器模式

- PipedInputStream 作为多线程中的数据源,将其与FilterInputStream对象相连以提供有用接口
FilterInputStream类定义了一些方法1
2
3
4
5
6
7
8
9
10
11
12public void mark(int readlimit)
在输入流中的当前位置上作标记。
reset 方法的后续调用将此流重新定位在最后标记的位置上,
以便后续读取操作重新读取相同的字节。
readlimit 参数告知此输入流在标记位置无效之前允许读取的字节数。
public void reset() throws IOException
将此流重新定位到对此输入流最后调用 mark 方法时的位置。
此方法只执行 in.reset()。
在需要提前读取一小部分数据以查看流中有什么的情况下,可以使用流的标记。
通过调用通用解析器常常最容易做到这一点。如果流属于通过解析处理的类型,那么解析起来就很容易。
如果流不属于那种类型,那么解析器应该在解析失败时抛出一个异常。
如果这发生在 readlimit 个字节内,那么它允许外部代码重置流,并尝试另一种解析器。- 除了DataInputStream以外的FilterInputStream类在内部修改了InputStream的行为方式,是否缓冲,是否记录读过的行,以及是否把单一字符推回输入流等。
OutputStream
接收端对象包括字节数组(但不是String)、文件或者管道。
字节输出流都是继承于OutputStream并以OutputStream结尾

字节输入流和输出流的对应关系

字符流
字节流仅支持8位字节流,不能很好的处理16位的Unicode字符,Reader和Writer提供了兼容Unicode与面向字符的I/O功能,可以方便的实现国际化。
Reader 字符输入流
读取字符序列的类都是继承于Reader并且以Reader结尾

Writer 字符输出流
写入字符流的类都是继承于Writer并且以Writer结尾

- FilterWriter形同虚设: BufferedWriter并不是FilterWriter的子类,尽管FilterWriter是抽象类。没有任何子类。
- CharArrayWriter、StringWriter 是两种基本的介质流,它们分别向Char 数组、String 中写入数据。PipedWriter 是向与其它线程共用的管道中写入数据
- PrintWriter 和 PrintStream 极其类似,功能和使用也非常相似。
- OutputStreamWriter 是OutputStream 到Writer 转换的桥梁
字符输入流与字符输出流的对应关系

字节流与字符流的对应关系


字节流与字符流的区别
把一片二进制数据数据逐一输出到某个设备中,或者从某个设备中逐一读取一片二进制数据,不管输入输出设备是什么,我们要用统一的方式来完成这些操作,用一种抽象的方式进行描述,这个抽象描述方式起名为IO流,对应的抽象类为OutputStream和InputStream ,不同的实现类就代表不同的输入和输出设备,它们都是针对字节进行操作的。
在应用中,经常要完全是字符的一段文本输出去或读进来,用字节流可以吗?计算机中的一切最终都是二进制的字节形式存在。对于“中国”这些字符,首先要得到其对应的字节,然后将字节写入到输出流。读取时,首先读到的是字节,可是我们要把它显示为字符,我们需要将字节转换成字符。由于这样的需求很广泛,人家专门提供了字符流的包装类。
底层设备永远只接受字节数据,有时候要写字符串到底层设备,需要将字符串转成字节再进行写入。字符流是字节流的包装,字符流则是直接接受字符串,它内部将串转成字节,再写入底层设备,这为我们向IO设别写入或读取字符串提供了一点点方便。字符向字节转换时,要注意编码的问题,因为字符串转成字节数组,其实是转成该字符的某种编码的字节形式,读取也是反之的道理。
字节流
FileInputStream 文件输入流
构造器只能接受File对象 File路径字符串或者 FileDescriptor对象
1 | int available() |
FileOutputSteam 文件输出流
文件输出流是用于将数据写入 File 或 FileDescriptor 的输出流。与字符流中的FileWriter对应.
BufferedInputStream 缓冲输入流
支持一次性读入多个字节
BufferedInputStream 为另一个输入流添加一些功能,即缓冲输入以及支持 mark 和 reset 方法的能力。
在创建 BufferedInputStream 时,会创建一个内部缓冲区数组。
在读取或跳过流中的字节时,可根据需要从包含的输入流再次填充该内部缓冲区,一次填充多个字节。
实现了FilterInputStream的mark(int readLine)方法
mark 操作记录输入流中的某个点,reset 操作使得在从包含的输入流中获取新字节之前,再次读取自最后一次 mark 操作后读取的所有字节。
构造器只能接受 InputStream
1 | int available() |
BufferedOutputStream 缓冲输出流
该类实现缓冲的字节输出流。通过设置这种输出流,应用程序就可以将各个字节写入底层输出流中,而不必针对每次字节写入调用底层系统。
BufferedOutputStream重写了OutputStream中的write方法,
1 | public void write(byte[] b, |
OutputStream写入多个字节时会逐个字节的调用单字节写入方法
1 | public void write(int b) |
而 BufferedOutputStream 中的可以一次性将这些字节写入输出流, 因此效率比较高.
DataInputStream 数据输入流
DataInputStream几乎可以任何形式输出,而readLine()是唯一成不了首选的原因。
数据输出流允许应用程序以适当方式将基本 Java 数据类型写入输出流中。然后,应用程序可以使用数据输入流将数据读入。
使用DataInputStream写字符串并且能够通过DataInputStream恢复的唯一的方法是使用UTF-8编码
DataOutputStream 数据输出流
不同的数据类型(基本数据类型和String对象)的读取方法都以read开头,如readByte()、readFloat()等
1 | DataOutputStream outputStream = new DataOutputStream(new BufferedOutputStream(new FileOutputStream("abc.txt"))); |
字符流
FileReader 文件输入字符流
用来读取字符文件的便捷类。此类的构造方法假定默认字符编码和默认字节缓冲区大小都是适当的。
要自己指定这些值,可以先在 FileInputStream 上构造一个 InputStreamReader。
FileReader 用于读取字符流。要读取原始字节流,请考虑使用 FileInputStream。
FileWriter 文件输出字符流
用来写入字符文件的便捷类。此类的构造方法假定默认字符编码和默认字节缓冲区大小都是可接受的。
要自己指定这些值,可以先在 FileOutputStream 上构造一个 OutputStreamWriter。
文件是否可用或是否可以被创建取决于底层平台。特别是某些平台一次只允许一个 FileWriter(或其他文件写入对象)打开文件进行写入。
在这种情况下,如果所涉及的文件已经打开,则此类中的构造方法将失败。
FileWriter 用于写入字符流。要写入原始字节流,请考虑使用 FileOutputStream。
BufferedReader 缓冲字符输入流
从字符输入流中读取文本,缓冲各个字符,从而实现字符、数组和行的高效读取。
可以指定缓冲区的大小,或者可使用默认的大小。大多数情况下,默认值就足够大了。
通常,Reader 所作的每个读取请求都会导致对底层字符或字节流进行相应的读取请求。
因此,建议用 BufferedReader 包装所有其 read() 操作可能开销很高的 Reader(如 FileReader 和 InputStreamReader)。例如,
1 | BufferedReader in |
将缓冲指定文件的输入。如果没有缓冲,则每次调用 read() 或 readLine() 都会导致从文件中读取字节,并将其转换为字符后返回,而这是极其低效的。
通过用合适的 BufferedReader 替代每个 DataInputStream,可以对将 DataInputStream 用于文字输入的程序进行本地化。
主要的方法:
1 | String readLine() |
BufferedWriter 缓冲字符输出流
将文本写入字符输出流,缓冲各个字符,从而提供单个字符、数组和字符串的高效写入。
可以指定缓冲区的大小,或者接受默认的大小。在大多数情况下,默认值就足够大了。
该类提供了 newLine() 方法,它使用平台自己的行分隔符概念,此概念由系统属性 line.separator 定义。
并非所有平台都使用新行符 (‘\n’) 来终止各行。因此调用此方法来终止每个输出行要优于直接写入新行符。
通常 Writer 将其输出立即发送到底层字符或字节流。
除非要求提示输出,否则建议用 BufferedWriter 包装所有其 write() 操作可能开销很高的 Writer(如 FileWriters 和 OutputStreamWriters)。
例如,
1 | PrintWriter out = new PrintWriter(new BufferedWriter(new FileWriter("foo.out"))); |
将缓冲 PrintWriter 对文件的输出。如果没有缓冲,则每次调用 print() 方法会导致将字符转换为字节,然后立即写入到文件,而这是极其低效的。
PrintWriter 打印输出字符流
向文本输出流打印对象的格式化表示形式,支持写入各种格式的字符流。但不能写入字节流。
如果启用了自动刷新(autoFlush),则只有在调用 println、printf 或 format 的其中一个方法时才可能完成此操作,而不是每当正好输出换行符时才完成。这些方法使用平台自有的行分隔符概念,而不是换行符。
为了更容易的过渡到使用PrintWriter,他提供了一个既能接受Writer对象又能接收OutputStream对象的构造器。
甚至提供了可以直接接收文件的构造器。
例如,基本文件输出
1 | PrintWriter printWriter = new PrintWriter(new BufferedWriter(new FileWriter("abc.text"))); |
构造器有:
1 | PrintWriter(File file) |
构造PrintWriter的方式主要有: File或者文件名、OutputStream以及Writer.
主要的方法:
append方法将指定字符、字符序列和指定字符序列的子序列添加到此 writer。
1 | PrintWriter append(char c) |
1 | void println() 通过写入行分隔符字符串终止当前行。 |
PrintStream 存在两个重要的方法:print和println。对它们了重载,以便可打印出各种不同的类型。print可以打印的类型有 boolean char char[] double float int long Object和String.println的参数有boolean char char[] double float int long Object和String.
printf、format和write可以打印更多的格式
1 | PrintWriter printf(Locale l, String format, Object... args) |
其他方法
1 | boolean checkError() |
InputStreamReader 输入流转换器
有时需要将字节流转换为字符流,为了实现这个目的,要用到适配器(adapter)类: InputStreamReader将InputStream转换为Reader; OutputStreamWriter将OutputStream转换为Writer
OutputStreamWriter 输出流转换器
其他
管道流
- PipedInputStream
- PipedOutputStream
- PipedReader
- PipedWriter
1 | /** |
SequenceInputStream
SequenceInputStream可以认为是一个工具类,将两个或者多个输入流当成一个输入流依次读取。完全可以从IO 包中去除,还完全不影响IO 包的结构,却让其更“纯洁”――纯洁的Decorator 模式。
【案例】将两个文本文件合并为另外一个文本文件
1 | /** |
RandomAccessFile
支持搜寻方法,并且只适用于文件。
getFilePointer() 用于查找当前所处的文件位置,seek() 用于在文件内移至新的位置, length()用于判断文件的最大尺寸. 构造器支持在第二个参数中指示随机读(r)还是读写(rw).但是不支持只写.
在JDK1.4后, RandomAccessFile的大多数功能由NIO存储映射文件所取代.
断点下载
注意:
- 使用
FileWriter、BufferedOutputStream或者BufferedWriter写文件时,一定要清理缓冲区并且关闭输出流,否则会造成写入的内容丢失- 使用字符流装饰字节流时,一定要都关闭掉
- 如何判断读到文件结尾?
- 字符流: 每一个
Reader都包含一个ready()方法,该方法用于判断IO是否准备完毕,但是不能是否读到文件的结尾。判断是否读到文件结尾要通过判断读入的内容是否为空判断。
File类
getPath getAbsolutePath和getCanonicalPath
public String getPath()获取为文件赋值时的路径名public String getAbsolutePath()获取绝对路径名(包含..和.等字符)。public String getCanonicalPath()throws IOException获取规范路径名(经过计算后的精简路径名)
getCanonicalPath()
1 | File file = new File("D:\\Text.txt"); |
Windows系统下
(1),确定D盘下没有Text.txt这个文件,直接执行这段代码,得到的结果是:D:\Text.txt
注意这里试大写的Text.txt
(2)在D盘下建立一个文件,名叫text.txt,再次执行代码,得到结果D:\text.txt
尝试在linux下执行上边的步骤,两次打印的结果是相同的,因为linux是大小写敏感的系统。
遍历目录下的文件
- String[] list()
返回一个字符串数组,这些字符串指定此抽象路径名表示的目录中的文件和目录。 - String[] list(FilenameFilter filter)
返回一个字符串数组,这些字符串指定此抽象路径名表示的目录中满足指定过滤器的文件和目录。 - File[] listFiles()
返回一个抽象路径名数组,这些路径名表示此抽象路径名表示的目录中的文件。 - File[] listFiles(FileFilter filter)
返回抽象路径名数组,这些路径名表示此抽象路径名表示的目录中满足指定过滤器的文件和目录。 - File[] listFiles(FilenameFilter filter)
返回抽象路径名数组,这些路径名表示此抽象路径名表示的目录中满足指定过滤器的文件和目录。
上面的遍历方法中存在两个通过FilenameFilter和FileFilter过滤文件的方法。
这是策略模式的典型应用。
以String[] list(FilenameFilter filter)为例
list提供了基本的功能,而且按照FilenameFilter的形式提供策略,以便完善list在提供服务时所需的算法。因为list接受FilenameFilter对象作为参数, 任何实现了FilenameFilter接口的对象都可以传递给list方法,用以选择list方法的行为方式。
1 | public interface FilenameFilter{ |
1 | File srcDir=new File("~/testDir"); |
设置指定权限
迭代创建目录
public boolean mkdirs() 创建此抽象路径名指定的目录,包括所有必需但不存在的父目录。注意,此操作失败时也可能已经成功地创建了一部分必需的父目录。
文件系统根
public static File[] listRoots()
样例模板
关闭
无论使用任何方式打开文件或者流,必须准确的关闭, 标准的形式是在try-catch语句块的finally中关闭.
1 | String fileName = "abc.txt"; |
读取文件
直接通过FileInputStream, 由于没有使用缓冲BufferedInputStream包装, 每次读取一组字节其实是在一个字节一个字节的从系统中读取.
一次性读入字节数组
1 | File f=new File(fileName); |
或者
1 | File f=new File(fileName); |
逐个字节读
FileInputStream的底层原理决定了和上面的读法是一样的
读取字节流时, 读到文件的末尾的时候会返回-1, 以此作为判断读取结束的依据.
1 | File f=new File(fileName); |
加缓冲
使用BufferedInputStream包装输入流
1 | File f=new File(fileName); |
字符流读取
不加缓冲
1 | // 直接通过字符流读文件 file可为File对象或者文件名String |
加缓冲
1 | // 创建FileInputStream |
1 | // 直接通过字符流读文件 file可为File对象或者文件名String |
写文件
字节流写文件
1 | OutputStream out =new FileOutputStream(file); |
或者
1 | OutputStream out =new FileOutputStream(file); |
加缓冲
1 | OutputStream outputStream = new FileOutputSteam(file); |
利用字节流写文件
不加缓冲, 底层实现是逐个字符添加到系统文件中
1 | FileWriter fileWriter = new FileWriter(file); |
加缓冲
1 | // file为File对象 文件名字符串 FileDescriptor对象 |
或者
1 | FileWriter fileWriter = new FileWriter(file); |
格式化字符输出
1 | PrintWriter printWriter = new PrintWriter(new BufferedWriter(new FileWriter("abc.text"))); |
内存中的字符串读取
从内存逐个字节读入字符串
1 | String s = "这是需要读入的字符串"; |
输出结果
1 | 这 |
格式化内存输入
1 | String s = "这是需要输出的字符串!"; |
DataInputStream.available()方法检测还有多少可供读取的字符
文件拷贝
这是采用Java IO实现的版本, 采用Java NIO可以大大提高效率。
1 | File file1=new File(args[0]); |
RandomAccessFile
1 | String fileName="D:"+File.separator+"hello.txt"; |
与字节数组结合
//TODO 与字节数组
标准IO
三个标准IO分别是 System.out System.err System.in, System.out和System.err已经包装成PrintStream对象, 而System.io是没有被包装的未经过加工的InputStream, 因此在读取System.io之前必须对其进行包装.
例子: 一行一行读取系统输入
1 | InputStreamReader in = new InputStreamReader(System.in); |
字符输出到标准输出
1 | PrintWriter out = new PrintWriter(System.out,true); |
标准IO重定向
1 | System.setIn(in); |
//TODO 如何重定向到java.swing.JTextArea
Java IO 异常与错误
进程控制
所谓的进程控制, 是当Java 调用系统指令启动系统进程时, 利用Java IO输出运行过程中的输出和错位信息
1 | String command = "CMD /C ping www.baidu.com"; |
[参考文献]:
- Think in Java