Java语言基础(SE)-第十节 IO

本节需要仔细阅读,了解IO的使用,认证理解,每种语言都有类似的IO,以便于触类旁通

简介

  1. I/O 流
    1. I/O 流 全称是 Input/Output Stream,译为“输入/输出流”
    2. 输出流:应用程序将数据写入到文件、设备等
    3. 输入流:将文件、设备等数据读入到应用程序中
  2. 常用类型
    1. I/O 流的常用类型都在 java.io 包中

       类型                               输入流                输出流
       字节流(Byte Streams)           InputStream         OutputStream
       字符流(Character Streams)         Reader              Writer
       缓冲流(Buffered Streams)   BufferedInputStream     BufferedOutputStream
                                     BufferedReader         BufferedWriter
       数据流(Data Streams)          DataInputStream       DataOutputStream
       对象流(Object Streams)        ObjectInputStream     ObjectOutputStream
      
    2. 不管什么类型的流都有输入流和输出流

File

  1. 一个 File 对象就代表一个文件或目录(文件夹)

     //file1、file2都能访问test.txt文件
     File file1 = new File("F:\\Files\\Texts\\test.txt");
     File file2 = new File("F:/Files/Texts/test.txt"); 
    
  2. 名字分隔符(name separator):
    1. 在 UNIX、Linux、Mac 系统中:正斜杠(/)
    2. 在 Windows 系统中:反斜杠(\)
    3. 用File.separator方法可以查看当前系统支持哪种斜杠
  3. 路径分隔符(path separator):当有多个路径的时候
    1. 在 UNIX、Linux、Mac系统中:冒号(:)
    2. 在 Windows 系统中:分号(;)
    3. 用File.pathSeparator方法可以查看当前系统支持哪种分隔符
  4. 在 Windows、Mac 系统中
    1. 文件名、目录名不区分大小写
  5. 在 UNIX、Linux 系统中
    1. 文件名、目录名区分大小写

常用方法

  1. 常用方法如下

     String getName() //获取文件或目录的名称
     String getParent() //获取父路径
     File getParentFile() //获取父文件
     String getPath() //获取路径(相对路径)
     String getAbsolutePath() //获取绝对路径
     File getAbsoluteFile() //获取绝对路径形式的文件
     long lastModified() //最后一次修改的时间
     long length() // 文件的大小(不支持目录)
     boolean isAbsolute() //是否绝对路径
     boolean exists() //是否存在
     boolean isDirectory()
     boolean isFile()
     boolean isHidden()
     boolean canRead()
     boolean canWrite()
     String[] list() //获取当前目录下所有文件、目录的名称
     String[] list(FilenameFilter filter)
     File[] listFiles() // 获取当前目录下所有文件、目录
     File[] listFiles(FilenameFilter filter)
     File[] listFiles(File filter)
     boolean createNewFile() //创建文件(不会覆盖旧文件)
     boolean delete()//删除文件或空目录(不经过回收站,彻底删除)
     boolean mkdir() // 创建当前目录
     boolean mkdirs() //创建当前目录(包括不存在的父目录,如果没有就创建)
     boolean rename To(Filed est)//剪切到新路径
     boolean setLastModified(long time) //设置修改时间
     boolean setReadOnly() //设置可读写
     boolean setWritable(boolean writable,boolean owner Only)
     boolean setWritable(boolean writable)
     boolean setReadable(boolean readable,boolean owner Only)
     boolean setReadable(boolean readable)
    
  2. 举例:

     package com.zh;
     import java.io.File;
     import java.util.function.Consumer;
        
     public class Files {
         //搜索文件夹目录
         public static void search(File dir,Consumer<File> operation) {
             if (dir == null || operation == null) return;
             //文件夹是否存在,如果是文件也不行,文件就不需要遍历
             if (!dir.exists() || dir.isFile()) return;
             File[] subfiles = dir.listFiles();
             for (File file : subfiles) {
                 operation.accept(file);
                 if (file.isFile()) continue;
                 search(file, operation);
             }
         }
            	
         //删除
         public static void delete(File file) {
             if (file == null || !file.exists())return;
             clean(file);
             file.delete();
         }
         public static void clean(File dir) {
             if (dir == null || !dir.exists() || dir.isFile())return;
             File[] subfiles = dir.listFiles();
             //遍历删除文件夹的所有内容
             for (File sf : subfiles) {
                 delete(sf);
             }
         }
            	
         //剪切
         public static void move(File src,File dest) {
             if (src == null ||dest == null)return;
             //不覆盖
             if (!src.exists() ||!dest.exists())return;
             mkparents(dest);
             src.renameTo(dest);
         }
         //没有文件路径,自动创建文件路径
         private static void mkparents(File file) {
             //获取父文件路径
             File parent = file.getParentFile();
             if (parent == null) return;
             parent.mkdirs();
         }
     }
     //打印出F:\\Files\\Texts目录下的所有文件、文件夹
     Files.search(new File("F:\\Files\\Texts"), (file)->{
         System.out.println(file);
     });
    

字符编码(Character Encoding)

字符集(Character Set)

  1. 在计算机里面一个中文汉字、英文字母、阿拉伯数字、标点符号都是一个字符
  2. 字符集(简称 Charset):由字符组成的集合
  3. 常见的字符集有
    1. ASCII:128个字符(包括了英文字母大小写、阿拉伯数字等)
    2. ISO-8859-1:支持欧洲的部分语言文字,在有些环境也叫 Latin-1
    3. GB2312:支持中文(包括了 6763 个汉字)
    4. BIG5:支持繁体中文(包括了 13053 个汉字)
    5. GBK:是对 GB2312、BIG5 的扩充(包括了 21003 个汉字),支持中日韩
    6. GB18030:是对 GBK 的扩充(包括了 27484 个汉字)
    7. Unicode:包括了世界上所有的字符
  4. ISO-8859-1、GB2312、BIG5、GBK、GB18030、Unicode 中都已经包括了 ASCII 中的所有字符

字符编码(Character Encoding)

  1. 每个字符集都有对应的字符编码,它决定了每个字符如何转成二进制存储在计算机中
  2. ASCII:单字节编码,编码范围是 0x00 ~ 0x7F (0 ~ 127)
  3. ISO-8859-1:单字节编码,编码范围是 0x00 ~ 0xFF
    1. 0x00 ~ 0x7F 和 ASCII 一致,0x80 ~ 0x9F 是控制字符,0xA0 ~ 0xFF 是文字符号
  4. GB2312、BIG5、GBK:采用双字节表示一个汉字
  5. GB18030:采用单字节、双字节、四字节表示一个字符
  6. Unicode:有 Unicode、UTF-8、UTF-16、UTF-32 等编码,最常用的是 UTF-8 编码
    1. UTF-8 采用单字节、双字节、三字节、四字节表示一个字符
  7. 总结:同样一个字符串,不同的编码方式,转化成的二进制不一样

字符编码比较

  1. 举例

     String str = "zh华仔";
     str.getBytes("ASCII");//[122, 104, 63, 63]
     str.getBytes("ISO-8859-1");//[122, 104, 63, 63]
     str.getBytes("UTF-8");//[122, 104, -27, -115, -114, -28, -69, -108]
     System.out.println(Arrays.toString(str.getBytes("ASCII")));
     System.out.println(Arrays.toString(str.getBytes("ISO-8859-1")));
     System.out.println(Arrays.toString(str.getBytes("UTF-8")));
    
  2. 如果 String.getBytes 方法没有传参,就使用 JVM 的默认字符编码,一般跟随 main 方法所在文件的字符编码
  3. 可以通过 Charset.defaultCharset 方法获取 JVM 的默认字符编码
    1. Charset 类的全名是 java.nio.charset.Charset

乱码

  1. 一般将【字符串】转为【二进制】的过程称为:编码(Encode)
  2. 一般将【二进制】转为【字符串】的过程称为:解码(Decode)
  3. 编码、解码时使用的字符编码必须要保持一致,否则会造成乱码

     String str1 = "Java不难";
     //编码
     byte[]bytes = str1.getBytes("UTF-8");
     //解码
     String str2 = new String(bytes,"GB18030");
     //乱码: Java涓嶉毦
     System.out.println(str2);
    

字节流(Byte Streams)

  1. 字节流的特点
    1. 一次只读写一个字节,以字节为单位读写
    2. 最终都继承自 InputSteam、OutputStream
  2. 常用的是字节流有 FileInputStream、FileOutputStream

FileOutStream

  1. 输出数据到文件

     //将“zh华仔”写入到桌面的text.txt文件夹中
     OutputStream os = new FileOutputStream("/Users/mac/Desktop/text.txt");
     //true表示追加内容,不覆盖原来的内容
     //OutputStream os2 = new FileOutputStream("/Users/mac/Desktop/text.txt",true);
     //写一个字节
     os.write(122);//z
     //写第二个字节
     os.write(104);//h
     os.write("华仔".getBytes());
     //按指定编码写入
     //os.write("华仔".getBytes("UTF-8"));
     os.close();
    

FileInputStream

  1. 读取文件数据

     InputStream is = new FileInputStream("/Users/mac/Desktop/text.txt");
     //一个字节一个字节的读取----
     //读取第一个字节
     int byte1 = is.read();
     //读取第2个字节
     int byte2 = is.read();
     byte[] bytes = new byte[1024];
     //一次读取多个字节,都是以字节为单位---
     //read返回实际读取的字节数,
     int len = is.read(bytes);
     is.close();
     //122-104-6,6代表6个字节
     System.out.println(byte1 + "-" + byte2 + "-" + len);
    

练习

  1. 将内存中的数据写入文件

     //将内存中的数据写入文件
     public static void write(byte[] data, File file) {
         if (data == null || file == null || file.exists()) return;
         //没有文件路径,自动创建文件路径
         mkparents(file);
         try(OutputStream os = new FileOutputStream(file)) {
             os.write(data);
         } catch (IOException e) {
             e.printStackTrace();
         }
     }
    
  2. 从文件读取数据到内存

     //从文件读取数据到内存
     public static byte[] read(File file) {
         if (file == null || !file.exists()) return null;
         if (file.isDirectory()) return null;
         try(InputStream is = new FileInputStream(file)) {
             byte[] data = new byte[(int)file.length()];
             is.read(data);
             return data;
         } catch (IOException e) {
             e.printStackTrace();
             return null;
         }
     }
     //使用
     byte[] data =  Files.read(new File("/Users/mac/Desktop/text.txt"));
     //zh华仔
     System.out.println(new String(data));
    
  3. 复制

     //复制
     public static void copy(File src,File dest) {
         if(src == null || dest == null) return;
         if (!src.exists() ||!dest.exists())return;
         if(src.isDirectory()) return;
         mkparents(dest);
         InputStream is = null;
         OutputStream os = null;
         try {
             is = new FileInputStream(src);
             os = new FileOutputStream(dest);
             byte[] data = new byte[8192];//8kb
             int len;
             //每次读取data这么长的数据,一直读取
             while ((len = is.read(data)) !=-1) {
                 //data读取到的数据,从0开始,写len这么长
                 os.write(data,0,len);
             }
         } catch (FileNotFoundException e) {
         }catch (IOException e) {
         }finally {
             if(is != null) {
                 try {
                     is.close();
                 }catch (IOException e) {}
             }
         if(os != null) {
             try {
                 os.close();
             }catch (IOException e) {}
             }
         }
     }
     //使用
     Files.copy(new File("/Users/mac/Desktop/text.txt"), new File("/Users/mac/Desktop/text2.txt"));
    

try-with-resources 语句

  1. 下面就是从 Java 7 开始推出的try-with-resources语句(可以没有catch、finally)

     try(资源1;资源2;...){
     }catch(Exception e){
     }finally {
     }
    
    1. 最后一个资源可以不写分号(;)
  2. 可以在try后面的小括号中声明一个或多个资源(resource)
    1. 实现了 java.lang.AutoCloseable 接口的实例,都可以称之为是资源
    2. 所有的IO流都实现了java.lang.AutoCloseable接口
    3. 也就是说try-with-resource使用是有条件的
  3. 不管try中的语句是正常还是意外结束
    1. 最终都会自动按顺序调用每一个资源的close方法(close 方法的调用顺序与资源的声明顺序相反
    2. 调用完所有资源的 close 方法后,再执行finally中的语句
  4. 简化copy方法

     public static void copy(File src,File dest) {
         if(src == null || dest == null) return;
         if (!src.exists() ||!dest.exists())return;
         if(src.isDirectory()) return;
         mkparents(dest);
            	
         try(
             InputStream is = new FileInputStream(src);
             OutputStream os = new FileOutputStream(dest);
         ) {
             byte[] data = new byte[8192];//8kb
             int len;
             //每次读取data这么长的数据,一直读取
             while ((len = is.read(data)) !=-1) {
                 //data读取到的数据,从0开始,写len这么长
                 os.write(data,0,len);
             }
         } catch (FileNotFoundException e) {
         }catch (IOException e) {
         }
     }
    

字符流(Character Streams)

  1. 字符流的特点
    1. 一次只读写一个字符,以字符为单位读写
    2. 最终都继承自 Reader、Writer
  2. 常用的是字符流有 FileReader、FileWriter
    1. 注意:这 2 个类只适合文本文件,比如 .txt、.java 等这类文件
    2. 因为文本中才是各种字符组成

FileWriter

  1. 向文本中写入内容

     Writer writer = new FileWriter("/Users/mac/Desktop/text.txt");
     //一个字符一个字符写入
     writer.write('z');
     writer.write('h');
     writer.write('华');
     writer.write('仔');
     writer.write("zh");
     //多个字符写入
     //将字符串转化为字符数组
     writer.write("华仔".toCharArray());
     writer.close();
     //zh华仔zh华仔
    

FileReader

  1. 读取文本中的内容

     //文本中的数据为
     Reader reader = new FileReader("/Users/mac/Desktop/text.txt");
     //读取第一个字符
     int c1 = reader.read();
     //读取第二个字符
     int c2 = reader.read();
     //读取第三个字符
     int c3 = reader.read();
     char[] chars = new char[1024];
     //read方法返回实际读取的字符数,读取的结果在字符数组chars中
     int len = reader.read(chars);
     reader.close();
     //z
     System.out.println((char)c1);
     //h
     System.out.println((char)c2);
     //华
     System.out.println((char)c3);
     //1
     System.out.println(len);
     //仔
     System.out.println(chars);
    

练习

  1. 将文本文件的内容逐个字符打印出来

     File file = new File("/Users/mac/Desktop/text.txt");
     try (
         Reader reader = new FileReader(file);
     ) {
         int c;
         while ((c = reader.read()) != -1) {
             System.out.println((char)c);
             Thread.sleep(10);
         }
     } catch (FileNotFoundException e) {
     }catch (IOException e) {
     }catch (InterruptedException e) {
     }
    

缓冲流(Buffered Streams)

  1. 之前学习的字节流、字符流,都是无缓冲的 I/O 流,每个读写操作均由底层操作系统直接处理
    1. 每个读写操作通常会触发磁盘访问,因此大量的读写操作,可能会使程序的效率大大降低
  2. 为了减少读写操作带来的开销,Java 实现了缓冲的 I/O 流
    1. 缓冲输入流:从缓冲区读取数据,并且只有当缓冲区为空时才调用本地的输入 API(Native Input API)
    2. 缓冲输出流:将数据写入缓冲区,并且只有当缓冲区已满时才调用本地的输出 API(Native Output API)
  3. 分析
    1. 本地的输入/输出 API: 操作系统底层API
    2. 操作系统的API才能真正访问计算机磁盘
    3. 字节流、字符流的原理是每访问一个字节、字符,都会去调用操作系统的API,由操作系统的API去访问磁盘
    4. 缓冲流是有个缓冲区,字节流、字符流先到缓冲区,缓冲区满了或者为空时才会去调用系统的API去访问磁盘

                   缓冲输入流               缓冲输出流
       缓冲字节流   BufferedInputStream     BufferedOutputStream
       缓冲字符流   BufferedReader          BufferedWriter
      
    5. 上述表格中 4 个缓冲流的默认缓冲区大小是 8192 字节(8KB),可以通过构造方法传参设置缓冲区大小

使用

  1. 缓冲流的常见使用方式:将无缓冲流传递给缓冲流的构造方法(将无缓冲流包装成缓冲流)
    1. 如果把无缓冲流比作是一个无装备的士兵,那么缓冲流就是一个有强力装备的士兵

File file = new File("/Users/mac/Desktop/text.txt");
InputStream is = new FileInputStream(file);
//将无缓冲流,包装成缓冲流,缓冲区扩展到16k
BufferedInputStream bis = new BufferedInputStream(is,16384);
//只需要关闭缓冲流即可,不需要关闭is
bis.close();
	
File file2 = new File("/Users/mac/Desktop/text.txt");
BufferedWriter writer = new BufferedWriter(new FileWriter(file2));
writer.write("111");
//换行
writer.newLine();
writer.write("222");
writer.close();

缓冲流 – close、flush

  1. 只需要执行缓冲流的 close 方法,不需要执行缓冲流内部包装的无缓冲流的 close 方法
  2. 调用缓冲输出流的 flush 方法,会强制调用本地的输出 API,将缓冲区的数据真正写入到文件中
    1. 缓冲输出流的 close 方法内部会调用一次 flush 方法
File file2 = new File("/Users/mac/Desktop/text.txt");
BufferedWriter writer = new BufferedWriter(new FileWriter(file2));
writer.write("111");
//writer.flush();//直接调用flush,强制将没有满的缓冲区数据写入到磁盘文件中去
//不调用close,不会写入到文件,因为此时缓冲区还没有满
//close内部会调用flush 方法,强制将没有满的缓冲区数据写入到磁盘文件中去
writer.close();

练习

  1. 用缓冲流修改 write

     //将内存中的数据写入文件
     public static void write(byte[] data, File file) {
         if (data == null || file == null || file.exists()) return;
         //没有文件路径,自动创建文件路径
         mkparents(file);
         try(OutputStream os = new BufferedOutputStream(new FileOutputStream(file))) {
             os.write(data);
         } catch (IOException e) {
             e.printStackTrace();
         }
     }
    
  2. 用缓冲流修改 read、copy、逐个打印字符,同理包装即可
  3. 用缓冲流逐行打印字符串

     File file = new File("/Users/mac/Desktop/text.txt");
     try (
         BufferedReader reader = new BufferedReader(new FileReader(file));
     ) {
         String line; //一行内容的字符串
         //逐行打印
         while ((line = reader.readLine()) != null) {
             System.out.println(line);
             Thread.sleep(10);
         }
     } catch (FileNotFoundException e) {
     }catch (IOException e) {
     }catch (InterruptedException e) {
     }
    
  4. 转换文本文件编码

     //思路,先将gbk文件中的数据解码出来,解析成字符串,然后再按照UTF-8的编码格式写入到utf8文件中
     try(
         //现有的文件是GBK格式编码
         BufferedReader reader = new BufferedReader(
         //将字节流转化为字符流,按照BGK编码转化,注意InputStreamReader是字符流,是FileReader的父类
         new InputStreamReader(new FileInputStream("/Users/mac/Desktop/bgk.txt"),"GBK"));
     //转化为UTF8文件复制到utf8文件中去
     BufferedWriter writer = new BufferedWriter(
         //将字节流转化为字符流,按照UTF-8编码转化,注意OutputStreamWriter是字符流,是FileWriter的父类
         new OutputStreamWriter(new FileOutputStream("/Users/mac/Desktop/utf8.txt"),"UTF-8"));	
     ) {
         char[] chars = new char[1024];
         int len;
         //一个字符一个字符的写入
         while ((len = reader.read(chars)) != -1) {
             writer.write(chars,0,len);
         }
     }
    
  5. 模拟AI 代码

     //读取控制台的输入,就是键盘的输入,标准输入
     BufferedReader reader = new BufferedReader(new InputStreamReader(System.in));
     String str ;
     //reader.readLine有等待键盘输入敲击回车的功能
     while ((str = reader.readLine()) != null) {
         str = str.replace("你", "朕");
         str = str.replace("吗", "");
         str = str.replace("么", "");
         str = str.replace("?", "!");
         str = str.replace("?", "!");
         System.out.println("\t" + str);
     }
     reader.close();
    
    1. 在控制台输入:”你吃饭了吗?”,则输出:”朕吃饭了!”
    2. System.in 属于标准输入流,可以从键盘接收输入
    3. 利用 InputStreamReader 可以实现【字节输入流】转【字符输入流】
    4. 同理,利用 OutputStreamWriter 可以实现【字节输出流】转【字符输出流】

Scanner

  1. java.util.Scanner 是一个可以使用正则表达式来解析基本类型和字符串的简单文本扫描器
    1. 它默认利用空白(空格\制表符\行终止符)作为分隔符将输入分隔成多个 token
     Scanner(InputStream source)
     Scanner(Readable source)
     Scanner(File source)
     Scanner(String source)
    
  2. 举例

     //根据空格自动分割
     Scanner s = new Scanner("jack rose kate");
     while (s.hasNext()) {
         System.out.println(s.next());
     }
     //jack
     //rose
     //kate
     s.close();
    
  3. 该类是专门针对控制台输入输出的,不常用(略)

格式化输出

  1. 有 2 个类可以实现格式化输出
    1. PrintStream、PrintWriter
  2. 它们有 3 个常用方法:print、println、format
  3. print、write 的区别
    1. write(97) 写入的是字符'97'
    2. print(97) 写入的是字符串"97"

PrintStream

  1. System.out、System.err 是 PrintStream 类型的实例
    1. 属于标准输出流(Standard Ouput Stream)
    2. 比如输出到屏幕、控制台(Console)
  2. PrintStream 是字节流,但它内部利用字符流对象来模拟字符流的许多功能

PrintWriter

  1. 平时若要创建格式化的输出流,一般使用 PrintWriter,它是字符流

     String name = "jack";
     int age = 20;
     PrintWriter writer = new PrintWriter("/Users/mac/Desktop/bgk.txt");
     //格式化输出到文件中去
     writer.format("My name is %s,age is %d", name,age);
     writer.close();
    
  2. 可以通过构造方法设置 PrintWriter.autoflush 为true
    1. 那么 println、printf、format 方法内部就会自动调用 flush 方法
     PrintWriter writer2 = new PrintWriter(new FileOutputStream(new File("/Users/mac/Desktop/bgk.txt")),true);
    
  3. 与System.out.format区别
    1. System.out.format是格式化输出到屏幕、控制台
    2. PrintWriter格式化输出到文件中去

数据流

  1. 有 2 个数据流:DataInputStream、DataOutputStream,支持基本类型、字符串类型的 I / O 操作
double height =1.75;
int age= 20;
int money = 3000;
String name = "Jack";
//数据输出流
DataOutputStream dos= new DataOutputStream(new FileOutputStream("F:/66.txt")) ;
//向文件中写基本类型、字符串类型的数据
dos.writeInt(age) ;
dos.writeInt(money) ;
dos.writeDouble(height) ;
dos.writeUTF(name) ;
dos.close() ;
//文件内容:0000 0014 0000 0bb8 3ffc 0000 0000 0000 0004 4a61 636b

//数据输入流,获取文件的数据
DataInputStream dis = new DataInputStream(new FileInputStream("F:/66.txt")) ;
System.out.println(dis.readInt()) ;//20
System.out.println(dis.readInt()) ;//3000
System.out.println(dis.readDouble()) ; //1.75
System.out.println(dis.readUTF()) ; //Jack
dis.close() ;

对象流

  1. 有 2 个对象流:ObjectInputStream、ObjectOutputStream,支持引用类型的 I/O 操作
  2. 只有实现了 java.io.Serializable 接口的类才能使用对象流进行 I / O 操作
    1. 否则会抛出 java.io.NotSerializableException 异常
  3. Serializable 是一个标记接口(Maker Interface),不要求实现任何方法,仅仅用来标记一下

对象的序列化和反序列化

  1. 序列化(Serialization)
    1. 将对象转换为可以存储或传输的数据(二进制)
    2. 利用 ObjectOutputStream 可以实现对象的序列化
  2. 反序列化(Deserialization )
    1. 从序列化后的数据中恢复出对象(二进制转为对象)
    2. 利用 ObjectInputStream 可以实现对象的反序列化

public class Book implements Serializable {
    private double price;
    private String name;
    public Book(double price,String name) {
        this.price = price;
        this.name = name;
    }
    	
    @Override
    public String toString() {
        return "Book [price=" + price + ", name=" + name + "]";
    }
}
public class Car implements Serializable {
    private double price;
    private String band;
    public Car(double price,String band) {
        this.price = price;
        this.band = band;
    }
    @Override
    public String toString() {
        return "Car [price=" + price + ", band=" + band + "]";
    }
}
public class Person implements Serializable {
    //所有的成员必须都实现了Serializable
    private int age;
    private Car car;
    private String name;
    private List<Book> books = new ArrayList<>();
    public Person(int age,String name) {
        this.age = age;
        this.name = name;
    }
    public List<Book> getBooks() {
        return books;
    }
    public void setCar(Car car) {
        this.car = car;
    }
    @Override
    public String toString() {
        return "Person [age=" + age + ", name=" + name +", car=" + car + ", books=" + books + "]";
    }
}

//main函数
//序列化
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("/Users/mac/Desktop/text.txt"));
Person p = new Person(20, "Jack");
p.setCar(new Car(305.6, "Bently"));
p.getBooks().add(new Book(19.9, "Java"));
p.getBooks().add(new Book(38.8, "C++"));
oos.writeObject(p);
	
Car c = new Car(107.8,"BMW");
oos.writeObject(c);
oos.close();

//反序列化
ObjectInputStream ois = new ObjectInputStream(new FileInputStream("/Users/mac/Desktop/text.txt"));
Person p = (Person) ois.readObject();
//Person [age=20, name=Jack, car=Car [price=305.6, band=Bently], books=[Book [price=19.9, name=Java], Book [price=38.8, name=C++]]]
System.out.println(p);
	
Car c = (Car) ois.readObject();
//Car [price=107.8, band=BMW]
System.out.println(c);
ois.close();

transient

  1. 被transient修饰的实例变量不会被序列化

     //Dog类
     package com.zh;
     import java.io.Serializable;
    
     public class Dog implements Serializable {
         private static final long serialVersionUID = 1L;
         //transient
         private transient int age;
         private String name;
         public Dog(int age,String name) {
             this.age = age;
             this.name = name;
         }
         @Override
         public String toString() {
             return "Dog [age=" + age + ", name=" + name + "]";
         }
     }
        
     //main
     ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("/Users/mac/Desktop/text.txt"));
     oos.writeObject(new Dog(5, "Larry"));
     oos.close();
        
     ObjectInputStream ois = new ObjectInputStream(new FileInputStream("/Users/mac/Desktop/text.txt"));
     //Dog [age=0, name=Larry]
     //age没有被序列化
     System.out.println(ois.readObject());
     ois.close();
    

serialVersionUID

  1. 每一个可序列化类都有一个serialVersionUID,相当于类的版本号
    1. 默认情况下会根据类的详细信息计算出serialVersionUID的值,根据编译器实现的不同可能千差万别
    2. 一旦类的信息发生修改,serialVersionUID的值就会发生变化
  2. 如果序列化、反序列时的serialVersionUID不一致
    1. 会认定为序列化、反序列时的类不兼容,会抛出 java.io.InvalidClassException 异常
  3. 强烈建议每一个可序列化类(注意条件)都自定义serialVersionUID,不要使用它的默认值
    1. 必须是static final long
    2. 建议声明为private
    3. 如果没有自定义serialVersionUID,编译器会发出警告,点击警告可自动添加这个属性

       private static final long serialVersionUID = 1L;
      

总结

  1. 字节流一个一个字节访问,可以访问任何数据
  2. 字符流一个一个字符访问,只能访问文本文件,按字符读取
  3. 同一段文本,不同的编码格式生成的字节数可能不一样
  4. 字节流、字符流的继承体系

    图1

    1. 紫色为接口、蓝色为类、绿色为抽象类
  5. 数据流、对象流的继承体系 图1
Table of Contents