一、不可变字符串(String 类)
1. 不可变特性
String 类在 Java 中代表不可变字符串,这意味着一旦一个 String 对象被创建,它的值就不能在原内存地址上被修改。例如,当我们执行以下操作:
String str = "Hello";
str = str + " World";
看起来好像是对原来的字符串"Hello"
进行了修改,但实际上并非如此。在 Java 中,字符串的拼接操作会导致创建一个新的 String 对象来存储拼接后的结果"Hello World"
,而原来的"Hello"
字符串对象依然存在于内存中,只是str
这个引用变量现在指向了新创建的字符串对象。这就是所谓的不可变不是指的值绝对不可变,而是不能在原地址改变,只有再申请一个新的地址来存新的数据。
2. 内存管理与常量池
在 Java 中,对于字符串的赋值操作,会先判断常量池有没有这个值。如果常量池中已经存在相同的字符串值,那么就不会再申请一个新的内存空间来存储该字符串,而是让新的引用变量一起指向那个已存在于常量池中的值。例如:
String str1 = "Hello";
String str2 = "Hello";
System.out.println(str1 == str2);
在上述代码中,str1
和str2
都会指向常量池中的同一个"Hello"
字符串对象,所以==
比较的结果为true
,因为==
运算符用于判断两个引用是否指向同一个对象(即比较的是对象的引用地址)。
另外,当系统判断一块内存地址不再需要时,例如某个字符串对象没有任何引用指向它了,Java 会自动进行垃圾回收,释放该内存空间以便其他程序使用。
3. equals () 方法与 == 的区
在 Java 中,两个非基本类型比较时,==
比较的是它们的引用是否指向相同的对象(即对象的引用地址)。而对于 String 类,它重写了从 Object 类继承来的 equals () 方法,使其不再像默认的==
那样只比较引用地址,而是重点关注对象内部所包含的数据内容是否相同。例如:
String str3 = new String("Hello");
String str4 = "Hello";
System.out.println(str3 == str4);
System.out.println(str3.equals(str4));
在上述代码中,str3
是通过new
关键字创建的字符串对象,即使它的值和str4
一样,但它们的地址是不一样的(因为new
的对象,就算值一样,地址也是不一样的,不在常量池中),所以str3 == str4
的结果为false
。然而,str3.equals(str4)
的结果为true
,因为 equals () 方法在字符串中被重写后只比较值。
需要注意的是,当重写 equals () 方法时,通常还需要重写 hashCode () 方法,这是因为在一些数据结构如 HashMap 的使用中,需要这两个方法配合工作。如果只重写 equals () 方法而不重写 hashCode () 方法,可能会导致在 HashMap 中出现一些意想不到的问题,比如无法正确获取或存储对象等。
4. 空串与 null 串
在 Java 中,空串是指一个字符串对象,其长度为 0,例如""
就是一个空串。而 null 串并不是一个真正意义上的字符串对象,而是表示一个字符串引用变量没有指向任何字符串对象,即它的值为null
。在实际编程中,需要对这两种情况进行准确的区分和处理,以避免出现空指针异常等问题。
5. char 类型编码
如果想看 char 类型的编码,可以直接用 int 来看。因为在 Java 中,char 类型本质上是一个无符号的 16 位整数,它可以存储 Unicode 字符。每个 Unicode 字符都对应一个唯一的整数值,所以通过将 char 类型转换为 int 类型,就可以查看其对应的编码值。例如:
char ch = 'A';
int code = (int) ch;
System.out.println(code);
在上述代码中,变量ch
存储的字符'A'
,将其转换为 int 类型后,就可以得到它的 Unicode 编码值 65。
二、StringBuilder 类与 StringBuffer 类
1. 拼接速度对比
在字符串拼接操作方面,StringBuilder 和 StringBuffer 远远快于 String 类。这是因为 String 类每次拼接都会创建一个新的字符串对象,而 StringBuilder 和 StringBuffer 则是在原对象的基础上进行动态修改,避免了大量创建新对象的开销。例如,当我们需要拼接一个较长的字符串时:
String str5 = "";
for (int i = 0; i < 1000; i++) {str5 = str5 + i;
}StringBuilder sb = new StringBuilder();
for (int i = 0; i < 1000; i++) {sb.append(i);
}StringBuffer sf = new StringBuffer();
for (int i = 0; i < 1000; i++) {sf.append(i);
}
在上述代码中,使用 String 类进行拼接时,每次循环都会创建一个新的字符串对象,导致性能较差。而使用 StringBuilder 和 StringBuffer 时,它们会在原对象上直接进行修改,拼接速度要快得多。
2. 线程安全性差异
StringBuilder 多线程并发操作不安全,而 StringBuffer 多线程并发操作下是安全的。这是因为 StringBuffer 里面加了锁机制,在多线程环境下,当多个线程同时访问和修改 StringBuffer 对象时,锁机制会确保每次只有一个线程能够对其进行操作,从而避免了数据不一致的问题。然而,这种锁机制也导致了 StringBuffer 的速度稍微慢一点点,因为加锁和解锁操作会带来一定的性能开销。
3. 内存消耗与类型确定性
在 Java 中,一个变量消耗一个内存页,这是因为变量不确定类型是否一样。但是用数组可以减少内存的消耗,这是因为数组里的量是确定的,都是一种类型。不过,对于弱类型语言(如 Python、PHP、JS)来说,情况有所不同。弱类型语言的数组可以存各种数据类型,虽然它们的底层可能也是基于 C 等语言实现的,但由于其可以存储多种类型的数据,在内存管理上相对复杂,所以弱语言的内存消耗会很大,速度会很慢。