🔥码云GVP开源项目 12k star Uniapp+ElementUI 功能强大 支持多语言、二开方便! 广告
# Java 核心面试问题 – 第 3 部分 > 原文: [https://howtodoinjava.com/interview-questions/core-java-interview-questions-series-part-3/](https://howtodoinjava.com/interview-questions/core-java-interview-questions-series-part-3/) 在 [**面试问题系列中:第 1 部分**](//howtodoinjava.com/java/interviews-questions/core-java-interview-questions-series-part-1/ "Core java interview questions series : Part 1")和[**第 2 部分**](//howtodoinjava.com/java/interviews-questions/core-java-interview-questions-series-part-2/ "Core java interview questions series : Part 2"),我们讨论了面试官通常会问的一些重要问题。 现在是推进该讨论的时候了。 在这篇文章中,我将在下面给出的问题列表中进行讨论。 ```java Deep copy and shallow copy? What is synchronization? Class level locking and object level locking? Difference between sleep() and wait()? Can you assign null to this reference variable? What if the difference between && and &?? How to override equals and hashCode() methods? Explain all access modifiers? What is garbage collection? Can we enforce it? What is native keyword? What is serialization? Explain the catches? ``` ## 深拷贝和浅拷贝? 克隆是原始副本的精确副本。 在 Java 中,它实质上意味着能够创建状态与原始对象相似的对象。`clone()`方法提供了此功能。 浅拷贝应尽可能少地重复。 默认情况下,Java 克隆是浅表复制或“逐字段复制”,即,由于`Object`类不了解将在其上调用`clone()`方法的类的结构。 因此,JVM 被要求克隆时,请执行以下操作: 1)如果该类只有原始数据类型成员,则将创建该对象的全新副本,并返回对该新对象副本的引用。 2)如果该类包含任何类类型的成员,则仅复制对那些成员的对象引用,因此原始对象和克隆对象中的成员引用都引用同一对象。 深层副本会复制所有内容。 集合的深层副本是两个集合,原始集合中的所有元素都重复了。 在这里,我们想要一个独立于原始版本的克隆,对克隆进行更改不应影响原始版本。 *深度克隆要求满足以下规则。* 1. 无需单独复制原始类型。 2. 原始类中的所有成员类都应支持克隆,而上下文中原始类的`clone`方法应在所有成员类上调用`super.clone()`。 3. 如果任何成员类不支持克隆,则必须在克隆方法中创建该成员类的新实例,并将其所有属性一一复制到新的成员类对象中。 这个新的成员类对象将在克隆对象中设置。 [在此处详细了解克隆](//howtodoinjava.com/java/cloning/a-guide-to-object-cloning-in-java/ "A guide to object cloning in java")。 ## 什么是同步? 对象级锁定和类级锁定? ***同步***是指多线程。 同步的代码块一次只能由一个线程执行。 Java 支持执行多个线程。 这可能会导致两个或多个线程访问相同的字段或对象。 同步是使所有并发线程在执行中保持同步的过程。 同步避免了由于共享内存视图不一致而导致的内存一致性错误。 当方法声明为已同步时; 该线程持有该方法对象的监视器。如果另一个线程正在执行同步方法,则该线程将被阻塞,直到该线程释放监视器。 Java 中的同步是使用`syncronized`关键字实现的。 您可以在类中的已定义方法或块上使用`syncronized`关键字。 关键字不能与类定义中的变量或属性一起使用。 ***对象级别锁定***是一种机制,当您要同步非静态方法或非静态代码块,以便仅一个线程将能够在给定实例的代码上执行代码块时, 类。 应该始终这样做以确保实例级数据线程安全。 ***类级别锁定***可防止多个线程在运行时进入所有可用实例中的任何同步块。 这意味着,如果在运行时有 100 个`DemoClass`实例,则一次只能在一个实例中的任何一个线程上执行`demoMethod()`,而所有其他实例将被其他线程锁定。 为了确保静态数据线程的安全,应该始终这样做。 [在此处了解有关同步的更多信息。](//howtodoinjava.com/java/multi-threading/thread-synchronization-object-level-locking-and-class-level-locking/ "Thread synchronization, object level locking and class level locking") ## `sleep()`和`wait()`之间的区别? `sleep()`是一种用于将进程保留几秒钟或所需时间的方法,但是如果使用`wait()`方法,线程将进入等待状态,直到我们调用`notify()`或`notifyAll()`。 主要区别在于,`wait()`释放锁定或监视器,而`sleep()`不在等待期间释放任何锁或监视器。 通常,“等待”用于线程间通信,而“睡眠”用于引入执行暂停。 `Thread.sleep()`在一段时间内将当前线程发送到“不可运行”状态。 该线程将保留其已获取的监视器 - 即,如果该线程当前处于同步块或方法中,则其他线程无法进入该块或方法。 如果另一个线程调用`t.interrupt()`,它将唤醒睡眠线程。 请注意,`sleep`是一种静态方法,这意味着它始终会影响当前线程(正在执行`sleep`方法的线程)。 一个常见的错误是调用`t.sleep()`,其中`t`是一个不同的线程。 即使这样,当前线程仍将睡眠,而不是`t`线程。 `object.wait()`将当前线程发送到“不可运行”状态,就像`sleep()`一样,但要稍加调整。 `Wait`是在对象而不是线程上调用的; 我们将此对象称为“锁定对象”。 在调用`lock.wait()`之前,当前线程必须在锁对象上进行同步。 然后,`wait()`释放此锁定,并将线程添加到与该锁定关联的“等待列表”。 稍后,另一个线程可以在同一个锁对象上同步并调用`lock.notify()`。 这将唤醒原始的等待线程。 基本上,`wait()`/`notify()`就像`sleep()`/`interrupt()`一样,仅活动线程不需要直接指针指向睡眠线程,而仅需要指向共享锁对象的指针。 [在此处详细阅读区别。](//howtodoinjava.com/java/multi-threading/difference-between-sleep-and-wait/ "Difference between sleep() and wait()?") ## 可以为`this`引用变量分配`null`吗? 没有。 你不能。在 Java 中,赋值语句的左侧必须是变量。 “`this`”是一个特殊的关键字,始终代表当前实例。 这不是任何变量。 同样,不能将`null`分配给“`super`”或任何此类关键字。 ## `&&`和`&`之间是否有区别? `&`是按位的,`&&`是逻辑的。 * `&`求值操作的双方。 * `&&`求值操作的左侧,如果为真,则继续并求值右侧。 [请阅读此处以深入了解。](https://en.wikipedia.org/wiki/Bitwise_operation "bitwise operations") ## 如何覆盖`equals`和`hashCode()`方法? `hashCode()`和`equals()`方法已在`Object`类中定义,`Object`类是 Java 对象的父类。 因此,所有 java 对象都继承这些方法的默认实现。 `hashCode()`方法用于获取给定对象的唯一整数。 当此对象需要存储在类似数据结构的`HashTable`中时,此整数用于确定存储桶位置。 默认情况下,对象的`hashCode()`方法返回存储对象的内存地址的整数表示形式。 顾名思义,`equals()`方法用于简单地验证两个对象的相等性。 默认实现只是检查两个对象的对象引用以验证它们的相等性。 以下是在覆盖这些函数时要记住的要点。 1. 始终使用对象的相同属性来生成`hashCode()`和`equals()`两者。 在本例中,我们使用了员工 ID。 2. `equals(`)必须一致(如果未修改对象,则必须保持返回相同的值)。 3. 只要`a.equals(b)`,则`a.hashCode()`必须与`b.hashCode()`相同。 4. 如果覆盖一个,则应覆盖另一个。 [在此处阅读更多有趣的事实以及指南。](//howtodoinjava.com/java/related-concepts/working-with-hashcode-and-equals-methods-in-java/ "Working with hashCode and equals methods in java") ## 解释所有访问修饰符? Java 类,字段,构造器和方法可以具有四种不同的访问修饰符之一: ***`private`*** 如果将方法或变量标记为私有,则只有同一类内的代码才能访问该变量或调用该方法。 子类中的代码无法访问变量或方法,也不能从任何外部类中进行代码。 如果将一个类标记为私有,则没有外部类可以访问该类。 不过,对于类来说,这实际上没有多大意义。 因此,访问修饰符`private`主要用于字段,构造器和方法。 ***默认*** 通过根本不编写任何访问修饰符来声明默认访问级别。 默认访问级别意味着与类相同的包中的类本身内的代码+类内的代码可以访问类,字段,构造器或方法。 因此,默认访问修饰符有时也称为包访问修饰符。 如果子类声明了默认的可访问性,则子类不能访问父类中的方法和成员变量,除非子类与父类位于同一包中。 ***`protected`*** `protected`访问修饰符的作用与默认访问权限相同,除了子类还可以访问超类的受保护方法和成员变量。 即使子类与超类不在同一个包中,也是如此。 ***`public`*** 公开访问修饰符意味着所有代码都可以访问类,字段,构造器或方法,无论访问代码位于何处。 | ***修饰符*** | *相同的类* | *相同的包* | *子类* | *其他包* | | --- | --- | --- | | `public` | Y | Y | Y | Y | | `protected` | Y | Y | Y | N | | 默认 | Y | Y | N | N | | `private` | Y | N | N | N | ## 什么是垃圾回收? 我们可以执行吗? 垃圾回收是许多现代编程语言(例如 Java 和 .NET 框架中的语言)中的自动内存管理功能。 使用垃圾回收的语言通常在 JVM 之类的虚拟机中解释或运行。 在每种情况下,运行代码的环境还负责垃圾回收。 GC 具有两个目标:应释放所有未使用的内存,并且除非程序不再使用它,否则不应释放任何内存。 你能强迫垃圾收集吗? 不,`System.gc()`尽可能接近。 最好的选择是调用`System.gc()`,这只是向垃圾收集器提示您要它进行收集。 由于垃圾收集器是不确定的,因此无法强制立即收集。 另外,在`OutOfMemoryError`文档下,它声明除非 VM 在完全垃圾回收后未能回收内存,否则不会抛出该错误。 因此,如果在出现错误之前一直分配内存,那么您将已经强制执行完整的垃圾回收。 [在此处详细了解垃圾收集。](//howtodoinjava.com/java/garbage-collection/revisiting-memory-management-and-garbage-collection-mechanisms-in-java/ "Revisiting memory management and garbage collection mechanisms in java") ## 什么是`native关键字? 详细解释? 将`native`关键字应用于方法,以指示该方法是使用 JNI 在本地代码中实现的。 它标记了一种方法,它将以其他语言而不是 Java 来实现。 过去曾使用本机方法来编写对性能至关重要的部分,但随着 Java 变得越来越快,这种方法现在已不那么普遍了。 当前需要本机方法 * 您需要从 Java 调用用其他语言编写的库。 * 您需要访问只能从其他语言(通常为 C)访问的系统或硬件资源。 实际上,许多与真实计算机交互的系统功能(例如磁盘和网络 IO)只能执行此操作,因为它们调用了本机代码。 使用本机代码库的缺点也很重要: 1. JNI/JNA 倾向于破坏 JVM 的稳定性,尤其是当您尝试做一些复杂的事情时。 如果您的本机代码错误地执行了本机代码内存管理,则很有可能会使 JVM 崩溃。 如果您的本机代码是不可重入的,并且从多个 Java 线程中调用,则坏事……会偶尔发生。 等等。 2. 带有本机代码的 Java 比纯 Java 或纯 C/C++ 更难调试。 3. 本机代码可能为其他平台无关的 Java 应用引入重要的平台依赖项/问题。 4. 本机代码需要一个单独的构建框架,并且也可能存在平台/可移植性问题。 ## 什么是序列化? 解释渔获物? 在计算机科学中,在数据存储和传输的上下文中,序列化是将数据结构或对象状态转换为一种格式的过程,该格式可以稍后在相同或另一台计算机环境中存储和恢复。 当根据序列化格式重新读取生成的一系列位时,可以使用它来创建原始对象的语义相同的克隆。 Java 提供了自动序列化,该序列化要求通过实现`java.io.Serializable`接口来标记对象。 实现该接口会将类标记为“可以序列化”,然后 Java 将在内部处理序列化。 在`Serializable`接口上没有定义任何序列化方法,但是可序列化类可以选择定义具有某些特殊名称和签名的方法,如果定义了这些特殊名称和签名,这些方法将在序列化/反序列化过程中被调用。 对象序列化后,其类中的更改会破坏反序列化过程。 要确定您的类中将来的变化,这些变化将是兼容的,而其他变化将被证明是不兼容的,请在此处阅读完整的[**指南**](//howtodoinjava.com/java/serialization/a-mini-guide-for-implementing-serializable-interface-in-java/ "A mini guide for implementing serializable interface in java")。 简而言之,我在这里列出: **不兼容的更改** * 删除字段 * 将类上移或下移 * 将非静态字段更改为静态或将非瞬态字段更改为瞬态 * 更改原始字段的声明类型 * 更改`writeObject`或`readObject`方法,使其不再写入或读取默认字段数据 * 将类从可序列化更改为可外部化,反之亦然 * 将类从非枚举类型更改为枚举类型,反之亦然 * 删除`Serializable`或`Externalizable` * 将`writeReplace`或`readResolve`方法添加到类 **兼容的更改** * 新增字段 * 添加/删除类 * 添加`writeObject`/`readObject`方法(首先应调用`defaultReadObject`或`defaultWriteObject`) * 删除`writeObject`/`readObject`方法 * 添加`java.io.Serializable` * 更改对字段的访问 * 将字段从静态更改为非静态或将瞬态更改为非瞬态 学习愉快!