对于Java I/O来说,I意味着Input(输入),O意味着Output(输出)。读书写作并非易事,而创建一个好的I/O系统更是一项艰难的任务。
古人云:“读书破万卷,下笔如有神”。也就是说,只有大量的阅读,写作的时候才能风生水起——写作意味着输出(我的知识传播给他人),而读书意味着输入(从他人的知识中汲取营养)。
01、数据流之字节与字符
Java所有的I/O机制都是基于数据流进行的输入输出。数据流可分为两种:
1)字节流,未经加工的原始二进制数据,最小的数据单元是字节。
2)字符流,经过一定编码处理后符合某种格式规定的数据,最小的数据单元是字符——占用两个字节。
OutputStream和InputStream用来处理字节流;Writer和Reader用来处理字符流;OutputStreamWriter可以把OutputStream转换为Writer,InputStreamReader可以把InputStream转换为Reader。
Java的设计者为此设计了众多的类,见下图。
看到这么多类,你一定感觉头晕目眩。反正我已经看得不耐烦了。搞这么多类,看起来头真的大——这也从侧面说明实际的应用场景各有各的不同——你也完全不用担心,因为实际项目当中,根本就不可能全用到(我就没用过SequenceOutputStream)。
我建议你在学习的时候要掌握一种“挑三拣四”的能力——学习自己感兴趣的、必须掌握的、对能力有所提升的知识。切不可囫囵吞枣,强迫自己什么都学。什么都学,最后的结果可能是什么都不会。
字符流是基于字节流的,因此,我们先来学习一下字节流的两个最基础的类——OutputStream和InputStream,它们是必须要掌握的。
1)OutputStream
OutputStream提供了4个非常有用的方法,如下。
public void write(byte b[]):将数组b中的字节写到输出流。
public void write(byte b[], int off, int len):将数组b的从偏移量off开始的len个字节写到输出流。
public void flush() : 将数据缓冲区中数据全部输出,并清空缓冲区。
public void close() : 关闭输出流并释放与流相关的系统资源。
其子类ByteArrayOutputStream和BufferedOuputStream最为常用(File相关类放在下个小节)。
①、ByteArrayOutputStream通常用于在内存中创建一个字节数组缓冲区,数据被“临时”放在此缓冲区中,并不会输出到文件或者网络套接字中——就好像一个中转站,负责把输入流中的数据读入到内存缓冲区中,你可以调用它的toByteArray()方法来获取字节数组。
来看下例。
public static byte[] readBytes(InputStream in, long length) throws IOException { ByteArrayOutputStream bo = new ByteArrayOutputStream(); byte[] buffer = new byte[1024]; int read = 0; while (read < length) { int cur = in.read(buffer, 0, (int) Math.min(1024, length - read)); if (cur < 0) { break; } read += cur; bo.write(buffer, 0, cur); } return bo.toByteArray(); }
ByteArrayOutputStream的责任就是把InputStream中的字节流“一字不差”的读出来——这个工具方法很重要,很重要,很重要——可以解决粘包的问题。
②、BufferedOuputStream实现了一个缓冲输出流,可以将很多小的数据缓存为一个大块的数据,然后一次性地输出到文件或者网络套接字中——这里的“缓冲”和ByteArrayOutputStream的“缓冲”有着很大的不同——前者是为了下一次的一次性输出,后者就是单纯的为了缓冲,不存在输出。
来看下例。
protected void write(byte[] data) throws IOException { out.write(intToByte(data.length)); out.write(data); out.flush(); out.close(); } public static byte[] intToByte(int num) { byte[] data = new byte[4]; for (int i = 0; i < data.length; i++) { data[3-i] = (byte)(num % 256); num = num / 256; } return data; }
使用BufferedOuputStream的时候,一定要记得调用flush()方法将数据从缓冲区中全部输出。使用完毕后,调用close()方法关闭输出流,释放与流相关的系统资源。
2)InputStream
InputStream也提供了4个非常有用的方法,如下。
public int read(byte b[]):读取b.length个字节的数据放到数组b中,返回值是读取的字节数。
public int read(byte b[], int off, int len):从输入流中最多读取len个字节的数据,存放到偏移量为off的数组b中。
public int available():返回输入流中可以读取的字节数。
public int close() :使用完后,对打开的流进行关闭。
其子类BufferedInputStream(缓冲输入流)最为常用,效率最高(当我们不确定读入的是大数据还是小数据)。
无缓冲流上的每个读取请求通常会导致对操作系统的调用以读取所请求的字节数——进行系统调用的开销非常大。但缓冲输入流就不一样了,它通过对内部缓冲区执行(例如)高达8k字节的大量读取,然后针对缓冲区的大小再分配字节来减少系统调用的开销——性能会提高很多。
使用示例如下。
先来看一个辅助方法byteToInt,把字节转换成int。
public static int byteToInt(byte[] b) { int num = 0; for (int i = 0; i < b.length; i++) { num*=256; num+=(b[i]+256)%256; } return num; }
再来看如何从输入流中,根据指定的长度contentLength来读取数据。readBytes()方法在之前已经提到过。
BufferedInputStream in = new BufferedInputStream(socket.getInputStream()); byte[] tmpByte = new byte[4]; // 读取四个字节判断消息长度 in.read(tmpByte, 0, 4); // 将byte转为int int contentLength = byteToInt(tmpByte); byte[] buf = null; if (contentLength > in.available()) { // 之前提到的方法 buf = readBytes(in, contentLength); } else { buf = new byte[contentLength]; in.read(buf, 0, contentLength); // 发生粘包了 if (in.available() > 0) { } }
我敢保证,只要你搞懂了字节流,字符流也就不在话下——所以,我们在此略过字符流。
