Java中的IO

作者: sixleaves | 来源:发表于2018-08-02 16:05 被阅读2次

    什么是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没有序列化

    流的使用总结

    • 创建节点流和相应的节点关联
    • 根据需要的功能的创建处理流, 对节点流进行增强.
    • 进行输入输出操作
    • 关闭流
      • 关闭流是有顺序要求, 要先关闭处理流在关闭对应的节点流.
      • 处理流关闭时, 其实也会取关闭对应的节点流, 所以关闭节点流也可以不写.

    相关文章

      网友评论

        本文标题:Java中的IO

        本文链接:https://www.haomeiwen.com/subject/tfrjvftx.html