String类
一、概述
根据JavaAPI所述,Java 程序中的所有字符串字面值(如 "abc" )都是String类的实例实现;字符串是常量,它们的值在创建之后不能更改,因此它是可以共享的;字符串缓冲区支持可变的字符串;Java 语言提供对字符串串联符号("+")以及将其他对象转换为字符串的特殊支持;字符串串联是通过 StringBuilder(或 StringBuffer)类及其 append 方法实现的。
二、字符串的不可变性
2.1 什么是不可变性?
当一个字符串在堆中或常量池中被创建后,它就不能被更改。如String s = "abcd",当s被重新赋值"abcdel"时,它并不是在"abcd"的原地址上进行了修改,而是重新指向了新创建的"abcdel"地址。
2.2 String怎样实现不可变?
1 public final class String 2 implements java.io.Serializable, Comparable<String>, CharSequence { 3 /** The value is used for character storage. */ 4 private final char value[]; 5 6 /** Cache the hash code for the string */ 7 private int hash; // Default to 0 8 } 9
首先String被final修饰,不可被继承;其底层结构是被final修饰的char数组,设置后就不能被修改。但数组作为引用数据类型,被final修饰只能表示存储在当前变量中的对象引用不会再指示其他对象引用,但这个对象是可以被修改的。
1 final char[] value = {'a', 'b', 'c'}; 2 char[] other = {'o', 't'}; 3 value = other; //报错,value不能再指向其他引用
1 final char[] value = {'a', 'b', 'c'}; 2 value[0] = 'p'; 3 System.out.println(value);//"pbc"
String类是不可变类,不可变类是指类中每个方法都不会改变其对象。因此,其不可变性也是因为构建时,每个方法都没有改动char[ ] value。
2.3 不可变的意义
安全性:Java文档中将String类对象称为不可变字符串,即字符串中的字符不能被修改,因此能够实现多线程环境下的共享。不会出现两个变量p1,p2指向同一字符串s,一个线程使用p1时,另一个线程通过p2修改了s;同时String不能被继承,也就保证它的语义不会被其子类改变语义,即如果有一个String引用,它引用的一定是String对象,不可能是其他对象;作为HashSet、HashMap的键值时,若String可变就会破坏其不可重复性,如若StringBuilder就可能出现相同值的情况,同时提醒我们不能把可变类型作为Set、Map类的键值(在String、StringBuilder、StringBuffer详解)
节约内存和缓存HashCode:在字符串常量池中,每个字符串都是唯一的,可以有多个引用指向一个字符串,String中成员变量hash存储了hashcode,其不可变性使得一个字符串只用计算一次hashcode(堆中情况不同,每new String( )都开辟了一块内存,即使字符串相同)
三、字符串常量池
3.1 常量池的种类
Class文件常量池:.java文件编译后得到.class文件,里面包含了类的全部信息,其中有一项信息为常量池(Constant Pool Table),用于存放编译期生成的各种字面量和符号引用。类还未被加载进内存。
字面量:文本字符串(如"aaa");八种基本类型的值;被final修饰的常量等
符号引用:类和方法的全限定名;字段的名称和描述符;方法的名称和描述符
运行时常量池:.class文件会在类加载后进入方法区中的运行时常量池,方法区是共有的,因此多个类共享一个运行时常量池,也就是说不同类中相同的字符串在常量池中只有一份。
字符串常量池:字符串常量池存在于运行时常量池中,也就是在方法区中,但在JDK1.7之后字符串常量池就被迁入堆中,运行时常量池仍在方法区中。
3.2 intern( )函数
其功能是返回字符串对象的规范化表示形式,它保证了字符串常量池中存在唯一的字符串实例。当调用intern( )函数时,实际操作是:
JDK1.6:先查找字符串常量池中是否存在该字符串常量,如果已经存在,直接返回池中字符串;如果不存在,则在字符串常量池中创建该字符串的副本;
JDK1.7:先查找字符串常量池中是否存在该字符串常量,如果已经存在,直接返回池中字符串;如果不存在,则不会将该字符串拷贝到常量池,而是在常量池中生成一个对堆中该字符串的引用,也就是该字符串对象在堆中,常量池中是堆中对象的引用
3.3 字符串创建方式
3.3.1 字面量赋值
如String s = "abc"就是自面量赋值,其具体过程就是intern( )的操作过程。所有字面值字符串和字符串赋值常量表达式都使用 intern 方法进行操作
3.3.2 new关键字创建
String s = new String("abc"),JVM首先会在字符串常量池中查找是否已经存在该字符串,若不存在,则会创建该字符串;若存在,则不会再在常量池中创建。然后再在堆中创建该字符串,返回堆中字符串的引用
3.4 问题解析
3.4.1 以下代码生成了几个对象?分别在什么位置?(假设字符串常量池为初始化状态)
1 String str0 = new String("idea"); 2 System.out.println(str0.intern() == str0);//false
2个对象,分别在字符串常量池和堆区;
1 String str0 = new String("idea"); 2 String str1 = new String("idea"); 3 System.out.println(str0 == str1); //false
3个对象,当str0创建时分别在字符串常量池和堆区中生成一个对象,str1创建时intern( )时,常量池中已存在"idea",则不会再创建,只在堆区中再创建一个"idea"对象;
1 String str4 = "java"; 2 String str5 = "ja" + "va"; 3 System.out.println(str4 == str5); //true
1个对象,自面量赋值,在字符创常量池中创建一个"java"对象,"ja"+"va"是两个字符串常量相加,因此在编译阶段就被优化为"java";
1 String str4 = "java"; 2 String str6 = "ja"; 3 String str7 = str6 + "va"; 4 System.out.println(str4 == str7); //false
str4与str7共产生了4对象,分别是str4在常量池中创建"java"对象,str6在堆中创建的"java"对象和在常量池中创建的"ja"和"va"对象。因为str6是变量,使用需要地址,不能在编译器进行优化,在运行中str7 = new StringBuffer("ja").append("va").toString,过程中会在堆中创建"java",常量池中生成"ja"和"va"对象,但不会在常量池中生成"java"对象;
1 String str4 = "java"; 2 final String str8 = "ja"; 3 String str9 = str8 + "va"; 4 System.out.println(str4 == str9); //true
str4和Str9共产生1个对象,在常量池中,因为str8由final修饰,可在编译期进行优化为"java";
1 String str10 = new String("ja") + new String("va"); 2 System.out.println(str10.intern() == str10);//false
3个对象,JVM会调用StringBuffer的append方法将"ja"和"va"拼接,过程中会在常量池中生成"ja"和"va"对象,但不会生成"java"对象,会在堆中产生两个"ja"和"va"匿名对象,后被回收,最终产生"java"对象
3.4.2 JDK1.6和JDK1.7处理差异的实例
1 String p = new String("java"); 2 p.intern(); 3 String p2 = "java"; 4 System.out.println(p == p2); 5 6 String p3 = new String("ja") + new String("va"); 7 p3.intern(); 8 String p4 = "java"; 9 System.out.println(p3 == p4);
JDK1.6:两个输出都为false。第一段代码中,在new时就已经在堆区和常量池中创建了"java"对象,不调用intern( )也可以,处理p2时常量池中已经存在该对象,直接返回引用给p2;第二段代码中,处理p3时,在堆中创建了"java"对象,但常量池中没有,调用intern( )在常量池中创建"java"对象,因此两者不等;
JDK1.7:输出false和true。第一段代码和1.6相同;第二段代码中,调用intern( )时,常量池中不存在"java"对象,但JVM并没有在常量池中创建,而是将堆中"java"对象的引用存储下来,在处理p4时,常量池中对象已经存在,就将引用返回给p4,因此p3 == p4