JAVA篇:Java IO (三)访问文件--转换流和文件流
JAVA IO中的文件流涉及流式部分的转换流InputStreamReader和OutputStreamWriter、文件流FlieReader和FileWriter、FileInputStream和FileOutputStream。还有非流式部分辅助流式部分的类File类、RandomAccessFile类和FileDescriptor等。
3.1 转换流
转换流用于在字节流和字符流之间的转换。在IO包中实际上只有字节流,字符流是在字节流的基础上转换出来的,JDK提供了两种转换流InputStreamReader和OutputStreamWriter。
3.1.1 InputStreamReader
java.io.InputStreamReader
是字节流通向字符流的桥梁,它使用指定的
InputStreamReader有四个构造方法,需要传入InputStream对象以及指定的charset(或者使用默认字符集)。其相关方法都是通过私有常量StreamDecoder对象调用对应的方法实现。
private final StreamDecoder sd; public String getEncoding() {return sd.getEncoding();} 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); } public boolean ready() throws IOException { return sd.ready(); } public void close() throws IOException { sd.close(); }
而sun.nio.cs.StreamDecoder
也是Reader的子类。它涉及五个对象java.nio.charset.Charset
, java.nio.charset.CharsetDecoder
, java.nio.ByteBuffer
, java.io.InputStream
, java.nio.channels.ReadableByteChannel
。在其字节流相关的构造函数中,指定了字符集、解码器和输入源InputStream以及缓冲区的大小。
private Charset cs;//字符集,16 位的 Unicode 代码单元序列和字节序列之间的指定映射关系 private CharsetDecoder decoder;//解码器,能够把特定 charset 中的字节序列转换成 16 位 Unicode 字符序列的引擎。 private ByteBuffer bb;//字节缓冲区。 // 两者是两种输入方式的输入源,必须有一个对象是非空 private InputStream in;//字节输入流 private ReadableByteChannel ch;//可读取字节的通道。 //字节流输入相关的构造方法 StreamDecoder(InputStream in, Object lock, CharsetDecoder dec) { super(lock); this.cs = dec.charset(); this.decoder = dec; // 这个if已经常为false,因为这个FileInputStream相关方法暂时并不会加快读取速度 if (false && in instanceof FileInputStream) { ch = getChannel((FileInputStream)in); if (ch != null) bb = ByteBuffer.allocateDirect(DEFAULT_BYTE_BUFFER_SIZE); } if (ch == null) { this.in = in; this.ch = null; bb = ByteBuffer.allocate(DEFAULT_BYTE_BUFFER_SIZE);//分配缓冲区大小8192 } bb.flip(); // 清空字符缓冲对象 }
StreamDecoder实现了read(char cbuf[], int offset, int length)
方法和read()
方法,其具体实现是参考了
3.1.2 OutputStreamWriter
java.io.OutputStreamWriter
是字符流通向字节流的桥梁,它使用指定的
OutputStreamWriter也有四个构造方法,需要传入OutputStream对象以及指定的charset(或者使用默认字符集)。其相关方法都是通过私有常量StreamEncoder 对象调用对应的方法实现。
而sun.nio.cs.StreamEncoder
也是Writer的子类。它涉及五个对象java.nio.charset.Charset
, java.nio.charset.CharsetEncoder
, java.nio.ByteBuffer
, java.io.OutputStream
, java.nio.channels.WritableByteChannel
。在其字节流相关的构造函数中,指定了字符集、解码器和字节输出流以及缓冲区的大小。
private Charset cs; private CharsetEncoder encoder; private ByteBuffer bb; // Exactly one of these is non-null private final OutputStream out; private WritableByteChannel ch; //字节流相关构造函数 private StreamEncoder(OutputStream out, Object lock, CharsetEncoder enc) { super(lock); this.out = out; this.ch = null; this.cs = enc.charset(); this.encoder = enc; // This path disabled until direct buffers are faster if (false && out instanceof FileOutputStream) { ch = ((FileOutputStream)out).getChannel(); if (ch != null) bb = ByteBuffer.allocateDirect(DEFAULT_BYTE_BUFFER_SIZE); } if (ch == null) { bb = ByteBuffer.allocate(DEFAULT_BYTE_BUFFER_SIZE);//8192 } }
在StreamEncoder的三个write方法中,
-
其他两个方法最终都是调用
write(char cbuf[], int off, int len)
方法 -
而
write(char cbuf[], int off, int len)
方法调用implWrite(cbuf, off, len)
-
implWrite
方法则是调用writeBytes()
实现的 -
writeBytes()
最终调用指定字节输出流的write方法
//implWrite方法 void implWrite(char cbuf[], int off, int len) throws IOException { CharBuffer cb = CharBuffer.wrap(cbuf, off, len);//获得写入目标数组的字符缓冲区对象 if (haveLeftoverChar) flushLeftoverChar(cb, false); while (cb.hasRemaining()) {//判断字符缓冲去是否还有元素 CoderResult cr = encoder.encode(cb, bb, false); if (cr.isUnderflow()) { assert (cb.remaining() <= 1) : cb.remaining(); if (cb.remaining() == 1) {//remaining()方法则会返回缓冲区中剩余的元素数 haveLeftoverChar = true; leftoverChar = cb.get(); } break; } if (cr.isOverflow()) { assert bb.position() > 0; writeBytes(); continue; } cr.throwException(); } } //writeBytes方法 private void writeBytes() throws IOException { bb.flip(); int lim = bb.limit(); int pos = bb.position(); assert (pos <= lim); int rem = (pos <= lim ? lim - pos : 0); if (rem > 0) { if (ch != null) { if (ch.write(bb) != rem) assert false : rem; } else { out.write(bb.array(), bb.arrayOffset() + pos, rem);//最终调用指定字节输出流的write方法 } } bb.clear(); }
3.2 字节文件流FileInputStream和FileOutputStream
FileInputStream从文件系统中的某个文件(可以是Flie或者FileDescriptor)获得输入字节流,FileOutputStream是用于将数据写入File或者FileDescriptor的输出流。文件是否可用或者能否可以创建取决于基础平台。
无论是FileInputStream还是FileOutputStream都是用于读取或写入诸如图像数据之类的原始字节的流。若是要操作字符流,应当考虑使用FileReader和FileWriter。
java.io.FileInputStream
是InputStream的子类,提供了三个构造方法,可以通过文件名,文件(File),已有的文件连接(FileDescriptor)来打开一个到实际文件的连接。在InputStream中重写了read(),skip()、available()等方法,不过这些方法都是通过调用本地方法来实现的。
java.io.FileOutputStream
是OutputStream的子类,提供了五个构造方法,除了提供文件名,文件(File),已有的文件连接(FileDescriptor)三种打开到实际文件连接的方法,还添加了布尔变量append用以区别是否使用文件的追加模式。
3.3 字符文件流FileReader和FileWriter
FileReader和FileWriter是用来读取或者写入字符文件的便携类。此两类的构造方法假定默认字符编码和默认字节缓冲区大小都是可接受的。要自己指定这些值,可以在其上构造转换流类InputStreamReader和OutputStreamWriter。
文件是否可用或是否可以被创建取决于底层平台。字符文件流用于操作字符流,要操作原始字节流,考虑使用字节文件流FileInputStream和FileOutputStream。
java.io.FileReader
继承转换流InputStreamReader,其三个构造方法对应FileInputStream的三个构造方法,通过传入生成的FileInputStream对象进行字节流和字符流的转换来实现,并没有其他实现。
public class FileReader extends InputStreamReader { 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)); } }
java.io.FileWriter
继承转换流OutputStream,其五个构造方法也与FileOutputStream一一对应。
public class FileWriter extends OutputStreamWriter { 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)); } }
3.4 File类
java.io.File
是文件和目录路径名的抽象表示形式。用户界面和操作系统使用与系统相关的路径名字字符串来命名文件和目录,此类呈现分层路径名一个抽象的、与系统无关的视图。无论是抽象路径名还是路径名字符串,都可以是绝对路径或相对路径,默认情况下,java.io包中的类总是根据当前用户目录来解析相对路径名,此目录由系统属性user.dir指定,通常是java虚拟机的调用目录。
此类的实例可能表示(也可能不表示)实际文件系统对象,如文件或目录。
文件系统可以实现对实际文件系统对象上的某些操作(如读、写、执行)进行限制,这些限制统称为访问权限。对象上的访问权限可能导致File类的某些方法执行失败。
File类的实例是不可变的,即一旦创建,File对象表示的抽象路径名将永不改变。
private final String path;
File类的构造函数只是创建一个File实例,并没有以文件做读取等操作,因此即使路径是错误的,也可以创建实例不报错。构造函数包含四种。其构造方法最终都是对抽象路径名进行初始化。其中的
private static final FileSystem fs = DefaultFileSystem.getFileSystem();//平台的本地文件系统的抽象 private final String path; //抽象路径名 private final transient int prefixLength;//抽象路径的前缀长度 public File(String pathname) { if (pathname == null) { throw new NullPointerException(); } this.path = fs.normalize(pathname);// this.prefixLength = fs.prefixLength(this.path);// }
File类主要的方法比较多,但是并不包含文件读取和写入、执行:
-
一些文件、目录操作(创建或者删除)
-
boolean createNewFile()
,当且仅当不存在具有此抽象路径名指定名称的文件时,不可分地创建一个新的空文件。 -
static File createTempFile(String prefix, String suffix)
,在默认临时文件目录中创建一个空文件,使用给定前缀和后缀生成其名称。 -
static File createTempFile(String prefix, String suffix, File directory)
,在指定目录中创建一个新的空文件,使用给定的前缀和后缀字符串生成其名称。 -
boolean delete()
,删除此抽象路径名表示的文件或目录。 -
void deleteOnExit()
,在虚拟机终止时,请求删除此抽象路径名表示的文件或目录。 -
boolean mkdir()
,创建此抽象路径名指定的目录。 -
boolean mkdirs()
,创建此抽象路径名指定的目录,包括所有必需但不存在的父目录。 -
boolean renameTo(File dest)
,重新命名此抽象路径名表示的文件。
-
-
一些有关抽象路径的设置(读写权限)
-
boolean setExecutable(boolean executable)
,设置此抽象路径名所有者执行权限的一个便捷方法。 -
boolean setExecutable(boolean executable, boolean ownerOnly)
,设置此抽象路径名的所有者或所有用户的执行权限。 -
boolean setLastModified(long time)
,设置此抽象路径名指定的文件或目录的最后一次修改时间。 -
boolean setReadable(boolean readable)
,设置此抽象路径名所有者读权限的一个便捷方法。 -
boolean setReadable(boolean readable, boolean ownerOnly)
,设置此抽象路径名的所有者或所有用户的读权限。 -
boolean setReadOnly()
,标记此抽象路径名指定的文件或目录,从而只能对其进行读操作。 -
boolean setWritable(boolean writable)
,设置此抽象路径名所有者写权限的一个便捷方法。 -
boolean setWritable(boolean writable, boolean ownerOnly)
,设置此抽象路径名的所有者或所有用户的写权限。
-
-
一些有关文件状态的判断(测试文件是否可读可写可执行,抽象路径是否存在、是否是绝对路径、是否是文件、是否目录、是否隐藏)
-
boolean canExecute()
,测试应用程序是否可以执行此抽象路径名表示的文件。 -
boolean canRead()
,测试应用程序是否可以读取此抽象路径名表示的文件。 -
boolean canWrite()
,测试应用程序是否可以修改此抽象路径名表示的文件。 -
boolean exists()
,测试此抽象路径名表示的文件或目录是否存在。 -
boolean isAbsolute()
,测试此抽象路径名是否为绝对路径名。 -
boolean isDirectory()
,测试此抽象路径名表示的文件是否是一个目录。 -
boolean isFile()
,测试此抽象路径名表示的文件是否是一个标准文件。 -
boolean isHidden()
,测试此抽象路径名指定的文件是否是一个隐藏文件。
-
-
获取文件、目录信息
-
File getAbsoluteFile()
,返回此抽象路径名的绝对路径名形式。 -
String getAbsolutePath()
,返回此抽象路径名的绝对路径名字符串。 -
File getCanonicalFile()
,返回此抽象路径名的规范形式。 -
String getCanonicalPath()
,返回此抽象路径名的规范路径名字符串。 -
long getFreeSpace()
,返回此抽象路径名指定的分区中未分配的字节数。 -
String getName()
,返回由此抽象路径名表示的文件或目录的名称。 -
String getParent()
,返回此抽象路径名父目录的路径名字符串;如果此路径名没有指定父目录,则返回 null。 -
File getParentFile()
,返回此抽象路径名父目录的抽象路径名;如果此路径名没有指定父目录,则返回 null。 -
String getPath()
,将此抽象路径名转换为一个路径名字符串。 -
long getTotalSpace()
,返回此抽象路径名指定的分区大小。 -
long getUsableSpace()
,返回此抽象路径名指定的分区上可用于此虚拟机的字节数。 -
long lastModified()
,返回此抽象路径名表示的文件最后一次被修改的时间。 -
long length()
,返回由此抽象路径名表示的文件的长度。 -
String[] list()
,返回一个字符串数组,这些字符串指定此抽象路径名表示的目录中的文件和目录。 -
String[] list(FilenameFilter filter)
,返回一个字符串数组,这些字符串指定此抽象路径名表示的目录中满足指定过滤器的文件和目录。 -
File[] listFiles()
,返回一个抽象路径名数组,这些路径名表示此抽象路径名表示的目录中的文件。 -
File[] listFiles(FileFilter filter)
,返回抽象路径名数组,这些路径名表示此抽象路径名表示的目录中满足指定过滤器的文件和目录。 -
File[] listFiles(FilenameFilter filter)
,返回抽象路径名数组,这些路径名表示此抽象路径名表示的目录中满足指定过滤器的文件和目录。 -
static File[] listRoots()
,列出可用的文件系统根。 -
String toString()
,返回此抽象路径名的路径名字符串。
-
-
其他
-
int compareTo(File pathname)
,按字母顺序比较两个抽象路径名。 -
boolean equals(Object obj)
,测试此抽象路径名与给定对象是否相等。 -
int hashCode()
,计算此抽象路径名的哈希码。 -
URI toURI()
,构造一个表示此抽象路径名的 file: URI。
-
3.5 FileDescriptor类
java.io.FileDescriptor
与字节文件流FileInputstream以及FileOutputStream相关,字节文件流实例的构造过程最终会对一个私有常量FileDescriptor实例进行初始化。
FileInputstream是一个final修饰的类,而在api中是这样子描述该类的:文件描述符类的实例用作与基础机器有关的某种不透明句柄。该结构表示开放文件、开放套接字或者字节的另一个源或者接收者。文件描述符的主要实际用途是创建一个包含该结构的FileInputStream或FileOutputStream。应用程序不应创建自己的文件描述符。
如果单单看以上语句似乎有些云里雾里,参考
内核(kernel)利用文件描述符(file descriptor)来访问文件。文件描述符是非负整数。打开现存文件或新建文件时,内核会返回一个文件描述符。读写文件也需要使用文件描述符来指定待读写的文件。
文件描述符在形式上是一个非负整数。实际上,它是一个索引值,指向内核为每一个进程所维度的该进程打开文件的记录表。当程序打开一个现有文件或者创建一个新文件时,内核向进程返回一个文件描述符。在程序设计中,一些设计底层的程序编写往往会围绕着文件描述符展开,但是文件描述符这一概念往往只适用于UNIX、Linux这样的操作系统。在Windows操作系统上,文件描述符被称为文件句柄。
FileDescriptor
一个打开的文件通过唯一的描述符进行引用,该描述符是打开文件的元数据到文件本身的映射。在Linux内核中,这个描述符被称为文件描述符,文件描述符用一个整数表示(C语言中的类型为int),简写为fd。
文件描述符在用户空间(相对于内核空间而言,也就是我们应用程序的那层)中共享,允许用户程序用文件描述符直接访问文件。
同一个文件能被不同或者相同的进程多次打开,每一个打开文件的实例(譬如java中的File实例、FileInputStream实例)都产生一个唯一的文件描述符。同一个描述符可以被多个进程使用。不同进程能同时对一个文件进行读写,所以存在并发修改问题。
而有关于java.io.FileDescriptor
的实现,以下源码及注释引自:
// jdk7 package java.io; import java.util.concurrent.atomic.AtomicInteger; /** * 文件描述符类的实例用作与基础机器有关的某种结构的不透明句柄,该结构表示开放文件、开放套接字或者字节的另一个源或接收者。 * 文件描述符的主要实际用途是创建一个包含该结构的 FileInputStream 或 FileOutputStream。 * * 应用程序不应创建自己的文件描述符。 * @since JDK1.0 */ public final class FileDescriptor { private int fd; private long handle; /** * 用于跟踪使用此FileDescriptor的FIS / FOS / RAF实例的使用计数器。 * 如果FileDescriptor仍被任何流使用,FIS / FOS.finalize()将不会释放。 */ private AtomicInteger useCount; /** * 构造一个(无效)FileDescriptor对象。 * Constructs an (invalid) FileDescriptor object. */ public /**/ FileDescriptor() { fd = -1; handle = -1; useCount = new AtomicInteger(); } static { // 例行初始化该类的JNI字段偏移量 initIDs(); }a // Set up JavaIOFileDescriptorAccess in SharedSecrets // 在SharedSecrets中设置JavaIOFileDescriptorAccess static { sun.misc.SharedSecrets.setJavaIOFileDescriptorAccess( new sun.misc.JavaIOFileDescriptorAccess() { public void set(FileDescriptor obj, int fd) { obj.fd = fd; } public int get(FileDescriptor obj) { return obj.fd; } public void setHandle(FileDescriptor obj, long handle) { obj.handle = handle; } public long getHandle(FileDescriptor obj) { return obj.handle; } } ); } /** * 标准输入流的句柄。 * 通常,该文件描述符不直接使用,而是通过称为System.in的输入流。 * @see java.lang.System#in */ public static final FileDescriptor in = standardStream(0); /** * 标准输出流的句柄。 * 通常,此文件描述符不是直接使用,而是通过称为System.out的输出流。 * @see java.lang.System#out */ public static final FileDescriptor out = standardStream(1); /** * 标准错误流的句柄。 * 通常,该文件描述符不直接使用,而是通过称为System.err的输出流。 * @see java.lang.System#err */ public static final FileDescriptor err = standardStream(2); /** * 测试此文件描述符对象是否有效。 */ public boolean valid() { return ((handle != -1) || (fd != -1)); } /** * 强制所有系统缓冲区与基础设备同步 * @since JDK1.1 */ public native void sync() throws SyncFailedException; // 例行初始化该类的JNI字段偏移量 private static native void initIDs(); private static native long set(int d); private static FileDescriptor standardStream(int fd) { FileDescriptor desc = new FileDescriptor(); desc.handle = set(fd); return desc; } // FIS,FOS和RAF使用的私有方法。 // package private methods used by FIS, FOS and RAF. int incrementAndGetUseCount() { return useCount.incrementAndGet(); } int decrementAndGetUseCount() { return useCount.decrementAndGet(); } }
3.6 RandomAccessFile类
java.io.RandomAccessFile
类既可以读取文件内容,也可以向文件输出数据,同时RandomAccessFile支持“随机访问”的方式,程序可以直接跳转到文件的任意地方来读写数据。RandomAccessFile只能读写文件不能读写其他IO节点,其主要应用场景包含:
-
访问文件的部分内容而不是把文件从头读到尾。
-
程序需要向已存在的文件后追加内容
-
RandomAccessFile的一个重要使用场景就是网络请求中的多线程下载及断点续传。
3.6.1 RandomAccessFile的构造方法
RandomAccessFile·有两个构造方法,可通过直接传入File对象或者传入文件名来进行构建。参数model用来指定RandomAccessFile的访问模式,一共有4种模式:
-
'r'
:以只读方式打开。调用结果对象的任何write方法都将导致抛出IOException。 -
'rw'
:打开以便读取和写入 -
'rws'
:打开以便读取和写入。相比于'rw'
,'rws'
还要求对“文件的内容”或“元数据”的每个更新都同步写入到基础存储设备。 -
'rwd'
:打开以便读取和写入。相比于'rw'
,'rwd'
还要求对“文件的内容”的每个更新都同步写入到基础存储设备。
3.6.2 RandomAccessFile的重要方法
RandomAccessFile既可以读文件,也可以写文件,所以具备了许多类型的read()方法和write()方法。其中比较特殊的是readline()方法。
String readline()
用来从文件中读取下一行的文本。在其源码中,通过判断换行符'\r'和'\n'来实现。
public final String readLine() throws IOException { StringBuffer input = new StringBuffer(); int c = -1; boolean eol = false; while (!eol) { switch (c = read()) { case -1: case '\n': eol = true; break; case '\r': eol = true; long cur = getFilePointer(); if ((read()) != '\n') { seek(cur); } break; default: input.append((char)c); break; } } if ((c == -1) && (input.length() == 0)) { return null; } return input.toString(); }
除了读写操作,RandomAccessFile还支持随机访问。RandomAccessFile对象包含了一个记录指针,用以标识当前读写处的位置,当程序新创建一个RandomAccessFile对象时,该对象的文件指针记录位于文件头(也就是0处),当读/写了n个字节后,文件记录指针将会后移n个字节。除此之外,RandomAccessFile还可以自由移动该记录指针。下面就是RandomAccessFile具有的两个特殊方法:
-
long getFilePointer()
:返回文件记录指针的当前位置。 -
void seek(long pos)
:将文件指针定位到pos位置。
3.7 文件访问总结
在IO包中实际上只有字节流,字符流是在字节流的基础上转换出来的,转换流InputStreamReader (OutputStreamWriter)是字节流和字符流之间的桥梁,字符文件流FileReader (FileWriter)通过继承InputStreamReader (OutputStreamWriter)将字节文件流FileInputStream (OutputStream)转换成字符流。
用户界面和操作系统使用与系统相关的路径名字字符串(绝对路径或相对路径)来命名文件和目录,而File类是文件和目录路径名的抽象表示形式,在构造函数中File类会初始化一个私有常量path,一旦File实例创建,该抽象路径不可更改(该抽象路径可能指向实际目录或文件,可能不)。
而在操作文件的输入输出流中会维护一个常量FileDescriptor(文件描述符)实例,因为内核(kernel)是利用文件描述符(file descriptor)来访问文件的。
而RandomAccessFile类是一个特殊的类,它既可以读取文件也可以写文件,也可以进行随机访问,它通过构造函数中的mode来指定文件访问模式,通过文件指针来进行随机访问。