Map集合
在数据结构之中,除了进行单个对象的保存,也可以进行二元偶对象的保存(key=value),通过key获取value
Collection集合保存数据的目的是为了输出,Map集合保存数据是为了key的查找
1、Map接口
Map接口是二元偶对象保存的最大父接口,定义如下:
public interface Map<K,V>
该接口是一个独立的父接口,在进行接口对象实例化的时候需要设置Key与Value
Map接口中定义有许多方法,其中核心操作方法有:
No | 方法名称 | 类型 | 描述 |
---|---|---|---|
1 | V put(K key,V value) | 普通 | 向集合中保存数据 |
2 | V get(Object key) | 普通 | 根据key查询数据 |
3 | Set<Map.Entry<K,V>> entrySet() | 普通 | 将Map集合转为Set集合 |
4 | boolean containsKey(Object key) | 普通 | 查询指定的key是否存在 |
5 | Set keySet() | 普通 | 将Map集合的key转为Set集合 |
6 | V remove(Object key) | 普通 | 根据key删除指定数据 |
Map集合集合中也提供了static方法
观察Map集合特点
public class JavaDemo {
public static void main(String[] args) throws Exception {
Map<String, Integer> map = Map.of("One" , 1 , "Two" , 2);
System.out.println(map);
}
}
在Map集合之中数据保存就是按照“key=value”的形式存储的
使用of()方法时,里面的数据是不允许重复的,如果重复则会出现“IllegalArgumentException“异常,如果设置为null,会出现“NullPointerException”异常
of()方法并不是Map集合的标准用法,开发之中需要通过Map集合的子类进行接口对象实例化
常用子类有:HashMap , Hashtable , TreeMap , LinkedHashMap
2、HashMap子类
HashMap定义:
public class HashMap<K,V>
extends AbstractMap<K,V>
implements Map<K,V>, Cloneable, Serializable
HashMap主要特点:无序存储
HashMap继承结构:
观察Map集合的使用
public class JavaDemo {
public static void main(String[] args) throws Exception {
Map<String, Integer> map = new HashMap<>();
map.put("one" , 1);
map.put("two" , 2);
map.put("one" , 3); //key重复
map.put(null , 0); //key 为 null
map.put("zero" , null); //value 为 null
System.out.println(map);
}
}
//{null=0, zero=null, one=3, two=2}
以上的操作形式为Map集合使用的最标准的处理形式
通过HashMap实例化的Map接口可以针对key或value保存null的数据,即便保存数据的key重复,也不会出现异常,而是出现内容替换
Map接口中的put()方法本身是有返回值的,这个返回值是在重复key的情况下返回旧的value
观察put方法
public class JavaDemo {
public static void main(String[] args) throws Exception {
Map<String, Integer> map = new HashMap<>();
System.out.println(map.put("one" , 1)); //key不重复,返回null
System.out.println(map.put("one" , 3)); //key重复,返回旧数据
}
}
//null
//1
当使用无参构造时会有一个loadFactor属性,该属性默认值为0.75
static final float DEFAULT_LOAD_FACTOR = 0.75f;
public HashMap() {
this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted
}
在使用put()方法进行数据保存时会调用一个putVal()方法,同时会将key进行hash处理(生成hash码)
public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
}
putVal()方法里面依然提供有一个Node节点类进行数据的保存
在使用putVal()方法时会调用resize()方法,此方法是进行容量的扩充
面试题:在进行HashMap的put()操作的时候,如何实现容量扩充的?
- 在HashMap类中提供有一个“DEFAULT_INITIAL_CAPACITY”常量,作为初始化容量配置,该常量默认大小为16个元素
- 当保存内容的容量超过了阈值(DEFAULT_LOAD_FACTOR = 0.75f),相当于“容量 * 阈值 = 12”个时就会进行容量扩充
- 在进行扩充的时候HashMap采用的是成倍的扩充模式,即:每次扩充2倍容量
面试题:请解释HashMap的工作原理?(JDK1.8之后开始的)
- 在HashMap中进行数据存储依然利用了Node类完成,这种情况就证明可以使用的数据结构只有两种:链表(时间复杂度:“O(n)”)、二叉树(时间复杂度:“O(logn)”)
- 从JDK 1.8开始,HashMap的实现出现了改变,因为其要适应于大数据时代,所以对于存储发生了变化,在HashMap类内部提供有一个重要的常量:“static final int TREEIFY_THRESHOLD = 8;”。在使用HashMap保存时,如果保存数据个数没有超过阈值(8),则会按照链表的形式存储;如果超过了,则将链表转为红黑树,利用左旋与右旋保证数据的查询性能
3、LinkedHashMap子类的子类
LinkedHashMap是基于链表实现的,保存数据的顺序为增加顺序
LinkedHashMap定义:
public class LinkedHashMap<K,V>
extends HashMap<K,V>
implements Map<K,V>
因为是链表保存,所以使用LinkedHashMap时数据量不要特别大,否则时间复杂度攀升
LinkedHashMap继承关系:
使用LinkedHashMap
public class JavaDemo {
public static void main(String[] args) throws Exception {
Map<String, Integer> map = new LinkedHashMap<>();
map.put("one" , 1);
map.put("two" , 2);
map.put("one" , 3); //key重复
map.put(null , 0); //key 为 null
map.put("zero" , null); //value 为 null
System.out.println(map);
}
}
//{one=3, two=2, null=0, zero=null}
LinkedHashMap进行存储的保存数据是添加顺序
4、Hashtable子类
Hashtable定义:
public class Hashtable<K,V>
extends Dictionary<K,V>
implements Map<K,V>, Cloneable, Serializable
Hashtable继承结构:
观察Hashtable的使用
public class JavaDemo {
public static void main(String[] args) throws Exception {
Map<String, Integer> map = new Hashtable<>();
map.put("one" , 1);
map.put("two" , 2);
map.put("one" , 3); //key重复
System.out.println(map);
}
}
//{two=2, one=3}
在使用Hashtable时,key和value都不允许为空,否则会报“NullPointerException”异常
面试题:请解释HashMap与Hashtable的区别?
- HashMap中的方法都属于异步操作(非线程安全),HashMap允许保存null的数据
- Hashtable中的方法都属于同步操作(线程安全),Hashtable不允许保存null的数据,否则出现“NullPointerException”异常
5、Map.Entry内部接口
List(LinkedList子类)依靠的是链表实现的数据存储,在进行存储数据时将数据保存在Node节点中,在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:K getKey()
- 获取value:V getValue()
JDK1.9之后,Map接口中追加了一个新的方法:
- 创建Map.Entry对象:public static <K,V>Map.Entry<K,V> entry(K k,V v)
创建Map.Entry对象
public class JavaDemo {
public static void main(String[] args) throws Exception {
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
在整个Map集合里面,Map.Entry的主要作用就是作为Key和Value的包装类型使用,大部分情况下在进行存储时都会将key和value包装为一个Map.Entry对象进行使用
6、利用Iterator输出Map集合
Map集合中没有方法可以直接返回Iterator接口对象
在Map集合里面保存的实际上是一组Map.Entry_接口对象(里面包装的是Key与Value),因此Map依然实现的是单值的保存
在Map中有一个方法:Set<Map.Entry<K,V>> entrySet() , 将全部的Map集合转为Set集合
如果要使用Iterator实现Map集合的输出,必须按照以下步骤处理:
- 利用Map接口中提供的entrySet()方法将Map集合转为Set集合
- 利用Set接口中的Iterator()方法将Set集合转为Iterator接口实例
- 利用Iterator进行迭代输出获取每一组的Map.Entry对象,随后通过getKey()与getValue()获取数据
利用Iterator输出Map集合
public class JavaDemo {
public static void main(String[] args) throws Exception {
Map<String, Integer> map = new HashMap<>();
map.put("one" , 1);
map.put("two" , 2);
Set<Map.Entry<String, Integer>> set = map.entrySet(); //将Map集合变为Set集合
Iterator<Map.Entry<String, Integer>> iterator = set.iterator();
while (iterator.hasNext()) {
Map.Entry<String, Integer> next = iterator.next();
System.out.println(next.getKey() + " = " + next.getValue());
}
}
}
//one = 1
//two = 2
虽然Map集合支持迭代输出,但是Map的主要用法是实现数据的key的查找操作
如果不使用Iterator,而使用foreach输出,也需要将Map集合转为Set集合
使用foreach输出Map集合
public class JavaDemo {
public static void main(String[] args) throws Exception {
Map<String, Integer> map = new HashMap<>();
map.put("one" , 1);
map.put("two" , 2);
Set<Map.Entry<String, Integer>> set = map.entrySet(); //将Map集合变为Set集合
for (Map.Entry<String , Integer> entry : set) {
System.out.println(entry.getKey() + " = " + entry.getValue());
}
}
}
//one = 1
//two = 2
7、自定义Map的key类型
对于自定义Key类型所在的类中一定要重写hashCode()与equals()方法
使用自定义类作为Key类型
public class JavaDemo {
public static void main(String[] args) throws Exception {
Map<Person, String> map = new HashMap<>();
map.put(new Person("张三" , 18) , "linlin");
System.out.println(map.get(new Person("张三" , 18)));
}
}
class Person{
private String name;
private int age;
public Person(String name, int age) {
this.name = name;
this.age = age;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Person person = (Person) o;
return age == person.age && Objects.equals(name, person.name);
}
@Override
public int hashCode() {
return Objects.hash(name, age);
}
}
//linlin
虽然允许使用自定义类作为Key的类型,但在开发之中,Map集合的Key常用类型就是:String , Long , Integer , 尽量使用系统类
面试题:如果在进行HashMap数据操作时出现了Hash冲突(Hash码相同),HashMap是如何解决的?
- 当出现了Hash冲突后为了保证程序的正常执行,会在冲突的位置上将所有Hash冲突的内容转为链表保存