Java 文件 IO 详解(下)
在“Java IO详解(上)”这个文章里面我们明确了输入/输出流的4个基类,并且详细了解了4个访问节点流的用法。接下来我们使用跟简单的处理流来访问文件。
1. 处理流的用法
使用处理流的思路是:通过处理流来包装节点流,程序通过处理流来执行输入/输出功能,让节点流与底层的 I/O设备、文件交互。
处理流非常简单,只要流的构造器不是一个物理节点,而是已经存在的流,那这个流就一定是处理流。而节点流都是直接以物理节点作为构造器参数的。下面通过PrintStream处理流来包装OutputStream,感受下处理流的简便:
/**
* Created by 杨Sir on 2017/10/30.
* 处理流的用法:只需在创建处理流时传入节点流即可
*/
public class PrintStreamTest {
public static void main(String[] args){
try (
//创建文件输出流,,节点流
FileOutputStream fos = new FileOutputStream("PrintStreamTest.txt");
//创建处理流
PrintStream ps = new PrintStream(fos))
{
//使用 PrintStream执行输出
ps.println("普通字符串");
//直接使用 PrintStream 输出对象
ps.println(new PrintStreamTest());
} catch (IOException e) {
e.printStackTrace();
}
}
}
通常如果需要输出文本内容,都应该将输出流包装为PrintStream后进行输出。使用了处理流包装了底层节点流后,关闭输入/输出资源时只需要关闭上层处理流即可,底层节点流会自动关闭。
2. 输入/输出流体系
Java 输入/输出流体系提供了40多个类,但却很有规律。 我们来看一看:
分类 | 字节输入流 | 字节输出流 | 字符输入流 | 字符输出流 |
---|---|---|---|---|
抽象基类 | InputStream | OutputStream | Reader | Writer |
访问文件 | FileInputStream | FileOutputStream | FileReader | FileWriter |
访问数组 | ByteArrayInputStream | ByteArrayOutputStream | CharArrayReader | CharArrayWriter |
访问管道 | PipedInputStream | PipedOutputStream | PipedReader | PipedWriter |
访问字符串 | StringReader | StringWriter | ||
缓冲流 | BufferInputStream | BufferOutputStream | BufferReader | BufferWriter |
装换流 | InputStreamReader | OutputStreamWriter | ||
对象流 | ObjectInputStream | ObjectOutputStream | ||
抽象基类 | FilterInputStream | FilterOutputStream | FilterReader | FilterWriter |
打印流 | PrintStream | PrintWriter | ||
推回输入流 | PushbackInputStream | PushbackReader | ||
特殊流 | DataInputStream | DataOutputStream |
注:上面的粗体代表节点流,必须直接与物理节点相连;斜体代表抽象基类,无法创建实例。
通常情况下:如果输入/输出的内容是文本内容,则考虑使用处理流,如果进行输入/输出是二进制文件,那么使用节点流。能用记事本打开的称为文本文件,否则为二进制文件。
字符流还有一个好处就是可以把字符串当做是一个物理节点,用于从字符串读取内容,获将内容写入字符串(用StringBuffer充当字符串)的功能。我们用程序见证一下字符串作为物理输入/输出节点:
/**
* Created by 杨Sir on 2017/10/31.
* 使用字符串作为物理节点的字符输入/输出流的用法
*
* 以字符串作为物理节点时,使用 StringReader和StringWriter进行访问字符串,
* StringWriter写入字符串其实是将字符串写在StringBuffer中,用StringBuffer充当字符串。
*/
public class StringNodeTest {
public static void main(String[] args) {
String src = "既然4个基类访问文件的节点流比较\n" +
"麻烦,那么我们来看看如何使用处理流操\n" +
"作文件。处理流可以隐藏底层节点流之间\n" +
"的差异,想外提供更加方便的输入输出方法。\n";
char[] buf = new char[32];
int hasRead = 0;
try (
StringReader sr = new StringReader(src))
{
while ((hasRead = sr.read(buf)) > 0) {
System.out.println(new String(buf, 0, hasRead));
}
} catch (IOException e) {
e.printStackTrace();
}
/** StringWriter 底层的部分代码。
public StringWriter(int initialSize) {
buf = new StringBuffer(initialSize);
lock = buf;
}
*/
try (
//创建指定的 StringWriter,实际上以一个 StringBuffer 作为输出节点。
StringWriter sw = new StringWriter(20))
{
//调用StringWriter的方法执行输出
sw.write("用处理流来包装节点流,\n");
sw.write("程序通过处理流来进行输入输出,\n");
sw.write("通过节点流与底层的I/O设备进行,\n");
sw.write("如何识别处理流,\n");
System.out.println("-------下面是 sw 字符串节点的内容------");
System.out.println(sw.toString());
}catch (IOException e){
e.printStackTrace();
}
}
}
- 转换流
I/O 体系提供了两个转换流将字节流转为字符流,InputStreamReader和OutputStreamWriter;我以获取键盘输入为例介绍下转换流的用法,Java的System.in代表标准输入,这个标准输入流是InputStream类的实例,键盘输入都是文本内容。
/**
* Created by 杨Sir on 2017/10/31.
* 装换流: InputStreamReader:将字节输出流转换为字符输出流; OutputStreamReader:将字节输出流转换为字符输出流。
*
* BufferedReader流具有缓冲的功能,可以逐行读取,以换行符为标致,没有读到换行符,则程序阻塞,直到读到换行符。
*/
public class KeyinTest {
public static void main(String[] args){
try(
// System.in 标准输入流是InputStream的类的实例。因此可以将 System.in 转换为 Reader对象
InputStreamReader reader = new InputStreamReader(System.in);
//将普通的Reader包装为 BufferedReader
BufferedReader br = new BufferedReader(reader))
{
String line = null;
//采用循环的方式逐行读取
while((line = br.readLine()) != null){
//如果程序读到 "exit",则程序退出
if(line.equals("exit")){
System.exit(1);
}
//打印读取内容
System.out.println("输入内容为:"+line);
}
}catch(IOException e){
e.printStackTrace();
}
}
}
BufferReader流具有缓冲能力,他还可以一行一行读取文本,以换行符为标志,如果没有读到换行符,则程序阻塞。
4. 推回输入流
在I/O 体系,有两个流很与众不同,就是PushbackInputStream和 PushbackReader,他们都提供了三个方法:
void unread(byte[]/char[] buf): 将一个字节或字符数组内容推回到缓冲区,从而允许重复读取刚才的内容。
void unread(byte[]/char[] buf, int off, int len): 将一个字节/字符数组从off开始,长度为len 字节/字符的内容推回到缓冲区,从而允许重复读取刚刚的内容。
void unread(int b): 将一个字节或字符内容推回到缓冲区,从而允许重复读取刚才的内容。
废话不多说,直接看例子:
/**
* Created by 杨Sir on 2017/10/31.
* 推回输入流:PushbackInputStream 和 PushbackReader,两个推回输入流都带有一个推回缓冲区。推回时会将指定数组内容推回到缓冲区,
* 读取时先读取推回到缓冲区中的数据,推回缓冲区内容不够时才从原输入流中读取。推回缓冲区长度默认为 1。
*
* 该程序功能:找出 "new FileReader" 字符串,输出该字符串之前的内容
*/
public class PushbackTest {
public static void main(String[] args){
try(
//创建一个PushbackReader对象,指定推回缓冲区长度为64
PushbackReader pr =
new PushbackReader(new FileReader("D:\\绝对路径PushbackTest.java"),64))
{
char[] buf = new char[32];
//用以保存上次读取的字符串内容
String lastContent = "";
int hasRead = 0;
//循环读取文件内容
while((hasRead = pr.read(buf))>0){
//将读取内容转换为字符串
String content = new String(buf, 0, hasRead);
int targetIndex = 0;
//将上次读取的字符串和这次的连接起来,查看是否包含目标字符串
if((targetIndex = (lastContent+content).indexOf("new FileReader")) > 0){
//将本次内容和上次内容以前推回缓冲区
pr.unread((lastContent+content).toCharArray());
//重新定义一个长度为 targetIndex的数组
if(targetIndex > 32){
buf = new char[targetIndex];
}
//再次读取指定长度的内容
pr.read(buf, 0, targetIndex);
//打印读取的内容
System.out.println(new String(buf, 0, targetIndex));
System.exit(0);
}else{
//打印上次读取的内容,只要没匹配成功,就打印出上次读取的内容,,
//匹配成功后只需打印出 匹配成功的那次的上一次到目标字符串之前 的内容即可。
System.out.println(lastContent);
lastContent = content;
}
}
}catch (IOException e){
e.printStackTrace();
}
}
}
- 重定向标准输入/输出
Java的标准输入/输出分别通过System.in 和 System.out代表,默认情况下是 键盘和显示器。
重定向标准输入/输出: System类里提供了三个重定向标准输入/输出的方法
– static void setErr(PrintStream err): 重定向“标准”错误输出流
– static void setIn(InputStream in): 重定向”标准”输入流
– static void setOut(OutputStream out): 重定向“标准”输出流。
例如:重定向标准输出流,将System.out 输出重定向到文件输出
/**
* Created by 杨Sir on 2017/10/31.
*/
public class RedirectOut {
public static void main(String[] args){
try(
//一次性创建PrintStream输出流
PrintStream ps = new PrintStream(new FileOutputStream("out.txt")))
{
//将标准输出流重定向到ps输出流,,sout的输出将由控制台转到 ps的输出流
System.setOut(ps);
//向标准输出 输出一个字符串
System.out.println("普通字符串");
//向标准输出 输出一个对象
System.out.println(new RedirectOut());
} catch (FileNotFoundException e) {
e.printStackTrace();
}
}
}
- RandomAccessFile(任意位置访问文件)
RandomAccessFile是Java输入输出体系中功能最丰富的文件内容访问类,提供了很多方法访问文件内容,既可以读取文件内容,也可以像文件输出数据,可以是任意位置。它的缺点是只能读写文件,不能读写其他IO节点。
RandomAccessFile提供了两个方法操作文件记录指针:
long getFilePointer(): 返回文件记录指针当前的位置。
void seek(long pos): 将文件记录指针定位到pos位置。
RandomAccessFile的读写文件的方法完全类似于InputStream和OutputStream的read方法和write方法。
RandomAccessFile 有两个构造器,一个使用String参数指定文件名,一个使用File参数指定文件本身;除此之外还要指定一个 mode参数,该参数确定RandomAccessFile的访问方式。
“r”: 以只读的方式打开指定文件。
“rw”: 以读、写的方式打开指定文件。
“rws”:以读、写的方式打开指定文件,相对于“rw”,还要求对文件内容或元数据的每个更新都同步写到底层存储设备。
“rwd”: 以读、写的方式打开指定文件,相对于“rw”,还要求对文件内容的每个更新都同步写到底层存储设备。
还是来个实例爽快,功能:向指定文件的指定位置后面追加内容,为防止覆盖,先要创建一个临时文件保存指定位置后面的数据,插入要插的内容以后,再将临时文件里的内容追加到插入后的内容的后面:
/**
* Created by 杨Sir on 2017/11/1.
* 向指定文件、指定位置插入内容,,并且不覆盖指定位置后面的内容
*
*/
public class InsertContent {
public static void main(String[] args) throws IOException {
insert("out.txt", 5,"恩恩haha我是插入的内容\r\n");
}
public static void insert(String filename , long pos , String content) throws IOException {
//创建临时文件
File tmp = File.createTempFile("emp",null); //默认后缀为 .tmp
tmp.deleteOnExit();
try(
RandomAccessFile raf =
new RandomAccessFile("D:\\IDEARep\\CrazyJavaLiGang\\"+filename,"rw");
//使用临时文件来保持插入点后的数据
FileInputStream tmpfis = new FileInputStream(tmp);
FileOutputStream tmpfos = new FileOutputStream(tmp);
)
{
raf.seek(pos);
//--------将插入点后的数据保持在临时文件
byte[] buf = new byte[64];
int hasRead = 0;
while((hasRead = raf.read(buf)) > 0){
tmpfos.write(buf, 0, hasRead);
}
//插入内容
raf.seek(pos);
raf.write(content.getBytes());
//追加临时文件中的内容
while((hasRead = tmpfis.read(buf)) > 0 ){
raf.write(buf, 0 ,hasRead);
}
}catch (IOException e){
e.printStackTrace();
}
}
}