Java『数据容器』

HashMap

先从Java中Object的hashCode()方法说起,从方法注释的第一行可以看到该方法的存在主要是为了支持HashMap:

//Returns a hash code value for the object. This method is supported for the benefit of hash tables such as those provided by HashMap.
public int hashCode() {
    return identityHashCode(this);
}
  • 结构:HashMap的设计初衷是为了在有限的容器内放置若干范围很大的数据。JDK1.8之前底层使用的是数组+链表,1.8之后当链表长度大于阈值(默认为8)时,会将链表转换成红黑树,以减少搜索时间;HashMap中有一个hash方法用来得到元素Key的hashCode值,这个hash方法是对Object的HashCode再做了一次优化,为了减少发生哈希碰撞的次数。
  • 问题
    • 为什么HashMap的长度是2的幂次方?
      为了更高效,因为如果除数是2的幂次方,那么取余操作等同于右移一位(hash % length) == (hash & (length -1))
  • 注意
    • 循环引用,当在多线程环境下,对HashMap进行put操作,可能会出发扩容;扩容意味着数组元素及元素下挂载的链表的复制,所以可能在扩容是会发生循环引用。
  • equals和hashCode

Hashtable

  • 结构:Hashtable是基于数组+链表(和JDK 1.8之前的HashMap一样),继承自Dictionary(一个比较陈旧的类)Hashtable是线程安全的,其内部方法通过使用synchronized修饰来实现线程安全,(相应的效率会低一些,如果需要使用线程安全的哈希表,可以使用ConcurrentHashMap);
  • 注意:影响Hashtable性能的有两个参数:初始容量加载因子。Hashtable默认的初始大小为11,扩容方式为2n+1,而HashMap的扩容方式为扩充为2的冥次方。另外,Hashtable中key和null都不能为null
ConcurrentHashMap
  • 结构:JDK 1.8之前采用分段的数组+链表实现,JDK1.8开始数据结构和JDK 1.8下的HashMap 一样,采用数组+链表/红黑树
  • 注意:在JDK 1.8之前,对整个桶进行分割分段(Segment),每一把锁只锁定其中一部分数据,多线程访问容器中不同数据段的数据时就不存在锁竞争了。在JDK 1.8中摒弃了Segment的概念,而是使用JDK 1.8 HashMap的数据结构配合Synchronized和CAS来操作,看起来就像是优化过且线程安全的HashMap
LinkedHashMap
  • 结构:继承自HashMap,底层数据基本和HashMap一样,只是增加了一条双向链表,使之可以保持键值对的插入顺序。
TreeMap
  • 结构:红黑树

HashSet

  • 结构:HashSet是基于HashMap实现的,可以看做一个所有Value都是null的HashMap。所以HashSet中不能存储相同的对象(即HashMap中的Key)
  • 注意:当尝试把一个对象add到HashSet时,首先会在HashSet中寻找和该对象的hashCode一样的元素,如果存在,则会比较这两个元素是否equals,如果equals
LinkedHashSet
  • 结构:继承自HashSet,不过其内部是通过LinkedHashMap来实现的。
TreeSet
  • 结构:红黑树,有序,唯一。
ArrayList
  • 结构:ArrayList是基于数组实现,非线程安全,因为基于是基于数组实现的,所以检索时间复杂度为O(1),增删时间复杂度为O(n)
  • 扩容
  • 注意事项:
    • 删除元素时,索引移动
LinkedList
  • 结构:LinkedList是基于双向链表实现的(JDK 1.7之前是循环列表),非线程安全,因为是基于链表实现的,所以检索时间复杂度为O(n),增删时间复杂度为O(1)
Vector
  • 结构:Vector和ArrayList很相似,也是基于数组实现的,不同的是它是线程安全的;当容量不足时,会进行扩容(基于扩容系数,如果系数为空,则将容量增加一倍),扩容会将数组中的元素拷贝到新的数组中去;Vector实现了RandomAccess接口,支持随机访问。

RandomAccess

RandomAccess是一个空的标记接口,用来标记对应的数据容器是否支持随机访问。ArrayList、Vector实现了RandomAccess接口,而LinkedList没有实现该接口;举例:在Arrays的binarySearch方法中,会判断数据容器是否被RandomAccess接口标记,进而选择相应的检索逻辑。

public static <T>
    int binarySearch(List<? extends Comparable<? super T>> list, T key) {
        if (list instanceof RandomAccess || list.size()<BINARYSEARCH_THRESHOLD)
            return Collections.indexedBinarySearch(list, key);
        else
   		    return Collections.iteratorBinarySearch(list, key);
}
equals方法和hashCode方法

hashCode方法用于得到一个对象的整型类型的hashCode值。equals**通常用来比较两个对象的值**是否相等。查看Object类中的equals方法的默认实现是这样的:

 public boolean equals(Object obj) {
        return (this == obj);
    }

从这里看出默认不重写equals方法的情况下,调用equals是等同于==的,然而很多类都重写了equals方法,使之用于比较对象上的值是否相等,如字符串类String中的equals:

    public boolean equals(Object anObject) {
        if (this == anObject) {
            return true;
        }
        if (anObject instanceof String) {
            String anotherString = (String)anObject;
            int n = length();
            if (n == anotherString.length()) {
                int i = 0;
                while (n-- != 0) {
                    if (charAt(i) != anotherString.charAt(i))
                            return false;
                    i++;
                }
                return true;
            }
        }
        return false;
    }

我们知道一个HashMap中不能存在两个相同的key。这里的相同的key,指的是hash相等,且key的值相等(==或者equals)。
**为什么重写equals方法必须重写hashCode方法?**可以看hashCode方法注释中三点限制:

  1. 同一个对象上调用多次hashCode必须输出相同的整型数据。
  2. 两个对象如果equals,那么他们必须有相同的hashCode。
  3. 有相同的hashCode的2个对象,不一定equals。
    由第2点可以得出结论,如果重写了equals,必须重写hashCode;因为euqals方法中设计的字段必须得参与到hashCode的计算中去,才能保证第2点能够正确。
//HashMap.hash()
static final int hash(Object key) {
    int h;
    return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
  /**
     * Implements Map.put and related methods
     *
     * @param hash hash for key
     * @param key the key
     * @param value the value to put
     * @param onlyIfAbsent if true, don't change existing value
     * @param evict if false, the table is in creation mode.
     * @return previous value, or null if none
     */
    final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
                   boolean evict) {
        Node<K,V>[] tab; Node<K,V> p; int n, i;
        if ((tab = table) == null || (n = tab.length) == 0)
            n = (tab = resize()).length;
        //1. 首先使用(n-1) & hash得到key对应HashMap桶上的位置(即数组的索引位置),如果该位置为空,则直接插入新Node。
        if ((p = tab[i = (n - 1) & hash]) == null)
            tab[i] = newNode(hash, key, value, null);
        else { //2. 数组索引位置上已存在对象
            Node<K,V> e; K k;
            //2.1 如果该位置上对象的hash和A的hash相等,且key的引用相等或值相等
            if (p.hash == hash &&
                ((k = p.key) == key || (key != null && key.equals(k))))
                e = p;
            //2.2 如果桶上找到了hash相等的对象,但是他们的key不相等,那么判断节点是红黑树节点还是链表节点
            else if (p instanceof TreeNode) 
                //2.3 是红黑树节点,调用putTreeVal(),这个暂不分析
                e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
            else {
                //2.4 是链表节点,那么久使用Node.next遍历链表,直至遍历到末尾插入新Node;或者找到一个key相等的,替换之
                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;
    }
HashMap的putVal方法

设有对象A要添加到HashMap中去,putVal方法回返回插入对象的旧值(return previous value, or null if none),过程如下:

  1. 首先使用**(n-1) & hash得到key对应HashMap数组的索引位置,如果该位置为空,则直接插入新Node。过程结束**,返回null
  2. 如果1中数组索引位置上已存在对象,那么
    1. 如果该位置上对象的hash和A的hash相等,且key的引用相等或值相等,直接替换(即在数组上已经找到了hash和key相同的元素,不需要在从链表或红黑树中查找元素)
    2. 如果桶上找到了hash相等的对象,但是他们的key不相等,那么需要判断节点是红黑树节点还是链表节点来选择不同的put方式。
    3. 是红黑树节点,调用putTreeVal(),这个本段落暂不分析
    4. 是链表节点,那么久使用Node.next遍历链表,直至遍历到末尾插入新Node;或者找到一个key相等的,替换之
      注意此时,记录了遍历的节点数,当**遍历次数**大于等于阈值(TREEIFY_THRESHOLD,默认是8),会进行树化操作(treeifyBin())。

转载请注明出处:https://blog.csdn.net/u011638883/article/details/89461627

参考【https://github.com/Snailclimb/JavaGuide

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值