ArrayList 有序 可重复 底层:数组
LinkList 有序 可重复 底层:链表(双向循环链表)
vector 有序 可重复 底层:数组 安全)
HashSet 无序 不可重复 底层:1.7(数组+链表)1.8(数组+链表+红黑树)
LinkedHashSet 有序 不可重复 底层: 哈希表和链表
TreeSet 无序(可以按大小排序(可排序集合)) 不可重复 底层:红黑树
HashMap 无序 不可重复 底层:1.7(数组+链表)1.8(数组+链表+红黑树)
LinkedHashMap 有序 不可重复 底层: 哈希表和链表
TreeMap 无序(可以按大小排序(可排序集合)) 不可重复 底层:红黑树
Hashtable 无序 不可重复 底层:数组+链表 (安全)
List
Java集合框架图析(Collection-List)_java集合图_12点前就睡的博客-CSDN博客
ArrayList
小知识点:
1.ArrayList的底层原理是由动态数组实现的。其数组的长度是随着元素的增多而变长的;当实例化ArrayList时(List array = new ArrayList();)它的长度是默认为10(jdk1.7)。
2.ArrayList是有顺序的,当ArrayList增添元素时,他是按照顺序从头部往后添加的
3.当新增的数据超过当前数组时,它会创建一个新的数组,其新数组的长度为当前数组的1.5倍,然后将当前数组的元素复制到新数组中,当前数组的内存被释放。
4.数组存在的位置为在JVM的堆中,用来存储固定大小同类型元素的。当新的元素需要存储时,会存储在最前面,所以每次存储新元素时,所有的元素都会向后移动位置。同理,删除一个元素时,数组中所有的元素都会向前移动位置,所以ArrayList对于增删的效率很低。
5.数组里面的元素占用的内存相同并且连续排列的。在查询时可以根据数组的下标来进行快速访问,所以ArrayList对于查询效率高。
ArrayList自动扩容原理(jdk1.7)
每次扩容, 都会将老数组的内容拷贝到新数组中, 每次数组容量的增长是oldCapacity + (oldCapacity >> 1), 大约是原数组的1.5倍, 这种代价还是蛮高的, 所以我们可以使用之前, 预知需要的元素空间, 在构造ArrayList时, 就指定容量, 避免扩容过程, 或者根据生产的大量需求, 手动配置ensureCapacity(int minCapacity)
这段代码的作用是在需要时扩展容器的大小,以满足最小容量要求。
public void ensureCapacity(int minCapacity) {
if (minCapacity > elementData.length
&& !(elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA
&& minCapacity <= DEFAULT_CAPACITY)) {
modCount++;
grow(minCapacity);
}
}
}
意思是接收一个minCapacity参数表示最小需要的容量大小首先会检查minCapacity是否大于当前已分配的容器实际大小elementData.length如果满足条件,而且当前容器不是默认大小的空容器并且minCapacity也不小于DEFAULT_CAPACITY
1.modCount++:这是一个用于记录容器结构修改次数的变量,通常用于迭代过程中检测容器是否在迭代过程中被修改
2.调用grow方法:这个方法会根据需要增加容器的大小,以满足minCapacity要求。
这段代码的作用是根据最小容量要求增加容器的大小,并返回新的容器对象。它使用了Arrays.copyOf方法来创建新的容器,并更新了容器的引用。
private Object[] grow(int minCapacity) {
return elementData = Arrays.copyOf(elementData,
newCapacity(minCapacity));
}
newCapacity`是一个私有方法,只能在类内部被调用。这意味着它只能在当前类中使用,并且无法从类外部进行访问。(自己了解一下)
private int newCapacity(int minCapacity) {
// overflow-conscious code
int oldCapacity = elementData.length;
int newCapacity = oldCapacity + (oldCapacity >> 1);
if (newCapacity - minCapacity <= 0) {
if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA)
return Math.max(DEFAULT_CAPACITY, minCapacity);
if (minCapacity < 0) // overflow
throw new OutOfMemoryError();
return minCapacity;
}
return (newCapacity - MAX_ARRAY_SIZE <= 0)
? newCapacity
: hugeCapacity(minCapacity);
}
private static int hugeCapacity(int minCapacity) {
if (minCapacity < 0) // overflow
throw new OutOfMemoryError();
return (minCapacity > MAX_ARRAY_SIZE) ?
Integer.MAX_VALUE :
MAX_ARRAY_SIZE;
}
预估容量需求:在创建容器时,如果能够预估需要存储的元素数量,可以使用newCapacity来计算并设置
容器的初始容量。这样可以在一开始就为容器分配足够的内存空间,以避免后续的扩容操作。
批量添加元素:如果需要一次性添加一批元素到容器中,可以在添加元素之前使用ensureCapacity方法来
确保容器具备足够的容量。这样可以避免频繁的扩容操作,提高添加元素的效率。
预留空间:如果预计容器会在未来进行元素的添加操作,可以使用ensureCapacity方法预先为容器分配一
定的空间。这样可以避免后续添加元素时频繁的扩容操作,从而提高性能。
性能优化:在对性能要求较高的场景下,可以使用newCapacity和ensureCapacity来显式地控制容器的容
量,以避免不必要的扩容操作和内存分配开销,从而提高整体性能
jdk1.7和jdk1.8在扩容上区别:
jdk1.7 :
在调用构造器的时候,数组长度初始化10,扩容的时候为原数组的1.5倍
jdk1.8
在调用构造器的时候,底层数组为{},在调用add方法时以后底层数组才重新赋值为新数组,新数组长度10
LinkedList
知识点
LinkedList底层是由双向链表来实现的。
1.双向链表是由三部分来组成的:prev、data、next
prev:存储上一个节点的地址;
data:存储将要存储的数据;
next:存储下一个节点的地址;
2.双向链表的排序方式是没有顺序的;当我们新增一个元素时,只需要修改前一个元素的next和后一个元素的prev即可,删除元素同理;这样使得LinkedList对于新增和删除的效率大大提高。但是查询数据时,需要一个一个的查找,直到找到为止,使得查询的效率变得很低。(LinkedList是一种非索引式的数据结构,每次查询都需要遍历链表来查找目标元素,因此在大规模数据集上进行频繁的查询操作可能会导致性能下降。如果需要频繁进行查询操作,可能考虑使用其他数据结构,如哈希表(HashSet、HashMap)或树结构(TreeSet、TreeMap)来提高查询性能。)
场景 :ArrayList适用于随机访问较多、元素数量固定或较少插入删除操作的场景,而LinkedList适用于频繁插入删除、对随机访问性能要求较低的场景
ArrayList和LinkedList的区别是什么?
Vector(线程安全)
**vector的底层实现原理 - 百度文库?wkts=1689638984082&bdQuery=Vector%E7%9A%84%E5%BA%95%E5%B1%82%E5%8E%9F%E7%90%86**
容量(capacity)表示当前分配给 Vector 的内存空间的大小,而大小(size)表示 Vector 中实际存储的元素数量。`reserve` 和 `resize` 是用于改变 Vector 容量和大小的方法。
reserve 方法
原理:reserve 方法用于增加 Vector 的容量,但不会改变 Vector 的大小。它会预分配多余的内存空间,以便在添加元素时避免频繁的内存重新分配操作。当大小超过容量时,Vector 会重新分配内存并将其元素复制到新的内存区域
std::vector<int> my Vector;
myVector.reserve(10); // 预留容量为 10 的内存空间
// 当添加元素时,不会触发内存重新分配
for (int i = 0; i < 10; ++i) {
myVector.push_back(i);
}
resize 方法
原理:`resize` 方法用于改变 Vector 的大小,它可以增加或减少 Vector 中存储的元素数量,并相应地调整容量。在增加大小时,新增的元素将进行初始化;在减小大小时,多余的元素将被销毁或移除。
std::vector<int> myVector;
myVector.resize(5); // 大小设置为 5
// 大小调整后,可以直接访问和修改元素
for (int i = 0; i < myVector.size(); ++i) {
myVector[i] = i;
}
myVector.resize(3); // 大小调整为 3
// 多余的元素被移除或销毁
for (int i = 0; i < myVector.size(); ++i) {
std::cout << myVector[i] << " ";
}
// 输出:0 1 2
注意事项 :resize` 方法会更改 Vector 的大小和容量。增大 Vector 大小时,如果超过当前容量,将会进行内存重新分配;减小大小时,会销毁或移除多余的元素。一般情况下,更改 Vector 大小时,都会触发内存重新分配。
vector扩容
vector的扩容一般有两种方式:半步扩容法(即扩容1.5倍)和2倍扩容法(即扩容2倍)
两者之间的区别重点表现在空间和时间两方面。
2倍扩容时间快,但是不能使用之前的空间,造成空间上的浪费。而1.5倍扩容刚好相反,节省了空间,却因此消耗了更多的时间。
set
Java中Set集合的使用和底层原理_java set是如何实现的_学全栈的灌汤包的博客-CSDN博客
set特点
无序:存取数据的顺序是不一定的, 当数据存入后, 集合的顺序就固定下来了
不重复:可以去除重复
无索引:没有带索引的方法,所以不能使用普通for循环遍历,也不能通过索引来获取元素。
HashSet
底层 HashSet底层结构和源码分析_hashset底层数据结构_天上的云川的博客-CSDN博客https://www.cnblogs.com/hubaoxi/p/15964019.html (1) HashSet的底层原理_hashset默认创建底层数据长度jdk7_qq_45376750的博客-CSDN博客 (2)
TreeSet
Java中Set集合的使用和底层原理_java set是如何实现的_学全栈的灌汤包的博客-CSDN博客
Map
HsahMap
HashMap集合是一个无序的集合,存储元素和取出元素的顺序有可能不一致
扩容原理
https://blog.csdn.net/eg1107/article/details/128228687
原理图
扩容图
if (oldTab != null) {
for (int j = 0; j < oldCap; ++j) {
Node<K,V> e;
判断当前下标为j的数组如果不为空的话赋值为e
if ((e = oldTab[j]) != null) {
判断是否为空
oldTab[j] = null;
判断是否有下一个节点
if (e.next == null)
如果没有,就重新计算在新数组中的下标
newTab[e.hash & (newCap - 1)] = e;
红黑树
else if (e instanceof TreeNode)
((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
else { // preserve order
Node<K,V> loHead = null, loTail = null;
Node<K,V> hiHead = null, hiTail = null;
Node<K,V> next;
do {
next = e.next;
if ((e.hash & oldCap) == 0) {
if (loTail == null)
loHead = e;
else
loTail.next = e;
loTail = e;
}
else {
if (hiTail == null)
hiHead = e;
else
hiTail.next = e;
hiTail = e;
}
} while ((e = next) != null);
if (loTail != null) {
loTail.next = null;
newTab[j] = loHead;
}
if (hiTail != null) {
hiTail.next = null;
newTab[j + oldCap] = hiHead;
}
}
}
}
}
Treemap
红黑树
每个节点有属性
添加流程
public V put(K key, V value) {
Entry<K,V> t = root; 一开始未添加时为null 放入第二个值
if (t == null) {
compare(key, key); // type (and possibly null) check 自己与自己比较检验
root = new Entry<>(key, value, null); 将key和value封装成Entry并付给root
size = 1; 数量+1
modCount++;
return null;
}
int cmp;
Entry<K,V> parent; 父节点
// split comparator and comparable paths
将外部比较器付给cpr
Comparator<? super K> cpr = comparator;
不等于null意味着创建对象的时候调用了有参构造器指定外部比较器
if (cpr != null) {
do {
parent = t;
cmp = cpr.compare(key, t.key);
if (cmp < 0)
t = t.left;
else if (cmp > 0)
t = t.right;
else
return t.setValue(value);
} while (t != null);
}
else {
等于null意味着创建对象的时候调用了空构造器没有指定外部比较器,使用内部比较器比
if (key == null)
throw new NullPointerException();
@SuppressWarnings("unchecked")
Comparable<? super K> k = (Comparable<? super K>) key;
do {
parent = t;
cmp = k.compareTo(t.key);
if (cmp < 0)
t = t.left;
else if (cmp > 0)
t = t.right;
else
return t.setValue(value);
} while (t != null);
}
Entry<K,V> e = new Entry<>(key, value, parent);
if (cmp < 0)
parent.left = e;
else
parent.right = e;
fixAfterInsertion(e);
size++;
modCount++;
return null;
}
红黑树
面试题
说⼀下HashMap的Put⽅法
先说HashMap的Put⽅法的⼤体流程:
1. 根据Key通过哈希算法与与运算得出数组下标
2. 如果数组下标位置元素为空,则将key和value封装为Entry对象(JDK1.7中是Entry对象,JDK1.8中
是Node对象)并放⼊该位置
3. 如果数组下标位置元素不为空,则要分情况讨论
a. 如果是JDK1.7,则先判断是否需要扩容,如果要扩容就进⾏扩容,如果不⽤扩容就⽣成Entry对
象,并使⽤头插法添加到当前位置的链表中
b. 如果是JDK1.8,则会先判断当前位置上的Node的类型,看是红⿊树Node,还是链表Node
ⅰ. 如果是红⿊树Node,则将key和value封装为⼀个红⿊树节点并添加到红⿊树中去,在这个
过程中会判断红⿊树中是否存在当前key,如果存在则更新value
Jdk1.7到Jdk1.8 HashMap 发⽣了什么变化(底层)?
说⼀下HashMap的Put⽅法
9
ⅱ. 如果此位置上的Node对象是链表节点,则将key和value封装为⼀个链表Node并通过尾插
法插⼊到链表的最后位置去,因为是尾插法,所以需要遍历链表,在遍历链表的过程中会
判断是否存在当前key,如果存在则更新value,当遍历完链表后,将新链表Node插⼊到链
表中,插⼊到链表后,会看当前链表的节点个数,如果⼤于等于8,那么则会将该链表转成
红⿊树
ⅲ. 将key和value封装为Node插⼊到链表或红⿊树中后,再判断是否需要进⾏扩容,如果需要
就扩容,如果不需要就结束PUT⽅法
HashMap的扩容机制原理
1.7版本
1. 先⽣成新数组
2. 遍历⽼数组中的每个位置上的链表上的每个元素
3. 取每个元素的key,并基于新数组⻓度,计算出每个元素在新数组中的下标
4. 将元素添加到新数组中去
5. 所有元素转移完了之后,将新数组赋值给HashMap对象的table属性
1.8版本
1. 先⽣成新数组
2. 遍历⽼数组中的每个位置上的链表或红⿊树
3. 如果是链表,则直接将链表中的每个元素重新计算下标,并添加到新数组中去
4. 如果是红⿊树,则先遍历红⿊树,先计算出红⿊树中每个元素对应在新数组中的下标位置
a. 统计每个下标位置的元素个数
b. 如果该位置下的元素个数超过了8,则⽣成⼀个新的红⿊树,并将根节点的添加到新数组的对应
位置
c. 如果该位置下的元素个数没有超过8,那么则⽣成⼀个链表,并将链表的头节点添加到新数组的
对应位置
5. 所有元素转移完了之后,将新数组赋值给HashMap对象的table属性
CopyOnWriteArrayList的底层原理是怎样的
1. ⾸先CopyOnWriteArrayList内部也是⽤过数组来实现的,在向CopyOnWriteArrayList添加元素
时,会复制⼀个新的数组,写操作在新数组上进⾏,读操作在原数组上进⾏
2. 并且,写操作会加锁,防⽌出现并发写⼊丢失数据的问题
3. 写操作结束之后会把原数组指向新数组
4. CopyOnWriteArrayList允许在写操作时来读取数据,⼤⼤提⾼了读的性能,因此适合读多写少的应
⽤场景,但是CopyOnWriteArrayList会⽐较占内存,同时可能读到的数据不是实时最新的数据,所
以不适合实时性要求很⾼的场景
CopyOnWriteArrayList是Java中的线程安全(thread-safe)的List集合实现类之一。它在并发环境下提供了一种安全的方式来进行读操作,而不需要进行显式的同步。(并发安全性)
HashMap和HashTable的区别
1、两者父类不同
HashMap是继承自AbstractMap类,而Hashtable是继承自Dictionary类。不过它们都实现了同时
实现了map、Cloneable(可复制)、Serializable(可序列化)这三个接口。
2、对外提供的接口不同
Hashtable比HashMap多提供了elments() 和contains() 两个方法。 elments() 方法继承自
Hashtable的父类Dictionnary。elements() 方法用于返回此Hashtable中的value的枚举。
contains()方法判断该Hashtable是否包含传入的value。它的作用与containsValue()一致。事实
上,contansValue() 就只是调用了一下contains() 方法。
3、对null的支持不同
Hashtable:key和value都不能为null。
HashMap:key可以为null,但是这样的key只能有一个,因为必须保证key的唯一性;可以有多个
key值对应的value为null。
4、安全性不同
HashMap是线程不安全的,在多线程并发的环境下,可能会产生死锁等问题,因此需要开发人员自
己处理多线程的安全问题。
Hashtable是线程安全的,它的每个方法上都有synchronized 关键字,因此可直接用于多线程中。
虽然HashMap是线程不安全的,但是它的效率远远高于Hashtable,这样设计是合理的,因为大部
分的使用场景都是单线程。当需要多线程操作的时候可以使用线程安全的ConcurrentHashMap。
ConcurrentHashMap虽然也是线程安全的,但是它的效率比Hashtable要高好多倍。因为
ConcurrentHashMap使用了分段锁,并不对整个数据进行锁定。
5、初始容量大小和每次扩充容量大小不同
6、计算hash值的方法不同
Collection包结构,与Collections的区别
Collection是集合类的上级接口,子接口有 Set、List、LinkedList、ArrayList、Vector、Stack、
Set;
Collections是集合类的一个帮助类, 它包含有各种有关集合操作的静态多态方法,用于实现对各种
集合的搜索、排序、线程安全化等操作。此类不能实例化,就像一个工具类,服务于Java的
Collection框架。
数组索引为什么从0开始,不从一开始?
在根据数组索引获取元素的时候,会用索引和寻址公式来计算内存所对应的元素数据,寻址公式是:数组的首地址+索引乘以存储数组的类型大小
如果数组的索引从1开始,寻址公式中,就需要增加一次减法操作,对于CPU来说就多了一个指令,性能不高
数组和list互转?
HashMap的jdk1.7和jdk1.8区别
hashmap的寻址算法
为何HashMap的数组长度一定是2的次幂?
如何解决Hash冲突
什么是hash冲突
哈希表其实就是一个存放哈希值的一个数组,哈希值是通过哈希函数计算出来的,那么哈希冲突就是两个不同值的东西,通过哈希函数计算出来的哈希值相同,这样他们存在数组中的时候就会发生冲突,这就是**哈希冲突**,而且这种冲突只能尽可能的减少,不能完全避免
不能完全避免的原因
因为哈希函数是从关键字集合和地址集合的映像,通常关键字集合为无限大、长度不受限制(密码、或者文件都可以作为关键字),而地址集合确有限,无限量 映射到 有限量 上肯定是存在重合的部分,这就是冲突。
解决方法
1 开放地址法(再散列法)
开放定址法就是一旦发生了冲突,就去寻找下一个空的散列地址,只要散列表足够大,空的散列地址总能找到,并将记录存入
这种方法也称再散列法,其基本思想是:当关键字key的哈希地址p=H(key)出现冲突时,以p为基础,产生另一个哈希地址p1,如果p1仍然冲突,再以p为基础,产生另一个哈希地址p2,…,直到找出一个不冲突的哈希地址pi ,将相应元素存入其中。
就是说当发生冲突时,就去寻找下一个空的地址把数据存入其中,只要哈希表足够大,就总能找到
2.再哈希法Rehash
再哈希法又叫双哈希法,有多个不同的Hash函数,当发生冲突时,使用第二个,第三个,….,等哈希函数
计算地址,直到无冲突。虽然不易发生聚集,但是增加了计算时间。
3.链地址法(HashMap 采用的这个方法)
每个哈希表节点都有一个next指针,多个哈希表节点可以用next指针构成一个单向链表,被分配到同一个索引上的多个节点可以用这个单向链表连接起来,因而查找、插入和删除主要在同义词链中进行。链地址法适用于经常进行插入和删除的情况。