Java复习——I/O与序列化

Posted OverZeal

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Java复习——I/O与序列化相关的知识,希望对你有一定的参考价值。

File类

java.io.File只用于表示文件(目录)的信息(名称、大小等),不能用于文件内容的访问,我们可以通过通过给其构造函数传一个路径来构建以文件,传入的路径名有一个小问题,就是Windows和UNIX 中的路径分隔符斜杠方向的问题:"/" 表示 UNIX 中的根目录,"\\" 表示Windows 的根目录。File类有静态的参数可以很简单的解决这个问题:pathSeparator 就是其中一个。File类中常用的方法有:

createNewFile():在指定目录下创建文件,如果该文件已存在,则不创建。

注意:对于操作文件的输出流而言,输出流对象已建立,就会创建文件,如果文件已存在,会覆盖。除非续写,这是流创建文件与File类这个方法的不同

exists():测试此抽象路径名表示的文件或目录是否存在

isDirectory()测试此抽象路径名表示的文件是否是一个目录

isFile():测试此抽象路径名表示的文件是否是一个标准文件

mkdir():创建此抽象路径名指定的目录

mkdirs():创建此抽象路径名指定的目录,包括所有必需但不存在的父目录

listFiles()返回一个抽象路径名数组,这些路径名表示此抽象路径名表示的目录中的文件

还有一些方法,我们需要的时候查API就好了

一个遍历读取文件名例子:(用于练习File类的API)

import java.io.File;
import org.junit.Test;

public class FileTest {
    
    /**
     * 遍历读取文件名
     * @param path
     */
    private void listFile(File file){
        
        if(!file.exists()){
            throw new IllegalArgumentException("文件或目录不存在");
        }
        
        File[] listFile=file.listFiles();
        for (File file2 : listFile) {
            if(file2.isDirectory()){  //如果文件时目录则进行递归
                listFile(file2);
            }else{
                System.out.println(file2.getPath());
            }
        }
    }
    
    @Test
    public void fun1(){
        listFile(new File("D:"+File.separator+"Java"));
    }
}

 

认识文本和文本文件

java的文本(char)是16位无符号整数,是字符的unicode编码(双字节编码),文件是以byte byte byte ...的数据序列存储的,而文本文件是文本(char)序列按照某种编码方案(utf-8,utf-16be,gbk)序列化为byte的存储结果,所以就会引出一个乱码的问题,当写入文件的文本编码和读出文本的编码不一致时,就会出现乱码问题,解决的方法就是将这两个方式使用的编码统一起来。

一点小知识:

UTF-8中的一个中文字符需要3个字节数来表示,一个英文字符是1个字节表示。

GBK中的一个中文字符需要2个字节数来表示,一个英文字符是1个字节表示。

 

I/O

I/O操作主要是依靠流来进行操作,流可以理解成数据的流动,我们可以把流进行分类:

  1. 输入流和输出流(输入和输出都是相对于内存而说的,加载到内存就是输入流,从内存中加载出来就是输出流)
  2. 字节流和字符流

所有操作I/O的类都在java.io包中。流的操作只有两种:读和写

流的体系因为功能不同,但是有共性内容,不断抽取,形成继承体系。该体系一共有四个基类,而且都是抽象类。

字节流:InputStream  OutputStream

字符流:Reader  Writer

在这四个系统中,它们的子类,都有一个共性特点:子类名后缀都是父类名,前缀名都是这个子类的功能名称。

字节流:处理字节数据的流对象。设备上的数据无论是图片或者dvd,文字,它们都以二进制存储的。二进制的最终都是以一个8位为数据单元进行体现,所以计算机中的最小数据单元就是字节。意味着,字节流可以处理设备上的所有数据,所以字节流一样可以处理字符数据。

那么为什么要有字符流呢?因为字符每个国家都不一样,所以涉及到了字符编码问题,那么GBK编码的中文用unicode编码解析是有问题的,所以需要获取中文字节数据的同时+ 指定的编码表才可以解析正确数据。为了方便于文字的解析,所以将字节流和编码表封装成对象,这个对象就是字符流。只要操作字符数据,优先考虑使用字符流体系。

字符流的底层实现一定是使用字节流实现的。

 

close()和flush()的区别:

flush():将缓冲区的数据刷到目的地中后,流可以使用。

close():将缓冲区的数据刷到目的地中后,流就关闭了,该方法主要用于结束调用的底层资源。这个动作一定做。

 

注意:io异常的处理方式:io一定要写finally,我们将关闭流的操作放在finally中

 

IO中的使用到了一个设计模式装饰设计模式

装饰设计模式解决:对一组类进行功能的增强。

包装:写一个类(包装类)对被包装对象进行包装;

  1. 包装类和被包装对象要实现同样的接口;
  2. 包装类要持有一个被包装对象;
  3. 包装类在实现接口时,大部分方法是靠调用被包装对象来实现的,对于需要修改的方法我们自己实现;

所以我们可以看到I/O体系中类很多,比较学院派,设计的很优雅

 

字节流

输入流:

InputStream是表示字节输入流的所有类的超类。

FileInputStream从文件系统中的某个文件中获得输入字节。哪些文件可用取决于主机环境。FileInputStream 用于读取诸如图像数据之类的原始字节流。要读取字符流,请考虑使用 FileReader。

BufferedInputStream该类实现缓冲的输入流。

常用方法:

  • read()从此输入流中读取一个数据字节(这个没有参数的方法默认一次只读一字节)
  • read(byte[] b)从此输入流中将最多 b.length个字节的数据读入一个 byte 数组中
  • read(byte[] b,int off,int len)从此输入流中将最多 len 个字节的数据读入一个 byte 数组中。

这些read方法如果到达流末尾,则返回 -1。

还可以看到有的read方法中有一个byte数组,这就是需要我们自定义一个缓冲区

强烈建议读取数据的时候使用自定义缓冲区(不论是使用字节流还是字符流),可以明显提升速度。

 

输出流:

OutputStream此抽象类是表示输出字节流的所有类的超类。

FileOutputStream文件输出流是用于将数据写入 File 或 FileDescriptor 的输出流。

BufferedOutputStream该类实现缓冲的输出流。

常用方法:

  • write(int b)将指定字节写入此文件输出流。
  • write(byte[] b)b.length 个字节从指定 byte 数组写入此文件输出流中。
  • write(byte[] b,int off,int len)将指定 byte 数组中从偏移量 off 开始的 len 个字节写入此文件输出流。

同样写的方法也有一个byte数组,需要写的数据是存在byte数组中的

 

需求:复制文件

不使用自定义缓冲区:

/**
     * @param srcFile
     * @param destFile
     * @throws IOException
     * 最原始一次读一字节
     */
    public static void copyFile(File srcFile,File destFile){
        FileInputStream in=null;
        FileOutputStream out=null;
        try {
            if(!srcFile.exists()){
                throw new IllegalArgumentException(srcFile+"不存在");
            }
            if(!srcFile.isFile()){
                throw new IllegalArgumentException(srcFile+"不是文件");
            }
            in = new FileInputStream(srcFile);
            out = new FileOutputStream(destFile);
            int b ;
            while((b = in.read())!=-1){
                out.write(b);
                out.flush();
            }
        } catch (Exception e) {
            e.printStackTrace();
        }finally {
            try {
                if(in!=null){
                    in.close();
                }
                
                if(out!=null){
                    out.close();
                }
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
        
    }

 

使用自定义缓冲区:

/**
     * @param srcFile
     * @param destFile
     * @throws IOException
     */
    public static void copyFileByByte(File srcFile,File destFile){
        FileInputStream in=null;
        FileOutputStream out=null;
        try {
            if(!srcFile.exists()){
                throw new IllegalArgumentException("文件:"+srcFile+"不存在");
            }
            if(!srcFile.isFile()){
                throw new IllegalArgumentException(srcFile+"不是一个文件");
            }
            in = new FileInputStream(srcFile);
            out = new FileOutputStream(destFile);
            int c ;
            byte[] buff=new byte[6*1024];        //自定义一个缓冲区6K
            while((c = in.read(buff,0,buff.length))!=-1){
                out.write(buff,0,c);
                out.flush();
            }
        } catch (Exception e) {
            e.printStackTrace();
        }finally {
            try {
                if(in!=null){
                    in.close();
                }
                
                if(out!=null){
                    out.close();
                }
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }

 

我们还可以使用Buffer打头的字节流,它内部有一个默认大小的缓存区:

/**
     * @param srcFile
     * @param destFile
     * @throws IOException
     */
    public static void copyFileByBuffer(File srcFile,File destFile){
        BufferedInputStream bis=null;
        BufferedOutputStream bos=null;
        try {
            if(!srcFile.exists()){
                throw new IllegalArgumentException("文件:"+srcFile+"不存在");
            }
            if(!srcFile.isFile()){
                throw new IllegalArgumentException(srcFile+"不是一个文件");
            }
            bis = new BufferedInputStream(
                    new FileInputStream(srcFile));
            bos = new BufferedOutputStream(
                    new FileOutputStream(destFile));
            int c ;
            byte[] bytes=new byte[6*1024]; //改造(添加一个byte数组,使read一次可以读bytes.length大小的数据,明显提升速度)
            while((c = bis.read(bytes,0,bytes.length))!=-1){
                bos.write(bytes,0,c);
                bos.flush();
            }
        } catch (Exception e) {
            e.printStackTrace();
        }finally {
            try {
                if(bis!=null){
                    bis.close();
                }
                if(bos!=null){
                    bos.close();
                }
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }

注意:缓冲区对象建立时,必须要先有流对象。明确要提高具体的流对象的效率。从BufferedInputStream 的构造方法可以看到

 

字符流

输入流:

Reader用于读取字符流的抽象类。子类必须实现的方法只有 read(char[], int, int) 和 close()。

BufferedReader从字符输入流中读取文本,缓冲各个字符,从而实现字符、数组和行的高效读取。 可以指定缓冲区的大小,或者可使用默认的大小。大多数情况下,默认值就足够大了。

InputStreamReader是字节流通向字符流的桥梁:它使用指定的 charset 读取字节并将其解码为字符。它使用的字符集可以由名称指定或显式给定,或者可以接受平台默认的字符集。

FileReader用来读取字符文件的便捷类。此类的构造方法假定默认字符编码和默认字节缓冲区大小都是适当的。要自己指定这些值,可以先在 FileInputStream 上构造一个 InputStreamReader。

Read类中也有很多的read方法,使用方法和字节流的一致,这里也强烈建议使用自定义缓冲区的方法去读,不要使用无参的read 一个字符一个字符去读

这里给出一个带自定义缓冲区的Reader例子:

import java.io.*;
class FileReaderDemo2 {
    public static void main(String[] args) throws IOException {
        FileReader fr = new FileReader("demo.txt"); //创建读取流对象和指定文件关联。
        //因为要使用read(char[])方法,将读取到字符存入数组。所以要创建一个字符数组,一般数组的长度都是1024的整数倍。
        char[] buf = new char[1024];
        int len = 0;
        while(( len=fr.read(buf)) != -1) {
            System.out.println(new String(buf,0,len));
        }
        fr.close();
    }
}

 

输出流:

Writer写入字符流的抽象类。子类必须实现的方法仅有 write(char[], int, int)、flush() 和 close()。

BufferedWriter将文本写入字符输出流,缓冲各个字符,从而提供单个字符、数组和字符串的高效写入。

OutputStreamWriter是字符流通向字节流的桥梁:可使用指定的 charset 将要写入流中的字符编码成字节。它使用的字符集可以由名称指定或显式给定,否则将接受平台默认的字符集。

FileWriter用来写入字符文件的便捷类。此类的构造方法假定默认字符编码和默认字节缓冲区大小都是可接受的。要自己指定这些值,可以先在 FileOutputStream 上构造一个 OutputStreamWriter。

PrintWriter向文本输出流打印对象的格式化表示形式。此类实现在 PrintStream 中的所有 print 方法。它不包含用于写入原始字节的方法,对于这些字节,程序应该使用未编码的字节流进行写入

字符流经常将BufferedReader和PrintWriter配合使用。而且BufferedReader有一个readLine读一行的操作很方便:

public class BrAndBwOrPwDemo {
    public static void main(String[] args) throws IOException{
        BufferedReader br = new BufferedReader(
                new InputStreamReader(
                        new FileInputStream("e:\\\\test.txt")));

        PrintWriter pw = new PrintWriter("e:\\\\test.txt");
        String line ;
        while((line = br.readLine())!=null){
            System.out.println(line);//一次读一行,并不能识别换行符
            pw.println(line);      //可以实现换行
            pw.flush();
        }
        br.close();
        pw.close();
    }

}

 

Write类也有很多write方法,使用方法类似,这里给出一个FileWriter使用追加的方式写一个日志文件:

// 日志
    public void logg(String msg) {
        
        FileWriter writer=null;
        try {
            
            //如果目录不存在则创建
            if(!new File("log").exists()){
                new File("log").mkdirs();
            }
            
            File file = new File("log/logging.txt");
            //日志格式
              //  \\r\\n在输出的文件中就是换行
            String mString = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date()) + "\\t" + msg+"\\r\\n";
            if (file.exists()) {
                writer=new FileWriter(file,true);   //第二个参数为true就是追加的方式写内容
                writer.write(mString);
                writer.flush();
            }else {
                
                file.createNewFile();
                writer=new FileWriter(file);
                writer.write(mString);
                writer.flush();
            }
        } catch (Exception e) {
            e.printStackTrace();
        } finally {

            try {
                writer.close();
            } catch (Exception e) {
                e.printStackTrace();
            }
        }

    }

 

对象序列化

所谓的对象序列化就是将Object对象转化为byte数组,为什么要转换为byte数组?,因为例如在网络上传输使用的字节,所以需要序列化。反之就是反序列化

序列化使用 ObjectOutputStream类的writeObject方法  ,反序列使用的是ObjectInputStream类的readObject方法

注意:需要序列化的类一定要实现Serializable接口,这是一个强制规定,如果需要序列化的类不实现这个接口,会报下面这个异常:

需求:将一个Student 的POJO类序列化

Student:

import java.io.Serializable;

public class Student implements Serializable{          //需要序列化的类一定要实现Serializable接口
    private String stuno;
    private String stuname;
    
    public Student(String stuno, String stuname, int stuage) {
        super();
        this.stuno = stuno;
        this.stuname = stuname;
    }

    public String getStuno() {
        return stuno;
    }
    public void setStuno(String stuno) {
        this.stuno = stuno;
    }
    public String getStuname() {
        return stuname;
    }
    public void setStuname(String stuname) {
        this.stuname = stuname;
    }

    @Override
    public String toString() {
        return "Student [stuno=" + stuno + ", stuname=" + stuname  + "]";
    }

}

 

先序列化成byte数组存在一个文件中,再从文件中反序列化成一个对象输出:

import java.io.FileInputStream;
import java.io.ObjectInputStream;

public class ObjectSeriaDemo1 {
    public static void main(String[] args) throws Exception{
        String file = "demo/obj.dat";
        //1.对象的序列化
        /*ObjectOutputStream oos = new ObjectOutputStream(
                new FileOutputStream(file));
        Student stu = new Student("10001", "张三", 20);
        oos.writeObject(stu);
        oos.flush();
        oos.close();*/
        
        //反序列化
        ObjectInputStream ois = new ObjectInputStream(
                new FileInputStream(file));
        Student stu = (Student)ois.readObject();
        System.out.println(stu);
        ois.close();
        
    }
}

 

transient关键字

使用transient关键字声明属性不进行序列化,或者是按照需求自己去完成这个属性的序列化。这个关键字之前在进行POJO类转JSON数据的时候使用过,使用这个关键字修饰的属性不会生成到JSON中,说了一点题外话,我们可以在student类中添加一个属性,把这个属性声明为transient,不进行序列化

Student:

import java.io.Serializable;

public class Student implements Serializable{
    private String stuno;
    private String stuname;
    //该元素不会进行jvm默认的序列化,可以按需求自己完成这个元素的序列化
    private transient int stuage;  
    
    public Student(String stuno, String stuname, int stuage) {
        super();
        this.stuno = stuno;
        this.stuname = stuname;
        this.stuage = stuage;
    }

    public String getStuno() {
        return stuno;
    }
    public void setStuno(String stuno) {
        this.stuno = stuno;
    }
    public String getStuname() {
        return stuname;
    }
    public void setStuname(String stuname) {
        this.stuname = stuname;
    }
    public int getStuage() {
        return stuage;
    }
    public void setStuage(int stuage) {
        this.stuage = stuage;
    }
    @Override
    public String toString() {
        return "Student [stuno=" + stuno + ", stuname=" + stuname + ", stuage="
                + stuage + "]";
    }

}

Stuage属性就不会序列化

 

自己完成属性的序列化和反序列化

我们可以查看ArrayList的源码,ArrayList的writeObject方法就是自己去完成序列化,readObject实现了反序列化,为什么ArrayList需要自行去做这些事情?是因为ArrayList底层使用数组来完成工作,但是数组中空的元素不需要序列化,所以它自己去做序列化可以提升性能,这也是自行序列化的一个优点。

我们可以将这两个方法的方法签名拿到Student类中,来自行序列化和反序列化:

     private void writeObject(java.io.ObjectOutputStream s)
                throws java.io.IOException{
         s.defaultWriteObject();//把jvm能默认序列化的元素进行序列化操作
         s.writeInt(stuage);//自己完成stuage的序列化
     }
     private void readObject(java.io.ObjectInputStream s)
                throws java.io.IOException, ClassNotFoundException{
          s.defaultReadObject();//把jvm能默认反序列化的元素进行反序列化操作
          this.stuage = s.readInt();//自己完成stuage的反序列化操作
    }

 

序列化中子父类构造函数的调用问题

只要父类实现了Serializable接口,那么子类就可以序列化了,序列化中构造函数的调用是先调用父类构造,然后是子类,和我们不进行序列化操作的父子类调用构造的方法一致

但是反序列化有些不同,如果父类实现Serializable接口,子类在进行反序列化的时候就只调用自己的构造函数,不调用父类的构造;如果父类没有实现序列化接口,只是子类实现了,在反序列的时候就会先调用父类的,再调用子类的构造。

这也是一个特点,相当于父类实现了序列化接口,子类序列化的时候就可以直接读出来,不用再调用父类构造,而父类没有实现序列化接口,就要先调用父类构造

 

最后来看一个错误

我们使用InputStream来读一个文件:

        //使用InputStream
        InputStream inputStream=new FileInputStream("D:\\\\text.txt");
        byte[] b=new byte[1024];
        int acc=inputStream.read(b);
        while(acc!=-1){
            System.out.println(new String(b));
            acc=inputStream.read(b);
        }

我使用一个名为b的缓冲,大小为1024个字节,一次从text.txt中读取1024个字节到b这个缓冲中并输出成String。

这样的写法存在一些问题,如果剩余的数据小于1024个字节,我们还将其以1024个字节输出,就会出现一些无用的空数据,这不是我们想要的,所以在转换为String的时候要使用:

System.out.println(new String(b,0,acc));

这样有多少个字节我们就转换多少个字节,就不会出现空的无用数据了。writer数据时也存在该问题,需要注意。

 

以上是关于Java复习——I/O与序列化的主要内容,如果未能解决你的问题,请参考以下文章

二次元码农的成长之路I/O复习1

Java I/O - 对象的输入输出与序列化

《文件与I/O流》第4节:对象序列化

Java Web的开始学习

I/O(输入/输出)---序列化与反序列化

笔记:I/O流-字符集