在面试中被问到这个问题该咋么回答呢?这个问题是我认为最难回答的一个问题,原因就他问的范围太大了不知道该咋么回答;就我而言遇到这种问题首先给他说个总的,从最大的开始说起,在说一下里面包含的东西,包括里面的区别;如果你理解到位了也可以说一下底层的实现那么这时候面试官的眼睛就亮了;下面是我的思路可以参考一下,如有错误还请个位指正!
首先上一张图,在自己脑子里有一个大概的框架:
看过脑图有一个大概的知识体系我们就可以开始说了:
集合框架它分为单列集合和双列集合,单列集合都继承自Collection而双列集合就是常说的Map;那么我们先来说说单列集合,单列集合分为List和Set都是继承自Collection 接口(这时候我们可以说List跟Set的区别):
List和Set的区别
List
- 一个有序(元素存入集合的顺序和取出的顺序一致)容器,元素可以重复,可以插入多个null元素,元素都有索引;
- 常用的实现类有 ArrayList、LinkedList 和 Vector;
- 和数组类似,List可以动态增长,查找元素效率高,插入删除元素效率低,因为会引起其他元素位置改变;
- List 支持for循环,也就是通过下标来遍历,也可以用迭代器;
- 存储和取出是一致的;
Set
- 一个无序(存入和取出顺序有可能不一致)容器,不可以存储重复元素,只允许存入一个null元素,必须保证元素唯一性;
- Set 接口常用实现类是 HashSet、LinkedHashSet 以及TreeSet;
- 检索元素效率低下,删除和插入效率高,插入和删除不会引起元素位置改变;
- 因为他无序,无法用下标来取得想要的值,所以只能用迭代器;
- 存储和取出不一致(不能保证该顺序恒久不变);
接着我们可以说一下List的三个子集的区别:
ArrayList、LinkedList 与voctor的区别
ArrayList
底层结构是动态数组,它允许对元素进行快速随机访问。数组的缺点是每个元素之间不能有间隔,当数组大小不满足时需要增加存储能力,就要将已经有数组的数据复制到新的存储空间中。当从 ArrayList 的中间位置插入或者删除元素时,需要对数组进行复制、移动、代价比较高。因此,查找和遍历的效率较高,增删慢;LinkedList
底层结构是双向链表,很适合数据的动态插入和删除,随机访问和遍历速度比较慢。另外,他还提供了 List 接口中没有定义的方法,专门用于操作表头和表尾元素,可以当作堆栈、队列和双向队列使用;Voctor
底层结构是数组,支持线程的同步,即某一时刻只有一个线程能够写 Vector,避免多线程同时写而引起的不一致性,但实现同步需要很高的花费,因此,访问它比访问 ArrayList 慢;属于线程安全的集合,增删慢,查询慢;
说完之后呢,我们可以往下继续说Set的子集,Set里面最常用的就是HashSet和TreeSet,先说一下HashSet的实现原理以及它如何保证数据的不重复性:
HashSet实现原理
HashSet内部是基于HashMap实现的,底层使用Hash表实现,存取速度快;HashSet的值存放于HashMap的key上,HashMap的value统一为present,因此 HashSet 的实现比较简单,相关 HashSet 的操作,基本上都是直接调用底层HashMap 的相关方法来完成,HashSet排列无序,不允许重复的值;
HashSet如何保证数据不重复
HashSet 存储元素的顺序并不是按照存入时的顺序(和 List 显然不同)而是按照哈希值来存的所以取数据也是按照哈希值取得。元素的哈希值是通过元素的hashcode方法来获取的,HashSet首先判断两个元素的哈希值,如果哈希值一样,接着会比较equals 方法 如果equls结果为true,HashSet 就视为同一个元素。如果equals为false就不是同一个元素;
接着就可以说一下TreeSet,他的内部是TreeMap而TreeMap的底层是红黑树一种自平衡的二叉树,所以我们也可以说一下红黑树的相关东西;
TreeSet红黑树
TreeSet底层使用二叉树的原理实现的,排列无序,不可重复;排序存储;内部是TreeMap的SortedSet;对新add()的对象按照指定的顺序排序(升序、降序),每增加一个对象都会进行排序,将对象插入的二叉树指定的位置;
Integer 和 String 对象都可以进行默认的 TreeSet 排序,而自定义类的对象是不可以的,自己定义的类必须实现 Comparable 接口,并且覆写相应的 compareTo()函数,才可以正常使用;在覆写 compare()函数时,要返回相应的值才能使 TreeSet 按照一定的规则来排序;
红黑树一种自平衡的二叉查找树,但在每个结点上增加一个存储位表示结点的颜色,可以是Red或Black;
public static void main(String[] args) {
//创建TreeSet集合对象
TreeSet<Integer> ts = new TreeSet<Integer>();
//add()添加
ts.add(20) ;
ts.add(18) ;
ts.add(23) ;
ts.add(22) ;
ts.add(17) ;
ts.add(24) ;
ts.add(19) ;
ts.add(18) ;
ts.add(24) ;
//遍历集合
for(Integer i : ts) {
System.out.print(i+" ");//17 18 19 20 22 23 24
}
}
遍历原理:拿上面的数字20,18,23,22,17,24,19,18,24来说:
存储元素:
遍历元素:
使用中序遍历(左根右)
这样单列集合就差不多了;如果你能说到这里并且面试官没有打断你,那么恭喜你就应该稳了;但是我们不要断继续说双列集合:
双列集合Map是一个键值对集合,存储键、值和之间的映射。 Key无序,唯一;value 不要求有序,允许重复。Map没有继承于Collection接口,从Map集合中检索元素时,只要给出键对象,就会返回对应的值对象。Map的常用实现类:HashMap、TreeMap、HashTable等等;接着我们就可以说说他们之间的关系以及各自的特点:
Hashtable键不可重复,值可以重复;底层是Hash表;Key和Value都不能为Null;很多映射的常用功能与HashMap类似,不同的是它承自 Dictionary 类,并且是线程安全的,任一时间只有一个线程能写 Hashtable,并发性不如 ConcurrentHashMap,因为 ConcurrentHashMap 引入了分段锁。Hashtable 不建议在新代码中使用,不需要线程安全的场合可以用 HashMap 替换,需要线程安全的场合可以用 ConcurrentHashMap 替换;
TreeMap实现SortedMap接口,键不可重复,值可以重复;底层是基于红黑树(Red-Black tree)实现;是一个有序的key-value集合;线程非同步的,能够把它保存的记录根据键排序,默认是按键值的升序排序,也可以指定排序的比较器,当用 Iterator 遍历 TreeMap 时,得到的记录是排过序的。如果使用排序的映射,建议使用 TreeMap。在使用 TreeMap 时,key 必须实现 Comparable 接口或者在构造 TreeMap 传入自定义的Comparator,否则会在运行时抛出 java.lang.ClassCastException 类型的异常;
HashMap 键不可重复,值可以重复;底层是Hash表;key和value都可以为null;根据键的hashCode值存储数据,大多数情况下可以直接定位到它的值,因而具有很快的访问速度,但遍历顺序却是不确定的。 HashMap 最多只允许一条记录的键为 null,允许多条记录的值为 null。HashMap 非线程安全,即任一时刻可以有多个线程同时写 HashMap,可能会导致数据的不一致;
说完三个基本特点后,我们可以着重说一下HashMap的实现原理,因为我认为他是整个集合框架里比较重要的一个模块:
HashMap概述
HashMap是基于哈希表的Map接口的非同步实现。此实现提供所有可选的映射操作,并允许使用null值和null键。此类不保证映射的顺序,特别是它不保证该顺序恒久不变。
HashMap的数据结构
在Java编程语言中,最基本的结构就是两种,一个是数组,另外一个是模拟指针(引用),所有的数据结构都可以用这两个基本结构来构造的,HashMap也不例外。HashMap实际上是一个“链表散列”的数据结构,即数组和链表的结合体。
HashMap基于Hash算法实现的
- 当我们往HashMap中put元素时,利用key的hashCode重新hash计算出当前对象的元素在数组中的下标;
- 存储时,如果出现hash值相同的key,此时有两种情况:
如果key相同,则覆盖原始值;
如果key不同(出现冲突),则将当前的key-value放入链表中;- 获取时,直接找到hash值对应的下标,在进一步判断key是否相同,从而找到对应值;
- HashMap是如何解决hash冲突的问题,核心就是使用了数组的存储方式,然后将冲突的key的对象放入链表中,一旦发现冲突就在链表中做进一步的对比。需要注意JDK1.8中对HashMap的实现做了优化,当链表中的节点数据超过八个之后,该链表会转为红黑树来提高查询效率,从原来的O(N)到O(logN);
在Java中保存数据有两种比较简单的数据结构:数组和链表。数组的特点是:寻址容易,插入和删除困难;链表的特点是:寻址困难,但插入和删除容易;所以我们将数组和链表结合在一起,发挥两者各自的优势,使用一种叫做拉链法的方式可以解决哈希冲突;
HashMap JDK1.8之前:
JDK1.8之前采用的是拉链法。拉链法:将链表和数组相结合。也就是说创建一个链表数组,数组中每一格就是一个单向链表。若遇到哈希冲突,则将冲突的值加到链表中即可;
HashMap JDK1.8之后:
我们知道之前查找的时候,根据hash值我们能够快速定位到数组的具体下标,但是之后的话,需要顺着链表一个个比较下去才能找到我们需要的,时间复杂度取决于链表的长度为O(N)。相比于之前的版本,为了降低这部分的开销JDK1.8在解决哈希冲突时有了较大的变化,当链表长度大于阈值(默认为8)时,会将链表转换为红黑树,在这些位置进行查找时可以降低时间复杂度,以减少搜索时间由O(N)变为O(logN);