String能够说是在Java开发中必不可缺的一种类,String容易忽略的细节也不少,对String的了解程度也反映了一个Java程序员的基本功。下面就由一个面试题来引出对String的剖析。java
从源码能够看出,String有三个私有方法,底层是由字符数组来存储字符串程序员
public final class String implements java.io.Serializable, Comparable<String>, CharSequence { /**存储字符串的字符数组*/ private final char value[]; /** 缓存字符串的hashcode */ private int hash; // 默认是0 /** 用于验证一致性来是否进行反序列化 */ private static final long serialVersionUID = -6849794470754667710L;
// String 为参数的构造方法 public String(String original) { this.value = original.value; this.hash = original.hash; } // char[] 为参数构造方法 public String(char value[]) { //从新复制一份char数组的值和信息,保证字符串不会被修改传回 this.value = Arrays.copyOf(value, value.length); } // StringBuffer 为参数的构造方法 public String(StringBuffer buffer) { synchronized(buffer) { this.value = Arrays.copyOf(buffer.getValue(), buffer.length()); } } // StringBuilder 为参数的构造方法 public String(StringBuilder builder) { this.value = Arrays.copyOf(builder.getValue(), builder.length()); }
/**比较两个字符串是否相等,返回值为布尔类型*/ public boolean equals(Object anObject) {//比较类型能够是object /*引用对象相同时返回true*/ if (this == anObject) { return true; } /*判断引用对象是否为String类型*/ if (anObject instanceof String) { //instanceof用来判断数据类型是否一致 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; }
equals()
方法首先经过instanceof判断数据类型是否一致,是则进行下一步将两个字符串转换成字符数组逐一判断。最后再返回判断结果。面试
/*比较两个字符串是否相等,返回值为int类型*/ public int compareTo(String anotherString) {//比较类型只能是String类型 int len1 = value.length; int len2 = anotherString.value.length; /*得到两字符串最短的字符串长度lim*/ int lim = Math.min(len1, len2); char v1[] = value; char v2[] = anotherString.value; /*逐一比较两字符组的字符*/ int k = 0; while (k < lim) { char c1 = v1[k]; char c2 = v2[k]; //若两字符不相等,返回c1-c2 if (c1 != c2) { return c1 - c2; } k++; } return len1 - len2; }
compareTo()
经过逐一判断两字符串中的字符,不相等则返回两字符差,反之循环结束最后返回0数组
compareTo()
和equals()
都能比较两字符串,当equals()返回true,compareTo()返回0时,都表示两字符串彻底相同。compareTo()
是boolean,equals()
是int。compareTo()
是Object,equals()
只能是String类型。indexOf()
:查询字符串首次出现的下标位置lastIndexOf()
:查询字符串最后出现的下标位置contains()
:查询字符串中是否包含另外一个字符串toLowerCase()
:把字符串所有转换成小写toUpperCase()
:把字符串所有转换成大写length()
:查询字符串的长度trim()
:去掉字符串首尾空格replace()
:替换字符串中的某些字符split()
:把字符串分割并返回字符串数组join()
:把字符串数组转为字符串知道了String的实现和方法,下面就要引出常见的String面试问题缓存
Java 语言之父 James Gosling 的回答是,他会更倾向于使用 final,由于它可以缓存结果,当你在传参时不须要考虑谁会修改它的值;若是是可变类的话,则有可能须要从新拷贝出来一个新值进行传参,这样在性能上就会有必定的损失。安全
James Gosling 还说迫使 String 类设计成不可变的另外一个缘由是安全,当你在调用其余方法时,好比调用一些系统级操做指令以前,可能会有一系列校验,若是是可变类的话,可能在你校验事后,它的内部的值又被改变了,这样有可能会引发严重的系统崩溃问题,这是迫使 String 类设计成不可变类的一个重要缘由。并发
因此只有当字符串不可改变时,才能利用字符常量池,保证在使用字符的时候不会被修改。app
那么问题来了,咱们在使用final修饰一个变量时,不变的是引用地址,引用地址对应的对象是能够发生变化的。如:ide
import java.util.Arrays; public class IntTest{ public static void main(String args[]){ final char[] arr = new char[]{'a', 'b', 'c', 'd'}; System.out.println("arr的地址1:" + arr); System.out.println("arr的值2:" + Arrays.toString(arr)); arr[2] = 'b';//修改arr[2]的值 /**修改arr数组的地址,这里会发生编译错误,因此没法修改引用地址 arr = new char[]{'1', '2', '3'};*/ System.out.println("arr的地址2:" + arr); System.out.println("arr的值2:" + Arrays.toString(arr)); } } /*运行结果: arr的地址1:[C@15db9742 arr的值1:[a b c d] arr的地址2:[C@15db9742 arr的值2:[a b b d] 显然不变的是引用地址,引用地址所指对象的内容能够被修改 */
而在上面1中的源码里,String类下有一个私有的char数组成员性能
public final class String implements java.io.Serializable, Comparable<String>, CharSequence { /**存储字符串的字符数组*/ private final char value[];
那么是否能够经过修改char数组所指对象的内容,来改变string的值呢?来试一试:
import java.util.Arrays; public class IntTest{ public static void main(String args[]){ char[] arr = new char[]{'a','b','c','d'}; String str = new String(arr); System.out.println("arr的地址1:" + arr); System.out.println("str= " + str); System.out.println("arr[]= "+Arrays.toString(arr)); arr[2]='b';//修改arr[2]的值 System.out.println("arr的地址2:" + arr); System.out.println("str= "+str); System.out.println("arr[]= "+Arrays.toString(arr)); } } /*运行结果: arr的地址1:[C@15db9742 str= abcd arr[]= [a, b, c, d] arr的地址2:[C@15db9742 str= abcd arr[]= [a, b, b, d] */
显然没法修改字符串,这是为什么,咱们再看看构造方法
// String 为参数的构造方法 public String(String original) { this.value = original.value; this.hash = original.hash; } // char[] 为参数构造方法 public String(char value[]) { //从新复制一份char数组的值和信息,保证字符串不会被修改传回 this.value = Arrays.copyOf(value, value.length); }
发现string的构造方法里将原来的char数组的值和信息copy了一份,保证字符串不会被修改传回。
==在基本类型中比较其对应的值,在引用类型中比较其地址值
equals()在未被重写时和 == 彻底一致,被重写后是比较字符串的值
public class StringTest { public static void main(String args[]) { String str1 = "Java"; //放在常量池中 String str2 = new String("Java"); //在堆中建立对象str2的引用 String str3 = str2; //指向堆中的str2的对象的引用 String str4 = "Java"; //从常量池中查找 String str5 = new String("Java"); System.out.println(str1 == str2); //false System.out.println(str1 == str3); //false System.out.println(str1 == str4); //true System.out.println(str2 == str3); //true System.out.println(str2 == str5); //false System.out.println(str1.equals(str2)); //true System.out.println(str1.equals(str3)); //true System.out.println(str1.equals(str4)); //true System.out.println(str2.equals(str3)); //true } }
实际上equals()
方法也是继承Object的equals()
方法。
public boolean equals(Object obj) { return (this == obj); }
从上面的equals()
方法的源码能够看出,String在继承方法后对应修改了方法中的相关内容,因此上述代码的equals()
方法输出都是true。
相似于String str1 = "Java";
的和String str2 = new String("Java");
形式有很大的区别,String str1 = "Java";
形式首先在编译过程当中Java虚拟机就会去常量池中查找是否存在“Java”,若是存在,就会在栈内存中开辟一块地方用于存储其常量池中的地址。因此这种形式有可能建立了一个对象(常量池中),也可能一个对象也没建立,即str1是直接在常量池中建立“Java”字符串,str4是先在常量池中查找有“Java”,因此直接地址直接指向常量池中已经存在的”Java“字符串。
而String str2 = new String("Java");
的形式在编译过程当中,先去常量池中查找是否有“Java”,没有则在常量池中新建"Java"。到了运行期,无论常量池中是否有“Java”,一概从新在堆中建立一个新的对象,然若是常量池中存在“Java”,复制一份放在堆中新开辟的空间中。若是不存在则会在常量池中建立一个“Java”后再复制到堆中。因此这种形式至少建立了一个对象,最多两个对象。所以str1和str2的引用地址必然不相同。
调用intern方法时,若是常量池中存在该字符串,则返回池中的字符串。不然将此字符串对象添加到常量池中,并返回该字符串的引用。
String s1 = new String("Java"); String s2 = s1.intern();//直接指向常量池中的字符串 String s3 = "Java"; System.out.println(s1 == s2); // false System.out.println(s2 == s3); // true
关于这三者的区别,主要借鉴这篇博文String,StringBuffer与StringBuilder的区别??首先,String是字符串常量,后二者是字符串变量。其中StringBuffer是线程安全的,下面说说他们的具体区别。
String适用于字符串不可变的状况,由于在常常改变字符串的情形下,每次改变都会在堆内存中新建对象,会形成 JVM GC的工做负担,所以在这种情形下,须要使用字符串变量。
再说StringBuffer,它是线程安全的可变字符序列,它提供了append和insert方法用于字符串拼接,并用synchronized来保证线程安全。而且能够对这些方法进行同步,像以串行顺序发生,并且该顺序与所涉及的每一个线程进行的方法调用顺序一致。
@Override public synchronized StringBuffer append(Object obj) { toStringCache = null; super.append(String.valueOf(obj)); return this; } @Override public synchronized StringBuffer append(String str) { toStringCache = null; super.append(str); return this; }
最后是StringBuilder,由于StringBuffer要保证线程安全,因此性能不是很高,因而在JDK1.5后引入了StringBuilder,在没有了synchronize后性能获得提升,并且二者的方法基本相同。因此在非并发操做下,如单线程状况可使用StringBuilder来对字符串进行修改。
其实在2.4中提到,String是字符串常量,具备不可变性。因此在拼接字符串、修改字符串时,尽可能选择StringBuilder和StringBuffer。下面再谈一谈String中出现“+”操做符的状况:
String s1 = "Ja"; String s2 = "va"; String s3 = "Java"; String s4 = "Ja" + "va"; //在编译时期就在常量池中建立 String s5 = s1 + s2; //实际上s5是stringBuider,这个过程是stringBuilder的append System.out.println("s3 == s4 " + (s3 == s4)); System.out.println("s3 == s5 " + (s3 == s5)); /** 运行结果: s3 == s4 true s3 == s5 false */
为何s4==s3结果是true? 反编译看看:
1 String s = "Ja";//s1 2 String s1 = "va";//s2 3 String s2 = "Java";//s3 4 String s3 = "Java";//s4 5 String s4 = (new StringBuilder()).append(s).append(s1).toString();//s5 6 System.out.println((new StringBuilder()).append("s3 == s4").append(s2 == s3).toString()); 7 System.out.println((new StringBuilder()).append("s3 == s5").append(s2 == s4).toString());
从第5行代码中看出s4在编译时期就已经将“Ja”+“va”编译“Java” ,这就是JVM的优化
第6行的代码说明在s5 = s1 +s2;
执行过程,s5变成StringBuilder,并利用append方法将s1和s2拼接。
所以在String类型中使用“+”操做符,编译器通常会将其转换成new StringBuilder().append()来处理。