一、集合框架总览
集合框架是一个用来代表和操纵集合的统一架构。所有的集合框架都包含如下内容:
- 接口:是代表集合的抽象数据类型。例如 Collection、List、Set、Map 等。之所以定义多个接口,是为了以不同的方式操作集合对象。
- 实现(类):是集合接口的具体实现。从本质上讲,它们是可重复使用的数据结构,例如:ArrayList、LinkedList、HashSet、HashMap。
- 算法:是实现集合接口的对象里的方法执行的一些有用的计算,例如:搜索和排序,这些算法实现了多态,那是因为相同的方法可以在相似的接口上有着不同的实现。
除了集合,该框架也定义了几个 Map 接口和类。Map 里存储的是键/值对。尽管 Map 不是集合,但是它们完全整合在集合中。
(ps:实线是继承,虚线是实现)
Java 集合框架主要包括两种类型的容器,一种是集合(Collection),存储一个元素集合,另一种是图(Map),存储键/值对映射。
在集合框架体系下,衍生出四种具体的集合类型:Map、Set、List、Queue,再下面是一些抽象类,最后是具体实现类。
- List:元素放入有序,可重复;
- Set:元素放入无序,不可重复;
- Map:保存键值对映射,一对一或一对多;
- Queue:一个可存储重复元素的队列,其特性与List相同,但只能从队头和队尾操作元素。
集合框架体系如图所示
二、迭代器
一般遍历数组都是采用for循环或者增强for,这两个方法也可以用在集合框架,但是还有一种方法是采用迭代器遍历集合框架,它是一个对象,实现了Iterator 接口或 ListIterator接口。
迭代器,使你能够通过循环来得到或删除集合的元素。
1. Iterator
Iterator接口提供的API接口如下:
- hasNext():判断集合中是否存在下一个对象
- next():返回集合中的下一个对象,并将访问指针移动一位
- remove():删除集合中调用next()方法返回的对象
2. Iterator
Iterator 是提供集合操作内部对象的一个迭代器,它可以遍历、移除对象,且只能够单向移动
Iterable是对Iterator的封装,在JDK 1.8时,实现了Iterable接口的集合可以使用增强 for 循环遍历集合对象。
List<Integer> list = new ArrayList<>();
for (Integer num : list) {
System.out.println(num);
}
3. ListIterator
ListIterator 继承了 Iterator ,以允许双向遍历列表和修改元素。
ListIterator 存在于 List 集合之中,通过调用方法可以返回起始下标为 index的迭代器,可以在任意一个下标位置返回该迭代器,且可以实现双向遍历。
public interface ListIterator<E> extends Iterator<E> {
boolean hasNext();//判断集合中是否存在下一个对象
E next();//返回集合中的下一个对象,并将访问指针移动一位
boolean hasPrevious();//判断集合中是否存在上一个对象
E previous();//返回集合中的上一个对象,并将访问指针移动一位
int nextIndex();//返回下一个对象的下标位置
int previousIndex();//返回上一个对象的下标位置
void remove();//删除集合中调用next()方法返回的对象
void set(E e);
void add(E e);
}
三、Map集合体系详解
Map接口是由<key, value>组成的集合,由key映射到唯一的value,所以Map不能包含重复的key,每个键至多映射一个值。
1. HashMap
- 它是集合中最常用的Map集合类型,底层由数组 + 链表 + 红黑树组成
- 插入元素时,通过计算元素的哈希值,通过哈希映射函数转换为数组下标;查找元素时,同样通过哈希映射函数得到数组下标定位元素的位置
- HashMap不是线程安全的
2. LinkedHashMap
- LinkedHashMap是 HashMap 的子类,所以它具备 HashMap 的所有特点,它也不是线程安全的。
- 它在 HashMap 的基础上维护了一条双向链表,该链表存储了所有元素,默认元素的顺序与插入顺序一致。
- 可实现LRU缓存淘汰策略,其原理是通过设置accessOrder为true并重写removeEldestEntry方法定义淘汰元素时需满足的条件
3. TreeMap
- TreeMap 是 SortedMap 的子类,所以它具有排序功能。
- 它底层是由红黑树这种数据结构实现的,所以操作的时间复杂度恒为O(logN)
- TreeMap 可以对key进行自然排序或者自定义排序,自定义排序时需要传入Comparator,而自然排序要求key实现了Comparable接口
- TreeMap 不是线程安全的。
通过传入新的Comparator比较器,可以覆盖默认的排序规则。
compare()方法的返回值有三种,分别是:0,-1,+1
- 如果返回0,代表两个元素相等,不需要调换顺序
- 如果返回+1,代表前面的元素需要与后面的元素调换位置
- 如果返回-1,代表前面的元素不需要与后面的元素调换位置
4.WeakHashMap
- 它的键是一种弱键,放入 WeakHashMap 时,里面Entry中的键在每一次的垃圾回收都会被清除掉,所以不能确保某次访问元素一定存在
- 它依赖普通的Map进行实现,是一个非线程安全的集合
- WeakHashMap 通常作为缓存使用,适合存储那些只需访问一次、或只需保存短暂时间的键值对
5. Hashtable
- Hashtable 底层的存储结构是数组 + 链表
- HashTable 是一个线程安全的 Map,它所有的方法都被加上了 synchronized 关键字
- Hashtable 的性能在并发环境下非常差
四、Set接口
Set接口继承了Collection接口,是一个不包括重复元素的集合。存入可变元素时,必须非常小心,因为任意时候元素状态的改变都有可能使得 Set 内部出现两个相等的元素,即 o1.equals(o2) = true,所以一般不要更改存入 Set 中的元素,否则将会破坏了 equals() 的作用。而Set 的最大作用就是判重,在项目中最大的作用也是判重。
1. AbstractSet 抽象类
AbstractSet 是一个实现 Set 的一个抽象类,定义在这里可以将所有具体 Set 集合的相同行为在这里实现,避免子类包含大量的重复代码
2. SortedSet 接口
SortedSet 是一个接口,它在 Set 的基础上扩展了排序的行为,所以所有实现它的子类都会拥有排序功能。
Comparator<? super E> comparator(); | 元素的比较器,决定元素的排列顺序 |
SortedSet subSet(E var1, E var2) | 获取 [var1, var2] 之间的 set |
SortedSet headSet(E var1) | 获取以 var1 开头的 Set |
SortedSet tailSet(E var1) | 获取以 var1 结尾的 Set |
E first() | 获取首个元素 |
E last() | 获取最后一个元素 |
3. HashSet
- 底层数据结构:HashSet 也是采用数组 + 链表 + 红黑树实现
- 线程安全性:由于采用 HashMap 实现,而 HashMap 本身线程不安全,在HashSet 中没有添加额外的同步策略,所以 HashSet 也线程不安全
- 存入 HashSet 的对象的状态最好不要发生变化,因为有可能改变状态后,在集合内部出现两个元素o1.equals(o2),破坏了 equals()的语义。
4. LinkedHashSet
- 它继承了 HashSet,而 HashSet 默认是采用 HashMap 存储数据的,但是 LinkedHashSet 调用父类构造方法初始化 map 时是 LinkedHashMap 而不是 HashMap
- 由于 LinkedHashMap 不是线程安全的,且在 LinkedHashSet 中没有添加额外的同步策略,所以 LinkedHashSet 集合也不是线程安全的
5.TreeSet
- TreeSet 是基于 TreeMap 的实现,所以存储的元素是有序的,底层的数据结构是数组 + 红黑树,任意操作的平均时间复杂度为 O(logN)
- 元素的排列顺序有2种,和 TreeMap 相同:自然排序和定制排序,TreeSet 默认按照自然排序,如果需要定制排序,需要传入Comparator
- TreeSet 是一个线程不安全的集合
- TreeSet 常应用于对不重复的元素定制排序
三、List接口
List 接口直接继承 Collection 接口,它定义为可以存储重复元素的集合,并且元素按照插入顺序有序排列,且可以通过索引访问指定位置的元素。常见的实现有:ArrayList、LinkedList、Vector和Stack
1.AbstractList 和 AbstractSequentialList
- AbstractList 抽象类实现了 List 接口,其内部实现了所有的 List 都需具备的功能。
- AbstractSequentialList 抽象类继承了 AbstractList,在原基础上限制了访问元素的顺序只能够按照顺序访问,而不支持随机访问,如果需要满足随机访问的特性,则继承 AbstractList。
2.Vector
JDK 1.0 时代,ArrayList 还没诞生,大家都是使用 Vector 集合,但由于 Vector 的每个操作都被 synchronized 关键字修饰,即使在线程安全的情况下,仍然进行无意义的加锁与释放锁,造成额外的性能开销,做了无用功。因为性能低下原因被淘汰了。
3.Stack
Stack是一种后入先出(LIFO)型的集合容器,Stack 继承了 Vector 类,所以也被淘汰了。
4. ArrayList
- 具备随机访问特点,访问元素的效率较高,ArrayList 在频繁插入、删除集合元素的场景下效率较低。
- 底层数据结构:ArrayList 底层使用数组作为存储结构,具备查找快、增删慢的特点
- 线程安全性:ArrayList 是线程不安全的集合
- ArrayList 首次扩容后的长度为 10,调用 add() 时需要计算容器的最小容量。可以看到如果数组elementData为空数组,会将最小容量设置为10,之后会将数组长度完成首次扩容到 10。
- 集合从第二次扩容开始,数组长度将扩容为原来的 1.5 倍
5. LinkedList
- 优势:LinkedList 底层没有扩容机制,使用双向链表存储元素,所以插入和删除元素效率较高,适用于频繁操作元素的场景
- 劣势:LinkedList 不具备随机访问的特点,查找某个元素只能从 head 或 tail 指针一个一个比较,所以查找中间的元素时效率很低
- 查找优化:LinkedList 查找某个下标 index 的元素时做了优化,若 index > (size / 2),则从 head 往后查找,否则从 tail 开始往前查找
- 双端队列:使用双端链表实现,并且实现了 Deque 接口,使得 LinkedList 可以用作双端队列。
遍历 ArrayList
import java.util.*;
public class Test{
public static void main(String[] args) {
List<String> list=new ArrayList<String>();
list.add("Hello");
list.add("World");
list.add("HAHAHAHA");
//第一种遍历方法使用 For-Each 遍历 List
for (String str : list) { //也可以改写 for(int i=0;i<list.size();i++) 这种形式
System.out.println(str);
}
//第二种遍历,把链表变为数组相关的内容进行遍历
String[] strArray=new String[list.size()];
list.toArray(strArray);
for(int i=0;i<strArray.length;i++) //这里也可以改写为 for(String str:strArray) 这种形式
{
System.out.println(strArray[i]);
}
//第三种遍历 使用迭代器进行相关遍历
Iterator<String> ite=list.iterator();
while(ite.hasNext())//判断下一个元素之后有值
{
System.out.println(ite.next());
}
}
}
遍历 Map
import java.util.*;
public class Test{
public static void main(String[] args) {
Map<String, String> map = new HashMap<String, String>();
map.put("1", "value1");
map.put("2", "value2");
map.put("3", "value3");
//第一种:普遍使用,二次取值
System.out.println("通过Map.keySet遍历key和value:");
for (String key : map.keySet()) {
System.out.println("key= "+ key + " and value= " + map.get(key));
}
//第二种
System.out.println("通过Map.entrySet使用iterator遍历key和value:");
Iterator<Map.Entry<String, String>> it = map.entrySet().iterator();
while (it.hasNext()) {
Map.Entry<String, String> entry = it.next();
System.out.println("key= " + entry.getKey() + " and value= " + entry.getValue());
}
//第三种:推荐,尤其是容量大时
System.out.println("通过Map.entrySet遍历key和value");
for (Map.Entry<String, String> entry : map.entrySet()) {
System.out.println("key= " + entry.getKey() + " and value= " + entry.getValue());
}
//第四种
System.out.println("通过Map.values()遍历所有的value,但不能遍历key");
for (String v : map.values()) {
System.out.println("value= " + v);
}
}
}
总结
Java集合框架为程序员提供了预先包装的数据结构和算法来操纵他们。
集合是一个对象,可容纳其他对象的引用。集合接口声明对每一种类型的集合可以执行的操作。
集合框架的类和接口均在java.util包中。
任何对象加入集合类后,自动转变为Object类型,所以在取出的时候,需要进行强制类型转换。