java集合底层原理面试题总结

版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接: https://blog.csdn.net/weixin_43806056/article/details/89150927

java中的常用的集合有几种?(set,list,map)除了这几个常用的,还有一个queue(队列), 并且set,list,queue的同一个父类是collection,map和collectin是 同一级,但是没有什么联系,是相互独立的。
Set集合:集合元素是不能重复的。元素是没有顺序的。所以它不能基于位置访问元素。TreeSet和HashSet是它的实现类。
List集合: 集合元素是可以重复的。元素是有顺序的。所以它可以基于位置访问元素。ArrayList和LinkedList是它的实现类。
Map:它包含键值对。Map的键是不能重复的。Map不能保证存储的顺序。HashMap和TreeMap是它的实现类。
Queue用于模拟队列这种数据结构。

java集合的数据结构
Set接口是collection的子接口:
HashSet 其底层是由哈希表(其实还是数组和单链表实现)实现,hashset的底层是基于hashMap来实现的。Hash可以存入null值,但是只能存储一个null。
HashSet遍历的三种方法:foreach遍历,使用数组遍历,使用迭代器
LinkedHashSet 底层实现:继承hashset类,基于LinkedHashMap来实现的,和hashSet的区别主要在于它是双向链表结构,有顺序。在插入元素的时候,也是通过元素的哈希值来进行来决定元素的存储位置。但是利用链表的来保证以插入 的顺序来进行保存元素

TreeSet 底层实现是通过treeMap(二叉树)实现的,是一个有序集合类,存入Treeset集合的值,并不是按照插入顺序来排序。排序的实现有两种:
自然排序:实现comparable要使用要排序的元素的compareTo(object obj)方法进行比较元素之间的大小关系,然后将元素进行升序排列。
定制排序:如果要定制排序,就要实现comparator接口,实现int compare方法

List接口是collection的子接口:
ArrayList 数据结构:底层实现是object数组(动态数组,可以扩容),它的随机访问速度极快,但是插入和删除的操作需要移动数组中的元素,比较麻烦。
注意:(1)它在没有初始化长度的时候,默认长度是10
(2)如果向ArrayLIst添加元素,超过了原来的容量,那么扩容方案:扩容到原数组的1.5倍,如果扩容完之后还是小于返回到mincapacity(最小容量)
(3)ArrayList是线程不安全的,在多线程的情况下不要使用
如果非要使用list,就推荐使用vector,它基本上和ArrayList一样,区别在于vector中大部分方法都使用了同步关键字synchronized修饰,这样在多线程不会出现并发错误,扩容区别vector默认扩容就是原来的2倍。
ArrayList遍历的几种方式:1:使用foreach遍历list 2:将list转化为数组,在进行遍历 3:使用迭代器遍历
LinkedList 数据结构:双向链表 随机访问速度慢,查找元素是从头开始一个一个查找,但是适合于频繁的插入和删除操作。线程不安全
LinkedList内部实现原理:LinkedList内部有一个类似于C语言的1结构体的Entry内部类,它包含了前一个元素的地址引用和后一个元素的地址引用,类似C语言的指针。进行操作元素。

Map集合:
HashMap 基于哈希表的map接口实现,底层使用数组和单链表实现。使用了哈希算法,以便进行快速查询,线程不安全,存入的顺序和遍历的顺序可能不一致。
LinkedHashMap是map接口的哈希表和链表来实现的。
TreeMap 底层实现是红黑树,可以用于排序。排序分为两种:TreeMap提供了四个构造方法,实现了方法的重载。无参构造方法中比较器的值为null,采用的是自然排序的方法,如果指定了比较器则是定制排序。

集合是否线程安全的总结:
线程安全:Vector,HashTable(jdk1.0引入),
线程不安全:ArrayList,LinkedList,HashMap,TreeMap,TreeSet,HashSet

HashTable和hashMap之间的区别:

  1. HashMap几乎可以等价于Hashtable,除了HashMap是非synchronized的,并可以接受null(HashMap可以接受为null的键值(key)和值(value),而Hashtable则不行)。
    2.HashMap是非synchronized,而Hashtable是synchronized,这意味着Hashtable是线程安全的,多个线程可以共享一个Hashtable;而如果没有正确的同步的话,多个线程是不能共享HashMap的。Java 5提供了ConcurrentHashMap,它是HashTable的替代,比HashTable的扩展性更好。
  2. 由于Hashtable是线程安全的也是synchronized,所以在单线程环境下它比HashMap要慢。如果你不需要同步,只需要单一线程,那么使用HashMap性能要好过Hashtable。

TreeSet和TreeMap区别:

  1. 最主要的区别就是TreeSet和TreeMap分别实现Set和Map接口
    2.TreeSet只存储一个对象,而TreeMap存储两个对象Key和Value(仅仅key对象有序)
    3.TreeSet中不能有重复对象,而TreeMap中可以重复
    4.TreeMap的底层采用红黑树的实现,完成数据有序的插入,排序。因此它要求一定要有Key比较的方法,要么传入Comparator实现,要么key对象实现Comparable接口。

hashmap是如何工作的?
HashMap是一个针对数据结构的键值,每个键都会有相应的值,关键是识别这样的值。
HashMap 基于 hashing原理,我们通过 put ()和 get ()方法储存和获取对象。当我们将键值对传递给 put ()方法时,它调用键对象的 hashCode()方法来计算 hashcode,让后找到 bucket 位置来储存
值对象。当获取对象时,通过键对象的 equals ()方法找到正确的键值对,然后返回值对象。HashMap 使用 LinkedList(链表) 来解决碰撞问题,当发生碰撞了,对象将会储存在 LinkedList (链表)的下一个节点中。 HashMap 在每个 LinkedList(链表) 节点中储存键值对对象。

hashmap是如何插入数据

(1)、先判断数组是否为空,要是为空,初始化数组,可以自定义数组大小(默认是16),先根据数据的key,求出哈希值
(2)、再将此数据的hash值取余(%)数据的长度(默认长度是16),得到数据对应的下标
(3)、再根据下标,将数据插入数组中对应的位置(此时我们要考虑是否要扩容,如果容量超过数组大小的0.75倍,就需要扩容,最大容量是2的30次方)
(4)、如果插入数据时,发现此下标对应位置已经有元素,我们有两种办法(hashmap使用链表法):
1)链表:那么我们就要用到hashmap的单链表了。将这个数据的next指针(指针域)指向此下标对应的节点,作为链表的头部,再将这个节点放到此下标对应的数组的位置,便成功插入数据。
2)再次求哈希值,再取余,一直求到下标对应的位置为空,插入数据
注意:我们插入数据之前会判断key是否存在,利用for循环,从该数据对应下标的链表的头部,开始比较key值,如果我们插入数据的key和链表某个节点的key相同,那么我们将老的value返回出去,将新的value插入进去覆盖老的value

数组达到阀值之后,进行扩容:
调用扩容方法resize(int newcapacity),传入一个新数组的长度,是老数组长度的2倍,,进入resize方法首先判断老数组长度是否达到最大长度,如果超出,则不进行扩容操作。如果没有超出,那么创建新数组,调用transfer()方法,将原来数组中的值放到新数组中(transfer实现:利用for循环内部加一个while循环,for循环遍历老数组,while判断数组是否为空,不为空则通过key的哈希值和新数组的大小取余算出在数组中的存放位置,如果新数组对应位置有数据,那么用链表方式添加元素:先将该数据指向对应索引的节点,插入链表头部,再将该节点放到对应数组下标的位置),直到循环结束,再将这个新数组赋值给table

jdk1.8中hashMap不同:
1、在 Java 1.8 中,如果链表的长度超过了 8 ,那么链表将转化为红黑树;
2、发生 hash 碰撞时,Java 1.7 会在链表头部插入,而 Java 1.8 会在链表尾部插入;
3、在 Java 1.8 中,Entry 被 Node 代替(换了一个马甲)。

hashmap取出数据:
(1)、先根据数据的key,求出哈希值
(2)、再将此数据的hash值取余(%)数据的长度(默认长度是16),得到数据对应的下标
(3)、再根据下标,去数组中找出对应位置的数据,利用将数组中数据的key和自己搜索的数据的key相比较,如果相等就返回value。如果第一次找的不是自己想要的数据,便根据此节点的指针,去链表下一个节点找数据,直到找到为止,返回数据的value

为什么hashmap数组初始化大小都是2的倍数?
当数组长度为2的倍数时,不同的key算出的index(索引)相同的几率小,hash碰撞的几率小,数据在数组上均匀分布,提升查询效率。

treeMap插入操作:
关于红黑树的节点插入操作,首先是改变新节点,新节点的父节点,祖父节点,和新节点的颜色,能在当前分支通过节点的旋转改变的,则通过此种操作,来满足红黑书的特点。
如果当前相关节点的旋转解决不了红黑树的冲突,则通过将红色的节点移动到根节点解决,最后在将根节点设置为黑色
红黑树的特点:
每个节点只能是红色或者黑色
根节点永远是黑色的
所有的叶子的子节点都是空节点,并且都是黑色的
每个红色节点的两个子节点都是黑色的(不会有两个连续的红色节点)
从任一个节点到其子树中每个叶子节点的路径都包含相同数量的黑色节点(叶子节点到根节点的黑色节点数量每条路径都相同)

判断hashset中是否是重复的对象?
先判断两个对象的hashcode是否相同(如果两个对象的hashcode相同,不一定是一个对象,如果两个对象的hashcode不同,一定不是一个对象)如果两个hashcode相同,在进行equals方法判断,如果equals相同则是同一个对象,如果equals判断不相同,那么就不是一个对象。

hashset存储原理?
1、将要传入的数据根据系统的hash算法得到一个hash值
2、根据hash值计算出数据在hash表中的位置
3、判断该位置是否有值,没有值就将数据插入进来,如果有值则再次判断传入的值与已经存储的值的equals结果是否相同,如果相同则不存,如果不相同则通过单链表的形式存储

Arraylist添加元素的原理?
1、调用add方法,在插入数据之前,先要判断数组是否能够再容下元素(调用方法参数是size+1,是当前数组长度+1也就是mincapacity最小容量),此时要判断是默认数组还是自定义数组。如果是默认数组先判断mincapacity和默认数组长度10谁大,返回谁。如果是自定义的数组,直接返回mincapacity。根据返回的值和数组的定义长度大小比较,如果返回的值大,则进行扩容。
2、扩容:先根据原来的数组的长度,进行扩容到原来数组长度的1.5倍。如果扩容之后的容量小于mincapacity,则将mincapacity赋值给新数组的容量。如果扩容之后的容量大于数组定义最大容量,那么赋值给新数组Integer.MAX_VALUE-8
3、调用copyof方法,新建了一个数组,将原数组的对象复制到新的数组中,并且将现有的数组指向新的数组。
4、在调用element[size++]=e添加元素

arraylist add(index,E element)在某个位置添加元素实现
首先判断下标是否越界,检查当前容量是否可以容下一个元素,不够则扩容
通过arraycopy方法将elementdata从index开始后面的元素往后移一位,在指定下标位置添加元素

arraylist get(int index)方法
功能:返回指定下标的元素
调用get(index)方法,之后先检查index的值是否大于Arraylist的大小
如果超过arraylist大小,会抛出异常,index<0了,系统会抛出异常
返回指定下标的元素

linkedlist的add方法添加元素的操作:
LInkedList添加操作时每个新添加的对象都会被放到新建的Node对象中,Node对象中包含加入的对象和前后指针属性(pred和next,pred和next其实都是Node对象,直接存放的就是当前对象的前一个节点对象和后一个节点对象,最后一个及节点的next值为null),然后将原来最后一个last节点的next指针指向新加入的对象。

linkedlist的get方法,查询元素:
判断给定的索引值,若索引值大于整个链表长度的一半,则从后往前找,若索引值小于整个链表的长度的一般,则从前往后找。 这样就可以保证,不管链表长度有多大,搜索的时候最多只搜索链表长度的一半就可以找到,大大提升了效率。

猜你喜欢

转载自blog.csdn.net/weixin_43806056/article/details/89150927