JAVA基础String学习探究

String

String是JAVA中最常用的对象,就是这样一个最常用最普通的对象,当你深入研究时却发现我们并不是真的很了解它,那么让我们一起来学习它吧!

因为String不可变的性质,因此Java内部实现了常量池。当一个String被创建时,会先去常量池查看有没有值相同的示例,有的话直接返回。节省了内存,加快了字符串的加载速度。不可变的对象也可以保证在并发中保持线程安全

特性

  • 字符串常量,实际上也是String对象
  • 所有不是通过new创建的String都是放在常量池中
  • String类型的对象是不可变的
  • String实现了CharSequence接口

String对象创建方式

String str1 = "abcd";
String str2 = new String("abcd");

这两种不同的创建方法是有差别的,第一种方式是在常量池中拿对象,第二种方式是直接在堆内存空间创建一个新的对象。
只要使用new方法,便需要创建新的对象

连接表达式+(加号)

  1. 只有使用引号包含文本的方式创建的String对象之间使用“+”连接产生的新对象才会被加入字符串池中。
  2. 对于所有包含new方式新建对象(包括null)的“+”连接表达式,它所产生的新对象都不会被加入字符串池中
String str1 = "str";
String str2 = "ing";

String str3 = "str" + "ing";
String str4 = str1 + str2;
System.out.println(str3 == str4);//false

String str5 = "string";
System.out.println(str3 == str5);//true
1、 Sting s; //定义了一个变量s,没有创建对象;
2、 =    // 赋值,将某个对象的引用(句柄)赋给s ,没有创建对象;
3、 “abc”    //创建一个对象;
4new String(); // 创建一个对象。

常用方法

  • length 返回字符串长度

  • isEmpty 判断字符串是否为空

  • charAt 根据索引位置获取char

  • getChars 复制对应位置范围的char到数组中

  • equals, equalsIgnoreCase 对比顺序依次为引用地址,char数组长度,char数组内容

  • compareTo 对比字符串大小

  • startsWith, endsWith 判断前后缀

  • hashCode 计算hash值, 公式为s[0]*31^(n-1) + s[1]*31^(n-2) + … + s[n-1]

  • indexOf 查找首次出现的位置

  • lastIndexOf 查找最后出现的位置

  • substring 返回子串(旧版本是返回一个引用在父串的一个新串,节省重新分配内存。但实际如果子串引用了一个占用极大的父串,会因为子串一直被使用导致父串没法被垃圾回收,新版本substring每次重新复制char数组)

  • concat 拼接字符串(拼接char数组,重新创建字符串)

  • replace 用新字符替换所有的旧字符(会先遍历一次char数组,寻找时候存在,再去替换,避免每次都要分配char数组)

  • matches 判断是否符合正则 (复用Pattern.matches()方法)

  • contains 判断是否包含子串(复用indexOf()方法)

  • replaceFirst 只替换一次

  • replaceAll 替换所有正则符合的地方

  • split 按照正则分割字符串

  • toLowerCase 返回小写

  • toUpperCase 返回大写

  • trim 去除前后空格

  • toCharArray 重新复制char数组返回

  • join(CharSequence delimiter, CharSequence… elements)

    String.join(",", "you", "bao", "luo");
    //out: you,bao,luo
  • equals(Object anObject)

String.equals()代码逻辑:

  1. 判断传入的对象与当前对象是否为同一个对象,如果是就直接返回true;

  2. 判断传入的对象是否为String,若不是则返回false(如果为null也不成立);

  3. 判断传入的String与当前String长度是否一致,若不一致则返回false;

  4. 循环对比两个字符串的char[]数组,逐个对比字符是否一致,若不一致则直接返回false;

  5. 循环结束没有找到不匹配的则返回true;

JDK8源码:
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;
}
  • intern():naive方法,直接返回常量池中的引用

当调用intern()方法时,JVM会在常量池中通过equals()方法查找是否存在等值的String,如果存在则直接返回常量池中这个String对象的地址;如果不存在则会创建等值的字符串(即等值的char[]数组字符串,但是char[]是新开辟的一份拷贝空间),然后再返回这个新创建空间的地址;

在常量池查找等值String时,通常不止一个字符串而是多个字符串因此效率会比较低,另外为保证唯一性,需要有锁的介入;

String str1 = "ab";
String str2 = new String("ab");
System.out.println(str1== str2);//false
System.out.println(str2.intern() == str1);//true

System.out.println(str1== str2);//false
str2 = str2.intern();
System.out.println(str1== str2);//true

知识点

  • 在调用x.toString()的地方可以用”“+x替代;
  • 字符串的+拼接操作
public static void main(String[] args) throws InterruptedException {
    String s = "a";
    String st = s + "b" + "c";
  }
javap out====>
Code:
  stack=3, locals=3, args_size=1
     0: ldc           #19                 // String a
     2: astore_1
     3: new           #21                 // class java/lang/StringBuilder
     6: dup
     7: aload_1
     8: invokestatic  #23                 // Method java/lang/String.valueOf:(Ljava/lang/Object;)Ljava/lang/String;
    11: invokespecial #29                 // Method java/lang/StringBuilder."<init>":(Ljava/lang/String;)V
    14: ldc           #32                 // String b
    16: invokevirtual #34                 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
    19: ldc           #38                 // String c
    21: invokevirtual #34                 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
    24: invokevirtual #40                 // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
    27: astore_2
    28: return
  • StringBuffer是线程安全操作
  public synchronized StringBuffer append(String str) {
    toStringCache = null;
    super.append(str);
    return this;
  }
  • StringBuilder非线程安全

    public StringBuilder append(String str) {
    super.append(str);
    return this;
    }
System.err.println("hello,world"); ##hello,world实际是String对象

printf格式化输出

这里写图片描述

FAQ
1. String str1 = “abc”; System.out.println(str1 == “abc”);
步骤:
a> 栈中开辟一块空间存放引用str1;
b> String池中开辟一块空间,存放String常量”abc”;
c> 引用str1指向池中String常量”abc”;
d> str1所指代的地址即常量”abc”所在地址,输出为true;

  1. String str2 = new String(“abc”); System.out.println(str2 == “abc”);
    步骤:
    a> 栈中开辟一块空间存放引用str2;
    b> 堆中开辟一块空间存放一个新建的String对象”abc”;
    c> 引用str2指向堆中的新建的String对象”abc”;
    d> str2所指代的对象地址为堆中地址,而常量”abc”地址在池中,输出为false;
    注意:对于通过new产生的对象,会先去常量池检查有没有 “abc”,如果没有,先在常量池创建一个 “abc” 对象,然后在堆中创建一个常量池中此 “abc” 对象的拷贝对象;

  2. String s2 = new String(“Hello”); 产生几个对象?
    首先,在jvm的工作过程中,会创建一片的内存空间专门存入string对象。我们把这片内存空间叫做string池;
    String s2 = new String(“Hello”);jvm首先在string池内里面看找不找到字符串”Hello”,如果找到不做任何事情;否则创建新的string对象,放到string池里面。由于遇到了new,还会在内存Heap上(不是string池里面)创建string对象存储”Hello”,并将内存上的(不是string池内的)string对象返回给s2。
    Re: 如果常量池中原来没有“Hello”, 则创建两个对象。如果原来的常量池中存在“Hello”时,就是一个对象;

  3. 其它

String str1 = "a"String str2 = "b"String str3 = str1 + "b"//str1 和 str2 是字符串常量,所以在编译期就确定了。  
//str3 中有个 str1 是引用,所以不会在编译期确定。  
//又因为String是 final 类型的,所以在 str1 + "b" 的时候实际上是创建了一个新的对象,在把新对象的引用传给str3

final String str1 = "a"String str2 = "b"String str3 = str1 + "b"//这里和\(3\)的不同就是给 str1 加上了一个final,这样str1就变成了一个常量。  
//这样 str3 就可以在编译期中就确定了  

编译期优化

编译器在编译期会针对字符串常量叠加得到固定值,字符串常量包括”hello”或用fianl修饰的变量,编译器认为这些常量是不可变的

编译器优化String常量连接

示例一

String str = "hello" + "java" + 1;
// 编译期编译器会直接编译为"hellojava1"
#2 = String             #21            // hellojava1
#21 = Utf8               hellojava1

示例二

public static final String A = "ab"; // 常量A
public static final String B = "cd"; // 常量B
public static void main(String[] args) {
     String s = A + B;  // 将两个常量用+连接对s进行初始化
     String t = "abcd";   
    if (s == t) {   
         System.out.println("s等于t,它们是同一个对象");   
     } else {   
         System.out.println("s不等于t,它们不是同一个对象");   
     }   
 }
output ==> s等于t,它们是同一个对象

说明:A和B都是常量,值是固定的,因此s的值也是固定的,它在类被编译时就已经确定了。也就是说:String s=A+B; 等同于:String s=”ab”+”cd”;

示例三

public static final String A; // 常量A
public static final String B;    // 常量B
static {   
     A = "ab";   
     B = "cd";   
 }   
 public static void main(String[] args) {   
    // 将两个常量用+连接对s进行初始化   
     String s = A + B;   
     String t = "abcd";   
    if (s == t) {   
         System.out.println("s等于t,它们是同一个对象");   
     } else {   
         System.out.println("s不等于t,它们不是同一个对象");   
     }   
 }
output ==> s不等于t,它们不是同一个对象

A和B虽然被定义为常量,但是它们都没有马上被赋值。在运算出s的值之前,他们何时被赋值,以及被赋予什么样的值,都是个变数。因此A和B在被赋值之前,性质类似于一个变量。那么s就不能在编译期被确定,而只能在运行时被创建了

循环内String加操作

  • 性能较低的代码:
public void  implicitUseStringBuilder(String[] values) {
  String result = "";
  for (int i = 0 ; i < values.length; i ++) {
      result += values[i];
  }
  System.out.println(result);
}

编译后的字节码:

public void implicitUseStringBuilder(java.lang.String[]);
  Code:
     0: ldc           #11                 // String
     2: astore_2
     3: iconst_0
     4: istore_3
     5: iload_3
     6: aload_1
     7: arraylength
     8: if_icmpge     38
    11: new           #5                  // class java/lang/StringBuilder
    14: dup
    15: invokespecial #6                  // Method java/lang/StringBuilder."<init>":()V
    18: aload_2
    19: invokevirtual #7                  // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
    22: aload_1
    23: iload_3
    24: aaload
    25: invokevirtual #7                  // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
    28: invokevirtual #8                  // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
    31: astore_2
    32: iinc          3, 1
    35: goto          5
    38: getstatic     #9                  // Field java/lang/System.out:Ljava/io/PrintStream;
    41: aload_2
    42: invokevirtual #10                 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
    45: return

其中8: if_icmpge 38 和35: goto 5构成了一个循环;

8: if_icmpge 38的意思是如果(i < values.length的相反结果)成立,则跳到第38行(System.out)。

35: goto 5则表示直接跳到第5行。

但是这里面有一个很重要的就是StringBuilder对象创建发生在循环之间,也就是意味着有多少次循环会创建多少个StringBuilder对象,这样明显性能较低

  • 性能较高的代码
public void explicitUseStringBuider(String[] values) {
  StringBuilder result = new StringBuilder();
  for (int i = 0; i < values.length; i ++) {
      result.append(values[i]);
  }
}
public void explicitUseStringBuider(java.lang.String[]);
  Code:
     0: new           #5                  // class java/lang/StringBuilder
     3: dup
     4: invokespecial #6                  // Method java/lang/StringBuilder."<init>":()V
     7: astore_2
     8: iconst_0
     9: istore_3
    10: iload_3
    11: aload_1
    12: arraylength
    13: if_icmpge     30
    16: aload_2
    17: aload_1
    18: iload_3
    19: aaload
    20: invokevirtual #7                  // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
    23: pop
    24: iinc          3, 1
    27: goto          10
    30: return

从上面可以看出,13: if_icmpge 3027: goto 10构成了一个loop循环,而0: new #5位于循环之外,所以不会多次创建StringBuilder.

注意:循环体中需要尽量避免隐式或者显式创建StringBuilder

不可变的String

String对象是不可变的。 String类中每一个看起来会修改String值的方法,实际上都是创建了一个全新的String对象,以包含修改后的字符串内容

String str1 = "java";
String str2 = "java";
System.out.println\("str1=str2   " + \(str1 == str2\)\);

在代码中,可以创建同一个String对象的多个别名,而它们所指的对象是相同的,一直待在一个单一的物理位置上

重载“+”

在Java中,唯一被重载的运算符就是用于String的“+”与“+=”。除此之外,Java不允许程序员重载其他的运算符

public class StringTest {
    String a = "abc";
    String b = "mongo";
    String info = a + b + 47;
}

String对象是不可变的,所以在上述的代码过程中可能会是这样工作的:

  1. “abc” + “mongo”创建新的String对象abcmongo;
  2. “abcmongo” + “47”创建新的String对象abcmongo47;
  3. 引用info 指向最终生成的String;

但是这种方式会生成一大堆需要垃圾回收的中间对象,性能相当糟糕

编译器的优化处理

Compiled from "StringTest.java"
public class StringTest {
  java.lang.String a;

  java.lang.String b;

  java.lang.String info;

  public StringTest();
    Code:
       0: aload_0
       1: invokespecial #12                 // Method java/lang/Object."<init>":
()V
       4: aload_0
       5: ldc           #14                 // String abc
       7: putfield      #16                 // Field a:Ljava/lang/String;
      10: aload_0
      11: ldc           #18                 // String mongo
      13: putfield      #20                 // Field b:Ljava/lang/String;
      16: aload_0
      17: new           #22                 // class java/lang/StringBuilder
      20: dup
      21: aload_0
      22: getfield      #16                 // Field a:Ljava/lang/String;
      25: invokestatic  #24                 // Method java/lang/String.valueOf:(
Ljava/lang/Object;)Ljava/lang/String;
      28: invokespecial #30                 // Method java/lang/StringBuilder."<
init>":(Ljava/lang/String;)V
      31: aload_0
      32: getfield      #20                 // Field b:Ljava/lang/String;
      35: invokevirtual #33                 // Method java/lang/StringBuilder.ap
pend:(Ljava/lang/String;)Ljava/lang/StringBuilder;
      38: bipush        47
      40: invokevirtual #37                 // Method java/lang/StringBuilder.ap
pend:(I)Ljava/lang/StringBuilder;
      43: invokevirtual #40                 // Method java/lang/StringBuilder.to
String:()Ljava/lang/String;
      46: putfield      #44                 // Field info:Ljava/lang/String;
      49: return
}

反编译以上代码会发现,编译器自动引入了StringBuilder类。
编译器创建了一个StringBuilder对象,并调用StringBuilder.append()方法,最后调用toString()生成结果,从而避免中间对象的性能损耗

字符串常量池的设计思想

  • 字符串的分配,和其他的对象分配一样,耗费高昂的时间与空间代价,作为最基础的数据类型,大量频繁的创建字符串,极大程度地影响程序的性能
  • JVM为了提高性能和减少内存开销,在实例化字符串常量的时候进行了一些优化
    • 为字符串开辟一个字符串常量池,类似于缓存区
    • 创建字符串常量时,首先坚持字符串常量池是否存在该字符串
    • 存在该字符串,返回引用实例,不存在,实例化该字符串并放入池中
  • 实现的基础
    • 实现该优化的基础是因为字符串是不可变的,可以不用担心数据冲突进行共享
    • 运行时实例创建的全局字符串常量池中有一个表,总是为池中每个唯一的字符串对象维护一个引用,这就意味着它们一直引用着字符串常量池中的对象,所以,在常量池中的这些字符串不会被垃圾收集器回收
  • 常量池的好处:常量池是为了避免频繁的创建和销毁对象而影响系统性能,其实现了对象的共享。例如字符串常量池,在编译阶段就把所有的字符串文字放到一个常量池中;
    • 节省内存空间:常量池中所有相同的字符串常量被合并,只占用一个空间;
    • 节省运行时间:比较字符串时,==比equals()快。对于两个引用变量,只用==判断引用是否相等,也就可以判断实际值是否相等

字符串常量池存储位置

字符串常量池则存在于方法区

String str1 = “abc”;
String str2 = “abc”;
String str3 = “abc”;
String str4 = new String(“abc”);
String str5 = new String(“abc”);

这里写图片描述

参考资料
http://rednaxelafx.iteye.com/blog/774673

猜你喜欢

转载自blog.csdn.net/luoyoub/article/details/80190793