Java面试题-容器

1. 说说常见的集合有哪些?
  • Collection
  • List
    ArrayList
    LinkedList
    Vector
    Stack
  • Set
    HashSet
    LinkedHashSet
    TreeSet
  • Map
    HashMap
    LinkedHashMap
    TreeMap
    ConcurrentHashMap
    Hashtable
2. 哪些集合类可对元素随机访问?

    ArrayList、HashMap、TreeMap、Hashtable 类提供对元素的随机访问。

3. Comparable 和 Comparator 接口的区别?
  • Comparable 和 Comparator 接口用来对对象集合或者数组进行排序;
  • Comparable 接口用来提供对象的自然排序,我们可以使用它来提供基于单个逻辑的排序;
  • Comparator 用来提供不同的排序算法,我们可以选择需要使用的 Comparator 来对给定的对象集合进行排序;
4. Collection 和 Collections的区别?
  • Collection 是一个集合接口,它提供了对集合对象进行基本操作的通用接口方法,所有集合都是它的子类,比如 List、Set 等。
  • Collections 是一个包装类,包含了很多静态方法,不能被实例化,就像一个工具类,比如提供的排序方法: Collections. sort(list)。
5. Enumeration 和 Iterator 接口的区别?
  • Enumeration 只能读取集合的数据,而不能对数据进行修改,Iterator除了能读取集合的数据之外,还能删除集合中的数据;
  • 与 Enumeration 相比,Iterator更加安全,因为当一个集合正在遍历的时候,它会阻止其他线程去修改集合(fail-fast机制);
6. 集合使用范型有什么优点?
  • 范型规定了一个集合中可以容纳的对象类型,添加其他类型的对象将编译失败;
  • 避免了运行时可能出现的 ClassCastException 异常;
  • 范型也使得代码整洁,我们不需要使用显式转换和instanceOf操作符;
  • 它也给运行时带来了好处,因为不会产生类型检查的字节码指令;
7. List、Set、Map 之间的区别是什么?

在这里插入图片描述

8. 为什么 Map 接口不继承 Collection 接口?

    尽管 Map 接口和它的实现也是集合框架的一部分,但 Map 不是集合,集合也不是 Map ,因此 Map 继承 Collection 是毫无意义的。
    如果 Map 继承 Collection 接口,那么元素去哪儿?Map 包含 key-value 对,它提供抽取 key 或 value 列表集合的方法,但是它不适合“一组对象”规范。

9. 常用的线程安全的 Map 有哪些?
  • Hashtable
    Hashtable的 get/put 方法都被 synchronized 修饰,说明他们是方法级别阻塞的,他们占用共享资源锁,效率低,不推荐使用;
  • SynchronizedMap
    使用 Collections 工具类创建出来的同步集合,通过对象锁实现,每次调用方法必须先获取对象锁,效率低,不推荐使用;
  • ConcurrentHashMap
    JKD1.7 之前使用分段锁方法,分成 16 个桶,每次只加锁其中一个桶,而在 JDK1.8 中又加入了红黑树和 CAS 算法,同步效率很高,推荐使用;
10. HashMap 与 Hashtable 的区别?
  • 存储:HashMap 允许一个 key 和 多个 value 为 null,而 Hashtable 不允许。
  • 线程安全:Hashtable 是线程安全的,而 HashMap 是非线程安全的。
  • 推荐使用:在 Hashtable 的类注释可以看到,Hashtable 是保留类不建议使用,推荐在单线程环境下使用 HashMap 替代,如果需要多线程使用则用 ConcurrentHashMap 替代。
11. HashMap 与 TreeMap 怎么选?
  • 对于在 Map 中插入、删除、定位一个元素这类操作,HashMap 是最好的 选择,因为相对而言 HashMap 的插入会更快;
  • 如果你要对一个 key 集合进行有序的遍历,那 TreeMap 是更好的选择;
12. HashMap 的数据结构是什么?
  • JDK 1.7:数据 + 链表
  • JDK 1.8:数据 + 链表 + 红黑树(如果数组的长度大于 64 并且链表的长度大于 8 将转换为红黑树)
13. HashMap 在 JDK 8 中有哪些改变?
  • 在 JDK 1.8 中,如果链表长度超过了 8,那么链表将转换为红黑树(桶数组长度必须大于 64,小于 64 只会扩容);
  • 发生 hash 碰撞时,JDK 1.7 会在链表的头部插入,而 JDK 1.8 会在链表的尾部插入;
  • 在 JDK 1.8 中,Entry 被 Node 替代;
14. HashMap 的 put 方法逻辑?
  • 通过hash()函数对key进行hash运算得到当前key的hash值;
  • 通过indexFor(hash, table.length)函数获取在table中的实际位置;
  • 如果当前位置为空,则创建新的Entry对象并插入实际位置;
  • 如果当前位置不为空,则遍历当前链表,如果对应数据已经存在则覆盖如果对应数据不存在则创建新的Entry对象并将当前链表最后一个元素的next指针指向新创建的对象;

源码:

public V put(K key, V value) {
    //如果table数组为空数组{},进行数组填充(为table分配实际内存空间),入参为threshold,此时threshold为initialCapacity 默认是1<<4(24=16)
    if (table == EMPTY_TABLE) {
        inflateTable(threshold);
    }
   //如果key为null,存储位置为table[0]或table[0]的冲突链上
    if (key == null)
        return putForNullKey(value);
    int hash = hash(key);//对key的hashcode进一步计算,确保散列均匀
    int i = indexFor(hash, table.length);//获取在table中的实际位置
    for (Entry<K,V> e = table[i]; e != null; e = e.next) {
    //如果该对应数据已存在,执行覆盖操作。用新value替换旧value,并返回旧value
        Object k;
        if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
            V oldValue = e.value;
            e.value = value;
            e.recordAccess(this);
            return oldValue;
        }
    }
    modCount++;//保证并发访问时,若HashMap内部结构发生变化,快速响应失败
    addEntry(hash, key, value, i);//新增一个entry
    return null;
}

final int hash(Object k) {
    int h = hashSeed;
    if (0 != h && k instanceof String) {
        return sun.misc.Hashing.stringHash32((String) k);
    }

    h ^= k.hashCode();

    h ^= (h >>> 20) ^ (h >>> 12);
    return h ^ (h >>> 7) ^ (h >>> 4);
}

static int indexFor(int h, int length) {
    return h & (length-1);//位运算
}

put方法流程给图:
在这里插入图片描述

15. HashMap 的 get 方法逻辑?
  • 通过hash()函数对key进行hash运算得到当前key的hash值;
  • 通过indexFor(hash, table.length)函数获取在table中的实际位置;
  • 比较参数key的hash值和当前位置元素的hash值及key的内容是否相等,如果相等则直接返回该元素,否则遍历当前链表。

源码:

public V get(Object key) {
    Node<K,V> e;
    return (e = getNode(hash(key), key)) == null ? null : e.value;
}

final Node<K,V> getNode(int hash, Object key) {
    Node<K,V>[] tab; Node<K,V> first, e; int n; K k;
    if ((tab = table) != null && (n = tab.length) > 0 &&
        (first = tab[(n - 1) & hash]) != null) {//获取的下标位置不为空
        if (first.hash == hash && //总是检查第一个元素是否相等
            ((k = first.key) == key || (key != null && key.equals(k))))
            return first;
        if ((e = first.next) != null) {//如果该链表有下一个节点
            if (first instanceof TreeNode)//如果是红黑树,则调用getTreeNode
                return ((TreeNode<K,V>)first).getTreeNode(hash, key);
            do {//遍历当前链表获取元素
                if (e.hash == hash &&
                    ((k = e.key) == key || (key != null && key.equals(k))))
                    return e;
            } while ((e = e.next) != null);
        }
    }
    return null;
}
16. HashMap 是线程安全的吗?
  • 当用在方法内部的局部变量时,局部变量属于当前线程级别的变量,其他线程访问不了,所以不存在线程安全问题;
  • 如果是成员变量,则不是线程安全的,两条线程的 put 操作可能发生覆盖(当两个值的 hashCode 相同时);
17. HashMap 是怎么解决 hash 冲突的?

    HashMap 采用了一种链表数据结构来解决 hash 冲突的情况,当两个对象的 hashCode 相同时,它们会放到当前数组索引位置的链表中。

18. HashMap 是怎么扩容的?
  • table 数组的大小是由 capacity 这个参数确定的,默认是 16,也可以构造传入,最大限制是 1 << 30;
  • loadFactor 是装载因子,主要目的是用来确认 table 数组是否需要动态扩展,默认值是 0.75,如 table 数组大小为16,装载因子为 0.75 时, threshould 就是 12,当 table 的实际大小超过 12 时,table 就需要动态扩容;
  • 扩容时,调用 resize() 方法,将 table 长度变为原来的两倍(注意是 table 长度,而不是 threshold);
  • 如果数据很大的情况下,扩展时将会带来性能的损失,在性能要求很高的地方,这种损失很可能是致命的;

源码:

void addEntry(int hash, K key, V value, int bucketIndex) {
    if ((size >= threshold) && (null != table[bucketIndex])) {
        resize(2 * table.length);//当size超过临界阈值threshold,并且即将发生哈希冲突时进行扩容
        hash = (null != key) ? hash(key) : 0;
        bucketIndex = indexFor(hash, table.length);
    }

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

void resize(int newCapacity) {
    Entry[] oldTable = table;
    int oldCapacity = oldTable.length;
    if (oldCapacity == MAXIMUM_CAPACITY) {
        threshold = Integer.MAX_VALUE;
        return;
    }

    Entry[] newTable = new Entry[newCapacity];
    transfer(newTable, initHashSeedAsNeeded(newCapacity));
    table = newTable;
    threshold = (int)Math.min(newCapacity * loadFactor, MAXIMUM_CAPACITY + 1);
}

void transfer(Entry[] newTable, boolean rehash) {
    int newCapacity = newTable.length;
    for (Entry<K,V> e : table) {//for循环中的代码,逐个遍历链表,重新计算索引位置,将老数组数据复制到新数组中去
        while(null != e) {
            Entry<K,V> next = e.next;
            if (rehash) {
                e.hash = null == e.key ? 0 : hash(e.key);
            }
            int i = indexFor(e.hash, newCapacity);
            e.next = newTable[i];//将当前entry的next链指向新的索引位置,newTable[i]有可能为空,有可能也是个entry链,如果是entry链,直接在链表头部插入。
            newTable[i] = e;
            e = next;
        }
    }
}
19. 为何HashMap的数组长度一定是2的次幂?
  • 获取元素下表位置的方式为:(n - 1) & hash,这个操作如果在 n 为 2 的 N 次幂的情况下是等同于 hash % n 取余数的值;
  • 至于为什么要使用与(&)运算呢:因为与运算的效率要高于 hash % n 取余的运算,这也就解释了为什么 HashMap 的数组长度是 2 的 N 次幂;
20. HashMap 是如何实现同步的?
  • 使用 Collections.synchronizedMap(…) 来同步 Map;
  • 使用 ConcurrentHashMap;
21. Hashtable 为什么不叫 HashTable?

    Hashtable 实在 JDK 1.0 的时候创建的,而集合统一规范命名是在后来的 JDK 1.2 开始约定的,为了兼容老版本所以就没有改变;

22. ConcurrentHashMap 的数据结构?
  • JDK 1.7 中,采用分段锁机制,实现并发更新操作,底层采用 数组 + 链表 的存储结构,包括两个核心静态内部类 Segment 和 HashEntry。
    1)Segment 继承 ReentrantLock(可重入锁)用来充当锁的角色,每个Segment 对象守护每个散列表的若干个桶,Segment 数组默认大小为 16,并且不会扩容。
    2)HashEntry 用来封装映射表的键-值对;
    3)每个桶是由若干个 HashEntry 对象链接起来的链表;
  • JDK 1.8 中,采用 Node + CAS + Synchronized 来保证并发安全。取消类 Segment,直接用 table 数组存储键值对;当 Node 对象组成的链表长度超过 TREEIFY_THRESHOLD 时,链表转换为红黑树,提升性能。底层变更为数组 + 链表 + 红黑树。
23. ArrayList 是线程安全的吗?

    不是线程安全的,多线程操作时可能存在以下问题:

  • 发生 ArrayIndexOutOfBoundsException异常;
  • add 时程序正常运行,结果实际存储的数据少于存入的数据;
24. 常用的线程安全的 List 集合有哪些?
  • Vector
  • SynchronizedList
  • CopyOnWriteArrayList
    CopyOnWriteArrayList 和 CopyOnWriteArraySet 是在 JDK 1.5 时加入的在 java.util.concurrent 包下。
25. 循环删除 List 集合可能会发生什么异常?
  • ArrayIndexOutOfBoundsException:数组下标越界异常。
  • ConcurrentModificationException:使用增强 for 循环遍历删除时会报该异常,通过 Iterator 遍历时则不会,因为取下个元素时会判断要修改的数量和期待修改的数量是否一致,不一致会报错,而迭代器的remove方法会同步该值。
26. ArrayList 和 LinkedList 的区别?
  • 数据结构实现:ArrayList 是动态数组的数据结构实现,而 LinkedList 是双向链表的数据结构实现。

  • 随机访问效率:ArrayList 比 LinkedList 在随机访问的时候效率要高,因为 LinkedList 是线性的数据存储方式,所以需要移动指针从前往后依次查找。

  • 增加和删除效率:在非首尾的增加和删除操作,LinkedList 要比 ArrayList 效率要高,因为 ArrayList 增删操作要影响数组内的其他数据的下标。

    综合来说,在需要频繁读取集合中的元素时,更推荐使用 ArrayList,而在插入和删除操作较多时,更推荐使用 LinkedList。

27. ArrayList 和 Vector 的区别?
  • 线程安全:Vector 使用了 Synchronized 来实现线程同步,是线程安全的,而 ArrayList 是非线程安全的。
  • 性能:ArrayList 在性能方面要优于 Vector。
  • 扩容:ArrayList 和 Vector 都会根据实际的需要动态的调整容量,只不过在 Vector 扩容每次会增加 1 倍,而 ArrayList 只会增加 50%。
28.如何实现数组和 List 之间的转换?
  • 数组转 List:使用 Arrays. asList(array) 进行转换。
  • List 转数组:使用 List 自带的 toArray() 方法。

例如:

List<String> list = new ArrayList<>();
list.add("abc");
list.add("bcd");
Object[] objects = list.toArray();
for (Object object : objects) {
    System.out.println(object);
}

List<String> list1 = Arrays.asList("abc", "bcd");
System.out.println(list1.toString());
29.Iterator 和 ListIterator 有什么区别?
  • Iterator 可以遍历 Set 和 List 集合,而 ListIterator 只能遍历 List。
  • Iterator 只能单向遍历,而 ListIterator 可以双向遍历(向前/后遍历)。
  • ListIterator 从 Iterator 接口继承,然后添加了一些额外的功能,比如添加一个元素、替换一个元素、获取前面或后面元素的索引位置。
30.怎么确保一个集合不能被修改?

    可以使用 Collections. unmodifiableCollection(Collection c) 方法来创建一个只读集合,这样改变集合的任何操作都会抛出 Java. lang. UnsupportedOperationException 异常。

List<String> list = new ArrayList<>();
list. add("x");
Collection<String> clist = Collections. unmodifiableCollection(list);
clist. add("y"); // 运行时此行报错
System. out. println(list. size());
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值