HashMap

hashmap

JDK1.7采用的是头插法,容易产生环形链和数据丢失问题

JDK1.8采用的是尾插法,容易产生数据丢失问题

HashMap的实现原理

散列或者哈希:以节点的key值为自变量,通过一定的函数关系(散列函数)计算出对应的函数值,以这个值为该节点存储在散列表中的地址

JDK1.7采用的是数组+链表,而JDK1.8采用的是数组+链表+红黑树

HashMap在JDK1.7和JDK1.8的区别

JDK1.7JDK1.8
存储结构数组+链表数组+链表+红黑树
初始化方式单独函数inflateTable()直接集成到扩容函数resize()中
hash值的计算方式(扰动处理)9次扰动处理2次扰动处理
存放数据的规则无冲突时存放数组,有冲突时存入链表无冲突时存放数组,冲突 & 链表的长度 < 8 时存放单链表,冲突 & 链表的长度 > 8 时树化并存放到红黑树
插入数据的方法头插法尾插法
扩容处理将全部节点按照原来的方法重新计算位置按照扩容后的规律计算

Hashtable

类定义

继承的是字典类,而不是AbstractMap。是从JDK1.0开始提供的一个古老的类,目前已经不再建议使用了

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

hashtable的绝大多数方法上都有同步约束,所以线程安全的,但是hashtable线程安全的策略实现代价太大了。get和put所有相关操作都是synchronized的,相当于给整个hashtable添加一个所,多线程并发时只能有一个线程访问,其它线程只能阻塞等待,相当于将所有操作串行化,性能非常差

存储数据

使用Entry数组,hashmap采用的是Node数组

private transient Entry<?,?>[] table;

hashtable也是一个散列表,存储内容是key-value映射,通过链地址法实现的哈希表

hashtable中不允许null值的key和value

初始化操作

public Hashtable() { //初始化容积值为11,加载因子为0.75 
    this(11, 0.75f); 
}
public Hashtable(int initialCapacity) { //允许自定义初始化容积值 
    this(initialCapacity, 0.75f); 
}
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); 
    //如果初始化容积为0,则默认为1 
    if (initialCapacity==0)
        initialCapacity = 1; 
    this.loadFactor = loadFactor; 
    table = new Entry<?,?>[initialCapacity]; //初始化数组,不是hashmap的懒加载 
    threshold = (int)Math.min(initialCapacity * loadFactor, MAX_ARRAY_SIZE + 1); //获取扩容的阈值,默认值为[初始化容积*负载因子],最大值为Integer.MAX_VALUE - 8+1 
}

添加元素

public synchronized V put(K key, V value) { 
    //同步约束 
    //不允许value值为null,否则异常 
    if (value == null) { 
        throw new NullPointerException(); 
    }
    // Makes sure the key is not already in the hashtable. 
    Entry<?,?> tab[] = table; 
    int hash = key.hashCode(); //直接调用key的hashCode方法以获取hash值,不允 许key为null,否则异常 
    int index = (hash & 0x7FFFFFFF) % tab.length; //通过求余计算获取元素在 数组中的存储下标位置。为什么不使用 hash & (length-1) //遍历对应位置上的链表 
    Entry<K,V> entry = (Entry<K,V>)tab[index]; 
    for(; entry != null ; entry = entry.next) {
        if ((entry.hash == hash) && entry.key.equals(key)) { 
            V old = entry.value; 
            entry.value = value; //如果key值冲突,则后盖前,并返回被覆盖的旧数据 
            return old; 
        } 
    }//如果链表中不存在数据,则进行节点的添加。节点添加采用的是头插法 
    addEntry(hash, key, value, index); 
    return null; 
}

基础用法参照map的用法

Map<String, Integer> map1 = new HashMap<>(); 
Map<String, Integer> map2 = new Hashtable<>(); 
//在hashmap中键和值都允许为null,只是key中只能有一个null 
map1.put(null, 123); 
map1.put("ddd", null); 
System.out.println(map1.get(null)); 

map2.put(null, 999); //在hashtable中key不允许为null,否则NullPointerException 
map2.put("ccc",null); //在hashtable中value也不允许为null,否则 NullPointerException 
System.out.println(map2.get(null));

Hashtable和HashMap的区别

  • 线程安全问题:HashMap不是线程安全的,Hashtable线程安全的;
    • 目前已经不建议使用Hashtable得到线程安全的特性,一般建议并发量不大时采用Collections.synchronizedMap,或者使用ConcurrentHashMap
  • 效率:因为线程安全问题,HashMap的执行效率比Hashtable高一些,目前Hashtable基本被淘汰,已经不建议使用
  • 空值和空键:HashMap中允许null的key,但是只能有一个,允许null的value,个数没有限制;Hashtable中不允许null的key和value,否则异常
  • 初始化容积和扩容处理
    • 使用无参构造器不指定初始化容积,Hashtable的默认初始化大小为11,之后扩容容量变为2n+1;HashMap默认的初始化大小为16,而且采用的是懒加载,之后扩容容量变为2n
    • 使用初始化容积作为参数,那么Hashtable直接使用给定的值作为初始化大小,而HashMap会针对初始化容积进行计算,将其转换为2的n次方,使用2的幂作为哈希表的大小
  • 底层数据结构:JDK1.8HashMap中解决哈希冲突时引入红黑树,以减少搜索时间; Hashtable没有
    • JDK1.8中使用的Node,JDK1.7使用的是Entry
  • 推荐使用:在Hashtable的类注释中可以看到,Hashtable是保留类,已经不建议使用;推荐在单线程环境下使用HashMap替代,如果需要多线程使用ConcurrentHashMap

TreeMap

TreeMap用于实现一个key-value的有序集合,是通过红黑树实现的。该Map根据key的自然顺序进行排序【Comparable接口】,同时允许根据创建Map时提供的Comparator比较器进行排序,具体使用取决于所使用的构造器

public class TreeMap<K,V> extends AbstractMap<K,V> implements NavigableMap<K,V>, Cloneable, java.io.Serializable { 
    private transient Entry<K,V> root; //具体存放数据 
    private final Comparator<? super K> comparator;//排序的比较器 
    private transient int size = 0; //元素个数 
    private transient int modCount = 0; //修改次数,为了实现fail-fast

类定义

  • 继承于AbstractMap同时实现了Map接口,所以是一个Map类型,其中存储key-value对

  • 实现了NavigatableMap接口,意味着支持一系列的导航方法,比如获取有序的key集合

  • 实现了Cloneable接口,意味着支持浅克隆操作

  • 实现了Serializable接口,意味着支持序列化和反序列化操作

节点定义

static final class Entry<K,V> implements Map.Entry<K,V> { 
    K key; //节点中存储的key值 
    V value;//节点中存储的对应key的value值 
    Entry<K,V> left; //左子树 
    Entry<K,V> right; //右子树 
    Entry<K,V> parent; //当前节点的父节点 
    boolean color = BLACK; //节点的颜色,红黑树
}

常用方法

  • put(K key, V value)用于添加一个Entry节点,其中key为指定的value所关联的键,value为指定key对应的值,如果key值已经存在,则将key对应的value修改为新值

  • V get(K key)用于获取对应key的value值

特殊方法

  • firstEntry返回TreeMap中最小的key

  • higherEntry(K key)获取位于key后一位的key-value

  • lowerEntry(K key)获取位于指定key前一个的key-value

  • … …

HashMap通过hashCode值对其内容进行快速查找,而TreeMap中的所有元素会保持其固定的顺序。如果需要得到一个有序的结果集则应该使用TreeMap,注意:key需要实现Comparable接口或者创建TreeMap时需要指定实现Comparator接口的比较器

  • HashMap和key类型中的hashCode和equals方法相关

  • TreeMap和key类型中的compareTo方法相关。一般建议如果定义Comparable接口实现时,最好保证当compareTo返回0时,equals为真,避免产生二义性

HashMap通常比TreeMap效率要高一些,HashMap采用的是hash表,而TreeMap采用的是红黑树。建议多使用HashMap,只有在需要排序时才使用TreeMap

Map<Integer,String> map=new TreeMap<>(); 
Random r=new Random(); 
for(int i=0;i<10;i++){ 
    map.put(r.nextInt(100), i + "-value"); 
    if(i==5) 
        map.put(111, "value"); //后盖前 
}
System.out.println(map); //会自动按照key值进行比较排序,如果key值相等,则后盖前;使用 中序遍历输出数据,从而显示key值有序

如果key对应的类型定义并没有实现Comparable接口,则需要在定义TreeMap时指定对应的Comparator比较器,否则报出异常

使用自定义比较器的编程实现

//既可以使用匿名内部类指定对应的比较器,也可以使用lambda表达式进行定义,因为Comparator是 函数式接口@FunctionalInterface 
Map<Integer, String> map = new TreeMap<>(new Comparator<Integer>() { 
    public int compare(Integer o1, Integer o2) { 
        // return o1.compareTo(o2);//调用Integer类中定义的compareTo方法进行 比较 
        return o2.compareTo(o1); 
    } 
});

使用lambda表达式自定义比较器

// 如果Person类充当value,则对于Person类没有任何特殊要求 
// Map<String, Person> map = new TreeMap<>(); 
Map<Person, String> map = new TreeMap<>(); 
map.put(new Person(), "bbbb"); //运行时异常ClassCastException 

// 如何解决?在构建TreeMap对象时可以使用自定义比较器 
Map<Person, String> map2 = new TreeMap<>((obj1, obj2) -> { 
    //根据业务规则定义对应的比较方法,例如按照id倒序排序,如果id相等则按照name 正序 
    int res=obj2.getId().compareTo(obj1.getId()); 
    if(res==0){ 
        res=obj1.getName().compareTo(obj2.getName()); 
    }
    return res; 
});

HashMap、Hashtable和TreeMap

1、元素特性:Hashtable中的key和value都不允许为null,HashMap中的key和value都可以为null,TreeMap中当key没有实现Comparator接口或者实现Comparable接口但是没有对null情况进行判断处理,则key不允许为null

2、顺序特性:Hashtable和HashMap具有无序性【不是插入顺序而且随着增删操作顺序会变】,TreeMap采用红黑树实现的,默认按照key的升序排序【自然序】

3、初始化和扩容:Hashtable默认容积11,不要求容积为2的整数次幂;HashMap默认容量为16,要求容量必须是2的整数次幂;扩容时hashtable时2倍+1,hashmap是2倍;TreeMap没有限制【int】,也没有扩容处理

4、线程安全:hashtable是同步的,但是在多线程环境下效率表现非常低下,新版本中已经不推荐使用;hashmap没有同步处理,所以扩容时容易出现rehash操作死循环、脏读丢失数据和size不精确的问题。TreeMap线程不安全,有可能出现死循环,可以使用ConcurrentSkipListMap

LinkedHashMap

LinkedHashMap是HashMap的子类,LinkedHashMap额外维护了一个运行于所有条目的双向链表,默认存储的是数据的插入顺序

Map<Integer, String> map = new LinkedHashMap<>(); 
	Random r = new Random(); 
	for (int i = 0; i < 10; i++) 
   		map.put(r.nextInt(100), i + "-value"); 
System.out.println(map); 

Map<Integer,String> map2=new HashMap<>(); 
	for (int i = 0; i < 10; i++) 
    	map2.put(r.nextInt(100), i + "-value"); 
System.out.println(map2);

HashMap是无序的,如果需要记录顺序的方式存取key-value可以使用LinkedHashMap

特殊的构造器

public LinkedHashMap(int initialCapacity, float loadFactor, boolean accessOrder) 参数accessOrder涉及到LinkedHashMap存储的顺序,具体的存储顺序可以分为插入数据的顺序(false)和访问数据的顺序(true,调用get方法会导致数据位置的移动)。如果accessOrder值为false,是默认值,表示采用插入顺序,表示LinkedHashMap双向链表中存储的是调用put方法插入数据的顺序进行排序

设置采用访问顺序

如果一个key被访问过则自动调整位置到默认位置的末尾,如果不进行访问则插入顺序一致

Map<Integer,String> map=new LinkedHashMap(10,0.75f,true); 
Random r=new Random(); 
map.put(111,"value1"); 
for(int i=0;i<10;i++) 
    map.put(r.nextInt(100),i+"-value"); 
System.out.println(map.get(111));//如果访问过111则自动排到双向链的末尾 
System.out.println(map);

如果删除链头的数据,是不是就是一种缓存管理策略的实现:LRU最近最少使用

总结:

  • LinkedHashMap是继承于HashMap,是基于HashMap存储和双向链表记录顺序来实现

    • 存储位置和双向链上的位置没有关闭
  • HashMap是无序的,LinkedHashMap有序,可以分为插入顺序和访问顺序两种

  • LinkedHashMap存取数据和HashMap一致,双向链表是在存储位置的基础上附加记录顺序

  • LinkedHashMap线程不安全

ConcurrentHashMap

重点、难点

ConcurrentHashMap属于juc包【java.util.concurrent】,主要用于应对java.util.HashMap在高并发应用场景下的线程不安全的问题

  • HashMap线程不安全,一般针对少量并发访问可以使用Collections.synchronizedMap(new HashMap())达到线程安全的目的,这里只使用一个全局锁达到方法互斥的目的,锁的颗粒度太大,会影响线程的并发性

  • 如果高并发时优先考虑使用ConcurrentHashMap

  • ConcurrentHashMap的设计和实现大量的利用 volatile、final、CAS等lock-free技术减少锁竞争对于性能的影响

ConcurrentHashMap避免了对全局加锁造成的并发性能问题,改成了局部加锁,可以极大的提高并发环境下的操作速度

ConcurrentHashMap结合了HashMap和Hashtable二者的优势,HashMap没有考虑同步,Hashtable考虑了同步问题。但是Hashtable在每次同步执行时都要锁住整个数据结构,ConcurrentHashMap的锁方式是一种细粒度的锁机制,执行并发性能好

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值