zoukankan      html  css  js  c++  java
  • JDK源码分析之String篇

    ------------------------------String在内存中的存储情况(一下内容摘自参考资料1)-----------------------------------

    前提:先了解下什么是声明,什么时候才算是产生了对象实例

    其中x并未看到内存分配,变量在使用前必须先声明,再赋值,然后才可以使用。java基础数据类型会用对应的默认值进行初始化

    一、首先看看Java虚拟机JVM的内存块及其变量、对象内存空间是怎么存储分配的

           1、栈:存放基本数据类型及对象变量的引用,对象本身不存放于栈中而是存放于堆中

                 1)、基础类型 byte (8位)、boolean (1位)、char (16位)、int (32位)、short (16位)、float (32位)、double (64位)、long (64位)

                 2)、java代码作用域中定义一个变量时,则java就在栈中为这个变量分配内存空间,当该变量退出该作用域时,java会自动释放该变量所占的空间

           2、堆:new操作符的对象

                 1)、new创建的对象和数组

                 2)、在堆中分配的内存,由Java虚拟机的自动垃圾回收器来管理

           3、静态域:static定义的静态成员变量

           4、常量池:存放常量

    二、Java String类型

            Java中String不是基本数据类型,而是一种特殊的类。String代表的是不可变的字符序列,为不可变对象,一旦被创建,就不能修改它的值,对于已经存在的String对象的修改都是重新创建一个新的对象,然后把新的值保存进去

    三 、String实例代码分析

     1 package terry.java.base;
     2 
     3 public class StringTest {
     4      public static void main(String[] args) {
     5       String a = "hello";
     6       String b = "hello";
     7       
     8       String newA = new String("hello");
     9       String newB = new String("hello");
    10       
    11       System.out.println("****** Testing Object == ******");
    12       System.out.println("a==b ? :" + (a==b));
    13       System.out.println("newA==newB ? :" +(newA==newB));
    14       System.out.println("a==newA ? :" + (a==newA));
    15       
    16       System.out.println("***** Testing String Object intern method******");
    17       System.out.println("a.intern()==b.intern() ? : " + (a.intern()==b.intern()));
    18       System.out.println("newA.intern()==newB.intern() ? :" + (newA.intern()==newB.intern()));
    19       System.out.println("a.intern()==newA.intern() ? :" + (a.intern()==newA.intern()));
    20       System.out.println("a=a.intern() ? :" + (a==a.intern()));
    21       System.out.println("newA==newA.intern() ? : " + (newA==newA.intern()));
    22       
    23       System.out.println("****** Testing String Object equals method******");
    24       System.out.println("equals() method :" + a.equals(newA));
    25       
    26       String c = "hel";
    27       String d = "lo";
    28       final String finalc = "hel";
    29       final String finalgetc = getc();
    30       
    31       System.out.println("****** Testing Object splice ******");
    32       System.out.println("a=="hel"+"lo" ? :" + (a=="hel"+"lo"));
    33       System.out.println("a==c+d ? : " + (a==c+d));
    34       System.out.println("a==c+"lo" ? : " + (a==c+"lo"));
    35       System.out.println("a==finalc+"lo" ? :" + (a==finalc+"lo"));
    36       System.out.println("a==finalgetc+"lo" ? :" + (a==finalgetc+"lo"));
    37       
    38      }
    39      private static String getc(){
    40       return "hel";
    41      } 
    42 }
    View Code

     

    Run As Java Application -- 输出结果:

     1 ****** Testing Object == ******
     2 a==b ? :true
     3 newA==newB ? :false
     4 a==newA ? :false
     5 ***** Testing String Object intern method******
     6 a.intern()==b.intern() ? : true
     7 newA.intern()==newB.intern() ? :true
     8 a.intern()==newA.intern() ? :true
     9 a==a.intern() ? :true
    10 newA==newA.intern() ? : false
    11 ****** Testing String Object equals method******
    12 equals() method :true
    13 ****** Testing Object splice ******
    14 a=="hel"+"lo" ? :true
    15 a==c+d ? : false
    16 a==c+"lo" ? : false
    17 a==finalc+"lo" ? :true
    18 a==finalgetc+"lo" ? :false
    View Code

    内存分析:

    上述各个变量及引用在JVM分配的内存情况

    String类型对象实例直接赋值和new操作符产生的结果在JVM内存分配过程是不同的,如下注释说明

    1 String a = "hello"; //先在栈中创建一个对String类的对象引用变量a,然后通过符号引用去字符串常量池里找有没有"hello",如果没有,则将"hello"存放进字符串常量池 ,并令a指向"hello",如果已经有"hello"则直接将a指向"hello"    -->  产生1个对象及1个引用
    2 String b = "hello"; //先在栈中创建一个对String类的对象引用变量b,然后通过符号引用去字符串常量池里找有没有"hello",因为之前在常量池中已经有"hello",所以直接将b指向"hello"    -->  因为不需要在常量池产生"hello",所以只是在栈中产生1个引用
    3 String newA = new String("hello"); //先在栈中创建一个对String类的对象引用变量newA,然后new()操作会在heap堆中产生一个新的对象"hello",并将newA指向堆中的"hello",同时检查String pool常量池中是否有对象"hello",如果没有也产生一个对象"hello",如果有则不产生,因为这里之前已经在常量池中产生过了,所以   -->  只需要产生1个对象及1个引用
    4 String newB = new String("hello");  //因为new每次都会保证在heap堆内存中产生新的对象,并将栈中的引用指向对应的堆中的地址,所以此语句同上一条的处理
    View Code

    String常量+的拼接 及 String常量与引用实例+的拼接 的区别 

    1 System.out.println("a=="hel"+"lo" ? :" + (a=="hel"+"lo"));  //JVM对于字符串常量的"+"号连接,在程序编译期,JVM就将常量字符串的"+"连接优化为连接后的值,因此"hel" + "lo"优化后完全等同于"hello"
    2 System.out.println("a==c+d ? : " + (a==c+d)); 
    3 System.out.println("a==c+"lo" ? : " + (a==c+"lo"));  //JVM对于字符串引用,由于在字符串的"+"连接中,有字符串引用存在,而引用的值在程序编译期是无法确定的,所以c+"lo"实际是c在栈中保存的地址+字符串"lo"于常量池中指向的地址 所指向的在堆中新分配的一块内存空间
    4 System.out.println("a==finalc+"lo" ? :" + (a==finalc+"lo"));  //对于final修饰的变量,它在编译时被解析为常量值的一个本地拷贝存储到自己的常量 池中或嵌入到它的字节码流中,在编译期就已经确定了内存空间地址,所以此类似于2个字符串常量的+
    5 System.out.println("a==finalgetc+"lo" ? :" + (a==finalgetc+"lo"));
    View Code

    关于String对象的intern()方法的说明

    一个初始时为空的字符串池,它由类 String 私有地维护

    当调用 intern 方法时,如果池已经包含一个等于此 String 对象的字符串(该对象由 equals(Object) 方法确定),则返回池中的字符串。否则,将此String 对象添加到池中,并且返回此String 对象的引用,因此a.intern(),b.intern(),newA.intern(),newB.intern()隐含的各自在栈中分配了各自的内存区域,同时都将栈中的应用全部指向了String pool常量池中的同一块区域"hello"

    -----------------------------------------------Thinking in Java 读书笔记--------------------------------------------

    《Java编程思想<第四版>》-第13章

    本章第一小节标题为“不可变String”,第一句话为“String对象时不可变的”。String类中任何一个看起来会修改String值的方法,实际上都是创建了一个全新的String对象,以包含修改后的字符串内容,原来的String对象丝毫未动。

    给Java初学者举个栗子,有基础的直接跳过:

    1 String s = "ABCabc";
    2 System.out.println("s = " + s);
    3  
    4 s = "123456";
    5 System.out.println("s = " + s);
    View Code

    打印结果是:

    s = ABCabc
    s = 123456
    View Code

    貌似s的值被改变了,改变的只是s的这个引用,本来s该引用指向了常量池中的“ABCabc”,后来指向了常量池中的“123456”。结合上面的内存模型,常量池中有两个常量“ABCabc”、“123456”,栈中只有一个引用s,本来这个s指向“ABCabc”,后来被赋值给了“123456”,图就不画了,还不明白,就别往下看了,看一点更基础的比较好。

    来看下String的源代码:

    public final class String
        implements java.io.Serializable, Comparable<String>, CharSequence {
        /** The value is used for character storage. */汉化一下:这个value是用来装char的.明白了吧,String其实是char数组的包装类。
    //而这个数组是final类型的,不可能指向别的对象,但是可以改,这个请耐心看下去
    private final char value[]; /** Cache the hash code for the string */缓存hashCode private int hash; // Default to 0 }

    源码中String的本质是一个final类型的char数组,既然是final类型,那个该数组引用value就不允许再指向其他对象了,因此只从类的设计角度讲:如果jdk源码中并没有提供对value本身的修改,那么理论上来讲String是不可变的。

    StringBuilder的故事

    大家都知道String的有两个关系很亲近的小伙伴:StringBuffer、StringBuilder,其中的区别大家可以看源代码,StringBuffer对每个方法(除了构造函数)都用了同步,StringBuilder就内向多了。

    大家还知道java中是不支持运算符重载的,但是有且仅有两个例外:+、+=对String进行的重载。

    +、+=被重载了,+用来连接String操作:就是说 “a" + "b" + ”c“= ”abc“;

    上文书说道:

    String是不可变的,即是说String a = "a" ; a + "b" 之后,a还是”a“,这个”ab“实际上又生成了一个新的String对象。将这个赋值语句剖析一下:

    "a" + "b" + "c":先计算前两个  "a" + "b":这时内存中其实有四个字符串,”a“,"b","c","ab",然后再+”c“,这是内存中有五个String:”a“,"b","c","ab",”abc“。

    真的是这样吗?

    可以通过javap来反编译上面的赋值语句,会发现,在编译本条语句时,编译器会自作主张的引入了StringBuilder,并调用了StringBuilder.append方法,这样就不用再生成多余的字符串了;

    因此在用StringBuilder进行append操作时候,千万不要使用append("a" + "b")这样的操作,因为酱,编译器会为你另外的创建一个StringBuilder对象来处理括号里的字符串操作。

    toString的故事,无意识的递归:

    toString方法里面谨慎返回this,可能因此无限递归

    this遇到+“”时候,会将this转换成String,怎么转换呢?通过调用toString方法的好了,无限递归,栈溢出。

    String真的不可变吗?

    从上文可知String的成员变量是private final 的,也就是初始化之后不可改变。那么在这几个成员中, value比较特殊,因为他是一个引用变量,而不是真正的对象。value是final修饰的,也就是说final不能再指向其他数组对象,那么我能改变value指向的数组吗? 比如将数组中的某个位置上的字符变为下划线“_”。 至少在我们自己写的普通代码中不能够做到,因为我们根本不能够访问到这个value引用,更不能通过这个引用去修改数组。 那么用什么方式可以访问私有成员呢? 没错,用反射, 可以反射出String对象中的value属性, 进而改变通过获得的value引用改变数组的结构。下面是实例代码: 

        //创建字符串"Hello World", 并赋给引用s
        String s = "Hello World"; 
         
        System.out.println("s = " + s); //Hello World
         
        //获取String类中的value字段
        Field valueFieldOfString = String.class.getDeclaredField("value");
         
        //改变value属性的访问权限
        valueFieldOfString.setAccessible(true);
         
        //获取s对象上的value属性的值
        char[] value = (char[]) valueFieldOfString.get(s);
         
        //改变value所引用的数组中的第5个字符
        value[5] = '_';
         
        System.out.println("s = " + s);  //Hello_World
    }
    View Code

    打印结果为: s = Hello World
    s = Hello_World

    在这个过程中,s始终引用的同一个String对象,但是再反射前后,这个String对象发生了变化, 也就是说,通过反射是可以修改所谓的“不可变”对象的。但是一般我们不这么做。这个反射的实例还可以说明一个问题:如果一个对象,他组合的其他对象的状态是可以改变的,那么这个对象很可能不是不可变对象。例如一个Car对象,它组合了一个Wheel对象,虽然这个Wheel对象声明成了private final 的,但是这个Wheel对象内部的状态可以改变, 那么就不能很好的保证Car对象不可变。 

    常用方法:

        public char charAt(int index) {
            if ((index < 0) || (index >= value.length)) {
                throw new StringIndexOutOfBoundsException(index);
            }
            return value[index];//先判断越界,然后直接返回合法值
        }
    CharAt()
        public boolean isEmpty() {
            return value.length == 0;
        }
    isEmpty()
        public int length() {
            return value.length;
        }
    length()

     从源码的实现来看,length() == 0 和 isEmpty()效率是一样的。

        public boolean equals(Object anObject) {
            if (this == anObject) {
                return true;
            }//重写了Object的equals方法,判断字符串内容
            if (anObject instanceof String) {
                String anotherString = (String) anObject;
                int n = value.length;
                if (n == anotherString.value.length) {
                    char v1[] = value;
                    char v2[] = anotherString.value;
                    int i = 0;
                    while (n-- != 0) {
                        if (v1[i] != v2[i])
                                return false;
                        i++;
                    }
                    return true;
                }
            }
            return false;
        }
    equals()
        public boolean contentEquals(StringBuffer sb) {
            synchronized (sb) {
                return contentEquals((CharSequence) sb);
            }
        }
    contentEquals(StringBuilder sb)
        public boolean contentEquals(CharSequence cs) {
            if (value.length != cs.length())
                return false;
            // Argument is a StringBuffer, StringBuilder
            if (cs instanceof AbstractStringBuilder) {
                char v1[] = value;
                char v2[] = ((AbstractStringBuilder) cs).getValue();
                int i = 0;
                int n = value.length;
                while (n-- != 0) {
                    if (v1[i] != v2[i])
                        return false;
                    i++;
                }
                return true;
            }
            // Argument is a String
            if (cs.equals(this))
                return true;
            // Argument is a generic CharSequence
            char v1[] = value;
            int i = 0;
            int n = value.length;
            while (n-- != 0) {
                if (v1[i] != cs.charAt(i))
                    return false;
                i++;
            }
            return true;
        }
    contentEquals(CharSequence cs)

     contentEquals(charSequence cs)参数可以是StringBuilder、StringBuffer、String以及CharSequence,contentEquals(StringBuilder sb)只是为了保证sb的一致性,在外面加了互斥锁。equals方法也只是contentEquals的一种情况的实现,完全可以用contentEquals(cs)来取代。

        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);//返回了新的string
        }
    concat(String str)

    concat方法可以看出,最后返回的其实是一个新new的string。并不对原value内容进行改动。

    同样的:

     public String substring(int beginIndex) {
            if (beginIndex < 0) {
                throw new StringIndexOutOfBoundsException(beginIndex);
            }
            int subLen = value.length - beginIndex;
            if (subLen < 0) {
                throw new StringIndexOutOfBoundsException(subLen);
            }
            return (beginIndex == 0) ? this : new String(value, beginIndex, subLen);
        }
    substring

    这两个方法很好用

        public boolean startsWith(String prefix, int toffset) {
            char ta[] = value;
            int to = toffset;
            char pa[] = prefix.value;
            int po = 0;
            int pc = prefix.value.length;
            // Note: toffset might be near -1>>>1.
            if ((toffset < 0) || (toffset > value.length - pc)) {
                return false;
            }
            while (--pc >= 0) {
                if (ta[to++] != pa[po++]) {
                    return false;
                }
            }
            return true;
        }
    startsWith
        public boolean endsWith(String suffix) {
            return startsWith(suffix, value.length - suffix.value.length);
        }
    endsWith

     endsWith是用startsWith实现的,只看名字有点搞笑。

    --------------------------------------未完成,但是不想写了,觉得有点浪费时间---------------------------------------

    参考材料:

    http://blog.csdn.net/yihuiworld/article/details/13511925

    http://www.2cto.com/kf/201401/272974.html#comment_iframe

  • 相关阅读:
    svn cleanup failed–previous operation has not finished 解决方法
    开源SNS社区系统推荐
    从网络获取图片本地保存
    MS SQL Server 数据库连接字符串
    KeepAlive
    Configure Git in debian
    sqlserver query time
    RPi Text to Speech (Speech Synthesis)
    SQL Joins with C# LINQ
    search or reseed identity columns in sqlserver 2008
  • 原文地址:https://www.cnblogs.com/huntfor/p/3909059.html
Copyright © 2011-2022 走看看