java之I/O流
I/O流的使用情况多种多样,首先它的数据源就可能是文件、控制台、服务器等,它的单位可能是按字节、按字符、按行等。为了涵盖所有的可能,java类库中创建了大量的类,如此多的类让我们在使用时感觉有点难以选择。
I/O流按数据单位可分为字节流(InputStream/OutputStream)和字符流(Reader/Writer),其它的类主要是从这四个抽象类中派生出来的。除此之外,还有一个独立于这两种流之外的类——RamdomAccessFile。
一、字节流(InputStream/OutputStream)
1.InputStream
每一种数据源都有相应的InputStream的子类:
(1)字节数组——ByteArrayInputStream:
它能将字节数组转化为输入流,下面为简单示例:
public static void main(String[] args) { byte[] strArr={11,22,45,65}; ByteArrayInputStream bais=new ByteArrayInputStream(strArr); int b; while((b=bais.read())!=-1){ System.out.println(b); } /* output: 11 22 45 65 */ }
(3)文件——FileInputStream:
将文件转换为输入流,下面为简单的文本输出操作示例:
public static void main(String[] args) throws FileNotFoundException { File file=new File("D:/Test/log.txt"); FileInputStream fis=new FileInputStream(file); byte[] buffer=new byte[8*1024]; int len; try { while((len=fis.read(buffer))!=-1){ System.out.println(new String(buffer,0,len)); } } catch (IOException e) { e.printStackTrace(); } }
管道——PipedInputStream:
需和PipeOutputStream搭配使用,下面是一个简单的示例:
public class PipedStreamTest { public static void main(String[] args) { Sender sender = new Sender(); Receiver receiver = new Receiver(); PipedOutputStream outStream = sender.getOutStream(); PipedInputStream inStream = receiver.getInStream(); try { inStream.connect(outStream); // 与outStream.connect(inStream);等效,二选其一 } catch (Exception e) { e.printStackTrace(); } sender.start(); receiver.start(); } } class Sender extends Thread { private PipedOutputStream outStream = new PipedOutputStream(); public PipedOutputStream getOutStream() { return outStream; } public void run() { Scanner in = new Scanner(System.in); while (true) { String str = in.nextLine(); if (str.equals("exit")) { break; } try { System.out.println("send message: " + str); outStream.write(str.getBytes()); } catch (IOException e) { e.printStackTrace(); } } try { if (outStream != null) outStream.close(); } catch (IOException e) { e.printStackTrace(); } System.out.println("Sender close"); } } class Receiver extends Thread { private PipedInputStream inStream = new PipedInputStream(); public PipedInputStream getInStream() { return inStream; } public void run() { byte[] buffer = new byte[1024]; int len; try { while ((len = inStream.read(buffer)) != -1) { System.out.println("receive message: " + new String(buffer, 0, len)); System.out.println("-------------------------"); } } catch (IOException e1) { e1.printStackTrace(); } try { if (inStream != null) inStream.close(); } catch (IOException e) { e.printStackTrace(); } System.out.println("Receiver closed"); System.out.println("end"); } }
对于上面的例子运行后在控制台进行测试:
hello //Console input send message: hello receive message: hello ------------------------- world //Console input send message: world receive message: world ------------------------- exit Sender close Receiver closed end
其它的InputStream流集合——SequenceInputStream
将两个InputStream转化成一个InputStream:
byte[] bytes1={12,34}; byte[] bytes2={56,78}; ByteArrayInputStream bais1=new ByteArrayInputStream(bytes1); ByteArrayInputStream bais2=new ByteArrayInputStream(bytes2); SequenceInputStream sis=new SequenceInputStream(bais1,bais2); int b; try { while((b=sis.read())!=-1){ System.out.print(b+"; "); } } catch (IOException e) { e.printStackTrace(); } /* output: 12; 34; 56; 78; */
SequenceInputStream可以将不同类型的InputStream转化为一个InputStream,当InputStream大于两个的时候,需要用Vector类:
public static void main(String[] args) throws IOException { InputStream is1=new ByteArrayInputStream(new byte[]{111,112,113}); InputStream is2=new FileInputStream("D:/log.txt"); InputStream is3=new URL("http://www.baidu.com").openStream(); Vector<InputStream> v=new Vector<>(); v.add(is1); v.add(is2); v.add(is3); SequenceInputStream is=new SequenceInputStream(v.elements()); byte[] buffer=new byte[1024]; int len; int count=0; while((len=is.read(buffer))!=-1){ System.out.println(new String(buffer,0,len)); if(++count%100==0){ break; } } is.close(); }
……
2.OutputStream
(1)ByteArrayOutputStream。在内存中创建缓冲区,将写入的数据保存在缓冲区中。
public static void main(String[] args) throws FileNotFoundException { ByteArrayOutputStream baos=new ByteArrayOutputStream(); baos.write(12); baos.write(23); baos.write(34); byte[] bytes=baos.toByteArray(); System.out.println(Arrays.toString(bytes)); /* output: [12, 23, 34] */ }
(2)FileOutputStream。可将信息写入文件,可选择覆盖或在末尾添加。
public static void main(String[] args) throws IOException { FileOutputStream fos=new FileOutputStream("D:/Test/log.txt",true); fos.write("我是好人".getBytes()); fos.close(); }
(3)PipeOutputSream。需和PipeOutputStream搭配使用,上文中已经提到,这里就不再叙述。
……
上面所提及的几种类型都是InputStream/OutputStream的直接子类,是进行I/O流操作的几种基本类型,但是对于写入和读取的数据类型限制太大,比如如果我们如果想要直接写入float或long类型的数据,就无法完成。
要实现这个需求,需要对InputStream和OutputStream进行封装处理,这里就得提一下两个装饰器类:FilterInputStream和FiltOutputStream。
FilterInputStream和FiltOutputStream分别是InputStream和OutputStream的子类,它们增加了I/O流操作的灵活性,但同时也增加了代码的复杂性。
这两个类是I/O操作的基础类,通常我们不会直接使用,而是用功能更加具体完善的它们的子类。
1.FilterInputStream
FilterInputStream的常见子类有DataInputStream、BufferedInputStream。
(1)DataInputStream
与DataOutputStream搭配使用,主要特点是可以读写基本类型数据,不过需要读写对应。
public static void main(String[] args) throws IOException { String path="D:/test.txt"; DataOutputStream dos=new DataOutputStream(new FileOutputStream(path)); DataInputStream dis=new DataInputStream(new FileInputStream(path)); dos.writeInt(110); dos.writeUTF("我是好人"); System.out.println(dis.readInt()); System.out.println(dis.readUTF()); dis.close(); dos.close(); }
(2)BufferedInputStream
使用了缓冲区,避免每次读取时都进行实际操作,可指定缓冲区大小,默认为8192byte。每次读取时,首先会从缓冲区获取,若缓冲区中读取完了则从数据源输入流中重新获取数据至缓冲区中。
下面为读取操作的源码:
private int read1(byte[] b, int off, int len) throws IOException { int avail = count - pos; if (avail <= 0) { /* If the requested length is at least as large as the buffer, and if there is no mark/reset activity, do not bother to copy the bytes into the local buffer. In this way buffered streams will cascade harmlessly. */ if (len >= getBufIfOpen().length && markpos < 0) { return getInIfOpen().read(b, off, len); } fill(); avail = count - pos; if (avail <= 0) return -1; } int cnt = (avail < len) ? avail : len; System.arraycopy(getBufIfOpen(), pos, b, off, cnt); pos += cnt; return cnt; }
值得注意的是当读取的长度大于缓冲区的长度,将直接调用数据源输入流的read方法。
缓冲是用来减少来自输入设备的单独读取操作数的数量的,因此缓冲有时并不必要,当例如数据块足够大的时候,不需要使用缓冲,如文件的复制就不需要缓冲。
2.FilterOutputStream
FilterOutputStream常见的子类有DataOutputStream、BufferedOutputStream。
(1)DataOutputStream
需和DataInputStream搭配使用,上文中已经提及,此处不再叙述。
(2)BufferedOutputStream
BufferedOutputStream在输出的时候会先将数据写入缓冲区,满足一定条件后再将缓冲区中的数据输出,所以当我们完成输出操作时,需调用flush方法将缓冲区中的数据输出。
下面为输出操作源码:
public synchronized void write(byte b[], int off, int len) throws IOException { if (len >= buf.length) { /* If the request length exceeds the size of the output buffer, flush the output buffer and then write the data directly. In this way buffered streams will cascade harmlessly. */ flushBuffer(); out.write(b, off, len); return; } if (len > buf.length - count) { flushBuffer(); } System.arraycopy(b, off, buf, count, len); count += len; }
当输出的长度大于等于缓冲长度时,先将缓冲区中的数据输出,在讲待输出数据直接输出,不经过缓冲区这个过程。
当输出的长度小于缓冲区长度但又大于缓冲区剩余长度时,会先将缓冲区的数据输出,再将待输出数据写入缓冲区。
当输出的长度小于缓冲区长度时,数据会写入缓冲区。
jdk1.8中FilterOutputStream的close方法源码如下:
public void close() throws IOException { try (OutputStream ostream = out) { flush(); } }
这是一种新的实现机制,在try语句结束后,会自动调用Closeable.close()方法。close方法中调用的flush方法,避免了我们忘记调用flush方法时的输出遗漏,虽然如此,但我们最好还是自己调用flush方法。
二、字符流(Reader/Writer)
InputStream和OutputStream仅支持8位字节流,不能很好地支持16位的Unicode字符,Reader和Writer支持Unicode,实现了国际化。
对于纯文本通常使用字符流,对于其它的例如视频、音频和图片等文件通常使用字节流。
1.InputStreamReader&OutputStreamWriter
有些时候,我们需要将字节流和字符流结合起来使用,因此需要适配器,InputStreamReader和OutputStreamWriter这两个类能够将字节流转化为字符流,同时可以设置字符编码。
OutPutStreamWriter使用的缓冲区,所以输入完成后需要调用flush方法。
2.FileReader&FileWriter
这两个类分别是InputStreamReader和OutputStreamWriter的子类,它们只是简单地增加了文件和文件名转化为流的构造方法。
3.BufferedReader&BufferedWriter
这两个类是文本读写中较为常见,其内部结构与BufferedInputStream&BufferedOutputStream类似。
比较特殊的是输出时的newLine方法可换行,读取时的readLine方法可实现一行一行的读取。
下面为这两个类使用的简单示例:
public static void main(String[] args) throws IOException { String filename = "D:/Test/log.txt"; String charset = "utf-8"; String[] msgArr={"I'm line 1","I'm line 2","I'm line 3","I'm line 4","I'm line 5"}; BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(new FileOutputStream(filename), charset)); BufferedReader reader = new BufferedReader(new InputStreamReader(new FileInputStream(filename), charset)); for (String string : msgArr) { writer.write(string); writer.newLine(); } writer.flush(); StringBuilder sb=new StringBuilder(); String line; while((line=reader.readLine())!=null){ sb.append(line+"\n"); } writer.close(); reader.close(); System.out.println(sb.toString()); /* output: I'm line 1 I'm line 2 I'm line 3 I'm line 4 I'm line 5 */ }
4.PrintWriter
这个类最大的特点是有很多的构造方法,方便我们面对各种情况是的使用,它还增加了各种类型数据输出,包括long、float、double等。
它的一个构造方法如下:
public PrintWriter(Writer out, boolean autoFlush) { super(out); this.out = out; this.autoFlush = autoFlush; lineSeparator = java.security.AccessController.doPrivileged( new sun.security.action.GetPropertyAction("line.separator")); }
当autoFlush为true的时候,每当调用newLine方法,将自动清空缓冲区,不过newLine是私有方法,需通过println方法调用。
……
三、RamdomAccessFile
RamdomAccessFile支持对随机访问文件的读取和写入,常用于多线程下载。
下面来看看RamdomAccessFile几个常用方法:
1. void setLength(long newLength)
此方法可设置文件大小,当文件原来的大小大于设置的大小,将会把文件裁剪为设置大小。
2. int length()
返回文件的大小。
3. long getFilePointer()
返回指针的位置。
4. void seek(long pos)
移动指针至pos位置。
下面为一个简单的示例:
import java.io.IOException; import java.io.RandomAccessFile; public class Test { public static final String CHARSET = "utf-8"; public static final String[] MSG_ARR = { "1床前明月光,", "12疑是地上霜,", "123举头望明月,", "1234低头思故乡。" }; public static final int[] POS_ARR = new int[5]; public static final String FILENAME = "D:/log.txt"; public static void main(String[] args) throws IOException, InterruptedException { POS_ARR[0] = 0; int count = 0; for (int i = 0; i < 4; i++) { count += MSG_ARR[i].getBytes(CHARSET).length; POS_ARR[i + 1] = count; } RandomAccessFile raf = new RandomAccessFile(FILENAME, "rw"); raf.setLength(POS_ARR[4]); for (int i = 0; i < 4; i++) { RafThread rt = new RafThread(FILENAME, POS_ARR[i], MSG_ARR[i]); rt.start(); } Thread.sleep(100); String line; while ((line = raf.readLine()) != null) { System.out.println(new String(line.getBytes("ISO-8859-1"), CHARSET)); } raf.close(); /* outtput: 1床前明月光,12疑是地上霜,123举头望明月,1234低头思故乡。 */ } } class RafThread extends Thread { private String filename; private int po; private String msg; public RafThread(String filename, int po, String msg) { this.filename = filename; this.po = po; this.msg = msg; } @Override public void run() { RandomAccessFile raf = null; try { raf = new RandomAccessFile(filename, "rw"); raf.seek(po); raf.write(msg.getBytes("utf-8")); } catch (IOException e) { e.printStackTrace(); } finally { if (raf != null) { try { raf.close(); } catch (IOException e) { e.printStackTrace(); } } } } }
四、新I/O
jdk1.4引入了新的I/Ol类库java.nio,而旧的I/O也用新I/O重新实现过,所以就算不显示使用,相对旧I/O效率也有了提升。
由于没有什么使用经验,留待以后再说……