一、集合的框架体系
Java 集合框架提供了一套性能优良,使用方便的接口和类,其位于 java.util 包中, 所以当使用集合框架的时候需要进行导包。
Java 集合框架主要包括两种类型的容器,一种是集合(Collection),存储一个元素集合;另一种是图(Map),存储键/值对映射。
- 集合(Collection)体系如下:
- 图(Map)体系如下:
说明:
(1)Collection 接口有两个重要的子接口 List、 Set , 他们的实现子类都是单列集合。
(2)Map 接口的实现子类是双列集合,存放的 K-V(键值对)
1. 常用集合接口概述
如下表:
接口 | 描述 |
---|---|
Collection 接口 | Collection 是最基本的集合接口,一个 Collection 代表一组 Object(即 Collection 的元素), Java不提供直接继承自 Collection的类,只提供继承自 Collection 接口的子接口(如 List和 set)。Collection 接口存储一组不唯一,无序的对象(不能通过索引来访问 Collection 集合中的对象)。 |
List 接口 | List 接口继承自 Collection 接口 ,但 List 接口 是一个有序的集合,使用此接口能够精确的控制每个元素插入的位置,能够通过索引(即元素在 List 中的位置,类似于数组的下标)来访问 List 集合中的元素,第一个元素的索引为 0。而且 List 集合中允许有相同的元素。可以说,List 接口的集合存储一组不唯一,有序(插入顺序)的对象。 |
Set 接口 | Set 接口继承自 Collection 接口,具有与 Collection 完全一样的接口,只是方法上有部分不同,和 Collection 接口 相同,Set 接口存储一组唯一,无序的对象。 |
Map 接口 | Map 接口与 Collection 接口同级(彼此没有继承关系),Map 图存储一组 键-值 对象,提供key(键)到value(值)的映射。 |
Set 和 List 接口的区别:
(1)Set 接口集合存储的是无序的,不重复的数据。List 接口集合存储的是有序的,可以重复的元素。
(2)Set 集合 底层使用的是 链表数据结构,其检索效率低下,删除和插入效率高,插入和删除不会引起元素位置改变 (实现子类有 HashSet , TreeSet 等)。
(3)List 结合 底层和数组类似,但是它可以动态增长,根据实际存储的数据的长度自动增长 List 的长度。其检索元素效率高,插入和删除效率低,插入和删除会引起其他元素位置改变 (实现子类有 ArrayList , LinkedList , Vector 等)。
2. 常用 Collection 集合的实现子类
Java 提供了一套实现了 Collection 接口的标准集合类。其中一些是具体类,这些类可以直接拿来使用,而另外一些是抽象类,提供了接口的部分实现。
如下表:
类名 | 描述 |
---|---|
ArrayList 类 | 该类实现了 List 接口,允许存储 null(空值)元素,且可存储重复元素。该类实现了可变大小的数组,随机访问和遍历元素时,提供了更好的性能。该类是非同步的, 在多线程的情况下不要使用。ArrayList 类在扩容时会扩容当前容量的1.5倍。 |
Vector 类 | 该类和 ArrayList 类非常相似,但该类是同步的,可以用在多线程的情况,该类允许设置默认的增长长度,默认扩容方式为原来的2倍。 |
LinkedList 类 | 该类实现了 List 接口,允许存储 null(空值)元素,且可存储重复元素,主要用于创建链表数据结构,该类没有同步方法,如果多个线程同时访问一个 LinkedList,则必须自己实现访问同步,解决方法就是在创建 LinkedList 类 时候再构造一个同步的 LinkedList 。 |
HashSet 类 | 该类实现了 Set 接口,不允许存储重复元素,并且不保证集合中元素的顺序,其允许存储 null (空值)元素,但最多只能存储一个。 |
TreeSet 类 | 该类实现了 Set 接口,不允许存储重复元素,并且不保证集合中元素的顺序,其允许存储 null (空值)元素,但最多只能存储一个。该类可以实现排序等功能。 |
3. 常用的 Map 图的实现子类
如下表:
类名 | 描述 |
---|---|
HashMap 类 | HashMap 类是一个散列表,它存储的内容是键-值对 (key-value) 映射。该类实现了 Map 接口,根据键的 HashCode 值存储元素,具有很快的访问速度,但最多允许一个元素的键为 null (空值),它不支持线程同步。 |
TreeMap 类 | TreeMap 类继承了AbstractMap ,实现了大部分 Map 接口,并且使用一颗树。 |
HashTable 类 | Hashtable 继承自 Dictionary(字典) 类,用来存储 键-值对。 |
Properties 类 | Properties 继承自 HashTable,表示一个持久的属性集,属性列表中每个键及其对应值都是一个字符串。 |
特此说明:由于集合框架的内容繁多,因此本文只介绍 Collection 集合下的 List 接口及其重要实现子类的内容,其余集合框架的知识将会在下篇博文分享。 |
二、Collection 接口
1. Collection 接口常用方法
- 说明:所有实现了 Collection 接口的子类集合都可以使用 Collection 接口中的方法。下面使用的是实现了 List 接口的 ArrayList 子类来举例,但实现了 List 接口的子类都可以使用下列方法。
- 代码实现:
import java.util.ArrayList;
import java.util.List;
public class CollectionMethod {
public static void main(String[] args) {
// 以实现了Collection 接口 的子类 ArrayList 来举例;
ArrayList list = new ArrayList();
// 1. add:添加单个元素
list.add("jack");
list.add(10);// 底层自动装箱:list.add(new Integer(10))
list.add(true);// 同上
System.out.println("list=" + list);// [jack, 10, true]
// 2. addAll:添加多个元素
ArrayList list2 = new ArrayList();// 创建一个新的集合
list2.add("红楼梦");
list2.add("三国演义");
list.addAll(list2);
System.out.println("list=" + list);// [jack, 10, true, 红楼梦, 三国演义]
// 3. remove:删除指定元素,如果不指定则默认删除第一个元素
list.remove(true);// 指定删除某个元素
System.out.println("list=" + list);// [jack, 10, 红楼梦, 三国演义]
// 4. removeAll:删除多个元素
list.add("聊斋");
list.removeAll(list2);
System.out.println("list=" + list);// [jack, 10, 聊斋]
// 5. contains:查找元素是否存在,返回 boolean 值
System.out.println(list.contains("jack"));// T
// 6. containsAll:查找多个元素是否都存在,返回 boolean 值
System.out.println(list.containsAll(list2));// T
// 7. size:获取元素个数
System.out.println(list.size());// 3
// 8. isEmpty:判断是否为空
System.out.println(list.isEmpty());// F
// 9. clear:清空
list.clear();
System.out.println("list=" + list);// []
}
}
2. 迭代器(Iterator)
-
Iterator(迭代器)不是一个集合,它是一种用于访问 Collection 集合的接口,主要用于遍历 Collection 集合中的元素,所有实现了 Collection 接口的子类集合都可以使用迭代器。
-
迭代器的执行原理:
- 迭代器就相当于一个游标,初始时指向集合中的第1个元素的的前一个位置;
- 首先使用 hasNext() 方法来判断迭代器的下一个位置是否还有元素,
- 若下一个位置有元素则使用 next() 方法返回下一个元素,并将迭代器的位置向后移一位。
- 若没有,则不调用 next() 方法,直接退出迭代器。
迭代器常用方法:
(1)调用 coll.next() 会返回迭代器的下一个元素,并且更新迭代器的状态(迭代器下移)。
(2)调用 coll.hasNext() 判断集合中是否还有下一个元素。
(3)调用 coll.remove() 将迭代器返回的元素删除。
-
注意:在使用 next() 方法前必须使用 hasNext() 方法判断集合中是否还有下一个元素,否则可能会出现异常。
-
代码演示:
import java.util.ArrayList;
import java.util.Collection;
import java.util.Iterator;
public class CollectionIterator {
public static void main(String[] args) {
List list = new ArrayList();// 创建一个新的集合,该集合实现了 Collection 接口
list.add(1);
list.add(2);
list.add(3);
// 1. 先得到 list集合 对应的 迭代器
// 使用集合的 iterator() 方法来获得该集合的迭代器;
// 所有实现了 Collection 接口的子类都拥有 iterator() 方法
Iterator iterator = list.iterator();
// 2. 使用 while 循环 + 迭代器 遍历集合
// 首先判断集合中(下一个位置)是否还有元素
while (iterator.hasNext()) {
// 若有,则获取下一个位置的元素,迭代器位置向下移一位
Object obj = iterator.next();
// 输出该元素
System.out.println("obj=" + obj);
}
// 3. 注意:当退出 while 循环后 , 这时 iterator 迭代器,指向的是集合中的最后一个元素;
// 若再次 使用 iterator.next(); 会产生 NoSuchElementException 异常,因为集合中下一个位置没有元素存在了;
// 4. 如果希望再次遍历集合,需要重置 迭代器的状态;
iterator = list.iterator();// 重置迭代器
System.out.println("===第二次遍历===");
while (iterator.hasNext()) {
Object obj = iterator.next();
System.out.println("obj=" + obj);
}
}
}
3. Collection 集合的遍历
- 说明:所有实现了 Collection 接口的子类集合都可以使用 下面的遍历方法。
(1)使用迭代器遍历(所有集合中都可以使用 迭代器 来遍历)。
(2)使用普通 for 循环遍历(普通 for 循环 是通过元素的索引来获取元素,在无序的集合中不能此方式来获取元素。比如 实现了 Set 接口的子类集合)。
(3)使用增强 for 循环遍历(增强 for 循环的底层其实是实现了 迭代器,因此在 所有集合中都可以使用该方式遍历)。
- 代码实现:
import java.util.*;
public class ListFor {
public static void main(String[] args) {
ArrayList list = new ArrayList();// 创建一个有序的集合
list.add("jack");
list.add("tom");
list.add("鱼香肉丝");
list.add("北京烤鸭子");
//遍历方式:
//1. 迭代器
Iterator iterator = list.iterator();
while (iterator.hasNext()) {
Object obj = iterator.next();
System.out.println(obj);
}
System.out.println("=====增强for=====");
//2. 增强 for 循环
for (Object o : list) {
System.out.println("o=" + o);
}
System.out.println("=====普通for====");
//3. 普通 for 循环
for (int i = 0; i < list.size(); i++) {
System.out.println("对象=" + list.get(i));
}
}
}
三、 List 接口
(1)List 接口是继承了 Collection 接口的子接口,它是一个有序的集合,使用此接口能够精确的控制每个元素插入的位置。
(2)能够通过索引(元素在List中位置,类似于数组的下标)来访问 List 集合中的元素,第一个元素的索引为 0,而且允许有相同的元素。
(3)List 集合存储一组不唯一,有序(插入顺序)的对象/元素。
List 接口的常用方法
- 实现 List 接口的子类可以使用以下的所有方法。
List 接口的常用方法:(以其子类 ArrayList 举例)
import java.util.ArrayList;
import java.util.List;
public class ListMethod {
public static void main(String[] args) {
List list = new ArrayList();// 向上转型
list.add("张三丰");// 直接添加元素
list.add("贾宝玉");
// 1. void add(int index, Object ele):在 index 位置插入 ele元素
list.add(1, "韩顺平");// 在 index = 1的位置插入一个对象
System.out.println("list=" + list);// [张三丰, 韩顺平, 贾宝玉]
// 2. boolean addAll(int index, Collection eles):从 index位置开始将 eles中的所有元素添加进来
List list2 = new ArrayList();// 创建一个新的集合
list2.add("jack");
list2.add("tom");
list.addAll(1, list2);// 从第 1个位置开始添加
System.out.println("list=" + list);// [张三丰, jack, tom, 韩顺平, 贾宝玉]
// 3. Object get(int index):获取指定 index位置的元素
Object obj = list.get(1);// 返回了第 2个元素, jack
// 4. int indexOf(Object obj):返回 obj在集合中首次出现的位置
System.out.println(list.indexOf("tom"));// 2
// 5. int lastIndexOf(Object obj):返回 obj在当前集合中末次出现的位置
list.add("韩顺平");
System.out.println("list=" + list);
System.out.println(list.lastIndexOf("韩顺平"));// 3
// 6. Object remove(int index):移除指定 index位置的元素,并返回此元素
Object obj1 = list.remove(0);// 返回了第一个元素
System.out.println("list=" + list);// [jack, tom, 韩顺平, 贾宝玉]
// 7. Object set(int index, Object ele):设置指定 index位置的元素为 ele , 相当于是替换
list.set(1, "玛丽");
System.out.println("list=" + list);// [jack, 玛丽, 韩顺平, 贾宝玉]
// 8.List subList(int fromIndex, int toIndex):返回从 fromIndex到 toIndex位置的子集合
// 注意返回的子集合范围要满足: fromIndex <= subList < toIndex,否则会出现异常
List returnlist = list.subList(0, 2);
System.out.println("returnlist=" + returnlist);// [jack, 玛丽, 韩顺平]
}
}
四、ArrayList 类(列表)
- ArrayList 类实现了 List 接口;在ArrayList 集合中可以加入多个 null (空值)作为元素。
- ArrayList 集合的底层是一个可以动态修改的数组,与普通数组的区别就是它是没有固定大小的限制,可以随意添加或删除元素。
- ArrayList 类中的常用方法已经在上面的 Collection 接口和 List 接口常用方法中介绍过了,它可以直接使用Collection 接口和 List 接口中的常用方法。
ArrayList 类源码剖析
- ArrayList 集合动态扩容的源码分析:
(1)ArrayList 类中维护了一个 Object 类型的数组 elementData。
transient Object elementData ;// transient: 表示瞬间, 短暂的, 代表该属性不会被序列化
(2)当创建 ArrayList 对象时, 如果使用的是无参构造器, 则初始 elementData 容量为 0;当第 1 次添加元素时, 会动态地将集合容量 elementData 扩充为 10 , 如需再次扩容, 则默认扩容为当前 elementData 的1.5倍。
(3)如果使用的是指定容量大小的构造器, 则初始 elementData 容量为指定值, 如果再次需要扩容, 则直接扩充为当前 elementData 的1.5倍。
- 源码分析示意图:(很详细)
- 注意:上面源码的分析需要小伙伴自己去 debug,这样才能深入理解。
五、Vector 类(向量)
- Vector 类实现了 List 接口,底层也是一个动态的数组;
- Vector 类和 ArrayList 类非常相似,但 Vector 类是同步的,可以用在多线程的情况。
- Vector 类的带参构造器允许设置默认的集合长度,其默认扩容大小为原来容量的2 倍。
1. Vector 类源码剖析
- 源码分析如下:
import java.util.Vector;
public class Vector_ {
public static void main(String[] args) {
// 无参构造器
// Vector vector = new Vector();
// 有参数的构造器
Vector vector = new Vector(8);
for (int i = 0; i < 10; i++) {
vector.add(i);
}
vector.add(100);
System.out.println("vector=" + vector);
源码分析:
1. Vector vector = new Vector() : 调用vector 的无参构造器
// 无参构造器的底层还是调用了有参构造器,默认传入容量为 10
public Vector() {
this(10);
}
// Vector vector = new Vector(8) : 使用带参构造器
public Vector(int initialCapacity) {
this(initialCapacity, 0);
}
2. vector.add(i) : 调用 add() 方法添加元素;
2.1 // 下面这个方法添加数据到 vector集合
public synchronized boolean add(E e) {
modCount++;
ensureCapacityHelper(elementCount + 1);
elementData[elementCount++] = e;
return true;
}
2.2 // 确定是否需要扩容条件 : minCapacity - elementData.length > 0
private void ensureCapacityHelper(int minCapacity) {
// overflow-conscious code
if (minCapacity - elementData.length > 0)
grow(minCapacity);
}
2.3 // 如果需要的数组容量不够用,就扩容 , 扩容的算法:
newCapacity = oldCapacity + ((capacityIncrement > 0) ? capacityIncrement : oldCapacity);
// 其实就是扩容两倍
private void grow(int minCapacity) {
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);
}
}
}
2. Vector 和 ArrayList 类的比较
- 如下图:
六、LinkedList 类(链表)
- LinkedList 类实现了 List 接口,允许有null(空)元素。主要用于创建链表数据结构。
- LinkedList 类没有同步方法,如果多个线程同时访问一个 LinkedList 类,则必须自己实现访问同步,解决方法就是在创建 LinkedList 类 时候构造一个同步的 LinkedList 类。
- LinkedList 类底层实现了双向链表和双向队列的特点(数据结构部分的知识)。
1. LinkedList 类的常用方法
- 其实也是实现了 List 接口中的常用方法,只是底层的代码有区别,但最终呈现结果是一样的,了解即可。
import java.util.Iterator;
import java.util.LinkedList;
public class LinkedListCRUD {
public static void main(String[] args) {
LinkedList linkedList = new LinkedList();
// 1.增加结点
linkedList.add(1);
linkedList.add(2);
linkedList.add(3);
System.out.println("linkedList=" + linkedList);
// 2.删除结点
linkedList.remove(); // 这里默认删除的是第一个结点
linkedList.remove(2);// 删除指定位置的结点
System.out.println("linkedList=" + linkedList);
// 3.修改某个结点对象
linkedList.set(1, 999);
System.out.println("linkedList=" + linkedList);
// 4.得到某个结点对象
// get(1) 是得到双向链表的第二个对象
Object o = linkedList.get(1);
System.out.println(o);// 999
// 因为 LinkedList 实现了 List接口, 可以使用迭代器遍历
System.out.println("====LinkeList遍历迭代器====");
Iterator iterator = linkedList.iterator();
while (iterator.hasNext()) {
Object next = iterator.next();
System.out.println("next=" + next);
}
}
}
2. LinkedList 的底层操作机制
- 如下图:
- 代码模拟双向链表:
public class LinkedList01 {
public static void main(String[] args) {
// 模拟一个简单的双向链表
Node jack = new Node("jack");
Node tom = new Node("tom");
Node hsp = new Node("老韩");
// 连接三个结点,形成双向链表
// jack -> tom -> hsp
jack.next = tom;
tom.next = hsp;
// hsp -> tom -> jack
hsp.pre = tom;
tom.pre = jack;
Node first = jack;// 让 first 引用指向 jack,就是双向链表的头结点
Node last = hsp; // 让 last 引用指向 hsp,就是双向链表的尾结点
// 演示,从头到尾进行遍历
System.out.println("===从头到尾进行遍历===");
while (true) {
if(first == null) {
break;
}
// 输出 first 信息
System.out.println(first);
first = first.next;
}
// 演示,从尾到头的遍历
System.out.println("====从尾到头的遍历====");
while (true) {
if(last == null) {
break;
}
// 输出 last 信息
System.out.println(last);
last = last.pre;
}
// 演示链表的添加对象/数据,很方便
// 要求,在 tom 直接插入一个对象 smith
// 1. 先创建一个 Node 结点,name 就是 smith
Node smith = new Node("smith");
// 下面就把 smith 加入到双向链表了
smith.next = hsp;
smith.pre = tom;
hsp.pre = smith;
tom.next = smith;
// 让first 再次指向 jack
first = jack;// 让 first 引用指向 ack,就是双向链表的头结点
System.out.println("===从头到尾进行遍历===");
while (true) {
if(first == null) {
break;
}
// 输出 first 信息
System.out.println(first);
first = first.next;
}
last = hsp; // 让last 重新指向最后一个结点
// 演示,从尾到头的遍历
System.out.println("====从尾到头的遍历====");
while (true) {
if(last == null) {
break;
}
// 输出 last 信息
System.out.println(last);
last = last.pre;
}
}
}
// 定义一个Node 类,一个 Node 对象表示双向链表的一个结点
class Node {
public Object item; // 真正存放数据
public Node next; // 指向后一个结点
public Node pre; // 指向前一个结点
public Node(Object name) {
this.item = name;
}
public String toString() {
return "Node name=" + item;
}
}
-
在简单了解了双向链表的结构后,我们进一步分析 LinkedList 集合添加和删除元素的底层机制源码。
-
源码分析如下:
一、add 源码阅读:linkedList.add(1);
1. 使用无参构造器,创建一个集合:public LinkedList() {}
LinkedList linkedList = new LinkedList();
2. 这时 linkeList 的属性 first = null, last = null
3. 执行 add 方法
public boolean add(E e) {
linkLast(e);
return true;
}
4. 将新的结点,直接加入到双向链表的最后
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++;
}
二、删除源码阅读: linkedList.remove(); // 不传入参数则默认删除的是第一个结点
1. 执行 removeFirst 方法
public E remove() {
return removeFirst();
}
2. 执行 removeFirst()方法
public E removeFirst() {
final Node<E> f = first;
if (f == null)
throw new NoSuchElementException();
return unlinkFirst(f);
}
3. 执行 unlinkFirst 方法, 将 f 指向的双向链表的第一个结点拿掉
private E unlinkFirst(Node<E> f) {
// assert f == first && f != null;
final E element = f.item;
final Node<E> next = f.next;
f.item = null;
f.next = null; // help GC
first = next;
if (next == null)
last = null;
else
next.prev = null;
size--;
modCount++;
return element;
}
- 上面的源码小伙伴们要自己 debug 一遍,才能更深入地理解 LinkedList 集合的底层机制。
3. ArrayList 和 LinkedList 比较
- 如下图:
总结
- 本文是小白博主在学习B站韩顺平老师的Java网课时整理总结的学习笔记,在这里感谢韩顺平老师的网课,如有有兴趣的小伙伴也可以去看看。
- 本文详细介绍了 集合框架 的基本概念,深入讲解了 Collection 集合 的常用接口 List, 常用子类 ArrayList 、LinkedList、Vector 的细节和常用方法;并介绍了迭代器的使用,分析了各个子类底层的源码实现,举了很多例子讲解,希望小伙伴们看后能有所收获!
- 最后,如果本文有什么错漏的地方,欢迎大家批评指正!一起加油!!我们下一篇博文见吧!