今天来和大家聊聊字符串那些事,来彻底把字符串类型吃透摸透,让你在面试时,当问到字符串时,你可以和他喷个半小时,来个吊打面试官,哈哈,开玩笑啦,可不能吊打面试官,要是吊打了,那你还想不想拿到录取offer啦呀。好了,废话少说,来开始干吧,准备吊打面试官的工具。
1、String字符串
java八大基本类型:byte,short,int,long,float,double,char,boolean。在八大基本类型中,我们没有找到String类型,那说明它不属于基本类型,java中除了基本类型,剩下的全是引用类型,毫无以外,String也不例外,它属于引用类型。
八大基本类型即存放在栈中也存放在堆中,那么它是如何确定是存在堆中还是栈中呢,下面我举个例子,大家心里就一目了然。
void function(){
//局部变量
int a = 3;
}
这个例子中的基本类型是在方法中所定义,也就是局部变量,那自然它是存放在栈中。
class test{
//全局变量
int a = 3;
}
这个例子的基本类型是在类中所定义,它也就是全局变量,那肯定是随对象存放在堆里。
因此不要相信大多数博客中所说的基本类型全部存放在栈中,,不要一概而论,要视情况而定。
往更深层次来了解它,为什么全局变量存放在堆中,而局部变量存放在栈中呢?
如果你熟悉Java的内存结构的话,那么你对这种问题就不感觉到疑惑了,堆是所有线程共享的内存区域,栈是每个线程独享内存区域。如果将全部变量的的基本类型也放到栈中,那么多个线程就不能访问同一个对象资源,这显然是不对的,全局变量线程是不安全的。
好了,上面和大家探讨了那么多基本类型,也算是个抛砖引玉吧,接下来把玉String给引出来。
1.1、String存放地址
它属于引用类型,它的内容即可以存放在堆中,也可以存放在常量池中,同样视情况而定,但是它肯定会在栈中开辟一块区域来存放变量名,这点毋庸置疑。
第一种,直接创建String:
//String 直接创建
String str1 = "china";
String str2 = "china";
这个例子是直接创建,变量名会存放在栈中,内容会存放在常量池中。
步骤:
1、首先将str1变量名存放在栈中,然后将内容"china"存放在常量池中,然后将str1的地址指向常量池"china"。
2、将str2变量名存放在栈中,然后将"china"去与常量池中所有内容进行对比,查看到常量池中已经存在“china”。
3、将str2地址直接指向常量池中已经存在"china"。
第二种,对象创建String
//对象创建String
String str3 = new String("china");
String str4 = new String("china");
这个例子是对象创建,变量名存放在栈中,对象存放在堆中。
步骤:
1、str3创建时,先在栈中开辟一块内存,存放变量名,将对象存放堆中,然后将str3指向堆中对象地址。
2、str4创建时,先在栈中开辟一块内存,存放str4变量名,然后新创建对象存放在堆中,然后str4指向堆中新创建地址。
注意: String对象创建时,和直接创建是不一样的,每次对象创建时都会在堆中重新创建个新对象;但是直接创建时会会先在常量池中查看对比是否有这个内容,如果有的话,那么直接将地址指向它即可,否则的话,将在常量池中新建。
1.2、String源码分析
首先,我给大家贴出String部分源码来分析下,我的是jdk11,和1.8的有些不同,源码如下所示:
public final class String
implements java.io.Serializable, Comparable<String>, CharSequence {
@Stable
private final byte[] value;
}
a、从源码中可以看出String类是用final来进行修饰,说明String这个类不能被子类继承重写。
b、底层采用的是byte[]数组,并且变量是用final关键字来进行修饰,说明引用类型一旦初始化之后,便不会再指向另一个对象;基本类型被赋值之后,其数值就不会发生改变。byte[]类型是引用类型,说明用final修饰之后,其指向对象地址不会改变,但是数据可以发生改变,那么结合private一起使用,这样就保证了String的不可变性,因此String也是线程安全的。
c、这里我贴出源码是jdk11的,但是在jdk1.8中源码是不是这样的,1.8中底层采用的是char[]数组为底层。我们可以思考下为什么之前采用char数组,现在却采用byte数组。char是占用2个字节,byte占用1个字节 ,这样在每次初始化的时候为内存节省1个字节,节省其内存。
1.3、String的双等号(==)比较其地址
在双等号比较值时,比较的是其地址,如果为true则说明是同一个值或者同一个对象,反之则为false。我下列出一段代码,然后再详细分析。代码如下:
public class TestString {
public static void main(String[] args) {
//将内容存放在常量池中,如果常量池中已经存在,则不需要再进行创建
String str1 = "china"; //直接创建
String str2 = "china"; //直接创建
//创建一个对象存放在堆中
String str3 = new String("china"); //对象创建
//这里的拼接也是个知识点,具体分析大家可以看下面的str4和str5比较分析
String str4 = str1 + "add"; // 利用"+"来进行拼接
//这里的code4和下面的code5值是一样的,这就能说明他们指向的是同一个地址么?
//显然是不可以的,String中将hashcode()方法进行重写,分析源码可以看出:只要内容相等,那么hashCode值就相等,因此可以说明hashcode相等,不代表值相等
int code4 = str4.hashCode();//code4:1661267946
String str5 = "chinaadd"; //直接拼接好字符串
int code5 = str5.hashCode();//code5:1661267946
String str6 = new String("china"); //对象创建
//两个直接创建的String类型变量名直接双等号比较,返回值为true
//说明两个变量指向的是同一个常量
//这也说明了直接创建的内容存放在常量池中,变量名中存放在栈中
//栈中变量名直接指向常量池,每当直接创建一个String时,会拿它的内容去常量池中对比看是否已经存在
//如果已经存在,则直接将地址指向已经存在的内容;否则在常量池中重新定义一个内容,变量名地址指向它
//因此str1地址指向常量池的内容和str2指向的地址是一样的。因此双等号地址比较是相等
System.out.println(str1 == str2);//true
//str1地址指向的是常量池中"china"内容
//str3指向的是对堆中创建的地址
//因此,他们两个完全指向的不是一个地址,因此双等号地址比较为false
System.out.println(str1 == str3);//false
//str4是用的字符串拼接,str5用的是直接拼接好的字符串,我刚一开始认为这str4拼接的字符串直接存放在常量池中
//str5正好直接指向常量池中的内容,双等号比较应该是true啊,为什么是false呢?
//经过分析源码后发现,字符串在拼接时,利用了new StringBuilder(),然后再利用append()方法来拼接
//最后在转换String的时候,调用了toString()方法,在toString()方法中发现用的时new String()对象
//因此,拼接的时候就类似于new了个新对象放在堆中,这两个变量名指向的都不是同一个地址
//所以返回值为false,我分析到这里大家应该都比较一目了然了。
System.out.println("str4 == str5:" + (str4 == str5));//false
//这两个比较我就不过多详述了吧,大家都比较熟悉
//这两个创建对象存放在堆中,指向的都不是同一个对象地址,因此为false
System.out.println("str5 == str6:" + (str5 == str6));//false
这里为了方便大家去理解,我把分析内容不在这里和大家分析,直接在代码中的注释中给大家分析,这样方便大家理解,省的来回找代码了。
1.4、String中的equals()方法比较
大家对equals()比较都特别熟悉了,今天我这篇文章里也再老生常谈一次,给大家再分析一下equals()的源码,让大家再增深一次印象。我还是写下举例代码,在例子中给大家来详细分析其内容。例子如下所示:
public class TestString {
public static void main(String[] args) {
String str1 = "china";
String str2 = "china";
String str3 = new String("china");
String str4 = str1 + "add";
String str5 = "chinaadd";
System.out.println(str1.equals(str2));//true
System.out.println(ObjectUtils.nullSafeEquals(str1, str2));//true
System.out.println(str1.equals(str3));//true
System.out.println("str4.equals(str5):" + (str4.equals(str5)));//true
}
equals()是值比较,这里所说的值比较是只针对于String来说,如果是对象用equals来比较的话,那么就不是值比较了,还是用的地址比较,我把源码给大家贴在下面:
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;
}
在源码中可以看到,首先进行地址比较,如果地址相等,那么直接返回true,如果地址不相等,那么去判断是否是String类型,如果是String类型,那么去比较hashcode,刚刚在上面双等号代码例子中也提到了hashcode值,String重写了hashcode方法,只要值相等,那么hashcode就相等,因此可以通过hashcode来判断是否相等;如果不是String类型,那么就直接返回false。
从上面的分析中就可以看出当类型为String时,进行的是值比较;不是String类型时,比较的是其地址。
注意: 在equals比较时,例如str1.equals(str2)
时,str1为空,那么在运行时将会报空指针异常,这里我们可以采用先进行判断下是否为null,如果为null,那么不判断;你也可以采用另外一种方法,就是对象工具类中的equasl()方法——>ObjectUtils.nullSafeEquals(str1, str2)
这里的源码中就直接对空做了处理,不需要我们额外处理空指针。
对了,说到空指针,我再给大家提下字符串在赋值的时候。
//这是空字符串,长度为0,不会报空指针异常
String s1 = "";
//这是空,无值,和刚才的空字符串是不一样的,会报空指针异常
String s2 = null;
今天突然想起来StringUtils还有两个方法未和大家分享,自我感觉比较重要些,现在给大家分享下,分别是isNotEmpty()和isNotBlank()方法。
isNotEmpty() 方法举例如下
public static void main(String[] args) {
// isNotEmpty==判断某字符串是否非空
System.out.println(StringUtils.isNotEmpty(null)); // = false;
//空字符串
System.out.println(StringUtils.isNotEmpty("")); // false;
//有字符串,字符串为 空格
System.out.println(StringUtils.isNotEmpty(" "));// true;
System.out.println(StringUtils.isNotEmpty("bob")); // true;
}
isNotBlank() 举例如下所示:
public static void main(String[] args) {
// isNotBlank:判断某字符串是否不为空且长度不为0且不由空白符(whitespace)构成,
System.err.println(StringUtils.isNotBlank(null)); // false
System.err.println(StringUtils.isNotBlank("")); // false
System.err.println(StringUtils.isNotBlank(" ")); // false
System.err.println(StringUtils.isNotBlank("\t \n \f \r")); // false
}
isNotEmpty()和isNotBlank()方法总结:
isNotEmpty判断字符是否为null和空字符串,
isNotBlank判断字符是否为null和空字符串,且字符串不是空白字符串
StringUtils.isNotEmpty(str) 等同于:
str != null && str.length > 0
StringUtils.isNotBlank(str) 等价于:
str != null && str.length > 0 && str.trim().length > 0
即:判断是否==null时,还需要判断length是否>0
2、StringBuffer详细分析
谈到String不和大家聊聊StringBuffer我都无脸再去和家乡父老相见,哈哈,开玩笑开玩笑,来步入正题。
当我们频繁的修改String类型字符串时,那么他会创建很多对象或者常量池内容,那么这样势必会效率比较低下且最主要的是占用其内存。这个时候,StringBuffer就登场开始它的表演。
StringBuffer即能保证线程安全性,因为StringBuffer类上是的操作方法全是用synchronized关键字来修饰的;又能够节省内存开支,因为StringBuffer类型下的字符串支持在本身字符串上进行更改,这样省去了来回创建对象的开支,这一点是String类型的字符串所没有的;StringBuffer的效率相对来说是比较低的,因为在安全性和效率方面只能选其一,不可二者兼得;为了追求效率,那么你的安全性不得不舍弃;追求安全性,那么你的效率不得不丢掉。
2.1、追加方法append()
这个方法比较长用,它是在原对象基础上添加新字符,不用生成新对象。
老规矩,我还是写段代码,在代码中给大家进行一步一步的延申和分享,代码如下:
public class TestString {
public static void main(String[] args) {
String str1 = "china";
//如果是空构造函数,则默认长度是16;调用有参构造时,默认长度为 传值参数长度(lenth)+16
StringBuffer stringBuffer1 = new StringBuffer("china");
int stringBuffer1HashCode = stringBuffer1.hashCode();
//append添加字符是在原有对象上进行操作,不产生新对象,线程安全,效率较低
//当追加的字符大于其容量时,扩充按照(旧容量*2+2)长度 采用的是位运算,位运算效率比较高
//内部结构是new byte[];也就是数组,只能是byte类型数据。
StringBuffer stringBuffer2 = stringBuffer1.append("add");
int stringBuffer2HashCode = stringBuffer2.hashCode();
//这里equals()比较源码是其地址比较,不像String重写了equals方法
System.out.println(stringBuffer1.equals(str1)); //false
//这里是两个对象相互比较,地址都不相等,因此肯定为false
System.out.println(str1.equals(stringBuffer1)); //false
//这里比较的hashcode值,其目的是为了看操作字符串的时候是不是又生成了新对象
//结果为false,那说明是在原对象上操作,没有生成新对象
System.out.println("stringBuffer是否可以在原来字符上操作:" + (stringBuffer1HashCode == stringBuffer2HashCode)); //false
}
上面例子的分析,我全部写在了代码的注解中,这里就不作详细描述了,大家不懂的一定好好看看代码中的注解,这样能让你了解更透彻。
3、StringBuilder详细分析
哈哈,这个就特别好和大家分享了,和StringBuffer基本一样,只有一点不一样,那就是,它是线程不安全的,但是效率比较高,好了,这就是不一样的地方,特别容易吧。
又到了说再见的时候了,哪里有分享不对的地方,希望大家在下面评论区中指出哦
记得一键三联哦——>点赞、转发、评论