Java--集合框架之Map接口

Java集合框架总图
在这里插入图片描述
Map接口和具体实现类
在这里插入图片描述
Java集合总体分为两个根接口,Map和Collection,其中Collection是单列集合,Map是双列集合。
Map与List、Set接口不同,它并不继承自Collection,它是由一系列键值对组成的集合,提供了key到value的映射。在Map中一个key对应一个value,所以key的存储不可重复,但value可以。

哈希结构——通过关键码找到值的数据结构
哈希函数——建立关键字和值的映射关系
注:好的哈希函数能使值均匀的分步在哈希结构中

生成哈希函数的两种方式
(1)直接寻址法:f(x)=kx+b(k和b是常数)
(2)除留余数法:f(x)=x mod m (m<p)(m小于哈希长度p)
哈希冲突的两种解决方式
(1)链地址法:当发生哈希冲突时,将哈希到对应位置的值连接在该位置的数据后面。
(2)线性探测法
Map接口常用方法

int size();          //map集合中存储的键值对的个数
boolean isEmpty();   //判断map集合是否为空 true:空 false:不为空
boolean containsKey(Object key)  //判断集合中是否存在该键key
boolean containsValue(Object value); 判断集合中是否存在该值value
V get(Object key)  //通过键获取对应的值
V put(K key, V value);  //添加键值对
V remove(Object key);  //删除操作 通过键删除键值对

常用三种遍历方式

        System.out.println("通过键值对遍历");
        //1、通过键值对遍历
        Iterator <Map.Entry <String, String>> iterator = hashMap.entrySet().iterator();
        while (iterator.hasNext()) {
            Map.Entry <String, String> entry = iterator.next();
            String key = entry.getKey();
            String value = entry.getValue();
            System.out.println(key+":"+value);
        }
        System.out.println();
        System.out.println("通过键遍历");
        //通过键遍历
        Iterator <String> iterator1 = hashMap.keySet().iterator();
        while (iterator1.hasNext()) {
            System.out.println(iterator1.next());
        }
        System.out.println();
        System.out.println("通过值遍历");
        //通过值遍历
        Iterator <String> iterator2 = hashMap.values().iterator();
        while (iterator2.hasNext()) {
            System.out.println(iterator2.next());
        }

一、HashMap
HashMap底层是一个Entry数组,Entry是HashMap的基本组成单元,每一个Entry包含一个key-value键值对。当发生哈希冲突时,HashMap采用链表的方式来解决,在对应的数组位置存放链表的头结点。对链表而言,新加入的节点会从头结点加入。
1、继承关系

public class HashMap<K,V> extends AbstractMap<K,V>
implements Map<K,V>, Cloneable, Serializable

2、属性分析

//默认的初始容量是16
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4;

//最大容量
static final int MAXIMUM_CAPACITY = 1 << 30;

//默认的填充因子
static final float DEFAULT_LOAD_FACTOR = 0.75f;

//当桶(bucket)上的结点数大于这个值时会转成红黑树
static final int TREEIFY_THRESHOLD = 8;

//当桶(bucket)上的结点数小于这个值时树转链表
static final int UNTREEIFY_THRESHOLD = 6;

//桶中结构转化为红黑树对应的table的最小大小
static final int MIN_TREEIFY_CAPACITY = 64;

//存储元素的数组,总是2的幂次倍
transient Node<k,v>[] table;

//存放具体元素的集
transient Set<map.entry<k,v>> entrySet;

//存放元素的个数,注意这个不等于数组的长度。
transient int size;

//每次扩容和更改map结构的计数器
transient int modCount;

//临界值
//实际大小(容量*填充因子)超过临界值时,会进行扩容
//threshold = capacity * loadFactor,
//当Size>=threshold的时候,就要考虑对数组扩容
//即这是衡量数组是否需要扩容的一个标准
int threshold;

//加载因子
//用来控制数组存放数据的疏密程度
//loadFactor越趋近于1,数组中存放的数据(entry)就越多,也就越密,会让链表的长度增加
//loadFactor越小,趋近于0,数组中存放的数据(entry)也就越少,就越稀疏
//loadFactor太大导致查找元素效率低
//loadFactor太小导致数组的利用率低,存放的数据会很分散
//loadFactor的默认值为0.75f是官方给出的一个比较好的临界值
//给定的默认容量为16,负载因子为 0.75
//Map在使用过程中不断的往里面存放数据,当数量达到了16*0.75=12
//就需要将当前16的容量进行扩容
//扩容过程涉及到rehash、复制数据等操作,非常消耗性能
final float loadFactor;

HashMap内部定义了一个hash表数组,元素通过哈希转换函数将哈希地址转换成数组中存放的索引,如果有冲突,则使用散列链表的形式将所有相同哈希地址的元素串起来,通过查看HashMap.Entry的源码可以看出是一个单链表结构:

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

        Node(int hash, K key, V value, Node<K,V> next) {
            this.hash = hash;
            this.key = key;
            this.value = value;
            this.next = next;
        }

        public final K getKey()        { return key; }
        public final V getValue()      { return value; }
        public final String toString() { return key + "=" + value; }

        public final int hashCode() {
            return Objects.hashCode(key) ^ Objects.hashCode(value);
        }

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

        public final boolean equals(Object o) {
            if (o == this)
                return true;
            if (o instanceof Map.Entry) {
                Map.Entry<?,?> e = (Map.Entry<?,?>)o;
                if (Objects.equals(key, e.getKey()) &&
                    Objects.equals(value, e.getValue()))
                    return true;
            }
            return false;
        }
    }

3、构造函数

//如果指定了加载因子和初始容量,就调用这个构造方法
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;  
          //确保容量为2的n次幂,使capacity为大于initialCapacity的最小的2的n次幂
         while (capacity < initialCapacity)  
             capacity <<= 1;
 
         this.loadFactor = loadFactor;
         threshold = (int)(capacity * loadFactor);
         table = new Entry[capacity];
        init();
    }
 
     public HashMap(int initialCapacity) {
         this(initialCapacity, DEFAULT_LOAD_FACTOR);
    }
 
     public HashMap() {
         this.loadFactor = DEFAULT_LOAD_FACTOR;
         threshold = (int)(DEFAULT_INITIAL_CAPACITY * DEFAULT_LOAD_FACTOR);
         table = new Entry[DEFAULT_INITIAL_CAPACITY];
        init();
     }

4、常用方法
(1)put()
key不可重复但可以为空,value可重复可为空,插入无序。

public V put(K key, V value) {
        //如果table数组为空数组,进行数组填充(为table分配实际内存空间),入参为threshold,此时threshold为initialCapacity 默认是1<<4
        if (table == EMPTY_TABLE) {
            inflateTable(threshold);
        }
       //如果key为null,存储位置为table[0]或table[0]的冲突链上
        if (key == null)
            return putForNullKey(value);
        int hash = hash(key);//对key的hashcode进一步计算,确保散列均匀
        int i = indexFor(hash, table.length);//获取在table中的实际位置
        for (Entry<K,V> e = table[i]; e != null; e = e.next) {
        //如果该对应数据已存在,执行覆盖操作。用新value替换旧value,并返回旧value
            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++;//保证并发访问时,若HashMap内部结构发生变化,快速响应失败
        addEntry(hash, key, value, i);//新增一个entry
        return null;
    }

注:
①inflateTable方法
用于为主干数组table在内存中分配存储空间,通过roundUpToPowerOf2(toSize)可以确保capacity为大于或等于toSize的最接近toSize的二次幂,比如toSize=13,则capacity=16;to_size=16,capacity=16;to_size=17,capacity=32。

private void inflateTable(int toSize) {
        int capacity = roundUpToPowerOf2(toSize);//capacity一定是2的次幂
        threshold = (int) Math.min(capacity * loadFactor, MAXIMUM_CAPACITY + 1);//此处为threshold赋值,取capacity*loadFactor和MAXIMUM_CAPACITY+1的最小值,capaticy一定不会超过MAXIMUM_CAPACITY,除非loadFactor大于1
        table = new Entry[capacity];
        initHashSeedAsNeeded(capacity);
    }

②roundUpToPowerOf2方法
使数组长度一定为2的次幂,Integer.highestOneBit用来获取最左边的bit(其他bit位为0)所代表的数值。

private static int roundUpToPowerOf2(int number) {
        // assert number >= 0 : "number must be non-negative";
        return number >= MAXIMUM_CAPACITY
                ? MAXIMUM_CAPACITY
                : (number > 1) ? Integer.highestOneBit((number - 1) << 1) : 1;
    }

③hash函数
用异或、移位等运算,对key的hashcode进一步计算以及并调整二进制位,保证最终获取的存储位置尽量分布均匀

final int hash(Object k) {
        int h = hashSeed;
        if (0 != h && k instanceof String) {
            return sun.misc.Hashing.stringHash32((String) k);
        }

        h ^= k.hashCode();

        h ^= (h >>> 20) ^ (h >>> 12);
        return h ^ (h >>> 7) ^ (h >>> 4);
    }
 static int indexFor(int h, int length) {
       //h&(length-1)保证获取的index一定在数组范围内
        return h & (length-1);
    }

以上两步获取到当前元素要存储的哈希表的索引位置。

拓展:不同场景下的put
hash (通过 key 计算出的 int 值)
key (传入的 key)
value (传入的 value)

场景1:存储时 HashMap的 Node数组未初始化。
初始化数组,直接放入。

场景2:当前坐标下不存在值。
直接存放到0。

场景3:当前坐标下只有一个值并且存储的 key相等。
将旧值覆盖。

场景4:当前坐标下只有一个值并且存储的 key不相等。
将新值存到下标为0的next属性处,形成链表结构。

场景5:当前坐标下有多数值(小于8)且其中有一个值和新传入的 key相同。
替换旧值,并复制旧值的next属性给新值。

场景6:当前坐标下有多数值(小于8)且 key 都不相同。
直接接到最后面,若连完大于8则转为红黑树。

场景7:当前坐标下有多数值(大于等于8(链表长度大于等于8的时候,当前数组长度大于64时才会转换成红黑树)),数据结构已经变为红黑树了。
(2)resize()

void resize(int newCapacity) {
        Entry[] oldTable = table;
        int oldCapacity = oldTable.length;
        if (oldCapacity == MAXIMUM_CAPACITY) {
            threshold = Integer.MAX_VALUE;
            return;
        }

        Entry[] newTable = new Entry[newCapacity];
        //↓hashMap线程不安全,容易形成死循环
        transfer(newTable, initHashSeedAsNeeded(newCapacity));
        table = newTable;
        threshold = (int)Math.min(newCapacity * loadFactor, MAXIMUM_CAPACITY + 1);
    }
void transfer(Entry[] newTable, boolean rehash) {
        int newCapacity = newTable.length;
     //for循环中的代码,逐个遍历链表,重新计算索引位置,将老数组数据复制到新数组中去(数组不存储实际数据,所以仅仅是拷贝引用而已)
        for (Entry<K,V> e : table) {
            while(null != e) {
                Entry<K,V> next = e.next;
                if (rehash) {
                    e.hash = null == e.key ? 0 : hash(e.key);
                }
                int i = indexFor(e.hash, newCapacity);
          //将当前entry的next链指向新的索引位置,newTable[i]有可能为空,有可能也是个entry链,如果是entry链,直接在链表头部插入。
                e.next = newTable[i];
                newTable[i] = e;
                e = next;
            }
        }
    }

(3)get()

public V get(Object key) {
     //如果key为null,则直接去table[0]处去检索即可。
        if (key == null)
            return getForNullKey();
        Entry<K,V> entry = getEntry(key);
        return null == entry ? null : entry.getValue();
 }
final Entry<K,V> getEntry(Object key) {
            
        if (size == 0) {
            return null;
        }
        //通过key的hashcode值计算hash值
        int hash = (key == null) ? 0 : hash(key);
        //indexFor (hash&length-1) 获取最终数组索引,然后遍历链表,通过equals方法比对找出对应记录
        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;
    }

操作过程:通过key算出存的位置然后查询。
(4)remove()

public V remove(Object key) {
    //临时变量
    Node<K,V> e;
    /**调用removeNode(hash(key), key, null, false, true)进行删除,第三个value为null,表示,把key的节点直接都删除了,不需要用到值,如果设为值,则还需要去进行查找操作**/
    return (e = removeNode(hash(key), key, null, false, true)) == null ?
        null : e.value;
}

/**第一参数为哈希值,第二个为key,第三个value,第四个为是为true的话,则表示删除它key对应的value,不删除key,第四个如果为false,则表示删除后,不移动节点**/
final Node<K,V> removeNode(int hash, Object key, Object value,
                           boolean matchValue, boolean movable) {
    //tab 哈希数组,p 数组下标的节点,n 长度,index 当前数组下标
    Node<K,V>[] tab; Node<K,V> p; int n, index;
    //哈希数组不为null,且长度大于0,然后获得到要删除key的节点所在是数组下标位置
    if ((tab = table) != null && (n = tab.length) > 0 &&
        (p = tab[index = (n - 1) & hash]) != null) {
        //nodee 存储要删除的节点,e 临时变量,k 当前节点的key,v 当前节点的value
        Node<K,V> node = null, e; K k; V v;
        //如果数组下标的节点正好是要删除的节点,把值赋给临时变量node
        if (p.hash == hash &&
            ((k = p.key) == key || (key != null && key.equals(k))))
            node = p;
        //也就是要删除的节点,在链表或者红黑树上,先判断是否为红黑树的节点
        else if ((e = p.next) != null) {
            if (p instanceof TreeNode)
                //遍历红黑树,找到该节点并返回
                node = ((TreeNode<K,V>)p).getTreeNode(hash, key);
            else { //表示为链表节点,一样的遍历找到该节点
                do {
                    if (e.hash == hash &&
                        ((k = e.key) == key ||
                         (key != null && key.equals(k)))) {
                        node = e;
                        break;
                    }
                    /**注意,如果进入了链表中的遍历,那么此处的p不再是数组下标的节点,而是要删除结点的上一个结点**/
                    p = e;
                } while ((e = e.next) != null);
            }
        }
        //找到要删除的节点后,判断!matchValue,我们正常的remove删除,!matchValue都为true
        if (node != null && (!matchValue || (v = node.value) == value ||
                             (value != null && value.equals(v)))) {
            //如果删除的节点是红黑树结构,则去红黑树中删除
            if (node instanceof TreeNode)
                ((TreeNode<K,V>)node).removeTreeNode(this, tab, movable);
            //如果是链表结构,且删除的节点为数组下标节点,也就是头结点,直接让下一个作为头
            else if (node == p)
                tab[index] = node.next;
            else /**为链表结构,删除的节点在链表中,把要删除的下一个结点设为上一个结点的下一个节点**/
                p.next = node.next;
            //修改计数器
            ++modCount;
            //长度减一
            --size;
            /**此方法在hashMap中是为了让子类去实现,主要是对删除结点后的链表关系进行处理**/
            afterNodeRemoval(node);
            //返回删除的节点
            return node;
        }
    }
    //返回null则表示没有该节点,删除失败
    return null;
}

(5)元素修改
也是使用put()方法,因为key是唯一的,所以修改元素,可以通过传入新值将旧值覆盖。
5、应用
用HashMap统计第一个出现重复的数据和重复次数最多的数据。

import java.util.*;

public class CountNumber {

    public static void main(String[] args) {
        // 初始化数据
        int cap = 100;
        ArrayList<Integer> arrayList = new ArrayList<Integer>(cap);
        Random random = new Random();
        for (int i = 0; i < cap; i++) {
            arrayList.add(random.nextInt(101)); //(0,1000)
        }

        //统计出现的次数
        //用key存arrayList数据,用value存出现次数
        HashMap<Integer, Integer> hashMap = new HashMap<Integer, Integer>();
        Iterator<Integer> iterator = arrayList.iterator();
        while (iterator.hasNext()) {
            Integer value = iterator.next();
            if (hashMap.containsKey(value)) {
                hashMap.put(value, hashMap.get(value) + 1);
            } else {
                hashMap.put(value, 1);
            }
        }

        //打印
        Iterator<Map.Entry<Integer, Integer>> iterator1 = hashMap.entrySet().iterator();
        while (iterator1.hasNext()) {
            Map.Entry<Integer, Integer> entry = iterator1.next();
            Integer key = entry.getKey();
            Integer value = entry.getValue();
            System.out.println(key + ":" + value);
        }


        //寻找第一个重复的数据
        Iterator<Map.Entry<Integer, Integer>> iterator2 =hashMap.entrySet().iterator();
            for (int i = 0; i < hashMap.size(); i++) {
                Map.Entry<Integer, Integer> entry = iterator2.next();
                if (entry.getValue() > 1) {
                    System.out.println("第一个重复的数据是" + entry.getKey() + ",重复了" + entry.getValue() + "次");
                }
                break;
            }


        //找出最大重复次数对应的数据
        Iterator<Map.Entry<Integer, Integer>> iterator3 = hashMap.entrySet().iterator();
        int key = 0;
        int max = 0;
        while (iterator3.hasNext()) {

            Map.Entry<Integer, Integer> entry = iterator3.next();

//            if (key == 0) {
//                key = entry.getKey();
//                max = entry.getValue();
//            } else {
                if (entry.getValue() > max) {
                    key = entry.getKey();
                    max = entry.getValue();
                }
            }
//        }
        System.out.println(key + "重复最多,重复了" + max + "次");
    }
}

6、HashMap的优点
(1)HashMap采用了数组和链表的数据结构,能在查询和修改方便继承了数组的线性查找和链表的寻址修改。
(2)HashMap可以接受键和值null,而Hashtable则不能(原因就是equlas()方法需要对象,因为HashMap是后出的API经过处理才可以)
7、HashMap的工作原理
(1)将键值对传递给put()方法时调用键对象的hashCode()方法来计算hashcode,找到bucket位置来储存值对象。
(2)获取对象时,通过键对象的equals()方法找到正确的键值对,然后返回值对象。HashMap使用链表来解决碰撞问题,当发生碰撞了,对象将会储存在链表的下一个节点中。 HashMap在每个链表节点中储存键值对对象。
二、Hashtable
Hashtable是一个散列表,存储键值对(key-value)映射,且映射不是有序的。Hashtable函数是同步的,所以它是线程安全的。
1、继承关系

    extends Dictionary<K,V>
    implements Map<K,V>, Cloneable, java.io.Serializable

2、属性

//table:Entry[]数组类型
//每一个Entry代表了一个键值对,哈希表的key-value键值对都存储在Entry数组中
private transient HashtableEntry table[]; 

//Hashtable所包含Entry键值对的数量
private transient int count;    

//阈值,用于判断是否需要调整Hashtable的容量
//阈值=容量×加载因子
//初始容量和加载因子影响其性能。
//容量是哈希表中桶的数量,初始容量是哈希表创建时的容量
private int threshold;         
 
//加载因子
//衡量哈希表在其容量自动增加之前可以达到多满的尺度
//默认0.75,加载因子如果过高虽然减少了空间开销,但同时也增加了查找某个条目的时间
private float loadFactor;  

注:Entry数组里的属性有hash、prev、next、value,该Entry数组采用哈希表的存储结构,采用链地址法解决哈希冲突。
3、构造函数

public Hashtable(int initialCapacity, float loadFactor) {
        if (initialCapacity < 0)
            throw new IllegalArgumentException("Illegal Capacity: "+
                                               initialCapacity);
        if (loadFactor <= 0 || Float.isNaN(loadFactor))
            throw new IllegalArgumentException("Illegal Load: "+loadFactor);

        if (initialCapacity==0)
            initialCapacity = 1;
        this.loadFactor = loadFactor;
       
        table = new Entry[initialCapacity];
        threshold = (int)Math.min(initialCapacity * loadFactor, MAX_ARRAY_SIZE + 1);
        initHashSeedAsNeeded(initialCapacity);
    }

    /**
     * Constructs a new, empty hashtable with the specified initial capacity
     * and default load factor (0.75).
     *
     * @param     initialCapacity   the initial capacity of the hashtable.
     * @exception IllegalArgumentException if the initial capacity is less
     *              than zero.
     */
    public Hashtable(int initialCapacity) {
        this(initialCapacity, 0.75f);
    }

    /**
     * Constructs a new, empty hashtable with a default initial capacity (11)
     * and load factor (0.75).
     */
    public Hashtable() {
        this(11, 0.75f);
    }

    /**
     * Constructs a new hashtable with the same mappings as the given
     * Map.  The hashtable is created with an initial capacity sufficient to
     * hold the mappings in the given Map and a default load factor (0.75).
     *
     * @param t the map whose mappings are to be placed in this map.
     * @throws NullPointerException if the specified map is null.
     * @since   1.2
     */
    public Hashtable(Map<? extends K, ? extends V> t) {
        this(Math.max(2*t.size(), 11), 0.75f);
        putAll(t);
    }

4、常用方法
(1)put()
可以看出key、value都不可以null,且key重复会覆盖旧值,value可以重复。

`public synchronized V put(K key, V value) {
        // Make sure the value is not null
        if (value == null) {//value不可为null
            throw new NullPointerException();
        }

        // Makes sure the key is not already in the hashtable.
        Entry tab[] = table;
        int hash = hash(key);
        int index = (hash & 0x7FFFFFFF) % tab.length;
        for (Entry<K,V> e = tab[index] ; e != null ; e = e.next) {
            if ((e.hash == hash) && e.key.equals(key)) {
                V old = e.value;
                e.value = value;
                return old;
            }
        }

        modCount++;
        if (count >= threshold) {
            // Rehash the table if the threshold is exceeded
            rehash();

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

        // Creates the new entry.
        Entry<K,V> e = tab[index];
        tab[index] = new Entry<>(hash, key, value, e);
        count++;
        return null;
    }
private int hash(Object k) {
        // hashSeed will be zero if alternative hashing is disabled.
        return hashSeed ^ k.hashCode();//此处可证明key不可为null,否则会抛空指针异常
    }

操作过程:计算key的hash值,确认在table[]中的索引位置,迭代index索引位置,如果该位置处的链表中存在一个一样的key,则替换其value,返回旧值;不存在则将该key-value节点插入该index索引位置处。

注:
①synchronized关键字表示这个方法加锁,当它锁定一个方法或者一个代码块的时候,同一时刻最多只有一个线程可以执行这段代码。即例如一个线程A运行到这个方法时都要检查有没有其它线程B正在用这个方法(或者该类的其他同步方法),有的话要等正在使用synchronized方法的线程B运行完这个方法后再运行此线程A,没有的话,锁定调用者,然后直接运行。
该关键字表明同一时刻只能有一个线程访问Hashtable实例,所以Hashtable是线程安全的。
②计算hash值时,HashMap重新计算了key的hash值,而Hashtable直接用key的hashCode();
求对应的位置索引时,HashMap在求位置索引时,则用与运算,而Hashtable用取模运算,且一般先用hash&0x7FFFFFFF后,再对length取模。(&0x7FFFFFFF的目的是为了将负的hash值转化为正值,因为hash值有可能为负数,而&0x7FFFFFFF后,只有符号外改变,而后面的位都不变)
(2)remove()

public synchronized V remove(Object key) {
        Entry tab[] = table;
        int hash = hash(key);
        int index = (hash & 0x7FFFFFFF) % tab.length;
        for (Entry<K,V> e = tab[index], prev = null ; e != null ; prev = e, e = e.next) {
            if ((e.hash == hash) && e.key.equals(key)) {
                modCount++;
                if (prev != null) {
                    prev.next = e.next;
                } else {
                    tab[index] = e.next;
                }
                count--;
                V oldValue = e.value;
                e.value = null;
                return oldValue;
            }
        }
        return null;
    }

(3)get()

public synchronized V get(Object key) {
        Entry tab[] = table;
        int hash = hash(key);
        int index = (hash & 0x7FFFFFFF) % tab.length;
        for (Entry<K,V> e = tab[index] ; e != null ; e = e.next) {
            if ((e.hash == hash) && e.key.equals(key)) {
                return e.value;
            }
        }
        return null;
    }

三、HashMap和Hashtable
1、相同点
①都是存储键值对的散列表。

②都是通过table数组存储,数组的每一个元素都是一个Entry;一个Entry就是一个单向链表,Entry链表中的每一个节点就保存了key-value键值对数据。

③添加key-value键值对时:
先根据key计算出哈希值,再计算出数组索引;
然后根据数组索引找到Entry,再遍历单向链表,将key和链表中的每一个节点的key进行对比;
若key已存在于Entry链表中,则用该value值取代旧value值;
若key不存在,则新建一个key-value节点,并将该节点插入Entry链表的表头位置。

④删除key-value键值对时:
先根据key计算出哈希值,再计算出数组索引;
然后根据索引找出Entry;
若节点key-value存在与链表Entry中,则删除链表中的节点。

⑤都实现了Map、Cloneable、java.io.Serializable接口。
2、不同点
①继承和实现方式不同
HashMap继承于AbstractMap;
Hashtable继承于Dictionary。

②线程安全不同
HashMap的函数是非同步的,不是线程安全的;
Hashtable几乎所有函数都是同步的,它是线程安全的,支持多线程,主要是通过在方法上加关键字synchronized。

③key和value是否可以null值
HashMap的key、value都可以为null;
Hashtable的key、value都不可以为null。
注:如果在Hashtable中有put(null,null)操作,编译同样可以通过,因为key和value都是Object类型,但运行时会抛出NullPointerException异常。

④遍历方式不同
HashMap只支持Iterator遍历;
Hashtable支持Iterator和Enumeration两种方式。
并且通过迭代器遍历时,HashMap是从前往后,Hashtable是从后往前。

⑤添加元素时的hash值算法不同
HashMap添加元素时,是使用自定义的哈希算法;
Hashtable没有自定义哈希算法,而直接采用的key的hashCode()。

⑥初始不同
HashMap默认容量大小是16,扩容时“原始容量x2”;
Hashtable默认的容量大小是11,增加容量时,每次将容量变为“原始容量x2+1”。
注:HashTable在不指定容量的情况下的默认容量为11,而HashMap为16,Hashtable不要求底层数组的容量一定要为2的整数次幂,而HashMap则要求一定为2的整数次幂。

⑦效率
单线程情况下HashMap效率高。

⑧扩容
HashMap扩容时2倍table.length;
Hashtable扩容时2倍table.length+1。
四、LinkedHashMap
LinkedHashMap是Map接口的哈希表和链接列表实现,具有可预知的迭代顺序。此实现提供所有可选的映射操作,并允许使用null值和null键。此类不保证映射的顺序,特别是它不保证该顺序恒久不变。
1、继承关系

    extends HashMap<K,V>
    implements Map<K,V>

2、关于插入有序

private static class Entry<K,V> extends HashMap.Entry<K,V> {
        // These fields comprise the doubly linked list used for iteration.
        Entry<K,V> before, after;

        Entry(int hash, K key, V value, HashMap.Entry<K,V> next) {
            super(hash, key, value, next);
        }

        /**
         * Removes this entry from the linked list.
         */
        private void remove() {
            before.after = after;
            after.before = before;
        }

        /**
         * Inserts this entry before the specified existing entry in the list.
         */
        private void addBefore(Entry<K,V> existingEntry) {
            after  = existingEntry;
            before = existingEntry.before;
            before.after = this;
            after.before = this;
        }

        /**
         * This method is invoked by the superclass whenever the value
         * of a pre-existing entry is read by Map.get or modified by Map.set.
         * If the enclosing Map is access-ordered, it moves the entry
         * to the end of the list; otherwise, it does nothing.
         */
        void recordAccess(HashMap<K,V> m) {
            LinkedHashMap<K,V> lm = (LinkedHashMap<K,V>)m;
            if (lm.accessOrder) {
                lm.modCount++;
                remove();
                addBefore(lm.header);
            }
        }

        void recordRemoval(HashMap<K,V> m) {
            remove();
        }
    }

LinkedHashMap.Entry继承自HashMap.Node,除了Node本身有的属性外,还额外增加了before、after用于指向前一个Entry、后一个Entry。即元素之间维持着一条总的链表数据结构,也因此保证了LinkedHashMap的有序性。
3、常用方法
(1)put()

   public V put(K key, V value) {
         if (table == EMPTY_TABLE) {
             inflateTable(threshold);
         }
         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);//this代表一个集合对象
                 return oldValue;
             }
         }
 
         modCount++;
         addEntry(hash, key, value, i);
         return null;
   } 
   
   //父类的实现
  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) {
          HashMap.Entry<K,V> old = table[bucketIndex];
          Entry<K,V> e = new Entry<>(hash, key, value, old);
          table[bucketIndex] = e;
          e.addBefore(header);
          size++;
      }
      
  //子类的实现
    private void addBefore(Entry<K,V> existingEntry) {
        after  = existingEntry;
        before = existingEntry.before;
        before.after = this;
        after.before = this;
    }         
   
   //子类的实现
    void addEntry(int hash, K key, V value, int bucketIndex) {
          super.addEntry(hash, key, value, bucketIndex);
  
          // Remove eldest entry if instructed
          Entry<K,V> eldest = header.after;
          
          if (removeEldestEntry(eldest)) {
              removeEntryForKey(eldest.key);
          }
      }

当put元素时,不但把它加入到HashMap中去,还要加入到双向链表中,所以LinkedHashMap就是HashMap+双向链表,header是一个Entry类型的双向链表表头,本身不存储数据。
(2)get()

//accessOrder:true按访问顺序组织数据,false:按插入顺序,默认值false  
LinkedHashMap<Integer,Integer> linkedHashMap=new LinkedHashMap<Integer,Integer>(16,0.75f,true);

4、使用场景
LinkedHashMap是HashMap的一个子类,保留了插入的顺序,如果需要输出的顺序和输入时的相同,那么就选用LinkedHashMap。
存储的数据是键值对,并且需要按顺序存储。
5、与HashMap比较
(1)LinkedHashMap维护着一个运行于所有条目的双重链接列表。此链接列表定义了迭代顺序,该迭代顺序可以是插入顺序或者是访问顺序。根据链表中元素的顺序可以分为:按插入顺序的链表,和按访问顺序(调用get方法)的链表。默认是按插入顺序排序,如果指定按访问顺序排序,那么调用get方法后,会将这次访问的元素移至链表尾部,不断访问可以形成按访问顺序排序的链表。
注意,此实现不是同步的。如果多个线程同时访问链接的哈希映射,而其中至少一个线程从结构上修改了该映射,则它必须保持外部同步。
(2)由于LinkedHashMap需要维护元素的插入顺序,因此性能略低于HashMap的性能,但在迭代访问Map里的全部元素时将有很好的性能,因为它以链表来维护内部顺序。
五、TreeMap
1、继承关系

extends AbstractMap<K,V>
implements NavigableMap<K,V>, Cloneable, java.io.Serializable

2、重点
(1)TreeMap是一个有序的key-value集合,基于红黑树实现,每一个key-value作为红黑树的一个节点。
(2)TreeMap存储时会根据key对key-value键值对进行排序,排序方式分自然排序、定制排序两种。
注:
自然排序:TreeMap中所有的key必须实现Comparable接口,并且所有的key都应该是同一个类的对象,否则会报ClassCastException异常。
定制排序:定义TreeMap时,创建一个comparator对象,该对象对所有的treeMap中所有的key值进行排序,采用定制排序的时候不需要TreeMap中所有的key必须实现Comparable接口。
(3)TreeMap判断两个元素相等的标准:两个key通过equals()方法返回为true,并且通过compareTo()方法比较应该返回为0。因此在使用自定义的类作为TreeMap中的key值时,则要重写自定义类中的equals()方法。
六、WeakHashMap
1、继承关系

extends AbstractMap<K,V>
    implements Map<K,V> 

2、重点
插入的数据会随着不断的后续的插入渐渐消失,本质是数据被垃圾回收机制回收,使用到弱引用。
3、拓展
关于java的四种引用——强引用、弱引用、软引用、虚引用
(1)强引用StrongReference<>
垃圾回收器不会回收,且内存不足时,jvm即使抛异常也不回收。
例:通过new创建对象的引用。
(2)软引用SoftReference<>
垃圾回收器在若内存不足时回收,充足时不回收。
(3)弱引用WeakReference<>
无论内存是否充足,只要触发垃圾回收,该引用作用的对象都会被回收。
(4)虚引用PhantomReference<>
并不决定对象的回收周期,如果一个对象持有虚引用,在任何时候都有可能被回收。

  • 1
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值