Java I/O

I/O就是输入、输出,我们读取硬盘上的文件、网络文件传输、鼠标键盘输入或者是单片机发回的数据,支持这些操作的设备就是I/O设备

计算机的总线结构:
总线结构
常见的I/O设备一般是鼠标、键盘这类通过USB进行传输的外设或者是通过Sata接口或是M.2连接的硬盘。一般情况下,这些设备是由CPU发出指令通过南桥芯片间接进行控制,而不是由CPU直接操作。

而我们在程序中,想要读取这些外部连接的I/O设备中的内容,就需要将数据传输到内存中。而需要实现这样的操作,单单凭借一个小的程序是无法做到的,而操作系统(如:Windows/Linux/MacOS)就是专门用于控制和管理计算机硬件和软件资源的软件,我们需要读取一个IO设备的内容时,就可以向操作系统发出请求,由操作系统帮助我们来和底层的硬件交互以完成我们的读取/写入请求。

从读取硬盘文件的角度来说,不同的操作系统有着不同的文件系统(也就是文件在硬盘中的存储排列方式,如Windows就是NTFS、MacOS就是APFS),硬盘只能存储一个个0和1这样的二进制数据,至于0和1如何排列,各自又代表什么意思,就是由操作系统的文件系统来决定的。从网络通信角度来说,网络信号通过网卡等设备翻译为二进制信号,再交给系统进行读取,最后再由操作系统来给到程序。

Java提供了一套用于用于IO操作的框架。根据IO流的传输方向和读取单位,分为字节流InputStreamOutputStream以及字符流ReaderWriter的IO框架。通过流,我们就可以一直从流中读取数据,直到读取到尽头,或是不断向其中写入数据,直到我们写入完成,而这类IO就是我们所说的BIO。

字节流一次读取一个字节,也就是一个byte的大小,字符流一次读取一个字符也就是一个char的大小(在读取纯文本文件的时候更加适合)。

文件字节流

我们可以使用FileInputStream来获取文件的输入流:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
package org.ep;

import java.io.FileInputStream;
import java.io.FileNotFoundException;

public class Main {
public static void main(String[] args) {
try { //注意,IO相关操作会有很多影响因素,有可能出现异常,所以需要明确进行处理
FileInputStream inputStream = new FileInputStream("D:\\桌面\\test.txt");
//路径支持相对路径和绝对路径
} catch (FileNotFoundException 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
package org.ep;

import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;

public class Main {
public static void main(String[] args) {
FileInputStream inputStream = null; //定义可以先放在try外部
try {
inputStream = new FileInputStream("D:\\桌面\\test.txt");
} catch (FileNotFoundException e) {
e.printStackTrace();
} finally {
try { //建议在finally中进行,因为关闭流是任何情况都必须要执行的
if (inputStream != null) inputStream.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}

但是因为这种方法过于繁琐,所以在JDK1.7中新增了try-with-resource语法,用于简化这种写法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
package org.ep;

import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;

public class Main {
public static void main(String[] args) {
try (FileInputStream inputStream = new FileInputStream("D:\\桌面\\test.txt")) { //直接在try()中定义要在完成之后释放的资源
//对字节流进行的操作
} catch (IOException e) { //这里变成IOException是因为调用close()可能会出现,而FileNotFoundException是继承自IOException的
e.printStackTrace();
} //无需再编写finally语句块,因为在最后自动帮我们调用了close()
}
}

现在我们拿到了文件的输入流,那么怎么才能读取文件里面的内容呢?我们可以使用read()方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
package org.ep;

import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;

public class Main {
public static void main(String[] args) {
try (FileInputStream inputStream = new FileInputStream("D:\\桌面\\test.txt")) {
int i =inputStream.read();
System.out.println((char) i);
} catch (IOException e) {
e.printStackTrace();
}
}
}
1
H

无参的read()方法会返回一个int值来代表从流里读到的数据:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
package org.ep;

import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;

public class Main {
public static void main(String[] args) {
try (FileInputStream inputStream = new FileInputStream("D:\\桌面\\test.txt")) {
int i =inputStream.read();
System.out.println((char) i);

int j =inputStream.read();
System.out.println((char) j);
} catch (IOException e) {
e.printStackTrace();
}
}
}
1
2
H
e

使用read()方法可以直接读取一个字节的数据,但是流的内容是有限的,读取一个就少一个。如果想一次性全部读取完毕,可以直接使用一个while循环来完成:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
package org.ep;

import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;

public class Main {
public static void main(String[] args) {
try (FileInputStream inputStream = new FileInputStream("D:\\桌面\\test.txt")) {
int tmp;
while ((tmp = inputStream.read()) != -1) {
System.out.println((char) tmp);
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
1
2
3
4
5
6
7
8
9
10
11
H
e
l
l
o

W
o
r
l
d

还可以给read方法添加参数来实现其他功能,如read(byte[] b),通过定义一个byte数组的长度来控制read方法每次读取的字节数量:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
package org.ep;

import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;

public class Main {
public static void main(String[] args) {
try (FileInputStream inputStream = new FileInputStream("D:\\桌面\\test.txt")){
byte[] bytes = new byte[3];
while (inputStream.read(bytes) != -1)
System.out.println(new String(bytes));
} catch (IOException e) {
e.printStackTrace();
}
}
}
1
2
3
4
Hel
lo
Wor
ldr

可以发现输出有一些问题,这是因为我们在test.txt文件中”Hello World”有11个字节,但是我们每次读取三个字节,就会导致:

  • 第一次读取时,byte[]数组中存的内容是:Hel
  • 第二次读取时,byte[]数组中存的内容是:lo
  • 第三次读取时,byte[]数组中存的内容是:Wor
  • 第四次读取时,只能读取两个新的字节,即ld,而第三位没有被更新所以还是ldr

为了解决这个问题,我们可以使用inputStream.read(bytes)返回的字节数,以便只将实际读取的字节转换为字符串,而不是整个 bytes 数组。这将确保只有有效的字符被转换并输出:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
package org.ep;

import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;

public class Main {
public static void main(String[] args) {
try (FileInputStream inputStream = new FileInputStream("D:\\桌面\\test.txt")){
byte[] bytes = new byte[3];
int bytesRead;
while ((bytesRead = inputStream.read(bytes)) != -1) {
System.out.println(new String(bytes, 0, bytesRead));
//创建一个新的字符串,它的内容从 bytes 数组的索引 0 处开始,长度为 bytesRead
//第四次读取时,inputStream.read(bytes)的值为2
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
1
2
3
4
Hel
lo
Wor
ld

使用available方法可以查看当前可读的剩余字节数量:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
package org.ep;

import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;

public class Main {
public static void main(String[] args) {
try (FileInputStream inputStream = new FileInputStream("D:\\桌面\\test.txt")){
System.out.println(inputStream.available());
} catch (IOException e) {
e.printStackTrace();
}
}
}
1
11

也就是说如果我们不知道我们的文件里到底有多少个字节就可以用这种方法输出全部内容:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
package org.ep;

import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;

public class Main {
public static void main(String[] args) {
try(FileInputStream inputStream = new FileInputStream("D:\\桌面\\test.txt")) {
byte[] bytes = new byte[inputStream.available()]; //我们可以提前准备好合适容量的byte数组来存放
System.out.println(inputStream.read(bytes)); //一次性读取全部内容(返回值是读取的字节数)
System.out.println(new String(bytes)); //通过String(byte[])构造方法得到字符串
} catch (IOException e){
e.printStackTrace();
}
}
}
1
2
11
Hello World

但是这种方法仅限于纯文本文件。

也可以控制要读取数量:

1
System.out.println(inputStream.read(bytes, 1, 2));   //第二个参数是从给定数组的哪个位置开始放入内容,第三个参数是读取流中的字节数

注意:一次性读取同单个读取一样,当没有任何数据可读时,依然会返回-1

通过skip()方法可以跳过指定数量的字节:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
package org.ep;

import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;

public class Main {
public static void main(String[] args) {
try (FileInputStream inputStream = new FileInputStream("D:\\桌面\\test.txt")){
System.out.println(inputStream.skip(1));
System.out.println((char) inputStream.read()); //跳过了一个字节
} catch (IOException e)
{
e.printStackTrace();
}
}
}
1
2
1
e

既然有输入流,那么也有输出流:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
package org.ep;

import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;

public class Main {
public static void main(String[] args) {
try (FileOutputStream outputStream = new FileOutputStream("D:\\桌面\\test.txt")){
outputStream.write('A');
} catch (IOException e) {
e.printStackTrace();
}
}
}
1
A

但是这种方法一次只能写入一个字符,要想写入一个字符串,需要借助调用字符串的getBytes()方法将其转化为byte[]数组:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
package org.ep;

import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;

public class Main {
public static void main(String[] args) {
try (FileOutputStream outputStream = new FileOutputStream("D:\\桌面\\test.txt")){
outputStream.write("Hello World!".getBytes());
} catch (IOException e) {
e.printStackTrace();
}
}
}

与输出流相同,我们也可以控制输入的范围:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
package org.ep;

import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;

public class Main {
public static void main(String[] args) {
try (FileOutputStream outputStream = new FileOutputStream("D:\\桌面\\test.txt")){
outputStream.write("Hello World!".getBytes(), 1, 3); //从第1个位置开始写入,共写三位
} catch (IOException e) {
e.printStackTrace();
}
}
}
1
ell

可以在最后执行一次刷新操作(强制写入)来保证数据正确写入到硬盘文件中:

1
outputStream.flush();

我们发现,使用以上的方法会覆盖文件原有的内容,那么想要只在文件尾部进行写入操作,我们需要一个构造方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
package org.ep;

import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;

public class Main {
public static void main(String[] args) {
try (FileOutputStream outputStream = new FileOutputStream("D:\\桌面\\test.txt", true)){ //true参数代表追加写入
outputStream.write("enen".getBytes());
} catch (IOException e) {
e.printStackTrace();
}
}
}
1
Hello Worldenen

利用输入流和输出流,可以轻松实现文件的拷贝:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
package org.ep;

import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;

public class Main {
public static void main(String[] args) {
try (FileInputStream inputStream = new FileInputStream("02-2.mp4");
FileOutputStream outputStream = new FileOutputStream("02.mp4")){
int i;
while ((i = inputStream.read()) != -1) {
outputStream.write(i);
}
} catch (IOException e) {
e.printStackTrace();
}
}
}

但是以上方法拷贝的速度较慢,所以我们采用了另一种方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
package org.ep;

import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;

public class Main {
public static void main(String[] args) {
try (FileInputStream inputStream = new FileInputStream("02-2.mp4");
FileOutputStream outputStream = new FileOutputStream("02-1.mp4")){
byte[] bytes = new byte[1024];
int bytesRead;
while ((bytesRead = inputStream.read(bytes)) != -1) {
outputStream.write(bytes, 0, bytesRead);
}
} catch (IOException e) {
e.printStackTrace();
}
}
}

使用这种方法拷贝会非常快。

文件字符流

字符流不同于字节流,字符流是以一个具体的字符进行读取,因此它只适合读纯文本的文件,如果是其他类型的文件不适用:

1
2
3
4
5
6
7
8
9
10
11
12
13
package org.ep;

import java.io.*;

public class Main {
public static void main(String[] args) {
try (FileReader reader = new FileReader("D:\\桌面\\test.txt")) { //test.txt内容为“测试test”
System.out.println((char) reader.read()); //直接读取一个字符
} catch (IOException e) {
e.printStackTrace();
}
}
}
1

字符流支持char[]类型作为存储:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
package org.ep;

import java.io.*;

public class Main {
public static void main(String[] args) {
try (FileReader reader = new FileReader("D:\\桌面\\test.txt")) {
char[] str = new char[3];
reader.read(str);
System.out.println(str);
} catch (IOException e) {
e.printStackTrace();
}
}
}
1
测试t

Reader相对的,我们可以使用Writer写入:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
package org.ep;

import java.io.*;

public class Main {
public static void main(String[] args) {
try (FileWriter writer = new FileWriter("D:\\桌面\\test.txt", true)) {
writer.getEncoding(); //获取编码
writer.write("噫");
writer.write("唔");
writer.flush();
} catch (IOException e) {
e.printStackTrace();
}
}
}
1
2
//test.txt
测试test噫唔

我们也可以用append()方法来写入,append支持链式调用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
package org.ep;

import java.io.*;

public class Main {
public static void main(String[] args) {
try (FileWriter writer = new FileWriter("D:\\桌面\\test.txt",true)) {
writer.getEncoding(); //获取编码
writer.append("1").append("5");
writer.flush();
} catch (IOException e) {
e.printStackTrace();
}
}
}
1
2
//test.txt
测试test15

我们也可以使用ReaderWriter来拷贝纯文本文件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
package org.ep;

import java.io.*;

public class Main {
public static void main(String[] args) {
try (FileReader reader = new FileReader("test.txt");
FileWriter writer = new FileWriter("copy.txt")) {
char[] chars = new char[3];
int len;
while((len = reader.read(chars)) != -1) {
writer.write(chars, 0, len);
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
1
2
//copy.txt
测试test15

Flie类

File类专门用于表示一个文件或文件夹,只不过只是代表这个文件,但并不是这个文件本身。通过File对象可以更好地管理和操作硬盘上的文件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
package org.ep;

import java.io.*;

public class Main {
public static void main(String[] args) {
File file = new File("test.txt"); //直接创建文件对象,可以是相对路径,也可以是绝对路径
System.out.println(file.exists()); //此文件是否存在
System.out.println(file.length()); //获取文件的大小
System.out.println(file.isDirectory()); //是否为一个文件夹
System.out.println(file.canRead()); //是否可读
System.out.println(file.canWrite()); //是否可写
System.out.println(file.canExecute()); //是否可执行
}
}
1
2
3
4
5
6
true
12
false
true
true
true

此外,我们还可以使用file.mkdir()创建文件夹,或者使用file.createNewFile()创建文件。