Java 集合List、Set、Map知识结构大全详解

版权声明:本文为博主原创文章,遵循 CC 4.0 by-sa 版权协议,转载请附上原文出处链接和本声明。
本文链接:https://blog.csdn.net/csdn_aiyang/article/details/96173115

目录

概述

一、Collection 接口

(1)List列表 —— 有序、值可重复

(2)Set 集  —— 值不可重复

二、Map 接口

(1)HaspMap ——  无序

(2)HashTable —— 无序

(3)TreeMap —— 有序

三、List、Set、Map的值能否为null?

(1)List —— 允许为null

(2)Set 

(3)Map


概述

集合类存放于java.util包中。集合类存放的都是对象的引用,而非对象本身。常见的集合主要有三种——Set(集)、List(列表)和Map(映射)。其中,List和Set 都实现了 Collection 接口,并且List和Set也是接口,而Map 为独立接口。常见的实现类如下:

  • List  的实现类有:ArrayList、Vector、LinkedList;
  • Set   的实现类有:HashSet、LinkedHashSet、TreeSet;
  • Map 的实现类有:Hashtable、LinkedHashMap、HashMap、TreeMap。

补充知识:散列表,也叫哈希表(Hash table),是根据关键码值(Key value)而直接进行访问的数据结构。也就是说,它通过把关键码值映射到表中一个位置来访问记录,以加快查找的速度。这个映射函数叫做散列函数,存放记录的数组叫做散列表

 

一、Collection 接口

(1)List列表 —— 有序、值可重复

1、ArrayList
优点: 底层数据结构是数组Array,查询快。增删慢。
缺点: 线程不安全,效率高

2、Vector
优点: 底层数据结构是数组Array,查询快。增删慢。
缺点: 线程安全,效率低

*Vector是实现了 synchronized 的,这也是Vector和ArrayList的唯一的区别。

3、LinkedList
优点: 底层数据结构是链表LinkedList,增删快。查询慢。
缺点: 线程不安全,效率高

*链表的每一个节点(Node)都包含两方面的内容:1.节点本身的数据(data);2.下一个节点的信息(nextNode)。所以当对LinkedList做添加,删除动作的时候就不用像基于Array的List一样,必须进行大量的数据移动。只要更改nextNode的相关信息就可以实现了。这就是LinkedList的优势。详细《数据结构与算法》
 

(2)Set 集  —— 值不可重复

1、HashSet
底层数据结构是哈希表,无序。
依赖两个方法来保证元素唯一性:hashCode()和equals()

2、LinkedHashSet
底层数据结构是双向链表哈希表,有序。
由链表保证元素有序,由哈希表保证元素唯一。

3、TreeSet
底层数据结构是红黑树,内部实现排序,也可以自定义排序规则。
自然排序、比较器排序保证元素排序。根据比较的返回值是否是0来保证元素唯一性。

 

二、Map 接口

Map接口有三个比较重要的实现类:HashMap、TreeMap、HashTable。

(1)HaspMap ——  无序

  • 散列桶(数组和链表)
  • 没有 synchronized关键字,即非线程安全,因此效率较高;
  • 允许null值(key和value都允许);
  • 父类是AbstractMap。

工作原理:

通过put()方法传递键(key)和值(value)时,我们先对键调用hashCode()方法,计算并返回的hashCode是用于找到Map数组的bucket位置来储存Node 对象。hashCode()方法中是根据key的hash值(hash算法)来求得对应数组中的位置。因为HashMap的数据结构是数组和链表的结合,如果元素位置尽量的分布均匀些,使得每个位置上的元素数量只有一个,当我们用hash算法求得这个位置的时候,马上就可以知道对应位置的元素就是我们要的,而不用再去遍历链表。所以,我们首先想到的就是把hashcode对数组长度取模运算。

1、取模法

假如数组的长度等于5,这时有一个数据6,我们如何把这个6储存到长度只有5的数组中呢?取余法计算6%5等于1,即把6这个数据放到数组下标为1的位置。如果再有一个数据需要存储,取余法计算后也等于1,就调用equals()判断值是否相同,相同的话就不存储。但是,问题来了,值不相同就会造成Hash碰撞冲突了。

2、Hash碰撞冲突

HashCode()的作用就是保证对象返回唯一hash值对应Map数组中的bucket存储位置,但当两个数据计算后的值一样时,这就发生了碰撞冲突。例如,前面有6%5=1 得到数组下标为1的位置。此时,有个数据是11,那么11%5=1,但是这个为1的位置已经有了6这个数据了,这就叫Hash冲突。

3、解决Hash冲突

(1)开放定址法

开放地址法有个非常关键的特征,就是所有输入的元素全部存放在哈希表里,也就是说,位桶的实现是不需要任何的链表来实现的,换句话说,也就是这个哈希表的装载因子不会超过1。它的实现是在插入一个元素的时候,先通过哈希函数进行判断,若是发生哈希冲突,就以当前地址为基准,某种探查技术在散列表中形成一个探查(测)序列,去寻找下一个地址,若发生冲突再去寻找,直至找到一个为空的地址为止。所以这种方法又称为再散列法

按照形成探查序列的方法不同,可将开放定址法区分为线性探查法、二次探查法、伪随机探测法等。

①线性探查

这种方法的特点是:冲突发生时,顺序查看表中下一单元,直到找出一个空单元或查遍全表。(使用例子:ThreadLocal里面的ThreadLocalMap)

②二次探查

这种方法的特点是:冲突发生时,在表的左右进行跳跃式探测,比较灵活。

③ 伪随机探测

具体实现时,应建立一个伪随机数发生器,(如i=(i+p) % m),生成一个伪随机序列,并给定一个随机数做起点,每次去加上这个伪随机数就可以了。

(2)拉链法

HashMap,HashSet其实都是采用的拉链法来解决哈希冲突就是在每个位桶实现的时候,我们采用链表的数据结构来去存取发生哈希冲突的输入域的关键字(也就是被哈希函数映射到同一个位桶上的关键字)。(jdk1.8之后采用链表+红黑树)

①插入操作:在发生哈希冲突的时候,我们输入域的关键字去映射到位桶中去的时候,我们先检查带插入元素x是否出现在表中,很明显,这个查找所用的次数不会超过装载因子(n/m:n为输入域的关键字个数,m为位桶的数目),它是个常数,所以插入操作的最坏时间复杂度为O(1)的。

②查询操作:和①一样,在发生哈希冲突的时候,我们去检索的时间复杂度不会超过装载因子,也就是检索数据的时间复杂度也是O(1)的

③删除操作:如果在拉链法中我们想要使用链表这种数据结构来实现位桶,那么这个链表一定是双向链表,因为在删除一个元素x的时候,需要更改x的前驱元素的next指针的属性,把x从链表中删除。这个操作的时间复杂度也是O(1)的。

与开放定址法相比,拉链法有如下几个优点:

①拉链法处理冲突简单,且无堆积现象,即非同义词决不会发生冲突,因此平均查找长度较短;

②由于拉链法中各链表上的结点空间是动态申请的,故它更适合于造表前无法确定表长的情况;

③开放定址法为减少冲突,要求装填因子α较小,故当结点规模较大时会浪费很多空间。而拉链法中可取α≥1,且结点较大时,拉链法中增加的指针域可忽略不计,因此节省空间;

④在用拉链法构造的散列表中,删除结点的操作易于实现。只要简单地删去链表上相应的结点即可。

拉链法的缺点指针需要额外的空间,故当结点规模较小时,开放定址法较为节省空间,而若将节省的指针空间用来扩大散列表的规模,可使装填因子变小,这又减少了开放定址法中的冲突,从而提高平均查找速度。

 

(2)HashTable —— 无序

  • 散列表,也叫哈希表,存储的内容是键值对(key-value)映射;
  • Hashtable源码所有 public 方法声明中都有 synchronized关键字,线程安全,效率低;
  • 不允许null值;(因为equlas()方法需要对象)
  • 父类是Dictionary。

(3)TreeMap —— 有序

  • 父类是SortMap接口。能够把它保存的键值对根据key排序,基于红黑树,从而保证TreeMap中所有键值对处于有序状态。

*红黑色的见解

1、每个节点非红即黑

2、根节点总是黑色的

3、如果节点是红色的,则它的子节点必须是黑色的(反之不一定)

4、每个叶子节点都是黑色的空节点(NIL节点)

5、从根节点到叶节点或空子节点的每条路径,必须包含相同数目的黑色节点(即相同的黑色高度)

 

三、List、Set、Map的值能否为null?

(1)List —— 允许为null

1、可以看到ArrayList可以存储多个null,ArrayList底层是数组,添加null并未对他的数据结构造成影响。

 public void testArrayList(){
        ArrayList<String> list = new ArrayList<>();
        list.add(null);
        list.add(null);
        Assert.assertEquals(2,list.size()); // success
    }

2、LinkedList底层为双向链表,node.value = null 也没有问题。

   public void testLinkedList(){
        LinkedList<String> list = new LinkedList<>();
        list.add(null);
        list.add(null);
        Assert.assertEquals(2,list.size()); // success
    }

3、Vector 底层是数组,所以不会管你元素的内容是什么,可以存储多个null。 

public void VectorTest(){
        Vector box = new Vector();
        box.add(null);
        box.add(null);
        Assert.assertEquals(2,box.size()); //success
    }

    //Vector的add函数源码
    public synchronized boolean add(E e) {
        modCount++;
        ensureCapacityHelper(elementCount + 1);
        elementData[elementCount++] = e;
        return true;
    }

(2)Set 

1、HashSet底层是HashMap,可以有1个为null的元素。

public void testHashSet(){
        HashSet<String> set = new HashSet<>();
        set.add(null);
        Assert.assertEquals(1,set.size()); //OK size = 1
        set.add(null);
        Assert.assertEquals(2,set.size()); //Error size = 1

    }

2、LinkHashSet底层也是hashmap,允许存在一个为null的元素。

3、TreeSet不能有key为null的元素,会报NullPointerException

 public void testTreeSet(){
        TreeSet<String> set = new TreeSet<>();
        set.add(null); //Error NullPointException
    }

(3)Map

1、HashMap中只能有一个key为null的节点。因为Map的key相同时,后面的节点会替换之前相同key的节点。

 public void testHashMap(){
        HashMap<String,String> map = new HashMap<>();
        map.put(null,null);
        Assert.assertEquals(1,map.size()); //OK size = 1

        map.put(null,null);
        Assert.assertEquals(2,map.size()); //Error size = 1
    }

2、TreeMap的put方法会调用compareTo方法,对象为null时,会报空指针错。

   public void testTreeMap(){
        TreeMap<String,String> map = new TreeMap<>();
        map.put(null,null);
        Assert.assertEquals(1,map.size()); //Error NullPointException
        
    }

3、HashTable底层为散列表,无论是key为null,还是value为null,都会报错

public void HashTableTest(){
        Hashtable table = new Hashtable();
        table.put(new Object(),null); //Exception
        table.put(null,new Object()); //Exception
        table.put(null,null); //Exception
        Assert.assertEquals(1,table.size());
    }

    //hashTable  put函数源码
   public synchronized V put(K key, V value) {
        if (value == null) {   //value 需要判空,所以value不可为null
            throw new NullPointerException();
        }
        Entry<?,?> tab[] = table;
        int hash = key.hashCode();     //key需要拥有实例去调用hashCode方法,所以也不能为空
        int index = (hash & 0x7FFFFFFF) % tab.length;
        @SuppressWarnings("unchecked")
        Entry<K,V> entry = (Entry<K,V>)tab[index];
        for(; entry != null ; entry = entry.next) {
            if ((entry.hash == hash) && entry.key.equals(key)) {
                V old = entry.value;
                entry.value = value;
                return old;
            }
        }
        addEntry(hash, key, value, index);
        return null;
    }

 

结束。


参考链接

https://blog.csdn.net/zhangqunshuai/article/details/80660974

https://www.jianshu.com/p/c32e192e371d

 

展开阅读全文

没有更多推荐了,返回首页