核心Java面试问题 - 第1部分
介绍
你打算学习核心的Java吗?或者在接下来的日子里要准备面试?不要担心,阅读下面给出的所有面试问题,以重新让你对java的核心概念有新的认知。
如何在Java中创建一个不可变的对象?计算所有好处?
一个不可变的类是一旦创建就无法更改的状态。这里,对象的状态本质上是指在类中的实例变量中存储的值,无论它们是原值类型(primitive types)还是引用类型(reference types)。
为了使一个类不可变,需要遵循下面的步骤:
- 不要提供修改字段或对象引用的“setter”方法。 Setter方法是为了改变对象的状态,这就是我们想要在这里防止的。
- 使所有的字段都是最终(final)的和私有(private)的。声明私有(private)的字段将无法在类之外访问,并使它们最终将确保不能更改它们。
- 要让子类重写方法。最简单的方法是将类声明为final。 java中的最终(Final)类不能被覆盖。
- 永远记住你的实例变量是可变的或不可变的。识别它们并返回具有所有可变对象(对象引用)的复制内容的新对象。不变的变量(原始类型)可以安全地返回,没有其他的副作用。
另外,你应该记住不可变类的好处。面试时你可能需要他们。不可变类 :
- 很容易构建,测试和使用
- 自动线程安全并且没有同步问题
- 不需要复制构造函数
- 不需要克隆的实现
- 允许hashCode使用延迟初始化,并缓存它的返回值
- 当作为一个属性使用时不需要被复制
- 做出好的Map键和Set元素(这些对象在集合中不能改变状态)
- 他们的类不变性一旦在构造上建立,就再也不需要被检查了
- 总是有“失败原子性”(Joshua Bloch使用的术语):如果一个不可变对象抛出一个异常,它就永远不会处于不受欢迎的或不确定的状态。
Java是通过引用传递还是按值传递?
Java规范说,Java中的一切都是按值传递的。Java中没有“传引用”的东西。这些术语与方法调用和传递变量作为方法参数相关联。原始类型总是通过值传递。但是,这个概念应该在复杂类型的方法参数的背景下来理解。
在java中,当我们将任何复杂类型的引用作为任何方法参数传递时,总是将内存地址逐位地复制到新的引用变量中。见下图:
在上例中,第一个实例的地址位被复制到另一个引用变量,从而导致两个引用指向存储实际对象的单个存储器位置。请记住,使另一个引用为null不会使第一个引用也为空。但是,从引用变量的变化状态也可以在其他引用中受到影响。
finally有什么用处?Java保证finally会被调用吗?什么时候finally不被调用?
finally总是在try代码块退出时执行。这确保即使出现意外异常,也会执行最终的代码块。但finally,它不只是对异常处理的应用程序是有用的——它还允许通过返回、继续或中断不小心绕过清除代码。最后,将清理代码放入一个finally块中始终是一个很好的实践,即使在没有预料到异常的情况下也是如此。
如果JVM在try或catch代码执行时退出,那么finally的块可能不会执行。同样,如果执行try或catch代码的线程被中断或被杀死,那么即使应用程序作为一个完整的应用程序继续执行,finally块也可能不会执行。
为什么有两个Date类;一个在java.util包中,另一个在java.sql中?
java.util.Date
表示日期和时间,java.sql.Date
只表示日期。
java.sql.Date
的补充是java.sql.Time
,它只代表一天的时间。
java.sql.Date
是java.util.Date
的子类(扩展)。那么,在java.sql.Date
中改变了什么:
toString()
生成不同的字符串表示形式:yyyy-mm-dd
- 一个静态的
valueOf(String)
方法,用一个具有上述表示的字符串来创建一个日期 - 几个小时,几分钟和几秒钟的getter和setter被弃用
java.sql.Date
类与JDBC一起使用,它的目的是没有时间部分,即小时,分钟,秒和毫秒应该是零,但这不是由类强制执行的。
解释标记接口?
标记接口模式是计算机科学中的一种设计模式,与提供有关对象的运行时类型信息的语言一起使用。它提供了一种方法,将元数据与一种语言没有显式支持此类元数据的类关联起来。在java中,它被用作没有指定方法的接口。
在java中使用标记接口的一个很好的例子是Serializable
接口。一个类实现了这个接口,以表明它的非瞬态数据成员可以被写入字节流或文件系统。
标记接口的一个主要问题是一个接口定义了一个实现类的契约,该契约被所有的子类继承。这意味着你不能“未实现”一个标记。在给出的例子中,如果你创建了一个你不想序列化的子类(可能是因为它依赖于临时状态),你必须采取手段显式抛出NotSerializableException
异常。
为什么java中的main()被声明为public static void?
Why public?main方法是公开的,这样它就可以在任何地方访问,并且可以访问所有想要使用它来启动应用程序的对象。在这里,我不是说JDK / JRE有类似的原因,因为java.exe或javaw.exe(对于windows)使用Java本地接口(JNI)调用来调用方法,因此无论访问修饰符如何,它们都可以调用它。
Why static?让我们假设我们没有静态的主要方法。现在,要调用任何方法,你需要一个实例。对? Java可以有重载的构造函数,大家都知道。现在,应该使用哪一个,重载构造函数的参数来自哪里
Why void?然后没有任何值返回给JVM,谁实际调用这个方法。应用程序想要与调用进程通信的唯一方法是:正常或异常终止。这已经可以使用System.exit(int)
。非零值表示异常终止,否则一切正常。
创建字符串作为new()和字符有什么区别?
当我们使用new()创建String会将其添加到字符串池中时,使用literal创建的String仅在存在于堆的Perm区域中的字符串池中创建。
那么你真的需要深入地了解字符串池的概念来回答这个问题或类似的问题。我的建议..关于字符串类和字符串池要“努力学习”。
String中的substring()如何工作?
在Java中的字符串就像任何其他编程语言,是字符序列。这更像是一个工具类来处理该字符序列。这个字符序列保存在以下变量中:
/** 该值用于字符存储. */
private final char value[];
要在不同的场景下访问这个数组,可以使用以下变量:
/** The offset is the first index of the storage that is used. */
private final int offset;
/** The count is the number of characters in the String. */
private final int count;
每当我们从任何已有的字符串实例创建子字符串时,substring()方法只设置偏移量和计数变量的新值。内部char数组不变。如果不小心使用substring()方法,那么这可能是内存泄漏的源头。
解释HashMap的工作。如何解决重复的碰撞?
你们大多数人都会同意HashMap是现在面试中最受欢迎的讨论话题。如果有人问我描述“HashMap是如何工作的”,我只是简单地回答:“根据散列原理”。尽可能简单。
现在,以最简单的形式进行哈希,这是在应用任何公式/算法后为任何变量/对象分配惟一代码的一种方法。
根据定义,映射是:“将键映射到值的对象”。很简单..对吗?所以,HashMap有一个内部类Entry,看起来像这样:
static class Entry<k ,V> implements Map.Entry<k ,V>
{
final K key;
V value;
Entry<k ,V> next;
final int hash;
...//More code goes here
}
当有人试图在HashMap中存储一个键值对时,会发生以下情况:
-
首先,检查要插入的key对象是否为null。如果key对象为空,则将值存储在table[0]位置。因为null的哈希码总是0。
-
然后在下一步中,通过调用hashCode()方法使用key的哈希码计算哈希值。个散列值用于计算存储Entry对象的数组中的索引。JDK设计者认为,可能会有一些写得不好的hashCode()函数可以返回非常高或者低的哈希码值。为了解决这个问题,他们引入了另一个hash()函数,并将该对象的哈希代码传递给这个hash()函数,以将哈希值放在数组索引大小的范围内。
-
现在调用indexFor(hash,table.length)函数来计算存储Entry对象的精确索引位置。
-
这里是主要部分。现在,我们知道两个不相等的对象可以有相同的散列码值,两个不同的对象将如何存储在同一个数组位置(称为bucket)。答案是LinkedList。如果您还记得,Entry类有一个属性“next”。这个属性总是指向链中的下一个对象。这正是LinkedList的行为。
因此,在发生冲突时,条目对象存储在LinkedList表单中。当一个条目对象需要存储在特定的索引中,HashMap检查是否已经有一个条目?如果没有条目,条目对象就存储在这个位置。如果已经有一个对象位于计算索引上,则检查其下一个属性。如果为null,则当前的Entry对象成为LinkedList中的下一个节点。如果下一个变量不为空,则按照过程进行操作,直到下一个变量为空。
如果我们将另一个具有相同键的值对象添加进来,将会怎样呢?从逻辑上说,它应该替换旧的值。它是如何做的?在确定输入对象的索引位置后,在计算索引的链表上迭代LinkedList时,HashMap调用equals()方法在每个条目对象的关键对象上。在LinkedList中的所有这些条目对象都有类似的散列代码,但是equals()方法将测试真正的等式。如果key . equals(k)将是true,那么这两个键将被视为相同的键对象。这将导致在条目对象内替换值对象。
这样,HashMap保证了key的唯一性。
接口和抽象类之间的区别?
如果你正在面向初级程序员,这是很常见的问题。那么,最显着的差异如下:
- 在Java接口中声明的变量默认为final。抽象类可能包含非final变量。
- Java接口是隐式抽象的,不能有实现。 Java抽象类可以有实现默认行为的实例方法。Java接口的成员默认是公共的。 Java抽象类可以具有通常的私有类,抽象类类成员的风格。
- Java接口应该使用关键字“implements”来实现; Java抽象类应该使用关键字“extends”进行扩展。
- 一个Java类可以实现多个接口,但它只能扩展一个抽象类。
- 接口是绝对抽象的,不能被实例化; Java抽象类也不能被实例化,但是如果存在main(),则可以被调用。从Java 8开始,您可以在接口中定义默认方法。
- 抽象类比接口稍快,因为接口在调用Java中的任何重写方法之前涉及搜索。这在大多数情况下并不是一个显着的差异,但是如果您正在编写一个时间关键的应用程序,那么您可能不想做任何事情。
什么时候重写hashCode()和equals()?
在Java对象的父类中的Object类中定义了hashCode()和equals()方法。由于这个原因,所有java对象都继承这些方法的默认实现。
hashCode()方法用于获取给定对象的唯一整数。
该整数用于确定桶(bucket)位置,当这个对象需要存储在一些像HashTable的数据结构。默认情况下,Object的hashCode()方法返回并存储对象的内存地址的整数表示。 如名称所示,equals()方法用于简单地验证两个对象的相等性。默认实现只需检查两个对象的对象引用来验证它们的相等性。
请注意,无论何时重写此方法,通常都必须重写hashCode方法,以便维护hashCode()方法的一般约定,该方法声明相等对象必须具有相同的哈希码。
- equals()必须定义一个等式关系(它必须是自反的,对称的和传递的)。另外,它必须是一致的(如果对象没有被修改,那么它必须保持返回相同的值)。而且,o.equals(null)必须总是返回false。
- hashCode()也必须一致(如果对象没有按照equals()方式修改,它必须保持返回相同的值)。
这两种方法之间的关系是:
每当a.equals(b),则a.hashCode()必须与b.hashCode()相同。