1. 概述
- 一方面, 面向对象语言对事物的体现都是以对象的形式,为了方便对多个对象的操作,就要对对象进行存储。另一方面,使用 Array 存储对象方面具有一些弊端,而 Java 集合就像一种容器,可以动态地把多个对象的引用放入容器中
- 数组在内存存储方面的特点
- 数组初始化以后,长度就确定了
- 数组声明的类型,就决定了进行元素初始化时的类型
- 数组在存储数据方面的弊端
- 数组初始化以后,长度就不可变了,不便于扩展
- 数组中提供的属性和方法少,不便于进行添加、删除、插入等操作,且效率不高。 同时无法直接获取存储元素的个数
- 数组存储的数据是有序的、可以重复的 → 对于无序、不可重复的需求,不能满足
- 数组在内存存储方面的特点
- Java 集合类可以用于存储数量不等的多个对象,还可用于保存具有映射关系的关联数组。Java 集合可分为
Collection和Map两种体系Collection接口:单列数据,定义了存取一组对象的方法的集合

Map接口:双列数据,保存具有映射关系 "key-value对" 的集合

- 集合的使用场景

2. Collection 接口
- Collection 接口是 List、Set 和 Queue 接口的父接口,该接口里定义的方法 既可用于操作 Set 集合,也可用于操作 List 和 Queue 集合
- JDK 不提供此接口的任何直接实现,而是提供更具体的子接口(如:Set和List) 实现
- 在 JDK 5.0 之前,Java 集合会丢失容器中所有对象的数据类型,把所有对象都当成 Object 类型处理;从 JDK 5.0 增加了 [泛型] 以后,Java 集合可以记住容器中对象的数据类型
- 接口方法
boolean add(Object obj) / addAll(Collection<?> coll):添加元素/某集合的所有元素int size():获取有效元素个数void clear():清空集合boolean isEmpty():是否是空集合boolean contains(Object obj):是通过元素的equals()来判断是否是同一个对象boolean containsAll(Collection<? extends E> c):也是调用元素的equals()来比较的。拿两个集合的元素挨个比较boolean remove(Object obj):通过元素的equals()判断是否是要删除的那个元素。只会删除找到的第 1 个元素boolean removeAll(Collection<?> coll):取当前集合的差集boolean retainAll(Collection<?> c):把交集的结果存在当前集合(this) 中,不影响形参集合 cboolean equals(Object obj):集合是否相等Object[] toArray():转成对象数组// ↑→ public static <T> List<T> asList(T... a) List list = Arrays.asList(new Integer[]{1, 2, 3}); System.out.println(list); // [1, 2, 3] List list2 = Arrays.asList(new int[]{1, 2, 3}); // 基本类型数组被当作一个元素 System.out.println(list2); // [[I@4554617c] List list3 = Arrays.asList(1,2, 3); System.out.println(list3); // [1, 2, 3]int hashCode():获取集合对象的哈希值Iterator<E> iterator():返回迭代器对象,用于集合遍历
3. Iterator迭代器接口
3.1 概述
public interface Collection<E> extends Iterable<E>Iterator对象称为迭代器(设计模式的一种),主要用于遍历Collection集合中的元素- GOF 给迭代器模式的定义为:提供一种方法访问一个容器(container)对象中各个元素,而又不需暴露该对象的内部细节。迭代器模式,就是为容器而生
Collection<I>继承了java.lang.Iterable<I>,该接口有一个iterator(),那么所有实现了Collection<I>的集合类都有一个iterator(),用以返回一个实现了Iterator<I>的对象Iterator仅用于遍历集合,Iterator本身并不提供承装对象的能力。如果需要创建Iterator对象,则必须有一个被迭代的集合- 集合对象每次调用
iterator()都得到一个全新的迭代器对象,游标(cursor) 默认都在集合的第 1 个元素之前
3.2 三个方法
3.1 遍历集合元素

boolean hasNext():判断 iterator 内是否存在下1个元素,如果存在,返回true,否则返回false(注意,这时上面的那个指针位置不变)E next():返回 iterator 内下1个元素,同时上面的指针向后移动一位。如果不断地循环执行next()方法,就可以遍历容器内所有的元素了void remove():删除 iterator 内指针的前1个元素,前提是至少执行过1次next()
迭代的错误写法 // 在调用 it.next() 之前必须要调用 it.hasNext() 进行检测。若不调用,且下一条记录无效,直接调用 it.next() 会抛出 NoSuchElementException。
Iterator it = c.iterator();
Object obj;
while ((obj = it.next()) != null) // 第 c.size() + 1 次,会抛异常
System.out.println(obj);
3.2 删除集合元素

Iterator iter = coll.iterator();
while(iter.hasNext()) {
Object obj = iter.next();
if(obj.equals("Tom")) {
iter.remove();
}
}
- Iterator 可以删除集合的元素,但是是遍历过程中是通过迭代器对象的
remove(),不是集合对象的remove(obj)。 - 如果还未调用
next()或在上一次调用next()之后已经调用了remove(), 再调用remove()都会报IllegalStateException
3.4 foreach
JDK5.0 起,提供了 for each 循环迭代访问 Collection 和 数组。编译器简单地将 for each 循环翻译为带有迭代器的循环。 for each 循环可以与任何实现了 Iterator<I> 的对象一起工作,这个接口直播暗含了一个方法:Iterator<E> iterator()。
Collection<I> 扩展了 Iterator<I>。因此,对于标准类库中的任何集合都可以使用 for each 循环。


4. List 接口
- 鉴于 Java 中数组用来存储数据的局限性,我们通常使用
List替代数组 - List集合类中元素有序、且可重复,集合中的每个元素都有其对应的顺序索引
- List 容器中的元素都对应一个整数型的序号记载其在容器中的位置,可以根据序号存取容器中的元素
- JDK API中
List<I>的实现类常用的有:ArrayList、LinkedList和Vector
4.1 ArrayList 源码
JDK 7
public class ArrayList<E> extends AbstractList<E>
implements List<E>, RandomAccess, Cloneable, java.io.Serializable
{
private transient Object[] elementData;
private int size;
public ArrayList() {
this(10);
}
public ArrayList(int initialCapacity) {
super();
if (initialCapacity < 0)
throw new IllegalArgumentException("Illegal Capacity: "+
initialCapacity);
this.elementData = new Object[initialCapacity];
}
public boolean add(E e) {
ensureCapacityInternal(size + 1); // Increments modCount!!
elementData[size++] = e;
return true;
}
private void ensureCapacityInternal(int minCapacity) {
modCount++;
// overflow-conscious code
if (minCapacity - elementData.length > 0)
grow(minCapacity);
}
private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;
private void grow(int minCapacity) {
// overflow-conscious code
int oldCapacity = elementData.length;
// 新长度是加上原来的一半
int newCapacity = oldCapacity + (oldCapacity >> 1);
if (newCapacity - minCapacity < 0)
newCapacity = minCapacity;
if (newCapacity - MAX_ARRAY_SIZE > 0)
newCapacity = hugeCapacity(minCapacity);
// minCapacity is usually close to size, so this is a win:
elementData = Arrays.copyOf(elementData, newCapacity);
}
private static int hugeCapacity(int minCapacity) {
if (minCapacity < 0) // overflow
throw new OutOfMemoryError();
return (minCapacity > MAX_ARRAY_SIZE) ?
Integer.MAX_VALUE :
MAX_ARRAY_SIZE;
}
}
JDK 8
public class ArrayList<E> extends AbstractList<E>
implements List<E>, RandomAccess, Cloneable, java.io.Serializable
{
private static final int DEFAULT_CAPACITY = 10;
transient Object[] elementData;
private int size;
public ArrayList(int initialCapacity) {
if (initialCapacity > 0) {
this.elementData = new Object[initialCapacity];
} else if (initialCapacity == 0) {
this.elementData = EMPTY_ELEMENTDATA;
} else {
throw new IllegalArgumentException("Illegal Capacity: "+
initialCapacity);
}
}
// 初始长度 0
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};
public ArrayList() {
this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
}
public boolean add(E e) {
ensureCapacityInternal(size + 1); // Increments modCount!!
elementData[size++] = e;
return true;
}
private void ensureCapacityInternal(int minCapacity) {
if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity);
}
ensureExplicitCapacity(minCapacity);
}
private void ensureExplicitCapacity(int minCapacity) {
modCount++;
// overflow-conscious code
if (minCapacity - elementData.length > 0)
grow(minCapacity);
}
private void grow(int minCapacity) {
// overflow-conscious code
int oldCapacity = elementData.length;
int newCapacity = oldCapacity + (oldCapacity >> 1);
if (newCapacity - minCapacity < 0)
newCapacity = minCapacity;
if (newCapacity - MAX_ARRAY_SIZE > 0)
newCapacity = hugeCapacity(minCapacity);
// minCapacity is usually close to size, so this is a win:
elementData = Arrays.copyOf(elementData, newCapacity);
}
}
- JDK 7
ArrayList list = new ArrayList();// 底层创建了长度是 10 的Object[] elementDatalist.add(1);// elementData[0] = new Integer(1);list.add(11);// 如果此次的添加导致底层elementData[]容量不够,则扩容- 默认情况下,扩容为原来的容量的 1.5 倍,同时需要将原有数组中的数据复制到新的数组中
- [结论] 建议开发中使用带参的构造器:
ArrayList list = new ArrayList(int capacity)
- JDK 8
ArrayList list = new ArrayList();// 底层Object[] elementData初始化为{}list.add(123);// 第一次调用add()时,底层才创建了长度 10 的数组,并将数据123 添加到elementData[0]- 后续的添加和扩容操作与 JDK 7 无异
- 小结
- JDK 7 中的 ArrayList 的对象的创建类似于单例的饿汉式
- JDK 8 中的 ArrayList 的对象的创建类似于单例的懒汉式,延迟了数组的创建,节省内存
4.2 LinkedList 源码
public class LinkedList<E>
extends AbstractSequentialList<E>
implements List<E>, Deque<E>, Cloneable, java.io.Serializable
{
// 实现序列化接口后,不想被序列化的成员变量前加 transient
transient int size = 0;
transient Node<E> first;
transient Node<E> last;
public LinkedList() {}
// 双向链表
private static class Node<E> {
E item;
Node<E> next;
Node<E> prev;
Node(Node<E> prev, E element, Node<E> next) {
this.item = element;
this.next = next;
this.prev = prev;
}
}
public boolean add(E e) {
linkLast(e);
return true;
}
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++;
}
public void addFirst(E e) {
linkFirst(e);
}
private void linkFirst(E e) {
final Node<E> f = first;
final Node<E> newNode = new Node<>(null, e, f);
first = newNode;
if (f == null)
last = newNode;
else
f.prev = newNode;
size++;
modCount++;
}
}
4.3 Vector 源码
public class Vector<E>
extends AbstractList<E>
implements List<E>, RandomAccess, Cloneable, java.io.Serializable
{
protected Object[] elementData;
protected int elementCount;
protected int capacityIncrement;
public Vector() {
this(10);
}
public Vector(int initialCapacity) {
this(initialCapacity, 0);
}
public Vector(int initialCapacity, int capacityIncrement) {
super();
if (initialCapacity < 0)
throw new IllegalArgumentException("Illegal Capacity: "+
initialCapacity);
this.elementData = new Object[initialCapacity];
this.capacityIncrement = capacityIncrement;
}
public synchronized boolean add(E e) {
modCount++;
ensureCapacityHelper(elementCount + 1);
elementData[elementCount++] = e;
return true;
}
private void ensureCapacityHelper(int minCapacity) {
// overflow-conscious code
if (minCapacity - elementData.length > 0)
grow(minCapacity);
}
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);
}
}
4.4 List 接口常用方法
void add(int index, E ele):在index位置插入eleboolean addAll(int index, Collection<? extends E> c):从index位置开始将c中的所有元素添加进来E get(int index):获取指定index位置的元素int indexOf(E obj):返回obj在集合中首次出现的位置int lastIndexOf(E obj):返回obj在集合中末次出现的位置E remove(int index):移除指定index位置的元素,并返回此元素E set(int index, E ele):设置指定index位置的元素为eleList<E> subList(int fromIndex, int toIndex):返回[fromIndex, toIndex)位置的子集合

5. Set
Set<I>是Collection的子接口,Set<I>没有提供额外的方法Set<I>不允许包含相同(根据equals())的元素,如果试把两个相同的元素加入同一个 Set 集合中,则添加操作失败Set<I>存储无序、不可重复的数据- 无序性:不等于随机性。存入底层数组的数据中并非按照数组索引顺序存放,而是根据数据的哈希值决定
- 不可重复性:保证添加的元素按照
equals()判断时,不能返回true,即相同的元素不能重复添加
5.1 散列


5.2 HashSet
- HashSet 是
Set<I>的典型实现,大多数时候使用 Set 集合时都使用这个实现类 - HashSet 按 Hash 算法来存储集合中的元素,因此具有很好的存取、查找、删除性能
- HashSet 具有以下特点
- 不能保证元素的排列顺序
- HashSet 不是线程安全的
- 集合元素可以是 null
- HashSet 集合判断两个元素相等的标准:两个对象通过
hashCode()比较相等,并且两个对象的equals()返回值也相等 - 对于存放在 Set 容器中的对象,对应的类一定要重写
equals()和hashCode(),以实现对象相等规则。即:"相等的对象必须具有相等的散列码"- 重写
hashCode()的基本原则- 在程序运行时,同一个对象多次调用
hashCode()应该返回相同的值 - 当两个对象的
equals()比较返回 true 时,这两个对象的hashCode()的返回值也应相等 - 对象中用作
equals()比较的 Field,都应该用来计算 hashCode 值
- 在程序运行时,同一个对象多次调用
- 重写
equals()的基本原则- 当一个类有自己特有的“逻辑相等”概念,当改写
equals()的时候,总是要改写hashCode(),根据一个类的equals()(改写后),两个截然不同的实例有可能在逻辑上是相等的。但是,根据Object.hashCode(),它们仅仅是两个对象, 因此,违反了 "相等的对象必须具有相等的散列码" - 【结论】复写
equals()的时候一般都需要同时复写hashCode()。通常参与计算hashCode()的对象的属性也应该参与到equals()中进行计算
- 当一个类有自己特有的“逻辑相等”概念,当改写
- 重写
- 为什么用 Eclipse/IDEA 自动复写
hashCode(),有 31 这个数字?

5.2.2 add
HashSet 底层:数组 + 链表的结构
当向 HashSet 集合中存入一个元素 a,HashSet 首先调用元素 a 所在类的 hashCode(),计算元素 a 的 hashCode 值,此 hashCode 值接着通过某种散列函数计算出在 HashSet 底层数组中的存放位置(这个散列函数会与底层数组的长度相计算得到在 数组中的下标,并且这种散列函数计算还尽可能保证能均匀存储元素,越是散列分布, 该散列函数设计的越好) ,判断数组此位置上是否已经有元素:
- 如果此位置上没有其他元素,则元素 a 添加成功 ---> [情况1]
- 如果此位置上有其他元素 b(或以链表形式存在的多个元素),则比较元素 a 与元素 b 的 hash 值
- 如果 hash 值不相同,则元素 a 添加成功 ---> [情况2]
- 如果 hash 值相同,进而需要调用元素 a 所在类的
equals()equals()返回 true,则元素 a 添加失败equals()返回 false,则元素 a 添加成功 ---> [情况3]

对于添加成功的 [情况2] 和 [情况3] 而言,元素 a 与已经存在指定索引位置上数据以链表的方式存储:
- JDK 7:元素 a 放到数组中,指向原来的数组元素
- JDK 8:链表尾元素指向元素 a // 总结:七上八下
5.2.3 例题
public void test() {
HashSet set = new HashSet();
Person p1 = new Person("AA",21);
Person p2 = new Person("BB",22);
set.add(p1);
set.add(p2);
p1.name = "CC";
set.remove(p1);
System.out.println(set);
set.add(new Person("CC",21));
System.out.println(set);
set.add(new Person("AA",22));
System.out.println(set);
}
-------------------------------------
[Person{name='CC', age=21}, Person{name='BB', age=22}]
[Person{name='CC', age=21}, Person{name='BB', age=22}, Person{name='CC', age=21}]
[Person{name='CC', age=21}, Person{name='BB', age=22}, Person{name='CC', age=21}, Person{name='AA', age=22}]
5.2.4 LinkedHashSet
- LinkedHashSet 是 HashSet 的子类
- LinkedHashSet 根据元素的 hashCode 值来决定元素的存储位置, 但它同时使用双向链表维护元素的次序,这使得元素看起来是以插入顺序保存的
- LinkedHashSet 插入性能略低于 HashSet,但在迭代访问 Set 里的全部元素时有很好的性能
- LinkedHashSet 不允许集合元素重复
5.3 TreeSet
TreeSet是SortedSet<I>的实现类,TreeSet可以确保集合元素处于排序状态TreeSet底层使用 [红黑树] 结构存储数据- 常用方法
Comparator<? super E> comparator()E first()E last()E lower(Object e)E higher(Object e)SortedSet<E> subSet(fromElement, toElement)SortedSet<E> headSet(toElement)SortedSet<E> tailSet(fromElement)
- TreeSet 两种排序方法:[自然排序] 和 [定制排序]。默认情况下,TreeSet 采用自然排序
5.3.1 自然排序
TreeSet 会调用集合元素的 compareTo(T obj) 来比较元素之间的大小关系,然后将集合元素按升序(默认情况)排列。所以,如果试图把一个对象添加到 TreeSet 时,则该对象的类必须实现 Comparable<I>。
Comparable 的典型实现:

- 向 TreeSet 中添加元素时,只有第一个元素无须比较
compareTo(),后面添加的所有元素都会调用compareTo()进行比较 - 因为只有相同类的两个实例才会比较大小,所以向 TreeSet 中添加的应该是同 一个类的对象
- 对于 TreeSet 集合而言,它判断两个对象是否相等的唯一标准是:两个对象通过
compareTo(T obj)比较返回值 - 当需要把一个对象放入 TreeSet 中,重写该对象对应的
equals()时,应保证该方法与compareTo(T obj)有一致的结果:如果两个对象通过equals()比较返回 true,则通过compareTo(T obj)比较应返回 0。 否则,让人难以理解。
5.3.2 定制排序
- TreeSet 的自然排序要求元素所属的类实现
Comparable<I>,如果元素所属的类没有实现Comparable<I>,或不希望按照升序(默认情况)的 方式排列元素或希望按照其它属性大小进行排序,则考虑使用 [定制排序]。定制排序,通过Comparator<I>来实现,需要重写compare(T o1, T o2) - 利用
int compare(T o1, T o2),比较 o1 和 o2 的大小:如果方法返回正整数,则表示 o1 大于 o2;如果返回 0,表示相等;返回负整数,表示 o1 小于 o2。 - 使用 [定制排序] 判断两个元素相等的标准是:通过
Comparator比较两个元素返回了 0。 - 要实现定制排序,需要将实现
Comparator<I>的实例作为形参传递给 TreeSet 的构造器:TreeSet(Comparator<? super E> comparator)。此时,仍然只能向 TreeSet 中添加类型相同的对象。否则发生ClassCastException