【Java】Java集合容器总结
Java集合容器总结
东瓜发布于 2 月 9 日
集合与数组的区别
- 数组长度固定
- 集合有自动扩容机制,长度可变
- 数组可以存储基本数据类型和引用数据类型,前者存值,后者存地址值
- 集合只能存储引用数据类型
- 使用场景:元素个数固定用数组,不固定用集合
常用的集合类
Map接口和Collection接口是所有集合类接口的父接口:
Collection:
List:有序(元素存入集合的顺序与取出集合的顺序一致)、可重复、可通过下标访问
- ArrayList:底层是数组,查找快、增删慢
- LinkedList:底层是链表,增删快、查找慢
- Vector:底层是数组,线程安全
Set:无序、不可重复、不可通过下标访问
- HashSet:底层是单列HashMap
- TreeSet:由单列TreeMap实现,内部是红黑树结构,可对存入元素进行排序
- LinkedHashSet:底层通过LinkedHashMap实现
Map:
Map是一个键值对集合,存储键值对之间的映射。key无序,唯一。值不要求有序,可以重复
- HashMap:底层由数组和链表组成,数组是主体,链表是为了解决Hash冲突,jdk1.8后,当链表的长度大于阈值8时,会转换为红黑树
- HashTable:底层由数组和链表组成,线程安全
- LikedHashMap:继承自HashMap,在HashMap的基础上维护了一个双向链表来记录元素添加的顺序,使得遍历的结果遵从插入的顺序,所以LinkedHashMap是有序的
- TreeMap:底层是红黑树,每个键值对是红黑树的一个节点,按键进行排序
集合类结构图:
集合的容量与扩容
初始容量 | 扩容数 | 加载因子 | |
---|---|---|---|
ArrayList | 10 | 0.5 | 1 |
Vector | 10 | 1 | 1 |
HashMap | 16 | 1 | 0.75 |
HashTable | 16 | 1 | 0.75 |
实现原理
LinkedHashMap
继承自HashMap,在HashMap的基础上维护了一个双向链表来记录元素添加的顺序,使得遍历的结果遵从插入的顺序,所以LinkedHashMap是有序的。
由于是双向链表所以要维护prev和next指针,所以这里创建一个Node的子类LinkedNode,而且LinkedHashMap类中还要维护first和last
public class LinkedHashMap<K, V> extends HashMap<K, V> {private LinkedNode<K, V> first;
private LinkedNode<K, V> last;
private static class LinkedNode<K, V> extends Node<K, V> {
LinkedNode<K, V> prev;
LinkedNode<K, V> next;
public LinkedNode(K key, V value, Node<K, V> parent) {
super(key, value, parent);
}
}
}
LinkedHashMap是继承HashMap的,添加元素使用HashMap的添加逻辑即可,然后在节点创建时维护节点的next和prev即可。在HashMap中定义一个createNode()方法,LinkedHashMap中实现如下:
/*** 添加元素时维护链表中元素的next和prev属性
*/
@Override
protected Node<K, V> createNode(K key, V value, Node<K, V> parent) {
LinkedNode<K, V> node = new LinkedNode(key, value, parent);
if (first == null)
first = last = node;
else {
last.next = node;
node.prev = last;
last = node;
}
return node;
}
HashSet
HashSet由单列的HashMap实现,HashMap在使用自定义的对象的作为key时,必须重写对象的hashCode和equals方法
TreeMap、TreeSet
- TreeMap实现了SortedMap接口,它是有序的。它的内部是一个红黑树结构,每个key-value是红黑树的一个节点。
- 如果在调用TreeMap的构造函数时没有指定比较器,则根据key执行自然排序。如果指定了比较器则按照比较器来进行排序。
- TreeMap使用自定义对象作为key时,类必须实现Comparable<>接口,并重写compareTo()方法(也可在创建集合对象时传入Comparator比较器,并重写compare()方法),使其按照指定规则排序。
HashMap
几种线程安全的Map解析
HashTable:
HashTable的get/put方法都被synchronized关键字修饰,说明它们是方法级别阻塞的,它们占用共享资源锁,所以导致同时只能一个线程操作get或者put,而且get/put操作不能同时执行,所以这种同步的集合效率非常低,一般不建议使用这个集合
SynchronizedMap:
private Map<String, Object> map = Collections.synchronizedMap(new HashMap<String, Object>());
这种是直接使用工具类里面的方法创建SynchronizedMap,把传入进行的HashMap对象进行了包装同步而已,实现方式是加了个对象锁,每次对HashMap的操作都要先获取这个对象锁才能进入,所以性能也不会比HashTable好到哪里去
ConcurrentHashMap:
最推荐使用的线程安全的Map,实现方式最复杂, jdk8之前是使用分段加锁的方式,分成16个桶,每次只加锁其中一个桶,而在jdk8又加入了红黑树和CAS算法来实现。
一些面试题
如何边遍历边移除 Collection 中的元素?
边遍历边修改 Collection 的唯一正确方式是使用 Iterator.remove() 方法,如下:
Iterator<Integer> it = list.iterator();while(it.hasNext()){
*// do something*
it.remove();
}
一种最常见的错误代码如下:
for(Integer i : list){list.remove(i)
}
Iterator 和 ListIterator 有什么区别?
- Iterator 可以遍历 Set 和 List 集合,而 ListIterator 只能遍历 List。
- Iterator 只能单向遍历,而 ListIterator 可以双向遍历(向前/后遍历)。
- ListIterator 实现 Iterator 接口,然后添加了一些额外的功能,比如添加一个元素、替换一个元素、获取前面或后面元素的索引位置。
遍历一个 List 有哪些不同的方式?每种方法的实现原理是什么?Java 中 List 遍历的最佳实践是什么?
遍历方式有以下几种:
- for 循环遍历,基于计数器。在集合外部维护一个计数器,然后依次读取每一个位置的元素,当读取到最后一个元素后停止。
- 迭代器遍历,Iterator。Iterator 是面向对象的一个设计模式,目的是屏蔽不同数据集合的特点,统一遍历集合的接口。Java 在 Collections 中支持了 Iterator 模式。
- foreach 循环遍历。foreach 内部也是采用了 Iterator 的方式实现,使用时不需要显式声明 Iterator 或计数器。优点是代码简洁,不易出错;缺点是只能做简单的遍历,不能在遍历过程中操作数据集合,例如删除、替换。
最佳实践:Java Collections 框架中提供了一个 RandomAccess 接口,用来标记 List 实现是否支持 Random Access。
- 如果一个数据集合实现了该接口,就意味着它支持 Random Access,按位置读取元素的平均时间复杂度为 O(1),如ArrayList。
- 如果没有实现该接口,表示不支持 Random Access,如LinkedList。
推荐的做法就是,支持 Random Access 的列表可用 for 循环遍历,否则建议用 Iterator 或 foreach 遍历。
说一下 ArrayList 的优缺点
ArrayList的优点如下:
- ArrayList 底层以数组实现,支持以下标访问,查找效率高。
- ArrayList 在做顺序添加时,直接在数组尾部添加元素,效率也高。
ArrayList 比较适合顺序添加、随机访问的场景。
ArrayList的插入分三种情况,从首位插入,中间位置插入,尾部插入。线性表的插入删除操作都是通过移动来实现的,由于数组长度固定不变,插入数据时,需要一个新的数组。
- 首位插入时,先将新的数据放入到新的数组内,然后将原始数组中的数据复制到新的数组。
- 中间位置插入,先将插入位置前面的数据先放到新的数组里,再放新的数据,再复制旧的数据完成添加
- 尾部插入,由于不会影响其他元素,因此会直接插入到后面。
如何实现数组和 List 之间的转换?
- 数组转 List:使用 Arrays. asList(array) 进行转换。
- List 转数组:使用 List 自带的 toArray() 方法。
ArrayList 和 LinkedList 的区别是什么?
- ArrayList底层由数组实现,查找快,增删慢
- LinkedList底层由链表实现,增删快,查找慢
查找较多,或只需顺序遍历时,推荐使用ArrayList。在插入和删除操作较多时,推荐使用 LinkedList。
ArrayList 和 Vector 的区别是什么?
- ArrayList线程不安全,扩容时每次只扩0.5倍
- Vector线程安全,扩容时每次扩1倍
阅读 9更新于 今天 10:04
本作品系原创,采用《署名-非商业性使用-禁止演绎 4.0 国际》许可协议
东瓜
12 声望
0 粉丝
东瓜
12 声望
0 粉丝
宣传栏
目录
集合与数组的区别
- 数组长度固定
- 集合有自动扩容机制,长度可变
- 数组可以存储基本数据类型和引用数据类型,前者存值,后者存地址值
- 集合只能存储引用数据类型
- 使用场景:元素个数固定用数组,不固定用集合
常用的集合类
Map接口和Collection接口是所有集合类接口的父接口:
Collection:
List:有序(元素存入集合的顺序与取出集合的顺序一致)、可重复、可通过下标访问
- ArrayList:底层是数组,查找快、增删慢
- LinkedList:底层是链表,增删快、查找慢
- Vector:底层是数组,线程安全
Set:无序、不可重复、不可通过下标访问
- HashSet:底层是单列HashMap
- TreeSet:由单列TreeMap实现,内部是红黑树结构,可对存入元素进行排序
- LinkedHashSet:底层通过LinkedHashMap实现
Map:
Map是一个键值对集合,存储键值对之间的映射。key无序,唯一。值不要求有序,可以重复
- HashMap:底层由数组和链表组成,数组是主体,链表是为了解决Hash冲突,jdk1.8后,当链表的长度大于阈值8时,会转换为红黑树
- HashTable:底层由数组和链表组成,线程安全
- LikedHashMap:继承自HashMap,在HashMap的基础上维护了一个双向链表来记录元素添加的顺序,使得遍历的结果遵从插入的顺序,所以LinkedHashMap是有序的
- TreeMap:底层是红黑树,每个键值对是红黑树的一个节点,按键进行排序
集合类结构图:
集合的容量与扩容
初始容量 | 扩容数 | 加载因子 | |
---|---|---|---|
ArrayList | 10 | 0.5 | 1 |
Vector | 10 | 1 | 1 |
HashMap | 16 | 1 | 0.75 |
HashTable | 16 | 1 | 0.75 |
实现原理
LinkedHashMap
继承自HashMap,在HashMap的基础上维护了一个双向链表来记录元素添加的顺序,使得遍历的结果遵从插入的顺序,所以LinkedHashMap是有序的。
由于是双向链表所以要维护prev和next指针,所以这里创建一个Node的子类LinkedNode,而且LinkedHashMap类中还要维护first和last
public class LinkedHashMap<K, V> extends HashMap<K, V> {private LinkedNode<K, V> first;
private LinkedNode<K, V> last;
private static class LinkedNode<K, V> extends Node<K, V> {
LinkedNode<K, V> prev;
LinkedNode<K, V> next;
public LinkedNode(K key, V value, Node<K, V> parent) {
super(key, value, parent);
}
}
}
LinkedHashMap是继承HashMap的,添加元素使用HashMap的添加逻辑即可,然后在节点创建时维护节点的next和prev即可。在HashMap中定义一个createNode()方法,LinkedHashMap中实现如下:
/*** 添加元素时维护链表中元素的next和prev属性
*/
@Override
protected Node<K, V> createNode(K key, V value, Node<K, V> parent) {
LinkedNode<K, V> node = new LinkedNode(key, value, parent);
if (first == null)
first = last = node;
else {
last.next = node;
node.prev = last;
last = node;
}
return node;
}
HashSet
HashSet由单列的HashMap实现,HashMap在使用自定义的对象的作为key时,必须重写对象的hashCode和equals方法
TreeMap、TreeSet
- TreeMap实现了SortedMap接口,它是有序的。它的内部是一个红黑树结构,每个key-value是红黑树的一个节点。
- 如果在调用TreeMap的构造函数时没有指定比较器,则根据key执行自然排序。如果指定了比较器则按照比较器来进行排序。
- TreeMap使用自定义对象作为key时,类必须实现Comparable<>接口,并重写compareTo()方法(也可在创建集合对象时传入Comparator比较器,并重写compare()方法),使其按照指定规则排序。
HashMap
几种线程安全的Map解析
HashTable:
HashTable的get/put方法都被synchronized关键字修饰,说明它们是方法级别阻塞的,它们占用共享资源锁,所以导致同时只能一个线程操作get或者put,而且get/put操作不能同时执行,所以这种同步的集合效率非常低,一般不建议使用这个集合
SynchronizedMap:
private Map<String, Object> map = Collections.synchronizedMap(new HashMap<String, Object>());
这种是直接使用工具类里面的方法创建SynchronizedMap,把传入进行的HashMap对象进行了包装同步而已,实现方式是加了个对象锁,每次对HashMap的操作都要先获取这个对象锁才能进入,所以性能也不会比HashTable好到哪里去
ConcurrentHashMap:
最推荐使用的线程安全的Map,实现方式最复杂, jdk8之前是使用分段加锁的方式,分成16个桶,每次只加锁其中一个桶,而在jdk8又加入了红黑树和CAS算法来实现。
一些面试题
如何边遍历边移除 Collection 中的元素?
边遍历边修改 Collection 的唯一正确方式是使用 Iterator.remove() 方法,如下:
Iterator<Integer> it = list.iterator();while(it.hasNext()){
*// do something*
it.remove();
}
一种最常见的错误代码如下:
for(Integer i : list){list.remove(i)
}
Iterator 和 ListIterator 有什么区别?
- Iterator 可以遍历 Set 和 List 集合,而 ListIterator 只能遍历 List。
- Iterator 只能单向遍历,而 ListIterator 可以双向遍历(向前/后遍历)。
- ListIterator 实现 Iterator 接口,然后添加了一些额外的功能,比如添加一个元素、替换一个元素、获取前面或后面元素的索引位置。
遍历一个 List 有哪些不同的方式?每种方法的实现原理是什么?Java 中 List 遍历的最佳实践是什么?
遍历方式有以下几种:
- for 循环遍历,基于计数器。在集合外部维护一个计数器,然后依次读取每一个位置的元素,当读取到最后一个元素后停止。
- 迭代器遍历,Iterator。Iterator 是面向对象的一个设计模式,目的是屏蔽不同数据集合的特点,统一遍历集合的接口。Java 在 Collections 中支持了 Iterator 模式。
- foreach 循环遍历。foreach 内部也是采用了 Iterator 的方式实现,使用时不需要显式声明 Iterator 或计数器。优点是代码简洁,不易出错;缺点是只能做简单的遍历,不能在遍历过程中操作数据集合,例如删除、替换。
最佳实践:Java Collections 框架中提供了一个 RandomAccess 接口,用来标记 List 实现是否支持 Random Access。
- 如果一个数据集合实现了该接口,就意味着它支持 Random Access,按位置读取元素的平均时间复杂度为 O(1),如ArrayList。
- 如果没有实现该接口,表示不支持 Random Access,如LinkedList。
推荐的做法就是,支持 Random Access 的列表可用 for 循环遍历,否则建议用 Iterator 或 foreach 遍历。
说一下 ArrayList 的优缺点
ArrayList的优点如下:
- ArrayList 底层以数组实现,支持以下标访问,查找效率高。
- ArrayList 在做顺序添加时,直接在数组尾部添加元素,效率也高。
ArrayList 比较适合顺序添加、随机访问的场景。
ArrayList的插入分三种情况,从首位插入,中间位置插入,尾部插入。线性表的插入删除操作都是通过移动来实现的,由于数组长度固定不变,插入数据时,需要一个新的数组。
- 首位插入时,先将新的数据放入到新的数组内,然后将原始数组中的数据复制到新的数组。
- 中间位置插入,先将插入位置前面的数据先放到新的数组里,再放新的数据,再复制旧的数据完成添加
- 尾部插入,由于不会影响其他元素,因此会直接插入到后面。
如何实现数组和 List 之间的转换?
- 数组转 List:使用 Arrays. asList(array) 进行转换。
- List 转数组:使用 List 自带的 toArray() 方法。
ArrayList 和 LinkedList 的区别是什么?
- ArrayList底层由数组实现,查找快,增删慢
- LinkedList底层由链表实现,增删快,查找慢
查找较多,或只需顺序遍历时,推荐使用ArrayList。在插入和删除操作较多时,推荐使用 LinkedList。
ArrayList 和 Vector 的区别是什么?
- ArrayList线程不安全,扩容时每次只扩0.5倍
- Vector线程安全,扩容时每次扩1倍
以上是 【Java】Java集合容器总结 的全部内容, 来源链接: utcz.com/a/114578.html
得票时间