首页 文章 精选 留言 我的

精选列表

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

Java序列化 ObjectOutputStream源码解析

概述 众所周知,Java原生的序列化方法可以分为两种: 实现Serializable接口 实现Externalizable接口 其实还有一种,可以完全自己实现转为二进制内容,用Unsafe写到内存里面,然后写入文件 Serializable 可以使用ObjectStream默认实现的writeObject和readObject方法并且可以通过transit关键字来使得变量不被序列化,开发简单 除了输出协议和包名类名外,会额外输出类的变量信息 有缓存机制,对于重复对象会直接输出所在位置,所以类较大且重复内容多时反而效率高,但会消耗额外内存空间 如果父类没有无参构造函数则不会序列化父类 Externalizable 必须完全由自己来实现序列化规则所以可以直接控制哪些变量需要序列化,所以开发工作量较大 可以自己决定输出内容,只会固定输出协议和包名类名,较为简洁,对于小对象的序列化Externalizable会快一些 必须有无参构造函数否则编译会出错 ​ 但是,普遍实际项目开发中对于原生序列化的使用非常少,我觉得这里面的主要原因还是出在原生的对象流本身设计上一些是否安全的判断过多,加上缓冲区本身大小只有1K有点小,很明显一个16K的对象一次写入硬盘是比1K*16次快很多。尤其是大多数情况下重复对象判断就是在浪费时间,比如一个网站的一条用户信息,根本不会有几个重复字段。所以在很多网上的性能测试案例中,Serializable ​ 因为对象流篇幅过长,加上很多内容是系统安全或者是分隔符标志之类的东西,下面就只挑重点来说。 ObjectOutputStream 先看一眼内部变量一大堆,光看注释根本不知道是干吗用的。大致分类一下,内部类Caches用于安全审计缓存。一面一块是用于输出的部分,bout是下层输出流,两个表是用于记录已输出对象的缓存便于之前说的重复输出的时候输出上一个相同内容的位置。接下来两个是writeObject()/writeExternal()上行调用记录上下文用的。debugInfoStack用于存储错误信息。 private static class Caches { /** cache of subclass security audit results 子类安全审计结果缓存*/ static final ConcurrentMap<WeakClassKey,Boolean> subclassAudits = new ConcurrentHashMap<>(); /** queue for WeakReferences to audited subclasses 对审计子类弱引用的队列*/ static final ReferenceQueue<Class<?>> subclassAuditsQueue = new ReferenceQueue<>(); } /** filter stream for handling block data conversion 解决块数据转换的过滤流*/ private final BlockDataOutputStream bout; /** obj -> wire handle map obj->线性句柄映射*/ private final HandleTable handles; /** obj -> replacement obj map obj->替代obj映射*/ private final ReplaceTable subs; /** stream protocol version 流协议版本*/ private int protocol = PROTOCOL_VERSION_2; /** recursion depth 递归深度*/ private int depth; /** buffer for writing primitive field values 写基本数据类型字段值缓冲区*/ private byte[] primVals; /** if true, invoke writeObjectOverride() instead of writeObject() 如果为true,调用writeObjectOverride()来替代writeObject()*/ private final boolean enableOverride; /** if true, invoke replaceObject() 如果为true,调用replaceObject()*/ private boolean enableReplace; //下面的值只在上行调用writeObject()/writeExternal()时有效 /** * 上行调用类定义的writeObject方法时的上下文,持有当前被序列化的对象和当前对象描述符。在非writeObject上行调用时为null */ private SerialCallbackContext curContext; /** current PutField object 当前PutField对象*/ private PutFieldImpl curPut; /** custom storage for debug trace info 常规存储用于debug追踪信息*/ private final DebugTraceInfoStack debugInfoStack; 构造函数有两个,第一个是自身的构造需要提供一个输出流,第二个实际上是提供给子类用的,创建一个自身相关内部变量全为空的对象输出流。但是,两个构造器都会进行安全检查,检查序列化的类是否重写了安全敏感方法,如果违反了规则会抛出异常。正常的构造类还会直接输出头部信息,包括对象输出流的魔数和协议版本信息,所以即使只新建一个对象输出流就会输出头部信息。 /** * 创建一个ObjectOutputStream写到指定的OutputStream。这个构造器写序列化流头部到下层流中, * 调用者可能希望立即刷新流来确保接收的ObjectInputStreams构造器不会再读取头部时阻塞。 * 如果一个安全管理器被安装,这个构造器将会在被直接调用和被子类的构造器间接调用时检查enableSubclassImplementation序列化许可, * 如果这个子类重写了ObjectOutputStream.putFields或者ObjectOutputStream.writeUnshared方法 */ public ObjectOutputStream(OutputStream out) throws IOException { verifySubclass(); bout = new BlockDataOutputStream(out);//通过下层流out创建一个块输出流 handles = new HandleTable(10, (float) 3.00); subs = new ReplaceTable(10, (float) 3.00); enableOverride = false; writeStreamHeader(); bout.setBlockDataMode(true);//默认采用块模式 if (extendedDebugInfo) { debugInfoStack = new DebugTraceInfoStack(); } else { debugInfoStack = null; } } /** * 给子类提供一个路径完全重新实现ObjectOutputStream,不会分配任何用于实现ObjectOutputStream的私有数据 * 如果安装了一个安全管理器,这个方法会先调用安全管理器的checkPermission方法来检查序列化许可来确保可以使用子类 */ protected ObjectOutputStream() throws IOException, SecurityException { SecurityManager sm = System.getSecurityManager(); if (sm != null) { sm.checkPermission(SUBCLASS_IMPLEMENTATION_PERMISSION); } bout = null; handles = null; subs = null; enableOverride = true; debugInfoStack = null; } /** * 验证这个实例(可能是子类)可以不用违背安全约束被构造:子类不能重写安全敏感的非final方法,或者其他enableSubclassImplementation序列化许可检查 * 这个检查会增加运行时开支 */ private void verifySubclass() { Class<?> cl = getClass(); if (cl == ObjectOutputStream.class) { return;//不是子类直接返回 } SecurityManager sm = System.getSecurityManager(); if (sm == null) { return;//没有安全管理器直接返回 } processQueue(Caches.subclassAuditsQueue, Caches.subclassAudits);//从弱引用队列中出队所有类,并移除缓存中相同的类 WeakClassKey key = new WeakClassKey(cl, Caches.subclassAuditsQueue); Boolean result = Caches.subclassAudits.get(key);//缓存中是否已有这个类 if (result == null) { result = Boolean.valueOf(auditSubclass(cl));//检查这个子类是否安全 Caches.subclassAudits.putIfAbsent(key, result);//将结果存储到缓存 } if (result.booleanValue()) { return;//子类安全直接返回 } sm.checkPermission(SUBCLASS_IMPLEMENTATION_PERMISSION);//检查子类实现许可 } /** * 提供writeStreamHeader方法这样子类可以扩展或者预先考虑它们自己的流头部。 * 这个方法写魔数和版本到流中。 * * @throws IOException if I/O errors occur while writing to the underlying * stream */ protected void writeStreamHeader() throws IOException { bout.writeShort(STREAM_MAGIC);//流魔数 bout.writeShort(STREAM_VERSION);//流版本 } 接下来开始关键部分,来分析writeObject到底做了什么,首先看这个方法本身是final方法也就是说即使继承了ObjectOutputStream也不能重写这个方法而是重写writeObjectOverride并且enableOverride=true public final void writeObject(Object obj) throws IOException { if (enableOverride) { writeObjectOverride(obj);//如果流子类重写了writeObject则调用这里的方法 return; } try { writeObject0(obj, false); } catch (IOException ex) { if (depth == 0) { writeFatalException(ex); } throw ex; } } writeObject0这个方法代码很长,一部分一部分来看,首先我们注意到上面的都是缓存替换部分,第一次进入这个方法是不需要考虑的,直接看到writeOrdinaryObject这里,因为用于数据化的类是实现了Serializable接口,所以会进入这个分支。 private void writeObject0(Object obj, boolean unshared) throws IOException { boolean oldMode = bout.setBlockDataMode(false);//将输出流设置为非块模式 depth++;//增加递归深度 try { // handle previously written and non-replaceable objects处理之前写的不可替换对象 int h; if ((obj = subs.lookup(obj)) == null) { writeNull();//替代对象映射中这个对象为null时,写入null代码 return; } else if (!unshared && (h = handles.lookup(obj)) != -1) { writeHandle(h);//不是非共享模式且这个对象在对句柄的映射表中已有缓存,写入该对象在缓存中的句柄值 return; } else if (obj instanceof Class) { writeClass((Class) obj, unshared);//写类名 return; } else if (obj instanceof ObjectStreamClass) { writeClassDesc((ObjectStreamClass) obj, unshared);//写类描述 return; } // check for replacement object检查替代对象,要求对象重写了writeReplace方法 Object orig = obj; Class<?> cl = obj.getClass(); ObjectStreamClass desc; for (;;) { // REMIND: skip this check for strings/arrays? Class<?> repCl; desc = ObjectStreamClass.lookup(cl, true); if (!desc.hasWriteReplaceMethod() || (obj = desc.invokeWriteReplace(obj)) == null || (repCl = obj.getClass()) == cl) { break; } cl = repCl; } if (enableReplace) { Object rep = replaceObject(obj);//如果不重写这个方法直接返回了obj也就是什么也没做 if (rep != obj && rep != null) { cl = rep.getClass(); desc = ObjectStreamClass.lookup(cl, true); } obj = rep; } // if object replaced, run through original checks a second time如果对象被替换,第二次运行原本的检查,大部分情况下不执行此段 if (obj != orig) { subs.assign(orig, obj);//将原本对象和替代对象作为一个键值对存入缓存 if (obj == null) { writeNull(); return; } else if (!unshared && (h = handles.lookup(obj)) != -1) { writeHandle(h); return; } else if (obj instanceof Class) { writeClass((Class) obj, unshared); return; } else if (obj instanceof ObjectStreamClass) { writeClassDesc((ObjectStreamClass) obj, unshared); return; } } // remaining cases剩下的情况 if (obj instanceof String) { writeString((String) obj, unshared); } else if (cl.isArray()) { writeArray(obj, desc, unshared); } else if (obj instanceof Enum) { writeEnum((Enum<?>) obj, desc, unshared); } else if (obj instanceof Serializable) { writeOrdinaryObject(obj, desc, unshared);//传入流的对象第一次执行这个方法 } else { if (extendedDebugInfo) { throw new NotSerializableException( cl.getName() + "\n" + debugInfoStack.toString()); } else { throw new NotSerializableException(cl.getName()); } } } finally { depth--; bout.setBlockDataMode(oldMode); } } writeOrdinaryObject这个方法主要是在Externalizable和Serializable的接口出现分支,如果实现了Externalizable接口并且类描述符非动态代理,则执行writeExternalData,否则执行writeSerialData。同时,这个方法会写类描述信息。 private void writeOrdinaryObject(Object obj, ObjectStreamClass desc, boolean unshared) throws IOException { if (extendedDebugInfo) { debugInfoStack.push( (depth == 1 ? "root " : "") + "object (class \"" + obj.getClass().getName() + "\", " + obj.toString() + ")"); } try { desc.checkSerialize(); bout.writeByte(TC_OBJECT); writeClassDesc(desc, false);//写类描述 handles.assign(unshared ? null : obj);//如果是share模式把这个对象加入缓存 if (desc.isExternalizable() && !desc.isProxy()) { writeExternalData((Externalizable) obj); } else { writeSerialData(obj, desc); } } finally { if (extendedDebugInfo) { debugInfoStack.pop(); } } } writeExternalData和writeSerialData(Object, ObjectStreamClass)这里有个上下文的操作,目的是保证序列化操作同一时间只能由一个线程调用。前者直接调用writeExternal,后者如果重写了writeObject则调用它,否则调用defaultWriteFields。defaultWriteFields会先输出基本数据类型,对于非基本数据类型的部分会再递归调用writeObject0,所以这里也就会增加递归深度depth。 private void writeExternalData(Externalizable obj) throws IOException { PutFieldImpl oldPut = curPut; curPut = null; if (extendedDebugInfo) { debugInfoStack.push("writeExternal data"); } SerialCallbackContext oldContext = curContext;//存储上下文 try { curContext = null; if (protocol == PROTOCOL_VERSION_1) { obj.writeExternal(this); } else {//默认协议是2,所以会使用块输出流 bout.setBlockDataMode(true); obj.writeExternal(this);//这里取决于类的方法怎么实现 bout.setBlockDataMode(false); bout.writeByte(TC_ENDBLOCKDATA); } } finally { curContext = oldContext;//恢复上下文 if (extendedDebugInfo) { debugInfoStack.pop(); } } curPut = oldPut; } private void writeSerialData(Object obj, ObjectStreamClass desc) throws IOException { ObjectStreamClass.ClassDataSlot[] slots = desc.getClassDataLayout(); for (int i = 0; i < slots.length; i++) { ObjectStreamClass slotDesc = slots[i].desc; if (slotDesc.hasWriteObjectMethod()) {//重写了writeObject方法 PutFieldImpl oldPut = curPut; curPut = null; SerialCallbackContext oldContext = curContext; if (extendedDebugInfo) { debugInfoStack.push( "custom writeObject data (class \"" + slotDesc.getName() + "\")"); } try { curContext = new SerialCallbackContext(obj, slotDesc); bout.setBlockDataMode(true); slotDesc.invokeWriteObject(obj, this);//调用writeObject方法 bout.setBlockDataMode(false); bout.writeByte(TC_ENDBLOCKDATA); } finally { curContext.setUsed(); curContext = oldContext; if (extendedDebugInfo) { debugInfoStack.pop(); } } curPut = oldPut; } else { defaultWriteFields(obj, slotDesc);//如果没有重写writeObject则输出默认内容 } } } private void defaultWriteFields(Object obj, ObjectStreamClass desc) throws IOException { Class<?> cl = desc.forClass(); if (cl != null && obj != null && !cl.isInstance(obj)) { throw new ClassCastException(); } desc.checkDefaultSerialize(); int primDataSize = desc.getPrimDataSize(); if (primVals == null || primVals.length < primDataSize) { primVals = new byte[primDataSize]; } desc.getPrimFieldValues(obj, primVals);//将基本类型数据的字段值存入缓冲区 bout.write(primVals, 0, primDataSize, false);//输出缓冲区内容 ObjectStreamField[] fields = desc.getFields(false); Object[] objVals = new Object[desc.getNumObjFields()];//获取非基本数据类型对象 int numPrimFields = fields.length - objVals.length; desc.getObjFieldValues(obj, objVals); for (int i = 0; i < objVals.length; i++) { if (extendedDebugInfo) { debugInfoStack.push( "field (class \"" + desc.getName() + "\", name: \"" + fields[numPrimFields + i].getName() + "\", type: \"" + fields[numPrimFields + i].getType() + "\")"); } try { writeObject0(objVals[i], fields[numPrimFields + i].isUnshared());//递归输出 } finally { if (extendedDebugInfo) { debugInfoStack.pop(); } } } } 然后看一下类描述信息是怎么写的,动态代理类和普通类有一些区别,但都是先写这个类本身的信息再写入父类的信息。 private void writeClassDesc(ObjectStreamClass desc, boolean unshared) throws IOException { int handle; if (desc == null) { writeNull();//描述符不存在时写null } else if (!unshared && (handle = handles.lookup(desc)) != -1) { writeHandle(handle);//共享模式且缓存中已有该类描述符时,写对应句柄值 } else if (desc.isProxy()) { writeProxyDesc(desc, unshared);//描述符是动态代理类时 } else { writeNonProxyDesc(desc, unshared);//描述符是标准类时 } } private void writeProxyDesc(ObjectStreamClass desc, boolean unshared) throws IOException { bout.writeByte(TC_PROXYCLASSDESC); handles.assign(unshared ? null : desc);//存入缓存 //获取类实现的接口,然后写入接口个数和接口名 Class<?> cl = desc.forClass(); Class<?>[] ifaces = cl.getInterfaces(); bout.writeInt(ifaces.length); for (int i = 0; i < ifaces.length; i++) { bout.writeUTF(ifaces[i].getName()); } bout.setBlockDataMode(true); if (cl != null && isCustomSubclass()) { ReflectUtil.checkPackageAccess(cl); } annotateProxyClass(cl);//装配动态代理类,子类可以重写这个方法存储类信息到流中,默认什么也不做 bout.setBlockDataMode(false); bout.writeByte(TC_ENDBLOCKDATA); writeClassDesc(desc.getSuperDesc(), false);//写入父类的描述符 } private void writeNonProxyDesc(ObjectStreamClass desc, boolean unshared) throws IOException { bout.writeByte(TC_CLASSDESC); handles.assign(unshared ? null : desc); if (protocol == PROTOCOL_VERSION_1) { // do not invoke class descriptor write hook with old protocol desc.writeNonProxy(this); } else { writeClassDescriptor(desc); } Class<?> cl = desc.forClass(); bout.setBlockDataMode(true); if (cl != null && isCustomSubclass()) { ReflectUtil.checkPackageAccess(cl); } annotateClass(cl);//子类可以重写这个方法存储类信息到流中,默认什么也不做 bout.setBlockDataMode(false); bout.writeByte(TC_ENDBLOCKDATA); writeClassDesc(desc.getSuperDesc(), false);//写入父类的描述信息 } 最后看一下几个写方法,写字符串是写入UTF编码的二进制流数据。写枚举会额外写入一次枚举的类描述,然后将枚举名作为字符串写入。如果是写一个数组,先写入数组长度,然后如果数组是基本数据类型则可以直接写入,否则需要递归调用writeObject0 private void writeString(String str, boolean unshared) throws IOException { handles.assign(unshared ? null : str); long utflen = bout.getUTFLength(str);//获得UTF编码长度 if (utflen <= 0xFFFF) { bout.writeByte(TC_STRING); bout.writeUTF(str, utflen); } else { bout.writeByte(TC_LONGSTRING); bout.writeLongUTF(str, utflen); } } private void writeEnum(Enum<?> en, ObjectStreamClass desc, boolean unshared) throws IOException { bout.writeByte(TC_ENUM); ObjectStreamClass sdesc = desc.getSuperDesc(); writeClassDesc((sdesc.forClass() == Enum.class) ? desc : sdesc, false); handles.assign(unshared ? null : en); writeString(en.name(), false); } BlockDataOutputStream BlockDataOutputStream是一个内部类,它继承了OutputStream并实现了DataOutput接口,缓冲输出流有两种模式:在默认模式下,输出数据和DataOutputStream使用同样模式;在块数据模式下,使用一个缓冲区来缓存数据到达最大长度或者手动刷新时将内容写入下层输入流,这点和BufferedOutputStream类似。不同之处在于,块模式在写数据之前,要先写入一个头部来表示当前块的长度。 从内部变量和构造函数中可以看出,缓冲区的大小是固定且不可修改的,其中包含了一个下层输入流和一个数据输出流以及是否采用块模式的标识,在构造时默认不采用块数据模式。 /** maximum data block length 最大数据块长度1K*/ private static final int MAX_BLOCK_SIZE = 1024; /** maximum data block header length 最大数据块头部长度*/ private static final int MAX_HEADER_SIZE = 5; /** (tunable) length of char buffer (for writing strings) 字符缓冲区的可变长度,用于写字符串*/ private static final int CHAR_BUF_SIZE = 256; /** buffer for writing general/block data 用于写一般/块数据的缓冲区*/ private final byte[] buf = new byte[MAX_BLOCK_SIZE]; /** buffer for writing block data headers 用于写块数据头部的缓冲区*/ private final byte[] hbuf = new byte[MAX_HEADER_SIZE]; /** char buffer for fast string writes 用于写快速字符串的缓冲区*/ private final char[] cbuf = new char[CHAR_BUF_SIZE]; /** block data mode 块数据模式*/ private boolean blkmode = false; /** current offset into buf buf中的当前偏移量*/ private int pos = 0; /** underlying output stream 下层输出流*/ private final OutputStream out; /** loopback stream (for data writes that span data blocks) 回路流用于写跨越数据块的数据*/ private final DataOutputStream dout; /** * 在给定的下层流上创建一个BlockDataOutputStream,块数据模式默认关闭 */ BlockDataOutputStream(OutputStream out) { this.out = out; dout = new DataOutputStream(this); } setBlockDataMode可以改变当前的数据模式,从块数据模式切换到非块数据模式时,要讲缓冲区内的数据写入到下层输入流中。getBlockDataMode可以查询当前的数据模式。 /** * 设置块数据模式为给出的模式true是开启,false是关闭,并返回之前的模式值。 * 如果新的模式和旧的一样,什么都不做。 * 如果新的模式和旧的模式不同,所有的缓冲区数据要在转换到新模式之前刷新。 */ boolean setBlockDataMode(boolean mode) throws IOException { if (blkmode == mode) { return blkmode; } drain();//将缓冲区内的数据全部写入下层输入流 blkmode = mode; return !blkmode; } /** * 当前流为块数据模式返回true,否则返回false */ boolean getBlockDataMode() { return blkmode; } drain这个方法在多个方法中被调用,作用是将缓冲区内的数据全部写入下层输入流,但不会刷新下层输入流,在写入实际数据前要先用writeBlockHeader写入块头部,头部包含1字节标志位和1字节或4字节的长度大小 void drain() throws IOException { if (pos == 0) { return;//pos为0说明当前缓冲区为空 } if (blkmode) { writeBlockHeader(pos);//块数据模式下要先写入头部 } out.write(buf, 0, pos);//写入缓冲区数据 pos = 0;//缓冲区被清空 } /** * 写入块数据头部。数据块小于256字节会增加2字节头部前缀,其他会增加5字节头部。 * 第一字节是标识长度范围,因为255字节以内可以用1字节来表示长度,4字节可以表示int范围内的最大整数 */ private void writeBlockHeader(int len) throws IOException { if (len <= 0xFF) { hbuf[0] = TC_BLOCKDATA; hbuf[1] = (byte) len; out.write(hbuf, 0, 2); } else { hbuf[0] = TC_BLOCKDATALONG; Bits.putInt(hbuf, 1, len); out.write(hbuf, 0, 5); } } 下面的方法等价于他们在OutputStream中的对应方法,除了他们参与在块数据模式下写入数据到数据块中的部分有所不同。写入都需要先检查缓冲区有没有达到上限,达到时需要先刷新,然后再将数据复制到缓冲区。刷新和关闭操作都不难理解。 public void write(int b) throws IOException { if (pos >= MAX_BLOCK_SIZE) { drain();//达到块数据上限时,将缓冲区内的数据全部写入下层流 } buf[pos++] = (byte) b;//存储b到buf中 } public void write(byte[] b) throws IOException { write(b, 0, b.length, false); } public void write(byte[] b, int off, int len) throws IOException { write(b, off, len, false); } /** * 将指定的字节段从数组中写出。如果copy是true,复制值到一个中间缓冲区在将它们写入下层流之前,来避免暴露一个对原字节数组的引用 */ void write(byte[] b, int off, int len, boolean copy) throws IOException { if (!(copy || blkmode)) {// 非copy也非块数据模式直接写入下层输入流 drain(); out.write(b, off, len); return; } while (len > 0) { if (pos >= MAX_BLOCK_SIZE) { drain(); } if (len >= MAX_BLOCK_SIZE && !copy && pos == 0) { // 长度大于缓冲区非copy模式且缓冲区为空直接写,避免不必要的复制 writeBlockHeader(MAX_BLOCK_SIZE); out.write(b, off, MAX_BLOCK_SIZE); off += MAX_BLOCK_SIZE; len -= MAX_BLOCK_SIZE; } else { //剩余内容在缓冲区内放得下或者缓冲区不为空或者是copy模式,则将数据复制到缓冲区 int wlen = Math.min(len, MAX_BLOCK_SIZE - pos); System.arraycopy(b, off, buf, pos, wlen); pos += wlen; off += wlen; len -= wlen; } } } /** * 将缓冲区数据刷新到下层流,同时会刷新下层流 */ public void flush() throws IOException { drain(); out.flush(); } /** * 刷新之后关闭下层输出流 */ public void close() throws IOException { flush(); out.close(); } 上面的方法等价于他们在DataOutputStream中的对应方法,除了他们参与在块数据模式下写入数据到数据块中部分有所不同。基本上逻辑都是先检查空间是否足够,不足的话先刷新缓冲区,然后将数据存储到缓冲区中。因为篇幅原因,这里只贴几个方法为例。写一个字符串时,需要先将字符串中的字符存储到字符缓冲数组中,然后再转换成字节存储到buf中。 public void writeBoolean(boolean v) throws IOException { if (pos >= MAX_BLOCK_SIZE) { drain(); } Bits.putBoolean(buf, pos++, v); } public void writeByte(int v) throws IOException { if (pos >= MAX_BLOCK_SIZE) { drain(); } buf[pos++] = (byte) v; } /** * 写入单个字符,块未满时存储到缓冲区,块满时调用的是BlockDataOutputStream.write(int v)方法 */ public void writeChar(int v) throws IOException { if (pos + 2 <= MAX_BLOCK_SIZE) { Bits.putChar(buf, pos, (char) v); pos += 2; } else { dout.writeChar(v); } } /** * 先将String中的内容复制到字符缓冲区,再将其中的内容转为字节复制到块数据缓冲区 */ public void writeBytes(String s) throws IOException { int endoff = s.length(); int cpos = 0;//当前字符串开始位置 int csize = 0;//当前字符串大小 for (int off = 0; off < endoff; ) { if (cpos >= csize) { cpos = 0; csize = Math.min(endoff - off, CHAR_BUF_SIZE); s.getChars(off, off + csize, cbuf, 0);//将字符串中指定位置的片段复制到字符数组缓冲区 } if (pos >= MAX_BLOCK_SIZE) { drain(); } int n = Math.min(csize - cpos, MAX_BLOCK_SIZE - pos); int stop = pos + n; while (pos < stop) { buf[pos++] = (byte) cbuf[cpos++];//将字符数组中的内容复制到块数据缓冲区 } off += n; } } 下面的方法写出连贯的原始数据值。尽管和重复调用对应的原始写方法结果相同,这些方法对于写一组原始数据值进行了效率优化。优化的方式是先计算出缓冲区内的剩余大小,计算可以写入的个数,然后直接写入而不是每次写入之前检查缓冲区是否有空间,减少判断次数。写UTF编码字符串时,如果能够提前知道编码长度,可以省去一次遍历字符串确定大小的过程,因为UTF编码中单个字符可能是一个1-3个字节不等。 void writeBooleans(boolean[] v, int off, int len) throws IOException { int endoff = off + len; while (off < endoff) { if (pos >= MAX_BLOCK_SIZE) { drain(); } int stop = Math.min(endoff, off + (MAX_BLOCK_SIZE - pos)); while (off < stop) {//连续存储数据到缓冲区,减少了判断缓冲区是否满的次数 Bits.putBoolean(buf, pos++, v[off++]); } } } void writeChars(char[] v, int off, int len) throws IOException { int limit = MAX_BLOCK_SIZE - 2; int endoff = off + len; while (off < endoff) { if (pos <= limit) { int avail = (MAX_BLOCK_SIZE - pos) >> 1;//一个字符=2个字节所以要除以2 int stop = Math.min(endoff, off + avail); while (off < stop) { Bits.putChar(buf, pos, v[off++]); pos += 2; } } else { dout.writeChar(v[off++]); } } } /** * 返回给定字符串在UTF编码下的字节长度 */ long getUTFLength(String s) { int len = s.length(); long utflen = 0; for (int off = 0; off < len; ) { int csize = Math.min(len - off, CHAR_BUF_SIZE); s.getChars(off, off + csize, cbuf, 0); for (int cpos = 0; cpos < csize; cpos++) { char c = cbuf[cpos]; if (c >= 0x0001 && c <= 0x007F) { utflen++; } else if (c > 0x07FF) { utflen += 3; } else { utflen += 2; } } off += csize; } return utflen; } /** * 写给定字符串的UTF格式。这个方法用于字符串的UTF编码长度已知的情况,这样可以避免提前扫描一遍字符串来确定UTF长度 */ void writeUTF(String s, long utflen) throws IOException { if (utflen > 0xFFFFL) { throw new UTFDataFormatException(); } writeShort((int) utflen);//先写长度 if (utflen == (long) s.length()) { writeBytes(s);//没有特殊字符 } else { writeUTFBody(s);//有特殊字符 } } HandleTable HandleTable是一个轻量的hash表,它的作用是缓存写过的共享class便于下次查找,内部含有3个数组,spine、next和objs。objs存储的是对象也就是class,spine是hash桶,next是冲突链表,每有一个新的元素插入需要计算它的hash值然后用spine的大小取模,找到它的链表,新对象会被插入到链表的头部,它在objs和next中对应的数据是根据加入的序号顺序存储,spine存储它的handle值也就是在另外两个数组中的下标。 /** number of mappings in table/next available handle 表中映射的个数或者下一个有效的句柄*/ private int size; /** size threshold determining when to expand hash spine 决定什么时候扩展hash脊柱的大小阈值*/ private int threshold; /** factor for computing size threshold 计算大小阈值的因子*/ private final float loadFactor; /** maps hash value -> candidate handle value 映射hash值->候选句柄值*/ private int[] spine; /** maps handle value -> next candidate handle value 映射句柄值->下一个候选句柄值*/ private int[] next; /** maps handle value -> associated object 映射句柄值->关联的对象*/ private Object[] objs; /** * 创建一个新的hash表使用给定的容量和负载因子 */ HandleTable(int initialCapacity, float loadFactor) { this.loadFactor = loadFactor; spine = new int[initialCapacity]; next = new int[initialCapacity]; objs = new Object[initialCapacity]; threshold = (int) (initialCapacity * loadFactor); clear(); } assign就是插入操作,它会检查3个数组大小是否足够,其中spine是根据next.length*负载因子来决定阈值的,数组大小扩大是乘以2加1,这个和HashTable时同样的设计。插入的时候注意到next的值被赋为原本的spine[index]值,说明之前的链表头成为了新结点的后驱,也就是说结点被插入链表头部。 /** * 分配下一个有效的句柄给给出的对象并返回句柄值。句柄从0开始升序被分配。相当于put操作 */ int assign(Object obj) { if (size >= next.length) { growEntries(); } if (size >= threshold) { growSpine(); } insert(obj, size); return size++; } /** * 通过延长条目数组增加hash表容量,next和objs大小变为旧大小*2+1 */ private void growEntries() { int newLength = (next.length << 1) + 1;//长度=旧长度*2+1 int[] newNext = new int[newLength]; System.arraycopy(next, 0, newNext, 0, size);//复制旧数组元素到新数组中 next = newNext; Object[] newObjs = new Object[newLength]; System.arraycopy(objs, 0, newObjs, 0, size); objs = newObjs; } /** * 扩展hash脊柱,等效于增加常规hash表的桶数 */ private void growSpine() { spine = new int[(spine.length << 1) + 1];//新大小=旧大小*2+1 threshold = (int) (spine.length * loadFactor);//扩展阈值=spine大小*负载因子 Arrays.fill(spine, -1);//spine中全部填充-1 for (int i = 0; i < size; i++) { insert(objs[i], i); } } /** * 插入映射对象->句柄到表中,假设表足够大来容纳新的映射 */ private void insert(Object obj, int handle) { int index = hash(obj) % spine.length;//hash值%spine数组大小 objs[handle] = obj;//objs顺序存储对象 next[handle] = spine[index];//next存储spine[index]原本的handle值,也就是说新的冲突对象插入在链表头部 spine[index] = handle;//spine中存储handle大小 } hash值计算就是通过系统函数计算出hash值然后去有符号int的有效位 private int hash(Object obj) { return System.identityHashCode(obj) & 0x7FFFFFFF;//取系统计算出的hash值得有效整数值部分 } lookup是查找hash表中是否含有指定对象,这里相等必须是==,因为class在完整类名相等时就是== /** * 查找并返回句柄值关联给与的对象,如果没有映射返回-1 */ int lookup(Object obj) { if (size == 0) { return -1; } int index = hash(obj) % spine.length;//通过hash值寻找在spine数组中的位置 for (int i = spine[index]; i >= 0; i = next[i]) { if (objs[i] == obj) {//遍历spine[index]位置的链表,必须是对象==才是相等 return i; } } return -1; } clear是清空hash表,size返回当前表中映射对数 /** * 重置表为初始状态,next不需要重新赋值是因为插入第一个元素时,原本的spine[index]一定是-1,链表中不会出现之前存在的值 */ void clear() { Arrays.fill(spine, -1); Arrays.fill(objs, 0, size, null); size = 0; } /** * 返回当前表中的映射数量 */ int size() { return size; }

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

Kotlin 设计模式解析之单例

单例模式介绍 单例模式是一个比较简单的设计模式,同时也是挺有意思的一个模式,虽然看起来简单,但是可以玩出各种花样。比如 Java 当中的懒饿汉式单例等。 什么是单例 单例模式的定义: Ensure a class only has one instance, and provide a global point of access to it. 简单来说,确保某一个类只有一个实例,且自行实例化并向整个系统提供。 单例模式的适用场景 提供一个全局的访问,且只要求一个实例,如应用的配置信息 创建一个对象比较耗费资源,如数据库连接管理、文件管理、日志管理等 资源共享,如线程池 工具类对象(也可以直接使用静态常量或者静态方法) 要求一个类只能产生两三个实例对象,比如某些场景下,会要求两个版本的网络库实例,如公司内网和外网的网络库实例 单例模式的简单实现 Java

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

POI框架EXCEL解析性能优化

背景 在做商品EXCEL的时候,线上发现了Full GC,排查得知是商家搞了一个巨大的excel,单商品发布接口平均耗时400ms(调用sell耗时200ms左右,系统自身处理商品同步耗时150ms左右),对于3000个商品的发布,耗时在20min左右,这20min内该excel的内存一直未能释放。 第一时间想到的是POI真坑,真吃内存。 事情发生了就想着怎么处理, 止血 线上机器分批重启, 马上加一个excel行数的限制然后发布 线上半个小时左右就没有任何问题了。 思考 为什么poi这么吃内存,poi这么老了,肯定有人踩过这个坑,撸起袖子,搜poi full gc. 很多文档将的都太粗糙了,本质没有说透 原因 excel本质上是xml文件的集合体。从office 2007起开始使用xml来存档和数据交换:https://zh.wikipedia

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

Android多线程之AsyncTask源码解析

AsyncTask 是一个较为轻量级的异步任务类,在底层通过封装 ThreadPool 和 Handler ,实现了线程的复用,后台任务执行顺序的控制、子线程和 UI 线程的切换,使得开发者可以以简单的方法来执行一些耗时任务 此篇文章就基于 Android API 27 版本的源码来对 AsyncTask 进行一次整体分析,以便对其底层工作流程有所了解 一般,AsyncTask 是以类似于以下的方式来调用的 new AsyncTask<String, Integer, String>() { @Override protected String doInBackground(String... strings) { return null; } }.execute("leavesC"); 所以这里就从 execute() 方法入手 //以默认的串行任务执行器 sDefaultExecutor 来执行后台任务 @MainThread public final AsyncTask<Params, Progress, Result> execute(Params... params) { return executeOnExecutor(sDefaultExecutor, params); } execute(Params)方法内部调用的是 executeOnExecutor(sDefaultExecutor, params)方法,当中 sDefaultExecutor用于定义任务队列的执行方式,AsyncTask 默认使用的是串行任务执行器 //以指定的任务执行器 Executor 来执行后台任务 @MainThread public final AsyncTask<Params, Progress, Result> executeOnExecutor(Executor exec, Params... params) { //Task 只能被执行一次,如果 mStatus != Status.PENDING ,说明 Task 被重复执行,此时将抛出异常 if (mStatus != Status.PENDING) { switch (mStatus) { case RUNNING: throw new IllegalStateException("Cannot execute task:" + " the task is already running."); case FINISHED: throw new IllegalStateException("Cannot execute task:" + " the task has already been executed " + "(a task can be executed only once)"); } } //将状态值置为运行状态 mStatus = Status.RUNNING; //在 doInBackground() 方法之前被调用,用于做一些界面层的准备工作 onPreExecute(); //执行耗时任务 mWorker.mParams = params; exec.execute(mFuture); return this; } mStatus是一个枚举变量,用于定义当前 Task 的运行状态,用于防止 Task 被重复执行 //用于标记 Task 的当前状态 public enum Status { //Task 还未运行 PENDING, //Task 正在运行 RUNNING, //Task 已经结束 FINISHED, } 之后就调用任务执行器,提交任务 //执行耗时任务 mWorker.mParams = params; exec.execute(mFuture); executeOnExecutor(Executor, Params)方法可以从外部传入自定义的任务执行器对象,例如可以传入 AsyncTask.THREAD_POOL_EXECUTOR 使 AsyncTask 中的任务队列以并行的方式来完成 这里先来看下默认的串行任务执行器是如何执行的 每一个被提交的任务都会被加入任务队列 mTasks当中,mActive表示当前在执行的任务,每当有新任务 Runnable 到来时,就会在 Runnable 的外层多包裹一层 Runnable ,然后将之插入到任务队列中,当 execute(Runnable)方法第一次被执行时,mActive为 null ,因此就会触发 scheduleNext()方法获取任务队列的第一个任务并提交给线程池 THREAD_POOL_EXECUTOR 进行处理,当 r.run()方法返回时(即任务处理结束),在 finally中又会获取下一个任务进行处理,从而实现了任务队列的串行执行 //串行任务执行器,即提交给线程池的任务是按照顺序一个接一个被执行的 private static class SerialExecutor implements Executor { //任务队列 final ArrayDeque<Runnable> mTasks = new ArrayDeque<Runnable>(); //当前在执行的任务 Runnable mActive; public synchronized void execute(final Runnable r) { //向任务队列尾端插入任务 //在外部任务外部包装多一层 Runnable mTasks.offer(new Runnable() { public void run() { try { r.run(); } finally { scheduleNext(); } } }); //如果当前没有在执行任务,则调取队列中的任务进行处理 if (mActive == null) { scheduleNext(); } } //获取队列的首个任务并处理 protected synchronized void scheduleNext() { if ((mActive = mTasks.poll()) != null) { THREAD_POOL_EXECUTOR.execute(mActive); } } } 再看下线程池 THREAD_POOL_EXECUTOR 是如何定义的 可以看到,具体的线程池实现类是 ThreadPoolExecutor,使用线程池从而避免了线程重复的创建与销毁操作,有利于提高系统性能 //CPU 核数量 private static final int CPU_COUNT = Runtime.getRuntime().availableProcessors(); //线程池中的核心线程数 //至少有2个,最多4个,线程数至少要比 CPU 核数量少1个,以避免 CPU 与后台工作饱和 private static final int CORE_POOL_SIZE = Math.max(2, Math.min(CPU_COUNT - 1, 4)); //线程池容纳的最大线程数量 private static final int MAXIMUM_POOL_SIZE = CPU_COUNT * 2 + 1; //线程在闲置时的存活时间(30秒),超出这个时间将被回收 private static final int KEEP_ALIVE_SECONDS = 30; //线程队列 //当 LinkedBlockingDeque 已满时,新增的任务会直接创建新线程来执行,当创建的线程数量超过最大线程数量 KEEP_ALIVE_SECONDS 时会抛出异常 private static final BlockingQueue<Runnable> sPoolWorkQueue = new LinkedBlockingQueue<Runnable>(128); //线程工厂,提供创建新线程的功能,通过线程工厂可以对线程的一些属性进行定制 private static final ThreadFactory sThreadFactory = new ThreadFactory() { private final AtomicInteger mCount = new AtomicInteger(1); public Thread newThread(Runnable r) { return new Thread(r, "AsyncTask #" + mCount.getAndIncrement()); } }; //线程池对象 public static final Executor THREAD_POOL_EXECUTOR; static { ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor( CORE_POOL_SIZE, MAXIMUM_POOL_SIZE, KEEP_ALIVE_SECONDS, TimeUnit.SECONDS, sPoolWorkQueue, sThreadFactory); //包括核心线程在内的所有线程在闲置时间超出 KEEP_ALIVE_SECONDS 后都将其回收 threadPoolExecutor.allowCoreThreadTimeOut(true); THREAD_POOL_EXECUTOR = threadPoolExecutor; } //当前 Task 使用的任务执行器 private static volatile Executor sDefaultExecutor = SERIAL_EXECUTOR; 看到线程池,这里就又引出了另外一个问题,后台任务是在子线程中调用的,那 AsyncTask 又是如何在 UI 线程中回调 onPreExecute()、onPostExecute(Result)、onProgressUpdate(Progress)这几个方法的呢? 先看几个相关方法的声明 //在子线程中被调用,用于执行后台任务 @WorkerThread protected abstract Result doInBackground(Params... params); //在 UI 线程中被调用,在 doInBackground() 方法之前调用,用于在后台任务开始前做一些准备工作 @MainThread protected void onPreExecute() { } //在 UI 线程中被调用,在 doInBackground() 方法之后调用,用于处理后台任务的执行结果 //参数 result 是 doInBackground() 方法的返回值 @SuppressWarnings({"UnusedDeclaration"}) @MainThread protected void onPostExecute(Result result) { } //在 UI 线程中被调用,当调用了 publishProgress() 方法后被触发 //用于更新任务进度值 @SuppressWarnings({"UnusedDeclaration"}) @MainThread protected void onProgressUpdate(Progress... values) { } //在 UI 线程中被调用 //当调用了 cancel(boolean) 方法取消后台任务后会被调用 //在 doInBackground() 方法结束时也会被调用 //方法内部默认调用了 onCancelled() 方法 @SuppressWarnings({"UnusedParameters"}) @MainThread protected void onCancelled(Result result) { onCancelled(); } //在 UI 线程中被调用,被 onCancelled(Result) 方法调用 @MainThread protected void onCancelled() { } onPreExecute()在 executeOnExecutor(Executor, Params)中有被调用,因为 executeOnExecutor()方法被要求在 UI 线程中调用,因此 onPreExecute()自然也会在 UI 线程中被执行 其它方法的调用则涉及到了 Handler、Looper 与 MessageQueue 的相关知识点,关于这些可以从这里获取详细介绍: Java_Android_Learn ,这里就简单介绍下 看下 AsyncTask 类的三个构造函数。当中,除了无参构造函数,其他两个构造函数都使用 @hide注解隐藏起来了,因此我们在一般情况下只能使用调用无参构造函数来初始化 AsyncTask //创建一个新的异步任务,必须在UI线程上调用此构造函数 public AsyncTask() { this((Looper) null); } /** * 隐藏的构造函数 * 创建一个新的异步任务,必须在UI线程上调用此构造函数 * * @hide */ public AsyncTask(@Nullable Handler handler) { this(handler != null ? handler.getLooper() : null); } /** * 隐藏的构造函数 * 创建一个新的异步任务,必须在UI线程上调用此构造函数 * @hide */ public AsyncTask(@Nullable Looper callbackLooper) { //如果 callbackLooper 为 null 或者是等于主线程 Looper ,则以主线程 Looper 对象为参数构建一个与主线程关联的 Handler 对象 //否则就以传入的 Looper 对象为参数来构建与子线程关联的 Handler mHandler = callbackLooper == null || callbackLooper == Looper.getMainLooper() ? getMainHandler() : new Handler(callbackLooper); mWorker = new WorkerRunnable<Params, Result>() { public Result call() throws Exception { mTaskInvoked.set(true); Result result = null; try { Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND); //noinspection unchecked result = doInBackground(mParams); Binder.flushPendingCommands(); } catch (Throwable tr) { mCancelled.set(true); throw tr; } finally { postResult(result); } return result; } }; mFuture = new FutureTask<Result>(mWorker) { @Override protected void done() { try { postResultIfNotInvoked(get()); } catch (InterruptedException e) { android.util.Log.w(LOG_TAG, e); } catch (ExecutionException e) { throw new RuntimeException("An error occurred while executing doInBackground()", e.getCause()); } catch (CancellationException e) { postResultIfNotInvoked(null); } } }; } 因此我们传给构造函数 AsyncTask(Looper) 的参数为 null ,因为 mHandler 变量其实是赋值为绑定了 UI 线程 Looper 的 InternalHandler 变量 因为 InternalHandler 绑定了 UI 线程的 Looper 对象,因此 handleMessage(Message)方法其实是在 UI 线程被执行,从而实现了子线程和 UI 线程之间的切换 //按照正常情况来说,在初始化 AsyncTask 时我们使用的都是其无参构造函数 //因此 InternalHandler 绑定的 Looper 对象即是与主线程关联的 Looper 对象 //所以 InternalHandler 可以用来在 UI 线程回调某些抽象方法,例如 onProgressUpdate() 方法 private static InternalHandler sHandler; //等于 sHandler private final Handler mHandler; private static class InternalHandler extends Handler { public InternalHandler(Looper looper) { super(looper); } @SuppressWarnings({"unchecked", "RawUseOfParameterizedType"}) @Override public void handleMessage(Message msg) { AsyncTaskResult<?> result = (AsyncTaskResult<?>) msg.obj; switch (msg.what) { case MESSAGE_POST_RESULT: //处理后台任务的执行结果 result.mTask.finish(result.mData[0]); break; case MESSAGE_POST_PROGRESS: //更新后台任务的进度 result.mTask.onProgressUpdate(result.mData); break; } } } //获取与主线程关联的 Looper 对象,以此为参数构建一个 Handler 对象 //所以在 Task 的运行过程中,能够通过此 Handler 在 UI 线程执行操作 private static Handler getMainHandler() { synchronized (AsyncTask.class) { if (sHandler == null) { sHandler = new InternalHandler(Looper.getMainLooper()); } return sHandler; } } 例如,在通过 publishProgress(Progress) 方法更新后台任务的执行进度时,在内部就会将进度值包装到 Message 中,然后传递给 Handler 进行处理 //运行于工作线程,此方法用于更新任务的进度值 //会触发 onProgressUpdate() 被执行 @WorkerThread protected final void publishProgress(Progress... values) { if (!isCancelled()) { //将与进度值相关的参数 Progress 包装到 AsyncTaskResult 对象当中,并传递给 Handler 进行处理 getHandler().obtainMessage(MESSAGE_POST_PROGRESS, new AsyncTaskResult<Progress>(this, values)).sendToTarget(); } } 以上就是 AsyncTask 较为关键的几个点,看过后应该就能明白 AsyncTask 的整体工作流程了,如果需要 AsyncTask 更为详细的源码注释,可以看这里:AsyncTask 更多的源码解读请看这里:Java_Android_Learn

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

Java集合框架源码解析之ArrayList

ArrayList 可能是很多人使用得最为频繁的容器类了,ArrayList 实现了 List 接口,是一个有序容器,即存放元素的顺序与添加顺序相同,允许添加相同元素,包括 null ,底层通过数组来实现数据存储,容器内存储的元素个数不能超出数组空间。所以向容器中添加元素时如果发现数组空间不足,容器会自动对底层数组进行扩容操作 ArrayList 的类声明 public class ArrayList<E> extends AbstractList<E> implements List<E>, RandomAccess, Cloneable, java.io.Serializable 从其实现的几个接口可以看出来,ArrayList 是支持快速访问,可克隆,可序列化的 包含的成员变量 //序列化ID private static final long serialVersionUID = 8683452581122892189L; //集合默认的初始大小 private static final int DEFAULT_CAPACITY = 10; //如果外部为集合设置的初始化大小为 0,则 elementData 指向空数组对象 EMPTY_ELEMENTDATA private static final Object[] EMPTY_ELEMENTDATA = {}; //如果在初始化集合时使用的是无参数的构造函数,则 elementData 指向空数组对象 DEFAULTCAPACITY_EMPTY_ELEMENTDATA private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {}; //包含实际元素的数组 transient Object[] elementData; //集合大小 private int size; elementData 是一个 Object 类型的数组对象,即可用来存放任何对象类型,也是 ArrarList 中用来存放数据的容器。而 ArrayList 是一个泛型类,我们在初始化时就直接指定了数据类型,Java泛型只是编译器为我们提供的语法糖,方便我们在向 elementData 存取数据时,将之自动转换为特定的类型 包含的构造函数 //指定集合的初始容量,以此来进行数组的初始化操作 public ArrayList(int initialCapacity) { if (initialCapacity > 0) { this.elementData = new Object[initialCapacity]; } else if (initialCapacity == 0) { this.elementData = EMPTY_ELEMENTDATA; } else { throw new IllegalArgumentException("Illegal Capacity: "+initialCapacity); } } //使用默认的初始化大小 public ArrayList() { this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA; } //传入一份初始数据,以此进行初始化 public ArrayList(Collection<? extends E> c) { elementData = c.toArray(); if ((size = elementData.length) != 0) { // c.toArray might (incorrectly) not return Object[] (see 6260652) if (elementData.getClass() != Object[].class) elementData = Arrays.copyOf(elementData, size, Object[].class); } else { this.elementData = EMPTY_ELEMENTDATA; } } 可以在初始化 ArrayList 的时候传入集合的初始化大小,这通常来说都是更为高效率一些的,因为如果是让集合在赋值的过程中自动扩容,则可能会需要进行多次扩容操作,而每次扩容都需要复制原有数据到新数组,这会导致运行效率降低 添加/修改 元素 在获取指定索引处的元素时,都是直接通过坐标指向该元素 (E) elementData[index],而无需从头开始遍历集合,所以说 ArrayList 的遍历效率较高 //通过索引直接访问数组 @SuppressWarnings("unchecked") E elementData(int index) { return (E) elementData[index]; } //获取索引 index 处的元素值 public E get(int index) { rangeCheck(index); return elementData(index); } //将索引 index 出的元素值置为 element,并返回原始数值 public E set(int index, E element) { rangeCheck(index); E oldValue = elementData(index); elementData[index] = element; return oldValue; } ArrayList 在存入数据时相对来说就不是那么理想了 如果是直接向集合尾端添加数据 add(E e),则先检查是否需要扩容,需要的话则创建一个新的符合大小的数组,并将原数组中的元素移到新数组中,再向数组尾端添加待存入的元素 如果是向集合非尾端位置添加数据 add(int index, E element),一样需要先检查是否需要扩容,然后将数组中索引 index 后的所有元素向后推移一位,然后将 element 插入到空出的位置上 由此看出来,向集合添加元素由于可能会导致数组扩容,从而导致数组元素的大量移动,所以说 ArrayList 存入数据的效率并不高 //向集合添加数据 public boolean add(E e) { //检查是否需要扩容 ensureCapacityInternal(size + 1); //赋值 elementData[size++] = e; return true; } //将元素 element 添加索引 index 位置 public void add(int index, E element) { rangeCheckForAdd(index); //检查是否需要扩容 ensureCapacityInternal(size + 1); //将索引 index 后的所有数值向后推移一位 System.arraycopy(elementData, index, elementData, index + 1,size - index); //将 element 插入到空出的位置 elementData[index] = element; //集合大小加1 size++; } 以上说的是存入单个元素,此外还有存入整个集合的情况 //向集合添加数据 //如果待添加的数据不为空则返回 true,否则返回 false public boolean addAll(Collection<? extends E> c) { Object[] a = c.toArray(); int numNew = a.length; //检查是否需要扩容 ensureCapacityInternal(size + numNew); //将数组 a 复制到 elementData 的尾端 System.arraycopy(a, 0, elementData, size, numNew); size += numNew; return numNew != 0; } //从指定索引处添加数据 //如果待添加的数据不为空则返回 true,否则返回 false public boolean addAll(int index, Collection<? extends E> c) { rangeCheckForAdd(index); Object[] a = c.toArray(); int numNew = a.length; //检查是否需要扩容 ensureCapacityInternal(size + numNew); //需要移动的数组元素数量 int numMoved = size - index; //因为要添加的数据可能刚好是从数组最尾端开始添加,所以 numMoved 可能为 0 //所以只在 numMoved > 0 的时候才需要对数组的元素值进行移动,以此空出位置给数组 a if (numMoved > 0) System.arraycopy(elementData, index, elementData, index + numNew, numMoved); //将数组 a 包含的数据添加到 elementData 中 System.arraycopy(a, 0, elementData, index, numNew); size += numNew; return numNew != 0; } 移除元素 再看下移除元素的方法 因为数组是一种内存地址连续的数据结构,所以移除某个元素同样可能导致大量元素的移动 //移除指定索引处的元素值,并返回该值 public E remove(int index) { rangeCheck(index); modCount++; //待移除的元素值 E oldValue = elementData(index); //因为要移除元素导致需要移动的元素数量 int numMoved = size - index - 1; //因为要移除的元素可能刚好是数组最后一位,所以 numMoved 可能为 0 //所以只在 numMoved > 0 的时候才需要对数组的元素值进行移动 if (numMoved > 0) System.arraycopy(elementData, index+1, elementData, index, numMoved); //不管数组是否需要对元素值进行移动,数组的最后一位都是无效数据了 //此处将之置为 null 以帮助GC回收 elementData[--size] = null; return oldValue; } //移除集合中包含的第一位元素值为 o 的对象 //如果包含该对象,则返回 true ,否则返回 false public boolean remove(Object o) { if (o == null) { for (int index = 0; index < size; index++) if (elementData[index] == null) { fastRemove(index); return true; } } else { for (int index = 0; index < size; index++) if (o.equals(elementData[index])) { fastRemove(index); return true; } } return false; } 扩容机制 以上多处说到了数组的扩容,这里就来看下数组的扩容机制 实际进行扩容操作的是 grow(int grow(int minCapacity)) 方法,参数 minCapacity 用于指定要求的最小空间,在扩容前,会先判断如果将当前容量提升到当前的 1.5 倍是否能达到 minCapacity 的要求 ,如果符合要求则直接将数据扩充到当前的 1.5 倍容量,否则则扩充到 minCapacity ,构建一个新的符合大小的数组后,就将原数组中的元素复制到新数组中 由此可想到,如果在初始化 ArrayList 前已知目标数据的数据量,则最好使用 ArrayList(int initialCapacity)来进行初始化,直接让底层数组扩充到目标大小,避免之后赋值过程中多次扩容 //触发集合进行扩容操作,参数 minCapacity 表示集合扩容后的最小空间 //如果在向集合进行赋值操作前已知数据量大小,则直接调用此方法让集合直接扩容到该大小有助于提高集合的运行效率 //如果是让集合在赋值的过程中自动扩容,则可能会需要进行多次扩容操作,而每次扩容都需要复制原有数据到新数组,这会导致运行效率降低 public void ensureCapacity(int minCapacity) { //1. 如果当前数组还未进行任何赋值操作,即 elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA,则数组空间只由参数 minCapacity 影响 //2. 如果当前数组已进行过赋值操作,即 elementData != DEFAULTCAPACITY_EMPTY_ELEMENTDATA,则当前数组大小可能还是在使用默认容量 DEFAULT_CAPACITY // 则此时只有当 minCapacity 大于 DEFAULT_CAPACITY 时,才需要进行扩容 int minExpand = (elementData != DEFAULTCAPACITY_EMPTY_ELEMENTDATA) // any size if not default element table ? 0 // larger than default for default empty table. It's already // supposed to be at default size. : DEFAULT_CAPACITY; if (minCapacity > minExpand) { ensureExplicitCapacity(minCapacity); } } //检查当前的数组容量,minCapacity 用于指定当前需要的最小空间 //如果数组容量没有达到一定标准,则需要进行扩容 private void ensureCapacityInternal(int minCapacity) { if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) { //数组空间至少需要扩容到 DEFAULT_CAPACITY minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity); } ensureExplicitCapacity(minCapacity); } private void ensureExplicitCapacity(int minCapacity) { modCount++; //如果当前数组大小的确是比需要的最小空间 minCapacity 小,则进行扩容 if (minCapacity - elementData.length > 0) grow(minCapacity); } //数组可扩充到的最大空间 private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8; //对数组进行扩容 private void grow(int minCapacity) { //扩容前的数组大小 int oldCapacity = elementData.length; //oldCapacity >> 1 的含义即为:将 oldCapacity 值除以 2 //即先假设扩容后的空间大小是原先的1.5倍 int newCapacity = oldCapacity + (oldCapacity >> 1); //如果 newCapacity 依然是达不到最小空间要求,则直接将空间扩大到由 minCapacity 指定的大小 if (newCapacity - minCapacity < 0) newCapacity = minCapacity; //如果扩容后的数组空间超出了最大容量限制,则将容量定为 Integer.MAX_VALUE if (newCapacity - MAX_ARRAY_SIZE > 0) newCapacity = hugeCapacity(minCapacity); //构建符合容量大小的数组并复制原数组的数据 elementData = Arrays.copyOf(elementData, newCapacity); } 遍历集合的方法 //遍历集合元素 @Override public void forEach(Consumer<? super E> action) { Objects.requireNonNull(action); final int expectedModCount = modCount; @SuppressWarnings("unchecked") final E[] elementData = (E[]) this.elementData; final int size = this.size; //如果 modCount 值被改动,则直接停止遍历并抛出异常 for (int i=0; modCount == expectedModCount && i < size; i++) { //将集合元素依次传递给 accept 方法 action.accept(elementData[i]); } if (modCount != expectedModCount) { throw new ConcurrentModificationException(); } } 遍历并过滤集合的方法 //按照给定规则对集合元素进行过滤,如果元素符合过滤规则 filter 则将之移除 @Override public boolean removeIf(Predicate<? super E> filter) { Objects.requireNonNull(filter); //要移除的元素个数 int removeCount = 0; //用于标记集合是哪个索引位置需要被移除 final BitSet removeSet = new BitSet(size); final int expectedModCount = modCount; final int size = this.size; for (int i=0; modCount == expectedModCount && i < size; i++) { @SuppressWarnings("unchecked") final E element = (E) elementData[i]; //依次判断集合元素是否符合过滤规则 if (filter.test(element)) { //set 方法将导致索引位置 i 的元素变为 true removeSet.set(i); removeCount++; } } if (modCount != expectedModCount) { throw new ConcurrentModificationException(); } //只有 removeCount > 0 才说明需要移除元素 final boolean anyToRemove = removeCount > 0; if (anyToRemove) { //集合移除指定元素后的大小 final int newSize = size - removeCount; for (int i=0, j=0; (i < size) && (j < newSize); i++, j++) { //略过被标记为 true 的位置,直接跳到不需要移除元素的数组索引位 i = removeSet.nextClearBit(i); //有效数据逐渐从尾部向头部聚集 elementData[j] = elementData[i]; } //移除尾部的无效数据,帮助GC回收 for (int k=newSize; k < size; k++) { elementData[k] = null; } this.size = newSize; if (modCount != expectedModCount) { throw new ConcurrentModificationException(); } modCount++; } return anyToRemove; } //将集合元素遍历传递给 operator,并将原始数据替换为 operator 的返回值 @Override @SuppressWarnings("unchecked") public void replaceAll(UnaryOperator<E> operator) { Objects.requireNonNull(operator); final int expectedModCount = modCount; final int size = this.size; for (int i=0; modCount == expectedModCount && i < size; i++) { //依次传递数组元素给 apply 方法,并将其返回值替换原始数据 elementData[i] = operator.apply((E) elementData[i]); } //不允许在排序的过程中集合被其它方法修改了数据结构(例如:移除元素) if (modCount != expectedModCount) { throw new ConcurrentModificationException(); } modCount++; } 迭代器 在这里有个小细节,ArrayList 里多处使用到了 modCount 这个参数,每当集合的结构发生变化时,modCount 就会递增,当在对集合进行迭代操作时,迭代器会检查此参数值,如果检查到此参数的值发生变化,就说明在迭代的过程中集合的结构发生了变化,此时迭代的元素可能就并不是最新的了,因此会直接抛出异常 //返回集合迭代器 public Iterator<E> iterator() { return new Itr(); } //一个优化版本的迭代器 private class Itr implements Iterator<E> { //lastRet 指向的元素的下一个元素的索引 int cursor; //最后一个返回的元素的索引 //如果值为 -1,说明还未返回过元素或者改元素被移除了 int lastRet = -1; //用于验证集合的数据结构在迭代的过程中是否被修改了 int expectedModCount = modCount; //是否还有元素未被遍历 public boolean hasNext() { return cursor != size; } //获取下一个元素 @SuppressWarnings("unchecked") public E next() { checkForComodification(); int i = cursor; //如果索引值超出集合的可索引范围则抛出异常 if (i >= size) throw new NoSuchElementException(); Object[] elementData = ArrayList.this.elementData; //如果索引值超出数组的可索引范围则抛出异常 if (i >= elementData.length) throw new ConcurrentModificationException(); //游标递增 cursor = i + 1; return (E) elementData[lastRet = i]; } //移除 lastRet 位置的元素 public void remove() { if (lastRet < 0) throw new IllegalStateException(); checkForComodification(); try { ArrayList.this.remove(lastRet); //因为 lastRet 位置原始的元素被移除了,所以此时 lastRet 指向的元素是原先 lastRet+1 位置的元素 cursor = lastRet; lastRet = -1; //因为是 Itr 主动对集合进行修改,所以此处需要主动更新 expectedModCount 值,避免之后抛出异常 expectedModCount = modCount; } catch (IndexOutOfBoundsException ex) { throw new ConcurrentModificationException(); } } //遍历集合从索引 cursor 开始之后剩下的元素 @Override @SuppressWarnings("unchecked") public void forEachRemaining(Consumer<? super E> consumer) { Objects.requireNonNull(consumer); final int size = ArrayList.this.size; int i = cursor; if (i >= size) { return; } final Object[] elementData = ArrayList.this.elementData; if (i >= elementData.length) { throw new ConcurrentModificationException(); } //遍历调用 accept 方法 while (i != size && modCount == expectedModCount) { consumer.accept((E) elementData[i++]); } cursor = i; lastRet = i - 1; checkForComodification(); } //判断迭代器在遍历集合的过程中,集合是否被外部改动了(例如被其它迭代器移除了元素) //如果是的话则抛出异常 final void checkForComodification() { if (modCount != expectedModCount) throw new ConcurrentModificationException(); } } 效率测试 最后来测试下 ArrayList 扩容次数的高低对其运行效率的影响 对三个 ArrayList 存入相同数据量的数据,但分别为 ArrayList 指定不同的初始化大小 public static void main(String[] args) { //开始时间 long startTime = System.currentTimeMillis(); List<String> stringList = new ArrayList<>(); for (int i = 0; i < 300000; i++) { stringList.add("leavesC " + i); } //结束时间 long endTime = System.currentTimeMillis(); System.out.println("不指定初始大小,所用时间:" + (endTime - startTime) + "毫秒"); //开始时间 startTime = System.currentTimeMillis(); List<String> stringList2 = new ArrayList<>(100000); for (int i = 0; i < 300000; i++) { stringList2.add("leavesC " + i); } //结束时间 endTime = System.currentTimeMillis(); System.out.println("指定初始大小为目标数据量的三分之一,所用时间:" + (endTime - startTime)+ "毫秒"); //开始时间 startTime = System.currentTimeMillis(); List<String> stringList3 = new ArrayList<>(300000); for (int i = 0; i < 300000; i++) { stringList3.add("leavesC " + i); } //结束时间 endTime = System.currentTimeMillis(); System.out.println("指定初始大小为目标数据量,所用时间:" + (endTime - startTime)+ "毫秒"); } 可以看出来,各种方式之间的运行效率差距还是很大的 关于 ArrayList 的内容就讲到这里了,一方面是篇幅所限,一方面我是觉得很多知识点其实也不需要怎么讲,直接看源码的话认知会更为深刻一点,因此我也把对 ArrayList 的详细源码注释开源到了 GitHub 上,欢迎关注 源码地址:Java_Android_Learn

资源下载

更多资源
Mario

Mario

马里奥是站在游戏界顶峰的超人气多面角色。马里奥靠吃蘑菇成长,特征是大鼻子、头戴帽子、身穿背带裤,还留着胡子。与他的双胞胎兄弟路易基一起,长年担任任天堂的招牌角色。

腾讯云软件源

腾讯云软件源

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

Rocky Linux

Rocky Linux

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

WebStorm

WebStorm

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

用户登录
用户注册