一、String字符串
1.1、认识String类
String:表示为字符串,可以使用字符串字面值与类实例来给该类进行赋值,底层是使用fina char[] value(常量字符数组),并且String类是final常量类,无法被继承,无参构造是创建空的字符串。
//常量类:无法被继承 public final class String implements java.io.Serializable, Comparable<String>, CharSequence { //底层使用char型数组保存字符串 private final char value[]; .... }
特性说明:String代表不可变的字符序列,String对象一旦创建了,对String对象的任何改变都不回影响到原对象,任何更改对象内容的操作都会生成新的对象。
看下String类中的几个更改操作方法:
//合并 public String concat(String str) { int otherLen = str.length(); if (otherLen == 0) { return this; } int len = value.length; char buf[] = Arrays.copyOf(value, len + otherLen); str.getChars(buf, len); return new String(buf, true); } //替换 public String replace(char oldChar, char newChar) { if (oldChar != newChar) { int len = value.length; int i = -1; char[] val = value; /* avoid getfield opcode */ while (++i < len) { if (val[i] == oldChar) { break; } } if (i < len) { char buf[] = new char[len]; for (int j = 0; j < i; j++) { buf[j] = val[j]; } while (i < len) { char c = val[i]; buf[i] = (c == oldChar) ? newChar : c; i++; } return new String(buf, true); } } return this; }
可以看到执行这些方法,最后都返回了新的String对象。
这也就是为什么String被称为不可变的字符序列。
1.2、String两种赋值方式(=、new)
首先看几种赋值方式:
public static void main(String[] args) { //两种创建字符串方式: //方式一:通过在常量池中创建对象返回引用给s String s = "changlu"; //方式二:通过new实例,将引用返回给s1 String s1 = new String("changlu"); //+来合并字符串(也是有区别的) String s2 = "chang"+"lu"; String s3 = "" + s; }
方式一:对于直接赋值字面值的s来说,会在常量池中创建一个字符串并将其引用地址传给s。
方式二:对于使用new创建字符串的,会先判断常量池中是否有此字符串,①若没有,会创建两个对象(常量池及堆),先在常量池中创建一个字符串,将字符串引用传递给堆中对象中的value,接着将堆中对象的引用地址传递给s1;②若有,创建一个对象(堆),首先将常量池中找的该字符串将其引用传递给堆中对象的value,接着再将堆中创建的对象引用传递给s1。
例题情况:由于之前已在常量池中创建了一个字符串"changlu",所以只在堆中创建了一个对象,接着步骤与②一致。
认识下字符串常量池(Constant Pool Table)
字符串常量池出现原因:字符串的分配与其他对象相同,需要消耗高昂的时间与空间,对于字符串的使用是很频繁的,JVM为了提高性能和减少内存的开销引入了字符串常量池,目的是为了共享字符串,相当于给字符串开辟了一个空间,每当创建字符串时会先去常量池中找是否有已经创建相同的字符串,若有直接返回,从而省去了创建的过程(但是有一个遍历查找的过程)。
一旦我们创建字符串时,首先会去检查字符串常量池中是否存在要创建的字符串,若是存在会直接返回该字符串的引用;若不存在才会去创建。
重复赋值区别:
String s = "changlu";//首先在常量池中创建该字符串,再返回引用。(有创建过程) String s1 = "changlu";//在常量池中发现了已创建的字符串返回引用。(无创建过程)
注意点:对于常量池中的字符串在创建之后无法更改,若是使用"+"字符串(在常量区找合并的字符串是否存在),若不存在,则会在字符串常量区中重新再创建一个字符串。
关于字符串常量区所处的位置:
JDK1.6:字符串常量池在方法区中(具体实现:永久代)。
JDK1.7:字符串常量池在堆中。
JDK1.8:字符串常量池在方法区(具体实现:元空间)。
1.3、字符串赋值的各类情况
我们先看一道题:
public static void main(String[] args) { String s = "changlu"; String s1 = new String("changlu"); String s2 = "chang" + "lu"; String s3 = "" + s; //判断是否相同 System.out.println(s == s1);//false System.out.println(s == s2);//true System.out.println(s == s3);//false System.out.println(s1 == s2);//false System.out.println(s1 == s3);//false System.out.println(s2 == s3);//false }
我们将所有字符串都进行了逐一比较,对于这种题很容易混淆不清,实际上我们只需要知道他们对应引用的地址是在哪里创建之后,对于这种问题就很轻松了。
首先记几个关键点:
对于使用=直接赋值字符串的(非拼接),一定是使用的常量区的引用。
对于new出来的字符串对象,一定是在堆中的引用。
常量与常量的拼接结果在常量池。(注意常量池不会有相同内容的常量)
常量与变量的拼接结果在堆中。(在堆中新创建一个对象)
分析过程:
String s = "changlu";//在常量区创建字符串对象"changlu",在常量区中地址为0x11,返回引用给s。==>s=0x11 String s1 = new String("changlu");//s1引用地址的对象中的底层数组value引用地址0x11,而s1存放的是堆中创建对象的引用地址为0x22。==>s1=0x22 String s2 = "chang" + "lu";//对于常量与常量拼接,在编译器完成,所以会直接去常量区找拼接后的字符串是否存在,这里是找到将地址0x11引用给s2。==>s2=0x11 String s3 = "" + s;//常量与变量(字符串对象实例)拼接,会在堆中创建新的对象地址为0x36,该对象中的value则会引用常量区的字符串,由于也已经存在了所以将0x11引用给value。==>s3=0x36
对于引用数据类型进行==判断,是比较它们的引用地址,根据上面分析就能够很快得出程序最后的执行结果了。
补充点:
若是常量与常量拼接,在编译期就能够确定了,jvm会直接在此期间就优化如"chang"+"lu"为"changlu",然后直接拿"changlu"去常量池中找,若有直接返回引用地址,没有则在常量区创建并返回引用地址。
若是常量与变量拼接,在编译期无法确定变量的引用地址,如""+s中的s无法确定,那么就不能在编译期优化字符串,只有在程序运行期间动态分配地址并将新地址赋值给栈中的变量。
对于补充点1中的实际验证可以去参考文章2的链接中查看。
1.4、认识intern()方法
intern()方法是String类中方法:
public native String intern();
该方法是一个本地方法,底层调用c++的方法实现。
由于知识量有限,所以直接说结论不往深处探讨:使用该方法会返回指向常量池的地址。
public static void main(String[] args) { String str = new String("changlu"); String str1 = "changlu"; System.out.println(str == str1);//false System.out.println(str1 == str.intern());//true }
str指向堆中的开辟的地址;
str1指向常量区的地址;
str1.intern()指向堆中实例的value对应的地址(即常量区地址);
1.5、常用方法
字符串与基本数据类型及包装类转换方法
字符串 => 基本数据类型及包装类:Integer包装的parseInt(String s);,其他包装类类似parseXXX(String s);
基本数据类型、包装类 => 字符串:调用不同的String类静态方法String valueOf(int i) 及其他的valueOf()方法
字符数组 => 字符串(String构造器):String(char[]) 和 String(char[],int offset,int length)。
字符串 => 字符数组(String静态方法):public char[] toCharArray(),public void getChars(int srcBegin, int srcEnd, char[] dst, int dstBegin)
字节数组 => 字符串(String构造器):String(byte[])、String(byte[],int offset,int length)
字符串 => 字节数组(String静态方法):public byte[] getBytes() 、public byte[] getBytes(String charsetName)
常见方法
引用的是尚硅谷的课件:
分类总结:长度、指定索引、为空、大小写转换、比较、连接、截取、测试前后缀、是否包含、替换(单个、所有)、正则匹配、拆分字符串。
相关面试题
例1、在使用new String("")创建了几个对象?
通过看博客得知一个比较好的答案,1个或2个。使用new来创建字符串对象时会有如下过程,首先会去检查要创建字符串在字符串常量区中是否存在?
①如果存在,那么只创建一个对象在堆区,接着将在常量区中找到的字符串引用返回给堆中对象的value,再将堆中对象的引用地址返回给栈中的变量。
②如果不存在,那么会创建2个对象(堆与常量区),首先在常量区中创建字符串并将引用传递给堆中对象的value,接着再将堆中对象的引用传递给栈中的常量。
例2:输出下面运行结果
public class StringTest { String s = new String("changlu"); char[] ch = {'h','a','v','a'}; public void change(String s,char ch[]){ s = "liner"; ch[0] = 'j'; } public static void main(String[] args) { StringTest str = new StringTest(); str.change(str.s,str.ch); System.out.println(str.s); System.out.println(str.ch); } }
说明:本题还是有些迷惑性的,结果是str.s没有改变,str.ch改变了。为啥呢,我们看个图一下子就能懂了。
为什么str.s没有更改?因为方法中只是传递了一个String的字符串,那么给它重新赋值字面值,过程是在常量区创建liner,之后将引用地址给s,并没有涉及到str中s的引用地址指向。
str.s更改了?方法中传递的是地址值0x34,做的操作是0x34地址部分的第一个字符赋值为j,对应str实例中的ch依旧是指向0x34,所以最后结果得出更改了。
那么怎样传递才能让str.s改变值呢?
//方法中参数改为str的实例,这时候传过来更改其中的s变量值才是有效的,因为是对str实例中的属性进行更改操作 public void change1(StringTest str){ str.s = "liner"; str.ch[0] = 'j'; }