第 1 题,奇怪的 nullnull
第 1 题,奇怪的 nullnull
QA
Step 1
Q:: 面试题1
A:: 在 Java 中,为什么未初始化的 String 对象相加会打印 'nullnull'
?
Step 1
Q:: 答案
A:: 在 Java 中,未初始化的 String 对象默认值为 null。当使用 '+' 运算符连接两个未初始化的 String 对象时,编译器会将这个操作优化为使用 StringBuilder 进行字符串拼接。在 StringBuilder 的 append 方法中,如果传入的字符串为 null,StringBuilder 会调用 appendNull 方法,将 'null' 字符串追加到 StringBuilder 内部的字符数组中,因此两个 null 拼接后的结果是 'nullnull'
。
Step 2
Q:: 面试题2
A:: 在 Java 中,字符串相加操作是如何优化的?
Step 2
Q:: 答案
A:: Java 编译器会将字符串相加操作优化为 StringBuilder 的使用。即使只是简单的字符串相加操作,如 'str1 + str2',编译器会将其转换为 'new StringBuilder().append(str1).append(str2).toString()'
。这样做的目的是为了提高字符串拼接的性能,因为 StringBuilder 在进行字符串拼接时不需要频繁地创建新对象,而是操作内部的字符数组。
Step 3
Q:: 面试题3
A:: String 和 StringBuilder 的区别是什么?为什么在拼接字符串时推荐使用 StringBuilder?
Step 3
Q:: 答案
A:: String 是不可变对象,每次对 String 的修改都会生成一个新的 String 对象,这样在进行大量字符串拼接时会产生很多中间对象,导致性能下降。而 StringBuilder 是可变对象,它在内部使用一个字符数组来存储字符,可以高效地进行字符串拼接操作,不会产生过多的中间对象。因此,在需要进行大量字符串拼接时,推荐使用 StringBuilder 而不是 String。
Step 4
Q:: 面试题4
A:: 在什么情况下应该避免使用 StringBuilder 进行字符串操作?
Step 4
Q:: 答案
A:: 当字符串操作的次数较少且对线程安全有要求时,应避免使用 StringBuilder,因为 StringBuilder 不是线程安全的。在这种情况下,可以使用 StringBuffer,它与 StringBuilder 类似,但所有方法都是同步的,确保了线程安全。
Step 5
Q:: 面试题5
A:: 如何避免在 Java 中因字符串拼接导致的性能问题?
Step 5
Q:: 答案
A:: 在需要大量拼接字符串的场景下,避免使用 '+' 操作符,而是使用 StringBuilder 或 StringBuffer 进行拼接。此外,如果可以预估最终字符串的长度,可以使用 StringBuilder(int capacity)
构造函数来初始化一个合适大小的字符数组,避免数组扩容带来的性能损失。
用途
这个面试内容涉及 Java 中字符串处理的底层机制,理解这些概念对于编写高效、可靠的代码非常重要。在实际生产环境中,字符串拼接操作非常常见,尤其是在处理大量文本数据时,选择合适的字符串处理方式(如 StringBuilder 而非 String)能够显著提升性能。此外,了解 String 的不可变性有助于避免潜在的线程安全问题和内存浪费。\n相关问题
第 2 题,改变 String 的值
QA
Step 1
Q:: 如何改变一个 String 的值,而不重新指向其他对象?
A:: 在 Java 中,String 是不可变的,一旦创建,String 的值不能直接修改。但是,可以通过反射机制修改 String 内部的 char 数组来改变其值,而不改变引用指向的对象。具体做法是通过 Field
类访问 String
类的 value
字段,并使用 setAccessible(true)
方法绕过私有访问权限,然后将新的字符数组设置为 String
对象的内部数组。以下是代码示例:
public static void main(String[] args) throws NoSuchFieldException, IllegalAccessException {
String s="Hydra";
System.out.println(s+": "+s.hashCode());
Field field = String.class.getDeclaredField("value");
field.setAccessible(true);
field.set(s,new char[]{'T','r','u','n','k','s'});
System.out.println(s+": "+s.hashCode());
}
运行后可以看到 hashCode
没有变化,表明对象本身没有改变。
Step 2
Q:: 为什么 String 是不可变的?
A:: Java 的 String 类被设计为不可变(immutable),主要是出于以下几个原因:
1.
线程安全:不可变对象天生是线程安全的,因为它们的状态在创建后不会改变,多个线程可以安全地共享它们。
2.
性能优化:由于 String 不可变,因此它的哈希码可以被缓存,这使得 String 对象可以高效地用作哈希表的键。
3.
安全性:不可变对象的值一旦被创建,就不会被篡改,这对于像字符串常量池、类加载器、敏感数据(如用户名、密码)等场景是非常重要的。
Step 3
Q:: String 类中的 hashCode 是如何实现的?
A:: 在 Java 中,String 类的 hashCode
方法通过对字符串中的字符进行迭代并计算加权和来生成哈希码。具体公式为:
public int hashCode() {
int h = hash;
if (h == 0 && value.length > 0) {
char val[] = value;
for (int i = 0; i < value.length; i++) {
h = 31 * h + val[i];
}
hash = h;
}
return h;
}
这里的 hash
是一个实例变量,用于缓存计算出的哈希码,以便后续调用时直接返回已计算的值,从而提高性能。
用途
这个内容主要考察面试者对 Java String 的不可变性及其背后的原理理解。在实际生产环境中,不可变对象在并发编程中非常有用,可以避免因为对象状态修改而引发的线程安全问题。此外,在使用 String 作为哈希表的键或者进行敏感数据传输时,不可变性提供了额外的安全保障。理解这些知识有助于开发者编写更安全和高效的代码,特别是在涉及多线程或需要优化性能的场景中。\n相关问题
第 3 题,创建了几个对象?
QA
Step 1
Q:: 面试题:
创建了几个对象?
A:: 在代码 String s = new String("Hydra");
中,实际创建了两个对象。一个是在堆上创建的字面量字符串对象 "Hydra"
,另一个是通过 new
关键字创建的 String 对象,这个对象包装了前面的字面量字符串。
Step 2
Q:: 面试题:
什么是字符串常量池?
A:: 字符串常量池(String Pool)是 JVM 中的一块特殊区域,用于存储字符串字面量。当使用字面量形式创建字符串时,JVM 首先检查常量池中是否已经存在相同的字符串对象,如果存在则返回该对象的引用,否则将该字符串放入常量池中并返回其引用。
Step 3
Q:: 面试题:
字符串的 intern()
方法有什么作用?
A:: intern()
方法用于将字符串对象放入字符串常量池中。如果常量池中已经包含一个相同内容的字符串,则返回该字符串的引用;如果没有,则将该字符串添加到常量池并返回它的引用。
Step 4
Q:: 面试题:
JVM 在处理字符串时是如何优化内存的?
A:: JVM 通过字符串常量池优化内存使用。在程序运行过程中,重复使用的字符串字面量只会在常量池中存储一次,这样可以避免重复创建相同内容的字符串对象,减少内存消耗。
Step 5
Q:: 面试题:
在字面量和 new
关键字创建字符串对象时有什么区别?
A:: 使用字面量创建字符串时,JVM 会首先检查字符串常量池,如果存在则直接返回该对象引用。如果使用 new
关键字创建字符串对象,则无论常量池中是否存在相同内容的字符串,都会在堆上创建一个新的 String 对象。
用途
面试这个内容是为了考察应聘者对 Java 内存模型、JVM 优化机制以及字符串处理的理解。在实际生产环境中,字符串的创建和管理对应用程序的性能和内存使用有重要影响,尤其是在处理大量字符串操作时,了解如何有效使用字符串常量池和 `intern()` 方法可以显著减少内存消耗并提升性能。\n相关问题
第 4 题,烧脑的 intern
QA
Step 1
Q:: 为什么在 Java 中会有 String 常量池?
A:: Java 中的 String 常量池(String Intern Pool)是一种优化手段,用于减少内存消耗并提高字符串操作的效率。当多个字符串对象内容相同且是通过字面量定义的方式创建时,JVM 会将这些字符串的引用指向常量池中的同一个字符串对象,从而避免重复创建相同内容的字符串对象。
Step 2
Q:: 调用 String.intern()
方法的作用是什么?
A:: String.intern()
方法的作用是将当前字符串对象的引用加入到字符串常量池中。如果常量池中已经存在与该字符串内容相同的对象,则返回常量池中该对象的引用;如果不存在,则将当前字符串对象加入常量池,并返回它的引用。
Step 3
Q:: 在什么情况下,String.intern()
方法会返回与原字符串不同的引用?
A:: 当常量池中已经存在与该字符串内容相同的对象时,String.intern()
方法会返回常量池中已有对象的引用,而不是当前字符串对象的引用。比如在 String s1 = new String("Hydra"); String s2 = s1.intern();
中,如果常量池中已经存在内容为 "Hydra" 的字符串对象,s2 将指向这个常量池中的对象,而不是 s1
所指向的对象。
Step 4
Q:: 为什么第一段代码中的 s1 == s2 和 s1 == "Hydra"
都返回 false?
A:: 在第一段代码中,s1
是通过 new String("Hydra")
创建的,它在堆中分配了一个新的对象,而 s2
则是 s1.intern()
返回的引用,指向常量池中的字符串对象。由于 s1
和 s2
分别指向不同的对象,因此 s1 == s2
返回 false。同样,s1
也与常量池中的 "Hydra"
不是同一个对象,因此 s1 == "Hydra"
也返回 false。
Step 5
Q:: 为什么第二段代码中 s1 == s2
返回 true?
A:: 在第二段代码中,s1
是通过拼接字符串 "Hy" 和 "dra"
而生成的,并在 s1.intern()
时将这个对象的引用放入常量池。此时,常量池中还没有 "Hydra"
的引用,因此 s1.intern()
返回的就是 s1
自身的引用。当 String s2 = "Hydra";
执行时,JVM 发现常量池中已经存在 "Hydra",所以 s2
直接指向 s1
,最终 s1 == s2
返回 true。
用途
面试这个内容的目的是为了考察应聘者对 Java 内存管理机制的理解,特别是在字符串的处理和优化方面的知识。对于大型项目,特别是涉及大量字符串操作的应用程序,如日志处理、大型数据集的解析、文本处理等场景,理解和运用 String 常量池和 intern 方法可以显著提高应用程序的性能和减少内存消耗。此外,了解这些机制也是应对内存泄漏问题的关键之一。\n相关问题
第 5 题,还是创建了几个对象?
QA
Step 1
Q:: 面试题:
在 Java 中,编译期常量和运行时常量的区别是什么?举例说明。
A:: 答案: 编译期常量是在编译时即可确定其值的常量,通常是被 final 修饰且在声明时就已经被初始化的基本类型或字符串类型。编译期常量的值在编译阶段已被计算并替换,因此其值在字节码中可以直接呈现。而运行时常量则是尽管被 final 修饰,但其值需要在程序运行期间才能确定,例如通过调用函数获取的值。编译器无法在编译时优化或折叠运行时常量。示例:
final String s1 = "hello" + "Hydra";
(编译期常量)
和 final String s2 = UUID.randomUUID().toString() + "Hydra";
(运行时常量)
。
Step 2
Q:: 面试题:
为什么在 Java 中,字符串拼接有时会在编译期间进行优化?
A:: 答案: Java 编译器应用了一种称为常量折叠 (Constant Folding) 的优化技术。如果两个字符串拼接的操作可以在编译期确定 (如两个字面量拼接)
,编译器会在编译期间直接计算出结果并用拼接后的字符串替换原有表达式。这种优化减少了运行时不必要的计算,从而提高了程序的性能。例如:String s = "a" + "b" + "c";
将在编译时直接折叠为 String s = "abc";
。
Step 3
Q:: 面试题:
为什么字符串 s1
和字面量 "helloHydra"
比较返回 true,而 s2
与 "helloHydra"
比较返回 false?
A:: 答案:
s1
是由 final
修饰的编译期常量参与拼接而成,因此在编译期间已被折叠为字面量 "helloHydra"
,所以 s1
与 "helloHydra"
是同一个字符串对象,比较结果为 true。而 s2
中的 h2
虽然被 final
修饰,但其值在编译时未确定,因此 s2
拼接结果为运行时常量,与 "helloHydra"
不是同一个对象,比较结果为 false。
Step 4
Q:: 面试题: 什么是字面量 (Literal)
?它在 Java 中的作用是什么?
A:: 答案:
字面量是源代码中表示固定值的符号,直接表示一个常量值。Java 中的字面量包括整数型、浮点型、字符型、字符串型和布尔型字面量。字面量无需使用 new
关键字来创建对象,而是直接在代码中赋值。例如,int i = 10;
中的 10
就是一个整数型字面量。字面量在代码中可以用来初始化变量,定义编译期常量等。
用途
面试中讨论编译期常量和运行时常量的目的在于考察候选人对 Java 编译器行为和 JVM 内存管理的理解。这些知识在实际生产环境中非常重要,特别是在处理大量字符串操作时,可以显著影响程序性能和内存使用效率。例如,在处理高并发应用程序时,理解字符串的驻留机制以及避免不必要的对象创建可以显著减少垃圾回收的压力。编译器优化技术,如常量折叠,也帮助开发人员编写更高效的代码。因此,了解这些概念对于编写高性能 Java 应用程序至关重要。\n相关问题
总结
QA
Step 1
Q:: String s="a"+"b"+"c"
,到底创建了几个对象?
A:: 在JDK 8
中,String s = "a" + "b" + "c";
只会创建一个String对象,因为在编译期,编译器会将常量表达式直接优化为String s = "abc";
。所以只会创建一个字符串对象。如果涉及到运行时的字符串拼接,例如String s = str1 + str2;
,则会涉及到StringBuilder对象的创建。
Step 2
Q:: JDK中字符串常量池是什么?其作用是什么?
A:: 字符串常量池是JDK为优化性能和减少内存消耗而设计的一种机制。它存储所有在编译时确定的字符串常量。通过这个池,重复的字符串字面量不会占用多余的内存空间,而是引用池中的同一对象。JDK 7
之后,字符串常量池从PermGen移到了Heap中,这样做是为了优化GC和减少PermGen内存溢出的问题。
Step 3
Q:: JDK 6 与 JDK 7
之后,字符串常量池有什么变化?
A:: 在JDK 6及之前,字符串常量池存储在PermGen中,池中存储的是字符串的对象实例。JDK 7
之后,字符串常量池被移到了Heap中,且池中存储的是字符串对象的引用而不是对象本身,这减少了PermGen区的内存压力,并使得字符串处理更为高效。
用途
面试中关于字符串常量池和字符串对象创建的问题是为了考察应聘者对Java内存管理的理解,以及他们对编译期和运行期字符串处理机制的熟悉程度。这些知识在优化应用程序性能、减少内存消耗以及避免潜在的内存泄漏或溢出问题时非常重要。在实际生产环境中,当应用程序涉及大量字符串操作时,正确理解这些概念有助于编写高效、健壮的代码。\n相关问题
参考资料
QA
Step 1
Q:: 题目:请解释下面代码的输出是什么,并解释为什么:
public class Test1 {
private static String s1;
private static String s2;
public static void main(String[] args) {
String s = s1 + s2;
System.out.println(s);
}
}
A:: 答案:代码的输出是 nullnull
。在这段代码中,s1
和 s2
是静态字符串变量,但未被初始化,因此它们的值为 null
。在 Java 中,当 null
值参与字符串拼接时,会被转换为字符串字面量 "null"
。因此,s1 + s2
的结果是 "nullnull"
。
Step 2
Q:: 题目:在 Java 中,String 是不可变的。请解释为什么 String
被设计为不可变,以及这种设计的优点是什么?
A:: 答案:String
类在 Java 中被设计为不可变的(immutable),这是通过将其底层数据结构(即 char[]
数组)定义为 final
且私有的来实现的。String
的不可变性带来了多个优点,包括:
1.
线程安全性:由于 String
对象是不可变的,多个线程可以安全地共享同一个 String
对象,而无需进行同步。
2.
性能优化:由于不可变性,Java 编译器和 JVM 可以对 String
进行大量优化,例如字符串池(String Pool)和常量折叠(Constant Folding)。
3.
安全性:不可变的 String
对象可以用作安全的标识符,如在网络连接或文件路径中,避免被恶意代码修改。
Step 3
Q:: 题目:解释下面代码中,创建了多少个 String 对象?
String s = new String("Hydra");
A:: 答案:在这段代码中,一共创建了两个 String
对象。首先,"Hydra"
是一个字符串字面量,它会在编译期间存储在字符串常量池中(如果常量池中没有相同的字符串对象)。其次,new String("Hydra")
会在堆上创建一个新的 String
对象,它的内容与常量池中的 "Hydra"
相同,但它们是不同的对象。
Step 4
Q:: 题目:请解释 String
的 intern()
方法的作用,并说明下面代码的输出是什么:
String s1 = new String("Hydra");
String s2 = s1.intern();
System.out.println(s1 == s2);
System.out.println(s1 == "Hydra");
System.out.println(s2 == "Hydra");
A:: 答案:intern()
方法用于将字符串添加到字符串常量池中,并返回该字符串在池中的引用。如果常量池中已经包含了一个与当前字符串相等的字符串,则返回池中的那个字符串引用。上述代码的输出为:
false
false
true
在代码中,s1
是通过 new
关键字创建的,因此它是堆中的一个新对象。s1.intern()
返回字符串常量池中已存在的 "Hydra"
字符串的引用,赋值给 s2
。因此,s1
和 s2
是不同的对象,s2
和常量池中的 "Hydra"
是同一个对象。
Step 5
Q:: 题目:在以下代码中,共创建了多少个对象?
String s = "a" + "b" + "c";
A:: 答案:只创建了一个对象。由于编译器会对字符串字面量进行常量折叠(Constant Folding),"a" + "b" + "c"
在编译期间会被折叠为 "abc"
,因此在运行时只会创建一个 "abc"
字符串对象,并且这个对象会被存储在字符串常量池中。