Java中的String,这一篇就够了

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

今天我们一起看一下Java基础类:String

定义

  1. String表示字符串类型,属于引用数据类型,不属于基本数据类型。
  2. 在java中随便使用 双引号括起来 的都是String对象。例如:"abc","def","hello world!",这是3个String对象。
  3. java中规定,双引号括起来的字符串,是 不可变 的,也就是说"abc"自出生到最终死亡,不可变,不能变成"abcd",也不能变成"ab"

源码解读

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

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

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

声明方式

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

JDK1.6

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

图片图片

  1. 程序计数器:也称为 PC 寄存器,保存的是程序当前执行的指令的地址(也可以说保存下一条指令的所在存储单元的地址),当CPU需要执行指令时,需要从程序计数器中得到当前需要执行的指令所在存储单元的地址,然后根据得到的地址获取到指令,在得到指令之后,程序计数器便自动加1或者根据转移指针得到下一条指令的地址,如此循环,直至执行完所有的指令。线程私有。
  2. 虚拟机栈:基本数据类型、对象的引用都存放在这。线程私有。
  3. 本地方法栈:虚拟机栈是为执行Java方法服务的,而本地方法栈则是为执行本地方法(Native Method)服务的。在JVM规范中,并没有对本地方法栈的具体实现方法以及数据结构作强制规定,虚拟机可以自由实现它。在HotSopt虚拟机中直接就把本地方法栈和虚拟机栈合二为一。
  4. 方法区:存储了每个类的信息(包括类的名称、方法信息、字段信息)、静态变量、常量以及编译器编译后的代码等。注意:在Class文件中除了类的字段、方法、接口等描述信息外,还有一项信息是常量池,用来存储编译期间生成的字面量和符号引用。
  5. 堆:用来存储对象本身的以及数组(当然,数组引用是存放在Java栈中的)。

JDK1.7及以后

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

图片图片

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

  1. 字面量创建字符串或者纯字符串(常量)拼接字符串会先在字符串池中找,看是否有相等的对象,没有的话就在字符串池创建该对象;有的话则直接用池中的引用,避免重复创建对象。
  2. new关键字创建时,直接在堆中创建一个新对象,变量所引用的都是这个新对象的地址,但是如果通过new关键字创建的字符串内容在常量池中存在了,那么会由堆在指向常量池的对应字符;但是反过来,如果通过new关键字创建的字符串对象在常量池中没有,那么通过new关键词创建的字符串对象是不会额外在常量池中维护的。
  3. 使用包含变量表达式来创建String对象,则不仅会检查维护字符串池,还会在堆区创建这个对象,最后是指向堆内存的对象。

内存分析

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

图片图片

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

图片图片

4. String str = new String ("Hello") + new String("World");

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

public class stringclass {
    public static void main(String[] args) {
        String str=new String("Hello") + new String("World");
    }
}

图片图片

常见操作

1. equals(Object anObject)

public boolean equals(Object anObject) {
   if (this == anObject) {
        return true;
    }
    if (anObject instanceof String) {
        String anotherString = (String)anObject;
        int n = value.length;
        if (n == anotherString.value.length) {
            char v1[] = value;
           char v2[] = anotherString.value;
           int i = 0;
           while (n-- != 0) {
               if (v1[i] != v2[i])
                  return false;
               i++;
           }
           return true;
       }
   }
   return false;
}

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

2. hashCode()

public int hashCode() {
    int h = hash;
    if (h == 0 && value.length > 0) {
        char val[] = value;

        for (int i = 0; i < value.length; i++) {
            h = 31 * h + val[i];
        }
        hash = h;
   }
   return h;
}

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。因为移位运算比乘法运行更快更省性能。

3. charAt(int index)

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

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

4. compareTo(String anotherString)

public int compareTo(String anotherString) {
    int len1 = value.length;
    int len2 = anotherString.value.length;
    int lim = Math.min(len1, len2);
    char v1[] = value;
    char v2[] = anotherString.value;

    int k = 0;
    while (k < lim) {
       char c1 = v1[k];
       char c2 = v2[k];
       if (c1 != c2) {
           return c1 - c2;
       }
       k++;
   }
   return len1 - len2;
}

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

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

5. concat(String str)

public String concat(String str) {
    int otherLen = str.length();
    if (otherLen == 0) {
        return this;
    }
    int len = value.length;
    char buf[] = Arrays.copyOf(value, len + otherLen);
    str.getChars(buf, len);
    return new String(buf, true);
}

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

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

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

6. indexOf(int ch)

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

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

7. substring(int beginIndex)

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

返回一个从索引 beginIndex 开始一直到结尾的子字符串。

String不可变性

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

  • 安全

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

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

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

  • 性能
  • 当字符串是不可变时,字符串常量池才有意义。字符串常量池的出现,可以减少创建相同字面量的字符串,让不同的引用指向池中同一个字符串,为运行时节约很多的堆内存。若字符串可变,字符串常量池失去意义,基于常量池的String.intern()方法也失效,每次创建新的 String 将在堆内开辟出新的空间,占据更多的内存。
责任编辑:武晓燕 来源: Java技术指北
相关推荐

2024-04-10 08:22:44

2020-08-03 10:00:11

前端登录服务器

2023-04-24 08:00:00

ES集群容器

2020-07-03 08:21:57

Java集合框架

2020-02-18 16:20:03

Redis ANSI C语言日志型

2020-05-14 16:35:21

Kubernetes网络策略DNS

2023-02-10 09:04:27

2022-06-20 09:01:23

Git插件项目

2022-04-07 10:39:21

反射Java安全

2023-09-28 08:59:38

2022-08-01 11:33:09

用户分析标签策略

2019-08-13 15:36:57

限流算法令牌桶

2023-09-11 08:13:03

分布式跟踪工具

2021-04-08 07:37:39

队列数据结构算法

2019-05-14 09:31:16

架构整洁软件编程范式

2021-05-14 23:31:50

大数据计算机开发

2023-10-17 08:15:28

API前后端分离

2020-11-06 10:01:06

Nginx

2018-05-22 08:24:50

PythonPyMongoMongoDB

2024-09-23 08:00:00

消息队列MQ分布式系统
点赞
收藏

51CTO技术栈公众号