话题一:编码/转码
我们知道,在网络传输过程中,说到底,是要传输字节流的,字符流(Writer/Reader)不过是在字节流(InputStream/OutputStream)基础上做了一下封装而已,是JAVA在语法层面上给我们做的一个东西。
下面我们来先看看一段代码:
运行结果: get info : 涓栫晫锛屼綘濂? get info : 世界,你?? 分析: 首先来说,S1是乱码,这个是好理解的,可是为什么S2也是乱码呢? 第一,S通过UTF-8编码形成字节数据B 第二,接收方拿到B,通过GBK来进行转码,得到S1,很显然会乱码。 更加重要的一点是:很可能B在GBK字符集中根本没有任何映射,那么此时GBK会指定一些特殊符号代替,比如? 第三,利用S1想还原原先的字节流的想法可能失败! 因为在上面的第二中,原来有些字节数据通过GBK的转码,可能被弄成?,而?在GBK中的字节数据和原先的字节数据就很可能不一样了!于是S2就这样被乱码了。 这说明,在编码的转换过程中,是很可能转不回来的! |
话题二:网络交换数据的IO过程
从上面的图可以看出,JAVA本身不参与数据在网络上的传输,JAVA仅仅做的是,如果要发送数据,那么把字节流交给KERNEL;如果需要读取数据,那么从KERNEL中读取字节流。
发送与接收,如果采取一致的编码,那么就不会乱码!
在网络中传输的,应该是一种协议,不过是用字节流表达而已,比如,字节流的编码,大小,字节内容等。
话题三:JAVA BIO
Java BIO,即传统的IO,阻塞的IO,也是最常用,最基本的IO了,下面就来剖析下它!
- Input/Output
Input:输入,把数据从磁盘、键盘、网络,内存中输入到程序中;
Output:输出,把程序中的数据输出到磁盘、显示器、网络、内存中;
可以说,没有IO,就没有结果,就没有价值!
- 字节流 VS 字符流
字节流的源头是:InputStream/OutputStream
字符流的源头是:Reader/Writer
首先来说,我们一定得忘记字符流这回事,而是从字节流的角度进行理解IO。
之所以,存在字符流,是因为毕竟字节不是那么可视化,因此提供了把字节流转化成字符流的方式。
我们可以关注下Reader/Writer的子类:
可以清楚的看到,在InputStreamReader/OutputStreamWriter提供的构造方法中,接受一个字节流以及编码。其实,这就是字节流转化成字符流的桥梁。我们需要注意的是,在这个转化过程中,需要特别注意编码问题,最好就是手动的、明确的指定编码,而不是走默认的环境编码什么的。
- DataInputStream/DataOutputStream
这种数据流有个特点,就是提供了各种readXXX/writeXXX方法,如下所示:
试想下,如果我们想通过网络,给对方发送个int,我们难道得自己将int转成4个字节来进行发送?这也太麻烦了吧,而DataInputStream已经提供好了众多数据格式的操作了。在Socket通信时,当我们需要封装Socket的IO操作时,可以考虑用这个数据流。
- ByteArrayInputStream/ByteArrayOutputStream
内存字节流,这种流,会将内存中的字节包装形成流,延伸来看,String也可以先获取byte[],然后在转化成这种内存字节流。
- ObjectInputStream/ObjectOutputStream
对象字节流,类比的思想,就是将对象包装形成字节流,然后,可以写入磁盘(持久化),可以通过网络发送出去(对方接受后,将字节流反序列化得到对象)。
- 压缩流
ZipInputStream/GZinputStream/JarInputStream...
这些压缩流,可以套在文件字节流上,也可以套在网络字节流上(节省网络通信量)。
- BufferedInputStream/BufferedOutputStream
如果我们想要Buffer功能,可以套一层Buffer流,Buffer往往会快点,这是为什么呢?
下面我们简单来看看BufferedOutputStream的实现:
可以看到在BufferedOutputStream内部维护了一个byte[],默认构造的情况下,大小是8192。
如果写,没有超出byte[]大小,那么写入byte[],这个过程当然相当快;如果超出了byte[]大小,那么flushBuffer,而flushBuffer才是真正写入的地方,而且是一次性的写入byte[]。
实际上,利用Buffer功能,将随机写变成了顺序写,当然快了!
- PrintStream
打印字节流,我们经常使用到的System.out,System.err就是PrintStream对象,提供了众多数据格式的print方法。在重定向中,利用System.setOut/setErr,其实都是set到一个PrintStream上。
话题四:IO中的装饰思想
IO流涉及的类那么多,每种流都有自己的特点,如果希望多种功能/特点都聚集在一起,那么就要进行包装和装饰功能了。我们可以以最简单的BUFFER功能,来分析下这个装饰思想。
通过阅读FilterOutputStream类的源码,发现FilterOutputStream类持有OutputStream类的引用,它的每个方法,都没有干啥,就是对持有的OutputStream的引用进行调用对应方法罢了。
BufferedOutputStream extends FilterOutputStream,它内部通过自己的byte[],实现BUFFER功能。
到这里,我们好像发现点端倪了,开始有了些猜测:
那些有特点的IO类,是不是通过extends FilterXXXStream,然后在自己内部“耍些手段”来实现特定功能的呢?
通过上图,可以发现,的确有些有特点的IO类extends FilterXXXStream,比如DataXXXStream/BufferedXXXStream/PrintStream等。
FilterXXXStream就好像一层代理,虽然它什么都没有做,仅仅是通过引用去调用对应方法而已;这样看似无用,其实不然,比如想实现buffer功能,只需要extends FilterXXXStream,然后仅仅重写read/write方法即可,这样很多代码不需要再写,因为从FilterXXXStream上已经获得。Java这样的设计,巧妙!
话题五:flush() / close()
流用完了,要关闭流时,我们经常想到要flush,要close,下面我们来分析分析它们!
2个标示接口,Closeable需要close,Flushable需要flush。
在OutputStream中,实际上它们是空的方法:
很显然,这是希望子类自己去flush,close。如果用到了Buffer机制,特别是通过修饰的功能拥有了BufferedXXXStream的特点,那么一定得注意flush/close,实际上是将byte[]中未处理的数据处理掉。
注意到FileXXXStream并没有去重写flush方法,如果extends FileXXXStream类,又没有提供flush的实现,那么将调用的是OutputStream中一个空的flush方法!比如SocketOutputStream extends FileOutputStream,却没有提供重写的flush方法,说明在SOCKET通信中,并不需要我们去调用flush方法,它是在内核层面上做的缓冲,而不是JAVA层面的。
因此,对一个IO类,要不要flush,要不要close,我们得看看它extends who?有buffer机制吗?
话题六:Java BIO vs NIO vs AIO
关于Java BIO/NIO,在前面的博客中已经有所介绍,大家可以参考下面的:
http://zhangfengzhe.blog.51cto.com/8855103/1715530
http://zhangfengzhe.blog.51cto.com/8855103/1726488
http://zhangfengzhe.blog.51cto.com/8855103/1712845
Java BIO:小张同学有一份快递,于是他去物流中转站去拿货,他在那里等着,一直等到快递来,这段期间,他哪里也不去。
Java NIO:小张同学,每天去一趟物流中转站,检测下是不是快递的货到了,如果到了,就带回来,如果没有则返回,明天继续。
Java AIO:小张同学并不去物流中转站,如果货到了,那么快递员“送货上门”。
上面的例子,形象的说明了BIO/NIO/AIO的特点和区别,胖哥说的好,存在就是有价值的,AIO是最新出来的东西,想法是好的,但是不一定是适合的。比如JDBC程序中,我们发了一个SQL,但是这是个AIO,那么麻烦了,发出这个SQL后,程序立刻向下执行,一段时间后SQL结果送来了,这个时候,我们要拿出保存有发出SQL那个时刻的一些信息。想一想,如果这样的AIO很多的话,程序会复杂很多,保存很多信息,回调很多方法。
这也再次说明了一个道理,如果它简单,那么它内在肯定很复杂,真是因为它内在的复杂,导致了外在的简单!
到这里,JAVA IO那些事就结束了,你学到了吗?