Java 集合知识快速记忆

这次咱们换个总结的方式,以提问的形式聊聊这个大知识点。
在这里插入图片描述

基础部分

1、List、Set集合类特点?

List,Set都是继承自Collection接口,Map则不是。

List特点:

  1. 元素有放入顺序,元素可重复 。
  2. List支持for循环,也就是通过下标来遍历,也可以用迭代器。
  3. List和数组类似,List可以动态增长,查找元素效率高,插入删除元素效率低,因为会引起其他元素位置改变。

Set特点:

  1. 元素无放入顺序,元素不可重复,重复元素会覆盖掉,注意:元素虽然无放入顺序,但是元素在set中的位置是有该元素的HashCode决定的,其位置其实是固定的,加入Set 的Object必须定义equals()方法 。
  2. Set只能用迭代,因为他无序,无法用下标来取得想要的值。
  3. 检索元素效率低下,删除和插入效率高,插入和删除不会引起元素位置改变。

2、ArrayList、LinkedList的区别和适用场景?

1)ArrayList

  • 优点:ArrayList是实现了基于动态数组的数据结构,因地址连续,一旦数据存储好了,查询操作效率会比较高(在内存里是连着放的)。
  • 缺点:因为地址连续,ArrayList要移动数据,所以插入和删除操作效率比较低。

2)LinkedList

  • 优点:LinkedList基于链表的数据结构,地址是任意的,其在开辟内存空间的时候不需要等一个连续的地址,对新增和删除操作add和remove,LinedList比较占优势。LinkedList 适用于要头尾操作或插入指定位置的场景。
  • 缺点:因为LinkedList要移动指针,所以查询操作性能比较低.

3、ArrayList与Vector区别?

ArrayList和Vector都是用数组实现的,主要有这么四个区别

  • Vector是线程安全的,线程安全就是说多线程访问代码,不会产生不确定的结果。而ArrayList不是,这可以从源码中看出,Vector类中的方法很多有synchronied进行修饰,这样就导致了Vector在效率上无法与ArrayList相比。
  • 两个都是采用的线性连续空间存储元素,但是当空间充足的时候,两个类的增加方式是不同。
  • Vector可以设置增长因子,而ArrayList不可以。
  • Vector是一种老的动态数组,是线程同步的,效率很低,一般不赞成使用。

4、ArrayList与LinkedList支持动态扩容吗?

  • ArrayList 初始化大小是 10 (如果你知道你的ArrayList 会达到多少容量,可以在初始化的时候就指定,能节省扩容的性能开支) 扩容点规则是,新增的时候发现容量不够用了就去扩容,规则是扩容后的大小= 原始大小+原始大小/2 + 1。(例如:原始大小是 10 ,扩容后的大小就是 10 + 5+1 = 16)。
  • LinkedList 是一个双向链表,没有初始化大小,也没有扩容的机制,就是一直在前面或者后面新增就好。

5、HashSet与TreeSet的适用场景?

  • TreeSet 是二叉树(红黑树的树据结构)实现的,TreeSet中的数据是自动排好序的,不允许放入null值 。
  • HashSet 是哈希表实现的,HashSet中的数据是无序的,可以放入null,但只能放入一个null,两者中的值都不能重复,就如数据库中唯一约束 。
  • HashSet 要求放入的对象必须实现HashCode()方法,放入的对象是以HashCode码作为标识的,而具有相同内容的String对象,hashcode是一样,所以放入的内容不能重复。但是同一个类的对象可以放入不同的实例

适用场景分析 : HashSet是基于Hash算法实现的,其性能通常都优于TreeSet。为快速查找而设计的Set,我们通常都应该使用HashSet,在我们需要排序的功能时,我们才使用TreeSet

6、HashMap与TreeMap、HashTable的区别及适用场景?

  • HashMap :非线程安全,基于哈希表实现。使用HashMap要求添加的键类明确定义了hashCode()和equals()[可以重写hashCode()和equals()],为了优化HashMap空间的使用,可以调优初始容量和负载因子。 其中散列表的冲突处理主分两种,一种是开放定址法,另一种是链表法。HashMap实现中采用的是链表法
  • TreeMap:非线程安全,基于红黑树实现。TreeMap没有调优选项,因为该树总处于平衡状态。
  • HashTable:线程安全(方法是Synchronized 修饰的),基于哈希表实现,不允许空键

7、Set集合从原理上如何保证不重复 ?

  • 在往set中添加元素时,如果指定元素不存在,则添加成功。
  • 具体来讲:当向HashSet中添加元素的时候,首先计算元素的hashcode值,然后用这个(元素的hashcode)%(HashMap集合的大小)+1计算出这个元素的存储位置,如果这个位置为空,就将元素添加进去;如果不为空,则用equals方法比较元素是否相等,相等就不添加,否则找一个空位添加。

中级部分

1、HashMap何时扩容?

当向容器添加元素的时候,会判断当前容器的元素个数,如果大于等于阈值—即大于当前数组的长度乘以加载因子的值的时候,就要自动扩容。

2、HashMap如何解决散列碰撞?

  • Java中HashMap是利用“拉链法”处理hashcode的碰撞问题。在调用HashMap的put方法或get方法时,都会首先调用hashcode方法,去查找相关的key,当有冲突时,再调用equals方法。
  • HashMap基于hasing原理,我们通过put和get方法存取对象。当我们将键值对传递给put方法时,他调用键对象的hashCode()方法来计算hashCode,然后找到bucket(哈希桶)位置来存储对象。当获取对象时,通过键对象的equals()方法找到正确的键值对,然后返回值对象。
  • 当碰撞发生了,对象将会存储在链表的下一个节点中。HashMap在每个链表节点存储键值对对象。当两个不同的键却有相同的hashCode时,他们会存储在同一个bucket位置的链表中。键对象的equals()来找到键值对。

3、HashMap底层为什么是线程不安全的?

  • 并发场景下使用时容易出现死循环,在 HashMap 扩容的时候会调用 resize() 方法,就是这里的并发操作容易在一个桶上形成环形链表;这样当获取一个不存在的 key 时,计算出的 index 正好是环形链表的下标就会出现死循环;
  • 在 1.7 中 hash 冲突采用的头插法形成的链表,在并发条件下会形成循环链表,一旦有查询落到了这个链表上,当获取不到值时就会死循环。

4、ArrayMap跟SparseArray在HashMap上面的改进有哪些?

SparseArray

  • SparseArray比HashMap更省内存。主要是因为它避免了对key的自动装箱(int转为Integer类型),它内部则是通过两个数组来进行数据存储的,一个存储key,另外一个存储value,为了优化性能,它内部对数据还采取了压缩的方式来表示稀疏数组的数据,从而节约内存空间。
  • SparseArray在存储和读取数据时候,使用的是二分查找法。也就是在put添加数据的时候,会使用二分查找法和之前的key比较当前我们添加的元素的key的大小,然后按照从小到大的顺序排列好。所以,SparseArray存储的元素都是按元素的key值从小到大排列好的

ArrayMap

  • ArrayMap采用的是两个一维数组,一个用来存储key的hashcode,其下标代表添加元素的起始下标;一个用来存储添加元素的key和value。它属于哈希表。默认容量为0。
  • ArrayMap没有最大容量的限制。
  • ArrayMap默认每次扩容时原来容量一半的增量。
  • ArrayMap是非线程安全的类,大量方法中通过对mSize判断是否发生并发,来决定抛出异常。

总结:

  • 数据结构:ArrayMap和SparseArray采用的都是两个数组,Android专门针对内存优化而设计。HashMap采用的是数据+链表+红黑树。
  • 内存优化:ArrayMap比HashMap更节省内存,综合性能方面在数据量不大的情况下,推荐使用ArrayMap。HashMap需要创建一个额外对象来保存每一个放入map的entry,且容量的利用率比ArrayMap低,整体更消耗内存。SparseArray比ArrayMap节省1/3的内存,但SparseArray只能用于key为int类型的Map,所以int类型的Map数据推荐使用SparseArray。
  • 性能优化:ArrayMap查找时间复杂度O(logN);ArrayMap增加、删除操作需要移动成员,速度相比较慢,对于个数小于1000的情况下,性能基本没有明显差异。HashMap查找、修改的时间复杂度为O(1)。SparseArray适合频繁删除和插入来回执行的场景,性能比较好。
  • 缓存机制:ArrayMap针对容量为4和8的对象进行缓存,可避免频繁创建对象而分配内存与GC操作,这两个缓存池大小的上限为10个,防止缓存池无限增大。HashMap没有缓存机制。SparseArray有延迟回收机制,提供删除效率,同时减少数组成员来回拷贝的次数。
  • 扩容机制:ArrayMap是在容量满的时机触发容量扩大至原来的1.5倍,在容量不足1/3时触发内存收缩至原来的0.5倍,更节省的内存扩容机制。HashMap是在容量的0.75倍时触发容量扩大至原来的2倍,且没有内存收缩机制。HashMap扩容过程有hash重建,相对耗时。所以能大致知道数据量,可指定创建指定容量的对象,能减少性能浪费。
  • 并发问题:ArrayMap是非线程安全的类,大量方法中通过对mSize判断是否发生并发,来决定抛出异常。HashMap是在每次增加、删除、清空操作的过程将modCount加1,在关键方法内进入时记录当前mCount,执行完核心逻辑后,再检测mCount是否被其他线程修改,来决定抛出异常。这一点的处理比ArrayMap更有全面。

高级部分

1、HashMap的原理?

1)HashMap 1.7原理
HashMap 底层是基于数组 + 链表组成的,不过在 jdk1.7 和 1.8 中具体实现稍有不同。

负载因子

  • 给定的默认容量为 16,负载因子为 0.75。Map 在使用过程中不断的往里面存放数据,当数量达到了 16 * 0.75 = 12 就需要将当前 16 的容量进行扩容,而扩容这个过程涉及到 rehash、复制数据等操作,所以非常消耗性能。
  • 建议能提前预估 HashMap 的大小最好,尽量的减少扩容带来的性能损耗。其实真正存放数据的是 Entry<K,V>[] table,Entry 是 HashMap 中的一个静态内部类,它有key、value、next、hash(key的hashcode)成员变量。

Put 方法:

  • 1、判断当前数组是否需要初始化。如果 key 为空,则 put 一个空值进去。
  • 2、根据 key 计算出 hashcode。
  • 3、根据计算出的 hashcode 定位出所在桶。
  • 4、如果桶是一个链表则需要遍历判断里面的 hashcode、key 是否和传入 key 相等,如果相等则进行覆盖,并返回原来的值。
  • 5、如果桶是空的,说明当前位置没有数据存入,新增一个 Entry 对象写入当前位置。(当调用 addEntry 写入 Entry 时需要判断是否需要扩容。如果需要就进行两倍扩充,并将当前的 key 重新 hash 并定位。而在 createEntry 中会将当前位置的桶传入到新建的桶中,如果当前桶有值就会在位置形成链表。)

Get 方法:

  • 1、首先也是根据 key 计算出 hashcode,然后定位到具体的桶中。
  • 2、判断该位置是否为链表。不是链表就根据key 的 hashcode 是否相等来返回值,是链表则需要遍历直到 key 及 hashcode 相等时候就返回值。
  • 3、啥都没取到就直接返回 null 。

2)HashMap 1.8原理:
当 Hash 冲突严重时,在桶上形成的链表会变的越来越长,这样在查询时的效率就会越来越低;时间复杂度为 O(N),因此 1.8 中重点优化了这个查询效率。当链表长度大于一定阈值时(当同一个Hash值的节点数大于8时),链表转换为红黑树,这样减少链表查询时间。HashEntry 修改为 Node。

put 方法:

  • 1、判断当前桶是否为空,空的就需要初始化(在resize方法 中会判断是否进行初始化)。
  • 2、根据当前 key 的 hashcode 定位到具体的桶中并判断是否为空,为空表明没有 Hash 冲突就直接在当前位置创建一个新桶即可。
  • 3、如果当前桶有值( Hash 冲突),那么就要比较当前桶中的 key、key 的 hashcode 与写入的 key 是否相等,相等就赋值给 e,在第 8 步的时候会统一进行赋值及返回。
  • 4、如果当前桶为红黑树,那就要按照红黑树的方式写入数据。
  • 5、如果是个链表,就需要将当前的 key、value 封装成一个新节点写入到当前桶的后面(形成链表)。
  • 6、接着判断当前链表的大小是否大于预设的阈值,大于时就要转换为红黑树。
  • 7、如果在遍历过程中找到 key 相同时直接退出遍历。
  • 8、如果 e != null 就相当于存在相同的 key,那就需要将值覆盖。
  • 9、最后判断是否需要进行扩容。

Get 方法:

  • 1、将 key hash 之后取得所定位的桶。
  • 2、如果桶为空则直接返回 null 。否则判断桶的第一个位置(有可能是链表、红黑树)的 key 是否为查询的 key,是就直接返回 value。
  • 3、如果第一个不匹配,则判断它的下一个是红黑树还是链表。红黑树就按照树的查找方式返回值。不然就按照链表的方式遍历匹配返回值。

:修改为红黑树之后查询效率直接提高到了 O(logn)。但是 HashMap 原有的问题也都存在,比如在并发场景下使用时容易出现死循环。

2、ConcurrentHashMap 原理?

1.7版本
ConcurrentHashMap 采用了分段锁技术,其中 Segment 继承于 ReentrantLock。不会像 HashTable 那样不管是 put 还是 get 操作都需要做同步处理,理论上 ConcurrentHashMap 支持 CurrencyLevel (Segment 数组数量)的线程并发。每当一个线程占用锁访问一个 Segment 时,不会影响到其他的 Segment。

1.8版本
1.7 已经解决了并发问题,并且能支持 N 个 Segment 这么多次数的并发,但依然存在 HashMap 在 1.7 版本中的问题:那就是查询遍历链表效率太低。和 1.8 HashMap 结构类似:其中抛弃了原有的 Segment 分段锁,而采用了 CAS + synchronized 来保证并发安全性
put 方法

  • 1、根据 key 计算出 hashcode 。
  • 2、判断是否需要进行初始化。如果当前 key 定位出的 Node为空表示当前位置可以写入数据,利用 CAS 尝试写入,失败则自旋保证成功。如果当前位置的 hashcode == MOVED == -1,则需要进行扩容。如果都不满足,则利用 synchronized 锁写入数据。
  • 3、最后,如果数量大于 TREEIFY_THRESHOLD 则要转换为红黑树。

get 方法

  • 1、根据计算出来的 hashcode 寻址,如果就在桶上那么直接返回值。
  • 2、如果是红黑树那就按照树的方式获取值。就不满足那就按照链表的方式遍历获取值。

补充:
分享两个谷歌给咱实现的几个算法,看看是否和咱自己平时写的有啥区别:
1、二分查找:

static int binarySearch(int[] array, int size, int value) {
        int lo = 0;
        int hi = size - 1;

        while (lo <= hi) {
            // 注意这里的 >>> 1 表示除2的意思,这样写的好处就是可以避免栈溢出。等价 >> 2
            final int mid = (lo + hi) >>> 1;
            final int midVal = array[mid];

            if (midVal < value) {
                lo = mid + 1;
            } else if (midVal > value) {
                hi = mid - 1;
            } else {
                return mid;  // value found
            }
        }
        return ~lo;  // value not present
    }
  • 3
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值