java基础(五) String性质深入解析

引言

本文将讲解String的几个性质。

一、String的不可变性

对于初学者来说,很容易误认为String对象是可以改变的,特别是+链接时,对象似乎真的改变了。然而,String对象一经创建就不可以修改。接下来,我们一步步 分析String是怎么维护其不可改变的性质

1. 手段一:final类 和 final的私有成员

我们先看一下String的部分源码:

public final class String
    implements java.io.Serializable, Comparable<String>, CharSequence {
    /** The value is used for character storage. */
    private final char value[];

    /** 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类,且3个成员都是私有的,这就意味着String是不能被继承的,这就防止出现:程序员通过继承重写String类的方法的手段来使得String类是“可变的”的情况。

??从源码发现,每个String对象维护着一个char数组 —— 私有成员value。数组value 是String的底层数组,用于存储字符串的内容,而且是 private final ,但是数组是引用类型,所以只能限制引用不改变而已,也就是说数组元素的值是可以改变的,而且String 有一个可以传入数组的构造方法,那么我们可不可以通过修改外部char数组元素的方式来“修改”String 的内容呢?

我们来做一个实验,如下:

public static void main(String[] args) {

        char[] arr = new char[]{‘a‘,‘b‘,‘c‘,‘d‘};
        String str = new String(arr);
        arr[3]=‘e‘;
        System.out.println("str= "+str);
        System.out.println("arr[]= "+Arrays.toString(arr));
    }

运行结果

str= abcd
arr[]= [a, b, c, e]

??结果与我们所想不一样。字符串str使用数组arr来构造一个对象,当数组arr修改其元素值后,字符串str并没有跟着改变。那就看一下这个构造方法是怎么处理的:

public String(char value[]) {
        this.value = Arrays.copyOf(value, value.length);
    }

??原来 String在使用外部char数组构造对象时,是重新复制了一份外部char数组,从而不会让外部char数组的改变影响到String对象。

2. 手段二:改变即创建对象的方法

??从上面的分析我们知道,我们是无法从外部修改String对象的,那么可不可能使用String提供的方法,因为有不少方法看起来是可以改变String对象的,如replace()replaceAll()substring()等。我们以substring()为例,看一下源码:

public String substring(int beginIndex, int endIndex) {
        //........
        return ((beginIndex == 0) && (endIndex == value.length)) ? this
                : new String(value, beginIndex, subLen);
    }

从源码可以看出,如果不是切割整个字符串的话,就会新建一个对象。也就是说,只要与原字符串不相等,就会新建一个String对象

扩展

基本类型的包装类跟String很相似的,都是final类,都是不可改变的对象,以及维护着一个存储内容的private final成员。如 Integer类:

public final class Integer extends Number implements Comparable<Integer> {

     private final int value;
}

二、String的+操作 与 字符串常量池

我们先来看一个例子:

public class MyTest {
    public static void main(String[] args) {

        String s = "Love You";
        String s2 = "Love"+" You";
        String s3 = s2 + "";
        String s4 = new String("Love You");

        System.out.println("s == s2 "+(s==s2));
        System.out.println("s == s3 "+(s==s3));
        System.out.println("s == s4 "+(s==s4));
    }
}

运行结果:

s == s2 ?true
s == s3 ?false
s == s4 ?false

??是不是对运行结果感觉很不解。别急,我们来慢慢理清楚。首先,我们要知道编译器有个优点:在编译期间会尽可能地优化代码,所以能由编译器完成的计算,就不会等到运行时计算,如常量表达式的计算就是在编译期间完成的。所以,s2 的结果其实在编译期间就已经计算出来了,与 s 的值是一样,所以两者相等,即都属于字面常量,在类加载时创建并维护在字符串常量池中。但 s3 的表达式中含有变量 s2 ,只能是运行时才能执行计算,也就是说,在运行时才计算结果,在堆中创建对象,自然与 s 不相等。而 s4 使用new直接在堆中创建对象,更不可能相等。

??那在运行期间,是如何完成String的+号链接操作的呢,要知道String对象可是不可改变的对象。我们使用jad命令 jad MyTest.class 反编译上面例子的calss文件回java代码,来看看究竟是怎么实现的:

public class MyTest
{

    public MyTest()
    {
    }

    public static void main(String args[])
    {
        String s = "Love You";
        String s2 = "Love You";//已经得到计算结果
        String s3 = (new StringBuilder(String.valueOf(s2))).toString();
        String s4 = new String("Love You");
        System.out.println((new StringBuilder("s == s2 ")).append(s == s2).toString());
        System.out.println((new StringBuilder("s == s3 ")).append(s == s3).toString());
        System.out.println((new StringBuilder("s == s4 ")).append(s == s4).toString());
    }
}

??可以看出,编译器将 + 号处理成了StringBuilder.append()方法。也就是说,在运行期间,链接字符串的计算都是通过 创建StringBuilder对象,调用append()方法来完成的,而且是每一个链接字符串的表达式都要创建一个 StringBuilder对象。因此对于循环中反复执行字符串链接时,应该考虑直接使用StringBuilder来代替 + 链接,避免重复创建StringBuilder的性能开销。

字符串常量池

常量池可以参考我上一篇文章,此处不会深入,只讲解与String相关的部分。

??字符串常量池的内容大部分来源于编译得到的字符串字面常量。在运行期间同样也会增加,

String intern():

返回字符串对象的规范化表示形式。
一个初始为空的字符串池,它由类 String 私有地维护。
当调用 intern 方法时,如果池已经包含一个等于此 String 对象的字符串(用 equals(Object) 方法确定),则返回池中的字符串。否则,将此 String 对象添加到池中,并返回此 String 对象的引用。
它遵循以下规则:对于任意两个字符串 s 和 t,当且仅当 s.equals(t) 为 true 时,s.intern() == t.intern() 才为 true。

另外一点值得注意的是,虽然String.intern()的返回值永远等于字符串常量。但这并不代表在系统的每时每刻,相同的字符串的intern()返回都会是一样的(虽然在95%以上的情况下,都是相同的)。因为存在这么一种可能:在一次intern()调用之后,该字符串在某一个时刻被回收,之后,再进行一次intern()调用,那么字面量相同的字符串重新被加入常量池,但是引用位置已经不同。

三、String 的hashcode()方法

??String也是遵守equals的标准的,也就是 s.equals(s1)为true,则s.hashCode()==s1.hashCode()也为true。此处并不关注eqauls方法,而是讲解 hashCode()方法,String.hashCode()有点意思,而且在面试中也可能被问到。先来看一下代码:

public int hashCode() {
        int h = hash;
        if (h == 0 && value.length > 0) {
            char val[] = value;

            for (int i = 0; i < value.length; i++) {
                h = 31 * h + val[i];
            }
            hash = h;
        }
        return h;
    }

##为什么要选31作为乘数呢?

从网上的资料来看,一般有如下两个原因:

  • 31是一个不大不小的质数,是作为 hashCode 乘子的优选质数之一。另外一些相近的质数,比如37、41、43等等,也都是不错的选择。那么为啥偏偏选中了31呢?请看第二个原因。
  • 31可以被 JVM 优化,31 * i = (i << 5) - i。

出处:http://www.cnblogs.com/jinggod/p/8425182.html

文章有不当之处,欢迎指正,你也可以关注我的微信公众号:好好学java,获取优质资源。

原文地址:http://blog.51cto.com/sihai/2108486

时间: 2024-10-13 22:35:32

java基础(五) String性质深入解析的相关文章

java基础(五章)

java基础(五章) 一.        调试 步骤1:设置断点(不能在空白处设置断点) 步骤2:启动调试 步骤3:调试代码(F6单步跳过)笔记本Fn+F6(F5) 步骤4:结束调试 掌握调试的好处? l  很清晰的看到,代码执行的顺序 l  快速定位到异常代码出现的位置,并帮助修改或改进代码 二.     while循环 1.循环的优点? 减少重复代码的编写:程序会更加的简洁 2.语法 while(表达式){ // 1.表达式是[循环条件],结果必须是boolean类型 //2.{}中的代码,

Java基础五

Java基础五 一.成员变量和局部变量 二.static关键字 三.成员变量和静态变量区别 四.main函数 五.静态函数什么时候用 六.静态代码块 七.构造代码块 构造代码块先于构造函数执行

关于Java基础知识 String StringBuffer StringBuilder三者的区别

Java基础中String StringBuffer StringBuilder 以下介绍 相同点:String,StringBuffer,StringBuilder最终底层存储与操作的都是char数组,StringBuffer和StringBuilder都继承了AbstractStringBuilder 不同点:String:char数组是final的,不可变,修改String时实际上是new一个新String对象返回,线程安全,频繁的增删操作时不建议使用 StringBuffer:线程安全(

06. Java基础之String

      通过学习string源码,可以知道String类其实是通过char数组来保存字符串的.String类是final类,也即意味着String类不能被继承,并且它的成员方法都默认为final方法. 一. toString Object中有个方法叫toString,所有的子类都可以重写这个方法.System.out.println(xx),括号里面的"xx"如果不是String类型的话,就自动调用xx的toString()方法.然后toString的基类实现是:该方法返回的是该J

【转载】Java基础之String中equals,声明方式,等大总结

转载请注明出处:http://blog.csdn.net/dmk877/article/details/49420141 无论你是一个编程新手还是老手,提到String你肯定感觉特别熟悉,因为String类我们在学习java基础的时候就已经学过,但是String类型有我们想象的那么简单吗?其实不然,String类型的知识点还是比较多的.今天就和大家来一起讨论一下,关于String的一些容易让人疑惑的地方,废话不多说进入正题...如有谬误请批评指正,如果有疑问请留言.我会在第一时间修改或回答 通过

Java基础笔记-String类

String 类(被final修饰) 字符串是一种特殊的对象,一旦字符串被初始化就不可以被改变了.(内容不变) 例如: String  s = “abc”; String  s1 = new String(“abc”); s在内存中有一个对象, s代表的是一个类类型变量,”abc”是一个对象. s1在内存中有两个对象,分别是new出来的和:  “abc” . s == s1;的结果是false.   因为s和s1它们所对应的地址不同,比较的两个地址,s和s1中存储的地址数值是不同的.因此是fal

3.Java基础:String对象的创建和使用

一.常用的创建方式 String s1="abc": String s2="abc": s1==s2    ==> true 解析:s1和s2指向的是同一个字符串池地址 二.不常用的创建方式 String s1=new String("abc"): String s2=new String("abc"):: s1==s2    ==> false 解析:s1和s2指向的是不同的字符串池地址,new的时候会重新创建一

java基础五 [数字与静态](阅读Head First Java记录)

本章主要讲了静态变量.静态方法,final关键词.以及介绍了怎么对数字和日期进行格式化输出.这里对这些内容进行了整理.本章还介绍了java.util.Date和java.util.Calendar来操作日期,但是这个直接看API就比较清楚,这里就不写了. static static标记过的,与类的实例对象无关 静态方法: 使用static关键词来标记静态方法.有些方法不需要用到类的实例变量,静态变量的代表“一种不依靠实例变量也就不需要对象的行为”.Math就是一个静态方法 调用静态方法:类名.方

Java基础之String类的细节问题

本文转载自http://sarin.iteye.com/blog/603684/ 先来看一个例子,代码如下: Java代码   public class Test { public static void main(String[] args) { String str = "abc"; String str1 = "abc"; String str2 = new String("abc"); System.out.println(str ==