JAVA编程讲义之输入/输出流
应用程序经常需要访问文件和目录,读取文件信息或写入信息到文件,即从外界输入数据或者向外界传输数据,这些数据可以保存在磁盘文件、内存或其他程序中。在Java中,对这些数据的操作是通过I/O技术来实现的。所谓I/O技术,就是数据的输入(Input)、输出(Output)技术。本章将对Java的 I/O系统进行讲解,包括I/O的体系结构、流的概念、字节流、处理字节流的基本类InputStream和OutputStream、字符流、处理字符流的基本类Reader和Writer、文件管理、序列化和反序列化等。
12.1 I/O流概述
Java将数据的输入/输出操作当作“流”来处理,“流”是一组从源头到目的地的有序的字节序列。在Java程序中,从某个数据源读取数据到程序的流称为输入流,通过程序使用数据流将数据写入到目的地的称为输出流。输入流和输出的读取和写入流程如图12.1所示。
(a)输入流 (b)输出流
图12.1 输入/输出流示意图
当程序需要从某个数据源读入数据的时候,就会开启一个输入流,数据源可以是文件、内存或网络等。相反,需要写出数据到某个数据源目的地的时候,也会开启一个输出流,这个数据源目的地也能够是文件、内存或网络等。I/O流有很多种,按操作数据单位不同可分为字节流和字符流,按数据流的方向不同分为输入流和输出流,如表12.1所示。
表12.1 流的分类
输入/输出 |
字节流 |
字符流 |
输入流 |
InputStream |
Reader |
输出流 |
OutputStream |
Writer |
输入流和输出流的区别是以程序为中心来进行判断,从外部设备读取数据到程序是输入流,从程序写入数据到外部设备是输出流。字节流的单位是一个字节,即8bit;字符流的单位是两个字节,即16bit。表12.1是I/O流的简单分类,实际开发中需要使用的的I/O流共涉及40多个类,都是从这4个抽象基类派生的。接下来,我们先学习输入/输出流的体系结构。
Java.io包中的最重要的部分是由5个类和一个接口组成。5个类是指File、RandomAccessFile、InputStream、OutputStream、Writer、Reader,一个接口指的是Serializable。掌握了这些I/O的核心操作,那么对于Java中的I/O体系也就有了一个初步的认识了。总体上看,Java I/O主要包括如下3个部分:
• 流式部分:I/O的主体部分。
• 非流式部分:主要包含一些辅助流式部分的类,如File类、RandomAccessFile类和FileDescriptor类等。
• 其他类:主要是文件读取部分的与安全相关的类(如SerializablePermission类),以及与本地操作系统相关的文件系统的类,如(FileSystem类、Win32FileSystem类和WinNTFileSystem类)。
这里,将Java I/O中主要的类简单介绍如下:
• File类(文件特征与管理类):用于文件或者目录的描述信息等(An abstract representation of file and directory pathnames),如生成新目录、修改文件名、删除文件、判断文件所在路径等。
• InputStream类(二进制格式操作类):基于字节输入操作的抽象类,是所有输入流的父类,定义了所有输入流都具有的共同特征。
• OutputStream类(二进制格式操作类):基于字节输出操作的抽象类,是所有输出流的父类,定义了所有输出流都具有的共同特征。
• Reader类(文件格式操作类):抽象类,基于字符的输入操作。
• Writer类(文件格式操作类):抽象类,基于字符的输出操作。
• RandomAccessFile类(随机文件操作类):它的功能丰富,可以从文件的任意位置进行存取(输入输出)操作。
综上所述,Java中I/O流的体系结构如图12.2所示。
图12.2 I/O流体系结构图
12.2 File类
File类可以用于处理文件目录。在对一个文件进行输入/输出,必须先获取有关该文件的基本信息,如文件是否可以读取、能否被写入、路径是什么等。java.io.File类不属于Java流系统,但它是文件流进行文件操作的辅助类,提供了获取文件基本信息以及操作文件的一些方法,通过调用File类提供的相应方法,能够完成创建文件、删除文件以及对目录的一些操作。
12.2.1 File类的常用方法
File类的对象是一个“文件或目录”的抽象,它并不打开文件或目录,而是指定要操作的文件或目录。File类的对象一旦创建,就不能再修改。要创建一个新的File对象,需要使用它的构造方法,如表12.2所示。
表12.2 File类构造方法
构造方法 |
功能描述 |
public File(String filename) |
创建File对象,filename表示文件或目录的路径 |
public File(String parent,String child) |
创建File对象,parent表示上级目录,child表示指定的子目录或文件名 |
public File(File obj,String child) |
设置File对象,obj表示File对象,child表示指定的子目录或文件名 |
使用表12.2所列的哪种构造方法要由其他被访问的文件来决定。例如,当在应用程序中只用到一个文件时,使用第1种构造方法最合适;如果使用了一个公共目录下的几个文件,那么使用第2种或第3种构造方法会更方便。
创建File类的对象后,就可以使用File的相关方法来获取文件信息。接下来,先了解一下File类的常用方法,如表12.3所示。
表12.3 File类常用方法
常用方法 |
功能描述 |
备注 |
String getName() |
获取相关文件名 |
与文件名相关的方法 |
String getPath() |
获取文件路径 |
|
String getAbsolutePath() |
获取文件绝对路径 |
|
String getParent() |
获取文件上级目录名称 |
|
boolean renameTo(File newName) |
更改文件名,成功则返回true,否则返回false |
|
boolean exists() |
检测文件对象是否存在 |
文件测定相关方法 |
boolean canWrite() |
检测文件对象是否可写 |
|
boolean canRead() |
检测文件对象是否可读 |
|
boolean isFile() |
检测文件对象是否是文件 |
|
boolean isDirectory() |
检测文件对象是否是目录 |
|
boolean isAbsolute() |
检测文件对象是否是绝对路径 |
|
long lastModified() |
返回此File对象表示的文件或目录最后一次被修改的时间 |
常用文件信息和方法 |
long length() |
返回此File对象表示的文件或目录的长度 |
|
boolean delete() |
删除文件或目录。如果File对象为目录,则该目录为空,方可删除。删除成功,返回true,否则返回false |
|
boolean mkdir() |
创建File对象指定目录。如果创建成功,则返回true,否则返回false |
目录相关类工具 |
boolean mkdirs() |
创建File对象指定的目录,如果此目录的父级不存在,则还会创建父目录。如创建成功,则返回true,否则返回false |
|
String []list() |
返回此File对象表示的目录中的文件和目录的名称所组成字符串数组 |
接下来,通过一个案例来演示File类常用方法的基本使用,先在当前目录创建一个1201.txt文件,在里面输入“AAA软件教育欢迎您!”,然后编写代码,如例12-1所示。
例12-1 Demo1201.java
1 package com.aaa.p120201;
2 import java.io.*;
3 import java.util.*;
4 import java.text.SimpleDateFormat;
5
6 public class Demo1201 {
7 public static void main(String[] args) {
8 File file = new File("src/1201.txt");
9 System.out.println("文件是否存在-->" + file.exists());
10 System.out.println("文件是否可写-->" + file.canWrite());
11 System.out.println("文件是否可读-->" + file.canRead());
12 System.out.println("文件是否是文件-->" + file.isFile());
13 System.out.println("文件是否是目录-->" + file.isDirectory());
14 System.out.println("文件是否是绝对路径-->" + file.isAbsolute());
15 System.out.println("文件名是-->" + file.getName());
16 System.out.println("文件的路径是-->" + file.getPath());
17 System.out.println("文件的绝对路径是-->" + file.getAbsolutePath());
18 System.out.println("文件的上级路径是-->" + file.getParent());
19 SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
20 System.out.print("最后修改时间-->");
21 System.out.println(sdf.format(new Date(file.lastModified())));
22 System.out.println("文件长度是-->" + file.length());
23 }
24 }
程序的运行结果如下:
文件是否存在-->true
文件是否可写-->true
文件是否可读-->true
文件是否是文件-->true
文件是否是目录-->false
文件是否是绝对路径-->false
文件名是-->1201.txt
文件的路径是-->src\1201.txt
文件的绝对路径是-->D:\work\AAA课程研发\教材编写\javaIO\src\1201.txt
文件的上级路径是-->src
最后修改时间-->2021-06-15
文件长度是-->25
例12-1在程序中构造了File类的对象,运用File类的各个方法得到文件的各种相关属性。在第19~21行代码中,通过格式化时间信息,获取文件最后修改时间,最后打印文件1201.txt相关属性的信息。
12.2.2 遍历目录下的文件
File类用来操作文件和获得文件的信息,但是不提供对文件读取的方法,这些方法由文件流提供。File类中提供了list()方法和listFiles()方法,用来遍历目录下所有文件。两者不同之处是list()方法只返回文件名,没有路径信息;而listFiles()方法不但返回文件名称,还包含有路径信息。
接下来,通过案例来演示list()方法与listFiles()方法的使用,如例12-2所示。
例12-2 Demo1202.java
1 package com.aaa.p120202;
2 import java.io.*;
3
1 public class Demo1202 {
2 public static void main(String[] args) {
3 System.out.printf("***********list()方法***********");
4 File file = new File("D:\\javaCode"); // 创建File对象
5 if (file.isDirectory()) { // 判断file目录是否存在
6 String[] list = file.list();
7 for (String fileName : list) {
8 System.out.println(fileName); // 打印文件名
9 }
10 }
11 System.out.printf("***********listFiles()方法***********");
12 files(file);
13 }
14 public static void files(File file) {
15 File[] listFile = file.listFiles(); // 遍历目录下所有文件
16 for (File f : listFile) {
17 if (f.isDirectory()) { // 判断是否是目录
18 files(f); // 递归调用
19 }
20 System.out.println(f.getAbsolutePath());
21 }
22 }
23 }
程序的运行结果如下:
***********list()方法***********
chapter02
test.txt
***********listFiles()方法***********
D:\javaCode\chapter02\.idea\.gitignore
D:\javaCode\chapter02\.idea\misc.xml
D:\javaCode\chapter02\.idea\modules.xml
D:\javaCode\chapter02\.idea\uiDesigner.xml
D:\javaCode\chapter02\.idea\workspace.xml
D:\javaCode\chapter02\.idea
D:\javaCode\chapter02\chapter02.iml
D:\javaCode\chapter02\out\production\chapter02\Demo02.class
D:\javaCode\chapter02\out\production\chapter02\Demo0201.class
例12-2中,首先创建File对象,指定File对象的目录。第5~10行代码先判断file目录是否存在,若存在,则调用list()方法,第6行代码以String数组的形式得到所有文件名,最后循环遍历数组内容并打印。如果目录下仍然有子目录则不能遍历到,此时就需要用到File类的listFiles()方法,遍历目录下所有文件之后,循环判断遍历到的是否是目录,如果是目录,则再次递归调用file(file)方法本身。第14~22行代码是自定义的静态方法,直到遍历完到文件。通过程序运行结果可以看到,listFiles()方法输出的信息比list()方法输出的信息更加详细,而且listFiles()方法返回值是File类型,可以直接使用该文件。
注意:在Windows系统中,目录的分隔符是反斜杠(\)。但是,在Java语言中,使用反斜杠表示转义字符,所以如果需要在Windows系统的路径下包括反斜杠,则应该使用两条反斜线,如D:\\javaCode或者直接用斜线(/)也可以。
12.2.3 删除文件及目录
在程序设计中,除了遍历文件,文件的删除操作也很常见,Java中通过使用File类的delete()方法来对文件进行删除操作。
接下来,演示如何删除文件及目录,如例12-3所示。
例12-3 Demo1203.java
1 package com.aaa.p120203;
2 import java.io.*;
3
4 public class Demo1203 {
5 public static void main(String[] args) {
6 String path = "D:/javaCode/chapter02/out";
7 deleteD(path);
8 }
9 private static void deleteD(String pp) {
10 File file = new File(pp);
11 if (file.isFile()) {
12 while (file.exists()) {
13 System.out.println("删除了文件:" + file.getName());
14 file.delete();
15 }
16 } else {
17 File[] listFiles = file.listFiles();
18 for (File file2 : listFiles) {
19 try {
20 deleteD(file2.getAbsolutePath());
21 } catch (Exception e) {
22 System.out.println(e.getMessage());
23 }
24 }
25 file.delete();
26 }
27 }
28 }
程序的运行结果如下:
删除了文件:Demo02.class
删除了文件:Demo0201.class
删除了文件:Demo0202.class
删除了文件:Demo03.class
删除了文件:Demo05.class
删除了文件:Demo06.class
例12-3中,在main()方法中deleteD(File file)方法中将待删除的内容以字符串形式传入,调用方法时创建File对象,然后遍历该目录下所有文件,判断遍历到的是否是目录,如果是目录,继续递归调用方法本身,如果是文件则输出文件信息然后直接删除,删除文件完成后,将目录删除。第18~23行代码增加了确保程序健壮性的异常信息处理。
注意:File类的delete()方法只是删除一个指定的文件,如果目录下还有子目录,是无法直接删除的,需要递归删除。另外,在Java中是直接从虚拟机中将文件或目录删除,可以不经过回收站,文件无法恢复,所以使用delete()操作时要谨慎。
12.2.4 RandomAccessFile类
Java提供的RandomAccessFile类,允许从文件的任何位置进行数据的读写。它不属于流,是Object类的子类,但它融合了InputStream类和OutStream类的功能,既能提供read()方法和write()方法,还能提供更高级的直接读写各种基本数据类型数据的读写方法,如readInt()方法和writeInt()方法等。
RandomAccessFile类的中文含义为随机访问文件类,随机意味着不确定性,指的是不需要从头读到尾,可以从文件的任意位置开始访问文件。使用RandomAccessFile类,程序可以直接跳到文件的任意地方读、写文件,既支持只访问文件的部分数据,又支持向已存在的文件追加数据。
为支持任意读写,RandomAccessFile类将文件内容存储在一个大型的byte数组中。RandomAccessFile类设置指向该隐含的byte数组的索引,称为文件指针,通过从文件开头就开始计算的偏移量来标明当前读写的位置。
RandomAccessFile类有两个构造方法,其实这两个构造方法基本相同,只是指定文件的形式不同而已,一个使用String参数来指定文件名,一个使用File参数来指定文件本身。具体示例如下:
// 访问file参数指定的文件,访问的形式由mode参数指定
public RandomAccessFile(File file, String mode)
// 访问name参数指定的文件,访问的形式由mode参数指定
public RandomAccessFile(String name, String mode)
在创建RandomAccessFile对象时还要设置该对象的访问形式,具体使用一个参数mode进行指定,mode的值及对应的访问形式如表12.4所示。
表12.4 mode的值及含义
mode值 |
含义 |
“r” |
以只读的方式打开,如果试图对该RandomAccessFile执行写入方法,都将抛出IOException异常 |
“rw” |
以读、写方式打开指定文件,如果该文件不存在,则尝试创建该文件 |
“rws” |
以读、写方式打开指定文件,相较于“rw”模式,还需要对文件的内容或元数据的每个更新都同步写入到底层存储设备 |
“rwd” |
以读、写方式打开指定文件,相较于“rw”模式,还要求对文件内容的每个更新都同步写入到底层存储设备 |
随机访问文件是由字节序列组成,一个称为文件指针的特殊标记定位这些字节中的某个字节的位置,文件的读写操作就是在文件指针所在的位置上进行的。打开文件时,文件指针置于文件的起始位置,在文件中进行读写数据后,文件指针就会移动到下一个数据项。如表12.5所示,列出了RandomAccessFile类所拥有的用来操作文件指针的方法。
表12.5 RandomAccessFile类操作指针的方法
方法声明 |
功能描述 |
long getFilePointer() |
获取当前读写指针所处的位置 |
void seek(long pos) |
指定从文件起始位置开始的指针偏移量,即设置读指针的位置 |
int skipBytes(int n) |
使读写指针从当前位置开始,跳过n个字节 |
void setLength(long num) |
设置文件长度 |
接下来,通过案例演示 RandomAccessFile类操作文件指针的方法的使用,如例12-4所示。
例12-4 Demo1204.java
1 package com.aaa.p1202;
2 import java.io.*;
3 import java.io.IOException;
4 import java.io.RandomAccessFile;
5
6 public class Demo1204 {
7 public static void main(String[] args) {
8 File file = new File("d:/javaCode/test.txt");
9 RandomAccessFile raf = null; // 声明RandomAccessFile对象
10
11 try{
12 raf = new RandomAccessFile(file,"rw");
13 for(int n = 0;n < 10;n++){
14 raf.writeInt(n);
15 }
16 System.out.println("当前指针位置:" + raf.getFilePointer());
17 System.out.println("文件长度:" + raf.length() + "字节");
18 raf.seek(0); // 返回数据的起始位置
19
20 System.out.println("当前指针位置:" + raf.getFilePointer());
21 System.out.println("读取数据");
22 for(int n = 0;n < 6;n++){
23 System.out.println("数值:" + raf.readInt() + "-->" +
24 (raf.getFilePointer() - 4));
25 if(n == 3)raf.seek(32); // 指针跳过 4 5 6 7
26 }
27 raf.close(); // 关闭随机访问文件流
28 }catch (IOException e){
29 e.printStackTrace();
30 }
31 }
32 }
程序的运行结果如下:
第0个值:0
当前指针位置:40
文件长度:40字节
当前指针位置:0
读取数据
数值:0-->0
数值:1-->4
数值:2-->8
数值:3-->12
数值:8-->32
数值:9-->36
例12-4中,先向test.txt文件写入0~9十个数字,此时文件的长度为10个int字节,数据指针位置为40。如果要读取文件的数据信息,则需要把文件指针移动到文件的起始位置,而执行seek(0)可以达到目的。虽然开始时会循环读取数据,但在i为3时,将指针移动到32,即跳过5、6、7、8,直接开始读取8和9,对应指针位置时32和36。
12.3 字节流
在前面小节中,我们学习了File类对文件或目录进行操作的方法,但是File类不包含向文件读写数据的方法。为了进一步进行文件输入/输出操作,需要使用正确的Java I/O类来创建对象。在程序设计中,程序如果要读取或写入8位bit的字节数据,应该使用字节流来处理。字节流一般用于读取或写入二进制数据,如图片、音频文件等。一般而言,只要是“非文本数据”就应该使用字节流来处理。
12.3.1 字节流概述
在计算机中,无论是文本、图片、音频还是视频,所有的文件都能以二进制(bit,1字节为8bit)形式传输或保存。Java中针对字节输入/输出操作提供了一系列流,统称为字节流。程序需要数据的时候要使用输入流来读取数据,而当程序需要将一些数据保存起来的时候就需要使用输出流来完成。在Java中,字节流提供了两个抽象基类InputStream和OutputStream,分别用于处理字节流的输入和输出。因为抽象类不能被实例化,所以在实际使用中,使用的是这两个类的子类。这里还需要强调的是,输入流和输出流的概念是有一个参照物的,参照物就是站在程序的角度来理解这两个概念,如图12.3所示。
图12.3中,从文件到程序是输入流(InputStream),通过程序,读取文件中的数据;从程序到文件是输出流(OutputStream),将数据从程序输出到文件。
InputStream类和OutputStream类都是抽象类,不能被实例化,所以如果要实现不同数据源的操作功能,须要用到它们的子类,这些子类可以在JDK的API文档里的类层次结构中查看,如图12.4和图12.5所示。
图12.4 InputStream子类结构图 图12.5 OutputStream子类结构图
从图12.4和图12.5中可看出,InputStream和OutputStream的子类虽然较多,但都有规律可循。因为输入流或输出流的数据源或目标的数据格式不同,如字节数组、文件、管道等,所以子类在命名的时候采用的格式是数据类型加抽象基类名。例如,FileInputStream的子类表示从文件中读取信息,InputStream为后缀。此外,InputStream和OutputStream的子类大多都是成对出现的,如数据过滤流FilterInputStream和FilterOutputStream。
InputStream类定义了输入流的一般方法,是字节输入流的父类,其他字节输入流都是在其基础上做功能上的增强。因此,了解了InputStream类就为了解其他输入流打下了基础,表12.6列出了InputStream类的常用方法。
表12.6 InputStream类的常用方法
方法声明 |
功能描述 |
public int available() |
获取输入流中可以不受阻塞地读取的字节数 |
public void close() |
关闭输入流并释放与该流关联的所有系统资源,该方法由子类重写 |
public void mark(int readlimit) |
在此输入流中标记当前的位置,该方法由子类重写 |
public boolean markSupported() |
判断当前输入流是否允许标记。若允许,则返回true,否则返回false |
public long skip(long n) |
从输入流中跳过指定n个指定的字节,并返回跳过的字节数 |
public int read() |
从输入流中读取数据的下一个字节 |
public int read(byte[] b) |
从输入流中读取一定数量的字节,并将其存储在缓冲区数组 b 中,返回读取的字节数。如果已经到达末尾,则返回-1 |
public int read(byte[] b, int off, int len) |
将输入流中最多 len 个数据字节读入 byte 数组。然后将读取的b数据以int返回。如果已经到达末尾,则返回-1 |
public void reset() |
将输入流重新定位到最后一次对此输入流设置标记的起始处 |
表12.6中列出了InputStream类的方法,上述所有方法都声明抛出IOException异常,因此使用时要注意处理异常。InputStream使用最多的方法为read()和close()方法,前者从已存在的文件中读取字节,在工作做完之后,由后者关闭字节流,释放系统资源,如果不关闭会浪费一定量的系统资源,会导致计算机运行效率下降。read()方法有构成函数重载的3种形式,无参的read()方法可以用来将字节挨个读入,另外两个可以指定一个字节数组作为读取字节的批量,甚至可以通过定义off和len,指定读取字节的起始位置和长度。
下面我们来看一看OutputStream类,它拥有和InputStream类似的用法和相对的功能方法。
表12.7 OutputStream类的常用方法
方法声明 |
功能描述 |
void close() |
关闭此输出流,并释放与之有关的所有系统资源,由子类重写该方法 |
void flush() |
刷新此输出流,并强制写出所有缓冲的输出字节 |
void write(byte[] b) |
将 数组b的数据写到输出流 |
void write(int b) |
将指定的int字节b写入此输出流 |
void write(byte[] b, int off, int len) |
将指定 byte 数组b中写入到输出流,从偏移量 off 开始的 len 个字节写入此输出流 |
表12.7所列的OutputStream类的常用方法可分为两类:3个重载write()方法能够向文件中写入数据,可以选择挨个或以数组的方式;flush()方法和close()方法能够操作输出流本身,close()方法关闭此流并释放系统资源,flush()方法会强制将缓冲区中的字节写入文件中,即使缓冲区还没有装满,该方法可在流关闭前调用,用于清空缓冲区。
12.3.2 读写文件
FileInputStream类和FileOutputStream类用于从文件/向文件读取/写入字节数据,FileInputStream是InputStream的子类,用来从文件中读取数据,操作文件的字节输入流;FileOutputStream是OutputStream的子类,可以指定文件名创建实例,一旦创建文档就开启,接着就可以用来写入数据。二者在使用时,都不需要用close()关闭文档。
接下来,通过实例来演示读取本地文件的流程。为了方便,我们先在当前目录下新建一个名为“read.txt”的文件,并向其中写入“AAA软件教育”,接着编写程序将文件中的内容读出并打印到控制台,如例12-5所示。
例12-5 Demo1205.java
1 package com.aaa.p120302;
2 import java.io.*;
3
4 public class Demo1205 {
5 public static void main(String[] args) {
6 FileInputStream fileInput = null;
7 try {
8 fileInput = new FileInputStream("read.txt"); // 创建文件输入流对象
9 int n = 1024; // 设定读取的字节数
10 byte buffer[] = new byte[n];
11 // 读取输入流
12 while ((fileInput.read(buffer, 0, n) != -1) && (n > 0)) {
13 System.out.print(new String(buffer));
14 }
15 } catch (Exception e) {
16 System.out.println(e);
17 } finally {
18 if (fileInput != null){
19 try {
20 fileInput.close(); // 释放资源
21 } catch (IOException e) {
22 e.printStackTrace();
23 }
24 }
25 }
26 }
27 }
程序的运行结果如下:
AAA软件教育
在例12-5中,我们建立了一个长度为1024的byte数组,将其传入read()方法中,并设置始末位置为0到n,此时read()方法一次读1024个字节。运行之后,我们看到控制台打印出“AAA软件教育”。
注意:在例12-5中,如果程序中途出现错误,程序将直接中断,所以一定要将关闭资源的close()方法写到finally中。另外,由于finally中不能直接访问try中的内容,所以要将FileInputStream定义在try的外面。由于篇幅有限,后面的代码不再重复异常处理的标准写法,直接将异常抛出。
需要注意的是,当创建文件输入流时,一定要保证目录下有对应文件存在,否则会报FileNotFoundException异常,提示“java.io.FileNotFoundException: read.txt (系统找不到指定的文件)”。
明白了FileInputStream的用法,下面我们来看看与之相对的FileOutputStream,它使用字节流向一个文件中写入内容,两者用法相似,如例12-6所示。
例12-6 Demo1206.java
1 package com.aaa.p120302;
2 import java.io.*;
3
4 public class Demo1206 {
5 public static void main(String[] args) throws Exception {
6 System.out.print("请输入要保存到文件的内容:");
7 int count, n = 1024;
8 byte buffer[] = new byte[n];
9 count = System.in.read(buffer); // 读取标准输入流
10 // 创建文件输出流对象
11 FileOutputStream fileOutput = new FileOutputStream("read.txt");
12 fileOutput.write(buffer, 0, count); // 写入输出流
13 System.out.println("已保存到read.txt!");
14 fileOutput.close(); // 释放资源
15 }
16 }
程序的运行结果如下:
请输入要保存到文件的内容:AAA软件欢迎你
已保存到read.txt!
例12-6程序的运行结果显示已保存到read.txt,此时文件内容如下:
AAA软件欢迎你
与输入流不同的是,当文件不存在时,输出流会先创建文件再向其中写入内容。当文件已经存在时,会先将原本的内容清空,再向其中写入。例12-6中执行之后,原本的内容被替换成了新的内容。如果想要保留原来内容,只需要在原来的基础上构造输出流时追加一个Boolean类型的参数,该参数用于指定是否为追加写入,如果为true,就能够在源文件尾部的下一行写入内容了,如例12-7所示。
例12-7 Demo1207.java
1 package com.aaa.p120302;
2 import java.io.*;
3
4 public class Demo1207 {
5 public static void main(String[] args) throws Exception {
6 System.out.print("请输入要保存到文件的内容:");
7 int count, n = 1024;
8 byte buffer[] = new byte[n];
9 count = System.in.read(buffer); // 读取标准输入流
10 // 创建文件输出流对象
11 FileOutputStream fileOutput = new FileOutputStream("read.txt", true);
12 fileOutput.write(buffer, 0, count); // 写入输出流
13 System.out.println("已保存到read.txt!");
14 fileOutput.close(); // 释放资源
15 }
16 }
程序的运行结果如下所示:
请输入要保存到文件的内容:专业的软件培训机构
已保存到read.txt!
运行结果显示已保存到read.txt,由于我们是自行创建了read.txt 文件,并在例12-6中重写,而本次运行的结果是将对应内容追加到read.txt中,此时文件内容如下:
AAA软件欢迎你
专业的软件培训机构
通过例12-7可以看出,构造FileOutputStream时声明append参数为true,即可在原文件基础上写入新内容。
12.3.3 文件拷贝
前面我们分别讲解了文件输入流和文件输出流的使用,现在我们将二者结合起来,就能够完成更复杂的操作,这也是我们在日常开发中可能使用到的。
输入流和文件输结合使用可以实现文件的复制,首先我们来做一些准备工作。在当前目录下建立两个文件夹,分别命名为image、target,之后向image目录下存放一张图片,并命名为img.png。然后,开始编写代码,如例12-8所示。
例12-8 Demo1208.java
1 package com.aaa.p1203;
2 import java.io.*;
3
4 public class Demo1208 {
5 public static void main(String[] args) throws Exception {
6 // 创建文件输入流对象
7 FileInputStream input = new FileInputStream("image\\img.png");
8 // 创建文件输出流对象
9 FileOutputStream output = new FileOutputStream("target\\img.png");
10 int len; // 定义len,记录每次读取的字节
11 long begin = System.currentTimeMillis(); // 拷贝文件前的系统时间
12 while ((len = input.read()) != -1) { // 读取文件并判断是否到达文件末尾
13 output.write(len); // 将读到的字节写入文件
14 }
15 long end = System.currentTimeMillis(); // 拷贝文件后的系统时间
16 System.out.println("拷贝文件耗时:" + (end - begin) + "毫秒");
17 output.close(); // 释放资源
18 input.close();
19 }
20 }
程序的运行结果如下所示:
拷贝文件耗时:875毫秒
控制台中打印出了程序拷贝文件所消耗的时间,而图片就在这段时间内由字节流的方式实现了拷贝,如图12.6所示。
图12.6 InputStream子类结构图 图12.7 OutputStream子类结构图
由于不同计算机性能不同,或同一个计算机在不同情况下负载不同,拷贝图片所消耗的时间都有可能会有差别,具体时间以现实情况为准。
注意:在例12-8中,指定image和target的目录用“\\”,这是因为windows系统目录用反斜杠“\”表示,但Java中反斜杠是特殊字符,所以写成“\\”指定路径,也可以使用“/”指定目录,如“image/img.png”。
12.3.4 字节流的缓冲区
前文讲解了字节流拷贝文件,还有一种更高效的拷贝方式,那就是在使用中加上缓冲区,缓冲区可以帮助提升字节传输效率。因为,不加缓冲区的时候是一个字节一个字节地传输,而加了缓冲区后则是先将字节填满一个缓冲区,再将整个缓冲区的字节一并传输,这样可以显著降低传输次数,提升传输效率。每次传输都会消耗一定的时间,但是使用缓冲区会在本地占用一定的空间,这属于空间换时间的方式,
接下来,通过案例来演示缓冲区在字节流拷贝中的用法,如例12-9所示。
例12-9 Demo1209.java
1 package com.aaa.p120304;
2 import java.io.*;
3
4 public class Demo1209 {
5 public static void main(String[] args) throws Exception {
6 // 创建文件输入流对象
7 FileInputStream input = new FileInputStream("image\\img.png");
8 // 创建文件输出流对象
9 FileOutputStream output = new FileOutputStream("target\\img.png");
10 byte[] b = new byte[1024]; // 定义缓冲区大小
11 int len; // 定义len,记录每次读取的字节
12 long begin = System.currentTimeMillis();// 拷贝文件前的系统时间
13 while ((len = input.read(b)) != -1) { // 读取文件并判断是否到达文件末尾
14 output.write(b, 0, len); // 从第1个字节开始,向文件写入len个字节
15 }
16 long end = System.currentTimeMillis(); // 拷贝文件后的系统时间
17 System.out.println("拷贝文件耗时:" + (end - begin) + "毫秒");
18 output.close(); // 释放资源
19 input.close();
20 }
21 }
程序的运行结果如下:
拷贝文件耗时:38毫秒
从例12-9的运行结果可以看出,与例12-8相比,拷贝所耗的时间大大地降低了,说明使用缓冲区有效减少了字节流的传输次数,从而提升了程序的运行效率。
除了上面这种方式,还有一种封装性更好,更易用的方式来使用带缓冲区的I/O流,那就是BufferedInputStream和BufferedOutputStream,它们的构造器接收对应的I/O流,并返回带缓冲的BufferedInputStream对象和BufferedOutputStream对象,这体现出了装饰设计模式的思想,其接收的参数为装饰对象,返回的类为装饰结果,结构如图12.8所示。
从图12.8可以看出,在程序和文件之间的核心由节点流传输数据,如我们在之前所讲到的FileInputStream和FileOutputStream。在外层为节点流的封装,如我们现在讲的BufferedInputStream和BufferedOutputStream。
接下来,我们通过案例来演示缓冲流的使用,如例12-10所示。
例12-10 Demo1210.java
1 package com.aaa.p120304;
2 import java.io.*;
3
4 public class Demo1210 {
5 public static void main(String[] args) throws Exception {
6 // 创建文件输入流对象
7 FileInputStream fInput = new FileInputStream("image\\img.png");
8 // 创建文件输出流对象
9 FileOutputStream fOutput = new FileOutputStream("target\\img.png");
10 // 将创建的节点流的对象作为形参传递给缓冲流的构造方法中
11 BufferedInputStream bInput = new BufferedInputStream(fInput);
12 BufferedOutputStream bOutput = new BufferedOutputStream(fOutput);
13 int len; // 定义len,记录每次读取的字节
14 long begin = System.currentTimeMillis();// 拷贝文件前的系统时间
15 while ((len = bInput.read()) != -1){ // 读取文件并判断是否到达文件末尾
16 bOutput.write(len); // 将读到的字节写入文件
17 }
18 long end = System.currentTimeMillis(); // 拷贝文件后的系统时间
19 System.out.println("拷贝文件耗时:" + (end - begin) + "毫秒");
20 bInput.close();
21 bOutput.close();
22 }
23 }
程序的运行结果如下:
拷贝文件耗时:51毫秒
例12-10的运行结果如上所示,拷贝img文件的时间为51毫秒,和未使用缓冲时相比,拷贝的速度明显加快,因为缓冲流内部定义了一个长度为8192的字节数组作为缓冲区,在使用read()方法或write()方法进行读写时首先将数据存入该数组中,然后以数组为对象进行操作,这显著降低了操作次数,让程序完成同样的工作花费了更少的时间。
12.4 字符流
前文讲解了使用InputStream和OutputStream来处理字节流,也就是二进制文件,而Reader和Writer是用来处理字符流的,也就是文本文件。与文件字节输入/输出流的功能一样,文件字符输入/输出类Reader和Writer只是建立了一条通往文本文件的通道,而要实现对字符数据的读写操作,还需要相应的读方法和写方法来完成。
12.4.1 字符流概述
除了字节流,Java还提供了字符流,用于操作字符。与字节流类似,字符流也有两个抽象基类,分别是Reader和Writer。Reader是字符输入流,用于从目标文件读取字符;Writer是字符输出流,用于向目标文件写入字符。字符流也是由两个抽象基类衍生出很多子类,由子类来实现功能,先来了解一下它们的子类结构,如图12.9和图12.10所示。
图12.9 Reader子类结构图 图12.10 Writer子类结构图
可以看出,字符流与字节流相似,也是很有规律的,这些子类都是以它们的抽象基类为结尾命名的,并且大多以Reader和Writer结尾,如CharArrayReader和CharArrayWriter。接下来,我们详细讲解字符流的使用。
12.4.2 操作文件
Reader和Writer有众多子类,其中FileReader和FileWriter是两个很常用的子类,FileReader类是用来从文件中读取字符的,操作文件的字符输入流。
接下来,通过案例来演示如何从文件中读取字符。首先在当前目录新建一个文本文件read.txt,文件内容如下:
AAA软件教育
fileReader
创建文件完成后,开始编写代码,如例12-11所示。
例12-11 Demo1211.java
1 package com.aaa.p120402;
1 import java.io.*;
2
3 public class Demo1211 {
4 public static void main(String[] args) throws Exception {
5 File file = new File("read.txt");
6 FileReader fileReader = new FileReader(file);
7 int len; // 定义len,记录读取的字符
8 while ((len = fileReader.read()) != -1){ // 判断是否读取到文件的末尾
9 System.out.print((char) len); // 打印文件内容
10 }
11 fileReader.close(); // 释放资源
12 }
13 }
程序的运行结果如下:
AAA软件教育
fileReader
例12-11中,首先定义一个文件字符输入流,然后在创建输入流实例时,将文件以参数传入,读取到文件后,用变量len记录读取的字符,然后循环输出。这里要注意len是int类型,所以输出时要强转类型,第10行中将len强转为char类型。
与FileReader类对应的是FileWriter类,它是用来将字符写入文件,操作文件字符输出流的。
接下来,通过案例来演示如何将字符写入文件,如例12-12所示。
例12-12 Demo1212.java
1 package com.aaa.p120402;
2 import java.io.*;
3
4 public class Demo1212 {
5 public static void main(String[] args) throws Exception {
6 File file = new File("read.txt");
7 FileWriter fileWriter = new FileWriter(file);
8 fileWriter.write("AAA软件专业的Java学习平台"); // 写入文件的内容
9 System.out.println("已保存到read.txt!");
10 fileWriter.close(); // 释放资源
11 }
12 }
程序的运行结果如下:
已保存到read.txt!
例12-12运行结果显示已保存到read.txt文件,文件内容如下:
AAA软件专业的Java学习平台
FileWriter与FileOutputStream类似,如果指定的目标文件不存在,则先新建文件,再写入内容,如果文件存在,会先清空文件内容,然后写入新内容,但是结尾不加换行符。如果想在文件内容的末尾追加内容,则需要调用构造方法FileWriter (String FileName,boolean append)来创建文件字符输出流对象,将参数append指定为true即可,将例12-12第5行代码修改如下:
FileWriter fileWriter = new FileWriter(file, true);
再次运行程序,输出流会将字符追加到文件内容的末尾,不会清除文件本身的内容,结尾同样是没有换行符的。
12.4.3 转换流
前文分别讲解了字节流和字符流,有时字节流和字符流之间可能也需要进行转换,在JDK中提供了可以将字节流转换为字符流的两个类,分别是InputStreamReader类和OutputStreamWriter类,它们被称之为转换流。其中,OutputStreamWriter类可以将一个字节输出流转换成字符输出流,而InputStreamReade类可以将一个字节输入流转换成字符输入流。转换流的出现方便了对文件的读写,它在字符流与字节流之间架起了一座桥梁,使原本没有关联的两种流的操作能够进行转换,提高了程序的灵活性。通过转换流进行读写数据的过程,如图12.11所示。
图12.11 转换流示意图
图12.11中,程序向文件写入数据时将输出的字符流转变为字节流,程序从文件读取数据时将输入的字节流变为字符流,有效地提高了读写效率。
接下来,通过案例来演示转换流的使用。首先在当前目录新建一个文本文件Conversion.txt,文件内容为“AAA软件教育”。创建文件完成后,开始编写代码,如例12-13所示。
例12-13 Demo1213java
1 package com.aaa.p120403;
2 import java.io.*;
3
4 public class Demo1213 {
5 public static void main(String[] args) throws IOException {
6 // 创建字节输入流
7 FileInputStream input = new FileInputStream("Conversion.txt");
8 // 将字节输入流转换为字符输入流
9 InputStreamReader inputReader = new InputStreamReader(input);
10 // 创建字节输出流
11 FileOutputStream output = new FileOutputStream("target.txt");
12 // 将字节输出流转换成字符输出流
13 OutputStreamWriter outputWriter = new OutputStreamWriter(output);
14 int str;
15 while ((str = inputReader.read()) != -1) {
16 outputWriter.write(str);
17 }
18 outputWriter.close();
19 inputReader.close();
20 }
21 }
例12-14程序运行结束后,会在当前目录生成一个target.txt文件,如图12.12和图12.13所示。
图12.12 文件拷贝前 图12.13 文件拷贝后
在例12-13中实现了字节流与字符流之间的互相转换,将字节流转换为字符流,从而实现直接对字符的读写。这里要注意,如果用字符流操作非文本文件,如操作视频文件,很有可能会造成部分数据丢失。
12.5 对象序列化方式
Java提供了一种对象序列化的机制,该机制中一个对象可以被表示为一个字节序列,该字节序列包括该对象的数据、对象的类型和存储在对象中的数据的类型。将序列化对象写入文件之后,可以从文件中读取出来,并且对它进行反序列化。也就是说,对象的类型信息、对象的数据,还有对象中的数据类型可以用来在内存中新建对象。上述整个过程都是Java虚拟机(JVM)独立完成的,这样在一个平台上序列化的对象可以在另一个完全不同的平台上反序列化该对象。
12.5.1 对象序列化概述
序列化机制可以将实现序列化的Java对象转换成字节序列,而这些字节序列可以保存在磁盘上,或者通过网络传输,以备以后重新恢复成原来的对象继续使用。序列化机制可以使Java对象脱离程序的运行而独立存在。
对象的序列化(Serialize)是指将一个Java对象写入I/O流中,与此对应,对象的反序列化(Deserialize)则是指从I/O流中恢复该Java对象。
如果需要让某个对象支持序列化机制,则必须让它的类是可序列化的(Serializable)。为了让某个类是可序列化的,该类就需要实现Serializable或者Externalizable这两个接口之一,一般推荐使用Serializable接口,因为Serializable接口只需实现不需要重写任何方法,使用起来较为简单。
Java的很多类其实已经实现了Serializable,该接口是一个标记接口,实现该接口时无须实现任何方法,它只是表明该类的实例是可序列化的。所有可能在网络上传输的对象的类都必须是可序列化的,否则程序可能会出现异常,如RMI(Remote Method Invoke,即远程方法调用,是Java EE的基础)过程中的参数和返回值。所有需要保存到磁盘里的对象的类都必须可序列化,如Web应用中需要保存到HttpSession或ServletContext属性的Java对象。
因为序列化是RMI过程的参数和返回值都必须实现的机制,而RMI又是Java EE技术的基础,所有的分布式应用常常需要跨平台、跨网络,所以要求所有传递的参数、返回值必须实现序列化。因此,序列化机制是Java EE平台的基础,通常建议程序创建的每个JavaBean类都实现Serializable接口。
12.5.2 如何实现对象序列化的持久化
如果需要将某个对象保存到磁盘上或者通过网络传输,那么这个类就需要实现Serializable接口或者Extermalizable接口之一。
使用Serializable来实现序列化非常简单,主要让目标类实现Serializable接口即可,无须实现任何方法。一旦某个类实现了Serializable接口,该类的对象就是可序列化的,程序可以通过如下两个步骤来序列化该对象:
• 创建一个ObjectOutputStream,这个输出流是一个处理流,所以必须建立在其他节点流的基础之上,代码如下:
// 创建个 ObjectOutputStreamn输出流
FileOutputStream fos = new FileOutputStream("person.txt");
ObjectOutputStream oos = new ObjectOutputStream(fos);
• 调用ObjectOutputStream对象的writeObject()方法输出可序列化对象,代码如下:
// 将一个Person对象输出到输出流中
oos.writeObject(person);
下面的程序定义了一个Person类,这个类就是一个普通的Java类,只是实现了Serializable接口,该接口代表该类的对象是可序列化的,代码如下:
1 import java.io.Serializable;
2
3 public class Person implements Serializable {
4 private String name;
5 private Integer age;
6 public Person(String name, Integer age) {
7 this.name = name;
8 this.age = age;
9 }
10 public String getName() {
11 return name;
12 }
13 public void setName(String name) {
14 this.name = name;
15 }
16 public Integer getAge() {
17 return age;
18 }
19 public void setAge(Integer age) {
20 this.age = age;
21 }
22 @Override
23 public String toString() {
24 return "Person{" + "name='" + name + '\'' + ", age=" + age + '}';
25 }
26 }
接下来,通过案例来演示使用ObjectOutputStream将一个Person对象写入磁盘文件,如例12-14所示。
例12-14 Demo1214.java
1 package com.aaa.p120502;
2
3 public class Demo1214 {
4 public static void main(String[] args) {
5 try (FileOutputStream fos = new FileOutputStream("person.txt");
6 ObjectOutputStream oos = new ObjectOutputStream(fos)) {
7 Person person = new Person("小乔", 18);
8 oos.writeObject(person);
9 } catch (IOException e) {
10 e.printStackTrace();
11 }
12 }
13 }
例12-14中,第6行代码创建了一个ObjectOutputStream输出流,这个ObjectOutputStream输出流建立在一个文件输出流的基础之上,第8行代码使用writeObject()方法将一个Person对象写入输出流。运行这段代码,将会看到生成了一个Person.txt文件,该文件的内容就是Person对象。
如果想从二进制流中恢复Java对象,则需要使用反序列化。反序化的的步骤如下:
• 创建一个ObjectInputStream输入流,这个输入流是个处理流,所以必须建立在其他节点流的基础之上,代码如下:
// 创建一个ObjectInputStream输入流
FileInputStream fis = new FileInputStream("person.txt");
ObjectInputStream ois = new ObjectInputStream(fis);
• 调用ObjectInputStream对象的readObject()方法读取流中的对象,该方法返回一个Object类型的Java对象,如果程序知道该Java对象的类型,则可以将该对象强制类型转换成其真实的类型,代码如下:
// 从输入流中读取一个Java对象,并将其强制类型转换为Person类
Person person = (Person) ois.readObject();
接下来,通过案例来演示从刚刚生成的person.txt文件中读取Person对象,如例12-15所示。
例12-15 Demo1215.java
1 package com.aaa.p120502;
2
3 public class Demo1215 {
4 public static void main(String[] args) {
5 try (FileInputStream fis = new FileInputStream("person.txt");
6 ObjectInputStream ois = new ObjectInputStream(fis)) {
7 Person person = (Person) ois.readObject();
8 System.out.println(person);
9 } catch (Exception e) {
10 e.printStackTrace();
11 }
12 }
13 }
例12-15中,第6行代码将一个文件输入流包装成ObjectInputStream输入流,第7行代码使用readObject()读取了文件中的Java对象,这就完成了反序列化过程。
必须指出的是,反序列化读取的仅仅是Java对象的数据,而不是Java类,因此采用反序列化恢复Java对象时,必须提供该Java对象所属类的class文件,否则将会引发ClassNotFoundException异常。
注意:在ObjectInputStream输入流中的readObject()方法声明抛出了ClassNotFoundException异常,也就是说,当反序列化时找不到对应的Java类时将会引发该异常。
如果使用序列化机制向文件中写入了多个Java对象,使用反序列化机制恢复对象时必须按实际写入的顺序读取。
当一个可序列化类有多个父类时(包括直接父类和间接父类),这些父类要么有无参数的构造器, 要么也是可序列化的,否则反序列化时将抛出InvalidClassException异常。如果父类是不可序列化的,只是带有无参数的构造器,则该父类中定义的成员变量值不会序列化到二进制流中。
12.5.3 引用对象的序列化控制
前文中的Person类的两个成员变量分别是String类型和Integer类型,如果某个类的成员变量的类型不是基本类型或String类型,而是另一个引用类型,那么这个引用类必须是可序列化的,否则拥有该类型成员变量的类也是不可序列化的。
下面的程序中,Teacher类持有一个Student类的引用,只有Student类是可序列化的,Teacher类才是可序列化 的。如果Student类不可序列化,则无论Teacher类是否实现Serilizable或Externalizable接口,则Teacher类都是不可序列化的。代码如下:
1 public class Teacher implements Serializable {
2 private String name;
3 private Student student;
4 public Teacher(String name, Student student) {
5 this.name = name;
6 this.student = student;
7 }
8 public String getName() {
9 return name;
10 }
11 public void setName(String name) {
12 this.name = name;
13 }
14 public Student getStudent() {
15 return student;
16 }
17 public void setStudent(Student student) {
18 this.student = student;
19 }
20 @Override
21 public String toString() {
22 return "Teacher{" + "name='" + name + '\'' +", student=" + student +'}';
23 }
24 }
25
26 class Student implements Serializable {
27 private String name;
28 private Integer age;
29 public Student(String name, Integer age) {
30 this.name = name;
31 this.age = age;
32 }
33 public String getName() {
34 return name;
35 }
36 public void setName(String name) {
37 this.name = name;
38 }
39 public Integer getAge() {
40 return age;
41 }
42 public void setAge(Integer age) {
43 this.age = age;
44 }
45 @Override
46 public String toString() {
47 return "Student{" + "name='" + name + '\'' + ", age=" + age + '}';
48 }
49 }
注意:当程序序列化一个Teacher对象时,如果该Teacher对象持有一个Student对象的引用,为了在反序列化时可以正常恢复该Teacher对象,程序会顺带将该Student对象也进行序列化,所以Student类也必须是可序列化的,否则Teacher类将不可序列化。
现在假设有如下特殊情形:程序中有两个Teacher对象,它们的student实例变量都引用同一个Student对象,而且该Student对象还有一个引用变量引用它,代码如下:
Student student = new Student("小乔", 18);
Teacher teacher1 = new Teacher("周瑜", student);
Teacher teacher2 = new Teacher("曹操", student);
上述代码创建了两个Teacher对象和一个Student对象,这3个对象在内存中的存储示意图如图12.14所示。
这里产生了一个问题,如果先序列化teacher1对象,则系统将该teacherl对象所引用的Student对象一起序列化。当程序再次序列化teacher2对象时,系统则一样会序列化该teacher2对象,并且会再次序列化teacher2对象所引用的Student对象。如果程序再显式序列化student对象,系统将再次序列化该Student对象。这个过程似乎会向输出流中输出3个Student对象。
如果系统向输出流中写入了3个Student对象,那么后果是当程序从输入流中反序列化这些对象时,将会得到3个Student对象,从而导致teacher1和teacher2所引用的Student对象不是同一个对象,这显然与图12.12所示的效果不一致,也违背了Java序列化机制的初衷。所以,Java序列化机制采用了一种特殊的序列化算法,其算法内容如下:
• 所有保存到磁盘中的对象都有一个序列化编号。
• 当程序试图序列化一个对象时,程序将先检查该对象是否已经被序列化过,只有该对象从未(在本次虚拟机中)被序列化过,系统才会将该对象转换成字节序列并输出。
• 如果某个对象已经序列化过,程序将只是直接输出一个序列化编号,而不是再次重新序列化该对象。
根据上面的序列化算法,可以得到一个结论,当第2次、第3次序列化Student对象时,程序不会再次将Student对象转换成字节序列并输出,而是仅仅输出一个序列化编号。例如,有如下顺序的序列化代码:
oos.writeObject(teacher1);
oos.writeObject(teacher2);
oos.writeObject(student);
上面代码一次序列化了teacher1、teacher2和student对象,序列化后磁盘文件的存储示意图如图12.15所示,通过改图可以很好地理解Java序列化的底层机制。不难看出,当多次调用writeObject()方法输出同一个对象时,只有当第1次调用writeObject()方法时才会将该对象转换成字节序列并输出。
接下来,通过案例来演示序列化两个Teacher对象,两个Teacher对象都持有一个引用同一个Student对象的引用,而且程序两次调用writeObject()方法输出同一个Teacher对象,如例12-16所示。
例12-16 Demo1216.java
1 package com.aaa.p120503;
2
3 public class Demo1216 {
4 public static void main(String[] args) {
5 try (FileOutputStream fos = new FileOutputStream("teacher.txt");
6 ObjectOutputStream oos = new ObjectOutputStream(fos)) {
7 Student student = new Student("小乔", 18);
8 Teacher teacher1 = new Teacher("周瑜", student);
9 Teacher teacher2 = new Teacher("曹操", student);
10 oos.writeObject(teacher1);
11 oos.writeObject(teacher2);
12 oos.writeObject(student);
13 oos.writeObject(teacher2);
14 } catch (Exception e) {
15 e.printStackTrace();
16 }
17 }
18 }
例12-16中,4次调用了writeObject0方法来输出对象,实际上只序列化了3个对象,而且序列的两个Teacher对象的student引用实际是同一个Student对象。
接下来,通过案例来演示读取序列化文件中的对象,如例12-17所示。
例12-17 Demo1217.java
1 package com.aaa.p120503;
2
3 public class Demo1217 {
4 public static void main(String[] args) {
5 try (FileInputStream fis = new FileInputStream("teacher.txt");
6 ObjectInputStream ois = new ObjectInputStream(fis)) {
7 Teacher t1 = (Teacher) ois.readObject();
8 Teacher t2 = (Teacher) ois.readObject();
9 Student s = (Student) ois.readObject();
10 Teacher t3 = (Teacher) ois.readObject();
11 System.out.println("t1的student引用和s是不是相同对象:"
12 + (t1.getStudent() == s));
13 System.out.println("t2的student引用和s是不是相同对象:"
14 + (t2.getStudent() == s));
15 System.out.println("t2和t3是不是相同对象:" + (t2 == t3));
16 } catch (Exception e) {
17 e.printStackTrace();
18 }
19 }
20 }
程序运行结果如下:
t1的student引用和s是不是相同对象:true
t2的student引用和s是不是相同对象:true
t2和t3是不是相同对象:true
例12-17中,代码依次读取了序列化文件中的4个Java对象,但通过后面的比较判断,不难发现t2和t3是同一个Java对象,tl、t2和s 的引用变量引用的也是同一个Java对象,这证明了图12.15所示的序列化机制。
根据Java序列化机制,如果多次序列化同一个Java对象时,只有第1次序列化时才会把该Java 对象转换成字节序列并输出,这样也可能会引发一个潜在的问题,即当程序序列化一个可变对象时,只有第1次使用writeObject()方法输出时才会将该对象转换成字节序列并输出,当程序再次调用writeObject()方法时,程序只是输出前面的序列化编号,即使后面该对象的实例变量值已被改变,改变的实例变量值也不会被输出,如例12-18所示。
例12-18 Demo1218.java
1 package com.aaa.p120503;
2
3 public class Demo1218 {
4 public static void main(String[] args) {
5 try (FileOutputStream fos = new FileOutputStream("teacher.txt");
6 ObjectOutputStream oos = new ObjectOutputStream(fos);
7 FileInputStream fis = new FileInputStream("teacher.txt");
8 ObjectInputStream ois = new ObjectInputStream(fis)) {
9 Student student1 = new Student("小乔", 18);
10 oos.writeObject(student1);
11 student1.setName("大乔");
12 System.out.println("修改name后:" + student1);
13 oos.writeObject(student1);
14 Student s2 = (Student) ois.readObject();
15 Student s3 = (Student) ois.readObject();
16 System.out.println("s2与s3进行对比:" + (s2 == s3));
17 System.out.println("s2反序列化后:" + s2);
18 System.out.println("s3反序列化后:" + s3);
19 } catch (Exception e) {
20 e.printStackTrace();
21 }
22 }
23 }
程序的运行结果如下:
修改name后:Student{name='大乔', age=18}
s2与s3进行对比:true
s2反序列化后:Student{name='小乔', age=18}
s3反序列化后:Student{name='小乔', age=18}
例12-18中,先使用writeObject()方法写入了一个Student对象,接着改变了Student对象的实例变量name的值,然后程序再次序列化输出Student对象,但这次不会将Student对象转换成字节序列输出了,而是仅输出了一个序列化编号。第14行和第15行的代码两次调用readObject()方法读取了序列化文件中的Java对象,比较两次读取的Java对象结果为true,证明是同一对象。然后,程序再次输出两个对象,两个对象的name值依然是“小乔”,表明改变后的Student对象并没有被写入,这与Java序列化机制相符。
注意:当使用Java序列化机制去序列化可变对象时一定要注意,只有第一次调用writeObject()方法来输出对象时才会将对象转换成字节序列,并写入到ObjectOutputStream。在后面程序中,即使该对象的实例变量发生了改变,再次调用writeObject()方法输出该对象时,改变后的实例变量也不会被输出。
12.6 本章小结
• Java I/O系统负责处理程序的输入和输出,I/O类库位于java.io包中,它对各种常见的输入流和输出流进行了抽象。
• 通过调用File类提供的各种方法,能够完成创建文件、删除文件、重命名文件、判断文件的读写权限以及文件是否存在、设置和查询文件的创建时间和权限等操作。
• 根据数据的读取或写入分类,流可以分为输入流和输出流。
• 根操作对象分类,流可以分为字节流和字符流。字节流可以处理所有类型数据,如图片、MP3、AVI视频文件等,而字符流只能处理字符数据。只要是处理纯文本数据,就要优先考虑使用字符流,除此之外都用字节流。
• Java的序列化机制可以将实现序列化的Java对象转换成字节序列,而这些字节序列可以保存在磁盘上,或者通过网络传输,以备以后重新恢复成原来的对象继续使用。
• Java的反序列化机制是客户端从文件中或网络上获得序列化后的对象字节流后,根据字节流中所保存的对象状态及描述信息,通过反序列化重建对象。
12.7 理论试题与实践练习
1.填空题
1.1 关于文件的类都放在 包下面。
1.2 字节流有两个抽象基类 和 ,分别处理字节流的输入和输出。
2.选择题
2.1 下面哪个流类属于面向字符的输入流( )
A.BufferedWriter B.FileInputStream
C.ObjectInputStream D.InputStreamReader
2.2 新建一个流对象,下面哪个选项的代码是错误的( )
A.new BufferedWriter(new FileWriter("a.txt"));
B.new BufferedReader(new FileInputStream("a.dat"));
C.new GZIPOutputStream(new FileOutputStream("a.zip"));
D.new ObjectInputStream(new FileInputStream("a.dat"));
2.3 要从文件“file.dat”中读出第10个字节到变量c中,下列哪个方法适合( )
A.FileInputStream in = new FileInputStream("file.dat"); in.skip(9); int c = in.read();
B.FileInputStream in = new FileInputStream("file.dat"); in.skip(10); int c = in.read();
C.FileInputStream in = new FileInputStream("file.dat"); int c = in.read();
D.RandomAccessFile in = new RandomAccessFile("file.dat"); in.skip(9);
2.4 Java I/O程序设计中,下列描述正确的是( )
A.OutputStream用于写操作 B.InputStream用于写操作
C.只有字节流可以进行读操作 D.I/O库不支持对文件可读可写API
2.5 下列哪个不是合法的字符编码( )
A.UTF-8 B.ISO8859-1
C.GBL D.ASCII
3.思考题
3.1 请简述Java中有几种类型的流?
4.编程题
4.1 编写一个程序,要求用户输入一个路径,运行程序列出路径下的所有文件。
4.2 编写一个程序实现文件拷贝功能,要求用户输入源文件路径和目标文件路径,运行程序实现文件拷贝。
4.3 编写一个程序,产生50个1~9999之间的随机数,然后利用BufferedWriter类将其写入到文件file.txt中,之后再读取这些数字,并进行升序排列。
- 点赞
- 收藏
- 关注作者
评论(0)