tips: 本文基于JDK8.0但不仅限于JDK8
目录
前言
一、String
1. 类的声明
2. 内部声明的属性
3.字符串常量的存储位置
4.String的不可变性
5.String实例化的两种方式
二、常用api
1.String
2.StringBuffer和StringBuilder
总结
今天秋秋学习了关于java中String家族的三兄弟的一些api以及底层原理,想通过这篇文章从多角度解析一下关于java中这三者的异同之处,有不明白的小伙伴可以通过这篇文章加强理解,也欢迎各位小伙伴们多多指正.
提示:以下是本篇文章正文内容,下面案例可供参考
首先我们来谈谈最常用的String类,我将分为以下几点阐述:类的声明,内部声明的属性,字符串常量的存储位置,String不可变性的理解以及实例化的两种方式来介绍
tips:以防有小伙伴不了解常量池,在开始之前我们先介绍一下常量池的概念.
String a = "abc";
String b = new String("abc");
System.out.println(a == b);
------------------------------------
结果:false
这里就是第一个要理解的地方,为什么a和b的地址不同,a的地址指向哪里,b的地址又指向哪里呢?
首先对象存在堆空间内,这是毋庸置疑的,而a作为字面量一开始就已经存储在了class文件中,之后运行时自然也就到了方法区,这两者的地址自然也就不同了.
下面我们通过几个字符串的拼接更好的理解一下常量池的概念
@Test
public void test5(){
String s1 = "hello";
String s2 = "world";
String s3 = "helloworld";
String s4 = "hello" + "world";
String s5 = s1 + "world";//通过查看字节码文件,调用了StringBuilder中的toString方法,创建了一个新对象
String s6 = "hello" + s2;
String s7 = s1 + s2 ;
System.out.println(s3 == s4);//true
System.out.println(s3 == s5);//false
System.out.println(s3 == s6);//false
System.out.println(s3 == s7);//false
System.out.println(s5 == s6);//false
System.out.println(s5 == s7);//false
String s8 = s5.intern();
System.out.println(s8 == s3);//true,返回的是字面量的地址
}
上述拼接操作我们分为以下几种情况
String的连接操作:
//情况1: 常量 + 常量 结果仍然存储在字符串常量池中
//情况2: 常量 + 变量 或者 变量+变量 会使用StringBuilder创建一个新String对象,返回堆空间地址
//情况3:调用字符串的intern()方法 返回的是字面量的地址
//特殊情况 :如果在变量前面加上final,就相当于常量加常量, == 和常量池中一样
//情况4:concat()拼接字符串,都需要去new,所以和常量池比都是false
//不管参数是常量还是变量,总之,调用完返回一个新new 的对象
//
很多小伙伴对intern()方法不理解,这里我们提前讲解,intern()方法分为两种情况,
声明为public native String intern();
1.常量池中有,就直接返回常量池中该字符串的地址(引用)
2.常量池中没有,就在常量池中复制一份,再返回常量池中该字符串的引用
public final class String
implements java.io.Serializable, Comparable, CharSequence,
Constable, ConstantDesc {}
>final:String是不可继承的
>Serializable :可序列化的接口,凡是实现这个接口的对象可以通过网络或本地流进行数据的传输
>Comparable:实现这个接口的对象可以比较大小
JDK8
private final char value[];//存储字符串数据的容器
>final:指明value数组一旦初始化,地址不可变,这也间接体现了String的不可变性
JDK9开始底层的结构就变了
private final byte[] value;
为了节省内存空间,做了优化,拉丁字母一个字节,其他的还是按照两个字节存
3.1 字符串常量都存在字符串常量池中,不允许存在两个相同的字符串
3.2 字符串常量池在不同的jdk版本中存放位置不同
JDK7之前存在方法区
JDK7之后存在堆空间
改变位置的原因:方法区是类的加载区,所以gc很少去, (gc:垃圾清理机制)
为了节省空间,就给到堆空间了,方法区改名元空间,因为后来允许方法区使用物理内存
4.1 当对字符串变量重写赋值时,需要重新指定一个字符串常量进行修改,不能在原有位置进行修改
4.2 当对现有的字符串进行拼接操作时,需要重新开辟空间,不能在原有位置修改
4.3 修改完了就在堆里面了,相当于new了一个对象出来了
4.4 当调用replace方法时也是新创建一个变量在堆空间里
String s1 = "hello"
String s1 = new String("hello")
String()构造器在内存中创建了几个对象?
两个,new的是同一个char数组,数组放的是hello
指向的是同一个字符串常量池里面的hello字符串
创建了两个对象,一个是在堆空间new的对象,另一个是在字符串常量池生成的字面量
boolean isEmpty():判断字符串是否为空
int length():判断字符串的长度
String concat():字符串的拼接
boolean equals(Object obj):比较字符串是否相等,区分大小写
boolean equalsIgnoreCase(Object obj):比较字符串是否相等,不区分大小写
int CompareTo(String other):比较字符串大小,区分大小写
int CompareToIgnoreCase(String other):比较字符串大小,不区分大小写
String toLowerCase():大写转小写
String toUpperCase():小写转大写
String trim():去掉字符串前后空白符
public String intern():结果在常量池共享
查找相关方法
boolean contains(xx):是否包含xx
int indexOf(xx):从前往后查找当前字符串中xx,如果有返回第一次的下标,没有就返
回-1
int indexOf(String str ,int fromIndex):返回指定子字符串在此字符串中第一次出现的索引,从指定的索引开始
int lastIndexOf(xx):从后往前找当前字符串最后一次出现的下标,没有返回-1
int lastIndexOf(String str ,int fromIndex):返回指定子字符串中最后一次出现的位置
取字符串
String substring(int beginIndex):返回一个新的字符串,它是此字符串空
beginIndex开始截取的
String substring(int beginIndex,int endIndex):返回一个新的字符串,左闭右开区间
char charAt(index):返回index位置的字符
char[] toCharArray():字符串转字符数组
static String valueOf(char[] data):返回指定数组中该字符序列的String
String s = String.valueOf(new char[]{'a','b','c'}); 新new的
开头和结尾
boolean startsWith(xx):
boolean startsWith(xx,int ):从哪个下标开始看
boolean endsWith(xx):以什么结束
String replace(char oldChar,char newChar)
String replace(charSequence target,charSequence replacement):替换此字符串,可以用很多个替换一个
相信大家对这里的大部分api都不是很陌生,大家稍作练习,会用就行,其中trim()方法只是取出字符串前后的空白符,字符串中间的是去出不了的.
String s = " hello p ";
s.trim();
打印出来是
---------------------
hello p
提到这两个家伙,有人就要问了,为啥存储一个字符串要这么多种类来描绘呢,他们有什么不同嘛,了解完String类我们再来谈谈关于StringBuffer和StringBuilder与String类的异同点,方便小伙伴们在开发中能更为高效的选择.
1.三个类的对比StringBuffer String StringBuilder
String:底层使用char型数组,其它两个也一样,JDK9之后使用byte型数组
>String:不可变的字符序列 new一个,字面量就重新在常量池拿
>StringBuffer:可变的字符序列; 在原位置修改 JDK1.0声明,线程安全的,相对效率低
>StringBuilder:可变的字符序列; JDK5.0声明的,线程不安全的,相对效率高
2.StringBuffer/StringBuilder可变性分析(源码分析)
String s1 = new String();//char[] value = new char[0];
String s2 = new String("abc");//char[] values = new char[]{'a','b','c'};
他俩父类都是AbstractStringBuilder ,char数组没有用final修饰
除了char[] value(存储字符序列),还有一个属性int count(实际存储字符的个数)
StringBuilder sBuffer = new StringBuilder("abc")//char[] balue = new char[16];
如果给了初始值,则长度就是16+字符串长度
如果我不断地添加(append),加多了就得新创建数组,数组变一下,对象还是没有变
一旦count要超过value.length,就要扩容,默认扩容为原有的2倍加2
并且将原有value数组的元素复制到新的数组中
3.源码启示
>如果开发中需要频繁针对字符串进行增删查改功能,建议使用StringBuffer或者
StringBuilde更好
>如果开发中不涉及线程安全问题,建议直接使用StringBuilder
>如果开发中大体确定要操作的字符的个数,建议使用带参数capacity的构造器,避免多次扩容
4.StringBuffer和StringBuilder的常用api
增:append(xx)
删:delete(int start, int end)
:deleteCharAt(int index)
改:replace(int start,int end,String str)
setCharAt(int index,char c)
查:charAt(int index)
插:insert(int index, xx)
长度,反转(反转的就是本身)
5.对比三者的查询效率
StringBuilder > StringBuffer > String
本文是对String三大类做了较为详细的讲解,StringBuffer和StringBuilder是在考虑线程安不安全的情况下使用的,StringBuffer是有synchronized修饰的,StringBuilder则没有,所以StringBuffer是线程安全的,StringBuilder则不然。但两者底层都是有自动扩容机制的,所以是可变的,String是不可变的,我们可以这样理解:String的修改不是在原地修改而是从常量池中拿了一个新的现成的字符串,或者是重新new了一个新的对象,而其他两个类则是可以在本身底层的数组中进行扩容,从而实现了原地修改,就称为可变.