Java集合框架是Java提供的一组用于存储和操作数据的类和接口。其中,ArrayList、LinkedList和HashMap、HashTable是常用的集合容器,它们在不同的场景中具有重要性和广泛应用。
ArrayList内部使用数组作为数据存储结构,数组可以通过下标直接访问元素的内存地址,因此访问的效率很高。通过下标访问元素的时间复杂度为O(1),即常数时间。
数组是一块连续的内存空间,数组建立后就无法修改数组长度。ArrayList为了能够动态扩容数组,在每次扩容时都创建一个新的数组,再把旧的数组中的元素添加到新数组中。通过不断更换更大容量的数组来实现动态数组。
在数组容量不足时,需要创建更大的新数组,这个过程叫做“扩容”。
一开始创建ArrayList对象,并为添加元素时,ArrayList的容量为0。再添加第一个元素后,初始容量变为默认的10,也就是可以存储下标0-9的的数据。
即使可以存10个元素,在存储数据时也要按下标顺序存,不允许按照下标跳着存数据,例如数组为空,直接在下标1存数据,不是从下标0开始存数据,这样会报错。
什么时候扩容?
每次添加元素都会判断是否需要扩容,如果需要,会先扩容,再插入元素。扩容时,新数组的容量是原数组容量的1.5倍。
随机访问高效
由于ArrayList使用数组作为内部数据结构,可以通过下标直接访问元素的内存地址,因此随机访问的效率很高。通过索引访问元素的时间复杂度为O(1),即常数时间。
查询和修改操作
ArrayList提供了丰富的查询和修改操作方法。可以根据索引查询元素,也可以根据索引修改元素。这使得ArrayList在需要频繁访问和修改元素的场景中非常适用。
因为底层数据结构是数组,在操作数组时会有些步骤,下图是场景操作的步骤,数组的优点是按下标读取快,但是操作数据就有些复杂。
下面是一个使用ArrayList的示例代码:
import java.util.ArrayList;
public class ArrayListExample {
public static void main(String[] args) {
// 创建ArrayList
ArrayList<String> list = new ArrayList<>();
// 添加元素
list.add("苹果");
list.add("香蕉");
list.add("橙子");
// 遍历元素
for (String fruit : list) {
System.out.println(fruit);
}
// 根据索引访问元素
String firstFruit = list.get(0);
System.out.println("第一个水果:" + firstFruit);
// 根据索引修改元素
list.set(1, "草莓");
System.out.println("修改后的水果列表:" + list);
}
}
LinkedList基于双向链表实现,可以高效地进行插入和删除操作,并提供了遍历和索引操作的能力。内部使用双向链表作为数据存储结构。每个Node(节点)包含一个元素和两个指针,分别指向前一个节点和后一个节点。通过这种结构,LinkedList可以在O(1)的时间复杂度内进行插入和删除操作。
插入和删除操作高效
由于LinkedList使用双向链表结构,插入和删除操作非常高效。在插入和删除元素时,只需要修改相邻节点的指针,不需要像数组那样进行元素的移动和复制。
遍历和索引操作
LinkedList可以通过遍历访问每个元素,也可以通过索引访问特定位置的元素。但是,由于链表没有数组那样的随机访问能力,索引操作的效率较低,并且遍历建议不要使用for循环,使用迭代器效率更高。
示例代码:
import java.util.LinkedList;
public class LinkedListExample {
public static void main(String[] args) {
// 创建LinkedList
LinkedList<String> list = new LinkedList<>();
// 添加元素
list.add("苹果");
list.add("香蕉");
list.add("橙子");
// 遍历元素
for (String fruit : list) {
System.out.println(fruit);
}
// 在指定位置插入元素
list.add(1, "草莓");
System.out.println("插入后的水果列表:" + list);
// 删除指定位置的元素
list.remove(2);
System.out.println("删除后的水果列表:" + list);
}
}
HashMap是基于哈希表实现的集合容器,具有高效的键值存储和查找能力。它适用于需要高效存储和查找键值对的场景,例如缓存数据、数据索引和键值对存储等。通过使用HashMap,我们可以方便地操作和管理键值对,提高代码的效率和灵活性。
HashMap内部使用哈希表作为数据存储结构。哈希表由一个数组和链表组成。数组的每个元素称为桶(bucket),每个桶可以存储一个或多个键值对。通过哈希函数将键映射到桶的索引(数组的下标),实现快速的存储和查找。
由于哈希函数的映射不是一对一的,可能会出现多个键映射到同一个桶的情况,称为散列冲突。
jdk1.8版本之前单纯使用链表解决哈希冲突,1.8以后使用链表+红黑树来解决。当链表长度超过阈值8时,链表会转换为红黑树,提高查找效率。当红黑树节点个数低于6时,又转变会链表结构。
HashMap的底层是数组,数组无法修改长度,本质也是创建一个新数组,再把数据挪进去。
什么时候需要扩容?
根据泊松算法,当数组的数据存储达到容量的70%-80%左右,产生哈希冲突的概率会大大增加,所以HashMap设置了一个负载因子——0.75,也就是说当数组的数据量达到容量的75%后就该马上扩容了。HashMap在每次插入新数据的时候都会判断是否需要扩容,和ArrayList扩容时间一样。最大扩到2的31次方-1(int类型最大值)。
HashMap的扩容流程如下:
HashMap初始容量是16,当元素数量达到负载因子(0.75)与容量的乘积时,即元素数量超过了扩容阈值,需要进行扩容操作。
创建一个新的数组,其长度是原数组的两倍。新数组的长度一般选择为原数组长度的两倍,这是经验性的选择,可以在时间和空间效率之间做一个折中。
遍历原数组中的每个元素,重新计算它们在新数组中的位置,并将其移动到新数组中的对应位置。这一步是为了保持元素之间的相对顺序不变。
在移动元素时,如果发现新数组中的某个位置已经被占用,则使用链表或红黑树(在JDK 8及以后的版本中)来解决哈希冲突。这是因为在扩容后,原本哈希冲突的元素可能被分散到新数组的不同位置,需要重新处理冲突。
扩容完成后,HashMap的容量会增加为原来的两倍,并且负载因子的值保持不变。
特性 | HashMap 1.7 | HashMap 1.8 |
---|---|---|
处理哈希冲突的方式 | 链表 | 链表 + 红黑树 |
扩容机制 | 先扩容再插入 | 先插入再扩容 |
扩容后 | 先重新计算所有元素hash值,更换桶位置 | 不用重新计算hash,新的下标为原索引+原来的容量 |
HashMap适用于需要高效的键值存储和查找的场景。
import java.util.HashMap;
public class HashMapExample {
public static void main(String[] args) {
// 创建HashMap
HashMap<String, Integer> map = new HashMap<>();
// 添加键值对
map.put("苹果", 10);
map.put("香蕉", 5);
map.put("橙子", 8);
// 获取键对应的值
int appleCount = map.get("苹果");
System.out.println("苹果的数量:" + appleCount);
// 遍历键值对
for (String fruit : map.keySet()) {
int count = map.get(fruit);
System.out.println(fruit + "的数量:" + count);
}
// 判断键是否存在
boolean containsKey = map.containsKey("橙子");
System.out.println("是否包含橙子:" + containsKey);
// 删除键值对
map.remove("香蕉");
System.out.println("删除香蕉后的水果列表:" + map);
}
}
HashTable是Java中的一个集合容器,它是线程安全的数据结构。
线程安全性
HashTable是线程安全的集合容器,它在操作上提供了同步机制,保证多线程环境下的安全性。它使用synchronized关键字来实现同步,但这也导致了性能的降低。
性能
由于HashTable提供了线程安全性,它在多线程环境下的性能较差。在单线程环境下,HashMap的性能通常优于HashTable。
import java.util.Arrays;
class HashTable {
private static final int TABLE_SIZE = 10;
private Entry[] table;
HashTable() {
table = new Entry[TABLE_SIZE];
}
// 哈希函数
private int hashFunction(int key) {
return key % TABLE_SIZE;
}
// 插入键值对
public void put(int key, String value) {
int index = hashFunction(key);
Entry entry = new Entry(key, value);
if (table[index] == null) {
table[index] = entry;
} else {
Entry current = table[index];
while (current.next != null) {
current = current.next;
}
current.next = entry;
}
}
// 获取键对应的值
public String get(int key) {
int index = hashFunction(key);
Entry current = table[index];
while (current != null) {
if (current.key == key) {
return current.value;
}
current = current.next;
}
return null;
}
// 删除键值对
public void remove(int key) {
int index = hashFunction(key);
Entry current = table[index];
Entry previous = null;
while (current != null) {
if (current.key == key) {
if (previous == null) {
table[index] = current.next;
} else {
previous.next = current.next;
}
return;
}
previous = current;
current = current.next;
}
}
// 打印哈希表内容
public void printTable() {
for (int i = 0; i < TABLE_SIZE; i++) {
Entry current = table[i];
System.out.print(i + ": ");
while (current != null) {
System.out.print("(" + current.key + ", " + current.value + ") ");
current = current.next;
}
System.out.println();
}
}
// 哈希表的节点
private static class Entry {
int key;
String value;
Entry next;
Entry(int key, String value) {
this.key = key;
this.value = value;
this.next = null;
}
}
}
public class Main {
public static void main(String[] args) {
HashTable hashTable = new HashTable();
// 插入键值对
hashTable.put(1, "Value 1");
hashTable.put(11, "Value 11");
hashTable.put(21, "Value 21");
hashTable.put(2, "Value 2");
// 打印哈希表
hashTable.printTable();
// 获取键对应的值
System.out.println("Value for key 1: " + hashTable.get(1));
System.out.println("Value for key 11: " + hashTable.get(11));
// 删除键值对
hashTable.remove(11);
// 打印哈希表
hashTable.printTable();
}
}
ArrayList和LinkedList都是Java中的集合容器,它们的区别如下:
HashMap和HashTable都是Java中的集合容器,它们的区别如下:
根据这些内容,可以选择适合自己需求的集合容器,并理解它们的特点和用法。