Map接口:存储的是键值对形式,key-value键值对存在,key值是不能重复的,value是可以重复的。
Map接口下方法:
集合中常用方法解释:
// V put(K key, V value) 向集合中添加键值对
hashMap.put("A","a");
System.out.println(hashMap.size());
//void clear() 将集合元素清空处理
// hashMap.clear();
//boolean containsKey(Object key) 判断集合中是否存在指定的键 key 存在返回true 不存在返回false
boolean key = hashMap.containsKey("A");
// System.out.println(key);
//boolean containsValue(Object value) 判断集合中是否存在指定的值value 存在返回true 不存在返回false
hashMap.containsValue("zhangsna");
// System.out.println(hashMap.size());
//V get(Object key) 通过键key获取value
String s = hashMap.get("A");
Map接口下集合的遍历形式:
//通过entrySet遍历键值对
Iterator <Map.Entry <String, String>> iterator = hashMap.entrySet().iterator();
while (iterator.hasNext()) {
Map.Entry <String, String> entry = iterator.next();
System.out.println("key:"+entry.getKey()+",value:"+entry.getValue()+ " ");
}
//通过keySet遍历键
Iterator <String> iterator1 = hashMap.keySet().iterator();
while (iterator1.hasNext()) {
String key1 = iterator1.next();
System.out.print(key1+" ");
}
//通过values()遍历值
Iterator <String> iterator2 = hashMap.values().iterator();
while (iterator2.hasNext()) {
String value = iterator2.next();
System.out.print(value+" ");
}
HashMap的介绍
特点:
1、存储的数据是键值对形式,key不能重复,value值可以重复
2、key和value都可以为null
3、不能保证内部元素的顺序
4、底层数据结构是哈希表
哈希表:key通过哈希函数映射到特定值的数据结构
哈希冲突:哈希函数f(x) ,f(m) =f(n) ,m不等于n
哈希函数:直接哈希,取模。。。
哈希冲突:链地址法,探测法(线性探测、随机探测)。。。
链地址法:如图所示:
HashMap源码研究
继承关系
public class HashMap<K,V>
extends AbstractMap<K,V>
implements Map<K,V>, Cloneable, Serializable
HashMap继承自AbstractMap,该抽象类对Map接口的常用方法做了实现,方便子类复用
实现类Map接口,具有Map中提供的所有方法
实现Cloneable和Serializable接口
构造函数
//通过初始容量和加载因子来实例HashMap对象
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);
this.loadFactor = loadFactor;
threshold = initialCapacity;
init();
}
//通过初始容量来实例HashMap,加载因子是默认值0.75
public HashMap(int initialCapacity) {
this(initialCapacity, DEFAULT_LOAD_FACTOR);
}
//通过无参构造实例化 默认初始容量16 默认加载因子0.75
public HashMap() {
this(DEFAULT_INITIAL_CAPACITY, DEFAULT_LOAD_FACTOR);
}
//通过map集合实例构造HashMap实例
public HashMap(Map<? extends K, ? extends V> m) {
this(Math.max((int) (m.size() / DEFAULT_LOAD_FACTOR) + 1,DEFAULT_INITIAL_CAPACITY),
DEFAULT_LOAD_FACTOR);
//初始化table、threshold参数,实例化哈希表
inflateTable(threshold);
//将map结合中的键值对存放到HashMap中
putAllForCreate(m);
}
属性及默认值
//默认初始容量 :值为16
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16
//最大容量
static final int MAXIMUM_CAPACITY = 1 << 30;
//默认的加载因子:0.75
static final float DEFAULT_LOAD_FACTOR = 0.75f;
//默认空表
static final Entry<?,?>[] EMPTY_TABLE = {};
//存放数据 table属性,是entry类型数组
transient Entry<K,V>[] table = (Entry<K,V>[]) EMPTY_TABLE;
//存放的键值对个数
transient int size;
//扩容阈值 ,在扩容时判断 threshold =table.length()*loadFactor
int threshold;
//加载因子
final float loadFactor;
//版本控制
transient int modCount;
//key哈希相关
static final int ALTERNATIVE_HASHING_THRESHOLD_DEFAULT = Integer.MAX_VALUE;
//key哈希相关
transient int hashSeed = 0;
底层数据结构
//存放数据位置
transient Entry<K,V>[] table = (Entry<K,V>[]) EMPTY_TABLE;
static class Entry<K,V> implements Map.Entry<K,V> {
final K key;
V value;
Entry<K,V> next;
int hash;
}
通过以上可知:HashMap底层存放数据是数据加链表形式,即是哈希表结构
扩容机制
扩容时机:在size>threshold时,会进行扩容
常用方法
插入元素:put(key,value)
public V put(K key, V value) {
if (table == EMPTY_TABLE) {
//当table数组为空时,进入初始化table数据,当第一次调用put操作会进入
inflateTable(threshold);
}
//key为null,将数据存放在0号卡槽位置
if (key == null)
return putForNullKey(value);
//key不为null的处理
//通过key找到对应的存放卡槽位置
int hash = hash(key);
int i = indexFor(hash, table.length);
for (Entry<K,V> e = table[i]; e != null; e = e.next) {
//通过位置i找到卡槽i位置的数据链表,从头遍历,找key是否存在
//判断条件是hash和key,相等值更新
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;
}
}
//key在i位置不存在,需要新增entry节点存放数据
modCount++;
addEntry(hash, key, value, i);
return null;
}
private V putForNullKey(V value) {
//key为null将哈希位置为0号位置,需要遍历0号卡槽链表、判断key为null是否存在
//存在将entry中value进行跟新,返回旧的value中
//不存在则新建entry实体,存放put的数据
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;
}
//通过key哈希找到对应卡槽
final int hash(Object k) {
int h = hashSeed;
if (0 != h && k instanceof String) {
return sun.misc.Hashing.stringHash32((String) k);
}
h ^= k.hashCode();
// 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);
}
通过key的哈希值找到在哈希表中的位置
static int indexFor(int h, int length) {
// assert Integer.bitCount(length) == 1 : "length must be a non-zero power of 2";
容量是16
0001 0000 16
0000 1111 15
1010 1001
-------------
0000 1001 9
return h & (length-1);
}
void addEntry(int hash, K key, V value, int bucketIndex) {
//当存放数据size大于扩容阈值threshold,进行扩容
if ((size >= threshold) && (null != table[bucketIndex])) {
//对HashMap进行2倍的扩容
resize(2 * table.length);
hash = (null != key) ? hash(key) : 0;
//获取当前key应该插入的新卡槽位置
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;
}
//新创建数组为原来的2倍
Entry[] newTable = new Entry[newCapacity];
//将原map中的每一个entry实体进行重新哈希到新的map中
transfer(newTable, initHashSeedAsNeeded(newCapacity));
table = newTable;
//threshold = table.length*loadFactor
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++;
}
put操作步骤:
1、先判断key是否为null,特殊处理,存放在哈希表的0号卡槽位置
2、key为null的数据是否存在,遍历0号卡槽位置的链表,如果存在(==)则更新value,返回
3、如果不存在,新建节点(addentry)
4、key不为null,通过key来计算哈希值,找到在哈希表的卡槽位置(hash,indexFor)
5、在对应卡槽获取链表,遍历找key是否存在,如果存在(hash&&equals)则更新value,返回
6、key在链表不存在,新建节点(addEntry)
7、考虑是否扩容(size>threshold),需要扩容,将新的大小为原来的2倍,然后将原哈希表中的数据都重新哈希到新的哈希表中, 并更新当前插入节点的卡槽位置
8、 采用头插将新entry节点插入到给定的卡槽位置
获取元素:get(key)
public V get(Object key) {
//key为null,直接到0号卡槽位置获取结果
if (key == null)
return getForNullKey();
//key不为null
Entry<K,V> entry = getEntry(key);
return null == entry ? null : entry.getValue();
}
private V getForNullKey() {
//如果map为空,返回null
if (size == 0) {
return null;
}
//在0号卡槽位置,对链表遍历,查找key为null是否存在,存在则找entry中value返回,否则返回null
for (Entry<K,V> e = table[0]; e != null; e = e.next) {
if (e.key == null)
return e.value;
}
return null;
}
通过key找到对应entry实例
final Entry<K,V> getEntry(Object key) {
if (size == 0) {
return null;
}
//通过key的哈希找到key对应卡槽位置
int hash = (key == null) ? 0 : hash(key);
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 != null && key.equals(k))))
return e;
}
return null;
}
查询过程:
1、如果key为null,在0号卡槽位置遍历链表查询,key存在则返回entry的value,否则返回null
2、如果key不为null,对key进行哈希,找到对应的卡槽,遍历链表,判断key是否存在(hash,key.equals),返回entry的value否则返回null.
删除元素:remove(key)
通过key删除所在entry实体
public V remove(Object key) {
Entry<K,V> e = removeEntryForKey(key);
return (e == null ? null : e.value);
}
final Entry<K,V> removeEntryForKey(Object key) {
//如果集合为空,直接返回null
if (size == 0) {
return null;
}
//通过key哈希找到对应卡槽(key为null卡槽为0)
int hash = (key == null) ? 0 : hash(key);
int i = indexFor(hash, table.length);
Entry<K,V> prev = table[i];
Entry<K,V> e = prev;
//删除节点即解决单向链表的删除问题:解决思路:给定两个指针,两指针一前一后,前指针表示要删除的节点,
、 //通过后一个指针来将节点和前节点指针的next建立联系
while (e != null) {
Entry<K,V> next = e.next;
Object k;
if (e.hash == hash &&((k = e.key) == key || (key != null && key.equals(k)))) {
modCount++;
size--;
//如果删除的是头结点,将后一个节点作为当前卡槽的头结点
if (prev == e)
table[i] = next;
else
//后一个指针prex来将节点和前节点指针的next建立联系
prev.next = next;
e.recordRemoval(this);
return e;
}
prev = e;
e = next;
}
return e;
}
删除过程:
1、通过key哈希来找到卡槽位置(key为null在0号卡槽)
2、对应卡槽的链表进行遍历查找,给定两个指针一前一后,前指针找到要删除节点,后指针建立和下一个节点关系
总结HashMap的特点
1、数据结构:哈希表(数组+链表)
2、默认数组大小:16
3、扩容机制:大小为原来数组长度的2倍
4、扩容因子:0.75f
5、扩容条件:0.75*数组的长度
6、线程安全:该集合是线程不安全的
7、储存数据的类型为键值对(key和value)
8、key和value都可以为null
9、储存的数据是无序的
10、key不能重复,value可以重复
11、key相同则会进行值覆盖
应用场景
做数据统计
LinkedHashMap介绍
通过集合框架图可知:LinkedHashMap属于HashMap的子实现类
public class LinkedHashMap<K,V> extends HashMap<K,V> implements Map<K,V>
基本特点:
- 1、key不能重复、value是能重复
- 2、LinkedHashMap是插入有序的/访问有序 accessOrder:true 访问有序 false:插入有序
- 3、底层的数据结构是哈希表
- 4、key和value都能为null
LinkedHashMap是如何做到有序的?
数据结构
public class LinkedHashMap<K,V> extends HashMap<K,V> implements Map<K,V>
通过LinkedHashMap的继承关系是继承自HashMap,HashMap的属性及方法被继承那table属性及entry类型都会被继承,其内部也是一个哈希表结构。
内部还有属性
//头结点
private transient Entry<K,V> header;
//accessOrder 控制是插入还是访问有序 默认是false即插入有序 ture:访问有序
private final boolean accessOrder;
class Entry<K,V> extends HashMap.Entry<K,V> {
Entry<K,V> before, after;
}
LinkedHashMap中entry类继承自HashMap.Entry,即具有key\value\hash\next等属性另还新增了两属性是entry类型的 before和after。
LInkedHashMap的实现是哈希表+双向链表
通过哈希表结构存储数据
双向链表在哈希表的基础上来讲节点按照插入或者访问的顺序进行连接
插入数据过程:put
//父类HashMap实现
public V put(K key, V value) {
if (table == EMPTY_TABLE) {
//如果table为空,按照默认16大小初始化哈希表
inflateTable(threshold);
}
if (key == null)
return putForNullKey(value);
int hash = hash(key);
int i = indexFor(hash, table.length);
for (Entry<K,V> e = table[i]; e != null; e = e.next) {
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;
}
}
modCount++;
addEntry(hash, key, value, i);
return null;
}
//linkedhashmap中entry中实现
void recordAccess(HashMap<K,V> m) {
LinkedHashMap<K,V> lm = (LinkedHashMap<K,V>)m;
if (lm.accessOrder) {
lm.modCount++;
remove();
addBefore(lm.header);
}
}
//如果当前是访问有序,将当前节点从双向链表中删除,然后将其插入head的before处理
//linkedHashmap中实现
void addEntry(int hash, K key, V value, int bucketIndex) {
super.addEntry(hash, key, value, bucketIndex);
// Remove eldest entry if instructed
Entry<K,V> eldest = header.after;
if (removeEldestEntry(eldest)) {
removeEntryForKey(eldest.key);
}
}
//HashMap中addentry
void addEntry(int hash, K key, V value, int bucketIndex) {
if ((size >= threshold) && (null != table[bucketIndex])) {
resize(2 * table.length);
hash = (null != key) ? hash(key) : 0;
bucketIndex = indexFor(hash, table.length);
}
createEntry(hash, key, value, bucketIndex);
}
//linkedHashMap中createEntry
void createEntry(int hash, K key, V value, int bucketIndex) {
HashMap.Entry<K,V> old = table[bucketIndex];
Entry<K,V> e = new Entry<>(hash, key, value, old);
table[bucketIndex] = e;
//处理双向链表,插入到header的before位置
e.addBefore(header);
size++;
}
LinkedHashMap的插入中实现和HashMap的实现的区别点
1、在插入时且key存在时,考虑如果是访问有序,将该节点从双向链表插入放入最后位置
2、在key不存在考虑新增entry节点时,在插入哈希表后,还需要维护双向链表,即插入双向链表的尾部
应用场景:
统计数据,按照访问顺序统计
HashTable
继承关系:
HashTable继承关系:
public class Hashtable<K,V>
extends Dictionary<K,V>
implements Map<K,V>, Cloneable, java.io.Serializable
HashMap的继承关系:
public class HashMap<K,V>
extends AbstractMap<K,V>
implements Map<K,V>, Cloneable, Serializable
Dictionary实现:
public abstract class Dictionary<K,V> {
public Dictionary() {
}
abstract public int size();
abstract public boolean isEmpty();
abstract public Enumeration<K> keys(); //获取键的集合
abstract public Enumeration<V> elements(); //获取值的集合
abstract public V put(K key, V value);
abstract public V remove(Object key);
}
AbstractMap类:
//实现其中部分方法
HashTable和HashMap的异同点
*
* 相同点:
* 1、底层数据机构是哈希表(JDK 1.7)
* 2、key-value键值对中,key是不能重复的,value是可以重复的
* 3、HashMap和Hashtable都是插入无序的
*
* 不同点:
* 1、HashTable继承自Dictionary类,该类是比较早期提供的map父类,现推荐使用AbstractMap类
* 2、HashTable的默认初始值是11
* 3、HashTable是线程安全的(通过在方法上添加synchronized关键)
* 4、HashTable中key和value都不能为null
* 5、HashTable中对key的哈希过程和HashMap是不一样的
* 6、HashTable的扩容按照2倍加1大小扩容((oldCapacity << 1) + 1)
*/
TreeMap集合
基本特点:
treeMap特点
1、key按照大小顺序排序,默认的是从小到大
2、key不能为null,value是可以为null
3、key不能重复、value可以重复
TreeMap是如何做到key有序的?
TreeMap底层数据结构是红黑树
TreeMap继承关系
public class TreeMap<K,V>
extends AbstractMap<K,V>
implements NavigableMap<K,V>, Cloneable, java.io.Serializable
treemap 实现NavigableMap接口,支持一系列的导航方法,返回具有顺序的key集合等
而NavigableMap接口声明如下:
public interface NavigableMap<K,V> extends SortedMap<K,V>
NavigableMap接口继承自SortedMap接口,SortedMap接口具有排序功能,具有比较器类Comparator。
比较器类说明:
Comparable和Comparator比较
两个比较器都是接口。
Comparable接口中提供方法:
public interface Comparable<T> {
public int compareTo(T o);
}
该接口中提供了一个compareTo方法,
返回值分别为 -1 ,0,1
在类内部作为比较器使用,一旦确定比较属性,就不能更改
Comparator接口:
public interface Comparator<T> {
int compare(T o1, T o2);
}
接口中提供了compare方法,
该方法返回结果为大于0,等于0,小于0
类外部实现,自定义实现比较过程使用该接口
使用场景:对数据进行排序选择TreeMap
weakHashMap
weakHashMap中没有特殊的数据结构,主要是为了优化JVM,是JVM在垃圾回收时能更智能的回收无用的对象
weakHashMap主要适用于缓存的场景,当一个键对象被垃圾回收器回收时,响应的值对象会从map中删除,weakhashmap能够节约内存空间,缓存一些非必要的数据
weakhashmap与其他的map的区别在于key是一个弱引用类型,其他的map中的key是一个强引用类型,
在Java中,引用有4中类型:强(Strong)引用、软引用(soft)、弱引用(weak)、虚引用(phantom)
引用类型
强引用
强引用是常见的引用类型,通常通过new形式创建的对象都是强引用对象
Object o = new Object();
强引用作用的对象无论何时都不会被GC清理掉
只要强引用存在,GC永不会回收掉被引用的对象,通过关键字new创建的对象所关联的引用就是强引用,只要这个强引用还指向某一个对象,那就说明对象还一直存活这,垃圾回收器就不会碰,即使当内存不足是,JVM抛出OOM(OutOfMemoryError)异常也不会也不会回收强引用作用对象。
软引用、若引用、虚引用这些都存在java.lang.ref包中,父类为Reference类
Reference抽象父类的构造函数
//referent为引用指向的对象
Reference(T referent) {
this(referent, null);
}
//ReferenceQueue 可以理解为队列,在GC之前,将引用的对象放入Queue中,方便清理引用对象本身
Reference(T referent, ReferenceQueue<? super T> queue) {
this.referent = referent;
this.queue = (queue == null) ? ReferenceQueue.NULL : queue;
}
举例子:
Object o = new Obeject();
ReferenceQueue queue = new ReferenceQueue();
WeakReference wk =new WeakReference(o,queue );
此时,wk为弱引用,指向了o对象,o会在一定的时机被GC清理,但是wk对象工作依赖于Queue对象,当wk出现在Queue中,说明其指向的对象已经无效,可以放心清理。
软引用:SoftReference
软引用所作用的对象,当发生GC操作时且内存充足时,不会回收软引用所作用的对象,当内存不足时,触发GC操作是,软引用所作用的对象会被回收。
ReferenceQueue<A> queue = new ReferenceQueue<A>();
SoftReference<A> w = new SoftReference<A>(new A(), queue);
System.out.println(w.get());
System.out.println(w.isEnqueued());
System.out.println(queue.poll());
/**
* main.java.com.tulun.WeakHashMapGY02$A@1e3ac11b
* false
* null
*/
/**
* 划重点:
* //此处需要的内存,内存受限,不够用了,因此触发GC,回收软引用对象
*
*/
byte[] array = new byte[7*1024*1024+500*1024];
System.out.println(array[0]);
System.gc();
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
System.out.println(w.get());
System.out.println(w.isEnqueued());
System.out.println(queue.poll());
弱引用:WeakReference
弱引用所作用的对象,当发生GC操作时,即使内存空间充足,GC操作也会回收弱引用所作用的对象。
ReferenceQueue<A> queue = new ReferenceQueue<A>();
WeakReference<A> w = new WeakReference<A>(new A(), queue);
System.out.println(w.get());
System.out.println(w.isEnqueued());
System.out.println(queue.poll());
// main.java.com.tulun.WeakHashMapGY02$A@20724356
// false
// null
System.gc();
System.out.println("---------------------");
System.out.println(w.get());
System.out.println(w.isEnqueued());
System.out.println(queue.poll());
虚引用:phantonRefercence
虚引用是最弱的一种引用关系,一个对象是否有虚引用的存在,不会对对象生存时间构成影响,也无法通过虚引用来取得一个对象的实例,
虚引用存在可以来判断对象是否被回收
ReferenceQueue<A> queue = new ReferenceQueue<A>();
PhantomReference<A> ptr = new PhantomReference<A>(new A(), queue);
System.gc(); //此处启动GC,回收A对象,queue收到通知,该对象回收了
if(ptr.isEnqueued()){
System.out.println("ptr.isEnqueued()");
}else{
System.out.println("not ptr.isEnqueued()");
}