1.String的特性
1.1不变性
我们常常听人说,HashMap 的 key 建议使用不可变类,比如说 String 这种不可变类。这里说不可变指的是类值一旦被初始化,就不能再被改变了,如果被修改,将会是新的类,我们写个demo 来演示一下。
public
class
test
{
public
static
void
main
(
String
[
] args
)
{
String str
=
"hello"
;
str
=str
+
"world"
;
}
}
从代码上来看,s 的值好像被修改了,但从 debug 的日志来看,其实是 s 的内存地址已经被修了,也就说 s =“world” 这个看似简单的赋值,其实已经把 s 的引用指向了新的 String,debug 截图显示内存地址已经被修改,两张截图如下,我们可以看到标红的地址值已经修改了。


用示意图来表示堆内存,即见下图。

我们可以看下str的地址已经改了,说了生成了两个字符串,String类的官方注释为 Strings are constant; their values cannot be changed after they are created. 简单翻译下为 字符串是常量;它们的值在创建后不能更改。
下面为String的相关代码,如下代码,我们可以看到:
1. String 被 final 修饰,说明 String 类绝不可能被继承了,也就是说任何对 String 的操作方法,都不会被继承覆写,即可保证双亲委派机制,保证基类的安全性。
2. String 中保存数据的是一个 char 的数组 value。我们发现 value 也是被 final 修饰的,也就是说 value 一旦被赋值,内存地址是绝对无法修改的,而且 value 的权限是 private 的,外部绝对访问不到,String没有开放出可以对 value 进行赋值的方法,所以说 value 一旦产生,内存地址就根本无法被修改。
/** The value is used for character storage. */
private final char value
[
]
;
/** Cache the hash code for the string */
private int hash
;
// Default to 0
/** use serialVersionUID from JDK 1.0.2 for interoperability */
private
static final long serialVersionUID
=
-
6849794470754667710L
;
1.2相等判断
相等判断逻辑写的很清楚明了,如果有人问如何判断两者是否相等时,我们可以从两者的底层结构出发,这样可以迅速想到一种贴合实际的思路和方法,就像 String 底层的数据结构是 char 的数组一样,判断相等时,就挨个比较 char 数组中的字符是否相等即可。
(这里先挖个坑,携程问过类似题目)
public boolean
equals
(
Object anObject
)
{
//如果地址相等,则直接返回true
if
(
this
== anObject
)
{
return
true
;
}
//如果为String字符串,则进行下面的逻辑判断
if
(anObject
instanceof
String
)
{
//将对象转化为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循环挨个比较每个char
while
(
n
--
!=
0
)
{
if
(v1
[i
]
!= v2
[i
]
)
return
false
;
i
++
;
}
return
true
;
}
}
return
false
;
}
相等逻辑的流程图如下,我们可以看到整个流程还是很清楚的。

1.3替换操作
替换在平时工作中也经常使用,主要有 replace 替换所有字符、replaceAll 批量替换字符串、replaceFirst这三种场景。
下面写了一个 demo 演示一下三种场景:
public
static
void
main
(
String
[
] args
)
{
String str
=
"hello word !!"
;
System
.out
.
println
(
"替换之前 :"
+ str
)
;
str
= str
.
replace
(
'l'
,
'd'
)
;
System
.out
.
println
(
"替换所有字符 :"
+ str
)
;
str
= str
.
replaceAll
(
"d"
,
"l"
)
;
System
.out
.
println
(
"替换全部 :"
+ str
)
;
str
= str
.
replaceFirst
(
"l"
,
""
)
;
System
.out
.
println
(
"替换第一个 l :"
+ str
)
;
}
输出的结果是:

这边要注意一点是 replace和 replaceAll的区别, 不是替换和替换所有的区别哦。
而是replaceAll支持 正则表达式,因此会对参数进行解析(两个参数均是),如replaceAll("\\d", "*"),而replace则不会,replace("\\d","*")就是替换"\\d"的字符串,而不会解析为正则。
1.4 intern方法
String.intern() 是一个 Native 方法,即是c和c++与底层交互的代码,它的作用(在
JDK1.6和1.7操作不同
)是:
如果运行时常量池中已经包含一个等于此 String 对象内容的字符串,则直接返回常量池中该字符串的引用;
如果没有, 那么
在jdk1.6中,将此String对象添加到常量池中,然后返回这个String对象的引用(此时引用的串在常量池)。
在jdk1.7中,放入一个引用,指向堆中的String对象的地址,返回这个引用地址(此时引用的串在堆)。
/**
* 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.
*java学习交流:737251827 进入可领取学习资源及对十年开发经验大佬提问,免费解答!
* @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
(
)
;
如果看上面看不懂,我们来看下一下具体的例子,并来分析下。
public
static
void
main
(
String
[
] args
)
{
String s1
=
new
String
(
"学习Java"
)
;
s1
.
intern
(
)
;
String s2
=
"学习Java"
;
System
.out
.
println
(s1
== s2
)
;
String s3
=
new
String
(
"学习Java"
)
+
new
String
(
"test"
)
;
s3
.
intern
(
)
;
String s4
=
"学习Javatest"
;
System
.out
.
println
(s3
== s4
)
;
}
我们来看下结果,实际的打印信息如下。

为什么显示这样的结果,我们来看下。所以在 jdk7 的版本中,字符串常量池已经从方法区移到正常的堆 区域了。
-
第一个false: 第一句代码String s1 = new String("学习Java");生成了2个对象。常量池中的“学习Java” 和堆中的字符串对象。
s1.intern();这一句是 s1 对象去常量池中寻找后,发现 “学习Java的小姐姐” 已经在常量池里了。接下来String s2 = "学习Java的";这句代码是生成一个 s2的引用指向常量池中的“学习Java”对象。 结果就是 s 和 s2 的引用地址明显不同,所以为打印结果是false。 -
第二个true:先看 s3和s4字符串。
String s3 = new String("学习Java") + new String("test");,这句代码中现在生成了3个对象,是字符串常量池中的“学习Java” ,"test"和堆 中的 s3引用指向的对象。此时s3引用对象内容是”学习Javatest”,但此时常量池中是没有 “学习Javatest”对象的,接下来s3.intern();这一句代码,是将 s3中的“学习Javatest”字符串放入 String 常量池中,因为此时常量池中不存在“学习Javatest”字符串,常量池不需要再存储一份对象了,可以直接存储堆中的引用。这份引用指向 s3 引用的对象。 也就是说引用地址是相同的。最后String s4 = "学习Javatest";这句代码中”学习Javatest”是显示声明的,因此会直接去常量池中创建,创建的时候发现已经有这个对象了,此时也就是指向 s3 引用对象的一个引用。所以 s4 引用就指向和 s3 一样了。因此最后的比较s3 == s4是 true。

我们再看下,如果把上面的两行代码调整下位置,打印结果是不是不同。
public
static
void
main
(
String
[
] args
)
{
String s1
=
new
String
(
"学习Java"
)
;
String s2
=
"学习Java"
;
s1
.
intern
(
)
;
System
.out
.
println
(s1
== s2
)
;
String s3
=
new
String
(
"学习Java"
)
+
new
String
(
"test"
)
;
String s4
=
"学习Javatest"
;
s3
.
intern
(
)
;
System
.out
.
println
(s3
== s4
)
;
//java学习交流:737251827 进入可领取学习资源及对十年开发经验大佬提问,免费解答!
}

第一个false: s1 和 s2 代码中,
s1.intern();,这一句往后放也不会有什么影响了,因为对象池中在执行第一句代码
String s = new String("学习Java");的时候已经生成“
学习Java”对象了。下边的s2声明都是直接从常量池中取地址引用的。 s 和 s2 的引用地址是不会相等的。
**第二个false:**与上面唯一的区别在于
s3.intern(); 的顺序是放在
String s4 = "学习Javatest";后了。这样,首先执行
String s4 = "学习Javatest";声明 s4 的时候常量池中是不存在“
学习Javatest”对象的,执行完毕后,“
学习Javatest“对象是 s4 声明产生的新对象。然后再执行
s3.intern();时,常量池中“
学习Javatest”对象已经存在了,因此 s3 和 s4 的引用是不同的。
2. String、StringBuilder和StringBuffer
2.1 继承结构
2.2 主要区别
1)String是不可变字符序列,StringBuilder和StringBuffer是可变字符序列。
2)执行速度StringBuilder > StringBuffer > String。
3)StringBuilder是非线程安全的,StringBuffer是线程安全的。