Java字符串

140 阅读6分钟

Java中字符串处理主要有这三个类完成:StringStringBuilderStringBuffer


String

String是不可变对象

构造方法

创建字符串常见方式:4种构造方法,1种直接构建

  • String() 初始化一个String对象,表示一个空字符串
  • String(String value) 利用一个直接的字符串来创建一个字符串对象
  • String(char[] value) 利用一个字符数组来创建
  • String(byte[] value) 利用一个字节数组来创建
  • 直接双引号创建 --用双引号来创建的字符串常量属于直接量字符串对象,JVM在处理这类字符串的时候,会进行缓存,产生时放入字符串常量池,当程序需要再次使用的时候,无需重新创建一个新的字符串,而是直接指向已存在的字符串。

常用方法与属性

1. 不可变的原因

private final char value[];
  • 底层是一个字符串数组
  • 这个数组被final关键字修饰,一旦赋值,不可以被改变

不可变的目的

  1. 字符串很常用,而且很大概率会出现重复的现象,为了提升效率和减少内存分配,于是诞生了字符串常量池。得益于不可变,常量池很容易被管理
  2. 考虑安全性。字符串涉及到的场景很多,不可变可以防止被有意无意的篡改(路径、全限定类名)
  3. 作为HashMap、HashTable等hash型数据key的必要。因为不可变的设计,jvm底层很容易在缓存String对象的时候缓存其hashcode,这样在执行效率上会大大提升

2. 字符串长度

public int length() {
    return value.length;
}
  • 因为底层是一个常量数组,所以字符串长度就是获取这个数组的长度

3. 比较

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;
}

public boolean equalsIgnoreCase(String anotherString) {
    return (this == anotherString) ? true
            : (anotherString != null) && (anotherString.value.length == value.length)
            && regionMatches(true, 0, anotherString, 0, value.length);
}
  1. 判断是否为同一个引用,否返回false
  2. 使用instanceof关键字来判断两者是否都是字符串,否返回false
  3. 强转(equals)
  4. 判断长度是否相等,否返回false
  5. 对字符数组逐字符比较

重写equals方法一般步骤

  1. 使用==来检查实参是否为对象的一个引用,如果不是,返回false
  2. 使用isinstanceof来检查实参是否为正确的类型(和我同一个类型),如果不是,返回false
  3. 把实参转换到正确的类型
  4. 检查实参中的属性与当前对象中对应的属性值是否匹配
    1. floatdouble类型的基本数据类型,直接用==来判断
    2. float类型用Float.floatToIntBits转成int类型的值,然后用==来比较
    3. double类型用Double.doubleToIntBits转成int类型的值,然后用==来比较
    4. 引用类型可以递归地调用其equals方法

重写之后考虑三个问题

  1. 是否对称:a.equals(b)的结果和b.equals(a)的结果相同
  2. 是否传递:a.equals(b)=true b.equals(c)=true => a.equals(c)
  3. 是否一致

4. 驻足

public native String intern();

简单来说:用来返回字符串常量池中的某个字符串,如果这个字符串在常量池已存在,则直接返回这个字符串常量的引用;如果不存在字符串常量池,在字符串常量池中加入这个字符串,再返回引用

只有创建的时候直接用双引号创建的字符串才会进入字符串常量池,用构造方法创建的字符串不会进入常量池,这个方法则可以把这个对象(如果不存在常量池)放入到常量池

String s1 = new String("abc");
String s2 = "abc";
System.out.println(s1 == s2);    // 显然是false

s1 = new String("xyz").intern();
s2 = "xyz";
System.out.println(s1 == s2);    // true

5. 比较

public int compareTo(String anotherString);
  1. 把两个字符串进行左对齐,从左到右依次比较
  2. 如果相同则继续比较
  3. 如果不同则比较不同的字符ascii码的差值
  4. 如果都相同则返回两个字符串长度的差值

"hello".compareTo("hel"); // 返回2

6.截取子字符串

//从指定的位置开始截到末尾
public String substring(int beginIndex) {
    return substring(beginIndex, count);
}

//指定截取开始和结束的索引
public String substring(int beginIndex, int endIndex) {
    if (beginIndex < 0) {
        throw new StringIndexOutOfBoundsException(beginIndex);
    }
    if (endIndex > count) {
        throw new StringIndexOutOfBoundsException(endIndex);
    }
    if (beginIndex > endIndex) {
        throw new StringIndexOutOfBoundsException(endIndex - beginIndex);
    }
    return ((beginIndex == 0) && (endIndex == count)) ? this : new String(offset + beginIndex, endIndex - beginIndex, value);
}

7.字符串切割

public String[] split(String regex);
  • 注意参数是一个正则表达式,在切割.的时候需要注意进行转义\\.
  • 如果没有找到切割的字符串则返回一个长度为1的字符串数组,其中的元素就是原始字符串

"hello".split("abc"); //返回["hello"]

编译器优化

String a="helloworld";
String b="hello"+"world";
System.out.println((a==b)); //返回true,因为b在编译期的时候已经被优化成helloworld,因此在运行期a和b指向的是字符串常量池中同一个字符串

String a="helloworld";
String b="hello";
String c=b+"world";
System.out.println((a==c)); //返回flase,因为有符号引用b存在,所以在编译器不会优化

String a="helloworld";
final String b="hello";
String c=b+"world";
System.out.println((a==c)); //返回true,因为被final修饰的对象,在编译器会直接被替换成直接引用(但是如果final修饰的对象是通过方法返回赋值的,就不管用)

StringBuilder

可变字符串,线程不安全,性能较好

构造方法

public StringBuilder();	//默认开辟16个字符长度的空间
public StringBuilder(int capacity);	//开辟capacity个字符长度的空间
public StringBuilder(String str);	//开辟str.length+16个字符长度的空间
public StringBuilder(CharSequence seq);	//开辟seq.length+16个字符长度的空间

常用方法

StringBuilder append(...);   //添加数据,可以添加任意类型(int、float、String)
String toString();  //  转换成String对象

和String互换

  • String -> StringBuilder: StringBuilder stringBuilder = new StringBuilder(str);
  • StringBuilder -> String: String str = stringBuilder.toString();

StringBuffer

可变字符串,线程安全(大多数方法普通方法都加上了synchronized关键字,这样就会有一定的性能消耗)

构造方法

public StringBuffer();	//默认开辟16个字符长度的空间
public StringBuffer(int capacity);	//开辟capacity个字符长度的空间
public StringBuffer(String str);	//开辟str.length+16个字符长度的空间
public StringBuffer(CharSequence seq);	//开辟seq.length+16个字符长度的空间

常用方法

public synchronized int length();	//获取当前字符串长度(内容长度)
public synchronized int capacity();	//获取当前缓冲区长度(数组长度)

StringBuffer stringBuffer = new StringBuffer("hello");
System.out.println(stringBuffer.length());      //5
System.out.println(stringBuffer.capacity());    //5+16(默认长度)=21

StringBuffer没有重写equals方法,所以调用该方法会使用父类Object类的equals方法,比较两个对象的地址值。

String、StringBuilder、StringBuffer三者区别

  • 可变性:StringBuilder和StringBuffer是可变的,String是不可变的。原因:final修饰,一旦赋值不可以修改。目的:String不可变的目的
  • 线程安全性:StringBuffer中的大部分方法都加上了同步锁,所以是线程安全的;StringBuilder和String的方法都没有加锁,是线程安全的。
  • 性能方面:由于StringBuffer加了同步锁,导致性能变差;StringBuilder没有加锁,性能较于StringBuffer来说性能比较好;String每次都会生成新的对象,性能更差一点。StringBuffer和StringBuilder都不会生成新的对象,比String来说效率更高。
  • 使用上来说:操作少量数据:String;单线程操作大量数据:StringBuilder;多线程操作大量数据:StringBuffer