Java I/O概述
流代表任何有能力产出数据的数据源对象或者是有能力接收数据的接收端对象,在java.io包下提供了一系列的类来实现数据的输入(Input)和输出(Output)。其中提供了按照字节为单位进行读写的字节流,也有按字符为单位进行读写的字符流。不管是字节流的输入还是字符流的输入都包含有read方法,而输出则包含有write方法。对于输入流对应数据读取(read),输出流对应数据写入(write)可能会有概念混淆,下面通过图示来进行更好的理解。需要注意的是数据源跟目的地可以是相同的。(网络IO不在本文讨论范围内)
输入流对应数据读取:从数据源的角度看是把数据输入到程序,而从程序的角度看是读取数据源的数据。
输出流对应数据写入:目的地的角度看是程序把数据输出到目的度,而从程序的角度看是把数据写到目的地。
下列是针对Java IO类的一个归纳图。
File使用
在介绍IO流之前有必要先了解下File类的使用因为大多数情况下不管是数据源还是数据存放的目的地都是文件。通过File类可以操作文件以及文件目录,下面进行一个简单的代码实例至于更全面的File方法可以直接翻阅jdk文档。
可以看到File提供了4种构造方法。
- 第一个构造方法,File parent参数表示的是文件的目录,后面的child表示文件名;
- 第二个构造方法pathname表示的就是一个完整的文件路径;
- 第三个构造方法parent表示文件的目录,相比第一个构造方法的区别是它是字符串的格式,后面的child一样是表示文件名;
- 第四个构造方法表示以URI对象的方式来获取文件。
下面是构造方法使用示例:
//第一种File file1 = new File("D://test/test");String child = "test2.txt";File file11 = new File(file1, child);//第二种File file2 = new File("D://test/test//test2.txt");//第三种String parent = "D://test/test";String child3 = "test2.txt";File file3 = new File(parent, child);//第四种 URI file scheme 格式file:///~/calendarFile file4 = new File(new URI("file:///D://test/test/test2.txt"));
下面是File常用方法使用示例:
File file = new File("D://test/test");//mkdir创建指定级的目录如上述的test如果父文件目录test不存在则失败System.out.println("mkdir:" + file.mkdir());System.out.println("exists or not:" + file.exists());//mkdirs创建文件夹包括所有目录System.out.println("mkdir:" + file.mkdirs());System.out.println("exists or not:" + file.exists());//文件的创建,删除与基本属性File file2 = new File("D://test/test/test.txt");for (;!file2.exists();) { System.out.println("file no exist"); file2.createNewFile();}System.out.println("getName:" + file2.getName());System.out.println("lastModified:" + file2.lastModified());System.out.println("delete:" + file2.delete());System.out.println("exists or not:" + file2.exists());//目录下文件筛选File[] files = file.listFiles(File::isHidden);File[] files2 = file.listFiles((f)-> "test2.txt".equals(f.getName()));
I/O流的使用
通过下图可以清晰的看到java.io包下对应类的操作对象或使用场景,下面将根据图上的分类依次来介绍下IO流的使用(为了篇幅长度,源码中的注释会删去)。
基础抽象类
- InputStream:字节流中所有输入流的超类,所有的子类都要实现read()方法。该方法从输入流中读取数据的下一个字节。返回
0
到255
范围内的int
字节值(参照ASCII码)。如果因为已经到达流末尾而没有可用的字节,则返回值-1
。在输入数据可用、检测到流末尾或者抛出异常前,此方法一直阻塞。 - OutputStream:字节流中所有输出流的超类,所有的子类都要实现write(int b)方法。该方法向输出流写入一个字节(一个字节占8位)。需要注意的是将要写入的字节是参数
b
(参照ASCII码,参数范围0到255)的八个低位。b
的 24 个高位将被忽略。举个例子假设参数b为50,那么根据ASCII码写入的就是2,如果参数b为306写入的结果还是2,因为write方法只写入低8位而306的二进制为1,00110010,低8位就是00110010换算成十进制就变成50了跟前面一致。 - Reader:字符流中所有输入流的超类,但是所有的子类都要重写read(char[] cbuf, int off, int len) 和 close()方法。read方法将字符读入数组的某一部分。在某个输入可用、发生 I/O 错误或者到达流的末尾前,此方法一直阻塞。cbuf表示目标缓存区,off表示开始存储字符处的偏移量,len表示读取的最多字符数。close方法用来关闭流。
- Writer:字符流中所有输出流的超类,除了提供所有的子类都要重写write(char[] cbuf, int off, int len)、flush() 和 close()方法。write跟Reader的read方法相对应。flush方法用于将缓冲区的数据写入。close方法用来关闭流,关闭流的时候也会把数据写入到目的地。
文件
字节流FileInputStream和FileOutputStream:
- 第一个构造方法用一个File对象为参数;
- 第二个构造方法使用文件描述符(有关文件描述符可查阅后文),该构造方法比较少用到,一般用在标准I/O以及Consle上;
- 第三个构造方法为文件名但是在源码中实际上还是使用了new File()。
public int read() throws IOException { return read0(); } private native int read0() throws IOException; private native int readBytes(byte b[], int off, int len) throws IOException; public int read(byte b[]) throws IOException { return readBytes(b, 0, b.length); } public int read(byte b[], int off, int len) throws IOException { return readBytes(b, off, len); }
FileInputStream提供了三种read方法来读取数据,其中read()每次从数据源中顺序读取单个字节,read(byte[] b),read(byte[] b, int off, int len)用于把多个字节读到一个字节数组中,其中b代表存放的字节数组,off表示数组的偏移量,len表示读取的最大字节数。
FileOutputStream的构造方法基本跟FileInputStream相对应,其中append参数表示是否把新的数据写入(添加)到文件末尾,否则新的数据会把文件覆盖掉。
private native void write(int b, boolean append) throws IOException; public void write(int b) throws IOException { write(b, append); } private native void writeBytes(byte b[], int off, int len, boolean append) throws IOException; public void write(byte b[]) throws IOException { writeBytes(b, 0, b.length, append); } public void write(byte b[], int off, int len) throws IOException { writeBytes(b, off, len, append); }
FileOutputStream的write方法参数除了前文提到的append其余的跟FileInputStream是一样的。
下面是使用示例:
try (FileOutputStream fos = new FileOutputStream("D://test/test/test.txt", true); FileInputStream fis = new FileInputStream("D://test/test/test.txt")) { fos.write(50); fos.write(306); int b = 0; for (;(b = fis.read()) != -1;) { //输出22 System.out.print((char) b); }} catch (IOException e) { e.printStackTrace();}
字符流FileReader和FileWriter:
FileReader构造方法参数与字节流FileInputStream一致,其内部还是使用的FileInputStream。
public FileReader(String fileName) throws FileNotFoundException { super(new FileInputStream(fileName)); } public FileReader(File file) throws FileNotFoundException { super(new FileInputStream(file)); } public FileReader(FileDescriptor fd) { super(new FileInputStream(fd)); }
FileReader本身没有实现读取数据的方法,都是继承父类InputStreamReader和超类Reader中的方法。InputStreamReader后面会详细介绍。
FileWriter构造方法参数与字节流FileOutputStream一致,其内部还是使用的FileOutputStream。
public FileWriter(String fileName) throws IOException { super(new FileOutputStream(fileName)); } public FileWriter(String fileName, boolean append) throws IOException { super(new FileOutputStream(fileName, append)); } public FileWriter(File file) throws IOException { super(new FileOutputStream(file)); } public FileWriter(File file, boolean append) throws IOException { super(new FileOutputStream(file, append)); } public FileWriter(FileDescriptor fd) { super(new FileOutputStream(fd)); }
FilWrite本身没有实现写入数据的方法,都是继承父类OutputStreamWriter和超类Writer中的方法。OutputStreamWriter后面会详细介绍。
下面是使用示例:
char[] chars = "测试字符流".toCharArray();try (FileWriter fw = new FileWriter("D://test/test/test2.txt", true); FileReader fr = new FileReader("D://test/test/test2.txt")) { //下面两个都是超类Write提供的方法 fw.write(chars); fw.write(" 哈哈"); fw.flush(); int b; for (;(b = fr.read(chars, 0, chars.length)) != -1;) { System.out.println(new String(chars, 0, b)); }}
文件字节流与文件字符流的相同点:
- 如果文件的目录存在但是文件不存在,写入的时候都会创建一个新的文件;
- 如果写入的时候没有指定append为true那么原文件会被覆盖;
文件字节流与文件字符流的不同点:
- 文件字符流FileWriter写入的时候如果调用的是超类Writer的方法会先把数据写入到缓冲数组,必须要调用flush方法或者关闭流的时候才会把数据写入文件,如果是用的父类InputStreamReader就不需要;
数组
字节流ByteArrayInputStream和ByteArrayOutputStream:
ByteArrayInputStream构造参数就是一个字节数组,后面一个是偏移量一个是读取的字节长度。不需要关闭流并且不会产生IOException。
public synchronized int read() { return (pos < count) ? (buf[pos++] & 0xff) : -1; } public synchronized int read(byte b[], int off, int len) { if (b == null) { throw new NullPointerException(); } else if (off < 0 || len < 0 || len > b.length - off) { throw new IndexOutOfBoundsException(); } if (pos >= count) { return -1; } int avail = count - pos; if (len > avail) { len = avail; } if (len <= 0) { return 0; } System.arraycopy(buf, pos, b, off, len); pos += len; return len; }
源码中buf表示数据源数组;count表示数组的长度;pos表示要从buf中读取的下一个字符的索引也就是数组下标。
ByteArrayOutputStream提供了两个构造方法,其中可以指定缓冲数组的大小。不需要关闭流并且不会产生IOException。
public void write(int b) { this.ensureCapacity(1); this.buf[this.count] = (byte)b; ++this.count;} private void ensureCapacity(int space) { int newcount = space + this.count; if (newcount > this.buf.length) { byte[] newbuf = new byte[Math.max(this.buf.length << 1, newcount)]; System.arraycopy(this.buf, 0, newbuf, 0, this.count); this.buf = newbuf; } } public void write(byte[] b, int off, int len) { this.ensureCapacity(len); System.arraycopy(b, off, this.buf, this.count, len); this.count += len;} public void write(byte[] b) { this.write(b, 0, b.length);} public byte[] getBytes() { return this.buf;}
在每次写入字节的时候会先调用ensureCapacity方法查看当前数组是否需要扩容。
下面是使用示例:
//ByteOutputStream以及ByteArrayInputStream都不需要关闭流ByteOutputStream bos = new ByteOutputStream(1);for (int i = 1; i < 10; i++) { bos.write(i);}ByteArrayInputStream bis = new ByteArrayInputStream(bos.getBytes());int b;//达到数组末尾的时候还是-1,但是在本例子中如果要取到1-9的数据就用!= 0判断,因为扩容后的数组默认初始值为0,而源码中可以看到getBytes就是返回整个缓冲数组//for (;(b = bis.read()) != 0;) {for (;(b = bis.read()) != -1;) { //输出1234567890000000 System.out.print(b);}
字符流CharArrayReader和CharArrayWriter:
CharArrayReader构造方法跟ByteArrayInputStream一样,提供的read方法的实现基本跟ByteArrayInputStream基本一样。
private void ensureOpen() throws IOException { if (buf == null) throw new IOException("Stream closed"); } public int read() throws IOException { synchronized (lock) { ensureOpen(); if (pos >= count) return -1; else return buf[pos++]; } }
CharArrayWriter的构造方法也跟ByteOutputStream一样,提供的write方法的实现ByteOutputStream基本一样。
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; } } public void write(char c[], int off, int len) { if ((off < 0) || (off > c.length) || (len < 0) || ((off + len) > c.length) || ((off + len) < 0)) { throw new IndexOutOfBoundsException(); } else if (len == 0) { return; } synchronized (lock) { int newcount = count + len; if (newcount > buf.length) { buf = Arrays.copyOf(buf, Math.max(buf.length << 1, newcount)); } System.arraycopy(c, off, buf, count, len); count = newcount; } } public void write(String str, int off, int len) { synchronized (lock) { int newcount = count + len; if (newcount > buf.length) { buf = Arrays.copyOf(buf, Math.max(buf.length << 1, newcount)); } str.getChars(off, off + len, buf, count); count = newcount; } } public char toCharArray()[] { synchronized (lock) { return Arrays.copyOf(buf, count); } }
下面是使用示例:
CharArrayWriter caw = new CharArrayWriter(1);for (int i = 20320; i < 20330; i++) { caw.write(i);}try { CharArrayReader car = new CharArrayReader(caw.toCharArray()); int b; //通过上述源码可以看到字符流数组在返回字符数组的时候只会返回字符数组已有的数据 for (;(b = car.read()) != -1;) { //输出 你佡佢佣佤佥佦佧佨佩 System.out.print((char) b); }} catch (IOException e) { //从源码可以看到当buf为NulL的时候抛出异常 e.printStackTrace();}
字节流数组与字符流数组的相同点:
- 都不需要关闭流;
- 针对写操作底层数组会自动扩容。
字节流数组与字符流数组的不同点:
- 数据源不同;
- 底层数据结构不同,字节流是byte数组,字符流是char数组;
- 字符流数组CharArrayReader在返回字符数组的时候根据已有数据返回具体数据数量的数组,而字节流数组则是返回整个缓冲区数组会包括扩容后的长度;
- 字符流数组会抛出IO异常。
管道
字节流PipedInputStream和PipedOutputStream:
管道输入流应该连接到管道输出流;管道输入流提供要写入管道输出流的所有数据字节。通常,数据由某个线程从 PipedInputStream对象读取,并由其他线程将其写入到相应的 PipedOutputStream。不建议对这两个对象尝试使用单个线程,因为这样可能导致线程死锁。管道输入流包含一个缓冲区,可在缓冲区限定的范围内将读操作和写操作分离开。 如果向连接管道输出流提供数据字节的线程不再存在,则认为该管道已损坏broken。
PipedInputStream提供了4种构造方法:
- 创建未连接的PipedInputStream且默认缓冲数组大小为1024;
- 创建未连接的PipedInputStream并自定义缓冲数组大小;
- 创建已连接管道的PipedInputStream与输出流PipedOutputStream关联且默认缓冲数组大小为1024;
- 创建已连接管道的PipedInputStream与输出流PipedOutputStream关联并自定义缓冲数组大小。
public synchronized int read() throws IOException { //判断是否跟管道输出流关联 if (!connected) { throw new IOException("Pipe not connected"); } else if (closedByReader) { //判断当前流是否关闭 throw new IOException("Pipe closed"); } else if (writeSide != null && !writeSide.isAlive() && !closedByWriter && (in < 0)) { //判断管道输出流是否关闭 throw new IOException("Write end dead"); } readSide = Thread.currentThread(); int trials = 2; while (in < 0) { if (closedByWriter) { /* closed by writer, return EOF */ return -1; } if ((writeSide != null) && (!writeSide.isAlive()) && (--trials < 0)) { throw new IOException("Pipe broken"); } /* might be a writer waiting */ notifyAll(); try { wait(1000); } catch (InterruptedException ex) { throw new java.io.InterruptedIOException(); } } int ret = buffer[out++] & 0xFF; if (out >= buffer.length) { out = 0; } if (in == out) { /* now empty */ in = -1; } return ret; } //PipedOutputStream调用,负责写入数据到缓冲数组 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; } }
PipedOutputStream提供了两种构造方法:
- 创建尚未连接到管道输入流的PipedOutputStream;
- 创建连接到指定管道输入流的PipedOutputStream。
public void write(int b) throws IOException { if (sink == null) { throw new IOException("Pipe not connected"); } //调用管道输入流PipedInputStream的方法 sink.receive(b); }
下面是使用示例:
try (PipedOutputStream pos = new PipedOutputStream(); PipedInputStream pis = new PipedInputStream(pos)) { Thread t1 = new Thread(()-> { try { byte[] b = {50, 51, 52, 53}; pos.write(b); //防止当前线程提前退出,抛出Write end dead异常 TimeUnit.SECONDS.sleep(1); } catch (IOException e) { e.printStackTrace(); } catch (InterruptedException e) { e.printStackTrace(); } }); Thread t2 = new Thread(()-> { try { int b; for (;(b = pis.read()) != -1;) { //输出50 51 52 53 System.out.println(b); } } catch (IOException e) { e.printStackTrace(); } }); t1.start(); t2.start(); //防止主线程执行完毕后直接关闭管道流,抛出Pipe closed异常 TimeUnit.SECONDS.sleep(3);} catch (IOException e) { e.printStackTrace();} catch (InterruptedException e) { e.printStackTrace();}
字符流PipedReader和PipedWrite:
PipedReader的read方法可以看到跟PipedInputStream可以说是一模一样,区别在于底层数组不同。
public synchronized int read() throws IOException { if (!connected) { throw new IOException("Pipe not connected"); } else if (closedByReader) { throw new IOException("Pipe closed"); } else if (writeSide != null && !writeSide.isAlive() && !closedByWriter && (in < 0)) { throw new IOException("Write end dead"); } readSide = Thread.currentThread(); int trials = 2; while (in < 0) { if (closedByWriter) { /* closed by writer, return EOF */ return -1; } if ((writeSide != null) && (!writeSide.isAlive()) && (--trials < 0)) { throw new IOException("Pipe broken"); } /* might be a writer waiting */ notifyAll(); try { wait(1000); } catch (InterruptedException ex) { throw new java.io.InterruptedIOException(); } } int ret = buffer[out++]; if (out >= buffer.length) { out = 0; } if (in == out) { /* now empty */ in = -1; } return ret; } //PipedWriter调用,负责把数据写到缓冲数组 synchronized void receive(int c) throws IOException { if (!connected) { throw new IOException("Pipe not connected"); } else if (closedByWriter || closedByReader) { throw new IOException("Pipe closed"); } else if (readSide != null && !readSide.isAlive()) { throw new IOException("Read end dead"); } writeSide = Thread.currentThread(); while (in == out) { if ((readSide != null) && !readSide.isAlive()) { throw new IOException("Pipe broken"); } /* full: kick any waiting readers */ notifyAll(); try { wait(1000); } catch (InterruptedException ex) { throw new java.io.InterruptedIOException(); } } if (in < 0) { in = 0; out = 0; } buffer[in++] = (char) c; if (in >= buffer.length) { in = 0; } }
PipedWrite跟PipedInputStream也是基本一样的。
public void write(int c) throws IOException { if (sink == null) { throw new IOException("Pipe not connected"); } //调用管道输入流PipedReader的方法 sink.receive(c); }
使用示例的话两者是一样的,只需要修改类名就可以运行。
字节流管道与字符流管道的不同点:
- 数据源不同。
- 底层数据结构不同,字节流是byte数组,字符流是char数组;
字符串
前文图中可以得知,字节流中针对数据源字符串的类StringBufferedInputStream已经弃用所以直接来看看字符流是如何处理数据源为字符串的。
StringReader提供了唯一的构造方法,指定参数为String对象。
/** Check to make sure that the stream has not been closed */ private void ensureOpen() throws IOException { if (str == null) throw new IOException("Stream closed"); } public int read() throws IOException { synchronized (lock) { ensureOpen(); if (next >= length) return -1; return str.charAt(next++); } } public int read(char cbuf[], int off, int len) throws IOException { synchronized (lock) { ensureOpen(); if ((off < 0) || (off > cbuf.length) || (len < 0) || ((off + len) > cbuf.length) || ((off + len) < 0)) { throw new IndexOutOfBoundsException(); } else if (len == 0) { return 0; } if (next >= length) return -1; int n = Math.min(length - next, len); str.getChars(next, next + n, cbuf, off); next += n; return n; } }
StringReader中可以看到源码就是通过String本身的方法来进行操作,不在赘述。
StringWriter提供了两个构造方法,构造参数initialSize用来指定内部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; } /** * Write a single character. */ public void write(int c) { buf.append((char) c); } public void write(char cbuf[], int off, int len) { if ((off < 0) || (off > cbuf.length) || (len < 0) || ((off + len) > cbuf.length) || ((off + len) < 0)) { throw new IndexOutOfBoundsException(); } else if (len == 0) { return; } buf.append(cbuf, off, len); } public void write(String str) { buf.append(str); }
通过源码可以看到就是对StringBuffer进行操作。
下面是使用示例:
StringWriter sw = new StringWriter();sw.write("ceshi 测试 123");StringReader sr = new StringReader(sw.toString());int b;try { //不需要关闭流,当StringReader构造参数为Null的时候抛出IO异常 for (;(b = sr.read()) != -1;) { //输出ceshi 测试 123 System.out.print((char) b); }} catch (IOException e) { e.printStackTrace();}
序列化对象
数据的持久化方式有很多种例如常用的有Json,XML,序列化对象等,本文只介绍下Java特有的对象持久化方式---对象序列化。在Java中只需要让对象实现序列化接口Serializable再通过I/O流中的ObjectInputStream和ObjectOutputStream就可以完成持久化操作。
ObjectInputStream提供了两种构造方法,但是对于客户端来说只能使用第二种,构造参数为输入流。由于ObjectInputStream类过于庞大(3900行代码)所以在后面使用示例会给出相关的源码信息。
ObjectOutputStream提供接受一个输出流为参数的构造方法,ObjectOutputStream类的源码也比较多。同上。
下面会介绍对象序列化的使用以及需要注意的情况:
首先需要一个实现序列化接口的类。
public class SeObject implements Serializable { private static final long serialVersionUID = -105996498570254302L; private int age; private String name; public SeObject() { System.out.println("默认构造方法 SeObject"); } public SeObject(int age, String name) { System.out.println("带参数构造方法 SeObject"); this.age = age; this.name = name; } @Override public String toString() { return "SeqObject2{" + "age=" + age + ", name='" + name + '\'' + '}'; }}
下面是使用示例:
try (ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("D://test/test/test4.txt")); ObjectInputStream ois = new ObjectInputStream(new FileInputStream("D://test/test/test4.txt"))) { SeObject so = new SeObject(1, "test1"); System.out.println(so); oos.writeObject(so); System.out.println("=============="); so = (SeObject) ois.readObject(); System.out.println(so);} catch (IOException e) { e.printStackTrace();} catch (ClassNotFoundException e2) { e2.printStackTrace();}//输出//带参数构造方法 SeObject//SeqObject2{age=1, name='test1'}//==============//SeqObject2{age=1, name='test1'}
可以看对象序列化后被写入到磁盘文件上,随后从文件上反序列化出一个新的对象(序列化对象与反序列化对象引用不同)该对象拥有相同的成员变量。
直接实现Serializable接口可以很容易的序列化对象,如果出于安全问题不希望类中的某些变量被序列化。那么可以通过transient修饰符,实现Externalizable接口(继承Serializable),序列化对象实现writeObject以及readObject方法(反射机制)三种方式来实现。
来看第一种情况使用transient修饰符,只需要在SeObject类上的name字段上加上,其余代码包括使用示例不变:
public class SeObject implements Serializable { private static final long serialVersionUID = -105996498570254302L; transient private String name; /* 下面代码不变,为了篇幅所以省略 */ //输出 //SeqObject2 age name //SeqObject2{age=1, name='test1'} //============== //SeqObject2{age=1, name='null'} }
通过输出结果可以看到字段name没有被序列化到文件去。
第二种情况实现Externalizable接口,首先提供一个新的类实现该接口并且需要重写writeExternal和readExternal方法。
public class ExtObject implements Externalizable { private int age; private String name; public ExtObject() { System.out.println("默认构造方法 ExtObject"); } public ExtObject(int age, String name) { System.out.println("带参数构造方法 ExtObject 2"); this.age = age; this.name = name; } @Override public String toString() { return "SeqObject{" + "age=" + age + ", name='" + name + '\'' + '}'; } @Override public void writeExternal(ObjectOutput out) throws IOException { System.out.println("writeExternal 方法"); out.writeInt(age); } @Override public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException { System.out.println("readExternal 方法"); age = in.readInt(); }} //修改前文使用示例中的对象,即把SeObject改为ExtObject //输出/* 带参数构造方法 ExtObject 2 SeqObject{age=1, name='test1'} writeExternal 方法 ============== 默认构造方法 ExtObject readExternal 方法 SeqObject{age=1, name='null'}*/
从输出结果可以看到在写入的时候使用的带参数的构造方法,但是在反序列化的时候调用的是默认构造方法,最后再通过readExternal方法把对象读出来赋值。如果没有提供默认的构造方法就会抛出异常。
有兴趣整个运行流程可以打断点调试下,在源码中jdk1.8 1178行入口是writeOrdinaryObject(obj, desc, unshared)。
最后看下第三种方法重写readObject方法以及writeObject方法,其实现的原理就是在输入跟输出的时候会查看序列化对象是否覆盖着两个方法,如果存在则调用。在SeObject类中添加。
public class SeObject implements Serializable { private static final long serialVersionUID = -105996498570254302L; private int age; private String name; public SeObject() { System.out.println("默认构造方法 SeObject"); } public SeObject(int age, String name) { System.out.println("带参数构造方法 SeObject"); this.age = age; this.name = name; } @Override public String toString() { return "SeqObject2{" + "age=" + age + ", name='" + name + '\'' + '}'; } private void writeObject(ObjectOutputStream out) throws IOException { System.out.println("writeObject 方法"); out.writeInt(age); } private void readObject(ObjectInputStream in) throws IOException { System.out.println("readObject 方法"); age = in.readInt(); }} //输出 /* 带参数构造方法 SeObject SeqObject2{age=1, name='test1'} writeObject 方法 ============== readObject 方法 SeqObject2{age=1, name='null'}*/
输入流合并
SequenceInputStream 表示其他输入流的逻辑串联。它从输入流的有序集合开始,并从第一个输入流开始读取,直到到达文件末尾,接着从第二个输入流读取,依次类推,直到到达包含的最后一个输入流的文件末尾为止。
SequenceInputStream提供了两种构造方法:
- 第一种接收一个枚举对象;
- 两个输入流。
public SequenceInputStream(Enumeration<? extends InputStream> e) { this.e = e; try { nextStream(); } catch (IOException ex) { // This should never happen throw new Error("panic"); } } public SequenceInputStream(InputStream s1, InputStream s2) { Vector<InputStream> v = new Vector<>(2); v.addElement(s1); v.addElement(s2); e = v.elements(); try { nextStream(); } catch (IOException ex) { // This should never happen throw new Error("panic"); } } public int read() throws IOException { while (in != null) { int c = in.read(); if (c != -1) { return c; } nextStream(); } return -1; } public int read(byte b[], int off, int len) throws IOException { if (in == null) { return -1; } else 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; } do { int n = in.read(b, off, len); if (n > 0) { return n; } nextStream(); } while (in != null); return -1; } 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; }
可以看到SequenceInputStream的read方法实际上是调用传进来的输入流参数的read方法。
下面是使用示例:
//test.txt = 22try (FileInputStream fis = new FileInputStream(new File("D://test/test/test.txt")); //test2.txt = 测试字符流1 哈哈1测试字符流2 哈哈2 FileInputStream fis2 = new FileInputStream(new File("D://test/test/test2.txt")); SequenceInputStream sis = new SequenceInputStream(fis, fis2)) { int b; byte[] bytes = new byte[1024]; for (;(b = sis.read(bytes, 0, bytes.length)) != -1;) { for (int i = 0; i < b; i++) { //输出22加上一堆乱码,因为字节流操作无法处理汉字字符 System.out.print((char) bytes[i]); } } catch (IOException e) { e.printStackTrace(); }}
流(字节与字符转换)
通过InputStreamReader可以把字节转换成字符,而OutputStreamWriter可以字符转换成字节(适配器模式)。
.InputStreamReader提供了4种构造方法:
- 把字节流转成字符流按照默认的字符集;
- 按照给定的字符集;
- 按照给定的字符集编码器;
- 按照给定的字符集(如果String为Null则用默认的字符集)。
private final StreamDecoder sd; public int read() throws IOException { return sd.read(); } public int read(char cbuf[], int offset, int length) throws IOException { return sd.read(cbuf, offset, length); }
InputStreamReader内部是通过StringDecoder类实现的,StringDecoder是用来将字节解码成字符。
OutputStreamWriter提供的构造方法跟InputStreamReader一样。并且其内部是通过StringEncoder类实现的,StringEncoder用于把字符编码成字节。这个地方可以这样来理解:OutputStreamWriter的构造参数为字节流,而其本身是继承了Write的字符流。那么在其调用write方法的时候是写入的字符,但是要写入的字节流无法接受字符此时StringEncoder就开始起到作用了,把要写入的新的字符数据编码成字节输入到构造参数字节输出流里面。
下面是使用示例:
try (InputStreamReader isr = new InputStreamReader(new FileInputStream("D://test/test/test2.txt")); OutputStreamWriter osw = new OutputStreamWriter(new FileOutputStream("D://test/test/test2.txt", true)) ) { osw.write("新的数据写进去看看"); osw.flush(); int b; for (;(b = isr.read()) != -1;) { System.out.print((char) b); }} catch (IOException e) { e.printStackTrace();}
Filter基础(抽象)类
java通过装饰器模式来提供多种不同的I/O类对应不同的功能以便灵活的针对不同的应用场景。
- FilterInoutStream:继承了InputStream,作为所有Filter字节输入流超类;
- FilterOutputStream:继承了OutputStream,作为所有Filter字节输出流的超类;
- FilterReader:继承了Reader,作为所有Filter字符输入流的超(抽象)类;
- FilterWriter:继承了Writer,作为所有Filter字符输出流的超(抽象)类。
缓冲
字节流BufferedInputStream和BufferedOutputStream:
BufferedInputStream 为另一个输入流添加缓冲功能避免直接跟底层进行操作。在创建 BufferedInputStream 时,会创建一个内部缓冲区数组。在读取或跳过流中的字节时,可根据需要从包含的输入流再次填充该内部缓冲区。
public synchronized int read() throws IOException { if (pos >= count) { fill(); if (pos >= count) return -1; } return getBufIfOpen()[pos++] & 0xff; } /** * Check to make sure that buffer has not been nulled out due to * close; if not return it; */ private byte[] getBufIfOpen() throws IOException { byte[] buffer = buf; if (buffer == null) throw new IOException("Stream closed"); return buffer; } private void fill() throws IOException { byte[] buffer = getBufIfOpen(); if (markpos < 0) pos = 0; /* no mark: throw away the buffer */ else if (pos >= buffer.length) /* no room left in buffer */ if (markpos > 0) { /* can throw away early part of the buffer */ int sz = pos - markpos; System.arraycopy(buffer, markpos, buffer, 0, sz); pos = sz; markpos = 0; } else if (buffer.length >= marklimit) { markpos = -1; /* buffer got too big, invalidate mark */ pos = 0; /* drop buffer contents */ } else if (buffer.length >= MAX_BUFFER_SIZE) { throw new OutOfMemoryError("Required array size too large"); } else { /* grow buffer */ int nsz = (pos <= MAX_BUFFER_SIZE - pos) ? pos * 2 : MAX_BUFFER_SIZE; if (nsz > marklimit) nsz = marklimit; byte nbuf[] = new byte[nsz]; System.arraycopy(buffer, 0, nbuf, 0, pos); if (!bufUpdater.compareAndSet(this, buffer, nbuf)) { // Can't replace buf if there was an async close. // Note: This would need to be changed if fill() // is ever made accessible to multiple threads. // But for now, the only way CAS can fail is via close. // assert buf == null; throw new IOException("Stream closed"); } buffer = nbuf; } count = pos; int n = getInIfOpen().read(buffer, pos, buffer.length - pos); if (n > 0) count = n + pos; }
BufferedOutputStream的构造方法跟BufferedInputStream一样。
/** Flush the internal buffer */ private void flushBuffer() throws IOException { if (count > 0) { out.write(buf, 0, count); count = 0; } } public synchronized void write(int b) throws IOException { if (count >= buf.length) { flushBuffer(); } buf[count++] = (byte)b; } public synchronized void flush() throws IOException { flushBuffer(); out.flush(); }
可以看到当写入数据的时候会先把数据写到缓冲数组上,当手动调用flush方法,close方法或者缓冲数组的数据量大于缓冲数组长度才会把数据写入目的地。
下面是使用示例:
try (BufferedOutputStream bos = new BufferedOutputStream(new FileOutputStream(new File("D://test/test/test.txt"), true)); BufferedInputStream bis = new BufferedInputStream(new FileInputStream(new File("D://test/test/test.txt")));) { bos.write(55); bos.flush(); int b; for (;(b = bis.read()) != -1;) { System.out.println(b); }} catch (IOException e) { e.printStackTrace();}
字符流BufferedReader和BufferedWriter:
从字符输入流中读取文本,缓冲各个字符,从而实现字符、数组和行的高效读取。可以指定缓冲区的大小,或者可使用默认的大小8192。大多数情况下,默认值就足够大了。通常,Reader 所作的每个读取请求都会导致对底层字符或字节流进行相应的读取请求。因此,建议用 BufferedReader 包装所有其 read() 操作可能开销很高的 Reader(如 FileReader 和 InputStreamReader)。如果没有缓冲,则每次调用 read() 或 readLine() 都会导致从文件中读取字节,并将其转换为字符后返回,而这是极其低效的。
因为BufferedReader用的最多的就是readLine(),下面贴出它的源码。
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 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); } } } public String readLine() throws IOException { return readLine(false); }
可以看到BufferedReader如果到达流的末尾返回的是Null而不是-1,并且每一次读的时候根据回车换行\r\n来作为结束标识符。
BufferedWriter的构造方法跟BufferedReader一样就不贴出来了。前面提到了BufferedReader以回车换行为标识符,那么对应的输出流也就提供了newLine方法来写入一个回车换行符。它使用平台自己的行分隔符概念,此概念由系统属性 line.separator 定义。并非所有平台都使用新行符 ('\n') 来终止各行。同样需要调用flush方法、close方法才能把数据写入。
public BufferedWriter(Writer out, int sz) { super(out); if (sz <= 0) throw new IllegalArgumentException("Buffer size <= 0"); this.out = out; cb = new char[sz]; nChars = sz; nextChar = 0; lineSeparator = java.security.AccessController.doPrivileged( new sun.security.action.GetPropertyAction("line.separator")); } public void newLine() throws IOException { write(lineSeparator); }
下面是使用示例:
try (BufferedWriter bw = new BufferedWriter(new FileWriter("D://test/test/test.txt",true)); BufferedReader br = new BufferedReader(new FileReader("D://test/test/test.txt"))) { bw.write(11); bw.newLine(); bw.write("adasd",0,5); bw.flush(); String s; for (;(s = br.readLine()) != null;) { System.out.println(s); }} catch (IOException e) { e.printStackTrace();}
缓冲字节流与缓冲字符流的不同点:
- 底层数组不同;
- 字符流提供了读行readLine的方法;
- 字节流到达末尾返回-1而字符流返回Null。
跟踪行号
字节流LineNumberInputStream已经弃用这边不再介绍。
LineNumberReader踪行号的缓冲字符输入流。此类定义了方法 setLineNumber(int) 和 getLineNumber(),它们可分别用于设置和获取当前行号。默认情况下,行编号从 0 开始。该行号随数据读取在每个行结束符处递增,并且可以通过调用setLineNumber(int) 更改行号。但要注意的是,setLineNumber(int) 不会实际更改流中的当前位置;它只更改将由 getLineNumber() 返回的值。可认为行在遇到以下符号之一时结束:换行符('\n')、回车符('\r')、回车换行符。
public void setLineNumber(int lineNumber) { this.lineNumber = lineNumber; } public int getLineNumber() { return lineNumber; } @SuppressWarnings("fallthrough") public int read() throws IOException { synchronized (lock) { int c = super.read(); if (skipLF) { if (c == '\n') c = super.read(); skipLF = false; } switch (c) { case '\r': skipLF = true; case '\n': /* Fall through */ lineNumber++; return '\n'; } return c; } }
下面是使用示例:
try (LineNumberReader lnr = new LineNumberReader(new FileReader("D://test/test/test.txt"))) { String s; for (;(s = lnr.readLine()) != null;) { //lnr.setLineNumber(5); System.out.println(lnr.getLineNumber() + " " + s); }} catch (IOException e) { e.printStackTrace();}
数据
字节流DataInputStream和DataOutputStream类允许程序按照与机器无关的风格读取java的原始数据。
DataInputStream只提供了一个构造参数来接收一个字节输入流。下面看下几个方法的源码。
public final int readInt() throws IOException { int ch1 = in.read(); int ch2 = in.read(); int ch3 = in.read(); int ch4 = in.read(); if ((ch1 | ch2 | ch3 | ch4) < 0) throw new EOFException(); return ((ch1 << 24) + (ch2 << 16) + (ch3 << 8) + (ch4 << 0)); } public final boolean readBoolean() throws IOException { int ch = in.read(); if (ch < 0) throw new EOFException(); return (ch != 0); }
DataOutputStream也是跟DataInputStream一样的。
public final void writeInt(int v) throws IOException { out.write((v >>> 24) & 0xFF); out.write((v >>> 16) & 0xFF); out.write((v >>> 8) & 0xFF); out.write((v >>> 0) & 0xFF); incCount(4); } public final void writeBoolean(boolean v) throws IOException { out.write(v ? 1 : 0); incCount(1); }
下面是使用示例:
try (DataOutputStream dos = new DataOutputStream(new FileOutputStream(new File("D://test/test/test1.txt"))); DataInputStream dis = new DataInputStream(new FileInputStream(new File("D://test/test/test1.txt")))) { dos.writeUTF("writeUTF"); dos.write(50); dos.writeInt(1); dos.writeBoolean(false); dos.writeByte(1); System.out.println(dis.readUTF()); System.out.println(dis.read()); System.out.println(dis.readInt()); System.out.println(dis.readBoolean()); System.out.println(dis.readByte());} catch (IOException e) { e.printStackTrace();}
数据推回
PushbackInputStream 为另一个输入流添加性能,即“推回 (push back)”或“取消读取 (unread)”一个字节的能力。在代码片段可以很方便地读取由特定字节值分隔的不定数量的数据字节时,这很有用;在读取终止字节后,代码片段可以“取消读取”该字节,这样,输入流上的下一个读取操作将会重新读取被推回的字节。例如,表示构成标识符字符的字节可能由表示操作符字符的字节终止;用于读取一个标识符的方法可以读取到遇到操作符为止,然后将该操作符推回以进行重读。
PushbackInputStream提供了两种构造方法用来指定输入流以及缓冲区数组大小。
protected byte[] buf; protected int pos; private void ensureOpen() throws IOException { if (in == null) throw new IOException("Stream closed"); } public PushbackInputStream(InputStream in, int size) { super(in); if (size <= 0) { throw new IllegalArgumentException("size <= 0"); } this.buf = new byte[size]; this.pos = size; } public PushbackInputStream(InputStream in) { this(in, 1); } public int read() throws IOException { ensureOpen(); if (pos < buf.length) { return buf[pos++] & 0xff; } return super.read(); } public void unread(int b) throws IOException { ensureOpen(); if (pos == 0) { throw new IOException("Push back buffer is full"); } buf[--pos] = (byte)b; } public int available() throws IOException { ensureOpen(); int n = buf.length - pos; int avail = super.available(); return n > (Integer.MAX_VALUE - avail) ? Integer.MAX_VALUE : n + avail; }
从源码可以看到,PushbackInputStream推回或者说取消先前读取字节的过程就是把已经读取到的字节放到缓冲数组而不是放回到流上,当你继续读取的时候会优先从缓冲数组上拿去。
下面是使用示例:
byte[] b = {50,51,52,53,54}; try (FileOutputStream fos = new FileOutputStream(new File("D://test/test/test5.txt")); PushbackInputStream pis = new PushbackInputStream(new FileInputStream(new File("D://test/test/test5.txt"))) ) { fos.write(b, 0, b.length); //判断剩余字节 System.out.println("读取前流中有多少字节:" + pis.available()); int a = pis.read(); System.out.println("读取一个字节 " + a + " 后流中有多少字节:" + pis.available()); pis.unread(a); System.out.println("推回一个字节后流中有多少字节:" + pis.available()); a = pis.read(); } catch (IOException e) { e.printStackTrace(); }/* 输出 读取前流中有多少字节:5 读取一个字节 50 后流中有多少字节:4 推回一个字节后流中有多少字节:5*/}
PushbackReade的构造方法以及使用也基本差不多,直接看源码。
/** Pushback buffer */ private char[] buf; /** Current position in buffer */ private int pos; public PushbackReader(Reader in, int size) { super(in); if (size <= 0) { throw new IllegalArgumentException("size <= 0"); } this.buf = new char[size]; this.pos = size; } public PushbackReader(Reader in) { this(in, 1); } /** Checks to make sure that the stream has not been closed. */ private void ensureOpen() throws IOException { if (buf == null) throw new IOException("Stream closed"); } public int read() throws IOException { synchronized (lock) { ensureOpen(); if (pos < buf.length) return buf[pos++]; else return super.read(); } }
格式化
字节流PrintStream提供了7种构造方法:
- 构造一个不会自动刷新数据的PrintStream;
- 构造一个不会自动刷新数据,指定字符集的PrintStream;
- 构造一个不会自动刷新数据的PrintStream;
- 构造一个可以指定是否自动刷新数据的PrintStream;
- 构造一个可以指定是否自动刷新数据和指定字符集的PrintStream;
- 构造一个不会自动刷新数据的PrintStream;
- 构造一个不会自动刷新数据,指定字符集的PrintStream;
PrintStream为字节输出流添加了功能,可以通过构造方法自由选择实现字符或字节的写入并且支持自动刷新缓冲区的数据。PrintStream不会抛出IO异常需要通过checkError方法来判断。其实PrintStream内部通过继承父类FilterInputStream来实现字节流的写入,同时还对字节流进行封装成一个BufferedWriter。下面来看下源码便一目了然。
private final boolean autoFlush; private boolean trouble = false; private Formatter formatter; /** * Track both the text- and character-output streams, so that their buffers * can be flushed without flushing the entire stream. */ private BufferedWriter textOut; private OutputStreamWriter charOut; /* Private constructors */ private PrintStream(boolean autoFlush, OutputStream out) { super(out); this.autoFlush = autoFlush; this.charOut = new OutputStreamWriter(this); this.textOut = new BufferedWriter(charOut); } private PrintStream(boolean autoFlush, OutputStream out, Charset charset) { super(out); this.autoFlush = autoFlush; this.charOut = new OutputStreamWriter(this, charset); this.textOut = new BufferedWriter(charOut); } private PrintStream(boolean autoFlush, Charset charset, OutputStream out) throws UnsupportedEncodingException { this(autoFlush, out, charset); } public PrintStream(OutputStream out) { this(out, false); } public PrintStream(OutputStream out, boolean autoFlush) { this(autoFlush, requireNonNull(out, "Null output stream")); } public PrintStream(OutputStream out, boolean autoFlush, String encoding) throws UnsupportedEncodingException { this(autoFlush, requireNonNull(out, "Null output stream"), toCharset(encoding)); } public PrintStream(String fileName) throws FileNotFoundException { this(false, new FileOutputStream(fileName)); } public PrintStream(String fileName, String csn) throws FileNotFoundException, UnsupportedEncodingException { // ensure charset is checked before the file is opened this(false, toCharset(csn), new FileOutputStream(fileName)); } public PrintStream(File file) throws FileNotFoundException { this(false, new FileOutputStream(file)); } public PrintStream(File file, String csn) throws FileNotFoundException, UnsupportedEncodingException { // ensure charset is checked before the file is opened this(false, toCharset(csn), new FileOutputStream(file)); } private void write(String s) { try { synchronized (this) { ensureOpen(); textOut.write(s); textOut.flushBuffer(); charOut.flushBuffer(); if (autoFlush && (s.indexOf('\n') >= 0)) out.flush(); } } catch (InterruptedIOException x) { Thread.currentThread().interrupt(); } catch (IOException x) { trouble = true; } } private void newLine() { try { synchronized (this) { ensureOpen(); textOut.newLine(); textOut.flushBuffer(); charOut.flushBuffer(); if (autoFlush) out.flush(); } } catch (InterruptedIOException x) { Thread.currentThread().interrupt(); } catch (IOException x) { trouble = true; } }
PrintStream提供了多种方式来写入数据,其中print和println是用来支持字符写入,后者带比前者多执行了newLine方法。而write方法则是支持字节的写入。还有format(printf也是引用了format方法)方法其内部引用了Formatter来格式化字符串。
下面是使用示例:
try (PrintStream ps = new PrintStream("D://test/test/test3.txt"); FileInputStream fis = new FileInputStream(new File("D://test/test/test3.txt"))) { ps.print(50); ps.println(50); ps.write(50); ps.printf("%d", 1); int b; for (;(b = fis.read()) != -1;) { //输出53 48 53 48 13 10 50 49 //对应的输入就是5 0 5 0 回车 换行 50 1 System.out.print(b + " "); }} catch (IOException e) { //这边的IO异常由FileInputStream抛出,PrintStream不会抛出IO异常需要手动调用checkError方法 e.printStackTrace();}
PrintWriter构造方法跟PrintStream一样的。它向文本输出流打印对象的格式化表示形式。此类实现在 PrintStream 中的所有 print 方法。它不包含用于写入原始字节的方法,对于这些字节,程序应该使用未编码的字节流进行写入。与 PrintStream 类不同,如果启用了自动刷新,则只有在调用 println、printf 或 format 的其中一个方法时才可能完成此操作,而不是每当正好输出换行符时才完成。这些方法使用平台自有的行分隔符概念,而不是换行符(前文缓冲BufferedWriter中提到的LineSeparator)。此类中的方法不会抛出 I/O 异常,尽管其某些构造方法可能抛出异常。客户端需要调用 checkError() 查看是否出现错误。
来看下源码便一目了然:
protected Writer out; private final boolean autoFlush; private boolean trouble = false; private Formatter formatter; private PrintStream psOut = null; private static Charset toCharset(String csn) throws UnsupportedEncodingException { Objects.requireNonNull(csn, "charsetName"); try { return Charset.forName(csn); } catch (IllegalCharsetNameException|UnsupportedCharsetException unused) { // UnsupportedEncodingException should be thrown throw new UnsupportedEncodingException(csn); } } 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")); } 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; } } public PrintWriter(String fileName) throws FileNotFoundException { this(new BufferedWriter(new OutputStreamWriter(new FileOutputStream(fileName))), false); } /* Private constructor */ private PrintWriter(Charset charset, File file) throws FileNotFoundException { this(new BufferedWriter(new OutputStreamWriter(new FileOutputStream(file), charset)), 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); } public void write(String s, int off, int len) { try { synchronized (lock) { ensureOpen(); out.write(s, off, len); } } catch (InterruptedIOException x) { Thread.currentThread().interrupt(); } catch (IOException x) { trouble = true; } } public void write(String s) { write(s, 0, s.length()); } 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; } }
下面是使用示例:
try (PrintWriter ps = new PrintWriter("D://test/test/test3.txt"); FileInputStream fis = new FileInputStream(new File("D://test/test/test3.txt"))) { ps.print(50); ps.println(50); ps.write(50); ps.printf("%d", 1); //PrintWriter中print,write,println没有PrintStream中的flushBuffer,因此如果没有指定自动刷新的话需要手动调用flush方法写入 ps.flush(); int b; for (;(b = fis.read()) != -1;) { //输出53 48 53 48 13 10 50 49 //对应的输入就是5 0 5 0 回车 换行 50 1 System.out.print(b + " "); } } catch (IOException e) { //这边的IO异常由FileInputStream抛出,PrintWriter不会抛出IO异常需要手动调用checkError方法 e.printStackTrace();}
字节流PrintStream和字符流PrintWriter不同点:
- 底层数组不同;
- PrintStream的write方法按照字节写入而PrintWriter按照字符写入;
- PrintStream换行符直接用\n表示,而PrintWriter采用跟平台相关换行符
字节流与字符流总结
可以看到两种流都有其各自的使用场景。但是在大部分情况下应该首选字符流其可以代替字节流的所有使用尤其在面向字符操作上面。从贴出的源码也可以看到所有的字符流在读写操作上都通过synchronized来实现方法或者方法块的同步一定程度保证了线程安全。此外字符流的设计在某种程度上读写操作更方便,速度更快,比如文件操作支持读行。但是这不意味着字符流可以完全取代字节流,在部分场景例如文件的压缩zip类库只能通过字节流来实现。不管字节流还是字符流都是阻塞的,所有的I/O流包括输入流或者输出流每次都只能读取一个字节或者写入一个字节。
标准I/O(System,FileDescriptor)与Console
标准I/O是许多操作系统的一个特性。一般情况下表示从键盘输入并在显示器上面输出。它还支持文件上的I/O和程序之间的I/O,但是该特性由命令行解释器(command line interpreter,也称为CLI、命令语言解释器、控制台用户界面、命令处理器、shell、命令行shell或命令解释器。)控制,而不是由程序控制。Java提供了三种标准流:标准输入System.in、标准输出System.out、标准错误System.error。(System类还具有多种功能比如对外部定义的属性和环境变量的访问;加载文件和库的方法;快速复制数组)
下面是System类源码中的关于标准IO的主要代码,从下面代码中可以知道几点信息:
- 标准I/O流是字节流;
- 标准I/O流基于FileDescriptor生成;
- 标准输入流System.in由BufferedInputStream装饰的输入流,但是对外提供的时候上转成基类InputStream;
- 标准输出流System.out跟System.error都是由PrintStream装饰的输出流;
- 标准I/O流提供了set方法来支持重定向。
public final static InputStream in = null; public final static PrintStream out = null; public final static PrintStream err = null; /** * Initialize the system class. Called after thread initialization. */ private static void initializeSystemClass() { FileInputStream fdIn = new FileInputStream(FileDescriptor.in); FileOutputStream fdOut = new FileOutputStream(FileDescriptor.out); FileOutputStream fdErr = new FileOutputStream(FileDescriptor.err); setIn0(new BufferedInputStream(fdIn)); setOut0(newPrintStream(fdOut, props.getProperty("sun.stdout.encoding"))); setErr0(newPrintStream(fdErr, props.getProperty("sun.stderr.encoding"))); } private static native void setIn0(InputStream in); private static native void setOut0(PrintStream out); private static native void setErr0(PrintStream err); public static void setIn(InputStream in) { checkIO(); setIn0(in); } public static void setOut(PrintStream out) { checkIO(); setOut0(out); } public static void setErr(PrintStream err) { checkIO(); setErr0(err); }
下面是使用示例:
//数据源为键盘,输出到目的地控制台上 //字节流/* try (BufferedInputStream bis = (BufferedInputStream) System.in) { int b = 0; for (;(b = bis.read()) != -1;) { System.out.println(b); } } catch (IOException e) { e.printStackTrace(); }*/ //键盘输入255 //控制台输出50 53 53 10 ,输出结果在前文介绍字节流抽象基类的时候已经介绍过。 //字符流/* try (BufferedReader br = new BufferedReader(new InputStreamReader(System.in))) { String s; for (;(s = br.readLine()) != null;) { System.out.println(s); } } catch (IOException e) { e.printStackTrace(); }*/ //键盘输入255 //控制台输出255 //下面是标准I/O流的重定向示例 重定向输入流把数据源又键盘改成文件,输出到控制台上 try ( BufferedInputStream in = new BufferedInputStream(new FileInputStream("D://test/test/test2.txt"))) { System.setIn(in); BufferedReader br = new BufferedReader(new InputStreamReader(System.in)); String s; while ((s = br.readLine()) != null) { System.out.println(s); } br.close(); } catch (IOException e) { e.printStackTrace(); } //因为test2.txt文件中的内容为 2323\n3232 //输出2323\n3232
FileDescriptor表示文件描述符,计算机系统内核利用文件描述符来访问文件。文件描述符是非负整数。打开现存文件或新建文件时,内核会返回一个文件描述符。读写文件也需要使用文件描述符来指定待读写的文件。在Java中文件描述符对象表示一个有效的、开放的文件、套接字或其他活动 I/O 连接。它提供了三种的标准流,但是一般情况下不建议直接操作文件描述符而是通过对其进行封装的System类。
/** * A handle to the standard input stream. Usually, this file * descriptor is not used directly, but rather via the input stream * known as {@code System.in}. * * @see java.lang.System#in */ public static final FileDescriptor in = standardStream(0); /** * A handle to the standard output stream. Usually, this file * descriptor is not used directly, but rather via the output stream * known as {@code System.out}. * @see java.lang.System#out */ public static final FileDescriptor out = standardStream(1); /** * A handle to the standard error stream. Usually, this file * descriptor is not used directly, but rather via the output stream * known as {@code System.err}. * * @see java.lang.System#err */ public static final FileDescriptor err = standardStream(2);
Console类可访问与当前 Java 虚拟机关联的基于字符的控制台设备,它具有标准流提供的绝大多数特性,尤其适用于控制台的密码输入。通过readPassword方法可以让用户输入的密码不可见。如果想要了解怎么使用可以查阅下方贴出的官方参考资料。
RandomAccessFile使用
RandomAccessFile既不属于字节流也不属于字符流它是一个独立的类,自身实现了所有的读写方法并且大部分都是本地方法。通过RandomAccessFile可以实现文件的随机读取并且可以选择文件的访问级别。
构造参数File以及name跟前文介绍的是一样的用来指定文件对象。后面mode用来表示文件的访问级别:
- “r”:以只读方式打开。调用结果对象的任何 write 方法都将导致抛出 IOException;
- "rw":以读写方式打开如果该文件不存在会创建一个新的文件;
- "rws":以读写方式打开,对于 "rw"来说还要求对文件的内容或元数据(访问权限、内容类型、最后修改时间)的每个更新都同步写入到底层存储设备。
- "rwd":以读写方式打开,对于 "rw"来说还要求对文件的内容的每个更新都同步写入到底层存储设备。
rws以及rwd模式对确保在系统崩溃时不会丢失重要信息特别有用。如果该文件不在本地设备上,则无法提供这样的保证。从效率上来看rw大于rwd大于rws。
由于源码中基本都是本地方法就补贴出来,直接看下如何实现随机读写:
RandomAccessFile raf = new RandomAccessFile("D://test/test/test11.txt", "rw");byte[] b = {50,51,52,53};//写入测试数据raf.write(b);//文件有多少个字节long length = raf.length();//倒序读出测试数据,由于最后一个字节为EOF标识-1所以在length上-1for (long i = length -1; i >= 0; i--) { //设置文件读取位置 raf.seek(i); System.out.print(raf.read() + " ");}//输出53 52 51 50
使用I/O流对对象进行复制
前文在介绍序列化的时候已经介绍过ObjectInputStream以及ObjectOutputStream可以用来对对象进行持久化操作。本节旨在说明通过这两个类可以实现Java对象的深度复制。这比实现Cloneable重写clone方法更为简单。
下面是使用示例:
public class Zoo1 implements Serializable { private int age; private Dog dog; public Zoo1() { System.out.println("Zoo1 default constructor"); } public Zoo1(int age, Dog dog) { System.out.println("Zoo1 age name constructor"); this.age = age; this.dog = dog; } public Dog getDog() { return dog; }}
public class Zoo2 implements Serializable { private int age; private Dog dog; public Zoo2() { System.out.println("Zoo2 default constructor"); } public Zoo2(int age, Dog dog) { System.out.println("Zoo2 age name constructor"); this.age = age; this.dog = dog; } public Dog getDog() { return dog; }}
public class Dog implements Serializable { private int age; private String name; public Dog() { System.out.println("Dog default constructor"); } public Dog(int age, String name) { this.age = age; this.name = name; System.out.println("Dog age name constructor"); }}
Dog dog = new Dog(1, "吐司"); Zoo1 z1 = new Zoo1(5, dog); Zoo2 z2 = new Zoo2(10, dog); System.out.println("before z1 : " + z1 + " zi.Dog : " + z1.getDog()); System.out.println("before z2 : " + z2 + " zi.Dog : " + z2.getDog()); ByteOutputStream bos1 = new ByteOutputStream(); ByteOutputStream bos2 = new ByteOutputStream(); try (ObjectOutputStream oos1 = new ObjectOutputStream(bos1); ObjectOutputStream oos2 = new ObjectOutputStream(bos2); ObjectInputStream ois1 = new ObjectInputStream(new ByteArrayInputStream(bos1.getBytes())); ObjectInputStream ois2 = new ObjectInputStream(new ByteArrayInputStream(bos1.getBytes()) ) ) { oos1.writeObject(z1); oos1.writeObject(z2); oos2.writeObject(z1); oos2.writeObject(z2); z1 = (Zoo1) ois1.readObject(); System.out.println("after oos1 z1 :" + z1 + " z1.Dog : " + z1.getDog()); z2 = (Zoo2) ois1.readObject(); System.out.println("after oos1 z1 :" + z2 + " z1.Dog : " + z2.getDog()); z1 = (Zoo1) ois2.readObject(); System.out.println("after oos2 z1 :" + z1 + " z1.Dog : " + z1.getDog()); z2 = (Zoo2) ois2.readObject(); System.out.println("after oos2 z1 :" + z2 + " z1.Dog : " + z2.getDog()); } catch (IOException e) { e.printStackTrace(); } catch (ClassNotFoundException e) { e.printStackTrace(); }/* 输出 Dog age name constructor Zoo1 age name constructor Zoo2 age name constructor before z1 : IO.Zoo1@2f0e140b zi.Dog : IO.Dog@7440e464 before z2 : IO.Zoo2@49476842 zi.Dog : IO.Dog@7440e464 after oos1 z1 :IO.Zoo1@cc34f4d z1.Dog : IO.Dog@17a7cec2 after oos1 z1 :IO.Zoo2@65b3120a z1.Dog : IO.Dog@17a7cec2 after oos2 z1 :IO.Zoo1@6f539caf z1.Dog : IO.Dog@79fc0f2f after oos2 z1 :IO.Zoo2@50040f0c z1.Dog : IO.Dog@79fc0f2f*/
从输出结果可以得到几个结果:
- 不同流直接反序列化后得到的对象以及成员对象是不同的;
- 同一个流下序列化前如果两个对象有相同的成员对象反序列后的两个对象同样拥有相同的成员对象;
- 反序列化后的对象跟原对象的引用以及成员对象与原成员对象的引用都不同,实现了深度复制;