• 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。

    想要更加深入了解的话,可以参考:科普:为什么 String hashCode 方法选择数字31作为乘子

  • 相关阅读:
    LUA脚本中的方法使用冒号和点,以及调用者使用冒号和点
    Lua类对象的继承
    Lua类对象和类对象的单例
    toLua使用protobuf协议转Lua表
    关于Lua表的弱引用
    Lua-面向对象中函数使用时冒号(:)和点(.)的区别
    Python【day 14-5】sorted filter map函数应用和练习
    Python【day 14-4】sorted filter map+递归文件夹+二分法查找
    Python【day 14-3】二分法查找
    Python【day 14-2】递归遍历文件夹
  • 原文地址:https://www.cnblogs.com/jinggod/p/8425182.html
Copyright © 2020-2023  润新知