Java IO流
在Java IO中,流是一个核心的概念。流从概念上来说是一个连续的数据流。你既可以从流中读取数据,也可以往流中写数据。流与数据源或者数据流向的媒介相关联。在Java IO中流既可以是字节流
(以字节为单位进行读写),也可以是字符流
(以字符为单位进行读写)。
一个程序需要InputStream或者Reader从数据源读取数据,需要OutputStream或者Writer将数据写入到目标媒介中。InputStream和Reader与数据源相关联,OutputStream和writer与目标媒介相关联
。
Java IO的用途和特征
Java IO中包含了许多InputStream、OutputStream、Reader、Writer的子类。这样设计的原因是让每一个类都负责不同的功能。这也就是为什么IO包中有这么多不同的类的缘故。各类用途汇总如下:
- 文件访问
- 网络访问
- 内存缓存访问
- 线程内部通信(管道)
- 缓冲
- 过滤
- 解析
- 读写文本 (Readers / Writers)
- 读写基本类型数据 (long, int etc.)
- 读写对象
Java IO类概述表
通过输入、输出、基于字节或者字符、以及其他比如缓冲、解析之类的特定用途划分的大部分Java IO类的表格。
字节流
简介
字节流是以面向字节
的形式提供IO操作。
-
字节流(OutputStream/InputStream)处理单元为1个字节,操作
字节
和字节数组
。 -
字节流可用于任何类型。
-
字节流在操作的时候本身是不会用到缓冲区的,是与文件本身直接操作的,所以字节流在操作文件时,即使不关闭资源,文件也能输出。
基类字节流
InputStream/OutputStream 是Java IO API中所有输入/输出
字节流
的父类。表示有序的字节流,换句话说,可以将 InputStream/OutputStream 中的数据作为有序的字节序列读写。该类是一个抽象类,我们一般很少直接使用它们,而是使用其子类实现我们对字节的读写操作。
InPutStream
所有输入字节流的父类
-
重要方法
read()
从输入流中读取单个字节,是个抽象类,由子类实现
public abstract int read()
read(byte b[])
从输入流中读取指定长度(b.length)的字节转换为字节数组(存储在数组中)(内部调用read(byte b[], int off, int len)方法)
public int read(byte b[]) throws IOException { return read(b, 0, b.length); }
read(byte b[], int off, int len)
从输入流中读取len长度的字节数据,存储在数组b中偏移off位置.(内部通过循环从输入流中逐个字节读取存储在数据中)
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++) { //循环需要读取的字节长度,调用read方法逐个字节读取 c = read(); if (c == -1) { break; } b[off + i] = (byte)c; } } catch (IOException ee) { } return i; }
OutputStream
所有输出字节流的父类
-
重要方法
write(int b)
将单个字节写入此文件的输出流
public abstract void write(int b) throws IOException;
write(byte b[])
将指定的字节数组写入此文件的输出流.(内部调用write(byte b[], int off, int len)方法)
public void write(byte b[]) throws IOException { write(b, 0, b.length); }
write(byte b[], int off, int len)
将指定的字节数组,从偏移off位置开始,len长度字节写入此文件输出流.(内部通过循环逐个字节将数组中数据写入输出流)
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; } //循环写入字节长度,调用write(int b)方法逐个将数组中字节写入到输出流 for (int i = 0 ; i < len ; i++) { write(b[off + i]); } }
文件字节流
FileInputStream
FileInputStream被称为文件字节输入流,意思指对文件数据以
字节
的形式进行读取操作如读取图片视频等。其继承于InputStream,拥有输入流的基本特性。
-
构造方法
FileInputStream(String name)
通过一个文件路径来创建一个FileInputStream文件输入流如果文件不存在,或者文件是一个目录,而不是常规文件,或由于其他原因无法打开,则抛出FileNotFoundException异常!(内部调用FileInputStream(File file)方法)
public FileInputStream(String name) throws FileNotFoundException { this(name != null ? new File(name) : null); } /** *代码示例 */ String filePath = "/data/data/com.test/test.txt"; FileInputStream fis = new FileInputStream(filePath);
FileInputStream(File file)
通过指定一个实际文件,来创建一个FileInputStream文件输入流。
/** *代码示例 */ String filePath = "/data/data/com.test/test.txt"; File file = new File(filepath); if(!file.exists()){ file.createNewFile(); } FileInputStream fis = new FileInputStream(file);
-
重要方法
read()
从输入流中每次读取一个字节
public int read() throws IOException { byte[] b = new byte[1]; if(read(b, 0, 1) != -1)){ return b[0] & 0xff; }else{ return -1; } }
read(byte b[])
从输入流中读取b.length长度字节转换为字节数组.(内部调用了read(byte b[], int off, int len)方法)
public int read(byte b[]) throws IOException { return read(b, 0, b.length); } /** *代码示例 */ FileInputStream fis = new FileInputStrean(new File("/data/data/com.test/test.txt")); byte buffer[] = new byte[8*1024]; int len =- 1; while((le = fis.read(buffer)) != -1{ System.out.println("str = " + new String(buffer,0,len)) }
read(byte b[], int off, int len)
FileInputStream的read(byte b[],int off,int len)方法内部通过JNI实现(
IoBridge.read(fd, b, off, len)
),我们无法查看具体实现,但是我们可以参考其父类InputStream的read(byte b[],int off,int len)的方法如下/** *从输入流中读取最多len字节的数据到字节数组中b中。 *@param b:存储读取的字节数据 *@param off:读取字节数据存储在b中偏移量 *@param len:需要读取的长度 *PS:内部通过循环调用read()逐个字节读取存目标数组中 */ 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 { //遍历目标的长度,调用read()方法,逐个字节读取到目标字节数组内 for (; i < len ; i++) { c = read(); if (c == -1) { break; } b[off + i] = (byte)c; } } catch (IOException ee) { } return i; } /** *代码示例 */ FileInputStream fis = new FileInputStrean(new File("/data/data/com.test/test.txt")); byte buffer[] = new byte[8*1024]; int len =- 1; while((le = fis.read(buffer,0,buffer.length)) != -1{ System.out.println("str = " + new String(buffer,0,len)) }
FileOutputStream
FileOutputStream被称为文件字节输出流,意思指对文件数据以字节的形式进行写入操作。专用于输出原始字节流如图像数据等,其继承OutputStream类,拥有输出流的基本特性
-
构造方法
FileOutputStream(String name)
通过指定文件名称创建FileOutputStream文件输出流。(内部调用FileOutputStream(File file)方法)
public FileOutputStream(String name) throws FileNotFoundException { this(name != null ? new File(name) : null, false); } /** *代码示例 */ String filePath = "/data/data/com.test/test.txt"; FileOutputStream fos = new FileOutputStream(filePath);
FileOutputStream(String name, boolean append)(内部调用FileOutputStream(File file, boolean append)方法)
/** *通过指定文件名称和append创建一个FileOutputStream文件输出流。 *@param append:true-表示向文件尾部追加写入数据;false-表示覆盖文件内容,默认为false */ public FileOutputStream(String name, boolean append) throws FileNotFoundException { this(name != null ? new File(name) : null, append); } /** *代码示例 */ String filePath = "/data/data/com.test/test.txt"; FileOutputStream fos = new FileOutputStream(filePath,true); fos.write(byte b[])//调用write方法时候会向目标文件尾部追加内容,而不是覆盖原来内容。
FileOutputStream(File file)(内部调用FileOutputStream(File file, boolean append)方法)
/** *通过指定实例文件创建一个FileOutputStream文件输出流。 */ public FileOutputStream(File file) throws FileNotFoundException { this(file, false); } /** *代码示例 */ String filePath = "/data/data/com.test/test.txt"; File file = new File(filepath); if(!file.exists()){ file.createNewFile(); } FileOutputStream fos = new FileOutputStream(file);
FileOutputStream(File file, boolean append)(以上构造方法最终都是通过该方法实现)
/** *通过指定的实例文件和append创建一个FileOutputStream文件输出流。 * 1)如果文件不存在,则自动创建; * 2)如果文件是一个目录,而不是一个常规文件,或者文件无法创建,或者由于其他原因无法打开文件, * 则会抛出FileNotFoundException异常 *@param append:true-表示向文件尾部追加写入数据;false-表示覆盖文件内容,默认为false */ public FileOutputStream(File file, boolean append) throws FileNotFoundException { String name = (file != null ? file.getPath() : null); SecurityManager security = System.getSecurityManager(); if (security != null) { security.checkWrite(name); } if (name == null) { throw new NullPointerException(); } if (file.isInvalid()) { throw new FileNotFoundException("Invalid file path"); } // BEGIN Android-changed: Open files through common bridge code. // this.fd = new FileDescriptor(); int flags = O_WRONLY | O_CREAT | (append ? O_APPEND : O_TRUNC); this.fd = IoBridge.open(name, flags); // END Android-changed: Open files through common bridge code. // Android-changed: Tracking mechanism for FileDescriptor sharing. // fd.attach(this); this.isFdOwner = true; this.append = append; this.path = name; // Android-removed: Open files through common bridge code. // open(name, append); // Android-added: File descriptor ownership tracking. IoUtils.setFdOwner(this.fd, this); // Android-added: CloseGuard support. guard.open("close"); } /** *代码示例 */ String filePath = "/data/data/com.test/test.txt"; File file = new File(filepath); if(!file.exists()){ file.createNewFile(); } FileOutputStream fos = new FileOutputStream(file,true); fos.write(byte b[])//调用write方法时候会向目标文件尾部追加内容,而不是覆盖原来内容。
-
重要方法
write(int b)(内部调用write(byte b[] , int off, int len)方法实现)
/** *将指定的单个字节写入此文件输出流 */ public void write(int b) throws IOException { write(new byte[] { (byte) b }, 0, 1); } /** *示例代码 */ String filePath = "/data/data/com.test/test.txt"; FileOutputStream fos = new FileOutputStream(filePath); byte byteData[] = "Hello".getBytes(); fos.write(byteData[0]); //写入单个字节
write(byte b[])(内部调用write(byte b[] , int off, int len)方法实现)
/** *将指定的字节数组写此入文件输出流 */ public void write(byte b[]) throws IOException { write(b, 0, b.length); } /** *示例代码 */ String filePath = "/data/data/com.test/test.txt"; FileOutputStream fos = new FileOutputStream(filePath); byte byteData[] = "Hello".getBytes(); fos.write(byteData); //写入字节数组
write(byte b[], int off, int len)(以上Write方法都是基于该方法实现)
FileOutputStream的Write(byte b[],int off,int len)方法内部通过libcore库JNI实现(
IoBridge.write(fd, b, off, len)
),我们无法查看具体实现,但是我们可以参考其父类OutputStream的Write(byte b[],int off,int len)的方法如下/** *将指定的字节数组b中的数据,从偏移量off开始写出len长度到此文件输出流 *@param b:要写出到输出流中的字节数组 *@param off:字节数组中偏移量 *@param len:需要写出的长度 *PS:内部通过循环调用write()方法逐个字节写出 */ 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]); } } /** *代码示例 */ String filePath = "/data/data/com.test/test.txt"; FileOutputStream fos = new FileOutputStream(filePath); byte byteData[] = "Hello".getBytes(); fos.write(byteData,0,byteData.length); //写入字节数组(偏移量为0,写入长度为整个数组长度)
缓冲字节流
BufferedInpuStream/BufferedOutputStream是FileInputStream/FileOutputStream的子类,是字节流的包装类,为字节流提供了缓冲功能。
BufferedInputStream
字节输入缓冲流,当我们通过read()读取输入流的数据时,BufferedInputStream会先将该输入流的数据
分批的填入到缓冲区中
,然后从缓冲区中读取数据
。每当缓冲区中的数据被读完之后,输入流会再次填充数据缓冲区;如此反复,直到我们读完输入流数据位置。
-
构造方法
BufferedInputStream(InputStream in)
/** *指定一个字节输入流,并且使用默认缓冲大小(8192字节)创建缓冲字节输入流 */ public BufferedInputStream(InputStream in) { this(in, defaultBufferSize); } /** *代码示例 */ String filePath = "/data/data/com.test/test.txt"; BufferedInputStream bis = new BufferedInputStream(new FileInputStream(filePath));
BufferedInputStream(InputStream in, int size)
/** *指定一个字节输入流和缓冲区大小,创建缓冲字节输入流 */ public BufferedInputStream(InputStream in, int size) { super(in); if (size <= 0) { throw new IllegalArgumentException("Buffer size <= 0"); } buf = new byte[size]; } /** *代码示例 */ String filePath = "/data/data/com.test/test.txt"; int bufSize = 8*1024; //指定缓冲区大小为8kb BufferedInputStream bis = new BufferedInputStream(new FileInputStream(filePath),bufSize);
-
重要方法
read()
/** *从缓冲区读取单个字节 */ public synchronized int read() throws IOException { // 若已经读完缓冲区中的数据,则调用fill()从输入流读取下一部分数据来填充缓冲区 if (pos >= count) { fill(); //填充之后,缓冲区依然没有数据,已经读取结束,返回-1 if (pos >= count) return -1; } // 否则从缓冲区中读取指定的字节 return getBufIfOpen()[pos++] & 0xff; } /** *代码示例 */ String filePath = "/data/data/com.test/test.txt"; BufferedInputStream bis = new BufferedInputStream(new FileInputStream(filePath)); int byteData = bis.read();
read(byte b[], int off, int len)
/** *一次性从缓冲区中读取len长度的字节,填充到目标字节数组b的偏移off位置(内部使用了数组拷贝) *@param b:用于存放从缓冲区中读取字节数据 *@param off:数组b偏移位置 *@param len:要从缓冲区读取的字节长度 */ public synchronized int read(byte b[], int off, int len) throws IOException{ getBufIfOpen(); // Check for closed stream if ((off | len | (off + len) | (b.length - (off + len))) < 0) { throw new IndexOutOfBoundsException(); } else if (len == 0) { return 0; } //读取到指定长度的数据才返回 int n = 0; for (;;) { int nread = read1(b, off + n, len - n); if (nread <= 0) return (n == 0) ? nread : n; n += nread; if (n >= len) return n; // if not closed but no bytes available, return InputStream input = in; if (input != null && input.available() <= 0) return n; } } /** *将缓冲区中的数据写入到字节数组b中。off是字节数组b的起始位置,len是写入长度 */ private int read1(byte[] b, int off, int len) throws IOException { int avail = count - pos; /** *1.如果缓冲区内数据为空,进一步判断目标读取长度与缓冲区大小 */ if (avail <= 0) { /** *1-1如果目标读取的长度大于缓冲区的长度 并且没有markpos,则直接从原始输入流中 *进行读取,从而避免无谓的COPY(从原始输入流至缓冲区,读取缓冲区全部数据,清空缓冲区, *重新填入原始输入流数据)。 */ if (len >= getBufIfOpen().length && markpos < 0) { return getInIfOpen().read(b, off, len); } /** *1-2如果目标读取长度小于缓冲区大小,则调用fill()从输入流读取下一部分数据来填充缓冲区 *再一次判断缓冲区是数据是否有效,如果没有则返回-1读取完毕! */ fill(); avail = count - pos; if (avail <= 0) return -1; } /** *2.如果缓冲区内有数据,取目标读取长度与缓冲区有效数据长度最小值,然后通过数组拷贝, * 将缓冲数组内数据拷贝到目标数组中的偏移off位置 */ int cnt = (avail < len) ? avail : len; System.arraycopy(getBufIfOpen(), pos, b, off, cnt); pos += cnt; return cnt; } // 获取输入流 private InputStream getInIfOpen() throws IOException { InputStream input = in; if (input == null) throw new IOException("Stream closed"); return input; } // 获取缓冲 private byte[] getBufIfOpen() throws IOException { byte[] buffer = buf; if (buffer == null) throw new IOException("Stream closed"); return buffer; } /** *代码示例 */ String filePath = "/data/data/com.test/test.txt"; BufferedInputStream bis = new BufferedInputStream(new FileInputStream(filePath)); byte buffer = new byte[8*1024]; int len = -1; while((len = bis.read(buffer,0,buffer.length)) != -1){ System.out.println(new String(buffer,0,buffer.length)); }
mark(int readlimit)
/** *标记“缓冲区”中当前位置 */ public synchronized void mark(int readlimit) { marklimit = readlimit; markpos = pos; }
reset()
/** *将“缓冲区”中当前位置重置到mark()所标记的位置 */ public synchronized void reset() throws IOException { getBufIfOpen(); // Cause exception if closed if (markpos < 0) throw new IOException("Resetting to invalid mark"); pos = markpos; }
注意:BufferedInputStream没有重写父类的read(byte b[])
方法,因此即使BufferedInputStream调用该方法,实际是父类执行,并不会触发缓冲功能。
BufferedOutputStream
字节输出缓冲流,当我们通过write()方法将字节数据写出时候,BufferedOutputStream会先将字节数据写入到内部的缓冲区,而不是目标文件,当缓冲区满以后才会将缓冲区数据一次写出到输出流。(注意:
如果写出的字节数组大于缓冲区大小,则会直接写出到输出流,不在通过缓冲区
)
-
构造方法
BufferedOutputStream(OutputStream out)
指定一个输出字节流作为参数,创建一个默认缓冲数组大小为8192字节的缓冲输出流
BufferedOutputStream(OutputStream out) /** *代码示例 */ String filePath = "/data/data/com.test/test.txt"; BufferedOutputStream bos = new BufferedOutputStream(new FileOutputStream(filePath))
BufferedOutputStream(OutputStream out, int size)
接受一个输出流和一个指定大小的缓冲数组作为参数,创建一个缓冲输出流
BufferedOutputStream(OutputStream out, int size); /** *代码示例 */ String filePath = "/data/data/com.test/test.txt"; int bufferSize = 8*1024; //指定缓冲区大小为8kb BufferedOutputStream bos = new BufferedOutputStream(new FileOutputStream(filePath),bufferSize)
-
重要方法
write(int b)
将单个字节写入缓冲区(
如果缓冲区已满,则先刷新缓冲区,然后再写入
)public void write(int b) throws IOException{ if (count >= buf.length) { flushBuffer(); } buf[count++] = (byte)b; } /** *代码示例 */ String filePath = "/data/data/com.test/test.txt"; BufferedOutputStream bos = new BufferedOutputStream(new FileOutputStream(filePath)); byte[] byteData = "Hello World!".getBytes(); bos.write(byteData[0]); //将单个字节写入缓冲区
write(byte[] b, int off, int len)
将指定字节数组,从off位置开始,写入len长度字节到缓冲区
1.如果写入的字节长度len > 缓冲区大小,那么先将缓冲区数据写入到输出流,然后再将此字节数组数据直接写入到输出流(不经过缓冲区)
;2.如果写入的字节长度 > 缓冲区实际可用大小,则先将缓冲区数据写入输出流,然后再将当前字节数据写入缓冲区中
;3.如果写入的字节长度 < 当前缓冲区实际可用大小,则直接将当前要写入的字节数据写入缓冲区内
;public void write(byte[] b, int off, int len) throws IOException{ /** *如果请求长度超过了输出缓冲区的大小,刷新输出缓冲区,然后调用其构造函数中传入的输出流 *直接将数据写入到底层输出流 */ if (len >= buf.length) { flushBuffer(); out.write(b, off, len); return; } /** *如果请求长度大于缓冲区可用大小,先刷新缓冲区(将缓冲区数据写入底层输出流),然后再将 *请求数据copy到缓冲区 */ if (len > buf.length - count) { flushBuffer(); } System.arraycopy(b, off, buf, count, len); count += len; } /** *代码示例 */ String filePath = "/data/data/com.test/test.txt"; BufferedOutputStream bos = new BufferedOutputStream(new FileOutputStream(filePath)); byte[] byteData = "Hello World!".getBytes(); bos.write(byteData,0,byteData.length) //将字节数组指定长度字节写入缓冲区
flush()
刷新缓冲输出流。强制将"缓冲的数据"写入到底层输出流中
public void flush() throws IOException{ flushBuffer(); out.flush(); }
close()
关闭此输出流并释放与该流关联的任何系统资源
public void close() throws IOException { try (OutputStream ostream = out) { flush(); } }
flushBuffer()
缓冲区刷新具体执行方法
private void flushBuffer() throws IOException { /** *如果缓冲区有效字节数大于0,则调用其构造函数中传入的底层输出流,将数据写入底层输出流, *然后将缓冲区有效字节数重置 */ if (count > 0) { out.write(buf, 0, count); count = 0; } }
注意:BufferedOutputStream并没有重写父类的write(byte b[])
方法,因此即使BufferedOutputStream使用该方法,本质是父类执行操作,不会出发缓冲功能。
字符流
简介
字符流是字节流的包装,字符流则可以直接接受字符串,它内部将字符串转换成字节,底层设备永远只接收字节数据。
注意:字符流自带缓冲功能,如果字符流不调用close或flush方法,则不会输出任何内容。因此在进行写操作后要调用flush()
或者close()
来将缓冲区的数据写到输出流。可能会有人疑问了?既然字符流自带缓冲功能,那还需要Buferedreader
和BufferedWriter
缓冲字符流干什么?它们有什么区别呢?具体详解见文尾《字符流自带的缓冲区与BufferedReader/BuferedWriter中缓冲区的区别?》
基类字符流
Reader与Writer是Java IO中所有输入输出字符流的基类。Reader与InputStream类似,Writer与OutputStream类似,不同点在于,
Reader与Writer基于字符
而InputStream与OutputStream是基于字节
。换句话说,Reader与Writer用于读取和写入文本,而InputStream与OutputStream用于读取和写入原始字节。通常我们会使用Reader与Writer的子类,而不会直接使用Reader或Writer。具体子类可查看Java IO类概述表表格。
Reader
所有输入字符流的父类
-
重要方法
read()
从字符输入流中读取单个字符并返回(内部调用
read(char cbuf[], int off, int len)
方法)public int read() throws IOException { char cb[] = new char[1]; if (read(cb, 0, 1) == -1) return -1; else return cb[0]; }
read(char cbuf[])
从字符输入流中读取指定长度的字符存储在内存数组cbuf中(内部调用
read(char cbuf[], int off, int len)
方法)public int read(char cbuf[]) throws IOException { return read(cbuf, 0, cbuf.length); }
read(char cbuf[], int off, int len)
从字符输入流中读取len长度的字符,存放在内存数组cbuf的偏移off位置,该方法是抽象方法,由子类实现。
abstract public int read(char cbuf[], int off, int len) throws IOException;
Writer
所有输出字符流的父类
-
重要方法
write(int c)
将单个字符写入到输出流(内部调用
write(char cbuf[])
方法)public void write(int c) throws IOException { synchronized (lock) { if (writeBuffer == null){ writeBuffer = new char[WRITE_BUFFER_SIZE]; } writeBuffer[0] = (char) c; write(writeBuffer, 0, 1); } }
write(char cbuf[])
将指定的字符数组写入到输出流(内部调用
write(char cbuf[])
方法)public void write(char cbuf[]) throws IOException { write(cbuf, 0, cbuf.length); }
write(char cbuf[], int off, int len)
将指定的字符数组中,偏移off开始,len长度的字符写入输出流(该方法是抽象方法,由子类实现)
abstract public void write(char cbuf[], int off, int len) throws IOException;
write(String str)
将指定的字符串写入到字符输出流(内部调用
write(String str, int off, int len)
方法)public void write(String str) throws IOException { write(str, 0, str.length()); }
write(String str, int off, int len)
从指定的字符串的off位置开始,将len长度的字符写入到输出流(内部将字符转换为字节数组调用了
write(char cbuf[], int off, int len)
方法)public void write(String str, int off, int len) throws IOException { synchronized (lock) { char cbuf[]; if (len <= WRITE_BUFFER_SIZE) { if (writeBuffer == null) { writeBuffer = new char[WRITE_BUFFER_SIZE]; } cbuf = writeBuffer; } else { // Don't permanently allocate very large buffers. cbuf = new char[len]; } str.getChars(off, (off + len), cbuf, 0); write(cbuf, 0, len); } }
flush()
刷新流,将缓冲区中保存的字符数据,强制写出到目标(该方法是抽象方法,由子类实现)
abstract public void flush() throws IOException;
close()
关闭输出流(该方法是抽象方法,由子类实现)
abstract public void close() throws IOException;
文件字符流
FileReader/FileWriter是InputStreamReader/OutputStreamWriter的子类,具备父类的基本特性。用于读写文件的字符流(
每次都是直接从文件单个字符读写,频繁操作IO
),该类构造函数使用系统默认的字符集编码。如果想要自己指定这些值,请使用FileInputStream/OutputStream来构造一个InputStreamReader/OuputStreamWrite。
FileReader
文件输入字符流
-
构造方法
FileReader(String fileName)
指定一个文件名称,来创建一个文件输入字符流(内部调用父类(InputStreamReader)的构造方法实现,默认指定一个FileInputStream(fileName)文件输入字节流和空的字符集)
public FileReader(String fileName) throws FileNotFoundException { super(new FileInputStream(fileName)); }
FileReader(File file)
指定一个实例文件,来创建一个文件输入字符流(内部调用父类InputStreamReader(InputStream in)的构造方法实现,默认指定一个new FileInputStream(file)文件输入字节流和空的字符集)
public FileReader(File file) throws FileNotFoundException { super(new FileInputStream(file)); } /** *代码示例 */ String filePath = "/data/data/com.test/test.txt"; File file = new File(filePath); if(!file.exists()){ file.createNewFile(); } FileReader fr = new FileReader(file); int charData = fr.read(); //从输入流读取单个字符 char buffer[] = new char[8*1024]; int len = fr.read(buffer) //从输入流读取指定长度字符,存储在字符数组buffer中
FileWriter
文件输出字符流
-
构造方法
FileWriter(String fileName)
指定一个文件名,创建一个文件输出字符流(内部调用父类OutputStreamWriter(OutputStream out)的 构造方法实现,默认指定一个FileOutputStream(fileName)文件输出字节流和空的字符集)
public FileWriter(String fileName) throws IOException { super(new FileOutputStream(fileName)); }
FileWriter(String fileName, boolean append)
指定一个文件名,和append表示符创建一个文件输出字符流(内部调用父类 OutputStreamWriter(OutputStream out)的构造方法实现,默认指定一个 FileOutputStream(fileName, append)文件输出字节流和空的字符集)
public FileWriter(String fileName, boolean append) throws IOException { super(new FileOutputStream(fileName, append)); }
FileWriter(File file)
指定一个实例文件,创建一个文件输出字符流(内部调用父类 OutputStreamWriter(OutputStream out)的构造方法实现,默认指定一个FileOutputStream(file) 文件输出字节流和空的字符集)
public FileWriter(File file) throws IOException { super(new FileOutputStream(file)); }
FileWriter(File file, boolean append)
指定一个实例文件,和append表示符创建一个文件输出字符流(内部调用父类 OutputStreamWriter(OutputStream out)的构造方法实现,默认指定一个 FileOutputStream(file, append)文件输出字节流和空的字符集)
public FileWriter(File file, boolean append) throws IOException { super(new FileOutputStream(file, append)); } /** *代码示例 */ String filePath = "/data/data/com.test/test.txt"; File file = new File(filePath); if(!file.exists()){ file.createNewFile(); } FileWriter fw = new FileWriter(file,false); String msg = "美国新冠病毒致死人数升至全球第一。特朗普政府想通过各种甩锅行为转移国内矛盾!"; fw.write(msg.charAt(0)); //将单个字符写入输出流 fw.write(msg); //将字符串写入输出流 fw.write(char cbuf,int off,int len); //将指定字符数组,从off开始写入len长度到输出字符流 fw.write(String str,int off,int len); //将指定字符串,从off开始,写入len长度字符到输出字符流
缓冲字符流
BuffredReader/BufferedWriter 缓冲字符流,
它可以包装字符流,为字符流的读写提供缓冲功能
,减少直接与磁盘之间的I/O操作,提升读写效率。原理类似于BufferedInputStream/BufferedOutputStream缓冲字节流。BufferedReader/BufferedWriter内部默认大小为8192字符长度的缓冲区。
- 当BufferedReader读取文本文件时,会尽量读取文本数据到缓冲区内,之后如果调用
read()
方法,会先从缓冲区读取数据,如果缓冲区数据不够,然后才会从文件读取;- 当使用BufferedWriter写文本文件时,写入的数据并不会立马输出到目的地,而是先存储到缓冲区,如果缓冲区内数据存满了,才会一次性对目的地进行写出;
BufferedReader
缓冲输入字符流
-
构造方法
BufferedReader(Reader in)
通过包装一个指定的字符输入流,创建一个缓冲字符输入流(使用默认的缓冲大小)
BufferedReader(Reader in)
BufferedReader(Reader in, int sz)
通过包装一个指定的字符输入流,并使用指定的缓冲大小创建一个缓冲字符输入流
BufferedReader(Reader in, int sz) /** *代码示例 */ String filePath = "/data/data/com.test/test.txt"; String bufSize = 8*1024; BufferedReader br = new BufferedReader(new FileReader(filePath),bufSize); //从缓冲输入流的缓冲区读取单个字符 int charData = br.read(); //从输入缓冲流的缓冲区读取buffer.length长度的字符到内存字符数组buffer中 char buffer[] = new char[8*1024]; int len = br.read(buffer); //等同于 br.read(buffer,0,buffer.length);
-
重要方法
read()
从缓冲区读取单个字符
- 如果缓冲区字符被读取完,先调用fill()方法从字符输入流中读取一部分数据填充到缓冲区,然后再从缓冲区读取一个字节(减少每次对磁盘的读操作,降低对磁盘损耗)。
public int read() throws IOException { synchronized (lock) { ensureOpen(); for (;;) { if (nextChar >= nChars) { fill(); //先从输入流读取一部分数据填充数据到缓冲区 if (nextChar >= nChars) return -1; } if (skipLF) { skipLF = false; if (cb[nextChar] == '\n') { nextChar++; continue; } } return cb[nextChar++]; } } }
read(char[] cbuf, int off, int len)
从缓冲区读取len长度的字符到内存数组cbuf的off位置
注意:
-
如果缓冲区没有数据,分为以下两种情况:
读取的len长度大于缓冲区大小,则从底层输入流直接单个读取len长度字符到内存数组cbuf中的off位置,不再经过缓冲区
;读取的len长度小于缓冲区大小,先从底层输入流读取一部分数据填充到缓冲区,再从缓冲区读取(copy))len长度字符到内存数组cbuf中的off位置
;
-
如果缓冲区有数据,分为以下下两种情况:
-
读取的长度(len)大于缓冲区内有效的数据长度(n),则先从缓冲区内读取余下有效的长度(n)字符存放到数组cbuf内,再循环一次(缓冲区数据被读完后再次读取先fill填充)从缓冲区中读取余下长度(len-n)的字符到内存数据cbuf中一并返回
; -
读取的长度(len)小于缓冲区内有效的数据长度,则直接从缓冲区内读取(copy)len长度的字符到内存数组cbuf中的off位置
。
-
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; } //从缓冲区读取字符的具体方法 int n = read1(cbuf, off, len); if (n <= 0) return n; //如果读取的字符长度小于目标长度,则循环读取,直到满足目标读取的长度len为止 while ((n < len) && in.ready()) { int n1 = read1(cbuf, off + n, len - n); if (n1 <= 0) break; n += n1; } return n; } } /** *具体从缓冲区读取字符数据的方法 */ private int read1(char[] cbuf, int off, int len) throws IOException { //如果缓冲区数据为空 if (nextChar >= nChars) { //并且要读取的长度大于缓冲区长度,则直接从底层流中读取 if (len >= cb.length && markedChar <= UNMARKED && !skipLF) { return in.read(cbuf, off, len); } //否则先从底层流读取数据填充到缓冲区 fill(); } if (nextChar >= nChars) return -1; if (skipLF) { skipLF = false; if (cb[nextChar] == '\n') { nextChar++; if (nextChar >= nChars) fill(); if (nextChar >= nChars) return -1; } } //计算要读取的长度len与当前缓冲区内有效数据长度最小值 int n = Math.min(len, nChars - nextChar); //通过数组copy将缓冲区内的有效数据copy到内存数组cbuf的off为止 System.arraycopy(cb, nextChar, cbuf, off, n); nextChar += n; return n; }
readLine()
从缓冲区内读取一行,返回改行内的字符串String
public String readLine() throws IOException { return readLine(false); }
-
代码示例
/** *创建一个输入字符缓冲流 */ File file = new File(localPath, fileName); BufferedReader br = new BufferedReader(new FileReader(file)); //从缓冲区读取单个字符 int charDara = br.read(); //从缓冲区读取指定长度的字符,存放在内存数组cbuf的off位置 //注意:定义的数组大小尽可能不要超过缓冲区大小,否则read方法内部会直接从个底层流直接读取而不是通过缓存) char cbuf[] = new char[8*1024]; int len = br.read(cbuf,0,cbuf.length); //从缓冲区中读取一行字符串 String lineData = br.readLine();
注意:BufferedReader类没有重写父类的read(char cbuf[])
方法,因此即使使用BuferedReader来调用这个方法,本质还是由父类执行并不会出发缓冲区功能。
BufferedWriter
缓冲输出字符流
-
构造函数
BufferedWriter(Writer out)
通过包装一个指定的字符输出流,创建一个缓冲字符输出流(使用默认的缓冲大小)
public BufferedWriter(Writer out) { this(out, defaultCharBufferSize); }
BufferedWriter(Writer out, int sz)
通过包装一个指定的字符输出流,并指定缓冲区大小,创建一个缓冲字符输出流
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")); }
-
重要方法
write(int c)
将单个字符写入到缓冲区。如果缓冲区已满,则先刷新缓冲区,然后再写入。
public void write(int c) throws IOException { synchronized (lock) { ensureOpen(); if (nextChar >= nChars) flushBuffer(); cb[nextChar++] = (char) c; } }
write(char cbuf[], int off, int len)
将指定的字符数组,从off位置开始,写入len长度字符到缓冲区。
注意:
如果写入的长度大于缓冲区大小,则先刷新缓冲区,然后将字符数组直接写出到底层输出流
。- 如果写入的长度小于缓冲区大小,有以下两种情况:
写入的长度小于缓冲区当前可用大小,则直接将字符数组copy到缓冲区
;写入的长度大于缓冲区当前可用大小,则先从数组中copy缓冲区剩余大小的数据到缓冲区中,然后刷新缓冲区,将数据写出到输出流,然后再循环一次将剩下的数组数据copy到缓冲区中
;
public void write(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; } /** *如果写入的字符长度大于缓冲区大小,则先刷新缓冲区,然后直接将字符数组写入到输出流 *而不是再写入缓冲区。 */ if (len >= nChars) { flushBuffer(); out.write(cbuf, off, len); return; } int b = off, t = off + len; /** *如果写入的数组大小大于缓冲区可用大小,循环将数组写入到缓冲区内(第一次先写入缓冲区当前 *可容纳的数据,之后刷新缓冲区将数据写出到输出流,第二次将剩下的数据再写入缓冲区内) */ while (b < t) { int d = min(nChars - nextChar, t - b); System.arraycopy(cbuf, b, cb, nextChar, d); b += d; nextChar += d; if (nextChar >= nChars) flushBuffer(); } } }
write(String s, int off, int len)
将指定字符串的数据,从off位置开始,写入len长度到缓冲区
public void write(String s, int off, int len) throws IOException { synchronized (lock) { ensureOpen(); int b = off, t = off + len; while (b < t) { int d = min(nChars - nextChar, t - b); s.getChars(b, b + d, cb, nextChar); b += d; nextChar += d; if (nextChar >= nChars) flushBuffer(); } } }
-
代码示例
/** *使用BufferedWriter包装一个字符输出流,调用write将指定的字符、字符数组、字符串写入到缓冲区 */ String filePath = "/data/data/com.test/test.txt"; BufferedWriter bw = new BufferedWriter(new FileWriter(filePath)); String msg = "美国新冠病毒致死人数升至全球第一。特朗普政府想通过各种甩锅行为转移国内矛盾!"; //将单个字符写入缓冲区 bw.write(msg.charAt(0)); //将指定字符数组,从off开始写入len长度字符到缓冲区 char charDatas[] = msg.toCharArray(); bw.write(charDatas, 0, charDatas.length); //将指定字符串,从off开始,写入len长度字符到缓冲区 bw.write(msg,0,msg.lenght);
/** *结合BufferedReader来对文件进行读写 */ BufferedReader br = new BufferedReader(new FileReader(new File(fileName))); BufferedWriter bw = new BufferedWriter(new FileWriter(new File(writerFile))); //单个字符读写 int charData; while ((charData = br.read()) != -1) { bw.write(charData); } bw.close(); br.close(); //通过字符数组进行读写 char buffer[] = new char[8 * 1024]; int len; while ((len = br.read(buffer, 0, buffer.length)) != -1) { bw.write(buffer, 0, len); } bw.close(); br.close(); //通过字符串进行读写 String str; while ((str = br3.readLine()) != null) { bw3.write(str, 0, str.length()); } bw3.close(); br3.close();
注意:BufferedWrite类没有重写父类的write(char cbuf[])
和write(String str)
方法,因此即使使用BuferedWrite来调用这两个方法,本质还是由父类执行并不会出发缓冲区功能。
转换流
InputStreamReader
是从**
输入字节流—>输入字符流
**转换流,它读取字节并使用指定的字符集或者平台默认的字符集将它们解码为字符。
-
构造方法
InputStreamReader(InputStream in)
通过指定输入字节流,创建一个输入转换流(字节流->字符流)。
public InputStreamReader(InputStream in) { super(in); try { sd = StreamDecoder.forInputStreamReader(in, this, (String)null); } catch (UnsupportedEncodingException e) { // The default encoding should always be available throw new Error(e); } }
InputStreamReader(InputStream in, String charsetName)
通过指定输入字节流和指定字符集,创建一个输入转换流(字节流—>字符流)
public InputStreamReader(InputStream in, String charsetName) throws UnsupportedEncodingException { super(in); if (charsetName == null) throw new NullPointerException("charsetName"); sd = StreamDecoder.forInputStreamReader(in, this, charsetName); } /** *代码示例 */ String filePath = "/data/data/com.test/test.txt"; String charset = "UTF-8"; //指定的字符集为UTF-8编码 InputStreamReader isr = new InputSteamReader(new FileInputStream(filePath),charset)
-
重要方法
read()
读取单个字符
public int read() throws IOException { return sd.read(); } /** *代码示例 */ String filePath = "/data/data/com.test/test.txt"; InputStreamReader isr = new InputSteamReader(new FileInputStream(filePath)) int charData = isr.read();
read(char cbuf[], int offset, int length)
读取length长度的字符,存储到内存字符数组cbuf的偏移off位置。
public int read(char cbuf[], int offset, int length) throws IOException { return sd.read(cbuf, offset, length); } /** *代码示例 */ String filePath = "/data/data/com.test/test.txt"; InputStreamReader isr = new InputSteamReader(new FileInputStream(filePath)) char buffer[] = new char[8*1024]; int len = -1; while((len = isr.read(buffer,0,buffer.length)) != -1){ System.out.println(new String(buffer,0,buffer.length)); }
OutputStreamWriter
是从**
字符流—>字节流
**转换流,它接受字符并使用指定的字符集或者平台默认的字符集将他们编码为字节。
-
构造函数
OutputStreamWriter(OutputStream out)
指定字节输出流,创建一个输出转换流(使用平台默认字符集进行转换),(字符->字节)
public OutputStreamWriter(OutputStream out) { super(out); try { se = StreamEncoder.forOutputStreamWriter(out, this, (String)null); } catch (UnsupportedEncodingException e) { throw new Error(e); } }
OutputStreamWriter(OutputStream out, String charsetName)
指定字节输出流和字符编码集,创建一个输出转换流(字符->字节)
public OutputStreamWriter(OutputStream out, String charsetName) throws UnsupportedEncodingException { super(out); if (charsetName == null) throw new NullPointerException("charsetName"); se = StreamEncoder.forOutputStreamWriter(out, this, charsetName); } /** *代码示例 */ String filePath = "/data/data/com.test/test.txt"; String charset = "UTF-8"; OutputStreamWriter osw = new OutputStreamWriter(new FileOutputStream(filePath,charset));
-
重要方法
write(int c)
写入单个字符
public void write(int c) throws IOException { se.write(c); }
write(char cbuf[], int off, int len)
将字符数组cbuf内数据,从off位置开始,指定len长度的字符写入到字符输出流
public void write(char cbuf[], int off, int len) throws IOException { se.write(cbuf, off, len); }
write(String str, int off, int len)
将指定字符串中的字符,从off位置开始,指定len长度字符写入到字符输出流
public void write(String str, int off, int len) throws IOException { se.write(str, off, len); } /** *代码示例 */ String filePath = "/data/data/com.test.test.txt"; OutputStreamWriter osw = new OutputStreamWriter(new FileOutputStream(filePath)); String msg = "美国新冠病毒致死人数升至全球第一。特朗普政府想通过各种甩锅行为转移国内矛盾!"; osw.write(msg,0,msg.length); osw.flush(); osw.close();
flush()
刷新流,将会把缓冲区中的字符,强制写入到输出流。
public void flush() throws IOException { se.flush(); }
close()
关闭输出流
public void close() throws IOException { se.close(); }
扩展
IO的常规的read()读操作为什么返回的是int类型?
-
确保返回类型一致
使用IO进行读操作时候,如果文件读取结束,那么我们会接受到一个
-1
来表示文件已经读取完毕,此时返回的-1
是int类型。倘若我们在正常读时候(文件没有读取结束)返回byte类型,那么显然是无法通过一个方法返回两种类型的,因此这里统一返回为int类型。 -
便于直观观察
在约定了返回为int类型的情况下,我们每次read操作其实返回的是8位二进制数据,二进制数据不利于分析观察,可以转换成十进制来展示,因此需要把读取的二进制字节转换成十进制,因为返回的是int类型
IO操作为什么需要缓冲?
原因很简单,效率高
!缓冲中的数据实际上是保存在内存中,而原始数据可能是保存在硬盘或NandFlash等存储介质中;而我们知道,从内存中读取数据的速度比从硬盘读取数据的速度至少快10倍以上。
- 那干嘛不干脆一次性将全部数据都读取到缓冲中呢?
- 读取全部的数据所需要的时间可能会很长。
- 内存价格很贵,容量不像硬盘那么大。
字符流自带的缓冲区与BufferedReader/BuferedWriter中缓冲区的区别?
Java 在IO操作中,都会有一个缓冲区的,它们不同在于缓冲区的大小。BufferWriter更合理的使用缓冲区,在你对大量的数据时,FileWrite的效率明显不如BufferWriter。
- BuferedWriter
- 提供缓冲区(
默认8192字符=16384字节
),且可通过构造函数来修改(一般不需要)。 - 效率高:当缓冲区满或者主动调用
flush()
或close()
方法,才会通过OutputStreamWriter
中的StreamEncode
负责查询码表(将字符转换成字节数)。
- 提供缓冲区(
- FileWriter
- 严格来说自身不提供缓冲,而是其父类(``OutputStreamWriter
)通过
StreamEncoder将字符转换为字节存储在
StreamEncoder`缓冲区中(DEFAULT_BYTE_BUFFER_SIZE = 8192) - 效率低:来一个字符查询一次编码表(通过
StreamEncoder
将字符转换为字节,存储在缓冲字节数组中)
- 严格来说自身不提供缓冲,而是其父类(``OutputStreamWriter
执行流程图
Android openFileInput与openFileOutput操作
Android中的openFileInput
与openFileOutput
是Android平台提供基于Context上下文环境,对内部存空间即/data/data/packagename/files
路径下文件进行读写操作的字节流。
代码示例
/**
* 打开内部存储当前应用files目录下指定文件,写入数据(如果文件不存在则创建)
*/
fun writeFile(context:Context){
val fileName = "test.txt"
val fos = this.openFileOutput(fileName, Context.MODE_PRIVATE)
fos.write("Hello World!".toByteArray())
fos.close()
}
/**
* 打开内部存储当前应用files目录下指定文件,读取文件数据。
*(如果文件不存在,或者无法正常打开,则抛出异常:Caused by: java.io.FileNotFoundException)
*/
fun testOpenFileInput(){
val fileName2 = "test2.txt"
val fis = this.openFileInput(fileName2)
val buffer = ByteArray(8 * 1024)
val len = fis.read(buffer)
println(String(buffer, 0, len))
}
参考文章: