Java集合类
Java中常用的容器
Java中的集合类主要分为两大类:Collection
接口和Map
接口.前者是存储对象的集合类,后者存储的是键值对(key-value)
Collection
接口下又分为List
,Set
,Queue
接口.每个接口有其具体实现类,以下是主要的集合类:
- List(成员之间是有序的):
ArrayList
基于动态数组,支持随机访问,查询速度快,插入/删除慢Vector
:和ArrayList
类似,但它是线程安全的.LinkedList
:基于双向链表实现,只能顺序访问,但是可以快速地在链表中间插入和删除元素.不仅如此,LinkedList
还可以用作栈,队列,和双向队列.线程不安全
- Set(成员不允许重复)
TreeSet
:基于红黑树实现,支持有序性地操作,存入的元素都是有序的HashSet
:基于Hash表实现,查找的时间复杂度为O(1),但是存入的元素是无序的,也就是说使用Iterator
遍历HashSet
得到的结果是不确定的LinkedListHashSet
:具有HashSet
的查询效率,并且内部使用的是双向链表维护元素的插入顺序(有序)
- Queue
PriorityQueue
:基于优先级堆,元素按照自然顺序或指定比较器排序LinkedList
:可以作为队列使用,支持FIFO(先进先出)
操作
- Map(以键值对存储)
HashMap
:基于Hash表实现,HashMap
的值也是没有顺序的,根据key获取到value,和HashSet
一样也具有O(1)的查找时间复杂度.HashMap
允许键值对值为NULL
,但是最多只允许一条记录key的值为null(多条会覆盖),允许多条记录的value为null.HashMap是线程不安全的TreeMap
:基于红黑树实现,把记录的key值按照升序排序,TreeMap
不允许key值为NULL,TreeMap
也是线程不安全的HashTable
:和HashMap
类似,但是它的key和value不允许是null,HashTable
是线程安全的,底层实现是使用synchronized
锁住整个HashTable
,效率非常低.–现在使用ConcurrentHashMap
来支持线程安全,ConcurrentHashMap
效率更高,jdk1.7
使用Segment分段锁机制,基于ReentrantLock
实现,jdk1.8
使用CAS+synchronized
实现,空节点插入使用CAS,有Node节点则使用synchronized
加锁LinkedHashMap
:使用双向链表来维护插入顺序,在用Iterator
遍历LinkedHashMap
时,先得到的记录肯定是先插入的,遍历的时候比HashMap
慢,key和value允许为空,线程不安全
List,Set,Map三者的区别?
Java容器分为Collention
和Map
两大类,Collection
集合的子接口有Set
,List
,Queue
三种,常用的是Set
和List
接口,Map
接口不是Collection
的子接口
List
:一个有序(元素存入和取出的顺序是一致的)容器,元素可以重复,可以插入多个null元素,元素都有索引,List
可以通过下标访问.常用的实现类有:ArrayList
,LinkedList
,Vector
Set
:一个无序(元素存入和取出顺序可能不一致)容器,不可以存储重复元素,只允许存入一个null元素,必须保证元素的唯一性,Set
不可以通过下标访问,常用的实现类有:HashSet
,TreeSet
,LinkedHashSet
Map
是一个键值对集合,存储键,值之间的映射.key无序且唯一,value不要求顺序,允许重复.Map
常用实现类有:HashMap
,TreeMap
,LinkedListMap
,HashTable
,ConcurrentHashMap
ArrayList和LinkedList的区别
- 线程安全:
ArrayList
和LinkedList
都是不同步的,都是线程不安全的 - 底层数据结构:
ArrayList
底层是使用数组来实现的,LinkedList
底层是使用双向链表来实现的 - 在插入和删除元素时:
ArrayList
是基于数组实现的,所以在插入和删除元素时,使用add(Element)方法,ArrayList
会默认尾插,这种情况下时间复杂度为O(1),但是如果是指定下标进行插入的话(add(int index,E element)),时间复杂度就为O(N- i),因为在指定下标插入的时候,第i个和第i个位置之后的(n-i)个元素都要执行向后移/前移的操作LinkedList
采用链表存储,所以插入删除时间复杂度都不受元素位置的影响,都是近似于O(1),数组是近似于O(n)
- 在随机访问时:
ArrayList
支持快速随机访问,借助下标进行访问;而LinkedList
不行- 扩容机制:
LinkedList
底层是双向链表,没有初始化大小,也没有扩容机制ArrayList
初始化如果不指定大小,初始大小为10;在使用add方法的时候,首先会调用ensureCapacityInternal
方法,传入size+1检查是否需要扩容,newCapacity = 扩充数组为原来的1.5倍;ArrayList
中copy数组的核心就是System.arraycopy
方法
- 扩容机制:
- 内存空间占用:
ArrayList
的空间浪费主要体现在List结尾需要预留一部分空间,而LinkedList
的空间浪费则体现在它的每一个元素都要消耗比ArrayList
更多的空间(存放数据和下一个节点的引用) - 如果数量有百万级以上时,还是要使用
ArrayList
,因为LinkedList
的时间主要消耗在遍历上,而ArrayList
的时间主要消耗在移动和复制上,LinkedList
的遍历速度是要慢于ArrayList
的移动复制速度的,如果需要处理的数据是百万级以上时,还是ArrayList
快
ArrayList和Array的区别?什么时候更适合用Array?
- 区别:
Array
可以容纳基本数据类型和对象,而ArrayList
只能容纳对象;Array
是指定大小的,而ArrayList
的大小是固定的;
- 什么时候适合用
Array
?- 如果数组大小已经指定,并且主要的操作是存储和遍历
- 操作基本数据类型时
- 如果要使用多维数组的时候
HashMap
HashMap的实现原理?
- jdk1.7中:拉链法–将链表与数组结合
- jdk1.8中:数组+链表/红黑树;当链表上的元素个数超过
8
个(产生hash冲突的元素)且数组长度>=64
的时候链表会自动转换成红黑树,节点会变成树节点.当链表长度小于6
的时候,就会变回链表
HashMap的put方法的执行过程
当想往一个HashMap
中添加一对key-value时,系统首先会计算key的hash值(利用HashCode方法),然后根据hash值确认在table中存储的位置。如果该位置没有元素,则直接创建一个新节点并插入;如果该位置有元素,则遍历该元素位置的链表/红黑树并依次比较key的hash值(通过equals方法),如果两个hash值相等并且value相等,则用新的Entry的value覆盖原来节点的value(更新值并返回旧值),如果value不相等,就把新节点插入到该链表(1.7是头插,1.8是尾插)
HashMap的get方法的执行过程
- 通过get(key)中的key的值找到table数组中索引处的Entry,然后返回该key对应的value
- 这里能够根据key快速的取到value,是因为
HashMap
在存储的过程中没有将key和value分开存储,而是当作一个整体key-value来处理的,这个整体就是Entry对象.在存储的过程中,系统会根据key的HashCode来决定Entry在table中的存储位置,取得过程也同样根据key的HashCode取出相对应的Entry对象(value就包含在其中)
HashMap中的get方法能否判断某个元素是否在map中?
get方法的返回值不能判断一个key是否包含在map中,因为get返回null有可能是因为map中并不包含该key,也可能是因为key对应的value值为null.(因为HashMap中允许key和value为null)
ConcurrentHashMap,HashMap,HashTable的区别
HashMap | HashTable | ConcurrentHashMap | |
---|---|---|---|
null键 | √ | × | × |
null值 | √ | × | × |
线程安全 | × | √ | √ |
效率 | 非常高 | 低 | 高 |
数据结构 | 数组+链表+红黑树 | 数组+链表 | 数组+链表+红黑树 |
同步方式 | 无 | synchronized同步方法 | 1.7版本:基于segment分段锁机制,基于ReentrantLock实现 1.8版本:基于CAS+synchronized实现,空节点插入使用CAS,有Node节点则使用synchronized加锁 |
迭代器类型 | fail-fast迭代器,在遍历时不能更新元素,否则将抛出异常 | fail-safe迭代器,基于容器的克隆,因此遍历操作时,元素的更新不影响遍历 | fail-safe迭代器,基于容器的克隆,因此遍历操作时,元素的更新不影响遍历 |
HashMap
允许键和值为null,HashTable
和ConcurrentHashMap
不允许HashMap
不保证线程安全,HashTable
和ConcurrentHashMap
是线程安全的- 对于效率问题:
- 因为
HashMap
不保证线程安全,所以效率最高,适合用于单线程场景 - 而
HashTable
是使用synchronized
修饰方法实现线程安全的,锁住整个HashTable
效率非常低; ConcurrentHashMap
则是使用CAS+synchronized实现线程安全( put方法存放元素时: 通过key对象的HashCode计算出数组的索引, 如果没有Node,则使用CAS尝试插入元素, 失败则无条件自旋直到插入成功; 如果存在Node,则使用synchronized
锁住该Node元素(链表/红黑树的头结点), 再执行插入操作)所以效率比HashTable
高,ConcurrentHashMap
已经可以完全取代HashTable
- 因为
- 对于扩容机制
HashTable
初始size为11, 扩容: newSize=oldSize*2+1HashMap
和ConcurrentHashMap
初始size都是16, 扩容为newSize=oldSize*2, size一定为2的n次幂
ConcurrentHashMap在1.7和1.8的变化?
- 在锁方面:由Segment分段锁升级为CAS+synchronized实现
- 数据结构方面:将Segment块换成了Node,每个Node独立,原来的默认并发度为16,变成了每个Node都独立,提高了并发度
- Hash冲突:1.7发生Hash冲突时用链表存储,1.8先用链表存储,如果添加元素的链表节点个数超过8个或者table数组长度大于64,就转换为红黑树存储来优化查询
- 在链表存储时:1.7是使用头插法,而1.8使用尾插法
HashMap和TreeMap的区别
HashMap
基于数组+链表/红黑树实现,TreeMap
基于红黑树实现;HashMap
允许NULL作为键和值,TreeMap
不允许Null;HashMap
存入元素时不保证顺序,TreeMap
在存入元素时,实现了Comparable
接口或者Comparator
接口, 按照排序后的顺序存入元素HashMap
和TreeMap
都是线程不安全的HashMap
和TreeMap
的key都是不能重复, value可以重复的
HashSet
HashSet的实现原理
HashSet
的实现是依赖于HashMap
的;HashSet
的值都是存储在HashMap
中的,在HashSet
的构造方法中会初始化一个HashMap
对象,HashSet
不允许值重复。HashSet的
值是作为HashMap
的key存储在HashMap
中的,当存储的值已经存在时返回false
HashSet如何保证元素不重复?
public boolean add(E e){
return map.put(e,PRESENT)==null;
}
元素值作为HashMap
中的key,HashMap
的value则是PRESENT变量,这个变量只作为放入HashMap
时的一个占位符而存在,并没有什么实际用处。这个问题的答案其实显而易见:HashMap
中的key不能重复,因为HashCode
计算出相同的Hash值后,会在对应存储位置的链表上操作,并不能出现两个相同的key,而HashSet
的元素又是作为HashMap
的key存入的,当然也不能重复
LinkedHashMap的实现原理?
LinkedHashMap
也是基于HashMap
实现的,它是HashMap
的亲儿子,它有自己的静态内部类Entry继承自HashMap.Entry,不同的是它新增了双向链表所需要的before、after指针,用来实现按照插入顺序或者访问顺序排序
Iterator
Iterator怎么使用?有什么特点?
Iterator
(迭代器)是一种设计模式,它是一个对象,可以遍历并选择序列中的对象。迭代器通常被称为”轻量级“对象,因为他创建代价小。java中的Iterator
只能单向移动
- 使用方法
Iterator()
要求容器返回一个Iterator
。第一次调用Iterator
的next()方法的时候,它返回序列的第一个元素。(iterator()
方法是java.lang.Iterable接口,被Collection
继承) - 使用next()方法获得序列中的下一个元素。
- 使用
hasNext()
方法检查序列中是否还有元素。 - 使用
remove()
方法将迭代器新返回的元素删除
Iterator使用方法如下实例
:
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
public class Test {
public static void main(String[] args){
List<String> ll = new LinkedList<String>();
ll.add("first");
ll.add("second");
ll.add("third");
ll.add("fourth");
for(Iterator<String> iterator = ll.iterator();iterator.hasNext();){
String string = iterator.next();
System.out.println(string);
}
}
}
Iterator和ListIterator有什么区别?
Iterator
可以用来遍历Set和List集合,但是ListIterator
只能用来遍历ListIterator
只能前向遍历,而ListIterator
可以前向也可以后向遍历ListIterator
实现了Iterator
接口,并包含了其他功能,如:增加元素,替换元素,获取前一个和后一个索引等
Iterator和Enumeration接口的区别?
enumeration(枚举)
与Enumeration
接口相比,Iterator
更安全,因为当一个集合正在被遍历的时候,它会阻止其他线程去修改集合,否则会抛出ConcurrentModificationException
异常。这其实就是fail-fast机制。具体的区别如下:
假设有两个线程,第一个线程在通过Iterator进行遍历集合A中的元素的时候,在某个时刻另外一个线程修改了集合A的结构(是结构上的修改,不是简单的修改集合元素的内容),这个时候程序会抛出ConcurrentModificationException异常,从而产生fail-fast机制
- Iterator的方法名比Enumeration更科学
- Iterator有fail-fast机制,比Enumeration更安全
- Iterator能够删除元素,Enumeration不能删除元素
fail-fast与fail-safe有什么区别?
- Iterator的fail-fast属性与当前的集合共同起作用,因此他不会受到集合中任何改动的影响
- Java.util包中所有的集合类都被设计为fail-fast的,而Java.util.concurrent中的集合类都为fail-safe的
- 当前检测到正在遍历的集合结构被改变的时候,fail-fast迭代器会抛出
ConcurrentModificationException
,而fail-safe迭代器从不抛出ConcurrentModificationException
fail-fast:快速失败
是一种错误检测机制,当在遍历集合时检测到集合被修改(如添加、删除元素),会立即抛出 ConcurrentModificationException
异常
- 实现原理:
- 集合内部维护一个 修改计数器(modCount),用于记录集合被修改的次数。
- 在创建迭代器时,迭代器会记录当前的
modCount
值。 - 每次调用迭代器的
next()
或remove()
方法时,迭代器会检查modCount
是否与初始值一致。- 如果不一致,说明集合被修改,抛出
ConcurrentModificationException
- 如果不一致,说明集合被修改,抛出
fail-safe:安全失败:
是一种容错机制,当在遍历集合时检测到集合被修改,不会抛出异常,而是继续遍历修改前的集合副本
- 实现原理:
- 集合在遍历时创建一个副本,迭代器操作的是副本而不是原始集合。
- 即使原始集合被修改,也不会影响迭代器的遍历
Collection和Collentions的区别
Collection
和 Collections
是 Java 中两个完全不同的概念
-
Collection:
是Java集合框架中的一个接口,位于java.util包中.它是所有集合类的根接口
-
Collections:
是Java集合框架中的一个工具类,位于java.util包中.它提供了一系列静态方法,用于操作或返回集合(如排序,查找,同步等)
entions的区别
Collection
和 Collections
是 Java 中两个完全不同的概念
-
Collection:
是Java集合框架中的一个接口,位于java.util包中.它是所有集合类的根接口
-
Collections:
是Java集合框架中的一个工具类,位于java.util包中.它提供了一系列静态方法,用于操作或返回集合(如排序,查找,同步等)