一直对IO比较半懂不懂,乘着闲暇时间系统梳理一遍IO知识,为以后学习做好铺垫。
什么是IO?
即input output,在Java中,流是一个核心的概念。
流从概念上来说是一个连续的数据流。你既可以从流中读取数据,也可以往流中写数据。
流与数据源或者数据流向的媒介相关联。在Java IO中流既可以是字节流(以字节为单位进行读写),也可以是字符流(以字符为单位进行读写)。注意区分字节和字符,字符是字节的上层单位,即字符由4(或其他)字节大小组成。
InputStream, OutputStream, Reader 和Writer
这四个是IO流中的基类,InputString和OutputStream是对于字节流的输入输出,而Reader和Writer则是对于字符流的操作,
需要InputStream或者Reader从数据源读取数据,需要OutputStream或者Writer将数据写入到目标媒介中。
注意此处的方向问题,要站在电脑基础上来思维,Input和Reader是从外部读入电脑,而Output和Writer则是向外处写文件。
用途总览
说过上面四个是基类,那么上面四个类里面定义的都是些大体对字节或者字符的操作,为了提升便利性,jdk为我们又定义众多子类来处理不同类型的文件:
文件访问、网络访问、内存缓存访问、线程内部通信(管道)、缓冲、过滤、解析、读写文本 (Readers / Writers)、读写基本类型数据 (long, int etc.)、读写对象。
具体的看如下表格:
Type | Byte Based Input | Byte Based Output | Character Based Input | Character Based Output |
---|---|---|---|---|
Basic | InputStream | OutputStream | Reader/InputStreamReader | Writer/OutputStreamWriter |
Arrays | ByteArrayInputStream | ByteArrayOutputStream | CharArrayReader | CharArrayWriter |
Files | FileInputStream/RandomAccessFile | FileOutputStream/RandomAccessFile | FileReader | FileWriter |
Pipes | PipedInputStream | PipedOutputStream | PipedReader | PipedWriter |
Buffering | BufferedInputStream | BufferedOutputStream | BufferedReader | BufferedWriter |
Filtering | FilterInputStream | FilterOutputStream | FilterReader | FilterWriter |
Parsing | PushbackInputStream/StreamTokenizer | PushbackReader/LineNumberReader | ||
Strings | StringReader | StringWriter | ||
Data | DataInputStream | DataOutputStream | ||
Data - Formatted | PrintStream | PrintWriter | ||
Objects | ObjectInputStream | ObjectOutputStream | ||
Utilities | SequenceInputStream |
下面从文件开始一点点了解用途。
文件
在Java应用程序中,文件是一种常用的数据源或者存储数据的媒介,我们可以从文件中读取数据,或者写入数据到文件中。
通过IO顺序读写文件
可以通过Java将一个文件数据读入到java流中,可以根据是二进制还是文本文件选择使用FileInputStream还是FileReader。关于二进制还是文本文件,可以将文本文件看作一个特殊的二进制文件,他们二者只是解释方式不同,具体可看:
https://www.zhihu.com/question/19971994
这两个类允许你从文件开始到文件末尾一次读取一个字节或者字符,或者将读取到的字节写入到字节数组或者字符数组。你不必一次性读取整个文件,相反你可以按顺序地读取文件中的字节和字符。
同样,对于写文件,可以根据你要写入的数据是二进制型数据还是字符型数据选用FileOutputStream或者FileWriter。可以一次写入一个字节或者字符到文件中,也可以直接写入一个字节数组或者字符数据。数据按照写入的顺序存储在文件当中。随机存取文件
可以通过RandomAccessFile对文件进行随机存取,随机存取并不意味着你可以在真正随机的位置进行读写操作,它只是意味着你可以跳过文件中某些部分进行操作,并且支持同时读写,不要求特定的存取顺序。这使得RandomAccessFile可以覆盖一个文件的某些部分、或者追加内容到它的末尾、或者删除它的某些内容,当然它也可以从文件的任何位置开始读取文件。文件和目录信息的读取
可以由File知道文件的大小和属性,对于目录来说也是一样的,比如你需要获取某个目录下的文件列表。通过File类可以获取文件和目录的信息。
管道
Java IO中的管道为运行在同一个JVM中的两个线程提供了通信的能力。所以管道也可以作为数据源以及目标媒介。
你不能利用管道与不同的JVM中的线程通信(不同的进程)。在概念上,Java的管道不同于Unix/Linux系统中的管道。在Unix/Linux中,运行在不同地址空间的两个进程可以通过管道通信。
在Java中,通信的双方应该是运行在同一进程中的不同线程。
一个线程通过PipedOutputStream写入的数据可以被另一个线程通过相关联的PipedInputStream读取出来。
看下面一个简单例子:
public static void main(String[] args) throws IOException {
final PipedOutputStream pos = new PipedOutputStream();
final PipedInputStream pis = new PipedInputStream(pos);
Thread t1 = new Thread(new Runnable() {
public void run() {
try {
pos.write("hello world?".getBytes());
} catch (Exception e) {
e.printStackTrace();
} finally {
if (pos != null) {
try {
pos.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
});
Thread t2 = new Thread(new Runnable() {
public void run() {
try {
int data = pis.read();
while (data != -1) {
System.out.print((char) data);
data = pis.read();
}
} catch (Exception e) {
e.printStackTrace();
} finally {
if (pis != null) {
try {
pis.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
});
ExecutorService executor = Executors.newFixedThreadPool(2);
executor.execute(t1);
executor.execute(t2);
}
当然对于上述pis和pos的read方法是阻塞式的,只有当有值才会读取并且输出。所以如果尝试在同一个线程里面调用read()方法和write()方法会导致流阻塞,可能会导致线程死锁。
网络
当两个进程建立起网络连接后,也可以把数据传输抽象成IO流的输入输出。
即利用InputStream读取数据,利用OutputStream写入数据。换句话来说,Java网络API用来在不同进程之间建立网络连接,而Java IO则用来在建立了连接之后的进程之间交换数据。
字节数组和字符数组
主要就是围绕ByteArrayInputStream和ByteArrayOutputStream来讲。
在Java中,常用字节和字符数组在应用中临时存储数据。而这些数组又是通常的数据读取来源或者写入目的地。如果你需要在程序运行时需要大量读取文件里的内容,那么你也可以把一个文件加载到数组中。当然你可以通过直接指定索引来读取这些数组。
写了个简单例子,助于理解:
private static void read() throws FileNotFoundException, IOException{
File file = new File("/home/anla7856/workspace/io.examples/pom.xml");
byte[] data = new byte[1024];
int length = new FileInputStream(file).read(data);
System.out.println(length);
InputStream is = new ByteArrayInputStream(data);
String content = new String(data, "UTF-8"); //输出的则为pom文件内容
System.out.println(content);
int temp = is.read();
while(temp != -1){
System.out.print(temp); //这里输出的是字节符号
temp = is.read();
}
is.close();
}
private static void write() throws UnsupportedEncodingException, IOException {
File file = new File("/home/anla7856/workspace/io.examples/pom1.xml");
FileOutputStream fos = new FileOutputStream(file);
OutputStream output = new BufferedOutputStream(fos);
output.write("This text is converted to bytes".getBytes("utf-8"));
output.close();
fos.close();
}
System.in, System.out, System.err
- System.in是一个典型的连接控制台程序和键盘输入的InputStream流。通常当数据通过命令行参数或者配置文件传递给命令行Java程序的时候,比如在刷oj时候,会使用到这个和Scanner来辅助输入。
- System.out是一个PrintStream流。System.out一般会把你写到其中的数据输出到控制台上。System.out通常仅用在类似命令行工具的控制台程序上。
- System.err是一个PrintStream流。System.err与System.out的运行方式类似,但它更多的是用于打印错误文本。一些类似Eclipse的程序,为了让错误信息更加显眼,会将错误信息以红色文本的形式通过System.err输出到控制台上。
替换系统流:
可以替换这几个系统流,也就是可以讲他们直接输出到文件中:
OutputStream output = new FileOutputStream("/home/anla7856/workspace/io.examples/log.txt");
PrintStream printOut = new PrintStream(output);
System.setOut(printOut);
现在所有的System.out
都将重定向到/home/anla7856/workspace/io.examples/log.txt
文件中。请记住,务必在JVM关闭之前冲刷System.out
,即调用flush()
,确保System.out把数据输出到了文件中。
流
Java IO流是既可以从中读取,也可以写入到其中的数据流。
流和数组不一样,不能通过索引读写数据。在流中,你也不能像数组那样前后移动读取数据,除非使用RandomAccessFile 处理文件。流仅仅只是一个连续的数据流。
某些类似PushbackInputStream 流的实现允许你将数据重新推回到流中,以便重新读取。然而你只能把有限的数据推回流中,并且你不能像操作数组那样随意读取数据。流中的数据只能够顺序访问。
如下例子:
PushbackInputStream input = new PushbackInputStream(
new FileInputStream("/home/anla7856/workspace/io.examples/input.txt"));
int data = input.read();
input.unread(data);
或者,你也能限制回退长度:
int pushbackLimit = 8;
PushbackInputStream input = new PushbackInputStream(
new FileInputStream("/home/anla7856/workspace/io.examples/input.txt"),
pushbackLimit);
Java IO流通常是基于字节或者基于字符的。
字节流通常以“stream”命名,比如InputStream和OutputStream。除了DataInputStream 和DataOutputStream 还能够读写int, long, float和double类型的值以外,其他流在一个操作时间内只能读取或者写入一个原始字节。
字符流通常以“Reader”或者“Writer”命名。字符流能够读写字符(比如Latin1或者Unicode字符)。
组合流:
你可以将流整合起来以便实现更高级的输入和输出操作。
比如,一次读取一个字节是很慢的,所以可以从磁盘中一次读取一大块数据,然后从读到的数据块中获取字节。为了实现缓冲,可以把InputStream包装到BufferedInputStream中。
InputStream input = new BufferedInputStream(
new FileInputStream("/home/anla7856/workspace/io.examples/input.txt"));
缓冲只是通过流整合实现的其中一个效果。你可以把InputStream包装到PushbackInputStream中,之后可以将读取过的数据推回到流中重新读取,在解析过程中有时候这样做很方便,将不同的流整合到一个链中,可以实现更多种高级操作。
Reader And Writer
前面说过,InputStream和OutputStream是基于字节的,而Reader和Writer除了基于字符。
例如在上面写过一个例子,stream读取的是字节流,而Reader和Writer读取的,则是字符,可以根据不同的编码读取数据。
而不用想上面的,读取byte[]后,再由String去转码。
Reader
Reader类是Java IO中所有Reader的基类。子类包括BufferedReader,PushbackReader,InputStreamReader,StringReader和其他Reader。
例如下面:
private static void reader() throws IOException{
Reader read = new FileReader("/home/anla7856/workspace/io.examples/pom.xml");
int data = read.read();
while(data != -1){
System.out.print((char)data); //读出的是字符,所以通过char可以强行转化得出字符
data = read.read();
}
}
private static void reader1() throws IOException{
//用utf-8解码
Reader read = new InputStreamReader(
new FileInputStream(
new File("/home/anla7856/workspace/io.examples/pom.xml")),"UTF-8");
int data = read.read();
while(data != -1){
System.out.print((char)data);
data = read.read();
}
}
上面两个输出都是一样的。
InputStream的read()方法返回一个字节,意味着这个返回值的范围在0到255之间(当达到流末尾时,返回-1),
Reader的read()方法返回一个字符,意味着这个返回值的范围在0到65535之间(当达到流末尾时,同样返回-1)。想要获得正常输出,需要char转化。
这并不意味着Reade只会从数据源中一次读取2个字节,Reader会根据文本的编码,一次读取一个或者多个字节。
Writer
Writer类是Java IO中所有Writer的基类。子类包括BufferedWriter和PrintWriter等等。
看一个例子:
private static void writer() throws IOException{
Writer writer = new FileWriter("/home/anla7856/workspace/io.examples/output.txt");
writer.write("Hello World Writer"); //不需要转码
writer.close();
}
最好使用Writer的子类,不需要直接使用Writer,因为子类的实现更加明确,更能表现你的意图。常用子类包括OutputStreamWriter,CharArrayWriter,FileWriter等。
Writer的write(int c)方法,会将传入参数的低16位写入到Writer中,忽略高16位的数据。,因为只有0~65535大小
并发IO
有时候你可能需要并发地处理输入和输出。换句话说,你可能有超过一个线程处理输入和产生输出。比如,你有一个程序需要处理磁盘上的大量文件,这个任务可以通过并发操作提高性能。又比如,你有一个web服务器或者聊天服务器,接收许多连接和请求,这些任务都可以通过并发获得性能的提升。
如果你需要并发处理IO,这里有几个问题可能需要注意一下:
- 在同一时刻不能有多个线程同时从InputStream或者Reader中读取数据,也不能同时往OutputStream或者Writer里写数据。你没有办法保证每个线程读取多少数据,以及多个线程写数据时的顺序。
- 如果线程之间能够保证操作的顺序,即你能够才想到线程之间尽管有并发,但是仍然能够正确完成自己的工作,就像集合里面的Spliterator那样,使用分区域的完成。
InputStream和OutputStream
InputStream:
InputStream用于读取基于字节的数据,一次读取一个字节,例子就不提了,上面有个类似的,Java7之后,在捕获一场方面,
出现了一种新的方式:“try-with-resource”结构
如下:
private static void printFileJava7() throws IOException {
try( FileInputStream input = new FileInputStream("file.txt");
BufferedInputStream bufferedInput = new BufferedInputStream(input)
) {
int data = bufferedInput.read();
while(data != -1){
System.out.print((char) data);
data = bufferedInput.read();
}
}
}
主要方法:
- read():一次读取一个字节,大小在0~255,可以用char强制转化后输出,InputStream的子类可能会包含read()方法的替代方法。比如,DataInputStream允许你利用readBoolean(),readDouble()等方法读取Java基本类型变量int,long,float,double和boolean。
- read(byte[]):一次读取byte[]大小的字节到byte里面,相似的还有
int read(byte, int offset, int length)
方法,一次性读取一个字节数组的方式,比一次性读取一个字节的方式快的多,所以,尽可能使用这两个方法代替read()方法。
OutputStream:
OutputStream类是Java IO API中所有输出流的基类。子类包括BufferedOutputStream,FileOutputStream等等。
看一些主要方法:
write(byte) :
write(byte)方法用于把单个字节写入到输出流中。OutputStream的write(byte)方法将一个包含了待写入数据的int变量作为参数进行写入。只有int类型的第一个字节会被写入,其余位会被忽略。(即写入低8位,忽略高24位)。
OutputStream的子类可能会包含write()方法的替代方法。比如,DataOutputStream允许你利用writeBoolean(),writeDouble()等方法将基本类型int,long,float,double,boolean等变量写入。write(byte[]):
OutputStream同样包含了将字节数据中全部或者部分数据写入到输出流中的方法,分别是write(byte[])和write(byte[], int offset, int length)。flush():
通过调用flush()方法,可以把缓冲区内的数据刷新到磁盘(或者网络,以及其他任何形式的目标媒介)中。close():
当你结束数据写入时,需要关闭OutputStream。通过调用close()可以达到这一点。因为OutputStream的各种write()方法可能会抛出IO异常,所以你需要把调用close()的关闭操作方在finally块中执行。
RandomAccessFile
RandomAccessFile允许你来回读写文件,也可以替换文件中的某些部分。FileInputStream和FileOutputStream没有这样的功能。
看下面一个例子:
private static void seek() throws IOException{
RandomAccessFile file = new RandomAccessFile("/home/anla7856/workspace/io.examples/pom.xml", "rw");
file.seek(200);
long pointer = file.getFilePointer();
file.close();
System.out.println(pointer);
}
BufferedInputStream和BufferedOutputStream
这两个BufferedStream可以提供一个缓冲区,能提高IO的读取速度,你可以一次读取一大块的数据,而不需要每次从网络或者磁盘中一次读取一个字节。特别是在访问大量磁盘数据时,缓冲通常会让IO快上许多。
用法:
InputStream input = new BufferedInputStream(new FileInputStream("c:\\data\\input-file.txt"));
默认大小为8kb:
private static int DEFAULT_BUFFER_SIZE = 8192;
当然,你也可以自己更改:
InputStream input = new BufferedInputStream(new FileInputStream("c:\\data\\input-file.txt"), 8 * 1024);
然后通过InputStream类似的方法进行操作。
BufferedOutputStream和BufferedInputStream用法上类似,注意读取数据时候,要使用flush方法清空缓冲区数据。
DataInputStream和DataOutputStream
这两个类可以帮助我们写入或者读出Java类型的数据,而不是操作byte或者byte[]。
例如下面例子:
private static void readData() throws IOException{
DataInputStream input = new DataInputStream(
new FileInputStream("/home/anla7856/workspace/io.examples/pom.xml"));
int data = input.readInt();
System.out.println(data);
}
当你要读取的数据中包含了int,long,float,double这样的基本类型变量时,DataInputStream可以很方便地处理这些数据。
DataOutputStream与DataInputStream类似。
序列化与ObjectInputStream、ObjectOutputStream
利用上面两个类,我们可以把一个类序列化到一个目的数据源(例如文件),然后需要时后再将他们读取出来。
看下面例子:
private static void writeObject() throws FileNotFoundException, IOException{
Dog dog = new Dog("tom",18);
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream(new File("/home/anla7856/workspace/io.examples/data.xml")));
oos.writeObject(dog);
oos.close();
}
private static void readObject() throws FileNotFoundException, IOException, ClassNotFoundException {
ObjectInputStream ois = new ObjectInputStream(new FileInputStream("/home/anla7856/workspace/io.examples/data.xml"));
Dog dog = (Dog) ois.readObject();
System.out.println(dog);
}
ObjectInputStream和ObjectOutputStream还有许多read和write方法,比如readInt、writeLong等等。
在你序列化和反序列化一个对象之前,该对象的类必须实现了java.io.Serializable接口。
InputStreamReader和OutputStreamWriter
这两个类是用来讲reader,writer和inputStream,outputStream相互转化的类。
看如下一个例子:
private static void reader1() throws IOException{
Reader read = new InputStreamReader(new FileInputStream(new File("/home/anla7856/workspace/io.examples/pom.xml")),"UTF-8");
int data = read.read();
while(data != -1){
System.out.print((char)data);
data = read.read();
}
}
注意在int(4字节)和char(2字节)之间有精度问题,但是
char aChar = (char) data; //这里不会造成数据丢失,因为返回的int类型变量data只有低16位有数据,高16位没有数据
OutputStreamWriter类似~
其他IO类
下面介绍一些其他io类:
SequenceInputStream
SequenceInputStream把一个或者多个InputStream整合起来,形成一个逻辑连贯的输入流。当读取SequenceInputStream时,会先从第一个输入流中读取,完成之后再从第二个输入流读取,以此推类。
具体使用:
InputStream input1 = new FileInputStream("/home/anla7856/workspace/io.examples/file1.txt");
InputStream input2 = new FileInputStream("/home/anla7856/workspace/io.examples/file2.txt");
InputStream combined = new SequenceInputStream(input1, input2);
PrintStream
PrintStream允许你把格式化数据写入到底层OutputStream中。比如,写入格式化成文本的int,long以及其他原始数据类型到输出流中,而非它们的字节数据。
下面看一个例子:
private static void printStream(){
String s = "printfStream";
OutputStream os = System.out;
PrintStream output = new PrintStream(os);
output.print(true);
output.printf(Locale.UK, "Text + data: %s$", s);
output.print((float) 123.456);
output.close();
}
例如上面,获得控制台OutputStream,并且能够格式化输出,注意这一行:
output.printf(Locale.UK, "Text + data: %s$", s);
对于占位符,需要名字一致。
LineNumberReader
LineNumberReader是记录了已读取数据行号的BufferedReader。默认情况下,行号从0开始,当LineNumberReader读取到行终止符时,行号会递增(换行\n,回车\r,或者换行回车\n\r都是行终止符)。
你可以通过getLineNumber()方法获取当前行号,通过setLineNumber()方法设置当前行数(译者注:setLineNumber()仅仅改变LineNumberReader内的记录行号的变量值,不会改变当前流的读取位置。流的读取依然是顺序进行,意味着你不能通过setLineNumber()实现流的跳跃读取)。
看一段代码例子:
LineNumberReader lineNumberReader =
new LineNumberReader(new FileReader("/home/anla7856/workspace/io.examples/input.txt"));
int data = lineNumberReader.read();
while(data != -1){
char dataChar = (char) data;
data = lineNumberReader.read();
int lineNumber = lineNumberReader.getLineNumber();
}
lineNumberReader.close();
这个类能用来干嘛呢?
能够帮我们很容易的定位到出错的位置,以行数来决定。
StreamTokenizer
StreamTokenizer可以帮助我们干嘛呢?
简单来说,就是它可以分词(英文),并且可以区分词语属性从而输出,看一段例子:
private static void streamTokenizer() throws IOException{
StreamTokenizer streamTokenizer = new StreamTokenizer(
new StringReader("Mary had 1 little lamb..."));
while(streamTokenizer.nextToken() != StreamTokenizer.TT_EOF){
if(streamTokenizer.ttype == StreamTokenizer.TT_WORD) {
System.out.println(streamTokenizer.sval); //sval 如果读取到的符号是字符串类型,该变量的值就是读取到的字符串的值
} else if(streamTokenizer.ttype == StreamTokenizer.TT_NUMBER) {
System.out.println(streamTokenizer.nval); //nval 如果读取到的符号是数字类型,该变量的值就是读取到的数字的值
} else if(streamTokenizer.ttype == StreamTokenizer.TT_EOL) {
System.out.println();
}
}
}
以及它的输出:
Mary
had
1.0
little
lamb...
StreamTokenizer可以识别标示符,数字,引用的字符串,和多种注释类型。你也可以指定何种字符解释成空格、注释的开始以及结束等。在StreamTokenizer开始解析之前,所有的功能都可以进行配置。
StringReader和StringWriter
StringReader能够将原始字符串转换成Reader,而StringWriter能够以字符串的形式从Writer中获取写入到其中数据。
例如以下StringReader:
String input = "Input String... ";
StringReader stringReader = new StringReader(input);
int data = stringReader.read();
while(data != -1) {
doSomethingWithData(data);
data = stringReader.read();
}
stringReader.close();
而对于StringWriter:
StringWriter stringWriter = new StringWriter();
stringWriter.write("This is a text");
String data = stringWriter.toString();
StringBuffer dataBuffer = stringWriter.getBuffer();
stringWriter.close();