String 分析

讲得好啊…

Java 源码分析 — String 的设计

这篇讲的太好了,源码方面的分析直接看这篇吧…

JDK1.8关于运行时常量池, 字符串常量池的要点

Java 永久代去哪儿了

JAVA中String.intern的理解

几张图轻松理解String.intern()

说明

讨论基于 jdk 1.8 +

这一块太抽象了,很多地方要有图才说明得清楚…我只是稍作记录,便于回顾,就不弄那么详细了…

1
2
3
public final class String
implements java.io.Serializable, Comparable<String>, CharSequence {
}

能看到,String 类由 final 修饰,这代表 String 初始化后就不能改变.这里的改变,指的是创建的对象不可变.

理解错了!!!

String,StringBuffer 和 StringBuilder 都是 final 类,String 不可变是由于里面真正存储数据的属性 :

1
2
/** The value is used for character storage. */
private final char value[];

value 数组为 final…

比如:

1
2
String str = "hello";
str = "world";

看起来似乎是改变了,实际上只是引用的指向变了,第一次创建的那个对象 “hello” 并没有变化.

其次,String 实现了三个接口.

三个接口

java.io.Serializable

这个是序列化.

在源码中有一个属性:

1
2
/** use serialVersionUID from JDK 1.0.2 for interoperability */
private static final long serialVersionUID = -6849794470754667710L;

java 在序列化时会通过判断类的 serialVersionUID 验证版本一致性.

Comparable

排序…

CharSequence

查看,有 length(), charAt(int index), subSequence(int start, int end) 这几个 api.

StringBuffer 和 StringBuilder 也要实现该接口.

实现

看源码,底层由一个 char 数组实现

1
private final char value[];

String 不可变,String 类中每一个看起来会修改 String 值的方法,实际上都是新建了一个对象.

以 substring() 举例.

1
2
3
4
5
6
7
public String substring(int beginIndex, int endIndex) {
...
int subLen = endIndex - beginIndex;
...
return ((beginIndex == 0) && (endIndex == value.length)) ? this
: new String(value, beginIndex, subLen);
}

实际上返回了一个新对象.

PS : 本来想用字符串的拼接举例的,比如

1
String str = "hell" + "o";

记得在 C++ 中可以重载 “+” 运算符,可是 JAVA 怎么做呢…

[java 编程思想]

java 并不允许程序员重载任何操作符,用于 String 的 “+” 和 “+=” 是 java 中仅有的两个重载过的操作符.

jdk 1.8 文档

字符串连接是通过StringBuilder (或StringBuffer )类及其append方法实现的.

[java 编程思想]

编译期做了一定程度的优化,自动引入了 StringBuilder,调用它的 append 和 toString 方法实现拼接.

不能依赖编译器的优化,频繁拼接应选用 StringBuilder.

创建对象

String 创建对象有两种方式.

1
2
1. String str1 = "hello";
2. String str2 = new String("hello");

看源码

1
2
3
4
5
6
7
8
9
10
11
12
/**
* Initializes a newly created {@code String} object so that it represents
* the same sequence of characters as the argument; in other words, the
* newly created string is a copy of the argument string. Unless an
* explicit copy of {@code original} is needed, use of this constructor is
* unnecessary since Strings are immutable.
*
*/
public String(String original) {
this.value = original.value;
this.hash = original.hash;
}

第二种方式实际上创建了两个对象.

in other words, the newly created string is a copy of the argument string.

参数实际传的是一个 String 对象的引用,这里生成了一个对象.

第二次又用 new 关键字创建了一个对象,这个对象实际是参数对象的副本.

区别

到底有啥区别…

先引入 JVM 中字符串常量池的概念…

字符串常量池

[…他妈的什么鬼,睡觉了…

日了狗了 jvm 书没带回家…

这段吐槽要保留到这…

2019 -01-31-23:06]

jdk 1.7 以前,字符串常量池在运行时常量池里,而运行时常量池放在方法区.在此时(jdk 1.7),hotspot 虚拟机对方法区的实现为永久代.永久代这个称呼是为了垃圾回收而命名的,具体这里暂不谈.

在 jdk 1.7 中,把字符串常量池从方法区移动到堆中.而运行时常量池中的其他东西,不变动.

字符串常量池的字符串被存储在永久代,导致了一系列的性能问题和内存溢出错误.于是在 jdk 1.8 中,hotspot 虚拟机移除了永久代,用元空间来表示.(为什么?好处是什么?)

此时,字符串常量池在堆中,运行时常量池在方法区, 不过方法区的实现从永久代变成了元空间.

区别

由 intern() 方法开始讨论.

1
2
3
String str = "hello";
1. System.out.println(str);
2. System.out.println(str.intern());

从输出结果来看,1,2 没区别…

1
2
hello
hello

[头大啊,网上咋说的都有…]

intern() 返回的是字符串对象的规范表示.

怎么个规范法?

它返回的内容取自具有唯一字符串的值.

怎么说?

When the intern method is invoked, if the pool already contains a string equal to this String object as determined by the equals(Object)method, then the string from the pool is returned. Otherwise, this String object is added to the pool and a reference to this String object is returned.

调用 intern() 时,比如

"hello".intern();

如果池(字符串常量池)中已经有一个与这个字符串对象("hello")相等的字符串(用 equal() )判断.(equal() 用于判断两个对象是否相等,== 用于判断两者是否指向同一个地址.)那么,返回这个字符串常量池中的字符串.

(!!! 应该也是返回字符串的引用吧?)

Ps : 返回字符串的引用这个说法似乎恰当些,虽然反正最后的效果都是栈中的引用指向字符串的地址…

否则,将此 String 对象添加到池中,并返回此 String 对象的引用.


这么说,字符串常量池存放了对象,也存放了引用?

有的说,字符串常量池应该只存放引用,但是按照说明,似乎也应该存对象.

对象应当放在堆中,但是字符串常量池不也在堆中嘛…这样似乎说得通.


new 关键字产生的对象都是放在堆中的,String 也不例外.

1
String str = new String("hello");

前面有提到这段代码,这段代码实际产生的对象放在堆中.

此时再调用

1
str.intern();

通过判断,常量池中没有该字符串,于是编译期将 “hello” 字符串添加到字符串常量池中,然后返回该常量的引用.

此后再调用 intern(),就直接返回常量池中 “hello” 的引用.


我们回头看创建 String 对象的两种方式.

1
2
1. String str1 = "hello";
2. String str2 = new String("hello");

第二种,new 关键字生成对象,在运行期确定,字符串对象在堆中分配.然后调用 intern() 时才会…(如上,就不赘叙了.)

而第一种,值 “hello” 在编译期确定,所以直接去字符串常量池中查找是否存在相同的字符串,若存在,就返回该字符串(或者说该字符串的引用),达到栈中的引用指向该字符串的效果.

若不存在,则在常量池中生成一个字符串,再将栈中的引用指向该字符串.

网上一些博文这样说 :

常量池中保存的仍然是引用,实例在堆中分配.

实例应该是在字符串常量池中分配.(当然,上面这样说也没错,毕竟字符串常量池也在堆中…)

不同之处在于字符串常量池中的对象及内容是唯一的,而堆中,可以有两个内容相同的不同对象.

例如:

1
2
3
4
String s1 = “abc”; 
String s2 = “abc”;
System.out.println(s1 == s2);
输出 : true
1
2
3
4
String s1 = new String(“abc”); 
String s2 = new String(“abc”);
System.out.println(s1 == s2);
输出 : false

囧囧囧囧囧囧囧囧囧囧囧囧囧囧囧囧囧囧.

不知道有没有理解错,就这样吧!我要吃饭了.

JVM 内存模型

https://hqweay.cn/2018/10/09/jvm/#内存管理机制

拼接字符串

[JAVA 编程思想中文版] P284

尝试了一下拼接字符串,然后调用 jdk 自带的反编译工具查看…

javap 使用

首先对 java 文件进行编译.如 Test.java

javac Test.java

生成了 Test.class

再执行 javap -c Test.calss

或者 javap -c Test

即可查看反编译的代码.

测试

1
String str4 = "haha" + "hehe" + 112 + "shsh";

当只有这么一行的时候,反编译效果.

1
2
ldc           #5                  // String hahahehe112shsh
astore 4

后面并未注释使用了 StringBuilder.

但是执行

1
2
3
for(int i = 0; i < 15; i++){
str4 += "llo";
}

反编译发现:

1
2
3
4
5
6
7
8
9
30: new           #6                  // class java/lang/StringBuilder
33: dup
34: invokespecial #7 // Method java/lang/StringBuilder."<init>":()V
37: aload 4
39: invokevirtual #8 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
42: ldc #9 // String llo
44: invokevirtual #8 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
47: invokevirtual #10 // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
50: astore 4

这些代码看不懂啊,大概知道是这么回事就行了…再说再说…