Java基础IO流,字节输出流与字节输入流,转换流等

267 阅读24分钟

开启掘金成长之旅!这是我参与「掘金日新计划 · 12 月更文挑战」的第13天,点击查看活动详情


前言

本文将为大家对Java基础IO流的相关知识进行介绍,具体包含字节输出流与字节输入流;字符;缓冲流;对文件内容进行排序;转换流;序列化与反序列化等Java IO基础相关知识。

Java全栈学习路线可参考:【Java全栈学习路线】最全的Java学习路线及知识清单,Java自学方向指引,内含最全Java全栈学习技术清单~


​ Java的IO是实现输入和输出的基础,可以方便的实现数据的输入和输出操作。在Java中把对于输入/输入操作是以流的方式进行操作的。java.io 包下提供了大量的供我们使用的操作【流】的方法和接口,用于进行各类数据的处理和传输。

​ 计算机的输入和输出都是通过二进制来完成的。在网络中我们要传递数据就要将数据【流化】,换句话说就是将文件、复杂的对象转化成能够在网络上传输的一个个的0和1,我在这里先画几幅图帮助大家理解一下。

文件在磁盘的输入输出:

image.png

文件在网络中的输入输出:

image.png

内存中的对象的输入输出:

image.png

一、IO流的分类(OutputStream)

首先我们先来看一下对于IO流的分类。

Java中一切皆对象,流也是对象,在学习之前我们不妨先看分类和概念,至于是哪个类其实没那么重要。

​ 其实说到流,我们能想到流水,其实这已经很形象了,水从汪洋大海流入湖泊就是要通过河流。如果你还不知道,接着往下看。

​ 其实到目前为止,我们对流已经有了基本的概念,接下来我们就要深入学习流了。按照不同的分类方式,可以把流分为不同的类型。常用的分类有三种:

1.按照流向分

  • 输入流: 只能从中读取数据,而不能向其写入数据。
  • 输出流:只能向其写入数据,而不能向其读取数据。

image.png

其实计算机在读取文件的时候是很麻烦的:

image.png

​ 当然系统级别的方法调用我们可以暂时不用考虑。但是我们确确实实看到一个文件在传输过程中经历了很多次的拷贝,IO的性能本来就不是很高,所以后来又有了零拷贝、Nio等技术,这些知识点我们计划在附加课讲解。

2.按照操作单元划分

  • 字节流:是一个字节一个字节的读取或写入
  • 字符流:是一个字符一个字符的读取或写入,一个字符就是两个字节,主要用来处理字符。

3.按照角色划分

  • 节点流:直接从/向一个特定的IO设备(如磁盘,网络)读/写数据的流,称为节点流。
  • 处理流:“连接”在已存在的流(节点流或处理流)之上通过对数据的处理为程序提供更为强大的读写功能的流。

image.png

4.Java输入/输出流体系中常用的流的分类表

分类字节输入流字节输出流字符输入流字符输出流
抽象基类InputStreamOutputStreamReaderWriter
访问文件FileInputStreamFileOutputStreamFileReaderFileWriter
访问数组ByteArrayInputStreamByteArrayOutputStreamCharArrayReaderCharArrayWriter
访问字符串StringReaderStringWriter
缓冲流(处理)BufferedInputStreamBufferedOutputStreamBufferedReaderBufferedWriter
操作对象ObjectInputStreamObjectOutputStream

二、字节输出流(OutputStream)

1.字节输出流简介

  • 字节输出流:OutputStream(byte)
  • 抽象类,是所以输出字节流的超类

包含的成员方法:

public void close():关闭输出流并释放与此流相关联的任何系统资源。
public void flush():刷新此输出流并强制任何缓冲的输出字节被写出。
public void write(byte[] b):将b.length的字节从指定的字节数组写入此输出流
public void write(byte[] b,int off,int len):从指定的字节数组写入 len 字节,从偏移量 off 开始输出到此输出流
public abstract void write(int b):将指定的字节写入此输出流。 (一个字节)

2.子类:FileOutputStream

构造方法:

FileOutputStream(File file) 
创建文件输出流以写入由指定的 File对象表示的文件。 
FileOutputStream(String name) 
创建文件输出流以指定的名称写入文件。  
参数:写入数据的目的地。
    file:目的地是一个文件
    string:目的地是一个文件的路径
构造方法的作用:
    1.创建FileOutputStream对象。
    2.会根据构造方法中传递的文件/文件路径,创建一个文件(如果存在则不会创建)
    3.会把FileOutputStream对象指向创建好的文件(构造方法不是写入的方法,而是准备工作)

流的使用步骤

  • 1.创建FileOutputStream对象,构造方法中写,写入数据的目的地。
  • 2.调用FileOutputStream对象中的write方法,把数据写入到文件中
  • 3.释放资源
  • 4.写入数据原理:java程序通知java虚拟机JVm,通过虚拟机通知操作系统,操作系统调用写入数据的方法,把数据写入

注意:使用时程序会抛出异常,需要对异常进行处理 代码实现:

import java.io.FileOutputStream;
import java.io.IOException;
public class Java_io_01 {
    //将异常从方法中抛出
    public static void main(String[] args) throws IOException {
        FileOutputStream fos=new FileOutputStream("C:\\Users\\邹飞鸣\\Desktop\\学习资料\\2020.7.14(暑假)\\markdown学习笔记\\javaIO流笔记\\练习Out流.txt");
        fos.write('s');      
        fos.close();
    }
}
// 结果:在相应的文件夹下面产生了一个对应的文件,并写入了's'

注意:1、存储的数会以二进制形式存储下来,如果存储97这个数值时,真正存储的是字符’a’,即二进制会转换成ASCII表中对应的数值,如果数值超过127,则会去查找系统默认的码(GBK等等)并表示。2、如果是(-97,-98,-97,98)这种情况,98一个是正数,一个是负数,即使是这样-97-98和-9798所表示的值还是不一样。

一次输入多个字节

 -  public void write(byte[] b):将b.length的字节从指定的字节数组写入此输出流
 -  public void write(byte[] b,int off,int len):从指定的字节数组写入 len 字节,从偏移量 off 开始输出到此输出流
    两个方法,如果第一个字节是负数(如:-1,2),那么第一个字节会和第二个字节合并作为一个中文显示,查询的是系统默认码表

代码实现:

// 1.  public void write(byte[] b):将b.length的字节从指定的字节数组写入此输出流
import java.io.FileOutputStream;
import java.io.IOException;
public class Java_io_01 {
    public static void main(String[] args) throws IOException {
        byte[] bytes={-97,-98,-97,98,101,102};
        FileOutputStream fos=new FileOutputStream("javaIO流Out类测试文件.txt");
        fos.write(bytes);
        fos.close();
    }
}
-----------------------------------------------------------------------------------------------------------
// 2.public void write(byte[] b,int off,int len):从指定的字节数组写入 len 字节,从偏移量 off 开始输出到此输出流 
// 这个方法可以指定输入的字节
// off:在数组中,开始写入字节的位置
// len:写入字节的位数
// 代码实现:
import java.io.FileOutputStream;
import java.io.IOException;
public class Java_io_01 {
    public static void main(String[] args) throws IOException {
        byte[] bytes={-97,-98,-97,98,101,102};
        FileOutputStream fos=new FileOutputStream("javaIO流Out类测试文件.txt");
        //定义字节录入位置和长度
        fos.write(bytes,0,2);
        fos.close();
    }
}

可以写入字符串

  • 字符串类中有一个将字符串转换成字节数组的方法:byte[] getBytes();
  • 转换后就可以作为字节利用write方法录入文件了。

换行写以及续写 续写:

// 每次重新运行程序,如果有相同文件名,是直接覆盖原来的文件及其内容。那如果想不覆盖而是在其后继续录入字符串就需要用到这个类的另外两个构造方法:
//       FileOutputStream(String name, boolean append) 创建文件输出流以指定的名称写入文件。
//       FileOutputStream(File file, boolean append) 创建文件输出流以写入由指定的 File对象表示的文件。 
//       参数:append作用是一个开关,为true时创建对象不会覆盖源文件,而是继续在文件的末尾追加数据,为false时,创建新文件,覆盖源文件
// 代码:
import java.io.FileOutputStream;
import java.io.IOException;
public class Java_io_01 {
    public static void main(String[] args) throws IOException {
        byte[] bytes={97,98,97,98,101,102};
        //正常录入
        FileOutputStream fos=new FileOutputStream("javaIO流Out类测试文件.txt");
        fos.write(bytes,0,2);
        fos.close();
        //打开续写开关后的录入
        FileOutputStream fos1=new FileOutputStream("javaIO流Out类测试文件.txt",true);
        fos1.write(bytes,0,2);
        fos1.close();
    }
}
// 结果: abab

换行:

  • 写换行符,windows的是:\r\n ,Linux是:/n
  • 注意:换行符只能作为字符串写入。
import java.io.FileOutputStream;
import java.io.IOException;
public class Java_io_01 {
    public static void main(String[] args) throws IOException {
        byte[] bytes={97,98,97,98,101,102};
        FileOutputStream fos=new FileOutputStream("javaIO流Out类测试文件.txt");
        for (byte i:bytes){
            fos.write(i);
            //用字符串来调用方法,将字符串转换成字节数组
            fos.write("\r\n".getBytes());
        }
        fos.close();
    }
}
// 结果:
 //    a
 //    b
 //    a
 //    b
 //    e
 //    f

三、字节输入流(OutputStream)

1.字节输入流简介

共性方法:

  • int read() 从该输入流读取一个字节的数据。 读取完成后最后会返回一个-1
  • int read(byte[] b) 从该输入流读取最多 b.length个字节的数据为字节数组。
  • void close() 关闭此文件输入流并释放与流相关联的任何系统资源。
  • 还有其他的...

2.子类:FileInputStream

简介:

  • 文件字节输入流
  • 作用:把硬盘文件中的数据,读取到内存中

构造方法:

  • FileInputStream(File file) 通过打开与实际文件的连接创建一个 FileInputStream,该文件由文件系统中的 File对象 file命名。
  • FileInputStream(String name) 通过打开与实际文件的连接来创建一个 FileInputStream,该文件由文件系统中的路径名 name命名。
  • 作用:会创建一个FileInputStream对象并指向构造方法中要读取的文件

读取文件内容: 创建FileInputStream对象,调用read方法。 代码实现:

import java.io.FileInputStream;
import java.io.IOException;
public class Java_Io_02 {
    public static void main(String[] args) throws IOException {
        int i=0;
        FileInputStream fis=new FileInputStream("javaIO流Out类测试文件.txt");
        //判断文件是否为空,若不空,则读取。
       while ((i=fis.read())!=-1){
            System.out.print(i+",");
        }
       fis.close();
    }
}
// 结果:
//     97,13,10,98,13,10,97,13,10,98,13,10,101,13,10,102,13,10,
// 原因:文件中本来只存储了,
// a
// b
// a
// b
// e
// f
// 但是除了会自动转换成int类型外,多出来的13,10是换行符,起换行作用,且文件中没有显示,但是存在

注意:记得释放资源

一次读取多个字节:

  • 将字节数组全部内容转换为字符串:String(byte[] bytes) 通过使用平台的默认字符集解码指定的字节数组来构造新的String 。(是String类的构造方法,还有转换部分字节的构造方法,这里撇下不表)
  • 创建FileInputStream对象,创建字节数组接收,字节数组的大小就是一次接收的多少,int read(byte[] b)方法的返回值是:读取的有效字节数。

代码实现:

import java.io.FileInputStream;
import java.io.IOException;
public class Java_Io_02 {
    public static void main(String[] args) throws IOException {
        //定义i为判断返回值是否为-1,即是否取完
        int i=0;
        FileInputStream fis = new FileInputStream("javaIO流Out类测试文件.txt");
        //定义字节数组,大小是2的倍数最好
        byte[] bytes = new byte[1024];
        //调用read方法取字节存储到字节数组中,并将有效字节数返回并赋值给i
       while((i=fis.read(bytes))!=-1){
           //将字节数组中的值取出来用String的构造函数转换成字符串数组,取值范围是0到i(i为有效值)
           System.out.print(new String(bytes,0,i));
       }
        fis.close();
    }
}

3.文件赋值(字节)

import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
public class Java_Io_02 {
    public static void main(String[] args) throws IOException {
        //首先输入到内存,即用输入类
        FileInputStream fis=new FileInputStream("C:\\Users\\邹飞鸣\\Desktop\\学习资料\\2020.7.14(暑假)\\markdown学习笔记\\javaSE笔记\\JavaSE基础语法笔记.md");
        //其次输出到硬盘,即用输出类
        FileOutputStream fos=new FileOutputStream("javaIO流Out类测试文件.md");
        int i;
        byte[] bytes=new byte[1024];
        //i返回的是有效文件个数
        while ((i=fis.read(bytes))!=-1){
            fos.write(bytes,0,i);
        }
        //先关文件输出流
        fos.close();
        fis.close();
    }
}

四、字符(Char)

1.输入流Reader

共性成员方法:

  • int read() 读一个字符
  • int read(char[] cbuf) 将字符读入数组。
  • abstract int read(char[] cbuf, int off, int len) 将字符读入数组的一部分。
  • abstract void close() 关闭流并释放与之相关联的任何系统资源。

2.FileReader输入流

  • 继承自InputStreamReader,InputStreamReader继承自Reader

构造方法:

  • FileReader(File file) 创建一个新的 FileReader ,给出 File读取。
  • FileReader(String fileName) 创建一个新的 FileReader ,给定要读取的文件的名称。
  • 作用:创建此类的对象并指向要读的文件

使用字符流读取文件中字符的步骤

  • 创建类的对象,使用read方法读取文件内容,方法返回值是int,接收并输出

代码示例:

import java.io.FileReader;
import java.io.IOException;
public class Java_Io_03 {
    public static void main(String[] args) throws IOException {
        FileReader fr=new FileReader("字符测试文件.txt");
        int len;
        while ((len=fr.read())!=-1){
            System.out.print((char)len);
        }
    }
}

读取多个字符:

import java.io.FileReader;
import java.io.IOException;
public class Java_Io_03 {
    public static void main(String[] args) throws IOException {
        FileReader fr=new FileReader("字符测试文件.txt");
        int len;
        char[] chars=new char[1024];
        //用字符数组装字符,并用String类中构造方法将字符转换成字符串
        //String(char[] value, int offset, int count) 分配一个新的 String ,其中包含字符数组参数的子阵列中的字符。 
        while ((len=fr.read(chars))!=-1){
            System.out.print(new String(chars,0,len));
        }
    }
}

3.字符输出流Writer

成员方法:

  • abstract void close() 关闭流,先刷新。
  • abstract void flush() 刷新流。
  • void write(char[] cbuf) 写入一个字符数组。
  • abstract void write(char[] cbuf, int off, int len) 写入字符数组的一部分。
  • void write(int c) 写一个字符。
  • void write(String str) 写一个字符串。
  • void write(String str, int off, int len) 写一个字符串的一部分。

4.FileWriter

  • FileWriter 继承 OutputStreamWriter 继承 Writer

构造方法:

  • FileWriter(File file) 给一个File对象构造一个FileWriter对象。
  • FileWriter(File file, boolean append) 给一个File对象构造一个FileWriter对象。
  • FileWriter(String fileName) 构造一个给定文件名的FileWriter对象。
  • FileWriter(String fileName, boolean append)构造一个FileWriter对象,给出一个带有布尔值的文件名,表示是否附加写入的数据。

文件写入步骤

  • 创建此类对象,构造方法写文件路径,调用write方法把数据写入到内存缓存区中(此间会将字符转换为字节),然后使用flush方法把内存缓冲区中的数据刷新到文件中,之后释放资源(释放资源也会把缓冲区数据刷新到文件中)
import java.io.FileWriter;
import java.io.IOException;
public class J04 {
    public static void main(String[] args) throws IOException {
        FileWriter fw=new FileWriter("字符测试文件.txt");
        fw.write("知识点");
        fw.flush();//刷新缓冲区数据到文件中
        fw.close();
    }
}
// 结果:
//     文件中显示:知识点

续写和换行

  • 续写:用有追加写开关的构造方法,即可,和字节续写相同,换行也相同。

五、缓冲流(增强流对象)

1.字节输出缓冲流(BufferedOutputStream)

  • BufferedOutputStream(OutputStream out) 继承 OutputStream
  • 相当于将要输入的全部字节存储到某个地方,一次性发送到指定的地方,使虚拟机和操作系统间交互尽可能少,以提高效率。
  • 构造方法: BufferedOutputStream(OutputStream out) 创建一个新的缓冲输出流,以将数据写入指定的底层输出流 BufferedOutputStream(OutputStream out,int size) 创建一个新的缓冲输出流,以将具有指定缓冲区大小的数据写入指定的底层输出流
  • 当传递一个输出流对象后缓冲流会给这个对象增加一个缓冲区,提高FileOutputStream的写入效率,size可以指定也能默认。

使用: 创建FileOutputStream对象,构造方法中绑定输出地址,然后将创建的对象作为参数创建BufferedOutputStream类的对象,使用缓冲流的对象对文件进行写入操作,然后刷新缓冲区(flush),然后释放资源()

import java.io.BufferedOutputStream;
import java.io.FileOutputStream;
import java.io.IOException;
public class Io_06 {
    public static void main(String[] args) throws IOException {
        FileOutputStream fos=new FileOutputStream("缓冲流测试.txt");
        BufferedOutputStream bos=new BufferedOutputStream(fos);
        bos.write("测试缓冲流".getBytes());
        bos.flush();
        bos.close();
    }
}

2.字节输入缓冲流(BufferedInputStream)

  • 继承自InputStream

构造方法:

  • BufferedInputStream(InputStream in) 创建一个BufferedInputStream并保存其参数,输入流 in ,供以后使用。
  • BufferedInputStream(InputStream in, int size) 创建BufferedInputStream具有指定缓冲区大小,并保存其参数,输入流 in ,供以后使用。

代码示例:

import java.io.*;
public class Io_06 {
    public static void main(String[] args) throws IOException {
        FileInputStream fis=new FileInputStream("缓冲流测试.txt");
        BufferedInputStream bis=new BufferedInputStream(fis);
        //是显示字符串,所以先创建字节数组,将输入的字节存储到字节数组中,然后通过Sting的构造方法将字节数组转换为字符串输出
        int len;
        byte[] bytes=new byte[1024];
        while ((len=bis.read(bytes))!=-1){
            System.out.println(new String(bytes));
        }
        bis.close();
    }
}

3.字符缓冲输出流(BufferedWriter)

  • 继承自Writer

构造方法:

  • BufferedWriter(Writer out) 创建使用默认大小的输出缓冲区的缓冲字符输出流。
  • BufferedWriter(Writer out, int sz) 创建一个新的缓冲字符输出流,使用给定大小的输出缓冲区。

成员方法(特有):

  • void newLine() 写一行行分隔符。

实现代码:

import java.io.BufferedWriter;
import java.io.FileWriter;
import java.io.IOException;
public class Io_07 {
    public static void main(String[] args) throws IOException {
        FileWriter fw=new FileWriter("缓冲流测试文件.txt");
        BufferedWriter bw=new BufferedWriter(fw);
        bw.write("zdhd");
        bw.newLine();
        bw.write("shd");
        bw.flush();
        bw.close();
    }
}
// 结果:
//     zdhd
//     shd

4.字符缓冲输入流(BufferedReader)

  • 继承自Reader

构造方法:

  • BufferedReader(Reader in) 创建使用默认大小的输入缓冲区的缓冲字符输入流。
  • BufferedReader(Reader in, int sz) 创建使用指定大小的输入缓冲区的缓冲字符输入流

特有的成员方法

  • String readLine() 读一行文字。
  • 以换行符('\n'),回传('\r')或者回车后直接跟着换行(\r\n)作为结束行的标志
  • 如果读取到的这一行无数据了,会返回null值,其他时候返回的是读取的数据(但是不会读取行的终止符合,即如果本来有换行,读取后是不会读取换行符)

代码示例:

import java.io.BufferedReader;
import java.io.FileReader;
import java.io.IOException;
public class Io_08 {
    public static void main(String[] args) throws IOException {
        BufferedReader br=new BufferedReader(new FileReader("缓冲流测试文件.txt"));
        //读的是一行,所以只有一字符串为类型接收数据
        String str;
        while ((str=br.readLine())!=null){
            System.out.println(str);
        }
    }
}
// 结果:
//     zdhd
//     shd

六、对文件内容进行排序

文件内容:

1.测试缓冲流10
9.测试缓冲流2
2.测试缓冲流9
3.测试缓冲流8
6.测试缓冲流5
4.测试缓冲流7
5.测试缓冲流6
8.测试缓冲流3
10.测试缓冲流1
7.测试缓冲流4

排序代码:

import java.io.*;
import java.util.HashMap;
public class IO_00 {
    public static void main(String[] args) throws IOException {
        /*
        目的:将无序的10段话有序的录入进其他文件中
        1.使用字符缓冲输入流,获取文件中的数据。
        2.对数据进行切割,分离序号和文本内容
        3.对文件数据用HashMap集合进行存储,K为序号,V为文本内容
        4.遍历集合,将K,V值存储到指定文件
         */
        HashMap<String,String> hashMap=new HashMap<>();
        BufferedReader br=new BufferedReader(new FileReader("缓冲流测试.txt"));
        BufferedWriter bw=new BufferedWriter(new FileWriter("缓冲流测试排序.txt"));
        String str;
        //每次获取一行数据,返回值为null表示获取完成
        while ((str=br.readLine())!=null){
            //'.'是转义字符,前面必须加\\来表示后面的 . 是普通字符
            String[] strings=str.split("\\.");
            //将分割后的数据存储到集合中,K存储序号,V存储内容(HashMap会对数据按照K的数字大小进行排序)
            hashMap.put(strings[0],strings[1]);
        }
        //利用keySet方法将所有k值转换成set数组,并用增强for循环获取每个k值,然后利用get方法和k值获取每个v值
        for (String k:hashMap.keySet()) {
            //分割后,分割符会取消,所以需要手动添加
            bw.write(k+"."+hashMap.get(k));
            //换行,同样换行符也会取消,也需要手动添加
            bw.newLine();
        }
        br.close();
        bw.flush();
        bw.close();
    }
}

结果:

1.测试缓冲流10
2.测试缓冲流9
3.测试缓冲流8
4.测试缓冲流7
5.测试缓冲流6
6.测试缓冲流5
7.测试缓冲流4
8.测试缓冲流3
9.测试缓冲流2
10.测试缓冲流1

七、转换流

  • 作用:在文件进行写入或者读取操作时,文件是以字节形式存储在文件中,然后通过编码表对字节进行解码以呈现看得懂的文字,而就在在编码中,一般情况默认使用UTF-8编码,但是有的会使用GBK编码,如果编码录入,与解码读取所用的编码表不同,则会出现乱码情况,此时就可以使用转换流指定使用什么编码表进行读取或者写入操作。
  • OutputStreamWriter继承自Writer
  • InputStreamReader继承自Reader

构造方法:

  • 输出的: OutputStreamWriter(OutputStream out)创建一个使用默认字符编码的OutputStreamWriter。 OutputStreamWriter(OutputStream out, String charsetName) 创建一个使用命名字符集的OutputStreamWriter。
  • 输入的: InputStreamReader(InputStream in) 创建一个使用默认字符集的InputStreamReader 。InputStreamReader(InputStream in, String charsetName)创建一个使用命名字符集的InputStreamReader。
  • 参数:OutputStream,InputStream:分别是字节输出流和字节输入流; charsetName:编码名称,不区分大小写,默认为UTF-8

使用步骤:

  • 1.创建OutputStreamWriter对象,构造方法中new一个字节输出流,和指定编码。
  • 2.用OutputStreamWriter对象,调用方法写入数据,然后刷新缓冲区,释放资源
  • 3.创建InputStreamReader对象,构造方法new一个字节输入流,和指定编码。
  • 4.调用方法读取文件,释放资源。

实现代码:

import java.io.*;
public class Io_11 {
    public static void main(String[] args) throws IOException {
        OutputStreamWriter osw=new OutputStreamWriter(new FileOutputStream("转换流测试.txt"),"gbk");
        osw.write("转换流");
        osw.flush();
        osw.close();
        InputStreamReader isr=new InputStreamReader(new FileInputStream("转换流测试.txt"),"gbk");
        int i;
        while ((i=isr.read())!=-1){
            System.out.print((char) i);
        }
        isr.close();
    }
}

八、序列化与反序列化

  • 定义:用来将对象进行存储,将对象利用字节方式存储进文件为序列化,将对象从文件中读取叫反序列化。

1.ObjectInputStream和ObjectOutputStream

构造方法:

  • 输出的:ObjectOutputStream(OutputStream out) 创建一个写入指定的OutputStream的ObjectOutputStream。参数是字节输出流
  • 输入的:ObjectInputStream(InputStream in) 创建从指定的InputStream读取的ObjectInputStream。

特有方法:

  • 输出的:void writeObject(Object obj) 将指定的对象写入ObjectOutputStream。
  • 输入的:Object readObject() 从ObjectInputStream读取一个对象。

使用步骤:

  • 1.先创建一个类及其对象a,并实现接口Serializable(不实现是会抛出异常的)
  • 2.再创建ObjectOutputStream对象,调用writeObject方法,参数是a(即第一步创建的对象),把对象写入文件中
  • 3.释放资源
  • 4.创建ObjectInputStream对象,调用readObject方法,将对象读取出来,除了io异常外还有一个关于class文件的异常,即需要有这个文件,读取的对象用类型为Object的变量接收。
  • 5.释放资源,打印对象。

实现代码:

import java.io.*;
public class Io_12 {
    public static void main(String[] args) throws IOException, ClassNotFoundException {
        A a=new A("小新");
        ObjectOutputStream oos=new ObjectOutputStream(new FileOutputStream("序列化测试.txt"));
        oos.writeObject(a);
        oos.close();
        ObjectInputStream ois=new ObjectInputStream(new FileInputStream("序列化测试.txt"));
        Object obj=ois.readObject();
        ois.close();
        System.out.println(obj);
    }
}
//实现接口
class A implements Serializable{
    private String str;
    //定义有参构造方法一定还要定义一个无参的
    public A(){}
    public A(String str){
        this.str=str;
    }
    public String getStr() {
        return str;
    }
    public void setStr(String str) {
        this.str = str;
    }
//为了输出具体的数值 
    @Override
    public String toString() {
        return "A{" +
                "str='" + str + '\'' +
                '}';
    }
}
// 结果:
// A{str='小新'}

2.对象序列化

  • 序列化:将对象写入到IO流中,说的简单一点就是将内存模型的对象变成字节数字,可以进行存储和传输。
  • 反序列化:从IO流中恢复对象,将存储在磁盘或者从网络接收的数据恢复成对象模型。
  • 使用场景:所有可在网络上传输的对象都必须是可序列化的,否则会出错;所有需要保存到磁盘的Java对象都必须是可序列化的。

该对象必须实现Serializable接口,才能被序列化。

我们的

import java.io.Serializable;


public class User implements Serializable {
    private String name;

    private int age;

    private int gander;

    public User(String name, int age, int gander) {
        this.name = name;
        this.age = age;
        this.gander = gander;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        this.age = age;
    }

    public int getGander() {
        return gander;
    }

    public void setGander(int gander) {
        this.gander = gander;
    }
}
@Test
public void testObjectOut() throws Exception{
    //怼了一个string
    InputStream is = new FileInputStream("E:\test\a\user.txt");
    ObjectInputStream oi  = new ObjectInputStream(is);
    User user = (User)(oi.readObject());
    System.out.println(user.getName());
    is.close();
    oi.close();
}

3.序列化版本号

我们知道,反序列化必须拥有class文件,但随着项目的升级,class文件也会升级,序列化怎么保证升级前后的兼容性呢?

​ Java序列化提供了一个private static final long serialVersionUID 的序列化版本号,只要版本号相同,即使更改了序列化属性,对象也可以正确被反序列化回来。

public class Person implements Serializable {
    //序列化版本号
    private static final long serialVersionUID = 1111013L;
    private String name;
    private int age;
    //省略构造方法及get,set
}

​ 如果反序列化使用的版本号与序列化时使用的不一致,反序列化会报InvalidClassException’异常。

image.png

​ 序列化版本号可自由指定,如果不指定,JVM会根据类信息自己计算一个版本号,这样随着class的升级、代码的修改等因素无法正确反序列化;

​ 不指定版本号另一个明显隐患是,不利于jvm间的移植,可能class文件没有更改,但不同jvm可能计算的规则不一样,这样也会导致无法反序列化。

什么情况下需要修改serialVersionUID呢:

  • 如果只是修改了方法,反序列化不容影响,则无需修改版本号;
  • 如果只是修改了静态变量,瞬态变量(transient修饰的变量),反序列化不受影响,无需修改版本号。

4.反序列化序列号异常问题

当类实现Serializable后类会有一个序列号,这个序列号会随着对象进行序列化存储进文件中,当进行反序列化时会比较文件中的和类中的是否一致,如果一致才会读取,否则会抛出异常,而当类中代码发生改变后(如变量修饰符)序列号也是会改变的,而此时再读取已经被序列化的文件时,就会产生异常,所以可以将序列号固定不变,可防止此类问题:需在类中添加代码:

static final long serialVersionUID = 42L;

5.transient(瞬态关键字)

  • 作用,使修饰的变量不能够被序列化,即不能将赋好值的这个变量将值随着对象一起被存储进去。
  • static修饰后也不能被序列化。

6.总结

  1. 所有需要网络传输的对象都需要实现序列化接口。
  2. 对象的类名、实例变量(包括基本类型,数组,对其他对象的引用)都会被序列化;方法、类变量、transient实例变量都不会被序列化。
  3. 如果想让某个变量不被序列化,使用transient修饰。
  4. 序列化对象的引用类型成员变量,也必须是可序列化的,否则,会报错。
  5. 反序列化时必须有序列化对象的class文件。
  6. 同一对象序列化多次,只有第一次序列化为二进制流,以后都只是保存序列化编号,不会重复序列化。
  7. 建议所有可序列化的类加上serialVersionUID 版本号,方便项目升级。

Intellij idea用快捷键自动生成序列化id,类继承了Serializable接口之后,使用alt+enter快捷键自动创建序列化id

方法:进入setting→inspections→serialization issues→选择图中的选项。serializable class without ‘serialVersionUID’

image.png

7.深拷贝

(1)对象的引用改变:

image.png

public void deepCopyTest() throws  CloneNotSupportedException {
    User user = new User(12, "zhagnsna");
    user.setDog(new Dog(2));
    User user1 = user;

    user.getDog().setAge(23);
    System.out.println(user1);
}

(2)浅拷贝:实现clonable接口,重写clone方法。

image.png

public class User implements Serializable,Cloneable {


    @Override
    protected Object clone() throws CloneNotSupportedException {
        return super.clone();
    }
}

@Test
public void deepCopyTest() throws  CloneNotSupportedException {
    User user = new User(12, "zhagnsna");
    user.setDog(new Dog(2));
    User user1 = (User)user.clone();

    user.getDog().setAge(23);
    System.out.println(user1);
}

深拷贝:使用对象流先写入byte数组,再读出来。

image.png

@Test
public void deepCopyTest2() throws CloneNotSupportedException, IOException, ClassNotFoundException {
    User user = new User(12, "zhangsan");
    user.setDog(new Dog(2));

    // 将对象写到字节数组当中
    ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
    ObjectOutputStream objectOutputStream = new ObjectOutputStream(outputStream);
    objectOutputStream.writeObject(user);
    // 获取字节数组
    byte[] bytes = outputStream.toByteArray();
    // 用输入流读出来
    ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(bytes);
    ObjectInputStream objectInputStream = new ObjectInputStream(byteArrayInputStream);
    Object object = objectInputStream.readObject();
    User user1 = (User) object;

    user.setAge(44);
    user.getDog().setAge(11);
    System.out.println(user);
    System.out.println(user1);

}

九、流的案例

1.继承结构

InputStream和OutputStream:

image.png

Reader和Writer:

image.png

2.流到底怎么用

(1)将一个流对象插在一个节点上:

其实通过名字我们就可以很好的理解了:FileInputStream就是怼在文件上的输入流啊!

public abstract class InputStream implements Closeable

InputStream本身是抽象类,我们需要使用它的子类去构造对象:

InputStream inputStream = new FileInputStream(file);

既然是输入流就要一点一点的往内存里读数据啊!

image.png

其实inputStream的方法并不多,关键在于几个read方法,管子已经插上了,接下来就是读了。

// 读一个字节
int read = inputStream.read();

// 一次性读1024个字节到那个内存数组
int read = inputStream.read(new byte[1024]);

// 从第0个字节开始读,读120个字节
int read = inputStream.read(new byte[1024],0,120);

(2)使用read()方法读取

它的读取流程大概是这个样子的,inputStream内部有一个游标,它会记录目前读到哪里了,看下图:

image.png

我们不妨尝试一下:

我的D盘的code目录下新建一个文本:

image.png

我知道:read返回-1时就代表文件读完了,所以我写了如下代码:

public static void main(String[] args) throws IOException {
    InputStream inputStream = new FileInputStream("D:/code/a.txt");
    int read;
    while ((read =inputStream.read())  != -1){
        System.out.print(read+" ");
    }
}
72 101 108 108 111 32 87 111 114 108 100 33 
H  e   l   l   0      W  o   r   l   d   !

read就是每次读出的字节,直到-1就停止。

小tips:一个流我读完了一次还能读第二次吗?

 public static void main(String[] args) throws IOException {
        InputStream inputStream = new FileInputStream("D:/code/a.txt");
        int read;

        while ((read =inputStream.read())  != -1){
            System.out.print(read+" ");
        }
        System.out.println("再读一次---------------");
        while ((read =inputStream.read())  != -1){
            System.out.print(read+" ");
        }
    }
    
    72 101 108 108 111 32 87 111 114 108 100 33 
再读一次---------------

我们发现一个流读完了就没有了,就不能在读了。当然文档里有mark和reset方法,我们在系统中测试是不可用的。

System.out.println(inputStream.markSupported());
public static void main(String[] args) throws IOException {
    InputStream inputStream = new FileInputStream("D:/code/a.txt");
    int read;

    byte[] buf = new byte[3];
    while ((read =inputStream.read(buf))  != -1){
        System.out.print(read+" ");
    }
}

image.png

(3)使用read(byte[] byte)读取

public static void main(String[] args) throws IOException {
    InputStream inputStream = new FileInputStream("D:/code/a.txt");
    int read;

    byte[] buf = new byte[3];
    while ((read =inputStream.read(buf))  != -1){
        System.out.print(read+" ");
    }
}

image.png

我们想向深入走一步,看看源码:

但是发现,源码目前位置看不了了,这些方法都带有native,这更加说明了读文件一定是JVM调用系统方法读取的。

image.png

(4)输出流的使用

我们要学会举一反三,其实他们的区别就是一个读,一个写嘛,我写一个例子就好了。

有一个小知识点:

​ 在定义文件输出流时,有两个参数,第二个如果是true代表追加文件,如果false代表覆盖文件,意思就是如果人家这个文件原来有内容,就覆盖的没了,这一点要注意。

OutputStream outputStream = new FileOutputStream("D:/code/a.txt",true);
OutputStream outputStream = new FileOutputStream("D:/code/a.txt",true);
// 一个字节一个字节的写
outputStream.write(97);

// 97是一个字节啊

我们发现文件中被写入的是一个a

image.png

我们不妨升级一下,一个文件的拷贝程序就写好了。

public static void main(String[] args) throws IOException {
    InputStream inputStream = new FileInputStream("D:/code/a.txt");
    OutputStream outputStream = new FileOutputStream("D:/code/b.txt",true);

    byte[] buf = new byte[3];
    int len;
    while ((len =inputStream.read(buf))  != -1){
        outputStream.write(buf,0,len);
    }
}

(5)资源的释放

一个IO流的标准写法是什么呢?

我们发现IO有以下几点需要我们处理:

1、绝大部分的对IO的操作都需要处理可能出现的IO异常。

image.png

2、我们发现不管是inputStream还是outputStream都有一个close方法,IO是需要消耗系统资源的,每一个stream都需要系统分配资源,是弥足珍贵的,所以没有流一旦使用完成就一定要关闭资源。

经过反复修改我们写出了如下代码:

 public static void main(String[] args) {
        // 定义资源
        InputStream inputStream = null;
        OutputStream outputStream = null;
        try {
            inputStream = new FileInputStream("D:/code/a.txt");
            outputStream = new FileOutputStream("D:/code/b.txt",true);

            byte[] buf = new byte[3];
            int len;
            while ((len =inputStream.read(buf))  != -1){
                outputStream.write(buf,0,len);
            }
        }  catch (IOException e) {
            e.printStackTrace();
         // 最终无论如何,都要释放资源
        } finally {
            if(inputStream != null){
                try {
                    inputStream.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
            if(outputStream != null){
                try {
                    outputStream.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }

【AutoCloseable接口的好处】

以上代码如此繁杂,jdk1.7之后,很多资源类的类都实现了AutoCloseable接口

实现了这个接口的类可以在try中定义资源,并会主动释放资源:

这样就极大的简化了代码的编写,但是你这么写了可能会有人看不懂呦!

public static void main(String[] args) {
    try(InputStream inputStream = new FileInputStream("D:/code/a.txt");
        OutputStream outputStream= new FileOutputStream("D:/code/b.txt",true)) {
        byte[] buf = new byte[3];
        int len;
        while ((len =inputStream.read(buf))  != -1){
            outputStream.write(buf,0,len);
        }
    }  catch (IOException e) {
        e.printStackTrace();
    }
}

后记

本文呢为大家介绍了Java基础IO流的相关知识,首先为大家介绍了IO流的先相关概念,然后详细介绍了包含字节输出流与字节输入流;字符;缓冲流;对文件内容进行排序;转换流;序列化与反序列化等Java IO基础相关知识,最后又为大家展示了流的相关使用案例。

希望本文的分享能够使你有所收获。如果你想继续深入的了解学习Java相关的知识与技术,可以参考:

Java全栈学习路线可参考:【Java全栈学习路线】最全的Java学习路线及知识清单,Java自学方向指引,内含最全Java全栈学习技术清单~

看完不关注就想跑.gif