前言
总结Java集合相关知识,希望能帮助兄弟们,💪O(∩_∩)O哈哈~
Java集合基础知识总结
Java集合框架提供了一套用于保存对象和数据的标准接口和实现。主要包含以下几个部分:
- List:有序集合,可以包含重复元素。主要实现有ArrayList、LinkedList和Vector。
- Set:无序集合,不可以包含重复元素。主要实现有HashSet、LinkedHashSet和TreeSet。
- Map:键值对集合,键是无序的,值可以重复。主要实现有HashMap、LinkedHashMap、TreeMap和Hashtable。
- Queue:队列,先进先出。主要实现有PriorityQueue、ArrayDeque和LinkedList。
- Collection:集合框架最基本的接口,定义了集合框架中的公共接口方法。
集合框架主要特点: - 容器性:可以保存多个对象和数据。
- 遍历性:可以遍历容器中的所有元素。
- 有序性:List集合的元素是有序的,Set和Map集合的元素是无序的。
- 唯一性:Set集合的元素是唯一的,List和Map集合的元素可以重复。
- 同步性:Vector、Hashtable和Stack这些集合是同步的,线程安全的。其他的集合都是非同步的。
- 快速查找:通过HashMap和HashSet可以快速查找元素。
- 增删改查:可以对集合进行增加、删除、修改、查询操作。
重要接口和实现类:- Collection:集合框架最基本的接口
- List:有序集合接口,实现类ArrayList、LinkedList、Vector
- Set:无序集合接口,实现类HashSet、LinkedHashSet、TreeSet
- Map:键值对集合接口,实现类HashMap、LinkedHashMap、TreeMap、Hashtable
- Queue:队列接口,实现类PriorityQueue、ArrayDeque
List遍历删除元素remove()
在遍历List的时候直接remove()元素会出现ConcurrentModificationException。这是因为remove()方法会改变List的大小,而遍历List的时候是通过索引来访问元素的,索引正在变化,就会抛出该异常。
解决方法有两种:
- 使用Iterator遍历,在遍历的时候删除元素:
List<Integer> list = Arrays.asList(1, 2, 3, 4, 5); Iterator<Integer> iterator = list.iterator(); while (iterator.hasNext()) { Integer num = iterator.next(); if (num == 3) { iterator.remove(); } }
- 使用 list.remove() 方法的时候,意识到索引在变化,所以从后往前删除:
List<Integer> list = Arrays.asList(1, 2, 3, 4, 5); for (int i = list.size() - 1; i >= 0; i--) { if (list.get(i) == 3) { list.remove(i); } }
除此之外,常见的List集合还有:
- ArrayList: 动态数组,查询快,增删慢。
- LinkedList: 双向链表,查询慢,增删快。
- Vector: 与ArrayList类似,线程安全,查询快,增删慢。
- Stack: 栈,FILO,线程安全。
- HashSet: 无序不重复集合。
- LinkedHashSet: 有序不重复集合。
- TreeSet: 红黑树,有序集合。
List<T>和List<?>的区别
List遍历删除元素remove()需要注意:
- 使用Iterator遍历删除元素时, iterator.remove() 方法会删除迭代器刚遍历过的元素。这是合法的。
- 如果直接使用list.remove(i) 删除元素,会导致索引i之后的元素全部减小1,这会跳过某些元素不遍历到。
解决办法是使用iterator遍历删除:List<Integer> list = Arrays.asList(1, 2, 3, 4, 5); Iterator<Integer> it = list.iterator(); while (it.hasNext()) { Integer num = it.next(); if (num == 2) it.remove(); }
List
和List<?>的区别: - List
是一个强类型的List,T代表某种具体的类型,比如List 。 - List<?>是一个无界通配符的List,表示List中元素类型不确定,可以保存任何类型的元素。
- 主要区别在于:
- List
可以添加T类型的元素,也可以读出T类型的元素。 - List<?>只能添加null元素,读取时返回Object类型。
- List<? extends T>可以添加T类型及T子类型的元素,读取时返回T类型。
- List<? super T>可以添加T类型及T父类型的元素,读取时返回Object类型。
所以通常我们会用List<?>表示不关心集合元素类型,只要获取元素就可以了。List更加严格,要求元素类型一致。
List用于生产者,因为生产者清楚要添加什么类型元素。
List<?>更适用于消费者,因为消费者只关心获取元素,不关心元素的具体类型。Java.lang.Character类详解
集合概述:
集合类可分为Collection和Map两大类。Collection存储多个对象,Map存储键值对。
List遍历删除元素remove():
List支持随机访问,可以通过索引直接访问元素。遍历List时删除元素需要注意,因为remove会改变List的大小,会影响后续元素的索引。
正确方式:for (int i = 0; i < list.size(); i++) { if (满足条件) { list.remove(i); i--; // i--很重要,因为remove后索引会变化 } }
错误方式:
for (int i = 0; i < list.size(); i++) { if (满足条件) { list.remove(i); } } // 这种方式会跳过某些元素
List
和List<?>的区别: - List
是一个原始类型列表,存储相同类型的元素。 - List<?>是一个通配符列表,可以存储任何类型的元素。
- List
可以添加T类型的元素,List<?>只能添加null元素。 - List
可以获取T类型的元素,List<?>只能获取Object类型的元素。
Character类详解:
Character类对char类型做了很好的封装,提供了许多方法: - isLetter() 判断是否为字母
- isDigit() 判断是否为数字
- isWhitespace() 判断是否为空白字符
- isUpperCase() 判断是否为大写字母
- isLowerCase() 判断是否为小写字母
- toUpperCase() 转为大写字母
- toLowerCase() 转为小写字母
- getNumericValue() 获取字符的数值,如'A'的数值是65
- 比较字符:compareTo()方法
- 转义字符:'\t'、'\n' 等
Character还定义了许多常量,如: - Character.MIN_VALUE 最小值'\u0000'
- Character.MAX_VALUE 最大值'\uffff'
- Character.MIN_RADIX 最小进制数2
- Character.MAX_RADIX 最大进制数36
List遍历删除元素remove():
在迭代List时,如果进行元素的remove()/add()操作,会抛出ConcurrentModificationException。
这是因为迭代器在创建时会记住列表的modCount(修改次数),若列表在迭代过程中被修改,则modCount会改变,从而抛出异常。
解决方法是使用迭代器的remove()方法,或使用ListIterator。
List和List<?>的区别:
List是具体的范型列表,T代表具体的类型,如List 。
List<?>是非具体的范型列表,代表某种未知的类型,通常用于作为方法的参数类型。
Java.lang.Character类详解:
Character类用于对单个字符进行操作。
主要方法:
isLetter()、isDigit()、isWhitespace()、isUpperCase()、isLowerCase() - 判断字符的类型
toUpperCase()、toLowerCase() - 字母的大小写转化
getValue() - 返回字符的ASCII值HashMap + 软引用进行缓存
利用HashMap和SoftReference可以实现一个简单的缓存机制。SoftReference内部维护一个弱引用,允许GC回收其所引用的对象。
实现方法:Map<String, SoftReference<Object>> cache = new HashMap<>();
- List
Object obj = cache.get(key).get();
if (obj == null) {
obj = createObj(); // 创建新的对象
cache.put(key, new SoftReference<>(obj));
}
这样,当内存不足时,GC会回收SoftReference所引用的对象,从而实现缓存的回收。
# System.arraycopy详解
System.arraycopy()方法用于复制数组中的元素。该方法的签名为:
```java
public static void arraycopy(Object src, int srcPos, Object dest, int destPos, int length)
参数说明:
- src:源数组
- srcPos:源数组中的起始位置
- dest:目的数组
- destPos:目的数组中的起始位置
- length:要复制的元素个数
注意:- src和dest应该是同类型或者可以进行转换类型的数组。
- srcPos和destPos表示要复制的起始位置,length表示要复制的长度。
- 如果src和dest有重叠,则dest的元素会被源元素覆盖。
- 该方法执行快速的数组元素复制,比手动for循环复制更加高效。
例子:int[] src = {1, 2, 3, 4}; int[] dest = {5, 6, 7, 8}; System.arraycopy(src, 0, dest, 1, 2); // dest is now {5, 1, 2, 8}
该方法对于合并两个数组,修改数组元素顺序等场景都非常有用。
总结:
System.arraycopy()用于在数组内部或者数组之间复制数据元素。该方法执行速度快,是数组元素复制的最佳选择。Java队列Queue
Queue接口表示一个线性集合,按照“先进先出”的规则来组织元素。常用实现类有:
- LinkedList:底层使用链表结构,线程不安全,可以用于栈和队列。
- ArrayBlockingQueue:基于数组结构,线程安全,可以用于生产者消费者场景。
- PriorityQueue:基于优先堆结构,线程不安全,可以用于实现优先级队列。
- DelayQueue:基于优先堆结构,线程安全,可以用于定时任务调度。
Queue接口中的主要方法有: - add(e): 将元素e添加到队列尾部。
- offer(e): 将元素e添加到队列尾部。
- remove(): 移除队列头部的元素并返回。
- poll(): 移除队列头部的元素并返回。
- element(): 获取队列头部的元素。
- peek(): 获取队列头部的元素。
add()和offer()的区别在于: - add(e): 如果添加失败则报IllegalStateException。
- offer(e): 如果添加失败则返回false,不抛出异常。
remove()和poll()的区别在于: - remove(): 如果队列为空则报NoSuchElementException。
- poll(): 如果队列为空则返回null,不抛出异常。
element()和peek()的区别在于: - element(): 如果队列为空则报NoSuchElementException。
- peek(): 如果队列为空则返回null,不抛出异常。
使用示例:Queue<Integer> queue = new LinkedList<>(); queue.add(1); queue.add(2); queue.add(3); queue.element(); // 1 queue.remove(); // 1 queue.poll(); // 2 queue.peek(); // 3
Queue接口为FIFO(先进先出)的数据结构提供了一套标准的接口规范,我们在实际开发中根据需要选择不同的实现类即可。
Java中Queue和Deque的区别
Queue和Deque都是Java集合框架中的队列结构,主要区别在于:
Queue: 队列,只有两端操作,一端插入(add),一端删除(remove)。实现类有LinkedList,ArrayBlockingQueue等。
Deque: 双端队列,两端都可以插入(addFirst,addLast)和删除(removeFirst,removeLast)。实现类有LinkedList,ArrayDeque等。
相同点: - 都遵循先进先出(FIFO)的原则
- 都是线程安全的
不同点: - Queue只有两端操作,Deque两端都可以操作。
- Queue插入和删除只在一端,Deque可以在两端同时进行插入和删除操作。
- Queue没有size的最大值限制,Deque可以设置最大值。
示例代码:
Queue:Queue<Integer> queue = new LinkedList<>(); queue.add(1); // [1] queue.add(2); // [1, 2] queue.remove(); // 1 [2] queue.remove(); // 2 []
Deque:
Deque<Integer> deque = new LinkedList<>(); deque.addFirst(1); // [1] deque.addLast(2); // [1, 2] deque.removeFirst(); // 1 [2] deque.removeLast(); // 2 []
Queue和Deque都是线性集合,遵循FIFO规则。但Deque更加灵活,可以在两端同时进行插入和删除操作。根据具体需求选择Queue或者Deque。
Java中的ConcurrentHashMap中为什么不能存储null?
ConcurrentHashMap不能存储null键值对的原因主要有:
- null键会导致拉链冲突,因为hash算法结果都一样。ConcurrentHashMap使用分段锁技术来实现线程安全,不同的key会定位到不同的segment上。如果有两个线程都要操作null键的值,那么会导致线程不安全。
- AbstractMap中的keySet(), entrySet(), values()方法返回的视图,包含null会导致NullPointerException。ConcurrentHashMap继承自AbstractMap,所以也会有这个问题。
- 计算hash值时,公共的hash算法对null运算结果都是0,所以null键的元素会聚集到table[0]上,导致哈希冲突。ConcurrentHashMap使用拉链法来解决哈希冲突,但过多的冲突会导致性能下降。
这里举个例子说明第一点:ConcurrentHashMap<String, Integer> map = new ConcurrentHashMap<>(); Thread t1 = new Thread() { public void run() { map.put(null, 1); } }; Thread t2 = new Thread() { public void run() { map.put(null, 2); } }; t1.start(); t2.start();
这里t1和t2线程都要插入null键的值,但由于ConcurrentHashMap使用分段锁,null键必定会定位到同一个segment上,所以会产生线程安全问题。
解决方法是: - 使用其他的Map实现,如Hashtable, HashMap。这两个实现都是线程不安全的,但可以存储null键值对。
- 使用ConcurrentHashMap,但不存储null键值对。可以用一个默认的初始化值来表示null键。
- 自己实现一个线程安全的可以存储null键值对的Map。
所以总结来说,ConcurrentHashMap之所以不可以存储null键值对,主要是出于线程安全考虑,以及hash算法的特点。在实际使用中需要根据业务场景选择合适的Map实现。