Java集合框架中的Collection接口是所有集合类的基础接口,定义了一些基本的集合操作,如添加元素、删除元素、判断是否包含某个元素等。常见的集合类包括List、Set和Queue。
List
List接口定义了按照索引访问和操作元素的方法。它允许元素重复,并且有序。在List中可以使用get()和set()方法访问指定位置的元素,使用add()和remove()方法添加和删除元素。
常见的List实现类有:
ArrayList:ArrayList 是一个基于动态数组的实现,支持随机访问,插入和删除操作效率低。
LinkedList:底层使用双向链表实现,插入和删除操作效率高,但随机访问效率低。
Vector:与ArrayList类似,但是线程安全,效率较低。
Set
Set接口表示一个不允许有重复元素的集合,实现类必须重写equals()方法和hashCode()方法。常见的Set实现类有:
HashSet:底层使用哈希表实现,无序,元素唯一。
LinkedHashSet:底层使用哈希表和链表实现,有序,元素唯一。
TreeSet:底层使用红黑树实现,有序,元素唯一。
Queue
Queue接口表示一个先进先出(FIFO)的队列。常见的Queue实现类有:
LinkedList:底层使用链表实现,效率较高,LinkedList实现了Queue接口,它支持在队列的头部和尾部进行元素的添加和删除操作,因此可以被用作栈、队列和双端队列。。
PriorityQueue:是一种基于优先级堆的Queue,它保证了每次取出的元素都是队列中优先级最高的元素。。
需要注意的是,这些集合类都是基于Object的,如果需要在集合中存储特定类型的元素,需要使用泛型。例如,List
ArrayList 的底层实现基于数组,它继承了 AbstractList 抽象类并实现了 List 接口。下面是一些关于 ArrayList 的底层实现的细节:
数组:ArrayList 的内部实现是一个数组,使用数组实现可以方便地进行随机访问,根据索引直接访问指定位置的元素。
自动扩容:ArrayList 可以自动扩容以适应动态变化的容量需求,每次扩容会增加 50% 的容量。
元素的添加:ArrayList 中的 add(E e) 方法会在末尾添加一个元素,如果当前容量不足,则会进行扩容。
元素的删除:ArrayList 中的 remove(int index) 方法会删除指定索引位置的元素,将该位置后面的元素向前移动一位。
当调用ArrayList的add方法时,如果当前列表中的元素数量已经达到容量的极限,那么就需要自动扩容。扩容的过程就是创建一个新的数组,并将原来数组中的元素复制到新数组中。
默认情况下,ArrayList的容量是10。当第一个元素被添加时,内部数组会被初始化为长度为10的数组。当添加第11个元素时,原始数组将会被复制到一个新的长度为15的数组中,容量增加了50%。如果再添加元素,当超过了15个元素时,内部数组将再次扩容到新的长度为22的数组中。
当使用ensureCapacity方法增加数组容量时,ArrayList使用给定参数的最大值和当前容量的大小来决定新的容量大小。
private void ensureCapacityInternal(int minCapacity) {
// 判断是否需要扩容
if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity);
}
ensureExplicitCapacity(minCapacity);
}
private void ensureExplicitCapacity(int minCapacity) {
modCount++;
// 判断是否需要扩容
if (minCapacity - elementData.length > 0) {
grow(minCapacity);
}
}
private void grow(int minCapacity) {
int oldCapacity = elementData.length;
int newCapacity = oldCapacity + (oldCapacity >> 1);
if (newCapacity - minCapacity < 0) {
newCapacity = minCapacity;
}
if (newCapacity - MAX_ARRAY_SIZE > 0) {
newCapacity = hugeCapacity(minCapacity);
}
elementData = Arrays.copyOf(elementData, newCapacity);
}
扩容操作的代价是很高的,因此在实际使用时,我们应该尽量避免数组容量的扩张。当我们可预知要保存的元素的多少时,要在构造ArrayList实例时,就指定其容量,以避免数组扩容的发生。或者根据实际需求,通过调用ensureCapacity方法来手动增加ArrayList实例的容量。
在 Java 中,如果使用集合类的迭代器来遍历集合元素,而同时修改了集合中的元素,就有可能会发生 ConcurrentModificationException 异常。这是因为 Java 集合类的迭代器是快速失败(fail-fast)机制,如果在迭代集合时集合发生了结构性变化(例如添加或删除元素),迭代器就会立即抛出异常,而不是等到迭代完成再抛出异常。
ArrayList 是一个支持随机访问的序列容器,底层使用数组实现,所以在对 ArrayList 进行并发操作时,可能会出现不同步的问题,因此 ArrayList 也使用了快速失败机制来保证线程安全。
具体来说,如果在对 ArrayList 进行迭代操作的同时,对其进行增删改操作,会导致 ArrayList 的 modCount(修改次数)和迭代器的 expectedModCount(预期的修改次数)不一致,迭代器会立即抛出 ConcurrentModificationException 异常。
以下是一个简单的示例代码,用来演示 ArrayList 快速失败机制的工作原理:
List list = new ArrayList<>(Arrays.asList(1, 2, 3, 4, 5, 6, 7));
Iterator iterator = list.iterator();
while (iterator.hasNext()) {
Integer element = iterator.next();
if (element == 2) {
list.remove(element);
}
}
在上面的示例代码中,我们在迭代过程中删除了元素 2,这会导致 ConcurrentModificationException 异常的抛出。为了避免出现这种情况,我们可以使用 Iterator 的 remove() 方法来进行元素的删除,或者使用线程安全的集合类,例如 CopyOnWriteArrayList。
Java中的Map接口定义了一个键值对映射的数据结构,可以通过给定的键快速查找对应的值。Map接口有很多实现类,常见的有以下几种:
HashMap:基于哈希表实现的Map,支持null键和null值,非线程安全的。
LinkedHashMap:基于哈希表和双向链表实现的Map,可以按照插入顺序或者访问顺序遍历键值对,非线程安全的。
TreeMap:基于红黑树实现的Map,键值对按照自然顺序或者自定义顺序排序,非线程安全的。
ConcurrentHashMap:线程安全的HashMap,使用分离锁来控制并发访问,支持高并发,可以通过一定的控制减小锁的竞争。
Hashtable:早期Java版本中提供的线程安全的哈希表,支持null键和null值,但是效率较低,已经被ConcurrentHashMap取代。
Properties:Hashtable的子类,用来读取和写入属性文件,通常用于读取配置文件。
除了以上这些常见的实现类,还有一些其他的实现类,比如WeakHashMap、IdentityHashMap、EnumMap等,不过它们使用的较少,一般只在特定场景下使用。
JDK7 的底层实现
在 JDK7 中,HashMap 是通过数组和链表的结合来实现的。其基本思路是:将 key 通过哈希函数映射为数组下标,将 value 存储在对应的数组元素中。如果不同的 key 映射到了同一个数组下标,就会以链表的形式存储在该数组元素中。
HashMap 在 JDK7 中的底层结构主要由两部分组成:一个 Entry 数组和一个链表。其中,Entry 是 HashMap 的基本单元,它包含了 key、value 和指向下一个 Entry 的指针。当使用 put() 方法向 HashMap 中添加元素时,会根据 key 的哈希值计算出在数组中的位置,然后将 Entry 添加到该位置的链表中。如果两个不同的 key 哈希值相同,那么它们会被放到同一个链表中,形成一个链表结构。这就是 JDK7 中 HashMap 的基本实现原理。
然而,这种实现方式有一个严重的问题:当链表过长时,查询效率会大大降低,因为需要遍历整个链表才能找到对应的元素。在极端情况下,当所有的元素都映射到了同一个数组下标,HashMap 的时间复杂度就会退化到 O(n),这就是所谓的哈希冲突问题。
JDK8 的底层实现
JDK8 中的 HashMap 对 JDK7 中的实现进行了优化,主要是通过引入红黑树来解决链表过长的问题。当链表长度超过一定阈值时(默认为 8),链表就会转换为红黑树。这样,在查询时,如果在链表中需要遍历的节点数量超过了阈值,就会使用红黑树进行快速查找,从而提高了查询的效率。
在 JDK8 中,HashMap 的底层结构主要由三部分组成:一个数组、一个链表和一个红黑树。当使用 put() 方法向 HashMap 中添加元素时,如果对应数组下标上已经存在元素,就会进行以下操作:
如果该元素是一个链表,就将新元素追加到链表的末尾。
如果该元素是一个红黑树,就在树中查找 key 对应的节点,然后将节点的 value 替换成新的 value。如果树中不存在对应的节点,就将新元素添加到树中。
如果该元素为 null,就直接在该数组的位置插入新的 Entry。
在 JDK8 中,HashMap 的 get() 方法的实现方式也发生了变化。在查询时,先根据 key 的哈希值计算出在数组中的位置,然后判断该位置上的元素是否为 null。如果为 null,则返回 null;如果不为 null,则判断该元素是链表还是红黑树。如果是链表,则遍历链表寻找对应的元素;如果是红黑树,则在树中进行查找。
JDK8 中 HashMap 的优化主要体现在两个方面:
引入红黑树,解决链表过长的问题,提高了查询效率。当链表长度超过一定阈值时,将链表转换为红黑树,避免了链表过长时查询效率下降的问题。
除了对链表和红黑树的优化之外,JDK 8 还对哈希函数进行了改进。在 JDK 8 中,对于 key 的 hash 值,不再采用传统的取模运算(%)计算哈希桶的索引,而是采用了一种新的方式,使用 key 的 hash 值高位和低位进行异或运算,以此来增加哈希桶的分布性。这种新的方式能够更好地抵抗哈希冲突,从而提高了 HashMap 的性能。
HashMap 将插入元素时使用的方式从头插法改为了尾插法,更好地支持并发操作。在多线程环境下,头插法容易导致多线程竞争同一个桶位,从而导致链表成环。成环后会导致链表转换成红黑树的操作失败,进而影响整个 HashMap 的性能。而尾插法不会导致链表成环,因此在多线程环境下更为安全。
总的来说,JDK8 中 HashMap 的底层实现相比于 JDK7 发生了较大的变化,通过引入红黑树和优化哈希算法,提高了 HashMap 的性能和稳定性。
HashSet 是基于 HashMap 实现的,底层是一个 HashMap 对象。在 HashSet 中,所有元素都是存储在一个 HashMap 的键上,而这个键的值则是一个静态的 Object 常量(通常是一个 dummy Object)。因此,HashSet 的实现过程可以简单概括为将所有元素作为 HashMap 的 key 存储,而 value 为一个静态的 Object 对象。
具体来说,HashSet 就是在 HashMap 的基础上去掉了 value,只保留了 key。在使用 HashSet 时,我们只需要调用 HashMap 的 put() 方法,把元素作为 key 插入 HashMap 中,value 则使用一个常量对象(例如 private static final Object PRESENT = new Object())来占位即可。
相比于 HashMap,HashSet 的实现过程更为简单,因为它只需要存储键而不需要存储值。因此,HashSet 在大多数情况下比 HashMap 更加高效。同时,由于 HashSet 也是基于 HashMap 实现的,因此它们的底层实现也非常相似,可以复用 HashMap 的很多特性。
以下是 HashSet 的部分源码:
public class HashSet
extends AbstractSet
implements Set, Cloneable, java.io.Serializable
{
// HashSet 底层就是一个 HashMap,所有元素作为 key 存储在 HashMap 中
private transient HashMap map;
// 常量对象,用于占位
private static final Object PRESENT = new Object();
public HashSet() {
map = new HashMap<>();
}
public boolean add(E e) {
return map.put(e, PRESENT)==null;
}
}
可以看到,在 HashSet 中,我们只需要调用 HashMap 的 put() 方法来将元素插入到 HashMap 中。这样做的好处是可以节省很多重复代码,而且可以复用 HashMap 的很多特性。同时,由于 HashSet 只存储键而不存储值,因此在大多数情况下比 HashMap 更加高效。