用linux做网站,软件开发培训学校杭州,wordpress优化版,学校网站模板 中文文章目录 String一、String 概述1、基本特性2、不可变性3、String、StringBuilder、StringBuffer 二、字符串 创建与内存分配1、字面量 / 双引号2、new关键字3、StringBuilder.toString()4、intern() 方法5、小结 三、字符串 拼接1、常量常量2、变量 拼接3、final变量 拼接4、拼… 文章目录 String一、String 概述1、基本特性2、不可变性3、String、StringBuilder、StringBuffer 二、字符串 创建与内存分配1、字面量 / 双引号2、new关键字3、StringBuilder.toString()4、intern() 方法5、小结 三、字符串 拼接1、常量常量2、变量 拼接3、final变量 拼接4、拼接小结5、拼接 vs append拼接6、StringBuilder的扩容7、优化小结 四、new String() 会创建几个对象1、单独new2、常量 拼接 new3、new 拼接 new 五、intern()1、intern() 的作用2、案例分析1案例12案例23案例3 3、intern() 的效率空间角度4、运行时内存案例 String
一、String 概述
1、基本特性
public final class String implements java.io.Serializable, ComparableString, CharSequence {// jdk1.8及之前private final char value[];public String() {this.value .value;}
}String 实现了 Serializable 接口表示字符串是支持序列化的。String 实现了 Comparable 接口表示 String 可以比较大小。String 声明为 final 的不可被继承一旦被创建就不会改变对字符串的操作只是产生了一个新的字符串。String 在 jdk8及以前 定义了 final char[] value 存储字符串数据jdk9时 改为 final byte[] value
2、不可变性
String 声明为 final 的一旦被创建就不会改变。String的每次操作都是生成一个新的对象不改变原来的对象。
例1重新赋值
Test
public void test1() {String s1 abc;String s2 s1;System.out.println(s1.hashCode()); // 96354System.out.println(s2.hashCode()); // 96354// 不会改变原来的对象abc只是新生成一个对象hello并指向新对象s2 hello;System.out.println(s1.hashCode()); // 96354System.out.println(s2.hashCode()); // 99162322System.out.println(s1); // abcSystem.out.println(s2); // hello
}例2拼接操作
Test
public void test2() {String s1 abc;String s2 s1 def;System.out.println(s1); // abcSystem.out.println(s2); // abcdef
}例3replace() 方法
Test
public void test3() {String s1 abc;String s2 s1.replace(a, m);System.out.println(s1); // abcSystem.out.println(s2); // mbc
}例4方法参数传递
Test
public void test4() {String str old;char[] ch {t, e, s, t};change(str, ch);System.out.println(str); // oldSystem.out.println(ch); // best
}public void change(String str, char ch[]) {// 拼接和replace同理str new;ch[0] b;
}3、String、StringBuilder、StringBuffer
可变性 String 使用字符数组 private final char value[] 保存字符串因此String不可变。 StringBuilder 与 StringBuffer 继承于 AbstractStringBuilder使用字符数组 char[] value 保存字符串因此这两种对象都是可变的。
线程安全性
StringString对象是不可变的一旦创建后其内容不能更改线程安全。StringBuffer加了 synchronized 同步锁线程安全。StringBuilder非线程安全。
性能
String每次对 String 类型进行改变的时候都会生成一个新的String对象。StringBuffer每次都会对StringBuffer对象本身进行操作而不是生成新的对象并改变对象引用。StirngBuilder没有加锁操作比 StringBuffer 能获得 10%~15% 左右的性能提升。
使用场景
操作少量数据String单线程操作大量数据StringBuilder多线程操作大量数据StringBuffer
二、字符串 创建与内存分配
1、字面量 / 双引号 直接由双引号给出声明的字符串存储在字符串常量池中并且相同的字符串只会存在一份 public static void test() {String str ab;
}0 ldc #2 ab
2 astore_0
3 return使用双引号创建字符串时JVM会先去常量池中查找是否存在这个字符串对象。
不存在在 字符串常量池 创建这个字符串对象并返回地址。存在直接返回 字符串常量池 中 字符串对象的地址。
2、new关键字 new 关键字声明的字符串先在堆内存中创建一个字符串对象new然后在字符串常量池中创建一个字符串常量ldc。 public static void test() {String s1 new String(ab);String s2 ab;
}0 new #3 java/lang/String3 dup4 ldc #2 ab6 invokespecial #4 java/lang/String.init : (Ljava/lang/String;)V9 astore_0
10 ldc #2 ab
12 astore_1
13 return使用 new 创建字符串时JVM也会先去常量池中查找是否存在这个字符串对象。
不存在先在堆内存中创建一个字符串对象然后在字符串常量池中创建一个字符串常量。存在直接在堆内存中创建另一个字符串对象。
注意最后返回的是堆内存中字符串对象的地址不是常量池中的字符串对象的地址。
public static void test() {String s1 new String(ab);String s2 ab;System.out.println(s1 s2); // false
}3、StringBuilder.toString()
从下面的源码可以看到StringBuilder的toString()其实会new一个String对象
public final class StringBuilder extends AbstractStringBuilder implements Serializable, CharSequence {Overridepublic String toString() {// Create a copy, dont share the arrayreturn new String(value, 0, count);}
}需要注意的是StringBuilder.toString() 不会在常量池中创建对象下面写个例子分析一下。
public static void test() {StringBuilder stringBuilder new StringBuilder(a);stringBuilder.append(b);String str stringBuilder.toString();
}0 new #5 java/lang/StringBuilder3 dup4 ldc #6 a6 invokespecial #7 java/lang/StringBuilder.init : (Ljava/lang/String;)V9 astore_0
10 aload_0
11 ldc #8 b
13 invokevirtual #9 java/lang/StringBuilder.append : (Ljava/lang/String;)Ljava/lang/StringBuilder;
16 pop
17 aload_0
18 invokevirtual #10 java/lang/StringBuilder.toString : ()Ljava/lang/String;
21 astore_1
22 return可以看到没有出现 ldc #x ab可见StringBuilder.toString() 只在堆内存创建了一个字符串并没有放到字符串常量池
4、intern() 方法 intern()判断常量池中是否存在该字符串存在则返回常量池中的地址不存在则在常量池中加载一份并返回地址 Test
public void test() {String s1 nb;String s2 n;String s3 s2 b;String s4 s3.intern(); // 常量池中存在返回常量池中的地址System.out.println(s1 s3); // falseSystem.out.println(s1 s4); // true
}关于intern()方法下面会详细展开。
5、小结
什么情况下字符串会被放入 字符串常量池 呢
直接由双引号给出声明的字符串会直接放在字符串常量池中。使用new创建的字符串也会有一份放在字符串常量池中。调用intern()方法的字符串也会被放到字符串常量池中。
注意StringBuilder.toString()生成的字符串是不会放到字符串常量池中的只会在堆中创建一份。
三、字符串 拼接
1、常量常量 场景1常量与常量拼接拼接结果在字符串常量池原理是编译期优化 public class AppendTest {public void test() {String s1 a b c;String s2 abc;System.out.println(s1 s2); // true}
}从 class文件 的 反编译结果 可以看出编译器做了优化将 a b c 优化成了 abc 0 ldc #2 abc2 astore_03 ldc #2 abc5 astore_16 getstatic #3 java/lang/System.out : Ljava/io/PrintStream;9 aload_0
10 aload_1
11 if_acmpne 18 (7)
14 iconst_1
15 goto 19 (4)
18 iconst_0
19 invokevirtual #4 java/io/PrintStream.println : (Z)V
22 return从 IDEA 的 AppendTest.class 也可以直接看出来
public class AppendTest {public AppendTest() {}public void test() {String s1 abc; // 显示 String s1 abc; 说明做了代码优化String s2 abc;System.out.println(s1 s2);}
}2、变量 拼接 场景2拼接中只要有一个是变量拼接结果就在堆中原理是StringBuilder的append操作。 public void test2() {String s1 n;String s2 b;String s3 nb;String s4 n b; // 编译期优化String s5 s1 b;String s6 n s2;String s7 s1 s2;System.out.println(s3 s4); // trueSystem.out.println(s3 s5); // falseSystem.out.println(s3 s6); // falseSystem.out.println(s3 s7); // falseSystem.out.println(s5 s6); // falseSystem.out.println(s5 s7); // falseSystem.out.println(s6 s7); // false// 这里使用intern()会返回常量池中nb的地址并赋给s8这里先了解具体用法后续会详细展开String s8 s7.intern();System.out.println(s3 s8); // true
}下面我们从 class文件 的 反编译结果 进行分析 0 ldc #5 n2 astore_13 ldc #6 b5 astore_26 ldc #7 nb8 astore_39 ldc #7 nb11 astore 4s4之前都是【例1】的内容这里就不赘述了主要看一下 s5、s6、s7 这三行
可以看出都是先 new 了一个 StringBuilder 对象然后使用 append() 拼接最后调用了 toString() 创建 String对象 并赋值 13 new #8 java/lang/StringBuilder16 dup17 invokespecial #9 java/lang/StringBuilder.init : ()V20 aload_121 invokevirtual #10 java/lang/StringBuilder.append : (Ljava/lang/String;)Ljava/lang/StringBuilder;24 ldc #6 b26 invokevirtual #10 java/lang/StringBuilder.append : (Ljava/lang/String;)Ljava/lang/StringBuilder;29 invokevirtual #11 java/lang/StringBuilder.toString : ()Ljava/lang/String;32 astore 534 new #8 java/lang/StringBuilder37 dup38 invokespecial #9 java/lang/StringBuilder.init : ()V41 ldc #5 n43 invokevirtual #10 java/lang/StringBuilder.append : (Ljava/lang/String;)Ljava/lang/StringBuilder;46 aload_247 invokevirtual #10 java/lang/StringBuilder.append : (Ljava/lang/String;)Ljava/lang/StringBuilder;50 invokevirtual #11 java/lang/StringBuilder.toString : ()Ljava/lang/String;53 astore 655 new #8 java/lang/StringBuilder58 dup59 invokespecial #9 java/lang/StringBuilder.init : ()V62 aload_163 invokevirtual #10 java/lang/StringBuilder.append : (Ljava/lang/String;)Ljava/lang/StringBuilder;66 aload_267 invokevirtual #10 java/lang/StringBuilder.append : (Ljava/lang/String;)Ljava/lang/StringBuilder;70 invokevirtual #11 java/lang/StringBuilder.toString : ()Ljava/lang/String;73 astore 73、final变量 拼接 场景3final修饰的String变量视作String常量。 public static void test3() {final String s1 n;final String s2 b;String s3 nb;String s4 n b; // 编译期优化String s5 s1 b;String s6 n s2;String s7 s1 s2;System.out.println(s3 s4); // trueSystem.out.println(s3 s5); // trueSystem.out.println(s3 s6); // trueSystem.out.println(s3 s7); // trueSystem.out.println(s5 s6); // trueSystem.out.println(s5 s7); // trueSystem.out.println(s6 s7); // true
}可以看到我们只是在String变量前加上final结果就完全不同了。
下面我们看一下 class文件 的 反编译结果 0 ldc #6 n2 astore_03 ldc #7 b5 astore_16 ldc #8 nb8 astore_29 ldc #8 nb11 astore_312 ldc #8 nb14 astore 416 ldc #8 nb18 astore 520 ldc #8 nb22 astore 6可以看出String变量被final修饰之后所有的拼接操作都在编译期优化了而没有使用StringBuilder
4、拼接小结
常量与常量拼接 拼接结果在字符串常量池原理是编译期优化 拼接中只要有一个是变量 拼接结果在堆中原理是 先new 一个StringBuilder然后用append()拼接最后调用 toString() 返回结果补充说明在 jdk5 之前使用的是 StringBuffer在 jdk5 之后改为了 StringBuilder final修饰的String变量拼接 拼接结果在字符串常量池仍然使用编译期优化而非StringBuilder。
因此在开发中能使用上final的时候还是建议使用
5、拼接 vs append拼接
public class StringAppendTest {public static void main(String[] args) {long start System.currentTimeMillis();// String s1 append1(100000); // 1670msString s2 append2(100000); // 4mslong end System.currentTimeMillis();System.out.println(拼接花费的时间为 (end - start));}public static String append1(int highLevel) {String str ;for (int i 0; i highLevel; i) {str str a; // 每次循环都会创建一个StringBuilder、String}return str;}public static String append2(int highLevel) {StringBuilder strBuilder new StringBuilder();for (int i 0; i highLevel; i) {strBuilder.append(a); // 只需要创建一个StringBuilder}return strBuilder.toString();}
}结论通过StringBuilder的append()的方式拼接字符串的效率远远高于 拼接
原因
StringBuilder的append()的方式 自始至终中只创建过一个StringBuilder的对象。 拼接的方式 每一次 字符串变量 拼接的过程都会new一个StringBuilder对象这从之前的反编译结果中也可以看出来
因此使用字符串变量拼接会占用更大的内存产生大量垃圾字符串如果发生了GC也会花费额外的时间。
6、StringBuilder的扩容
StringBuilder 空参构造器的初始化大小为16超过该大小会进行扩容涉及数组的copy操作
public StringBuilder() { super(16);
}如果提前知道需要拼接 String 的长度就应该直接使用带参构造器指定capacity以减少扩容的次数
public StringBuilder(int capacity) { super(capacity);
}7、优化小结
允许的情况下尽量使用final。这样拼接操作会在编译期优化而不会创建StringBuilder对象去append。使用StringBuilder的append()效率要高于使用拼接。如果知道最终的字符串长度应该使用带容量的构造器创建StringBuilder避免频繁扩容。
四、new String() 会创建几个对象
1、单独new 场景1new String(ab) 会创建几个对象答案是2个 0: new #2 // class java/lang/String3: dup4: ldc #3 // String ab6: invokespecial #4 // Method java/lang/String.init:(Ljava/lang/String;)V9: astore_1
10: return对象1 new String(ab)
对象2 常量池中的ab2、常量 拼接 new 场景2a new String(b) 会创建几个对象答案是5个 0: new #2 // class java/lang/StringBuilder3: dup4: invokespecial #3 // Method java/lang/StringBuilder.init:()V7: ldc #4 // String a9: invokevirtual #5 // Method java/lang/StringBuilder.append:
12: new #6 // class java/lang/String
15: dup
16: ldc #7 // String b
18: invokespecial #8 // Method java/lang/String.init:(Ljava/lang/String;)V
21: invokevirtual #5 // Method java/lang/StringBuilder.append:
24: invokevirtual #9 // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
27: astore_1
28: return对象1 new StringBuilder()
对象2 常量池中的a
对象3 new String(b)
对象4 常量池中的b
对象5 StringBuilder.toString() 会 new String(ab)注意StringBuilder.toString 不会在 字符串常量池中 生成 ab。
3、new 拼接 new 场景3new String(a) new String(b) 会创建几个对象答案是6个 0: new #2 // class java/lang/StringBuilder3: dup4: invokespecial #3 // Method java/lang/StringBuilder.init:()V7: new #4 // class java/lang/String
10: dup
11: ldc #5 // String a
13: invokespecial #6 // Method java/lang/String.init:(Ljava/lang/String;)V
16: invokevirtual #7 // Method java/lang/StringBuilder.append:
19: new #4 // class java/lang/String
22: dup
23: ldc #8 // String b
25: invokespecial #6 // Method java/lang/String.init:(Ljava/lang/String;)V
28: invokevirtual #7 // Method java/lang/StringBuilder.append:
31: invokevirtual #9 // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
34: astore_1
35: return对象1 new StringBuilder()
对象2 new String(a)
对象3 常量池中的a
对象4 new String(b)
对象5 常量池中的b
对象6 StringBuilder.toString() 会 new String(ab)注意StringBuilder.toString 不会在 字符串常量池中 生成 ab。
五、intern()
intern是一个native方法底层调用的是C的方法
public final class String implements java.io.Serializable, ComparableString, CharSequence {public native String intern();
}1、intern() 的作用
调用intern()时会判断 字符串常量池 中 是否已存在当前字符串通过equals()方法判断
已存在返回 字符串常量池 中 已存在的 该字符串对象的地址不存在将 该字符串 放入 字符串常量池并返回 字符串对象的地址这里jdk7前后略有不同 JDK1.6中会把 调用对象复制一份新的引用地址放入常量池并返回 新的引用地址。JDK1.7起会把 调用对象的引用地址 复制一份相同的引用地址放入常量池并返回 调用对象的引用地址。
也就是说任意字符串调用intern() 返回结果所指向的那个类实例必定和直接以常量形式出现的字符串实例完全相同。 (abc).intern() abc // true;intern()可以确保字符串在内存里只有一份即字符串常量池中的可以节约内存空间加快字符串操作任务的执行速度。
2、案例分析
1案例1
public void test() {String s1 new String(a);s1.intern();String s2 a;System.out.println(s1 s2); // jdk6/7/8 false
}s1记录的是堆中new String(a)的地址s2记录的是字符串常量池中a的地址
2案例2
public void test() {String s1 new String(a) new String(b);s1.intern();String s2 ab;System.out.println(s1 s2); // jdk6 false ; jdk7/8 true
}s1记录的是堆中ab的地址注意这个ab是StringBuilder.toString()生成的没有往常量池里放s1.intern()调用这个方法之前字符串常量池中并不存在ab所以要把ab放入字符串常量池 jdk6中字符串常量池中的ab指向新的地址。jdk7起字符串常量池中的ab指向的是调用intern()的s1的地址 s2记录的是字符串常量池中的ab指向的地址
3案例3
public void test() {String s1 new String(a) new String(b);String s2 s1.intern(); // 常量池没有ab会放入System.out.println(s1 ab); // jdk6 false ; jdk7/8 trueSystem.out.println(s2 ab); // jdk6 true ; jdk7/8 true
}public void test() {String s1 ab; // 常量池中创建一个新的对象abString s2 new String(a) new String(b);String s3 s2.intern(); // 常量池已有ab不会再放入System.out.println(s1 s2); // jdk6/7/8 falseSystem.out.println(s1 s3); // jdk6/7/8 true
}3、intern() 的效率空间角度
public class StringInternTest { static final int MAX_COUNT 1000 * 10000; static final String[] arr new String[MAX_COUNT]; public static void main(String[] args) { Integer [] data new Integer[]{1,2,3,4,5,6,7,8,9,10}; long start System.currentTimeMillis(); for (int i 0; i MAX_COUNT; i) {
// arr[i] new String(String.valueOf(data[i%data.length])); // 不用intern 7256ms arr[i] new String(String.valueOf(data[i%data.length])).intern(); // 使用intern 1395ms} long end System.currentTimeMillis(); System.out.println(花费的时间为 (end - start)); try { Thread.sleep(1000000); } catch (Exception e) { e.getStackTrace(); } System.gc();}
}直接new堆 和 字符串常量池 可能会存在相同的字符串的两个对象。使用intern()保证内存中相同的字符串对象只会有一个。
结论对于程序中大量使用存在的字符串时尤其存在很多已经重复的字符串时使用 intern()方法能够节省内存空间。
4、运行时内存案例
class Memory {public static void main(String[] args) {int i 1;Object obj new Object();Memory mem new Memory();mem.foo(obj);}private void foo(Object param) {String str param.toString().intern();System.out.println(str); // java.lang.Object42a57993}
}