string机制

来源:互联网 发布:mac如何玩qq游戏 编辑:程序博客网 时间:2024/06/05 19:27

1.String类概念

(1)String是final的,不可被继承。public final class String。String是的本质是字符数组char[], 并且其值不可改变。private final char value[];

(2)Java运行时会维护一个String Pool(String池)。String池用来存放运行时中产生的各种字符串,并且池中的字符串的内容不重复。而一般对象不存在这个缓冲池,仅仅存在于方法的堆栈区。

(3)创建字符串的方式很多,归纳起来有三类:(1)使用new关键字创建字符串-->String s1 = new String("abc");(2)直接指定-->String s2 = "abc";(3)用串联生成新的字符串-->String s3 = "ab" + "c";

2.String对象创建的机制

原理1:当使用任何方式来创建一个字符串对象s时,Java运行时(运行中JVM)会拿着这个s在String池中找是否存在内容相同的字符串对象,如果不存在,则在池中创建一个字符串s,否则,不在池中添加。例如:String str="abc";这行代码被执行的时候,JAVA虚拟机首先在字符串池中查找是否已经存在了值为"abc"的这么一个对象,它的判断依据是String 类equals(Object obj)方法的返回值。如果有,则不再创建新的对象,直接返回已存在对象的引用;如果没有,则先创建这个对象,然后把它加入到字符串池中,再将它的引用返回。

原理2:Java中,只要使用new关键字来创建对象,则一定会(在堆区或栈区)创建一个新的对象。而不在常量池中创建String对象,只有使用了str.intern(),才会在常量池中创建一个String对象。

原理3:使用直接指定或者使用纯字符串串联来创建String对象,则仅仅会检查维护String池中的字符串,池中没有就在池中创建一个,有则罢了!但绝不会在堆栈区再去创建该String对象。

原理4:使用包含变量的表达式来创建String对象,则不仅会检查维护String池,而且还会在堆栈区创建一个String对象。

最后,有几点问题请大家注意:String a; 与String a=null在作为类变量时候是等价的,在局部变量则不同。null表示一个空引用,String a=null意思是在栈中声明了a,但是这个a没有指向任何地址。此时我们注意到String a在栈中声明了a,但是也没有指向任何地址,但是java的语法检查如果在局部变量中,String a是不能直接使用的,String a=null中的这个a可以直接使用。

3.经典面试题

(1)String s = new String("abc");创建了几个String Object? 答:[两个,pool中1个,heap中1个]

(2)String s0 = new String("abc");String s1 = new String("abc");创建了几个String Object?答:[三个,pool中1个,heap中2个]

(3)解释

涉及概念:字符串池[pool of literal strings]的字符串对象和堆[heap]中的字符串对象。

字符串对象的创建:由于字符串对象的大量使用[它是一个对象,一般而言普通对象总是在heap分配内存],Java中为了节省内存空间和运行时间,在编译阶段就把所有的字符串文字放到一个字符串池[pool of literal strings]中,而运行时字符串池成为常量池的一部分。字符串池的好处,就是该池中所有相同的字符串常量被合并,只占用一个空间。另一种解释:在JAVA虚拟机(JVM)中存在着一个字符串池,其中保存着很多String对象,并且可以被共享使用,因此它提高了效率。由于String类是final的,它的值一经创建就不可改变,因此我们不用担心String对象共享而带来程序的混乱。字符串池由String类维护,我们可以调用intern()方法来访问字符串池。

现在看String s = new String("abc")语句,在执行new String()时,先检查pool中有没有"abc"对象,如果没有,则先在pool中创建一个"abc"对象,再将其复制一份放到heap中,并且把heap中的这个对象的引用交给s持有,这条语句就创建了2个String对象;如果有则将pool中的对象复制一份放到heap中,并且把heap中的这个对象的引用交给s持有,这条语句就创建了1个String对象(也可以理解为:如果有则新创建的"abc"对象将原有pool中的"abc"对象覆盖,这样可以勉强说创建了2个String对象)。

补充:常量池(constant pool)指的是在编译期被确定,并被保存在已编译的.class文件中的一些数据。它包括了关于类、方法、接口等中的常量,也包括字符串常量。jdk编译时会将字符串池并入常量池。

4.举例

  1. public static void main(String[] args) {
  2.     String str_0 = "forrest";
  3.     String str_1 = "forrest";
  4.     if (str_0 == str_1) System.out.println("pool中值创建了一个String对象:/"forrest/", str_0和str_1分别对其进行了引用");
  5.     else System.out.println("Trouble");
  6.     
  7.     String str_2 = "vivian";
  8.     String str_3 = new String("vivian");
  9.     String str_4 = new String("vivian");
  10.     if (str_2 != str_3) System.out.println("str_2是对pool中String对象引用, str_3是对heap中String对象引用, 所以二者不是引用的同一个对象");
  11.     else System.out.println("Trouble");
  12.     if (str_2 != str_4) System.out.println("str_2是对pool中String对象引用, str_4是对heap中String对象引用, 所以二者不是引用的同一个对象");
  13.     else System.out.println("Trouble");
  14.     if (str_3 != str_4) System.out.println("str_3和str_4都是对heap中String对象引用, 但二者引用对象的内存地址不同, 所以二者不是引用的同一个对象");
  15.     else System.out.println("Trouble");
  16.     
  17.     String str_5 = str_3.intern(); //把str_3在heap中的String对象复制到loop中(虽然在String str_3 = new String("vivian")时已经在loop中创建了一份, 但intern()方法会要求重新复制),并将loop中这个对象引用到str_5
  18.     if (str_5 != str_3) System.out.println("str_5是对pool中String对象引用, str_3是对heap中String对象引用, 所以二者不是引用的同一个对象");
  19.     else System.out.println("Trouble");
  20.     if (str_5 == str_2) System.out.println("str_5和str_2都是对loop中某一String对象引用, 所以二者引用的是同一个对象");
  21.     else System.out.println("Trouble");
  22.     
  23.     str_4.intern();
  24.     if (str_4 != str_2) System.out.println("str_4虽然执行了str_4.intern(), 但它的返回值没有赋给str_4, 所以str_4和str_2不是引用的同一个对象");
  25.     else System.out.println("Trouble");
  26.     if (str_4.intern() == str_2) System.out.println("str_4.intern()和str_2都是对loop中某一String对象引用, 所以二者引用的是同一个对象");
  27.     else System.out.println("Trouble");
  28.     if (str_4.intern() != str_4) System.out.println("str_4.intern()是对pool中String对象引用, str_4是对heap中String对象引用, 所以二者不是引用的同一个对象");
  29.     else System.out.println("Trouble");
  30. }
  31. /*
  32.  * 输出为:
  33.  * pool中值创建了一个String对象:"forrest", str_0和str_1分别对其进行了引用
  34.  * str_2是对pool中String对象引用, str_3是对heap中String对象引用, 所以二者不是引用的同一个对象
  35.  * str_2是对pool中String对象引用, str_4是对heap中String对象引用, 所以二者不是引用的同一个对象
  36.  * str_3和str_4都是对heap中String对象引用, 但二者引用对象的内存地址不同, 所以二者不是引用的同一个对象
  37.  * str_5是对pool中String对象引用, str_3是对heap中String对象引用, 所以二者不是引用的同一个对象
  38.  * str_5和str_2都是对loop中某一String对象引用, 所以二者引用的是同一个对象
  39.  * str_4虽然执行了str_4.intern(), 但它的返回值没有赋给str_4, 所以str_4和str_2不是引用的同一个对象
  40.  * str_4.intern()和str_2都是对loop中某一String对象引用, 所以二者引用的是同一个对象
  41.  * str_4.intern()是对pool中String对象引用, str_4是对heap中String对象引用, 所以二者不是引用的同一个对象
  42.  * 
  43.  * */
  1. public static void main(String[] args) {
  2.     String a = "ab";
  3.     String b = "cd";
  4.     String c = "abcd";
  5.     String d = "ab" + "cd"
  6.     
  7.     // 如果d和c指向了同一个对象,则说明d被加入字符串池 
  8.     if (c == d) System.out.println("/"ab/"+/"cd/" 创建的对象 /"加入了/" 字符串池中"); 
  9.     else System.out.println("/"ab/"+/"cd/" 创建的对象 /"没加入/" 字符串池中"); 
  10.     String e = a + "cd"
  11.     // 如果e和c指向了同一个对象,则说明e也被加入了字符串池 
  12.     if (e == c) System.out.println("a +/"cd/" 创建的对象 /"加入了/" 字符串池中"); 
  13.     else System.out.println("a +/"cd/" 创建的对象 /"没加入/" 字符串池中"); 
  14.     String f = "ab" + b; 
  15.     // 如果f和c指向了同一个对象,则说明f也被加入了字符串池 
  16.     if (f == c) System.out.println("/"ab/"+ b 创建的对象 /"加入了/" 字符串池中");  
  17.     else System.out.println("/"ab/"+ b 创建的对象 /"没加入/" 字符串池中"); 
  18.     String g = a + b; 
  19.     // 如果g和c指向了同一个对象,则说明g也被加入了字符串池 
  20.     if (g == c) System.out.println("a + b 创建的对象 /"加入了/" 字符串池中"); 
  21.     else System.out.println("a + b 创建的对象 /"没加入/" 字符串池中"); 
  22. }
  23. /*
  24.  * 输出为:
  25.  * "ab"+"cd" 创建的对象 "加入了" 字符串池中
  26.  * a +"cd" 创建的对象 "没加入" 字符串池中
  27.  * "ab"+ b 创建的对象 "没加入" 字符串池中
  28.  * a + b 创建的对象 "没加入" 字符串池中
  29.  * 
  30.  * */

从第二段代码结果中我们不难看出,只有使用引号包含文本的方式创建的String对象之间使用“+”连接产生的新对象才会被加入字符串池中。对于所有包含new方式新建对象(包括null)的“+”连接表达式,它所产生的新对象都不会被加入字符串池中;对于类似str=1+"AAA"+2+3这种强制类型转化形成的String对象也不会放到pool中的。对此我们不再赘述。因此我们提倡大家用引号包含文本的方式来创建String对象以提高效率,实际上这也是我们在编程中常采用的。

接上一篇博文中那个经典面试问题。这里我用JProfiler对String对象的产生进行了监控。结果表明上篇博文的分析是正确的。

  1. public static void main(String[] args) {
  2.     System.out.println("=====Start========");
  3.     try {
  4.         Thread.sleep(500L*1000L);//停留5分钟,目的是为了方便JProfiler来监控内存。
  5.     } catch (InterruptedException e) {
  6.         e.printStackTrace();
  7.     }
  8.     System.out.println("=====End========");
  9. }
  10. /*
  11.  * 测试结果:String对象数目为:2406
  12.  * */
  1. public static void main(String[] args) {
  2.     System.out.println("=====Start========");
  3.     try {
  4.         String str1 = "abc";
  5.         Thread.sleep(500L*1000L);//停留5分钟,目的是为了方便JProfiler来监控内存。
  6.     } catch (InterruptedException e) {
  7.         e.printStackTrace();
  8.     }
  9.     System.out.println("=====End========");
  10. }
  11. /*
  12.  * 测试结果:String对象数目为:2407
  13.  * */
  1. public static void main(String[] args) {
  2.     System.out.println("=====Start========");
  3.     try {
  4.         String str2 = new String("abc");
  5.         Thread.sleep(500L*1000L);//停留5分钟,目的是为了方便JProfiler来监控内存。
  6.     } catch (InterruptedException e) {
  7.         e.printStackTrace();
  8.     }
  9.     System.out.println("=====End========");
  10. }
  11. /*
  12.  * 测试结果:String对象数目为:2408
  13.  * */
  1. public static void main(String[] args) {
  2.     System.out.println("=====Start========");
  3.     try {
  4.         String str1 = "abc";
  5.         String str2 = new String("abc");
  6.         Thread.sleep(500L*1000L);//停留5分钟,目的是为了方便JProfiler来监控内存。
  7.     } catch (InterruptedException e) {
  8.         e.printStackTrace();
  9.     }
  10.     System.out.println("=====End========");
  11. }
  12. /*
  13.  * 测试结果:String对象数目为:2408
  14.  * */
  1. public static void main(String[] args) {
  2.     System.out.println("=====Start========");
  3.     try {
  4.         String str1 = new String("abc");
  5.         String str2 = new String("abc");
  6.         Thread.sleep(500L*1000L);//停留5分钟,目的是为了方便JProfiler来监控内存。
  7.     } catch (InterruptedException e) {
  8.         e.printStackTrace();
  9.     }
  10.     System.out.println("=====End========");
  11. }
  12. /*
  13.  * 测试结果:String对象数目为:2409
  14.  * */
  1. public static void main(String[] args) {
  2.     System.out.println("=====Start========");
  3.     try {
  4.         String str1 = "abc";
  5.         String str2 = new String("def");
  6.         Thread.sleep(500L*1000L);//停留5分钟,目的是为了方便JProfiler来监控内存。
  7.     } catch (InterruptedException e) {
  8.         e.printStackTrace();
  9.     }
  10.     System.out.println("=====End========");
  11. }
  12. /*
  13.  * 测试结果:String对象数目为:2409
  14.  * */

好了,测试代码就这么多,分析结果很容易得出上篇博文的结论的。

此外:通过String str = new String("abc");创建出的pool中的String对象实际是没有引用的,这种对象很容易造成内存泄漏,java中的gc会自动在String pool中去清理这些没有引用的String对象。所以我们尽量不要使用new String("abc")的方式去创建String对象,事实上,effective java中也鄙视了这种创建方法。

1.intern()定义

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

换一种解释:存在于.class文件中的常量池,在运行期被JVM装载,并且可以扩充。String的intern()方法就是扩充常量池的一个方法:当一个String实例str调用intern()方法时,Java查找常量池中是否有相同Unicode的字符串常量,如果有,则返回其的引用,如果没有,则在常量池中增加一个Unicode等于str的字符串并返回它的引用。换即当调用intern方法时,如果池已经包含一个等于此String对象的字符串(该对象由equals(Object)方法确定),则返回池中的字符串,否则,将此String对象添加到池中,并且返回此String。

2.intern()应用

我们说String str1 = "abc" 和 String str2 = new String("def")都分别在pool中创建了"abc"和"def"对象,String.intern()还能有什么用(pool已经存在与该对象值相等对象,还判断什么)。但我们错了,定义String对象就上面两种方法,但获得String对象的方式却很多:rs.getString(1)、new String("abc")+def、10+"abc"等等这些获得String的方法都没有在loop中创建String对象(而只是在heap中创建了对象)。这样intern()就有了用武之地。
举个例子:

sql脚本为

  1. DROP TABLE IF EXISTS temp;
  2. CREATE TABLE temp (
  3.     id INT(32) AUTO_INCREMENT NOT NULL,
  4.     content VARCHAR(200) NOT NULL,
  5.     PRIMARY KEY (id)
  6. );
  7. INSERT INTO temp (content) VALUES 
  8.     ('forrest test the performance of intern() method of String!! --> 1'),
  9.     ('forrest test the performance of intern() method of String!! --> 1'),
  10.     ... #插入10000个相同的数据
  11.     ('forrest test the performance of intern() method of String!! --> 1');

测试代码为

  1. public static void main(String[] args) {
  2.     Connection conn = null;
  3.     PreparedStatement pstmt = null;
  4.     ResultSet rs = null;
  5.     List<String> cList = new ArrayList<String>();
  6.     String sql = "SELECT content FROM temp";
  7.     try {
  8.         Class.forName("com.mysql.jdbc.Driver");
  9.         conn = DriverManager.getConnection("jdbc:mysql://localhost:3306/testcasetrack""root""");
  10.         pstmt = conn.prepareStatement(sql);
  11.         rs = pstmt.executeQuery();
  12.         while (rs.next()) {
  13.             String contentBean = new String();
  14.          contentBean = rs.getString(1);
  15.             cList.add(contentBean);
  16.         }
  17.     } catch (SQLException e) {
  18.         e.printStackTrace();
  19.     } catch (ClassNotFoundException e) {
  20.         e.printStackTrace();
  21.     } finally {
  22.         try {
  23.             rs.close();
  24.             pstmt.close();
  25.             conn.close();
  26.         } catch (SQLException e) {
  27.             e.printStackTrace();
  28.         }
  29.         System.out.println(Runtime.getRuntime().totalMemory());
  30.         System.out.println(Runtime.getRuntime().freeMemory());
  31.     }
  32. }
  33. /*
  34.  * 测试结果
  35.  * Runtime.getRuntime().totalMemory() :5984256
  36.  * Runtime.getRuntime().freeMemory() :2441160 
  37.  * 
  38.  * */
  1. public static void main(String[] args) {
  2.     Connection conn = null;
  3.     PreparedStatement pstmt = null;
  4.     ResultSet rs = null;
  5.     List<String> cList = new ArrayList<String>();
  6.     String sql = "SELECT content FROM temp";
  7.     try {
  8.         Class.forName("com.mysql.jdbc.Driver");
  9.         conn = DriverManager.getConnection("jdbc:mysql://localhost:3306/testcasetrack""root""");
  10.         pstmt = conn.prepareStatement(sql);
  11.         rs = pstmt.executeQuery();
  12.         while (rs.next()) {
  13.             String contentBean = new String();
  14.          contentBean = rs.getString(1).intern();
  15.             cList.add(contentBean);
  16.         }
  17.     } catch (SQLException e) {
  18.         e.printStackTrace();
  19.     } catch (ClassNotFoundException e) {
  20.         e.printStackTrace();
  21.     } finally {
  22.         try {
  23.             rs.close();
  24.             pstmt.close();
  25.             conn.close();
  26.         } catch (SQLException e) {
  27.             e.printStackTrace();
  28.         }
  29.         System.out.println(Runtime.getRuntime().totalMemory());
  30.         System.out.println(Runtime.getRuntime().freeMemory());
  31.     }
  32. }
  33. /*
  34.  * 测试结果
  35.  * Runtime.getRuntime().totalMemory() :3411968
  36.  * Runtime.getRuntime().freeMemory() :1585368 
  37.  * 
  38.  * */
  1. static String get() {
  2.     return "forrest test the performance of intern() method of String!! --> 1";
  3. }
  4.     
  5. public static void main(String[] args) {
  6.     Connection conn = null;
  7.     PreparedStatement pstmt = null;
  8.     ResultSet rs = null;
  9.     List<String> cList = new ArrayList<String>();
  10.     String sql = "SELECT content FROM temp";
  11.     try {
  12.         Class.forName("com.mysql.jdbc.Driver");
  13.         conn = DriverManager.getConnection("jdbc:mysql://localhost:3306/testcasetrack""root""");
  14.         pstmt = conn.prepareStatement(sql);
  15.         rs = pstmt.executeQuery();
  16.         while (rs.next()) {
  17.             String contentBean = new String();
  18.             contentBean = get();
  19.             cList.add(contentBean);
  20.         }
  21.     } catch (SQLException e) {
  22.         e.printStackTrace();
  23.     } catch (ClassNotFoundException e) {
  24.         e.printStackTrace();
  25.     } finally {
  26.         try {
  27.             rs.close();
  28.             pstmt.close();
  29.             conn.close();
  30.         } catch (SQLException e) {
  31.             e.printStackTrace();
  32.         }
  33.         System.out.println(Runtime.getRuntime().totalMemory());
  34.         System.out.println(Runtime.getRuntime().freeMemory());
  35.     }
  36. }
  37. /*
  38.  * 测试结果
  39.  * Runtime.getRuntime().totalMemory() :3428352
  40.  * Runtime.getRuntime().freeMemory() :1609760 
  41.  * 
  42.  * */

从以上代码的测试结果可以看出:如果db中有10000条相同数据的时候,使用intern()的话,系统会占用更少内存(使用intern()占用的内存/没有使用intern()占用的内存 = 57%);而如果直接利用get()方法将pool中的String对象赋值,系统占用的内存和使用intern()从db中取数据时占用的内存几乎是相同的。

当然,如果我们sql脚本是创建的10000个不同的数据,则上面三段代码测试的系统使用内存情况几乎是相同的(也做过测试。1000个测试数据是用一个循环产生的or利用excel的数字拖拽自增长的方式来产生),这也就印证了我们在前两篇博文中得出的结论。

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

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

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

我们查看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'。



1 0
原创粉丝点击