Java 集合类知识点

1. 你了解哪些集合类型?

你应该知道如下几个最重要的类型:java

  • ArrayList
  • LinkedList
  • HashMap
  • HashSet

以后,你可能会被问到这样一些问题,好比应该什么时候使用此种特定类型,它比其余的好在哪里,它是怎么存储数据的以及隐匿在背后的数据结构是什么。最好的方法是尽量多地了解这些集合类型,由于这类问题几乎是无穷尽的。面试

2. HashMap 有什么特色?

HashMap 基于Map接口实现,存储键值对时,能够接收 null 为键值。HashMap 是非同步的。算法

3. HashMap 的工做原理是怎样的?

HashMap 在 Map.Entry 静态内部类实现中存储键值对,使用哈希算法。在 put 和 get 方法中,使用 hashCode() 和 equals() 方法。设计模式

  • 调用 put 方法时,使用键值对中的 Key hashCode() 和哈希算法找出存储键值对索引。键值对 Entry 存储在 LinkedList 中,若是存在 Entry,使用 equals() 方法来检查 Key 是否已经存在:若是存在,则覆盖 value;若是不存在,会建立一个新的 Entry 而后保存。
  • 调用 get 方法时,HashMap 使用键值 Key hashCode() 来找到数组中的索引,而后使用 equals() 方法找出正确的 Entry,返回 Entry 中的 Value。

HashMap 中容量、负荷系数和阀值是重要的参数。HashMap 默认的初始容量是32,负荷系数是0.75。阀值 = 负荷系数 x 容量。添加 Entry时,若是 Map 的大小 > 阀值,HashMap 会对 Map 的内容从新哈希,使用更大的容量(容量老是2的幂)。关于 JDK 中的 hash 算法实现以及由此引起的哈希碰撞现象(DDos攻击)均可能是面试的延伸问题。数组

4. 可否使用任何类做为 Map 的 key?

可使用任何类做为 Map 的 key,然而在使用以前,须要考虑如下几点:缓存

  • 若是类重写了 equals() 方法,也应该重写 hashCode() 方法。
  • 类的全部实例须要遵循与 equals() 和 hashCode() 相关的规则。
  • 若是一个类没有使用 equals(),不该该在 hashCode() 中使用它。
  • 用户自定义 Key 类最佳实践是使之为不可变的,这样 hashCode() 值能够被缓存起来,拥有更好的性能。不可变的类也能够确保 hashCode() 和 equals() 在将来不会改变,这样就会解决与可变相关的问题了。

若是有一个类 MyKey,在 HashMap 中使用它:安全

HashMap<MyKey, String> myHashMap = new HashMap<MyKey, String>();

//传递给 MyKey 的 name 参数被用于 equals() 和 hashCode() 中

MyKey key = new MyKey("Pankaj"); // 假设 hashCode=1234
myHashMap.put(key, "Value");


// 如下的代码会改变 key 的 hashCode() 和 equals() 值
key.setName("Amit"); // 假设新的 hashCode=7890

//下面会返回 null,由于 HashMap 会尝试查找存储一样索引的 key,而 key 已被改变了,匹配失败,返回 null
System.out.println(myHashMap.get(new MyKey("Pankaj")));

这就是为何 String 一般会用做 HashMap 的 Key,由于 String 的设计是不可变的(immutable)。数据结构

5. 插入数据时,ArrayList、LinkedList、Vector谁速度较快?

ArrayList、LinkedList、Vector 底层的实现都是使用数组方式存储数据。数组元素数大于实际存储的数据以便增长和插入元素,它们都容许直接按序号索引元素,可是插入元素要涉及数组元素移动等内存操做,因此索引数据快而插入数据慢。多线程

  • Vector 中的方法因为加了 synchronized 修饰,所以 Vector 是线程安全容器,但性能上较ArrayList差。
  • LinkedList 使用双向链表实现存储,按序号索引数据须要进行前向或后向遍历,但插入数据时只须要记录当前项的先后项便可,因此 LinkedList 插入速度较快。
6. 多线程场景下如何使用 ArrayList?

ArrayList 不是线程安全的,若是遇到多线程场景,能够经过 Collections 的 synchronizedList 方法将其转换成线程安全的容器后再使用。例如像下面这样:框架

List<String> synchronizedList = Collections.synchronizedList(list);

synchronizedList.add("aaa");

synchronizedList.add("bbb");

for (int i = 0; i < synchronizedList.size(); i++)

{

    System.out.println(synchronizedList.get(i));

}

使用Collections 的 synchronizedList 方法将其转换成线程安全的容器后再使用。

7. 说一下 ArrayList 的优缺点

ArrayList的优势以下:

  • ArrayList 底层以数组实现,是一种随机访问模式。ArrayList 实现了 RandomAccess 接口,所以查找的时候很是快。
  • ArrayList 在顺序添加一个元素的时候很是方便。

ArrayList 的缺点以下:

  • 删除元素的时候,须要作一次元素复制操做。若是要复制的元素不少,那么就会比较耗费性能。
  • 插入元素的时候,也须要作一次元素复制操做,缺点同上。

ArrayList 比较适合顺序添加、随机访问的场景。

8. 为何 ArrayList 的 elementData 加上 transient 修饰?

ArrayList 中的数组定义以下:

private transient Object[] elementData;

再看一下 ArrayList 的定义:

public class ArrayList<E> extends AbstractList<E>
        implements List<E>, RandomAccess, Cloneable, java.io.Serializable

能够看到 ArrayList 实现了 Serializable 接口,这意味着 ArrayList 支持序列化。transient 的做用是说不但愿 elementData 数组被序列化,重写了 writeObject 实现:

private void writeObject(java.io.ObjectOutputStream s) throws java.io.IOException{
    // Write out element count, and any hidden stuff
    int expectedModCount = modCount;
    s.defaultWriteObject();
        // Write out array length
    s.writeInt(elementData.length);
        // Write out all elements in the proper order.
    for (int i=0; i<size; i++)
        s.writeObject(elementData[i]);
    if (modCount != expectedModCount) {
        throw new ConcurrentModificationException();
    }

每次序列化时,先调用 defaultWriteObject() 方法序列化 ArrayList 中的非 transient 元素,而后遍历 elementData,只序列化已存入的元素,这样既加快了序列化的速度,又减少了序列化以后的文件大小。

9. 遍历一个 List 有哪些不一样的方式?每种方法的实现原理是什么?Java 中 List 遍历的最佳实践是什么?

遍历方式有如下几种:

  • for 循环遍历,基于计数器。在集合外部维护一个计数器,而后依次读取每个位置的元素,当读取到最后一个元素后中止。
  • 迭代器遍历,Iterator。Iterator 是面向对象的一个设计模式,目的是屏蔽不一样数据集合的特色,统一遍历集合的接口。Java 在 Collections 中支持了 Iterator 模式。
  • foreach 循环遍历。foreach 内部也是采用了 Iterator 的方式实现,使用时不须要显式声明 Iterator 或计数器。优势是代码简洁,不易出错;缺点是只能作简单的遍历,不能在遍历过程当中操做数据集合,例如删除、替换。

最佳实践:Java Collections 框架中提供了一个 RandomAccess 接口,用来标记 List 实现是否支持 Random Access。

  • 若是一个数据集合实现了该接口,就意味着它支持 Random Access,按位置读取元素的平均时间复杂度为 O(1),如ArrayList。
  • 若是没有实现该接口,表示不支持 Random Access,如LinkedList。

推荐的作法就是,支持 Random Access 的列表可用 for 循环遍历,不然建议用 Iterator 或 foreach 遍历。

10. 如何边遍历边移除 Collection 中的元素?

边遍历边修改 Collection 的惟一正确方式是使用 Iterator.remove() 方法,以下:

Iterator<Integer> it = list.iterator();
while(it.hasNext()){
    // do something
    it.remove();
}

一种最多见的错误代码以下:

for(Integer i : list){
    list.remove(i)
}

运行以上错误代码会报 ConcurrentModificationException 异常。这是由于当使用 foreach(for(Integer i : list)) 语句时,会自动生成一个iterator 来遍历该 list,但同时该 list 正在被 Iterator.remove() 修改。Java 通常不容许一个线程在遍历 Collection 时另外一个线程修改它。


原文地址:https://mp.weixin.qq.com/s/Im3JbJk2IuzlSXenoLQK6Q

相关文章
相关标签/搜索