java中的集合概念、继承关系图、各个集合底层所使用的数据结构图、面试问题标准答案

本文围绕Java集合展开,介绍了Collection和Map两大类型及其子类,如List、Set、Queue、HashMap等。阐述了集合涉及的单向链表、双向链表、哈希表、二叉树等数据结构。还针对集合常见面试题,分析了各集合特点、线程安全性、扩容机制及相互区别等内容。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

一、java中的集合概念

1、集合是一个容器,是一个载体,可以一次容纳多个对象。

2、java中的集合主要包含collection和map两大类型,
  collection下有List,set,queue三大接口。List代表有序的可重复的数据列表,set代表无序的不可重复的数据集合,Queque代表的是先进先出的数据队列。map代表的是key-value类型的映射表。常用的实现类包括Arraylist,linkedList,TreeSet、Hashset,Hashmap等。上述的集合都是线程不安全的,针对不同的类型java也提供相应的线程安全的集合例如vector,hashtable,concurrentHashMap等。对于不安全的集合也可以使用Collctions工具类的synchroinzedXxx方法将其包装成线程安全的集合类。

  (2)set分为hashset treeset map分为haspmap treemap hashtable。 hashmap 是动态数组加链表加红黑树 初始长度16 对key取hashcode值与长度取余 余数是几就进入哪 有相同的以链表挂上 链表长度大于 8变成红黑树 小于6变回链表 如果有相同的hash值 后来的会覆盖之前的。

3、ConcurrentHashMap是线程安全的HashMap,ConcurrentHashMap和HashMap以及Hashtable的区别?
  (1)HashMap是线程不安全的,因为HashMap中操作都没有加锁,因此在多线程环境下会导致数据覆盖之类的问题,所以,在多线程中使用HashMap是会抛出异常的。
  (2)HashTable是线程安全的,但是HashTable只是单纯的在put()方法上加上synchronized。保证插入时阻塞其他线程的插入操作。虽然安全,但因为设计简单,所以性能低下。
  (3)ConcurrentHashMap是线程安全的,ConcurrentHashMap并非锁住整个方法,而是通过原子操作和局部加锁的方法保证了多线程的线程安全,且尽可能减少了性能损耗。

二、java中的集合分为两大类、继承关系图

1、单个方式存储元素:
这一类集合中的超级父类接口为:java.util.Collection

继承图如下:
Collection继承图

2、以键值(key–value)对的方式存储元素:
这一类的超级父类为:java.util.Map

继承图如下:
Map继承图

三、集合中所涉及的数据结构示意图

1、单向链表结构示意图

单向链表

2、双向链表结构示意图

双向链表

3、哈希表结构示意图

哈希表

4、二叉树结构示意图

二叉树

四、集合涉及的面试题

1、说说你了解的集合
  集合从大的方向分有两个,一是Collection集合,二是Map集合。 Collection集合下有List、Set、Queue。Map集合下有HashMap、LinkedHashMap、TreeMap、HashTable、ConcurrentHashMap。 -List集合下有ArrayList、LinkedList、Vector、CopyOnWriteArrayList。Set集合下有HashSet、LinkedHashSet、TreeSet、CopyOnWriteArraySet。

2、List集合需要掌握的知识

  (1)、List集合的特点
  List接口下的存储特点都是有序、可重复的、有元素下标的(这个有序是指存进去的顺序和取出来的顺序是一样的,这里的顺序不是按照元素的大小排序的)。

  (2)、说说你对ArrayList的理解和常用的方法
  ArrayList是List集合的一个实现类,其底层实现是数组,数组的查询是直接通过索引查询,所以查询速度比较快,时间复杂度是O(1),增删的话,根据增删的位置,时间复杂度有所不同,如果是中间第i个位置,时间复杂度就是O(n-i),简单理解其时间复杂度是O(n)
  扩容机制:当构造方法中没有指定数组的大小时,其默认大小是10,当集合个数大于容量的时候,就会以1.5倍容量去进行扩容,如果还不够大就在扩容后的1.5倍再扩容。扩容后要对原来的集合进行cope到新的集合中。(在add()方法中调用确保内部容量方法,传入参数当前元素个数加一,当它大于实际数组大小的时候就调用grow()方法进行扩容。扩容是右移一位,进行扩容1.5倍,不够再进行扩容,通过copeof()方法进行数组的复制。)源码:int newCapacity = oldCapacity + (oldCapacity >> 1);。
  线程问题:ArrayList是线程不安全的,在add()方法的时候,首先会检查一下数组的容量是否够用,如果够用,那么就会执行elementData[size++] = e;方法,该语句执行了两大步,第一大步是,将e放到elementData缓冲区,第二大步是,将size的大小进行加1操作,也就是说,这个操作并非原子性操作。当在并发的情况时,就会出现问题。
在这里插入图片描述
  (3)、说说你对LinkedList的理解
  LinkedList也是List的一个实现类,其底层是双向链表,其内部有一个next指针指向下一个节点,一个prev指针,指向上一个节点,由于是链表的数据结构,所以在查询的时候相对就比较慢了,时间复杂度是O(n),因为当我们需要查询某个元素的时候,需要从第一个节点开始遍历,直到查询结束。而他的增删就比较快了,如果增加一个N节点,直接将后一个节点的prev指向N节点,N节点的next指向后一个节点,前一个节点的next指向N节点,N节点的prev指针指向前一个节点即可,时间复杂度为O(1),空间复杂度一般比ArrayList大,因为每个节点都要存储两个指针。
  扩容机制:没有有初始化大小,也没有扩容的机制,就是一直在前面或者后面新增就好。
   - 线程问题:LinkedList也是线程不安全的,其添加元素的操作,通过linkLast方法在尾部进行添加的,添加完之后,并把size的大小加1。其他的不说,单单一个size++就不是原子性了。简单的a加1操作会执行三步:1:把a的值加载到内存、2:将内存中的值,存储到变量中、3:然后进行加1操作。

  (4)、说说你对vectror的理解
   Vector也是List的一个实现类,其底层也是一个数组protected Object[] elementData;,底层ArrayList差不多,也就是因为每个方法都加了synchronized关键字,所以是线程安全的,效率没有ArrayList高,一般不建议使用。当集合中元素的个数大于集合容量的时候,就会触发扩容机制,扩容后的新集合容量是旧集合容量的2倍,

  (5)、如果不使用Vector来解决ArrayList的线程安全问题,还有其他的解决方案吗?
  既然不建议使用Vector,还有一个包java.util.concurrent(JUC)包,它下面有一个类CopyOnWriteArrayList也是线程安全的。CopyOnWriteArrayList也是List的一个实现类。 - add方法用Lock锁来解决并发问题,其中在进行添加数据的时候,用了copyOf方法,也就是复制了一份,然后再set进去。 - CopyOnWriteArrayList底层也是用的数组,但是它的数组是用volatile修饰了,主要是保证了数据的可见性。get操作时,并没有加锁,因为volatile保证了数据的可见性,当数据被修改的时候,读操作能立刻知道。

  (6)ArrayList和LinkedList 的区别是什么
  ArrayList底层的数据结构是数组,支持随机访问,而 LinkedList 的底层数据结构是双向循环链表,不支持随机访问。使用下标访问一个元素,ArrayList 的时间复杂度是 O(1),而 LinkedList 是 O(n)。

  (7)ArrayList和Vector 的区别是什么?
   Vector使用了synchronized来实现线程同步,是线程安全的,而ArrayList是非线程安全的。Vector扩容每次会增加1倍,而ArrayList只会增加0.5倍。

  (8)有了Array(数组),为啥还要有ArrayList集合(底层是数组)
   ●数组高效,但使用前先设定容量,容量固定难以动态扩展。ArrayList容量动态增长,牺牲效率。
   ●Array始终是连续存放的;而ArrayList的存放不一定连续;
   ●数组可以存储基本数据类型、对象数据,但声明object[]的数组除外,存储类型校验,ArrayList存储对象,不校验类型,编译时会效验。
   ●Array不能随意添加、删除;而ArrayList可以在任意位置插入和删除。
   注意:基于效率和类型检验,应尽可能使用Array,无法确定数组大小时才使用ArrayList;Array最高效;但是其容量固定且无法动态改变; ArrayList容量可动态增长;但牺牲效率。

3、谈谈你对queue集合的理解
  queue是一个队列,先进先出(FIFO)的数据结构。自动扩容。

4、set集合需要掌握的知识
  (1)、Set集合的特点
无序、不可重复、没有元素下标(无序是指元素存进去和取出来的顺序是不一样的)。

  (2)、说说List和Set的区别
   List的存储顺序是按照存入的顺序来的,而Set是根据哈希值来的 - List可以存储相同的元素,Set不可以存储相同的元素。

  (3)、说说对HashSet的理解和常用方法掌握
  HashSet是Set集合的一个实现类,其底层实现是HashMap的key,初始化容量是16,负载因子是0.75,扩容机制,是变为原来的2倍。
  HashSet存储元素的顺序并不是按照存入时的顺序(和List不同)而是按照哈希值来存的所以取数据也是按照哈希值取得。元素的哈希值是通过元素的hashcode方法来获取的, HashSet首先判断两个元素的哈希值,如果哈希值一样,接着会比较equals方法 如果 equals结果为true ,HashSet就视为同一个元素。如果equals 为false就不是同一个元素。 哈希值相同equals为false的元素是怎么存储呢,就是在同样的哈希值下顺延(可以认为哈希值相同的元素放在一个哈希桶中)。也就是哈希一样的存一列。
在这里插入图片描述
  (4)、说说对LinkedHashSet的理解
  LinkedHashSet是对在HashSet的基础上维护了一个双向链表,使得LinkedHashSet存取有序。

  (5)、说说对TreeSet的理解
  TreeSet()实现了SortedSet接口。是使用二叉树的原理对新add()的对象按照指定的顺序排序(升序、降序),每增加一个对象都会进行自动排序,将对象插入的二叉树指定的位置。

5、Map集合需要掌握的知识
  (1)、Map集合的特点
  Map集合和collection集合没有关系、
  Map集合是以key和value这种键值对的方式存储元素、
  key和value都存储java对象的内存地址、所有Map集合key值的特点:无序不可重复( Map集合的key值和Set集合存储元素的特点相同)。

  (2)、聊聊对HashMap的理解(重要)和常用方法
  HashMap是Map的一个实现类,其默认初始化容量大小是16。
   扩容机制:是根据扩容因子来扩容的,当容量的使用量达到总容量的0.75时,就会触发扩容,举例说就是,当总容量是16时,使用量达到12,就会触发扩容机制。 每次扩容是将容量扩大为两倍。而且hashmap容量要时刻保持2的幂次方,因为我们计算出hashcode之后,还要对length取余,从而得到 hashmap中index的位置。但是我们用另外一种位运算的方式得到这个位置,速度更快,只是要求length必须为2的幂次方,因此 hashmap 的容量就一直保持为2的幂次方。
   Resize步骤扩容:创建一个新的Entry空数组,长度是原数组的2倍。ReHash:遍历原Entry数组,把所有的Entry重新Hash到新数组。为什么要重新Hash呢?因为长度扩大以后,Hash的规则也随之改变。hash公式:index = HashCode(Key) & (Length - 1)
  当我们put一个值的时候,通过key来计算出hash值,计算出来的hash值做为数组的索引,Node节点中封装了hash值,key,value和next。当链表的长度小于8的时候,处理Hash碰撞冲突的方式是链表。链表的长度大于等于8的时候,就会触发红黑树方式存储。 当元素个数小于等于6的时候,会触发红黑树转化为链表的形式,为什么不是小于等于7,是因为给一个过度,也就是防止添加一个刚好为8,删除一个刚好为7,这样来回转化。
   JDK1.7的时候底层的数据结构是:数组+链表,JDK1.8是:数组+链表+红黑树组成。所有的数据都是通过一个Node节点进行封装,其中Node节点中封装了hash值,key,value,和next指针。hash是通过key计算出的hashCode值进行对数组容量减一求余得到的(官方的求余方式是通过&运算进行的)。不同的key计算出来的hash值可能相同,解决冲突是通过拉链法(链表和红黑树)进行处理。正是因为这种存储形势,所以HashMap的存取顺序是无序的。
   懒加载机制,在put值的时候会判断数组是否为空,如果是就初始化数组,而不是new的时候就初始化。
在这里插入图片描述
  (3)、HashMap在JDK1.8和JDK1.7的区别?
  结论:HashMap在1.7和1.8中最大的区别就是底层数据结构的变化,在1.7中HashMap采用的底层数据结构是数组+链表的形式,而在1.8中HashMap采用的是数组+链表+红黑树的数据结构(当链表长度大于8时,链表会转成红黑树,当长度低于6时红黑树又会转成链表),红黑树是一种平衡二叉搜索树,它能通过左旋、右旋、变色保持树的平衡,关于红黑树大家想了解的可以自行百度,这里不再讲述。之所以用红黑树是因为他能够大大提高查找效率,链表的时间复杂度是O(n)而红黑树的时间复杂度是O(logn)
  那么为啥要链表长度大于8且数组长度大于64才转成红黑树呢,简单来说就是节点太少的时候没必要转换数据结构,因为不仅转换数据结构需要浪费时间同时也要浪费空间。而为什么不直接一直用红黑树呢?这是因为树的结构太浪费空间,只有节点足够多的情况下用树才能体现出它的优势,而如果在节点数量不够多的情况下用链表的效率更高,占用的空间更小。

  (4)、HashMap是如何存放元素的呢
在这里插入图片描述
  当我们向HashMap中存放数据时,首先会根据key的hashCode方法计算出值,然后结合数组长度和算法(如取余法、位运算等)来计算出向数组存放数据的位置索引值。如果索引位置不存在数据的话则将数据放到索引位中;如果索引位置已经存在数据,该位置的hash值和新元素的hash值相同,这时候要利用equals方法比较两个元素的内容如果内容相同则进行覆盖操作,如果内容不同则继续找链表中的下一个元素进行比较,以此类推如果都没重复的则在链表中新开辟一个空间存放元素。
  以上图为例,假设张三、王五、赵六、王五(hash值1001)对应的数组数组下标是1,李四(hash值1002)对应的数组下标是2。刚开始索引位置1为空,(张三,15)直接放入1位置,索引位置 2为空,(李四,22)直接放入2位置,(王五,18)发现索引位置1已经有数据,这时候调用equals方法和张三进行内容比较,发现内容不同,链表开辟新空间存放数据;(赵六,25)发现1位置已经有元素,调用equals和张三比较,内容不同,继续向下和王五比较,内容不同,开辟新空间存放数据;(王五,28)发现1位置已经有元素,调用equals和张三进行内容比较,不同,继续向下和(王五,18)比较,发现内容也相同,这时候则进行覆盖操作将原来的(王五,18)改成(王五,28)。

  (5)、为啥HashMap的底层数据机采用的是数组+链表的形式或数组+链表+红黑树
  当我们向HashMap中存放数据时,首先会根据key的hashCode方法计算出值,然后结合数组长度和算法(如取余法、位运算等)来计算出向数组存放数据在数组和链表中的位置索引值。如果索引位置不存在数据的话则将数据放到索引位中;如果索引位置已经存在数据,这时就发生了hash碰撞**(两个不同的原始值在经过哈希运算后得到相同的结果)**,为了解决hash碰撞,JDK1.8前用的是数组+链表的形式,而JDK1.8后用的是数组+链表+红黑树。jdk1.8后不全部取消链表的原因就是,当数量非常小的时候,扩容的时候,红黑树的拆分迁移效率远远低于链表。

  (6)、聊聊对Hashtable的理解
  Hashtable集合底层的数据机构是:哈希表数据结构
  Hashtable集合是线程安全的,其所有的方法都带有synchronized关键字,效率较低。现在使用的少了。因为控制线程安全有其他更好的方案。
  Hashtable的key和value值不允许为null。
  Hashtable的初始容量为11,Hashtable的扩容机制是:原容量的2倍+1。

  (7)、聊聊对TreeMap的理解
  TreeMap实现SortedMap接口,能够把它保存的记录根据键排序,默认是按键值的升序排序。

  (8)、如果想要保证HashMap的线程安全,应该怎么办?
  可以通过HashTable,该类的出现主要是解决了HashMap的线程安全问题,直接用了synchronized锁,所以效率上不高,不建议使用(发现JDK1.0的线程问题,解决都很暴力)。 - ConcurrentHashMap是java.util.concurrent包下的,并发包下的。他就是对HashMap进行了一个扩展,也就是解决了他的线程安全问题。ConcurrentHashMap用了大量的CAS来进行优化。

  (9)、大家选择ConcurrentHashMap来保证hashMap的线程安全而不是hashTable的原因?
  hashtable采用在所有的方法上加上synchronized来保证线程安全,而synchronized是全局锁
  ConcurrentHashMap 采用分段锁的机制,锁粒度更小,因此性能更高。

  (10)、什么是CAS(乐观锁)+cas呢?
  CAS(Compare and swap)比较和替换是设计并发算法时用到的一种技术。简单来说,比较和替换是使用一个期望值和一个变量的当前值进行比较,如果当前变量的值与我们期望的值相等,就使用一个新值替换当前变量的值。比较和替换这两步操作的原子行是由操作系统提供的指令保证的。单线程进来就不会加锁,但是悲观锁(synchronized、ReedtrantLock)单线程也会加锁。

  (11)、Collection 和 Collections 有什么区别?
  Collection 是一个集合接口(集合类的一个顶级接口)。它提供了对集合对象进行基本操作的通用接口方法。Collection接口在Java 类库中有很多具体的实现。Collection接口的意义是为各种具体的集合提供了最大化的统一操作方式,其直接继承接口有List与Set。 - Collections则是集合类的一个工具类,其中提供了一系列静态方法,用于对集合中元素进行排序、搜索以及线程安全等各种操作。

  (12)、HashMap和HashTable的区别?
  HashMap允许空键值,HashTable不允许。 - HashMap线程不安全(效率高),HashTable线程安全。

  (13)、ConcurrentHashMap默认初始容量是多少、扩容机制、key,value是否可以为null。?
  它的初始容量为16
  它次扩容是原来容量2倍,在transfer方法里面会创建一个原数组的俩倍的node数组来存放原数据。
  它的key和value不可以为null,为null会抛出空指针异常

  (14)、存储在ConCurrentHashmap中每个节点是什么样的,有哪些变量?
  它是实现Map.Entry<K,V>接口。里面存放了hash,key,value,以及next节点。它的value和next节点是用volatile进行修饰,可以保证多线程之间的可见性。

  (15)、ConcurrentHashMap使用什么技术来保证线程安全??
  ①、dk1.7:ReentrantLock+Segment+HashEntry来进行实现的;
 jdk1.7版本concurrentHashMap底层示例
                     jdk1.7版本concurrentHashMap底层示例
     ●介绍:ConcurrentHashMap是基于Segment分段实现的
        每个Segment相对于一个小型的HashMap
        扩容时,对待扩容Segment内部会进行扩容,不影响其他Segment对象
        扩容时,先生成新的数组,然后转移元素到新数组中
        扩容的判断也是每个Segment内部单独判断的,判断是否超过阈值

     ●数据结构:ReentrantLock+Segment+HashEntry,一个Segment中包含一个HashEntry数组, 每个HashEntry又是一个链表结构
     ●元素查询:采用两次hash的方式进行元素查询。第一次Hash定位到Segment,第二次Hash定位到元素所在的链表的头部
     ●锁:采用Segment分段锁。Segment继承了ReentrantLock,通过ReentrantLock进行加锁操作。加锁时,只锁定操作的Segment,其他的Segment不受影响。并发度为segment个数,可以通过构造函数指定,数组扩容不会影响其他的segment。

  ②jdk1.8:放弃了Segment臃肿的设计,采用Node+CAS+Synchronized来保证线程安全;
     ●介绍:ConcurrentHashMap不再基于Segment实现
        ——当某个线程进行put时,如果发ConcurrentHashMap正在进行扩容那么该线程一起进行扩容
        ——如果某个线程put时,发现没有正在进行扩容,则将key-value添加到ConcurrentHashMap中, 然后判断是否超过阈值,超过了则进行扩容
        ——ConcurrentHashMap是支持多个线程同时扩容的。扩容前也是生成一个新数组,在转移元素时,会按照不同的线程进行分组
        ——在转移元素时,先将原数组分组,将每组分给不同的线程来进行元素的转移,每个线程负责一组或多组的元素转移工作
     ●数据结构:synchronized+CAS(乐观锁)+Node+红黑树,Node的val和next都用volatile修饰, 保证可见性,查找,替换,赋值操作都使用CAS。
     ●:锁住的是链表的head节点,不影响其他元素的读写,相比jdk1…7锁粒度更细,效率更高。扩容时,阻塞所有的读写操作、并发扩容。
     ●读操作无锁:Node节点的val和next使用volatile修饰,读写线程对该变量互相可见,可以保证不会读取到脏数据。数组用volatile修饰,保证扩容时被读线程感知。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值