String

  • 「Java 没有内置的字符串类型」, 而是在标准 Java 类库中提供了一个「预定义类」 String。每个用「双引号括起来的字符串都是 String 类的一个实例」
  • 在 Java 8 中,String 内部是使用 char 数组来存储数据的
    • 而在 Java 9 之后,String 类的实现改用 byte 数组存储字符串」,同时使用 coder 来标识使用了哪种编码
  • String对象有一个方法length() str.length()即可获取字符串长度
    • 判断字符串内容是否为空除了检查长度是否为0,还可以str.equals("")==0 “”表示字符串有内容但是为空
    • 判断字符串既不是 null也不为空串if(str != null && str.length() != 0) 必须首先检查 str 是否为 null,因为如果在一个 null 值上调用方法,编译器会报错」。
  • 当将一个字符串与一个非字符串的值进行拼接时,后者被自动转换成字符串(「任何一个 Java 对象都可以转换成字符串如int age = 13;String rating = "PG" + age; // rating = "PG13"
    • 空串和 null 拼接的结果还是null,因为字符串不可变,本质上会使用append()方法进行连接。而当append()方法检查到传入参数为null时,会调用appendNull(),该方法返回一个null;
  • 论java8还是9,用来存储数据的 char 或者 byte 数组 都是final类型的,因此字符串是不可变的。(不仅仅如此) 但是可以通过反射修改
    • **String a+=str;**本质上是初始化了一个 StringBuilder 来进行拼接的,相当于
  • 因为String字符串不可变,所以是线程安全的

    1. String a = "hello";
    2. String b = "world";
    3. StringBuilder builder = new StringBuilder();
    4. builder.append(a);
    5. builder.append(b);
    6. a = builder.toString();
    7. //toString方法同样是生成了一个新的 String 对象,而不是在旧字符串的内容上做更改,相当于把旧字符串的引用指向的新的String对象。这也就是字符串 a 发生变化的原因。
    • 而如String str = "asdf";String x = str.toUpperCase();该方法本质上是创建了一个新的String对象,并将str引用指向了它

      String的equals()源码

      value是String类持有的一个final char value[]字段
      length是数组的一个数组的一个属性,表示数组长度
  • 可以看出,首先会先比较调用方法的对象与参数对象是否地址相同,如果地址相同自然值也会相同,返回true

  • ……

image.png\

字符串常量池String Pool

因为String类型非常常用,频繁创建字符串将会极大程度的影响程序的性能。为此,JVM 为了提高性能和减少内存开销,在实例化字符串常量的时候进行了一些优化:

  • 为字符串开辟了一个「字符串常量池 String Pool」,可以理解为缓存区
    • 创建字符串常量时,首先检查字符串常量池中是否存在该字符串
    • 若字符串常量池中存在该字符串,则直接返回该引用实例,无需重新实例化;若不存在,则实例化该字符串并放入池中(下次使用时便可直接取用) ```java String str1 = “hello”; String str2 = “hello”;

System.out.printl(”str1 == str2” : str1 == str2 ) //true
//创建str2时在字符串常量池中找到了值为hello的对象(str1),然后将str2引用指向了该对象,所以str1==str2

String aa=new String(“56CC”); String a=”56CC”; System.out.println(a==aa); //false 一个在栈内存中 一个在堆内存中,肯定内存位置不相等

String aa=new String(“56CVC”); String a=new String(“56CVC”); System.out.println(a==aa); //false 都是new一个新的对象,所以肯定不相等

  1. - **通过**`**String str = "i"**`** 的方式创建的字符串,java 虚拟机会自动将其分配到常量池中;**
  2. - **通过**`**String str = new String(“i”)**`**创建的字符串 则会被分到堆内存中。可通过 intern 方法手动加入常量池。**
  3. - **如果常量池已存在返回引用,不存在会在常量池创建该字符串引用并返回**
  4. - **所以通过new创建的String对象一定是个新对象。即便存在相同值的字符串在常量池,它也还会在堆中创建一个而不是直接引用**
  5. - 不使用new("?")的创建方式如果没找到只创建一个对象在常量池,而new方式没找到则创建2个对象,一个在常量池,一个在堆内存(这里跟上面的intern冲突了,会自动加进去为什么还要手动intern)
  6. ---
  7. - **可以使用 **`**String.intern()**`**方法在运行过程中手动的将字符串添加到 String Pool 中。除了添加,它还会返回一个引用,可以创建字符串变量,如**`String str3 = str1.intern();`
  8. - **当一个字符串调用 intern() 方法时,如果 String Pool 中已经存在一个字符串和该字符串的值相等,那么就会返回 String Pool 中字符串的引用;否则,就会在 String Pool 中添加一个新的字符串,并返回这个新字符串的引用**
  9. ---
  10. 需要注意的是,字符串常量池的位置在 JDK 1.7 有所变化:
  11. - JDK 1.7 之前」,字符串常量池存在于「常量存储」(Constant storage)中
  12. - JDK 1.7 之后」,字符串常量池存在于「堆内存」(Heap)中。
  13. ![image.png](https://cdn.nlark.com/yuque/0/2021/png/2319994/1625227906365-5b2f5d9a-a965-42f5-90d8-a86f121a826f.png#clientId=u56d3d1ed-70d1-4&crop=0&crop=0&crop=1&crop=1&from=paste&height=222&id=u67021465&margin=%5Bobject%20Object%5D&name=image.png&originHeight=718&originWidth=1488&originalType=binary&ratio=1&rotation=0&showTitle=false&size=288382&status=done&style=none&taskId=ua857fcd7-b213-4f8f-ad32-de7a0c70c2e&title=&width=461)
  14. <a name="w2ZDn"></a>
  15. # StringBuilder/StringBuffer
  16. - **相比于不可变的String,二者是可变字符串**
  17. - **二者的使用方法都是一致的**
  18. - **二者区别在于:StringBuilder 不是线程安全的,在多线程环境下使用会出现数据不一致的问题,而 StringBuffer 是线程安全的。**
  19. - **在 StringBuffer 类内,常用的方法都使用了synchronized 关键字进行同步,所以是线程安全的,而Builder没有进行同步所以不安全**
  20. - **但是synchronized同步会影响运行速度,所以运行速度 StringBuilder 大于 StringBuffer。所以单线程下优先使用StringBuilder**
  21. ---
  22. - **二者不能直接字符串赋值,必须通过构造函数初始化时赋值,否则就只能通过append赋值**
  23. - **但是可以改变引用的对象**
  24. - 添加,连接内容使用`append()方法`,该方法可添加字符串或者字符 **不能使用+连接符**
  25. - `toString方法`可以通过一个`StringBuilder/Buffer`来构建一个String对象并返回
  26. - **可变字符串即String可以直接转换**,如`String str=new String(new StringBuilder("1234"));`
  27. ```java
  28. StringBuilder s1=new StringBuilder("DDDD");
  29. StringBuilder s2=new StringBuilder();
  30. s2="555";//错误,不能字符串赋值
  31. s2=s2.append("555");
  32. s1+=s2;//错误,不能字符串赋值
  33. s1=s1.append(s2);
  34. System.out.println(s1); //->DDDD555
  35. s1=s2;//正确,虽然不能字符串赋值但是可以改变引用指向

可变字符串方法

  • 连接字符串append(String s)将指定的字符串追加到此字符序列。
  • 反转字符串 reverse()将此字符序列用其反转形取代。
  • delete(int start, int end)移除此序列的子字符串中的字符。
  • insert(int offset, int i)将int参数的字符串表示形式插入此序列中。
  • replace(int start, int end, String str)使用给定String中的字符替换此序列的子字符串中的字符。

StringJoiner 分隔符拼接

  • 空时添加第一个字符串时不会拼接分隔符
  • 可以指定开头与结尾:var sj = new StringJoiner(", ", "Hello ", "!");
  • 数组分隔符拼接为字符串:String s=String.join("...", 数组);
    var sj = new StringJoiner(", ");
    sj.add("ss");   //ss   #为空时添加第一个字符串时不会拼接分隔符
    sj.add("ss");   //ss-ss
    //-----------------
    var sj = new StringJoiner(", ", "Hello ", "!");
          for (String name : names) {
              sj.add(name);
          }
    

String不可变原理详解

上述所说,String不可变是因为实现String的数组是final类型的,所以数组无法被修改。
但是,这个无法被修改仅仅是指引用地址不可被修改(也就是说栈里面的这个叫 value 的引用地址不可变,编译器不允许我们把 value 指向堆中的另一个地址),并不代表存储在堆中的这个数组本身的内容不可变。举个例子:
image.png
如果我们直接修改数组中的元素,是完全 OK 的
image.png
所以,除了final外,还有如下原因

  • 实现String的数组被设置为了private。并且 String 类没有对外提供修改这个数组的方法,所以它初始化之后外界没有有效的手段去改变它;
  • 其次,String 类被 final 修饰的,也就是不可继承,避免被他人继承后破坏;
  • 最重要的!是因为 Java 作者在 String 的所有方法里面,都很小心地避免去修改了 char 数组中的数据,涉及到对 char 数组中数据进行修改的操作全部都会重新创建一个 String 对象。

    设计为不可变的原因

  • 首先,字符串常量池的需要,这可以节省资源。但是这容易造成多个字符串变量引用同一个对象。而如果String可变,那改变一个字符串变量就会引起很多跟他指向一个对象的字符串变量改变值

  • 另外一个原因就是为了安全。作为最基础最常用的数据类型,String 被许多 Java 类库用来作为参数,如果 String 不是固定不变的,将会引起各种安全隐患。
    • 线程安全
    • 我们用不能存在相同元素的hashSet举例,原本hashset存了2个不同值的String,现在让s3引用s1对象,但是hashset识别不出s3和s1区别

image.png

通过反射修改String

image.png

创建不可变对象的方法

  • 不要提供 setter 方法(包括修改字段的方法和修改字段引用对象的方法);
  • 将类的所有字段定义为 final、private 的;
    • 不允许子类重写方法。简单的办法是将类声明为 final,
    • 更好的方法是将构造函数声明为私有的,通过工厂方法创建对象;
  • 如果类的字段是对可变对象的引用,不允许修改被引用对象

    数组与字符串的转换

  • 字符串转字符数组:char ch[]=str.toCharArray();

  • 字符串转字节数组:str.getBytes(); 参数可选。参数为字符编码,如utf-8
  • 数组转字符串:String str=new String(ch[]);

    • String.valueOf(char [])

      实用方法

  • equalsIgnoreCase(String s) 忽略大小写比较字符串内容是否相同

  • int compareTo:比较字符串大小 先比较第一个,如果现同再比第二个…详见链接
  • compareToIgnoreCase(String s)忽略大小写比较字符串大小
  • indexOf(String s) 查找字符串s在另外一个字符串中首次出现的位置
  • trim() 清除字符串前面与后面的空格
  • public String[] split(String regex, int limit) 方法根据匹配给定的正则表达式来拆分字符串。

    • 注意: . 、 $、 | 和 * 等转义字符,必须得加 \。
    • 注意:多个分隔符,可以用 | 作为连字符。 ```java public static void main(String args[]) { String str = new String(“Welcome-to-Runoob”);

      System.out.println(“- 分隔符返回值 :” ); for (String retval: str.split(“-“)){

         System.out.println(retval);
      

      }

      System.out.println(“”); System.out.println(“- 分隔符设置分割份数返回值 :” ); for (String retval: str.split(“-“, 2)){

         System.out.println(retval);
      

      }

      System.out.println(“”); String str2 = new String(“www.runoob.com”); System.out.println(“转义字符返回值 :” ); for (String retval: str2.split(“\.”, 3)){

         System.out.println(retval);
      

      }

      System.out.println(“”); String str3 = new String(“acount=? and uu =? or n=?”); System.out.println(“多个分隔符返回值 :” ); for (String retval: str3.split(“and|or”)){

         System.out.println(retval);
      

      } }

  • 分隔符返回值 : Welcome to Runoob

  • 分隔符设置分割份数返回值 : Welcome to-Runoob

转义字符返回值 : www runoob com

多个分隔符返回值 : acount=? uu =? n=? ```

常量堆叠

  • 常量折叠会把常量表达式的值求出来作为常量嵌在最终生成的代码
    • String str3 = "str" + "ing";编译器会给你优化成 String str3 = "string";所以”str”和ing不会进常量池,进常量池的是string
  • 只有编译器在程序编译期就可以确定值的常量才可以:
    • 基本数据类型( byte、boolean、short、char、int、float、long、double)以及字符串常量。
    • final 修饰的基本数据类型和字符串变量
    • 字符串通过 “+”拼接得到的字符串、基本数据类型之间算数运算(加减乘除)、基本数据类型的位运算(<<、>>、>>> )