java 数据结构(6) Hashmap Hashtable Treemap LinkedHashMap

参考
java提高篇(二三)-----HashMap
HashMap vs. TreeMap vs. Hashtable vs. LinkedHashMap
Java之美[从菜鸟到高手演变]之HashMap、HashTable
hashmap方法摘要

Java中数据存储方式最底层的两种结构,一种是数组,另一种就是链表,数组的特点:连续空间,寻址迅速,但是在删除或者添加元素的时候需要有较大幅度的移动,所以查询速度快,增删较慢。而链表正好相反,由于空间不连续,寻址困难,增删元素只需修改指针,所以查询慢、增删快。有没有一种数据结构来综合一下 数组和链表,以便发挥他们各自的优势?答案是肯定的!就是:哈希表。哈希表具有较快(常量级)的查询速度,及相对较快的增删速度,所以很适合在海量数据的环境中使用。一般实现哈希表的方法采用“拉链法”,我们可以理解为“链表的数组”,如下图:


数组里全是链表结构

从上图中,我们可以发现哈希表是由数组+链表组成的,一个长度为16的数组中,每个元素存储的是一个链表的头结点。那么这些元素是按照什么样的规则 存储到数组中呢。一般情况是通过hash(key)%len获得,也就是元素的key的哈希值对数组长度取模得到。比如上述哈希表 中,12%16=12,28%16=12,108%16=12,140%16=12。所以12、28、108以及140都存储在数组下标为12的位置。它 的内部其实是用一个Entity数组来实现的,属性有key、value、next。

一、HashMap内部结构

1、初始化

static final int DEFAULT_INITIAL_CAPACITY = 16;// 初始容量:16
static final int MAXIMUM_CAPACITY = 1 << 30; //最大容量:2的30次方:1073741824
static final float DEFAULT_LOAD_FACTOR = 0.75f; //装载因子,后面再说它的作用

其中Entry为HashMap的内部类,它包含了键key、值value、下一个节点next,以及hash值,这是非常重要的,正是由于Entry才构成了table数组的项为链表。

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

        /**
         * Creates new entry.
         */
        Entry(int h, K k, V v, Entry<K,V> n) {
            value = v;
            next = n;
            key = k;
            hash = h;
        }
        .......
    }

2.构造方法

public HashMap() {  
        this.loadFactor = DEFAULT_LOAD_FACTOR;  
        threshold = (int)(DEFAULT_INITIAL_CAPACITY * DEFAULT_LOAD_FACTOR);  
        table = new Entry[DEFAULT_INITIAL_CAPACITY]; //默认开辟16个
        init();  
    }

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;  
        while (capacity < initialCapacity)  
            capacity <<= 1;  
  
        this.loadFactor = loadFactor;  
        threshold = (int)(capacity * loadFactor);  
        table = new Entry[capacity];  
        init();  
    } 

capacity <<= 1表示实际的开辟的空间要大于传入的第一个参数的值。举个例子:new HashMap(7,0.8),loadFactor为0.8,capacity为7,通过上述代码后,capacity的值为:8.(1 << 2的结果是2,2 << 2的结果为4)。所以,最终capacity的值为8,最后通过new Entry[capacity]来创建大小为capacity的数组。

3.put操作

public V put(K key, V value) {
        //当key为null,调用putForNullKey方法,保存null与table
        //第一个位置中,这是HashMap允许为null的原因
        if (key == null)
            return putForNullKey(value);
        //计算key的hash值
        int hash = hash(key.hashCode());                  ------(1)
        //计算key hash 值在 table 数组中的位置
        int i = indexFor(hash, table.length);             ------(2)
        //从i出开始迭代 e,找到 key 保存的位置
        for (Entry<K, V> e = table[i]; e != null; e = e.next) {
            Object k;
            //判断该条链上是否有hash值相同的(key相同)
            //若存在相同,则直接覆盖value,返回旧value
            if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
                V oldValue = e.value;    //旧值 = 新值
                e.value = value;
                e.recordAccess(this);
                return oldValue;     //返回旧值
            }
        }
        //修改次数增加1
        modCount++;
        //将key、value添加至i位置处
        addEntry(hash, key, value, i);
        return null;
    }

    private V putForNullKey(V value) {
        for (Entry<K,V> e = table[0]; e != null; e = e.next) {
            if (e.key == null) {
                V oldValue = e.value;
                e.value = value;
                e.recordAccess(this);
                return oldValue;
            }
        }
        modCount++;
        addEntry(0, null, value, 0);
        return null;
    }

static int indexFor(int h, int length) {
        return h & (length-1);
    }

就是说,获取Entry的第一个元素table[0],并基于第一个元素的next属性开始遍历,直到找到key为null的Entry,将其value设置为新的value值。如果没有找到key为null的元素,则调用如上述代码的addEntry(0, null, value, 0);增加一个新的entry,代码如下:

void addEntry(int hash, K key, V value, int bucketIndex) {
    Entry<K,V> e = table[bucketIndex];
        table[bucketIndex] = new Entry<K,V>(hash, key, value, e);
        if (size++ >= threshold)
            resize(2 * table.length); 
    }

先获取第一个元素table[bucketIndex],传给e对象,新建一个entry,key为null,value为传入的value值,next为获取的e对象。如果容量大于threshold,容量扩大2倍。如果key不为null,这也是大多数的情况,重新看一下源码:

public V put(K key, V value) {  
        if (key == null)  
            return putForNullKey(value);  
        int hash = hash(key.hashCode());//---------------2---------------  
        int i = indexFor(hash, table.length);  
        for (Entry<K,V> e = table[i]; e != null; e = e.next) {//--------------3-----------  
            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;  
            }  
        }//-------------------4------------------  
        modCount++;//----------------5----------  
        addEntry(hash, key, value, i);-------------6-----------  
        return null;  
    }  

看源码中2处,首先会进行key.hashCode()操作,获取key的哈希值,hashCode()是Object类的一个方法,为本地方法,内部实现比较复杂,我们会在后面作单独的关于Java中Native方法的分析中介绍。hash()的源码如下:

static int hash(int h) {
        // This function ensures that hashCodes that differ only by
        // constant multiples at each bit position have a bounded
        // number of collisions (approximately 8 at default load factor). 
        h ^= (h >>> 20) ^ (h >>> 12);
        return h ^ (h >>> 7) ^ (h >>> 4);
    }

int i = indexFor(hash, table.length);的意思,相当于int i = hash % Entry[].length;得到i后,就是在Entry数组中的位置,上述代码5和6处是如果Entry数组中不存在新要增加的元素,则执行5,6处的代码,如果存在,即Hash冲突,则执行 3-4处的代码,此处HashMap中采用链地址法解决Hash冲突。具体方法可以解释为下面的这段文字:
上面我们提到过Entry类里面有一个next属性,作用是指向下一个Entry。如, 第一个键值对A进来,通过计算其key的hash得到的i=0,记做:Entry[0] = A。一会后又进来一个键值对B,通过计算其i也等于0,现在怎么办?HashMap会这样做:B.next = A,Entry[0] = B,如果又进来C,i也等于0,那么C.next = B,Entry[0] = C;这样我们发现i=0的地方其实存取了A,B,C三个键值对,他们通过next这个属性链接在一起,也就是说数组中存储的是最后插入的元素。
到这里为止,HashMap的大致实现,我们应该已经清楚了。当然HashMap里面也包含一些优化方面的实现,这里也说一下。比 如:Entry[]的长度一定后,随着map里面数据的越来越长,这样同一个i的链就会很长,会不会影响性能?HashMap里面设置一个因素(也称为因子),随着map的size越来越大,Entry[]会以一定的规则加长长度。
4.get操作

public V get(Object key) {  
        if (key == null)  
            return getForNullKey();  
        int hash = hash(key.hashCode());  
        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.equals(k)))
                return e.value;  
        }  
        return null;  
    }  

意思就是:当key为null时,调用getForNullKey(),源码如下:

private V getForNullKey() {  
        for (Entry<K,V> e = table[0]; e != null; e = e.next) {  
            if (e.key == null)  
                return e.value;  
        }  
        return null;  
    }  

当key不为null时,先根据hash函数得到hash值,在更具indexFor()得到i的值,循环遍历链表,如果有:key值等于已存在的key值,则返回其value。如上述get()代码1处判断。
总结下HashMap新增put和获取get操作:

//存储时:  
int hash = key.hashCode();  
int i = hash % Entry[].length;  
Entry[i] = value;  
  
//取值时:  
int hash = key.hashCode();  
int i = hash % Entry[].length;  
return Entry[i]; 

5.遍历
参考
简单分析Java的HashMap.entrySet()的实现
keySet 与entrySet 遍历HashMap性能差别
对于keySet其实是遍历了2次,一次是转为iterator,一次就从hashmap中取出key所对于的value。而entryset只是遍历了第一次,他把key和value都放到了entry中,所以就快了。
(1)keySet

Iterator<String> keySetIterator = 
keySetMap.keySet().iterator();
        while (keySetIterator.hasNext()) {
            String key = keySetIterator.next();
            String value = keySetMap.get(key);
            
        }

(2)entrySet

Iterator<Entry<String, String>> entryKeyIterator = 
entrySetMap.entrySet().iterator();
        while (entryKeyIterator.hasNext()) {
            Entry<String, String> e = entryKeyIterator.next();
            String value=e.getValue();
        }

entrySet()方法返回的是一个特殊的Set,定义为HashMap的内部私有类

private final class EntrySet extends AbstractSet<Map.Entry<K,V>>

HashMap的entrySet()方法返回一个特殊的Set,这个Set使用EntryIterator遍历,而这个Iterator则直接操作于HashMap的内部存储结构table上。通过这种方式实现了“视图”的功能。整个过程不需要任何辅助存储空间。从这一点也可以看出为什么entrySet()是遍历HashMap最高效的方法,原因很简单,因为这种方式和HashMap内部的存储方式是一致的。

二、hashmap hashtable treemap

Java SE中有四种常见的Map实现——HashMap, TreeMap, Hashtable和LinkedHashMap。如果我们使用一句话来分别概括它们的特点,就是:

public static void main(String args[]){
    System.out.println("map");
    testHash(new HashMap(),"HashMap");
    testHash(new Hashtable(),"Hashtable");
    testHash(new TreeMap(),"TreeMap");
}
private static void testHash(Map map,String typeStr){
    map.put("a","aaa");
    map.put("b","bbb");
    map.put("c","ccc");
    map.put("d","ddd");
    Iterator iterator = map.keySet().iterator();
    while(iterator.hasNext()){
      Object key = iterator.next();
      System.out.println(typeStr+map.get(key));
    }
    System.out.println("---------");
}
三、如果HashMap的键(key)是自定义的对象,那么需要按规则定义equals和hashCode
  • public boolean equals(Object o)
  • public int hashCode()
class Dog {
    String color;
    Dog(String c) {
   color = c;
    }
    public String toString(){   
   return color + " dog";
    }
}
public class TestHashMap {
    public static void main(String[] args) {
   HashMap hashMap = new HashMap();
   Dog d1 = new Dog("red");
   Dog d2 = new Dog("black");
   Dog d3 = new Dog("white");
   Dog d4 = new Dog("white");
   hashMap.put(d1, 10);
   hashMap.put(d2, 15);
   hashMap.put(d3, 5);
   hashMap.put(d4, 20);
   //print size
   System.out.println(hashMap.size());
   //loop HashMap
   for (Entry entry : hashMap.entrySet()) {
       System.out.println(entry.getKey().toString() 
       + " - " + entry.getValue());
   }
    }
}
//结果:
4
white dog - 5
black dog - 15
red dog - 10
white dog - 20

注意,我们错误的将”white dogs”添加了两次,但是HashMap却接受了两只”white dogs”。这不合理(因为HashMap的键不应该重复),我们会搞不清楚真正有多少白色的狗存在。

class Dog {
    String color;
 
    Dog(String c) {
        color = c;
    }
 
    public boolean equals(Object o) {
        return ((Dog) o).color == this.color;
    }
 
    public int hashCode() {
        return color.length();
    }
 
    public String toString(){   
        return color + " dog";
    }
}
//现在输出结果如下
3
red dog - 10
white dog - 20
black dog - 15

输出结果如上是因为HashMap不允许有两个相等的元素存在。默认情况下(也就是类没有实现hashCode()和equals()方法时),会使用Object类中的这两个方法。Object类中的hashCode()对于不同的对象会返回不同的整数,而只有两个引用指向的同样的对象时equals()才会返回true。
另外,关于这两个方法,可以参考java中正确使用equals和hashCode

三、hashtable实际使用

1.参考现在搞java的人,还有用vector和hashtable的嘛?

一直很好奇。感觉这俩是只在教科书上出现过的东西。实际中从没见过有人使用。但很多人面试时候总会问这俩东西。如果从深入理解的角度来说了解一下无妨。但是从应用角度来说意义不大。

不需要线程同步有 ArrayList / HashMap,需要线程同步有 java.util.concurrent 的 ConcurrentHashMap / CopyOnWriteArrayList 或者根据代码行为进行手工同步 [1] 。这种被淘汰的东西当然不应该用。
2.嵌套使用
参考java Map集合嵌套,value为Map和value为List

import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;


public class MapDemo {

    public static void main(String[] args) {
        System.out.println("Map集合的值为Map");
        oneToMap();
        
        System.out.println("Map集合的值为List,特别常用!必须会!");
        oneToList();
    }
    
    /*
     * 这种是把班级和学生作为映射
     * 而且又把学号和姓名做了映射
     */
    public static void oneToMap() {
        Map<String,Map<String,String>> jiSuanJi = 
        new HashMap<String,Map<String,String>>();
        Map<String,String> ruanJian = new HashMap<String,String>();
        Map<String,String> wangZhan = new HashMap<String,String>();
        
        /*
         * 千万不要每次都new一个进去,这样就不是原来的集合了
         * 结果yingyong这个key对应的value集合是null
         * 遍历Map的时候还会出现空指针错误
         */
        //jiSuanJi.put("yingyong", (Map<String, String>) 
        //new HashMap().put("01", "haha"));
        //jiSuanJi.put("yingyong", (Map<String, String>) 
        //new HashMap().put("02", "xixi"));
        
        /*
         * 要使用下面这种方式,先把集合定义好,
         * 把映射关系设置好,再去给集合添加元素
         */
        jiSuanJi.put("ruanJian", ruanJian);
        jiSuanJi.put("wangZhan", wangZhan);
        
        ruanJian.put("01", "zhangsan");
        ruanJian.put("02", "lisi");
        
        wangZhan.put("01", "zhaoliu");
        wangZhan.put("02", "zhouqi");
        
        Set<String> keySet = jiSuanJi.keySet();
        for(Iterator<String> it = keySet.iterator();it.hasNext();) {
            String key = it.next();
            System.out.println(key);
            Map<String,String> map = jiSuanJi.get(key);
            Set<Map.Entry<String, String>> entrySet = map.entrySet();
            for(Iterator<Map.Entry<String, String>> it2 
                = entrySet.iterator();it2.hasNext();) {
                Map.Entry<String, String> me = it2.next();
                System.out.println(me.getKey() + ".." + me.getValue());
            }
        }
    }
    
    /*
     * 这种把班级和学生做了映射
     * 学生类中封装了学号和姓名
     */
    public static void oneToList() {
        Map<String,List<PersonDemo>> jiSuanJi = 
        new HashMap<String,List<PersonDemo>>();
        List<PersonDemo> ruanJian = new ArrayList<PersonDemo>();
        List<PersonDemo> wangZhan = new ArrayList<PersonDemo>();
        
        jiSuanJi.put("ruanJian", ruanJian);
        jiSuanJi.put("wangZhan", wangZhan);
        
        ruanJian.add(new PersonDemo("01","zhangsan"));
        ruanJian.add(new PersonDemo("02","lisi"));
        wangZhan.add(new PersonDemo("01","wangwu"));
        wangZhan.add(new PersonDemo("02","zhaoliu"));
        
        Set<String> keySet = jiSuanJi.keySet();
        for(Iterator<String> it = keySet.iterator();it.hasNext();){
            String key = it.next();
            System.out.println(key);
            List<PersonDemo> list = jiSuanJi.get(key);
            for(Iterator<PersonDemo> it2 = list.iterator();it2.hasNext();) {
                PersonDemo pd = it2.next();
                System.out.println(pd);
            }
        }
    }
    
}

class PersonDemo {
    
    private String id;
    private String name;
    
    public PersonDemo(String id, String name) {
        super();
        this.id = id;
        this.name = name;
    }
    public String getId() {
        return id;
    }
    public void setId(String id) {
        this.id = id;
    }
    public String getName() {
        return name;
    }
    public void setName(String name) {
        this.name = name;
    }
    @Override
    public String toString() {
        return this.id + "..." + this.name;
    }
}
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,547评论 6 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,399评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,428评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,599评论 1 274
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,612评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,577评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,941评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,603评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,852评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,605评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,693评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,375评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,955评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,936评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,172评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 43,970评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,414评论 2 342

推荐阅读更多精彩内容