Java
day15——2023.8.25
Map
之前的Collection称为集合,使用它可以存放单个的对象,可以快速找到我们想要的元素。
而Map称为映射,它可以存放一对键值数据,一对key:value数据
比如:name:张三,age:20,1001:Student{张三,20,1班}
Map和Collection的区别
1,Map中的元素,是成对出现的,Map中的键是唯一的,值是可以重复的,可以有null值
2,Collection中的元素,都是单个出现的,Collection的子类,set中元素是唯一的,不能重复,List是可以重复的
Map的功能
1.添加功能
V put(K key , V value)
将指定的值与该映射中的指定键相关联(可选操作)。
2.获取功能
V get(Object key)
返回到指定键所映射的值,或 null如果此映射包含该键的映射。
Set<Map.Entry<K,V>> entrySet()
返回此Map中包含的映射的Set视图。
Set keySet()
返回此Map中包含的键的Set视图。
Collection values()
返回此地图中包含的值的Collection视图
3.判断功能
boolean isEmpty()
如果此Map不包含键值映射,则返回 true 。
boolean containsKey(Object key)
如果此映射包含指定键的映射,则返回 true 。
boolean containsValue(Object value)
如果此地图将一个或多个键映射到指定的值,则返回 true 。
4.删除功能
V remove(Object key)
如果存在(从可选的操作),从该地图中删除一个键的映射。
void clear()
从该地图中删除所有的映射(可选操作)。
5.长度功能
int size()
返回此地图中键值映射的数量。
Map的常用子类 HashMap
HashMap基于哈希表的实现的Map接口。
此实现提供了所有可选的Map操作,并允许null的值和null键,null键不能重复。线程不安全的,存放数据是无序的
构造方法
HashMap()
构造一个空的 HashMap ,默认初始容量(16)和默认负载系数(0.75)。
HashMap(int initialCapacity)
构造一个空的 HashMap具有指定的初始容量和默认负载因子(0.75)。
HashMap(int initialCapacity, float loadFactor)
构造一个空的 HashMap具有指定的初始容量和负载因子
public class MapDemo {
public static void main(String[] args) {
Map<String, String> map = new HashMap() ; //键值都是String
//通过put方法,完成map数据的存放
map.put("one","hello");
map.put("one","mysql");
map.put("two","java");
map.put("three","world");
map.put("1","oracle");
map.put(null,"spring"); //可以把null作为键
map.put(null,"Linux"); //可以把null作为键,相同的键对应的值被覆盖
System.out.println(map);
//获取
System.out.println(map.get("one"));
System.out.println(map.get("2")); //获取不存在,返回null
//判断
System.out.println(map.containsKey("one")); //true
System.out.println(map.containsValue("hello"));//false
//移除
map.remove("one");
System.out.println(map);
//长度
System.out.println(map.size());
System.out.println("------------------------");
//遍历
//第一种:先获取key,通过key再获取值
Set<String> set = map.keySet(); //先获取所有key的set集合
for (String key : set) {
//遍历set
System.out.println(key + "----" + map.get(key));
}
System.out.println("------------------------");
Map<Integer, String> map1 = new HashMap() ;//键是Integer,值是String
map1.put(1,"hello");
map1.put(2,"java");
map1.put(3,"oracle");
System.out.println(map1);
//分开分别获取key和value
for (Integer key : map1.keySet()) {
System.out.println("key:" + key);
}
for (String value : map1.values()) {
System.out.println("value:" + value);
}
System.out.println("------------------------");
Map<String, Object> map2 = new HashMap() ;//键是String,值是对象
map2.put("name","张三");
map2.put("age",20);
map2.put("1001",new Student("李四",22,"1班"));
System.out.println(map2);
//使用entrySet()方法来实现遍历
Set<Map.Entry<String, Object>> entries = map2.entrySet();
//使用增强for遍历返回的entrySet
for (Map.Entry<String, Object> entry : entries) {
System.out.println(entry.getKey() + "--" + entry.getValue());
}
System.out.println("------------------------");
//使用iterator遍历返回的entrySet
Iterator<Map.Entry<String, Object>> iterator = entries.iterator();
while (iterator.hasNext()){
Map.Entry<String, Object> entry = iterator.next();
System.out.println(entry.getKey() + "--" + entry.getValue());
}
}
}
HashMap的源码解析
HashMap的底层结构 :jdk1.7 数组+链表 ,jdk1.8 之后 数组+链表+红黑树
数组和链表都可以按照用户的意愿来排列元素的次序,是有序(存取有序),
链表中也有缺点,想要获取某个元素,要访问所有元素,直到找到位置,这是比较耗时的
数组+链表的组合,可以看做是一个新的数据结构:散列表 (hash表)
这个结构在存储的时候,不在意元素的顺序,也能够快速的找到元素。
散列表的原理
将每个对象 计算出一个 数值,这个数值称为散列码(hash码),根据这些散列吗,计算数据保存的位置。
每个散列表,在Java中,也称之为 桶 。
一个桶上,多个对象存储的时候可能会遇到散列码相同的情况,这个是,散列码相同的对象,就得存储到同一个位置上。这种情况称为 :散列冲突(hash冲突)
发生散列冲突后,就需要将存放的数据,进行比较,看看发生冲突的对象是否是同一个对象,如果是同一个对象,将原来的替换,如果不是,将对象添加在桶的链表上
HashMap的基本说明
注释上的说明:
基于哈希表的实现的Map接口,允许null的值和null键,不能保证Map的顺序
HashMap的对象有两个影响其性能的参数,初始容量16和负载因子0.75
初始容量太高和负载因子太低,都是不太好的,会影响性能
当存储的数量达到了16*0.75=12的时候,哈希表开始扩容,扩容2倍
如果要存储很多数据在Map中,可以在初始化的时候,把容量设置的高一些,可以提升效率
HashMap是线程不安全的
属性的说明:
默认的初始容量值是16,必须是2的n次方
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4;
最高容量是2的30次方
static final int MAXIMUM_CAPACITY = 1 << 30;
默认的负载因子是0.75f
static final float DEFAULT_LOAD_FACTOR = 0.75f;
内部链表结构转成红黑树结构的节点,链表的数量=8个
static final int TREEIFY_THRESHOLD = 8;
当树的结构中数量=6了,退化为链表
static final int UNTREEIFY_THRESHOLD = 6;
链表结构转为红黑树另一个要满足的条件,桶的数量=64
static final int MIN_TREEIFY_CAPACITY = 64;
构造方法
无参构造
public HashMap() {
this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted
}
有参构造
public HashMap(int initialCapacity) {
this(initialCapacity, DEFAULT_LOAD_FACTOR);
}
public HashMap(int initialCapacity, float loadFactor) {
//如果初始值、加载因子小于0或者是非数字,就抛异常
//如果初始值超过了最大范围,就把初始值设为最大值
this.loadFactor = loadFactor;
this.threshold = tableSizeFor(initialCapacity);
}
tableSizeFor(initialCapacity) 用来将传入的数值转成2的n次方后,返回
《HashMap中内容解析》
普通方法
put方法:
1,将key和value传入put方法后,先把key传入hash方法后,完成计算,在传入putVal这个方法
public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
}
2,key传入hash方法后,将key的hashCode值,和key的hashCode值右移16位后,做的位异或运算
这种计算方式是为了减少hash冲突。
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
3,将key经过hash计算后的值、key本身的值、value值,传入putVal
这个是putVal方法中的一个代码片段,表示存放元素的时候,将数组(hashmap对象)的长度-1之后,和key计算出来的hash值做运算,通过运算结果判断这个数组的下标位置是否存在元素,如果没有元素,就将元素传入新节点,创建后,放到这个数组的下标位置
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null);
代码拆分:
i = (n - 1) & hash; //计算出来数组的下标
p = tab[i]; //通过下标取出来的值
if(p == null){ //如果这个数组的下标元素为null,那么将这个下标位置指定元素
tab[i] = newNode(hash, key, value, null);
}
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;
}
HashMap中怎么使用散列表存放元素
1,先判断键值对数组table[i]是否为null或者为空,如果是空,执行reisze()方法进行扩容
2, 根据键值key计算hash值,得到插入的数组索引位置 i,如果table[i] == null,直接创建节点,插入到数组下标i位置 ,然后计算是否需要扩容,如果table[i] 不为空,就继续下一步
3,判断table[i] 的首个元素是否和key一样,如果相同(通过hashCode值和equals判断),直接把value覆盖,如果不同,继续下一步
4,判断table[i] 是否是一颗红黑树,如果是红黑树,直接在树上插入键值对数据,否则继续下一步
5,遍历table[i],判断元素的next节点是否为null,如果是null,就把新的节点放入链表中,并让上一个节点的next,指向新节点,接着判断链表的长度是否大于8,如果大于8,把链表转为红黑树
6,插入成功,判断实际的size值是否超过了 扩容的阈值,如果超过,则进行扩容
HashMap中,如何扩容?
通过resize()方法完成扩容
final Node<K,V>[] resize() {
Node<K,V>[] oldTab = table;
int oldCap = (oldTab == null) ? 0 : oldTab.length;
int oldThr = threshold;
int newCap, newThr = 0;
if (oldCap > 0) {
if (oldCap >= MAXIMUM_CAPACITY) {
threshold = Integer.MAX_VALUE;
return oldTab;
}
else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
oldCap >= DEFAULT_INITIAL_CAPACITY)
newThr = oldThr << 1; // double threshold
}
else if (oldThr > 0) // initial capacity was placed in threshold
newCap = oldThr;
else { // zero initial threshold signifies using defaults
newCap = DEFAULT_INITIAL_CAPACITY;
newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
}
if (newThr == 0) {
float ft = (float)newCap * loadFactor;
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) {
oldTab[j] = 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 { // preserve order
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) {
if (loTail == null)
loHead = e;
else
loTail.next = 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;
}
get方法步骤
先计算key的hash值,调用getNode()方法获取到对应的value
getNode() :先计算hash是否在hash表上,如果在桶的首位能找到,直接返回,否则看看是否是红黑树,如果是,就在树中找,不是就遍历链表
remove()方法
通过计算key的hash值删除value
具体通过removeNode()方法实现:
判断桶是否为空,映射的哈希值是否存在,如果在桶的首位找到对应的元素,记录下来,
不在首位就去红黑树或者链表中 去找到,找到对应的节点了,将内容删除
Map.Entry对象
Map中没有迭代器对象,所以Map的遍历只能通过将键值返回到集合中的方式,完成遍历
第一种遍历方式,通过Map子代的keySet()方法和values()方法
keySet()方法用来返回所有的key的set集合,可以用来获取到所有的key
values()方法,用来返回所有的value的集合,然后可以用来获取所有的value
第二种遍历方式,通过Map中的entrySet()方法
Set<Map.Entry<K,V>> entrySet()
这个方法方法,map对象调用后,也是返回一个set集合,但是这个set集合中,存放的是Map.Entry对象
Map.Entry是Map接口的内部接口,里面提供了getKey()和getValue()两个抽象方法,既然存在这两个方法,那么返回的set集合中的每个Entry对象,都可以通过这两个方法,获取键和值
通过查看源码发现,HashMap中的 内部类 Node,实现了Map.Entry接口
static class Node<K,V> implements Map.Entry<K,V>
这个Node实际上又是存放HashMap中键值数据的节点,所以说,其实每个Map.Entry对象就是一对键值数据的存放对象(通过它的自实现类Node来实现数据存放)
那既然实现了Map.Entry接口,Node类必然实现了接口中的方法,getKey()和getValue()方法
因为key和value 是Node类的属性,所以,直接以getter方法的形式返回属性值就可以了。
public final K getKey() { return key; }
public final V getValue() { return value; }
HashMap和HashTable的区别
从存储结构来讲,HashTable除了没有红黑树,其他结构一样,HashTable扩容 是 2n+1,它和HashMap最大的不同是,HashTable是线程安全的,不允许key和value为null,HashTable是一个过时的集合了,所以将来如果需要使用线程安全的HashMap,一般使用ConcurrentHashMap
HashMap总结
1,JDK8中,HashMap底层结构是 :数组+单向链表 + 红黑树, 无序、可以存放null值,null键,键不能重复
2,HashMap的初始容量是16,负载因子默认是0.75,当容量达到16*0.75=12的时候,会扩容
3,初始容量和负载因子都可以自己设置 ,负载因子设置的过大和过小都不太好,
过大的话,会减少扩容,增加哈希冲突,
过小可以减少冲突,扩容次数变多
4,如果需要存入大量元素,那么可以指定一个特定的初始容量,HashMap的容量值都是2的n次方
5,当散列表中链表的数量超过8,并且散列表的容量超过了64,链表会进行转树的操作
LinkedHashMap
继承结构
注释说明
1,底层是 散列表(数组+链表) + 双向链表
2,允许为null,线程不安全
3,插入顺序有序的
4,负载因子和初始容量影响LinkedHashMap的性能
构造方法
LinkedHashMap()
构造具有默认初始容量(16)和负载因子(0.75)的空插入创建 LinkedHashMap实例。
LinkedHashMap(int initialCapacity)
构造具有指定初始容量和默认负载因子(0.75)的空插入创建 LinkedHashMap实例。
LinkedHashMap(int initialCapacity, float loadFactor)
构造具有指定初始容量和负载因子的空插入创建 LinkedHashMap实例。
LinkedHashMap(int initialCapacity, float loadFactor, boolean accessOrder)
accessOrder - 创建模式 - true的访问顺序, false的插入顺序
构造一个空的 LinkedHashMap实例,具有指定的初始容量,负载因子和创建模式。
public class HashMapDemo {
public static void main(String[] args) {
LinkedHashMap<String, String> linkedHashMap = new LinkedHashMap<>();
//添加元素
linkedHashMap.put("one","hello");
linkedHashMap.put("two","world");
linkedHashMap.put("three","java");
linkedHashMap.put("four","oracle");
//获取
System.out.println(linkedHashMap.get("two"));
//判断
System.out.println(linkedHashMap.containsKey("one"));
System.out.println(linkedHashMap.containsValue("hello"));
//移除
linkedHashMap.remove("two");
System.out.println(linkedHashMap);
//遍历
Set<String> set = linkedHashMap.keySet();
for (String key : set) {
System.out.println(key + " -- " + linkedHashMap.get(key));
}
//通过entrySet完成遍历
}
}
accessOrder - 创建模式 - true的访问顺序, false的插入顺序
public class LinkedHashMapDemo {
public static void main(String[] args) {
//accessOrder - 创建模式 - true的访问顺序, false的插入顺序
LinkedHashMap<Integer, String> map =
new LinkedHashMap(16,0.75f,true);
int i = 1;
map.put(i++,"hello1");
map.put(i++,"hello2");
map.put(i++,"hello3");
map.put(i++,"hello4");
map.put(i++,"hello5");
//访问之后,元素的顺序被改变
System.out.println(map.get(3));
System.out.println(map.get(1));
//Set<Integer> sets = map.keySet();
//for (Integer key : sets) {
// System.out.println(key + "--" + map.get(key)) ;
//}
Set<Map.Entry<Integer, String>> entries = map.entrySet();
for (Map.Entry<Integer, String> entry : entries) {
System.out.println(entry.getKey() + "--" + entry.getValue());
}
}
}
LinkedHashMap总结
LinkedHashMap是HashMap的子类,LinkedHashMap比HashMap多了一个双向链表的结构
LinkedHashMap大部分方法都是来自HashMap
它们之间很多的地方都体现了多态的使用
LinkedHashMap可以设置两种遍历顺序:
访问顺序 和 插入顺序,默认是插入顺序的
TreeMap
注释说明
TreeMap实现了NavigableMap接口,NavigableMap继承了SortedMap,使用TreeMap有序
TreeMap的层是红黑树
线程不同步的
使用Comparator或者Comparable实现键的比较,完成最终数据的排序
构造方法
TreeMap()
使用其键的自然排序构造一个新的空树状图。
TreeMap(Comparator<? super K> comparator)
构造一个新的,空的树图,按照给定的比较器排序。
TreeMap(Map<? extends K,? extends V> m)
构造一个新的树状图,其中包含与给定地图相同的映射,根据其键的 自然顺序进行排序 。
TreeMap(SortedMap<K,? extends V> m)
构造一个包含相同映射并使用与指定排序映射相同顺序的新树映射。
public class TreeMapDemo {
public static void main(String[] args) {
//用无参构造创建的TreeMap,默认使用自然排序
TreeMap<String, String> map = new TreeMap<>();
//添加元素
map.put("1","hello");
map.put("2","java");
map.put("4","world");
map.put("5","spring");
map.put("3","list");
System.out.println(map);
//构造TreeMap对象的时候,键必须要可以互相比较
//如果传入键的时候键的数据类型不一致,可能会导致异常的发生
TreeMap<Object, String> map1 = new TreeMap<>();
//map1.put(1,"hello");
//map1.put("a","java");
//map1.put(new Student(),"oracle");
//System.out.println(map1);
//使用对象作为键,作为键的对象,必须要实现Comparable(比较器)接口
//重写其中的CompareTo()方法
TreeMap<Student, String> map2 = new TreeMap<>();
map2.put(new Student("jack",20,"1班"),"hello");
map2.put(new Student("lucy",22,"1班"),"oracle");
map2.put(new Student("zs",22,"2班"),"java");
map2.put(new Student("lisi",24,"2班"),"mysql");
map2.put(new Student("wangwu",21,"2班"),"spring");
System.out.println(map2);
}
}
public class Student implements Comparable<Student> {
private String name;
private int age;
private String className;
public Student() {
}
public Student(String name, int age, String className) {
this.name = name;
this.age = age;
this.className = className;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (!(o instanceof Student)) return false;
Student student = (Student) o;
if (age != student.age) return false;
if (name != null ? !name.equals(student.name) : student.name != null) return false;
return className != null ? className.equals(student.className) : student.className == null;
}
@Override
public int hashCode() {
int result = name != null ? name.hashCode() : 0;
result = 31 * result + age;
result = 31 * result + (className != null ? className.hashCode() : 0);
return result;
}
@Override
public String toString() {
return "Student{" +
"name='" + name + '\'' +
", age=" + age +
", className='" + className + '\'' +
'}';
}
@Override
public int compareTo(Student o) {
if (o.age> this.age){
return -1;
}
if (o.age < this.age){
return 1;
}
if (o.name.compareTo(this.name) > 0){
return -1;
}
if (o.name.compareTo(this.name) < 0){
return 1;
}
if (o.className.compareTo(this.className) > 0){
return -1;
}
if (o.className.compareTo(this.className) < 0){
return 1;
}
return 0;
}
}
构造TreeMap对象的时候,传入Comparator对象
public class User {
private String name;
private int age;
public User(String name, int age) {
this.name = name;
this.age = age;
}
public User() {
}
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 "User{" +
"name='" + name + '\'' +
", age=" + age +
'}';
}
}
public class TreeMapDemo01 {
public static void main(String[] args) {
TreeMap<User, String> treeMap =
new TreeMap<>(new Comparator<User>() {
@Override
public int compare(User o1, User o2) {
if (o1.getAge() > o2.getAge()){
return 1;
}
if (o1.getAge() < o2.getAge()){
return -1;
}
if (o1.getName().compareTo(o2.getName()) > 0){
return 1;
}
if (o1.getName().compareTo(o2.getName()) < 0){
return -1;
}
return 0;
}
});
treeMap.put(new User("jack",20),"java");
treeMap.put(new User("tom",22),"hello");
treeMap.put(new User("tony",19),"hello");
treeMap.put(new User("lucy",22),"hello");
System.out.println(treeMap);
}
}
TreeMap的总结
1.底层是红黑树,能够实现该Map集合有序(自然排序)
2.key不能为null
3.可以在构造方法中传入Comparator对象,实现比较方法,也可以让键对象实现Comparator接口,并重写compareTo()方法来实现 键的自然排序
4.TreeMap也是线程不安全的
1,常见的数据结构及特点
栈:先进后出
队列:先进先出
数组:查询速度快,增删速度慢
链表:查询速度慢,增删速度快
树:每个节点有0个或多个子节点;没有父节点的节点叫做根节点;每一个非父节点,有且仅有一个父节点;除了根节点外,每个子节点可以分为多个不相交的子树;增删查询都很快
2,Collection的体系结构
列表(List)
集(Set)
映射(Map)
栈(Stack)
队列(Queue)
3,List集合的特点,Map的特点
List接口是Collection接口的子接口,表示有序的序列,可以通过索引访问到List中的元素
List中可以存放 重复的元素 ,并且可以存放null,存放多个null
List接口中提供了 ListIterator,除了实现Iterator接口中的往后遍历的方法,还写了往前遍历的方法
Map一次存储两个元素,一个是key(键),一个是value(值)
Map中的key(键)是唯一的,
Map中的value(值)是可以重复的,可以有null值
4,ArrayList的特点
可变数组实现List接口,相当于一个动态数组实现所有方法
可以存放任意类型的元素,包括null
创建ArrayList会有一个容量,来定义将来存放元素的数组的大小
5,HashMap的特点
键值对存储
快速查询
无序存储
不允许键重复
允许存放空键和空值
非线程安全
容量可调
迭代顺序不确定