剖析Java中HashMap数据结构的源码及其性能优化

存储结构
首先,HashMap是基于哈希表存储的。它内部有一个数组,当元素要存储的时候,先计算其key的哈希值,根据哈希值找到元素在数组中对应的下标。如果这个位置没有元素,就直接把当前元素放进去,如果有元素了(这里记为A),就把当前元素链接到元素A的前面,然后把当前元素放入数组中。所以在Hashmap中,数组其实保存的是链表的首节点。下面是百度百科的一张图:

如上图,每个元素是一个Entry对象,在其中保存了元素的key和value,还有一个指针可用于指向下一个对象。所有哈希值相同的key(也就是冲突了)用链表把它们串起来,这是拉链法。

内部变量

//默认初始容量
static final int DEFAULT_INITIAL_CAPACITY = 16;
//最大容量
static final int MAXIMUM_CAPACITY = 1 << 30;
//默认装载因子
static final float DEFAULT_LOAD_FACTOR = 0.75f;
//哈希表
transient Entry<K,V>[] table;
//键值对的数量
transient int size;
//扩容的阈值
int threshold;
//哈希数组的装载因子
final float loadFactor;

在上面的变量中,capacity是指哈希表的长度,也就是table的大小,默认为16。装载因子loadFactor是哈希表的“装满程度”,JDK的文档是这样说的:

The load factor is a measure of how full the hash table is allowed to get before its capacity is automatically increased. When the number of entries in the hash table exceeds the product of the load factor and the current capacity, the hash table is rehashed (that is, internal data structures are rebuilt) so that the hash table has approximately twice the number of buckets.
大体意思是:装载因子是哈希表在扩容之前能装多满的度量值。当哈希表中“键值对”的数量超过当前容量(capacity)和装载因子的乘积后,哈希表重新散列(也就是内部的数据结构重建了),并且哈希表的容量大约变为原来的两倍。

从上面的变量定义可以看出,默认的装载因子DEFAULT_LOAD_FACTOR 是0.75。这个值越大,空间利用率越高,但查询速度(包括get和put)操作会变慢。明白了装载因子之后,threshold也就能理解了,它其实等于容量*装载因子。

构造器

public HashMap(int initialCapacity, float loadFactor) {
  if (initialCapacity < 0)
    throw new IllegalArgumentException("Illegal initial capacity: " +
                      initialCapacity);
  if (initialCapacity > MAXIMUM_CAPACITY)
    initialCapacity = MAXIMUM_CAPACITY;
  if (loadFactor <= 0 || Float.isNaN(loadFactor))
    throw new IllegalArgumentException("Illegal load factor: " +
                      loadFactor);

  // Find a power of 2 >= initialCapacity
  int capacity = 1;
  while (capacity < initialCapacity) //计算出大于指定容量的最小的2的幂
    capacity <<= 1;

  this.loadFactor = loadFactor;
  threshold = (int)Math.min(capacity * loadFactor, MAXIMUM_CAPACITY + 1);
  table = new Entry[capacity];  //给哈希表分配空间
  useAltHashing = sun.misc.VM.isBooted() &&
      (capacity >= Holder.ALTERNATIVE_HASHING_THRESHOLD);
  init();
}

构造器有好几个,最终都会调用上面的这个。它接受两个参数,一个是初始容量,还有一个是装载因子。刚开始先判断值合不合法,有问题的话会抛出异常。重要的是下面的capacity的计算,它的逻辑是计算出大于initialCapacity的最小的2的幂。其实目的就是要让容量能大于等于指定的初始容量,但这个值还得是2的指数倍,这是一个关键的问题。这么做的原因主要是为了哈希值的映射。先来看一下HashMap中有关哈希的方法:

final int hash(Object k) {
  int h = 0;
  if (useAltHashing) {
    if (k instanceof String) {
      return sun.misc.Hashing.stringHash32((String) k);
    }
    h = hashSeed;
  }
  h ^= k.hashCode();
  // This function ensures that hashCodes that differ only by
  // constant multiples at each bit position have a bounded
  // number of collisions (approximately 8 at default load factor).
  h ^= (h >>> 20) ^ (h >>> 12);
  return h ^ (h >>> 7) ^ (h >>> 4);
}
static int indexFor(int h, int length) {
  return h & (length-1);
}

hash()方法重新计算了key的哈希值,用了比较复杂的位运算,具体逻辑我也不清楚,反正肯定是比较好的方法,能减少冲突什么的。

下面的indexFor()是根据哈希值得到元素在哈希表中的下标。一般在哈希表中是用哈希值对表长取模得到。当length(也就是capacity)为2的幂时,h & (length-1)是同样的效果。并且,2的幂一定是偶数,那么减1之后就是奇数,二进制的最后一位一定是1。那么h & (length-1)的最后一位可能是1,也可能是0,可以均匀地散列。如果length是奇数,那么length-1就是偶数,最后一位是0。此时h & (length-1)的最后一位只可能是0,所有得到的下标都是偶数,那么哈希表就浪费了一半的空间。所以HashMap中的容量(capacity)一定要是2的幂。可以看到默认的DEFAULT_INITIAL_CAPACITY=16和MAXIMUM_CAPACITY=1<<30都是这样的。

Entry对象
HashMap中的键值对被封装成Entry对象,这是HashMap中的一个内部类,看一下它的实现:

static class Entry<K,V> implements Map.Entry<K,V> {
  final K key;
  V value;
  Entry<K,V> next;
  int hash;

  Entry(int h, K k, V v, Entry<K,V> n) {
    value = v;
    next = n;
    key = k;
    hash = h;
  }

  public final K getKey() {
    return key;
  }

  public final V getValue() {
    return value;
  }

  public final V setValue(V newValue) {
    V oldValue = value;
    value = newValue;
    return oldValue;
  }

  public final boolean equals(Object o) {
    if (!(o instanceof Map.Entry))
      return false;
    Map.Entry e = (Map.Entry)o;
    Object k1 = getKey();
    Object k2 = e.getKey();
    if (k1 == k2 || (k1 != null && k1.equals(k2))) {
      Object v1 = getValue();
      Object v2 = e.getValue();
      if (v1 == v2 || (v1 != null && v1.equals(v2)))
        return true;
    }
    return false;
  }

  public final int hashCode() {
    return (key==null  ? 0 : key.hashCode()) ^
        (value==null ? 0 : value.hashCode());
  }

  public final String toString() {
    return getKey() + "=" + getValue();
  }
  void recordAccess(HashMap<K,V> m) {
  }
  void recordRemoval(HashMap<K,V> m) {
  }
}

这个类的实现还是简洁易懂的。提供了getKey()、getValue()等方法供调用,判断相等是要求key和value均相等。

put操作
先put了才能get,所以先看一下put()方法:

public V put(K key, V value) {
  if (key == null)
    return putForNullKey(value);
  int hash = hash(key);
  int i = indexFor(hash, table.length);
  for (Entry<K,V> e = table[i]; e != null; e = e.next) {
    Object k;
    if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
      V oldValue = e.value;
      e.value = value;
      e.recordAccess(this);
      return oldValue;
    }
  }

  modCount++;
  addEntry(hash, key, value, i);
  return null;
}

在这个方法中,先判断key是否为null,是的话调用putForNullKey()方法,这说明HashMap允许key为null(其实value可以)。如果不是null,计算哈希值并且得到在表中的下标。然后到对应的链表中查询是否已经存在相同的key,如果已经存在就直接更新值(value)。否则,调用addEntry()方法进行插入。

看一下putForNullKey()方法:

private V putForNullKey(V value) {
  for (Entry<K,V> e = table[0]; e != null; e = e.next) {
    if (e.key == null) {
      V oldValue = e.value;
      e.value = value;
      e.recordAccess(this);
      return oldValue;
    }
  }
  modCount++;
  addEntry(0, null, value, 0);
  return null;
}

可以看到,key为null时直接在下标0处插入,同样是存在就更新值,否则调用addEntry()插入。

下面是addEntry()方法的实现:

void addEntry(int hash, K key, V value, int bucketIndex) {
  if ((size >= threshold) && (null != table[bucketIndex])) {
    resize(2 * table.length);
    hash = (null != key) ? hash(key) : 0;
    bucketIndex = indexFor(hash, table.length);
  }

  createEntry(hash, key, value, bucketIndex);
}
void createEntry(int hash, K key, V value, int bucketIndex) {
  Entry<K,V> e = table[bucketIndex];
  table[bucketIndex] = new Entry<>(hash, key, value, e);
  size++;
}

首先判断是否要扩容(扩容会重新计算下标值,并且复制元素),然后计算数组下标,最后在createEntry()中使用头插法插入元素。

get操作

public V get(Object key) {
  if (key == null)
    return getForNullKey();
  Entry<K,V> entry = getEntry(key);

  return null == entry ? null : entry.getValue();
}
private V getForNullKey() {
  for (Entry<K,V> e = table[0]; e != null; e = e.next) {
    if (e.key == null)
      return e.value;
  }
  return null;
}
final Entry<K,V> getEntry(Object key) {
  int hash = (key == null) ? 0 : hash(key);
  for (Entry<K,V> e = table[indexFor(hash, table.length)];
     e != null;
     e = e.next) {
    Object k;
    if (e.hash == hash &&
      ((k = e.key) == key || (key != null && key.equals(k))))
      return e;
  }
  return null;
}

这个比put()简单一些,同样要判断key是不是null,然后就是链表的遍历查询。

性能优化
HashMap是一个高效通用的数据结构,它在每一个Java程序中都随处可见。先来介绍些基础知识。你可能也知 道,HashMap使用key的hashCode()和equals()方法来将值划分到不同的桶里。桶的数量通常要比map中的记录的数量要稍大,这样 每个桶包括的值会比较少(最好是一个)。当通过key进行查找时,我们可以在常数时间内迅速定位到某个桶(使用hashCode()对桶的数量进行取模) 以及要找的对象。

这些东西你应该都已经知道了。你可能还知道哈希碰撞会对hashMap的性能带来灾难性的影响。如果多个hashCode()的值落到同一个桶内的 时候,这些值是存储到一个链表中的。最坏的情况下,所有的key都映射到同一个桶中,这样hashmap就退化成了一个链表——查找时间从O(1)到 O(n)。我们先来测试下正常情况下hashmap在Java 7和Java 8中的表现。为了能完成控制hashCode()方法的行为,我们定义了如下的一个Key类:

class Key implements Comparable<Key> {
private final int value;
Key(int value) {
this.value = value;
}
@Override
public int compareTo(Key o) {
return Integer.compare(this.value, o.value);
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass())
return false;
Key key = (Key) o;
return value == key.value;
}
@Override
public int hashCode() {
return value;
}
}

Key类的实现中规中矩:它重写了equals()方法并且提供了一个还算过得去的hashCode()方法。为了避免过度的GC,我将不可变的Key对象缓存了起来,而不是每次都重新开始创建一遍:

class Key implements Comparable<Key> {
public class Keys {
public static final int MAX_KEY = 10_000_000;
private static final Key[] KEYS_CACHE = new Key[MAX_KEY];
static {
for (int i = 0; i < MAX_KEY; ++i) {
KEYS_CACHE[i] = new Key(i);
}
}
public static Key of(int value) {
return KEYS_CACHE[value];
}

现在我们可以开始进行测试了。我们的基准测试使用连续的Key值来创建了不同的大小的HashMap(10的乘方,从1到1百万)。在测试中我们还会使用key来进行查找,并测量不同大小的HashMap所花费的时间:

import com.google.caliper.Param;
import com.google.caliper.Runner;
import com.google.caliper.SimpleBenchmark;
public class MapBenchmark extends SimpleBenchmark {
private HashMap<Key, Integer> map;
@Param
private int mapSize;
@Override
protected void setUp() throws Exception {
map = new HashMap<>(mapSize);
for (int i = 0; i < mapSize; ++i) {
map.put(Keys.of(i), i);
}
}
public void timeMapGet(int reps) {
for (int i = 0; i < reps; i++) {
map.get(Keys.of(i % mapSize));
}
}
}

有意思的是这个简单的HashMap.get()里面,Java 8比Java 7要快20%。整体的性能也相当不错:尽管HashMap里有一百万条记录,单个查询也只花了不到10纳秒,也就是大概我机器上的大概20个CPU周期。 相当令人震撼!不过这并不是我们想要测量的目标。

假设有一个很差劲的key,他总是返回同一个值。这是最糟糕的场景了,这种情况完全就不应该使用HashMap:

class Key implements Comparable<Key> {
//...
@Override
public int hashCode() {
return 0;
}
}

Java 7的结果是预料中的。随着HashMap的大小的增长,get()方法的开销也越来越大。由于所有的记录都在同一个桶里的超长链表内,平均查询一条记录就需要遍历一半的列表。因此从图上可以看到,它的时间复杂度是O(n)。

不过Java 8的表现要好许多!它是一个log的曲线,因此它的性能要好上好几个数量级。尽管有严重的哈希碰撞,已是最坏的情况了,但这个同样的基准测试在JDK8中的时间复杂度是O(logn)。单独来看JDK 8的曲线的话会更清楚,这是一个对数线性分布:

为什么会有这么大的性能提升,尽管这里用的是大O符号(大O描述的是渐近上界)?其实这个优化在JEP-180中已经提到了。如果某个桶中的记录过 大的话(当前是TREEIFY_THRESHOLD = 8),HashMap会动态的使用一个专门的treemap实现来替换掉它。这样做的结果会更好,是O(logn),而不是糟糕的O(n)。它是如何工作 的?前面产生冲突的那些KEY对应的记录只是简单的追加到一个链表后面,这些记录只能通过遍历来进行查找。但是超过这个阈值后HashMap开始将列表升 级成一个二叉树,使用哈希值作为树的分支变量,如果两个哈希值不等,但指向同一个桶的话,较大的那个会插入到右子树里。如果哈希值相等,HashMap希 望key值最好是实现了Comparable接口的,这样它可以按照顺序来进行插入。这对HashMap的key来说并不是必须的,不过如果实现了当然最 好。如果没有实现这个接口,在出现严重的哈希碰撞的时候,你就并别指望能获得性能提升了。

这个性能提升有什么用处?比方说恶意的程序,如果它知道我们用的是哈希算法,它可能会发送大量的请求,导致产生严重的哈希碰撞。然后不停的访问这些 key就能显著的影响服务器的性能,这样就形成了一次拒绝服务攻击(DoS)。JDK 8中从O(n)到O(logn)的飞跃,可以有效地防止类似的攻击,同时也让HashMap性能的可预测性稍微增强了一些。我希望这个提升能最终说服你的 老大同意升级到JDK 8来。

测试使用的环境是:Intel Core i7-3635QM @ 2.4 GHz,8GB内存,SSD硬盘,使用默认的JVM参数,运行在64位的Windows 8.1系统 上。

总结
HashMap的基本实现就如上面所分析的,最后做下总结:

  • HashMap内部用Entry对象保存键值对,基于哈希表存储,用拉链法解决冲突。
  • HashMap的默认容量大小为16,默认装载因子为0.75。可以指定容量大小,容量最终一定会被设置为2的幂,这是为了均匀地散列。
  • HashMap的key和value都可以是null,当然只能有一个key是null,value可以有多个。
  • HashMap的键值对数量超过容量*装载因子时会扩容,扩容后的容量大约是原来的两倍。扩容会重新散列,所以元素的位置可能发生会变化,而且这是一个耗时操作。
  • HashMap是线程不安全的。
(0)

相关推荐

  • Java中String性能优化

    不用使用String的构造函数,可能的话直接使用字符串. 两个特例: 1)想把char []转换为一个String, 2) 使用一个大的String对象的substring()方法: String.equals() 比 String.equalsIgnoreCase()要快: 尽量使用StringBuilder来构造一个String,而不是"+"操作符和String.concat() (除非是一个表达式,String s = a + b + c): StringBuilder是不同步的

  • Java性能优化技巧汇总

    本文实例汇总了Java性能优化技巧.分享给大家供大家参考.具体分析如下: 这里参考了些书籍,网络资源整理出来,适合于大多数Java应用 在JAVA程序中,性能问题的大部分原因并不在于JAVA语言,而是程序本身.养成良好的编码习惯非常重要,能够显著地提升程序性能. 1.尽量使用final修饰符. 带有final修饰符的类是不可派生的.在JAVA核心API中,有许多应用final的例子,例如java.lang.String.为String类指定final防止了使用者覆盖length()方法.另外,如

  • Java代码性能优化的35个方法总结

    代码优化,一个很重要的课题.可能有些人觉得没用,一些细小的地方有什么好修改的,改与不改对于代码的运行效率有什么影响呢?这个问题我是这么考虑的,就像大海里面的鲸鱼一样,它吃一条小虾米有用吗?没用,但是,吃的小虾米一多之后,鲸鱼就被喂饱了.代码优化也是一样,如果项目着眼于尽快无BUG上线,那么此时可以抓大放小,代码的细节可以不精打细磨:但是如果有足够的时间开发.维护代码,这时候就必须考虑每个可以优化的细节了,一个一个细小的优化点累积起来,对于代码的运行效率绝对是有提升的. 代码优化细节如下: 1.尽

  • Java虚拟机JVM性能优化(二):编译器

    本文将是JVM 性能优化系列的第二篇文章(第一篇:传送门),Java 编译器将是本文讨论的核心内容. 本文中,作者(Eva Andreasson)首先介绍了不同种类的编译器,并对客户端编译,服务器端编译器和多层编译的运行性能进行了对比.然后,在文章的最后介绍了几种常见的JVM优化方法,如死代码消除,代码嵌入以及循环体优化. Java最引以为豪的特性"平台独立性"正是源于Java编译器.软件开发人员尽其所能写出最好的java应用程序,紧接着后台运行的编译器产生高效的基于目标平台的可执行代

  • Java编程代码性能优化

    一.咱们之所以这么干的目的: 1.效率(最重要) 2.可读性,便于后期维护.(同样很重要) 二.代码优化的要求: 1.减小代码的体积. 2.提高代码的运行效率. 三.常用的代码的优化: 1.尽量重用对象 : 特别是String对象的重用.最常用的就是字符串的拼接: 当遇到频繁擦拼接String时.记住一定用StringBuilder/StringBuffer 例如: ArrayList<String> list; //省去list初始化. StringBuilder builder = new

  • Java虚拟机JVM性能优化(三):垃圾收集详解

    Java平台的垃圾收集机制显著提高了开发者的效率,但是一个实现糟糕的垃圾收集器可能过多地消耗应用程序的资源.在Java虚拟机性能优化系列的第三部分,Eva Andreasson向Java初学者介绍了Java平台的内存模型和垃圾收集机制.她解释了为什么碎片化(而不是垃圾收集)是Java应用程序性能的主要问题所在,以及为什么分代垃圾收集和压缩是目前处理Java应用程序碎片化的主要办法(但不是最有新意的). 垃圾收集(GC)的目的是释放那些不再被任何活动对象引用的Java对象所占用的内存,它是Java

  • Java虚拟机JVM性能优化(一):JVM知识总结

    Java应用程序是运行在JVM上的,但是你对JVM技术了解吗?这篇文章(这个系列的第一部分)讲述了经典Java虚拟机是怎么样工作的,例如:Java一次编写的利弊,跨平台引擎,垃圾回收基础知识,经典的GC算法和编译优化.之后的文章会讲JVM性能优化,包括最新的JVM设计--支持当今高并发Java应用的性能和扩展. 如果你是一个开发人员,你肯定遇到过这样的特殊感觉,你突然灵光一现,所有的思路连接起来了,你能以一个新的视角来回想起你以前的想法.我个人很喜欢学习新知识带来的这种感觉.我已经有过很多次这样

  • Java中性能优化的35种方法汇总

    前言 对程序员们来说,代码优化是一个很重要的课题.可能有些人觉得没用,一些细小的地方有什么好修改的,改与不改对于代码的运行效率有什么影响呢?这个问题我是这么考虑的,就像大海里面的鲸鱼一样,它吃一条小虾米有用吗?没用,但是,吃的小虾米一多之后,鲸鱼就被喂饱了.代码优化也是一样,如果项目着眼于尽快无BUG上线,那么此时可以抓大放小,代码的细节可以不精打细磨:但是如果有足够的时间开发.维护代码,这时候就必须考虑每个可以优化的细节了,一个一个细小的优化点累积起来,对于代码的运行效率绝对是有提升的.

  • Android性能优化之利用Rxlifecycle解决RxJava内存泄漏详解

    前言: 其实RxJava引起的内存泄漏是我无意中发现了,本来是想了解Retrofit与RxJava相结合中是如何通过适配器模式解决的,结果却发现了RxJava是会引起内存泄漏的,所有想着查找一下资料学习一下如何解决RxJava引起的内存泄漏,就查到了利用Rxlifecycle开源框架可以解决,今天周末就来学习一下如何使用Rxlifecycle. 引用泄漏的背景: RxJava作为一种响应式编程框架,是目前编程界网红,可谓是家喻户晓,其简洁的编码风格.易用易读的链式方法调用.强大的异步支持等使得R

  • 剖析Java中HashMap数据结构的源码及其性能优化

    存储结构 首先,HashMap是基于哈希表存储的.它内部有一个数组,当元素要存储的时候,先计算其key的哈希值,根据哈希值找到元素在数组中对应的下标.如果这个位置没有元素,就直接把当前元素放进去,如果有元素了(这里记为A),就把当前元素链接到元素A的前面,然后把当前元素放入数组中.所以在Hashmap中,数组其实保存的是链表的首节点.下面是百度百科的一张图: 如上图,每个元素是一个Entry对象,在其中保存了元素的key和value,还有一个指针可用于指向下一个对象.所有哈希值相同的key(也就

  • Java中ArrayList类的源码解析

    前言:在前面我们提到数据结构的线性表.那么今天我们详细看下Java源码是如何实现线性表的,这一篇主要讲解顺序表ArrayList链式表下一篇在提及. 1:ArrayList结构图 2:关于Collection和List的区别 最好的比对就是查看他们的源码我们先看Collection的所有接口 public interface Collection<E> extends Iterable<E> { int size(); boolean contains(Object o); Ite

  • java中break和continue源码解析

    在自己学习java语言的过程中,很容易把break和continue的用法混淆.为了便于以后快速查阅及温习,在此特留学习笔记一份. 简述 在任何迭代语句的主体部分,都可以用break和continue控制循环的流程.其中,break用于强行退出循环,不执行循环中剩余的语句.而continue则停止执行当前迭代,然后退回循环起始处,开始下一次迭代. 源码 下面这个程序向大家展示了break和continue在for和while循环中的例子: package com.mufeng.thefourth

  • Java同步锁Synchronized底层源码和原理剖析(推荐)

    目录 1 synchronized场景回顾 2 反汇编寻找锁实现原理 3 synchronized虚拟机源码 3.1 HotSpot源码Monitor生成 3.2 HotSpot源码之Monitor竞争 3.3 HotSpot源码之Monitor等待 3.4 HotSpot源码之Monitor释放 1 synchronized场景回顾 目标:synchronized回顾(锁分类–>多线程)概念synchronized:是Java中的关键字,是一种同步锁.Java中锁分为以下几种:乐观锁.悲观锁(

  • Java并发系列之ConcurrentHashMap源码分析

    我们知道哈希表是一种非常高效的数据结构,设计优良的哈希函数可以使其上的增删改查操作达到O(1)级别.Java为我们提供了一个现成的哈希结构,那就是HashMap类,在前面的文章中我曾经介绍过HashMap类,知道它的所有方法都未进行同步,因此在多线程环境中是不安全的.为此,Java为我们提供了另外一个HashTable类,它对于多线程同步的处理非常简单粗暴,那就是在HashMap的基础上对其所有方法都使用synchronized关键字进行加锁.这种方法虽然简单,但导致了一个问题,那就是在同一时间

  • java 中HashMap、HashSet、TreeMap、TreeSet判断元素相同的几种方法比较

    java 中HashMap.HashSet.TreeMap.TreeSet判断元素相同的几种方法比较 1.1     HashMap 先来看一下HashMap里面是怎么存放元素的.Map里面存放的每一个元素都是key-value这样的键值对,而且都是通过put方法进行添加的,而且相同的key在Map中只会有一个与之关联的value存在.put方法在Map中的定义如下. V put(K key, V value); 它用来存放key-value这样的一个键值对,返回值是key在Map中存放的旧va

  • 详谈HashMap和ConcurrentHashMap的区别(HashMap的底层源码)

    HashMap本质是数组加链表,根据key取得hash值,然后计算出数组下标,如果多个key对应到同一个下标,就用链表串起来,新插入的在前面. ConcurrentHashMap在HashMap的基础上将数据分为多个segment,默认16个,然后每次操作对一个segment加锁,避免多线程锁的几率,提高并发效率. 1. HashMap的数据结构 HashMap底层就是一个数组结构,数组中存放的是一个Entry对象,如果产生的hash冲突,这时候该位置存储的就是一个链表了. HashMap中En

  • 浅谈Java中常用数据结构的实现类 Collection和Map

    线性表,链表,哈希表是常用的数据结构,在进行Java开发时,JDK已经为我们提供了一系列相应的类来实现基本的数据结构.这些类均在java.util包中.本文试图通过简单的描述,向读者阐述各个类的作用以及如何正确使用这些类. Collection ├List │├LinkedList │├ArrayList │└Vector │ └Stack └Set Map ├Hashtable ├HashMap └WeakHashMap Collection接口 Collection是最基本的集合接口,一个C

  • 深入剖析java中String、StringBuffer、StringBuilder的区别

    java中String.StringBuffer.StringBuilder是编程中经常使用的字符串类,他们之间的区别也是经常在面试中会问到的问题.现在总结一下,看看他们的不同与相同. 1. 可变与不可变 String类中使用字符数组保存字符串,如下就是,因为有"final"修饰符,所以可以知道string对象是不可变的. private final char value[]; StringBuilder与StringBuffer都继承自AbstractStringBuilder类,在

  • Java并发系列之ReentrantLock源码分析

    在Java5.0之前,协调对共享对象的访问可以使用的机制只有synchronized和volatile.我们知道synchronized关键字实现了内置锁,而volatile关键字保证了多线程的内存可见性.在大多数情况下,这些机制都能很好地完成工作,但却无法实现一些更高级的功能,例如,无法中断一个正在等待获取锁的线程,无法实现限定时间的获取锁机制,无法实现非阻塞结构的加锁规则等.而这些更灵活的加锁机制通常都能够提供更好的活跃性或性能.因此,在Java5.0中增加了一种新的机制:Reentrant

随机推荐