扯一扯Java集合

大家好!我是仰望星空的鑫。今天我们来聊一聊Java中的集合。

为什么我们要学习集合呢?其实主要有两大目的,

  1. 是方便我们用集合中的API来优化我们的代码。

  2. 是我们可以通过阅读集合源码来学习借鉴jdk是如何写出优雅代码的。

目录

集合框架(collections framework)

两大基类Collection与Map

Collection

Map

Collection接口

1.List接口

2.Set接口

Map接口

Map是java中的一个接口,常见的实现类有Hashmap,TreeMap,LinkedHashMap,ConcurrenHashMap。

HashMap底层是通过数组+链表+红黑树

TreeMap底层是红黑树实现

LinkedHashMap底层是数组+链表+红黑树+双向链表


 

集合框架(collections framework)

        首先大家要先知道什么是集合,集合和数组有什么区别呢?其实集合存储的就是一些对象,但是数组的长度不能改变,集合的长度是可以改变的。我们也可以把集合想成一种微型的数据库。那么我们操作集合的目的就是实现“增删改查”功能。

两大基类Collection与Map

在集合框架的类继承体系中,有两个基类接口,分别是:

  • Collection表示一组纯数据

  • Map表示一组key-value对

Collection

 

1e01c8f011fbe42db2344c704454c841.png

由图可以看出collection集合有三个接口

  • set:表示不允许有重复元素的集合。

  • List:允许有重复元素的集合。

  • Queue:主要用于存储数据,不能改变数据。

Map

16b9589da5c9856e204b359201593e7e.png

由图可以看出Map集合有四个接口:

  • 把map的内容看做key集合

  • 把map的内容看作value的集合

  • 把map的内容看做key—value映射的集合

 

Collection接口

Collection接口有两个主要的子接口,set接口和list接口,(注意,map不是collection的子接口)

1.List接口

特点:

1、代表一个有序集合

2、允许存放重复元素

3、集合中的每个元素都有其对应的顺序索引,可以通过索引来查找元素。(这点类似于java中的数组)

4、实现list接口的子就接口有:Arraylist,Linkedlist,Vector,Stack。

(1)ArrayList

ArrayList是一个动态数组(具有数组的属性),也是我们最常用的集合。它允许任何符合规则的元素插入甚至包括null。每一个ArrayList都有一个初始容量(10),该容量代表了数组的大小。随着容器中的元素不断增加,容器的大小也会随着增加。在每次向容器中增加元素的同时都会进行容量检查,当快溢出时,就会进行扩容操作。所以如果我们明确所插入元素的多少,最好指定一个初始容量值,避免过多的进行扩容操作而浪费时间、效率。

ArrayList是可以随机访问的,同时ArrayList是非同步的(及多线程情况下是线程不安全的)

(2)LinkedList

 Linklist是一个双向链表实现的,所以他具有链表的一些优点(查询慢,但是增删快)。同时他也是非同步的,如果多个线程共享一个数据使用,就会出现线程不安全的情况,如果想避免该情况的发生,一种方法就是创建List时同时构造一个同步list。

(3)Vector

与ArrayList相同,但是他是线程安全的。所以他是一种线程安全的动态数组。

2.Set接口

Set是一种不包括重复元素的Collection。它维持它自己的内部排序,所以随机访问没有任何意义。与List一样,它同样允许null的存在但是仅有一个。由于Set接口的特殊性,所有传入Set集合中的元素都必须不同,同时要注意任何可变对象,如果在对集合中元素进行操作时,导致e1.equals(e2)==true,则必定会产生某些问题。Set接口有三个具体实现类,分别是散列集HashSet、链式散列集LinkedHashSet和树形集TreeSet。

需要注意的是:虽然Set中元素没有顺序,但是元素在set中的位置是由该元素的HashCode决定的,其具体位置其实是固定的。

(1)HashSet

        HashSet 是一个没有重复元素的集合。它是由HashMap实现的,不保证元素的顺序(这里所说的没有顺序是指:元素插入的顺序与输出的顺序不一致),而且HashSet允许使用null 元素。HashSet是非同步的,如果多个线程同时访问一个哈希set,而其中至少一个线程修改了该set,那么它必须保持外部同步。 HashSet按Hash算法来存储集合的元素,因此具有很好的存取和查找性能。

(2)LinkedHashSet

        LinkedHashSet继承自HashSet,其底层是基于LinkedHashMap来实现的,有序,非同步。LinkedHashSet集合同样是根据元素的hashCode值来决定元素的存储位置,但是它同时使用链表维护元素的次序。这样使得元素看起来像是以插入顺序保存的,也就是说,当遍历该集合时候,LinkedHashSet将会以元素的添加顺序访问集合的元素。

(3)TreeSet

        TreeSet是一个有序集合,其底层是基于TreeMap实现的,非线程安全。TreeSet可以确保集合元素处于排序状态。

 

Map接口

讲解Map接口,其实就是讲解Map接口的实现类,首先Map与Collection集合是并列关系,Map是保存具有映射关系的数据:Key-value;

Map中的key,value可以是任何引用类型的数据,他们的值会封装到HashMap$Node中去

public static void main(String[] args) {
        Map map = new HashMap();
        map.put("no1","渣渣鑫");
        map.put("no2","渣渣鑫");
        System.out.println("map"+map);
    }

  088478200d3544bfa1ee30b41ec9d5ba.pngMap中的key不允许重复,如果重复了,就会覆盖之前key对应的值,但是value可以重复,大家看代码可以发现,value可以重复,同时你会发现,map集合是无序的,输出顺序与输入顺序不同。

这个与源码有关。

efaca39cfc3e46b884e854fccf2810d4.png

Map的key值可以为null,但是只能有一个为null,value可以有多个null值。 

Map与List和Set集合不同,Map是通过键值对的映射来存储数据的,每一个Key对应一个Value,所以它不能存储相同的key值,但是value可以相同。

 

Map是java中的一个接口,常见的实现类有Hashmap,TreeMap,LinkedHashMap,ConcurrenHashMap。

HashMap底层是通过数组+链表+红黑树

我们new一个HashMap的时候会发生什么呢?首先Hashmap有多种构造方法,但是最重要的一个是指定初始值的大小和负载因子。如果不指定的话,默认初始容量为16,负载因子为0.75

问题:初始化容量值和负载因子是什么?

这二者的作用与HashMap的扩容机制密切相关,所以我先讲一下关于HashMap的扩容机制。

首先将(key1,val1)直接放入Node类型的数组中去,此时没有给构造方法传入任何参数,默认数组初始化容量是16,默认的负载因子是0.75,当在集合中存放数据的时候,首先先看一下哈希表中的数组的数量是否达到计算出来的阈值【初始化容量*负载因子(16*0.75=12)】,也就是说当存入数据大于12的时候就会开启自动扩容机制(初始化容量为2的幂次),扩容后的容量为原容量的两倍。

问题:HashMap的初始化容量必须是2的幂次方吗?2的奇数或者2的倍数不可以吗?

  • 2的幂次方:hashmap在确定元素落在数组的位置的时候,计算方法是(n - 1) & hash,n为数组长度也就是初始容量 。hashmap结构是数组,每个数组里面的结构是node(链表或红黑树),正常情况下,如果你想放数据到不同的位置,肯定会想到取余数确定放在那个数组里,计算公式:hash % n,这个是十进制计算。在计算机中, (n - 1) & hash,当n为2次幂时,会满足一个公式:(n - 1) & hash = hash % n,计算更加高效。
  • 奇数不行:在计算hash的时候,确定落在数组的位置的时候,计算方法是(n - 1) & hash,奇数n-1为偶数,偶数2进制的结尾都是0,经过hash值&运算后末尾都是0,那么0001,0011,0101,1001,1011,0111,1101这几个位置永远都不能存放元素了,空间浪费相当大,更糟的是这种情况中,数组可以使用的位置比数组长度小了很多,这样就会造成空间的浪费而且会增加hash冲突。

问题:HashMap的负载因子必须是0.75吗?

负载因子太大

负载因子的大小决定了扩容阈值的大小,当负载因子变大,扩容发生的频率就会变低,发生hash冲突的几率就会比较大。【哈希冲突:简单理解就是参与哈希运算的数据是无限的,但是计算后的结果是有限的,因此总会存在不同数据经过哈希运算得出相同的结果,这就是哈希冲突】,哈希冲突导致的结果就是数组中的某一条链表比较长,会影响性能。

负载因子太小

当负载因子太小的时候,扩容频率比较高,浪费的空间也比较多,发生Hash冲突的几率小,但是空间浪费严重。比如负载因子为0.5的时候,hashmap的初始容量是128,当数量达到65的时候就会触发扩容机制,扩容后的容量为256,256里面只存储了65个元素,其余的全部浪费了。

问题:哈希算法是如何计算key值得?

他就是先算出正常的哈希值,然后与高16位做异或运算,产生最终的哈希值。

    static final int hash(Object key) {
        int h;
        return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
    }

问题:HashMap是如何存入数据的

首先根据Key值进行哈希算法得到index的值,相当于是索引值,如果该值没有与其他索引值产生碰撞,就直接放入数组中去,如果产生了哈希冲突,就需要判断目前数据结构采用的是链表还是红黑树的存储,根据不同的情况进行插入。

问题:什么时候用链表什么时候用红黑树

当数组的大小大于64且链表的大小大于8的时候才会将链表改为红黑树,当红黑树的值小于6的时候,会退化为链表。

问题:关于HashMap线程安全问题?

HashMap是线程不安全的,在多线程情况下,put操作会导致数据不一致,扩容的时候,在jdk1.8之前采用的是头插法,当两个线程同时检测到hashmap需要扩容,在进行同时扩容的时候可能会造成链表的循环。在1.8之后就不存在这个问题了,因为当链表长度大于8的时候就会转变成红黑树。

问题:如何解决线程不安全的问题呢?

使用HashTable代替HashMap,因为HashTable在外层套了synchronize,这种被包装出来的效率都比较低效,所以我们用的最多的线程安全的集合是ConcurrenHashMap;ConcurrenHashMap的底层数据结构是数组+链表/红黑树。他支持高并发的访问和更新,是线程安全的。

TreeMap底层是红黑树实现

TreeMap的key不能为null,如果为null会报空指针异常。

LinkedHashMap底层是数组+链表+红黑树+双向链表

watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETkDku7DmnJvmmJ_nqbrnmoTpkas,size_9,color_FFFFFF,t_70,g_se,x_16

如果大家想了解更多的编程知识,欢迎大家关注渣渣鑫的公众号!和渣渣鑫一起进步! 

 

 

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

仰望星空的鑫

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值