在做java开发中,我们一定没少使用String这个东西,对它可谓是非常熟悉了,但是我今天要来仔细的学习学习String;
初看String
首先学习一个类的最好方式肯定是看源码,我对源码整理一下:
public final class String
implements java.io.Serializable, Comparable, CharSequence
{
/** The value is used for character storage. */
private final char value[];
/** The offset is the first index of the storage that is used. */
private final int offset;
/** The count is the number of characters in the String. */
private final int count;
/** Cache the hash code for the string */
private int hash; // Default to 0
/** use serialVersionUID from JDK 1.0.2 for interoperability */
private static final long serialVersionUID = -6849794470754667710L;
........
}
大概就是这个样子,我们从中可以得到很多信息:
首先,String是被final修饰的,因此便知道为什么不能被继承了,且成员方法都是默认final类型;
其次,String的底层实现是使用char数组来保存字符串;
因为String是被final修饰的,因此其实不可被修改的,那么我们平时使用的concat、replace、sub等是怎么回事尼?跟进去看看就可以发现:
public String substring(int beginIndex, int endIndex) {
if (beginIndex < 0) {
throw new StringIndexOutOfBoundsException(beginIndex);
}
if (endIndex > count) {
throw new StringIndexOutOfBoundsException(endIndex);
}
if (beginIndex > endIndex) {
throw new StringIndexOutOfBoundsException(endIndex - beginIndex);
}
return ((beginIndex == 0) && (endIndex == count)) ? this :
new String(offset + beginIndex, endIndex - beginIndex, value);
}
public String concat(String str) {
...
return new String(0, count + otherLen, buf);
}
public String replace(char oldChar, char newChar) {
if (oldChar != newChar) {
int len = count;
int i = -1;
char[] val = value; /* avoid getfield opcode */
int off = offset; /* avoid getfield opcode */
while (++i < len) {
if (val[off + i] == oldChar) {
break;
}
}
if (i < len) {
char buf[] = new char[len];
for (int j = 0 ; j < i ; j++) {
buf[j] = val[off+j];
}
while (i < len) {
char c = val[off + i];
buf[i] = (c == oldChar) ? newChar : c;
i++;
}
return new String(0, len, buf);
}
}
return this;
}
无论是sub、concat、replace操作都不是在原有的字符串上进行的,而是重新生成了一个新的字符串对象。一句话解释就是,最原始的字符串始终没有被改变。
我们可以得出一个结论:
String对象一旦被创建就固定不变了,对String对象的任何操作都不会影响到原对象,所改变都是生成了新的对象
常量池
String和其他类一样,产生对象就得为其分配空间,但是String的使用太常见了,JVM为了提高性能,就在字符串实例化的时候做了一些优化:
每当我们创建字符串常量时,JVM会首先检查字符串常量池,如果该字符串已经存在常量池中,那么就直接返回常量池中的实例引用。如果字符串不存在常量池中,就会实例化该字符串并且将其放到常量池中。由于String字符串的不可变性我们可以十分肯定常量池中一定不存在两个相同的字符串.
String b = "chenssy";
String b = "chenssy";
String c = new String("chenssy");
a、b和字面上的chenssy都是指向JVM字符串常量池中的"chenssy"对象,他们指向同一个对象;new关键字一定会产生一个对象chenssy(注意这个chenssy和上面的chenssy不同),同时这个对象是存储在堆中。所以上面应该产生了两个对象:保存在栈中的和保存堆中的。但是在Java中根本就不存在两个完全一模一样的字符串对象。故堆中的chenssy应该是引用字符串常量池中chenssy。
1.用new String() 创建的字符串不是常量,不能在编译期就确定,所以new String() 创建的字符串不放入常量池中,它们有自己的地址空间.
2.JVM对String str="abc"对象放在常量池中是在编译时做的,而String str1=str+str是在运行时刻才能知道的,new对象也是在运行时才做的.
3.字符串的"+"连接中,如果有字符串引用存在,而引用的值在程序编译期是无法确定的,例如s2="a"+s1;s1="b";即"a" + s1无法被编译器优化,只有在程序运行期来动态分配并将连接后的新地址赋给s2;字面量"+"拼接是在编译期间进行的,拼接后的字符串存放在字符串池中;而字符串引用的"+"拼接运算实在运行时进行的,新创建的字符串存放在堆中。
字符串重要提示
- String使用private final char value[]来实现字符串的存储,也就是说String对象创建之后,就不能再修改此对象中存储的字符串内容,就是因为如此,才说String类型是不可变的(immutable),而且String的编辑功能是通过创建一个新的对象来实现的,而不是对原有对象进行修改,例如replace。
- A a;这个语句声明一个类A的引用变量a[我们常常称之为句柄],而对象一般通过new创建,所以aa仅仅是一个引用变量,它不是对象。
- 创建字符串的方式
(1)使用""引号创建字符串;
(2)使用new关键字创建字符串。
(3)单独使用""引号创建的字符串都是常量,编译期就已经确定存储到String Pool中;
(4)使用new String("")创建的对象会存储到heap中,是运行期新创建的;new创建字符串时首先查看池中是否有相同值的字符串,如果有,则拷贝一份到堆中,然后返回堆中的地址;如果池中没有,则在堆中创建一份,然后返回堆中的地址(注意,此时不需要从堆中复制到池中,否则,将使得堆中的字符串永远是池中的子集,导致浪费池的空间)!
(5)使用只包含常量的字符串连接符如"aa" + "aa"创建的也是常量,编译期就能确定,已经确定存储到String Pool中;
(6)使用包含变量的字符串连接符如"aa" + s1创建的对象是运行期才创建的,存储在heap中; - 在执行到双引号包含字符串的语句时,如String a = "123",JVM会先到常量池里查找,如果有的话返回常量池里的这个实例的引用,否则的话创建一个新实例并置入常量池里。所以,当我们在使用诸如String str = "abc";的格式定义对象时,总是想当然地认为,创建了String类的对象str。对象可能并没有被创建!而可能只是指向一个先前已经创建的对象。只有通过new()方法才能保证每次都创建一个新的对象。
- 在执行String a = new String("123")的时候,首先走常量池的路线取到一个实例的引用,然后在堆上创建一个新的String实例,通过构造函数给value属性赋值,然后把实例引用赋值给a:虽然是新创建了一个String的实例,但是value是等于常量池中的实例的value,即是说没有new一个新的字符数组来存放"123"。
- intern方法使用:一个初始为空的字符串池,它由类String独自维护。当调用 intern方法时,如果池已经包含一个等于此String对象的字符串(用equals(oject)方法确定),则返回池中的字符串。否则,将此String对象添加到池中,并返回此String对象的引用。它遵循以下规则:对于任意两个字符串 s 和 t,当且仅当 s.equals(t) 为 true 时,s.intern() == t.intern() 才为 true
- 关于equals和==
(1)对于==,如果作用于基本数据类型的变量(byte,short,char,int,long,float,double,boolean ),则直接比较其存储的"值"是否相等;如果作用于引用类型的变量(String),则比较的是所指向的对象的地址(即是否指向同一个对象)。
(2)equals方法是基类Object中的方法,因此对于所有的继承于Object的类都会有该方法。在Object类中,equals方法是用来比较两个对象的引用是否相等,即是否指向同一个对象。
(3)对于equals方法,注意:equals方法不能作用于基本数据类型的变量。如果没有对equals方法进行重写,则比较的是引用类型的变量所指向的对象的地址;而String类对equals方法进行了重写,用来比较指向的字符串对象所存储的字符串是否相等。其他的一些类诸如Double,Date,Integer等,都对equals方法进行了重写用来比较指向的对象所存储的内容是否相等。 - String中的"+"
(1).String中使用 + 字符串连接符进行字符串连接时,连接操作最开始时如果都是字符串常量,编译后将尽可能多的直接将字符串常量连接起来,形成新的字符串常量参与后续连接(通过反编译工具jd-gui也可以方便的直接看出);
(2).接下来的字符串连接是从左向右依次进行,对于不同的字符串,首先以最左边的字符串为参数创建StringBuilder对象,然后依次对右边进行append操作,最后将StringBuilder对象通过toString()方法转换成String对象(注意:中间的多个字符串常量不会自动拼接)。
也就是说String c = "xx" + "yy " + a + "zz" + "mm" + b; 实质上的实现过程是: String c = new StringBuilder("xxyy ").append(a).append("zz").append("mm").append(b).toString();
由此得出结论:当使用+进行多个字符串连接时,实际上是产生了一个StringBuilder对象和一个String对象。 - String、StringBuffer、StringBuilder的区别
(1)可变与不可变:String是不可变字符串对象,StringBuilder和StringBuffer是可变字符串对象(其内部的字符数组长度可变)。
(2)是否多线程安全:String中的对象是不可变的,也就可以理解为常量,显然线程安全。StringBuffer 与 StringBuilder 中的方法和功能完全是等价的,只是StringBuffer 中的方法大都采用了synchronized 关键字进行修饰,因此是线程安全的,而 StringBuilder 没有这个修饰,可以被认为是非线程安全的。
(3)String、StringBuilder、StringBuffer三者的执行效率:
StringBuilder > StringBuffer > String 当然这个是相对的,不一定在所有情况下都是这样。比如String str = "hello"+ "world"的效率就比 StringBuilder st = new StringBuilder().append("hello").append("world")要高。因此,这三个类是各有利弊,应当根据不同的情况来进行选择使用:
当字符串相加操作或者改动较少的情况下,建议使用 String str="hello"这种形式;
当字符串相加操作较多的情况下,建议使用StringBuilder,如果采用了多线程,则使用StringBuffer。 - 关于String str = new String("abc")创建了多少个对象?
new只调用了一次,也就是说只创建了一个对象。而这道题目让人混淆的地方就是这里,这段代码在运行期间确实只创建了一个对象,即在堆上创建了"abc"对象。而为什么大家都在说是2个对象呢,这里面要澄清一个概念,该段代码执行过程和类的加载过程是有区别的。在类加载的过程中,确实在运行时常量池中创建了一个"abc"对象,而在代码执行过程中确实只创建了一个String对象。
因此,这个问题如果换成 String str = new String("abc")涉及到几个String对象?合理的解释是2个。
个人觉得在面试的时候如果遇到这个问题,可以向面试官询问清楚”是这段代码执行过程中创建了多少个对象还是涉及到多少个对象“
String.split()用法小结
在java.lang包中有String.split()方法,返回是一个数组;
1、如果用“.”作为分隔的话,必须是如下写法,String.split("\."),这样才能正确的分隔开,不能用String.split(".");
2、如果用“|”作为分隔的话,必须是如下写法,String.split("\|"),这样才能正确的分隔开,不能用String.split("|");“.”和“|”都是转义字符,必须得加"\";
3、如果在一个字符串中有多个分隔符,可以用“|”作为连字符,比如,“acount=? and uu =? or n=?”,把三个都分隔出来,可以用String.split("and|or");使用String.split方法分隔字符串时,分隔符如果用到一些特殊字符,可能会得不到我们预期的结果。
4、用竖 * 分隔字符串运行将抛出java.util.regex.PatternSyntaxException异常,用加号 + 也是如此。
5、显然, + * 不是有效的模式匹配规则表达式,用"\*" "\+"转义后即可得到正确的结果。"|" 分隔串时虽然能够执行,但是却不是预期的目的,"\|"转义后即可得到正确的结果。还有如果想在串中使用""字符,则也需要转义.首先要表达"aaaa\bbbb"这个串就应该用"aaaa\bbbb",如果要分隔就应该这样才能得到正确结果。
String.trim()
String.Trim()方法会去除字符串两端,不仅仅是空格字符,它总共能去除25种字符: ('/t', '/n', '/v', '/f', '/r', ' ', '/x0085', '/x00a0', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', '?', '/u2028', '/u2029', ' ', '?')
如果你想保留其中的一个或多个(例如/t制表符,/n换行符,/r回车符等),请慎用Trim方法。
请注意,Trim删除的过程为从外到内,直到碰到一个非空白的字符为止,所以不管前后有多少个连续的空白字符都会被删除掉
空格 != 空白字符,删除空格请使用: Trim(‘ ‘);