🔥码云GVP开源项目 12k star Uniapp+ElementUI 功能强大 支持多语言、二开方便! 广告
# Java 核心面试问题 – 第 2 部分 > 原文: [https://howtodoinjava.com/interview-questions/core-java-interview-questions-series-part-2/](https://howtodoinjava.com/interview-questions/core-java-interview-questions-series-part-2/) 在 [Java 面试问题系列:第 1 部分](//howtodoinjava.com/java/interviews-questions/core-java-interview-questions-series-part-1/ "Core java interview questions series : Part 1")中,我们讨论了面试官通常问的一些重要问题。 现在是推进该讨论的时候了。 在这篇文章中,我将在下面给出的问题列表中进行讨论。 ```java Why finalize() method should be avoided? Why HashMap should not be used in multithreaded environment? Can it cause infinite loop as well? Explain abstraction and encapsulation? How are they related? Difference between interfaces and abstract classes? How StringBuffer save the memory? Why wait and notify is declared in Object class instead of Thread ? Write Java program to create deadlock in Java and fix it ? What happens if your Serializable class contains a member which is not serializable? How do you fix it? Explain transient and volatile keywords in java? Difference between Iterator and ListIterator? ``` ## 为什么要避免使用`finalize()`方法? 我们都知道,在回收分配给对象的内存之前,垃圾收集器线程会调用[`finalize()`](https://docs.oracle.com/javase/8/docs/api/java/lang/Object.html#finalize--)方法的基本声明。 请参阅[这篇文章](//howtodoinjava.com/java/related-concepts/why-not-to-use-finalize-method-in-java/ "Why not to use finalize() method in java"),证明根本不能保证`finalize()`调用。 其他原因可能是: 1. `finalize()`方法无法像构造器一样在链接中工作。 这意味着就像您调用构造器时一样,所有超类的构造器都将被隐式调用。 但是,在使用`finalize`方法的情况下,则不遵循此方法。 超类的`finalize()`应该显式调用。 2. 由`finalize`方法引发的任何异常都将被 GC 线程忽略,并且不会进一步传播,实际上不会记录在日志文件中。 真糟糕,不是吗? 3. 另外,当您的类中包含`finalize()`时,也会影响性能。 Joshua bloch 在《Effective Java》(第 2 版)中说, “哦,还有一件事:使用终结器会严重影响性能。 在我的机器上,创建和销毁简单对象的时间约为 5.6 ns。 添加终结器会将时间增加到 2,400 ns。 换句话说,使用终结器创建和销毁对象要慢 430 倍。” ## 为什么不应该在多线程环境中使用`HashMap`? 它也会引起无限循环吗? 我们知道`HashMap`是非同步集合,因为它的同步计数器是`HashTable`。 因此,当您在多线程环境中访问集合时,所有线程都在访问集合的单个实例时,出于各种明显的原因,例如`HashTable`,使用`HashTable`更安全。 以避免脏读并保持数据一致性。 在最坏的情况下,这种多线程环境也可能导致无限循环。 是的,它是真实的。 `HashMap.get()`可能导致无限循环。 让我们看看如何? 如果查看[`HashMap.get(Object key)`](http://hg.openjdk.java.net/jdk8/jdk8/jdk/file/687fd7c7986d/src/share/classes/java/util/HashMap.java)方法的源代码,则如下所示: ```java public Object get(Object key) { Object k = maskNull(key); int hash = hash(k); int i = indexFor(hash, table.length); Entry e = table[i]; while (true) { if (e == null) return e; if (e.hash == hash &amp;&amp; eq(k, e.key)) return e.value; e = e.next; } } ``` 在多线程环境中,`while(true){...}`始终是运行时无限循环的受害者,因此`e.next`可以指向自身。 这将导致无限循环。 但是,`e.next`将如何指向自身(即)。 这可能在`void transfer(Entry[] newTable)`方法中发生,该方法在`HashMap`调整大小时调用。 ```java do { Entry next = e.next; int i = indexFor(e.hash, newCapacity); e.next = newTable[i]; newTable[i] = e; e = next; } while (e != null); ``` 如果调整大小并且同时其他线程试图修改映射实例,则这段代码很容易产生上述条件。 避免这种情况的唯一方法是在代码中使用同步,或者更好的方法是使用同步集合。 ## 解释抽象和封装? 它们有什么关系? #### 抽象 [抽象](//howtodoinjava.com/object-oriented/understanding-abstraction-in-java/)仅捕获与当前视角有关的那些对象的详细信息。 在[面向对象编程](//howtodoinjava.com/object-oriented/object-oriented-principles/)理论中,抽象涉及定义定义代表抽象“角色”的对象的功能,这些抽象“角色”可以执行工作,报告和更改其状态,并与系统中的其他对象“通信”。 任何编程语言中的抽象都可以通过多种方式工作。 从创建子例程到定义用于进行低级语言调用的接口可以看出。 一些抽象试图通过完全隐藏它们依次建立在其上的抽象,来限制程序员所需概念的广度。 例如设计模式。 通常,可以通过两种方式查看抽象: **数据抽象**是创建复杂数据类型并仅公开有意义的操作以与数据类型进行交互的方法,其中隐藏了外部工作中的所有实现细节。 **控制抽象**是识别所有此类语句并将其作为工作单元公开的过程。 我们通常在创建函数来执行任何工作时使用此功能。 #### 封装 将类中的数据和方法与实现隐藏(通过访问控制)结合起来通常称为封装。 结果是具有特征和行为的数据类型。 封装本质上既具有信息隐藏又具有实现隐藏。 “***无论进行任何更改,都封装它***”。 它被引用为著名的设计原则。 因此,在任何类中,运行时中的数据都可能发生更改,将来的发行版中可能会更改实现。 因此,封装既适用于数据也适用于实现。 因此,它们可以像下面这样关联: - 抽象更多是关于“类可以做什么”。(想法) - 封装更多地是关于“如何”实现该功能。(实现) ## 接口和抽象类之间的区别? 接口和抽象类之间的基本区别可以算作如下: * 接口不能有任何方法,而抽象类可以(在 [Java8 默认方法](//howtodoinjava.com/java8/default-methods-in-java-8/)之后不正确) * 一个类可以实现许多接口,但只能有一个超类(是否抽象) * 接口不属于类层次结构。 不相关的类可以实现相同的接口 您应该记住:“当您可以用***做什么***充分描述该概念时,而无需指定任何***它是如何工作的***,那么您应该使用一个接口。 如果您需要包括一些实现细节,那么您将需要在抽象类中表示您的概念。” 另外,如果我说的不同:是否可以将许多*分组在一起并用一个名词描述的类? 如果是这样,请以该名词的名称创建一个抽象类,并从其继承这些类。 例如,`Cat`和`Dog`都可以从抽象类`Animal`继承,并且此抽象基类将实现方法`void breathe()`,所有动物都将以完全相同的方式执行该方法。* 什么样的动词可以应用于我的类,通常也可以应用于其他动词? 为每个动词创建一个接口。 例如,所有动物都可以喂食,因此我将创建一个名为`IFeedable`的接口,并让`Animal`实现该接口。 尽管`Dog`和`Horse`足以实现`ILikeable`,但有些还不够。 正如某人所说:主要区别在于您想要实现的地方。 通过创建接口,可以将实现移动到实现接口的任何类。 通过创建一个抽象类,您可以在一个中央位置共享所有派生类的实现,并且避免了很多不好的事情,例如代码重复。 ## `StringBuffer`如何节省内存? [字符串](https://docs.oracle.com/javase/8/docs/api/java/lang/String.html)被实现为[不变对象](//howtodoinjava.com/java/related-concepts/how-to-make-a-java-class-immutable/); 也就是说,当您最初决定将某些内容放入`String`对象时,JVM 会分配一个与初始值大小正好相等的固定宽度数组。 然后将其视为 JVM 内部的常量,在不更改`String`值的情况下,可以大大节省性能。 但是,如果您决定以任何方式更改`String`的内容,那么 JVM 实质上要做的就是将原始`String`的内容复制到一个临时空间中,进行更改,然后将这些更改保存到一个全新的内存阵列中。 因此,在初始化后更改`String`的值是一项相当昂贵的操作。 [`StringBuffer`](https://docs.oracle.com/javase/8/docs/api/java/lang/StringBuffer.html)在 JVM 内部被实现为可动态增长的数组,这意味着任何更改操作都可以在现有内存位置上进行,而新内存仅按需分配。 但是,JVM 没有机会围绕`StringBuffer`进行优化,因为假定其内容在任何情况下都是可更改的。 ## 为什么在对象类而不是线程中声明了`wait`和`notify`? [`wait`,`notify`,`notifyAll`方法](//howtodoinjava.com/java/multi-threading/how-to-work-with-wait-notify-and-notifyall-in-java/)仅在您希望线程访问共享资源并且共享资源可以是堆上的任何 Java 对象时才需要。 因此,这些方法是在核心`Object`类上定义的,因此每个对象都可以控制允许线程在其监视器上等待。 Java 没有用于共享公共资源的任何特殊对象。 没有定义这样的数据结构。因此,在`Object`类上有责任成为共享资源,条件是它可以使用`wait()`,`notify()`和`notifyAll()`之类的辅助方法。 Java 基于 [Hoare](https://en.wikipedia.org/wiki/Tony_Hoare) 的监视器概念。 在 Java 中,所有对象都有一个监视器。 线程在监视器上等待,因此要执行等待,我们需要 2 个参数: – 线程 – 监视器(任何对象) 在 Java 设计中,无法指定线程,它始终是当前运行代码的线程。 但是,我们可以指定监视器(这是我们称为`wait`的对象)。 这是一个很好的设计,因为如果我们可以让任何其他线程在所需的监视器上等待,则会导致“入侵”,给设计/编程并发程序带来了困难。 请记住,在 Java 中,不推荐使用会干扰另一个线程执行的所有操作(例如`stop()`)。 ## 编写 Java 程序以在 Java 中创建死锁并修复死锁? 在 Java 中,死锁是指至少有两个线程在某个不同的资源上持有锁,并且都在等待其他资源来完成其任务的情况。 而且,没有人能够锁定它所拥有的资源。 > 阅读更多:[编写死锁并使用 Java 解决](//howtodoinjava.com/java/multi-threading/writing-a-deadlock-and-resolving-in-java/ "Writing a deadlock and resolving in java") ## 如果您的`Serializable`类包含一个不可序列化的成员,该怎么办? 您如何解决? 在这种情况下,将在运行时引发`NotSerializableException`。 要解决此问题,一个非常简单的解决方案是将此类字段标记为瞬态。 这意味着这些字段将不会被序列化。 如果还要保存这些字段的状态,则应考虑已经实现[`Serializable`](https://docs.oracle.com/javase/7/docs/api/java/io/Serializable.html)接口的引用变量。 您可能还需要使用`readResolve()`和`writeResolve()`方法。 让我们总结一下: * 首先,设置您的不可序列化字段`transient`。 * 在`writeObject()`中,首先在流上调用`defaultWriteObject()`来存储所有非瞬态字段,然后调用其他方法来序列化不可序列化对象的各个属性。 * 在`readObject()`中,首先在流上调用`defaultReadObject()`以读回所有非瞬态字段,然后调用其他方法(与您添加到`writeObject`的方法相对应)来反序列化不可序列化的对象。 另外,我强烈建议阅读[**关于 Java**](//howtodoinjava.com/java/serialization/a-mini-guide-for-implementing-serializable-interface-in-java/ "A mini guide for implementing serializable interface in java") 中序列化的完整指南。 ## 解释 Java 中的`transient`和`volatile`关键字? #### `transient` “*Java 中的`transient`关键字用于指示不应序列化字段。*”根据语言规范:可以将变量标记为`transient`,以指示它们不是对象持久状态的一部分。 例如,您可能具有从其他字段派生的字段,并且仅应以编程方式进行操作,而不要通过序列化来保持状态。 例如,在`BankPayment.java`类中,可以对`principal`和`rate`之类的字段进行序列化,而即使在反序列化之后也可以随时计算`interest`。 回想一下,java 中的每个线程也都有自己的本地内存空间,并且它在本地内存中执行所有读/写操作。 完成所有操作后,它将所有线程访问该变量的位置写回主存储器中变量的修改状态。 通常,这是 JVM 内部的默认流程。 但是,`volatile`修饰符告诉 JVM,访问该变量的线程必须始终将其自身的变量私有副本与内存中的主副本进行协调。 这意味着每次线程想要读取变量的状态时,它都必须刷新其本地内存状态并从主内存中更新变量。 #### `volatile` `volatile`在无锁算法中最有用。 当您不使用锁定来访问该变量并且希望一个线程所做的更改在另一个线程中可见时,或者您要创建“后接”关系以确保计算无需重新排序,以确保更改在适当的时间可见。 `volatile`应该用于在多线程环境中安全发布不可变对象。 声明诸如`public volatile ImmutableObject foo`之类的字段可确保所有线程始终看到当前可用的实例引用。 ## `Iterator`和`ListIterator`之间的区别? 我们可以使用`Iterator`遍历`Set`或`List`或`Map`。 但是`ListIterator`仅可用于遍历`List`。 其他差异如下所示。 您可以: 1. 向后迭代。 2. 随时获取索引。 3. 随时添加新值。 4. 在这个时间设置一个新值。 例如: ```java List<String> names = new ArrayList<String>(); names.add("Alex"); names.add("Bob"); names.add("Charles"); System.out.println(names); ListIterator<String> listIterator = names.listIterator(); //Add a value at any place using ListIterator while(listIterator.hasNext()){ listIterator.next(); listIterator.add("Lokesh"); } System.out.println(names); listIterator = names.listIterator(); //Set a value at any place using ListIterator while(listIterator.hasNext()){ listIterator.next(); listIterator.set("John"); } System.out.println(names); Output: [Alex, Bob, Charles] [Alex, Lokesh, Bob, Lokesh, Charles, Lokesh] [John, John, John, John, John, John] ``` 显然,我可以在列表中的任意位置添加元素,同时对其进行迭代 – 同样,我也可以更改任何元素。 使用`Iterator`不可能。 学习愉快!