天天看点

【Java】9、Java IO 流

文章目录

  • ​​IO 流​​
  • ​​什么是 IO​​
  • ​​字节流​​
  • ​​字节流概念​​
  • ​​字节流读写文件​​
  • ​​文件的拷贝​​
  • ​​字节流的缓冲区​​
  • ​​装饰设计模式​​
  • ​​字节缓冲流​​
  • ​​字符流​​
  • ​​字符流定义及基本用法​​
  • ​​字符流操作文件​​
  • ​​转换流​​
  • ​​File 类​​
  • ​​File 类的常用方法​​
  • ​​遍历目录下的文件​​
  • ​​删除文件及目录​​
  • ​​字符编码​​
  • ​​常用字符集​​
  • ​​微信公众号​​

IO 流

什么是 IO

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

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

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-6cXJDz5g-1642058941599)(http://47.107.171.232/easily-j/images/20190111/9766628d-b101-45b0-b592-9af06cde7531.png)]

字节流

字节流概念

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

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-eZ4OSMv2-1642058941601)(http://47.107.171.232/easily-j/images/20190111/bcfbd193-8f57-444c-a6f4-d8e0d7ac28ea.png)]

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

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

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-FwInIid8-1642058941603)(http://47.107.171.232/easily-j/images/20190111/961a30d6-deb2-4a38-bea3-2c417c2b222f.png)]

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

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

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Qyl5faff-1642058941604)(http://47.107.171.232/easily-j/images/20190111/d2835b5e-42af-4e74-8869-ae9775214172.png)]

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

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

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-mnDjt0W2-1642058941606)(http://47.107.171.232/easily-j/images/20190111/dd8d76df-53c9-480b-a7f2-b75ee2db7c7c.png)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-UJtxL5o4-1642058941607)(http://47.107.171.232/easily-j/images/20190111/baea8226-fb97-432b-8b3f-d970844abd75.png)]

从图中可以看出,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 类型的参数作为被包装对象,在读写数据时提供缓冲功能。应用程序、缓冲流和底层字节流之间的关系如图所示。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-buSIMf4w-1642058941609)(http://47.107.171.232/easily-j/images/20190111/dfccccb9-741b-419b-8e47-5ca52e4540f6.png)]

从图中可以看出应用程序是通过缓冲流来完成数据读写的,而缓冲流又是通过底层被包装的字节流与设备进行关联的。接下来通过一个案例来学习 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 的一些常用子类,如图所示。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-rSGU0iRn-1642058941610)(http://47.107.171.232/easily-j/images/20190111/c2d5f0fd-0f90-4bb8-922a-247e2f96539e.png)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-EUDqEAoO-1642058941611)(http://47.107.171.232/easily-j/images/20190111/eb650e26-dc33-4eb3-8ad9-6860954b7e1e.png)]

从图可以看到,字符流的继承关系与字节流的继承关系有些类似,很多子类都是成对(输入流和输出流)出现,其中 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 的子类,它可以将一个字节输入流包装成字符输入流,方便直接读取字符。通过转换流进行数据读写的过程如图所示。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-NZ3r10TG-1642058941613)(http://47.107.171.232/easily-j/images/20190111/e7663ad2-4ad5-4632-a1a5-48b0d2b3b90c.png)]

接下来通过一个案例来学习如何将字节流转为字符流,为了提高读写效率,可以通过 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 类常用的构造方法,如表所示。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-cDwqJsfq-1642058941614)(http://47.107.171.232/easily-j/images/20190111/6c73686e-16fc-4ed6-b676-390bb5a68b14.png)]

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

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

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ZdzxvURF-1642058941615)(http://47.107.171.232/easily-j/images/20190111/62735cf7-6e4a-41f1-a4cf-6c27f56dab7e.png)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-gAGgFrG1-1642058941617)(http://47.107.171.232/easily-j/images/20190111/7e92b847-0b5a-4b18-a032-cac4fb58bff5.png)]

表中,列出了 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 中删除目录是从虚拟机直接删除而不走回收站,文件将无法恢复,因此在进行删除操作的时候需要格外小心。

字符编码

常用字符集

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

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

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-esDhzKLt-1642058941619)(http://47.107.171.232/easily-j/images/20190111/86838adf-7d69-4ac1-b642-54cd02c634d5.png)]

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

微信公众号