Java集合

本文很多知识点源自《JavaGuide ⾯试突击版》。

1.List、Set、Map的区别
  • List:保证数据存放有序、可以存储重复元素、可以通过下标操作元素。
  • Set:无序、不能存储重复元素
  • Map:使用键值对来存储。Map会维护与key有关联的值。键不能重复,值可以重复。
2.ArrayList和LinkedList的区别?
  • ArrayList:底层是由数组实现,初始容量为10,底层是根据右移运算进行扩容,每次扩容是在原容量的基础上增加一半,增删效率较低、查询效率较高,线程不安全的集合。
  • LinkedList:底层是基于基金二点来存储数据,通过地址值的指向来维系节点,内存不连续,不需要扩容,增删效率较高、查询效率较低,线程不安全的集合。
3.ArrayList 与 Vector 区别呢?为什么要用Arraylist取代Vector呢?
  • Vector是最早的集合类,底层基于数组实现存储,默认初始容量为10,底层是根据三目运算符进行扩容,如果增量大于0,每次扩容的值就是增量的值,如果增量的值不大于0,每次扩容的值就是原容量的值,是线程安全的集合。
  • 由于Vector类的所有方法都是同步的,可以由两个线程安全地访问一个Vector对象,但是一个线程安全的访问Vector的代码在同步上将会耗费大量时间。
    而ArrayList不是同步的,所以在不需要保证线程安全时建议使用ArrayList.
4.ArrayList的扩容机制。

扩容调用的是grow()方法,根据右移运算进行扩容,每次扩容是在原容量的基础上增加一半,之后通过grow()方法中调用的Arrays.copyof()方法进行对原数组的复制。

5.HashMap和Hashtable的区别
  • HashMap:
    1)底层基于数组+链表存储数据
    2)不能重复且不能保证顺序恒久不变
    3)允许存储null键和null值
    4)默认初始长度为16,默认加载因子为0.75,默认的扩容是在原来的基础上增加一倍。
    5)当给定初始容量时(2n~2(n+1)),底层真实容量就是2^(n+1) 值(如果创建时给定了初始容量,底层将会将其扩充为2的幂次方大小)
    6)异步式线程不安全的映射
    7)JDK1.8 以后的 HashMap 在解决哈希冲突时有了较⼤的变化,当链表⻓度⼤于阈值(默认为8)时,将链表转化为红⿊树,以减少搜索时间。Hashtable 没有这样的机制。
  • Hashtable:
    1)最早的映射类
    2)键和值都不能为null
    3)默认初始容量为11,默认加载因子为0.75,默认扩容是在原来的基础上增加一倍再加1
    4)指定初始容量为多少底层真实的初始容量就为多少
    5)同步式线程安全的映射
6.HashMap 和 HashSet区别

HashSet底层是基于HashMap实现的(除了 clone() 、writeObject() 、 readObject() 是HashSet 自己实现之外,其他方法都是直接调用 HashMap 中的方法。)


image.png
7.HashSet如何检查重复。

当把对象加入到HashSet中时,HashSet会先计算出对象的hashcode值,对哈希码值进行二次计算保证落在某个桶中,拿着新对象和对应桶上的所有对象进行equals比较,如果为true,就说明对象有重复。

8.HashMap的底层实现
  • jdk1.8之前:
     HashMap底层是基于数组+链表实现的。
     HashMap在进行对象存储时,会先计算出对象的哈希码值,对哈希码值进行二次计算保证对象能够落在某个桶中,拿着新对象和对应桶上所有对象进行equals比较,如果为true(说明有重复对象)就舍弃新对象不存储,如果全部比较完都是false则存在所有对象的最前面形成---链式栈结构.
     扩容机制:当时用的桶数/总桶数大于加载因子(默认0.75)进行扩容,每次扩容是在原来的基础上增加一倍。而在扩容之后需要把已经存储元素对象重新进行二次运算,这个过程称为rehash。
  • 在jdk1.8之后,在解决哈希冲突时有了较大的变化,当链表长度大于阈值(默认为8)时,将链表转为红黑树,以减少搜索时间。
  • TreeMap、TreeSet以及JDK1.8之后的HashMap底层都用到了红黑树。红黑树就是为了解决⼆叉查找树的缺陷,因为⼆叉查找树在某些情况下会退化成⼀个线性结构。
9.HashMap 的长度为什么是2的幂次方
  • 为了能够让HashMap存取高效,尽量较少的碰撞,也就是要尽量把数据分配均匀。hash值的范围-2147483648到2147483647,前后加起来大概40亿的映射空间,只要哈希函数映射得比较均匀松散,⼀般应⽤是很难出现碰撞的。但是一个40亿长度的数组,内存是放不下的。所以需要⽤之前还要先做对数组的⻓度取模运算,得到的余数才能⽤来要存放的位置也就是对应的数组下标。这个数组下标的计算⽅法是“ (n - 1) & hash ”。(n代表数组⻓度)。这也就解释了 HashMap 的⻓度为什么是2的幂次⽅。
  • 这个算法应该如何设计呢?
     我们⾸先可能会想到采⽤%取余的操作来实现。但是,重点来了:“取余(%)操作中如果除数是2的幂次则等价于与其除数减⼀的与(&)操作(也就是说 hash%lengthdehash&(length-1)的前提是 length 是2的n 次⽅;)。” 并且 采用⼆进制位操作 &,相对于%能够提⾼运算效率,这就解释了 HashMap 的⻓度为什么是2的幂次方
10.HashMap 多线程操作导致死循环问题

主要原因在于并发下的rehash会造成元素之间形成一个循环链表,不过,jdk1.8之后解决了这个问题,但还是不建议在多线程下使用HashMap,因为多线程下使用HashMap还是会存在其他问题,比如说数据丢失。并发环境下推荐使用ConcurrentHashMap.
详情请查看:https://coolshell.cn/articles/9606.html

11.ConcurrentHashMap 和 Hashtable 的区别.

ConcurrentHashMap 和 Hashtable 的区别主要体现在实现线程安全的方式上不同。

  • 底层数据结构:jdk1.7的ConcurrentHashMap底层采用的数据结构和hashMap1.8一样,数组+链表/红黑二叉树。hashtable和jdk1.8之前的hashMap的底层数据结构类似都是采用数组+链表的形式,数组时hashtable的主体,链表则是为了解决哈希冲突而存在的
  • 实现线程安全的方式(重要):
    1)在jdk1.7之前,ConcurrentHashMap(分段锁)对整个桶数据进行了分段分割,每一把锁只锁容器的一部分数据,多线程访问容器里的不同数据段的数据,不会存在锁竞争,提高并发访问率。 到了 JDK1.8 的时候已经摒弃了Segment的
    概念,而是直接用 Node 数组+链表+红黑树的数据结构来实现,并发控制使用 synchronized 和
    CAS 来操作。
    2)hashtable(同一把锁):使用synchronized 来保证线程安全,效率非常低下。当⼀个线程访问同步方法时,其他线程也访问同步方法,可能会进⼊阻塞或轮询状态,如使⽤ put 添加元素,另⼀个线程不能使用 put 添加元素,也不能使⽤ get,竞争会越来越激烈效率越低。
12. ConcurrentHashMap线程安全的具体实现⽅式/底层具体实现.

见知识点11。

13.comparable和Comparator的区别
  • comparable接口实际上来自java.lang包,他有一个compareTo(Object obj)方法来排序
  • Comparator接口上出自java.util包,他有一个compare(Object obj1,Object obj2)方法用来排序。
  • 一般需要对一个集合使用自定义排序时,我们重写compareTo()方法或compare()方法;当我们需要对某一个集合实现两种排序方式,比如⼀个song对象中的歌名和歌手名分别采用⼀种排序方
    法的话,我们可以重写 compareTo() ⽅法和使⽤自制的Comparator⽅法或者以两个Comparator来实现歌名排序和歌星名排序。

你可能感兴趣的:(Java集合)