集合框架的好处
- 容量自增长;
- 提供了高性能的数据结构和算法,使编码更加方便,提高了程序的速度和质量;
- 允许不同API之间的互操作,不同API之间可以互相传递集合;
- 可以方便的扩展或修改接口,提高了代码的复用性和可操作性;
- 通过使用jdk提供的集合框架,降低了代码维护和学习新API的成本。
常用集合类
- Collection和Map是所有集合框架的父接口,Collection子接口有set,list,queue;
- set的实现类主要有:HashSet,TreeSet,LinkedHashSet;
- list的实现类主要有:ArrayList,LinkedList,Vector,stack;
- queue的实现类主要有:PriorityQueue
- Map的实现类主要有:HashMap,TreeMap,LinkedHashMap,HashTable
数组跟集合的区别
- 数组是固定长度的,集合是不固定长度的;
- 数组中的元素都是同一类型的,集合中的元素可以不是同一类型的;
- 数组可以存放基本数据类型,集合不可以,只能存储引用类型。
List,Set,Map三者的区别
- List和Set都是Collection的子类,而Map不是,Map里面存放的是键值对;
- List里面的元素是有序的(取出元素的顺序跟插入元素的顺序能保持一致),而Set跟Map都是无序的;
- List可以存储重复元素,Set和Map的key键不能存放重复元素;
- List可以放null值,Set和Map的实现类如果是HashSet和HashMap那就可以存放null值;
- List可以通过for循环来遍历,Set和Map只能通过迭代器来进行遍历。
集合中的Inerator
- Iterator是面向对象的一种设计模式,屏蔽了不同集合的特点,不需要确切知道集合,只要是Collection的子类,就可以用Interator来进行迭代;
- Iteretor是“fail—fast”机制,在迭代过程中不能对元素内容进行修改,如果修改了,那下次迭代就会抛异常。Iterator在迭代过程中会直接访问集合元素中的内容,在迭代过程中会使用一个modcount变量。如果集合元素中的内容修改了,那modcount也会修改。每次在用hasNext()和next()来判断时,都会检测modcount是否符合预期的modcount,如果符合就迭代,如果不符合就会终止迭代并抛出异常。
Iterator和ListIterator的区别
- ListIterator只能遍历list,Iterator可以遍历整个Collection
- Iterator只能单向遍历,ListIterator可以双向遍历
- ListIterator实现了Iterator接口,而且还添加了别的API
怎么确保一个集合不能被修改
Collection<String> clist = Collections. unmodifiableCollection();
可以用Collections.unmodifiableCollection(Collection c),如果被修改,就会抛出异常。
遍历List的方式
- for循环,基于计数器原理。在外部维护一个计数器,依次遍历每一个位置的元素;
- Iterator,用迭代器来进行遍历;
- for each语句,本质上还是迭代器,只是写法更加简洁。
ArrayList跟LinkedList的区别
- Arraylist底层结构是动态数组,LinkedList底层结构是双向链表;
- 因为ArrayList底层结构是数组,而且也实现了RandomAcces接口,支持随机访问,所在查询效率非常高,可以达到O(1);而LinkedList只能从头依次遍历,查询比较慢
- 在插入和删除元素时,ArrayList每次都要移动元素,在插入时还需要判断是否需要扩容,所以比较繁琐;LinkedList在插入和删除时只需要改变一下节点之间的指向,非常方便。
- 在空间上,LinkedList的每个节点还需要再多开辟出两块内存来存放该节点的前驱和后继;ArrayList也可能会造成空间浪费,可能开辟了长度为50的数组,但只存入了30个元素。
- 所以,ArrayList适用于频繁查询元素增删少的场景,LinkedList适用于插入和删除操作比较多的场景。
- 两者都不是线程安全的。
ArrayList和Vector的区别
- ArrayList是非线程安全的,Vector是线程安全的,因为是线程安全的,所以性能就比ArrayList低一些;
- ArrayList扩容时每次扩到原来的1.5倍,Vector扩容到原来的2倍
多线程场景下如何使用ArrayList
可以通过Collections.synchronizedList()将其转化为线程安全的集合
为什么ArrayList的element会加上transient
ArrayList实现了serializable接口,说明ArrayList支持序列化,但element前面又加上了transient,说明不希望将element序列化。重写了writeObject方法,在序列化时,先调用defaultWriteObject方法将非transient元素序列化,然后再遍历element数组,只将存入的元素序列化,这样既加快了序列化的速度,也减小了序列化后的文件大小。
HashSet的实现原理
HashSet底层是用HashMap实现的,HashSet的值放在HashMap的key,value统一为present,HashSet的API也大都是调用HashMap的相关API来实现的。
Queue中的poll和remove有什么区别
当队列中没有元素时,poll()会返回null,remove会抛出异常。
PriorityQueue
- PriorityQueue中的元素必须要能比较大小,而且不能为null
- PriorityQueue默认使用的是comparable来比较大小
HashMap的实现原理
- HashMap的数据结构:在java中,有两种基本的结构,一种是数组,一种是链表,基本所有的数据结构都可以由这两种结构构造出来,HashMap也不例外,HashMap采用的是数组+链表的结构,也就是链地址法,数组寻址容易但增删比较麻烦,链表增删容易但寻址比较麻烦,HashMap将两者结合起来,发挥各自的优势,在没有发生冲突时,用数组来存放元素,发生冲突时就用链表来存放冲突元素。jdk1.8中,当某条链表过长时又会转化为红黑树来进行优化。
- 当往HashMap中放入元素的时候,先将key的hashCode重新hash一下,然后再将hash值跟数组长度-1按位与得到在数组中的下标i,如果存在与key的hash值相同的元素就直接覆盖,否则就将键值对放在链表中;当取元素的时候,同样先找到hash值所对应的下标,然后再找到key相同的值。
put()方法的具体实现
- 首先判断table是否为空和table长度是否为0,如果数组为空或者长度为零,则先扩容;
- 将key的hashCode重新hash一下,再将hash值跟(table.length-1)按位与,得到key在数组中的索引下标i,如果table[i]为空,那就直接插入成功,如果不为空就进行后续操作;
- 判断首元素是否跟key相同,如果相同就直接覆盖,如果不相同,就判断首元素是否为红黑树,如果为红黑树,那就在树中进行元素的插入,如果不为红黑树,那就遍历链表;
- 在遍历链表时会统计节点个数,如果找到跟key相同的元素,直接退出遍历;如果找到最后一个元素还没有找到相同的元素,就尾插在后面,再判断是否达到转化成红黑树的条件,如果达到了就转化成红黑树;
- 如果找到相同的元素就直接覆盖,返回旧值;
- 判断table中的键值对数量是否超过threshold,如果超过了就扩容。
java7跟java8HashMap的不同
- 存储结构:java7为数组+链表;java8为数组+链表+红黑树
- 初始化方式:java7为单独的一个函数;java8集成到resize()中,而且并没有在构造方法中直接给数组开辟内存,而是延迟到了第一次put()的时候
- hash计算的方式:java7中9次扰动,4次位运算+5次异或运算;java8中2次扰动,1次位运算+1次异或运算
- 存储数据的规则:java7:当没有发生冲突时,存放在数组中,发生冲突时存放在链表中;java8:没有发生冲突时,存放在数组中,发成冲突时如果数组长度超过64并且链表长度超过8就会放在红黑树中,否则放在链表中
- 插入数据的方式:java7:头插,先将首元素往后移动一个位置,然后再插入新元素,在多线程环境下可能会死循环;尾插,在多线程下虽然避免了死循环,但还是不安全的,可能会造成数据丢失
- 扩容后存储位置的计算方式:java7:还是跟以前一样,将key的hashCode重新hash一下,然后计算在扩容后的数组中的位置;java8:将扩容后的容量跟hash值按位与,只需要看hash值中对应扩容后多出来的那一位为1还是为0
HashMap是如何解决hash冲突的
- hash冲突就是不同的元素通过同一个hash函数计算出来的hash值一样
- 采用数组+链表+红黑树的结构,也就是链地址法,链表用来存放hash冲突的键值对,当键值对过多时就将链表转化为红黑树,降低遍历的时间复杂度
- hash函数的计算方式:hashCode的高16位不变,让低16位跟高16位按位异或,让更多的比特位参与运算,减少碰撞
HashMap的容量为什么总是2的N次幂
- 在计算key在数字中的索引位置时,可以用hash&(table.length-1),在容量为2的N次幂时等价于除留余数法,但是位运算效率更高
- 扩容后存储位置的计算方式:将扩容后的容量跟hash值按位与,只需要看hash值中对应扩容后多出来的那一位为1还是为0,如果为0就不需要移动,如果为1就移动到现在的位置+原容量的位置,效率也更高
能否将任何类作为key存入HashMap
可以,只要该类重写了hashCode和equals方法
- equals相等hashCode一定相等
- hashCode相等equals不一定相等
- 两个相同的元素,它们的equals方法都返回true
HashMap为什么不直接用hashCode作为数组中的下标
- hashCode的返回值是int,取值范围在
-2^31~2^31-1
,而HashMap的取值范围在16~2^30
,而且通常情况下是取不到最大值的,就有可能会出现通过hashCode计算出来的值不在HashMap中,设备也很难一下子开辟出这么大的内存 - 直接用hashCode碰撞率比较大,将hashCode的高16位跟低16位做个异或,让更多比特位参与运算,减少了碰撞。
HashMap跟HashTable的区别
- HashMap是线程非安全的,HashTable是线程安全的,所以效率
比较低一些 - HashMap的key可以为null,HashTable不能
- HashMap的默认容量大小为16,每次扩容都是2的N次幂;HashTable默认容量为11,每次都扩容到2n+1
- HashMap底层数据结构是数组+链表+红黑树;HashTable底层数据结构是数组+链表
HashMap和ConcurrentHashMap
- HashMap不是线程安全的,ConcurrentHashMap是线程安全的
- HashMap的key可以存放null,ConcurrentHashMap不能
ConcurrentHashMap和HashTable
都是线程安全的,但是实现线程安全的机制不一样
- HashTable对整个数组加了一把锁,并发效率比较低,只要访问数组中的元素,就需要竞争锁
- ConcurrentHashMap1.7:采用分割分段的方法,给数组分段,每一段加上一把锁,如果两个线程访问的不是一个段内的数据就不用竞争锁,大大提高了效率,默认分配了16个Segment,Segment相当于锁,HashEntry数组存放键值对,如果想要访问HashEntry数组中某个键值对,就需要获取对应的Segment。
- ConcurrentHashMap1.8:采用Node+CAS+synchronized来保证线程安全,synchronized只锁每个链表或红黑树的头结点
Comparable和Comparator
- Comparable是java.lang包中,实现Comparable接口,需要重写comparTo方法
- Comparator是java.uitl包中,实现Comparator接口,需要重写compare方法
Collection和Collections
- Collection:是一个集合接口,提供了对集合对象进行操作的各种通用接口方法
- Collections:是集合类的一个工具类,里面有一系列的静态方法,用于多Collection排除,查找,线程安全等操作