集合
集合的框架体系
Collection
- Collection实现子类可以存放多个元素,每个元素都可以是Object
- 有些Collection子类可以存放重复的元素,有些不可以存放重复的元素
- 有些实现子类是有序的,有些是无序的
- Collection没有直接的实现子类,是通过子接口List、Set来实现的
Collection常用方法
//Collection
List list = new ArrayList();
//添加元素
list.add("红楼梦");
list.add("三国");
list.add(true);
list.add(10);
System.out.println(list);
//移出元素
//移出指定索引位置元素
list.remove(0);
System.out.println(list);
//移出指定元素
list.remove(true);
System.out.println(list);
//contains判断某个元素是否存在
System.out.println(list.contains("三国"));
//获取元素个数
System.out.println(list.size());
//判断集合是否为空
System.out.println(list.isEmpty());
//清空集合元素
list.clear();
System.out.println(list);
//添加多个元素
List list2 = new ArrayList();
list2.add("遥远的她");
list2.add("张学友");
list.addAll(list2);
System.out.println(list);
//判断多个元素是否存在
System.out.println(list.containsAll(list2));
//删除多个元素
list.add(true);
list.removeAll(list2);
System.out.println(list);
Collection集合的遍历
1.使用迭代器Iterator
Iterator对象称为迭代器,主要用于遍历Collection集合的元素。所有实现Collection集合的类都有一个iterator()方法,用于返回一个实现了Iterator接口的对象,即返回一个迭代器。Iterator仅仅用于遍历集合,本身并不能存放数据对象
迭代器执行原理:
Iterator iterator = coll.iterator()//得到一个迭代器
hasNext()判断是否还有下一个元素
next()指针下移,将下移以后集合上的元素返回;在调用next之前,必须先调用hasNext
List list = new ArrayList();
list.add("三国演义");
list.add("红楼梦");
list.add("水浒传");
list.add("西游记");
//使用迭代器遍历元素
//首先获取迭代器
Iterator iterator = list.iterator();
//循环遍历迭代器,先判断有无元素,再下移返回元素
while(iterator.hasNext()){
Object obj = iterator.next();
System.out.println(obj);
}
//当迭代器遍历完成以后不能再次使用next()方法,会抛出异常,如果想要再次使用迭代器遍历,重新获取迭代器
2.使用增强for循环遍历集合元素
增强for循环底层仍然是使用迭代器,可以看做是一个简易版的迭代器
//Collection
List list = new ArrayList();
list.add("三国演义");
list.add("红楼梦");
list.add("水浒传");
list.add("西游记");
//增强for循环
for (Object o : list) {
System.out.println(o);
}
List和常用方法
List接口是Collection接口的子接口
1.List接口中的元素是有序的(添加和取出顺序一致),可重复的;
2.List接口中的每个元素都有其对应的顺序索引,从零开始
3.常用的实现类有:ArrayList、LinkedList、Vector
List list = new ArrayList();
//添加元素
list.add("张三丰");
list.add("无涯子");
System.out.println(list);
//在指定索引位置添加元素
list.add(1,"张学友");
System.out.println(list);
//添加所有元素
List list1 = new ArrayList();
list1.add("jack");
list1.add("tom");
list.addAll(list1);
System.out.println(list);
//获取指定索引的元素
Object obj = list.get(1);
System.out.println(obj);
//查找首次出现和最后一次出现
int index = list.indexOf("jack");
int index1 = list.lastIndexOf("jack");
System.out.println(index + " " + index1);
//移出指定位置元素
list.remove(1);
System.out.println(list);
//替换某个位置的元素
list.set(1,"hello");
System.out.println(list);
//截取子集合,左闭右开
List relist = list.subList(0,2);
System.out.println(relist);
List接口的遍历方式
- 使用Iterator迭代器遍历
- 使用增强for循环遍历
- 使用普通for循环遍历
ArrayList底层结构
- ArrayList可以存放任何数据类型,包括null,可以存入多个null
- ArrayList底层是用数组来实现的存储结构
- ArrayList不是线程安全的,在多线程下不要使用
ArrayList底层的扩容机制:
- ArrayList底层维护了一个Object类型的数组elementData
- 当创建ArrayList的时候,使用的是无参构造器,初始elementData的容量为0,第一次添加元素,则扩容为10,若需再次扩容,则后面每次增加当前的1.5倍
- 当创建ArrayList的时候,使用的是有参构造器,初始elementData的容量为指定大小,如需扩容,每次扩容为当前容量的1.5倍
- 使用无参构造函数构造ArrayList的扩容机制情况:
创建一个空的对象数组
当第一次添加元素的时候,进入添加方法,首先会使用ensureCapacityInternal(size + 1)
判断是否需要扩容
进入ensureCapacityInternal方法
首先调用的calculateCapacity判断是否需要扩容,进入方法
我们可以看出,如果我们是第一次添加元素的话,会给我们返回DEFAULT_CAPACITY,也就是10,否则的话就返回minCapacity;
然后我们就会进入ensureExplicitCapacity方法,把我们需要的最小容量传递进入:
modCount记录修改的次数,这时我们会进行一个是否真的需要扩容的判断,如果我们需要的最小容量大于存放数据的数据的容量就进行扩容,否则不进行扩容,不是每次都进行真的扩容。
进入grow对容器进行扩容,传入最小需要的容量,在进行扩容的时候,先保存旧容量,每次扩大1.5倍,第一次会扩容为10,然后使用Arrays.copyOf将原数组内容拷贝到新容量的数组中,并赋值给elementData。至此扩容机制完成,返回到add方法里面进行赋值操作。
每次都会判断是否需要扩容,只有真的需要扩容的时候才会扩容,不是每次都会扩容
2.使用有参构造函数构造ArrayList的扩容机制情况:
有参构造函数的扩容机制和无参构造的扩容机制差不多,唯一的不同是在最初创建的容量是不相同,有参的构造函数最初的容量是指定的大小:
当大于0就创建指定容量的对象数据,等于0就创建空数组,小于0就抛出异常,然后后面的扩容机制跟无参一样,每次扩容1.5倍,每次都会去判断是否需要扩容,只有真的需要扩容的时候才会去扩容。
Vector底层结构和源码解析
1.Vector底层也是一个对象数组elementData;
2.Vector是线程同步的,即线程安全的,方法都是使用synchronized
3.在开发中如果需要使用线程安全的,优先考虑Vector
Vector与ArrayList的比较
底层结构 版本 | 线程安全 效率 | 扩容倍数 | |
---|---|---|---|
ArrayList | Object数组 JDK1.2 | 线程不安全 效率高 | 如果是有参构造函数是1.5倍,无参第一次是10,以后每次都是1.5倍 |
Vector | Object数组 JDK1.0 | 线程安全 效率不高 | 如果是无参,第一次是10,以后每次扩容2倍,如果是有参,每次扩容2倍 |
当使用无参构造函数的时候扩容机制:
可以看出,默认创建10个容量大小的对象数组,内部调用的有参构造函数,继续进入查看:
继续调用其它构造函数,继续查看:
通过这里可以看出,当我们使用无参构造函数的时候,默认创建容量为10的Object数组
当我们第一次添加的元素的时候,继续下面代码:
modCount用于记录修改次数,使用ensureCapacityHelper方法判断是否需要扩容,我们进入查看:
进行一个简单的判断,查看我们需要的容量是否比对象数组的容量大,如果大就扩容
查看扩容方法:
capacityIncrement默认就是0,所以可以看出,每次扩容大小为2倍
当使用有参构造函数的时候,只是最开始创建容量的大小不一样,扩容机制跟无参一样:
这里的initialCapacity为我们指定的创建大小。
LinkedList底层结构
- LinkedList底层实现了双向链表和双向队列的特点
- 可以添加任意元素,包括null
- 线程不安全的,没有实现同步
LinkedList底层维护了一个双向链表,LinkedList中维护了两个属性first,last分别指向头节点和尾节点;每个节点内部又维护了三个属性,prev,next,item三个属性,prev指向前一个节点,next指向后一个节点,最终实现双向链表。LinkedList添加和删除元素很高效率,不是通过数组来实现的。
模拟双向链表:
class Node{
public Object item;
public Node prev;
public Node next;
public Node(Object item) {
this.item = item;
}
@Override
public String toString() {
return "Node{" +
"item=" + item +
'}';
}
}
//模拟双向链表
Node jack = new Node("jack");
Node tom = new Node("tom");
Node wyz = new Node("wyz");
//将节点建立双向链表关系
jack.next = tom;
tom.next = wyz;
wyz.prev = tom;
tom.prev = jack;
//头节点和尾节点指向
Node first = jack;
Node last = wyz;
//遍历链表
while(first != null){
System.out.println(first);
first = first.next;
}
System.out.println("======");
while(last != null){
System.out.println(last);
last = last.prev;
}
System.out.println("======");
//添加一个节点
Node yaona = new Node("yaona");
yaona.next = wyz;
yaona.prev = tom;
tom.next = yaona;
wyz.prev = yaona;
//再次遍历
first = jack;
while(first != null){
System.out.println(first);
first = first.next;
}
LinkedList的CURD底层源码分析
增:
默认我们创建的LinkedList是一个空的链表,这是内部的size为0,first为null,last为null
添加元素里面最核心的方法就是linkLast方法,我们可以看出,添加元素是添加在最后面,进入方法体:
首先将双向链表的last赋值给l,因为我们是第一次添加所以这时l也是null,然后创建一个新节点,将对应的值赋予这个节点
从构造函数可以看出,创建第一个节点的时候,前一个节点和后一个节点分别指向null,创建完成返回之后,将last赋值为这个新创建的节点,判断l是否为空,为空说明是添加的第一个元素,将first也指向这个新创建的节点,如果不为空,将前一个节点的next赋值为新创建的节点,建立双向链表关系。size是记录链表的节点个数,modCount用于记录修改链表的次数。添加完成之后返回真。
我们这里使用的是尾部添加,还可以使用添加的指定位置,当我们使用添加到指定位置的时候分析如下:
首先会检查添加的位置是否是合法的,必须是大于0,小于或者等于链表元素个数,进入方法查看:
检查位置是否合法,合法就返回,不合法就抛出异常
返回之后我们就会进入一个判断,如果我们添加的位置是最后一个就调用linkLast(element);如果不是最后一个就调用linkBefore(element, node(index));两个参数的含义是,第一个是我们要插入的元素,第二个是要插入位置的后一个元素节点
通过二分查找找到要插入元素的后一个元素或者前一个元素,如果是在前半段从first开始遍历查找,找到前一个元素,如果是后半段从last开始遍历查找,找到后一个元素,找到元素之后返回。
然后进行元素插入,建立双向链表关系。
删:
默认情况下是删除第一个元素的节点
判断第一个元素是否为空,为空抛出异常,否则执行真的删除函数:
将第一个元素的内容取出,将第二个元素赋值给next,将第一个元素的指向赋值为null,帮助GC回收空间,判断第二个元素是否为空,为空说明链表为空,将last也赋值为空,赋值的话,将第二个节点的前一个指向赋值为null,修改size大小和修改次数,返回element。
查和改跟增删里面的机制大同小异。
ArrayList和LinkedList的对比
底层结构 | 增删效率 | 改查效率 | |
---|---|---|---|
ArrayList | 数组 | 可能需要扩容,较慢 | 较快 |
LinkedList | 双向链表 | 不需要扩容,较快 | 较慢 |
Set接口
- 无序(添加和取出顺序不一致),没有索引,虽然是无序,但位置是固定的,不会每次取出的位置都在变化
- 不允许出现重复元素,最多添加一个null
- 遍历方式只能使用迭代器或者增强for循环,不能使用for循环,没有索引
Set set = new HashSet();
set.add("jack");
set.add("tom");
set.add(null);
set.add(1);
set.add("tom");//重复添加
System.out.println(set);
//迭代器遍历
Iterator iterator = set.iterator();
while(iterator.hasNext()){
Object obj = iterator.next();
System.out.println(obj);
}
System.out.println("============");
//增强for循环遍历
for(Object obj : set){
System.out.println(obj);
}
HashSet
1.底层实际上是HashMap
2.可以存放null,只能存放一个null
3.HashSet不保证元素是有序的,取决于Hash后,再确定索引
4.不可以存放重复元素
Set set = new HashSet();
set.add("jack");
set.add("jack");
set.add(new Dog1("wyz"));
set.add(new Dog1("wyz"));
//此时有几个对象 3个,两个Dog1添加成功
System.out.println("set: " +set);
set.add(new String("wyz"));
set.add(new String("wyz"));
//有几个对象,4个,只添加成功一个String对象,为什么,这里就有看HashSet底层添加机制
HashSet底层添加元素机制:
- HashSet的底层是HashMap
- 添加一个元素的时候,会得到这个值的hash值,然后将hash值转换成一个索引
- 找到存储数据表table,查看这个索引的位置是否已经存放数据元素
- 如果没有直接加入,如果有,调用equals比较,如果相同则放弃添加,不同就添加到链表的最后
- 在Java8中,如果一条链表的元素个数到达8个,并且table的大小,大于或者等于64,就会进行树化(红黑树)
当我们创建一个HashSet的时候,底层其实是创建了一个HashMap
当我们第一次调用add方法添加元素的时候,每次都会调用下面的语句判断
执行map.put(e,PRESENT);这里的e就是我们添加的内容,将其设置为HashMap结构的键,值是一个Object对象
主要是一个占位符的作用,使得能够使用HashMap。进入put方法
通过调用putVal方法来进行添加元素,在添加之前会调用hash方法,得到我们想要添加元素的哈希值,我们进入hash方法:
是一个静态方法,判断key也就是我们添加的内容是否为null,等于null的时候返回0,不等于null的时候通过一个算法:h = key.hashCode()) ^ (h >>> 16
,通过得到当前key的hashCode然后与自身右移16位结果异或得到最终的哈希值。然后我们进入putVal方法查看:当返回null表示添加成功,不是null表示添加不成功
//传入的参数分别为,得到的key的哈希值,要添加的内容,也就是key,占位符对象PRESENT,false,true
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<K,V>[] table,用来存储节点的数组,当我们第一次添加的时候table为null,所以会进入下面的/方法体内部
if ((tab = table) == null || (n = tab.length) == 0)
//这里有一个很重要的方法resize()扩容方法,将扩容的结果赋值给tab,并将扩容后大小赋值给n,看下面截图查看扩容方法
//执行完第一次扩容之后n=16
n = (tab = resize()).length;
//然后利用一个算法确定我们要添加位置的索引,主要是通过我们要添加的内容的得到的哈希值与(n-1)进行&操作得到,查看对应位置是否为空,当等于空的时候,就将这个key添加到这个索引位置,否则的话进行进行判断
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null);
else {
//当我们想要添加的索引已经有元素的时候,就要进行比较,能不能添加,这里p是我们想要添加索引位置的第一个元素的节点
Node<K,V> e; K k;
//判断p.hash值跟我们想要添加的元素的哈希值是否一样,以及添加的内容是否相同或者equals内容是否相同
//主要的意思是判断两个对象是否是相同,相同则不能添加,将p赋值给e退出,如果不相等就判断当前这个节点是不是红黑树,不是的话就跟后面每一个节点进行比较,通过一个死循环进行判断,只有两种情况能退出,遍历到最后插入元素,或者找到了一个相同的元素,不能添加就退出
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;
//这里的size是我们添加节点的个数,不管是添加在第一个索引位置,还是在链表上面,都算添加,第一次添加,size为1
if (++size > threshold)
resize();
//下面其实在HashMap里面是一个空方法
afterNodeInsertion(evict);
return null;
}
扩容方法resize()
final Node<K,V>[] resize() {
Node<K,V>[] oldTab = table;
//判断table是否为null,为null,那么oldCap为0,threshold默认为0
int oldCap = (oldTab == null) ? 0 : oldTab.length;
int oldThr = threshold;
//用于记录扩容之后的新容量,先赋值为0
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
//第一次会执行下面的语句,将新容量赋值为static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16
//第二次参数是加载因子,这里默认就是使用static final float DEFAULT_LOAD_FACTOR = 0.75f;
//所以第一次赋值结果为:16*0.75=12
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)[],将其赋值给HashMap属性table
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;
}
HashSet的扩容和转成红黑树机制
- HashSet底层是HashMap,第一次添加的时候,table数组就扩容,16,临界值threshold等于16*0.75(加载因子)=12,
- 如果table表使用到了临界值12,就进行扩容,每次扩大2倍,第二次容量就是32,临界值就是32*0.75=24,以此类推。
- 在Java8中如果一条链表的个数达到8个,并且table的大小>=64,链表会进行树化,否则进行数组的扩容机制。
LinkedHashSet
LinkedHashSet是HashSet的子类,底层是一个LinkedHashMap,底层维护了一个数组+双向链表;LInkedHashSet根据元素的HashCode值来决定元素的存储位置,同时使用链表维护链表的次序,这使得元素看起来是以插入的顺序保存,LinkedHashSet不允许添加重复的元素。
-
在LinkedHashSet中维护了一个hash表和双向链表里面有两个属性head和tail分别指向双向链表的头和尾节点
-
每一个节点都有before和after属性,用于记录前一个节点和后一个节点的位置,这样就形成双向链表
-
在添加一个元素的时候先求hash,再求索引,确定该元素在table表中的位置,然后将该元素添加到双向链表(如果已经存在就不添加,跟HashSet规则一样)
tail.next = newElement; newElement.pre = tail; tail = newElement;
-
这样遍历LInkedHashSet也能保证插入顺序和遍历的顺序一样
TreeSet
底层是TreeMap,可以对加入的元素进行排序
当我们构造TreeSet的时候传入一个比较器,就会在我们每次添加元素的时候,对元素进行排序添加,如果我们不传入一个比较器,默认还是无序的。通常我们传入一个比较器Comparator接口的匿名内部类,重写compare编写比较规则。
底层分析
首先我们传入一个比较器的对象的时候调用的是TreeMap的构造器
将我们的比较器赋值给TreeMap内部的一个属性comparator。
当我们添加元素的时候调用add方法
进入put方法体内部:
public V put(K key, V value) {
Entry<K,V> t = root;
if (t == null) {
compare(key, key); // type (and possibly null) check
root = new Entry<>(key, value, null);
size = 1;
modCount++;
return null;
}
int cmp;
Entry<K,V> parent;
// split comparator and comparable paths
//最核心的比较规则
Comparator<? super K> cpr = comparator;
if (cpr != null) {
do {
parent = t;
cmp = cpr.compare(key, t.key);
if (cmp < 0)
t = t.left;
else if (cmp > 0)
t = t.right;
else
return t.setValue(value);
} while (t != null);
}
else {
if (key == null)
throw new NullPointerException();
@SuppressWarnings("unchecked")
Comparable<? super K> k = (Comparable<? super K>) key;
do {
parent = t;
cmp = k.compareTo(t.key);
if (cmp < 0)
t = t.left;
else if (cmp > 0)
t = t.right;
else
return t.setValue(value);
} while (t != null);
}
Entry<K,V> e = new Entry<>(key, value, parent);
if (cmp < 0)
parent.left = e;
else
parent.right = e;
fixAfterInsertion(e);
size++;
modCount++;
return null;
}
Map
- Map用于保存具有映射关系的数据
- Map中的key和value可以是任何引用类型数据,会封装到HashMap&Node对象中
- Map中的Key不允许重复,value可以重复
- Map的key可以为null,value也可以为null,key为null只能有一个,value可以有多个
- 重复添加key会使用后面的元素把前面的元素覆盖
- 常用String类型作为Map的key
- key和value存在一对一的关系,可以通过key找到value
- Map存放数据的结构,一对K-v键值对是存放到HashMap&Node(内部类)中,因为Node实现了Map.Entry接口,所以有一些书上面也说,一对K-V就是一个Entry。
Map接口的遍历方法
Map map = new HashMap();
map.put("001","jack");
map.put("002","tom");
//通过获取Map集合中key的集合来遍历Map
Set keySet = map.keySet();
//使用增强for遍历
for(Object key: keySet){
System.out.println(key+"-" + map.get(key));
}
//使用迭代器遍历
Iterator iterator = keySet.iterator();
while(iterator.hasNext()){
Object key = iterator.next();
System.out.println(key + "-" + map.get(key));
}
//通过获取Map集合中value的集合来遍历,这种情况只能是获取值
Collection values = map.values();
for(Object obj: values){
System.out.println(obj);
}
Iterator iterator1 = values.iterator();
while(iterator1.hasNext()){
Object obj = iterator1.next();
System.out.println(obj);
}
//在Map集合中,我们实现存放数据的结果是HashMap&Node这个内部类,为了方便我们进行遍历这个内部类实现了
//Map.Entry这个接口,所以我们可以将这个HashMap&Node结构对应一个EntrySet结构,里面每一个Entry就是一个
//K-V键值对,其中K可以看出Set集合,V可以看出Collection结构,我们通过将一个Entry转换成Map.Entry来方便遍历
Set entrySet = map.entrySet();
for(Object obj : entrySet){
Map.Entry entry = (Map.Entry) obj;
System.out.println(entry.getKey()+"-" + entry.getValue());
}
Iterator iterator2 = entrySet.iterator();
while(iterator2.hasNext()){
Map.Entry entry = (Map.Entry) iterator2.next();
System.out.println(entry.getKey()+"-"+entry.getValue());
}
HashMap小结
- Map接口常用的实现类:HashMap,HashTable,Properties
- HashMap是Map使用最频繁的实现类
- HashMap是以键值对的方式来存储数据,以HashMap&Node结构存储
- Key不能重复,value可以重复,key可以为null,value也可以为null
- 如果添加相同的key,会覆盖原来的K-V键值对,不会修改key,会修改value
- 与HashSet一样,不会保存映射的顺序,底层是以hash表的方式来存储
- hashMap没有实现线程同步,底层是不安全的
HashMap底层机制
- HashMap底层维护了Node(HashMap&Node)类型的数组table,默认是null
- 创建对象的时候,将加载因子loadfactor初始化为0.75
- 当添加键值对的时候,通过key的哈希值得到在table中的索引,然后判断该索引位置是否有元素,没有则直接添加,如果有元素就继续判断该元素的key和准备添加元素的key是否相等,如果相等则直接替换value,如果不相等就需要继续判断树结构还是数组结构,做出相应的处理,当添加的时候发现容量不够,就需要扩容。
- 第一个添加元素,table就扩容为16,临界值(threshold)为12(16*0.75)
- 以后再扩容,每次table扩大为原来的2倍,临界值也为原来的2倍
- 在Java8中,如果一条链表的元素个数超过:TREEIFY_THRESHOLD(8),并且table的大小>= MIN_TREEIFY_CAPACITY(64)就会进行树化(红黑树)
通过下面代码进行Debug分析:
HashMap hashMap = new HashMap();
hashMap.put("java",10);
hashMap.put("php",10);
hashMap.put("java",20);
System.out.println(hashMap);
1.创建一个HashMap,调用无参构造函数,设置loadFactor加载因子为0.75
2.调用put方法进行元素的添加
从方法可以看出,put方法调用putVal进行元素添加,添加之前会通过hash方法获取对应key的哈希值,进入hash方法查看:
通过判断key是否为null来进行返回哈希值,当不为null的时候,哈希值通过(h = key.hashCode()) ^ (h >>> 16);
得到
进入putVal方法:
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是否为空或者table长度是否为0,第一次默认就是null
if ((tab = table) == null || (n = tab.length) == 0)
//调用resize方法进行扩容,扩容为16
n = (tab = resize()).length;
//通过hash值求对应key在table中的索引,查看对应位置是否有元素,没有则直接添加
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null);
else {
Node<K,V> e; K k;
//当对应位置有元素的时候,查看当前添加的元素的key与第一个元素的key是否相同,不仅仅比较hash还比较equals,
//如果相同则将新的元素替换第一个元素,否则就看当前元素的链表是否是红黑树,如果是进行相应的处理
//如果上面二种情况都不满足,则进行链表的循环遍历,对比链表中的每一个元素是否与当前元素相同,相同就替换,下面的循环是
//一个死循环,只有两种情况可以结束循环,所有元素都不相同将新元素添加到末尾,找到了一个与其相同的元素
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);
//判断当前链表的元素数量是否大于8,大于则要进行树化,看是否进行树化,最终是否树化还要看是否满足条件
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
//进行元素的value替换
V oldValue = e.value;
if (!onlyIfAbsent || oldValue == null)
e.value = value;
afterNodeAccess(e);
return oldValue;
}
}
//判断是否需要进行扩容,通过判断当前table中的元素个数和临界值比较判断
++modCount;
if (++size > threshold)
resize();
afterNodeInsertion(evict);
return null;
}
判断是否要进行树化,将链表转换成红黑树要看下面条件是否成立:
HashTable
存放的元素是键值对,其中key和value都不能为null,否则会抛出异常,使用方法和HashMap基本相同,HashTable是线程安全的。
Hashtable底层存储元素的结构是Hashtable&Entry[] table,使用无参构造函数的时候创建大小为11,加载因子是0.75,当添加元素的时候调用put:
从代码中可以看出当value为null的时候抛出异常,通过key的hashCode进行简单算法:(hash & 0x7FFFFFFF) % tab.length得到一个索引位置,查看对应索引位置是否有元素,当没有元素的时候直接调用addEntry添加元素,有元素则进入循环判断,如果链表中的某一个元素与要添加的元素相同,则进行value替换。
通过判断当前元素的个数和临界值比较,进行是否扩容,第一次临界值为8(11*0.75)
每次扩容大小为2倍+1。
版本 | 线程安全 | 效率 | 允许key为null,value为null | |
---|---|---|---|---|
HashMap | 1.2 | 不安全 | 效率高 | 可以 |
Hashtable | 1.0 | 安全 | 效率低 | 不可以 |
Properties
Properties继承自Hashtable并实现了Map接口,并且也是一对键值对保存数据,使用特点和Hashtable一致,K和V都不能为null,Properties还可以从xxx.properties文件中加载数据到Properties类对象,并进行读取和修改。XXX.properties文件通常用作配置文件。
Properties properties = new Properties();
properties.put("php","101");
properties.put("java","102");
System.out.println(properties);
//读取
System.out.println(properties.get("java"));
System.out.println(properties.getProperty("php"));
//删除
properties.remove("php");
System.out.println(properties);
//改
properties.put("java","104");
System.out.println(properties);
TreeMap
可以对键值对进行排序,底层存储数据结构是Entry(TreeMap$Entry)
当我们创建对象的时候传入一个构造器,用于对添加的元素进行排序
我们看最核心的添加排序方法:
public V put(K key, V value) {
Entry<K,V> t = root;
//第一次添加数据的时候,root等于空
if (t == null) {
compare(key, key); // type (and possibly null) check
//创建一个Entry对象用于存储数据,并且第一次添加没有父节点
root = new Entry<>(key, value, null);
size = 1;
modCount++;
return null;
}
int cmp;
Entry<K,V> parent;
// split comparator and comparable paths
//获取比较器
Comparator<? super K> cpr = comparator;
if (cpr != null) {
do {
parent = t;
//对添加的元素进行比较添加
cmp = cpr.compare(key, t.key);
if (cmp < 0)
t = t.left;
else if (cmp > 0)
t = t.right;
else
//相同则替换值
return t.setValue(value);
} while (t != null);
}
else {
if (key == null)
throw new NullPointerException();
@SuppressWarnings("unchecked")
Comparable<? super K> k = (Comparable<? super K>) key;
do {
parent = t;
cmp = k.compareTo(t.key);
if (cmp < 0)
t = t.left;
else if (cmp > 0)
t = t.right;
else
return t.setValue(value);
} while (t != null);
}
Entry<K,V> e = new Entry<>(key, value, parent);
if (cmp < 0)
parent.left = e;
else
parent.right = e;
fixAfterInsertion(e);
size++;
modCount++;
return null;
}
Entry(K key, V value, Entry<K,V> parent) {
this.key = key;
this.value = value;
this.parent = parent;
}