20个经典投资银行Java面试题解析:多线程HashMap问题详解

点击上方“Java编程精选”,选择“置顶公众号”

20个经典投资银行Java面试题解析:多线程HashMap问题详解

关键时刻,第一时间送达!

本文收集了 20 个 Java 面试题,这些面试题来自投资银行。每一个面试题都特别经典,现分享给各位 Java 开发者。

在多线程环境中使用 HashMap 会出现哪些问题呢?在何种情况下使用 get()方法会导致无限循环呢?

HashMap本身不存在问题,其是否有问题取决于使用方式。例如,在一个线程中初始化了 HashMap ,接着在多个其他线程中对其进行读取,这种情况肯定是没有问题的。有这样一个例子,即使用 HashMap 来存储系统配置项。当多个线程对 HashMap 进行修改操作时才会出现问题,例如进行增加、删除、更新键值对等操作。由于 put()操作可能会引发重新分配存储大小(re-sizeing)的行为,所以有可能导致无限循环的出现。因此,在这种情况下需要使用 Hashtable 或者 ConcurrentHashMap,并且 ConcurrentHashMap 更为优越。

问题二:不重新编写 Bean 的 hashCode()方法是否会对性能产生影响?

这个问题很好,每个人或许都有自身的体会。从我的知识层面来看,倘若一个计算 hash 的方法编写得不佳,那么直接带来的影响就是,在向 HashMap 中添加元素时,会更频繁地引发冲突,从而最终致使耗时增加。但是从 Java8 开始,这种影响不再如前几个版本那般显著了。因为当冲突的发生超出一定限度后,链表类的实现会被替换为二叉树实现。此时,你依然能够获得 O(logN)的开销,这比链表类的 O(n)更优。

对于一个不可修改的类,其每个对象是否都必须声明为 final 的呢?

不一定,因为可以把成员声明为非 final 且 private 的,并且不能在除构造函数之外的其他地方去修改它。不能为它们提供 setter 方法,同时也不能通过任何函数泄露对此成员的引用。要记住,将对象声明为 final 只是保证它不会被重新赋予另一个值,但是仍然可以通过此引用来修改引用对象的属性。这一点是关键,面试官通常喜欢听到你强调这一点。

问题四:String 的 substring()方法是如何在内部实现的呢?

Java 面试中的一个好问题,你需要答出“substring 方法会通过原字符串创建一个新的对象”,不然你的回答肯定不能让人满意。此问题也常被用于测试应聘者是否了解 substring()可能引发的内存泄漏风险。在 Java1.7 版本之前,substring 会持有原字符串字符数组的引用。这就表明,倘若从 1GB 大小的字符串中截取 5 个字符,那么这 5 个字符会阻止 1GB 内存被回收,原因是此引用为强引用。

Java1.7 时,这个问题得到了解决。原字符串的字符数组不再被引用。然而,这一改变使得 substring()创建字符串的操作变得更加耗时。之前的开销是 O(1),而现在最坏情况是 O(n)。

问题五:能否写一个单例模式,并且保证实例的唯一性?

这是 Java 一个较为核心的问题。面试官期望你知晓,在编写单例模式时,应当对实例的初始化情况进行双重检查。要记住,对实例的声明需使用 Volatile 关键字,这样能确保单例模式具有线程安全性。以下是一段示例,它展示了如何以一种线程安全的方式来实现单例模式:

public class Singleton {    private static volatile Singleton _instance;    Singleton 上的双重检查锁定代码。 这种代码用于在多线程环境中确保单例对象的正确初始化。 它通过在获取对象实例之前进行两次检查来避免不必要的同步开销。 第一次检查是为了判断对象是否已经实例化,如果已经实例化则直接返回该实例。 第二次检查是在第一次检查为 false 时进行的,以确保在对象实例化过程中没有其他线程同时进行实例化操作。 这样可以提高性能,同时保证单例的线程安全性。@return Singelton instance     */    public static Singleton getInstance() {        if (_instance == null) {            synchronized (Singleton.class) {                if (_instance == null3. (当出现这种情形时){ 该实例为new调用 Singleton 函数;执行后续的代码块;完成相关的操作;结束整个流程。return _instance;    }}

在写存储过程或者在 Java 里调用存储过程时,如何处理错误情况呢?具体而言,需要考虑以下方面:比如在存储过程内部,要通过特定的错误处理机制来捕获和处理可能出现的错误;在 Java 调用存储过程时,要根据返回的状态码或异常信息来判断是否发生了错误,并采取相应的处理措施,如输出错误信息、回滚事务等。

这是一个较为棘手的 Java 面试题,其答案并非固定。我的答案为,在编写存储过程时,倘若有操作失败的情况,就必须要返回错误码。然而,当在调用存储过程时出现错误,捕捉 SQLException 便是唯一可行的做法。

Executor.submit()和Executor.execute()这两个方法存在哪些区别?

前者会返回一个 Future 对象,借助这个对象能够获取到工作线程所执行的结果。

当考察异常处理时,会发现另一个不同。使用 execute 提交的任务抛出异常时,此异常会由未捕捉异常处理过程(uncaughtexceptionhandler)来处理。若未显式指定异常处理器,在默认情况下,只会通过 System.err 打印出错误堆栈。

当用 submit 提交一个任务时,若此任务抛出异常(不论是否为运行时异常),该异常就会成为任务返回对象的一部分。在这种情形下,当调用 Future.get()方法时,此方法会重新抛出该异常,并且会用 ExecutionException 进行包装。

问题八:工厂模式和抽象工厂模式有何不同?

抽象工厂模式提供了更高级别的抽象。不同的工厂类都承继了同一个抽象工厂方法,然而它们会依据工厂的类别来创建不同的对象。比如,AutomobileFactory、UserFactory、RoleFactory 都承继了 AbstractFactory,并且每个工厂类都会创建自身对应类型的对象。下面是工厂模式和抽象工厂模式对应的UML图。

问题九:单例模式是什么?在创建单例对象时,是将整个方法都标记为 synchronized 好呢,还是仅仅把创建的语句标记为 synchronized 好?

在 Java 里,有一种类叫做单例类,这种类在整个 Java 程序中只会有一份实例。比如 java.lang.Runtime 就是一个单例类。在 Java4 版本以及之前去创建单例是比较麻烦的。然而,自从 Java5 引入了 Enum 类型之后,创建单例这件事就变得简单了。可以去看看我那篇关于如何使用 Enum 来创建单例类的文章,并且再看看问题五,以了解在创建单例类的时候如何进行双重检查。

能否给出一段用 Java4 来遍历一个 HashMap 的代码?能否给出一段用 Java5 来遍历一个 HashMap 的代码?

事实上,用 Java 有以下四种遍历任何一个 Map 的方式。一种是利用 keySet()方法获取所有的键,接着遍历这些键,之后再通过 get()方法获取对应的值。第二种方法是使用 entrySet()获取键值对的集合,然后运用 foreach 语句来遍历这个集合,在遍历过程中,获得的每个键值对已经包含了键和值。这种方式更为优。因为在每轮遍历时,既能获得 key 又能获得 value,无需调用 get()方法。而 get()方法在 bucket 位置有巨大链表时,性能开销为 O(n)。第三种方法是先获取 entrySet,然后用 iterator 依次获取每个键值对。获得 keyset 之后,可通过 iterator 依次获取每个 key。接着,依据获取到的 key 来调用 get 方法。

你在何种情况下会对 hashCode()和 equals()方法进行重写?

当你需要依据业务逻辑进行相等性判断,而非依据对象相等性判断时,就需要重写这两个函数了。比如,两个 Employee 对象相等的依据是它们具有相同的 emp_id,即便它们可能是两个不同的 Object 对象,并且分别在不同的地方被创建。如果你准备将它们当作 HashMap 中的 key 来使用,那么你就必须重写这两个方法。现在,在 Java 中,根据 equals-hashcode 的一个约定,当你重写 equals 方法时,必须同时重写 hashcode 方法,不然的话,你就会打破诸如 Set、Map 等集合能够正常工作的约定。

问题十二:如果不重写hashCode方法会有什么问题?

如果不重写 equals 方法,那么 equals 和 hashCode 之间的约定就会遭到破坏。也就是说,当通过 equals 方法判断为相等的两个对象,它们的 hashCode 必须是相同的。如果不重写 hashCode 方法,对于使用 equals 方法返回值为 true 的两个对象,当它们插入同一个 map 时,由于 hashCode 返回不同,仍会被插入到两个不同位置。这样就违背了 HashMap 的原本目的,因为 Map 本身不允许存入两个 key 相同的值。使用 put 方法插入一个对象时,HashMap 会先对该对象计算 hashcode。接着,依据这个 hashcode 找到存储位置(bucket)。之后,遍历此存储位置上的所有 Map.Entry 对象,以查看是否与待插入对象相同。倘若没有提供 hashCode,那么这些操作就都无法进行。

问题十三:我们是要对整个 getInstance()方法进行同步呢,还是仅仅对 getInstance()方法中的关键部分进行同步?

只同步关键部分(CriticalSection)。因为若同步整个方法,每次线程调用 getInstance()方法时都会等待其他线程调用完成,即便此方法中未执行对象创建操作。也就是说,只需同步创建对象的代码,而创建对象的代码只会执行一次。对象创建完成后,就没必要再对方法进行同步保护了。实际上,从性能角度看,对方法进行同步保护这种编码方式很糟糕,会使性能降低 10 到 20 倍。以下是单例模式的 UML 图。

创建线程安全的单例对象有多种方法,你可以顺便提一下。另外还需补充说明这一点。

问题十四:在调用 HashMap 的 get()方法时,equals()方法起到了怎样的作用?在调用 HashMap 的 get()方法时,hashCode()方法起到了怎样的作用?

这个问题是对问题十二的补充。应聘者需要知道,一旦提及 hashCode()方法,人们大概率会询问 HashMap 是怎样使用这个函数的。当向 HashMap 插入一个 key 时,首先会调用这个对象的 hashCode()方法,其调用结果会被用于计算将要存储的位置(bucket)。

某个位置可能以链表方式包含多个 Map.Entry 对象。因此,HashMap 会使用 equals()方法,将此对象与所有这些 Map.Entry 所包含的 key 进行对比,以确定此 key 对象是否已经存在。

问题十五:在Java中如何避免死锁?

你能够通过打破彼此等待的状况来防止死锁。要做到这一点,就需要在代码里恰当安排获取和释放锁的先后顺序。倘若获取锁的顺序是固定的,并且获取的顺序与释放的顺序恰好相反,那么就不会具备产生死锁的条件了。

创建字符串对象时,一种方式是使用字面值,另一种方式是使用 newString()构造器,这两种方式有哪些不同呢?

当使用 newString 构造器去创建字符串时,字符串的值会在堆里被创建,并且不会被添加到 JVM 的字符串池中。与之相反,通过字面值创建的 String 对象会被放置到堆的 PermGen 段中。例如:

String str=new String(“Test”);

这句代码创建的对象 str 不会自动被放入字符串池中。我们必须显式地调用 String.intern()方法,才能够将它放入字符串池中。只有当你使用字面值来创建字符串时,Java 才会自动把它放入字符串池中,例如:Strings = "Test"。这里有个易被忽视的地方需顺便提及。当把参数“Test”传入构造器时,此参数为字面值,所以它会在字符串池中另外保存一份。

下图很好地解释了这种差异。

什么是不可修改对象?能否写一个不可修改对象的例子?

有些对象一旦被创建就不能被修改。对于这类对象,任何改动的结果都是会创建一个新的对象,而不是在原对象上进行修改。比如在 Java 中,String 类就是不可修改的。大多数这类类通常是 final 类型的,因为这样能避免自身被继承后被覆盖方法,而在被覆盖的方法里,不可修改的特性就难以得到保障了。你通常可以通过把类的成员设置为 private 且非 final 的方式来达到同样的效果。

另外,你需保证你的类不通过任何方式暴露成员,尤其是那些可修改类型的成员。同样,若你的方法接收客户类传入的可修改对象,应使用复制的对象,以防止客户代码修改刚传入的可修改类。例如,传入 java.util.Date 对象时,应自行使用 clone() 方法获取副本。

当你通过类函数返回一个可修改对象时,需采取类似防护措施,返回类成功的副本,以防止客户代码通过此引用修改成员对象的属性。绝对不要直接将可修改成员返回给客户代码。

如何用最简单的方式在不使用任何分析工具的情况下计算某个方法的执行所花费的时间呢? 可以通过记录方法开始执行的时间点,然后在方法执行结束后记录结束时间点,最后用结束时间点减去开始时间点,就可以得到该方法执行所花费的时间。 也可以使用一些基础的编程语言中的计时函数来实现,比如在 Python 中可以使用 time 模块的 time()函数来记录时间。 先调用 time()函数获取开始时间,在方法执行完毕后再调用一次 time()函数获取结束时间,两者相减就能得出执行时间。 不同的编程语言可能有不同的计时方式和函数,但基本原理是相似的。

获取一个系统时间,这是在执行此方法之前;再获取一个系统时间,这是在执行此方法之后;然后取这两个时间的差值,这样就能得到此方法所花费的时间。

需要注意的是,若执行此方法所花费的时间极为短暂,那么所得出的时间值或许会是 0ms。在此情况下,你可以在一个计算量较大的方法上尝试其效果。

long start获取当前时间戳并将其赋值给一个变量,然后调用 method 方法,同时声明一个 long 类型的变量。end获取当前时间的毫秒数;然后输出“执行所花费的时间是”加上获取到的时间差值。end-start));

当你要将某个类用作 HashMap 的 key 时,你需要重写这个类的哪两个方法呢?首先,你需要重写 equals 方法,用于判断两个对象是否相等。其次,你还需要重写 hashCode 方法,用于计算对象的哈希码。

要让类能够在 HashMap 或 Hashtable 中当作 key 使用,就必须把这个类自身的 equals()和 hashCode()方法给实现出来。具体情况可参考问题十四。

你怎样阻止客户代码直接对自己的类的构造方法进行初始化呢?举例来说,你拥有一个名为 Cache 的接口以及两个具体的实现类 MemoryCache 和 DiskCache,那么你要怎样确保这两个类禁止客户代码通过 new 关键字来获取它们的实例呢?

这最后一个问题留给你们当作练习。我相信你们可以找到正确的方法。因为这是一个重要的方法,它能让你们将类的实现掌控在自己手中,并且还能为以后的维护带来巨大的好处。

你可能想看:
« 上一篇
下一篇 »

相关推荐

java中什么是冒泡,冒泡算法

2026年09月06日

27阅读

java中冒号,java双冒号

2026年09月06日

26阅读

java中的->,java中的是什么意思

2026年09月06日

24阅读

java中的super用法,Java中super的说明

2026年09月06日

28阅读

java中反射的应用,java中反射的应用场景

2026年09月06日

25阅读

java中开关的定义,Java关键字有哪些

2026年09月06日

25阅读