集合知识点一 : List/Set接口实现类

集合

长度可以实现自动扩容和截断,如果不指定泛型可以保存任何类型数据,默认放进去的是Object类型数据,可以保存对象,自身提供了一些列操作对象的方法

集合包括单列集合和双列集合,二者分别对应Collection接口和Map接口,且这两个接口都实现了同一个父接口Iterator

单列接口 Collection

单列接口结构图

List接口的实现类

支持索引,有序,插入和取出有序
允许重复元素
继承了Collection接口中的所有方法,并且多了一些特有方法

List接口的实现类ArrayList — 大数据量下适合查找,不适合添加删除

底层是一个Object类型的可变数组,支持null在内的所有元素,有序,非同步(线程不安全)

public class ArrayList<E> extends AbstractList<E> implements List<E>, RandomAccess, Cloneable, java.io.Serializable{}

  • ArrayList继承了AbstractList抽象类,拥有了相关的添加删除修改遍历等功能
  • ArrayList实现了List,List底层使用数组保存元素,所以其基本都是对数组进行操作
  • ArrayList实现了RandomAccess接口,即提供了快速随机访问功能,可通过元素的序号快速访问所要找的元素对象
  • ArrayList实现了Cloneable接口,能被克隆
  • ArrayList实现了java.io.Serializable接口,所以ArrayList支持序列化

在创建ArrayList集合时,有三种构造方法:

  • 无参构造 默认初始化容量为10(jdk 1.8 后 为0)
  • 有参构造 参数为int型数字, 集合默认长度为指定长度
  • 有参构造 参数为Object类型,定义集合保存数据类型

add()方法
每次添加元素时,都会判断当前数组是否需要扩容,即当前元素个数是否大于数组长度,需要扩容调用grow()方法,计算新的数组长度,以1.5倍扩容,再调用copyof()方法进行拷贝扩容,方法内会声明一个新数组,将旧数组的数据复制到新数组中,并将索引赋给新数组
remove()方法
remove方法会让所删元素以后的所有元素前移一位,并把最后一位置空,方便GC

ArrayList实现了fast-fail机制,即快速失败机制,通过对modcount判断是否有其他线程修改了当前ArrayList
如果大家没听过快速失败机制,应当是没有遇到过ConcurrentModificationException这个异常,下面大概解释一下,在多线程的环境下,当线程A对ArrayList进行修改时,每修改一次,参数nodcount就会+1。当修改完成后,进行数据迭代遍历时,在迭代器初始化或者过程中,会将modcount赋给迭代器的expectedModCount。此时线程B完成了对同一ArrayList的修改,modcount+1,此时的modcount和expectedModCount数值不相等,所以在遍历ArrayList时,迭代器内部会对modcount和expectedModCount数值进行判断二者是否相等,不相等说明数据被修改,此时的数据不是最新的,直接会抛出ConcurrentModificationException这个异常

那么在高并发情况下,需要用到ArrayList类型保存数据,但是又不能总抛异常吧,这个时候直接加锁是没有用的,那用什么呢???可以用这个CopyOnWriteArrayList

CopyOnWriteArrayList —采用写时复制,实现读写分离,且线程安全

CopyOnWriteArrayList 底层也是一个Object[]数组,和ArrayList基本一致,允许所有类型的值包括null

仅有一个构造,创建一个初始长度为0的数组,每添加一个元素数组长度+1

CopyOnWriteArrayList 为什么会线程安全呢?
原因是:CopyOnWriteArrayList 的每次增删改操作都会声明一个新数组,将旧数组copyof()到新数组中,再将旧数组的索引赋给新数组
CopyOnWriteArrayList 除了读操作,其他操作都有加锁和释放锁的过程,这就保证了在高并发的状态下数据的安全性,但是也造成了CopyOnWriteArrayList 只能保证数据的最终一致性,不能保证数据的实时一致性。即当一个线程在修改数据时,其他线程读到的是旧数据的值,出现脏读
CopyOnWriteArrayList适合读多写少的数据,且因为采用写时复制技术,所以对内存占用大,在实际应用中如果数据量大的情况,我们可以用ConcurrentHashMap来代替

方法时间复杂度备注
get()O(1)直接查找下标元素
add()O(1)直接插入到数组最后
add(index,E)O(n)插入点后所有元素后移
remove()O(n)删除点后所有元素前移
List接口实现类LinkedList —大数据量下适合增删不适合查找

底层是一个双向链表,LinkedList中维护了两个属性,first和last(头结点和尾节点),每个节点Node又维护了prev、item、next三个属性,prve指向前一个节点,last指向后一个节点,item为当前节点保存的数据
有序,支持null在内的所有元素,基于链表的非同步实现
LinkedList结构图

public class LinkedList<E>
     extends AbstractSequentialList<E>
     implements List<E>, Deque<E>, Cloneable, java.io.Serializable
  • LinkedList实现了Deque接口,这是其链表结构的基础
  • 其他的就不在赘述了

创建LinkedList时,有两种构造:

  • 无参构造 声明一个空链表( 闭环链表 )
  • 有参构造 参数为Object,定义链表数据类型

addAll(int index,collection<? Extends E> e)方法:
addAll()方法最后也会调用该有参方法。本方法先判断插入的位置是否合法,在将集合转换为数组,插入数组
remove方法 取出prev和last对前后节点的指向

Linkedlist查找采用的是 类二分查找,先判断用index与链表长度size/2比较,确定在链表的那个部分,然后再去对应的部分查找,这样最多遍历半个集合就可找到,但是只会从头结点正序遍历或尾节点倒序遍历,所以查找元素越靠近链表的size中间点,效率越低,实际这个效率是很低的

说一个我以前查到的小知识(实在不好意思,时间太久,出处想不起来了):
Linkedlist底层在遍历时,会先将Linkedlist转化为数组而不是直接用集合遍历,其实二者的遍历效果是一样的,为什么要转换再遍历呢?

  1. 如果直接遍历集合的话,那么在遍历过程中需要插入元素,在堆上分配内存空间,修改指针域,这个过程中就会一直占用着这个集合,考虑正确同步的话,其他线程只能一直等待。
  2. 如果转化为数组,只需要遍历集合,而遍历集合过程中不需要额外的操作,所以占用的时间相对是较短的,这样就利于其他线程尽快的使用这个集合。说白了,就是有利于提高多线程访问该集合的效率,尽可能短时间的阻塞。
方法时间复杂度备注
get()O(n)经最多一次遍历找到
add()O(1)直接插入到链表最后
add(index,E)O(n)查找到所插入位置,进行指针指向更改操作
remove()O(1)直接指针指向更改操作
List接口实现类Vector

大致与ArrayList一样,扩容倍数为2,是同步的

ArrayList与Vector对比

ArrayListVector
底层结构可变数组Object可变数组
出现版本1.21.0
同步不同步同步
效率
扩容倍数1.52.0

ArrayList与Linkedlist对比

ArrayListLinkedlist
底层结构可变数组Object双向链表
出现版本1.21.2
同步不同步不同步
增删效率
改查效率高 random-access低 类二分查找

对于随机访问的两个方法get和set,ArrayList效率高于linkedlist,因为linkedlist需要移动指针
对于增删的两个方法,add和remove,linkedlist效率高,因为ArrayList需要移动元素

Set接口实现类

无序,插入和取出没有顺序,没有索引
不允许重复元素,所以最多包含一个null

如果实现类是Hashset和LinkedHashset要求元素类型必须实现hashcode和equals方法
如果实现类是Treeset要求元素类型必须实现compareable接口并实现compareTo方法或者在Treeset构造器中加入comparaor实现类对象,并实现compare方法

Set接口实现类Hashset

底层结构维护了一个Hashmap,即底层和Hashmap一样是一个哈希表结构
有关Hashmap相关知识可以点击这里

	public class HashSet<E>  
	    extends AbstractSet<E>  
	    implements Set<E>, Cloneable, java.io.Serializable 

Hashset是如何过滤重复项的:通过hashcode和equals方法确定
首先,通过hashcode计算某元素值的索引值,如果在当前set中的索引处没有其他元素则直接添加,如果已有元素,则调用equals方法进行判断是否相等,直接覆盖
注意:待添加的元素需要重写hashcode和equals方法,所以判断等是根据自定义的方法进行的

Hashset底层是Hashmap,可是Hashmap是K-V结构,保存的是键值对,而Hashset保存的是值,这是为什么呢?
hashset底层的确是hashmap,且也是K-V结构,但是它的K值是对象的值,它的V是一个PRESENT常量的Object对象
hashset在去重时,是通过hashcode和equals方法实现的,所以hashset去重依靠的是hashcode进行K的比较,因为V都是PRESENT的Object常量所以无需比较,hashmap是依靠hashcode确定位置,equals进行值的比较

Hashset同样面临高并发问题,所以这里也可以引入

CopyOnWriteHashset 写时复制 读写分离

底层是一个Hashset,实质是一个hashmap,写时复制和读写分离具体理解可以参考CopyOnWriteArraylist,其他方法等均和Hashset无太大差别

Set接口的实现类Treeset

底层结构维护了一个Treemap,即底层和Treemap一样都是红黑树( 二叉树一种 ),可以实现对元素的自然排序和定制排序。添加元素时,会先判断有无比较器,如果有会按照比较器的比较规则进行排序,如果没有按照元素本身的比较性进行排序

public class TreeSet<E> extends AbstractSet<E>        
    implements NavigableSet<E>, Cloneable, java.io.Serializable{}

实现了NavigableSet接口,是Treeset实现排序的关键接口,NavigableSet是Sortedset的子接口

四个构造函数
无参 按照元素自然排序
有参 参数为collection类型 指定类型的自然排序set
有参 参数为comparator比较器 定制排序
有参 参数为sortedset

Hashset如何去重?
通过调用比较方法,将待添加元素和所有元素比较,返回值为0则为重复项

自然排序:
要求待添加元素实现compareable接口,并实现compareTo方法
定制排序:
待添加元素不用实现compareable接口,只要在hashset接口的构造器中传入comparator实现类对象(匿名内部类)即可,并实现compare方法

Set接口的实现类LinkedHashset

底层结构继承了hashset,底层使用一个linkedhashmap,即底层和linkedhashmap一样,是哈希表+双向链表构成,所以可以保证插入和取出顺序一致
Fast-Fail 实现了快速失败机制
不同步

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值