一、需要理解的代码:
import java.lang.reflect.Array;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Queue;
import java.util.Set;
class Test{
public static String appendStr(String s) {
s+="bbb";
return s;
}
public static StringBuilder appendSb(StringBuilder sb) {
return sb.append("bbb");
}
public static void main(String[] args) {
String s = new String("aaa");
String ns = Test.appendStr(s);
System.out.println(s); //aaa
System.out.println(ns); //aaabbb
StringBuilder sb = new StringBuilder("ccc");
StringBuilder nsb = Test.appendSb(sb);
System.out.println(sb); //cccbbb
System.out.println(nsb); //cccbbb
}
}
//public class Test {
// public static void main(String[] args) {
// String s1 = "hello";
// String s2 = "world";
// change(s1,s2);
// System.out.println("111:::" + s1+" "+s2);//hello---world
//
//
// String s3 = new String("hello");
// String s4 = new String("world");
// change(s3,s4);
// System.out.println("222:::" + s3+" "+s4);//hello--world
//
//
// StringBuilder sb1 = new StringBuilder("hello");
// StringBuilder sb2 =new StringBuilder("world");
// change(sb1,sb2);
// System.out.println("333:::" + sb1+" "+sb2); //hello--worldworldhuan
// }
//
// private static void change(String s1, String s2) {
// s1=s2;
// s1=s1+s2;
// System.out.println("444:::" + s1+" "+s2);
// }
// private static void change(StringBuilder sb1, StringBuilder sb2) {
// sb1=sb2;
// sb1=sb1.append(sb2).append("huan");
// System.out.println("555:::" + sb1+" "+sb2);
//
// }
//}
444:::hello world
111:::hello world
444:::helloworld world
111:::hello world
StringBuilder改变常量池中的内容了吗?
答:没有,参考 https://blog.csdn.net/aaqian1/article/details/113766231
常量池:
在堆中分配出来的一块存储区域,用于存储显式的String、float 或者 Integer。例如 String str = “abc”,"abc"这个字面量是显式声明,所以存储在常量池。
JAVA的JVM的内存可分为3个区:堆(heap)、栈(stack)和方法区(method)
堆区:
1、存储的全部是对象,每个对象都包含一个与之对应的class的信息(class的目的是得到操作指令)
2、jvm只有一个堆区(heap)被所有线程共享,堆中不存放基本类型和对象引用,只存放对象本身
栈区:
1、每个线程包含一个栈区,栈中只保存基本数据类型的对象和自定义对象的引用(不是对象),对象都存放在堆区中
2、每个栈中的数据(原始类型和对象引用)都是私有的,其它栈不能访问
3、栈分为三个部分:
图1:
图2:
图3:JVM中堆、栈、方法区、字符串常量池的关系
在图3中,在栈中
1、String类是通过char数组来保存字符串的。
String str = "hahaha";
对应源码:
public final class String
implements java.io.Serializable, Comparable<String>, CharSequence {
private final char value[];
··· ···
}
2 构造函数
String str = new String("abc");
1)先创建一个空的String对象
2)常量池中创建一个abc,并赋值给第二个String
3) 将第二个String的引用传递给第一个String
注意:如果常量池中有abc,则不用创建,直接把引用传递给第一个String
对应源码:
public String(String original) {
this.value = original.value;
this.hash = original.hash;
}
2、String对象一旦被创建就是固定不变的了,对String对象的任何改变都不影响原对象,相关的任何change操作都会生成新的对象。
二、字符串常量池
JVM为了提高性能和减少内存的开销,在实例化字符串的时候进行了一些优化:使用字符串常量池。
每当我们创建字符串常量时,JVM会首先检查字符串常量池,如果该字符串已经存在常量池中,那么就直接返回常量池中的实例引用。如果字符串不存在常量池中,就会实例化该字符串并且将其放到常量池中。由于String字符串的不可变性我们可以十分肯定常量池中一定不存在两个相同的字符串。
Java中的常量池,实际上分为两种形态:静态常量池 和 运行时常量池。
-
静态常量池:*.class文件中的常量池,class文件中的不仅仅包含字符串字面量,还包含类、方法的信息,占用class文件绝大部分空间。
-
运行时常量池:jvm虚拟机在完成类装载操作后,将class文件中的常量池载入到内存中,并保存在方法区中,我们常说的常量池,就是指方法区中的运行时常量池。
String a = "chenssy";
String b = "chenssy";
a、b都指向JVM字符串常量池中的“chenssy”,它们指向同一个对象
String c = new String("chenssy");
new关键字产生的对象是存储在堆中的,所以上面应该产生了两个对象:保存在栈中的 c 和保存在堆中的 chenssy 。但是在Java中根本就不存在两个完全一模一样的字符串对象,所以堆中的 chenssy ,应该是引用字符串常量池中的 chenssy 。所以栈中的变量c、堆中的变量 chenssy 、JVM字符串常量池中的 chenssy 的关系如下:
总结:
String c = new String(“chenssy”); 虽然 c 的内容是创建在堆中的,但是它的内部 value 还是指向JVM常量池中的 chenssy 的value。
例1:
/**
* 采用字面值的方式赋值
*/
public void test1(){
String str1="aaa";
String str2="aaa";
System.out.println("===========test1============");
System.out.println(str1==str2);//true 可以看出str1跟str2是指向同一个对象
}
结果为:true
分析:
当执行String str1="aaa"时,JVM首先会去字符串池中查找是否存在"aaa"这个对象,如果不存在,则在字符串池中创建"aaa"这个对象,然后将池中"aaa"这个对象的引用地址返回给字符串常量str1,这样str1会指向池中"aaa"这个字符串对象;如果存在,则不创建任何对象,直接将池中"aaa"这个对象的地址返回,赋给字符串常量。当创建字符串对象str2时,字符串池中已经存在"aaa"这个对象,直接把对象"aaa"的引用地址返回给str2,这样str2指向了池中"aaa"这个对象,也就是说str1和str2指向了同一个对象,因此语句System.out.println(str1 == str2)输出:true。
例2:
/**
* 采用new关键字新建一个字符串对象
*/
public void test2(){
String str3=new String("aaa");
String str4=new String("aaa");
System.out.println("===========test2============");
System.out.println(str3==str4);//false 可以看出用new的方式是生成不同的对象
}
结果为:false
分析:
采用new关键字新建一个字符串对象时,JVM首先在字符串池中查找有没有"aaa"这个字符串对象,如果有,则不在池中再去创建"aaa"这个对象了,直接在堆中创建一个"aaa"字符串对象,然后将堆中的这个"aaa"对象的地址返回赋给引用str3,这样,str3就指向了堆中创建的这个"aaa"字符串对象;如果没有,则首先在字符串池中创建一个"aaa"字符串对象,然后再在堆中创建一个"aaa"字符串对象,然后将堆中这个"aaa"字符串对象的地址返回赋给str3引用,这样,str3指向了堆中创建的这个"aaa"字符串对象。当执行String str4=new String(“aaa”)时, 因为采用new关键字创建对象时,每次new出来的都是一个新的对象,也即是说引用str3和str4指向的是两个不同的对象,因此语句System.out.println(str3 == str4)输出:false。
例3:
public void test5(){
String str1="abc";
String str2="def";
String str3=str1+str2;
System.out.println("===========test5============");
System.out.println(str3=="abcdef"); //false
}
结果为:false
分析:
因为str3指向堆中的 “abcdef” 对象,而 “abcdef” 是字符串池中的对象,所以结果为fasle。
这段代码总共创建了5个对象,字符串池中2个、堆中3个(abc、def、adbdef)。“+” 运算符会在堆中建立两个String对象,这两个对象的值分别是 “abc” 和 “def”,也就是说从字符串池中复制这两个值,然后在堆中创建两个对象, 然后再建立对象 str3,然后将 “abcdef” 的堆地址赋给 str3。
1: 栈中开辟一块空间存放引用str1,str1指向常量池中的"abc"
2: 栈中开辟一块空间存放引用str2,str2指向常量池中的"def"
3: 栈中开辟一块空间存放引用str3。
4:str1 + str2通过StringBuilder的最后一步toString()方法还原一个新的String对象"abcdef",因此堆中开辟一块空间存放此对象
5:引用str3指向堆中(str1 + str2)所还原的新的String对象
6:str3指向的对象在堆中,而常量"abcdef"在池中,输出为false。
例4:
/**
* 编译期优化
*/
public void test6(){
String s0 = "a1";
String s1 = "a" + 1;
String s6 = "1";
String s7 = "a" + s6;
System.out.println("===========test6============");
System.out.println((s0 == s1)); //result = true
System.out.println((s0 == s7)); //result = false
String s2 = "atrue";
String s3= "a" + "true";
System.out.println((s2 == s3)); //result = true
String s4 = "a3.4";
String s5 = "a" + 3.4;
System.out.println((s4 == s5)); //result = true
}
结果为:true、true、true
分析:
在程序编译期,JVM就将常量字符串“+”连接优化为连接后的值,比如"a"+1,在编译期优化后,在class中就已经是"a1"。在编译期其字符串常量的值就确定下来。而对于" s7 = “a” + s6 “来说,在”+" 连接中,有字符串引用存在,而引用的值在程序编译期是无法确定的,即"a" + s6 无法被编译器优化,只有在程序运行期来动态分配并将连接后的新地址赋给s7,所以上面程序的结果为false。
例5:
/**
* 比较字符串常量的“+”和字符串引用的“+”的区别
*/
public void test8(){
String test="javalanguagespecification";
String str="java";
String str1="language";
String str2="specification";
System.out.println("===========test8============");
System.out.println(test == "java" + "language" + "specification");
System.out.println(test == str + str1 + str2);
}
结果为:true、false
分析:
在编译器编译时,直接把"java"、“language"和"specification"这三个字面量进行”+“操作得到一个"javalanguagespecification"常量,并且直接将这个常量放入字符串池中。而字符串引用的”+“运算是在Java运行期间执行的,即str + str1 + str2在程序执行期间才会进行计算,它会在堆内存中重新创建一个拼接后的字符串对象。总结来说,就是字面量的”+“拼接是在编译期间进行的,拼接后的字符串存放在字符串池中;而字符串引用的”+"拼接运算是在运行时进行的,新创建的字符串存放在堆中。
对于直接相加的字符串,效率很高,因为在编译期便确定了它的值,即形如"I" + “love” + “java” 的字符串相加,在编译期间便被优化成了"Ilovejava"。对于间接相加(即包含字符串引用),形如 s1+s2+s3,效率要比直接相加低,因为在编译期不会对引用变量进行优化。
例6:
/**
* 编译期确定
*/
public void test9(){
String s0 = "ab";
final String s1 = "b";
String s2 = "a" + s1;
System.out.println("===========test9============");
System.out.println((s0 == s2)); //result = true
}
结果为:true
分析:这里的s1字符串加了final修饰,对于final修饰的变量,它在编译时被解析为常量值的一个本地拷贝存储到自己的常量池中或嵌入到它的字节码流中,所以此时的 “a” + s1 和 “a” + “b” 效果是一样的。
例7:
/**
* 编译期无法确定
*/
public void test10(){
String s0 = "ab";
final String s1 = getS1();
String s2 = "a" + s1;
System.out.println("===========test10============");
System.out.println((s0 == s2)); //result = false
}
private static String getS1() {
return "b";
}
结果:false
分析:虽然这里的s1用final修饰了,但是由于其赋值是通过方法调用返回的,那么它的值只能在运行期间确定,因此s0和s2指向的不是同一个对象,故上面的程序结果为false。
总结:
1、String类初始化后是不变的
String使用private final char value[] 来实现字符串的存储,也就是说String对象创建之后,就不能再修改此对象中存储的字符串内容,所以说String类型是不可变的。String类对象确实有编辑字符串的功能,比如replace(),但是这些编辑功能是通过创建一个新的对象来实现的,而不是对原有对象进行修改。
s = s.replace("World", "Universe");
2、引用变量与对象
A aa;
aa仅仅是一个引用变量,它不是对象,对象一般通过new创建。
3、创建字符串的方式
创建字符串的方式归纳起来有两类:(1)使用""引号创建字符串(2)使用new关键字创建字符串
使用引用创建的字符串都是常量,在编译期就已经存储到String Pool中;使用new String("")创建的对象会存储到heap中,是运行期创建的;使用只包含常量的字符串连接如"aa" + “bb” 创建的也是常量,编译期就能确定,已经确定存储到String Pool中;使用包含变量的字符串连接符,如"aa" + s1创建的对象是运行期才创建的,存储在heap中。
new创建字符串时,首先查看池中是否有相同值的字符串,如果有,则copy一份到堆中,然后返回堆中的地址;如果池中没有,则在堆中创建一份,然后返回堆中的地址(注意,此时不需要从堆中复制到池中,否则,将使得堆中的字符串永远是池中的子集,导致浪费池的空间!!)
4、使用String不一定创建对象
在执行双引号包含字符串的语句时,如String a = “123”,JVM会先到常量池里查找,如果有的话,返回常量池里的这个实例的引用,否则的话,创建一个新实例并置入常量池。所以,当使用String a = "123"时,对象可能并没有被创建!而可能只是指向一个先前已经创建的对象。 只有通过new()方法才能保证每次都创建一个新的对象。
5、关于equals和==
(1)对于 “==”, 如果作用于基本数据类型的变量(byte、short、char、int、long、float、double、boolean),则直接比较其存储的“值”是否相等;如果作用于引用类型的变量(String),则比较的是所指向的对象的地址(即判断其是否指向同一个对象)
(2)equals方法,是基类Object中的方法,因此对于所有的
参考:
https://www.cnblogs.com/xiaoxi/p/6036701.html
https://blog.csdn.net/qq_43012792/article/details/107372191
https://blog.csdn.net/aaqian1/article/details/113766231