Java学习—IO流

简介: Java学习—IO流

IO流


## 1、I/O流概述 I/O(Input/Output)流,即输入/输出流,是Java中实现输入/输出的基础,它可以方便地实现数据的输入/输出操作。 I/O流有很多种,按照不同的分类方式,可以分为以下三类: **1.字节流和字符流** 根据流操作的数据单位的不同,可以分为字节流和字符流。字节流以字节为单位进行数据的读写,每次读写一个或多个字节数据;字符流以字符为单位进行数据的读写,每次读写一个或者多个字符数据。 **2.输入流和输出流** 根据流传输方向的不同,又可分为输入流和输出流。其中输入流只能从流中读取数据,而不能向其写入数据;输出流只能向流中写入数据,而不能从中读取数据。 **3.节点流和处理流** 根据流的功能不同,可以分为节点流和处理流。其中节点流也被称为低级流,是指可以从一个特定的I/O设备(如磁盘)读写数据的流,它只能直接连接数据源,进行数据的读写操作;处理流也被称为高级流,它用于对一个已存在的节点流进行连接和封装,通过封装后的流来实现流的读写能力。当使用处理流时,程序不会直接连接到实际的数据源,而是连接在已存在的流之上。 Java中的I/O流主要定义在java.io包中,该包下定义了很多类,其中有4个类为流的顶级类,分别为InputStream和OutputStream,Reader和Writer。其中InputStream和OutPutStream是字节流,而Reader和Writer是字符流;InputStream和Reader是输入流,而OutPutStream和Writer是输出流。I/O流顶级类的分类如图1所示。 ![img](https://ucc.alicdn.com/images/user-upload-01/img_convert/82d5f2069f1830ccf92179067474aff9.png) 图1 I/O流的顶层类 ## 2、字节流与字符流 ### 2.1、字节流概述 在计算机中,无论是文本、图片、音频还是视频,所有文件都是以二进制(字节)形式存在的,I/O流中针对字节的输入/输出提供了一系列的流,统称为字节流。字节流是程序中最常用的流,根据数据的传输方向可将其分为字节输入流和字节输出流。在JDK中,提供了两个抽象类InputStream和OutputStream,它们是字节流的顶级父类,所有的字节输入流都继承自InputStream,所有的字节输出流都继承自OutputStream。为了方便理解,可以把InputStream和OutputStream比作两根“水管”,如图1所示。 ![img](https://ucc.alicdn.com/images/user-upload-01/img_convert/188cfd3914a79a6f97a852aec9819d6a.png) 图1 InputStream和OutputStream 图1中,InputStream被看成一个输入管道,OutputStream被看成一个输出管道,数据通过InputStream从源设备输入到程序,通过OutputStream从程序输出到目标设备,从而实现数据的传输。由此可见,I/O流中的输入/输出都是相对于程序而言的。 在JDK中,InputStream和 OutputStream提供了一系列与读写数据相关的方法,接下来先来了解一下InputStream的常用方法,如表1所示。 表1 InputStream的常用方法 | **方法声明** | **功能描述** | | ----------------------------------- | ------------------------------------------------------------ | | int read() | 从输入流读取一个8位的字节,把它转换为0~255之间的整数,并返回这一整数。当没有可用字节时,将返回-1 | | int read(byte[] b) | 从输入流读取若干字节,把它们保存到参数b指定的字节数组中,返回的整数表示读取字节的数目 | | int read(byte[] b,int off,int len) | 从输入流读取若干字节,把它们保存到参数b指定的字节数组中,off指定字节数组开始保存数据的起始下标,len表示读取的字节数目 | | void close() | 关闭此输入流并释放与该流关联的所有系统资源 | 表7-1中列举了InputStream的四个常用方法。前三个read()方法都是用来读数据的,其中,第一个read()方法是从输入流中逐个读入字节,而第二个和第三个read()方法则将若干字节以字节数组的形式一次性读入,从而提高读数据的效率。在进行I/O流操作时,当前I/O流会占用一定的内存,由于系统资源宝贵,因此,在I/O操作结束后,应该调用close()方法关闭流,从而释放当前I/O流所占的系统资源。 与InputStream对应的是OutputStream。OutputStream是用于写数据的,因此OutputStream提供了一些与写数据有关的方法,如表2所示。 表2 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() | 关闭此输出流并释放与此流相关的所有系统资源 | 表2中,列举了OutputStream类的五个常用方法。前三个是重载的write()方法,都用于向输出流写入字节,其中,第一个方法逐个写入字节,后两个方法是将若干个字节以字节数组的形式一次性写入,从而提高写数据的效率。flush()方法用来将当前输出流缓冲区(通常是字节数组)中的数据强制写入目标设备,此过程称为刷新。close()方法是用来关闭流并释放与当前IO流相关的系统资源。 InputStream和OutputStream这两个类虽然提供了一系列和读写数据有关的方法,但是这两个类是抽象类,不能被实例化,因此,针对不同的功能,InputStream和OutputStream提供了不同的子类,这些子类形成了一个体系结构,如图2和图3所示。 ![img](https://ucc.alicdn.com/images/user-upload-01/img_convert/2961971decc7142a8773399db909dff9.png) 图2 InputStream的子类 ![img](https://ucc.alicdn.com/images/user-upload-01/img_convert/32439b10e992c8dd85b3b85696a01d6f.png) 图3 OutputStream的子类 从图2和图3中可以看出,InputStream和OutputStream的子类有很多是大致对应的,比如,ByteArrayInputStream和ByteArrayOutputStream、FileInputStream和FileOutputStream等。图中所列出的I/O流都是程序中很常见的,接下来将为读者讲解开发时常用流的具体用法。 ### 2.2、字节流读写文件 由于计算机中的数据基本都保存在硬盘的文件中,因此操作文件中的数据是一种很常见的操作。在操作文件时,最常见的就是从一个文件中读取数据并将数据写入到另一个文件,这一过程就是文件的读写。针对文件的读写操作,JDK专门提供了两个类,分别是FileInputStream和FileOutputStream。 FileInputStream是InputStream的子类,它是操作文件的字节输入流,专门用于读取文件中的数据。由于从文件读取数据是重复的操作,因此需要通过循环语句来实现数据的持续读取。 接下来通过一个案例来实现字节流对文件数据的读取。首先在Eclipse项目的根目录下创建一个文本文件test.txt,在文件中输入内容“hello”并保存;然后创建一个读取文本文件的类,具体代码如文件1所示。 文件1 Example01.java ```java 1 import java.io.*; 2 public class Example01 { 3 public static void main(String[] args) throws Exception { 4 // 创建一个文件字节输入流来读取文件 5 FileInputStream in = new FileInputStream("test.txt"); 6 // 定义一个int类型的变量b 7 int b = 0; 8 // 通过循环来读取文件,当返回值为-1结束循环 9 while((b=in.read()) != -1){ 10 System.out.println(b); 11 } 12 // 关闭流 13 in.close(); 14 } 15 } ``` 运行结果如图1所示。 ![img](https://ucc.alicdn.com/images/user-upload-01/img_convert/d97ea0966e954006f82256a12f1efbb4.png) 图1 运行结果 文件1中,创建的字节流对象FileInputStream通过read()方法将当前项目中的文件“test.txt”中的数据读取并打印。从图7-5中的运行结果可以看出,结果分别为104、101、108、108和111,这与test.txt文件中显示效果不太一样,这是因为读取到硬盘上的文件是以字节的形式存在的。在“test.txt”文件中,字符‘h’、‘e’、‘l’、‘l’、‘o’各占一个字节,因此,最终结果显示的是文件“test.txt”中的5个字节所对应的ASCII码值。 需要注意的是,在读取文件数据时,必须保证文件在相应目录存在并且是可读的,否则会抛出java.io.FileNotFoundException,即文件找不到的异常。如上述案例如果未在项目目录下创建文本文件“test.txt”,程序运行后,会出现图2所示的异常。 ![img](https://ucc.alicdn.com/images/user-upload-01/img_convert/4654fb99fcd0d2ebafa1aab481b0015a.png) 图2 运行结果 与FileInputStream对应的是FileOutputStream。FileOutputStream是OutputStream的子类,它是操作文件的字节输出流,专门用于把数据写入文件。接下来通过一个案例来演示如何将数据写入文件,如文件2所示。 文件2 Example02.java ```java 1 import java.io.*; 1 public class Example02 { 2 public static void main(String[] args) throws Exception { 3 // 创建文件输出流对象,并指定输出文件名称 4 FileOutputStream out = new FileOutputStream("out.txt"); 5 // 定义一个字符串 6 String str = "hello"; 7 // 将字符串转换为字节数组进行写入操作 8 out.write(str.getBytes()); 9 // 关闭流 10 out.close(); 11 } 12 } ``` 程序运行后,会在项目当前根目录下生成一个新的文本文件out.txt(运行程序后,该文件可能不会立刻显示,此时使用鼠标右击项目,在弹出窗口中,单击“Refresh”,对项目进行刷新即可),打开此文件,会看到如图3所示的内容。 ![img](https://ucc.alicdn.com/images/user-upload-01/img_convert/d80a417f94c27a08cb2bd65175caf09a.png) 图3 out.txt 从图3可以看出,通过FileOutputStream写数据时,自动创建了文件out.txt,并将自定义的字符串内容写入到了目标文件。 需要注意的是,如果是通过FileOutputStream向一个已经存在的文件中写入数据,那么该文件中的数据首先会被清空,再写入新的数据。若希望在已存在的文件内容之后追加新内容,则可使用FileOutputStream的构造函数FileOutputStream(String fileName, boolean append)来创建文件输出流对象,并把append 参数的值设置为true。接下来通过一个案例来演示如何将数据追加到out.txt文件末尾,如文件3所示。 文件3 Example03.java ```java 1 import java.io.*; 2 public class Example03 { 3 public static void main(String[] args) throws Exception { 4 // 创建文件输出流对象,并指定输出文件名称 5 FileOutputStream out = new FileOutputStream("out.txt",true); 6 // 定义一个字符串 7 String str = " world"; 8 // 将字符串转换为字节数组进行写入操作 9 out.write(str.getBytes()); 10 out.close(); 11 } 12 } ``` 程序运行后,查看项目当前目录下的文件“out.txt”,如图4所示。 ![img](https://ucc.alicdn.com/images/user-upload-01/img_convert/bbff9118c511d0c51797d4c01d221792.png) 图4 out.txt 从图4可以看出,程序通过字节输出流对象向文件“out.txt”写入“ world”后,并没有将文件之前的数据清空,而是将新写入的数据追加到了文件的末尾。 由于I/O流在进行数据读写操作时会出现异常,为了代码的简洁,在上面的程序中使用了throws关键字将异常抛出,然而一旦遇到IO异常,I/O流的close()方法将无法执行,流对象所占用的系统资源将不能够释放。因此,为了保证I/O流的close()方法一定执行,通常会将关闭流的操作写在finally代码块中,具体代码如下所示: ```java finally{ try{ if(in!=null) // 如果in不为空,关闭输入流 in.close(); }catch(Exception e){ e.printStackTrace(); } try{ if(out!=null) // 如果out不为空,关闭输出流 out.close(); }catch(Exception e){ e.printStackTrace(); } } ``` ### 2.3、文件的拷贝 在应用程序中,I/O流通常都是成对出现的,即输入流和输出流一起使用。例如文件的拷贝就需要通过输入流来读取源文件中的数据,并通过输出流将数据写入新文件。接下来通过一个案例来演示如何进行文件内容的拷贝。 首先在当前项目目录下创建文件夹source和target(右击项目名称)→【New】→【Folder】),然后在source文件夹中存放一个名称为“src.jpg”的图片文件,拷贝文件的代码如文件1所示。 文件1 Example04.java ```java 1 import java.io.*; 2 public class Example04 { 3 public static void main(String[] args) throws Exception { 4 // 创建文件输入流对象读取指定目录下的文件 5 FileInputStream in = new FileInputStream("source/src.jpg"); 6 // 创建文件输出流对象将读取到的文件内容写入到指定目录的文件中 7 FileOutputStream out = new FileOutputStream("target/dest.jpg"); 8 // 定义一个int类型的变量len 9 int len = 0; 10 // 获取拷贝文件前的系统时间 11 long beginTime = System.currentTimeMillis(); 12 // 通过循环将读取到的文件字节信息写入到新文件 13 while ((len = in.read()) != -1) { 14 out.write(len); 15 } 16 // 获取拷贝之后的系统时间 17 long endTime = System.currentTimeMillis(); 18 // 输出拷贝花费时间 19 System.out.println("花费时间为:"+(endTime-beginTime) +"毫秒"); 20 // 关闭流 21 in.close(); 22 out.close(); 23 } 24 } ``` 程序运行结束后,刷新并打开target文件夹,发现source文件夹中的“src.jpg”文件被成功拷贝到了target文件夹并进行了重命名,如图1所示。 ![img](https://ucc.alicdn.com/images/user-upload-01/img_convert/1d765f9dcb5b6899e49b0876aaf441c1.png) 图1 拷贝前后的文件 文件1中,实现了图片文件的拷贝。在拷贝过程中,通过while循环将字节逐个进行拷贝。每循环一次,就通过FileInputStream的read()方法读取一个字节,并通过FileOutputStream的write()方法将该字节写入指定文件,循环往复,直到读取的长度len的值为-1,表示读取到了文件的末尾,结束循环,完成文件的拷贝。 程序运行结束后,会在命令行窗口打印拷贝图片文件所消耗的时间,如图2所示。 ![img](https://ucc.alicdn.com/images/user-upload-01/img_convert/fa4414933c2d08f17de2dae9c99768fb.png) 图2 运行结果 从图2可以看出,程序拷贝图片文件共消耗了2755毫秒。在拷贝文件时,由于计算机性能等各方面原因,会导致拷贝文件所消耗的时间不确定,因此每次运行程序的结果并不一定相同。 ### 2.4、字节流的缓冲区 虽然上一小节实现了文件的拷贝,但是一个字节一个字节的读写,需要频繁的操作文件,这种效率是非常低的。这就好比从北京运送快递到上海,如果有一万件快递,一件一件的运送就必须运输一万次,这样的效率显然非常低。为了减少运输次数,可以先把一批快递装在一个车厢中,这样就可以成批的运送快递,这时的车厢就相当于一个临时缓冲区。同样,当通过流的方式拷贝文件时,为了提高传输效率,也可以定义一个字节数组作为缓冲区。这样,在拷贝文件时,就可以一次性读取多个字节的数据,将数据先保存在字节数组中,然后将字节数组中的数据一次性写入到新文件中。 接下来通过案例来学习一下如何使用缓冲区来拷贝文件,如文件1所示。 文件1 Example05.java ```java 1 import java.io.*; 2 public class Example05 { 3 public static void main(String[] args) throws Exception { 4 // 创建文件输入流对象读取指定目录下的文件 5 FileInputStream in = new FileInputStream("source/src.jpg"); 6 // 创建文件输出流对象将读取到的文件内容写入到指定目录的文件中 7 FileOutputStream out = new FileOutputStream("target/dest.jpg"); 8 // 定义一个int类型的变量len 9 int len = 0; 10 // 定义一个长度为1024的字节数组 11 byte[] buff = new byte[1024]; 12 // 获取拷贝文件前的系统时间 13 long beginTime = System.currentTimeMillis(); 14 // 通过循环将读取到的文件字节信息写入到新文件 15 while ((len = in.read(buff)) != -1) { 16 // 每循环读取一次字节数组,就将所读取到的内容写入到文件 17 out.write(buff,0,len); 18 } 19 // 获取拷贝之后的系统时间 20 long endTime = System.currentTimeMillis(); 21 // 输出拷贝花费时间 22 System.out.println("花费时间为:"+(endTime-beginTime) +"毫秒"); 23 // 关闭流 24 in.close(); 25 out.close(); 26 } 27 } ``` 文件1同样实现了图片文件的拷贝。在拷贝过程中,使用while循环语句逐渐实现字节文件的拷贝,每循环一次,就从文件读取若干字节填充到字节数组,并通过变量len记住读入数组的字节数,然后从数组的第一个字节开始,将len个字节依次写入到新文件。循环往复,当len值为-1时,说明已经读到了文件的末尾,循环会结束,整个拷贝过程也就结束了,最终程序会将整个文件拷贝到目标文件夹,并将拷贝过程所消耗的时间打印了出来,如图1所示。 ![img](https://ucc.alicdn.com/images/user-upload-01/img_convert/86e7607214f61740c3b03f40eeb5eb72.png) 图1 运行结果 通过图1可以看出拷贝文件所消耗的时间明显减少了很多,这说明使用缓冲区读写文件可以有效的提高程序的传输效率。程序中的缓冲区就是一块内存,该内存主要用于存放暂时输入/输出的数据,由于使用缓冲区减少了对文件的操作次数,所以可以提高读写数据的效率。 ### 2.5、字节流缓冲流 在IO包中提供了两个带缓冲的字节流,分别是BufferedInputStream和BufferedOutputStream,它们的构造方法中分别接收InputStream和OutputStream类型的参数作为对象,在读写数据时提供缓冲功能。应用程序、缓冲流和底层字节流之间的关系如图1所示。 ![img](https://ucc.alicdn.com/images/user-upload-01/img_convert/b568604b6f3906d7e2ab180a6571d097.png) 图1 缓冲流 从图1可以看出,应用程序是通过缓冲流来完成数据读写的,而缓冲流又是通过底层的字节流与设备进行关联的。接下来通过一个案例来学习BufferedInputStream和BufferedOutputStream 这两个缓冲流的用法。 同样以上一小节中的拷贝图片文件为例,使用字节缓冲流的实现代码如文件1所示。 文件1 Example06.java ```java 1 import java.io.*; 2 public class Example06 { 3 public static void main(String[] args) throws Exception { 4 // 创建用于输入和输出的字节缓冲流对象 5 BufferedInputStream bis = new BufferedInputStream( 6 new FileInputStream("source/src.jpg")); 7 BufferedOutputStream bos = new BufferedOutputStream( 8 new FileOutputStream("target/dest.jpg")); 9 // 定义一个int类型的变量len 10 int len = 0; 11 // 获取拷贝文件前的系统时间 12 long beginTime = System.currentTimeMillis(); 13 // 通过循环读取输入字节缓冲流中的数据,并通过输出字节缓冲流写入到新文件 14 while ((bis.read()) != -1) { 15 bos.write(len); 16 } 17 // 获取拷贝之后的系统时间 18 long endTime = System.currentTimeMillis(); 19 // 输出拷贝花费时间 20 System.out.println("花费时间为:"+(endTime-beginTime) +"毫秒"); 21 // 关闭流 22 bis.close(); 23 bos.close(); 24 } 25 } ``` 运行结果如图2所示。 ![img](https://ucc.alicdn.com/images/user-upload-01/img_convert/b2f51083ac6e06308e98e4ac31d295f4.png) 图2 运行结果 文件1中,创建了BufferedInputStream和BufferedOutputStream两个缓冲流对象,这两个流内部都定义了一个大小为8192的字节数组,当调用read()或者write()方法读写数据时,首先将读写的数据存入到定义好的字节数组,然后将字节数组的数据一次性读写到文件中,这种方式与前面小节中讲解的字节流的缓冲区类似,都对数据进行了缓冲,从而有效的提高了数据的读写效率。 ### 2.6、字符流概述 InputStream类和OutputStream类在读写文件时操作的都是字节,如果希望在程序中操作字符,使用这两个类就不太方便了,为此JDK提供了用于实现字符操作的字符流。 同字节流一样,字符流也有两个抽象的顶级父类,分别是Reader和Writer。其中Reader是字符输入流,用于从某个源设备读取字符。Writer是字符输出流,用于向某个目标设备写入字符。Reader和Writer作为字符流的顶级父类,也有许多子类,接下来通过继承关系图来列出Reader和Writer的一些常用子类,如图1和图2所示。 ![img](https://ucc.alicdn.com/images/user-upload-01/img_convert/933139aaca6b0e84f3f322e211785255.png) 图1 Reader的子类 ![img](https://ucc.alicdn.com/images/user-upload-01/img_convert/3eaa4bf5cbd3d73d7ef541176c071c6b.png) 图2 Writer的子类 从图1和2可以看出,字符流的继承关系与字节流的继承关系有些类似,很多子类都是成对(输入流和输出流)出现的,其中FileReader和FileWriter用于读写文件,BufferedReader和BufferedWriter是具有缓冲功能的流,使用它们可以提高读写效率。 ### 2.7、字符流操作文件 在程序开发中,经常需要对文本文件的内容进行读取,如果想从文件中直接读取字符便可以使用字符输入流FileReader,通过此流可以从文件中读取一个或一组字符。接下来通过一个案例来学习如何使用FileReader以字符的形式读取文件中的内容。 首先在项目当前目录下新建文本文件“reader.txt”并在其中输入字符“好好学习,天天向上!”,然后创建一个使用字符输入流FileReader读取文件中字符的类,如文件1所示。 文件1 Example07.java ```java 1 import java.io.*; 2 public class Example07 { 3 public static void main(String[] args) throws Exception { 4 // 创建FileReader对象,并指定需要读取的文件 5 FileReader fileReader = new FileReader("reader.txt"); 6 // 定义一个int类型的变量len,其初始化值为0 7 int len = 0; 8 // 通过循环来判断是否读取到了文件末尾 9 while ((len = fileReader.read()) != -1) { 10 // 输出读取到的字符 11 System.out.print((char)len); 12 } 13 // 关闭流 14 fileReader.close(); 15 } 16 } ``` 运行结果如图1所示。 ![img](https://ucc.alicdn.com/images/user-upload-01/img_convert/fcca63b80afe74ae0916967fa57a7600.png) 图1 运行结果 文件1实现了读取文件字符的功能。首先创建一个FileReader对象来读取指定文件内容,然后通过while循环每次从文件中读取一个字符并打印,这样便实现了FileReader读文件字符的操作。由于字符输入流的read()方法返回的是int类型的值,如果想获得字符就需要进行强制类型转换,所以输出语句中将变量len强转为了char类型。 文件1讲解了如何使用FileReader读取文件中的字符,如果要向文件中写入字符就需要使用FileWriter类,该类是Writer的一个子类。接下来通过一个案例来学习如何使用FileWriter将字符写入文件,如文件2所示。 文件2 Example08.java ```java 1 import java.io.*; 2 public class Example08 { 3 public static void main(String[] args) throws Exception { 4 // 创建字符输出流对象,并指定输出文件 5 FileWriter fileWriter = new FileWriter("writer.txt"); 6 // 将定义的字符写入文件 7 fileWriter.write("轻轻的我走了,\r\n"); 8 fileWriter.write("正如我轻轻的来;\r\n"); 9 fileWriter.write("我轻轻的招手,\r\n"); 10 fileWriter.write("作别西天的云彩。\r\n"); 11 // 关闭流 12 fileWriter.close(); 13 } 14 } ``` 程序运行结束后,会在当前目录下生成一个名称为“writer.txt”的文件,打开此文件会看到如图2所示的内容。 ![img](https://ucc.alicdn.com/images/user-upload-01/img_convert/bfe4e1d1ff2bbd3acddb254536d837b0.png) 图2 writer.txt FileWriter同FileOutputStream一样,如果指定的文件不存在,就会先创建文件,再写入数据,如果文件存在,则会首先清空文件中的内容,再进行写入。如果想在文件末尾追加数据,同样需要调用重载的构造方法,现将文件2中的第5行代码进行如下修改: ```java FileWriter writer = new FileWriter("writer.txt",true); ``` 修改后,再次运行程序,即可实现在文件中追加内容的效果。 上面两个文件示例通过字符流的形式完成了对文件内容的读写操作,但也是逐个字符进行读写,这样也需要频繁的操作文件,效率仍非常低。这里也可以使用提供的字符流缓冲区(类似于字节流缓冲区)进行读写操作,来提高执行效率。 接下来,通过一个案例来学习如何使用字符流的缓冲区实现文件的拷贝,如文件3所示。 文件3 Example09.java ```java 1 import java.io.*; 2 public class Example09 { 3 public static void main(String[] args) throws Exception { 4 // 创建FileReader对象,并指定需要读取的文件 5 FileReader fileReader = new FileReader("reader.txt"); 6 // 创建FileWriter对象,并指定写入数据的目标文件 7 FileWriter fileWriter = new FileWriter("writer.txt"); 8 // 定义一个int类型的变量len,其初始化值为0 9 int len = 0; 10 // 定义一个长度为1024的字符数组 11 char[] buff = new char[1024]; 12 // 通过循环来判断是否读取到了文件末尾 13 while ((len = fileReader.read(buff)) != -1) { 14 // 输出读取到的字符 15 fileWriter.write(buff, 0, len); 16 } 17 // 关闭流 18 fileReader.close(); 19 fileWriter.close(); 20 } 21 } ``` 文件3使用字符流缓冲区实现了文本文件reader.txt的拷贝,具体操作与字节流缓冲区实现文件拷贝类似,只是创建的缓冲区不一样,这里是使用了new char[1024]创建了一个字符缓冲区。 同样,在字节流中提供了带缓冲区功能的字节缓冲流,字符流也提供了带缓冲区的字符缓冲流,分别是BufferedReader和BufferedWriter,其中BufferedReader用于对字符输入流进行操作,BufferedWriter用于对字符输出流进行操作。需要注意的是,在BufferedReader中有一个重要的方法readLine(),该方法用于一次读取一行文本。 接下来通过一个案例来学习如何使用这两个缓冲流实现文件的拷贝,如文件4所示。 文件4 Example10.java ```java 1 import java.io.*; 2 public class Example10 { 3 public static void main(String[] args) throws Exception { 4 // 创建一个字符输入缓冲流对象 5 BufferedReader br = new BufferedReader( 6 new FileReader("reader.txt ")); 7 // 创建一个字符输出缓冲流对象 8 BufferedWriter bw = new BufferedWriter( 9 new FileWriter("writer.txt")); 10 // 声明一个字符串变量str 11 String str = null; 12 // 循环时每次读取一行文本,如果不为null(即到了文件末尾),则继续循环 13 while ((str = br.readLine()) != null) { 14 // 通过缓冲流对象写入文件 15 bw.write(str); 16 // 写入一个换行符,该方法会根据不同的操作系统生成相应的换行符 17 bw.newLine(); 18 } 19 // 关闭流 20 br.close(); 21 bw.close(); 22 } 23 } ``` 程序运行结束后,同样会将源文件“reader.txt”中的内容拷贝到目标文件“writer.txt”中,结果如图3所示。 ![img](https://ucc.alicdn.com/images/user-upload-01/img_convert/11688d71f3ffa8bb19caedde3550a4ed.png) 图3 拷贝后文件的内容 在文件4中,使用了输入输出字符缓冲流BufferedReader和BufferedWriter,并通过一个while循环实现了文本文件的拷贝。在拷贝过程中,每次循环都使用readLine()方法读取文件的一行,然后通过write()方法写入目标文件,同时使用了newLine()进行换行写入,否则读取源文件所有行内容都会追加写入目标文件一行中。其中readLine()方法会逐个读取字符,当读到回车符“\r”或换行符“\n”时会将读到的字符作为一行的内容返回。 需要注意的是,在字符缓冲流内部也使用了缓冲区,在循环中调用BufferedWriter的write()方法写入字符时,这些字符首先会被写入缓冲区,当缓冲区写满时或调用close()方法时,缓冲区中的字符才会被写入目标文件。因此在循环结束时一定要调用close()方法,否则极有可能会导致部分存在缓冲区中的数据没有被写入目标文件。 ### 2.8、转换流 前面提到IO流可分为字节流和字符流,有时字节流和字符流之间也需要进行转换。在JDK中,提供了两个类用于实现将字节流转换为字符流,它们分别是InputStreamReader和OutputStreamWriter。InputStreamReader是Reader的子类,它可以将一个字节输入流转换成字符输入流,方便直接读取字符。OutputStreamWriter是Writer的子类,它可以将一个字节输出流转换成字符输出流,方便直接写入字符。 通过转换流进行数据读写的过程如图1所示。 ![img](https://ucc.alicdn.com/images/user-upload-01/img_convert/def956365a2a974f812ffba2efdbc2f7.png) 图1 字节流字符流转换过程 接下来通过一个案例来学习如何将字节流转为字符流,为了提高读写效率,可以通过BufferedReader和BufferedWriter来实现转换工作,具体代码如文件1所示。 文件1 Example11.java ```java 1 import java.io.*; 2 public class Example11 { 3 public static void main(String[] args) throws Exception { 4 // 1、创建字节输入流对象,获取源文件 5 FileInputStream in = new FileInputStream("reader.txt"); 6 // 将字节输入流对象转换成字符输入流对象 7 InputStreamReader isr = new InputStreamReader(in); 8 // 创建字符输入缓冲流对象 9 BufferedReader br = new BufferedReader(isr); 10 // 2、创建字节输出流对象,指定目标文件 11 FileOutputStream out = new FileOutputStream("writer.txt"); 12 // 将字节输出流对象转换成字符输出流对象 13 OutputStreamWriter osw = new OutputStreamWriter(out); 14 // 创建字符输出缓冲流对象 15 BufferedWriter bw = new BufferedWriter(osw); 16 // 定义一个字符串变量 17 String line = null; 18 // 通过循环判断是否读到文件末尾 19 while ((line = br.readLine()) != null) { 20 // 输出读取到的文件 21 bw.write(line); 22 bw.newLine(); 23 } 24 // 关闭流 25 br.close(); 26 bw.close(); 27 } 28 } ``` 程序运行结束后,同样会将源文件“reader.txt”中的内容拷贝到目标文件“writer.txt”中,结果如图2所示。 ![img](https://ucc.alicdn.com/images/user-upload-01/img_convert/4bd5ddf877011cdf7c965ffd7455e0c1.png) 图2 拷贝后文件内容 上述程序实现了将字节流转换为字符流并实现文本文件拷贝的功能。 需要注意的是,在使用转换流时,只能针对操作文本文件的字节流进行转换,如果字节流操作的是字节码内容的文件(如图片、视频等),此时转换为字符流就会造成数据丢失。 ## 3、File类 ### 3.1、File类的常用方法 File类用于封装一个路径,这个路径可以是从系统盘符开始的绝对路径,如:“D:\file\a.txt”,也可以是相对于当前目录而言的相对路径,如:“src\Hello.java”。File类内部封装的路径可以指向一个文件,也可以指向一个目录,在File类中提供了针对这些文件或目录的一些常规操作。 接下来介绍一下File类常用的构造方法,如表1所示。 表1 File类常用的构造方法 | **方法声明** | **功能描述** | | -------------------------------- | ------------------------------------------------------------ | | File(String pathname) | 通过指定的一个字符串类型的文件路径来创建一个新的File对象 | | File(String parent,String child) | 根据指定的一个字符串类型的父路径和一个字符串类型的子路径(包括文件名称)创建一个File对象 | | File(File parent,String child) | 根据指定的File类的父路径和字符串类型的子路径(包括文件名称)创建一个File对象 | 表1中列出了File类的三个构造方法。通常来讲,如果程序只处理一个目录或文件,并且知道该目录或文件的路径,使用第一个构造方法较方便。如果程序处理的是一个公共目录中的若干子目录或文件,那么使用第二个或者第三个构造方法会更方便。 File类中提供了一系列方法,用于操作其内部封装的路径指向的文件或者目录,例如判断文件/目录是否存在、创建和删除文件/目录等。接下来介绍一下File类中的常用方法,如表2所示。 表2 File类的常用方法 | **方法声明** | **功能描述** | | ------------------------------------ | ------------------------------------------------------------ | | boolean exists() | 判断File对象对应的文件或目录是否存在,若存在则返回ture,否则返回false | | boolean delete() | 删除File对象对应的文件或目录,若成功删除则返回true,否则返回false | | boolean createNewFile() | 当File对象对应的文件不存在时,该方法将新建一个此File对象所指定的新文件,若创建成功则返回true,否则返回false | | String getName() | 返回File对象表示的文件或文件夹的名称 | | String getPath() | 返回File对象对应的路径 | | String getAbsolutePath() | 返回File对象对应的绝对路径(在Unix/Linux等系统上,如果路径是以正斜线/开始,则这个路径是绝对路径;在Windows等系统上,如果路径是从盘符开始,则这个路径是绝对路径) | | String getParent() | 返回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() | 列出指定目录的全部内容,只是列出名称 | | String[] list(FilenameFilter filter) | 接收一个FilenameFilter参数,通过该参数可以只列出符合条件的文件 | | File[] listFiles() | 返回一个包含了File对象所有子文件和子目录的File数组 | 表2中,列出了File类的一系列常用方法,此表仅仅通过文字对File类的方法进行介绍,对于初学者来说很难弄清它们之间的区别,接下来,通过一个案例来演示File类的常用方法。 首先,在项目当前目录下创建一个文件“example.txt”,并在文件中输入内容“File”,然后创建一个使用File常用方法的类,来查看文件的相应信息,如文件1所示。 文件1 Example12.java ```java 1 import java.io.*; 2 public class Example12 { 3 public static void main(String[] args) { 4 // 创建File文件对象 5 File file = new File("example.txt"); 6 System.out.println("文件名称:" + file.getName()); 7 System.out.println("文件的相对路径:" + file.getPath()); 8 System.out.println("文件的绝对路径:" + file.getAbsolutePath()); 9 System.out.println("文件的父路径:" + file.getParent()); 10 System.out.println(file.canRead() ? "文件可读" : "文件不可读"); 11 System.out.println(file.canWrite() ? "文件可写": "文件不可写"); 12 System.out.println(file.isFile() ? "是一个文件" :"不是一个文件"); 13 System.out.println(file.isDirectory()? "是一个目录":"不是一个目录"); 14 System.out.println(file.isAbsolute() ? "是绝对路径": "不是绝对路径"); 15 System.out.println("最后修改时间为:" + file.lastModified()); 16 System.out.println("文件大小为:" + file.length() + " bytes"); 17 System.out.println("是否成功删除文件"+file.delete()); 18 } 19 } ``` 运行结果如图1所示。 ![img](https://ucc.alicdn.com/images/user-upload-01/img_convert/ed6620b42de4153caf0586db0626ae06.png) 图1 运行结果 在文件1中,调用File类的一系列方法获取到了文件的名称、相对路径、绝对路径、文件是否可读等信息,并通过delete()方法将文件删除。 ### 3.2、遍历目录下的文件 File类中有一个list()方法,该方法用于遍历某个指定目录下的所有文件的名称,接下来通过一个案例来演示list()方法的使用,如文件1所示。 文件1 Example13.java ```java 1 import java.io.File; 2 import java.util.Arrays; 3 public class Example13 { 4 public static void main(String[] args) { 5 // 创建File对象,并指定文件路径 6 File file = new File("F:\\Java基础入门\\workspace\\chapter07"); 7 // 判断是否是目录 8 if (file.isDirectory()) { 9 // 获取目录中的所有文件的名称 10 String[] fileNames = file.list(); 11 // 对指定路径下的文件或目录进行遍历 12 Arrays.stream(fileNames) 13 .forEach(f -> System.out.println(f)); 14 } 15 } 16 } ``` 运行结果如图1所示。 ![img](https://ucc.alicdn.com/images/user-upload-01/img_convert/288d38ab592a7c07ce700f1012305a32.png) 图1 运行结果 在文件1中,创建了一个File对象,并指定了一个路径“F:\Java基础入门\workspace\chapter07”(本章项目存放在本地磁盘路径,读者在进行测试时可自行指定),通过调用File的isDirectory()方法判断路径指向是否为目录,如果是目录就调用list()方法,获得一个String类型的数组fileNames,数组中包含这个目录下所有文件的文件名(包括文件和文件夹名称)。接着通过数组工具类Arrays的stream()方法将数组先转换为Stream流并进行遍历,依次打印出每个文件的文件名。 虽然上使用list()方法可以遍历某个目录下的所有文件,但有时只需要获取指定类型的文件,如获取指定目录下所有的“.txt”文件。针对这种需求,File类中提供了一个重载的list(FilenameFilter filter)方法,该方法接收一个FilenameFilter接口类型的参数。FilenameFilter是一个函数式接口,被称作文件过滤器,接口中定义了一个抽象方法accept(File dir,String name)用于依次对指定File的所有子目录或文件进行迭代。在调用list(FilenameFilter filter)方法时,需要实现文件过滤器FilenameFilter,并在accept(File dir,String name)方法中进行筛选,从而获得指定类型的文件。 接下来,在上一个案例基础上来演示如何筛选遍历指该目录下所有扩展名为“.txt”的文件,如文件2所示。 文件2 Example14.java ```java 1 import java.io.*; 2 import java.util.Arrays; 3 public class Example14 { 4 public static void main(String[] args){ 5 // 创建File对象,并指定文件路径 6 File file = new File("F:\\Java基础入门\\workspace\\chapter07"); 7 // 判断是否是目录 8 if (file.isDirectory()) { 9 // 使用Lambda表达式过滤目录中所有以.txt结尾的文件的名称 10 String[] fileNames = file.list( 11 (dir,name) -> name.endsWith(".txt")); 12 // 对指定路径下的文件或目录进行遍历 13 Arrays.stream(fileNames) 14 .forEach(f -> System.out.println(f)); 15 } 16 } 17 } ``` 运行结果如图2所示。 ![img](https://ucc.alicdn.com/images/user-upload-01/img_convert/f21ab6b555bcfe1bbeb2a14d3c7145f7.png) 图2 运行结果 在文件2中,在第10~11行代码中调用list(FilenameFilter filter)方法时传入了一个Lambda表达式形式的参数,该参数实现了函数式接口filenameFilter中的accept(File dir,String name)方法,对目录下的文件进行过滤,筛选出所有以“.txt”结尾的文件,然后使用数组工具类Arrays的stream()方法将数组先转换为Stream流并进行遍历,打印出筛选结果。 有时候在一个目录下,除了文件还有子目录,如果想得到所有子目录下的File类型对象,list()方法显然不能满足要求,这时需要使用File类提供的另一个方法listFiles()。listFiles()方法返回一个File对象数组,当对数组中的元素进行遍历时,如果元素中还有子目录需要遍历,则可以使用递归再次遍历子目录。 接下来通过一个案例来实现遍历指定目录下的文件,如文件3所示。 文件3 Example15.java ```java 1 import java.io.*; 2 public class Example15 { 3 public static void main(String[] args){ 4 // 创建File对象,并指定文件路径 5 File file = new File(" F:\\Java基础入门\\workspace\\chapter07"); 6 // 调用fileDir()方法,遍历目录 7 fileDir(file); 8 } 9 // 遍历目录及其子目录 10 public static void fileDir(File file) { 11 // 获得目录下所有文件,并赋给数组 12 File[] listFiles = file.listFiles(); 13 // 循环遍历数组 14 for (File files : listFiles) { 15 // 如果遍历的是目录,则递归调用fileDir()方法 16 if(files.isDirectory()){ 17 fileDir(files); 18 } 19 // 输出文件路径 20 System.out.println(files); 21 } 22 } 23 } ``` 运行结果如图3所示。 ![img](https://ucc.alicdn.com/images/user-upload-01/img_convert/dc207f7593b7b05a1cac94fe7cd8a17c.png) 图3 运行结果 在文件3中,定义了一个静态方法fileDir(),该方法接收一个表示目录的File对象。在方法中,首先通过调用listFiles()方法把该目录下所有的子目录和文件存到一个File类型的数组listFiles中,接着遍历数组listFiles,并对当前遍历的File对象进行判断,如果是目录就递归调用fileDir()方法进行子目录遍历,如果是文件就直接打印输出文件的路径,这样该目录下的所有文件就被成功遍历出来了。 ### 3.3、删除文件及目录 在操作文件时,经常需要删除一个目录下的某个文件或者整个文件夹,这时可以使用File类的delete()方法来实现,在使用该方法时需要判断当前目录下是否存在文件,如果存在则需要先删除内部文件,然后再删除空的文件夹。 接下来通过一个案例讲解如何使用delete()方法删除指定目录下的文件和文件夹,如文件1所示。 文件1 Example16.java ```java 1 import java.io.*; 2 public class Example16 { 3 public static void main(String[] args){ 4 // 创建File对象,并指定文件路径 5 File files = new File("D:\\test\\新建文件夹"); 6 // 调用删除方法 7 deleteDir(files); 8 } 9 // 删除方法 10 public static void deleteDir(File files) { 11 // 获取File对象中的所有文件,并将其放在数组中 12 File[] listFiles = files.listFiles(); 13 // 循环遍历数组 14 for (File file : listFiles) { 15 // 如果是目录文件,则递归调用删除方法 16 if(file.isDirectory()){ 17 deleteDir(file); 18 } 19 // 如果是文件,则删除 20 file.delete(); 21 } 22 // 删除文件夹内所有文件后,再删除文件夹 23 files.delete(); 24 } 25 } ``` 在文件1中,指定要删除文件的路径为“D:\test\新建文件夹”(读者测试时需要提前创建自行定义删除的文件路径),接着定义了一个删除目录的静态方法deleteDir()来接收一个File类型的参数。在这个方法中,通过listFiles()方法把这个目录下所有的子目录和文件保存到一个File类型的数组listFiles中,然后通过for循环遍历数组。在循环过程中,如果是目录,就递归调用deleteDir()方法进行遍历;如果是文件,就直接调用File的delete()方法删除。当删除完一个文件夹下的所有文件后,还需要删除当前这个最外层文件夹,这样便从里层到外层递归地删除了整个目录。 注意: 在Java中删除目录的操作是通过Java虚拟机直接删除而不走回收站的,文件一旦删除就无法恢复,因此在进行删除操作的时候需要格外小心。 ## 4、RandomAccessFile 前面介绍的I/O流有一个共同特点,就是只能按照数据的先后顺序读取源设备中的数据,或者按照数据的先后顺序向目标设备写入数据,但如果希望从文件的任意位置开始执行读写操作,则字节流和字符流都无法实现。为此,在IO包中,提供了一个RandomAccesseFile类,它不属于流类,但具有读写文件数据的功能,可以随机从文件的任何位置开始执行读写数据的操作。 RandomAccessFile可以将文件以指定的操作权限(如只读、可读写等)的方式打开,具体使用哪种权限取决于创建它所采用的构造方法,接下来列举RandomAccessFile的构造方法,如表1所示。 表1 RandomAccessFile的构造方法 | **方法声明** | **功能描述** | | ----------------------------------------- | ---------------------------------------------------------- | | RandomAccessFile(File file,String mode) | 使用参数file指定被访问的文件,并使用mode来指定访问模式 | | RandomAccessFile(String name,String mode) | 使用参数name指定被访问文件的路径,并使用mode来指定访问模式 | 表1中,列举了创建RandomAccessFile对象的两个构造方法。通过这两种构造方法创建RandomAccessFile对象时,都需要接受两个参数,第一个参数指定关联的文件,第二个参数mode指定访问文件的模式,也就是文件的操作权限。 参数mode有四个值,这四个值的含义如下: ● r:表示以只读的方式打开文件。如果试图对RandomAccessFile对象执行写入操作,会抛出IOException异常, ● rw:表示以“读写”的方式打开文件。如果该文件不存在,则会自动创建该文件。 ● rws:表示以“读写”方式打开文件。与“rw”相比,它要求对文件的内容或元数据的每个更新都同步写入到底层的存储设备。 ● rwd:表示以“读写”方式打开文件。与“rw”相比,它要求对文件的内容的每个更新都同步写入到底层的存储设备。 RandomAccessFile对象中包含了一个记录指针来标识当前读写处的位置。当程序新建RandomAccessFile对象时,该对象的文件记录指针会在文件开始处(即标识为0的位置),当读写了n个字节后,文件记录指针会向后移动n个字节。除了按顺序读写外,RandomAccessFile对象还可以自由的移动记录指针,既可以向前移动,也可以向后移动。 RandomAccessFile类针对文件的随机操作,提供了一系列常用方法,如表2所示。 表2 RandomAccesseFile中的常用方法 | **方法声明** | **功能描述** | | ------------------------------ | ---------------------------------------------------- | | long getFilePointer() | 返回当前读写指针所处的位置 | | void seek(long pos) | 设定读写指针的位置,与文件开头相隔pos个字节数 | | int skipBytes(int n) | 使读写指针从当前位置开始,跳过n个字节 | | void write(byte[] b) | 将指定的字节数组写入到这个文件,并从当前文件指针开始 | | void setLength(long newLength) | 设置此文件的长度 | | final String readLine() | 从指定文件当前指针读取下一行内容 | 在表2中,seek(long pos)方法可以使记录指针向前、向后自由移动,通过RandomAccessFile的getFilePointer()方法,便可获取文件当前记录指针的位置。 RandomAccessFile在实际开发中也有常见的应用,大家都知道,有一些软件在使用时是需要付费的,但是一般都会有几次免费试用的机会。接下来使用RandomAccessFile类来模拟实现记录软件试用次数的过程。在编写这个程序之前需要在当前目录下创建一个文本文件“time.txt”,在文件中输入数字5作为软件试用的次数,代码如文件1所示。 文件1 Example17.java ```java 1 import java.io.*; 2 public class Example17 { 3 public static void main(String[] args) throws Exception{ 4 // 创建RandomAccessFile对象,并以读写模式打开time.txt文件 5 RandomAccessFile raf = new RandomAccessFile("time.txt", "rw"); 6 // 读取还可以使用次数,第一次读取时times为5 7 int times = Integer.parseInt(raf.readLine())-1; 8 // 判断剩余次数 9 if(times > 0){ 10 // 每执行一次代表使用一次,次数就减少一次 11 System.out.println("您还可以试用"+ times+"次!"); 12 // 将记录指针重新指向文件开头 13 raf.seek(0); 14 // 将剩余次数再次写入文件 15 raf.write((times+"").getBytes()); 16 }else{ 17 System.out.println("试用次数已经用完!"); 18 } 19 // 关闭这个随机存取文件流并释放任何系统 20 raf.close(); 21 } 22 } ``` 上述程序运行两次后的结果如图1所示。 ![img](https://ucc.alicdn.com/images/user-upload-01/img_convert/fe916eaa9da428889bdb0ff74b0da056.png) 图1 运行结果 在文件1中,在当前目录下创建了一个RandomAccessFile对象关联访问的文件“time.txt”,并设置为“rw”的访问模式。在使用软件时,使用变量times记录软件能够试用的次数,time.txt中原本记录次数为5,每执行一次程序,软件会把试用次数减1,同时提示用户剩余试用次数,然后通过调用raf.seek(0)方法把文件的记录指针跳转到文件头的位置,并通过write()方法将剩余的次数重新写入文件。当表示试用的次数times<=0时,则提示“试用次数已经用完!”。最后调用raf.close()方法关闭,便完成了软件试用的功能。 ## 5、对象序列化 程序在运行过程中,可能需要将一些数据永久的保存到磁盘上,而数据在Java中都是保存在对象当中的。那么我们要怎样将对象中的数据保存到磁盘上呢?这时就需要使用Java中的对象序列化。 对象的序列化(Serializable)是指将一个Java对象转换成一个I/O流中字节序列的过程。其目的是为了将对象保存到磁盘中,或允许在网络中直接传输对象。对象序列化机制可以使内存中的Java对象转换成与平台无关的二进制流,既可以将这种二进制流持久地保存在磁盘上,又可以通过网络将这种二进制流传输到另一个网络节点,其他程序在获得了这种二进制流后,还可以将它恢复成原来的Java对象。这种将I/O流中的字节序列恢复为Java对象的过程被称之为反序列化(Deserialize)。 如果想让某个对象支持序列化机制,那么这个对象所在的类必须是可序列化的。在Java中,可序列化的类必须实现Serializable或Externalizable两个接口之一。这两个接口实现序列化机制的主要区别如表1所示。 表1 实现Serializable与实现Externalizable的对比 | **实现Serializable\****接口** | **实现Externalizable\****接口** | | ------------------------------------------------------------ | ------------------------------------------------------------ | | 系统自动存储必要的信息 | 由程序员决定所存储的信息 | | Java内部支持,易于实现,只需实现该接口即可,不需要其他代码支持 | 接口中只提供了两个空方法,实现该接口必须为两个空方法提供实现 | | 性能较差 | 性能较好 | 与实现Serializable接口相比,虽然实现Externalizable接口可以带来一定性能上的提升,但也将导致编程的复杂度增加。在实际开发时,大部分都是采用实现Serializable接口的方式来实现序列化的。 使用Serializable接口实现序列化非常简单,只需要让目标类实现Serializable接口即可,无需实现任何方法。例如让Person类实现序列化接口的代码如下: ```java public class Person implements Serializable{ // 为该类指定一个serialVersionUID变量值 private static final long serialVersionUID = 1L; //声明变量 private int id; private String name; private int age; // 此处省略各属性的getter和setter方法 ... } ``` 在上述代码中,Person类实现了Serializable接口,并指定了一个serialVersionUID变量值,该变量值的作用是标识Java类的序列化版本。如果不显示的定义serialVersionUID变量值,那么将由JVM根据类的相关信息计算出一个serialVersionUID变量值。 小提示: serialVersionUID适用于Java的序列化机制。简单来说,Java的序列化机制是通过判断类的serialVersionUID来验证版本一致性的。在进行反序列化时,JVM会把传来的字节流中的serialVersionUID与本地相应实体类的serialVersionUID进行比较,如果相同就认为是一致的,可以进行反序列化,否则就会出现序列化版本不一致的异常。因此,为了在反序列化时确保序列化版本的兼容性,最好在每一个要序列化的类中加入private static final long serialVersionUID的变量值,具体数值可自定义(默认是1L,系统还可以根据类名、接口名、成员方法及属性等生成的一个64位的哈希字段)。这样,某个对象被序列化之后,即使它所对应的类被修改了,该对象也依然可以被正确的反序列化。 ## 6、NIO ### 6.1、NIO概述 NIO是为替代传统标准的I/O而出现的。与标准的IO相比,NIO提供了一种与I/O不同的工作方式。NIO采用内存映射文件的方式来处理输入/输出,它将文件或文件的一段区域映射到内存中,这样就可以像访问内存一样来访问文件了。 在标准IO中,使用的是字节流和字符流,而在NIO中,使用的是通道(Channel)和缓冲区(Buffer)。数据总是从通道读入缓冲区,或从缓冲区写入通道。 在Java API中,与NIO相关的包介绍如下: ● java.nio:主要包含各种与Buffer相关的类。 ● java.nio.channels:主要包含与Channel和Selector(多线程相关选择器)相关的类。 ● java.nio.channels.spi:主要包含与Channel相关的服务提供者编程接口。 ● java.nio.charset:主要包含与字符集相关的类。 ● java.nio.charset.spi:主要包含与字符集相关的服务提供者编程接口。 NIO主要有三大核心部分:Buffer、Channel和Selector。其中Buffer可以被看成是一个容器,其本质是一个数组缓冲区,读入或写出到Channel中的所有对象都会先放在Buffer中;Channel是对传统的输入/输出的模拟,在NIO中,所有的数据都需要通过通道流的形式传输;Selector(选择器)用于监听多个通道的事件(例如:连接打开、数据到达等),主要用于多线程处理。 ### 6.2、Buffer(缓冲器) Java NIO中的Buffer用于和NIO中的Channel进行交互,交互时数据会从Channel读取到Buffer中,或从Buffer写入到Channel中,如图1所示。 ![img](https://ucc.alicdn.com/images/user-upload-01/img_convert/8078252c42af99095175bae5ce87f378.png) 图1 Buffer和Channel 从结构上来说,Buffer类似于一个数组,它可以保存多个类型相同的数据。从类型上来说,Buffer是一个抽象类,其子类有ByteBuffer、CharBuffer、DoubleBuffer、FloatBuffer、IntBuffer、LongBuffer和ShortBuffer,这些子类中最常用的是ByteBuffer和CharBuffer,其他则使用较少。 Buffer类的子类中并没有提供构造方法,因此不能通过构造方法来创建对象。要想创建Buffer对象,通常会通过子类中的static XxxBuffer allocate(int capacity)方法来实现,其中Xxx表示不同的数据类型,而capacity表示容量,此方法的含义是创建一个容量为capacity的XxxBuffer对象。例如创建一个容量为6的CharBuffer对象的语句如下: ```java CharBuffer buffer = CharBuffer.allocate(6); ``` 在学习Buffer的使用之前,需要对Buffer中的三个重要概念有所理解,这三个概念分别是capacity(容量)、limit(界限)和position(位置),其含义如下: ● capacity(容量):缓冲区的容量表示该Buffer的最大数据容量,即最多可以存储多少数据。缓冲区的容量值不能为负数,也不能够改变。 ● limit(界限):表示Buffer容器中不可被读取的区域的第一个索引,即位于Buffer容器中索引为0到limit之间的区域都可以进行读取操作。缓冲区的limit值从不为负,也从不大于其容量。 ● position(位置):用于指定下一个可以被读写的缓冲区位置索引。新创建的Buffer对象,position的默认值为0,每进行一次读取或写入操作,position的值都会自动向后移动一步。如果向Buffer缓冲区中执行8次写入操作,那么position的值为8,即指向Buffer中的第9个元素的索引位置。 在Buffer类中,定义了很多方法,其常见方法如表1所示。 表1 Buffer类的常用方法 | **方法声明** | **功能描述** | | -------------------------------- | ------------------------------------------------------------ | | int capacity() | 获取缓冲区的大小 | | Buffer clear() | 清除缓冲区,将position设置为0,limit设置为capacity | | Buffer flip() | 反转缓冲区,先将limit设置为当前position位置,然后再将position设置为0 | | boolean hasRemaining() | 判断当前位置(position)和界限(limit)之间是否还有元素 | | int limit | 获取Buffer的limit位置 | | Buffer limit(int newLimit) | 设置limit的值,并返回一个新的limit缓冲区对象 | | Buffer mark() | 设置Buffer的标记(mark),只能在0与position之间做标记 | | int position() | 获取Buffer中position的值 | | Buffer position(int newPosition) | 设置Buffer的position,并返回位置被修改之后的Buffer对象 | | int remaining() | 获取当前位置和界限之间的元素个数 | | Buffer reset() | 将此缓冲区的位置重置为先前标记的位置 | | Buffer rewind() | 倒带缓冲区,将position设置为0,并取消设置的标记 | 除上表中的方法外,Buffer的所有子类中都额外提供了put()和get()方法用于向Buffer中放入数据和取出数据。在使用put()和get()方法放入和取出数据时,Buffer既支持单个数据的访问,也支持批量数据的访问。 对Buffer类有了一定的了解后,下面通过一个具体的案例来演示Buffer的使用,如文件1所示。 文件1 Example18.java ```java 1 import java.nio.CharBuffer; 2 public class Example18 { 3 public static void main(String[] args){ 4 // 创建CharBuffer对象,并指定缓冲区容量大小为6 5 CharBuffer charBuffer = CharBuffer.allocate(6); 6 System.out.println("容量:" + charBuffer.capacity()); 7 System.out.println("界限值:" + charBuffer.limit()); 8 System.out.println("初始位置:" + charBuffer.position()); 9 // 向CharBuffer对象中放入3个元素 10 charBuffer.put('x'); 11 charBuffer.put('y'); 12 charBuffer.put('z'); 13 System.out.println("加入元素后的界限值:" + charBuffer.limit()); 14 System.out.println("加入元素后的位置:" + charBuffer.position()); 15 // 执行flip()方法 16 charBuffer.flip(); 17 System.out.println("执行flip()后的界限值:" + charBuffer.limit()); 18 System.out.println("执行flip()后的位置:" + charBuffer.position()); 19 // 取出第1个元素 20 System.out.println("取出的第1个元素为:" + charBuffer.get()); 21 System.out.println("取出后的界限值:" + charBuffer.limit()); 22 System.out.println("取出后的位置:" + charBuffer.position()); 23 // 执行clear()方法 24 charBuffer.clear(); 25 System.out.println("执行clear()后的界限值:" + charBuffer.limit()); 26 System.out.println("执行clear()后的位置:" + charBuffer.position()); 27 // 取出第1个元素 28 System.out.println("取出的第1个元素为:" + charBuffer.get(0)); 29 System.out.println("取出后的界限值:" + charBuffer.limit()); 30 System.out.println("取出后的位置:" + charBuffer.position()); 31 } 32 } ``` 运行结果如图2所示。 ![img](https://ucc.alicdn.com/images/user-upload-01/img_convert/a7dd8e984828c695f8bd0c4d8abf5e05.png) 图2 运行结果 文件1中,首先创建了一个容量为6的CharBuffer对象,并输出了该对象的容量、界限以及初识位置值;然后向CharBuffer对象中放入了3个字符元素,并输出了加入元素后的位置,从输出结果可以看出,元素位置已经发生了变化。接下来执行了flip()方法并再次输出界限值和位置,此时界限值已由原来的6变为3,位置由3变为了0,这是因为调用了flip()方法之后,limit会移动到到原来的position的位置,而position会被设置为0。接着通过get()方法取出了对象中的第1个元素,并输出此时的界限值和位置,从输出结果可以看出,界限值没有变,而位置变为1,这是因为取出一个元素后,position会向后移动一位。程序继续向下执行完clear()方法后,limit会被设置为与容量相等,而position会被设置为0。最后通过索引的方式取出第1个元素的值,并输出界限值和位置。从运行结果可以看出,执行完clear()方法之后,Buffer对象中的数据依然存在,并且通过索引取出元素值后,position的值并没有受到影响。 ### 6.3、Channel(通道) Channel是一个接口对象,它类似于传统的流对象,但与传统的流对象又有些不同,具体表现如下: ● Channel可以异步的执行I/O读写操作。 ● Channel的读写操作是双向的,既可以从Channel中读取数据,又可以写数据到Channel,而流的读写操作通常都是单向的。 ● Channel可以直接将指定文件的部分或者全部直接映射成Buffer。 ● Channel只能与Buffer进行交互,程序不能直接读写Channel中的数据。 要使用Channel,就需要使用它的实现类。在java.nio.channels包中,提供了很多Channel接口的实现类,包括DatagramChannel、FileChannel、Pipe.SinkChannel、Pipe.SourceChannel、ServerSocketChannel、SocketChannel等。其中DatagramChannel用于支持UDP网络通信,FileChannel用于从文件中读写数据,Pipe.SinkChannel和Pipe.SourceChannel用于支持线程之间的通信,ServerSocketChannel和SocketChannel用于支持TCP网络通信。这里将主要讲解FileChannel的使用。 Channel对象并不是通过构造方法来创建的,而是通过传统I/O的getChannel()方法来获取对应的Channel。不同的流所获取的Channel是不同的,例如FileInputStream和FileOutputStream获取的是FileChannel,同时还可以使用RandomAccessFile获取该对象,而PipedInputStream和PipedOutputStream所获得的是Pipe.SinkChannel和Pipe.SourceChannel。 FileChannel类可以实现常用的读写操作,在类中提供了很多专门用于操作文件的方法,其常用方法如表1所示。 表1 FileChannel类的常用方法 | **方法声明** | **功能描述** | | ------------------------------------------------------------ | ------------------------------------------------------------ | | MappedByteBuffer map(MapMode mode, long position, long size) | 将该通道文件的区域直接映射到内存中。其中第1个参数用于执行映射时的模式,包含只读、读写等模式;第2个参数表示映射区域开始的文件中的位置;第3个参数表示要映射区域的大小 | | long position() | 返回该通道的文件位置 | | Int read(ByteBuffer dst) | 从这个通道读取一个字节序列到给定的缓冲区 | | Int read(ByteBuffer dst, long position) | 从给定的文件位置开始,从这个通道读取一个字节序列到给定的缓冲区 | | long read(ByteBuffer[] dsts, int offset, int length) | 从这个通道读取一个字节序列到给定缓冲区的子序列 | | long size() | 返回该通道文件的当前大小 | | long transferTo(long position, long count, WritableByteChannel target) | 读取该通道文件中给定位置的字节数,并将它们写入目标通道 | | Int write(ByteBuffer src) | 从给定的缓冲区写入这个通道的字节序列 | | long write(ByteBuffer[] srcs, int offset, int length) | 从给定缓冲区的子序列中写入该通道的字节序列 | | Int write(ByteBuffer src, long position) | 从给定的缓冲区开始,从给定的文件位置开始向该通道写入一个字节序列 | 了解了FileChannel类的常用方法及其功能后,下面通过一个文件拷贝的案例,来演示FileChannel的使用,如文件1所示。 文件1 Example19.java ```java 1 import java.io.*; 2 import java.nio.channels.*; 3 public class Example19 { 4 public static void main(String[] args) throws Exception { 5 // 创建RandomAccessFile对象,指定源文件 6 RandomAccessFile infile = 7 new RandomAccessFile("source/src.jpg","rw"); 8 // 获取读取源文件FileChannel通道 9 FileChannel inChannel = infile.getChannel(); 10 // 创建RandomAccessFile对象,指定目标文件 11 RandomAccessFile outfile = 12 new RandomAccessFile("target/dest.jpg","rw"); 13 // 获取复制目标文件FileChannel通道 14 FileChannel outChannel = outfile.getChannel(); 15 // 使用transferTo()方法进行整体复制 16 long transferTo = inChannel.transferTo(0, inChannel.size(), 17 outChannel); 18 if(transferTo>0){ 19 System.out.println("复制成功!"); 20 } 21 // 关闭资源 22 infile.close(); 23 inChannel.close(); 24 outfile.close(); 25 outChannel.close(); 26 } 27 } ``` 运行结果如图1所示。 ![img](https://ucc.alicdn.com/images/user-upload-01/img_convert/1ac70b865ca2624788c3a520c5daf954.png) 图1 运行结果 文件1中,使用RandomAccessFile类的构造方法生成两个RandomAccessFile对象,同时还指定了复制文件的源文件和目标文件名称以及可执行的操作,然后通过getChannel()方法获取对应的FileChannel类分别用于文件读取和写入通道。接下来通过FileChannel类的transferTo(long position, long count, WritableByteChannel target)方法实现了整个文件的拷贝,该方法的第1个参数表示所需转移文件的起始位置,这里表示从0开始;第2个参数表示要传输的最大字节数,这里通过size()方法获取了文件的字节数;第3个参数表示目标通道,即要传输到的位置。最后文件拷贝完毕后,关闭了所有的资源。 ## 7、NIO.2 ### 7.1、Path接口 通过前面的学习可知,File虽然可以访问文件系统,但是File类所提供的方法性能较低,大多数方法在出错时仅返回失败而不提供异常信息,不仅如此,File类还不能利用特定的文件系统的特性。为了弥补这种不足,NIO.2提供了一个Path接口,该接口是一共用在文件系统中定位文件的对象,通常表示一个依赖于系统的文件路径。除此之外,NIO.2还提供了Paths和Files两个工具类,其中Paths类中提供了两个返回Path的静态方法,通过这两个方法可以创建Path对象,而Files类中提供了大量的静态方法来操作文件。 接下来列举一下Path中的常用方法,如表1所示。 表1 Path接口的常用方法 | **方法声明** | **功能描述** | | ------------------------------ | ------------------------------------------------------------ | | boolean endsWith(String other) | 判断当前路径是否以指定的字符串结尾 | | Path getName(int index) | 返回此路径的名称元素作为路径对象 | | int getNameCount() | 返回路径中名称元素的数量 | | Path getParent() | 返回父路径,如果此路径没有父路径,则返回null | | Path getRoot() | 返回该路径的根组件作为路径对象,如果此路径没有根组件,则返回null | | Path toAbsolutePath() | 返回表示此路径的绝对路径的路径对象 | | URI toUri() | 返回表示此路径的URI地址 | 了解了Path接口的常用方法及其功能后,接下来通过一个案例来演示Path接口的基本使用,如文件1所示。 文件1 Example20.java ```java 1 import java.nio.file.Path; 2 import java.nio.file.Paths; 3 public class Example20 { 4 public static void main(String[] args) { 5 // 使用Paths的get()方法创建Path对象 6 Path path = Paths.get("D:\\test\\文件夹\\test.txt"); 7 // 输出Path对象中的信息 8 System.out.println("path的根路径:" + path.getRoot()); 9 System.out.println("path的父路径:" + path.getParent()); 10 System.out.println("path中的路径名称数:" + path.getNameCount()); 11 // 循环输出路径名称 12 for (int i = 0; i < path.getNameCount(); i++) { 13 // 获取指定索引处的路径名称 14 Path name = path.getName(i); 15 System.out.println("索引为" + i + " 的路径的名称为: " + name); 16 } 17 System.out.println("path的URI路径为:" + path.toUri()); 18 System.out.println("path的绝对路径:" + path.toAbsolutePath()); 19 } 20 } ``` 运行结果如图1所示。 ![img](https://ucc.alicdn.com/images/user-upload-01/img_convert/3e1e5a37b0c90c8ee4e436d5b4ed95bf.png) 图1 运行结果 文件1中,首先使用Paths的get()方法创建了Path对象,然后分别使用Path对象中的各种方法来输出对象中的路径信息。 ### 7.2、Files工具类 在NIO.2中,针对于文件操作除了前面小节讲解的Paths工具类和Path接口外,还有一个Files工具类,该类是一个操作文件的工具类,其中包含了大量的方法,如表1所示。 表1 Files工具类中的常用方法 | **方法声明** | **功能描述** | | ------------------------------------------------------------ | ------------------------------------------------------------ | | static Path createDirectories(Path dir, FileAttribute<?>... attrs) | 创建多级文件目录 | | static Path createFile(Path path, FileAttribute<?>... attrs) | 创建一个新的空文件,如果文件已经存在,则创建失败 | | static Path copy(Path source, Path target,CopyOption... options) | 该方法将一个文件复制到目标文件,并使用选项参数指定如何执行复制 | | static List readAllLines(Path path) | 从文件中读取所有行 | | static long size(Path path) | 返回文件的大小(以字节为单位) | | static Stream list(Path dir) | 将指定路径转换为Stream流对象 | | static Path write(Path path, Iterable<? extends CharSequence> lines, OpenOption... options) | 将文本行写入文件,并传入指定的写入模式 | 了解了Files工具类的常用方法及其功能后,接下来通过一个具体的案例来演示Files类中的一些常用方法的使用,如文件1所示。 文件1 Example21.java ```java 1 import java.io.*; 2 import java.nio.file.*; 3 import java.util.*; 4 public class Example21 { 5 public static void main(String[] args) throws IOException { 6 // 定义一个目录路径的Path对象 7 Path directoryPath = Paths.get("D:/test/sample"); 8 // 根据Path对象创建多级目录 9 Files.createDirectories(directoryPath); 10 System.out.println("目录创建成功!"); 11 // 定义一个文件路径的Path对象 12 Path filePath = Paths.get("D:/test/sample/test.txt"); 13 // 根据Path对象创建一个文件 14 Files.createFile(filePath); 15 // 创建一个List集合,并向集合中添加内容 16 List list = new ArrayList(); 17 list.add("这是一个测试文件"); 18 // 将集合中的内容追加写入到指定的文件中 19 Files.write(filePath, list, StandardOpenOption.APPEND); 20 List lines = Files.readAllLines(filePath); 21 System.out.println("文件的大小为:" + Files.size(filePath)); 22 System.out.println("文件中的内容为:" + lines); 23 } 24 } ``` 运行结果如图1所示。 ![img](https://ucc.alicdn.com/images/user-upload-01/img_convert/ef199e5c51eb8ee446a2c31dcc57e7ee.png) 图1 运行结果 文件1中,简单的演示了Files工具类的一些用法,包括文件的创建、写入、读取等功能。除上述代码中的方法外,Files类中还有很多实用的方法,由于篇幅有限,这里就不再赘述。关于Files类中更多方法的使用,读者可查找官方文档来学习。
相关文章
|
20天前
|
XML Java 编译器
Java学习十六—掌握注解:让编程更简单
Java 注解(Annotation)是一种特殊的语法结构,可以在代码中嵌入元数据。它们不直接影响代码的运行,但可以通过工具和框架提供额外的信息,帮助在编译、部署或运行时进行处理。
85 43
Java学习十六—掌握注解:让编程更简单
|
5天前
|
Java 大数据 API
14天Java基础学习——第1天:Java入门和环境搭建
本文介绍了Java的基础知识,包括Java的简介、历史和应用领域。详细讲解了如何安装JDK并配置环境变量,以及如何使用IntelliJ IDEA创建和运行Java项目。通过示例代码“HelloWorld.java”,展示了从编写到运行的全过程。适合初学者快速入门Java编程。
|
28天前
|
存储 SQL 小程序
JVM知识体系学习五:Java Runtime Data Area and JVM Instruction (java运行时数据区域和java指令(大约200多条,这里就将一些简单的指令和学习))
这篇文章详细介绍了Java虚拟机(JVM)的运行时数据区域和JVM指令集,包括程序计数器、虚拟机栈、本地方法栈、直接内存、方法区和堆,以及栈帧的组成部分和执行流程。
26 2
JVM知识体系学习五:Java Runtime Data Area and JVM Instruction (java运行时数据区域和java指令(大约200多条,这里就将一些简单的指令和学习))
|
30天前
|
存储 缓存 Java
java基础:IO流 理论与代码示例(详解、idea设置统一utf-8编码问题)
这篇文章详细介绍了Java中的IO流,包括字符与字节的概念、编码格式、File类的使用、IO流的分类和原理,以及通过代码示例展示了各种流的应用,如节点流、处理流、缓存流、转换流、对象流和随机访问文件流。同时,还探讨了IDEA中设置项目编码格式的方法,以及如何处理序列化和反序列化问题。
65 1
java基础:IO流 理论与代码示例(详解、idea设置统一utf-8编码问题)
|
13天前
|
JavaScript Java 项目管理
Java毕设学习 基于SpringBoot + Vue 的医院管理系统 持续给大家寻找Java毕设学习项目(附源码)
基于SpringBoot + Vue的医院管理系统,涵盖医院、患者、挂号、药物、检查、病床、排班管理和数据分析等功能。开发工具为IDEA和HBuilder X,环境需配置jdk8、Node.js14、MySQL8。文末提供源码下载链接。
|
28天前
|
小程序 Oracle Java
JVM知识体系学习一:JVM了解基础、java编译后class文件的类结构详解,class分析工具 javap 和 jclasslib 的使用
这篇文章是关于JVM基础知识的介绍,包括JVM的跨平台和跨语言特性、Class文件格式的详细解析,以及如何使用javap和jclasslib工具来分析Class文件。
35 0
JVM知识体系学习一:JVM了解基础、java编译后class文件的类结构详解,class分析工具 javap 和 jclasslib 的使用
|
30天前
|
前端开发 Java 应用服务中间件
Javaweb学习
【10月更文挑战第1天】Javaweb学习
30 2
|
1月前
|
存储 安全 Java
【用Java学习数据结构系列】探索顺序表和链表的无尽秘密(附带练习唔)pro
【用Java学习数据结构系列】探索顺序表和链表的无尽秘密(附带练习唔)pro
22 3
|
1月前
|
存储 安全 Java
【用Java学习数据结构系列】探索栈和队列的无尽秘密
【用Java学习数据结构系列】探索栈和队列的无尽秘密
29 2
|
1月前
|
存储 Java 编译器
【用Java学习数据结构系列】初识泛型
【用Java学习数据结构系列】初识泛型
18 2
下一篇
无影云桌面