【Java】Java中的字符串

参考:
https://blog.csdn.net/xialei199023/article/details/63251366
https://www.jianshu.com/p/aeb7799ba09f

==”对于基本数据类型,比较的数值;对于引用类型,比较的地址

Java中String类位于java.lang包下,是整个Java语言的基石。同时String类使用final关键词修饰,意味着外部调用者无法通过继承和重写来更改其功能。Java中的字符串与语言相比,也有其特殊性。本文深入地理解Java字符串,主要内容有:

  1. String的初始化
  2. String与常量池
  3. String的不变性
  4. String、StringBuffer与StringBuilder
  5. “+”操作符

1. 字符串初始化

String并不是Java中的基础类型,它也是一个对象。在源代码层面来说,String有多种不同的初始化方法,本节就介绍这些初始化方法。

//直接初始化
String str = "abc";
//使用带参构造方法初始化
char[] char = {
    
    'a','b','c'};
String str1 = new String("abc");
String str2 = new String(str);
String str3 = new String(char);

字面量法

String的字面量初始化法如下所示:

String a = "abc";
String b = "hello world";

这种方法首先从常量池中查找是否有相同值的字符串对象,如果有,则直接将对象地址赋予引用变量;如果没有,在首先在常量池区域中创建一个新的字符串对象,然后将地址赋予引用变量。

构造方法

String的构造方法初始化法如下所示:

String a = new String("abc");
String b = new String("hello world");

String类的构造方法有:

public String() {
    
    } // 构造空串(注意与null的区别)
public String(String original) {
    
    } // 基于另外一个字符串构造一个新字符串对象
public String(char value[]) {
    
    } // 使用byte数组构造字符串
public String(char value[], int offset, int count){
    
    } // 使用byte数组以及偏移参数构造
public String(int[] codePoints, int offset, int count) {
    
    } // 基于Uncode编码数组以及偏移量构造

这种初始化方法与一般对象的初始化方法完全一样。与字面量法不同的是,每次调用构造方法都会在堆内存中创建一个新的字符串对象。下面的例子可以清楚地显示它们的区别:

package demo.blog.java.string;

/**
 * 不同初始化方法的区别
 */
public class StringEqual {
    
    

    public static void main(String[] args) {
    
    
        String a1 = "123";
        String b1 = "123";
        System.out.println(a1 == b1); // true

        String a2 = new String("123");
        String b2 = new String("123");
        System.out.println(a2 == b2); // false
    }
}

我们知道,Java中的 “ == ” 符比较的变量保存的实际内存数据,由于基础数据类型变量保存的是数据的实际值,而引用类型变量保存的是对象的地址,不同的地址代表着不同的对象。a1 == b1为true表明a1和b1指向同一个对象,而a2和b2分别指向不同的对象。

2. String与JVM常量池

说起String ,就不能不提到JVM常量池,是笔试和面试中经常喜欢出题的点。

常量池

在正式进入常量池之前,首先简单地介绍一下JVM(Java虚拟机,由于市面上有多种不同的JVM,本文中仅考虑Hotspot VM)的内存结构,也是作为Java程序员必须要了解的内容(以后再深入JVM)。由于本文的主体并不是Java虚拟机,内容会比较粗糙,更加详细的JVM知识会在以后撰写。

JVM的内存顶层结构如下图所示:

JVM内存结构

下面分别来说说主要的内存区域:

  • 1 程序计数器:这是一块比较小的内存区域,可以看做是当前线程所执行的字节码的行号指示器。每一个线程都会需要有一个独立的程序计数器,各个程序计数器之间相互不影响。如果线程当前执行的是Java方法,则计数器记录当前正在执行字节码指令地址,如果是Native方法,则计数器的值为空。

  • 2 栈区:栈区是线程私有的,其生命周期与线程相同。栈区描述的是Java方法执行的内存模型(以后会详细研究)。

  • 3 本地方法栈:功能与栈区的功能相似,不过是为Java中的Native方法服务的。

  • 4 堆区:这是JVM管理的最大的一块内存区域,是所有线程共享的,几乎所有的对象实例都放置在这个区域。堆又可以细分为:年轻代、老年代。我们通常所说的垃圾回收就是发现在这个内存区域。

  • 5 方法区:与堆一样,也是所有线程所共享的。由于习惯问题,方法区也被叫做永久代,因为上面放置的数据很难被回收(条件很苛刻)。

常量池(准确地说是运行时常量池),在JDK1.6及以前都是方法区中的一部分,在JDK1.7之后被移入堆区,用来存放编译时生成的各种字面量和符号引用。下面来看一下普通类编译后的字节码。

public class Test {
    
    

    public static void main(String[] args) {
    
    
        String a = "123";
    }
}

上面的源代码编译完后的字节码如下:

public class Test
  minor version: 0
  major version: 52
  flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
   #1 = Methodref          #4.#13         // java/lang/Object."<init>":()V
   #2 = String             #14            // 123
   #3 = Class              #15            // Test
   #4 = Class              #16            // java/lang/Object
   #5 = Utf8               <init>
   #6 = Utf8               ()V
   #7 = Utf8               Code
   #8 = Utf8               LineNumberTable
   #9 = Utf8               main
  #10 = Utf8               ([Ljava/lang/String;)V
  #11 = Utf8               SourceFile
  #12 = Utf8               Test.java
  #13 = NameAndType        #5:#6          // "<init>":()V
  #14 = Utf8               123
  #15 = Utf8               Test
  #16 = Utf8               java/lang/Object
{
    
    
  public Test();
    descriptor: ()V
    flags: ACC_PUBLIC
    Code:
      stack=1, locals=1, args_size=1
         0: aload_0
         1: invokespecial #1                  // Method java/lang/Object."<init>":()V
         4: return
      LineNumberTable:
        line 4: 0

  public static void main(java.lang.String[]);
    descriptor: ([Ljava/lang/String;)V
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=1, locals=2, args_size=1
         0: ldc           #2                  // String 123
         2: astore_1
         3: return
      LineNumberTable:
        line 7: 0
        line 8: 3
}

第6~21行是编译后产生的常量池(编译时常量池),这些常量在类加载时被保存到运行时常量池中,代码中123字符串在编译后放入常量池中(见第19行),这也就解释了上一节示例中的a1 == b1为true的问题。常量池中的大部分数据都来自编译时常量池,也可以在运行时将数据放入常量池。

intern()方法

intern()方法的作用是在常量池中查找值等于(equals)当前字符串的对象,如果找到,则直接返回这个对象的地址;如果没有找到,则将当前字符串拷贝到常量池中,然后返回拷贝后的对象地址。下面的代码可以解释intern()的功能。

String a = "123";
String b = new String("123");
String c = new String("123");

System.out.println(a == b); // false
System.out.println(a == b.intern()); // true
System.out.println(c == b.intern()); // false
System.out.println(c.intern() == b.intern()); // true

由于String a = "123";产生的字符串对象会直接放入常量池中,当调用b.intern()方法时,由于已经存在值为123的对象(即a所指向的对象),直接返回这个对象,所以a == b.intern()判定为true。

3. String的不可变性

类的不可变性

什么叫做类的不变性?简单地说,就是其实例一旦创建完成,在其整个生命周期内状态都不会发生变化。状态这个词有一些抽象,在Java中对象的状态是由其成员变量来表现的,那么状态不变即是成员变量不变(具体来说,基本类型变量的值不变、引用类型变量的引用地址不变)。不可变的类有不少好处:

  • 更加易于设计、实现和使用。
  • 并发时,不容易出错,并且更加安全。

为了使类成为不可变,要遵循下面五条规则(引用自《Effective Java》):

  1. 不提供任何会修改对象状态的方法;
  2. 保证类不能被扩展;
  3. 所有的成员变量都被final修饰的;
  4. 所有的成员变量都是private的;
  5. 确保对于任何可变组件的互斥访问。如果一个类的成员变量引用了可变对象,则必须确保外部调用类无法获取指向这些对象的引用。

当然,所有的这些条件都是针对正常调用而言的,如果使用反射,则仅仅满足上述的条件也无法保证对象不可变。

String的不可变性

首先来通过一个图文案例来说明String不可变性。

1、声明一个String类型变量。

String s = "abcd";

2、将字符串变量赋予另外一个String类型变量。

String s2=s;

3、连接另外一个字符串对象。

String s3 = s.concat("ef");  

可以看到s3指向是另外一个对象,而不是原来a所指向的对象。在调用concat(String)方法之后,a所指向的对象状态并没有发生改变,而是生成了一个新的对象。下面是concat(String)方法的源代码(JDK1.8):

public String concat(String str) {
    
    
    int otherLen = str.length();
    if (otherLen == 0) {
    
    
        return this;
    }
    int len = value.length;
    char buf[] = Arrays.copyOf(value, len + otherLen);
    str.getChars(buf, len);
    return new String(buf, true);
}

注意到第9行代码,它直接创建一个新的String对象。

为什么String要设计为不可变呢?主要原因如下:

字符串池(String pool)的需求。之前已经说过,通过字面量初始化一个Java字符串时,会将这个字符串保存在常量池中。如果定义了另外一个相同值的字符串变量,则直接指向之前初始化的那个对象。如果字符串是可变的,改变另一个字符串变量,就会使另一个字符串变量指向错误的值。

缓存字符串hashcode码的需要。字符串的hashcode是经常被使用的,字符串的不变性确保了hashcode的值一直是一样的,在需要hashcode时,就不需要每次都计算,这样会很高效。

出于安全性考虑。字符串经常作为网络连接、数据库连接等参数,不可变就可以保证连接的安全性。

4. String、StringBuffer与StringBuilder

StringBuffer与StringBuilder是创建字符串常用的类,采用构建器模式来构建字符串对象,使得可以在运行时动态地构建字符串对象。下表是它们各自的特点。

可变性 线程安全
String 不可变 线程安全
StringBuffer 可变 线程安全
StringBuilder 可变 非线程安全

我们对String、StringBuffer、StringBuiler先有一个简单的认识。String是不可变字符串,StringBuffer和StringBuilder是长度可变的字符串,区别是StringBuffer是线程安全的,StringBuilder是线程不安全的,同样StringBuilder的效率也会更高。

StringBuffer 上的主要操作是 appendinsert 方法,可重载这些方法,以接受任意类型的数据。每个方法都能有效地将给定的数据转换成字符串,然后将该字符串的字符追加或插入到字符串缓冲区中。append 方法始终将这些字符添加到缓冲区的末端;而 insert 方法则在指定的点添加字符。

例如,如果 z 引用一个当前内容为 “start” 的字符串缓冲区对象,则此方法调用 z.append(“le”) 会使字符串缓冲区包含 “startle”,而 z.insert(4, “le”) 将更改字符串缓冲区,使之包含 “starlet”。

通常,如果 sb 引用 StringBuilder 的一个实例,则 sb.append(x)sb.insert(sb.length(), x) 具有相同的效果。

还有delete删除方法

deleteCharAt(int index)
delete(int start ,int end)
序号 方法 描述
1 public StringBuffer append(String s) 将指定的字符串追加到此字符序列。
2 public StringBuffer reverse() 将此字符序列用其反转形式取代。
3 public delete(int start, int end) 移除此序列的子字符串中的字符。
4 public insert(int offset, int i) 将 int 参数的字符串表示形式插入此序列中。
5 replace(int start, int end, String str) 使用给定 String 中的字符替换此序列的子字符串中的字符。
序号 方法 描述
1 int capacity() 返回当前容量。
2 char charAt(int index) 返回此序列中指定索引处的 char 值。
3 void ensureCapacity(int minimumCapacity) 确保容量至少等于指定的最小值。
4 void getChars(int srcBegin, int srcEnd, char[] dst, int dstBegin) 将字符从此序列复制到目标字符数组 dst。
5 int indexOf(String str) 返回第一次出现的指定子字符串在该字符串中的索引。
6 int indexOf(String str, int fromIndex) 从指定的索引处开始,返回第一次出现的指定子字符串在该字符串中的索引。
7 int lastIndexOf(String str) 返回最右边出现的指定子字符串在此字符串中的索引。
8 int lastIndexOf(String str, int fromIndex) 返回 String 对象中子字符串最后出现的位置。
9 int length() 返回长度(字符数)。
10 void setCharAt(int index, char ch) 将给定索引处的字符设置为 ch。
11 void setLength(int newLength) 设置字符序列的长度。
12 CharSequence subSequence(int start, int end) 返回一个新的字符序列,该字符序列是此序列的子序列。
13 String substring(int start) 返回一个新的 String,它包含此字符序列当前所包含的字符子序列。
14 String substring(int start, int end) 返回一个新的 String,它包含此序列当前所包含的字符子序列。
15 String toString() 返回此序列中数据的字符串表示形式。

5. “+”操作符

String是一个异类,除了基本类型及其包装类之外,只有它可以使用”+” 操作符号。在String中,”+” 表示字符串连接,而不是数学运算中的加法运算。下面通过一些例子(例子引用自《深入理解Java:String》)来说明不同场景下使用”+”操作符的特点。

编译时优化

/*
 * 由于常量的值在编译的时候就被确定(优化)了。 
 * 在这里,"ab"和"cd"都是常量,因此变量str3的值在编译时就可以确定。 
 * 这行代码编译后的效果等同于: String str3 = "abcd"; 
 */  
String str1 = "ab" + "cd";   
String str11 = "abcd";   
System.out.println("str1 = str11 : "+ (str1 == str11));  // true

为了提高效率和减少内存占用,Java编译器会在编译时做一些其力所能及的事情。上面的代码中,由于在编译时即可以确定str1的值为”abcd”,所以编译时,直接将”abcd”字符串对象赋予str1,所以str1和str11引用的是常量池中的同一个对象。

利用StringBuilder实现

/*  
 * 局部变量str2,str3存储的是存储两个拘留字符串对象(intern字符串对象)的地址。 
 *  
 * 第三行代码原理(str2+str3): 
 * 运行期JVM首先会在堆中创建一个StringBuilder类, 
 * 同时用str2指向的拘留字符串对象完成初始化, 
 * 然后调用append方法完成对str3所指向的拘留字符串的合并, 
 * 接着调用StringBuilder的toString()方法在堆中创建一个String对象, 
 * 最后将刚生成的String对象的堆地址存放在局部变量str3中。 
 *  
 * 而str5存储的是字符串池中"abcd"所对应的拘留字符串对象的地址。 
 * str4与str5地址当然不一样了。 
 *  
 * 内存中实际上有五个字符串对象: 
 *       三个拘留字符串对象、一个String对象和一个StringBuilder对象。 
 */  
String str2 = "ab";  
String str3 = "cd";                                          
String str4 = str2 + str3;                                        
String str5 = "abcd";    
System.out.println("str4 = str5 : " + (str4 == str5)); // false  

上面的源代码编译后的字节码如下,第5行创建了一个StringBuilder对象,并在第9行和第11行分别将字符串”ab”和”cd”append到对象中,生成新的字符串对象”abcd”,并将引用赋予变量str4。str5引用的对象保存在常量池中,而str4引用的对象是保存在Java堆中的,它们不是同一个对象。

0: ldc           #2                  // String ab
 2: astore_1
 3: ldc           #3                  // String cd
 5: astore_2
 6: new           #4                  // class java/lang/StringBuilder
 9: dup
10: invokespecial #5                  // Method java/lang/StringBuilder."<init>":()V
13: aload_1
14: invokevirtual #6                  // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
17: aload_2
18: invokevirtual #6                  // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
21: invokevirtual #7                  // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
24: astore_3
25: ldc           #8                  // String abcd
27: astore        4
29: getstatic     #9                  // Field java/lang/System.out:Ljava/io/PrintStream;
32: new           #4                  // class java/lang/StringBuilder
35: dup
36: invokespecial #5                  // Method java/lang/StringBuilder."<init>":()V
39: ldc           #10                 // String str4 = str5 :
41: invokevirtual #6                  // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
44: aload_3
45: aload         4
47: if_acmpne     54
50: iconst_1
51: goto          55
54: iconst_0
55: invokevirtual #11                 // Method java/lang/StringBuilder.append:(Z)Ljava/lang/StringBuilder;
58: invokevirtual #7                  // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
61: invokevirtual #12                 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
64: return

final字符串编译时优化

/*
 *  JAVA编译器对string + 基本类型/常量 是当成常量表达式直接求值来优化的。 
 *  运行期的两个string相加,会产生新的对象的,存储在堆(heap)中 
 */   
final String str8 = "b";  
String str9 = "a" + str8;  
String str89 = "ab";  
System.out.println("str9 = str89 : "+ (str9 == str89)); // true  
//↑str8为常量变量,编译期会被优化  

用final修饰的变量str8表示字符串常量,str8不可能引用其他的字符串。在编译时,直接将”ab”字符串赋予str9变量,所以,上面的判断结果为true。

6. String类的方法

(1)求字符串长度和某一位置字符:

String str = new String("abcdef");
int strlength = str.length();//strlength = 7
char ch = str.charAt(4);//ch = e

(2)提取子串

用String类的substring方法可以提取字符串中的子串,该方法有两种常用参数:
1)public String substring(int beginIndex)//该方法从beginIndex位置起,从当前字符串中取出剩余的字符作为一个新的字符串返回。
2)public String substring(int beginIndex, int endIndex)//该方法从beginIndex位置起,从当前字符串中取出到endIndex-1位置的字符作为一个新的字符串返回。

String str1 = new String("abcdef");
String str2 = str1.substring(2);//str2 = "cdef"
String str3 = str1.substring(2,5);//str3 = "cde"

(3)字符串比较

1)public int compareTo(String anotherString)//该方法是对字符串内容按字典顺序进行大小比较,通过返回的整数值指明当前字符串与参数字符串的大小关系。若当前对象比参数大则返回正整数,反之返回负整数,相等返回0。
2)public int compareToIgnoreCase(String anotherString)//与compareTo方法相似,但忽略大小写。
3)public boolean equals(Object anotherObject)//比较当前字符串和参数字符串,在两个字符串相等的时候返回true,否则返回false。
4)public boolean equalsIgnoreCase(String anotherString)//与equals方法相似,但忽略大小写。


String str1 = new String("abc");
String str2 = new String("ABC");
int a = str1.compareTo(str2);//a>0
int b = str1.compareToIgnoreCase(str2);//b=0
boolean c = str1.equals(str2);//c=false
boolean d = str1.equalsIgnoreCase(str2);//d=true

(4)字符串链接

public String concat(String str)//将参数中的字符串str连接到当前字符串的后面,效果等价于"+"


String str = "aa".concat("bb").concat("cc");
相当于:
String str = "aa"+"bb"+"cc";

(5)字符串中单个字符查找

1)public int indexOf(int ch/String str)//用于查找当前字符串中字符或子串,返回字符或子串在当前字符串中从左边起首次出现的位置,若没有出现则返回-1。
2)public int indexOf(int ch/String str, int fromIndex)//与第一种类似,区别在于该方法从fromIndex位置向后查找。
3)public int lastIndexOf(int ch/String str)//该方法与第一种类似,区别在于该方法从字符串的末尾位置向前查找。
4)public int lastIndexOf(int ch/String str, int fromIndex)//该方法与第二种方法类似,区别于该方法从fromIndex位置向前查找。

String str = "I really miss you !";
int a = str.indexOf('a');//a = 4
int b = str.indexOf("really");//b = 2
int c = str.indexOf("gg",2);//c = -1
int d = str.lastIndexOf('s');//d = 6
int e = str.lastIndexOf('s',7);//e = 7

(6)大小写转换

1)public String toLowerCase()//返回将当前字符串中所有字符转换成小写后的新串
2)public String toUpperCase()//返回将当前字符串中所有字符转换成大写后的新串

String str = new String("abCD");
String str1 = str.toLowerCase();//str1 = "abcd"
String str2 = str.toUpperCase();//str2 = "ABCD"

(7)字符串中字符的替换

1)public String replace(char oldChar, char newChar)//用字符newChar替换当前字符串中所有的oldChar字符,并返回一个新的字符串。
2)public String replaceFirst(String regex, String replacement)//该方法用字符replacement的内容替换当前字符串中遇到的第一个和字符串regex相匹配的子串,应将新的字符串返回。
3)public String replaceAll(String regex, String replacement)//该方法用字符replacement的内容替换当前字符串中遇到的所有和字符串regex相匹配的子串,应将新的字符串返回。

String str = "asdzxcasd";
String str1 = str.replace('a','g');//str1 = "gsdzxcgsd"
String str2 = str.replace("asd","fgh");//str2 = "fghzxcfgh"
String str3 = str.replaceFirst("asd","fgh");//str3 = "fghzxcasd"
String str4 = str.replaceAll("asd","fgh");//str4 = "fghzxcfgh"

(8)其他方法

1)String trim()//截去字符串两端的空格,但对于中间的空格不处理。
String str = " a bc ";
String str1 = str.trim();
int a = str.length();//a = 6
int b = str1.length();//b = 4

2)boolean startsWith(String prefix)boolean endsWith(String suffix)//用来比较当前字符串的起始字符或子字符串prefix和终止字符或子字符串suffix是否和当前字符串相同,重载方法中同时还可以指定比较的开始位置offset。
String str = "abcdef";
boolean a = str.startsWith("ab");//a = true
boolean b = str.endsWith("ef");//b = true


3)boolean contains(String str)//判断参数s是否被包含在字符串中,并返回一个布尔类型的值。
String str = "abcdef";
str.contains("ab");//true
str.contains("gh");//false

4)String[] split(String str)//将str作为分隔符进行字符串分解,分解后的字符串在字符串数组中返回。
String str = "abc def ghi";
String[] str1 = str.split(" ");//str1[0] = "abc";str1[1] = "def";str1[2] = "ghi";

(9)类型转换
字符串转基本类型
java.lang包中有Byte、Short、Integer、Float、Double类的调用方法:

public static byte parseByte(String s)
public static short parseShort(String s)
public static short parseInt(String s)
public static long parseLong(String s)
public static float parseFloat(String s)
public static double parseDouble(String s)
int n = Integer.parseInt("12");
float f = Float.parseFloat("12.34");
double d = Double.parseDouble("1.124");

基本类型转字符串
String类中提供了String valueOf()放法,用作基本类型转换为字符串类型

static String valueOf(char data[])
static String valueOf(char data[], int offset, int count)
static String valueOf(boolean b)
static String valueOf(char c)
static String valueOf(int i)
static String valueOf(long l)
static String valueOf(float f)
static String valueOf(double d)
//将char '8' 转换为int 8
String str = String.valueOf('8');
int num = Integer.parseInt(str);

进制转换
使用Long类中的方法得到整数之间的各种进制转换的方法:

Long.toBinaryString(long l)//二进制
Long.toOctalString(long l)//十进制
Long.toHexString(long l)//十六进制
Long.toString(long l, int p)//p作为任意进制

(10)与转换相关的其他函数

char[] chars = "hello".toCharArray();

7. String特性

这一部分介绍String的一些特性,涉及到字符串常量池、String.intern()以及我们经常遇到的“==”和“equals()”问题。
下面我们将通过不同的例子来解释:

例子1:

String a = "Hello World!";  
String b = "Hello World!";  
String c = new String("Hello World!");  
String d = "Hello"+" "+"World!";
System.out.println(a == b);//true
System.out.println(a == c);//false
System.out.println(a == d);//true

我们应该明白:

首先String不属于8种基本数据类型,String是一个引用类型。
因为对象的默认值是null,所以String的默认值也是null;但它又是一种特殊的对象,有其它对象没有的一些特性。
在这里,我们先不谈堆,也不谈栈,只先简单引入常量池这个简单的概念。
常量池(constant pool)指的是在编译期被确定,并被保存在已编译的.class文件中的一些数据。它包括了关于类、方法、接口等中的常量,也包括字符串常量。

Java会确保一个字符串常量只有一个拷贝。

因为例子中的a和b都是字符串常量,它们在编译期就被确定了,所以
ab为true;而"Hello"和" "以及"World!"也都是字符串常量,当一个字符串由多个字符串常量连接而成时,它自己肯定也是字符串常量,所以d也同样在编译期就被解析为一个字符串常量,所以d也是常量池中"Hello World!"的一个引用。所以我们得出ab==d;

用new String() 创建的字符串不是常量,不能在编译期就确定,所以new String()创建的字符串不放入常量池中,它们有自己的地址空间。

例子2:

String a = "HelloWorld";    
String b = new String("HelloWorld");    
String c = "Hello"+ new String("World");    
System.out.println( a == b );//false
System.out.println( a == c );//false
System.out.println( b == c );//false

例子2中a还是常量池中”HelloWorld”的引用,b因为无法在编译期确定,所以是运行时创建的新对象”HelloWorld”的引用,c因为有后半部分new String(“World”)所以也无法在编译期确定,所以也是一个新创建对象”HelloWorld”的引用,明白了这些也就知道为何得出此结果了。

**PS: ** String.intern()

再补充介绍一点:存在于.class文件中的常量池,在运行期被JVM装载,并且可以扩充。String的intern()方法就是扩充常量池的一个方法;当一个String实例str调用intern()方法时,Java查找常量池中是否有相同Unicode的字符串常量,如果有,则返回其的引用,如果没有,则在常量池中增加一个Unicode等于str的字符串并返回它的引用,看例3就清楚了。

例子3:

String a = "Hello";    
String b = new String("Hello");    
String c = new String("Hello");    
System.out.println( a == b );//false
System.out.println(**********);    
b.intern();   
c = c.intern(); //把常量池中"Hello"的引用赋给c    
System.out.println( a == b);//false虽然执行了b.intern()但没有赋值给b
System.out.println( a == b.intern() );//true  
System.out.println( a == c ); //true  

例子4:
关于equals()==:
equals()是比较两个对象的值是否相等,这个对于String简单来说就是比较两字符串的Unicode序列是否相当,如果相等返回true;而==是比较两字符串的地址是否相同,也就是是否是同一个字符串的引用。

例子5:
String是不可变的 :
这一说又要说很多,大家只要知道String的实例一旦生成就不会再改变了,比如说:

String str=”aa”+”bb”+” “+”cc”;

就是有4个字符串常量,首先”aa”和”bb”生成了”aabb”存在内存中,后”aabb”又和” “ 生成 ”aabb “存在内存中,最后又和生成了”aabb cc”,并把这个字符串的地址赋给了str,就是因为String的“不可变”产生了很多临时变量这也就是为什么建议用StringBuffer的原因了

猜你喜欢

转载自blog.csdn.net/qq_30885821/article/details/109293378