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
        */
    }
View Code

 

(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();
        }
    }
View Code

 

管道——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");
    }
}
View Code

对于上面的例子运行后在控制台进行测试:

hello  //Console input
send message: hello
receive message: hello
-------------------------
world //Console input
send message: world
receive message: world
-------------------------
exit
Sender close
Receiver closed
end
View Code

 

其它的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; 
        */    
View Code

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();
    }
View Code

 ……

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]
        */            
    }
View Code

 

(2)FileOutputStream。可将信息写入文件,可选择覆盖或在末尾添加。

    public static void main(String[] args) throws IOException {
        FileOutputStream fos=new FileOutputStream("D:/Test/log.txt",true);
        fos.write("我是好人".getBytes());
        fos.close();
    }
View Code

 

(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();
    }
View Code

 

(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;
    }
View Code

值得注意的是当读取的长度大于缓冲区的长度,将直接调用数据源输入流的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;
    }
View Code

当输出的长度大于等于缓冲长度时,先将缓冲区中的数据输出,在讲待输出数据直接输出,不经过缓冲区这个过程。

当输出的长度小于缓冲区长度但又大于缓冲区剩余长度时,会先将缓冲区的数据输出,再将待输出数据写入缓冲区。

当输出的长度小于缓冲区长度时,数据会写入缓冲区。

 

jdk1.8中FilterOutputStream的close方法源码如下:

    public void close() throws IOException {
        try (OutputStream ostream = out) {
            flush();
        }
    }
View Code

这是一种新的实现机制,在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
        */
    }
View Code

 

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"));
    }
View Code

当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();
                }
            }
        }

    }
}
View Code

 四、新I/O

jdk1.4引入了新的I/Ol类库java.nio,而旧的I/O也用新I/O重新实现过,所以就算不显示使用,相对旧I/O效率也有了提升。

由于没有什么使用经验,留待以后再说……

posted @ 2016-03-27 18:58  maozs  阅读(281)  评论(0编辑  收藏  举报