前言
今天秋秋学习了关于java中String家族的三兄弟的一些api以及底层原理,想通过这篇文章从多角度解析一下关于java中这三者的异同之处,有不明白的小伙伴可以通过这篇文章加强理解,也欢迎各位小伙伴们多多指正.
提示:以下是本篇文章正文内容,下面案例可供参考
一、String
首先我们来谈谈最常用的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.常量池中没有,就在常量池中复制一份,再返回常量池中该字符串的引用
1. 类的声明
public final class String implements java.io.Serializable, Comparable<String>, CharSequence, Constable, ConstantDesc {} >final:String是不可继承的 >Serializable :可序列化的接口,凡是实现这个接口的对象可以通过网络或本地流进行数据的传输 >Comparable:实现这个接口的对象可以比较大小
2. 内部声明的属性
JDK8 private final char value[];//存储字符串数据的容器 >final:指明value数组一旦初始化,地址不可变,这也间接体现了String的不可变性 JDK9开始底层的结构就变了 private final byte[] value; 为了节省内存空间,做了优化,拉丁字母一个字节,其他的还是按照两个字节存
3.字符串常量的存储位置
3.1 字符串常量都存在字符串常量池中,不允许存在两个相同的字符串 3.2 字符串常量池在不同的jdk版本中存放位置不同 JDK7之前存在方法区 JDK7之后存在堆空间 改变位置的原因:方法区是类的加载区,所以gc很少去, (gc:垃圾清理机制) 为了节省空间,就给到堆空间了,方法区改名元空间,因为后来允许方法区使用物理内存
4.String的不可变性
4.1 当对字符串变量重写赋值时,需要重新指定一个字符串常量进行修改,不能在原有位置进行修改 4.2 当对现有的字符串进行拼接操作时,需要重新开辟空间,不能在原有位置修改 4.3 修改完了就在堆里面了,相当于new了一个对象出来了 4.4 当调用replace方法时也是新创建一个变量在堆空间里
5.String实例化的两种方式
String s1 = "hello" String s1 = new String("hello") String()构造器在内存中创建了几个对象? 两个,new的是同一个char数组,数组放的是hello 指向的是同一个字符串常量池里面的hello字符串 创建了两个对象,一个是在堆空间new的对象,另一个是在字符串常量池生成的字面量
二、常用api
1.String
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
2.StringBuffer和StringBuilder
提到这两个家伙,有人就要问了,为啥存储一个字符串要这么多种类来描绘呢,他们有什么不同嘛,了解完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了一个新的对象,而其他两个类则是可以在本身底层的数组中进行扩容,从而实现了原地修改,就称为可变.