Java 基础:String 类源码分析

1,159 阅读35分钟

String 类实现接口属性1、私有属性私有类 CaseInsensitiveComparatorString 构造方法1、无参构造函数2、参数为 String 类型3、参数为字符数组4、参数为 int 数组5、参数为字节数组6、参数为 StringBuilder 或 StringBuffer7、特殊的 protected 构造方法其他方法1、concat 连接函数2、getBytes3、equals 和 hashCode4、比较方法5、前后缀判定6、索引获取7、字符串截取8、字符串替换9、valueOf10、intern()方法参考链接

Java 中数据类型分为两大类:基础数据类型(byte,short,int,long,float,double,char,boolean)和引用类型(String 类型和基础数据类型的包装类),可以看出 String 类型是非常特殊的,同时也是编写代码过程中使用比较频繁的一种类型,为了更好的了解该类型,决心钻研一下 String 类源码,希望能有所收获。

String 类实现接口

public final class String implements SerializableComparable<String>, CharSequence

从该类的声明中我们可以看出String是final类型的,表示该类不能被继承,同时该类实现了三个接口。

  • Serializable 接口是为了实现类对象的序列化,主要是能把堆内存中的对象的生命周期延长,做持久化操作。当下次再需要这个对象的时候,我们不用 new了,直接从硬盘中读取就可以了。
  • Comparable 接口强行对实现它的每个类的对象进行整体排序。此排序被称为该类的自然排序 ,类的 compareTo 方法被称为它的自然比较方法 。实现此接口的对象列表(和数组)可以通过 Collections.sort(和 Arrays.sort)进行自动排序。实现此接口的对象可以用作有序映射表中的键或有序集合中的元素,无需指定比较器。后续会讲解 String 类中的 compareTo 方法。
  • CharSequence 是一个接口,它只包括 length(), charAt(int index), subSequence(int start, int end)这几个 API 接口。除了 String 实现了 CharSequence 之外,StringBuffer 和 StringBuilder 也实现了 CharSequence 接口。CharSequence 就是字符序列,String, StringBuilder 和 StringBuffer 本质上都是通过字符数组实现的!

属性

1、私有属性

String 的底层是由 char 数组构成的

private final char[] value;
private int hash;

由于底层 char 数组是 final 的,所以 String 对象是不可变的,且不可被继承。 value:是一个 private
final 修饰的 char 数组,String 类是通过该数组来存在字符串的。 hash:是一个 private 修饰的 int
变量,用来存放 String 对象的 hashCode。

private staticfinal long serialVersionUID = -6849794470754667710L;
private static final ObjectStreamField[] serialPersistentFields = new ObjectStreamField[0];

因为String实现了Serializable接口,所以支持序列化和反序列化支持。Java的序列化机制是通过在运行时判断类的serialVersionUID来验证版本一致性的。在进行反序列化时,JVM会把传来的字节流中的serialVersionUID与本地相应实体(类)的serialVersionUID进行比较,如果相同就认为是一致的,可以进行反序列化,否则就会出现序列化版本不一致的异常(InvalidCastException)。

私有类 CaseInsensitiveComparator

private static class CaseInsensitiveComparator implements Comparator<String>, Serializable

该类同样实现了 Comparator 和 Serializable 接口,用于 String 类对象的排序。

public int compare(String var1, String var2) {
    int var3 = var1.length();
    int var4 = var2.length();
    int var5 = Math.min(var3, var4);

    for(int var6 = 0; var6 < var5; ++var6) {
        char var7 = var1.charAt(var6);
        char var8 = var2.charAt(var6);
        if (var7 != var8) {
            var7 = Character.toUpperCase(var7);
            var8 = Character.toUpperCase(var8);
            if (var7 != var8) {
                var7 = Character.toLowerCase(var7);
                var8 = Character.toLowerCase(var8);
                if (var7 != var8) {
                    return var7 - var8;
                }
            }
        }
    }

    return var3 - var4;
}

从源码来看,在该类中存在一个 compare 方法,方法对两个对象的比较流程如下:循环的次数为长度最小的字符串的长度;从头开始比较每个字符,如果不相等则转换为大写再比较,再不相等转换为小写比较,最后字符之间相减。减法操作会将获取字符的 hashCode,然后相减(汉字也是如此)。如果循环过程中字符比较都相等,最后返回两个字符串对象长度的差值。

String 构造方法

1、无参构造函数

public String() {
    this.value = "".value;
}
String s = new String();
System.out.println(s);  //值为"",也就是空字符串
System.out.println(s.hashCode());   //hash未赋初始值,所以默认值为0,后期再详细讲hashCode方法

2、参数为 String 类型

public String(String var1) {
    this.value = var1.value;
    this.hash = var1.hash;
}

首先声明一下 Java 的语法是允许在一个类中访问该类的实例对象的私有属性的。但是其他类就不可了。

3、参数为字符数组

public String(char[] var1) {
    this.value = Arrays.copyOf(var1, var1.length);
}

//var2是字符数组开始截取的位置,从0开始;var3是截取的长度
public String(char[] var1, int var2, int var3) {
    if (var2 < 0) {
         throw new StringIndexOutOfBoundsException(var2);
     } else {
         if (var3 <= 0) {
             if (var3 < 0) {
                 throw new StringIndexOutOfBoundsException(var3);
             }

             if (var2 <= var1.length) {
                 this.value = "".value;
                 return;
             }
         }

         if (var2 > var1.length - var3) {
             throw new StringIndexOutOfBoundsException(var2 + var3);
         } else {
             this.value = Arrays.copyOfRange(var1, var2, var2 + var3);
         }
     }
 }

同样都是用字符数组创建 String,前者是复制完整的字符数组到 String 中的 value 值,后者是从截取字符数组的一部分内容复制到 String 中。使用 Arrays.copyOf 方法或 Arrays.copyOfRange 方法进行复制,创建一个新的字符串对象,随后修改的字符数组不影响新创建的字符串。

4、参数为 int 数组

    public String(int[] var1, int var2, int var3) {
        if (var2 < 0) {
            throw new StringIndexOutOfBoundsException(var2);
        } else {
            if (var3 <= 0) {
                if (var3 < 0) {
                    throw new StringIndexOutOfBoundsException(var3);
                }

                if (var2 <= var1.length) {
                    this.value = "".value;
                    return;
                }
            }

            if (var2 > var1.length - var3) {
                throw new StringIndexOutOfBoundsException(var2 + var3);
            } else {
                int var4 = var2 + var3;
                int var5 = var3;

                int var7;
                for(int var6 = var2; var6 < var4; ++var6) {
                    var7 = var1[var6];
                    if (!Character.isBmpCodePoint(var7)) {//判断是否为负数,正数为true
                        if (!Character.isValidCodePoint(var7)) {
                            throw new IllegalArgumentException(Integer.toString(var7));
                        }

                        ++var5;
                    }
                }

                char[] var10 = new char[var5];
                var7 = var2;

                for(int var8 = 0; var7 < var4; ++var8) {
                    int var9 = var1[var7];
                    if (Character.isBmpCodePoint(var9)) {
                        var10[var8] = (char)var9;
                    } else {
                        Character.toSurrogates(var9, var10, var8++);
                    }

                    ++var7;
                }

                this.value = var10;
            }
        }
    }

需要注意的是:作为参数的 int 数组中值,至少需要满足“大写字母(A-Z):65 (A)~ 90(Z);小写字母(a-z):97(a) ~ 122(z);字符数字(‘0’ ~ ‘9’):48(‘0’) ~ 57(‘9’)”的条件。当数组中值为其他数字时,得到的字符串结果可能为空或特殊符号。

5、参数为字节数组

在 Java 中,String 实例中保存有一个 char[] 字符数组,char[] 字符数组是以 unicode 码来存储的,String 和 char 为内存形式。

byte 是网络传输或存储的序列化形式,所以在很多传输和存储的过程中需要将 byte[] 数组和 String 进行相互转化。所以 String 提供了一系列重载的构造方法来将一个字符数组转化成 String,提到 byte[] 和 String 之间的相互转换就不得不关注编码问题。

String(byte[] bytes, Charset charset)

该构造方法是指通过 charset 来解码指定的 byte 数组,将其解码成 unicode 的 char[] 数组,构造成新的 String。

这里的 bytes 字节流是使用 charset 进行编码的,想要将他转换成 unicode 的 char[] 数组,而又保证不出现乱码,那就要指定其解码方式。

通过字节数组构造 String 有很多形式,会使用 StringCoding.decode 方法进行解码,按照是否指定解码方式分的话可以分为两种:

a、

public String(byte[] var1, int var2, int var3) {
   checkBounds(var1, var2, var3);
     this.value = StringCoding.decode(var1, var2, var3);
 }

 public String(byte[] var1) {
     this((byte[])var1, 0, var1.length);
 }

这两种构造方法没有指定编码格式,默认使用 ISO-8859-1 编码格式进行编码操作。

    static char[] decode(byte[] var0, int var1, int var2) {
        String var3 = Charset.defaultCharset().name();

        try {
            return decode(var3, var0, var1, var2);
        } catch (UnsupportedEncodingException var6) {
            warnUnsupportedCharset(var3);

            try {
                return decode("ISO-8859-1", var0, var1, var2);
            } catch (UnsupportedEncodingException var5) {
                MessageUtils.err("ISO-8859-1 charset not available: " + var5.toString());
                System.exit(1);
                return null;
            }
        }
    }

b、

String(byte bytes[], Charset charset)
String(byte bytes[], String charsetName)
String(byte bytes[], int offset, int length, Charset charset)
String(byte bytes[], int offset, int length, String charsetName)

当构造方法参数中带有 charsetName 或者 charset 的时候,使用的解码的字符集就是我们指定的 charsetName 或者 charset。

6、参数为 StringBuilder 或 StringBuffer

    public String(StringBuffer var1) {
        synchronized(var1) {
            this.value = Arrays.copyOf(var1.getValue(), var1.length());
        }
    }

    public String(StringBuilder var1) {
        this.value = Arrays.copyOf(var1.getValue(), var1.length());
    }

这两个构造方法是很少用到的,平时多使用 StringBuffer.toString 方法或者 StringBuilder.toString 方法。其中 StringBuffer.toString 是调用 String(char[] var1, boolean var2);而 StringBuilder.toString 则是调用 String(char[] var1, int var2, int var3)。关于效率问题,Java 的官方文档有提到说使用 StringBuilder 的 toString 方法会更快一些,原因是 StringBuffer 的 toString 方法是 synchronized 的,在牺牲了效率的情况下保证了线程安全。

7、特殊的 protected 构造方法

    String(char[] var1, boolean var2) {
        this.value = var1;
    }

从代码中我们可以看出,该方法和 String(char[] value)有两点区别:

  • 第一个区别:该方法多了一个参数:boolean var2,这个参数上述我们也提到过,在 StringBuffer.toString 方法中有使用过,当时传值为 true。但是实际使用过程中可以通过该构造函数直接进行字符串对象的生成,加入这个 var2 的可以有效区分于 String(char[] value) 方法,不加这个参数就没办法定义这个函数,只有参数是不同才能进行重载。
  • 第二个区别:具体的方法实现不同。我们前面提到过 String(char[] value) 方法在创建 String 的时候会用到 Arrays 的 copyOf 方法将 value 中的内容逐一复制到 String 当中,而这个 String(char[] var1, boolean var2) 方法则是直接将 var1的引用赋值给 String 的 value。那么也就是说,这个方法构造出来的 String 和参数传过来的 char[] var1共享同一个数组。

在网上看到说是有两点区别,关于第二点区别,关键在于调用方调用该构造方法前是怎么处理的,在 StringBuffer.toString 方法中可以详细的看出 new String(char[] var1, boolean var2) 的使用。

    public synchronized String toString() {
        if (this.toStringCache == null) {
            this.toStringCache = Arrays.copyOfRange(this.value, 0this.count);
        }

        return new String(this.toStringCache, true);
    }

其中 toStringCache 表示缓存,用来保存上一次调用 toString 的结果,如果 value 的字符串序列发生改变,就会将它清空。首先判断 toStringCache 是否为 null,如果是先将 value 通过 Arrays.copyOfRange 方法复制到缓存里,然后使用 toStringCache new一个 String。

综上,我认为该构造方法虽然无法拿来直接使用,可是在别的地方可以使用,比如说 StringBuffer.toString 方法中。至于为何要再构建这么一个特殊的构造方法,而不是直接使用 String(char[] value) 方法,原因在于 StringBuffer 中的 toStringCache 属性存在,它的意义不支持在 toString 方法中直接使用 String(char[] value) 方法。(该部分仅为个人观点,如有差错,请指正!)

为什么 Java 会提供这样一个方法呢?

  • 性能好:一个是直接给数组赋值(如果 StringBuffer 的 value 字符串序列不发生改变,仅需要复制一次即可),一个是逐一拷贝,当然是直接赋值快了。
  • 节约内存:该方法之所以设置为 protected,是因为一旦该方法设置为公有,在外面可以访问的话,如果构造方法没有对 arr 进行拷贝,那么其他人就可以在字符串外部修改该数组,由于它们引用的是同一个数组,因此对 arr 的修改就相当于修改了字符串,那就破坏了字符串的不可变性。
  • 安全的:对于调用他的方法来说,由于无论是原字符串还是新字符串,其 value 数组本身都是 String 对象的私有属性,从外部是无法访问的,因此对两个字符串来说都很安全。

讲到这里,就需要讲下 String 类中还有没有其他的方法像这个构造函数那样“性能好的、节约内存的、安全”。其实在 Java7 之前 String 类中也有很多这样的方法,比如 substring,replace,concat,valueOf 等方法,实际上它们使用的是 String(int var1, int var2, char[] var3)方法来实现。

但是在 Java 7 之后,substring 已经不再使用这种“优秀”的方法了,以下是 Java8 中的源码:

    public String substring(int var1) {
        if (var1 < 0) {
            throw new StringIndexOutOfBoundsException(var1);
        } else {
            int var2 = this.value.length - var1;
            if (var2 < 0) {
                throw new StringIndexOutOfBoundsException(var2);
            } else {
                return var1 == 0 ? this : new String(this.value, var1, var2);
            }
        }
    }

Java8 中 substring 方法涉及到的 new String 源码如下:

    public String(char[] var1, int var2, int var3) {
        if (var2 < 0) {
            throw new StringIndexOutOfBoundsException(var2);
        } else {
            if (var3 <= 0) {
                if (var3 < 0) {
                    throw new StringIndexOutOfBoundsException(var3);
                }

                if (var2 <= var1.length) {
                    this.value = "".value;
                    return;
                }
            }

            if (var2 > var1.length - var3) {
                throw new StringIndexOutOfBoundsException(var2 + var3);
            } else {
                this.value = Arrays.copyOfRange(var1, var2, var2 + var3);
            }
        }
    }

会对传进来的 value 值通过 Arrays.copyOfRange 方法进行拷贝。

反观 Java6 中 substring 方法涉及到的 new String 源码如下:

    String(int var1, int var2, char[] var3) {
        this.value = var3;
        this.offset = var1;
        this.count = var2;
    }

同 new String(char[] var1, boolean var2) 构造函数的第二个特点一样,构造出来的 String 和参数传过来的 char[] value 共享同一个数组 。这就可能会造成内存泄漏问题。

看一个例子,假设一个方法从某个地方(文件、数据库或网络)取得了一个很长的字符串,然后对其进行解析并提取其中的一小段内容,这种情况经常发生在网页抓取或进行日志分析的时候。

下面是示例代码:

String aLongString = "...averylongstring...";
String aPart = aLongString .substring(2040);    //aPart字符串共享aLongString部分数据
return aPart;

在这里 aLongString 只是临时的,真正有用的是 aPart,其长度只有 20 个字符,但是它的内部数组却是从 aLongString 那里共享的,因此虽然 aLongString 本身可以被回收,但它的内部数组却不能释放。这就导致了内存泄漏。如果一个程序中这种情况经常发生有可能会导致严重的后果,如内存溢出,或性能下降。

新的实现虽然损失了性能,而且浪费了一些存储空间,但却保证了字符串的内部数组可以和字符串对象一起被回收,从而防止发生内存泄漏,因此新的 substring 比原来的更健壮。

其他方法

length() 返回字符串长度
isEmpty() 返回字符串是否为空
charAt(int index) 返回字符串中第(index+1)个字符(数组索引)
char[] toCharArray() 转化成字符数组
trim()去掉两端空格
toUpperCase()转化为大写
toLowerCase()转化为小写
boolean matches(String regex) 判断字符串是否匹配给定的regex正则表达式
boolean contains(CharSequence s) 判断字符串是否包含字符序列 s
String[] split(String regex, int limit) 按照字符 regex将字符串分成 limit 份
String[] split(String regex) 按照字符 regex 将字符串分段

1、concat 连接函数

    public String concat(String var1) {
        int var2 = var1.length();
        if (var2 == 0) {
            return this;
        } else {
            int var3 = this.value.length;
            char[] var4 = Arrays.copyOf(this.value, var3 + var2);
            var1.getChars(var4, var3);
            return new String(var4, true);
        }
    }

拼接 var1 会生成一个新的字符串对象,对原有字符串无影响。

2、getBytes

byte[] getBytes() 使用平台的默认字符集将此 String 编码为 byte 序列,并将结果存储到一个新的 byte 数组中。
byte[] getBytes(String var1)     使用指定的字符集将此 String 编码为 byte 序列,并将结果存储到一个新的 byte 数组中。
byte[] getBytes(Charset var1)        使用给定的 charset 将此 String 编码到 byte 序列,并将结果存储到新的 byte 数组。
void getBytes(int var1, int var2, byte[] var3, int var4)    已过时

值得注意的是,在使用这些方法的时候一定要注意编码问题。比如:String s = "你好,世界!"; byte[] bytes = s.getBytes();这段代码在不同的平台上运行得到结果是不一样的。由于没有指定编码方式,所以在该方法对字符串进行编码的时候就会使用系统的默认编码方式。

在中文操作系统中可能会使用 GBK 或者 GB2312 进行编码,在英文操作系统中有可能使用 iso-8859-1 进行编码。这样写出来的代码就和机器环境有很强的关联性了,为了避免不必要的麻烦,要指定编码方式。

3、equals 和 hashCode

    public boolean equals(Object var1) {
        if (this == var1) {
            return true;
        } else {
            if (var1 instanceof String) {
                String var2 = (String)var1;
                int var3 = this.value.length;
                if (var3 == var2.value.length) {
                    char[] var4 = this.value;
                    char[] var5 = var2.value;

                    for(int var6 = 0; var3-- != 0; ++var6) {
                        if (var4[var6] != var5[var6]) {
                            return false;
                        }
                    }

                    return true;
                }
            }

            return false;
        }
    }

    public int hashCode() {
        int var1 = this.hash;
        if (var1 == 0 && this.value.length > 0) {
            char[] var2 = this.value;

            for(int var3 = 0; var3 < this.value.length; ++var3) {
                var1 = 31 * var1 + var2[var3];
            }

            this.hash = var1;
        }

Java 中基于各种数据类型分析 == 和 equals 的区别一节中讲到 String 类有重写自己的 equals 和 hashCode 方法,所以它俩需要一起讲述。

首先是 equals 方法,它比较的流程是:字符串对象相同(即自我比较);类型一致且长度相等时,比较字符内容是否相同。

然后是 hashCode 方法,如果 hash 值不等于 0,且 value.length 大于 0,则进行 hash 值计算。这里重点说下 var1 == 0 这一判定条件,var1 是一个 int 类型的值,默认值为 0,因此 0 可以表示可能未执行过 hash 计算,但不能表示一定未执行过 hash 计算,原因是我们现在还不确定 hash 计算后是否会产生 0 值;

执行 hash 计算后,会不会产生值为 0 的 hash呢?根据 hash 的计算逻辑,当 val2[0] = 0 时,根据公式 var1 = 31 * var1 + val2[i]; 进行计算, var1 的值等于 0。但是经过查询 ASCII 表发现,null 的 ASCII 值为 0 。显然 val2[0]中永远不可能存放 null,因此 hash 计算后不会产生 0 值, var1== 0 可以作为是否进行过 hash 计算的判定条件。

最后得到计算公式为:

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

为什么要使用这个公式,就是在存储数据计算 hash 地址的时候,我们希望尽量减少有同样的 hash 地址。如果使用相同 hash 地址的数据过多,那么这些数据所组成的 hash 链就更长,从而降低了查询效率。
所以在选择系数的时候要选择尽量长的系数并且让乘法尽量不要溢出的系数,因为如果计算出来的 hash 地址越大,所谓的“冲突”就越少,查找起来效率也会提高。

选择31作为因子的原因: 为什么 String hashCode 方法选择数字31作为乘子

有这样一道面试题“两个对象的 hashCode()相同,则 equals()也一定为 true,对吗?”答案:不对,两个对象的 hashCode()相同,equals()不一定 true。

String str1 = "通话";
String str2 = "重地";
System.out.println(String.format("str1:%d | str2:%d", str1.hashCode(),str2.hashCode()));
System.out.println(str1.equals(str2));

//结果
str1:1179395 | str2:1179395
false

很显然“通话”和“重地”的 hashCode() 相同,然而 equals() 则为 false,因为在散列表中,hashCode()相等即两个键值对的哈希值相等,然而哈希值相等,并不一定能得出键值对相等。

4、比较方法

boolean equals(Object anObject); 比较对象
boolean contentEquals(StringBuffer sb); 与StringBuffer对象比较内容
boolean contentEquals(CharSequence var1); 与字符比较内容
boolean equalsIgnoreCase(String anotherString);忽略大小写比较字符串对象
int compareTo(String anotherString); 比较字符串
int compareToIgnoreCase(String str); 忽略大小写比较字符串
boolean regionMatches(int toffset, String other, int ooffset, int len)局部匹配
boolean regionMatches(boolean ignoreCase, int toffset, String other, int ooffset, int len) 可忽略大小写局部匹配

contentEquals 方法

    public boolean contentEquals(CharSequence var1) {
        if (var1 instanceof AbstractStringBuilder) {
            if (var1 instanceof StringBuffer) {
                synchronized(var1) {
                    return this.nonSyncContentEquals((AbstractStringBuilder)var1);
                }
            } else {
                return this.nonSyncContentEquals((AbstractStringBuilder)var1);
            }
        } else if (var1 instanceof String) {
            return this.equals(var1);
        } else {
            char[] var2 = this.value;
            int var3 = var2.length;
            if (var3 != var1.length()) {
                return false;
            } else {
                for(int var4 = 0; var4 < var3; ++var4) {
                    if (var2[var4] != var1.charAt(var4)) {
                        return false;
                    }
                }
                return true;
            }
        }
    }

String 、StringBuilder、StringBuffer 都实现了 CharSequence 接口,所以上述方法可以接收这三种类型的参数。另外 StringBuilder、StringBuffer 继承了 AbstractStringBuilder 父类,所以它俩通过 nonSyncContentEquals 方法进行比较,注意 StringBuffer 需要考虑线程安全,加锁之后再调用。

compareTo 方法

    public int compareTo(String var1) {
        int var2 = this.value.length;
        int var3 = var1.value.length;
        int var4 = Math.min(var2, var3);
        char[] var5 = this.value;
        char[] var6 = var1.value;

        for(int var7 = 0; var7 < var4; ++var7) {
            char var8 = var5[var7];
            char var9 = var6[var7];
            if (var8 != var9) {
                return var8 - var9;
            }
        }

        return var2 - var3;
    }

通过下面这个例子进行展示:

String s1 = new String("abc");
String s2 = new String("abcdfg");
System.out.println(s2.compareTo(s1));    //3
System.out.println(s1.compareTo(s2));    //-3
s2 = new String("fghjkl");
System.out.println(s2.compareTo(s1));    //5

当两个对象内容完全一致时,返回结果为 0;在字符串最小长度下,如果有不同的字符,系统则自动转换为 int 类型做差值。

5、前后缀判定

boolean startsWith(String var1, int var2)     测试此字符串从指定索引开始的子字符串是否以指定前缀开始
boolean startsWith(String var1)     测试此字符串是否以指定的前缀开始。
boolean endsWith(String var1)        测试此字符串是否以指定的后缀结束。

6、索引获取

int indexOf(int var1)        返回指定字符(int 转 char)在此字符串中第一次出现处的索引。从0索引开始
int indexOf(int var1, int var2)    返回在此字符串中第一次出现指定字符处的索引,从指定的索引开始搜索。var2 小于字符串的长度
int indexOf(String var1)        返回指定子字符串在此字符串中第一次出现处的索引,从0索引开始。var1必须是此字符串的一个连续子集,否则返回-1
int indexOf(String var1, int var2)        返回指定子字符串在此字符串中第一次出现处的索引,从指定的索引开始。

int lastIndexOf(int var1)    返回指定字符(int 转 char)在此字符串中最后一次出现处的索引。从0索引开始
int lastIndexOf(int var1, int var2)    返回在此字符串中最后一次出现指定字符处的索引,从指定的索引开始搜索。var2 小于字符串的长度
int lastIndexOf(String var1)    返回指定子字符串在此字符串中最后一次出现处的索引,从0索引开始。var1必须是此字符串的一个连续子集,否则返回-1
int lastIndexOf(String var1, int var2)    返回指定子字符串在此字符串中最后一次出现处的索引,从指定的索引开始。

7、字符串截取

String substring(int var1)    返回一个新的字符串,它是此字符串的一个子字符串。从var1开始截取,截取长度为此字符串的长度减去var1
String substring(int var1, int var2)    返回一个新字符串,它是此字符串的一个子字符串。从var1开始截取,截取长度为var2。
CharSequence subSequence(int var1, int var2)        返回一个新的字符序列,它是此序列的一个子序列。

8、字符串替换

String replace(char oldChar, char newChar)     返回一个新的字符串,它是通过用 newChar 替换此字符串中出现的所有 oldChar 得到的。
String replaceFirst(String regex, String replacement)         使用给定的 replacement 替换此字符串匹配给定的正则表达式的第一个子字符串。
String replaceAll(String regex, String replacement)     使用给定的 replacement 替换此字符串所有匹配给定的正则表达式的子字符串。
String replace(CharSequence var1, CharSequence var2)    使用指定的字面值替换序列替换此字符串所有匹配字面值目标序列的子字符串。
    public String replace(char var1, char var2) {
        if (var1 != var2) {
            int var3 = this.value.length;
            int var4 = -1;
            char[] var5 = this.value;

            do {
                ++var4;
            } while(var4 < var3 && var5[var4] != var1);

            if (var4 < var3) {
                char[] var6 = new char[var3];

                for(int var7 = 0; var7 < var4; ++var7) {
                    var6[var7] = var5[var7];
                }

                while(var4 < var3) {
                    char var8 = var5[var4];
                    var6[var4] = var8 == var1 ? var2 : var8;
                    ++var4;
                }

                return new String(var6, true);
            }
        }

        return this;
    }

replace 的参数可以是 char 或者 CharSequence,即可以支持字符的替换, 也支持字符串的替换。当参数为 char 时,是通过自己自定义的方法来更换字符;当参数为 CharSequence 时,实际调用的是 replaceAll 方法。replaceAll 和 replaceFirst 的参数是 regex,即基于规则表达式的替换。区别是一个全部替换,一个只替换第一个。

9、valueOf

static String valueOf(Object var0)
static String valueOf(char[] var0)
static String valueOf(char[] var0, int var1, int var2)
static String valueOf(boolean var0)
static String valueOf(int var0)
static String valueOf(long var0)
static String valueOf(float var0)
static String valueOf(double var0)

valueOf 都是静态函数,不需要实例化 String 对象,直接调用用于将其他基本数据类型转换为 String 类型。

10、intern()方法

public native String intern();

intern 方法是 Native 调用,它的作用是在全局字符串常量池里寻找等值的对象的引用,如果没有找到则在常量池中存放当前字符串对象的引用并返回该引用,否则直接返回常量池中已存在的 String 对象引用。

intern 方法与常量池有着很大的联系,通过学习该方法的使用,便于我们了解内存分配的概念,所以该方法会在后续章节里详细讲解。想要了解的朋友可以先了解一下常量池的知识,前往 Java 中方法区与常量池 即可。

参考链接