Java容器面试

Java容器在这里插入图片描述

Collection

List

Java 的 List 是非常常用的数据类型。List 是有序的 Collection。Java List 一共三个实现类: 分别是 ArrayList、Vector 和 LinkedList。

  • ArrayList:ArrayList 是最常用的 List 实现类,内部是通过数组实现的,它允许对元素进行快速随机访问。数组的缺点是每个元素之间不能有间隔,当数组大小不满足时需要增加存储能力,就要将已经有数 组的数据复制到新的存储空间中。当从ArrayList 的中间位置插入或者删除元素时,需要对数组进 行复制、移动、代价比较高。因此,它适合随机查找和遍历,不适合插入和删除。
  • Vector:Vector 与 ArrayList 一样,也是通过数组实现的,不同的是它支持线程的同步,即某一时刻只有一 个线程能够写Vector,避免多线程同时写而引起的不一致性,但实现同步需要很高的花费,因此, 访问它比访问 ArrayList 慢。
  • LinkedList:LinkedList 是用链表结构存储数据的,很适合数据的动态插入和删除,随机访问和遍历速度比较慢。另外,他还提供了List 接口中没有定义的方法,专门用于操作表头和表尾元素,可以当作堆 栈、队列和双向队列使用。

Set

Set 注重独一无二的性质,该体系集合用于存储无序(存入和取出的顺序不一定相同)元素,值不能重 复。对象的相等性本质是对象 hashCode 值(java 是依据对象的内存地址计算出的此序号)判断 的,如果想要让两个不同的对象视为相等的,就必须覆盖 Object 的 hashCode 方法和 equals 方 法。

  • HashSet:基于哈希表实现,存入数据是按照哈希值,所以并不是按照存入的顺序排序,为保证存入的唯一性,存入元素哈希值相同时,会使用equals 方法比较,如果比较出不同就放入同一个哈希桶里。
  • TreeSet:基于红黑树实现,支持有序性操作,每增加一个对象都会进行排序,将对象插入的二叉树指定的位置。
  • LinkedHashSet( HashSet+LinkedHashMap ):继承于 HashSet、又是基于 LinkedHashMap来实现的, 具有 HashSet 的查找效率 。

Queue

Map

  • HashMap: 根据键的 hashCode 值存储数据,大多数情况下可以直接定位到它的值,因而具有很快的访问速度,但遍历顺序却是不确定的。 HashMap 非线程安全,底层实现为数组+链表+红黑树。
  • ConcurrentHashMap:支持并发操作的HashMap,在JDK1.7和1.8实现线程安全的方式不同。
  • HashTable:Hashtable 是遗留类,不建议使用,很多映射的常用功能与 HashMap 类似,通过 synchronized把整个(table)表锁住来实现线程安全的,效率十分低下。
  • TreeMap: 红黑树实现,可排序,需要对一个有序的key集合进行遍历时建议使用。
  • LinkedHashMap: 是 HashMap 的一个子类, 增加了一条双向链表, 从而可以保存记录的插入顺序,在用 Iterator 遍历LinkedHashMap 时,先得到的记录肯定是先插入的,也可以在构造时带参数,按照访问次序排序。

为什么要设计出迭代器?

迭代器本质是一种设计模式,为了解决为不同的集合类提供统一的遍历操作接口。

List、Set、Map 之间的区别?

  • List
    1.可以允许重复的对象。 2.可以插入多个null元素。 3.是一个有序容器,保持了每个元素的插入顺序,输出的顺序就是插入的顺序。 4.常用的实现类有 ArrayList、LinkedList 和 Vector。
  • Set
    1.不允许重复对象 2.无序容器,无法保证每个元素的存储顺序,TreeSet通过 Comparator 或者 Comparable 维护了一个排序顺序。 3.只允许一个 null 元素 4.Set 接口最流行的几个实现类是 HashSet、LinkedHashSet以及 TreeSet。
  • Map
    1.Map不是collection的子接口或者实现类。Map是一个接口。 2.Map 的 每个 Entry 都持有两个对象,也就是一个键一个值(键值对),Map 可能会持有相同的值对象但键对象必须是唯一的。 3.TreeMap 也通过Comparator 或者 Comparable 维护了一个排序顺序。 4.Map 里你可以拥有随意个 null 值但最多只能有一个null 键。 5.Map 接口最流行的几个实现类是 HashMap、LinkedHashMap、Hashtable 和TreeMap。(HashMap、TreeMap最常用)

Arraylist 与 LinkedList 的区别

1. 底层数据结构: Arraylist 底层使用的是 动态数组 ;LinkedList 底层使用的是 双向链表 数据结构
2.插入删除数据方式: ArrayList插入删除元素时分两种情况:①把元素添加到末尾所需的时间复杂度是O(1),②在指定位置添加删除元素时就需要移动整个数组,操作代价就比较大了;LinkedList 对于添加删除方法只需要断链然后更改指向,所需的消耗就很小了。
3.是否支持快速随机访问: ArrayList 支持高效的随机元素访问而LinkedList 不支持。
4. 内存空间占用: ArrayList的空 间浪费主要体现在在list列表的结尾会预留一定的容量空间,而LinkedList的空间花费则体现在它的每一个元素都需要消耗比ArrayList更多的空间(因为要存放直接后继和直接前驱以及数据)。

ArrayList 和 Vector 的区别

  • Vector类的所有方法都是同步的。可以由两个线程安全地访问一个Vector对象、但是一个线程访问Vector的话代码要在同步操作上耗费大量的时间。Vector一次扩容为原来的2倍。
  • Arraylist不是同步的,所以在不需要保证线程安全时建议使用Arraylist,需要时使用 CopyOnWriteArrayList 。Arraylist一次扩容为1.5倍。

ArrayList 的扩容机制?

前提:以无参数构造方法创建 ArrayList 时,实际上初始化赋值的是一个空数组。当真正对数组进行添加元素操作时,才真正分配容量。

  • 当我们要 add 进第1个元素到 ArrayList 时,数组长度为0 (因为还是一个空的 list),因为执行了ensureCapacityInternal() 方法 ,所以更改了minCapacity,此时为10(默认最小容量)。此时,就会执行grow方法把容量扩大为10,然后一直add到第11个元素时,会对旧数组位移运算得到新数组,然后把旧数组整个复制到新数组上,是旧容量的 1.5倍。

如何实现数组和 List 之间的转换?

  • List转换成为数组:调用ArrayList的toArray方法。
  • 数组转换成为List:调用Arrays的asList方法。

Array 和 ArrayList 有何区别?

  • Array可以容纳基本类型和对象,而ArrayList只能容纳对象。
  • Array是指定大小的,而ArrayList大小是固定的。
  • Array没有提供ArrayList那么多功能,比如addAll、removeAll和iterator等。

HashMap 和 Hashtable 的区别

  • 线程是否安全: HashMap 是非线程安全的,HashTable 是线程安全的;HashTable内部的方法基本都经过synchronized 修饰。(如果保证线程安全的话使用 ConcurrentHashMap );
  • 效率: 因为线程安全的问题,HashMap 要比 HashTable 效率高一点。另外,HashTable基本被淘汰,不要在代码中使用它;
  • 对Null key 和Null value的支持: HashMap 中,null可以作为键,这样的键只有一个,可以有一个或多个键所对应的值为 null。。但是在 HashTable 中 put 进的键值只要有一个null,直接抛出 NullPointerException。
  • 初始容量大小和每次扩充容量大小的不同 : ①创建时如果不指定容量初始值,Hashtable默认的初始大小为11,之后每次扩充,容量变为原来的2n+1。HashMap默认的初始化大小为16。之后每次扩充,容量变为原来的2倍。②创建时如果给定了容量初始值,那么 Hashtable会直接使用你给定的大小,而 HashMap 会将其扩充为2的幂次方大小;
  • 底层数据结构: JDK1.8 以后的 HashMap在解决哈希冲突时有了较大的变化,当链表长度大于阈值(默认为8)时,将链表转化为红黑树,以减少搜索时间。Hashtable 没有这样的机制。

说说 HashMap

1.内部存储结构:数组+链表+红黑树(JDK8)
2.默认容量16(数组长度),默认装载因子0.75。
3.key 和 value 对数据类型的要求都是泛型。
4.key 可以为 null,放在 table[0] 中。
5.hashcode:计算键的hashcode作为存储键信息的数组下标用于查找键对象的存储位置。equals:HashMap 使用 equals() 判断当前的键是否与表中存在的键相同。

HashMap 的结构

在 JDK 1.7 中 HashMap 是以数组加链表的形式组成的,JDK 1.8 之后新增了红黑树的组成结构,当链表大于 8 并且容量大于 64 时,链表结构会转换成红黑树结构,添加红黑树是因为一旦链表过长,会严重影响 HashMap 的性能,而红黑树具有快速增删改查的特点,这样就可以有效的解决链表过长时操作比较慢的问题。

HashMap 的长度为什么是2的幂次方

HashMap为了存取高效,要尽量较少碰撞,就是要尽量把数据分配均匀,每个链表长度大致相同,这个实现就在把数据存到哪个链表中的算法;这个算法实际就是取模,hash%length,计算机中直接求余效率不如位移运算,源码中做了优化hash&(length-1),

什么是 HashMap 的加载因子?加载因子为什么是 0.75?

判断什么时候进行扩容的,假如加载因子是 0.5,HashMap 的初始化容量是 16,那么当 HashMap 中有 16*0.5=8 个元素时,HashMap 就会进行扩容。
这其实是出于容量和性能之间平衡的结果:

  • 当加载因子设置比较大的时候,扩容的门槛就被提高了,扩容发生的频率比较低,占用的空间会比较小,但此时发生Hash冲突的几率就会提升,因此需要更复杂的数据结构来存储元素,这样对元素的操作时间就会增加,运行效率也会因此降低;
  • 而当加载因子值比较小的时候,扩容的门槛会比较低,因此会占用更多的空间,多次扩容也会影响性能。
  • HashMap的容量有一个固定的要求就是一定是2的幂次方。所以,如果负载因子是3/4的话,那么和capacity的乘积结果就可以是一个整数。

所以综合了以上情况就取了一个 0.5 到 1.0 的平均数 0.75 作为加载因子。

HashMap 的扩容机制

扩容时机:当size大于threshold的时候,并不一定会触发扩容机制,只要有一个新建的节点出现哈希冲突,则立刻resize

  • size记录的是map中包含的Entry的数量
  • 而threshold记录的是需要resize的阈值 且 threshold = loadFactor *capacity
  • capacity 其实就是桶的长度

步骤:

  • 数组,阈值都扩大一倍
  • 如果旧数组不为空,开始遍历旧数组
  • 遍历到的数组上只有一个元素,就直接迁移
  • 如果是红黑树就使用 split 方法
  • 如果是链表就把链表拆成两个,按照高位运算的结果放到新数组中并且保留顺序

JDK 1.8 在扩容方面对 HashMap 做了哪些优化?

  • 1.7创建一个容量的新数组,重新计算每个元素在数组中的位置并且进行迁移。
  • 1.8中在扩容HashMap的时候,不需要像1.7中去重新计算元素的hash,只需要看看原来的hash值新增的哪个二进制数是1还是0就好了,如果是0的话表示索引没有变,是1的话表示索引变成“oldCap+原索引”,这样即省去了重新计算hash值的时间,并且扩容后链表元素位置不会倒置。

HashMap 1.7和1.8版本区别

1.数据结构:1.7:数组+链表,1.8:数组+链表+红黑树
2.新节点插入方式:1.7:头插法 ,1.8:直接在尾部插入
3.扰动运算次数:1.7:运算多,1.8:运算少
4.插入和扩容的判断:1.7:先扩容后插入,1.8:先插入后扩容
    为什么?1.8增加了判断是否为红黑树节点,先扩容的话不知道到底扩链表节点还是红黑树。

HashMap1.7为什么不安全?

HashMap在rehash的时候,这个会重新将原数组的内容重新hash到新的扩容数组中,在多线程的环境下,存在同时其他put操作,如果hash值相同,把值插入同一个链表,会因为头插法的特性造成闭环,导致在get时会出现死循环,所以HashMap是线程不安全的。

高并发下HashMap1.7的环是如何产生的

若当前线程一此时获得ertry节点,但是被线程中断无法继续执行,此时线程二进入transfer函数,并把函数顺利执行,此时新表中的某个位置有了节点,之后线程一获得执行权继续执行,在tranfer方法中会把next指向自己造成闭环,然后在get时会出现死循环。

为什么HashMap中String、Integer这样的包装类适合作为Key?

String、Integer等包装类的特性能够保证Hash值的不可更改性和计算准确性,能够有效的减少Hash碰撞的几率

  • 都是final类型,即不可变性,保证key的不可更改性,不会存在获取hash值不同的情况
  • 内部已重写了equals()、hashCode()等方法,遵守了HashMap内部的规范,不容易出现Hash值计算错误的情况。

如果我想要让自己的Object作为K应该怎么办呢?

  • 重写hashCode()和equals()方法 。 重写hashCode()是因为需要计算存储数据的存储位置, 重写equals()方法目的是为了保证key在哈希表中的唯一性;

ConcurrentHashMap线程安全的实现方式/数据结构?

  • 在JDK1.7版本中,ConcurrentHashMap维护了一个Segment数组,Segment这个类继承了重入锁ReentrantLock,在该类里面维护了一个
    HashEntry<K,V>[]table数组,在写操作put,remove,扩容的时候,会对Segment加锁,所以仅仅影响这个Segment,不同的Segment还是可以并发的,所以解决了线程的安全问题,同时又采用了分段锁也提升了并发的效率。在JDK1.8版本中,ConcurrentHashMap摒弃了Segment的概念,而是直接用Node数组+链表+红黑树的数据结构来实现,并发控制使用Synchronized和CAS来操作,整个看起来就像是优化过且线程安全的HashMap。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值