【JavaSE】深入理解String、StringTable、String.intern()

198 阅读18分钟

开启掘金成长之旅!这是我参与「掘金日新计划 · 12 月更文挑战」的第1天,点击查看活动详情

【JavaSE】深入理解String、StringTable、String.intern()String概览String字符串的不可变性JDK 6 和 JDK 7 中 substring 的原理及区别replaceFirst、replaceAll、replace 区别String 对“+”的重载字符串拼接的几种方式和区别使用+拼接字符串的实现原理concat 是如何实现的StringBuffer and StringBuilder效率比较String.valueOf 和 Integer.toString 的区别String的内存分配为什么StringTable要做调整?String.intern()入门理解:深入理解:面试题intern的使用:JDK6 vs JDK7/8如何分割一个String?如何判断两个String是否相等

String概览

String 被声明为 final,因此它不可被继承。

内部使用 char 数组存储数据,该数组被声明为 final,这意味着 value 数组初始化之后就不能再引用其它数组。并且 String 内部没有改变 value 数组的方法,因此可以保证 String 不可变。

public final class String
    implements java.io.Serializable, Comparable<String>, CharSequence {
    /** The value is used for character storage. */
    private final char value[];
​

String字符串的不可变性

不可变字符串主要是:

  • String 类被 final修饰不能被子类继承,进而避免了子类破坏 String不可变。
  • 保存字符串的数组被final修饰并且是私有的,并且 String 类没有提供和暴露修改这个字符串的方法。

实际中可变的原因:

其实并不是改变 String ,是新创建了一个 String 对象指向改变后的值,原本的 String 成为副本字符串对象存留在内存中。

定义一个字符串

String s = "abcd";

image-20220906222442331

s 中保存了 string 对象的引用。下面的箭头可以理解为“存储他的引用”。

使用变量来赋值变量

String s2 = s;

image-20220906222546772

s2 保存了相同的引用值,因为他们代表同一个对象。

字符串连接

s = s.concat("ef");

image-20220906222627276

s 中保存的是一个重新创建出来的 string 对象的引用。

不可变的好处

1. 可以缓存 hash 值

因为 String 的 hash 值经常被使用,例如 String 用做 HashMap 的 key。不可变的特性可以使得 hash 值也不可变,因此只需要进行一次计算。

2. String Pool 的需要

如果一个 String 对象已经被创建过了,那么就会从 String Pool 中取得引用。只有 String 是不可变的,才可能使用 String Pool。

img

3. 安全性

String 经常作为参数,String 不可变性可以保证参数不可变。例如在作为网络连接参数的情况下如果 String 是可变的,那么在网络连接过程中,String 被改变,改变 String 对象的那一方以为现在连接的是其它主机,而实际情况却不一定是。

4. 线程安全

String 不可变性天生具备线程安全,可以在多个线程中安全地使用。

JDK 6 和 JDK 7 中 substring 的原理及区别

substring(int beginIndex, int endIndex)方法在不同版本的 JDK 中的实现是不同的。

substring() 的作用

substring(int beginIndex, int endIndex)方法截取字符串并返回其[beginIndex,endIndex-1]范围内的内容。

调用 substring()时发生了什么?

你可能知道,因为 x 是不可变的,当使用 x.substring(1,3)对 x 赋值的时候,它会指向一个全新的字符串:

image-20220906224114377

然而,这个图不是完全正确的表示堆中发生的事情。因为在 jdk6 和 jdk7 中调用substring 时发生的事情并不一样。

JDK 6 中的 substring

String 是 通 过 字 符 数 组 实现 的 。 在 jdk 6 中 , String 类 包 含 三 个 成 员 变量 :char value[], int offset,int count。他们分别用来存储真正的字符数组,数组的第一个位置索引以及字符串中包含的字符个数。

当调用 substring 方法的时候,会创建一个新的 string 对象,但是这个 string 的值仍然指向堆中的同一个字符数组。这两个对象中只有 count 和 offset 的值是不同的。

image-20220906224213117

下面是证明上说观点的 Java 源码中的关键代码:

//JDK 6
String(int offset, int count, char value[]) {
this.value = value;
this.offset = offset;
this.count = count;
}
public String substring(int beginIndex, int endIndex) {
//check boundary
return new String(offset + beginIndex, endIndex - beginIndex, value);
}

JDK 6 中的 substring 导致的问题

如果你有一个很长很长的字符串,但是当你使用 substring 进行切割的时候你只需要很短的一段。这可能导致性能问题,因为你需要的只是一小段字符序列,但是你却引用了整个字符串(因为这个非常长的字符数组一直在被引用,所以无法被回收,就可能导致内存泄露)。在 JDK 6 中,一般用以下方式来解决该问题,原理其实就是生成一个新的字符串并引用他。

JDK 7 中的 substring

上面提到的问题,在 jdk 7 中得到解决。在 jdk 7 中,substring 方法会在堆内存中创建一个新的数组。

image-20220906224456964

Java 源码中关于这部分的主要代码如下:

//JDK 7
public String(char value[], int offset, int count) {
//check boundary
this.value = Arrays.copyOfRange(value, offset, offset + count);
}
public String substring(int beginIndex, int endIndex) {
//check boundary
int subLen = endIndex - beginIndex;
return new String(value, beginIndex, subLen);
}

以上是 JDK 7 中的 subString 方法,其使用 new String 创建了一个新字符串,避免对老字符串的引用。从而解决了内存泄露问题。

所以,如果你的生产环境中使用的 JDK 版本小于 1.7,当你使用 String 的 subString方法时一定要注意,避免内存泄露。

replaceFirst、replaceAll、replace 区别

replace、replaceAll 和 replaceFirst 是 Java 中常用的替换字符的方法,它们的方法定义是:

replace(CharSequence target, CharSequence replacement) ,用replacement 替换所有的 target,两个参数都是字符串。

replaceAll(String regex, String replacement) ,用 replacement 替换所有的regex 匹配项,regex 很明显是个正则表达式,replacement 是字符串。

replaceFirst(String regex, String replacement) ,基本和 replaceAll 相同,区别是只替换第一个匹配项。

可以看到,其中 replaceAll 以及 replaceFirst 是和正则表达式有关的,而 replace 和正则表达式无关。

replaceAll 和 replaceFirst 的区别主要是替换的内容不同,replaceAll 是替换所有匹配的字符,而 replaceFirst()仅替换第一次出现的字符。

String 对“+”的重载

1、String s = "a" + "b",编译器会进行常量折叠(因为两个都是编译期常量,编译期可知),即变成 String s = "ab"

2、对于能够进行优化的(String s = "a" + 变量 等)用 StringBuilder 的 append()方法替代,最后调用 toString() 方法 (底层就是一个 new String())

字符串拼接的几种方式和区别

字符串,是 Java 中最常用的一个数据类型了。字符串拼接是我们在 Java 代码中比较经常要做的事情,就是把多个字符串拼接到一 起。

我们都知道,String 是 Java 中一个不可变的类,所以他一旦被实例化就无法被修改。

但是,既然字符串是不可变的,那么字符串拼接又是怎么回事呢?

其实,所有的所谓字符串拼接,都是重新生成了一个新的字符串。

下面一段字符串拼接代码:

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

其实最后我们得到的 s 已经是一个新的字符串了。如下图:

image-20220906225424173

s 中保存的是一个重新创建出来的 String 对象的引用。

那么,在 Java 中,到底如何进行字符串拼接呢?字符串拼接有很多种方式,这里简单介绍几种比较常用的。

使用+拼接字符串

在 Java 中,拼接字符串最简单的方式就是直接使用符号+来拼接。如:

String a = "lyh";
String b = "yby";
String c = a + "," + b;

Concat

除了使用+拼接字符串之外,还可以使用 String 类中的方法 concat 方法来拼接字符串。

String a = "lyh";
String b = "yby";
String c = a.concat(b);

StringBuffer || StringBuilder

关于字符串,Java 中除了定义了一个可以用来定义字符串常量的 String 类以外,还提供了可以用来定义字符串变量的 StringBuffer 类,它的对象是可以扩充和修改的。

使用 StringBuffer 可以方便的对字符串进行拼接。如:

StringBuffer a = new StringBuffer("lyh");
String b = "yby";
StringBuffer c = a.append(",").append(b);

以上就是比较常用的五种在 Java 种拼接字符串的方式,那么到底哪种更好用呢?为什么 Java 开发手册中不建议在循环体中使用+进行字符串拼接呢?

image-20220906230204309

使用+拼接字符串的实现原理

还是这样一段代码。我们把他生成的字节码进行反编译,看看结果。

String a = "lyh";
String b = "yby";
String c = a + "," + b;
String a = "lyh";
String b = "yby";
String c = (new StringBuilder()).append(a).append(",").append(b).toString();

通过查看反编译以后的代码,我们可以发现,原来字符串常量在拼接过程中,是将String 转成了 StringBuilder 后,使用其 append 方法进行处理的。

那 么 也 就 是 说 , J a v a 中 的 + 对 字 符 串 的 拼 接 , 其 实 现 原 理 是 使 用StringBuilder.append。

concat 是如何实现的

我们再来看一下 concat 方法的源代码,看一下这个方法又是如何实现的。

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);
}

这段代码首先创建了一个字符数组,长度是已有字符串和待拼接字符串的长度之和,再把两个字符串的值复制到新的字符数组中,并使用这个字符数组创建一个新的 String 对象并返回。

通过源码我们也可以看到,经过 concat 方法,其实是 new 了一个新的 String,这也就呼应到前面我们说的字符串的不变性问题上了。

StringBuffer and StringBuilder

1. 可变性

  • String 不可变
  • StringBuffer 和 StringBuilder 可变

2. 线程安全

  • String 不可变,因此是线程安全的
  • StringBuilder 不是线程安全的
  • StringBuffer 是线程安全的,内部使用 synchronized 进行同步

接下来我们看看 StringBuffer 和 StringBuilder 的实现原理。和 String 类类似,StringBuilder 类也封装了一个字符数组,定义如下:

char[] value;

与 String 不同的是,它并不是 final 的,所以他是可以修改的。另外,与 String 不同,字符数组中不一定所有位置都已经被使用,它有一个实例变量,表示数组中已经使用的字符个数,定义如下:

int count;

其 append 源码如下:

public StringBuilder append(String str) {
super.append(str);
return this;
}

该类继承了 AbstractStringBuilder 类,看下其 append 方法:

public AbstractStringBuilder append(String str) {
if (str == null)
return appendNull();
int len = str.length();
ensureCapacityInternal(count + len);
str.getChars(0, len, value, count);
count += len;
return this;
}

append 会直接拷贝字符到内部的字符数组中,如果字符数组长度不够,会进行扩展。

StringBuffer 和 StringBuilder 类似,最大的区别就是 StringBuffer 是线程安全的,看一下 StringBuffer 的 append 方法。

public synchronized StringBuffer append(String str) {
toStringCache = null;
super.append(str);
return this;
}

该方法使用 synchronized 进行声明,说明是一个线程安全的方法。而 StringBuilder则不是线程安全的。

效率比较

用时从短到长的是:

StringBuilder < StringBuffer < concat < +

StringBuffer 在 StringBuilder 的基础上,做了同步处理,所以在耗时上会相对多一些。

那么问题来了,前面我们分析过,其实使用+拼接字符串的实现原理也是使用的StringBuilder,那为什么结果相差这么多,高达 1000 多倍呢?

我们再把以下代码反编译下:

image-20220906231108568

反编译后代码如下:

image-20220906231134899

我们可以看到反编译后的代码,在 for 循环中,每次都是 new 了一个 StringBuilder,然后再把 String 转成 StringBuilder,再进行 append。

而频繁的新建对象当然要耗费很多时间了,不仅仅会耗费时间,频繁的创建对象,还会造成内存资源的浪费。

所以, Java 开发手册建议:循环体内,字符串的连接方式,使用 StringBuilder 的append 方法进行扩展。而不要使用+。

总结

image-20220920004426689

因此,经过对比,我们发现,直接使用 StringBuilder 的方式是效率最高的。因为StringBuilder 天生就是设计来定义可变字符串和字符串的变化操作的。

但是,还要强调的是:

1、如果不是在循环体中进行字符串拼接的话,直接使用+就好了。 2 、 如 果 在 并 发 场 景 中 进 行 字 符 串 拼 接 的 话 , 要 使 用 StringBuffer来 代 替StringBuilder。

String.valueOf 和 Integer.toString 的区别

我们有三种方式将一个 int 类型的变量变成呢过 String 类型,那么他们有什么区别?

1.int i = 5;
2.String i1 = "" + i;
3.String i2 = String.valueOf(i);
4.String i3 = Integer.toString(i);

第三行和第四行没有任何区别,因为 String.valueOf(i)也是调用 Integer.toString(i)来实现的。

第二行代码其实是 String i1 = (new StringBuilder()).append(i).toString(); 首先创建一个 StringBuilder 对象,然后再调用 append 方法,再调用 toString 方法。

String的内存分配

  • 在Java语言中有8种基本数据类型和一种比较特殊的类型String。这些类型为了使它们在运行过程中速度更快、更节省内存,都提供了一种常量池的概念。

  • 常量池就类似一个Java系统级别提供的缓存。8种基本数据类型的常量池都是系统协调的,String类型的常量池比较特殊。

    • 直接使用双引号声明出来的String对象会直接存储在常量池中。

      • 比如:String info = “baidu.com”;
    • 如果不是用双引号声明的String对象,可以使用String提供的intern( )方法。

  • Java 6及以前,字符串常量池存放在永久代

  • Java 7中 Oracle的工程师对字符串池的逻辑做了很大的改变,即将字符串常量池的位置调整到Java堆内

    • 所有的字符串都保存在堆(Heap)中,和其他普通对象一样,这样可以让你在进行调优应用时仅需要调整堆大小就可以了。
    • 字符串常量池概念原本使用得比较多,但是这个改动使得我们有足够的理由让我们重新考虑在Java 7中使用String.intern()
  • Java8元空间,字符串常量在堆空间中

image-20220920003836026

image-20220920003846543

image-20220920003902743

为什么StringTable要做调整?

  • JDK7中将StringTable放到了堆空间中。因为永久代的回收效率很低,在Full GC的时候才会执行永久代的垃圾回收,而Full GC是老年代的空间不足、永久代不足时才会触发。
  • 这就导致StringTable回收效率不高,而我们开发中会有大量的字符串被创建,回收效率低,导致永久代内存不足。放到堆里,能及时回收内存

String.intern()

官方API文档中的解释:

当调用intern方法时,如果池子里已经包含了一个与这个String对象相等的字符串,正如equals(Object)方法所确定的,那么池子里的字符串会被返回。否则,这个String对象被添加到池中,并返回这个String对象的引用。

由此可见,对于任何两个字符串s和t,当且仅当s.equals(t)为真时,s.intern( ) == t.intern( )为真。

所有字面字符串和以字符串为值的常量表达式都是interned。

返回一个与此字符串内容相同的字符串,但保证是来自一个唯一的字符串池。

入门理解:

使用 String.intern() 可以保证相同内容的字符串变量引用同一的内存对象。

在 JVM 中,为了减少相同的字符串的重复创建,为了达到节省内存的目的。会单独开辟一块内存,用于保存字符串常量,这个内存区域被叫做字符串常量池。

当代码中出现双引号形式(字面量)创建字符串对象时,JVM 会先对这个字符串进行检查,如果字符串常量池中存在相同内容的字符串对象的引用,则将这个引用返回;否则,创建新的字符串对象,然后将这个引用放入字符串常量池,并返回该引用。

下面示例中,s1 和 s2 采用 new String() 的方式新建了两个不同对象,而 s3 是通过 s1.intern() 方法取得一个对象引用。intern() 首先把 s1 引用的对象放到 String Pool(字符串常量池)中,然后返回这个对象引用。因此 s3 和 s1 引用的是同一个字符串常量池的对象。

String s1 = new String("aaa");
String s2 = new String("aaa");
System.out.println(s1 == s2);           // false
String s3 = s1.intern();
System.out.println(s1.intern() == s3);  // true

如果是采用 "bbb" 这种使用双引号的形式创建字符串实例,会自动地将新建的对象放入 String Pool 中。

String s4 = "bbb";
String s5 = "bbb";
System.out.println(s4 == s5);  // true

深入理解:

  • intern() 是一个 native 方法,调用的是底层 C 的方法。
public native String intern();
  • 如果不是用双引号声明的String对象,可以使用String提供的intern方法,它会从字符串常量池中查询当前字符串是否存在,若不存在就会将当前字符串放入常量池中。
String myInfo = new string("I love 字节跳动").intern();
  • 也就是说,如果在任意字符串上调用String.intern方法,那么其返回结果所指向的那个类实例,必须和直接以常量形式出现的字符串实例完全相同。因此,下列表达式的值必定是true
("a"+"b"+"c").intern() == "abc"
  • 通俗点讲,Interned string就是确保字符串在内存里只有一份拷贝,这样可以节约内存空间,加快字符串操作任务的执行速度。注意,这个值会被存放在字符串内部池(String Intern Pool)
/**
 * 如何保证变量s指向的是字符串常量池中的数据呢?
 * 有两种方式:
 * 方式一: String s = "shkstart";//字面量定义的方式
 * 方式二: 调用intern()
 *         String s = new String("shkstart").intern();
 *         String s = new        StringBuilder("shkstart").toString().intern();
 */

image-20220920005128293

面试题

new String(“ab”)会创建几个对象?

/**
 * new String("ab") 会创建几个对象? 
 * 看字节码就知道是2个对象
 */
public class StringNewTest {
    public static void main(String[] args) {
        String str = new String("ab");
    }
}

image-20220920005527904

  • 这里面就两个对象

    • 一个对象是:new 关键字在堆空间中创建
    • 另一个对象:字符串常量池中的对象 “ab”

new String(“a”) + new String(“b”) 会创建几个对象

/**
 * new String("a") + new String("b") 会创建几个对象? 
 */
public class StringNewTest {
    public static void main(String[] args) {
        String str = new String("a") + new String("b");
    }
}
  • 我们转换成字节码来查看
 0 new #2 <java/lang/StringBuilder> //new StringBuilder()
 3 dup
 4 invokespecial #3 <java/lang/StringBuilder.<init> : ()V>
 7 new #4 <java/lang/String> //new String()
10 dup
11 ldc #5 <a> //常量池中的 “a”
13 invokespecial #6 <java/lang/String.<init> : (Ljava/lang/String;)V> //new String("a")
16 invokevirtual #7 <java/lang/StringBuilder.append : (Ljava/lang/String;)Ljava/lang/StringBuilder;> //append()
19 new #4 <java/lang/String> //new String()
22 dup
23 ldc #8 <b> //常量池中的 “b”
25 invokespecial #6 <java/lang/String.<init> : (Ljava/lang/String;)V> //new String("b")
28 invokevirtual #7 <java/lang/StringBuilder.append : (Ljava/lang/String;)Ljava/lang/StringBuilder;> //append()
31 invokevirtual #9 <java/lang/StringBuilder.toString : ()Ljava/lang/String;> //toString()里面会new一个String对象
34 astore_1
35 return
  • 我们创建了 6 个对象

    • 对象1:new StringBuilder()

    • 对象2:new String("a")

    • 对象3:常量池中的 “a”

    • 对象4:new String("b")

    • 对象5:常量池中的 “b”

    • 对象6:toString 中会创建一个 new String("ab")

    • toString( )的调用,在字符串常量池中,没有生成"ab"

    toString( )的调用,在字符串常量池中,没有生成"ab"

    备注:这个地方有个疑惑,为什么对象2会放入常量池,而toString()中new String却不会放入常量池?

    网上的解释:

    • StringBuilder中toString( )源码
            @Override
        public String toString() {
            // Create a copy, don't share the array
            return new String(value, 0, count);
        }
    

image-20220920012412603

  • 可以看到toString( )里面只是new了一个String对象,并没有存放到字符串常量池中

intern的使用:JDK6 vs JDK7/8

public class StringIntern {
​
    public static void main(String[] args) {
​
        /**
         * ① String s = new String("1")
         * 创建了两个对象
         *      堆空间中一个new对象
         *      字符串常量池中一个字符串常量"1"(注意:此时字符串常量池中已有"1")
         * ② s.intern()由于字符串常量池中已存在"1"
         *
         * s  指向的是堆空间中的对象地址
         * s2 指向的是堆空间中常量池中"1"的地址
         * 所以不相等
         */
        String s = new String("1");
        s.intern();//调用此方法之前,字符串常量池中已经存在了"1"
        String s2 = "1";
        System.out.println(s == s2);//jdk6:false   jdk7/8false
​
        /**
         * ① String s3 = new String("1") + new String("1")
         * 等价于new String"11"),但是,常量池中并不生成字符串"11";
         *
         * ② s3.intern()
         * 由于此时常量池中并无"11",所以把s3中记录的对象的地址存入常量池
         * 所以s3 和 s4 指向的都是一个地址
         */
        String s3 = new String("1") + new String("1");//s3变量记录的地址为:new String("11")
        //执行完上一行代码以后,字符串常量池中,是否存在"11"呢?答案:不存在!!
        s3.intern();//在字符串常量池中生成"11"。如何理解:jdk6:在常量池中真正创建了一个新的对象"11",也就有新的地址。
                                            //         jdk7:此时常量池中并没有真正创建"11",而是创建一个指向堆空间中new String("11")的地址
        String s4 = "11";//s4变量记录的地址:使用的是上一行代码代码执行时,在常量池中生成的"11"的地址
        System.out.println(s3 == s4);//jdk6:false  jdk7/8true
    }
​
}

JDK 6 中:

image-20220920012531121

JDK 7中:

image-20220920012554966

总结String的intern( )的使用

  • JDK1.6中,将这个字符串对象尝试放入字符串常量池中。

    • 如果字符串常量池中有,则并不会放入。返回已有的字符串常量池中的对象的地址
    • 如果没有,会把此对象复制一份,放入字符串常量池,并返回字符串常量池中的对象地址
  • JDK1.7起,将这个字符串对象尝试放入字符串常量池中。

    • 如果字符串常量池中有,则并不会放入。返回已有的字符串常量池中的对象的地址
    • 如果没有,则会把对象的引用地址复制一份,放入字符串常量池,并返回字符串常量池中的引用地址

练习(对JDK不同版本intern的进一步理解)

public class StringExer1 {
    
    public static void main(String[] args) {
        String s = new String("a") + new String("b");//new String("ab")
        //在上一行代码执行完以后,字符串常量池中并没有"ab"
​
        String s2 = s.intern();//jdk6中:在字符串常量池中创建一个字符串"ab",并把字符串常量池中的"ab"地址返回给s2
                               //jdk8中:字符串常量池中没有创建字符串"ab",而是创建一个引用,指向new String("ab"),将此引用返回给s2
​
        System.out.println(s2 == "ab");//jdk6:true  jdk8:true
        System.out.println(s == "ab");//jdk6:false  jdk8:true
    }
    
}

image-20220920012753276

image-20220920012824750

从网上扒拉了一些资料,发现有些东西从个人理解是有问题的,比如:

(先放出链接:) java中toString返回的字符串是存在字符串常量池中的吗? - 知乎 (zhihu.com)

image-20220920013143010

从个人理解,这里的概述只是针对于JDK7 / 8的吧。

如何分割一个String?

split() 方法根据匹配给定的正则表达式来拆分字符串。

语法:

public String[] split(String regex, int limit)

参数:

示例:

当limit等于1时,会把字符串分成长度为1的数组(就是转成数组,不分割)

        String str = "aaa,bbb,ccc,";
        String[] split = str.split(",", 1);
长度:1
值:
aaa,bbb,ccc,

当limit等于2时,会把字符串分成长度为2的数组(从第一个","分割)

        String str = "aaa,bbb,ccc,";
        String[] split = str.split(",", 2);
长度:2
值:
aaa
bbb,ccc,

以此类推,limit为多少,就分割成长度为多少的数组(最大长度等于可分割的长度)...

        String str = "aaa,bbb,ccc,";
        String[] split = str.split(",", 5);
长度:5
值:
[aaa,bbb,ccc,"",""]

有两个特殊的值 当limit为0的时候,会丢弃后面的空值,split()方法默认使用

    String str = "aaa,bbb,ccc,";
    String[] split = str.split(",", 0);
长度:3
值:
aaa
bbb
ccc

当limit为-1的时候,会保留后面的空字符串(按照可分割的最大长度分割)

        String str = "aaa,bbb,ccc,,";
        String[] split = str.split(",", -1);
长度:5
值:
[aaa,bbb,ccc,"",""]

如何判断两个String是否相等

有两种方式判断字符串是否相等,使用”==”或者使用equals方法。当使用”==”操作符时,不仅比较字符串的值,还会比较引用的内存地址。大多数情况下,我们只需要判断值是否相等,此时用equals方法比较即可。

        String s1 = "abc";
        String s2 = "abc";
        String s3= new String("abc");
        System.out.println("s1 == s2 ? "+(s1==s2)); //true
        System.out.println("s1 == s3 ? "+(s1==s3)); //false
        System.out.println("s1 equals s3 ? "+(s1.equals(s3))); //true