集合底层解析

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指针构成一个单向链表,被分配到同一个索引上的多个节点可以用这个单向链表连接起来,因而查找、插入和删除主要在同义词链中进行。链地址法适用于经常进行插入和删除的情况。

为什么HashMap会产生死循环?

为什么HashMap会产生死循环?_sufu1065的博客-CSDN博客 ​​​​​​​

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值