Java集合框架

JAVA集合结构
JAVA集合,也称作容器,主要是由两大接口派生出来的:
Collection 和 Map

顾名思义,容器就是用来存放数据的。那么Collection 和Map 这两大接口的不同之处在于:

  • Collection 存放单一元素
  • Map 存放key-value 键值对

Colleciton 接口

Collection 接口有3个子接口:

  • List 接口
  • Queue 接口
  • Set 接口

List 接口

List 最大的特点就是 **有序 ,可重复 **

List 接口有3个实现:

ArrayList 实现类

  • 底层数据结构是:数组

  • 优点:

    • 随机访问性强,可以根据下标快速定位数据
    • 查找速度快
  • 缺点:

  • 插入和删除效率低,因为需要移动数据,特别操作的数据在数组中间的时候

  • 大小固定,不方便动态扩展

  • 可能浪费内存(因为数组连续的,每次申请数组之前必须规定数组的大小,如果不合适就会浪费内存

    • 数组初始大小默认是 10 ,需要扩容时,在原数组大小上增加 1/2

    LinkedList 实现类

    • 底层数据结构:链表 ,准确的时候是双链表 ,不仅实现了 List 接口,还实现了 Queue接口
    • 优点:
      • 插入速度快
      • 大小不固定,扩展灵活
      • 内存利用率高,不会浪费内存
  • 缺点

    • 不能随机查找,需要进行遍历查找(使用的是二分法进行查找)

    Vector

    • 数据结构跟ArrayList 基本一致

    • 里面大部分方法都用sychronized 修饰,所以这是一个线程安全的类

    • 扩容的时候跟 ArrayList 是不一样的 ,Vector 是可以指定扩容时新增的数组大小,如果没有指定的话,每次扩容时就在原数组基础上增加一倍

    • Stack 是Vector 的子类,也就是栈

扩展问题

  • 为什么使用ForEach 遍历集合时进行元素删除会报错? 而使用迭代器时不会报错?

    AbstractList 中有个重要的属性叫 modCount ,从名字上就可以得知 这叫 修改计数器 ,只要你对集合中的元素进行了添加/删除操作 ,没有修改元素的方法啊 。只要对元素进行了添加/删除操作,这个修改计数器就会加 1
    同时不论是foreach 遍历 还是迭代器遍历,在内部都维护了一个 expectedModCount ,初始值就是 集合的modCount
    在for循环中,如果进行了元素的添加或者删除,会导致这个计数器加 1 ,等到下一次遍历的时候就会出现modCount 和 exceptedModCount 不一致,于是系统就会认为出现了并发修改的情况,报一个 concurrentModifyException
    为什么迭代器可以在遍历的时候删除元素呢?
    迭代器每迭代一次就会将 modCount 重新赋值给 expectedModCount

  • ArrayList 和 LinkedList 的存储效率?

ArrayList 底层是数组,也就是每个元素就是直接的数据
LinkedList 底层是链表,准确的说是双链表,每个节点维护了3个东西:当前节点的值,上一个节点的地址,下一个节点的地址 。
从数据结构上来看,同样多的元素,LinkedList 需要更大的内存,以32位系统来算的话,n个元素就要多出 4 (4个字节)* 2n 的大小

  • 为什么数组可以实现随机访问? 为什么数组下标是从0开始?

数组本质上是一种线性表的结构,它是一组连续的空间,来存储相同类型的数据
计算机在分配内存时会对应地分配一个内存地址,连续的内存空间对应的就是连续的内存地址,而计算机是通过内存地址或者内存中的值
所以数组这个线性表中,只要记住第一个元素的内存地址,也叫首地址
如果想访问第 n 个元素,在没有越界的情况下,只需要用首地址 + (n -1) *数据类型占用的空间 就可以得到第n 个元素的内存地址,进而得到相应的值
下标为什么从0开始呢?
这其实是一种优化,我们刚才提到了在计算第n个元素的地址时使用的是 n-1 .,那么如果我们约定第一个元素的下标为 0,第n个元素的下标为 n-1 ,那么在算地址的时候是不是可以省略掉 减 1 这个步骤。比如查第5个元素,如果下标从1开始,那么送个5进去 ,数组内部在查找元素就要做 5-1 这个操作,如果下标从0开始,要查第5个元素,我们送进去的就是个 4,数组内部在查找时就省掉了 -1 这个操作

Set接口

  • HashSet
    使用 hashMap的 key来存储元素,主要特点是无序,查找速度很快
  • LinkedHashSet
    底层数据结构是数组加链表 ,数组存元素,链表存元素顺序
  • TreeSet
    采用红黑树的方式,特点是有序,可以采用自然排序,也就是元素对象自身的排序,这种需要元素实现comparable 接口 ;另一种方式是初始化时送一个实现了compator 接口的比较器,由这个比较器进行排序。排序的时候,值大的放后面,值小的放前面,相等的认为是同一个值就不往里面放了。

Queue

是一种一端进,一端出的线性数据结构,从语义上来看队列就是先进先出的。

ArrayList

Deque

两端都可以进出

  • ArrayQueue

PriorityQueue

元素取出的时候,按规定的优先级出去
还有一些有容量限制的队列,比如线程中常用的BlockingQueue

Map接口

HashMap

  • 底层实现:数组 + 链表 ,允许有一个key 为null, value 也可以为null

  • 元素插入过程

    • 先算出key 的hash 值,然后对hash值做一个二次hash 获得一个最终的hash值
      - 用最终的hash值 对数组长度取模,得到对应数组的下标,也就是确定了key的位置
      - 如果当前没有数组位置没有元素,直接创建一个节点存入数组
      - 如果当前位置已经有元素了,这种情况就叫做hash碰撞/hash冲突,这时候需要进去equals判断,如果equals相同,则认为是两个相同的元素,直接用后面的value覆盖之前的value;
      - 如果equals算下来不相等,则判断当前节点是否为树节点,是的话直接插入树节点中;
      - 不是的话,那就是个链表了 。检查下链表的高度是否达到8 ,数组长度是否达到64 ,如果达到了,则将链表转换为红黑树,再将元素插入;如果没有达到,就正常在链表最后端加一个节点
      - 这个过程中会出现链表转红黑树的情况,也会出现红黑树转回链表的情况,当红黑树高度小于等于6时就会转为链表。

    允许 key 为 null ,那这个元素放什么位置呢 ?
    key为null的算出来的hash 为0 ,放在数组第一个位置

    二次hash 是怎么做的? 有什么作用
    二次hash 是使用了一个扰动函数进行处理,就是将hash值右移动1 6位 然后与原hash值进行异或运算,这样就混合了原hash值中高位和地位,增大了随机性,可以让数据元素均衡的散列,减少碰撞。

  • 默认初始容量是 16 ,加载因子为0.75 ,可以指定初始容量,但不一定是

  • 扩容: 在数组的位置达到阈值的时候就会可能执行扩容,但是是先放置元素再判断是否要扩容。所以可能出现无效扩容的情况,放置这个元素之后我发现要扩容,我给他翻一倍,后面又不加元素,那就有空间浪费了。

LinkedHashMap

有序的map ,维护了一个双向链表用来记录元素顺序的

TreeMap

可以自然排序 也可以选择排序

ConcurrentHashMap

这里面用到了一个分段的思维,就是把map里的数组分为了16部分,那么理论上最多可以同时有16个线程对这个map进行操作。就刚好每个线程操作一个分段。比如有两个线程要进行数据插入,通过hash算出来,key的位置属于两个分段,如果是hashMap 同时有2个线程要插入元素,那么肯定是先把整个map锁住,然后看谁先获得锁进行操作 。ConcurrentHashMap就不是这么做的了,如果发现key需要放置的是位置属于两个分段,那么这两个线程只要分别去竞争这2个分段的所就行了 ,他们之间是没有竞争的,只是锁住了每一个分段。当然了有一些操作,比如contains sie 这些操作,还是需要锁住整个map 的,但我们一般认为size contains这种属于不是那么重要的操作

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值