从”0“开始学JAVA——第九节下 泛型和集合框架
上一节我们介绍了泛型的概念、集合框架的两大根接口——Collection(单列集合)接口、Map(双列集合)接口,以及Collection接口下的两个重要的子接口——List接口和Set接口,除此之外还举例了这几个接口的常用方法。
那么这一节,我们一起来了解一下List接口、Set接口、Map接口下的几个常用实现类以及TreeSet中的两种排序方法——Comparable(自然排序)、Comparator(比较器排序):
第一部分:List接口下的实现类
1. ArrayList
特点特性:
底层实现:基于动态数组(
Object[] elementData
)。线程安全:否。非同步,多线程环境下不安全。
元素访问:支持随机访问(通过索引),实现了
RandomAccess
标记接口。扩容机制:当数组容量不足时,会自动扩容(通常是增加为原来的1.5倍)。这是一个相对耗时的操作。
优点:
查询效率高(
get(int index)
,set(int index, E element)
)。因为数组在内存中是连续存储的,通过索引可以直接计算出内存地址,时间复杂度为 O(1)。
缺点:
增删效率较低(除非在尾部操作)。在列表中间进行插入(
add(int index, E element)
)或删除(remove(int index)
)操作时,需要移动后续的所有元素,平均时间复杂度为 O(n)。
适用场景:
查询操作远多于增删操作的场景。
例如:商品列表展示、配置文件读取、学生管理系统(学期开始录入后,主要以查询为主)。
2. LinkedList
特点特性:
底层实现:基于双向链表数据结构。
线程安全:否。
元素访问:不支持高效随机访问。需要从链表头或尾开始遍历,平均时间复杂度为 O(n)。
优点:
增删效率高。在已知位置(例如通过
listIterator
获得的位置)进行插入和删除操作,只需要修改相邻节点的指针,时间复杂度为 O(1)。特别适合在列表中间进行频繁的增删。天然支持队列操作。实现了
Deque
接口,可以很方便地作为栈、队列、双端队列使用。
缺点:
查询效率低。按索引访问元素需要遍历,时间复杂度为 O(n)。
内存占用略高,因为每个节点都需要存储前后节点的指针。
适用场景:
增删操作远多于查询操作的场景。
需要实现队列、栈等数据结构。
例如:浏览器的前进后退历史记录、消息队列、LRU缓存。
3. Vector
特点特性:
底层实现:和
ArrayList
一样,基于动态数组。线程安全:是。其几乎所有方法都使用了
synchronized
关键字修饰,保证了同步访问。
优点:
线程安全。
缺点:
性能低下。由于方法同步带来的锁开销,效率远低于
ArrayList
。作为早期JDK的类,其API设计不够现代(如枚举遍历)。
适用场景:
已过时,不推荐在新代码中使用。
如果需要线程安全的列表,应使用
Collections.synchronizedList(new ArrayList<>())
或java.util.concurrent.CopyOnWriteArrayList
。
实现类 | 直接继承的类 | 直接实现的接口 |
---|---|---|
ArrayList | AbstractList | List , RandomAccess (标记接口), Cloneable , java.io.Serializable |
LinkedList | AbstractSequentialList (而它又继承自 AbstractList ) | List , Deque , Cloneable , java.io.Serializable |
Vector | AbstractList | List , RandomAccess (标记接口), Cloneable , java.io.Serializable |
第二部分:Set接口下的实现类
1. HashSet
特点特性:
底层实现:基于
HashMap
实现,实际上就是使用 HashMap 的 Key 来存储元素,Value 是一个固定的Object
常量。元素顺序:不保证顺序,特别是它不保证顺序恒久不变。
线程安全:否。
允许元素:允许
null
元素。
优点:
添加、删除、查找(
add
,remove
,contains
)效率极高,时间复杂度为 O(1)。性能是 Set 实现中最好的。
缺点:
无序。
适用场景:
需要快速去重且不关心元素顺序的场合。
例如:存储黑名单ID、快速判断某个元素是否存在。
2. LinkedHashSet
特点特性:
底层实现:继承自
HashSet
,但基于LinkedHashMap
实现。元素顺序:维护了一个运行于所有条目的双向链表,保证了元素的插入顺序(Insertion-Order)。
线程安全:否。
优点:
既拥有了
HashSet
的查询效率,又保证了迭代顺序。
缺点:
由于需要维护链表,性能略低于
HashSet
,但迭代访问时比HashSet
更快。
适用场景:
需要去重,且需要保持元素添加顺序的场景。
例如:缓存需要按访问顺序淘汰(可实现LRU)、记录用户操作序列(去重且有序)。
3. TreeSet
特点特性:
底层实现:基于
TreeMap
实现,使用红黑树数据结构。元素顺序:元素不是按插入顺序排序,而是根据元素的自然顺序(实现
Comparable
接口)或者构造时提供的Comparator
(比较器) 进行排序。线程安全:否。
允许元素:不允许
null
元素(取决于使用的比较器,如果使用自然排序,则不允许)。
优点:
元素自动排序,并且提供了很多按顺序访问的方法(如
first()
,last()
,headSet()
,tailSet()
)。
缺点:
添加、删除、查找操作的时间复杂度为 O(log n),比
HashSet
和LinkedHashSet
慢。
适用场景:
需要元素去重且自动排序的场景。
例如:存储成绩列表并自动按分数从高到低排序、维护一个有序的单词列表。
实现类 | 直接继承的类 | 直接实现的接口 |
---|---|---|
HashSet | AbstractSet | Set , Cloneable , java.io.Serializable |
LinkedHashSet | HashSet (它又继承自 AbstractSet ) | Set , Cloneable , java.io.Serializable |
TreeSet | AbstractSet | NavigableSet (而它又继承自 SortedSet ) |
第三部分:Map接口下的实现类
1. HashMap
特点特性:
底层实现:基于数组+链表+红黑树(JDK1.8+)。
元素顺序:不保证顺序。
线程安全:否。
允许键值:允许
null
键和null
值。
优点:
查询、插入、删除效率极高,在理想情况下(哈希函数均匀分布)时间复杂度为 O(1)。是最常用的 Map。
缺点:
无序。
适用场景:
绝大多数键值对存储场景,无需排序,无需线程安全。
例如:存储用户ID和用户信息、Web请求参数键值对。
2. Hashtable
特点特性:
底层实现:和
HashMap
类似,基于哈希表。线程安全:是。方法使用
synchronized
修饰。允许键值:不允许
null
键或null
值。
优点:
线程安全。
缺点:
性能低下,已过时。
适用场景:
已过时,不推荐使用。替代方案是
ConcurrentHashMap
。
3. LinkedHashMap
特点特性:
底层实现:继承自
HashMap
,在其基础上增加了一条双向链表来维护所有 Entry 的顺序。元素顺序:默认按插入顺序(Insertion-Order) 迭代。如果在构造时指定
accessOrder
为true
,则会按访问顺序(Access-Order) 排序(LRU算法的基础)。线程安全:否。
优点:
保证了迭代顺序,迭代速度比
HashMap
快。
缺点:
性能略低于
HashMap
。
适用场景:
需要保持键值对插入顺序或访问顺序的场景。
例如:实现 LRU(最近最少使用)缓存、记录操作日志(按时间顺序)。
4. TreeMap
特点特性:
底层实现:基于红黑树。
元素顺序:根据键的自然顺序或构造时提供的
Comparator
进行排序。线程安全:否。
允许键值:不允许
null
键(如果使用自然排序)。
优点:
键自动排序,并提供了一系列基于顺序的导航方法(如
firstKey()
,lowerKey()
)。
缺点:
操作的时间复杂度为 O(log n)。
适用场景:
需要按键排序的映射场景。
例如:字典应用程序、按部门编号排序的员工信息。
实现类 | 直接继承的类 | 直接实现的接口 |
---|---|---|
HashMap | AbstractMap | Map , Cloneable , java.io.Serializable |
LinkedHashMap | HashMap (它又继承自 AbstractMap ) | Map |
Hashtable | Dictionary (一个已过时的抽象类) | Map , Cloneable , java.io.Serializable |
TreeMap | AbstractMap | NavigableMap (而它又继承自 SortedMap ) |
第四部分:TreeSet的排序机制详解
TreeSet
的所有排序功能都依赖于其底层的 TreeMap
。要保证 TreeSet
能正确地对添加的元素进行排序,有两种方式:
1. 自然排序(Comparable)
机制:让添加到
TreeSet
中的元素类实现java.lang.Comparable
接口,并重写其compareTo(Object o)
方法。该方法定义了对象之间的自然比较规则。compareTo
方法重写规则:this
(当前对象) > 参数对象o
:返回正整数this
== 参数对象o
:返回 0(TreeSet
会认为这是重复元素,无法添加)this
< 参数对象o
:返回负整数
示例:
Student基础类:
测试类:
运行结果:
2. 比较器排序(Comparator)
机制:在创建
TreeSet
时,传入一个实现了java.util.Comparator
接口的比较器对象。该方式更加灵活,它不需要修改元素类本身,并且可以为同一个类定义多种不同的排序规则。compare
方法重写规则:参数
o1
> 参数o2
:返回正整数o1
==o2
:返回 0o1
<o2
:返回负整数
示例:
Student基础类:
测试类:
运行结果:
特性 | 自然排序 (Comparable) | 比较器排序 (Comparator) |
---|---|---|
耦合度 | 高,排序规则定义在类内部 | 低,排序规则定义在类外部 |
灵活性 | 差,一个类只能有一种自然顺序 | 高,可以轻松创建多种不同的排序规则 |
适用场景 | 类有明显的、唯一的自然顺序(如String , Integer ) | 1. 类没有实现Comparable 2. 想覆盖类的自然顺序 3. 需要为同一个类提供多种排序方式 |
总结:优先考虑使用 Comparator
,因为它更灵活,对代码的侵入性更小。