Java常见的集合类
Java提供常见的集合有那些?
单列集合:Collection,有两个接口分别为List和Set
- List的实现类有:ArrayList、LinkList
- set的实现类有:HashSet、TreeSet
双列集合:Map。其实现类有
- 线程不安全的:HashMap、TreeMap
- 线程安全的:ConcurrentHashMap
List
ArrayList底层是如何实现的?
ArrayList底层是通过动态数组来实现的。
- 在添加元素时首先确保当前数组已使用长度(size)+1能存储下一个元素,如果+1后大于数组长度通过grow方法进行扩容(扩大1.5倍),否则将新元素添加到size的位置
ArrayList list=new ArrayList(10)中的list扩容几次?
- 在ArrayList源码当中提供了一个带参构造方法,这个参数就是集合的初始长度,这里就是指定了初始长度为10,没有发生扩容
数组和List之间的转换?
数组转List:
- 可以使用Arrays中的asList方法进行转换
- 这种方法得到的List是一个固定长度的List,和数组共享相同的内存地址,因此修改原数组内容List的内容也会发生改变
List转数组:
- 可以使用List的toArray方法
- 底层是进行内容拷贝,得到一个新的数组对象。对该数组修改内容不会影响List
ArrayList和LinkedList的区别是什么?
- 底层数据结构:ArrayList是基于动态数组实现的;LinkList是基于双向链表实现的
- 随机访问:ArrayList支持通过索引快速随机访问,而LinkList不支持
- 插入和删除操作:如果给定一个节点进行插入删除时,ArrayList需要移动该节点的后续节点,但是LinkList只需要修改指针即可。但是若不给定节点,它们俩插入删除效率都为o(n)
- 内存占有:LinkedList 是双向链表需要存储数据和两个指针,而ArrayList只需要存储数据即可
ArrayList 和 LinkedList 不是线程安全的,在项目中是如何解决这个的线程安全问题的?
- 使用集合时,优先在方法内使用,定义局部变量,这样就不会有线程安全问题了
- 如果非要在成员变量中使用,可以通过collection的synchronizedList方法将ArrayList转换为线程安全的,将LinkList转换为ConcurrentLinkedqueue来使用
HashMap
说下HashMap的实现原理?
- 底层是通过hash表结构。即数组+(链表|红黑树)。数组每个元素称为桶,每个桶对应一个链表或者红黑树
- 在添加元素时,通过hash函数(hashcode)计算key的hash码进而来确定其在元素的下标。若该下标上存在元素且key相同就进行替换,不同则将健值存入该桶的链表或红黑树中
- 负载因子表示hash表填充程度的参数,默认为0.75,。若hash表填充超过了元素这个比例则会发生扩容(扩两倍)
HashMap的jdk1.7和jdk1.8有什么区别?
- JDK1.8之前采用的拉链法,数组+链表
- JDK1.8之后采用数组+链表+红黑树,链表长度大于8且数组长度大于64则会从链表 转化为红黑树
你能说下HashMap的put方法的具体流程吗?
- 先判断hash表是否为空,如果为空通过resize进行扩容(初始化)
- 通过hash函数计算键的数组位置(称为桶),通过桶为空,直接将元素放入桶中
- 如果桶已经存在节点,则对链表或红黑树进行遍历
- 如果遍历节点的键与要插入的键相同(equals判断),直接进行覆盖
- 如果不同且桶中为红黑树,走红黑树添加逻辑即可。
- 若桶中为链表,在链表尾部插入元素,若插入元素后链表长度是否大于8且桶的数量大于64将链表转换为红黑树
- 插入成功后若存有元素的桶的数量大于桶数量乘以负载因子,则对hashmap进行扩容
你多次介绍了hsahmap的扩容,能讲一讲HashMap的扩 容机制吗?
- 初始时通过resize方法对散列表进行初始化,其初始化长度为16,后续当达到扩容阈值时对散列表进行扩容
- 扩容时,首先会创建一个数容量为之前数组两倍的数组。然后将原数组的元素迁移到新数组当中
- 对于没有冲突的桶,直接使用e.hash&(newCap-1)计算其在新数组中位置
- 如果是红黑树,遍历红黑树,走红黑树的添加
- 如果是链表,遍历链表,判断e.hash&oldCap是否为0。如果为0则在新数组中的位置与原数组相同。若为1在新数组的位置为原数组位置+旧数组长度
你说的通过hash计算后找到数组的下标,是如何找到的 呢,你了解hashMap的寻址算法吗?
通过哈希码来计算键值对应的存储位置(桶的索引)具体步骤如下
- 实现HashMap会调用键的hashCode方法来获取其哈希码
- 然后将右移16位得到的哈希码与原哈希码按位进行异或运算得到最终的hash码
- 将最总的hash码与数组长度取模得到其桶的值(数组下标)。(当然为了提高找数组下标的效率,可以通过e.hash&(n-1))
为何HashMap的数组长度一定是2的次幂?
- 计算索引时效率更高。可以使用位与运算代替取模
- 扩容时计算索引效率更高
我看你对hashmap了解的挺深入的,你知道hashmap在1.7情况下的多线程死循环问题吗?
- jdk1.7采用的数组+链表,当数组在扩容时,因为链表是头插法,在进行数据迁移时,有可能会导致死循环。
- 例子:假如有俩线程,线程1正在读取hashmap,准备扩容时线程2介入。线程而也读取hashmap并直接扩容。由于采用头插法对链表数据进行迁移,索引迁移完成后原顺序AB会变为BA。这时线程1再进行操作时,就会形成BAB的死循环
- jdk1.8中采用尾插法对链表数据进行迁移,使得新数组中链表元素顺序与扩容前一致
hashmap是线程安全的吗?
- 不是线程安全的
- 可以采用ConcurrentHashMap进行使用,它是一个线程安全的 HashMap
HashSet与HashMap的区别?
- hashset内部是通过hashmap来实现的,hashset中的元素被存储为hashmap的键,其值被默认为一个常量object。其存储的为独一无二的元素
- hashmap存储的是键值对,每个键对应一个值,通过键来找到其=值
HashTable与HashMap的区别?
- 线程安全性:HashTable加了synchronized锁是线程安全的,而HashMap不是
- 扩容方式:HashTable扩容是当前容量+1。而HashMap是当前容量翻倍
- 空值:HashTable存储数据时不能为null,而hashmap可以
- 数据结构:HashTable是数组+链表,而hashmap1.8之后是数组+链表+红黑树
Java面试的最后一章。后续会更新微服务、设计模式、场景题的一些总结,敬请期待!