硬货,咱们来聊聊Java中String类~~~
Posted 司腾
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了硬货,咱们来聊聊Java中String类~~~相关的知识,希望对你有一定的参考价值。
java.lang.String 类可能是大家日常用的最多的类,但是对于它是怎么实现的,你真的明白吗? 认真阅读这篇文章,包你一看就明白了。
String 类定义
public final class String implements
java.io.Serializable, Comparable<String>, CharSequence
复制代码
从源码可以看出,String 是一个用 final 声明的常量类,不能被任何类所继承,而且一旦一个String对象被创建,包含在这个对象中的字符序列是不可改变的,包括该类后续的所有方法都是不能修改该对象的,直至该对象被销毁,这是我们需要特别注意的(该类的一些方法看似改变了字符串,其实内部都是创建一个新的字符串,下面讲解方法时会介绍)。接着实现了 Serializable接口,这是一个序列化标志接口,还实现了 Comparable 接口,用于比较两个字符串的大小(按顺序比较单个字符的ASCII码),后面会有具体方法实现;最后实现了 CharSequence 接口,表示是一个有序字符的集合,相应的方法后面也会介绍。
字段属性
/**用来存储字符串 */
private final char value[];
/** 缓存字符串的哈希码 */
private int hash; // Default to 0
/** 实现序列化的标识 */
private static final long serialVersionUID = -6849794470754667710L;
复制代码
一个 String 字符串实际上是一个 char 数组。
构造方法
String 类的构造方法很多。可以通过初始化一个字符串,或者字符数组,或者字节数组等等来创建一个 String 对象。
String str1 = "abc";//注意这种字面量声明的区别,文末会详细介绍
String str2 = new String("abc");
String str3 = new String(new char[]'a','b','c');
复制代码
equals(Object anObject) 方法
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;
复制代码
String 类重写了 equals 方法,比较的是组成字符串的每一个字符是否相同,如果都相同则返回true,否则返回false。
hashCode() 方法
public int hashCode()
int h = hash;
if (h == 0 && value.length > 0)
hash = h = isLatin1() ? StringLatin1.hashCode(value)
: StringUTF16.hashCode(value);
return h;
复制代码
String 类的 hashCode 算法很简单,主要就是中间的 for 循环,计算公式如下:
s[0]*31^(n-1) + s[1]*31^(n-2) + … + s[n-1]
s 数组即源码中的 val 数组,也就是构成字符串的字符数组。这里有个数字 31 ,为什么选择31作为乘积因子,而且没有用一个常量来声明?主要原因有两个:
1、31是一个不大不小的质数,是作为 hashCode 乘子的优选质数之一。
2、31可以被 JVM 优化,31 * i = (i « 5) - i。因为移位运算比乘法运行更快更省性能。
charAt(int index) 方法
public char charAt(int index)
if (isLatin1())
return StringLatin1.charAt(value, index);
else
return StringUTF16.charAt(value, index);
复制代码
我们知道一个字符串是由一个字符数组组成,这个方法是通过传入的索引(数组下标),返回指定索引的单个字符。
intern() 方法
这是一个本地方法:
public native String intern();
当调用intern方法时,如果池中已经包含一个与该String确定的字符串相同equals(Object)
的字符串,则返回该字符串。否则,将此String对象添加到池中,并返回此对象的引用。
这句话什么意思呢?就是说调用一个String对象的intern()
方法,如果常量池中有该对象了,直接返回该字符串的引用(存在堆中就返回堆中,存在池中就返回池中);如果没有,则将该对象添加到池中,并返回池中的引用。
String str1 = "hello";//字面量 只会在常量池中创建对象
String str2 = str1.intern();
System.out.println(str1==str2);//true
String str3 = new String("world");//new 关键字只会在堆中创建对象
String str4 = str3.intern();
System.out.println(str3 == str4);//false
String str5 = str1 + str2;//变量拼接的字符串,会在常量池中和堆中都创建对象
String str6 = str5.intern();//这里由于池中已经有对象了,直接返回的是对象本身,也就是堆中的对象
System.out.println(str5 == str6);//true
String str7 = "hello1" + "world1";//常量拼接的字符串,只会在常量池中创建对象
String str8 = str7.intern();
System.out.println(str7 == str8);//true
复制代码
关于String类里面的众多方法,这里不一一介绍了,下面我们来深入了解一下,String 类不可变型。
面试精选
分析一道经典的面试题:
public static void main(String[] args)
String A = "abc";
String B = "abc";
String C = new String("abc");
System.out.println(A==B);
System.out.println(A.equals(B));
System.out.println(A==C);
System.out.println(A.equals(C));
复制代码
答案是:true、true、false、true
对于上面的题目,我们可以先来看一张图,如下:
首先 String A= “abc”,会先到常量池中检查是否有“abc”的存在,发现是没有的,于是在常量池中创建“abc”对象,并将常量池中的引用赋值给A;第二个字面量 String B= “abc”,在常量池中检测到该对象了,直接将引用赋值给B;第三个是通过new关键字创建的对象,常量池中有了该对象了,不用在常量池中创建,然后在堆中创建该对象后,将堆中对象的引用赋值给C,再将该对象指向常量池。
需要说明一点的是,在object中,equals()是用来比较内存地址的,但是String重写了equals()方法,用来比较内容的,即使是不同地址,只要内容一致,也会返回true,这也就是为什么A.equals(C)返回true的原因了。
注意:看上图红色的箭头,通过 new 关键字创建的字符串对象,如果常量池中存在了,会将堆中创建的对象指向常量池的引用。
再来看一道题目,使用包含变量表达式创建对象:
String str1 = "hello";
String str2 = "helloworld";
String str3 = str1+"world";//编译器不能确定为常量(会在堆区创建一个String对象)
String str4 = "hello"+"world";//编译器确定为常量,直接到常量池中引用
System.out.println(str2==str3);//fasle
System.out.println(str2==str4);//true
System.out.println(str3==str4);//fasle
复制代码
str3 由于含有变量str1,编译器不能确定是常量,会在堆区中创建一个String对象。而str4是两个常量相加,直接引用常量池中的对象即可。
String 不可变性
String类是Java中的一个不可变类(immutable class)。
简单来说,不可变类就是实例在被创建之后不可修改。
String不可变这个话题应该是老生长谈了,String自打娘胎一出生就跟他们的兄弟姐妹不一样,好好的娃被戴了一个final的帽子,
以至于byte,int,short,long等基本类型的小伙们都不带它玩。
如果你仔细阅读源码注释,你会发现这样一句话:
大致意思就是String是个常量,从一出生就注定不可变。
首先需要补充一个容易混淆的知识点:当使用final修饰基本类型变量时,不能对基本类型变量重新赋值,因此基本类型变量不能被改变。但对于引用类型变量而言,它保存的仅仅是一个引用,final只保证这个引用变量所引用的地址不会改变,即一直引用同一个对象,但这个对象完全可以发生改变。例如某个指向数组的final引用,它必须从此至终指向初始化时指向的数组,但是这个数组的内容完全可以改变。
String 类是用 final 关键字修饰的,所以我们认为其是不可变对象。但是真的不可变吗?
每个字符串都是由许多单个字符组成的,我们知道其源码是由 char[] 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
复制代码
value 被 final 修饰,只能保证引用不被改变,但是 value 所指向的堆中的数组,才是真实的数据,只要能够操作堆中的数组,依旧能改变数据。而且 value 是基本类型构成,那么一定是可变的,即使被声明为 private,我们也可以通过反射来改变。
public static void main(String[] args) throws Exception
String str = "Hello World";
System.out.println("修改前的str:" + str);
System.out.println("修改前的str的内存地址" + System.identityHashCode(str));
// 获取String类中的value字段
Field valueField = String.class.getDeclaredField("value");
// 改变value属性的访问权限
valueField.setAccessible(true);
// 获取str对象上value属性的值
char[] value = (char[]) valueField.get(str);
// 改变value所引用的数组中的字符
value[3] = '?';
System.out.println("修改后的str:" + str);
System.out.println("修改前的str的内存地址" + System.identityHashCode(str));
复制代码
修改前的str:Hello World
修改前的str的内存地址1746572565
修改后的str:Hel?o World
修改前的str的内存地址1746572565
复制代码
通过前后两次打印的结果,我们可以看到 str 值被改变了,但是str的内存地址还是没有改变。但是在代码里,几乎不会使用反射的机制去操作 String 字符串,所以,我们会认为 String 类型是不可变的。
不可变的好处
首先,我们应该站在设计者的角度思考问题,而不是觉得这不好,那不合理:
- 可以实现多个变量引用堆内存中的同一个字符串实例,避免创建的开销。
- 我们的程序中大量使用了String字符串,有可能是出于安全性考虑。
- 当我们在传参的时候,使用不可变类不需要去考虑谁可能会修改其内部的值,如果使用可变类的话,可能需要每次记得重新拷贝出里面的值,性能会有一定的损失。
小结
String 被new时是要创建对象的,+ 号拼接同理,程序中尽量不要使用 + 拼接,推荐使用StringBuffer或者StringBuilder。
以上是关于硬货,咱们来聊聊Java中String类~~~的主要内容,如果未能解决你的问题,请参考以下文章
精选版:用Java扩展Nginx(nginx-clojure 入门)
精选版:用Java扩展Nginx(nginx-clojure 入门)