首页 文章 精选 留言 我的

精选列表

搜索[移动解析],共10000篇文章
优秀的个人博客,低调大师

Java FileWriter OutputStreamWriter类源码解析

FileWriter 因为篇幅原因,上一篇直接了字符输入流,今天来分析一下跟FileReader相对应的字符输出流FileWriter。FileWriter是将字符写入文件的通用类,构造函数假定使用默认的字符编码和默认的字节缓冲区大小8K是使用者可以接受的,如果要指定这些值,需要通过一个FileOutputStream来构造FileWriter的父类OutputStreamWriter。 文件是否有效或者是否能够被创建取决于平台,在一些平台上,对于同一个文件同一时间只允许一个FileWriter或者其他文件写入对象打开。在这种情况下,如果一个文件已经被打开,构造函数会抛出异常。 和FileReader类似,FileWriter也是除了构造函数以外全部是继承了父类的方法。先创建一个FileOutputStream,如果不给出append参数或者append为false则清空原文件从头开始写入,否则是从尾部开始扩展文件内容,使用文件描述符创建时必定是从文件头部开始写。然后通过FileOutputStream创建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)); } OutputStreamWriter OutputStreamWriter继承了抽象类Writer,跟OutputStreamReader类似,主要重写的方法都是基于StreamEncoder来完成,StreamEncoder在构造函数中通过工厂方法构造。 在构造函数中存在super(out),作用是构造父类Writer,将OutputStream作为加锁的对象 //指定字符集名字 public OutputStreamWriter(OutputStream out, String charsetName) throws UnsupportedEncodingException { super(out); if (charsetName == null) throw new NullPointerException("charsetName"); se = StreamEncoder.forOutputStreamWriter(out, this, charsetName);//传给StreamEncoder的加锁对象是OutputStreamWriter对象自身 } //使用默认字符集 public OutputStreamWriter(OutputStream out) { super(out); try { se = StreamEncoder.forOutputStreamWriter(out, this, (String)null); } catch (UnsupportedEncodingException e) { throw new Error(e); } } //使用指定的字符集 public OutputStreamWriter(OutputStream out, Charset cs) { super(out); if (cs == null) throw new NullPointerException("charset"); se = StreamEncoder.forOutputStreamWriter(out, this, cs); } //使用指定的CharsetEncoder public OutputStreamWriter(OutputStream out, CharsetEncoder enc) { super(out); if (enc == null) throw new NullPointerException("charset encoder"); se = StreamEncoder.forOutputStreamWriter(out, this, enc); } OutputStreamWriter重写了Writer中的write, flush, close,此外Writer中的append方法最终也是基于write来完成的,这些方法都是直接调用StreamEncoder中的对应方法。 //写入单个字符 public void write(int c) throws IOException { se.write(c); } //写入一部分字符数组 public void write(char cbuf[], int off, int len) throws IOException { se.write(cbuf, off, len); } //写入一部分字符串 public void write(String str, int off, int len) throws IOException { se.write(str, off, len); } //刷新 public void flush() throws IOException { se.flush(); } //关闭输出流 public void close() throws IOException { se.close(); } getEncoding和flushBuffer是OutputStreamWriter相比于Writer新增的两个调用StreamEncoder中的中间接口 //返回字符集的名称 public String getEncoding() { return se.getEncoding(); } //只有PrintStream这样的类可以调用这个方法,将输出缓冲区的内容刷新到字节流中,但是字节流不会刷新到文件中 void flushBuffer() throws IOException { se.flushBuffer(); } StreamEncoder 跟上一篇一样,要分析字符输出流关键还是要分析StreamEncoder,很多方法可以对照StreamDecoder进行比较,它们从设计上除了输入和输出外是相近的。StreamEncoder同样一次至少操作两个字符,避免出现代替对,也就是2个字节码表示一个字符的特殊情况,当然前面提到过,这种通常是一些机器或者数学上的特殊符号,键盘输入是不会出现的。StreamEncoder继承了抽象类Writer,并重写了其中的write、flush、close方法。 StreamEncoder的构造函数也是private函数,外部只能通过工厂方法类调用 private Charset cs; private CharsetEncoder encoder; private ByteBuffer bb; // Exactly one of these is non-null至少有一个不为null private final OutputStream out; private WritableByteChannel ch; // Leftover first char in a surrogate pair代理对中剩下的第一个字符 private boolean haveLeftoverChar = false; private char leftoverChar; private CharBuffer lcb = null; private StreamEncoder(OutputStream out, Object lock, Charset cs) { this(out, lock, cs.newEncoder().onMalformedInput(CodingErrorAction.REPLACE)// 有畸形输入错误时解码器丢弃错误的输入,替换为替代值然后继续后面的操作 .onUnmappableCharacter(CodingErrorAction.REPLACE));// 有不可用图形表示的字符错误出现时解码器丢弃错误的输入,替换为替代值然后继续后面的操作 } private StreamEncoder(OutputStream out, Object lock, CharsetEncoder enc) { super(lock);// lock是OutputStream对象本身 this.out = out; this.ch = null; this.cs = enc.charset(); this.encoder = enc; // 在堆外内存速度更快之前不使用这段代码 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);//分配一个8K的堆内ByteBuffer } } private StreamEncoder(WritableByteChannel ch, CharsetEncoder enc, int mbc) { this.out = null; this.ch = ch; this.cs = enc.charset(); this.encoder = enc; this.bb = ByteBuffer.allocate(mbc < 0 ? DEFAULT_BYTE_BUFFER_SIZE : mbc);//分配一个大小mbc的堆内ByteBuffer } 在看工厂方法,这里传入的lock对象是OutputStreamWriter本身 // java.io.OutputStreamWriter工厂模式 public static StreamEncoder forOutputStreamWriter(OutputStream out, Object lock, String charsetName) throws UnsupportedEncodingException { String csn = charsetName; if (csn == null) csn = Charset.defaultCharset().name(); try { if (Charset.isSupported(csn)) return new StreamEncoder(out, lock, Charset.forName(csn)); } catch (IllegalCharsetNameException x) { } throw new UnsupportedEncodingException(csn); } public static StreamEncoder forOutputStreamWriter(OutputStream out, Object lock, Charset cs) { return new StreamEncoder(out, lock, cs); } public static StreamEncoder forOutputStreamWriter(OutputStream out, Object lock, CharsetEncoder enc) { return new StreamEncoder(out, lock, enc); } // java.nio.channels.Channels.newWriter工厂模式 public static StreamEncoder forEncoder(WritableByteChannel ch, CharsetEncoder enc, int minBufferCap) { return new StreamEncoder(ch, enc, minBufferCap); } 内部属性isOpen标记流当前是否打开,在关闭时会被设置为close,实际操作方法都会先检查流是否开启,否则会抛出异常 private volatile boolean isOpen = true; private void ensureOpen() throws IOException { if (!isOpen) throw new IOException("Stream closed"); } private boolean isOpen() { return isOpen; } getEncoding返回字符集的历史名,没有的话返回官方名,这里的名字可能和构造时传入的有不同 public String getEncoding() { if (isOpen()) return encodingName(); return null; } String encodingName() { return ((cs instanceof HistoricallyNamedCharset) ? ((HistoricallyNamedCharset) cs).historicalName() : cs.name()); } 接下来分析write部分,通过将字符编码为字节放入ByteBuffer中,然后通过文件通道或者输出流进行输出 public void write(int c) throws IOException { char cbuf[] = new char[1]; cbuf[0] = (char) c; write(cbuf, 0, 1); } //这个是实际调用写入的方法 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); } } 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);// 将str中的value从off开始长度len的内容复制到cbuf中 write(cbuf, 0, len); } void implWrite(char cbuf[], int off, int len) throws IOException { CharBuffer cb = CharBuffer.wrap(cbuf, off, len);// 将字符数组组装成一个堆内CharBuffer,数组中的内容不存在复制 if (haveLeftoverChar) flushLeftoverChar(cb, false);//如果有的话,将leftoverChar写入输出流 while (cb.hasRemaining()) { CoderResult cr = encoder.encode(cb, bb, false);//将字符编码为二进制字节直到ByteBuffer满或者CharBuffer中没有更多内容 if (cr.isUnderflow()) {//ByteBuffer没有满,说明CharBuffer内的内容全部编码完成 assert (cb.remaining() <= 1) : cb.remaining(); if (cb.remaining() == 1) { //如果当前缓冲区仅剩一个字符,保存到leftoverChar并修改haveLeftoverChar状态,结束输出 haveLeftoverChar = true; leftoverChar = cb.get(); } break; } if (cr.isOverflow()) {//ByteBuffer满了 assert bb.position() > 0; writeBytes();//将ByteBuffer的内容写入到输出流里面 continue; } cr.throwException(); } } private void writeBytes() throws IOException { bb.flip();//ByteBuffer准备输出当前内容,将limit设为当前位置,pos设为0 int lim = bb.limit(); int pos = bb.position(); assert (pos <= lim); int rem = (pos <= lim ? lim - pos : 0); if (rem > 0) {//输出ByteBuffer中全部内容 if (ch != null) { if (ch.write(bb) != rem) assert false : rem; } else { out.write(bb.array(), bb.arrayOffset() + pos, rem); } } bb.clear();//清空ByteBuffer } implWrite调用了内部方法flushLeftoverChar,作用是将缓存的字符写入输出流,这个方法同时在close时也被调用,因为leftoverChar只有一个字符,而一次输出至少是两个字符,所以还要从cb中读取字符,保证写入的是2个字符 private void flushLeftoverChar(CharBuffer cb, boolean endOfInput) throws IOException { if (!haveLeftoverChar && !endOfInput) return; if (lcb == null)//lcb内一开始是空的 lcb = CharBuffer.allocate(2); else lcb.clear(); if (haveLeftoverChar) lcb.put(leftoverChar);//将leftoverChar放入lcb中 if ((cb != null) && cb.hasRemaining()) lcb.put(cb.get());//cb的内容复制到lcb中 lcb.flip();//将limit设为当前位置,pos设为0,所以现在lcb要输出的内容就是刚才从leftoverChar(如果有的话)和cb里读入的 while (lcb.hasRemaining() || endOfInput) { CoderResult cr = encoder.encode(lcb, bb, endOfInput);//将lcb的内容编码为字节尽可能多的放入到ByteBuffer中 if (cr.isUnderflow()) {//cr未溢出,ByteBuffer还有剩余的空间 if (lcb.hasRemaining()) {//lcb还有剩余的数据 leftoverChar = lcb.get(); if (cb != null && cb.hasRemaining()) flushLeftoverChar(cb, endOfInput); return; } break; } if (cr.isOverflow()) {//cr溢出,超出了ByteBuffer的上限 assert bb.position() > 0;//ByteBuffer中存在数据 writeBytes();//将ByteBuffer中的数据写入到输出流后清空ByteBuffer continue; } cr.throwException(); } haveLeftoverChar = false; } flushBuffer()和flush()将ByteBuffer中的数据写入输出流,flush()同时还会进行输出流的刷新,具体操作取决于输出流的实现,比如FileOutputStream是什么也不做,因为没有缓冲区。 public void flushBuffer() throws IOException { synchronized (lock) { if (isOpen()) implFlushBuffer(); else throw new IOException("Stream closed"); } } public void flush() throws IOException { synchronized (lock) { ensureOpen(); implFlush(); } } void implFlush() throws IOException { implFlushBuffer(); if (out != null) out.flush();//这里out的刷盘操作取决于子类的具体实现 } void implFlushBuffer() throws IOException { if (bb.position() > 0)// 如果ByteBuffer内还有剩余的数据,将它们写入文件 writeBytes(); } close()同样也只能关闭一次,并且是线程同步方法,在关闭之前需要先将缓冲区的内容全部输入到输入流中 public void close() throws IOException { synchronized (lock) { if (!isOpen) return; implClose(); isOpen = false;//只能关闭一次 } } void implClose() throws IOException { flushLeftoverChar(null, true);//将leftoverChar的内容写入的输出流 try { for (;;) { CoderResult cr = encoder.flush(bb); if (cr.isUnderflow())//cr未溢出说明ByteBuffer中的数据全部写入到输入流了 break; if (cr.isOverflow()) {//cr溢出说明ByteBuffer仍然存在数据 assert bb.position() > 0; writeBytes();//将ByteBuffer中的数据写入输出流 continue; } cr.throwException(); } if (bb.position() > 0) writeBytes(); if (ch != null)//关闭文件通道或者输出流 ch.close(); else out.close(); } catch (IOException x) { encoder.reset(); throw x; } } 总结 基于OutputStreamWriter和StreamEncoder来实现字符输出时,保证字符编码为字节后输入到输入流中的操作是可靠的,并且直到最后一个字符前一次会传递成对的字符来解决代理对的问题,后续的部分由输入流来完成,而OutputStreamWriter在通过FileWriter构建时是基于FileOutputStream,也就是没有缓冲区,有多少内容就写多少。

优秀的个人博客,低调大师

Java FileInputStream FileOutputStream类源码解析

FileInputStream和FileOutputStream是匹配的文件输出输出流,读取和写入的是byte,所以适合用来处理一些非字符的数据,比如图片数据。因为涉及到大量关于文件的操作,所以存在很多的native方法和利用操作系统的文件系统实现,所以要深入了解文件输入输出流还是需要加强操作系统和native源码的知识。先来看一下简单的使用示例: public static void main(String args[]) throws IOException{ FileOutputStream out = new FileOutputStream("D:/test/file.txt"); out.write("1234567890".getBytes());//1234567890 out.close(); out = new FileOutputStream("D:/test/file.txt"); out.write("765".getBytes());//765 out.close(); out = new FileOutputStream("D:/test/file.txt", true); out.write("asdfgh".getBytes());//765asdfgh out.close(); new File("D:/test/file.txt");//765asdfgh out = new FileOutputStream("D:/test/file.txt"); out.close();//内容为空 //测试filechannel位置改变对stream的影响 out = new FileOutputStream("D:/test/file.txt"); out.write("1234567890".getBytes());//1234567890 out.close(); FileInputStream in = new FileInputStream("D:/test/file.txt"); System.out.print(String.valueOf((byte)in.read() & 0xf));//1 System.out.print(String.valueOf((byte)in.read() & 0xf));//2 System.out.print(String.valueOf((byte)in.read() & 0xf));//3 in.getChannel().position(5); System.out.print(String.valueOf((byte)in.read() & 0xf));//6 in.getChannel().position(0); System.out.print(String.valueOf((byte)in.read() & 0xf));//1 in.skip(-1);//向前跳一位 System.out.print(String.valueOf((byte)in.read() & 0xf));//1 } FileInputStream 实现了抽象类InputStream,FileInputStream从文件系统中的文件获取bytes,文件是否有效取决于主机环境。FileInputStream对于直接从流中读取bytes数据非常有意义如图像数据。如果要读取字符信息,考虑使用FileReader。我们可以看到除了close以外,没有出现任何锁,但是实际上如果我们使用多个线程读取同一个文件时,单次读取是原子操作,见下方代码: package test; import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.IOException; public class FileThreadTest implements Runnable { private int type;// 0做skip操作,1做读取操作 private int gap; private FileInputStream in; public FileThreadTest(int type, FileInputStream in, int gap) { this.type = type; this.in = in; this.gap = gap; } @Override public void run() { byte[] body = new byte[gap]; if (this.type == 0) { try { for(int i = 0; i < 4; i++) { in.skip(gap); } } catch (IOException e) { e.printStackTrace(); } } else { try { for(int i = 0; i < 10; i++) { in.read(body); System.out.println(Thread.currentThread().getName() + "-" + new String(body)); } } catch (IOException e) { e.printStackTrace(); } } } public static void main(String args[]) throws IOException, InterruptedException { FileOutputStream out = new FileOutputStream("D:/test/file.txt"); for (int i = 0; i < 1000; i++) { out.write("1234567890".getBytes());// 写入测试数据 } out.close(); FileInputStream in = new FileInputStream("D:/test/file.txt"); FileThreadTest t1 = new FileThreadTest(0, in, 2); FileThreadTest t2 = new FileThreadTest(1, in, 4); FileThreadTest t3 = new FileThreadTest(1, in, 3); Thread thread1 = new Thread(t1,"线程1"); Thread thread2 = new Thread(t2,"线程2"); Thread thread3 = new Thread(t3,"线程3"); thread2.start(); thread3.start(); thread1.start(); thread1.join(); thread2.join(); thread3.join(); in.close(); /* 线程3-567 线程3-678 线程2-1234 线程3-901 线程3-678 线程2-2345 线程2-2345 线程2-6789 线程2-0123 线程2-4567 线程2-8901 线程2-2345 线程2-6789 线程3-901 线程3-456 线程3-789 线程2-0123 线程3-012 线程3-345 线程3-678 */ } } 运行结果试机器配置每次运行会有所不同,但是我们可以看到无论怎么运行,每一次read操作本身是不会被其他线程抢占而中断的,它一定会完整的读取到这次要读取的内容,但是由于其他线程可以改变输入流的位置,所以每个线程读取时开始的位置是不可预知的,每个线程的read和skip操作都会改变流的位置。 先来看下内部变量,一个文件输出流有文件路径名、文件描述符、文件通道属性,若由文件描述符来创建,则文件路径名为null /* 文件描述符,用来打开文件*/ private final FileDescriptor fd; /** * 引用文件的路径,如果流是通过文件描述符创建时该值为null */ private final String path; private FileChannel channel = null; //用于保证close操作的线程安全性 private final Object closeLock = new Object(); private volatile boolean closed = false; 然后是构造函数,主要分为通过文件路径名、文件描述符、具体文件,通过文件描述符创建时文件路径名为null /** * 通过打开一个连接实际文件的连接来创建一个FileInputStream,文件通过文件系统中的路径名name来命名。 * 一个新的文件描述符对象会被创建来表示这个文件连接 * 如果存在安全管理器,checkRead方法会被调用,name作为参数传入 * 如果文件名不存在,或者是一个目录而不是规则文件,或者因为其他原因无法打开读取,抛出FileNotFoundException */ 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);//name是file的路径名 SecurityManager security = System.getSecurityManager(); if (security != null) { security.checkRead(name);//检查是否对文件有读取权限 } if (name == null) { throw new NullPointerException(); } if (file.isInvalid()) { throw new FileNotFoundException("Invalid file path"); } fd = new FileDescriptor(); fd.attach(this);//绑定文件描述符便于关闭对象 path = name; open(name);//打开文件 } /** * 通过文件描述符fdObj来创建FileInputStream,代表了对一个文件系统中实际存在文件的连接 * 如果FileInputStream是null会抛出NullPointerException * 如果fdObj是无效的,构造器不会抛出异常,但是,如果调用这个流的IO方法,会抛出IOException */ public FileInputStream(FileDescriptor fdObj) { SecurityManager security = System.getSecurityManager(); if (fdObj == null) { throw new NullPointerException(); } if (security != null) { security.checkRead(fdObj); } fd = fdObj; path = null; //文件描述符被流共享,将这个流注册到文件描述符的追踪器 fd.attach(this); } 文件本身需要打开才能进行操作,open只能由构造函数来调用,最终是由native方法open0来完成的系统的交互 private void open(String name) throws FileNotFoundException { open0(name); } private native void open0(String name) throws FileNotFoundException; 读取根据参数重载分为读取单个字节和读取字符数组,它们分别基于native方法read0和readBytes,前面的测试中,我们可以看到被多个线程共享的FileInputStream依然能够保证单次的read操作读取的信息是完整的,这应该与readBytes的实现有关 //从流中读取byte,如果没有有输出没有完成会阻塞方法 public int read() throws IOException { return read0(); } //从流中读取最大b.length的bytes数据到数组b中。该方法会被阻塞知道某些输入完成 public int read(byte b[]) throws IOException { return readBytes(b, 0, b.length); } //从流中读取长度为len的数据,放到b中从off开始的位置 public int read(byte b[], int off, int len) throws IOException { return readBytes(b, off, len); } private native int read0() throws IOException; private native int readBytes(byte b[], int off, int len) throws IOException; skip也是native方法跳过并废弃输入流中n个bytes数据,skip方法可能因为一些不同的原因以跳过更少的bytes数结束,可能这个数量是0.如果n是负数,方法会尝试往回跳(见本文最上方例子)。如果文件不支持从当前位置往回掉,会抛出IOException。返回的是实际跳过的bytes数量,为正是往后跳,为负是往前跳。可能会跳过比文件中剩余数量更多的bytes数,这个过程不会产生异常,跳过的bytes数可能包括了一些文件中超过了EOF文件结束符的bytes数,跳过了文件结束符再尝试读取会返回-1,说明已经到达文件末尾。 public native long skip(long n) throws IOException; available也是native方法,返回剩余可读取的或者可跳过的bytes数的估计值,这个过程不会阻塞下一个操作。文件超过EOF时返回0。下一个调用可能是相同的线程或不同的线程,一个读取或者跳过这么多bytes的操作不会被阻塞,但是可能读取或跳过更少的bytes。一些情况下,一个非阻塞的读取或者跳过可能在非常慢时被阻塞,比如从一个很慢的网络中读取大文件。 public native int available() throws IOException; close关闭文件输入流并释放相关联的任何系统资源,如果流关联通道则通道也要关闭。通过加锁保证只能进行一次,避免重复关闭。最终由文件描述符通过close0来关闭文件。 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(); } }); } private native void close0() throws IOException; getChannel返回关联的通道,初始化通道的位置是目前位置从文件中读取的bytes数量。从流中读取bytes会增加通道的位置,改变通道的位置会改变流中的文件位置。延迟初始化,第一次调用该方法才会打开文件通道。 public FileChannel getChannel() { synchronized (this) {//初始化单例 if (channel == null) { channel = FileChannelImpl.open(fd, path, true, false, this); } return channel; } } finalize是protected方法必须通过继承FileInputStram的类来使用,作用是确保close在没有任何对流的引用时被调用,也就是避免其他线程中流还在读取,另一个线程发起了close,因为可以通过文件描述符来判断是否是in状态也就是正在读取。 protected void finalize() throws IOException { if ((fd != null) && (fd != FileDescriptor.in)) { /* * 如果fd被共享,FileDescriptor中的引用会确保终结方法只会在安全的时候调用。 * 所有使用fd的引用都不可达时,我们调用close */ close(); } } FileOutputStream FileOutputStream是对应的文件输出流,文件输出流将数据也到一个文件或者是一个文件描述符中,无论文件是否有效或者可能根据所在平台创建一个新的文件。在一些平台上,一个文件只允许被一个FileOutputStream或者其他文件写入对象进行操作,在这种环境下,本类的构建在文件已经被打开时可能会出错。FileOutputStream写入的是比特,可以用于写入图片数据,如果要写入字符的话可以考虑使用FileWriter。 和上面的输入流很相似,FileOutputStream除了close以外也是不加锁的,但是write是一个原子性操作,必须在前一个byte串输出完之后,下一个输出才能开始。多线程可以强制输出流进行输出,但不能中断未进行完的write。 package test; import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.IOException; public class FileOutputThreadTest implements Runnable { private byte[] txt; private FileOutputStream out; @Override public void run() { for (int i = 0; i < 10; i++) { try { out.write(txt); } catch (IOException e) { e.printStackTrace(); } } } public FileOutputThreadTest(String txt, FileOutputStream out) { this.txt = txt.getBytes(); this.out = out; } public static void main(String args[]) throws InterruptedException, IOException { FileOutputStream out = new FileOutputStream("D:/test/file.txt"); StringBuilder build = new StringBuilder(); Thread[] t = new Thread[10]; for (int i = 0; i < 10; i++) { build.setLength(0); for (int j = 0; j < 5; j++) { build.append(i); } t[i] = new Thread(new FileOutputThreadTest(build.toString(), out)); } for (int i = 0; i < 10; i++) { t[i].start(); } for (int i = 0; i < 10; i++) { t[i].join(); } out.close(); // 根据输出结果,每个字符应该连续出现5的倍数 FileInputStream in = new FileInputStream("D:/test/file.txt"); int pos = 0; while (in.available() > 0) { int text = in.read(); pos++; for (int i = 1; i < 5; i++) { if (text != in.read()) { System.out.println("error" + String.valueOf(pos));// 没有出现 return; } pos++; } } /* * 00000000000000000000000000000000000000000000000000 * 22222222222222211111222222222222222222222222222222输入有交错 * 22222111111111111111111111111111111111111111111111 * 33333333333333333333333333333333333333333333333333 * 55555555555555555555555555555555555555555555555555 * 44444444444444444444444444444444444444444444444444 * 66666666666666666666666666666666666666666666666666 * 99999999999999999999999998888888888777777777777777 * 77777777777777777777777777777777777999999999999999 * 99999999998888888888888888888888888888888888888888 */ } } FileOutputStream有两种模式,清空文件从头开始输入和保留原本内容从文件末尾开始添加,这取决于内部属性append为true时是添加模式 /** * 系统依赖的文件描述符 */ private final FileDescriptor fd; /** * 文件为扩展模式在末尾添加时为true */ private final boolean append; /** * 相关联的文件通道,延迟初始化 */ private FileChannel channel; /** * 文件路径,如果该流是通过文件描述符来创建,为null */ private final String path; private final Object closeLock = new Object(); private volatile boolean closed = false; 构造方法传入的参数同样是三类:文件路径名、具体文件和文件描述符,append参数也在构造中指定,如果不输入默认是false /** * 通过具体的名字创建一个文件输出流来写入到文件。一个新的文件描述符被创建来代表这个文件. * 如果有一个安全管理器,它的checkWrite方法需要传入name参数 * 如果文件存在但它是一个目录而不是规则的文件,或者文件不出在但不能被创建,或者文件因为其他原因不能被打开,抛出FileNotFoundException */ public FileOutputStream(String name) throws FileNotFoundException { this(name != null ? new File(name) : null, false);//默认输出流从文件头部开始写入,会导致文本被清空 } public FileOutputStream(String name, boolean append) throws FileNotFoundException { this(name != null ? new File(name) : null, append); } //创建一个文件输出流来向一个具体的file中写入数据。一个新的文件描述符被创建来代表这个文件连接。 public FileOutputStream(File file) throws FileNotFoundException { this(file, false);//清空文件并且从头开始输入 } public FileOutputStream(File file, boolean append) throws FileNotFoundException { String name = (file != null ? file.getPath() : null);//file的路径和文件名 SecurityManager security = System.getSecurityManager();//获取操作系统的安全管理器 if (security != null) { security.checkWrite(name);//检查对文件是否有写入权限 } if (name == null) { throw new NullPointerException(); } if (file.isInvalid()) { throw new FileNotFoundException("Invalid file path"); } this.fd = new FileDescriptor(); fd.attach(this);//便于文件描述符关闭文件 this.append = append; this.path = name; open(name, append); } /** * 创建一个文件输入流写入到具体的文件描述符中,该文件描述符表示了对一个文件系统中实际文件存在的链接 * 安全管理器的checkWrite参数是文件描述符fdObj * 如果fdObj是null会抛出NullPointerException * 如果fdObj不可用不会抛出异常,但是,如果此时尝试调用该流的IO方法会抛出IOException */ 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); } 同样,文件需要打开才能进行写入,open方法只能由构造函数调用,基于native方法open0完成 private void open(String name, boolean append) throws FileNotFoundException { open0(name, append);//调用native方法打开文件 } private native void open0(String name, boolean append) throws FileNotFoundException; write操作上面提到过写入byte数组的操作是原子的,也就是native方法writeBytes是不可中断的。append变量作用在两个native方法中 //将具体的byte写入到文件输出流中,实现了OutputStream.write方法 public void write(int b) throws IOException { write(b, append); } public void write(byte b[]) throws IOException { writeBytes(b, 0, b.length, append); } public void write(byte b[], int off, int len) throws IOException { writeBytes(b, off, len, append); } private native void writeBytes(byte b[], int off, int len, boolean append) throws IOException; private native void write(int b, boolean append) throws IOException; close关闭这个文件输出流并释放任何关联的系统资源,这个输出流不能再用于写入bytes,如果流关联到了通道,则通道也关闭。通过加锁保证只会被关闭一次。 public void close() throws IOException { synchronized (closeLock) {//close只能进行一次所以需要是线程安全的,只能由一个线程进行 if (closed) { return; } closed = true; } if (channel != null) { channel.close();//关闭文件通道 } fd.closeAll(new Closeable() { public void close() throws IOException { close0(); } }); } private native void close0() throws IOException; getChannel获取的文件通道,返回通道的初始化等于到目前为止写入文件的bytes数量,除非当前的流是扩展模式,该模式下等于文件的大小。写入bytes将会增加通道的位置,无论是通过写入或者指明来改变通道的位置都会改变流的文件位置。文件通道是延迟初始化的设计,在调用时才进行初始化。 public FileChannel getChannel() { synchronized (this) {//确保只初始化一次 if (channel == null) { channel = FileChannelImpl.open(fd, path, false, true, append, this); } return channel; } } finalize和FIleInputStream一样也是要通过继承类来调用的protected方法,清除所有到文件的连接,确保当没有其他对这个流的引用时,close方法被调用。通过文件描述符的状态来判断当前是否在输出。 protected void finalize() throws IOException { if (fd != null) { if (fd == FileDescriptor.out || fd == FileDescriptor.err) { flush(); } else { /* * 如果fd被共享,FileDescriptor中的引用会确保终结器只在安全的时候被调用。 * 所有使用fd的引用都不可达时,我们调用close */ close(); } } }

优秀的个人博客,低调大师

Java ByteArrayInputStream ByteArrayOutputStream类源码解析

ByteArrayOutputStream ByteArrayOutputStream继承了抽象类OutputStream,本质是一个存在于堆内存中的可扩展byte数组,因为所有操作都在内存中所以flush()也什么都没做。 该类实现了一个输出流,数据写入一个byte数组,缓冲区随着写入的数据自动增大。数据可以通过toByteArray()和toString()重新取回。 关闭ByteArrayOutputStream没有影响,类中的方法可以在流被关闭后调用而不抛出IOException,因为close()实际上什么也没做。 从下面的代码中可以看出,所有涉及到输出流内容读写的操作,包括write、reset、toByteArray、size和toString都加了synchronized关键字,因为grow操作只改变数组大小不改变有效内容所以不需要加synchronized,因为要避免输出内容产生错乱。 从内部变量中可以看到,底层是一个byte数组,一个统计比特数的计数器 //存储数据的缓冲区 protected byte buf[]; //缓冲区内的有效byte protected int count; 无参构造时数组大小初始设为32,也可以指定size大小,size为负数时抛出IllegalArgumentException //创建一个新的ByteArrayOutputStream,初始缓冲区大小为32 public ByteArrayOutputStream() { this(32); } //创建一个新的ByteArrayOutputStream,初始缓冲区大小为size public ByteArrayOutputStream(int size) { if (size < 0) { throw new IllegalArgumentException("Negative initial size: " + size); } buf = new byte[size]; } 输入前要检查空间大小是否足够,下面一系列函数都是用于检查大小和进行扩大,数组大小存在上限,太大时会出现OOM错误。每次进行扩展时大小变为当前容量*2和需要的最小容量参数中的较大值,除非minCapacity>MAX_ARRAY_SIZE,否则不会突破这个大小,MAX_ARRAY_SIZE //在必要时增加容量来确保足够持有minCapacity表明的最小数目的元素 private void ensureCapacity(int minCapacity) { // overflow-conscious code if (minCapacity - buf.length > 0) grow(minCapacity); } //数组的最大大小,一些虚拟机保留了数组头部的一些位置。试图分配更大的数组会导致OutOfMemoryError:请求的数组大小超过了虚拟机的限制 private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8; private void grow(int minCapacity) { // overflow-conscious code int oldCapacity = buf.length;//当前数组大小 int newCapacity = oldCapacity << 1;//新容量=当前容量*2 if (newCapacity - minCapacity < 0) newCapacity = minCapacity;//新容量为当前容量*2和minCapacity中的较大值 if (newCapacity - MAX_ARRAY_SIZE > 0) newCapacity = hugeCapacity(minCapacity);//当前容量*2>MAX_ARRAY_SIZE是要做处理 buf = Arrays.copyOf(buf, newCapacity);//新分配一个数组把数据复制过去 } private static int hugeCapacity(int minCapacity) { if (minCapacity < 0) // 负数代表minCapacity超过正数所能表示的范围了,所以内存溢出 throw new OutOfMemoryError(); return (minCapacity > MAX_ARRAY_SIZE) ? Integer.MAX_VALUE ://minCapacity在Integer.MAX_VALUE到Integer.MAX_VALUE-8范围内,大小为Integer.MAX_VALUE MAX_ARRAY_SIZE;//小于MAX_ARRAY_SIZE时为MAX_ARRAY_SIZE } 每次的写入操作需要先确保数组大小足够,然后将内容复制到数组中 //将特定的byte写入到这个输出流中 public synchronized void write(int b) { ensureCapacity(count + 1);//确保数组还能再多存储一个 buf[count] = (byte) b;//int转为byte保存到数组中 count += 1;//计数器+1 } //将从off开始长度为len的bytes写入到输出流中 public synchronized void write(byte b[], int off, int len) { if ((off < 0) || (off > b.length) || (len < 0) || ((off + len) - b.length > 0)) { throw new IndexOutOfBoundsException(); } ensureCapacity(count + len); System.arraycopy(b, off, buf, count, len);//复制内容到数组中 count += len; } writeTo是将当前输出流中的内容全部写入到参数中的输出流中去,相当于调用out.write(buf, 0, count) public synchronized void writeTo(OutputStream out) throws IOException { out.write(buf, 0, count); } reset将count设为0,当前累计的输出都被丢弃了,这个输出流可以被重新使用,再次利用已经分配好的缓冲区空间,因为没有可以直接更改count的方法,所以相当于旧的内容无法再被访问了 public synchronized void reset() { count = 0; } toByteArray创建一个新的byte数组,大小为当前输出流中的byte大小,将其中的byte内容复制到新的数组中 public synchronized byte toByteArray()[] { return Arrays.copyOf(buf, count); } size返回当前流中的byte大小 public synchronized int size() { return count; } toString将输出流中的内容重新解码为String,可以指定字符集也可以直接默认使用平台字符集 //将缓冲区中的内容通过平台的默认字符集转换成string解码。新的String长度是字符集的函数,所以可能不等于buffer的大小,这个方法总是用平台的默认字符集中的默认替代字符串来替代畸形输入和非图形化表示字符序列 public synchronized String toString() { return new String(buf, 0, count); } //跟上面方法相比指定了字符集 public synchronized String toString(String charsetName) throws UnsupportedEncodingException { return new String(buf, 0, count, charsetName); } close可以看到什么都没做,所以一切照常,原因是该类操作都在堆内存中,不需要关闭一些句柄、套接字之类的东西所以什么都不用做 public void close() throws IOException { } ByteArrayInputStream ByteArrayInputStream是和ByteArrayOutputStream对应的输入流,继承了抽象类InputStream,本质上也是一个存储在堆内存中的byte数组,这个数组必须在构造时传入并直接被使用而不是复制,之后大小无法改变,可以通过直接对数组中的内容进行修改改变流中的内容。可以标记一个位置,通过reset重置回去来进行重复读取。 public static void main(String args[]) throws IOException{ ByteArrayOutputStream out = new ByteArrayOutputStream(); out.write("1234567890".getBytes()); byte buf[] = out.toByteArray(); ByteArrayInputStream in = new ByteArrayInputStream(buf); byte res[] = new byte[10]; in.read(res); System.out.println(new String(res));//1234567890 buf[0] = '4'; in.reset(); in.read(res); System.out.println(new String(res));//4234567890 } ByteArrayInputStream含有一个内部缓冲区包含了可能从流中读取的比特,一个内部计数器用来保持对下一个read方法读取的byte的跟踪。 关闭ByteArrayInputStream没有作用,所有方法都可以继续使用不会出现IOException,因为全部操作都在堆内存中不存在要关闭的句柄等。 从内部变量中可以看出,底层通过buf[]来存储流中的内容 /** * 构造时提供一个数组,buf中的比特是唯一能够从流中读取的,buf[pos]是下一个要被读取的byte */ protected byte buf[]; /** * 下一个要被读取的byte的下标,总是非负数,不能超过count,buf[pos]是下一个要被读取的byte */ protected int pos; /** * 流中当前标记的位置,ByteArrayInputStream在构造时默认标记位置是0。可以通过mark()方法标记缓冲区中的其他位置,能够通过reset()回到当前标记的位置 * 如果没有设置过mark,他的值会是传递给构造器的值,如果没有的话是0 */ protected int mark = 0; /** * 比当前有效字符的index大1,总是非负数,不超过buf数组的长度。比buf数组中最后一个能读取的byte的下标大1 */ protected int count; 构造函数必须要传入buf[],传入后对于buf的引用无法改变,但可以通过缓存引用来直接修改其中的内容 //创建一个ByteArrayInputStream,使用buf作为缓冲区,缓冲区没有拷贝而是直接引用这个数组。pos初始值为0,count的初始值是buf的数组大小 public ByteArrayInputStream(byte buf[]) { this.buf = buf; this.pos = 0; this.count = buf.length; } //创建一个ByteArrayInputStream,使用buf作为缓冲区,pos初始值是offset,count的初始值是offset+length和buf.length中的较小值。缓冲区没有拷贝,直接引用了buf,mark设为offset public ByteArrayInputStream(byte buf[], int offset, int length) { this.buf = buf; this.pos = offset; this.count = Math.min(offset + length, buf.length); this.mark = offset; } 读取单个byte时,读取输入流中的下一个byte,返回值是0-255的int,已经到达末端没有可读的byte返回-1 public synchronized int read() { return (pos < count) ? (buf[pos++] & 0xff) : -1;//pos==count时到达末端返回-1 } 从流中读取最多len长度的bytes。如果pos==count,返回-1表示已经到达了文件末端,否则返回读取到的bytes数量k,k是len和count-pos中的较小值。如果k是正数,buf[pos]~buf[pos+k-1]将通过ystem.arraycopy被复制到b[off]~b[off+k-1]中,pos增加k,k被返回。 public synchronized int read(byte b[], int off, int len) { if (b == null) { throw new NullPointerException(); } else if (off < 0 || len < 0 || len > b.length - off) { throw new IndexOutOfBoundsException(); } if (pos >= count) { return -1; } int avail = count - pos; if (len > avail) { len = avail;//最多读取到流中数据末端 } if (len <= 0) { return 0; } System.arraycopy(buf, pos, b, off, len);//复制数据到b中 pos += len;//增加pos return len; } skip跳过输入流中的n个bytes,如果到达了末端跳过的bytes会少于n。实际跳过的bytes数量k等于n和count-pos中的较小值,pos增加k,k被返回 public synchronized long skip(long n) { long k = count - pos; if (n < k) { k = n < 0 ? 0 : n; } pos += k; return k; } available返回输入流中剩余可以读取或者跳过的bytes数量,等于count-pos,也就是缓冲区中剩余的可被读取的数量 public synchronized int available() { return count - pos; } markSupported测试这个InputStream是否支持mark/reset操作,ByteArrayInputStream总是支持该操作所以直接返回true public boolean markSupported() { return true; } mark将当前的位置标记,ByteArrayInputStream的标记位再构造时默认值是0,可以通过这个方法标记到缓冲区的其他位置,在构造时也可以通过传入offset设置其他mark,若没有传入则标记0。readAheadLimit这个参数在这里没有实际作用,InputStream中加这个参数是留给其他类中mark使用指出mark位置开始能读取的最大bytes数量。 public void mark(int readAheadLimit) { mark = pos; } reset将位置重置到mark的位置 public synchronized void reset() { pos = mark; } close方法什么都没做,所以依然可以照常使用,不会产生IOException public void close() throws IOException { }

优秀的个人博客,低调大师

Java StringBuffer StringBuilder类源码解析

StringBuffer StringBuffer是线程安全的字符动态序列,像String但是可以修改,在任何时点他都含有字符的特定序列,但是序列的长度和内容可以通过调用某些方法来修改。 StringBuffer对于多线程是安全的,在必要的方法上都加了synchronized。核心方法是append和insert,他们通过重载可以接受任何类型的数据。将数据转换为String然后扩展或者插入到StringBuffer中。append将字符添加到末尾,insert是添加到某个指定的位置。举个例子,z是一个StringBuffer,当前内容为"start",此时调用z.append("le")则内容变为"startle",若调用的是z.insert(4, "le")则内容变为"starlet"。sb是一个StringBuffer,sb.append(x)和sb.insert(sb.length(), x)是等效的。 当有一个包含源序列的操作发生时,只有StringBuffer同步操作,不会发生在源上。 由于StringBuffer被设计为线程安全类,所以在通过一个被多个线程共享的源序列构造和append insert操作时,调用的程序必须确保在这些操作期间源序列没有发生变化。这个可以通过调用者在操作期间加锁来保证,或者通过使用一个不可变的源序列,或者不使用线程共享的源序列。 除非另外说明,对构建或者其他方法传入一个null参数会引起抛出NullPointerException错误。 JDK5中,补充了StringBuffer的单线程版本StringBuilder,StringBuilder应该优先使用,他有同样的操作但是没有synchronized所以速度更快。 内部变量与构造函数 从类的定义中可以看出StringBuffer继承了AbstractStringBuilder,下面会介绍到复用了AbstractStringBuilder的内部变量与函数 public final class StringBuffer extends AbstractStringBuilder implements java.io.Serializable, CharSequence StringBuffer自身有一个内部变量toStringCache,这是上一个toString返回值的高速缓存,一旦StringBuffer被修改就会清空,作用是在调用toString的时如果没有变更可以快速返回结果不用重新构造字符串 private transient char[] toStringCache; 观察StringBuffer的构造函数,可以看到他们都是基于super(capacity)这个方法来展开的,也就是AbstractStringBuilder的构造函数 //构造一个初始大小为16的StringBuffer public StringBuffer() { super(16); } //构造指定初始容量大小 public StringBuffer(int capacity) { super(capacity); } //构造一个StringBuffer,初始内容为str,初始大小为16+str的长度 public StringBuffer(String str) { super(str.length() + 16); append(str); } //构造一个StringBuffer内容和CharSequence一致,初始容量为16+CharSequence.length,如果CharSequence的长度为0,则返回一个空的buffer容量为16 public StringBuffer(CharSequence seq) { this(seq.length() + 16); append(seq); } 下面的内部变量和构造函数来自AbstractStringBuilder,可以看到他的构造方法主要是新分配了一个给定大小的数组 char[] value;//存储字符 int count;//字符个数 AbstractStringBuilder(int capacity) { value = new char[capacity];//分配一个大小为capacity的字符数组给value } 下面两个方法是对容量和字符长度的查询,只做查询而不会做出修改 public synchronized int length() { return count;//返回字符个数 } public synchronized int capacity() { return value.length;//返回容量大小也就是数组大小 } 而ensureCapacity是会修改数组大小的,他会确保value数组的大小不小于minimumCapacity,如果容量小于该大小,会分配一个新的数组并将原本的字符复制到新数组中,新数组大小是当前容量*2+2和minimumCapacity中的较大值,minimumCapacity有大小限制,超过一定的值会内存溢出 public synchronized void ensureCapacity(int minimumCapacity) { super.ensureCapacity(minimumCapacity);//确保value数组的大小不小于minimumCapacity } //下面的代码来自父类 //确保容量不小于最小值,如果当前容量小于参数值,分配一个新的更大的内部数组,他的大小是minimumCapacity和旧容量旧容量*2+2中的较大值。如果minimumCapacity是负数,什么也不做直接返回。 public void ensureCapacity(int minimumCapacity) { if (minimumCapacity > 0) ensureCapacityInternal(minimumCapacity); } private void ensureCapacityInternal(int minimumCapacity) { // overflow-conscious code if (minimumCapacity - value.length > 0) { value = Arrays.copyOf(value, newCapacity(minimumCapacity));//根据minimumCapacity分配一个新的数组,并将原来的字符复制到新的数组中 } } //返回不小于minCapacity的大小,如果当前大小*2+2足够的话就取该值。不会返回超过MAX_ARRAY_SIZE的大小,除非minCapacity超过该值 private int newCapacity(int minCapacity) { // overflow-conscious code int newCapacity = (value.length << 1) + 2; if (newCapacity - minCapacity < 0) { newCapacity = minCapacity; }//新的大小为旧大小*2+2与minCapacity中的较大值 return (newCapacity <= 0 || MAX_ARRAY_SIZE - newCapacity < 0) ? hugeCapacity(minCapacity) : newCapacity; } //如果minCapacity在MAX_ARRAY_SIZE到Integer.MAX_VALUE之间的话返回minCapacity,超过Integer.MAX_VALUE抛出OutOfMemoryError private int hugeCapacity(int minCapacity) { if (Integer.MAX_VALUE - minCapacity < 0) { // 内存溢出 throw new OutOfMemoryError(); } return (minCapacity > MAX_ARRAY_SIZE) ? minCapacity : MAX_ARRAY_SIZE; } trimToSize在value中存在没有存储的空间时,会重新分配一个大小和字符个数相等的数组将字符复制过去,提高空间利用率,会改变capacity()的值 public synchronized void trimToSize() { super.trimToSize();//新分配一个数组仅保留与字符个数相等的大小,将字符复制过去 } //尝试减少用于存储字符串的空间。如果缓冲区比保存当前字符串所需的空间更大,会变更大小提高空间利用率。这个方法可能会改变capacity()的返回值 public void trimToSize() { if (count < value.length) { value = Arrays.copyOf(value, count);//新分配一个大小为字符个数的数组,将现有的字符复制过去 } } setLength在newLength小于等于当前数组大小时直接返回,大于时新分配一个大小为newLength和当前容量*2+2的较大值的新数组,并复制字符,然后将数组中的剩余位置填充上'0',count设为newLength public synchronized void setLength(int newLength) { toStringCache = null;//清空上一次toString的缓存 super.setLength(newLength); } public void setLength(int newLength) { if (newLength < 0) throw new StringIndexOutOfBoundsException(newLength); ensureCapacityInternal(newLength);//newLength小于等于当前数组大小的话直接返回,否则分配一个大小为newLength和当前容量*2+2的较大值的新数组,并复制字符 if (count < newLength) { Arrays.fill(value, count, newLength, '\0');//字符个数小于newLength时,用'\0'填充剩余的位置 } count = newLength;//count设为newLength } charAt返回指定位置的字符,会检查index返回是否大于等于0且小于count public synchronized char charAt(int index) { if ((index < 0) || (index >= count)) throw new StringIndexOutOfBoundsException(index); return value[index]; } public char charAt(int index) { if ((index < 0) || (index >= count)) throw new StringIndexOutOfBoundsException(index); return value[index]; } codePointAt是返回index位置的代码点,代码点这个东西之前在String里讲过,这里再贴一次:字符数据类型是一个采用UTF-16编码表示Unicode代码点的代码单元。大多数的常用Unicode字符使用一个代码单元就可以表示,而辅助字符需要一对代码单元表示。而length返回的是UTF-16下的代码单元的数量,而codePointCount返回的是代码点的数量。对于大部分人工输入的字符,这两者是相等的,会出现length比codePointCount长的通常是某些数学或者机器符号,需要两个代码单元来表示一个代码点 。codePointBefore返回index前一个位置的代码点,codePointCount则是统计指定序列段中的代码点数量 public synchronized int codePointAt(int index) { return super.codePointAt(index); } public int codePointAt(int index) { if ((index < 0) || (index >= count)) { throw new StringIndexOutOfBoundsException(index); } return Character.codePointAtImpl(value, index, count); } public synchronized int codePointBefore(int index) { return super.codePointBefore(index); } public int codePointBefore(int index) { int i = index - 1; if ((i < 0) || (i >= count)) { throw new StringIndexOutOfBoundsException(index); } return Character.codePointBeforeImpl(value, index, 0); } public synchronized int codePointCount(int beginIndex, int endIndex) { return super.codePointCount(beginIndex, endIndex);//统计从beginIndex到endIndex之间的代码点数量 } public int codePointCount(int beginIndex, int endIndex) { if (beginIndex < 0 || endIndex > count || beginIndex > endIndex) { throw new IndexOutOfBoundsException(); } return Character.codePointCountImpl(value, beginIndex, endIndex-beginIndex); } offsetByCodePoints这个方法单看注释翻译比较难理解:返回从index到codePointOffset的代码点偏移index,每个不成对的代理(两个代码单元表示一个代码点时称为两个代理)在范围内被记为一个代码点。实际上可以理解为,如果不存在两个代码单元表示一个代码点的情况,返回的结果就是index+codePointOffset;如果存在那种特殊代码点,则index的变化量会偏移特殊代码点的个数,例如有3个特殊代码点,则返回值为index+codePointOffset+3(codePointOffset>0)或者index+codePointOffset-3(codePointOffset<0) public synchronized int offsetByCodePoints(int index, int codePointOffset) { return super.offsetByCodePoints(index, codePointOffset); } public int offsetByCodePoints(int index, int codePointOffset) { if (index < 0 || index > count) { throw new IndexOutOfBoundsException(); } return Character.offsetByCodePointsImpl(value, 0, count, index, codePointOffset); } getChars会再检查参数范围后,复制指定位置的字符串到指定的位置 public synchronized void getChars(int srcBegin, int srcEnd, char[] dst, int dstBegin) { super.getChars(srcBegin, srcEnd, dst, dstBegin);//复制value从srcBegin到srcEnd的内容到dst从dstBegin开始的位置 } setCharAt修改指定位置的字符 public synchronized void setCharAt(int index, char ch) { if ((index < 0) || (index >= count)) throw new StringIndexOutOfBoundsException(index); toStringCache = null;//清空toString缓存 value[index] = ch;//修改对应位置的字符 } 核心函数之一的append有众多的重载,篇幅原因就不全贴了。append需要注意一点,直接在参数里输入null是会报错的,但是以对象赋值null的方式传入是可行的,相当于添加"null"。对于传入的非字符串对象,统一调用toString方法转换为字符串;数值对象的话通过包装类的方法转为字符串。 public synchronized StringBuffer append(String str) { toStringCache = null; super.append(str); return this; } public AbstractStringBuilder append(String str) { if (str == null) return appendNull(); int len = str.length(); ensureCapacityInternal(count + len);//确保数组容量足够大 str.getChars(0, len, value, count);//将str从头到尾复制到value中从count开始的位置,实现拼接 count += len;//增加字符数量 return this; } private AbstractStringBuilder appendNull() { int c = count; ensureCapacityInternal(c + 4); final char[] value = this.value; value[c++] = 'n';//null当做"null"来进行扩展 value[c++] = 'u'; value[c++] = 'l'; value[c++] = 'l'; count = c; return this; } delete删除包括start在内到end之前的字符,end开始部分保留,通过复制保留部分到start的位置来实现 public synchronized StringBuffer delete(int start, int end) { toStringCache = null;//清除toString缓存 super.delete(start, end);//删除从start到end-1位置的元素 return this; } public AbstractStringBuilder delete(int start, int end) { if (start < 0) throw new StringIndexOutOfBoundsException(start); if (end > count) end = count;//end最大为count if (start > end) throw new StringIndexOutOfBoundsException(); int len = end - start; if (len > 0) { System.arraycopy(value, start+len, value, start, count-end);//将start+len开始的长度为count-end的部分,复制到start开始的位置 count -= len;//修改count值 } return this; } deleteCharAt只删除单个字符,也是通过复制来实现 public synchronized StringBuffer deleteCharAt(int index) { toStringCache = null; super.deleteCharAt(index);//将index后一位开始的内容复制到index的位置 return this; } replace操作会移除start到end-1的内容,将str插入到start开始的位置,实现的话会先把value中的后面那段复制到他最终所处的位置,中间留出一段空间供str复制进去 public synchronized StringBuffer replace(int start, int end, String str) { toStringCache = null; super.replace(start, end, str);//移除start到end-1的内容,将str插入到start开始的位置 return this; } substring和subSequence方法截取子串,substring可以不输入end参数截取到末尾,方法都是基于父类的同一个函数来返回一个新的String public String substring(int start, int end) { if (start < 0) throw new StringIndexOutOfBoundsException(start); if (end > count) throw new StringIndexOutOfBoundsException(end); if (start > end) throw new StringIndexOutOfBoundsException(end - start); return new String(value, start, end - start); } insert方法同样是重载众多,但是主要参数只有在value中插入的位置、插入的对象、插入对象从哪里开始截取、截取长度是多少,后两个可以不输入那么就是整个对象进行插入。会清空toStringCache public synchronized StringBuffer insert(int index, char[] str, int offset, int len) { toStringCache = null; super.insert(index, str, offset, len); return this; } public AbstractStringBuilder insert(int index, char[] str, int offset, int len) { if ((index < 0) || (index > length())) throw new StringIndexOutOfBoundsException(index); if ((offset < 0) || (len < 0) || (offset > str.length - len)) throw new StringIndexOutOfBoundsException( "offset " + offset + ", len " + len + ", str.length " + str.length); ensureCapacityInternal(count + len);//确保空间足够,不足时扩展为当前容量*2+2和count+len的较大值 System.arraycopy(value, index, value, index + len, count - index);//将index开始的内容复制到index+len的位置,空出留给str的空间 System.arraycopy(str, offset, value, index, len);//str复制到留出的空间中 count += len;//count增加str的长度 return this; } indexOf和lastIndexOf两个方法分别是从头开始向后寻找第一个完全相等的字符串和从尾部开始从头寻找第一个,可以指定开始寻找的位置,直接调用了String的同名方法 public synchronized int indexOf(String str, int fromIndex) { return super.indexOf(str, fromIndex);//调用了String.indexOf } public synchronized int lastIndexOf(String str, int fromIndex) { return super.lastIndexOf(str, fromIndex);//调用了String.lastIndexOf } reverse这个方法会逆序字符串内容,从中心开始做轴对称的交换 public synchronized StringBuffer reverse() { toStringCache = null; super.reverse();//以中心为轴,从中间点开始做轴对称位置的字符复制交换 return this; } toString有缓存直接返回,否则新建一个数组复制value里的有效字符。所有会导致value中内容变化的方法都会清空缓存,还有setLength无论是否导致长度变化并填充了'0'都会清空 public synchronized String toString() { if (toStringCache == null) { toStringCache = Arrays.copyOfRange(value, 0, count);//缓存无效时,创建一个新的数组将value中的有效字符复制进去 } return new String(toStringCache, true);//缓存有效时直接返回,缓存中的字符串是被共享的 } StringBuilder JDK1.5加入,同样继承了AbstractStringBuilder,实现了java.io.Serializable, CharSequence接口。 StringBuilder是没有toStringCache的,所以他的toString函数必定是复制产生一个新的String,猜测是出于StringBuilder默认是用于单线程环境,不需要进行共享操作,所以也就没有了cache public String toString() { // Create a copy, don't share the array return new String(value, 0, count); } StringBuilder在单线程情况下由于没有了同步锁性能更好,推荐优先使用。他的实现和StringBuffer除了上面提到的cache和同步的问题外几乎没有区别,另外一个有区别的地方是序列化部分。 先看StringBuilder的序列化函数,非常简单,除了缺省对象外只有count和value的读写 private void writeObject(java.io.ObjectOutputStream s) throws java.io.IOException { s.defaultWriteObject(); s.writeInt(count); s.writeObject(value); } private void readObject(java.io.ObjectInputStream s) throws java.io.IOException, ClassNotFoundException { s.defaultReadObject(); count = s.readInt(); value = (char[]) s.readObject(); } 而StringBuffer就不同了,用了ObjectStreamField来声明序列化的字段,至于这两个序列化的方式到底有什么区别,以后能更新到IO流的时候再说吧 private static final java.io.ObjectStreamField[] serialPersistentFields = { new java.io.ObjectStreamField("value", char[].class), new java.io.ObjectStreamField("count", Integer.TYPE), new java.io.ObjectStreamField("shared", Boolean.TYPE), }; private synchronized void writeObject(java.io.ObjectOutputStream s) throws java.io.IOException { java.io.ObjectOutputStream.PutField fields = s.putFields(); fields.put("value", value); fields.put("count", count); fields.put("shared", false); s.writeFields(); } private void readObject(java.io.ObjectInputStream s) throws java.io.IOException, ClassNotFoundException { java.io.ObjectInputStream.GetField fields = s.readFields(); value = (char[])fields.get("value", null); count = fields.get("count", 0); }

优秀的个人博客,低调大师

Android EventBus使用(不含源码解析)

官方文档:https://github.com/greenrobot/EventBus simplifies the communication between components decouples event senders and receivers performs well with Activities, Fragments, and background threads avoids complex and error-prone dependencies and life cycle issues makes your code simpler is fast is tiny (~50k jar) is proven in practice by apps with 100,000,000+ installs has advanced features like delivery threads, subscriber priorities, etc. 这句话大概是说: 简化组件之间的通信 解耦事件发送者和接收者 对活动、片段和后台线程进行良好的操作 而且非常快 jar包小至50k 已经有超过了一亿用户安装 而且还可以定义优先级 不看了,反正对于开发者来说就一句话:好用! 不废话了,下面开始说使用教程: 1、加入EventBus3.0依赖 implementation 'org.greenrobot:eventbus:3.0.0' 2、既然说了EventBus是用来传值用的,那么先定义这个值吧。 创建一个实体类,MyStudent public class MyStudent extends Observable { private String name; private int sex; private int old; public String getName() { return name == null ? "" : name; } public void setName(String name) { this.name = name; } public int getSex() { return sex; } public void setSex(int sex) { this.sex = sex; } public int getOld() { return old; } public void setOld(int old) { this.old = old; } @Override public String toString() { return "MyStudent{" + "name='" + name + '\'' + ", sex=" + sex + ", old=" + old + '}'; } 3、值有了,那么这个值有入口和出口的吧 建立两个Activity,我这里就建两个,一个MainActivity,一个Main2Activity,(这里创建流程就不写了,只写Activity中的核心代码) public class MainActivity extends AppCompatActivity { private Button button; private MyStudent myStudent; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); EventBus.getDefault().register(this);//注册eventbus button = findViewById(R.id.main_btn); button .setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { startActivity(new Intent(MainActivity.this, Main2Activity.class)); } }); } @Override protected void onDestroy() { super.onDestroy(); EventBus.getDefault().unregister(MainActivity.this); } //接收事件,EventBus3.0之后采用注解的方式 @Subscribe(threadMode = ThreadMode.MAIN) public void Event(MyStudent myStudent) { Log.e("MainActivity", myStudent.toString()); } } 下面看看Main2 public class Main2Activity extends AppCompatActivity { Button button; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main2); button = findViewById(R.id.main2_btn); button.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { MyStudent myStudent = new MyStudent(); myStudent.setName("eventbus"); myStudent.setOld(2); myStudent.setSex(2); EventBus.getDefault().post(myStudent); finish(); } }); } 这里Log的打印结果是:(我不说,打印结果希望看博客的同学可以自己动手操作一波,这样你的记忆力才深刻。) 4、其实最基本的使用到这里就完了,有一些需要注意的地方在这里说一下: 我们可以看到,在接收参数的方法上面会有一个注解: @Subscribe(threadMode = ThreadMode.MAIN) 在接收参数的方法上一定要带这个注解,不然参数会接收不到。 注解中的: threadMode = ThreadMode.MAIN,指的是在什么线程下操作。我们点进去源码看看 public enum ThreadMode { /** * Subscriber will be called in the same thread, which is posting the event. This is the default. Event delivery * implies the least overhead because it avoids thread switching completely. Thus this is the recommended mode for * simple tasks that are known to complete is a very short time without requiring the main thread. Event handlers * using this mode must return quickly to avoid blocking the posting thread, which may be the main thread. */ POSTING, /** * Subscriber will be called in Android's main thread (sometimes referred to as UI thread). If the posting thread is * the main thread, event handler methods will be called directly. Event handlers using this mode must return * quickly to avoid blocking the main thread. */ MAIN, /** * Subscriber will be called in a background thread. If posting thread is not the main thread, event handler methods * will be called directly in the posting thread. If the posting thread is the main thread, EventBus uses a single * background thread, that will deliver all its events sequentially. Event handlers using this mode should try to * return quickly to avoid blocking the background thread. */ BACKGROUND, /** * Event handler methods are called in a separate thread. This is always independent from the posting thread and the * main thread. Posting events never wait for event handler methods using this mode. Event handler methods should * use this mode if their execution might take some time, e.g. for network access. Avoid triggering a large number * of long running asynchronous handler methods at the same time to limit the number of concurrent threads. EventBus * uses a thread pool to efficiently reuse threads from completed asynchronous event handler notifications. */ ASYNC } 哦,是个枚举类型。 POSTING:意思大概是,为了避免线程切换,在什么线程发的你接受默认就是什么线程 MAIN:主线程,也就是ui线程,不要做耗时操作哟 BACKGROUND:顾名思义,就是子线程啦。 ASYNC:异步,我感觉EventBus很贴心,异步都提供了。 5、EventBus还有一种使用,那就是EventBus的粘性事件(这里仅仅简单举个例子,我目前并没有在实际场景中用到) 依旧是这两个Activity public class MainActivity extends AppCompatActivity { private Button button; private MyStudent myStudent; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); EventBus.getDefault().register(this);//注册eventbus button = findViewById(R.id.main_btn); button .setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { startActivity(new Intent(MainActivity.this, Main2Activity.class)); } }); } @Override protected void onDestroy() { super.onDestroy(); EventBus.getDefault().unregister(MainActivity.this); } //接收事件,EventBus3.0之后采用注解的方式 @Subscribe(threadMode = ThreadMode.MAIN , sticky = true)//sticky是为了声明是粘性事件 public void Event(MyStudent myStudent) { Log.e("MainActivity", myStudent.toString()); } } 看看Main2 public class Main2Activity extends AppCompatActivity { Button button; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main2); button = findViewById(R.id.main2_btn); button.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { MyStudent myStudent = new MyStudent(); myStudent.setName("eventbus"); myStudent.setOld(2); myStudent.setSex(2); EventBus.getDefault().postSticky(myStudent); finish(); } }); } 为什么叫粘性事件呢? 先举个小例子,比如说:你定报纸,本来按理说你必须提前订阅了,在发报纸的时候才能收到。 而粘性事件是: 你别管他什么时候发的,就算他先发了报纸,那么你订阅的时候你也能收到这个报纸。(我觉得这个例子已经很形象了) 那么EventBus的粘性事件也是这样,如果他先发消息,发的时候你还没注册,不要紧,你什么时候注册什么时候接收,处理下面的事情。 学习的同学可以多打印log看看。多看多试。 这节课就到这里,下节课再见。

优秀的个人博客,低调大师

《现代前端技术解析》读后鬼扯

别名:前端工程师培养大纲 评分:4星 作者:张成文 GitHub:https://github.com/ouvens/ 豆瓣:https://book.douban.com/subject/27021790/ 本书就像它前言中提到的那样,是一本以现代前端技术思想与理论为主要内容的书,类似于大学刚入学发的那本专业培养大纲,总结和梳理了整个前端开发体系,让我们站在一个全局的角度去看待前端这个领域,展示了可供我们深入探索的各个方向。 这类书籍写起来不容易,没把握好很容易就变成博客合集。这本书到底怎么样,见仁见智吧。书中很多原理的讲解都附有代码,不是很复杂,仔细揣摩下都能懂,作者确实是费了些心思的。个人感觉不足的地方有几点,第一个就是对前端构建的工具介绍的不多,我们常见的webpack,gulp等都没怎么提及,另一个是对MVV*模式介绍的过于简单,React Native,weex都没留下只言片语。前端技术发展日新月异,各种工具框架层出不穷,争奇斗艳,让我们这些搞前端的眼花缭乱,真心有些学不动的感觉。 作者的GitHub上有一张 前端知识导图, 一定要看,一定要看,一定要看,重要的事情说三遍。大家可能比较熟悉StuQ出品的 HTML5开发技能图谱,也很全面,但是比前者精简了很多。 本书第一章就回答了一个常见的问题,即我们在浏览器的地址栏输入网址到展示页面这一过程中发生了什么。由此引出了浏览器的组成结构, 介绍了渲染引擎的渲染流程,我们最常见的是webkit内核,渲染DOM的流程如下: 接着是浏览器缓存的话题,有一张图还是很经典,如下: 第二章的内容让我怀疑作者了解Android开发,因为在Hybrid app的介绍中原生的部分都是Android的,对于jsBridge的原理,我之前还花过力气研究过,大家最常见的应该是大头鬼的开源的项目 JsBridge 了,还有其他的实现方法,不过原理都是一样的,但是实现的细节有不同。 第三章提及了web Components这个提上议程的原生组件化标准。其实我们也接触过,比如说 <video>、<audio> 等标签,深入下去你会发现这个其实就是Shadow DOM。我觉得web Components的组件化和常用框架的组件化有着很大的区别,前者目的就是为了组件,而后者的目的是高效的同步UI和状态,无论是使用虚拟DOM还是通过观察者检测变化。 大名鼎鼎的ionic在V4版本中放弃了Angular,转而使用了Stencil,它是一个把你所写的组件代码最终生成Web Components的编译器。官网上介绍它的特色如下: Virtual DOM Async rendering (inspired by React Fiber) Reactive data-binding TypeScript JSX 我粗略看了下它的文档,可以说是Angular+React的结合体,上一段他们官网的示例代码,大家体会一下: import { Component, Prop } from '@stencil/core'; @Component({ tag: 'my-first-component', styleUrl: 'my-first-component.scss' }) export class MyComponent { // Indicate that name should be a public property on the component @Prop() name: string; render() { return ( <p> My name is {this.name} </p> ); } } 复制代码 @Component,render()这两个看起来很眼熟吧。 第四章介绍了现代前端框架的原理,通过简单的代码粗略实现了双向数据绑定,静下心来看的话,肯定可以懂得,掘金上介绍Vue实现原理的博客很多,可以对照着看。Virtual DOM的设计理念当然是不可或缺的,短短几页内容,深入了解的话,还是不够的。 第五章是前端项目的各种规范,这里我就不得不吐槽一下Angular,官方文档里居然连变量命名都提供了建议,真是够够的。相比Vue和React,Angular真的是管的太宽了,可能唯一的好处就是从java转过来的我对ts有种莫名的好感吧。不过Angular自带的Rxjs真心是好用,让我想起当年写Android的痛苦岁月。 第六章提到了后端直出,这个我没用过,就不鬼扯了。 本书一个有意思的地方是在最后一部分还专门教大家如何成为一个优秀的前端工程师,这个在其他的技术类书籍中确实是少见的。 本书不推荐购买,借阅看两遍就足够了,不过上面的知识导图强烈建议存档的。 前端类图书还是挺多的,有些真的没必要买,看一遍就过去了,有些呢确实是不可或缺的好书,为了防止自己花了冤枉钱,可以办理一张图书馆的借书证,很多书都可以在里面找到,先借阅一番,再来决定是否买。吐槽一下南京的金陵图书馆,号称是排在全国前列,我以为管理有多好,然而有些书的位置压根儿就不对,可以搜到,就是找不到,也是醉了。 原文发布时间为:2018年06月28日 原文作者:JerryMissTom 本文来源: 掘金 如需转载请联系原作者

资源下载

更多资源
腾讯云软件源

腾讯云软件源

为解决软件依赖安装时官方源访问速度慢的问题,腾讯云为一些软件搭建了缓存服务。您可以通过使用腾讯云软件源站来提升依赖包的安装速度。为了方便用户自由搭建服务架构,目前腾讯云软件源站支持公网访问和内网访问。

Rocky Linux

Rocky Linux

Rocky Linux(中文名:洛基)是由Gregory Kurtzer于2020年12月发起的企业级Linux发行版,作为CentOS稳定版停止维护后与RHEL(Red Hat Enterprise Linux)完全兼容的开源替代方案,由社区拥有并管理,支持x86_64、aarch64等架构。其通过重新编译RHEL源代码提供长期稳定性,采用模块化包装和SELinux安全架构,默认包含GNOME桌面环境及XFS文件系统,支持十年生命周期更新。

Sublime Text

Sublime Text

Sublime Text具有漂亮的用户界面和强大的功能,例如代码缩略图,Python的插件,代码段等。还可自定义键绑定,菜单和工具栏。Sublime Text 的主要功能包括:拼写检查,书签,完整的 Python API , Goto 功能,即时项目切换,多选择,多窗口等等。Sublime Text 是一个跨平台的编辑器,同时支持Windows、Linux、Mac OS X等操作系统。

WebStorm

WebStorm

WebStorm 是jetbrains公司旗下一款JavaScript 开发工具。目前已经被广大中国JS开发者誉为“Web前端开发神器”、“最强大的HTML5编辑器”、“最智能的JavaScript IDE”等。与IntelliJ IDEA同源,继承了IntelliJ IDEA强大的JS部分的功能。

用户登录
用户注册