当前位置:   article > 正文

Java基础——String类_java string

java string

每日正能量

        积极向上的人总是把苦难化为积极向上的动力。

字符串

【本章内容】1. 字符串的定义 2. 字符串操作 3. 字符串与基本类型的转换
【能力目标】1. 掌握字符串对象与字面量的区别2. 掌握字符串常用方法3. 掌握字符串与基本类型的转换

1. String类的定义

1、String表示字符串类型,属于引用数据类型,不属于基本数据类型。

2、在java中随便使用 双引号括起来 的都是String对象。

例如:“abc”,“def”,“hello world!”,这是3个String对象。

3、java中规定,双引号括起来的字符串,是 不可变 的,也就是说"abc"自出生到最终死亡,不可变,不能变成"abcd",也不能变成"ab"

4、在JDK当中双引号括起来的字符串,例如:“abc” "def"都是直接存储在“方法区”的“字符串常量池”当中的。

5、为什么SUN公司把字符串存储在一个“字符串常量池”当中呢?

因为字符串在实际的开发中使用太频繁。为了执行效率,所以把字符串放到了方法区的字符串常量池当中。

2. String类源码解读

  1. public final class String
  2.    implements java.io.Serializable, Comparable<String>, CharSequence {
  3. /**用来存储字符串 */
  4. private final char value[];
  5. /** 缓存字符串的哈希码 */
  6. private int hash; // Default to 0
  7. /** 实现序列化的标识 */
  8. private static final long serialVersionUID = -6849794470754667710L;
  9. }

        这是一个用 final 声明的常量类,不能被任何类所继承,而且一旦一个String对象被创建, 包含在这个对象中的字符序列是不可改变的, 包括该类后续的所有方法都是不能修改该对象的,直至该对象被销毁,这是我们需要特别注意的(该类的一些方法看似改变了字符串,其实内部都是创建一个新的字符串,下面讲解方法时会介绍)

  通过上述代码可以发现,一个 String 字符串实际上是一个 char 数组。

3.声明方式

  1. String str1 = "abc";//注意这种字面量声明的区别
  2. String str2 = new String("abc");

那么这两种声明方式有什么区别呢?在讲解之前,我们先介绍 JDK1.7(不包括1.7)以前的 JVM 的内存分布:

 

①、程序计数器:也称为 PC 寄存器,保存的是程序当前执行的指令的地址(也可以说保存下一条指令的所在存储单元的地址),当CPU需要执行指令时,需要从程序计数器中得到当前需要执行的指令所在存储单元的地址,然后根据得到的地址获取到指令,在得到指令之后,程序计数器便自动加1或者根据转移指针得到下一条指令的地址,如此循环,直至执行完所有的指令。线程私有

  ②、虚拟机栈:基本数据类型、对象的引用都存放在这。线程私有

  ③、本地方法栈:虚拟机栈是为执行Java方法服务的,而本地方法栈则是为执行本地方法(Native Method)服务的。在JVM规范中,并没有对本地方法栈的具体实现方法以及数据结构作强制规定,虚拟机可以自由实现它。在HotSopt虚拟机中直接就把本地方法栈和虚拟机栈合二为一。

  ④、方法区:存储了每个类的信息(包括类的名称、方法信息、字段信息)、静态变量、常量以及编译器编译后的代码等。注意:在Class文件中除了类的字段、方法、接口等描述信息外,还有一项信息是常量池,用来存储编译期间生成的字面量和符号引用。

  ⑤、堆:用来存储对象本身的以及数组(当然,数组引用是存放在Java栈中的)。

  在 JDK1.7 以后,方法区的常量池被移除放到堆中了,如下:

 

常量池:Java运行时会维护一个String Pool(String池), 也叫“字符串缓冲区”。String池用来存放运行时中产生的各种字符串,并且池中的字符串的内容不重复。

  ①、字面量创建字符串或者纯字符串(常量)拼接字符串会先在字符串池中找,看是否有相等的对象,没有的话就在字符串池创建该对象;有的话则直接用池中的引用,避免重复创建对象。

  ②、new关键字创建时,直接在堆中创建一个新对象,变量所引用的都是这个新对象的地址,但是如果通过new关键字创建的字符串内容在常量池中存在了,那么会由堆在指向常量池的对应字符;但是反过来,如果通过new关键字创建的字符串对象在常量池中没有,那么通过new关键词创建的字符串对象是不会额外在常量池中维护的。

  ③、使用包含变量表达式来创建String对象,则不仅会检查维护字符串池,还会在堆区创建这个对象,最后是指向堆内存的对象。

内存分析

String str = “Hello”

  1. public class stringclass {
  2.    public static void main(String[] args) {
  3.       String str="Hello";
  4.        String str2="Hello";
  5.        System.out.println(str==str2);
  6.        str="World";
  7.   }
  8. }
//输出结果:
true

 

 

String str = new String (“Hello”)

  1. public class stringclass {
  2.    public static void main(String[] args) {
  3. String str= new String("Hello");
  4.        String str2= new String("Hello");
  5.        String str3 = "Hello";
  6.        System.out.println(str==str2);
  7.        System.out.println(str==str3);
  8.   }
  9. }  
  10. //输出结果:
  11. false
  12. false

String str = “Hello” + “World”

public class stringclass {
    public static void main(String[] args) {
        //当一个字符串由多个字符串常量连接而成时,它自己肯定也是字符串常量。
        //该字符串是在编译期就能确定。先是在池里生成“a”和“b”,再通过拼接的方式在池里生成"ab"。
        String str="Hello" + "World";
    }
}

 

String str = new String (“Hello”) + new String(“World”) 当使用了变量字符串的拼接(+, sb.append)都只会在堆区创建该字符串对象, 并不会在常量池创建新生成的字符串。

  1. public class stringclass {
  2.    public static void main(String[] args) {
  3.        String str=new String("Hello") + new String("World");
  4.   }
  5. }

 

4. 常见操作

4.1 equals(Object anObject) 方法

  1.  public boolean equals(Object anObject) {
  2.         if (this == anObject) {
  3.              return true;
  4.         }
  5.          if (anObject instanceof String) {
  6.              String anotherString = (String)anObject;
  7.              int n = value.length;
  8.              if (n == anotherString.value.length) {
  9.                  char v1[] = value;
  10.                 char v2[] = anotherString.value;
  11.                 int i = 0;
  12.                 while (n-- != 0) {
  13.                     if (v1[i] != v2[i])
  14.                        return false;
  15.                     i++;
  16.                 }
  17.                 return true;
  18.             }
  19.         }
  20.         return false;
  21.     }

  String 类重写了 equals 方法,比较的是组成字符串的每一个字符是否相同,如果都相同则返回true,否则返回false。

4.2 hashCode() 方法

  1.  public int hashCode() {
  2.          int h = hash;
  3.          if (h == 0 && value.length > 0) {
  4.              char val[] = value;
  5.  
  6.              for (int i = 0; i < value.length; i++) {
  7.                  h = 31 * h + val[i];
  8.             }
  9.              hash = h;
  10.         }
  11.         return h;
  12.     }

  String 类的 hashCode 算法很简单,主要就是中间的 for 循环,计算公式如下:

s[0]*31^(n-1) + s[1]*31^(n-2) + ... + s[n-1]

  s 数组即源码中的 val 数组,也就是构成字符串的字符数组。这里有个数字 31 ,为什么选择31作为乘积因子,而且没有用一个常量来声明?主要原因有两个:

  ①、31是一个不大不小的质数,是作为 hashCode 乘子的优选质数之一。

  ②、31可以被 JVM 优化,

31 * i = (i << 5) - i。因为移位运算比乘法运行更快更省性能。

4.3 charAt(int index) 方法

  1. public char charAt(int index) {
  2.         //如果传入的索引大于字符串的长度或者小于0,直接抛出索引越界异常
  3.         if ((index < 0) || (index >= value.length)) {
  4.             throw new StringIndexOutOfBoundsException(index);
  5.         }
  6.         return value[index];//返回指定索引的单个字符
  7.     }

  我们知道一个字符串是由一个字符数组组成,这个方法是通过传入的索引(数组下标),返回指定索引的单个字符。

4.4 compareTo(String anotherString) 和 compareToIgnoreCase(String str) 方法

  我们先看看 compareTo 方法:

  1. public int compareTo(String anotherString) {
  2.          int len1 = value.length;
  3.          int len2 = anotherString.value.length;
  4.          int lim = Math.min(len1, len2);
  5.          char v1[] = value;
  6.          char v2[] = anotherString.value;
  7.  
  8.          int k = 0;
  9.          while (k < lim) {
  10.             char c1 = v1[k];
  11.             char c2 = v2[k];
  12.             if (c1 != c2) {
  13.                 return c1 - c2;
  14.             }
  15.             k++;
  16.         }
  17.         return len1 - len2;
  18.     }

  源码也很好理解,该方法是按字母顺序比较两个字符串,是基于字符串中每个字符的 Unicode 值。当两个字符串某个位置的字符不同时,返回的是这一位置的字符 Unicode 值之差,当两个字符串都相同时,返回两个字符串长度之差。

 compareToIgnoreCase() 方法在 compareTo 方法的基础上忽略大小写,我们知道大写字母是比小写字母的Unicode值小32的,底层实现是先都转换成大写比较,然后都转换成小写进行比较。

4.5 concat(String str) 方法

  该方法是将指定的字符串连接到此字符串的末尾。

  1. public String concat(String str) {
  2. int otherLen = str.length();
  3. if (otherLen == 0) {
  4. return this;
  5. }
  6. int len = value.length;
  7. char buf[] = Arrays.copyOf(value, len + otherLen);
  8. str.getChars(buf, len);
  9. return new String(buf, true);
  10. }

首先判断要拼接的字符串长度是否为0,如果为0,则直接返回原字符串。如果不为0,则通过 Arrays 工具类的copyOf方法创建一个新的字符数组,长度为原字符串和要拼接的字符串之和,前面填充原字符串,后面为空。接着在通过 getChars 方法将要拼接的字符串放入新字符串后面为空的位置。

  注意:返回值是 new String(buf, true),也就是重新通过 new 关键字创建了一个新的字符串,原字符串是不变的。这也是前面我们说的一旦一个String对象被创建, 包含在这个对象中的字符序列是不可改变的。

4.6 indexOf(int ch) 和 indexOf(int ch, int fromIndex) 方法

  indexOf(int ch),参数 ch 其实是字符的 Unicode 值,这里也可以放单个字符(默认转成int),作用是返回指定字符第一次出现的此字符串中的索引。其内部是调用 indexOf(int ch, int fromIndex),只不过这里的 fromIndex =0 ,因为是从 0 开始搜索;而 indexOf(int ch, int fromIndex) 作用也是返回首次出现的此字符串内的索引,但是从指定索引处开始搜索。

  1. public int indexOf(int ch) {
  2. return indexOf(ch, 0);//从第一个字符开始搜索
  3. }
  4. public int indexOf(int ch, int fromIndex) {
  5. final int max = value.length;//max等于字符的长度
  6. if (fromIndex < 0) {//指定索引的位置如果小于0,默认从 0 开始搜索
  7. fromIndex = 0;
  8. } else if (fromIndex >= max) {
  9. //如果指定索引值大于等于字符的长度(因为是数组,下标最多只能是max-1),直接返回-1
  10. return -1;
  11. }
  12. if (ch < Character.MIN_SUPPLEMENTARY_CODE_POINT) {//一个char占用两个字节,如果ch小于2的16次方(65536),绝大多数字符都在此范围内
  13. final char[] value = this.value;
  14. for (int i = fromIndex; i < max; i++) {//for循环依次判断字符串每个字符是否和指定字符相等
  15. if (value[i] == ch) {
  16. return i;//存在相等的字符,返回第一次出现该字符的索引位置,并终止循环
  17. }
  18. }
  19. return -1;//不存在相等的字符,则返回 -1
  20. }else {//当字符大于 65536时,处理的少数情况,该方法会首先判断是否是有效字符,然后依次进行比较
  21. return indexOfSupplementary(ch, fromIndex);
  22. }
  23. }

4.7 substring(int beginIndex) 和 substring(int beginIndex, int endIndex) 方法

  ①、substring(int beginIndex):返回一个从索引 beginIndex 开始一直到结尾的子字符串。

  1. public String substring(int beginIndex) {
  2. if (beginIndex < 0) {//如果索引小于0,直接抛出异常
  3. throw new StringIndexOutOfBoundsException(beginIndex);
  4. }
  5. int subLen = value.length - beginIndex;//subLen等于字符串长度减去索引
  6. if (subLen < 0) {//如果subLen小于0,也是直接抛出异常
  7. throw new StringIndexOutOfBoundsException(subLen);
  8. }
  9. //1、如果索引值beginIdex == 0,直接返回原字符串
  10. //2、如果不等于0,则返回从beginIndex开始,一直到结尾
  11. return (beginIndex == 0) ? this : new String(value, beginIndex, subLen);
  12. }

  ②、 substring(int beginIndex, int endIndex):返回一个从索引 beginIndex 开始,到 endIndex 结尾的子字符串。

5. 字符串常用的API

 

        在编程开发中,经常需要对字符串进行各种操作,熟练掌握字符串的各种操作,对提高编程技巧很有帮助。要学习字符串的操作,首先要了解字符串的组成。字符串内部使用char数组来保存字符串的内容,数组中的每一位存放一个字符,char数组的长度也就是字符串的长度,下图以字符串“Hello World”为例说明其在内存中的分配:

下表中列出了字符串中提供的常用操作方法:

返回类型方法名称作用
booleanequals(String)比较两个字符串是否相等
booleanequalsIgnoreCase(String)忽略大小写比较两个字符串是否相等
intlength()获取字符串的长度
charcharAt(int)获取字符串中的一个字符
intindexOf(String)判断传入字符串在原字符串中第一次出现的位置
intlastIndexOf(String)判断传入字符串在原字符串中最后一次出现的位置
booleanstartsWith(String)判断原字符串是否以传入字符串开头
booleanendsWith(String)判断原字符串是否以传入字符串结尾
intcompareTo(String)判断两个字符串的大小
StringtoLowerCase()获取小写字符串
StringtoUpperCase()获取大学字符串
Stringsubstring(int)截取字符串,从传入参数位置开始截取到末尾
Stringsubstring(int,int)截取字符串,从参数1位置开始截取到参数2位置
Stringtrim()去掉字符串首尾的空格
String[]split(String)将原字符串按照传入参数分割为字符串数组
Stringreplace(String,String)将字符串中指定的内容替换成另外的内容

表 字符串常用操作方法

下面通过典型的例子演示字符串各种方法的应用。

5.1 验证用户名的长度

从键盘上输入用户名,对用户名进行验证,合法的用户名长度在6到20之间。如果在这区间,输出用户名长度合法,否则输出用户名长度不合法。

任务1:用户名长度验证

  1. public static void main(String[] args) {
  2. Scanner input =new Scanner(System.in);
  3. System.out.println("请输入用户名");
  4. String name = input.next();
  5. if(name.length()>=6 && name.length()<=20){
  6. System.out.println("用户名长度合法");
  7. }else{
  8. System.out.println("用户名长度不合法");
  9. }
  10. }

说明:字符串的length()方法用于获取字符串的长度。

5.2 验证Email地址是否合法

从键盘上输入email,对email进行验证,合法的email的条件是:

  1. 必须包含“@”和“.”

  2. “@”必须在“.”的前面

  3. “@”只能出现一次

任务2:验证Email地址是否合法

  1. public static void main(String[] args) {
  2. Scanner input =new Scanner(System.in);
  3. System.out.println("请输入Email");
  4. String email = input.next();
  5. int atIndex = email.indexOf("@");
  6. int dotIndex = email.indexOf(".");
  7. //必须包含“@”和“.”
  8. if(atIndex==-1 || dotIndex ==-1){
  9. System.out.println("Email非法,不存在@或.");
  10. return;
  11. }
  12. //“@”必须在“.”的前面
  13. if(atIndex>dotIndex){
  14. System.out.println("Email非法,不允许@在.的后面");
  15. return;
  16. }
  17. //“@”不在开头和结尾,并且只能出现一次
  18. if(email.startsWith("@")==false && email. endsWith("@")){
  19. String array[] = email.split("@");
  20. if(array.length!=2){
  21. System.out.println("Email非法,要求@有且只有一个");
  22. return;
  23. }
  24. }
  25. System.out.println("Email合法");
  26. }

说明:

  1. int atIndex = email.indexOf("@"),indexOf方法表示在email中查找是否包含“@”,返回“@”在email中的下标,若不存在返回-1。

  2. email.split("@"),split方法表示将email中的字符串以“@”为标志,将email切割成多个字符,并将切割后的字符串存储到array数组中。

5.3 验证类名是否以.java结尾,以com开头

任务3:验证类名

  1. public static void main(String[] args) {
  2. Scanner input =new Scanner(System.in);
  3. System.out.println("请输入类名");
  4. String classname = input.next();
  5. classname = classname.toLowerCase();
  6. if(classname.startsWith("com")==false){
  7. System.out.println("类名不是以com开头的");
  8. return;
  9. }
  10. if(!classname.endsWith(".java")){
  11. System.out.println("类名不是以.结尾的");
  12. return;
  13. }
  14. System.out.println("类名是以com开头,.java结尾的");
  15. }

说明:

  1. classname=classname.toLowerCase(),toLowerCase方法用于将字符串转换成小写。

  2. classname.startsWith("com"),startWith方法用于判断字符串是否以“com”开头。

  3. classname.endsWith(".java"),endsWith方法用于判断字符串是否以“.java”结尾。

5.4 将新闻标题中的“爪洼”换成“java”

任务4:字符串内容替换

  1. public static void main(String[] args) {
  2. String title ="爪哇技术发展这些年";
  3. System.out.println("替换前的标题"+title);
  4. title = title.replace("爪哇","java");
  5. System.out.println("替换后的标题"+title);
  6. }

说明

  1. title = title.replace("爪哇","java"),replace方法用于将title字符串中所有的“爪哇”替换成“java”。

5.5 将路径中的文件名截取出来

任务5:字符串截取

  1. public static void main(String[] args) {
  2. String path ="C:\\HTML\\front\\assets\\img\\pc\\logo.png";
  3. int startIndex = path.lastIndexOf("\\");
  4. int endIndex = path.lastIndexOf(".");
  5. String fileName = path.substring(startIndex+1,endIndex);
  6. System.out.println(path+"路径中的文件是:"+fileName);
  7. }

说明:

  1. path变量中的[\中第一个](file:///\中第一个)表示转义字符。

  2. substring方法用于从一个字符串中截取部分字符串,第一个参数是从第几位截取,第二个参数是截取到第几位。

问:字符串的方法好多,我如何去学习呢?
答:Java提供了JDK的API文档,你必须学会查询API文档学习知识,不仅仅是字符串类。

5.6 字符串格式化

String类的format()方法用于创建格式化的字符串以及连接多个字符串对象。format方法定义是format(String format, Object... args);第一个参数是被格式化的字符串,第二个参数是替换格式符的字符串,第二个参数中的…表示方法可变参数(后续课程中讲解),即参数的个数根据格式符个个数来确定。字符串格式化就是使用第二个可变参数中的值按照顺序替换第一个参数中的格式符。format方法的格式符定义如下:

格式符说明示例
%s字符串类型"李逵"
%c字符类型'm'
%b布尔类型true
%d整数类型(十进制)100
%x整数类型(十六进制)FF
%o整数类型(八进制)77
%f浮点类型99.99

任务6:字符串格式化

  1. public static void main(String[] args) {
  2. String str=null;
  3. str=String.format("见过,%s及%s", "晁天王","众位头领");
  4. System.out.println(str);
  5. str=String.format("字母a的大写是:%c", 'A');
  6. System.out.println(str);
  7. str=String.format("3>7的结果是:%b", 3>7);
  8. System.out.println(str);
  9. str=String.format("100的一半是:%d", 100/2);
  10. System.out.println(str);
  11. //使用printf代替format方法来格式化字符串
  12. System.out.printf("50元的书打8.5折扣是:%f 元", 50*0.85);
  13. }

运行结果

见过,晁天王及众位头领

字母a的大写是:A

3>7的结果是:false

100的一半是:50

50元的书打8.5折扣是:42.500000 元

说明:

  1. 字符串的格式化避免了使用+来连接字符串,使得字符串的构建更方便。

  2. 是否System.out.printf()可以代替String.format()方法格式化字符串

6. StringBuilder、StringBuffer

6.1 “+”连接符

“+”连接符的实现原理

Java语言为“+”连接符以及对象转换为字符串提供了特殊的支持,字符串对象可以使用“+”连接其他对象。其中字符串连接是通过 StringBuilder(或 StringBuffer)类及其append 方法实现的,对象转换为字符串是通过 toString 方法实现的,该方法由 Object 类定义,并可被 Java 中的所有类继承。有关字符连接和转换的更多信息,可以参阅 Gosling、Joy 和 Steele 合著的 《The Java Language Specification》

我们可以通过反编译验证一下

  1. /**
  2. * 测试代码
  3. */
  4. public class Test {
  5. public static void main(String[] args) {
  6. int i = 10;
  7. String s = "abc";
  8. System.out.println(s + i);
  9. }
  10. }
  11. /**
  12. * 反编译后
  13. */
  14. public class Test {
  15. public static void main(String args[]) { //删除了默认构造函数和字节码
  16. byte byte0 = 10;
  17. String s = "abc";
  18. System.out.println((new StringBuilder()).append(s).append(byte0).toString());
  19. }
  20. }

由上可以看出,Java中使用"+"连接字符串对象时,会创建一个StringBuilder()对象,并调用append()方法将数据拼接,最后调用toString()方法返回拼接好的字符串。由于append()方法的各种重载形式会调用String.valueOf方法,所以我们可以认为:

  1. //以下两者是等价的
  2. s = i + ""
  3. s = String.valueOf(i);
  4. //以下两者也是等价的
  5. s = "abc" + i;
  6. s = new StringBuilder("abc").append(i).toString();

6.2 “+”连接符的效率

使用“+”连接符时,JVM会隐式创建StringBuilder对象,这种方式在大部分情况下并不会造成效率的损失,不过在进行大量循环拼接字符串时则需要注意。

  1. String s = "abc";
  2. for (int i=0; i<10000; i++) {
  3. s += "abc";
  4. }
  5. /**
  6. * 反编译后
  7. */
  8. String s = "abc";
  9. for(int i = 0; i < 1000; i++) {
  10. s = (new StringBuilder()).append(s).append("abc").toString();
  11. }

这样由于大量StringBuilder创建在堆内存中,肯定会造成效率的损失,所以在这种情况下建议在循环体外创建一个StringBuilder对象调用append()方法手动拼接(如上面例子如果使用手动拼接运行时间将缩小到1/200左右)。

  1. /**
  2. * 循环中使用StringBuilder代替“+”连接符
  3. */
  4. StringBuilder sb = new StringBuilder("abc");
  5. for (int i = 0; i < 1000; i++) {
  6. sb.append("abc");
  7. }
  8. sb.toString();

与此之外还有一种特殊情况,也就是当"+"两端均为编译期确定的字符串常量时,编译器会进行相应的优化,直接将两个字符串常量拼接好,例如:

  1. System.out.println("Hello" + "World");
  2. /**
  3. * 反编译后
  4. */
  5. System.out.println("HelloWorld");
  6. /**
  7. * 编译期确定
  8. * 对于final修饰的变量,它在编译时被解析为常量值的一个本地拷贝存储到自己的常量池中或嵌入到它的字节码流中。
  9. * 所以此时的"a" + s1和"a" + "b"效果是一样的。故结果为true。
  10. */
  11. String s0 = "ab";
  12. final String s1 = "b";
  13. String s2 = "a" + s1;
  14. System.out.println((s0 == s2)); //result = true
  15. /**
  16. * 编译期无法确定
  17. * 这里面虽然将s1用final修饰了,但是由于其赋值是通过方法调用返回的,那么它的值只能在运行期间确定
  18. * 因此s0和s2指向的不是同一个对象,故上面程序的结果为false。
  19. */
  20. String s0 = "ab";
  21. final String s1 = getS1();
  22. String s2 = "a" + s1;
  23. System.out.println((s0 == s2)); //result = false
  24. public String getS1() {
  25. return "b";
  26. }

综上,“+”连接符对于直接相加的字符串常量效率很高,因为在编译期间便确定了它的值,也就是说形如"I"+“love”+“java”; 的字符串相加,在编译期间便被优化成了"Ilovejava"。对于间接相加(即包含字符串引用,且编译期无法确定值的),形如s1+s2+s3; 效率要比直接相加低,因为在编译器不会对引用变量进行优化。

7. StringBuffer源码解析

String 是我们用到非常多的一个类,对于 String 做大量的操作,如果只使用 String 的话,效率没有那么高。一般会推荐使用 StringBuffer 和 Stringbuilder 来做字符串的操作。 那么 StringBuffer 和 StringBuilder 的区别是什么呢? StringBuffer 是线程安全的,因为它里面的方法都被 synchronized 关键字修饰,例如 append 方法:

  1. @Override
  2. public synchronized StringBuffer append(String str) {
  3. toStringCache = null;
  4. super.append(str);
  5. return this;
  6. }
  7. StringBuilder 是线程不安全的,他里面没有使用 synchronized 关键字修饰
  8. @Override
  9. public StringBuilder append(String str) {
  10. super.append(str);
  11. return this;
  12. }

其中两个类都是在调用父类的 append 方法,只是 StringBuffer 通过 synchronized 关键字来保证线程安全,当然这样做同时也会降低效率。除非是在单线程环境下,并且非常追求速率的情况下使用 StringBuilder,其他情况下还是推荐使用 StringBuffer 来做字符串的操作。我们的重点不是对比两个类的优缺点,直接去看这两个类的父类 AbstractStringBuilder 。父类中的 append 方法:

  1. public AbstractStringBuilder append(String str) {
  2. if (str == null)
  3. return appendNull();
  4. int len = str.length();
  5. ensureCapacityInternal(count + len); // 扩容
  6. str.getChars(0, len, value, count); // 添加新的 String 到尾部
  7. count += len; // 长度加上添加进来的长度
  8. return this;
  9. }
  10. 实际上不管是哪一个,都是在使用父类的方法以及成员变量。
  11. /** 存储字符的数组 */
  12. char[] value;
  13. /** 字符数组中已经使用得到长度 */
  14. int count;
  15. 父类 AbstractStringBuilder 中维护了一个存储字符的数组 value,用来保存字符串的全部字符。count 是用来表示使用了多长的数组。 接下来看一下扩容方法:
  16. private void ensureCapacityInternal(int minimumCapacity) {
  17. // overflow-conscious code
  18. if (minimumCapacity - value.length > 0)
  19. expandCapacity(minimumCapacity);
  20. }
  21. void expandCapacity(int minimumCapacity) {
  22. int newCapacity = value.length * 2 + 2; // 扩容为原来的 2 倍加 2
  23. if (newCapacity - minimumCapacity < 0) // 如果扩容后的长度仍然小于最小扩容长度,则新长度赋值为最小扩容长度
  24. newCapacity = minimumCapacity;
  25. if (newCapacity < 0) { // 新长度为负数,超过 int 的最大值,变为了负数
  26. if (minimumCapacity < 0) // overflow // 最小的扩容长度为负数,也是超过了 int 最大值
  27. throw new OutOfMemoryError();
  28. newCapacity = Integer.MAX_VALUE; // 如果最小扩容长度没有超过 int 最大值,但是原长度翻倍加2后超过了,则把新长度赋值为 int 最大值
  29. }
  30. value = Arrays.copyOf(value, newCapacity); // 调用 Arrays.copyOf 生成新长度的数组
  31. }
  32. 扩容方法就是扩容为原来的 2 倍再加 2 ,然后判断新长度的合法性,不合法会抛出 OOM ,合法会复制一个新长度的数组覆盖原来的数组。
  33. 真正添加 String 的方法是 getChars
  34. public void getChars(int srcBegin, int srcEnd, char dst[], int dstBegin) {
  35. if (srcBegin < 0) {
  36. throw new StringIndexOutOfBoundsException(srcBegin);
  37. }
  38. if (srcEnd > value.length) {
  39. throw new StringIndexOutOfBoundsException(srcEnd);
  40. }
  41. if (srcBegin > srcEnd) {
  42. throw new StringIndexOutOfBoundsException(srcEnd - srcBegin);
  43. }
  44. // 执行到这里,说明没有出现下标越界,调用 arraycopy 把该字符串追加到尾部
  45. System.arraycopy(value, srcBegin, dst, dstBegin, srcEnd - srcBegin);
  46. }

此时,StringBuffer 的 append 方法才算结束执行。 看完在尾部添加 String 的 append 方法,看一下可以在指定下标处添加的 insert 方法,直接贴出调用的父类的 insert 方法

  1. public AbstractStringBuilder insert(int offset, String str) {
  2. if ((offset < 0) || (offset > length()))
  3. throw new StringIndexOutOfBoundsException(offset);
  4. if (str == null)
  5. str = "null";
  6. int len = str.length();
  7. ensureCapacityInternal(count + len); // 扩容
  8. // 从 offset 处开始之后的字符都向后移动 len 长度的位置
  9. System.arraycopy(value, offset, value, offset + len, count - offset);
  10. // 把新添加的 String 添加到 offset 位置处
  11. str.getChars(value, offset);
  12. count += len;
  13. return this;
  14. }

检查下标的合法性后就做扩容操作,然后在将指定位置处以后的元素向后移动新加 String 长度的距离,然后将新 String 添加至指定位置。 删除指定区间的 delete 方法如下,依然用 synchronized 控制,并调用父类方法

  1. public AbstractStringBuilder delete(int start, int end) {
  2. if (start < 0)
  3. throw new StringIndexOutOfBoundsException(start);
  4. if (end > count)
  5. end = count;
  6. if (start > end)
  7. throw new StringIndexOutOfBoundsException();
  8. int len = end - start;
  9. if (len > 0) {
  10. // 把 start + len 位置开始的元素向前移动 len 个距离
  11. System.arraycopy(value, start+len, value, start, count-end);
  12. count -= len;
  13. }
  14. return this;
  15. }

也是使用了 arraycopy 来操作内部维护的字符数组。

最后看一下字符串反转方法 reverse

  1. public AbstractStringBuilder reverse() {
  2. boolean hasSurrogates = false;
  3. int n = count - 1; // 得到下标最大值
  4. /* 循环,从中间向两边移动,并交换位置
  5. count = 长度 n = 下表最大值 j = 下标减一后除
  6. 如果是偶数个->user count = 4, n = 3, j = 1 交换下标为 1 和 3-1 的两个值,即 s 和 e,然后向两头移动
  7. 如果奇数个->hello count = 5, n = 4, j = 1 交换下标为 1 和 4-1 的两个值,即 e 和第二个 l,然后向两头移动
  8. */
  9. for (int j = (n-1) >> 1; j >= 0; j--) {
  10. int k = n - j;
  11. char cj = value[j];
  12. char ck = value[k];
  13. value[j] = ck;
  14. value[k] = cj;
  15. if (Character.isSurrogate(cj) ||
  16. Character.isSurrogate(ck)) {
  17. hasSurrogates = true;
  18. }
  19. }
  20. if (hasSurrogates) {
  21. reverseAllValidSurrogatePairs();
  22. }
  23. return this;
  24. }

8. String.intern() 方法

  这是一个本地方法:

public native String intern();

  当调用intern方法时,如果池中已经包含一个与该String确定的字符串相同equals(Object)的字符串,则返回该字符串。否则,将此String对象添加到池中,并返回此对象的引用。

  这句话什么意思呢?就是说调用一个String对象的intern()方法,如果常量池中有该对象了,直接返回该字符串的引用(存在堆中就返回堆中,存在池中就返回池中),如果没有,则将该对象添加到池中,并返回池中的引用。

  1. String str1 = "hello";//字面量 只会在常量池中创建对象
  2. String str2 = str1.intern();
  3. System.out.println(str1==str2);//true
  4. String str3 = new String("world");//new 关键字只会在堆中创建对象
  5. String str4 = str3.intern();
  6. System.out.println(str3 == str4);//false
  7. String str5 = str1 + str2;//变量拼接的字符串,会在常量池中和堆中都创建对象
  8. String str6 = str5.intern();//这里由于池中已经有对象了,直接返回的是对象本身,也就是堆中的对象
  9. System.out.println(str5 == str6);//true
  10. String str7 = "hello1" + "world1";//常量拼接的字符串,只会在常量池中创建对象
  11. String str8 = str7.intern();
  12. System.out.println(str7 == str8);//true

9. String不可变性

String 类为什么要这样设计成不可变呢?我们可以从性能以及安全方面来考虑:

  • 安全

    • 引发安全问题,譬如,数据库的用户名、密码都是以字符串的形式传入来获得数据库的连接,或者在socket编程中,主机名和端口都是以字符串的形式传入。因为字符串是不可变的,所以它的值是不可改变的,否则黑客们可以钻到空子,改变字符串指向的对象的值,造成安全漏洞。

    • 保证线程安全,在并发场景下,多个线程同时读写资源时,会引竞态条件,由于 String 是不可变的,不会引发线程的问题而保证了线程。

    • HashCode,当 String 被创建出来的时候,hashcode也会随之被缓存,hashcode的计算与value有关,若 String 可变,那么 hashcode 也会随之变化,针对于 Map、Set 等容器,他们的键值需要保证唯一性和一致性,因此,String 的不可变性使其比其他对象更适合当容器的键值。

  • 性能

    • 当字符串是不可变时,字符串常量池才有意义。字符串常量池的出现,可以减少创建相同字面量的字符串,让不同的引用指向池中同一个字符串,为运行时节约很多的堆内存。若字符串可变,字符串常量池失去意义,基于常量池的String.intern()方法也失效,每次创建新的 String 将在堆内开辟出新的空间,占据更多的内存。

本文内容由网友自发贡献,转载请注明出处:【wpsshop博客】
推荐阅读
相关标签
  

闽ICP备14008679号