Java-集合类面试题总结

127 阅读14分钟

1、HashMap排序题

已知一个 HashMap<Integer,User>集合, User 有 name(String)和 age(int)属性。请写一个方法实现对HashMap 的排序功能,该方法接收 HashMap<Integer,User>为形参,返回类型为 HashMap<Integer,User>,要求对 HashMap 中的 User 的 age 倒序进行排序。排序时 key=value 键值对不得拆散。 

注意:要做出这道题必须对集合的体系结构非常的熟悉。HashMap本身就是不可排序的,但是该题偏偏让HashMap排序,那我们就得想在API中有没有这样的 Map 结构是有序的,我们不难发现其中LinkedHashMap就具有这样的结构,是链表结构有序的,更可喜的是他是 HashMap的子类,我们返回LinkedHashMap<Integer,User>即可,还符合面向接口编程的思想。 

但凡是对集合的操作,我们应该保持一个原则就是能用JDK中的API就用JDK中的 API,比如排序算法我们不应该去用冒泡或者选择,而是首先想到用 Collections 集合工具类。 

import java.util.*; class HashMapTest { public static void main(String[] args) { HashMap<Integer, User> users = new HashMap<>(); users.put(1, new User("张三", 25)); users.put(3,new User("李四",22)); users.put(2, new User("王五", 28)); System.out.println(users); HashMap<Integer, User> sortHashMap = sortHashMap(users); System.out.println(sortHashMap); /** * 控制台输出内容 * {1=User [name=张三, age=25], 2=User [name=王五,age=28], 3=User [name=李四, age=22]} * {2=User [name=王五, age=28], 1=User [name=张三, age=25], 3=User [name=李四, age=22]} */ }  public static HashMap<Integer, User> sortHashMap(HashMap<Integer, User> map) { // 首先拿到 map 的键值对集合 Set<Map.Entry<Integer, User>> entrySet = map.entrySet(); // 将 set 集合转为 List 集合,为什么,为了使用工具类的排序方法 List<Map.Entry<Integer,User>> list = new ArrayList<Map.Entry<Integer, User>>(entrySet); // 使用 Collections 集合工具类对 list 进行排序,排序规则使用匿名内部类来实现 Collections.sort(list, new Comparator<Map.Entry<Integer, User>>() { @Override public int compare(Map.Entry<Integer, User> o1, Map.Entry<Integer, User> o2) { //按照要求根据 User 的 age 的倒序进行排 return o2.getValue().getAge() - o1.getValue().getAge(); } }); //创建一个新的有序的 HashMap 子类的集合 LinkedHashMap<Integer, User> linkedHashMap = new LinkedHashMap<Integer, User>(); //将 List 中的数据存储在 LinkedHashMap 中 for (Map.Entry<Integer,User> entry : list) { linkedHashMap.put(entry.getKey(), entry.getValue()); } return linkedHashMap; }} class User { private String name; private int age;  public User(String name, int age) { this.name = name; this.age = age; }  public int getAge() { return age; }  public void setAge(int age) { this.age = age; }  public String getName() { return name; }  public void setName(String name) { this.name = name; }  @Override public String toString() { return "User{" + "name='" + name + '\'' + ", age=" + age + '}'; }}

2、请问 ArrayList、HashSet、HashMap 是线程安全的吗?如果不是怎么获取线程安全的集合?

通过以上类的源码进行分析,每个方法都没有加锁,显然都是非线程安全的。在集合中Vector 和HashTable是线程安全的。打开源码会发现其实就是把各自核心方法添加上了synchronized 关键字。Collections工具类提供了相关的 API,可以让上面那3个不安全的集合变为安全的。 

Collections.synchronizedCollection(c);

Collections.synchronizedList(list);

Collections.synchronizedMap(m);Collections.synchronizedSet(s); 

上面几个函数都有对应的返回值类型,传入什么类型返回什么类型。打开源码其实原理非常简单,就是将集合的核心方法添加上了synchronized关键字。 

3、ArrayList内部用什么实现的?

回答这样的问题,不要只回答个皮毛,可以再介绍一下ArrayList内部是如何实现数组的增加和删除的,因为数组在创建的时候长度是固定的,那么就有个问题我们往ArrayList中不断的添加对象,它是如何管理这些数组呢?通过源码可以看到ArrayList内部是用Object[]实现的。接下来我们分别分析ArrayList的构造以及add()、remove()、clear()方法的实现原理。 

● 无参数构造方法 

/** * Constructs a new {@code ArrayList} instance with zero initial capacity. */

public ArrayList(){  array=EmptyArray.OBJECT; } 

array 是一个 Object[]类型。当我们 new 一个空参构造时系统调用了 EmptyArray.OBJECT 属性,EmptyArray 仅仅是一个系统的类库,该类源码如下: 

public final class EmptyArray { private EmptyArray() { } public static final boolean[] BOOLEAN = new boolean[0]; public static final byte[] BYTE = new byte[0]; public static final char[] CHAR = new char[0]; public static final double[] DOUBLE = new double[0]; public static final int[] INT = new int[0]; public static final Class<?>[] CLASS = new Class[0]; public static final Object[] OBJECT = new Object[0]; public static final String[] STRING = new String[0]; public static final Throwable[] THROWABLE = new Throwable[0]; public static final StackTraceElement[] STACK_TRACE_ELEMENT = new StackTraceElement[0];} 

也就是说当我们 new 一个空参 ArrayList 的时候,系统内部使用了一个 new Object[0]数组。 

● 带容量参数的构造器 

/** * Constructs a new instance of {@code ArrayList} with the specified * initial capacity. * @param capacity the initial capacity of this {@code ArrayList}. */

public ArrayList(int capacity) { if (capacity < 0) { throw new IllegalArgumentException("capacity < 0: " + capacity); } array = (capacity == 0 ? EmptyArray.OBJECT : new Object[capacity]);} 

该构造函数传入一个 int 值,该值作为数组的长度值。如果该值小于 0,则抛出一个运行时异常。如果等于 0,则使用一个空数组,如果大于 0,则创建一个长度为该值的新数组。 

● 带集合参数的构造器 

/** * Constructs a new instance of {@code ArrayList} containing the elements of * the specified collection. * * @param collection the collection of elements to add. */

public ArrayList(Collection<? extends E> collection) { if (collection == null) { throw new NullPointerException("collection == null"); }  Object[] a = collection.toArray(); if (a.getClass() != Object[].class) { Object[] newArray = new Object[a.length]; System.arraycopy(a, 0, newArray, 0, a.length); a = newArray; } array = a; size = a.length;} 

如果调用构造函数的时候传入了一个 Collection 的子类,那么先判断该集合是否为 null,为 null 则抛出空指针异常。如果不是则将该集合转换为数组 a,然后将该数组赋值为成员变量 array,将该数组的长度作为成员变量 size。 

● add方法 

/** * Adds the specified object at the end of this {@code ArrayList}. * * @param object the object to add. * @return always true */

@Overridepublic boolean add(E object) { Object[] a = array; int s = size; if (s == a.length) { Object[] newArray = new Object[s + (s < (MIN_CAPACITY_INCREMENT / 2) ? MIN_CAPACITY_INCREMENT : s >> 1)]; System.arraycopy(a, 0, newArray, 0, s); array = a = newArray; } a[s] = object; size = s + 1; modCount++; return true;} 

● 第一:首先将成员变量 array 赋值给局部变量 a,将成员变量 size 赋值给局部变量 s。 

● 第二:判断集合的长度 s 是否等于数组的长度(如果集合的长度已经等于数组的长度了,说明数组已经满了,该重新分 配 新 数 组 了 ) , 重 新 分 配 数 组 的 时 候 需 要 计 算 新 分 配 内 存 的 空 间 大 小 , 如 果 当 前 的 长 度 小 于MIN_CAPACITY_INCREMENT/2(这个常量值是 12,除以 2 就是 6,也就是如果当前集合长度小于 6)则分配 12 个长度,如果集合长度大于 6 则分配当前长度 s 的一半长度。这里面用到了三元运算符和位运算,s >> 1,意思就是将s 往右移 1 位,相当于 s=s/2,只不过位运算是效率最高的运算。 

● 第三:将新添加的 object 对象作为数组的 a[s]个元素。 

● 第四:修 改 集 合 长 度size为s+1。 

● 第五:modCount++,该变量是父类中声明的,用于记录集合修改的次数,记录集合修改的次数是为了防止在用迭代器迭代集合时避免并发修改异常,或者说用于判断是否出现并发修改异常的。 

● 第六:return true,这个返回值意义不大,因为一直返回 true,除非报了一个运行时异常。 

● remove方法 

/** * Removes the object at the specified location from this list. * * @param index the index of the object to remove. * @return the removed object. * @throws IndexOutOfBoundsException when {@code location < 0 || location >= size()} */@Overridepublic E remove(int index) { Object[] a = array; int s = size; if (index >= s) { throwIndexOutOfBoundsException(index, s); } @SuppressWarnings("unchecked") E result = (E) a[index]; System.arraycopy(a, index + 1, a, index, --s - index); a[s] = null; // Prevent memory leak size = s; modCount++; return result;} 

● 第一:先将成员变量 array 和 size 赋值给局部变量 a 和 s。 

● 第二:判断形参 index 是否大于等于集合的长度,如果成了则抛出运行时异常 

● 第三:获取数组中脚标为 index 的对象 result,该对象作为方法的返回值 

● 第四:调用 System 的 arraycopy 函数完成数组拷贝。 

● 第五:接下来就是很重要的一个工作,因为删除了一个元素,而且集合整体向前移动了一位,因此需要将集合最后一个元素设置为 null,否则就可能内存泄露。 

● 第六:重新给成员变量 array 和 size 赋值。 

● 第七:记录修改次数。 

● 第八:返回删除的元素。 

● clear方法 

/** * Removes all elements from this {@code ArrayList}, leaving it empty. * * @see #isEmpty * @see #size */

@Overridepublic void clear() { if (size != 0) { Arrays.fill(array, 0, size, null); size = 0; modCount++; }} 

如果集合长度不等于 0,则将所有数组的值都设置为 null,然后将成员变量 size 设置为 0 即可,最后让修改记录加 1。 

4、并发集合和普通集合如何区别?

并发集合常见的有ConcurrentHashMap、ConcurrentLinkedQueue、ConcurrentLinkedDeque等。并发集合位于java.util.concurrent包下,是jdk1.5之后才有的,在 java 中有普通集合、同步(线程安全)的集合、并发集合。 

普通集合通常性能最高,但是不保证多线程的安全性和并发的可靠性。线程安全集合仅仅是给集合添加了 synchronized 同步锁,严重牺牲了性能,而且对并发的效率就更低了,并发集合则通过复杂的策略不仅保证了多线程的安全又提高的并发时的效率。 

5、List 和 Map、Set 的区别?

● 结构特点:List 和 Set 是存储单列数据的集合,Map 是存储键和值这样的双列数据的集合;List 中存储的数据是有顺序,并且允许重复;Map 中存储的数据是没有顺序的,其键是不能重复的,它的值是可以有重复的,Set 中存储的数据是无序的,且不允许有重复,但元素在集合中的位置由元素的 hashCode 决定,位置是固定的(Set 集合根据 hashCode 来进行数据的存储,所以位置是固定的,但是位置不是用户可以控制的,所以对于用户来说 Set 中的元素还是无序的); 

● 实现类:List 接口下的实现类(LinkedList:基于链表实现,链表内存是散乱的,每一个元素存储本身内存地址的同时还存储下一个元素的地址。链表增删快,查找慢;ArrayList:基于数组实现,非线程安全的,效率高,便于索引,但不便于插入删除;Vector:基于数组实现,线程安全的,效率低)。Map 接口下的实现类(HashMap:基于 hash 表的 Map 接口实现,非线程安全,高效,支持 null 值和 null 键;Hashtable:线程安全,低效,不支持 null 值和 null 键;LinkedHashMap:是HashMap 的一个子类,保存了记录的插入顺序;SortedMap 接口:TreeMap,能够把它保存的记录根据键排序,默认是键值的升序排序)。Set 接口下的实现类(HashSet:底层是由 HashMap 实现,不允许集合中有重复的值,使用该方式时需要重写 equals()和 hashCode()方法;LinkedHashSet继承与 HashSet,同时又基于LinkedHashMap 来进行实现,底层使用的是LinkedHashMp)。 

● 区别:List集合中对象按照索引位置排序,可以有重复对象,允许按照对象在集合中的索引位置检索对象,例如通过list.get(i)方法来获取集合中的元素;Map中的每一个元素包含一个键和一个值,成对出现,键对象不可以重复,值对象可以重复;Set集合中的对象不按照特定的方式排序,并且没有重复对象,但它的实现类能对集合中的对象按照特定的方式排序,例如 TreeSet类,可以按照默认顺序,也可以通过实现 java.util.Comparator接口来自定义排序方式。 

6、HashMap和Hashtable有什么区别?

HashMap是非线程安全的,HashMap是Map的一个实现类,是将键映射到值的对象,不允许键值重复。允许空键和空值;由于非线程安全,HashMap的效率要较 Hashtable 的效率高一些。Hashtable 是线程安全的一个集合,不允许 null 值作为一个 key 值或者value 值;Hashtable是sychronized,多个线程访问时不需要自己为它的方法实现同步,而 HashMap 在被多个线程访问的时候需要自己为它的方法实现同步。 

7、数组和链表分别比较适合用于什么场景,为什么? 

● 数组和链表的区别 

数组是将元素在内存中连续存储的;它的优点:因为数据是连续存储的,内存地址连续,所以在查找数据的时候效率比较高;它的缺点:在存储之前,我们需要申请一块连续的内存空间,并且在编译的时候就必须确定好它的空间的大小。在运行的时候空间的大小是无法随着你的需要进行增加和减少而改变的,当数据两比较大的时候,有可能会出现越界的情况,数据比较小的时候,又有可能会浪费掉内存空间。在改变数据个数时,增加、插入、删除数据效率比较低。链表是动态申请内存空间,不需要像数组需要提前申请好内存的大小,链表只需在用的时候申请就可以,根据需要来动态申请或者删除内存空间,对于数据增加和删除以及插入比数组灵活。还有就是链表中数据在内存中可以在任意的位置,通过应用来关联数据(就是通过存在元素的地址来联系) 

● 链表和数组使用场景 

数组应用场景:数据比较少;经常做的运算是按序号访问数据元素;数组更容易实现,任何高级语言都支持;构建的线性表较稳定。 

链表应用场景:对线性表的长度或者规模难以估计;频繁做插入删除操作;构建动态性比较强的线性表。 

8、Java中ArrayList和LinkedList区别?

ArrayList和Vector使用了数组的实现,可以认为 ArrayList 或者 Vector 封装了对内部数组的操作,比如向数组中添加、删除、插入新的元素或者数据的扩展和重定向。 

LinkedList 使用了循环双向链表数据结构。与基于数组的 ArrayList 相比,这是两种截然不同的实现技术,这也决定了它们将适用于完全不同的工作场景。 

LinkedList 链表由一系列表项连接而成。一个表项总是包含 3 个部分:元素内容,前驱表和后驱表,如图所示: 

image.png

在下图展示了一个包含 3 个元素的 LinkedList 的各个表项间的连接关系。在 JDK 的实现中,无论 LikedList 是否为空,链表内部都有一个 header 表项,它既表示链表的开始,也表示链表的结尾。表项 header 的后驱表项便是链表中第一个元素,表项 header 的前驱表项便是链表中最后一个元素。 

image.png

ArrayList 是实现了基于动态数组的数据结构,LinkedList 基于链表的数据结构。如果集合数据是对于集合随机访问 get 和 set,ArrayList 绝对优于 LinkedList,因为 LinkedList 要移动指针。如果集合数据是对于集合新增和删除操作 add 和 remove,LinkedList 比较占优势,因为ArrayList要移动数据。 

ArrayList 和 LinkedList 是两个集合类,用于存储一系列的对象引用(references)。例如我们可以用 ArrayList 来存储一系列的 String 或者 Integer。那么 ArrayList 和 LinkedList 在性能上有什么差别呢?什么时候应该用 ArrayList 什么时候又该用 LinkedList 呢? 

● 时间复杂度 

首先一点关键的是,ArrayList 的内部实现是基于基础的对象数组的,因此,它使用 get 方法访问列表中的任意一个元素时(random access),它的速度要比 LinkedList 快。LinkedList 中的 get 方法是按照顺序从列表的一端开始检查,直到另外一端。对 LinkedList 而言,访问列表中的某个指定元素没有更快的方法了。 

假设我们有一个很大的列表,它里面的元素已经排好序了,这个列表可能是 ArrayList 类型的也可能是 LinkedList 类型的,现在我们对这个列表来进行二分查找(binary search),比较列表是 ArrayList 和 LinkedList 时的查询速度,看下面的程序: 

public class TestList { public static final int N = 50000; //50000 个数 public static List values; //要查找的集合 //放入 50000 个数给 value; static { Integer vals[] = new Integer[N]; Random r = new Random(); for (int i = 0, currval = 0; i < N; i++) { vals = new Integer(currval); currval += r.nextInt(100) + 1; } values = Arrays.asList(vals); } //通过二分查找法查找 static long timeList(List lst) { long start = System.currentTimeMillis(); for (int i = 0; i < N; i++) { int index = Collections.binarySearch(lst, values.get(i)); if (index != i) System.out.println("***错误***"); } return System.currentTimeMillis() - start; } public static void main(String args[]) { System.out.println("ArrayList 消耗时间:" + timeList(new ArrayList(values))); System.out.println("LinkedList 消耗时间:" + timeList(new LinkedList(values))); }} 

LinkedList 做随机访问所消耗的时间与这个 list 的大小是成比例的。而相应的,在 ArrayList 中进行随机访问所消耗的时间是固定的。 

这是否表明 ArrayList 总是比 LinkedList 性能要好呢?这并不一定,在某些情况下 LinkedList 的表现要优于ArrayList,有些算法在 LinkedList 中实现时效率更高。比方说,利用 Collections.reverse 方法对列表进行反转时,其性能就要好些。看这样一个例子,加入我们有一个列表,要对其进行大量的插入和删除操作,在这种情况下LinkedList 就是一个较好的选择。请看如下一个极端的例子,我们重复的在一个列表的开端插入一个元素: 

public class ListDemo { static final int N = 50000; static long timeList(List list) { long start = System.currentTimeMillis(); Object o = new Object(); for (int i = 0; i < N; i++) list.add(0, o); return System.currentTimeMillis() - start; } public static void main(String[] args) { System.out.println("ArrayList 耗时:" + timeList(new ArrayList())); System.out.println("LinkedList 耗时:" + timeList(new LinkedList())); }} 

输出结果是: 

ArrayList 耗时:2463 

LinkedList 耗时:15 

● 空间复杂度 

在 LinkedList 中有一个私有的内部类,定义如下: 

private static class Entry { Object element; Entry next; Entry previous;} 

每个 Entry对象reference列表中的一个元素,同时还有在 LinkedList 中它的上一个元素和下一个元素。一个有1000个元素的LinkedList 对象将有 1000 个链接在一起的 Entry 对象,每个对象都对应于列表中的一个元素。这样的话,在一个 LinkedList 结构中将有一个很大的空间开销,因为它要存储这 1000 个 Entity 对象的相关信息。 

ArrayList 使用一个内置的数组来存储元素,这个数组的起始容量是10。当数组需要增长时,新的容量按如下公式获得:新容量=(旧容量*3)/2+1,也就是说每一次容量大概会增长 50%。这就意味着,如果你有一个包含大量元素的 ArrayList 对象,那么最终将有很大的空间会被浪费掉,这个浪费是由ArrayList 的工作方式本身造成的。如果没有足够的空间来存放新的元素,数组将不得不被重新进行分配以便能够增加新的元素。对数组进行重新分配,将会导致性能急剧下降。如果我们知道一个ArrayList将会有多少个元素,我们可以通过构造方法来指定容量。我们还可以通过 trimToSize 方法在 ArrayList 分配完毕之后去掉浪费掉的空间。 

● 总结 

ArrayList 和 LinkedList 在性能上各有优缺点,都有各自所适用的地方,总的说来可以描述如下: 

第一:对 ArrayList 和 LinkedList 而言,在列表末尾增加一个元素所花的开销都是固定的。对 ArrayList 而言,主要是在内部数组中增加一项,指向所添加的元素,偶尔可能会导致对数组重新进行分配;而对 LinkedList 而言,这个开销是统一的,分配一个内部 Entry 对象。 

第二:在 ArrayList 的中间插入或删除一个元素意味着这个列表中剩余的元素都会被移动;而在 LinkedList 的中间插入或删除一个元素的开销是固定的。 

第三:LinkedList 不支持高效的随机元素访问。 

第四:ArrayList 的空间浪费主要体现在在 list 列表的结尾预留一定的容量空间,而 LinkedList 的空间花费则体现在它的每一个元素都需要消耗相当的空间。 

可以这样说:当操作是在一列数据的后面添加数据而不是在前面或中间,并且需要随机地访问其中的元素时,使用ArrayList 会提供比较好的性能;当你的操作是在一列数据的前面或中间添加或删除数据,并且按照顺序访问其中的元素时,就应该使用LinkedList了。 

9、List a=new ArrayList()和ArrayList a =new ArrayList()的区别?

List list = new ArrayList();这句创建了一个 ArrayList 的对象后赋给了List。此时它是一个 List 对象了,有些ArrayList 有但是 List 没有的属性和方法,它就不能再用了。而ArrayList list=new ArrayList();创建一对象则保留了ArrayList 的所有属性。 所以需要用到 ArrayList 独有的方法的时候不能用前者。实例代码如下: 

List list = new ArrayList();ArrayList arrayList = new ArrayList();list.trimToSize(); //错误,没有该方法。arrayList.trimToSize(); //ArrayList 里有该方法。 

**10、请用两个队列模拟堆栈结构?

两个队列模拟一个堆栈,队列是先进先出,而堆栈是先进后出。模拟如下队列 a 和 b: 

● 入栈:a 队列为空,b 为空。例:则将”a,b,c,d,e”需要入栈的元素先放 a 中,a 进栈为”a,b,c,d,e”出栈:a 队列目前的元素为”a,b,c,d,e”。将 a 队列依次加入 Arraylist 集合 a 中。以倒序的方法,将 a 中的集合取出,放入 b 队列中,再将 b 队列出列。代码如下: 

public static void main(String[] args) { Queue queue = new LinkedList(); //a 队 列 Queue queue2 = new LinkedList(); //b 队列 ArrayList a = new ArrayList(); //arrylist 集合是中间参数 //往 a 队列添加元素 queue.offer("a"); queue.offer("b"); queue.offer("c"); queue.offer("d"); queue.offer("e"); System.out.print("进栈:"); //a 队列依次加入 list 集合之中 for (String q : queue) { a.add(q); System.out.print(q); } //以倒序的方法取出(a 队列依次加入 list 集合)之中的值,加入 b 对列 for (int i = a.size() - 1; i >= 0; i--) { queue2.offer(a.get(i)); } //打印出栈队列 System.out.println(""); System.out.print("出栈:"); for (String q : queue2) { System.out.print(q); }} 

运行结果为(遵循栈模式先进后出): 

进栈:a b c d e 

出栈:e d c b a 

11、Map中的key和value可以为null?

HashMap 对象的 key、value 值均可为 null。Hahtable 对象的 key、value 值均不可为 null。且两者的的 key 值均不能重复,若添加 key 相同的键值对,后面的 value 会自动覆盖前面的 value,但不会报错。测试代码如下: 

public class Test { public static void main(String[] args) { Map<String, String> map = new HashMap<String, String>();//HashMap 对象 Map<String, String> tableMap = new Hashtable<String, String>();//Hashtable 对象 map.put(null, null); System.out.println("hashMap 的[key]和[value]均可以为 null:" + map.get(null)); try { tableMap.put(null, "3"); System.out.println(tableMap.get(null)); } catch (Exception e) { System.out.println("【ERROR】:hashtable 的[key]不能为 null"); } try { tableMap.put("3", null); System.out.println(tableMap.get("3")); } catch (Exception e) { System.out.println("【ERROR】:hashtable的[value]不能为null"); } }} 

运行结果: 

hashMap 的[key]和[value]均可以为 null:null 

【ERROR】:hashtable 的[key]不能为 null 

【ERROR】:hashtable 的[value]不能为 null