重走Java路之String你真的了解吗?
上回我们回顾完了“基本数据类型”,提到了基本数据类型的与其包装类之间的转换等知识,期间我们提到了引用数据类型,今天我们就来说一说Java当中最常用的引用数据类型-字符串String。
在开始将字符串之前,我先提一个“什么是字符串”?这个习惯源于我的老师每次上课前也是喜欢先提问题,记得有一次讲文件系统,他半开玩笑的提了一个同学们“什么是文件系统”,当时我想其实大多数跟我一样心里知道是怎么个意思但是没法用一句比较精炼的话回答出来。后来还是老师自己说文件系统不就是一个文件的管理系统嘛这有什么难的怎么都没人回答。那“字符串是什么呢”?,字符串不就是一连串的字符嘛,一连串的字符不就是一个字符数组嘛。是吗?是!我可以很负责任的告诉你是,不信咱们看源码。
public final class String
implements java.io.Serializable, Comparable<String>, CharSequence {
/** The value is used for character storage. */
private final char value[];
这下应该相信了这个答案,明白了String丫就是一字符数组,Java中的String是通过数组来存储维护字符串的。
明白了String是什么之后我们再来看一个比较常见的Java面试题。
如下的代码会输出什么?
public static void main(String[] args) {
String a = "hello";
String b = new String("hello").intern();
String c = "he" + "llo";
String d = "he";
String e = d + "llo";
String f = new String("hellow");
String g = new String("java");
String h = "java";
String i = new String("java");
System.out.println(a == b);
System.out.println(a == c);
System.out.println(a == e);
System.out.println(a ==f);
System.out.println(g == h);
System.out.println(g ==i);
}
这里我还是坚持我的习惯先揭晓答案
true
true
false
false
false
false
完了之后呢我们再来分析答案。
a==b为什么为true,我们先来看看String a = "hello"的构建过程,我们都知道jvm有自己的内存模型,也有字符串常量池的存在,jvm在构建String a = "hello"时其实是:
- 先去检查字符串常量池中是否有"hello"字符串的引用,如果没有则先new一个"hello"存放于堆中,再将该字符串的引用存放于字符串常量中(ps:我的jdk版本为1.8,jdk1.6的字符串构建过程会有一点区别,后面我们细说jvm内存模型时再讲解)。
- 如果字符串常量池中存在"hello"的引用则直接将a指向该引用。
很显然例题中String a = "hello"的构建过程是先new一个"hello"存放于堆中,完了之后再将"hello"对象的引用存放于堆中。
接下来我们再来看一看String b = new String("hello").intern()的构建过程,这里我还是坚持我的习惯我们先去读一读源码,看看intern方法的注释写的啥。
/**
* Returns a canonical representation for the string object.
* <p>
* A pool of strings, initially empty, is maintained privately by the
* class {@code String}.
* <p>
* 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.
* <p>
* 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}.
* <p>
* All literal strings and string-valued constant expressions are
* interned. String literals are defined in section 3.10.5 of the
* <cite>The Java™ Language Specification</cite>.
*
* @return a string that has the same contents as this string, but is
* guaranteed to be from a pool of unique strings.
*/
public native String intern();
这个时候呢,就需要发挥你的英语水平,这些balaba的注释的意思是啥呢?
行吧虽然我的英语很low,但是我还是跟大家翻译一下,这个注释的意思大致就是说:intern方法返回字符串对象的引用,系统初始化的时候字符串常量池为空,当我们调用intern方法时,会先检查字符串常量池中是否有该字符串的引用存在,如果存在则直接返回常量池中的该引用。如果不存在则先去new该字符串对象,完了之后再将该字符串的引用存放于字符串常量池中并返回该引用。
回到题目a==b,因为在构建String a = "hello"的时候已经将"hello"的引用保存在了字符串常量池中,所以我们在执行String b = new String("hello").intern()时是直接返回的常量池中"hello"的引用,所以a==b为true。
如果大家感觉这个native方法没有方法体看的不爽,想看看他的源码我们可以去openjdk官网去查询,没办法oracle掌管Java之后一切都变了,我们要查看源码只能去openjdk官网查看openjdk8的源码,为方便浏览我建议下载下来浏览。
接下来我们再看a==c,因为上面我们已经分析了变量a的构建过程,这里我们直接分析String c = "he" + "llo"的构建过程,变量c的构建其实是先构建字符串"he"与"llo"完了之后再构建"hello",而对于这些确立的字符串jvm会在编译期间提前将"he"、"llo以及"hellow"加入到字符串常量中,因为此时"hello"的引用已经存在于字符串常量池中,所以a==c为true。
接下来我们再看a==e,由上一题a==c为true,我们应该觉得a==e应该也为true才对啊,为什么此时却为false呢?因为jvm在编译加载期间只能提前构建那些确立的字符串,也就是说对String e = d + "llo"这类含有变量的字符串在编译加载期间是无法提前初始化的。必须要等到执行期间才能完成字符串的实例化,而在执行期间字符串的拼接其实是通过StringBuilder的append方法来完成,最终字符串的实例化是通过调用StringBuilder的toString方法完成的。所以此时a==e为false。
接下来我们再看a==f,这里应该很简单得到a==f为false吧,因为变量a与变量f压根指向的就不是两个相同对象。
同理g==h,g==i都为false,因为他们是不同的对象。
这里我们补充一点知识,说一说==与equals的关系,看到csdn上很多帖子都是说==比较的是引用地址equals是得看你是如何重写的equals()方法,更有甚者说的是==比较的是引用equals比较的是值。其实我感觉这些说法都对但是却没有包含太多自己的理解,在阅读<<Head First Java>>这本书时,记得书上是这么说的==比较的是对象,我很赞同这个观点==就是比较两个变量指向的是否为同一个对象,没有那么复杂的什么应用地址啥的,记住==比较的是对象,java是面向对象的语言。
看完上面的经典面试题,我想你应该对String有了一定新的认识,但是既然是重走Java路,这里我们再来回顾一下String类中那些不太常用的方法。
第一个方法:toCharArray()
/**
* Converts this string to a new character array.
*
* @return a newly allocated character array whose length is the length
* of this string and whose contents are initialized to contain
* the character sequence represented by this string.
*/
public char[] toCharArray() {
// Cannot use Arrays.copyOf because of class initialization order issues
char result[] = new char[value.length];
System.arraycopy(value, 0, result, 0, value.length);
return result;
}
为什么要单独说一下,这个方法呢?一个是为了加深大家对字符串就是一个字符数组的认识,二个是因为有一个基础面试题就是对字符串进行反转。因为我们很少使用toCharArray()所以很多的时候我们第一时间想到的都是循环遍历字符串来实现,基础比较好的同学可能会想起来用StringBuilder和StringBuffer的reverse()方法,但是其实呢如果我们记得toCharArray()方法将字符串转化成数组也是很容易实现的。
第二个方法:charAt(int index)
/**
* Returns the {@code char} value at the
* specified index. An index ranges from {@code 0} to
* {@code length() - 1}. The first {@code char} value of the sequence
* is at index {@code 0}, the next at index {@code 1},
* and so on, as for array indexing.
*
* <p>If the {@code char} value specified by the index is a
* <a href="Character.html#unicode">surrogate</a>, the surrogate
* value is returned.
*
* @param index the index of the {@code char} value.
* @return the {@code char} value at the specified index of this string.
* The first {@code char} value is at index {@code 0}.
* @exception IndexOutOfBoundsException if the {@code index}
* argument is negative or not less than the length of this
* string.
*/
public char charAt(int index) {
if ((index < 0) || (index >= value.length)) {
throw new StringIndexOutOfBoundsException(index);
}
return value[index];
}
通过阅读注释我们很容易知道该方法,用于返回指定索引处的字符。为什么提出来仅仅只是应为用的少,为了加深印象,因为Java毕竟不像Python那样可以直接通过[index]方式取值。
第三个方法:trim()
/**
* Returns a string whose value is this string, with any leading and trailing
* whitespace removed.
* <p>
* If this {@code String} object represents an empty character
* sequence, or the first and last characters of character sequence
* represented by this {@code String} object both have codes
* greater than {@code '\u005Cu0020'} (the space character), then a
* reference to this {@code String} object is returned.
* <p>
* Otherwise, if there is no character with a code greater than
* {@code '\u005Cu0020'} in the string, then a
* {@code String} object representing an empty string is
* returned.
* <p>
* Otherwise, let <i>k</i> be the index of the first character in the
* string whose code is greater than {@code '\u005Cu0020'}, and let
* <i>m</i> be the index of the last character in the string whose code
* is greater than {@code '\u005Cu0020'}. A {@code String}
* object is returned, representing the substring of this string that
* begins with the character at index <i>k</i> and ends with the
* character at index <i>m</i>-that is, the result of
* {@code this.substring(k, m + 1)}.
* <p>
* This method may be used to trim whitespace (as defined above) from
* the beginning and end of a string.
*
* @return A string whose value is this string, with any leading and trailing white
* space removed, or this string if it has no leading or
* trailing white space.
*/
public String trim() {
int len = value.length;
int st = 0;
char[] val = value; /* avoid getfield opcode */
while ((st < len) && (val[st] <= ' ')) {
st++;
}
while ((st < len) && (val[len - 1] <= ' ')) {
len--;
}
return ((st > 0) || (len < value.length)) ? substring(st, len) : this;
}
其实trim()方法我们都经常使用,也都大概明白他是什么意思--去掉字符串首尾的空格,在工作中我们也都是这么干的,但是我们仔细阅读源码会发现ascii小于等于' '(32)的字符都会被去掉,而且也不是所谓的去掉其实是返回的一个新的字符串。所以这个提到trim()方法主要是加深大家对该方法的理解。
相信大家对String的理解比我还深所以我就不班门弄斧,故事未完,请听下回分解,重走Java路-Java常用数据结构-数组。