Java-String之寻根问底

引言

在java编程中,几乎每天都会跟String打交道,因此,深入理解String及其用法十分有必要。下面分三方面来详细说明下String相关的特点及用法 •Immutable(不可变)特性 •连接符号+的本质 •相等判断两种方式(==/equals)说明

一、 Immutable特性

Java设计人员为了方便大家对字符串的各种操作,抽象出String类,该类封装了对字符串的查找、拼接、替换、截取等一系列操作。查看java.lang.String的源码,首先就能看到如下描述:

The String class represents character strings. All string literals in Java programs, such as “abc”, are implemented as instances of this class. Strings are constant; their values cannot be changed after they are created. String buffers support mutable strings. Because String objects are immutable they can be shared.

大意是:String类代表着字符序列。Java语言中所有的字符串字面量,如“abc”,都实现为String类的实例。 String对象都是常量,其值在创建之后就不能改变。字符串缓冲区支持可变的字符序列。由于String对象的不可变性,他们可以被共享。

String的immutable体现在两个方面:

•String类被final关键字修饰,意味着该类不能被继承。由于String类不能有子类,保证了String类的静态和实例方法不可能被继承修改,保证了安全性。

•String类内部的私有成员变量,如value(char[]),offset(int),count(int)都被final关键字修饰。当String对象创建后,offset跟count字段的值不可改变(因为是基本数据类型int),value变量不能再指向其它的字符数组(因为其是引用类型变量)

看到这里,可能有人会说:虽然value属性不能指向其它的字符数组,但其指向的字符数组内容还是可以改变,如果能够改变其内容,那不意味着String对象是可变的了。

上面的说法没错,但是String类本身没有提供修改字符数组的方法,除非你用非常规手段(例如反射)去改变私有属性的值(后面会附上代码实现)。虽然从代码上完全是可以改变创建后String对象的各属性的值(即使属性被private final修饰),但毕竟是采用反射这种非常规手段。按照正常使用方式,我们是不能改变String对象的值,所以还是认为String对象是不可变的。

注意:这里说String不变性,是指String对象在创建后其值不能改变。对于引用类型的变量,是可以指向不同的String对象来改变其所代表的值。如

String s = "abc";
s = "def";

引用类型变量s指向值为‘abc’的String对象,然后s又指向了值为‘def’的String对象。虽然s所代表的字符串确实改变了,但是对于String对象abc和def并没有改变,仅仅是s指向了不同的String对象而已。

关于String设计为immutable,至少有两方面的好处:

•一是安全。String类被final修饰,意味着不可能有子类继承String类而改变其原有行为。并且,生成的String对象是不变的,在多线程环境也是安全的。

•二是效率。String类被final修饰,隐含着该类的所有方法都是final的,编译器可以进行一些优化。另外,由于String对象是不变的,可以被多处共享且不需要进行多线程之间的同步,提高了效率。

由于String对象的不变性,在用+号进行字符串连接时,可能会造成效率低下,下面详细说明下连接符号+的本质是什么?底层是如何进行字符串拼接的?什么情况下用+号进行字符串连接效率较低?

二、 连接符号+本质

要了解+号的本质,先从java编译说起。众所周知,java代码在运行前都需要先编译成Class文件(关于Class文件的结构,由于篇幅有限,这里不作详细说明)。在Class文件中,有一部分是叫属性表集合,其中包括Code属性,简单说,Code属性包含的就是方法体里面的代码经过编译后对应的字节码指令。因此,我们可以直接查看Class文件中的字节码指令来了解+的本质。示例代码如下

public class StringTest {
    public static void main(String[] args) {
        String s = "Hello";
        s = s + " world!";
    }
}

由于我们不熟悉Class文件结构,而且字节码非常不容易看懂,在这里不直接查看编译生成的StringTest.class文件的内容,而是通过jad工具反编译字节码查看结果。在cmd下执行jad命令jad -o -a -sjava StringTest.class成功执行上述命令后,会发现StringTest.class文件所在目录下会多出源文件StringTest.java,内容如下:

public class StringTest
{

    public StringTest()
    {
    //    0    0:aload_0
    //    1    1:invokespecial   #8   <Method void Object()>
    //    2    4:return
    }

    public static void main(String args[])
    {
        String s = "Hello";
    //    0    0:ldc1            #16  <String "Hello">
    //    1    2:astore_1
        s = (new StringBuilder(String.valueOf(s))).append(" world!").toString();
    //    2    3:new             #18  <Class StringBuilder>
    //    3    6:dup
    //    4    7:aload_1
    //    5    8:invokestatic    #20  <Method String String.valueOf(Object)>
    //    6   11:invokespecial   #26  <Method void StringBuilder(String)>
    //    7   14:ldc1            #29  <String " world!">
    //    8   16:invokevirtual   #31  <Method StringBuilder StringBuilder.append(String)>
    //    9   19:invokevirtual   #35  <Method String StringBuilder.toString()>
    //   10   22:astore_1
    //   11   23:return
    }
}

上述反编译出的源代码包含了注释行,代表与源代码相对应的字节码指令。很显然,源代码中并没有字符串连接符+,也就是说,+号在经过编译后,已经被替换成StringBuilder的append方法调用(实现上在jdk1.5版本之前,+号在编译器编译后是替换为StringBuffer的append方法调用)。所谓的+号连接字符串,本质上是通过new StringBuilder对象后调用其append方法进行字符串拼接。

java通过在编译阶段重载字符串操作符+,在方便对字符串的操作同时也带来了一定的副作用,比如由于程序员不清楚+号的本质而编写出效率低下的代码,请看如下代码:

 public String concat(){
        String result = "";
        for (int i = 0; i < 1000; i++) {
            result += i;
        }
        return result;
    }

在for循环体内,出现+号的地方,编译后都会被替换为如下调用:

result = (new StringBuilder(String.valueOf(result))).append(i).toString();

显然,每次循环都需要在构造StringBuilder对象时对result中的字符数组进行拷贝,而在调用toString方法时,又要拷贝StringBuilder中的字符数组来构建String对象。相当于每次for循环,要进行两次对象创建及两次字符数组拷贝,因而程序效率低下。更高效的代码如下:

  public String concat(){
        StringBuilder result = new StringBuilder();
        for (int i = 0; i < 1000; i++) {
            result.append(i);
        }
        return result.toString();
    }

至此,我相信大家已经知道了+号的本质,以及如何避免低效的使用+号。下面我们就来深入了解下String对象相等判断的两种方式(经常出现在java面试题中)。

三、 相等判断两种方式(==/equals)说明

•==:当两个操作数是基本数据类型时,比较值是否相等;当两个操作数是引用类型时,比较是否指向同一个对象。 •equals方法用来比较两个对象的内容是否相等。

由于String是引用类型,当用==判断时,比较的是两个String变量是否指向同一个String对象;当用equals方法判断时,才会比较两个String对象的内容是否相等。在实际项目中进行字符串比较时,基本是比较两个String对象的内容是否相等,因此建议大家全部使用equals方法进行比较。

用==进行字符串比较,经常出现在面试题中,而不是项目代码中,对于实际工作考核的意义不大,只是作为大家对String了解程度的一个考核。下面列举一些面试题如下:

题1

        String a = "a1";
        String b = "a" + 1;
        System.out.println(a == b);

答案:true 说明:当两个字面量进行连接时,实际上在java编译器的编译期,已经进行了字面量的拼接。也就是说编译生成的Class文件中并不存在String b = “a” + 1对应的字节码指令,已经被优化为String b = “a1”对应的字节码指令。我想这步优化大家应该很能理解,在编译期间能够确定结果并进行计算,就能有效减少Class文件中的字节码指令,即减少了程序运行时需要执行的指令,提高了程序效率(大家可以用上述jad命令反编译Class文件进行验证)。同理,对于基本数据类型字面量的算术操作,也在编译期间进行了计算,例如int day = 24 * 60 * 60,编译后被替换成代码int day = 0x15180。由于在编译期间已经进行了拼接,这样局部变量a和b都指向了常量池中的’a1’对象,因此a == b输出为true。

题2

        String hw = "Hello world!";
        String h = "Hello";
        h = h + " world!";
        System.out.println(h == hw);

答案:false 说明:通过之前关于字符串连接符+分析,我们知道h = h + “ world!”经过编译后会被替换成h = (new StringBuilder(String.valueOf(h))).append(“ world!”).toString()。查看下StringBuilder的toString方法,可以看到该方法实际就是return new String(value, 0, count),也就是h将指向java堆上的对象,而hw是指向常量池中的对象。虽然h和hw的内容相同,但由于指向不同的String对象,所以输出为false。

题3

    public static final String h2 = "Hello";

    public static final String h4 = getH();

    private static String getH() {
        return "Hello";
    }

    public static void main(String[] args) {
        String hw = "Hello world!";
        final String h1 = "Hello";
        final String h3 = getH();

        String hw1 = h1 + " world!";
        String hw2 = h2 + " world!";
        String hw3 = h3 + " world!";
        String hw4 = h4 + " world!";

        System.out.println(hw == hw1);
        System.out.println(hw == hw2);
        System.out.println(hw == hw3);
        System.out.println(hw == hw4);
    }

答案:true,true,false,false 说明:局部变量h1被final修饰,意味着h1是常量,同时h1被直接赋值为字符串字面量”Hello”,这样java编译器在编译期就能确定h1的值,从而将h1出现的地方直接替换成字面量”Hello”(类似c/c++用define定义的常量),再联系之前关于字面量会在编译期直接拼接说明,因此代码String hw1 = h1 + “ world!”编译后优化为String hw1 = “Hello world!”,hw、hw1都指向了常量池中的String对象,输出为true。同理h2是静态常量,且是直接字面量赋值方式,h2出现的地方也会在编译后直接被字面量”Hello”替换,最终,hw2也是指向常量池中的String对象,输出为true。

局部变量h3也被final修饰,为常量,但是其是通过方法调用进行赋值的,编译期无法确定其具体值(此时代码都没执行,是无法通过静态分析得到方法的返回值的,即使方法体中只是简单的返回字符串常量,如上述例子),再联系之前关于+的本质分析,因此String hw3 = h3 + “ world!”编译后为String hw3 = (new StringBuilder(String.valueOf(h3))).append(“ world!”).toString(),hw3将指向java堆上的String对象,hw == hw3输出为false。同理,hw4也指向java堆上的String对象,hw == hw4输出为false。

补充知识点

关于String类型变量的赋值,有两种方式: •其一、直接字面量赋值,即String str = “abc”; •其二、new方式赋值,即String str = new String(“abc”);

方式一中,变量str直接指向字符串常量池1中字面量为”abc”的String对象,即指向常量池中的String对象。 方式二中,变量str通过new构造函数String(String original)赋值,即指向java堆中的String对象。该构造函数接收String类型参数,而实参”abc”指向常量池中的String对象。

上面两种给String类型变量赋值的方式,除了它们指向不同的String对象外,其它并没有什么区别。从程序效率的角度看,推荐使用方式一给String类型变量赋值,因为方式二多了一次java堆的String对象分配。

前面说过,字符串字面量直接被看作String类的一个实例,实际是其在编译期就存放在Class文件的常量池中,当Class文件被jvm加载时,其就进入到方法区的运行时常量池中。如果想在运行期间将新的常量加入常量池中,可调用String的intern()方法。 当调用 intern方法时,如果常量池已经包含一个等于此String对象的字符串(用equals(Object)方法确定),则返回常池中的字符串。否则,将此String 对象添加到池中,并返回此String对象的引用。

附反射修改String对象代码:

 public static void main(String[] args) throws NoSuchFieldException, IllegalAccessException {
        String name = "angel";
        String name1 = "angel";

        Field strField = String.class.getDeclaredField("value");
        strField.setAccessible(true);
        char[] data = (char[])strField.get(name);
        data[4] = 'r';
        System.out.println(name);
        System.out.println(name1);
        System.out.println(name == name1);

        strField = String.class.getDeclaredField("count");
        strField.setAccessible(true);
        strField.setInt(name, 10);
        int i = (Integer)strField.get(name);
        System.out.println(i);
        System.out.println(name.length());
    }

 

Java-String之寻根问底的更多相关文章

  1. 从Java String实例来理解ANSI、Unicode、BMP、UTF等编码概念

    转(http://www.codeceo.com/article/java-string-ansi-unicode-bmp-utf.html#0-tsina-1-10971-397232819ff9a ...

  2. Java String.split()小点

    java String.split(); 别的不说,单说其中一个问题,这个函数去切分空字符串时,得到的结果: public static void main(String[] args) {// St ...

  3. Java总结篇系列:Java String

    String作为Java中最常用的引用类型,相对来说基本上都比较熟悉,无论在平时的编码过程中还是在笔试面试中,String都很受到青睐,然而,在使用String过程中,又有较多需要注意的细节之处. 1 ...

  4. java String.split()函数的用法分析

    java String.split()函数的用法分析 栏目:Java基础 作者:admin 日期:2015-04-06 评论:0 点击: 3,195 次 在java.lang包中有String.spl ...

  5. java string类型的初始化

    以下基本上是java string类型最常用的三种方法 new string()就不介绍了  基本等同于第三种 String a;  申明一个string类型的 a,即没有在申请内存地址,更没有在内存 ...

  6. Java String字符串/==和equals区别,str。toCharAt(),getBytes,indexOf过滤存在字符,trim()/String与StringBuffer多线程安全/StringBuilder单线程—— 14.0

    课程概要 String 字符串 String字符串常用方法 StringBuffer StringBuilder String字符串: 1.实例化String对象 直接赋值  String str=& ...

  7. Java String类详解

    Java String类详解 Java字符串类(java.lang.String)是Java中使用最多的类,也是最为特殊的一个类,很多时候,我们对它既熟悉又陌生. 类结构: public final ...

  8. java string,需要进行首字母大写改写

    java string,需要进行首字母大写改写,网上大家的思路基本一致,就是将首字母截取,转化成大写然后再串上后面的,类似如下代码 //首字母大写     public static String c ...

  9. Java String Class Example--reference

    reference:http://examples.javacodegeeks.com/core-java/lang/string/java-string-class-example/ 1. Intr ...

  10. java基础知识回顾之---java String final类普通方法

    辞职了,最近一段时间在找工作,把在大二的时候学习java基础知识回顾下,拿出来跟大家分享,如果有问题,欢迎大家的指正. /*     * 按照面向对象的思想对字符串进行功能分类.     *      ...

随机推荐

  1. ubuntu tengine 安装

    参考文章:http://wangyan.org/blog/install-openssl-from-source.html http://www1.site90.com/Linux/405.html ...

  2. 每天一条linux命令——crontab

    crontab命令被用来提交和管理用户的需要周期性执行的任务,与windows下的计划任务类似,当安装完成操作系统后,默认会安装此服务工具,并且会自动启动crond进程,crond进程每分钟会定期检查 ...

  3. SQL Function(方法)

    1.为什么有存储过程(procedure)还需要(Function) fun可以再select语句中直接调用,存储过程是不行的. 一般来说,过程显示的业务更为复杂:函数比较有针对性. create f ...

  4. Java源代码分析与生成

    源代码分析:可使用ANTLRANTLR是开源的语法分析器,可以用来构造自己的语言,或者对现有的语言进行语法分析. JavaParser 对Java代码进行分析 CodeModel 用于生成Java代码 ...

  5. 修改win8系统中启动管理器的系统引导信息

    最近用某软件做了个启动U盘,软件安装在电脑上,启动盘很快做完了,结果重启电脑的时候发现悲剧,windows启动后会显示出一个系统引导菜单,显示有3秒倒计时但是倒计时结束依然不能自动进入系统.. 然后. ...

  6. C语言和C++中动态申请内存

      在C语言和C++的动态内存的使用方法是不同的,在C语言中要使用动态内存要包含一个头文件即 #include<malloc.h> 或者是#include<stdlib.h>  ...

  7. c#自动更新+安装程序的制作 (转)

    c#自动更新+安装程序的制作 (转)  http://blog.csdn.net/myhuli120/article/details/6927588 一.自动更新的实现 让客户端实现自动更新,通常做法 ...

  8. C#——System.Diagnostics.Process.Start的妙用

    我们经常会遇到在Winform或是WPF中点击链接或按钮打开某个指定的网址, 或者是需要打开电脑中某个指定的硬盘分区及文件夹, 甚至是"控制面板"相关的东西, 那么如何做呢? 答案 ...

  9. Android中通过访问本地相册或者相机设置用户头像

    目前几乎所有的APP在用户注册时都会有设置头像的需求,大致分为三种情况: (1)通过获取本地相册的图片,经过裁剪后作为头像. (2)通过启动手机相机,现拍图片然后裁剪作为头像. (3)在APP中添加一 ...

  10. leetcode第七题Reverse Integer (java)

    Reverse Integer Reverse digits of an integer. Example1: x = 123, return 321 Example2: x = -123, retu ...