1. String类型
-
String类源码
为了从本质上理解String类型的特性所在,我们从String类型的源码看起,在源码中String类的注释中存在以下:
/**Strings are constant; their values cannot be changed after they * are created. String buffers support mutable strings. * Because String objects are immutable they can be shared. For example: * String str = "abc"; * is equivalent to: * char data[] = {'a', 'b', 'c'}; * String str = new String(data); */
从中可以理解到:首先,字符串是常量(constant),创建之后就不能再改变;其次,因为String对象是不可变(immutable)量,因此它们是不能共享的,即说明是线程安全的。之后又指出,一个字符串对象相当于一个字符数组。
继续看下去,发现String类使用final关键字修饰,说明String类不能被继承的。继续看类的成员变量:
/** The value is used for character storage. */ private final char value[];
用来存储字符的数组类型也使用final修饰,进一步说明String类型的实例在创建完之后是不可变的。
调用任何String类中的方法不会修改String自身值,除非重新生成对象。
-
equals()和“==”
equals()方法定义在Object类中,比较的是两个对象的内容;而使用“==”比较的是两个对象的地址,或者说是引用。
equals()方法不适用基本类型的比较,基本类型的比较直接使用相应运算符;
Object类的源码中对equals()方法的定义也是采用“==”的方法来比较:
public boolean equals(Object obj) { return (this == obj); }
这说明如果继承自Obejct类的equals()方法如果不经重写,仍然是采用比较对象的方式,从而必须在有需要的时候重写equals()方法进行自定义方式的比较,即equals()方法的默认行为是比较引用,所以除非在自定义类中覆盖equals()方法,否则不会表现出相应的行为。
String类中即对equals()方法进行了重写:
public boolean equals(Object anObject) { if (this == anObject) { return true; } if (anObject instanceof String) { String anotherString = (String)anObject; int n = value.length; if (n == anotherString.value.length) { char v1[] = value; char v2[] = anotherString.value; int i = 0; while (n-- != 0) { if (v1[i] != v2[i]) return false; i++; } return true; } } return false; }
如果两者均为同一对象的引用,则说明相等;如果两个均为String类实例,则需要比较String字符数组,具体为比较字符数组的长度并遍历其中的内容并进行比较。
-
常量池和String类
常量池在java用于保存在编译期已确定的,已编译的class文件中的一份数据。它包括了关于类,方法,接口等中的常量,也包括字符串常量,如String s = "java"这种字面值的声明方式;当然也可扩充,执行器产生的常量也会放入常量池,即常量池具有动态性,运行期间可以将新的常量加入常量池中,故认为常量池是JVM的一块特殊的内存空间。
虚拟机为每个被装载的类型维护一个常量池,池中为该类型所用常量的一个有序集合,包括直接常量(String、Integer和float常量)和对其他类型、字段和方法的符号引用
池化思想:把需要共享的数据放在池中,用一个存储区域俩存放一些公共的资源以减少和控制存储空间的开销。
在定义String时,如果采用字面值方式进行创建:
String str1 = "myString";
String str2 = "myString";
编译程序先去字符串常量池检查,是否存在“myString”,如果不存在,则在常量池中开辟一个内存空间存放“myString”,创建字符串对象;如果存在的话,则不用在常量池中重新开辟空间。反而需要在栈中开辟一块空间,命名为“str2”,存放的值为常量池中“myString”的内存地址,即返回串池中的字符串的地址,并将该地址赋给对象变量。
在定义String时,如果采用new方式进行创建:
在程序编译期,编译程序先去字符串常量池检查,是否存在“myString”,如果不存在,则在常量池中开辟一个内存空间存放“myString”;如果存在的话,则不用重新开辟空间,保证常量池中只有一个“myString”常量,节省内存空间。然后在内存堆中开辟一块空间存放new出来的String实例,在栈中开辟一块空间,命名为“str”,存放的值为堆中String实例的内存地址,这个过程就是将引用str指向new出来的String实例。堆中存放new出来的对象,栈中存放指向对象的指针。
2. StringBuffer和StringBuilder
利用StringBuilder和StringBuffer拼接字符串而不是String:
StringBuffer和StringBuilder都继承了抽象类AbstractStringBuilder,这个抽象类和String一样也定义了char[] value和int count,但是与String类不同的是,它们没有final修饰符。因此得出结论:String、StringBuffer和StringBuilder在本质上都是字符数组,不同的是,在进行连接操作时,String每次返回一个新的String实例,而StringBuffer和StringBuilder的append方法直接返回this,所以这就是为什么在进行大量字符串连接运算时,不推荐使用String,而推荐StringBuffer和StringBuilder。
在String类中,因为属性值是不可变的,当连接字符串的时候也就只能不断创建新的对象,对于有许多字符串连接时,应该使用StringBuffer类或者StringBuilder类,使用其实例化对象来进行字符串的连接时就不会有多余的中间对象生成。
例如:对于字符串连接String str = "A" + "B" + "C" + "D" ;产生有“AB”,“ABC”,“ABCD”,造成常量池中明显产生了多余的对象,浪费了空间。
StringBuffer stringBuffer = new StringBuffer("A"); stringBuffer.append("B"); stringBuffer.append("C"); stringBuffer.append("D"); System.out.println(stringBuffer.toString());
StringBuffer和StringBuilder类的区别:
StringBuffer在方法前加了一个synchronized修饰,起到同步的作用,可以在多线程环境使用,为此付出的代价就是降低了执行效率。因此,如果在多线程环境可以使用StringBuffer进行字符串连接操作,单线程环境使用StringBuilder,它的效率更高。