From 328d2c58a3913dfcad221f65051fdb684af5ee6f Mon Sep 17 00:00:00 2001 From: huancheng lu Date: Sun, 8 Mar 2020 22:08:50 +0800 Subject: [PATCH] =?UTF-8?q?69=20-=20=E7=AC=AC=E4=B8=80=E5=91=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- second/week_01/69/ArrayList.md | 333 ++++++++++++++++++++++++++++++++ second/week_01/69/HashMap.md | 72 +++++++ second/week_01/69/LinkedList.md | 77 ++++++++ 3 files changed, 482 insertions(+) create mode 100644 second/week_01/69/ArrayList.md create mode 100644 second/week_01/69/HashMap.md create mode 100644 second/week_01/69/LinkedList.md diff --git a/second/week_01/69/ArrayList.md b/second/week_01/69/ArrayList.md new file mode 100644 index 0000000..ecd8aac --- /dev/null +++ b/second/week_01/69/ArrayList.md @@ -0,0 +1,333 @@ +--- +title: 我理解的ArrayList +tags: + - JDK源码阅读 + - 集合框架 +categories: hexo +copyright: true +reward: true +related_posts: true +rating: true +urlname: arraylist +date: 2020-03-07 09:33:04 +--- +本篇是我个人阅读集合框架的第一篇文章,ArrayList源码的阅读。主要涉及: +- 集合框架的概览 +- Serializable、Cloneable、RandomAccess接口的说明 +- ArrayList的序列化 +- ArrayList的自动扩容 +- ArrayList的迭代器 + + + + +## 集合框架 +> ArrayList作为JDK集合框架中的一员,在了解它之前很有必要了解一下集合框架以及ArrayList在该框架中的位置。同时也为了之后阅读集合框架中其他类打下基础。 + +JDK通过[集合框架](https://docs.oracle.com/javase/8/docs/technotes/guides/collections/)封装了一系列常用数据结构供开发者日常使用。如ArrayList、LinkedList、HashMap、HashSet、TreeMap、TreeSet等。这样做的优点是: +- 无需开发者从底层实现数据结构和算法,从而降低编程成本 +- 通过提供数据结构和算法的高性能实现来提高性能。由于每个接口的各种实现是可互换的,因此可以通过切换实现来调优程序 +- 通过建立一种来回传递集合的公共集合,在不相关的api之间提供互操作性(PS:按照笔者的理解应该指的是不同集合类的构造函数入参 **Collection** ) +- 通过多个特定的集合api,减少了学习api的成本 +- 通过标准化的接口为使用集合的算法提高了软件重用性 + +那么,这么多优点是怎么做到的呢? +- Collection interfaces. Represent different types of collections, such as sets, lists, and maps. These interfaces form the basis of the framework. +- General-purpose implementations. Primary implementations of the collection interfaces. +- Legacy implementations. The collection classes from earlier releases, Vector and Hashtable, were retrofitted to implement the collection interfaces. +- Special-purpose implementations. Implementations designed for use in special situations. These implementations display nonstandard performance characteristics, usage restrictions, or behavior. +- Concurrent implementations. Implementations designed for highly concurrent use. +- Wrapper implementations. Add functionality, such as synchronization, to other implementations. +- Convenience implementations. High-performance "mini-implementations" of the collection interfaces. +- Abstract implementations. Partial implementations of the collection interfaces to facilitate custom implementations. +- Algorithms. Static methods that perform useful functions on collections, such as sorting a list. +- Infrastructure. Interfaces that provide essential support for the collection interfaces. +- Array Utilities. Utility functions for arrays of primitive types and reference objects. Not, strictly speaking, a part of the collections framework, this feature was added to the Java platform at the same time as the collections framework and relies on some of the same infrastructure. + +以上描述摘抄自官网,为了避免由于个人理解的误差就不进行翻译了。说下个人的理解,集合框架主要包含了几个主要部分: +- 集合的接口,如List、Set、Map这些基础接口 +- 通用的实现,如ArrayList、HashMap、TreeMap等 +- 遗留代码,如Vector、HashTable等早期版本遗留的集合类 +- 特殊目的的集合实现类,暂时想不到什么例子 +- 并发实现,用于并发场景下使用的集合实现类,如ConcurrentHashMap +- 包装实现,如为了避免集合被修改通过包装类代理:Collections.unmodifiableSet() +- 抽象实现:为了公用通用代码而实现的抽象类,可以基于这些抽象类的接口进行底层实现的封装。如:AbstractList的子类有ArrayList、LinkedList,就是基于AbstractList暴露的接口来对外提供统一接口,但两者底层的实现完全不一致。 +- 算法的封装,通过静态方法的形式封装了算法,如排序算法 + +以上就是对集合框架的一些基本认识。说实话,对于这些抽象的概括,没有通过源码阅读的了解很难完全理解这里列的1234...这么多点的好处和概念。 + +## ArrayList +![](https://img.mrglint.com/blog-luhuancheng-com/2020-03-07-021843.png) + +先从顶层看看ArrayList的接口,如果从来没有探究过ArrayList源码,单从这张类图中起码会有几个疑问: +- Iterable是啥? +- RandomAccess是啥? +- ArrayList实现了接口Serializable和Cloneable,那么在其内部会有什么样的表现? + +从日常的开发的使用过程中,是否有过疑问: +- 我们可以不断地往一个ArrayList对象中添加元素,其中的原理是什么?(别小看这个问题,其中的原理可以帮助你在某些场景写出更高效的代码) + +> 疑问是求知的开始,带着好的问题来阅读源码可以让我们抓住重点,不至于在繁杂的代码中脱离了主线。忘了当初为什么出发。 + +### ArrayList的自动扩容 +```java +public static void main(String[] args) { + List list = new ArrayList<>(); + list.add(1); +} +``` +分析源码之前,先看看整个添加操作的整体流程图 +![](https://img.mrglint.com/blog-luhuancheng-com/2020-03-07-053459.jpg) +有兴趣的读者可以自行沿着这个调用链去探究整个过程,我们这里着重看下扩容的代码,即grow函数 +```java +private void grow(int minCapacity) { + // overflow-conscious code + int oldCapacity = elementData.length; + int newCapacity = oldCapacity + (oldCapacity >> 1); + if (newCapacity - minCapacity < 0) + newCapacity = minCapacity; + if (newCapacity - MAX_ARRAY_SIZE > 0) + newCapacity = hugeCapacity(minCapacity); + // minCapacity is usually close to size, so this is a win: + elementData = Arrays.copyOf(elementData, newCapacity); +} +``` +可以看出整个过程非常简单,只是重新申请了一个1.5倍的数组空间,把原有的元素放入其中。 +> 从扩容机制可以知道,如果一开始数组空间开辟的比较小,随着数据的插入后达到容量上限则需要进行数组元素的拷贝,这一部分操作的时间复杂度为O(n)。因此如果可以事先知道列表的元素个数,可以直接开辟一个指定大小的ArrayList,减少这一部分开销 + +既然,添加元素存在扩容, 那么删除元素的时候是不是存在缩容呢? +```java +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; +} + +private void fastRemove(int index) { + modCount++; + int numMoved = size - index - 1; + if (numMoved > 0) + System.arraycopy(elementData, index+1, elementData, index, + numMoved); + elementData[--size] = null; // clear to let GC do its work +} +``` +可以看到并没有进行缩容相关操作。 + +### Iterable迭代器模式 +java给我们提供了一个语法糖,可以通过for-each语法来遍历列表,如下 +```java +List list = new ArrayList(); +list.add(1); +list.add(2); +// for-each的语法糖 +for (Integer i : list) { + // ... do something +} +``` +其底层原理正是Iterable接口在起作用。让我们来看看ArrayList是如何实现的 +在ArrayList中,实现了Iterator接口的方法,返回了一个Itr类实例 +```java +public Iterator iterator() { + return new Itr(); +} +``` +Itr类是ArrayList的内部类。 +```java +private class Itr implements Iterator { + int cursor; // index of next element to return + int lastRet = -1; // index of last element returned; -1 if no such + // 实例化时,捕获了ArrayList的modCount,用于实现fast-fail机制。即并发修改时抛出异常 + int expectedModCount = modCount; + + Itr() {} + + public boolean hasNext() { + // size是ArrayList实例的字段,表示当前元素个数。当cursor == size时,说明cursor已经遍历完数组 + return cursor != size; + } + + @SuppressWarnings("unchecked") + public E next() { + checkForComodification(); + int i = cursor; + if (i >= size) + throw new NoSuchElementException(); + // 内部类的语法。通过 ArrayList.this.elementData 获得外围类 ArrayList对象的属性 elementData + Object[] elementData = ArrayList.this.elementData; + if (i >= elementData.length) + throw new ConcurrentModificationException(); + cursor = i + 1; + // 将当前遍历到的元素赋值给 lastRet,用于在remove方法中使用 + return (E) elementData[lastRet = i]; + } + + // 通过迭代器删除当前遍历到的元素 + public void remove() { + if (lastRet < 0) + throw new IllegalStateException(); + checkForComodification(); + + try { + ArrayList.this.remove(lastRet); + cursor = lastRet; + lastRet = -1; + expectedModCount = modCount; + } catch (IndexOutOfBoundsException ex) { + throw new ConcurrentModificationException(); + } + } + + // forEachRemaining 用于遍历剩余的元素。不能多次遍历。 + // 区别于forEach方法,forEach方法支持多次遍历整个列表 + @Override + @SuppressWarnings("unchecked") + public void forEachRemaining(Consumer 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(); + } + while (i != size && modCount == expectedModCount) { + consumer.accept((E) elementData[i++]); + } + // update once at end of iteration to reduce heap write traffic + cursor = i; + lastRet = i - 1; + checkForComodification(); + } + + final void checkForComodification() { + if (modCount != expectedModCount) + throw new ConcurrentModificationException(); + } +} +``` +#### 迭代器总结 +从源码可以看出,迭代器用于遍历整个集合。并且提供了remove方法用于在遍历时删除某个元素(**为什么这么说呢?有兴趣的读者可以试试在普通for-each中删除某个元素看看,这样做的话将抛出异常**)。 +迭代器的实现方式是通过内部类,来捕获外围类实例中的实例数据进行访问实现的。至于为什么要通过内部类来实现,个人认为是为了隐藏集合的内部实现。 + +### Serializable和Cloneable实现细节 +#### Serializable +更多关于[Serializable](https://docs.oracle.com/javase/8/docs/api/java/io/Serializable.html)的说明,可以参考官方文档。总结来说,一个类是否实现Serializable接口是决定一个类是否支持JDK默认的序列化和反序列化的前提。Serializable是一个标记接口,没有任何方法。对于支持序列化和反序列化的类,需要遵循: +- 当一个类继承了一个实现Serializable接口的类时,子类默认默认支持序列化 +- 为了允许序列化非序列化类的子类型(即该子类型的父类不支持序列化操作),子类型可以负责保存和恢复超类型的public、protected和(如果可访问)包字段的状态。子类型只有在其扩展的类具有可访问的无参数构造函数来初始化类的状态时才可以承担此责任。如果不是这样,则声明一个可序列化的类是错误的。错误将在运行时检测到。 +- 在反序列化期间,将使用该类的public或protected无参数构造函数初始化不可序列化类的字段。无参数构造函数必须可被序列化的子类访问。可序列化子类的字段将从流中恢复 + +##### 可序列化类需支持的方法 +在序列化和反序列化过程中需要特殊处理的类必须使用这些确切的签名实现特殊的方法: +```java +private void writeObject(java.io.ObjectOutputStream out) + throws IOException + private void readObject(java.io.ObjectInputStream in) + throws IOException, ClassNotFoundException; + private void readObjectNoData() + throws ObjectStreamException; +``` +- 对于writeObject、readObject来说,比较好理解。writeObject用于将对象序列化为字节流、readObject用于从字节流中反序列化成对象。对于可序列化的类,开发者可以选择同时实现writeObject、readObject方法来自定义序列化和反序列化过程;如果不进行实现的情况,writeObject默认会调用 **ObjectOutputStream#defaultWriteObject**,将类中非静态字段、非transient修饰的字段写入流中;riteObject默认会调用 **ObjectOutputStream#defaultWriteObject**,将类中非静态字段、非transient修饰的字段写入流中;readObject默认会调用 **ObjectOutputStream#defaultReadObject**,将从流中读取类中非静态字段、非transient修饰的字段; +- **readObjectNoData** 是用于处理在反序列化时,字节流的版本跟当前类的版本不一致的场景下使用,个人觉得不常用。有兴趣的可以从网上搜索资料深入了解下。 + +##### serialVersionUID +对于实现Serializable接口的可序列化类中,需要定义 **serialVersionUID** 字段,用于序列化和反序列化的版本匹配。如果class文件中的 **serialVersionUID** 与字节流中的 **serialVersionUID** 不匹配的话会抛出异常,如下例子 +``` +java.io.InvalidClassException: com.mrglint.Person; local class incompatible: stream classdesc serialVersionUID = 1, local class serialVersionUID = 2 +``` +因此,虽然在 **Java(TM) Object Serialization Specification(即Java序列化规范中)** 规定,就算一个实现Serializable接口的可序列化类中,没有定义 **serialVersionUID** 字段也是可以的,在序列化运行时将通过class文件来计算出一个默认的serialVersionUID,但是该机制对于不同的编译器实现可能存在不同。因此**务必在使用JDK序列化机制时,指定序列化类的serialVersionUID属性** + +##### 如何序列化ArrayList +有了以上关于 **Serializable** 接口知识的铺垫后,我们可以来看看ArrayList中对于序列化是如何实现的了。 +**writeObject和readObject方法** 自定义了ArrayList的序列化和反序列化方式。 +```java +/** + * Save the state of the ArrayList instance to a stream (that + * is, serialize it). + * + * @serialData The length of the array backing the ArrayList + * instance is emitted (int), followed by all of its elements + * (each an Object) in the proper order. + */ +private void writeObject(java.io.ObjectOutputStream s) + throws java.io.IOException{ + // Write out element count, and any hidden stuff + int expectedModCount = modCount; + s.defaultWriteObject(); + + // Write out size as capacity for behavioural compatibility with clone() + s.writeInt(size); + + // Write out all elements in the proper order. + for (int i=0; iArrayList instance from a stream (that is, + * deserialize it). + */ +private void readObject(java.io.ObjectInputStream s) + throws java.io.IOException, ClassNotFoundException { + elementData = EMPTY_ELEMENTDATA; + + // Read in size, and any hidden stuff + s.defaultReadObject(); + + // Read in capacity + s.readInt(); // ignored + + if (size > 0) { + // be like clone(), allocate array based upon size not capacity + int capacity = calculateCapacity(elementData, size); + SharedSecrets.getJavaOISAccess().checkArray(s, Object[].class, capacity); + ensureCapacityInternal(size); + + Object[] a = elementData; + // Read in all elements in the proper order. + for (int i=0; i v = (ArrayList) super.clone(); + v.elementData = Arrays.copyOf(elementData, size); + v.modCount = 0; + return v; + } catch (CloneNotSupportedException e) { + // this shouldn't happen, since we are Cloneable + throw new InternalError(e); + } +} +``` +对于支持clone的类,默认的clone行为是**浅拷贝**,如果要支持**深拷贝**需要在clone方法中进行一定的数据处理。 +在这里ArrayList属于**浅拷贝**,因为其直接复制了数组中的元素的引用。在拷贝出来的新的ArrayList对象中操作其中的元素,将改变原来ArrayList中的元素。 + +### RandomAccess +同样是一个标记接口,实现该接口的集合类意味着可以进行随机访问。这对于某些算法性能的提升有非常大的帮助。比如在ArrayList内部是用数组实现的,众所周知,数组支持随机访问。因此通过list.get(i)来获取元素的话时间复杂度为O(1) diff --git a/second/week_01/69/HashMap.md b/second/week_01/69/HashMap.md new file mode 100644 index 0000000..ad2b72a --- /dev/null +++ b/second/week_01/69/HashMap.md @@ -0,0 +1,72 @@ +--- +title: 我理解的HashMap +tags: + - JDK源码阅读 + - 集合框架 +categories: hexo +copyright: true +reward: true +related_posts: true +rating: true +urlname: hashmap.html +date: 2020-03-07 16:26:40 +--- +> Map, 一个将key映射到value的对象。一个Map不能包含两个重复的key,每个key最多只能映射到一个value上 -- JDK + +Map接口,在JDK中有多种实现方式。比较典型的有散列表实现的HashMap、有红黑树实现的TreeMap、结合双向链表和HashMap实现的LinkedHashMap(其特质可以帮助我们实现LRU算法),除了以上三个之外,在并发包中提供了一个基于散列表实现的线程安全的ConcurrentHashMap + + + +## HashMap +### JDK注释说明 +![](https://img.mrglint.com/blog-luhuancheng-com/2019-03-19-234418.png) +![](https://img.mrglint.com/blog-luhuancheng-com/2019-03-19-234858.png) + +### 类继承图 +![](https://img.mrglint.com/blog-luhuancheng-com/2019-03-20-001309.png) + +### 源码分析 +平时我们使用HashMap的时候,一般都是直接new HashMap()实例化一个对象,之后进行map.put(key, value)操作。此时我们依照这个调用路径来看看HashMap内部是如何为我们工作的。 +#### 静态成员常量 +![](https://img.mrglint.com/blog-luhuancheng-com/2019-03-20-002338.png) +#### 构造器 +默认初始化一个容量16、load factor为0.75的HashMap对象 +![](https://img.mrglint.com/blog-luhuancheng-com/2019-03-20-002434.png) +#### put(key, value)操作 +![](https://img.mrglint.com/blog-luhuancheng-com/2019-03-20-002905.png) +##### HashMap中的哈希算法 +![](https://img.mrglint.com/blog-luhuancheng-com/2019-03-20-003902.png) +注释强调了:由于hash表使用了2的次方数作为容量大小,使用key.hashCode()和key.hashCode()的高16位进行异或运算得到的hash值, +可以减少hash冲突,为什么呢?这里先保有一个疑问,等理解了添加的流程之后再回过来看看 +##### 元素的添加操作 +![](https://img.mrglint.com/blog-luhuancheng-com/2019-03-20-051003.png) +在整个添加操作的过程中,有几个比较巧妙的地方 +- 通过key的hash值定位key在哈希表中的索引时,使用了位与运算代替了取余运算,极致提升性能 +其中原理取决于我们构造出来的哈希表的容量为2 ^ n(2的n次方) +```java +// 相当于tab[i = (hash % n)], n为容量大小 +tab[i = (n - 1) & hash]) +``` +- 如何保证容量为2 ^ n(2的n次方)呢? +我们先来看一下resize()方法 +![](https://img.mrglint.com/blog-luhuancheng-com/2019-03-20-054221.png) +从代码可以看出 +- 通过无参的构造器实例化一个对象时,容量是在方法resize()时进行分配。默认为16。 +- 通过传入capacity、load factor参数的构造器时,容量是在方法tableSizeFor(initialCapacity)中计算得到的,大小为:比传入capacity大的最小2 ^ n(2的n次方)的数值 +![](https://img.mrglint.com/blog-luhuancheng-com/2019-03-20-054906.png) +[为什么tableSizeFor(capacity)可以保证返回的容量为2 ^ n](https://www.cnblogs.com/loading4/p/6239441.html) +- 现在回过头看一下之前留下来的问题,HashMap中的哈希算法为什么要使用key.hashCode() 与 key.hashCode >>> 16 进行异或? +这是因为在HashMap中,hash表的索引为 (hash表的长度 - 1) & hash。hash表的长度为2的幂次方,因此hash表的长度 - 1其实为低位的掩码,(hash表的长度 - 1) & hash 得到的是hash低位的值。通过以上的异或运算,可以保留key.hashCode()中高位的差异,减少hash冲突。 +[参考扰动函数](https://www.zhihu.com/question/20733617) + +#### resize()扩容操作 +![](https://img.mrglint.com/blog-luhuancheng-com/2019-03-30-021457.png) +resize操作的用于初始化hash表、或者扩容hash表为2倍原始大小。由于hash表的扩容时通过2倍的大小扩容,因此hash表中的所有元素要么经过重新hash后保持原有的索引,要么就是在原有索引基础上偏移旧hash表容量的位置上。即原有的索引为index,旧的hash表容量为oldCap,那么扩容后,这个索引将变为(index + oldCap)。通过这个机制,避免了扩容后重新计算每个元素的hash值,提高性能。 +![](https://img.mrglint.com/blog-luhuancheng-com/2019-03-30-022956.png) +基本原理是,hash表的原有容量为2 ^ n次方,扩容时新的hash表容量为旧容量左移1位。那么hash表中的元素e的hash值与新的容量取模(e.hash % newCap = e.hash & (newCap - 1))后的索引值会比旧的索引值多1bit。即假设,原有的索引为1000,那么新的索引值为x1000。那么元素在新的hash表中的索引就取决于e.hash在多出来的1bit(即X1000中的X是0,还是1)。如果X为0,那么在新hash表中的索引与旧的hash表的索引一致;如果X为1,那么在新hash表中的索引要在原有的索引值上偏移oldCap(即index+oldCap)。那么我们就可以通过(e.hash & oldCap)来判断这个X是否为0。 + +## 总结 +1. 为了高性能的取模运算,capacity为默认的16,或者由tableSizeFor来转换为比传入capacity大的最小2 ^ n的值。例如传入的capacity为5,那么tableSizeFor将返回capacity为8 +2. 由于我们的capacity为2 ^ n,那么在进行hash运算时,低位的值对我们的hash算法影响较大。如果低位的值不均匀的话,会造成hash冲突高概率发送,因此利用hash值的高位16位与hash值异或运算,来混入高位的特征,降低hash冲突的概率 +3. HashMap采用链地址法来处理hash冲突,在JDK8时代,hash桶中的元素如果达到8个且整个map的元素个数达到64个,将会由链表转为红黑树结构 +4. hash表扩容的步长是2倍原有容量(即oldCap << 1),这样扩容后我们无需对其中的所有Node重新计算hash \ No newline at end of file diff --git a/second/week_01/69/LinkedList.md b/second/week_01/69/LinkedList.md new file mode 100644 index 0000000..068afbe --- /dev/null +++ b/second/week_01/69/LinkedList.md @@ -0,0 +1,77 @@ +--- +title: 我理解的LinkedList +tags: + - JDK源码阅读 + - 集合框架 +categories: hexo +copyright: true +reward: true +related_posts: true +rating: true +urlname: linkedlist.html +date: 2020-03-07 16:28:22 +--- +LinkedList的实现细节: +- 底层由双向链表实现 +- 由于链表结构的特性,从首尾进行元素的操作时效率较高(O(1)时间复杂度),而通过索引的方式进行元素的操作则效率较低(O(n)时间复杂度),因为需要进行链表的遍历 +- 没有容量大小的限制,对于添加的元素直接挂到链表的尾部即可 +- 与ArrayList类似,覆盖了默认的序列化机制 + + + +### 源码 +```java +// 通过内部类Node作为链表的节点,Node通过next、prev两个指针来关联前后节点 +private static class Node { + E item; + Node next; + Node prev; + + Node(Node prev, E element, Node next) { + this.item = element; + this.next = next; + this.prev = prev; + } +} +``` +```java +// 直接在尾部添加元素 +public boolean add(E e) { + linkLast(e); + return true; +} +void linkLast(E e) { + final Node l = last; + final Node newNode = new Node<>(l, e, null); + last = newNode; + if (l == null) + first = newNode; + else + l.next = newNode; + size++; + modCount++; +} + +// 前面提到由于链表的特性,如果通过索引来操作元素需要遍历链表,效率较低。 +// 在源码中jdk利用了一个比较巧妙的方式,通过二分法来进行遍历 +public E get(int index) { + checkElementIndex(index); + return node(index).item; +} +Node node(int index) { + // assert isElementIndex(index); + // 如果index的值小于size的一半,则从首部开始遍历 + if (index < (size >> 1)) { + Node x = first; + for (int i = 0; i < index; i++) + x = x.next; + return x; + } else { + // 如果index大于size的一半,则从尾部开始遍历 + Node x = last; + for (int i = size - 1; i > index; i--) + x = x.prev; + return x; + } +} +``` \ No newline at end of file -- Gitee