Java集合包含面试题

Java集合

Java 所有的集合类都位于 java.util 包下

集合类和数组不一样,数组元素既可以是基本类型的值,也可以是对象(实际上保存的是对象的引用变量),而集合里只能保存对象(实际上只是保存对象的引用变量,但通常习惯上认为集合里保存的是对象)。

Java 集合类型(也称容器)分为: Collection 和 Map

  1. List:有序集合,允许有相同元素(指的是存储时,与存放顺序保持一致)
  2. Set:无序集合,不允许有重复元素(Collection的子接口)
    • Set 判断两个对象是否相同不是使用 == 运算符,而是根据 equals() 方法
    • 如果试把两个相同的元素加入同一个Set 集合中,则添加操作失败。
    • set接口没有提供额外的方法
  3. Queue:队列实现,类似List
  4. Map:双列数据,保存具有映射关系“key-value”的集合
    1. 以键值对的形式进行存储。
    2. 键不可以重复,值可以重复。
    3. 键只能有一个null键,值可以有多个null键。

List的4种实现类

1、ArrayList

  • ArrayList是我们在java开发过程中最常见的一种List实现类,属于线程不安全,读取速度快的一种List实现类。也是java入门时最常用的实现类。

  • 其中最重要的三个参数,初始数组增量和一个数组

  • 因为ArrayList采用的是数组的方式实现,所以其取值速度快,插入碰到扩容问题时速度会减慢

2、Vector

  • 和ArrayList基本相似,利用数组及扩容实现List,但Vector是一种线程安全的List结构,它的读写效率不如ArrayList,其原因是在该实现类内在方法上加上了同步关键字,其不同之处还在于Vector的增长速度不同

  • Vector在默认情况下是以两倍速度递增

  • 所以capacityIncrement可以用来设置递增速度,因此Vector的初始化多了一种方式,即设置数组增量

3、LinkedList

  • LinkedList是利用内部类Node为数据单元的双向链表,同样LinkedList是线程不安全的,其具有读效率低,写效率高,操作效率高等特性,适合用于频繁add,remove等操作的List,同时可以节省一定的内存,在clear的情况下推荐使用GC回收,并且没有最大长度限制。
  • 可以看出双向链表的节点操作没有扩充的拷贝操作,在这种情况下操作相对于反复扩容效率要高,但也仅是相对的,但是有大量数据操作,特别是删除等,只需要做节点的横向移动,效率是很高的。
  • LinkedList没有预留排序接口

4、Stack

  • 继承于Vector,基本特性与Vector一致,线程安全,但效率低,实际就是利用Vector抽象成栈,使之拥有后进先出的特性

ArrayList 的扩容机制?

ArraylList底层实现是Object数组。数组大小一旦规定则无法修改。
扩容机制:当调用ArrayList的add方法时就会设计到扩容机制。

  1. 创建ArrayList对象时,若未指定集合容量,集合默认容量为0
  2. 当ArrayList调用add方法存储数据时,进行容量初始化。容量为10
  3. 若使用addAll方法添加元素,则初始化大小为10和添加集合长度的较大值
  4. 集合初始化后,在调用add方法,先将集合扩大1.5倍,如果仍然不够直接将我们预计的值作为扩容长度 。并调用Array.copyOf方法将elementData数组指向新的长度为扩容后长度的内存空间

总结:ArrayList在第一次插入元素add()时分配10(默认)个对象空间。假如有20个数据需要添加,那么会在第11个数据的时候(原始数组容量存满时),按照1.5倍增长;之后扩容会按照1.5倍增长每次扩容都是通过Arrays.copyOf(elementData, newCapacity) —>Array.copyOf方法将elementData数组指向新的长度为扩容后长度的内存空间这样的方式实现的。

ArrayList和LinkedList的异同?

都是线程不安全的,相对于线程安全的Vector,执行效率高。

ArrayList是实现了基于动态数组的数据结构,LinkedList基于链表的数据结构。

对于随机访问get和set,ArrayList优于LinkedList,因为LinkedList要移动指针。对于新增和删除操作add(特指插入)和remove,LinkedList比较占优势,因为ArrayList要移动数据。

哈希算法就是典型的O(1)时间复杂度,无论数据规模多大,都可以在一次计算后找到目标(不考虑冲突的话)。 3、时间复杂度为O(n)。 就代表数据量增大几倍,耗时也增大几倍。

ArrayList集合实现RandomAccess接口有何作用?为何LinkedList集合却没实现这接口?

ArrayList实现RandomAccess接口,但是RandomAccess接口里面是空的!LinkedList并没有实现RandomAccess接口。

原因:实现RandomAccess接口的List可以通过for循环来遍历数据比使用iterator遍历数据更高效,未实现RandomAccess接口的List可以通过iterator遍历数据比使用for循环来遍历数据更高效。

RandomAccess接口是一个标志接口(Marker)。只要List集合实现这个接口,就能支持快速随机访问

ArrayList用for循环遍历比iterator迭代器遍历快,LinkedList用iterator迭代器遍历比for循环遍历快

RandomAccess接口这个空架子的存在,是为了能够更好地判断集合是否ArrayList或者LinkedList,从而能够更好选择更优的遍历方式,提高性能!

怎么判断出接收的List子类是ArrayList还是LinkedList呢?

这时就需要用InstanceOf来判断List集合子类是否实现RandomAccess接口!

数组(Array)和列表(ArrayList)有什么区别?什么时候应该使用Array而不是ArrayList?

  1. Array可以包含基本数据类型和对象类型;ArrayList只能包含对象类型
  2. Array的大小是固定的;ArrayList大小是动态变化的
  3. ArrayList提供了更多的方法和特性:addAll、removeAll、iterator(迭代器)
  4. ArrayList可以存放不同类型的数据,在存储基本类型数据的时候要使用基本数据类型的包装类
  5. 当能确定长度并且数据类型一致的时候就用数组,其他时候使用ArrayList

ArrayList和Vector的区别?

Vector是同步类(synchronize),属于强同步类。因此开销就比ArrayList要大,访问要慢。正常情况下,大多数的Java程序员使用ArrayList而不是Vector,因为同步完全可以由程序员自己来控制。Vector每次扩容请求其大小的2倍空间,而ArrayList是1.5倍。Vector还有一个子类Stack。

Vector中的方法由于添加了synchronized修饰,因此Vector是线程安全的容器,但性能上较ArrayList差 ,因此已经是Java中的遗留容器

Set的实现类

1、HashSet

  • HashSet 按 Hash 算法来存储集合中的元素,因此具有很好的存取、查找、删除性能
  • 对于存放在Set容器中的对象,对应的类一定要重写equals()和hashCode(Object obj)方法,以实现对象相等规则。即:“相等的对象必须具有相等的散列码”。
  • HashSet的实现依赖于HashMap,HashSet的值都是存储在HashMap中
  • HashSet不允许重复值
  • HashSet的值是作为HashMap的key存储在HashMap中的,当存储的值已经存在时返回false

2、LinkedHashSet

  • LinkedHashSet根据元素的hashCode值来决定元素的存储位置,但它同时使用双向链表维护元素的次序,这时的元素看起来是以插入顺序保存的。
  • 插入性能低于HashSet
  • 不允许集合元素重复

3、TreeSet

  • TreeSet 是 SortedSet 接口的实现类,TreeSet 可以确保集合元素处于排序状态。
  • TreeSet底层使用红黑树结构存储数据
  • TreeSet 两种排序方法:自然排序和定制排序。默认情况下,TreeSet 采用自然排序

HashSet 怎么保证元素不重复的?

元素的值是map的key,map的value是present变量,这个变量只作为放入map的一个占位符而存在,并没有什么实际作用。

HashMap 的 key 是不能重复的,而这里HashSet的元素又是作为了map的key,当然不能重复

Map接口常用实现类

HashMap、TreeMap、LinkedHashMap和Properties。其中,HashMap是 Map 接口使用频率最高的实现类

HashMap存储结构

  • JDK 7及以前版本:HashMap是数组+链表结构(即为链地址法)
  • JDK 8版本发布以后:HashMap是数组+链表+红黑树实现。
  • 非线程安全的,在多线程环境下,多个线程同时触发HashMap的改变时,有可能会发生冲突。所以多线程下不建议使用。可以考虑使用Collection将HashMap转为线程安全的HashMap,推荐方式使用ConcurrentHashMap

2、LinkedHashMap

  • LinkedHashMap 是 HashMap 的子类
  • 在HashMap存储结构的基础上,使用了一对双向链表来记录添加元素的顺序
  • 与LinkedHashSet类似,LinkedHashMap 可以维护 Map 的迭代顺序:迭代顺序与 Key-Value 对的插入顺序一致

3、TreeMap

  • TreeMap存储 Key-Value 对时,需要根据 key-value 对进行排序。TreeMap 可以保证所有的 Key-Value 对处于有序状态。

  • TreeSet底层使用红黑树结构存储数据

  • TreeMap 的 Key 的排序:

    • 自然排序:TreeMap 的所有的 Key 必须实现 Comparable 接口,而且所有的 Key 应该是同一个类的对象,否则将会抛出 ClasssCastException
    • 定制排序:创建 TreeMap 时,传入一个 Comparator 对象,该对象负责对TreeMap 中的所有 key 进行排序。此时不需要 Map 的 Key 实现Comparable 接口
  • TreeMap判断两个key相等的标准:两个key通过compareTo()方法或者compare()方法返回0。

4、Hashtable

  • Hashtable是个古老的 Map 实现类,JDK1.0就提供了。不同于HashMap,Hashtable是线程安全的。
  • Hashtable实现原理和HashMap相同,功能相同。底层都使用哈希表结构,查询速度快,很多情况下可以互用。
  • 与HashMap不同,Hashtable 不允许使用 null 作为 key 和 value
  • 与HashMap一样,Hashtable 也不能保证其中 Key-Value 对的顺序
  • Hashtable判断两个key相等、两个value相等的标准,与HashMap一致。

5、Properties

  • Properties 类是 Hashtable 的子类,该对象用于处理属性文件
  • 由于属性文件里的 key、value 都是字符串类型,所以 Properties 里的 key 和 value 都是字符串类型
  • 存取数据时,建议使用setProperty(String key,String value)方法和getProperty(String key)方法

Set和Map容 器 都有基于哈希存储和排序树的两种实现版本,基于哈希存储 的版本理论存取时间复杂度为O(1),而基于排序树版本的实现在插入或删除元素时会按照元素或元素的键(key)构成排序树从而达到排序和去重的效果 。

HashMap的底层原理

在JDK1.7之前,HashMap采用数据+联表的结构来实现。在JDK1.8中,HashMap采用数据+联表+红黑树实现。当链表长度超过阈值8时,将链表转换为红黑树。

扩容机制:HashMap的默认初始容量:16,这个容量会以2的指数增长。当元素到达一定比例就会扩容,这个比例是负载因子,默认为0.75。阈值=初始容量16*负载因子0.75

HashMap的put方法流程

  1. 判断数组,若发现数组为空,则进行首次扩容

  2. 判断头节点,若发现头节点为空,则新建链表节点,存入数组

  3. 判断头结点,若发现头结点不为空,则将元素插入槽内

    1. 若元素的key与头节点一致,则直接覆盖头节点

    2. 若元素为树形节点,则元素追加到树中

    3. 若元素为链表节点,则将元素追加到链表中。追加后,需要判断链表的长度以决定是否转为红黑树。(1)若链表长度达到8,数组容量没达到64,则扩容。(2)若链表长度达到8、数组容量达到4,则转为红黑树。

      扩容机制:向HashMap中添加数据时,三个条件触发扩容行为
      (1)如果数组为空,则进行首次扩容
      (2)将元素接入链表后,如果长度达到8,并且数组长度 < 64,则扩容

      (3)添加后,如果数组中元素超过阈值,即超出负载因子的限制,则扩容。

      并且每次扩容时,都将容量翻倍,即创建一个2倍大的新数组,然后将旧数组中的数据迁移到新数组里。由于HashMap中数组的容量为2^N,所以可以用位移运算计算新容量,效率很高。

  4. 插入元素后,判断元素的个数,若发现超过阈值则再次扩容

HashMap 的 get方法流程

  1. 首先根据hash方法获取到key的hash值
  2. 然后通过 hash &(length-1)的方式获取到key所对应的Node数组下标 ( length对应数组长度 )
  3. 首先判断此节点是否为空,是否是要找的值;是则返回空,否则进入第二个节点
  4. 接着判断第二个节点是否为空,是则返回空,不是则判断此时数据结构是链表还是红黑树
  5. 链表结构进行顺利便利查找操作,每次用==符号和equals方法来判断key是否相同,满足条件则直接返回该节点。链表遍历完都没有找到则返回空
  6. 红黑树结构执行相应的getTreeNode查找

HashMap 的 resize 方法的执行过程

  1. 当旧的数组只有一个元素,就是判断出它next==null,也就是说没有冲突,那就直接把该元素放置到新table里面同样索引的位置
  2. 如果要复制的节点是一个红黑树型节点,进行红黑树操作
  3. 如果要复制的节点下存在冲入,也就是有链表存在。就从头节点开始遍历,事先通过一个巧妙的运算e.hash & oldCap,这个运算的结果只有0和1。用来判断该元素在新table中索引的位置是否发生变化。
    结果是:0 直接元素存放在 newtable[ j ]
    结果是:1 存放在newtable[ j+oldCap ]

HashMap 的 size 为什么必须是 2 的整数次方?

  1. 提升计算效率,更快算出元素的位置
  2. 减少哈希碰撞,使得元素分布均匀

Hash值的范围值-2147483648到2147483647,前后加起来大概40亿的映射空间,只要哈希函数映射得比较均匀松散,一般应用是很难出现碰撞的

HashMap 多线程死循环问题?

死循环形成是在扩容转移元素的时候发生的。具体在tansfer函数中,默认情况下rehash方法为false

当往HashMap中添加元素时,会引起HashMap容器的扩容

假设有两个线程:T1 T2,假设HashMap的当前树组容量是2:
1、现在,有线程T1和T2同时对该HashMap进行扩容,并且它们扩容后,都把结点元素全部移动到新树组的索引3处。-----在索引1处的链表引用关系是 a -> b -> c -> d -> null。
2、假设线程T1运行到Entry next = e.next;这行代码,时间片就用完,即当前T1已计算得出e=a,e.next=b
3、线程T2开始执行并且完成了整个扩容操作,并把链表移到了索引3处----索引3处的链表引用关系是 d -> c -> b -> a -> null。
4、线程T1拿到时间片了,继续执行Entry next = e.next;后面的代码,注意此时T1中e=a,e.next=b,所以需要将结点a头插到索引3的位置----索引1处的链表引用关系是 a -> null
5、由于T2中扩容后得到的链表关系是 d -> c -> b -> a -> null,因此T1线程中此时链表结点引用关系实际上应是这样的:b -> a -> null -> d -> c
6、然后,执e = next;Entry next = e.next;代码,对e变量以及e.next变量重新赋值,得到:e=a,e.next=null。
继续将a头插到索引3的位置:a -> b 而 b -> a; d -> c -> b。链表结点a和b互相引用了,即形成了一个环。当我们使用get方法,取到索引为3中的某元素时候,将会出现死循环
由于d结点和c结点并没有其他结点指向它们,所以,d和c结点的数据也将会丢失。

原因

  1. 多线程put操作后,get操作导致死循环
  2. 多线程put非null元素后,给操作得到null值

解决

  1. 使用ConcurrentHashMap代替HashMap
  2. 使用Collections.synchronizedMap(Mao m)方法把HashMap变成一个线程安全的Map

HashMap 的 get 方法能否判断某个元素是否在 map 中?

不能。因为get返回null有可能是不包含该key,也有可能该key对应的value为null。因为HashMap中的key和value都允许为null

LinkedHashMap 的实现原理?

LinkedHashMap 基于HashMap实现的,不同的是它定义了一个Entry header,这个header不是放在table里,是额外独立出来的。

LinkedHashMap通过继承HashMap 中的Entry ,并添加两个属性Entry before,after。和header结合起来组成一个双向链表,来实现安插入顺序或访问顺序排序

LinkedHashMap 定义了排序模式AccessOrder,该属性为boolean型变量,对于访问顺序为true,对于插入顺序则为false。一般情况下,不比指定排序模式,其迭代顺序即为默认插入顺序。

HashTable和HashMap的区别

  • HashTable是同步的,HashMap不是。在多线程场合要手动同步HashMap,这个区别就像Vector和ArrayList一样
  • HashTable不允许null值(key和value都不行),HashMap允许(key和value都可以)
  • HashTable直接使用对象的HashCode,而HashMap重新计算hash值,而且用于代替求模。font>
  • HashTable基于Dictionary类,HashMap基于AbstractMap类
  • HashTable使用Enumeration,HashMap使用terator

相同点:

  1. 两者的遍历方式差不多(大同小异)。HashTable仅仅比HashMap多一个elements方法。
  2. HashTable和HashMap都能通过values()方法返回一个Collection,然后进行遍历处理。
  3. 两者也都可以通过entrySet()方法返回一个Set,然后进行遍历处理

HashMap和HashSet的区别

  1. HashMap实现Map接口,HashSet实现Set接口
  2. HashMap存储键值对,HashSet进存储对象
  3. HashMap调用put()向map中添加元素,HashSet调用add()方法向Set中添加元素
  4. HashMap使用建(key)计算HashCode;HashSet使用成员对象来计算HashCode值,对于两个对象来说HashCode可能相同,所以equals()方法用来判断对象的相等性,如果两个对象不同的话,返回false
  5. HashMap相对于HashSet比较快,因为使用唯一的键获取对象;HashSet较HashMap慢

ConcurrentHashMap 的实现原理是什么?

JDK1.7中,ConcurrentHashMap 采用数组+Segment+分段锁的实现方式
JDK1.8中,ConcurrentHashMap 采用数组+链表+红黑树的实现方式,内部大量采用CAS操作

ConcurrentHashMap 主干是Segment数组。Segment继承了ReentrantLock,所以它是一种可重入锁

在ConcurrentHashMap ,一个Segment就是一个子哈希表,Segment里维护了一个HashEntry数组,并发环境下,对于不同Segment的数据进行操作是不用考虑锁竞争的。就按默认的ConcurrentLevel为16来讲,理论上就允许16个线程并发执行。

所以,对于同一个Segment的操作才需考虑线程同步不同的Segment 则无需考虑。Segment类似与HashMap,一个Segment维护一个HashEntry数组

HashEntry 是目前我们提到的最小逻辑处理单元。一个ConcurrentHashMap 定位一个元素的过程需要进行两次Hash操作。第一次Hash定位到Segment,第二次Hash定位到元素所在的链表的头部。

HashMap和HashTable与ConCurrentHashMap的区别

  • HashMap是非线程安全的,CurrentHashMap是线程安全的
  • ConCurrentHashMap将整个hash桶进行了分段segment,即将这个大的数组分成几个小的片段segment,而且每个小的片段segment上面都有锁的存在,那么在插入元素的时候就需要先找到应该插入到哪一个片段segment,然后再在这个片段上面进行插入,而且这里还需要获取segment锁。
  • ConCurrentHashMap让锁的粒度更精细一些,并发性能更好。
  1. ConCurrentHashMap仅仅锁定map的某个部分,HashMap锁住整个map

    当一个线程访问同步方法时,其他线程也访问同步方法,可能会进入阻塞或轮询状态,如使用put添加元素,另一个线程不能使用put添加元素,也不能使用get,竞争会越来越激烈效率越低。

  2. HashTable(同一把锁):使用synchronize来保证线程安全,但效率非常低下。ConCurrentHashMap(分段锁):(锁分段技术)每一把锁只锁容器其中一部分数据,多线程访问容器不同数据段的数,就不会存在竞争,提高并发访问率。

    首先将数据分为一段一段的存储,然后给每一段数据配一把锁,当一个线程占用锁访问其中一个段数据时,其他段的数据也能被其他线程访问。concurrenthashmap是由Segment数组结构和HahEntry数组结构组成。Segment是一种可重入锁ReentrantLock,扮演锁的角色。HashEntry用于存储键值对数据。一个concurrenthashmap里包含一个Segment数组。Segment的结构和Hashmap类似,是一种数组和链表结构,一个Segment包含一个HashEntry数组,每个HashEntry是一个链表结构的元素,每个Segment守护着一个HashEntry数组里的元素,当对HashEntry数组的数据进行修改时,必须首先获得对应的Segment。

Iterator 怎么使用?有什么特点?
迭代器是一种设计模式,它是一个对象,它可以遍历并选择序列中的对象,而开发人员不需要了解该序列的底层结构。迭代器通常被称为“轻量级”对象,因为创建它的代价小。Java 中的 Iterator 功能比较简单,并且只能单向移动:

使用方法 iterator() 要求容器返回一个 Iterator。第一次调用 Iterator 的 next() 方法时,它返回序列的第一个元素。注意:iterator() 方法是 java.lang.Iterable 接口,被 Collection 继承。

  • 使用 next() 获得序列中的下一个元素。

  • 使用 hasNext() 检查序列中是否还有元素。

  • 使用 remove() 将迭代器新返回的元素删除。

Iterator 和 ListIterator 有什么区别?

  • Iterator 可用来遍历 Set 和 List 集合,但是 ListIterator 只能用来遍历 List。
  • Iterator 对集合只能是前向遍历,ListIterator 既可以前向也可以后向。
  • ListIterator 实现了 Iterator 接口,并包含其他的功能,比如:增加元素,替换元素,获取前一个和后一个元素的索引等等。

Iterator 和 Enumeration 接口的区别?
与 Enumeration 相比,Iterator 更加安全,因为当一个集合正在被遍历的时候,它会阻止其它线程去修改集合。否则会抛出 ConcurrentModificationException 异常。这其实就是 fail-fast 机制。具体区别有三点:

  • Iterator 的方法名比 Enumeration 更科学;
  • Iterator 有 fail-fast 机制,比 Enumeration 更安全;
  • Iterator 能够删除元素,Enumeration 并不能删除元素。

fail-fast (快速失败)与 fail-safe(安全失败)有什么区别?

Iterator的fail-fast属性与当前的集合共同起作用(安全失败是基于对底层集合做拷贝),因此他不会受到集合中任何改动的影响。Java.util包中的所有集合类都被设计为fail-fast的,而Java.util.concurrent中的集合类都为fail-safe当检测到正在遍历的集合的结构被改变时,fail-fast迭代器抛出ConcurrentModificationException,而fail-safe迭代器从不抛出ConcurrentModificationException

Collection 和 Collections 有什么区别?

  • Collection :最基本的集合接口,一个Collection代表一组Object,即Collection的元素。它的直接继承接口有List、Set和Queue
  • Collections:不属于Java的集合框架,它是集合类的一个工具类/帮助类。此类不能被实例化,服务于Java的Collection框架。它包含有关集合操作的静态多态方法,实现各种集合的搜索、排查、线程安全等操作

Hash算法

指任意长度输入经过hash算法转化为固定长度输出。多本书,总结出一本摘要,理解为hash值。hash算法可以理解为摘要算法或者散列算法

特点:

  1. 不可逆:不能通过hash值计算出原值。可应用在密码学及数字签名,可以验证文件完整性
  2. 效率高:hash运算能够快速得到结果
  3. 冲突少:优秀的hash算法具备的条件

hash冲突解决方案:key经过hash算法定位到hash桶的某一个位置,该位置有值时会产生冲突,这个通过1、链表解决称为链地址法;2、也可以向下线性或者随机探测不冲突的地址,称为开放地址法。3、也可以通过多个hash算法重新定位称为再hash法。

  1. 加法hash,把输入元素一个一个加起来构成最后的结果
  2. 运位算hash,这类型hash函数通过利用各种位运算(常见的是唯一和异或)来充分混合输入元素
  3. 乘法hash,这种类型的hash函数利用乘法的不相关性–乘法的这种性质,最有名的莫过于平方取头尾的随机数生成算法,虽然这种算法效果并不好;jdk5.0里面的String类的HashCode方法也使用乘法Hash,32位FNV算法
  4. 除法hash除法和乘法一样具有表上看起开不相关性。不过因为除法太慢,这种方式几乎找不到真正的应用
  5. 查表hash,查表最有名的例子就是CRC系列算法。虽然CRC系列算法本身并不是查表。但是查表是它的一种最快的实现方式。查表hash中有名例子:Universal Hashing和Zobrist Hashing。他们的表格否是随机生成的。
  6. 混合hash:混合Hash算法利用了以上各种方式。各种常见的Hash算法。如:MD5、Tiger都属于这个范围。他们一般很少在面向查找的Hash函数里面使用

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