1、String 为什么不可变?
在Java中,String被设计为不可变的,这意味着一旦创建了String对象,其内容就不能被修改。这是由于String类的设计和实现有以下几个原因:
-
安全性:字符串在Java中广泛用于各种用途,包括作为方法参数、作为键值对的键、作为URL等等。如果字符串是可变的,那么在使用字符串时可能会被修改,导致意外的行为或安全问题。通过使String不可变,可以确保字符串的内容在创建后不会被修改,从而保证代码的安全性。
-
线程安全:在多线程环境下,如果字符串是可变的,那么多个线程可能同时修改同一个字符串对象,从而导致线程安全问题。通过使String不可变,可以避免多线程并发修改字符串的情况,保证线程安全。
-
缓存利用:由于String的不可变性,JVM可以对字符串进行缓存和重用。在Java中,字符串常量池是一种特殊的字符串缓存机制,当创建字符串时,如果字符串常量池中已经存在相同内容的字符串,那么就会重用已有的字符串对象,而不会再创建新的对象。这样可以节省内存空间,提高性能。
-
哈希值优化:String的不可变性使得它的哈希值(hash code)在创建后就不会改变。这在使用字符串作为Map的键值时特别有用,因为如果字符串是可变的,当修改了字符串的内容时,其哈希值也会改变,导致在Map中无法正确获取对应的值。
由于上述优点,使String成为不可变类是Java设计的一个重要决策,这也成为了Java编程中的一个重要特性。不可变性带来了许多优势,包括更高的安全性、线程安全性以及更好的性能和缓存利用。因此,在Java中,我们可以放心地使用String类,而不用担心它的内容会被修改。
2、为何JDK9要将String的底层实现由char[]改成byte[]?
在JDK 9中,并没有将String的底层实现由char[]改成byte[]。String类的底层实现仍然是基于char[],用于表示Unicode字符序列。
在JDK 9中,对于String的改进主要集中在Compact Strings特性上。Compact Strings是一种优化措施,旨在减少String对象的内存占用,特别是当字符串中的字符都是ASCII字符(即字符编码在0到127之间)的情况下。
在JDK 9之前,String对象中的字符序列使用UTF-16编码表示,即每个字符使用16位(2个字节)来存储。对于ASCII字符来说,只需要使用其中的7位(0到127)就足够表示了,但由于每个字符都使用2个字节,会造成内存浪费。
为了解决这个问题,JDK 9引入了Compact Strings特性,使得String对象在特定情况下(大部分字符为ASCII字符)可以使用byte[]数组来存储字符序列。当字符串中的字符都是ASCII字符时,String对象使用byte[]数组来存储字符,每个字符只需要占用1个字节,从而减少了内存的消耗。
需要注意的是,String类的公共API仍然保持不变,对开发者来说,String类的使用方式和以前没有任何区别。Compact Strings是在JVM的内部实现中进行的优化,对开发者是透明的。这个改进使得String对象在存储ASCII字符时更加高效,提升了Java应用程序的性能和内存利用率。
3、String, StringBuffer 和 StringBuilder区别
String、StringBuffer和StringBuilder是Java中用于处理字符串的类,它们之间的区别如下:
-
不可变性:
- String是不可变类,一旦创建了String对象,其内容就不能被修改。如果对String进行拼接、替换等操作,实际上是创建了新的String对象,原有的String对象不会发生变化。
- StringBuffer和StringBuilder是可变类,它们允许对字符串进行修改,而不会创建新的对象。通过调用相应的方法,可以在原有的字符串基础上进行添加、插入、删除等操作。
-
线程安全性:
- String是不可变类,因此是线程安全的。多个线程共享同一个String对象时不会出现线程安全问题。
- StringBuffer是线程安全的,它的方法是通过synchronized关键字来保证线程安全。多个线程可以同时访问同一个StringBuffer对象,不会导致数据错乱。
- StringBuilder是非线程安全的,它的方法没有使用synchronized关键字,因此在多线程环境中使用StringBuilder时需要额外注意线程安全问题。
-
性能:
- 由于String是不可变类,对String进行拼接、替换等操作会频繁创建新的String对象,造成大量的对象创建和内存开销。在频繁操作字符串的情况下,性能可能会受到影响。
- StringBuffer和StringBuilder是可变类,它们在进行字符串操作时直接修改原有的字符序列,避免了频繁创建对象,因此性能相对较好。StringBuilder比StringBuffer性能稍好,因为StringBuilder的方法没有使用synchronized关键字,不需要进行同步。
综上所述,选择使用哪种类取决于具体的需求。如果需要频繁进行字符串操作且涉及多线程场景,建议使用StringBuffer或StringBuilder。如果字符串不会改变,可以使用String来保证不可变性和线程安全性。
4、什么是StringJoiner?
StringJoiner是Java 8中新增的一个用于拼接字符串的工具类。它可以方便地将多个字符串拼接成一个字符串,并且可以指定分隔符、前缀和后缀。
StringJoiner类提供了以下几个主要方法:
-
构造方法:
- StringJoiner(CharSequence delimiter):使用指定的分隔符创建一个StringJoiner对象,不指定前缀和后缀。
- StringJoiner(CharSequence delimiter, CharSequence prefix, CharSequence suffix):使用指定的分隔符、前缀和后缀创建一个StringJoiner对象。
-
add方法:添加要拼接的字符串。可以多次调用add方法来添加多个字符串。
-
merge方法:将另一个StringJoiner对象的内容合并到当前StringJoiner对象中。
-
toString方法:返回拼接后的字符串结果。
下面是一个简单的示例,演示如何使用StringJoiner来拼接字符串:
import java.util.StringJoiner;
public class Main {
public static void main(String[] args) {
// 使用逗号作为分隔符,没有前缀和后缀
StringJoiner joiner1 = new StringJoiner(",");
joiner1.add("Apple");
joiner1.add("Banana");
joiner1.add("Orange");
System.out.println(joiner1.toString()); // 输出:Apple,Banana,Orange
// 使用逗号作为分隔符,方括号作为前缀和后缀
StringJoiner joiner2 = new StringJoiner(",", "[", "]");
joiner2.add("Red");
joiner2.add("Green");
joiner2.add("Blue");
System.out.println(joiner2.toString()); // 输出:[Red,Green,Blue]
// 合并两个StringJoiner对象
joiner1.merge(joiner2);
System.out.println(joiner1.toString()); // 输出:Apple,Banana,Orange,Red,Green,Blue
}
}
StringJoiner类提供了一种简单而灵活的方式来拼接字符串,并且使得代码更加清晰和易读。在拼接多个字符串时,特别是在构建复杂的输出字符串时,StringJoiner可以帮助减少手动处理分隔符和拼接操作,提高代码的可维护性和可读性。
5、String 类的常用方法有哪些?
4 / 4
String类是Java中用于操作字符串的核心类,提供了许多常用的方法来处理字符串。以下是String类的一些常用方法:
-
length():返回字符串的长度。
-
charAt(int index):返回指定索引位置的字符。
-
concat(String str):将指定的字符串连接到当前字符串的末尾。
-
substring(int beginIndex):返回从指定索引开始到字符串末尾的子字符串。
-
substring(int beginIndex, int endIndex):返回从指定索引开始到指定索引结束的子字符串。
-
indexOf(char ch):返回指定字符在字符串中第一次出现的索引。
-
indexOf(String str):返回指定字符串在字符串中第一次出现的索引。
-
lastIndexOf(char ch):返回指定字符在字符串中最后一次出现的索引。
-
lastIndexOf(String str):返回指定字符串在字符串中最后一次出现的索引。
-
startsWith(String prefix):判断字符串是否以指定的前缀开头。
-
endsWith(String suffix):判断字符串是否以指定的后缀结尾。
-
isEmpty():判断字符串是否为空。
-
toUpperCase():将字符串转换为大写。
-
toLowerCase():将字符串转换为小写。
-
trim():去除字符串两端的空格。
-
replace(char oldChar, char newChar):将字符串中的旧字符替换为新字符。
-
replaceAll(String regex, String replacement):将字符串中所有匹配正则表达式的子串替换为指定字符串。
-
split(String regex):根据指定的正则表达式拆分字符串为字符串数组。
-
equals(Object obj):判断字符串是否和指定对象相等。
-
equalsIgnoreCase(String anotherString):忽略大小写判断字符串是否和指定字符串相等。
这只是String类提供的一些常用方法,还有其他一些方法也非常有用。String类的不可变性使得这些方法都是线程安全的,可以在多线程环境中安全地使用。在处理字符串时,熟练掌握这些方法可以使得代码更加简洁和高效。
6、new String(“dabin”)会创建几个对象?
使用new String("dabin")
语句会创建两个对象。
-
首先,会在堆内存中创建一个新的String对象,内容为
"dabin"
。这个String对象是通过构造函数String(String original)
创建的,其中original
是一个已有的String对象。由于String是不可变类,因此这个新的String对象也是不可变的。 -
然后,会在字符串常量池(String Pool)中查找是否已经有内容为
"dabin"
的字符串对象。如果字符串常量池中已经有了这个字符串对象,就会直接返回该对象;如果没有,则将新的String对象添加到字符串常量池中,并返回它。
总结:new String("dabin")
语句会创建一个堆内存中的新的String对象,同时在字符串常量池中进行查找或添加,最终可能返回一个已存在的字符串对象或者返回新创建的字符串对象。因此,尽管使用了new
关键字,但并不一定每次都创建新的对象,这取决于字符串常量池中是否已经存在相同内容的字符串对象。
7、String最大长度是多少?
在Java中,String的最大长度取决于可用的内存空间。String类本身并没有定义字符串的最大长度限制,因为它的长度是动态可变的,并且受到堆内存的限制。
Java中的字符串是通过字符数组来存储的,每个字符占用2个字节(16位),使用UTF-16编码表示。在32位Java虚拟机中,堆内存的默认大小通常是2GB,这意味着一个String对象的字符数组最大长度是约1GB(2^30个字符)。
但是,在实际应用中,由于内存有限,通常不会创建特别大的字符串,以避免占用过多的内存资源和性能问题。如果需要处理大量的数据,通常会采用分段读取、流式处理等方式,而不是将所有数据都存储在单个String对象中。
需要注意的是,在不同的Java虚拟机和操作系统上,对于String的长度限制可能会有所不同。因此,当需要处理大量字符串或者特别大的字符串时,应该根据具体的环境和需求来进行相应的优化和处理。
8、Object常用方法有哪些?
在Java中,所有类都直接或间接地继承自Object类,因此Object类提供了一些通用的方法,这些方法可以在所有类中使用。以下是Object类中一些常用的方法:
-
equals(Object obj):判断当前对象是否和指定对象obj相等。默认情况下,Object类的equals方法使用的是引用比较,即判断两个对象是否指向同一个内存地址。子类可以重写equals方法,来实现自定义的相等判断逻辑。
-
hashCode():返回当前对象的哈希码值(整数)。哈希码是根据对象的内存地址计算得出的,用于在哈希表等数据结构中进行快速查找。
-
toString():返回当前对象的字符串表示。默认情况下,toString方法返回的是对象的类名和哈希码的十六进制表示。子类可以重写toString方法,来返回更有意义的字符串表示。
-
getClass():返回当前对象的运行时类(Class对象)。
-
notify():唤醒在此对象监视器上等待的单个线程。
-
notifyAll():唤醒在此对象监视器上等待的所有线程。
-
wait():使当前线程等待,直到另一个线程调用该对象的notify()或notifyAll()方法来唤醒它。
-
finalize():在垃圾回收器确定不再引用该对象时,垃圾回收器调用该方法来清理资源。
需要注意的是,这些方法在Object类中都是使用protected
修饰的,因此在子类中可以直接调用这些方法。对于一些方法(如equals、hashCode、toString),通常我们在自定义的类中会进行重写,以实现类特定的逻辑。
9、讲讲深拷贝和浅拷贝?
深拷贝(Deep Copy)和浅拷贝(Shallow Copy)都是在进行对象复制时涉及的概念,它们有不同的含义和操作方式:
-
浅拷贝(Shallow Copy):
- 浅拷贝是指将一个对象复制到另一个对象,复制的是对象的引用而不是对象本身。
- 浅拷贝产生一个新对象,该对象与原始对象共享同一个内部数据(如数组、集合等)的引用,因此修改新对象会影响原始对象,反之亦然。
- 浅拷贝通常可以通过
clone()
方法来实现,或者手动复制对象的字段。
-
深拷贝(Deep Copy):
- 深拷贝是指将一个对象复制到另一个对象,复制的是对象本身而不是对象的引用。
- 深拷贝产生一个新对象,该对象拥有原始对象所有内部数据的副本,而不是共享同一个内部数据的引用,因此新对象和原始对象互不影响。
- 深拷贝通常需要递归地复制对象的所有引用类型字段,以确保所有数据都是独立的。
示例代码如下所示:
class Person implements Cloneable {
private String name;
private int age;
private Address address;
// ... 省略构造方法和其他代码 ...
// 浅拷贝
public Object clone() throws CloneNotSupportedException {
return super.clone();
}
// 深拷贝
public Person deepCopy() throws CloneNotSupportedException {
Person newPerson = (Person) super.clone();
newPerson.address = new Address(address.getCity(), address.getStreet());
return newPerson;
}
}
在上面的示例中,Person
类包含一个Address
对象作为其字段。在浅拷贝方法中,调用了super.clone()
来复制对象本身,但由于Address
对象是引用类型,所以新对象和原始对象共享同一个Address
对象。在深拷贝方法中,通过手动创建一个新的Address
对象,将address
字段的数据复制给新的对象,从而实现了完全独立的深拷贝。
总结:浅拷贝复制对象的引用,深拷贝复制对象的实际数据。在选择拷贝方式时,需要根据具体的需求和对象的复杂度来决定使用哪种方式。
10、两个对象的hashCode()相同,则 equals()是否也一定为 true?
不一定。如果两个对象的hashCode()相同,equals()可能为true,也可能为false。这是因为hashCode()和equals()是两个不同的方法,它们的实现逻辑可以相互独立。
hashCode()方法用于计算对象的哈希码值,哈希码值是一个整数,用于在哈希表等数据结构中进行快速查找。如果两个对象的hashCode()相同,意味着它们的哈希码值相同,但并不表示这两个对象是相等的。
equals()方法用于判断两个对象是否相等。默认情况下,equals()方法是使用引用比较,即判断两个对象是否指向同一个内存地址。但是,可以在类中重写equals()方法,自定义对象的相等判断逻辑。通常情况下,重写equals()方法时会根据对象的字段来进行比较,而不仅仅是比较引用。
Java中规定,如果两个对象的equals()方法返回true,那么它们的hashCode()方法必须返回相同的值。这是因为在使用哈希表等数据结构时,对象相等的话,它们的哈希码值必须相同,以保证哈希表能正常工作。
但是,并没有规定如果两个对象的hashCode()相同,equals()方法就一定要返回true。这是因为不同的对象可能有相同的哈希码值(哈希冲突),因此hashCode()相同并不代表对象相等。
因此,正确的实现方式是:在重写equals()方法时,必须同时重写hashCode()方法,保证相等的对象具有相同的哈希码值,从而遵守hashCode()和equals()方法之间的规则。
11、Java创建对象有几种方式?
在Java中,可以使用以下几种方式来创建对象:
使用new关键字:最常见的方式是使用new
关键字来创建对象。通过new
关键字可以调用类的构造方法来初始化对象。
MyClass obj = new MyClass();
使用Class的newInstance()方法:通过反射机制可以使用Class类的newInstance()
方法来创建对象。这种方式可以在运行时动态创建对象,但需要注意该方法在Java 9及之后版本已被废弃。
Class<MyClass> clazz = MyClass.class;
MyClass obj = clazz.newInstance();
使用Constructor类的newInstance()方法:通过反射机制也可以使用Constructor类的newInstance()
方法来创建对象。这种方式可以在运行时动态创建对象,并且可以传递构造方法的参数。
Class<MyClass> clazz = MyClass.class;
Constructor<MyClass> constructor = clazz.getConstructor(paramTypes); // paramTypes是构造方法的参数类型数组
MyClass obj = constructor.newInstance(args); // args是构造方法的实际参数数组
使用clone()方法:对象实现了Cloneable接口,可以使用clone()
方法来创建对象的副本。需要注意的是,clone()方法执行的是浅拷贝。
MyClass obj1 = new MyClass();
MyClass obj2 = obj1.clone();
使用反序列化:通过对象的序列化和反序列化可以创建对象的深拷贝。
// 对象序列化为字节流
ByteArrayOutputStream baos = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(baos);
oos.writeObject(obj);
// 字节流反序列化为对象
ByteArrayInputStream bais = new ByteArrayInputStream(baos.toByteArray());
ObjectInputStream ois = new ObjectInputStream(bais);
MyClass obj2 = (MyClass) ois.readObject();
总结:以上是创建对象的几种常用方式,每种方式都有其适用的场景和特点。通常情况下,使用new
关键字是最常见且简单的创建对象的方式。对于特定的需求,例如动态创建对象或者对象的深拷贝,可以考虑使用反射或序列化等方式来实现。
12、equals和==有什么区别?
在Java中,equals()和==是用于比较对象的两种不同方式,它们有以下区别:
-
equals()方法:
- equals()方法是用于比较两个对象的内容是否相等。
- equals()方法是Object类的方法,所有的类都继承自Object类,但是在子类中可以重写equals()方法,来实现自定义的相等判断逻辑。
- 默认情况下,equals()方法在Object类中实现的是引用比较,即判断两个对象是否指向同一个内存地址。因此,如果没有在子类中重写equals()方法,使用equals()方法和使用==是等价的。
-
==运算符:
- ==运算符是用于比较两个对象的引用是否相等。
- 无论是基本数据类型还是对象,使用==运算符比较的都是它们的内存地址。
- 如果两个对象的引用指向相同的内存地址,则==运算符返回true;如果指向不同的内存地址,则返回false。
示例代码如下所示:
String str1 = "Hello";
String str2 = "Hello";
String str3 = new String("Hello");
System.out.println(str1.equals(str2)); // true,内容相等
System.out.println(str1.equals(str3)); // true,内容相等
System.out.println(str1 == str2); // true,引用相同(常量池共享字符串)
System.out.println(str1 == str3); // false,引用不同(新的对象在堆中创建)
在上面的示例中,str1和str2都是指向字符串常量池中的"Hello"字符串的引用,因此它们的内容相等且引用相同。而str3是通过new关键字在堆内存中创建的新对象,虽然内容与"Hello"相等,但引用不同。因此,str1.equals(str3)返回true,而str1 == str3返回false。
总结:equals()方法比较对象的内容,==运算符比较对象的引用。在比较对象时,通常应该使用equals()方法来确保逻辑正确,除非特别需要比较对象的引用。
13、final, finally, finalize 的区别
final
, finally
, 和 finalize
是三个在Java中具有不同用途的关键字。
-
final:
final
是一个修饰符,用于标记一个类、方法或变量,表示其不可改变或不可继承。- 当应用于类时,
final
表示该类不能被继承,即它是一个最终类,不能有子类。 - 当应用于方法时,
final
表示该方法不能被子类重写,即它是一个最终方法。 - 当应用于变量时,
final
表示该变量只能被赋值一次,即它是一个常量。
-
finally:
finally
是一个关键字,用于定义在 try-catch-finally 块中的一个代码块。finally
块中的代码无论在 try 或 catch 块中是否有异常发生,都会被执行。finally
常用于释放资源、关闭连接等必须执行的操作,确保资源的正确释放。
try {
// 可能会产生异常的代码
} catch (Exception e) {
// 异常处理逻辑
} finally {
// 无论是否有异常,都会执行的代码块
}
- finalize:
finalize
是一个方法,是Object类的一个成员方法。finalize
方法是在垃圾回收器回收对象时调用的,用于进行对象的清理操作。- 通常不建议直接使用
finalize
方法来进行资源回收,因为它不是及时和可靠的方式,而是由垃圾回收器自行决定何时执行。
总结:
final
:用于修饰类、方法或变量,表示不可改变或不可继承。finally
:用于定义在 try-catch-finally 块中的一个代码块,确保资源的正确释放。finalize
:是Object类的一个方法,在对象被垃圾回收器回收时执行,用于对象的清理操作。不推荐直接使用此方法进行资源回收。
14、final关键字的作用?
final
关键字在Java中有多种用途,它可以用于修饰类、方法和变量,其作用如下:
- final 修饰类:
- 当类被声明为
final
时,它变成了一个最终类,不能被继承。 - final 类不能有子类,因此它的实现是最终的,不能再被改变。
- 常见用法是在设计不希望被继承的类时,或者为了确保安全性和完整性而限制子类的扩展。
- 当类被声明为
final class MyClass {
// 类的内容
}
- final 修饰方法:
- 当方法被声明为
final
时,它变成了一个最终方法,不能在子类中被重写。 - final 方法的行为在子类中是不可改变的,保持一致性。
- 常见用法是在设计不希望被重写的方法,或者为了确保子类不改变某些行为而限制方法的重写。
- 当方法被声明为
class MyClass {
final void myMethod() {
// 方法的内容
}
}
- final 修饰变量:
- 当变量被声明为
final
时,它变成了一个常量,只能被赋值一次。 - final 变量必须在声明时或构造器中进行初始化,并且不能再被修改。
- 常见用法是在声明常量或一次性赋值的情况下,以保持变量的不变性。
- 当变量被声明为
final int myNumber = 10; // 声明一个常量
总结:final
关键字用于表示最终、不可改变的特性。它可以用于类、方法和变量,用途包括限制类的继承、方法的重写和变量的再赋值,从而增加代码的可靠性、安全性和可维护性。