一、Java集合类框架的基本接口有哪些?二、Collection 和 Collections 有什么区别?三、为何集合类接口没有实现 Cloneable 和 Serializable 接口?四、List、Set、Map 之间的区别是什么?五、什么是迭代器(Iterator)?六、Iterator 和 ListIterator 的区别是什么?七、快速失败(fail-fast)和安全失败(fail-safe)的区别是什么?八、HashMap 和 Hashtable 有什么区别?九、ConcurrentHashMap 和 Hashtable 的区别十、ConcurrentHashMap 线程安全的具体实现方式/底层具体实现十一、Java 中的 HashMap 的工做原理是什么?十二、hashmap 大小为何是 2 的幂次方1三、hashCode()和 equals()方法的重要性体如今什么地方?1四、数组(Array)和列表(ArrayList)有什么区别?何时应该使用 Array 而不是 ArrayList?1五、ArrayList 和 LinkedList 有什么区别?1六、Comparable 和 Comparator 接口是干什么的?列出它们的区别。1七、什么是 Java 优先级队列(Priority Queue)?1八、Java集合类框架的最佳实践有哪些?1九、HashSet 和 TreeSet 有什么区别?20、HashSet 的实现原理?如何实现数组和 List 之间的转换?2一、Arrays.asList()使用指南Collection.toArray()方法使用的坑&如何反转数组2二、ArrayList 和 Vector 的区别是什么?2三、在 Queue 中 poll()和 remove()有什么区别?php
总共有两大接口:Collection 和 Map ,一个元素集合,一个是键值对集合; 其中 List 和 Set 接口继承了 Collection 接口,一个是有序元素集合,一个是无序元素集合; 而 ArrayList 和 LinkedList 实现了 List 接口,HashSet 实现了 Set 接口,这几个都比较经常使用; HashMap 和 HashTable 实现了 Map 接口,而且 HashTable 是线程安全的,可是 HashMap 性能更好;html
java.util.Collection 是一个集合接口(集合类的一个顶级接口)。它提供了对集合对象进行基本操做的通用接口方法。Collection 接口在 Java 类库中有不少具体的实现。Collection 接口的意义是为各类具体的集合提供了最大化的统一操做方式,其直接继承接口有 List 与 Set。java
Collections 则是集合类的一个工具类/帮助类,其中提供了一系列静态方法,用于对集合中元素进行排序、搜索以及线程安全等各类操做。git
克隆(cloning)或者是序列化(serialization)的语义和含义是跟具体的实现相关的。所以,应该由集合类的具体实现来决定如何被克隆或者是序列化。github
迭代器是一种设计模式,它是一个对象,它能够遍历并选择序列中的对象,而开发人员不须要了解该序列的底层结构。迭代器一般被称为“轻量级”对象,由于建立它的代价小。web
Java 中的 Iterator 功能比较简单,而且只能单向移动:设计模式
(1) 使用方法 iterator()要求容器返回一个 Iterator。第一次调用 Iterator 的 next()方法时,它返回序列的第一个元素。注意:iterator()方法是 java.lang.Iterable 接口,被 Collection 继承。数组
(2) 使用 next()得到序列中的下一个元素。安全
(3) 使用 hasNext()检查序列中是否还有元素。数据结构
(4) 使用 remove()将迭代器新返回的元素删除。
Iterator 是 Java 迭代器最简单的实现,为 List 设计的 ListIterator 具备更多的功能,它能够从两个方向遍历 List,也能够从 List 中插入和删除元素。
Iterator 可用来遍历 Set 和 List 集合,可是 ListIterator 只能用来遍历 List。
Iterator 对集合只能是前向遍历,ListIterator 既能够前向也能够后向。
ListIterator 实现了 Iterator 接口,并包含其余的功能,好比:增长元素,替换元素,获取前一个和后一个元素的索引等等。
快速失败:当你在迭代一个集合的时候,若是有另外一个线程正在修改你正在访问的那个集合时,就会抛出一个 ConcurrentModification 异常。
在 java.util 包下的都是快速失败,不能在多线程下发生并发修改(迭代过程当中被修改)。
安全失败:你在迭代的时候会去底层集合作一个拷贝,因此你在修改上层集合的时候是不会受影响的,不会抛出 ConcurrentModification 异常。
在 java.util.concurrent 包下的全是安全失败的。能够在多线程下并发使用,并发修改。
一、HashMap 是非线程安全的,HashTable 是线程安全的。
二、HashMap 的键和值都容许有 null 值存在,而 HashTable 则不行。
三、由于线程安全的问题,HashMap 效率比 HashTable 的要高。
四、Hashtable 是同步的,而 HashMap 不是。所以,HashMap 更适合于单线程环境,而 Hashtable 适合于多线程环境。
五、二者提供了可供应用迭代的键的集合,都是快速失败的。此外 Hashtable 提供了对键的列举(Enumeration)
六、初始容量大小和每次扩充容量大小的不一样: ①建立时若是不指定容量初始值,Hashtable 默认的初始大小为 11,以后每次扩充,容量变为原来的 2n+1。HashMap 默认的初始化大小为 16。以后每次扩充,容量变为原来的 2 倍。②建立时若是给定了容量初始值,那么 Hashtable 会直接使用你给定的大小,而 HashMap 会将其扩充为 2 的幂次方大小(HashMap 中的 tableSizeFor()方法保证,下面给出了源代码)。也就是说 HashMap 老是使用 2 的幂做为哈希表的大小。
通常如今不建议用 HashTable, ①是 HashTable 是遗留类,内部实现不少没优化和冗余。②即便在多线程环境下,如今也有同步的 ConcurrentHashMap 替代,没有必要由于是多线程而用 HashTable。
HashMap 中带有初始容量的构造函数:
public HashMap(int initialCapacity, float loadFactor) {
if (initialCapacity < 0)
throw new IllegalArgumentException("Illegal initial capacity: " +
initialCapacity);
if (initialCapacity > MAXIMUM_CAPACITY)
initialCapacity = MAXIMUM_CAPACITY;
if (loadFactor <= 0 || Float.isNaN(loadFactor))
throw new IllegalArgumentException("Illegal load factor: " +
loadFactor);
this.loadFactor = loadFactor;
this.threshold = tableSizeFor(initialCapacity);
}
public HashMap(int initialCapacity) {
this(initialCapacity, DEFAULT_LOAD_FACTOR);
}
复制代码
下面这个方法保证了 HashMap 老是使用2的幂做为哈希表的大小。
/**
* Returns a power of two size for the given target capacity.
*/
static final int tableSizeFor(int cap) {
int n = cap - 1;
n |= n >>> 1;
n |= n >>> 2;
n |= n >>> 4;
n |= n >>> 8;
n |= n >>> 16;
return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
}
复制代码
二者的对比图:
图片来源:http://www.cnblogs.com/chengxiao/p/6842045.html
HashTable:
JDK1.7 的 ConcurrentHashMap:
JDK1.7(上面有示意图)
首先将数据分为一段一段的存储,而后给每一段数据配一把锁,当一个线程访问其中一个段数据时,只是占用当前数据段的锁,其余段的数据也能被其余线程访问。
ConcurrentHashMap 是由 Segment 数组结构和 HashEntry 数组结构组成。
Segment 实现了 ReentrantLock,因此 Segment 是一种可重入锁,扮演锁的角色。HashEntry 用于存储键值对数据。
static class Segment<K,V> extends ReentrantLock implements Serializable {
}
复制代码
一个 ConcurrentHashMap 里包含一个 Segment 数组。Segment 的结构和 HashMap 相似,是一种数组和链表结构,一个 Segment 包含一个 HashEntry 数组,每一个 HashEntry 是一个链表结构的元素,每一个 Segment 守护着一个HashEntry数组里的元素,当对 HashEntry 数组的数据进行修改时,必须首先得到对应的 Segment的锁。
JDK1.8 (上面有示意图)
ConcurrentHashMap 取消了Segment 分段锁,采用 CAS 和 synchronized 来保证并发安全。数据结构跟 HashMap1.8 的结构相似,数组+链表/红黑二叉树。Java 8 在链表长度超过必定阈值(8)时将链表(寻址时间复杂度为O(N))转换为红黑树(寻址时间复杂度为 O(log(N)))
synchronized 只锁定当前链表或红黑二叉树的首节点,这样只要 hash 不冲突,就不会产生并发,效率又提高 N 倍。
推荐阅读:
hashmap 是一个 key-value 键值对的数据结构,从结构上来说在 jdk1.8 以前是用数组加链表的方式实现,jdk1.8 加了红黑树,hashmap 数组的默认初始长度是 16,hashmap 数组只容许一个 key 为 null,容许多个 value 为 null。
hashmap 的内部实现,hashmap 是使用数组+链表+红黑树的形式实现的,其中数组是一个一个 Node[]数组,咱们叫他 hash 桶数组,它上面存放的是 key-value 键值对的节点。HashMap 是用 hash 表来存储的,在 hashmap 里为解决 hash 冲突,使用链地址法,简单来讲就是数组加链表的形式来解决,当数据被 hash 后,获得数组下标,把数据放在对应下标的链表中。
而后再说一下 hashmap 的方法实现
put 方法,put 方法的第一步,就是计算出要 put 元素在 hash 桶数组中的索引位置,获得索引位置须要三步,计算要 put 元素 key 的 hashcode 值,高位运算,取模运算,高位运算就是用第一步获得的值 h,用 h 的高 16 位和低 16 位进行异或操做,第三步为了使 hash 桶数组元素分布更均匀,采用取模运算,取模运算就是用第二步获得的值和 hash 桶数组长度-1的值取与。这样获得的结果和传统取模运算结果一致,并且效率比取模运算高。
jdk1.8 中 put 方法的具体步骤,先判断 hashmap 是否为空,为空的话扩容,不为空计算出 key 的 索引值 i,而后看 table[i]是否为空,为空就直接插入,不为空判断当前位置的 key 和 table[i] 是否相同,相同就覆盖,不相同就查看 table[i] 是不是红黑树节点,若是是的话就用红黑树直接插入键值对,若是不是开始遍历链表插入,若是遇到重复值就覆盖,不然直接插入,若是链表长度大于 8,转为红黑树结构,执行完成后看 size 是否大于阈值 threshold,大于就扩容,不然直接结束。
get 方法就是计算出要获取元素的 hash 值,去对应位置取便可。
HashMap 的两个重要属性是容量 capacity 和加载因子 loadfactor,默认值分布为 16 和 0.75,当容器中的元素个数大于 capacity*loadfactor 时,容器会进行扩容 resize 为 2n,在初始化 Hashmap 时能够对着两个值进行修改,负载因子 0.75 被证实为是性能比较好的取值,一般不会修改,那么只有初始容量 capacity 会致使频繁的扩容行为,这是很是耗费资源的操做,因此,若是事先能估算出容器所要存储的元素数量,最好在初始化时修改默认容量 capacity,以防止频繁的 resize 操做影响性能。
扩容机制,hashmap 的扩容中主要进行两步,第一步把数组长度变为原来的两倍,第二部把旧数组的元素从新计算 hash 插入到新数组中,在 jdk1.8 时,不用从新计算 hash,只用看看原来的 hash 值新增的一位是零仍是 1,若是是 1, 这个元素在新数组中的位置,是原数组的位置加原数组长度,若是是零就插入到原数组中。扩容过程第二步一个很是重要的方法是 transfer 方法,采用头插法,把旧数组的元素插入到新数组中。
在计算插入元素在 hash 桶数组的索引时第三步,为了使元素分布的更加均匀,用取模操做,可是传统取模操做效率低,而后优化成 h&(length-1),设置成 2 幂次方,是由于 2 的幂次方-1后的值每一位上都是 1,而后与第二步计算出的 h 值与的时候,最终的结果只和 key 的 hashcode 值自己有关,这样不会形成空间浪费而且分布均匀。
若是 length 不为 2 的幂,好比 15。那么 length-1 的 2 进制就会变成 1110。在 h 为随机数的状况下,和 1110 作&操做。尾数永远为 0。那么 000一、100一、1101 等尾数为 1 的位置就永远不可能被 entry 占用。这样会形成浪费,不随机等问题。
HashMap 的不少函数要基于 equal()函数和 hashCode()函数。hashCode()用来定位要存放的位置,equal()用来判断是否相等。hashcode 和 equals 组合在一块儿肯定元素的惟一性。
查找元素时,若是单单使用 equals 来肯定一个元素,须要对集合内的元素逐个调用 equals 方法,效率过低。所以加入了 hashcode 方法,将元素映射到随机的内存地址上,经过 hashcode 快速定位到元素(大体)所在的内存地址,再经过使用 equals 方法肯定元素的精确位置。
比较两个元素时,先比较 hashcode,若是 hashcode 不一样,则元素必定不相等;若是相同,再用 equals 判断。
HashMap 采用这两个方法实现散列存储,提升键的索引性能。HashSet 是基于 HashMap 实现的。
适用场景:
当集合长度固定时,使用数组;当集合的长度不固定时,使用 ArrayList。
因为 ArrayList 不支持基本数据类型,因此保存基本数据类型时须要装箱处理,对比数组性能会降低。这种状况尽可能使用数组。
数组支持的操做方法不多,但内存占用少,若是只需对集合进行随机读写,选数组
ArrayList 和 LinkedList 都实现了 List 接口,他们有如下的不一样点:
ArrayList 是基于索引的数据接口,它的底层是数组。它能够以O(1)时间复杂度对元素进行随机访问,适合元素查找。与此对应,LinkedList 基于链表,为双向链表(JDK1.6 以前为循环链表,JDK1.7 取消了循环), 每个元素都和它的前一个和后一个元素连接在一块儿,在这种状况下,查找某个元素的时间复杂度是 O(n)。
相对于 ArrayList,LinkedList 增删操做速度更快,由于当元素被添加到集合任意位置的时候,不须要像数组那样从新计算大小或者是更新索引。
LinkedList 比 ArrayList 更占内存,由于 LinkedList 为每个节点存储了两个引用,一个指向前一个元素,一个指向后一个元素。
Comparable & Comparator 都是用来实现集合中元素的比较、排序的。
Comparable 接口(内部比较器):
Comparator 接口(外部比较器):
PriorityQueue 的逻辑结构是一棵彻底二叉树,存储结构实际上是一个数组。逻辑结构层次遍历的结果恰好是一个数组。
PriorityQueue 是一个基于优先级堆的无界队列,它的元素是按照天然顺序(natural order)排序的。在建立的时候,咱们能够给它提供一个负责给元素排序的比较器。PriorityQueue 不容许 null 值,由于他们没有天然顺序,或者说他们没有任何相关联的比较器。最后,PriorityQueue 不是线程安全的,入队和出队的时间复杂度是 O(log(n))。
Hashset 的底层是由哈希表实现的,Hashset 中的元素是无序的。add(),remove(),contains()方法的时间复杂度是 O(1)。
Treeset 底层是由红黑树实现的。若是须要在 Treeset 中插入对象,须要实现 Comparable 接口,重写 compareTo 方法。
HashSet 底层由 HashMap 实现
HashSet 的值存放于 HashMap 的 key 上
HashMap 的 value 统一为 PRESENT
List 转换成为数组:调用 ArrayList 的 toArray 方法。
数组转换成为 List:调用 Arrays 的 asList 方法。
String[] myArray = { "Apple", "Banana", "Orange" };
List<String> myList = Arrays.asList(myArray);
//上面两个语句等价于下面一条语句
List<String> myList = Arrays.asList("Apple","Banana", "Orange");
复制代码
注意:使用工具类 Arrays.asList()把数组转换为集合时,不能使用其修改集合相关的方法,它的 add/remove/clear 方法会抛出 UnsupportedOperationException 异常。
说明:asList 的返回对象是一个 Arrays 内部类,并无实现集合的修改方法。Arrays.asList 体现的是适配器模式,只是转换接口,后台的数据还是数组。
String[] str = new String[]{"and","you"};
List list = Arrays.asList(str);
System.out.println(list.get(1));//输出you
list.add("yes");//抛出异常
str[0] = s"yes";
System.out.println(list);//list也会发生改变,输出[yes, you]
复制代码
Arrays.asList()是泛型方法,传入的参数对象必须是对象数组。
int[] myArray = { 1, 2, 3 };
List myList = Arrays.asList(myArray);
System.out.println(myList.size());//1
System.out.println(myList.get(0));//数组地址值
System.out.println(myList.get(1));//报错:ArrayIndexOutOfBoundsException
int [] array=(int[]) myList.get(0);
System.out.println(array[1]);//1
复制代码
当传入一个原生数据类型数组时,Arrays.asList() 的真正获得的参数就不是数组中的元素,而是数组对象自己!此时List 的惟一元素就是这个数组,这也就解释了上面的代码。
转换为包装数据类型便可。
Integer[] myArray = { 1, 2, 3 };
List myList = Arrays.asList(myArray);
System.out.println(myList.size());//数组长度3
System.out.println(myList.get(0));//1
System.out.println(myList.get(1));//2
复制代码
使用集合的修改方法:add()、remove()、clear()会抛出异常。
Arrays.asList() 方法返回的并非 java.util.ArrayList ,而是 java.util.Arrays 的一个内部类,这个内部类并无实现集合的修改方法或者说并无重写这些方法。
List myList = Arrays.asList(1, 2, 3);
System.out.println(myList.getClass());//class java.util.Arrays$ArrayList
复制代码
如何正确的将数组转换为ArrayList?
一、自定义代码
static <T> List<T> arraysToList(T[] array){
List<T> list = new ArrayList<T>(array.length);
for(T obj:array){
list.add(obj);
}
return list;
}
String[] str = new String[]{"and","you"};
List ll = arraysToList(str);
System.out.println(ll.getClass());//class java.util.ArrayList
int[] myArray = { 1, 2, 3 };
List ll2 = arraysToList(myArray);//编译报错
复制代码
该方法暂不支持基本数据类型。
二、最简便的方法(推荐)
List list = new ArrayList<>(Arrays.asList("a", "b", "c"))
复制代码
三、使用 Java8 的 Stream(推荐)
Integer [] myArray = { 1, 2, 3 };
List myList = Arrays.stream(myArray).collect(Collectors.toList());
//基本类型也能够实现转换(依赖boxed的装箱操做)
int [] myArray2 = { 1, 2, 3 };
List myList = Arrays.stream(myArray2).boxed().collect(Collectors.toList());
复制代码
四、 使用 Guava(推荐)
对于可变集合,你可使用 Lists 类及其 newArrayList()工厂方法:
String[] str = {"acd","yes"};
List list = Lists.newArrayList(str);//class java.util.ArrayList
int[] myArray = { 1, 2, 3 };
list = Lists.newArrayList(myArray);//class java.util.ArrayList
复制代码
五、使用 Apache Commons Collections
List<String> list = new ArrayList<String>();
CollectionUtils.addAll(list, str);
复制代码
该方法是一个泛型方法: T[] toArray(T[] a); 若是 toArray 方法中没有传递任何参数的话返回的是 Object 类型数组。
String [] s= new String[]{
"dog", "lazy", "a", "over", "jumps", "fox", "brown", "quick", "A"
};
List<String> list = Arrays.asList(s);//class java.util.Arrays$ArrayList
Collections.reverse(list);
s=list.toArray(new String[0]);//没有指定类型的话会报错,s2类型为String
Object[] s2 = list.toArray();//效果同上
list = Lists.newArrayList(s);//class java.util.ArrayList
Collections.reverse(list);
s = list.toArray(new String[0]);//编译报错
Object[] s2 = list.toArray(new String[0]);//s2类型为String
复制代码
综上可知,当数组为引用类型数组时,使用 Arrays.asList 转换为 List,反转操做后,再转换为数组,不管 toArray()方法中是否有参数,声明为 Object 类型的数组实际类型都为原引用类型。使用其余数组转 List 方法,好比 Lists.newArrayList,须要在 toArray()参数中传入对应类型,最后获得的数组类型也是原类型。
当数组为基本数据数组时,最后转换以后获得的数组为 Object 类型。
poll() 和 remove() 都是从队列中取出一个元素,可是 poll() 在获取元素失败的时候会返回空,可是 remove() 失败的时候会抛出异常。