Java I/O学习(二)

Stream概述

Stream是一个数据流,可以从它读取数据或写入数据。它是连接数据源或数据目的地,例如文件,网络连接。

Stream中没有和数组一样,读、写数据时利用索引访问的概念。也没有和数组或RandomAccessFile一样的,向前或向后移动。Stream是一个连续的数据流。

某些流,如PushbackInputStream可以把数据放回流中重新读取,但是这只能是有限个数的数据,而且无法随意遍历数据。数据只能被顺序访问。

翻译自Java IO: Streams

流的特性:

  • 先进先出,最先写入输出流的数据最先被输入流读取到。
  • 顺序存取,可以一个接一个地往流中写入一串字节,读出时也将按写入顺序读取一串字节,不能随机访问中间的数据。
  • 只读或只写,每个流只能是输入流或输出流的一种,不能同时具备两个功能,在一个数据传输通道中,如果既要写入数据,又要读取数据,则要分别提供两个流。

I/O流分类

有无数据源或目的地

根据流对象构造时是否需要数据源或数据目的地,可以分为两类:节点流和处理流。

节点流需要有数据源或数据目的地,处理流需要一个另一个流作为参数。java.io包结构采用装饰者模式设计流。

数据源一般有,File、ByteArray、String、char、pipes。

pipes

pipes,中文意思为通道。他的能力是为2个在同一个JVM中运行的线程提供通信。所以它可以是数据源也可以是数据目的地。不可以使用pipe在两个不同进程中的线程间提供通信。

Java中的pipe和Linux/Unix中的概念不同。后者可以用于运行在两块不同空间地址的进程间通信。

使用时一个pipedInputStream应该和一个PipedOutputStream相连。写入pipe输出流的数据是在另一个线程中通过与其相连的pipe输入流读取到的。然后调用PipedOutputStream.write()输出到当前线程。

public class PipeExample {

    public static void main(String[] args) throws IOException {

        final PipedOutputStream output = new PipedOutputStream();
        final PipedInputStream  input  = new PipedInputStream(output);


        Thread thread1 = new Thread(new Runnable() {
            @Override
            public void run() {
                try {
                    output.write("Hello world, pipe!".getBytes());
                } catch (IOException e) {
                }
            }
        });


        Thread thread2 = new Thread(new Runnable() {
            @Override
            public void run() {
                try {
                    int data = input.read();
                    while(data != -1){
                        System.out.print((char) data);
                        data = input.read();
                    }
                } catch (IOException e) {
                }
            }
        });

        thread1.start();
        thread2.start();

    }
}

可以看到输出流作为参数传递给输入流的构造器,使它们相连。当然可以用PipedOutputStream/PipedInputStream.connect()方法去连接另一个pipe流。

注意,pipe流的read()或者write()都是阻塞方法,必须在同一个进程中的不同线程中使用。如果在同一个线程中使用会造成线程死锁。而且一般线程通信,传递的都是一个完整对象,很少有用raw byte。翻译自Java IO: Pipes

流向

依据流的方向,可以分为输入流河输出流。

输入流只进行读操作,输出流写进行写操作。

处理数据类型

按照处理数据类型,可以分为:字符流和字节流。

由于字符集的原因,导致同一个字符根据不同的字符集有不同的编码标示。于是有了适合处理字符的便捷流。其本质就是字符和字节根据字符集之间的相互转换。两者区别:

  • 读写单位不同:字节流以字节(8bit)为单位,字符流以字符为单位,根据码表映射字符,一次可能读多个字节。
  • 处理对象不同:字节流能处理所有类型的数据(如图片、avi等),而字符流只能处理字符类型的数据。

如果只是处理文本字符,优先使用字符流。其它数据类型(字符数据也可以)使用字节流。

字节流

java.io包中的类结构是采用装饰者模式设计的,所以分别介绍字节流输入,输出流的节点流和处理流。

输入流

所有字节输入流的父类是一个抽象类,InputStream。java.io包中,它的直接子类如下:

Stream Name Dec
ByteArrayInputStream 属于节点流,连接byte数组作为数据源
PipedInputStream 属于节点流,连接线程间共享的通道(pipe)
StringBufferInputStream 属于节点流,连接字符串(已废弃)
FileInputStream 属于节点流,连接本地文件(通过FileSystem连接JVM可访问的file system中的文件)
ObjectInputStream 属于处理流,反序列化先前通过ObjectOutputStream写入的数据
FilterInputStream 属于处理流,覆写了InputStream中的方法来实现数据的转换或提供额外的方法。它的子类扩展了更多的功能也属于处理流
SequenceInputStream 属于处理流,用于其他输入流的逻辑连接。它依照输入流集合顺序开始读取,直到读到最后一个流中数据源尾为止
输出流

所有字节流输出流的负累是一个抽象类,OutputStream。java.io包中,他的直接子类如下:

Stream name Dec
ByteArrayOutputStream 属于节点流,连接byte数组作为数据输出对象
PipedOutputStream 属于节点流,连接线程间共享的通道(pipe)
FileOutputStream 属于节点流,连接本地文件(通过FileSystem连接JVM可访问的file system中的文件)
FilterOutputStream 属于处理流,覆写了OutputStream中的方法来实现数据的转换或提供额外的方法。它的子类扩展了更多的功能也属于处理流
ObjectOutputStream 属于处理流,向包含的OutputStream输出基本数据类型和对象实例。可写入的对象实例必须是后期可通过ObjectInputStream反序列化的,即它必须是可序列化的
PrintStream 属于处理流,给其他输出流添加功能,使用系统默认的字符集编码各种类型的数据(基本数据类型,字符串,引用类型),转换成字节输出
流详解
PushbackInputStream

一个装饰流,他的主要作用就是回退字节(字节数组)或者称作字节(字节数组)未读,下一个读取操作继续读取该字节(字节数组)。它适用于一个片段代码读取一串以特定字节值结束,数量不确定的字节数组;当读取到特定的字节并调用unread()后以便于下一个读取操作读取到回退的字节。

PushbackInputStream有两个字段:

  • buf,用于缓存回退字节的字节数组。
  • pos,缓存字节数组中元素个数。

构造PushbackInputStream时,不指定buf大小,默认值为1。注意unread()操作并不会跳过回退的一个或多个字节,下一个读取操作一定会从缓存数组取出回退的项目。

SequenceInputStream

一个装饰流,用于逻辑连接多个输入流,并且按照集合顺序开始读取操作。它的构造函数有:

  • SequenceInputStream(Enumeration<? extends InputStream> e)
  • SequenceInputStream(InputStream s1, InputStream s2)

Enumeration封装了有关遍历集合的方法,同样还可以遍历集合的接口是Iterator。

注意遍历并不是指单纯的获取,它的行为类似for循环。所以这两个接口和集合类中的获取元素方法并不重叠。

它们的区别在于:

  1. Enumeration只能获取集合中的数据,不能修改集合结构。而Iterator除了遍历集合,还可以删除集合中的数据,修改集合结构。
  2. Iterator支持fail-fast错误检测机制,而Enumeration不支持。
  3. Enumeration只能为Vector,Hashtable类型集合提供遍历,且由它们生成对象;而Iterator可以为HashMap,ArrayList等集合提供遍历。

相同点:它们的方法都是线程安全,支持同步。

可以看出Enumeration的命名和它本身提供的功能有关,只能够枚举集合元素,不能修改集合结构。这和Enum类似。

fail-fast错误检测机制

fail-fast错误检查机制指,同一时间有多个线程,使用除了Iterator自身的方法对集合的结构修改,会快速失败。并且抛出ConcurrentModificationException异常。

关于fail-fast,这里需要注意两点:

  1. Iterator自身方法支持线程安全,所以不会触发fail-fast
  2. 必须是同一时间有多个线程对集合作出结构上的修改。

实践测试fail-fast

public class TestFailFast {
    private static List<Integer> list = new ArrayList<>();
    private static Hashtable<String, Integer> table = new Hashtable<>();
    public static void main(String[] args) {
        for(int i = 0; i < 10; i++) {
            list.add(i);
            table.put(String.valueOf(i), i);
        }
        System.out.println("测试fail-fast发生时机");
        //测试fail-fast,以及不同时操作不触发fail-fast的情况
        new Thread01().start();
        new Thread02().start();

        try {
            Thread.sleep(1000);
        }catch (InterruptedException e) {
            e.printStackTrace();
        }

        System.out.println("测试Enumeration不支持fail-fast");
        //测试Enumeration不支持fail-fast
        new Thread03().start();
        new Thread04().start();
    }

    private static class Thread01 extends Thread {
        public void run() {
            //A.测试线程1,2不同时操作集合list,
            //会不会抛出ConcurrentModificationException
            //给线程1睡眠10毫秒,让线程2先执行
            // try {
            //     Thread.sleep(10);
            // }catch(InterruptedException e) {
            //     e.printStackTrace();
            // }
            Iterator<Integer> iterator = list.iterator();
            while(iterator.hasNext()) {
                int i = iterator.next();
                System.out.println("Thread 1 iterator in: " + i);
                //B.如果希望抛出ConcurrentModificationException,
                //就把当前线程睡眠10毫秒。并且注释A代码片段执行
                try {
                    Thread.sleep(10);
                }catch(InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }

    private static class Thread02 extends Thread {
        public void run() {
            int i = 0;
            while(i < list.size()) {
                System.out.println("Thread 2 run in: " + i);
                if(i == list.size() / 2) {
                    list.remove(i);
                }
                i++;
            }
        }
    }

    private static class Thread03 extends Thread {
        public void run() {
            Enumeration<Integer> e = table.elements();
            //c.测试同时有另一个线程改变了table结构,会不会在后续的遍历中看到改变的结果
            //线程3睡眠10毫秒,等待线程4修改集合后再遍历。
            // try {
            //     Thread.sleep(10);
            // }catch(InterruptedException ex) {
            //     ex.printStackTrace();
            // }
            while(e.hasMoreElements()) {

                int i = e.nextElement();
                System.out.println("Thread 3 iterator in:" + i);

                //d.测试同时有两个线程操作table,
                //会不会抛出ConcurrentModificationException。
                //给当前线程3睡眠10毫秒,让线程4操作集合。并且注释c代码片段执行。
                try {
                    Thread.sleep(10);
                }catch(InterruptedException ex) {
                    ex.printStackTrace();
                }
            }
        }
    }

    private static class Thread04 extends Thread {
        public void run() {
            int count = table.size();
            int i = 0;
            while(i < count) {
                System.out.println("Thread 4 run in: " + i);
                if(i == count / 2) {
                    table.put("random", 23);
                }
                i++;
            }
        }
    }
}

A,C情况同时运行时,输出如下:

测试Iterator支持fail-fast以及Enumeration不支持fail-fast.png

B,D情况同时运行时,输出如下:

测试多线程不同时操作集合不会触发fail-fast,以及Enumeration遍历时集合被修改可以遍历修改部分.png

注意Hashtable使用Enumeration遍历时,是从后往前遍历。参考Java 集合系列18之 Iterator和Enumeration比较Java 集合系列04之 fail-fast总结Java提高篇(三四)—–fail-fast机制

字符流

同样以装饰者设计模式角度来看看java.io包下的字符流

Reader

Reader是字符流输入流的父类,它是一个抽象类。

Reader Name Dec
BufferedReader 属于处理流,缓存字符,提高读取字符,数组以及行的效率
CharArrayReader 属于节点流,连接char数组数据源
FilterReader 属于处理流,过滤式字符读取流的抽象父类,子类扩展该类
InputStreamReader 属于处理流,连接字节流和字符流的桥梁,将字节转换为字符
PipedReader 属于节点流,连接线程间共享的通道(pipe)数据源
StringReader 属于节点流,连接String对象数据源
Writer

Writer是字符输出流的父类,它是一个抽象类

Writer Name Dec
BufferedWriter 属于处理流,缓冲字符,提高输出字符,数组,字符串的效率
CharArrayWriter 属于节点流,连接char数组作为数据写入对象
FilterWriter 属于处理流,字符过滤式输出流的抽象父类,子类扩展该类功能
OutputStreamWriter 属于处理流,连接字节流与字符流的桥梁,将字符编码转换为字节
PipedWriter 属于节点流,连接线程件共享的通道(pipe)作为数据输出对象
PrintWriter 属于处理流,向文本输出流打印对象的格式化形式(包括基本数据类型,字符串和对象)
StringWriter 属于节点流,一个String缓冲输出流,生成String对象
流详解
PushbackReader&PushbackInputStream

两个都具有回退功能,前者针对字符,后者针对字节。PushbackReader内部维护了一个char数组缓存回退字符;PushbackInputStream内部维护了一个byte数组缓存回退字节。两个又一个共性,在没有指定缓存区大小时,默认只能回退一个字符或字节。

BufferedReader&inputStreamReader

BufferedReader是给其它Reader对象添加字符缓冲区,而InputStreamReader内部有一个字节数组缓冲区,用于每次进行底层读取(native关键词的读方法,磁盘I/O交互操作)时,尽可能读取更多字节而不是满足必须的数量。

官方文档中建议为了提高效率,应该使用BufferedReader+InputStreamReader组合。既然InputStreamReader有了缓冲区,干嘛还需要BufferedReader?

这是因为两者缓存的并不是同一样东西,提高的效率也不是同一个对象。

BufferedReader的作用是每一次的读操作尽量从底层的字节流或字符流(这里的底层字节、字符流是指BufferedReader包含的其他流对象)读取更多的字符放入缓冲区,从而避免多次字节转换字符,并为其分配内存。如果底层是字节流,还会减少与磁盘文件的I/O交互。

InputStreamReader属于处理流,必须由InputStream对象作为构造参数来实例化对象。它内部的缓存区主要作用是减少与磁盘的底层I/O交互。每一次读取时尽可能多的读取字节,放入字节数组缓存区。

InputStreamReader和BufferedReader组合的意义是,每调用BufferedReader对象的读方法,尽可能多的从InputStreamReader中获取解码后的字符,放入缓存区。而此时InputStreamReader对象与底层磁盘文件交互时尽可能多的读取字节放入缓存区,减少I/O交互。

实践效率差

public class TestEfficiency {
    public static void main(String[] args) {
        File file = new File("../file/TestFile0.txt");
        long startTime = 0;
        long endTime = 0;

        InputStreamReader in = null;
        BufferedReader reader = null;
        FileInputStream underlyIn = null;

        try {
            try {
                underlyIn = new FileInputStream(file);
                in = new InputStreamReader(underlyIn, "GBK");
                int c = -1;
                startTime = System.nanoTime();
                while((c = in.read()) != -1) {
                }
                endTime = System.nanoTime() - startTime;
                System.out.println(
                    "using InputStreamReader input characters from text spend time:"
                    + endTime);

                reader = new BufferedReader(in);
                startTime = System.nanoTime();
                while((c = reader.read()) != -1) {
                }
                endTime = System.nanoTime() - startTime;
                System.out.println(
                    "using BufferedReader input characters from text spend time:"
                    + endTime);
            }finally {
                reader.close();
            }
        }catch(IOException e) {
            e.printStackTrace();
        }
    }
}

时间输出比较:

using InputStreamReader input characters from text spend time:22936559
using BufferedReader input characters from text spend time:33166

从上面的分析可以看出,从JVM外部文件读写都是采用字节流中的native方法,实现I/O交互。而在内存中操作字符优化(如为字符、字节分配内存)是依靠BufferedReader/BufferedWriter。

LineNumberReader

一个缓冲字符输入流,监视行数。从0开始,没读取到一个行结束数据加一。虽然类中有setLineNumber()可以改变行数的数值,实际上无法达到随机访问文件的效果,仍旧是顺序读取。设置的行数只是改变了getLineNumber()的返回值(也就是类内部记录行数的变量值被修改)。

FileReader

InputStreamReader是连接字符和字节的桥梁,而一般读取文件使用字符流是它的子类FileReader。内部实现了FileInputStream和Reader之间的转换,提供了读取文本的快捷方式。

PrintWriter&PrintStream

打印对象格式化形式

通过文档得知,两个类都可以对引用类型对象进行格式化打印。那么怎么打印呢?

PrintStream.java&PrintWriter.java

    public void print(Object obj) {
        write(String.valueOf(obj));
    }

String.java

public static String valueOf(Object obj) {
        return (obj == null) ? "null" : obj.toString();
    }

分析:首先使用String.valueOf(),调用Object.toString()得到String对象。然后使用系统默认的字符集转换成字节,使用write(int)方法输出。

原理

两个类都有涉及到OutputStreamWriter,它是字符和字节之间的桥梁。

PrintStream

类内部有以下几个成员变量:

  • BufferedWriter textOut
  • OutputStreamWriter charOut

在构造PrintStream时,传入OutputStream(因为PrintStream是处理流),并且执行父类构造函数。然后使用当前对象(实际类型为PrintStream)构造charOut对象,最后构造textOut对象。

由于是字节流(继承自OutputStream),有两个公共的write()重载方法,输出字节。其内部都是调用父类中的成员变量out(这个out就是PrintStream构造函数传入的OutputStream参数)相应的write()

这样做的目的是为了冲刷BufferedWriter的缓冲区。而out变量不一定有冲刷缓冲区的方法。

所有的print()重载方法内部都是调用了PrintStream的write(byte[])write(String)私有方法。而println()是调用了相应的print()后再跟上一个依赖系统的换行符。

write(String)为例

    private void write(String s) {
        try {
            synchronized (this) {
                ensureOpen();
                textOut.write(s);
                textOut.flushBuffer();
                charOut.flushBuffer();
                if (autoFlush && (s.indexOf('\n') >= 0))
                    out.flush();
            }
        }
        catch (InterruptedIOException x) {
            Thread.currentThread().interrupt();
        }
        catch (IOException x) {
            trouble = true;
        }
    }

流程分析:

  1. 字符串s传给textOut.write()
  2. BufferedWriter内部也有一个out变量,就是charOut。传递给charOut.write()
  3. OutputStreamWriter内部有一个se变量,实际类型为SteamEncoding。而构造se对象时,需要传入OutputStream,OutputStreamWriter本身,以及字符集名称。
  4. 在StreamEncoding内部,通过字符集编码字符串s,并且转传成字节序列,调用传入的OutputStream对象的write()。而这个OutputStream对象实际类型就是PrintStream,也就是它自身实现的Write()公共方法。

PrintWriter

PrintWriter类内部有一个成员变量out,其表现类型为Writer。该变量实际类型有两种情况,一是BuferedWriter,另一种是构造PrintWriter对象时传入的Writer对象。

类的print()重载方法都是调用相应的write()方法。而write()方法内部都是调用out.write()方法。

println()内部就是调用print()然后加上以来系统的换行符。其余方法内部实现可以参考Java I/O PrintWriter

总结

从上述分析来看,两者在print()重载方法方面没有什么区别。但两者根本区别是PrintStream是字节流,有自己处理字节的方法。而PrintWriter是字符流,没有处理字节的方法。

另一方面就是自动冲刷缓冲区机制的区别:

  • PrintStream自动冲刷情况,write(byte[]),println()的重载方法,print()的重载方法,以及write(byte)输出换行符字节或者字节值为10的调用时。
  • PrintWriter自动冲刷情况,printf(),println()的重载方法,format()调用时。

猜测PrintStream的存在是为了让字节流使用字节意外的数据进行I/O操作。一般字节流,如FileOutputStream没有一个输出方法可以传入除字节类型的数据。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 159,117评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,328评论 1 293
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,839评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,007评论 0 206
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,384评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,629评论 1 219
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,880评论 2 313
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,593评论 0 198
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,313评论 1 243
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,575评论 2 246
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,066评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,392评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,052评论 3 236
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,082评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,844评论 0 195
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,662评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,575评论 2 270

推荐阅读更多精彩内容

  • 一、流的概念和作用。 流是一种有顺序的,有起点和终点的字节集合,是对数据传输的总成或抽象。即数据在两设备之间的传输...
    布鲁斯不吐丝阅读 9,950评论 2 95
  • tags:io categories:总结 date: 2017-03-28 22:49:50 不仅仅在JAVA领...
    行径行阅读 2,130评论 0 3
  • 在经过一次没有准备的面试后,发现自己虽然写了两年的android代码,基础知识却忘的差不多了。这是程序员的大忌,没...
    猿来如痴阅读 2,740评论 3 10
  • 一、IO流整体结构图 流是一组有顺序的,有起点和终点的字节集合,是对数据传输的总称或抽象。即数据在两设备间的传输称...
    慕凌峰阅读 1,116评论 0 12
  • 慢慢来 时间久远 慢慢是长策
    NatashaDobby阅读 99评论 0 0