本文章还是得提到String Immutable 特性。这样才有利于String 、StringBuffer 、StringBuilder 特点的比较。
以下基于 java for windows
java version “1.8.0_171”
Java™ SE Runtime Environment (build 1.8.0_171-b11)
Java HotSpot™ 64-Bit Server VM (build 25.171-b11, mixed mode)图中示例代码注释只是为了展示、对比方便,所以放在代码尾部
String
String Immutable 特性
以下为 java.lang.String 属性字段
/** The value is used for character storage. */
private final char value[];
/** Cache the hash code for the string */
private int hash; // Default to 0
/** use serialVersionUID from JDK 1.0.2 for interoperability */
private static final long serialVersionUID = -6849794470754667710L;
String 类 implements java.io.Serializable, Comparable< String >, CharSequence。类是用final修饰的。且其属性字段为value未提供set修改其对象内部值。首先要了解一点
-
每次新建String类型对象 其值(方法区 String Pool 、堆区) 都是不可变的(immutable)。最直观代码如下
// 这种行为是在 String pool 创建对象,其引用存在栈中 String str1 = "hello world"; // 使用此种方法重新创建String对象时 java会首先在String pool中检查是否存在 hello world 存在即返回其引用 所以 str1、str2 指向同一对象 String str2 = "hello world"; System.out.println("result:" + (str1 == str2)); // result:true
// 此种方式创建的String对象位于堆内 每次创建都指向一个新的对象 各自独立的堆空间 String str3 = new String("hello world"); String str4 = new String("hello world"); System.out.println("result2:" + (str3 == str4));// result2:false // 在堆中的String不同于在String pool 的String System.out.println("result3:" + (str2 == str4)); // result3:false
// 此操作在Pool中重新创建String 其引用改变 但原 hello world 未变 str1 = "hello Man"; System.out.println("change1: " + str1);// change1: hello Man System.out.println("change1: " + str2);// change1: hello world
-
String immutable特性带来很多便利
- 不可变对象可以提高String Pool的效率和安全性。如果你知道一个对象是不可变的,那么需要拷贝这个对象的内容时,就不用复制它的本身而只是复制它的地址,复制地址(通常一个指针的大小)需要很小的内存效率也很高。对于同时引用这个“hello world” 的其他变量也不会造成影响。节省空间,加快效率。
- 不可变对象对于多线程是安全的,因为在多线程同时进行的情况下,一个可变对象的值很可能被其他进程改变,这样会造成不可预期的结果,而使用不可变对象就可以避免这种情况。(最直观的例子就是 String被广泛的作为方法的参数,比如打开网络连接时传递主机名和端口号作为参数,传递数据库URL用于连接数据库,也可以在打开文件时传递文件名给Java的I/O class。如果String不是immutable,会带来严重的安全问题,假设一个人对其授权访问的文件,改变了文件名并重新获了文件访问权限,但是因为String是immutable,你就不需要担心这种情况。这也可以解释为啥String 是final的,final string保证了任何人都不能重写String而改变String的行为。)
- 不可变性,导致其是线程安全的。
- 支持hash映射和缓存(此点在之后的文章中会提到)
反射修改了String的值?
在 JAVA不可变类(immutable)机制与String的不可变性 中 看到博主的一段说明:
发现String的值已经发生了改变。也就是说,通过反射是可以修改所谓的“不可变”对象的
特以下代码验证此点
String str1 = "hello world";
String str2 = "hello world";
System.out.println("result1:" + (str1 == str2)); // result1:true
//获取String类中的value字段
Field valueFieldOfString = String.class.getDeclaredField("value");
//改变value属性的访问权限
valueFieldOfString.setAccessible(true);
//获取s对象上的value属性的值
char[] value = (char[]) valueFieldOfString.get(str1);
//改变value所引用的数组中的第5个字符
value[5] = 'X';
System.out.println("str1 changed = " + str1); // str1 changed = helloXworld
System.out.println("str2 = " +str2);// str2 = helloXworld
结果显而易见:改了里面的值,并且执行其值的 str2 值也被修改
但是,如果将 str1 使用final修饰,输出的结果将是:
str1 changed = hello world
str2 = helloXworld
也就是说 str1 的值并未改变
为什么会出现这种情况?请见下文。
这是我的java类 .java(str1未被final修饰前)
import java.lang.reflect.Field;
/**
* @Description:
* @Author: amarone
*/
public class Test {
public static void main(String[] args) throws Exception{
String str1 = "hello world";
String str2 = "hello world";
System.out.println("result1:" + (str1 == str2));
Field valueFieldOfString = String.class.getDeclaredField("value");
valueFieldOfString.setAccessible(true);
char[] value = (char[]) valueFieldOfString.get(str1);
value[5] = 'X';
System.out.println("str1 changed = " + str1); //str1 changed = helloXworld
System.out.println("str2 = " +str2); // str2 = helloXworld
}
}
这是经过javac 编译.java 后的 .class 。 通过 jd-gui.exe 反编译 (str1未被final修饰前)
import java.io.PrintStream;
import java.lang.reflect.Field;
public class Test
{
public static void main(String[] paramArrayOfString)
throws Exception
{
String str1 = "hello world";
String str2 = "hello world";
System.out.println("result1:" + (str1 == str2));
Field localField = String.class.getDeclaredField("value");
localField.setAccessible(true);
char[] arrayOfChar = (char[])localField.get(str1);
arrayOfChar[5] = 'X';
System.out.println("str1 changed = " + str1);
System.out.println("str2 = " + str2);
}
}
这是经过javac 编译.java 后的 .class 。 通过 jd-gui.exe 反编译 (str1被final修饰)
import java.io.PrintStream;
import java.lang.reflect.Field;
public class Test
{
public static void main(String[] paramArrayOfString)
throws Exception
{
String str = "hello world";
System.out.println("result1:" + ("hello world" == str));
Field localField = String.class.getDeclaredField("value");
localField.setAccessible(true);
char[] arrayOfChar = (char[])localField.get("hello world");
arrayOfChar[5] = 'X';
System.out.println("str1 changed = hello world");
System.out.println("str2 = " + str);
}
}
可以注意到: 在加上final 修饰之后,java文件被编译为class文件时
(这也是我上篇文章《(四)Final 关键字》 中提到部分有涉及的 编译期优化 )
System.out.println("str1 changed = " + str1); ==> System.out.println("str1 changed = hello world");
输出的不是引用的变量而是一个字符串!也就是说在编译器就已经优化了,变量再怎么改都没用。final修饰Stirng是为了防止继承String暴露Stirng内部属性。
为什么要有StringBuffer或者StringBuilder?
Stirng 字符串拼接时
import java.io.PrintStream;
public class Test
{
public static void main(String[] paramArrayOfString)
throws Exception
{
String str1 = "aaa";
String str2 = str1 + "b";
System.out.println(str1);
System.out.println(str2);
System.out.println(str1 + str2);
}
}
str1、str2、"b" 都是单独的对象,如果存在大量的字符串拼接,那么就会大量创建对象,然后你的新生代(幸存者区)很快就满了80% 然后就触发GC了。或者就直接挂了。
为了应对大量拼接字符串拼接,java提供了 StringBuffer 类。
public final class StringBuffer
extends AbstractStringBuilder
implements java.io.Serializable, CharSequence
{
**
* A cache of the last value returned by toString. Cleared
* whenever the StringBuffer is modified.
*/
private transient char[] toStringCache;
/** use serialVersionUID from JDK 1.0.2 for interoperability */
static final long serialVersionUID = 3388685877147921107L;
········
}
StringBuffer 继承于 AbstractStringBuilder
abstract class AbstractStringBuilder implements Appendable, CharSequence {
/**
* The value is used for character storage.
*/
char[] value;
/**
* The count is the number of characters used.
*/
int count;
}
StringBuffer 提供了append() 方法用以在指定位置添加字符串
@Override
public synchronized StringBuffer append(Object obj) {
toStringCache = null;
super.append(String.valueOf(obj));
return this;
}
@Override
public synchronized StringBuffer append(String str) {
toStringCache = null;
super.append(str);
return this;
}
可以看到几乎所有对字符串操作的方法全部添加了synchronized关键字。保证了线程安全,但是需要付出部分性能代价。