IO流与文件

IO流

第一节 简介

1.1 IO

IO 就是指应用程序对这些设备的数据输入与输出,Java语言定义了许多类专门负责各种方式的输入、输出,这些类都被放在 java.io 包中。

1.2 流

流可以根据传输的基本单位分为字节流字符流,字节流一般以Stream命名,字符流则以Reader/Writer命名。计算机传输本质上都是字节流,字符流只是不需要再手动指定一次编码格式。以下直接用流来指代字节流。

能够读出或写入一个字节序列的对象分别叫输入流输出流,这些对象可以是文件,网络连接或内存块等。

InputStreamOutputStream字节流设计,ReaderWriter字符流设计,字节流和字符流形成分离的层次结构。一般来说,处理字符字符串使用字符流类,处理字节二进制对象使用字节流。

操作文件流时,不管是字符流还是字节流,都可以按照以下方式进行:

  1. 使用File类找到一个对象。
  2. 通过File类的对象去实例化字节流或字符流的子类。
  3. 进行字节(字符)的读、写操作。
  4. 关闭文件流。

1.3 File

  • File类是IO包中唯一代表磁盘文件本身的对象,File类定义了一些与平台无关的方法来操作文件。
  • 通过调用File类提供的各种方法,能够完成创建、删除文件、重命名文件、判断文件的读写权限权限是否存在、设置和查询文件的最近修改时间等操作。
  • File类没有无参构造方法,最常用的是使用下面的构造方法来生成File对象(注意分隔符可以使用”/“和”",但是使用”"必须写”\“,因为涉及转义的问题):File(String pathName) pathName指的是文件的路径名。

第二节 文件

2.1 File类

File定义了很多获取File对象标准属性的方法,如下示例代码。

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
public class FileTest {

public static void main(String[] args){

String fileName = "D:" + File.separator + "Files";
File file = new File(fileName);

// 判断路径指向的文件/文件夹是否存在、是否目录
if(file.exists() && file.isDirectory()){
System.out.println("file是一个文件夹\n");

// 获取目录下的所有文件/文件夹(仅该层路径下)
File[] files = file.listFiles();
System.out.print("路径下有文件:");
for (File f : files){
System.out.print(f + "\t");
}
System.out.println();

System.out.println("files[0]的文件名:" + files[0].getName()); // 获取文件名、文件夹名
System.out.println("files[0]的文件路径:" + files[0].getPath()); // 获取文件、文件夹路径
System.out.println("files[0]的绝对路径:" + files[0].getAbsolutePath()); // 获取文件、文件夹绝对路径
System.out.println("files[0]的父文件夹名:" + files[0].getParent()); // 获取文件父目录路径
System.out.println(files[0].exists() ? "files[0]的存在" : "files[0]的不存在"); // 判断文件、文件夹是否存在
System.out.println(files[0].canWrite() ? "files[0]的可写" : "files[0]的不可写"); // 判断文件是否可写
System.out.println(files[0].canRead() ? "files[0]的可读" : "files[0]的不可读"); // 判断文件是否可读
System.out.println(files[0].canExecute() ? "file[0]可执行" : "file[0]不可执行"); // 判断文件是否可执行
System.out.println(files[0].isDirectory() ? "files[0]的是目录" : "files[0]的不是目录"); // 判断文件、文件夹是不是目录
System.out.println(files[0].isFile() ? "files[0]的是文件" : "files[0]的不是文件"); // 判断拿文件、文件夹是不是标准文件
System.out.println(files[0].isAbsolute() ? "files[0]的路径名是绝对路径" : "files[0]的路径名不是绝对路径"); // 判断路径名是不是绝对路径
System.out.println("files[0]的最后修改时间:" + files[0].lastModified()); // 获取文件、文件夹上一次修改时间
System.out.println("files[0]的大小:" + files[0].length() + " Bytes"); // 获取文件的字节数,如果是一个文件夹则这个值为0
System.out.println("files[0]的路径转换为URI:" + files[0].toURI()); // 获取文件路径URI后的路径名

if (files[0].exists()){
files[0].delete(); // 删除指定的文件、文件夹
}
if (files[1].exists()){
files[1].deleteOnExit(); // 当虚拟机终止时删除指定的文件、文件夹
}

}
}
}

运行结果为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
file是一个文件夹

路径下有文件:D:\Files\a.txt D:\Files\b.txt D:\Files\c.txt
files[0]的文件名:a.txt
files[0]的文件路径:D:\Files\a.txt
files[0]的绝对路径:D:\Files\a.txt
files[0]的父文件夹名:D:\Files
files[0]的存在
files[0]的可写
files[0]的可读
file[0]可执行
files[0]的不是目录
files[0]的是文件
files[0]的路径名是绝对路径
files[0]的最后修改时间:1488333781671
files[0]的大小:37 Bytes
files[0]的路径转换为URI:file:/D:/Files/a.txt

2.2 Path和Files

JDK 1.7时 PathFiles 随着 NIO 引入,位于 java.nio.file 包,用来代替File。

Path 表示一个目录名序列,还可以加一个文件名。

1
Path p = Paths.get("/demo","test");//通过Paths.get()创建Path实例,可以使用绝对路径或相对路径。

Path相关方法如下所示。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
Path path = Paths.get("E:\\BlogUpload\\pic\\upload\\cover","1");
System.out.println("文件名:" + path.getFileName());
System.out.println("名称元素的数量:" + path.getNameCount());
System.out.println("父路径:" + path.getParent());
System.out.println("根路径:" + path.getRoot());
System.out.println("是否是绝对路径:" + path.isAbsolute());
//startsWith()方法的参数既可以是字符串也可以是Path对象
System.out.println("是否是以为给定的路径D:开始:" + path.startsWith("D:\\") );
System.out.println("该路径的字符串形式:" + path.toString());

//.表示的是当前目录
Path currentDir = Paths.get(".");
System.out.println(currentDir.toAbsolutePath());
Path currentDir2 = Paths.get(".\\test.dat");
System.out.println("原始路径格式:"+currentDir2.toAbsolutePath());//生成绝对路径
System.out.println("执行normalize()方法之后:"+currentDir2.toAbsolutePath().normalize());//移除冗余的.和..部件
System.out.println("执行toRealPath()方法之后:"+currentDir2.toRealPath());

//..表示父目录或者说是上一级目录:
Path currentDir3 = Paths.get("..");
System.out.println("原始路径格式:"+currentDir3.toAbsolutePath());
System.out.println("执行normalize()方法之后:"+currentDir3.toAbsolutePath().normalize());
System.out.println("执行toRealPath()方法之后:"+currentDir3.toRealPath());

结果如下。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
文件名:1
名称元素的数量:5
父路径:E:\BlogUpload\pic\upload\cover
根路径:E:\
是否是绝对路径:true
是否是以为给定的路径D:开始:false
该路径的字符串形式:E:\BlogUpload\pic\upload\cover\1

D:\Java\WorkPlace\main\datastructure\.
原始路径格式:D:\Java\WorkPlace\main\datastructure\.\test.dat
执行normalize()方法之后:D:\Java\WorkPlace\main\datastructure\test.dat
执行toRealPath()方法之后:D:\Java\WorkPlace\main\datastructure\test.dat
原始路径格式:D:\Java\WorkPlace\main\datastructure\..
执行normalize()方法之后:D:\Java\WorkPlace\main
执行toRealPath()方法之后:D:\Java\WorkPlace\main

Path表示一个目录名序列,还可以加一个文件名。

Path是接口,而 PathsFiles 就是其对应的工具类,区别于File的面向对象设计,Path和Files是面向接口设计。以下为Files提供的一些功能。

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
boolean pathExits = Files.exists(path,new LinkOption[]{LinkOption.NOFOLLOW_LINKS});//检测时不包含符号链接文件
System.out.println("pathExits:"+pathExits);
Path target = Paths.get("D:\\a.txt");
Path newDir = Files.createDirectories(path);//创建文件夹,会创建所有不存在目录,createDirectory() 则在父目录不存在时抛出FileAlreadyExistsException
Files.delete(target);//删除文件或目录
Files.copy(target,newDir);//复制文件,把一个文件从一个地址复制到另一个位置,增加参数StandardCopyOption.REPLACE_EXISTING可以覆盖已存在的目标文件
Files.move(target,newDir);//移动文件,就是复制并删除原文件,增加参数StandardCopyOption.REPLACE_EXISTING可以覆盖已存在的目标文件

if(!Files.exists(target))
Files.createFile(target);//创建文件

//获取文件相关属性
System.out.println(Files.getLastModifiedTime(path));
System.out.println(Files.size(path));
System.out.println(Files.isSymbolicLink(path));
System.out.println(Files.isDirectory(path));
System.out.println(Files.readAttributes(path, "*"));

//遍历单个文件夹, Files.walkFileTree()可以递归遍历目录
Path dir = Paths.get("D:\\Java");
try(DirectoryStream<Path> stream = Files.newDirectoryStream(dir)){
for(Path e : stream){
System.out.println(e.getFileName());
}
}

Files也可以通过实现的read和write方法对文件进行简洁的读写,但如果是较大的文件还是应该使用流来处理。

Files相较File通过数组来存储目录中的所有文件,而是生成了一个Iterable对象,迭代器遍历对于声明了RandomAccessList的数组相较for循环要快。

面向接口设计,可以有效的降低耦合度,面向对象需要涉及到对象之间的交互问题,而接口本质上是规范和约束,也就是系统设计者的理解,比较适合设计和开发分离的情况。如今的Java流行框架都提倡这种模式,在接口中定义行为,然后在业务中去实现,将实现由框架管理,即控制反转。

但当系统业务比较简单,只是controller,service,dao之间相互调用,不存在设计和实现的分离,那么就没要再采用这种模式来增加工作量。

2.3 RandomAccessFile

2.3.1 简述

RandomAccessFile 类可以说是Java语言中功能最为丰富的文件访问类,它提供了众多的文件访问方法。它支持随机访问方式,可以跳转到文件的任意位置处读写数据。要访问一个文件的时候,不想把文件从头读到尾,而是希望像访问一个数据库一样地访问一个文本文件,使用RandomAccessFile类是最佳选择。

RandomAccessFile对象类中有个位置指示器,指向当前读写处的位置,当读写n个字节后,文件指示器将指向这n个字节后的下一个字节处。刚打开文件时,文件指示器指向文件的开头处,可以移动文件指示器到新的位置,随后的读写将从新的位置开始。

RandomAccessFile类在文件随机(相对于顺序)读取时有很大的优势,但该类仅限于操作文件,不能访问其他得IO设备,如网络、内存映像等。

2.3.2 构造方法

RandomAccessFile类为用户提供了两种构造方法:

  1. RandomAccessFile(File file, String mode)
  2. RandomAccessFile(String name, String mode)

其实第二种构造方法也是new一个File出来再调用第一种构造方法,建议使用第一种构造方法,因为第一篇文章就说了File是IO的基础,有一个File不仅仅可以通过RandomAccessFile对文件进行操作,也可以通过File对象对文件进行操作。至于mode,Java给开发者提供了四种mode:

模 式 作 用
r 表示以只读方式打开,调用结果对象的任何write方法都将导致抛出IOException
rw 打开以便读取和写入,如果该文件尚不存在,则尝试创建该文件
rws 打开以便读取和写入,相对于”rw”,还要求对文件内容或元数据的每个更新都同步写入到底层存储设备
rwd 打开以便读取和写入,相对于”rw”,还要求对文件内容的每个更新都同步写入到底层存储设备

2.3.3 API

方 法 作 用
void close() 重要,关闭此随机访问文件流并释放与该流关联的所有系统资源
FileChannel getChannel() 返回与此文件关联的唯一FileChannel对象,NIO用到
long getFilePointer() 返回此文件中的当前偏移量
long length() 返回此文件的长度
int read() 从此文件中读取一个数据字节
int read(byte[] b) 将最多b.length个数据字节从此文件读入byte数组,返回读入的总字节数,如果由于已经达到文件末尾而不再有数据,则返回-1。在至少一个输入字节可用前,此方法一直阻塞
int read(byte[] b, int off, int len) 将最多len个数据字节从此文件的指定初始偏移量off读入byte数组
boolean readBoolean() 从此文件读取一个boolean,其余readByte()、readChar()、readDouble()等类似
String readLine() 从此文件读取文本的下一行
void seek(long pos) 重要,设置到此文件开头测量到的文件指针偏移量,在该位置发生下一个读取或写入操作
int skipBytes(int n) 重要,尝试跳过输入的n个字节以丢弃跳过的字节,返回跳过的字节数
void write(byte[] b) 将b.length个字节从指定byte数组写入到此文件中
void write(byte[] b, int off, int len) 将len个字节从指定byte数组写入到此文件,并从偏移量off处开始
void write(int b) 向此文件写入指定的字节
void writeBoolean(boolean v) 按单字节值将boolean写入该文件,其余writeByte(int v)、writeBytes(String s)、writeChar(int v)等都类似

2.3.4 示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class Employee {
private String name;
private int age;
private final static int LEN = 8;

public Employee(String name, int age) {
if (name.length() > LEN) {
name = name.substring(0, 8);
} else {
while (name.length() < LEN) {
name = name + "\u0000";
}
}
this.name = name;
this.age = age;
}

// getter and setter
}

第一部分,写文件,该文件在路径下并没有,所以Java会自动帮我们创建:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
/**
* 写文件
*/
@Test
public void writeFile() throws IOException{
Employee e1 = new Employee("zhangsan", 23);
Employee e2 = new Employee("lisi", 24);
Employee e3 = new Employee("wangwu", 25);

RandomAccessFile raf0 = new RandomAccessFile("D:/Files/employee.txt", "rw");
raf0.writeBytes(e1.getName());
raf0.writeInt(e1.getAge());
raf0.writeBytes(e2.getName());
raf0.writeInt(e2.getAge());
raf0.writeBytes(e3.getName());
raf0.writeInt(e3.getAge());
raf0.close();
}

文件创建好了,D盘下的Files文件夹也有该文件了,所以读取一下,这里使用了一些小技巧来演示seek方法和skipBytes方法:

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
/**
* 读文件
*/
@Test
public void ReadFile() throws IOException{
RandomAccessFile raf1 = new RandomAccessFile("D:/Files/employee.txt", "r");
int len = 8;
raf1.skipBytes(12); // 跳过第一个员工的信息,其姓名8字节,年龄4字节
System.out.println("第二个员工的信息:");
String str = "";
for (int i = 0; i < len; i++) {
str = str + (char)raf1.readByte();
}
System.out.println("name:" + str);
System.out.println("age:" + raf1.readInt());
System.out.println("第一个员工的信息:");
raf1.seek(0);
str = "";
for (int i = 0; i < len; i++) {
str = str + (char)raf1.readByte();
}
System.out.println("name:" + str);
System.out.println("age:" + raf1.readInt());
System.out.println("第三个员工的信息:");
raf1.skipBytes(12); // 跳过第二个员工的信息
str = "";
for (int i = 0; i < len; i++) {
str = str + (char)raf1.readByte();
}
System.out.println("name:" + str.trim());
System.out.println("age:" + raf1.readInt());
raf1.close();
}

运行结果:

1
2
3
4
5
6
7
8
9
第二个员工的信息:
name:lisi
age:24
第一个员工的信息:
name:zhangsan
age:23
第三个员工的信息:
name:wangwu
age:25

可能有人奇怪,”zhangsan”加上一个int跳过12个字节可以理解,但是”lisi”、”wangwu”为什么加上int要跳过12个字节呢?明明”lisi”只有4个字节,”wangwu”只有6个字节啊。这个就涉及到一个”字节对齐”的问题了,有兴趣的可以了解一下。另外,再说一下,RandomAccessFile使用完一定要及时close()。


第三节 字符集

不同字符集对应不同的编码格式,java.nio包用Charset类统一了对字符集的转换,本地编码方式模式无法表示所有的unicode字符,当遇到不能表示的字符时会用 ? 代替。

通过字符集可以在字符串和字节序列之间转换。

1
2
3
4
5
6
7
8
9
10
//String -> 字节序列
Charset charset = Charset.forName("UTF-8");
String str = "Hello世界";
ByteBuffer buffer = charset.encode(str);
byte[] bytes = buffer.array();

//字节序列 -> String
ByteBuffer buffer1 = ByteBuffer.wrap(bytes);
CharBuffer charBuffer = charset.decode(buffer1);
String str1 = charBuffer.toString();

第四节 字节流与字符流

4.1 字节流

抽象类 InputStreamOutputStream 是I/O结构中字节流的基础。其抽象读写方法是基于字节读写,但并不方便处理Unicode格式,且读写方法执行时会阻塞,可以通过 available() 检查当前可读入字节数量,避免等待。在使用流后切记要即时的 close() 释放资源,可以通过 try-with-resource 来实现无论何种情况自动释放资源。

输入输出流的层次结构

Java通过分离职能来分类输入流和输出流,所以在实现某些功能时需要结合多种类型的流,Filter流的的子类用来向原生的字节流添加额外的功能,所以可以通过嵌套过滤器来添加多重功能。如下从文件中读取数字并引入缓冲区。

1
DataInputStream dis = new DataInputStream(new BufferedInputStream(new FileInputStream("/test.txt")));

多个流链接在一起时可以通过 PushbackInputStream 跟踪各个中介流,PushbackInputStream 可以推回读入字节。

DataInputDataOutput 定义了一些方法用来读写二进制格式数据。

1
2
3
4
5
6
7
boolean readBoolean() throws IOException;
byte readByte() throws IOException;
......

void write(int b) throws IOException;
void write(byte b[]) throws IOException;
......

ZipStream 读写ZIP格式的压缩文档。

4.2 字符流

字节流提供了处理任何类型输入/输出操作的功能(因为对于计算机而言,一切都是0和1,只需把数据以字节形式表示就够了),但它们不可以直接操作Unicode字符,因为一个Unicode字符占用2个字节,而字节流一次只能操作一个字节。既然Java的口号就是”一次编写、处处运行”,那么包含直接的字符输入/输出的支持是必要的。因此就有一些字符输入/输出流,之前已经说明过了,字符流顶层是Reader和Writer这两个抽象类,所以Unicode格式可以通过使用抽象类Reader和Writer的子类来处理(字符流)。

Reader和Writer的层次结构

4.3 基础接口

基础接口包括:CloseableFlushableReadableAppendable 接口。

  1. Closeable接口扩展了 AutoCloseable 接口,所以可以使用try-with-resource语句,Closeable的 close() 方法限制只能抛出 IOException ,而 AutoCloseable 接口的 close() 方法则没有限制。InputStreamOutputStreamReaderWriter 都实现了 Closeable 接口。
  2. Flushableflush() 方法通过写入缓冲输出到底层流来刷新当前流。
  3. Readableread() 方法需要 CharBuffer 类型参数,其表示内存中的缓冲区或内存映像的文件,可以顺序或随机的进行读写访问。
  4. Appendable接口则可以添加字符或字符序列。
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
public abstract class InputStream implements Closeable
public abstract class OutputStream implements Closeable, Flushable
public abstract class Reader implements Readable, Closeable
public abstract class Writer implements Appendable, Closeable, Flushable

public interface Closeable extends AutoCloseable {
public void close() throws IOException;
}

public interface Flushable {
void flush() throws IOException;
}

public interface Readable {
public int read(java.nio.CharBuffer cb) throws IOException;
}

public interface Appendable {

Appendable append(CharSequence csq) throws IOException;

Appendable append(CharSequence csq, int start, int end) throws IOException;

Appendable append(char c) throws IOException;
}

4.4 OutputStream

OutputStream 是定义了Java流式字节输入模式的抽象类。该类的所有方法返回一个void值并且在出错的情况下引发一个 IOExceptionOutputStream 提供的抽象方法有:

方 法 作 用
void close() 关闭输入流,关闭后的写操作会引发IOException
flush() 刷新此输入流并强制写出所有缓冲的输出字节
write(byte[] b) 向输入流写入单个字节,注意参数是一个int型,它允许设计者不必把参数转换成字节型就可以调用write()方法
write(byte[] b, int off, int len) 以b[off]为起点,向文件写入字节数组b中len个字节
write(int b) 向一个输出流写一个完整的字节数组

FileOutpuStream 应该是Java中最常见的字节输出流了,它创建一个可向文件写入字节的类OutputStream,它常用的构造方法如下:

  1. FileOutputStream(String name)

  2. FileOutputStream(File file)

  3. FileOutputStream(File file, boolean append)

前两个构造方法类似,前者输入文件的绝对路径,后者输入File的实例对象,和 RandomAccessFile 一样,推荐后者。第三个构造方法有一点不同,append如果设置为true,文件则以搜索路径模式打开。

FileOutputStream的创建不依赖于文件是否存在,在创建对象时,FileOutputStream 会在打开输出文件之前就创建它。这种情况下如果试图打开一个只读文件,会引发 IOException

写一个例子,现在D盘下的Files文件夹里并没有”stream.txt”这个文件:

1
2
3
4
5
6
7
8
9
public class FileOutputStreamTest {
public static void main(String[] args) throws Exception{
File file = new File("D:/Files/stream.txt");
OutputStream out = new FileOutputStream(file);
byte b[] = "Hello World!!!".getBytes(); //操作字节流,要转换成字节
out.write(b);
out.close();
}
}

到D盘下的Files文件夹里多了”stream.txt”,且文件中的内容和我们写入的一致,同样这个例子也证明了FileOutputStream并不依赖指定路径下的文件是否存在。那如果指定路径下本来就有文件,那么写将会覆盖而不是追加,很好证明:

1
2
3
4
5
6
7
8
9
public class FileOutputStreamTest {
public static void main(String[] args) throws Exception{
File file = new File("D:/Files/stream.txt");
OutputStream out = new FileOutputStream(file);
byte b[] = "abcdefghijklmnopqrstuvwxyz".getBytes(); //操作字节流,要转换成字节
out.write(b);
out.close();
}
}

4.5 InputStream

InputStream 是定义了Java流式字节输入模式的抽象类。该类所有方法在出错的时候都会引发一个 IOExceptionInputStream 提供的抽象方法有:

方 法 作 用
int available() 返回当前可读的字节数
void close() 关闭此输入流并释放与该流关联的所有系统资源,关闭之后再读取会产生IOException
int mark(int readlimit) 在输入流中放置一个标记,该流在读取N个Bytes字节前都保持有效
boolean markSupported() 如果调用的流支持mark()/reset()就返回true
int read() 如果下一个字节可读取则返回一个整形,遇到文件尾时返回-1
int read(byte b[]) 试图读取buffer.length个字节到buffer中,并返回实际成功读取的字节数,遇到文件尾则返回-1
int read(byte b[], int off, int len) 将输入流中最多len个数组直接读入byte数组,off表示数组b中写入数据的初始偏移量。注意,三个read方法,在输入数据可用、检测到流末尾或者抛出异常前,此方法将一直阻塞
void reset() 重新设置输入指针到先前设置的标记处
long skip(long n) 跳过和丢弃此输入流中数据的n个字节

FileInputStream 应该是Java中最常见的字节输入流了,它创建一个能从文件读取字节的 InputStream 类,它的两个常用构造方法如下:

  1. FileInputStream(String name)

  2. FileInputStream(File file)

和FileOutputStream差不多,推荐后者的用法。同样写一个例子,操作的是上面D盘下的Files文件夹生成的”stream.txt”:

1
2
3
4
5
6
7
8
9
10
11
12
13
public class FileInputStreamTest {

public static void main(String[] args) throws Exception{
File file = new File("D:/Files/stream.txt");
InputStream in = new FileInputStream(file);
byte b1[] = new byte[(int)file.length()];
int i = 0;
i = in.read(b1);
System.out.println(i);
System.out.println(new String(b1, 0, i));

}
}

运行结果为:

1
2
26
abcdefghijklmnopqrstuvwxyz

要区分清楚,OutputStream 的作用是将内容由Java内存输出到文件中、InputStream 是将内容由文件输入到Java内存中。read(byte b[]) 表示”**试图读取buffer.length个字节到buffer中,并返回实际读取的字节数”**,返回的是实际字节的大小。

4.6 Reader

Reader 是定义Java的字符输入流的抽象类,该类的所有方法在出错的情况下都将引发 IOExceptionReader 类中有这些方法:

方 法 作 用
abstract void close() 关闭该流并释放与之关联的所有资源
void mark(int readAheadLimit) 标记流中的当前位置
boolean markSupported() 判断此流是否支持mark()操作
int read() 从文件中读取单个字符
int read(char[] cbuf) 从文件中读取字符到cbuf
abstract int read(char[] cbuf, int off, int len) 将文件中的字符读入cbuf数组,从off位置开始,读取len个字符。三个read方法在字符可用、发生I/O异常或者已经到达流的末尾前,此方法会一直阻塞
int read(CharBuffer target) 试图将文件中的字符读入指定的字符缓冲区
boolean ready() 判断是否准备读取此流
voi reset() 重置该流
long skip(long n) 跳过n个字符

4.7 Writer

Writer 是定义字符输出流的抽象类,所有该类的方法都返回一个void值并在出错的条件下引发 IOExceptionWriter 类中的方法有:

方 法 作 用
Writer append(char c) 将制定字符添加到此writer
Writer append(CharSequence csq) 将制定字符序列添加到此writer
Writer append(CharSequence csq, int start, int end) 将指定字符序列的子序列添加到此writer.Appendable
abstract void close() 关闭此流,但要先flush()它
abstract void flush() 刷新该流的缓冲
void write(char[] cbuf) 将cbuf中的内容写入文件
abstract void write(char[] cbuf, int off, int len) 将字符数组cbuf中从off开始的len个字节写入文件
void write(int c) 写入单个字符到文件中
void write(String str) 写入字符串到文件中
void write(String str, int off, int len) 写入str从off位置开始的len个字符到文件中

4.8 FileReader和FileWriter

FileReader 类创建了一个可以读取文件内容的Reader类,最常用的构造方法是:

  1. FileReader(String fileName)

  2. FileReader(File file)

FileWriter创建了一个可以写文件的Writer类,最常用的构造方法是:

  1. FileWriter(String fileName)

  2. FileWriter(String fileName, boolean append)

  3. FileWriter(File file)

其中第二个构造方法,如果append为true,那么输出是追加到文件结尾的。FileWriter类的创建不依赖文件是否存在,在创建文件之前,FileWriter将在创建对象时打开它来作为输出。如果试图打开一个只读文件,那么将引发一个IOException。看一下FileWriter和FileReader的使用,现在D盘FIles文件夹下没有”writer.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
public class FileReaderWriterTest {
public static void main(String[] args) throws Exception{
File file = new File("D:/Files/writer.txt");
Writer out = new FileWriter(file);
// 声明一个String类型对象
String str = "Hello World!!!";
out.write(str);
out.close();

//读文件操作
Reader in = new FileReader(file);
// 开辟一个空间用于接收文件读进来的数据
char c0[] = new char[1024];
int i = 0;
// 将c0的引用传递到read()方法之中,同时此方法返回读入数据的个数
i = in.read(c0);
in.close();
if(i==-1){
System.out.println("文件中无数据");
}else{
System.out.println(new String(c0,0,i));
}
}
}

4.9 字符流和字节流的区别

字符流和字节流最大的区别在于,字节流在操作时本身不会用到缓冲区(内存),是文件本身直接操作的,而字符流操作时使用了缓冲区,通过缓冲区再操作文件。这也解释了上面程序的那个问题,为什么不对资源进行 close() 就无法写入文件的原因。因为在关闭字符流时会强制性地将缓冲区中的内容进行输出,但是如果没有关闭,缓冲区中的内容是无法输出的。

什么是缓冲区?简单理解,缓冲区就是一块特殊的内存区域

为什么要使用缓冲区?因为如果一个程序频繁操作一个资源(文件或数据库),则性能会很低,为了提升性能,就可以将一部分数据暂时读入到内存的一块区域之中,以后直接从此区域读取数据即可,因为读取内存的速度要快于读取磁盘中文件内容的速度

在字符流的操作中,所有的字符都是在内存中形成的在输出前会将所有的内容暂时保存在内存之中所以使用了缓冲区

如果不想在关闭时再输出字符流的内容也行,使用 Writerflush() 方法就可以了。

Java支持字符流和字节流,字符流本身就是一种特殊的字节流,之所以要专门有字符流,是因为Java中有大量对于字符的操作,所以专门有字符流。字节流和字符流的转换是以 InputStreamReaderOutputStreamWriter 为媒介的,InputStreamReader 可以将一个字节流中的字节解码成字符, OutputStreamWriter 可以将写入的字符编码成自节后写入一个字节流。

InputStreamReader中的解码字节,是由 StreamDecoder 完成的,StreamDecoder是Reader的实现类,定义在InputStreamReader的开头:

1
2
3
public class InputStreamReader extends Reader {

private final StreamDecoder sd;

同样,OutputStreadWriter中的编码字节,是由 StreamEncoder 完成的,StreamEncoder是Writer的实现类,定义在OutputStreamWriter的开头:

1
2
3
public class OutputStreamWriter extends Writer {

private final StreamEncoder se;

假如不对StreamDecoder和StreamEncoder指定 Charset 编码格式,将使用本地环境中的默认字符集,例如中文环境中将使用 GBK编码

InputStreamReader有两个主要的构造函数:

  1. InputStreamReader(InputStream in)

  2. InputStreamReader(InputStream in, String charsetName)

OutputStreamWriter也有两个主要的构造函数:

  1. OutputStreamWriter(OutputStream out)

  2. OutputStreamWriter(OutputStream out, String charsetName)

从构造函数就可以看出,字符流是利用字节流实现的。InputStreamReader和OutputStreamWriter的两个构造函数的区别在于,一个是使用的默认字符集一个可以指定字符集名称

其实FileReader和FileWriter可以看一下源码,很简单,只有构造函数,里面都是分别根据传入的文件绝对路径或者传入的File实例,new出FileInputStream和FileOutputStream,在调用InputStreamReader和OutputStreamWriter的构造方法。这么做,帮助开发者省去了实例化FileInputStream和FileOutputStream的过程,让开发者可以直接以fileName或file作为构造函数的参数。

4.10 BufferedWriter和BufferedReader

为了达到最高的效率,避免频繁地进行字符与字节之间的相互转换,最好不要直接使用 FileReaderFileWriter 这两个类进行读写,而使用 BufferedWriter 包装 OutputStreamWriter ,使用 BufferedReader 包装 InputStreamReader 。同样,在D盘Files文件夹下没有”buffered”这个文件,代码示例为:

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
public class BufferedWriterReaderTest {

public static void main(String[] args) throws Exception{
File file = new File("D:/Files/buffered.txt");
Writer writer = new FileWriter(file);
BufferedWriter bw = new BufferedWriter(writer);
bw.write("1234\n");
bw.write("2345\n");
bw.write("3456\n");
bw.write("\n");
bw.write("4567\n");
bw.close();
writer.close();

if (file.exists() && file.getName().endsWith(".txt")){
Reader reader = new FileReader(file);
BufferedReader br = new BufferedReader(reader);
String str = null;
while ((str = br.readLine())!= null){
System.out.println(str);
}
reader.close();
br.close();
}
}
}

运行一下,首先D盘Files文件夹下多出了”buffered.txt”这个文件,并查看文件中的内容。

然后看一下控制台的输出结果:

1
2
3
4
5
1234
2345
3456

4567

没什么问题,输出了文件中的内容。注意两点:

  1. 利用 BufferedWriter 进行写操作,写入的内容会放在缓冲区内,直到遇到 close()flush() 的时候才会将内容一次性写入文件。另外注意 close() 的顺序,一定要先关闭 BufferedWriter ,再关闭 Writer ,不可以倒过来,因为 BufferedWriter 的写操作是通过 Writerwrite() 方法写的,如果先关闭 Writer 的话,就无法将缓冲区内的数据写入文件了,会抛出异常。

  2. 利用 BufferedReader 进行读操作,不可以用父类Reader指向它,因为 readLine() 这个方法是 BufferedReader 独有的,readLine() 的作用是逐行读取文件中的内容。


参考:

🔗《Java核心技术 卷Ⅱ》

🔗 Serializable和Externalizable浅析

🔗 Java NIO之拥抱Path和Files

🔗 Java IO(一):IO和File

🔗 Java IO(二)——RandomAccessFile

🔗 Java IO(三)——字节流

🔗 Java IO(四)——字符流

🔗 Java IO(五)——字符流进阶及BufferedWriter、BufferedReader