Java集合容器面试题(面试必备)


万恶之源:

1. 集合

1.1 什么是集合

集合是存储数据的容器,这里的数据指的是对象,可以存储不同的对象,并且长度可变。

1.2 集合特点

  • 集合用于对象封装数据,存储对象
  • 对象的个数可以确定时使用数组,不确定时使用集合,因为集合长度可变

1.3 集合和数组得区别

  • 数组的长度是固定的,集合是可变的
  • 数组可以存储基本数据类型,也可以存储引用数据类型,集合只能存储引用数据类型
  • 数组存储的元素必须是同一个数据类型,集合存储的对象可以是不同类型

1.3 什么是集合框架

集合框架是为了操作集合而规定的一种标准的体系结构,任何集合框架都包括三大块内容:对外接口,接口实现,集合运算算法。
对外接口:即集合的抽象数据类型,接口提供了我们能够对集合提供的内容进行操作。
接口实现:也就是集合框架的接口的具体实现,实际上是哪些可以复用的数据结构。
集合运算算法:例如,查找,排序算法,都是一些可以复用的函数。

1.4 集合框架的优点有哪些

  1. 降低核心类的开发成本,而非使用我们自己开发的集合类。
  2. 通过使用JDK附带的集合类,可以降低开发成本。
  3. 集合框架类经过严格的测试,开发质量会有所提高。
  4. 容量自增长。

1.5 集合框架中的泛型有什么优点?

自从JDK 5发行后,引入的泛型编程,在集合类和接口中都在大量使用泛型,泛型提供可以容纳的对象类型,因此如果你添加了其它的对象类型,会出现类之间的转换异常,编译时就会报错ClassCastException,有些错误就可以避免,这样就会给运行时带来好处。

1.6 常用的集合类

Map接口和Collection接口是所有集合类的父接口

  1. Collection:实现接口主要有Set,List,Queue
  2. Map:实现接口主要有Hashtable,HashMap,TreeMap,ConcurrentHashMap以及Properties
  3. Set接口主要实现类有Hashset,SortSet,LinkedHashSet
  4. List接口主要实现有ArrayList,LinkedList,Vector

1.5 哪些集合类是线程安全的?

  • Vector就比ArrayList多了个同步机制(线程安全)但因为效率低,已经很少被使用了。
  • Stack:堆栈类,先进后出
  • HashTable:就比HashMap多了个线程安全
  • enumeration:枚举,相当于迭代器

1.6 怎么确保一个集合不被修改?

可以使用Colections工具类的方法,任何集合对象经过处理都不能被修改
否则就会抛出异常

        List<String> list = new ArrayList<>();
        list.add("x");
        Collection<String> collection = Collections.unmodifiableCollection(list);
        collection.add("y");
        System.out.println(collection.size());
Exception in thread "main" java.lang.UnsupportedOperationException

1.7 Java集合的快速失败机制 “fail-fast”?

fail-fast是Java的一种错误检测机制,当多线程对集合上的结构进行操作时,就可能产生fail-fast机制。
出现场景
假如存在两个线程(线程1,线程2),线程1通过Iterator在遍历集合A的元素,在某个线程2中修改了集合的结构(不是单纯的修饰其内容),那么这个时候程序就会抛出CurrentModificationException异常,从而产生fail-fast机制。
原因
迭代器在遍历时直接访问集合中的内容,并且在遍历的时候使用一个modCount变量,集合在被遍历的过程中,如果内容发生变化(如元素被删除),则modCount就会发生改变。每次迭代器使用hashNext()/next()遍历下一个元素之前,就会先比较mCount是否与expectedModCount相等,expectedModCount默认值等于集合元素的个数,如果是就返回遍历,不是的话就抛出异常。

解决方法:

  • 在遍历过程是,所有涉及改变modCount的值的地方全部加上synchronized
  • 使用CopyOnWriteArrayList(线程安全)来替换ArrayList

    更多CopyOnWriteArrayList:

https://baijiahao.baidu.com/s?id=1666117483794506320&wfr=spider&for=pc

1.8 List如何一边遍历一边删除

先说说我们可能会犯的错误

   List<Integer> list = new ArrayList<Integer>();
   list.add(11);
   list.add(22);
   list.add(33);

 for (Integer integer : list) {
   
       list.remove(integer);
   }

如此我们会犯什么错了,看上去好像没错,但是运行之后就会产生上面1.8处说明的fail-fast机制,报出ConcurrentModificationException
产生的原因:和上面分析分析一致,foreach遍历其实内部采用的就是Iterator的遍历思想,当迭代器(线程1)在遍历的时候会维护着一个modCount遍历,但是如果list.remove(integer)(线程2)进行集合结构内容的改变时,就会改变modCount值,影响到迭代器遍历,爆出线程并发异常。
解决方法:除了上面提到的两个外这边可以直接使用迭代器的删除方法,如下:

   List<Integer> list = new ArrayList<Integer>();
   list.add(11);
   list.add(22);
   list.add(33);

   Iterator<Integer> it = list.iterator();
   while (it.hasNext()){
   
        Integer next = it.next();
        System.out.println(next);
       it.remove();
   }

注意:Iterator对象开始的cursor(游标)是指向0,类似于头指针,hasNext查看有没有下一个元素,所以it.next();不能放remove下方。

2.Collection接口

2.1 List接口

2.1.1 Iterator是什么

迭代器Iterator接口提供遍历任何Collection接口, public interface Collection<E> extends Iterable<E>,我们可以从Collection中使用迭代器方法获取迭代器实例,迭代器允许Java集合在遍历的时候删除元素。

2.1.2 Iterator有什么特点,怎么使用

   List<Integer> list = new ArrayList<Integer>();
   list.add(11);
   list.add(22);
   list.add(33);

   Iterator<Integer> it = list.iterator();
   while (it.hasNext()){
   
        Integer next = it.next();
        System.out.println(next);
   }

Iterator特点是单向遍历,但是更加安全,它可以保证当前正在遍历集合元素被修改的时候,爆出并发异常ConcurrentModificationException

2.1.3 如何一边遍历一边删除集合元素

### 1.8 List如何一边遍历一边删除

2.1.4 Iterator和ListIterator有什么区别

  • Iterator可以遍历List和Set的集合,而ListIterator只能遍历L集合
  • Iterator只支持单向遍历,而ListIterator支持双向遍历(前/后)
  • ListIterator实现Iterator接口,又添加了一些额外功能,如添加一个元素,替换一个元素。

2.1.5 遍历List的方式

遍历方法

  1. for循环遍历:基于计数器,集合外部维护着一个计数器,然后依次读取每个元素,直到读到最后一个元素即可。
  2. Iterator遍历:Iterator是面向对象的一种设计模式,目的是屏蔽不同集合的特点,统一遍历集合接口,Collectioin中采用的迭代器模式。
  3. foreach:内部还是使用了迭代器的方式实现,使用时不需要显示的声明迭代器或计数器,优点时代码简洁,不易出错,缺点是,在遍历的时候不能操作集合,如增删改,否则报会并发异常。

最佳方法:Java Collection接口中提供了RandomAccess 接口,用于标记集合是否支持RandomAccess接口

  1. 如果一个集合实现了该接口,则它支持快速随机访问,按位置读取元素的平均时间复杂度是O(1),如ArrayList
  2. 如果集合没有实现该接口,表示不支持RandomAccess接口,如LinkedList
    综上:推荐的做法是,如果集合支持RandomAccess可以使用for遍历,否则使用Iterator/foreach遍历。

2.1.6 说下ArrayList的优缺点

优点

  • ArrayList底层采用数组实现,是一种随机的访问模式,并且实现了RandomAccess接口,因此查找的速度非常快。
  • ArrayList顺序添加元素非常方便

缺点

  • 删除元素的时候需要做元素的复制操作,如果元素的很多的话,很消耗性能。
  • 插入元素的时候,同样遇到上面的问题,移动移动元素,让出插入位置。

2.1.7 数组与List之间的转换

  • 数组转List:使用Arrays.asList方法
  • List转数组:使用toArray方法
 List<Integer> list = new ArrayList<Integer>();
 list.add(11);
 list.add(22);
 list.add(33);

 Object[] objects = list.toArray();  //list->[]

 String []strs = new String[]{
   "aa","bb"};
 List<String> strings = Arrays.asList(strs);   //[]->list

2.1.8 ArrayList与LinkedList之间的区别

数据结构:ArrayList底层采用的是数组实现,LinkedList采用双向链表实现。
数据访问:ArrayList要优于LinkedList,因为ArrayList底层采用数组实现,LinkedList采用线性的链式存储结构,需要移动指针来访问。
增删效率:LinkedList要优于ArrayList,ArrayList增删要复制元素,影响其它数组元素的下标,而LinkedList只需要移动指针。
线程安全:ArrayList和LinkedList都是线程不同步的,都不能保证线程安全。

综上:如果对集合元素增删比较频繁的话推荐使用LinkedList,查找比较频繁的话推荐使用ArrayList。

2.1.9 ArrayList与Vector之间的区别

ArrayList与Vector都是List接口的实现,List接口又实现了Collection接口,因此ArrayList与Vector都是有序的集合。

  • Vector相比ArrayList更为安全,因此它增加了Synchronized来实现线程同步,因此是线程安全的,而ArrayList线程不安全。
  • ArrayList的性能要优于Vector

综上:追求效率至上不需要保证线程安全的情况下推荐使用ArrayList,需要线程安全的情况下推荐Vector

2.1.10 插入数据ArrayList,LinkedList与Vector的性能

ArrayList和Vector底层都是采用数组实现,Vector相比ArrayList加入了线程安全机制,相比ArrayList更为安全,但是效率却不及ArrayList,它们都不适合用频繁增删集合元素,因此会影响到集合内的其它元素。
LinkedList底层采用双向链表实现,增删元素靠指针来完成,不会影响到集合内的其它元素,因此效率更高。

2.1.11 多线程场景下使用ArrayList

由于ArrayList非线程安全,多线程的场景下使用ArrayList,可能会出现问题,可以使用Collections的synchronizedList方法将其转成一个线程安全的List再使用

  List<Integer> list = new ArrayList<Integer>();
  list.add(11);
  list.add(22);
  list.add(33);

  List<Integer> integers = Collections.synchronizedList(list);
  integers.add(44);

2.1.12 为什么ArrayList的elementData加上transient修饰

transient Object[] elementData; // non-private to simplify nested class access

回答问题前必须先明确transient的作用是什么?
持久化对象是,如果一个对象不想用序列化的机制来保存它,就可以在前面加一个transient。
回到刚刚的问题elementData前面加上transient说明elementData不会被序列化。这样做又什么好处呢?请看ArrayList中两个重要的函数

private void writeObject(java.io.ObjectOutputStream s)
    throws java.io.IOException{
   
    // Write out element count, and any hidden stuff
    int expectedModCount = modCount;
    s.defaultWriteObject();
 
    // Write out size as capacity for behavioural compatibility with clone()
    s.writeInt(size);
 
    // Write out all elements in the proper order.
    for (int i=0; i<size; i++) {
   
        s.writeObject(elementData[i]);
    }
 
    if (modCount != expectedModCount) {
   
        throw new ConcurrentModificationException();
    }
}

private void readObject(java.io.ObjectInputStream s)
    throws java.io.IOException, ClassNotFoundException {
   
    elementData = EMPTY_ELEMENTDATA;
 
    // Read in size, and any hidden stuff
    s.defaultReadObject();
 
    // Read in capacity
    s.readInt(); // ignored
 
    if (size > 0) {
   
        // be like clone(), allocate array based upon size not capacity
        ensureCapacityInternal(size);
 
        Object[] a = elementData;
        // Read in all elements in the proper order.
        for (int i=0; i<size; i++) {
   
            a[i] = s.readObject();
        }
    }
}

elementData是ArrayList存储元素的数组,是一个缓存数组,它通常会预留一些空间,等容量不足再进行扩容。每次序列化时,先调用defaultWriteObject方法来序列化ArrayList中哪些非transient修饰的内容,然后遍历elementData,用readObject和writeObject方法来实序列化就可以保证之序列化哪些实际存储的元素,从不是整个数组,从而节省了序列化过程中的空间和时间。

参考:https://blog.csdn.net/weixin_33924220/article/details/92348708

2.1.13 List与Set的区别

List和Set都继承于Collection接口
特点
List:一个有序的(存入和取出的顺序一致)的容器,可以插入null值,可以又重复元素,元素都有索引,常用的实现类有ArrayList,LinkedList,Vector.
Set:一个无序的(存入和取出的顺序可能不一致)的容器,不允许重复元素,只允许存入一个null,必须保证元素的唯一性,Set常见的实现类有HashSet,LinkedHashSet,TreeSet.
遍历
List因为是有序的,所以支持使用for,也就是通过下标来遍历,迭代器遍历,而Set只支持迭代器遍历,因为它是无序的,无法通过下标来获取值。

2.2 Set接口

2.2.1 说一下HashSet的实现原理

HashSet是基于HashMap实现的,HashSet的值存放在HashMap的key上,相关HashSet的操作,基本上都是直接调用底层的HashMap的相关方法来实现的,并且HashSet不可重复,是无序的。

2.2.2 说一下HashSet如何检查重复,如何保证数据不可重复?

说在前面:HashSet是Set的实现,不是Map接口下的实现。。。。
HashSet的add方法底层使用HashMap的put方法来插入数据,判断数据是否存在的依据,不仅要比较hashcode值还要equals方法。
HashSet部分源码:

private static final Object PRESENT = new Object();
private transient HashMap<E,Object> map;

public HashSet() {
   
    map = new HashMap<>();
}

public boolean add(E e) {
   
    // 调用HashMap的put方法,PRESENT是一个至始至终都相同的虚值
	return map.put(e, PRESENT)==null;
}

HashMap的key是唯一的,由源码可知HashSet的key就是使用HashMap的key,如果HashSet中遇到相同的key时,对于新的value就会覆盖旧的value,返回旧的value,所以是不会重复的。
HashMap比较key是否相等是先比较hashcode,再equals。

<mark>回顾hashcode和equals。</mark>
Java对hashcode()和equals()有以下规定:

  1. 如果两个对象相等,则它们的hashcode()一定相等
  2. 如果两个对象相等,则它们的equals也返回true
  3. 如果两个对象的hashcode相等,但是它们不一定相等。

综上:我们再重写equals时,规定也必须重写hashcode
hashcode()的默认行为是对堆上的对象产生独特值,如果没有重写hashcode(),无论如何两个对象都不会相等(即使两个对象指向相同的数据)
== 与 equals的区别

  • == 是判断两个变量或实例是否指向相同的内存地址,equals是判断两个变量或实例指向的内存地址对应的值是否相等
  • ==是对内存地址进行比较,equals是对字符串进行比较
  • ==是比较引用是否相同,equals是值是否相同

2.2.3 HashSet和HashMap的区别?

HashMap HashSet
实现Map接口 实现Set接口
存储键值对 存储对象
调用put方法向map中添加元素 调用add方法向Set中添加元素
HashMap使用key来计算hashcode值 HashSet使用成员对象来计算hashcode值,如果相等则在调用equals判断,如果对象不同的话,返回false
HashMap相比HashSet快,因为它是使用唯一的key来获取值 HashSet比HashMap稍慢点

2.3 Queue的poll()和remove()区别是什么

  Queue<String> queue = new LinkedList<>();
        queue.offer("11");
// System.out.println(queue.poll()); //11
// System.out.println(queue.poll()); //null
        System.out.println(queue.remove()); //11
        System.out.println(queue.remove()); //java.util.NoSuchElementException

区别已经很明显了

  • 相同点:poll()和remove()都是删除队列元素并返回删除元素
  • 不同点:当队列中没有元素时remove会抛出异常java.util.NoSuchElementException,而poll返回null

3. Map接口

https://blog.csdn.net/JAYU_37/article/details/104433085

4. 辅助工具类

4.1 comparable和comparator的区别

  1. comparable在java.lang.Comparable它有一个comparaTo方法用于排序;comparator在java.util包中,它有一个compare(Object obj1,Object obj2)用于排序
  2. 如果实现类没有实现comparable接口或者实现了comparable接口但是不满足于comparable接口提供的compareTo方法,我们可以自定义比较方法,只需要实现Comparator接口重写compare(Object obj1,Object obj2)方法。
  3. comparable需要比较对象来实现接口,使用对象调用方法来比较对象,需要改变对象内部结构(重写compareTo),耦合度高。comparator相当于一个通用的比较工具接口,需要定制一个比较类去实现它,重写里面的compare方法,方法参数即比较的对象,对象不用做任何的改变,解耦。

4.2 Collection和Collections的区别

Collectionjava.utils.Collectioni的集合接口,也是集合的顶级接口,它提供了对集合对象的基本操作和通用方法,Collection类在Java类库中有很多体现,直接实现类有List,Set。
Collections:是集合类的一个工具类,提供了一系列的静态方法,用于对集合中的元素进行排序,搜索等操作。

全部评论

相关推荐

一颗宏心:华为HR晚上过了十二点后还给我法消息。
点赞 评论 收藏
分享
1 1 评论
分享
牛客网
牛客企业服务