Java输入与输出补充介绍

小tips:本文省略了FIle类的讲解,还有对一开始的简单字节,字符流都进行了简化讲解,如果你需要更基础的入门,可以参考我的知乎文章:IO流

Java输入/输出

字节流和字符流

你可以把Java的输出和输入流想象为我们在一个竹筒中取水,向竹筒中把水倒出来就是输出流。字节流的话就是一滴一滴地流出,字符流的话就是多滴多滴流出来。输入也是相同的,我们向竹筒中注水,字节流就是一滴一滴地注入,字符流就是多滴多滴地注入。

演示输出流

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
import java.io.FileInputStream;
import java.io.IOException;

public class FileInputStreamTest {
public static void main(String[] args) throws IOException {

//创建字节输入流
FileInputStream fis = new FileInputStream("FileInputStreamTest.java");

//创建一个长度为1024的“竹筒”
byte[] bbuf = new byte[1024];

//用于保存实际读取的字节数
int hasRead = 0;

//使用循坏重复“取水”过程
while((hasRead = fis.read(bbuf))>0) {

//取出竹筒中的水滴(字节),将字节数组转换成字符串输入
System.out.println(new String(bbuf,0,hasRead));
}

//关闭文件输入流,放在finally块中更安全
fis.close();
}
}

演示输入流

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
import java.io.FileInputStream;
import java.io.FileOutputStream;

public class FileOutputStreamTest {
public static void main(String[] args) {
try(
//创建字节输入流
FileInputStream fis = new FileInputStream("FileOutputStreamTest.java");

//创建字节输出流
FileOutputStream fos = new FileOutputStream("newFile.txt")
) {
byte[] bbuf = new byte[32];
int hasRead = 0;

//循环从输入流中取出数据
while((hasRead = fis.read(bbuf))>0) {

//每读取一次,即写入文件输出流,读了多少,就写多少
fos.write(bbuf,0,hasRead);

}

} catch (Exception e) {
e.printStackTrace();
}
}
}

输入流/输出流体系

缓冲流,转换流,对象流均需要在黑色标注的基础流的基础上使用。相当于是对基础流的一层封装,让其实现更多功能。这里的话只对特殊的类(访问字符串,推回输入流),接触比较少的类进行介绍。其他类的话可以参考我的知乎文档。

Java中关于流的类

打印流

演示打印流的程序,我们可以看到PrintStream ps = new PrintStream(fos)是建立在访问文件的字节输入流中的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import java.io.FileOutputStream;
import java.io.PrintStream;

public class PrintStreamTest {
public static void main(String[] args) {
try(
FileOutputStream fos = new FileOutputStream("test.txt");
PrintStream ps = new PrintStream(fos)
){

//使用PrintStream执行输出
ps.print("普通字符串");

//直接使用PrintStream输出对象
ps.println(new PrintStreamTest());

} catch (Exception e) {
e.printStackTrace();
}
}
}

读取字符串

其中字符流中有一个比较特殊的流—StringWriter/StringRead

演示此类的demo程序,此流可以通过字符串来当作它的节点。不过用法上与其他流并无不同

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
import java.io.StringReader;
import java.io.StringWriter;

public class StringNodeTest {
public static void main(String[] args) {

String src = "从明天起,做一个幸福的人\n"
+ "喂马,劈柴,周游世界\n"
+ "从明天起,关心粮食和蔬菜\n"
+ "我有一所 房子,面朝大海,春暖花开\n"
+ "从明天起,和每一个亲人通信\n"
+ "告诉他们我的幸福\n";

char[] buffer = new char[32];
int hasRead = 0;

try(
StringReader sr = new StringReader(src)) {

//采用循环读取的方式读取字符串
while((hasRead = sr.read(buffer))>0) {
System.out.println(new String(buffer,0,hasRead));
}
} catch (Exception e) {
e.printStackTrace();
}

try(
//创建StringWriter时,实际上以一个StringBuffer作为输出节点
//下面指定的20就是StringBuffer的初始长度
StringWriter sw = new StringWriter(20)
) {

//调用StringWriter的方法执行输出
sw.write("有一个美丽的新世界,\n她在远方等我,\n那里有天真的孩子,\n还有姑娘的酒窝\n");
System.out.println("----下面是sw字符串节点里的内容----");

//使用toString()方法返回StringWriter字符串节点的内容
System.out.println(sw.toString());
} catch (Exception e) {
e.printStackTrace();
}
}
}

推回输入流

PushbackInputSteam 和 PushbackReader,他们都提供了三个特殊的方法

方法 描述
void unread( byte[]/ char[] buf) 将一个字节/字符数组内容推回到推回缓冲区里,从而允许重复读取刚刚读取的内容。
void unread( byte[]/ char[] b, int off, int len) 将 一个 字节/ 字符 数组 里 从 off 开始, 长度 为 len 字节/ 字符 的 内容 推 回到 推 回 缓冲 区里, 从而 允许 重复 读取 刚刚 读取 的 内容。
void unread( int b) 将 一个 字节/ 字符 推 回到 推 回 缓冲 区里, 从而 允许 重复 读取 刚刚 读取 的 内容。

是不是和InpuStream和Reader的read()方法很像,没错这三个方法就相当于反着的read。

通过这个方法,我们可以把一些数据重新冲入缓冲区,使用这个流的read方法时,会先把缓冲区的数据拿出来以后在从原输入流中读取数据。

下面的程序通过这个特殊的流实现了复制本代码程序一遍的功能

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
import java.io.FileNotFoundException;
import java.io.FileReader;
import java.io.IOException;
import java.io.PushbackReader;

public class PushbackTest {
public static void main(String[] args) {
try(
//创建一个PushbackReader对象,指定推回缓冲区的长度为64
PushbackReader pr = new PushbackReader(new FileReader(
"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 PushbackReader"))>0) {

//将本次内容和上次内容一起推回缓冲区
pr.unread((lastContent + content).toCharArray());

//重新定义一个长度为targetIndex的char数组
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 (FileNotFoundException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
};
}
}

转换流

这两个特殊的流可以把字节流转换为字符流,当然是没有字符流转换成字节流的特殊流啦。

下面的程序通过包装实现把System.in(标准输入),然后在通过缓冲流实现一次读取一行的操作

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.io.Reader;

public class KeyinTest {
public static void main(String[] args) {
try(
//将system.in对象转换成Reader对象
Reader 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 (Exception e) {
e.printStackTrace();
}
}
}

重定向标准输入/输出流

上一节讲到,通过包装System.in来实现一次读取一行的操作。默认情况下,System.in和System.out分别代表键盘和显示器,在System类中提供了如下方法可以重新定义输入的对象,比如可以实现把输出定向到文件输出,而不是在屏幕上输出。

方法 描述
static void setErr( PrintStream err) 重定向标准错误流
static void setIn( InputStream in) 重定向标准输入流
static void setOut( PrintStream out) 重定向标准输出流

重定向输入到文件代码demo

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
import java.io.FileOutputStream;
import java.io.PrintStream;

public class RedirectOut {
public static void main(String[] args) {
try(
//一次性创建PrintStream输出流
PrintStream ps = new PrintStream(new FileOutputStream("out.txt"))
) {

//将标准输出重定向到ps输出流
System.setOut(ps);

//向标准输出输出一个字符串
System.out.println("普通字符串");

//向标准输出输出一个对象
System.out.println(new RedirectOut());

} catch (Exception e) {
e.printStackTrace();
}
}
}

从文件中读取输入,而不是键盘

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
import java.io.FileInputStream;
import java.util.Scanner;

public class RedirectIn {
public static void main(String[] args) {
try(
FileInputStream fis = new FileInputStream("RedirectIn.java")){

//将标准输入重定向到fis输入流
System.setIn(fis);

//使用System.in创建Scanner对象,用于获取输入
Scanner sc = new Scanner(System.in);

//增加下面一行只把回车作为分隔符
sc.useDelimiter("\n");

//判断是否还有下一项输入
while(sc.hasNext()) {

//输出输入项
System.out.println("键盘输入的内容是:"+sc.next());
}
} catch (Exception e) {
e.printStackTrace();
}
}
}

java虚拟机读写其他进程的数据

使用Runtime对象的exec()方法可以运行平台上的其他程序,该方法产生一个Process对象,这个对象代表由Java程序启动的子进程。这个类提供了如下方法用于让程序之间进行通信。

方法 描述
InputStream getErrorStream() 获取子进程的错误流
InputStream getInputStream() 获取子进程的输入流。
OutputStream getOutputStream() 获取子进程 的输出流。

注意此处的输入输出均是以Java程序的角度来看,给Java程序的数据就是输入,Java程序给出的数据就是输出。

下面程序示范了读取其他进程(javac)的输出信息

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;

public class ReadFromProcess {
public static void main(String[] args) throws IOException {

//运行javac命令,返回运行该命令的子进程
Process p = Runtime.getRuntime().exec("javac");
try(
//以p进程的错误流创建BufferedReader对象
//这个错误流对本程序是输入流,对p则是输出流
BufferedReader br = new BufferedReader(new
InputStreamReader(p.getErrorStream()))
){
String buff = null;

//采取循环方式来读取p进程的错误输出
while((buff = br.readLine())!= null) {
System.out.println(buff);
}
}
}
}

下面程序演示了能帮我们运行Java程序的Java程序,以下程序运行后,会产生一个out.txt文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.PrintStream;
import java.util.Scanner;

public class WriteToProcess {
public static void main(String[] args) throws IOException {

//运行java ReadStandard命令,返回运行该命令的子进程
Process p = Runtime.getRuntime().exec("java ReadStandard");

try(
//以p进程的输出流创建PrintStream对象
//这个输出流对本程序是输出流,对p进程则是输入流
PrintStream ps = new PrintStream(p.getOutputStream())){

//向ReadStandard程序写入内容,这些内容将被ReadStandard读取
ps.println("普通字符串");
ps.println(new WriteToProcess());

} catch (Exception e) {
e.printStackTrace();
}
}
}

//定义一个ReadStandard类,该类可以接受标准输入
//并将该标准输入写入out.txt文件
class ReadStandard{
public static void main(String[] args) {
try(
//使用System.in创建Scanner对象,用于获取标准输入
Scanner sc = new Scanner(System.in);
PrintStream ps = new PrintStream(new FileOutputStream("out.txt"))
) {

//增加下面一行只把回车作为分隔符
sc.useDelimiter("\n");

//判断是否还有下一个输入项
while(sc.hasNext()) {

//输出输入项
ps.println("键盘输入的内容是:"+sc.next());
}

} catch (Exception e) {
e.printStackTrace();
}
}
}

RandomAccessFile(Java中功能丰富地读取文件类)

RandomAccessFile是Java中读取文件比较灵活的类,他能够自由定义从哪里开始读取文件,读取到哪里结束。比较特殊的是包含了两个方法来移动我们读取文件的指针

方法 描述
long getFilePointer() 返回文件记录指针的当前位置
void seek( long pos) 将文件记录指针定位到 pos 位置。

其他使用方法就和流的类似。还有一个特殊的点在于这个类的构造器,其中除了指定文件名以外还有一个参数要指定访问这个文件的mode,使用Linux系统的小伙伴就比较熟悉了。

描述
r 以只读方式打开指定文件。 如果试图对该RandomAccessFile执行写入方法,都将抛出 IOException异常。
rw 以读、写方式打开指定文件。 如果该文件尚不存在,则尝试创建该文件。
rws 以读、写方式打开指定文件。相对于” rw” 模式,还要求对文件的内容或元数据的每个更新都同步写入 到底层存储设备。(不经常用,主要我也不太懂)
rwd 以读、 写方式打开指定文件。相对于” rw” 模式,还要求对文件内容的每个更新都同步写入到底层存储设备。(不经常用,主要我也不太懂)

下面的程序可以访问文件的特定部分

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
import java.io.RandomAccessFile;

public class RandomAccessFileTest {
public static void main(String[] args) {
try(
RandomAccessFile raf = new RandomAccessFile("RandomAccessFileTest.java", "r"))
{
//获取RandomAcessFile对象文件指针的位置,初始位置是0
System.out.println("RandomAcessFile的文件指针的初始位置"
+ raf.getFilePointer());

//移动raf的文件记录指针的位置
raf.seek(300);
byte[] bbuf = new byte[1024];

//用于保存实际读取的字节数
int hasRead = 0;

//使用循环来重复取水的过程
while((hasRead = raf.read(bbuf))>0) {

//取出竹筒中的水滴(字节),将字节数组转换成字符串输入
System.out.println(new String(bbuf,0,hasRead));
}

} catch (Exception e) {
e.printStackTrace();
}
}
}

下面的程序示范了如何向指定的文件后追加内容

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import java.io.RandomAccessFile;

public class AppendContent {
public static void main(String[] args) {
try(
//以读、写方式打开一个RandomAccessFile对象
RandomAccessFile raf = new RandomAccessFile("out.txt","rw"))
{
//将记录指针移动到out.txt文件的最后
raf.seek(raf.length());
raf.write("追加的内容!\n".getBytes());

} catch (Exception e) {
e.printStackTrace();
}
}
}

下面程序实现了向指定文件、指定位置插入内容的功能

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.RandomAccessFile;

public class InsertContent {
public static void insert(String fileName,long pos,String insertContent) throws IOException {

//创建一个临时文件,该临时文件会在JVM退出时被删除
File tmp = File.createTempFile("tmp", null);
tmp.deleteOnExit();
try(
RandomAccessFile raf = new RandomAccessFile(fileName, "rw");
//使用临时文件来保存 插入点后的数据
FileOutputStream tmpOut = new FileOutputStream(tmp);
FileInputStream tmpIn = new FileInputStream(tmp))
{
raf.seek(pos);
// ------下面代码将插入点后的内容读入临时文件中保存
byte[] bbuf = new byte[64];

//用于保存实际读取的字节数
int hasRead = 0;

//使用循环方式读取插入点后的数据
while((hasRead = raf.read(bbuf))>0) {

//将读取的数据写入临时文件
tmpOut.write(bbuf,0,hasRead);
}

//-------下面的代码用于插入内容
//把文件记录指针重新定位到pos位置
raf.seek(pos);

//追加需要插入的内容
raf.write(insertContent.getBytes());

//追加临时文件中的内容
while((hasRead = tmpIn.read(bbuf))>0) {
raf.write(bbuf,0,hasRead);
}

} catch (Exception e) {
e.printStackTrace();
}
}

public static void main(String[] args) throws IOException {
insert("InsertContent.java",45,"插入的内容\r\n");
}
}

这个类给我们的启示是:我们可以用这个来实现类似于断点续传的问题。具体的示例我可以会放在我多线程的文章中。