Java字符串简介
字符串的表示
public final class String
implements java.io.Serializable, Comparable<String>, CharSequence
Java中的字符串是表示一个字符序列的对象。在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
。