Map集合
- Map接口实现类的特点
- Map和 Collection 并列存在,用于保存具有映射关系的数据 key-value
- Map中的key 和 value 可以是任何引用类型的数据,会封装到、HashMap$Node 对象中
- Map 中的 key 不允许重复,原因和 HashSet 一样,前面分析过源码。hash 值和内容一样就会取消。
- Map 中的 value 可以重复
- Map 的 key 可以为 null,value 也可以是 null ,注意 key 为 null,只能有一个,value 为 null ,可以为多个。
- 常用 String 类作为 Map 的 key。
- key 和 value 之间存在单向一对一关系,即通过指向的key总能找到对应的 value
前面已经分析过源码
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
boolean evict) {
Node<K,V>[] tab; Node<K,V> p; int n, i;
if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length;
if ((p = tab[i = (n - 1) & hash]) == null) // 这里的hash只是key的,跟value没关系
tab[i] = newNode(hash, key, value, null);
else {
Node<K,V> e; K k;
if (p.hash == hash && // 如hash值和当前值key一致,进行替换操作
((k = p.key) == key || (key != null && key.equals(k))))
e = p;
else if (p instanceof TreeNode)
e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
else {
for (int binCount = 0; ; ++binCount) {
if ((e = p.next) == null) {
p.next = newNode(hash, key, value, null);
if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
treeifyBin(tab, hash);
break;
}
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
break;
p = e;
}
}
if (e != null) { // existing mapping for key
V oldValue = e.value;
if (!onlyIfAbsent || oldValue == null)
e.value = value;
afterNodeAccess(e);
return oldValue;
}
}
++modCount;
if (++size > threshold)
resize();
afterNodeInsertion(evict);
return null;
}
结论:首次添加还是扩容16个,添加时判断的key的hash值是否一致,与value里的东西没有关系。
HashMap
HashMap添加时,会把自己的 key-value 让 Entry 进行引用
- put操作 最后存放在 HashMap$Node node = new Node(hash,key,value,null)
- 为了方便遍历,会创建 EntrySet集合,用来存放该元素的类型 就是 Entry<k,v>
- 在 EntrySet 中,定义的类型是 Map.Entry,但存放的还是 HashMap$Node
- ?为什么 HashMap&Node类型能放到 Map.Entry 中,原因是实现了接口的多态,HashMap$Node 实现了 Map.Entry类 (static class Node<K,V> implements Map.Entry<K,V>)
- 当把 HashMap$Node 对象存放到 entrySet 就会方便我们遍历,因为 Map.Entry 提供了非常重要的方法:K getKey(),V getValue();;
/*
* 1.K-V 最后存放在 HashMap$Node node = new Node(hash,key,value,null)
* 2.K-V 为了方便遍历,还会创建 EntrySet 集合,该集合存放元素的类型就是就 Entry,而一个Entry 就有 key-value
* EntrySet<Entry<K,V>> transient Set<Map.Entry<K,V>> entrySet;
* 3. 在 EntrySet中,定义的类型是 Map.Entry,但存放的还是 HashMap$Node
* ?为什么 HashMap&Node类型能放到 Map.Entry 中,
* 原因是实现了接口的多态,HashMap$Node 实现了 Map.Entry类 (static class Node<K,V> implements Map.Entry<K,V>)
* 4. 当把 HashMap$Node对象存放到 entrySet 就会方便我们遍历,因为 Map.Entry提供了非常重要的方法:K getKey(),V getValue();;
*/
Set set = map.entrySet();
System.out.println(set.getClass());
for (Object o : set){
/*为了从 HashMap$node 取出K-V*/
/*先做一个向下转型*/
Map.Entry entry = (Map.Entry) o;
System.out.println(entry.getKey());
}
Set set1 = map.keySet();
Collection values = map.values();
Map接口的常用方法
- put:添加
- remove:根据键删除映射关系
- get:根据键获取值
- size:获取 元素个数
- isEmpty:判断格式是否为0
- clear:清除
- containsKey:查找键是否存在
Map接口的遍历方式
- containsKey:查找键是否存在
- keySet:获取所有的键
- entrySet:获取所有关系
- values:获取所有的值
@SuppressWarnings({"all"})
public class MapFor {
public static void main(String[] args) {
Map map = new HashMap();
map.put("邓超01","孙俪01");
map.put("邓超02","孙俪02");
map.put("邓超03","孙俪03");
map.put("邓超04","孙俪04");
map.put("邓超05","null");
map.put("null","孙俪06");
System.out.println("———————增强for循环———————");
Set keymap = map.keySet();
for (Object k : keymap){
System.out.println(map.get(k));
}
System.out.println("———————迭代器———————");
Iterator iterator = keymap.iterator();
while (iterator.hasNext()) {
Object next = iterator.next();
System.out.println(next);
}
System.out.println("———————把所有的values取出———————");
Collection values = map.values();
// 这里可以使用所有Collections使用的遍历方法
// (1) 增强for
for (Object o : values){
System.out.println(o);
}
System.out.println("———————迭代器———————");
Iterator iterator1 = values.iterator();
while (iterator1.hasNext()) {
Object next = iterator1.next();
System.out.println(next);
}
System.out.println("————————通过EntrySet来获取 k-v————————");
Set entrySet = map.entrySet();
// 增强for
for (Object entry:entrySet){
Map.Entry entry1 = (Map.Entry)entry;
System.out.println(entry1);
}
System.out.println("———————迭代器———————");
Iterator iterator2 = entrySet.iterator();
while (iterator2.hasNext()) {
Object next = iterator2.next();
Map.Entry entry1 = (Map.Entry)next;
System.out.println(entry1);
}
}
}
HashMap小结
- Map接口的常用实现类:HashMap\Hashtable\Properties
- HashMap是Map接口使用频率最高的实现类
- HashMap是以 key-val 的方式来存储数据的(HashMap$Node)
- key不能重复,但是值可以重复,允许使用null值和null值。
- 如果添加相同的key,则会覆盖原来的key-val,等同于修改。(key,不会替换,val会替换)
- 与HashSet一样,不保证映射的顺序,因为底层是以hash表的方式来存储的。
-
HashMap
没有实现同步,因此是线程不安全的。没有做同步互斥 sync
HashMap底层机制
HashMap 扩容机制就是【HashSet】
- HashMap底层维护了Node类型的数组table,默认为Null
- 当创建对象时,将加载因子(loadfactor)初始值为.075
- 当添加 key-value 时,通过 key 的哈希值得到在 table 的索引,然后判断该索引是否有元素,如果没有元素直接添加,如果有元素,继续判断该元素的 key 是否和准备加入的值是否相等,如果相等,直接替换,如果不相等需要判断是否为树结构还是链表结构,做出相应处理。如果添加是发现容量不够,则需要扩容。
- 第一次添加,则需要扩容 table 容量为 16,临界值为 12,是容量的 0.75
- 以后再扩容,则需要扩容 table 容量原来的2倍,临界值为原来的 2 倍,即 24,以此类推
- 在jdk8.如果链表超过8,并且容量大小大于等于 64,进行树化。
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
boolean evict) {
Node<K,V>[] tab; Node<K,V> p; int n, i; // 定义了辅助变量
// table是HashMap的一个数组,类型是 Node[],如果为null
if ((tab = table) == null || (n = tab.length) == 0)
// 第一次扩容到16个空间,执行完resize()会得到长度为16的Node数组, n = 16
n = (tab = resize()).length;
// 根据key得到的hash,去计算该key应该存放到的table表的哪个索引位置
// 并把这个位置的对象赋值给p
// 如果 p = null,表示为没有存放过,将新节点添加到数组
// 如果 p != null,表示不是第一次进行添加
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null);
else { // 如果重复,进行此语句
Node<K,V> e; K k; // 辅助变量
// 如果当前索引位置对应的链表的第一个元素 和 准备 添加的key的hash值一样。
if (p.hash == hash &&
// 如果要加入的key和现在的key是同一个对象,或者 key不能为空和内容相同
((k = p.key) == key || (key != null && key.equals(k))))
e = p; // 将要添加的节点信息赋值给e
else if (p instanceof TreeNode) // 如果对象和内容都不一样,是否是一个红黑树
e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
else { // 对象不同内容也不同,也不是红黑树,就执行此条(是链表,但是不是红黑树)
for (int binCount = 0; ; ++binCount) { // 循环链表
if ((e = p.next) == null) { // 如果是最后一个链表说明都不相同,
p.next = newNode(hash, key, value, null); // 则加入该链表的最后,
if (binCount >= TREEIFY_THRESHOLD - 1) // 加入后面后,判断该链表已经达到8个节点,如果达到进化成红黑树
treeifyBin(tab, hash);
break;
}
if (e.hash == hash && // 判断每一个hash和引用的对象和对象的值是否一致
((k = e.key) == key || (key != null && key.equals(k))))
break; // 一致就退出
p = e; // 将每次循环的值替换成下一个
}
}
if (e != null) {
V oldValue = e.value; // 获取要添加的值
if (!onlyIfAbsent || oldValue == null) // 如果要添加的值不等于Null
e.value = value;
afterNodeAccess(e);
return oldValue; // 就把要添加的值返回回去
}
}
++modCount; // 修改了一次
if (++size > threshold) // 如果现在的大小是否超过数组容量的0.75
resize(); // 进行扩容
afterNodeInsertion(evict); // 为了让子列实现这个方法继续扩展
return null; // 返回 null 代表执行成功
}
// 树化
// 判断如果表为null,或者表的长度没有64大就进行扩容
if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY)
resize();
//****************************************************************************
// 执行扩容的方法
final Node<K,V>[] resize() {
Node<K,V>[] oldTab = table; // 获取HashMap的table数组
int oldCap = (oldTab == null) ? 0 : oldTab.length; // 获取Node数组的长度
int oldThr = threshold; // 将临界点赋值给oldThr
int newCap, newThr = 0; // 创建辅助变量
if (oldCap > 0) { // 如果数组长度大于0,表示不是第一次添加则进入此通道
if (oldCap >= MAXIMUM_CAPACITY) { // 判断长度是否大于等于 1 << 30
threshold = Integer.MAX_VALUE; // 数组最大值赋值给临界值
return oldTab; // 将数组长度返回
}
// 如果扩容以后小于 1 << 30的值并且数组长度大于默认的数组长度(16)
else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
oldCap >= DEFAULT_INITIAL_CAPACITY)
newThr = oldThr << 1; // 进行扩容操作,临界点*2赋值给新的临界点
}
// 如果数组长度不大于0,而且临界点大于0
else if (oldThr > 0)
newCap = oldThr; // 就将旧的临界点赋值给新的数组长度
else { // 如果临界点不大于0,临界点也不小于0,进入此通道
newCap = DEFAULT_INITIAL_CAPACITY; // 默认长度赋值给新长度
// 新长度 * 0.75获得临界点
newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
}
// 如果新临界点是0,进入此通道
if (newThr == 0) {
// 将新长度 * 临界点(临界点可以自定义,在构造函数中定义)赋值给ft
float ft = (float)newCap * loadFactor;
// 设置新临界点:如果新长度小于1 << 30 并且 如果临界点 小于1 << 30,返回 ft,否则返回Integer最大长度)
newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
(int)ft : Integer.MAX_VALUE);
}
// 将新临界点赋值给全局变量的临界点
threshold = newThr;
@SuppressWarnings({"rawtypes","unchecked"})
// 创建辅助变量
Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
table = newTab;
// 判断如果旧表的内容不等空
if (oldTab != null) {
// 循环此表数组
for (int j = 0; j < oldCap; ++j) {
Node<K,V> e; // 创建辅助变量
if ((e = oldTab[j]) != null) { // 如果 每个数组的内容不为null
oldTab[j] = null; // 就将内容变为null
if (e.next == null) // 如果每个节点没有下一个节点,代表最后一次执行
newTab[e.hash & (newCap - 1)] = e; // 将此节点添加到新表数组中
else if (e instanceof TreeNode) // 如果不是最后一个节点,判断是否是树形,如果是树形
// 将循环的这个节点进行分解,将旧的表长度添加到新表中
((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
else { // 如果不是树型并且不是循环到最后一位。
// 创建局部变量辅助操作
Node<K,V> loHead = null, loTail = null;
Node<K,V> hiHead = null, hiTail = null;
Node<K,V> next;
do { // 循环
next = e.next; // 节点的下一个节点,
if ((e.hash & oldCap) == 0) { // 判断循环的这个节点的hash值是否==0
// 如果 为空
if (loTail == null)
loHead = e; // 将节点赋值给 loHead
else // 否则
loTail.next = e; // 将loTail的下个节点指向 e
loTail = e;
}
else {
if (hiTail == null)
hiHead = e;
else
hiTail.next = e;
hiTail = e;
}
} while ((e = next) != null);
if (loTail != null) {
loTail.next = null;
newTab[j] = loHead;
}
if (hiTail != null) {
hiTail.next = null;
newTab[j + oldCap] = hiHead;
}
}
}
}
}
return newTab; // 向扩容的新表结构返回回去
}
Hashtable
- 存放的元素是键值对,即k-v
- hashtable的键和值都不能为null
- hashtable 使用的方法基本和 hashMap 一样
- hashtable是线程安全的,hashMap 是线程不安全的。
构造方法
// 无参构造
public Hashtable() {
this(11, 0.75f); // 参数为默认容量11,默认临界值比例0.75倍
}
// 有参构造,参数自定义默认容量
public Hashtable(int initialCapacity) {
this(initialCapacity, 0.75f);
}
// 有参构造,参数自定义默认容量和临界值的比例
public Hashtable(int initialCapacity, float loadFactor) {
if (initialCapacity < 0) // 如果容量小于0抛异常
throw new IllegalArgumentException("Illegal Capacity: "+
initialCapacity);
if (loadFactor <= 0 || Float.isNaN(loadFactor)) // 如果临界值小于等于0或者为空抛异常
throw new IllegalArgumentException("Illegal Load: "+loadFactor);
if (initialCapacity==0) // 如果容量等于0,自动让容量+1
initialCapacity = 1;
this.loadFactor = loadFactor;
table = new Entry<?,?>[initialCapacity]; // 创建Entry集合
threshold = (int)Math.min(initialCapacity * loadFactor, MAX_ARRAY_SIZE + 1);
}
put源码解析
public synchronized V put(K key, V value) {
// 判断value是否为空,为空抛异常
if (value == null) {
throw new NullPointerException();
}
// 将构造函数时创建的table放到tab中
Entry<?,?> tab[] = table;
int hash = key.hashCode(); // 获取key的hash值
int index = (hash & 0x7FFFFFFF) % tab.length; // 将长度*临界值的比例得到临界值
@SuppressWarnings("unchecked")
Entry<K,V> entry = (Entry<K,V>)tab[index];
for(; entry != null ; entry = entry.next) { // 循环tab
if ((entry.hash == hash) && entry.key.equals(key)) { //如果出现相等的hash和key
V old = entry.value; // 进行更新操作
entry.value = value;
return old;
}
}
addEntry(hash, key, value, index); // 程序走到这代表没有为null,并且没有重复时,执行添加
return null;
}
———————————————————————————————————真正执行添加———————————————————————————————————
private void addEntry(int hash, K key, V value, int index) {
modCount++; // 修改次数+1
Entry<?,?> tab[] = table; // 创建局部变量获取tab值,复制操作为了避免修改真正的数据
if (count >= threshold) { // 如果插入后的数量大于等于 临界值
// 进行扩容操作
rehash();
tab = table; // 扩容以后将扩容的信息复制到局部变量中
hash = key.hashCode(); // 获取key的hashCode
index = (hash & 0x7FFFFFFF) % tab.length; // 获取下标索引
}
//执行添加操作
@SuppressWarnings("unchecked")
Entry<K,V> e = (Entry<K,V>) tab[index]; // 将tab数组向下转型到Entry
tab[index] = new Entry<>(hash, key, value, e);
count++; 存在值的长度++
}
扩容机制
protected void rehash() {
int oldCapacity = table.length; // 获取表长度
Entry<?,?>[] oldMap = table; // 将表内容存放到Entry集合中
// 进行扩容,2倍+1
int newCapacity = (oldCapacity << 1) + 1;
if (newCapacity - MAX_ARRAY_SIZE > 0) { // 如果扩容后比数组最大的长度大。
if (oldCapacity == MAX_ARRAY_SIZE) // 如果旧长度最大的长度
// 返回
return;
newCapacity = MAX_ARRAY_SIZE;// 如果扩容后比数组最大的长度大。 就设置成数组最大的长度
}
Entry<?,?>[] newMap = new Entry<?,?>[newCapacity]; // 创建Entry集合
modCount++; // 次数++
threshold = (int)Math.min(newCapacity * loadFactor, MAX_ARRAY_SIZE + 1);
table = newMap;
for (int i = oldCapacity ; i-- > 0 ;) {
for (Entry<K,V> old = (Entry<K,V>)oldMap[i] ; old != null ; ) {
Entry<K,V> e = old;
old = old.next;
int index = (e.hash & 0x7FFFFFFF) % newCapacity;
e.next = (Entry<K,V>)newMap[index];
newMap[index] = e;
}
}
}
Properties
- Properties类继承自Hashtable类并且实现了Map接口,也是使用一种键值对的形式来保存数据
- 他的使用特点和Hashtable类似
- Properties 还可以用于 从 xxx.properties 文件中,加载数据到Properties类对象,并进行读取和修改
特点
- Properties,**继承的是 Hashtable,**所以机制都用的是 Hashtable
- 可以通过key - value存放数据。当前 key-value 不能为 null.
增(put)、删(remove)、改(put相同key)、查(get())。
TreeMap
传入比较器,决定添加的顺序【如果比较的值一样就放弃,不会进行替换】
TreeMap treeMap = new TreeMap(new Comparator() {
@Override
public int compare(Object o1, Object o2) {
// return ((String) o1).compareTo((String) o2);
return ((String) o1).length() - ((String) o2).length();
}
});
构造器
// 无参构造
public TreeMap() {
comparator = null;
}
// 有参构造(把匿名比较器传入,进行比较)
public TreeMap(Comparator<? super K> comparator) {
this.comparator = comparator;
}
添加时进行比较
Comparator<? super K> cpr = comparator; // 获取比较器
if (cpr != null) {// 比较器不为空
do {
parent = t; // 循环每个值
cmp = cpr.compare(key, t.key); // 获取比较器的值
if (cmp < 0) // 如果比0小,
t = t.left; // 放左边
else if (cmp > 0) // 如果比0大,
t = t.right; // 放右边
else // 如果等于0
return t.setValue(value); // 放弃
} while (t != null);
}
Collections方法
- reverse:反转
Collections.reverse(arrayList);
- shuffle:打乱(抽奖)
Collections.shuffle(arrayList);
- sort:自然排序(字符串的升序)
Collections.sort(arrayList);
- max:最大
- min:最小
- frequency:查看出现的次数
Collections.frequency(arrayList, "misad0")
- copy:拷贝
// 拷贝
ArrayList arrayList1 = new ArrayList();
for (int i = 0; i < arrayList.size(); i++) {
arrayList1.add("");
}
// 前提是要 复制和被复制的长度一致
Collections.copy(arrayList1,arrayList);
// 遍历数组
Iterator iterator = arrayList1.iterator();
while (iterator.hasNext()) {
Object next = iterator.next();
System.out.println(next);
}
- replaceAll:修改值
Collections.replaceAll(arrayList,"misad0","asdas");// 要修改的集合,旧值,新值
开发如何选择集合实现类
在开发中,选择什么集合实现类,主要取决于业务的操作特点。
- 先判断存储类型(一组对象【单列】或一组键值对【双列】)
- 一组对象:Collection 接口
- 允许重复:List
- 增删多:LinkedList【底层维护了一个双向链表】
- 查改多:ArrayList【底层维护Object类型的可变数组】
- 不允许重复:Set
- 无序:HashSet【底层是HashMap,维护了一个哈希表,极(数组+链表+红黑树)】
- 排序:TreeSet
- 插入和取出的顺序一致【LinkedHashSet】,维护数组 + 双向链表
- 一组键值对:Map
- 键无序:HashMap【底层是Hash表】
- 键排序:TreeMap
- 检查如和取出顺序一致:LinkedHashMap
- 读取文件:Properties