一、InputStreamReader------FileInputStream和FileReader之间的桥梁
计算机存储的单位是字节,尽管我们平时在操作系统(此处以windows操作系统为例)中使用.txt文件保存了汉字这样的字符,但是对计算机而言,这些.txt文件中的汉字其实是以字节形式存在的,所以,当使用者使用字节输入流(比如FileInputStream)来读取文件的时候其实是一个一个的单字节读取(1个字节可以表示28=256个符号,比如ASCII码就是用1个字节表示的编码方案 ,覆盖了英文中的所有符号),但是不同字符集解码成字符的时候需要不同个数的字节(比如中文,1个字节就无法表示完所有的中文汉字,因为中文汉字有5000+个,远远超过了28=256覆盖的范围),因此使用字节输入流(比如FileInputStream)来读取中文的时候会出现乱码,InputStreamReader.class就可以把字节输入流读取的字节进行缓冲然后再通过字符集解码成字符返回。
换句话说,InputStreamReader.class是从字节流到字符流的桥接器,InputStreamReader.class可以使用指定的字符集读取字节并将它们解码为字符。 它使用的字符集可以通过名称指定,也可以明确指定,或者可以接受操作系统平台的默认字符集。每次调用一个InputStreamReader的read()函数都可能导致从底层字节输入流中读取一个或多个字节。 为了实现字节到字符的有效转换,InputStreamReader.class可以从底层字节输入流中提取比满足当前读取操作所需的更多字节。
read()函数会尝试尽量从底层字节输入流中读取2个字符到字符缓冲区中,注意这里是尽量,比如遇到文件的最后字符时,就只能读取到1个字符,因此每次read()函数读取的字节数是不定的。
InputStreamReader的UML关系图,如下所示:
InputStreamReader.class的源码,如下所示:
package java.io;
import java.nio.charset.Charset;
import java.nio.charset.CharsetDecoder;
import sun.nio.cs.StreamDecoder;
public class InputStreamReader extends Reader {
private final StreamDecoder sd;
//使用InputStream类型字节输入流对象构造InputStreamReader对象,Reader.class::lock变量指向的对象(锁)也是这个InputStream类型字节输入流对象,使用的字符集为UTF-8(默认值)
public InputStreamReader(InputStream in) {
super(in);//设置Reader.class::lock变量指向的对象(锁)也是这个InputStream类型字节输入流对象
try {
sd = StreamDecoder.forInputStreamReader(in, this, (String)null); // ## check lock object
} catch (UnsupportedEncodingException e) {
//如果有异常,抛出一个Error,直接退出系统进程
// The default encoding should always be available
throw new Error(e);
}
}
//使用InputStream类型字节输入流对象构造InputStreamReader对象,Reader.class::lock变量指向的对象(锁)也是这个InputStream类型字节输入流对象,使用的字符集为自己用字符串指定的字符集
public InputStreamReader(InputStream in, String charsetName)
throws UnsupportedEncodingException
{
super(in);//设置Reader.class::lock变量指向的对象(锁)也是这个InputStream类型字节输入流对象
if (charsetName == null)
throw new NullPointerException("charsetName");
sd = StreamDecoder.forInputStreamReader(in, this, charsetName);
}
//使用InputStream类型字节输入流对象构造InputStreamReader对象,Reader.class::lock变量指向的对象(锁)也是这个InputStream类型字节输入流对象,使用的字符集为自己用Charset对象指定的字符集
public InputStreamReader(InputStream in, Charset cs) {
super(in);//设置Reader.class::lock变量指向的对象(锁)也是这个InputStream类型字节输入流对象
if (cs == null)
throw new NullPointerException("charset");
sd = StreamDecoder.forInputStreamReader(in, this, cs);
}
//使用InputStream类型字节输入流对象构造InputStreamReader对象,Reader.class::lock变量指向的对象(锁)也是这个InputStream类型字节输入流对象,使用的字符集为自己用CharsetDecoder对象指定的字符集
public InputStreamReader(InputStream in, CharsetDecoder dec) {
super(in);
if (dec == null)
throw new NullPointerException("charset decoder");
sd = StreamDecoder.forInputStreamReader(in, this, dec);
}
//调用StreamDecoder.class::getEncoding()函数获取StreamDecoder使用的解码方案(字符集),这些解码方案(字符集)包括但不限于ISO_8859_1,US_ASCII,Unicode(包括UTF-8)...
public String getEncoding() {
return sd.getEncoding();
}
//调用StreamDecoder.class::read()函数返回1个字符(如果还有字符的话)
public int read() throws IOException {
return sd.read();
}
//调用StreamDecoder.class::read(char cbuf[], int offset, int length) 函数一次性读取length个字符到字符数组char[] cbuf的[offset,offset+length)索引位置,并返回实际读取到的字符数量(如果有足够多的字符的话)
public int read(char cbuf[], int offset, int length) throws IOException {
return sd.read(cbuf, offset, length);
}
//调用StreamDecoder.class::ready()函数
//如果字节缓冲区ByteBuffer bb不为空,或者底层字节输入流或者文件中有字节可供读取,则返回true
public boolean ready() throws IOException {
return sd.ready();
}
//调用StreamDecoder.class::close()函数
public void close() throws IOException {
sd.close();
}
}
二、StreamDecoder------InputStreamReader中实际将字节解码的JDK内部类
InputStreamReader.class中的所有函数本质都是在调用StreamDecoder.class中的函数,包括InputStreamReader的构造函数都是在调用StreamDecoder.class::forInputStreamReader()函数,StreamDecoder的函数中会根据指定的字符集创造指定的字节解码器,字节解码器的作用就是解码字节成字符。
同时StreamDecoder.class这个类是在sun.nio.cs包下的类,因此Oracle的官方JDK中无法看到相关的源码,必须要通过OpenJDK来查看,OpenJDK的github地址如下:https://github.com/openjdk/jdk/blob/jdk-25%2B20/src/java.base/share/classes/sun/nio/cs/StreamEncoder.java
StreamDecoder的UML关系图,如下所示:
StreamDecoder.class的源码,如下所示:
package sun.nio.cs;
import java.io.IOException;
import java.io.InputStream;
import java.io.UnsupportedEncodingException;
import java.io.Reader;
import java.nio.ByteBuffer;
import java.nio.CharBuffer;
import java.nio.channels.FileChannel;
import java.nio.channels.ReadableByteChannel;
import java.nio.charset.Charset;
import java.nio.charset.CharsetDecoder;
import java.nio.charset.CoderResult;
import java.nio.charset.CodingErrorAction;
import java.nio.charset.IllegalCharsetNameException;
import java.nio.charset.UnsupportedCharsetException;
import java.util.Arrays;
public class StreamDecoder extends Reader {
// ByteBuffer(字节缓冲区) bb的大小,分为以下2中情况:
//场景一:当使用Channels对象创建自己这个StreamDecoder对象的时候,会对实参中的minBufferCap(mbc)变量做判断,又分为以下3种情况:
// a、当minBufferCap(mbc)<0时,会为ByteBuffer(字节缓冲区) bb分配一个8192(DEFAULT_BYTE_BUFFER_SIZE)的长度;
// b、当0<=minBufferCap(mbc)<32时,会为ByteBuffer(字节缓冲区) bb分配一个32(MIN_BYTE_BUFFER_SIZE)的长度;
// c、当32<=minBufferCap(mbc)时,为ByteBuffer(字节缓冲区) bb分配的长度就是minBufferCap(mbc)这个实参;
//场景二:当使用InputStreamReader 对象创建自己这个StreamDecoder对象的时候,没有指定ByteBuffer(字节缓冲区)长度的实参,会直接为ByteBuffer(字节缓冲区) bb分配一个8192(DEFAULT_BYTE_BUFFER_SIZE)的长度
private static final int MIN_BYTE_BUFFER_SIZE = 32;
private static final int DEFAULT_BYTE_BUFFER_SIZE = 8192;
//标记这个StreamDecoder是否开启,false表示开启,可以使用StreamDecoder.class::read()函数和StreamDecoder.class::ready()函数了,true表示关闭,不能再使用StreamDecoder.class::read()函数和StreamDecoder.class::ready()函数了
private volatile boolean closed;
//如果StreamDecoder已经被boolean closed变量标记为关闭(true)了,则不能再执行StreamDecoder.class::read()函数和StreamDecoder.class::ready()函数了,否则会抛出一个IOException("Stream closed")
private void ensureOpen() throws IOException {
if (closed)
throw new IOException("Stream closed");
}
// In order to handle surrogates properly we must never try to produce
// fewer than two characters at a time. If we're only asked to return one
// character then the other is saved here to be returned later.
//
// 特别为read()函数准备(可以更好的处理代理字符),read()函数只要求返回1个字符,但是实际上是读取了2个字符,多读取的1个字符会被赋值(缓存)到char leftoverChar变量中,而boolean haveLeftoverChar变量只是用来标记char leftoverChar中是否有缓存的字符
//这个思路类似于BufferedReader中用boolean skipLF跳过换行符\r\n(在windows操作系统下面),但是又并不完全相同,BufferedReader中的所有数据都保存在char[] cb(缓冲区数组)中
private boolean haveLeftoverChar = false;
private char leftoverChar;
//提供给InputStreamReader对象创建自己这个StreamDecoder对象的工厂函数
public static StreamDecoder forInputStreamReader(InputStream in,
Object lock,
String charsetName)
throws UnsupportedEncodingException
{
try {
return new StreamDecoder(in, lock, Charset.forName(charsetName));
} catch (IllegalCharsetNameException | UnsupportedCharsetException x) {
throw new UnsupportedEncodingException (charsetName);
}
}
//提供给InputStreamReader对象创建自己这个StreamDecoder对象的工厂函数
public static StreamDecoder forInputStreamReader(InputStream in,
Object lock,
Charset cs)
{
return new StreamDecoder(in, lock, cs);
}
//提供给InputStreamReader对象创建自己这个StreamDecoder对象的工厂函数
public static StreamDecoder forInputStreamReader(InputStream in,
Object lock,
CharsetDecoder dec)
{
return new StreamDecoder(in, lock, dec);
}
// Factory for java.nio.channels.Channels.newReader
//提供给Channels对象创建自己这个StreamDecoder对象的工厂函数
public static StreamDecoder forDecoder(ReadableByteChannel ch,
CharsetDecoder dec,
int minBufferCap)
{
return new StreamDecoder(ch, dec, minBufferCap);
}
//接下来的函数都是与 InputStreamReader 中的函数一一对应的,换句话说,接下来的函数都是通过 InputStreamReader 中对应功能的函数(可能名称不对应,但是功能一定对应)来调用的
//所有同步和状态/参数检查都在这些函数中完成;下面定义的具体流解码器子类无需进行此类检查。
// -- Public methods corresponding to those in InputStreamReader --
// All synchronization and state/argument checking is done in these public
// methods; the concrete stream-decoder subclasses defined below need not
// do any such checking.
//返回这个StreamDecoder使用的解码方案(字符集),这些解码方案(字符集)包括但不限于ISO_8859_1,US_ASCII,Unicode(包括UTF-8)...
public String getEncoding() {
if (isOpen())
return encodingName();
return null;
}
//对应InputStreamReader.class::read()函数,返回单个字符,实际调用下面的read0()函数
public int read() throws IOException {
return read0();
}
//@SuppressWarnings("fallthrough")可以压制(即消除)编译器对 case 2 分支缺少中断语句(return、break、throw...等)的报警
//函数内部做了线程同步,如果有足够字符的话,一次性从read(char[] cbuf, int offset, int length)函数中读取2个字符
@SuppressWarnings("fallthrough")
private int read0() throws IOException {
synchronized (lock) {
if (haveLeftoverChar) {
//如果char leftoverChar变量中有被赋值(缓存)的字符char,则直接返回,并把boolean haveLeftoverChar标记改为false表示char leftoverChar变量中有被赋值(缓存)的字符已经被读取了
haveLeftoverChar = false;
return leftoverChar;
}
// 创建了长度为2的字符数组char[] cb
char[] cb = new char[2];
// 调用下面的read(char[] cbuf, int offset, int length)函数,返回读取的字符个数
int n = read(cb, 0, 2);
switch (n) {
case -1:// 若read(char[] cbuf, int offset, int length)函数返回-1,代表读到末尾
return -1; //直接返回-1
case 2:// 若read(char[] cbuf, int offset, int length)函数返回2,代表读到了2个字符
leftoverChar = cb[1];//将char[] cb数组中第2个字符(索引1位置)放到char leftoverChar变量中暂时缓存
haveLeftoverChar = true;//并把boolean haveLeftoverChar标记改为true表示char leftoverChar变量中有被赋值(缓存)的字符
// FALL THROUGH
case 1:// 若read(char[] cbuf, int offset, int length)函数返回1,代表读到了1个字符
return cb[0];//实际执行的时候,如果read(char[] cbuf, int offset, int length)函数的返回值为2,char[] cb数组中有2个字符,这里会被case2穿透,直接返回char[] cb数组中的第1个字符(索引0位置)
//详情看小标题x.x.x
default:
assert false : n;//实际编译后,这里会被编译器优化到case2之前,详情看小标题x.x.x
return -1;
}
}
}
//如果有足够多字符的话,一次性读取length个字符到字符数组char[] cbuf的[offset,offset+length)索引位置,并返回实际读取到的字符数量
//函数内部做了线程同步
public int read(char[] cbuf, int offset, int length) throws IOException {
synchronized (lock) {
//先在这个函数的调用栈上复制offset为off、length为len
int off = offset;
int len = length;
ensureOpen();
if ((off < 0) || (off > cbuf.length) || (len < 0) ||
((off + len) > cbuf.length) || ((off + len) < 0)) {//char[]数组cbuf的[offset,offset+length)(左闭右开)索引位置是否有越界的检查
throw new IndexOutOfBoundsException();//越界的话,抛出一个IndexOutOfBoundsException
}
if (len == 0)
return 0;//如果len==0,结束本次函数调用,并返回0
//实际读取到的字符数量
int n = 0;
// 如果char leftoverChar变量中有被赋值(缓存)的字符char,则boolean haveLeftoverChar变量为true
if (haveLeftoverChar) {
// 将char leftoverChar变量中被赋值(缓存)的字符char先放到字符数组char[] cbuf的off(offset)索引位置
cbuf[off] = leftoverChar;
off++; len--;//然后将off(offset)索引位置右移1位,将len-=1
haveLeftoverChar = false;//并把boolean haveLeftoverChar标记改为false表示char leftoverChar变量中有被赋值(缓存)的字符了
n = 1;//设置n=1
if ((len == 0) || !implReady())
// Return now if this is all we can produce w/o blocking
//如果这已经是StreamDecoder在不阻塞的情况下能读取到字符数组char[] cbuf中的全部字符了,就立即返回。
return n;
}
if (len == 1) {
//如果此时len==1,则表示接下来只需要向字符数组char[] cbuf中再读取1个字符,那么直接调用read0()函数即可,此时len==1分为以下2中情况:
//场景一:要读取到字符数组char[] cbuf中的字符数量length=2,同时char leftoverChar变量中被赋值(缓存)了1个字符
//场景二:要读取到字符数组char[] cbuf中的字符数量length=1,并且char leftoverChar变量中没有缓存字符
int c = read0();//read0()函数一次只能读取1个字符
if (c == -1)//c==-1表示从已经从read0()函数中读取不到任何字符了
return (n == 0) ? -1 : n;//返回实际读取到字符数组char[] cbuf中的字符数量n或者-1
cbuf[off] = (char)c;//如果还能从read0()函数中读取到字符,则将读取到的这个字符保存到字符数组char[] cbuf中
return n + 1;//累加1到实际读取到字符数组char[] cbuf中的字符数量n上面,并返回
}
// Read remaining characters
//执行完上面的代码到这里的时候,off和len有以下2种可能:
//可能1:char leftoverChar变量中没有被赋值(缓存)的字符char,并且len>1(要填充到字符数组char[] cbuf中的字符数量length>1);
//可能2:char leftoverChar变量中有被赋值(缓存)的字符char,并且len>2(要填充到字符数组char[] cbuf中的字符数量length>2)。
//那么执行implRead()函数向字符数组char[] cbuf的[off,off + len)索引位置填充len个字符,并返回实际填充的字符数量nr,举例如下:
//假如当int offset的实参为0,int length的实参为5时,如果之前满足可能1,则此时[off,off + len)为[0,5),返回5或者-1,如果之前满足可能2,则此时[off,off + len)为[1,5),返回4或者-1
int nr = implRead(cbuf, off, off + len);
// At this point, n is either 1 if a leftover character was read,
// or 0 if no leftover character was read. If n is 1 and nr is -1,
// indicating EOF, then we don't return their sum as this loses data.
//此时nr有以下2种可能得值,-1或者实际向字符数组char[] cbuf的[off,off + len)索引位置填充的字符数量nr,当nr=-1时,n只有以下2种可能:
//可能a:之前char leftoverChar变量中没有被赋值(缓存)的字符char,n=0,此时return -1
//可能b:之前char leftoverChar变量中有被赋值(缓存)的字符char,n=1,此时return 1
//当nr≠=-1时,表示的确向字符数组char[] cbuf的[off,off + len)索引位置填充了nr个字符,return n+nr(0+nr或者1+nr)
return (nr < 0) ? (n == 1 ? 1 : nr) : (n + nr);
}
}
public boolean ready() throws IOException {
synchronized (lock) {
ensureOpen();
return haveLeftoverChar || implReady();
}
}
//关闭这个StreamDecoder中成员变量引用的字节输入流和其它资源,并且设置boolean closed=true标记这个StreamDecoder已经关闭了,不能再使用所有重载的StreamDecoder.class::read()函数和StreamDecoder.class::ready()函数了
//函数内部做了线程同步
public void close() throws IOException {
synchronized (lock) {
if (closed)
return;
try {
implClose();
} finally {
closed = true;
}
}
}
private boolean isOpen() {
return !closed;
}
public void fillZeroToPosition() throws IOException {
synchronized (lock) {
Arrays.fill(bb.array(), bb.arrayOffset(),
bb.arrayOffset() + bb.position(), (byte)0);
}
}
// -- Charset-based stream decoder impl --
private final Charset cs;
//StreamDecoder中真正用来将ByteBuffer bb中的字节解码成不同字符的工具(ByteBuffer bb中的字节也是从字节输入流InputStream in中获取到的),可以解码的字符集(解码方案)包括但不限于ISO_8859_1,US_ASCII,Unicode(包括UTF-8)...
private final CharsetDecoder decoder;
//字节输入流InputStream in中的字节被CharsetDecoder decoder解码成字符之前,会先读取到ByteBuffer bb中缓存
private final ByteBuffer bb;
//字节输入流,真正通过StreamDecoder转换成字符的字节都是从这个字节输入流中获取的,不过将这些字节转换成字符之前会先读取到ByteBuffer bb中缓存
private final InputStream in;
//只有通过Channels对象创建自己这个StreamDecoder对象的时候才会使用到这个变量,通InputStreamReader对象创建自己这个StreamDecoder对象的时候用不到这个变量
private final ReadableByteChannel ch;
//构造函数
StreamDecoder(InputStream in, Object lock, Charset cs) {
this(in, lock,
cs.newDecoder()
.onMalformedInput(CodingErrorAction.REPLACE)
.onUnmappableCharacter(CodingErrorAction.REPLACE));
}
//构造函数
StreamDecoder(InputStream in, Object lock, CharsetDecoder dec) {
//设置Reader.class::lock变量指向的对象(锁)是创建自己这个StreamDecoder对象的InputStreamReader对象
super(lock);
this.cs = dec.charset();
this.decoder = dec;
this.in = in;
this.ch = null;
this.bb = ByteBuffer.allocate(DEFAULT_BYTE_BUFFER_SIZE);
bb.flip(); // So that bb is initially empty
}
//构造函数
StreamDecoder(ReadableByteChannel ch, CharsetDecoder dec, int mbc) {
this.in = null;
this.ch = ch;
this.decoder = dec;
this.cs = dec.charset();
this.bb = ByteBuffer.allocate(mbc < 0
? DEFAULT_BYTE_BUFFER_SIZE
: (mbc < MIN_BYTE_BUFFER_SIZE
? MIN_BYTE_BUFFER_SIZE
: mbc));
bb.flip();
}
//最终调用的方法,利用底层字节输入流,读取字节到字节缓冲区ByteBuffer bb中
private int readBytes() throws IOException {
bb.compact();
try {
if (ch != null) {
// Read from the channel
int n = ch.read(bb);
if (n < 0)
return n;
} else {
// Read from the input stream, and then update the buffer
// 从输入流中读取,然后更新字节缓冲区ByteBuffer bb
int lim = bb.limit();
int pos = bb.position();
assert (pos <= lim);
int rem = (pos <= lim ? lim - pos : 0);
int n = in.read(bb.array(), bb.arrayOffset() + pos, rem);
if (n < 0)
return n;
if (n == 0)
throw new IOException("Underlying input stream returned zero bytes");
assert (n <= rem) : "n = " + n + ", rem = " + rem;
bb.position(pos + n);
}
} finally {
//即使抛出 IOException 也要翻转,否则字节缓冲区ByteBuffer会有问题
bb.flip();//反转字节缓冲区中的所有字节
}
//计算读取到字节缓冲区ByteBuffer中的字节数量,并返回
int rem = bb.remaining();
assert (rem != 0) : rem;
return rem;
}
//如果有足够多字符的话,一次性填充end-off个字符到字符数组char[] cbuf的[off,end)索引位置,并返回实际填充到字符数组char[] cbuf中的字符数量
int implRead(char[] cbuf, int off, int end) throws IOException {
//要填充到字符数组char[] cbuf中的字符数量必须>1
assert (end - off > 1);
//将字符数组char[] cbuf包装到字符缓冲区CharBuffer cb中,包装到缓冲区之后,字符缓冲区CharBuffer cb中修改的内容会同步修改到字符数组char[] cbuf中
//可以将CharBuffer对象理解为一个数组,管理的索引位置为[off,end)区间的end - off个字符
CharBuffer cb = CharBuffer.wrap(cbuf, off, end - off);
if (cb.position() != 0) {
//将字符缓冲区CharBuffer cb管理的索引区间和要填充的字符数组char[] cbuf进行对齐,确保cb[0]=cbuf[0ff]
cb = cb.slice();
}
//eof=false表示还没有读到字节输入流(或者叫文件)的末尾,字节输入流或者文件中还有字节可以提供给StreamDecoder来进行转换
boolean eof = false;
for (;;) {
//从字节缓冲区ByteBuffer bb中解码尽可能多的字节,将结果写入给定的字符缓冲区CharBuffer cb中,也就是字符数组char[] cbuf中用于读取字符
CoderResult cr = decoder.decode(bb, cb, eof);
if (cr.isUnderflow()) {
if (eof)
break;
if (!cb.hasRemaining())
//检查字符缓冲区CharBuffer cb中是否还有剩余空间,若没有则跳出循环
break;
if ((cb.position() > 0) && !inReady())
//字符缓冲区CharBuffer cb存在内容时,跳出循环,以便返回读取到的字符数
break; // Block at most once
//读取字节到字节缓冲区ByteBuffer bb
int n = readBytes();
if (n < 0) {
//若读取字节到字节缓冲区ByteBuffer bb时返回了-1,代表着已到了字节输入流(或者叫文件)的末尾,则不会有其它字节输入了
eof = true;
if ((cb.position() == 0) && (!bb.hasRemaining()))
//如果此时字符缓冲区CharBuffer cb和字节缓冲区ByteBuffer bb中都没有数据了,则跳出循环
break;
}
continue;
}
if (cr.isOverflow()) {
//当从字节缓冲区ByteBuffer bb中解码的字节太多导致溢出时,字符缓冲区CharBuffer cb必须有解码的字符,否则这里的断言就会报错
assert cb.position() > 0;
//溢出时也跳出循环
break;
}
cr.throwException();
}
if (eof) {
// ## Need to flush decoder
//如果已经到了字节输入流(或者叫文件)的末尾,则重置解码器,清除所有内部状态。
decoder.reset();
}
if (cb.position() == 0) {
if (eof) {
return -1;
}
assert false;
}
//返回字符缓冲区CharBuffer cb的当前位置,代表着实际读取或者填充到字符数组char[] cbuf中的字符数量
return cb.position();
}
String encodingName() {
return ((cs instanceof HistoricallyNamedCharset)
? ((HistoricallyNamedCharset)cs).historicalName()
: cs.name());
}
private boolean inReady() {
try {
return (((in != null) && (in.available() > 0))
|| (ch instanceof FileChannel)); // ## RBC.available()?
} catch (IOException x) {
return false;
}
}
boolean implReady() {
return bb.hasRemaining() || inReady();
}
void implClose() throws IOException {
if (ch != null) {
ch.close();
} else {
in.close();
}
}
}
2.1、构造函数和可以解码的字符集(解码方案)
StreamDecoder的构造函数从来不单独对外提供,所以,使用者包括JDK的内部类必须要同过forInputStreamReader()、forDecoder()这些工厂函数来创建StreamDecoder.class的实例。同时,StreamDecoder.class在sun.nio.cs包中,使用者无法引入这个包,所以,使用者也无法在自己的代码中创建StreamDecoder的实例,因此,StreamDecoder.class中提供的工厂函数forInputStreamReader()、forDecoder()只是提供给JDK的内部类InputStreamReader.class和Channels.class来使用的。如下所示:
public class StreamDecoder extends Reader {
...省略部分代码...
//提供给InputStreamReader对象创建自己这个StreamDecoder对象的工厂函数
public static StreamDecoder forInputStreamReader(InputStream in,
Object lock,
String charsetName)
throws UnsupportedEncodingException
{
try {
return new StreamDecoder(in, lock, Charset.forName(charsetName));
} catch (IllegalCharsetNameException | UnsupportedCharsetException x) {
throw new UnsupportedEncodingException (charsetName);
}
}
//提供给InputStreamReader对象创建自己这个StreamDecoder对象的工厂函数
public static StreamDecoder forInputStreamReader(InputStream in,
Object lock,
Charset cs)
{
return new StreamDecoder(in, lock, cs);
}
//提供给InputStreamReader对象创建自己这个StreamDecoder对象的工厂函数
public static StreamDecoder forInputStreamReader(InputStream in,
Object lock,
CharsetDecoder dec)
{
return new StreamDecoder(in, lock, dec);
}
// Factory for java.nio.channels.Channels.newReader
//提供给Channels对象创建自己这个StreamDecoder对象的工厂函数
public static StreamDecoder forDecoder(ReadableByteChannel ch,
CharsetDecoder dec,
int minBufferCap)
{
return new StreamDecoder(ch, dec, minBufferCap);
}
//构造函数
StreamDecoder(InputStream in, Object lock, Charset cs) {
this(in, lock,
cs.newDecoder()
.onMalformedInput(CodingErrorAction.REPLACE)
.onUnmappableCharacter(CodingErrorAction.REPLACE));
}
//构造函数
StreamDecoder(InputStream in, Object lock, CharsetDecoder dec) {
//设置Reader.class::lock变量指向的对象(锁)是创建自己这个StreamDecoder对象的InputStreamReader对象
super(lock);
this.cs = dec.charset();
this.decoder = dec;
this.in = in;
this.ch = null;
this.bb = ByteBuffer.allocate(DEFAULT_BYTE_BUFFER_SIZE);
bb.flip(); // So that bb is initially empty
}
//构造函数
StreamDecoder(ReadableByteChannel ch, CharsetDecoder dec, int mbc) {
this.in = null;
this.ch = ch;
this.decoder = dec;
this.cs = dec.charset();
this.bb = ByteBuffer.allocate(mbc < 0
? DEFAULT_BYTE_BUFFER_SIZE
: (mbc < MIN_BYTE_BUFFER_SIZE
? MIN_BYTE_BUFFER_SIZE
: mbc));
bb.flip();
}
...省略部分代码...
}
2.1.1、可以解码的字符集(解码方案)
StreamDecoder.class中将字节输入流中的字节解码成字符的过程是通过CharsetDecoder decoder这个工具来完成的,如下所示:
...省略部分代码...
//StreamDecoder中真正用来将ByteBuffer bb中的字节解码成不同字符的工具(ByteBuffer bb中的字节也是从字节输入流InputStream in中获取到的),可以解码的字符集(解码方案)包括但不限于ISO_8859_1,US_ASCII,Unicode(包括UTF-8)...
private final CharsetDecoder decoder;
//如果有足够多字符的话,一次性填充end-off个字符到字符数组char[] cbuf的[off,end)索引位置,并返回实际填充到字符数组char[] cbuf中的字符数量
int implRead(char[] cbuf, int off, int end) throws IOException {
...省略部分代码...
//eof=false表示还没有读到字节输入流(或者叫文件)的末尾,字节输入流或者文件中还有字节可以提供给StreamDecoder来进行转换
boolean eof = false;
for (;;) {
//从字节缓冲区ByteBuffer bb中解码尽可能多的字节,将结果写入给定的字符缓冲区CharBuffer cb中,也就是字符数组char[] cbuf中用于读取字符
CoderResult cr = decoder.decode(bb, cb, eof);
...省略部分代码...
StreamDecoder.class可以解码的字符集(解码方案)包括但不限于ISO_8859_1,US_ASCII,Unicode(包括UTF-8)...,StreamDecoder.class可以解码的所有字符集(解码方案)如下所示:
三、StreamDecoder的一次完整解码过程
package sun.nio.cs;
import java.io.IOException;
import java.io.InputStream;
import java.io.UnsupportedEncodingException;
import java.io.Reader;
import java.nio.ByteBuffer;
import java.nio.CharBuffer;
import java.nio.channels.FileChannel;
import java.nio.channels.ReadableByteChannel;
import java.nio.charset.Charset;
import java.nio.charset.CharsetDecoder;
import java.nio.charset.CoderResult;
import java.nio.charset.CodingErrorAction;
import java.nio.charset.IllegalCharsetNameException;
import java.nio.charset.UnsupportedCharsetException;
import java.util.Arrays;
public class StreamDecoder extends Reader {
...省略部分代码...
// ByteBuffer(字节缓冲区) bb的大小,分为以下2中情况:
//场景一:当使用Channels对象创建自己这个StreamDecoder对象的时候,会对实参中的minBufferCap(mbc)变量做判断,又分为以下3种情况:
// a、当minBufferCap(mbc)<0时,会为ByteBuffer(字节缓冲区) bb分配一个8192(DEFAULT_BYTE_BUFFER_SIZE)的长度;
// b、当0<=minBufferCap(mbc)<32时,会为ByteBuffer(字节缓冲区) bb分配一个32(MIN_BYTE_BUFFER_SIZE)的长度;
// c、当32<=minBufferCap(mbc)时,为ByteBuffer(字节缓冲区) bb分配的长度就是minBufferCap(mbc)这个实参;
//场景二:当使用InputStreamReader 对象创建自己这个StreamDecoder对象的时候,没有指定ByteBuffer(字节缓冲区)长度的实参,会直接为ByteBuffer(字节缓冲区) bb分配一个8192(DEFAULT_BYTE_BUFFER_SIZE)的长度
private static final int MIN_BYTE_BUFFER_SIZE = 32;
private static final int DEFAULT_BYTE_BUFFER_SIZE = 8192;
//标记这个StreamDecoder是否开启,false表示开启,可以使用StreamDecoder.class::read()函数和StreamDecoder.class::ready()函数了,true表示关闭,不能再使用StreamDecoder.class::read()函数和StreamDecoder.class::ready()函数了
private volatile boolean closed;
//如果StreamDecoder已经被boolean closed变量标记为关闭(true)了,则不能再执行StreamDecoder.class::read()函数和StreamDecoder.class::ready()函数了,否则会抛出一个IOException("Stream closed")
private void ensureOpen() throws IOException {
if (closed)
throw new IOException("Stream closed");
}
// In order to handle surrogates properly we must never try to produce
// fewer than two characters at a time. If we're only asked to return one
// character then the other is saved here to be returned later.
//
// 特别为read()函数准备(可以更好的处理代理字符),read()函数只要求返回1个字符,但是实际上是读取了2个字符,多读取的1个字符会被赋值(缓存)到char leftoverChar变量中,而boolean haveLeftoverChar变量只是用来标记char leftoverChar中是否有缓存的字符
//这个思路类似于BufferedReader中用boolean skipLF跳过换行符\r\n(在windows操作系统下面),但是又并不完全相同,BufferedReader中的所有数据都保存在char[] cb(缓冲区数组)中
private boolean haveLeftoverChar = false;
private char leftoverChar;
//提供给InputStreamReader对象创建自己这个StreamDecoder对象的工厂函数
public static StreamDecoder forInputStreamReader(InputStream in,
Object lock,
Charset cs)
{
return new StreamDecoder(in, lock, cs);
}
//对应InputStreamReader.class::read()函数,返回单个字符,实际调用下面的read0()函数
public int read() throws IOException {
return read0();
}
//@SuppressWarnings("fallthrough")可以压制(即消除)编译器对 case 2 分支缺少中断语句(return、break、throw...等)的报警
//函数内部做了线程同步,如果有足够字符的话,一次性从read(char[] cbuf, int offset, int length)函数中读取2个字符
@SuppressWarnings("fallthrough")
private int read0() throws IOException {
synchronized (lock) {
if (haveLeftoverChar) {
//如果char leftoverChar变量中有被赋值(缓存)的字符char,则直接返回,并把boolean haveLeftoverChar标记改为false表示char leftoverChar变量中有被赋值(缓存)的字符已经被读取了
haveLeftoverChar = false;
return leftoverChar;
}
// 创建了长度为2的字符数组char[] cb
char[] cb = new char[2];
// 调用下面的read(char[] cbuf, int offset, int length)函数,返回读取的字符个数
int n = read(cb, 0, 2);
switch (n) {
case -1:// 若read(char[] cbuf, int offset, int length)函数返回-1,代表读到末尾
return -1; //直接返回-1
case 2:// 若read(char[] cbuf, int offset, int length)函数返回2,代表读到了2个字符
leftoverChar = cb[1];//将char[] cb数组中第2个字符(索引1位置)放到char leftoverChar变量中暂时缓存
haveLeftoverChar = true;//并把boolean haveLeftoverChar标记改为true表示char leftoverChar变量中有被赋值(缓存)的字符
// FALL THROUGH
case 1:// 若read(char[] cbuf, int offset, int length)函数返回1,代表读到了1个字符
return cb[0];//实际执行的时候,如果read(char[] cbuf, int offset, int length)函数的返回值为2,char[] cb数组中有2个字符,这里会被case2穿透,直接返回char[] cb数组中的第1个字符(索引0位置)
//详情看小标题x.x.x
default:
assert false : n;//实际编译后,这里会被编译器优化到case2之前,详情看小标题x.x.x
return -1;
}
}
}
//如果有足够多字符的话,一次性读取length个字符到字符数组char[] cbuf的[offset,offset+length)索引位置,并返回实际读取到的字符数量
//函数内部做了线程同步
public int read(char[] cbuf, int offset, int length) throws IOException {
synchronized (lock) {
//先在这个函数的调用栈上复制offset为off、length为len
int off = offset;
int len = length;
ensureOpen();
if ((off < 0) || (off > cbuf.length) || (len < 0) ||
((off + len) > cbuf.length) || ((off + len) < 0)) {//char[]数组cbuf的[offset,offset+length)(左闭右开)索引位置是否有越界的检查
throw new IndexOutOfBoundsException();//越界的话,抛出一个IndexOutOfBoundsException
}
if (len == 0)
return 0;//如果len==0,结束本次函数调用,并返回0
//实际读取到的字符数量
int n = 0;
// 如果char leftoverChar变量中有被赋值(缓存)的字符char,则boolean haveLeftoverChar变量为true
if (haveLeftoverChar) {
// 将char leftoverChar变量中被赋值(缓存)的字符char先放到字符数组char[] cbuf的off(offset)索引位置
cbuf[off] = leftoverChar;
off++; len--;//然后将off(offset)索引位置右移1位,将len-=1
haveLeftoverChar = false;//并把boolean haveLeftoverChar标记改为false表示char leftoverChar变量中有被赋值(缓存)的字符了
n = 1;//设置n=1
if ((len == 0) || !implReady())
// Return now if this is all we can produce w/o blocking
//如果这已经是StreamDecoder在不阻塞的情况下能读取到字符数组char[] cbuf中的全部字符了,就立即返回。
return n;
}
if (len == 1) {
//如果此时len==1,则表示接下来只需要向字符数组char[] cbuf中再读取1个字符,那么直接调用read0()函数即可,此时len==1分为以下2中情况:
//场景一:要读取到字符数组char[] cbuf中的字符数量length=2,同时char leftoverChar变量中被赋值(缓存)了1个字符
//场景二:要读取到字符数组char[] cbuf中的字符数量length=1,并且char leftoverChar变量中没有缓存字符
int c = read0();//read0()函数一次只能读取1个字符
if (c == -1)//c==-1表示从已经从read0()函数中读取不到任何字符了
return (n == 0) ? -1 : n;//返回实际读取到字符数组char[] cbuf中的字符数量n或者-1
cbuf[off] = (char)c;//如果还能从read0()函数中读取到字符,则将读取到的这个字符保存到字符数组char[] cbuf中
return n + 1;//累加1到实际读取到字符数组char[] cbuf中的字符数量n上面,并返回
}
// Read remaining characters
//执行完上面的代码到这里的时候,off和len有以下2种可能:
//可能1:char leftoverChar变量中没有被赋值(缓存)的字符char,并且len>1(要填充到字符数组char[] cbuf中的字符数量length>1);
//可能2:char leftoverChar变量中有被赋值(缓存)的字符char,并且len>2(要填充到字符数组char[] cbuf中的字符数量length>2)。
//那么执行implRead()函数向字符数组char[] cbuf的[off,off + len)索引位置填充len个字符,并返回实际填充的字符数量nr,举例如下:
//假如当int offset的实参为0,int length的实参为5时,如果之前满足可能1,则此时[off,off + len)为[0,5),返回5或者-1,如果之前满足可能2,则此时[off,off + len)为[1,5),返回4或者-1
int nr = implRead(cbuf, off, off + len);
// At this point, n is either 1 if a leftover character was read,
// or 0 if no leftover character was read. If n is 1 and nr is -1,
// indicating EOF, then we don't return their sum as this loses data.
//此时nr有以下2种可能得值,-1或者实际向字符数组char[] cbuf的[off,off + len)索引位置填充的字符数量nr,当nr=-1时,n只有以下2种可能:
//可能a:之前char leftoverChar变量中没有被赋值(缓存)的字符char,n=0,此时return -1
//可能b:之前char leftoverChar变量中有被赋值(缓存)的字符char,n=1,此时return 1
//当nr≠=-1时,表示的确向字符数组char[] cbuf的[off,off + len)索引位置填充了nr个字符,return n+nr(0+nr或者1+nr)
return (nr < 0) ? (n == 1 ? 1 : nr) : (n + nr);
}
}
// -- Charset-based stream decoder impl --
private final Charset cs;
//StreamDecoder中真正用来将ByteBuffer bb中的字节解码成不同字符的工具(ByteBuffer bb中的字节也是从字节输入流InputStream in中获取到的),可以解码的字符集(解码方案)包括但不限于ISO_8859_1,US_ASCII,Unicode(包括UTF-8)...
private final CharsetDecoder decoder;
//字节输入流InputStream in中的字节被CharsetDecoder decoder解码成字符之前,会先读取到ByteBuffer bb中缓存
private final ByteBuffer bb;
//字节输入流,真正通过StreamDecoder转换成字符的字节都是从这个字节输入流中获取的,不过将这些字节转换成字符之前会先读取到ByteBuffer bb中缓存
private final InputStream in;
//只有通过Channels对象创建自己这个StreamDecoder对象的时候才会使用到这个变量,通InputStreamReader对象创建自己这个StreamDecoder对象的时候用不到这个变量
private final ReadableByteChannel ch;
//构造函数
StreamDecoder(InputStream in, Object lock, Charset cs) {
this(in, lock,
cs.newDecoder()
.onMalformedInput(CodingErrorAction.REPLACE)
.onUnmappableCharacter(CodingErrorAction.REPLACE));
}
//构造函数
StreamDecoder(InputStream in, Object lock, CharsetDecoder dec) {
//设置Reader.class::lock变量指向的对象(锁)是创建自己这个StreamDecoder对象的InputStreamReader对象
super(lock);
this.cs = dec.charset();
this.decoder = dec;
this.in = in;
this.ch = null;
this.bb = ByteBuffer.allocate(DEFAULT_BYTE_BUFFER_SIZE);
bb.flip(); // So that bb is initially empty
}
//最终调用的方法,利用底层字节输入流,读取字节到字节缓冲区ByteBuffer bb中
private int readBytes() throws IOException {
bb.compact();
try {
if (ch != null) {
// Read from the channel
int n = ch.read(bb);
if (n < 0)
return n;
} else {
// Read from the input stream, and then update the buffer
// 从输入流中读取,然后更新字节缓冲区ByteBuffer bb
int lim = bb.limit();
int pos = bb.position();
assert (pos <= lim);
int rem = (pos <= lim ? lim - pos : 0);
int n = in.read(bb.array(), bb.arrayOffset() + pos, rem);
if (n < 0)
return n;
if (n == 0)
throw new IOException("Underlying input stream returned zero bytes");
assert (n <= rem) : "n = " + n + ", rem = " + rem;
bb.position(pos + n);
}
} finally {
//即使抛出 IOException 也要翻转,否则字节缓冲区ByteBuffer会有问题
bb.flip();//反转字节缓冲区中的所有字节
}
//计算读取到字节缓冲区ByteBuffer中的字节数量,并返回
int rem = bb.remaining();
assert (rem != 0) : rem;
return rem;
}
//如果有足够多字符的话,一次性填充end-off个字符到字符数组char[] cbuf的[off,end)索引位置,并返回实际填充到字符数组char[] cbuf中的字符数量
int implRead(char[] cbuf, int off, int end) throws IOException {
//要填充到字符数组char[] cbuf中的字符数量必须>1
assert (end - off > 1);
//将字符数组char[] cbuf包装到字符缓冲区CharBuffer cb中,包装到缓冲区之后,字符缓冲区CharBuffer cb中修改的内容会同步修改到字符数组char[] cbuf中
//可以将CharBuffer对象理解为一个数组,管理的索引位置为[off,end)区间的end - off个字符
CharBuffer cb = CharBuffer.wrap(cbuf, off, end - off);
if (cb.position() != 0) {
//将字符缓冲区CharBuffer cb管理的索引区间和要填充的字符数组char[] cbuf进行对齐,确保cb[0]=cbuf[0ff]
cb = cb.slice();
}
//eof=false表示还没有读到字节输入流(或者叫文件)的末尾,字节输入流或者文件中还有字节可以提供给StreamDecoder来进行转换
boolean eof = false;
for (;;) {
//从字节缓冲区ByteBuffer bb中解码尽可能多的字节,将结果写入给定的字符缓冲区CharBuffer cb中,也就是字符数组char[] cbuf中用于读取字符
CoderResult cr = decoder.decode(bb, cb, eof);
if (cr.isUnderflow()) {
if (eof)
break;
if (!cb.hasRemaining())
//检查字符缓冲区CharBuffer cb中是否还有剩余空间,若没有则跳出循环
break;
if ((cb.position() > 0) && !inReady())
//字符缓冲区CharBuffer cb存在内容时,跳出循环,以便返回读取到的字符数
break; // Block at most once
//读取字节到字节缓冲区ByteBuffer bb
int n = readBytes();
if (n < 0) {
//若读取字节到字节缓冲区ByteBuffer bb时返回了-1,代表着已到了字节输入流(或者叫文件)的末尾,则不会有其它字节输入了
eof = true;
if ((cb.position() == 0) && (!bb.hasRemaining()))
//如果此时字符缓冲区CharBuffer cb和字节缓冲区ByteBuffer bb中都没有数据了,则跳出循环
break;
}
continue;
}
if (cr.isOverflow()) {
//当从字节缓冲区ByteBuffer bb中解码的字节太多导致溢出时,字符缓冲区CharBuffer cb必须有解码的字符,否则这里的断言就会报错
assert cb.position() > 0;
//溢出时也跳出循环
break;
}
cr.throwException();
}
if (eof) {
// ## Need to flush decoder
//如果已经到了字节输入流(或者叫文件)的末尾,则重置解码器,清除所有内部状态。
decoder.reset();
}
if (cb.position() == 0) {
if (eof) {
return -1;
}
assert false;
}
//返回字符缓冲区CharBuffer cb的当前位置,代表着实际读取或者填充到字符数组char[] cbuf中的字符数量
return cb.position();
}
...省略部分代码...
}
如果使用者用的是InputStreamReader对象创建的StreamDecoder对象,并在InputStreamReader的构造函数中指定了字符集(解码方案)为GB2312,之后在循环调用InputStreamReader的read()函数来读取字符,如下所示:
package com.xxx.bio;
import java.io.*;
import java.nio.charset.Charset;
public class InputStreamReaderTest {
public static void main(String[] args) throws FileNotFoundException {
Charset defaultCharset = Charset.defaultCharset();
System.out.println("系统默认编解码字符集: " + defaultCharset.name());
try {
// 创建 FileInputStream 实例
FileInputStream fis = new FileInputStream("D:\\nio-data.txt");
// 创建 InputStreamReader 实例,使用GB2312字符集
InputStreamReader isr = new InputStreamReader(fis, Charset.forName("GB2312"));
System.out.println("InputStreamReader实例的编解码字符集:"+isr.getEncoding());
// 读取文件内容
int data;
while ((data = isr.read()) != -1) {
System.out.print((char) data);
}
// 关闭流
isr.close();
fis.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
我的windows操作系统的D盘根目录下有nio-data.txt文件,该文件使用的字符集(编码方案)为GB2312,内容如下:
程序运行结果,如下所示(这里用的JDK版本为graalvm17,不是JDK8,因为JDK8的sun.nio.cs包反编译之后会把变量名优化掉,导致可读性变差,但是不论是JDK8还是graalvm17,StreamDecoder的解码核心思路是相同的):
整个执行过程,分为以下7步:
①、先执行StreamDecoder.class::forInputStreamReader()这个工厂函数,构造一个StreamDecoder对象,如下所示:
public class StreamDecoder extends Reader {
...省略部分代码...
//提供给InputStreamReader对象创建自己这个StreamDecoder对象的工厂函数
public static StreamDecoder forInputStreamReader(InputStream in,
Object lock,
Charset cs)
{
return new StreamDecoder(in, lock, cs);
}
//构造函数
StreamDecoder(InputStream in, Object lock, Charset cs) {
this(in, lock,
cs.newDecoder()
.onMalformedInput(CodingErrorAction.REPLACE)
.onUnmappableCharacter(CodingErrorAction.REPLACE));
}
//构造函数
StreamDecoder(InputStream in, Object lock, CharsetDecoder dec) {
//设置Reader.class::lock变量指向的对象(锁)是创建自己这个StreamDecoder对象的InputStreamReader对象
super(lock);
this.cs = dec.charset();
this.decoder = dec;
this.in = in;
this.ch = null;
this.bb = ByteBuffer.allocate(DEFAULT_BYTE_BUFFER_SIZE);
bb.flip(); // So that bb is initially empty
}
StreamDecoder对象中的Charset cs、CharsetDecoder decoder(DoubleByte.class::Decoder.class类型)、ByteBuffer bb(容量为8192)、InputStream in(使用者创建的FileInputStream对象)、ReadableByteChannel ch(在InputStreamReader创建的StreamDecoder中为null)...等属性,分别如下:
ps:执行bb.flip()函数之前limit=8192,执行完bb.flip()函数之后limit=0
②、然后第1次进入使用者的while循环,执行InputStreamReader.class::read()函数,实际上执行的是StreamDecoder.class::read()函数,此步又分为以下3步:
a、先执行StreamDecoder.class::readBytes()函数向ByteBuffer bb(容量为8192)中填充10个字节,这10个字节对应"世界,你好"这5个GB2313编码的字符,如下所示:
b、利用
CoderResult cr = decoder.decode(bb, cb, eof);
这行代码将ByteBuffer bb中0,4)索引位置上的字节转换为2个字符"世"和"界",填充到给定的字符缓冲区CharBuffer cb中,也就是字符数组char\[ cbuf中用于读取字符,并修改ByteBuffer bb(容量为8192)的position(可以理解为左指针)=4,如下所示:
c、在read0()函数中的switch case代码块中先将字符数组char\[\] cbuf中的第2个字符(索引位置1)"界"缓存到char leftoverChar变量中,再返回字符数组char\[\] cbuf中的第1个字符(索引位置0),如下所示:
第1次进入使用者的while循环的流程图,如下所示:
③、接下来第2次进入使用者的while循环,执行InputStreamReader.class::read()函数,实际上执行的是StreamDecoder.class::read()函数,此时在上述步骤②的c步骤中有缓存的字符"界",直接返回,并修改boolean haveLeftoverChar = false,如下所示:
第2次进入使用者的while循环的流程图,如下所示:
④、接下来第3次进入使用者的while循环,执行InputStreamReader.class::read()函数,实际上执行的是StreamDecoder.class::read()函数,此步又分为以下2步:
a、利用
CoderResult cr = decoder.decode(bb, cb, eof);
这行代码将ByteBuffer bb中4,8)索引位置上的字节转换为2个字符","和"你",填充到给定的字符缓冲区CharBuffer cb中,也就是字符数组char\[ cbuf中用于读取字符,并修改ByteBuffer bb(容量为8192)的position(可以理解为左指针)=8,如下所示:
b、在read0()函数中的switch case代码块中先将字符数组char\[\] cbuf中的第2个字符(索引位置1)"界"缓存到char leftoverChar变量中,再返回字符数组char\[\] cbuf中的第1个字符(索引位置0),如下所示:
第3次进入使用者的while循环的流程图,如下所示:
⑤、接下来第4次进入使用者的while循环,执行InputStreamReader.class::read()函数,实际上执行的是StreamDecoder.class::read()函数,此时在上述步骤④的b步骤中有缓存的字符"你",直接返回,并修改boolean haveLeftoverChar = false,如下所示:
第4次进入使用者的while循环的流程图,如下所示:
⑥、接下来第5次进入使用者的while循环,执行InputStreamReader.class::read()函数,实际上执行的是StreamDecoder.class::read()函数,此步又分为以下3步:
a、利用
CoderResult cr = decoder.decode(bb, cb, eof);
这行代码将ByteBuffer bb中8,10)索引位置上的字节转换为1个字符"好",填充到给定的字符缓冲区CharBuffer cb的第1个位置(索引0)中,也就是字符数组char\[ cbuf的第1个位置(索引0)中用于读取字符(第2个索引1的位置仍然是初始化的值\u0000),并修改ByteBuffer bb(容量为8192)的position(可以理解为左指针)=10,如下所示:
b、再执行StreamDecoder.class::readBytes()函数之前,会执行
if ((cb.position() > 0) && !inReady())
//字符缓冲区CharBuffer cb存在内容时,跳出循环,以便返回读取到的字符数
break; // Block at most once
这段代码来检查字符缓冲区CharBuffer cb也就是字符数组char\[\] cbuf中是否已经填充过字符了,并检查字节输入流InputStream in(真正通过StreamDecoder转换成字符的字节都是从这个字节输入流中获取的,不过将这些字节转换成字符之前会先读取到ByteBuffer bb中缓存)中是否还有可以用的字节,此时发现字符缓冲区CharBuffer cb也就是字符数组char\[\] cbuf中已经填充过字符了并且字节输入流InputStream in已经没有可以用的字节了,则执行break跳出循环。
c、在read0()函数中的switch case代码块中直接返回字符数组char\[\] cbuf中的第1个字符(索引位置0),如下所示:
第5次进入使用者的while循环的流程图,如下所示:
⑦、接下来第6次进入使用者的while循环,执行InputStreamReader.class::read()函数,实际上执行的是StreamDecoder.class::read()函数,此步又分为以下x步:
a、执行
CoderResult cr = decoder.decode(bb, cb, eof);
的时候无法将ByteBuffer bb中的任何字节转换为字符并填充到给定的字符缓冲区CharBuffer cb中,也就是字符数组char\[\] cbuf中,然后执行
int n = readBytes();
函数时返回了-1,此时字节缓冲区ByteBuffer bb中的内容,如下所示:
虽然节缓冲区ByteBuffer bb的[0,10)索引位置仍然有值(这些值就是"世界,你好"的GB2312编码),但是节缓冲区ByteBuffer bb的position=limit=0,这表示这个节缓冲区ByteBuffer bb已经不能再使用了。
b、然后认为这个字节输入流InputStream in(或者叫文件)已经读取完了(没有字节可以提供使用了),并将StreamDecoder中真正用来将ByteBuffer bb中的字节解码成不同字符的工具CharsetDecoder decoder重置
decoder.reset();
然后一层一层的返回-1给使用者;
c、使用者的while循环在执行isr.read()的时候
while ((data = isr.read()) != -1) {
获得的返回值为-1,经过判断,跳出这个while循环,然后关闭所有的流。第6次进入使用者的while循环的流程图,如下所示:
3.1、怎样将GB2312编码的"世界,你好"是怎样填充到字节缓冲区ByteBuffer bb(默认容量为8192)中
GB2312是中国的汉字编码标准,收录了6763个汉字。它是一个双字节编码,编码空间是一个 94 区 × 94 位 的二维表格,实际取值范围为 0xA1~0xFE。比如某个字符的 区号为Q(1--94) , 位号为W(1--94),其 GB2312 双字节编码为:
- 第1个字节(高字节)B1 = Q + 0xA0
- 第2个字节(低字节)B2 = W + 0xA0
"世界,你好"中的"世"字在GB2312中的区位码是 42 区 32 位,按照GB2312 的双字节编码公式计算完成后"世"字对应的2个字节就是无符号数的 0xCA 0xC0,对应的二进制为11001010和11000000,而Java中的字节byte类型表示的是有符号数,取值范围为-128~127之间,对于任何超过127的数,如果要存储为Java中的byte类型,需要按照8 位有符号二进制补码规则来转换,拿11001010来举个例子,11001010的最高位1表示符号,其余低7位取反再加1就是11001010的补码,如下所示:
因此11001010的补码为10110110,表示为有符号数是-54,同理,11000000的补码为11000000,表示为有符号数就是-64。
"世界,你好"中的"界"字在GB2312中的区位码是 29 区 71 位,按照GB2312 的双字节编码公式计算完成后"界"字对应的2个字节就是无符号数的 0xBD 0xE7,对应的8位二进制为10111101和11100111,全都超过了127(Java中的字节byte类型,取值范围为-128~127之间),因此,10111101用补码表示为11000011,表示为有符号数是-67,11100111用补码表示为10011001,表示为有符号数是-25。
"世界,你好"中的","(中文逗号)在GB2312中的区位码是 3 区 12 位,按照GB2312 的双字节编码公式计算完成后","(中文逗号)对应的2个字节就是无符号数的 0xA3 0xAC,对应的8位二进制为10100011和10101100,全都超过了127(Java中的字节byte类型,取值范围为-128~127之间),因此,10100011用补码表示为11011101,表示为有符号数是-93,10101100用补码表示为11010100,表示为有符号数是-84。
"世界,你好"中的"你"字在GB2312中的区位码是 36 区 67 位,按照GB2312 的双字节编码公式计算完成后"你"字对应的2个字节就是无符号数的 0xC4 0xE3,对应的8位二进制为11000100和11100011,全都超过了127(Java中的字节byte类型,取值范围为-128~127之间),因此,11000100用补码表示为10111100,表示为有符号数是-60,11100011用补码表示为10011101,表示为有符号数是-29。
"世界,你好"中的"好"字在GB2312中的区位码是 26 区 35 位,按照GB2312 的双字节编码公式计算完成后"好"字对应的2个字节就是无符号数的 0xBA 0xC3,对应的8位二进制为10111010和11000011,全都超过了127(Java中的字节byte类型,取值范围为-128~127之间),因此,10111010用补码表示为11000110,表示为有符号数是-70,11000011用补码表示为10111101,表示为有符号数是-61。
综上所述,"世界,你好"在节缓冲区ByteBuffer bb中的存储,如下所示:
3.2、JDK8的编译器会优化read0()函数的代码顺序
在OpenJDK8的源码中,StreamDecoder.class::read0()函数的源码如下所示:
//@SuppressWarnings("fallthrough")可以压制(即消除)编译器对 case 2 分支缺少中断语句(return、break、throw...等)的报警
//函数内部做了线程同步,如果有足够字符的话,一次性从read(char[] cbuf, int offset, int length)函数中读取2个字符
@SuppressWarnings("fallthrough")
private int read0() throws IOException {
synchronized (lock) {
if (haveLeftoverChar) {
//如果char leftoverChar变量中有被赋值(缓存)的字符char,则直接返回,并把boolean haveLeftoverChar标记改为false表示char leftoverChar变量中有被赋值(缓存)的字符已经被读取了
haveLeftoverChar = false;
return leftoverChar;
}
// 创建了长度为2的字符数组char[] cb
char[] cb = new char[2];
// 调用下面的read(char[] cbuf, int offset, int length)函数,返回读取的字符个数
int n = read(cb, 0, 2);
switch (n) {
case -1:// 若read(char[] cbuf, int offset, int length)函数返回-1,代表读到末尾
return -1; //直接返回-1
case 2:// 若read(char[] cbuf, int offset, int length)函数返回2,代表读到了2个字符
leftoverChar = cb[1];//将char[] cb数组中第2个字符(索引1位置)放到char leftoverChar变量中暂时缓存
haveLeftoverChar = true;//并把boolean haveLeftoverChar标记改为true表示char leftoverChar变量中有被赋值(缓存)的字符
// FALL THROUGH
case 1:// 若read(char[] cbuf, int offset, int length)函数返回1,代表读到了1个字符
return cb[0];//实际执行的时候,如果read(char[] cbuf, int offset, int length)函数的返回值为2,char[] cb数组中有2个字符,这里会被case2穿透,直接返回char[] cb数组中的第1个字符(索引0位置)
//详情看小标题x.x.x
default:
assert false : n;//实际编译后,这里会被编译器优化到case2之前,详情看小标题x.x.x
return -1;
}
}
}
OpenJDK8的StreamDecoder.class::read0()函数经过编译后被OracleJdk8中的编译器修改了一些内容,如下所示(OracleJdk8的编译器会优化和调整case的顺序):
//OracleJdk8的编译器修改的变量名笔者已经恢复,不再是编译器编译后的varX这种变量命名方式(X表示1、2、3...)
private int read0() throws IOException {
synchronized(this.lock) {
if (this.haveLeftoverChar) {
this.haveLeftoverChar = false;
return this.leftoverChar;
} else {
char[] cb = new char[2];
int n = this.read(cb, 0, 2);
switch (n) {
case -1:
return -1;
case 0: //主要的修改点在这里,OpenJDK8的源码中并没有case 0这个场景的考虑
default: //OpenJDK8的源码中default的逻辑在case 1这个场景之后,也就是switch case结构的末尾
assert false : n;
return -1;
case 2:
this.leftoverChar = cb[1];
this.haveLeftoverChar = true;
case 1:
return var2[0];
}
}
}
}
类似于上述中的这种OracleJdk的编译器修改OpenJDK源码的场景,在OracleJdk17以后(包括17这个版本)就没有发生了,以下是OracleJdk17中的StreamDecoder.class::read0()函数,如下所示:
@SuppressWarnings("fallthrough")
private int read0() throws IOException {
synchronized (lock) {
// Return the leftover char, if there is one
if (haveLeftoverChar) {
haveLeftoverChar = false;
return leftoverChar;
}
// Convert more bytes
char[] cb = new char[2];
int n = read(cb, 0, 2);
switch (n) {
case -1:
return -1;
case 2:
leftoverChar = cb[1];
haveLeftoverChar = true;
// FALL THROUGH
case 1:
return cb[0];
default:
assert false : n;
return -1;
}
}
}
笔者认为,OracleJdk17中的StreamDecoder.class::read0()函数包括StreamDecoder.class的整个类的源码都没有在运行前被编译,是因为及时编译技术的发展,jvm在在运行前已经不需要将所有.java文件先编译成.class文件。但是OracleJdk8中的编译器对OpenJDK8中源码(包括但不限于StreamDecoder.class::read0()函数)进行编译优化时,是怎样的思维架构和算法逻辑,仍然需要深究,此处篇幅有限,不再赘述。