【JAVA高级&常用类】深入了解String类

【JAVA高级&常用类】深入了解String类

概述

通过该笔记来巩固对 String 类的了解和使用,以达到 “温故知新” 的目的。

目录

具体内容

0x01:String类的特性

概述

String 类: 代表字符串。 Java 程序中的所有字符串字面值(如 "abc" )都作为此类的实例实现。

String 是一个 final 类,具有 “不可变性”,代表不可变的字符串序列,并且不可被继承

1、当字符串被重新赋值时,需要在指定内存区域重新赋值,不能使用原有的 value 字符数组进行储存。

2、当调用 Stringreplace() 方法修改指定字符或字符串时,也需要重新指定内存区域赋值,不能使用原有的 value 进行赋值。如下代码所示

@Test
public void test1(){
    String s1 = "abc";//字面量的定义方式
    String s2 = "abc";
  
    System.out.println(s1 == s2); //true
   
    s1 = "hello";
 
    System.out.println("*******场景1:比较s1和s2的地址值*******");
    System.out.println(s1 == s2); //false
 
    System.out.println(s1);//hello
    System.out.println(s2);//abc
 
    System.out.println("*******场景2:重新赋值*******");
 
    String s3 = "abc";
    s3 += "def";
    System.out.println(s3); //abcdef
    System.out.println(s2); //abc
 
    System.out.println("*********场景3:替换字符串的值********");
 
    String s4 = "abc";
    String s5 = s4.replace('a', 'm');
    System.out.println(s4);//abc
    System.out.println(s5);//mbc
}

从上述的代码我们可以看出以下几种情况

  • 在场景一当中,我们定义了两个值都为 "abc" 的变量 s1 和 s2 ,虽然此时他们的内存地址是相同的,当我们修改了 s1 的值之后,s1 的内存地址被修改,s2 的值仍然还是 "abc"

  • 在场景二当中,我们将 “def” 字符串拼接到了 s3 当中,此时 s3 的值为 "abcdef" ,而 s2 的值仍为 “abc” ,这就再次证明了,无论是重新赋值,还是频接新的字符串,都会在 "方法区" 中重新开辟一个内存空间进行储存新的字符串值。

  • 场景三,使用 replace 替换字符串的值,也会开辟一个新的内存空间进行储存

  • 字符串常量池中是不会存储相同内容的字符串的:如果该新增的 “字符串值” 已经存在方法区中已经存在,则直接引用已存在的字符串内存地址。

  • String 实现了 Serializable 接口,表示字符串是支持序列化的

    序列化:将一个 JAVA 类的对象序列化为字节数据在网络中传输

String 实现了 Comparable 接口,表示 String 可以比较大小

  • 通过字面量的方式(区别于 new Class())给一个字符串赋值,例如 String foo = "abc" ,此时的字符串声明在字符串常量池当中。

  • String 内部定义了 final char[] value 用于存储字符串数据,如下

    public final class String
        implements java.io.Serializable, Comparable<String>, CharSequence {
        /** The value is used for character storage. */
        private final char value[];
        /** Cache the hash code for the string */
        private int hash; // Default to 0
        ///.....
    }
    

String对象的 “创建” 以及 “储存方式”

String 的实例化方式有以下两种

  • 方式一:通过字面量定义的方式
  • 方式二:通过 new + 构造器的方式
//方式一:
//通过字面量定义的方式:此时的s1和s2的数据javaEE声明在方法区中的字符串常量池中。
String s1 = "javaEE";
String s2 = "javaEE";

//方式二:
//通过new + 构造器的方式:此时的s3和s4保存的地址值,是数据在堆空间中开辟空间以后对应的地址值。
String s3 = new String("javaEE");
String s4 = new String("javaEE");

//内存地址值对比
System.out.println(s1 == s2);//true
System.out.println(s1 == s3);//false
System.out.println(s1 == s4);//false
System.out.println(s3 == s4);//false

需要注意的是,这两种方式创建的 String ,就算内容相同,但是内存地址是不相同的。

原因是,通过 new String() 方式构建的字符串,实际是在 “堆” 中开辟了一个新的空间,用于储存该字符串在常量池中的内存地址。如下图所示

对象中的字符串属性是如何存储的?

每个对象都会再堆中开辟一个内存空间进行储存,而对象里面又包含了多个属性,如果属性的值为字符串类型,那么这个属性的值则储存的是该 “字符串值” 在常量池中的内存地址,所以由此可知,如果两个对象的字符串属性的 “字符串值” 相同,他们的内存地址也是相同的,如下代码所示:

Person p1 = new Person("Tom",12);
Person p2 = new Person("Tom",12);

System.out.println(p1.name.equals(p2.name));//对比值:true
System.out.println(p1.name == p2.name);//对比内存地址:true

p1.name = "Jerry";
System.out.println(p2.name);//Tom

如下图所示

String拼接操作的对比

String s1 = "javaEE";
String s2 = "hadoop";

String s3 = "javaEEhadoop";
String s4 = "javaEE" + "hadoop";
String s5 = s1 + "hadoop";
String s6 = "javaEE" + s2;
String s7 = s1 + s2;

情况一:通过字面量的形式(常量于常量之间)进行字符串拼接,值相同,他们的内存地址也是相同的,因为常量池中不会存在相同内容的常量。如下代码

System.out.println(s3 == s4);    //true

情况二:在对字符串进行赋值、拼接时,等号的右边有其他变量参与的情况下,则会在堆中开辟一个新的空间进行储存,类似于 new String() 的方式进行创建,如下代码

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
System.out.println(s6 == s7);    //false

但是需要注意得是,如果参与赋值、拼接的 “变量” 为一个常量(使用 final 修饰的字符串),此时等号左边的变量与具有相同的 “字符串值” 的变量的内存地址也是相同的,如下代码所示

String s1 = "javaEEhadoop";
String s2 = "javaEE";
String s3 = s2 + "hadoop";
System.out.println(s1 == s3);    //false
final String s4 = "javaEE";    //s4:常量
String s5 = s4 + "hadoop";
System.out.println(s1 == s5);    //true

使用 str.intern() 方法可以取到字符串变量在常量池中的 “内存地址” ,并赋值给另一个变量

String s8 = s6.intern();//返回值得到的s8使用的常量值中已经存在的“javaEEhadoop”
System.out.println(s3 == s8);//true

小练习

解释一下以下输出的结果

public class StringTest {
    String str = new String("good");
    char[] ch = { 't', 'e', 's', 't' };

    public void change(String str, char ch[]) {
        str = "test ok";
        ch[0] = 'b';
    }
   
    public static void main(String[] args) {
        StringTest ex = new StringTest();
        ex.change(ex.str, ex.ch);
        System.out.println(ex.str);    //good
        System.out.println(ex.ch);    //best
    }
}

1、在向 change() 方法传递形参 str 时,虽然传递的是变量的内存地址,但是由于 String 类型值不可变的性质,如果重新赋值,则会在内存中开辟新的内存空间,并赋给形参 str ,所以不会使类中原有的属性 str 的指向的地址值发生改变。

2、由于将 char 数组 ch 的内存地址传递给了形参 ch,此时他们指向的是同一个内存地址,当形参 ch 通过使用数组的下标修改了数组中的值,会导致原有的 ch 数组也发生了改变。

0x02:String常用的方法

常用的方法如下

  • int length()

    返回字符串的长度: return value.length

  • char charAt(int index)

    返回某索引处的字符 return value[index]

  • boolean isEmpty()

    判断是否是空字符串:return value.length == 0

  • String toLowerCase()

    使用默认语言环境,将 String 中的所有字符转换为小写

  • String toUpperCase()

    使用默认语言环境,将 String 中的所有字符转换为大写

  • String trim()

    返回字符串的副本,忽略前导空白和尾部空白

  • boolean equals(Object obj)

    比较字符串的内容是否相同

  • boolean equalsIgnoreCase(String anotherString)

    与equals方法类似,忽略大小写

  • String concat(String str)

    将指定字符串连接到此字符串的结尾。 等价于用 “+”

  • int compareTo(String anotherString)

    比较两个字符串的大小

  • String substring(int beginIndex)

    返回一个新的字符串,它是此字符串的从 beginIndex 开始截取到最后的一个子字符串。

  • String substring(int beginIndex, int endIndex)

    返回一个新字符串,它是此字符串,beginIndex 开始截取到 endIndex(不包含)的一个子字符串。

  • boolean endsWith(String suffix)

    测试此字符串是否以指定的后缀结束

  • boolean startsWith(String prefix)

    测试此字符串是否以指定的前缀开始

  • boolean startsWith(String prefix, int toffset)

    测试此字符串从指定索引开始的子字符串是否以指定前缀开始

  • boolean contains(CharSequence s)

    当且仅当此字符串包含指定的 char 值序列时,返回 true

  • int indexOf(String str)

    返回指定子字符串在此字符串中第一次出现处的索引

  • int indexOf(String str, int fromIndex)

    返回指定子字符串在此字符串中第一次出现处的索引,从指定的索引开始

  • int lastIndexOf(String str)

    返回指定子字符串在此字符串中最右边出现处的索引

  • int lastIndexOf(String str, int fromIndex)

    返回指定子字符串在此字符串中最后一次出现处的索引,从指定的索引开始反向搜索

    注:indexOflastIndexOf 方法如果未找到都是返回 -1

替换

  • String replace(char oldChar, char newChar)

    返回一个新的字符串,它是通过用 newChar 替换此字符串中出现的所有 oldChar 得到的。

  • String replace(CharSequence target, CharSequence replacement)

    使用指定的字面值替换序列替换此字符串所有匹配字面值目标序列的子字符串。

  • String replaceAll(String regex, String replacement)

    使用给定的 replacement 替换此字符串所有匹配给定的正则表达式的子字符串。

  • String replaceFirst(String regex, String replacement)

    使用给定的 replacement 替换此字符串匹配给定的正则表达式的第一个子字符串。

匹配

  • boolean matches(String regex)

    告知此字符串是否匹配给定的正则表达式。

正则表达式:参考文档

切片

  • String[] split(String regex)

    根据给定正则表达式的匹配拆分此字符串。

  • String[] split(String regex, int limit)

    根据匹配给定的正则表达式来拆分此字符串,最多不超过 limit 个,如果超过了,剩下的全部都放到最后一个元素中。

0x03:String类型转换

与基本数据类型、包装类之间的转换

  • String 转 基本数据类型、包装类:

    调用包装类的静态方法:parseXxx(str)

  • 基本数据类型、包装类 转 String:

    调用 String 重载的 valueOf(xxx)

如下例子所示

String str1 = "123";
// int num = (int)str1;//错误的
int num = Integer.parseInt(str1);
String str2 = String.valueOf(num);//"123"
String str3 = num + "";
System.out.println(str1 == str3);

与char[]之间的转换

  • String 转 char[]:

    调用 String 的 toCharArray()

  • char[] 转 String:

    调用 String 的构造器

如下例子所示

String str1 = "abc123";  //题目: a21cb3

char[] charArray = str1.toCharArray();
for (int i = 0; i < charArray.length; i++) {
    System.out.println(charArray[i]);
}

char[] arr = new char[]{'h','e','l','l','o'};
String str2 = new String(arr);
System.out.println(str2);

输出结果

a
b
c
1
2
3
hello

与byte[]之间的转换

编码:String 转 byte[]:

字符串转为字节,可以理解为将看得懂的数据,转为看不懂的二进制数据

  • 调用 StringgetBytes()

解码:byte[] 转 String:

编码的逆过程,字节转为字符串 (看不懂的二进制数据 转为 看得懂的数据)

  • 调用 String 的构造器

说明:解码时,要求解码使用的字符集必须与编码时使用的字符集一致,否则会出现乱码。

如下例子

String str1 = "abc123中国";
byte[] bytes = str1.getBytes();//使用默认的字符集,进行编码。
System.out.println(Arrays.toString(bytes));

byte[] gbks = str1.getBytes("gbk");//使用gbk字符集进行编码。
System.out.println(Arrays.toString(gbks));

System.out.println("******************");

String str2 = new String(bytes);//使用默认的字符集,进行解码。
System.out.println(str2);

String str3 = new String(gbks);
System.out.println(str3);//出现乱码。原因:编码集和解码集不一致!


String str4 = new String(gbks, "gbk");
System.out.println(str4);//没有出现乱码。原因:编码集和解码集一致!

输出结果

[97, 98, 99, 49, 50, 51, -28, -72, -83, -27, -101, -67]
[97, 98, 99, 49, 50, 51, -42, -48, -71, -6]
******************
abc123中国
abc123�й�
abc123中国

0x04:关于 StringBuffer、StringBuilder

StringStringBufferStringBuilder 三者有哪些异同的地方?

  • String
    • 不可变的字序列
    • 底层使用 char[] 储存
  • StringBuffer
    • 可变的字符序列,底层使用 char[] 储存
    • 线程安全的(源码中大量的额使用了 synchronized
    • 效率低
  • StringBuilder
    • 可变的字符序列,底层使用 char[] 储存
    • JDK5.0 之后新增的
    • 线程不安全的(考虑在非多线程环境下使用)
    • 效率高

从源码的角度去分析

String str = new String();//char[] value = new char[0];
String str1 = new String("abc");//char[] value = new char[]{'a','b','c'};
StringBuffer sb1 = new StringBuffer();//char[] value = new char[16];底层创建了一个长度是16的数组。

我们通过观察 StringBuffer 的源码可以得知,在初始化 StringBuffer 对象后,默认构建一个长度为 16 的 char[] 数组,如下图

如果我们在初始化 StringBuffer 对象时声明的了默认的字符串值,则长度会在默认值的基础上 +16,如下代码所示

StringBuffer sb2 = new StringBuffer("abc");
//char[] value = new char["abc".length() + 16];

但是需要注意的是,StringBufferlength() 方法实际返回的是该对象中实际存在的字符长度,而不是整个字符数组的长度,如下所示

StringBuffer sb1 = new StringBuffer();
StringBuffer sb2 = new StringBuffer("abc");
System.out.println(sb1.length()); //0
System.out.println(sb2.length()); //3

扩容问题

如果需要添加的数据底层数组的容量 “装” 不下了,那就需要扩容底层的数组。

默认情况下:扩容为原来容量的 2 倍 + 2,同时将原有数组中的元素 Arrays.copyOf() 复制到新的数组中。 如下源码所示

具体的扩容逻辑体现在 java/lang/AbstractStringBuilder.javanewCapacity() 方法当中,如下所示

源码跟踪:stringBuffer.append() --> super.append() --> ensureCapacityInternal() --> ensureCapacityInternal() --> newCapacity()

0x05:StringBuffer的常用方法

StringBuilder中的使用与StringBuffer相同,只是在StringBuilder中是同步的

  • StringBuffer append(xxx)
    • 提供了很多的append()方法,用于进行字符串拼接
  • StringBuffer delete(int start,int end)
    • 删除指定位置的内容
  • StringBuffer replace(int start, int end, String str)
    • 把[start,end)位置替换为str
  • StringBuffer insert(int offset, xxx)
    • 在指定位置插入xxx
  • StringBuffer reverse()
    • 把当前字符序列逆转
  • public int indexOf(String str)
    • 返回指定字符串的出现的起始坐标
  • public String substring(int start,int end) :
    • 返回一个从start开始到end索引结束的左闭右开区间的子字符串
  • public int length()
    • 返回当前对象的包含的字符串值长度
  • public char charAt(int n )
    • 获取指定坐标的字符值
  • public void setCharAt(int n ,char ch)
    • 设置指定坐标的字符

总结:

  • 增:append(xxx)

    • 由于append返回的当前对象(this)所以我们可以以方法链的形式去操作,例如

      StringBuffer s1 = new StringBuffer("abc");
      s1.append(1).append(2).append("3");
      System.out.println(s1);  //abc123
      
  • 删:delete(int start,int end)

  • 改:setCharAt(int n ,char ch) / replace(int start, int end, String str)

  • 查:charAt(int n )

  • 插:insert(int offset, xxx)

  • 长度:length();

  • 遍历:for() + charAt() / toString()

0x06:效率对比

对比 StringStringBufferStringBuilder 三者的效率

//初始设置
long startTime = 0L;
long endTime = 0L;
String text = "";
StringBuffer buffer = new StringBuffer("");
StringBuilder builder = new StringBuilder("");

//StringBuffer
startTime = System.currentTimeMillis();
for (int i = 0; i < 20000; i++) {
    buffer.append(String.valueOf(i));
}
endTime = System.currentTimeMillis();


System.out.println("StringBuffer的执行时间:" + (endTime - startTime));

//StringBuilder
startTime = System.currentTimeMillis();
for (int i = 0; i < 20000; i++) {
    builder.append(String.valueOf(i));
}
endTime = System.currentTimeMillis();
System.out.println("StringBuilder的执行时间:" + (endTime - startTime));

//String
startTime = System.currentTimeMillis();
for (int i = 0; i < 20000; i++) {
    text = text + i;
}
endTime = System.currentTimeMillis();
System.out.println("String的执行时间:" + (endTime - startTime));

执行结果

StringBuffer的执行时间:11
StringBuilder的执行时间:2
String的执行时间:1162

从执行结果可以看出,在进行相同的字符串拼接操作时,StringBuilder 的效率是最高的,String 的效率最低,且与前两者的差距都很大,说明了 String 在进行字符串拼接时,每次都需要在方法区中构建一块内存进行存储,导致开销巨大,效率也非常的低下。

结论,效率从从高到低排列:StringBuilder > StringBuffer > String

0x07:小练习

1、将一个字符串进行反转。将字符串中指定部分进行反转。比如 “abcdefg” 反转为 ”abfedcg

方式一:转换为char[]

public String reverse(String str,int startIndex,int endIndex){

    if(str != null){
        char[] arr = str.toCharArray();
        for(int x = startIndex,y = endIndex;x < y;x++,y--){
            char temp = arr[x];
            arr[x] = arr[y];
            arr[y] = temp;
        }

        return new String(arr);
    }
    return null;
}

方式二:使用String的拼接

public String reverse1(String str,int startIndex,int endIndex){
    if(str != null){
        //第1部分
        String reverseStr = str.substring(0,startIndex);
        //第2部分
        for(int i = endIndex;i >= startIndex;i--){
            reverseStr += str.charAt(i);
        }
        //第3部分
        reverseStr += str.substring(endIndex + 1);

        return reverseStr;

    }
    return null;
}

方式三:使用 StringBuffer/StringBuilder 替换String

public String reverse2(String str,int startIndex,int endIndex){
    if(str != null){
        StringBuilder builder = new StringBuilder(str.length());

        //第1部分
        builder.append(str.substring(0,startIndex));
        //第2部分
        for(int i = endIndex;i >= startIndex;i--){

            builder.append(str.charAt(i));
        }
        //第3部分
        builder.append(str.substring(endIndex + 1));

        return builder.toString();
    }
    return null;

}

2、获取一个字符串在另一个字符串中出现的次数。

比如:获取 “ab” 在 “abkkcadkabkebfkaabkskab” 中出现的次数

*/

    /**
     * 获取subStr在mainStr中出现的次数
     * @param mainStr
     * @param subStr
     * @return
     */
    public int getCount(String mainStr,String subStr){
    int mainLength = mainStr.length();
    int subLength = subStr.length();
    int count = 0;
    int index = 0;
    if(mainLength >= subLength){
        //方式一:
        //            while((index = mainStr.indexOf(subStr)) != -1){
        //                count++;
        //                mainStr = mainStr.substring(index + subStr.length());
        //            }

        //方式二:对方式一的改进
        while((index = mainStr.indexOf(subStr,index)) != -1){
            count++;
            index += subLength;
        }

        return count;
    }else{
        return 0;
    }
}

@Test
public void testGetCount(){
    String mainStr = "abkkcadkabkebfkaabkskab";
    String subStr = "ab";
    int count = getCount(mainStr, subStr);
    System.out.println(count);
}

3、获取两个字符串中最大相同子串。

比如:str1 = "abcwerthelloyuiodefabcdef"; str2 = "cvhellobnm"

提示:将短的那个串进行长度依次递减的子串与较长的串比较

 //前提:两个字符串中只有一个最大相同子串
    public String getMaxSameString(String str1,String str2){
        if(str1 != null && str2 != null){
            String maxStr = (str1.length() >= str2.length())? str1 : str2;
            String minStr = (str1.length() < str2.length())? str1 : str2;
            int length = minStr.length();

            for(int i = 0;i < length;i++){
                for(int x = 0,y = length - i;y <= length;x++,y++){
                    String subStr = minStr.substring(x,y);
                    if(maxStr.contains(subStr)){
                        return subStr;
                    }

                }
            }

        }
        return null;
    }

    // 如果存在多个长度相同的最大相同子串
    // 此时先返回String[],后面可以用集合中的ArrayList替换,较方便
    public String[] getMaxSameString1(String str1, String str2) {
        if (str1 != null && str2 != null) {
            StringBuffer sBuffer = new StringBuffer();
            String maxString = (str1.length() > str2.length()) ? str1 : str2;
            String minString = (str1.length() > str2.length()) ? str2 : str1;

            int len = minString.length();
            for (int i = 0; i < len; i++) {
                for (int x = 0, y = len - i; y <= len; x++, y++) {
                    String subString = minString.substring(x, y);
                    if (maxString.contains(subString)) {
                        sBuffer.append(subString + ",");
                    }
                }
//                System.out.println(sBuffer);
                if (sBuffer.length() != 0) {
                    break;
                }
            }
            String[] split = sBuffer.toString().replaceAll(",$", "").split("\\,");
            return split;
        }

        return null;
    }

    @Test
    public void testGetMaxSameString(){
        String str1 = "abcwerthello1yuiodefabcdef";
        String str2 = "cvhello1bnmabcdef";
        String[] maxSameStrings = getMaxSameString1(str1, str2);
        System.out.println(Arrays.toString(maxSameStrings));

    }

总结

通过本篇笔记的整理,再次巩固了对 StringStringBufferStringBuilder 类的了解和使用场景,以及其存在的一些优点和使用弊端,在后续的开发过程中,可以根据实际开放场景中来选择效率更高的实现。

Copyright: 采用 知识共享署名4.0 国际许可协议进行许可

Links: https://codeyee.com/archives/java-string-class.html