前言
在我看来,任何一门程序语言,使用到最多的都是字符串。而在Java中,我们使用String类来表示字符串。在Java中,字符串类型不是基本类型,但他依然是一个非常重要的类型。今天我们将从几个方面来分析下这个我们在编程中使用最频繁的类String。
为什么说String是不可变的
什么是不可变
我的理解是说你对一个字符串的任何修改操作都会生成一个新的对象,而不是在原有的对象内存中修改,这就是String的不可变。
为什么不可变
要探究String为什么是不可变的,我们必须要去看String的源码了。
public final class String implements java.io.Serializable, Comparable<String>, CharSequence { /** The value is used for character storage. */ private final char value[]; ... }
我们只给出最关键的三行源码,我们通过观察源码可以知道,String的底层实现的是依靠char数组的。
而且我们发现String这个类使用了final来修饰,有些人是不是看到String使用了final修饰就说这个就是String类是不可变的呢,很遗憾的说,并不是,并没有那么简单,这里为什么要用final修饰,你需要真正明白final这个关键字修饰类时候的作用,这里先不说为什么要用final修饰,但是可以先告诉大家,肯定是有作用的,但是并不是关键。
我们先需要分析这个关键的底层实现-char数组。
我们发现这个数组也是使用了final修饰,那是不是意味着这个终于是String不可变的关键了呢?很遗憾,也不是,我们来回顾一下final关键字在修饰变量的时候的作用把:
当final修饰基本类型时,其数值一旦在初始化之后便不能更改。
当final修饰普通类型时,一旦该引用指向了一个对象,便该引用不能再指向另一对象,也就是说该引用指向的堆内存空间地址是一个固定值,不能改变。我们看到这里使用final修饰了一个char类型的数组,char类型是基本类型,但是char数组并不属于基本类型,属于引用类型(关于Java中的数组,后面会讲到)。既然是引用,那我们就知道了,虽然我们不能改变这个引用指向的内存地址,但是,我们能够改变这个引用指向的堆内存中的具体内容。
@Test public void arrayTest() { final char[] c= {'d','g'}; System.out.println(c); c[1]= 'k'; System.out.println(c); }
输出为:
dg dk
很明显改变了引用指向的堆内存中的具体内容,所以仅仅是final修饰了这个数组引用,并不能做到我们要求的即不改变引用的指向,也不改变具体内容。
String之所以不可变,关键是因为SUN公司的工程师在String类的所有方法中都很小心的没有去动这个数组中的元素,而且没有暴露内部成员变量。所以说,private这个限定符的作用在这里比final关键字作用还要大,他防止用户可以直接的去使用这个数组。这就防止了用户直接通过String类去获得char数组,进入对其作出修改,想必说到这里,你应该知道为什么String类需要使用final修饰了把,这是为了防止其他的类来继承String来破坏他的不可变性。我们一定要明白,String之所以不可变,和final真的没有太大的关系,但是使用final还是有其作用的。
但是还有一个问题,String类真的能保证完全的不可变吗?
因为Java中反射的存在,导致了程序员还是可以去改变SUN公司的大佬们辛辛苦苦封装好的String类的。
反射是Java中一个强大的技术,但是也带来了一些问题,之前我们就在序列化的时候,说过反射破坏序列化的问题。
现在我们就简单来看下反射是如何破坏String的不可变的。
@Test public void reflectTest() throws NoSuchFieldException, SecurityException, IllegalArgumentException, IllegalAccessException { String s="kobe"; Field field=s.getClass().getDeclaredField("value"); field.setAccessible(true); char[] c=(char[]) field.get(s); c[2]='j'; System.out.println(s); } }
上面的程序里,我们就利用了反射来更改了String,这样做确实是改变了String的不变性,但是一般我们并不会这样去做。
不可变的优势
我们都知道String是不可变的,那么到底为什么Java的设计者要把String设计成不可变的呢?大家有想过这个问题吗?今天我们就来通过代码来看看把String设计成不可变的优势。
StringBuilder和String
在多线程环境中的优势
String在字符串常量池中的特性
我们都知道,当出现如下代码的时候:
String a="aaa"; String b="aaa";
a和b两个引用都指向字符串常量池(JDK1.6及之前,常量池位于方法区内(方法区逻辑上也是在堆中,但是我们习惯把它称为非堆区域),在JDK1.7及之后,我们的常量池之间位于堆中了)中的同一个内存块。这样在大量使用字符串的时候,就可以节约大量的内存,而想要实现这个特性,我们的最基本的前提就是保证String不可变。
String的intern()方法
在讲述了String不可变之后,我们在来看看String中的一个很特别的方法intern()方法,我们对这个方法的分析有利于我们了解JMM(Java内存模型)。
首先,我们来看下intern()这个方法是用来干什么的。他是一个native本地方法,最开始的初衷是用来重用String对象,以节省内存消耗。
那么我们就先通过代码证明一下是否真的可以这样。
给出一个测试方法
@Test
public void test() {
Integer[] sample = new Integer[10];
Random random = new Random(1000);
for (int i = 0; i < sample.length; i++) {
sample[i] = random.nextInt();
}
//记录程序开始时间
long t = System.currentTimeMillis();
//使用/不使用intern方法为10万个String赋值,值来自于Integer数组的10个数
for (int i = 0; i < MAX; i++) {
//arr[i] = new String(String.valueOf(sample[i % sample.length]));
arr[i] = new String(String.valueOf(sample[i % sample.length])).intern();
}
System.out.println((System.currentTimeMillis() - t) + "ms");
System.gc();
}
先定义一个长度为10的Integer数组,并随机为其赋值,在通过for循环为长度为10万的String对象依次赋值,这些值都来自于Integer数组。两种情况分别运行,可通过Window —> Preferences –> Java –> Installed JREs设置JVM启动参数为-agentlib:hprof=heap=dump,format=b,将程序运行完后的hprof置于工程目录下。再通过MAT插件查看该hprof文件。(注:intern()方法的内容大多引用一篇博客:http://blog.youkuaiyun.com/seu_calvin/article/details/52291082)
我们会发现使用了intern()方法之后生成的String对象确实是减少了很多。
不过我们今天要介绍这个方法的主要原因并不是这个点,主要是要解开由下面这段代码的输出结果带来的困惑:
@Test
public void internTest() {
//String s1="KOBE";
String s3 = new String("KO") + new String("BE");
s3.intern();
String s4 = "KOBE";
System.out.println(s3.intern()==s4);
System.out.println(s3 == s4);
}
我们运行代码输出为:
true
true
而当我们把注释放开的时候,即在前面加一行
String s1="KOBE";
我们再运行的时候,就会发现
false
false
注:上面的代码是在JDK1.8的环境下测试的
你会不会觉得奇怪,为什么加了一行代码,就导致了结果的完全不同呢?
这就需要我们深入的去了解intern()方法
深入理解intern()方法
之前有有提到JDK1.7后,常量池被放入到堆空间中,这导致intern()函数的功能不同,具体怎么个不同法,且看看下面代码:
““java
@Test
public void internTest1() {
String s = new String(“1”);
s.intern();
String s2 = “1”;
System.out.println(s == s2);
String s3 = new String("1") + new String("1");
s3.intern();
String s4 = "11";
System.out.println(s3 == s4);
}
““
原文中说测试结果是
JDK1.6以及以下:false false
JDK1.7以及以上:false true
但是我在本地JDK1.8的环境里都是false,但是如果内容不是”1”的话,如果换成”2”。
返回的都会是true,这一个问题我一直没有搞懂。
如果是
@Test
public void internTest1() {
String s = new String("1");
s.intern();
String s2 = "1";
System.out.println(s == s2);
//
String s3 = new String("2") + new String("2");
s3.intern();
String s4 = "22";
System.out.println(s3 == s4);
}
在JDK1.8下就会是false true。
我们就用这个代码来分析下吧
JDK1.6及以下
JDK1.7以上
String s3 = new String(“2”) + newString(“2”),这行代码在字符串常量池中生成“2” ,并在堆空间中生成s3引用指向的对象(内容为”22”)。注意此时常量池中是没有 “22”对象的。
s3.intern(),这一行代码,是将 s3中的“22”字符串放入 String 常量池中,此时常量池中不存在“22”字符串,JDK1.6的做法是直接在常量池中生成一个 “11” 的对象。但是在JDK1.7中,常量池中不需要再存储一份对象了,可以直接存储堆中的引用。这份引用直接指向 s3 引用的对象,也就是说s3.intern() ==s3会返回true。
String s4 = “22”, 这一行代码会直接去常量池中创建,但是发现已经有这个对象了,此时也就是指向 s3 引用对象的一个引用。因此s3 == s4返回了true。
啊啊啊,不想写呀,以后在写把。主要是在JDK1.7中,常量池中不需要再存储一份对象了,可以直接存储堆中的引用。这份引用直接指向 s3 引用的对象,也就是说s3.intern() ==s3会返回true。之所以会有区别就是这个造成。
String的编译优化
最后再来看下String和final的一个小知识点。
还是直接上代码
@Test
public void stringFinalTest() {
final String s1="KO";
String s2=s1+"BE";
String s3="KOBE";
System.out.println(s2==s3);
String s4="KO";
String s5=s4+"BE";
System.out.println(s3==s5);
final String s6=new String("KO");
String s7=s6+"BE";
System.out.println(s3==s7);
}
输出结果:
true
false
false
如果只给你代码,你能准确的给出输出结果吗?
我们来分析下为什么是这样一个结果