结合JavaGuide和《On Java》的集合笔记。
不要使用时代的眼泪Vector
、HashTable
、Stack
。
在Java5之前,编译器允许向集合中插入不正确的类型。
比如,考虑一个Apple对象的集合,它由这条语句创建:
ArrayList apples = new ArrayList();
由于这个示例没有泛型,正常情况下编译器会给出警告:
Raw use of parameterized class 'ArrayList'
但如果我们向这个apples
集合中插入一个Orange
对象,代码并不会报错。
apples.add(new Orange())
而当我们取出对象时,会出现问题,提示我们Orange cannot be cast to Apple
, 得到一个Orange是在我们意料之外的行为。
Apple apple = (Apple) apples.get(0);
所以java提供了泛型,当我们希望指明一个集合中包含的是Apple类型时,只需要定义类型参数为Apple
的ArrayList
。
在Java7之前,我们需要像第二行代码一样,在两侧重复写出类型声明。
程序员观察到,所有的信息都可以在左侧得到,因此没有理由任由编译器强迫我们在右侧再写一遍,因此变成了钻石语法<>的空括号。(解开了我多年的疑惑
ArrayList<Apple> apples = new ArrayList<>();
ArrayList<Apple> apples = new ArrayList<Apple>();
当指定了某个类型为泛型参数时,并不仅限于只能将确切类型的对象放入集合中。向上转型也可以像作用于其他类型一样作用于泛型,其子类对象也可加入集合。
Java集合类库从设计上讲,可以分为两个不同的概念,表示为库的两个基本接口:
集合Collection
:一个独立元素的序列
映射Map
: 一组成对的“键值对”对象
尽管并非总是如此,但在理想情况下,我们编写的大部分代码在与这些接口打交道,只有在创建时才需要指明所使用的确切类型,因此可以这样创建一个List
。
List<Apple> apples = new ArrayList<>();
当希望修改实现时,只需要更改apples的实例化,而无需更改其他代码。
List<Apple> apples = new LinkedList<>();
当然,因为有些类具有额外的功能,我们有时可能需要使用这些功能,而希望使用更详细的数据类型。
方法 | 描述 | 说明 |
---|---|---|
Arrays.asList |
将数组 / 多个元素变为List 对象 |
|
Collection() |
构造方法,可接受使用另一个Colletion 作为参数 |
浅拷贝(深拷贝需要使用clone ) |
Collection.addAll() |
向Collection 中加入另一个Collection 的元素 |
浅拷贝 |
Collections.addAll() |
接受数组、Collection、多个元素作为参数 | 浅拷贝 |
Arrays.asList()
方法可以接受一个数组,或者一个用逗号分隔的元素列表(使用可变参数),并将其转换为一个List
对象。但是它的底层实现是一个数组,大小无法调整,不能进行add
或者remove
操作。// 逗号分隔的元素列表
Arrays.asList(1, 2, 3, 4, 5)
// 接受一个数组
Integer[] moreInts = {6, 7, 8, 9, 10};
Arrays.asList(moreInts)
// 如果想改变数组大小,要像这样再构造一个新对象
List<Integer> arrayList = new ArrayList<>(Arrays.asList(moreInts));
Collection
类的构造器可以接受使用另一个Colletion
作为参数: Collection<Integer> collection = new ArrayList<>(Arrays.asList(1, 2, 3, 4, 5));
Collection
类的addAll()
函数只能接受使用另一个Colletion
作为参数,不如Arrays.asList
或者Collections.addAll()
方法灵活,二者可以接受可变参数列表。collection.addAll(Arrays.asList(moreInts));
Collections.addAll()
方法接受一个Collection
对象、一个数组、或者一个用逗号分隔的列表,将其中所有的元素都加入这个Collection中。Collections.addAll()
方法速度要快得多,也更加灵活,因此更推荐这种方式:Collections.addAll(collection, 11, 12, 13, 14, 15);
Collections.addAll(collection, moreInts);
Collections.addAll(collection, Arrays.asList(moreInts));
ArrayList
和 LinkedList
都是 List 的类型,从输出中可以看出,它们都按插入顺序保存元素。两者之间的区别不仅在于执行某些类型的操作时的性能,而且 LinkedList
包含的操作多于 ArrayList
。
HashSet
, TreeSet
和LinkedHashSet
是 Set 的类型。从输出中可以看到, Set 仅保存每个相同项中的一个,并且不同的 Set 实现存储元素的方式也不同。
TreeSet
,它将按比较结果的升序保存对象。 LinkedHashSet
,它按照被添加的先后顺序保存对象。键和值保存在 HashMap 中的顺序不是插入顺序,因为 HashMap 实现使用了非常快速的算法来控制顺序。
TreeMap
升序保存键LinkedHashMap
在保持 HashMap
查找速度的同时按键的插入顺序保存键。有两种类型的 List
:
基本的 ArrayList
,底层实现为Object[]
数组。擅长随机访问元素,但在List
中间插入和删除元素时速度较慢。
LinkedList
,底层实现为双向链表。它通过代价较低的在 List 中间进行的插入和删除操作,提供了优化的顺序访问。LinkedList
对于随机访问来说相对较慢,但它具有比 ArrayList 更多的功能。
方法 | 说明 | 备注 |
---|---|---|
contains | 判断某个对象是否在列表中 | |
add | 加入元素 / 按照指定下标添加元素 | |
remove | 按照下标删除某个元素 / 删除某个指定元素 | |
indexOf | 从列表中获取对象的对应下标,如果没查到,返回 -1 | |
subList | 截取列表, [ s t a r t , e n d ) [start,end) [start,end) | subList生成的列表,底层是原始列表。 |
containsAll | 是否包含某个集合 | |
Collections.sort(list); | 对集合进行排序,可以使用自定义的new Comparator 重写compareTo 方法 |
|
A.retainAll(B) | 求交集,保留A在B中的所有元素。 | |
removeAll | 删除参数包含的所有对象 | |
toArray | 转换为数组 |
toArray
的无参版本会返回一个Object
数组;如果向重载的toArray
版本传递一个目标类型的数组,它就会生成指定类型的数组。
如果参数数组太小,以至于无法将所有对象保存在这个List中,toArray
方法会自动创建一个大小适当的新数组。
// toArray() 将列表转为数组
Object[] o = pets.toArray();
// toArray() 还有一个重载方法,传入一个形参,可以直接返回对应类型的数组,
// 这里的 new Integer[0] 就是起一个模板的作用,指定了返回数组的类型,
// 0是为了节省空间
Integer[] pa = pets.toArray(new Integer[pets.length()]);
以ArrayList
的 toArray(T[] a)
方法为例:
@SuppressWarnings("unchecked")
public <T> T[] toArray(T[] a) {
if (a.length < size)
// 新建一个运行时类型的数组,但是ArrayList数组的内容
return (T[]) Arrays.copyOf(elementData, size, a.getClass());
//调用System提供的arraycopy()方法实现数组之间的复制
System.arraycopy(elementData, 0, a, 0, size);
if (a.length > size)
a[size] = null;
return a;
}
Comparable
接口和 Comparator
接口都是 Java 中用于排序的接口,它们在实现类对象之间比较大小、排序等方面发挥了重要作用:
Comparable
接口实际上是出自java.lang
包 它有一个 compareTo(Object obj)
方法用来排序Comparator
接口实际上是出自 java.util
包它有一个compare(Object obj1, Object obj2)
方法用来排序一般我们需要对一个集合使用自定义排序时,我们就要重写compareTo()
方法或compare()
方法。
// Comparator定制排序的用法
Collections.sort(arrayList, new Comparator<int[]>() {
@Override
public int compare(int[] o1, int[] o2) {
return o1[0] - o2[0];
}
});
// 使用Comparabale接口进行排序
public class Person implements Comparable<Person> {
...
@Override
public int compareTo(Person o) {
if (this.age > o.getAge()) {
return 1;
}
if (this.age < o.getAge()) {
return -1;
}
return 0;
}
}
TreeMap<Person, String> pdata = new TreeMap<Person, String>();
ArrayList
的底层是数组队列,相当于动态数组。与 Java 中的数组相比,它的容量能动态增长。在添加大量元素前,应用程序可以使用ensureCapacity
操作来增加 ArrayList
实例的容量。
List
: 表明它是一个列表,支持添加、删除、查找等操作,并且可以通过下标进行访问。
RandomAccess
:这是一个标志接口,表明实现这个接口的 List
集合是支持 快速随机访问 的。在 ArrayList
中,我们即可以通过元素的序号快速获取元素对象,这就是快速随机访问。
Cloneable
:表明它具有拷贝能力,可以进行深拷贝或浅拷贝操作。
Serializable
: 表明它可以进行序列化操作,也就是可以将对象转换为字节流进行持久化存储或网络传输。
public class ArrayList<E> extends AbstractList<E>
implements List<E>, RandomAccess, Cloneable, java.io.Serializable
{
private static final long serialVersionUID = 8683452581122892189L;
// 默认初始容量大小
private static final int DEFAULT_CAPACITY = 10;
// 空数组(用于空实例)。
private static final Object[] EMPTY_ELEMENTDATA = {};
// 用于默认大小空实例的共享空数组实例。
// 我们把它从EMPTY_ELEMENTDATA数组中区分出来,以知道在添加第一个元素时容量需要增加多少。
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};
// 保存ArrayList数据的数组
transient Object[] elementData; // non-private to simplify nested class access
// ArrayList 所包含的元素个数
private int size;
}
1、带初始容量的构造函数,将创建initialCapacity
大小的数组。
// 带初始容量参数的构造函数(用户可以在创建ArrayList对象时自己指定集合的初始大小)
public ArrayList(int initialCapacity) {
if (initialCapacity > 0) {
//如果传入的参数大于0,创建initialCapacity大小的数组
this.elementData = new Object[initialCapacity];
} else if (initialCapacity == 0) {
//如果传入的参数等于0,创建空数组
this.elementData = EMPTY_ELEMENTDATA;
} else {
//其他情况,抛出异常
throw new IllegalArgumentException("Illegal Capacity: "+ initialCapacity);
}
}
2、默认无参构造函数,elementData
为空数组,第一次加入元素时容量变为默认大小10。
// 默认无参构造函数,DEFAULTCAPACITY_EMPTY_ELEMENTDATA 为0.初始化为10,
// 也就是说初始其实是空数组 当添加第一个元素的时候数组容量才变成10
public ArrayList() {
this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
}
3、从一个Collection
构造数组Object[]
,大小与传入的Collection
参数相同。
// 构造一个包含指定集合的元素的列表,按照它们由集合的迭代器返回的顺序。集合类型中元素为E及其子类。
public ArrayList(Collection<? extends E> c) {
//将指定集合转换为数组
elementData = c.toArray();
//如果elementData数组的长度不为0
if ((size = elementData.length) != 0) {
// 如果elementData不是Object类型数据(c.toArray可能返回的不是Object类型的数组)
if (elementData.getClass() != Object[].class)
//将原来不是Object类型的elementData数组的内容,赋值给新的Object类型的elementData数组
elementData = Arrays.copyOf(elementData, size, Object[].class);
} else {
// 其他情况,用空数组代替
this.elementData = EMPTY_ELEMENTDATA;
}
}
插入:
头部插入:由于需要将所有元素都依次向后移动一个位置,因此时间复杂度是 O ( n ) O(n) O(n)。
尾部插入:当 ArrayList
的容量未达到极限时,往列表末尾插入元素的时间复杂度是 O ( 1 ) O(1) O(1),因为它只需要在数组末尾添加一个元素即可;当容量已达到极限并且需要扩容时,则需要执行一次 O ( n ) O(n) O(n)的操作将原数组复制到新的更大的数组中,然后再执行 O ( 1 ) O(1) O(1) 的操作添加元素。
public boolean add(E e) {
modCount++;
add(e, elementData, size);
return true;
}
private void add(E e, Object[] elementData, int s) {
// 如果空间足够,不需要进行扩容;如果空间不够,就进行grow
if (s == elementData.length)
elementData = grow();
elementData[s] = e;
size = s + 1;
}
指定位置插入:需要将目标位置之后的所有元素都向后移动一个位置arraycopy
,然后再把新元素放入指定位置。这个过程需要移动平均 n / 2 n/2 n/2 个元素,因此时间复杂度为 O ( n ) O(n) O(n)。
public void add(int index, E element) {
rangeCheckForAdd(index);
modCount++;
final int s;
Object[] elementData;
if ((s = size) == (elementData = this.elementData).length)
elementData = grow();
System.arraycopy(elementData, index, elementData, index + 1, s - index);
elementData[index] = element;
size = s + 1;
}
对于删除:
核心函数为grow
。
如果旧容量为0,创建一个新数组即可。如果旧容量不等于0,则需要判断 m i n C a p a c i t y minCapacity minCapacity和 1.5 × o l d C a p a c i t y 1.5 \times oldCapacity 1.5×oldCapacity的大小,将数组容量扩充为更大的那个值,并用ArrayCopy
方法复制旧数组。
private Object[] grow() { return grow(size + 1); }
private Object[] grow(int minCapacity) {
// 旧容量大小
int oldCapacity = elementData.length;
// 如果旧容量不为0
if (oldCapacity > 0 || elementData != DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
// 扩容后所需的长度(minCapacity)是否大于当前数组长度的1.5倍
// 若是则将新的数组容量(newCapacity)设为minCapacity
// 若不是则将新的数组容量设为当前数组长度的1.5倍(oldCapacity >> 1,位右移运算,表示当前数组长度的0.5倍)
int newCapacity = ArraysSupport.newLength(oldCapacity,
minCapacity - oldCapacity, /* minimum growth */
oldCapacity >> 1 /* preferred growth */);
return elementData = Arrays.copyOf(elementData, newCapacity);
}
// 旧容量为0,建立一个新数组即可
else {
return elementData = new Object[Math.max(DEFAULT_CAPACITY, minCapacity)];
}
}
此外,在大量加入元素之前,可以通过ensureCapacity
手动进行扩容,以减少不断调用grow方法的复制次数。
public void ensureCapacity(int minCapacity) {
if (minCapacity > elementData.length
&& !(elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA
&& minCapacity <= DEFAULT_CAPACITY)) {
modCount++;
grow(minCapacity);
}
}
与ArrayList相比,LinkedList执行插入和删除的效率更高 ,不过随机访问操作的表现要差一些。
头部插入/删除:只需要修改头结点的指针即可完成插入/删除操作,因此时间复杂度为 O(1)。
尾部插入/删除:只需要修改尾结点的指针即可完成插入/删除操作,因此时间复杂度为 O(1)。
指定位置插入/删除:需要先移动到指定位置,再修改指定节点的指针完成插入/删除,因此需要移动平均 n/2 个元素,时间复杂度为 O(n)。
更重要的是,LinkedList中加入了一些可以使其用作栈、队列、双端队列的方法。
功能 | 为空抛出异常 | 为空返回null | |
---|---|---|---|
返回头部 | getFirst / element | peek / peekFirst | |
移除并返回头部 | removeFirst / remove / pop | poll / pollFirst | |
头部插入元素 | addFirst / push | ||
返回尾部 | peekLast | ||
尾部插入元素 | addLast / add / offer | ||
移除并返回尾部 | removeLast | pollLast |
Java1.0就提供了Stack类,但是这个类的设计十分糟糕。它是一个类而不是接口,继承了设计糟糕的Vector
类,Vector类可以看成方法名又臭又长但线程安全的ArrayList
,Stack
继承了Vector
所有的方法与行为。而Java由于向后兼容,没有办法摆脱这种糟糕的设计错误。
Java6加入ArrayDeque
,提供了直接实现栈功能的方法push
、pop
、peek
。
请不要使用Stack
类!
Set 不保存重复的元素。查找通常是 Set 最重要的操作,因此通常会选择HashSet
实现,该实现针对快速查找进行了优化。Set 具有与 Collection 相同的接口,因此没有任何额外的功能。实际上, Set 就是一个 Collection ,只是行为不同。
HashSet
(无序): 使用哈希函数,基于 HashMap
实现,底层采用 HashMap
来保存元素。LinkedHashSet
: LinkedHashSet
是 HashSet
的子类,并且其内部是通过 LinkedHashMap
来实现的。使用链表和哈希表,按照插入顺序维护元素。TreeSet
: 将元素排序存储在红黑树数据结构中。==
对于基本类型和引用类型的作用效果是不同的:
==
比较的是值。==
比较的是对象的内存地址。equals()
不能用于判断基本数据类型的变量,只能用来判断两个对象是否相等。equals()
方法存在两种使用情况:
equals()
方法:通过equals()
比较该类的两个对象时,使用的默认是 Object
类equals()
方法,等价于直接比较this == obj
。equals()
方法:一般我们都重写 equals()
方法来比较两个对象中的属性是否相等;若它们的属性相等,则返回 true(即,认为这两个对象相等)。
String
中的equals
方法是被重写过的,因为Object
的equals
方法是比较的对象的内存地址,而String
的equals
方法比较的是对象的值。当创建String
类型的对象时,虚拟机会在常量池中查找有没有已经存在的值和要创建的值相同的对象,如果有就把它赋给当前引用。如果没有就在常量池中重新创建一个String
对象。
⭐当你把对象加入 HashSet
时:
HashSet
会先计算对象的 hashCode
值来判断对象加入的位置,同时也会与其他已经加入的对象的 hashCode
值作比较,如果没有相符的 hashCode
,HashSet
会假设对象没有重复出现。hashCode
值的对象,这时会调用 equals()
方法来检查 hashCode
相等的对象是否真的相同:
HashSet
就不会让其加入操作成功。hashCode()
的作用是获取哈希码(int
整数),也称为散列码。这个哈希码的作用是确定该对象在哈希表中的索引位置。
两个对象的hashCode
值相等并不代表两个对象就相等,但是有时可以大大减少equals
的次数,相应提高执行速度。
重写equals()
时必须重写 hashCode()
方法,因为两个相等对象的hashCode
值必须相等。否则可能导致在进行HashSet
插入的类似操作时出现错误。
总结就是:
如果两个对象的hashCode
值相等,那这两个对象不一定相等(哈希碰撞)。
如果两个对象的hashCode
值相等并且equals()
方法也返回 true
,我们才认为这两个对象相等。
如果两个对象的hashCode
值不相等,我们就可以直接认为这两个对象不相等。
底层为HashTable实现,只是简单的调用了HashMap
的put()
方法,并且判断了一下返回值以确保是否有重复元素。
// Returns: true if this set did not already contain the specified element
// 返回值:当 set 中没有包含 add 的元素时返回真
public boolean add(E e) {
return map.put(e, PRESENT)==null;
}
方法 | 说明 | 示例 |
---|---|---|
get() | 根据键查找值 | |
getOrDefault() | 根据键查找值,查找不到时使用默认值代替 | |
put() | 放入键值对 | |
containsKey() | 是否含某个键 | |
containsValue() | 是否含某个值 | |
keySet() | 键的集合 | |
values() | 值的集合 |
Queue
是单端队列,只能从一端插入元素,另一端删除元素,实现上一般遵循 先进先出(FIFO) 规则。
Queue
扩展了 Collection
的接口,根据 因为容量问题而导致操作失败后处理方式的不同 可以分为两类方法: 一种在操作失败后会抛出异常,另一种则会返回特殊值。
Queue 接口 |
抛出异常 | 返回特殊值 |
---|---|---|
插入队尾 | add(E e) | offer(E e) |
删除队首 | remove() | poll() |
查询队首元素 | element() | peek() |
Deque
是双端队列,在队列的两端均可以插入或删除元素。
Deque
扩展了 Queue
的接口, 增加了在队首和队尾进行插入和删除的方法,同样根据失败后处理方式的不同分为两类:
Deque 接口 |
抛出异常 | 返回特殊值 |
---|---|---|
插入队首 | addFirst(E e) | offerFirst(E e) |
插入队尾 | addLast(E e) | offerLast(E e) |
删除队首 | removeFirst() | pollFirst() |
删除队尾 | removeLast() | pollLast() |
查询队首元素 | getFirst() | peekFirst() |
查询队尾元素 | getLast() | peekLast() |
事实上,Deque
还提供有 push()
和 pop()
等其他方法,可用于模拟栈。
ArrayDeque
和 LinkedList
都实现了 Deque
接口,两者都具有队列的功能,但两者有什么区别呢?
ArrayDeque
是基于可变长的数组和双指针来实现,而 LinkedList
则通过链表来实现。ArrayDeque
插入时可能存在扩容过程, 不过均摊后的插入操作依然为 O(1)。虽然 LinkedList
不需要扩容,但是每次插入数据时均需要申请新的堆空间,均摊性能相比更慢。从性能的角度上,选用 ArrayDeque
来实现队列要比 LinkedList
更好。