Java集合——第二部分

120 阅读19分钟

本文已参与「新人创作礼」活动,一起开启掘金创作之路。

11.6 Map接口

1. Map接口概述
  • Map与Collection并列存在。用于保存具有映射关系的数据:key-value,是双列集合
  • Map 中的 key 和 value 都可以是任何引用类型的数据
  • Map 中的 key 用Set来存放,不允许重复,即同一个 Map 对象所对应的类,须重写hashCode()和equals()方法
  • 常用String类作为Map的“键”
  • key 和 value 之间存在单向一对一关系,即通过指定的 key 总能找到唯一的、确定的 value
  • Map接口的常用实现类:HashMap、TreeMap、LinkedHashMap和Properties。其中,HashMap是 Map 接口使用频率最高的实现类
2. Map值存储结构

Map值存储结构

  • Map中的 key:无序的、不可重复的,使用Set存储所有的key;key所在的类要重写equals()和hashCode() (以HashMap为例,例如TreeMap不需要必须重写)
  • Map中的 value:无序的、可重复的,使用Collection存储所有的value;value所在的类要重写equals()
  • 一个键值对key-value构成了一个Entry对象;Map中的entry是无序的、不可重复的,使用Set存储所有的entry
3. Map接口常用方法
  1. 添加、删除、修改操作:
    • V put(K key, V value):将指定key-value添加到(或修改)当前map对象中
    • void putAll(Map<? extends K, ? extends V> m):将m中的所有key-value对存放到当前map中
    • V remove(Object key):移除指定key的key-value对,并返回value
    • void clear():清空当前map中的所有数据
  2. 元素查询的操作:
    • V get(Object key):获取指定key对应的value
    • boolean containsKey(Object key):是否包含指定的key
    • boolean containsValue(Object value):是否包含指定的value
    • int size():返回map中key-value对的个数
    • boolean isEmpty():判断当前map是否为空
    • boolean equals(Object o):判断当前map和参数对象o是否相等
  3. 元视图操作的方法:
    • Set<K> keySet():返回所有key构成的Set集合
    • Collection<V> values():返回所有value构成的Collection集合
    • Set<Map.Entry<K, V>> entrySet():返回所有key-value对构成的Set集合
4. Map实现类之一:HashMap

相关说明

  • HashMap是 Map 接口使用频率最高的实现类。
  • HashMap是线程不安全的,效率高
  • 允许使用null键和null值,与HashSet一样,不保证映射的顺序
  • 所有的key构成的集合是Set:无序的、不可重复的。所以,key所在的类要重写:equals()和hashCode()
  • 所有的value构成的集合是Collection:无序的、可以重复的。所以,value所在的类要重写:equals()
  • 一个key-value构成一个entry
  • 所有的entry构成的集合是Set:无序的、不可重复的
  • HashMap 判断两个 key 相等的标准是:两个 key 通过 equals() 方法返回 true,hashCode 值也相等
  • HashMap 判断两个 value相等的标准是:两个 value 通过 equals() 方法返回 true

源码中相关属性

相关属性说明

  • DEFAULT_INITIAL_CAPACITY:HashMap的默认容量,16
  • MAXIMUM_CAPACITY:HashMap的最大支持容量,2^30
  • DEFAULT_LOAD_FACTOR:HashMap的默认加载因子
  • TREEIFY_THRESHOLD:Bucket中链表长度大于该默认值,转化为红黑树
  • UNTREEIFY_THRESHOLD:Bucket中红黑树存储的Node小于该默认值,转化为链表
  • MIN_TREEIFY_CAPACITY:桶中的Node被树化时最小的hash表容量(当桶中Node的数量大到需要变红黑树时,若hash表容量小于MIN_TREEIFY_CAPACITY时,此时应执行resize扩容操作这个MIN_TREEIFY_CAPACITY的值至少是TREEIFY_THRESHOLD的4倍)
  • table:存储元素的数组,总是2的n次幂
  • entrySet:存储具体元素的集
  • size:HashMap中存储的键值对的数量
  • modCount:HashMap扩容和结构改变的次数
  • threshold:扩容的临界值,=容量*填充因子
  • loadFactor:填充因子

存储结构

jdk 7情况下

在这里插入图片描述

说明:

  • HashMap的内部存储结构其实是数组和链表的结合。当实例化一个HashMap时,系统会创建一个长度为Capacity的Entry数组,这个长度在哈希表中被称为容量(Capacity),在这个数组中可以存放元素的位置称之为“桶”(bucket),每个bucket都有自己的索引,系统可以根据索引快速的查找bucket中的元素。

  • 每个bucket中存储一个元素,即一个Entry对象,但每一个Entry对象可以带一个引用变量,用于指向下一个元素,因此,在一个桶中,就有可能生成一个Entry链而且新添加的元素作为链表的head

  • 扩容:

    当HashMap中的元素越来越多的时候,hash冲突的几率也就越来越高,因为数组的长度是固定的。所以为了提高查询的效率,就要对HashMap的数组进行扩容,而在HashMap数组扩容之后,最消耗性能的点就出现了:原数组中的数据必须重新计算其在新数组中的位置,并放进去,这就是resize

  • 扩容时间

    当HashMap中的元素个数超过数组大小(数组总大小length,不是数组中个数size)*loadFactor并且要存放的位置非空时,就会进行数组扩容,loadFactor的默认值(DEFAULT_LOAD_FACTOR)为0.75,这是一个折中的取值。也就是说,默认情况下,数组大小(DEFAULT_INITIAL_CAPACITY)为16,那么当HashMap中元素个数超过16*0.75=12(这个值就是代码中的threshold值,也叫做临界值)的时候,就把数组的大小扩展为2*16=32,即扩大一倍,然后重新计算每个元素在数组中的位置,而这是一个非常消耗性能的操作,所以如果已经预知HashMap中元素的个数,那么预设元素的个数能够有效的提高HashMap的性能

jdk 8情况下

JDK8中HashMap存储结构

说明:

  • HashMap的内部存储结构其实是数组+链表+树的结合。当实例化一个HashMap时,会初始化initialCapacity和loadFactor,在put第一对映射关系时,系统会创建一个长度为initialCapacity的Node数组,这个长度在哈希表中被称为容量(Capacity),在这个数组中可以存放元素的位置称之为“桶”(bucket),每个bucket都有自己的索引,系统可以根据索引快速的查找bucket中的元素。

  • 每个bucket中存储一个元素,即一个Node对象,但每一个Node对象可以带一个引用变量next,用于指向下一个元素,因此,在一个桶中,就有可能生成一个Node链;也可能是一个一个TreeNode对象,每一个TreeNode对象可以有两个叶子结点left和right,因此,在一个桶中,就有可能生成一个TreeNode树。而新添加的元素作为链表的last,或树的叶子结点

  • 扩容时间:

    当HashMap中的元素个数超过数组大小(数组总大小length,不是数组中个数size)*loadFactor时,就会进行数组扩容,loadFactor的默认值(DEFAULT_LOAD_FACTOR)为0.75,这是一个折中的取值。也就是说,默认情况下,数组大小(DEFAULT_INITIAL_CAPACITY)为16,那么当HashMap中元素个数超过16*0.75=12(这个值就是代码中的threshold值,也叫做临界值)的时候,就把数组的大小扩展为2*16=32,即扩大一倍,然后重新计算每个元素在数组中的位置,而这是一个非常消耗性能的操作,所以如果已经预知HashMap中元素的个数,那么预设元素的个数能够有效的提高HashMap的性能

  • 树形化时间:

    当HashMap中的其中一个链的对象个数如果达到了8个,此时如果capacity没有达到64,那么HashMap会先扩容解决,如果已经达到了64,那么这个链会变成树,结点类型由Node变成TreeNode类型。如果当映射关系被移除后,下次resize方法时判断树的结点个数低于6个,也会把树再转为链表

映射关系的key修改问题

映射关系存储到HashMap中会存储key的hash值,这样就不用在每次查找时重新计算每一个Entry或Node(TreeNode)的hash值了,因此如果已经put到Map中的映射关系,再修改key的属性,而这个属性又参与hashcode值的计算,那么会导致匹配不上,所以最好不要修改

添加元素过程

jdk 7情况下

  1. 使用无参构造器创建HashMap对象,在实例化以后,底层创建了长度是16的一维数组Entry[] table
  2. 当调用put(key1,value1)方法(可能已经存储了多个元素)向HashMap集合中存入一个元素时,首先key1会调用所在类的hashCode()方法来计算哈希值,然后根据哈希值,通过某种算法计算出在Entry数组中的存放位置
  3. 判断数组此位置上是否已经有元素,如果此位置上没有其他元素,则key1-value1添加成功(直接添加在数组中,实际添加的是Entry对象);如果此位置上有其他元素(或以链表形式存在的多个元素),则比较key1与该位置上的所有元素的哈希值
  4. 如果哈希值都不相等,则key1-value1添加成功(通过链表的方式继续链接);如果key1的哈希值和已经存在的某一个数据(key2-value2)的哈希值相同,则会再继续调用key1所在类的equals(key2)方法,如果equals方法结果为false,则key1-value1添加成功(通过链表的方式继续链接);如果为true,则使用value1替换value2

对于后面两种添加成功的情况而言:新添加的元素与已经存在指定索引位置上数据以链表的方式存储;新添加的元素放到数组中,指向原来的元素,即新添加的key-value放链表的头部

jdk 8情况下(与jdk 7对比的变化)

  1. 在使用无参构造方法创建对象时,默认情况下,先不创建长度为16的数组;当首次调用put()时,再创建长度为16的数组
  2. 数组为Node类型,在jdk 7中称为Entry类型
  3. 形成链表结构时,原来的元素在数组中,指向新添加的元素,即新添加的key-value对在链表的尾部
  4. 当数组指定索引位置的链表长度 > 8时,且map中的数组的长度 > 64时,此索引位置上的所有key-value对使用红黑树进行存储
  5. jdk7底层结构只有:数组+链表;jdk8中底层结构:数组+链表+红黑树

源码分析

分析即为添加元素过程

jdk 7情况下

// 默认初始化长度
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;
// Entry类
static class Entry<K,V> implements Map.Entry<K,V> {
    final K key; // 元素的key
    V value; // 元素的value
    Entry<K,V> next; // 下一个元素
    int hash; // hash值
	// 有参构造创建对象
    Entry(int h, K k, V v, Entry<K,V> n) {
        value = v;
        next = n;
        key = k;
        hash = h;
    }
    ...
}
// 无参构造
public HashMap() {
    // 调用有参构造
    this(DEFAULT_INITIAL_CAPACITY, DEFAULT_LOAD_FACTOR);
}
// 有参构造
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);

    // 创建容量为1
    int capacity = 1; 
    // 当容量小于初始化容量时将容量扩为原来的2倍,这里是为了保证容量为2的倍数
    while (capacity < initialCapacity)
        capacity <<= 1; // 扩大2倍
	// 初始化加载因子
    this.loadFactor = loadFactor;
    // 计算临界值(容量*加载因子)
    threshold = (int)Math.min(capacity * loadFactor, MAXIMUM_CAPACITY + 1);
    // 初始化存放数据的Entry数组
    table = new Entry[capacity];
    useAltHashing = sun.misc.VM.isBooted() &&
        (capacity >= Holder.ALTERNATIVE_HASHING_THRESHOLD);
    init();
}
// 放入元素
public V put(K key, V value) {
    if (key == null) // 如果key为null,则调用放null的逻辑
        return putForNullKey(value);
    int hash = hash(key); // 调用hash()方法计算key的hash值
    int i = indexFor(hash, table.length); // 调用indexFor()方法获取元素应该存放的位置
    // 判断数组中该位置是否有元素或以链表形式存在的元素,如果有则进入循环
    for (Entry<K,V> e = table[i]; e != null; e = e.next) {
        Object k;
        // 判断当前位置元素的key和添加元素的key是否相同
        if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
            V oldValue = e.value; // 获取当前位置元素的value
            e.value = value; // 将当前位置元素的value替换为添加元素的value
            e.recordAccess(this); 
            return oldValue; // 返回旧元素值
        }
    }

    modCount++;
    addEntry(hash, key, value, i); // 进行元素的添加
    return null;
}
// 添加元素
void addEntry(int hash, K key, V value, int bucketIndex) {
    // 判断当前的数组中的元素个数是否超过临界值并且当前要添加元素的位置是否为空,如果为true,则进行扩容
    if ((size >= threshold) && (null != table[bucketIndex])) {
        resize(2 * table.length); // 扩容为原来的2倍
        hash = (null != key) ? hash(key) : 0; // 重新获取当前key的hash值
        bucketIndex = indexFor(hash, table.length); // 重新获取需要存放的位置
    }
	// 进行元素的创建和添加
    createEntry(hash, key, value, bucketIndex);
}
// 进行扩容
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];
    boolean oldAltHashing = useAltHashing;
    useAltHashing |= sun.misc.VM.isBooted() &&
        (newCapacity >= Holder.ALTERNATIVE_HASHING_THRESHOLD);
    boolean rehash = oldAltHashing ^ useAltHashing;
    // 调用transfer()方法进行数据的复制
    transfer(newTable, rehash);
    table = newTable;
    threshold = (int)Math.min(newCapacity * loadFactor, MAXIMUM_CAPACITY + 1);
}
// 进行元素的添加
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++; // 长度加1
}

jdk 8情况下

// 默认初始化长度
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // 16
// 定义的最大值
static final int MAXIMUM_CAPACITY = 1 << 30;
// 默认加载因子
static final float DEFAULT_LOAD_FACTOR = 0.75f;
// 链表转红黑树的临界值
static final int TREEIFY_THRESHOLD = 8;
// 转红黑树的数组的最大长度
static final int MIN_TREEIFY_CAPACITY = 64;
// 存放数据的数组
transient Node<K,V>[] table;
// 存放的数据个数
transient int size;
// 临界值
int threshold;
// 加载因子
final float loadFactor;
// Node类
static class Node<K,V> implements Map.Entry<K,V> {
    final int hash; // hash值
    final K key; // key值
    V value; // 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 HashMap() {
    // 初始化加载因子
    this.loadFactor = DEFAULT_LOAD_FACTOR;
}
// 放入元素
public V put(K key, V value) {
    // 获取key的hash值,随后调用其他方法放入元素
    return putVal(hash(key), key, value, false, true);
}

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;
        // 判断当前位置上的元素的key是否与新添加元素的key相同
        if (p.hash == hash &&
            ((k = p.key) == key || (key != null && key.equals(k))))
            e = p; // 如果相等则将当前位置上的元素给e
        // 判断当前位置上的元素是否是红黑树形式存储
        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) // 判断当前链表是否需要转为红黑树
                        treeifyBin(tab, hash); // 需要,则调用方法进行转换
                    break; // 放完元素结束循环
                }
                // 判断当前遍历的元素的key是否与新添加元素的key相同
                if (e.hash == hash &&
                    ((k = e.key) == key || (key != null && key.equals(k))))
                    break; // 相等直接结束循环
                p = e; // 继续查找下一个
            }
        }
        if (e != null) { // 判断e是否为空,不为空说明有元素的key与添加的元素的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;
}
// 进行扩容
final Node<K,V>[] resize() {
    Node<K,V>[] oldTab = table; // 获取当前数组
    int oldCap = (oldTab == null) ? 0 : oldTab.length; // 获取当前数组的容量(第一次放入元素还未进行初始化数组,因此为0)
    int oldThr = threshold; // 获取临界值(第一次放入元素还没数组,因此是初始值0)
    int newCap, newThr = 0;
    // 判断当前容量是否大于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 { // 第一次放入元素会执行该逻辑
        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; // 返回新数组
}
// 链表转为红黑树
final void treeifyBin(Node<K,V>[] tab, int hash) {
    int n, index; Node<K,V> e;
    // 判断当前数组的长度是否小于定义的值
    if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY)
        resize(); // 小于进行扩充,不进行转换
    else if ((e = tab[index = (n - 1) & hash]) != null) {
        TreeNode<K,V> hd = null, tl = null;
        do {
            TreeNode<K,V> 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的影响

  • 负载因子的大小决定了HashMap的数据密度。
  • 负载因子越大密度越大,发生碰撞的几率越高,数组中的链表越容易长,造成查询或插入时的比较次数增多,性能会下降。
  • 负载因子越小,就越容易触发扩容,数据密度也越小,意味着发生碰撞的几率越小,数组中的链表也就越短,查询和插入时比较的次数也越小,性能会更高。但是会浪费一定的内容空间。而且经常扩容也会影响性能,建 议初始化预设大一点的空间。
  • 按照其他语言的参考及研究经验,会考虑将负载因子设置为0.7~0.75,此时平均检索长度接近于常数。
5. Map实现类之二:LinkedHashMap

相关说明

  • LinkedHashMap 是 HashMap 的子类
  • 在HashMap存储结构的基础上,使用了一对双向链表来记录添加元素的顺序
  • 与LinkedHashSet类似,LinkedHashMap 可以维护 Map 的迭代顺序:迭代顺序与 Key-Value 对的插入顺序一致
  • 在遍历map元素时,可以按照添加的顺序实现遍历
    • 原因:在原有的HashMap底层结构基础上,添加了一对指针,指向前一个和后一个元素
    • 对于频繁的遍历操作,此类执行效率高于HashMap

源码分析

static class Entry<K,V> extends HashMap.Node<K,V> {
    Entry<K,V> before, after; // 用来记录前一个元素和后一个元素
    Entry(int hash, K key, V value, Node<K,V> next) {
        super(hash, key, value, next);
    }
}

LinkedHashMap调用put方法时调用的还是父类的方法,但其重写了newNode等方法,记录了其前后添加的元素

6. Map实现类之三:TreeMap

相关说明

  • TreeMap存储 Key-Value 对时,需要根据 key-value 对进行排序

    TreeMap可以保证所有的 Key-Value 对处于有序状态

  • TreeMap底层使用红黑树结构存储数据

  • TreeMap的key不可以存null,value可以存null

  • TreeMap 的 Key 的排序:

    • 自然排序:TreeMap 的所有的 Key 必须实现 Comparable 接口,而且所有的 Key 应该是同一个类的对象,否则将会抛出 ClasssCastException
    • 定制排序:创建 TreeMap 时,传入一个 Comparator 对象,该对象负责对 TreeMap 中的所有 key 进行排序。此时不需要 Map 的 Key 实现 Comparable 接口
  • TreeMap判断两个key相等的标准:两个key通过compareTo()方法或者compare()方法返回0

7. Map实现类之四:Hashtable

相关说明

  • Hashtable是个古老的 Map 实现类,JDK1.0就提供了。不同于HashMap,Hashtable是线程安全的。
  • Hashtable实现原理和HashMap相同,功能相同。底层都使用哈希表结构,查询速度快,很多情况下可以互用。
  • 与HashMap不同,Hashtable 不允许使用 null 作为 key 和 value
  • 与HashMap一样,Hashtable 也不能保证其中 Key-Value 对的顺序
  • Hashtable判断两个key相等、两个value相等的标准,与HashMap一致
8. Map实现类之五:Properties

相关说明

  • Properties 类是 Hashtable 的子类,该对象用于处理属性文件
  • 由于属性文件里的 key、value 都是字符串类型,所以 Properties 里的 key 和 value 都是字符串类型
  • 存取数据时,建议使用setProperty(String key, String value)方法和getProperty(String key)方法

使用例子

public class PropertiesTest {
    public static void main(String[] args) {
        InputStream in = null;
        try {
            in = new FileInputStream("jdbc.properties");
            Properties prop = new Properties();
            prop.load(in);
            String username = prop.getProperty("username");
            String password = prop.getProperty("password");
            System.out.println(username + "---" + password);
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            if (in != null) {
                try {
                    in.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }

    }
}
9. Map集合总结

遍历Map集合

public class MapTest {

    public static void main(String[] args) {
        Map map = new HashMap();
        map.put("123", 456);
        map.put("456", 789);
        map.put("aa", "bb");
        // 获取key集
        Set set = map.keySet();
        Iterator iterator = set.iterator();
        while (iterator.hasNext()) {
            System.out.println(iterator.next());
        }

        // 获取value集
        Collection values = map.values();
        for (Object value : values) {
            System.out.println(value);
        }

        // 遍历方法一
        // 获取Entry集
        Set set1 = map.entrySet();
        Iterator iterator1 = set1.iterator();
        while (iterator1.hasNext()) {
            Map.Entry next = (Map.Entry) iterator1.next();
            System.out.println(next.getKey() + "---" + next.getValue());
        }

        // 遍历方法二
        Set set2 = map.keySet();
        Iterator iterator2 = set2.iterator();
        while (iterator2.hasNext()) {
            Object key = iterator2.next();
            Object value = map.get(key);
            System.out.println(key + "---" + value);
        }
    }
}

常用方法

  • 添加:put(Object key,Object value)

  • 删除:remove(Object key)

  • 修改:put(Object key,Object value)

  • 查询:get(Object key)

  • 长度:size()

  • 遍历:keySet() / values() / entrySet()

11.7 Collections工具类

1. 相关说明
  • Collections 是一个操作 Set、List 和 Map 等集合的工具类
  • Collections 中提供了一系列静态的方法对集合元素进行排序、查询和修改等操作,还提供了对集合对象设置不可变、对集合对象实现同步控制等方法
2. 相关方法
  1. 排序操作:(均为static方法)

    • reverse(List):反转 List 中元素的顺序
    • shuffle(List):对 List 集合元素进行随机排序
    • sort(List):根据元素的自然顺序对指定 List 集合元素按升序排序
    • sort(List,Comparator):根据指定的 Comparator 产生的顺序对 List 集合元素进行排序
    • swap(List,int, int):将指定 list 集合中的 i 处元素和 j 处元素进行交换
  2. 查找、替换

    • Object max(Collection):根据元素的自然顺序,返回给定集合中的最大元素

    • Object max(Collection,Comparator):根据 Comparator 指定的顺序,返回给定集合中的最大元素

    • Object min(Collection):根据元素的自然顺序,返回给定集合中的最小元素

    • Object min(Collection,Comparator):根据 Comparator 指定的顺序,返回给定集合中的最小元素

    • int frequency(Collection,Object):返回指定集合中指定元素的出现次数

    • void copy(List dest,List src):将src中的内容复制到dest中

      public class CollectionsTest {
          public static void main(String[] args) {
              List list = new ArrayList();
              list.add(123);
              list.add(456);
              list.add(888);
              list.add(555);
              list.add(-123);
      
              //报异常:IndexOutOfBoundsException
      //        List dest = new ArrayList();
      //        Collections.copy(dest,list);
      
              List dest = Arrays.asList(new Object[list.size()]);
              System.out.println(dest.size());
              Collections.copy(dest,list);
              System.out.println(dest);
          }
      }
      
    • boolean replaceAll(List list,Object oldVal,Object newVal):使用新值替换 List 对象的所有旧值

  3. 同步控制

    Collections 类中提供了多个 synchronizedXxx() 方法,该方法可使将指定集合包装成线程同步的集合,从而可以解决多线程并发访问集合时的线程安全问题

3. 补充:Enumeration
  • Enumeration 接口是 Iterator 迭代器的 “古老版本”
  • 具有hasMoreElements()和nextElement()方法,用法类似迭代器