本文收集了 20 个来自投资银行的 Java 面试题, 每一个都特别经典, 分享给各位 Java 开发者
问题一: 在多线程环境中使用 HashMap 会有什么问题? 在什么情况下使用 get()方法会产生无限循环?
HashMap 本身没有什么问题, 有没有问题取决于你是如何使用它的比如, 你在一个线程里初始化了一个 HashMap 然后在多个其他线程里对其进行读取, 这肯定没有任何问题有个例子就是使用 HashMap 来存储系统配置项当有多于一个线程对 HashMap 进行修改操作的时候才会真正产生问题, 比如增加删除更新键值对的时候因为 put()操作可以造成重新分配存储大小 (re-sizeing) 的动作, 因此有可能造成无限循环的发生, 所以这时需要使用 Hashtable 或者 ConcurrentHashMap, 而后者更优
问题二: 不重写 Bean 的 hashCode()方法是否会对性能带来影响?
这个问题非常好, 每个人可能都会有自己的体会按照我掌握的知识来说, 如果一个计算 hash 的方法写得不好, 直接的影响是, 当向 HashMap 中添加元素的时候会更频繁地造成冲突, 因此最终增加了耗时但是自从 Java 8 开始, 这种影响不再像前几个版本那样显著了, 因为当冲突的发生超出了一定的限度之后, 链表类的实现将会被替换成二叉树 (binary tree) 实现, 这时你仍可以得到 O(logN)的开销, 优于链表类的 O(n)
问题三: 对于一个不可修改的类, 它的每个对象是不是都必须声明成 final 的?
不尽然, 因为你可以通过将成员声明成非 final 且 private, 并且不要在除了构造函数的其他地方来修改它不要为它们提供 setter 方法, 同时不会通过任何函数泄露出对此成员的引用需要记住的是, 把对象声明成 final 仅仅保证了它不会被重新赋上另外一个值, 你仍然可以通过此引用来修改引用对象的属性这一点是关键, 面试官通常喜欢听到你强调这一点
问题四: String 的 substring()方法内部是如何实现的?
又一个 Java 面试的好问题, 你应该答出 substring 方法通过原字符串创建了一个新的对象, 否则你的回答肯定是不能令人满意的这个问题也经常被拿来测试应聘者对于 substring()可能带来的内存泄漏风险是否有所了解直到 Java 1.7 版本之前, substring 会保存一份原字符串的字符数组的引用, 这意味着, 如果你从 1GB 大小的字符串里截取了 5 个字符, 而这 5 个字符也会阻止那 1GB 内存被回收, 因为这个引用是强引用
到了 Java 1.7, 这个问题被解决了, 原字符串的字符数组已经不再被引用, 但是这个改变也使得 substring()创建字符串的操作更加耗时, 以前的开销是 O(1), 现在最坏情况是 O(n)
问题五: 能否写一个单例模式, 并且保证实例的唯一性?
这算是 Java 一个比较核心的问题了, 面试官期望你能知道在写单例模式时应该对实例的初始化与否进行双重检查记住对实例的声明使用 Volatile 关键字, 以保证单例模式是线程安全的下面是一段示例, 展示了如何用一种线程安全的方式实现了单例模式:
- public class Singleton {
- private static volatile Singleton _instance;
- /**
- * Double checked locking code on Singleton
- * @return Singelton instance
- */
- public static Singleton getInstance() {
- if (_instance == null) {
- synchronized (Singleton.class) {
- if (_instance == null) {
- _instance = new Singleton();
- }
- }
- }
- return _instance;
- }
- }
问题六: 你在写存储过程或者在 Java 里调用存储过程的时候如何来处理错误情况?
这是个很棘手的 Java 面试题, 答案也并不固定我的答案是, 写存储过程的时候一旦有操作失败, 则一定要返回错误码但是在调用存储过程的时候出错的话捕捉 SQLException 却是唯一能做的
问题七: Executor.submit()和 Executor.execute()这两个方法有什么区别?
此问题来自另外一篇文章, 15 个最流行的 java 多线程面试问题, 现在对熟练掌握并发技能的开发者的需求越来越大, 因此这个问题也越来越引起大家的重视答案是: 前者返回一个 Future 对象, 可以通过这个对象来获得工作线程执行的结果
当我们考察异常处理的时候, 又会发现另外一个不同当你使用 execute 提交的任务抛出异常时, 此异常将会交由未捕捉异常处理过程来处理 (uncaught exception handler), 当你没有显式指定一个异常处理器的话, 默认情况下仅仅会通过 System.err 打印出错误堆栈当你用 submit 来提交一个任务的时候, 这个任务一旦抛出异常(无论是否是运行时异常), 那这个异常是任务返回对象的一部分对这样一种情形, 当你调用 Future.get() 方法的时候, 这个方法会重新抛出这个异常, 并且会使用 ExecutionException 进行包装
问题八: 工厂模式和抽象工厂模式有何不同?
抽象工厂模式提供了多一级的抽象不同的工厂类都继承了同一个抽象工厂方法, 但是却根据工厂的类别创建不同的对象例如, AutomobileFactory, UserFactory, RoleFactory 都继承了 AbstractFactory, 但是每个工厂类创建自己对应类型的对象下面是工厂模式和抽象工厂模式对应的 UML 图
问题九: 什么是单例模式? 创建单例对象的时候是将整个方法都标记为 synchronized 好还是仅仅把创建的的语句标记为 synchronized 好?
在 Java 中, 单例类是指那些在整个 Java 程序中只存在一份实例的类, 例如 java.lang.Runtime 就是一个单例类在 Java 4 版本及以前创建单例会有些麻烦, 但是自从 Java 5 引入了 Enum 类型之后, 事情就变得简单了可以去看看我的关于如何使用 Enum 来创建单例类的文章, 同时再看看问题五来看看如何在创建单例类的时候进行双重检查
问题十: 能否写一段用 Java 4 或 5 来遍历一个 HashMap 的代码?
事实上, 用 Java 可以有四种方式来遍历任何一个 Map, 一种是使用 keySet()方法获取所有的键, 然后遍历这些键, 再依次通过 get()方法来获取对应的值第二种方法可以使用 entrySet()来获取键值对的集合, 然后使用 for each 语句来遍历这个集合, 遍历的时候获得的每个键值对已经包含了键和值这种算是一种更优的方式, 因为每轮遍历的时候同时获得了 key 和 value, 无需再调用 get()方法, get()方法在那种如果 bucket 位置有一个巨大的链表的时候的性能开销是 O(n)第三种方法是获取 entrySet 之后用 iterator 依次获取每个键值对第四种方法是获得 key set 之后用 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, 然后根据它来找到存储位置(bucket), 然后遍历此存储位置上所有的 Map.Entry 对象来查看是否与待插入对象相同如果没有提供 hashCode 的话, 这些就都做不到了
问题十三: 我们要同步整个 getInstance()方法, 还是只同步 getInstance()方法中的关键部分?
答案是: 仅仅同步关键部分 (Critical Section) 这是因为, 如果我们同步整个方法的话, 每次有线程调用 getInstance()方法的时候都会等待其他线程调用完成才行, 即使在此方法中并没有执行对象的创建操作换句话说, 我们只需要同步那些创建对象的代码, 而创建对象的代码只会执行一次一旦对象创建完成之后, 根本没有必要再对方法进行同步保护了事实上, 从性能上来说, 对方法进行同步保护这种编码方法非常要命, 因为它会使性能降低 10 到 20 倍下面是单例模式的 UML 图
再补充一下, 创建线程安全的单例对象有多种方法, 你也可以顺便提一下
问题十四: HashMap, 在调用 get()方法的时候 equals()和 hashCode()方法都起了什么样的作用?
这个问题算是对问题十二的补充, 应聘者应该知道的是, 一旦你提到了 hashCode()方法, 人们很可能要问 HashMap 是如何使用这个函数的当你向 HashMap 插入一个 key 的时候, 首先, 这个对象的 hashCode()方法会被调用, 调用结果用来计算将要存储的位置(bucket)
因为某个位置上可能以链表的方式已经包含了多个 Map.Entry 对象, 所以 HashMap 会使用 equals()方法来将此对象与所有这些 Map.Entry 所包含的 key 进行对比, 以确定此 key 对象是否已经存在
问题十五: 在 Java 中如何避免死锁?
你可以通过打破互相等待的局面来避免死锁为了达到这一点, 你需要在代码中合理地安排获取和释放锁的顺序如果获得锁的顺序是固定的, 并且获得的顺序和释放的顺序刚好相反的话, 就不会产生出现死锁的条件了
问题十六: 创建字符串对象的时候, 使用字面值和使用 new String()构造器这两种方式有什么不同?
当我们使用 new String 构造器来创建字符串的时候, 字符串的值会在堆中创建, 而不会加入 JVM 的字符串池中相反, 使用字面值创建的 String 对象会被放入堆的 PermGen 段中例如:
String str=new String(Test);
这句代码创建的对象 str 不会放入字符串池中, 我们需要显式调用 String.intern()方法来将它放入字符串池中仅仅当你使用字面值创建字符串时, Java 才会自动将它放入字符串池中, 比如: String s=Test 顺便提一下, 这里有个容易被忽视的地方, 当我们将参数 Test 传入构造器的时候, 这个参数是个字面值, 因此它也会在字符串池中保存另外一份想了解更多关于字面值字符串和字符串对象之间的差别, 请看这篇文章
下图很好地解释了这种差异
问题十七: 什么是不可修改对象(Immutable Object)? 你能否写一个例子?
不可修改对象是那些一旦被创建就不能修改的对象对这种对象的任何改动的后果都是会创建一个新的对象, 而不是在原对象本身做修改例如 Java 中的 String 类就是不可修改的大多数这样的类通常都是 final 类型的, 因为这样可以避免自己被继承继而被覆盖方法, 在覆盖的方法里, 不可修改的特性就难以得到保证了你通常也可以通过将类的成员设置成 private 但是非 final 的来获得同样的效果
另外, 你同样要保证你的类不要通过任何方法暴露成员, 特别是那些可修改类型的成员同样地, 当你的方法接收客户类传入的可修改对象的话, 你应该使用一个复制的对象来防止客户代码来修改这个刚传入的可修改类比如, 传入 java.util.Date 对象的话, 你应该自己使用 clone()方法来获得一个副本
当你通过类函数返回一个可修改对象的时候, 你也要采取类似的防护措施, 返回一个类成功的副本, 防止客户代码通过此引用修改了成员对象的属性千万不要直接把你的可修改成员直接返回给客户代码
问题十八: 如何在不使用任何分析工具的情况下用最简单的方式计算某个方法的执行所花费的时间?
在执行此方法之前和之后获取一个系统时间, 取这两个时间的差值, 即可得到此方法所花费的时间
需要注意的是, 如果执行此方法花费的时间非常短, 那么得到的时间值有可能是 0ms 这时你可以在一个计算量比较大的方法上试一下效果
- long start=System.currentTimeMillis();
- method();
- long end=System.currentTimeMillis();
- System.out.println("Time taken for execution is"+(end-start));
问题十九: 当你要把某个类作为 HashMap 的 key 使用的话, 你需要重写这个类的哪两个方法?
为了使类可以在 HashMap 或 Hashtable 中作为 key 使用, 必须要实现这个类自己的 equals()和 hashCode()方法具体请参考问题十四
问题二十: 你如何阻止客户代码直接初始化你的类的构造方法? 例如, 你有一个名为 Cache 的接口和两个具体的实现类 MemoryCache 和 DiskCache, 你如何保证这两个类禁止客户代码用 new 关键字来获取它们的实例?
我把这最后一个问题留给你做练习吧, 你可以在我给出答案之前好好思索一下我确信你能够找到正确的方法的, 因为这是将类的实现掌控在自己手中的一个重要的方法, 同时也能为以后的维护提供巨大的好处
译文链接: http://www.codeceo.com/20-java-interview-questions-from-investment-banks.html
来源: http://www.codeceo.com/20-java-interview-questions-from-investment-banks.html