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[] elementData
list.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
位置插入ele
boolean 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
位置的元素为ele
List<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