学过Java的同学都知道string是用来处理字符串的,拼接、切割、获取一条龙服务,几乎每一个用Java编写的类都能看到它的身影,可谓是Java中的香饽饽。也难怪从JDK1.0版本到现在jdk14版,都很少看到它的较大的更新,可足见其稳定性。今天我们讨论的话题是设计者为何将String类设置为final的,String类都有哪些奥秘?我们在开发中都应该注意什么?我们将以以下问题展开,本文主要使用的是jdk1.8版:
- String的特点
- String与StringBuffer、StringBuilder有哪些用法的区别
- 为何要将String设置成不可变的
- String的扩展
一、String的特点(源码分析)
我们看下jdk1.8的String源码:
public final class String implements Serializable, Comparable<String>, CharSequence {
private final char[] value;//定义char数组
private int hash;//缓存字符串的hashcode
private static final long serialVersionUID = -6849794470754667710L;
private static final ObjectStreamField[] serialPersistentFields = new ObjectStreamField[0];
public static final Comparator<String> CASE_INSENSITIVE_ORDER = new CaseInsensitiveComparator((1)null);
//...
从源码中我们看到String类是被final修饰并实现了Comparable、Serializable、CharSequence接口方法。Comparable主要提供比较方法compareTo用于对字符串比较处理,Serializable主要是实现对象序列化操作而CharSequence接口主要描述字符串行为并提供有限的字串符处理方法。并且在String类中使用char数组用于存储。
而接下来我们看到有很多字符串的构造方法。包括无参的构造器和有参数的,我们选取几个代表性的分析:
public String() {
this.value = "".value;
}
//String类型的构造器
public String(String var1) {
this.value = var1.value;
this.hash = var1.hash;
}
//char数组的构造器
public String(char[] var1) {
this.value = Arrays.copyOf(var1, var1.length);
}
//StringBuffer构造器
public String(StringBuffer var1) {
synchronized (var1) {
this.value = Arrays.copyOf(var1.getValue(), var1.length());
}
}
//StringBuilder构造器
public String(StringBuilder var1) {
this.value = Arrays.copyOf(var1.getValue(), var1.length());
}
从代码中我们看到主要实现了String、StringBuffer、char数组、StringBuilder有参的构造器并且StringBuffer使用synchronized对象锁保证多线程环境下线程安全问题。我们看到构造器中value的赋值使用Arrays.copyOf浅拷贝处理。
接下来我们再看看String类中的几个重要的字符串处理方法:
- equals方法
public boolean equals(Object var1) {
if (this == var1) {
return true;
} else {
if (var1 instanceof String) { //var1基本类型是否为String类型
String var2 = (String) var1;
int var3 = this.value.length;
if (var3 == var2.value.length) {
char[] var4 = this.value;
char[] var5 = var2.value;
for (int var6 = 0; var3-- != 0; ++var6) {
if (var4[var6] != var5[var6]) {
return false;
}
}
return true;
}
}
return false;
}
}
我们知道equals方法是Object方法,而Object中equals方法为:
public boolean equals(Object var1) {
return this == var1;
}
显然在String类中重写的equals方法对String类型做了专门处理。注意在Object中equals比较的是“值”相同,在基本数据类型中是值是否相等而在引用类型中就比较引用地址是否相同了。所以我们要特别注意“==”和equals的细微区别。
- Compareto方法
public int compareTo(String var1) {
int var2 = this.value.length;
int var3 = var1.value.length;
int var4 = Math.min(var2, var3);//math方法取出最短的那个值
char[] var5 = this.value;
char[] var6 = var1.value;
for (int var7 = 0; var7 < var4; ++var7) {
char var8 = var5[var7];
char var9 = var6[var7];
if (var8 != var9) {
return var8 - var9;
}
}
return var2 - var3;
}
通过源码我们可知compareto中使用Math方法取出最短的那个值后在通过循环依次比较每一个char值。当比较值不相同的时候便返回两个值之间的差值。那么equals与compareto方法都有哪些不同呢?
首先compareto中入参是String,equals方法是Object这就说明了compare只能处理String类而无法比较Object类型。同样返回类型也有所不同。
其次由于compareto中对每一个字符都比较取短值,效率有所打折扣。
然后是父类问题,由于equals是Object类,是所有类的基类,compareto重写Comparable方法,这就是说equals方法基本保证都可用。
其他的处理方法包括检索、转化、切割、获取index、大小写等.罗列如下:
1.Length():获取当前字串长度
2.charAt(int index):获取当前字符串对象下标index处的字符
3.getChars():获取从指定位置起的子串复制到字符数组中参数:int srcBegin,int srcEnd,char[] dst,int dstBegin
srcBegin - 字符串中要复制的第一个字符的索引。
srcEnd - 字符串中要复制的最后一个字符之后的索引。
dst - 目标数组。
dstBegin - 目标数组中的起始偏移量。
4.replace(char ch1,char ch2):将字符串的字符ch1替换为字符串ch2.
5.toUpperCase():将字符串中的小写字符转换为大写字符
6.toLowerCase():将字符串中的大写字符转换为小写字符
7.trim():去除头尾空格,Trim删除的过程为从外到内,直到碰到一个非空白的字符为止,所以不管前后有多少个连续的空白字符都会被删除掉。
8.toCharArray():将字符串对象转换为字符数组。
二、String与StringBuffer、StringBuilder有哪些用法的区别
我们看到StringBuffer和StringBuilder也是final类。而Stringbuffer设计成线程安全的,通过synchronized处理线程同步问题。而synchronized是隐式的悲观锁在实现线程安全的同时其性能的开销不容忽视。
另外相比较String处理字符串的方式,StringBuffer无需创建新的对象而使内存处理更快捷。
而StringBuilder是在StringBuffer性能问题上的一个改进,同样使用append和insert等处理字符串对象,所以多数不考虑线程问题上StringBuilder明显有速度的优势。
三、为何要将String设置成不可变的
为何将字符串处理类设置成final类型的?我们看到Java的语言设计者们认为他更倾向于使用final可以缓存结果在传递参数的时候不用考虑有被篡改值。在处理字符串上不用考虑可变类拷贝对象带来的性能问题。
另一方面,String设置成final也是基于安全方面的考虑。字符串设置成可变的话试想你在校验数据之后发生了改变,那将是多么糟糕的处理方式甚至会造成系统奔溃。而final类型天然线程安全而不会出现同步问题。
从程序的使用方面我们也能体会到String设置成final类的好处:
(1)可以实现字符串常量池
String作为Java中特殊的数据类型,也同样被分配常量池。这些常量池类似于系统级别的缓存,操作内存更快。而在jdk1.7之前常量池存放在方法区,jdk1.8之后移除永久区改成metaspace后,常量池就存放在堆内存中。
(2)可以用作hashmap的key
我们通常将不可变的值设置为hashMap(或者hashtable)的key值。这样在hash计算的时候不会因为key的变化造成查找不到对应的value问题。
四、String的扩展
我们知道String创建的方式有两种,一种是定义String s = “”;而另外一种是直接String s1 = new String(“”);显然第二种创建了对象并且在内存中开辟了空间;而第一种直接变量赋值的方式会去字符串常量池去查找是否存在该值。若存在就直接地址指向引用值。这个方法的实现就是String中的intern()本地方法。所以在比较诸如s和s1的值时我们不仅要考虑值大小还应该地址位置。
所以应注意以下代码的执行结果:
String s1 = “1”;
String s2 = new String(“1”);
String s3 = “he” + “llo”:
String s4 = “hello”;
System.out.priintln(s1 == s2);//false
System.out.println(s3 == s4);//true