Java常见的文件输入输出流详解
Java 默认使用 Unicode 字符集来表示字符;
Java 语言中,中文字符所占的字节数取决于字符的编码方式,采用 ISO8859-1 编码方式时,一个中文字符与一个英文字符一样只占1个字节;
采用 GB2312 或 GBK 或 Unicode 编码方式时,一个中文字符占 2 个字节;
而采用 UTF-8 编码方式时,一个中文字符会占 3 个字节。
public static void main(String[] args) { byte d[]="abc夏日的海滩".getBytes(); System.out.println(d.length); // 18 个字节
}
上面的程序采用的就是 UTF-8 编码方式,5个中文字符占15个字节,加上3个英文字节:输出 d.length = 18
Java中一般修改后常使用的是 UTF-8 编码, 兼容性强;
一 File类
【1】定义:Java中通过 java.io.File类来对一个文件(包括目录)进行抽象的描述。但有 File 对象,并不代表真实存在该文件。
【2】属性:
【3】构造方法
【4】方法
File构造的方法能够传入一个路径来指定一个文件,这个路径可以是绝对路径也可以是相对路径对象构造好之后, 我们就可以用下面的方法来进行一些具体功能的实现。
二 InputStream和OutputStream
二进制文件是以字节来读写的,通过InputStream来读,通过OutputStream来写。
文本文件是以字符单位进行读写的,通过Reader来读,通过Writer来写。
注:上述这些都是抽象类因此实际使用往往都需要他们的子类:FileInputStream、FileOutputStream、FileReader、FileWriter来具体实现文件的读写。
【1】InputStream
InputStream只是一个抽象类,要使用还需要具体的实现类。关于 InputStream 的实现类有很多,基本可以认为不同的输入设备都可以对应一个InputStream 类,我们现在只关心从文件中读取,所以使用FileInputStream类。
【2】FileInputStream
【3】FileOutputStream
说明OutputStream 同样只是一个抽象类,要使用还需要具体的实现类。我们现在还是只关心写入文件中,所以使用 FileOutputStream!
三 缓冲流 BufferInputStream 和 FileInputStream 的区别
import java.io.*; /** * 分别用普通数据流和带缓冲区的数据流复制一个167M的数据文件 * 通过用时比较两者的工作效率 * @author Zues * */ public class CopyMp3 { private static File file = new File("D:\\1.mp4"); private static File file_cp = new File("D:\\1_cp.mp4"); // FileInputStream复制 public void copy() throws IOException { FileInputStream in = new FileInputStream(file); FileOutputStream out = new FileOutputStream(file_cp); int len = 0; while ((len = in.read(buf)) != -1) { out.write(buf); } in.close(); out.close(); } // BufferedStream复制 public void copyByBuffer() throws IOException { BufferedInputStream in = new BufferedInputStream(new FileInputStream(file)); BufferedOutputStream out = new BufferedOutputStream(new FileOutputStream(file_cp)); byte[] buf = new byte[1024]; int len; while ((len = in.read(buf)) != -1) { out.write(buf); } in.close(); out.close(); } public static void main(String[] args) throws IOException { CopyMp3 copy=new CopyMp3(); long time1=System.currentTimeMillis(); copy.copy(); long time2=System.currentTimeMillis(); System.out.println("直接复制用时:"+(time2-time1)+"毫秒"); long time3=System.currentTimeMillis(); copy.copyByBuffer(); long time4=System.currentTimeMillis(); System.out.println("缓冲区复制用时:"+(time4-time3)+"毫秒"); } }
【1】输出结果:
【2】分析
上述InputStream使用了byte[] buf = new byte[1024]; 作为read方法的缓冲区。根据运行结果可以知道InputStream的缓冲区与BufferedInputStream的缓冲区是不一样的。那么现在通过查看源码来分析两者有什么不一样。
package java.io; public class FileInputStream extends InputStream{ /** *从输入流中读取一个字节 *该方法为private私有方法,用户不能直接调用。 *该方法为native本地方法,这是因为Java语言不能直接与操作系统或计算机硬件交互, *只能通过调用C/C++编写的本地方法来实现对磁盘数据的访问。 */ private native int read0() throws IOException; public int read() throws IOException { Object traceContext = IoTrace.fileReadBegin(path); //指向Path int b = 0; try { b = read0(); //调用read0()方法 } finally { IoTrace.fileReadEnd(traceContext, b == -1 ? 0 : 1); //从Path读出b=read0() } return b; } /** * 从输入流中读取多个字节到byte数组中 * 该方法也是私有本地方法,不对用户开放,只供内部调用。 */ private native int readBytes(byte b[], int off, int len) throws IOException; //调用native方法readBytes(b, 0, b.length)每次读取多个字节 public int read(byte b[]) throws IOException { Object traceContext = IoTrace.fileReadBegin(path); int bytesRead = 0; try { bytesRead = readBytes(b, 0, b.length); //每次读取缓冲区全部的 } finally { IoTrace.fileReadEnd(traceContext, bytesRead == -1 ? 0 : bytesRead); } return bytesRead; } }
【3】BufferedInputStream
package java.io; public class BufferedInputStream extends FilterInputStream { private static int defaultBufferSize = 8192; //缓冲区数组默认大小8192Byte,也就是8K protected volatile byte buf[]; //内部缓冲数组,会根据需要进行填充,大小默认为8192字节,也可以用构造函数自定义大小 protected int count; //当count=0时,说明缓冲区内容已读完,会再次填充 protected int pos; // 缓冲区指针,记录缓冲区当前读取位置 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; } //创建默认大小的BufferedInputStream public BufferedInputStream(InputStream in) { this(in, defaultBufferSize); } //此构造方法可以自定义缓冲区大小 public BufferedInputStream(InputStream in, int size) { super(in); if (size <= 0) { throw new IllegalArgumentException("Buffer size <= 0"); } buf = new byte[size]; } private void fill() throws IOException { byte[] buffer = getBufIfOpen(); if (markpos < 0) pos = 0; //....部分源码省略 count = pos; int n = getInIfOpen().read(buffer, pos, buffer.length - pos); if (n > 0) count = n + pos; } /** * 读取一个字节 * 与FileInputStream中的read()方法不同的是,这里是从缓冲区数组中读取了一个字节 * 也就是直接从内存中获取的,效率远高于前者 */ public synchronized int read() throws IOException { if (pos >= count) { fill(); if (pos >= count) return -1; } return getBufIfOpen()[pos++] & 0xff; } //从缓冲区中一次读取多个字节 private int read1(byte[] b, int off, int len) throws IOException { int avail = count - pos; if (avail <= 0) { if (len >= getBufIfOpen().length && markpos < 0) { return getInIfOpen().read(b, off, len); } fill(); avail = count - pos; if (avail <= 0) return -1; } int cnt = (avail < len) ? avail : len; System.arraycopy(getBufIfOpen(), pos, b, off, cnt); pos += cnt; return cnt; } public synchronized int read(byte b[], int off, int len){ //为减少文章篇幅,源码就不显示了 } }
结论:
- InputStream:通过源码可以看到,如果用read()方法读取一个文件,每读取一个字节就要访问一次硬盘,这种读取的方式效率是很低的。即便使用read(byte b[])方法一次读取多个字节,每次读取多个字节就要访问一次硬盘。当读取的文件较大时,也会频繁的对磁盘操作。
- BufferedInputStream:也就是说,Buffered类初始化时会创建一个较大的byte数组,一次性从底层输入流中读取多个字节来填充byte数组,当程序读取一个或多个字节时,可直接从byte数组中获取,当内存中的byte读取完后,会再次用底层输入流填充缓冲区数组。这种从直接内存中读取数据的方式要比每次都访问磁盘的效率高很多。
【4】总结
- InputStream每次read,都需要从硬件读取文件一个或者若干个字节。
- BufferedInputStream会提前读取默认大小字节到内部缓冲区,每次read都直接从内存读取一个或若干个字节。