Map分为Hashmap,Treemap,Hashmap下又有HashTable
1.Hashmap
特点:hashmap在存入数据时,无序且不能重复
按照K进行排序,底层key遵循哈希表的结构(数组加链表)
相关方法:
增加:put(k,v)
删除:clear(),remove()
查看:entryset():查看map全部(k,v),KeySet(), size(),values()
判断:containKey(),ContainValue(),equals(),isEmpty()
下面是对entryset()方法的使用以及之后分别遍历k,v的方法
Set<Map.Entry<String,Integer>> t4 = t1.entrySet(); for (Map.Entry<String,Integer>e:t4){ System.out.println(e.getKey()+"--"+e.getValue()); }
2.其中不能重复的原因且排序的原因
不重复需要重写那个类的equal()和hashcode()方法
@Override public boolean equals(Object o) { if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; Student student = (Student) o; return age == student.age && Objects.equals(name, student.name); } @Override public int hashCode() { return Objects.hash(name, age); }
3.LinkedHashmap
特点:有序,按照输入的顺序输出
4.TreeMap
1)特点:唯一,有序(按照升序或者降序)
原理:二叉树,key按照二叉树的特点放入集合的key的数据类型对应。的数据内部一定要重写毕比较器(外部比较器或者内部比较器),下面是快速生成比较器的方法
Map<Student,Integer> map = new TreeMap<>(new Comparator<Student>() { @Override public int compare(Student o1, Student o2) { return ((Double)o1.getHigh()).compareTo((Double)o2.getHigh()); }
5.Hashtable
1)Hashmap:JDK1.2,效率高,线程不安全,key可以存放null值
Hashtable:JDK1.0,效率低,线程安全,key不能为null
6.HashMap的底层原理
1)代码(摘要)
package Map.map.Hashmap; import java.io.Serializable; import java.util.AbstractMap; import java.util.Map; public class HashMap<K,V> extends AbstractMap<K,V> implements Map<K,V>,Cloneable, Serializable { //重要属性 static final int DeFAULT_INITIAL_CAPACITY = 16 ; //定义了一个16,一会要赋给数组的 static final int MAXMUM_CAPACITY =1; //定义了一个很大的数 static final float DEFAULT_LOAD_FACTOR = 0.75f;//负载因子,加载因子,用来判断自动扩容 transient Entry<K,V>[] table;//底层主数组 transient int size; //添加的元素的数量 int shthreshould; //定义变量,默认为0,用来判断扩容的边界值,门槛值 final float loadFactor; //这个变量用来接收;装载因子,负载因子,加载因子 //构造器 //1).空构造器,一般调用这个 public HashMap(){this(DeFAULT_INITIAL_CAPACITY,DEFAULT_LOAD_FACTOR);} //2).带参构造器 public HashMap(int initialCapacity,float loadFactor){//传入自定义数组长度,负载因子 if (initialCapacity > MAXMUM_CAPACITY) initialCapacity = MAXMUM_CAPACITY; if (loadFactor<=0 || Float.isNaN(loadFactor)) throw new IllegalAccessException("illegqal load factor:" + loadFactor); int capacity = 1; while (capacity<initialCapacity) capacity<<=1; //如果小于输入的数组长度,就自增一倍 this.loadFactor = loadFactor; threshold=(int)Math.min(capacity*loadFactor,MAXMUM_CAPACITY+1);//取自定义的负载因子乘当前数组的最大二次幂算出扩容边界值 table = new Entry[capacity]; //将底层数组放入,生成一个Entry对象,内部由4个属性(key,value,哈希值,地址) } //存储数据的方法 public V put(K key,V value){//K:Integer V:String //对key进行判断是否为空,允许为空 if(key == null) return putforNullKey(Value); //获取key的哈希码 int hash = hash(key); //得到元素在数组中的位置 int i = indexFor(hash,table.length); //如果放入的数组的位置上没有元素的话,那么直接添加就行了,不用走下面的for循环 //如果满足e!= null的话,说明位置上已经有东西了 for(Entry<K,V> e = table[i];e! = null;e = e.next){ Object k; //发生哈希碰撞的时候,会先比较哈希值 //比较key是否是一个对象,如果key是一个对象的话,equals就不比较了 //如果不是同一个对象,会比较equals方法 //如果hash值一样,equals值也一样,就会走这个 if (e.hash == hash && ((k = e.key) == key || key.equals(k))){ V oldValue = e.value; e.Value = value; e.recordAccess(this); return oldValue; } } addEntry(hash,key,value,i); //哈希码,key,value,以及数组位置 return null; } final int hash(Object k){ int h = 0; if(useAltHashing){ if (k instanceof String){ return sun.misc.Hashing,stringHash32((String) k); } h = hashCode(); } //二次散列,没有直接用hashCode的值,解决哈希冲突 h ^= k.hashCode(); //扰动函数,核心思想,增加值的不确定性 h ^= (h>>>20) ^ (h>>>1); return h ^(h>>>7) ^ (h>>>1); } static int indexFor(int h,int length){ return h&(length-1);//计算位置对应的公式 等效于:h&length求余 因为效率低,所以用 &运算 } //添加元素的方法: void addEntry(int hash,K key,V value,int bucketIndex){ //如果size>=threshold这个变量不满足,这个if不走 if ((size>=threshold) && (null != table[bucketIndex])){ resize(2*table.length); hash = (null != key) ?hash(key) : 0 ; bucketIndex = indexFor(hash,table.length); } //走这里创建Entry对象: createEntry(hash,key,value,bucketIndex); } void createEntry(int hash,K key,V value,int bucketIndex){ //将下标位置上的元素给e Entry<K,V> e = table[bucketIndex]; //封装对象,将这个对象给table[bucketIndex] table[bucketIndex] = new Entry<>(hash,key,value,e); size++; } }
2)从代码中可以看出每一个存入Map的元素其实可以看做一个封装的值,包括(hash,k,y,index)分别对应哈希值,key,value,数组中所在的位置。
哈希值:对应hash()方法,主要的参数为底层数组中的key值,经过hashCode()方法求出哈希值,得到值后通过二次散列和扰动函数方法解决可能出现的哈希冲突,简单了来说就是通过方法得出更多的不同的哈希值,防止哈希冲突
哈希冲突:元素key值相同或者key值求出来的哈希值相等
final int hash(Object k){ int h = 0; if(useAltHashing){ if (k instanceof String){ return sun.misc.Hashing,stringHash32((String) k); } h = hashCode(); } //二次散列,没有直接用hashCode的值,解决哈希冲突 h ^= k.hashCode(); //扰动函数,核心思想,增加值的不确定性 h ^= (h>>>20) ^ (h>>>1); return h ^(h>>>7) ^ (h>>>1); }
key:存入数据中的key值,其中涉及到值的存储,以及发生哈希冲突,哈希碰撞的解决方法
方法:存入数据时,会先判断key是否为空(允许为空),获取哈希码,通过indefor方法获取在数组中的位置,判断位置上是否有元素,如果没有就直接放入,如果有,就比较哈希值,哈希值相同接着判断是否是同一个对象,不是则继续比较值,如果值和hash值都一样,就将后插入的值替换掉老值。
存储元素的方法:
public V put(K key,V value){//K:Integer V:Stringinde //对key进行判断是否为空,允许为空 if(key == null) return putforNullKey(Value); //获取key的哈希码 int hash = hash(key); //得到元素在数组中的位置 int i = indexFor(hash,table.length); //如果放入的数组的位置上没有元素的话,那么直接添加就行了,不用走下面的for循环 //如果满足e!= null的话,说明位置上已经有东西了 for(Entry<K,V> e = table[i];e! = null;e = e.next){ Object k; //发生哈希碰撞的时候,会先比较哈希值 //比较key是否是一个对象,如果key是一个对象的话,equals就不比较了 //如果不是同一个对象,会比较equals方法 //如果hash值一样,equals值也一样,就会走这个 if (e.hash == hash && ((k = e.key) == key || key.equals(k))){ V oldValue = e.value; e.Value = value; e.recordAccess(this); return oldValue; } } addEntry(hash,key,value,i); //哈希码,key,value,以及数组位置 return null; }
3)获取在数组中的位置
就是判断hashcod,qiuyu5e对数组长度求雨,得到位置
static int indexFor(int h,int length){ return h&(length-1);//计算位置对应的公式 等效于:h&length求余 因为效率低,所以用 &运算 }
4)其他方法:
构造器:需要一个构造器,创建底层数组,可以自定义获取使用默认的空构造器
默认构造器:其中加载因子为0.75,数组长度为16
自定义构造器:自己传入加载因子和数组初始长度
扩容边界值:加载因子*数组长度
基本扩容1:直接判断数组长度,如果大于16,则直接长度*2.
基本扩容2:实际数组长度>=加载因子*初始数组长度,就扩展2倍,为了防止哈希冲突,因为当实际数组长度>=加载因子*初始数组长度时,很容易发生哈希碰撞(数组过小,实际数组都占的差不多了,很容易得到hash值相同)。
为什么加载因子不为1或者0.5,因为过小会导致空间浪费,过小就直接扩容了,过大会导致hash冲突,取中解决相对效果较好一些。
为什么扩容必须是原数组的两倍,不是其他倍数呢。
防止hash冲突,位置冲突,h%(length-1)等效于H%length求余的前提是length必须是2的倍数
例子:2的整数倍
2的
legth :8
hash 3 00000011
length-1 00000111
位置 00000011
hash2 00000010
length-1 00000111
位置 00000010
不是2的整数倍
length 9
hash 3 00000011
length-1 00001000
位置 00000000
hash 2 00000010
length-1 00001000
位置 00000000
s//构造器 //1).空构造器,一般调用这个 public HashMap(){this(DeFAULT_INITIAL_CAPACITY,DEFAULT_LOAD_FACTOR);} //2).带参构造器 public HashMap(int initialCapacity,float loadFactor){//传入自定义数组长度,负载因子 if (initialCapacity > MAXMUM_CAPACITY) initialCapacity = MAXMUM_CAPACITY; if (loadFactor<=0 || Float.isNaN(loadFactor)) throw new IllegalAccessException("illegqal load factor:" + loadFactor); int capacity = 1; while (capacity<initialCapacity) capacity<<=1; //如果小于输入的数组长度,就自增一倍 this.loadFactor = loadFactor; threshold=(int)Math.min(capacity*loadFactor,MAXMUM_CAPACITY+1);//取自定义的负载因子乘当前数组的最大二次幂算出扩容边界值 table = new Entry[capacity]; //将底层数组放入,生成一个Entry对象,内部由4个属性(key,value,哈希值,地址) }
void addEntry(int hash,K key,V value,int bucketIndex){ //如果size>=threshold这个变量不满足,这个if不走 if ((size>=threshold) && (null != table[bucketIndex])){ resize(2*table.length); hash = (null != key) ?hash(key) : 0 ; bucketIndex = indexFor(hash,table.length); } //走这里创建Entry对象: createEntry(hash,key,value,bucketIndex); } void createEntry(int hash,K key,V value,int bucketIndex){ //将下标位置上的元素给e Entry<K,V> e = table[bucketIndex]; //封装对象,将这个对象给table[bucketIndex] table[bucketIndex] = new Entry<>(hash,key,value,e); size++; }