一、StreamEncoder流
该类源码的下载地址(注意源码中的注释为个人理解并非官方注释)
https://download.csdn.net/download/ai_bao_zi/10560419
二、StreamEncoder中的实例域
//默认的字节缓冲区大小,8192个字节
private static final int DEFAULT_BYTE_BUFFER_SIZE =8192;
//确保流打开以便可以进行输出和输入
private volatile boolean isOpen = true;
//每次输出前都确保流是打开的状态
private void ensureOpen() throws IOException
{
if (!isOpen)
throw new IOException("Stream closed");
}
private boolean isOpen()
{
return isOpen;
}
//字符集即我们设置的"utf-8"这种
private Charset cs;
//字符编码器
private CharsetEncoder encoder;
//字节缓冲区
private ByteBuffer bb;
//底层输出流
private final OutputStream out;
//写入信道
private WritableByteChannel ch;
//为了保证读入的字符不乱码 则每次读入不能少于两个字符
private boolean haveLeftoverChar = false;
//左侧字符
private char leftoverChar;
//字符缓冲区对象--专用于左侧字符操作
private CharBuffer lcb = null;
三、StreamEncoder流的构造方法
//构造方法初始化实例域
private StreamEncoder(OutputStream out, Object lock, Charset cs)
{
this(out, lock, cs.newEncoder()
.onMalformedInput(CodingErrorAction.REPLACE)
.onUnmappableCharacter(CodingErrorAction.REPLACE));
}
//构造方法初始化实例域--注意此处有点疑问,不管ch是否为null都会设置字节缓冲区的大小,感觉是否最后if有点多余
@SuppressWarnings("unused")
private StreamEncoder(OutputStream out, Object lock, CharsetEncoder enc)
{
super(lock);
this.out = out;
this.ch = null;
this.cs = enc.charset();
this.encoder = enc;
//这个if的条件不可能为真,永远不进入该条件中
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);
}
}
四、StreamEncoder流与OutputStreamWriter的联系详解
1)OutputStreamWriter的构造方法都是调用了StreamEncoder的forOutputStreamWriter方法,那么我们细看下forOutputStreamWriter方法
forOutputStreamWriter本质:利用底层的FileOutputStream流和字符集名称来调用StreamEncoder的构造方法创建StreamEncoder对象,因此可以看出OutputStreamWriter的构造方法就是创建StreamEncoder的对象然后赋予给其中的final常量引用
//初始化对象StreamEncoder
public static StreamEncoder forOutputStreamWriter(OutputStream out,
Object lock, String charsetName)
throws UnsupportedEncodingException
{
String csn = charsetName;
//当字符集未为null时,使用系统默认的字符集即"utf-8"
if (csn == null)
csn = Charset.defaultCharset().name();
try
{
//检测设置的字符集是否为JAVA所支持
if (Charset.isSupported(csn))
return new StreamEncoder(out, lock, Charset.forName(csn)); //利用字符集名称创建字符编码器
}
catch (IllegalCharsetNameException x)
{
}
throw new UnsupportedEncodingException(csn);
}
2)OutputStreamWriter中的三种write方法都是调用了StreamEncoder对象的write方法,那么我们实际看下StreamEncoder的write方法
本质:可以看出所有的write方法最终都会调用implWrite(cbuf, off, len)方法,因此所有的输出方法的细节都在此方法中实现
//write方法,调用了write(char cbuf[], int off, int len)方法
public void write(int c) throws IOException
{
char cbuf[] = new char[1];
cbuf[0] = (char) c;
write(cbuf, 0, 1);
}
//write(String str, int off, int len)方法,调用了write(char cbuf[], int off, int len)方法
public void write(String str, int off, int len) throws IOException
{
if (len < 0)
throw new IndexOutOfBoundsException();
char cbuf[] = new char[len];
str.getChars(off, off + len, cbuf, 0);
write(cbuf, 0, len);
}
//write(char cbuf[], int off, int len)方法调用了implWrite(cbuf, off, len)方法
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;
}
implWrite(cbuf, off, len);
}
}
3)implWrite(cbuf, off, len)方法的实现细节如下:
1、先将目标字符数组通过CharBuffer类的方法得到CharBuffer的字符缓冲区对象 ,该对象的hasRemaining()方法用于判断字符缓冲区中是否还有元素,而remaining()方法则会返回缓冲区中剩余的元素数
CharBuffer cb = CharBuffer.wrap(cbuf, off, len); // 将字符数组包装到缓冲区中,得到一个缓冲区对象
2、通过字符编码器CharsetEncoder把字符缓冲区中的字符进行编码成字节,然后存入到字节缓冲区中ByteBuffer最终返回终止原因的编码结果对象CoderResult,该对象有三种情况,一种是UNDERFLOW代表着字符缓冲区内容都编码完毕存入到字节缓冲区了,一种是OVERFLOW代表着字节缓冲区空间不足,没有存完编码后的内容,还有一种就是遇见异常
//从给定的缓冲区对象中编码尽可能多的字符,把结果(字节)写入给定的输出缓冲区并返回终止原因的对象,false对象代表有可能提供其他输入
CoderResult cr = encoder.encode(cb, bb, false);
3、 判定编码结果对象CoderResult的状态进行不同操作,若字符缓冲区CharBuffer已全部编码完毕存入字节缓冲区ByteBuffer,则跳出当前循环则目标数据已全部存在字节缓冲区中,但是还并未到计算机哦。若字符缓冲区CharBuffer内容没有编码完毕,字节缓冲区ByteBuffer中空间不足,只存了一部分,则调用 writeBytes()方法--该方法的作用就是把字节缓冲区的内容通过底层字节流输出到计算机并清空缓冲区。而后有再次循环处理,直到字符缓冲区内容已全部编码完毕为止。最后一次编码后的内容存在字节缓冲区中,但是并没有到计算机这里要注意
//最后的执行方法
public 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()) //判断缓冲区中是否还有元素
{
//从给定的缓冲区对象中编码尽可能多的字符,把结果(字节)写入给定的输出缓冲区并返回终止原因的对象,false对象代表有可能提供其他输入
CoderResult cr = encoder.encode(cb, bb, false);
if (cr.isUnderflow()) //判断是否下溢即判断输入是否已完毕,若没有进一步的输入则执行其他操作,若有的话则继续调用encode方法进行输入
{
assert (cb.remaining() <= 1) : cb.remaining(); //此处采用了断言机制进行参数判断缓冲区中当前位置与限制之间的元素数是否小于1
if (cb.remaining() == 1) //代表复缓冲区中还存在一个字符
{
haveLeftoverChar = true; //下次不读入
leftoverChar = cb.get(); //得到最后的一个剩余字符
}
break;
}
if (cr.isOverflow()) //判断输出缓冲区是否没有空间了
{
assert bb.position() > 0; //断言机制判断
writeBytes();
continue;
}
cr.throwException();
}
}
4) writeBytes()方法实现细节如下:
本质:利用了最开始构造OutputStreamWriter对象所使用到的底层字节输出流FileOutputStream把字节缓冲区的内容给输出出去然后清空字节流
//最后的输出字节方法
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); //调用底层的FileOutputStream流进行字节数组输出
}
}
bb.clear(); //清空缓冲字节流
}
5) OutputStreamWriter的colse方法实际是调用StreamEncoder的colse方法,因此查看StreamEncoder的colse方法的实现
1、调用colse方法实际上调用implClose方法在执行
2、 implClose方法中的 flushLeftoverChar(null, true)主要是为了使单独出现一个左字符的情况的时候把字符存入到字节中(要结合write方法来看)---但是这个情况暂时未遇到
3、 最后刷新编码器,确保所有的字节都是存储到了字节缓冲区中
4、 最终调用writeBytes()方法将字节输出到计算机上
5、 因此最终发挥作用输出数据到计算机的方法是writeBytes()
//关闭资源--关闭资源之前会把字节缓冲区的内容给输出掉
public void close() throws IOException
{
synchronized (lock)
{
if (!isOpen)
return;
implClose();
isOpen = false;
}
}
void implClose() throws IOException
{
flushLeftoverChar(null, true); //调用方法看是否剩余一个单独的左字符然后存入到字节缓冲区中
try
{
for (;;)
{
CoderResult cr = encoder.flush(bb); //刷新编码器,确保字节全部到了输出字节缓冲区中
if (cr.isUnderflow()) //代表方法成功
break;
if (cr.isOverflow()) //若字节缓冲空间不足则调用writeBytes输出到计算机,然后再次刷新编码器
{
assert bb.position() > 0;
writeBytes();
continue;
}
cr.throwException();
}
if (bb.position() > 0)
writeBytes(); //将最后的缓冲区的字节全部通过字节流输出
if (ch != null)
ch.close();
else
out.close();
}
catch (IOException x)
{
encoder.reset();
throw x;
}
}
五、再次理解StreamEncoder、OutputStreamWriter、FileWriter
根据三个类的实际源码的实现,对之前FileWriter类和OutputStreamWriter类的API说明有了更深入的理解
1、FileWriter流中的API解释提到此类的构造函数假定默认字符编码和默认字节缓冲区大小是可接受的,默认的是在哪个类出现的?
FileWriter流中的构造函数调用的是父类OutputStreamWriter构造函数;且父类OutputStreamWriter构造函数本质是StreamEncoder类的forOutputStreamWriter方法,
默认字符编码是方法中的这行代码:
if (csn == null)
csn = Charset.defaultCharset().name();
默认字节缓冲区大小是方法的这行代码:
bb = ByteBuffer.allocate(DEFAULT_BYTE_BUFFER_SIZE);
2、FileWriter流中要自己指定这些值,请在FileOutputStream上构造OutputStreamWriter?指定这些值是指字符编码和字节缓冲区大小?
指定值只是指字符编码可以通过OutputStreamWriter的构造函数中传递字符编码字符串作为参数,而字节缓冲区大小是无法指定的,只能使用默认大小
//构造OutputStreamWriter函数,使用字符编码字符串作为参数
public OutputStreamWriter(OutputStream out, String charsetName)
throws UnsupportedEncodingException
{
super(out);
if (charsetName == null)
throw new NullPointerException("charsetName");
se = StreamEncoder.forOutputStreamWriter(out, this, charsetName);
}
3、OutputStreamWriter类中API说明提到它使用的字符集可以通过名称指定,也可以明确指定,或者可以接受平台的默认字符集?
这里就跟上述FileWriter流的解释一样的,指定则使用构造函数把字符编码字符串作为参数传递进去,若使用默认,则不传递参数即可
4、OutputStreamWriter类中每次调用write()方法都会导致在给定字符上调用编码转换器且生成的字节在写入底层输出流之前在缓冲区中累积?
OutputStreamWriter类的write()方法本质是StreamEncoder的implWrite(cbuf, off, len)方法,该方法每调用一次就必定会执行一次编码器编码字符为字节的代码,因此每次调用write()方法都会调用编码转换器
调用编码转换器后会把字符缓冲区的字符编码成字节,然后存入到字节缓冲区中,待最后调用close方法时,在写入到底层输入流中
CharBuffer cb = CharBuffer.wrap(cbuf, off, len); // 将字符数组包装到缓冲区中,得到一个缓冲区对象
CoderResult cr = encoder.encode(cb, bb, false); //从给定的缓冲区对象中编码尽可能多的字符,把结果(字节)写入给定的输出缓冲区并返回终止原因的对象,false对象代表有可能提供其他输入
5、OutputStreamWriter类API提到可以指定此缓冲区的大小,但默认情况下,它足够大,可用于大多数用途?
这里感觉存在误区,除非是重写StreamEncoder类,否则无法指定字节缓冲区的大小,正常情况不会去重写该类
默认情况下字节缓冲区的大小是8192,可以适应大部分情况。但是根据实际效果测试,该缓冲区大小无论是否满足,都会保证最后的输出效果,只是缓存不足时会内部多循环几次处理而已
6、 OutputStreamWriter中API提到请注意,传递给write()方法的字符不会被缓冲?
这里感觉也存在误区,传递到write方法里面的字符,无论是单个,还是字符数组,还是字符串,最后的形式都会变成字符数组在implWrite(char cbuf[], int off, int len)方法中执行,都会被存入到字节缓冲区中去的,因此会被缓冲
7、OutputStreamWriter中API提到为了获得最高效率,请考虑在BufferedWriter中包装OutputStreamWriter,以避免频繁的转换器调用?
从这句话可以理解出编码器编码是很耗资源和效率的(原因暂时未知),但根据多次的方法执行,OutputStreamWriter类若要输出的字符经过默认或指定字符集编码后的字节数不超过8192个,那么只会调用一次编码器,就可以实现全部编码字节存入到字节缓冲区。若字节数超过了8192个,则会调用字节数/8192次的编码转换器
因此需要查看下BufferedWriter的源码来理解BufferedWriter类的write方法是怎么实现的来看是否避免了第二种多次调用情况以提高效率
8、使用OutputStreamWriter类或者FileWrite类进行字符输出的时候,务必调用close方法,一方面是关闭资源释放链接,另一方面close方法会把缓冲字节区的内容给输出到底层字节输出流,才可以保证数据输出到了计算机文件中。若不调用close方法,则会出现内容丢失或者全无的情况
9、OutputStreamWriter或者FileWrite类输出字符流,实际是StreamEncoder流发挥作用,结合第7点来看,因此若要使用字符输出流输出字符,那么我们可以使用BufferedWriter类来操作,而不是FileWrite类或者OutputStreamWriter类,这两个类实际就是傀儡性质