本文编写于 265 天前,最后修改于 265 天前,其中某些信息可能已经过时。

IO

大多数应用程序都需要实现与设备之间的数据传输,例如键盘可以输入数据,显示器可以显示程序的运行结果等。在Java中,将这种通过不同输入输出设备(键盘,内存,显示器,网络等)之间的数据传输抽象表述为“流”,程序允许通过流的方式与输入输出设备进行数据传输。Java中的“流”都位于java.io包中,称为IO(输入输出)流。

IO流有很多种,按照操作数据的不同,可以分为字节流和字符流,按照数据传输方向的不同又可分为输入流和输出流,程序从输入流中读取数据,向输出流中写入数据。在IO包中,字节流的输入输出流分别用java.io.InputStream 和java.io.OutputStream 表示,字符流的输入输出流分别用java.io.Reader和java.io.Writer表示,具体分类如图所示。

字节流

字节流概念

在计算机中,无论是文本、图片、音频还是视频,所有的文件都是以二进制(字节)形式存在,IO流中针对字节的输入输出提供了一系列的流,统称为字节流。字节流是程序中最常用的流,根据数据的传输方向可将其分为字节输入流和字节输出流。在JDK 中,提供了两个抽象类InputStream 和OutputStream,它们是字节流的顶级父类,所有的字节输入流都继承自InputStream,所有的字节输出流都继承自OutputStream。为了方便理解,可以把InputStream 和OutputStream 比作两根“水管”,如图所示。

图中,InputStream 被看成一个输入管道,OutputStream 被看成一个输出管道,数据通过InputStream 从源设备输入到程序,通过OutputStream 从程序输出到目标设备,从而实现数据的传输。由此可见,IO流中的输入输出都是相对于程序而言的。

在JDK中,InputStream 和OutputStream 提供了一系列与读写数据相关的方法,接下来先来了解一下InputStream 的常用方法,如表所示。

表中列举了InputStream 的四个常用方法。前三个read()方法都是用来读数据的,其中,第一个read()方法是从输入流中逐个读入字节,而第二个和第三个read()方法则将若干字节以字节数组的形式一次性读入,从而提高读数据的效率。在进行IO 流操作时,当前IO流会占用一定的内存,由于系统资源宝贵,因此,在IO 操作结束后,应该调用close()方法关闭流,从而释放当前IO流所占的系统资源。

与InputStream 对应的是OutputStream。OutputStream 是用于写数据的,因此OutputStream 提供了一些与写数据有关的方法,如表所示。

表中,列举了OutputStream 类的五个常用方法。前三个是重载的write()方法,都是用于向输出流写入字节,其中,第一个方法逐个写入字节,后两个方法是将若干个字节以字节数组的形式一次性写入,从而提高写数据的效率。flush()方法用来将当前输出流缓冲区(通常是字节数组)中的数据强制写入目标设备,此过程称为刷新。close()方法是用来关闭流并释放与当前IO流相关的系统资源。

InputStream 和OutputStream 这两个类虽然提供了一系列和读写数据有关的方法,但是这两个类是抽象类,不能被实例化,因此,针对不同的功能,InputStream 和OutputStream 提供了不同的子类,这些子类形成了一个体系结构,如图所示。

从图中可以看出,InputStream 和OutputStream 的子类有很多是大致对应的,比如ByteArrayInputStream 和ByteArrayOutputStream,FileInputStream 和FileOutputStream 等。图中所列出的IO 流都是程序中很常见的,接下来将逐步为大家讲解这些流的具体用法。

字节流读写文件

由于计算机中的数据基本都保存在硬盘的文件中,因此操作文件中的数据是一种很常见的操作。在操作文件时,最常见的就是从文件中读取数据并将数据写入文件,即文件的读写。针对文件的读写,JDK 专门提供了两个类,分别是FileInputStream 和FileOutputStream。

FileInputStream 是InputStream 的子类,它是操作文件的字节输入流,专门用于读取文件中的数据。由于从文件读取数据是重复的操作,因此需要通过循环语句来实现数据的持续读取。接下来通过一个案例来实现字节流对文件数据的读取,首先在D盘目录下创建一个文本文件IO.txt,在文件中输入内容“小海绵”,具体代码如例所示。

import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
public class IOTest {
    public static void main(String[] args) {
        try {
            // 创建一个文件字节输入流
            FileInputStream in = new FileInputStream("D:/IO.txt");
                int b = 0; // 定义一个int 类型的变量b,记住每次读取的一个字节
                while (true) {
                    b = in.read(); // 变量b 记住读取的一个字节
                    if (b == -1) { // 如果读取的字节为-1,跳出while 循环
                        break;
                    }
                    System.out.println(b); // 否则将b 写出
                }
                in.close();
        } catch (FileNotFoundException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

运行结果:

208
161
186
163
195
224

例中,创建的字节流FileInputStream 通过read()方法将当前目录文件“D://IO.txt”中的数据读取并打印。通常情况下读取文件应该输出字符,之所以输出数字是因为硬盘上的文件是以字节的形式存在的,在“IO.txt”文件中,字符’小’,’海’,’绵’各占2个字节,因此,最终结果显示的就是文件中的六个字节所对应的十进制数。

与FileInputStream对应的是FileOutputStream。FileOutputStream 是OutputStream 的子类,它是操作文件的字节输出流,专门用于把数据写入文件。接下来通过一个案例来演示如何将数据写入文件,如例所示。

import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
public class IOTest {
    public static void main(String[] args) {
        try {
            // 创建一个文件字节输出流
            FileOutputStream out = new FileOutputStream("D:/example.txt");
            String str = "小海绵";
            byte[] b = str.getBytes();
            for (int i = 0; i < b.length; i++) {
                out.write(b[i]);
            }
            out.close();
        } catch (FileNotFoundException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

程序运行后,会在D盘目录下生成一个新的文本文件example.txt,打开此文件,文件内容为小海绵。

通过运行结果可以看出,通过FileOutputStream 写数据时,自动创建了文件example.txt,并将数据写入文件。需要注意的是,如果是通过FileOutputStream 向一个已经存在的文件中写入数据,那么该文件中的数据首先会被清空,再写入新的数据。若希望在已存在的文件内容之后追加新内容,则可使用FileOutputStream 的构造函数FileOutputStream(StringfileName,booleanappend)来创建文件输出流对象,并把append参数的值设置为true。接下来通过一个案例来演示如何将数据追加到文件末尾,如例所示。

import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStream;
public class IOTest {
    public static void main(String[] args) {
        try {
            OutputStream out = new FileOutputStream("D:/example.txt", true);
            String str = "炒鸡帅";
            byte[] b = str.getBytes();
            for (int i = 0; i < b.length; i++) {
                out.write(b[i]);
            }
            out.close();
        } catch (FileNotFoundException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

查看D://example.txt文件,文件中的内容为 小海绵炒鸡帅。

文件的拷贝

在应用程序中,IO流通常都是成对出现的,即输入流和输出流一起使用。例如文件的拷贝就需要通过输入流来读取文件中的数据,通过输出流将数据写入文件。接下来通过一个案例来演示如何进行文件内容的拷贝,首先在D盘创建文件夹one,和two,然后在one文件夹中存放一个“example.txt”文件,拷贝文件的代码如例所示。

import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
public class IOTest {
    public static void main(String[] args) {
        try {
            // 创建一个字节输入流,用于读取当前目录下source 文件夹中的mp3 文件
            InputStream in = new FileInputStream("D:/one/example.txt");
            // 创建一个文件字节输出流,用于将读取的数据写入target 目录下的文件中
            OutputStream out = new FileOutputStream("D:/two/example.txt");
            int len; // 定义一个int 类型的变量len,记住每次读取的一个字节
            long begintime = System.currentTimeMillis(); // 获取拷贝文件前的系统时间
            while ((len = in.read()) != -1) { // 读取一个字节并判断是否读到文件末尾
                out.write(len); // 将读到的字节写入文件
            }
            long endtime = System.currentTimeMillis(); // 获取文件拷贝结束时的系统时间
            System.out.println("拷贝文件所消耗的时间是: " + (endtime - begintime) + "毫秒");
            in.close();
            out.close();
        } catch (FileNotFoundException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

运行结果:

拷贝文件所消耗的时间是: 2毫秒

在拷贝过程中,通过while循环将字节逐个进行拷贝。每循环一次,就通过FileInputStream 的read()方法读取一个字节,并通过FileOutputStream 的write()方法将该字节写入指定文件,循环往复,直到len的值为-1,表示读取到了文件的末尾,结束循环,完成文件的拷贝。程序运行结束后,会在命令行窗口打印拷贝文件所消耗的时间。

字节流的缓冲区

虽然上一个案例实现了文件的拷贝,但是一个字节一个字节的读写,需要频繁的操作文件,效率非常低,这就好比从北京运送烤鸭到上海,如果有一万只烤鸭,每次运送一只,就必须运输一万次,这样的效率显然非常低。为了减少运输次数,可以先把一批烤鸭装在车厢中,这样就可以成批的运送烤鸭,这时的车厢就相当于一个临时缓冲区。当通过流的方式拷贝文件时,为了提高效率也可以定义一个字节数组作为缓冲区。在拷贝文件时,可以一次性读取多个字节的数据,并保存在字节数组中,然后将字节数组中的数据一次性写入文件。接下来通过一个案例来学习如何使用缓冲区拷贝文件,如例所示。

import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
public class IOTest {
    public static void main(String[] args) {
        try {
            // 创建一个字节输入流,用于读取当前目录下source 文件夹中的mp3 文件
            InputStream in = new FileInputStream("D:/one/example.txt");
            // 创建一个文件字节输出流,用于将读取的数据写入当前目录的target 文件中
            OutputStream out = new FileOutputStream("D:/one/example.txt");
            // 以下是用缓冲区读写文件
            byte[] buff = new byte[1024]; // 定义一个字节数组,作为缓冲区
            // 定义一个int 类型的变量len 记住读取读入缓冲区的字节数
            int len;
            long begintime = System.currentTimeMillis();
            while ((len = in.read(buff)) != -1) { // 判断是否读到文件末尾
                out.write(buff, 0, len); // 从第一个字节开始,向文件写入len 个字节
            }
            long endtime = System.currentTimeMillis();
            System.out.println("拷贝文件所消耗的时间是: " + (endtime - begintime) + "毫秒");
            in.close();
            out.close();
        } catch (FileNotFoundException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

运行结果:

拷贝文件所消耗的时间是: 0毫秒

在拷贝过程中,使用while循环语句逐渐实现字节文件的拷贝,每循环一次,就从文件读取若干字节填充字节数组,并通过变量len记住读入数组的字节数,然后从数组的第一个字节开始,将len个字节依次写入文件。循环往复,当len值为-1时,说明已经读到了文件的末尾,循环会结束,整个拷贝过程也就结束了,最终程序将整个拷贝过程所消耗的时间打印了出来。

通过两种拷贝方式的对比,可以看出拷贝文件所消耗的时间明显减少了,从而说明缓冲区读写文件可以有效的提高程序的效率。这是因为程序中的缓冲区就是一块内存,用于存放暂时输入输出的数据,使用缓冲区减少了对文件的操作次数,所以可以提高读写数据的效率。

装饰设计模式

俗话说“人靠衣装马靠鞍”,漂亮得体的装扮不仅能提升形象,还能提高竞争力。在程序设计中,同样可以通过“装饰”一个类,增强它的功能。装饰设计模式就是通过包装一个类,动态地为它增加功能的一种设计模式。

装饰设计模式在现实生活中随处可见,比如买了一辆车,想为新车装一个倒车雷达,这就相当于为这辆汽车增加新的功能。接下来通过一个案例来实现上述过程,如例所示。

class Car {
    private String carName; // 定义一个属性,代表车名
    public Car(String carName) {
        this.carName = carName;
    }
    public void show() { // 实现Car 的show()方法
        System.out.println("我是" + carName + ",具有基本功能");
    }
}
// 定义一个类RadarCar
class RadarCar {
    public Car myCar;
    public RadarCar(Car myCar) { // 通过构造方法接收被包装的对象
        this.myCar = myCar;
    }
    public void show() {
        myCar.show();
        System.out.println("具有倒车雷达功能"); // 实现功能的增强
    }
}
public class IOTest {
    public static void main(String[] args) {
        Car benz = new Car("Benz"); // 创建一个NewCar 对象
        System.out.println("--------------包装前--------------");
        benz.show();
        RadarCar decoratedCar_benz = new RadarCar(benz); // 创建一个RadarCar 对象
        System.out.println("--------------包装后--------------");
        decoratedCar_benz.show();
    }
}

运行结果:

--------------包装前--------------
我是Benz,具有基本功能
--------------包装后--------------
我是Benz,具有基本功能
具有倒车雷达功能

例实现了RadarCar类对Car类的包装。包装类RadarCar的构造方法中接收一个Car类型的实例对象。通过运行结果可以看出,当RadarCar对象调用show()方法时,被RadarCar包装后的对象benz不仅具有车的基本功能,而且具有了倒车雷达的功能。

字节缓冲流

在IO包中提供两个带缓冲的字节流,分别是BufferedInputStream和BufferdOutputStream,这两个流都使用了装饰设计模式。它们的构造方法中分别接收InputStream 和OutputStream 类型的参数作为被包装对象,在读写数据时提供缓冲功能。应用程序、缓冲流和底层字节流之间的关系如图所示。

从图中可以看出应用程序是通过缓冲流来完成数据读写的,而缓冲流又是通过底层被包装的字节流与设备进行关联的。接下来通过一个案例来学习BufferedInputStream和BufferedOutputStream 这两个流的用法,如例所示。

import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
public class IOTest {
    public static void main(String[] args) {
        try {
            // 创建一个带缓冲区的输入流
            BufferedInputStream bis = new BufferedInputStream(new FileInputStream("D:/example.txt"));
            // 创建一个带缓冲区的输出流
            BufferedOutputStream bos = new BufferedOutputStream(new FileOutputStream("D:/example.txt"));
            int len;
            while ((len = bis.read()) != -1) {
                bos.write(len);
            }
            bis.close();
            bos.close();
        } catch (FileNotFoundException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

例中,创建了BufferedInputStream 和BufferedOutputStream 两个缓冲流对象,这两个流内部都定义了一个大小为8192的字节数组,当调用read()或者write()方法读写数据时,首先将读写的数据存入定义好的字节数组,然后将字节数组的数据一次性读写到文件中,这种方式与字节流的缓冲区类似,都对数据进行了缓冲,从而有效的提高了数据的读写效率。

字符流

字符流定义及基本用法

前面我们讲过InputStream 类和OutputStream 类在读写文件时操作的都是字节,如果希望在程序中操作字符,使用这两个类就不太方便,为此JDK 提供了字符流。同字节流一样,字符流也有两个抽象的顶级父类,分别是Reader和Writer。其中Reader是字符输入流,用于从某个源设备读取字符,Writer是字符输出流,用于向某个目标设备写入字符。Reader和Writer作为字符流的顶级父类,也有许多子类,接下来通过继承关系图来列出Reader和Writer的一些常用子类,如图所示。

从图可以看到,字符流的继承关系与字节流的继承关系有些类似,很多子类都是成对(输入流和输出流)出现,其中FileReader和FileWriter用于读写文件,BufferedReader和BufferedWriter是具有缓冲功能的流,它们可以提高读写效率。

字符流操作文件

在程序开发中,经常需要对文本文件的内容进行读取,如果想从文件中直接读取字符便可以使用字符输入流FileReader,通过此流可以从关联的文件中读取一个或一组字符。接下来首先在D盘目录下新建文件“example.txt”并在其中输入字符“小海绵”,然后通过一个案例来学习如何使用FileReader读取文件中的字符,如例所示。

import java.io.FileNotFoundException;
import java.io.FileReader;
import java.io.IOException;
public class IOTest {
    public static void main(String[] args) {
        try {
            // 创建一个FileReader 对象用来读取文件中的字符
            FileReader reader = new FileReader("D:/example.txt");
            int ch; // 定义一个变量用于记录读取的字符
            while ((ch = reader.read()) != -1) { // 循环判断是否读取到文件的末尾
                System.out.println((char) ch); // 不是字符流末尾就转为字符打印
            }
            reader.close();
        } catch (FileNotFoundException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

运行结果:

小
海
绵

例实现了读取文件字符的功能。首先创建一个FileReader对象与文件关联,然后通过while循环每次从文件中读取一个字符并打印,这样便实现了FileReader读文件字符的操作。需要注意的是,字符输入流的read()方法返回的是int类型的值,如果想获得字符就需要进行强制类型转换。

例讲解了如何使用FileReader读取文件中的字符,如果要向文件中写入字符就需要使用FileWriter类。FileWriter是Writer的一个子类,接下来通过一个案例来学习如何使用FileWriter将字符写入文件,如例所示。

import java.io.FileNotFoundException;
import java.io.FileWriter;
import java.io.IOException;
public class IOTest {
    public static void main(String[] args) {
        try {
            // 创建一个FileWriter 对象用于向文件中写入数据
            FileWriter writer = new FileWriter("D:/example.txt", true);
            String str = "小哥哥";
            writer.write(str); // 将字符数据写入到文本文件中
            writer.write("\r\n"); // 将输出语句换行
            writer.close(); // 关闭写入流,释放资源
        } catch (FileNotFoundException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

再次运行程序就可以实现在文件中追加内容的效果。

接下来通过一个案例来学习如何使用BufferedReader和BufferedWriter实现文件的拷贝,如例所示。

import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.FileNotFoundException;
import java.io.FileReader;
import java.io.FileWriter;
import java.io.IOException;
public class IOTest {
    public static void main(String[] args) {
        try {
            FileReader reader = new FileReader("D:/one/example.txt");
            // 创建一个BufferedReader 缓冲对象
            BufferedReader br = new BufferedReader(reader);
            FileWriter writer = new FileWriter("D:/one/example.txt");
            // 创建一个BufferdWriter 缓冲对象
            BufferedWriter bw = new BufferedWriter(writer);
            String str;
            while ((str = br.readLine()) != null) { // 每次读取一行文本,判断是否到文件末尾
                bw.write(str);
                bw.newLine(); // 写入一个换行符,该方法会根据不同的操作系统生成相应的换行符
            }
            br.close();
            bw.close();
        } catch (FileNotFoundException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

在例中,首先对输入输出流进行了包装,并通过一个while循环实现了文本文件的拷贝。在拷贝过程中,每次循环都使用readLine()方法读取文件的一行,然后通过write()方法写入目标文件。其中readLine()方法会逐个读取字符,当读到回车’r’或换行’ n’时会将读到的字符作为一行的内容返回。

需要注意的是,由于包装流内部使用了缓冲区,在循环中调用BufferedWriter的write()方法写字符时,这些字符首先会被写入缓冲区,当缓冲区写满时或调用close()方法时,缓冲区中的字符才会被写入目标文件。因此在循环结束时一定要调用close()方法,否则极有可能会导致部分存在缓冲区中的数据没有被写入目标文件。

转换流

前面提到IO流可分为字节流和字符流,有时字节流和字符流之间也需要进行转换。在JDK中提供了两个类可以将字节流转换为字符流,它们分别是InputStreamReader和OutputStreamWriter。

转换流也是一种包装流,其中OutputStreamWriter是Writer的子类,它可以将一个字节输出流包装成字符输出流,方便直接写入字符,而InputStreamReader是Reader的子类,它可以将一个字节输入流包装成字符输入流,方便直接读取字符。通过转换流进行数据读写的过程如图所示。

接下来通过一个案例来学习如何将字节流转为字符流,为了提高读写效率,可以通过BufferedReader和BufferedWriter对转换流进行包装,具体代码如例所示。

import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.OutputStreamWriter;
public class IOTest {
    public static void main(String[] args) {
        try {
            FileInputStream in = new FileInputStream("D:/example.txt"); // 创建字节输入流
            InputStreamReader isr = new InputStreamReader(in);
            // 将字节流输入转换成字符输入流
            BufferedReader br = new BufferedReader(isr); // 对字符流对象进行包装
            FileOutputStream out = new FileOutputStream("D:/example.txt");
            // 将字节输出流转换成字符输出流
            OutputStreamWriter osw = new OutputStreamWriter(out);
            BufferedWriter bw = new BufferedWriter(osw); // 对字符输出流对象进行包装
            String line;
            while ((line = br.readLine()) != null) { // 判断是否读到文件末尾
                bw.write(line); // 输出读取到的文件
            }
            br.close();
            bw.close();
        } catch (FileNotFoundException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

例实现了字节流和字符流之间的转换,将字节流转换为字符流,从而实现直接对字符的读写。需要注意的是,在使用转换流时,只能针对操作文本文件的字节流进行转换,如果字节流操作的是一张图片,此时转换为字符流就会造成数据丢失。

File类

本章前面讲解的IO流可以对文件的内容进行读写操作,在应用程序中还会经常对文件本身进行一些常规操作,例如创建一个文件,删除或者重命名某个文件,判断硬盘上某个文件是否存在,查询文件最后修改时间等。针对文件的这类操作,JDK 中提供了一个File类,该类封装了一个路径,并提供了一系列的方法用于操作该路径所指向的文件,接下来围绕File类展开详细讲解。

File类的常用方法

File类用于封装一个路径,这个路径可以是从系统盘符开始的绝对路径,如D:/example.txt,也可以是相对于当前目录而言的相对路径,如src/example.txt。File类内部封装的路径可以指向一个文件,也可以指向一个目录,在File类中提供了针对这些文件或目录的一些常规操作。接下来首先介绍一下File类常用的构造方法,如表所示。

表中列出了File类的三个构造方法。通常来讲,如果程序只处理一个目录或文件,并且知道该目录或文件的路径,使用第一个构造方法较方便。如果程序处理的是一个公共目录中的若干子目录或文件,那么使用第二个或者第三个构造方法会更方便。

File类中提供了一系列方法,用于操作其内部封装的路径指向的文件或者目录,例如判断文件/目录是否存在、创建、删除文件/目录等。接下来介绍一下File类中的常用方法,如表所示。

表中,列出了File类的一系列常用方法,此表仅仅通过文字对File类的方法进行介绍,对于初学者来说很难弄清它们之间的区别,接下来,首先在D盘目录下创建一个文件“example.txt”并输入内容“小海绵”,然后通过一个案例来演示File类的常用方法,如例所示。

import java.io.File;
public class IOTest {
    public static void main(String[] args) {
        File file = new File("D:/example.txt"); // 创建File 文件对象,表示一个文件
        // 获取文件名称
        System.out.println("文件名称:" + file.getName());
        // 获取文件的相对路径
        System.out.println("文件的相对路径:" + file.getPath());
        // 获取文件的绝对路径
        System.out.println("文件的绝对路径:" + file.getAbsolutePath());
        // 获取文件的父路径
        System.out.println("文件的父路径:" + file.getParent());
        // 判断文件是否可读
        System.out.println(file.canRead() ? "文件可读" : "文件不可读");
        // 判断文件是否可写
        System.out.println(file.canWrite() ? "文件可写" : "文件不可写");
        // 判断是否是一个文件
        System.out.println(file.isFile() ? "是一个文件" : "不是一个文件");
        // 判断是否是一个目录
        System.out.println(file.isDirectory() ? "是一个目录" : "不是一个目录");
        // 判断是否是一个绝对路径
        System.out.println(file.isAbsolute() ? "是绝对路径" : "不是绝对路径");
        // 得到文件最后修改时间
        System.out.println("最后修改时间为:" + file.lastModified());
        // 得到文件的大小
        System.out.println("文件大小为:" + file.length() + " bytes");
        // 是否成功删除文件
        System.out.println("是否成功删除文件" + file.delete());
    }
}

运行结果:

文件名称:example.txt
文件的相对路径:D:\example.txt
文件的绝对路径:D:\example.txt
文件的父路径:D:\
文件可读
文件可写
是一个文件
不是一个目录
是绝对路径
最后修改时间为:1547187548383
文件大小为:6 bytes
是否成功删除文件true

遍历目录下的文件

在表中列举的方法中有一个list()方法,该方法用于遍历某个指定目录下的所有文件的名称,例8-25中没有演示该方法的使用,接下来通过一个案例来演示list()方法的用法,如例所示。

import java.io.File;
public class IOTest {
    public static void main(String[] args) {
        File file = new File("D:/"); // 创建File 对象
        if (file.isDirectory()) { // 判断File 对象对应的目录是否存在
            String[] names = file.list(); // 获得目录下的所有文件的文件名
            for (String name : names) {
                System.out.println(name); // 输出文件名
            }
        }
    }
}

例中,创建了一个File对象,封装了一个路径,通过调用File的isDirectory()方法判断路径指向的是否为存在的目录,如果存在就调用list()方法,获得一个String类型的数组names,数组中包含这个目录下所有文件的文件名。接着通过循环遍历数组names,依次打印出每个文件的文件名。

例实现了遍历一个目录下所有的文件,有时程序只是需要得到指定类型的文件,如获取指定目录下所有的“.java”文件。针对这种需求,File类中提供了一个重载的list(FilenameFilterfilter)方法,该方法接收一个FilenameFilter 类型的参数。FilenameFilter是一个接口,被称作文件过滤器,当中定义了一个抽象方法accept(File dir,String name),在调用list()方法时,需要实现文件过滤器,在accept()方法中做出判断,从而获得指定类型的文件。

为了让初学者更好地理解文件过滤的原理,接下来分步骤分析list(FilenameFilter filter)方法的工作原理。

  • 调用list()方法传入FilenameFilter文件过滤器对象。
  • 取出当前File对象所代表目录下的所有子目录和文件。
  • 对于每一个子目录或文件,都会调用文件过滤器对象的accept(File dir,String name)方法,并把代表当前目录的File对象以及这个子目录或文件的名字作为参数dir和name传递给方法。
  • 如果accept()方法返回true,就将当前遍历的这个子目录或文件添加到数组中,如果返回false,则不添加。

接下来通过一个案例来演示如何遍历指定目录下所有扩展名为.java的文件,如例所示。

import java.io.File;
import java.io.FilenameFilter;
public class IOTest {
    public static void main(String[] args) {
        // 创建File 对象
        File file = new File("D:/test");
        // 创建过滤器对象
        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);
            }
        }
    }
}

例的main()方法中,定义了FilenameFilter文件过滤器对象filter,并且实现了accept()方法,在accept()方法中对当前正在遍历的currFile对象进行判断,只有当currFile对象代表文件,并且扩展名“.java”时,才返回true。在调用File对象的list()方法时将filter过滤器对象传入,就得到包含所有“.java”文件名字的字符串数组。

前面的两个例子演示的都是遍历目录下文件的文件名,有时候在一个目录下,除了文件,还有子目录,如果想得到所有子目录下的File类型对象,list()方法显然不能满足要求,这时需要使用File类提供的另一个方法listFiles()。listFiles()方法返回一个File对象数组,当对数组中的元素进行遍历时,如果元素中还有子目录需要遍历,则需要使用递归。接下来通过一个案例来实现遍历指定目录下的文件,如例所示。

import java.io.File;
public class IOTest {
    public static void main(String[] args) {
        File file = new File("D:/test"); // 创建一个代表目录的File 对象
        fileDir(file);
    }
    public static void fileDir(File dir) {
        File[] files = dir.listFiles(); // 获得表示目录下所有文件的数组
        for (File file : files) { // 遍历所有的子目录和文件
            if (file.isDirectory()) {
                fileDir(file); // 如果是目录,递归调用FileDir()
            }
            System.out.println(file.getAbsolutePath()); // 输出文件的绝对路径
        }
    }
}

运行结果:

D:\test\one
D:\test\test.txt
D:\test\two

例中,定义了一个静态方法fileDir(),方法接收一个表示目录的File对象。在方法中,首先通过调用listFiles()方法把该目录下所有的子目录和文件存到一个File类型的数组files中,接着遍历数组files,对当前遍历的File对象进行判断,如果是目录就重新调用fileDir()方法进行递归,如果是文件就直接打印输出文件的路径,这样该目录下的所有文件就被成功遍历出来了。

删除文件及目录

在操作文件时,经常需要删除一个目录下的某个文件或者删除整个目录,这时大家首先会想到File类的delete()方法,接下来通过一个案例来演示使用delete()方法删除文件,如例所示。

import java.io.File;
public class IOTest {
    public static void main(String[] args) {
        File file = new File("D:/test"); // 这是一个代表目录的File 对象
        if (file.exists()) {
            System.out.println(file.delete());
        }
    }
}

运行结果:

false

图的运行结果中输出了false,这说明删除文件失败了。大家可能会疑惑,为什么会失败呢? 那是因为File类的delete()方法只是删除一个指定的文件,假如File对象代表目录,并且目录下包含子目录或文件,则File类的delete()方法不允许对这个目录直接删除。在这种情况下,需要通过递归的方式将整个目录以及其中的文件全部删除,接下来通过一个案例来演示,如例所示。

import java.io.File;
public class IOTest {
    public static void main(String[] args) {
        File file = new File("D:/test"); // 创建一个代表目录的File 对象
        deleteDir(file); // 调用deleteDir 删除方法
    }
    public static 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();
        }
    }
}

例中,定义了一个删除目录的静态方法deleteDir(),接收一个File类型的参数。在这个方法中,调用listFiles()方法把这个目录下所有的子目录和文件保存到一个File类型的数组files中,然后遍历files,如果是目录就重新调用deleteDir()方法进行递归,如果是文件就直接调用File的delete()方法删除。当删除完一个目录下的所有文件后,再删除当前这个目录,这样便从里层到外层递归地删除了整个目录。

需要注意的是,在Java中删除目录是从虚拟机直接删除而不走回收站,文件将无法恢复,因此在进行删除操作的时候需要格外小心。

字符编码

常用字符集

看战争片时,经常会看到剧中出现收发电报的情况,发报员拿着密码本将文字翻译成某种码文发出,收报员使用同样的密码本将收到的码文再翻译成文字。这个密码本其实是发送方和接收方约定的一套电码表,电码表中规定了文字和电码之间的一一对应关系。

在计算机之间,同样无法直接传输一个一个的字符,而只能传输二进制数据。为了使发送的字符信息能以二进制数据的形式进行传输,同样需要使用一种“密码本”,它叫做字符码表。字符码表是一种可以方便计算机识别的特定字符集,它是将每一个字符和一个唯一的数字对应而形成的一张表。针对不同的文字,每个国家都制定了自己的码表,下面就来介绍几种最常用的字符码表,如表所示。

表中列举了最常用的几种码表,通过选择合适的码表就能完成字符和二进制数据之间的转换,从而实现数据的传输。