Java中常用的容器有哪些?
常见容器主要包括Collection和Map两种,Collection存储着对象的集合,而Map存储着键值对(两个对象)的映射表
Collection
-
Set
-
TreeSet:基于红黑树实现,支持有序性操作,例如:根据一个范围查找元素的操作。但是查找效率不如HashSet,HashSet查找的时间复杂度为O(1),TreeSet则为O(logN)。
-
HashSet:基于哈希表实现,支持快速查找,但不支持有序性操作。并且失去了元素的插入顺序信息,也就是说使用Iterator遍历HashSet得到的结果是不确定的。
-
LinkedHashSet:具有HashSet的查找效率,且内部使用双向链表维护元素的插入顺序。
-
List
-
ArrayList:基于动态数组实现,支持随机访问。
-
Vector:和ArrayList类似,但它是线程安全的(这里需要注意:Vector的单个操作时原子性的,也就是线程安全的。但是如果两个原子操作复合而来,这个组合的方法是非线程安全的,需要使用锁来保证线程安全,具体可以看这篇文章:Vection的非线程安全操作
-
LinkedList:基于双向链表实现,只能顺序访问,但是可以快速地在链表中间插入和删除元素。不仅如此,LinkedList还可以用作栈、队列和双向队列。
-
Queue
-
LinkedList:可以用它来实现双向队列。
-
PriorityQueue:基于堆结构实现,可以用它来实现优先队列。
Map
-
TreeMap:基于红黑树实现。
-
HashMap:基于哈希表实现。
-
HashTable:和HashMap类似,但它是线程安全的,这意味着同一时刻多个线程可以同时写入HashTable并且不会导致数据不一致。它是遗留类,不应该去使用它。现在可以使用ConcurrentHashMap来支持线程安全,并且ConcurrentHashMap的效率会更高,因为ConcurrentHashMap引入了分段锁。
-
LinkedHashMap:使用双向链表来维护元素的顺序,顺序为插入顺序或最近最少使用(LRU)顺序。
ArrayList和LinkedList的区别?
ArrayList:底层是基于数组实现的,查找快,增删较慢;
LinkedList:底层是基于链表实现的。确切的说是循环双向链表(JDK1.6之前是双线循环链表、JDK1.7之后取消了煦暖),查找慢、增删快。LinkedList链表由一系列表项连接而成,一个表项包含3个部分:元素内容、前驱表和后驱表。链表内部有一个header表项,既是链表的开始也是链表的结尾。header的后继表项是链表中的第一个元素,header的前驱表项是链表中的最后一个元素。
ArrayList的增删未必就是比LinkedList要慢:
-
如果增删都是在末尾来操作【每次调用的都是remove()和add()】,此时ArrayList就不需要移动和复制数组来进行操作。如果数据量具有百万级的时,速度是会比LinkedList要快的。
-
如果删除操作的位置是在中间。由于LinkedList的消耗主要是在遍历上,ArrayList的消耗主要是在移动和复制上(底层调用的是arraycopy()方法,是native方法)。LinkedList的遍历速度是要慢于ArrayList的复制移动速度的,如果数量有百万级的时候,还是ArrayList要快。
ArrayList 实现 RandomAccess 接口有何作用?为何 LinkedList 却没实现这个接口?
-
RandomAccess接口只是一个标志接口,只要List集合实现这个接口,就能支持快速随机访问。通过查看Collections类中的binarySearch()方法,可以看出,判断List是否实现RandomAccess接口来实行indexedBinarySearch(list,key)或iteratorBinarySerach(list, key)方法。再通过查看这两个方法的源码发现:实现RandomAccess接口的List集合采用一般的for循环遍历,而未实现这接口则采用迭代器,即ArrayList一般采用for循环遍历,而LinkedList一般采用迭代器遍历;
-
ArrayList用for循环遍历比iterator迭代器遍历快,LinkedList用iterator迭代器遍历比for循环遍历快。所以说,当我们在做项目时,应该考虑到List集合的不同子类采用不同的遍历方式,能够提高性能。
Array和ArrayList有何区别?什么时候更适合用Array?
-
Array可以指定容纳基本类型和对象,而ArrayList只能容纳对象;
-
Array可以指定大小,而ArrayList大小是不固定的。
什么时候更适合用Array:
-
如果列表的大小已经指定,大部分情况下是存储和遍历他们;
-
对于遍历基本数据类型,尽管Collection使用自动装箱来减轻编码任务,在指定大小的基本类型的列表上工作也会变得很慢;
-
如果使用多维数组,使用 [ ] [ ] 比List<<>>更容易。
HashMap的实现原理/底层数据结构?JDK1.7和JDK1.8
JDK1.7:Entry数组+链表
JDK1.8:Node数组+链表/红黑树,当链表上的元素超过8个并且数组长度>=64时自动转化成红黑树,节点变成树节点,以提高搜索效率和插入效率到O(logN)。Entry和Node都包括key、value、hash、next属性。
HashMap的put方法的执行过程?
但我们想往一个HashMap中添加一对key-value时,系统首先会计算key的hash值,然后根据hash值确认在table中存储的位置。若该位置没有元素,则直接插入。否则迭代该处元素链表并依次比较其key的hash值。如果两个hash值相等且key值相等(e.hash hash && ((k = e.key) key || key.equals(k))),则用新的Entry的value覆盖原来节点的value。如果两个hash值相等但key值不等,则进行插入操作。
不过呢,插入操作在JDK1.7和JDK1.8是有所不同的,JDK1.7底层采用数组+链表,插入时采用头插法,JDK1.8,底层采用数组+链表/红黑树,并且吧头插法改成了尾插法,主要是为了减少线程安全的问题,另外,当链表长度大于8,且数组长度大于64时,会把链表转化为红黑树处理,这个时候,就无关是头插还是尾插了,得按照红黑树的规则来插了。
HashMap 的get方法的执行过程?
通过key的hash值找到在table数组中的索引处的Entry,然后返回该key对应的value即可。
在这里能够根据key快速的取到value除了和HashMap的数据结构密不可分外,还和Entry有莫大的关系。HashMap在存储过程中并没有将key,value分开来存储,而是当做一个整体key-value来处理,这个整体就是Entry对象。同时value也只相当于key的附属而已。在存储的过程中,系统根据key的HashCode来决定Entry在table数组中的存储位置,在取得过程中同样根据key的HashCode取出相对应的Entry对象(value就包含在里面)。
HashMap的resize方法的执行方法?
有两种情况会调用resize方法:
-
第一次调用HashMap的put方法时,会调用resize方法对table数组进行初始化,如果不传入指定值,默认大小为16。
-
扩容时会调用resize,即size > threshold时,table数组大小翻倍。
HashMap的size为什么必须是2的整数次方?
-
这样做总是能够保证HashMap的底层数组长度为2的n次方。当length为2的n次方时,h&(length-1)就相当于对length取模,而且速度比直接取模快得多,这是HashMap在速度上的一个优化。而且每次扩容都是翻倍。
-
如果length为2的次幂,则length-1转化为二进制必定是1111.......的形势,在与h的二进制进行与操作时效率会非常快,为15,则length-1为14,对应的二进制为1110,在于h与操作,最后一位都为0,而0001,0011,0101,1001,1011,0111,1101这几个位置永远都不能存放元素了,空间浪费相当大,更糟的是这种情况中,数组可以使用的位置比数组长度小了很多,这意味着进一步增加了碰撞的几率,减慢了查询的效率,这样就会造成空间的浪费。
HashMap的get方法能否判断某个元素是否在map中?
HashMap的get函数的返回值不能判断一个key是否包含在map中,因为get返回null有可能是不包含key,也有可能该key对应的value为null。因为HashMap中允许key为null,也允许value为null。
HashMap与HashTable的区别是什么?
-
HashTable基于Dictionary类,而HashMap是基于AbstractMap。Dictionary是任何可将键映射到相应值的类的抽象父类,而AbstractMap是基于Map接口的实现,他以最大限度地减少实现此接口所需的工作。
-
HashMap的key和value都允许为null,而HashTable的key和value都不允许为null。HashMap遇到key为null的时候,调用putForNullKey方法进行处理,而对value没有处理;HashTable遇到null,直接返回空指针异常(NullPointException)。
-
HashTable是线程安全的,HashMap不是线程安全的,但是我们也可以通过Collection.synchronizedMap(HashMap),使其实现同步。
HashTable的补充:
HashTable和HashMap的实现原理几乎都一样,差别无非是
-
HashTable不允许key和value为null;
-
HashTable是线程安全的。但是HashTable线程安全的策略实现代价却太大了,简单粗暴,get/put所有相关操作都是synchronize的,这相当于给整个哈希表上了一把大锁,多线程访问时候,只要有一个线程访问或操作该对象,那其他线程只能阻塞,相当于将所有的操作串行化,在竞争激烈的并发场景中性能就会非常差。
HashMap与ConcurrentHashMap的区别是什么?
HashMap不是线程安全的,而ConcurrentHashMap是线程安全的。
ConcurrentHashMap采用锁分段技术,将整个Hash桶进行了分段segment,也就是将这个大的数组分成了小的片段segment,而且每个小的segment上面都有锁存在,那么在插入元素的时候就需要先找到应该插入到哪一个片段segment,然后再在这个片段上面进行插入,而且这里还需要获取segment锁,这样做明显减小了锁的粒度。
ConcurrentHashMap的实现原理是什么?
数据结构
JDK 7 中:ConcurrentHashMap采用了数组 + Segment + 分段锁的方式实现。
JDK 8 中:ConcurrentHashMap参考了JDK 8 HashMap的实现,采用了数组+链表+红黑树的实现方式来设计,内部大量采用CAS操作。
ConcurrentHashMap采用了非常精妙的“分段锁”策略,ConcurrentHashMap的主干是Segment数组。
final Segment<K,V>[] segment;
Segment继承了ReentrantLock,所以它就是一种可重入锁(ReentrantLock)。在ConcurrentHashMap,一个Segment就是一个子哈希表,Segment里维护一个HashEntry树组,并发环境下,对于不同Segment的数据进行操作是不应考虑锁竞争的。就按默认的ConcurrentLevel为16来讲,理论上就允许16个线程并发执行。
所以,对于同一个Segment的操作才需要考虑线程同步,不同的Segment则无需考虑。Segment类似有HashMap,一个Segment维护着一个HashEntry数组:
transient volatile HashEntry<K,V>[] table;
HashEntry是目前我们提到的最小的逻辑处理单元了。一个ConcurrentHashMap维护一个Segment数组,一个Segment维护一个HashEntry数组。因此,ConcurrentHashMap定位一个元素的过程需要进行两次Hash操作。第一次Hash定位到Segment,第二次Hash定位到元素所在的链表的头部。
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;
}
因为HashSet的值是作为HashMap的key存储在HashMap中的,而HashMap中的key是不能重复的,所以HashSet的元素固然不会重复。
LinkedHashMap的实现原理?
LinkedHashMap也是基于HashMap实现的,不同的是它定义了一个Entry header,这个header不是放在Table里,它是额外独立出来的。LinkedHashMap通过继承HashMap中的Entry,并添加两个属性Entry before,after和header结合起来组成一个双线链表,来实现按插入或访问顺序排序。
LinkedHashMap定义了排序模式accessOrder,该属性boolean型变量,对于访问顺序,为true;对于插入顺序,则为false。一般情况下,不必指定排序模式,其迭代顺序即为默认插入顺序。
Iterator怎么使用?有什么特点?
迭代器是一种设计模式,它是一个对象,它可以遍历并选择序列中的对象,而开发人员不需要了解该序列的底层结构。迭代器通常被成为“轻量级“对象,因为创建它的代价小。Java中的Iterator功能比较简单,并且只能单向移动:
-
使用方法iterator()要求容器返回一个Iterator。第一次调用Iterator的next()方法时,他返回序列的第一个元素。注意:iterator()方法是java.lang.Iterable接口,被Collection继承。
-
使用next()获得序列中的下一个元素。
-
使用hashNext()检查序列中是否还有元素。
-
使用remove()迭代器新返回的元素删除。
Iterator和ListIterator有什么区别?
Iterator可以用来遍历Set和List集合,但是ListIterator只能用来遍历List。Iterator对集合只能是前向遍历,ListIterator既可以前向也可以后向。ListIterator实现了Iterator接口,并包含其他的功能,比如:增加元素,替换元素,获取前一个和后一个元素的索引等等。
Iterator和Enumeration接口的区别?
与Enumeration相比,Iterator更加安全,因为当一个集合正在被遍历的时候,它会阻止其他线程去修改集合。否则会抛出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 。
Collection和Collections有什么区别?
Collection:是最基本的集合接口,一个Collection代表一组Object,即Collection的元素。他的直接继承接口有List,Set和Queue。
Collections:是不属于Java的集合框架的,它是集合类的一个工具类/帮助类。此类不能被实例化,服务于Java的Collection框架。它包含有关集合操作的静态多态方法,实现对各种集合的搜索、排序、线程安全等操作。