Java IO

  输入 / 输出是在主存和外部设备(例如磁盘驱动器 、终端和网络 ) 之间复制数据的过 (I/O) 程:

  • I(输入操作):从 I/O 设备复制数据到主存
  • O(输出操作):从主存复制 数据到 I/O 设备

  在 Java 中, I/O 大概可以分成:

  • 磁盘操作:File
  • 字节操作:InputStream 和 OutputStream
  • 字符操作:Reader 和 Writer
  • 对象操作:Serializable
  • 加强版:NIO

磁盘操作:File 类

  Java 把电脑中的文件和文件夹(目录)抽象为File类(位于java.io包下)
  File类主要用于文件和目录的创建、查找和删除等操作,因此有下面这些方法:

  • 创建文件/文件夹
  • 删除文件/文件夹
  • 获取文件/文件夹
  • 判断文件/文件夹是否存在
  • 对文件夹进行遍历
  • 获取文件的大小

File类是一个与操作系统无关的类,任何操作系统都可以使用此类中的方法。

静态成员变量

  File类中 2 个常用的静态变量:

  • static String separator:与系统有关默认文件名称分隔符(winodw系统中为\\,而linux系统中为/
  • static String pathSeparator:与系统有关的路径分隔符(winodw系统中为;,而linux系统中为:

  以mac系统(类linux系统)获取的路径分隔符和文件分隔符为例:

1
2
3
4
5
6
7
8
9
10
/**
* 获取分隔符
*/
@Test
public void getSeparator() {
String separator = File.separator;
System.out.println("Mac 系统的文件分隔符为 " + separator);
String pathSeparator = File.pathSeparator;
System.out.println("Mac 系统的路径分隔符为 " + pathSeparator);
}

  运行结果:

1
2
Mac 系统的文件分隔符为 /
Mac 系统的路径分隔符为 :

构造方法

  File类常用的构造方法如下:

  • public File(String pathname):通过将给定的路径名的字符串转换为抽象路径来创建新的File对象;
  • public File(String parent,String child):根据parent路径名字符串和child路径名字符串创建新的File对象
  • public File(File parent,String child):根据parent抽象路径名和child路径名字符串创建新的File对象

  File类重写了toString方法,源码如下:

1
2
3
public String toString() {
return getPath();
}

  因此,获取File类的变量,其实返回的是路径字符串。
  对输入的路径pathname而言:

  • 可以是以文件结尾,也可以是以文件夹结尾;
  • 可以是相对路径,也可以是绝对路径;
  • 该路径可以存在,也可以不存在,不会报错;

常用方法

  File类的常用方法按功能可以分为 4 类。

获取相关

  下面列取一下常用的 API :

  • public String getAbsolutePath():返回此File的绝对路径字符串;
  • public String getPath():将此File转换为路径字符串;
  • public String getName():返回此File表示的文件或目录的名称;
  • public long length():返回此File表示的文件或文件夹的长度,若无文件或文件夹则返回0

判断相关

  下面列取一下常用的 API :

  • public boolean exists():判断此File表示的文件或目录是否存在;
  • public boolean isFile():判断此File表示的是否为文件
  • public boolean isDirectory():判断此File表示的是否为目录;

  以Mac电脑为例的demo

1
2
3
4
5
6
7
8
9
10
11
12
13
/**
* 判断文件或文件夹是否存在
*/
@Test
public void getFileOrDirectory() {
File file = new File("/Users/apple/Documents");
boolean exists = file.exists();
System.out.println(exists); // true
boolean directory = file.isDirectory();
System.out.println(directory); // true
boolean isFile = file.isFile();
System.out.println(isFile); // false
}

创建与删除相关

  下面列取一下常用的 API :

  • public boolean createNewFile():只有当该名称的文件不存在时,才创建一个新的空文件;
  • public boolean delete():删除由此File表示的文件或目录;
  • public boolean mkdir():创建由此File表示的单级目录;
  • public boolean mkdirs():创建由此File表示的多级目录

  以mac电脑为例的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
46
47
48
49
50
51
52
53
/**
* 创建新文件并输出其长度,然后删除
*
* @throws IOException
*/
@Test
public void createFile() throws IOException {
File file = new File("/Users/apple/Documents/a.txt");
if (!file.exists()) {
boolean newFile = file.createNewFile();
System.out.println(file); // true
}
if (file.exists()) {
long length = file.length();
System.out.println(length); // 0
boolean delete = file.delete();
System.out.println(delete); // true
}
}

/**
* 创建新目录,然后删除
*/
@Test
public void createDirectory() {
File file = new File("/Users/apple/Documents/TestDirectory");
if ((!file.exists())) {
boolean mkdir = file.mkdir();
System.out.println(mkdir); // true
}
if (file.exists()) {
boolean delete = file.delete();
System.out.println(delete); // true
}
}

/**
* 创建多级目录,想删除一个111的目录
*/
@Test
public void createDirectories() {
File file = new File("/Users/apple/Documents/TestDirectory/111/222/333/abc.txt");
if ((!file.exists())) {
boolean mkdirs = file.mkdirs();
System.out.println(mkdirs); // true
}

File delDir = new File("/Users/apple/Documents/TestDirectory/111");
if (delDir.exists()) {
boolean delete = delDir.delete();
System.out.println(delete); // false
}
}

  我们会发现delete方法默认是删除File最后一个文件分隔符的文件或目录,且其不能删除已经存在文件或文件夹的目录。
  那么我们如何删除这种目录(文件夹)呢?

遍历当前目录文件及文件夹

  下面列取一下常用的 API :

  • public String[] list():返回一个 String 数组,表示当前File目录中的所有子文件或目录的名称;
  • public File[] listFiles:返回一个File数组,表示当前File目录中的所有的子文件或目录的绝对路径名称。

:调用listFilesFile对象表示的必须是实际存在的目录,否则返回null,无法遍历。

  以Mac电脑为例的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
/**
* 创建一个多级目录,并在TestDirectory目录创建abc.txt文件
* 遍历了TestDirectory目录的文件及文件夹
*
* @throws Exception
*/
@Test
public void forDir() throws Exception {
File dirs = new File("/Users/apple/Documents/TestDirectory/111/222/333");
if ((!dirs.exists())) {
boolean mkdirs = dirs.mkdirs();
System.out.println(mkdirs);
}

File fileName = new File("/Users/apple/Documents/TestDirectory/abc.txt");
if (!fileName.exists()) {
boolean newFile = fileName.createNewFile();
System.out.println(newFile);
}
System.out.println("*************************");
File file = new File("/Users/apple/Documents/TestDirectory");
String[] list = file.list();
for (String f : list) {
System.out.println(f);
}
System.out.println("*************************");
File[] files = file.listFiles();
for (File f : files) {
System.out.println(f);
}
}

  运行结果:

1
2
3
4
5
6
7
true
*************************
abc.txt
111
*************************
/Users/apple/Documents/TestDirectory/abc.txt
/Users/apple/Documents/TestDirectory/111

  可以看到,两种不同方式遍历输出的结果不同,list方法输出的是文件或文件夹的名子,而listFiles方法输出的是该文件或文件夹的绝对路径,但它们都只遍历了TestDirectory目录下的文件,没有再去遍历111文件夹下的文件和目录。

  那么如何遍历一个目录下的所有文件及文件夹呢?

遍历目录下所有文件及文件夹

  我们可以通过递归的方式遍历:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
/**
* 遍历目录所有文件及文件夹
*
* @throws Exception
*/
@Test
public void forFilesAndDirs() throws Exception {
File dirs = new File("/Users/apple/Documents/TestDirectory");
getAllFiles(dirs);
}
// 递归遍历
public void getAllFiles(File file) {
File[] files = file.listFiles();
for (File f : files) {
if (f.isDirectory()) {
getAllFiles(f);
} else {
System.out.println(f);
}
}
}

IO 流

什么是流?

  流是一种有顺序的\有起点和终点的字节集合,流是对数据传输的一种抽象。
  数据在两设备之间的传输称之为流,流的本质是数据传输,根据数据传输的特性将流抽象为各种类,方便更直观的进行数据操作。
  《Thinking in Java》一书中这么解释:

:代表任何有能力产出数据的数据源对象或者是有能力接受数据的接收端对象

  简单来讲,流可以为数据源和目的地建立一个输送通道

流的分类

从不同方向考虑 可划分
数据处理 字节流和字符流
数据流向 输入流和输出流

  Java 中流还可以细分为转换流,缓冲流、序列化流等等。

字节流和字符流

  我们知道:一切的数据(文本、图片、视频等等)在存储时,都是以二进制数字的形式保存,而在网络传输时传输的也是字节(8 个二进制位为一个字节)。
  字节流:能传入任意文件(二进制)数据;
  字符流:因为数据编码的不同,而有了对字符进行高效操作的流对象,其本质就是基于字节流读取时,去查指定的编码表(在 Java 中默认使用 Unicode 码表)

  推荐文章:

字节流 VS 字符流

  字节流和字符流有什么区别呢?
  大致有以下区别:
(1)读取单位不同:字节流以一字节( 8 bit )为单位,而字符流则以字符为单位,根据编码表来映射字符,因使用的编码表可能不同,所以一次可能读多个字节。

(2)处理对象不同:字节流能处理所有类型的数据(例如图片,视频,文档),而字符流只能处理字符类型的数据

(3)缓冲区的使用:字节流在操作的时候本身是不会用到缓冲区的,是对文件本身的直接操作。而字符流在操作的时候是会用到缓冲区的,通过缓冲区来操作文件。

  那么,优先使用字节流还是字符流?为什么?
  优先使用字节流,首先因为在硬盘上所有的文件都是以字节的形式进行传输或保存的,如图片视频等内容。但是字符流只是在内存中才会形成,所以在开发中字节流使用的更为广泛。

输入流和输出流

  计算机中,输入/输出( I/O )是在主存和外部设备(例如磁盘、终端、网络)直接拷贝数据的过程。输入操作是从 I/O 设备拷贝数据到主存,而输出操作时从主存拷贝数据到 I/O 设备。
  Java 中,可以通过输入流从数据源中读取数据,通过输出流将数据输出到数据源。
  数据源可以是文件、网络,还可以是键盘。
  在 Java 中:

  • 将文件数据抽象为File
  • 将网络数据抽象为Socket
  • 将输入流输出流抽象为InputStreamOutputStream等许多类

  通过使用这些类库,开发者就能操作各种数据了。

JAVA 中的 IO 流

  详见下图,后面会详细介绍:
Java中的IO流

① 基础流

  上图中,InputStream 、OutputStream 、Reader 、Writer 作为其他流的顶层父类,它们都是抽象类,只提供抽象方法,不能被实例化。
  这些类的方法非常多,此处就不一一列举了。

② 转换流

  转换流是字节流和字符流之间的桥梁,包括:

  • InputStreamReader:字节转字符输入流,可对读取到的字节数据经过指定编码转换成字符
  • OutputStreamWriter:字符转字节输出流,可对读取到的字符数据经过指定编码转换成字节

③ 文件流

  文件流包括:

  • FileInputStream:文件输入流,可以获取源文件中的数据
  • FileOutputStream:文件输出流,可以将数据输出到目的文件中。

  这两个类的构造方法差不多,所以此处只列举一个:

  • FileInputStream(File file):通过打开一个到实际文件的连接来创建一个FileInputStream,该文件通过File类指定。
  • FileInputStream(String name):通过打开一个到实际文件的连接来创建一个FileInputStream,该文件通过文件系统中的路径名name指定。

  下面是一个文件数据复制的例子:

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
/**
* 文件数据复制
*/
@Test
public void testDataCopy() {
FileInputStream in = null;
FileOutputStream out = null;
try {
in = new FileInputStream("g:\\test.txt");
out = new FileOutputStream("g:\\dest.txt");
byte[] data = new byte[1024];
int len = 0;
while ((len = in.read(data)) != -1) {
out.write(data, 0, len);
}
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
} finally {
if (in != null) {
try {
in.close();
} catch (IOException e) {
e.printStackTrace();
}
}
if (out != null) {
try {
out.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}

  上面的例子中,我们将g盘的test.txt文件中的数据都复制到了g盘的dest.txt文件中。

④ 缓冲流

  缓冲流可以提高文件读取输出的效率,其包括:

  • 字节缓冲流:BufferedInputStream,BufferedOutputStream
  • 字符缓冲流:BufferedReader,BufferedWriter

  下面通过例子演示一下字节缓冲流的性能。
  我们通过不同的方法将一个40M的文件从D盘复制到F盘:

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
/**
* 每次复制一个字节
*
* @param origin
* @param destination
* @throws IOException
*/
public static void copyByByte(File origin, File destination) throws IOException {
FileInputStream fs = new FileInputStream(origin);
FileOutputStream fos = new FileOutputStream(destination);
int len = 0;
while ((len = fs.read()) != -1) {
fos.write(len);
}
fos.close();
fs.close();
}

/**
* 每次复制 1024 个字节(最后一次可能存在意外情况)
*
* @param origin
* @param destination
* @throws IOException
*/
public static void copyByByteArray(File origin, File destination) throws IOException {
FileInputStream fs = new FileInputStream(origin);
FileOutputStream fos = new FileOutputStream(destination);
byte[] bytes = new byte[1024];
int len = 0;
while ((len = fs.read(bytes)) != -1) {
fos.write(bytes, 0, len);
}
fos.close();
fs.close();
}

/**
* 以缓冲流的方式每次复制一个字节
*
* @param origin
* @param destination
* @throws IOException
*/
public static void copyByBufferByte(File origin, File destination) throws IOException {
BufferedInputStream bis = new BufferedInputStream(new FileInputStream(origin));
BufferedOutputStream bos = new BufferedOutputStream(new FileOutputStream(destination));
int len = 0;
while ((len = bis.read()) != -1) {
bos.write(len);
}
bos.close();
bis.close();
}

/**
* 以缓冲流每次复制 1024 个字节(最后一次可能存在意外情况)
*
* @param origin
* @param destination
* @throws IOException
*/
public static void copyByBufferByteArray(File origin, File destination) throws IOException {
BufferedInputStream bis = new BufferedInputStream(new FileInputStream(origin));
BufferedOutputStream bos = new BufferedOutputStream(new FileOutputStream(destination));
byte[] bytes = new byte[1024];
int len = 0;
while ((len = bis.read(bytes)) != -1) {
bos.write(bytes, 0, len);
}
bos.close();
bis.close();
}

  时间比较方法之一:

1
2
3
4
5
6
public static void main(String[] args) throws IOException {
long start = System.currentTimeMillis();
copyByBufferByteArray(new File("D:\\TestBuffered.rar"), new File("F:\\TestBuffered5.rar"));
long end = System.currentTimeMillis();
System.out.println(end - start);
}

  各方法的时间对比如下:

方式 时间/毫秒
单字节-无缓冲 159798
1024 字节-无缓冲 313
单字节-有缓冲 1699
1024 字节- 有缓冲 114

  结论:使用缓冲流后的复制操作能大大提高执行速度

⑤ 打印流

  打印流包括PrintStreamPrintWriter.
  特点:

  • 不负责数据源,只负责目的地
  • OutputStream流添加自动刷新功能(仅对OutputStream流有效,必须调用println,printf,format3个方法中的一个)
  • 永远不会抛出IO异常,但可能抛出其他异常

⑥ 序列化流

  序列化流用于操作对象可以将对象写入到文件中,也可以从文件中读取对象

  • ObjectOutputStream:序列化流,作用向流中写入对象的操作流
  • ObjectInputStream: 反序列化流,作用从流中读取对象的操作流

  ObjectOutputStream的构造方法和成员方法如下:

  • public ObjectOutputStream(OutputStream out):创建一个指定的OutputStreamObjectOutputStream
  • void writeObject(Object obj):将指定的对象写入到ObjectOutputStream指定的文件中

  ObjectInputStream的构造方法和成员方法如下:

  • public ObjectInputStream(InputStream in):创建从指定的InputStream读取的ObjectInputStream
  • Object readObject():从ObjectInputStream指定的文件中中读取对象。

什么是序列化与反序列化?

  说明如下:

  • 序列化:能够把一个对象用二进制的表示出来
  • 反序列化:可以通过序列化后的字段还原成这个对象本身。若一个字段不被标识为序列化,则不会被还原

  Java 中的序列化是指将对象转换为字节序列的过程,而反序列化则是将字节序列转换成目标对象的过程。

为什么要序列化/反序列化?

  我们都知道,在进行浏览器访问的时候,我们看到的文本、图片、音频、视频等都是通过二进制序列进行传输的,那么如果我们需要将 Java 对象进行传输,是不是也应该先将对象进行序列化?
  答案是肯定的,我们需要先将 Java 对象进行序列化,然后通过使用与网络相关的 IO 流进行传输,当到达目的地之后,再进行反序列化获取到我们想要的对象,最后完成通信。

Java 序列化的条件

  一个对象若想序列化,需满足一下条件:

  • 该类必须实现java.io.Serializable接口,Serializable是一个标记接口,不实现此接口的类将不会使任何状态序列化或反序列化,会抛出一个NotSerializableException异常
  • 该类的所有属性必须是可序列化的,若有一个属性不需要可序列化,该属性必须注明是瞬态的,用transient关键字修饰即可:

  下面为Serializable接口:

1
public interface Serializable {}

注意

  什么属性不应序列化呢?
  对用户的密码信息等属性应标识其不能被序列化,以防止其在网络传输中被窃取,特别是 Web 程序。

序列化的例子

  首先定义一个可序列化的Person类,实现Serializable接口:

1
2
3
4
5
6
7
8
9
10
11
public class Person implements Serializable {
public String name;
public int age;

public Person(String name, int age) {
this.name = name;
this.age = age;
}

//getter、setter方法省略
}

  编写序列化操作:

1
2
3
4
5
6
@Test
public void test() throws Exception {
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("test.txt"));
oos.writeObject(new Person("张三", 18));
oos.close();
}

  然后可以看到测试项目下多了一个test.txt文件,内容如下:

1
��sr&cn.wk.basicjava.IO.serializable.Person��ˎIageLnametLjava/lang/String;xpt张三

反序列化的例子

  我们将刚刚序列化的文件进行反序列化操作:

1
2
3
4
5
6
7
@Test
public void test2() throws Exception {
ObjectInputStream ois = new ObjectInputStream(new FileInputStream("test.txt"));
Object o = ois.readObject();
System.out.println(o);
ois.close();
}

  运行结果:

1
Person{name='张三', age=18}

Properties 类

  Properties类表示了一个持久的属性集,其可以从输入流中加载相应文件数据将集合写入输出流中并存到相应文件中
  Properties属性列表中每个键及其对应值都是一个字符串

特点

  • Hashtable(线程安全)的子类,map集合中的方法都可以用。
  • 该集合没有泛型。键值都是字符串
  • 它是一个可以持久化的属性集。键值可以存储到集合中,也可以存储到持久化的设备(硬盘、U盘、光盘)上。键值的来源也可以是持久化的设备。

读取文件(后缀名 .properties 的文件)数据并保存到集合

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
	//1.创建集合
Properties properties = new Properties();
//2.创建输入流
InputStream in = null;
try {
//3.设置数据源
in = new FileInputStream("demo.properties");
//4.读取流中数据到集合中
properties.load(in);
//获取键对应的值
properties.get("");

} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
} finally {
try {
//5.关闭流
in.close();
} catch (IOException e) {
e.printStackTrace();
}
}

注意:Java还有一个ResourceBundle类可以读取.properties文件,不过它:

  • 只能读取.properties文件
  • 只能用于读取,无法写入
  • 只能读取类路径写的文件
  • 参数不用写.properties后缀,直接写文件名即可
1
ResourceBundle resource = ResourceBundle.getBundle("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
//1.创建集合
Properties properties = new Properties();

//2.添加数据
properties.setProperty("1号", "小明");
properties.setProperty("2号", "小华");
properties.setProperty("3号", "小红");

//3.创建输出流
OutputStream out = null;
try {
out = new FileOutputStream("out.properties");
//4.将集合数据输出到流的目的文件中
properties.store(out, "描述信息");
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
} finally {
try {
//5.关闭流
out.close();
} catch (IOException e) {
e.printStackTrace();
}
}

一个通俗化的例子

  Java 中将输入输出抽象称为流,就好像水管,将两个容器连接起来。
  所以我们可以把源文件看成一个装了水的水箱,FileInputStream看成一个水管(连接有水的水箱,所以这个水管也有水), 再把目的文件看成另一个水箱,此水箱可能有水也可能没有(本例中它是没水的),有没有好多水这谁知道呢?就叫薛定谔的水箱吧!之后,再把FileOutputStream也看成一个水管(这个水管是空的,连接的是薛定谔的水箱),那么假设条件就建立完成了。

  既然如此,下面看一个文件写入写出的例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
@Test
public void test() {
FileInputStream in = null;
FileOutputStream out = null;
try {
in = new FileInputStream("g:\\test.txt");
out = new FileOutputStream("g:\\get.txt");
byte[] data = new byte[1024];
int len = data.length;
while ((len = in.read(data)) != -1) {
out.write(data, 0, len);
}
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
} finally {
...
in.close();
...
out.close();
...
}
}

  对上面的代码:

  • g:\\test.txt:有水水箱;
  • g:\\get.txt:薛定谔的水箱

  通过一根水管FileInputStream往另一根水管FileOutputStream送水,就好像水从有水水箱通过这个水管再经过另一个水管流向了薛定谔的的水箱

  执行完之后(薛定谔的水箱也有水了了,该水箱是目的文件)

  那么中间发生了什么?有水水管的水是通过什么方法到空水管的呢?

  这个问题先放着,我们先看一下FileInputStream流的 API 相关方法,其构造方法前面介绍过,只是关联一下源文件(水箱),其普通方法如下:

  • int read():从此输入流中读取一个字节;
  • int read(byte[] b):从此输入流中将最多b.length个字节的数据读入一个byte数组中;
  • int read(byte[] b, int off,int len):从此输入流中将最多len个字节读入一个byte数组中;

  原来有三种读取方法呀!它们就相当于有水水管的水流到空水水管的方式。
  对第一种方法而言,每次从输入流中读取一个字节,所以我们可以将其当作每次从有水水管中取一滴,一滴?这样肯定很慢呀!那有没有什么更好的方法呢?
  有啊,第二种方法就更好:从此输入流中将最多b.length个字节的数据读入一个byte数组中,即我们可以通过一个固定大小的小勺子(byte数组)来从水管中取水,可以提高效率。
  第三种方法则可以指定取水的开始位置及结束位置。
  注意哦:较常使用的为第二种方法。
  那么水箱中没有水了咋办?没有了水read方法就返回-1,那我们就可以不用再取水了了,所以一般以-1作为while循环的终止条件。
  下面通过从一个内容为a12test.txt文件中读取数据的例子来证明这点:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
@Test
public void useSingleByte() {
FileInputStream in = null;
try {
in = new FileInputStream("g:\\test.txt");

int i = in.read(); // 从此输入流中读取一个数据字节
System.out.println(i); // 打印第一个读取到的字节
System.out.println((char) i); // 第一个字节数字对应 ASCII 表中的 a

i = in.read(); // 从此输入流中读取一个数据字节
System.out.println(i); // 打印第二个读取到的字节

i = in.read(); // 从此输入流中读取一个数据字节
System.out.println(i); // 打印第三个读取到的字节

i = in.read(); // 从此输入流中读取一个数据字节
System.out.println(i); // 打印第四个读取到的字节(为空所以打印 -1)

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

  运行结果:

1
2
3
4
5
97
a
49
50
-1

  我们知道a对应的字节代表的数字为971对应492对应50,,当读完这 3 个字节后则返回-1

  前面我们知道read方法的返回值是int,int代表什么,数组又具体代表什么?

  看一个从g盘读取一个含abcde内容的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
@Test
public void noCircle() throws IOException{
FileInputStream in = null;
try {
in = new FileInputStream("g:\\testbyteint.txt");
byte[] b = new byte[2];

int len = in.read(b);
System.out.println(new String(b));//ab
System.out.println(len);//2

len = in.read(b);
System.out.println(new String(b));//cd
System.out.println(len);//2

len = in.read(b);
System.out.println(new String(b));//ed
System.out.println(len);//1

len = in.read(b);
System.out.println(new String(b));//ed
System.out.println(len);//-1

} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}finally {
in.close();
}
}

  其中:new String(b)代表把字节数组转成字符串,其 API:

  • String(byte[] bytes):通过使用平台默认字符集解码指定的byte数组,构造一个新的String

  具体的读取过程图示:

  每次读取操作完成后指针都会往后移位:

  • 第一次读取ab两字符时,分别将其放进数组的 2 个位置,指针从开始位置到b位置,读取长度为2
  • 第二次读取cd两字符时,会覆盖掉数组中原先的ab字符,指针会跳到d位置,读取长度也为2
  • 第三次操作只读取e字符(因为之后没有字符,不再往数组中写字符),所以e覆盖数组中的c,而d保持不变,读取长度为1
  • 第四次读取时因为后面没有数据,但是指针还将继续后移并指向结束标记,而结束标记会直接返回-1,此时数组没有变化还是ed

  结论:int代表多少个有效的字节数,而数组起到缓冲的作用,能用来提高效率

代码精简

  程序这么写非常麻烦,所以我们可以通过循环简化代码量:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@Test
public void Circle() throws IOException {
FileInputStream in = null;
try {
in = new FileInputStream("g:\\testbyteint.txt");
byte[] b = new byte[2];
int len = 0;
while ((len = in.read(b)) != -1) {
System.out.print(new String(b));
}
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
} finally {
in.close();
}
}

  这么写的输出结果肯定为abcded,原理刚刚分析过了,那怎么让输出结果变成abcde呢?

  我们知道String还有一个API:

  • String(byte[] bytes,int offset,int length):通过使用平台的默认字符集解码指定的byte数组,构造一个新的String

  该方法可以从数组的offset索引处开始构造长度为length的新String.

  前两次len= 2,即new String(b,0,2),第三次的时候len=1,此时数组b = ed,而最后一次len = -1由于我们加了判断所以不会打印,

  而我们只要d不要e,此时应该new String(b,0,1),那我们把len传递进入就行了呀!

  所以我们代码应该这么写:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@Test
public void Circle() throws IOException {
FileInputStream in = null;
try {
in = new FileInputStream("g:\\testbyteint.txt");
byte[] b = new byte[2];
int len = 0;
while ((len = in.read(b)) != -1) {
System.out.print(new String(b, 0, len));
}
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
} finally {
in.close();
}
}

参考

  • Randal E.Bryant / David O’Hallaron 深入理解计算机系统 [M]. 机械工业出版社,2016
0%