Java IO

作者: 不惜留恋_ | 来源:发表于2018-03-28 21:56 被阅读80次
IO.png

字节流

基本输入输出流

抽象类 InputStreamOutputStream 为字节流输入和输出的基类。

InputStream

InputStream定义了读取单个字节的方法。

// InputStream.java
public abstract int read() throws IOException;

在读写单字节的基础上,还封装了读取多个字节的方法。

    // InputStream.java
    public int read(byte b[]) throws IOException {
        return read(b, 0, b.length);
    }

    public int read(byte b[], int off, int len) throws IOException {
        if (b == null) {
            throw new NullPointerException();
        } else if (off < 0 || len < 0 || len > b.length - off) {
            throw new IndexOutOfBoundsException();
        } else if (len == 0) {
            return 0;
        }

        int c = read();
        if (c == -1) {
            return -1;
        }
        b[off] = (byte)c;

        int i = 1;
        try {
            for (; i < len ; i++) {
                c = read();
                if (c == -1) {
                    break;
                }
                b[off + i] = (byte)c;
            }
        } catch (IOException ee) {
        }
        return i;
    }
     

读取多个字节的方法的原理还是一个一个字节的读,但是在实现类中,往往因为输入流的类型的不同而覆写这个方法。

OutputStream

OuputStream,定义了写入单个字节的方法

public abstract void write(int b) throws IOException;

同时也定义了写入多个字节的方法

    public void write(byte b[]) throws IOException {
        write(b, 0, b.length);
    }

    public void write(byte b[], int off, int len) throws IOException {
        if (b == null) {
            throw new NullPointerException();
        } else if ((off < 0) || (off > b.length) || (len < 0) ||
                   ((off + len) > b.length) || ((off + len) < 0)) {
            throw new IndexOutOfBoundsException();
        } else if (len == 0) {
            return;
        }
        for (int i = 0 ; i < len ; i++) {
            write(b[off + i]);
        }
    }

多字节写入的原理是一个一个字节的写,但是在实现类中,往往因为输出流的类型的不同而覆写这个方法。

字节数组输入输出流

ByteArrayInputStream

ByteArrayInputStream 代表输入源为字节数组的输入流,因此接受一个字节数组作为输入,并用一个字节缓存数组保存输入源的数据。

    protected byte buf[];

    public ByteArrayInputStream(byte buf[]) {
        this.buf = buf;
        this.pos = 0;
        this.count = buf.length;
    }

    public ByteArrayInputStream(byte buf[], int offset, int length) {
        this.buf = buf;
        this.pos = offset;
        this.count = Math.min(offset + length, buf.length);
        this.mark = offset;
    }    

当读取一个字节的时候,就会从缓存的字节数组中取出字节

    public synchronized int read() {
        return (pos < count) ? (buf[pos++] & 0xff) : -1;
    }

synchronized 关键字可以看出,ByteArrayInputStream 读取字节是线程安全的,与之对应的 ByteArrayOuputStream 写字节也是线程安全的。

ByteArrayOuputStream

ByteArrayOutputStream 代表输出源为字节数组的输出流,因此与 ByteArrayInputStream 一样,内部有持有一个字节数组。

protected byte buf[];

它把读取到的字节存放到这个字节数组中。

    public synchronized int read() {
        return (pos < count) ? (buf[pos++] & 0xff) : -1;
    }

ByteArrayOuputStream 能使用 toByteArray() 返回字节数组,还可以通过 toString() 返回字符串

    public synchronized byte toByteArray()[] {
        return Arrays.copyOf(buf, count);
    }

    public synchronized String toString() {
        return new String(buf, 0, count);
    }    

举例

这里我们以 ByteArrayInputStream 作为输出流,以 ByteArrayOuputStream 作为输入流举例。

        String s = "Hello David!";
        ByteArrayInputStream is = new ByteArrayInputStream(s.getBytes());
        ByteArrayOutputStream os = new ByteArrayOutputStream();
        int b;
        while ((b = is.read()) != -1) {
            os.write(b);
        }
        String s1 = os.toString();
        System.out.println(s1);

这个例子并没有去手动关闭流,是因为 ByteArrayInputStreamByteArrayOuputStream 是用内部数组实现的,关闭这两个流也没有太大意义,查看源码也可以发现 close() 为一个空方法。

文件输入输出流

FileInputStreamFileInputOuputStream 定义了文件的输入输出流。

FileInputStream

FileInputStream 代表输入源为文件作的输入流,它可以传入一个文件路径,也可以传入一个 File 对象。

    public FileInputStream(String name) throws FileNotFoundException {
        this(name != null ? new File(name) : null);
    }

    public FileInputStream(File file) throws FileNotFoundException {}    

如果更接近操作系统一点,也可以传入一个文件描述符

public FileInputStream(FileDescriptor fdObj){}

FileInputStream 读取字节的方式依赖于操作系统,这里就不深追究原理了,只要明白是从文件读取字节即可。

FileOuputStream

FileOutStream代表输出源为文件的输出流,用来向文件写数据(字节)。

FileInputStream 的构造一样,FileOuputStream 可以用文件路径,File对象,文件描述符来指定文件。

但是在构造函数中有一点需要注意,它还有一个参数,表明向文件写数据的方式是追加还是覆盖,默认的是覆盖。

    public FileOutputStream(String name, boolean append)
        throws FileNotFoundException
    {
        this(name != null ? new File(name) : null, append);
    }

    public FileOutputStream(String name, boolean append)
        throws FileNotFoundException
    {
        this(name != null ? new File(name) : null, append);
    }

举例

文件一般都是比较大的,如果每次读写一个字节,那太慢了,如果每次读写多个字节,那就可以提高效率,尤其在读写大文件的时候特别明显。

    private void readByBuffer(String srcFile, String dstFile) {
        long start = System.currentTimeMillis();
        FileInputStream fis = null;
        FileOutputStream fos = null;
        byte[] buff = new byte[1024];
        int length;
        try {
            fis = new FileInputStream(srcFile);
            fos = new FileOutputStream(dstFile);
            while ((length = fis.read(buff, 0, buff.length)) != -1) {
                fos.write(buff, 0, length);
            }
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            try {
                if (fis != null) {
                    fis.close();
                }
                if (fos != null) {
                    fos.close();
                }
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
        System.out.println("copy time: " + (System.currentTimeMillis() - start));
    }

    private void readByByte(String srcFile, String dstFile) {
        long start = System.currentTimeMillis();
        FileInputStream fis = null;
        FileOutputStream fos = null;
        int b;
        try {
            fis = new FileInputStream(srcFile);
            fos = new FileOutputStream(dstFile);
            while ((b = fis.read()) != -1) {
                fos.write(b);
            }
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            try {
                if (fis != null) {
                    fis.close();
                }
                if (fos != null) {
                    fos.close();
                }
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
        System.out.println("copy time: " + (System.currentTimeMillis() - start));
    }

管道输入输出流

有时候一个线程需要不断的发送数据到另外一个线程,如果不采用管道,就需要对这个数据加锁进行控制访问,才能达到线程安全,基于此原理,就有了PipedInputStreamPipedOuputStream

PipedInputStream 和 PipedOutputStream

PipedInputStreamPipedOutputStream 的原理就是不同线程访问一个临界资源,这个临界资源就是字节数组

protected byte buffer[];

PipedOutputStream 负责写出数据

   public void write(int b)  throws IOException {
        if (sink == null) {
            throw new IOException("Pipe not connected");
        }
        sink.receive(b);
    }

而实际是调用 PipedInputStreamreceiver() 方法接收数据

    protected synchronized void receive(int b) throws IOException {
        checkStateForReceive();
        writeSide = Thread.currentThread();
        if (in == out)
            awaitSpace();
        if (in < 0) {
            in = 0;
            out = 0;
        }
        buffer[in++] = (byte)(b & 0xFF);
        if (in >= buffer.length) {
            in = 0;
        }
    }

而从 PipedInputStream 中读取数据也是从这个字符数组中读取的

    public synchronized int read()  throws IOException {
        // ...
        int ret = buffer[out++] & 0xFF;
        // ...
        return ret;
    }

byte buffer[] 就是线程之间的临界资源,因此 PipedInputStreamPipedOuputStream 就用了 synchronized 关键字保证了线程安全。

为了知道写到哪个管道,因此管道输入流必须和管道输出流相连,这个可以用构造函数来相连,也可以通过 connect() 方法来相连

    // PipedInputStream.java
    public PipedInputStream(PipedOutputStream src) throws IOException {
        this(src, DEFAULT_PIPE_SIZE);
    }

    // PipedOuputStream.java
    public PipedOutputStream(PipedInputStream snk)  throws IOException {
        connect(snk);
    }

    // PipedInputStream.java
    public void connect(PipedOutputStream src) throws IOException {
        src.connect(this);
    }    

    // PipedOutputStream.java
    public synchronized void connect(PipedInputStream snk) throws IOException {
    }    

可以注意到 PipedInputStreamconnect() 方法实际是调用的 PipedOuputStreamconnect() 方法,因此两个方法都是线程安全的。

在同一线程使用管道输入输出流容易造成死锁,因为如果不写入数据,而一直读数据的话,线程就会一直阻塞。

举例

首先写一个用 PipedOuputStream 发送数据的任务

public class Sender implements Runnable {
    private PipedOutputStream pos = new PipedOutputStream();

    public PipedOutputStream getPos() {
        return pos;
    }

    @Override
    public void run() {
        String s = "Hello World!!!";
        int length = s.getBytes().length;
        if (length < 1024) {
            try {
                pos.write(s.getBytes());
            } catch (IOException e) {
                e.printStackTrace();
            } finally {
                try {
                    pos.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}

然而写一个用 PipedInputStream 接收数据的任务

public class Receiver implements Runnable {
    private PipedInputStream pis = new PipedInputStream();

    public PipedInputStream getPis() {
        return pis;
    }

    @Override
    public void run() {
        byte[] buffer = new byte[1024];
        try {
            int length = pis.read(buffer);
            if (length > 0) {
                String s = new String(buffer, 0, length);
                System.out.println("read string: " + s);
            }
        } catch (IOException e) {
            e.printStackTrace();
        }finally {
            try {
                pis.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
}

现在把这两个任务提交到线程池来执行。

public class Test {
    public static void main(String[] args) {
        ExecutorService executorService = Executors.newCachedThreadPool();
        Sender sender = new Sender();
        Receiver receiver = new Receiver();
        try {
            sender.getPos().connect(receiver.getPis());
            executorService.execute(sender);
            executorService.execute(receiver);
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

这只是一个简单的例子,实际情况中到底如何写数据,定多少数据,如何接收数据,接收后如何转换数据,都要根据实际情况来写代码,这里只是抛砖引玉。

集合流

SequenceInputStream

SequenceInputStream 是把许多流放到一个集合中人,然后按照顺序依次从这个流中读取字节。 这个原理比较简单,看看 API 就能使用,略过~

序列化流和反序列化流

ObjectInputStream 和 ObjectOuputStream

查阅序列化相关的文章(https://blog.csdn.net/javazejian/article/details/52665164).

装饰流

FilterInputStream & FilterOutputStream

FilterInputStreamFilterOutputStream 分别继承自 InputStreamOutputStream。但是它们底层的读写操作分别是用另外一个输入和输出流。

它们就相当于一个最简单的装饰类或者代理类。例如 FilterInputStream 的读取操作如下:

public class FilterInputStream extends InputStream {
    protected volatile InputStream in;
    protected FilterInputStream(InputStream in) {
        this.in = in;
    }

    public int read() throws IOException {
        return in.read();
    }
}

虽然 FilterInputStreamFilterOutputStream 并没有实质的用处,但是它们的子类在它的基础上添加了一些功能,子类功能如下

  1. DataInputStreamDataOutputStream 封装了可以读取基本类型数据的方法,例如 readInt()
  2. BufferedInputStreamBufferedOuputStream 的带了缓冲的输入输出流。
  3. PushBackInputStream(FilterInputStream子类) 可以回退一个字节,再重新读取
  4. PrintStream(FilterOuputStream子类) 可以很方便的写出各种格式化的数据,例如 println() 方法可以在写出数据后添加一个换行符。

DataInputStream & DataOuptStream

如前面所说,DataInputStreamDataOuputStream 封装了读取基本类型的数据的方法,它们的原理就是根据要读取数据的类型,读取相应的字节数,例如 readInt() 就是读取四个字节,然后转换成 int 类型。

    public final int readInt() throws IOException {
        readFully(readBuffer, 0, 4);
        return Memory.peekInt(readBuffer, 0, ByteOrder.BIG_ENDIAN);
    }

    public final void readFully(byte b[], int off, int len) throws IOException {
        if (len < 0)
            throw new IndexOutOfBoundsException();
        int n = 0;
        while (n < len) {
            int count = in.read(b, off + n, len - n);
            if (count < 0)
                throw new EOFException();
            n += count;
        }
    }

举例

public class Test {
    public static void main(String[] args) {
        DataOutputStream dos = null;
        DataInputStream dis = null;
        try {
            ByteArrayOutputStream bos = new ByteArrayOutputStream();
            dos = new DataOutputStream(bos);
            dos.writeInt(110);
            dos.writeUTF("Hello");
            dis = new DataInputStream(new ByteArrayInputStream(bos.toByteArray()));
            System.out.println("read int value : " + dis.readInt());
            System.out.println("read UTF string : " + dis.readUTF());
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            try {
                if (dos != null) {
                    dos.close();
                }
                if (dis != null) {
                    dis.close();
                }
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
}

DataOuputStream 用一个 ByteArrayOuputStream 构造,writeInt()等一些方法写出的数据,实际是写入到了 ByteArrayOuputStream 的内部字节数组中。

DataInputStream 用一个 ByteArrayInputStream 构造,readInt() 等一些方法其实是从 ByteArrayInputStream 中读取的。而 ByteArrayInputStream 的数据源其实就来源于 ByteArrayOuputStream 的内部字节数组。

缓存输入输出流

BufferedInputStream

BufferedInputStream 在创建的时候,会一起创建一个字节缓冲数组,这个缓冲字节数组是可以指定大小的。

    private static final int DEFAULT_BUFFER_SIZE = 8192;

    public BufferedInputStream(InputStream in) {
        this(in, DEFAULT_BUFFER_SIZE);
    }

    public BufferedInputStream(InputStream in, int size) {
        super(in);
        if (size <= 0) {
            throw new IllegalArgumentException("Buffer size <= 0");
        }
        buf = new byte[size];
    }    

可以看到,默认的缓冲字节数组是 8192 个字节,也就是 8k

当读取数据的时候,如果缓存字节数组中没有数据可读,就会先进行填充

    private void fill() throws IOException {
        // ...
        // 从底层输入流中读取字节到 buffer
        int n = in.read(buffer, pos, buffer.length - pos);
        // ...
    }

然而,再从缓存数组中读取

    public synchronized int read() throws IOException {
        if (pos >= count) {
            fill();
            if (pos >= count)
                return -1;
        }
        return buffer[pos++] & 0xff;
    }

从这里就可以看出,BufferedInputStream 实际就是调用底层输入流的 read(byte[] buff, int off, int length) 方法,那为何还要封装成一个类呢? 理由如下:

  1. 它提醒我们开发者,带缓冲的输入流才是正常的输入流。而一般我们使用的输入流并不是带缓冲的。
  2. 它的所有方法都是线程安全的。从上面的 read() 方法可以看出。
  3. 它支持 mark()reset() 方法来读取指定位置的缓存字节。

BufferedOuputStream

BufferedOuputStream 在创建的时候,也会创建一个缓冲字节数组,与 BufferedInputStream 一样,是在构造函数中指定。

那看看它是如何写一个字节的

    public synchronized void write(int b) throws IOException {
        // 如果写入的字节数超过缓存长度,就刷新缓存
        if (count >= buf.length) {
            flushBuffer();
        }
        // 把字节放入到缓存
        buf[count++] = (byte)b;
    }

    private void flushBuffer() throws IOException {
        if (count > 0) {
            out.write(buf, 0, count);
            count = 0;
        }
    }    

从实现我们惊讶的看到,如果刚开始字节,是会直接放到缓存数组中,只有当缓存数组放不下的时候,才会刷新缓存来真正的写字节。那么我们可以手动刷新缓存

    public synchronized void flush() throws IOException {
        flushBuffer();
        out.flush();
    }

或者关闭 BufferedOuputStream 来达到刷新的目的

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

例子

public class Test {
    public static void main(String[] args) {
        BufferedInputStream bis = null;
        BufferedOutputStream bos = null;
        try {
            bis = new BufferedInputStream(new FileInputStream("Kalimba.mp3"));
            bos = new BufferedOutputStream(new FileOutputStream("KalimpaCopy.mp3"));
            byte[] buff = new byte[4 * 1024];
            int length;
            while ((length = bis.read(buff, 0, buff.length)) != -1) {
                bos.write(buff, 0, length);
                // 可以手动刷新缓存
                // bos.flush();
            }
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            try {
                if (bis != null) {
                    bis.close();
                }
                if (bos != null) {
                    // 关闭输出流,可以刷新缓存
                    bos.close();
                }
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
}

打印流

PrintStream

PrintStream 可以很方便以各种格式写入数据,例如可以写完后换行的 println()

    public void println(int x) {
        synchronized (this) {
            print(x);
            newLine();
        }
    }

    public void print(int i) {
        write(String.valueOf(i));
    }    

PrintStream 还有一个特色,可以在构造参数中设置自动刷新

    public PrintStream(OutputStream out) {
        this(out, false);
    }

    public PrintStream(OutputStream out, boolean autoFlush) {
        this(autoFlush, requireNonNull(out, "Null output stream"));
    }    

字符流

基本字符输入输出流

Reader & Writer

ReaderWriter 是字符流的基类,它们定义了读写单个字符的方法

    // Reader.java
    public int read() throws IOException {}

    // Writer.java
    public void write(int c) throws IOException {}

字符 charint 形式表示

当然也定义了读写多个字符的方法

    // Reader.java
    public int read(char cbuf[]) throws IOException {}
    abstract public int read(char cbuf[], int off, int len) throws IOException;

    // Writer.java
    public void write(char cbuf[]) throws IOException {}
    abstract public void write(char cbuf[], int off, int len) throws IOException;

几乎所有的字符输入输出流都是线程安全的,因为它们都有一个用于同步的对象

    protected Object lock;
    
    protected Reader() {
        this.lock = this;
    }

    protected Reader(Object lock) {
        if (lock == null) {
            throw new NullPointerException();
        }
        this.lock = lock;
    }    

字符数组输入输出流

CharArrayReader & CharArrayWriter

类似于 ByteArrayInputStreamByteArrayOuputStreamCharArrayReaderCharArrayWriter 内部包含一个字符数组

    /** The character buffer. */
    protected char buf[];

读的时候,就从这个字符数组中读

    // CharArrayReader.java
    protected char buf[];

    public int read() throws IOException {
        synchronized (lock) {
            ensureOpen();
            if (pos >= count)
                return -1;
            else
                return buf[pos++];
        }
    }

写的时候呢,就往这个字节数组中写

    // CharArrayWriter.java
    private char[] writeBuffer;

    public void write(int c) {
        synchronized (lock) {
            int newcount = count + 1;
            if (newcount > buf.length) {
                // 如果数组长度不够,就扩充数组
                buf = Arrays.copyOf(buf, Math.max(buf.length << 1, newcount));
            }
            buf[count] = (char)c;
            count = newcount;
        }
    }

从上面代码中的synchronized关键字可知,CharArrayReaderCharArrayWriter 的方法都是线程安全的。

例子

public class JavaIO {

    public static void main(String[] args) {
        String s = "hello中国";
        CharArrayReader reader = new CharArrayReader(s.toCharArray());
        CharArrayWriter writer = new CharArrayWriter();
        int c;
        try {
            while ((c = reader.read()) != -1) {
                writer.write(c);
            }
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            reader.close();
            writer.close();
        }
        System.out.println(writer.toString());
    }
}

字符串输入输出流

StringReader 表示输入源是字符串,StringReader 表示输出源是字符串。

StingReader

既然 StringReader 代表的输入源是字符串,那么构造函数就需要一个字符串作为参数

    private String str;
    public StringReader(String s) {
        this.str = s;
        this.length = s.length();
    }

既然内部用一个 String 实现的,那么读取字符也是从这个 String 对象中读的

    public int read() throws IOException {
        synchronized (lock) {
            // ...
            return str.charAt(next++);
        }
    }

    public int read(char cbuf[], int off, int len) throws IOException {
        synchronized (lock) {
            // ...
            int n = Math.min(length - next, len);
            str.getChars(next, next + n, cbuf, off);
            next += n;
            return n;
        }
    }    

从实现看,read() 方法是线程安全的。

StringWriter

StringWriter 代表输出源是字符串,内部却是用 StringBuffer 实现的

    private StringBuffer buf;

    public StringWriter() {
        buf = new StringBuffer();
        lock = buf;
    }

   public StringWriter(int initialSize) {
        if (initialSize < 0) {
            throw new IllegalArgumentException("Negative buffer size");
        }
        buf = new StringBuffer(initialSize);
        lock = buf;
    }

提起 StringBuffer 自然想到的就是线程安全,所以它的写操作不会像 StingReader 那样需要加锁

    public void write(int c) {
        buf.append((char) c);
    }

那有人可能会问,为何不用 String ?因为 String 的不可变性,它的操作每次都会创建新的 String,而 StringBuffer比起 StringBuilder ,既是线程安全,比起 String,又不需要每次操作都去创建新的 String

举例

public class JavaIO {

    public static void main(String[] args) {
        String s = "hello中国";
        StringReader reader = new StringReader(s);
        StringWriter writer = new StringWriter();
        int c;
        try {
            while ((c = reader.read()) != -1) {
                writer.write(c);
            }
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            reader.close();
            try {
                writer.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
        System.out.println(writer.toString());
    }
}

文件输入输出流

FileReaderFileWriter 分别代表输入和输出源为文件。

InputStreamReader & InputStreamWriter

文件作为输入或输出流,只能以字节流的形式存在,如果要从文件中读取字符或者向文件中写入字符,就需要字符流和字节流相互转换,这就是FileReaderFileWriter 的父类 InputStreamReaderOuputStreamWriter 所做的。

InputStreamReader 转换代码如下

    private final StreamDecoder sd;

    public InputStreamReader(InputStream in) {
        super(in);
        try {
            sd = StreamDecoder.forInputStreamReader(in, this, (String)null); // ## check lock object
        } catch (UnsupportedEncodingException e) {
            throw new Error(e);
        }
    }

然后读取字符就是从这个 StreamDecoder 中读取的

    public int read() throws IOException {
        return sd.read();
    }

FileReader & FileWriter

FileReaderFileWriter 分别继承自 InputStreamReaderInputStreamWriter,但是它们只是提供一个文件字节输入和输出流,其它什么也没做,转换啊,读取和写入字符这些功能都是由父类完成的。

例子

public class JavaIO {

    public static void main(String[] args) {
        FileReader reader = null;
        FileWriter writer = null;
        char[] buff = new char[256];
        int length;
        try {
            reader = new FileReader("./src/test/JavaIO.java");
            writer = new FileWriter("JavaIOCopy.java");
            while ((length = reader.read(buff, 0, buff.length)) != -1) {
                writer.write(buff, 0, length);
            }
            // 立即刷新写入到文件中,否则只能在 writer.close() 刷新缓存
            writer.flush();
        } catch (FileNotFoundException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            try {
                if (reader != null) {
                    reader.close();
                }
                if (writer != null) {
                    writer.close();
                }
            } catch (IOException e) {
                // TODO Auto-generated catch block
                e.printStackTrace();
            }
        }
    }
}

管道输入输出流

PipedReaderPipedWriter 代表字符的管道输入和输出流。

PipedReader & PipedWriter

它们的原理与 PipedInputStreamPipedOuputStream 一样。只不过它们之间的临界资源是一个字符数组

char buffer[];

同样,PipedReaderPipedWriter 是用于不同线程之间通信,如果用于同一线程中通信,可能会造成死锁。

例子

// Sender.java
public class Sender implements Runnable {

    private PipedWriter writer = new PipedWriter();
    
    @Override
    public void run() {
        System.out.println("Sender thread: " + Thread.currentThread());
        try {
            writer.write("Hello 中国");
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
    
    public PipedWriter getWriter(){
        return writer;
    }

}

// Receiver.java
public class Receiver implements Runnable {
    private PipedReader reader = new PipedReader();

    @Override
    public void run() {
        System.out.println("Receiver thread: " + Thread.currentThread());
        char[] cs = new char[200];
        try {
            reader.read(cs);
            System.out.println("Read result: " + new String(cs));
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    
    public PipedReader getReader(){
        return reader;
    }
}

// JavaIO.java
public class JavaIO {

    public static void main(String[] args) {
        ExecutorService service = Executors.newCachedThreadPool();
        Sender sender = new Sender();
        Receiver receiver = new Receiver();
        try {
            sender.getWriter().connect(receiver.getReader());
            service.execute(sender);
            service.execute(receiver);
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

缓存输入输出流

BufferedReader

BufferedReader 包装了一个 Reader,并在内部创建了一个字符缓冲数组,默认的缓存数组大小为 8*1024.

    private static int defaultCharBufferSize = 8192;

    public BufferedReader(Reader in) {
        this(in, defaultCharBufferSize);
    }

    public BufferedReader(Reader in, int sz) {
        super(in);
        if (sz <= 0)
            throw new IllegalArgumentException("Buffer size <= 0");
        this.in = in;
        cb = new char[sz];
        nextChar = nChars = 0;
    }

读字符的时候就是从这个缓存字符数组中读的

    public int read() throws IOException {
        synchronized (lock) {
            ensureOpen();
            for (;;) {
                // 如果数组字符已经读完
                if (nextChar >= nChars) {
                    // 填充缓存数组
                    fill();
                    // 如果读到文件末尾就返回 -1
                    if (nextChar >= nChars)
                        return -1;
                }
                if (skipLF) {
                    skipLF = false;
                    if (cb[nextChar] == '\n') {
                        nextChar++;
                        continue;
                    }
                }
                // 从数组中返回字符
                return cb[nextChar++];
            }
        }
    }

从实现可以看到是从缓存数组中读取的,如果缓存数组已经读完了就要先填充再读取,这样就不用每次都从输入源中读一个字符,提高了效率。

BufferedReader 还有一个特色,就是可以读取一行字符串

    public String readLine() throws IOException {
        return readLine(false);
    }

    String  readLine(boolean ignoreLF) throws IOException {
        StringBuffer s = null;
        int startChar;

        synchronized (lock) {
            ensureOpen();
            boolean omitLF = ignoreLF || skipLF;

        bufferLoop:
            for (;;) {
                // 如果没有多的字节可读就先填充
                if (nextChar >= nChars)
                    fill();
                // 填充后,还是没有过多字节可读,代表读到文件结尾了
                if (nextChar >= nChars) { /* EOF */
                    // 如果读到了字符不为空,就直接返回
                    if (s != null && s.length() > 0)
                        return s.toString();
                    else
                    // 否则返回 null
                        return null;
                }
                boolean eol = false;
                char c = 0;
                int i;

                /* Skip a leftover '\n', if necessary */
                if (omitLF && (cb[nextChar] == '\n'))
                    nextChar++;
                skipLF = false;
                omitLF = false;

            charLoop:
                // 查找缓存数组中是否有换行或回车符
                for (i = nextChar; i < nChars; i++) {
                    c = cb[i];
                    if ((c == '\n') || (c == '\r')) {
                        eol = true;
                        break charLoop;
                    }
                }

                startChar = nextChar;
                nextChar = i;

                // 如果找到换行或回车符,就把读到的所有字符返回
                if (eol) {
                    String str;
                    if (s == null) {
                        str = new String(cb, startChar, i - startChar);
                    } else {
                        s.append(cb, startChar, i - startChar);
                        str = s.toString();
                    }
                    nextChar++;
                    if (c == '\r') {
                        skipLF = true;
                    }
                    return str;
                }

                // 如果没有找到换行或回车符,就存储读到的字符,并继续往下读
                if (s == null)
                    s = new StringBuffer(defaultExpectedLineLength);
                s.append(cb, startChar, i - startChar);
            }
        }
    }    

读取一行字符的原理就是先填充再读取,直到遇到换行或者回车字符,就把读到的所有字符返回。但是这一行的签字,显然并不包括换行或者回车换行。

BufferedWriter

BufferedReaderBufferedWriter 一样,带了 8k 的缓存数组,当然在构造函数中也可以指定大小。

写字符当然也是往缓存数组中写,但是有点小的区别

    public void write(int c) throws IOException {
        synchronized (lock) {
            ensureOpen();
            // 超过缓存长度,先刷新缓存
            if (nextChar >= nChars)
                flushBuffer();
            // 写入到缓存数组中
            cb[nextChar++] = (char) c;
        }
    }

    void flushBuffer() throws IOException {
        synchronized (lock) {
            ensureOpen();
            if (nextChar == 0)
                return;
            out.write(cb, 0, nextChar);
            nextChar = 0;
        }
    }

可以看出,如果缓存数组没有满,就直接往缓存数组中写字符,否则是先刷新缓存,再向缓存数组中写字符。因此,如果我们调用了 BufferedWriterwriter() 方法后,如果你想快速看到效果,可以手动调用 flush() 来刷新缓存,也可以在在最后关闭输出流的时候刷新缓存

    public void flush() throws IOException {
        synchronized (lock) {
            flushBuffer();
            out.flush();
        }
    }

    @SuppressWarnings("try")
    public void close() throws IOException {
        synchronized (lock) {
            if (out == null) {
                return;
            }
            try (Writer w = out) {
                flushBuffer();
            } finally {
                out = null;
                cb = null;
            }
        }
    }

例子

我们经常会去复制一个较大的文件,这个时候使用缓存就显然尤为重要

public class FileUtils {
    /**
     * 每次从 BufferedReader 读入字符到缓存数组中,再向 BufferedWriter 写入
     */
    public static void copyFileByCharBuffer(String src, String dst) {
        BufferedReader reader = null;
        BufferedWriter writer = null;
        // 缓存的大小影响效率
        char[] buff = new char[20 * 1024];
        int length;
        try {
            reader = new BufferedReader(new FileReader(src));
            writer = new BufferedWriter(new FileWriter(dst));
            while ((length = reader.read(buff, 0, buff.length)) != -1) {
                writer.write(buff, 0, length);
            }
            // 刷新缓存,让数据写入文件
            writer.flush();
        } catch (FileNotFoundException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            try {
                if (reader != null) {
                    reader.close();
                }
                if (writer != null) {
                    writer.close();
                }
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }

    /**
     * 每次从 BufferedReader 读取一行字符串,再写入到 BufferedWrtier 中
     */
    public static void copyFileByLine(String src, String dst) {
        BufferedReader reader = null;
        BufferedWriter writer = null;
        String line;
        try {
            reader = new BufferedReader(new FileReader(src));
            writer = new BufferedWriter(new FileWriter(dst));
            while ((line = reader.readLine()) != null) {
                // 写入一行数据,但不包括换行符
                writer.write(line);
                // 手动添加换行符
                writer.newLine();
            }
            writer.flush();
        } catch (FileNotFoundException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            try {
                if (reader != null) {
                    reader.close();
                }
                if (writer != null) {
                    writer.close();
                }
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }

    /**
     * 每次从 BufferedInputStream 中读取字节到缓存数组中,再写入到 BufferedOuputStream
     */
    public static void copyFileByByteBuffer(String src, String dst) {
        BufferedInputStream bis = null;
        BufferedOutputStream bos = null;
        // 缓存的大小影响读写的效率
        byte[] buff = new byte[1024];
        int length;
        try {
            bis = new BufferedInputStream(new FileInputStream(src));
            bos = new BufferedOutputStream(new FileOutputStream(dst));
            while ((length = bis.read(buff, 0, buff.length)) != -1) {
                bos.write(buff, 0, length);
            }
            bos.flush();
        } catch (FileNotFoundException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            try {
                if (bis != null) {
                    bis.close();
                }
                if (bos != null) {
                    bos.close();
                }
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }

}

这三个方法在复制文件的时候,例如一个Java 文件,效率都差不多。 但是在复制一个音频文件的时候,通常都是几十兆,以字节缓存的复制方式的效率明显是要高一些,而且复制出来的文件大小也精确些,这个在实际中需要注意。

<< Java编程思想 >> 一书中提到,“尽量尝试使用 Reader 和 Writer”,然而一旦发现有问题,应该使用字节流的类库。

带有行号的输入流

LineNumberReader

LineNumberReader 继承自 BufferedReader,它拥有获取行号的能力,例如 readLine() 方法就可以轻松获取行号

    public String readLine() throws IOException {
        synchronized (lock) {
            String l = super.readLine(skipLF);
            skipLF = false;
            if (l != null)
                lineNumber++;
            return l;
        }
    }

只要读取到一行字符串不为 null,行号就加一。

然而它的设置行号的方法,并没有太大的意思

    public void setLineNumber(int lineNumber) {
        this.lineNumber = lineNumber;
    }

setLineNumber() 并不影响读取的位置,它只是改变这个值(真不知道这个方法有个吊用)。

打印字符输出流

PrintWriter

PrintWriterPrintStream 有着相同的方法,都是为了格式化写入各种数据。

它可以接受 Writer 的字符输出流作为构造参数

    public PrintWriter (Writer out) {
        this(out, false);
    }

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

也可以接受一个 OuputStream 的字节流作为参数

    public PrintWriter(OutputStream out) {
        this(out, false);
    }

    public PrintWriter(OutputStream out, boolean autoFlush) {
        this(new BufferedWriter(new OutputStreamWriter(out)), autoFlush);

        // save print stream for error propagation
        if (out instanceof java.io.PrintStream) {
            psOut = (PrintStream) out;
        }
    }    

其实代码中最后也把字符流转换为了字节流,这个字节流还用 BufferedWriter 包装了一层,为了效率。

如果面对输出源是文件,它还提供了一个更过分的方法,直接接受文件名作为参数

    public PrintWriter(String fileName) throws FileNotFoundException {
        this(new BufferedWriter(new OutputStreamWriter(new FileOutputStream(fileName))),
             false);
    }

    public PrintWriter(String fileName, String csn)
        throws FileNotFoundException, UnsupportedEncodingException
    {
        this(toCharset(csn), new File(fileName));
    }
    
    public PrintWriter(File file) throws FileNotFoundException {
        this(new BufferedWriter(new OutputStreamWriter(new FileOutputStream(file))),
             false);
    }

    public PrintWriter(File file, String csn)
        throws FileNotFoundException, UnsupportedEncodingException
    {
        this(toCharset(csn), file);
    }            

PrintStream 相比较,PrintWriter 在构造函数中传入的自动刷新参数,只有在使用 println(),printf(),format()方法时候才会调用

    public void println(String x) {
        synchronized (lock) {
            print(x);
            println();
        }
    }

    public void print(String s) {
        if (s == null) {
            s = "null";
        }
        write(s);
    }

    public void println() {
        newLine();
    }

    private void newLine() {
        try {
            synchronized (lock) {
                ensureOpen();
                out.write(lineSeparator);
                if (autoFlush)
                    out.flush();
            }
        }
        catch (InterruptedIOException x) {
            Thread.currentThread().interrupt();
        }
        catch (IOException x) {
            trouble = true;
        }
    }        

参考文档

编码
http://www.cnblogs.com/gdayq/p/5817367.html

Charset
https://blog.csdn.net/nicewuranran/article/details/52123516
https://www.cnblogs.com/lngrvr/p/java_AutoCharsetReader.html

相关文章

网友评论

      本文标题:Java IO

      本文链接:https://www.haomeiwen.com/subject/jdrrfftx.html