zoukankan      html  css  js  c++  java
  • 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作为乘子

  • 相关阅读:
    【已解决】github中git push origin master出错:error: failed to push some refs to
    好记心不如烂笔头,ssh登录 The authenticity of host 192.168.0.xxx can't be established. 的问题
    THINKPHP 5.0目录结构
    thinkphp5.0入口文件
    thinkphp5.0 生命周期
    thinkphp5.0 架构
    Django template
    Django queryset
    Django model
    Python unittest
  • 原文地址:https://www.cnblogs.com/jinggod/p/8425182.html
Copyright © 2011-2022 走看看