Java中的IO流

Java中的IO流

作为一段能实现完整的功能的长期使用的程序,能够读写数据是其必须具备的能力,今天就来看一看关于如何实现不同种类的数据的处理

什么是IO,input和output,即输入和输出,这两种流分别负责了对不同的数据的输入与输出,想要掌握好IO流,要涉及的方方面面很多,我就得网上找到的一段总结非常好(以下内容引自博客园的@宜春)

(1)明确要操作的数据是数据源还是数据目的(也就是要读还是要写)
(2)明确要操作的设备上的数据是字节还是文本
(3)明确数据所在的具体设备
(4)明确是否需要额外功能(比如是否需要转换流、高效流等)

如果能实现这些要点,那么一个程序员就真的算的上掌握了IO流

File中的流

为什么要先从文件流讲起?因为事实上是在发展过程中先出现了文件的输入与输出流,后来流的概念被完善才进一步出现了更多的种类的流,这些流其实是以文件流为模版建立的所以我们可以直接先来学习文件流(这部分可以先去看一看我以前写的[Java的文件处理](浅谈Java中的文件处理 - Soul的小站 (soulmate.org.cn))),我们先来创建一个文件输入流

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class Main {
public static void main(String[] args) {
FileInputStream fis = null;
try {//由于流的操作过程中我们可能会遇到各种问题,所以我们要使用try-catch语句来处理异常
fis = new FileInputStream("你的路径");//这里的路径支持绝对路径与相对路径
//接下来进行某些操作
} catch (FileNotFoundException e) {
throw new RuntimeException(e);
} finally {//对于任何流,我们在使用完后都需要关闭
if (fis != null) {
try {
fis.close();
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}
}
}

当然,这里也可以使用try-with-resource的语法

1
2
3
4
5
try(FileInputStream fileInputStream=new FileInputStream("asd")){
//do something
}catch (FileNotFoundException e){
e.printStackTrace();
}

这样,在括号中的内容在try块结束后会被自动结束

那么,我们在得到文件的输入流后可以做哪些操作呢,可以看看源码(下面的源码中我直接将private的部分删除,有兴趣可以自己阅读)

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
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
public class FileInputStream extends InputStream
{

//下面的几个构造方法分别支持用不同的参数构造输入流
public FileInputStream(String name) throws FileNotFoundException {
this(name != null ? new File(name) : null);
}


public FileInputStream(File file) throws FileNotFoundException {
String name = (file != null ? file.getPath() : null);
@SuppressWarnings("removal")
SecurityManager security = System.getSecurityManager();
if (security != null) {
security.checkRead(name);
}
if (name == null) {
throw new NullPointerException();
}
if (file.isInvalid()) {
throw new FileNotFoundException("Invalid file path");
}
fd = new FileDescriptor();
fd.attach(this);
path = name;
open(name);
FileCleanable.register(fd);
}

/
public FileInputStream(FileDescriptor fdObj) {
@SuppressWarnings("removal")
SecurityManager security = System.getSecurityManager();
if (fdObj == null) {
throw new NullPointerException();
}
if (security != null) {
security.checkRead(fdObj);
}
fd = fdObj;
path = null;
fd.attach(this);
}


//返回文件的一个字节(注意是字节而不是字符,所以返回的是int),到达文件末尾返回-1
@Override
public int read() throws IOException {
long comp = Blocker.begin();
try {
return read0();
} finally {
Blocker.end(comp);
}
}

//下面这两个方法返回的是读取的字节的数量
//依照传入的字节数组大小读取文件的字节并将内容保存在传入的字节数组中
@Override
public int read(byte[] b) throws IOException {
long comp = Blocker.begin();
try {
return readBytes(b, 0, b.length);
} finally {
Blocker.end(comp);
}
}


//同样的将字节数保存在传入的数组中,off代表偏移量,即从开头处跳过多少个字节,len是读取多少个字节
@Override
public int read(byte[] b, int off, int len) throws IOException {
long comp = Blocker.begin();
try {
return readBytes(b, off, len);
} finally {
Blocker.end(comp);
}
}
//显然是返回一个包含全部字节的数组
@Override
public byte[] readAllBytes() throws IOException {
long length = length();
long position = position();
long size = length - position;

if (length <= 0 || size <= 0)
return super.readAllBytes();

if (size > (long) Integer.MAX_VALUE) {
String msg =
String.format("Required array size too large for %s: %d = %d - %d",
path, size, length, position);
throw new OutOfMemoryError(msg);
}

int capacity = (int)size;
byte[] buf = new byte[capacity];

int nread = 0;
int n;
for (;;) {
// read to EOF which may read more or less than initial size, e.g.,
// file is truncated while we are reading
while ((n = read(buf, nread, capacity - nread)) > 0)
nread += n;

// if last call to read() returned -1, we are done; otherwise,
// try to read one more byte and if that fails we're done too
if (n < 0 || (n = read()) < 0)
break;

// one more byte was read; need to allocate a larger buffer
capacity = Math.max(ArraysSupport.newLength(capacity,
1, // min growth
capacity), // pref growth
DEFAULT_BUFFER_SIZE);
buf = Arrays.copyOf(buf, capacity);
buf[nread++] = (byte)n;
}
return (capacity == nread) ? buf : Arrays.copyOf(buf, nread);
}
//读取指定数量的字节并将返回一个数组
@Override
public byte[] readNBytes(int len) throws IOException {
if (len < 0)
throw new IllegalArgumentException("len < 0");
if (len == 0)
return new byte[0];

long length = length();
long position = position();
long size = length - position;

if (length <= 0 || size <= 0)
return super.readNBytes(len);

int capacity = (int)Math.min(len, size);
byte[] buf = new byte[capacity];

int remaining = capacity;
int nread = 0;
int n;
do {
n = read(buf, nread, remaining);
if (n > 0) {
nread += n;
remaining -= n;
} else if (n == 0) {
// Block until a byte is read or EOF is detected
byte b = (byte)read();
if (b == -1 )
break;
buf[nread++] = b;
remaining--;
}
} while (n >= 0 && remaining > 0);
return (capacity == nread) ? buf : Arrays.copyOf(buf, nread);
}
//直接将读到的全部内容放到传入的输出流中并进行输出
@Override
public long transferTo(OutputStream out) throws IOException {
long transferred = 0L;
if (out instanceof FileOutputStream fos) {
FileChannel fc = getChannel();
long pos = fc.position();
transferred = fc.transferTo(pos, Long.MAX_VALUE, fos.getChannel());
long newPos = pos + transferred;
fc.position(newPos);
if (newPos >= fc.size()) {
return transferred;
}
}
try {
return Math.addExact(transferred, super.transferTo(out));
} catch (ArithmeticException ignore) {
return Long.MAX_VALUE;
}
}



//跳过一定的字节
@Override
public long skip(long n) throws IOException {
long comp = Blocker.begin();
try {
return skip0(n);
} finally {
Blocker.end(comp);
}
}
//获得一个估算的总字节量,通常情况下在本地读写的情况下这个值是准确的,在网络传输的情况下这个值会出现各种意外
@Override
public int available() throws IOException {
long comp = Blocker.begin();
try {
return available0();
} finally {
Blocker.end(comp);
}
}

//关闭输入流
@Override
public void close() throws IOException {
if (closed) {
return;
}
synchronized (closeLock) {
if (closed) {
return;
}
closed = true;
}

FileChannel fc = channel;
if (fc != null) {
// possible race with getChannel(), benign since
// FileChannel.close is final and idempotent
fc.close();
}

fd.closeAll(new Closeable() {
public void close() throws IOException {
fd.close();
}
});
}
//返回一个文件描述符对象,不用管
public final FileDescriptor getFD() throws IOException {
if (fd != null) {
return fd;
}
throw new IOException();
}

//返回文件管道,还是不用管
public FileChannel getChannel() {
FileChannel fc = this.channel;
if (fc == null) {
synchronized (this) {
fc = this.channel;
if (fc == null) {
this.channel = fc = FileChannelImpl.open(fd, path, true,
false, false, this);
if (closed) {
try {
// possible race with close(), benign since
// FileChannel.close is final and idempotent
fc.close();
} catch (IOException ioe) {
throw new InternalError(ioe); // should not happen
}
}
}
}
}
return fc;
}

}

如果你去看源码,会发下有很多有native关键字的方法,这些方法通过c或c++实现,所以看不懂属于正常情况

解下来是文件的输出流

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
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127

public class FileOutputStream extends OutputStream
{
//这里的几个构造方法中含有append形参的是指如果文件名已经存在,true则在原文件后末尾写入新数据,默认的false则会覆盖源文件
public FileOutputStream(String name) throws FileNotFoundException {
this(name != null ? new File(name) : null, false);
}


public FileOutputStream(String name, boolean append)
throws FileNotFoundException
{
this(name != null ? new File(name) : null, append);
}


public FileOutputStream(File file) throws FileNotFoundException {
this(file, false);
}

//关于File类可以去看我之前写的文件处理
public FileOutputStream(File file, boolean append)
throws FileNotFoundException
{
String name = (file != null ? file.getPath() : null);
@SuppressWarnings("removal")
SecurityManager security = System.getSecurityManager();
if (security != null) {
security.checkWrite(name);
}
if (name == null) {
throw new NullPointerException();
}
if (file.isInvalid()) {
throw new FileNotFoundException("Invalid file path");
}
this.fd = new FileDescriptor();
fd.attach(this);
this.path = name;

open(name, append);
FileCleanable.register(fd); // open sets the fd, register the cleanup
}


public FileOutputStream(FileDescriptor fdObj) {
@SuppressWarnings("removal")
SecurityManager security = System.getSecurityManager();
if (fdObj == null) {
throw new NullPointerException();
}
if (security != null) {
security.checkWrite(fdObj);
}
this.fd = fdObj;
this.path = null;

fd.attach(this);
}

//将对应的字节写入文件
@Override
public void write(int b) throws IOException {
boolean append = FD_ACCESS.getAppend(fd);
long comp = Blocker.begin();
try {
write(b, append);
} finally {
Blocker.end(comp);
}
}

//将整个传入的字符数组写入文件
@Override
public void write(byte[] b) throws IOException {
boolean append = FD_ACCESS.getAppend(fd);
long comp = Blocker.begin();
try {
writeBytes(b, 0, b.length, append);
} finally {
Blocker.end(comp);
}
}

//将传入的数组偏移off个字节后写入len个
@Override
public void write(byte[] b, int off, int len) throws IOException {
boolean append = FD_ACCESS.getAppend(fd);
long comp = Blocker.begin();
try {
writeBytes(b, off, len, append);
} finally {
Blocker.end(comp);
}
}

//关闭输出流
@Override
public void close() throws IOException {
if (closed) {
return;
}
synchronized (closeLock) {
if (closed) {
return;
}
closed = true;
}

FileChannel fc = channel;
if (fc != null) {
// possible race with getChannel(), benign since
// FileChannel.close is final and idempotent
fc.close();
}

fd.closeAll(new Closeable() {
public void close() throws IOException {
fd.close();
}
});
}



}

现在我们试着写一个简单的复制文件的程序

1
2
3
4
5
6
7
8
9
10
11
12
13
public class Main {
public static void main(String[] args) {
try(FileInputStream fileInputStream=new FileInputStream("E:\\1.txt");
FileOutputStream fileOutputStream=new FileOutputStream("E:\\2.txt")){
fileInputStream.transferTo(fileOutputStream);
fileOutputStream.flush();//这里是指刷新缓存区,确保所有内容写入
}catch (FileNotFoundException e){
e.printStackTrace();
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}

看,是不是非常简洁

这里插播一个小知识点,所有的文件类型本质上都是一串又一串的0与1,而每个文件的开头都会有一个事先约定好的文件类型标识,这一标识是一串固定的数字计算机通过这一串的数字来决定选用什么样的解析规则将接下来的内容转换为人类可以理解的内容,所以只要输入与输出是同一个文件类型就可以正常复制

但是这种流存在一个问题如果我们希望对文件进行某种处理,那么由于人是无法直接理解字节内容的,所以我们对内容的操作就特别困难,所以除了字节流以外还存在另一种字符流,当然实际上这东西用的不是特别多所以我写的简单一点

1
2
3
4
5
6
7
8
public static void main(String[] args) {
try(FileReader reader = new FileReader("test.txt")){
reader.skip(1); //现在跳过的是一个字符
System.out.println((char) reader.read()); //现在是按字符进行读取,而不是字节,因此可以直接读取到中文字符
}catch (IOException e){
e.printStackTrace();
}
}

简单的读取,需要注意的是read返回的值实际上是char类型的数组

1
2
3
4
5
6
7
8
9
10
public static void main(String[] args) {
try(FileWriter writer = new FileWriter("output.txt")){
writer.getEncoding(); //支持获取编码(不同的文本文件可能会有不同的编码类型)
writer.write('牛');
writer.append('牛'); //其实功能和write一样
writer.flush(); //刷新
}catch (IOException e){
e.printStackTrace();
}
}

写入文件,显然是很简单的,唯一需要注意的是不要关于File类的使用,可以去看看我之前写的文件处理,也可以自己读一读File类的源码

搞定了这些一般的文件读写其实就没什么难点了

缓冲流

解下来我们再看一看缓冲流,其实缓冲流的输入与输出本质上还是使用了普通的输入与输出流的方法来进行数据的操作,但是缓冲流添加了缓冲的过程,将内容先预加载到内存中的缓冲区中,当内容需要多次使用时可以省去反复的向外部设备读取的时间,提高程序的运行效率.

缓冲流是对应的字符/字节流的子类,所以字符字节流怎么用对应的缓冲流就怎么用,而在创建对应的缓冲流时可以直接将原来的输入输出流作为参数传入,就像这样

1
BufferedInputStream bufferedInputStream=new BufferedInputStream(fileInputStream);

同样的不要忘记关闭流(只需要关闭缓冲流,对应的输入输出流同时会被关闭)

接下来请考虑一个问题,正常的输入流和输出流可以反复读取已经读取完成的内容吗(在较老版本的jdk中可以但不推荐),事实上是不存在这样的方法的.数据的读取过程实际上是一个指针的移动过程,是不可逆的.但是在缓冲输入流中却实现了这样的方法,,因为在缓冲流中数据被一字节或字符数组的形式保存在内存中,数组当然能做到随机查询了

看两个方法

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
 public void mark(int readlimit) {
if (lock != null) {
lock.lock();
try {
implMark(readlimit);
} finally {
lock.unlock();
}
} else {
synchronized (this) {
implMark(readlimit);
}
}
}//对当前的位置进行标记,接下来只允许读到后readlimit个字节
public void reset() throws IOException {
if (lock != null) {
lock.lock();
try {
implReset();
} finally {
lock.unlock();
}
} else {
synchronized (this) {
implReset();
}
}
}将读取指针回退到最后一次使用mark处

用这两个方法就可以实现对同一段内容的多次读入

转换流

在网络传输时所有的传输都是通过字节流的方式进行的,但受到信息进行展示时我们需要将这些数据转换为字符,该怎么做呢,其实不同的包中提供了很多方法实现,我这里提供一种最常用的–转换流

1
2
3
4
5
6
7
public static void main(String[] args) {
try(OutputStreamWriter writer = new OutputStreamWriter(new FileOutputStream("test.txt"))){ //虽然给定的是FileOutputStream,但是现在支持以Writer的方式进行写入
writer.write("lbwnb"); //以操作Writer的样子写入OutputStream
}catch (IOException e){
e.printStackTrace();
}
}
1
2
3
4
5
6
7
public static void main(String[] args) {
try(InputStreamReader reader = new InputStreamReader(new FileInputStream("test.txt"))){ //虽然给定的是FileInputStream,但是现在支持以Reader的方式进行读取
System.out.println((char) reader.read());
}catch (IOException e){
e.printStackTrace();
}
}

非常的简单

对象流

对象流提供了将对象直接进行传输的方法,使用这种流可以直接将数据转换为一个对象,下面给一个例子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class Main {
public static void main(String[] args) {
try (ObjectOutputStream outputStream = new ObjectOutputStream(new FileOutputStream("output.txt"));
ObjectInputStream inputStream = new ObjectInputStream(new FileInputStream("output.txt"))){
People people = new People("aaa");
outputStream.writeObject(people);
outputStream.flush();
people = (People) inputStream.readObject();
System.out.println(people.name);
}catch (IOException | ClassNotFoundException e) {
e.printStackTrace();
}
}
}

当然,要求传输的对象必须实现序列化

1
2
3
4
5
6
ublic class People implements Serializable {//只要使用了这个接口就可以实现序列化
String name;
public People(String name) {
this.name = name;
}
}

关于序列化到底是什么东西,我们会在将来的网络编程部分学到(注意,序列化后的内容人类无法直接阅读,所以输出的会是一串乱码),但读取时自带的反序列化可以将数据还原

如果我们希望对象的某个属性不被传输,则添加transient关键字,就像下面这样

1
2
3
4
5
6
7
ublic class People implements Serializable {
transient String name;
public People(String name) {
this.name = name;
}

}

此时的name就无法被序列化,数据传输时也不会包含name.如果你有阅读部分源码,就会发现很多源码中就有这个关键字用来避免被序列化

结语

其实流还有好几种,例如打印流,数据流等,但是并不是很常用,所以我就没有写,有兴趣的可以自行了解

放张图


Java中的IO流
http://soulmate.org.cn/2024/12/27/Java中的IO流/
作者
Soul
发布于
2024年12月27日
许可协议