String:字符串常量、线程安全
StringBuffer:字符串变量、线程安全
StringBuilder:字符串变量、线程不安全
CharSequence是字符序列,String,StringBuffer和StringBuilder都实现了CharSequence接口,本质上都是通过字符数组实现的。
String 类
String类实现了Serializable, Comparable, CharSequence接口,被final所修饰,也就是说String对象是不可变类,是线程安全的。
字符串可以通过两种方式进行初始化:字面常量和String对象。
字面常量:
String a = "java"; String b = "java"; String c = "ja" + "va"; a==b // true; a==c // true;
变量a、b和c都指向常量池的 “java” 字符串,表达式 “ja” + “va” 在编译期间会把结果值”java”直接赋值给c。
String对象:
public class StringTest { public static void main(String[] args) { String a = "java"; String c = new String("java"); a==c // false } }
对于 String c = new String("java") 这行代码,new 指令会在java堆上为String对象申请内存;然后尝试从常量池中获取”java”字符串,如果常量池中不存在,则在常量池中新建”java”字符串,并返回;最后,调用构造方法,初始化String对象。
其中String对象中使用char数组存储字符串,变量a指向常量池的”java”字符串,变量c指向Java堆的String对象,且该对象的char数组指向常量池的”java”字符串,所以很显然 a != c,如下图所示:
通过 “字面量 + String对象” 进行赋值会发生什么?
public class StringTest { public static void main(String[] args) { String a = "hello "; String b = "world"; String c = a + b; String d = "hello world"; c==d // false } }
字符串变量的连接动作,在编译阶段会被转化成StringBuilder的append操作。
对于 String c = a + b 这行代码,先在Java堆上为StringBuilder对象申请内存;调用构造方法,初始化StringBuilder对象;调用append方法,添加a和b字符串;调用toString方法,生成String对象。变量c最终指向Java堆上新建String对象,变量d指向常量池的”hello world”字符串,所以 c != d。
特殊情况:当final修饰的变量发生连接动作时,虚拟机会进行优化,将表达式结果直接赋值给目标变量.
public class StringTest { public static void main(String[] args) { final String a = "hello "; final String b = "world"; String c = a + b; String d = "hello world"; c==d // true } }
String 类源码
1、成员变量
String类中包含一个不可变的char数组用来存放字符串,一个int型的变量hash用来存放计算后的哈希值。
//用于存储字符串 private final char value[]; //缓存String的hash值 private int hash; // Default to 0 private static final long serialVersionUID = -6849794470754667710L;
2、构造函数
//不含参数的构造函数,一般没什么用,因为value是不可变量 public String() { this.value = new char[0]; } //参数为String类型 public String(String original) { this.value = original.value; this.hash = original.hash; } //参数为char数组,使用java.utils包中的Arrays类复制 public String(char value[]) { this.value = Arrays.copyOf(value, value.length); } //从bytes数组中的offset位置开始,将长度为length的字节,以charsetName格式编码,拷贝到value public String(byte bytes[], int offset, int length, String charsetName) throws UnsupportedEncodingException { if (charsetName == null) throw new NullPointerException("charsetName"); checkBounds(bytes, offset, length); this.value = StringCoding.decode(charsetName, bytes, offset, length); } //调用public String(byte bytes[], int offset, int length, String charsetName)构造函数 public String(byte bytes[], String charsetName) throws UnsupportedEncodingException { this(bytes, 0, bytes.length, charsetName); }
3、常用方法
String设计成不可变类的原因
(1)字符串常量池的需要。当创建一个String对象时,假如此字符串值已经存在于常量池中,则不会创建一个新的对象,而是引用已经存在的对象。
(2)HashCode 的需要。Java中String对象的哈希码被频繁地使用, 比如在hashMap 等容器中。
字符串不变性保证了hash码的唯一性,因此可以放心地进行缓存.这也是一种性能优化手段,意味着不必每次都去计算新的哈希码.
(3)String被许多的Java类(库)用来当做参数,例如 网络连接地址URL,文件路径path,还有反射机制所需要的String参数等, 假若String不是固定不变的,将会引起各种安全隐患。
(4)因为字符串是不可变的,所以是多线程安全的,同一个字符串实例可以被多个线程共享。这样便不用因为线程安全问题而使用同步。字符串自己便是线程安全的。
要实现一个不可变类:
既然不可变类有这么多优势,那么我们借鉴String类的设计,自己实现一个不可变类。
不可变类的设计通常要遵循以下几个原则:
将类声明为final,所以它不能被继承。 将所有的成员声明为私有的,这样就不允许直接访问这些成员。 对变量不要提供setter方法。 将所有可变的成员声明为final,这样只能对它们赋值一次。 通过构造器初始化所有成员,进行深拷贝(deep copy)。 在getter方法中,不要直接返回对象本身,而是克隆对象,并返回对象的拷贝。
对于下面的代码:
String s = "abcd";
s = s+1;
System.out.print(s); // result : abcd1
首先创建对象s,赋予一个abcd,然后再创建一个新的对象用来执行第二行代码,之前对象s并没有变化。
再比如:
String a = "a"; //假设a指向地址0x0001
a = "b"; //重新赋值后a指向地址0x0002,但0x0001地址中保存的"a"依旧存在,但已经不再是a所指向的。
但是,对于:
String str = “This is only a” + “ simple” + “test”;
就相当于 String str = “This is only a simple test”;
toCharArray()方法
String s = " a b "; char[] arr = s.toCharArray(); System.out.println(arr.length); // output: 6 System.out.println(Arrays.toString(arr)); //output: [ , a, , b, , ]
StringBuffer
StringBuffer对象都有一定的缓冲区容量,当字符串大小没有超过容量时,不会分配新的容量,当字符串大小超过容量时,会自动增加容量 。