• Java IO/NIO


    1、IO体系

    Java IO 体系看起来类很多,感觉很复杂,但其实是 IO 涉及的因素太多了。在设计 IO 相关的类时,编写者也不是从同一个方面考虑的,所以会给人一种很乱的感觉,并且还有设计模式的使用,更加难以使用这些 IO 类,所以特地对 Java 的 IO 做一个总结。

    IO 类设计出来,肯定是为了解决 IO 相关的操作的,想一想哪里会有 IO 操作?网络、磁盘。网络操作相关的类是在 java.net 包下,不在本文的总结范围内。提到磁盘,你可能会想到文件,文件操作在 IO 中是比较典型的操作。在 Java 中引入了 “流” 的概念,它表示任何有能力产生数据源或有能力接收数据源的对象。数据源可以想象成水源,海水、河水、湖水、一杯水等等。数据传输可以想象为水的运输,古代有用桶运水,用竹管运水的,现在有钢管运水,不同的运输方式对应不同的运输特性。

    从数据来源或者说是操作对象角度看,IO 类可以分为:

    • 1、文件(file):FileInputStream、FileOutputStream、FileReader、FileWriter
    • 2、数组([]):
      • 2.1、字节数组(byte[]):ByteArrayInputStream、ByteArrayOutputStream
      • 2.2、字符数组(char[]):CharArrayReader、CharArrayWriter
    • 3、管道操作:PipedInputStream、PipedOutputStream、PipedReader、PipedWriter
    • 4、基本数据类型:DataInputStream、DataOutputStream
    • 5、缓冲操作:BufferedInputStream、BufferedOutputStream、BufferedReader、BufferedWriter
    • 6、打印:PrintStream、PrintWriter
    • 7、对象序列化反序列化:ObjectInputStream、ObjectOutputStream
    • 8、转换:InputStreamReader、OutputStreWriter
    • 9、字符串(String)Java8中已废弃StringBufferInputStream、StringBufferOutputStream、StringReader、StringWriter
    从数据传输方式或者说是运输方式角度看,可以将 IO 类分为:
    • 1、字节流
    • 2、字符流

    字节流是以一个字节单位来运输的,比如一杯一杯的取水。而字符流是以多个字节来运输的,比如一桶一桶的取水,一桶水又可以分为几杯水。

    字节流和字符流的区别:

    字节流读取单个字节,字符流读取单个字符(一个字符根据编码的不同,对应的字节也不同,如 UTF-8 编码是 3 个字节,中文编码是 2 个字节。)字节流用来处理二进制文件(图片、MP3、视频文件),字符流用来处理文本文件(可以看做是特殊的二进制文件,使用了某种编码,人可以阅读)。简而言之,字节是个计算机看的,字符才是给人看的。

    字节流和字符流的划分可以看下面这张图。

    不可否认,Java IO 相关的类确实很多,但我们并不是所有的类都会用到,我们常用的也就是文件相关的几个类,如文件最基本的读写类 File 开头的、文件读写带缓冲区的类 Buffered 开头的类,对象序列化反序列化相关的类 Object 开头的类。

    2、IO类和相关方法

    IO 类虽然很多,但最基本的是 4 个抽象类:InputStream、OutputStream、Reader、Writer。最基本的方法也就是一个读 read() 方法、一个写 write() 方法。方法具体的实现还是要看继承这 4 个抽象类的子类,毕竟我们平时使用的也是子类对象。这些类中的一些方法都是(Native)本地方法、所以并没有 Java 源代码,这里给出笔者觉得不错的 Java IO 源码分析 传送门,按照上面这个思路看,先看子类基本方法,然后在看看子类中还新增了那些方法,相信你也可以看懂的,我这里就只对上后面说的常用的类进行总结。

    先来看 InputStream 和 OutStream 中的方法简介,因为都是抽象类、大都是抽象方法、所以就不贴源码喽!注意这里的读取和写入,其实就是获取(输入)数据和输出数据。

    InputStream 类

    方法方法介绍
    public abstract int read() 读取数据
    public int read(byte b[]) 将读取到的数据放在 byte 数组中,该方法实际上是根据下面的方法实现的,off 为 0,len 为数组的长度
    public int read(byte b[], int off, int len) 从第 off 位置读取 len 长度字节的数据放到 byte 数组中,流是以 -1 来判断是否读取结束的(注意这里读取的虽然是一个字节,但是返回的却是 int 类型 4 个字节,这里当然是有原因,这里就不再细说了,推荐这篇文章,链接
    public long skip(long n) 跳过指定个数的字节不读取,想想看电影跳过片头片尾
    public int available() 返回可读的字节数量
    public void close() 读取完,关闭流,释放资源
    public synchronized void mark(int readlimit) 标记读取位置,下次还可以从这里开始读取,使用前要看当前流是否支持,可以使用 markSupport() 方法判断
    public synchronized void reset() 重置读取位置为上次 mark 标记的位置
    public boolean markSupported() 判断当前流是否支持标记流,和上面两个方法配套使用

    OutputStream 类

    方法方法介绍
    public abstract void write(int b) 写入一个字节,可以看到这里的参数是一个 int 类型,对应上面的读方法,int 类型的 32 位,只有低 8 位才写入,高 24 位将舍弃。
    public void write(byte b[]) 将数组中的所有字节写入,和上面对应的 read() 方法类似,实际调用的也是下面的方法。
    public void write(byte b[], int off, int len) 将 byte 数组从 off 位置开始,len 长度的字节写入
    public void flush() 强制刷新,将缓冲中的数据写入
    public void close() 关闭输出流,流被关闭后就不能再输出数据了

    再来看 Reader 和 Writer 类中的方法,你会发现和上面两个抽象基类中的方法很像。

    Reader 类

    方法方法介绍
    public int read(java.nio.CharBuffer target) 读取字节到字符缓存中
    public int read() 读取单个字符
    public int read(char cbuf[]) 读取字符到指定的 char 数组中
    abstract public int read(char cbuf[], int off, int len) 从 off 位置读取 len 长度的字符到 char 数组中
    public long skip(long n) 跳过指定长度的字符数量
    public boolean ready() 和上面的 available() 方法类似
    public boolean markSupported() 判断当前流是否支持标记流
    public void mark(int readAheadLimit) 标记读取位置,下次还可以从这里开始读取,使用前要看当前流是否支持,可以使用 markSupport() 方法判断
    public void reset() 重置读取位置为上次 mark 标记的位置
    abstract public void close() 关闭流释放相关资源

    Writer 类

    方法方法介绍
    public void write(int c) 写入一个字符
    public void write(char cbuf[]) 写入一个字符数组
    abstract public void write(char cbuf[], int off, int len) 从字符数组的 off 位置写入 len 数量的字符
    public void write(String str) 写入一个字符串
    public void write(String str, int off, int len) 从字符串的 off 位置写入 len 数量的字符
    public Writer append(CharSequence csq) 追加吸入一个字符序列
    public Writer append(CharSequence csq, int start, int end) 追加写入一个字符序列的一部分,从 start 位置开始,end 位置结束
    public Writer append(char c) 追加写入一个 16 位的字符
    abstract public void flush() 强制刷新,将缓冲中的数据写入
    abstract public void close() 关闭输出流,流被关闭后就不能再输出数据了

    下面我们就直接使用他们的子类,在使用中再介绍下面没有的新方法。

    1、读取控制台中的输入

    public class IOTest {
        public static void main(String[] args) throws IOException {
            // 三个测试方法
    //        test01();
    //        test02();
            test03();
        }
    
        public static void test01() throws IOException {
            BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(System.in));
            System.out.println("请输入一个字符");
            char c;
            c = (char) bufferedReader.read();
            System.out.println("你输入的字符为"+c);
        }
    
        public static void test02() throws IOException {
            BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(System.in));
            System.out.println("请输入一个字符,按 q 键结束");
            char c;
            do {
                c = (char) bufferedReader.read();
                System.out.println("你输入的字符为"+c);
            } while (c != 'q');
        }
    
        public static void test03() throws IOException {
            BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(System.in));
            System.out.println("请输入一行字符");
            String str = bufferedReader.readLine();
            System.out.println("你输入的字符为" + str);
        }
    }
    至于控制台的输出,我们其实一直都在使用呢,System.out.println() ,out 其实是 PrintStream 类对象的引用,PrintStream 类中当然也有 write() 方法,但是我们更常用 print() 方法和 println() 方法,因为这两个方法可以输出的内容种类更多,比如一个打印一个对象,实际调用的对象的 toString() 方法。

    2、二进制文件的写入和读取

    注意这里文件的路径,可以根据自己情况改一下,虽然这里的文件后缀是txt,但该文件却是一个二进制文件,并不能直接查看。

    @Test
        public void test04() throws IOException {
            byte[] bytes = {12,21,34,11,21};
            FileOutputStream fileOutputStream = new FileOutputStream(new File("").getAbsolutePath()+"/io/test.txt");
            // 写入二进制文件,直接打开会出现乱码
            fileOutputStream.write(bytes);
            fileOutputStream.close();
        }
    
        @Test
        public void test05() throws IOException {
            FileInputStream fileInputStream = new FileInputStream(new File("").getAbsolutePath()+"/io/test.txt");
            int c;
            // 读取写入的二进制文件,输出字节数组
            while ((c = fileInputStream.read()) != -1) {
                System.out.print(c);
            }
        }

    3、文本文件的写入和读取

    write() 方法和 append() 方法并不是像方法名那样,一个是覆盖内容,一个是追加内容,append() 内部也是 write() 方法实现的,也非说区别,也就是 append() 方法可以直接写 null,而 write() 方法需要把 null 当成一个字符串写入,所以两者并无本质的区别。需要注意的是这里并没有指定文件编码,可能会出现乱码的问题。

    write() 方法和 append() 方法并不是像方法名那样,一个是覆盖内容,一个是追加内容,append() 内部也是 write() 方法实现的,也非说区别,也就是 append() 方法可以直接写 null,而 write() 方法需要把 null 当成一个字符串写入,所以两者并无本质的区别。需要注意的是这里并没有指定文件编码,可能会出现乱码的问题。
        public void test06() throws IOException {
            FileWriter fileWriter = new FileWriter(new File("").getAbsolutePath()+"/io/test.txt");
            fileWriter.write("Hello,world!
    欢迎来到 java 世界
    ");
            fileWriter.write("不会覆盖文件原本的内容
    ");
    //        fileWriter.write(null); 不能直接写入 null
            fileWriter.append("并不是追加一行内容,不要被方法名迷惑
    ");
            fileWriter.append(null);
            fileWriter.flush();
            System.out.println("文件的默认编码为" + fileWriter.getEncoding());
            fileWriter.close();
        }
    
        @Test
        public void test07() throws IOException {
            FileWriter fileWriter = new FileWriter(new File("").getAbsolutePath()+"/io/test.txt", false); // 关闭追加模式,变为覆盖模式
            fileWriter.write("Hello,world!欢迎来到 java 世界
    ");
            fileWriter.write("我来覆盖文件原本的内容");
            fileWriter.append("我是下一行");
            fileWriter.flush();
            System.out.println("文件的默认编码为" + fileWriter.getEncoding());
            fileWriter.close();
        }
    
        @Test
        public void test08() throws IOException {
            FileReader fileReader = new FileReader(new File("").getAbsolutePath()+"/io/test.txt");
            BufferedReader bufferedReader = new BufferedReader(fileReader);
            String str;
            while ((str = bufferedReader.readLine()) != null) {
                System.out.println(str);
            }
            fileReader.close();
            bufferedReader.close();
        }
    
        @Test
        public void test09() throws IOException {
            FileReader fileReader = new FileReader(new File("").getAbsolutePath()+"/io/test.txt");
            int c;
            while ((c = fileReader.read()) != -1) {
                System.out.print((char) c);
            }
        }

    使用字节流和字符流的转换类 InputStreamReader 和 OutputStreamWriter 可以指定文件的编码,使用 Buffer 相关的类来读取文件的每一行。

        public void test10() throws IOException {
            FileOutputStream fileOutputStream = new FileOutputStream(new File("").getAbsolutePath()+"/io/test2.txt");
            OutputStreamWriter outputStreamWriter = new OutputStreamWriter(fileOutputStream, "GBK"); // 使用 GBK 编码文件
            outputStreamWriter.write("Hello,world!
    欢迎来到 java 世界
    ");
            outputStreamWriter.append("另外一行内容");
            outputStreamWriter.flush();
            System.out.println("文件的编码为" + outputStreamWriter.getEncoding());
            outputStreamWriter.close();
            fileOutputStream.close();
        }
    
        @Test
        public void test11() throws IOException {
            FileInputStream fileInputStream = new FileInputStream(new File("").getAbsolutePath()+"/io/test2.txt");
            InputStreamReader inputStreamReader = new InputStreamReader(fileInputStream, "GBK"); // 使用 GBK 解码文件
            BufferedReader bufferedReader = new BufferedReader(inputStreamReader);
            String str;
            while ((str = bufferedReader.readLine()) != null) {
                System.out.println(str);
            }
            bufferedReader.close();
            inputStreamReader.close();
        }
    使用字节流和字符流的转换类 InputStreamReader 和 OutputStreamWriter 可以指定文件的编码,使用 Buffer 相关的类来读取文件的每一行。
    @Test
        public void test10() throws IOException {
            FileOutputStream fileOutputStream = new FileOutputStream(new File("").getAbsolutePath()+"/io/test2.txt");
            OutputStreamWriter outputStreamWriter = new OutputStreamWriter(fileOutputStream, "GBK"); // 使用 GBK 编码文件
            outputStreamWriter.write("Hello,world!
    欢迎来到 java 世界
    ");
            outputStreamWriter.append("另外一行内容");
            outputStreamWriter.flush();
            System.out.println("文件的编码为" + outputStreamWriter.getEncoding());
            outputStreamWriter.close();
            fileOutputStream.close();
        }
    
        @Test
        public void test11() throws IOException {
            FileInputStream fileInputStream = new FileInputStream(new File("").getAbsolutePath()+"/io/test2.txt");
            InputStreamReader inputStreamReader = new InputStreamReader(fileInputStream, "GBK"); // 使用 GBK 解码文件
            BufferedReader bufferedReader = new BufferedReader(inputStreamReader);
            String str;
            while ((str = bufferedReader.readLine()) != null) {
                System.out.println(str);
            }
            bufferedReader.close();
            inputStreamReader.close();
        }

    4、复制文件

    这里笔者做了一些测试,不使用缓冲对文件复制时间的影响,文件的复制实质还是文件的读写。缓冲流是处理流,是对节点流的装饰。

    注:这里的时间是在我这台华硕笔记本上测试得到的,只是为了说明使用缓冲对文件的读写有好处。

    @Test
        public void  test12() throws IOException {
            // 输入和输出都使用缓冲流
            FileInputStream in = new FileInputStream("E:\视频资料\大数据原理与应用\1.1大数据时代.mp4");
            BufferedInputStream inBuffer = new BufferedInputStream(in);
            FileOutputStream out = new FileOutputStream("1.1大数据时代.mp4");
            BufferedOutputStream outBuffer = new BufferedOutputStream(out);
            int len = 0;
            byte[] bs = new byte[1024];
            long begin = System.currentTimeMillis();
            while ((len = inBuffer.read(bs)) != -1) {
                outBuffer.write(bs, 0, len);
            }
            System.out.println("复制文件所需的时间:" + (System.currentTimeMillis() - begin)); // 平均时间约 200 多毫秒
            inBuffer.close();
            in.close();
            outBuffer.close();
            out.close();
        }
    
    
        @Test
        public void  test13() throws IOException {
            // 只有输入使用缓冲流
            FileInputStream in = new FileInputStream("E:\视频资料\大数据原理与应用\1.1大数据时代.mp4");
            BufferedInputStream inBuffer = new BufferedInputStream(in);
            FileOutputStream out = new FileOutputStream("1.1大数据时代.mp4");
            int len = 0;
            byte[] bs = new byte[1024];
            long begin = System.currentTimeMillis();
            while ((len = inBuffer.read(bs)) != -1) {
                out.write(bs, 0, len);
            }
            System.out.println("复制文件所需时间:" + (System.currentTimeMillis() - begin)); // 平均时间约 500 多毫秒
            inBuffer.close();
            in.close();
            out.close();
        }
    
        @Test
        public void test14() throws IOException {
            // 输入和输出都不使用缓冲流
            FileInputStream in = new FileInputStream("E:\视频资料\大数据原理与应用\1.1大数据时代.mp4");
            FileOutputStream out = new FileOutputStream("1.1大数据时代.mp4");
            int len = 0;
            byte[] bs = new byte[1024];
            long begin = System.currentTimeMillis();
            while ((len = in.read(bs)) != -1) {
                out.write(bs, 0, len);
            }
            System.out.println("复制文件所需时间:" + (System.currentTimeMillis() - begin)); // 平均时间 700 多毫秒
            in.close();
            out.close();
        }
    
        @Test
        public void test15() throws IOException {
            // 不使用缓冲
            FileInputStream in = new FileInputStream("E:\视频资料\大数据原理与应用\1.1大数据时代.mp4");
            FileOutputStream out = new FileOutputStream("1.1大数据时代.mp4");
            int len = 0;
            long begin = System.currentTimeMillis();
            while ((len = in.read()) != -1) {
                out.write(len);
            }
            System.out.println("复制文件所需时间:" + (System.currentTimeMillis() - begin)); // 平均时间约 160000 毫秒,约 2 分多钟
            in.close();
            out.close();
        }

    序列化与反序列化

    序列化是将 Java 对象转换成与平台无关的二进制流,而反序列化则是将二进制流恢复成原来的 Java 对象,二进制流便于保存到磁盘上或者在网络上传输。

    如何实现序列化和反序列化?

    如果想要序列化某个类的对象,就需要让该类实现 Serializable 接口或者 Externalizable 接口。

    如果实现 Serializable 接口,由于该接口只是个 “标记接口”,接口中不含任何方法,序列化是使用 ObjectOutputStream(处理流)中的 writeObject(obj) 方法将 Java 对象输出到输出流中,反序列化是使用 ObjectInputStream 中的 readObject(in) 方法将输入流中的 Java 对象还原出来。

    下面程序演示实现 Serializable 接口,将对象序列化到文件中,再从文件中反序列化对象。

    import java.io.Serializable;
    public class Person implements Serializable {
        private String name;
        private int age;
        // 此处没有提供无参的构造函数
        public Person(String name, int age) {
            this.name = name;
            this.age = age;
        }
        // 省略 getter 和 setter 方法
    }
    public class Test {
        public static void main(String[] args) {
            // 序列化
            try (ObjectOutputStream outputStream = new ObjectOutputStream(new File("").getAbsolutePath()+"/object.txt"))) {
                Person person = new Person("小明", 21);
                outputStream.writeObject(person);
            } catch (FileNotFoundException e) {
                e.printStackTrace();
            } catch (IOException e) {
                e.printStackTrace();
            }
            // 反序列化
            try (ObjectInputStream objectInputStream = new ObjectInputStream(new FileInputStream(new File("").getAbsolutePath()+"/object.txt"))) {
                Person person = (Person) objectInputStream.readObject();
                System.out.println("name:" + person.getName() + ",age:" + person.getAge());
            } catch (FileNotFoundException e) {
                e.printStackTrace();
            } catch (IOException e) {
                e.printStackTrace();
            } catch (ClassNotFoundException e) {
                e.printStackTrace();
    
            }
        }
    }

    需要注意的是反序列化读取的仅仅是 Java 对象中的数据,而不是包含 Java 类的信息,所以在反序列化时还需要对象所属类的字节码(class)文件,否则会出现 ClassNotFoundException 异常。

    如果实现 Externalizable接口,该接口继承自 Serializable 接口,在 Java Bean 类中实现接口中的 writeExternal(out) 和 readExternal(in) 方法,需要注意的是必须提供默认的无参构造函数,否则反序列化失败。

    上面的 Java Bean 代码可修改为:

    public class Person implements Externalizable {
        private String name;
        private int age;
        
        // 需要提供默认的无参构造函数
        public Person() {
        }
    
        public Person(String name, int age) {
            this.name = name;
            this.age = age;
        }
    
        // 省略 getter 和 setter 方法
    
        @Override
        public void writeExternal(ObjectOutput out) throws IOException {
            out.writeObject(name);
            out.writeInt(age);
        }
    
        @Override
        public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException {
            this.name = in.readObject().toString();
            this.age = in.readInt();
        }
    }

    这两种序列化反序列化方式,前一种是使用默认的 Java 实现,而后一种是自定义实现,可以在序列化中选择如何序列化,比如对某个属性加密处理。

    注意序列化属性的顺序要和属性反序列化中的顺序一样,否则在反序列化时不能恢复出原来的对象。

    其实让类实现 Serializable 接口也是可以实现自定义序列化,只但需要在类中提供下面这三个方法。

    private void writeObject(java.io.ObjectOutStream out) throws IOException;
    private void readObject(java.io.ObjectInputStream in) throws IOException, ClassNotFoundException;
    private void readObjectNoData() throws ObjectStreamException;

    writeObject() 方法的作用和上面 writeExternal() 方法类似,readObject() 方法的作用和上面 readExternal() 方法类似,而 readObjectNoData() 方法是在序列化流不完整、序列化和反序列化版本不一致导致不能正确反序列时调用的容错方法。

    使用默认的序列化方式,会将对象中的每个实例属性依次进行序列化,如果某个属性是一个类类型,那么需要保证这个类也要是可序列化的类,否则将不能序列化该对象。在 Java 的序列化机制中,被序列化后的对象都有一个编号,多次序列化同一个对象,除了第一次真正序列化对象外,其他都是保存一个序列化编号。这样的机制带来的问题就是如果在序列化一个对象后,修改了对象中的属性,也不会生效。并不是对象中每个属性都需要序列化的,如被 static 修饰的属性是属于类的,而不是只属于某个对象。使用默认序列化方式,是不会将这些属性序列化的,在自定义的序列化方式中,我们也可以将这些属性忽略掉。除此之外,可以使用 transient 关键字来修饰某个属性,这样默认的序列化方式就不会序列化该属性了,自定义还是可以的。如果在反序列化时强行得到这些没有被序列化的值,得到的会是默认值(0 或 null)

    序列化和反序列化的版本问题

    在 Java 的序列化机制中,允许给类提供一个 private static final 修饰的 SerialVersionUID 类常量,来作为类版本的代号。这样即使类被修改了(如修改了方法),也会把修改前的类和修改后的类当成同一版本的类,序列化和反序列化照样可以正常使用。如果我们不显式的定义这个 SerialVersionUID,Java 虚拟机会根据类的信息帮我们自动生成,修改前和修改后的计算结果往往不同,造成版本不兼容而发生反序列化失败,另外由于平台的差异性,在程序移植中也可能出现无法反序列化。强大的 IDE 工具,也都有自动生成 SeriaVersionUID 的方法,这里就不多说了。JDK 中自带的也有生成 SeriaVersionUID 值的工具 serialver.exe,使用 serialver 类名(编译后) 命令就能生成该类的 SeriaVersionUID 值啦!

    总结:

    • 序列化和反序列化的方式可以分为三种,一种是实现 Serializable 接口使用默认的序列化和反序列化方式,一种是实现 Serializable 接口但是自定义序列化和反序列化方法,另外一种是实现 Externalizable 接口,实现接口中的方法。
    • 序列化和反序列化要注意版本问题,自定义序列化和反序列化时还要注意属性的顺序要保持一致,这些都可能会导致反序列化失败。
    原文:https://www.jianshu.com/p/715659e4775f
     
    NIO:
     

    NIO即New IO,这个库是在JDK1.4中才引入的。NIO和IO有相同的作用和目的,但实现方式不同,NIO主要用到的是块,所以NIO的效率要比IO高很多。

    在Java API中提供了两套NIO,一套是针对标准输入输出NIO,另一套就是网络编程NIO,本篇文章重点介绍标NIO,关于网络编程NIO请见Java NIO详解(二)

    1.3 流与块的比较

    NIO和IO最大的区别是数据打包和传输方式。IO是以流的方式处理数据,而NIO是以块的方式处理数据。

    面向流的IO一次一个字节的处理数据,一个输入流产生一个字节,一个输出流就消费一个字节。为流式数据创建过滤器就变得非常容易,链接几个过滤器,以便对数据进行处理非常方便而简单,但是面向流的IO通常处理的很慢。

    面向块的IO系统以块的形式处理数据。每一个操作都在一步中产生或消费一个数据块。按块要比按流快的多,但面向块的IO缺少了面向流IO所具有的有雅兴和简单性。

    二、NIO基础

    Buffer和Channel是标准NIO中的核心对象(网络NIO中还有个Selector核心对象,具体请参考Java NIO详解(二)),几乎每一个IO操作中都会用到它们。

    Channel是对原IO中流的模拟,任何来源和目的数据都必须通过一个Channel对象。一个Buffer实质上是一个容器对象,发给Channel的所有对象都必须先放到Buffer中;同样的,从Channel中读取的任何数据都要读到Buffer中。

    2.1 关于Buffer

    Buffer是一个对象,它包含一些要写入或读出的数据。在NIO中,数据是放入buffer对象的,而在IO中,数据是直接写入或者读到Stream对象的。应用程序不能直接对 Channel 进行读写操作,而必须通过 Buffer 来进行,即 Channel 是通过 Buffer 来读写数据的。

    在NIO中,所有的数据都是用Buffer处理的,它是NIO读写数据的中转池。Buffer实质上是一个数组,通常是一个字节数据,但也可以是其他类型的数组。但一个缓冲区不仅仅是一个数组,重要的是它提供了对数据的结构化访问,而且还可以跟踪系统的读写进程。

    使用 Buffer 读写数据一般遵循以下四个步骤:

    1. 写入数据到 Buffer;
    2. 调用 flip() 方法;
    3. 从 Buffer 中读取数据;
    4. 调用 clear() 方法或者 compact() 方法。

    当向 Buffer 写入数据时,Buffer 会记录下写了多少数据。一旦要读取数据,需要通过 flip() 方法将 Buffer 从写模式切换到读模式。在读模式下,可以读取之前写入到 Buffer 的所有数据。

    一旦读完了所有的数据,就需要清空缓冲区,让它可以再次被写入。有两种方式能清空缓冲区:调用 clear() 或 compact() 方法。clear() 方法会清空整个缓冲区。compact() 方法只会清除已经读过的数据。任何未读的数据都被移到缓冲区的起始处,新写入的数据将放到缓冲区未读数据的后面。

    Buffer主要有如下几种:

    这里写图片描述

    2.3 关于Channel

    Channel是一个对象,可以通过它读取和写入数据。可以把它看做IO中的流。但是它和流相比还有一些不同:

    1. Channel是双向的,既可以读又可以写,而流是单向的
    2. Channel可以进行异步的读写
    3. 对Channel的读写必须通过buffer对象

    正如上面提到的,所有数据都通过Buffer对象处理,所以,您永远不会将字节直接写入到Channel中,相反,您是将数据写入到Buffer中;同样,您也不会从Channel中读取字节,而是将数据从Channel读入Buffer,再从Buffer获取这个字节。

    因为Channel是双向的,所以Channel可以比流更好地反映出底层操作系统的真实情况。特别是在Unix模型中,底层操作系统通常都是双向的。

    这里写图片描述

    在Java NIO中Channel主要有如下几种类型:

    • FileChannel:从文件读取数据的
    • DatagramChannel:读写UDP网络协议数据
    • SocketChannel:读写TCP网络协议数据
    • ServerSocketChannel:可以监听TCP连接

    三、从理论到实践:NIO中的读和写

    IO中的读和写,对应的是数据和Stream,NIO中的读和写,则对应的就是通道和缓冲区。NIO中从通道中读取:创建一个缓冲区,然后让通道读取数据到缓冲区。NIO写入数据到通道:创建一个缓冲区,用数据填充它,然后让通道用这些数据来执行写入。

    3.1 从文件中读取

    我们已经知道,在NIO系统中,任何时候执行一个读操作,您都是从Channel中读取,而您不是直接从Channel中读取数据,因为所有的数据都必须用Buffer来封装,所以您应该是从Channel读取数据到Buffer。

    因此,如果从文件读取数据的话,需要如下三步:

    1. 从FileInputStream获取Channel
    2. 创建Buffer
    3. 从Channel读取数据到Buffer

    下面我们看一下具体过程:
    第一步:获取通道

    FileInputStream fin = new FileInputStream( "readandshow.txt" );
    FileChannel fc = fin.getChannel();  

    第二步:创建缓冲区

    ByteBuffer buffer = ByteBuffer.allocate( 1024 );
    

    第三步:将数据从通道读到缓冲区

    fc.read( buffer );

    3.2 写入数据到文件

    类似于从文件读数据,
    第一步:获取一个通道

    FileOutputStream fout = new FileOutputStream( "writesomebytes.txt" );
    FileChannel fc = fout.getChannel();

    第二步:创建缓冲区,将数据放入缓冲区

    ByteBuffer buffer = ByteBuffer.allocate( 1024 );
    
    for (int i=0; i<message.length; ++i) {
     buffer.put( message[i] );
    }
    buffer.flip();

    第三步:把缓冲区数据写入通道中

    fc.write( buffer );

    3.3 读写结合

    CopyFile是一个非常好的读写结合的例子,我们将通过CopyFile这个实力让大家体会NIO的操作过程。CopyFile执行三个基本的操作:创建一个Buffer,然后从源文件读取数据到缓冲区,然后再将缓冲区写入目标文件。

    /**
     * 用java NIO api拷贝文件
     * @param src
     * @param dst
     * @throws IOException
     */
    public static void copyFileUseNIO(String src,String dst) throws IOException{
        //声明源文件和目标文件
                FileInputStream fi=new FileInputStream(new File(src));
                FileOutputStream fo=new FileOutputStream(new File(dst));
                //获得传输通道channel
                FileChannel inChannel=fi.getChannel();
                FileChannel outChannel=fo.getChannel();
                //获得容器buffer
                ByteBuffer buffer=ByteBuffer.allocate(1024);
                while(true){
                    //判断是否读完文件
                    int eof =inChannel.read(buffer);
                    if(eof==-1){
                        break;  
                    }
                    //重设一下buffer的position=0,limit=position
                    buffer.flip();
                    //开始写
                    outChannel.write(buffer);
                    //写完要重置buffer,重设position=0,limit=capacity
                    buffer.clear();
                }
                inChannel.close();
                outChannel.close();
                fi.close();
                fo.close();
    }     

    四、需要注意的点

    上面程序中有三个地方需要注意

    4.1 检查状态

    当没有更多的数据时,拷贝就算完成,此时 read() 方法会返回 -1 ,我们可以根据这个方法判断是否读完。

    int r= fcin.read( buffer );
    if (r==-1) {
         break;
         }

    4.2 Buffer类的flip、clear方法

    控制buffer状态的三个变量

    • position:跟踪已经写了多少数据或读了多少数据,它指向的是下一个字节来自哪个位置
    • limit:代表还有多少数据可以取出或还有多少空间可以写入,它的值小于等于capacity。
    • capacity:代表缓冲区的最大容量,一般新建一个缓冲区的时候,limit的值和capacity的值默认是相等的。

    flip、clear这两个方法便是用来设置这些值的。

    flip方法

    我们先看一下flip的源码:

    public final Buffer flip() {
        limit = position;
        position = 0;
        mark = -1;
        return this;
     }

    这里写图片描述

    在上面的FileCopy程序中,写入数据之前我们调用了buffer.flip();方法,这个方法把当前的指针位置position设置成了limit,再将当前指针position指向数据的最开始端,我们现在可以将数据从缓冲区写入通道了。 position 被设置为 0,这意味着我们得到的下一个字节是第一个字节。 limit 已被设置为原来的 position,这意味着它包括以前读到的所有字节,并且一个字节也不多。

    clear方法

    先看一下clear的源码:

     public final Buffer clear() {
        position = 0;
        limit = capacity;
        mark = -1;
        return this;
    }

    这里写图片描述

    在上面的FileCopy程序中,写入数据之后也就是读数据之前,我们调用了 buffer.clear();方法,这个方法重设缓冲区以便接收更多的字节。上图显示了在调用 clear() 后缓冲区的状态。

    转载请说明出处,原文链接:http://blog.csdn.net/suifeng3051/article/details/48160753

    异步IO

    异步 I/O 是一种没有阻塞地读写数据的方法。通常,在代码进行 read() 调用时,代码会阻塞直至有可供读取的数据。同样, write()调用将会阻塞直至数据能够写入,关于同步的IO请参考另一篇文章Java IO

    另一方面,异步 I/O 调用不但不会阻塞,相反,您可以注册对特定 I/O 事件诸如数据可读、新连接到来等等,而在发生这样感兴趣的事件时,系统将会告诉您。

    异步 I/O 的一个优势在于,它允许您同时根据大量的输入和输出执行 I/O。同步程序常常要求助于轮询,或者创建许许多多的线程以处理大量的连接。使用异步 I/O,您可以监听任何数量的通道上的事件,不用轮询,也不用额外的线程。

    Selector

    在我的JavaNIO详解(一)中已经详细介绍了Java NIO三个核心对象中的Buffer和Channel,现在我们就重点介绍一下第三个核心对象Selector。Selector是一个对象,它可以注册到很多个Channel上,监听各个Channel上发生的事件,并且能够根据事件情况决定Channel读写。这样,通过一个线程管理多个Channel,就可以处理大量网络连接了。

    采用Selector模式的的好处

    有了Selector,我们就可以利用一个线程来处理所有的channels。线程之间的切换对操作系统来说代价是很高的,并且每个线程也会占用一定的系统资源。所以,对系统来说使用的线程越少越好。

    但是,需要记住,现代的操作系统和CPU在多任务方面表现的越来越好,所以多线程的开销随着时间的推移,变得越来越小了。实际上,如果一个CPU有多个内核,不使用多任务可能是在浪费CPU能力。不管怎么说,关于那种设计的讨论应该放在另一篇不同的文章中。在这里,只要知道使用Selector能够处理多个通道就足够了。

    下面这幅图展示了一个线程处理3个 Channel的情况:

    这里写图片描述

    同步和异步:同步和异步是针对应用程序和内核的交互而言的,同步指的是用户进程触发IO 操作并等待或者轮询的去查看IO 操作是否就绪,而异步是指用户进程触发IO 操作以后便开始做自己的事情,而当IO 操作已经完成的时候会得到IO 完成的通知。

    以银行取款为例:

    同步 : 自己亲自出马持银行卡到银行取钱(使用同步 IO 时,Java 自己处理IO 读写);

    异步 : 委托一小弟拿银行卡到银行取钱,然后给你(使用异步IO 时,Java 将 IO 读写委托给OS 处理,需要将数据缓冲区地址和大小传给OS(银行卡和密码),OS 需要支持异步IO操作API);

    阻塞和非阻塞:阻塞和非阻塞是针对于进程在访问数据的时候,根据IO操作的就绪状态来采取的不同方式,说白了是一种读取或者写入操作方法的实现方式,阻塞方式下读取或者写入函数将一直等待,而非阻塞方式下,读取或者写入方法会立即返回一个状态值。

    以银行取款为例:

    阻塞 : ATM排队取款,你只能等待(使用阻塞IO时,Java调用会一直阻塞到读写完成才返回);

    非阻塞 : 柜台取款,取个号,然后坐在椅子上做其它事,等号广播会通知你办理,没到号你就不能去,你可以不断问大堂经理排到了没有,大堂经理如果说还没到你就不能去(使用非阻塞IO时,如果不能读写Java调用会马上返回,当IO事件分发器通知可读写时再继续进行读写,不断循环直到读写完成)

    1.BIO 编程

    Blocking IO: 同步阻塞的编程方式。

    BIO编程方式通常是在JDK1.4版本之前常用的编程方式。编程实现过程为:首先在服务端启动一个ServerSocket来监听网络请求,客户端启动Socket发起网络请求,默认情况下ServerSocket回建立一个线程来处理此请求,如果服务端没有线程可用,客户端则会阻塞等待或遭到拒绝。

    且建立好的连接,在通讯过程中,是同步的。在并发处理效率上比较低。大致结构如下:

    同步并阻塞,服务器实现模式为一个连接一个线程,即客户端有连接请求时服务器端就需要启动一个线程进行处理,如果这个连接不做任何事情会造成不必要的线程开销,当然可以通过线程池机制改善。

    BIO方式适用于连接数目比较小且固定的架构,这种方式对服务器资源要求比较高,并发局限于应用中,JDK1.4以前的唯一选择,但程序直观简单易理解。

    使用线程池机制改善后的BIO模型图如下:

    2.NIO 编程:Unblocking IO(New IO): 同步非阻塞的编程方式。

    NIO本身是基于事件驱动思想来完成的,其主要想解决的是BIO的大并发问题,NIO基于Reactor,当socket有流可读或可写入socket时,操作系统会相应的通知引用程序进行处理,应用再将流读取到缓冲区或写入操作系统。也就是说,这个时候,已经不是一个连接就要对应一个处理线程了,而是有效的请求,对应一个线程,当连接没有数据时,是没有工作线程来处理的。

    NIO的最重要的地方是当一个连接创建后,不需要对应一个线程,这个连接会被注册到多路复用器上面,所以所有的连接只需要一个线程就可以搞定,当这个线程中的多路复用器进行轮询的时候,发现连接上有请求的话,才开启一个线程进行处理,也就是一个请求一个线程模式。

    在NIO的处理方式中,当一个请求来的话,开启线程进行处理,可能会等待后端应用的资源(JDBC连接等),其实这个线程就被阻塞了,当并发上来的话,还是会有BIO一样的问题

    3.AIO编程:Asynchronous IO: 异步非阻塞的编程方式。

    与NIO不同,当进行读写操作时,只须直接调用API的read或write方法即可。这两种方法均为异步的,对于读操作而言,当有流可读取时,操作系统会将可读的流传入read方法的缓冲区,并通知应用程序;对于写操作而言,当操作系统将write方法传递的流写入完毕时,操作系统主动通知应用程序。即可以理解为,read/write方法都是异步的,完成后会主动调用回调函数。在JDK1.7中,这部分内容被称作NIO.2,主要在java.nio.channels包下增加了下面四个异步通道:AsynchronousSocketChannel、AsynchronousServerSocketChannel、AsynchronousFileChannel、AsynchronousDatagramChannel

    https://blog.csdn.net/suifeng3051/article/details/48441629

     https://blog.csdn.net/forezp/article/details/88414741



     
     
     
  • 相关阅读:
    drf序列化组件
    drf入门规范
    vue发送ajax请求与跨域问题
    Vue对象提供的属性功能
    vue.js库的下载与使用
    admin后台管理与media配置
    Auth认证模块
    学习总结3月11日
    学习总结3月10日
    基于 Spark 的物流企业数据仓库 的设计与实现
  • 原文地址:https://www.cnblogs.com/baldprogrammer/p/13638012.html
Copyright © 2020-2023  润新知