《面试题》集合容器

这篇博客深入探讨了Java集合框架中的关键概念,包括HashMap与Hashtable的区别、Collection与Collections的不同、ArrayList、Vector和LinkedList的存储性能与特性。此外,还详细阐述了集合框架的底层数据结构,如ArrayList的扩容机制,以及各种集合接口如List、Set、Map的实现与特性。博客还讨论了迭代器、线程安全、排序和删除元素的方法,以及Comparable和Comparator接口的作用。
摘要由CSDN通过智能技术生成


HashMap和Hashtable的区别。

  1. 继承结构不同。HashMap继承AbstractMap,HashTable继承Dictionary。
  2. 初始容量不同,HashMap初始大小为16,以2的幂次方增长。HashTable默认初始大小为11,自动扩容时 新容量=原来数组容量 * 2 + 1。
  3. 允许空键空值不同。HashMap允许,但HashTable不允许value为null
  4. 线程安全,HashMap是非线程安全的映射。HashTable内的方法用synchronized修饰了。

Collection 和 Collections的区别。

Colletion是集合类的顶级接口,给具体类提供一些统一方法,其直接的继承接口有List、Set、Queue。
Collections是java.util.Colletions集合的工具类,不属于集合框架。不能被实例化,内部提供了一些静态方法用于集合操作。比如排序、反转、二分查找、填充、拷贝、交换元素、将集合包装成线程安全的集合等。

说出ArrayList,Vector, LinkedList的存储性能和特性?

你所知道的集合类都有哪些?主要方法?

Java 集合框架的基础接口有哪些?

Collection ,为集合层级的根接口。一个集合代表一组对象,这些对象即为它的元素。Java 平台不提供这个接口任何直接的实现。

  • Set ,是一个不能包含重复元素的集合。这个接口对数学集合抽象进行建模,被用来代表集合,就如一副牌。

  • List ,是一个有序集合,可以包含重复元素。你可以通过它的索引来访问任何元素。List 更像长度动态变换的数组。

    Map ,是一个将 key 映射到value 的对象。一个 Map 不能包含重复的 key,每个 key 最多只能映射一个 value 。
    一些其它的接口有 Queue、Dequeue、SortedSet、SortedMap 和 ListIterator 。

Collection 和 Collections 的区别?

Collection ,是集合类的上级接口,继承与他的接口主要有 Set 和List 。
Collections ,是针对集合类的一个工具类,它提供一系列静态方法实现对各种集合的搜索、排序、线程安全化等操作。

集合框架底层数据结构总结

1)List

ArrayList :Object 数组。
Vector :Object 数组。
LinkedList :双向链表

2)Map

HashMap :
JDK8 之前,HashMap 由数组+链表组成的,数组是HashMap的主体,链表则是主要为了解决哈希冲突而存在的(“拉链法”解决冲突)。
JDK8 以后,在解决哈希冲突时有了较大的变化,当链表长度大于阈值(默认为 8 )时,将链表转化为红黑树,以减少搜索时间。

LinkedHashMap :LinkedHashMap 继承自 HashMap,所以它的底层仍然是基于拉链式散列结构即由数组和链表或红黑树组成。另外,LinkedHashMap 在上面结构的基础上,增加了一条双向链表,使得上面的结构可以保持键值对的插入顺序。同时通过对链表进行相应的操作,实现了访问顺序相关逻辑。

Hashtable :数组+链表组成的,数组是 HashMap 的主体,链表则是主要为了解决哈希冲突而存在的。

TreeMap :红黑树(自平衡的排序二叉树)。

3)Set

HashSet :无序,唯一,基于 HashMap 实现的,底层采用 HashMap 来保存元素。
LinkedHashSet :LinkedHashSet 继承自 HashSet,并且其内部是通过 LinkedHashMap 来实现的。有点类似于我们之前说的LinkedHashMap 其内部是基于 HashMap 实现一样,不过还是有一点点区别的。
TreeSet :有序,唯一,红黑树(自平衡的排序二叉树)。

什么是迭代器(Iterator)?

Iterator 接口,提供了很多对集合元素进行迭代的方法。每一个集合类都包含了可以返回迭代器实例的迭代方法。迭代器可以在迭代的过程中删除底层集合的元素,但是不可以直接调用集合的 #remove(Object Obj) 方法删除,可以通过迭代器的 #remove() 方法删除。

Iterator 和 ListIterator 的区别是什么?

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

快速失败(fail-fast)和安全失败(fail-safe)的区别是什么?

差别在于 ConcurrentModification 异常:

  • 快速失败:当你在迭代一个集合的时候,如果有另一个线程正在修改你正在访问的那个集合时,就会抛出一个 ConcurrentModification 异常。 在 java.util 包下的都是快速失败。
  • 安全失败:你在迭代的时候会去底层集合做一个拷贝,所以你在修改上层集合的时候是不会受影响的,不会抛出 ConcurrentModification 异常。在 java.util.concurrent 包下的全是安全失败的。

如何删除 List 中的某个元素?

有两种方式,分别如下:

  • 方式一,使用 Iterator ,顺序向下,如果找到元素,则使用迭代器的 remove 方法进行移除。
  • 方式二,倒序遍历 List ,普通for循环遍历找到元素,则使用 remove 方法进行移除。(防止索引乱序)

Enumeration 和 Iterator 接口有什么不同?

  • Enumeration 跟 Iterator 相比较快两倍,而且占用更少的内存。
  • 但是,Iterator 相对于 Enumeration 更安全,因为其他线程不能修改当前迭代器遍历的集合对象。同时,Iterators 允许调用者从底层集合中移除元素,这些 Enumerations 都没法完成。

对于很多胖友,可能并未使用过 Enumeration 类,所以可以看看 《Java Enumeration 接口》 文章。

为何 Iterator 接口没有具体的实现?

Iterator 接口,定义了遍历集合的方法,但它的实现则是集合实现类的责任。每个能够返回用于遍历的 Iterator 的集合类都有它自己的 Iterator 实现内部类。

这就允许集合类去选择迭代器是 fail-fast 还是 fail-safe 的。比如,ArrayList 迭代器是 fail-fast 的,而 CopyOnWriteArrayList 迭代器是 fail-safe 的。(CopyOnWriteArrayList 就是在底层将数组拷贝一份)

Comparable 和 Comparator 的区别?

  • Comparable 接口,在 java.lang 包下,用于当前对象和其它对象的比较,所以它有一个 #compareTo(Object obj) 方法用来排序,该方法只有一个参数。
  • Comparator 接口,在 java.util 包下,用于传入的两个对象的比较,所以它有一个 #compare(Object obj1, Object obj2) 方法用来排序,该方法有两个参数。

详细的,可以看看 《Java 自定义比较器》 文章,重点是如何自己实现 Comparable 和 Comparator 的方法。

? compareTo 方法的返回值表示的意思?

  • 大于 0 ,表示对象大于参数对象。
  • 小于 0 ,表示对象小于参数对象
  • 等于 0 ,表示两者相等。

如何对 Object 的 List 排序?

  • Object[] 数组进行排序时,我们可以用 Arrays#sort(...) 方法。
  • List<Object> 数组进行排序时,我们可以用 Collections#sort(...) 方法。

List 和 Set 区别?

List,Set 都是继承自 Collection 接口。

  • List 特点:元素有放入顺序,元素可重复。
  • Set 特点:元素无放入顺序,元素不可重复,重复元素会覆盖掉。

注意:元素虽然无放入顺序,但是元素在 Set 中的位置是有该元素的 hashcode 决定的,其位置其实是固定的。

另外 List 支持 for 循环,也就是通过下标来遍历,也可以用迭代器,但是 Set 只能用迭代,因为他无序,无法用下标来取得想要的值。

Set 和 List 对比:

  • Set:检索元素效率高,删除和插入效率低,插入和删除不会引起元素位置改变。
  • List:和数组类似,List 可以动态增长,查找元素效率低,插入删除元素效率,因为可能会引起其他元素位置改变。

ArrayList 与 LinkedList 区别?

? ArrayList

  • 优点:ArrayList 是实现了基于动态数组的数据结构,因为地址连续,一旦数据存储好了,查询操作效率会比较高(在内存里是连着放的)。
  • 缺点:因为地址连续,ArrayList 要移动数据,所以插入和删除操作效率比较低。

? LinkedList

  • 优点:LinkedList 基于链表的数据结构,地址是任意的,所以在开辟内存空间的时候不需要等一个连续的地址。对于新增和删除操作 add 和 remove ,LinedList 比较占优势。LinkedList 适用于要头尾操作或插入指定位置的场景。
  • 缺点:因为 LinkedList 要移动指针,所以查询操作性能比较低。

ArrayList 是如何扩容的?

直接看 《ArrayList 动态扩容详解》 文章,很详细。主要结论如下:

  • 如果通过无参构造的话,初始数组容量为 0 ,当真正对数组进行添加时,才真正分配容量。每次按照 1.5 倍(位运算)的比率通过 copeOf 的方式扩容。
  • 在 JKD6 中实现是,如果通过无参构造的话,初始数组容量为10,每次通过 copeOf 的方式扩容后容量为原来的 1.5 倍。

重点是 1.5 倍扩容,这是和 HashMap 2 倍扩容不同的地方。

ArrayList 与 Vector 区别?

ArrayList 和 Vector 都是用数组实现的,主要有这么三个区别:

  • 1、Vector 是多线程安全的,线程安全就是说多线程访问同一代码,不会产生不确定的结果,而 ArrayList 不是。这个可以从源码中看出,Vector 类中的方法很多有 synchronized 进行修饰,这样就导致了 Vector 在效率上无法与 ArrayList 相比。

    Vector 是一种老的动态数组,是线程同步的,效率很低,一般不赞成使用。

  • 2、两个都是采用的线性连续空间存储元素,但是当空间不足的时候,两个类的增加方式是不同。

  • 3、Vector 可以设置增长因子,而 ArrayList 不可以。

适用场景分析:

  • 1、Vector 是线程同步的,所以它也是线程安全的,而 ArrayList 是线程无需同步的,是不安全的。如果不考虑到线程的安全因素,一般用 ArrayList 效率比较高。

    实际场景下,如果需要多线程访问安全的数组,使用 CopyOnWriteArrayList 。

  • 2、如果集合中的元素的数目大于目前集合数组的长度时,在集合中使用数据量比较大的数据,用 Vector 有一定的优势。

    这种情况下,使用 LinkedList 更合适。

HashMap 和 Hashtable 的区别?

Hashtable 是在 Java 1.0 的时候创建的,而集合的统一规范命名是在后来的 Java2.0 开始约定的,而当时其他一部分集合类的发布构成了新的集合框架。

  • Hashtable 继承 Dictionary ,HashMap 继承的是 Java2 出现的 Map 接口。
  • 2、HashMap 去掉了 Hashtable 的 contains 方法,但是加上了 containsValue 和 containsKey 方法。
  • 3、HashMap 允许空键值,而 Hashtable 不允许。
  • 【重点】4、HashTable 是同步的,而 HashMap 是非同步的,效率上比 HashTable 要高。也因此,HashMap 更适合于单线程环境,而 HashTable 适合于多线程环境。
  • 5、HashMap 的迭代器(Iterator)是 fail-fast 迭代器,HashTable的 enumerator 迭代器不是 fail-fast 的。
  • 6、HashTable 中数组默认大小是 11 ,扩容方法是 old * 2 + 1 ,HashMap 默认大小是 16 ,扩容每次为 2 的指数大小。

一般现在不建议用 HashTable 。主要原因是两点:

  • 一是,HashTable 是遗留类,内部实现很多没优化和冗余。
  • 二是,即使在多线程环境下,现在也有同步的 ConcurrentHashMap 替代,没有必要因为是多线程而用 Hashtable 。

Hashtable 的 #size() 方法中明明只有一条语句 “return count;” ,为什么还要做同步?

同一时间只能有一条线程执行固定类的同步方法,但是对于类的非同步方法,可以多条线程同时访问。所以,这样就有问题了,可能线程 A 在执行 Hashtable 的 put 方法添加数据,线程 B 则可以正常调用 #size() 方法读取 Hashtable 中当前元素的个数,那读取到的值可能不是最新的,可能线程 A 添加了完了数据,但是没有对 count++ ,线程 B 就已经读取 count 了,那么对于线程 B 来说读取到的 count 一定是不准确的。

而给 #size() 方法加了同步之后,意味着线程 B 调用 #size() 方法只有在线程 A 调用 put 方法完毕之后才可以调用,这样就保证了线程安全性

HashSet 和 HashMap 的区别?

  • Set 是线性结构,值不能重复。HashSet 是 Set 的 hash 实现,HashSet 中值不能重复是用 HashMap 的 key 来实现的。

  • Map 是键值对映射,可以空键空值。HashMap 是 Map 的 hash 实现,key 的唯一性是通过 key 值 hashcode 的唯一来确定,value 值是则是链表结构。

    因为不同的 key 值,可能有相同的 hashcode ,所以 value 值需要是链表结构。

他们的共同点都是 hash 算法实现的唯一性,他们都不能持有基本类型,只能持有对象。

为了更好的性能,Netty 自己实现了 key 为基本类型的 HashMap ,例如 IntObjectHashMap

如何决定选用 HashMap 还是 TreeMap?

  • 对于在 Map 中插入、删除和定位元素这类操作,HashMap 是最好的选择。
  • 然而,假如你需要对一个有序的 key 集合进行遍历, TreeMap 是更好的选择。

基于你的 collection 的大小,也许向 HashMap 中添加元素会更快,再将 HashMap 换为 TreeMap 进行有序 key 的遍历。

HashMap 的工作原理是什么?

我们知道在 Java 中最常用的两种结构是数组和模拟指针(引用),几乎所有的数据结构都可以利用这两种来组合实现,HashMap 也是如此。实际上 HashMap 是一个链表散列

HashMap 是基于 hashing 的原理。

HashMap 图解

  • 我们使用 #put(key, value) 方法来存储对象到 HashMap 中,使用 get(key) 方法从 HashMap 中获取对象。
  • 当我们给 #put(key, value) 方法传递键和值时,我们先对键调用 #hashCode() 方法,返回的 hashCode 用于找到 bucket 位置来储存 Entry 对象。

? 当两个对象的 hashCode 相同会发生什么?

因为 hashcode 相同,所以它们的 bucket 位置相同,“碰撞”会发生。

因为 HashMap 使用链表存储对象,这个 Entry(包含有键值对的 Map.Entry 对象)会存储在链表中。

? hashCode 和 equals 方法有何重要性?

HashMap 使用 key 对象的 #hashCode()#equals(Object obj) 方法去决定 key-value 对的索引。当我们试着从 HashMap 中获取值的时候,这些方法也会被用到。

  • 如果这两个方法没有被正确地实现,在这种情况下,两个不同 Key 也许会产生相同的 #hashCode()#equals(Object obj) 输出,HashMap 将会认为它们是相同的,然后覆盖它们,而非把它们存储到不同的地方。

同样的,所有不允许存储重复数据的集合类都使用 #hashCode()#equals(Object obj) 去查找重复,所以正确实现它们非常重要。#hashCode()#equals(Object obj) 方法的实现,应该遵循以下规则:

  • 如果 o1.equals(o2) ,那么 o1.hashCode() == o2.hashCode() 总是为 true 的。
  • 如果 o1.hashCode() == o2.hashCode() ,并不意味 o1.equals(o2) 会为 true

我们能否使用任何类作为 Map 的 key?

我们可以使用任何类作为 Map 的 key ,然而在使用它们之前,需要考虑以下几点:

  • 1、如果类重写了 equals 方法,它也应该重写 hashcode 方法。

  • 2、类的所有实例需要遵循与 equals 和 hashcode 相关的规则。

  • 3、如果一个类没有使用 equals ,你不应该在 hashcode 中使用它。

  • 4、用户自定义 key 类的最佳实践是使之为不可变的,这样,hashcode 值可以被缓存起来,拥有更好的性能。不可变的类也可以确保hashcode 和 equals 在未来不会改变,这样就会解决与可变相关的问题了。

    比如,我有一个 类MyKey ,在 HashMap 中使用它。代码如下:

    //传递给MyKey的name参数被用于equals()和hashCode()中
    MyKey key = new MyKey('Pankaj'); //assume hashCode=1234
    myHashMap.put(key, 'Value');
    // 以下的代码会改变key的hashCode()和equals()值
    key.setName('Amit'); //assume new hashCode=7890
    //下面会返回null,因为HashMap会尝试查找存储同样索引的key,而key已被改变了,匹配失败,返回null
    myHashMap.get(new MyKey('Pankaj'));
    
    
    
    • 这就是为何 String 和 Integer 被作为 HashMap 的 key 大量使用。

HashSet 的工作原理是什么?

HashSet 是构建在 HashMap 之上的 Set hashing 实现类。让我们直接撸下源码,代码如下:

HashSet 如何检查重复?

艿艿:正如我们上面看到 HashSet 的实现原理,我们自然可以推导出,HashMap 也是如何检查重复滴。

如下摘取自 《Head First Java》 第二版:

当你把对象加入 HashSet 时,HashSet会先计算对象的hashcode值来判断对象加入的位置,同时也会与其他加入的对象的hashcode值作比较。

  • 如果没有相符的 hashcode ,HashSet会假设对象没有重复出现。
  • 但是如果发现有相同 hashcode 值的对象,这时会调用 equals 方法来检查 hashcode 相等的对象是否真的相同。
    • 如果两者相同,HashSet 就不会让加入操作成功。
    • 如果两者不同,HashSet 就会让加入操作成功
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

_popo_

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值