![[Java 面试突击训练] 一些 Java 基础面试题](http://pic.xiahunao.cn/yaotu/[Java 面试突击训练] 一些 Java 基础面试题)
前言这些问题主要来自 Java 核心部分 不涉及 Java EE 相关问题。你可能知道这些棘手的 Java 问题的答案或者觉得这些不足以挑战你的 Java 知识但这些问题都是容易在各种 Java 面试中被问到的都很难回答全面。具体问题为什么等待和通知是在 Object 类而不是 Thread 中声明的这个问题的好在于它能反映了面试者对等待通知机制的了解以及他对此主题的理解是否明确。为什么在 Object 类中定义 wait 和 notify 方法每个人都能说出一些理由。因此如果你去参加 Java 面试请确保对 wait 和 notify 机制有充分的了解并且可以轻松地使用 wait 来编写代码并通过生产者-消费者问题或实现阻塞队列等了解通知的机制。为什么等待和通知需要从同步块或方法中调用以及 Java 中的 waitsleep 和 yield 方法之间的差异如果你还没有读过你会觉得有趣。为何 waitnotify 和 notifyAll 属于 Object 类? 为什么它们不应该在 Thread 类中? 以下是我认为有意义的一些想法wait 和 notify 不仅仅是普通方法或同步工具更重要的是它们是 Java 中两个线程之间的通信机制对语言设计者而言, 如果不能通过 Java 关键字例如 synchronized实现通信此机制同时又要确保这个机制对每个对象可用那么 Object 类则是的正确声明位置。记住同步和等待通知是两个不同的领域不要把它们看成是相同的或相关的。同步是提供互斥并确保 Java 类的线程安全而 wait 和 notify 是两个线程之间的通信机制。每个对象都可上锁这是在 Object 类而不是 Thread 类中声明 wait 和 notify 的另一个原因在 Java 中为了进入代码的临界区线程需要锁定并等待锁定他们不知道哪些线程持有锁而只是知道锁被某个线程持有 并且他们应该等待取得锁, 而不是去了解哪个线程在同步块内并请求它们释放锁定。Java 是基于 Hoare 的监视器的思想在Java中所有对象都有一个监视器。线程在监视器上等待为执行等待我们需要2个参数一个线程一个监视器任何对象。在 Java 设计中线程不能被指定它总是运行当前代码的线程。但是我们可以指定监视器这是我们称之为等待的对象。这是一个很好的设计因为如果我们可以让任何其他线程在所需的监视器上等待这将导致“入侵”导致在设计并发程序时会遇到困难。请记住在 Java 中所有在另一个线程的执行中侵入的操作都被弃用了例如 stop 方法。为什么Java中不支持多重继承这个 Java 核心问题很难回答因为你的答案可能不会让面试官满意在大多数情况下面试官正在寻找答案中的关键点如果你提到这些关键点面试官会很高兴。在 Java 中回答这种棘手问题的关键是准备好相关主题以应对后续的各种可能的问题。为什么Java不支持多重继承, 可以考虑以下两点第一个原因是围绕钻石形继承问题产生的歧义考虑一个类 A 有 foo() 方法, 然后 B 和 C 派生自 A, 并且有自己的 foo() 实现现在 D 类使用多个继承派生自 B 和 C如果我们只引用 foo()编译器将无法决定它应该调用哪个 foo()。这也称为 Diamond 问题因为这个继承方案的结构类似于菱形。如果你把这个理由告诉面试官他会问为什么 C 和 Python 可以支持多重继承。在 C 和 Python 中又称这种情况为二义性对此 C 的解决办法是在调用前指定类名或者使用同名覆盖。而 Python 也是需要指明需要使用的是哪个父类的方法。并且其实这两种语言在支持多重继承这个角度上也都建议要把这种多个父类中的出现同名方法的情况给尽量避免掉。第二个也是更有说服力的理由是多重继承确实使设计复杂化并在转换、构造函数链接等过程中产生问题假设你需要多重继承的情况并不多简单起见明智的决定是省略它。此外Java 可以通过使用接口支持单继承来避免这种歧义。现在我们想一想多接口实现为何就不会存在二义性问题由于接口只有方法声明而且没有提供任何实现从JDK1.8之后接口中允许给出一些默认方法的实现这里不考虑这个就算一个类实现了多个接口且这些接口中存在某个同名方法但是我们要清楚的知道这个同名方法最终需要由这个类自己来实现所以并不会出现二义性的问题。然后有的人可能就会问了不同接口中定义了相同的方法不会造成二义性的问题但是接口允许定义 public static 的变量啊那如果变量名一样不也是会有二义性问题吗答案是不会这种情况实际的结果是无法通过编译。为什么Java不支持运算符重载为什么 C 支持运算符重载而 Java 不支持? 有人可能会说 “” 运算符在 Java 中已被重载用于字符串连接不要被这些论据所欺骗。与 C 不同Java 不支持运算符重载。Java 不能为程序员提供自由的标准算术运算符重载。虽然我不知道背后的真正原因但我认为以下说法有些道理为什么 Java 不支持运算符重载。简单性和清晰性清晰性是Java设计者的目标之一。设计者不是只想复制语言而是希望拥有一种清晰真正面向对象的语言。添加运算符重载比没有它肯定会使设计更复杂并且它可能导致更复杂的编译器, 或减慢 JVM因为它需要做额外的工作来识别运算符的实际含义并减少优化的机会, 以保证 Java 中运算符的行为。避免编程错误Java 不允许用户定义的运算符重载因为如果允许程序员进行运算符重载将为同一运算符赋予多种含义这将使任何开发人员的学习曲线变得陡峭事情变得更加混乱。据观察当语言支持运算符重载时编程错误会增加从而增加了开发和交付时间。由于 Java 和 JVM 已经承担了大多数开发人员的责任如在通过提供垃圾收集器进行内存管理时因为这个功能增加污染代码的机会, 成为编程错误之源, 因此没有多大意义。JVM 复杂性从JVM的角度来看支持运算符重载使问题变得更加困难。通过更直观更干净的方式使用方法重载也能实现同样的事情因此不支持 Java 中的运算符重载是有意义的。与相对简单的 JVM 相比复杂的 JVM 可能导致 JVM 更慢并为保证在 Java 中运算符行为的确定性从而减少了优化代码的机会。让开发工具处理更容易这是在 Java 中不支持运算符重载的另一个好处。省略运算符重载使语言更容易处理这反过来又更容易开发处理语言的工具例如 IDE 或重构工具。Java 中的重构工具远胜于 C。为什么 String 在 Java 中是不可变的这个问题可能有很多可能的答案而 String 类的唯一设计者可以放心地回答它。我认为以下几点解释了为什么 String 类在 Java 中是不可变的或 final 的。String 对象缓存在 String 池中一个字符串对象例如 “Test” 已被许多参考变量引用因此如果其中任何一个更改了值其他参数将自动受到影响。假设String ATest; String BTest;现在字符串 B 调用 “Test”.toUpperCase(), 将同一个对象改为“TEST”所以 A 也是 “TEST”这不是期望的结果。字符串已被广泛用作许多 Java 类的参数例如为了打开网络连接你可以将主机名和端口号作为字符串传递你可以将数据库 URL 作为字符串传递, 以打开数据库连接你可以通过将文件名作为参数传递给 File I/O 类来打开 Java 中的任何文件。如果 String 是可变的这将导致严重的安全威胁意思是有人可以访问他有权授权的任何文件然后可以故意或意外地更改文件名并获得对该文件的访问权限。由于不变性你无需担心这种威胁Java设计者确保没有人覆盖 String 类的任何行为。可以安全地共享许多线程这对于多线程编程非常重要并且避免了 Java 中的同步问题不变性也使得 String 实例在 Java 中是线程安全的这意味着你不需要从外部同步 String 操作。关于 String 的另一个要点是由截取字符串 substring 引起的内存泄漏这不是与线程相关的问题但也是需要注意的。允许 String 缓存其哈希码Java 中的不可变 String 缓存其哈希码并且不会在每次调用 String 的 hashcode 方法时重新计算这使得它在 Java 中的 HashMap 中使用的 HashMap 键非常快。简而言之因为 String 是不可变的所以没有人可以在创建后更改其内容这保证了 String 的 hashCode 在多次调用时是相同的。String 被类加载机制使用因此具有深刻和基本的安全考虑如果 String 是可变的加载“java.io.Writer” 的请求可能已被更改为加载 “mil.vogoon.DiskErasingWriter”。为什么 char 数组比 Java 中的 String 更适合存储密码这是一个真正艰难的核心Java面试问题并且需要对 String 的扎实知识才能回答这个问题。如果你还没有遇到过这种情况那么字符数组和字符串都可以用来存储文本数据但是要说清楚选择一个而不是另一个很难。任何与 String 相关的问题都必须对字符串的特殊属性有一些线索比如不变性他用它来说服访提问的人。在这里我们将探讨为什么你应该使用char[] 存储密码而不是 String 的一些原因。String 缓存池原因由于字符串在 Java 中是不可变的如果你将密码存储为纯文本它将在内存中可用直到垃圾收集器清除它。并且为了可重用性它会存在字符串池中很可能会保留在内存中持续很长时间从而构成安全威胁。由于任何有权访问内存转储的人都可以以明文形式找到密码这是另一个原因你应该始终使用加密密码而不是纯文本。由于字符串是不可变的所以不能更改字符串的内容因为任何更改都会产生新的字符串而如果你使用char[]你就可以将所有元素设置为空白或零。因此在字符数组中存储密码可以明显降低窃取密码的安全风险。Java 语言的建议Java 本身建议使用 JPasswordField 的 getPassword() 方法该方法返回一个 char[] 和不推荐使用的 getTex() 方法该方法以明文形式返回密码由于安全原因。应遵循 Java 团队的建议坚持标准而不是反对它。避免日志打印问题使用 String 时总是存在在日志文件或控制台中打印纯文本的风险但如果使用 Array则不会打印数组的内容而是打印其内存位置。虽然不是一个真正的原因但仍然有道理。我还建议使用散列或加密的密码而不是纯文本并在验证完成后立即从内存中清除它。因此在Java中用字符数组用存储密码比字符串是更好的选择。虽然仅使用 char[] 还不够还你需要擦除内容才能更安全。String strPassword“Unknown”;char[]charPasswordnewchar[]{Unkwon};System.out.println(字符串密码strPassword);System.out.println(字符密码charPassword);// 输出字符串密码Unknown 字符密码[C110b053如何使用双重检查锁定在 Java 中创建线程安全的单例这个 Java 问题也常被问什么是线程安全的单例你怎么创建它。好吧在Java 5之前的版本使用双重检查锁定创建单例 Singleton 时如果多个线程试图同时创建 Singleton 实例则可能有多个 Singleton 实例被创建。从 Java 5 开始使用 Enum 创建线程安全的Singleton很容易。但如果面试官坚持双重检查锁定那么你必须为他们编写代码。记得使用volatile变量。双检查锁定的单例下面的代码是单例模式中双重检查锁定的示例,此处的 getInstance() 方法检查两次以查看 INSTANCE 是否为空这就是为什么它被称为双检查锁定模式。我们看到双重校验锁即实现了延迟加载又解决了线程并发问题同时还解决了执行效率问题是否真的就万无一失了呢这里要提到Java中的指令重排优化。所谓指令重排优化是指在不改变原语义的情况下通过调整指令的执行顺序让程序运行的更快。JVM 中并没有规定编译器优化相关的内容也就是说JVM 可以自由的进行指令重排序的优化。这个问题的关键就在于由于指令重排优化的存在导致初始化 Singleton 和将对象地址赋给 INSTANCE 字段的顺序是不确定的。在某个线程创建单例对象时在构造方法被调用之前就为该对象分配了内存空间并将对象的字段设置为默认值。此时就可以将分配的内存地址赋值给 INSTANCE 字段了然而该对象可能还没有初始化。若紧接着另外一个线程来调用 getInstance取到的就是状态不正确的对象程序就会出错。以上就是双重校验锁会失效的原因不过还好在 JDK1.5 之后版本增加了 volatile 关键字。volatile 的一个语义是禁止指令重排序优化也就保证了 INSTANCE 变量被赋值的时候对象已经是初始化过的从而避免了上面说到的问题。publicclassSingleton{privatestaticvolatileSingleton INSTANCEnull;privateSingleton(){}publicstaticSingletongetInstance(){if(INSTANCEnull){synchronized(Singleton.class){if(INSTANCEnull){INSTANCEnewSingleton();}}}returninstance;}}枚举的单例这是我们通常声明枚举的单例的方式它可能包含实例变量和实例方法。但为了简单起见我没有使用任何实例方法只是要注意如果你使用的实例方法且该方法能改变对象的状态的话则需要确保该方法的线程安全。默认情况下创建枚举实例是线程安全的但 Enum 上的任何其他方法是否线程安全都是程序员的责任。publicenumSingleton{INSTANCE;publicvoidwhateverMethod(){}}静态内部类的单例这种方式同样利用了类加载机制来保证只创建一个 INSTANCE 实例。它与饿汉模式一样也是利用了类加载机制因此不存在多线程并发的问题。不一样的是它是在内部类里面去创建对象实例。这样的话只要应用中不使用内部类JVM 就不会去加载这个单例类也就不会创建单例对象从而实现懒汉式的延迟加载。也就是说这种方式可以同时保证延迟加载和线程安全。publicclassSingleton{privatestaticclassSingletonHolder{publicstaticSingleton INSTANCEnewSingleton();}privateSingleton(){}publicstaticSingletonnewInstance(){returnSingletonHolder.INSTANCE;}}编写 Java 程序时, 如何在 Java 中创建死锁并修复它经典但核心 Java 面试问题之一。如果你没有参与过多线程并发 程序的编码你可能会失败。// 死锁参考代码publicclassDeadLockDemo{// 此方法请求两个锁第一个字符串然后整数publicvoidmethod1(){synchronized(String.class){System.out.println(Aquired lock on String.class object);synchronized(Integer.class){System.out.println(Aquired lock on Integer.class object);}}}// 相反的顺序即首先整数然后字符串publicvoidmethod2(){synchronized(Integer.class){System.out.println(Aquired lock on Integer.class object);synchronized(String.class){System.out.println(Aquired lock on String.class object);}}}}如果 method1() 和 method2() 都由两个或多个线程调用则存在死锁的可能性因为如果线程 1 在执行 method1() 时在 Sting 对象上获取锁线程 2 在执行 method2() 时在 Integer 对象上获取锁等待彼此释放 Integer 和 String 上的锁以继续进行一步但这永远不会发生。如果你仔细查看了上面的代码那么你可能已经发现死锁的真正原因不是多个线程而是它们请求锁的方式如果你提供有序访问则问题将得到解决。publicclassDeadLockFixed{/** * 两种方法现在都以相同的顺序请求锁,首先采用整数,然后是 String。 * 你也可以做反向,例如,第一个字符串,然后整数, * 只要两种方法都请求锁定,两者都能解决问题 * 顺序一致。 */publicvoidmethod1(){synchronized(Integer.class){System.out.println(Aquired lock on Integer.class object);synchronized(String.class){System.out.println(Aquired lock on String.class object);}}}publicvoidmethod2(){synchronized(Integer.class){System.out.println(Aquired lock on Integer.class object);synchronized(String.class){System.out.println(Aquired lock on String.class object);}}}}谈谈你对序列化的认识Java 中的可序列化接口和可外部接口之间的区别是什么这是 Java 序列化访谈中最常问的问题。给我们提供 writeExternal() 和 readExternal() 方法这让我们灵活地控制 Java 序列化机制而不是依赖于 Java 的默认序列化。正确实现 Externalizable 接口可以显著提高应用程序的性能。可序列化的方法有多少如果没有方法,那么可序列化接口的用途是什么可序列化 Serializalbe 接口存在于 java.io 包中构成了 Java 序列化机制的核心。它没有任何方法在 Java 中也称为标记接口。当类实现 java.io.Serializable 接口时它将在 Java 中变得可序列化并指示编译器使用 Java 序列化机制序列化此对象。什么是 serialVersionUID 如果你不定义这个, 会发生什么erialVersionUID 是一个 private static final long 型 ID当它被印在对象上时它通常是对象的哈希码,你可以使用 serialver 这个 JDK 工具来查看序列化对象的 serialVersionUID。SerialVerionUID 用于对象的版本控制。也可以在类文件中指定 serialVersionUID。不指定 serialVersionUID的后果是当你添加或修改类中的任何字段时则已序列化类将无法恢复因为为新类和旧序列化对象生成的 serialVersionUID 将有所不同。Java 序列化过程依赖于正确的序列化对象恢复状态的并在序列化对象序列版本不匹配的情况下引发 java.io.InvalidClassException 无效类异常。序列化时,你希望某些成员不要序列化你如何实现它另一个经常被问到的序列化面试问题。这也是一些时候也问如什么是瞬态 trasient 变量瞬态和静态变量会不会得到序列化等。所以如果你不希望任何字段是对象的状态的一部分然后声明它静态或瞬态这样就不会是在 Java 序列化过程中被包含在内。如果类是可序列化的但其超类不是则反序列化后从超类继承的实例变量的状态如何Java 序列化过程仅在对象层次都是可序列化结构中继续即实现 Java 中的可序列化接口并且从超类继承的实例变量的值将通过调用构造函数初始化。一旦构造函数链接启动就不可能停止。因此即使层次结构中较高的类实现可序列化接口也将执行构造函数。正如你从陈述中看到的这个序列化面试问题看起来非常棘手和有难度但如果你熟悉关键概念则并不难。是否可以自定义序列化过程或者是否可以覆盖 Java 中的默认序列化过程答案是肯定的可以。我们都知道对于序列化一个对象需调用 ObjectOutputStream.writeObject()并用 ObjectInputStream.readObject() 读取对象。但 Java 虚拟机为你提供的还有一件事是定义这两个方法。如果在类中定义这两种方法则 JVM 将调用这两种方法而不是应用默认序列化机制。你可以在此处通过执行任何类型的预处理或后处理任务来自定义对象序列化和反序列化的行为。假设新类的超类实现可序列化接口如何避免新类被序列化如果类的 Super 类已经在 Java 中实现了可序列化接口那么它在 Java 中已经可以序列化因为你不能取消接口它不可能真正使它无法序列化类。但是有一种方法可以避免新类序列化。为了避免 Java 序列化你需要在类中实现 writeObject() 和 readObject() 方法并且需要从该方法引发不序列化异常NotSerializableException。在 Java 中的序列化和反序列化过程中使用哪些方法这是很常见的面试问题在序列化上面试官试图知道你是否熟悉 readObject() 的用法、writeObject()、readExternal() 和 writeExternal()。Java 序列化由 java.io.ObjectOutputStream 类完成。该类是一个筛选器流它封装在较低级别的字节流中以处理序列化机制。要通过序列化机制存储任何对象我们调用 ObjectOutputStream.writeObject()并反序列化该对象。我们称之为 ObjectInputStream.readObject() 方法。调用 writeObject() 方法在 java 中触发序列化过程。关于 readObject() 方法需要注意的一点很重要一点是它用于从持久性读取字节并从这些字节创建对象并返回一个对象该对象需要类型强制转换为正确的类型。假设你有一个类它序列化并存储在持久性中然后修改了该类以添加新字段。如果对已序列化的对象进行反序列化会发生什么情况这取决于类是否具有其自己的 serialVersionUID。正如我们从上面的问题知道如果我们不提供 serialVersionUID则 Java 编译器将生成它通常它等于对象的哈希代码。通过添加任何新字段有可能为该类新版本生成的新 serialVersionUID 与已序列化的对象不同。在这种情况下Java 序列化 API 将引发 java.io.InvalidClassException因此建议在代码中拥有自己的 serialVersionUID并确保在单个类中始终保持不变。你能用 Java 覆盖静态方法吗如果我在子类中创建相同的方法是编译时错误你不能覆盖Java中的静态方法因为方法覆盖基于运行时的动态绑定静态方法在编译时使用静态绑定进行绑定。虽然可以在子类中声明一个具有相同名称和方法签名的方法看起来可以在Java中覆盖静态方法但实际上这是方法隐藏。Java不会在运行时解析方法调用并且根据用于调用静态方法的 Object 类型将调用相应的方法。这意味着如果你使用父类的类型来调用静态方法那么原始静态将从父类中调用另一方面如果你使用子类的类型来调用静态方法则会调用来自子类的方法。简而言之你无法在Java中覆盖静态方法。