Java集合

23 篇文章 0 订阅
3 篇文章 0 订阅

这里主要对集合框架内部数据结构和Java面试问题进行总结,集合常见方法的调用请看我其他整理的汇总文章。

1. List,Set,Map三者的区别?

List:List接⼝存储⼀组不唯⼀,有序的对象
Set: 不允许重复的集合。不会有多个元素引⽤相同的对象。
Map: 使⽤键值对存储。Map会维护与Key有关联的值。两个Key可以引⽤相同的对象,但Key不能重复。

2. ArrayList Vector LinkedList的底层原理,Arraylist 与 Linked List 区别?

底层原理:
Arraylist: Object数组
Vector: Object数组
LinkedList: 双向链表(JDK1.6之前为循环链表,JDK1.7取消了循环)

ArrayList和LinkedList的区别(考察4次)
相同点:ArrayList和LinkedList都是非线程安全的,都是实现了List接口的容器类,用于存储一系列的对象引用。他们都可以对元素的增删改查进行操作。
区别:

  1. ArrayList是基于Object数组的数据结构,LinkedList是基于 双向链表结构。
  2. 对于随机访问的get方法,ArrayList要优于LinkedList,因为LinkedList要移动指针。
  3. 对于新增和删除操作add和remove,LinkedList比较占优势,因为ArrayList要移动数据。
    (快速随机访问:LinkedList 不⽀持⾼效的随机元素访问,而ArrayList 实现了 RandomAccess 接⼝,具有快速随机访问功能。这点在提及RandomAccess 接⼝的时候可以进行补充,平常可以不提。)

ArrayList 与 Vector 的区别?为什么要⽤Arraylist取代Vector?
Vector 是线程安全的。
Arraylist 是非线程安全的,所以在不需要保证线程安全时(单线程)建议使⽤Arraylist,运行更快。

3. ArrayList 的扩容机制(考察2次)

ArrayList 默认创建一个长度为10的数组,之后随着元素的增加,以1.5倍原数组的长度创建一个新数组,将原来的元素拷贝到新数组之中,如果数组长度达到上限,则会以MAX_ARRAY_SIZE 或者 Integer.MAX_VALUE作为最大长度,而多余的元素就会被舍弃掉。此外,在开始时也可以使用的ArrayList的有参构造函数,指定初始数组的长度。

4. HashMap底层原理(考察4次)

JDK1.7时,HashMap底层是数组+链表,也就是链表散列;其中table数组,每个元素存放一条链表,链表的每一个节点都是<Key,Value>型的Node节点。
JDK1.8之后,hashMap底层是数组+链表+红黑树,当链表长度超过阈值,默认为8时,为加快检索速度,将链表转化成红黑树。当红黑树上的节点数量小于6个,会重新把红黑树变成链表。

5. HashMap的put()和get()的实现

map.put(k,v)实现原理(以JDK1.8为例)(考察3次)

  1. 判断Table数组是否为空或者长度为0,若是则进行初始化,即第一次扩容,第一次扩容的容量默认值为16;
  2. 若不为空,计算 key的 hash 值,通过(n - 1) & hash计算应当存放在数组的下标 index;
  3. 查看 table[index] 是否存在数据,没有数据就构造一个Node节点存放在table[index] 中;
  4. 存在数据,说明发生了hash冲突(存在二个节点key的hash值一样), 则使用equals方法判断key是否相等,相等,用新的value替换原数据;
  5. 如果不相等,判断当前节点类型是不是树型节点,如果是树型节点,创造树型节点插入红黑树中;
  6. 如果不是树型节点,创建Node节点添加到链表的末尾;之后判断链表长度是否大于 8并且数组长度大于64, 大于的话链表转换为红黑树;
  7. 插入完成之后判断当前节点数是否大于阈值,如果大于开始扩容为原数组的二倍。

map.get(k)实现原理(以JDK1.7为例,不涉及红黑树):
第一步:先计算 key的 hash 值,并通过(n - 1) & hash计算数组的下标 index;。
第二步:通过数组下标快速定位到某个位置上。如果这个位置上什么都没有,则返回null。如果这个位置上有单向链表,那么它就会拿着参数K和单向链表上的每一个节点的K进行equals,如果所有equals方法都返回false,则get方法返回null。如果其中一个节点的K和参数K进行equals返回true,那么此时该节点的value就是我们要找的value了,get方法最终返回这个要找的value。

6. HashMap怎么设定初始容量大小(考察一次)

如果new HashMap() 不传入参数,hashmap的默认初始化容量为16,默认加载因子为0.75,也就是说当hashMap集合底层数组的容量达到75%时,数组就开始扩容,如果自己传入初始大小k,初始化大小为 大于k的 2的整数次方,例如如果传10,大小为16。

7. HashMap的哈希函数怎么设计的?为什么要这样设计?

hash函数是先拿到 key 的hashcode,是一个32位的int值,然后让hashcode的高16位和低16位进行异或操作。
在这里插入图片描述
为什么这么设计?
hash函数也叫扰动函数,这么设计有二点原因:

  1. 一定要尽可能降低hash碰撞,越分散越好;右移16位,正好是32位的一半,高半区和低半区做异或,就是为了混合原始哈希码的高位和低位,以此来加大低位的随机性。而且混合后的低位掺杂了高位的部分特征,这样高位的信息也被变相保留下来。
  2. 算法一定要尽可能高效,因为这是高频操作, 因此采用位运算;

8. HashMap的长度为什么是2的幂次⽅?

为了能让 HashMap 存取⾼效,尽量较少碰撞,也就是要尽量把数据分配均匀。只要哈希函数映射得⽐较均匀松散,⼀般应⽤是很难出现碰撞的。但hash函数返回数据范围较大,内存是放不下的,不能直接当做数组下标。

我们⾸先可能会想到采⽤%取余的操作来实现。但是,取余(%)操作中如果被除数是2的幂次则等价于与其被除数减⼀的与(&)操作(也就是说 hash%length==hash&(length-1)的前提是 length 是2的n 次⽅)。并且 采⽤⼆进制位操作 &,相对于%能够提⾼运算效率,这就解释了 HashMap 的⻓度为什么是2的幂次⽅。

bucketIndex = indexFor(hash, table.length);
static int indexFor(int h, int length) {
     return h & (length-1);
}

9. HashMap的扩容机制(考察1次)

当hashmap中的元素越来越多的时候,因为数组的长度是固定的,发生碰撞的几率也就越来越高,所以为了提高查询的效率,就要对hashmap的数组进行扩容,原数组中的数据必须重新计算其在新数组中的位置,并放进去,这就是resize。 扩容的时候1.7需要对原数组中的元素进行重新hash定位在新数组的位置,1.8采用更简单的判断逻辑,位置不变或索引+旧容量大小。

当hashmap中的元素个数超过数组大小loadFactor时,就会进行数组扩容,loadFactor的默认值为0.75,也就是说,默认情况下,数组大小为16,那么当hashmap中元素个数超过160.75=12的时候,就把数组的大小扩展为216=32,即扩大一倍,然后重新计算每个元素在数组中的位置。

扩容的时候为什么JDK1.8 不用重新hash就可以直接定位原节点在新数据的位置呢?
因为扩容是扩大为原数组大小的2倍,用于计算数组位置的掩码仅仅只是高位多了一个1。因为是& 运算,1和任何数 & 都是它本身,那就分二种情况,key的hash值高位为0和高位为1的情况;高位为0,则重新hash数值不变,最高位为1,重新hash数值比原来增加一个旧数组的容量。

10. JDK1.8对HashMap的优化

  1. 数组+链表改成了数组+链表或红黑树;使得发生哈希冲突时查询的时间复杂度由O(n)降为O(logn);
  2. 链表的插入方式从头插法改成了尾插法,简单说就是插入时,如果数组位置上已经有元素,1.7将新元素放到数组中,原始节点作为新节点的后继节点,1.8遍历链表,将元素放置到链表的最后;之所以做出改进是因为JDK1.7头插法扩容时,头插法会使链表发生反转,多线程环境下会产生环;
  3. 扩容的时候1.7需要对原数组中的元素进行重新hash定位在新数组的位置,1.8采用更简单的判断逻辑,位置不变或索引+旧容量大小;
  4. 在插入时,1.7先判断是否需要扩容,再插入,1.8先进行插入,插入完成再判断是否需要扩容;

11. HashMap是否线程安全,怎么才能线程安全?(考察2次)

HashMap是非线程安全的,在多线程环境下,1.7 会产生死循环、数据丢失、数据覆盖的问题,其死循环的原因是因为其采用头插法,并发下的Rehash 会造成元素之间会形成⼀个循环链表。JDK 1.8 改为尾插法后解决了这个问题,但是还是不建议在多线程下使⽤ HashMap。因为多线程下还是会存在其他问题⽐如数据覆盖和多线程同时扩容等问题。并发环境下推荐使⽤ ConcurrentHashMap 。

怎么才能让HashMap变成线程安全的呢?
Java中有HashTable、Collections.synchronizedMap、以及ConcurrentHashMap可以实现线程安全的Map。

  1. HashTable是直接在操作方法上加synchronized关键字,锁住整个数组,粒度比较大。
  2. Collections.synchronizedMap是使用Collections集合工具的内部类,通过传入Map封装出一个SynchronizedMap对象,内部定义了一个对象锁,方法内通过对象锁实现;
  3. 使用ConcurrentHashMap,锁粒度较小,并发度大大提高。

12. HashMap内部节点是否有序?是否存在有序的Map?

HashMap内部节点是无序的,根据hash值随机插入。

LinkedHashMap 和 TreeMap是有序的Map。

LinkedHashMap内部维护了一个单链表,有头尾节点,同时LinkedHashMap节点Entry内部除了继承HashMap的Node属性,还有before 和 after用于标识前置节点和后置节点。可以实现按插入的顺序或访问顺序排序。

TreeMap是按照Key的自然顺序或者Comprator的顺序进行排序,内部是通过红黑树来实现。所以要么key所属的类实现Comparable接口,或者自定义一个实现了Comparator接口的比较器,传给TreeMap用于key的比较。

13. 解决哈希冲突的方法(考察2次)

  1. 开放定址法,其基本思想是:当关键字key的哈希地址p=H(key)出现冲突时,以p为基础,产生另一个哈希地址p1,如果p1仍然冲突,再以p1为基础,产生另一个哈希地址p2,…,直到找出一个不冲突的哈希地址pi ,将相应元素存入其中。
  2. 拉链法:也是HashMap中采用的方法,将所有哈希地址相同的记录都链接在同一链表中。
  3. 再哈希法:这种方法是同时构造多个不同的哈希函数:
    当key的哈希地址Hi=RH1(key)发生冲突时,再利用一个新的哈希函数计算出该key一个新的哈希地址Hi=RH2(key)……,直到冲突不再产生。这种方法不易产生聚集,但增加了计算时间。
  4. 建立公共溢出区:将哈希表分为基本表和溢出表两部分,凡是和基本表发生冲突的元素,一律填入溢出表。

14. HashMap 和 Hashtable 的区别

两者都实现了Map接口;
不同点:

  1. 线程安全性: HashMap 是⾮线程安全的(如果要保证线程安全的话可以使⽤ ConcurrentHashMap),HashTable 是线程安全的,HashTable 内部的⽅法基本都经过 synchronized 修饰。
  2. 对Null key 和Null value的⽀持:
    HashMap中,null可以为key也可以为value;Hashtable中,key和value都不允许出现null值。
  3. 初始容量和扩充容量:
    ①创建时如果不指定容量初始值,Hashtable 默认的初始⼤⼩为11,之后每次扩充,容量变为原来的2n+1。HashMap 默认的初始化⼤⼩为16。之后每次扩充,容量变为原来的2倍。
    ②创建时如果给定了容量初始值,那么 Hashtable 会直接使⽤你给定的⼤⼩,⽽ HashMap 会将其扩充为2的幂次⽅⼤⼩,也就是说 HashMap 总是使⽤2的幂作为哈希表的⼤⼩。
  4. 底层数据结构:
    JDK1.8之前HashMap由数组+链表组成的,数组是HashMap的主体,链表则是主要为了解决哈希冲突⽽存在的(“拉链法”解决冲突)。JDK1.8以后在解决哈希冲突时有了较⼤的变化,当链表⻓度⼤于阈值(默认为8)时,将链表转化为红⿊树,以减少搜索时间。Hashtable 没有这样的机制, 底层数据结构是数组+链表组成的,数组是主体,链表则是主要为了解决哈希冲突⽽存在的。

15. ConcurrentHashMap 底层原理、和 Hashtable 的区别

ConcurrentHashMap 和 Hashtable 的区别主要体现在实现线程安全的⽅式上不同。

1. 底层数据结构:

JDK1.7的 ConcurrentHashMap 底层采⽤ 分段的数组 +链表 实现,JDK1.8 采⽤的数据结构跟HashMap1.8的结构⼀样,数组+链表/红⿊⼆叉树。(JDK1.8在链表⻓度超过⼀定阈值(8)时将链表转换为红⿊树)。
Hashtable 一直采⽤ 数组+链表 的形式。

2. 实现线程安全的⽅式:

对于ConcurrentHashMap:
① 在JDK1.7的时候,ConcurrentHashMap 对整个桶数组进⾏了分割分段,其中的分段锁称为Segment,它类似于HashMap的结构,即内部拥有Entry数组,而数组中的每个元素又是一个链表,同时Segment继承了ReentrantLock,每⼀把锁只锁容器其中⼀部分数据,多线程访问容器⾥不同数据段的数据,就不会存在锁竞争,提⾼了并发访问率。
②JDK1.8 的时候已经摒弃了Segment的概念,⽽是直接⽤ Node 数组+链表+红⿊树的数据结构来实现,ConcurrentHashMap成员变量使用volatile 修饰,免除了指令重排序,同时保证内存可见性,另外使用CAS操作和synchronized结合实现赋值操作。虽然在JDK1.8中还能看到 Segment 的数据结构,但是已经简化了属性,只是为了兼容旧版本;其中,synchronized只锁定当前链表或红⿊⼆叉树的⾸节点,这样只要hash不冲突,就不会产⽣并发。

对于Hashtable: 实现线程安全的方式是在修改数据时使⽤ synchronized 锁住整个HashTable,效率低。

16. HashSet

Hash Set 底层就是基于 Hash Map 实现的,HashSet是作为Map的key而存在的,而value是一个命名为present的static的Object对象,因为是一个类属性,所以只会有一个。

Hash Set如何检查重复
以HashSet为例,HashSet不能添加重复的元素,当调用add方法时候,首先会调用hashCode()方法判该元素的hashCode是否已经存在,如不存在则直接插入元素;如果存在,会接着调用equals()方法来检查该对象是否真的相同。如果两者相同,HashSet就不会让add()操作成功。

HashSet: 无序
LinkedHashSet: 按照插入顺序
TreeSet: 从小到大排序

17. 集合中什么结构使用了红黑树?

TreeMap、TreeSet以及JDK1.8之后的HashMap底层都⽤到了红⿊树。红⿊树就是为了解决⼆叉查找树的缺陷,因为⼆叉查找树在某些情况下会退化成⼀个线性结构。**

18. String、String Buffer 和 String Builder 的区别是什么? String 为什么是不可变的?

可变性(底层原理):
String 类中使⽤ final 关键字修饰字符数组来保存字符串(在 Java 9 之后,使用byte 数组)。所以 String 对象是不可变的。
当创建 String 类型的对象时,虚拟机会在常量池中查找有没有已经存在的值和要创建的值相同的对象,如果有就把它赋给当前引⽤。如果没有就在常量池中重新创建⼀个 String 对象。
String Builder 与 String Buffer 都继承⾃ Abstract String Builder 类,在Abstract String Builder 中也是使⽤字符数组保存字符串,但是没有⽤ final 关键字修饰,所以这两种对象都是可变的。

线程安全性:
String 中的对象是不可变的,也就可以理解为常量,线程安全。
String Buffer 对⽅法加了同步锁或者对调⽤的⽅法加了同步锁,所以是线程安全的。
String Builder 并没有对⽅法进⾏加同步锁,所以是⾮线程安全的。

使用:
操作少量的数据: 使用String。
当进行大量字符串拼接操作的时候,如果是单线程就用StringBuilder,会更快些,如果是多线程,就需要用StringBuffer保证数据的安全性。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值