[翻译]在字符串常量池中字符串方法,关键字以及运算符如何处理字符串比较 (How String methods, keywords, and operators process comparison)

时间:2022-06-09 19:58:35

原文链接:https://www.javaworld.com/article/3276354/java-language/java-challengers-2-string-comparisons.html

在Java中,String类封装一个char数组。简单来说,String 是一个char数组用于组合文字,语句或者任何你想要的数据。

封装是面向对象编程最强大的概念之一。因为封装,你不用了解String类是如何运作的,你只需要知道如何从它的接口中使用对应的方法。

当你阅读String类的源码,你能了解char数组是如何被封装的:

public String(char value[]) {
    this(value, 0, value.length, null);
} 

为了更好地理解封装,我们可以以一辆车为例。为了能够驾驶车辆我们是否需要了解车辆在引擎盖下是如何运作的?当然不需要,但是你需要知道车辆的接口能够做什么比如油门,刹车以及方向盘。在这里,每一个接口都具有某个具体的功能比如加速,刹车,向左转,向右转。这些和面向对象编程是一样的。

什么是字符串常量池?

String可能是Java中最常用的一个类。如果我们每次都创建一个新的String对象,可能就会导致浪费跟多内存。字符串常量池能够通过存储每一个String值对应的对象,示例如下:

[翻译]在字符串常量池中字符串方法,关键字以及运算符如何处理字符串比较 (How String methods, keywords, and operators process comparison)

尽管我们创建了数个值为Duck和Juggy的String对象,实际上只有两个对象被创建并存储在堆上。为了证明以上结论,可以参考以下示例代码(这里重申下在Java中“==”运算符用于比较两个对象是否相等):

String juggy = "Juggy";
String anotherJuggy = "Juggy";
System.out.println(juggy == anotherJuggy); 

这段代码会返回true,因为这两个对象在字符串常量池中指向的是同一个对象。它们的值是一样的。

一个例外:“new”运算符

现在我们来看下这段代码,它看起来和上面的代码很相似但是却仍有一点不同。

String duke = new String("duke");
String anotherDuke = new String("duke");

System.out.println(duke == anotherDuke);

基于之前的实例,你也许会认为这段代码会返回true但是实际上却是false。使用new运算符会强制在堆中创建一个新的String对象。因此,这里JVM会创建两个不同的对象。

字符串常量池与intern()方法

为了在字符串常量池中存储一个String,我们会用到一个称为String interning的技术。以下是从Javadoc中摘录的关于intern()方法的说明:

/**
     * Returns a canonical representation for the string object.
     *
     * A pool of strings, initially empty, is maintained privately by the
     * class {@code String}.
     *
     * When the intern method is invoked, if the pool already contains a
     * string equal to this {@code String} object as determined by
     * the {@link #equals(Object)} method, then the string from the pool is
     * returned. Otherwise, this {@code String} object is added to the
     * pool and a reference to this {@code String} object is returned.
     *
     * It follows that for any two strings {@code s} and {@code t},
     * {@code s.intern() == t.intern()} is {@code true}
     * if and only if {@code s.equals(t)} is {@code true}.
     * 
     * All literal strings and string-valued constant expressions are
     * interned. String literals are defined in section 3.10.5 of the
     * The Java™ Language Specification.
     *
     * @returns  a string that has the same contents as this string, but is
     *          guaranteed to be from a pool of unique strings.
     * @jls 3.10.5 String Literals
     */ public native String intern();
intern()方法用于在字符串常量池中存储String。首先它会验证你所创建的String是否存在于常量池中。如果不存在,则在常量池中创建一个新的String。

现在我们来看下当我们使用new关键字强制创建两个String会发生什么,代码如下:

String duke = new String("duke");
String duke2 = new String("duke");
System.out.println(duke == duke2); // The result will be false here
System.out.println(duke.intern() == duke2.intern()); // The result will be true here

不同域之前的示例,在这个例子中,这里的比较结果为true。这是因为这里使用了intern()方法来保证该字符串存储于常量池中。

String类的equals方法

在String类中,equals()方法用于验证两个Java类是否相同。因为equals()方法来自Object类,每一个Java类都会继承它。但是为了使该方法能够正确地工作,equals()方法必须重写。当然,String类也继承了equals()方法,代码如下:

public boolean equals(Object anObject) {
    if (this == anObject) {
        return true;
    }
    
    if (anObject instanceof String) {
        String aString = (String)anObject;
        if (coder() == aString.coder()) {
          return isLatin1() ? StringLatin1.equals(value, aString.value)
            : StringUTF16.equals(value, aString.value);
        }
    }
    
    return false;
}

正如所见,equals方法比较的是String类的值而不是对象的引用。它不关心对象的引用是否不同而是比较对象的值。

最常用的String方法

在你比较String之前还有最后一件事需要注意,观察以下String类的常用方法:

// Removes spaces from the borders
trim() 
// Gets a substring by indexes
substring(int beginIndex, int endIndex)
// Returns the characters length of the String
length() 
// Replaces String, regex can be used.
replaceAll(String regex, String replacement)
// Verifies if there is a specified CharSequence in the String
contains(CharSequences) 

挑战String比较!

让我们尝试以下挑战。

在这里,你将使用我们学到的String概念来比较多个String。观察以下代码,你能否得出每一个比较的结果?

public class ComparisonStringChallenge {
	public static void main(String... doYourBest) {
		String result = "";
		result += " powerfulCode ".trim() == "powerfulCode" 
				? "0" : "1";

		result += "flexibleCode" == "flexibleCode" ? "2" : "3";
		
		result += new String("doYourBest") 
				== new String("doYourBest") ? "4" : "5";

		result += new String("noBugsProject")
				.equals("noBugsProject") ? "6" : "7";

        result += new String("breakYourLimits").intern()
                == new String("breakYourLimits").intern() ? "8" : "9";

		System.out.println(result);
	}
}

以下哪一个是正确选项?

A:02468

B:12469

C:12579

D:12568

答案是D

上述代码发生了什么?理解String运行原理

在第一段代码中我们看到:

result += " powerfulCode ".trim() == "powerfulCode" 
				? "0" : "1";

尽管String经过trim()方法调用之后不会改变,String “ powerfulcode ”在这种情况下是不一样的。在这里比较结果是false,因为当trim()方法从边界移除空格,JVM将强制通过new运算符创建一个新的String对象。

我们再看接下来的代码:

result += "flexibleCode" == "flexibleCode" ? "2" : "3";

这里毫无疑问,两个string在String pool中是相同的,这里比较的结果是true。

接下来的代码如下:

result += new String("doYourBest") 
				== new String("doYourBest") ? "4" : "5";

使用new保留关键字强制创建两个新的String对象,无论他们是否相等。在这种情况下比较的结果是false即使他们的值是相同的。

接下来的代码如下:

result += new String("noBugsProject")
				.equals("noBugsProject") ? "6" : "7";

因为我们使用了equals()方法,两个String的值会进行比较而不是对象的实例。在这个例子中,JVM不关心对象是否相同因为比较的是它们的值。这里比较的结果是true。

关于String一些常见的错误

确认两个String是否指向同一个object是困难的,特别对于包含相同值的String对象来说。需要记住的是使用new关键字创建对象,即使创建的对象值是相同的,JVM仍旧会创建一个新的对象。

关于String需要记住的项

a)String至不可变的因此String的状态不可变。

b)为了节约内存,JVM会在String pool中存储String对象。当一个信息String被创建,JVM会检查它的值并指向一个已经存在的对象。如果在pool中不存在对应值的String对象,JVM将会创建一个String。

c)使用“==”是比较对象的引用,使用equals()方法比较的是String的值。相同的规则适用于所有对象。

d)当使用new关键字时,JVM将会创建一个新的String即使已存在值相同的String对象。