Java中的IO

什么是IO

IO顾名思义就是Input和Output.主要是以程序作为参照物来定义.

  • 研究的是数据从内存中(程序)输出(Output)到节点中(文件、网络等)的方式
  • 研究的是数据从节点中读取入内存(程序中的)方式

Java中对于IO的分类

所谓分类就是根据特点可以对事物进行归类.而对IO进行分类, 也就是以IO类的特点进行分类.先看如下图

流的分类

按流方向划分

可以划分为输入和输出流

按流操作的数据单位划分

可以划分为字节流和字符流。
字节流能操作读写任意类型文件, 字符流只能操作文本文件

按流的角色划分

可以分为节点流和处理流。

  • 字节流和字符流都是节点流。
  • 处理流建立在节点流之上, 是对节点流的包装,进一步争强其功能.(比如有字节转字符流的包装流等)

四大IO抽象类和常用的节点流处理流

抽象类 节点流 处理流(提供的是缓冲功能)
InputStream FileInputPutStream BufferedInputStream
OutputSteam FileOutputStream BufferedOutputStream
Reader FileReader BufferedReader
Writer FileWriter BufferedWriter
  • 实际应用中,我们的流肯定要和节点(文件、网络等统称为几点)发送关系,所以和节点直接建立练习的流称为节点流,节点流主要和文件节点建立链接.
  • 再按流处理数据的单位分为字节流和字符流。接着再按数据的方向分为字节输入输出流,字符输入输出流。
  • 最后由于节点流都只实现了最小的核心功能。而此时就诞生了处理流, 可以节点流的基础上进一步包装,所以其是依赖于节点流,但是功能更加的强大.

FileInputStream

其常用的API

  • read() 读取一个字节
  • read(byte[]) 读取多个字节, 具体长度为min(byte.length, 文件剩余的字节大小),并将这些字节存入byte数组
  • read(byte[], off, len) 读取多个字节, 将这些字节存入byte数组.存入位置为off,长度为min(len, 文件剩余的字节)

示例代码

   /**
     * read(buffer, off, len)
     */
    @Test
    public void testInputStreamWithBytesArray2() {
        File file = new File("1.txt");
//        System.out.println(file.getAbsolutePath());
        InputStream is = null;
        try {
            is = new FileInputStream(file);
            int len = 3;
            int off = 0;
            int totalLength = 0;
            byte[] buffer = new byte[100];

            while ((len = is.read(buffer, off, len)) != -1) {
                off += len;
                System.out.print("len = " + len);
            }

            for (int i = 0; i < off; i++) {
                System.out.print((char)buffer[i]);
            }

        } catch (IOException e) {
            e.printStackTrace();
        } finally {

            if (is!=null) {
                try {
                    is.close();
                }catch (IOException e) {

                }
            }

        }
    }


    @Test
    public void testInputStreamWithBytesArray() {

        File file = new File("1.txt");
//        System.out.println(file.getAbsolutePath());
        InputStream is = null;
        try {
            is = new FileInputStream(file);
            int len = -1;
            byte[] buffer = new byte[5];
            while ((len = is.read(buffer)) != -1) {
                System.out.print("len = " + len);
                for (int i = 0; i < len; i++) {
                    System.out.print((char)buffer[i]);
                }

            }

        } catch (IOException e) {
            e.printStackTrace();
        } finally {

            if (is!=null) {
                try {
                    is.close();
                }catch (IOException e) {

                }
            }

        }
    }

 @Test
    public void testInputStreamWithSingleByte() {
        File file = new File("1.txt");
//        System.out.println(file.getAbsolutePath());
        InputStream is = null;
        try {
            is = new FileInputStream(file);

            int len = -1;

            while ((len = is.read()) != -1) {
                System.out.print((char)len);
            }

        } catch (IOException e) {
            e.printStackTrace();
        } finally {

            if (is!=null) {
                try {
                    is.close();
                }catch (IOException e) {

                }
            }

        }
    }


源码解析

read()和read0
read(byte b[], int off, int len)
readBytes(byte b[], int off, int len)

跟踪FileInputStream起代码可见其调用的是native的read0方法。证明该方法是一个系统调用, 频繁调用的话效率和性能会很低。
而后两者是对native类型的方法readBytes(byte[], off, len)的一层包装,这个方法也是一个系统调用,但它可以次调用读取多个
这就降低了系统调用带来的性能开销, 提高了程序性能

文件字节输入流总结

  • 使用FileInputStream按字节读取文件, 当读取到文件尾部会返回-1.
  • 如果读取结束, 需要调用close方法关闭文件资源.
  • read()方法本质上是调用系统的read0.read(byte[])和read(byte[], off, len)本质上是对native方法readBytes(byte[], off, len)的封装
  • 如果读取的文件不存在会抛出FileNotFoundException异常.关闭文件资源可能抛出IOException异常.

FileOutputStream

  • write(int) 写一个字节到对应的文件
  • write(byte[]) 写一个字节数组到对应的文件
  • write(byte[], off, len) 将一个字节数组从off位置开始, 取len长度,写到对应的文件

示例代码

@Test
    public  void testOutputStream() {

        File file = new File("testOutput.txt");
        OutputStream os = null;
        try {
           os = new FileOutputStream(file, true);
           os.write("I love Beijing".getBytes());

        } catch (FileNotFoundException e) {
            e.printStackTrace();
        } catch (IOException e) {

        }finally {
            if (null != os) {
                try {
                    os.close();
                }catch (IOException e) {

                }
            }
        }
    }

文件的写操作和读差不多,其接口使用就不一一列举。直接总结

文件字节输出流总结

  • 文件字节输出流的常用的三个接口是write(int)、write(byte[])、write(byte[], off, len).这三个接口具有可能抛出IOException
  • write(int)接口底层调用的是native方法write(int b, boolean append).而write(byte[])、write(byte[],off,len)底层调用都是native方法writeBytes(byte[], off, len).所以使用后两者写入字节数据的性能会更高
  • 文件字节输出流操作完后,要调用close方法进行关闭.该方法也可能抛出IOException
  • 创建输出流的时候,如果关联的文件不存在,并不会抛出FileNotFoundException

FileReader

FileReader接口和FileInputStream基本一样

示例代码


    /**
     *
     * FileReader继承自InputStreamReader
     */
    @Test
    public void testFileReader() {

        File file = new File("2.txt");
        FileReader fr = null;
        try {
            fr = new FileReader(file);

            int len;
            char[] buf = new char[100];
            while ((len = fr.read(buf)) != -1) {
                String str = new String(buf, 0, len);
                System.out.print(str);
            }

        } catch (FileNotFoundException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        }finally {

            if (null != fr) {
                try {
                    fr.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }

        }
    }

FileWriter

FileWriter接口和FileInputStream基本一样

示例代码

  @Test
    public void testFileReaderAndWriter() {

        FileReader fr = null;
        FileWriter fw = null;

        try {
            fr = new FileReader("2.txt");
            fw = new FileWriter("2_copy.txt");

            int len;
            char[] buf = new char[100];
            while ((len = fr.read(buf)) != -1) {
                fw.write(buf, 0, len);
            }

        } catch (FileNotFoundException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        }finally {

            try {
                if (null != fw)
                    fw.close();
                if (null != fr)
                    fr.close();
            }catch (IOException e) {

            }

        }
    }

BufferedReader和BufferedWriter

  • BufferedReader、BufferedWriter的接口和FileReader、FileWriter基本一样.
  • BufferedReader有一个能直接按行读取的接口readLine().如果读到行尾该接口返回null

示例代码

    @Test
    public void testBufferedReaderAndWriter() throws IOException {


        FileReader fr = new FileReader("2.txt");
        FileWriter fw = new FileWriter("bufferedWriterCopy3.txt");


        BufferedReader br = new BufferedReader(fr);
        BufferedWriter bw = new BufferedWriter(fw);

        int len;
        char[] buf = new char[100];
        // 方式一
//        while ((len = br.read(buf)) !=-1) {
//            bw.write(buf, 0, len);
//        }

        // 方式二
        String line;
        while ((line = br.readLine()) != null) {
            bw.write(line+"\n");
        }

        bw.close();
        br.close();
        fw.close();
        fr.close();

    }

转换流(处理流二)

转换流主要实现了字节流和字符流之间的转换.主要涉及到以下两个流。对于程序来说,输入输出都是字符串表示。只有从磁盘刚读出后者要写入磁盘才会转成字节
涉及的流

  • InputStreamReader
    将字节输入流转成字符输入流
  • OutputStreamWriter
    实现字符的输出流转换为字节的输出流

编码解码

  • 编码过程: 字符串、字符数组 转换成 字节数组
  • 解码过程: 字节数组 转换成 字符串、字符数组

转换流使用步骤

  • 创建对应的字节流(要读取就创建输入字节流, 要写入就创建输出字节流).
  • 创建对应的转换流,将字节流转换成字符流,并指定对应要编码或解码的字符集
  • 调用read或者write方法进行读取和写入.

常见的编码表名称

常见的编码集

示例代码
将GBK编码的文件读取出来, 并转换成UTF-8编码写出到新文件


    @Test
    public void testISRAndOSW() throws IOException {
        
        // 读取出来的时候也是用字节
        FileInputStream fis = new FileInputStream("test_gbk.txt");
        InputStreamReader isr = new InputStreamReader(fis, "GBK");

        // 写入的时候是用字节
        FileOutputStream fos= new FileOutputStream("test_utf8.txt");
        OutputStreamWriter osw = new OutputStreamWriter(fos, "UTF-8");

        int len;
        char[] buf = new char[100];
        while ((len = isr.read(buf)) != -1) {
            String line = new String(buf, 0, len);
            System.out.print(line);
            osw.write(buf, 0, len);
        }
        isr.close();
        fis.close();

        osw.close();
        fos.close();
    }

注意

  • 面向程序的始终是字符串,输出到节点的始终是字节信息。
  • 从节点到程序,是先经过字节流再转为字符流
  • 从程序到节点,是先通过字符流再转为字节流

标准输入输出流(处理流三)

打印流(处理流四)

PrintStream\PrintWriter都是输出流

打印流也是处理流, 其争强了字节输出流的功能. System.out是PrintStream的实例, 其关联的是键盘节点

  • 提供了8大基本基本数据类型的打印, Object对象的打印和, 字符串数组的打印。

  • 分别重载了print和println().后者功能是多一个换行.

  • PrintStream和PrintWriter的输出不会抛异常

  • PrintStream和PrintWriter有自动flush功能

print和println

示例代码

使用打印流PrintStream包装字节输出流,提供各种数据类型的便利输出方法


    /**

     * PrintStream

     *

     */

    @Test

    public void testPrintStream() throws FileNotFoundException {

        // 创建一个PrintStream对象,将其关联到printStream.txt节点

        FileOutputStream fos = new FileOutputStream("printStream.txt");

        PrintStream ps = new PrintStream(fos, true);

        if (ps != null) {

            System.setOut(ps);

        }

        System.out.println("我爱北京!!!!");

    }

运行后创建的文件

System.out重定向示例

数据流(处理流五)

数据流主要用来序列化java字符串和基本数据类型

为什么不直接使用字节流和字符流读写基本数据类型

因为如果你想使用FileOutputStream或者FileWriter来将java中的数据输出, 对于FileOutputStream需要将所有的基本数据类型转为字节或者字节数组。而FileWriter你需要将所有基本数据类型转为字符串。实现起来就十分麻烦。

数据流的特点

  • 数据流不保证线程安全
  • 数据输入流的方法基本上都是readXxx
  • 数据输出流的方法基本上都是writerXxx

DataInputStream中的方法

DataInputStream中的方法

示例代码

    @Test
    public void testDataOutputStream() throws IOException {

        String name = "张三";
        int age = 24;
        char gender = '男';
        double salary = 10000.0;

        FileOutputStream fos = new FileOutputStream("test_dataOutputStream.txt");
        DataOutputStream dos = new DataOutputStream(fos);
        dos.writeUTF(name);
        dos.writeInt(age);
        dos.writeChar(gender);
        dos.writeDouble(salary);
    }

test_dataOutputStream.txt文件


使用文本打开是乱码

之所以乱码, 是因为我们写入到文件中本质上也是字节, 但系统的文本编辑器并不知道java存储这些数据的志节规则,比如int它是序列化为几个字节.所以会乱码, 这时候如果我们还还原数据需要使用数据输入流来读出。

DataOutputStream中的方法

将上述的方法的read改为相应的write即可.其提供了将基本类型数据写入字节流的功能, 可以用数据输入流再读出.

使用数据输入流来读取上面生成的文件.读取的顺序要和写入的顺序一样.

示例代码


    @Test
    public void testDataInputStream() throws IOException {

        FileInputStream fis = new FileInputStream("test_dataOutputStream.txt");

        DataInputStream dis = new DataInputStream(fis);

        String name = dis.readUTF();
        int age = dis.readInt();
        char gender = dis.readChar();
        double salary = dis.readDouble();
        System.out.println("name = " + name);
        System.out.println("age = " + age);
        System.out.println("gender = " + gender);
        System.out.println("salary = " + salary);

    }

输出

DataOutputStream

总结数据流

  • 数据流是用来处理Java中String基本数据类型的读写
  • 数据流输出的字节流数据并不能用文本编辑器直接打开解析, 因为它有自己的存储规则.有点像序列化.
  • 数据输入流读取数据, 需要按照数据输出流写入的顺序读取

对象流(处理流六)

对象流是java中用来序列化对象和反序列化对象的流.其对应的类是ObjectOutputStream和ObjectInputStream.

序列化反序列化概念

  • 序列化对象就是将对象转成二进制数据进行传输或者存储.
  • 反序列化对象就是将二进制数据转换成对象在程序中使用.

对象流和数据流的异同

  • 数据流的相同点就是都是用来序列化数据, 并持久化存储。
  • 数据流的不同是其是用来序列化对象,数据流是用来序列化基本数据类型和字符串

序列化步骤

  • 要序列化的对象需要实现Serializable接口
  • 标记版本(定义final long serialVersionUID)
  • 创建对象流包装字节流, 使其能处理对象.(需要先创建字节流关联相应节点.)
  • 调用writeObject()方法序列化对象
public class TestObjectOutputStreamAndObjectInputStream {


    @Test
    public void testObjectSerialization() throws IOException {
        Student stu = new Student("苏轼", 20, '男', 10000.0);

        FileOutputStream fos = new FileOutputStream("stu.dat");
        ObjectOutputStream oos = new ObjectOutputStream(fos);
        oos.writeObject(stu);

        oos.close();        // 记得关闭流
        fos.close();
        /**
         * 流的操作步骤
         * 1.创建节点流和相应的节点关联
         * 2.根据需要的功能的创建处理流, 对节点流进行争强.
         * 3.进行输入输出操作
         * 4.关闭流
         */
    }

    @Test
    public void testObjectDeserialize() throws IOException, ClassNotFoundException {


        FileInputStream fis = new FileInputStream("stu.dat");
        ObjectInputStream ois = new ObjectInputStream(fis);

        Object o = ois.readObject();

        System.out.println(o);
        ois.close();
        fis.close();
    }
    
}

class Student implements Serializable {
    @Override
    public String toString() {
        return "Student{" +
                "name='" + name + '\'' +
                ", age=" + age +
                ", gender=" + gender +
                ", salary=" + salary +
                '}';
    }

    public Student(String name, int age, char gender, double salary) {
        this.name = name;
        this.age = age;
        this.gender = gender;
        this.salary = salary;
    }

    private String name;
    private int age;
    private char gender;
    private double salary;
    // 代码过长省略了getter和setter

transient

可以使用transient关键字声明对象中哪些字段不被序列化,以提高序列化和反序列化效率.

serialVersionUID

在对象进行序列化或反序列化操作的时候,要考虑JDK版本的问 题,如果序列化的JDK版本和反序列化的JDK版本不统一则就有可能造成异常。所以在序列化操作中引入了一个serialVersionUID的常量,可以通过此常量来验证版本的一致性,在进行反序列化时,JVM会把传来的字节流中的serialVersionUID与本地相应实体(类)的serialVersionUID进行比较,如果相同就认为是一致的,可以进行反序列化,否则就会出现版本不一致的异常。
但是在进行序列化或反序列化操作的时候,对于不同的JDK版本,实际上会出现版本的兼容问题。

Externalizable接口

作用

既然序列化可以使用Serializable接口, 为什么还有Externalizable接口?该接口的功能其实等效于 Serializable + transient.

两个方法
  • writeExternal(ObjectOutput out) 用来定制要序列化哪些属性
  • readExternal(ObjectInput in) 用来定制要反序列化哪些属性

示例代码,将Student类替换成实现Externalziable接口, 并实现上述两个方法。然后重新运行代码。

class Student implements Externalizable {

    private static final long serialVersionUID = 1L;

    public Student() {

    }

    public void writeExternal(ObjectOutput out) throws IOException {
        out.writeUTF(name);
        out.writeInt(age);
        out.writeChar(gender);
    }

    public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException {
        this.name = in.readUTF();
        this.age = in.readInt();
        this.gender = in.readChar();
    }

    @Override
    public String toString() {
        return "Student{" +
                "name='" + name + '\'' +
                ", age=" + age +
                ", gender=" + gender +
                ", salary=" + salary +
                '}';
    }

    public Student(String name, int age, char gender, double salary) {
        this.name = name;
        this.age = age;
        this.gender = gender;
        this.salary = salary;
    }


    private String name;
    private int age;
    private char gender;
    private transient double salary;
}

控制台输出
由于我们没有定制salary的序列化,所以反序列化出来的时候改字段默认值就是0.0

salary没有序列化

流的使用总结

  • 创建节点流和相应的节点关联
  • 根据需要的功能的创建处理流, 对节点流进行增强.
  • 进行输入输出操作
  • 关闭流
    • 关闭流是有顺序要求, 要先关闭处理流在关闭对应的节点流.
    • 处理流关闭时, 其实也会取关闭对应的节点流, 所以关闭节点流也可以不写.
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

推荐阅读更多精彩内容