集合体系图
Collection常用方法
1)add:添加单个元素
2)remove:删除指定元素
3)contains:查找元素是否存在
4)size:获取元素个数
5)isEmpty:判断是否为空
6)clear:清空
7)addAll::添加多个元素
8)containsAll::查找多个元素是否都存在
9)removeAll:删除多个元素
Interator使用
Iterator属于Collection接口的方法,只要实现了Collection接口就能用Iterator
快捷键 itit 直接生成带 while 的循环
Collection arrayList = new ArrayList();
arrayList.add("asd");
arrayList.add("as2d");
arrayList.add("a3sd");
Iterator iterator = arrayList.iterator();
while (iterator.hasNext()) {
Object next = iterator.next();
System.out.println("next = " + next);
}
Iterator迭代一遍后next的位置在最后一个元素上,如果想要再重新迭代需要再用arrayList.iterator();方法生成一个新的Iterator
增强for
增强for是简化版的迭代器,底层也使用了Iterator。
快捷键 I (大 i)或 arrayList.iter 或 arrayList.for 或直接 iter
List
List有序,可重复,可用索引取元素
- public void add(int index, E element) 在index位置插入元素
- public boolean addAll(int index, Collection<? extends E> c)与上同理
- public E get(int index)
- public int indexOf(Object o)元素第一次出现的位置(即使有多个元素)
- public int lastIndexOf(Object o)元素最后一次出现的位置(即使有多个元素)
- public E remove(int index)
- public E set(int index, E element)
- public List subList(int fromIndex, int toIndex)截取从fromIndex到toIndex的元素,左闭右开
遍历方式有三种:iterator、增强for、普通for
ArrayList
ArrayList基本等同于Vector,除了ArrayList是线程不安全(执行效率高)看源码。在多线程情况下,不建议使用ArrayList
1)ArrayListE中维护了一个Object类型的数组elementData.[debug看源码]
transient Object[]elementData;//transient:表示瞬间,短暂的,表示该属性不会被序列化
2)当创建ArrayListi对象时,如果使用的是无参构造器,则初始elementData容量为0,第1次添加,则扩容elementData为10,如需要再次扩容,则扩容elementData为1.5倍。
3)如果使用的是指定大小的构造器,则初始elementData容量为指定大小,如果需要扩容,
则直接扩容elementData为1.5倍。
老师建议:自己去debug一把我们的ArrayList的创建和扩容的流程,
源码剖析
IDEA会简化数组显示,为方便debug要进行设置:取消掉 启动集合类的替代视图
Idea会自动跳过java包下的源代码,需要设置
源码流程图:
Vector
底层也是一个elementDate数组Object类型
源码剖析
Vector的源码也需要自己debug走一遍
初始化:
//1、空构造器,默认初始化大小为10
public Vector() {
this(10);
}
//2、构造器
public Vector(int initialCapacity) {
this(initialCapacity, 0);//这里的0是控制扩容幅度,默认是扩原来的2倍,如果这里赋值就扩这里的值的大小
}
//3、构造器
public Vector(int initialCapacity, int capacityIncrement) {
super();
if (initialCapacity < 0)
throw new IllegalArgumentException("Illegal Capacity: "+
initialCapacity);
this.elementData = new Object[initialCapacity];//重点在这句,反应底层是维护数组
this.capacityIncrement = capacityIncrement;
}
add添加函数:
//1、基本与ArrayList相同
public synchronized boolean add(E e) {
modCount++;//记录修改次数,与多线程相关
ensureCapacityHelper(elementCount + 1);//确定是否扩容
elementData[elementCount++] = e;
return true;
}
//2、确定是否扩容
private void ensureCapacityHelper(int minCapacity) {
// overflow-conscious code
if (minCapacity - elementData.length > 0)
grow(minCapacity);
}
//3、确定扩容大小
private void grow(int minCapacity) {
// overflow-conscious code
int oldCapacity = elementData.length;
//这里如果初始化时给了默认扩容增量就用它,如果没有就扩容增加一倍
int newCapacity = oldCapacity + ((capacityIncrement > 0) ?
capacityIncrement : oldCapacity);
if (newCapacity - minCapacity < 0)
newCapacity = minCapacity;
if (newCapacity - MAX_ARRAY_SIZE > 0)
newCapacity = hugeCapacity(minCapacity);
elementData = Arrays.copyOf(elementData, newCapacity);
}
ArrayList和Vector比较
LinkedList
1)LinkedList底层实现了双向链表和双端队列特点
2)可以添加任意元素(元素可以重复),包括null
3)线程不安全,没有实现同步
源码剖析
1)LinkedList底层维护了一个双向链表.
2)LinkedList中维护了两个属性first和last分别指向首节点和尾节点
3)每个节点(Node对象),里面又维护了prev、next、item三个属性,其中通过prev指向前一个,通过next指向后一个节点。最终实现双向链表.
4)所以LinkedListl的元素的添加和删除,不是通过数组完成的,相对来说效率较高。
add方法:
//主要添加过程就是:l=last; last=新结点; l.next=新节点
void linkLast(E e) {
final Node<E> l = last;
final Node<E> newNode = new Node<>(l, e, null);
last = newNode;
if (l == null)
first = newNode;
else
l.next = newNode;
size++;
modCount++;
}
remove方法:
private E unlinkFirst(Node<E> f) {
// assert f == first && f != null;
final E element = f.item;
final Node<E> next = f.next;
f.item = null;
f.next = null; // help GC (*这里这样处理后GC能更好的清空那个被删除的元素)
first = next;
if (next == null)
last = null; //此时为空list了
else
next.prev = null; //如果next为空的话就无法访问prev
size--;
modCount++;
return element;
}
ArrayListi和LinkedList比较
如何选择ArrayListi和LinkedList:
1)如果我们改查的操作多,选择ArrayList
2)如果我们增删的操作多,选择LinkedList
3)一般来说,在程序中,80%-90%都是查询,因此大部分情况下会选择ArrayList
4)在一个项目中,根据业务灵活选择,也可能这样,一个模块使用的是ArrayList,另外一个模块是LinkedList,也就是说,要根据业务来进行选择
Set
1)无序(添加和取出的顺序不一致,但每次取出的顺序是固定的,只是不按照添加的顺序来,底层是数组加链表)没有索引
2)不允许重复元素,所以最多包含一个null
遍历方法有两种:迭代器和增强for,无法使用普通for
HashSet
- HashSet 基于 HashMap 来实现的,是一个不允许有重复元素的集合。
- HashSet 允许有 null 值。
- HashSet 是无序的,即不会记录插入的顺序。
- HashSet 不是线程安全的, 如果多个线程尝试同时修改 HashSet,则最终结果是不确定的。 您必须在多线程访问时显式同步对 HashSet 的并发访问。
HashSet实际上是HashMap,看下面源码
public HashSet() {
map = new HashMap<>();
}
HashSet不保证元素是有序的,取决于hash后,再确定索引的结果
源码剖析
分析HashSet底层是HashMap,HashMap底层是(数组+链表+红黑树),即数组每个元素存一个链表,后期随着元素增多链表会换成红黑树
先给一个经典面试题的坑
HashSet hashSet = new HashSet();
hashSet.add(new HashSetDemo());//加入成功
hashSet.add(new HashSetDemo());//加入成功
hashSet.add(new String("abc"));//加入成功
hashSet.add(new String("abc"));//加入失败,原因看add方法底层机制
System.out.println("hashSet = " + hashSet);
先说结论:
1.HashSet底层是HashMap
2.添加一个元素时,先得到hash值会转成->数组的索引值
3.找到存储数据表table,看这个索引位置是否已经存放的有元素
4.如果没有,直接存放
5.如果有,调用equals比较该条链表每一个元素(*因为equals来自Object类,所以不同类的equals可以自行编写),如果相同,就放弃添加,如果不相同,则添加到最后
6.在Java8中,如果一条链表的元素个数到达TREEIFY THRESHOLD(默认是8),并且tablel的大小>=MIN TREEIFY CAPACITY(默认64)( *table不够64的话会先扩容,一般是2倍),( *注意是两个条件),就会进行树化(红黑树)
初始化:
public HashSet() {
map = new HashMap<>();
}
add方法:
//1、HashSet:add
public boolean add(E e) {
//private static final Object PRESENT = new Object();
//简而言之:PRESENT的作用是帮助判断e是否为不重复的新值,最终是否加入成功
//PRESENT使得传入的对象e的所以对应的value值都是Object类型。
//因为map是键值对形式的,而set不是。所以PRESENT无实际意义,是占位符,可以帮助判断是否为新加入的对象。
//map也保证了作为key的e不能重复
//HashMap#put() 方法返回的是上一次以同一 key 加入的 value(*此时加入失败,key不能重复),若从未以该 key 加入任何数据,则返回 null
//因为上面所说,所以PRESENT不能使用null代替,因为这样就无法判断 null 究竟意味着这个 key 是第一次加入还是上一次使用了 null 作为 value 加入
//传入的e是作为map的key,而value是固定的PRESENT的new Object()
return map.put(e, PRESENT)==null;
}
//2、HashMap:put
public V put(K key, V value) {
//为了进入hash方法,debug时要用红色的force setp into
return putVal(hash(key), key, value, false, true);
}
//3、HashMap:hash
static final int hash(Object key) {
int h;
//>>>表示无符号右移,正数负数都是高位补零
//^ 异或操作:相同为0,不相同为1,可以理解为不进位相加
//右移16位是为了让高16位也参与运算,可以更好的均匀散列,减少碰撞,进一步降低hash冲突的几率
//异或运算是为了更好保留两组32位二进制数中各自的特征
//简而言之:hash方法是为了打散散列表减少冲突
//面试官问:set和map用到的哈希值是Object原生hashcode吗,一定要回答不是
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
//4、HashMap: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为哈希表数组:transient Node<K,V>[] table;
if ((tab = table) == null || (n = tab.length) == 0)
//resize的作用是管理table哈希表的长度,初始化长度(为16)和决定扩容长度(为2倍)
n = (tab = resize()).length;
//一、i = (n - 1) & hash:根据key的hash值获取key在哈希表(其中tab=table)中的位置,然后赋给i
//再将tab中在i位置的元素赋给p
//注意:这里的(n-1)&hash = hash % n 是取余的意思。此公式的前提是n为2的幂次,如2、4、6、8...。
//(n-1)&hash等价于hash&(n-1)。与运算规则:0&0=0;0&1=0;1&0=0;1&1=1
//上面取余公式的目的是:将hash值对应到数组的下标范围内,同时与运算速度快于直接用%取余运算。
//Hash 值的范围值-2147483648到2147483647,即为int的范围,前后加起来大概40亿的映射空间。
//简而言之:取到key生成的hash值对应到哈希表数组的位置上的元素,判断该元素是否为空
if ((p = tab[i = (n - 1) & hash]) == null)
//如果为空就在哈希表数组中的该位置赋上新建Node结点
//hash为key的hash,以后判断相同时会用;key就是要插入的对象;value为PRESENT占位对象;null该节点的next结点。
tab[i] = newNode(hash, key, value, null);
else {
//局部使用的辅助变量就在局部定义
Node<K,V> e; K k;
//将发生冲突的两个对象(位于哈希表数组的该索引下标上的链表的第一个元素 和 准备插入的元素)进行比较
//比较hash值和key。这里的key也用equals比较了,所以相同内容的new String()也认为相等
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
e = p;
//判断p是否为红黑树,是的话就用红黑树方法去插入
else if (p instanceof TreeNode)
e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
//上面已经判断与该条链表的一个元素bu'x't在链表里逐个比较
else {
for (int binCount = 0; ; ++binCount) {
//如果到最后都没有相同元素,就新建Node结点插在后面,此时e=null
if ((e = p.next) == null) {
p.next = newNode(hash, key, value, null);
//TREEIFY_THRESHOLD = 8;
//binCount从0开始,此时不算位于哈希表数组的链表的第一个元素,总共为8个元素
if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
//链表元素大于等于8个,就把链表树化
//在treeifyBin方法中会先判断哈希表数组长度是否到达64,如果没有的话会先扩容哈希表,而不会去树化
treeifyBin(tab, hash);
break;
}
//如果找到相同元素直接退出循环,此时e在上面的if语言已经被赋值,值为相同元素Node结点
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
break;
p = e;
}
}
//如果发现相同元素,即插入失败
if (e != null) { // existing mapping for key
//value就是PRESENT
V oldValue = e.value;
//onlyIfAbsent为false
if (!onlyIfAbsent || oldValue == null)
e.value = value;
//空方法
afterNodeAccess(e);
//返回的是PRESENT
return oldValue;
}
}
//记录修改次数
++modCount;
//如果哈希表内所有元素个数(注意包括链表里的)超过阈值就扩容
if (++size > threshold)
resize();
//该方法是留给HashMap的子类如LinkedHashMap去使用的。此处是空方法
afterNodeInsertion(evict);
//如果插入的是不重复的新元素,yi
return null;
}
//5、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
//在此处初始化table长度,默认是16,默认临界值时12
//static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16
//static final float DEFAULT_LOAD_FACTOR = 0.75f;
newCap = DEFAULT_INITIAL_CAPACITY;//16
//newThr得到的值时12,表示到12就要开始扩容,不能等到16才开始扩容,防止多线程访问造成溢出、卡死等,所以留了4个元素作为缓冲层
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"})
//此处创建了数组长度为16
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;
}
重写equals和hashcode方法
当在HashSet插入对象时,即把对象作为HashMap的key,可以重写equals和hashcode方法使两个地址不同内容相同的对象相等,防止重复插入。
需要重写equals方法的类,都需要重写hashcode方法
重写方法一般就是把所有成员变量的hashcode累加
-
基本数据类型,大家可以参考其对应的包装类型的hashcode方法
-
引用类型则直接调用hashcode()
-
数组类型则需要遍历数组,依次调用hashcode()
IDEA可以自动生成重写方法
注意:在HashSet插入时,先看hashcode是否相同(找索引),再比较equals是否相同。所以equals比hashcode更精确。
equals相同hashcode一定相同,反之hashcode相同equals不一定相同。精确度:equals > hashcode
IDEA自动生成的hashCode方法源码
//1、自定义对象:hashCode
public int hashCode() {
return Objects.hash(name, age);
}
//2、Objects:hash
public static int hash(Object... values) {
return Arrays.hashCode(values);
}
//3、Arrays:hashCode
public static int hashCode(Object a[]) {
if (a == null)
return 0;
int result = 1;
for (Object element : a)
//这里的因子31是科学家推论出来的,防止碰撞。
result = 31 * result + (element == null ? 0 : element.hashCode());
return result;
}
LinkedHashSet
- LinkedHashSet是HashSet的子类
- LinkedHashSet底层是一个LinkedHashMap,底层维护了一个数组+双向链表
- LinkedHashSet有序,它根据元素的hashCode值来决定元素的存储位置,同时使用链表维护元素的次序(图),这使得元素看起来是以插入顺序保存的。
- LinkedHashSet不允许添重复元素
源码剖析
初始化
//1、LinkedHashSet:LinkedHashSet
public LinkedHashSet() {
//初始化容积是16,与HashSet相同
super(16, .75f, true);
}
//2、HashSet:HashSet
HashSet(int initialCapacity, float loadFactor, boolean dummy) {
//此处使底层为LinkedHashMap,而LinkedHashMap是HashMap的子类,debug时会进入HashMap类的源代码,但根据动态绑定机制会走LinkedHashMap的某些方法。
map = new LinkedHashMap<>(initialCapacity, loadFactor);
}
在add过程中一些在LinkedHashMap中的方法。
//1、LinkedHashMap:newNode
Node<K,V> newNode(int hash, K key, V value, Node<K,V> e) {
LinkedHashMap.Entry<K,V> p =
new LinkedHashMap.Entry<K,V>(hash, key, value, e);
//插入之后变为最后的结点
linkNodeLast(p);
return p;
}
//2、LinkedHashMap:Entry
//通过HashMap.Node的引用方式可以看出,Node是HashMap的内部类且是静态的。只有静态内部类才能继承静态内部类。
static class Entry<K,V> extends HashMap.Node<K,V> {
Entry<K,V> before, after;//与Node相比就是多了两个前后指针
Entry(int hash, K key, V value, Node<K,V> next) {
super(hash, key, value, next);
}
}
//3、LinkedHashMap:linkNodeLast
private void linkNodeLast(LinkedHashMap.Entry<K,V> p) {
LinkedHashMap.Entry<K,V> last = tail;
tail = p;
if (last == null)
head = p;
else {
//更新了双向链表
p.before = last;
last.after = p;
}
}
//4、LinkedHashMap:afterNodeInsertion
void afterNodeInsertion(boolean evict) { // possibly remove eldest
LinkedHashMap.Entry<K,V> first;
if (evict && (first = head) != null && removeEldestEntry(first)) {
K key = first.key;
removeNode(hash(key), key, null, false, true);
}
}
总结:LinkedHashMap的区别就在于结点升级为双向链表,其他添加机制扩容机制都是相同的。
Map
-
如果key重复,put之后会替换原先的元素
-
null可以为key,但Map中只能有一个key为null
-
常用string最为key
-
一对k-v是放在一个HashMap$Node中的,又因为Node实现了Entry接口,有些书上也说一对k-v就是一个Entry。
为了方便程序员遍历,会创建EntrySet存放key-value。//HashMap中有一个内部类叫EntrySet ,它属于Set,存放Entey,Entey存放key和value final class EntrySet extends AbstractSet<Map.Entry<K,V>> {} //Node是Entey接口的实现子类,实际运行中其实是Node存放在EntrySet中 static class Node<K,V> implements Map.Entry<K,V> {} //Entry是接口,是Map接口中的内部接口。它声明了getKey和getValue方法,所以能方便取值遍历 interface Entry<K,V> {}
除了EntrySet,还有KeySet(属于Set)和Values(属于Collection)内部类,分别专门存放key和value。返回这三者分别使用的方法为entrySet、keySet、values。
上面三者存放的key或value与对应的Node存放的key或value指向的都是同一个对象,而不是拷贝。即真正的k-v对象都存放在Node中,哈希表中是存Node,EntrySet等三个存的是Node的父类Entry,但实际都是Node
Map的遍历
对于keySet
因为是set,所以有两种方式遍历:增强for和迭代器
对于values
也只有两种方式遍历:增强for和迭代器
对于entrySet
也只有两种方式遍历:增强for和迭代器。
此时要把遍历出来的对象转化为Map.Entry类型,才能调用getKey和getValue方法。注意不能转化为HashMap.Node,因为Node不是public的
HashMap
1)Map接口的常用实现类:HashMap、Hashtable和Properties。
2)HashMap是Map接口使用频率最高的实现类。
3)HashMap是以key-val对的方式来存储数据(HashMap$Node类型)[案例Entry]
4)key不能重复,但是值可以重复,允许使用null键和null值。
5)如果添加相同的key,则会覆盖原来的key-val,等同于修改.(key不会替换,val会替换)
6)与HashSet一样,不保证映射的顺序,因为底层是以hash表的方式来存储的.(Gjdk8的hashMap底层数组+链表+红黑树)
7)HashMap没有实现同步,因此是线程不安全的,方法没有做同步互斥的操作,没有synchronized
源码剖析
初始化
public HashMap() {
//static final float DEFAULT_LOAD_FACTOR = 0.75f;
this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted
}
put方法
public V put(K key, V value) {
//hash方法内容同HashSet相同
return putVal(hash(key), key, value, false, true);
}
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;
if ((tab = table) == null || (n = tab.length) == 0)
//第一步先创建table表
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;
//注意hash是key的hash,不是key+value的hash。
//只要key相同就不再添加新结点
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)
//这里是修改原先相同key结点的value值,key是原来的key,只修改value
e.value = value;
afterNodeAccess(e);
return oldValue;
}
}
++modCount;
if (++size > threshold)
resize();
afterNodeInsertion(evict);
return null;
}
扩容机制总结,和HashSet相同
1)HashMap底层维护了Node类型的数组table,默认为null
2)当创建对象时,将加载因子(loadfactor)初始化为0.75
3)当添加key-val时,通过key的哈希值得到在table的索引l。然后判断该索引处是否有元素,如果没有元素直接添加。如果该索引处有元素,继续判断该元素的key和准备加入的key相是否等,如果相等,则直接替换vl;如果不相等需要判断是树结构还是链表结构,做出相应处理。如果添加时发现容量不够,则需要扩容。
4)第1次添加,则需要扩容table容量为16,临界值(threshold)为12(16*0.75)
5)以后再扩容,则需要扩容table容量为原来的2倍(32),临界值为原来的2倍,即24,依次类推
6)在ava8中,如果一条链表的元素个数超过TREEIFY THRESHOLD(默认是8),并且tablel的大小>=MIN TREEIFY CAPACITY(默认64),就会进行树化(红黑树)
HashTable
1)存放的元素是键值对:即K-V
2)hashtablel的键和值都不能为null,否则会抛出NullPointerException
3)hashTable使用方法基本上和HashMap一样
4)hashTable是线程安全的(synchronized),hashMap是线程不安全的
源码剖析
HashTable的继承结构,除了实现了Map,还继承了字典Dictionary,该字典类已被废弃,而转用Map
public class Hashtable<K,V>
extends Dictionary<K,V>
implements Map<K,V>, Cloneable, java.io.Serializable {
初始化
底层HashTableEntry数组(实现了MapEntry数组(实现了MapEntry数组(实现了MapEntry)。初始化大小为11,临界因子也为0.75
public Hashtable() {
//11为初始化数组容量,0.75为临界值因子,故临界值为8
this(11, 0.75f);
}
//上面的this即这个构造器
public Hashtable(int initialCapacity, float loadFactor) {
if (initialCapacity < 0)
throw new IllegalArgumentException("Illegal Capacity: "+
initialCapacity);
if (loadFactor <= 0 || Float.isNaN(loadFactor))
throw new IllegalArgumentException("Illegal Load: "+loadFactor);
if (initialCapacity==0)
initialCapacity = 1;
this.loadFactor = loadFactor;
//初始化了Entry数组,大小为11
table = new Entry<?,?>[initialCapacity];
//MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;
//临界值为8
threshold = (int)Math.min(initialCapacity * loadFactor, MAX_ARRAY_SIZE + 1);
}
put方法:主要处理key重复的情况,key不重复交给addEntry处理
public synchronized V put(K key, V value) {
// Make sure the value is not null
if (value == null) {
//这里保证value值不能为null
throw new NullPointerException();
}
// Makes sure the key is not already in the hashtable.
Entry<?,?> tab[] = table;
int hash = key.hashCode();
//0x7FFFFFFF代表最大正整数(32位):0111 1111 1111 1111 1111 1111 1111 1111
//取得数组下标
int index = (hash & 0x7FFFFFFF) % tab.length;
@SuppressWarnings("unchecked")
//找到头部链表节点:通过hash取出tab数组中位于该索引的头部entry
Entry<K,V> entry = (Entry<K,V>)tab[index];
//遍历该条链表
for(; entry != null ; entry = entry.next) {
//如果key完全相同则用新value替换旧value
if ((entry.hash == hash) && entry.key.equals(key)) {
V old = entry.value;
entry.value = value;
return old;
}
}
//上面操作处理完了key相同的情况,下面进行真正的添加新元素
addEntry(hash, key, value, index);
return null;
}
addEntry方法:处理key不重复时,添加一个全新的元素
private void addEntry(int hash, K key, V value, int index) {
modCount++;
Entry<?,?> tab[] = table;
//如果hashtable内的元素个数大于临界值就扩容
if (count >= threshold) {
// Rehash the table if the threshold is exceeded
rehash();
tab = table;
hash = key.hashCode();
index = (hash & 0x7FFFFFFF) % tab.length;
}
// Creates the new entry.
@SuppressWarnings("unchecked")
Entry<K,V> e = (Entry<K,V>) tab[index];
//这里是头插法,新节点插在链表头部,即直接在数组上
tab[index] = new Entry<>(hash, key, value, e);
count++;
}
rehash方法:数组超过临界值扩容,扩容大小为原先容量乘以二再加一
protected void rehash() {
int oldCapacity = table.length;
Entry<?,?>[] oldMap = table;
// overflow-conscious code
//扩容方法为:乘以二再加一
int newCapacity = (oldCapacity << 1) + 1;
//超过最大容量的情况
if (newCapacity - MAX_ARRAY_SIZE > 0) {
if (oldCapacity == MAX_ARRAY_SIZE)
// Keep running with MAX_ARRAY_SIZE buckets
return;
newCapacity = MAX_ARRAY_SIZE;
}
//扩容了
Entry<?,?>[] newMap = new Entry<?,?>[newCapacity];
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;
}
}
}
Hashtable 和 HashMap 对比
Properties
- Properties类继承自Hashtable类并且实现了Map接口,也是使用一种键值对的形式来保存数据。
- 他的使用特点和Hashtable类似
key和value不可以为null
无序 - Properties还可以用于从xxx.properties文件中,加载数据到Properties类对象,并进行读取和修改
- 说明:工作后Xxx.properties文件通常作为配置文件,这个知识点在IO流举例,有兴趣可先看文章
TreeSet
treeset通过Comparator使列表有序
TreeSet判断相同的标准就是Comparator中重写compare方法的结果。compare返回int,只要compare判断结果为0,就判断为相同,就不会往里添加。返回负数就放在前面,返回正数就放在后面
底层是红黑树,红黑树属于平衡二叉查找树,但他比AVL平衡二叉树进行平衡的代价要小。红黑树的时间复杂度为: O(lgn)。
TreeSet treeSet = new TreeSet(new Comparator() {
@Override
public int compare(Object o1, Object o2) {
return ((String)o1).compareTo((String) o2);
}
});
源码剖析
初始化
//TreeSet
public TreeSet(TreeMapComparator<? super E> comparator) {
//底层是TreeMap
this(new TreeMap<>(comparator));
}
//上面this传到下面
TreeSet(NavigableMap<E,Object> m) {
//public interface NavigableMap<K,V> extends SortedMap<K,V> {
//public interface SortedMap<K,V> extends Map<K,V> {
//总而言之NavigableMap继承了Map
//private transient NavigableMap<E,Object> m;
this.m = m;
}
//TreeMap
public TreeMap(Comparator<? super K> comparator) {
this.comparator = comparator;
}
add方法
public boolean add(E e) {
return m.put(e, PRESENT)==null;
}
put方法
public V put(K key, V value) {
//可以发现TreeMap中的节点为Entry类型
Entry<K,V> t = root;
//root为空时,即为加入一个元素,要对root初始化
if (t == null) {
//检查一下key是否为空值,没有别的实际意义
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为构造函数传入的匿名比较器
Comparator<? super K> cpr = comparator;
if (cpr != null) {
do {
parent = t;
//!特别注意:这里调用的是构造器传入的比较器匿名类的compare方法
//重点在于:只要compare方法返回的int整数为0就被判断为相同,就不会把它添加到集合中
cmp = cpr.compare(key, t.key);
//比t小就和它的左孩子继续比较
if (cmp < 0)
t = t.left;
//比t大就和它的右孩子继续比较
else if (cmp > 0)
t = t.right;
else
//key和t的key相同的话就把value赋给t的value
//注意不动key,只动value
return t.setValue(value);
} while (t != null);
}
else {
//这里保证key不能为null
if (key == null)
throw new NullPointerException();
@SuppressWarnings("unchecked")
//本身没有比较器,自己创建比较器
//这里的key必须要实现了Comparable接口(该接口只包含一个compareTo方法,必须重写它),否则会报类型转换错误:key无法转换为Comparable
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);
//cmp和parent已在上方得出,此时parent在cmp方向的孩子为空
if (cmp < 0)
parent.left = e;
else
parent.right = e;
fixAfterInsertion(e);
size++;
modCount++;
return null;
}
//compare方法
final int compare(Object k1, Object k2) {
//如果创建treeset时构造器包含了比较器就用比较器,没有的话就把key转换成父类Comparable
return comparator==null ? ((Comparable<? super K>)k1).compareTo((K)k2)
: comparator.compare((K)k1, (K)k2);
}
试分析HashSet和TreeSet分别如何实现去重的
(1)HashSet的去重机制:hashCode0+equals(0,底层先通过存入对象,进行运算得到一个hash值,通过hash值得到对应的索引,如果发现table索引所在的位置,没有数据,就直接存放如果有数据,就进行equalsl比较[遍历比较],如果比较后,不相同,就加入,否则就不加入.
(2)TreeSet的去重机制:如果你传入了一个Comparator匿名对象,就使用实现的compare去重,如果方法返回0,就认为是相同的元素/数据,就不添加,如果你没有传入一个Comparator匿名对象,则以你添加的对象实现的Compareable接口的compareTo去重
开发中如何选择集合实现类
Collections工具类
1)reverse(List):反转List中元素的顺序
2)shuffle(List):对List集合元素进行随机排序
3)sort(List):根据元素的自然顺序对指定List集合元素按升序排序
4)sort(List,Comparator):根据指定的Comparator产生的顺序对List集合元素进行排序
5)swap(List,int,int):将指定list集合中的i处元素和j处元素进行交换
1)Object max(Collection):根据元素的自然顺序,返回给定集合中的最大元素
2)Object max(Collection,Comparator):根据Comparator指定的顺序,返回给定集合中的最大元素
3)Object min(Collection)
4)Object min(Collection,Comparator)
5)int frequency(Collection,Object):返回指定集合中指定元素的出现次数
6)void copy(List dest,.List src):将src中的内容复制到dest中
7)boolean replaceAll(List list,Object oldVal,Object newVal):使用新值替换List对象的所有旧值
习题
已知:News类按照title和content重写了hashCode和equals方法,问下面代码输出什么?
HashSet hashSet = new HashSet();
News news1 = new News("t1", "AA");
News news2 = new News("t2", "BB");
hashSet.add(news1);
hashSet.add(news2);
news1.setContent("CC");
//因为重写了hashCode和equals方法,所以news1的hash值发生了改变,
//而在hashset中news1指向的对象还保存在根据原来hash值找到的位置,所以remove找不到对象无法删除
hashSet.remove(news1);
System.out.println("hashSet = " + hashSet);
//相当于hashSet.add(news1)。此时会根据修改后的hash值存在新的位置
hashSet.add(new News("t1","CC"));
System.out.println("hashSet = " + hashSet);
//存在t1修改前的位置,但因为equals不同所以会正常添加不会覆盖
hashSet.add(new News("t1","AA"));
System.out.println("hashSet = " + hashSet);
看源码感悟
学习新API的方法
(为自我总结)
先看类的结构图,分析不同类和接口之间的关系
再看公开的方法,怎么使用类和方法
最后看源码,分析底层具体如何实现
在一个方法内,使用的变量尽量保证是在方法内部定义的局部变量,比如要是有类成员变量,就先赋给方法内新建的一个同类型变量并取个简单易懂的名字。总的来说就是让整个方法内部更有逻辑性更清晰,方法内部局部变量的起名要易于联想到该变量在该方法内部的作用。
一个方法内部包含了一个小方法,这个小方法一般写在该大方法的上面