目录
一、IO说明
按照POSIX的标准划分IO, 可以把IO分成两类:同步IO 和 异步IO
按照线程对IO业务的处理是否被阻塞的角度,可以将IO分成两类:阻塞IO 和 非阻塞IO
(POSIX的标准 : UNIX系统的一个设计标准)
对于IO的操作步骤可以分成两步:
1、程序发出IO请求
2、完成实际的IO操作
- 阻塞IO 和 非阻塞IO 的划分标准是针对第一步来划分的(程序发出IO请求),如果发出IO请求会阻塞线程,就是阻塞IO;如果发出的IO请求没有阻塞IO,就是非阻塞IO
举例:传统IO 是阻塞IO; 基于Channel的非阻塞IO是非阻塞IO
- 同步IO 和 异步IO的区别在第二步,如果实际的IO操作由操作系统完成,在将结果返回给应用程序,就是异步IO; 如果实际的IO需要应用程序本身执行,会阻塞线程,那就是同步IO.
举例:传统IO、基于Channel的非阻塞IO, 都是异步IO
二、输入流 与 输出流:
按照流的流向可以分为输入流和输出流。
输入流:只能从中读取数据,而不能向其中写入数据
输出流:只能向其中写入数据,而不能从中读数据
注意:这里的输入输出是从程序运行所在的内存的角度来划分的,比如,从内存到硬盘的称为输出流。
InputStream / Reader : 所有输入流的基类,前者是字节流, 后者是字符流
OutputStream / Writer : 所有输出流的基类,前者是字节流, 后者是字符流
(说明:字节流处理的数据单元是8位的字节, 字符流处理的数据单元是16位的字符)
输入流 使用隐式的指针来标识应该从当前哪个位置读取数据。每当从输入流中读取一个数据后,这个指针自动向后移动
输出流 使用隐式的指针来标识 数据即将要放入的位置。 每当向输出流里输出一个数据后,这个指针自动向后移动
(可以看出字符流或字节流处理的数据单元 是以本身能处理的单位为单位(8位字节或16位字符))
2.1 输入/输出流的主要方法
InputStream里的主要方法:(读取数据的基本单位是字节)
1. public abstract int read(): 从输入流中读取单个字节,返回读取到的字节数据(字节数据可以直接转换成int类型)
2.public int read(byte b[]) : 从输入流中最多读取b.length个字节数据,并将其存储再数组b中,返回实际读取的字节数
3.public int read(byte b[], int off, int len):从输入流中最多读取len个字节数据,并将其存储再数组b中,且是从数组的off位置开始放数据,返回实际读取的字节数
Reader里的主要方法:
1. public int read():从输入流中读取单个字符,返回读取的字符数据(字符数据可以直接转换成int类型)
2.public int read(char cbuf[]):从输入流中最多读取cbuf.length个字符数据,返回实际读取的字符数
3.public int read(char cbuf[], int off, int len):从输入流中最多读取len个字符数据,并将其存储再数组cbuf中,且是从数组的off位置开始放数据,返回实际读取的字符数
两者相同方法:
1. public long skip(long n) : 将指针向前移动n个字节/字符, 返回实际skip的数据单元
2. public synchronized void mark(int readlimit) :将记录指针当前位置记录一个标记(mark)
3. public boolean markSupported():判断输入流是否支持mark操作,即是否支持记录标记
4. public synchronized void reset():将此流的记录指针重新定位到上次记录标记(mark)的位置
OutputStream 与 Writer 同理
1. void write(int c) : 将指定的字节/字符输出到输出流中,其中c即可以代表字节,也可以代表字符
2. void write(byte[]/char[] buf) : 将字节/字符数组输出到指定的输出流中
3. void write(byte[]/char[] buf, int off, int len) : 将字节/字符数组从off位置开始的len个字节/字符输出到指定的输出流中
Writer还包括的两个方法:
字符流直接以字符位操作单位,所以Writer可以使用字符串来代理字符数组,即以String对象作为参数
1. void write(String str) : 将str字符串包含的字符输出到指定的输出流中
2. void write(String str, int off, int len) : 将str字符串从off位置开始的len个字符输出到指定的输出流中
2.2 输入/输出流构造方法
输入、输出流体系中表示的类众多,不过可以分为两大类:节点流 和 处理流
节点流:直接以物理节点创建的流,直接和底层的I/O设备、文件交互
处理流:以节点流为参数来创建,用于对节点流的包装
以物理节点作为构造器的参数创建流的方式(物理节点可选的方式):
1、字节流 / 字符流 以文件作为物理节点,示例:
FileOutputStream fileOutputStream2 = new FileOutputStream("test2.txt");
FileReader fileReader = new FileReader("test2.txt");
2、字节流 / 字符流 以字节数组 / 字符数组 为物理节点, 示例:
3、字符流 以字符串作为物理节点,示例:
String ss = "hello world";
// 读取
StringReader sr = new StringReader(ss);
// 写入
StringWriter sw = new StringWriter();
sw.write(ss);
输入/输出流体系:
通常来说,字节流的功能比字符流更强大,因为计算机里所有的数据都是二进制的,而字节流可以处理所有的二进制文件。But, 如果使用字节流处理文本文件,则需要把这些字节转换成字符,这就增加了编码的复杂度。
所以一个准则是:如果进行输入/输出的内容是文本内容, 应该考虑用字符流;
如果进行输入/输出的内容是二进制内容,应该考虑用字节流。
思考:如何区分文本内容和二进制内容?
计算机文本通常分为文本文件和二进制文件。所有可以使用记事本打开的文件称为文本文件,反之则是二进制文件。实际上文本文件是一种特殊的二进制文件,只是恰巧该二进制文件的内容能够使用合适的字符集(utf-8、 gbk)解析成字符。
但是更多的情况下我们操作的都是文本文件,所以字符流还是很重要的。
一个简单的示例:
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.PrintStream;
/**
* 存在问题:
* 1、一个中文字符使用两个字节,如果read()方法读取时只读取到了半个中文字符,就会导致乱码
* 2、程序里打开的IO资源不属于内存资源,垃圾回收机制无法回收该资源,所以应该显示的关闭IO资源。
* Java7改写了所有的IO资源,他们本身都实现了AutoClosable接口,因此可以通过自动关闭资源的try语句(try-with-resources)来关闭这些IO流
* 3、直接以物理节点作为构造器的参数创建的流称为字节流,相反,以原有流作为构造器的参数创建的流称为处理流(高级流)
* 在使用处理流包装了底层节点流后,关闭输入/输出资源时,只要关闭最上层的处理流即可,系统会自动关闭 其包装的底层的节点流
*/
public class FileInputStreamTest {
public static void main(String[] args) {
try (FileInputStream fileInputStream = new FileInputStream("/Users/qinwenjing/Documents/Projects/Test/src/main"
+ "/java/com/es/MyRestClient.java");
FileOutputStream fileOutputStream = new FileOutputStream("test.txt");
// 使用处理流直接往输出流中写入一段文字
FileOutputStream fileOutputStream2 = new FileOutputStream("test2.txt");
PrintStream printStream = new PrintStream(fileOutputStream2)) {
byte[] buff = new byte[1024];
int hasRead = 0;
while ((hasRead = fileInputStream.read(buff)) > 0) {
fileOutputStream.write(buff, 0, hasRead);
//System.out.println(new String(buff, 0, hasRead));
}
printStream.println("hello world, 大家好");
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
}
}
三、转换流:
用于实现 字节流 → 字符流,其中
InputStreamReader将 字节输入流 转换成 字符输入流
OutputStreamWriter将 字节输出流 转换成 字符输出流
示例:
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
/**
* BufferedReader:具有缓冲的功能,可以一次读取一行数据,以换行作为标志,如果没有读取到换行,readLine()方法就会被阻塞。
* BufferedReader具有readLine()方法,可以方便的一次读入一行数据,所以疆场把读取文本内容的输入流包装成BufferedReader,用来方便的读取输入流的文本内容
*/
public class KeyInTest {
public static void main(String[] args) {
String line = null;
try ( // 字节流(System.in)转换成字符流(InputStreamReader)
InputStreamReader inputStreamReader = new InputStreamReader(System.in);
// 将普通的Reader包装成BufferedReader
BufferedReader bufferedReader = new BufferedReader(inputStreamReader)) {
while ((line = bufferedReader.readLine()) != null) {
if (line.endsWith("exit")) {
System.exit(1);
}
System.out.println("输出内容:" + line);
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
四、推回输入流:
PushbackInputStream 和 PushbackReader (是处理流)
1. void unread(int b) : 将一个字节/字符推回到缓冲区中,从而允许重复读取刚刚读取的内容
2. void unread(byte[]/char[] buf) : 将一个字节/字符数组内容推回到缓冲区中,从而允许重复读取刚刚读取的内容
3. void unread(byte[]/char[] buf, int off, int len) : 将一个字节/字符数组从off位置开始的len长度内容推回到缓冲区中,从而允许重复读取刚刚读取的内容
而推回输入流每次调用read()方法时,先从推回缓冲区读取数据,只有完全读取了推回缓冲区的内容后,才会从原输入流中读取数据
五、重定向标准输入/输出流:
java的标准 输入/输出流 分别通过System.in和System.out表示,默认情况下他们分别代表键盘和屏幕。
但是,如果还想使用System.out输出数据,但是不输出到屏幕上呢?或者说还想使用System.in输入数据,但是不想通过键盘输入呢?这时候可以对标注输入/输出进行重定向。
示例:
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.PrintStream;
import java.util.Scanner;
public class RedirectTest {
public static void main(String[] args) {
try (PrintStream printStream = new PrintStream(new FileOutputStream("test.txt"));
FileInputStream fileInputStream = new FileInputStream("/Users/qinwenjing/Documents/Projects/Test/src"
+ "/main/java/IO/RedirectTest.java");
) {
// 重定向输出流到printStream
System.setOut(printStream);
/* System.out.println("hello world"); // 发现结果输出到了文件test.txt中
System.out.println(new RedirectTest());*/
// 重定向输入流到printStream
System.setIn(fileInputStream);
Scanner scanner = new Scanner(System.in);
scanner.useDelimiter("\n");
while (scanner.hasNextLine()) {
System.out.println("输出内容是:" + scanner.nextLine());
}
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
}
}
结果会看到没有通过键盘输入任何数据,也没有再屏幕上输出任何东西
其中test.txt文件写入了要写的数据:
六、RandomAccessFile
RandomAccessFile是Java输入输出体系中内容丰富的文件内容访问类。RandomAccessFile对象包含一个记录指针,用以标识当前读写处的位置,当程序创建一个该对象时,该对象的文件指针位于文件头(也就是0),当读/写了n个字节后,文件记录指针向后移动n个字节,另外,RandomAccessFile可以自由的移动记录指针。
优点:
可以定位到文件中的某个位置进行操作,
同时提供的读和写的方法
应用场景:需要读取从某个位置开始的文件; 需要在文件的末尾追加内容
局限:
只能读写文件,不能读写其他IO节点
主要方法:
1. long getFilePointer() : 返回文件记录指针的当前位置
2. void seek(long pos) : 将文件指针定位到pos位置(第pos个字节处)
3. 类似与InputStream的三个read()方法
4. 类似OutputStream的三个write() 方法
5. getChannel() : 返回的FileChannel是只读的还是读写的,取决去RandomAccessFile打开文件的模式。
示例:
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.RandomAccessFile;
public class RandomAccessFileTest {
public static void main(String[] args) {
try(RandomAccessFile ref = new RandomAccessFile
("test.txt", "rw")) {
// 获取RandomAccessFile对象文件指针的位置,初始位置应该为0
System.out.println("RandomAccessFile的文件的初始位置:"+ref.getFilePointer());
// 移动ref的文件记录指针的位置,也就是说从300字节的位置开始读写
ref.seek(300);
byte[] bbuf = new byte[1024];
int hasread = 0;
while((hasread = ref.read(bbuf)) > 0){
System.out.println(new String(bbuf, 0, hasread));
}
ref.seek(ref.length());
ref.write("我是追加的字符哈哈哈".getBytes());
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
}
}
RandomAccessFile可以直接以物理节点创建,是个节点流。
RandomAccessFile和普通输入输出流比对:
1、读写的任意性
普通的输入流读取必须要顺序读取,比如:
hasRead = fileInputStream.read(buff)
line = bufferedReader.readLine()
而RandomAccessFile可以定位一个具体的文件位置进行读写,这是RandomAccessFile的优势
2、内容的追加
普通的输出流写文件时,会覆盖掉原有的文件内容,而RandomAccessFile可以在文件的末尾进行文件的追加
传统IO流总结:
1、读写是以字节为单位(即使不直接处理字节流,但底层的实现还是依赖于字节处理),也就是输入/输出系统一次只能处理一个字节,因此效率注定不高
2、传统的输入/输出流都是阻塞的输入输出,即,如果读写不能拿到数据,现成将会被阻塞。
由于这些原因,才有了NIO
3、到此处,还是没有看出传统IO和NIO的阻塞体现在哪里