简介:
在Collection接口中,保存的数据都是单个的对象,在数据结构中除了单个对象的数据,也可以进行二元偶对象的保存(key=value)的形式来存储,而存储二元偶对象的核心意义在于:通过key获取对应的value。
在开发中:Collection集合保存数据的目的是为了输出,而Map集合保存数据的目的是为了key的查找。
Map接口:
Map接口是进行二元偶对象保存的最大父接口,该接口定义:
public interface Map<K,V>;
该接口为一个独立的父接口,并且在进行接口对象实例化的 时候需要设置Key与Value的类型,所以在保存的时候需要两个内容,在Map接口中定义有许多操作方法,但是有几个方法需要牢记:
- 向集合中保存数据:public V put(K key, V value);
- 根据Key查询数据:public V get(Object key);
- 将Map集合转为Set集合:public Set<Map.Entry<K, V>> entrySet();
- 查询指定的Key是否存在:public boolean containsKey(Object key);
- 将Map集合中的Key转为Set集合:public Set<K> keySet();
- 根据Key删除指定的数据:public V remove(Object key);
红色的是重要方法。
从JDK1.9之后Map集合提供了一些静态方法供用户使用。
代码实现:
import java.util.Map;
public class MAIN {
public static void main(String[] args) {
Map<String, Integer> map = Map.of("one",1,"two",2);
System.out.println(map);
}
}
输出结果:
在Map集合中数据的保存按照形式“Key=Value”,并且使用of()方法操作,里面数据的Key是不允许重复的和为空。
正常的使用情况是使用Map的子类进行,常用的子类有:HashMap、HashTable、TreeMap、LinkedHashMap
Map接口子类 - HashMap类:
HashMap是Map接口中最为常见的一个子类,该类的特点是无序存储。
public class HashMap<K,V>
extends AbstractMap<K,V>
implements Map<K,V>, Cloneable, Serializable
继承形式符合集合定义的形式,提供又要抽象类,需要重复实现Map接口。
public HashMap() {
this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted
}
|
当使用无参构造的时候会出现有一个loadFactor属性,并且该属性默认的内容是“0.75f”
static final float DEFAULT_LOAD_FACTOR = 0.75f;
|
public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
}
|
在使用put()方法进行数据保存的时候会调用putVal()方法,同时会将Key进行hash处理(生成一个hash码),而对于putVal()方法会发现提供了一个节点类进行数据保存,而在使用putVal()方法操作的过程之中会调用一个resize()的方法可以进行容量的扩充。 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)
tab[i] = newNode(hash, key, value, null);
else {
Node<K,V> e; K k;
if (p.hash == hash &&
((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;
}
static class Node<K,V> implements Map.Entry<K,V>
|
面试题:
在进行HashMap的put()操作的时候如何实现容量扩充的?
- 在HashMap类里面专门提供有一个“ DEFAULT_INITIAL_CAPACITY ”常量,作为初始化容量配置,而后这个常量的默认大小为16个元素,默认可以保存最大长度是16;
- 当保存的内容的容量超过了阈值( DEFAULT_LOAD_FACTOR = 0.75f ),相当于 “ 容量 * 阈值 = 12 ” 保存12个元素的时候就会进行容量的扩充。
- 在进行扩充的时候HashMap采用的是成倍的扩充模式,即:每一次都扩充2倍的容量
面试题:
请解释HashMap的工作原理?(JDK1.8之后)
- 在HashMap之中进行数据存储的依然是利用Node类完成的,那么这种情况下就证明可以使用的数据结构有两种:链表(时间复杂度“O(n)”)、二叉树(时间复杂度(“O(logn)”));
- 从JDK1.8开始,HashMap的实现出现了改变,因为其要适应于大数据时代的海量数据,所以对于其存储发生了变化,并且在HashMap类中提供了一个重要的常量:
static final int TREEIFY_THRESHOLD = 8;
在使用HashMap进行数据保存的时候,如果保存的数据没有超过阈值“ 8 ”,那么会按照链表的形式进行存储,如果超出了阈值,则会将链表转换为红黑树以实现树的平衡,并且利用左旋与右旋保证数据的查询性能。
Map集合的使用:
import java.util.HashMap;
import java.util.Map;
public class MAIN {
public static void main(String[] args) {
Map<String, Integer> map = new HashMap<String, Integer>();
map.put("1", 1);
map.put("2", 2);
map.put("3", 3);
map.put("4", 4);
map.put("4", 3); // Key重复
map.put(null, 0); // Key为空
map.put("5", null); // Value为空
System.out.println(map.get(null)); // Key不存在
System.out.println(map.get("4")); // Key存在
System.out.println(map.get("5")); // Key存在
}
}
输出结果:
可以发现:
Key不存在也能获取对应的Value
而出现重复Key的时候,后面的Key会覆盖前面的Key
只要Key存在就能获取Value,即使Value为空
Put()方法的返回值观察:
import java.util.HashMap;
import java.util.Map;
public class MAIN {
public static void main(String[] args) {
Map<String, Integer> map = new HashMap<String, Integer>();
System.out.println(map.put("1", 1)); // Key不重复,返回null
System.out.println(map.put("1", 101)); // Key重复,返回旧数据
}
}
输出结果:
在设置了相同Key内容的时候,put()方法会返回旧的Value.
Map接口子类 - HashMap类
HashMap是Map集合最为常用的一种子类,但是其本身保存的数据都是无序的(有没有序对Map没有影响),如果希望Map集合之中保存的数据的顺序为其增加顺序,则可以更换子类为LinkedHashMap(基于链表实现)。
public class LinkedHashMap<K,V>
extends HashMap<K,V>
implements Map<K,V>
因为是链表的形式,所以一般在使用LinkedHashMap类的时候数据量都不要太大,因为会造成时间复杂度攀升。
从继承关系可以发现LinkedHashMap是HashMap的子类:
使用LinkedHashMap:
import java.util.LinkedHashMap;
import java.util.Map;
public class MAIN {
public static void main(String[] args) {
Map<String, Integer> map = new LinkedHashMap<String, Integer>();
map.put("one", 1);
map.put("three", 3);
map.put("two", 2);
System.out.println(map);
}
}
输出结果:
从结果可以发现,LinkedHashMap集合中元素的顺序就是代码添加的顺序。
Map接口子类 - Hashtable类
Hashtable类是JDK1.0提供的,和Vector、Enumeration属于最早的一批动态数组的实现类,后来为了将其保存下来,就让其多实现了一个Map接口。
Hashtable类定义:
public class Hashtable<K,V>
extends Dictionary<K,V>
implements Map<K,V>, Cloneable, java.io.Serializable
Hashtable类继承结构:
使用Hashtable类:
import java.util.Hashtable;
import java.util.Map;
public class MAIN {
public static void main(String[] args) {
Map<String, Integer> map = new Hashtable<String, Integer>();
map.put("one", 1);
map.put("three", 3);
map.put("two", 2);
System.out.println(map);
}
}
输出结果:
可以试试将Key或者Value设置为空,运行后会发现程序出现异常NullPointerException。
面试题:
请解释HashMap与Hashtable的区别?
- HashMap中的方法都属于异步操作,非线程安全;而Hashtable都属于同步方法,属于线程安全。
- HashMap允许保存null的数据;而Hashtable不允许保存null的数据,否则会出现NullPointerException的异常。
但是更多情况下使用的还是HashMap。
Map.Entry接口:
在Map集合中有一个核心问题需要理解:Map集合里面是如何进行数据存储的?
对应List而言(LinkedList类)依靠的是链表的形式实现的数据存储,在进行数据存储的时候一定是将数据保存在Node节点之中,虽然在HashMap里面也能见到Node类型的定义(puVal()方法),HashMap中的Node内部类本身实现了Map.Entry接口:
static class Node<K,V> implements Map.Entry<K,V>
所以可以得出的结论:所有的Key和Value的数据都被封装在Map.Entry 接口之中。
Map.Entry定义:
public static interface Map.Entry<K, V>
可以发现这是一个内部接口,并且在这个内部接口中提供有两个重要的操作方法:
- 获取Key:public K getKey();
- 获取Value:public VgetValue();
在JDK1.9之前的开发中使用者基本不考虑Map.Entry的对象,在正常的开发过程之中使用者也不需要关心Map.Entry对象,从JDK1.9之后Map接口中追加有一个新的方法:
- 创建Map.Entry对象:public static <K, V> Map.Entry<K, V> entry(K k, V v)
创建此对象:
import java.util.HashMap;
import java.util.Map;
import java.util.Map.Entry;
public class MAIN {
public static void main(String[] args) {
Map.Entry<String, Integer> entry = Map.entry("one", 1);
System.out.println("获取Key:" + entry.getKey());
System.out.println("获取Value:" + entry.getValue());
System.out.println(entry.getClass().getName()); // 观察使用的子类
}
}
输出结果:
获取Key:one
获取Value:1
java.util.KeyValueHolder
可以发现,使用的是java.util.KeyValueHolder,那么java.util.KeyValueHolder是什么:
专门做数据保存的类。
final class KeyValueHolder<K,V> implements Map.Entry<K,V>
final class KeyValueHolder<K,V> implements Map.Entry<K,V> {
@Stable
final K key;
@Stable
final V value;
KeyValueHolder(K k, V v) {
key = Objects.requireNonNull(k);
value = Objects.requireNonNull(v);
}
/**
* Gets the key from this holder.
*
* @return the key
*/
@Override
public K getKey() {
return key;
}
/**
* Gets the value from this holder.
*
* @return the value
*/
@Override
public V getValue() {
return value;
}
/**
* Throws {@link UnsupportedOperationException}.
*
* @param value ignored
* @return never returns normally
*/
@Override
public V setValue(V value) {
throw new UnsupportedOperationException("not supported");
}
/**
* Compares the specified object with this entry for equality.
* Returns {@code true} if the given object is also a map entry and
* the two entries' keys and values are equal. Note that key and
* value are non-null, so equals() can be called safely on them.
*/
@Override
public boolean equals(Object o) {
return o instanceof Map.Entry<?, ?> e
&& key.equals(e.getKey())
&& value.equals(e.getValue());
}
/**
* Returns the hash code value for this map entry. The hash code
* is {@code key.hashCode() ^ value.hashCode()}. Note that key and
* value are non-null, so hashCode() can be called safely on them.
*/
@Override
public int hashCode() {
return key.hashCode() ^ value.hashCode();
}
/**
* Returns a String representation of this map entry. This
* implementation returns the string representation of this
* entry's key followed by the equals character ("{@code =}")
* followed by the string representation of this entry's value.
*
* @return a String representation of this map entry
*/
@Override
public String toString() {
return key + "=" + value;
}
}
通过分析可以发现在整个的Map集合里面,Map.Entry的主要作用就是作为一个Key和Value的包装类型使用,而大部分情况下在进行数据存储的时候都会将Key和Value包装为一个Map.Entry对象进行使用。
使用Iterator输出Map集合:
对于集合的输出最标准的做法就是利用Iterator接口来完成,但是Map接口中没有一个方法可以直接返回Iterator接口对象,所以这种情况下就必须分析不直接提供Iterator接口实例化的原因:
在Map集合中实际上保存的是一组Map.Entry接口对象(里面包装的是Key和Value),所以整个来讲Map依然实现的是单值的保存,Map里面提供有一个方法:
- 将全部的Map集合转换为Set集合:public Set<Map.Entry<K, V>> entrySet();
通过这个方法将Map集合(Key不重复)转换为Set集合 (元素不重复),Set集合中保存的就是Map.Entry对象,而Set集合又能使用Iterator接口,所以就实现了Map集合间接使用Iterator接口的操作。
1、使用Map集合中的entrySet()方法将Map集合转换成Set集合;
2、利用Set接口中的iterator()方法将Set集合转为Iterator接口实例;
3、利用Iterator进行迭代输出获取每一组的Map.Entry对象,然后使用getKey()、getValue()获取Key和Value。
利用Iterator输出与Map集合:
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
public class MAIN {
public static void main(String[] args) {
map.put("one", 1);
map.put("two", 2);
map.put("three", 3);
map.put("four", 4);
map.put("five", 5);
System.out.println(map);
Set<Entry<String, Integer>> set = map.entrySet();
Iterator<Entry<String, Integer>> iter = set.iterator();
while(iter.hasNext()) {
Entry<String, Integer> me = iter.next();
System.out.println("**Key:" + me.getKey() + " --- Value:" + me.getValue());
}
}
}
输出结果:
{four=4, one=1, two=2, three=3, five=5}
**Key:four --- Value:4
**Key:one --- Value:1
**Key:two --- Value:2
**Key:three --- Value:3
**Key:five --- Value:5
虽然Map支持迭代输出,但是大部分情况下Map都不会使用迭代输出,因为Map集合主要的作用就是通过Key实现查询操作;
如果现在不使用Map集合而使用foreach进行迭代输出,同样需要将Map集合转为Set集合:
使用foreach:
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
public class MAIN {
public static void main(String[] args) {
Map<String, Integer> map = new HashMap<String, Integer>();
map.put("one", 1);
map.put("two", 2);
map.put("three", 3);
map.put("four", 4);
map.put("five", 5);
System.out.println(map);
Set<Entry<String, Integer>> set = map.entrySet();
for(Map.Entry<String,Integer> me : set) {
System.out.println(me.getKey() + " = " + me.getValue());
}
}
}
输出结果:
{four=4, one=1, two=2, three=3, five=5}
four = 4
one = 1
two = 2
three = 3
five = 5
由于Map迭代输出的情况相对较少,所以对于此类的语法应该深入理解,并且一定要灵活掌握。
自定义Key类型
在使用Map集合的时候可以发现对于Key和Value的类型都可以由使用者任意决定,那么就可以使用自定义的类进行Key的类型定义。
自定义Person类做Key的类型:
package Demo_3_15_Map集合;
import java.util.HashMap;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Objects;
import java.util.Set;
// 自定义类做为Map集合的Key的类型
class Person {
private String name;
private int age;
public Person() {
}
public Person(String name, int age) {
this.name = name;
this.age = age;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
@Override
public String toString() {
return "name = " + this.name + ", age = " + this.age;
}
@Override
public int hashCode() {
return Objects.hash(age, name);
}
@Override
public boolean equals(Object obj) {
if (this == obj)
return true;
if (obj == null)
return false;
if (getClass() != obj.getClass())
return false;
Person other = (Person) obj;
return age == other.age && Objects.equals(name, other.name);
}
}
public class MAIN {
public static void main(String[] args) {
Map<Person, String> map = new HashMap<Person, String>();
map.put(new Person("张三",12), "1");
map.put(new Person("李四",14), "2");
map.put(new Person("王五",15), "3");
map.put(new Person("狗蛋",16), "4");
System.out.println(map.get(new Person("张三",12)));
}
}
这个通过Key获取Value有所差异,因为是类对象,所以需要实现hashCode()、equals()方法才能获取Key对象对应的Value。
输出结果:
{name = 张三, age = 12=1, name = 王五, age = 15=3, name = 狗蛋, age = 16=4, name = 李四, age = 14=2}
1
源代码:
hashMap的put()方法 |
hashMap的get()方法 |
public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
}
|
public V get(Object key) {
Node<K,V> e;
return (e = getNode(key)) == null ? null : e.value;
}
final Node<K,V> getNode(Object key) {
Node<K,V>[] tab; Node<K,V> first, e; int n, hash; K k;
if ((tab = table) != null && (n = tab.length) > 0 &&
(first = tab[(n - 1) & (hash = hash(key))]) != null) {
if (first.hash == hash && // always check first node
((k = first.key) == key || (key != null && key.equals(k))))
return first;
if ((e = first.next) != null) {
if (first instanceof TreeNode)
return ((TreeNode<K,V>)first).getTreeNode(hash, key);
do {
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
return e;
} while ((e = e.next) != null);
}
}
return null;
}
|
在进行数据保存的时候会自动使用传入Key的数据生成一个hash码,存储的时候是有个Ha sh数值的。 |
在根据Key获取数据的时候依然要将传入的Key通过hash()方法来获取其对应的hash码,所以查询的过程之中首先要利用hashCode()来进行数据查询。在getNode()方法中还可以发现需要进行equals()方法的使用。 |
通过以上分析查看得出结论:
对于自定义Key类型所在的类中一定要重写hashCode()方法和equals()方法
否则无法查找到。
虽然允许使用自定义类进行Key类型的定义,但是也要注意一点,在实际的开发之中对于Map集合的Key常用的类型就是:String、Long、Integer,尽量使用系统类。
面试题:
如果在进行HashMap进行数据操作的时候出现了Hash冲突(Hash码相同),HashMap是如何解决的?
- 当出现了Hash冲突之后为了保证程序的正常执行,会在冲突的位置将所有重复 Hash冲突的内容转为链表保存;
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 分享4款.NET开源、免费、实用的商城系统
· 全程不用写代码,我用AI程序员写了一个飞机大战
· MongoDB 8.0这个新功能碉堡了,比商业数据库还牛
· 白话解读 Dapr 1.15:你的「微服务管家」又秀新绝活了
· 上周热点回顾(2.24-3.2)