引言:
在Java世界中,有几种常用的集合。在实际使用过程中,可以被研发人员轻松的使用,但在数据结构底层中隐含着诸多学问。
只有了解了这些概念,才能说的上是真正掌握了知识。趁着自己年轻,不负大好时光,扩宽自己的知识视野。
另外,在Redis中也涉及到了Java集合的数据结构,原理近乎一致。
Java集合(一):
一、根接口:Collection接口
Collection下属的子接口包含:List、Set、Queue(队列)、Deque(双端队列)
再细分:List(有序可重复)——>ArrayList(基于数组,但可以自动扩容)
——>LinkedList(基于双向链表)
二、ArrayList<>扩容机制:
1、初始容量为10,扩容是按照前一次容量大小的1.5倍进行扩容。
2、扩容后,将原来的数据导入新的ArrayList<>中。
三、ArrayList<>与LinkedList<>使用场景划分:
1、ArrayList是基于数组的结构,所以在处理获取某一位置上的元素或者对某一位置进行赋值性能很高。
2、LinkedList是基于双向链表的结构,在处理插入和删除操作时效率高。只需要修改链表某一位置前后的指针结构就能完成此操作。
四、ArrayList<>与LinkedList<>安全性问题:
这两种集合都不是线程安全的。所以在多线程的环境下需要注意。可以使用同步代码块的方式,利用Collections.synchronizedList
使用Collections.synchronizedList
方法来创建一个线程安全的ArrayList
List<> synchronizedList = Collections.synchronizedList(new ArrayList<>());
或者使用Collections.synchronizedList
包装LinkedList
:
List<> synchronizedList = Collections.synchronizedList(new LinkedList<>());
这样创建的列表会在每个方法上都进行同步,确保线程安全。但请注意,这可能会导致性能开销,特别是在高并发环境中。但这样在高 并发的场景下会导致性能急剧下降。
五、集合操作工具类:
Collections是集合的一个工具类,源自于java.util包下。而CollectionUtil是由hutool提供。
hutool中CollectionUtil常用的几种方法:
1. isEmpty() 检查集合是否为空
Collection<String> collection = new ArrayList<>();
boolean isEmpty = CollectionUtil.isEmpty(collection);
2. isNotEmpty() 检查集合是否非空
Collection<String> collection = new ArrayList<>();
boolean isNotEmpty = CollectionUtil.isNotEmpty(collection);
3. removeAll() 从集合中移除另一个集合中包含的元素。
Collection<String> collection = new ArrayList<>(Arrays.asList("apple", "banana", "orange"));
Collection<String> toRemove = Arrays.asList("banana", "orange");
CollectionUtil.removeAll(collection, toRemove);
4. retainAll() 保留集合中与另一个集合相同的元素,移除其他元素。
Collection<String> collection = new ArrayList<>(Arrays.asList("apple", "banana", "orange"));
Collection<String> toRetain = Arrays.asList("banana", "orange");
CollectionUtil.retainAll(collection, toRetain);
5. join() 将集合中的元素用指定的分隔符连接成字符串。
Collection<String> collection = Arrays.asList("apple", "banana", "orange");
String result = CollectionUtil.join(collection, ",");
6. toArray() 将集合转换为数组。
Collection<String> collection = Arrays.asList("apple", "banana", "orange");
String[] array = CollectionUtil.toArray(collection, String.class);
7. get() 获取集合中指定索引位置的元素。
Collection<String> collection = Arrays.asList("apple", "banana", "orange");
String element = CollectionUtil.get(collection, 1); // 返回 "banana"
六、ArrayList的常用操作:
1、创建ArrayList
ArrayList<String> list = new ArrayList<>();
2、添加元素
//末尾添加
list.add("A");
list.add("B");
list.add("C");
//指定位置添加
list.add(1, "E");
3、获取元素
String element = list.get(0);
4、循环遍历
//方法一、增强for循环
for (String begin : list) {
System.out.println(item);
}
//方法二、迭代器
Iterator<String> iterator = list.iterator();
while (iterator.hasNext()) {
String item = iterator.next();
System.out.println(item);
}
5、获取列表长度
int size = list.size();
6、由ArrayList——>Array
String[] array = list.toArray(new String[0]);
//这种写法和下面的意思是一致的,0不代表空间大小是0,而意味着创建一个与原ArrayList相同大小空间的数组。
String[] array = list.toArray(new String[list.size()]);
七、ArrayList实现了Cloneable接口:
可以使用clone()方法,但这中复制是一种浅复制。
浅复制相当于在内存中新创建一个指针来指向由来的区域。本质上就是两个指针指向一块区域,学过数据结构应该了解这部分。
深复制相当于完全复制了一份新的。与原来的毫无关联。
Java集合(二):
一、根接口:Map接口
HashMap集合:(基于Key-Value键值对的集合)HashMap的键具有唯一性。
二、HashMap线程安全性:
HashMap是线程不安全的,如果想使用线程安全的需要采用JUC下的ConcurrentHashMap。
简单讲讲ConcurrentHashMap,在JDK1.7之前,这个实际上是有多个HashMap组成,利用分段锁,将每个细小的HashMap分成段(Segment),没有将整个ConcurrentHashMap锁住。采用Segment段+HashEntry(类似链表)的结构。
而在JDK1.8之后,采用Node(相对于HashEntry多了红黑树) + CAS(节点级别) + Synchronized的结构。
CAS的含义是:为了保证数据的原子性,避免在多线程的环境下,其他的线程不能修改着个值。
线程A,假设内存中的值是10,期望值是10,更新值为11。可以将10->11。
线程B,期望值是10,此时内存为11,更新值是12。无法更新。
线程C,期望值是11,此时内存是11,更新值为12,可以将11->12.
三、HashMap工作原理:
HashMap底层数据结构:在JDK1.7之前是采用数组+链表的结构,而在JDK1.8之后采用数组+链表+红黑树的结构来解决哈希冲突。
HashMap提供了两个阈值,控制采用链表还是红黑树数据结构。**TREEIFY_THRESHOLD
**默认值为8,当桶中达到这个值会转变为红黑树。
**UNTREEIFY_THRESHOLD
**默认值为6,当桶中的小于等于这个值会将红黑树转化为链表。
数组中,每个位置叫做哈希槽。你可以想象到有16块砖头,连续并排的排列在桌子上。这每一块砖头都代表着一个槽位。
现在有了槽,就可以向里面放入元素了。之前提到了HashMap是Key-Value的结构。
第一步:根据Key的hashcode()方法计算得到hash值。
第二步:再由计算得到的hash值&(数组长度-1)=槽位。
第三步:如果此时槽位里没有元素,直接放入就ok。
第四步:如果有多个Key计算之后得到的槽位是一致的,首先调用equals()方法计算槽中是否已经存在要插入的key,如果已经存在, 则将value替换。如果没有元素的equals()是相同的,这时候需要解决hash冲突,而可以选择拉链法。向每一个砖头下面继续悬挂重 物,再将value放入其中。
当达到一定的数组容量之后,就会触发扩容,在下面会讲到。
取出元素的步骤也相对容易,基本与放入元素没什么区别:
第一步:由查询的键值的hashcode()方法计算出hash值。
第二步:再由计算得到的hash值&(数组长度-1)=槽位,计算出所存放的位置。
第三步:如果槽位里没有元素,说明HashMap中没有存放次元素,返回null。
第四步:因为HashMap中key的唯一性,所以在槽中有元素的情况下,遍历链表或者红黑树。调用key的equals()方法找到该元素,将 value取出来。
四、HashMap扩容机制:
首先,在初始化HashMap时需要指定容量大小,可以自主定义转载因子,默认是0.75。这个值是经过科学计算得到的。
假设初始容量为16(默认值),在集合容量超过了16*0.75。就会触发扩容机制,扩容的大小是初始容量的2倍,也就是32。之后将 原始HashMap中的元素转移至新的HashMap中。
在此,初始容量大小的设定也是有讲究的,为了充分的考虑HashMap的性能,在二进制中,2的N次幂只有首位的是1,剩下的位全 都是0。这样在解决哈希冲突时,就可以避免用除法取余数这种低效的方式处理,采用位运算的方式.
举个例子:
假设由key调用hashcode()方法计算出的hash值为188,二进制为1011 1100。采用hash&(length-1)得到12,二进制为0000 1100
因为length为16,二进制为0001 0000。减一后,15的二进制为0000 1111。做&运算,高位全都是0,只保留低位。
采用取余的算法,也是hash%length=12。但从效率上讲不如位运算,所以在选择初始容量大小时,尽量采取2的N次幂大小。
Map<String, Object> map = new HashMap<>(16);
五、HashMap的使用方法:
-
插入元素:
Map<String, Integer> hashMap = new HashMap<>(); hashMap.put("one", 1); hashMap.put("two", 2); hashMap.put("three", 3);
-
获取元素:
int value = hashMap.get("two"); System.out.println("Value of 'two': " + value);
-
遍历元素:
-
遍历键:
for (String key : hashMap.keySet()) { System.out.println("Key: " + key); }
-
遍历值:
for (int val : hashMap.values()) { System.out.println("Value: " + val); }
-
遍历键值对:
for (Map.Entry<String, Integer> entry : hashMap.entrySet()) { System.out.println("Key: " + entry.getKey() + ", Value: " + entry.getValue()); }
-
-
判断是否包含某个键或值:
boolean containsKey = hashMap.containsKey("two"); boolean containsValue = hashMap.containsValue(3);
-
获取键值对数量:
int size = hashMap.size();
-
删除元素:
hashMap.remove("two");
-
清空 HashMap:
hashMap.clear();
-
判断是否为空:
boolean isEmpty = hashMap.isEmpty();
-
替换元素值:
hashMap.replace("one", 10);
-
获取默认值:
int defaultValue = hashMap.getOrDefault("four", 0);
六、HashMap的序列化:
因为HashMap实现了Serializable接口,所以可以被序列化或者反序列化。在实际使用中通常是以JSON格式。在举的例子中使用 Alibaba的FastJson工具。
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.TypeReference;
import java.util.HashMap;
public class SerializeHashMapToFastjson {
public static void main(String[] args) {
// 创建一个HashMap
HashMap<String, Integer> hashMap = new HashMap<>();
hashMap.put("One", 1);
hashMap.put("Two", 2);
hashMap.put("Three", 3);
try {
// 将HashMap对象序列化为JSON字符串
String jsonString = JSON.toJSONString(hashMap);
System.out.println("Serialized HashMap to JSON: " + jsonString);
// 将JSON字符串反序列化为HashMap对象
HashMap<String, Integer> deserializedHashMap = JSON.parseObject(jsonString, new TypeReference<HashMap<String, Integer>>() {});
System.out.println("Deserialized HashMap from JSON: " + deserializedHashMap);
} catch (Exception e) {
e.printStackTrace();
}
}
}
七、延展问题:
equals相等hashcode是否相等,反过来又是如何:
先说下答案:equals相等hashcode值一定相同,但是hashcode相同equals不一定是相等的。
这个问题需要必须要理解上面所描述的hashmap工作原理,如果不理解那这个问题也不会理解。
原因如下:
hashcode决定的是数据需要放在哪一个hash槽中,hashcode如果相同,说明两个元素处于同一个槽内。但是hashmap为了解决冲突,采用拉链法来避免这种情况,而且hashmap的key具有唯一性,所以即便是在一个槽内,由于槽内还有别的元素,所以equals是不一定相等的。
equals相等的话,说明两个元素就是一个东东,那么在hashmap中的位置一定是在一个hash槽内,所以hashcode值一定是相同的。