文章目录
一、容器
当我们编程时,经常需要使用数据来存储和组织我们的信息。容器是一种用于存储和操作数据的数据结构。它们提供了一种方便的方式来组织和访问数据。
容器允许我们以不同的方式组织数据,例如列表、集合、映射等。每种容器都有其特定的特点和用途。
以下是一些常见的容器类型:
-
列表(List):列表是一个有序的元素集合,可以包含重复的元素。列表通常支持插入、删除和访问元素的操作。常见的列表实现包括数组列表(ArrayList)和链表(LinkedList)。
-
集合(Set):集合是一个不包含重复元素的无序集合。集合通常支持添加、删除和检查元素的操作。常见的集合实现包括哈希集(HashSet)和树集(TreeSet)。
-
映射(Map):映射是一种将键和值关联起来的容器。每个键都是唯一的,可以用来查找相应的值。常见的映射实现包括哈希映射(HashMap)和树映射(TreeMap)。
-
队列(Queue):队列是一种先进先出(FIFO)的容器,通常用于实现任务调度和消息传递等场景。常见的队列实现包括数组队列(ArrayDeque)和链表队列(LinkedList)。
-
栈(Stack):栈是一种后进先出(LIFO)的容器,通常用于实现算法的递归、表达式求值等场景。常见的栈实现包括数组栈(ArrayDeque)和链表栈(LinkedList)。
容器提供了丰富的方法和操作,使我们能够方便地对数据进行增删改查。在选择容器时,需要根据具体的需求和性能要求进行选择。
总结来说,容器是一种用于存储和组织数据的数据结构,提供了便捷的操作和访问方式。不同类型的容器适用于不同的场景,可以根据具体需求选择合适的容器来解决问题。
1.1 容器的结构
二、单例集合
2.1 Collection接口介绍
Collection 接口是 Java 集合框架中的一个顶级接口,它表示一组对象的集合。它是所有集合类的父接口,定义了集合类共有的方法和行为。Collection 接口继承自 Iterable 接口,因此可以使用迭代器遍历集合中的元素。
Collection 接口的主要特点如下:
- 无序性:集合中的元素没有固定的顺序,每次遍历的结果可能不同。
- 可重复性:集合中可以包含重复的元素,不会对重复元素进行额外处理。
- 大小可变:集合的大小可以根据需要进行动态调整。
- 简单操作:提供了添加、删除、判断包含等基本操作方法。
- 遍历功能:可以使用迭代器遍历集合中的元素。
Collection 接口有两个主要的子接口,分别是 List 接口和 Set 接口。List 接口表示有序可重复的集合,例如 ArrayList 和 LinkedList;Set 接口表示无序不可重复的集合,例如 HashSet 和 TreeSet。
需要注意的是,Collection 接口中的操作是针对单个元素进行的,并没有提供按索引访问和查找等操作。如果需要按索引操作,请使用 List 接口的实现类。
总结来说,Collection 接口是 Java 集合框架中所有集合类的父接口,定义了集合类的共有方法和行为。它具有无序性、可重复性和大小可变等特点,提供了简单的操作和遍历功能。通过 Collection 接口,我们可以方便地管理和操作一组对象。
2.2 Collection接口中定义的方法
2.3 List接口介绍
List 接口是 Java 集合框架中的一个子接口,它继承自 Collection 接口。List 表示有序的、可重复的集合,它可以按照插入顺序保存元素,并且允许包含重复元素。List 提供了按照索引访问和操作元素的功能。
List 接口的主要特点如下:
- 有序性:List 中的元素按照插入的顺序进行存储,每个元素都有一个对应的索引。
- 可重复性:List 允许包含重复的元素,相同的元素可以插入多次。
- 大小可变:List 的大小可以根据需要进行动态调整。
List 接口有很多实现类,常用的有 ArrayList 和 LinkedList。ArrayList 是基于数组实现的动态数组,它支持快速随机访问元素;LinkedList 是基于链表实现的双向链表,它对插入和删除操作具有较好的性能。
通过 List 接口,我们可以方便地按照索引访问和操作集合中的元素,可以进行元素的插入、删除、修改和查找等操作。如果需要保持元素的有序性并允许重复,可以选择使用 List 接口及其实现类来存储和操作数据。
总结来说,List 接口是 Java 集合框架中表示有序可重复集合的接口,它提供了按索引访问和操作元素的功能。List 允许包含重复元素,并且提供了丰富的方法来插入、删除、修改和查找元素。使用 List 接口及其实现类可以方便地管理和操作有序的数据集合。
2.4 List接口中定义的方法
2.5 ArrayList 容器介绍
ArrayList 是 Java 集合框架中 List 接口的一个实现类,它基于数组实现的动态数组。它提供了一种便捷的方式来存储和操作数据,可以根据需要动态调整大小。
ArrayList 的主要特点如下:
- 动态数组:ArrayList 内部使用数组来保存元素,可以根据需要自动扩展和收缩数组的大小。当元素个数超过当前数组容量时,会自动进行扩容操作。
- 有序性:ArrayList 保持元素的插入顺序,并且每个元素都有对应的索引,可以按照索引访问和操作元素。
- 允许重复元素:ArrayList 允许包含重复的元素,相同的元素可以插入多次。
- 随机访问:由于 ArrayList 是基于数组实现的,所以支持快速随机访问元素。可以通过索引直接访问任意位置的元素。
使用 ArrayList 时,需要注意以下几点:
- 容量管理:ArrayList 内部维护了一个数组来保存元素,当元素个数超过当前数组容量时,会自动进行扩容操作。扩容会导致创建新的数组并将旧元素复制到新数组中,因此频繁的插入和删除操作可能会影响性能,可以通过初始化时指定初始容量来提高性能。
- 插入和删除:ArrayList 在尾部插入元素的时间复杂度是 O(1),而在中间或开头插入元素的时间复杂度是 O(n),因为需要移动后续元素。类似地,删除操作也需要移动后续元素。因此,如果需要频繁进行插入和删除操作,可能会导致性能下降,可以考虑使用 LinkedList。
- 非线程安全:ArrayList 不是线程安全的,如果多个线程同时访问同一个 ArrayList 实例并进行修改,可能会导致数据不一致的问题。如果需要在多线程环境下使用,可以考虑使用线程安全的集合类,如 Vector 或 Collections 的 synchronizedList 方法包装。
总结来说,ArrayList 是一种动态数组实现的有序可重复集合,提供了快速随机访问元素的能力。它可以根据需要自动调整大小,但在频繁插入和删除时可能影响性能。ArrayList 不是线程安全的,适用于单线程环境下的数据存储和操作。
2.5.1 ArrayList容器的索引操作
import java.util.ArrayList;
public class ArrayListIndexOperation {
public static void main(String[] args) {
ArrayList<String> list = new ArrayList<>();
// 添加元素
list.add("Apple");
list.add("Banana");
list.add("Orange");
// 获取元素
String firstElement = list.get(0);
System.out.println("第一个元素:" + firstElement);
// 修改元素
list.set(1, "Grapes");
System.out.println("修改后的列表:" + list);
// 在指定位置插入元素
list.add(2, "Mango");
System.out.println("插入元素后的列表:" + list);
// 删除指定位置的元素
String removedElement = list.remove(0);
System.out.println("被删除的元素:" + removedElement);
System.out.println("删除元素后的列表:" + list);
}
}
2.5.2 ArrayList的并集、交集、差集
要计算两个ArrayList的并集、交集和差集,可以使用Java中的一些集合操作方法和工具类。以下是示例代码:
import java.util.ArrayList;
import java.util.List;
public class ArrayListSetOperations {
public static void main(String[] args) {
// 创建两个ArrayList
List<Integer> list1 = new ArrayList<>();
list1.add(1);
list1.add(2);
list1.add(3);
List<Integer> list2 = new ArrayList<>();
list2.add(2);
list2.add(3);
list2.add(4);
// 计算并集
List<Integer> union = new ArrayList<>(list1);
union.addAll(list2);
System.out.println("并集:" + union);
// 计算交集
List<Integer> intersection = new ArrayList<>(list1);
intersection.retainAll(list2);
System.out.println("交集:" + intersection);
// 计算差集
List<Integer> difference = new ArrayList<>(list1);
difference.removeAll(list2);
System.out.println("差集:" + difference);
}
}
- 并集:创建一个新的ArrayList
union
,将第一个ArrayList的所有元素添加到其中,然后再添加第二个ArrayList的所有元素。 - 交集:创建一个新的ArrayList
intersection
,将第一个ArrayList的所有元素添加到其中,然后使用retainAll()方法只保留第二个ArrayList中也包含的元素。 - 差集:创建一个新的ArrayList
difference
,将第一个ArrayList的所有元素添加到其中,然后使用removeAll()方法移除第二个ArrayList中包含的元素。
2.5.1 ArrayList源码分析
(1) 成员变量
/**
* DEFAULT_CAPACITY:这是一个常量,表示 ArrayList 的默认初始容量。当没有指定容量时,ArrayList 会使用这个默认值。
*/
private static final int DEFAULT_CAPACITY = 10;
/**
* EMPTY_ELEMENTDATA:这是一个空数组,用于在创建空的 ArrayList 时作为初始存储。
* 当向 ArrayList 添加第一个元素时,会创建一个初始容量为默认容量或者指定容量的数组。
*/
private static final Object[] EMPTY_ELEMENTDATA = {};
/**
* DEFAULTCAPACITY_EMPTY_ELEMENTDATA:这也是一个空数组,用于在创建空的 ArrayList 时作为初始存储。
* 与 EMPTY_ELEMENTDATA 不同的是,当指定了容量时,会创建一个具有指定容量的数组。
*/
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};
/**
* elementData:这是一个 Object 类型的数组,用于存储 ArrayList 中的元素。
* ArrayList 的大小就是此数组的长度。通过这个数组,ArrayList 实现了可变大小的功能。
*/
transient Object[] elementData; // non-private to simplify nested class access
/**
* size:这是一个 int 类型的变量,用于记录 ArrayList 中元素的个数。
* 它表示 ArrayList 当前的大小。
*/
private int size;
/**
* MAX_ARRAY_SIZE:这是一个常量,表示 ArrayList 允许的最大数组大小。
* 它的值为 Integer.MAX_VALUE - 8。
*/
private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;
这两个字段的作用是确保ArrayList对象在初始状态下具有不同的内部数组引用。
- EMPTY_ELEMENTDATA:
- 值为
{}
,表示一个空的Object数组。- 当创建一个空的ArrayList时,内部数组会使用这个空数组来初始化。
- 该数组不会被修改或重新分配内存,因此在没有元素时,多个空ArrayList实例可以共享同一个EMPTY_ELEMENTDATA数组实例,从而节省内存。
- DEFAULTCAPACITY_EMPTY_ELEMENTDATA:
- 值也为
{}
,表示一个默认大小的空的Object数组。- 当创建一个具有默认初始容量的空ArrayList时,内部数组会使用这个空数组来初始化。
- 与EMPTY_ELEMENTDATA相比,区别在于当第一个元素被添加到ArrayList时,ArrayList需要根据默认容量进行扩容,以容纳更多的元素。
- 此时,DEFAULTCAPACITY_EMPTY_ELEMENTDATA将不再使用,而是会分配一个具有默认容量的新数组,并将元素从EMPTY_ELEMENTDATA复制到新数组中。这样可以避免空ArrayList的初始容量过大。
总结起来,EMPTY_ELEMENTDATA和DEFAULTCAPACITY_EMPTY_ELEMENTDATA是为了节省内存以及优化空ArrayList的初始容量。它们用于初始化ArrayList的内部数组,并确保在不同情况下使用不同的引用。
(2) 添加元素
ArrayList 的底层源码中的添加元素操作是通过 add
方法实现的,主要涉及以下几个步骤:
-
检查是否需要扩容:在添加元素之前,首先会检查当前元素个数是否已经达到数组容量上限,如果是,则需要进行扩容操作。
-
扩容:如果需要扩容,会调用
ensureCapacityInternal
方法来进行扩容操作。扩容的逻辑是根据当前数组容量和待添加元素的数量来计算新的容量,并创建新的数组。然后将原数组中的元素复制到新数组中。 -
添加元素:在确定容量足够的情况下,将待添加的元素直接存储到数组的末尾,并更新
size
变量,表示元素个数加一。
下面是对ArrayList底层添加方法add(E e)
的代码进行详细注释:
public boolean add(E e) {
// 确保容量足够来容纳新元素
ensureCapacityInternal(size + 1);
// 将新元素添加到数组末尾,并增加ArrayList的大小
elementData[size++] = e;
// 添加成功,返回true
return true;
}
private void ensureCapacityInternal(int minCapacity) {
// 如果当前内部数组为空的话,使用默认容量
if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity);
}
// 确保容量足够来容纳元素
ensureExplicitCapacity(minCapacity);
}
private void ensureExplicitCapacity(int minCapacity) {
// 修改计数加1,用于在结构发生变化时提供快速失败机制
modCount++;
// 如果需要的最小容量大于当前数组长度,则进行扩容
if (minCapacity - elementData.length > 0)
grow(minCapacity);
}
private void grow(int minCapacity) {
// 保存旧的容量
int oldCapacity = elementData.length;
// 新的容量为旧容量的1.5倍(右移1位相当于除以2)
int newCapacity = oldCapacity + (oldCapacity >> 1);
// 如果新容量仍然不足以满足最小需求,则直接使用所需的最小容量
if (newCapacity - minCapacity < 0)
newCapacity = minCapacity;
// 检查新容量是否超过数组最大大小限制,如果超过则调用hugeCapacity()方法来获取更大的容量
if (newCapacity - MAX_ARRAY_SIZE > 0)
newCapacity = hugeCapacity(minCapacity);
// 使用Arrays.copyOf()方法创建一个新的、容量更大的数组,并将旧数组的元素复制到新数组中
elementData = Arrays.copyOf(elementData, newCapacity);
}
这段代码实现了向ArrayList中添加元素的逻辑。主要做的事情是:
-
ensureCapacityInternal()
方法根据需要添加的元素数量,确保内部数组的容量足够。- 如果当前内部数组为空,则使用默认容量。
- 然后调用
ensureExplicitCapacity()
方法进行进一步的容量检查。
-
ensureExplicitCapacity()
方法在确保容量足够的前提下,根据需要扩容。- 首先将修改计数加1,用于支持快速失败机制。
- 如果需要的最小容量大于当前数组长度,则调用
grow()
方法进行扩容。
-
grow()
方法根据旧的容量来计算新的容量,然后进行扩容操作。- 新的容量为旧容量的1.5倍(右移1位相当于除以2)。
- 如果新容量仍然不足以满足最小需求,则直接使用所需的最小容量。
- 如果新容量超过了ArrayList内部数组的最大大小限制,则调用
hugeCapacity()
方法来获取更大的容量。 - 最后,使用
Arrays.copyOf()
方法创建一个新的、容量更大的数组,并将旧数组的元素复制到新数组中。
通过这样的方式,ArrayList能够动态地扩容以适应不断增加的元素数量。
2.6 Vector容器介绍
在Java中,Vector是一种常用的容器类,它实现了动态数组(可自动调整大小)的功能。以下是Vector容器的一些介绍:
-
动态数组:Vector可以存储任意类型的对象,并且在内部以动态数组的形式保存这些对象。它的大小会根据需要自动增长或缩小。
-
线程安全:Vector是线程安全的,这意味着它可以在多线程环境下使用,而不会出现数据冲突或不一致的问题。在每个方法中,Vector都使用synchronized关键字进行同步。
-
兼容性:Vector是Java集合框架的一部分,因此它与其他集合类(如ArrayList和LinkedList)之间存在较高的兼容性。你可以使用类似的方法对Vector进行添加、删除、查找等操作。
-
排序功能:Vector提供了排序功能。你可以使用Collections类的sort()方法对Vector中的元素进行排序。这对于需要按照特定顺序访问元素的场景非常有用。
-
迭代功能:Vector实现了Iterable接口,因此你可以使用增强的for循环或迭代器来遍历Vector中的元素。
-
初始容量和增长因子:在创建Vector对象时,你可以指定初始容量和增长因子。初始容量表示Vector的初始大小,默认为10。当Vector已满时,增长因子决定了Vector的容量该如何增长,默认为当前容量的2倍。
总的来说,Vector是一个功能强大且线程安全的动态数组容器,它适用于多线程环境和需要频繁进行添加、删除等操作的场景。然而,由于它的线程安全特性可能带来一些性能上的开销,在单线程环境下可能更适合使用其他非线程安全的容器类。
2.6.1 Vector源码分析
(1) 成员变量
/**
* elementData:这是一个存储实际元素的数组,类型为Object[]。
* 它用于保存Vector中的元素。
*/
protected Object[] elementData;
/**
* elementCount:这是一个整数,表示当前Vector中的元素数量。
* 它始终保持与实际元素的数量一致。
*/
protected int elementCount;
/**
* capacityIncrement:这是一个整数,表示在需要增加Vector容量时,采用的增量值。
* 当Vector的容量不足以容纳新元素时,会自动增加capacityIncrement个单位的容量。
*/
protected int capacityIncrement;
/**
* serialVersionUID:这是一个长整型常量,用于在序列化和反序列化过程中确保版本一致性。
*/
private static final long serialVersionUID =-2767605614048989439L;
/**
* modCount:这是一个整数,表示对Vector的结构进行修改的次数。
* 用于支持迭代器的快速失败机制。
*/
protected transient int modCount = 0;
elementData
数组用于存储实际的元素
elementCount
记录元素数量
capacityIncrement
决定容量增长的方式
modCount
用于支持快速失败机制
serialVersionUID
确保序列化的兼容性
(2) 构造方法
/**
* Vector():无参数构造方法,创建一个初始容量为10的空Vector对象。
*/
public Vector() {
this(10);
}
/**
* Vector(int initialCapacity):创建一个具有指定初始容量的空Vector对象。
* 初始容量表示Vector的初始大小。
*/
public Vector(int initialCapacity) {
this(initialCapacity, 0);
}
/**
* Vector(int initialCapacity, int capacityIncrement):创建一个具有指定初始容量和增长因子的空Vector对象。
* 初始容量表示Vector的初始大小,而增长因子决定了Vector的容量增长方式。
*/
public Vector(int initialCapacity, int capacityIncrement) {
super();
if (initialCapacity < 0)
throw new IllegalArgumentException("Illegal Capacity: "+initialCapacity);
this.elementData = new Object[initialCapacity];
this.capacityIncrement = capacityIncrement;
}
/**
* Vector(Collection<? extends E> c):创建一个包含指定集合所有元素的Vector对象。
*/
public Vector(Collection<? extends E> c) {
elementData = c.toArray();
elementCount = elementData.length;
if (elementData.getClass() != Object[].class)
elementData = Arrays.copyOf(elementData, elementCount, Object[].class);
}
(3)添加元素
/**
* addElement(E obj):该方法用于在Vector的末尾添加一个元素。
* 它先对Vector进行容量检查,如果当前容量不足以容纳新元素,则通过ensureCapacityInternal()方法增加容量。
* 然后将元素添加到elementData数组的末尾,并更新elementCount和modCount。
*/
public synchronized boolean add(E e) {
modCount++;
ensureCapacityHelper(elementCount + 1);
elementData[elementCount++] = e;
return true;
}
注意: 在添加元素时,如果多个线程同时修改Vector,可能会导致数据不一致或并发修改异常。为了保证线程安全,Vector中的添加元素操作会使用synchronized关键字进行同步。
(4)数组扩容
/**
* ensureCapacityHelper()方法根据需要的最小容量,确保Vector的容量足够。
*/
private void ensureCapacityHelper(int minCapacity) {
// 如果需要的最小容量大于当前数组长度,则进行扩容
if (minCapacity - elementData.length > 0)
grow(minCapacity);
}
/**
* grow()方法根据旧的容量和增量(capacityIncrement)来计算新的容量,然后进行扩容操作
*/
private void grow(int minCapacity) {
// 保存旧的容量
int oldCapacity = elementData.length;
// 根据需要扩容的最小容量计算新的容量
int newCapacity = oldCapacity + ((capacityIncrement > 0) ? capacityIncrement : oldCapacity);
// 如果新容量仍然不足以满足最小需求,则直接使用所需的最小容量
if (newCapacity - minCapacity < 0)
newCapacity = minCapacity;
// 检查新容量是否超过数组最大大小限制,如果超过则调用hugeCapacity()方法来获取更大的容量
if (newCapacity - MAX_ARRAY_SIZE > 0)
newCapacity = hugeCapacity(minCapacity);
// 使用Arrays.copyOf()方法创建一个新的、容量更大的数组,并将旧数组的元素复制到新数组中
elementData = Arrays.copyOf(elementData, newCapacity);
}
/**
* hugeCapacity()方法用于判断最小容量是否溢出,并返回合适的容量大小。
*/
private static int hugeCapacity(int minCapacity) {
// 判断最小容量是否小于零,如果小于零表示溢出了
if (minCapacity < 0) // 溢出
throw new OutOfMemoryError();
// 根据最小容量的大小来返回合适的容量大小
return (minCapacity > MAX_ARRAY_SIZE) ? Integer.MAX_VALUE : MAX_ARRAY_SIZE;
}
2.7 LinkedList容器介绍
LinkedList(链表)是一种常用的数据结构,它可以用来实现线性表。它通过节点之间的指针来连接数据,相比于数组,链表的插入和删除操作更加高效。
在LinkedList中,每个节点都包含了一个数据元素和一个指向下一个节点的指针。首节点表示链表的开头,尾节点表示链表的结束,尾节点的指针通常指向一个空值(null)。
LinkedList具有以下特点:
- 动态性:相比于数组,在插入和删除元素时,LinkedList不需要移动其他元素,只需修改指针的指向。这使得链表的插入和删除操作的时间复杂度为O(1)。
- 不连续性:链表中的节点可以存储在内存的任意位置,它们之间的物理顺序并不一定与逻辑顺序相同。这意味着插入和删除操作不受内存的限制,可以非常灵活。
- 相对较低的存储效率:由于每个节点都需要存储额外的指针信息,链表相对于数组来说需要更多的存储空间。
LinkedList的存储结构图
每个节点都有三个内容
class Node<E> {
Node<E> previous; //前一个节点
E element; //本节点保存的数据
Node<E> next; //后一个节点
}
List实现类的选用规则
- 需要线程安全时,用Vector。
- 不存在线程安全问题时,并且查找较多用ArrayList(一般使用它)
- 不存在线程安全问题时,增加或删除元素较多用LinkedList
2.7.1 LinkedList容器的使用(List标准)
LinkedList实现了List接口,所以LinkedList是具备List的存储特征的(有序,元素有重复)。
public class LinkedListTest {
public static void main(String[] args) {
//实例化LinkedList容器
List<String> list = new LinkedList<>
();
//添加元素
boolean a = list.add("a");
boolean b = list.add("b");
boolean c = list.add("c");
list.add(3,"a");
System.out.println(a+"\t"+b+"\t"+c);
for(int i=0;i<list.size();i++){
System.out.println(list.get(i));
}
}
}
2.7.2 LinkedList容器的使用(非List标准)
示例:
public class LinkedListTest2 {
public static void main(String[] args) {
System.out.println("-------LinkedList-------------");
//将指定元素插入到链表开头
LinkedList<String> linkedList1 = new LinkedList<>();
linkedList1.addFirst("a");
linkedList1.addFirst("b");
linkedList1.addFirst("c");
for (String str:linkedList1){
System.out.println(str);
}
System.out.println("----------------------");
//将指定元素插入到链表结尾
LinkedList<String> linkedList = new LinkedList<>();
linkedList.addLast("a");
linkedList.addLast("b");
linkedList.addLast("c");
for (String str:linkedList){
System.out.println(str);
}
System.out.println("---------------------------");
//返回此链表的第一个元素
System.out.println(linkedList.getFirst());
//返回此链表的最后一个元素
System.out.println(linkedList.getLast());
System.out.println("-----------------------");
//移除此链表中的第一个元素,并返回这个元素
linkedList.removeFirst();
//移除此链表中的最后一个元素,并返回这个元素
linkedList.removeLast();
for (String str:linkedList){
System.out.println(str);
}
System.out.println("-----------------------");
linkedList.addLast("c");
//从此链表所表示的堆栈处弹出一个元素,等效于removeFirst
linkedList.pop();
for (String str:linkedList){
System.out.println(str);
}
System.out.println("-------------------");
//将元素推入此链表所表示的堆栈 这个等效于addFisrt(E e)
linkedList.push("h");
for (String str:linkedList){
System.out.println(str);
}
}
}
2.7.3 LinkedList的源码分析
(1)静态内部节点类
private static class Node<E> {
E item;
Node<E> next;
Node<E> prev;
Node(Node<E> prev, E element, Node<E> next) {
this.item = element;
this.next = next;
this.prev = prev;
}
}
(2)成员变量
/**
* 表示LinkedList中存储的元素个数。
* 用transient关键字修饰,表示在序列化过程中会忽略该变量。
*/
transient int size = 0;
/**
* 表示链表的首节点。
*/
transient Node<E> first;
/**
* 表示链表的尾节点。
*/
transient Node<E> last;
(3)添加元素
LinkedList的添加元素操作涉及到了多个方法,其中最常用的是add()
、addFirst()
和addLast()
方法。下面对这些方法进行源码分析:
add(E e)
方法:将指定的元素添加到链表的末尾。
public boolean add(E e) {
linkLast(e);
return true;
}
在该方法中,调用了linkLast()
方法将元素添加到链表的末尾。
addFirst(E e)
方法:将指定的元素插入到链表的开头。
public void addFirst(E e) {
linkFirst(e);
}
该方法直接调用了linkFirst()
方法将元素插入到链表开始的位置。
addLast(E e)
方法:将指定的元素添加到链表的末尾,与add(E e)
方法功能相同。
public void addLast(E e) {
linkLast(e);
}
该方法也是调用了linkLast()
方法来实现添加元素的操作。
下面是linkFirst(E e)
和linkLast(E e)
方法的源码:
private void linkFirst(E e) {
final Node<E> f = first;
final Node<E> newNode = new Node<>(null, e, f);
first = newNode;
if (f == null)
last = newNode;
else
f.prev = newNode;
size++;
modCount++;
}
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++;
}
这两个方法的实现逻辑相似,都是创建一个新节点,并将其连接到链表中。具体来说:
-
linkFirst(E e)
方法会将新节点添加到链表的开头。它首先将旧的首节点赋值给临时变量f,然后创建一个新节点newNode
,其中尾指针指向f,首指针指向null。接着,将新节点设为首节点,并更新旧的首节点的prev指针,最后更新链表大小和修改次数。 -
linkLast(E e)
方法将新节点添加到链表的末尾。它首先将旧的尾节点赋值给临时变量l,然后创建一个新节点newNode
,其中头指针指向l,尾指针指向null。接着,将新节点设为尾节点,并更新旧的尾节点的next指针,最后更新链表大小和修改次数。
需要注意的是,这些方法涉及到对首节点、尾节点以及新创建的节点的指针操作,以确保链表的正确连接和维护。
(4)获取元素
LinkedList提供了多种方法来获取元素,包括根据索引获取元素、获取首尾元素等。下面分别分析这些方法的源码实现:
get(int index)
方法:根据索引获取指定位置的元素。
public E get(int index) {
checkElementIndex(index);
return node(index).item;
}
private void checkElementIndex(int index) {
if (!isElementIndex(index))
throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
}
在该方法中,首先调用checkElementIndex(int index)
方法对索引进行检查。接着调用node(int index)
方法获取指定索引位置的节点,然后返回该节点的数据元素。
getFirst()
方法:获取链表的首元素。
public E getFirst() {
final Node<E> f = first;
if (f == null)
throw new NoSuchElementException();
return f.item;
}
该方法首先将首节点赋值给临时变量f,然后检查f是否为null。如果f为null,表示链表为空,会抛出NoSuchElementException异常;否则,返回f节点的数据元素。
getLast()
方法:获取链表的尾元素。
public E getLast() {
final Node<E> l = last;
if (l == null)
throw new NoSuchElementException();
return l.item;
}
该方法与getFirst()
方法类似,只是将尾节点赋值给临时变量l,并返回l节点的数据元素。
node(int index)
方法是一个私有方法,用于获取指定索引位置的节点。
private Node<E> node(int index) {
// 如果索引小于链表大小的一半,则从首节点开始向后遍历
if (index < (size >> 1)) {
Node<E> x = first;
for (int i = 0; i < index; i++)
x = x.next;
return x;
} else { // 否则,从尾节点开始向前遍历
Node<E> x = last;
for (int i = size - 1; i > index; i--)
x = x.prev;
return x;
}
}
在该方法中,首先根据索引位置与链表大小的一半进行比较,以确定从哪个方向遍历节点。如果索引小于链表大小的一半,则从首节点开始向后遍历;否则,从尾节点开始向前遍历。最后返回找到的节点。
通过调用这些方法,可以根据需要获取LinkedList中的元素,无论是根据索引位置、获取头部元素还是获取尾部元素,都有相应的方法提供。源码中对节点的遍历和指针操作保证了获取元素的正确性和效率。
2.8 Set接口介绍
Set接口是Java编程语言中的一个接口,它继承自Collection接口。Set接口用于存储一组不重复的元素,它不能包含重复的元素。Set接口的实现类常用的有HashSet、LinkedHashSet和TreeSet。
Set接口与List接口不同,它没有提供根据索引访问元素的方法,因为Set中的元素是无序且不重复的。
Set接口的实现类根据不同的需求和性能特点有所区别:
- HashSet:基于哈希表实现,具有较快的插入和查找速度,不保证元素的顺序。
- LinkedHashSet:基于哈希表和链表实现,可以按照元素的插入顺序进行遍历。
- TreeSet:基于红黑树实现,可以对元素进行排序,具有较快的查找速度。
Set接口在实际开发中常用于去重操作,或者需要保持元素不重复、无需索引访问的场景。
2.8.1 HashSet存储特征分析
HashSet是Set接口的一个实现类,它基于哈希表实现。下面是HashSet存储的特征分析:
-
唯一性:HashSet中不允许存在重复的元素。当我们向HashSet中添加元素时,会先计算元素的哈希码(通过hashCode()方法),然后根据哈希码确定元素在内部数据结构中的位置。如果两个元素的哈希码相同,还需要通过equals()方法来确保元素的唯一性。
-
无序性:HashSet中元素的存储顺序是不确定的,因为它是基于哈希表实现的。哈希表使用数组和链表/红黑树的结合来存储元素,元素的存放位置是根据哈希码决定的,而哈希码并不能反映元素的逻辑顺序。
-
查询速度快:HashSet中的元素是根据哈希码存储的,而不是按照元素的插入顺序。这样可以实现快速的查找操作。当我们需要判断一个元素是否存在于HashSet中时,HashSet会根据元素的哈希码在指定位置进行查找,而不需要遍历所有元素。
-
高效的插入和删除:由于HashSet使用哈希表实现,插入和删除元素的速度较快。当我们向HashSet中添加元素时,HashSet会根据元素的哈希码确定插入位置,这样可以实现常数时间的插入操作。同样,删除元素也是通过哈希码进行定位和删除的,也可以达到较快的速度。
需要注意的是,HashSet对于存储的元素没有提供排序功能。如果需要按照元素的顺序进行遍历或排序,可以考虑使用LinkedHashSet或TreeSet。
2.8.2 在HashSet中存储Users对象
在HashSet中存储Users
对象,需要确保Users
类正确实现了hashCode()
和equals()
方法。这是因为HashSet在判断元素唯一性时会使用这两个方法。
hashCode()
方法用于计算对象的哈希码,不同的对象应该返回不同的哈希码。而equals()
方法用于比较两个对象是否相等。
以下是一个示例的Users
类,演示如何正确实现hashCode()
和equals()
方法:
public class Users {
private int id;
private String name;
// 构造方法和其他成员方法省略
// 重写hashCode()方法
@Override
public int hashCode() {
final int prime = 31;
int result = 1;
result = prime * result + id;
result = prime * result + ((name == null) ? 0 : name.hashCode());
return result;
}
// 重写equals()方法
@Override
public boolean equals(Object obj) {
if (this == obj)
return true;
if (obj == null || getClass() != obj.getClass())
return false;
Users other = (Users) obj;
if (id != other.id)
return false;
if (name == null) {
if (other.name != null)
return false;
} else if (!name.equals(other.name))
return false;
return true;
}
}
在使用HashSet存储Users
对象时,可以按照以下方式进行操作:
HashSet<Users> usersSet = new HashSet<>();
Users user1 = new Users(1, "Alice");
Users user2 = new Users(2, "Bob");
Users user3 = new Users(1, "Alice"); // 与user1相同的对象
// 添加对象到HashSet
usersSet.add(user1);
usersSet.add(user2);
usersSet.add(user3); // 由于hashCode和equals方法的重写,HashSet会判断它与user1是相同的对象,不会重复添加
// 遍历HashSet中的Users对象
for (Users user : usersSet) {
System.out.println(user.getId() + ": " + user.getName());
}
通过正确实现hashCode()
和equals()
方法,可以确保在HashSet存储Users
对象时,根据对象的属性进行唯一性判断。
2.8.3 HashSet底层源码分析
(1)成员变量
/**
* 该变量是HashSet的核心成员变量,它是一个HashMap实例,用于存储HashSet中的元素。
* HashMap的键是HashSet中的元素,值是一个固定的Object对象(用作所有元素的值)。
* 该变量被声明为transient,表示在对象被序列化时,map对象不会被序列化。
*/
private transient HashMap<E, Object> map;
/**
* PRESENT是一个静态常量,用作HashMap中的值。
* 它是一个Object对象,其作用是作为HashMap中的value,用于表示HashSet中的每个元素。
* 使用这样的方式可以节省内存空间。
*/
private static final Object PRESENT = new Object();
(2)添加元素
实际上是调用了底层HashMap的put()
方法来添加元素的,具体的源码如下所示:
public boolean add(E e) {
return map.put(e, PRESENT) == null;
}
在这段代码中,map
表示HashSet内部使用的底层HashMap对象,e
表示待添加的元素,PRESENT
是一个私有的静态常量对象。下面是对代码的分析:
map.put(e, PRESENT)
: 这一步调用底层HashMap的put()
方法将元素e
作为键,PRESENT
作为值进行插入操作。== null
:put()
方法返回之前关联该键的值,如果之前该键不存在则返回null
。return map.put(e, PRESENT) == null;
: 如果之前不存在此键(即之前HashSet中没有相同的元素),则插入成功,返回true
;如果之前已经存在此键(即元素重复),则插入失败,返回false
。
通过底层HashMap的特性,HashSet能够利用HashMap的键唯一性来保证集合中不会存储重复的元素。HashSet的底层实现依赖于HashMap,并且元素的插入操作实际上是通过调用HashMap的put()
方法来实现的。
2.8.4 LinkedHashSet容器介绍
- LinkedHashSet 是 HashSet 的子类
- LinkedHashSet 根据元素的 hashCode 值来决定元素的存储位置,但它同时使用双向链表维护元素的次序,这使得元素看起来是以插入顺序保存的。
- LinkedHashSet插入性能略低于 HashSet, 但在迭代访问 Set 里的全部元素时有很好的性能。
- LinkedHashSet 不允许集合元素重复。
- LinkedHashSet 底层结构
public class LinkedHashSetDemo1 {
/**
* LinkedHashSet特点
* - 不能重复
* - 有序(跟插入顺序有关)
* - 双向链表
* @param args
*/
public static void main(String[] args) {
LinkedHashSet set = new LinkedHashSet();
set.add("zzz");
set.add("zzz");
set.add("zzz");
set.add("yyy");
set.add("yyy");
set.add("xxx");
set.add("xxx");
set.add("fff");
set.add("eee");
set.add("www");
set.add("111");
set.add("222");
set.add("333");
set.add("666");
set.add("555");
set.add("444");
for (Object o : set) {
System.out.println(o);
}
}
}
2.8 TreeSet容器介绍
TreeSet 是 Java 集合框架中的一个实现类,它基于红黑树数据结构来存储和管理元素。TreeSet 是一个有序的集合,它默认按照元素的自然顺序进行排序,或者通过提供的比较器来进行自定义排序。
TreeSet 的特点如下:
- 有序性:TreeSet 中的元素按照升序排列。对于基本数据类型,按照比较大小的规则排序;对于自定义对象,可以通过重写对象的 compareTo 方法或者提供一个 Comparator 对象来指定排序规则。
- 唯一性:TreeSet 中不允许存在重复的元素。当插入新元素时,TreeSet 会根据元素的值判断是否已经存在相同的元素。
- 动态性:TreeSet 是动态增长和收缩的,可以根据需要自动调整内部存储容量。
- 快速访问:TreeSet 支持快速的插入、删除和查找操作,平均时间复杂度为 O(log n)。
- 新增的方法如下: (了解)
- Comparator comparator():返回定制排序器
- Object first():返回此集合中当前的第一个(最低)元素。
- Object last():返回此集合中当前的最后一个(最高)元素。
- Object lower(Object e):返回此集合中的最大元素严格小于给定元素,如果没有这样的元素,则 null 。
- Object higher(Object e)
- SortedSet subSet(fromElement, toElement)
- SortedSet headSet(toElement)
- SortedSet tailSet(fromElement)
使用 TreeSet 需要注意以下几点:
- TreeSet 中的元素必须实现 Comparable 接口或者通过 Comparator 来指定比较规则。
- 自定义对象在排序时需要谨慎处理,保证排序的稳定性和一致性。
- TreeSet 不是线程安全的,如果需要在多线程环境中使用,需要进行适当的同步处理。
排序规则实现方式:
- 通过元素自身实现比较规则。
- 通过比较器指定比较规则。
2.8.1 通过元素自身实现比较规则
要通过元素自身实现比较规则,需要让元素类实现 Comparable
接口,并重写其中的 compareTo
方法。在 compareTo
方法中定义元素之间的比较逻辑,返回一个负整数、零或正整数,表示当前对象小于、等于或大于比较对象。
以下是一个示例:
import java.util.TreeSet;
class Person implements Comparable<Person> {
private String name;
private int age;
public Person(String name, int age) {
this.name = name;
this.age = age;
}
// 实现 compareTo 方法
@Override
public int compareTo(Person other) {
// 比较年龄
return Integer.compare(this.age, other.age);
}
@Override
public String toString() {
return name + " - " + age;
}
}
public class TreeSetExample {
public static void main(String[] args) {
TreeSet<Person> people = new TreeSet<>();
// 添加元素
people.add(new Person("Alice", 25));
people.add(new Person("Bob", 30));
people.add(new Person("Charlie", 20));
// 遍历输出
for (Person person : people) {
System.out.println(person);
}
}
}
在上述示例中,创建了一个 Person
类来表示人员信息,实现了 Comparable<Person>
接口,并重写了 compareTo
方法,按照年龄来比较人员对象。然后在 TreeSet
中添加了几个 Person
对象,并通过遍历输出,结果将按照年龄的升序排列。
2.8.2 通过比较器实现比较规则
通过比较器(Comparator)来实现比较规则,可以为已有的类创建一个单独的比较器对象,并传递给 TreeSet 的构造方法或使用 TreeSet 的带有比较器参数的 add 方法。
以下是一个示例:
import java.util.Comparator;
import java.util.TreeSet;
class Person {
private String name;
private int age;
public Person(String name, int age) {
this.name = name;
this.age = age;
}
@Override
public String toString() {
return name + " - " + age;
}
}
//创建比较器
class AgeComparator implements Comparator<Person> {
@Override
public int compare(Person person1, Person person2) {
// 比较年龄
return Integer.compare(person1.getAge(), person2.getAge());
}
}
public class TreeSetExample {
public static void main(String[] args) {
TreeSet<Person> people = new TreeSet<>(new AgeComparator());
// 添加元素
people.add(new Person("Alice", 25));
people.add(new Person("Bob", 30));
people.add(new Person("Charlie", 20));
// 遍历输出
for (Person person : people) {
System.out.println(person);
}
}
}
在上述示例中,定义了一个 Person
类来表示人员信息,没有实现 Comparable
接口。然后创建了一个 AgeComparator
类作为比较器,在其中实现了 compare
方法来比较两个人员对象的年龄。最后,在创建 TreeSet
对象时,将 AgeComparator
对象传递给构造方法,或者使用带有比较器参数的 add
方法来添加元素。
2.8.3 TreeSet底层源码分析
(1)成员变量
/**
* The backing map.
*/
private transient NavigableMap<E,Object> m;
// Dummy value to associate with an Object in the backing Map
private static final Object PRESENT = new Object();
-
TreeSet 使用一个成员变量 m 来存储数据,类型为 NavigableMap,而 NavigableMap 是一个接口,TreeSet 内部使用的是 TreeMap 的实例来实现 NavigableMap 接口。
-
TreeSet 是通过调用 TreeMap 来实现的,因此底层的源码分析需要参考 TreeMap。
(2)构造方法
public TreeSet() {
this(new TreeMap<E,Object>());
}
-
在 TreeSet 的默认构造方法中,调用了另一个构造方法 TreeSet(SortedSet s),传入了一个新创建的 TreeMap 对象作为参数。
-
TreeSet 的构造方法中使用的是 new TreeMap() 来创建一个 TreeMap 对象:
(3)添加元素
public boolean add(E e) {
return m.put(e, PRESENT)==null;
}
TreeSet 的底层源码中,添加元素是通过调用 TreeMap 的 put 方法实现的。
三、双例集合
3.1 Map接口介绍
Map接口定义了双例集合的存储特征,它并不是Collection接口的子接口。双例集合的存储特征是以key与value结构为单位进行存储。体现的是数学中的函数 y=f(x)感念。
Map 集合是以 Key-Value 键值对作为存储元素实现的晗希结构, Key 按某种晗希函数计算后是唯一 的, Value 则是可以重复的。 Map 类提供三种 Collection 视图,在集合框架图中, Map 指向 Collection 的箭头仅表示两个类之间的依赖关系 。 可以使用keySet()查看所有的 K町,使用 values()查看所有的 Value ,使用 ent可Set()查看所有的键值对。最早用于存储键值对的 Hashtable 因为性能瓶颈已经被淘汰,而如今广泛使用的 HashMap , 线程是不安全的。 ConcurrentHashMap 是线程安全的,在 JDK8 中进行了锁的大幅度优化,体现出不错的性能。在多线程并发场景中,优先推荐使用ConcurrentHashMap ,而不是 HashMap 。 TreeMap 是 Key 有序的 Map 类集合。
Map与Collecton的区别:
- Collection中的容器,元素是孤立存在的(理解为单身),向集 合中存储元素采用一个个元素的方式存储。
- Map中的容器,元素是成对存在的(理解为现代社会的夫妻)。每 个元素由键与值两部分组成,通过键可以找对所对应的值。
- Collection中的容器称为单列集合,Map中的容器称为双列集 合。
- Map中的集合不能包含重复的键,值可以重复;每个键只能对应一个值。
- Map中常用的容器为HashMap,TreeMap等。
Map接口中常用的方法表
方法 | 说明 |
---|---|
方法 | 说明 |
V put (K key,V value) | 把key与value添加到Map集合中 |
void putAll(Map m) | 从指定Map中将所有映射关系复制到此Map中 |
V remove (Object key) | 删除key对应的value |
V get(Object key) | 根据指定的key,获取对应的value |
boolean containsKey(Object key) | 判断容器中是否包含指定的key |
boolean containsValue(Object value) | 判断容器中是否包含指定的value |
Set keySet() | 获取Map集合中所有的key,存储到Set集合中 |
Set<Map.Entry<K,V>> entrySet() | 返回一个Set基于Map.Entry类型包含Map中所有映射。 |
void clear() | 删除Map中所有的映射 |
3.2 HashMap容器介绍
- 简介
- 允许使用null键和null值,与HashSet一样,不保证映射的顺序。
- 所有的key构成的集合是Set:无序的、不可重复的。所以, key所在的类要重写:equals()和hashCode()
- 所有的value构成的集合是Collection:无序的、可以重复的。所以, value所在的类要重写: equals()
- 一个key-value构成一个entry
- 所有的entry构成的集合是Set:无序的、不可重复的
- HashMap 判断两个 key 相等的标准是:两个 key 通过 equals() 方法返回 true,hashCode 值也相等。
- HashMap 判断两个 value相等的标准是:两个 value 通过 equals() 方法返回 true。
- JDK1.7前存储结构
JDK 7及以前版本: HashMap是数组+链表结构(即为链地址法)
- HashMap的内部存储结构其实是数组和链表的结合。 当实例化一个HashMap时,系统会创建一个长度为Capacity的Entry数组, 这个长度在哈希表中被称为容量(Capacity), 在这个数组中可以存放元素的位置我们称之为“桶” (bucket), 每个bucket都有自己的索引, 系统可以根据索引快速的查找bucket中的元素。
- 每个bucket中存储一个元素, 即一个Entry对象, 但每一个Entry对象可以带一个引用变量, 用于指向下一个元素, 因此, 在一个桶中, 就有可能生成一个Entry链。而且新添加的元素作为链表的head。
- 添加元素的过程:向HashMap中添加entry1(key, value), 需要首先计算entry1中key的哈希值(根据key所在类的hashCode()计算得到), 此哈希值经过处理以后, 得到在底层Entry[]数组中要存储的位置i。 如果位置i上没有元素, 则entry1直接添加成功。 如果位置i上已经存在entry2(或还有链表存在的entry3, entry4), 则需要通过循环的方法, 依次比较entry1中key和其他的entry。 如果彼此hash值不同, 则直接添加成功。 如果hash值相同, 继续比较二者是否equals。 如果返回值为true, 则使用entry1的value去替换equals为true的entry的value。 如果遍历一遍以后, 发现所有的equals返回都为false,则entry1仍可添加成功。 entry1指向原有的entry元素。
- HashMap的扩容: 当HashMap中的元素越来越多的时候, hash冲突的几率也就越来越高, 因为数组的长度是固定的。 所以为了提高查询的效率, 就要对HashMap的数组进行扩容, 而在HashMap数组扩容之后, 最消耗性能的点就出现了:原数组中的数据必须重新计算其在新数组中的位置, 并放进去, 这就是resize。
- 那么HashMap什么时候进行扩容呢? 当HashMap中的元素个数超过数组大小(数组总大小length,不是数组中个数size)loadFactor 时 , 就 会 进 行 数 组 扩 容 , loadFactor 的 默 认 值(DEFAULT_LOAD_FACTOR)为0.75, 这是一个折中的取值。 也就是说, 默认情况下, 数组大小(DEFAULT_INITIAL_CAPACITY)为16, 那么当HashMap中元素个数超过160.75=12(这个值就是代码中的threshold值, 也叫做临界值) 的时候, 就把数组的大小扩展为 2*16=32, 即扩大一倍, 然后重新计算每个元素在数组中的位置,而这是一个非常消耗性能的操作, 所以如果我们已经预知HashMap中元素的个数,那么预设元素的个数能够有效的提高HashMap的性能。
- JDK 8存储结构
JDK 8版本发布以后: HashMap是数组+链表+红黑树实现。
- HashMap的内部存储结构其实是数组+链表+树的结合。 当实例化一个HashMap时, 会初始化initialCapacity和loadFactor, 在put第一对映射关系时, 系统会创建一个长度为initialCapacity的Node数组, 这个长度在哈希表中被称为容量(Capacity), 在这个数组中可以存放元素的位置我们称之为“桶” (bucket), 每个bucket都有自己的索引, 系统可以根据索引快速的查找bucket中的元素。
- 每个bucket中存储一个元素, 即一个Node对象, 但每一个Node对象可以带一个引用变量next, 用于指向下一个元素, 因此, 在一个桶中, 就有可能生成一个Node链。 也可能是一个一个TreeNode对象, 每一个TreeNode对象可以有两个叶子结点left和right, 因此, 在一个桶中, 就有可能生成一个TreeNode树。 而新添加的元素作为链表的last, 或树的叶子结点。
- HashMap扩容: 当HashMap中的元素个数超过数组大小(数组总大小length,不是数组中个数size)loadFactor 时 , 就 会 进 行 数 组 扩 容 , loadFactor 的 默 认 值(DEFAULT_LOAD_FACTOR)为0.75, 这是一个折中的取值。 也就是说, 默认情况下, 数组大小(DEFAULT_INITIAL_CAPACITY)为16, 那么当HashMap中元素个数超过160.75=12(这个值就是代码中的threshold值, 也叫做临界值)的时候, 就把数组的大小扩展为 2*16=32, 即扩大一倍, 然后重新计算每个元素在数组中的位置, 而这是一个非常消耗性能的操作, 所以如果我们已经预知HashMap中元素的个数, 那么预设元素的个数能够有效的提高HashMap的性能。
- HashMap树化和链化: 当HashMap中的其中一个链的对象个数如果达到了8个,此时如果capacity没有达到64,那么HashMap会先扩容解决,如果已经达到了64,那么这个链会变成树,结点类型由Node变成TreeNode类型。当然,如果当映射关系被移除后,下次resize方法时判断树的结点个数低于6个,也会把树再转为链表。
- 扩容和树化举个例子:
- 初始情况下,hashmap 的capacity为16,因子为0.75。
- 当hashmap桶内元素小于等于8,且size小于12时,不进行扩容和树化的操作。
- 当hashmap桶内元素为9,因为capacity为16,因此不进行树化,而选择扩容,将capacity扩容为32。
- 当hashmap桶内元素为10,因为capacity为32,因此不进行树化,而选择扩容,将capacity扩容为64。
- 当hashmap桶内元素大于10,由于capacity已经达到64,此时进行树化。
- 最后当HashMap中元素个数超过48(64*0.75=48),进行扩容
- JDK1.8HashMap新变化
- HashMap map = new HashMap();//默认情况下,先不创建长度为16的数组
- 当首次调用map.put()时,再创建长度为16的数组
- 数组为Node类型,在jdk7中称为Entry类型
- 形成链表结构时,新添加的key-value对在链表的尾部(七上八下)
- 当数组指定索引位置的链表长度>8时,且map中的数组的长度> 64时,此索引位置上的所有key-value对使用红黑树进行存储。
HashMap采用哈希算法实现,是Map接口最常用的实现类。 由于底层采用了哈希表存储数据,我们要求键不能重复,如果发生重复,新的键值对会替换旧的键值对。 HashMap在查找、删除、修改方面都有非常高的效率。
HashTable类和HashMap用法几乎一样,底层实现几乎一样,只不过HashTable的方法添加了synchronized关键字确保线程同步检查,效率较低。
HashMap与HashTable的区别
- HashMap: 线程不安全,效率高。允许key或value为null
- HashTable: 线程安全,效率低。不允许key或value为null
3.2.1 HashMap的底层源码分析
(1)底层存储介绍
HashMap底层实现采用了哈希表,这是一种非常重要的数据结构。对于我们以后理解很多技术都非常有帮助。
数据结构中由数组和链表来实现对数据的存储,他们各有特点。
(1) 数组:占用空间连续。 寻址容易,查询速度快。但是,增加和删除效率非常低。
(2) 链表:占用空间不连续。 寻址困难,查询速度慢。但是,增加和删除效率非常高。
那么,我们能不能结合数组和链表的优点(即查询快,增删效率也高)呢? 答案就是“哈希表”。 哈希表的本质就是“数组+链表”。
(2)HashMap中的成员变量
// 用于版本控制的序列化ID。
private static final long serialVersionUID = 362498820763181265L;
// HashMap的默认初始容量。
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16
// HashMap的最大容量,必须是小于等于1<<30的2的幂次方。
static final int MAXIMUM_CAPACITY = 1 << 30;
// 当在构造函数中未指定加载因子时使用的默认加载因子。
static final float DEFAULT_LOAD_FACTOR = 0.75f;
// 将链表转换为树结构的阈值。
// 当一个桶(数组中的一个位置)上的链表长度达到该阈值时,会将链表转换为红黑树。
static final int TREEIFY_THRESHOLD = 8;
// 将树结构转换回链表的阈值。
//当一个桶上的红黑树节点数量小于等于该阈值时,会将红黑树转换回链表。
static final int UNTREEIFY_THRESHOLD = 6;
// 进行树化操作的最小桶容量。
//当HashMap的容量达到该值时,才会进行链表转换为红黑树的操作。
static final int MIN_TREEIFY_CAPACITY = 64;
// 存储实际数据的数组,用于存储键值对。
transient Node<K,V>[] table;
// HashMap中的键值对集合,提供对键值对的遍历操作。
transient Set<Map.Entry<K,V>> entrySet;
// HashMap中键值对的数量。
transient int size;
// HashMap结构修改的次数,用于在迭代器中实现快速失败机制。
transient int modCount;
// 容量的阈值,当实际存储的键值对数量超过该值时,HashMap会进行扩容操作。
int threshold;
// 负载因子,默认为0.75。负载因子决定了什么时候进行扩容操作。
//当 size > threshold * loadFactor 时,进行扩容操作。
final float loadFactor;
(2)HashMap中存储元素的节点类型
Node类
/**
* Basic hash bin node, used for most entries. (See below for
* TreeNode subclass, and in LinkedHashMap for its Entry subclass.)
*/
static class Node<K,V> implements Map.Entry<K,V> {
final int hash;
final K key;
V value;
Node<K,V> next;
Node(int hash, K key, V value, Node<K,V> next) {
this.hash = hash;
this.key = key;
this.value = value;
this.next = next;
}
public final K getKey() { return key; }
public final V getValue() { return value; }
public final String toString() { return key + "=" + value; }
public final int hashCode() {
return Objects.hashCode(key) ^ Objects.hashCode(value);
}
public final V setValue(V newValue) {
V oldValue = value;
value = newValue;
return oldValue;
}
public final boolean equals(Object o) {
if (o == this)
return true;
if (o instanceof Map.Entry) {
Map.Entry<?,?> e = (Map.Entry<?,?>)o;
if (Objects.equals(key, e.getKey()) &&
Objects.equals(value, e.getValue()))
return true;
}
return false;
}
}
TreeNode类
/**
* Entry for Tree bins. Extends LinkedHashMap.Entry (which in turn
* extends Node) so can be used as extension of either regular or
* linked node.
*/
static final class TreeNode<K,V> extends LinkedHashMap.Entry<K,V> {
TreeNode<K,V> parent; // red-black tree links
TreeNode<K,V> left;
TreeNode<K,V> right;
TreeNode<K,V> prev; // needed to unlink next upon deletion
boolean red;
TreeNode(int hash, K key, V val, Node<K,V> next) {
super(hash, key, val, next);
}
/**
* Returns root of tree containing this node.
*/
final TreeNode<K,V> root() {
for (TreeNode<K,V> r = this, p;;) {
if ((p = r.parent) == null)
return r;
r = p;
}
}
}
它们的继承关系
(3)HashMap中的数组初始化和扩容
在JDK 1.8中,HashMap的resize()方法用于初始化或者扩容底层数组。下面是该方法的源码分析:
final Node<K,V>[] resize() {
Node<K,V>[] oldTab = table; // 旧的数组
int oldCap = (oldTab == null) ? 0 : oldTab.length; // 旧的容量
int oldThr = threshold; // 旧的阈值
int newCap, newThr = 0; // 新的容量和阈值
if (oldCap > 0) { // 旧的数组不为空
if (oldCap >= MAXIMUM_CAPACITY) { // 如果旧的容量已经达到了最大容量
threshold = Integer.MAX_VALUE; // 阈值设为最大整数值,表示不能再进行扩容操作了
return oldTab;
}
else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY && // 新的容量为旧的容量的两倍
oldCap >= DEFAULT_INITIAL_CAPACITY)
newThr = oldThr << 1; // 新的阈值为旧的阈值的两倍,即扩容时的阈值也会翻倍
}
else if (oldThr > 0) // 初始化容量值存储在阈值中
newCap = oldThr; // 新的容量为旧的阈值
else { // 表示使用默认值来初始化
newCap = DEFAULT_INITIAL_CAPACITY; // 新的容量为默认初始容量
newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY); // 新的阈值根据默认负载因子计算得出
}
if (newThr == 0) { // 如果新的阈值为0,说明需要根据新的容量和负载因子重新计算阈值
float ft = (float)newCap * loadFactor;
newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
(int)ft : Integer.MAX_VALUE);
}
threshold = newThr; // 更新阈值
@SuppressWarnings({"rawtypes","unchecked"})
Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap]; // 创建新的数组
table = newTab; // 更新table属性为新的数组
if (oldTab != null) { // 如果旧的数组不为空,需要将元素从旧的数组迁移到新的数组中
for (int j = 0; j < oldCap; ++j) {
Node<K,V> e;
if ((e = oldTab[j]) != null) { // 从旧数组中获取节点
oldTab[j] = null; // 将旧数组对应位置的值设为null
if (e.next == null) // 如果该节点没有下一个节点,直接放入新数组中对应位置
newTab[e.hash & (newCap - 1)] = e;
else if (e instanceof TreeNode) // 如果该节点是树节点,调用split()方法进行处理
((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
else { // 否则保持节点顺序不变,按原来的顺序插入新数组中对应位置
Node<K,V> loHead = null, loTail = null;
Node<K,V> hiHead = null, hiTail = null;
Node<K,V> next;
do {
next = e.next;
if ((e.hash & oldCap) == 0) { // 判断是否需要在低位链表中插入
if (loTail == null)
loHead = e;
else
loTail.next = e;
loTail = e;
}
else { // 否则在高位链表中插入
if (hiTail == null)
hiHead = e;
else
hiTail.next = e;
hiTail = e;
}
} while ((e = next) != null);
if (loTail != null) { // 将低位链表放入新数组中
loTail.next = null;
newTab[j] = loHead;
}
if (hiTail != null) { // 将高位链表放入新数组中
hiTail.next = null;
newTab[j + oldCap] = hiHead;
}
}
}
}
}
return newTab; // 返回新的数组
}
在resize()方法中,首先获取旧的数组、容量和阈值。然后根据情况计算出新的容量和阈值。如果旧的容量已经达到了最大容量,则阈值设为最大整数值,表示不能再进行扩容操作。如果旧的容量小于最大容量且大于等于默认初始容量,则新的容量为旧的容量的两倍,新的阈值为旧的阈值的两倍。否则,如果旧的阈值大于0,则新的容量为旧的阈值。如果既没有旧的容量也没有旧的阈值,则使用默认初始容量和根据默认负载因子计算得出的阈值。
接下来,根据新的容量和负载因子重新计算阈值。如果新的阈值为0,说明需要根据新的容量和负载因子重新计算阈值。然后更新阈值和table属性。
最后,如果旧的数组不为空,则需要将元素从旧的数组迁移到新的数组中。遍历旧的数组,对每个位置上的节点进行处理。如果节点没有下一个节点,直接放入新的数组中对应位置;如果节点是树节点,则调用split()方法进行处理;否则,保持节点顺序不变,按原来的顺序插入新数组中对应位置。
最后返回新的数组。通过这个过程,HashMap的底层数组就完成了初始化或者扩容的操作。
HashMap中计算Hash值
// 获取key的hashCode值,根据hashCode值进行计算索引位置
final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
hash()
方法首先检查传入的key是否为null,如果为null
则直接返回0。否则,调用key.hashCode()
方法获取key的hashCode
值,并将该值与右移16位后的结果进行异或操作。这样做的目的是为了让hashCode
的高位和低位都参与到混合运算中,以增加散列均匀性。
(n - 1) & hash
利用hashCode
值和数组的长度进行按位与运算,来得到key
在数组中的索引位置。这里使用的技巧是,由于数组的长度通常是2的幂次方,所以length - 1
的二进制表示形式中,除了最高位都为1,其余位都为0。通过将hashCode
值与这个掩码进行与运算,可以保证计算得到的索引位置永远在数组的有效范围内。
(4)HashMap中添加元素
在JDK 1.8中,HashMap的添加元素方法是通过putVal()方法来实现的。下面是源码分析:
public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
}
// 获取key的hashCode值,根据hashCode值进行计算索引位置
final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
final V putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict) {
Node<K,V>[] tab;
Node<K,V> p;
int n, i;
// 如果数组为空或者长度为0,则进行初始化操作
if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length; // 调用resize()方法进行初始化或者扩容
// 计算key的hashCode值,并通过indexFor()方法计算对应的索引位置
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null); // 如果该索引位置为空,则直接新建一个节点放入该位置
else { // 否则,发生了哈希冲突,需要处理冲突
Node<K,V> e;
K k;
// 判断该位置上的第一个节点是否与插入的键相等,如果相等,则直接覆盖原有值
if (p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k))))
e = p;
// 如果该位置上的第一个节点是树节点,则插入到树节点中
else if (p instanceof TreeNode)
e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
// 否则,遍历链表查找或插入节点
else {
for (int binCount = 0; ; ++binCount) {
if ((e = p.next) == null) { // 到达链表末尾,插入新节点
p.next = newNode(hash, key, value, null);
// 如果链表长度大于等于TREEIFY_THRESHOLD,则将该链表转为红黑树
if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
treeifyBin(tab, hash);
break;
}
if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k))))
break;
p = e;
}
}
// 如果找到了相同的键,则更新对应的值
if (e != null) { // existing mapping for key
V oldValue = e.value;
if (!onlyIfAbsent || oldValue == null)
e.value = value;
afterNodeAccess(e);
return oldValue;
}
}
++modCount; // 修改计数器
// 如果增加元素后的元素数量大于阈值,进行扩容操作
if (++size > threshold)
resize();
afterNodeInsertion(evict);
return null;
}
在putVal()
方法中,首先判断数组是否为空或长度为0,如果是,则调用resize()
方法进行初始化或者扩容。
然后,计算key
的hashCode
值,并通过按位与计算出对应的索引位置。如果该索引位置为空,直接新建一个节点放入该位置。
如果索引位置上已经存在节点,则需要处理哈希冲突。首先判断第一个节点是否与插入的键相等,如果相等,则直接覆盖原有值。如果第一个节点是树节点,则插入到树节点中。如果既不是树节点也不是相同键的节点,则遍历链表查找或插入节点。
如果找到了相同的键,则更新对应的值。如果未找到相同的键,则将新节点插入到链表末尾,并根据链表长度是否达到阈值进行判断是否需要将链表转为红黑树。
最后,增加元素的数量,如果数量超过阈值,则进行扩容操作。完成插入操作后,会调用afterNodeInsertion()
方法进行必要的后处理。
3.3 TreeMap容器介绍
- TreeSet使用TreeMap实现,只是value使用静态空对象,只是用key实现TreeSet
- TreeMap存储 Key-Value 对时, 需要根据 key-value 对进行排序。TreeMap 可以保证所有的 Key-Value 对处于有序状态。
- TreeSet底层使用红黑树结构存储数据
- TreeMap 的 Key 的排序:
- 自然排序: TreeMap 的所有的 Key 必须实现 Comparable 接口,而且所有的 Key 应该是同一个类的对象,否则将会抛出 ClasssCastException
- 定制排序:创建 TreeMap 时,传入一个 Comparator 对象,该对象负责对TreeMap 中的所有 key 进行排序。此时不需要 Map 的 Key 实现Comparable 接口
- TreeMap判断两个key相等的标准:两个key通过compareTo()方法或者compare()方法返回0,1,-1
- 0:对象相等,添加失败
- -1:比对象小,添加到左边
- 1:比对象打,添加到右边
TreeMap是Java集合框架中的一种有序映射容器,实现了NavigableMap<K,V>接口,该接口是SortedMap<K,V>的一个子接口,并基于红黑树数据结构来保持元素的有序性。TreeMap可以存储键值对,并根据键进行自然排序或者使用自定义的比较器进行排序。
TreeMap的主要特点如下:
- 有序性:TreeMap中的键值对按照键的顺序进行排序。通过实现Comparable接口或者在构造TreeMap时指定Comparator来定义排序规则。
- 重复键处理:TreeMap不允许存在重复的键,如果插入的键已经存在,则会用新值覆盖旧值。
- 动态性:TreeMap可以根据添加或删除元素的操作动态地调整自身的结构,以保持有序性。
- 快速查找:TreeMap支持快速的查找操作,根据键可以在O(logn)的时间复杂度内找到对应的值。
- 树结构:TreeMap内部使用红黑树数据结构来实现,这种平衡二叉搜索树能够在插入、删除和查找等操作上保持较好的性能。
使用TreeMap时,可以通过put(key, value)方法向容器中插入键值对,通过get(key)方法获取指定键对应的值,通过remove(key)方法删除指定键值对。此外,TreeMap还提供了一些其他的方法用于遍历、查询和操作键值对。
需要注意的是,TreeMap不是线程安全的,如果在多线程环境下使用,需要进行适当的同步处理或者使用线程安全的并发容器(如ConcurrentSkipListMap)代替。
3.3.1 在使用TreeMap时需要给定排序规则
- 元素自身实现比较规则
- 通过比较器实现比较规则
import java.util.Comparator;
import java.util.TreeMap;
class Person implements Comparable<Person> {
private String name;
private int age;
public Person(String name, int age) {
this.name = name;
this.age = age;
}
public String getName() {
return name;
}
public int getAge() {
return age;
}
@Override
public int compareTo(Person other) {
return this.name.compareTo(other.name);
}
}
class PersonComparator implements Comparator<Person> {
@Override
public int compare(Person p1, Person p2) {
return Integer.compare(p1.getAge(), p2.getAge());
}
}
public class TreeMapSortingExample {
public static void main(String[] args) {
// 使用元素自身实现比较规则,按照姓名的字母顺序排序
TreeMap<Person, String> treeMap1 = new TreeMap<>();
treeMap1.put(new Person("Jack", 25), "Engineer");
treeMap1.put(new Person("Alice", 30), "Doctor");
treeMap1.put(new Person("Bob", 20), "Student");
System.out.println("按照姓名排序结果:");
for (Person person : treeMap1.keySet()) {
System.out.println(person.getName() + ": " + treeMap1.get(person));
}
System.out.println();
// 使用比较器实现比较规则,按照年龄排序
Comparator<Person> comparator = new PersonComparator();
TreeMap<Person, String> treeMap2 = new TreeMap<>(comparator);
treeMap2.put(new Person("Jack", 25), "Engineer");
treeMap2.put(new Person("Alice", 30), "Doctor");
treeMap2.put(new Person("Bob", 20), "Student");
System.out.println("按照年龄排序结果:");
for (Person person : treeMap2.keySet()) {
System.out.println(person.getName() + ": " + treeMap2.get(person));
}
}
}
输出结果:
按照姓名排序结果:
Alice: Doctor
Bob: Student
Jack: Engineer
按照年龄排序结果:
Bob: Student
Jack: Engineer
Alice: Doctor
在这个示例中,Person
类实现了Comparable
接口,并重写compareTo
方法来定义按照姓名排序的规则。另外,定义了PersonComparator
类实现了Comparator
接口,并重写compare
方法来定义按照年龄排序的规则。通过TreeMap
和自定义的比较规则,可以实现根据不同的排序规则来对Person
对象进行排序。
3.3.2 TreeMap的底层源码分析
在 JDK 1.8 中,TreeMap
的底层源码实现主要涉及红黑树的概念。红黑树是一种自平衡的二叉搜索树,通过保持一组特定规则来保持树的平衡性。
下面是 TreeMap
的部分关键源码解析:
-
TreeMap.Entry
类:TreeMap
内部定义了一个私有内部类Entry
,用于表示树中的节点。static final class Entry<K,V> implements Map.Entry<K,V> { K key; V value; Entry<K,V> left; Entry<K,V> right; Entry<K,V> parent; boolean color = BLACK; // 构造方法和其他方法省略... }
Entry
类包含了键值对的信息,以及左右子节点、父节点和颜色等属性。 -
TreeMap
类的成员变量:private transient Entry<K,V> root; // 根节点 private transient int size = 0; // 元素数量 private transient int modCount = 0; // 修改计数 private final Comparator<? super K> comparator; // 键的比较器
root
存储树的根节点,size
表示元素数量,modCount
记录修改次数,comparator
用于比较键的顺序。 -
TreeMap
类的构造方法:public TreeMap() { comparator = null; } public TreeMap(Comparator<? super K> comparator) { this.comparator = comparator; }
TreeMap
可以使用默认的无参构造方法创建,也可以传入一个键的比较器来创建。 -
TreeMap
类的核心方法之一是put()
方法,用于插入键值对到树中:public V put(K key, V value) { Entry<K,V> t = root; if (t == null) { // 如果根节点为空,直接创建新节点作为根节点 compare(key, key); // 检查键是否合法(防止比较器错误) root = new Entry<>(key, value, null); size = 1; modCount++; return null; } int cmp; Entry<K,V> parent; Comparator<? super K> cpr = comparator; // 获取比较器 if (cpr != null) { do { parent = t; cmp = cpr.compare(key, t.key); // 使用比较器比较键的顺序 if (cmp < 0) // 小于当前节点,向左子树查找 t = t.left; else if (cmp > 0) // 大于当前节点,向右子树查找 t = t.right; else // 如果已存在相同键,则更新值并返回旧值 return t.setValue(value); } while (t != null); } else { // 没有比较器时,使用键的自然顺序进行比较 if (key == null) throw new NullPointerException(); @SuppressWarnings("unchecked") Comparable<? super K> k = (Comparable<? super K>) key; do { parent = t; cmp = k.compareTo(t.key); if (cmp < 0) t = t.left; else if (cmp > 0) t = t.right; else return t.setValue(value); } while (t != null); } // 创建新节点并插入到树中 Entry<K,V> e = new Entry<>(key, value, parent); if (cmp < 0) parent.left = e; else parent.right = e; fixAfterInsertion(e); // 插入后修复红黑树性质 size++; modCount++; return null; }
put()
方法首先根据是否有比较器来选择使用比较器或键的自然顺序进行键的比较。然后根据比较结果向左子树或右子树遍历找到合适的位置插入新节点。最后,通过调用fixAfterInsertion()
方法来修正红黑树的平衡。 -
TreeMap
类还提供了其他操作方法,如get()
、containsKey()
、entrySet()
等,这些方法都是基于树结构进行的。
总结一下,JDK 1.8 中的 TreeMap
底层使用红黑树实现,通过比较器(或键的自然顺序)来存储和检索键值对。在插入或删除节点时,使用红黑树的性质来保持树的平衡。通过这种方式,TreeMap
实现了高效的键值对的有序存储和检索。
3.4 LinkedHashMap容器介绍
- LinkedHashSet和LinedHashMap的关系,从逻辑上这两个集合实现方式完全一致,只是LinkedHashSet使用LinkedHashMap实现,只有key,而value是一个静态的空对象
- 底层使用链表实现
- 有顺序(插入顺序),没有重复的集合
public static void main(String[] args) {
LinkedHashSet set = new LinkedHashSet();
set.add("zzz");
set.add("zzz");
set.add("zzz");
set.add("yyy");
set.add("yyy");
set.add("xxx");
set.add("xxx");
set.add("fff");
set.add("eee");
set.add("www");
set.add("111");
set.add("222");
set.add("333");
set.add("666");
set.add("555");
set.add("444");
for (Object o : set) {
System.out.println(o);
}
}
3.5 Hashtable
- Hashtable是个古老的 Map 实现类, JDK1.0就提供了。不同于HashMap,Hashtable是线程安全的。
- Hashtable实现原理和HashMap相同,功能相同。底层都使用哈希表结构,查询速度快,很多情况下可以互用。
- 与HashMap不同, Hashtable 不允许使用 null 作为 key 和 value
- 与HashMap一样, Hashtable 也不能保证其中 Key-Value 对的顺序
- Hashtable判断两个key相等、两个value相等的标准, 与HashMap一致。
public class HashTableDemo1 {
public static void main(String[] args) {
Hashtable map = new Hashtable();
map.put("aaaa", 11);
map.put("aaaa", 1111);
map.put("bbbb", 22);
map.put("cccc", 33);
map.put("dddd", 44);
map.put("eeee", 55);
map.put("eeee", 5555);
map.put("ffff", 666);
map.put("gggg", 666);
map.put("hhhh", 666);
map.put("3333", 666);
map.put("4444", 666);
map.put("1111", 666);
map.put("2222", 666);
// map.put(null, 666); //不能存null
for (Object key : map.entrySet()) {
System.out.println(key + " " + map.get(key));
}
}
}
3.6 Properties
-
Properties 类是 Hashtable 的子类,该对象用于处理属性文件
-
由于属性文件里的 key、 value 都是字符串类型,所以 Properties 里的 key和 value 都是字符串类型
-
存取数据时,建议使用setProperty(String key,String value)方法和getProperty(String key)方法
user.dir = 项目目录
username=root
password=123456
jdbc.url=jdbc:mysql://localhost:3306/test
jdbc.driver=com.mysql.driver.Driver
public class PropertiesDemo1 {
public static void main(String[] args) throws IOException {
Properties properties = new Properties();
// FileInputStream in = new FileInputStream("F:\\work\\lxs\\49_java_basic\\01-Java-基础\\9-集合\\code\\java-demo9\\jdbc.properties");
//文件在user.dir目录下,可以使用下面相对目录
FileInputStream in = new FileInputStream("jdbc.properties");
properties.load(in);
System.out.println(properties.getProperty("username"));
System.out.println(properties.getProperty("password"));
System.out.println(properties.getProperty("jdbc.url"));
System.out.println(properties.getProperty("jdbc.driver"));
in.close();
}
}
四、Iterator接口
4.1 Iterator迭代器接口介绍
Collection接口继承了Iterable接口,在该接口中包含一个名为iterator的抽象方法,所有实现了Collection接口的容器类对该方法做了具体实现。iterator方法会返回一个Iterator接口类型的迭代器对象,在该对象中包含了三个方法用于实现对单例容器的迭代处理。
4.2 Iterator对象的工作原理
4.3 Iterator接口方法
boolean hasNext();
//判断游标当前位置的下一个位置是否还有元素没有被遍历;Object next();
//返回游标当前位置的下一个元素并将游标移动到下一个位置;void remove();
//删除游标当前位置的元素,在执行完next后该操作只能执行一次;
4.4 Iterator迭代器的使用
迭代List接口类型容器
import java.util.Iterator;
import java.util.List;
import java.util.ArrayList;
public class IteratorExample {
public static void main(String[] args) {
List<String> myList = new ArrayList<>();
myList.add("Apple");
myList.add("Banana");
myList.add("Orange");
// 使用Iterator进行迭代
Iterator<String> iterator = myList.iterator();
while (iterator.hasNext()) {
String element = iterator.next();
System.out.println(element);
}
}
}
迭代Set接口类型容器
import java.util.Iterator;
import java.util.Set;
import java.util.HashSet;
public class IteratorExample {
public static void main(String[] args) {
Set<String> mySet = new HashSet<>();
mySet.add("Apple");
mySet.add("Banana");
mySet.add("Orange");
// 使用Iterator进行迭代
Iterator<String> iterator = mySet.iterator();
while (iterator.hasNext()) {
String element = iterator.next();
System.out.println(element);
}
}
}
迭代Map接口类型容器
import java.util.Iterator;
import java.util.Map;
import java.util.HashMap;
public class IteratorExample {
public static void main(String[] args) {
Map<String, Integer> myMap = new HashMap<>();
myMap.put("Apple", 10);
myMap.put("Banana", 20);
myMap.put("Orange", 30);
// 获取键的集合,并使用Iterator进行迭代
Iterator<String> keyIterator = myMap.keySet().iterator();
while (keyIterator.hasNext()) {
String key = keyIterator.next();
Integer value = myMap.get(key);
System.out.println(key + " - " + value);
}
// 获取键值对的集合,并使用Iterator进行迭代
Iterator<Map.Entry<String, Integer>> entryIterator = myMap.entrySet().iterator();
while (entryIterator.hasNext()) {
Map.Entry<String, Integer> entry = entryIterator.next();
String key = entry.getKey();
Integer value = entry.getValue();
System.out.println(key + " - " + value);
}
}
}
4.5 在迭代器中删除元素
在使用迭代器迭代容器时,如果需要删除元素,我们不能直接使用容器的remove方法,而是要使用迭代器的remove方法。这是因为直接使用容器的remove方法可能会导致并发修改异常(ConcurrentModificationException)。
下面是一个示例代码,展示了如何使用迭代器删除集合中的元素:
import java.util.Iterator;
import java.util.List;
import java.util.ArrayList;
public class IteratorExample {
public static void main(String[] args) {
List<String> myList = new ArrayList<>();
myList.add("Apple");
myList.add("Banana");
myList.add("Orange");
Iterator<String> iterator = myList.iterator();
while (iterator.hasNext()) {
String element = iterator.next();
if (element.equals("Banana")) {
iterator.remove();
}
}
System.out.println(myList);
}
}
在上述代码中,我们创建了一个ArrayList类型的容器myList,并添加了一些元素。然后,我们通过调用容器的iterator方法获取到一个Iterator对象iterator,该对象用于遍历容器中的元素。
在循环中,我们使用hasNext方法检查是否还有下一个元素,如果有,使用next方法获取下一个元素。在这个示例中,我们检查每个元素是否等于"Banana",如果是,则使用iterator的remove方法将其从集合中删除。
最后,我们打印输出集合中的元素,可以看到"Banana"已经被成功删除。
输出结果将是:
[Apple, Orange]
可以看到,使用迭代器的remove方法能够安全地删除集合中的元素。这是因为迭代器在遍历过程中跟踪了集合的状态,并且通过该方法删除元素不会导致并发修改异常。
4.6 遍历集合的方法总结
当遍历一个List时,可以使用以下几种方法:
- 使用for循环:
for (Object element : list) {
// 对element进行操作
}
这是一种简单而常见的方法,可以遍历List中的每个元素。需要注意的是,这种方法只能顺序遍历List,不能进行索引操作。
- 使用for循环和索引:
for (int i = 0; i < list.size(); i++) {
Object element = list.get(i);
// 对element进行操作
}
这种方法与前一种方法类似,使用了一个循环变量i来作为索引值,可以通过索引值获取List中的元素。可以顺序遍历List,并且可以根据需要进行索引操作。
- 使用迭代器(Iterator):
Iterator<Object> iterator = list.iterator();
while (iterator.hasNext()) {
Object element = iterator.next();
// 对element进行操作
}
使用迭代器可以遍历List中的元素,通过调用hasNext()
判断是否还有下一个元素,再通过next()
获取下一个元素。迭代器提供了更多的灵活性,可以在遍历过程中对元素进行增删操作。
- 使用Java 8的Stream:
list.stream().forEach(element -> {
// 对element进行操作
});
Java 8引入了Stream API,可以通过将List转换为Stream来进行遍历和操作。使用Stream可以进行过滤、映射、排序等操作,更加灵活和函数式。
当遍历一个Set时,可以使用以下几种方法:
- 使用for-each循环:
for (Object element : set) {
// 对element进行操作
}
这是一种简单常见的方法,可以遍历Set中的每个元素。由于Set是无序的,所以循环顺序是不确定的。
- 使用迭代器(Iterator):
Iterator<Object> iterator = set.iterator();
while (iterator.hasNext()) {
Object element = iterator.next();
// 对element进行操作
}
使用迭代器可以遍历Set中的元素。由于Set是无序的,所以迭代器的顺序也是不确定的。迭代器提供了更多的灵活性,可以在遍历过程中对元素进行增删操作。
- 使用Java 8的Stream:
set.stream().forEach(element -> {
// 对element进行操作
});
Java 8引入的Stream API可以将Set转换为Stream来进行遍历和操作。使用Stream可以进行过滤、映射、排序等操作。
注意,上述方法都是适用于遍历Set中的元素,并没有提供索引访问的方式。另外需要注意的是,遍历Set时元素的顺序是不确定的,因为Set是无序的数据结构。不同的Set实现类(如HashSet、TreeSet)有不同的顺序规则。
当遍历一个Map时,可以使用以下几种方法:
- 使用for-each循环遍历键值对(Entry):
for (Map.Entry<KeyType, ValueType> entry : map.entrySet()) {
KeyType key = entry.getKey();
ValueType value = entry.getValue();
// 对key和value进行操作
}
这种方法是最常见和推荐的遍历Map的方式。通过entrySet()
方法获取Map中的所有键值对(Entry),然后使用for-each循环遍历每个Entry,再通过getKey()
和getValue()
方法获取键和值,进行相应的操作。
- 使用迭代器(Iterator)遍历键值对(Entry):
Iterator<Map.Entry<KeyType, ValueType>> iterator = map.entrySet().iterator();
while (iterator.hasNext()) {
Map.Entry<KeyType, ValueType> entry = iterator.next();
KeyType key = entry.getKey();
ValueType value = entry.getValue();
// 对key和value进行操作
}
这种方法与前一种方法类似,使用迭代器遍历Map的键值对(Entry),然后通过getKey()
和getValue()
方法获取键和值。迭代器提供了更多的灵活性,可以在遍历过程中对Entry进行增删操作。
- 只遍历键或值:
如果你只关心Map中的键或值,可以使用以下两种方法:
- 遍历键(Key):
for (KeyType key : map.keySet()) {
// 对key进行操作
}
- 遍历值(Value):
for (ValueType value : map.values()) {
// 对value进行操作
}
这两种方法分别使用keySet()
和values()
方法获取Map中的键集合和值集合,然后使用for-each循环遍历键或值,进行相应的操作。
需要注意的是,Map是无序的数据结构,因此无法确定键值对的遍历顺序。如果你需要保持特定的顺序,可以考虑使用LinkedHashMap
或TreeMap
等有序的Map实现类。
五、Collections工具类
- Collections 是一个操作 Set、 List 和 Map 等集合的工具类
- Collections 中提供了一系列静态的方法对集合元素进行排序、查询和修改等操作,还提供了对集合对象设置不可变、对集合对象实现同步控制等方法
- 排序操作: (均为static方法)
- reverse(List): 反转 List 中元素的顺序
- shuffle(List): 对 List 集合元素进行随机排序
- sort(List): 根据元素的自然顺序对指定 List 集合元素按升序排序
- sort(List, Comparator): 根据指定的 Comparator 产生的顺序对 List 集合元素进行排序
- swap(List, int, int): 将指定 list 集合中的 i 处元素和 j 处元素进行交换
- 查找、替换
- Object max(Collection): 根据元素的自然顺序,返回给定集合中的最大元素
- Object max(Collection, Comparator): 根据 Comparator 指定的顺序,返回给定集合中的最大元素
- Object min(Collection)
- Object min(Collection, Comparator)
- int frequency(Collection, Object): 返回指定集合中指定元素的出现次数
- void copy(List dest,List src):将src中的内容复制到dest中
- boolean replaceAll(List list, Object oldVal, Object newVal): 使用新值替换List 对象的所有旧值
- Collections 类中提供了多个 synchronizedXxx() 方法,该方法可使将指定集合包装成线程同步的集合,从而可以解决多线程并发访问集合时的线程安全问题
类 java.util.Collections 提供了对Set、List、Map进行排序、填充、
查找元素的辅助方法。
方法名 | 说明 |
---|---|
void sort(List) | 对List容器内的元素排序,排序规则是升序。 |
void shuffle(List) | 对List容器内的元素进行随机排列 |
void reverse(List) | 对List容器内的元素进行逆续排列 |
void fill(List, Object) | 用一个特定的对象重写整个List容器 |
int binarySearch(List, Object) | 对于顺序的List容器,折半查找查找特定对象 |
Collections是Java中提供的一个工具类,用于操作集合(Collection)类的静态方法。下面是一些常用的Collections工具类方法:
sort(List<T> list)
:对List进行升序排序。
List<Integer> numbers = new ArrayList<>();
numbers.add(3);
numbers.add(1);
numbers.add(2);
Collections.sort(numbers);
System.out.println(numbers); // 输出 [1, 2, 3]
reverse(List<?> list)
:反转List元素的顺序。
List<String> names = new ArrayList<>();
names.add("Alice");
names.add("Bob");
names.add("Charlie");
Collections.reverse(names);
System.out.println(names); // 输出 ["Charlie", "Bob", "Alice"]
shuffle(List<?> list)
:随机打乱List中的元素顺序。
List<Integer> numbers = new ArrayList<>();
numbers.add(1);
numbers.add(2);
numbers.add(3);
Collections.shuffle(numbers);
System.out.println(numbers); // 输出 [3, 1, 2](可能的输出结果会不同)
binarySearch(List<? extends Comparable<? super T>> list, T key)
:使用二分查找算法在有序List中查找指定元素,并返回索引位置。
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);
int index = Collections.binarySearch(numbers, 3);
System.out.println(index); // 输出 2
max(Collection<? extends T> coll)
和min(Collection<? extends T> coll)
:找到集合中的最大值和最小值。
List<Integer> numbers = Arrays.asList(1, 5, 3, 2, 4);
int max = Collections.max(numbers);
int min = Collections.min(numbers);
System.out.println(max); // 输出 5
System.out.println(min); // 输出 1
frequency(Collection<?> coll, Object obj)
:统计集合中指定元素的出现次数。
List<String> names = Arrays.asList("Alice", "Bob", "Alice", "Charlie", "Bob");
int count = Collections.frequency(names, "Alice");
System.out.println(count); // 输出 2
这只是Collections工具类中的一些常用方法,还有其他方法可以用于操作集合类。通过使用Collections工具类,我们可以方便地对集合进行排序、反转、随机打乱等操作,并且不需要手动编写对应的算法。