四、 Java集合框架体系结构
4.1 集合
01、什么是集合?
Java 集合,也叫作容器,主要是由两大接口派生而来:Collection
接口和Map
接口。
-
Collection 接口:主要由 List(元素有序、可重复)、Set(元素无序、不可重复)、Queue(代表队列) 组成。
-
Map 接口:代表键值对。
注:此图来自 Guide 哥。图中只列举了主要的继承派生类关系,并没有列举出所有关系。本文只是对整个集合框架的体系进行概括性介绍。
02、集合与数组
集合:集合是 java 中提供的一种容器,可以用来存储多个数据。
数组:数组就是存储数据长度固定的容器,保证多个数据的数据类型要一致。
区别:
- 长度:数组的长度是固定的;集合的长度是可变的。
- 存储:数组存储的是同一类型的元素,可以存储基本数据类型值;集合存储的都是对象,而且对象的类型可以不一致。
4.2 Collection接口
集合本身是一个工具,它存放在 java.util 包中,在 Collection 接口中定义着单列集合框架中共性的内容。
Collection 是所有单列集合的父接口,因此在 Collection 中定义了单列集合( List 和 Set )通用的一些方法,这些方法可用于操作所有的单列集合:
- public boolean add(E e):把给定的对象添加到当前集合中 。
- public void clear():清空集合中所有的元素。
- public boolean remove(E e):把给定的对象在当前集合中删除。
- public boolean contains(E e):判断当前集合中是否包含给定的对象。
- public boolean isEmpty():判断当前集合是否为空。
- public int size():返回集合中元素的个数。
- public Object[] toArray():把集合中的元素,存储到数组中。
public class array1 {
public static void main(String[] args) {
Collection list = new ArrayList();
// 1.public boolean add(E e):把给定的对象添加到当前集合中 。
list.add("张三");
list.add("张晓丹");
list.add("小李");
// 2.public boolean contains(E e):判断当前集合中是否包含给定的对象。
System.out.println(list.contains("小李"));//true
// 3.public boolean isEmpty():判断当前集合是否为空。
System.out.println(list.isEmpty());//false
// 4.public int size():返回集合中元素的个数。
System.out.println(list.size());//3
/**
* 5.public Object[] toArray():把集合中的元素,存储到数组中。
* 如果定义的集合没有指定泛型,那么放入集合的数据都会自动转换成Object类型。
* String[] str = (String[]) list.toArray();错误,对象不能强转换成基本类型
* String str = String.valueOf(list.toArray());正确,可以使用包装类,但是集合不能使用
*/
Object[] arr = list.toArray();
for (int i = 0;i < arr.length;i++){
System.out.println(arr[i]);
}
// 6.public boolean remove(E e):把给定的对象在当前集合中删除。
list.remove("小李");
System.out.println(list.size()); // 2
// 7.public void clear():清空集合中所有的元素。
list.clear();
}
}
01、List
List 的特点是存取有序、可以存放重复的元素
,可以用下标对元素进行操作,是对付顺序的好帮手。
List 接口主要有三个实现类:
- ArrayList: Object[ ] 数组
- Vector:Object[ ] 数组
- LinkedList: 双向链表(JDK1.6 之前为循环链表,JDK1.7 取消了循环)
ArrayList
- ArrayList 是由数组实现的,支持随机存取,也就是可以通过下标直接存取元素。
- 从尾部插入和删除元素会比较快捷,从中间插入和删除元素会比较低效,因为会涉及到数组元素的复制和移动。
- 内部数组的容量不足时会自动扩容,因此当元素非常庞大时效率会比较低。
LinkedList
LinkedList 一般都归在 List 下,不过它也实现了 Deque 接口,可以作为队列来使用。等于说,LinkedList 同时实现了 Stack、Queue、PriorityQueue 的所有功能。
- LinkedList 是由双向链表实现的,不支持随机存取,只能从一端开始遍历,直到找到需要的元素后返回。
- 任意位置插入和删除元素都很方便,因为只需要改变前一个节点和后一个节点的引用即可,不像 ArrayList 哪有需要复制和移动数组元素。
- 因为每个元素都存储了前一个和后一个节点的引用,所以相对来说,占用的内存空间会比 ArrayList 多一些。
Vextor 和 Stack
List 的实现类还有一个 Vector,是一个元老级的类,比 ArrayList 出现得更早。ArrayList 和 Vector 非常相似,只不过 Vector 是线程安全的,像 get、set、add 这些方法都加了 synchronized 关键字,就导致执行执行效率会比较低,所以现在已经很少用了。
更好的选择是并发包下的 CopyOnWriteArrayList。
Stack 是 Vector 的一个子类,本质上也是由动态数组实现的,只不过还实现了先进后出的功能(在 get、set、add 方法的基础上追加了 pop、peek 等方法),所以叫栈。
不过,由于 Stack 执行效率比较低(方法上同样加了 synchronized 关键字),就被双端队列 ArrayDeque 取代了。
02、Set
Set 的特点是存取无序、不可以存放重复的元素
,不可以用下标对元素进行操作,比较注重其独一无二的性质。
Set 接口主要有三个实现类:
- HashSet(无序,唯一): 基于 HashMap 实现的,底层采用 HashMap 来保存元素。
- LinkedHashSet: LinkedHashSet 是 HashSet 的子类,其内部其实是通过 LinkedHashMap 来实现的。有点类似于 LinkedHashMap,其内部是基于 HashMap 实现一样,不过还是有一点点区别的。
- TreeSet(有序,唯一): 红黑树(自平衡的排序二叉树)。
HashSet
HashSet 其实是由 HashMap 实现的,只不过值由一个固定的 Object 对象填充,而键用于操作。
扒一下 HashSet 的源码:
public class HashSet<E>
extends AbstractSet<E>
implements Set<E>, Cloneable, java.io.Serializable
{
static final long serialVersionUID = -5024744406713321676L;
private transient HashMap<E,Object> map;
// Dummy value to associate with an Object in the backing Map
private static final Object PRESENT = new Object();
public HashSet() {
map = new HashMap<>();
}
public boolean add(E e) {
return map.put(e, PRESENT)==null;
}
public boolean remove(Object o) {
return map.remove(o)==PRESENT;
}
}
LinkedHashSet
LinkHashSet 继承自 HashSet,其实是由 LinkedHashMap 实现的,LinkedHashSet 的构造方法调用了 HashSet 的一个特殊的构造方法:
public LinkedHashSet(int initialCapacity, float loadFactor) {
super(initialCapacity, loadFactor, true);
}
// 特殊的构造方法
HashSet(int initialCapacity, float loadFactor, boolean dummy) {
map = new LinkedHashMap<>(initialCapacity, loadFactor);
}
TreeSet
TreeSet 使用 NavigableMap 接口,最终是由 TreeMap 实现的,只不过同样操作的键位,值由一个固定的 Object 对象填充。
03、Queue
Queue,也就是队列,存储的元素是有序的、可重复的
。通常遵循先进先出(FIFO)的原则,新元素插入到队列的尾部,访问元素返回队列的头部。
Queue 接口主要有两个实现类:
- PriorityQueue: Object[ ] 数组来实现二叉堆。
- ArrayQueue: Object[ ] 数组 + 双指针。
ArrayDeque
ArrayDeque 是一个基于数组实现的双端队列,为了满足可以同时在数组两端插入或删除元素的需求,数组必须是循环的,也就是说数组的任何一点都可以被看做是起点或者终点。
举个例子:
这是一个包含了 4 个元素的双端队列,和一个包含了 5 个元素的双端队列。head 指向队列的第一个有效的元素,tail 指向队尾的第一个可以插入元素的空位,因为是循环数组,所以 head 不一定总是从 0 开始,tail 也不一定总比 head 大。
PriorityQueue
PriorityQueue 是一种优先级队列,它的出队顺序与元素的优先级有关,执行 remove 或者 poll 方法,返回的总是优先级最高的元素。
要想有优先级,元素就需要实现 Comparable 接口或者 Comparator 接口。
4.3 Map接口
Map 保存的是键值对,键要求保持唯一性,值可以重复
。
Map 接口主要有三个实现类:
- HashMap: JDK1.8 之前 HashMap 由数组+链表组成的,数组是 HashMap 的主体,链表则是主要为了解决哈希冲突而存在的(“拉链法”解决冲突)。JDK1.8 以后在解决哈希冲突时有了较大的变化,当链表长度大于阈值(默认为 8)(将链表转换成红黑树前会判断,如果当前数组的长度小于 64,那么会选择先进行数组扩容,而不是转换为红黑树)时,将链表转化为红黑树,以减少搜索时间。
- LinkedHashMap: LinkedHashMap 继承自 HashMap,所以它的底层仍然是基于拉链式散列结构即由数组和链表或红黑树组成。另外,LinkedHashMap 在上面结构的基础上,增加了一条双向链表,使得上面的结构可以保持键值对的插入顺序。同时通过对链表进行相应的操作,实现了访问顺序相关逻辑。
- Hashtable: 数组+链表组成的,数组是 Hashtable 的主体,链表则是主要为了解决哈希冲突而存在的。
- TreeMap: 红黑树(自平衡的排序二叉树)。
01、HashMap
HashMap 实现了 Map 接口,根据键的 HashCode 值来存储数据,具有很快的访问速度,最多允许一个 null 键。HashMap 的使用频率是相当高的,随着 JDK 版本的不断更新,HashMap 的底层也优化了很多次。
02、LinkedHashMap
大多数情况下,只要不涉及线程问题,Map 基本都可以使用 HashMap,不过 HashMap 有一个问题,就是迭代 HashMap 的顺序并不是 HashMap 放置的顺序,也就是无序。HashMap 的这一缺点往往会带来困扰,因为有些场景,我们期待一个有序的 Map。
于是 LinkedHashMap 就闪亮登场了。LinkedHashMap 是 HashMap 的子类,内部使用链表来记录插入/访问元素的顺序。
LinkedHashMap 可以看作是 HashMap + LinkedList 的合体,它使用了 哈希表来存储数据,又用了双向链表来维持顺序。
03、TreeMap
HashMap 是无序的,所以遍历的时候元素的顺序也是不可测的。TreeMap 是有序的,它在内部会对键进行排序,所以遍历的时候就可以得到预期的顺序。
为了保证顺序,TreeMap 的键必须要实现 Comparable 接口或者 Comparator 接口。