• Java 由浅及深之 String 对象的创建及堆、栈的解释


    参考文章

     http://www.cnblogs.com/dolphin0520/p/3778589.html (探秘Java中String、StringBuilder以及StringBuffer)

     String str=new String("abc");

      紧接着这段代码之后的往往是这个问题,那就是这行代码究竟创建了几个String对象呢?相信大家对这道题并不陌生,答案也是众所周知的,2个。接下来我们就从这道题展开,一起回顾一下与创建String对象相关的一些JAVA知识。

      我们可以把上面这行代码分成String str、=、"abc"和new String()四部分来看待。String str只是定义了一个名为str的String类型的变量,因此它并没有创建对象;=是对变量str进行初始化,将某个对象的引用(或者叫句柄)赋值给它,显然也没有创建对象;现在只剩下new String("abc")了。那么,new String("abc")为什么又能被看成"abc"和new String()呢?我们来看一下被我们调用了的String的构造器:

      public String(String original) {

      //other code ...

      }

      大家都知道,我们常用的创建一个类的实例(对象)的方法有以下两种:

      使用new创建对象

      调用Class类的newInstance方法,利用反射机制创建对象。

      我们正是使用new调用了String类的上面那个构造器方法创建了一个对象,并将它的引用赋值给了str变量。同时我们注意到,被调用的构造器方法接受的参数也是一个String对象,这个对象正是"abc"。由此我们又要引入另外一种创建String对象的方式的讨论——引号内包含文本

      这种方式是String特有的,并且它与new的方式存在很大区别。

      String str="abc";

      毫无疑问,这行代码创建了一个String对象。

      String a="abc";

      String b="abc";

      那这里呢?答案还是一个。

      String a="ab"+"cd";

      再看看这里呢?答案是三个。有点奇怪吗?说到这里,我们就需要引入对字符串池相关知识的回顾了。

    在JAVA虚拟机(JVM)中存在着一个字符串池,其中保存着很多String对象,并且可以被共享使用,因此它提高了效率。由于String类是final的,它的值一经创建就不可改变,因此我们不用担心String对象共享而带来程序的混乱。字符串池由String类维护,我们可以调用intern()方法来访问字符串池。

      我们再回头看看String a="abc";,这行代码被执行的时候,JAVA虚拟机首先在字符串池中查找是否已经存在了值为"abc"的这么一个对象,它的判断依据是String类equals(Object obj)方法的返回值。如果有,则不再创建新的对象,直接返回已存在对象的引用;如果没有,则先创建这个对象,然后把它加入到字符串池中,再将它的引用返回。因此,我们不难理解前面三个例子中头两个例子为什么是这个答案了。

      对于第三个例子:

      String a="ab"+"cd";

      "ab"和"cd"分别创建了一个对象,它们经过“+”连接后又创建了一个对象"abcd",因此一共三个,并且它们都被保存在字符串池里了。


      现在问题又来了,是不是所有经过“+”连接后得到的字符串都会被添加到字符串池中呢?我们都知道“==”可以用来比较两个变量,它有以下两种情况:

      1) 如果比较的是两个基本类型(char,byte,short,int,long,float,double,boolean),则是判断它们的值是否相等。

      2) 如果表较的是两个对象变量,则是判断它们的引用是否指向同一个对象。

      下面我们就用“==”来做几个测试。为了便于说明,我们把指向字符串池中已经存在的对象也视为该对象被加入了字符串池:

      

    public class StringTest {
    
      public static void main(String[] args) {
    
      String a = "ab";// 创建了一个对象,并加入字符串池中
    
      System.out.println("String a = \"ab\";");
    
      String b = "cd";// 创建了一个对象,并加入字符串池中
    
      System.out.println("String b = \"cd\";");
    
      String c = "abcd";// 创建了一个对象,并加入字符串池中
    
      String d = "ab" + "cd";
    
      // 如果d和c指向了同一个对象,则说明d也被加入了字符串池
    
      if (d == c) {
    
      System.out.println("\"ab\"+\"cd\" 创建的对象 \"加入了\" 字符串池中");
    
      }
    
      // 如果d和c没有指向了同一个对象,则说明d没有被加入字符串池
    
      else {
    
      System.out.println("\"ab\"+\"cd\" 创建的对象 \"没加入\" 字符串池中");
    
      }
    
      String e = a + "cd";
    
      // 如果e和c指向了同一个对象,则说明e也被加入了字符串池
    
      if (e == c) {
    
      System.out.println(" a  +\"cd\" 创建的对象 \"加入了\" 字符串池中");
    
      }
    // 如果e和c没有指向了同一个对象,则说明e没有被加入字符串池
    
      else {
    
      System.out.println(" a  +\"cd\" 创建的对象 \"没加入\" 字符串池中");
    
      }
    
      String f = "ab" + b;
    
      // 如果f和c指向了同一个对象,则说明f也被加入了字符串池
    
      if (f == c) {
    
      System.out.println("\"ab\"+ b   创建的对象 \"加入了\" 字符串池中");
    
      }
    
      // 如果f和c没有指向了同一个对象,则说明f没有被加入字符串池
    
      else {
    
      System.out.println("\"ab\"+ b   创建的对象 \"没加入\" 字符串池中");
    
      }
    
      String g = a + b;
    
      // 如果g和c指向了同一个对象,则说明g也被加入了字符串池
    
      if (g == c) {
    
      System.out.println(" a  + b   创建的对象 \"加入了\" 字符串池中");
    
      }
    
      // 如果g和c没有指向了同一个对象,则说明g没有被加入字符串池
    
      else {
    
      System.out.println(" a  + b   创建的对象 \"没加入\" 字符串池中");
    
      }
    
      }
    
      }
    
      运行结果如下:
    
      String a = "ab";
    
      String b = "cd";
    
      "ab"+"cd" 创建的对象 "加入了" 字符串池中
    
      a  +"cd" 创建的对象 "没加入" 字符串池中
    
      "ab"+ b   创建的对象 "没加入" 字符串池中
    
      a  + b   创建的对象 "没加入" 字符串池中

      从上面的结果中我们不难看出,只有使用引号包含文本的方式创建的String对象之间使用“+”连接产生的新对象才会被加入字符串池中。对于所有包含new方式新建对象(包括null)的“+”连接表达式,它所产生的新对象都不会被加入字符串池中,对此我们不再赘述。因此我们提倡大家用引号包含文本的方式来创建String对象以提高效率,实际上这也是我们在编程中常采用的。


      

      接下来我们再来看看intern()方法,它的定义如下:

      public native String intern();

      这是一个本地方法。在调用这个方法时,JAVA虚拟机首先检查字符串池中是否已经存在与该对象值相等对象存在,如果有则返回字符串池中对象的引用;如果没有,则先在字符串池中创建一个相同值的String对象,然后再将它的引用返回。

      我们来看这段代码:

      

    public class StringInternTest {
    
      public static void main(String[] args) {
    
      // 使用char数组来初始化a,避免在a被创建之前字符串池中已经存在了值为"abcd"的对象
    
      String a = new String(new char[] { 'a', 'b', 'c', 'd' });
    
      String b = a.intern();
    
      if (b == a) {
    
      System.out.println("b被加入了字符串池中,没有新建对象");
    
      } else {
    
      System.out.println("b没被加入字符串池中,新建了对象");
    
      }
    
      }
    
      }
    
      运行结果:
    
      b没被加入字符串池中,新建了对象

      如果String类的intern()方法在没有找到相同值的对象时,是把当前对象加入字符串池中,然后返回它的引用的话,那么b和a指向的就是同一个对象;否则b指向的对象就是JAVA虚拟机在字符串池中新建的,只是它的值与a相同罢了。上面这段代码的运行结果恰恰印证了这一点。


      最后我们再来说说String对象在JAVA虚拟机(JVM)中的存储,以及字符串池与堆(heap)和栈(stack)的关系。我们首先回顾一下堆和栈的区别:

      栈(stack):主要保存基本类型(或者叫内置类型)(char、byte、short、int、long、float、double、boolean)和引用变量,包裹基本数据类型的字面值引用变量类对象的引用变量,数据可以共享,速度仅次于寄存器(register),快于堆。

      基本类型(primitive types), 共有8种,即int, short, long, byte, float, double, boolean, char(注意,并没有string的基本类型)。这种类型的定义是通过诸如int a = 3; long b = 255L;的形式来定义的,称为自动变量。值得注意的是,自动变量存的是字面值,不是类的实例,即不是类的引用,这里并没有类的存在。如int a = 3; 这里的a是一个指向int类型的引用,指向3这个字面值。这些字面值的数据,由于大小可知,生存期可知(这些字面值固定定义在某个程序块里面,程序块退出 后,字段值就消失了),出于追求速度的原因,就存在于栈中。

      栈有一个很重要的特殊性,就是存在栈中的数据可以共享。假设我们同时定义: 
      int a = 3; 
      int b = 3; 
      编译器先处理int a = 3;首先它会在栈中创建一个变量为a的引用,然后查找栈中是否有3这个值,如果没找到,就将3存放进来,然后将a指向3。接着处理int b = 3;在创建完b的引用变量后,因为在栈中已经有3这个值,便将b直接指向3。这样,就出现了a与b同时均指向3的情况。 

      这时,如果再令a=4;那么编译器会重新搜索栈中是否有4值,如果没有,则将4存放进来,并令a指向4;如果已经有了,则直接将a指向这个地址。因此a值的改变不会影响到b的值。 

      特别注意的是,这种字面值的引用类对象的引用不同。假定两个类对象的引用同时指向一个对象,如果一个对象引用变量修改了这个对象的内部状态,那么另一个 对象引用变量也即刻反映出这个变化。相反,通过字面值的引用来修改其值,不会导致另一个指向此字面值的引用的值也跟着改变的情况。如上例,我们定义完a与 b的值后,再令a=4;那么,b不会等于4,还是等于3。在编译器内部,遇到a=4;时,它就会重新搜索栈中是否有4的字面值,如果没有,重新开辟地址存 放4的值;如果已经有了,则直接将a指向这个地址。因此a值的改变不会影响到b的值。

      堆(heap):用于存储对象。

      堆内存用来存放由new创建的对象和数组。  
       
      在堆中分配的内存,由Java虚拟机的自动垃圾回收器来管理。  
       
      在堆中产生了一个数组或对象后,还可以在栈中定义一个特殊的变量,让栈中这个变量的取值等于数组或对象在堆内存中的首地址,栈中的这个变量就成了数组或对象的引用变量

    对于String类型,有一张固定长度的CONSTANT_String_info表用来存储文字字符串值(字符串常量池)(常量池技术,),该表只存储文字字符串值,不存储符号引用。,常量池会储存在方法区(Method Area),而不是堆中,下面详细说明:

    拘留字符串对象
    源代码中所有相同字面值的字符串常量只可能建立唯一 一个拘留字符串对象。 实际上JVM是通过一个记录了拘留字符串引用的内部数据结构来维持这一特性的。在Java程序中,可以调用String的intern()方法来使得一个常规字符串对象成为拘留字符串对象。
    (1)String s=new String("Hello world"); 编译成class文件后的指令(在myeclipse中查看):
    事实上,在运行这段指令之前,JVM就已经为"Hello world"在堆中创建了一个拘留字符串( 值得注意的是:如果源程序中还有一个"Hello world"字符串常量,那么他们都对应了同一个堆中的拘留字符串)。然后用这个拘留字符串的值来初始化堆中用new指令创建出来的新的String对象,局部变量s实际上存储的是new出来的堆对象地址。
    (2)String s="Hello world";
    这跟(1)中创建指令有很大的不同,此时局部变量s存储的是早已创建好的拘留字符串的堆地址。
    java常量池技术  java中的常量池技术,是为了方便快捷地创建某些对象而出现的,当需要一个对象时,就可以从池中取一个出来(如果池中没有则创建一个),则在需要重复创建相等变量时节省了很多时间。常量池其实也就是一个内存空间,常量池存在于方法区中。
    String类也是java中用得多的类,同样为了创建String对象的方便,也实现了常量池的技术

      总结:

      我们查看String类的源码就会发现,它有一个value属性,保存着String对象的值,类型是char[],这也正说明了字符串就是字符的序列。

      当执行String a="abc";时,JAVA虚拟机会在栈中创建三个char型的值'a'、'b'和'c',然后在堆中创建一个String对象,它的值(value)是刚才在栈中创建的三个char型值组成的数组{'a','b','c'},最后这个新创建的String对象会被添加到字符串池中。如果我们接着执行String b=new String("abc");代码,由于"abc"已经被创建并保存于字符串池中,因此JAVA虚拟机只会在堆中新创建一个String对象,但是它的值(value)是共享前一行代码执行时在栈中创建的三个char型值值'a'、'b'和'c'。

      所以一定要分清值,对象,引用: 值和引用都是存在栈中的,具有共享性,对象是存在堆中的。

      说到这里,我们对于篇首提出的String str=new String("abc")为什么是创建了两个对象这个问题就已经相当明了了。

  • 相关阅读:
    383. Ransom Note
    598. Range Addition II
    453. Minimum Moves to Equal Array Elements
    492. Construct the Rectangle
    171. Excel Sheet Column Number
    697. Degree of an Array
    665. Nondecreasing Array
    视频网站使用H265编码能提高视频清晰度吗?
    现阶段的语音视频通话SDK需要解决哪些问题?
    企业远程高清会议平台视频会议系统在手机端使用的必备要求有哪些?
  • 原文地址:https://www.cnblogs.com/alexlo/p/2920209.html
Copyright © 2020-2023  润新知