4-3 集合
类集概述
-
引入的目的:数组这种数据结构无法满足各种应用场景,因此Java内部对各种数据结构进行了成熟的实现,最终JDK1.2后把这些类放在了一个
java.util
包中。 -
类集中最大的几个操作接口:
Collection
、Map
、Iterator
,这三个接口为以后要使用的最重点的接口。 -
Java类集结构图:
链表与二叉树
-
数组结构的分析:
- 数组缺点:插入和删除操作均涉及到移动,比较繁琐,且效率低。
- 数组优点:查找和存储比较快。
-
链表结构分析:
-
每个节点除了存储自己的数据以外,还存储了指向与其关联的节点的引用。
-
单向链表中,只存储了下一个节点的引用,而双向链表还存储了上一个节点的引用。
-
循环链表中,最后一个节点指向了第一个节点,行程一个环。
-
举例:
//以单向链表存储类为例 class Node { Object data; Node next; // 指向下一个节点 } //以双向链表存储类为例 class Node { Object data; Node prev;// 指向上一个节点 Node next;// 指向下一个节点 }
-
-
二叉树结构分析:
-
每个节点除了存储自己的数据以外,还存储了两个与其关联的节点引用。
-
一个引用指向左边的下一个节点,另一个引用指向右边下一个节点。
-
在存储时可以将数据与当前节点的数据进行比较,分两类存放在当前节点的左右节点中。
-
由于左右节点的分类存储,导致在二叉树查找数据时,效率比较高。
-
举例:
//以双向链表存储类为例 class Node { Object data; Node left; // 指向左边的节点 Node right;// 指向右边的节点 } /** * 如果左边存放比当前节点小的数 右边存放其他数 * 这样在查找时,没找到一个节点就省去一个分支的查找开销 */
-
-
二叉树遍历(相对于当前节点即根节点而言,每输出一个节点后,当前节点发生改变):
- 先序遍历:先输出根节点,后输出左节点,最后输出右节点。
- 中序遍历:先输出左节点,后输出根节点,最后输出右节点。
- 先序遍历:先输出左节点,后输出右节点,最后输出根节点。
常见数据结构
栈结构
- 又称堆栈,注意不是堆!
- 是限定仅在表尾进行插入和删除操作的线性表。
- 根据存取数据的先后顺序:先进后出。存元素成为压栈,取元素成为弹栈。
队列结构
- 队列是一种特殊的线性表,是运算受到限制的一种线性表,只允许在表的一端进行插入,而在另一端进行删除元素的线性表。队尾(rear)是允许插入的一端。队头(front)是允许删除的一端。空队列是不含元素的空表。
- 根据存取数据的先后顺序:先进先出。存元素成为入队,取元素成为出队。
二叉树
-
binary tree ,是每个结点不超过2的有序树(tree)。
-
红黑树在二叉树的基础上增加了一些条件限制:
- 节点可以是红色的或者黑色的
- 根节点是黑色的。
- 叶子节点(特指空节点)是黑色的。
- 每个红色节点的子节点都是黑色的。
- 任何一个节点到其每一个叶子节点的所有路径上黑色节点数相同。
-
红黑树特点:速度特别快,趋近平衡树,查找叶子元素最少和最多次数不多于二倍。
Collection接口(重点)
-
Collection 接口是在整个 Java 类集中保存单值的最大操作父接口,里面每次操作的时候都只能保存一个对象的数据
-
其父接口一般情况下不使用,基本上使用其子接口,其子接口继承了其父接口,并重载了部分接口:
-
在开发中不会直接使用
Collection
接口。而使用其操作的子接口:List
、Set
List 接口(重点)
- 单值操作的接口,接口不能直接使用,需要有具体的实现类。接口表如下:

- 单独看一下
remove
接口,在List中由于重载了一个接口,因此有两个接口:- 继承父接口的
remove
:传入Object,返回是布尔值类型,注意不一定代表是否删除成功。 - 自己重载的接口
remove
:出入删除的指定位置,返回当前被删除的内容。 - 应用场景:当需要取出某个数据并不再使用它时,可以考虑重载后的
remove
接口。
- 继承父接口的
ArrayList
实现类(重点)
-
ArrayList
是线程不安全的,但是效率高。 -
使用
ArrayList
实例化List时,在内存中开辟了一个Object数组,注意其长度为0!!! -
当第一次使用接口增加内容时,会扩容到一个默认长度为10。
-
动态扩容的算法考虑到了:
- 数据量太大,无法分配,抛出溢出异常。
- 首先将原来的长度扩容到1.5倍,然后拿这个数与最小的需求长度进行比较,用来排查原来长度为0或1或一组数据(长度不可控)的情况。
- 若扩容1.5倍后的长度够用,且长度未溢出,还会在(
int类型最大值-8
)中进行处理,若在8个范围内,则取int
类型最大值,如果在范围之外(左边),则取(int类型最大值-8
)。
-
常用方法举例:
List<String> all = new ArrayList<String>(); // 实例化List对象,并指定泛型类型 all.add("hello "); // 增加内容,此方法从Collection接口继承而来 all.add(0, "LAMP ");// 增加内容,此方法是List接口单独定义的 all.add("world"); // 增加内容,此方法从Collection接口继承而来 all.remove(1); // 根据索引删除内容,此方法是List接口单独定义的 all.remove("world");// 删除指定的对象 System.out.print("集合中的内容是:"); for (int x = 0; x < all.size(); x++) { // size()方法从Collection接口继承而来 System.out.print(all.get(x) + "、"); // 此方法是List接口单独定义的 }
Vector
实现类(重点)
-
Vector
是同步的,线程安全的,但效率较低。 -
其操作基本上是和
ArrayList
相同的。 -
构造方法中,可以指定数据容量和增量,初始容量为10,初始增量为0。
-
动态扩容算法:
- 若增量设置为0,则扩容时是扩容到2倍。
-
常用方法举例:
List<String> all = new Vector<String>(); // 实例化List对象,并指定泛型类型 all.add("hello "); // 增加内容,此方法从Collection接口继承而来 all.add(0, "LAMP ");// 增加内容,此方法是List接口单独定义的 all.add("world"); // 增加内容,此方法从Collection接口继承而来 all.remove(1); // 根据索引删除内容,此方法是List接口单独定义的 all.remove("world");// 删除指定的对象 System.out.print("集合中的内容是:"); for (int x = 0; x < all.size(); x++) { // size()方法从Collection接口继承而来 System.out.print(all.get(x) + "、"); // 此方法是List接口单独定义的 }
LinkedList
实现类(了解)
-
内部使用的是一个双向链表来存储数据的,在性能上插入和删除效率较高与
ArrayList
、Vector
互补。 -
其除了是
List
的实现类外,还是队列接口的实现类。 -
其提供了从头部/尾部增加和删除的方法,因此可以用来模拟栈和队列的操作:
-
从头部增加:
public void addFirst(E e)
public boolean offerFirst(E e)
-
从尾部增加:
public void addLast(E e)
public boolean offer(E e)
public boolean offerLast(E e)
-
从头部删除:
public E poll()
public E remove()
public E removeFirst()
-
从尾部删除:
public E removeLast()
-
另外,在实际工作中,很少使用相关方法进行栈或队列的模拟,因为链表结构开发较少。
-
Iterator
与ListIterator
Iterator接口
-
它与
Collection
、Map
接口有所不同,Collection
接口与Map
接口主要用于存储元素,而Iterator
主要用于迭代访问(即遍历)Collection 中的元素。 -
迭代:即Collection集合元素的通用获取方式。在取元素之前先要判断集合中有没有元素,如果有,就把这个元素取出来,继续在判断,如果还有就再取出出来。一直把集合中的所有元素全部取出。这种取出方式专业术语称为迭代。
-
ListIterator
只用来迭代List
集合,而Iterator
可以用来迭代所有Collection
下的所有单值存储的集合。Iterator
中没有previous
方法,但是ListIterator
具有更多的方法,包含previous
方法。 -
使用举例:
List<String> all = new ArrayList<>(); // 实例化List对象,并指定泛型类型 all.add("hello "); // 增加内容,此方法从Collection接口继承而来 all.add(0, "LAMP ");// 增加内容,此方法是List接口单独定义的 all.add("world"); // 增加内容,此方法从Collection接口继承而来 /** * ListIterator 只能用来迭代list下的集合 */ ListIterator<String> iterator = all.listIterator(); System.out.print("集合中的内容是:"); //往后移动 打印 while (iterator.hasNext()){ System.out.println(iterator.next()); } //往迁移动 打印 这个方法Iterator不具备 while (iterator.hasPrevious()){ System.out.println(iterator.previous()); }
增强for
-
增强for循环(也称for each循环)是JDK1.5以后出来的一个高级for循环,专门用来遍历数组和集合的。
-
for each循环
,内部原理其实是个Iterator迭代器
,所以在遍历的过程中,不能对集合中的元素进行增删操作。 -
注意,新for循环使用有两个条件:
- 必须有被遍历的目标。且只能为数组或collection集合。
- 新式for循环仅仅作为遍历操作出现。
-
使用举例:
/** * for each循环迭代数组 */ int[] arr = {3, 5, 6, 87}; for (int a : arr) {//a代表数组中的每个元素 System.out.println(a); } /** * for each循环迭代集合 */ Collection<String> coll = new ArrayList<String>(); coll.add("锄禾日当午"); coll.add("汗滴禾下土"); coll.add("谁知盘中餐"); coll.add("粒粒皆辛苦"); //使用增强for遍历 for (String s : coll) {//接收变量s代表 代表被遍历到的集合元素 System.out.println(s); }
Set接口
java.util.Set
接口和java.util.List
接口一样,同样继承自Collection
接口,它与Collection
接口中的方法基本一致,并没有对Collection
接口进行功能上的扩充,只是比Collection
接口更加严格了。- 与
List
接口不同的是,Set
接口中元素无序不重复,并且都会以某种规则保证存入的元素不出现重复, - 该接口没有提供
get
方法,取出元素可以采用迭代器和增强for循环。 - Set接口的实现类举例:
java.util.HashSet
、java.util.LinkedHashSet
。接口必须通过实现类来使用。无法单独使用。
HashSet
实现类
-
java.util.HashSet
是 Set 接口的一个实现类,它所存储的元素是不可重复的,并且元素都是无序的(即存取顺序不一致)。特别是它不保证该顺序恒久不变。java.util.HashSet
底层的实现其实是一个java.util.HashMap
支持。 -
HashSet
是根据对象的哈希值来确定元素在集合中的存储位置,因此具有良好的存取和查找性能。保证元素唯一性的方式依赖于:hashCode
与equals
方法。 -
存放自定义类型元素时,需要重写对象中的
hashCode
和equals
方法,建立自己的比较方式,才能保证HashSet
集合中的对象唯一。 -
HashSet
下面有一个子类java.util.LinkedHashSet
,它是链表和哈希表组合的一个数据存储结构。该结构可以保证存储的数据是有序的。 -
举例说明:
//创建 Set集合 HashSet<String> set = new HashSet<String>(); //添加元素 set.add("锄禾日当午"); set.add("锄禾日当午");//该方法返回false并未存储成功 set.add("汗滴禾下土"); //遍历 for (String name : set) { System.out.println(name); } /** * 输出: 汗滴禾下土 * 锄禾日当午 * 注意:输出的顺序并非按添加的顺序,因为存储的数据结构既不是栈也不是队列,而是Map。 */
TreeSet
实现类
- 基于
TreeMap
结构,生成一个总是处于排序状态的set
,内部以TreeMap
来实现。它是使用元素的自然顺序对元素进行排序,或者根据创建Set 时提供的Comparator
进行排序,具体取决于使用的构造方法。 - 存放自定义类型的元素时,需要实现
Comparator
接口的方法compareTo
,否则无法进行存储操作!!!
Comparable
接口
- 实现接口方法
compareTo
规则:- 比较的是什么:比较传入的元素和this元素进行比较。
- 如果小于this元素则返回1(正数即可),如果相等则返回0,否则返回-1(负数即可)。
- 注意,大小的规则由用户自己定义,可以根据元素的某些属性,或者所有属性进行排序。
- 记忆口诀:小的排在后面。
- 只在实现类中实现一次,无法动态更改。
Comparator
接口(扩展)
- 应用场景:java数组工具类和集合工具类中提供对sort方法排序就是使用
Comparator
接口来处理排序的。 - 例如数组排序中,传入的是一个对象数组,这时候就需要从外部传入一个
Comparator
接口实现,主要是实现int compare(T o1, T o2)
方法。 int compare(T o1, T o2)
实现规则:- a negative integer, zero, or a positive integer as the first argument is less than, equal to, or greater than the second.
- 即当第一个元素小于第二时,返回正数。
- 如果相等返回0.
- 如果大于第二个元素,则返回负数。
- 记忆口诀:小的排在后面。
- 在外部实现,可以动态调整排序算法。
Map集合(键值对存储)
-
单值存储中的Set集合借用了Map集合下的Map数据结构,由于Map的键值对存储机制中要求键名不能重复,在Set集合中借用这个特性,将Set需要存储的值作为Map的键名进行存储,而键值填充一个默认
空Object
. -
第二大集合操作接口:
-
常用的接口举例:
clear
方法:清空集合的内容。keySet
方法:由于没有下标无法向数组那样遍历,只能先通过该方法将key
变为一个Set集合
,然后通过这个集合中的key
,分别通过get
去获取各自的值。- 存储方法:
put
存储一个键值对,且返回与键关联的之前的值(null代表该键没有被关联过),putAll
增加一组集合。 remove
删除方法:返回与该键关联的之前的值,应用场景是:当取出某个键值对后不再使用时,可以采用remove
方法代替get
方法来实现。- 判断是否存在的方法:
containsKey
判断某个键是否存在,containsValue
判定某个值是否存在。 size()
获取键值对数量。
-
应用时需要使用其实现类如:
HashMap
、TreeMap
、Hashtable
。
HashMap
实现类
-
HashMap
本身是属于无序存放的。 -
Object
类中提供了默认的hasCode
方法,各对象可以重写该方法,也可以直接使用默认的方法,只是效率较低。该方法提供了一个因对象的变化而生成一个比较均匀的int
类型的哈希值,散列分布。 -
哈希表的结构:
-
首先有一个数组成为哈希桶,每个桶中存放一个链表,当链表的数据量大于8时,桶中的数据结构由链表变为红黑二叉树,当红黑二叉树的数据量降低为6时,又退化到链表结构。(注意6-8时可能存在两者中的一种结构,并非确定,视具体情况而定)。
-
空间和时间的权衡:哈希桶的数量默认为16,默认加载因子0.75,创建时可以指定。当哈希桶桶的数量已经使用达到75%后,则会扩容哈希桶到其的两倍,当该值过小,就可能出现某一个哈希桶中存放了过多的数据,这样就会降低操作该桶中数据的效率。
-
当不同哈希值计算得到的哈希桶为同一个时,就往同一个桶中存放该数据,
-
底层原理介绍:
-

HashMap
/Hashtable
/ConcurrentHashMap
- 三者最主要的区别在于多线程安全与否。在代码调用方面基本相同。三者不保证存储顺序。
HashMap
是线程不安全的,效率高。Hashtable
是线程安全的,效率低。ConcurrentHashMap
,线程安全,效率比较高。采用的是分段锁机制,对不同的桶分别进行加锁。
TreeMap
/LinkedHashMap
TreeMap
不保证存储顺序,但是会自动进行排序,按自然顺序排序,但是不会按存储的顺序进行排序。- 使用
TreeMap
需要实现Comparable
接口的compareTo
方法。 LinkedHashMap
加入了链表的机制,可以根据存储的顺序进行排序,且查找效率较高。
Map存储自定义对象
- 需要支持
hashCode
和equals
方法。 - 一旦存入了某个对象实例,如果之后改变了对象的某些属性:
- 要求要求要求:作为键名的值一定不要发生改变。
- 对象作为键名去哈希桶中取数据,发现根据对象新计算出来哈希值发生改变,不再指向原来的哈希桶。
- 存入哈希桶中的键值对可能不定期会更换哈希桶(重新散列),在更换之前如果改变了某些属性,则重新入桶时,是根据新计算的哈希值入桶,如果使用之前的对象的副本来查找该数据,将无法找到。
- 当存储后改变了某个属性,假设未发生重新散列,再使用相同对象的副本可以获取相同的哈希值去查找到对应的哈希桶,但是根据
equals
方法比较时,发现某个属性发生改变,也同样无法查找到原数据。
JDK9集合新特性
- JDK9为
List
/Set
/Map
三个接口提供了静态方法创建固定长度的集合,其子类都不具备。 - 通过重载的方法提供了列表长度从0到10个数量的这11个
of
方法用来创建一个不能更改的集合。
我的疑问
Set
接口下的元素是无序不重复的,但是其实现类TreeSet
为啥是按照自然顺序存储