天天看点

java笔记(三):IO流InputStreamOutputStreamReaderWriter

笔记终于进行到第一个有深度且重要的模块了。

IO流是java用于处理输入输出数据的类的集合。

java笔记(三):IO流InputStreamOutputStreamReaderWriter

上图包含了java中所有的IO类。

在阅读源码的基础上,写一些对于各个类的简单理解。

InputStream

首先看的是字节流。字节流是以byte为基本表现形式的输入数据。主要区别于文件里的文字。

InputStream是抽象类,规定了输入字节流的主要公共方法。

ByteArrayInputStream

该类主要用于操作byte数组,和大部分的io类不同,该ByteArrayInputStream的数据来源是数组(可以是代码中的数据,也可以是内存块)。

该类对于InputStream的所有方法都进行了重写,主要原因是该类操作数组,因此很多操作和操作文件不同,有许多简易的方法。

贴一段简单的代码供参考:

byte[] b = {,,,,};
ByteArrayInputStream b1 = new ByteArrayInputStream(b);
for (;b1.available()>;){
    System.out.print((char)b1.read());
    b1.skip();
} //结果是:)?U
           

用起来,感觉ByteArrayInputStream就是对byte[]做了一层封装,和通常意义上的输入输出关系不大。

实际上该类的设计使用到了适配器模式(Adapter):将一个类的接口转换成客户希望的另外一个接口,Adapter模式使得原本由于接口不兼容而不能一起工作的那些类可以一起工作(总感觉有点强行OO的意思)。

FilterInputStream

该类包含一个InputStream类型的属性,然后该类又继承了InputStream(无语)。

public class FilterInputStream extends InputStream {
    /**
     * The input stream to be filtered.
     */
    protected volatile InputStream in;
           

其作用是:封装其它的输入流,并为它们提供额外的功能(装饰器模式)。

该类对被装饰(封装)的流进行基本的包裹,不增加额外的功能。

BufferedInputStream

BufferedInputStream继承自FilterInputStream,主要用于缓存读取自io的数据,避免过多的io操作,当然也因此不可避免的占用了较多的内存(因为读取的io数据都存在数组里):

public class BufferedInputStream extends FilterInputStream {

    private static int DEFAULT_BUFFER_SIZE = ;

    /**
     * The maximum size of array to allocate.
     */
    private static int MAX_BUFFER_SIZE = Integer.MAX_VALUE - ;

    /**
     * The internal buffer array where the data is stored. 
     */
    protected volatile byte buf[];
           

如上是BufferedInputStream的部分属性源码,数据就存在buf[]中。

BufferedInputStream中最重要的一个方法当属fill(),该方法的作用是:

当缓存(即buf[])中的数据都被取出时,调用属性in(输入流),读取io数据,并将数据存至buf[]中。

一个简单的实例了解下:

byte[] b = {'1','2','3','a','b','c'};
ByteArrayInputStream bais = new ByteArrayInputStream(b);
BufferedInputStream bis = new BufferedInputStream(bais);
try {
    int i = -;
    while( (i=bis.read()) > ){
        System.out.print(i+" ");
    }
} catch (IOException e) {
    e.printStackTrace();
}//输出结果是49 50 51 97 98 99
           

DataInputStream

该类继承自FilterInputStream,主要功能是提供了各种和数据操作有关的功能,比如读取行,读取一个short, int, double等。

除了read()方法和read(byte[])方法外,该类还提供了一个readFully(byte b[])方法,该方法源码如下:

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

该方法用于读取长度为len的数据到数组b中,和read(byte b[], int off, int len)不同的是,当in中的数据没有len这么多时,会报EOFException,而不会返回实际读取的长度。

另外值得注意的是,该类实现了DataInput接口,实现了许多读取不同类型数据的方法。

byte[] b = {'l','o','d',,,'i',,,,,};
ByteArrayInputStream bais = new ByteArrayInputStream(b);
DataInputStream dis = new DataInputStream(bais);

try {
    byte by = dis.readByte();
    int uby = dis.readUnsignedByte();
    short sh = dis.readShort();
    char ch = dis.readChar();
    int in = dis.readInt();

    System.out.println(by);  //108
    System.out.println(uby); //111
    System.out.println(sh);  //25600
    System.out.println(ch);  //i
    System.out.println(in);  //111          
} catch (IOException e) {
    e.printStackTrace();
}
           

其中DataInputStream也实现了readLine()方法,但官方建议使用BufferedReader替代。

另一个值得注意的方法是readUTF()。该方使用utf编码的文件中的内容。其实上,在文件写入jdk时,会带上该文件的长度。因此readUTF()一开始会读取文件的长度,在按照这个长度读取数据。因此该方法建议读取打开的文件。下面的例子虽然不是打开的文件,但模拟了代开文件使用readUTF()的方法。

byte[] b = {,,'l','o','v','e','l','i','v','e','!'};
ByteArrayInputStream bais = new ByteArrayInputStream(b);
DataInputStream dis = new DataInputStream(bais);
try {
    String str = dis.readUTF();
    System.out.println(str); //lovelive           
} catch (IOException e) {
    e.printStackTrace();
}
           

FileInputStream

该类用于从文件读取数据,它的文件(对象)可以用关键字 new 来创建,也可以直接使用字符串代替。

该类的源码比较通俗易懂,并没有特别值得注意的方法或属性。

具体实例如下:

/**
*运行结果:
*4 l o v e
*4 l i v e
*/
try{
    FileInputStream fis = new FileInputStream("test.txt");
    byte[] b = new byte[];
    int i;
    while(( i= fis.read(b))!=-){
        System.out.println(i+" "+(char)b[]+" "+(char)b[]+" "+(char)b[]+" "+(char)b[]);
    }
    fis.close();
} catch(FileNotFoundException e){
} catch(IOException e){
}
           

StringBufferInputStream

该类已经过时,简单看了下源码,其实很简单。

一般推荐使用ByteArrayInputStream替代StringBufferInputStream。

ByteArrayInputStream bais = new ByteArrayInputStream("hello world".getBytes());
           

SequenceInputStream

该类用于存储多个输入流,并有序的从第一个流一直读到最后一个流。

说起来也就是对InputStrem做了一个封装。

public class SequenceInputStream extends InputStream {
    //用于存储多个输入流
    Enumeration<? extends InputStream> e;
    //用于读取数据的当前输入流
    InputStream in;
           

其中的一个初始化方法如下:

public SequenceInputStream(InputStream s1, InputStream s2) {
    Vector<InputStream> v = new Vector<>();

    v.addElement(s1);
    v.addElement(s2);
    e = v.elements();
    try {
        nextStream();
    } catch (IOException ex) {
        // This should never happen
        throw new Error("panic");
    }
}
           

其实是先把两个输入流转变为 Enumeration后再存储在e中。

该类中最重要的方法:

final void nextStream() throws IOException {
    if (in != null) {
        in.close();
    }

    if (e.hasMoreElements()) {
        in = (InputStream) e.nextElement();
        if (in == null)
            throw new NullPointerException();
    }
    else in = null;
}
           

其实就是顺序检查e中的输入流,并将可用的输入流赋给in而已。

一个简单的实例如下:

byte[] by1 = {'l','o','v','e'};
byte[] by2 = {'l','i','v','e','!'};
ByteArrayInputStream b1 = new ByteArrayInputStream(by1);
ByteArrayInputStream b2 = new ByteArrayInputStream(by2);
SequenceInputStream sis = new SequenceInputStream(b1, b2);
int c;
try {
    while ( (c = sis.read()) != -){
        System.out.print((char)c);
    }
} catch (IOException e) {
    e.printStackTrace();
}//输出结果为lovelive!
           

ObjectInputStream

在进入这一节之前,强烈建议先阅读ObjectOutputStream的部分。

ObjectInputStream可以看做是ObjectOutputStream的逆操作,同时自定义了一个内部类,BlockDataInputStream用于读取数据。同时利用该类实现了ObjectInput和DataInput中的各方法。

ByteArrayOutputStream baos = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(baos);
Cat c = new Cat();
c.i++;
oos.writeObject(c);
oos.flush();
byte[] data = baos.toByteArray();

ByteArrayInputStream bais = new ByteArrayInputStream(data);
ObjectInputStream ois = new ObjectInputStream(bais);
Cat c1 = (Cat)ois.readObject();
System.out.println(c1.i); //2
System.out.println(c1.s); //Cat

class Cat implements Serializable{
    public static int i = ;
    public String s = "Cat";
    public Cat(){};
}
           

OutputStream

InputStream暂时告一段落,先看看OutputStream。

和InputStream一样,OutputStream也是个抽象类,无法被实例化。

该类的源码很少,主要是write()等系列的方法。继承自该类的子类只需实现write(int b).

FileOutputStream

和FileInputStream一样,该类用于操作文件。官方文档推荐使用该类操作存byte类型的数据,比如图片。

该类有FileOutputStream(File file, boolean append)方法来指明写入文件的数据是追加还是覆盖。

FileOutputStream fos = new FileOutputStream("test.txt");
byte[] b = {'l','o','v','e','l','i','v','e'};
fos.write(b);
fos.close();
           

ByteArrayOutputStream

和ByteArrayInputStream一样,该类同样对byte数组做一层封装,将数据写入自己的属性中。

/**
* The data can be retrieved using toByteArray() and toString()
*/
public class ByteArrayOutputStream extends OutputStream {
    //The buffer where data is stored.
    protected byte buf[];
    //The number of valid bytes in the buffer.
    protected int count;
           

如上所述,该类在将数据写入自身的属性buf后,可以通过toByteArray()和toString()方法读出。

byte[] by = {'o','v','e'};
ByteArrayOutputStream baos = new ByteArrayOutputStream();
baos.write();
baos.write(by,  ,by.length);
String str = baos.toString();
System.out.println(str);  //love
byte[] by1 = baos.toByteArray();
for(int i = ;i<by1.length;i++){
    System.out.print(by1[i]+" ");  //108 111 118 101
}
           

上述方法使用了ByteArrayOutputStream 最重要的四个方法

write(int b)

write(byte b[], int off, int len)

toByteArray()

toString()

ByteArrayOutputStream 与ByteArrayInputStream相互独立,两者不必同时使用。

另外,该类有个方法

writeTo(OutputStream out)

可以将数据写入指定输出流中。如下:

FileOutputStream fos = new FileOutputStream("test.txt");
ByteArrayOutputStream baos = new ByteArrayOutputStream();
byte[] by = {'o','v','e'};
baos.write();
baos.write(by,  ,by.length);
baos.writeTo(fos);  //文件test.txt中被写入love
fos.close();
           

FilterOutputStream

该类是所有 过滤 输出流的父类。说白了,就是在输出流上又套了一层操作,和FilterInputStream相对。

DataOutputStream

主要是提供了许多和数据类型有关的输出流操作。

ByteArrayOutputStream baos = new ByteArrayOutputStream();
DataOutputStream dos = new DataOutputStream(baos);
byte[] b = {'l','o','v','e'};
dos.write(b);
dos.writeShort();
dos.writeUTF("live");
dos.close();
System.out.println(baos.toString());  //输出love  live
           

BufferedOutputStream

与BufferedInputStream相对,提供写入流的缓存。没啥新东西,不写例子了。

PrintStream

该类提供格式化输出流,除了我们理解的屏幕输出外,该类更多的可以用于格式化输出文件。

虽然该类继承自FilterOutputStream,该类除了可以使用OutputStream初始化外,还可以使用文件(文件名)初始化。

在初始化时,可以选择是否自动清理缓存 :

PrintStream(OutputStream out, boolean autoFlush)

虽然该类也实现了write()等一系列方法,但更多的还是建议使用print()或者println()方法。实际上print()的方法体基本上就是调用write()方法。

一个简单的测试初始化和写入的例子:

/**
*class Person{}
*Person定义如上
*/
ByteArrayOutputStream baos = new ByteArrayOutputStream();
PrintStream ps = new PrintStream(baos, true);
String s = "school";
Person p = new Person();
ps.println(p);
ps.close();
byte[] b = baos.toByteArray();
for(int i=;i<b.length;i++){
    System.out.print((char)b[i]);
}//输出[email protected]
           

上述用于初始化的输出流换成FileInputStream,则最后结果就不是打印到屏幕上,而是输出到文件中。

ObjectOutputStream

用于将基本类型和类的对象写进输出流。

对于对象,只有实现了Serializable的类才能被写入流(String类实现了Serializable接口)。

该类的源码过于复杂,这里只简单剖析下基本实现。

在ObjectOutputStream的内部定义了一个私有类,并有一个指向该私有类的引用。

private final BlockDataOutputStream bout;

private static class BlockDataOutputStream extends OutputStream implements DataOutput {
    /** underlying output stream */
    private final OutputStream out;
    /** loopback stream (for data writes that span data blocks) */
    private final DataOutputStream dout;
    //初始化方法,在初始化ObjectOutputStream的同时,
    //初始化了内部的dout,dout=BlockDataOutputStream(out)
    BlockDataOutputStream(OutputStream out) {
        this.out = out;
        dout = new DataOutputStream(this);
    }
    //BlockDataOutputStream的其中一个写方法,
    //BlockDataOutputStream内部,write()系方法都调用的是
    //out的一系列方法
    public void write(int b) throws IOException {
        if (pos >= MAX_BLOCK_SIZE) {
            drain();
        }
        buf[pos++] = (byte) b;
    }
    //writeXXX()系方法都调用的是dout的一系列方法
    public void writeChar(int v) throws IOException {
        if (pos +  <= MAX_BLOCK_SIZE) {
            Bits.putChar(buf, pos, (char) v);
            pos += ;
        } else {
            dout.writeChar(v);
        }
    }
}   
           

然后,ObjectOutputStream内部对于继承自ObjectOuput和DataOutput的方法,都是通过调用bout的方法来完成的。

然后我们知道,因为调用的是stream的输出流(OutputStream)其实写入的东西都是二进制的byte数据。

ByteArrayOutputStream baos = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(baos);

oos.write();
oos.writeInt();
oos.writeDouble();
oos.writeBytes("hello");
oos.writeChars("hello");
oos.writeUTF("hello");
oos.writeObject(new Cat());
oos.flush();
byte[] data = baos.toByteArray();
for(byte b:data){
    System.out.print(b+" ");
}
/*
-84 -19 0 5 119 35 123 0 0 0 123 64 94 -64 16 98 77 -46 -14 104 101 108 108 111 0 104 0 101 0 108 0 108 0 111 0 5 104 101 108 108 111 115 114 0 3 67 97 116 -19 -85 -95 117 -55 -104 -79 -94 2 0 1 73 0 1 105 120 112 0 0 0 0
*/
class Cat implements Serializable{
    public int i = ;
    public Cat(){};
}
           

对于类的属性的写入,有一些限制,比如非transient和非静态属性。

Reader

看完了字节流,来看字符流。

Reader也是抽象类,该类实现了两个接口:

且Reader将Closeable的方法也声明为了abstract,并自己也声明了一个abstract方法abstract public int read(char cbuf[], int off, int len)。

因此继承自Reader的类都必须实现read(char[], int, int)和close()方法。

CharArrayReader

CharArrayReader(图片上写的是CharReader)的功能和ByteArrayInpitStream的功能有些类似,区别在于前者使用char[]数组进行初始化,后者使用byte[]初始化。

public class CharArrayReader extends Reader {
    /** The character buffer. */
    protected char buf[];

    /** The current buffer position. */
    protected int pos;

    /** The position of mark in buffer. */
    protected int markedPos = ;

    /**
     *  The index of the end of this buffer.
     *  data at or beyond this index.
     */
    protected int count;
           

简单看下实例:

char[] cha = {'l','o','v','e','l','i','v','e','!'};
CharArrayReader car= new CharArrayReader(cha);
char[] ch = new char[];
int i = car.read(ch);
//输出lovelive!
System.out.println(String.valueOf(ch)+(char)car.read());
           

StringReader

和CharArrayReader类似,StringReader以字符串为初始化对象,但奇怪的是,读取的返回值仍然是char[]。

只贴一段简单的例子好了:

StringReader sr = new StringReader("lovelive!");
char[] ch = new char[];
int i = sr.read(ch);
//输出lovelive!
System.out.println(String.valueOf(ch)+(char)sr.read());
           

BufferedReader

该类和FilterInputStream的设计思路类似,使用装饰器模式,缓存字符输入。

直接贴部分源码及源码的注释:

/**
 * Reads text from a character-input stream, buffering   
 * characters so as to provide for the efficient reading
 * of characters, arrays, and lines.
 * It is advisable to wrap a BufferedReader around any Reader whose read() operations may be costly, such as FileReaders and InputStreamReaders.  For example,
 * BufferedReader in = new BufferedReader(new FileReader("foo.in"));
 * 建议使用BufferedReader来包装文件的读取,这是因为文件的读取操作(read())是非常昂贵的(因为文件涉及到硬件)。
 * will buffer the input from the specified file.  Without buffering, each invocation of read() or readLine() could cause bytes to be read from the file, converted into characters, and then returned, which can be very inefficient.
 *Programs that use DataInputStreams for textual input can be localized by replacing each DataInputStream with an appropriate BufferedReader.
 建议DataInputStreams操作文本这种写法使用BufferedReader替代。
 */
 public class BufferedReader extends Reader {

    private Reader in;

    private char cb[];
           

因为是buffer,因此和BufferedInputStream一样,该类同样有一个fill()方法用于填充自身的缓存cb[]。

StringReader sr = new StringReader("lovelive!\nschool idol");
BufferedReader db = new BufferedReader(sr);
int ch = db.read();
char[] cha = new char[];
db.read(cha);
String s1 = db.readLine();
String s2 = db.readLine();
System.out.print((char)ch);
System.out.print(""+(char)cha[]+(char)cha[]+(char)cha[]+(char)cha[]);
System.out.print(s1);
System.out.print(s2);
//lovelive!school idol
           

上述方法演示了BufferedReader中最重要的两个读方法。可以看出,实际上在readLine时,自动剔除了原数据中的换行操作。

LineNumberReader

该类继承自BufferedReader,用于需要跟踪行号的字符输入流。该类多了两个方法setLineNumber(int lineNumber)和getLineNumber(),其他方法和BufferedReader没有本质区别。

需要注意的是,该类的getLineNumber()方法实际上并未实际改变缓存中指针的位置,而只是单纯的改变了当前的行号。即该类不支持随意行的读操作。

StringReader sr = new StringReader("lovelive!\nschool idol");
LineNumberReader lnb = new LineNumberReader(sr);

lnb.setLineNumber();

int s2i = lnb.getLineNumber();
String s2 = lnb.readLine();

int s3i = lnb.getLineNumber();
String s3 = lnb.readLine();

System.out.println(s2i+":"+s2);  //2:lovelive!
System.out.println(s3i+":"+s3);  //3:school idol
           

InputStreamReader

该类是字节流和字符流的一个桥梁,通过读取字节流将其转换为字符。

注意,这是用到了适配器模式。

在初始化时可以设定使用的字符格式。

byte[] b = {'l','o','v','e','l','i','v','e','!'};
ByteArrayInputStream bais = new ByteArrayInputStream(b);
InputStreamReader isr = new InputStreamReader(bais);
String encode = isr.getEncoding();
int i = isr.read();
char[] ch = new char[];
isr.read(ch);
String chs = String.valueOf(ch);
System.out.println(encode+":"+(char)i+chs);  //UTF8:lovelive!  
           

处于性能考虑,建议使用BufferedReader包装一下InputStreamReader。

FileReader

该类大概是整个io流非抽象类最简单的一个,其实就是对InputStreamReader增加了几个针对文件的初始化方法。

还是直接看例子吧:

/**test.txt
 *hello
 *world
 */
FileReader fr = new FileReader("test.txt");
char[] by = new char[];
fr.read(by);
String s = String.valueOf(by);
fr.close();
System.out.println(s);
//hello
//world
           

Writer

来看看IO流的最后一个模块writer吧。

该抽象类实现了三个接口:

public abstract class Writer implements Appendable, Closeable, Flushable {

    /**
     * Temporary buffer used to hold writes of strings and single characters
     */
    private char[] writeBuffer;
           

分别是追加接口(Appendable),关闭接口(Closeable),刷新缓存接口(Flushable)。

CharArrayWriter

类似CharArrayrReader,子什么包含一个存储字符的数组。

CharArrayWriter caw = new CharArrayWriter();
caw.write('l');
char[] ch = {'o','v','e'};
caw.write(ch);
caw.write("liv");
caw.append("e!");
String s1 = String.valueOf(caw.toCharArray());
String s2 = caw.toString();
System.out.println(s1);  //lovelive!
System.out.println(s2);  //lovelive!
           

CharArrayrReader还有一个写向其他流的方法:writeTo(Writer out)

StringWriter

该类其实是对StringBuffer做了一层包装。可以返回String,也可以返回StringBuffer,源码十分简单,就不举例子了。

FilterWriter

抽象类,包含一个Writer类型的属性,其他方法都是对于该属性操作的封装,同理于FilterReader,使用装饰器模式。目前暂无实现类。

OutputStreamWriter

将字符转换为字节。关于该类,需要注意的是,每一次的read()方法都会进行转码(encoding converter)操作。因此官方建议使用BufferedWriter进行包装。

Writer out = new BufferedWriter(new OutputStreamWriter(System.out));
           

一个简单的例子如下:

ByteArrayOutputStream baos = new ByteArrayOutputStream();
OutputStreamWriter osw = new OutputStreamWriter(baos);
String encode = osw.getEncoding();
osw.write('l');
char[] ch = {'o','v','e'};
osw.write(ch);
osw.write("liv");
osw.append("e!");
osw.flush();
System.out.println(encode+":"+baos.toString());  //GBK:lovelive!
           

FileWriter

该类其实就是在OutputStreamWriter的基础上增加了初始化文件的构造方法。简单的不能再简单的类,直接看源代码吧。

BufferedWriter

带有缓存的写入流,当write()操作十分昂贵时,建议使用该类进行包装。

CharArrayWriter caw = new CharArrayWriter();
FileWriter fw = new FileWriter("test.txt");
BufferedWriter bw1 = new BufferedWriter(caw);
BufferedWriter bw2 = new BufferedWriter(fw);
bw1.write("love");
bw2.write("love");
bw1.newLine();
bw2.newLine();
bw1.write("live!");
bw2.write("live!");
bw1.close();
bw2.close();
System.out.println(caw.toString());  
//love
//live!
fw.close();
//文件里是
//love
//live!
           

PrintWriter

格式化输出到文本的内容。

值得一提的是,该类既可以使用writer进行初始化,又可以使用OutputStream进行初始化,还可以使用字符串进行初始化。

值得一提的是:

//下面两个初始化方法等价
PrintWriter out = new PrintWriter(new BufferedWriter(new FileWriter("foo.out")));
PrintWriter out = new PrintWriter("foo.out");
           

除了本身的write()一系列方法,该类还提供了各种数据类型的打印方法(print)。当然也包括append()方法。

PrintWriter pw = new PrintWriter("test.txt");
pw.print("lovelive!");
pw.println();
pw.println("μ's");
pw.close();
//文件的内容是:
//lovelive!
//μ's
           

到这里io流的方法暂告一段落。

pip等系列的io待更。