Java集合篇:ArrayList、LinkedList、HashMap、HashTable、HashSet的区别

一、ArrayList 和 LinkedList:

1、区别:

(1)ArrayList是基于动态数组的数据结构,查询快,增删慢,线程不安全。

LinkedList是基于链表的数据结构,查询慢,增删快。线程不安全。

(2)对ArrayList和LinkedList而言,在列表末尾增加一个元素所花的开销都是固定的。

(3)对ArrayList而言,主要开销是在内部数组中增加一项,指向所添加的元素,偶尔可能会导致对数组重新进行分配;而对LinkedList而言,这个开销是统一的,分配一个内部Entry对象。
(4)在ArrayList的中间插入或删除一个元素意味着这个列表中剩余的元素都会被移动;而在LinkedList的中间插入或删除一个元素的开销是固定的。
(5)LinkedList不支持高效的随机元素访问。
(6)ArrayList的空间浪费主要体现在在list列表的结尾预留一定的容量空间,而LinkedList的空间花费则体现在它的每一个元素都需要消耗相当的空间。

2、ArrayList和LinkedList的时间复杂度:

(1)ArrayList 是线性表:
get() 直接读取第几个下标,复杂度 O(1);
add(E) 添加元素,直接在后面添加,复杂度O(1);
add(index, E) 添加元素,在第几个元素后面插入,后面的元素需要向后移动,复杂度O(n);
remove()删除元素,后面的元素需要逐个移动,复杂度O(n)。
(2)LinkedList 是链表的操作:
get() 获取第几个元素,依次遍历,复杂度O(n);
add(E) 添加到末尾,复杂度O(1);
add(index, E) 添加第几个元素后,需要先查找到第几个元素,直接指针指向操作,复杂度O(n);
remove()删除元素,直接指针指向操作,复杂度O(1)。

3、ArrayList为什么是线程不安全的?

(1)在 Items[Size] 的位置存放此元素;
(2)增大 Size 的值:
①在单线程运行的情况下,如果 Size = 0,添加一个元素后,此元素在位置 0,而且 Size=1;
②而如果是在多线程情况下,比如有两个线程,线程 A 先将元素存放在位置 0。但是此时 CPU 调度线程A暂停,线程 B 得到运行的机会。线程B也向此 ArrayList 添加元素,因为此时 Size 仍然等于 0 (注意哦,我们假设的是添加一个元素是要两个步骤哦,而线程A仅仅完成了步骤1),所以线程B也将元素存放在位置0。然后线程A和线程B都继续运行,都增加 Size 的值。 那好,现在我们来看看 ArrayList 的情况,元素实际上只有一个,存放在位置 0,而 Size 却等于 2。这就是“线程不安全”了。

4、ArrayList能否无限添加元素?会抛异常吗?

使用ArrayList时,可以无限的往里添加元素,因为它底层是由数组实现的,使用无参构造方法时系统会默认提供默认参数10,而使用有参构造函数时我们会指定大小,当我们添加的元素个数大于数组的初始化长度时,ArrayList会自动为其扩容,扩容后的大小是int newCapacity = (oldCapacity * 3)/2 + 1; (自动增加原来的50%)

5、Java数组动态增加容量:
(1)采用ArrayList类数组,它可以在需要时自动扩容;

(2)采用System.arraycopy方法实现,其声明为:araycopy(Object src,int srcPos,Object dest,int destPos, int length) 。

二、HashMap简介:

1、首先介绍一下解决Hash冲突的四种方法:

(1)开放地址法:(线性探测再散列、二次探测再散列、伪随机探测再散列)

当冲突发生时,使用某种探测技术在散列表中形成一个探测序列。沿此序列逐个单元地查找,直到找到给定的关键字,或者碰到一个开放的地址(即该地址单元为空)为止(若要插入,在探查到开放的地址,则可将待插入的新结点存人该地址单元)。查找时探查到开放的地址则表明表中无待查的关键字,即查找失败。

(2)再哈希法:产生冲突时,计算另一个哈希函数地址、直到冲突不再发生(构造不同的哈希函数)

(3)链地址法:将所有哈希地址相同的记录都链接在同一个链表中。(HashMap使用这种方法解决哈希冲突)

(4)建立一个公共溢出区:把冲突的记录都放在另一个存储空间,不放在表里面。

2、HashMap介绍:

(1)HashMap是基于Map接口的非同步实现,结构可以看成数组+链表。

HashMap 是一个散列表,它存储的内容是键值对映射,每一个键值对也叫做Entry(Entry的组成:Key、value、next),key和value都可以为null。HashMap中的映射不是有序的。

(2)HashMap 的实现不是同步的,这意味着它不是线程安全的,后执行的线程会覆盖先执行线程的修改,在多线程环境下若使用HashMap需要使用Collections.synchronizedMap()方法来获取一个线程安全的集合。

Collections.synchronizedMap()实现原理是:Collections定义了一个SynchronizedMap的内部类,这个类实现了Map接口,在调用方法时使用synchronized来保证线程同步,当然了实际上操作的还是我们传入的HashMap实例,简单的说就是Collections.synchronizedMap()方法帮我们在操作HashMap时自动添加了synchronized来实现线程同步。

(3)HashMap 的实例有两个参数影响其性能:“初始容量” 和 “加载因子”。

    容量是哈希表中桶的数量,初始容量只是哈希表在创建时的容量,容量必须是2的N次幂,这是为了提高计算机的执行效率。加载因子是哈希表在其容量自动增加之前可以达到多满的一种尺度。当哈希表中的条目数超出了加载因子与当前容量的乘积时,则要对该哈希表进行 resize 操作(即重建内部数据结构),从而哈希表将具有大约两倍的桶数。(创建新表,将旧表映射到新表中)

    通常,默认加载因子是 0.75, 这是在时间和空间成本上寻求一种折衷。加载因子过高虽然减少了空间开销,但同时也增加了查询成本。在设置初始容量时应该考虑到映射中所需的条目数及其加载因子,以便最大限度地减少 resize 操作次数。如果初始容量大于最大条目数除以加载因子,则不会发生 resize 操作。

(4)JDK8中HashMap的新特性:如果某个桶中的链表记录过大的话(当前是TREEIFY_THRESHOLD = 8),就会把这个链动态变成红黑二叉树,使查询最差复杂度由O(N)变成了O(logN)。

3、一些有关HashMap的问题:

(1)HashMap的get()、put()方法的工作原理?

    我们通过put()和get()方法储存和获取对象。当我们将键值对传递给put()方法时,它调用键对象的hashCode()方法来计算hashcode,让后找到bucket位置来储存Entry对象。当获取对象时,通过键对象的equals()方法找到正确的键值对,然后返回值对象。

(2)为什么String, Interger这样的wrapper类适合作为HashMap键?

    因为String是不可变的,也是final的,而且已经重写了equals()和hashCode()方法了。其他的wrapper类也有这个特点。不可变性是必要的,因为为了要计算hashCode(),就要防止键值改变,如果键值在放入时和获取时返回不同的hashcode的话,那么就不能从HashMap中找到你想要的对象。不可变性还有其他的优点如线程安全。如果你可以仅仅通过将某个field声明成final就能保证hashCode是不变的,那么请这么做吧。因为获取对象的时候要用到equals()和hashCode()方法,那么键对象正确的重写这两个方法是非常重要的。如果两个不相等的对象返回不同的hashcode的话,那么碰撞的几率就会小些,这样就能提高HashMap的性能。

三、HashMap和Hashtable的区别:

1、两者最主要的区别在于Hashtable是线程安全,而HashMap则非线程安全。Hashtable的实现方法里面都添加了synchronized关键字来确保线程同步,因此相对而言HashMap性能会高一些。

2、HashMap可以使用null作为key,HashMap以null作为key时,总是存储在table数组的第一个节点上。而Hashtable则不允许null作为key。

3、HashMap继承了AbstractMap,HashTable继承Dictionary抽象类,两者均实现Map接口。

4、HashMap的初始容量为16,Hashtable初始容量为11,两者的填充因子默认都是0.75。

5、HashMap扩容时是当前容量翻倍即:capacity2,Hashtable扩容时是容量翻倍+1即:capacity2+1。

6、HashMap和Hashtable的底层实现都是数组+链表结构实现。

7、两者计算hash的方法不同:
Hashtable计算hash是直接使用key的hashcode对table数组的长度直接进行取模:

HashMap计算hash对key的hashcode进行了二次hash,以获得更好的散列值,然后对table数组长度取摸:

8、判断是否含有某个键 :
在HashMap 中,null 可以作为键,这样的键只有一个;可以有一个或多个键所对应的值为null。当get()方法返回null 值时,既可以表示HashMap 中没有该键,也可以表示该键所对应的值为null。因此,在HashMap 中不能用get()方法来判断HashMap 中是否存在某个键,而应该用containsKey()方法来判断。Hashtable 的键值都不能为null,所以可以用get()方法来判断是否含有某个键。

四、HashMap和HashSet的区别:

在这里插入图片描述

五、ConcurrentHashMap实现的原理(线程安全):

1、实现原理:

(1)引入了“分段锁”【Segment(默认16)】的概念,可以理解为把一个大的Map拆分成N个小的HashTable,根据key.hashCode()来决定把key放到哪个HashTable中。

(2)当实现Put方法时,在key值经过正常的hash后,还要再经过一次segmentForHash算法,用来分配具体防盗哪个Segment。后来的线程如果经过计算也是放在这个Segment下,则需要先获取锁,如果计算得出应该放在其他的Segment,则正常执行,不会影响效率,以此实现线程安全。ConcurrentHashMap使用锁分离技术,只要多个修改操作不发生在同一个Segment上,它们就可以并发进行。

(3)有些方法需要跨段,比如size()和containsValue(),需要锁定整个表而而不仅仅是某个段,这需要按顺序锁定所有段,操作完毕后,又按顺序释放所有段的锁。这里“按顺序”是很重要的,否则极有可能出现死锁,在ConcurrentHashMap内部,段数组是final的,并且其成员变量实际上也是final的,但是,仅仅是将数组声明为final的并不保证数组成员也是final的,这需要实现上的保证。这可以确保不会出现死锁,因为获得锁的顺序是固定的。

2、可以使用CocurrentHashMap来代替Hashtable吗?

Hashtable是synchronized的,但是ConcurrentHashMap同步性能更好,因为它仅仅根据同步级别对map的一部分进行上锁。ConcurrentHashMap当然可以代替HashTable,但是HashTable提供更强的线程安全性。

原文:https://blog.csdn.net/a745233700/article/details/80803541

  • 1
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值