String理解
String是一个字符串类型的类,以前关于String的理解有这么几点:
- String 是final类,不可继承,且其内部一些重要方法被定义为final类型,不可重写
- String 类比较字符串相等时时不能用“ == ”,只能用 "equals"
- 内部实现Serializable接口(支持字符串序列化)和Comparable接口(支持字符串比较大小)。
-
String类对象有两种实例化方式(直接复制 String a="abc";)和构造器复制(String b=new String("b"););
深入解析String#intern
JAVA 语言中有8中基本类型和一种比较特殊的类型String
。这些类型为了使他们在运行过程中速度更快,更节省内存,都提供了一种常量池的概念。常量池就类似一个JAVA系统级别提供的缓存。
8种基本类型的常量池都是系统协调的,String
类型的常量池比较特殊。它的主要使用方法有两种:
- 直接使用双引号声明出来的
String
对象会直接存储在常量池中。 - 如果不是用双引号声明的
String
对象,可以使用String
提供的intern
方法。intern 方法会从字符串常量池中查询当前字符串是否存在,若不存在就会将当前字符串放入常量池中
intern方法 (1.7版本,返回常量池中该字符串的引用)
(1) 当常量池中不存在"abc"这个字符串的引用,将这个对象的引用加入常量池,返回这个对象的引用。
(2) 当常量池中存在"abc"这个字符串的引用,返回这个对象的引用;
接下来我们主要来谈一下String#intern
方法。
/**
* Returns a canonical representation for the string object.
* <p>
* A pool of strings, initially empty, is maintained privately by the
* class <code>String</code>.
* <p>
* When the intern method is invoked, if the pool already contains a
* string equal to this <code>String</code> object as determined by
* the {@link #equals(Object)} method, then the string from the pool is
* returned. Otherwise, this <code>String</code> object is added to the
* pool and a reference to this <code>String</code> object is returned.
* <p>
* It follows that for any two strings <code>s</code> and <code>t</code>,
* <code>s.intern() == t.intern()</code> is <code>true</code>
* if and only if <code>s.equals(t)</code> is <code>true</code>.
* <p>
* All literal strings and string-valued constant expressions are
* interned. String literals are defined in section 3.10.5 of the
* <cite>The Java™ Language Specification</cite>.
*
* @return a string that has the same contents as this string, but is
* guaranteed to be from a pool of unique strings.
*/
public native String intern();
String#intern
方法中看到,这个方法是一个 native 的方法,但注释写的非常明了。“如果常量池中存在当前字符串, 就会直接返回当前字符串. 如果常量池中没有此字符串, 会将此字符串放入常量池中后, 再返回”。
public class HelloWorld{
public static void main(String[] args){
String a="abc";
String b=new String("abc");
b.intern();
}
}
使用intern的好处: 如果有大量重复的字符串生成,使用String的new构造方法创建的话,就会在堆内存中产生大量重复的对象,占用程序内存,使用intern后,如果是重复的String对象都会放到常量池中,节省内存.
jdk1.6和jdk1.7的区别
- 将String常量池 从 Perm 区移动到了 Java Heap区
String#intern
方法时,如果存在堆中的对象,会直接保存对象的引用,而不会重新创建对象。(jdk1.6中,因为常量池在perm区中,java heap的引用和perm的引用不一致,所以会再创建一个新的对象地址值)
到底创建了几个字符串
注: String a="abc"; 其中 abc 会放在常量池中是只编译期生效的. 或者String a="ab"; String b="c"; String c=a+b 所产生的的字符串 abc 也是只有在编译期才会放到常量池,如果是运行期间的话就会放在堆内存中.
相信很多 JAVA 程序员都做做类似 String s = new String("abc")
这个语句创建了几个对象的题目。 这种题目主要就是为了考察程序员对字符串对象的常量池掌握与否。上述的语句中是创建了2个对象,第一个对象是"abc"字符串存储在常量池中,第二个对象在JAVA Heap中的 String 对象。
String str1 = "ABC";
String str2 = new String("ABC");
String str1 = “ABC”;可能创建一个或者不创建对象,如果”ABC”这个字符串在java String池里不存在,会在java String池里创建一个创建一个String对象(“ABC”),然后str1指向这个内存地址,无论以后用这种方式创建多少个值为”ABC”的字符串对象,始终只有一个内存地址被分配,之后的都是String的拷贝,Java中称为“字符串驻留”,所有的字符串常量都会在编译之后自动地驻留。
String str2 = new String(“ABC”);至少创建一个对象,也可能两个。因为用到new关键字,肯定会在heap中创建一个str2的String对象,它的value是“ABC”。同时如果这个字符串再java String池里不存在,会在java池里创建这个String对象“ABC”
String str1 = new String("ABC");
String str2 = new String("ABC");
System.out.println(str1 == str2); //false
String str3 = "ABC";
String str4 = "ABC";
String str5 = "AB" + "C";
System.out.println(str3 == str4); //true
System.out.println(str3 == str5); // true
String a = "ABC";
String b = "AB";
String c = b + "C";
System.out.println( a == c );//false
注意最后一个a与c相等的判断:a、b在编译时就已经被确定了,而c是引用变量,不会在编译时就被确定。运行时b与“C”的拼接是通过StringBuilder(JDK1.5之前是StringBuffer)实现的,最后调用的StringBuilder的toString函数返回一个新的String对象
应用的情况:建议在平时的使用中,尽量使用String = “abcd”;这种方式来创建字符串,而不是String = new String(“abcd”);这种形式,因为使用new构造器创建字符串对象一定会开辟一个新的heap空间,而双引号则是采用了String intern(字符串驻留)进行了优化,效率比构造器高。
看下面程序的运行
public class HelloWorld{
public static void main(String[] args){
String a="abc";
String b="abc";
String c="ab"+"c";
System.out.println(a==b);
System.out.println(a==c);
String d="ab";
String e="c";
System.out.println(a==(d+e));
System.out.println(a==(d+e).intern());
}
}
输出:
true
true
false
true
原因: ==在比较引用对象时,比较的是地址值,如果两个对象的地址值一样,说明是同一个对象.a,b,c 三个字符串在运行期间就都会放到常量池中,指向同一个地址值,所以 a==b, a==c 都是 true.
String d="ab";
String e="c";
( d+e) 是在运行期间创建的,组成的 abc 字符串 会作为String对象存储在堆内存中.然后使用常量池中的abc字符串地址和堆内存中的abc字符串地址对比,地址值不一样,所以 a==(d+e) 就为 false.
通过我们上面的学习,知道intern()方法把一个运行时创建的字符串加到字符串常量池(如果它还没有入池);
(d+e).intern() 方法执行时,会发现常量池中已经有一个字符串为abc了,就会把对应的地址值直接返回.这时 a==(d+e).intern() 比较时,地址值都是一样的,所以为 true;(注意如果是jdk1.6及以下的话,该结果为false,因为intern方法会把string对象引用放在Perm 区,perm区的java heap区的地址值 完全不一样,即使调用String.intern
方法也是没有任何关系的 ).
参照:https://tech.meituan.com/2014/03/06/in-depth-understanding-string-intern.html
String和StringBuilder,StringBuffer
String+和Stringbuffer的速度比较
public class HolloWorld{
public static void main(String[] args){
/* 1 */
String string = "a" + "b" + "c";
/* 2 */
StringBuffer stringBuffer = new StringBuffer();
stringBuffer.append("a");
stringBuffer.append("b");
stringBuffer.append("c");
string = stringBuffer.toString();
}
}
上面的写法中,大家觉得是Sting的拼接速度快还是StringBuffer的拼接速度快呢,其实string+的写法要比stringbuffer快,是因为在编译这段程序的时候,编译器会进行常量优化,它会将a、b、c直接合成一个常量abc保存在对应的class文件当中。
编译后的class文件的反编译代码,如下。
public class HolloWorld
{
public static void main(String[] args)
{
String string = "abc";
StringBuffer stringBuffer = new StringBuffer();
stringBuffer.append("a");
stringBuffer.append("b");
stringBuffer.append("c");
string = stringBuffer.toString();
}
}
可以看出,在编译这个java文件时,编译器已经直接进行了+运算,这是因为a、b、c这三个字符串都是常量,是可以在编译期由编译器完成这个运算的。假设我们换一种写法。
public class HolloWorld{
public static void main(String[] args){
/* 1 */
String a = "a";
String b = "b";
String c = "c";
String string = a + b + c;
/* 2 */
StringBuffer stringBuffer = new StringBuffer();
stringBuffer.append(a);
stringBuffer.append(b);
stringBuffer.append(c);
string = stringBuffer.toString();
}
}
此处的答案貌似应该是stringbuffer更快,因为此时a、b、c都是对象,编译器已经无法在编译期进行提前的运算优化了。
但是,事实真的是这样的吗?
其实答案依然是第一种写法更快,也就是string+的写法更快,这一点可能会有猿友比较疑惑。这个原因是因为string+其实是由stringbuilder完成的,而一般情况下stringbuilder要快于stringbuffer,这是因为stringbuilder线程不安全,少了很多线程锁的时间开销,因此这里依然是string+的写法速度更快。