写在前面
最近在看《On Java 8》中文版,大概翻了一遍,发现里面内容有 Java 开发不太关心的很多方面(主要是C/C++的对比),甚至中间看到一半怀疑这本书是假的 Java 编程思想,但最终还是决定看下来,并做做笔记,抽丝剥茧一般。
本次笔记随机看章节记录,先从 集合
章节开始看,没有所谓先后顺序,并且此次准备看书的同时结合源码理解,希望能有比较好的总结。
集合
迭代器 Iterator
迭代器模式
涉及 4 个角色如下:
- Iterator,迭代器角色,提供通用遍历方法,如
java.util.Iterator
- Container,迭代器容器,提供生成迭代器,如
java.lang.Iterable
- Concrete Container,具体迭代器容器,如
java.util.ArrayList
- Concrete Iterator,具体迭代器角色实现,以内部类的方式挂载于具体迭代器容器,如
java.util.ArrayList.Itr
Demo 示例:
/**
* Iterator(迭代器角色),提供通用遍历方法
*
* @author akazc
* @date 2020/9/1 22:32
*/
public interface Iterator<E> {
boolean hasNext();
E next();
void remove();
}
/**
* Container (迭代器容器),提供生成迭代器
*
* @author akazc
* @date 2020/9/1 22:35
*/
public interface Iterable<T> {
Iterator<T> iterator();
}
/**
* Concrete Container(具体迭代器容器)
*
* @author akazc
* @date 2020/9/1 22:37
*/
public class MyList<E> implements Iterable {
private Object[] elements;
private int size = 0;
public MyList() {
elements = new Object[16];
}
public int getSize() {
return size;
}
public void add(E e) {
elements[size++] = e;
}
// Concrete Iterator (具体迭代器角色实现),以内部类的方式挂载于具体迭代器容器中
private class myIterator implements Iterator {
int cursor;
@Override
public boolean hasNext() {
return cursor != size;
}
@Override
@SuppressWarnings("unchecked")
public Object next() {
if (cursor >= size) {
return null;
}
return (E) elements[cursor++];
}
@Override
public void remove() {
}
}
@Override
public Iterator iterator() {
return new myIterator();
}
}
Fail-Fast相关
依次掌握以下概念:
-
增强 For 循环做了什么
增强 For 循环是 Java 提供的语法糖,用于遍历。但如果我们查看编译后的 class 文件可以看出,本质上增强 For 循环使用的是 Iterator 进行遍历,编译前后代码如下:
public static void main(String[] args) { List<Integer> ints = new ArrayList<>(Arrays.asList(1, 2, 3, 5, 5, 6, 7, 5, 9)); for (Integer i : ints) { System.out.println(i); } }
public static void main(String[] args) { List<Integer> ints = new ArrayList(Arrays.asList(1, 2, 3, 5, 5, 6, 7, 5, 9)); Iterator var2 = ints.iterator(); while(var2.hasNext()) { Integer i = (Integer)var2.next(); System.out.println(i); } }
-
什么是 Fail-Fast
通俗的讲 fail-fast 就是一种系统设计理念,即在系统设计时优先考虑异常情况,一旦异常发生,直接停止并上报。在实际开发中,开发人员在写业务代码时也时常会运用到这种机制,简单示例如下:
public int divide(int divisor,int dividend){ if(dividend == 0){ throw new RuntimeException("dividend can't be null"); } return divisor/dividend; }
-
Java 集合中的 Fail-Fast
Java 集合模块中,Fail-Fast 的设计应用在 防止多个线程操作同一个集合的内容进行操作时产生错误 ,如存在集合 C,此时 A 线程对 C 进行 Iterator 遍历时,B 线程对 C 进行了修改,则会抛出
ConcurrentModificationException
异常。但是实际应用场景中,使用不当的时候,单线程操作也会发生该异常,这就需要追溯到 Java 集合Fail-Fast 机制的具体实现。举 ArrayList 为例,查看源码如下:ArrayList 维护了一个变量 modCount,简单理解为用于记录每次集合元素的操作次数
protected transient int modCount = 0;
如 remove 方法,执行时会改变 modCount 值
public E remove(int index) { rangeCheck(index); modCount++; E oldValue = elementData(index); 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 return oldValue; }
ArrayList 维护了两个 Iterator,一个是 Itr,一个是 ListItr,ListItr 提供了向前遍历的方法;查看 Itr 源码可以看出, 初始化时生成
expectedModCount = modCount
,在进行元素操作时调用checkForComodification()
方法去校验 modCount 值,从而保证了数组元素的正常操作,就这样形成了 fail-fast 机制。private class Itr implements Iterator<E> { int cursor; // index of next element to return int lastRet = -1; // index of last element returned; -1 if no such 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]; } 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(); } } @Override @SuppressWarnings("unchecked") public void forEachRemaining(Consumer<? super E> consumer) { // do something ... } final void checkForComodification() { if (modCount != expectedModCount) throw new ConcurrentModificationException(); } }
结合源码,我们可以触发集合类的 fail-fast 机制,抛出
ConcurrentModificationException
异常,如下:public static void main(String[] args) { List<Integer> ints = new ArrayList<>(Arrays.asList(1, 2, 3, 5, 5, 6, 7, 5, 9)); // 透过实际编译出来的代码可以看出:高级for循环遍历本质是Iterator遍历。 // 简单讲:Iterator遍历时内部维护自己的cursor,且遍历每一个值都会检查 modCount 值(checkForComodification); // 此时调用 List.remove方法会修改 modCount 值,故抛出异常 Exception java.util.ConcurrentModificationException,遍历无法继续 for (Integer i : ints) { if (i == 5) { ints.remove(i); } } // 迭代器遍历删除,内部实现逻辑是:先检查 modCount 值,再调用删除方法,成功删除后更新 modCount 值 Iterator<Integer> iterator = ints.iterator(); // can remove by Iterator while (iterator.hasNext()) { Integer next = iterator.next(); if (next == 5) { iterator.remove(); } } }
上述例子也是常常遇到的问题,遍历集合时如何删除元素,我们可以发现,使用迭代器进行元素删除是最推荐的做法。使用普通 for 循环进行遍历删除,如果不控制角标,是可能遇到非正确结果(即删不干净),在这里不做展开比较讨论;当然,Java 8 中已经可以使用 filter 去进行元素过滤,在这里也不展开讨论了。
-
Fail-Safe
同样的,存在 Fail-Fast,也存在其对立面 Fail-Safe,其所属类位于 java.util.concurrent 包下,在此不做扩展,后续在 concurrent 相关内容讨论。
参考
www.hollischuang.com/archives/33
大框架关系
关系图
下图为 On Java 8 的图,引用参考
《On Java 8》原版:
最常使用的集合用黑色粗线线框表示。虚线框表示接口,实线框表示普通的(具体的)类。带有空心箭头的虚线表示特定的类实现了一个接口。实心箭头表示某个类可以生成箭头指向的类的对象!
《On Java 8》译版:
黄色为接口,绿色为抽象类,蓝色为具体类。虚线箭头表示实现关系,实线箭头表示继承关系。
源码接口梳理
笔者多此一举又看了一下源码,梳理了一下常用集合的关系链,用代码的方式呈现,主要是真的当成笔记字典
(势必要把 punchline 写进代码注释的程序员对上句话又偷偷押了韵,见笑)
- Collection
public interface Iterable<T> { }
public interface Collection<E> extends Iterable<E> { }
public abstract class AbstractCollection<E> implements Collection<E> { }
- List
public interface List<E> extends Collection<E> { }
public abstract class AbstractList<E> extends AbstractCollection<E> implements List<E> { }
- ArrayList:
public class ArrayList<E> extends AbstractList<E>
implements List<E>, RandomAccess, Cloneable, java.io.Serializable { }
- LinkedList:
public abstract class AbstractSequentialList<E> extends AbstractList<E> { }
public class LinkedList<E>
extends AbstractSequentialList<E>
implements List<E>, Deque<E>, Cloneable, java.io.Serializable { }
- Set
public interface Set<E> extends Collection<E> { }
public abstract class AbstractSet<E> extends AbstractCollection<E> implements Set<E> { }
- HashSet
public class HashSet<E>
extends AbstractSet<E>
implements Set<E>, Cloneable, java.io.Serializable{ }
- LinkedHashSet
public class LinkedHashSet<E>
extends HashSet<E>
implements Set<E>, Cloneable, java.io.Serializable { }
- TreeSet
public interface SortedSet<E> extends Set<E> { }
public interface NavigableSet<E> extends SortedSet<E> { }
public class TreeSet<E> extends AbstractSet<E>
implements NavigableSet<E>, Cloneable, java.io.Serializable{ }
- Map
public interface Map<K,V> { }
public abstract class AbstractMap<K,V> implements Map<K,V> { }
- HashMap
public class HashMap<K,V> extends AbstractMap<K,V>
implements Map<K,V>, Cloneable, Serializable { }
- LinkedHashMap
public class LinkedHashMap<K,V>
extends HashMap<K,V>
implements Map<K,V> { }
- TreeMap:
public interface SortedMap<K,V> extends Map<K,V> { }
public interface NavigableMap<K,V> extends SortedMap<K,V> { }
public class TreeMap<K,V>
extends AbstractMap<K,V>
implements NavigableMap<K,V>, Cloneable, java.io.Serializable { }
List
List 常用且常见的就是 ArrayList 和 LinkedList,从字面可以看出一个基于数组,一个基于链表。
异点
ArrayList 基于数组实现,整个源码观摩下来感受就是对数组的把控,这里想分享一下 System.arraycopy
:
在 ArrayList 的源码中发现,System.arraycopy 方法出现的频率很高,这是一个 native 方法 (底层由非 Java 实现例如 C/C++,但是被编译成 DLL 供 Java 调用),用于数组拷贝,在 ArrayList 中,大部分元素操作方法是调用 System.arraycopy 去实现的,例如 remove()
、add(int index, E element)
等。
这里有意思的点是 System.arraycopy 的拷贝方式。这里提一下,大多数人习惯用 深拷贝
、 浅拷贝
、引用类型
、基本类型
的字面概念去理解,个人觉得有点绕,分享一下个人观点:
在 Java 里面,所谓 引用类型
本质上也是 基本类型
,相当于指向实际对象的指针,所以只要实际对象没有发生替换,所有的类型都是基本类型;在此前提下,区分深浅拷贝只需要关注实际对象有没有发生替换即可。例子:
// 所谓`浅拷贝`,没有发生实际对象替换
public static Object copyNotDeep (Object A) {
// do something copy just for A
return A;
}
// 所谓`深拷贝`,发生实际对象替换
public static Object copyDeep (Object A) {
Object B = new Object();
// do something copy
return B;
}
System.arraycopy 拷贝对象时没有发生 对象替换
,拷贝的是指针引用。
LinkedList 基于链表实现,维护了一个 Node
内部类实现双向链表,主要是操控 Node
对元素进行操作,对于数据结构熟悉的同学估计看几遍源码就大概吃透了,里面有很多值得我们学习的地方,这里就不展开细节进行讨论了。
同点
-
Iterator
两者皆维护了迭代器,除了基本的迭代器之外,还有 ListIterator 提供
hasPrevious
、previous
方法供调用者进行往前遍历。LinkedList 多提供了一个逆序遍历的 DescendingIterator 迭代器。关于迭代器,下面会单独拎出来讲。
-
Java 8 新特性
Java 8 版本开始引入 函数式编程、流式编程,集合这一块也提供对应的接口予以支持,包括
spliterator()
、forEach()
、parallelStream()
、stream()
。关于这些接口,后续有机会在对应章节做笔记总结,在此不展开讨论。
Map-HashMap
无论是从哪个角度触发,HashMap 都是值得好好学习的。开始着手总结时,阅读源码的同时结合了大量资料,发现这一块内容实在是很多,单独拎出来写一篇总结笔记也不为过,但是既然放在一个模块里面,最主要还是想记录最精简纯粹的东西,基于 Java 8,开始记录 HashMap。
HashMap 如图
这张粗糙的 HashMap 自画像可以让我们初步认识一下他,涉及到 数组 + 链表 + 红黑树 三种数据结构,细节循序渐进给出。
成员变量
HashMap 有很多成员变量定义,如果没有对概念理解清楚,后续学习起来就十分吃力。
概念
-
table 数组
transient Node<K,V>[] table;
-
entrySet
transient Set<Map.Entry<K,V>> entrySet;
-
默认 table 初始化容量,值为16 (源码注释也有aka,值得玩味)
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16
-
最大 table 容量,扩容不可超过该值,每次扩容都是 double,但是
MUST be a power of two <= 1<<30.
static final int MAXIMUM_CAPACITY = 1 << 30;
-
HashMap 实际存储长度
transient int size;
-
加载因子,默认为 0.75f,可在初始化 HashMap 的时候修改
static final float DEFAULT_LOAD_FACTOR = 0.75f; // 默认值
final float loadFactor; // 没有初始化则默认取 0.75f
-
扩容阈值,借助公式简单理解
threshold = table.length * loadFactor
int threshold;
-
链表树化(链表转为红黑树)阈值
static final int TREEIFY_THRESHOLD = 8;
-
红黑树链化(红黑树转为链表)阈值
static final int UNTREEIFY_THRESHOLD = 6;
-
modCount,用于迭代的 fail-fast机制,强调一点:结构发生变化才会修改该值,若某个key对应的value值被覆盖则不属于结构变化,不会修改 modCount 值。
transient int modCount;
联系
-
size 和 capacity (即 table.length)
size 是 map 中实际存储 key-value 对的对数,如存了 4 个 key-value,size 就是 4 。
capacity 是 map 容量,表示能够存入多少 key-value 对。默认初始化值为 16,如若没有设定
loadFactor
,则存在关系 capacity > size。另外,map 存在特殊的扩容机制,达到扩容条件时,map 自身会进行扩容增大 capacity 值以存储更多元素,每次扩容都是 double 增长,所以 capacity 一直是 2 的幂。
此时,有杠精就会来事儿了:如果我初始化 map 时传入的 capacity 是单数呢,那岂不是不能保证 capacity 是 2 的幂?对此,Java 已帮你考虑到了,请你细品以下源码:
/** * Returns a power of two size for the given target capacity. */ static final int tableSizeFor(int cap) { int n = cap - 1; n |= n >>> 1; n |= n >>> 2; n |= n >>> 4; n |= n >>> 8; n |= n >>> 16; return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1; }
-
loadFactor 和 threshold
threshold 是扩容阈值,存在公式 threshold = loadFactor * capacity 。capacity 即 table.length 。
loadFactor 是加载因子,默认为 0.75f,0.75f 与 2 的幂的乘积是整数,即保证了threshold 值一直为整数。
这两个关系与上面提到的 特殊的扩容机制 有关,查阅 map 源码就可以发现:调用 put( ) 方法增加元素时,都会比较 size 与 threshold 的值(间接比较 size 与 capacity 的关系),从而决定是否调用
resize( )
进行扩容。通俗的讲法就是:存在一个16个格子的容器,当装满了 13 > (16 * 0.75 = 12 ) 时,容器就会扩大一倍,增至容量 32 个格子。如若继续装满至 25 > (32 * 0.75 = 24) 时,容器继续扩大一倍,增至容量 64 个格子。
此时不经提出疑问:为什么要选择 0.75f 即 3/4 去衡量一个 map 的承载能力?别着急,后面深究会提到。
-
TREEIFY_THRESHOLD 和 UNTREEIFY_THRESHOLD
Java 8 的 map 相对于 java 7 ,在保留链表的机制上引入了红黑树,是属于一大优化操作。
HashMap 中存在两个阈值,用于操作 链表 与 红黑树 的转换,这两个值就是 TREEIFY_THRESHOLD 和 UNTREEIFY_THRESHOLD。
当 链表元素长度大于 TREEIFY_THRESHOLD (value = 8) 时,链表转换为红黑树。
当 红黑树元素长度小于 UNTREEIFY_THRESHOLD (value = 6) 时,红黑树转为链表。
此时又要提出疑问:Java 为什么选择 8 、6 作为链树转换的阈值?熟悉数据结构的同学比较能体会,在此不做展开讨论。
深入
-
Hash 与 数组角标
hashmap 是通过 **hash 算法 ** 结合 位运算 去确定数组角标位置的,具体代码如下:
static final int hash(Object key) { // hashmap 中的 hash 算法 int h; return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16); // 高位运算 } // (n - 1) & hash // 位运算,n为数组长度。该操作等价于对 length 取模,但是效率比取模运算高 // example find index in hashmap table where key equals "haha" // index = table[(n-1) & hash("haha")]
高位运算的好处:通过 key 的 hashCode 进行 高16位异或低16位运算,保证高低bit都参与到hash计算中,同时不会有太大的开销。
"位运算(&)" 取代 "取模运算(%)" 的原因:效率高,只需在内存中进行操作,无需转换为十进制。
Java 8 Hashmap 通过以上计算得出一个 key 在数组中的角标位置从而进行操作,个中原理在此不做展开讨论。
-
put 操作
源码如下:
final V putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict) { Node<K, V>[] tab; Node<K, V> p; // put int n, i; if ((tab = table) == null || (n = tab.length) == 0) // 初始化 table n = (tab = resize()).length; if ((p = tab[i = (n - 1) & hash]) == null) { // 无hash碰撞则直接插入元素 tab[i] = newNode(hash, key, value, null); } else { // 发生hash碰撞 Node<K, V> e; // exist K k; if (p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k)))) { // 通过比较key值( == 、equals ) 命中第一个节点元素 e = p; } else if (p instanceof TreeNode) // 若为红黑树节点,则进行红黑树put操作并返回树节点 e = ((TreeNode<K, V>) p).putTreeVal(this, tab, hash, key, value); else { // 链表遍历put操作,其中包含转为红黑树操作 for (int binCount = 0; ; ++binCount) { if ((e = p.next) == null) { // 插入至链表末尾 p.next = newNode(hash, key, value, null); // 若超过链树阈值,则将链表转为红黑树 if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st treeifyBin(tab, hash); break; } if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k)))) break; p = e; } } if (e != null) { // existing mapping for key V oldValue = e.value; if (!onlyIfAbsent || oldValue == null) e.value = value; afterNodeAccess(e); return oldValue; } } ++modCount; if (++size > threshold) // 大于阈值则进行数组扩容 resize(); afterNodeInsertion(evict); return null; }
可以看到其中涉及到的操作很多,包括 hash 碰撞的处理,链树转化,数组扩容,但是总结步骤可以如下:
- 判断键值对数组 table[] 是否为空或为 null,否则执行 resize( ) 进行初始化 table;
- 索引位置 table[i = (n - 1) & hash] 是否有元素存在,不存在则直接新增节点元素存入,转向 f. ;存在则说明发生 hash 碰撞,转向 c. 。
- 判断 table[i] 的首个元素是否 key 相同,若相同则直接覆盖 value 值存入,否则转向 d. 。这里提及的 key 相同是指 hashCode 及 equals 方法(这里可以复习一下 == 与 equals 的区别)。
- 判断第一个节点是否为 TreeNode 从而确定当前结构是否为红黑树,若是则调用红黑树的 putTreeVal 方法存入 value,否则转入 e. 。
- 遍历 table[i] 的链表,进行链表的插入操作,key存在且相同则直接覆盖value。若在更新后的链表长度超过 8 ,则进行链树转换变成红黑树。
- 插入成功后,比较 size 与 threshold 从而判断是否进行扩容处理。
从源码中可以看出有几个细节可以细品:
- put 操作中,modCount 只有在 table 上新增了键值对才会自增,其他发生hash碰撞的情况都是不会改变的。
- key 相同的比较判断是
(k = p.key) == key || (key != null && key.equals(k))
,先比较了 hashCode 再进行 equals 比较。这里可以思考一下,为什么上层已经进行了 hash 定位,这里还是进行了 key 的hash 判断比较。
-
hash 冲突的解决
hashmap 处理 hash 冲突的方法是链地址法,其可能导致的问题是冲突 key 所在的链表会越来越长,导致效率大幅度降低。针对此问题,java 8 进行了优化,把超过长度 8 的链表转换成红黑树查询,从 O(N) 提高至 O(logN)。
通常,hash 冲突需要优化的两个点:
- 扩大 hash 桶即 table 数组容量。hash 桶容量大了才不容易发生 hash 碰撞
- 优化 hash 算法。通过优秀的 hash 算法计算出的数组下标使得元素分布均匀,不易发生 hash 冲突。
针对两个优化点,hashmap 在设计时也做了对应的优化,我们之前提及到的包括 loadFactor (加载因子) 、hash( ) 算法都是 Java 在设计 hashmap 做出的设定。
-
为什么要把 loadFactor 默认设定为 0.75f
hashmap size 达到 threshold 需要调用 resize( ) 进行扩容,resize( ) 进行扩容时除了元素的复制之外,还需要进行大量的 rehash 计算,重新对扩容后的数组进行 hash 分配下标,这一操作是存在性能开销的。Java 官方说法是,0.75 这个值在时间和空间成本上提供了很好的权衡。若该值太大,则出现 hash 碰撞的几率就越高;若该值太小,则会频繁出现扩容,增加性能开销。在一般非特殊情况下,开发中不会特意去修改这个值。另外我们之前提到,0.75f 与 2 的幂 的乘积结果都是整数,而其扩容翻倍的机制注定了 capacity 的值一直是 2 的幂,所以也保证了 threshold 的结果最终也会是整数。
-
为什么 hashmap 初始默认容量为 16?初始化 hashmap 时如何指定初始化容量
为什么是 16,是 16 一定有 16 的道理,很多事情讲究一个度,太大或者太小都不合适,相信 Java 选定 16 一定是适中的
(没有找到设定 16 的最好的解释,姑且糊弄过去)。在已知 map 需要存储的元素个数时,我们最好是在初始化 map 的时候指定容器大小。阿里巴巴开发手册也建议我们在创建 map 的时候去指定初始值大小。那问题来了,该如何去指定初始化值大小,拍脑袋直接定肯定是不可取的吧。
初始化 hashmap 大小的目的就是为了减少 resize( ) 扩容带来的性能开销。假设已知要往 hashmap 中存入 7 个元素,那么我们指定初始化容器容量为 7 的话,根据 tableSizeFor( ) 方法我们得知最终生成的 capacity 值为 8,即生成的 table[] 数组长度为 8,接着当我们进行 put 操作存入第 6 个元素时,达到扩容阈值 8 * 0.75f = 6,故下一次操作就会进行扩容操作。
所以,当我们了解了 hashmap 的扩容机制之后,就可以推断出上述场景下,应该指定容器容量为区间 9到16之间取值,最终生成长度为 16 的数组,在 put 操作时就不会产生扩容问题。反推即可得出公式:
result = (int) ((float) expectedSize / 0.75F + 1.0F)
参考
youzhixueyuan.com/the-underly…
hollischuang.github.io/toBeTopJava…
Map-LinkedHashMap
LinkedHashMap 继承 HashMap,基本上与 HashMap 无异,最大的区别是 LinkedHashMap 保证了输入元素顺序与输出元素顺序一致,其实现原理是内部维护了一个链表节点,代码如下:
static class Entry<K,V> extends HashMap.Node<K,V> {
Entry<K,V> before, after;
Entry(int hash, K key, V value, Node<K,V> next) {
super(hash, key, value, next);
}
}
HashMap 维护了三个方法,供 LinkedHashMap 重载,保证了元素输入输出的有序性,三个方法如下:
// Callbacks to allow LinkedHashMap post-actions
void afterNodeAccess(Node<K,V> p) { }
void afterNodeInsertion(boolean evict) { }
void afterNodeRemoval(Node<K,V> p) { }
具体如何实现可查阅 LinkedHashMap 源码,不在此进行展开讨论。
Map-TreeMap
TreeMap 与 HashMap 的实现大有不同,其底层实现主要是树。TreeMap 最大的特点是保证了元素的有序性,维护元素有序性的原理是内部维护了一个 Comparator 供 key 值进行比较。其 get( )方法实现如下:
// get( ) 方法实现
final Entry<K,V> getEntry(Object key) {
// Offload comparator-based version for sake of performance
if (comparator != null)
return getEntryUsingComparator(key);
if (key == null)
throw new NullPointerException();
@SuppressWarnings("unchecked")
Comparable<? super K> k = (Comparable<? super K>) key;
Entry<K,V> p = root;
while (p != null) {
int cmp = k.compareTo(p.key);
if (cmp < 0)
p = p.left;
else if (cmp > 0)
p = p.right;
else
return p;
}
return null;
}
可以看出,其实现过程完全基于 key 的比较实现,put( ) 也是一样基于 key 的比较再在对应节点进行 set value 操作。其他方法实现细节有兴趣的可以自行查阅源码,在此不进行展开讨论。
Set
关于 Set ,常用实现有:HashSet、LinkedHashSet、TreeSet。稍微看下源码就可以得知:所有 Set 的实现机制全部基于 Map,HashSet 对应 HashMap,LinkedHashSet 对应 LinkedHashMap,TreeSet 对应 TreeMap,举例如下:
private transient HashMap<E,Object> map;
public HashSet() {
map = new HashMap<>();
}
通俗的理解就是 Set 就是没有 value 的 Map,所以 Set 的元素不重复性实现就是 Map 的 key 唯一性实现,懂 Map 即懂 Set 。
集合工具类
Java 提供了 Arrays 、 Collections 工具类,供开发者进行集合相关操作。调用时直接查阅 API 文档即可,但是查阅其源码还是有很多值得我们学习的地方,例如排序算法的具体实现,元素查找的具体实现等等。
Arrays
-
Arrays.asList( )
将数组转化成 List。
注意坑点:转换得到的 ArrayList 是 Arrays 的一个内部类,没有提供增删操作,直接调用会报错;若需要可以作为参数传给真正的 ArrayList,如下:
ArrayList<Integer> integers = new ArrayList<>(Arrays.asList(1, 5, 5, 6, 7, 5, 9));
后续补充 ... ...
Collections
后续补充 ... ...
JUC的集合类
JUC 是 java.util.concurrent 的缩写,代指并发编程相关包。日常开发所用的 List、Set、Map 都是线程不安全的,存在并发编程的通病,而 Java 提供了关于集合的并发类,常见的有 ConcurrentHashMap、CopyOnWriteArrayList、CopyOnWriteArraySet 等等,在此稍作了解即可,知道有这么一回事的存在,可以用什么去替代,相关内容细节应该会在 Java 并发编程笔记中去总结。
总结
整篇笔记整理下来,其实最着重的工作量在 HashMap。有些地方梳理得并不好,比如 List;有些地方没有展开梳理,比如说 集合工具类、JUC集合类 的内容,存在种种原因吧,有些是想后续遇到再补充上去,有些是想在另外的模块做处理。如果文中有误的地方或者是有不同的看法,欢迎指出探讨。