Java面试 4.9 容器

4.9.1 Java Collections 框架是什么

Java Collections 框架中包含了大量集合接口以及这些接口的实现类和操作它们的算法(例如排序、查找、反转、替换、复制、取最小元素、取最大元素等),具体而言,主要提供了 List(列表)、Queue(队列)、Set(集合)、Stack(栈)和 Map(映射表,用于存放键值对)等数据结构。其中,List、Queue、Set、Stack 都继承自 Collection 接口。

下面分别介绍 Set、List 和 Map 3 个接口。

1)Set 表示数学意义上的集合概念。其最主要的特点是集合中的元素不能重复,因此存入 Set 的每个元素都必须定义 equals()方法来确保对象的唯一性。该接口有两个实现类:Hash-Set 和 TreeSet。其中 TreeSet 实现了 SortedSet 接口,因此 TreeSet 容器中的元素是有序的。

2)List 又称为有序的 Collection。它按对象进入的顺序保存对象,所以它能对列表中的每个元素的插入和删除位置进行精确的控制。同时,它可以保存重复的对象。LinkedList、Array-List 和 Vector 都实现了 List 接口。

3)Map 提供了一个从键映射到值的数据结构。它用于保存键值对,其中值可以重复,但键是唯一的,不能重复。Java 类库中有多个实现该接口的类:HashMap、TreeMap、Linked-HashMap、WeakHashMap 和 IdentityHashMap。虽然它们都实现了相同的接口,但执行效率却不是完全相同的。具体而言,HashMap 是基于散列表实现的,采用对象的 HashCode 可以进行快速查询。LinkedHashMap 采用列表来维护内部的顺序。TreeMap 基于红黑树的数据结构来实现的,内部元素是按需排列的。

 Map 是一个接口,不能直接实例化 Map 的对象,但是可以实例化实现 Map 接口的类的对象,例如 Map m=new HashMap()


4.9.2 什么是迭代器

迭代器(Iterator)是一个对象,它的工作是遍历并选择序列中的对象,它提供了一种访问一个容器(container)对象中的各个元素,而又不必暴露该对象内部细节的方法。通过迭代器,开发人员不需要了解容器底层的结构,就可以实现对容器的遍历。由于创建迭代器的代价小,因此迭代器通常被称为轻量级的容器。

迭代器的使用主要有以下 3 个方面的注意事项:

1)使用容器的 iterator()方法返回一个 Iterator,然后通过 Iterator 的 next()方法返回第一个元素。

2)使用 Iterator 的 hasNext()方法判断容器中是否还有元素,如果有,可以使用 next()方法获取下一个元素。

3)可以通过 remove()方法删除迭代器返回的元素。

Iterator 支持派生的兄弟成员。ListIterator 只存在于 List 中,支持在迭代期间向 List 中添加或删除元素,并且可以在 List 中双向滚动。

在使用 iterator()方法时经常会遇到 ConcurrentModificationException 异常,这通常是由于在使用 Iterator 遍历容器的同时又对容器做增加或删除操作所导致的,或者由于多线程操作导致,当一个线程使用迭代器遍历容器的同时,另外一个线程对这个容器进行增加或删除操作。

下例主要介绍单线程抛出 ConcurrentModificationException 的情况:

以上介绍了单线程的解决方案,那么多线程访问容器的过程中抛出 ConcurrentModifi-cationException 异常又该怎么解决呢?

1)在 JDK 1.5 版本引入了线程安全的容器,比如 ConcurrentHashMap 和 CopyOnWriteArray-List 等。可以使用这些线程安全的容器来代替非线程安全的容器。

2)在使用迭代器遍历容器时对容器的操作放到 synchronized 代码块中,但是当引用程序并发程度比较高时,这会严重影响程序的性能。

引申:Iterator 与 ListIterator 有什么区别?

Iterator 只能正向遍历集合,适用于获取移除元素。ListIerator 继承自 Iterator,专门针对 List,可以从两个方向来遍历 List,同时支持元素的修改。


4.9.3 ArrayList、Vector 和 LinkedList 有什么区别

ArrayList、Vector、LinkedList 类均在 java.util 包中,均为可伸缩数组,即可以动态改变长

ArrayList 和 Vector 都是基于存储元素的 Object[]array 来实现的,它们会在内存中开辟一块连续的空间来存储,由于数据存储是连续的,因此,它们支持用序号(下标)来访问元素,同时索引数据的速度比较快。但是在插入元素时需要移动容器中的元素,所以对数据的插入操作执行得比较慢。ArrayList 和 Vector 都有一个初始化的容量的大小,当里面存储的元素超过这个大小时就需要动态地扩充它们的存储空间。为了提高程序的效率,每次扩充容量,不是简单地扩充一个存储单元,而是一次增加多个存储单元。Vector 默认扩充为原来的 2 倍(每次扩充空间的大小是可以设置的),而 ArrayList 默认扩充为原来的 1.5 倍(没有提供方法来设置空间扩充的方法)。

ArrayList 与 Vector 最大的区别就是 synchronization(同步)的使用,没有一个 ArrayList 的方法是同步的,而 Vector 的绝大多数方法(例如 add、insert、remove、set、equals、hashcode 等)都是直接或者间接同步的,所以 Vector 是线程安全的,ArrayList 不是线程安全的。正是由于 Vector 提供了线程安全的机制,其性能上也要略逊于 ArrayList。

LinkedList 是采用双向列表来实现的,对数据的索引需要从列表头开始遍历,因此用于随机访问则效率比较低,但是插入元素时不需要对数据进行移动,因此插入效率较高。同时,LinkedList 是非线程安全的容器。

那么,在实际使用时,如何从这几种容器中选择合适的使用呢?当对数据的主要操作为索引或只在集合的末端增加、删除元素时,使用 ArrayList 或 Vector 效率比较高;当对数据的操作主要为指定位置的插入或删除操作时,使用 LinkedList 效率比较高;当在多线程中使用容器时(即多个线程会同时访问该容器),选用 Vector 较为安全。

常见笔试题:

1.若线性表最常用的操作是存取第 i 个元素及其前趋的值,则采用( )存储方式节省时间。

A.单链表 B.双链表 C.单循环链表 D.顺序表

答案:D。顺序适合在随机访问的场合使用,访问时间复杂度为 O(1),而列表的随机访问操作的时间复杂度为 O(n)。


4.9.4 HashMap、Hashtable、TreeMap 和 WeakHashMap 有哪些区别

Java 为数据结构中的映射定义了一个接口 java.util.Map,它包括 3 个实现类:HashMap、Hashtable 和 TreeMap。Map 是用来存储键值对的数据结构,在数组中通过数组下标来对其内容索引的,而在 Map 中,则是通过对象来进行索引,用来索引的对象叫做 key,其对应的对象叫做 value。

HashMap 是一个最常用的 Map,它根据键的 HashCode 值存储数据,根据键可以直接获取它的值,具有很快的访问速度。由于 HashMap 与 Hashtable 都采用了 hash 法进行索引,因此二者具有许多相似之处,它们主要有如下的一些区别:

1)HashMap 是 Hashtable 的轻量级实现(非线程安全的实现),它们都完成了 Map 接口,主要区别在于 HashMap 允许空(null)键值(key)(但需要注意,最多只允许一条记录的键为 null,不允许多条记录的值为 null),而 Hashtable 不允许。

2)HashMap 把 Hashtable 的 contains 方法去掉了,改成 containsvalue 和 containsKey,因为 contains 方法容易让人引起误解。Hashtable 继承自 Dictionary 类,而 HashMap 是 Java 1.2 引进的 Map interface 的一个实现。

3)Hashtable 的方法是线程安全的,而 HashMap 不支持线程的同步,所以它不是线程安全的。在多个线程访问 Hashtable 时,不需要开发人员对它进行同步,而对于 HashMap,开发人员必须提供额外的同步机制。所以,就效率而言,HashMap 可能高于 Hashtable。

4)Hashtable 使用 Enumeration,HashMap 使用 Iterator。

5)Hashtable 和 HashMap 采用的 hash/rehash 算法都几乎一样,所以性能不会有很大的差异。

6)在 Hashtable 中,hash 数组默认大小是 11,增加的方式是 old×2+1。在 HashMap 中,hash 数组的默认大小是 16,而且一定是 2 的指数。

7)hash 值的使用不同,Hashtable 直接使用对象的 hashCode。

以上 3 种类型中,使用最多的是 HashMap。HashMap 里面存入的键值对在取出时没有固定的顺序,是随机的。一般而言,在 Map 中插入、删除和定位元素,HashMap 是最好的选择。由于 TreeMap 实现了 SortMap 接口,能够把它保存的记录根据键排序,因此,取出来的是排序后的键值对,如果需要按自然顺序或自定义顺序遍历键,那么 TreeMap 会更好。LinkedHash-Map 是 HashMap 的一个子类,如果需要输出的顺序和输入的相同,那么用 LinkedHashMap 可以实现,它还可以按读取顺序来排列。

WeakHashMap 与 HashMap 类似,二者的不同之处在于 WeakHashMap 中 key 采用的是「弱引用」的方式,只要 WeakHashMap 中的 key 不再被外部引用,它就可以被垃圾回收器回收。而 HashMap 中 key 采用的是「强引用的方式」,当 HashMap 中的 key 没有被外部引用时,只有在这个 key 从 HashMap 中删除后,才可以被垃圾回收器回收。

常见笔试题:

1.在 Hashtable 上下文中,同步指的是什么?

答案:同步意味着在一个时间点只能有一个线程可以修改 hash 表,任何线程在执行 Hash-table 的更新操作前都需要获取对象锁,其他线程则等待锁的释放。

2.如何实现 HashMap 的同步?

答案:HashMap 可以通过 Map m=Collections.synchronizedMap(new HashMap())来达到同步的效果。具体而言,该方法返回一个同步的 Map,该 Map 封装了底层的 HashMap 的所有方法,使得底层的 HashMap 即使是在多线程的环境中也是安全的。

4.9.5 用自定义类型作为 HashMap 或 Hashtable 的 key 需要注意哪些问题

HashMap 与 Hashtable 是用来存放键值对的一种容器,在使用这两个容器时有一个限制:不能用来存储重复的键。

从表面上看,向 HashMap 中添加的两个键值对的 key 值是相同的,可是为什么在后面添加的键值对没有覆盖前面的 value 呢?为了说明这个问题,下面首先介绍 HashMap 添加元素的操作过程。具体而言,在向 HashMap 中添加键值对 <key,value> 时,需要经过如下几个步骤:首先,调用 key 的 hashCode()方法生成一个 hash 值 h1,如果这个 h1 在 HashMap 中不存在,那么直接将 <key,value> 添加到 HashMap 中;如果这个 h1 已经存在,那么找出 HashMap 中所有 hash 值为 h1 的 key,然后分别调用 key 的 equals()方法判断当前添加的 key 值是否与已经存在的 key 值相同。如果 equals()方法返回 true,说明当前需要添加的 key 已经存在,那么 HashMap 会使用新的 value 值来覆盖掉旧的 value 值;如果 equals()方法返回 false,说明新增加的 key 在 HashMap 中不存在,因此会在 HashMap 中创建新的映射关系。当新增加的 key 的 hash 值已经在 HashMap 中存在时,就会产生冲突。一般而言,对于不同的 key 值可能会得到相同的 hash 值,因此就需要对冲突进行处理。一般而言,处理冲突的方法有开放地址法、再 hash 法、链地址法等。HashMap 使用的是链地址法来解决冲突,具体操作方法如图 4-13 所示。

向 HashMap 中添加元素时,若有冲突产生,其实现方式如图 4-14 所示。

从 HashMap 中通过 key 查找 value 时,首先调用的是 key 的 hashCode()方法来获取到 key 对应的 hash 值 h,这样就可以确定键为 key 的所有值存储的首地址。如果 h 对应的 key 值有多个,那么程序接着会遍历所有 key,通过调用 key 的 equals()方法来判断 key 的内容是否相等。只有当 equals()方法的返回值为 true 时,对应的 value 才是正确的结果。

在上例中,由于使用自定义的类作为 HashMap 的 key,而没有重写 hashCode()方法和 e-quals()方法,默认使用的是 Object 类的 hashCode()方法和 equals()方法。Object 类的 equals ()方法的比较规则如下:当参数 obj 引用的对象与当前对象为同一个对象时,就返回 true,否则返回 false。hashCode()方法会返回对象存储的内存地址。由于在上例中创建了两个对象,虽然它们拥有相同的内容,但是存储在内存中不同的地址,因此在向 HashMap 中添加对象时,调用 equals()方法的返回值为 false,HashMap 会认为它们是两个不同的对象,就会分别创建不同的映射关系,因此为了实现在向 HashMap 中添加键值对,可以根据对象的内容来判断两个对象是否相等,这就需要重写 hashCode()方法和 equals()方法,示例如下:

由此可以看出,开发者在使用自定义类作为 HashMap 的 key 时,需要注意以下几个问题:

1)如果想根据对象的相关属性来自定义对象是否相等的逻辑,此时就需要重写 equals()方法,一旦重写了 equals()方法,那么就必须重写 hashCode()方法。

2)当自定义类的多项作为 HashMap(Hashtable)的 key 时,最好把这个类设计为不可变类。

3)从 HashMap 的工作原理可以看出,如果两个对象相等,那么这两个对象有着相同的 hashCode,反之则不成立。


4.9.6 Collection 和 Collections 有什么区别

Collection 是一个集合接口。它提供了对集合对象进行基本操作的通用接口方法。实现该接口的类主要有 List 和 Set,该接口的设计目标是为各种具体的集合提供最大化的统一的操作方式。

Collections 是针对集合类的一个包装类,它提供一系列静态方法以实现对各种集合的搜索、排序、线程安全化等操作,其中大多数方法都是用来处理线性表。Collections 类不能实例化,如同一个工具类,服务于 Collection 框架。若在使用 Collections 类的方法时,对应的 collec-tion 的对象为 null,则这些方法都会抛出 NullPointerException。

============================

end

发布了101 篇原创文章 · 获赞 20 · 访问量 6万+

猜你喜欢

转载自blog.csdn.net/qq_40993412/article/details/104059813
4.9