1、说说 Java 中常见的集合
Java 中常见的集合可以概括如下
-
Map 接口和 Collection 接口是所有集合框架的父接口。
-
Collection 接口的子接口包括:Set 接口和 List 接口。
-
Map 接口的实现类主要有:HashMap、TreeMap、Hashtable、LinkedHashMap、ConcurrentHashMap以及 Properties 等。
-
Set 接口的实现类主要有:HashSet、TreeSet、LinkedHashSet 等。
-
List 接口的实现类主要有:ArrayList 、LinkedList、Stack 以及 Vector 等。
2、HashMap 和 Hashtable 的区别有哪些
- HashMap 没有考虑同步,是线程不安全的;Hashtable 使用了 synchronized 关键字,是线程安全的;
- HashMap允许 null 作为 Key;Hashtable 不允许 null 作为Key,Hashtable 的 value 也不可以为 null。
HashMap是线程不安全的是吧?你可以举一个例子吗?
有一个快速失败fast-fail机制,当对HashMap遍历的时候,调用了remove方法使其迭代器发生改变的时候会抛出一个异常ConcurrentModificationException。Hashtable因为在方法上做了synchronized处理,所以不会抛出异常。(错误)
- HashMap线程不安全主要是考虑到了多线程环境下进行扩容可能会出现HashMap死循环
- Hashtable线程安全是由于其内部实现在put和remove等方法上使用synchronized进行了同步,所以对单个方法的使用是线程安全的。但是对多个方法进行复合操作时,线程安全性无法保证。 比如一个线程在进行get操作,一个线程在进行remove操作,往往会导致下标越界等异常。
Java集合中的快速失败(fast-fail)机制
快速失败是Java集合的一种错误检测机制,当多个线程对集合进行结构上的改变的操作时,有可能会产生fail-fast。
例如:假设存在两个线程(线程1、线程2),线程1通过Iterator在遍历集合A中的元素,在某个时候线程2修改了集合A的结构(是结构上面的修改,而不是简单的修改集合元素的内容),那么这个时候程序就可能会抛出 ConcurrentModificationException异常,从而产生fast-fail快速失败。
ArrayList<Integer> list = new ArrayList<>();
list.add(2);
for (Integer integer : list) {
if (integer == 2) {
list.remove(integer);
}
}
快速失败机制底层是怎么实现的
迭代器在遍历时直接访问集合中的内容,并且在遍历过程中使用一个 modCount 变量。集合在被遍历期间如果内容发生变化,就会改变modCount的值。当迭代器使用hashNext()/next()遍历下一个元素之前,都会检测modCount变量是否为expectedModCount值,是的话就返回遍历;否则抛出异常,终止遍历。
* This exception may be thrown by methods that have detected concurrent * modification of an object when such modification is not permissible. * <p> * For example, it is not generally permissible for one thread to modify a Collection * while another thread is iterating over it. In general, the results of the * iteration are undefined under these circumstances. Some Iterator * implementations (including those of all the general purpose collection implementations * provided by the JRE) may choose to throw this exception if this behavior is * detected. Iterators that do this are known as <i>fail-fast</i> iterators, * as they fail quickly and cleanly, rather that risking arbitrary, * non-deterministic behavior at an undetermined time in the future. * <p> * Note that this exception does not always indicate that an object has * been concurrently modified by a <i>different</i> thread. If a single * thread issues a sequence of method invocations that violates the * contract of an object, the object may throw this exception. For * example, if a thread modifies a collection directly while it is * iterating over the collection with a fail-fast iterator, the iterator * will throw this exception. * * <p>Note that fail-fast behavior cannot be guaranteed as it is, generally * speaking, impossible to make any hard guarantees in the presence of * unsynchronized concurrent modification. Fail-fast operations * throw {@code ConcurrentModificationException} on a best-effort basis. * Therefore, it would be wrong to write a program that depended on this * exception for its correctness: <i>{@code ConcurrentModificationException} * should be used only to detect bugs.</i>
当检测到一个并发的修改,就可能会抛出该异常,一些迭代器的实现会抛出该异常,以便可以快速失败。但是你不可以为了便捷而依赖该异常,而应该仅仅作为一个程序的侦测。
3、HashMap底层实现结构
HashMap底层实现数据结构为数组+链表的形式,JDK8及其以后的版本中使用了数组+链表+红黑树实现,解决了链表太长导致的查询速度变慢的问题。
HashMap的初始容量,加载因子,扩容增量是多少?
HashMap的初始容量16,加载因子为0.75,扩容增量是原容量的1倍。如果HashMap的容量为16,一次扩容后容量为32。HashMap扩容是指元素个数**(包括数组和链表+红黑树中)**超过了16*0.75=12之后开始扩容。
HashMap的长度为什么是2的幂次方?
- 我们将一个键值对插入HashMap中,通过将Key的hash值与length-1进行&运算,实现了当前Key的定位,2的幂次方可以减少冲突(碰撞)的次数,提高HashMap查询效率。
- 如果length为2的幂次方,则length-1 转化为二进制必定是11111……的形式,在与h的二进制与操作效率会非常的快,而且空间不浪费。
- 如果length不是2的幂次方,比如length为15,则length-1为14,对应的二进制为1110,在与h与操作,最后一位都为0,而0001,0011,0101,1001,1011,0111,1101这几个位置永远都不能存放元素了,空间浪费相当大,更糟的是这种情况中,数组可以使用的位置比数组长度小了很多,这意味着进一步增加了碰撞的几率,减慢了查询的效率!这样就会造成空间的浪费。
- 也就是说2的N次幂有助于减少碰撞的几率,空间利用率比较大。加载因子,如果设置太小不利于空间利用,设置太大则会导致碰撞增多,降低了查询效率,所以设置了0.75。
HasMap的存储和获取原理:
当调用put()方法传递键和值来存储时,先对键调用hashCode()方法,返回的hashCode用于找到bucket位置来储存Entry对象,也就是找到了该元素应该被存储的桶中(数组)。当两个键的hashCode值相同时,bucket位置发生了冲突,也就是发生了Hash冲突,这个时候,会在每一个bucket后边接上一个链表(JDK8及以后的版本中还会加上红黑树)来解决,将新存储的键值对放在表头(也就是bucket中)。
当调用get方法获取存储的值时,首先根据键的hashCode找到对应的bucket,然后根据equals方法来在链表和红黑树中找到对应的值。
HasMap的扩容步骤:
当Map中的元素个数**(包括数组,链表和红黑树中)超过了16*0.75=12之后开始扩容。将会创建原来HashMap大小的两倍的bucket数组,来重新调整map的大小,并将原来的对象放入新的bucket数组中。这个过程叫作rehashing**,因为它调用hash方法找到新的bucket位置。
解决Hash冲突的方法有哪些?
- 拉链法 (HashMap使用的方法)
- 线性探测再散列法
- 二次探测再散列法
- 伪随机探测再散列法
哪些类适合作为HashMap的键?
String和Interger这样的包装类很适合做为HashMap的键,因为他们是final类型的类,而且重写了equals和hashCode方法,避免了键值对改写,有效提高HashMap性能。
4、ConcurrentHashMap和Hashtable的区别?
**ConcurrentHashMap结合了HashMap和Hashtable二者的优势。**HashMap没有考虑同步,Hashtable考虑了同步的问题。但是Hashtable在每次同步执行时都要锁住整个结构。
ConcurrentHashMap锁的方式是稍微细粒度的,ConcurrentHashMap将hash表分为16个桶(默认值),诸如get,put,remove等常用操作只锁上当前需要用到的桶。
在实际的开发中,我们在单线程环境下可以使用HashMap,多线程环境下可以使用ConcurrentHashMap,至于Hashtable已经不被推荐使用了。
5、TreeMap有哪些特性?
答:TreeMap底层使用红黑树实现,TreeMap中存储的键值对按照键来排序。
- 如果Key存入的是字符串等类型,那么会按照字典默认顺序排序
- 如果传入的是自定义引用类型,比如说User,那么该对象必须实现Comparable接口,并且覆盖其compareTo方法;或者在创建TreeMap的时候,我们必须指定使用的比较器。如下所示:
// 方式一:定义该类的时候,就指定比较规则
class User implements Comparable{
@Override
public int compareTo(Object o) {
// 在这里边定义其比较规则
return 0;
}
}
public static void main(String[] args) {
// 方式二:创建TreeMap的时候,可以指定比较规则
new TreeMap<User, Integer>(new Comparator<User>() {
@Override
public int compare(User o1, User o2) {
// 在这里边定义其比较规则
return 0;
}
});
}
解析:
关于TreeMap的考察,会涉及到两个接口Comparable和Comparator的比较。Comparable接口的后缀是able大概表示可以的意思,也就是说一个类如果实现了这个接口,那么这个类就是可以比较的。类似的还有cloneable接口表示可以克隆的。而Comparator则是一个比较器,是创建TreeMap的时候传入,用来指定比较规则。
那么Comparable接口和Comparator接口有哪些区别呢?
- Comparable实现比较简单,但是当需要重新定义比较规则的时候,必须修改源代码,即修改User类里边的compareTo方法
- Comparator接口不需要修改源代码,只需要在创建TreeMap的时候重新传入一个具有指定规则的比较器即可。
6、ArrayList和LinkedList有哪些区别?
ArrayList和LinkedList的区别总结如下。
- ArrayList底层使用了动态数组实现,实质上是一个动态数组
- LinkedList底层使用了双向链表实现,可当作堆栈、队列、双端队列使用
- ArrayList在随机存取方面效率高于LinkedList
- LinkedList在节点的增删方面效率高于ArrayList
- ArrayList必须预留一定的空间,当空间不足的时候,会进行扩容操作
- LinkedList的开销是必须存储节点的信息以及节点的指针信息
7、HashSet和TreeSet有哪些区别?
HashSet和TreeSet的区别总结如下。
- HashSet底层使用了Hash表实现。
保证元素唯一性的原理:判断元素的hashCode值是否相同。如果相同,还会继续判断元素的equals方法,是否为true
- TreeSet底层使用了红黑树来实现。
保证元素唯一性是通过Comparable或者Comparator接口实现
8、LinkedHashMap和LinkedHashSet有了解吗?
LinkedHashMap在面试题中还是比较常见的。LinkedHashMap可以记录下元素的插入顺序和访问顺序,具体实现如下:
- LinkedHashMap内部的Entry继承于HashMap.Node,这两个类都实现了Map.Entry<K,V>
- LinkedHashMap的Entry不光有value,next,还有before和after属性,这样通过一个双向链表,保证了各个元素的插入顺序
- 通过构造方法public LinkedHashMap(int initialCapacity,float loadFactor,boolean accessOrder), accessOrder传入true可以实现LRU缓存算法(访问顺序)
- LinkedHashSet 底层使用LinkedHashMap实现,两者的关系类似与HashMap和HashSet的关系,大家可以自行类比。
9、List和Set的区别?
List和Set的区别可以简单总结如下。
- List是有序的并且元素是可以重复的
- Set是无序(LinkedHashSet除外)的,并且元素是不可以重复的
(此处的有序和无序是指放入顺序和取出顺序是否保持一致)
10、Iterator和ListIterator的区别是什么?
常见的两种迭代器的区别如下。
- Iterator可以遍历list和set集合;ListIterator只能用来遍历list集合
- Iterator前者只能前向遍历集合;ListIterator可以前向和后向遍历集合
- ListIterator其实就是实现了前者,并且增加了一些新的功能。
Iterator其实就是一个迭代器,在遍历集合的时候需要使用。Demo实现如下:
ArrayList list = new ArrayList<>();
list.add("zjy");
list.add("def");
list.add("abc");
// 创建迭代器实现遍历集合
Iterator iterator = list.iterator();
while(iterator.hasNext()){
System.out.println(iterator.next());
}
11、数组和集合List之间的转换:
数组转为集合List:
通过Arrays.asList方法搞定,转换之后不可以使用add/remove等修改集合的相关方法,因为该方法返回的其实是一个Arrays的内部私有的一个类ArrayList,该类继承于Abstractlist,并没有实现这些操作方法,调用将会直接抛出UnsupportOperationException异常。这种转换体现的是一种适配器模式,只是转换接口,本质上还是一个数组。
集合转换数组:
List.toArray方法搞定了集合转换成数组,这里最好传入一个类型一样的数组,大小就是list.size()。因为如果入参分配的数组空间不够大时,toArray方法内部将重新分配内存空间,并返回新数组地址;如果数组元素个数大于实际所需,下标为list.size()及其之后的数组元素将被置为null,其它数组元素保持原值。所以,建议该方法入参数组的大小与集合元素个数保持一致。
若是直接使用toArray无参方法,此方法返回值只能是Object[ ]类,若强转其它类型数组将出现ClassCastException错误。
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
public class ConverTest {
public static void main(String[] args) {
// list集合转换成数组
ArrayList<String> list = new ArrayList<>();
list.add("zjy");
list.add("aaa");
list.add("bbb");
Object[] arr = list.toArray();
for (int i = 0; i < arr.length; i++) {
System.out.println(arr[i]);
}
// 数组转换为list集合
String[] arr2 = {"niuke", "alibaba"};
List<String> asList = Arrays.asList(arr2);
for (int i = 0; i < asList.size(); i++) {
System.out.println(asList.get(i));
}
}
}
12、Collection和Collections有什么关系?
Collection和Collections的关系: Collection是一个顶层集合接口,其子接口包括List和Set;而Collections是一个集合工具类,可以操作集合,比如说排序,二分查找,拷贝集合,寻找最大最小值等。