JavaGuide集合汇总

一 ,List Set Map三者的区别

List(注重有序):List接口存储一组不唯一(可以有多个元素引用相同的对象),有序的对象。
Set(注重独一无二):不允许重复的集合,不会有多个元素引用相同的对象。
Map(注重key-value):使用键值对进行存储。Map会维护与Key有关联的value,两个Key可以引用相同的对象,但Key不能重复,典型的Key是String类型,但也可以是任何对象。

二 , ArrayList和LinkedList的区别

  1. 是否保证线程安全:ArrayList和LinkedList都是不同步的,不保证线程安全。
  2. 底层数据结构:ArrayList底层采用Object数组,LinkedList底层采用双向链表数据结构
  3. 增删是否受元素的位置影响
    ①ArrayList采用数组存储,所以增删元素的时间复杂度受元素位置的影响。
    每当add(E e)的时候,ArrayList会默认将添加的元素追加到此列表的末尾,此时复杂度为O(1)。
    但如果在指定位置插入或删除元素的话,复杂度为(n-i),因为在进行上述操作的时候,第i个元素以及它之后的n-i个元素都要执行向后/向前移位的操作,则数组的添加删除操作时间复杂度近似为O(n)。
    ②LinkedList采用链表存储,所以增删元素的时间复杂度不受元素位置的影响,近似O(1)
  4. 是否支持快速随机访问:LinkedList不支持高效的随机元素访问,而ArrayList支持。快速随机访问就是通过元素的序号快速获取元素对象(get(int index)方法)。因为:
    ①ArrayList底层采用数组存储,当快速随机访问时只需要提供对应的下表即可获取到该下标对应位置的对象
    ②LinkedList底层采用链表存储,当快速随机访问时,需要从头/尾遍历数组(若index<双向链表长度的1/2,则从头到尾遍历,否则从尾到头遍历)。
  5. 内存空间占用:ArrayList的空间浪费主要体现在list列表结尾会预留一定的容量空间,而LinkedList的空间花费主要体现在它的每一个元素都需要消耗比ArrayList更多的空间(因为要存放直接后继next与直接前驱prev以及数据)

三 , RandomAccess接口

public interface RandomAccess {
}

查看源码我们发现实际上RandomAccess接口中什么都没有定义,所以RandomAccess接口只是一个标识接口,标识实现该接口的类具有随机访问功能。

在binarySearch()方法中,它要判断传入的list是否为RandomAccess的实例,如果是,调用indexBinarySearch(),如果不是则调用iteratorBinarySearch()

public static <T>
    int binarySearch(List<? extends Comparable<? super T>> list, T key) {
        if (list instanceof RandomAccess || list.size()<BINARYSEARCH_THRESHOLD)
            return Collections.indexedBinarySearch(list, key);
        else
            return Collections.iteratorBinarySearch(list, key);
}

为什么ArrayList实现了RandomAccess接口,而LinkedList没有实现?
ArrayList底层是数组,而LinkedList底层是链表,数组天然支持随机访问,时间复杂度为O(1),链表需要遍历到特定位置才能访问特定位置的元素,时间复杂度为O(n),所以不支持快速随机访问。

ArrayList实现了RandomAccess接口,表面它具有快速随机访问的功能,而RandomAccess只是标识。

下面再总结以下list的遍历方式选择:
实现RandomAccess接口的list,优先选择普通for循环,其次foreach。
未实现RandomAccess接口的list,优先选择iterator遍历(foreach底层也是通过iterator实现的),大size的list,千万不能采用普通for循环。

四 , 双向链表和双向循环链表

双向链表:包含两个指针,一个prev指向前一个结点,一个next指向后一个结点
在这里插入图片描述
双向循环链表:最后一个结点的next指向head,而head的prev指向最后一个结点,构成一个环
在这里插入图片描述

五 , ArrayList与Vector区别,为什么要用ArrayList取代Vector

Vector类的所有方法都是同步的,可以由两个线程安全的访问同一个Vector对象,但是Vector再同步操作上会消耗大量的时间。

ArrayList不是同步的,所以不需要保证线程安全的情况下,建议采用ArrayList

六 , HashMap的底层实现

JDK1.8之前:
HashMap底层采用数组加链表结合在一起使用,也就是链表散列。HashMap通过key的hashCode方法(经过扰动函数处理)计算出其hash值,然后通过(数组长度n - 1)&hash 判断元素存放位置,如果当前位置存在元素的话,就判断当前存在的元素与要存入的元素的hash值以及key是否相同,如果相同的话则覆盖,不相同通过拉链法解决冲突。

扰动函数指HashMap的hash方法,可以减少hash碰撞。

JDK1.8的HashMap的hash方法源码:

static final int hash(Object key) {
      int h;
      // key.hashCode():返回散列值也就是hashcode
      // ^ :按位异或
      // >>>:无符号右移,忽略符号位,空位都以0补齐
      return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
  }

对比JDK1.7的hash方法源码:

static int hash(int h) {
    // This function ensures that hashCodes that differ only by
    // constant multiples at each bit position have a bounded
    // number of collisions (approximately 8 at default load factor).

    h ^= (h >>> 20) ^ (h >>> 12);
    return h ^ (h >>> 7) ^ (h >>> 4);
}

相比起JDK1.8的hash方法,1.7的性能稍微差一些,因为其扰动了4次

拉链法

将链表和数组结合,创建一个链表数组,数组中每一个都是一个链表。若遇到了hash冲突,则将冲突的值加到链表中即可。

而JDK1.8之后在解决hash冲突又有了较大的变化,当链表的长度大于8时,将该链表自动转换为红黑树,而当其红黑树长度小于6时,又自动转换为链表

这里值得一提的是,普通HashMap中,其链表为单链表,而LinkedHashMap是将链表变为双向链表的HashMap,底层实现并没有改变。

七 , HashMap和HashTable区别

  1. 线程安全:HashMap是非线程安全的,而HashTable是线程安全的;HashTable内部的方法基本都通过synchronized关键字修饰。(如果想要保证线程安全,建议使用CurrentHashMap);

  2. 效率:因为线程安全问题,HashMap效率要比HashTable稍微高一些,此外,HashTable基本被淘汰了,不要再代码中使用它。

  3. 对Null key 还有 Null value的支持:HashMap中,null是可以作为key的,但是这样的key只能存在一个(这是因为HashMap中key不能重复);可以存在一个或多个key对应的value为null,但是在HashTable中,put(key,value)时只要key和value有其中一个为null,就抛出空指针异常。

  4. 初始容量大小和每次扩容大小的不同
    ①创建时,如果不指定容量初始值,HashTable默认容量为11,之后每次扩容都变为原本的2n+1。HashMap默认容量为16(与ArrayList相同,毕竟是数组加链表构成的),之后每次扩容,容量为之前的2倍。
    ②创建时,如果指定了容量初始值,HashTable会直接采用给定的大小;HashMap会将其扩充为2的幂次方大小。

  5. 底层数据结构:JDK1.8以后,HashMap在解决hash冲突时有了较大的变化,当链表长度大于阈值(默认为8)时,将链表转换为红黑树,以减少搜索时间。而HashTable并没有这样的机制。

HashMap中的带有初始容量的构造函数:

public HashMap(int initialCapacity, float loadFactor) {
		//若初始容量小于0 则抛出异常
        if (initialCapacity < 0)
            throw new IllegalArgumentException("Illegal initial capacity: " +
                                               initialCapacity);
        //若初始容量大于其定义的最大容量,则将最大容量作为初始容量
        if (initialCapacity > MAXIMUM_CAPACITY)
            initialCapacity = MAXIMUM_CAPACITY;
        //若装载因子小于等于0  抛出异常
        if (loadFactor <= 0 || Float.isNaN(loadFactor))
            throw new IllegalArgumentException("Illegal load factor: " +
                                               loadFactor);
        this.loadFactor = loadFactor;
        this.threshold = tableSizeFor(initialCapacity);
    }
    
     public HashMap(int initialCapacity) {
        this(initialCapacity, DEFAULT_LOAD_FACTOR);
    }

下面这个方法保证了 HashMap 总是使用2的幂作为哈希表的大小。

 /**
     * Returns a power of two size for the given target capacity.
     */
    static final int tableSizeFor(int cap) {
        int n = cap - 1;
        n |= n >>> 1;
        n |= n >>> 2;
        n |= n >>> 4;
        n |= n >>> 8;
        n |= n >>> 16;
        return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
    }

八 , 为什么HashMap的长度是2的幂次方

为了能让HashMap存取高效,尽量减少碰撞,也就是要尽量把数据在数组上分配均匀,避免资源浪费。Hash值的取值范围-2147483648到2147483647,前后加起来大概40亿的映射空间,只要hash函数映射均匀松散,一般是很难出现hash碰撞的。但一个40亿长度的数组,是不可能存放于内存的,所以这个散列值并不能拿来直接使用,用之前还要先对数组的长度取模运算,得到余数才能用作数组下标进行存放,(n-1)&hash。

例如一个初始数组长度为16的HashMap,16-1=15,换算成二进制:1111,再于某个hash值进行&操作。

		 10100101 11000100 00100101
	&    00000000 00000000 00001111
    =    0101    //高位全部归零,只保留末四位
取模运算的算法设计思想

hash%length == hash&(n-1),这里有个前提条件时n必须为2的幂次方,而HashMap中之所以采用二进制&操作,是因为相比起%,&能提高运算效率。这就是为什么HashMap的长度为什么是2的幂次方(为了二进制&运算)。

取模运算后的扰动函数调用

那么这时问题又出现了,即便hash值分布再松散,进行模运算后得到的结果都大概率会出现碰撞。这时,就需要调用HashMap的扰动函数了。当HashMap数组长度为512的时候,也就是用掩码取低9位的时候,在没有扰动函数的情况下,发生了103次碰撞,接近30%。而在使用了扰动函数之后只有92次碰撞。碰撞减少了将近10%。看来扰动函数确实还是有功效的。

九 , HashMap和HashSet的区别

HashSet底层就是基于HashMap实现的

HashMapHashSet
实现了Map接口实现了Set接口
存储键值对仅存储对象
调用put(key,value)在map中添加元素调用add(Obj obj)向Set中添加元素
采用key值计算hashCode采用成员对象计算HashCode,对于两个对象来说hashCode可能相同,所以必须通过equals方法判断对象的相等性

十 , HashSet如何检查重复

当把对象加入到HashSet时,会先计算对象的hashCode值判断对象加入的位置,同时也会与其他加入的对象的hashCode比较,如果没有相同的hashCode则假设没有出现重复,如果发现重复的hashCode时,通过调用equals方法判断加入对象与具有相同hashCode的对象内存地址是否相同,如果二者相同,则HashSet不会让其加入成功。

hashCode方法与equals方法的相关规定:

  1. 如果两个对象相同,则hashCode一定相同。
  2. 如果两个对象相同,则调用equals方法一定返回true
  3. 如果两个对象具有同一个hashCode,它们不一定相同
  4. 综上,如果equals被重写了,则hashCode也必须被重写
  5. hashCode方法默认行为是对堆上的对象产生独特值,如果不重写hashCode方法,即使两个对象指向相同的数据,该class的两个对象也不会相等。
  • 说直接点,也就是原本equals方法为true的时候是可以判断两个对象是一定指向同一个内存地址的,如果我们进行了equals重写,那么比较的也就不再是内存地址。这时,两个对象调用equals为ture的情况下,这两个对象的hashCode也不一定会相同,所以在我们重写equals方法后,也必须要对hashCode进行重写,这是因为hashCode相同时,两个对象是一定相同的。

十一 , ConcurrentHashMap和HashTable的区别

二者区别主要体现在实现线程安全的方式上的不同:

  • 底层数据结构:JDK1.7的ConcurrentHashMap底层采用分段的数组加链表,JDK1.8版本的ConcurrentHashMap底层与HashMap相同(数组+链表/红黑树)。
    而HashTable都是采用数组+链表。
  • 线程安全
    ①JDK1.7时,ConcurrentHashMap的分段锁对整个位桶数组进行了分割分段,每一把锁只锁容器其中一部分数据,在多线程环境下访问容器里的不同数据段的数据时,就不会存在锁竞争,提高了并发访问效率。
    ②JDK1.8时,ConcurrentHashMap摒弃了位桶数组分段的概念,直接采用Node数组+链表+红黑树的数据结构实现,并发控制使用synchronized和CAS操作(JDK1.6以后对synchronized锁做了很多优化)。为了兼容旧版本,在1.8版本还是能看到简化的位桶数组分段的数据结构。
    ③HashTable:使用synchronized(同一把锁)保证线程安全,效率低下。当一个线程正在访问同步方法时,其他线程也访问同步方法,就会造成阻塞或轮询状态。
    例如一个线程使用put添加元素,另一个线程不能使用put,也不能使用get,在线程竞争下效率越来越低。
    可以想象HashTable是一个房间只有一把钥匙,一个人拿着钥匙开门进去后,后面的人必须等待其出来后才能获得钥匙进房间。而ConcurrentHashMap是多个房间多把钥匙,一个结点对应一个房间。
    而HashTable效率之所以低下的根本原因就是采用了全表锁保证线程安全。

HashTable
在这里插入图片描述
JDK1.7的ConcurrentHashMap:
在这里插入图片描述
JDK1.8的ConcurrentHashMap(TreeBin:红黑树结点,Node:链表结点):
在这里插入图片描述

十二 , 集合框架底层数据结构总结

Collection接口
List:

  1. ArrayList:Object数组
  2. Vector:Object数组
  3. LinkedList:双向链表(JDK1.6之前为循环链表,在1.7时取消了循环)

Set:
4. HashSet(无序,唯一):基于HashMap实现,底层采用HashMap保存
5. LinkedHashSet:继承HashSet,且内部通过LinkedHashMap实现。
6. TreeSet(有序,唯一): 红黑树

Map接口
7. HashMap:JDK1.8以前HashMap由数组+链表构成,数组为HashMap的主体,链表则主要为了解决hash冲突而存在的。JDK1.8在解决hash冲突时,当链表长度大于阈值(默认8),将链表转换为红黑树,当红黑树长度小于6时,将红黑树转换为链表。
8. LinkedHashMap:LinkedListHashMap继承了HashMap,所以底层仍然基于拉链式散列结构,即数组和链表或红黑树组成。此外,在以上结构的基础之上增加了双向链表,使得以上的结构可以保持键值对的插入顺序。同时对链表进行相应操作,实现了访问顺序的相关逻辑。
9. HashTable:没有红黑树结构的HashMap
10. TreeMap:红黑树

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值