20道java集合源码面试题,请笑纳

2023-11-02

问题一:看到这个图,你会想到什么?

在这里插入图片描述

答:

这个图由Map指向Collection的Produces并不是说Map是Collection的一个子类(子接口),这里的意思是指Map的KeySet获取到的一个视图是Collection的子接口。

我们可以看到集合有两个基本接口:Map和Collection。但是我个人认为Map并不能说是一个集合,称之为映射或许更为合适,因为它的KeySet视图是一个Set类型的键集,所以我们姑且把它也当做集合。

Collection继承了Iterator接口,而Iterator的作用是给我们提供一个只能向后遍历集合元素的迭代器,也就是说所有实现Collection的类都可以使用Iterator遍历器去遍历。

每种接口都有一个Abstract开头的抽象子类,这个子类中包括了一些默认的实现,我们在自定义类的时候都需要去继承这个抽象类,然后根据我们不同的需求,对于其中的方法进行重写。

从容器角度上来说,只有四种容器:Map,Queue,Set,List。

问题二:列出常见的集合,并进行简单的介绍

答:

  1. ArrayList: 一种可以动态增长和缩减的的索引序列
  2. LinkedList:一种可以在任何位置进行高效地插入和删除操作的有序序列
  3. ArrayDeque:一种用循环数组实现的双端队列
  4. HashSet:一种没有重复元素的无序集合
  5. TreeSet:一种有序集
  6. EnumSet:一种包含枚举类型值的集
  7. LinkedHashSet:一种可以记住元素插入次序的集
  8. PriorityQueue:一种允许高效删除最小元素的集合
  9. HashMap:一种存储键/值关联的数据结构
  10. TreeMap:一种键值有序排列的映射表
  11. EnumMap:一种键值属于枚举类型的映射表
  12. LinkedHashMap:一种可以记住键/值项添加次序的映射表
  13. WeakHashMap:一种其值无用武之地后可以被垃圾回收期回收的映射表
  14. IdentityHashMap:一种用==而不是用equals比较键值的映射表
  15. Vector:目前使用较少,因为设计理念的陈旧和性能的问题被ArrayList所取代
  16. Hashtable:线程非同步可以使用HashMap来替代,同步的话可以使用ConcurrentHashMap来替代

问题三:关于Iterator,聊聊你的看法

从鸟瞰图中我们可以看到,所有实现Collection的子类都继承了Iterable接口。这个接口提供了一个iterator()方法可以构造一个Iterator接口对象。然后我们可以使用这个迭代器对象依次访问集合中的元素。

迭代器一般使用方法是这样的:

Collection<String> c = ...;
Iterator<String> iter = c.iterator();
while (iter.hasNext()) {
   
    String s = iter.next();
    System.out.println(s);
}

或者是这样的:

//适用于JDK1.8以后的版本
iter.forEachRemaining(element -> System.out.println(element));

迭代器的next()工作原理是这样的:

迭代器是位于两个集合元素之间的位置,当我们调用next()方法的时候迭代器指针就会越过一个元素,并且返回刚刚越过的元素,所以,当我们迭代器的指针在最后一个元素的时候,就会抛出会抛出一个NoSuchElementException的异常。所以,在调用next()之前需要调用hasNext()去判断这个集合的迭代器是否走到了最后一个元素。

通过调用next()方法可以逐个的去访问集合中的每个元素,而访问元素的顺序跟该容器的数据结构有关,比如ArrayList就是按照索引值开始,每次迭代都会使索引值加1,而对于HashSet这种数据结构是散列表的集合,就会按照某种随机的次序出现。

Iterator的接口中还有一个remove()方法,这个方法实际上删除的是上次调用next()方法返回的元素,下面我来展示一下remove()方法的使用方法

Collection<String> c = ...;
Iterator<String> iter = c.iterator();
iter.next();
iter.remove();

这样就可以删除该集合中的第一个元素,但是需要注意一点,如果我们需要删除两个元素,必须这样做:

iter.remove();
iter.next();
iter.remove();

而不能这么做:

iter.remove();
iter.remove();

因为next()方法和remove()方法之间是有依赖性的,如果调用remove之前没有调用next就会抛出一个IllegalStateException的异常。

问题四:对于Collection,你了解多少?

可以看出,作为顶级的框架,Collection仅仅是继承了Iterable接口,接下来,我们来看一下Iterable的源码,看看有什么收获。

public interface Iterable<T> {
   
   
    Iterator<T> iterator();
    
    default void forEach(Consumer<? super T> action) {
   
        Objects.requireNonNull(action);
        for (T t : this) {
   
            action.accept(t);
        }
    }

    default Spliterator<T> spliterator() {
   
        return Spliterators.spliteratorUnknownSize(iterator(), 0);
    }
}

可以看到这个接口中有三个方法,其中iterator()方法可以给我们提供一个迭代器,这个在之前的教程就已经说过了,而forEach()方法提供了一个函数式接口的参数,我们可以使用lambda表达式结合来使用:

Collection<String> collection = ...;
collection.forEach(String s -> System.out.println(s));

这样就可以获取到每个值,它的底层实现是加强for循环,实际上也是迭代器去遍历,因为编译器会把加强for循环编译为迭代遍历。
Spliterator()是1.8新加的方法,字面意思可分割的迭代器,不同以往的iterator()需要顺序迭代,Spliterator()可以分割为若干个小的迭代器进行并行操作,既可以实现多线程操作提高效率,又可以避免普通迭代器的fail-fast(fail-fast机制是java集合中的一种错误机制。当多个线程对同一个集合的内容进行操作时,就可能会产生fail-fast事件)机制所带来的异常。Spliterator()可以配合1.8新加的Stream()进行并行流的实现,大大提高处理效率。
在这里插入图片描述
Collection()中提供了17个接口方法(除去了继承自Object的方法)。接下来,我们来了解一下这些方法的作用:

  1. size(),返回当前存储在集合中的元素个数。
  2. isEmpty(),如果集合中没有元素,返回true。
  3. contains(Object obj),如果集合中包含了一个与obj相等的对象,返回true。
  4. iterator(),返回这个集合的迭代器。
  5. toArray(),返回这个集合的对象数组
  6. toArray(T[] arrayToFill),返回这个集合的对象数组,如果arrayToFill足够大,就将集合中的元素填入这个数组中。剩余空间填补null;否则,分配一个新数组,其成员类型与arrayToFill的成员类型相同,其长度等于集合的大小,并填充集合元素。
  7. add(Object element),将一个元素添加到集合中,如果由于这个调用改变了集合,返回true。
  8. remove(Object obj),从集合中删除等于obj的对象,如果有匹配的对象被删除,返回true。
  9. containsAll(Collection<?> other),如果这个集合包含other集合中的所有元素,返回true。
  10. addAll(Collection<? extends E> other),将other集合中的所有元素添加到这个集合,如果由于这个调用改变了集合,返回true。
  11. removeAll(Collection<?> other),从这个集合中删除other集合中存在的所有元素。如果由于这个调用改变了集合,返回true。
  12. removeIf(Predicate<? super E> filter),从这个集合删除filter返回true的所有元素,如果由于这个调用改变了集合,则返回true。
  13. retainAll(Collection<?> other),从这个集合中删除所有与other集合中的元素不同的元素。如果由于这个调用改变了集合,返回true。
  14. clear(),从这个集合中删除所有的元素。
  15. spliterator(),返回分割后的若干个小的迭代器。
  16. stream(),返回这个集合对于的流对象。
  17. parallelStream(),返回这个集合的并行流对象。

作为第一级的集合接口,Collection提供了一些基础操作的借口,并且可以通过实现Iterable接口获取一个迭代器去遍历获取集合中的元素。

问题五:那么AbstractCollection呢?

作为Collection的抽象类实现,它的方法都是基于迭代器来完成的,这里只贴出了源码中几个需要特殊的注意的点,
在这里插入图片描述

TAG 1 :

数组作为一个对象,需要一定的内存存储对象头信息,对象头信息最大占用内存不可超过8 byte。

TAG 2 :

finishToArray(T[] r, Iterator<?> it)方法用于数组扩容,当数组索引指向最后一个元素+1时,对数组进行扩容:即创建一个大小为(cap + cap/2 +1)的数组,然后将原数组的内容复制到新数组中。扩容前需要先判断是否数组长度是否溢出。这里的迭代器是从上层的方法(toArray(T[] t))传过来的,并且这个迭代器已执行了一部分,而不是从头开始迭代的

TAG 3 :

hugeCapacity(int minCapacity)方法用来判断该容器是否已经超过了该集合类默认的最大值即(Integer.MAX_VALUE -8),一般我们用到这个方法的时候比较少,后面我们会在ArrayList类的学习中,看到ArrayList动态扩容用到了这个方法。

TAG 4 :

这里的add(E)方法默认抛出了一个异常,这是因为如果我们想修改一个不可变的集合时,抛出 UnsupportedOperationException 是正常的行为,比如当你用 Collections.unmodifiableXXX() 方法对某个集合进行处理后,再调用这个集合的修改方法(add,remove,set…),都会报这个错。因此 AbstractCollection.add(E) 抛出这个错误是准从标准。

问题六: 能否详细说一下toArray方法的实现?

高能预警:废话不多说,直接上源码

 /**
    * 分配了一个等大空间的数组,然后依次对数组元素进行赋值
    */
    public Object[] toArray() {
   
        //新建等大的数组
        Object[] r = new Object[size()];
        Iterator<E> it = iterator();
        for (int i = 0; i < r.length; i++) {
   
            //判断是否遍历结束,以防多线程操作的时候集合变得更小
            if (! it.hasNext()) 
                return Arrays.copyOf(r, i);
            r[i] = it.next();
        }
         //判断是否遍历未结束,以防多线程操作的时候集合变得更大,进行扩容
        return it.hasNext() ? finishToArray(r, it) : r;
    }


    /**
    * 泛型方法的`toArray(T[] a)`方法在处理里,会先判断参数数组的大小,
    * 如果空间足够就使用参数作为元素存储,如果不够则新分配一个。
    * 在循环中的判断也是一样,如果参数a能够存储则返回a,如果不能再新分配。
    */
    @SuppressWarnings("unchecked")
    public <T> T[] toArray(T[] a) {
   
        int size = size();
        //当数组a的长度大于等于a,直接将a赋予给r,否则使用反射API获取一个长度为size的数组
        T[] r = a.length >= size ? a :
                  (T[])java.lang.reflect.Array
                .newInstance(a.getClass().getComponentType(), size);
        Iterator<E> it = iterator();
        for (int i = 0; i < r.length; i++) {
   
            //判断是否遍历结束
            if (! it.hasNext()) {
    
                //如果 a == r,将r的每项值赋空,并将a返回
                if (a == r) {
   
                    r[i] = null;
                } else if (a.length < i) {
   
                    //如果a的长度小于r,直接调用Arrays.copyOf进行复制获取一个新的数组
                    return Arrays.copyOf(r, i);
                } else {
   
                    System.arraycopy(r, 0, a, 0, i);
                    if (a.length > i) {
   
                        a[i] = null;
                    }
                }
                return a;
            }
            //如果遍历结束,将迭代器获取的值赋给r
            r[i] = (T)it.next();
        }
        //判断是否遍历未结束,以防多线程操作的时候集合变得更大,进行扩容
        return it.hasNext() ? finishToArray(r, it) : r;
    }
    
    /**
    * 设定该容器的最大值
    */
    private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;

    /**
    *   用于动态扩容
    */
    @SuppressWarnings("unchecked")
    private static <T> T[] finishToArray(T[] r, Iterator<?> it) {
   
        int i = r.length;
        while (it.hasNext()) {
   
            int cap = r.length;
            if (i == cap) {
   
                int newCap = cap + (cap >> 1) + 1;
                
                if (newCap - MAX_ARRAY_SIZE > 0)
                    newCap = hugeCapacity(cap + 1);
                r = Arrays.copyOf(r, newCap);
            }
            r[i++] = (T)it.next();
        }
        return (i == r.length) ? r : Arrays.copyOf(r, i);
    }
    
    private static int hugeCapacity(int minCapacity) {
   
        if (minCapacity < 0) // overflow
            throw new OutOfMemoryError
                ("Required array size too large");
        return (minCapacity > MAX_ARRAY_SIZE) ?
            Integer.MAX_VALUE :
            MAX_ARRAY_SIZE;
    }

为了帮助了解,我把Arrays.copyOf(r.i)的源码也贴出来:

//参数original代表你传入的需要复制的泛型数组,newLength复制得到数组的大小
public static <T> T[] copyOf(T[] original, int newLength) {
   
    return (T[]) copyOf(original, newLength, original.getClass());
}

public static <T,U> T[] copyOf(U[] original, int newLength, Class<? extends T[]> newType) {
   
    @SuppressWarnings("unchecked")
    T[] copy = ((Object)newType == (Object)Object[].class)
        ? (T[]) new Object[newLength]
        : (T[]) Array.newInstance(newType.getComponentType(), newLength);
    System.arraycopy(original, 0, copy, 0,
                     Math.min(original.length, newLength));
    return copy;
}

我们可以观察到其中调用了System.arraycopy方法,为了保持刨根问底的态度,我们又去翻看了这个方法的源码:

//src数组里从索引为srcPos的元素开始, 复制到数组dest里的索引为destPos的位置, 复制的元素个数为length个. 
 public static native void arraycopy(Object src, int srcPos, Object dest, int destPos,int length);

可以看到这个方式是由关键字native修饰的方法,那么native修饰的方法有什么含义呢?
native关键字说明其修饰的方法是一个原生态方法,方法对应的实现不是在当前文件,而是在用其他语言(如C和C++)实现的文件中。Java语言本身不能对操作系统底层进行访问和操作,但是可以通过JNI接口调用其他语言来实现对底层的访问。

JNI是Java本机接口(Java Native Interface),是一个本机编程接口,它是Java软件开发工具箱(java Software Development Kit,SDK)的一部分。JNI允许Java代码使用以其他语言编写的代码和代码库。Invocation API(JNI的一部分)可以用来将Java虚拟机(JVM)嵌入到本机应用程序中,从而允许程序员从本机代码内部调用Java代码。

然后我们来分析toArray()中需要注意的点,通过原源码中的英文注解,toArray得到的数组跟原collection没有任何关系,我们可以对数组的每个引用值做修改,而不会影响到原collection.这个看起来好像是多余说明的,但是考虑到ArrayList其实就是基于数组实现的,那这个限制保证了即使是将ArrayList转化为数组,那也应该是分配一个新数组,而不是返回原来的数组。

如果我们在单线程操作的情况下,collection集合大小不变,正常应该是执行到 return it.hasNext() ? finishToArray(r, it) : r;这条语句结束,但考虑到在复制的过程中,collection的集合可能会有变化,可能是变大也可能是变小,所以方法增加了对这种情况的处理,这就是为什么每次循环都要判断是collection是否遍历完,以及最后再判断collection是否变得更长,如果是的话,还需要重新再为array分配空间。

通常情况下,我们不会执行到hugeCapacity,但作为一个框架来说,这体现了设计时的严谨。

问题七:用的最多的集合之一——List,说说你对它的理解

List是继承自Collection的一个子接口,它提供了一个有序的集合,在这个集合中我们可以使用索引去获取集合中的值,同时,我们也可以通过迭代器去访问集合中的元素,第一种方法被称为随机访问,因为我们可以按照任意的顺序去访问元素,而使用迭代器就必须顺序的去访问元素。

相比于它的父接口Collection,并没有发生很大的改动,但是由于List是一个有序的集合,所以提供了一些基于索引进行的操作:

get(int index):获取该集合中索引等于index的元素

set(int index, E element):将该集合中索引等于index的元素赋值为element

add(int index, E element):在集合中索引等于index的位置将element插入,并将当前处于该位置的元素及其后续元素的索引加1。

remove(int index):删除指定索引(index)位置的元素,并将处于该位置后面的元素索引减1

indexOf(Object o):获取对象o在集合中的索引

lastIndexOf(Object o):获取对象o在集合中最后一次出现的索引值,如果集合中不存在这个对象,返回-1。

同时,提供了一个Iterator的子接口ListIterator,基于这个迭代器,我们实现了两个默认方法replaceAll(UnaryOperator operator)和sort(Comparator<? super E> c)。

replaceAll(UnaryOperator operator)这里和String类中replaceAll()方法并不相同,这里的接收参数是一个函数式接口,我们来看一下这个函数式接口的源码:

package java.util.function;

@FunctionalInterface
public interface UnaryOperator<T> extends Function<T, T> {
   

    static <T> UnaryOperator<T> identity() {
   
        return t -> t;
    }
}

用法如下:

List<String> strList = new ArrayList<>();
strList.add("Hungary");
strList.add("Foolish");

strList.replaceAll(t -> "Stay " + t);
strList.forEach(s -> System.out.println(s));

打印结果为

Stay Hungary Stay Foolish

而sort(Comparator<? super E> c)传入的同样是一个函数式接口,我们可以自定义排序规则后,调用这个方法进行排序:

List<Human> humans = Lists.newArrayList(new Human("Sarah", 10), new Human("Jack", 12));
 
humans.sort((Human h1, Human h2) -> h1.
本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

20道java集合源码面试题,请笑纳 的相关文章

  • 十进制到八进制的转换[重复]

    这个问题在这里已经有答案了 可能的重复 十进制转换错误 https stackoverflow com questions 13142977 decimal conversion error 我正在为一个类编写一个程序 并且在计算如何将八进
  • Java TestNG 与跨多个测试的数据驱动测试

    我正在电子商务平台中测试一系列商店 每个商店都有一系列属性 我正在考虑对其进行自动化测试 是否有可能有一个数据提供者在整个测试套件中提供数据 而不仅仅是 TestNG 中的测试 我尝试不使用 testNG xml 文件作为机制 因为这些属性
  • 如何将 pfx 文件转换为 jks,然后通过使用 wsdl 生成的类来使用它来签署传出的肥皂请求

    我正在寻找一个代码示例 该示例演示如何使用 PFX 证书通过 SSL 访问安全 Web 服务 我有证书及其密码 我首先使用下面提到的命令创建一个 KeyStore 实例 keytool importkeystore destkeystore
  • JRE 系统库 [WebSphere v6.1 JRE](未绑定)

    将项目导入 Eclipse 后 我的构建路径中出现以下错误 JRE System Library WebSphere v6 1 JRE unbound 谁知道怎么修它 右键单击项目 特性 gt Java 构建路径 gt 图书馆 gt JRE
  • AWS 无法从 START_OBJECT 中反序列化 java.lang.String 实例

    我创建了一个 Lambda 函数 我想在 API 网关的帮助下通过 URL 访问它 我已经把一切都设置好了 我还创建了一个application jsonAPI Gateway 中的正文映射模板如下所示 input input params
  • 如何在seaborn displot中使用hist_kws

    我想在同一图中用不同的颜色绘制直方图和 kde 线 我想为直方图设置绿色 为 kde 线设置蓝色 我设法弄清楚使用 line kws 来更改 kde 线条颜色 但 hist kws 不适用于显示 我尝试过使用 histplot 但我无法为
  • 每个 X 具有多个 Y 值的 Python 散点图

    我正在尝试使用 Python 创建一个散点图 其中包含两个 X 类别 cat1 cat2 每个类别都有多个 Y 值 如果每个 X 值的 Y 值的数量相同 我可以使用以下代码使其工作 import numpy as np import mat
  • Java执行器服务线程池[关闭]

    很难说出这里问的是什么 这个问题是含糊的 模糊的 不完整的 过于宽泛的或修辞性的 无法以目前的形式得到合理的回答 如需帮助澄清此问题以便重新打开 访问帮助中心 help reopen questions 如果我使用 Executor 框架在
  • Google App Engine 如何预编译 Java?

    App Engine 对应用程序的 Java 字节码使用 预编译 过程 以增强应用程序在 Java 运行时环境中的性能 预编译代码的功能与原始字节码相同 有没有详细的信息这是做什么的 我在一个中找到了这个谷歌群组消息 http groups
  • 无法捆绑适用于 Mac 的 Java 应用程序 1.8

    我正在尝试将我的 Java 应用程序导出到 Mac 该应用程序基于编译器合规级别 1 7 我尝试了不同的方法来捆绑应用程序 1 日食 我可以用来在 Eclipse 上导出的最新 JVM 版本是 1 6 2 马文 看来Maven上也存在同样的
  • 有人用过 Dabo 做过中型项目吗? [关闭]

    Closed 这个问题是基于意见的 help closed questions 目前不接受答案 我们正处于一个新的 ERP 风格的客户端 服务器应用程序的开始阶段 该应用程序是作为 Python 富客户端开发的 我们目前正在评估 Dabo
  • 如何从指定日期获取上周五的日期? [复制]

    这个问题在这里已经有答案了 如何找出上一个 上一个 星期五 或指定日期的任何其他日期的日期 public getDateOnDay Date date String dayName 我不会给出答案 先自己尝试一下 但是 也许这些提示可以帮助
  • 使用 Python 绘制 2D 核密度估计

    I would like to plot a 2D kernel density estimation I find the seaborn package very useful here However after searching
  • 如何从泛型类调用静态方法?

    我有一个包含静态创建方法的类 public class TestClass public static
  • 静态变量的线程安全

    class ABC implements Runnable private static int a private static int b public void run 我有一个如上所述的 Java 类 我有这个类的多个线程 在里面r
  • 在 Maven 依赖项中指定 jar 和 test-jar 类型

    我有一个名为 commons 的项目 其中包含运行时和测试的常见内容 在主项目中 我添加了公共资源的依赖项
  • 捕获的图像分辨率太大

    我在做什么 我允许用户捕获图像 将其存储到 SD 卡中并上传到服务器 但捕获图像的分辨率为宽度 4608 像素和高度 2592 像素 现在我想要什么 如何在不影响质量的情况下获得小分辨率图像 例如我可以获取或设置捕获的图像分辨率为原始图像分
  • 从列表指向字典变量

    假设你有一个清单 a 3 4 1 我想用这些信息来指向字典 b 3 4 1 现在 我需要的是一个常规 看到该值后 在 b 的位置内读写一个值 我不喜欢复制变量 我想直接改变变量b的内容 假设b是一个嵌套字典 你可以这样做 reduce di
  • 按日期对 RecyclerView 进行排序

    我正在尝试按日期对 RecyclerView 进行排序 但我尝试了太多的事情 我不知道现在该尝试什么 问题就出在这条线上适配器 notifyDataSetChanged 因为如果我不放 不会显示错误 但也不会更新 recyclerview
  • 如何实现仅当可用内存较低时才将数据交换到磁盘的写缓存

    我想将应用程序生成的数据缓存在内存中 但如果内存变得稀缺 我想将数据交换到磁盘 理想情况下 我希望虚拟机通知它需要内存并将我的数据写入磁盘并以这种方式释放一些内存 但我没有看到任何方法以通知我的方式将自己挂接到虚拟机中before an O

随机推荐