文章目录
- Java容器种类
- 详细说说他们都有哪些内容
- Collection :存储对象的集合
- 为什么HashSet和ArrayDeque不支持有序性操作
- Map:存储键值对的映射表
- ArrayList和LinkedList的区别?
- ArrayList的增删一定比LinkedList的增删慢吗?
- native()方法是什么?
- ArrayList实现RandomAccess接口有何作用?
- 什么是标记接口?
- 说一下Vector和ArrayList的联系和区别
- ArrayList的扩容机制具体步骤
- ArrayList和Array的异同?
- 何时使用ArrayList最佳?
- 遍历一个List有哪些不同的方式?
- 不同的遍历方式有什么不一样的?
- Collection和Collections的区别
- PriorityQueue的特点
- HashSet的实现原理
- HashMap的实现原理
- HashMap的长度为什么是2的整数幂次方
- HashMap的put方法
- HashMap的get方法
- HashMap的resize()方法
Java容器种类
主要分为两大类Collection和Map
详细说说他们都有哪些内容
Collection :存储对象的集合
- List接口:Vector、ArrayList、LinkedList为其实现类;
- Queue接口:被Deque接口继承、其中 LinkedList和ArrayDeque也实现了Deque接口
- Set接口:被SortedSet接口继承,SortedSet接口又被TreeSet类继承; Set接口被HashSet和LinkedHashSet实现类继承;其中LinkedHashSet同时继承了HashSet类和Set接口
为什么HashSet和ArrayDeque不支持有序性操作
为什么HashSet和ArrayDeque不支持有序性操作?
- HashSet: 是基于HashMap实现的,而HashMap在1.8前内部使用哈希表(HashTable)来存储键值对,元素的存储位置由哈希码决定的,哈希码根据对象的内容实现的,故HashSet存储的值具有一定的随机性,不支持有序性操作
- ArrayDeque: 底层维护了一个双端队列,删除和添加操作遵循的是先进先出和后进后出的原则,也不具备有序性
Map:存储键值对的映射表
- HashTable实现类:HashTable相较于HashMap而言是线程安全的,因为HashTable的所有主要方法都加了synchronized关键字,所以在单线程环境下,HashTable的性能会比HashMap差一些。而在多线程环境下,由于HashTable的线程安全特性,其性能可能会优于HashMap。
当HashTable中的元素数量达到一定的阈值时,也会触发扩容。扩容后的HashTable大小会是原大小的两倍,并且所有的元素都需要被重新哈希,放入新的数组位置。 - HashMap实现类:1.7基于数组+链表实现;1.8后增加了红黑树。链表是为了解决哈希冲突而存在的JDK1.8 以后在解决哈希冲突时有了较大的变化,当链表长度大于阈值(默认为 8)(将链表转换成红黑树前会判断,如果当前数组的长度小于 64,那么会选择先进行数组扩容,而不是转换为红黑树)时,将链表转化为红黑树,以减少搜索时间。值得一提的是,数组的扩容取决于加载因子的大小(默认为0.75),较小的加载因子(0.5)会使得哈希表更频繁地进行扩容从而占用了过多的空间,较大的加载因子会减少扩容 的频率,但是可能导致较多的冲突,从而影响性能。
- TreeMap实现类:(根据谐音梗),它是基于红黑树实现的,Value值可重复,不同的key可以有相同的Value;但是key值不可重复,因为后进入的Value会被覆盖掉。它的Value是一种可以一对多,但是不能多对一的模式( 一个男生可以追多个女生,但多个男生不能追一个女生 )
- LinkedHashMap实现类:继承自 HashMap。使用双向链表来维护元素的顺序,顺序为插入顺序或者最近最少使用(LRU)顺序。
ArrayList和LinkedList的区别?
- ArrayList 底层是数组实现的,查找快(连续内存中)、增删慢(整个数组改变),支持高效的随机访问,即get(int index) 方法
- LinkedList同时实现了List接口和Deque接口,底层是由双向链表实现的,查找慢(链表在内存中是不连续的)增删快,由于存在前驱节点和后驱节点所以占用内存会比ArrayList多
ArrayList的增删一定比LinkedList的增删慢吗?
- 如果增删都是在末尾操作(调用remove()和add()方法),此时ArrayList就不需要移动和复制数组来进行操作了。如果数据量有百万级时,速度比LinkedList要快
- 如果增删在中间,LinkedList主要耗时在遍历上(链表内存不连续),ArrayList主要耗时在移动和复制上(底层调用的是arrayCopy()方法,是native方法)。LinkedList的遍历速度要比Array的复制和移动慢。如果数据量具有百万级时,依旧是ArrayList快
native()方法是什么?
native 方法是 Java 中声明,由操作系统中具体方法实现。
使用 native 关键字说明这个方法是原生函数,也就是这个方法是用 C/C++ 语言实现的,并且被编译成了 DLL,由 Java去调用。
ArrayList实现RandomAccess接口有何作用?
RandomAccess接口是一个标记接口 ,该接口中并没有实现任何东西,标记该类代表其有快速随机访问功能,此功能是其本身就有的功能,而非加了接口后才有的,因为ArrayList的底层是数组,数据的连续性内存,天然的具有快速随访问功能。而LinkedList的底层是链表,它分散的内存,天然的就不具备快速随机访问功能,所以并没有在LinkedList实现RandomAccess接口
什么是标记接口?
当一个类实现了标记接口后,编译器和运行时环境可以通过反射等机制来检查该类是否实现了特定的接口。这样可以在程序运行时根据标记接口的存在与否来进行下一步的处理。
说一下Vector和ArrayList的联系和区别
- 线程安全性:Vector的方法都有Synchronized关键字,是线程安全的,而ArrayList并没有该关键字,所以它并不是线程安全的
- 性能:由于加锁的原因,Vector的性能不如ArrayList的性能好
- Vector的默认扩容是1倍,ArrayList的默认扩容是1.5倍
ArrayList的扩容机制具体步骤
- 初始化:创建一个初始容量的数组,默认为10个元素大小。
- 添加元素:当你向 ArrayList 添加元素时,它会检查当前数组的容量是否足够,如果不够,则进行扩容操作。
- 扩容操作:扩容操作会创建一个新的数组,通常是当前容量的1.5倍或2倍大小,并将原有元素复制到新数组中。
- 更新引用:ArrayList 内部会将引用指向新的数组,以便后续的元素添加和访问。
- 继续添加:继续添加元素,如果数组再次满了,则重复上述扩容操作。
因此,ArrayList 的动态扩容机制保证了它可以根据需要动态地调整容量,以容纳任意数量的元素。这使得 ArrayList 具有灵活性和高效性,但也需要注意,频繁的扩容操作可能会导致性能下降,因此在预知大量元素添加的情况下,可以通过构造函数提供一个更大的初始容量,以减少扩容次数。
ArrayList和Array的异同?
- 存储类型的不同:
Array:只可存储基本数据类型和对象
ArrayList:只能存储对象 - 大小不同
Array:被设置的为固定大小
ArrayList:是可变数组,能动态扩容
何时使用ArrayList最佳?
- 不确定列表的大小
- 需要随机的访问元素
- 不需要线程安全
- 需要频繁的插入元素
遍历一个List有哪些不同的方式?
- for
- foreach
- iterator
不同的遍历方式有什么不一样的?
- for循环一般用来处理比较简单的有序的,可预知大小的集合或数组
- foreach可用于遍历任何集合或数组,而且操作简单易懂,他唯一的不好就是需要了解集合内部类型
- iterator是最强大的,他可以随时修改或者删除集合内部的元素,并且是在不需要知道元素和集合的大小的情况下进行的,当你需要对不同的容器实现同样的遍历方式时,迭代器是最好的选择!
- 同样遍历一个集合,iterator和foreach用时不相上下。for循环用时最少。
Collection和Collections的区别
- Collection是一个接口,是很多集合的父接口,它定义了多种集合的基本操作
- Collections是一个工具类,提供了很多实用的集合操作方法,能够方便我们进行集合操作
PriorityQueue的特点
- PriorityQue中放置的元素必须要能够比较大小,不能插入无法比较大小的对象,否则会抛出ClassCastExpection异常
- 不能存储null
- 默认数组初始长度是11,也可以指定初始容量
- 它是一个比较标准的队列,不是严格标准,它不是严格先进先出的,内部按队列元素的大小进行了重新排序,所以要放入集合中的元素必须可以比较
HashSet的实现原理
HashSet是基于HashMap实现的,默认构造函数是一个初始大小为16,加载因子为0.75的HashMap。封装了一个HashMap对象来存储所有的集合元素,所有放入HashSet中的集合元素实际上存入了HashMap的key中,当向HashSet中存入元素时必须要同时重写hashcode()方法和equals()方法,因为在向HashSet是不允许有重复的值存在的,hashCode是用来确定存入对象的位置,如果不重写hashCode,则两个意义相同但内存不一样的值会被hashCode认为是不同的,此时我们重写后的equals是认为他们相同的,又因为是先调用的hashCode后调用的equals的所以这种情况是会跳过equals直接把相同的值存入不同的位置
HashMap的实现原理
HashMap基于hashing原理,我们通过put()和get()方法储存和获取对象。当我们将键值对传递给put()方法时,它调用键对象的hashCode()方法来计算hashcode,让后找到bucket位置来储存值对象。当获取对象时,通过键对象的equals()方法找到正确的键值对,然后返回值对象。HashMap使用链表来解决碰撞问题,当发生碰撞了,对象将会储存在链表的下一个节点中。 HashMap在每个链表节点中储存键值对对象
HashMap的长度为什么是2的整数幂次方
- 因为这样可以通过构造位运算,快速寻址定址。这是由hash()方法的底层源码结构所决定的,无论是java7还是java8他们的hash()底层源码都涉及到了位运算,这种位运算让低位保留部分高位信息,减少哈希碰撞
- 当桶数组长度为2的正整数幂时,如果桶发生扩容(长度翻倍),则桶中的元素大概只有一半需要切换到新的桶中,另一半留在原先的桶中就可以
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
HashMap的put方法
调用key的hashCode方法计算哈希值,并据此计算出数组下标index
如果发现当前的桶数组为null,则调用resize()方法进行初始化
如果没有发生哈希碰撞,则直接放到对应的桶中
如果发生哈希碰撞,且节点已经存在,就替换掉相应的value
如果发生哈希碰撞,且桶中存放的是树状结构,则挂载到树上
如果碰撞后为链表,添加到链表尾,如果链表超度超过TREEIFY_THRESHOLD默认是8,则将链表转换为树结构
数据put完成后,如果HashMap的总数超过threshold就要resize
HashMap的get方法
调用key的hashCode方法计算哈希值,并据此计算出数组下标index
找到key所在桶的第一个元素
如果第一个元素的key等于待查找的key,直接返回
如果第一个元素是树节点就按照树的方式来查找
否则按照链表的方向查找
如果没有找到则返回null
HashMap的resize()方法
- 原数组是否为空
- 为空的话 初始化设置新数组长度以及新阈值,然后新建一个Node数组,赋值给table,并返回
- 如果不为空新建Node数组,赋值给table返回
- 若原数组不为空
- 设置新阈值是是旧阈值的二倍 新建一个Node数组,赋值给table
- 遍历数组
- 如果每个数组后面链表的位置没有链表,则根据算法讲旧元素赋值到新位置上