String是不可变类的原因:
Java把String设成immutable最大的原因应该是效率和安全。
1).不可变对象可以提高String Pool的效率和安全性。如果你知道一个对象是不可变的,那么需要拷贝这个对象的内容时,就不用复制它的本身而只是复制它的地址,复制地址(通常一个指针的大小)需要很小的内存效率也很高。对于同时引用这个“ABC”的其他变量也不会造成影响。
2).不可变对象对于多线程是安全的,因为在多线程同时进行的情况下,一个可变对象的值很可能被其他进程改变,这样会造成不可预期的结果,而使用不可变对象就可以避免这种情况。
具体分析如下:
1. 字符串常量池的需要
字符串常量池(String pool, String intern pool, String保留池) 是Java堆内存中一个特殊的存储区域, 当创建一个String对象时,假如此字符串值已经存在于常量池中,则不会创建一个新的对象,而是引用已经存在的对象。
如下面的代码所示,将会在堆内存中只创建一个实际String对象.
String s1 = "abcd";
String s2 = "abcd";
示意图如下所示:
假若字符串对象允许改变,那么将会导致各种逻辑错误,比如改变一个对象会影响到另一个独立对象. 严格来说,这种常量池的思想,是一种优化手段. |
假若代码如下所示,s1和s2还会指向同一个实际的String对象吗?
String s1= "ab" + "cd";
String s2= "abc" + "d";
也许这个问题违反新手的直觉, 但是考虑到现代编译器会进行常规的优化, 所以他们都会指向常量池中的同一个对象. 或者,你可以用 jd-gui 之类的工具查看一下编译后的class文件.
2. 允许String对象缓存HashCode
Java中String对象的哈希码被频繁地使用, 比如在hashMap 等容器中。
字符串不变性保证了hash码的唯一性,因此可以放心地进行缓存.这也是一种性能优化手段,意味着不必每次都去计算新的哈希码. 更加高效
在String类的定义中有如下代码:
private int hash;//用来缓存HashCode
在字符串创建的时候hashcode就被缓存了,不需要重新计算。这就使得字符串很适合作为Map中的键,字符串的处理速度要快过其它的键对象。这就是HashMap中的键往往都使用字符串。
3. 安全性
String被许多的Java类(库)用来当做参数,例如 网络连接地址URL,文件路径path,还有反射机制所需要的String参数等, 假若String不是固定不变的,将会引起各种安全隐患。
假如有如下的代码:
if (!isSecure(s)) {
throw new SecurityException();
}
// 如果在其他地方可以修改String,那么此处就会引起各种预料不到的问题/错误
causeProblem(s);
}
考虑下面的程序,来具体说明:
1
2
3
4
5
6
7
|
HashSet<String> set = new HashSet<String>();
set.add( new String( "a" ));
set.add( new String( "b" ));
set.add( new String( "c" ));
for (String a: set)
a.value = "a" ;
|
在这个例子中,如果String是可变的,它的值的改变将会违反集合(set)的设计(set中的元素是不重复的)。这个例子只是为简单起见而设计的,实际上字符串类并没有value域。
5. 多线程
由于String的不可变性,可以在多线程中使用,减少了同步
总体来说, String不可变的原因包括 设计考虑,效率优化问题,以及安全性这三大方面.
String类不可变性的好处
1.只有当字符串是不可变的,字符串池才有可能实现。字符串池的实现可以在运行时节约很多heap空间,因为不同的字符串变量都指向池中的同一个字符串。但如果字符串是可变的,那么String interning将不能实现(译者注:String interning是指对不同的字符串仅仅只保存一个,即不会保存多个相同的字符串。),因为这样的话,如果变量改变了它的值,那么其它指向这个值的变量的值也会一起改变。
2.如果字符串是可变的,那么会引起很严重的安全问题。譬如,数据库的用户名、密码都是以字符串的形式传入来获得数据库的连接,或者在socket编程中,主机名和端口都是以字符串的形式传入。因为字符串是不可变的,所以它的值是不可改变的,否则黑客们可以钻到空子,改变字符串指向的对象的值,造成安全漏洞。
3.因为字符串是不可变的,所以是多线程安全的,同一个字符串实例可以被多个线程共享。这样便不用因为线程安全问题而使用同步。字符串自己便是线程安全的。
4.类加载器要用到字符串,不可变性提供了安全性,以便正确的类被加载。譬如你想加载java.sql.Connection类,而这个值被改成了myhacked.Connection,那么会对你的数据库造成不可知的破坏。
5.因为字符串是不可变的,所以在它创建的时候hashcode就被缓存了,不需要重新计算。这就使得字符串很适合作为Map中的键,字符串的处理速度要快过其它的键对象。这就是HashMap中的键往往都使用字符串。
String类中使用字符数组保存字符串private final char value[];,由于有final修饰,所以是不可变的,可以理解为常量,所以线程安全。StringBuilder和StringBuffer都继承自AbstractStringBuilder(抽象)类,也用字符数组保存字符串char[] value;,可变。其中StringBuffer对方法加了同步锁,所以线程安全。
1、String
用于存放字符的数组被声明为final的,因此只能赋值一次,不可更改。这就导致每次对String的操作都会生成新的String对象,不仅效率低下,而且大量浪费有限的内存空间。
String a ="a"; //假设a指向地址0x0001
a ="b";//重新赋值后a指向地址0x0002,但0x0001地址中保存的"a"依旧存在,但已经不再是a所指向的,a 已经指向了其它地址。
因此String的操作都是改变赋值地址而不是改变值操作。对String对象的任何改变都不影响到原对象,相关的任何change操作都会生成新的对象,然后将指针指向新的字符串,所以经常改变内容的字符串最好不要用 String ,因为每次生成对象都会对系统性能产生影响,特别当内存中无引用对象多了以后, JVM 的 GC 就会开始工作,那速度是一定会相当慢的。所以不要使用String类的"+"来进行频繁的拼接,那样的性能极差,因为会默认生成新的字符串
而如果是使用 StringBuffer 类则结果就不一样了,每次结果都会对 StringBuffer 对象本身进行操作,而不是生成新的对象,再改变对象引用。所以在一般情况下我们推荐使用 StringBuffer ,特别是字符串对象经常改变的情况下。而在某些特别情况下, String 对象的字符串拼接其实是被 JVM 解释成了 StringBuffer 对象的拼接,所以这些时候 String 对象的速度并不会比 StringBuffer 对象慢,而特别是以下的字符串对象生成中, String 效率是远要比 StringBuffer 快的:
String S1 = “This is only a” + “ simple” + “ test”;
StringBuffer Sb = new StringBuilder(“This is only a”).append(“ simple”).append(“ test”);
你会很惊讶的发现,生成 String S1 对象的速度简直太快了,而这个时候 StringBuffer 居然速度上根本一点都不占优势。其实这是 JVM 的一个把戏,在 JVM 眼里,这个
String S1 = “This is only a” + “ simple” + “test”; 其实就是:
String S1 = “This is only a simple test”; 所以当然不需要太多的时间了。但大家这里要注意的是,如果你的字符串是来自另外的 String 对象的话,速度就没那么快了,譬如:
String S2 = “This is only a”;
String S3 = “ simple”;
String S4 = “ test”;
String S1 = S2 +S3 + S4;
String类实现了public interface Comparable<T>,而Comparable接口里有唯一的方法:public int compareTo(T o)。所以,String类还有另一个字符串比较方法:compareTo()
-----------------public int compareTo(String anotherString)---------------
compareTo()可实现比较两个字符串的大小,源码如下:
- public int compareTo(String anotherString) {
- int len1 = count;
- int len2 = anotherString.count;
- int n = Math.min(len1, len2);
- char v1[] = value;
- char v2[] = anotherString.value;
- int i = offset;
- int j = anotherString.offset;
- if (i == j) {
- int k = i;
- int lim = n + i;
- while (k < lim) {
- char c1 = v1[k];
- char c2 = v2[k];
- if (c1 != c2) {
- return c1 - c2;
- }
- k++;
- }
- } else {
- while (n-- != 0) {
- char c1 = v1[i++];
- char c2 = v2[j++];
- if (c1 != c2) {
- return c1 - c2;
- }
- }
- }
- return len1 - len2;
- }
compareTo是怎么实现的呢?
首先,会对两个字符串左对齐,然后从左到右一次比较,如果相同,继续,如果不同,则计算不同的两个字符的ASCII值的差,返回就行了。与后面的其他字符没关系。
举个例子:
- package com.xtfggef.string;
- /**
- * compareTo()测试
- * @author 二青
- *
- */
- public class CompareToTest {
- public static void main(String[] args) {
- String s = "hallo";
- String s2 = "ha";
- String s3 = "haeeo";
- int a = s.compareTo(s2);
- System.out.println("a:"+a);
- int b = s.compareTo(s3);
- System.out.println("b:"+b);
- int c = s2.compareTo(s3);
- System.out.println("c:"+c);
- }
- }
程序输出:
a:3
b:7
c:-3
s和s2相比,前两个相同,如果是这种情况,则直接返回length1-length2
s和s3相比,前两个相同,不用管,直接用第三个字符的ASCII码做差就行了。所以'l'-'a'=7
此处网友“han
s2和s3相比,同第一种情况一样,只是length1比length2小,因此值为负数。
2、StringBuffer
StringBuffer是一个从JDK1.0开始就存在的类,它就像String类一样,不过其内部成员是可以修改的。
StringBuffer是可变类,和线程安全的字符串操作类,任何对它指向的字符串的操作都不会产生新的对象。每个StringBuffer对象都有一定的缓冲区容量,当字符串大小没有超过容量时,不会分配新的容量,当字符串大小超过容量时,会自动增加容量。
StringBuffer 上的主要操作是append 和 insert 方法,可重载这些方法,以接受任意类型的数据。每个方法都能有效地将给定的数据转换成字符串,然后将该字符串的字符追加或插入到字符串缓冲区中。append 方法始终将这些字符添加到缓冲区的末端;而 insert 方法则在指定的点添加字符。例如,如果 z 引用一个当前内容是“start”的字符串缓冲区对象,则此方法调用 z.append("le") 会使字符串缓冲区包含“startle”,而z.insert(4, "le") 将更改字符串缓冲区,使之包含“starlet”。
3、StringBuilder
StringBuilder是一个可变的字符序列,是JDK5.0新增的。此类提供一个与StringBuffer 兼容的 API,但不保证同步。该类被设计用作StringBuffer 的一个简易替换,用在字符串缓冲区被单个线程使用的时候,即单线程,所以StringBuilder线程不安全。
它和StringBuffer提供相同的接口。
相同点:它们继承相同的父类:Abstract StringBuilder
它们实现相同的接口:java.io.Serializable, CharSequence
它们基本上对外提供相同的方法
不同点:StringBuffer类是线程安全的,所有StringBuffer提供的public方法基本上都是synchronized。StringBuilder类不是线程安全的,它的方法没有被synchronized锁修饰。相比而言,StringBuilder类效率较高
4、总结
如果要操作少量的数据用 String
单线程操作字符串缓冲区下操作大量数据用StringBuilder
多线程操作字符串缓冲区下操作大量数据用StringBuffer
如果一个字符串变量是在方法里面定义的,这种情况可能只有一个线程访问它,不存在不安全的因素,则用StringBuilder,如果要在类里面定义成员变量,并且这个类的实例对象会在多线程环境下使用那么最好使用StringBuffer。
String是不可变的对象, 因此在每次对String 类型进行改变的时候,都会生成一个新的 String 对象,然后将指针指向新的 String 对象,所以经常改变内容的字符串最好不要用 String ,因为每次生成对象都会对系统性能产生影响,特别当内存中无引用对象多了以后, JVM 的 GC 就会开始工作,性能就会降低。
不要使用String类的"+"来进行频繁的拼接,那样的性能极差,因为会默认生成新的字符串,应该使用StringBuffer或StringBuilder类,这在Java的优化上是一条比较重要的原则。
速度:StringBuilder>StringBuffer>String(大部分情况下)
StringBuilder非线程安全、StringBuffer线程安全
http://www.cnblogs.com/xudong-bupt/p/3961159.html
1.可变与不可变
String类中使用字符数组保存字符串,如下就是,因为有“final”修饰符,所以可以知道string对象是不可变的。
private final char value[];
StringBuilder与StringBuffer都继承自AbstractStringBuilder类,在AbstractStringBuilder中也是使用字符数组保存字符串,如下就是,可知这两种对象都是可变的。
char[] value;
2.是否多线程安全
String中的对象是不可变的,也就可以理解为常量,显然线程安全。
AbstractStringBuilder是StringBuilder与StringBuffer的公共父类,定义了一些字符串的基本操作,如expandCapacity、append、insert、indexOf等公共方法。
StringBuffer对方法加了同步锁或者对调用的方法加了同步锁,所以是线程安全的。看如下源码:
1 public synchronized StringBuffer reverse() { 2 super.reverse(); 3 return this; 4 } 5 6 public int indexOf(String str) { 7 return indexOf(str, 0); //存在 public synchronized int indexOf(String str, int fromIndex) 方法 8 }
StringBuilder并没有对方法进行加同步锁,所以是非线程安全的。
3.StringBuilder与StringBuffer共同点
StringBuilder与StringBuffer有公共父类AbstractStringBuilder(抽象类)。
抽象类与接口的其中一个区别是:抽象类中可以定义一些子类的公共方法,子类只需要增加新的功能,不需要重复写已经存在的方法;而接口中只是对方法的申明和常量的定义。
StringBuilder、StringBuffer的方法都会调用AbstractStringBuilder中的公共方法,如super.append(...)。只是StringBuffer会在方法上加synchronized关键字,进行同步。
最后,如果程序不是多线程的,那么使用StringBuilder效率高于StringBuffer。
本文用于分析当创建StringBuffer对象后,调用append方法时,StringBuffer内部是如何扩容的
1、创建StringBuffer对象,并调用append方法,这是我们开发时经常做的
- StringBuffer sb = new StringBuffer("abc");//原来数据的长度:3个字符
- sb.append("defghijklmnopqrst");//新增加数据的长度:17个字符
2、查看append方法的源码
- public synchronized StringBuffer append(String str) {
- super.append(str);
- return this;
- }
上面的代码可以知道它调用的是父类AbstractStringBuilder的append方法,接着进入父类的append方法
3、分析父类的append方法
- //str-->"defghijklmnopqrst"
- public AbstractStringBuilder append(String str) {
- //新增加的数据为null则直接返回字符串"null"
- if (str == null) str = "null";
- int len = str.length();//新增加数据的长度为:17
- ensureCapacityInternal(count + len);//扩容:count+len=3 + 17
- str.getChars(0, len, value, count);//将原来的数据复制到扩容后的数组中
- count += len;
- return this;
- }
从上面可以看出append方法调用了ensureCapacityInternal方法和str.getChars(0, len, value, count);,这里我们用3.1来分析前者(扩容操作),3.2来分析后者(将数据复制到扩容后的数组中)
=================================================开始扩容=================================================
3.1、ensureCapacityInternal()
- //int minimumCapacity = 20
- private void ensureCapacityInternal(int minimumCapacity) {
- // overflow-conscious code:判断是否需要扩容,也就是说原来的capacity是否足够大
- if (minimumCapacity - value.length > 0) //20-19=1,1>0
- expandCapacity(minimumCapacity);
- }
上面的代码又调用了expandCapacity方法,我们用3.1下面的子标题来进行一步步分析
3.1.1 expandCapacity
- void expandCapacity(int minimumCapacity) { //int minimumCapacity=20
- int newCapacity = value.length * 2 + 2; //新的容量capacity=原来的长度*2+2
- //扩容后的容量-字符串实际长度<0(就是说如果扩容后还装不下),
- //则使用字符串实际长度作为StringBuffer的capacity
- if (newCapacity - minimumCapacity < 0)
- newCapacity = minimumCapacity;
- if (newCapacity < 0) {
- if (minimumCapacity < 0) // overflow
- throw new OutOfMemoryError();
- newCapacity = Integer.MAX_VALUE;
- }
- value = Arrays.copyOf(value, newCapacity);
- }
3.1.1.1 Arrays.copyOf(value, newCapacity)
- //copyOf方法用于创建一个新数组,新数组的长度是扩容后的长度,并将原来的值复制到新的数组中
- //这里需要注意,虽然数组是新的,但是StringBuffer还是原来的StringBuffer
- //original:StringBuffer中原来的值,也就是'abc'
- //newLength:新的长度,19*2+2=40
- public static char[] copyOf(char[] original, int newLength) {
- char[] copy = new char[newLength];
- System.arraycopy(original, 0, copy, 0,
- Math.min(original.length, newLength));
- return copy;
- }
=================================================扩容结束=================================================
3.2 str.getChars(0, len, value, count);
将字符从str字符串复制到目标数组value中,这里就是把str的所有值复制到value数组的最后面
- //int srcBegin:字符串str中要复制的第一个字符的索引
- //int srcEnd:字符串str中要复制的最后一个字符之后的索引
- //char dst[]:目标数组
- //int dstBegin:目标数组中的起始偏移量
- public void getChars(int srcBegin, int srcEnd, char dst[], int dstBegin) {
- if (srcBegin < 0) {
- throw new StringIndexOutOfBoundsException(srcBegin);
- }
- if (srcEnd > value.length) {
- throw new StringIndexOutOfBoundsException(srcEnd);
- }
- if (srcBegin > srcEnd) {
- throw new StringIndexOutOfBoundsException(srcEnd - srcBegin);
- }
- System.arraycopy(value, srcBegin, dst, dstBegin, srcEnd - srcBegin);