Java-String那些事

文末有彩蛋!!!!!!

Java-String那些事

String对于广大程序员来说,并不陌生,是我们在编写程序中经常使用到的对象。但是,你真的对String了解吗,使用的方式对吗?

接下来,笔者就对String来进行全面的解析,让你对String有更深入的了解!

更重要的是,面试的时候,虐虐面试官!

String源码(截取)

public final class String 
implements java.io.Serializable, Comparable<String>, CharSequence {

    private final char value[];

    private int hash; // Default to 0

    private static final long serialVersionUID = -6849794470754667710L;
}

通过源码,可以看出String类被final修饰,也就意味着String不能被继承,它其中的方法都默认被final修饰(此特性是final的特点)。也就是说当String对象创建之后,就不能再修改此对象中存储的字符串内容,就是因为如此,才说String类型是不可变的(immutable)

在我们平常创建String对象时,在底层通过char数组来实现。

截取字符串:

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) {
    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 = 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;
}

截取了String类中的三个常用方法,从这三个方法的返回值中可以看出,无论是substring()、concat()还是replace()方法,他们对字符串的操作都不是在原有字符串上进行的,而是通过一系列操作生成了一个新的字符串对象。

这也符合了我们上面所说的,String类被final修饰不可改变,String对象一单创建就固定不变了,对String对象的任何操作都不会改变原对象,只会新生成一个对象。

image

创建String对象

在Java程序中,创建String对象有两种形式,一种叫做字面量形式,例如:String str = "jiaboyan";一种叫做构造形式,也就是我们通常的new对象,例如:String str = new String("jiaboyan");

无论是字面量,还是构造形式,在我们编码时都经常使用,尤其是前者。但是,这两种实现方式在性能和内存上却有着不小的差别。

采用字面值的方式赋值:

public static void test1(){
    String str1 = "jiaboyan";
    String str2 = "jiaboyan";
    System.out.println("test1比较结果为:"+ (str1 == str2));//true
    //System.out.println("test1比较结果为:"+ str1 == str2);//false 注意此种写法
}

执行String str1 = "jiaboyan",程序会去字符串常量池中中查找是否存在"jiaboyan"。如果不存在,则在字符串常量池中创建"jiaboyan",并将“jiaboyan”的引用地址返回给str1,也就是说str1拿到了字符串常量池中“jiaboyan”的引用。如果存在,则不创建任何字符串,直接将池中"jiaboyan"引用地址返回赋给所属变量。当创建字符串对象str2时,字符串池中已经存在"jiaboyan",此时会直接把对象"jiaboyan"的引用地址返回给str2。

采用new关键字新建一个字符串对象:

public static void test2(){
    String str1 = new String("jiaboyan");
    String str2 = new String("jiaboyan");
    System.out.println("test2比较结果为:"+ (str1 == str2));//false
}

采用new方式创建对象,执行String str1 = new String("jiaboyan"),程序会在字符串常量池中查找有没有"jiaboyan"这个字符串,如果有,则不在字符串常量池中创建"jiaboyan",直接在堆中创建一个"jiaboyan"字符串对象,然后将堆中的这个"jiaboyan"对象的地址返回给str1;如果没有,则首先在字符串常量池中创建一个"jiaboyan"字符串,然后再在堆中创建一个"jiaboyan"字符串对象,然后将堆中的这个"jiaboyan"对象的地址返回给str2。此时,str1和str2所指向不同的堆内存区域,使用==比较返回为false。

两种创建方式比较:

public static void compare(){
    String str1 = "jiaboyan";
    String str2 = new String("jiaboyan");
    System.out.println("compare比较结果为:"+ (str1 == str2));//false
}

根据前面的2个例子,可以得出,当我们在创建str1的对象时,实际上程序会去字符串常量池中去创建“jiaboyan”,而当程序执行到str2时,会首先检查字符串常量池中是否存在,若存在则直接在堆内存中创建一个字符串对象;若不存在,则首先在字符串常量池中创建“jiaboyan”,再在堆内存中创建字符串对象。所以,当两者进行比较时,实际上内存地址是不同的。

编译期确定:

public static void test3(){
    String str1 = "jiaboyan";
    String str2 = "jiaboyan";
    String str3 = "jia"+"boyan";
    System.out.println("test3比较结果为:"+ (str1 == str2)); //true
    System.out.println("test3比较结果为:"+ (str1 == str3)); //true
}

str1和str2的原理跟第一个例子相同,不在过多陈述。在str3中,两个字符串拼接起来合成一个字符串,在编译期做了拼接处理,被解析成了一个字符串常量,所以str3在运行期间是以一个整体"jiaboyan"在进行比较,结果为true;

使用javap命令,可以查看到test3()在编译期的处理情况。或者通过查看生成的.class文件。

image

编译期无法确定:

public static void test4(){
    String str1 = "jiaboyan";
    String str2 = new String("jiaboyan");
    String str3 = "jia" + new String("boyan");
    System.out.println("test4比较结果为:"+ ( str1==str2 )); //false
    System.out.println( "test4比较结果为:"+ (str1==str3 )); //false
    System.out.println( "test4比较结果为:"+ (str2==str3 )); //false
}

str1和str2的结果,上面的例子已经说明。str1在编译器可以确定,只会在字符串常量池中创建。str2在运行期,会在堆中对象。str3在编译期无法确定内容,所以编译时候无法进行优化拼接,直到运行时才可确定,并生成新的对象在堆中。

编译期无法确定:

public static void test5(){
    String str1 = "jia";
    String str2 = "boyan";
    String str3 = str1 + str2;
    System.out.println("test5比较结果为:"+ (str3 == "jiaboyan")); //false
}

String str3 = str1 + str2在编译器无法确定,所以无法做拼接优化。只能等到真正运行时,才能确定。所以当str3 == "jiaboyan"时,结果为false,因为一个在堆中创建,一个在字符串常量池中。此外,str3虽然无法在堆中做拼接优化,但是str3在编译期还是做了代码优化,使用的是StringBuilder。具体,请看.class文件:

image

两个在编译期无法确认的String,在编译后是通过StringBuilder对象的append()进行处理的,最后在调用toString()将结果返回给str3。所以,在代码中要么就使用全字符串拼接,要不就别拼接。

编译期确定:

public static void test6(){
    final String str1 = "jia";
    final String str2 = "boyan";
    String str3 = str1 + str2;
    System.out.println("test6比较结果为:"+ (str3 == "jiaboyan"));//true
}

回顾下final的含义,当用final修饰一个类时,表明这个类不能被继承。当用final修饰一个变量时,如果是基本数据类型的变量,则其数值一旦在初始化之后便不能更改;如果是引用类型的变量,则在对其初始化之后便不能再让其指向另一个对象。

在编译期间,由于str1和str2使用了final修饰,所以编译器知道该对象不可改变,所以当编译到str3时,会进行代码优化,直接将str1和str2进行字符串拼接,形成一个“jiaboyan”字符串。当执行比较时为true.

image

编译期无法确定:

public static void test7(){
    final String str1 = "jia";
    final String str2 = get();
    String str3 = str1 + str2;
    System.out.println("test6比较结果为:"+ (str3 == "jiaboyan"));//false
}

public static String get(){
    return "boyan";
}

与上面的例子类似,两个变量str1和str2都用了final修饰。不同的是,str2的值是通过方法来获得。在编译期间,无法确定最终的值,只能在运行时确定,因此str3和“jiaboyan”指向的是不同的内存区域。str3指向了堆中的内存地址,而“jiaboyan”指向的是字符创常量池中。

编译期无法确定:

public static void test8(){
    String str1 = "bo";
    String str2 = "yan";
    String str3 = "jia" + "www" + str1 + "qqq" + "xxx" + str2;
}

与前面的例子类似,本例子算是对上面的一个总结。在我们的程序中,是直接拼接字符串,还是字符串和变量共同连接使用。

通过,编译后的class文件来看,str3中使用了StringBuild来处理字符串之间的拼接,最后在通过toString的方式来返回给str3;

在字符串变量中,使用 + 连接符进行连接时,在编译期间,连接操作会将最左侧的字符串拼接,并创建StringBuilder对象,然后依次对右边进行append操作,最后将StringBuilder对象通过toString()方法转换成String对象。当使用 + 进行多个字符串连接时,实际上是产生了一个StringBuilder对象和一个String对象。

image

equals() 和 ==

关于 == 和 equals() 的使用,也是我们面试/日常工作中经常遇到的。对于这两种比较方式,我们需要有一个清晰的理解。

对于 == 来说,如果比较的是基本类型,例如:byte,short,char,int,long,float,double,boolean,那么实际比较的就是该变量真实值是否相同。但,如果比较的是引用类型,例如:new ArrayList(),new Obeject,那么实际比较的该变量实际在内存中的地址。

对于equals()来说,equals()是基类Object中定义的方法,所有对象都默认继承该类,所以也就默认继承了equals()方法。对于默认equals()来说,实际比较的两个对象在内存中的地址是否相同。

值得注意的是,由于equals()方法可以被重写,所以当类中对equals()重写时候,需要单独关注。例如:String类中就对对equals()进行了重写,实际比较的就是两个字符串中内容是否相同,而不是真实的内存地址。

String.intern()

在String类中,有一个intern()方法,该方法的作用是将在堆中的字符串,copy一份存放到字符串常量池中,设计的初衷其实是为了节省内存的使用,提高程序的性能,可以让程序重用String。

代码如下:

public class test {

    public static void main(String[] agrs){
        Integer[] sample = new Integer[10];
        sample[0] = 0;
        sample[1] = 1;
        sample[2] = 2;
        sample[3] = 3;
        sample[4] = 4;
        sample[5] = 5;
        sample[6] = 6;
        sample[7] = 7;
        sample[8] = 8;
        sample[9] = 9;
        String[] arr = new String[10000000];
        long t = System.currentTimeMillis();
        for (int i = 0; i < 10000000; i++) {
            arr[i] = new String(String.valueOf(sample[i % sample.length])).intern();    
            //arr[i] = new String(String.valueOf(sample[i % sample.length]));
        }
        System.out.println("总耗时:" + (System.currentTimeMillis() - t) + "ms");
    }
}

测试结果:

使用intern()方法的耗时,要比不使用intern()的耗时更长;

平均来看:1800ms 5500ms

在Java1.6中,String.intern()在调用后,会将在堆中生成的字符串,copy一份到字符串常量池中,进而在常量池中生成了一个新的对象;而在Java1.7中,String.intern()有所改变,不会在常量池中新生成对象,而是将在堆中的引用复制到常量池中。

将一下代码,分别在Java1.6和Java1.7下去执行:

public class test {

    public static void main(String[] agrs){
        String str1 = new String("1111") + new String("2222");
        str1.intern();
        String str2 = "11112222";
        System.out.println(str1 == str2);
    }
}

测试结果如下:

在Java1.6:false

在Java1.7:true
image
可伸缩服务架构-框架与中间件

京东购买链接:可伸缩服务架构-框架与中间件

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,383评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,522评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,852评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,621评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,741评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,929评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,076评论 3 410
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,803评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,265评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,582评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,716评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,395评论 4 333
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,039评论 3 316
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,798评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,027评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,488评论 2 361
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,612评论 2 350

推荐阅读更多精彩内容