首先看一下string的一部分源码吧
public final class String
private final char value[];
我们暂且只看这两行,
第一行String被final修饰,表示String不可以被继承且它的所有方法都隐式的被final所修饰。(不懂的伙伴可以看以下final,我也写了关于这个关键字的博文)
对于第二行呢,我们可以看到String内部有一个char类型的数组。这个数组被设置为final类型,也就是说这个数组引用被设置为常量,这可以被赋值一次。其次这个value被private修饰,这就保证了我们不能在类的外部获取到这个value,也不能更改value引用(这里指的是不能改变这个引用的赋值,因为是final类型,已经被指定,只能是那个数组,但是可以改变数组的元素。)所以这就保证了我们不可以在String外部对string进行改变。所以说String是一个不可变对象(),String内部的值也是固定的。而我们的引用指向另一个string是指向了另一个对象,而不是最初的这个对象。(但是实际上String内部value指向的数组是可以改变的,但是一般没人动他,因为要通过反射来做这件事。
如果你真的想动它,看我另一篇关于更改string底层数组的博文,不过要知道怎么使用反射。而且,也要懂这篇文章的知识才可以看懂那个代码)
但是我们还是要说,在设计上,我们的string是一个不可变对象。
任何对于String的操作都会使其创建一个新的对象(String)
字符串常量池:
目的是为了,节省我们创建相同字符串的时间与空间。(记住哦,接下来讲一个知识点的时候会用到它)
首先我们需要说明两个概念:
Java中的常量池分为两种:
静态常量池:.class文件中的常量池,存储了类的字符串、数字等常量、还包含类信息和方法信息
运行时常量池:JVM类加载之后会将class文件中的静态常量池加载到方法区中,其中字符串常量、数字常量(int char 这些常量)会保存在运行时常量池里。
运行时常量池包含:字符串常量池和其它的基本数据类型的各自的常量池。
而我们平时说的常量池就是指的运行时常量池。
在这里记住一句话,运行时常量池的所有对象,都是在类加载之后直接完成的。程序运行之后产生的数据类型并不会在这里存储。
当我们在编写代码时出现的所有常量都会被编译器编译成一个常量
比如说 string = “kkk”
这个kkk会被放在字符串常量池中,作为一个字符串常量。
在这里我们只讨论字符串常量池,其它的讨论也没啥意义。
首先我们从代码编写开始讨论,因为在编译时期有一个优化的过程
String str = “kkk” 等价于 string str = “kkk“
string str = "kkk"+"1"等价于string str = "kkk1"
因为在编译时期编译器在这里直接优化了。实际上在最终的字节码文件,这里的kkk不存在
1也不存在,存在的只有一个kkk1
string str = ”kkk“
string str2 = str +”1“
这里就不一样了啊,因为在str2的时候我们使用的是引用类型,所以在编译时期并不能确定你这个引用类型的确定值(你要知道,他是一个变量,即使你给他初始化了,但是,如果采用多线程的模式,是会被改变的,所以不能确定它到底是什么。所以编译器不优化它)
这时候在字符串常量池里只有 kkk和1这两个
string str = ”1“
string str2 = ”kkk“+”mmm“+str+”lll“
等价于 str2 = ”kkkmmm“+str + ”lll“
其实上一部分没有说的很全,说了一部分,因为在编译器编译时期,它会从左边开始优化,但是当它碰到第一个变量之后就停止优化了。(我认为这样顺序举例,大家可以更直观的感受到。不是上面故意不说清楚的,其实这里还没说完。接着看下面吧)
我们在上面的那种情况下其实有一个特例情况
final string str = ”kkk“
string str2 = str +"mmm"
等价为str2 = ”kkkmmm“
在这里因为str是一个被初始化的常量,所以在编译时期,编译器会直接把所有的str都替换成它的具体值。
这里一定要强调在编译之前已经初始化(也就是你代码中明确赋值 str = ‘kkk")但是如果在运行时初始化的话,那就不行了。
那编译器就不会进行优化了,因为编译器没办法替换。(运行时初始化是指的通过方法初始化那种)
类编译之后,会将这些字符串常量放在类的.class文件的静态常量区
好啦,以上我把我能想到的情况都说出来了。以下就该到字符串常量池了。
我们都知道(上面说的)运行时常量池中的字符串常量池是存储类中明确出现的字符串的,所以在类加载的过程中。会将.class文件中的字符串常量都加载到运行时字符串常量池里。所以说类中的所有明确表示的字符串常量都在我们的字符串常量池里(包括final string str)
之后我们该讨论一下,我们程序执行过程中的字符串了。
实际上,在我们程序执行过程中字符串的创建包括了以下几个情况
以下情况在字符串常量池里有(”kkk“,”mmm“,”lll“)这三个字符串的情况下。
首先第一种是
String str = ”kkk“,这种情况并不会创建一个字符串对象,因为我们的字符串常量池里已经有了kkk了。
所以就会直接使栈内的str引用指向常量池里的kkk
string str = new string(”kkk“)这里会创建一个对象,首先kkk已经存在不属于这段代码创建的哈(这里只说运行时)
这里记住一句话,一切的new,都会创建一个对象,绝大部分在堆区,这又匿名类对象在栈内。
上面个的代码就会在堆内创建一个string对象。然后此时的str指向的其实是堆内的string对象,然后堆内的String对象才会去字符串常量池找有没有”kkk“这个字符串,如果有堆内对象的value会等同于字符串常量池里的kkk的value。也就是指向了同一个数组,从而使得string对象和字符串常量池里的string,相等。但是如果string str = new string(s1 +”kkk“),这种传入的参数不是字符串常量池中的,实际上,他的value会指向在堆内的字符串对象的数组,而那个数组的值就是s1+”kkk“。也可以看成它指向了另一个字符串对象。不过那个string是堆内的不是字符串常量池内的。(在这里我们需要说明一下,在编译时期的字符串优化,如果没有完全优化,还存在引用类型,他会在运行时期创建一个stringbuilder来进行+操作,最后使用tostring方法来创建一个string对象,而这个string对象是在堆内的。)
实际上我们说的字符串指向哪个字符串实际上都是把哪个字符串的value拷贝了一份。其实两个字符串对象指向的是同一个字符数组。
补充:所有的字符串的操作,只要不是用反射给人家间接的改,实际上都会创建另一个string对象。因为string是不可变的
被final、private修饰了的value。
向字符串常量池里添加字符串对象:
其实也不是不可以添加,不过不建议吧,如果你的一个字符串是在运行时获得的且用的比较多,可以使用str.intern()方法,可以把它放进去。
关于 string 、stringbuilder 、 stringbuffer的关系
实际上 stringbuilder 、 stringbuffer内部都维护了一个char数组。
通过append向char数组里拼接字符串,最后使用tostring方法来生成返回一个string对象,可以说是为string服务的吧。
在我们的+的时候用的是stringbuilder,
stringbuilder是线程不安全的buffer是安全的。
在需要使用大量字符串拼接的时候单线程建议使用stringbuilder,多线程建议使用stringbuffer。
==和equals
首先说明哈,数组、类、接口、这些都是引用类型哈,不是值类型
在Object中有个方法是equals,所有所有的类都有咯。
==用于值类型,当用于值类型的时候,比较的是值,对吧。
==用于引用类型的时候,比较的是是否是同一个引用。
其实就是比较底层啦。看过我之前那篇值传递和引用传递的同学应该知道他们的底层都是些啥吧。
其实它就是比较底层的01是否相同,相同返回true,不相同返回false。
而equals呢,在object中定义的就是使用 == ,但是可以重写这个方法,定义你自己的规则。