Java笔记梳理(五)查漏补缺-[集合]

        继续,本篇文章记录一下,集合。

========== 【集合】==========

        集合,我们最常用的就是List和Map了,再说具体点,我们平时用的最多的就是ArrayList和HashMap。这两个算是比较常用的,偶尔还用用一下Set。但是集合这个大家庭里边显然不止这两个,之前一直没有去认真了解过它,今天就记录一下吧。
        PS:不要跟我研究或者争论Map属不属于集合,这个好多书上的说法都不统一,争论这个实在是没有意义啊!!
        先说一下集合的继承结构,我们知道集合的两大接口,Collection和Map,他们下属的有很多的接口和实现类,他们一起构成了集合这个大家庭。Collection下属的接口List和Set。List有两个实现类,分别是ArrayList和LinkedList,Set也有两个实现类,分别是HashSet和TreeSet。而Map接口下边有一个抽象的实现类AbstractMap,AbstractMap下边又有n多个子类,比如我们常用的HashMap。Map还有一个实现类叫HashTable,不过不常用。
        总结一下:
                -Collection:
                        -List:
                                -ArrayLIst
                                -LinkedList
                        -Set:
                                -HashSet
                                -TreeSet
                -Map:
                        -HashMap //其实这个HashMap是继承了AbstractMap的同时又实现了Map接口。
                        -HashTable
        然后接下来,我们一个一个来说。

-1.Collection

-1.1:List
-1.1.1ArrayList

        特点:底层封装的是一个Object类型的数组,查找快,增删慢。这个数组的默认长度是10,超出范围之后,增长1.5倍。
        ArrayList查找快、增删慢的原因是由于底层封装的是一个数组,数组的特点就是顺序存储,每个元素都有一个对应的下标,存储在连续的内存空间中,每个元素的对应的内存地址是连续的,因此通过下标查找元素非常快,但是这样增删就慢了,因为如果是末尾添加或删除就算了,如果是在任意位置添加或删除,添加或者删除之后每个元素都要移动一次下标,所以效率就相对较低。
        ArrayList默认长度是10,这个默认长度的并不是一开始就默认指定的,从它的构造函数就可以看出,初始构造的是一个空数组,只有在第一次添加元素时,如果数组为空,就会扩容到初始容量10,然后这个容量被存满之后,会扩容那个至原来的1.5倍,这个1.5倍的数据是来自ArrayList提供的grow()方法,这个方法会先获取数组的当前长度赋值给一个叫oldCapacity的变量,这个就是原始容量。然后根据int newCapacity = oldCapacity + (oldCapacity >> 1);这个算法,计算出新容量赋值给newCapacity,这里边oldCapacity >> 1就是一个移位运算,先把原始数据转为二进制,然后整体右移移位,相当于十进制的除以2,然后再加上原始数据,得到的新数据就是原来的1.5倍了。另外,它扩容的方法还不只是扩展数组长度这么简单,而是通过Arrays类的copyOf()方法将旧数组复制一份到新数组,而之前扩容的长度就是新数组的长度,这样扩容就结束了。

-1.1.2LinkedList

        特点:底层数据结构是一个双链表,增删快,查找慢,两端效率高。
        LinkedList是双链表结构,它真正存储数据东西的是一个个的结点对象。它有一个内部类Node,Node身上分别有item,prev,next三个属性,分别代表,结点数据域,前驱指针域,和后继指针域。因为它是链式存储,每个元素都对应一个索引号,存储在不连续的内存空间中,每个元素对应的内存地址是不连续的,因此查找起来比较麻烦。但是链式存储的优点就是增删快,在删除是只需要修改结点的前驱指针和后继指针即可,不需要移动元素。
        LinkedList两端效率高,是因为它继承了Deque接口,提供了很多操作集合首尾元素的方法,如:removeFirst()、addFirst()、getFirst()、removeLast()、addLast()、getLast()。这些方法的存在也使得LinkeList操作首尾元素比较方便,同时由于它实现了Deque接口,因此它也可以被作为栈和队列使用。

-1.2Set
-1.2.1HashSet

        特点:无序性,不可重复,唯一性允许存在空值,但只能有一个。
        感觉很有意思,Hashset底层居然是用HashMap来存放数据,而且看名字就知道,它也是散列式存储,而它的无序性就体现在这里,散列式存储都是无序的。
        Set集合是不可重复的,为什么?它不是底层封装了HashMap集合吗,而Map集合是可以出现重复元素的,答案很简单,从它的源码中可以看出,HashSet的add方法调用map集合的put方法来完成元素的新增,HashSet在使用这个put方法时,把元素存放在键值对中键的位置,而值的位置存放了一个Object类型的虚拟值PRESENT,源码对这个变量的注释描述就叫虚拟值:Dummy value to associate with an Object in the backing Map。所以,HashSet是利用HashMap的键来存放数据的,而HashMap的键名是不可重复的,这也就是为什么HashSet中不能存在重复元素。

-1.2.2TreeSet

        特点:有序性,不可重复,唯一性,可以排序。
        TreeSet的底层存储数据还是Map,数据结构式二叉树,不过这个不是HashMap,而是Map大家族中的另一个成员,NavigableMap接口。既然是Map,那么不可重复和唯一性就没有必要再说了。
        NavigableMap接口,我对它不是很了解,这个接口下边有n多实现类,不过我看不懂它我就找它的父接口SortMap,同时他还有一个实现类TreeMap。而TreeSet的构造函数在构造时传入的NavigableMap接口实现类默认就是TreeMap。源头找到了,所以TreeSet在添加元素时调用的方法默认会TreeMap中的put方法。而TreeMap在进行添加元素的,会用一个Comparator比较器,对所有元素进行比较重排序。以此来找到元素该添加到那个位置。而在比较时,两个元素的类型必须是一样的,所以,TreeSet中是不允许存在不同类型的元素的。一个TreeSet集合的存储元素类型是由它第一次存放的元素决定的。这也就是TreeSet有序性和可以自动排序的体现,而这个有序性和Set集合的无序性并不冲突。Set集合的无序性是指存取顺序不一样,而TreeSet的有序性是指,存储在TreeSet集合中的元素是经过排序之后有序存储的,而存取顺序依然是不一样的。
        另外,当向TreeSet中添加自定义类型对象时,要求自定义类型实现java.lang.Comparable接口并重写compareTo(Object obj)方法。在此方法中,指明按照自定义类的哪个属性进行排序。

-2.Map

-2.1HashMap

        特点:无序性,底层是基于数组和链表实现的。默认初始长度16,线程不安全。可以指定初始容量,但必须是2的指数幂。
        无序性就不在解释了,HashMap的底层是数组和链表,为什么是两个呢,看源码就知道了。HashMap中有一个静态内部类Node,这个Node实现了Map接口中的一个内部接口Entry<K,V>,Node有四个属性,分别是:hash(哈希值),key(键),value(值),next(指针域)。这是链表的体现,但是HashMap存储数据用的并不是它,而是根据Node创建的一个Node类型的数组,所以说,HashMap底层是基于数组和链表实现的,因为他既有数组又有链表。
        说一下它是怎么存的。
         1、首先当对象过来要存储时,回到用put方法,put方法会调用putValue方法,这个时候,如果我们是第一次存储时,它会调用resize()方法对数组进行一次扩容,扩容的初始长度就是默认值16。
         2、扩容后的数组会拿这个扩容后的数组长度-1同hash值相与,这个hash值是拿要存储元素的键名,根据HashMap中Node结点对象提供的hash值算法得来的key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16。相与的结果就是最终元素在结点数组中存储对应的下标。
         3、拿到这个下标之后,会先判断这个位置是否有元素,如果没有元素就创建一个新结点对象当做头结点直接插入即可。如果这个位置已经有元素了,就会对比,待插入元素和原有头结点元素的hash值是否相等,如果相等就判断key值是否相等,如果都相等直接覆盖掉,如果不相等,就会链表形式存储到该位置,将新插入元素作为头结点,原有元素作为新元素的后继结点。
        再说一下它是如何扩容的。
        刚才也说了,如果是第一次添加元素,会将数组容量扩容到初始容量16。HashMap有自己判断是否需要扩容的机制,就是I一个公式:threshold = DEFAULT_INITIAL_CAPACITY * DEFAULT_LOAD_FACTOR
        这个threshold就是判断标准,DEFAULT_INITIAL_CAPACITY 是初始化容量16,DEFAULT_LOAD_FACTOR是默认的加载因此0.75。根据16*0.75=12。这是第一次扩容。当元素个数达到12个时会进行第二次扩容,第二次扩容,会将数组长度扩容到原来的2倍。
        所以它的整个扩容机制是:首先要判断数组有没有扩容过,如果没有扩容过,就进行第一次扩容,扩容到长度16。如果已经扩容过就判断数组长度是否已经达到最大限度,如果达到就将数组长度设置为int的最大值。如果没有达到最大限度,就扩容到原来的2倍。
        另外,如果没有扩容过的话,还要判断一下,是否指定了初始容量,如果指定了初始容量,那就按照指定的初始容量扩容,如果没有指定,就要按默认的扩容。
        最后再说一下它为什么是线程不安全的。
        因为HashMap中的方法并没有用synchronized关键字修饰为是线程同步,所以一个HashMap对象如果被多个线程同时访问,假设他们同时执行添加元素的操作,如果多个进程会获取到同一个头结点,如果一个线程作为头结点添加进入数组之后,另一个线程也作为头结点加入,那么这两个线程的后继结点一样,那么第一个线程添加的数据就丢失了。所以HashMap是线程不安全的。
        HashMap搞了好长一段啊。。。。本来想换换脑子,现在头皮发麻

-2.2HashTable

        特点:线程安全,键值不允许出现空值。初始长度为11,可以扩容,扩容方式为原来的2倍+1。
        HashTable是线程安全的,因为HashTable中的所有方法都由synchronized关键字修饰为线程同步,这也导致了,HashTable的效率比较低。不允许出现空值,如果出现空值就会报空指针异常。
        说一下它是怎么存的。
        HashTable存储数据使用的是它内部类Entry<K,V>的数组,它也实现了Map接口中的内部接口Entry<K,V>,是不是和HashMap的Node内部类很像,依然是链表+数据。别急,他俩的属性都是一模一样的,也是包括了:hash(哈希值),key(键),value(值),next(指针域)。不过,HashTable的hash值算法和HashMap不一样,它们俩的Hash算法都是拿元素的key值的hash值和vaule的hash值做位运算,最后的出来一个新的hash值。不同点就在于,HashTable中key的hash值是通过Objects类中的hashCode算法获得的。HashMap是Object类。

========== 【总结】==========

ArrayList和LinkedList的区别

1.ArrayList查找快,增删慢,LinkedList增删快,查找慢
2.ArrayList底层封装的是Object数组,是顺序存储,LinkedList底层是结点对象,链式存储。
3.ArrayList有默认长度10,存满之后会扩容1.5倍,LinkedList是链式结构,没有初始容量,无需扩容。

HashSet、HashMap和HashTable的区别

1.HashSet底层封装的是HashMap,所以和HashMap允许出现空值,但只能有一个空值。HashTable不允许出现空值。
2.HashSet存储数据不可重复,HashMap和HashTable可以重复。
3.HashSet和HashMap是线程不安全的,HashTable是线程安全的。

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值