城市直播房产教育博客汽车
投稿投诉
汽车报价
买车新车
博客专栏
专题精品
教育留学
高考读书
房产家居
彩票视频
直播黑猫
投资微博
城市上海
政务旅游

JAVA输入输出流详细讲解

7月7日 火凤派投稿
  应用程序经常需要访问文件和目录,读取文件信息或写入信息到文件,即从外界输入数据或者向外界传输数据,这些数据可以保存在磁盘文件、内存或其他程序中。在Java中,对这些数据的操作是通过IO技术来实现的。所谓IO技术,就是数据的输入(Input)、输出(Output)技术。本章将对Java的IO系统进行讲解,包括IO的体系结构、流的概念、字节流、处理字节流的基本类InputStream和OutputStream、字符流、处理字符流的基本类Reader和Writer、文件管理、序列化和反序列化等。
  12。1IO流概述
  Java将数据的输入输出操作当作流来处理,流是一组从源头到目的地的有序的字节序列。在Java程序中,从某个数据源读取数据到程序的流称为输入流,通过程序使用数据流将数据写入到目的地的称为输出流。输入流和输出的读取和写入流程如图12。1所示。
  (a)输入流(b)输出流
  图12。1输入输出流示意图
  当程序需要从某个数据源读入数据的时候,就会开启一个输入流,数据源可以是文件、内存或网络等。相反,需要写出数据到某个数据源目的地的时候,也会开启一个输出流,这个数据源目的地也能够是文件、内存或网络等。IO流有很多种,按操作数据单位不同可分为字节流和字符流,按数据流的方向不同分为输入流和输出流,如表12。1所示。
  表12。1流的分类
  输入输出
  字节流
  字符流
  输入流
  InputStream
  Reader
  输出流
  OutputStream
  Writer
  输入流和输出流的区别是以程序为中心来进行判断,从外部设备读取数据到程序是输入流,从程序写入数据到外部设备是输出流。字节流的单位是一个字节,即8字符流的单位是两个字节,即16bit。表12。1是IO流的简单分类,实际开发中需要使用的的IO流共涉及40多个类,都是从这4个抽象基类派生的。接下来,我们先学习输入输出流的体系结构。
  Java。io包中的最重要的部分是由5个类和一个接口组成。5个类是指File、RandomAccessFile、InputStream、OutputStream、Writer、Reader,一个接口指的是Serializable。掌握了这些IO的核心操作,那么对于Java中的IO体系也就有了一个初步的认识了。总体上看,JavaIO主要包括如下3个部分:
  流式部分:IO的主体部分。
  非流式部分:主要包含一些辅助流式部分的类,如File类、RandomAccessFile类和FileDescriptor类等。
  其他类:主要是文件读取部分的与安全相关的类(如SerializablePermission类),以及与本地操作系统相关的文件系统的类,如(FileSystem类、Win32FileSystem类和WinNTFileSystem类)。
  这里,将JavaIO中主要的类简单介绍如下:
  File类(文件特征与管理类):用于文件或者目录的描述信息等(Anabstractrepresentationoffileanddirectorypathnames),如生成新目录、修改文件名、删除文件、判断文件所在路径等。
  InputStream类(二进制格式操作类):基于字节输入操作的抽象类,是所有输入流的父类,定义了所有输入流都具有的共同特征。
  OutputStream类(二进制格式操作类):基于字节输出操作的抽象类,是所有输出流的父类,定义了所有输出流都具有的共同特征。
  Reader类(文件格式操作类):抽象类,基于字符的输入操作。
  Writer类(文件格式操作类):抽象类,基于字符的输出操作。
  RandomAccessFile类(随机文件操作类):它的功能丰富,可以从文件的任意位置进行存取(输入输出)操作。
  综上所述,Java中IO流的体系结构如图12。2所示。
  图12。2IO流体系结构图
  12。2File类
  File类可以用于处理文件目录。在对一个文件进行输入输出,必须先获取有关该文件的基本信息,如文件是否可以读取、能否被写入、路径是什么等。java。io。File类不属于Java流系统,但它是文件流进行文件操作的辅助类,提供了获取文件基本信息以及操作文件的一些方法,通过调用File类提供的相应方法,能够完成创建文件、删除文件以及对目录的一些操作。
  12。2。1File类的常用方法
  File类的对象是一个文件或目录的抽象,它并不打开文件或目录,而是指定要操作的文件或目录。File类的对象一旦创建,就不能再修改。要创建一个新的File对象,需要使用它的构造方法,如表12。2所示。
  表12。2File类构造方法
  构造方法
  功能描述
  publicFile(Stringfilename)
  创建File对象,filename表示文件或目录的路径
  publicFile(Stringparent,Stringchild)
  创建File对象,parent表示上级目录,child表示指定的子目录或文件名
  publicFile(Fileobj,Stringchild)
  设置File对象,obj表示File对象,child表示指定的子目录或文件名
  使用表12。2所列的哪种构造方法要由其他被访问的文件来决定。例如,当在应用程序中只用到一个文件时,使用第1种构造方法最合适;如果使用了一个公共目录下的几个文件,那么使用第2种或第3种构造方法会更方便。
  创建File类的对象后,就可以使用File的相关方法来获取文件信息。接下来,先了解一下File类的常用方法,如表12。3所示。
  表12。3File类常用方法
  常用方法
  功能描述
  备注
  StringgetName()
  获取相关文件名
  与文件名相关的方法
  StringgetPath()
  获取文件路径
  StringgetAbsolutePath()
  获取文件绝对路径
  StringgetParent()
  获取文件上级目录名称
  booleanrenameTo(FilenewName)
  更改文件名,成功则返回true,否则返回false
  booleanexists()
  检测文件对象是否存在
  文件测定相关方法
  booleancanWrite()
  检测文件对象是否可写
  booleancanRead()
  检测文件对象是否可读
  booleanisFile()
  检测文件对象是否是文件
  booleanisDirectory()
  检测文件对象是否是目录
  booleanisAbsolute()
  检测文件对象是否是绝对路径
  longlastModified()
  返回此File对象表示的文件或目录最后一次被修改的时间
  常用文件信息和方法
  longlength()
  返回此File对象表示的文件或目录的长度
  booleandelete()
  删除文件或目录。如果File对象为目录,则该目录为空,方可删除。删除成功,返回true,否则返回false
  booleanmkdir()
  创建File对象指定目录。如果创建成功,则返回true,否则返回false
  目录相关类工具
  booleanmkdirs()
  创建File对象指定的目录,如果此目录的父级不存在,则还会创建父目录。如创建成功,则返回true,否则返回false
  String〔〕list()
  返回此File对象表示的目录中的文件和目录的名称所组成字符串数组
  接下来,通过一个案例来演示File类常用方法的基本使用,先在当前目录创建一个1201。txt文件,在里面输入AAA软件教育欢迎您!,然后编写代码,如例121所示。
  例121Demo1201。java
  1packagecom。aaa。p120201;
  2importjava。io。;
  3importjava。util。;
  4importjava。text。SimpleDateF
  5hr6publicclassDemo1201{
  7publicstaticvoidmain(String〔〕args){
  8FilefilenewFile(src1201。txt);
  9System。out。println(文件是否存在file。exists());
  10System。out。println(文件是否可写file。canWrite());
  11System。out。println(文件是否可读file。canRead());
  12System。out。println(文件是否是文件file。isFile());
  13System。out。println(文件是否是目录file。isDirectory());
  14System。out。println(文件是否是绝对路径file。isAbsolute());
  15System。out。println(文件名是file。getName());
  16System。out。println(文件的路径是file。getPath());
  17System。out。println(文件的绝对路径是file。getAbsolutePath());
  18System。out。println(文件的上级路径是file。getParent());
  19SimpleDateFormatsdfnewSimpleDateFormat(yyyyMMdd);
  20System。out。print(最后修改时间);
  21System。out。println(sdf。format(newDate(file。lastModified())));
  22System。out。println(文件长度是file。length());
  23}
  24}
  程序的运行结果如下:
  文件是否存在true
  文件是否可写true
  文件是否可读true
  文件是否是文件true
  文件是否是目录false
  文件是否是绝对路径false
  文件名是1201。txt
  文件的路径是src1201。txt
  文件的绝对路径是D:workAAA课程研发教材编写javaIOsrc1201。txt
  文件的上级路径是src
  最后修改时间20210615
  文件长度是25
  例121在程序中构造了File类的对象,运用File类的各个方法得到文件的各种相关属性。在第1921行代码中,通过格式化时间信息,获取文件最后修改时间,最后打印文件1201。txt相关属性的信息。
  12。2。2遍历目录下的文件
  File类用来操作文件和获得文件的信息,但是不提供对文件读取的方法,这些方法由文件流提供。File类中提供了list()方法和listFiles()方法,用来遍历目录下所有文件。两者不同之处是list()方法只返回文件名,没有路径信息;而listFiles()方法不但返回文件名称,还包含有路径信息。
  接下来,通过案例来演示list()方法与listFiles()方法的使用,如例122所示。
  例122Demo1202。java
  1packagecom。aaa。p120202;
  2importjava。io。;
  3hr1publicclassDemo1202{
  2publicstaticvoidmain(String〔〕args){
  3System。out。printf(list()方法);
  4FilefilenewFile(D:javaCode);创建File对象
  5if(file。isDirectory()){判断file目录是否存在
  6String〔〕listfile。list();
  7for(StringfileName:list){
  8System。out。println(fileName);打印文件名
  9}
  10}
  11System。out。printf(listFiles()方法);
  12files(file);
  13}
  14publicstaticvoidfiles(Filefile){
  15File〔〕listFilefile。listFiles();遍历目录下所有文件
  16for(Filef:listFile){
  17if(f。isDirectory()){判断是否是目录
  18files(f);递归调用
  19}
  20System。out。println(f。getAbsolutePath());
  21}
  22}
  23}
  程序的运行结果如下:
  list()方法
  chapter02
  test。txt
  listFiles()方法
  D:javaCodechapter02。idea。gitignore
  D:javaCodechapter02。ideamisc。xml
  D:javaCodechapter02。ideamodules。xml
  D:javaCodechapter02。ideauiDesigner。xml
  D:javaCodechapter02。ideaworkspace。xml
  D:javaCodechapter02。idea
  D:javaCodechapter02chapter02。iml
  D:javaCodechapter02outproductionchapter02Demo02。class
  D:javaCodechapter02outproductionchapter02Demo0201。class
  例122中,首先创建File对象,指定File对象的目录。第510行代码先判断file目录是否存在,若存在,则调用list()方法,第6行代码以String数组的形式得到所有文件名,最后循环遍历数组内容并打印。如果目录下仍然有子目录则不能遍历到,此时就需要用到File类的listFiles()方法,遍历目录下所有文件之后,循环判断遍历到的是否是目录,如果是目录,则再次递归调用file(file)方法本身。第1422行代码是自定义的静态方法,直到遍历完到文件。通过程序运行结果可以看到,listFiles()方法输出的信息比list()方法输出的信息更加详细,而且listFiles()方法返回值是File类型,可以直接使用该文件。
  注意:在Windows系统中,目录的分隔符是反斜杠()。但是,在Java语言中,使用反斜杠表示转义字符,所以如果需要在Windows系统的路径下包括反斜杠,则应该使用两条反斜线,如D:javaCode或者直接用斜线()也可以。
  12。2。3删除文件及目录
  在程序设计中,除了遍历文件,文件的删除操作也很常见,Java中通过使用File类的delete()方法来对文件进行删除操作。
  接下来,演示如何删除文件及目录,如例123所示。
  例123Demo1203。java
  1packagecom。aaa。p120203;
  2importjava。io。;
  3hr4publicclassDemo1203{
  5publicstaticvoidmain(String〔〕args){
  6StringpathD:javaCodechapter02
  7deleteD(path);
  8}
  9privatestaticvoiddeleteD(Stringpp){
  10FilefilenewFile(pp);
  11if(file。isFile()){
  12while(file。exists()){
  13System。out。println(删除了文件:file。getName());
  14file。delete();
  15}
  16}else{
  17File〔〕listFilesfile。listFiles();
  18for(Filefile2:listFiles){
  19try{
  20deleteD(file2。getAbsolutePath());
  21}catch(Exceptione){
  22System。out。println(e。getMessage());
  23}
  24}
  25file。delete();
  26}
  27}
  28}
  程序的运行结果如下:
  删除了文件:Demo02。class
  删除了文件:Demo0201。class
  删除了文件:Demo0202。class
  删除了文件:Demo03。class
  删除了文件:Demo05。class
  删除了文件:Demo06。class
  例123中,在main()方法中deleteD(Filefile)方法中将待删除的内容以字符串形式传入,调用方法时创建File对象,然后遍历该目录下所有文件,判断遍历到的是否是目录,如果是目录,继续递归调用方法本身,如果是文件则输出文件信息然后直接删除,删除文件完成后,将目录删除。第1823行代码增加了确保程序健壮性的异常信息处理。
  注意:File类的delete()方法只是删除一个指定的文件,如果目录下还有子目录,是无法直接删除的,需要递归删除。另外,在Java中是直接从虚拟机中将文件或目录删除,可以不经过回收站,文件无法恢复,所以使用delete()操作时要谨慎。
  12。2。4RandomAccessFile类
  Java提供的RandomAccessFile类,允许从文件的任何位置进行数据的读写。它不属于流,是Object类的子类,但它融合了InputStream类和OutStream类的功能,既能提供read()方法和write()方法,还能提供更高级的直接读写各种基本数据类型数据的读写方法,如readInt()方法和writeInt()方法等。
  RandomAccessFile类的中文含义为随机访问文件类,随机意味着不确定性,指的是不需要从头读到尾,可以从文件的任意位置开始访问文件。使用RandomAccessFile类,程序可以直接跳到文件的任意地方读、写文件,既支持只访问文件的部分数据,又支持向已存在的文件追加数据。
  为支持任意读写,RandomAccessFile类将文件内容存储在一个大型的byte数组中。RandomAccessFile类设置指向该隐含的byte数组的索引,称为文件指针,通过从文件开头就开始计算的偏移量来标明当前读写的位置。
  RandomAccessFile类有两个构造方法,其实这两个构造方法基本相同,只是指定文件的形式不同而已,一个使用String参数来指定文件名,一个使用File参数来指定文件本身。具体示例如下:
  访问file参数指定的文件,访问的形式由mode参数指定
  publicRandomAccessFile(Filefile,Stringmode)
  访问name参数指定的文件,访问的形式由mode参数指定
  publicRandomAccessFile(Stringname,Stringmode)
  在创建RandomAccessFile对象时还要设置该对象的访问形式,具体使用一个参数mode进行指定,mode的值及对应的访问形式如表12。4所示。
  表12。4mode的值及含义
  mode值
  含义
  r
  以只读的方式打开,如果试图对该RandomAccessFile执行写入方法,都将抛出IOException异常
  rw
  以读、写方式打开指定文件,如果该文件不存在,则尝试创建该文件
  rws
  以读、写方式打开指定文件,相较于rw模式,还需要对文件的内容或元数据的每个更新都同步写入到底层存储设备
  rwd
  以读、写方式打开指定文件,相较于rw模式,还要求对文件内容的每个更新都同步写入到底层存储设备
  随机访问文件是由字节序列组成,一个称为文件指针的特殊标记定位这些字节中的某个字节的位置,文件的读写操作就是在文件指针所在的位置上进行的。打开文件时,文件指针置于文件的起始位置,在文件中进行读写数据后,文件指针就会移动到下一个数据项。如表12。5所示,列出了RandomAccessFile类所拥有的用来操作文件指针的方法。
  表12。5RandomAccessFile类操作指针的方法
  方法声明
  功能描述
  longgetFilePointer()
  获取当前读写指针所处的位置
  voidseek(longpos)
  指定从文件起始位置开始的指针偏移量,即设置读指针的位置
  intskipBytes(intn)
  使读写指针从当前位置开始,跳过n个字节
  voidsetLength(longnum)
  设置文件长度
  接下来,通过案例演示RandomAccessFile类操作文件指针的方法的使用,如例124所示。
  例124Demo1204。java
  1packagecom。aaa。p1202;
  2importjava。io。;
  3importjava。io。IOE
  4importjava。io。RandomAccessF
  5hr6publicclassDemo1204{
  7publicstaticvoidmain(String〔〕args){
  8FilefilenewFile(d:javaCodetest。txt);
  9RandomAccessF声明RandomAccessFile对象
  10hr11try{
  12rafnewRandomAccessFile(file,rw);
  13for(intn0;n10;n){
  14raf。writeInt(n);
  15}
  16System。out。println(当前指针位置:raf。getFilePointer());
  17System。out。println(文件长度:raf。length()字节);
  18raf。seek(0);返回数据的起始位置
  19hr20System。out。println(当前指针位置:raf。getFilePointer());
  21System。out。println(读取数据);
  22for(intn0;n6;n){
  23System。out。println(数值:raf。readInt()
  24(raf。getFilePointer()4));
  25if(n3)raf。seek(32);指针跳过4567
  26}
  27raf。close();关闭随机访问文件流
  28}catch(IOExceptione){
  29e。printStackTrace();
  30}
  31}
  32}
  程序的运行结果如下:
  第0个值:0
  当前指针位置:40
  文件长度:40字节
  当前指针位置:0
  读取数据
  数值:00
  数值:14
  数值:28
  数值:312
  数值:832
  数值:936
  例124中,先向test。txt文件写入09十个数字,此时文件的长度为10个int字节,数据指针位置为40。如果要读取文件的数据信息,则需要把文件指针移动到文件的起始位置,而执行seek(0)可以达到目的。虽然开始时会循环读取数据,但在i为3时,将指针移动到32,即跳过5、6、7、8,直接开始读取8和9,对应指针位置时32和36。
  12。3字节流
  在前面小节中,我们学习了File类对文件或目录进行操作的方法,但是File类不包含向文件读写数据的方法。为了进一步进行文件输入输出操作,需要使用正确的JavaIO类来创建对象。在程序设计中,程序如果要读取或写入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。4InputStream子类结构图图12。5OutputStream子类结构图
  从图12。4和图12。5中可看出,InputStream和OutputStream的子类虽然较多,但都有规律可循。因为输入流或输出流的数据源或目标的数据格式不同,如字节数组、文件、管道等,所以子类在命名的时候采用的格式是数据类型加抽象基类名。例如,FileInputStream的子类表示从文件中读取信息,InputStream为后缀。此外,InputStream和OutputStream的子类大多都是成对出现的,如数据过滤流FilterInputStream和FilterOutputStream。
  InputStream类定义了输入流的一般方法,是字节输入流的父类,其他字节输入流都是在其基础上做功能上的增强。因此,了解了InputStream类就为了解其他输入流打下了基础,表12。6列出了InputStream类的常用方法。
  表12。6InputStream类的常用方法
  方法声明
  功能描述
  publicintavailable()
  获取输入流中可以不受阻塞地读取的字节数
  publicvoidclose()
  关闭输入流并释放与该流关联的所有系统资源,该方法由子类重写
  publicvoidmark(intreadlimit)
  在此输入流中标记当前的位置,该方法由子类重写
  publicbooleanmarkSupported()
  判断当前输入流是否允许标记。若允许,则返回true,否则返回false
  publiclongskip(longn)
  从输入流中跳过指定n个指定的字节,并返回跳过的字节数
  publicintread()
  从输入流中读取数据的下一个字节
  publicintread(byte〔〕b)
  从输入流中读取一定数量的字节,并将其存储在缓冲区数组b中,返回读取的字节数。如果已经到达末尾,则返回1
  publicintread(byte〔〕b,intoff,intlen)
  将输入流中最多len个数据字节读入byte数组。然后将读取的b数据以int返回。如果已经到达末尾,则返回1
  publicvoidreset()
  将输入流重新定位到最后一次对此输入流设置标记的起始处
  表12。6中列出了InputStream类的方法,上述所有方法都声明抛出IOException异常,因此使用时要注意处理异常。InputStream使用最多的方法为read()和close()方法,前者从已存在的文件中读取字节,在工作做完之后,由后者关闭字节流,释放系统资源,如果不关闭会浪费一定量的系统资源,会导致计算机运行效率下降。read()方法有构成函数重载的3种形式,无参的read()方法可以用来将字节挨个读入,另外两个可以指定一个字节数组作为读取字节的批量,甚至可以通过定义off和len,指定读取字节的起始位置和长度。
  下面我们来看一看OutputStream类,它拥有和InputStream类似的用法和相对的功能方法。
  表12。7OutputStream类的常用方法
  方法声明
  功能描述
  voidclose()
  关闭此输出流,并释放与之有关的所有系统资源,由子类重写该方法
  voidflush()
  刷新此输出流,并强制写出所有缓冲的输出字节
  voidwrite(byte〔〕b)
  将数组b的数据写到输出流
  voidwrite(intb)
  将指定的int字节b写入此输出流
  voidwrite(byte〔〕b,intoff,intlen)
  将指定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软件教育,接着编写程序将文件中的内容读出并打印到控制台,如例125所示。
  例125Demo1205。java
  1packagecom。aaa。p120302;
  2importjava。io。;
  3hr4publicclassDemo1205{
  5publicstaticvoidmain(String〔〕args){
  6FileInputStreamfileI
  7try{
  8fileInputnewFileInputStream(read。txt);创建文件输入流对象
  9intn1024;设定读取的字节数
  10bytebuffer〔〕newbyte〔n〕;
  11读取输入流
  12while((fileInput。read(buffer,0,n)!1)(n0)){
  13System。out。print(newString(buffer));
  14}
  15}catch(Exceptione){
  16System。out。println(e);
  17}finally{
  18if(fileInput!null){
  19try{
  20fileInput。close();释放资源
  21}catch(IOExceptione){
  22e。printStackTrace();
  23}
  24}
  25}
  26}
  27}
  程序的运行结果如下:
  AAA软件教育
  在例125中,我们建立了一个长度为1024的byte数组,将其传入read()方法中,并设置始末位置为0到n,此时read()方法一次读1024个字节。运行之后,我们看到控制台打印出AAA软件教育。
  注意:在例125中,如果程序中途出现错误,程序将直接中断,所以一定要将关闭资源的close()方法写到finally中。另外,由于finally中不能直接访问try中的内容,所以要将FileInputStream定义在try的外面。由于篇幅有限,后面的代码不再重复异常处理的标准写法,直接将异常抛出。
  需要注意的是,当创建文件输入流时,一定要保证目录下有对应文件存在,否则会报FileNotFoundException异常,提示java。io。FileNotFoundException:read。txt(系统找不到指定的文件)。
  明白了FileInputStream的用法,下面我们来看看与之相对的FileOutputStream,它使用字节流向一个文件中写入内容,两者用法相似,如例126所示。
  例126Demo1206。java
  1packagecom。aaa。p120302;
  2importjava。io。;
  3hr4publicclassDemo1206{
  5publicstaticvoidmain(String〔〕args)throwsException{
  6System。out。print(请输入要保存到文件的内容:);
  7intcount,n1024;
  8bytebuffer〔〕newbyte〔n〕;
  9countSystem。in。read(buffer);读取标准输入流
  10创建文件输出流对象
  11FileOutputStreamfileOutputnewFileOutputStream(read。txt);
  12fileOutput。write(buffer,0,count);写入输出流
  13System。out。println(已保存到read。txt!);
  14fileOutput。close();释放资源
  15}
  16}
  程序的运行结果如下:
  请输入要保存到文件的内容:AAA软件欢迎你
  已保存到read。txt!
  例126程序的运行结果显示已保存到read。txt,此时文件内容如下:
  AAA软件欢迎你
  与输入流不同的是,当文件不存在时,输出流会先创建文件再向其中写入内容。当文件已经存在时,会先将原本的内容清空,再向其中写入。例126中执行之后,原本的内容被替换成了新的内容。如果想要保留原来内容,只需要在原来的基础上构造输出流时追加一个Boolean类型的参数,该参数用于指定是否为追加写入,如果为true,就能够在源文件尾部的下一行写入内容了,如例127所示。
  例127Demo1207。java
  1packagecom。aaa。p120302;
  2importjava。io。;
  3hr4publicclassDemo1207{
  5publicstaticvoidmain(String〔〕args)throwsException{
  6System。out。print(请输入要保存到文件的内容:);
  7intcount,n1024;
  8bytebuffer〔〕newbyte〔n〕;
  9countSystem。in。read(buffer);读取标准输入流
  10创建文件输出流对象
  11FileOutputStreamfileOutputnewFileOutputStream(read。txt,true);
  12fileOutput。write(buffer,0,count);写入输出流
  13System。out。println(已保存到read。txt!);
  14fileOutput。close();释放资源
  15}
  16}
  程序的运行结果如下所示:
  请输入要保存到文件的内容:专业的软件培训机构
  已保存到read。txt!
  运行结果显示已保存到read。txt,由于我们是自行创建了read。txt文件,并在例126中重写,而本次运行的结果是将对应内容追加到read。txt中,此时文件内容如下:
  AAA软件欢迎你
  专业的软件培训机构
  通过例127可以看出,构造FileOutputStream时声明append参数为true,即可在原文件基础上写入新内容。
  12。3。3文件拷贝
  前面我们分别讲解了文件输入流和文件输出流的使用,现在我们将二者结合起来,就能够完成更复杂的操作,这也是我们在日常开发中可能使用到的。
  输入流和文件输结合使用可以实现文件的复制,首先我们来做一些准备工作。在当前目录下建立两个文件夹,分别命名为image、target,之后向image目录下存放一张图片,并命名为img。png。然后,开始编写代码,如例128所示。
  例128Demo1208。java
  1packagecom。aaa。p1203;
  2importjava。io。;
  3hr4publicclassDemo1208{
  5publicstaticvoidmain(String〔〕args)throwsException{
  6创建文件输入流对象
  7FileInputStreaminputnewFileInputStream(imageimg。png);
  8创建文件输出流对象
  9FileOutputStreamoutputnewFileOutputStream(targetimg。png);
  10定义len,记录每次读取的字节
  11longbeginSystem。currentTimeMillis();拷贝文件前的系统时间
  12while((leninput。read())!1){读取文件并判断是否到达文件末尾
  13output。write(len);将读到的字节写入文件
  14}
  15longendSystem。currentTimeMillis();拷贝文件后的系统时间
  16System。out。println(拷贝文件耗时:(endbegin)毫秒);
  17output。close();释放资源
  18input。close();
  19}
  20}
  程序的运行结果如下所示:
  拷贝文件耗时:875毫秒
  控制台中打印出了程序拷贝文件所消耗的时间,而图片就在这段时间内由字节流的方式实现了拷贝,如图12。6所示。
  图12。6InputStream子类结构图图12。7OutputStream子类结构图
  由于不同计算机性能不同,或同一个计算机在不同情况下负载不同,拷贝图片所消耗的时间都有可能会有差别,具体时间以现实情况为准。
  注意:在例128中,指定image和target的目录用,这是因为windows系统目录用反斜杠表示,但Java中反斜杠是特殊字符,所以写成指定路径,也可以使用指定目录,如imageimg。png。
  12。3。4字节流的缓冲区
  前文讲解了字节流拷贝文件,还有一种更高效的拷贝方式,那就是在使用中加上缓冲区,缓冲区可以帮助提升字节传输效率。因为,不加缓冲区的时候是一个字节一个字节地传输,而加了缓冲区后则是先将字节填满一个缓冲区,再将整个缓冲区的字节一并传输,这样可以显著降低传输次数,提升传输效率。每次传输都会消耗一定的时间,但是使用缓冲区会在本地占用一定的空间,这属于空间换时间的方式,
  接下来,通过案例来演示缓冲区在字节流拷贝中的用法,如例129所示。
  例129Demo1209。java
  1packagecom。aaa。p120304;
  2importjava。io。;
  3hr4publicclassDemo1209{
  5publicstaticvoidmain(String〔〕args)throwsException{
  6创建文件输入流对象
  7FileInputStreaminputnewFileInputStream(imageimg。png);
  8创建文件输出流对象
  9FileOutputStreamoutputnewFileOutputStream(targetimg。png);
  10byte〔〕bnewbyte〔1024〕;定义缓冲区大小
  11定义len,记录每次读取的字节
  12longbeginSystem。currentTimeMillis();拷贝文件前的系统时间
  13while((leninput。read(b))!1){读取文件并判断是否到达文件末尾
  14output。write(b,0,len);从第1个字节开始,向文件写入len个字节
  15}
  16longendSystem。currentTimeMillis();拷贝文件后的系统时间
  17System。out。println(拷贝文件耗时:(endbegin)毫秒);
  18output。close();释放资源
  19input。close();
  20}
  21}
  程序的运行结果如下:
  拷贝文件耗时:38毫秒
  从例129的运行结果可以看出,与例128相比,拷贝所耗的时间大大地降低了,说明使用缓冲区有效减少了字节流的传输次数,从而提升了程序的运行效率。
  除了上面这种方式,还有一种封装性更好,更易用的方式来使用带缓冲区的IO流,那就是BufferedInputStream和BufferedOutputStream,它们的构造器接收对应的IO流,并返回带缓冲的BufferedInputStream对象和BufferedOutputStream对象,这体现出了装饰设计模式的思想,其接收的参数为装饰对象,返回的类为装饰结果,结构如图12。8所示。
  从图12。8可以看出,在程序和文件之间的核心由节点流传输数据,如我们在之前所讲到的FileInputStream和FileOutputStream。在外层为节点流的封装,如我们现在讲的BufferedInputStream和BufferedOutputStream。
  接下来,我们通过案例来演示缓冲流的使用,如例1210所示。
  例1210Demo1210。java
  1packagecom。aaa。p120304;
  2importjava。io。;
  3hr4publicclassDemo1210{
  5publicstaticvoidmain(String〔〕args)throwsException{
  6创建文件输入流对象
  7FileInputStreamfInputnewFileInputStream(imageimg。png);
  8创建文件输出流对象
  9FileOutputStreamfOutputnewFileOutputStream(targetimg。png);
  10将创建的节点流的对象作为形参传递给缓冲流的构造方法中
  11BufferedInputStreambInputnewBufferedInputStream(fInput);
  12BufferedOutputStreambOutputnewBufferedOutputStream(fOutput);
  13定义len,记录每次读取的字节
  14longbeginSystem。currentTimeMillis();拷贝文件前的系统时间
  15while((lenbInput。read())!1){读取文件并判断是否到达文件末尾
  16bOutput。write(len);将读到的字节写入文件
  17}
  18longendSystem。currentTimeMillis();拷贝文件后的系统时间
  19System。out。println(拷贝文件耗时:(endbegin)毫秒);
  20bInput。close();
  21bOutput。close();
  22}
  23}
  程序的运行结果如下:
  拷贝文件耗时:51毫秒
  例1210的运行结果如上所示,拷贝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。9Reader子类结构图图12。10Writer子类结构图
  可以看出,字符流与字节流相似,也是很有规律的,这些子类都是以它们的抽象基类为结尾命名的,并且大多以Reader和Writer结尾,如CharArrayReader和CharArrayWriter。接下来,我们详细讲解字符流的使用。
  12。4。2操作文件
  Reader和Writer有众多子类,其中FileReader和FileWriter是两个很常用的子类,FileReader类是用来从文件中读取字符的,操作文件的字符输入流。
  接下来,通过案例来演示如何从文件中读取字符。首先在当前目录新建一个文本文件read。txt,文件内容如下:
  AAA软件教育
  fileReader
  创建文件完成后,开始编写代码,如例1211所示。
  例1211Demo1211。java
  1packagecom。aaa。p120402;
  1importjava。io。;
  2hr3publicclassDemo1211{
  4publicstaticvoidmain(String〔〕args)throwsException{
  5FilefilenewFile(read。txt);
  6FileReaderfileReadernewFileReader(file);
  7定义len,记录读取的字符
  8while((lenfileReader。read())!1){判断是否读取到文件的末尾
  9System。out。print((char)len);打印文件内容
  10}
  11fileReader。close();释放资源
  12}
  13}
  程序的运行结果如下:
  AAA软件教育
  fileReader
  例1211中,首先定义一个文件字符输入流,然后在创建输入流实例时,将文件以参数传入,读取到文件后,用变量len记录读取的字符,然后循环输出。这里要注意len是int类型,所以输出时要强转类型,第10行中将len强转为char类型。
  与FileReader类对应的是FileWriter类,它是用来将字符写入文件,操作文件字符输出流的。
  接下来,通过案例来演示如何将字符写入文件,如例1212所示。
  例1212Demo1212。java
  1packagecom。aaa。p120402;
  2importjava。io。;
  3hr4publicclassDemo1212{
  5publicstaticvoidmain(String〔〕args)throwsException{
  6FilefilenewFile(read。txt);
  7FileWriterfileWriternewFileWriter(file);
  8fileWriter。write(AAA软件专业的Java学习平台);写入文件的内容
  9System。out。println(已保存到read。txt!);
  10fileWriter。close();释放资源
  11}
  12}
  程序的运行结果如下:
  已保存到read。txt!
  例1212运行结果显示已保存到read。txt文件,文件内容如下:
  AAA软件专业的Java学习平台
  FileWriter与FileOutputStream类似,如果指定的目标文件不存在,则先新建文件,再写入内容,如果文件存在,会先清空文件内容,然后写入新内容,但是结尾不加换行符。如果想在文件内容的末尾追加内容,则需要调用构造方法FileWriter(StringFileName,booleanappend)来创建文件字符输出流对象,将参数append指定为true即可,将例1212第5行代码修改如下:
  FileWriterfileWriternewFileWriter(file,true);
  再次运行程序,输出流会将字符追加到文件内容的末尾,不会清除文件本身的内容,结尾同样是没有换行符的。
  12。4。3转换流
  前文分别讲解了字节流和字符流,有时字节流和字符流之间可能也需要进行转换,在JDK中提供了可以将字节流转换为字符流的两个类,分别是InputStreamReader类和OutputStreamWriter类,它们被称之为转换流。其中,OutputStreamWriter类可以将一个字节输出流转换成字符输出流,而InputStreamReade类可以将一个字节输入流转换成字符输入流。转换流的出现方便了对文件的读写,它在字符流与字节流之间架起了一座桥梁,使原本没有关联的两种流的操作能够进行转换,提高了程序的灵活性。通过转换流进行读写数据的过程,如图12。11所示。
  图12。11转换流示意图
  图12。11中,程序向文件写入数据时将输出的字符流转变为字节流,程序从文件读取数据时将输入的字节流变为字符流,有效地提高了读写效率。
  接下来,通过案例来演示转换流的使用。首先在当前目录新建一个文本文件Conversion。txt,文件内容为AAA软件教育。创建文件完成后,开始编写代码,如例1213所示。
  例1213Demo1213java
  1packagecom。aaa。p120403;
  2importjava。io。;
  3hr4publicclassDemo1213{
  5publicstaticvoidmain(String〔〕args)throwsIOException{
  6创建字节输入流
  7FileInputStreaminputnewFileInputStream(Conversion。txt);
  8将字节输入流转换为字符输入流
  9InputStreamReaderinputReadernewInputStreamReader(input);
  10创建字节输出流
  11FileOutputStreamoutputnewFileOutputStream(target。txt);
  12将字节输出流转换成字符输出流
  13OutputStreamWriteroutputWriternewOutputStreamWriter(output);
  14
  15while((strinputReader。read())!1){
  16outputWriter。write(str);
  17}
  18outputWriter。close();
  19inputReader。close();
  20}
  21}
  例1214程序运行结束后,会在当前目录生成一个target。txt文件,如图12。12和图12。13所示。
  图12。12文件拷贝前图12。13文件拷贝后
  在例1213中实现了字节流与字符流之间的互相转换,将字节流转换为字符流,从而实现直接对字符的读写。这里要注意,如果用字符流操作非文本文件,如操作视频文件,很有可能会造成部分数据丢失。
  12。5对象序列化方式
  Java提供了一种对象序列化的机制,该机制中一个对象可以被表示为一个字节序列,该字节序列包括该对象的数据、对象的类型和存储在对象中的数据的类型。将序列化对象写入文件之后,可以从文件中读取出来,并且对它进行反序列化。也就是说,对象的类型信息、对象的数据,还有对象中的数据类型可以用来在内存中新建对象。上述整个过程都是Java虚拟机(JVM)独立完成的,这样在一个平台上序列化的对象可以在另一个完全不同的平台上反序列化该对象。
  12。5。1对象序列化概述
  序列化机制可以将实现序列化的Java对象转换成字节序列,而这些字节序列可以保存在磁盘上,或者通过网络传输,以备以后重新恢复成原来的对象继续使用。序列化机制可以使Java对象脱离程序的运行而独立存在。
  对象的序列化(Serialize)是指将一个Java对象写入IO流中,与此对应,对象的反序列化(Deserialize)则是指从IO流中恢复该Java对象。
  如果需要让某个对象支持序列化机制,则必须让它的类是可序列化的(Serializable)。为了让某个类是可序列化的,该类就需要实现Serializable或者Externalizable这两个接口之一,一般推荐使用Serializable接口,因为Serializable接口只需实现不需要重写任何方法,使用起来较为简单。
  Java的很多类其实已经实现了Serializable,该接口是一个标记接口,实现该接口时无须实现任何方法,它只是表明该类的实例是可序列化的。所有可能在网络上传输的对象的类都必须是可序列化的,否则程序可能会出现异常,如RMI(RemoteMethodInvoke,即远程方法调用,是JavaEE的基础)过程中的参数和返回值。所有需要保存到磁盘里的对象的类都必须可序列化,如Web应用中需要保存到HttpSession或ServletContext属性的Java对象。
  因为序列化是RMI过程的参数和返回值都必须实现的机制,而RMI又是JavaEE技术的基础,所有的分布式应用常常需要跨平台、跨网络,所以要求所有传递的参数、返回值必须实现序列化。因此,序列化机制是JavaEE平台的基础,通常建议程序创建的每个JavaBean类都实现Serializable接口。
  12。5。2如何实现对象序列化的持久化
  如果需要将某个对象保存到磁盘上或者通过网络传输,那么这个类就需要实现Serializable接口或者Extermalizable接口之一。
  使用Serializable来实现序列化非常简单,主要让目标类实现Serializable接口即可,无须实现任何方法。一旦某个类实现了Serializable接口,该类的对象就是可序列化的,程序可以通过如下两个步骤来序列化该对象:
  创建一个ObjectOutputStream,这个输出流是一个处理流,所以必须建立在其他节点流的基础之上,代码如下:
  创建个ObjectOutputStreamn输出流
  FileOutputStreamfosnewFileOutputStream(person。txt);
  ObjectOutputStreamoosnewObjectOutputStream(fos);
  调用ObjectOutputStream对象的writeObject()方法输出可序列化对象,代码如下:
  将一个Person对象输出到输出流中
  oos。writeObject(person);
  下面的程序定义了一个Person类,这个类就是一个普通的Java类,只是实现了Serializable接口,该接口代表该类的对象是可序列化的,代码如下:
  1importjava。io。S
  2hr3publicclassPersonimplementsSerializable{
  4privateS
  5privateI
  6publicPerson(Stringname,Integerage){
  7this。
  8this。
  9}
  10publicStringgetName(){
  11
  12}
  13publicvoidsetName(Stringname){
  14this。
  15}
  16publicIntegergetAge(){
  17
  18}
  19publicvoidsetAge(Integerage){
  20this。
  21}
  22Override
  23publicStringtoString(){
  24returnPerson{namename,ageage};
  25}
  26}
  接下来,通过案例来演示使用ObjectOutputStream将一个Person对象写入磁盘文件,如例1214所示。
  例1214Demo1214。java
  1packagecom。aaa。p120502;
  2hr3publicclassDemo1214{
  4publicstaticvoidmain(String〔〕args){
  5try(FileOutputStreamfosnewFileOutputStream(person。txt);
  6ObjectOutputStreamoosnewObjectOutputStream(fos)){
  7PersonpersonnewPerson(小乔,18);
  8oos。writeObject(person);
  9}catch(IOExceptione){
  10e。printStackTrace();
  11}
  12}
  13}
  例1214中,第6行代码创建了一个ObjectOutputStream输出流,这个ObjectOutputStream输出流建立在一个文件输出流的基础之上,第8行代码使用writeObject()方法将一个Person对象写入输出流。运行这段代码,将会看到生成了一个Person。txt文件,该文件的内容就是Person对象。
  如果想从二进制流中恢复Java对象,则需要使用反序列化。反序化的的步骤如下:
  创建一个ObjectInputStream输入流,这个输入流是个处理流,所以必须建立在其他节点流的基础之上,代码如下:
  创建一个ObjectInputStream输入流
  FileInputStreamfisnewFileInputStream(person。txt);
  ObjectInputStreamoisnewObjectInputStream(fis);
  调用ObjectInputStream对象的readObject()方法读取流中的对象,该方法返回一个Object类型的Java对象,如果程序知道该Java对象的类型,则可以将该对象强制类型转换成其真实的类型,代码如下:
  从输入流中读取一个Java对象,并将其强制类型转换为Person类
  Personperson(Person)ois。readObject();
  接下来,通过案例来演示从刚刚生成的person。txt文件中读取Person对象,如例1215所示。
  例1215Demo1215。java
  1packagecom。aaa。p120502;
  2hr3publicclassDemo1215{
  4publicstaticvoidmain(String〔〕args){
  5try(FileInputStreamfisnewFileInputStream(person。txt);
  6ObjectInputStreamoisnewObjectInputStream(fis)){
  7Personperson(Person)ois。readObject();
  8System。out。println(person);
  9}catch(Exceptione){
  10e。printStackTrace();
  11}
  12}
  13}
  例1215中,第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类都是不可序列化的。代码如下:
  1publicclassTeacherimplementsSerializable{
  2privateS
  3privateS
  4publicTeacher(Stringname,Studentstudent){
  5this。
  6this。
  7}
  8publicStringgetName(){
  9
  10}
  11publicvoidsetName(Stringname){
  12this。
  13}
  14publicStudentgetStudent(){
  15
  16}
  17publicvoidsetStudent(Studentstudent){
  18this。
  19}
  20Override
  21publicStringtoString(){
  22returnTeacher{namename,studentstudent};
  23}
  24}
  25hr26classStudentimplementsSerializable{
  27privateS
  28privateI
  29publicStudent(Stringname,Integerage){
  30this。
  31this。
  32}
  33publicStringgetName(){
  34
  35}
  36publicvoidsetName(Stringname){
  37this。
  38}
  39publicIntegergetAge(){
  40
  41}
  42publicvoidsetAge(Integerage){
  43this。
  44}
  45Override
  46publicStringtoString(){
  47returnStudent{namename,ageage};
  48}
  49}
  注意:当程序序列化一个Teacher对象时,如果该Teacher对象持有一个Student对象的引用,为了在反序列化时可以正常恢复该Teacher对象,程序会顺带将该Student对象也进行序列化,所以Student类也必须是可序列化的,否则Teacher类将不可序列化。
  现在假设有如下特殊情形:程序中有两个Teacher对象,它们的student实例变量都引用同一个Student对象,而且该Student对象还有一个引用变量引用它,代码如下:
  StudentstudentnewStudent(小乔,18);
  Teacherteacher1newTeacher(周瑜,student);
  Teacherteacher2newTeacher(曹操,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对象,如例1216所示。
  例1216Demo1216。java
  1packagecom。aaa。p120503;
  2hr3publicclassDemo1216{
  4publicstaticvoidmain(String〔〕args){
  5try(FileOutputStreamfosnewFileOutputStream(teacher。txt);
  6ObjectOutputStreamoosnewObjectOutputStream(fos)){
  7StudentstudentnewStudent(小乔,18);
  8Teacherteacher1newTeacher(周瑜,student);
  9Teacherteacher2newTeacher(曹操,student);
  10oos。writeObject(teacher1);
  11oos。writeObject(teacher2);
  12oos。writeObject(student);
  13oos。writeObject(teacher2);
  14}catch(Exceptione){
  15e。printStackTrace();
  16}
  17}
  18}
  例1216中,4次调用了writeObject0方法来输出对象,实际上只序列化了3个对象,而且序列的两个Teacher对象的student引用实际是同一个Student对象。
  接下来,通过案例来演示读取序列化文件中的对象,如例1217所示。
  例1217Demo1217。java
  1packagecom。aaa。p120503;
  2hr3publicclassDemo1217{
  4publicstaticvoidmain(String〔〕args){
  5try(FileInputStreamfisnewFileInputStream(teacher。txt);
  6ObjectInputStreamoisnewObjectInputStream(fis)){
  7Teachert1(Teacher)ois。readObject();
  8Teachert2(Teacher)ois。readObject();
  9Students(Student)ois。readObject();
  10Teachert3(Teacher)ois。readObject();
  11System。out。println(t1的student引用和s是不是相同对象:
  12(t1。getStudent()s));
  13System。out。println(t2的student引用和s是不是相同对象:
  14(t2。getStudent()s));
  15System。out。println(t2和t3是不是相同对象:(t2t3));
  16}catch(Exceptione){
  17e。printStackTrace();
  18}
  19}
  20}
  程序运行结果如下:
  t1的student引用和s是不是相同对象:true
  t2的student引用和s是不是相同对象:true
  t2和t3是不是相同对象:true
  例1217中,代码依次读取了序列化文件中的4个Java对象,但通过后面的比较判断,不难发现t2和t3是同一个Java对象,tl、t2和s的引用变量引用的也是同一个Java对象,这证明了图12。15所示的序列化机制。
  根据Java序列化机制,如果多次序列化同一个Java对象时,只有第1次序列化时才会把该Java对象转换成字节序列并输出,这样也可能会引发一个潜在的问题,即当程序序列化一个可变对象时,只有第1次使用writeObject()方法输出时才会将该对象转换成字节序列并输出,当程序再次调用writeObject()方法时,程序只是输出前面的序列化编号,即使后面该对象的实例变量值已被改变,改变的实例变量值也不会被输出,如例1218所示。
  例1218Demo1218。java
  1packagecom。aaa。p120503;
  2hr3publicclassDemo1218{
  4publicstaticvoidmain(String〔〕args){
  5try(FileOutputStreamfosnewFileOutputStream(teacher。txt);
  6ObjectOutputStreamoosnewObjectOutputStream(fos);
  7FileInputStreamfisnewFileInputStream(teacher。txt);
  8ObjectInputStreamoisnewObjectInputStream(fis)){
  9Studentstudent1newStudent(小乔,18);
  10oos。writeObject(student1);
  11student1。setName(大乔);
  12System。out。println(修改name后:student1);
  13oos。writeObject(student1);
  14Students2(Student)ois。readObject();
  15Students3(Student)ois。readObject();
  16System。out。println(s2与s3进行对比:(s2s3));
  17System。out。println(s2反序列化后:s2);
  18System。out。println(s3反序列化后:s3);
  19}catch(Exceptione){
  20e。printStackTrace();
  21}
  22}
  23}
  程序的运行结果如下:
  修改name后:Student{name大乔,age18}
  s2与s3进行对比:true
  s2反序列化后:Student{name小乔,age18}
  s3反序列化后:Student{name小乔,age18}
  例1218中,先使用writeObject()方法写入了一个Student对象,接着改变了Student对象的实例变量name的值,然后程序再次序列化输出Student对象,但这次不会将Student对象转换成字节序列输出了,而是仅输出了一个序列化编号。第14行和第15行的代码两次调用readObject()方法读取了序列化文件中的Java对象,比较两次读取的Java对象结果为true,证明是同一对象。然后,程序再次输出两个对象,两个对象的name值依然是小乔,表明改变后的Student对象并没有被写入,这与Java序列化机制相符。
  注意:当使用Java序列化机制去序列化可变对象时一定要注意,只有第一次调用writeObject()方法来输出对象时才会将对象转换成字节序列,并写入到ObjectOutputStream。在后面程序中,即使该对象的实例变量发生了改变,再次调用writeObject()方法输出该对象时,改变后的实例变量也不会被输出。
  12。6本章小结
  JavaIO系统负责处理程序的输入和输出,IO类库位于java。io包中,它对各种常见的输入流和输出流进行了抽象。
  通过调用File类提供的各种方法,能够完成创建文件、删除文件、重命名文件、判断文件的读写权限以及文件是否存在、设置和查询文件的创建时间和权限等操作。
  根据数据的读取或写入分类,流可以分为输入流和输出流。
  根操作对象分类,流可以分为字节流和字符流。字节流可以处理所有类型数据,如图片、MP3、AVI视频文件等,而字符流只能处理字符数据。只要是处理纯文本数据,就要优先考虑使用字符流,除此之外都用字节流。
  Java的序列化机制可以将实现序列化的Java对象转换成字节序列,而这些字节序列可以保存在磁盘上,或者通过网络传输,以备以后重新恢复成原来的对象继续使用。
  Java的反序列化机制是客户端从文件中或网络上获得序列化后的对象字节流后,根据字节流中所保存的对象状态及描述信息,通过反序列化重建对象。
  12。7理论试题与实践练习
  1。填空题
  1。1关于文件的类都放在包下面。
  1。2字节流有两个抽象基类和,分别处理字节流的输入和输出。
  2。选择题
  2。1下面哪个流类属于面向字符的输入流()
  ABufferedWriterBFileInputStream
  CObjectInputStreamDInputStreamReader
  2。2新建一个流对象,下面哪个选项的代码是错误的()
  AnewBufferedWriter(newFileWriter(a。txt));
  BnewBufferedReader(newFileInputStream(a。dat));
  CnewGZIPOutputStream(newFileOutputStream(a。zip));
  DnewObjectInputStream(newFileInputStream(a。dat));
  2。3要从文件file。dat中读出第10个字节到变量c中,下列哪个方法适合()
  AFileInputStreaminnewFileInputStream(file。dat);in。skip(9);intcin。read();
  BFileInputStreaminnewFileInputStream(file。dat);in。skip(10);intcin。read();
  CFileInputStreaminnewFileInputStream(file。dat);intcin。read();
  DRandomAccessFileinnewRandomAccessFile(file。dat);in。skip(9);
  2。4JavaIO程序设计中,下列描述正确的是()
  AOutputStream用于写操作BInputStream用于写操作
  C只有字节流可以进行读操作DIO库不支持对文件可读可写API
  2。5下列哪个不是合法的字符编码()
  AUTF8BISO88591
  CGBLDASCII
  3。思考题
  3。1请简述Java中有几种类型的流?
  4。编程题
  4。1编写一个程序,要求用户输入一个路径,运行程序列出路径下的所有文件。
  4。2编写一个程序实现文件拷贝功能,要求用户输入源文件路径和目标文件路径,运行程序实现文件拷贝。
  4。3编写一个程序,产生50个19999之间的随机数,然后利用BufferedWriter类将其写入到文件file。txt中,之后再读取这些数字,并进行升序排列。
投诉 评论 转载

iPhone14就要上市了,还有哪些苹果手机更值得买入iPhone14预计将在9月份上市,价格也是可以想见的,如果不差钱的可以直接冲。但是个人觉得最正确的买法就是直接去买上一代的机皇。因为苹果手机每年都会上新机,所以每年的进……2022马来西亚大师赛国羽半决赛战绩3胜2负2022马来西亚大师赛,国羽半决赛战绩3胜2负。其中男单陆光祖和男双梁王组合,分别不敌瓦多约和阿山组合,无缘决赛。胜场中,雅思经过3局苦战,艰难战胜中国台北组合杨博轩胡绫芳,决……JAVA输入输出流详细讲解应用程序经常需要访问文件和目录,读取文件信息或写入信息到文件,即从外界输入数据或者向外界传输数据,这些数据可以保存在磁盘文件、内存或其他程序中。在Java中,对这些数据的操作是……4消息!辽篮送福利,小高交易叫停,浙江新星暴走,黄明依潜力大根据今天的消息,辽宁男篮会在明天的时候送出福利。因为今天他们众多球员会出现在一个活动现场,然后进行球迷见面会。甚至各个社交网站都会进行直播,这也算是大福利了。估计到时候现场肯定……多少钱才能跻身财富金字塔的前1,2万登顶肯尼亚,6万笑傲印度2000年2020年,全球资产从440万亿美元增长到1540万亿美元,而净资产从160万亿美元增长到510万亿美元。财富的总量增加了,但同时也更加集中了。极少数人掌握了大……老话说头伏萝卜二伏菜,三伏还能种荞麦,是什么意思?老话说:头伏萝卜二伏菜,三伏还能种荞麦,是什么意思?俗言道:寒有三九,热有三伏。每年7月份的时候,就开始进入三伏天了。三伏天是一年中气温最高且又潮湿闷热的时间段,而且它分……互联网营销师,是一个新职业,也是一个新未来2019年电商直播被推上了风口,仅‘双11’期间淘宝直播成交就达到了200亿元,有10名互联网营销师引导销售超过10亿元,100名互联网营销师引导销售超过千万元。这一连串数字,……快来这里耍水消暑,拥抱凉意夏天夏天是阳光灿烂的也是暑气熏蒸的但是水磨沟的夏季日均温度仅23C这样的避暑胜地值得您到此一游!马尾瀑灵秀温婉马尾瀑布山水相依人间仙境……期待,中国短道速滑队传来两大好消息,有望力压韩国成为顶级强队近日,在北京冬奥会上为中国奥运代表队夺得首金的短道速滑队传来了两个大好消息。这两大好消息让广大冰迷们感到振奋,有这两大好消息加持,中国短道速滑有望在未来力压韩国,真正成为世界顶……李立群扬言要死守宝岛,引发大规模脱粉浪潮,演艺事业恐将受影响日前,老戏骨李立群在接受台媒专访时,在聊到两岸问题的时候,表示自己是最爱台湾的人,还扬言要死守宝岛,反对武力攻台,甚至还回呛网友的谩骂,否认自己是墙头草,只想守护好家乡。……幸福到万家从万传家不打自招,才懂何幸福全力以赴为水尾村《幸福到万家》剧照文:青源阳阳《幸福到万家》热播,何幸福协助水尾村发现真相。《幸福到万家》正在热播,这部剧由赵丽颖,刘威,唐曾,罗晋,曹征,张可盈等主演。当水……破冰行动里让人感到遗憾的四个人夏日鉴片官《破冰行动》这部电视剧在2019年播出后,先后获得了第三届中国银川互联网电影节网络剧单元最佳网络剧奖;第26届上海电视节白玉兰最佳中国电视剧奖和第30届中国电视金鹰奖……
金价继续大跌!2022年9月1日各大金店黄金价格多少钱一克?韩国半导体设备材料厂商收购DDI芯片初创公司D2I搭载新一代骁龙的轻薄旗舰OPPOReno9Pro初体验小米13发布前的小插曲,雷军没让用户失望,看来库克得来拜师学2022互联网医院行业报告质与量齐驱,行业规范发展加速破产就完事了?广菲克人走茶凉,经销商车主怎么办?爽游白云云小尚尚小云,邀您游白云之七动在白云间排超全明星投票结果出炉!李盈莹成为票王,两位少帅意外落选孕期发现胎儿畸形,姐姐为啥要力排众议保住孩子?谷歌铁了心要撑乱港分子,特区立法机构发出最后通牒,准备出狠招4款目前公认无差评的手机,实际体验表现出色,值得闭眼入手赵阳副教授Small多级微裂纹结构电极助力高性能柔性水系锌电

友情链接:中准网聚热点快百科快传网快生活快软网快好知文好找江西南阳嘉兴昆明铜陵滨州广东西昌常德梅州兰州阳江运城金华广西萍乡大理重庆诸暨泉州安庆南充武汉辽宁