java容器专题整理,源码分析,面试可用

容器专题整理

以下基于jdk1.8版本

单列集合

image-20220407112751564

双列集合

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-6fDBZYxY-1649905516457)(C:\Users\Acerola\AppData\Roaming\Typora\typora-user-images\image-20220407112813373.png)]

ArrayList

ArrayList底层就是数组,重点聊一下扩容机制

ArrayList扩容机制

当创建ArrayList对象时,如果使用的是无参构造器,则初始elementData容量为0,第1次添加,则扩容elementData为10,如需要再次扩容,则扩容elementData为1.5倍。

LinkedList

LinkedList的基本构成

LinkedList本质是一个双向链表,由一个个的Node对象组成,如下图:

img

LinkedList由一个个Node组成,每一个Node持有前后节点的引用,也可以称之为指针,看下Node的结构,Node有当前元素对象、前一个节点信息、后一个节点信息三个属性,构造函数也是由这三个属性去组成,在LinkedList的添加方法中,会创建一个Node对象,持有前后节点的信息,添加到最后节点之后。

/关于Node构造函数
private static class Node<E> {
    E item;
    Node<E> next;
    Node<E> prev;
 
    Node(Node<E> prev, E element, Node<E> next) {
        this.item = element;
        this.next = next;
        this.prev = prev;
    }
}

先看下LinkedList中共的几个属性,下面三个属性分别描述LinkedList的尺寸、第一个元素、最后一个元素,这三个元素始终贯穿着在LinkedList的使用当中

//transient 保证以下几个属性不被序列化
/**
 * The number of times this list has been <i>structurally modified</i>.
 * 该字段表示list结构上被修改的次数。结构上的修改指的是那些改变了list的长度
 * 大小或者使得遍历过程中产生不正确的结果的其它方式。
 * 
 */
protected transient int modCount = 0;
 
transient int size = 0;
/**
 * Pointer to first node.
 * Invariant: (first == null && last == null) ||
 *            (first.prev == null && first.item != null)
 */
transient Node<E> first;
/**
 * Pointer to last node.
 * Invariant: (first == null && last == null) ||
 *            (last.next == null && last.item != null)
 */
transient Node<E> last;

LinkedList的添加实现

再看LinkedList的添加实现,可以添加为null的对象

public boolean add(E e) {
    linkLast(e);
    return true;
}
/**
 * Links e as last element.
 */
void linkLast(E e) {
     final Node<E> l = last;
    final Node<E> newNode = new Node<>(l, e, null);
    last = newNode;
    if (l == null)
        first = newNode;
    else
        l.next = newNode;
    size++;
    modCount++;
}

看上面的源码可以知道,当LinkedList添加一个元素时,会默认的往LinkedList最后一个节点后添加,具体步骤为

  • 获得最后一个节点last作为当前节点l
  • 用当前节点l、添加参数e、null创建一个新的Node对象
  • 将新创建的Node对象链接到最后节点,也就是last
  • 如果当前的LinkedList为空,那么添加的node就是first,也是last
  • 当前LinkedList的size+1,表示结构改变次数的对象modCount+1

整个添加过程中,系统只做了两件事情,添加一个新的节点,然后保存该节点和前节点的引用关系。

LinkedList的删除实现

public boolean remove(Object o) {
    if (o == null) {
        for (Node<E> x = first; x != null; x = x.next) {
            if (x.item == null) {
                unlink(x);
                return true;
            }
        }
    } else {
        for (Node<E> x = first; x != null; x = x.next) {
            if (o.equals(x.item)) {
                unlink(x);
                return true;
            }
        }
    }
    return false;
}

LinkedList的删除中,会根据传递进来的参数进行null判断,因为在LinkedList的元素移除中,null和非null的处理不一样,对于nul使用去判断是否匹配,对于非null使用.equals(Object o)去判断,至于和equals的区别,读者自行百度。

null判断之后,传入当前节点调用unlink(E e)方法,我们可以从方法名中看出点东西,可能设计者也是为了方便读者阅读源码,可以理解为“放开链接”,也可以反映出LinkedList保存数据的方式,一个个元素相互链接而成。

E unlink(Node<E> x) {
    // assert x != null;
    final E element = x.item;
    final Node<E> next = x.next;
    final Node<E> prev = x.prev;
 
    if (prev == null) {
        first = next;
    } else {
        prev.next = next;
        x.prev = null;
    }
 
    if (next == null) {
        last = prev;
    } else {
        next.prev = prev; 
        x.next = null;
    }
 
    x.item = null;
    size--;
    modCount++;
    return element;
}

通过阅读上面的源码,LinkedList的删除中,也是改变节点之间的引用关系去实现的,具体逻辑整理如下:

  • 如果前一个节点prev为null,即第一个节点元素,则链表first = x下一个节点;
  • 如果前一个节点prev不为null,即不是第一个节点元素,则将当前节点的next赋值给prev.next,x.prev置为null,也就是当前节点x的prev和next和当前链表中的其他元素不存在任何联系;
  • 如果下一个节点next为null,即为最后一个元素,则链表last = x前一个节点;
  • 如果下一个节点next不为null,即不为最后一个元素,则将当前节点的prev赋值给next.prev,同样当前节点x的prev和next和当前链表中的其他元素不存在任何联系;

LinkedList的查询实现源码如下

public E get(int index) {
    checkElementIndex(index);
    return node(index).item;
}
private void checkElementIndex(int index) {
    if (!isElementIndex(index))
        throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
}
private boolean isElementIndex(int index) {
    return index >= 0 && index < size;
}
Node<E> node(int index) {
    // assert isElementIndex(index);
 
    if (index < (size >> 1)) {
        Node<E> x = first;
        for (int i = 0; i < index; i++)
            x = x.next;
        return x;
    } else {
        Node<E> x = last;
        for (int i = size - 1; i > index; i--)
            x = x.prev;
        return x;
    }
}

通过阅读源码,我们也可以解读出LinkedList的查询逻辑

  • 根据传入的index去判断是否为LinkedList中的元素,判断逻辑为index是否在0和size之间,如果在则调用node(index)方法,否则抛出IndexOutOfBoundsException;
  • 调用node(index)方法,将size右移1位,即size/2,判断传入的size在LinkedList的前半部分还是后半部分
    • 如果在前半部分,即index < size/2,则从fisrt节点开始遍历匹配
    • 如果在后半部分,即index > size/2,则从last节点开始遍历匹配

可以看出,如果LinkedList链表size越大,则遍历的时间越长,查询所需的时间也越长。

ArrayList和LinkedList特征总结

  • ArrayList:长于随机访问元素,中间插入和移除元素比较慢,在插入时,必须创建空间并将它的所有引用向前移动,这会随着ArrayList的尺寸增加而产生高昂的代价,底层由数组支持。
  • LinkedList:通过代价较低的在List中间进行插入和删除操作,只需要链接新的元素,而不必修改列表中剩余的元素,无论列表尺寸如何变化,其代价大致相同,提供了优化的顺序访问,随机访问相对较慢,特性较ArrayList更大,而且还添加了可以使其作为栈、队列或双端队列的方法,底层由双向链表实现。

Set

HashSet

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-4d1H39rR-1649905516459)(C:\Users\Acerola\AppData\Roaming\Typora\typora-user-images\image-20220410161833079.png)]

HashSet特点

  • 不能保证元素的顺序,元素是无序的
  • HashSet是不同步的,需要外部保持线程之间的同步问题,Collections.synchronizedSet(new XXSet());
  • 集合元素值允许为null
  • 不允许重复的元素
  • 没有索引,不能使用普通for循环遍历
  • 底层为Hash表

HashSet基本构成

HashSet是使用HashMap实现的,构造方法如下:

//无参构造方法,完成map的创建
public HashSet() {
    map = new HashMap<>();
}
//指定集合转化为HashSet, 完成map的创建
public HashSet(Collection<? extends E> c) {
   map = new HashMap<>(Math.max((int) (c.size()/.75f) + 1, 16));
   addAll(c);
}
//指定初始化大小,和负载因子
public HashSet(int initialCapacity, float loadFactor) {
    map = new HashMap<>(initialCapacity, loadFactor);
}
//指定初始化大小
public HashSet(int initialCapacity) {
    map = new HashMap<>(initialCapacity);
}
//指定初始化大小和负载因子,dummy 无实际意义
HashSet(int initialCapacity, float loadFactor, boolean dummy) {
    map = new LinkedHashMap<>(initialCapacity, loadFactor);
}

Hash表的结构为:数组加链表

在这里插入图片描述

HashSet的添加实现与去重

HashSet的Add()使用HashMap的put()方法,通过debug来看add是怎么操作的

  1. PRESENT为HashSet类中定义的一个使用static final修饰的常量,其实无实际意义,而传入的属性作为key存储

    public boolean add(E e) {
        return map.put(e, PRESENT)==null;
    }
    
  2. put()方法中返回的是putVal()方法

public V put(K key, V value) {
    return putVal(hash(key), key, value, false, true);
}
  1. 先看hash()方法

    static final int hash(Object key) {
            int h;
            return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
        }
    

    key一定不为null,所以返回的是(h = key.hashCode()) ^ (h >>> 16),

  2. 重点看看hashcode()方法,hashcode()方法对于不同的数据类型有多种实现

    整型:

    Integer num = 123;
    System.out.println(num.hashCode()); //123
    
    public static int hashCode(int value) { 
        return value;
    }
    

    布尔型:

    Boolean ff = false;
    System.out.println(ff.hashCode()); //1237
    
    public static int hashCode(boolean value) {
        return value ? 1231 : 1237;
    }
    

    引用类型(String):

    String s1 = "abc";
    System.out.println(s1.hashCode());     //96354
    
    public int hashCode() {
        int h = hash;     //default is 0
            if (h == 0 && value.length > 0) {
                char val[] = value;
                for (int i = 0; i < value.length; i++) {
                     h = 31 * h + val[i];
                } 
                hash = h;
            } 
        return h;
    }
    

    引用类型计算过程:

    //分别先取出a、b、c对应的ascii码值对应val[]的值
    System.out.println((int)'a');    //97
    System.out.println((int)'b');    //98
    System.out.println((int)'c');    //99
     
    //字符串abc长度为3,循环次数3次,h参数对应hash根据源码查看默认为0
    //第一次循环:h = 31 * h + val[i];
    31 * 0 + 97 = 97
    //第二次循环:h = 31 * h + val[i];
    31 * 97 + 98 = 3105
    //第三次循环:h = 31 * h + val[i];
    31 * 3105 + 99 = 96354
    

    以String为例,由上可见,实际上是把数据存入数组后再遍历数组,并通过算法生成hashcode再返回,但这种算法会导致内容属性相同的变量或对象,其hashcode值相等,无法被add重复添加,因此, 假如两个对象属性相同,就无法添加,很显然不应该这样处理,所以add方法还有额外的判断机制

  3. putVal()方法

     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;
            if ((p = tab[i = (n - 1) & hash]) == null)
                tab[i] = newNode(hash, key, value, null);
            else {
                Node<K,V> e; K k;
                if (p.hash == hash &&
                    ((k = p.key) == key || (key != null && key.equals(k))))
                    e = p;
                else if (p instanceof TreeNode)
                    e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
                else {
                    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;
        }
    

    resize()方法:

       final Node<K,V>[] resize() {
            Node<K,V>[] oldTab = table;
            int oldCap = (oldTab == null) ? 0 : oldTab.length;
            int oldThr = threshold;
            int newCap, newThr = 0;
            if (oldCap > 0) {
                if (oldCap >= MAXIMUM_CAPACITY) {
                    threshold = Integer.MAX_VALUE;
                    return oldTab;
                }
                else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
                         oldCap >= DEFAULT_INITIAL_CAPACITY)
                    newThr = oldThr << 1; // double threshold
            }
            else if (oldThr > 0) // initial capacity was placed in threshold
                newCap = oldThr;
            else {               // zero initial threshold signifies using defaults
                newCap = DEFAULT_INITIAL_CAPACITY;
                newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
            }
            if (newThr == 0) {
                float ft = (float)newCap * loadFactor;
                newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
                          (int)ft : Integer.MAX_VALUE);
            }
            threshold = newThr;
            @SuppressWarnings({"rawtypes","unchecked"})
            Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
            table = newTab;
            if (oldTab != null) {
                for (int j = 0; j < oldCap; ++j) {
                    Node<K,V> e;
                    if ((e = oldTab[j]) != null) {
                        oldTab[j] = null;
                        if (e.next == null)
                            newTab[e.hash & (newCap - 1)] = e;
                        else if (e instanceof TreeNode)
                            ((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
                        else { // preserve order
                            Node<K,V> loHead = null, loTail = null;
                            Node<K,V> hiHead = null, hiTail = null;
                            Node<K,V> next;
                            do {
                                next = e.next;
                                if ((e.hash & oldCap) == 0) {
                                    if (loTail == null)
                                        loHead = e;
                                    else
                                        loTail.next = e;
                                    loTail = e;
                                }
                                else {
                                    if (hiTail == null)
                                        hiHead = e;
                                    else
                                        hiTail.next = e;
                                    hiTail = e;
                                }
                            } while ((e = next) != null);
                            if (loTail != null) {
                                loTail.next = null;
                                newTab[j] = loHead;
                            }
                            if (hiTail != null) {
                                hiTail.next = null;
                                newTab[j + oldCap] = hiHead;
                            }
                        }
                    }
                }
            }
            return newTab;
        }
    

    我们假设有一段代码为set.add(“Tom”),那么这段代码的执行过程为: 调用add方法→调用map.put方法→调用putVal方法→table原来并不存在,被创建后由于是引用类型,初始化为null→符合if条件,执行下面代码→resize方法中创建了newTab,实际上是一个Node对象数组并且让table与其相等→返回newTab数组并且tab与其相等(所以tab和table指向同一个数组) 长度为16→通过hash方法给元素取hash值,再通过tab[i = (n - 1) & hash]确定if语句判断是否为空的位置,这里Tom给出的i,在tab数组中之前并不存在这个位置→tab[i] = newNode(hash, key, value, null)给tab上这个位置赋值,由于tab和table指向同一地址,因此table其实也被改变了→返回null使得put方法也返回null,最终使add方法返回true→元素添加成功。

    但如果此时我们再次输入set.add(“Tom”)并且执行,过程就不一样了,在进入到putVal方法后,由于table已经存在,直接到了
    if ((p = tab[i = (n - 1) & hash]) == null) tab[i] = newNode(hash, key, value, null)→p其实就是之前执行putVal方法时最后得出的tab[i](Node对象),因为刚刚的元素就是"Tom",所以p不为null,走else路线→if (p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k)))),这里与刚刚的原因是相同的,都是Tom,所以hash相同,且key相等,于是e=p,并且直接到

    if (e != null) { // existing mapping for key
    V oldValue = e.value;
    if (!onlyIfAbsent || oldValue == null)
    e.value = value;

    由于e已经有值,老value被e.value替换,并且不为null,所以e.value被新value覆盖→返回被更新的e.value→最终add方法返回false,添加失败。

    我们注意到,虽然第二个Tom覆盖了第一次添加的object常量,但实际上Node对象还是原来的,换句话说,虽然我们进行了本来应该导致改变的“覆盖”行为,但最终因为用于覆盖和被覆盖的是同样的对象,最终的结果是覆盖“未成功”(没有实际的变化)。

    因此,我们总结的话,add方法的执行是如下原理:

    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;
            //如果table数组尚未创建,则新建table数组
        if ((p = tab[i = (n - 1) & hash]) == null)
            tab[i] = newNode(hash, key, value, null);
            //如果table[i]中没有数据则新建
            //tab与table所指向的地址相同,并且table[i]也是有值的,并且与tab[i]相同
        else {
            Node<K,V> e; K k;
            if (p.hash == hash &&
                ((k = p.key) == key || (key != null && key.equals(k))))
                e = p;
                //如果p=table[i]的关键字与给定关键字key相同,则替换旧值
            else if (p instanceof TreeNode)
                e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
                //判断p的类型,如果为TreeNode(Node的一个子类),就插入TreeNode节点
            else {
                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;
                    //将p更新
     
                }
            }
            if (e != null) { // existing mapping for key
                V oldValue = e.value;
                //如果存在这个映射就覆盖原有的
                if (!onlyIfAbsent || oldValue == null)
                    e.value = value;
                    //判断是否允许覆盖,以及映射是否为空,将新值赋值
                afterNodeAccess(e);
                return oldValue;
            }
        }
        ++modCount;  //map结构的修改次数加1
        if (++size > threshold)
            resize();
        afterNodeInsertion(evict);
        //超过阙值后进行扩容
        return null;
    }
    
  4. 还嫌听不懂看不懂,还可以再进一步总结

    • 先获取元素得哈希值(hashcode()方法)
    • 对哈希值进行运算,得出一个索引值,可以理解为在哈希表中的位置号
    • 如果该位置上没有其他元素,则直接存放,如果该位置有其他元素,进行equals()比较,如果相等,不再添加,如果不相等,则以链表的方式添加(底层为数组加链表的结构)

HashSet不存入重复元素的规则:使用hashcode和equals。 那么HashSet是如何检查重复?其实原理:HashSet会通过元素的hashcode()和equals()方法进行判断,当试图将元素加入到Set集合中,HashSet首先会使用对象的hashcode来判断对象加入的位置。同时也会与其他已经加入的对象的hashcode进行比较,如果没有相等的hashcode,HashSet就认为这个对象之前不存在,如果之前存在同样的hashcode值,就会进一步的比较equals()方法,如果equals()比较返回结果是true,那么认为该对象在集合中的对象是一模一样的,不会将其加入;如果比较返回的是false,那么HashSet认为新加入的对象没有重复,可以正确加入。 当两个对象的hashcode不一样时,说明两个对象是一定不相等的,当两个对象的hashcode相等,但是equals()不相等,在实际中,会在同一个位置,用链式结构来保存多个对象,而HashSet访问集合元素时也是根据元素的hashCode值快速定位,如果HashSet中两个以上的元素具有相同的hashCode值,将会导致性能下降。

hash算法的功能是能保证快速查找被检索的对象,hash算法的价值在于速度,当需要查询集合中某个元素时,hash算法可以直接根据该元素的hashCode值计算出该元素的存储位置,从而快速定位该元素。

HashSet的扩容机制

HashSet的底层为数组+链表(+红黑树),HashSet第一次添加时,将数组扩容到16,触发扩容的临界值为16*加载因子,加载因子为默认值0.75,扩容则是每次将数组大小*2,新的临界值就是32 *0.75,以此类推,

在java8中,如果一条链表的元素个数到达TREEIRFY_THRESHOLD(默认为8),并且table的大小>=MIN_TREEIFY_CAPACITY(默认为64),就会进行树化(红黑树),否则仍会采用数组的扩容机制

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-j0udvncs-1649905516460)(C:\Users\Acerola\AppData\Roaming\Typora\typora-user-images\image-20220411103623397.png)]

LinkedHashSet

LinkedHashSet特点

  • LinkedHashSet是HashSet的子类
  • LinkedHashSet底层是一个LinkedHashMap,底层维护了一个数组+双向链表
  • LinkedHashSet根据元素的hashcode值来决定元素的存储位置,同时使用链表维护元素的次序,这使得元素看起来是以插入顺序保存的
  • LinkedHashSet不允许添加重复的元素

LinkedHashSet基本构成

LinkedHashSet使用LinkedHashMap来实现:

public boolean add(E e) {
     return map.put(e, PRESENT)==null;
}

LinkedHashSet没有太多需要解释的,相比于HashSet(Hashset是数组+链表)多维护了一个双向链表,使用before和after用来连接数组元素,实现了有序

LinkedHashSet对比HastSet

  • LinkedHashSet 的插入性能可能略低于HashSet,因为它需要维护链表的顺序

  • LinkedHashSet 的迭代性能应该是略高于HashSet的,因为它只需要按照链表的顺序进行迭代即可,也就是只考虑元素的多少,而不用考虑容量的大小

TreeSet

TreeSet特点

  • 和HashSet类似
  • 可排序
  • TreeSet的去重是使用compareto方法,String默认就实现了Comparator,不多赘述
  • 可以通过自定义比较器(new Comparator)实现排序和去重

Map

Hashmap

Map接口特点

  • 无序,因为底层是hashmap,不多赘述
  • key不重复,相同key会覆盖value
  • key可以为null,但只能有一个,value都可以为null

Map遍历方法

 HashMap hashMap = new HashMap();
        hashMap.put("1", "a");
        hashMap.put("2", "b");
        hashMap.put("3", "c");
        hashMap.put("4", "d");
        hashMap.put(null, "e");
        hashMap.put("6", null);
  1. 取出所有key,通过key取出对应的value

    • 增强for循环

      Set set = hashMap.keySet();
              for (Object o : set) {
                  System.out.println(o+"-"+hashMap.get(o));
              }
      

      输出:

      null-e
      1-a
      2-b
      3-c
      4-d
      6-null
      
    • 迭代器

      Set set = hashMap.keySet();
      Iterator iterator = set.iterator();
              while (iterator.hasNext()) {
                  Object next = iterator.next();
                  System.out.println(next + "-" + hashMap.get(next));
              }
      
  2. 取出所有value

    • 增强for循环

              Collection values = hashMap.values();
              for (Object value : values) {
                  System.out.println(value);
              }
      
    • 迭代器

      Collection values = hashMap.values();
              Iterator iterator = values.iterator();
              while (iterator.hasNext()) {
                  Object next = iterator.next();
                  System.out.println(next);
              }
      
  3. entryset获取key-value

    • 增强for循环

      Set set = hashMap.entrySet();
          for (Object entry : set) {
           //将entry转换为Map.Entry
           Map.Entry m = (Map.Entry) entry;
           System.out.println(m.getKey() + "-" + m.getValue());
         }
      

      输出:

      null-e
      1-a
      2-b
      3-c
      4-d
      6-null
      
    • 迭代器

      Set set = hashMap.entrySet();   
      Iterator iterator = set.iterator();
              while (iterator.hasNext()) {
                  Object next = iterator.next();
                  //System.out.println(next.getClass()); Node没有提供getKey和getValue方法
                  Map.Entry m = (Map.Entry) next;
                  System.out.println(m.getKey() + "-" + m.getValue());
              }
      

Hashtable

Hashtable特点

  • 存放的元素是键值
  • hashtable的键值都不能为null,否则会抛出NullPointerException
  • hashtable使用方法基本上和hashmap一样
  • hashtable是线程安全的(synchronize),hashmap是线程不安全的

Hashtable基本构成

HashtableExercise.java

public class HashtableExercise {
    public static void main(String[] args) {
        Hashtable hashtable = new Hashtable();
        hashtable.put("1","a");
        hashtable.put(null,"b");
        hashtable.put("3",null);
        hashtable.put("4","d");
        hashtable.put("5","e");
    }
}
  • 底层构成是数组,Hashtable$Entry[] 初始化大小为11
  • 扩容的临界值为11*0.75=8,加载因子为0.75

Hashtable的扩容机制

  • 底层构成是数组,Hashtable$Entry[] 初始化大小为11

  • 扩容的临界值为11*0.75=8,加载因子为0.75

  • 扩容方法为rehash(),扩容大小为2n+1

        protected void rehash() {
            int oldCapacity = table.length;
            Entry<?,?>[] oldMap = table;
    
            // overflow-conscious code
            int newCapacity = (oldCapacity << 1) + 1; //扩容,左移1位+1,即两倍+1
            if (newCapacity - MAX_ARRAY_SIZE > 0) {
                if (oldCapacity == MAX_ARRAY_SIZE)
                    // Keep running with MAX_ARRAY_SIZE buckets
                    return;
                newCapacity = MAX_ARRAY_SIZE;
            }
            Entry<?,?>[] newMap = new Entry<?,?>[newCapacity];
    
            modCount++;
            threshold = (int)Math.min(newCapacity * loadFactor, MAX_ARRAY_SIZE + 1);
            table = newMap;
    
            for (int i = oldCapacity ; i-- > 0 ;) {
                for (Entry<K,V> old = (Entry<K,V>)oldMap[i] ; old != null ; ) {
                    Entry<K,V> e = old;
                    old = old.next;
    
                    int index = (e.hash & 0x7FFFFFFF) % newCapacity;
                    e.next = (Entry<K,V>)newMap[index];
                    newMap[index] = e;
                }
            }
        }
    

Hashtable添加实现

    private void addEntry(int hash, K key, V value, int index) {
        modCount++;

        Entry<?,?> tab[] = table;
        if (count >= threshold) {
            // Rehash the table if the threshold is exceeded
            rehash();

            tab = table;
            hash = key.hashCode();
            index = (hash & 0x7FFFFFFF) % tab.length;
        }

        // Creates the new entry.
        @SuppressWarnings("unchecked")
        Entry<K,V> e = (Entry<K,V>) tab[index];
        tab[index] = new Entry<>(hash, key, value, e);
        count++;
    }

Properties

Properties特点

略,用来读取配置文件,不多说,实用意义不大,现在已经完全被springboot替代了

  • Properties类继承自Hashtable类并且实现了Map接口,也是使用一种键值对的形式来保存数据。

  • 使用特点和Hashtable类似

  1. Properties还可以用于从xxxx.properties文件中,加载数据到Properties类对象,
    并进行读取和修改

ble添加实现

    private void addEntry(int hash, K key, V value, int index) {
        modCount++;

        Entry<?,?> tab[] = table;
        if (count >= threshold) {
            // Rehash the table if the threshold is exceeded
            rehash();

            tab = table;
            hash = key.hashCode();
            index = (hash & 0x7FFFFFFF) % tab.length;
        }

        // Creates the new entry.
        @SuppressWarnings("unchecked")
        Entry<K,V> e = (Entry<K,V>) tab[index];
        tab[index] = new Entry<>(hash, key, value, e);
        count++;
    }

Properties

Properties特点

略,用来读取配置文件,不多说,实用意义不大,现在已经完全被springboot替代了

  • Properties类继承自Hashtable类并且实现了Map接口,也是使用一种键值对的形式来保存数据。

  • 使用特点和Hashtable类似

  1. Properties还可以用于从xxxx.properties文件中,加载数据到Properties类对象,
    并进行读取和修改
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Acerola-

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值