在编程中,字符串是最常用的一种数据格式,这里以JAVA8为例,主要讲解一下源码中的重点。
String
public final class String
implements Serializable, Comparable<String>, CharSequence
{
private final char[] value;
private int hash;
private static final long serialVersionUID = -6849794470754667710L;
private static final ObjectStreamField[] serialPersistentFields = new ObjectStreamField[0];
public static final Comparator<String> CASE_INSENSITIVE_ORDER = new CaseInsensitiveComparator(null);
}
- 主要特点,也是最重要的特点,String被final修饰,表名String类型不能被继承。即每次对字符串的操作都是产生一个新的对象,而不是在原先对象上进行操作。
- 成员变量value的类型为字符数组,用于存储值,特点:查找快,增删慢。简单的理解就是数组有下标,查询时根据下标直接查找,可以称为“直接地址索引(个人见解)”。增删操作,都伴随着下标的维护。
- String是线程安全的,这个正式因为他的不可变性。
了解过JVM体系结构的都知道常量池,常量池简单的可以分为静态常量池与运行时常量池,这里重要说一下字符串常量池:
字符串常量池存在于运行常量池中(注意在JDK1.7是已经转移到堆中),字符串常量池的存在使JVM提高了性能并且减少了内存开销。
每当我们使用字面量创建一个字符串对象时(String s=“demo”),JVM首先会检查字符串常量池,如果在常量池中已存在该字符串,那么就将该字符串对象的地址赋值给引用‘s’,如果字符串不存在与常量池中,就会实例化该字符串并且放在常量池中,并将该字符串对象的地址赋值给引用‘s’。
每当我们使用关键字new(String s = new String(“1”))创建字符串常量时,JVM首先会检查字符串常量池,如果在常量池中已存在该字符串,直接在堆中复制该对象的副本,然后就将堆中字符串对象的地址赋值给引用‘s’,如果字符串不存在与常量池中,就会实例化该字符串并且放在常量池中,在堆中复制该对象的副本,并将堆中字符串对象的地址赋值给引用‘s’。
由于String的不可变性,我们可以十分的肯定在常量池中不存在相同的字符串。
下面是String的一个方法实现:
//截取字符串
public String substring(int paramInt1, int paramInt2)
{
if (paramInt1 < 0) {
throw new StringIndexOutOfBoundsException(paramInt1);
}
if (paramInt2 > value.length) {
throw new StringIndexOutOfBoundsException(paramInt2);
}
int i = paramInt2 - paramInt1;
if (i < 0) {
throw new StringIndexOutOfBoundsException(i);
}
//注意这里
return (paramInt1 == 0) && (paramInt2 == value.length) ? this : new String(value, paramInt1, i);
}
从最后一句可以看到,最初传入的String并没有改变,其返回的是一个new String(),即创建一个新的字符串对象。其他的String的方法也是同样如此,不会改变原字符串,这也是String的不可变性。
String的经典案例
关于"==和equals"
A:对于==,如果作用于基本数据类型的变量(boolean,byte,char,short,int,long,double,float),则比较的是存储的‘值’;如果作用于引用类型String,则比较的是指向对象的地址(即是否指向的是同一个对象)。
B:equals方法是基类object中的方法,所以所有继承object的类都会拥有该方法。在object类中,equals比较的是两个对象的引用是否相等。
public class Object
{
private static native void registerNatives();
public final native Class<?> getClass();
public native int hashCode();
//可以看到这里的equals方法,比较的是对象的引用
public boolean equals(Object paramObject)
{
return this == paramObject;
}
}
C:对于equals方法,不能作用于基本数据类型变量。如果没有对equals进行重写,则比较的是基本数据变量的所指向对象的地址;String类对equals进行了重写,用来比较所指向的字符串对象所存储的值是否相等。其他的一些类,如Date,Integer等都对equals进行了重写用来比较所指向的字符串对象所存储的值是否相等。
//String对equals方法进行了重写
public boolean equals(Object paramObject)
{
if (this == paramObject) {
return true;
}
if ((paramObject instanceof String))
{
//下面进行循环比较每个char值是否相等
String str = (String)paramObject;
int i = value.length;
if (i == value.length)
{
char[] arrayOfChar1 = value;
char[] arrayOfChar2 = value;
for (int j = 0; i-- != 0; j++) {
if (arrayOfChar1[j] != arrayOfChar2[j]) {
return false;
}
}
return true;
}
}
return false;
}
例子:
public class StringDemo1 {
public static void main(String[] args) {
/**
* 字符串常量池
*/
String str1="demo"; //在常量池创建一个对象 demo
String str2="demo"; //常量池已经存在对象“demo”,所以不再创建对象。
System.out.println("str1==str2:"+(str1 == str2));//(引用类型)true 指向的是同一个对象
System.out.println("str1.equals(str2):"+(str1.equals(str2)));//true,值相等。
/**
* 关键字new String()
*/
String str3 = new String("abc");//创建两个对象(字符串池,堆中),一个引用(栈中)
String str4 = new String("abc");//字符串池已有对象,所以只在堆中创建一个。
System.out.println("str3==str4:"+(str3 == str4));//false str3和str4指向的地址不同(栈区的地址不同)
System.out.println("str3.equals(str4):"+(str3.equals(str4)));//true str3和str4值相等
System.out.println("str1==str3:"+(str1 == str3));//false str1在栈中,str3在堆中
System.out.println("str1.equals(str3):"+(str1.equals(str3)));//true 值相等
}
}
所以String被设计为不可变和不被继承是为了效率以及安全。只有当字符串不可变时,字符串池才有可能实现,同时也可以在运行时节省很多堆空间,因为不同的字符串变量都指向的是字符串池的同一个对象。假若字符串允许改变,那么就会出现改变一个对象,其他指向这个对象的都会随之改变。
字符串的不可变性使得同一字符串对象被多个线程共享,所以保障了多线程的安全性。
(未完待续)