String、StringBuilder、StringBuffer

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不是固定不变的,将会引起各种安全隐患。
假如有如下的代码:

boolean connect(string s){
if (!isSecure(s)) {
throw new SecurityException();
}
// 如果在其他地方可以修改String,那么此处就会引起各种预料不到的问题/错误
causeProblem(s);
}

4. 有利于其它对象的使用

考虑下面的程序,来具体说明:

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修饰,所以是不可变的,可以理解为常量,所以线程安全。StringBuilderStringBuffer都继承自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()可实现比较两个字符串的大小,源码如下:

[java]  view plain copy
  1. public int compareTo(String anotherString) {  
  2.     int len1 = count;  
  3.     int len2 = anotherString.count;  
  4.     int n = Math.min(len1, len2);  
  5.     char v1[] = value;  
  6.     char v2[] = anotherString.value;  
  7.     int i = offset;  
  8.     int j = anotherString.offset;  
  9.   
  10.     if (i == j) {  
  11.         int k = i;  
  12.         int lim = n + i;  
  13.         while (k < lim) {  
  14.         char c1 = v1[k];  
  15.         char c2 = v2[k];  
  16.         if (c1 != c2) {  
  17.             return c1 - c2;  
  18.         }  
  19.         k++;  
  20.         }  
  21.     } else {  
  22.         while (n-- != 0) {  
  23.         char c1 = v1[i++];  
  24.         char c2 = v2[j++];  
  25.         if (c1 != c2) {  
  26.             return c1 - c2;  
  27.         }  
  28.         }  
  29.     }  
  30.     return len1 - len2;  
  31.     }  

compareTo是怎么实现的呢?

首先,会对两个字符串左对齐,然后从左到右一次比较,如果相同,继续,如果不同,则计算不同的两个字符的ASCII值的差,返回就行了。与后面的其他字符没关系。

举个例子:

[java]  view plain copy
  1. package com.xtfggef.string;  
  2.   
  3. /** 
  4.  * compareTo()测试 
  5.  * @author 二青 
  6.  * 
  7.  */  
  8. public class CompareToTest {  
  9.   
  10.     public static void main(String[] args) {  
  11.         String s = "hallo";  
  12.         String s2 = "ha";  
  13.         String s3 = "haeeo";  
  14.         int a = s.compareTo(s2);  
  15.         System.out.println("a:"+a);  
  16.         int b = s.compareTo(s3);  
  17.         System.out.println("b:"+b);  
  18.         int c = s2.compareTo(s3);  
  19.         System.out.println("c:"+c);  
  20.     }  
  21. }  

程序输出:

a:3
b:7
c:-3
s和s2相比,前两个相同,如果是这种情况,则直接返回length1-length2

s和s3相比,前两个相同,不用管,直接用第三个字符的ASCII码做差就行了。所以'l'-'a'=7

此处网友“handsomeman_wei”问我源码中的c1-c2理解不了,就是上面红字部分的解释。

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方法,这是我们开发时经常做的

[java]  view plain copy
  1. StringBuffer sb = new StringBuffer("abc");//原来数据的长度:3个字符  
  2. sb.append("defghijklmnopqrst");//新增加数据的长度:17个字符  

2、查看append方法的源码

[java]  view plain copy
  1. public synchronized StringBuffer append(String str) {  
  2.         super.append(str);  
  3.         return this;  
  4.     }  

上面的代码可以知道它调用的是父类AbstractStringBuilder的append方法,接着进入父类的append方法


3、分析父类的append方法

[java]  view plain copy
  1. //str-->"defghijklmnopqrst"  
  2. public AbstractStringBuilder append(String str) {  
  3.     //新增加的数据为null则直接返回字符串"null"  
  4.         if (str == null) str = "null";  
  5.         int len = str.length();//新增加数据的长度为:17  
  6.         ensureCapacityInternal(count + len);//扩容:count+len=3 + 17  
  7.         str.getChars(0, len, value, count);//将原来的数据复制到扩容后的数组中  
  8.         count += len;  
  9.         return this;  
  10.     }  

从上面可以看出append方法调用了ensureCapacityInternal方法和str.getChars(0, len, value, count);,这里我们用3.1来分析前者(扩容操作),3.2来分析后者(将数据复制到扩容后的数组中)


=================================================开始扩容=================================================

3.1、ensureCapacityInternal()

[java]  view plain copy
  1. //int minimumCapacity = 20  
  2. private void ensureCapacityInternal(int minimumCapacity) {  
  3.         // overflow-conscious code:判断是否需要扩容,也就是说原来的capacity是否足够大  
  4.         if (minimumCapacity - value.length > 0//20-19=1,1>0  
  5.             expandCapacity(minimumCapacity);  
  6.     }  

上面的代码又调用了expandCapacity方法,我们用3.1下面的子标题来进行一步步分析

3.1.1 expandCapacity

[java]  view plain copy
  1. void expandCapacity(int minimumCapacity) { //int minimumCapacity=20  
  2.         int newCapacity = value.length * 2 + 2//新的容量capacity=原来的长度*2+2  
  3.     //扩容后的容量-字符串实际长度<0(就是说如果扩容后还装不下),  
  4.     //则使用字符串实际长度作为StringBuffer的capacity  
  5.     if (newCapacity - minimumCapacity < 0)   
  6.             newCapacity = minimumCapacity;  
  7.         if (newCapacity < 0) {  
  8.             if (minimumCapacity < 0// overflow  
  9.                 throw new OutOfMemoryError();  
  10.             newCapacity = Integer.MAX_VALUE;  
  11.         }  
  12.         value = Arrays.copyOf(value, newCapacity);  
  13.     }  

3.1.1.1 Arrays.copyOf(value, newCapacity)

[java]  view plain copy
  1. //copyOf方法用于创建一个新数组,新数组的长度是扩容后的长度,并将原来的值复制到新的数组中  
  2. //这里需要注意,虽然数组是新的,但是StringBuffer还是原来的StringBuffer  
  3. //original:StringBuffer中原来的值,也就是'abc'  
  4. //newLength:新的长度,19*2+2=40  
  5. public static char[] copyOf(char[] original, int newLength) {  
  6.         char[] copy = new char[newLength];  
  7.         System.arraycopy(original, 0, copy, 0,  
  8.                          Math.min(original.length, newLength));  
  9.         return copy;  
  10.     }  

=================================================扩容结束=================================================



3.2 str.getChars(0, len, value, count);

将字符从str字符串复制到目标数组value中,这里就是把str的所有值复制到value数组的最后面

[java]  view plain copy
  1. //int srcBegin:字符串str中要复制的第一个字符的索引  
  2. //int srcEnd:字符串str中要复制的最后一个字符之后的索引  
  3. //char dst[]:目标数组  
  4. //int dstBegin:目标数组中的起始偏移量  
  5. public void getChars(int srcBegin, int srcEnd, char dst[], int dstBegin) {  
  6.         if (srcBegin < 0) {  
  7.             throw new StringIndexOutOfBoundsException(srcBegin);  
  8.         }  
  9.         if (srcEnd > value.length) {  
  10.             throw new StringIndexOutOfBoundsException(srcEnd);  
  11.         }  
  12.         if (srcBegin > srcEnd) {  
  13.             throw new StringIndexOutOfBoundsException(srcEnd - srcBegin);  
  14.         }  
  15.         System.arraycopy(value, srcBegin, dst, dstBegin, srcEnd - srcBegin);  



评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值