Java字符串简介
字符串的表示
public final class String
implements java.io.Serializable, Comparable<String>, CharSequenceJava中的字符串是表示一个字符序列的对象。在Java中,字符串是作为对象出现的,由java.lang.String类和java.lang.StringBuilder或java.lang.StringBuffer类实现。

字符串字面量
字符串字面量是Java中最常见的创建字符串的方式。它是由双引号包围的字符序列,例如 "Hello, World!"。当编译器遇到字符串字面量时,它会创建一个String对象,并将其放入字符串常量池中。如果字符串常量池中已经存在相同值的字符串对象,则不会再创建新的对象,而是返回已有对象的引用。
String hello = "Hello, World!"; // 字符串字面量在上面的代码中,hello变量引用了一个String对象,该对象包含文本 "Hello, World!"。
String对象
通过new关键字创建的字符串对象是String类的实例。这种方式会显式地创建一个新的String对象,而不是使用字符串常量池中的对象。当使用new关键字时,Java虚拟机会在堆内存中为新的String对象分配空间,并将其初始化为指定的字符串值。
String s = new String("Hello, World!"); // 使用new关键字创建String对象在上面的代码中,我们创建了一个新的String对象,并将其赋值给变量s。这个对象与任何其他使用相同字面量创建的字符串对象是独立的。
需要注意到"Hello, World!"是字符串字面量,因此会创建一个String对象,并将其放入字符串常量池中。这个对象是比s所引用的对象先创建的,并且它们相互独立。
StringBuilder和StringBuffer对象
StringBuilder和StringBuffer是两个用于创建可变字符串的类。它们都提供了可以在原有字符串基础上进行修改的方法,如append、insert、delete和reverse等。StringBuilder是Java 5引入的,它是StringBuffer的替代品,提供了相同的功能,但是不是线程安全的,因此在单线程环境下性能更好。
StringBuilder sb = new StringBuilder("Hello");
sb.append(", World!"); // 追加字符串
String s = sb.toString(); // 转换为String对象在上面的代码中,我们创建了一个StringBuilder对象,并使用append方法添加了额外的文本。最后,我们使用toString方法将StringBuilder的内容转换为String对象。
StringBuffer与StringBuilder类似,但它提供了线程安全的功能,适合在多线程环境中使用。
StringBuffer sb = new StringBuffer("Hello");
sb.append(", World!"); // 追加字符串
String s = sb.toString(); // 转换为String对象字符串的不可变性
Java中的String对象是不可变的,这意味着一旦一个String对象被创建,它包含的字符序列就不能被改变。这是因为String类的设计采用了final修饰的字符数组来存储字符串内容,这个数组在创建后就不能再改变大小或内容。
@Stable
private final byte[] value;
private final byte coder;
@Stable注解表示这个字段的引用是稳定的,即它引用的对象在GC期间不会移动,这对于性能优化是有帮助的。
coder字段表示value数组中字节所使用的编码。可能的值包括LATIN1和UTF16。这决定了String内部如何存储字符。
当我们在Java程序中对字符串执行修改操作时,实际上是在创建一个新的String对象。这是因为字符串操作(如连接、替换、截取等)都会返回一个新的String对象,而不是修改原始对象。这是因为String类提供了相应的方法,这些方法在执行操作时会创建新的字符串对象。
例如以下代码:
String s = "Hello";
s = s + " World!";在这段代码中,s最初引用了一个包含"Hello"的String对象。当执行+操作时,String类的重载+运算符会在后台调用String.concat方法,这个方法会创建一个新的String对象,包含了"Hello World!",然后变量s被重新分配指向这个新的对象。原始的"Hello"字符串对象仍然存在于内存中,但是没有引用指向它,它将成为垃圾收集的对象。
这种不可变性带来了几个重要的优点:
- 线程安全:由于
String对象是不可变的,它们在多线程环境中是安全的。这意味着我们可以在不同的线程之间共享String对象,而不用担心一个线程对字符串的修改会影响其他线程。因为字符串的值一旦确定就不会改变,所以多个线程看到的字符串内容总是相同的。 - 常量池优化:字符串的不可变性允许Java虚拟机实现字符串常量池(String Constant Pool)。当使用双引号创建字符串字面量时,Java虚拟机会首先在常量池中查找是否有相同的字符串。如果有,则返回这个字符串的引用;如果没有,则在常量池中创建一个新的字符串并返回其引用。这种机制可以节省内存,因为相同的字符串字面量不需要重复创建,同时提高了性能,因为字符串常量池中的对象可以快速访问。
- 安全性:字符串常用于存储敏感信息,如密码、文件路径等。字符串的不可变性确保了这些信息不会被意外的代码修改。例如,如果我们将一个密码存储在一个字符串变量中,即使另一个引用改变了这个变量的值,原始的密码字符串仍然保持不变,不会因为意外的代码而泄露。
- 简化编程:字符串的不可变性使得它们可以作为哈希表的键值。因为字符串的值不会改变,所以它们的哈希码也是固定的,同时哈希码可以被缓存,只需要计算一次。这简化了哈希表的实现,因为我们不需要担心键值的改变会导致哈希码的变化,从而影响哈希表的性能。
字符串常量和字符串对象的区别
字符串常量和字符串对象是两个不同的概念,它们在内存中的存储方式和使用场景都有所不同。下面是它们之间的区别:
- 字符串常量(String Literal):
- 字符串常量是直接用双引号括起来的字符序列,例如
"Hello, World!"。 - 当字符串常量在代码中出现时,Java编译器会将其放入字符串常量池(String Constant Pool)中。字符串常量池是一个内存区域,用于存储独一无二的字符串字面量。
- 如果字符串常量池中已经存在相同值的字符串对象,那么新的引用将指向已有的对象,而不会创建新的对象。
- 字符串常量是不可变的,一旦创建,其内容就不能改变。
- 使用字符串常量可以节省内存,因为相同的字符串字面量只会被存储一次。
- 字符串常量是直接用双引号括起来的字符序列,例如
- 字符串对象(String Object):
- 字符串对象是通过
new关键字显式创建的String类的实例,例如String s = new String("Hello, World!");。 - 每次使用
new关键字创建字符串对象时,都会在堆内存(Heap Memory)中创建一个新的String对象。 - 即使两个字符串对象的值相同,它们也是不同的对象实例,拥有不同的内存地址。
- 字符串对象同样是不可变的,一旦创建,其内容也不能改变。
- 创建字符串对象时,Java虚拟机不会检查字符串常量池,而是直接在堆内存中分配空间。
- 如果利用了字符串常量创建字符串对象,如果不存在该字符串常量,会先创建字符串常量再创建字符串对象。
- 字符串对象是通过
Java字符串方法
equals方法
在Java中,所有的类都默认继承自Object类,Object类提供了一个equals方法,用于比较两个对象的引用是否相同。这个方法在默认情况下是比较两个对象在内存中的地址,即两个对象的引用是否指向同一个对象实例。
public boolean equals(Object obj) {
return (this == obj);
}从上面
Object类的equals方法可以看出,如果不重写equals方法,equals和==是同一个逻辑。
==用于比较对象的引用是否指向同一个对象实例。重写后的
equals一般用于比较对象的内容是否相等。
对于大多数类来说,比较对象的内容才有意义,因此很多类会重写equals方法,以提供更合适的比较逻辑。String类就是这样一个例子。String类的equals方法被重写,以比较两个字符串对象的内容(即它们包含的字符序列)是否相同,而不是比较它们的引用是否相同。
以下是String类中equals方法的重写实现:
查看代码
public boolean equals(Object anObject) {
if (this == anObject) {
return true;
}
if (anObject instanceof String) {
String anotherString = (String)anObject;
int n = value.length;
if (n == anotherString.value.length) {
char v1[] = value;
char v2[] = anotherString.value;
int i = 0;
while (n-- != 0) {
if (v1[i] != v2[i])
return false;
i++;
}
return true;
}
}
return false;
}在这个实现中,equals方法首先检查传递的对象是否是String的实例。如果不是,则直接返回false。如果是,则将传递的对象转换为String类型,然后比较两个字符串的长度。如果长度不同,则返回false。如果长度相同,则逐个比较字符串中的每个字符。如果所有字符都相同,则返回true;如果发现任何不匹配的字符,则返回false。
这种实现确保了两个字符串对象的内容相同,即使它们是不同的对象实例,equals方法也会返回true。这是字符串比较的标准行为,也是String类设计的一个重要特点。
例如:
String s1 = new String("hello");
String s2 = new String("hello");
String s3 = "hello";
boolean result = s1.equals(s2); // true,因为s1和s2的内容相同在这个例子中,s1和s2是两个不同的对象实例,但它们的内容相同,所以equals方法返回true。同样,s1和s3也是内容相同的字符串,尽管s3是通过字符串常量创建的,equals方法同样返回true。这是因为String类的equals方法比较的是字符串的内容,而不是对象的引用。
intern() 方法
String.intern()方法用于处理字符串的内存分配和字符串常量池(String Pool)的引用。这个方法允许我们请求一个字符串对象的引用,该引用指向字符串常量池中的字符串。如果字符串常量池中已经存在相同的字符串,intern()方法将返回该字符串的引用;如果不存在,它将创建一个新的字符串对象,并将其添加到字符串常量池中,然后返回该字符串的引用。

下面是一个String.intern()方法的示例:
String s1 = new StringBuilder("abc").append("def").toString();
String s2 = s1.intern(); // 如果字符串常量池中没有"abcdef",则返回一个新的字符串对象的引用
String s3 = "abcdef"; // 字符串常量池中的字符串
System.out.println(s1 == s2); // false,因为s1和s2是不同的对象实例
System.out.println(s2 == s3); // true,因为s2和s3都引用字符串常量池中的"abcdef"在这个示例中,s1是通过StringBuilder创建的字符串对象。当调用s1.intern()时,如果字符串常量池中没有"abcdef",则返回一个新的字符串对象的引用。因此,s1和s2是不同的对象实例。然而,s2和s3都引用字符串常量池中的"abcdef",所以它们是相等的。
Java字符串与基本数据类型的转换
在Java中,字符串与基本数据类型之间的转换是常见的操作。这些转换可以通过多种方式实现,包括使用Java的内置方法、手动转换或者使用类型转换工具。
字符串转基本数据类型
转换为int
String str = "123";
int num = Integer.parseInt(str);转换为byte
String str = "123";
byte bnum = Byte.parseByte(str);转换为double
String str = "123.456";
double dnum = Double.parseDouble(str);转换为boolean
String str = "true";
boolean b = Boolean.parseBoolean(str);基本数据类型转字符串
- 使用
Integer.toString()javaint num = 123; String str = Integer.toString(num); - 使用
String.valueOf()javaint num = 123; String str = String.valueOf(num);
在进行字符串转基本数据类型的操作时,如果字符串无法转换为指定类型的数值,将会抛出NumberFormatException。
在进行基本数据类型转字符串的操作时,如果传递的数值为null,将会抛出NullPointerException。
