java集合集锦_

2023-05-16

java集合集锦

文章目录

  • java集合集锦
      • 一、java集合框架图
      • 二、集合简介
      • 三、集合遍历
      • 四、Arraylist 与 LinkedList 区别?
      • 五、ArrayList 与 Vector 区别呢?
      • 六、要对集合更新操作时,ArrayList 和 LinkedList 哪个更适合?
      • 七、conllection和conllections区别
      • 八、 vector和CopyOnWriteArrayList、Conllections.synchronizedList区别
      • 九、HashMap与HashTable的区别?
      • 十、HashMap的put方法流程?
      • 十一、HashMap 中的哈希函数 hash()
      • 十二、HashMap 中的初始化/扩容方法 resize()
      • 十三、HashMap是怎么解决哈希冲突的?
      • 十四、HashMap为什么不直接使用hashCode()处理后的哈希值直接作为table的下标?
      • 十五、HashMap在JDK1.7和JDK1.8中有哪些不同?
      • 十六、为什么HashMap中String、Integer这样的包装类适合作为K?
      • 十七、ConcurrentHashMap和Hashtable的区别?
      • 十八、Java集合的快速失败机制 “fail-fast”?
      • 十九、CopyOnWriteArraySet和HashSet

一、java集合框架图

在这里插入图片描述

如图所示:图中,实线边框的是实现类,折线边框的是抽象类,而点线边框的是接口

二、集合简介

  1. List(有序、可重复)

    List里存放的对象是有序的,同时也是可以重复的,List关注的是索引,拥有一系列和索引相关的方法,查询速度快。因为往list集合里插入或删除数据时,会伴随着后面数据的移动,所有插入删除数据速度慢。

    List 接口有三个实现类LinkedList:基于链表实现,链表内存是散乱的,每一个元素存储本身内存地址的同时还存储下一个元素的地址。链表增删快,查找慢;ArrayList:基于数组实现,非线程安全的,效率高,便于索引,但不便于插入删除;Vector:基于数组实现,线程安全的,效率低)。

  2. Set(无序、不能重复)

    Set里存放的对象是无序,不能重复的,集合中的对象不按特定的方式排序,只是简单地把对象加入集合中。

    Set 接口有两个实现类HashSet:底层是由 HashMap 实现,不允许集合中有重复的值,使用该方式时需要重写 equals()和 hashCode()方法;LinkedHashSet:继承与 HashSet,同时又基于 LinkedHashMap 来进行实现,底层使用的是 LinkedHashMap)。

  3. Map(键值对、键唯一、值不唯一)

    Map集合中存储的是键值对,键不能重复,值可以重复。根据键得到值,对map集合遍历时先得到键的set集合,对set集合进行遍历,得到相应的值。

    Map 集合HashMap: 是最常用的Map,它根据键的HashCode值存储数据,根据键可以直接获取它的值,具有很快的访问速度,遍历时,取得数据的顺序是完全随机的。因为键对象不可以重复,所以HashMap最多只允许一条记录的键为Null,允许多条记录的值为Null,是非同步的; HashTable: 与HashMap类似,是HashMap的线程安全版,它支持线程的同步,即任一时刻只有一个线程能写Hashtable,因此也导致了Hashtale在写入时会比较慢,它继承自Dictionary类,不同的是它不允许记录的键或者值为null,同时效率较低。;LinkedHashMap: 是 HashMap 的一个子类,保存了记录的插入顺序,在用Iterator遍历LinkedHashMap时,先得到的记录肯定是先插入的,在遍历的时候会比HashMap慢,有HashMap的全部特性。;TreeMap: TreeMap实现SortMap接口,能够把它保存的记录根据键排序,默认是按键值的升序排序(自然顺序),也可以指定排序的比较器,当用Iterator遍历TreeMap时,得到的记录是排过序的。不允许key值为空,非同步的;)

三、集合遍历

  1. Iterator,所有的集合类,都实现了Iterator接口,这是一个用于遍历集合中元素的接口,主要包含以下三种方法:
    1.hasNext()是否还有下一个元素。
    2.next()返回下一个元素。
    3.remove()删除当前元素。
   iterator的形式:
   Iterator it = arr.iterator();
   while(it.hasNext()){ 
       object o =it.next(); ...
   }
  1. foreach输出:JDK1.5之后提供的新功能,可以输出数组或集合。
   forint i:arr){...}
  1. for循环
   forint i=0;i<arr.size();i++{...}
  1. ListIterator:是Iterator的子接口,专门用于输出List中的内容。

四、Arraylist 与 LinkedList 区别?

  1. 都不保证线程安全。
  2. ArrayList是实现了基于动态数组的数据结构,LinkedList基于双向链表的数据结构。
  3. 对于随机访问get和set,ArrayList优于LinkedList,因为LinkedList要移动指针。
  4. LinkedList不支持高效的随机元素访问,而 ArrayList 支持。
  5. 对于新增和删除操作add和remove,LinedList比较占优势,因为ArrayList要移动数据。
  6. ArrayList的空间浪费主要体现在在list列表的结尾会预留一定的容量空间,而LinkedList的空间花费则体现在它的每一个元素都需要消耗比ArrayList更多的空间(因为要存放直接后继和直接前驱以及数据)。

五、ArrayList 与 Vector 区别呢?

  • Vector类的所有方法都是同步的。可以由两个线程安全地访问一个Vector对象、但是一个线程访问Vector的话代码要在同步操作上耗费大量的时间。
  • Arraylist不是同步的,所以在不需要保证线程安全时建议使用Arraylist。

六、要对集合更新操作时,ArrayList 和 LinkedList 哪个更适合?

当操作是在一列数据的后面添加数据而不是在前面或中间,并且需要随机地访问其中的元素时,使用ArrayList 会提供比较好的性能 ; 当你的操作是在一列数据的前面或中间添加或删除数据,并且按照顺序访问其中的元素时,就应该使用 LinkedList

七、conllection和conllections区别

Collections是个java.util下的类,它包含有各种有关集合操作的静态方法。服务于Java的Collection框架。

Collection是个java.util下的接口,它是各种集合结构的父接口。

八、 vector和CopyOnWriteArrayList、Conllections.synchronizedList区别

从JDK1.0开始,Vector便存在JDK中,Vector是一个线程安全的列表,采用数组实现。其线程安全的实现方式是对所有操作都加上了synchronized关键字,这种方式严重影响效率。

CopyOnWriteArrayList 只是在增删改上加锁,但是读不加锁,在读方面的性能就好于Vector,CopyOnWriteArrayList支持读多写少的并发情况

   /** The array, accessed only via getArray/setArray. */  
    private volatile transient Object[] array;//保证了线程的可见性  

    public void add(int index, E element) {
        final ReentrantLock lock = this.lock;
        lock.lock();
        try {
            Object[] elements = getArray();
            int len = elements.length;
            if (index > len || index < 0)
                throw new IndexOutOfBoundsException("Index: "+index+
                                                    ", Size: "+len);
            Object[] newElements;
            int numMoved = len - index;
            if (numMoved == 0)
                newElements = Arrays.copyOf(elements, len + 1);  //copy一份比当前数组长度+1的array数组
            else {
                newElements = new Object[len + 1];         //将add的参数赋值
                System.arraycopy(elements, 0, newElements, 0, index);
                System.arraycopy(elements, index, newElements, index + 1,
                                 numMoved);
            }
            newElements[index] = element;
            setArray(newElements);           //将原数组指向新的数组
        } finally {
            lock.unlock();
        }
    }

    public E remove(int index) {
        final ReentrantLock lock = this.lock;
        lock.lock();
        try {
            Object[] elements = getArray();
            int len = elements.length;
            E oldValue = get(elements, index);
            int numMoved = len - index - 1;    
            if (numMoved == 0)
                setArray(Arrays.copyOf(elements, len - 1));    //copy一份比当前数组长度-1的array数组
            else {
                Object[] newElements = new Object[len - 1];
                System.arraycopy(elements, 0, newElements, 0, index);
                System.arraycopy(elements, index + 1, newElements, index,
                                 numMoved);
                setArray(newElements);      //将原数组指向新的数组
            }
            return oldValue;
        } finally {
            lock.unlock();
        }
    }

    /**
     * 将原数组指向新的数组
     */
    final void setArray(Object[] a) {
        array = a;
    }

从以上源码我们可以看出,它在执行add方法和remove方法的时候,分别创建了一个当前数组长度+1和-1的数组,将数据copy到新数组中,然后执行修改操作。修改完之后调用setArray方法来指向新的数组。在整个过程中是使用ReentrantLock可重入锁来保证不会有多个线程同时copy一个新的数组,从而造成的混乱。并且使用volatile修饰数组来保证修改后的可见性。读写操作互不影响,所以在整个过程中整个效率是非常高的。

  • volatile 和 synchronized区别?
    volatile是一个类型修饰符,用来修饰被不同线程访问和修改的变量,当值被一个线程更改后,该值会在缓存中更新,保持一致。
    synchronized是同步锁,是LOCK的一个简化版本,性能不好,操作优势,被它声明的代码块具有操作的原则性。
  1. volatile修饰的变量,需要从主存(主内存)中读取,而不会从寄存(工作内存)中读取。synchronized是锁定当前变量,只有当前线程可以访问该变量,其他线程被阻塞住。
  2. volatile是仅能使用在变量级别,而synchronized可以使用在变量、方法中。
  3. volatile仅能实现的变量的修改可见性,synchronized可以保证变量的修改性和原子性。
  4. volatile不会造成线程阻塞,synchronized可能造成线程的阻塞。
  5. 使用volatile而不用synchronized的唯一安全情况是类中只有一个可变的域。

SynchronizedList其实现线程安全的方式是建立了list的包装类,对部分操作加上了synchronized关键字以保证线程安全。但其iterator()操作还不是线程安全的。

public void test(){
        ArrayList<String> list = new ArrayList<>();
        List<String> sycList = Collections.synchronizedList(list);
        sycList.add("a");
        sycList.remove("a");
    }
  • synchronizedList遍历为什么还需要加锁?

    synchronizedList官方文档中给出的使用方式是以下方式:

  List list = Collections.synchronizedList(new ArrayList());
        ...
    synchronized (list) {
        Iterator i = list.iterator(); // Must be in synchronized block
        while (i.hasNext())
            foo(i.next());
    }

虽然内部方法中大部分都已经加了锁,但是iterator方法却没有加锁处理。那么如果我们在遍历的时候不加锁会导致什么问题呢?
试想我们在遍历的时候,不加锁的情况下,如果此时有其他线程对此集合进行add或者remove操作,那么这个时候就会导致数据丢失或者是脏数据的问题,所以如果我们对数据的要求较高,想要避免这方面问题的话,在遍历的时候也需要加锁进行处理。

CopyOnWriteArrayListCollections.synchronizedList是实现线程安全的列表的两种方式。两种实现方式分别针对不同情况有不同的性能表现,其中CopyOnWriteArrayList的写操作性能较差,而多线程的读操作性能较好。而Collections.synchronizedList的写操作性能比CopyOnWriteArrayList在多线程操作的情况下要好很多,而读操作因为是采用了synchronized关键字的方式,其读操作性能并不如CopyOnWriteArrayList。 因此在不同的应用场景下,应该选择不同的多线程安全实现类。

Vector是增删改查方法都加了synchronized,保证同步,但是每个方法执行的时候都要去获得锁,性能就会大大下降;

CopyOnWriteArrayList,发生修改时候做copy,新老版本分离,保证读的高性能,适用于以读为主,读操作远远大于写操作的场景中使用,比如缓存;

Collections.synchronizedList则可以用在CopyOnWriteArrayList不适用,但是有需要同步列表的地方,读写操作都比较均匀的地方。

九、HashMap与HashTable的区别?

  1. HashMap没有考虑同步,是线程不安全的;Hashtable使用了synchronized关键字,是线程安全的;
  2. HashMap允许K/V都为null;后者K/V都不允许为null;
  3. HashMap继承自AbstractMap类;而Hashtable继承自Dictionary类;

十、HashMap的put方法流程?

final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
               boolean evict) {
    HashMap.Node<K,V>[] tab; HashMap.Node<K,V> p; int n, i;
    // 1.如果table为空或者长度为0,即没有元素,那么使用resize()方法扩容
    if ((tab = table) == null || (n = tab.length) == 0)
        n = (tab = resize()).length;
    // 2.计算插入存储的数组索引i,此处计算方法同 1.7 中的indexFor()方法
    // 如果数组为空,即不存在Hash冲突,则直接插入数组
    if ((p = tab[i = (n - 1) & hash]) == null)
        tab[i] = newNode(hash, key, value, null);
    // 3.插入时,如果发生Hash冲突,则依次往下判断
    else {
        HashMap.Node<K,V> e; K k;
        // a.判断table[i]的元素的key是否与需要插入的key一样,若相同则直接用新的value覆盖掉旧的value
        // 判断原则equals() - 所以需要当key的对象重写该方法
        if (p.hash == hash &&
                ((k = p.key) == key || (key != null && key.equals(k))))
            e = p;
        // b.继续判断:需要插入的数据结构是红黑树还是链表
        // 如果是红黑树,则直接在树中插入 or 更新键值对
        else if (p instanceof HashMap.TreeNode)
            e = ((HashMap.TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
        // 如果是链表,则在链表中插入 or 更新键值对
        else {
            // i .遍历table[i],判断key是否已存在:采用equals对比当前遍历结点的key与需要插入数据的key
            //    如果存在相同的,则直接覆盖
            // ii.遍历完毕后任务发现上述情况,则直接在链表尾部插入数据
            //    插入完成后判断链表长度是否 > 8:若是,则把链表转换成红黑树
            for (int binCount = 0; ; ++binCount) {
                if ((e = p.next) == null) {
                    p.next = newNode(hash, key, value, null);
                    if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
                        treeifyBin(tab, hash);
                    break;
                }
                if (e.hash == hash &&
                        ((k = e.key) == key || (key != null && key.equals(k))))
                    break;
                p = e;
            }
        }
        // 对于i 情况的后续操作:发现key已存在,直接用新value覆盖旧value&返回旧value
        if (e != null) { // existing mapping for key
            V oldValue = e.value;
            if (!onlyIfAbsent || oldValue == null)
                e.value = value;
            afterNodeAccess(e);
            return oldValue;
        }
    }
    ++modCount;
    // 插入成功后,判断实际存在的键值对数量size > 最大容量
    // 如果大于则进行扩容
    if (++size > threshold)
        resize();
    // 插入成功时会调用的方法(默认实现为空)
    afterNodeInsertion(evict);
    return null;
}

在这里插入图片描述

根据代码可以总结插入逻辑如下:

  1. 先调用 hash() 方法计算哈希值
  2. 然后调用 putVal() 方法中根据哈希值进行相关操作
  3. 如果当前 哈希表内容为空,新建一个哈希表
  4. 如果要插入的桶中没有元素,新建个节点并放进去
  5. 否则从桶中第一个元素开始查找哈希值对应位置
    1. 如果桶中第一个元素的哈希值和要添加的一样,替换,结束查找
    2. 如果第一个元素不一样,而且当前采用的还是 JDK 8 以后的树形节点,调用 putTreeVal() 进行插入
    3. 否则还是从传统的链表数组中查找、替换,结束查找
    4. 当这个桶内链表个数大于等于 8,就要调用 treeifyBin() 方法进行树形化
  6. 最后检查是否需要扩容

插入过程中涉及到几个其他关键的方法 :

  • hash():计算对应的位置
  • resize():扩容
  • putTreeVal():树形节点的插入
  • treeifyBin():树形化容器

十一、HashMap 中的哈希函数 hash()

HashMap 中通过将传入键的 hashCode 进行无符号右移 16 位,然后进行按位异或,得到这个键的哈希值。

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

由于哈希表的容量都是 2 的 N 次方,在当前,元素的 hashCode() 在很多时候下低位是相同的,这将导致冲突(碰撞),因此 1.8 以后做了个移位操作:将元素的 hashCode() 和自己右移 16 位后的结果求异或。

由于 int 只有 32 位,无符号右移 16 位相当于把高位的一半移到低位:

在这里插入图片描述

这样可以避免只靠低位数据来计算哈希时导致的冲突,计算结果由高低位结合决定,可以避免哈希值分布不均匀。
Java 位运算(移位、位与、或、异或、非)

十二、HashMap 中的初始化/扩容方法 resize()

每次添加时会比较当前元素个数和阈值:

    //如果超出阈值,就得扩容
    if (++size > threshold)
        resize();

resize():

   final Node<K,V>[] resize() {
    //复制一份当前的数据
    Node<K,V>[] oldTab = table;
    //保存旧的元素个数、阈值
    int oldCap = (oldTab == null) ? 0 : oldTab.length;
    int oldThr = threshold;
    int newCap, newThr = 0;
    if (oldCap > 0) {
        if (oldCap >= MAXIMUM_CAPACITY) {
            threshold = Integer.MAX_VALUE;
            return oldTab;
        }
        //新的容量为旧的两倍
        else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
                 oldCap >= DEFAULT_INITIAL_CAPACITY)
            //如果旧容量小于等于 16,新的阈值就是旧阈值的两倍
            newThr = oldThr << 1; // double threshold
    }
    //如果旧容量为 0 ,并且旧阈值>0,说明之前创建了哈希表但没有添加元素,初始化容量等于阈值
    else if (oldThr > 0) // initial capacity was placed in threshold
        newCap = oldThr;
    else {               // zero initial threshold signifies using defaults
        //旧容量、旧阈值都是0,说明还没创建哈希表,容量为默认容量,阈值为 容量*加载因子
        newCap = DEFAULT_INITIAL_CAPACITY;
        newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
    }
    //如果新的阈值为 0 ,就得用 新容量*加载因子 重计算一次
    if (newThr == 0) {
        float ft = (float)newCap * loadFactor;
        newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
                  (int)ft : Integer.MAX_VALUE);
    }
    //更新阈值
    threshold = newThr;
    //创建新链表数组,容量是原来的两倍
    @SuppressWarnings({"rawtypes","unchecked"})
        Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
    table = newTab;
    //接下来就得遍历复制了
    if (oldTab != null) {
        for (int j = 0; j < oldCap; ++j) {
            Node<K,V> e;
            if ((e = oldTab[j]) != null) {
                //旧的桶置为空
                oldTab[j] = null;
                //当前 桶只有一个元素,直接赋值给对应位置
                if (e.next == null)
                    newTab[e.hash & (newCap - 1)] = e;
                else if (e instanceof TreeNode)
                    //如果旧哈希表中这个位置的桶是树形结构,就要把新哈希表里当前桶也变成树形结构
                    ((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
                else { //保留旧哈希表桶中链表的顺序
                    Node<K,V> loHead = null, loTail = null;
                    Node<K,V> hiHead = null, hiTail = null;
                    Node<K,V> next;
                    //do-while 循环赋值给新哈希表
                    do {
                        next = e.next;
                        if ((e.hash & oldCap) == 0) {
                            if (loTail == null)
                                loHead = e;
                            else
                                loTail.next = e;
                            loTail = e;
                        }
                        else {
                            if (hiTail == null)
                                hiHead = e;
                            else
                                hiTail.next = e;
                            hiTail = e;
                        }
                    } while ((e = next) != null);
                    if (loTail != null) {
                        loTail.next = null;
                        newTab[j] = loHead;
                    }
                    if (hiTail != null) {
                        hiTail.next = null;
                        newTab[j + oldCap] = hiHead;
                    }
                }
            }
        }
    }
    return newTab;
}

扩容过程中几个关键的点:

  • 新初始化哈希表时,容量为默认容量16,阈值为 容量*加载因子

  • 已有哈希表扩容时,容量、阈值均翻倍

  • 如果之前这个桶的节点类型是树,需要把新哈希表里当前桶也变成树形结构

  • 复制给新哈希表中需要重新索引(rehash),这里采用的计算方法是

    • e.hash & (newCap - 1),等价于 e.hash % newCap

结合扩容源码可以发现扩容的确开销很大,需要迭代所有的元素,rehash、赋值,还得保留原来的数据结构。

所以在使用的时候,最好在初始化的时候就指定好 HashMap 的长度,尽量避免频繁 resize()。

十三、HashMap是怎么解决哈希冲突的?

简单总结一下HashMap是使用了哪些方法来有效解决哈希冲突的:

  1. 使用链地址法(使用散列表)来链接拥有相同hash值的数据;
  2. 使用2次扰动函数(hash函数)来降低哈希冲突的概率,使得数据分布更平均;
  3. 引入红黑树进一步降低遍历的时间复杂度,使得遍历更快;
  • 什么是哈希?

    Hash,一般翻译为“散列”,也有直接音译为“哈希”的,这就是把任意长度的输入通过散列算法,变换成固定长度的输出,该输出就是散列值(哈希值);这种转换是一种压缩映射,也就是,散列值的空间通常远小于输入的空间,不同的输入可能会散列成相同的输出,所以不可能从散列值来唯一的确定输入值。简单的说就是一种将任意长度的消息压缩到某一固定长度的消息摘要的函数。

    所有散列函数都有如下一个基本特性:根据同一散列函数计算出的散列值如果不同,那么输入值肯定也不同。但是,根据同一散列函数计算出的散列值如果相同,输入值不一定相同。

  • 什么是哈希冲突?

    当两个不同的输入值,根据同一散列函数计算出相同的散列值的现象,我们就把它叫做碰撞(哈希碰撞)。

  • HashMap的数据结构

    在Java中,保存数据有两种比较简单的数据结构:数组和链表。数组的特点是:寻址容易,插入和删除困难;链表的特点是:寻址困难,但插入和删除容易;所以我们将数组和链表结合在一起,发挥两者各自的优势,使用一种叫做链地址法的方式可以解决哈希冲突:
    在这里插入图片描述
    这样我们就可以将拥有相同哈希值的对象组织成一个链表放在hash值所对应的bucket下,但相比于hashCode返回的int类型,我们HashMap初始的容量大小DEFAULT_INITIAL_CAPACITY = 1 << 4(即2的四次方16)要远小于int类型的范围,所以我们如果只是单纯的用hashCode取余来获取对应的bucket这将会大大增加哈希碰撞的概率,并且最坏情况下还会将HashMap变成一个单链表, 所以我们还需要对hashCode作一定的优化。

  • 上面提到的问题,主要是因为如果使用hashCode取余,那么相当于参与运算的只有hashCode的低位,高位是没有起到任何作用的,所以我们的思路就是让hashCode取值出的高位也参与运算,进一步降低hash碰撞的概率,使得数据分布更平均,我们把这样的操作称为扰动,在JDK 1.8中HashMap()实现了自己的hash()函数。

十四、HashMap为什么不直接使用hashCode()处理后的哈希值直接作为table的下标?

hashCode()方法返回的是int整数类型,其范围为-(2 ^ 31)~(2 ^ 31 - 1),约有40亿个映射空间,而HashMap的容量范围是在16(初始化默认值)~2 ^ 30,HashMap通常情况下是取不到最大值的,并且设备上也难以提供这么多的存储空间,从而导致通过hashCode()计算出的哈希值可能不在数组大小范围内,进而无法匹配存储位置;

那怎么解决呢?

  1. HashMap自己实现了自己的hash()方法,通过两次扰动使得它自己的哈希值高低位自行进行异或运算,降低哈希碰撞概率也使得数据分布更平均;
  2. 在保证数组长度为2的幂次方的时候,使用hash()运算之后的值与运算(&)(数组长度 - 1)来获取数组下标的方式进行存储,这样一来是比取余操作更加有效率,二来也是因为只有当数组长度为2的幂次方时,h&(length-1)才等价于h%length,三来解决了“哈希值与数组大小范围不匹配”的问题;

为什么数组长度要保证为2的幂次方呢?

  1. 只有当数组长度为2的幂次方时,h&(length-1)才等价于h%length,即实现了key的定位,2的幂次方也可以减少冲突次数,提高HashMap的查询效率;
  2. 如果 length 为 2 的次幂 则 length-1 转化为二进制必定是 11111……的形式,在于 h 的二进制与操作效率会非常的快,而且空间不浪费;如果 length 不是 2 的次幂,比如 length 为 15,则 length - 1 为 14,对应的二进制为 1110,在于 h 与操作,最后一位都为 0 ,而 0001,0011,0101,1001,1011,0111,1101 这几个位置永远都不能存放元素了,空间浪费相当大,更糟的是这种情况中,数组可以使用的位置比数组长度小了很多,这意味着进一步增加了碰撞的几率,减慢了查询的效率!这样就会造成空间的浪费。

那为什么是两次扰动呢?

这样就是加大哈希值低位的随机性,使得分布更均匀,从而提高对应数组存储下标位置的随机性&均匀性,最终减少Hash冲突,两次就够了,已经达到了高位低位同时参与运算的目的;

十五、HashMap在JDK1.7和JDK1.8中有哪些不同?

不同JDK 1.7JDK 1.8
存储结构数组 + 链表数组 + 链表 + 红黑树
初始化方式单独函数:inflateTable()直接集成到了扩容函数resize()
hash值计算方式扰动处理 = 9次扰动 = 4次位运算 + 5次异或运算扰动处理 = 2次扰动 = 1次位运算 + 1次异或运算
存放数据的规则无冲突时,存放数组;冲突时,存放链表无冲突时,存放数组;冲突 & 链表长度 < 8:存放单链表;冲突 & 链表长度 > 8:树化并存放红黑树
插入数据方式头插法(先讲原位置的数据移到后1位,再插入数据到该位置)尾插法(直接插入到链表尾部/红黑树)
扩容后存储位置的计算方式全部按照原来方法进行计算(即hashCode ->> 扰动函数 ->> (h&length-1))按照扩容后的规律计算(即扩容后的位置=原位置 or 原位置 + 旧容量)

十六、为什么HashMap中String、Integer这样的包装类适合作为K?

String、Integer等包装类的特性能够保证Hash值的不可更改性和计算准确性,能够有效的减少Hash碰撞的几率

  1. 都是final类型,即不可变性,保证key的不可更改性,不会存在获取hash值不同的情况
  2. 内部已重写了equals()hashCode()等方法,遵守了HashMap内部的规范(不清楚可以去上面看看putValue的过程),不容易出现Hash值计算错误的情况;

如果我想要让自己的Object作为K应该怎么办呢?

重写hashCode()equals()方法

  1. 重写hashCode()是因为需要计算存储数据的存储位置,需要注意不要试图从散列码计算中排除掉一个对象的关键部分来提高性能,这样虽然能更快但可能会导致更多的Hash碰撞;
  2. 重写equals()方法,需要遵守自反性、对称性、传递性、一致性以及对于任何非null的引用值x,x.equals(null)必须返回false的这几个特性,目的是为了保证key在哈希表中的唯一性

十七、ConcurrentHashMap和Hashtable的区别?

ConcurrentHashMap 结合了 HashMap 和 HashTable 二者的优势。HashMap 没有考虑同步,HashTable 考虑了同步的问题。但是 HashTable 在每次同步执行时都要锁住整个结构。 ConcurrentHashMap 锁的方式是稍微细粒度的。
在这里插入图片描述

JDK1.8的ConcurrentHashMap(TreeBin: 红黑二叉树节点 Node: 链表节点):

在这里插入图片描述
ConcurrentHashMap取消了Segment分段锁,采用CAS和synchronized来保证并发安全。数据结构跟HashMap1.8的结构类似,数组+链表/红黑二叉树。Java 8在链表长度超过一定阈值(8)时将链表(寻址时间复杂度为O(N))转换为红黑树(寻址时间复杂度为O(log(N)))

插入元素过程(建议去看看源码):

  1. 如果相应位置的Node还没有初始化,则调用CAS插入相应的数据;
  2. 如果相应位置的Node不为空,且当前该节点不处于移动状态,则对该节点加synchronized锁,如果该节点的hash不小于0,则遍历链表更新节点或插入新节点;
  3. 如果该节点是TreeBin类型的节点,说明是红黑树结构,则通过putTreeVal方法往红黑树中插入节点;如果binCount不为0,说明put操作对数据产生了影响,如果当前链表的个数达到8个,则通过treeifyBin方法转化为红黑树,如果oldVal不为空,说明是一次更新操作,没有对元素个数产生影响,则直接返回旧值;
  4. 如果插入的是一个新节点,则执行addCount()方法尝试更新元素个数baseCount;

synchronized只锁定当前链表或红黑二叉树的首节点,这样只要hash不冲突,就不会产生并发,效率又提升N倍。

十八、Java集合的快速失败机制 “fail-fast”?

是java集合的一种错误检测机制,当多个线程对集合进行结构上的改变的操作时,有可能会产生 fail-fast 机制。

例如:假设存在两个线程(线程1、线程2),线程1通过Iterator在遍历集合A中的元素,在某个时候线程2修改了集合A的结构(是结构上面的修改,而不是简单的修改集合元素的内容),那么这个时候程序就会抛出 ConcurrentModificationException 异常,从而产生fail-fast机制。

原因:迭代器在遍历时直接访问集合中的内容,并且在遍历过程中使用一个 modCount 变量。集合在被遍历期间如果内容发生变化,就会改变modCount的值。每当迭代器使用hashNext()/next()遍历下一个元素之前,都会检测modCount变量是否为expectedmodCount值,是的话就返回遍历;否则抛出异常,终止遍历。

解决办法:

1. 在遍历过程中,所有涉及到改变modCount值得地方全部加上synchronized。

2. 使用CopyOnWriteArrayList来替换ArrayList

十九、CopyOnWriteArraySet和HashSet

  • HashSet是如何保证元素的不重复和无序

    因为HashSet的底层存储结构是HashMap,并且HashSet中的元素是作为Map的Key存储到Map中,所以HashMap中Key是不重复且无序,所以HashSet中的元素也就是不重复和无序的

  • HashSet的增删(改查?)原理

    HashSet的增删原理很简单,就是map的put和remove,为什么没有改查呢?那是因为HashSet中的元素是无序的,没办法根据索引进行查询和修改

  • CopyOnWriteArraySet支持并发的原理

    CopyOnWriteArraySet之所以叫CopyOnWriteArraySet,是因为它的底层存储结构是CopyOnWriteArrayList,同时也就是保证了它的并发安全性

  • CopyOnWriteArraySet的增删(改查?)原理

    CopyOnWriteArraySet继承了AbstractSet,跟HashSet一样只有增删,没有改查,增删原理也就是调用CopyOnWriteArrayList的增删方法,只不过增的时候需要判断一下List中是否存储该元素

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

java集合集锦_ 的相关文章

  • JPA 实体中的方法是否允许抛出异常?

    我尝试创建的 Entity 有问题 当尝试使用 OpenJPA 实现在 Eclipse 中测试类时出现问题 我有not尝试过其他人 所以不确定它是否适用于他们 我的测试用例非常简单 因为它创建一个 EntityManagerFactory
  • Android 上的 setTimeOut() 相当于什么?

    我需要等效的代码setTimeOut call function milliseconds 对于安卓 setTimeOut call function milliseconds 您可能想查看定时任务 http developer andro
  • 如何通过keytool命令删除已经导入的证书/别名?

    我正在尝试通过 keytool 命令删除已导入的证书 keytool delete noprompt alias initcert keystore keycloak jks 但低于异常 keytool 错误 java lang Excep
  • 用户“root”@“localhost”的访问被拒绝

    我正在尝试从数据库中获取记录 但我面临这个访问被拒绝的问题 我尝试了 Stack Overflow 上提到的其他解决方案 例如向用户授予权限 但没有任何效果 访问数据库的代码 public void service HttpServletR
  • 如何在Spring Security SAML示例中配置IDP元数据和SP元数据?

    我想处理 Spring Security SAML 为此 我开始探索Spring安全SAML http docs spring io spring security saml docs 1 0 x reference html chapte
  • 带有 spring-kafka 的 Kafka 死信队列 (DLQ)

    最好的实施方式是什么死信队列 DLQ Spring Boot 2 0 应用程序中的概念 使用 spring kafka 2 1 x 来处理无法处理的所有消息 KafkaListener某些bean发送到某些预定义的Kafka DLQ主题的方
  • 如何在 Android 中将 EditText 绘制到画布上?

    我想画画 EditText username new EditText context 到我画布上的特定位置 protected void onDraw Canvas canvas 是否可以在基础上画出x y在我的 Java 文件中协调而不
  • 按位非运算符

    为什么要按位运算 0 打印 1 在二进制中 不是0应该是1 为什么 你实际上很接近 在二进制中 不是0应该是1 是的 当我们谈论一位时 这是绝对正确的 然而 一个int其值为0的实际上是32位全零 将所有 32 个 0 反转为 32 个 1
  • 如何对JConsole的密码文件的密码进行加密

    我正在使用 JConsole 访问我的应用程序 MBean 并使用 password properties 文件 但根据 Sun 的规范 该文件仅包含明文格式的密码 com sun management jmxremote password
  • ObservableList 不更新 ArrayList

    对于学校作业 我们正在使用 JavaFX 中的 ObservableList 对象 对吗 我已经为此工作了一天多了 但无法弄清楚 老师只告诉我们 谷歌一下 所以这也没有帮助 基本上 我们正在开发一个基本的管理应用程序来跟踪人们及其家人 人们
  • 在java中设置Process对象的安全性

    有人可以告诉我如何限制通过进程对象访问系统属性吗 如果我通过进程对象运行以下代码 我可以抛出安全异常吗 System getProperty user home 请告诉我如何为流程对象配置证券 在ProcessBuilder类文档中 环境方
  • 使用 Java 进行 AES 加密并使用 Javascript 进行解密

    我正在制作一个需要基于 Java 的 AES 加密和基于 JavaScript 的解密的应用程序 我使用以下代码作为基本形式进行加密 public class AESencrp private static final String ALG
  • Android:如何以编程方式仅圆化位图的顶角?

    我目前正在使用这段代码 Override public Bitmap transform Bitmap source Bitmap result Bitmap createBitmap source getWidth source getH
  • 如何实现再次播放功能?

    我希望在游戏结束时得到提示 如果我还想再玩一次的话 并使用 Y N 输入 退出游戏或重复游戏 我该如何以最有效的方式解决这个问题 编辑 描述资源路径位置类型 类型 Main Main java ScaredyCat src se grupp
  • 仅在java中使用数组计算50的阶乘

    我是java的初学者 我有一个作业要编写一个完整的程序 使用数组计算 50 的阶乘 我无法使用像 biginteger 这样的任何方法 我只能使用数组 因为我的教授希望我们理解背后的逻辑 我猜 然而 他并没有真正教我们数组的细节 所以我在这
  • Spring Boot 健康执行器 - 什么时候上线?

    我找不到任何有关 Springs Health Actuator 何时返回 UP 状态的文档 你能依靠一切吗 Components正在初始化 会不会 Controller准备好满足请求了吗 为了测试应用程序上下文是否已加载 您可以执行此自定
  • java.lang.NoClassDefFoundError:com.google.ads.AdView

    我正在尝试将 admob 广告合并到我的应用程序中 到目前为止我已经添加了以下代码 在我的应用程序主要活动的 onCreate 方法中 adView new AdView this AdSize BANNER my code number
  • Spring Data JPA 和 Exists 查询

    我正在使用 Spring Data JPA 使用 Hibernate 作为我的 JPA 提供程序 并想要定义一个exists附加 HQL 查询的方法 public interface MyEntityRepository extends C
  • SAXParseException:找不到元素“定义”的声明

    我对 camunda 和 DMN 完全陌生 我试图在 spring boot 中运行 DMN 示例 链接在这里 https github com camunda camunda bpm examples tree master dmn en
  • PSQLException:错误:关系“TABLE_NAME”不存在

    我正在尝试在 PostgreSQL 8 4 2 DB 上运行休眠 每当我尝试运行简单的java代码时 例如 List

随机推荐

  • Centos的repos文件中的$releasever和$basearch的取值

    查看CentOS Base repo部分内容 xff0c 文件路径 etc yum repos d CentOS Base repo base baseurl 61 http mirror centos org centos release
  • 如何用Redis实现分布式锁

    为什么需要分布式锁 在聊分布式锁之前 xff0c 有必要先解释一下 xff0c 为什么需要分布式锁 与分布式锁相对就的是单机锁 xff0c 我们在写多线程程序时 xff0c 避免同时操作一个共享变量产生数据问题 xff0c 通常会使用一把锁
  • AQS与Synchronized异曲同工的加锁流程

    在并发多线程的情况下 xff0c 为了保证数据安全性 xff0c 一般我们会对数据进行加锁 xff0c 通常使用Synchronized或者ReentrantLock同步锁 Synchronized是基于JVM实现 xff0c 而Reent
  • 基于贫血模型的MVC开发模式VS领域建模开发模式

    大部分工程师都是做业务开发的 xff0c 很多业务系统都是基于MVC三层架构来开发的 xff0c 确切点说 xff0c 这是一种基于贫血模型的MVC三层架构开发模式 这种开发模式已经成为标准的Web项目开发模式 xff0c 但它却违反了面向
  • 从Servlet、Servlet容器及Web容器的演进历程总结框架设计的套路

    Web架构演进历程 早期的Web技术主要用于浏览静态页面 xff0c 首先浏览器发起HTTP请求 xff0c 请求一些静态资源 xff0c 这时候需要一个服务器来处理HTTP请求 xff0c 并且将相应的静态资源返回 这个服务器叫HTTP服
  • 信号量与生产者消费者问题

    生产者 消费者问题 生产者 消费者题型在各类考试 xff08 考研 程序员证书 程序员面试笔试 期末考试 xff09 很常见 xff0c 原因之一是生产者 消费者题型在实际的并发程序 xff08 多进程 多线程 xff09 设计中很常见 x
  • IOC的实现原理—反射与工厂模式

    反射机制概念 我们考虑一个场景 xff0c 如果我们在程序运行时 xff0c 一个对象想要检视自己所拥有的成员属性 xff0c 该如何操作 xff1f 再考虑另一个场景 xff0c 如果我们想要在运行期获得某个类的Class信息如它的属性
  • Spring中bean的作用域与生命周期

    在Spring中 xff0c 那些组成应用程序的主体及由Spring IoC容器所管理的对象 xff0c 被称之为bean 简单地讲 xff0c bean就是由IoC容器初始化 装配及管理的对象 xff0c 除此之外 xff0c bean就
  • 前后端分离架构概述

    1 背景 前后端分离已成为互联网项目开发的业界标准使用方式 xff0c 通过nginx 43 tomcat的方式 xff08 也可以中间加一个nodejs xff09 有效的进行解耦 xff0c 并且前后端分离会为以后的大型分布式架构 弹性
  • 数据库的事务隔离级别总结

    学习数据库的时候常常会接触到事务 ACID等概念 xff0c 那么到底什么是数据库的事务 xff0c 数据库事务又具有哪些特点 xff0c 和ACID有怎样的关系 xff0c 事务的隔离级别又是做什么的呢 xff1f 事务及其四大特性 xf
  • ubuntu18 遇到libc6 版本冲突问题

    问题描述 xff1a You might want to run 39 apt fix broken install 39 to correct these The following packages have unmet depende
  • Linux 系统裁剪--制作一个最小化的Linux iso镜像

    1 前言 一直以来都想制作一个最小化的Linux系统 xff0c 这个小系统需要有常用的Linux 命令 xff0c 以及定制化的某些功能 可是由于种种原因一直没能实现 xff0c 最近终于有时间把它做了出来 本文所说的精简的Linux系统
  • git为单独的仓库设置提交的用户名

    在我们平时的学习中可能有这么一种需求 xff0c 在公司进行开发的时候 xff0c 一般会参与多个项目的开发 xff0c 而项目提交代码时 xff0c 一般请求情况下提供的用户都是同一个 xff0c 而我们为了方便可能会使用全局进行git
  • Ubuntu18.04安装Xfce桌面与VNC远程工具

    1 Xfce桌面的安装 Xfce是一款轻量级的桌面环境 xff0c 运行在类Unix操作系统 xff08 如Linux FreeBSD 和 Solaris xff09 上 xff0c 界面清爽美观且对用户友好 在安装Xfce前需要更新一下系
  • Nginx的临时文件权限问题

    问题 在重启了nginx发现部分较大的Post请求出现了500错误 xff0c 然后 xff0c 查看Nginx错误日志 xff0c 类似如下 xff1a nginx open 34 usr local nginx client body
  • Spring Security中启用CSRF防护(REST版)

    问题 之前文章 Spring Security 43 Spring Session Redis 43 JJWT 介绍了怎么使用Spring Security实现restful风格的登录api 现在 xff0c 我们在这个基础上面实现csrf
  • Android Editable

    简单记一下 xff0c 以前没怎么用过 EditText View的getText直接返回的就是 Editable 而 TextView则是getEditableText才返回 Editable 还有就是注释所说 Replaces the
  • Linux虚拟机断电后开机出现:Entering emeryency mode. Exit the shell to continue.

    在一台服务器上 xff0c 搭建了4个linux虚拟机 可是昨晚不知道怎么的 xff0c 公司断电 今早来的时候发现服务器关机了 然后开机 xff0c 启动虚拟机 xff0c 其中有一台启动不了 xff0c 提示信息如下 xff1a 解决方
  • Web标准和常用浏览器及其内核

    Web标准 Web标准是由W3C组织和其他标准化组织制定的一 系列标准的集合 W3C 万维网联盟 是国际最著名的标准化组织 Web标准的优点 xff1a 1 遵循Web标准除了可以让不同的开发人员写出的页面更标准 更统一 2 让Web的发展
  • java集合集锦_

    java集合集锦 文章目录 java集合集锦一 java集合框架图二 集合简介三 集合遍历四 Arraylist 与 LinkedList 区别 五 ArrayList 与 Vector 区别呢 六 要对集合更新操作时 xff0c Arra