这篇文章很长,但是有各类经常用的所有集合,包括其底层源码分析哦!
小伙伴们可以选着看!!!
集合与数组的区别:
1.集合是可变的,其长度是固定不变的。
2.集合是存储引用数据类型。
应用场景:
1.集合:当我们储存的数据类型是引用数据类型或者元素个数不确定时。
2.数组:存储的是基本数据类型或者元素的个数确定时。
集合按照其存储的数据结构分为两大类:
单列集合:java.util.Collection
双列集合:java.util.Map
Collection:单列集合的超类,用于存储一系列符合某种规则元素,它有两个重要的子接口:
java.util.Set:元素是无序的,并且不可有相同的元素。
java.util.List:元素是有序的(即存进去的是什么顺序,出来的就是什么顺序),并且可以有相同的元素。
List两个重要接口:
ArrayList
LinkedList
Set两个重要接口:
HashSet
TreeSet
Collection常用功能:
Collection是所有的单列的父类接口,因此定义了List和Map通用的方法,这些方法用于所有的单列集合。
Collection接口遍历元素方式1:使用Iterator(迭代器)
1.所有实现了Collection接口的集合类都有一个iterator() 方法,用于返回一个实现了Iterator接口的对象,即可以返回一个迭代器。
2.迭代器的结构(如下图)
3.迭代器仅用于遍历集合,Iterator本身不存放对象。
Collection col=new ArrayList();
col.add(new Book("三国演义",90));
col.add(new Book("水浒传",90));
col.add(new Book("红楼梦",90));
//遍历col集合
//先得到col 对应的 迭代器
Iterator iterator=col.iterator();
//2.使用while循环遍历(快捷键:itit+Enter)
while(iterator.hasNext()){//判断是否还有数据
//返回下一个元素,类型是Object
Object obj=iterator.next();
sout(obj);
}
//3.当退出while循环后,这是迭代器已经指向最后的元素
//如果再获取下一个元素
//iterator.next();//抛出异常NoSuchElementException
//如果希望再次遍历,需要重置迭代器
iterator=col.iterator();
方式2:增强for(在Collection和数组上使用)
快捷方式:大写的I
源码:底层仍然是迭代器,即简化版本的迭代器遍历
for(Object book:col){
sout(book);
}
List接口:有序,可重复
内部类排序
ArrayList细节:
1.可以存放所有元素,包括null;
2.底层是由数组来实现存储的
3.基本等同与 Vector ,只是ArrayList是线程不安全的(源码:没有synchronized关键字)
多线程下,不建议使用ArrayList
ArrayList的底层源码分析:
1.ArrayList中维护的是Object[] elementData
源码:
transient Object[] elementData;//transient 表示瞬间,短暂的,表示该属性不会被序列化
2.当我们创建ArrayList对象时,使用无参构造器时,则初始elementData容量为0,第一次添加,则扩容elementData为10,如需要再次扩容,则扩容为elementData的1.5倍。
2.如果使用指定大小的构造器(有参构造器),则初始elementData容量为指定大小,如果需要扩容,则直接扩容elementData的1.5倍。
源码分析:
无参构造器:
有参构造器:
Vector类:
1.定义说明
2.Vector底层也是一个对象数组 ,protected Object[] elementData;
3.Vector是线程同步的,即线程安全,Vector类的操作方法带有synchronized
4.考虑线程安全时,用Vector
扩容机制:
LinkedList:
1.LinkedList底层实现了双向链表和双端队列特点
2.可以添加任意元素(可以重复),包括null
3.线程不安全,没有实现同步
4.添加/删除 数据,不是数组的操作,效率较高
添加方法:
remove方法:
linkedList.remove();//默认删除第一个元素
底层源码分析:
如何选择ArrayList和LinkedList:
1.改查多,ArrayList
2.增删多,LinkedList
3.大部分是查,即ArrayList
Set接口:
1.无序(添加的顺序和取出的顺序不一致,但是取出的顺序是固定的,不会每次运行都改变 ),没有索引
2.不允许重复元素,最多只能包含一个null
//如果添加了重复的元素,将不会多次打印,但也没有异常
常用方法:
Set接口也是Collection的子接口。
遍历方法:
1迭代器
2.增强for
3.不能通过索引去获取
//这是下面测试的集合初始化 代码
Set set=new HashSet();
set.add("john");
set.add("jack");
set.add(null);
set.add("jack");
set.add("john");
sout(set);//[null,john,jack]
set.remove(null);
Set接口实现类——HashSet
1.HashSet实现了Set接口
2.HashSet实际上是HashMap,看构造器的源码
3.HashSet可以存放null,但是只能由一个null,即元素不能重复
4.不保证存放的元素的顺序和取出顺序一致
5.不能存放相同的元素/对象
HashSet底层机制说明:
HashSet底层是HashMap,HashMap底层是(数组+链表+红黑树)
数组链表模拟:(实现如下效果)
代码:
public static void main(String[] args) {
Node[] table=new Node[16];
//创建节点
Node john = new Node("john", null);
table[2]=john;
Node jack = new Node("jack", null);
john.next=jack;//将jack 挂载到 john 后边
Node rose = new Node("rose", null);
jack.next=rose;//将rose 挂载到 jack 后边
Node lucy = new Node("lucy", null);
table[3]=lucy;
System.out.println(table);
}
public class Node {
public String item;
public Node next;
public Node(){}
public Node(String item, Node next) {
this.item = item;
this.next = next;
}
public String getItem() {
return item;
}
public Node setItem(String item) {
this.item = item;
return this;
}
public Node getNext() {
return next;
}
public Node setNext(Node next) {
this.next = next;
return this;
}
}
HashSet扩容机制:
1.HashSet的底层是HashMap
2.添加一个元素时,先得到hash值,会转成索引值
3.找到存储数据表table,看这个索引位置是否已经存放的有元素
4.如果没有,则直接加入
5.如果有,调用equals比较,如果相同就放弃添加,如果不相同则添加到next(如上代码的jack加到john后面)
6.在JDK8中,如果一条链表的元素个数到达TREEIFY_THRESHOLD(默认是8),并且table的大小>=MIN_TREEIFY_CAPACITY(默认64),就会进行树化(红黑树);否则仍然采用数组扩容机制。//满足以上两个条件,数组arr[i]处的链表将自动转化为红黑树,其他位置如arr[i+1]处的数组元素仍为链表。
扩容和转成红黑树机制:
1.HashSet底层是HashMap,第一次添加时,table数组扩容到16,临界值(threshold)是16*加载因子(loadFactor)是0.75=12
2.如果table数组使用到了临界值12,就扩容到16*2=32,新的临界值就是32*0.75=24,依次类推
底层源码分析:
HashSet hashSet=new HashSet();
PRESENT:static final Object PRESENT=new Object();
该方法会执行hash(key) 得到key对应的hash值 算法(h=key.hashCode())^(h>>>16)
key="java",value=PRENSENT共享
hash(key)方法:
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语句表示如果当前table是null,或者 大小=0
//就第一次扩容,到16个空间
if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length;//执行resize(),见下面
//(1)根据key,得到的hash值,去计算该key应该存放到table 的哪个索引位置
//并把这个位置的对象,赋给p
//(2)判断p是否为null
//(2.1)如果p为null,表示还没有存放元素,就创建一个Node(key="java",value=PRESENT)
//(2.2)就放在该位置tab[i]=newNode(hash,key,value,null)
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null);
else {
//开发技巧提示:在需要的局部变量(辅助变量)时候,再创建
Node<K,V> e; K k;//辅助变量
//如果当前索引位置对应的链表的第一个元素和准备添加的key的hash值一样
if (p.hash == hash &&
//并且满足 下面两个条件之一:
- //(1)准备加入的key 和 p指向的Node 结点的key 时同一个对象
//(2)p 指向的Node 结点的 key 的 equals() 和准备加入的key比较后相同(equals() 是程序员可以重写来确定的比较的内容,不能只理解为比较内容)
//就不能加入
((k = p.key) == key || (key != null && key.equals(k))))
e = p;
//再判断p 是不是一颗红黑树,
//如果是一颗红黑树,就调用putTreeVal(),来进行添加
else if (p instanceof TreeNode)
e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
//如果当前table对应的索引位置,已经是一个链表,就使用for循环比较
//1.依次和该链表的每一个元素比较后,都不相同,则加入到该链表的最后
//注意:在把元素添加到链表最后,之后,立即判断 该链表 是否已经到达8个结点
//如果是,就调用 treeifyBin() 对当前这个链表进行树化(转成红黑树)
//注意:在转成红黑树时,先判断,如果该table数组的大小<64,则先对table扩容;否则才进行转成红黑树
//2.依次和该链表的每一个元素比较过程中,如果有相同元素的情况,就直接break
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 就是我们每加入一个结点Node(k,v,h,next):不论是结点还是结点链表的结点(横还是竖),size++
if (++size > threshold)
resize();
afterNodeInsertion(evict);
return null;
}
执行resize()方法:
final Node<K,V>[] resize() {
Node<K,V>[] oldTab = table;//oldTab=null
int oldCap = (oldTab == null) ? 0 : oldTab.length;//oldCap=0
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;//newCap=16
newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);//newThr=(int)16*0.75=12
}
if (newThr == 0) {
float ft = (float)newCap * loadFactor;
newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
(int)ft : Integer.MAX_VALUE);
}
threshold = newThr; //临界值为12,即当链表的元素有12个时就扩容
@SuppressWarnings({"rawtypes","unchecked"})
Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];//创建一个16个节点的链表
table = newTab; //将创建的链表赋给table
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;//返回新创建的长度为16的链表
}
LinkedHashSet:
关系继承图:
基本介绍:
1.LinkedHashSet是HashSet的子类
2.LinkedHashSet底层是一个LinkedHashMap,底层维护了一个数组+双向链表
3..LinkedHashSet根据元素的hashCode 值来决定元素的存储位置,同时使用链表维护元素的次序(图),这使得元素看起来是以插入顺序保存的。
4..LinkedHashSet 不允许添加重复元素
底层机制:
源码说明:
1.在LinkedHashSet 中维护了一个hash 表和双向链表(LinkedHashSet 有 head 和tail)
2.每一个节点有before和after属性,这样可以形成双向链表
3.在添加一个元素时,先求hash值,再求索引,确定该元素在table的位置,然后将添加的元素加入到双向链表(如果已经存在,不添加【原则和hashSet一样】)
//示意代码
tail.next=newElement;
newElement.pre=tail;
tail=newElement;
4.这样的话,我们遍历LinkedHashSet 也能确保插入顺序和遍历顺序一致
5.添加第一次时,直接将 数组table 扩容到16,存放的节点类型为 LinkedHashMap$Entry
6.数组是 HashMap$Node[] 存放的数据/元素为 linkedHashMap$Entry(说明两者存在继承关系:如下)
//继承关系是在内部类完成
Map接口实现类的特点:
JDK8
1.Map 和 Collection 并列存在。用于保存具有映射关系的数据:Key-Value(双列元素)。
2.Map 中的key 和 value可以是任何引用类型的数据,会封装到HashMap$Node 对象中
3.Map 中的key 不允许重复,原因和HashSet一样。但是value可以重复
Map map=new HashMap():
map.put("no1","张三");
map.put("no2","李四");
map.put("no1","王二狗");//当出现key重复时,会将no1(重复key)对应的value替换
//即存入map.put("no1","王二狗");和map.put("no2","李四");
4.Map 的key 可以为null,value也可以为null,注意:key为null,只能由一个,否则替换;value可以由多个
5.常用String来作为Map的key,也可以是其他Object的子类
6.key与value存在一对一的关系,可以通过key找到唯一的value
map.get("no2");//通过get方法,传入key,获取唯一对应的value
7.第7个特点
1.k-v 最后是 HashMap$Node node=newNode(hash,key,value,null);
2.k-v 为了方便程序员的遍历,还会创建 EntrySet 集合,该集合存放的元素的类型 Entry ,而一个Entry对象就有k,v 。
EntrySet<Entry<K,V>> 即:tansient Set<Map.Entry<K,V>> entrySet;
3.entrySet 中,定义的类型是Map.Entry,但是实际上存放的还是HashMap$Node
这是因为static class Node<K,V> implements Map.Entry<K,V>
4.当把HashMap$Node 对象存放到 entrySet 就方便我们的遍历,因为Map.Entry提供了两个重要的方法
K getKey(); V getValue();
Map接口常用方法:
put();//添加元素
remove();//根据键删除映射关系
get();//通过键获取值,返回Object类型
size():获取元素个数,键盘对的对数
isEmpty();//判断个数是否为0
clear();//清空键值对
containsKey();//是否包含该键
Map接口遍历六大方式:
1.containsKey:查找键是否存在
2.keySet:获取所有的键
3.entrySet:获取所有的k-v关系
4.values:获取所有的值
示例:
Map map=new HashMap();
map.put("邓超","孙俪");
map.put("王宝强","马蓉");
map.put("刘林伯",null);
map.put(null,"刘亦菲");
map.put("鹿晗","关晓彤");
//第一组:先取出所有的key,通过key取出对应的value
Set keySet = map.keySet();
//(1)增强for
System.out.println("第一种方式:");
for (Object key :keySet) {
System.out.println(key+"-"+map.get(key));
}
//(2)通过迭代器
System.out.println("第二种方式:");
Iterator iterator = keySet.iterator();
while (iterator.hasNext()) {
Object key = iterator.next();
System.out.println(key+"-"+map.get(key));
}
//第二组:把所有的values取出来:只能取出值
Collection values = map.values();
//这里可以使用所有Collections使用的遍历方法
//增强for
for (Object value :values) {
System.out.println(value);
}
//迭代器
Iterator iterator1 = values.iterator();
while (iterator1.hasNext()) {
Object value = iterator1.next();
System.out.println(value);
}
//第三组:通过EntrySet 来获取k-v
Set entrySet = map.entrySet();//EntrySet<Map.Entry<K,V>>
//增强for
for (Object entry :entrySet) {
//将entry转成 Map.Entry
Map.Entry m=(Map.Entry) entry;
System.out.println(m.getKey()+"-"+m.getValue());
}
//迭代器
Iterator iterator2 = entrySet.iterator();
while (iterator2.hasNext()) {
Object entry = iterator2.next();
//System.out.println(next.getClass());//HashMap$Node implements Map.Entry<K,V>
//向下转型 Map.Entry
Map.Entry m=(Map.Entry)entry;
System.out.println(m.getKey()+"-"+m.getValue());
}
HashMap接口小结:
1.Map接口的常用实现类:HashMap,Hashtable和Properties.
2.HashMap是Map接口使用频率最高的实现类
3.以键值对的方式来存储数据(HashMap$Node类型)
4.HashMap没有实现同步,线程不安全,没有synchronized
HashMap底层机制:
扩容机制【和HashSet完全一样】:
1.底层维护了Node 类型的数组table,默认为Null
2.添加一个key相同的数据时,直接替换
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
e = p;
然后进入:
if (e != null) { // existing mapping for key
V oldValue = e.value;//将原来的与key相同键的value赋给oldValue
if (!onlyIfAbsent || oldValue == null)//onlyIfAbsent 系统默认为false
e.value = value;//重要!将value值替换
afterNodeAccess(e);
return oldValue;//返回一个值,导致后面返回值不为null(即添加失败)
}
Map接口实现类--Hashtable
基本介绍:
1.存放的元素是键值对:即K-V
2.Hashtable的键和值都不能为null,否则会抛出空指针异常
3.Hashtable使用方法基本上和HashMap一样
4.Hashtable 是线程安全的,HashMap是线程不安全的
Hashtable的底层:
1.底层有数组 Hashtable$Entry[] 初始化大小为11
2.临界值 threshold=11*0.75=8
3.扩容机制:
执行方法 addEntry(hash,key,value,index);
当if(count>=threshold)满足时,就进行扩容
HashMap与Hashtable对比:
Map接口实现类--Properties
基本介绍:
1.Properties类继承自Hashtable 类并且实现了Map接口,也是使用一种键值对的形式来保存数据。
2.它的特点和Hashtable类似
3.Properties 还可以用于 从xxx.properties 文件中,加载数据到Properties类对象,并进行读取和修改
put();//增加
remove();//删除
put();//修改
get();//查
getProperty();
选择集合规则:
1.先判断存储的类型(单列或双列)
2.单列:
2.1 允许重复:List
2.1.1 增删多:LinkedList[双向链表]
2.1.2 改查多:ArrayList[Object类型的可变数组]
2.2 不允许重复:Set
2.2.1 无序:HashSet[维护了一个哈希表,即(数组+链表+红黑树)]
2.2.2 排序:TreeSet
2.2.3 取出顺序和添加顺序一致:LinkedHashSet[维护了数组+双向链表]
3.双列:Map
3.1 键无序:HashMap[jdk7:数组+链表 jdk8:数组+链表+红黑树]
3.2 键排序:TreeMap
3.3 键插入和取出的顺序一致:LinkedHashMap
3.4 读取文件 Properties
TreeSet:
有参构造:
TreeSet treeSet = new TreeSet(new Comparator(){
@Override
public int compare(Object o1,Object o2){
//进行字符哈希码做差
return((String)o1).compareTo((String)o2);
}
});
compareTo方法:
底层源码分析:
1.底层仍然是HashMap
2.第一次添加元素
返回null,添加成功!
第二次添加时,
首先将比较器赋给cpr,
判断比较内是否为空:
若不为空,
则调用比较器的compare方法,接收返回的值
以下结果主要由compareTo方法得到:
如果小于0,则表示第一个元素的第一个字母小于当前元素的第一个字符,即加到后边
如果大于0,反之
如果等于0,则代表相等,则不能加入
如果为null,
Collections工具类:
1.reverse(List):反转List集合中的元素
2.shuffle(List):对List的元素进行随机排序(每次运行都不一样)
3.sort(List):根据元素的自然顺序对指定List集合元素按升序排列
4.sort(List,Comparator):根据定制的规则对List集合中元素进行排序
5.swap(List,int i,int j):将指定List集合中i处和j处元素进行交换
6.Object max(Collection):根据元素的自然排序,返回给定集合中最大的元素
7.Object max(Collection,Comparator):根据Comparator指定的顺序,返回给定集合中最大的元素
8.int frequency(Collection,Onject):返回指定集合中指定元素的出现次数
9.void copy(List dest,List,src):将src中的内容复制到dest中
10.boolean replaceAll(List list,Object oldVal,Object new Val):使用新值替换List对象的所有旧值
练习题1:
答:会,ClassCastException,因为Person类未实现Comparable接口
源码如下:
练习
移除不能成功:因为p1.name已经被修改,hash值对应的计算出的索引也会变
第三次添加:添加时hash值计算的索引和原来的索引不一样
第四次添加:因为已经存在1001和AA对象对应的索引(即第一次的索引),则挂载到p1后面
遗留问题:new String();两个相同的,只能添加一个