考察重点:1、2,5,6
1、请说明List、Map、Set三个接口存取元素时,各有什么特点?参考回答:2、阐述ArrayList、Vector、LinkedList的存储性能和特性参考回答:3、请简要说明ArrayList,Vector,LinkedList的存储性能和特性是什么?参考回答:4、请说明ArrayList和LinkedList的区别?参考回答:5、请你说明HashMap和Hashtable的区别? 参考回答:6、请说说快速失败(fail-fast)和安全失败(fail-safe)的区别?参考回答:7、请简单说明一下什么是迭代器?参考回答:8、请说明ArrayList是否会越界?参考回答:9、请解释一下TreeMap?参考回答:10、请说明Java集合类框架的基本接口有哪些?参考回答:11、请你简单介绍一下ArrayList和LinkedList的区别,并说明如果一直在list的尾部添加元素,用哪种方式的效率高?参考回答:12、Map的遍历方式?13、HashMap的自动扩容为什么是两倍?14、集合的负载因子15、Arraylist和linkedList的区别“List专题:16、ArrayList(1)底层是数组实现的(2)扩容机制(做了什么操作,为什么是容量无限)(3)为什么是线程不安全的?(4)能够序列化吗,为什么数组要用trsient修饰?(5)为什么ArrayList的增删效率低?(6)为什么ArrayList的查询效率高?(7)Arraylist的特点:(8)如何使得ArrayList变得线程安全?(9)Linkedlist和ArrayList谁更占空间?(10)什么情况下ArrayList会发生越界?(11)Array(数组)和ArrayList的区别17、LinkedList(1)基本结构(2)特点18、Vector19、ArrayList,LinkedList,vector的区别,相同点,存储性能,查询,增删的差别?Set专题20、java中hashcode的作用?21、equals和==的区别?22、HashSet(1)什么是哈希冲突?(2)基本结构(3)实现原理:(4)HashSet怎么保证元素不重复得?(5)为什么使用Object类时候必须重写.equals和hashcode方法?(6)HashSet的特点:23、LinkedHashSet,TreeSet,HashSet的区别Map专题24、HashMap(1)Hashmap的底层原理是什么(2)HashMap在JDK1.7和JDK1.8中有哪些不同?HashMap的底层实现(3)HashMap的put方法的具体流程?(3)HashMap的扩容操作是怎么实现的?(4)Hashmap的数组长度为什么是2的n次幂?(5)HashMap的Hash值为什么要进行两次扰动?(6)Hashmap如何解决哈希冲突的?(7)HashMap为什么不直接使用hashCode()处理后的哈希值直接作为table的下标?(8)为什么HashMap中String、Integer这样的包装类适合作为K?(8)一般用什么类作为HashMap的key?(9)如果使用Object作为HashMap的Key,应该怎么办呢?(10)特点(11)HashMap默认加载因子为什么选择0.75?(12)HashMap为什么是不安全的?(13)解决哈希冲突的方法有哪些,HashMap采取的哪一种?(14)可以用链表代替数组吗?(15)HashSet和HashMap的区别(16)LinkedHashMap的底层原理(17)) 如何决定使用 HashMap 还是 TreeMap?(18) HashMap在JDK1.7和JDK1.8中有哪些不同?25、Treemap,treeSet26、Hashtable(1)底层原理(2)为什么ConcurrentHashMap不能完全替代HashTable27、ConcurrentHashMap(1)ConcurrentHashMap的put方法流程?(2)ConcurrentHashMap 的并发度是什么 ?(3)ConcurrentHashMap的get方法是否要加锁为什么?(4)为什么它的key和value都不能null(5)ConcurrentHashMap在JDK 7和8之间的区别**(6)1.8为什么使用内置锁synchronized来代替重入锁ReentrantLock:(7)什么是锁分段技术(8)它的迭代器是强一致性还是弱一致性?(9)和Hashtable的区别以及效率28、集合框架(1)什么是集合(2)集合和数组的区别(3)常见的集合类(4)请说明List、Map、Set三个接口存取元素时,各有什么特点?(5)集合框架底层数据结构(6)java集合的fail-fast机制?(重要)(7)fail-fast和fail-safe的区别?(重要)(8) 请简单说明一下什么是迭代器?(记忆)(9)map接口的遍历方法(10)在ArrayLIst和LinkedList尾部加元素,谁的效率高?(11)请你说明一下TreeMap的底层实现?
1、请说明List、Map、Set三个接口存取元素时,各有什么特点?
考察点:List
参考回答:
List以特定索引来存取元素,可以有重复元素。Set不能存放重复元素(用对象的equals()方法来区分元素是否重复)。Map保存键值对(key-value pair)映射,映射关系可以是一对一或多对一。Set和Map容器都有基于哈希存储和排序树的两种实现版本,基于哈希存储的版本理论存取时间复杂度为O(1),而基于排序树版本的实现在插入或删除元素时会按照元素或元素的键(key)构成排序树从而达到排序和去重的效果。
注:这里的排序树我觉得应该是红黑树,因为TreeSet和TreeMap都是基于红黑树实现的
2、阐述ArrayList、Vector、LinkedList的存储性能和特性
考察点:ArrayList
参考回答:
ArrayList 和Vector都是使用数组方式存储数据,此数组元素数大于实际存储的数据以便增加和插入元素,它们都允许直接按序号索引元素,但是插入元素要涉及数组元素移动等内存操作,所以索引数据快而插入数据慢,Vector中的方法由于添加了synchronized修饰,因此Vector是线程安全的容器,但性能上较ArrayList差,因此已经是Java中的遗留容器。LinkedList使用双向链表实现存储(将内存中零散的内存单元通过附加的引用关联起来,形成一个可以按序号索引的线性结构,这种链式存储方式与数组的连续存储方式相比,内存的利用率更高),按序号索引数据需要进行前向或后向遍历,但是插入数据时只需要记录本项的前后项即可,所以插入速度较快。Vector属于遗留容器(Java早期的版本中提供的容器,除此之外,Hashtable、Dictionary、BitSet、Stack、Properties都是遗留容器),已经不推荐使用,但是由于ArrayList和LinkedListed都是非线程安全的,如果遇到多个线程操作同一个容器的场景,则可以通过工具类Collections中的synchronizedList方法将其转换成线程安全的容器后再使用(这是对装潢模式的应用,将已有对象传入另一个类的构造器中创建新的对象来增强实现)。
注:此数组元素数大于实际存储的数据以便增加和插入元素:ArrayList在初始化的时候默认容量为10,在插入元素导致数组长度不够的时候,数组会扩容到当前长度的【1.5倍】+1 ????????
3、请简要说明ArrayList,Vector,LinkedList的存储性能和特性是什么?
考察点:ArrayList
参考回答:
ArrayList 和Vector都是使用数组方式存储数据,此数组元素数大于实际存储的数据以便增加和插入元素,它们都允许直接按序号索引元素,但是插入元素要涉及数组元素移动等内存操作,所以索引数据快而插入数据慢,Vector由于使用了synchronized方法(线程安全),通常性能上较ArrayList差,而LinkedList使用双向链表实现存储,按序号索引数据需要进行前向或后向遍历,但是插入数据时只需要记录本项的前后项即可,所以插入速度较快。
4、请说明ArrayList和LinkedList的区别?
考察点:ArrayList
参考回答:
ArrayList和LinkedList都实现了List接口,他们有以下的不同点: ArrayList是基于索引的数据接口,它的底层是数组。它可以以O(1)时间复杂度对元素进行随机访问。与此对应,LinkedList是以元素列表的形式存储它的数据,每一个元素都和它的前一个和后一个元素链接在一起,在这种情况下,查找某个元素的时间复杂度是O(n)。 相对于ArrayList,LinkedList的插入,添加,删除操作速度更快,因为当元素被添加到集合任意位置的时候,不需要像数组那样重新计算大小或者是更新索引。 LinkedList比ArrayList更占内存,因为LinkedList为每一个节点存储了两个引用,一个指向前一个元素,一个指向下一个元素。
5、请你说明HashMap和Hashtable的区别?
考察点:集合
参考回答:
HashMap和Hashtable都实现了Map接口,因此很多特性非常相似。但是,他们有以下不同点: HashMap允许键和值是null,而Hashtable不允许键或者值是null。 Hashtable是同步的,而HashMap不是。因此,HashMap更适合于单线程环境,而Hashtable适合于多线程环境。
HashMap提供了可供应用迭代的键的集合,因此,HashMap是快速失败的。另一方面,Hashtable提供了对键的列举(Enumeration)。(不理解) 一般认为Hashtable是一个遗留的类。
6、请说说快速失败(fail-fast)和安全失败(fail-safe)的区别?
考察点:集合
参考回答:
Iterator的安全失败是基于对底层集合做拷贝,因此,它不受源集合上修改的影响。java.util包下面的所有的集合类都是快速失败的,而java.util.concurrent包下面的所有的类都是安全失败的。快速失败的迭代器会抛出ConcurrentModificationException异常,而安全失败的迭代器永远不会抛出这样的异常。
快速失败是直接对原集合遍历的,数据修改后可能抛出异常,而安全失败是对原集合的拷贝遍历的,不会抛出异常。 快速失败和安全失败的区别 Iterator的安全失败是基于对底层集合做拷贝,因此,它不受源集合上修改的影响。
在使用迭代器对集合对象进行遍历的时候,如果 A 线程正在对集合进行遍历,此时 B 线程对集合进行修改(增加、删除、修改),或者 A 线程在遍历过程中对集合进行修改,都会导致 A 线程抛出ConcurrentModificationException 异常。
原因:
因为java.util下他用迭代器迭代都是直接操作的原来数据,而不是操作副本,迭代器在遍历时直接访问集合中的内容,并且在遍历过程中使用一个 modCount 变量。集合在被遍历期间如果内容发生变化,就会改变modCount的值。每当迭代器使用hashNext()/next()遍历下一个元素之前,都会检测modCount变量是否为expectedmodCount值,是的话就返回遍历;否则抛出异常,终止遍历。
所以java.util.concurrent包下面的vetor虽然是单线程的,但是也使用了迭代器对原集合数据进行操作,所以也会报错
面试官:说说快速失败和安全失败是什么 - 知乎 (zhihu.com)
7、请简单说明一下什么是迭代器?
考察点:JAVA迭代器
参考回答:
Iterator提供了统一遍历操作集合元素的统一接口, Collection接口继承Iterable接口, 每个集合都通过实现Iterable接口中iterator()方法返回Iterator接口的实例, 然后对集合的元素进行迭代操作. 有一点需要注意的是:在迭代元素的时候不能通过集合的方法删除元素, 否则会抛出ConcurrentModificationException 异常. 但是可以通过Iterator接口中的remove()方法进行删除.
如果使用集合对象删除增加元素就会报错,这是因为多线程的原因????(可以用迭代器的方法remove),但是如果使用线程安全的集合便可以(和6有关)
8、请说明ArrayList是否会越界?
考点:集合
参考回答:
ArrayList是实现了基于动态数组的数据结构,而LinkedList是基于链表的数据结构2. 对于随机访问get和set,ArrayList要优于LinkedList,因为LinkedList要移动指针;ArrayList并发add()可能出现数组下标越界异常。
注意是并发多线程的时候可能会出现
9、请解释一下TreeMap?
考察点:key-value集合
参考回答:
TreeMap是一个有序的key-value集合,基于红黑树(Red-Black tree)的 NavigableMap实现。该映射根据其键的自然顺序进行排序,或者根据创建映射时提供的 Comparator进行排序,具体取决于使用的构造方法。
reeMap 的实现就是红黑树数据结构,也就说是一棵自平衡的排序二叉树,这样就可以保证当需要快速检索指定节点。红黑树的插入、删除、遍历时间复杂度都为O(lgN),所以性能上低于哈希表。但是哈希表无法提供键值对的有序输出,红黑树因为是排序插入的,可以按照键的值的大小有序输出。
注意:TreeMap的键是根据自然规则挥着比较器规则来进行排序的,此时的有序输出是指根据排序规则有序输出,和LinkedHashMap按照添加顺序输出的意义不一样,因此他也是无序的
10、请说明Java集合类框架的基本接口有哪些?
考察点:JAVA集合
参考回答:
集合类接口指定了一组叫做元素的对象。集合类接口的每一种具体的实现类都可以选择以它自己的方式对元素进行保存和排序。有的集合类允许重复的键,有些不允许。 Java集合类提供了一套设计良好的支持对一组对象进行操作的接口和类。Java集合类里面最基本的接口有: Collection:代表一组对象,每一个对象都是它的子元素。 Set:不包含重复元素的Collection。 List:有顺序的collection,并且可以包含重复元素。 Map:可以把键(key)映射到值(value)的对象,键不能重复。
11、请你简单介绍一下ArrayList和LinkedList的区别,并说明如果一直在list的尾部添加元素,用哪种方式的效率高?
考点:集合
参考回答:
ArrayList采用数组数组实现的,查找效率比LinkedList高。LinkedList采用双向链表实现的,插入和删除的效率比ArrayList要高。一直在list的尾部添加元素,LinkedList效率要高。
这个是针对于低数据量插入说的?高的时候呢
12、Map的遍历方式?
可以使用entrySet方法获取Entry并使用迭代器或者foreach循环遍历
直接用foreach循环或者迭代器直接遍历其keyset或者values
13、HashMap的自动扩容为什么是两倍?
第一是因为哈希函数的问题 通过除留余数法方式获取桶号,因为Hash表的大小始终为2的n次幂,因此可以将取模转为位运算操作,提高效率,容量n为2的幂次方,n-1的二进制会全为1,位运算时可以充分散列,避免不必要的哈希冲突,这也就是为什么要按照2倍方式扩容的一个原因 第二是因为是否移位的问题 是否移位,由扩容后表示的最高位是否1为所决定,并且移动的方向只有一个,即向高位移动。因此,可以根据对最高位进行检测的结果来决定是否移位,从而可以优化性能,不用每一个元素都进行移位,因为为0说明刚好在移位完之后的位置,为1说明不是需要移动oldCop,这也是其为什么要按照2倍方式扩容的第二个原因。 ———————————————— 原文链接:HashMap底层的扩容机制(以及2倍扩容的原因)_lzh_99999的博客-CSDN博客_hashmap扩容为什么是2倍
14、集合的负载因子
当元素个数超过集合大小的负载因子倍数时候,集合会扩容
比如ArrayList,vector是1,扩容增量分别为1.5倍+1, 2倍
ArrayList默认初始容量是10
HashSet,Hashmap是0.75,扩容增量分别为2倍,两倍,初始容量均为16
15、Arraylist和linkedList的区别“
ArrayList 和 LinkedList 的区别是什么?
数据结构实现:ArrayList 是动态数组的数据结构实现,而 LinkedList 是双向链表的数据结构实现。 随机访问效率:ArrayList 比 LinkedList 在随机访问的时候效率要高,因为 LinkedList 是线性的数据存储方式,所以需要移动指针从前往后依次查找。 增加和删除效率:在非首尾的增加和删除操作,LinkedList 要比 ArrayList 效率要高,因为 ArrayList 增删操作要影响数组内的其他数据的下标。 内存空间占用:LinkedList 比 ArrayList 更占内存,因为 LinkedList 的节点除了存储数据,还存储了两个引用,一个指向前一个元素,一个指向后一个元素。 线程安全:ArrayList 和 LinkedList 都是不同步的,也就是不保证线程安全
List专题:
16、ArrayList
(1)底层是数组实现的
也就是:
transient Object[] elementData;
它没有被final修饰
(2)扩容机制(做了什么操作,为什么是容量无限)
在调用无参构造的时候会创建一个空的数组,向里面添加值得时候会默认分配10的容量
动态数组,容量是无限的,因为它可以自动扩容
在对数据进行add操作时,会分配默认10的初始容量。
插入元素时检查数组长度是否已满,已满触发扩容
扩容的过程主要分为3步
-
创建一个原数组长度1.5倍的新数组
-
调用System.arraycopy()把原数组的数据复制到新数组(下标保持不变)
-
把指向原数组的地址换到新数组地址
说到扩容那就再说一下,ArrayList没有提供自动缩容的机制,调用remove方法的话只是删除元素,数组长度不会发生变化。只有主动调用trimToSize()才会把ArrayList内置的数组缩容到当前的size
。
扩容机制:
ArrayList扩容机制为:新容量为原容量的1.5倍取整,或者是新容量为旧容量加上旧容量右移一位,推荐后一种说法。
(18条消息) ArrayList的扩容机制,扩容为原容量的1.5倍这种说法严谨吗?ql_7256的博客-CSDN博客arraylist扩容为什么是1.5倍
(3)为什么是线程不安全的?
因为其方法没有加锁修饰,并且添加方法不是原子操作
(4)能够序列化吗,为什么数组要用trsient修饰?
可以序列化,但是序列化操作是通过writeObject和readObject来实现的
既然要将ArrayList的字段序列化(即将elementData序列化),那为什么又要用transient修饰elementData呢?
回想ArrayList的自动扩容机制,elementData数组相当于容器,当容器不足时就会再扩充容量,但是容器的容量往往都是大于或者等于ArrayList所存元素的个数。
比如,现在实际有了8个元素,那么elementData数组的容量可能是8x1.5=12,如果直接序列化elementData数组,那么就会浪费4个元素的空间,特别是当元素个数非常多时,这种浪费是非常不合算的。
所以ArrayList的设计者将elementData设计为transient,然后在writeObject方法中手动将其序列化,并且只序列化了实际存储的那些元素,而不是整个数组。
这样的目的是为了节省资源(只序列化那些存储元素,而不序列化那些有内存但是不存储值得位置)
(5)为什么ArrayList的增删效率低?
ArrayList 在小于扩容容量的情况下其实增加操作效率是非常高的,在涉及扩容的情况下添加操作效率确实低,删除操作需要移位拷贝,效率是低点。因为 ArrayList 中增加(扩容)或者是删除元素要调用 System.arrayCopy 这种效率很低的方法进行处理,所以如果遇到了数据量略大且需要频繁插入或删除的操作效率就比较低了,具体可查看 ArrayList 的 add 和 remove 方法实现,但是 ArrayList 频繁访问元素的效率是非常高的
对比LinkedList,插入对象只需要记录其前后项的地址即可,因此效率较高
注:ArrayList对于在末尾插入元素如果是需要扩容那么添加的效率很低,如果是使用add(int index,int ss)对中间进行插入,会导致后面的对象都向后移位,同理删除也会导致后面的对象向前移位,因此效率低
(6)为什么ArrayList的查询效率高?
查询时,使用首地址+元素字节数*下标,也就是说内存地址是连续的
LinkedList由于内存地址不是连续的需要从前向/后向遍历查询
O(1)和O(n)的区别
LinkedList查询方式:
查询数据效率就比较低,查询数据时,首先会计算下链表总长度的一半,判断对应索引是在该值的左边还是右边,然后决定从头节点还是尾节点开始遍历
(7)Arraylist的特点:
底层数组,数据有序,值可以重复,可以插入多个null,非线程安全,增删效率低,查找效率高,无限容量。
(8)如何使得ArrayList变得线程安全?
使用Collections.synchronizedList。它会自动将我们的list方法进行改变,最后返回给我们一个加锁了List
(9)Linkedlist和ArrayList谁更占空间?
一般情况下,LinkedList的占用空间更大,因为每个节点要维护指向前后地址的两个节点,但也不是绝对,如果刚好数据量超过ArrayList默认的临时值时,ArrayList占用的空间也是不小的,因为扩容的原因会浪费将近原来数组一半的容量,不过,因为ArrayList的数组变量是用transient关键字修饰的,如果集合本身需要做序列化操作的话,ArrayList这部分多余的空间不会被序列化。
因为ArrayList的扩容操作可能会导致有一部分的空间无法被利用
(10)什么情况下ArrayList会发生越界?
访问下标超过当前数组容量的时候
多线程下对ArrayList进行添加操作得时候
(11)Array(数组)和ArrayList的区别
ArrayList可以算是Array的加强版,(对array有所取舍的加强)。
1、存储内容比较:
Array数组可以包含基本类型和对象类型, ArrayList却只能包含对象类型。
2、但是需要注意的是:Array数组在存放的时候一定是同种类型的元素。ArrayList就不一定了,因为ArrayList可以存储Object。
3、空间大小比较:
Array的空间大小是固定的,需要创建的时候便进行初始化,空间不够时也不能再次申请,所以需要事前确定合适的空间大小。
ArrayList的空间是动态增长的,如果空间不够,它会创建一个空间比原空间大一倍的新数组,然后将所有元素复制到新数组中,接着抛弃旧数组。而且,每次添加新的元素的时候都会检查内部数组的空间是否足够。(比较麻烦的地方)。
4、相同点:都具有索引,所存放的数据都是有序的
17、LinkedList
(1)基本结构
双向链表结构
(2)特点
数据有序,可以插入重复的值,可以插入多条null,增删快,查询慢,非线程安全
有很多关于首尾插入删除的操作:
-
public void addFirst(E e)
:将指定元素插入此列表的开头。 -
public void addLast(E e)
:将指定元素添加到此列表的结尾。 -
public E getFirst()
:返回此列表的第一个元素。 -
public E getLast()
:返回此列表的最后一个元素。 -
public E removeFirst()
:移除并返回此列表的第一个元素。 -
public E removeLast()
:移除并返回此列表的最后一个元素。 -
public E pop()
:从此列表所表示的堆栈处弹出一个元素。 -
public void push(E e)
:将元素推入此列表所表示的堆栈。等效于addFirst -
public boolean isEmpty()
:如果列表不包含元素,则返回true。
查询使用了简单二分法:
从源码中我们可以发现,LinkedList并没有采用从头循环到尾的做法,而是采取了简单二分法, 首先看看 index是在链表的前半部分,还是后半部分。 如果是前半部分,就从头开始寻找,反之亦然。 通过这种方式,使循环的次数至少降低了一半,提高了查找的性能
18、Vector
其和ArrayList的主要两点区别是:
1、是线程安全的,主要表现为每个方法都加上了锁,是同步的,但是系统开销大
2、底层也是数组实现的,不过在扩容时,新数组长度是原数组的两倍
19、ArrayList,LinkedList,vector的区别,相同点,存储性能,查询,增删的差别?
略,参考上面的
Set专题
20、java中hashcode的作用?
1、在java中hashcode方法是Object类的native方法,返回值为int类型,根据一定的规则将与对象相关的信息(比如对象的存储地址,对象的字段等)映射成一个数值,这个数值称作为散列值。
2、作用:
在java中hashcode是用于快速查找对象物理存储区使用的,主要作用于散列集合(HashSet,HashMap,HashTable...),在插入散列集合前需要判断obj是否存在,首先判断插入obj的hashcode值是否存在,hashcode值不存在则直接插入集合,值存在还需判断equals方法判断对象是否相等。使用hashcode码确定对象存放区,若存放区不存在则对象一定不存在无需equals判断直接插入。若该区存在只比较该区对象equals判断。所以这种方法大量节省判断时间。 对于散列集合,提高其判断速度的方法是:首先根据hashcode,相同则根据equals方法判断两个对象是否相同,如果不同那么这两个对象一定不同
21、equals和==的区别?
equals被用来判断两个对象是否相等。
equals通常用来比较两个对象的内容是否相等,==用来比较两个对象的地址是否相等。
equals方法默认等同于“==”
Object类中的equals方法定义为判断两个对象的地址是否相等(可以理解成是否是同一个对象),地址相等则认为是对象相等。这也就意味着,我们新建的所有类如果没有复写equals方法,那么判断两个对象是否相等时就等同于“==”,也就是两个对象的地址是否相等。
22、HashSet
(1)什么是哈希冲突?
不同关键字通过相同哈希函数计算出相同的哈希地址,该种现象称为哈希冲突或哈希碰撞。
自己:不同对象通过哈希函数计算得到相同的哈希值,叫做哈希冲突
(2)基本结构
数组+链表/数组+红黑树
在JDK1.8之前,哈希表底层采用数组+链表实现,即使用链表处理冲突,同一hash值的链表都存储在一个链表里。但是当位于一个桶中的元素较多,即hash值相等的元素较多时,通过key值依次查找的效率较低。而JDK1.8中,哈希表存储采用数组+链表+红黑树实现,当链表长度超过阈值(8)时,将链表转换为红黑树,这样大大减少了查找时间。
(3)实现原理:
HashSet 是基于 HashMap 实现的,HashSet的值存放于HashMap的key上,HashMap的value统一为PRESENT,因此 HashSet 的实现比较简单,相关 HashSet 的操作,基本上都是直接调用底层 HashMap 的相关方法来完成,HashSet 不允许重复的值。
(4)HashSet怎么保证元素不重复得?
通过判断hashcode和.equals方法
首先根据hashcode,相同则根据equals方法判断两个对象是否相同,如果不同那么这两个对象一定不同
如果通过遍历发现已经存在相同的对象,那么这个新对象便不会被添加
注意:
Object中的hashcode对于不同对象(Object类的hashcode是根据其内存地址通过C++代码实现的)是不同的,所以为了让不同对象有可能有相同的hashcode,可以重写hashcode方法,比如String类就重写了:
这是原生的hashcode方法(JVM实现底层代码):
(5)为什么使用Object类时候必须重写.equals和hashcode方法?
package copyRandomList; import java.util.HashSet; import java.util.Iterator; import java.util.Objects; public class Main { static class Student{ private String name; //只重写equals方法不重写hashcode public Student(String name) { this.name = name; } @Override public boolean equals(Object o) { if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; Student student = (Student) o; return Objects.equals(name, student.name); } } public static void main(String[] args) { // String a = "通话"; // String b = "重地"; // System.out.println(a.hashCode()); // System.out.println(b.hashCode()); HashSet<Student> students = new HashSet<>(); students.add(new Student("s")); students.add(new Student("s")); students.add(new Student("r")); Iterator<Student> iterator = students.iterator(); while (iterator.hasNext()){ System.out.println(iterator.next().name); } } }
如果只重写equals方法不重写hashcode方法,有可能两个对象的内容是一样的时候比如·都为“s”,但是HashSet判断这两个对象的hashcode不同就会导致直接判定这两个对象不同,这与我们的初衷是不一样的。
如果我们想定义某个对象的某一个或两个属性相同时这两个对象就一杨,那我们需要把这个属性定义为hashcode和equals方法的内容,这样就可以实现hashcode相同,但是对象地址不同然而我们认为它是相同的。
(6)HashSet的特点:
无序,不可重复,可以添加null(但是遍历到null会报错),但是只能添加一条null
23、LinkedHashSet,TreeSet,HashSet的区别
HashSet的底层实现是哈希表,元素是无序的,并且只能插入一条null值
LinkedhashSet继承于HashSet,通过双向链表使得插入元素是有序的,同理只能插入一条Null值
TreeSet的底层实现是二叉树,会对插入的元素自动进行排序,排序的规则是与根节点比较,小于根节点插入到左子树,大于根节点插入到右子树,并以此规则递归,这样可以提高元素的查找效率,对于自定义的类需要实现Comparable接口,并重写CompareTo接口实现自定义比较大小规则;不能插入null值
TreeSet:比较此对象与指定对象的顺序。如果该对象小于、等于或大于指定对象,则分别返回负整数、零或正整数(-1,0,1),0保证了不存入相同元素
相同点:都不能插入相同的元素,都不是线程安全的
HashSet是基于散列表实现的,元素没有顺序;add、remove、contains(查找)方法的时间复杂度为O(1)。
TreeSet是基于树实现的(红黑树),元素是有序的;add、remove、contains方法的时间复杂度为O(log (n))。因为元素是有序的,它提供了若干个相关方法如first(), last(), headSet(), tailSet()等;
LinkedHashSet介于HashSet和TreeSet之间,是基于哈希表和链表实现的,支持元素的插入顺序;基本方法的时间复杂度为O(1);
HashSet: 无序 LinkedHashSet: 按照插入顺序 TreeSet: 从小到大排序
Map专题
24、HashMap
(1)Hashmap的底层原理是什么
HashMap 基于 Hash 算法实现的
当我们往Hashmap中put元素时,利用key的hashCode重新hash计算出当前对象的元素在数组中的下标 存储时,如果出现hash值相同的key,此时有两种情况。(1)如果key相同,则覆盖原始值;(2)如果key不同(出现冲突),则将当前的key-value放入链表中 获取时,直接找到hash值对应的下标,在进一步判断key是否相同,从而找到对应值。 理解了以上过程就不难明白HashMap是如何解决hash冲突的问题,核心就是使用了数组的存储方式,然后将冲突的key的对象放入链表中,一旦发现冲突就在链表中做进一步的对比。 需要注意Jdk 1.8中对HashMap的实现做了优化,当链表中的节点数据超过八个之后,该链表会转为红黑树来提高查询效率,从原来的O(n)到O(logn)
如果key相同则用最新的value值进行替换,但是key是最原始的key
map.put(k1,v1);
map.put(k2,v2);
map.put(k3,v3);
如果判定k1和k3相等,那么最后出现在map中的key是k1,value是v3
同理对于HashSet也是,只会保留最原始的key
(2)HashMap在JDK1.7和JDK1.8中有哪些不同?HashMap的底层实现
HashMap的底层实现是通过数组+链表/红黑树实现的,数组用来存储元素的哈希值。链表/红黑树存储具有相同hash值的元素(解决哈希冲突)
不同:1.7通过数组+链表的方式解决哈希冲突
1.8通过数组+链表+红黑树的方式解决哈希冲突(当哈希冲突的个数超过八个时候转换为红黑树,解决了链表查询效率低的问题)
(3)HashMap的put方法的具体流程?
1、对插入的键值对的key做hash处理,具体就是对于key值得32位hashcode做两次扰动即:
高16位右移到低十六位,高十六位补零,这是位运算
原hashcode和位运算之后得hashcode进行异或操作得到新的hash值
得到该key对应的hash值之后,通过hash&(table.length - 1)的运算得到该key在数组的存储下标
2、在该数组下标下的链表、红黑树中判断该key是否存在在(判断的标准是使用equals方法判断,如果相同)则新添加的键值对直接覆盖原始的value,否则添加至链表/红黑树
3、当某个链表的长度大于8的时候将链表转换为红黑树
4、插入成功后,判断实际存在的键值对数量size是否超多了最大容量threshold,如果超过,进行扩容resize,这时会对键值对进行重新。
(3)HashMap的扩容操作是怎么实现的?
-
在jdk1.8中,resize方法是在hashmap中的键值对大于阀值时(最大容量*0.75)或者初始化时,就调用resize方法进行扩容;
-
每次扩展的时候,都是扩展2倍;
-
扩展后Node对象的位置要么在原位置,要么移动到原偏移量两倍的位置。
-
在putVal()中,我们看到在这个函数里面使用到了2次resize()方法,resize()方法表示的在进行第一次初始化时会对其进行扩容,或者当该数组的实际大小大于其临界值值(第一次为12),这个时候在扩容的同时也会伴随的桶上面的元素进行重新分发,这也是JDK1.8版本的一个优化的地方,在1.7中,扩容之后需要重新去计算其Hash值,根据Hash值对其进行分发,但在1.8版本中,则是根据在同一个桶的位置中进行判断(e.hash & oldCap)是否为0,重新进行hash分配后,该元素的位置要么停留在原始位置(为0),要么移动到原始位置+增加的数组大小这个位置上(为1)
-
正常情况下,计算节点在table中的下标的方法是:hash&(oldTable.length-1),扩容之后,table长度翻倍,计算table下标的方法是hash&(newTable.length-1),也就是hash&(oldTable.length*2-1),于是我们有了这样的结论:这新旧两次计算下标的结果,要不然就相同,要不然就是新下标等于旧下标加上旧数组的长度。
-
所以,注释4处的e.hash & oldCap,就是用于计算位置b到底是0还是1用的,只要其结果是0,则新散列下标就等于原散列下标,否则新散列坐标要在原散列坐标的基础上加上原table长度。
(4)Hashmap的数组长度为什么是2的n次幂?
这是为了使得元素在数组中分布的更加均匀,由于要通过hash % (table.length)来进行求取该元素的数组下标,
一方面,让数组长度是2的n次幂,可以使得求余操作变为与运算,具体为hash值&(table.length - 1),这样可以大大简化计算
第二方面,数组length为偶数的时候,table.length - 1是奇数,这保证了最后一位是1,在与hash值做与运算的时候,最后一位可能是0可能是1,因此这样便增加了下标分布的随机性
总之,简化了计算量以及减少了哈希碰撞,使得数据分布更加均匀
(5)HashMap的Hash值为什么要进行两次扰动?
为了使得高位参加运算,减少哈希碰撞,由于数组长度是2的n次幂,当数组长度不大的时候,与运算基本都是低位参与,使用两次扰动可以让高位和低位都参与运算,从而得到更加均匀分布的数组下标,也就是减少了哈希碰撞
(6)Hashmap如何解决哈希冲突的?
1、使用两次扰动,以及数组长度为2的n次方幂,使得数据分布更加均匀
2、使用数组+链表+红黑树的结构存储具有相同数组下标值的元素
3、使用红黑树来增加查找速度O(logn)
(7)HashMap为什么不直接使用hashCode()处理后的哈希值直接作为table的下标?
hashCode()方法返回的是int整数类型,其范围为-(2 ^ 31)~(2 ^ 31 - 1),约有40亿个映射空间,而HashMap的容量范围是在16(初始化默认值)~2 ^ 30,HashMap通常情况下是取不到最大值的,并且设备上也难以提供这么多的存储空间,从而导致通过hashCode()计算出的哈希值可能不在数组大小范围内,进而无法匹配存储位置;
使用hash与数组长度取余操作使得元素均匀分布在数组长度范围内的链表/红黑树内
(8)为什么HashMap中String、Integer这样的包装类适合作为K?
String、Integer等包装类的特性能够保证Hash值的不可更改性和计算准确性,能够有效的减少Hash碰撞的几率
都是final类型,即不可变性,保证key的不可更改性,不会存在获取hash值不同的情况 内部已重写了equals()、hashCode()等方法,遵守了HashMap内部的规范(不清楚可以去上面看看putValue的过程),不容易出现Hash值计算错误的情况;
(8)一般用什么类作为HashMap的key?
HashMap一般采用String、Integer等类作为key、因为这些类底层已经重写了hashcode、equals方法,用的是final修饰类在多线程情况下相对安全。
(9)如果使用Object作为HashMap的Key,应该怎么办呢?
答:重写hashCode()和equals()方法
重写hashCode()是因为需要计算存储数据的存储位置,需要注意不要试图从散列码计算中排除掉一个对象的关键部分来提高性能,这样虽然能更快但可能会导致更多的Hash碰撞; 重写equals()方法,需要遵守自反性、对称性、传递性、一致性以及对于任何非null的引用值x,x.equals(null)必须返回false的这几个特性,目的是为了保证key在哈希表中的唯一性; 不可以只写equals方法但是不写hashcode方法
因为:
如果只重写equals方法不重写hashcode方法,有可能两个对象的内容是一样的时候比如·都为“s”,但是HashSet判断这两个对象的hashcode不同(存的数组下标不一致)就会导致直接判定这两个对象不同,这与我们的初衷是不一样的。
(10)特点
无序,键值只能有一条为null
(11)HashMap默认加载因子为什么选择0.75?
Hashtable 初始容量是11 ,扩容 方式为2N+1;
HashMap 初始容量是16,扩容方式为2N;
阿里的人突然问我为啥扩容因子是0.75,回来总结了一下; 提高空间利用率和 减少查询成本的折中,主要是泊松分布,0.75的话碰撞最小,
HashMap有两个参数影响其性能:初始容量和加载因子。容量是哈希表中桶的数量,初始容量只是哈希表在创建时的容量。加载因子是哈希表在其容量自动扩容之前可以达到多满的一种度量。当哈希表中的条目数超出了加载因子与当前容量的乘积时,则要对该哈希表进行扩容、rehash操作(即重建内部数据结构),扩容后的哈希表将具有两倍的原容量。
通常,加载因子需要在时间和空间成本上寻求一种折衷。
加载因子过高,例如为1,虽然减少了空间开销,提高了空间利用率,但同时也增加了查询时间成本;
加载因子过低,例如0.5,虽然可以减少查询时间成本,但是空间利用率很低,同时提高了rehash操作的次数。
选择0.75作为默认的加载因子,完全是时间和空间成本上寻求的一种折衷选择,
加载因子是表示Hash表中元素的填满的程度。 加载因子越大,填满的元素越多,空间利用率越高,但冲突的机会加大了。冲突机会高代表着链表/红黑树会变得复杂,从而查找慢 反之,加载因子越小,填满的元素越少,冲突的机会减小,但空间浪费多了
(12)HashMap为什么是不安全的?
因为他的方法是非原子性的,比如put方法,假设两个线程AB,A和B都添加同一个对象,对于A线程在判断完该集合中是否有相同key之后发生上下文切换,B线程添加此对象,再次上下文切换由于A线程已经判定该集合中没有相同的Key,于是A便会添加此Key这与B之前添加的Key是相同的,违背了HashMap的数据存储原则。
(13)解决哈希冲突的方法有哪些,HashMap采取的哪一种?
Hash算法解决冲突的方法一般有以下几种常用的解决方法 1, 开放定址法: 所谓的开放定址法就是一旦发生了冲突,就去寻找下一个空的散列地址,只要散列表足够大,空的散列地址总能找到,并将记录存入 公式为:fi(key) = (f(key)+di) MOD m (di=1,2,3,……,m-1) ※ 用开放定址法解决冲突的做法是:当冲突发生时,使用某种探测技术在散列表中形成一个探测序列。沿此序列逐个单元地查找,直到找到给定的关键字,或者 碰到一个开放的地址(即该地址单元为空)为止(若要插入,在探查到开放的地址,则可将待插入的新结点存人该地址单元)。查找时探测到开放的地址则表明表 中无待查的关键字,即查找失败。 比如说,我们的关键字集合为{12,67,56,16,25,37,22,29,15,47,48,34},表长为12。 我们用散列函数f(key) = key mod l2 当计算前S个数{12,67,56,16,25}时,都是没有冲突的散列地址,直接存入:
计算key = 37时,发现f(37) = 1,此时就与25所在的位置冲突。 于是我们应用上面的公式f(37) = (f(37)+1) mod 12 = 2。于是将37存入下标为2的位置:
2, 再哈希法: 再哈希法又叫双哈希法,有多个不同的Hash函数,当发生冲突时,使用第二个,第三个,….,等哈希函数 计算地址,直到无冲突。虽然不易发生聚集,但是增加了计算时间。
3, 链地址法: 链地址法的基本思想是:每个哈希表节点都有一个next指针,多个哈希表节点可以用next指针构成一个单向链表,被分配到同一个索引上的多个节点可以用这个单向 链表连接起来,如: 键值对k2, v2与键值对k1, v1通过计算后的索引值都为2,这时及产生冲突,但是可以通道next指针将k2, k1所在的节点连接起来,这样就解决了哈希的冲突问题 4, 建立公共溢出区: 这种方法的基本思想是:将哈希表分为基本表和溢出表两部分,凡是和基本表发生冲突的元素,一律填入溢出表
(14)可以用链表代替数组吗?
我用LinkedList代替数组结构可以么?
这里我稍微说明一下,此题的意思是,源码中是这样的
Entry[] table = new Entry[capacity];
ps:Entry就是一个链表节点。
那我用下面这样表示
List table = new LinkedList();
是否可行?
答案很明显,必须是可以的。
5.既然是可以的,为什么HashMap不用LinkedList,而选用数组?
因为用数组效率最高!
在HashMap中,定位桶的位置是利用元素的key的哈希值对数组长度取模得到。此时,我们已得到桶的位置。显然数组的查找效率比LinkedList大。
那ArrayList,底层也是数组,查找也快啊,为啥不用ArrayList?
(烟哥写到这里的时候,不禁觉得自己真有想法,自己把自己问死了,还好我灵机一动想出了答案)
因为采用基本数组结构,扩容机制可以自己定义,HashMap中数组扩容刚好是2的次幂,在做取模运算的效率高。
而ArrayList的扩容机制是1.5倍扩容
(15)HashSet和HashMap的区别
(1)HashSet实现了Set接口, 仅存储对象; HashMap实现了 Map接口, 存储的是键值对.
(2)HashSet底层其实是用HashMap实现存储的, HashSet封装了一系列HashMap的方法. 依靠HashMap来存储元素值,(利用hashMap的key键进行存储), 而value值默认为Object对象. 所以HashSet也不允许出现重复值, 判断标准和HashMap判断标准相同, 两个元素的hashCode相等并且通过equals()方法返回true.
静态常量PRESENT
(16)LinkedHashMap的底层原理
直接继承于HashMap,我们可以先了解到LinkedHashMap是通过哈希表和链表实现的,它通过维护一个链表来保证对哈希表迭代时的有序性,而这个有序是指键值对插入的顺序。另外,当向哈希表中重复插入某个键的时候,不会影响到原来的有序性。
LinkedHashMap的实现主要分两部分,一部分是哈希表,另外一部分是链表。哈希表部分继承了HashMap,拥有了HashMap那一套高效的操作
head
指向第一个插入的节点,tail
指向最后一个节点。
在LinkedHashMap类使用的仍然是父类HashMap的put方法,所以插入节点对象的流程基本一致。不同的是,LinkedHashMap重写了afterNodeInsertion
和afterNodeAccess
方法。afterNodeInsertion
方法用于移除链表中的最旧的节点对象,也就是链表头部的对象。afterNodeAccess
方法实现的逻辑,是把入参的节点放置在链表的尾部。
(17)) 如何决定使用 HashMap 还是 TreeMap?
对于在Map中插入、删除和定位元素这类操作,HashMap是最好的选择。然而,假如你需要对一个有序的key集合进行遍历,TreeMap是更好的选择。基于你的collection的大小,也许向HashMap中添加元素会更快,将map换为TreeMap进行有序key的遍历。
(18) HashMap在JDK1.7和JDK1.8中有哪些不同?
1.7:数组+链表
1.8:数组+链表+红黑树
扩容方式不同:1.7重新计算hash值,1.8原位置或是原位置+扩容数组长度
计算hash值:1.7是9次扰动,四次位运算+5次异或运算
1.8两次扰动,一次位运算+一次异或运算
25、Treemap,treeSet
1、都是有序集合 2、TreeMap是TreeSet的底层结构 3、运行速度都比hash慢
区别: 1、TreeSet只存储一个对象,而TreeMap存储两个对象Key和Value(仅仅key对象有序) 3、TreeMap的底层采用红黑树的实现,完成数据有序的插入,排序。
26、Hashtable
(1)底层原理
Hashtable采用"拉链法"实现哈希表,底层是数组+链表,所有的方法都做了synchronized处理,因此是同步的,所以效率很低,其并发度是1,key和value都不能是null,原因看27(4)
(2)为什么ConcurrentHashMap不能完全替代HashTable
因为ConcurrentHashMap是弱一致性,其get方法没有上锁,会导致get元素的并不是当前并行还未执行完的put的值,读取到的数据并不一定是最终的值,在一些要求强一致性的场景下可能会出错。例如:需要判断当前值是否为A如果不为A则修改为C,但是当前值为B而有个put方法将其更新为A还没执行完,则最终改值就是A,可能会造成后续程序或业务的异常。
作者:GeorgeDon 链接:ConcurrentHashMap与HashTable对比 - 简书 来源:简书 著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
27、ConcurrentHashMap
(1)ConcurrentHashMap的put方法流程?
jdk1.7的时候:
DK1.7中引入Segment,Segment类通过继承ReentrantLock类,进行加锁,从而控制整个插入过程。 Segment数组也是一种数组加链表的结构方式,每个segement[i]都有一把锁,当某对<key,value>想要进行插入操作,首先要找对应segment数组对应的index,并获取锁,才能对HashEntry进行操作。 在JDK1.7中,ConcurrentHashMap采用Segment + HashEntry的方式进行实现,结构如下:
一个 ConcurrentHashMap 里包含一个 Segment 数组。Segment 的结构和HashMap类似,是一种数组和链表结构,一个 Segment 包含一个 HashEntry 数组,每个 HashEntry 是一个链表结构的元素,每个 Segment 守护着一个HashEntry数组里的元素,当对 HashEntry 数组的数据进行修改时,必须首先获得对应的 Segment的锁。
该类包含两个静态内部类 HashEntry 和 Segment ;前者用来封装映射表的键值对,后者用来充当锁的角色; Segment 是一种可重入的锁 ReentrantLock,每个 Segment 守护一个HashEntry 数组里得元素,当对 HashEntry 数组的数据进行修改时,必须首先获得对应的 Segment 锁。
1.8的时候:
在JDK1.8中,放弃了Segment臃肿的设计,取而代之的是采用Node + CAS + Synchronized来保证并发安全进行实现,synchronized只锁定当前链表或红黑二叉树的首节点,这样只要hash不冲突,就不会产生并发,效率又提升N倍。
如果没有初始化就先调用initTable()方法来进行初始化过程
-
然后通过计算hash值来确定放在数组的哪个位置 如果没有hash冲突就直接CAS插入,如果hash冲突的话,则取出这个节点来
-
如果取出来的节点的hash值是MOVED(-1)的话,则表示当前正在对这个数组进行扩容,复制到新的数组,则当前线程也去帮助复制
-
最后一种情况就是,如果这个节点,不为空,也不在扩容,则通过synchronized来加锁,进行添加操作
-
然后判断当前取出的节点位置存放的是链表还是树
-
如果是链表的话,则遍历整个链表,直到取出来的节点的key来个要放的key进行比较,如果key相等,并且key的hash值也相等的话,
-
则说明是同一个key,则覆盖掉value,否则的话则添加到链表的末尾
-
如果是树的话,则调用putTreeVal方法把这个元素添加到树中去
-
最后在添加完成之后,调用addCount()方法统计size,判断在该节点处共有多少个节点(注意是添加前的个数),如果达到8个以上了的话,
-
则调用treeifyBin方法来尝试将处的链表转为树,或者扩容数组
####
1.7的时候
ConcurrentHashMap采用的结构是segment数组+HashEntry, segment继承于rentreelock,每个segment分组拥有者若干个HashEntry结构,其中HashEntry是数组+链表的结构,每次进行put操作时候,必须先通过该元素的hash计算得到对应的分组,获取该分组的锁,才可以进行插入操作
通过把整个Map分为N个Segment,可以提供相同的线程安全,但是效率提升N倍,默认提升16倍。
这样做的目的是提高其并发程度
1.8的时候
如果没有hash冲突就直接CAS插入,如果hash冲突的话,则取出这个节点来,synchroized来进行加锁
(2)ConcurrentHashMap 的并发度是什么 ?
ConcurrentHashMap 的并发度就是 segment 的大小,默认为 16,这意味着最多同时可以有 16 条线程操作
(3)ConcurrentHashMap的get方法是否要加锁为什么?
get操作的流程很简单,也很清晰,可以分为三个步骤来描述
全程无锁。get操作可以无锁是由于Node元素的val和指针next是用volatile修饰的,在多线程环境下线程A修改结点的val或者新增节点的时候是对线程B可见的。
计算hash值,定位到该table索引位置,如果是首节点符合就返回 如果遇到扩容的时候,会调用标志正在扩容节点ForwardingNode的find方法,查找该节点,匹配就返回 以上都不符合的话,就往下遍历节点,匹配就返回,否则最后就返回null
(4)为什么它的key和value都不能null
在并发中,null是一个非常严重的问题,高并发下,尽可能地消除歧义是必要的,你需要知道究竟是没有找到,还是它的值为null;
(5)ConcurrentHashMap在JDK 7和8之间的区别**
-
JDK1.8的实现降低锁的粒度,JDK1.7版本锁的粒度是基于Segment的,包含多个HashEntry,而JDK1.8锁的粒度就是HashEntry(首节点)
-
JDK1.8版本的数据结构变得更加简单,使得操作也更加清晰流畅,因为已经使用synchronized来进行同步,所以不需要分段锁的概念,也就不需要Segment这种数据结构了,由于粒度的降低,实现的复杂度也增加了
-
JDK1.8使用红黑树来优化链表,基于长度很长的链表的遍历是一个很漫长的过程,而红黑树的遍历效率是很快的,代替一定阈值的链表,这样形成一个最佳拍档
(6)1.8为什么使用内置锁synchronized来代替重入锁ReentrantLock:
-
低粒度加锁方式,synchronized并不比ReentrantLock差, 粗粒度加锁中ReentrantLock可能通过Condition来控制各个低粒度的边界,更加的灵活,而在低粒度中,Condition的优势就没有了
-
JVM的开发团队从来都没有放弃synchronized,而且基于JVM的synchronized优化空间更大,使用内嵌的关键字比使用API更加自然
-
在大量的数据操作下,对于JVM的内存压力,基于API的ReentrantLock会开销更多的内存
(7)什么是锁分段技术
锁分段技术:首先将数据分成一段一段的存储,然后给每一段数据配一把锁,当一个线程占用锁访问其中一个段数据的时候,其他段的数据也能被其他线程访问。
ConcurrentHashMap提供了与Hashtable和SynchronizedMap不同的锁机制。Hashtable中采用的锁机制是一次锁住整个hash表,从而在同一时刻只能由一个线程对其进行操作;而ConcurrentHashMap中则是一次锁住一个桶。
ConcurrentHashMap默认将hash表分为16个桶,诸如get、put、remove等常用操作只锁住当前需要用到的桶。这样,原来只能一个线程进入,现在却能同时有16个写线程执行,并发性能的提升是显而易见的。
(8)它的迭代器是强一致性还是弱一致性?
弱一致性的,就是说get方法可能看到的不是最新值,虽然变量是volitail的,但是因为get方法没有加锁,所以如果在某个线程获取锁添加值得过程中(还未执行完),另一个线程调用get方法,那么它获得的值便不是最新的,这是一种换取并发效率的方式,否则要全部加锁,便会导致并发效率低
(9)和Hashtable的区别以及效率
1.7中,它采用分段锁的概念,可以达到16的并发度,而HashTable却是对整个方法上锁,并发度为1
1.8中,如果没有哈希冲突,它采用CAS的方式,降低了锁的粒度,使得并发效率更高
28、集合框架
(1)什么是集合
集合是用于存储数据的容器
Java集合类存放在java.util包中,是一个用来存放对象的容器。
注意:
1.集合只能存放对象。比如你存入一个int型数据66放入集合中,其实它是自动转换成Integer类后存入的,Java中每一种基本数据类型都有对应的引用类型。
2.集合存放的都是对象的引用,而非对象本身。所以我们称集合中的对象就是集合中对象的引用。对象本身还是放在堆内存中。
3.集合可以存放不同类型,不限数量的数据类型。
(2)集合和数组的区别
-
数组是固定长度的;集合可变长度的。
-
数组可以存储基本数据类型,也可以存储引用数据类型;集合只能存储引用数据类型。
-
数组存储的元素必须是同一个数据类型;集合存储的对象可以是不同数据类型。
(3)常见的集合类
Collection接口的子接口包括:Set接口和List接口 Map接口的实现类主要有:HashMap、TreeMap、Hashtable、ConcurrentHashMap以及Properties等 Set接口的实现类主要有:HashSet、TreeSet、LinkedHashSet等 List接口的实现类主要有:ArrayList、LinkedList、Stack以及Vector等
(4)请说明List、Map、Set三个接口存取元素时,各有什么特点?
List:一个有序(元素存入集合的顺序和取出的顺序一致)容器,元素可以重复,可以插入多个null元素,元素都有索引。常用的实现类有 ArrayList、LinkedList 和 Vector。 Set:一个无序(存入和取出顺序有可能不一致)容器,不可以存储重复元素,只允许存入一个null元素,必须保证元素唯一性。Set 接口常用实现类是 HashSet、LinkedHashSet 以及 TreeSet。 Map是一个键值对集合,存储键、值和之间的映射。 Key无序,唯一;value 不要求有序,允许重复。
注意这里的无序是存储和取出的顺序是有可能不一致,因为LinkedHashMap就是有序的
顺序重复性Null元素
并且:Map接口不继承于Collection接口!
(5)集合框架底层数据结构
Collection
List
Arraylist: Object数组 Vector: Object数组 LinkedList: 双向循环链表 Set
HashSet(无序,唯一):基于 HashMap 实现的,底层采用 HashMap 来保存元素 LinkedHashSet: LinkedHashSet 继承与 HashSet,并且其内部是通过 LinkedHashMap 来实现的。有点类似于我们之前说的LinkedHashMap 其内部是基于 Hashmap 实现一样,不过还是有一点点区别的。 TreeSet(有序,唯一): 红黑树(自平衡的排序二叉树。) Map
HashMap: JDK1.8之前HashMap由数组+链表组成的,数组是HashMap的主体,链表则是主要为了解决哈希冲突而存在的(“拉链法”解决冲突).JDK1.8以后在解决哈希冲突时有了较大的变化,当链表长度大于阈值(默认为8)时,将链表转化为红黑树,以减少搜索时间 LinkedHashMap:LinkedHashMap 继承自 HashMap,所以它的底层仍然是基于拉链式散列结构即由数组和链表或红黑树组成。另外,LinkedHashMap 在上面结构的基础上,增加了一条双向链表,使得上面的结构可以保持键值对的插入顺序。同时通过对链表进行相应的操作,实现了访问顺序相关逻辑。 HashTable: 数组+链表组成的,数组是 HashMap 的主体,链表则是主要为了解决哈希冲突而存在的 TreeMap: 红黑树(自平衡的排序二叉树)
(6)java集合的fail-fast机制?(重要)
是java集合的一种错误检测机制,当多个线程对集合进行结构上的改变的操作时,有可能会产生 fail-fast 机制。
例如:假设存在两个线程(线程1、线程2),线程1通过Iterator在遍历集合A中的元素,在某个时候线程2修改了集合A的结构(是结构上面的修改,而不是简单的修改集合元素的内容),那么这个时候程序就会抛出 ConcurrentModificationException 异常,从而产生fail-fast机制。
原因:迭代器在遍历时直接访问集合中的内容,并且在遍历过程中使用一个 modCount 变量。集合在被遍历期间如果内容发生变化,就会改变modCount的值。每当迭代器使用hashNext()/next()遍历下一个元素之前,都会检测modCount变量是否为expectedmodCount值,是的话就返回遍历;否则抛出异常,终止遍历。
解决办法:
在遍历过程中,所有涉及到改变modCount值得地方全部加上synchronized。
使用CopyOnWriteArrayList来替换ArrayList
(7)fail-fast和fail-safe的区别?(重要)
一:快速失败(fail—fast)
多线程环境下:
在用迭代器遍历一个集合对象时,如果遍历过程中对集合对象的内容进行了修改(增加、删除、修改),则会抛出Concurrent Modification Exception。
原理:迭代器在遍历时直接访问集合中的内容,并且在遍历过程中使用一个 modCount 变量。集合在被遍历期间如果内容发生变化,就会改变modCount的值。每当迭代器使用hashNext()/next()遍历下一个元素之前,都会检测modCount变量是否为expectedmodCount值,是的话就返回遍历;否则抛出异常,终止遍历。
注意:这里异常的抛出条件是检测到 modCount!=expectedmodCount 这个条件。如果集合发生变化时修改modCount值刚好又设置为了expectedmodCount值,则异常不会抛出。因此,不能依赖于这个异常是否抛出而进行并发操作的编程,这个异常只建议用于检测并发修改的bug。
场景:java.util包下的集合类都是快速失败的,不能在多线程下发生并发修改(迭代过程中被修改)。
二:安全失败(fail—safe)
采用安全失败机制的集合容器,在遍历时不是直接在集合内容上访问的,而是先复制原有集合内容,在拷贝的集合上进行遍历。
原理:由于迭代时是对原集合的拷贝进行遍历,所以在遍历过程中对原集合所作的修改并不能被迭代器检测到,所以不会触发Concurrent Modification Exception。
缺点:基于拷贝内容的优点是避免了Concurrent Modification Exception,但同样地,迭代器并不能访问到修改后的内容,即:迭代器遍历的是开始遍历那一刻拿到的集合拷贝,在遍历期间原集合发生的修改迭代器是不知道的。
场景:java.util.concurrent包下的容器都是安全失败,可以在多线程下并发使用,并发修改。
(8) 请简单说明一下什么是迭代器?(记忆)
Iterator提供了统一遍历操作集合元素的统一接口, Collection接口实现Iterable接口, 每个集合都通过实现Iterable接口中iterator()方法返回Iterator接口的实例, 然后对集合的元素进行迭代操作. 有一点需要注意的是:在迭代元素的时候不能通过集合的方法删除元素, 否则会抛出ConcurrentModificationException 异常. 但是可以通过Iterator接口中的remove()方法进行删除.
注意:注意 remove()方法会让expectModcount和modcount 相等,所以是不会抛出这个异常。
(9)map接口的遍历方法
map.entrySet();返回一个Set集合,再使用迭代器进行遍历 map.values();返回一个Collection集合 map.keySet();返回一个Set集合
之后再用迭代器或者增强for循环对集合进行遍历
(10)在ArrayLIst和LinkedList尾部加元素,谁的效率高?
ArrayList采用数组数组实现的,查找效率比LinkedList高。LinkedList采用双向链表实现的,插入和删除的效率比ArrayList要高。一直在list的尾部添加元素,LinkedList效率要高。
当输入的数据一直是小于千万级别的时候,大部分是LinkedList效率高,而当数据量大于千万级别的时候,就会出现ArrayList的效率比较高了。
原来 LinkedList每次增加的时候,会new 一个Node对象来存新增加的元素,所以当数据量小的时候,这个时间并不明显,而ArrayList需要扩容,所以LinkedList的效率就会比较高,其中如果ArrayList出现不需要扩容的时候,那么ArrayList的效率应该是比LinkedList高的,当数据量很大的时候,new对象的时间大于扩容的时间,那么就会出现ArrayList的效率比LinkedList高了
(11)请你说明一下TreeMap的底层实现?
TreeMap 的实现就是红黑树数据结构,也就说是一棵自平衡的排序二叉树,这样就可以保证当需要快速检索指定节点。
红黑树的插入、删除、遍历时间复杂度都为O(lgN),所以性能上低于哈希表。但是哈希表无法提供键值对的有序输出,红黑树因为是排序插入的,可以按照键的值的大小有序输出。红黑树性质:
性质1:每个节点要么是红色,要么是黑色。
性质2:根节点永远是黑色的。
性质3:所有的叶节点都是空节点(即 null),并且是黑色的。
性质4:每个红色节点的两个子节点都是黑色。(从每个叶子到根的路径上不会有两个连续的红色节点)
性质5:从任一节点到其子树中每个叶子节点的路径都包含相同数量的黑色节点。