IO就是输入/输出。Java IO类库基于抽象基础类InputStream和OutputStream构建了一套I/O体系,主要解决从数据源读入数据和将数据写入到目的地问题。我们把数据源和目的地可以理解为IO流的两端。
当然,通常情况下,这两端可能是文件或者网络连接。
我们用下面的图描述下,加深理解:
从一种数据源中通过InputStream流对象读入数据到程序内存中
在这里插入图片描述
当然我们把上面的图再反向流程,就是OutputStream的示意了。
在这里插入图片描述
其实除了面向字节流的InputStream/OutputStream体系外,Java IO类库还提供了面向字符流的Reader/Writer体系。Reader/Writer继承结构主要是为了国际化,因为它能更好地处理16位的Unicode字符。
在学习是这两套IO流处理体系可以对比参照着学习,因为有好多相似之处。
要理解总体设计
刚开始写IO代码,总被各种IO流类搞得晕头转向。这么多IO相关的类,各种方法,啥时候能记住。
其实只要我们掌握了IO类库的总体设计思路,理解了它的层次脉络之后,就很清晰。知道啥时候用哪些流对象去组合想要的功能就好了,API的话,可以查手册的嘛。
首先从流的流向上可以分为输入流InputStream或Reader,输出流OutputStream或Writer。任何从InputStream或Reader派生而来的类都有read()基本方法,读取单个字节或字节数组;任何从OutputSteam或Writer派生的类都含有write()的基本方法,用于写单个字节或字节数组。
从操作字节还是操作字符的角度,有面向字节流的类,基本都以XxxStream结尾,面向字符流的类都以XxxReader或XxxWriter结尾。当然这两种类型的流是可以转化的,有两个转化流的类,这个后面会说到。
一般在使用IO流的时候会有下面类似代码:
1 FileInputStream inputStream = new FileInputStream(new File("a.txt"));
2 BufferedInputStream bufferedInputStream = new BufferedInputStream(inputStream);
这里其实是一种装饰器模式的使用,IO流体系中使用了装饰器模式包装了各种功能流类。
在Java IO流体系中FilterInputStream/FilterOutStream
和FilterReader/FilterWriter
就是装饰器模式的接口类,从该类向下包装了一些功能流类。有DataInputStream、BufferedInputStream、LineNumberInputStream、PushbackInputStream
等,当然还有输出的功能流类;面向字符的功能流类等。
下面几张图描述了整个IO流的继承体系结构
InputStream流体系
在这里插入图片描述
OutputStream流体系
在这里插入图片描述
Reader体系
在这里插入图片描述
Writer体系
在这里插入图片描述
在这里插入图片描述
File其实是个工具类
File类其实不止是代表一个文件,它也能代表一个目录下的一组文件(代表一个文件路径)。下面我们盘点一下File类中最常用到的一些方法
1File.delete() 删除文件或文件夹目录。
2File.createNewFile() 创建一个新的空文件。
3File.mkdir() 创建一个新的空文件夹。
4File.list() 获取指定目录下的文件和文件夹名称。
5File.listFiles() 获取指定目录下的文件和文件夹对象。
6File.exists() 文件或者文件夹是否存在
7
8String getAbsolutePath() // 获取绝对路径
9long getFreeSpace() // 返回分区中未分配的字节数。
10String getName() // 返回文件或文件夹的名称。
11String getParent() // 返回父目录的路径名字符串;如果没有指定父目录,则返回 null。
12File getParentFile() // 返回父目录File对象
13String getPath() // 返回路径名字符串。
14long getTotalSpace() // 返回此文件分区大小。
15long getUsableSpace() //返回占用字节数。
16int hashCode() //文件哈希码。
17long lastModified() // 返回文件最后一次被修改的时间。
18long length() // 获取长度,字节数。
19boolean canRead() //判断是否可读
20boolean canWrite() //判断是否可写
21boolean isHidden() //判断是否隐藏
22
23
24// 成员函数
25static File[] listRoots() // 列出可用的文件系统根。
26boolean renameTo(File dest) // 重命名
27boolean setExecutable(boolean executable) // 设置执行权限。
28boolean setExecutable(boolean executable, boolean ownerOnly) // 设置其他所有用户的执行权限。
29boolean setLastModified(long time) // 设置最后一次修改时间。
30boolean setReadable(boolean readable) // 设置读权限。
31boolean setReadable(boolean readable, boolean ownerOnly) // 设置其他所有用户的读权限。
32boolean setWritable(boolean writable) // 设置写权限。
33boolean setWritable(boolean writable, boolean ownerOnly) // 设置所有用户的写权限。
34
需要注意的是,不同系统对文件路径的分割符表是不一样的,比如Windows中是“\”,Linux是“/”。而File类给我们提供了抽象的表示File.separator,屏蔽了系统层的差异
。因此平时在代码中不要使用诸如“\”这种代表路径,可能造成Linux平台下代码执行错误。
下面是一些示例:
根据传入的规则,遍历得到目录中所有的文件构成的File对象数组
1public class Directory {
2 public static File[] getLocalFiles(File dir, final String regex){
3 return dir.listFiles(new FilenameFilter() {
4 private Pattern pattern = Pattern.compile(regex);
5 public boolean accept(File dir, String name) {
6 return pattern.matcher(new File(name).getName()).matches();
7 }
8 });
9 }
10
11 // 重载方法
12 public static File[] getLocalFiles(String path, final String regex){
13 return getLocalFiles(new File(path),regex);
14 }
15
16 public static void main(String[] args) {
17 String dir = "d:";
18 File[] files = Directory.getLocalFiles(dir,".*\\.txt");
19 for(File file : files){
20 System.out.println(file.getAbsolutePath());
21 }
22 }
23}
输出结果:
1d:\\1.txt
2d:\\新建文本文档.txt
上面的代码中dir.listFiles(FilenameFilter ) 是策略模式的一种实现,而且使用了匿名内部类的方式。
上面的例子是《Java 编程思想》中的示例,这本书中的每个代码示例都很经典,Bruce Eckel大神把面向对象的思想应用的炉火纯青,非常值得细品。
InputStream和OutputStream
InputStream是输入流,前面已经说到,它是从数据源对象将数据读入程序内容时,使用的流对象。
通过看InputStream的源码知道,它是一个抽象类,
1public abstract class InputStream extends Object implements Closeable
2
提供了一些基础的输入流方法:
1//从数据中读入一个字节,并返回该字节,遇到流的结尾时返回-1
2abstract int read()
3
4//读入一个字节数组,并返回实际读入的字节数,最多读入b.length个字节,遇到流结尾时返回-1
5int read(byte[] b)
6
7// 读入一个字节数组,返回实际读入的字节数或者在碰到结尾时返回-1.
8//b:代表数据读入的数组, off:代表第一个读入的字节应该被放置的位置在b中的偏移量,len:读入字节的最大数量
9int read(byte[],int off,int len)
10
11// 返回当前可以读入的字节数量,如果是从网络连接中读入,这个方法要慎用,
12int available()
13
14//在输入流中跳过n个字节,返回实际跳过的字节数
15long skip(long n)
16
17//标记输入流中当前的位置
18void mark(int readlimit)
19
20//判断流是否支持打标记,支持返回true
21boolean markSupported()
22
23// 返回最后一个标记,随后对read的调用将重新读入这些字节。
24void reset()
25
26//关闭输入流,这个很重要,流使用完一定要关闭
27void close()
28
直接从InputStream继承的流,可以发现,基本上对应了每种数据源类型。
类 | 功能 |
---|---|
ByteArrayInputStream | 将字节数组作为InputStream |
StringBufferInputStream | 将String转成InputStream |
FileInputStream | 从文件中读取内容 |
PipedInputStream | 产生用于写入相关PipedOutputStream的数据。实现管道化 |
SequenceInputStream | 将两个或多个InputStream对象转换成单一的InputStream |
FilterInputStream | 抽象类,主要是作为“装饰器”的接口类,实现其他的功能流 |
OutputStream
是输出流的抽象,它是将程序内存中的数据写入到目的地(也就是接收数据的一端)。看下类的签名:
1public abstract class OutputStream implements Closeable, Flushable {}
提供了基础方法相比输入流来说简单多了,主要就是write写方法(几种重载的方法)、flush冲刷和close关闭。
1// 写出一个字节的数据
2abstract void write(int n)
3
4// 写出字节到数据b
5void write(byte[] b)
6
7// 写出字节到数组b,off:代表第一个写出字节在b中的偏移量,len:写出字节的最大数量
8void write(byte[] b, int off, int len)
9
10//冲刷输出流,也就是将所有缓冲的数据发送到目的地
11void flush()
12
13// 关闭输出流
14void close()
15
同样地,OutputStream
也提供了一些基础流的实现,这些实现也可以和特定的目的地(接收端)对应起来,比如输出到字节数组或者是输出到文件/管道等。
类 | 功能 |
---|---|
ByteArrayOutputStream | 在内存中创建一个缓冲区,所有送往“流”的数据都要放在此缓冲区 |
FileOutputStream | 将数据写入文件 |
PipedOutputStream | 和PipedInputStream配合使用。实现管道化 |
FilterOutputStream | 抽象类,主要是作为“装饰器”的接口类,实现其他的功能流 |
使用装饰器包装有用的流
Java IO 流体系使用了装饰器模式来给哪些基础的输入/输出流添加额外的功能。这写额外的功能可能是:可以将流缓冲起来提高性能、是流能够读写基本数据类型等。
这些通过装饰器模式添加功能的流类型都是从FilterInputStream和FilterOutputStream抽象类扩展而来的。
可以再返回文章最开始说到IO流体系的层次时,那几种图加深下印象。
FilterInputStream类型
类 | 功能 |
---|---|
DataInputStream | 和DataOutputStream搭配使用,使得流可以读取int char long等基本数据类型 |
BufferedInputStream | 使用缓冲区,主要是提高性能 |
LineNumberInputStream | 跟踪输入流中的行号,可以使用getLineNumber、setLineNumber(int) |
PushbackInputStream | 使得流能弹出“一个字节的缓冲区”,可以将读到的最后一个字符回退 |
FilterOutStream类型
类 | 功能 |
---|---|
DataOutputStream | 和DataInputStream搭配使用,使得流可以写入int char long等基本数据类型 |
PrintStream | 用于产生格式化的输出 |
BufferedOutputStream | 使用缓冲区,可以调用flush()清空缓冲区 |
大多数情况下,其实我们在使用流的时候都是输入流和输出流搭配使用的。目的就是为了转移和存储数据,单独的read()对我们而言有啥用呢,读出来一个字节能干啥?对吧。因此要理解流的使用就是搭配起来或者使用功能流组合起来去转移或者存储数据。
Reader和Writer
Reader
是Java IO中所有Reader的基类。Reader
与InputStream
类似,不同点在于,Reader基于字符而非基于字节。
Writer
是Java IO中所有Writer的基类。与Reader
和InputStream
的关系类似,Writer基于字符而非基于字节,Writer用于写入文本,OutputStream
用于写入字节。
Reader
和Writer
的基础功能类,可以对比InputStream
、OutputStream
来学习。
面向字节 | 面向字符 |
---|---|
InputStream | Reader |
OutputStream | Writer |
FileInputStream | FileReader |
FileOutputStream | FileWriter |
ByteArrayInputStream | CharArrayReader |
ByteArrayOutputStream | CharArrayWriter |
PipedInputStream | PipedReader |
PipedOutputStream | PipedWriter |
StringBufferInputStream(已弃用) | StringReader |
无对应类 | StringWriter |
有两个“适配器” 流类型,它们可以将字节流转化成字节流。这就是InputStreamReader 可以将InputStream转成为Reader,OutputStreamWriter可以将OutputStream转成为Writer。
适配器类,字节流转字符流
在这里插入图片描述
当然也有类似字节流的装饰器实现方式,给字符流添加额外的功能或这说是行为。这些功能字符流类主要有:
-
BufferedReader
-
BufferedWriter
-
PrintWriter
-
LineNumberReader
-
PushbackReader
System类中的I/O流
想想你的第一个Java程序是啥?我没猜错的话,应该是 hello world。
1System.out.println("hello world")
简单到令人发指,今天就说说标准的输入/输出流。
在标准IO模型中,Java提供了System.in、System.out和System.error。
先说System.in
,看下源码
1public final static InputStream in
是一个静态域,未被包装过的InputStream
。通常我们会使用BufferedReader
进行包装然后一行一行地读取输入,这里就要用到前面说的适配器流InputStreamReader
。
1public class SystemInReader {
2 public static void main(String[] args) throws IOException {
3 BufferedReader reader = new BufferedReader(new InputStreamReader(System.in));
4 String s;
5 while ((s = reader.readLine()) != null && s.length() != 0){
6 System.out.println(s);
7 }
8 }
9}
该程序等待会一直等待我们输入,输入啥,后面会接着输出。输入空字符串可以结束。
11
21
3123
4123
System.out是一个PrintStream流。
System.out一般会把你写到其中的数据输出到控制台上。System.out通常仅用在类似命令行工具的控制台程序上。System.out也经常用于打印程序的调试信息(尽管它可能并不是获取程序调试信息的最佳方式)。
System.err是一个PrintStream流。
System.err与System.out的运行方式类似,但它更多的是用于打印错误文本。
可以将这些系统流重定向
尽管System.in, System.out, System.err这3个流是java.lang.System类中的静态成员,并且已经预先在JVM启动的时候初始化完成,你依然可以更改它们。
可以使用setIn(InputStream)、setOut(PrintStream)、setErr(PrintStream)进行重定向。
比如可以将控制台的输出重定向到文件中。
1OutputStream output = new FileOutputStream("d:/system.out.txt");
2PrintStream printOut = new PrintStream(output);
3System.setOut(printOut);