java 源码探索系列 -01 String

本贴最后更新于 2225 天前,其中的信息可能已经物是人非

这一系列的源码探索我打算结合一些常见的疑问深入探究一下。答案也许都知道,但是从源码角度来说为什么会这样,我想还是有许多人是模糊状态的。所以一起来带着问题来看看 java 的设计吧 ~

String 类中包含了许多方法,关于它的构造方法就有十来种。此外还有一些工具方法,比如字符串的比较:equals()contentEquals()compareTo()compareToIgnoreCase() 等等,另外还有比如字符串的长度 length(),字符串的拼接 concat() 等等方法....

1. equals()、hashCode()、==的区别?

众所周知,equals() hashCode() 在 Object 类中也有同样的方法:

//equals():就是直接比较对象的内存地址(==) //另外在看源码的过程中还注意到一个注释:当需要重写equals方法时,必须要重写HashCode方法,因为相等的对象必须具有相等的哈希代码。 /** Note that it is generally necessary to override the {@code hashCode} * method whenever this method is overridden, so as to maintain the * general contract for the {@code hashCode} method, which states * that equal objects must have equal hash codes. */ public boolean equals(Object obj) { return (this == obj); } //hashCode():返回对象的JVM内存地址 public native int hashCode();

下面再来看看 String 中重写的这两个方法又增加了什么内容吧 ~

public boolean equals(Object anObject) { //第一步,如果引用地址相同就直接返回true if (this == anObject) { return true; } //第二步,当地址不同的情况下,当都是Stirng类的实例的时候,然后再去挨个的比较它们的char字符是否相等 if (anObject instanceof String) { String anotherString = (String)anObject; int n = value.length; if (n == anotherString.value.length) { char v1[] = value; char v2[] = anotherString.value; int i = 0; while (n-- != 0) { if (v1[i] != v2[i]) return false; i++; } return true; } } return false; } private int hash; // Default to 0 // final修饰,因此String的内容也是不变的 private final char value[];// The value is used for character storage. public int hashCode() { int h = hash; if (h == 0 && value.length > 0) { char val[] = value; for (int i = 0; i < value.length; i++) { h = 31 * h + val[i]; } hash = h; } return h; }

嗯嗯,代码看完可以直接得出这个问题的结论了。
1. String 类中的 equals()方法用来比较 char 内容,而 Object 类的 equals()方法仅仅用来比较对象的引用地址。
2. String 类中的 hashCode()方法是重新计算出的 hashCode 数值,而 Object 类的 hashCode()方法返回对象的 JVM 内存地址。
3. ==是用来比较对象的内存地址的
4. 重写 equals()方法必须要重写 hashCode()方法。(其实什么引用地址内存地址指的就是一个地址,就是 Object 类中 hashCode()32 位 JVM 地址)

2. 创建 String 对象有哪些方式?它们的区别是什么?

第一个问题其实是非常清晰的:
1.我们常用到的 Stirng s = "xxx" 方式;
2.既然它是有构造函数的,我想它肯定也能通过构造函数来创建吧?话不多说直接看代码吧

// 这里只列出几个常用的构造,毕竟它有十几个构造,全列出来文章篇幅有点大... private final char value[]; private int hash; // Default to 0 public String() { this.value = "".value; } public String(String original) { this.value = original.value; this.hash = original.hash; } public String(byte bytes[]) { this(bytes, 0, bytes.length); } public String(StringBuffer buffer) { synchronized(buffer) { this.value = Arrays.copyOf(buffer.getValue(), buffer.length()); } } public String(StringBuilder builder) { this.value = Arrays.copyOf(builder.getValue(), builder.length()); }

好啦,上面的代码块只是列举出了创建 String 的第二种方法。还是无法看出这两种创建方式的区别是什么,那下面我们还是用一个例子来比较一下吧 ~

public static void main(String[] args) { String si = new String("ok"); String ok = "ok"; System.out.println(si==ok); // false System.out.println(ok==("o"+"k")); // true }

嗯?第一个为 false 我知道,但是为什么第二个为 true 呢?

其实这就引用到 JVM 堆内存里的常量池的定义了。常量池(constant pool)指的是在编译期被确定,并被保存在已编译的.class 文件中的一些数据。包括类、方法中的一些常量

那么,这里用 new 创建的显然是不属于常量池的咯。但是用 new 的方式如果常量池里面没有的话,它首先也会在常量池里面创建。

是的,其实 new 就是创建的一个字符串对象。当然,可以使用这个方法 intern() 返回从字符串池中的字符串对象的引用。

下面再来看一下这个问题

// 在没有创建相同字符串的情况下,下面会创建几个String对象? String s1 = new String("Sean Blog Site: https://code666.top"); String s2 = new String("Sean Blog Site: https://code666.top");

两个?恭喜你答错了!答案是三个。

第一个是字符串常量池中 Sean Blog Site: https://code666.top,另外两个就是堆内存创建的 Stirng 对象咯。所以应该是三个

还不懂???

其实它的执行顺序是这样的,首先在 string 池内找,如果找到就不创建 string 对象,否则创建,这样就有了一个 string 对象,然后遇到 new 运算符号了,在内存上创建 string 对象,并将其返回给 s1(s2),又一个对象。

还不懂???

那你的执行顺序应该要这样:搬上你的电脑主机爬上顶楼,然后朝向远处做抛物线运动...

3. String、StringBuilder、StringBuffer 的区别?

关于这个经典面试题,我想大多数人都能回答出。

区别:

  1. String 创建的字符内容是不可变的,而 StirngBuilder 和 StringBuffer 是可变的
  2. StringBuffer 是线程安全的,StringBuilder 是非线程安全,String 也属于线程安全(字符内容是不可变的)
  3. 运行速度(字符拼接数据较多的情况):StringBuilder 运行速度最快,StringBuffer 运行速度略快,String 运行速度最慢
//第一个区别: //String中char字段 private final char value[]; //StringBuffer private transient char[] toStringCache; //StringBuilder:直接使用的是父类AbstractStringBuilder的字段 char[] value;

由上可知,String 类的字符内容是不可变的,因为是用的 final 修饰。而 StringBuffer 与 StringBuilder 创建的字符内容都是可变的。transient 是用来表示该字段在序列化与反序列化不变,

//第二个区别 //StringBuffer中方法基本都用`synchronized`修饰 @Override public synchronized int length() { return count; } @Override public synchronized int capacity() { return value.length; } @Override public synchronized void ensureCapacity(int minimumCapacity) { super.ensureCapacity(minimumCapacity); } //而StringBuilder中的方法直接用public修饰,没有考虑到并发的情况 @Override public StringBuilder deleteCharAt(int index) { super.deleteCharAt(index); return this; } @Override public StringBuilder replace(int start, int end, String str) { super.replace(start, end, str); return this; }

由上可知,StringBuffer 是属于线程安全,就是因为它的方法都用关键词 synchronized 来修饰,而 StringBuilder 则没有。另外,String 为什么也是线程安全呢?因为它是不可变的,不可变的肯定也是属于线程安全啊。

第三个区别:

在数据量大的情况下为什么 StringBuilder>StringBuffer>String 呢?

其实从上面的代码可以得到答案,用 synchronized 修饰的方法多多少少也会影响到速度了。而 String 每次都要创建一个新的对象,那肯定也会是最慢的了。

  • Java

    Java 是一种可以撰写跨平台应用软件的面向对象的程序设计语言,是由 Sun Microsystems 公司于 1995 年 5 月推出的。Java 技术具有卓越的通用性、高效性、平台移植性和安全性。

    3196 引用 • 8215 回帖 • 2 关注

相关帖子

欢迎来到这里!

我们正在构建一个小众社区,大家在这里相互信任,以平等 • 自由 • 奔放的价值观进行分享交流。最终,希望大家能够找到与自己志同道合的伙伴,共同成长。

注册 关于
请输入回帖内容 ...
  • pleaseok
    作者

    回帖测试

  • pleaseok
    作者

    我知道了。。。这边评论的信息自己博客接收不到,博客评论的信息这边倒还能接收到。。