Java 集合可分为 Collection 和 Map 两种体系
1.单列集合:
2.双列集合:
常用方法 | 方法名 |
---|---|
1、添加 | add(Object obj) addAll(Collection coll) |
2、获取有效元素的个数 | int size() |
3、清空集合 | void clear() |
4、是否是空集合 | boolean isEmpty() |
5、是否包含某个元素 | boolean contains(Object obj):是通过元素的equals方法来判断是否是同一个对象; boolean containsAll(Collection c):也是调用元素的equals方法来比较的。拿两个集合的元素挨个比较。 |
6、删除 | boolean remove(Object obj) :通过元素的equals方法判断是否是要删除的那个元素。只会删除找到的第一个元素 boolean removeAll(Collection coll):取当前集合的差集 |
7、取两个集合的交集 | boolean retainAll(Collection c):把交集的结果存在当前集合中,不 影响c |
8、集合是否相等 | boolean equals(Object obj) |
9、转成对象数组 | Object[ ] toArray() |
10、获取集合对象的哈希值 | hashCode() |
11、遍历 | iterator():返回迭代器对象,用于集合遍历 |
Collection接口继承了java.lang.Iterable接口,该接口有一个iterator()方法,所有实现了Collection接口的集合类都有一个iterator()方法,用以返回一个实现了 Iterator 接口的对象。
Iterator 仅用于遍历集合,Iterator 本身并不提供承装对象的能力。如果需要创建 Iterator 对象,则必须有一个被迭代的集合。
Iterator iterator = coll.iterator();
while (iterator.hasNext()) {
Object obj = iterator.next();
System.out.println(obj);
}
注意:在调用 iterator.next() 方法之前必须要调用 iterator.hasNext()进行检测。若不调用,且下一条记录无效,直接调用 iterator.next()会抛出NoSuchElementException异常。
● 在调用循环结束后,iterator迭代器指向最后一个元素,再次使用 iterator.next( ) 方法会报错。
Iterator iterator = coll.iterator();
while (iterator.hasNext()) {
Object obj = iterator.next();
System.out.println(obj);
}
iterator.next();//报异常:java.util.NoSuchElementException
如果希望再次遍历,Collection集合需要重新调用 iterator() 获取一个新的迭代器对象,重置迭代器,集合对象每次调用 iterator() 方法都得到一个全新的迭代器对象,默认游标都在集合的第一个元素之前。
Iterator iterator = coll.iterator();
while (iterator.hasNext()) {
Object obj = iterator.next();
if(obj.equals("Tom")){
iterator.remove();
}
}
注意:
由于数组的一些局限性,常采用List替代数组。
● List集合类中元素有序、且可重复,集合中的每个元素都有其对应的顺序索引。
● List容器中的元素都对应一个整数型的序号记载其在容器中的位置,可以根据序号存取容器中的元素。
● JDK API中List接口的实现类常用的有:ArrayList、LinkedList 和 Vector。
List除了从Collection集合继承的方法外,List 集合里添加了一些根据索引来操作集合元素的方法。
序号 | 方法 |
---|---|
1 | void add(int index, Object ele):在index位置插入ele元素 |
2 | boolean addAll(int index, Collection eles):从index位置开始将eles中的所有元素添加进来 |
3 | Object get(int index):获取指定index位置的元素 |
4 | int indexOf(Object obj):返回obj在集合中首次出现的位置 |
5 | int lastIndexOf(Object obj):返回obj在当前集合中末次出现的位置 |
6 | Object remove(int index):移除指定index位置的元素,并返回此元素 |
7 | Object set(int index, Object ele):设置指定index位置的元素为ele |
8 | List subList(int fromIndex, int toIndex):返回从fromIndex到toIndex 位置的子集合 |
● ArrayList 是 List 接口的典型实现类、主要实现类
● 本质上,ArrayList是对象引用的一个"变长"数组
ArrayList的底层操作机制源码分析:(重点)
扩容机制:
transient Object[] elementData;// transient 表示瞬间,短暂的,表示该属性不会被序列化
private int size;
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = new Object[0];
//ArrayList的无参构造器
public ArrayList() {
this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA; //创建了一个空的elementData数组
}
//ArrayList的有参构造器
public ArrayList(Collection<? extends E> c) {
this.elementData = c.toArray();
if ((this.size = this.elementData.length) != 0) {
if (this.elementData.getClass() != Object[].class) {
this.elementData = Arrays.copyOf(this.elementData, this.size, Object[].class);
}
} else {
this.elementData = EMPTY_ELEMENTDATA;
}
}
第1次添加元素时:
public boolean add(E e) {
++this.modCount;
this.add(e, this.elementData, this.size);// 初始默认值 this.size:0
return true;
}
private void add(E e, Object[] elementData, int s) {
if (s == elementData.length) {
elementData = this.grow();
}
elementData[s] = e; //先确定是否需要扩容,再赋值
this.size = s + 1;
}
● 初始状态时,elementData长度默认为0的空数组;
● 第一次赋值时,先使用modCount 来记录集合被修改的次数;
然后调用add方法:
1)先判断是否需要扩容(元素数量等于数组长度);需要扩容则调用 grow() 方法扩容,第一次扩容后新数组长度为 10,第二次及之后扩容为为旧数组的1.5倍;
判断规则:
若第一次赋值的元素数量少于10,( minCapacity=元素个数+1<=10),利用扩容机制,设置新数组长度为10;
若第一次赋值的元素数量大于10,( minCapacity=元素个数+1>10),利用扩容机制,设置新数组长度为当前添加元素的数量长度;
2)再利用copeof()方法拷贝原来的数据,再赋值
● 第二次及之后赋值时,先记录修改次数,然后调用add方法:
先判断是否需要扩容;
1)不需要扩容时,直接赋值;
2)需要扩容则调用grow()方法扩容,扩容为为旧数组的1.5倍;再拷贝原来的数组元素,再赋值。
(此处有检查最大容量的判断)
扩容机制实现源码:
private Object[] grow() {
return this.grow(this.size + 1);
}
private Object[] grow(int minCapacity) {
return this.elementData = Arrays.copyOf(this.elementData, this.newCapacity(minCapacity)); //拷贝原数组元素
}
private int newCapacity(int minCapacity) {
int oldCapacity = this.elementData.length; //第一次扩容前数组长度:0
int newCapacity = oldCapacity + (oldCapacity >> 1); //第一次扩容后新数组长度为:10,第二次及之后需要扩容时为旧数组的1.5倍长度
if (newCapacity - minCapacity <= 0) { //判断是否需要扩容
if (this.elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
return Math.max(10, minCapacity); //第一次添加元素的数量小于10,数组初始长度设定为10;大于10,则为元素个数的指定长度
} else if (minCapacity < 0) { //数组长度小于0,则报异常
throw new OutOfMemoryError();
} else {
return minCapacity;
}
} else {
return newCapacity - 2147483639 <= 0 ? newCapacity : hugeCapacity(minCapacity); //判断是否超出最大长度
}
}
3)如果使用的是指定大小的有参构造器,则初始elementData的容量为指定大小,如果需要扩容,则直接扩容为elementData的1.5倍。
此处注意一个小细节:
Idea编译器在默认情况下,Debug时显示的数据是简化之后的,要看到完整的数据需要做一下设置:
1)Vector类的定义说明
public class Vector<E> extends AbstractList<E> implements List<E>, RandomAccess, Cloneable, Serializable
2)Vector底层也是一个对象数组: protected Object[ ] elementData;
3)Vector是线程同步的,即线程安全,其操作方法带有synchronized,例如:
public synchronized int size() {
return this.elementCount;
}
4)开发工作中,需要线程同步安全时,考虑使用Vector
集合类型 | 底层结构 | 版本 | 线程安全(同步效率) | 扩容倍数 |
---|---|---|---|---|
ArrayList | 可变数组 | jdk1.2 | 不安全,效率高 | 如果有参构造 :1.5倍 如果是无参构造: 1.第一次 10 2.第二次开始按 1.5倍扩 |
Vector | 可变数组 | jdk1.0 | 安全,效率不高 | 如果是无参,默认为10,满后就按 2倍扩容 如果指定大小,则每次直接按2倍扩容 |
protected Object[] elementData;
无参构造器默认容量是10,或者有容量大小参数,直接为指定大小
public Vector(int initialCapacity, int capacityIncrement) {
if (initialCapacity < 0) {
throw new IllegalArgumentException("Illegal Capacity: " + initialCapacity);
} else {
this.elementData = new Object[initialCapacity];
this.capacityIncrement = capacityIncrement;//初始值为0
}
}
public Vector(int initialCapacity) { //有容量大小参数,直接为指定大小
this(initialCapacity, 0);
}
public Vector() {
this(10); //无参构造器默认容量是10
}
public Vector(Collection<? extends E> c) {
this.elementData = c.toArray();
this.elementCount = this.elementData.length;
if (this.elementData.getClass() != Object[].class) {
this.elementData = Arrays.copyOf(this.elementData, this.elementCount, Object[].class);
}
}
扩容机制:
//调用add
private void add(E e, Object[] elementData, int s) {
if (s == elementData.length) { //判断是否需要扩容
elementData = this.grow();// 当有效元素个数+1,即elementData 达到了数组长度时,需要调用grow()扩容
}
//不需要扩容,直接赋值
elementData[s] = e;
this.elementCount = s + 1;
}
public synchronized boolean add(E e) {
++this.modCount;
this.add(e, this.elementData, this.elementCount);
return true;
}
其中,elementCount 为数组当前最后一个元素的下标+1。(如数组容量为10,有效元素个数为5个,最后一个元素为elementData[4],elementCount 此时为5)
扩容时:
private Object[] grow(int minCapacity) {
return this.elementData = Arrays.copyOf(this.elementData, this.newCapacity(minCapacity));
}
private Object[] grow() {
return this.grow(this.elementCount + 1);
}
private int newCapacity(int minCapacity) { //minCapacity为数组长度+1
int oldCapacity = this.elementData.length;
int newCapacity = oldCapacity + (this.capacityIncrement > 0 ? this.capacityIncrement : oldCapacity);//capacityIncrement为增量大小,未指定时初始值为0,新数组长度为旧数组长度的2倍
if (newCapacity - minCapacity <= 0) {
if (minCapacity < 0) {
throw new OutOfMemoryError();
} else {
return minCapacity;//无需扩容
}
} else {
return newCapacity - 2147483639 <= 0 ? newCapacity : hugeCapacity(minCapacity);//最大容量检查
}
}
此处,Arrays.copyOf(array, to_index);返回的是新数组,等于数组array的前to_index个数;
newCapacity()返回新数组的长度,需要扩容时是原来的2倍。
private static class Node<E> {
E item;
LinkedList.Node<E> next;
LinkedList.Node<E> prev;
Node(LinkedList.Node<E> prev, E element, LinkedList.Node<E> next) {
this.item = element;
this.next = next;
this.prev = prev;
}
}
源码分析:
public LinkedList() {
this.size = 0;
}
public boolean add(E e) {
this.linkLast(e);
return true;
}
原来最后一个元素的尾结点next指向新的元素e,使新的元素添加到双向链表的最后: this.last = newNode; ,它的前节点pre 指向 原来的最后一个元素,尾结点为空
void linkLast(E e) {
LinkedList.Node<E> l = this.last;
LinkedList.Node<E> newNode = new LinkedList.Node(l, e, (LinkedList.Node)null);
this.last = newNode;
if (l == null) {
this.first = newNode;
} else {
l.next = newNode;
}
++this.size;
++this.modCount;
}
LinkedList 的增删改方法原理类似,通过修改节点的指向来实现。
比较:
1)如果改查的操作多,选择ArrayList;
2)如果增删的操作多,选择LinkedList;
3)一般来说,在程序中,80%-90%都是查询,因此大部分情况选择ArrayList;
4)在开发项目中,根据业务灵活选择,可以一个模块选用ArrayList,另一个模块选择LinkedList。
● Set接口是Collection的子接口,set接口没有提供额外的方法
● Set 集合不允许包含相同的元素,如果试把两个相同的元素加入同一个 Set 集合中,则添加操作失败。可以添加一个null。(只能一个)
● Set 判断两个对象是否相同不是使用 == 运算符,而是根据 equals() 方法
● Set存放数据是无序的,即添加的顺序和取出的顺序不一致(注意:虽然取出顺序与添加时不一致,但是是固定的)
Set中的元素不能通过索引来获取。
● HashSet 是 Set 接口的典型实现,大多数时候使用 Set 集合时都使用这个实现类。
● HashSet 按 Hash 算法来存储集合中的元素,因此具有很好的存取、查找、删除 性能。
● HashSet 不是线程安全的
说明:
1)HashSte实现类Set接口;
2)HashSet实际上是HashMap (HashMap的底层是数组+链表+红黑树),源码如下;
public HashSet() {
this.map = new HashMap();
}
3)可以存放null值,但只能有一个null;
4)HashSet不能保证元素的排列顺序 ,取决于Hash后,再确定索引的结果(添加的顺序和取出的顺序不一致)
5)不能有重复元素/对象。
1).HashSet底层是HashMap
2)添加一个元素时,先得到hash值,然后它会转成索引值
3)找到存储数据表table,查看这个索引位置是否已经存放了元素
4)如果没有,则直接加入
5)如果有元素了,先调用equals方法比较,相同时放弃添加,不相同时,添加到最后
6)在java8中,如果一条链表的元素个数超过TREEIFY_THRESHOLD(默认值是8),并且table的大小>=MIN_TREEIFY_CAPACITY(默认值64),会进行树化(红黑树)。
案例分析:
public class HashSetSource {
public static void main(String[] args) {
HashSet hashSet = new HashSet();
hashSet.add("java");
hashSet.add("php");
hashSet.add("c++");
System.out.println("hashset="+hashSet);
}
}
源码分析:
(1)执行无参构造器
public HashSet() {
this.map = new HashMap();
}
(2)执行 add() 方法,其中 PRESENT 是一个静态对象,共享(参考单例模式理解)
public boolean add(E e) {
return this.map.put(e, PRESENT) == null;
}
(3)执行 put() 方法,该方法会执行 hash(key) 返回key对应的hash值,通过算法 key.hashCode()) ^ h >>> 16得到;
public V put(K key, V value) {
return this.putVal(hash(key), key, value, false, true); //此时key=“java”
}
static final int hash(Object key) {
int h;
return key == null ? 0 : (h = key.hashCode()) ^ h >>> 16;
}
*(4)执行 putVal 方法(见内部分析)
final V putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict) {
HashMap.Node[] tab; //定义了一些辅助变量,tab是HashMap 的一个数组,类型是Node
int n;
// if 语句判断当前tab 是null或者大小为空的情况,通过调用 resize() 方法,返回一个容量为16的数组,缓存极限为12
if ((tab = this.table) == null || (n = tab.length) == 0) {
n = (tab = this.resize()).length;
}
Object p;
int i;
/** (1)根据 key ,上一步(3)中调用 hash(key) 返回key对应的hash值 得到key应该存放到tab表的索引位置(即算法 i = n - 1 & hash ),
将这个位置的对象,赋值给 p
(2)此时判断 p 是否为 null
2.1 若为null,说明当前位置没有元素,则调用下面的方法,创建一个Node对象 Node(key=“java”,value=PRESENT,next=null)
HashMap.Node newNode(int hash, K key, V value, HashMap.Node next) {
//此处传入hash 是为了判断下一次添加时是否重复
return new HashMap.Node(hash, key, value, next);
}
然后将该对象放到这个位置;
2.2 若不为空,见下
*/
if ((p = tab[i = n - 1 & hash]) == null) {
tab[i] = this.newNode(hash, key, value, (HashMap.Node)null);
} else {
Object e;
Object k;
/** 在 p 不为空的情况下,
一、如果当前索引位置对应的链表中第一个元素和准备添加的元素hash值相同 ((HashMap.Node)p).hash == hash
并且满足一下条件之一时,都不能加入
1)准备添加的key 和 p 指向的Node节点的 key是同一个对象
2) p 指向的Node节点的 key使用equals 方法和准备加入的 key比较后相同
*/
if (((HashMap.Node)p).hash == hash && ((k = ((HashMap.Node)p).key) == key || key != null && key.equals(k))) {
e = p;
// 二、然后判断 p 是否是一棵红黑树,如果是一棵红黑树,则调用 putTreeVal 进行添加(细节较复杂,此处不详述)
} else if (p instanceof HashMap.TreeNode) {
e = ((HashMap.TreeNode)p).putTreeVal(this, tab, hash, key, value);
} else {
int binCount = 0;
/**三、以上情况都不满足时,需要使用循环依次和当前索引位置对应的链表中的元素逐一比较
(1)循环结束后,如果不和其中的元素相同,则添加到链表的最后
注意: 添加新的元素后,立即判断 此链表是否已经达到8个节点,
是就调用 treeifyBin 方法对当前链表树化,在转化为红黑树时,会进行判断:
tab != null && (n = tab.length) >= 64,满足时,先进行table扩容,
只有不满足才进行树化
(2)循环过程中,发现有相同的元素时,就break跳出
*/
while(true) {
if ((e = ((HashMap.Node)p).next) == null) {
((HashMap.Node)p).next = this.newNode(hash, key, value, (HashMap.Node)null);
if (binCount >= 7) {
this.treeifyBin(tab, hash);
}
break;
}
if (((HashMap.Node)e).hash == hash && ((k = ((HashMap.Node)e).key) == key || key != null && key.equals(k))) {
break;
}
p = e;
++binCount;
}
}
if (e != null) {
V oldValue = ((HashMap.Node)e).value;
if (!onlyIfAbsent || oldValue == null) {
((HashMap.Node)e).value = value;
}
this.afterNodeAccess((HashMap.Node)e);
return oldValue;
}
}
++this.modCount;
if (++this.size > this.threshold) { //此处判断是否要扩容
this.resize();
}
this.afterNodeInsertion(evict);//此方法在HashMap中是一个空方法,它主要是用来给HashMap的子类去实现这个方法,做一些操作
return null;
}
1).HashSet底层是HashMap,第一次添加时,table数组扩容到16,临界值为12(16加载因子0.75);
2).如果table数组使用到临界值,即12时,会扩容到162=32 ,新的临界值位 32*0.75=24,依次类推;
3).在Java8中,如果一条链表的元素个数到达TREEIFY_THRESHOLD(默认值为8),并且table 的大小 >=MIN_TREEIFY_CAPACITY(默认值64),就会进行树化,否则仍然采用数组扩容机制。
代码简单演示:
public class HashSetIncrement {
public static void main(String[] args) {
HashSet hashSet = new HashSet();
// for (int i = 0; i <= 100; i++) {
// hashSet.add(i);
// }
for (int i = 1; i <= 12; i++) {
hashSet.add(new Person(i));
}
}
}
class Person {
private int age;
public Person(int age) {
this.age = age;
}
@Override
public int hashCode() { //保证哈希值一样
return 100;
}
}
i=8时,不会树化:
当i=8时,准备添加第8个元素,前7个节点均有值,不为null,binCount 自增,当binCount =6时,判断table表的链表数组索引为7的元素是否为null,此时索引值为6的位置存放元素person(7) ,索引值为7的位置为null ,直接赋值,但此时不满足>=7,不跳转树方法;
当i=9时,准备添加第9个元素,前8个节点均有值,不为null,binCount 自增,当binCount =7时,判断table表的链表数组索引为8的元素是否为null,此时索引值为7的位置存放元素person(8) ,索引值为8的位置为null ,赋值后进入树方法。
进入树方法后,由于table容量小于64,跳过树化逻辑代码,直接跳转扩容机制方法,table扩容为原来的2倍,16*2=32,此时阈值为24。
当i=10时,准备添加第10个元素,直到binCount =8,判断发现table表中链表数组索引为9的元素才为null ,person(10) 赋值给索引值为9的位置后,发现满足条件 (binCount >= 7) 进入树方法,由于table容量小于64,跳过树化逻辑代码,直接跳转扩容机制方法,table扩容为32*2=64,此时阈值为48。
else {
int binCount = 0;
while(true) {
if ((e = ((HashMap.Node)p).next) == null) {
((HashMap.Node)p).next = this.newNode(hash, key, value, (HashMap.Node)null);
if (binCount >= 7) {
this.treeifyBin(tab, hash);
}
break;
}
if (((HashMap.Node)e).hash == hash && ((k = ((HashMap.Node)e).key) == key || key != null && key.equals(k))) {
break;
}
p = e;
++binCount;
}
当i=11时,准备添加第11个元素,直到binCount =9,判断发现table表中链表数组索引为10的元素才为null ,person(11) 赋值给索引值为10的位置后,发现满足条件 (binCount:9 >= 7) 进入树方法,由于table容量此时等于64,执行树化逻辑代码,完成树化,此处执行后,该链表转成红黑树。
关于扩容机制的补充说明
源码:
if (++this.size > this.threshold) {
this.resize();
}
其中的 size 只要往HashSet中添加一个元素,底层的table的size 就自增1,即使在 size达到12个元素时,可能这些元素只使用到了部分链表,依然会触发扩容机制。
例如测试代码:
public class HashSetIncrement_2 {
public static void main(String[] args) {
HashSet hashSet = new HashSet();
for (int i = 0; i <= 7; i++) {
hashSet.add(new PersonA(1));
}
for (int i = 0; i <= 7; i++) {
hashSet.add(new PersonB(1));
}
}
}
class PersonA {
private int age;
public PersonA(int age) {
this.age = age;
}
@Override
public int hashCode() {
return 100;
}
}
class PersonB {
private int age;
public PersonB(int age) {
this.age = age;
}
@Override
public int hashCode() {
return 50;
}
}
即使此时这12个元素只占用了table下的2个链表,依然会触发扩容机制。
HashSet 集合判断两个元素相等的标准:两个对象通过 hashCode() 方法比较相等,并且两个对象的 equals() 方法返回值也相等。
● 对于存放在Set容器中的对象,对应的类一定要重写equals()和hashCode(Object obj)方法,以实现对象相等规则。即:“相等的对象必须具有相等的散列码”。
重写原则:
1、 在程序运行时,同一个对象多次调用 hashCode() 方法应该返回相同的值。
2、当两个对象的 equals() 方法比较返回 true 时,这两个对象的 hashCode() 方法的返回值也应相等。
3、对象中用作 equals() 方法比较的 Field(属性),都应该用来计算 hashCode 值。
*4、复写equals方法的时候一般都需要同时复写hashCode方法。通 常参与计算hashCode的对象的属性也应该参与到equals()中进行计算。
● LinkedHashSet 是 HashSet 的子类;
● LinkedHashSet 底层是一个LinkedHashMap(HashMap的子类),底层维护了一个数组和双向链表;
● LinkedHashSet 根据元素的 hashCode 值来决定元素的存储位置, 但它同时使用双向链表维护元素的次序(图),这使得元素看起来是以插入 顺序保存的;
● LinkedHashSet插入性能略低于 HashSet,但在迭代访问 Set 里的全部元素时有很好的性能;
● LinkedHashSet 不允许集合元素重复。
1)在LinkedHashSet 中维护了一个Hash表和双向链表;(LinkedHashSet 有head 和 tail)
2)每一个节点有 before 和 after 属性,形成双向链表;
3)添加一个元素时,先计算其 hash值,再求索引,确定该元素在table 中的位置,然后将添加的元素加入到双向链表,如果已经存在不添加(原则和HashSet一样)
例如:
tail.next = newElement;
newElement.pre = tail;
tail = newElement;
4)在遍历LinkedHashSet 时,也能确保插入的顺序和遍历时一致。
public class LinkedHashSetSource {
public static void main(String[] args) {
LinkedHashSet set = new LinkedHashSet();
set.add(new String("Hello!"));
set.add(123);
set.add(123);
set.add(new Student("古力娜扎",25));
set.add(456);
set.add("AABB");
System.out.println("set="+set);
}
}
class Student{
private String name;
private int age;
public Student(String name, int age) {
this.name = name;
this.age = age;
}
@Override
public String toString() {
return "Student{" +
"name='" + name + '\'' +
", age=" + age +
'}';
}
}
分析:
添加元素时,底层依然是调用 HashMap 的 putVal() 方法,同上。遇到相同的元素时,判断机制也一样同上。
添加完成后,这些元素之间有链表连接的关系。
结构关系如下: