Java 编程面试问题与答案

2025 年 5 月 8 日 | 阅读 67 分钟

在本节中,我们讨论了一些热门的编程面试问题。练习本节提供的问题将提高您的编程技能。本教程对有经验的候选人和初学者都很有益。

它还涵盖了复杂度分析和边界情况,以帮助候选人理解如何处理它们。无论您是准备校园招聘还是希望转到更好的职位,扎实的 Java 编程概念都是必不可少的。本指南包含精心挑选的、在技术面试中经常被问到的问题。

每个问题都旨在评估您对数据结构、算法和核心 Java 基础知识的理解。答案以对初学者友好的方式呈现,使得逐步理解逻辑变得更加容易。

1) 编写一个 Java 程序,使用循环和 O(1) 空间生成第 n 个斐波那契数。

在斐波那契数列中,任何一项的值都是前两项值之和。

例如,如果我们想计算数列的第 3 项,那么它是第 1 项和第 2 项之和。请注意,斐波那契数列的前两项定义为 0 和 1。因此,第 3 项将是 0 + 1 = 1。第 4 项将是第 3 项和第 2 项之和,即 1 + 1 = 2。第 5 项将是第 4 项和第 3 项之和,即 2 + 1 = 3。

因此,斐波那契数列看起来如下:

0, 1, 1, 2, 3, 5, 8, 13, 21, 34, 55, … (考虑 1-索引)

因此,通常,数列的第 n 项可以写成

F(n) = F(n - 1) + F(n - 2)

现在,我们可以编写以下程序来计算斐波那契数列的第 n 项。请注意,用户将提供 n 的值。

示例

编译并运行

输出

The 6th Fibonacci number is: 5

复杂度分析:该程序计算斐波那契数列第 N 项的值需要 O(n) 的时间。该程序的空间复杂度为 O(1),因为没有使用额外的空间。

边界情况:此问题的边界是处理 N 的负值,我们已在 if-条件中完成此操作。在许多情况下,经常看到候选人遗漏了对 n 的负值的处理。

后续:也可以借助动态规划解决此问题。它将消耗 O(n) 时间和 O(n) 空间。建议读者自行尝试。


2) 编写一个 Java 程序,计算数字中存在的位数。

如果我们取数字 5647,我们发现该数字由 4 位数字组成,即 5、6、4 和 7。我们可以通过使用循环将给定数字连续除以 10,直到它变为零来找到数字中存在的总位数。在循环中,我们每迭代一次就递增一个计数器。

示例

编译并运行

输出

The total digits in number 7485 are: 4

复杂度分析:程序的时间复杂度为 O(log10(n)),其中 n 是输入数字。程序的空间复杂度为 O(1),因为程序没有使用任何额外的空间。

边界情况:观察上述程序中提到的循环。它的终止条件是当 n != 0 时。那么,如果输入数字是 0 会发生什么?对于数字 0,需要单独处理以给出答案 1。此外,我们必须处理负数。


3) 计算数字 N 中数字 D 出现的次数。用户必须提供 N 和 D 的值。

该问题是上一个问题的修改形式。因此,修改上述代码将为我们完成工作,以下代码也证明了这一点。

注意:我们应该单独处理 N 为 0 的情况,因为如果 D 也为 0,它应该算作出现 1 次。数字 D 必须介于 0 和 9 之间,因此最好在处理之前验证输入。此外,如果数字 N 为负数,我们应该在开始循环之前将其转换为其绝对值,以确保逻辑正确且一致地工作。

示例

编译并运行

输出

The total count of digit 2 occurring in the number 2228118 is: 3

复杂度分析:程序的时间复杂度为 O(log10(n)),其中 n 是输入数字。程序的空间复杂度为 O(1),因为程序没有使用任何额外的空间。

边界情况:我们必须处理 N 和 D 都为 0 的情况。此外,我们必须处理用户输入负数的情况。


4) 使用递归计算 an 的值。不允许使用递归调用栈以外的任何额外空间。

在递归中,需要寻找大问题和小问题之间的关系。在这个问题中,大问题和小问题之间存在关系。参见以下数学关系。

an = a x an - 1

在编程世界中,我们可以将上述内容写为

(a to the power n) = a x (a to the power n - 1)

例如,3 的 4 次方 = 81。它可以计算为 3 x (3 的 3 次方) = 3 x 27 = 81。

每个递推关系都有一个基本情况。在我们的例子中,基本情况是当指数为 0 时,我们知道任何数的 0 次方都是 1。现在我们可以编写以下代码了。

示例

编译并运行

输出

The value of 2.2 ^ 2 is: 4.840000000000001

复杂度分析:程序的时间复杂度为 O(n),其中 n 是作为数字 y 幂的输入数字。程序的空间复杂度为 O(1),因为程序没有使用任何额外的空间。

边界情况:我们必须处理指数为负数的情况。


5) 给定一个字符串。我们的任务是编写一个 Java 程序来切换给定字符串中的字符大小写。例如,对于字符串 "TpoiNttecH",切换后的字符串将是 "tPOInTTECh"。

众所周知,Java 中的字符串是不可变的。因此,需要创建一个新字符串。对于大小写切换,我们可以使用 ASCII 值;'a' 可以通过 'A' = 'a' - 32 转换为 'A'。

切换大小写的代码。

可以使用 ASCII 值进行切换

  • 'a' 到 'z' 可以通过减去 32 转换为 'A' 到 'Z' ('A' = 'a' - 32)
  • 'A' 到 'Z' 可以通过加上 32 转换为 'a' 到 'z' ('a' = 'A' + 32)
    或者,我们可以使用 Java 内置的 Character 方法,如 Character.isUpperCase() 和 Character.toLowerCase(),以获得更简洁和更安全的 C# 代码。

示例

编译并运行

输出

After toggling, the string "TpoiNttecH" becomes: "tPOInTTECh"

复杂度分析:程序的时间复杂度为 O(num)。程序的空间复杂度也为 O(num),因为程序使用了额外的空间来创建新字符串,而 num 是输入字符串中存在的字符总数。

边界情况:我们必须处理输入字符串的字符不包含小写大写字母的情况。例如,考虑字符串 "ut8Pmn"。在这个字符串中,字符 'u'、't'、'P'、'm'、'n' 会切换为 'U'、'T'、'p'、'M' 和 'N'。然而,字符 '8' 永远无法切换,因为没有所谓的小写 '8' 和大写 '8'。因此,这种情况需要特殊处理。


6) 编写一个 Java 程序,打印字符串中所有唯一的字符。例如,对于字符串 "pppdaf",打印字符 'd'、'a' 和 'f',因为它们是唯一的。字符 'p' 在程序中出现了三次。因此,它不会被打印。给定输入字符串只包含小写字母。

我们可以创建一个大小为 256 的整数数组,因为输入字符串中存在 256 个字符。现在,我们计算输入字符串中每个字符的出现频率,并相应地更新整数数组。现在遍历整数数组,检查哪个元素的值为 1。将与该元素索引对应的字符打印到控制台。请注意,数组的第 0 个索引映射到 'a',第 1 个索引映射到 'b',依此类推。

示例

编译并运行

输出

The unique characters present in the string: 'pppdaf' are:
a d f

复杂度分析:程序的时间复杂度为 O(num),其中 num 是输入字符串中存在的字符总数。程序的空间复杂度是常数,即使我们使用整数数组。请注意,输入数组的大小是常数。

边界情况:我们必须处理传递空字符串或传递所有重复字符的字符串的情况。我们必须单独处理这种情况。


7) 编写一个 Java 程序,证明 Java 中的字符串是不可变的。

为了检查 Java 中的字符串是否不可变,我们必须使用 == 运算符。这是因为 == 比较对象的引用或地址。如果在修改字符串后将其与未更改的字符串进行比较,我们得到 true 值,则表示字符串不是不可变的;否则,它们是不可变的。true 值表示更改后的字符串与之前的字符串具有相同的地址。以下程序展示了字符串的不可变性。

示例

编译并运行

输出

Strings are immutable.

8) 在 Java 中使用递归打印给定输入数组的逆序数组,不使用任何额外空间。忽略递归中用于计算空间复杂度的隐式栈。

要使用递归在不使用额外空间的情况下反转数组,我们交换起始和结束索引处的元素,并通过向内移动递归处理子数组。它一直持续到起始索引达到或超过结束索引。由于没有使用额外的数据结构,并且数组在原地修改,因此它仅使用递归栈即可有效地实现反转。

示例

编译并运行

输出

Original Array is:  23, 45, 12, 44, 9, 1, 22, 6 
The reversed array is:  6, 22, 1, 9, 44, 12, 45, 23 

复杂度分析:程序的时间复杂度为 O(num),其中 num 是输入数组中存在的元素总数。程序的空间复杂度是常数,即 O(1)。


9) 编写一个 Java 程序,以线性时间查找数组中元素第一次和最后一次出现的索引,不使用任何额外空间。考虑一索引。

该程序使用线性时间不额外空间来查找给定元素在数组中的第一次和最后一次出现的索引。它假定基于 1 的索引,有助于理解面试中高效的数组遍历。

示例

编译并运行

输出

For the target element: 2, First Index = 1 Last Index = 8

复杂度分析

程序的时间复杂度为 O(num),其中 num 是输入数组中存在的元素总数。程序的空间复杂度是常数,即 O(1)。

边界情况:我们必须处理数组中不存在目标元素的情况。这是因为如果元素不存在,元素的索引就变得无关紧要。


10) 编写一个 Java 程序,按照如下所示的波浪顺序显示矩阵的元素。(矩阵可以有不同数量的列和行。)

Java Coding Interview Questions

从图像中可以看出,对于偶数列,遍历方向是从上到下,而当列为奇数时,遍历方向是从下到上。该程序按照波浪顺序打印矩阵的元素,其中偶数索引列的遍历方向是从上到下,奇数索引列的遍历方向是从下到上。它适用于任何大小的矩阵,有助于理解 Java 中的二维数组操作。

示例

编译并运行

输出

The wave order traversal of the input matrix is: 
1 4 7 
8 5 2 
3 6 9

复杂度分析:程序的时间复杂度为 O(r x c),其中 r 是行大小,c 是列大小。程序的空间复杂度是常数,即 O(1)。


11) 实现一个 "Developer" 类。为其编写一些方法和属性,并说明如何在 main() 方法中通过实例化该类来使用它们。

在这个程序中,我们定义了一个名为 Developer 的类,它具有一些基本属性(如姓名、语言、经验)和方法(如编码和显示信息)。然后我们在 main 方法中实例化该类,以展示面向对象编程如何在 Java 中通过类的创建和方法的使 C# 用来工作。

示例

编译并运行

输出

Shiva writes code.
Shiva drinks tea and then converts the quadratic complexity codes to linear.

解释:您应该记住一些事情:属性通常必须设置为私有,并且我们应该有 getter 和 setter 方法来访问和修改它们。这是良好的 OOPS 实践。此外,始终创建一个默认构造函数,因为当我们创建带参数的构造函数时,Java 会删除它自己的默认构造函数,并且不传递参数给构造函数就无法创建对象。


12) 编写一个 Java 程序,统计输入字符串中存在的辅音字母和元音字母。字符串可能包含数字字符,并且输入字符串中只允许小写字母。

该程序计算给定字符串中元音和辅音的数量。输入字符串可能包含数字字符,但只考虑小写字母进行计数。它有助于练习 Java 中的字符检查和字符串遍历。

示例

编译并运行

输出

Count of vowels in the String: 'abcd44eiut' is: 4
Count of consonants in the String: 'abcd44eiut' is: 4
Count of other characters in the String: 'abcd44eiut' is: 2

边界情况

可能会遗漏字符串中包含数字或特殊字符(如 '0'、'5'、'#'、'@' 等)的情况。这些字符既不是元音也不是辅音。对于此类字符,需要单独处理。通常,候选人会找到元音计数,然后将其从字符串大小中减去以找到辅音,如果字符串包含一些非字母字符,这将给出错误的结果。

复杂度分析

程序的时间复杂度为 O(n),其中 n 是输入字符串中存在的字符总数。程序的空间复杂度为 O(1),因为程序使用了大小为 5 的数组,即使我们更改输入字符串,该数组也不会改变。


13) 编写一个 Java 程序来演示继承。

以下程序借助 extends 关键字演示了继承。以下程序展示了一个 SmartPhone 类,它继承了 Mobile 类,并具有播放和停止音乐播放器、拨打电话、拍照等功能。

示例

编译并运行

输出

Count of vowels in the String : '@51#$%290(){}' is: 0
Phone number is: 94863472
Calling the dialled number.
Music is getting Played.
Music player Paused.
Music player Stopped.
A photo is clicked.

14) 借助 Java 程序演示基本的“除以 0 异常”。

除以零异常,也称为 ArithmeticException,发生在我们尝试在 Java 中执行除以零的操作时。这在算术运算中是不允许的,并将导致运行时错误。以下 Java 程序中提到了其演示,该程序还展示了如何优雅地使用 try-catch 块处理异常。

示例

编译并运行

输出

Attempting to divide 3 by zero.
Exception caught: java.lang.ArithmeticException: / by zero
Program is completed.

注意:在上述程序中,try-catch 块用于处理除以零异常。因此,程序完全执行。否则,程序将在异常点停止,并且不会执行打印语句(程序已完成)。


15) 借助 Java 程序演示单线程。

该程序演示了 Java 中的单线程。在单线程中,程序一次顺序执行一个任务。Java 使用 Thread 类来定义线程行为。该示例展示了如何通过扩展 Thread 类创建线程并使用 start() 方法运行它。了解多线程的基础知识,从单线程执行开始,非常有用。

示例

编译并运行

输出

Thread[The Main Thread,7,main]
The Main Thread
7

16) 给定一个字符串;编写一个程序检查该字符串是否是回文。字符串可以包含空格、小写和大写字母以及其他字符。请注意,程序应该不区分大小写,并应忽略空格和其他特殊字符。例如,'a' 和 'A' 将被视为相同的字符。此外,“a&* A”是回文,因为我们忽略了 &、* 和空格。因此,“a&* A”可以写成“aa”,因为就这个问题而言,'a' 和 'A' 都是相同的。

方法是遍历整个字符串并删除所有非字母数字的字符。另外,删除空格。将所有字母转换为小写或大写字母。之后,借助循环检查更新后的字符串是否是回文。

示例

编译并运行

输出

The string 'a&*  B BA bB a' is a palindrome.

边界情况:需要注意的是,我们必须将所有字母转换为小写或大写字母。此外,我们还需要处理用户输入只包含空格的字符串的情况。在我们的例子中,只包含空格的字符串也应该是回文。

复杂度分析:程序的时间复杂度为 O(n),其中 n 是字符串中存在的字符总数。程序的空间复杂度是常数,即 O(1)。


17) 给出两个二进制字符串。编写一个 Java 程序,根据二进制加法规则添加给定的二进制字符串并显示结果二进制字符串。请注意,输入字符串只包含 0 和 1,没有其他内容。

二进制加法规则如下。

  1. 0 + 0 = 0
  2. 0 + 1 = 1
  3. 1 + 0 = 1
  4. 1 + 1 = 0 & c = 1

从第四条规则可以清楚地看出,每当结果超过 1 时,加法的结果变为零,进位为 1。这表明每当结果超过 1 时,加法的结果变为 0,进位变为 1。使用这四条规则,我们将从最右边的索引开始进行加法,并向第一个或最左边的索引移动。以下程序中提到了它的演示。

示例

编译并运行

输出

The addition of binary strings is: 11101

边界情况

重要的是要注意用户可以输入任何长度的字符串。问题中没有提到输入字符串的长度会相同。因此,第一个字符串的长度可以小于、大于或等于第二个字符串的长度。此外,应该注意的是,最终答案可能包含也可能不包含额外的位。例如,100 + 1 = 101,但是,111 + 111 = 1000。1000 比输入字符串多一位。

复杂度分析

程序的时间复杂度为 O(max(m, n)),其中 m 是第一个输入字符串中存在的字符总数,n 是第二个输入字符串中存在的字符总数。程序的空间复杂度是常数,即 O(1)。


18) 给出两个字符串作为输入。编写一个 Java 程序检查它们是否是字谜。

如果两个字符串包含相同数量的相同字符,则它们被视为字谜。但是,它们出现的顺序可能不同。

例如,“tpointtech”和“ttaaniojvp”被认为是字谜。 “tpointtech”中存在的每个字符也存在于“ttaaniojvp”中。同样,“tpointtech”中不存在的字符也不存在于“ttaaniojvp”中。

我们将使用一个 HashMap 来存储字符在第一个字符串中出现的次数。请注意,字符将是键,其出现的次数是其值。之后,我们将遍历第二个字符串,并开始减少存储在 HashMap 中的字符的出现频率。如果频率已经为零或 HashMap 中不存在该字符,我们可以说这些字符串不是字谜;否则,它们是字谜。

示例

编译并运行

输出

Strings 'tpointtech' & 'ttaaniojvp' are not anagrams.

边界情况:有必要检查字符串的长度是否相同。如果不同,我们可以说给定字符串不是字谜。

复杂度分析:程序的时间复杂度为 O(m + n),其中 m 和 n 是两个字符串的大小。程序的空间复杂度为 O(1)。


19) 给定一个已排序的唯一整数数组。已知数组中的每个元素都是唯一的。此外,给定一个元素。编写一个 Java 程序查找数组中该元素的索引。如果该元素不在数组中,则查找它应该插入数组的索引,以便插入该元素后数组仍然保持排序。请注意,该程序应在空间和时间复杂度方面进行优化。考虑零索引。

问题中给出数组已排序。因此,最好的方法是应用二分查找。以下代码是其演示。

示例

编译并运行

输出

For the input array: 
2 6 9 13 24 35 78 90 99 
The index of the target element 7 is: 2
 
 
For the input array: 
-3 5 24 40 51 80 89 97 
The index of the target element 51 is: 4

复杂度分析

程序的时间复杂度为 O(log2(nums)),其中 nums 是输入数组中存在的元素总数。程序的空间复杂度为 O(1),因为程序不使用任何额外的空间。


20) 给定一个整数数组。编写一个优化的 Java 程序来对给定数组进行波浪排序。

Java Coding Interview Questions

主要目标是生成波浪图。这可以通过在输入数组中生成峰值或查看输入数组中的波谷生成来实现。因此,我们将尝试在数组中创建峰值。我们希望第一个元素作为峰值元素,因此第一个元素保持不变,我们从第二个索引开始,保持不变,并从索引 = 2 开始。

Java Coding Interview Questions

在这里,由于我们要生成一个峰值,我们需要下一个和前一个元素大于第二个元素。以类似的方式,第四个元素将小于第三个和第五个元素。

因此,我们每次需要跳跃 2 个索引,直到到达给定数组的末尾。

Java Coding Interview Questions

示例

编译并运行

输出

The input array is: 
19 18 16 13 14 17 12 
 
After the wave sort
19 16 18 13 17 12 14 
 
The input array is: 
-45 45 -50 -60 0 34 9 12 
 
After the wave sort
45 -50 -45 -60 34 0 12 9

复杂度分析:程序的时间复杂度为 O(nums),其中 nums 是输入数组中存在的元素总数。程序的空间复杂度为 O(1),因为程序不使用任何额外的空间。

边界情况:在代码中,前一个和下一个元素正在交换;因此,需要处理索引越界条件。

注意 1:上面显示的输出不是唯一的答案。对于这个问题,可能还有其他答案。只需展示其中一个即可。

注意 2:另一种解决方案是排序数组,然后交换相邻元素以获得答案。然而,这可能导致 O(nums * log(nums)) 的时间复杂度,而上面提供的解决方案更优化,时间复杂度为 O(nums)。因此,上面没有讨论排序方法,因为我们必须编写最优化程序。


21) 创建一个用户自定义异常,并借助 Java 程序演示其工作原理(何时引发异常)。

在下面的程序中,为银行创建了一个名为 LowBalanceExcptn 的类。因此,当一个人创建银行账户时,他应保持的最低余额为 7000 卢比。因此,当银行余额低于 7000 卢比时,会引发异常。以下演示了相同的情况。

示例

编译并运行

输出

The account can't be created.
Account has the low balance: The bank balance can never be less than Rs.7000/-
The account has been created and the bank balance has set to: 7000
The bank account is created and the balance is set to: 10000
account - 1 balance = 0
account - 2 balance = 7000
account - 3 balance = 10000

22) 如何在 Java 中实现多重继承?

在 Java 中,无法实现多重继承。因此,我们需要借助接口来创建多重继承场景。在以下示例中,创建了一个 Main 类,它实现了多个接口,通过这样做,实现了多重继承。

示例

编译并运行

输出

Default Interface1
Default Interface2
Now Executing methods showOfInterface1() showOfInterface2()
Default Interface1
Default Interface2

注意:如果从“TestClass”类中删除默认方法实现,则会引发编译错误。如果通过接口存在菱形问题,则如果中间接口未提供最顶层接口的实现,则这不是问题。如果中间接口提供了实现,则可以使用 super 关键字获取实现。


23) 使用 Java 程序演示类的嵌套。

在一个类中定义另一个类称为类的嵌套。通常,在 Java 中,内部类是一个静态类。嵌套意味着在一个类中定义另一个类,并且这样的内部类可以是静态的或非静态的。静态嵌套类可以在不创建外部类实例的情况下访问。这个概念对于逻辑分组类很有用,有助于封装。观察以下程序,了解 Java 中如何实现嵌套。

示例

编译并运行

输出

The parameterized constructor of the Outer class is invoked.
The parameterized constructor of the second inner class is invoked.
Z = 40
101
The parameterized constructor of the third inner class is invoked.
Z = 409

24) 在 Java 中实现菱形问题。

菱形问题是多重继承的一个问题,其中一个类继承两个或多个类。换句话说,当子类具有多个父类时,会发生错误。下面讨论使用 Java 程序解决菱形问题。

示例

编译并运行

输出

Inside the display method of the interface C1.
Inside the display method of the interface C2.

解释:上述代码的问题在于,当我们在类 GC1 中覆盖 display() 方法时,编译器不知道是类 C1 的 display() 方法被覆盖还是类 C2 的 display() 方法被覆盖。


25) 在多线程中演示 isAlive() 或 join() 操作。

isAlive() 方法提供有关线程的信息,无论它是终止还是存活。这些终止和存活是 Java 中的线程状态。此外,join() 操作将一个线程与另一个线程连接起来,这意味着一个线程必须等待与其连接的线程完成,即使该线程已完成自己的工作。两者将一起终止。

示例

编译并运行

输出

Thread id: 14
Thread name: first Thread 
Thread priority: 10
Thread State: RUNNABLE
Thread alive: false
1
2
3
4
5
6

26) 使用 Java 程序演示线程同步。

我们可以借助 synchronization 关键字实现线程同步。当多个线程同时访问共享资源时,存在数据不一致或竞态条件的风险。为了防止这种情况,Java 提供了同步,它确保一次只有一个线程可以访问代码块或方法。我们使用 synchronized 关键字来实现这一点。以下示例展示了如何使用线程同步保护共享资源。

示例

编译并运行

输出

11 22 33 44 55 66 77 88 99 110 
12 24 36 48 60 72 84 96 108 120

27) 在 Java 中实现死锁条件。

当出现两个或两个以上线程永远锁定的情况时,这种场景就是死锁场景。死锁通常发生在多线程环境中。以下程序演示了死锁场景

示例

编译并运行

输出

th1 is taking control on the lock java.lang.Object@256f00b6
th1 acquired the lock on java.lang.Object@256f00b6
th2 is taking control on the lock java.lang.Object@5cdddb56
th2 acquired the lock on java.lang.Object@5cdddb56
th3 is taking control on the lock java.lang.Object@248f4cc7
th3 acquired the lock on java.lang.Object@248f4cc7

解释:所有三个线程都能够在第一个对象上获得锁。但是,它们也正在使用共享资源,并且以一种方式启动,即线程无限期地等待以获取第二个对象上的锁。


28) 借助 Java 程序解决数独谜题。

数独谜题是一种流行的日本谜题,它基于数字 1 到 9 的逻辑定位。任务是用数字 1 到 9 填充 9 x 9 的网格,使得所有列、行和子网格(将有 9 个 3 x 3 的子网格)包含每个数字 1 到 9,且没有重复。下面是解决数独的程序。

示例

编译并运行

输出

5 3 4 6 7 8 9 1 2 
6 7 2 1 9 5 3 4 8 
1 9 8 3 4 2 5 6 7 
8 5 9 7 6 1 4 2 3 
4 2 6 8 5 3 7 9 1 
7 1 3 9 2 4 8 5 6 
9 6 1 5 3 7 2 8 4 
2 8 7 4 1 9 6 3 5 
3 4 5 2 8 6 1 7 9

时间复杂度:在最坏情况下,需要将每个数字放入空单元格并检查它是否有效。因此,每个单元格有 9 种选择,使得时间复杂度为 O(9^(n x n))。

辅助空间:O(n x n)。


29) 如何在 Java 中实现线程安全的单例类?

目标是在 Java 中实现一个线程安全的单例类,该类确保即使在多线程环境中也只创建一个类的实例。这是一种常见的设计模式,用于当全局只需要一个类的实例时,例如管理数据库连接等共享资源。

方法

为了实现线程安全并确保单例

  1. 使用双重检查锁定来最小化同步开销。
  2. 使用 volatile 关键字来防止在多线程场景中实例的部分初始化。

示例

编译并运行

输出

Singleton instance created.
Hello from Singleton!
Hello from Singleton!

注意:无论有多少线程调用 getInstance(),消息“Singleton instance created.”都只会出现一次。

复杂度分析:初始化后访问 getInstance() 的时间复杂度为 O(1),首次调用时的同步开销最小。空间复杂度为 O(1),因为只存储了一个实例。


31) HashMap 和 ConcurrentHashMap 有什么区别?

在 Java 中,HashMap 和 ConcurrentHashMap 都是用于存储键值对的数据结构。然而,它们在行为上有所不同,尤其是在多线程环境中。理解这些差异对于根据用例选择合适的映射至关重要。

HashMap 和 ConcurrentHashMap 的主要区别

特性HashMapConcurrentHashMap
线程安全非线程安全。需要外部同步。线程安全。内部处理同步。
性能在单线程环境中速度更快。更适合多线程环境,阻塞最小。
并发级别不适合没有同步的并发操作。允许多个线程同时读写,使用基于段的锁定。
空键和空值允许一个空键和多个空值。不允许空键或空值。
锁定机制需要手动同步整个 HashMap 以实现线程安全。使用细粒度锁(每个段一个锁)以最小化竞争。
用途最适合单线程场景或非并发集合。最适合读/写操作频繁的并发场景。
引入于Java 1.2Java 1.5(作为 java.util.concurrent 包的一部分)。

HashMap 的实现

示例

编译并运行

输出

HashMap contents:
city → New York
name → Alice

ConcurrentHashMap 的实现

示例

编译并运行

输出

ConcurrentHashMap Contents:
city -> San Francisco
name -> Bobz
language -> Java

32) 如何在 Java 中查找两个数组的交集?

给定 Java 中的两个数组,您的任务是找到它们的交集。两个数组的交集是指两个数组共有的元素的集合。结果应该只包含不同的共同元素,除非另有说明(例如,包括重复项)。

示例

交集是

  • 如果我们想要唯一元素 → {2, 3}
  • 如果我们想要包含重复项的共同元素 → {2, 2, 3}

示例

编译并运行

输出

Intersection of the two arrays: 2 3 5

复杂度分析

时间复杂度:这种方法的时间复杂度是 O(n + m),其中 n 是第一个数组的大小,m 是第二个数组的大小。

空间复杂度:空间复杂度是 O(n + k),其中 n 是第一个集合中存储的元素数量,k 是结果集合中存储的共同元素数量。


33) 编写一个程序来查找给定字符串中最长的回文。

给定一个字符串 s,目标是找到其中最长的回文子字符串。回文是正向和反向读取都相同的字符序列,例如“racecar”或“abba”。您的任务是识别最长的此类子字符串。

方法:围绕中心扩展

这个想法是围绕字符串中的每个字符作为回文的潜在中心进行扩展。对于每个字符(以及对于偶数长度回文的每两个字符之间),我们尝试向外扩展,只要两端的字符匹配即可。

为什么采用这种方法?

  • 回文围绕中心对称。
  • 长度为 n 的字符串有 2n-1 个可能的中心(n 个奇数长度和 n-1 个偶数长度)。
  • 该方法易于实现且实践中运行高效。

示例

编译并运行

输出

Longest Palindromic Substring: aba

复杂度分析

时间复杂度:O(n²)

对于字符串中的每个字符,我们向两个方向扩展,在最坏情况下可能需要线性时间。

空间复杂度: O(1)

除了用于跟踪索引的变量之外,没有使用额外的空间。


34) 如何在 Java 中实现自定义异常?

在 Java 中,异常帮助我们在程序执行期间优雅地处理错误。虽然 Java 提供了许多内置异常,如 NullPointerException、IOException 和 ArrayIndexOutOfBoundsException,但在某些情况下,我们希望定义自己的异常,使代码更有意义并特定于应用程序逻辑。这些被称为自定义异常。

创建自定义异常

要在 Java 中创建自定义异常

  • 我们定义一个新类,它扩展 Exception(对于受检查异常)或 RuntimeException(对于未受检查异常)。
  • 我们可以添加构造函数并可选地重写方法。
  • 我们可以添加自定义字段或方法,以丰富异常的上下文。

示例

编译并运行

输出

Caught Exception: Age must be 18 or above to register.

35) 解释 String、StringBuilder 和 StringBuffer 之间的区别。

处理文本数据是一种常见的操作,该语言提供了三个主要的类来处理字符串:StringStringBuilderStringBuffer。虽然它们都处理字符序列,但它们在内存、性能和线程安全方面存在显著差异。

示例

编译并运行

输出

Result: Hello

解释:String 是不可变的,所以即使调用了 concat(),原始字符串文本也不会改变,除非明确重新赋值。

代码示例:StringBuilder

示例

编译并运行

输出

Result: Hello World

说明

StringBuilder 是可变的,并且在需要频繁修改时比 String 更高效。它非常适合单线程场景。

代码示例:StringBuffer

示例

编译并运行

输出

Result: Hello World

说明

StringBuffer 也是可变的,但与 StringBuilder 不同,它是同步的。这使得它在多线程环境中可以安全使用,尽管速度稍慢。

String、StringBuilder 和 StringBuffer 之间的区别

特性 StringStringBuilderStringBuffer
可变性不可变可变可变
线程安全非线程安全非线程安全线程安全
性能频繁更改时速度慢快速比 StringBuilder 慢
同步不能不能是的
用例常量数据,免受更改单线程代码中高效操作多线程代码中安全的字符串操作
引入于Java 1.0Java 1.5Java 1.0

36) 如何在 Java 中反转链表?

反转链表是一个经典的、考验您对指针和节点操作理解的数据结构问题。它是面试中常见的问题,也是链表上许多其他复杂操作的基础。在 Java 中,我们可以使用迭代或递归方法反转链表。

要使用迭代方法原地反转单链表

  • 使用三个指针:prev(最初为 null)、current(从头开始)和 next(临时存储下一个节点)。
  • 遍历列表
    1. 将 current.next 保存到 next 中。
    2. 将 current.next 指向 prev。
    3. 将 prev 移动到 current,将 current 移动到 next。
  • 完成后,prev 将成为反转列表的新头。

示例

编译并运行

输出

Original List:
1 2 3 4 5 
Reversed List:
5 4 3 2 1

复杂度分析

时间复杂度:O(n),其中 n 是节点数。我们只访问每个节点一次。

空间复杂度:O(1) 常数空间。我们在原地反转列表,不使用任何额外的数据结构。


37) 如何使用 Java 的 BlockingQueue 实现生产者-消费者问题?

生产者-消费者问题是多线程挑战的一个经典例子。它涉及两种类型的线程

  • 一个生产者,它生成数据并将其添加到共享资源中。
  • 一个消费者,它从共享资源中获取数据并处理它。

挑战在于确保适当的同步,以便消费者不会尝试消费尚未生产的数据,并且生产者在缓冲区已满时不会添加项目。

示例

编译并运行

输出

Produced: 0
Consumed: 0
Produced: 1
Produced: 2
Consumed: 1
Produced: 3
Consumed: 2
Produced: 4
Consumed: 3
Consumed: 4

复杂度分析

时间复杂度:put() 和 take() 等操作是 O(1),因为它们是原子操作。

空间复杂度:取决于队列的大小(容量为 n 的队列为 O(n))。


38) 编写一个程序来查找字符串中第一个不重复的字符。

给定一个字符串,找出在整个字符串中只出现一次的第一个字符。如果所有字符都重复,则返回一条消息表明这一点。

逻辑流程

  • 输入:“aabbcdeff”
  • 频率:{a=2, b=2, c=1, d=1, e=1, f=2}
  • 频率为 1 的第一个字符 → 'c'

示例

编译并运行

输出

First non-repeating character: c

复杂度分析

时间复杂度:我们扫描字符串两次:一次用于构建映射,一次用于查找不重复的字符。因此,时间复杂度为 O(n)。

空间复杂度:由于字符集是有限的(通常为 26 个小写字母或 256 个 ASCII 字符),因此空间保持不变。因此,时间复杂度为 O(1)。


39) 如何在 Java 中实现 LRU(最近最少使用)缓存?

缓存对于提高性能至关重要。最近最少使用 (LRU) 缓存是一种常见的策略,当缓存超出容量时,它会移除最近最少访问的项目。

主要挑战是设计一个能够实现以下功能的系统:

  • 在 O(1) 时间内检索项目
  • 在 O(1) 时间内插入/更新项目
  • 当缓存已满时移除最近最少使用的项目。

示例

编译并运行

输出

10
-1
-1
30
40

复杂度分析

时间复杂度:由于 HashMap 和常数时间列表操作,get() 和 put() 的时间复杂度为 O(1)

空间复杂度:存储在 HashMap 和链表中的节点的空间复杂度为 O(capacity)。


40) 在 Java 中实现一个线程安全的动态大小数组。

这是 Java 中由 java.util.concurrent 包提供的可调整大小的线程安全数组。每次进行更改时,都会创建一个底层数组的新副本,以保证线程安全。但是,对于经常更新的列表,这可能会占用大量内存。

线程安全性由并发集合提供,例如 CopyOnWriteArrayList,它们不需要显式同步。每次添加或修改元素时,都会创建底层数组的新副本,以确保安全迭代。

示例

编译并运行

输出

The elements present in the array is: 
10
20
30
40
50

41) 编写一个程序将两个已排序数组合并成一个已排序数组。

给定两个已排序的数组,任务是将它们合并成一个已排序的数组。结果数组也应按升序排列。由于两个输入数组都已排序,我们可以使用双指针技术高效地合并它们。

步骤:

  1. 初始化两个指针 i 和 j,从两个数组的 0 开始。
  2. 创建一个空的 결과数组或列表。
  3. 比较 arr1[i] 和 arr2[j] 的元素
    • 将较小的元素添加到结果数组中。
    • 向前移动相应的指针。

步骤 4:如果任何数组中还有剩余元素,请将它们直接添加到结果数组中。

示例

编译并运行

输出

Merged Sorted Array: [1, 2, 3, 4, 5, 6, 7, 8]

复杂度分析

时间复杂度:O(n + m)。我们精确地访问每个元素一次。

空间复杂度:O(n + m) 用于存储结果数组。


42) 如何检测链表中的环?

给定一个单链表的头部。您的任务是确定该链表是否包含。当节点的 next 指针指回列表中前面的节点,从而形成一个循环时,就会发生环。如果存在环,遍历可能会无限进行。

检测链表中环最流行和有效的方法是使用弗洛伊德环检测算法,也称为龟兔赛跑算法。

它是如何工作的?

  1. 使用两个指针
    • 慢指针一次移动一步。
    • 快指针一次移动两步。
  2. 如果链表有环,快指针最终会在环内遇到慢指针。
  3. 如果链表没有环,快指针将在没有遇到慢指针的情况下到达末尾 (null)。

示例

编译并运行

输出

Cycle Detected? True

复杂度分析

时间复杂度:每个指针最多遍历列表一次。因此,时间复杂度为 O(n)。

空间复杂度:不使用额外空间——只有两个指针。因此,时间复杂度为 O(1)。


43) 编写一个程序来查找二叉树的最大深度。

给定二叉树的根节点,您的任务是确定树的最大深度(或高度)。深度定义为从根节点到最远叶节点的最长路径上的节点数。

方法

要找到二叉树的最大深度,我们可以使用递归深度优先搜索 (DFS) 方法

  1. 如果当前节点为 null,则返回 0。
  2. 否则
    • 递归查找左子树的最大深度。
    • 递归查找右子树的最大深度。
    • 当前节点的深度是两个深度中较大的一个加 1(用于当前节点)。

示例

编译并运行

输出

Maximum Depth of Binary Tree: 3

复杂度分析

时间复杂度:每个节点被访问一次。因此,时间复杂度为 O(n)。

空间复杂度:由于递归栈空间,其中 h 是树的高度(在最坏情况下,对于倾斜树可以是 n)。因此,时间复杂度为 O(h)。


44) 如何使用两个队列实现栈?

使用两个队列(先进先出)实现(后进先出)行为。在栈中,最后插入的元素应该是第一个被移除的。然而,队列的操作方式不同——元素在尾部添加,从头部移除。挑战在于使用队列的限制来模拟栈操作(push、pop 和 peek)。

我们可以通过两种主要方式使用两个队列 (q1 和 q2) 来实现栈:

  1. 昂贵的 push() 方法
  2. 昂贵的 pop() 方法

在这里,我们将使用昂贵的 push() 方法,其中我们保持不变性,即最近添加的元素始终位于 q1 的头部。

它是如何工作的?

  1. push(x):
    • 将 x 入队到 q2。
    • 将 q1 中的所有元素出队并入队到 q2。
    • 交换 q1 和 q2 的名称。现在,q1 具有栈的顺序。
  2. pop():简单地从 q1 出队。
  3. top():查看 q1 的头部。
  4. empty():检查 q1 是否为空。

这种方法确保了栈的 LIFO 行为。

示例

编译并运行

输出

Top Element: 30
Popped: 30
Top Element: 20
Is Empty? False

复杂度分析

时间复杂度:push() 操作的时间复杂度为 O(n),因为它每次推入新元素时都涉及将所有元素从第一个队列传输到第二个队列。pop() 和 top() 操作以 O(1) 时间运行,因为它们直接访问或删除主队列的头部元素。

空间复杂度:空间复杂度为 O(n),因为实现使用两个队列来存储所有栈元素,导致空间使用与存储的元素数量成比例。


45) 编写一个 Java 8 程序,按字符串长度对字符串进行分组并打印分组。

在下面的 Java 程序中,我们首先将列表转换为流。之后,我们使用 Collectors.groupingBy(String::length) 方法根据字符串的长度对字符串进行分组,其中 String::length 是一个方法引用,指向 String 类的 length() 方法。最后,结果存储在 Map<Integer, List<String>> 中,其中 key 是字符串长度,value 是所有具有该长度的字符串列表。

示例

编译并运行

输出

{1=[a], 2=[bb, dd], 3=[ccc]}

46) 编写一个程序来查找数组中第 k 小的元素。

给定一个未排序的整数数组,你的任务是找到第 k 小的元素。例如,如果数组是 {7, 10, 4, 3, 20, 15} 且 k = 3,则第 3 小的元素是 7。为了高效地找到第 k 小的元素,一种常用的技术是

使用最小堆(PriorityQueue)

示例

编译并运行

输出

The 3rd smallest element is: 7

复杂度分析

时间复杂度:此方法的时间复杂度为 O(n + k log n)。将所有 n 个元素插入 PriorityQueue(最小堆)需要 O(n),然后弹出 k 次最小元素需要 O(k log n),因为每个 poll() 操作需要 log n 时间进行堆化。

空间复杂度:空间复杂度为 O(n),因为整个包含 n 个元素的数组都存储在最小堆中。除了此数据结构之外没有使用额外的空间,因此在这方面它是空间高效的。


47) 如何在 Java 中不使用内置 PriorityQueue 类实现优先级队列?

优先级队列是一种特殊类型的队列,其中每个元素都与一个优先级相关联,并且元素根据其优先级(更高或更低,取决于配置)进行服务。最小堆是一种二叉树结构,其中每个父节点都小于或等于其子节点。它允许快速访问根部的最小(或最高优先级)元素。

示例

编译并运行

输出

[3, 5, 20, 10]
Peek: 3
Poll: 3
[5, 10, 20]

复杂度分析

时间复杂度:在自定义优先级队列中使用二叉堆的 offer() 和 poll() 操作 each 都需要 O(log n) 时间,因为它们涉及通过向上堆化或向下堆化调整来维护堆属性。

空间复杂度:空间复杂度为 O(n),因为我们使用 ArrayList 在堆结构中存储所有 n 个元素,除了列表本身之外没有额外的内存开销。


48) 编写一个程序来检查两个字符串是否是彼此的排列。

如果两个字符串包含相同字符且频率相同,但顺序可能不同,则称它们互为排列。例如,“listen”和“silent”是排列,而“hello”和“world”不是。给定两个字符串,编写一个 Java 程序来检查一个字符串是否是另一个字符串的排列。比较应该区分大小写,并应考虑所有字符,包括空格和特殊符号。

示例

编译并运行

输出

The strings are permutations of each other.

复杂度分析

时间复杂度:此解决方案的时间复杂度O(n),其中 n 是输入字符串的长度。我们遍历两个字符串一次,并且哈希映射操作(put、get、containsKey)平均都是常数时间。

空间复杂度:如果假设字符集是固定且有限的(如 ASCII),则空间复杂度O(1)。HashMap 最多有 128 个条目。对于 Unicode,在最坏情况下,它将是 O(n),具体取决于字符的多样性。


49) 如何在 Java 中实现快速排序?

快速排序算法基于分治法,通过选择一个枢轴元素将数组分成子数组。在划分数组时,枢轴元素应以这样一种方式定位:小于枢轴的元素放在左侧,大于枢轴的元素放在右侧。

示例

编译并运行

输出

Unsorted Array
[8, 7, 2, 1, 0, 9, 6]
Sorted Array in Ascending Order
[0, 1, 2, 6, 7, 8, 9]

50) 编写一个 Java 8 程序,从列表中筛选并打印偶数。

首先,从列表中创建一个流。之后,它筛选流以仅包含偶数。最后,打印每个偶数。

示例

编译并运行

输出

2
4
6

Java 编程面试 MCQ

1. 二分查找算法的时间复杂度是多少?

  1. O(n)
  2. O(log n)
  3. O(n log n)
  4. O(1)
 

答案:B

解释:二分查找的时间复杂度为 O(log n),因为它在每次迭代中将搜索空间减半。


2. 在 Java 中何时适合使用 HashMap 而不是 TreeMap?

  1. 当元素需要根据其自然顺序排序时。
  2. 当需要保持插入顺序时。
  3. 当需要频繁更新或修改时。
  4. 当需要常数时间访问元素时。
 

答案:D

解释:HashMap 提供常数时间访问元素,使其在需要快速查找时适用。


3. 在 Java 中,使用接口而不是抽象类的主要优点是什么?

  1. 接口可以有默认方法实现。
  2. 接口允许多重继承。
  3. 接口可以定义常量。
  4. 接口允许松散耦合和实现灵活性。
 

答案:D

解释:接口允许类实现多种行为,促进松散耦合并实现实现灵活性。


4. 哪种设计模式用于创建对象而不向客户端暴露实例化逻辑?

  1. 单例
  2. 工厂方法
  3. 原型
  4. Builder
 

答案:B

解释:工厂方法模式定义了创建对象的接口,但允许子类更改将要创建的对象的类型。


5. 在 Java 中,volatile 关键字的目的是什么?

  1. 同步访问共享资源。
  2. 阻止类被子类化。
  3. 指定变量可能异步更改。
  4. 确保只创建类的单个实例。
 

答案:C

解释:Java 中的 volatile 关键字表示变量的值将由不同的线程异步修改。