IO操作主要是指使用Java程序完成输入(Input)、输出(Output)操作。所谓输入是指将文件内容以数据流的形式读取到内存中,输出是指通过Java程序将内存中的数据写入到文件中,输入、输出操作在实际开发中应用较为广泛。本章将针对IO的相关操作进行讲解。
1. File类
1.1. File类的构造方法
File类提供了多个构造方法用于创建File对象,具体如下表所示。
方法声明 | 功能描述 |
File(String pathname) | 通过指定的一个字符串类型的文件路径来创建一个新的File对象 |
File(String parent,String child) | 根据指定的一个字符串类型的父路径和一个字符串类型的子路径(包括文件名称)创建一个File对象 |
File(File parent,String child) | 根据指定的File类的父路径和字符串类型的子路径(包括文件名称)创建一个File对象 |
下面通过一个案例演示如何使用File类的构造方法创建File对象。具体代码如下所示。
@Test
public void test1(){
File f1 = new File("D:\\file\\hello.txt"); //使用绝对路径创建File对象
File f2 = new File("src\\Hello.java"); //使用相对路径创建File对象
System.out.println(f1);
System.out.println(f2);
}
注意:案例在创建File对象时传入的路径使用了\\,这是因为Windows中的目录符号为反斜线\,但反斜线\在Java中是特殊字符,具有转义作用,所以使用反斜线\时,前面应该再添加一个反斜线,即为\\。此外,目录符号还可以用正斜线/表示,如“D:/file/hello.txt”。
1.2. File类的常用方法
File类提供了一系列方法,例如判断文件是否存在、获取文件的名称、文件的大小、文件的路径、删除文件等,用于操作File类对象内部封装的路径指向的文件或者目录。具体如下表所示。
方法声明 | 功能描述 |
boolean exists() | 判断File对象对应的文件或目录是否存在,若存在则返回true,否则返回false |
boolean delete() | 删除File对象对应的文件或目录,若删除成功则返回true,否则返回false |
boolean createNewFile() | 当File对象对应的文件不存在时,该方法将新建一个文件,若创建成功则返回true,否则返回false |
String getName() | 返回File对象表示的文件或文件夹的名称 |
String getPath() | 返回File对象对应的路径 |
String getAbsolutePath() | 返回File对象对应的绝对路径(在Unix/Linux等系统上,如果路径是以正斜线/开始,则这个路径是绝对路径;在Windows等系统上,如果路径是从盘符开始,则这个路径是绝对路径) |
String getParentFile() | 返回File对象对应目录的父目录(即返回的目录不包含最后一级子目录) |
boolean canRead() | 判断File对象对应的文件或目录是否可读,若可读则返回true,反之返回false |
boolean canWrite() | 判断File对象对应的文件或目录是否可写,若可写则返回true,反之返回false |
boolean isFile() | 判断File对象对应的是否是文件(不是目录),若是文件则返回true,反之返回false |
boolean isDirectory() | 判断File对象对应的是否是目录(不是文件),若是目录则返回true,反之返回false |
boolean isAbsolute() | 判断File对象对应的文件或目录是否是绝对路径 |
long lastModified() | 返回1970年1月1日0时0分0秒到文件最后修改时间的毫秒值 |
long length() | 返回文件内容的长度(单位是字节) |
String[] list() | 递归列出指定目录的全部内容(包括子目录与文件),只是列出名称 |
File[] listFiles() | 返回一个包含了File对象所有子文件和子目录的File数组 |
下面通过一个案例演示如何使用File类的常用方法。具体代码如下所示。
@Test
public void test2() {
File file = new File("src/Hello.java");
System.out.println("文件是否存在:" + file.exists());
System.out.println("文件名:" + file.getName());
System.out.println("文件大小:" + file.length() + "bytes");
System.out.println("文件相对路径:" + file.getPath());
System.out.println("文件绝对路径:" + file.getAbsolutePath());
System.out.println("文件是否为文件:" + file.isFile());
System.out.println("文件删除是否成功:" + file.delete());
}
1.3. 遍历目录下的文件
通过调用File类中的list()方法,可以遍历目录下的文件。按照调用方法的不同,目录下的文件遍历可分为以下3种方式。
-
遍历指定目录下的所有文件。
@Test
public void test3(){
// 创建File对象
File file = new File("D:\\workspace-java\\case05");
if (file.isDirectory()) { // 判断File对象对应的目录是否存在
String[] names = file.list (); // 获得目录下的所有文件的文件名
for (String name : names) {
System.out.println(name); // 输出文件名
}
}
}
-
遍历指定目录下指定扩展名的文件。
有时程序需要获取指定类型的文件,如获取指定目录下所有的“.java”文件。针对这种需求,File类提供了一个重载的list()方法,该方法接收一个FilenameFilter类型的参数。FilenameFilter是一个接口,被称作文件过滤器,其中定义了一个抽象方法accept()用于依次对指定File的所有子目录或文件进行迭代。在调用list()方法时,需要实现文件过滤器FilenameFilter,并在accept()方法中进行筛选,从而获得指定类型的文件。
@Test
public void test4() {
// 创建File对象
File file = new File("D:\\workspace-java\\case05\\src\\com\\wfit\\test01");
// 创建过滤器对象
FilenameFilter filter = new FilenameFilter() {
// 实现accept()方法
public boolean accept(File dir, String name) {
File currFile = new File(dir, name);
// 如果文件名以.java结尾返回true,否则返回false
if (currFile.isFile() && name.endsWith(".java")) {
return true;
} else {
return false;
}
}
};
if (file.exists()) { // 判断File对象对应的目录是否存在
String[] lists = file.list(filter); // 获得过滤后的所有文件名数组
for (String name : lists) {
System.out.println(name);
}
}
}
-
遍历包括子目录中的文件在内的所有文件。
有时候在一个目录下,除了文件还有子目录,如果想获取所有子目录下的文件,list()方法显然不能满足要求,这时可以使用File类提供的另一个方法listFiles()。listFiles()方法返回一个File对象数组,当对数组中的元素进行遍历时,如果元素中还有子目录需要遍历,则可以使用递归再次遍历子目录。
@Test
public void test5() {
// 创建一个代表目录的File对象
File file = new File("D:\\workspace-java\\case05\\src\\com\\wfit");
fileDir(file); // 调用FileDir方法
}
public void fileDir(File dir) {
File[] files = dir.listFiles(); // 获得表示目录下所有文件的数组
for (File file : files) { // 遍历所有的子目录和文件
if (file.isDirectory()) {
fileDir(file); // 如果是目录,递归调用fileDir()
}
System.out.println(file.getAbsolutePath()); // 输出文件的绝对路径
}
}
1.4. 删除文件及目录
在操作文件时,可能会遇到需要删除一个目录下的某个文件或者删除整个目录的情况,这时可以调用File类的delete()方法。
@Test
public void test6(){
File file = new File("D:\\test\\testdel");
deleteDir(file); // 调用deleteDir删除方法
System.out.println("删除成功!");
}
public void deleteDir(File dir) {
if (dir.exists()) { // 判断传入的File对象是否存在
File[] files = dir.listFiles(); // 得到File数组
for (File file : files) { // 遍历所有的子目录和文件
if (file.isDirectory()) {
deleteDir(file); // 如果是目录,递归调用deleteDir()
} else {
file.delete(); // 如果是文件,直接删除
}
}
// 删除完一个目录里的所有文件后,就删除这个目录
dir.delete();
}
}
2. 字节流
在程序的开发中,经常需要处理设备之间的数据传输,而计算机中,无论是文本、图片、音频还是视频,所有文件都是以二进制(字节)形式存在的。对于字节的输入输出,I/O流提供了一系列的流,统称为字节流,字节流是程序中最常用的流,根据数据的传输方向可将其分为字节输入流和字节输出流。
JDK提供了两个抽象类InputStream和OutputStream,它们是字节流的顶级父类,所有的字节输入流都继承自InputStream,所有的字节输出流都继承自OutputStream。为了方便理解,可以把InputStream和OutputStream比作两根“水管”,具体如下:
在上图中,InputStream被看成一个输入管道,OutputStream被看成一个输出管道,数据通过InputStream从源设备输入到程序,通过OutputStream从程序输出到目标设备,从而实现数据的传输。由此可见,I/O流中的输入输出都是相对于程序而言的。
2.1. InputStream类
-
InputStream类的常用方法
InputStream类提供了一系列与读数据相关的方法。具体如下表所示。
方法声明 | 功能描述 |
int read() | 从输入流读取一个8位的字节,把它转换为0~255之间的整数,并返回这一整数 |
int read(byte[] b) | 从输入流读取若干字节,把它们保存到参数b指定的字节数组中,返回的整数表示读取字节的数目 |
int read(byte[] b,int off,int len) | 从输入流读取若干字节,把它们保存到参数b指定的字节数组中,off指定字节数组开始保存数据的起始索引,len表示读取的字节数目 |
void close() | 关闭此输入流并释放与该流关联的所有系统资源 |
-
InputStream体系结构
InputStream类虽然提供了一系列和读数据有关的方法,但是InputStream类是抽象类,不能被实例化,因此针对不同的功能,InputStream类提供了不同的子类,这些子类形成了一个体系结构。如下图所示。
-
字节流FileInputStream
InputStream就是JDK提供的基本输入流,它是所有输入流的父类,FileInputStream是InputStream的子类,它是操作文件的字节输入流,专门用于读取文件中的数据。
@Test
public void test1(){
File file = new File("d:\\filetest\\hello.txt");
//从文件中读取数据
try{
FileInputStream in = new FileInputStream(file);
int len = in.read();
while(len != -1){
System.out.print((char)len);
len = in.read();
}
in.close();
}catch (Exception e){
System.out.println(e.toString());
}
}
2.2. OutputStream类
-
OutputStream类的常用方法
OutputStream类提供了一系列与写数据相关的方法。具体如下表所示。
方法声明 | 功能描述 |
void write(int b) | 向输出流写入一个字节 |
void write(byte[] b) | 把参数b指定的字节数组的所有字节写到输出流 |
void write(byte[] b,int off,int len) | 将指定byte数组中从偏移量off开始的len个字节写入输出流 |
void flush() | 刷新此输出流并强制写出所有缓冲的输出字节 |
void close() | 关闭此输出流并释放与此流相关的所有系统资源 |
-
OutputStream体系结构
OutputStream类虽然提供了一系列和写数据有关的方法,但是OutputStream类是抽象类,不能被实例化,因此针对不同的功能,OutputStream类提供了不同的子类,这些子类形成了一个体系结构。如下图所示。
-
字节流FileOutputStream
OutputStream是JDK提供的最基本的输出流,与InputStream类似,OutputStream也是抽象类,它是所有输出流的父类。OutputStream是一个抽象类,如果使用此类,则必须先通过子类实例化对象。OutputStream类有多个子类,其中FileOutputStream子类是操作文件的字节输出流,专门用于把数据写入文件。
@Test
public void test2(){
File file = new File("d:\\filetest\\hello.txt");
//向文件中写入数据
FileOutputStream out = null;
try{
out = new FileOutputStream(file);
String str = "hello wfit";
out.write(str.getBytes());
out.close();
}catch (Exception e){
System.out.println(e.toString());
}
}
2.3. 文件的复制
在应用程序中,I/O流通常都是成对出现的,即输入流和输出流一起使用。例如,文件的复制就需要通过输入流读取文件中的数据,再通过输出流将数据写入文件。
编写程序,使用FileInputStream和FileOutputStream将文件helloA.txt 的内容复制到helloB.txt文件中。
@Test
public void test3(){
File helloA = new File("d:\\filetest\\helloA.txt");
File helloB = new File("d:\\filetest\\helloB.txt");
try{
FileInputStream in = new FileInputStream(helloA);
FileOutputStream out = new FileOutputStream(helloB);
int len = in.read();
while(len != -1){
out.write(len);
len = in.read();
}
in.close();
out.close();
}catch (Exception e){
System.out.println(e.toString());
}
}
3. 字符流
前面讲解内容都是通过字节流直接对文件进行读写,如果读写的文件内容是字符,考虑到使用字节流读写字符可能存在传输效率以及数据编码问题,此时建议使用字符流。同字节流一样,字符流也有两个抽象的顶级父类,分别是Reader类和Writer类。其中Reader类是字符输入流,用于从某个源设备读取字符。Writer类是字符输出流,用于向某个目标设备写入字符。
3.1. Reader类
-
Reader类的常用方法
Reader类提供了一系列与读数据相关的方法。具体如下表所示。
方法声明 | 功能描述 |
int read() | 以字符为单位读数据 |
int read(char cbuf[]) | 将数据读入char类型数组,并返回数据长度 |
int read(char cbuf[],int off,int len) | 将数据读入char类型数组的指定区间,并返回数据长度 |
void close() | 关闭数据流 |
long transferTo(Writer out) | 将数据直接读入字符输出流 |
-
Reader体系结构
Reader类作为字符流的顶级父类,也有许多子类,下面通过一张继承关系图列举Reader类的常用子类。如下图所示。
-
字符流FileReader
在程序开发中,经常需要对文本文件的内容进行读取,如果想从文件中直接读取字符便可以使用字符输入流FileReader,通过此流可以从关联的文件中读取一个或一组字符。
编写程序,使用FileReader对象从hello.txt文件中读出字符并输出。
@Test
public void test1(){
File file = new File("d:\\filetest\\hello.txt");
try{
FileReader in = new FileReader(file);
BufferedReader ins = new BufferedReader(in);
String line;
while ((line = ins.readLine()) != null){
System.out.println(line);
}
ins.close();
in.close();
}catch (Exception e){
System.out.println(e.toString());
}
}
3.2. Writer类
-
Writer类的常用方法
Writer类提供了一系列与写数据相关的方法。具体如下表所示。
方法声明 | 功能描述 |
void write(int c) | 以字符为单位写数据 |
void write(char cbuf[]) | 将char类型数组中的数据写出 |
void write(char cbuf[],int off,int len) | 将char类型数组中指定区间的数据写出 |
void write(String str) | 将String类型的数据写出 |
void wirte(String str,int off,int len) | 将String类型指定区间的数据写出 |
void flush() | 可以强制将缓冲区的数据同步到输出流中 |
void close() | 关闭数据流 |
-
Writer体系结构
Writer类作为字符流的顶级父类,也有许多子类,下面通过一张继承关系图列举Writer类的常用子类。如下图所示。
-
字符流FileWriter
在程序开发中,有时需要向文本文件写入内容,通过字符流向文本文件写入内容需要使用FileWriter类,FileWriter类可以一次向文件中写入一个或一组字符。
下面通过一个案例学习如何使用FileWriter字符流将字符写入文件。具体代码如下所示。
@Test
public void test2(){
File file = new File("d:\\filetest\\hello.txt");
try{
FileWriter out = new FileWriter(file);
String str = "hello wfit";
out.write(str);
out.close();
}catch (Exception e){
System.out.println(e.toString());
}
}
4. 转换流
I/O流分为字节流和字符流,字节流和字符流之间可以进行转换。JDK提供了两个类用于将字节流转换为字符流,它们分别是InputStreamReader和OutputStreamWriter,也称为转换流,其作用如下所示。
(1)InputStreamReader是Reader的子类,它可以将一个字节输入流转换成字符输入流,方便直接读取字符。
(2)OutputStreamWriter是Writer的子类,它可以将一个字节输出流转换成字符输出流,方便直接写入字符。
@Test
public void test(){
File file = new File("d:\\filetest\\hello.txt");
try{
// 创建一个字节输入流in
FileInputStream in = new FileInputStream(file);
// 将字节输入流in转换成字符输入流isr
InputStreamReader isr = new InputStreamReader(in);
int len = isr.read();
while(len != -1){
System.out.print((char)len);
len = isr.read();
}
isr.close();
in.close();
}catch (Exception e){
System.out.println(e.getMessage());
}
}
5. 序列化和反序列化
程序在运行过程中,数据都保存在Java中的对象中(内存),但很多情况下我们都需要将一些数据永久保存到磁盘上。为此,Java提供了对象序列化,对象序列化可以将对象中的数据保存到磁盘。
5.1. 序列化和反序列化
对象序列化(Serializable)是指将一个Java对象转换成一个I/O流的字节序列的过程。
反序列化(Deserialize)是指将I/O流中的字节序列恢复为Java对象的过程。
5.2. Serializable接口和Externalizable接口
对象实现支持序列化机制,这个对象所属的类必须是可序列化的。在Java中可序列化的类可通过实现Serializable或Externalizable两个接口之一。
Serializable接口 | Externalizable接口 |
系统自动存储必要的信息 | 由程序员决定所存储的信息 |
Java内部支持,易于实现,只需实现该接口即可,不需要其他代码支持 | 接口中只提供了两个抽象方法,实现该接口必须要实现这两个抽象方法 |
性能较差 | 性能较好 |
5.3. 实现序列化方法
与实现Serializable接口相比,虽然实现Externalizable接口可以带来一定性能上的提升,但由于实现Externalizable接口,需要实现两个抽象方法,所以实现Externalizable接口也将导致编程的复杂度增加。在实际开发时,大部分都采用实现Serializable接口的方式来实现对象序列化。使用Serializable接口实现对象序列化非常简单,只需要让目标类实现Serializable接口即可,无需实现任何方法。
-
Student类
public class Student implements Serializable {
private static final long serialVersionUID = 1L;
private String name;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
@Override
public String toString() {
return "Student{" +
"name='" + name + '\'' +
'}';
}
}
-
测试类
public class TestDemo3 {
@Test
public void test1(){
Student student = new Student();
student.setName("张三");
File file = new File("d:\\filetest\\hello.txt");
//向文件中写入数据
try{
ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream(file));
out.writeObject(student);
out.close();
}catch (Exception e){
System.out.println(e.toString());
}
}
@Test
public void test2(){
File file = new File("d:\\filetest\\hello.txt");
//向文件中写入数据
try{
ObjectInputStream in = new ObjectInputStream(new FileInputStream(file));
Student student = (Student) in.readObject();
System.out.println(student);
in.close();
}catch (Exception e){
System.out.println(e.toString());
}
}
}
serialVersionUID适用于Java的对象序列化机制。简单来说,Java的对象序列化机制是通过判断类的serialVersionUID来验证版本一致性。在进行反序列化时,JVM会把字节流中的serialVersionUID与本地相应实体类的serialVersionUID进行比较,如果相同就认为是一致的,可以进行反序列化,否则就会抛出序列化版本不一致的异常。因此,为了在反序列化时确保序列化版本的兼容性,最好在每一个要序列化的类中加入private static final long serialVersionUID的变量值,具体数值可自定义,默认是1L。