String
String的定义
String 在Java中是非常常见的一个数据类型了,Java本身对String是一个怎么样的描述呢?
我们来看其中对String的一些解释:
上面的意思就是说: String字符串是一个常量,String实例创建之后,它的的值是不能被改变的,但是字符缓冲区是支持可变的字符串,因为缓冲区里面的不可变字符串对象们可以被共享。(发生对象的引用)。
再来看String在jdk1.8中的定义
可以看出String 是final类型,表示该类就不能被其他类继承了,同时实现了序列化,比较、字符序列的接口。
Java的序列化机制是通过在运行时判断类的serialVersionUID来验证版本的一致性的,在进行反序列化是,JVM会把传过来的字节流中的seriaVersionUID与本地相应实体(类)的serialVersionUID进行比较,如果相同就认为是一致的,就可以进行反序列化,否则就会出现序列化版本不一致的异常(InvalidCastException)。
在Java中,被final类型修饰的类是不允许被其他类继承的,被final修饰的变量被复制后是不允许被修改的。
在研究上面的这段源码,我们知道,String 就是用一个char 数组来保存的,这个数组中是被final修饰的。, 同时String 的默认的hashCode(哈希值)值为0
构造器
我们先看看String的构造器源代码:
|
|
无参数造器
从上面的无参数构造器中可以发现,内部是创建了一个空的字符串对String 的value字符数组进行初始化的,所以这样的方法是会增加不必要的字符串对象(空字符串),所以尽量不要使用如下的方法创建String对象的实例:
字符串参数构造器
从上面的源代码可以看到,传入的字符串直接赋值给了目标字符串的字符数组,和哈希值。所以保证了原来的String字符对象的变不会影响到新的String对象,通过这个方法构造的额字符对象与原来的是一个不一样的对象。
字符数组构造器
源码中给出了字符数组的多种形式的构造器
第一种
第一种的字符数组,将整个字符数组转化为Sting对象,使用的方法是Arrays.copyof(char[], int)
将字符数组中的所有字符复制到目标String的value字符数组中。
从这里可以看出,是直接创建一个字符数组,调用系统的数组复制的方法,将新创建的字符数组对象返回, 而系统的数组复制的这个方法是一个native
方法。 native
方法不是用java实现的了,一般是用C或C++实现的了。
|
|
字符数组、个数的构造器
从参数中可以看出,第二个参数是偏移量,第三个参数是要送字符数组中转化成Sting对象的字符个数。
在构造方法里面,进行了offset 和count数值的异常检查, 最后调用Arrays.copyOfRange(char[],int, int)
方法对字符数组进行复制。我们来看这个方法是怎么实现的吧:
从源码中可以知道也是检查了一些常规的数值检查吗,然后创建新的字符数组对象,调用系统的arraycopy()方法进行过复制。
使用字节数组的构造器
在Java中,Sting 实例中保存了一个char[] 字符数组,char[]字符数组是以unicode码来存储的,Sring 和char为内存形式。byte是网络传输或存储的序列化形式,所以在很多的传输和存储的过程中需要将byte[] 数组和String进行相互的转化,所以String提供了一系列重载和构造方法将一个字节数组转化成String,当提到字节数组和String之间的相互转换的就不得不关注编码的问题。出现乱码的问题主要是编码和解码的方式不一样而导致的,所以有指定字符集(charset)来解码指定的byte数组,将其解码成unicode的char[]数组。这样就能避免乱码的问题。
我们从上面的构造方法中我们可以看到,首先进行的是边界的检查,然后调用的是StringCoding.decode(charsetName, bytes, offset, length);
的方法,返回字符数组。源码如下:
|
|
我们需要知道的是。使用byte[] 字节数组构造String的时候,如果没有指明解码使用的字符集的话,那么StringCoding的decode方法首先会调用系统默认的编码格式,默认是ISO-8859-1编码格式进行操作。
主要方法
hashCode的实现
我们探讨String的hashCode之前先来看看hashCode的官方定义:
hashcode方法返回该对象的哈希码值。支持该方法是为哈希表提供一些优点,例如,java.util.Hashtable 提供的哈希表.
hashCode 的常规协定是:
在 Java 应用程序执行期间,在同一对象上多次调用 hashCode 方法时,必须一致地返回相同的整数,前提是对象上 equals 比较中所用的信息没有被修改。从某一应用程序的一次执行到同一应用程序的另一次执行,该整数无需保持一致。
如果根据 equals(Object) 方法,两个对象是相等的,那么在两个对象中的每个对象上调用 hashCode 方法都必须生成相同的整数结果。
以下情况不 是必需的:如果根据 equals(java.lang.Object) 方法,两个对象不相等,那么在两个对象中的任一对象上调用 hashCode 方法必定会生成不同的整数结果。但是,程序员应该知道,为不相等的对象生成不同整数结果可以提高哈希表的性能。
实际上,由 Object 类定义的 hashCode 方法确实会针对不同的对象返回不同的整数。(这一般是通过将该对象的内部地址转换成一个整数来实现的,但是 JavaTM 编程语言不需要这种实现技巧。)
当equals方法被重写时,通常有必要重写 hashCode 方法,以维护 hashCode 方法的常规协定,该协定声明相等对象必须具有相等的哈希码。
由此我们可以知道,hashCode的哈希值最基本的作用是区分不同的对象。那么String的hashCode是怎么实现的呢?我们探究一下这个hashCode的具体实现吧,如下:
|
|
从源码中可以知道,String是重写了Object中的hashCode的方法,该方法是native方法,String 中的 hashCode的计算过程是:先判断hash是否被计算过,并且字符串不为空,则进行hashCode计算, 然后计算的hashCode的方式是s[0]*31^(n-1)+s[1]*31^(n-2)+ ... + s[n-1]
通过多项式计算而来,所以我们完全可以通过不同的字符串得出的相同的哈希值,所以两个String对象的hashCode相同,并不代表两个String 是一样的。(可以这么想:既然hashCode都是自己可以重写的,那么我自己设计hashCode值计算方式的时候,可能就会不同对象计算出相同的值。)
trim()
|
|
intern
|
|
intern方法是Native调用,它的作用是在方法区中的常量池中通过equals方法寻找等值的对象,如果没有找到则在常量池中开辟一片空间存放字符串并返回该对应String的引用,否则直接返回常量池中已经存在的String对象的引用。
例如
上面的输出是false,原因是st 和st2指向的是不同的两个对象,内存的地址值是不同的。
但是使用intern()方法会有什么样的效果呢?
上述代码输出的是true, 因为st指向的地址来自于常量池中,而st2创建字符对象的时候,是从常量池中去寻找是有相同的字符串,如果有,就返回地址,没有就在常量池中创建新的字符串。所以上面的代码输出的是true。
equals
|
|
equals方式是经常用到的,最常用来比较两个字符串字符内容是否是相等的,那么由上面可以知道,String对象的判断规则是:
- 内存地址相同,则为真。
- 如果对象类型不是String类型,则为假,否则继续判断。
- 如果对象长度不相等,则为假否则继续判断。
- 从后往前,判断String类中char数组value的单个字符是否相等,有不相等则为假,如果一直到第一个数,则返回真。
compareTo()
|
|
String中的compareTo
方法的比较方法是基于每个字符的Unicode的值进行比较的,比较的内容是文本的内容。
substring()
substring是一个使用起来非常方便的方法了,在我们对String的日常操作中是经常使用到的。当然这个方法方便的同时,也要小心使用,尤其是方法中的参数,如果参数没有弄清楚,哈哈,那么可能就得不到你想要的结果哦!那么看看String中是怎么实现的吧!图下面的代码所示:
从上面的代码中可以知道,这个方法做了参数数值的异常处理,然后重要的是在返回上,如果截取的是和原来一致的,那么就返回原来的字符对象,否则的化,构造了一个新的字符串,所以substring都是并没有对原字符串的修改。
注意点:
该方法
trim()
trim()方法也是我们常用的方法,经常用作表单中对用户输入数据的处理,去除前后的空格,怎么做到的呢?直接上源码吧!
|
|
总结
String 对象是不可变类型,返回类型为String 的String方法除了和原字符串相同的情况下返回原字符串,其他的都是返回新的String对象。
String对象的三种比较方式:
- == 是内存的比较:直接对比两个引用的内存值,精确简洁直接明了。
- equals比较: 比较两个引用所指向对象字面值是否相等。
- hashCode数值比较:将字符串数值化,两个引用的hashCode相同,不保证内存一定相同,不保证字面值一定相同。
StringBuffer
StringBuffer概述
上面讲解了String的源码解析,知道了最基本的一个概念是String对象是不可变的,但是StringBuffer是怎么样的呢?或许你学习过Java的人会有一点认识就是StringBuffer是可变的,那么到底是不是呢?我们马上上源码。
从上面对StringBuffer类是final的,那么也就是这个类不能被继承了,从定义中可以看到,StringBuffer是实现了序列化和字符序列。同时继承了抽象的AbstractStringBuilder。
从文档解释上可以得到:StringBuffer是一个线程安全,可变的字符序列。它像String, 但是和String不同的是可以被改变,在任何时候,它包含一些特定的字符序列,但是字符序列的长度和内容是可以通过方法的调用进行改变的。所以说是可变的。
StringBuffer 是线程安全的,想必有写人肯定也听说过,到底是不是呢?看他的解释吧!
从第二段解释中可以看到,StringBuffer是线程安全的,可以载多线程环境下使用。StringBuffer里面的方法进行了同步,所以在任何的实例对象对StringBuffer对象进行操作的时候能够串行的执行,就像是和单独的线程调用效果是一样的。它能够在多线程环境下使用,那么肯定是采取了同步的措施,那么它的措施是什么呢?看下面的源码:
|
|
从StringBufferd的源码可以知道,StringBuffer的同步采用的措施主要是使用关键字synchronized
对方法进行同步。
|
|
从上面源码中的成员变量和构造方法可以知道,StringBuffer的实现还是和String一样是字符数组(通过super(),在父类中有一个字符数组),但是使用默认对的构造方法创建StringBuffer实例的时候是默认字符数组的大小是16个字符的。见如下的源码:
父类中有一个value字符数组用于保存里面的字符数据,同时又一个count用来保存数组中已经使用了的个数。
StringBuffer成员变量中有一个字符缓冲区toStringBuffer
,这个字符缓冲区主要用于保存最后一次调用toString()时的字符值,当StringBuffer被修改的时候,这个缓冲区将会被修改。
从上面的源码中可以知道,StringBuffer中大部分的方法是同步的,在会对StringBuffer对象进行修改的方法中,会将StringBuffer中的缓冲区
toStringCache
清空(赋值为null),但是单调toString()方法的时候,将会把自己的字符数组中数据复制到缓冲区中,然后返一个新的String对象,我们从String源码中也可以看到带字节数组中的构造方法里是这个样的:
123 public String(char[] vlaue){this.value = vlaue;}从中我们可以看到,使用这个String构造方法构造出来的String对象不没有实际复制字符串,只是把value指向了构造参数,所以这样是一种节省空间和时间的操作。
主要方法
append
append学过java的人肯定是知道这个方法的。这个方法真个是很好用呀!那么它是怎么实现的呢?
StringBuffer中的append()方法,调用的都是父类AbstractStringBuilder
中的append
方法,那父类中的又是怎么实现的呢?看下面的源码:
|
|
从父类中的append方法中我们可以知道主要是使用父类中的ensureCapacityInternal
方法进行字符串的追加的,最后返回对象本地身(this),所以append()可以连续调用的,那么继续追踪ensureCapacityInternal
源码是怎么实现的:源码如下
从上面的源码解释中可以看出里面是发生了扩容的,最后将value的值进行拷贝。
delete
|
|
StringBuffter中主要使用的是父类中方法,看看父类中的delete方法是如何实现的
在父类中的delete中可以看到,方法里面做了数值的常规检查,然后确定需要删除的长度,然后调用系统的数组复制的方法进行复制,最后返回本对象。
subSting
subString靠父类AbstractStringBuilder
实在是太多了。subString也是不例外的,肯定是主要用父类的进行实现了。
从父类中的方法可以看返回的是一个新的String对象了。与原来的对象是不共享内存的(因为String(char[] ,int, int)是通过this.value = Arrays.copyOfRange(value, offset, offset+count);
使用系统的方法进行拷贝的)。 由于StringBuffer继承于AbstractStringBuilder,所以这个本身不太具有特殊的方法,,这里就不再一一介绍了。
StringBuilder
之前介绍了String、StringBuffer,那么最后一个StringBuilder怎么能够少了呢?
StringBuilder概述
StringBuffer是AbstractStringBuilder
的子类,StringBuilder也是它的子类。他们的继承和实现的接口都是一样的。AbstractStringBuilder
封装了StringBuffer和StringBuilder的大部分操作的实现。
|
|
其中要注意一点是StringBuffer中没有字符缓冲区了,那么在toString()的时候就不是从缓冲区中拿取字符串,的而是重新构造一个String对象出来,内存是不共享的,这个和
AbstractStringBuilder
中的substring()方法是一样的,都是新创建一个String.
如下是StringBuilder的toString方法源码:
还需要注意一点是,StringBuild与StringBuffer不同的是,StringBuild不是线程安全的,其中的方法没有实现同步,但StringBuffer是线程安全的。
关于String、StringBuffer、StringBuild
三者的区别
String 类型和StringBuffer的主要性能区别:String是不可变的对象, 因此在每次对String 类型进行改变的时候,都会生成一个新的 String 对象,然后将指针指向新的 String 对象,所以经常改变内容的字符串最好不要用 String ,因为每次生成对象都会对系统性能产生影响,特别当内存中无引用对象多了以后, JVM 的 GC 就会开始工作,性能就会降低。
使用 StringBuffer 类时,每次都会对 StringBuffer 对象本身进行操作,而不是生成新的对象并改变对象引用。所以多数情况下推荐使用 StringBuffer ,特别是字符串对象经常改变的情况下。
在某些特别情况下, String 对象的字符串拼接其实是被 Java Compiler 编译成了 StringBuffer 对象的拼接,所以这些时候 String 对象的速度并不会比 StringBuffer 对象慢,例如:
生成 String s1对象的速度并不比 StringBuffer慢。其实在Java Compiler里,自动做了如下转换:
Java Compiler直接把上述第一条语句编译为:
所以速度很快。但要注意的是,如果拼接的字符串来自另外的String对象的话,Java Compiler就不会自动转换了,速度也就没那么快了,例如:
这时候,Java Compiler会规规矩矩的按照原来的方式去做,String的concatenation(即+)操作利用了StringBuilder(或StringBuffer)的append方法实现,此时,对于上述情况,若s2,s3,s4采用String定义,拼接时需要额外创建一个StringBuffer(或StringBuilder),之后将StringBuffer转换为String;若采用StringBuffer(或StringBuilder),则不需额外创建StringBuffer。
使用策略
1. 基本的原则:如果要操作少量的数据,用String;单线程操作大量数据,用StringBuilder多线程操作大量数据用StringBuffer
2. 尽量不要使用String类的”+”来进行字符串的拼接,因为这样的操作性能是极差的,应该使用StringBuffer和StringBuild
|
|
3.为了获得更好的性能,在构造StringBuffer或者StringBuilder时指定它们的容量。如果你操作的字符串的长度不超过16个字符就不用了(默认分配给字符数组大小为16个字符)。
4. StringBuilder一般使用在方法内部来完成类似”+”功能,因为是线程不安全的,所以用完了以后可以丢弃,StringBuffer主要用在全局变量中
5. 相同情况下使用StrigBuilder相比使用StringBuffer仅获得10%~15%左右的性能提升,但是却要冒线程不安全的风险,而现实的模块化编程中,负责某一模块的程序员不一定清晰的判断该模块是否会放入多线程的环境中运行,因此:除非确定系统的瓶颈是在StringBuffer上,并且确定你的模块不会运行在多线程模式下,才可以采用StringBuilder;否则还是用StringBUffer
链接:http://blog.csdn.net/kingzone_2008/article/details/9220691
链接:http://docs.Oracle.com/javase/tutorial/java/data/buffers.html
链接:http://docs.oracle.com/javase/specs/jls/se7/html/jls-4.html#jls-4.12.4