java基础学习之集合-Map

来源:互联网 发布:网络监察大队图片 编辑:程序博客网 时间:2024/06/17 13:17

java中的Map集合是一个由键-值(key-value)组合而成的,Map作为一个接口,jdk解释为将键映射到值的对象。一个映射不能包含重复的键;每个键最多只能映射到一个值。
Map.entry是其内部的一个接口,由于Map没有实现Iterable接口,就不能使用iterator来遍历集合,所以通过entrySet方法转换为Set,这个Set里存放每个元素都是一个 Map.Entry< K , V >再来遍历这个Set即可。

一.HashMap介绍

HashMap 是基于哈希表的 Map 接口的实现。此实现提供所有可选的映射操作,并允许使用 null 值和 null 键。(除了非同步和允许使用 null 之外,HashMap 类与 Hashtable 大致相同。)此类不保证映射的顺序,特别是它不保证该顺序恒久不变。

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

可以看出HashMap继承了AbstractMap,实现了Map,Cloneable,Serializable接口,这里没有实现同步,所以它是线程不安全的,这里可以在创建是通过Collections的集合工具类方法来实现同步。

Map m = Collections.synchronizedMap(new HashMap(...));

HashMap的构造器:

// 默认构造函数。HashMap()// 指定“容量大小”的构造函数HashMap(int capacity)// 指定“容量大小”和“加载因子”的构造函数HashMap(int capacity, 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);        this.loadFactor = loadFactor;        //threshold 临界值   当实际大小超过临界值时,会进行扩容threshold = 加载因子*容量        threshold = initialCapacity;        init();}// 包含“子Map”的构造函数HashMap(Map<? extends K, ? extends V> map){    inflateTable(threshold);    putAllForCreate(m);}

二.HashMap结构

hashMap是一个散列表,通过“拉链法”来解决hash冲突

它是基于数组和链表实现的,从上面构造器的方法可以看到容量大小(capacity)和加载因子(loadFactor)两个参数对hashMap非常重要,容量指的是哈希表中桶的数量,初始容量是创建哈希表时的容量,加载因子是哈希表在其容量自动增加之前可以达到多满的一种尺度,它衡量的是一个散列表的空间的使用程度,负载因子越大表示散列表的装填程度越高,反之愈小。对于使用链表法的散列表来说,查找一个元素的平均时间是O(1+a),因此如果负载因子越大,对空间的利用更充分,然而后果是查找效率的降低;如果负载因子太小,那么散列表的数据将过于稀疏,对空间造成严重浪费。系统默认负载因子为0.75,这个值应该是比较均衡的值。若哈希表中元素超过了加载因子与当前容量的乘积时,则要对该哈希表进行 rehash 操作(即重建内部数据结构),从而哈希表将具有大约两倍的桶数。
在一个长度为16的数组中,每个数组的元素都是存储一条链表的头节点,hashmap通过对元素的key的hash值对数组的长度取模来得到这个元素应该位于数组的那个位置,hash(key)% len,比如12%16=12,28%16=12,这样12和28都会位于数组下标为12的链表中。那这个链表又如何实现的呢?
HashMap数据存储数组:

    //HashMap中的key-value都是存储在Entry数组中的。    transient Entry[] table;
    我们可以看到hashMap里包含了一个Entry<K,V>类
 static class Entry< K,V> implements Map.Entry< K,V> {        final K key;        V value;        Entry< K,V> next;        int hash;        /**         * Creates new entry.         */        Entry(int h, K k, V v, Entry< K,V> n) {            value = v;            next = n;            key = k;            hash = h;        }        ...   }  

三.HashMap的存储put

public V put(K key, V value) {        if (table == EMPTY_TABLE) {            inflateTable(threshold);        }        //当key为null,调用putForNullKey方法,保存null与table第一个位置中,这就是HashMap允许为null的原因        if (key == null)            return putForNullKey(value);        //计算key的hash值        int hash = hash(key);        //计算key hash 值在 table 数组中的位置        int i = indexFor(hash, table.length);        //从i出开始迭代 e,找到 key 保存的位置        for (Entry<K,V> e = table[i]; e != null; e = e.next) {            Object k;             //判断该条链上是否有hash值相同的(key相同)            if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {               //相同,则直接覆盖value,返回旧value                V oldValue = e.value;                e.value = value;                e.recordAccess(this);                //返回旧value                return oldValue;            }        }        //修改次数增加1        modCount++;        //将key、value添加至i位置处        addEntry(hash, key, value, i);        return null;    }    //计算key在数组的位置    static int indexFor(int h, int length) {        return h & (length-1);    }    //计算hash方法static int hash(int h) {        h ^= (h >>> 20) ^ (h >>> 12);        return h ^ (h >>> 7) ^ (h >>> 4);    }    //addEntry 加入链表元素方法    void addEntry(int hash, K key, V value, int bucketIndex) {        //获取bucketIndex处的Entry        Entry<K, V> e = table[bucketIndex];        //将新创建的 Entry 放入 bucketIndex 索引处,并让新的 Entry 指向原来的 Entry         table[bucketIndex] = new Entry<K, V>(hash, key, value, e);        //若HashMap中元素的个数超过极限了,则容量扩大两倍        if (size++ >= threshold)            resize(2 * table.length);    }

通过源码我们可以看到HashMap保存数据的过程为:首先判断key是否为null,若为null,则直接调用putForNullKey方法。若不为空则先计算key的hash值,然后根据hash值搜索在table数组中的索引位置,如果table数组在该位置处有元素,则通过比较是否存在相同的key,若存在则覆盖原来key的value,否则将该元素保存在链头(最先保存的元素放在链尾)。

四.HashMap的取值Get

public V get(Object key) {        // 若为null,调用getForNullKey方法返回相对应的value        if (key == null)            return getForNullKey();        // 根据该 key 的 hashCode 值计算它的 hash 码          int hash = hash(key.hashCode());        // 取出 table 数组中指定索引处的值        for (Entry<K, V> e = table[indexFor(hash, table.length)]; e != null; e = e.next) {            Object k;            //若搜索的key与查找的key相同,则返回相对应的value            if (e.hash == hash && ((k = e.key) == key || key.equals(k)))                return e.value;        }        return null;    }

HashMap在存储过程中把key,value当做一个整体key-value来处理的,这个整体就是Entry对象。同时value也只相当于key的附属而已。在存储的过程中,系统根据key的hashcode来决定Entry在table数组中的存储位置,在取的过程中同样根据key的hashcode取出相对应的Entry对象。

五.HashMap的遍历

hashMap是没有实现 Iterable接口,不能直接通过iterator() 方法得到遍历器
1.遍历HashMap的键
通过 keySet()来得到set,再通过Iterator遍历器来遍历set

// 假设map是HashMap对象// map中的key是String类型,value是Integer类型String key = null;Integer integ = null;Iterator iter = map.keySet().iterator();while (iter.hasNext()) {        // 获取key    key = (String)iter.next();        // 根据key,获取value    integ = (Integer)map.get(key);}

2.遍历HashMap的值
根据value()获取HashMap的“值”的集合。再通过Iterator迭代器遍历集合

// 假设map是HashMap对象// map中的key是String类型,value是Integer类型Integer value = null;Collection c = map.values();Iterator iter= c.iterator();while (iter.hasNext()) {    value = (Integer)iter.next();}

3.遍历HashMap的键值对
根据entrySet()获取HashMap的“键值对”的Set集合。再通过Iterator迭代器遍历集合

// 若map中的key是String类型,value是Integer类型Integer integ = null;Iterator iter = map.entrySet().iterator();while(iter.hasNext()) {    Map.Entry entry = (Map.Entry)iter.next();    // 获取key    key = (String)entry.getKey();        // 获取value    integ = (Integer)entry.getValue();}

六.Hashtable的介绍

HashTable实现一个哈希表,该哈希表将键映射到相应的值。任何非 null 对象都可以用作键或值。
为了成功地在哈希表中存储和获取对象,用作键的对象必须实现 hashCode 方法和 equals 方法。

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

hashTable是继承了Dictionary,实现了Map,Cloneable和Serializable,而且它是线程安全的。
Hashtable是非泛型的集合,所以在检索和存储值类型时通常会发生装箱与拆箱的操作。

public synchronized V put(K key, V value) {        // Make sure the value is not null        if (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;    }

当把某个元素添加到 Hashtable 时,将根据键的哈希代码将该元素放入存储桶中,由于是散列算法所以会出现一个哈希函数能够为两个不同的键生成相同的哈希代码,该键的后续查找将使用键的哈希代码只在一个特定存储桶中搜索,这将大大减少为查找一个元素所需的键比较的次数。

Hashtable 的加载因子确定元素与Hashtable 可拥有的元素数的最大比率。加载因子越小,平均查找速度越快,但消耗的内存也增加。默认的加载因子 0.72通常提供速度和大小之间的最佳平衡。当创建 Hashtable 时,也可以指定其他加载因子。

元素总量/ Hashtable 可拥有的元素数=加载因子

当向 Hashtable 添加元素时,Hashtable 的实际加载因子将增加。当实际加载因子达到指定的加载因子时,Hashtable 中存储桶的数目自动增加到大于当前 Hashtable 存储桶数两倍的最小素数。

扩容时所有的数据需要重新进行散列计算。虽然Hash具有O(1)的数据检索效率,但它空间开销却通常很大,是以空间换取时间。所以Hashtable适用于读取操作频繁,写入操作很少的操作类型。

七.TreeMap的介绍

底层红黑树(Red-Black tree)实现的。好处是自动对添加的键值对排序。

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

TreeMap是一颗红黑树,树的每个节点是一个类Entry< K , V>

static final class Entry<K,V> implements Map.Entry<K,V> { ... }

Entry包含了6个部分内容:key(键)、value(值)、left(左孩子)、right(右孩子)、parent(父节点)、color(颜色)
Entry节点根据key进行排序,Entry节点包含的内容为value。

八.Properties的介绍

在java中,很多配置文件都是用*.properties来保存配置信息,格式是每行都是键=值,用“#”来注释。常用来配置连接数据库的相关信息配置和日志配置。

Properties类继承自Hashtable类并且实现了Map接口,也是使用一种键值对的形式来保存属性集,位于java.util下。不过Properties有特殊的地方,就是它的键和值都是字符串类型。
properties的常见操作方法:
load(InputStream inStream)
从输入流中读取属性列表(键和元素对)

Properties prop = new Properties();FileInputStream fis = new FileInputStream("config.properties");prop.load(fis);

store(Writer writer,String comments)
以适合使用 load(Reader) 方法的格式,将此 Properties 表中的属性列表(键和元素对)写入输出字符。

FileOutputStream fos = new FileOutputStream(file, "config.properties");//comments 属性列表的描述prop.store(fos, "comments");

如果comments不为空,保存后的属性文件第一行会是#comments,表示注释信息;如果为空则没有注释信息。直接为null。
getProperty(String key)
用指定的键在此属性列表中搜索属性。如果在此属性列表中未找到该键,则接着递归检查默认属性列表及其默认值。如果未找到属性,则此方法返回 null。
setProperty(String key,String value)
调用 Hashtable 的方法 put。使用 getProperty 方法提供并行性。强制要求为属性的键和值使用字符串。返回值是 Hashtable 调用 put 的结果。

1 0
原创粉丝点击