2018_java集合面试题

一、集合体系图

    1、单列集合

    2、双列集合

二、单列集合面试题:

1、ArrayList 和 LinkedList 有什么区别。

ArrayList和LinkedList都实现了List接口,有以下的不同点:

a、ArrayList是基于索引的数据接口,它的底层是数组。它可以以O(1)时间复杂度对元素进行随机访问。与此对应,LinkedList是以元素列表的形式存储它的数据,每一个元素都和它的前一个和后一个元素链接在一起,在这种情况下,查找某个元素的时间复杂度是O(n)。所以ArrayList的查询是要比LinkedList要快。 

    b、相对于ArrayList,LinkedList的插入,添加,删除操作速度更快,因为当元素被添加到集合任意位置的时候,不需要像数组那样重新计算大小或者是更新索引。

    c、LinkedList比ArrayList更占内存,因为LinkedList为每一个节点存储了两个引用,一个指向前一个元素,一个指向下一个元素。

2、ArrayList 底层实现方式?

a、ArrayList 底层是通过数组实现,一旦实例化 ArrayList 无参构造函数,那么默认数组初始化长度就为 10。

b、add 方法底层实现如果增加的元素个数超过了 10 个,那么 ArrayList 底层会新生成一个数组,长度为原数组的 1.5 倍+1jdk1.6之前,1.7以后是用的位运算(效率更高),是1.5倍,然后将原数组的内容复制到新数组当中,并且后续增加的内容都会放到新数组当中。当新数组无法容纳增加的元素时,重复该过程。是一旦数组超出长度,就开始扩容数组。扩容数组调用的方法 Arrays.copyOf(objArr, objArr.length + 1);

3、LinkedList 底层实现方式?

a、LinkedList 底层的数据结构是基于双向循环链表的,且头结点中不存放数据。

b、LinkedList包含两个重要的成员:header 和 size。
header是双向链表的表头,它是双向链表节点所对应的类Entry的实例。Entry中包含成员变量: previous, next, element。其中,previous是该节点的上一个节点,next是该节点的下一个节点,element是该节点所包含的值。 
size是双向链表中节点的个数

c、从LinkedList的实现方式中可以发现,它不存在LinkedList容量不足的问题。
d、LinkedList底层的克隆函数,即是将全部元素克隆到一个新的LinkedList对象中

 

4、Vector的底层实现方式?

a、Vector底层是通过数组实现的,一旦实例化Vector无参构造函数,那么默认数组初始化长度为10。

b、加载因子为1,一旦元素的个数超过容量的长度时,会进行扩容。

c、扩容后的大小为原数组的2倍,即原来为10,扩容后为20。

d、Vector是线程安全的,因为底层的方法都是用Sychronized修饰的,它的速度相对ArrayList要慢些。

 

5、HashSet的底层实现方式?

  1. HashSet底层实现是一个HashMap用来保存数据,实现Set接口。
  2. 线程不安全,存取速度快
  3. 默认初始容量为16加载因子为0.75:即当元素个数超过容量长度的0.75倍时,进行扩容,扩容大小为原容量的2倍。如HashSet的容量为16,一次扩容后是容量为32

 

6、HashMap的底层实现之扩容?

HashMap默认初始容量为16(为何是16:16是2^4,可以提高查询效率,另外,32=16<<1)加载因子为0.75:即当 元素个数 超过 容量长度的0.75倍 时,进行扩容扩容后大小为原容量的2

7、Hashtable的底层实现之扩容?

Hashtable 线程安全的,底层方法都用Sychronized修饰的。默认初始容量为11加载因子为0.75:即当元素个数超过容量长度的0.75倍 时,进行扩容扩容后的大小为原容量的2倍+1如 Hashtable的容量为11,一次扩容后是容量为23

 

 

8、ArrayList、Vector、HashSet、HashMap、Hashtable的扩容

Class

初始大小

加载因子

扩容倍数

底层实现

Code

是否线程安全

同步方式

ArrayList

10

1

1.5倍+1

Object数组

int newCapacity = oldCapacity + (oldCapacity >> 1)+1;
">>"右移符号,所以是除以2,所以新的容量是就的1.5倍+1
Arrays.copyOf 调用 System.arraycopy 扩充数组

线程不安全

-

Vector

10

1

2倍

Object数组

int newCapacity = oldCapacity + ((capacityIncrement > 0) ?
capacityIncrement : oldCapacity);
capacityIncrement默认为0,所以是加上本身,也就是2*oldCapacity,2倍大小
Arrays.copyOf 调用 System.arraycopy 扩充数组

线程安全

synchronized

HashSet

16

0.75f

2倍

HashMap<E,Object>

add方法实际调用HashMap的方法put

线程不安全

-

HashMap

16

0.75f

2倍

Map.Entry

void addEntry(int hash, K key, V value, int bucketIndex) {
    if ((size >= threshold) && (null != table[bucketIndex])) {
        resize(2 * table.length);
        hash = (null != key) ? hash(key) : 0;
        bucketIndex = indexFor(hash, table.length);
    }

    createEntry(hash, key, value, bucketIndex);
}

线程不安全

-

Hashtable

11

0.75f

2倍+1

Hashtable.Entry数组

int newCapacity = (oldCapacity << 1) + 1;
if (newCapacity - MAX_ARRAY_SIZE > 0) {
    if (oldCapacity == MAX_ARRAY_SIZE)
        return;
    newCapacity = MAX_ARRAY_SIZE;
}

线程安全

synchronized

 

 

 

 

9、ArrayList 集合加入11万条数据,应该怎么提高效率?

因为 ArrayList 的底层是数组实现,并且数组的默认值是 10,如果插入10000 条要不断的扩容,耗费时间,所以我们调用 ArrayList 的指定容量的构造器方法 ArrayList(int size) 就可以实现不扩容,就提高了性能。

10、以下是关于HashMap 面试题

HashMap 的工作原理是近年来常见的 Java 面试题。几乎每个 Java 程序员都知道 HashMap,都知道哪里要用 HashMap,知道 Hashtable 和 HashMap 之间的区别,那么为何这道面试题如此特殊呢?是因为这道题考察的深度很深。这题经常出现在高级或中高级面试中。投资银行更喜欢问这个问题,甚至会要求你实现 HashMap 来考察你的编程能力。ConcurrentHashMap 和其它同步集合的引入让这道题变得更加复杂。让我们开始探索的旅程吧!先来些简单的问题

1)“你用过 HashMap 吗?” “什么是 HashMap?你为什么用到它?”

几乎每个人都会回答“是的”,然后回答 HashMap 的一些特性,譬如 HashMap可以接受 null 键值和值,而 Hashtable 则不能;HashMap 是非synchronized;HashMap 很快;以及 HashMap 储存的是键值对等等。这显示出你已经用过 HashMap,而且对它相当的熟悉。但是面试官来个急转直下,从此刻开始问出一些刁钻的问题,关于 HashMap 的更多基础的细节。面试官可能会问出下面的问题:

 

2)“你知道 HashMap的工作原理吗?” “你知道 HashMap 的get()方法的工作原理吗?”

你也许会回答“我没有详查标准的 Java API,你可以看看 Java 源代码或者Open JDK。”“我可以用 Google 找到答案。”但一些面试者可能可以给出答案,“HashMap 是基于 hashing 的原理,我们使用 put(key, value)存储对象到 HashMap 中,使用 get(key)从 HashMap 中获取对象。当我们给 put()方法传递键和值时,我们先对键调用 hashCode()方法,返回的 hashCode 用于找到 bucket 位置来储存 Entry 对象。”这里关键点在于指出,HashMap 是在 bucket 中储存键对象和值对象,作为 Map.Entry。这一点有助于理解获取对象的逻辑。如果你没有意识到这一点,或者错误的认为仅仅只在 bucket 中存储值的话,你将不会回答如何从 HashMap 中获取对象的逻辑。这个答案相当的正确,也显示出面试者确实知道 hashing 以及 HashMap 的工作原理。但是这仅仅是故事的开始,当面试官加入一些 Java 程序员每天要碰到的实际场景的时候,错误的答案频现。下个问题可能是关于 HashMap 中的碰撞探测(collision detection)以及碰撞的解决方法:

3)“当两个对象的 hashcode 相同会发生什么?”

从这里开始,真正的困惑开始了,一些面试者会回答因为 hashcode 相同,所以两个对象是相等的,HashMap 将会抛出异常,或者不会存储它们。然后面试官可能会提醒他们有 equals()和 hashCode()两个方法,并告诉他们两个对象就算hashcode 相同,但是它们可能并不相等。一些面试者可能就此放弃,而另外一些还能继续挺进,他们回答“因为 hashcode 相同,所以它们的 bucket 位置相同,‘碰撞’会发生。因为 HashMap 使用链表存储对象,这个 Entry(包含有键值对的 Map.Entry 对象)会存储在链表中。”这个答案非常的合理,虽然有很多种处理碰撞的方法,这种方法是最简单的,也正是 HashMap 的处理方法。但故事还没有完结,面试官会继续问:

4)“如果两个键的 hashcode 相同,你如何获取值对象?”

面试者会回答:当我们调用 get()方法,HashMap 会使用键对象的 hashcode 找到 bucket 位置,然后获取值对象。面试官提醒他如果有两个值对象储存在同一个 bucket,他给出答案:将会遍历链表直到找到值对象。面试官会问因为你并没有值对象去比较,你是如何确定确定找到值对象的?除非面试者直到HashMap 在链表中存储的是键值对,否则他们不可能回答出这一题。其中一些记得这个重要知识点的面试者会说,找到 bucket 位置之后,会调用keys.equals()方法去找到链表中正确的节点,最终找到要找的值对象。完美的答案!许多情况下,面试者会在这个环节中出错,因为他们混淆了 hashCode()和equals()方法。因为在此之前 hashCode()屡屡出现,而 equals()方法仅仅在获取值对象的时候才出现。一些优秀的开发者会指出使用不可变的、声明作final 的对象,并且采用合适的 equals()和 hashCode()方法的话,将会减少碰撞的发生,提高效率。不可变性使得能够缓存不同键的 hashcode,这将提高整个获取对象的速度,使用 String,Interger 这样的 wrapper 类作为键是非常好的选择。

如果你认为到这里已经完结了,那么听到下面这个问题的时候,你会大吃一

惊。

5)“如果 HashMap 的大小超过了负载因子(load factor)定义的容量,怎么办?”

除非你真正知道 HashMap 的工作原理,否则你将回答不出这道题。默认的负载因子大小为 0.75,也就是说,当一个 map 填满了 75%的 bucket 时候,和其它集合类(如 ArrayList 等)一样,将会创建原来 HashMap 大小的两倍的 bucket 数组,来重新调整 map 的大小,并将原来的对象放入新的 bucket 数组中。这个过程叫作 rehashing,因为它调用 hash 方法找到新的 bucket 位置。如果你能够回答这道问题,下面的问题来了:

6)“你了解重新调整 HashMap 大小存在什么问题吗?”

你可能回答不上来,这时面试官会提醒你当多线程的情况下,可能产生条件竞

争(race condition)。当重新调整 HashMap 大小的时候,确实存在条件竞争,因为如果两个线程都发现 HashMap 需要重新调整大小了,它们会同时试着调整大小。在调整大小的过程中,存储在链表中的元素的次序会反过来,因为移动到新的 bucket 位置的时候,HashMap 并不会将元素放在链表的尾部,而是放在头部,这是为了避免尾部遍历(tail traversing)。如果条件竞争发生了,那么就死循环了。这个时候,你可以质问面试官,为什么这么奇怪,要在多线程的环境下使用 HashMap呢?:)

7)为什么 String, Interger 这样的 wrapper 类适合作为键?

String, Interger 这样的 wrapper 类作为 HashMap 的键是再适合不过了,而且String 最为常用。因为 String 是不可变的,也是 final 的,而且已经重写了equals()和 hashCode()方法了。其他的wrapper 类也有这个特点。不可变性是必要的,因为为了要计算 hashCode(),就要防止键值改变,如果键值在放入时和获取时返回不同的hashcode的话,那么就不能从HashMap中找到你想要的对象。不可变性还有其他的优点如线程安全。如果你可以仅仅通过将某个 field 声明成final 就能保证 hashCode 是不变的,那么请这么做吧。因为获取对象的时候要用到 equals()和 hashCode()方法,那么键对象正确的重写这两个方法是非常重要的。如果两个不相等的对象返回不同的 hashcode 的话,那么碰撞的几率就会小些,这样就能提高 HashMap 的性能。

8)我们可以使用自定义的对象作为键吗?

这是前一个问题的延伸。当然你可能使用任何对象作为键,只要它遵守了equals()和 hashCode()方法的定义规则,并且当对象插入到 Map 中之后将不会再改变了。如果这个自定义对象时不可变的,那么它已经满足了作为键的条件,因为当它创建之后就已经不能改变了。

9)我们可以使用 CocurrentHashMap 来代替 Hashtable 吗?

这是另外一个很热门的面试题,因为 ConcurrentHashMap 越来越多人用了。我们知道 Hashtable 是 synchronized 的,但是 ConcurrentHashMap 同步性能更好,因为它仅仅根据同步级别对 map 的一部分进行上锁。ConcurrentHashMap当然可以代替 HashTable,但是 HashTable 提供更强的线程安全性。

10)HashMap  的工作原理

HashMap 基于 hashing 原理,我们通过 put()和 get()方法储存和获取对象。当我们将键值对传递给 put()方法时,它调用键对象的 hashCode()方法来计算hashcode,让后找到 bucket 位置来储存值对象。当获取对象时,通过键对象的equals()方法找到正确的键值对,然后返回值对象。HashMap 使用链表来解决碰撞问题,当发生碰撞了,对象将会储存在链表的下一个节点中。 HashMap 在每个链表节点中储存键值对对象。当两个不同的键对象的hashcode相同时会发生什么?它们会储存在同一个 bucket 位置的链表中。键对象的 equals()方法用来找到键值对。因为 HashMap 的好处非常多,我曾经在电子商务的应用中使用 HashMap 作为缓存。因为金融领域非常多的运用 Java,也出于性能的考虑,我们会经常用到HashMap 和 ConcurrentHashMap。

 

猜你喜欢

转载自blog.csdn.net/Ms_lang/article/details/81045407