diff --git "a/blog-site/content/posts/java/Java\351\233\206\345\220\210.md" "b/blog-site/content/posts/java/Java\351\233\206\345\220\210.md" index a86e05fb..7d702e2a 100644 --- "a/blog-site/content/posts/java/Java\351\233\206\345\220\210.md" +++ "b/blog-site/content/posts/java/Java\351\233\206\345\220\210.md" @@ -132,7 +132,7 @@ int array2[] = { 1,2,3,4,5 }; ``` ### 数组转集合 -1. 使用`Arrays.asList()`方法; +1. 使用`Arrays.asList`方法; ```java public class ArrayToCollection { public static void main(String[] args) { @@ -147,7 +147,7 @@ int array2[] = { 1,2,3,4,5 }; } } ``` -2. 使用`Collections.addAll()`方法; +2. 使用`Collections.addAll`方法; ```java public class ArrayToCollection { public static void main(String[] args) { @@ -230,7 +230,7 @@ protected native Object clone() throws CloneNotSupportedException; - 深拷贝:在浅拷贝的基础上,所有引用其他对象的变量也进行了`clone`,并指向被复制过的新对象; 如果一个被复制的属性都是基本类型,那么只需要实现当前类的`Cloneable`机制就可以了,此为浅拷贝。 -如果被复制对象的属性包含其他实体类对象引用,那么这些实体类对象都需要实现`cloneable`接口并覆盖`clone()`方法。 +如果被复制对象的属性包含其他实体类对象引用,那么这些实体类对象都需要实现`cloneable`接口并覆盖`clone`方法。 `ArrayList`中`clone`方法可以创建一个浅拷贝。 ```java // ArrayList类 @@ -520,16 +520,16 @@ public class HashSetExample { ### 去重原理 `HashSet`内部实际上是由一个`HashMap`实例支持的,其中`HashMap`的键值对中的键存储了`HashSet`中的元素,而值则是一个占位对象,用来表示键已经存在。 -当调用 `HashSet` 的 `add(E e)` 方法添加元素时,首先会调用元素 `e` 的 `hashCode()` 方法获取其哈希码。`HashSet` 根据哈希码确定元素在内部 `HashMap` 的存储位置。 -如果该位置上已经存在一个元素,则使用 `equals()` 方法比较新元素 `e` 与已存在的元素是否相等。 -如果 `equals()` 方法返回 `true`,则认为新元素与已存在元素相同,不进行添加操作,返回 `false`。 -如果 `equals()` 方法返回 `false`,则说明哈希码冲突,但实际上是不同的对象,此时将新元素添加到 `HashSet` 中,返回 `true`。 -如果该位置上不存在任何元素,则直接将新元素添加到 `HashSet` 中,并返回 `true`。 +当调用`HashSet`的`add(E e)`方法添加元素时,首先会调用元素`e`的 `hashCode`方法获取其哈希码。`HashSet`根据哈希码确定元素在内部`HashMap`的存储位置。 +如果该位置上已经存在一个元素,则使用 `equals` 方法比较新元素`e`与已存在的元素是否相等。 +如果`equals`方法返回`true`,则认为新元素与已存在元素相同,不进行添加操作,返回`false`。 +如果`equals`方法返回`false`,则说明哈希码冲突,但实际上是不同的对象,此时将新元素添加到 `HashSet`中,返回`true`。 +如果该位置上不存在任何元素,则直接将新元素添加到`HashSet`中,并返回`true`。 -简单来说,`HashSet` 利用对象的哈希码和`equals`方法来确保集合中不存储重复的元素。 +简单来说,`HashSet`利用对象的哈希码和`equals`方法来确保集合中不存储重复的元素。 当添加新元素时,先计算其哈希码确定存储位置,如果位置上已存在相同哈希码且通过`equals`方法比较相等的元素,则不添加,否则添加新元素到集合中。 -尝试阅读 HashSet 的具体实现源码,HashSet 添加方法的实现源码如下: +尝试阅读`HashSet`的具体实现源码,`HashSet`添加方法的实现源码如下: ```java // hashmap 中 put() 返回 null 时,表示操作成功 public boolean add(E e) { @@ -613,17 +613,17 @@ final V putVal(int hash, K key, V value, boolean onlyIfAbsent, return null; } ``` -从上述源码可以看出,当将一个键值对放入 HashMap 时,首先根据 key 的 hashCode() 返回值决定该 Entry 的存储位置。如果有两个 key 的 hash 值相同,则会判断这两个元素 key 的 equals() 是否相同,如果相同就返回 true,说明是重复键值对,那么 HashSet 中 add() 方法的返回值会是 false,表示 HashSet 添加元素失败。 -因此,如果向 HashSet 中添加一个已经存在的元素,新添加的集合元素不会覆盖已有元素,从而保证了元素的不重复。 -如果不是重复元素,put 方法最终会返回 null,传递到 HashSet 的 add 方法就是添加成功。 +从上述源码可以看出,当将一个键值对放入`HashMap`时,首先根据`key`的`hashCode()`返回值决定该`Entry`的存储位置。如果有两个`key`的`hash`值相同,则会判断这两个元素`key`的`equals()`是否相同,如果相同就返回`true`,说明是重复键值对,那么`HashSet`中`add`方法的返回值会是`false`,表示`HashSet`添加元素失败。 +因此,如果向`HashSet`中添加一个已经存在的元素,新添加的集合元素不会覆盖已有元素,从而保证了元素的不重复。 +如果不是重复元素,`put`方法最终会返回`null`,传递到`HashSet`的`add`方法就是添加成功。 ### equals与hashCode 因为`HashSet`,底层用到了`equals`和`hashCode`方法,如果对象中的`equals`和`hashCode`方法没有正确地重写,可能会导致`HashSet`在判断元素相等性时出现问题,从而允许添加相同的元素。 `equals()`地址比较是通过对象的哈希值来比较的。`hash`值是由`hashCode`方法产生的,`hashCode`属于`Object`类的本地方法,默认使用`==`比较两个对象,如果`equals()`相等`,hashcode`一定相等,如果`hashcode`相等,`equals`不一定相等。 -所以在覆盖`equals()`方法时应当总是覆盖` hashCode() `方法,保证等价的两个对象散列值也相等。 +所以在覆盖`equals`方法时应当总是覆盖`hashCode`方法,保证等价的两个对象散列值也相等。 -下面的代码中,新建了两个等价的对象,并将它们添加到`HashSet`中。我们希望将这两个对象当成一样的,只在集合中添加一个对象,但是因为`EqualExample`没有实现`hashCode()`方法,因此这两个对象的散列值是不同的,最终导致集合添加了两个等价的对象。 +下面的代码中,新建了两个等价的对象,并将它们添加到`HashSet`中。我们希望将这两个对象当成一样的,只在集合中添加一个对象,但是因为`EqualExample`没有实现`hashCode`方法,因此这两个对象的散列值是不同的,最终导致集合添加了两个等价的对象。 ```java public class MainTest { public static void main(String[] args) { @@ -639,145 +639,152 @@ public class MainTest { } } ``` -所以在覆盖 `equals()`方法时应当总是覆盖`hashCode()`方法,保证等价的两个对象散列值也相等。 - -### 线程安全 - +所以在覆盖`equals`方法时应当总是覆盖`hashCode`方法,保证等价的两个对象散列值也相等。 ## Queue -队列是一种经常使用的集合。Queue实际上是实现了一个先进先出(FIFO:First In First Out)的有序列表。它和List的区别在于,List可以在任意位置添加和删除元素,而Queue只有两个操作: -- 把元素添加到队列末尾; -- 从队列头部取出元素; +在Java中,队列是一种常用的数据结构,用于按顺序存储元素,通常以先进先出(`FIFO`:`First In First Out`)的方式进行操作。 +它和`List`的区别在于,`List`可以在任意位置添加和删除元素,而队列只有两个操作,把元素添加到队列末尾,或者从队列头部取出元素。 常见实现: -- LinkedList:可以用它来实现双向队列; -- PriorityQueue:基于堆结构实现,可以用它来实现优先队列; - -Queue实现通常不允许插入null元素,尽管一些实现,如LinkedList,不禁止插入null元素。即使在允许它的实现中,null也不应插入Queue中,因为poll方法也使用null作为特殊返回值,用来表示队列不包含任何元素。 -> poll(): 检索并删除此队列的头部,如果此队列为空,则返回null -> peek(): 检索但不删除此队列的头部,如果此队列为空,则返回null +- `LinkedList`:`LinkedList`类实现了`Queue`接口,同时也实现了`Deque`接口(双端队列)。它可以作为队列、双端队列或堆栈使用; + ```java + Queue queue = new LinkedList<>(); + queue.offer("A"); + queue.offer("B"); + queue.poll(); // 返回 "A" 并移除 + ``` +- `PriorityQueue`:`PriorityQueue`是一个基于优先级堆实现的队列,元素根据其自然顺序进行排序; + ```java + PriorityQueue pq = new PriorityQueue<>(); + pq.offer(2); + pq.offer(1); + pq.offer(3); + pq.poll(); // 返回 1 并移除 + ``` +- `ArrayDeque`:`ArrayDeque`类实现了`Deque`接口,是一个可调整大小的数组实现的双端队列,通常比`LinkedList`更快; + ```java + Deque deque = new ArrayDeque<>(); + deque.offer("A"); + deque.offerFirst("B"); + deque.pollLast(); // 返回 "A" 并移除 + ``` + +队列的实现通常不允许插入`null`元素,尽管一些实现,如`LinkedList`,不禁止插入`null`元素。 +即使在允许的实现中,`null`也不应插入队列中,因为`poll`方法也使用`null`作为特殊返回值,用来表示队列不包含任何元素。这会导致难以区分队列中的`null`元素和队列为空的情况。 + +`poll()`和`peek()`方法: +- `poll()`: 检索并删除此队列的头部,如果此队列为空,则返回`null`; +- `peek()`: 检索但不删除此队列的头部,如果此队列为空,则返回`null`; ## HashMap -- HashMap 是一个散列表,它存储的内容是键值对(key-value)映射; -- HashMap 实现了 Map 接口,根据键的 HashCode 值存储数据,具有很快的访问速度,最多允许一条记录的键为 null,不支持线程同步; -- HashMap 是无序的,即不会记录插入的顺序; +`HashMap`是一个散列表,它存储的内容是键值对(`key-value`)映射。 +`HashMap`实现了`Map`接口,根据键的`HashCode`值存储数据,具有很快的访问速度,最多允许一条记录的键为`null`,不支持线程同步。`HashMap`是无序的,即不会记录插入的顺序。 相关操作: -- 存贮: 通过key的hashcode方法找到在hashMap 存贮的位置,如果该位置有元素,则通过equals方法进行比较,equals返回值为true,则覆盖value,equals返回值为false则,在该数组元素的头部追加该元素,形成一个链表结构; -- 读取:通过key的hashcode方法获取元素存在该数组的位置,然后通过equals拿到该值; -- 结构: hashMap是一个散列数据结构,HashMap底层就是一个数组结构,数组中的每一项又是一个链表; - -总的来说hashMap底层将key-value(键值对)当成一个整体来处理,hashMap底层采用一个 Entry 数组保存所有的键值对,当存储一个entry对象时,会根据key的hash算法来决定存放在数组中的位置,在根据equals方法来确定在链表中的位置,读取一个entry对象,先根据hash算法确定在数组中的位置,再根据equals来获取该值,equals和equals在hashMap中就像一个坐标一样,来确定hashMap中的值。 +- 存贮:通过`key`的`hashcode`方法找到在`hashMap`存贮的位置,如果该位置有元素则通过`equals`方法进行比较,如果`equals`返回值为`true`,则覆盖`value`; +如果`equals`返回值为`false`则在该数组元素的头部追加该元素,形成一个链表结构; +- 读取:通过`key`的`hashcode`方法获取元素存在该数组的位置,然后通过`equals`拿到该值; -### 相关概念 -- `capacity`: 容量,默认16; -- `loadFactor`: 负载因子,表示HashMap满的程度,默认值为0.75f,也就是说默认情况下,当HashMap中元素个数达到了容量的3/4的时候就会进行自动扩容; -- `threshold`: 阈值;`阈值 = 容量 * 负载因子`。默认12; -- hash碰撞:即hash冲突,两个不同的输入值,根据同一散列函数计算出的散列值相同的现象叫做碰撞。hash碰撞就是用同一hash散列函数计算出相同的散列值;当插入hashmap中元素的key出现重复时,这个时候就发生了hash碰撞; +总的来说`hashMap`底层将`key-value`当成一个整体来处,`hashMap`底层采用一个`Entry`数组保存所有的键值对。 +当存储一个`entry`对象时,会根据`key`的`hashCode`来决定存放在数组中的位置,再根据`equals`方法来确定在链表中的位置,读取一个`entry`对象。 +`equals`和`hashCode`在`hashMap`中就像一个坐标一样,来定位`hashMap`中的值。 -### 结构 +### HashMap结构 ![HashMap结构](/iblog/posts/annex/images/essays/HashMap结构.png) -- JDK1.7:数组 + 单向链表; -- JDK1.8: 数组 + 单向链表/红黑树; - -在JDK1.8时,如果存储Map中数组元素对应的索引的每个链表超过8,就将单向链表转化为红黑树;当红黑树的节点少于6个的时候又开始使用链表。 - -#### 为什么要使用红黑树 -当有发生大量的hash冲突时,因为链表遍历效率很慢,为了提升查询的效率,所以使用了红黑树的数据结构。 - -#### 为什么不一开始就用红黑树代替链表结构 -JDK文档注释: - -> Because TreeNodes are about twice the size of regular nodes, we use them only when bins contain enough nodes to warrant use (see TREEIFY_THRESHOLD). -And when they become too small (due to removal or resizing) they are converted back to plain bins. - -单个 TreeNode 需要占用的空间大约是普通 Node 的两倍,所以只有当包含足够多的 Nodes 时才会转成 TreeNodes,而是否足够多就是由 TREEIFY_THRESHOLD 的值(默认值8)决定的。而当桶中节点数由于移除或者 resize 变少后,又会变回普通的链表的形式,以便节省空间,这个阈值是 UNTREEIFY_THRESHOLD(默认值6)。 - -#### 为什么树化阈值为8 -JDK1.8HashMap文档注释: -> 如果 hashCode 分布良好,也就是 hash 计算的结果离散好的话,那么红黑树这种形式是很少会被用到的,因为各个值都均匀分布,很少出现链表很长的情况。 -在理想情况下,链表长度符合泊松分布,各个长度的命中概率依次递减,当长度为 8 的时候,概率仅为 0.00000006。这是一个小于千万分之一的概率,通常我们的 Map 里面是不会存储这么多的数据的,所以通常情况下,并不会发生从链表向红黑树的转换。 +在JDK1.7及之前结构为`数组+单向链表`,JDK1.8及之后结构为`数组 + 单向链表/红黑树`。 +在JDK1.8时,如果存储`Map`中数组元素对应的索引的每个链表超过8,就将单向链表转化为红黑树,当红黑树的节点少于6个的时候又开始使用链表。 -HashMap是通过hash算法,来判断对象应该放在哪个桶里面的;JDK 并不能阻止我们用户实现自己的哈希算法,如果我们故意把哈希算法变得不均匀,那么每次存放对象很容易造成hash冲突。 +那么为什么要使用红黑树? +当有发生大量的`hash`冲突时,因为链表遍历效率很慢,为了提升查询的效率,所以使用了红黑树的数据结构。 -链表长度超过 8 就转为红黑树的设计,更多的是为了防止用户自己实现了不好的哈希算法时导致链表过长,从而导致查询效率低,而此时转为红黑树更多的是一种保底策略,用来保证极端情况下查询的效率。红黑树的引入保证了在大量hash冲突的情况下,HashMap还具有良好的查询性能。 +为什么不一开始就用红黑树代替链表结构? +> Because TreeNodes are about twice the size of regular nodes, we use them only when bins contain enough nodes to warrant use (see TREEIFY_THRESHOLD). +And when they become too small (due to removal or resizing) they are converted back to plain bins. -#### 为什么树化阈值和链表阈值不设置成一样 -为了防止出现节点个数频繁在一个相同的数值来回切换。 +这是JDK文档注释,大意为:单个`TreeNode`需要占用的空间大约是普通`Node`的两倍,所以只有当包含足够多的`Nodes`时才会转成`TreeNodes`,而是否足够多就是由`TREEIFY_THRESHOLD`的值(默认值8)决定的。 +而当桶中节点数由于移除或者`resize`变少后,又会变回普通的链表的形式,以便节省空间,这个阈值是`UNTREEIFY_THRESHOLD`(默认值6)。 -举个极端例子,现在单链表的节点个数是9,开始变成红黑树,然后红黑树节点个数又变成8,就又得变成单链表,然后节点个数又变成9,就又得变成红黑树,这样的情况消耗严重浪费。因此干脆错开两个阈值的大小,使得变成红黑树后“不那么容易”就需要变回单链表,同样,使得变成单链表后,“不那么容易”就需要变回红黑树。 +为什么树化阈值要设置为8? +如果`hashCode`分布良好,也就是`hash`计算的结果离散好的话,那么红黑树这种形式是很少会被用到的,因为各个值都均匀分布,很少出现链表很长的情况。 +在理想情况下,链表长度符合泊松分布,各个长度的命中概率依次递减,当长度为8的时候,概率仅为0.00000006。 +这是一个小于千万分之一的概率,通常我们的`Map`里面是不会存储这么多的数据的,所以通常情况下,并不会发生从链表向红黑树的转换。 -#### 引入红黑树后,如果单链表节点个数超过8个是否一定会树化 -不一定,在进行树化之前会进行判断`(n = tab.length) < MIN_TREEIFY_CAPACITY)`是否需要扩容,如果表中数组元素小于这个阈值(默认是64),就会进行扩容。 因为扩容不仅能增加表中的容量,还能缩短单链表的节点数,从而更长远的解决链表遍历慢问题。 -``` - /** - * Replaces all linked nodes in bin at index for given hash unless - * table is too small, in which case resizes instead. - */ - final void treeifyBin(Node[] tab, int hash) { - int n, index; Node e; - if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY) - resize(); - else if ((e = tab[index = (n - 1) & hash]) != null) { - TreeNode hd = null, tl = null; - do { - TreeNode p = replacementTreeNode(e, null); - if (tl == null) - hd = p; - else { - p.prev = tl; - tl.next = p; - } - tl = p; - } while ((e = e.next) != null); - if ((tab[index] = hd) != null) - hd.treeify(tab); - } - } -``` - -### 容量 +`HashMap`是通过`hash`算法来判断对象应该放在哪个桶里面的。JDK并不能阻止我们用户实现自己的哈希算法,如果我们故意把哈希算法变得不均匀,那么每次存放对象很容易造成`hash`冲突。 +链表长度超过8就转为红黑树的设计,更多的是为了防止用户自己实现了不好的哈希算法时导致链表过长,从而导致查询效率低,而此时转为红黑树更多的是一种保底策略,用来保证极端情况下查询的效率。 +红黑树的引入保证了在大量`hash`冲突的情况下,`HashMap`还具有良好的查询性能。 -#### 为什么负载因子默认是0.75 -HashMap中的负载因子这个值现在在JDK的源码中默认是0.75: -``` +那么在JDK1.8引入红黑树后,如果单链表节点个数超过8个是否一定会树化? +这是不一定的,在进行树化之前会进行判断`(n = tab.length) < MIN_TREEIFY_CAPACITY)`是否需要扩容,如果表中数组元素小于这个阈值(默认是64),就会进行扩容。 +因为扩容不仅能增加表中的容量,还能缩短单链表的节点数,从而更长远的解决链表遍历慢问题。 +```java /** - * The load factor used when none specified in constructor. + * Replaces all linked nodes in bin at index for given hash unless + * table is too small, in which case resizes instead. */ -static final float DEFAULT_LOAD_FACTOR = 0.75f; +final void treeifyBin(Node[] tab, int hash) { + int n, index; Node e; + if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY) + resize(); + else if ((e = tab[index = (n - 1) & hash]) != null) { + TreeNode hd = null, tl = null; + do { + TreeNode p = replacementTreeNode(e, null); + if (tl == null) + hd = p; + else { + p.prev = tl; + tl.next = p; + } + tl = p; + } while ((e = e.next) != null); + if ((tab[index] = hd) != null) + hd.treeify(tab); + } +} ``` -在[JDK的官方文档](https://docs.oracle.com/javase/6/docs/api/java/util/HashMap.html)中解释如下: -> As a general rule, the default load factor (.75) offers a good tradeoff between time and space costs. - Higher values decrease the space overhead but increase the lookup cost (reflected in most of the operations of the HashMap class, including get and put). -The expected number of entries in the map and its load factor should be taken into account when setting its initial capacity, so as to minimize the number of rehash operations. -If the initial capacity is greater than the maximum number of entries divided by the load factor, no rehash operations will ever occur. -大意:一般来说,默认的负载因子(0.75)在时间和空间成本之间提供了很好的权衡。更高的值减少了空间开销,但增加了查找成本(反映在HashMap类的大多数操作中,包括get和put)。在设置映射的初始容量时,应该考虑映射中预期的条目数及其负载因子,以便最小化重哈希操作的数量。如果初始容量大于最大条目数除以负载因子,则不会发生重新散列操作。 +### Hash冲突 +在`HashMap`中,哈希冲突,也叫`Hash`碰撞,是指两个不同的键通过同一散列函数计算得到的哈希值相同,从而映射到同一个桶中。 +当插入`HashMap`中元素的`key`出现重复时,这个时候就发生了`Hash`冲突了。哈希冲突是哈希表必须处理的问题。 -负载因子和hashmap中的扩容有关,当hashmap中的元素大于临界值(`threshold = loadFactor * capacity`)就会扩容。所以负载因子的大小决定了什么时机扩容,扩容又影响到了hash碰撞的频率。所以设置一个合理的负载因子可以有效的避免hash碰撞。 +链地址法是处理哈希冲突的常见方法之一,它在每个桶中使用一个链表来存储所有映射到该桶的键值对。具体插入步骤: +1. 计算键的哈希值并确定其在数组中的位置; +2. 如果桶为空,直接将键值对存储在该桶中; +3. 如果桶不为空,则遍历桶中的链表,检查是否存在相同的键; + - 如果存在相同的键,更新对应的值; + - 如果不存在相同的键,将新的键值对添加到链表的末尾; -设置为0.75的其他解释: -- 根据数学公式推算。这个值在`log(2)`的时候比较合理; -- 为了提升扩容效率,HashMap的容量有一个固定的要求,那就是一定是2的幂。如果负载因子是3/4的话,那么和容量的乘积结果就可以是一个整数; +在Java8及更高版本中,为了提高性能,当单个桶中的链表长度超过一定阈值(默认是 8)时,会将链表转换为红黑树。 +这种方法在大量冲突的情况下提供了更高效的查找和插入性能。具体插入步骤: +1. 计算键的哈希值确定存储位置; +2. 如果存储位置为空,则直接插入新键值对; +3. 如果存储位置已有节点,当链表长度超过阈值(默认为 8),将链表转换为红黑树,否则,继续用链表存储; +4. 如果当前结点为红黑树结点,则根据节点的哈希值和键值比较,找到合适的插入位置;插入后,根据红黑树的性质进行调整,保持平衡; +5. 当桶中元素数量减少到一定阈值以下时(默认是 6),会将红黑树转换回链表,以减少内存开销; -#### 如果指定容量大小为10,那么实际大小是多少 -实际大小是16。其容量为不小于指定容量的2的幂数。 +### HashMap容量 +`HashMap`在创建时需要指定初始容量,如果不指定默认是16。初始容量指的是`HashMap`中桶的数量,即存储键值对的数组大小。 +如果能预估要存储的键值对数量,可以在创建`HashMap`时指定初始容量,以避免频繁的扩容操作。 -**为什么容量始终是2的N次方?** +如果指定容量大小为10,那么实际大小是多少? +先说答案,实际大小是16,其容量为不小于指定容量的2的N次方。 -为了减少Hash碰撞,尽量使Hash算法的结果均匀分布。 +为什么容量始终是2的N次方? +为了减少`Hash`碰撞,尽量使`Hash`算法的结果均匀分布。 -当使用put方法时,到底存入HashMap中的那个数组中?这时是通过hash算法决定的,如果某一个数组中的链表过长旧会影响查询的效率;那么为了避免出现hash碰撞,让hash尽可能的散列分布,就需要在hash算法上做文章。 +当使用`put`方法时,到底存入`HashMap`的哪个数组中? +这是通过`hash`算法决定的,如果某一个数组中的链表过长旧会影响查询的效率。为了避免出现`hash`碰撞,让`hash`尽可能的散列分布,就需要在`hash`算法上做文章。 -JDK1.7通过逻辑与运算,来判断这个元素该进入哪个数组;在下面的代码中length的长度始终为不小于指定容量的2的幂数。 -``` +JDK1.7通过逻辑与运算,判断这个元素该进入哪个数组。在下面的代码中`length`的长度始终为不小于指定容量的2的N次方。 +```java static int indexFor(int h, int length) { return h & (length - 1); } ``` -为了更好的理解举个例子:假设h=2或h=3,length=15,进行与运算,最终逻辑与运算后的结果是一致的,因为最终结果是一致的所以就发生了hash碰撞,这种问题多了以后会造成容器中的元素分布不均匀,都分配在同一个数组上,在查询的时候就减慢了查询的效率,另一方面也造成空间的浪费。 -```` +为了更好的理解这个方法,举个例子,假设`h=2`或`h=3`,`length=15`,执行`indexFor`方法,最终逻辑与运算后的结果是一致的,因为最终结果是一致的所以就导致了`hash`碰撞。 +这种问题多了以后会造成容器中的元素分布不均匀,都分配在同一个数组上,在查询的时候就减慢了查询的效率,另一方面也造成空间的浪费。 +````text -- 2转换为2进制与15-1进行&运算 0000 0010 & 0000 1110 @@ -790,33 +797,30 @@ static int indexFor(int h, int length) { ———————————— 0000 1110 ```` -为了避免上面`length=15`这类问题出现,所以集合的容量采用必须是2的N次幂这种方式,因为2的N次幂的结果减一转换为二进制后都是以`...1111`结尾的,所以在进行逻辑与运算时碰撞几率小。 +为了避免上面`length=15`这类问题出现,所以集合的容量采用必须是2的N次幂这种方式。 +因为2的N次幂的结果减一转换为二进制后都是以`...1111`结尾的,所以在进行逻辑与运算时碰撞几率小。 -在JDK1.8中,在`putVal()`方法中通过`i = (n - 1) & hash`来计算key的散列地址: -``` -final V putVal(int hash, K key, V value, boolean onlyIfAbsent, - boolean evict) { - // 此处省略了代码 - // i = (n - 1) & hash] - if ((p = tab[i = (n - 1) & hash]) == null) - - tab[i] = newNode(hash, key, value, null); +在JDK1.8中,在`putVal`方法中通过`i = (n - 1) & hash`来计算key的散列地址: +```java +final V putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict) { + // 此处省略了代码 + // i = (n - 1) & hash] + if ((p = tab[i = (n - 1) & hash]) == null) - - else { - // 省略了代码 - } + tab[i] = newNode(hash, key, value, null); + + else { + // 省略了代码 + } } ``` -> 这里的 "&" 等同于 %",但是"%"运算的速度并没有"&"的操作速度快;"&"操作能代替"%"运算,必须满足一定的条件,也就是`a%b=a&(b-1)`仅当b是2的n次方的时候方能成立。 - -**容器容量怎么保持始终为2的N次方?** - -`HashMap`的`tableSizeFor()`方法做了处理,能保证n永远都是2次幂。 - -如果用户制定了初始容量,那么HashMap会计算出比该数大的第一个2的幂作为初始容量;另外就是在扩容的时候,也是进行成倍的扩容,即4变成8,8变成16。 +这里的"&"等同于"%",但是"%"运算的速度并没有"&"的操作速度快。 +"&"操作能代替"%"运算,但必须满足一定的条件,即`a%b=a&(b-1)`仅当b是2的n次方的时候才成立。 -``` +容器容量怎么保持始终为2的N次方? +`HashMap`的`tableSizeFor`方法做了处理,能保证永远都是2的N次幂。 +如果用户指定了初始容量,那么`HashMap`会计算出比该数大的第一个2的幂作为初始容量。另外就是在扩容的时候,也是进行成倍的扩容,即4变成8、8变成16。 +```java /** * Returns a power of two size for the given target capacity. */ @@ -847,42 +851,40 @@ static final int tableSizeFor(int cap) { return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1; } ``` -发现上面在进行`>>>`操作时会将cap的二进制值变为最高位后边全是1,`00010001 -> 00011111`这个算法就导致了任意传入一个数值,会将该数字变为它的2倍减1,因为任何尾数全为1的在加1都为2的倍数。 - -至于开头减1,是因为如果给定的n已经是2的次幂,但是不进行减1操作的话,那么得到的值就是大于给定值的最小2的次幂值,例如传入4就会返回8。 +发现上面在进行`>>>`操作时会将`cap`变量的二进制值变为最高位后边全是1,即`00010001 -> 00011111`,所以这个算法就导致了任意传入一个数值,会将该数字变为它的2倍减1,因为任何尾数全为1的在加1都为2的倍数。 +至于开头减1,是因为如果给定的n已经是2的次幂,但是不进行减1操作的话,那么得到的值就是大于给定值的最小2的次幂值,例如,传入4就会返回8。 +这里面最大右移到16位,因为最大值是32个1,这是int类型存储变量的最大值,在往后就没意义了。 -为什么最大右移到16位,因为可以得到的最大值是32个1,这个是int类型存储变量的最大值,在往后就没意义了。 +`HashMap`默认初始化容量为什么是16? +这个问题没有找到相关解释。我推断这应该就是个经验值,既然一定要设置一个默认的`2^n`作为初始值,那么就需要在效率和内存使用上做一个权衡。 +这个值既不能太小,也不能太大,太小了就有可能频繁发生扩容,影响效率;太大了又浪费空间,不划算。所以16就作为一个经验值被采用了。 -#### 默认初始化容量为什么是16 -没有找到相关解释,推断这应该就是个经验值,既然一定要设置一个默认的2^n 作为初始值,那么就需要在效率和内存使用上做一个权衡。这个值既不能太小,也不能太大。太小了就有可能频繁发生扩容,影响效率。太大了又浪费空间,不划算。所以,16就作为一个经验值被采用了。 - -关于默认容量的定义: -``` +关于默认容量的定义,故意把16写成`1 << 4`这种形式,就是提醒开发者,这个地方要是2的次幂,与上面呼应。 +```java /** * The default initial capacity - MUST be a power of two. */ static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16 ``` -故意把16写成`1 << 4`这种形式,就是提醒开发者,这个地方要是2的次幂。 -#### 初始化容量设置多少合适 -当我们使用`HashMap(int initialCapacity)`来初始化容量的时候,`HashMap`并不会使用我们传进来的`initialCapacity`直接作为初始容量。JDK会默认帮我们计算一个相对合理的值当做初始容量。所谓合理值,其实是找到第一个比用户传入的值大的2的幂。 +那么`HashMap`初始化容量设置多少合适? +当我们使用`HashMap(int initialCapacity)`来初始化容量的时候,`HashMap`并不会使用我们传进来的`initialCapacity`直接作为初始容量。 +JDK会默认帮我们计算一个相对合理的值当做初始容量,所谓合理值,其实是找到第一个比用户传入的值大的2的幂。 +如果创建`HashMap`初始化容量设置为7,那么`HashMap`通过计算会创建一个初始化为8的`HashMap`。当`HashMap`中的元素到`0.75 * 8 = 6`就会进行**扩容**,这明显是我们不希望看到的。 +> 负载因子,表示HashMap满的程度,默认值为0.75f,也就是说默认情况下,当HashMap中元素个数达到了容量的3/4的时候就会进行自动扩容; -如果创建hashMap初始化容量设置为7,那么JDK通过计算会创建一个初始化为8的hashMap。当hashMap中的元素到`0.75 * 8 = 6`就会进行扩容,这明显是我们不希望看到的。 - -参考JDK8中`putAll`方法中的实现: -``` +设置多少合适,可以参考JDK8中`putAll`方法中的实现: +```java (int) ((float) expectedSize / 0.75F + 1.0F); ``` -通过`expectedSize / 0.75F + 1.0F`计算,`7/0.75 + 1 = 10` ,10经过JDK处理之后,会被设置成16,这就大大的减少了扩容的几率。 +通过`expectedSize/0.75F+1.0F`计算,将初始化容量设置为7带入,得到`7/0.75+1=10`,10经过JDK处理之后,会被设置成16,这就大大的减少了扩容的几率。 +当我们明确知道`HashMap`中元素的个数的时候,把默认容量设置成`expectedSize/0.75F+1.0F` 是一个在性能上相对好的选择,但同时也会牺牲些内存。 -当我们明确知道HashMap中元素的个数的时候,把默认容量设置成`expectedSize / 0.75F + 1.0F` 是一个在性能上相对好的选择,但是,同时也会牺牲些内存。 - -这个算法在guava中有实现,开发的时候,可以直接通过Maps类创建一个HashMap: -``` +这个算法在`guava`中也有实现,开发的时候,可以直接通过`Maps`类创建一个`HashMap`: +```java Map map = Maps.newHashMapWithExpectedSize(7); ``` -``` +```java public static HashMap newHashMapWithExpectedSize(int expectedSize) { return new HashMap(capacity(expectedSize)); } @@ -897,25 +899,17 @@ static int capacity(int expectedSize) { } ``` -### 扩容 -- JDK1.7: 先扩容在添加元素; -- JDK1.8: 先添加元素在扩容; - -#### 为什么要进行扩容 -随着HashMap中的元素增加,Hash碰撞导致获取元素方法的效率就会越来越低,为了保证获取元素方法的效率,所以针对HashMap中的数组进行扩容。扩容数组的方式只能再去开辟一个新的数组,并把之前的元素转移到新数组上。 +### HashMap扩容 +随着`HashMap`中的元素增加,`Hash`碰撞导致获取元素方法的效率就会越来越低。为了保证获取元素方法的效率,所以针对`HashMap`中的数组进行扩容。 +扩容数组的方式只能再去开辟一个新的数组,并把之前的元素转移到新数组上。 -> PS 如何能避免哈希碰撞? ->- 容量太小。容量小,碰撞的概率就高了。狼多肉少,就会发生争抢。 ->- hash算法不够好。算法不合理,就可能都分到同一个或几个桶中。分配不均,也会发生争抢。 - -#### 什么时机进行扩容 -HashMap的扩容条件就是当HashMap中的元素个数(size)超过临界值(threshold)时就会自动扩容。在HashMap中,`threshold = loadFactor * capacity`。默认情况下负载因子为0.75,理解为当容器中元素到容器的3/4时就会扩容。 -``` +在`HashMap`中有一个概念叫`loadFactor`,即负载因子,它表示`HashMap`满的程度,默认值为`0.75f`,也就是说默认情况下,当`HashMap`中元素个数达到了容量的3/4的时候就会进行自动扩容。 +```java if (++size > threshold) resize(); ``` -HashMap的容量是有上限的,必须小于`1<<30`,即`1073741824`。如果容量超出了这个数,则不再增长,且阈值会被设置为`Integer.MAX_VALUE`: -``` +`HashMap`的容量是有上限的,必须小于`1<<30`,即`1073741824`。如果容量超出了这个数,则不再增长,且阈值会被设置为`Integer.MAX_VALUE`: +```java // Java8 if (oldCap >= MAXIMUM_CAPACITY) { threshold = Integer.MAX_VALUE; @@ -927,9 +921,35 @@ if (oldCapacity == MAXIMUM_CAPACITY) { return; } ``` + +为什么负载因子默认设置为0.75? +```java +/** + * The load factor used when none specified in constructor. + */ +static final float DEFAULT_LOAD_FACTOR = 0.75f; +``` +在[JDK的官方文档](https://docs.oracle.com/javase/6/docs/api/java/util/HashMap.html)中解释如下: +> As a general rule, the default load factor (.75) offers a good tradeoff between time and space costs. +Higher values decrease the space overhead but increase the lookup cost (reflected in most of the operations of the HashMap class, including get and put). +The expected number of entries in the map and its load factor should be taken into account when setting its initial capacity, so as to minimize the number of rehash operations. +If the initial capacity is greater than the maximum number of entries divided by the load factor, no rehash operations will ever occur. + +文档大意:一般来说,默认的负载因子(0.75)在时间和空间成本之间提供了很好的权衡。 +更高的值减少了空间开销,但增加了查找成本(反映在`HashMap`类的大多数操作中,包括`get`和`put`)。 +在设置映射的初始容量时,应该考虑映射中预期的条目数及其负载因子,以便最小化重哈希操作的数量。如果初始容量大于最大条目数除以负载因子,则不会发生重新散列操作。 + +负载因子和`HashMap`中的扩容有关,当`HashMap`中的元素大于临界值(`threshold = loadFactor * capacity`)就会扩容。 +所以负载因子的大小决定了什么时机扩容,扩容又影响到了hash碰撞的频率,所以设置一个合理的负载因子可以有效的避免`hash`碰撞。 + +设置为0.75的其他解释: +- 根据数学公式推算,这个值在`log(2)`的时候比较合理; +- 为了提升扩容效率,`HashMap`的容量有一个固定的要求,那就是一定是2的幂。如果负载因子是3/4的话,那么和容量的乘积结果就可以是一个整数; + #### 1.7扩容 -- `新容量 = 旧容量 * 2` -- `新阈值 = 新容量 * 负载因子` +- JDK1.7: 先扩容在添加元素; +- `新容量 = 旧容量 * 2` +- `新阈值 = 新容量 * 负载因子` ``` void addEntry(int hash, K key, V value, int bucketIndex) { @@ -981,8 +1001,8 @@ void transfer(Entry[] newTable) { } ``` - #### 1.8扩容 +- JDK1.8: 先添加元素在扩容; 容量变为原来的2倍,阈值也变为原来的2倍。容量和阈值都变为原来的2倍时,负载因子还是不变。 在1.8时做了一些优化,文档注释写的很清楚:"元素的位置要么是在原位置,要么是在原位置再移动2次幂的位置"。也就是对比1.7的迁移到新的数组上省去了重新计算hash值的时间。 @@ -1087,4 +1107,7 @@ void transfer(Entry[] newTable) { } return newTab; } -``` \ No newline at end of file +``` + +### 线程安全 + diff --git a/blog-site/public/index.xml b/blog-site/public/index.xml index 6c7520c6..d508959f 100644 --- a/blog-site/public/index.xml +++ b/blog-site/public/index.xml @@ -657,7 +657,7 @@ http://localhost:1313/iblog/posts/java/rookie-java-container/ Mon, 04 Oct 2021 00:00:00 +0000 http://localhost:1313/iblog/posts/java/rookie-java-container/ - 概述 Java中的集合主要包括 Collection 和 Map 两种,Collection 存储着对象的集合,而 Map 存储着键值对(两个对象)的映射表。 如果你看过ArrayList类源码,就知道A + 概述 Java中的集合主要包括Collection和Map两种,Collection存储着对象的集合,而Map存储着键值对的映射表。 数组 如果你看过ArrayLis Java反射 diff --git a/blog-site/public/page/7/index.html b/blog-site/public/page/7/index.html index 80c0c0a2..0b510a44 100644 --- a/blog-site/public/page/7/index.html +++ b/blog-site/public/page/7/index.html @@ -201,7 +201,7 @@

Java集合

-

概述 Java中的集合主要包括 Collection 和 Map 两种,Collection 存储着对象的集合,而 Map 存储着键值对(两个对象)的映射表。 如果你看过ArrayList类源码,就知道A......

+

概述 Java中的集合主要包括Collection和Map两种,Collection存储着对象的集合,而Map存储着键值对的映射表。 数组 如果你看过ArrayLis......

diff --git a/blog-site/public/posts/java/rookie-java-container/index.html b/blog-site/public/posts/java/rookie-java-container/index.html index 564557c5..076e7d6f 100644 --- a/blog-site/public/posts/java/rookie-java-container/index.html +++ b/blog-site/public/posts/java/rookie-java-container/index.html @@ -526,34 +526,6 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -