Java IO源码分析 - Reader,Writer系列(一)

说明

整个系列的文章全部参考或直接照搬下面两位作者的文章,这里只是根据自己需要对原作者的文章梳理的总结,仅给自己日后复习时提供思路,如有读者看到学习时建议移步原作。再次重申并非我所写

另两篇本人总结的IO系列

HikariCP:Java I/O源码分析 - InputStream,OutputStream系列
HikariCP:Java IO源码分析 - Reader,Writer系列(二)

File

File 是“文件”和“目录路径名”的抽象表示形式。

File 直接继承于Object,实现了Serializable接口和Comparable接口。实现Serializable接口,意味着File对象支持序列化操作。而实现Comparable接口,意味着File对象之间可以比较大小;File能直接被存储在有序集合(如TreeSet、TreeMap中)。

File类源码分析,在分析File类源码的时候很多地方引用到了抽象类FileSystem的函数,而File类默认FileSystem的默认实例是WinNTFileSystem类。即Windows平台的文件系统对象,应该是我们下载时Java官方根据我们请求的主机来反回了不同jdk。

由于扯到了WinNTFileSystem这个类,所以对于File类的函数,只要知道结果和File层异常抛出情况即可。

public class File implements Serializable, Comparable<File> {
    
     // 代表所在平台的文件系统对象
     private static final FileSystem fs = DefaultFileSystem.getFileSystem();
     // 文件路径名
     private final String path;
     // 内联枚举类,地址是否合法
     private static enum PathStatus { INVALID, CHECKED };
     // 指示文件路径是否无效的标志
     private transient PathStatus status = null;
     // 路径前缀的长度
     private final transient int prefixLength;
     // 分隔符,分隔同一个路径字符串中的目录的
     // UNIX systems is '/' | Microsoft is '\\'
     public static final char separatorChar = fs.getSeparator();
     // 分隔符
     public static final String separator = "" + separatorChar;
     // 路径分割符,分隔连续多个路径字符串的分隔符。
     // UNIX systems this character is ':' | on Microsoft Windows systems it is ';'
     public static final char pathSeparatorChar = fs.getPathSeparator();
     // 路径分割符
     public static final String pathSeparator = "" + pathSeparatorChar;
     
     //根据 parent 父路径和 child 子路径名字符串创建一个新 File 实例
     public File(File parent, String child) {// this.path,this.prefixLength
     }
     
     //通过将给定路径字符串转换为抽象路径名来创建一个新 File 实例。
     public File(String pathname) {
     }
     
     // 根据 parent 父路径名字符串和 child 子路径名字符串创建一个新 File 实例。
     public File(String parent, String child) {
     }
     
     // 通过将给定的 file: URI 转换为一个抽象路径名来创建一个新的 File 实例。
     public File(URI uri) {
     }
}

看完File类的成员变量后,我们大致了解了File类有哪些信息供直接表示,以及File类的所有构造函数并没有真正的构造File对象,是先初始化了path,prefixLength两个成员变量。

File类函数列表即释义

// 成员函数
boolean    canExecute()    // 测试应用程序是否可以执行此抽象路径名表示的文件。
boolean    canRead()       // 测试应用程序是否可以读取此抽象路径名表示的文件。
boolean    canWrite()      // 测试应用程序是否可以修改此抽象路径名表示的文件。
int    compareTo(File pathname)    // 按字母顺序比较两个抽象路径名。
boolean    createNewFile()         // 当且仅当不存在具有此抽象路径名指定名称的文件时,不可分地创建一个新的空文件。
static File    createTempFile(String prefix, String suffix)    // 在默认临时文件目录中创建一个空文件,使用给定前缀和后缀生成其名称。
static File    createTempFile(String prefix, String suffix, File directory)    // 在指定目录中创建一个新的空文件,使用给定的前缀和后缀字符串生成其名称。
boolean    delete()             // 删除此抽象路径名表示的文件或目录。
void    deleteOnExit()       // 在虚拟机终止时,请求删除此抽象路径名表示的文件或目录。
boolean    equals(Object obj)   // 测试此抽象路径名与给定对象是否相等。
boolean    exists()             // 测试此抽象路径名表示的文件或目录是否存在。
File    getAbsoluteFile()    // 返回此抽象路径名的绝对路径名形式。
String    getAbsolutePath()    // 返回此抽象路径名的绝对路径名字符串。
File    getCanonicalFile()   // 返回此抽象路径名的规范形式。
String    getCanonicalPath()   // 返回此抽象路径名的规范路径名字符串。
long    getFreeSpace()       // 返回此抽象路径名指定的分区中未分配的字节数。
String    getName()            // 返回由此抽象路径名表示的文件或目录的名称。
String    getParent()          // 返回此抽象路径名父目录的路径名字符串;如果此路径名没有指定父目录,则返回 null。
File    getParentFile()      // 返回此抽象路径名父目录的抽象路径名;如果此路径名没有指定父目录,则返回 null。
String    getPath()            // 将此抽象路径名转换为一个路径名字符串。
long    getTotalSpace()      // 返回此抽象路径名指定的分区大小。
long    getUsableSpace()     // 返回此抽象路径名指定的分区上可用于此虚拟机的字节数。
int    hashCode()               // 计算此抽象路径名的哈希码。
boolean    isAbsolute()         // 测试此抽象路径名是否为绝对路径名。
boolean    isDirectory()        // 测试此抽象路径名表示的文件是否是一个目录。
boolean    isFile()             // 测试此抽象路径名表示的文件是否是一个标准文件。
boolean    isHidden()           // 测试此抽象路径名指定的文件是否是一个隐藏文件。
long    lastModified()       // 返回此抽象路径名表示的文件最后一次被修改的时间。
long    length()             // 返回由此抽象路径名表示的文件的长度。
String[]    list()           // 返回一个字符串数组,这些字符串指定此抽象路径名表示的目录中的文件和目录。
String[]    list(FilenameFilter filter)    // 返回一个字符串数组,这些字符串指定此抽象路径名表示的目录中满足指定过滤器的文件和目录。
File[]    listFiles()                        // 返回一个抽象路径名数组,这些路径名表示此抽象路径名表示的目录中的文件。
File[]    listFiles(FileFilter filter)       // 返回抽象路径名数组,这些路径名表示此抽象路径名表示的目录中满足指定过滤器的文件和目录。
File[]    listFiles(FilenameFilter filter)   // 返回抽象路径名数组,这些路径名表示此抽象路径名表示的目录中满足指定过滤器的文件和目录。
static File[]    listRoots()    // 列出可用的文件系统根。
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)    // 设置此抽象路径名的所有者或所有用户的写权限。
String    toString()    // 返回此抽象路径名的路径名字符串。
URI    toURI()    // 构造一个表示此抽象路径名的 file: URI。
URL    toURL()    // 已过时。 此方法不会自动转义 URL 中的非法字符。建议新的代码使用以下方式将抽象路径名转换为 URL:首先通过 toURI 方法将其转换为 URI,然后通过 URI.toURL 方法将 URI 装换为 URL。

File类API典型应用

当前目录的子目录下,再新建一个目录

例如,我们想要在当前目录的子目录“dir”下,再新建一个子目录。有一下几种方法:

方法1

File sub1 = new File("dir", "sub1");
sub1.mkdir();

说明:上面的方法作用是,在当前目录下 "dir/sub1"。它能正常运行的前提是“sub1”的父目录“dir”已经存在!

方法2

File sub2 = new File(dir, "sub2");
sub2.mkdir();

说明:上面的方法作用是,在当前目录下 "dir/sub2"。它能正常运行的前提是“sub2”的父目录“dir”已经存在!

方法3

File sub3 = new File("dir/sub3");
sub3.mkdirs();

说明:上面的方法作用是,在当前目录下 "dir/sub3"。它不需要dir已经存在,也能正常运行;若“sub3”的父母路不存在,mkdirs()方法会自动创建父目录。

新建文件的几种常用方法

例如,我们想要在当前目录的子目录“dir”下,新建一个文件。有一下几种方法

try {
    File dir = new File("dir");    // 获取目录“dir”对应的File对象
    File file1 = new File(dir, "file1.txt");
    file1.createNewFile();
} catch (IOException e) {
    e.printStackTrace();
}

说明:上面代码作用是,在“dir”目录(相对路径)下新建文件“file1.txt”。

方法2

try {
    File file2 = new File("dir", "file2.txt");
    file2.createNewFile();
} catch (IOException e) {
    e.printStackTrace();
}

说明:上面代码作用是,在“dir”目录(相对路径)下新建文件“file2.txt”。

方法3

try {
    File file3 = new File("D:/dir/file4.txt");
    file3.createNewFile();
} catch (IOException e) {
    e.printStackTrace();
}

上面代码作用是,下新建文件“D:/dir/file4.txt”(绝对路径)。

FileDescriptor

  • FileDescriptor 是“文件描述符”。
  • FileDescriptor 可以被用来表示开放文件、开放套接字等。
  • 以FileDescriptor表示文件来说:当FileDescriptor表示某文件时,我们可以通俗的将FileDescriptor看成是该文件。但是,我们不能直接通过FileDescriptor对该文件进行操作;若需要通过FileDescriptor对该文件进行操作,则需要新创建FileDescriptor对应的FileOutputStream,再对文件进行操作。

in, out, err介绍

(01) in  -- 标准输入(键盘)的描述符
(02) out -- 标准输出(屏幕)的描述符
(03) err -- 标准错误输出(屏幕)的描述符

它们3个的原理和用法都类似。

out是标准输出(屏幕)的描述符。但是它有什么作用呢?
我们可以通俗理解,out就代表了标准输出(屏幕)。若我们要输出信息到屏幕上,即可通过out来进行操作;但是,out又没有提供输出信息到屏幕的接口(因为out本质是FileDescriptor对象,而FileDescriptor没有输出接口)。怎么办呢?
很简单,我们创建out对应的“输出流对象”,然后通过“输出流”的write()等输出接口就可以将信息输出到屏幕上。如下代码:

try {
    FileOutputStream out = new FileOutputStream(FileDescriptor.out);
    out.write('A');
    out.close();
} catch (IOException e) {
}

执行上面的程序,会在屏幕上输出字母'A'。

为了方便我们操作,java早已为我们封装好了“能方便的在屏幕上输出信息的接口”:通过System.out,我们能方便的输出信息到屏幕上。
因此,我们可以等价的将上面的程序转换为如下代码:
System.out.print('A');

public static final FileDescriptor out = standardStream(1);
 
private static FileDescriptor standardStream(int fd) {
    FileDescriptor desc = new FileDescriptor();
    desc.handle = set(fd);
    return desc;
}

/**
 * 构造一个无效的FileDescriptor对象
 */
public /**/ FileDescriptor() {
    fd = -1;
    handle = -1;
}

由于standardStream函数中调用的set函数是JNI,所以我们只能推测默认构造函数生成的FileDescriptor对象是无效的FileDescriptor对象。由此可知要想生成一个有效的FileDescriptor对象只能选择其内部声明好的3个静态域对象。至此我们大致也能猜出set函数应该是对fd变量赋值,并将返回值赋予handle,执行成功那么该返回的对象desc就不再是一个无效的FileDescriptor对象了。

fd对象是非常重要的一个变量,“fd=1”就代表了“标准输出”,“fd=0”就代表了“标准输入”,“fd=2”就代表了“标准错误输出”。

FileOutputStream out = new FileOutputStream(FileDescriptor.out); 就是利用构造函数FileOutputStream(FileDescriptor fdObj)来创建“Filed.out对应的FileOutputStream对象”。

通过上面的学习,我们知道,我们可以自定义标准的文件描述符[即,in(标准输入),out(标准输出),err(标准错误输出)]的流,从而完成输入/输出功能;但是,java已经为我们封装好了相应的接口,即我们可以更方便的System.in, System.out, System.err去使用它们。

FileInputStream,FileOutputStream

FileInputStream是文件输入流,用于从文件系统中的某个文件中获得输入字节。FileInputStream用于读取诸如图像数据之类的原始字节流。要读取字符流,请考虑使用FileReader

FileOutputStream是文件输出流,用于将数据写入FileFileDescriptor的输出流。FileOutputStream用于写入诸如图像数据之类的原始字节的流。要写入字符流,请考虑使用FileWriter

FileInputStream

public class FileInputStream extends InputStream {

    //打开的文件的文件描述符,负责handle文件
    private final FileDescriptor fd;
    //引用文件的路径,null if the stream is created with a file descriptor
    private final String path;
    //文件通道
    private FileChannel channel = null;
    //关闭锁
    private final Object closeLock = new Object();
    //标识流是否关闭
    private volatile boolean closed = false;
    
    // 根据传入的文件名来创建File对象
    public FileInputStream(String name) throws FileNotFoundException {
        this(name != null ? new File(name) : null);
    }
    
    public FileInputStream(File file) throws FileNotFoundException {
        //获取文件名
        String name = (file != null ? file.getPath() : null);
        //获取安全管理器
        SecurityManager security = System.getSecurityManager();
        if (security != null) {
            //如果调用线程没有访问指定文件的权限,抛出SecurityException
            security.checkRead(name);
        }
        if (name == null) {
            throw new NullPointerException();
        }
        if (file.isInvalid()) {
            throw new FileNotFoundException("Invalid file path");
        }
        //fd表示此文件连接
        fd = new FileDescriptor();
        fd.attach(this);
        path = name;
        //打开文件以便读取
        open(name);
    }

}

close,getChannel,getFD,finalize

// 关闭此文件输入流并释放与此流有关的所有系统资源。
// 如果此流有一个与之关联的通道,则关闭该通道
public void close() throws IOException {
    synchronized (closeLock) {
        if (closed) {
            return;
        }
        closed = true;
    }
    //如果此流有一个与之关联的通道,则关闭该通道。
    if (channel != null) {
       channel.close();
    }

    fd.closeAll(new Closeable() {
        public void close() throws IOException {
           close0();
       }
    });
}

/**
 * 返回与此文件输入流有关的唯一FileChannel对象。
 */
public FileChannel getChannel() {
    synchronized (this) {
        if (channel == null) {
            channel = FileChannelImpl.open(fd, path, true, false, this);
        }
        return channel;
    }
}

/**
 * 返回表示到文件系统中实际文件的连接的FileDescriptor对象
 */
public final FileDescriptor getFD() throws IOException {
    if (fd != null) {
        return fd;
    }
    throw new IOException();
}

/**
 * 确保在不再引用文件输入流时调用其close方法。
 */
protected void finalize() throws IOException {
    if ((fd != null) &&  (fd != FileDescriptor.in)) {
        close();
    }
}

FileOutputStream

public class FileOutputStream extends OutputStream {
    /**
     * 文件描述符
     */
    private final FileDescriptor fd;
    /**
     * 标识添加还是替换文件的内容
     */
    private final boolean append;
    /**
     * 关联的通道
     * 懒加载
     */
    private FileChannel channel;
    //引用文件的路径
    private final String path;
    //锁
    private final Object closeLock = new Object();
    //标识流是否关闭
    private volatile boolean closed = false;
    
    // 创建一个向具有指定name的文件中写入数据的输出文件流。
    // 如果第二个参数为append为true,则将字节写入文件末尾处,而不是写入文件开始处。
    public FileOutputStream(String name, boolean append) throws FileNotFoundException {
        this(name != null ? new File(name) : null, append);
    }
    
    // 创建一个向指定文件描述符处写入数据的输出文件流,该文件描述符表示一个到文件系统中的某个实际文件的现有连接。
    public FileOutputStream(FileDescriptor fdObj) {
        SecurityManager security = System.getSecurityManager();
        if (fdObj == null) {
            throw new NullPointerException();
        }
        if (security != null) {
            security.checkWrite(fdObj);
        }
        this.fd = fdObj;
        this.append = false;
        this.path = null;

        fd.attach(this);
    }
}

write

// 将指定字节写入此文件输出流。
// append 控制是写到文件尾还是头
private native void write(int b, boolean append) throws IOException;

可以看出FileOutputStream和FileInputStream的相同功能函数处理逻辑一致。

总结

  • FileInputStream是文件输入流,用于从文件系统中的某个文件中获得输入字节。FileInputStream用于读取诸如图像数据之类的原始字节流。要读取字符流,请考虑使用FileReader。
  • FileOutputStream是文件输出流,用于将数据写入File或FileDescriptor的输出流。FileOutputStream用于写入诸如图像数据之类的原始字节的流。要写入字符流,请考虑使用FileWriter。
  • Java语言本身不能对操作系统底层进行访问和操作,但是可以通过JNI接口调用其他语言来实现对底层的访问。
  • FileInputStream不支持mark方法与set方法。

ObjectInputStream,ObjectOutputStream

ObjectInputStream 和 ObjectOutputStream 的作用是,对基本数据和对象进行==序列化操作==支持。

创建“文件输出流”对应的ObjectOutputStream对象,该ObjectOutputStream对象能提供对“基本数据或对象”的持久存储;当我们需要读取这些存储的“基本数据或对象”时,可以创建“文件输入流”对应的ObjectInputStream,进而读取出这些“基本数据或对象”。

==注意==: 只有支持 java.io.Serializable 或 java.io.Externalizable 接口的对象才能被ObjectInputStream/ObjectOutputStream所操作!

public class ObjectOutputStream
    extends OutputStream implements ObjectOutput, ObjectStreamConstants {
    
}

public class ObjectInputStream
    extends InputStream implements ObjectInput, ObjectStreamConstants {
    
}

演示程序

/**
 * ObjectInputStream 和 ObjectOutputStream 测试程序
 *
 * 注意:通过ObjectInputStream, ObjectOutputStream操作的对象,必须是实现了Serializable或Externalizable序列化接口的类的实例。
 *
 * @author skywang
 */

import java.io.FileInputStream;   
import java.io.FileOutputStream;   
import java.io.ObjectInputStream;   
import java.io.ObjectOutputStream;   
import java.io.Serializable;   
import java.util.Map;
import java.util.HashMap;
import java.util.Iterator;
  
public class ObjectStreamTest { 
    private static final String TMP_FILE = "box.tmp";
  
    public static void main(String[] args) {   
        testWrite();
        testRead();
    }
  

    /**
     * ObjectOutputStream 测试函数
     */
    private static void testWrite() {   
        try {
            ObjectOutputStream out = new ObjectOutputStream(
                    new FileOutputStream(TMP_FILE));
            out.writeBoolean(true);
            out.writeByte((byte)65);
            out.writeChar('a');
            out.writeInt(20131015);
            out.writeFloat(3.14F);
            out.writeDouble(1.414D);
            // 写入HashMap对象
            HashMap map = new HashMap();
            map.put("one", "red");
            map.put("two", "green");
            map.put("three", "blue");
            out.writeObject(map);
            // 写入自定义的Box对象,Box实现了Serializable接口
            Box box = new Box("desk", 80, 48);
            out.writeObject(box);

            out.close();
        } catch (Exception ex) {
            ex.printStackTrace();
        }
    }
 
    /**
     * ObjectInputStream 测试函数
     */
    private static void testRead() {
        try {
            ObjectInputStream in = new ObjectInputStream(
                    new FileInputStream(TMP_FILE));
            System.out.printf("boolean:%b\n" , in.readBoolean());
            System.out.printf("byte:%d\n" , (in.readByte()&0xff));
            System.out.printf("char:%c\n" , in.readChar());
            System.out.printf("int:%d\n" , in.readInt());
            System.out.printf("float:%f\n" , in.readFloat());
            System.out.printf("double:%f\n" , in.readDouble());
            // 读取HashMap对象
            HashMap map = (HashMap) in.readObject();
            Iterator iter = map.entrySet().iterator();
            while (iter.hasNext()) {
                Map.Entry entry = (Map.Entry)iter.next();
                System.out.printf("%-6s -- %s\n" , entry.getKey(), entry.getValue());
            }
            // 读取Box对象,Box实现了Serializable接口
            Box box = (Box) in.readObject();
            System.out.println("box: " + box);

            in.close();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}


class Box implements Serializable {
    private int width;   
    private int height; 
    private String name;   

    public Box(String name, int width, int height) {
        this.name = name;
        this.width = width;
        this.height = height;
    }

    @Override
    public String toString() {
        return "["+name+": ("+width+", "+height+") ]";
    }
}

结果

boolean:true
byte:65
char:a
int:20131015
float:3.140000
double:1.414000
two    -- green
one    -- red
three  -- blue
box: [desk: (80, 48) ]

==再次提示==:通过ObjectInputStream, ObjectOutputStream操作的对象,必须是实现了Serializable或Externalizable序列化接口的类的实例。

字节输入流输出流分析结束

Reader,Writer

输入流的抽象类Reader,字符输出流的抽象类Writer。

Reader

Reader是字符输入流的抽象类。看一下类的结构可以知道,子类必须实现的方法只有read(char[], int, int)close()函数。但是,多数子类将重写此处定义的一些方法,以提供更高的效率和/或其他功能。

image

看一下类的签名可知其实现了Readable和Closeable接口。

public abstract class Reader implements Readable和, Closeable {

}

public interface Readable {

    public int read(java.nio.CharBuffer cb) throws IOException;
}

public interface Closeable extends AutoCloseable {
    public void close() throws IOException;

}

Reader类成员域

public abstract class Reader implements Readable, Closeable {
    //用于同步针对此流的操作的对象。
    protected Object lock;
     /** Maximum skip-buffer size */
    private static final int maxSkipBufferSize = 8192;
    /** Skip buffer, null until allocated */
    private char skipBuffer[] = null;
    
    /**
     * 构造函数之一
     * 创建一个新的字符流Reader,其重要部分将同步其自身。
     */
    protected Reader() {
        this.lock = this;
    }
    
    /**
     * 构造函数之一
     * 创建一个新的字符流reader,其重要部分将同步给定的对象lock
     */
    protected Reader(Object lock) {
        if (lock == null) {
            throw new NullPointerException();
        }
        this.lock = lock;
    }
}

read,skip

/**
 * 试图将字符读入指定的字符缓冲区。
 * 缓冲区可照原样用作字符的存储库:所做的唯一改变是put操作的结果。不对缓冲区执行翻转或重绕操作。
 * 将输入流管道中的target.length个字节读取到指定的缓存字符数组target中、返回实际存放到target中的字符数。 
 */
public int read(java.nio.CharBuffer target) throws IOException {
    //获取此缓冲区中的剩余空间大小
    int len = target.remaining();
    char[] cbuf = new char[len];
    //试图将输入流中的len个字符读入cbuf,返回实际读取的字节数n
    int n = read(cbuf, 0, len);
    //如果实际读到字符的个数大于0
    if (n > 0)
        //将实际读取到的n个字符存入缓冲区
        target.put(cbuf, 0, n);
    //返回实际读取的字符数
    return n;
}

/**
 * 从输入流中读取单个字符。
 */
public int read() throws IOException {
    char cb[] = new char[1];
    if (read(cb, 0, 1) == -1)
        return -1;
    else
        return cb[0];
}

/**
 * 跳过字符
 *
 * @param  n  要跳过的字符数
 * @return    实际跳过的字符数
 *
 */
public long skip(long n) throws IOException {
    //如果n为负数,抛出异常
    if (n < 0L)
        throw new IllegalArgumentException("skip value is negative");
    //不允许n大于maxSkipBufferSize
    int nn = (int) Math.min(n, maxSkipBufferSize);
    synchronized (lock) {
        //如果现有缓冲区为null或者大小小于nn,就新建一个大小为nn的缓冲区
        if ((skipBuffer == null) || (skipBuffer.length < nn))
            skipBuffer = new char[nn];
        long r = n;
        //循环跳过输入流中字符,一次尝试跳过nn个字符,直到到达输入流末尾或者n个字符被全部跳过
        while (r > 0) {
            int nc = read(skipBuffer, 0, (int)Math.min(r, nn));
            if (nc == -1)
                break;
            r -= nc;
        }
        //返回实际跳过的字符数
        return n - r;
    }
}

剩下的函数大部分都需要没有具体实现需要子类重写实现自己的处理逻辑。对于大部分操作Reader抽象类,默认是unsupported的

writer

Writer是字符输出流的抽象类。子类必须实现的方法仅有 write(char[], int, int)flush()close()。但是,多数子类将重写此处定义的一些方法,以提供更高的效率和/或其他功能。

image

从类的结构图可以看出,该类实现了Appendable接口的全部的3个抽象函数。并重写了父类Closeable,Flushable的全部的close,flush函数。但只是再次抽象声明,并没有提供默认实现。任然需要子类重写扩展

public abstract class Writer implements Appendable, Closeable, Flushable {
    /**
     * 字符缓存数组。
     * 用于临时存放要写入字符输出流中的字符
     */
    private char[] writeBuffer;
    /**
     * 字符缓存数组的默认大小。
     */
    private static final int WRITE_BUFFER_SIZE = 1024;
    /**
     * 用于同步此流的操作的对象。
     */
    protected Object lock;
    /**
     * 构造方法
     * 创建一个新的字符流writer,其关键部分将同步其自身。
     */
    protected Writer() {
        this.lock = this;
    }
    /**
     * 构造方法
     * 建一个新的字符流writer,其关键部分将同步给定的对象。
     */
    protected Writer(Object lock) {
        if (lock == null) {
            throw new NullPointerException();
        }
        this.lock = lock;
    }
}
/**
 * 写入单个字符。
 * 要写入的字符包含在给定整数值的16个低位中,16高位被忽略。
 */
public void write(int c) throws IOException {
    synchronized (lock) {
        if (writeBuffer == null){
            writeBuffer = new char[WRITE_BUFFER_SIZE];
        }
        //char为16位。所以要写入的字符包含在给定整数值的16个低位中,16高位被忽略。
        writeBuffer[0] = (char) c;
        write(writeBuffer, 0, 1);
    }
}

// 试图将字符串中从off开始的len个字符写入输出流中。
// 尽量写入len个字符,但写入的字节数可能少于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];
        }
        //将字符串中从off开始到off+len的字符复制到cbuf中
        str.getChars(off, (off + len), cbuf, 0);
        // 将该方法的暂存数组写入到输出流中。
        // 前面的一系列判断决定了是否使用Writer类给定的默认大小的字符串冲数组// 当大小超出则使用临时新定义的更大的缓冲数组
        write(cbuf, 0, len);
    }
}

/**
 * 添加字符序列的一部分
 */
public Writer append(CharSequence csq, int start, int end) throws IOException {
    CharSequence cs = (csq == null ? "null" : csq);
    write(cs.subSequence(start, end).toString());
    return this;
}

总结

  • Reader是字符输入流的抽象类。子类必须实现的方法只有read(char[], int, int) 和close()。但是,多数子类将重写此处定义的一些方法,以提供更高的效率和/或其他功能。如重写read方法提供更高的效率;重写mark/set方法提供标记功能。
  • Writer是字符输出流的抽象类。子类必须实现的方法仅有 write(char[], int, int)、flush() 和 close()。但是,多数子类将重写此处定义的一些方法,以提供更高的效率和/或其他功能。

Reader与InputStream区别

  • 操作对象的不同。字节流操作字节、字符操作字符。
  • 实现的接口不同。Reader比InputStream多实现了一个Readable接口,用于提供一个可以将字符写入到指定缓存数组的方法。
  • close方法不同。Reader的close方法是抽象的、子类必须重写,而InputStream则不是(空实现,即不要求子类必须重写)。 InputStream的close方法则不是抽象的。

Writer与OutputStream区别

  • 操作对象的不同。字节流操作字节、字符操作字符。
  • 实现的接口不同。Writer相比与OutputStream多实现了一个Appendable接口、用于提供几个向此流中追加字符的方法。
  • close、flush方法不同。Writer的close、flush方法都是抽象的,而OutputStream则不是(空实现,即不要求子类必须重写)。

CharArrayReader,CharArrayWriter

  • CharArrayReader实现一个可用作字符输入流的字符缓冲区。支持mark/set。
  • CharArrayWriter实现一个可用作字符输出流的字符缓冲区。缓冲区会随向流中写入数据而自动增长。可使用 toCharArray()和 toString()获取数据。

CharArrayReader

public class CharArrayReader extends Reader {
    /** 字符缓冲区 */
    protected char buf[];

    /** 缓冲区中下一个被获取的字符的索引 */
    protected int pos;

    /** 缓冲区中标记的位置. */
    protected int markedPos = 0;

    /** 字符缓冲区大小 **/
    protected int count;
    
    /**
     * 根据指定的char数组创建一个CharArrayReader。
     * 
     * buf不是复制得到的
     */
    public CharArrayReader(char buf[]) {
        this.buf = buf;
        this.pos = 0;
        this.count = buf.length;
    }

    //  根据指定的char数组创建一个CharArrayReader。
    public CharArrayReader(char buf[], int offset, int length) {
        //如果offset为负或大于buf.length,或者length为负,或者这两个值的和为负,抛出IllegalArgumentException。
        if ((offset < 0) || (offset > buf.length) || (length < 0) ||
            ((offset + length) < 0)) {
            throw new IllegalArgumentException();
        }
        this.buf = buf;
        this.pos = offset;
        //count为length或buf.length-offset其中的较小者
        this.count = Math.min(offset + length, buf.length);
        this.markedPos = offset;
    }
}

ensureOpen,ready,reset

/** 检查流是否被关闭。若字符缓冲为null,则认为流已关闭。*/
private void ensureOpen() throws IOException {
    if (buf == null)
        throw new IOException("Stream closed");
}

/**
 * 读取单个字符。
 * 如果到达缓冲区末尾,返回-1
 */
public int read() throws IOException {
    synchronized (lock) {
        // 输入流的打开检测
        ensureOpen();
        // 缓冲区容量判断
        if (pos >= count)
            return -1;
        else
            return buf[pos++];
    }
}

/**
 * 判断此流是否已准备好被读取。
 */
public boolean ready() throws IOException {
    synchronized (lock) {
        ensureOpen();
        // 缓冲区有数据可读即算准备好
        return (count - pos) > 0;
    }
}

/**
 * 将该流重置为最新的标记。
 * 如果从未标记过,则将其重置到开头。
 *
 * @exception  IOException  If an I/O error occurs
 */
public void reset() throws IOException {
    synchronized (lock) {
        ensureOpen();
        pos = markedPos;
    }
}

==注意:== 从ready函数我们可以总结出,CharArrayRead类相比于InputStream抽象类一派的子类来说性能更高,因为InputStream的子类在对需要加锁操作的方法上的处理是直接在函数签名上添加synchronized来保证的。这样的话锁的就是这个对象,这样对一些没有加锁的方法调用时性能就会很受影响。而这里CharArrayRead类则是使用其内部声明的对象锁来保证的,很好地解决了这个问题。

==注意:== 关于reset函数这里再贴一遍是为了提醒自己,不要忘记markPos变量声明时直接指定其值为0。也就是即便函数并没有调用过mark函数指定标记位置,也没有调用public CharArrayReader(char buf[], int offset, int length) {该多参构造函数构造对象,指定其markPos变量为offset位置。该值也会有默认reset到的位置。

测试程序

@Slf4j
public class TestController {

    public static void main(String[] args) {
        charArrayReaderTest();
    }

    public static void charArrayReaderTest() {
        char[] chars = new char[]{'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'j'};
        CharArrayReader charArrayReader = new CharArrayReader(chars);

        char[] container = new char[2];
        try {
            if (charArrayReader.ready()) {
                char target = (char) charArrayReader.read();
                System.out.println(target);// a - b

                if (charArrayReader.markSupported()) {
                    charArrayReader.mark(0);
                }

                charArrayReader.skip(2);// - d

                int containerLoaded = charArrayReader.read(container, 0, container.length);
                System.out.println(containerLoaded);// 2 - g

                charArrayReader.reset(); // - b

                container = new char[6];
                charArrayReader.read(container);
                for (int i = 0; i < container.length; i++) {
                    System.out.println(container[i]);// b-j
                }

                if (charArrayReader.ready()) {// 有没有数据要被读
                    charArrayReader.close();
                    charArrayReader.read(); // throw IOException
                }
            }
        } catch (IOException e) {
            log.error("buf is null");
        }
    }
}

结果

a
2
b
c
d
e
f
g
15:26:12.561 [main] ERROR com.mmall.concurrency.TestController - buf is null

总结

  • CharArrayReader实现了一个可和字符输入流一样使用的字符缓冲区。也就是说,其实CharArrayReader只是一个缓冲区,其数据总量也就是CharArrayReader实例在构造时传入的char数组的大小。
    • ==有意思的是== 它实现了Reader函数,包装成了输入流的样子,可以供我们以输入流的方式读取其缓冲池中的数据。
    • ==有点儿不懂的是==,还没有给默认的缓冲池,一定要求我们以输入流的方式读取我们给定的char数组中的数据。
  • 在做所有操作前,都要确认流处于open状态。判断流处于open状态的依据是buf不为null。close方法中会将buf置为null。
  • CharArrayReader支持mark()reset()操作。

CharArrayWriter

public class CharArrayWriter extends Writer {
    /**
     * 存储数据的字符缓冲区
     */
    protected char buf[];

    /**
     * 缓冲区中的字符个数
     */
    protected int count;

    /**
     * 创建一个新的CharArrayWriter。
     * 缓冲区大小默认为32
     */
    public CharArrayWriter() {
        this(32);
    }

    /**
     * 创建一个新的CharArrayWriter,指定缓冲区大小为initialSize
     * 如果initialSize为负数,抛出异常
     */
    public CharArrayWriter(int initialSize) {
        if (initialSize < 0) {
            throw new IllegalArgumentException("Negative initial size: "+ initialSize);
        }
        buf = new char[initialSize];
    }

}

write,writeTo,reset,toCharArray

/**
 * 将一个指定字符写到缓冲区中
 */
public void write(int c) {
    synchronized (lock) {
        int newcount = count + 1;
        //如果buf存满、则将buf容量扩大1倍、并将原来buf中count字符copy到新的buf中  
        if (newcount > buf.length) {
            buf = Arrays.copyOf(buf, Math.max(buf.length << 1, newcount));
        }
        // 插入到字符数据缓冲区的数据也尽量不要超过2个字节的表示区,
        // 否则结果不是你想要的,高16位舍去
        buf[count] = (char)c;
        count = newcount;
    }
}

/**
 * 将缓冲区的内容写入另一个字符流out。
 */
public void writeTo(Writer out) throws IOException {
    synchronized (lock) {
        out.write(buf, 0, count);
    }
}

/**
 * 重置该缓冲区,以便再次使用它而无需丢弃已分配的缓冲区。
 */
public void reset() {
    count = 0;
}

/**
 * 返回输入数据的副本。
 */
public char toCharArray()[] {
    synchronized (lock) {
        return Arrays.copyOf(buf, count);
    }
}

reset方法和toCharArray设计上行还是很简洁的。该类的flush,close方法无效。

总结

  • CharArrayWriter实现了一个可用作字符输出流的字符缓冲区。
  • CharArrayWriter的缓冲区会随着向流中写入数据的增多而自动增长(动态扩容 系数为1)。
  • 可使用CharArrayWritert的toCharArray()和 toString()获取缓冲区中数据或者writeTo数据传递到另一个Writer流中也行。
  • CharArrayWriter中close(),flush()方法无效(空实现)。
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,378评论 6 481
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,356评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 152,702评论 0 342
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,259评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,263评论 5 371
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,036评论 1 285
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,349评论 3 400
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,979评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,469评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,938评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,059评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,703评论 4 323
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,257评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,262评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,485评论 1 262
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,501评论 2 354
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,792评论 2 345

推荐阅读更多精彩内容