String类详解

String

String类是引用数据类型,类全名:java.lang.String

String类被final修饰,无法继承,另外String类实现了Serializable接口,表示String类是支持序列化的。另外还实现了Comparable接口,表示String对象是可比较的。还实现了CharSequence接口

public final class String
    implements java.io.Serializable, Comparable<String>, CharSequence,
               Constable, ConstantDesc {

    /**
     * The value is used for character storage.
     *
     * @implNote This field is trusted by the VM, and is a subject to
     * constant folding if String instance is constant. Overwriting this
     * field after construction will cause problems.
     *
     * Additionally, it is marked with {@link Stable} to trust the contents
     * of the array. No other facility in JDK provides this functionality (yet).
     * {@link Stable} is safe here, because value is never null.
     */
    @Stable
    private final byte[] value;

说明字符串底层实际上是一个字符数组;数组的特点是一旦确定长度不可变,并且value被final修饰,说明value不能重新指向新的char数组对象,说明字符串一旦创建长度不可变


Java中字符串使用效率较高,如果每一次都去堆内存中寻址再开辟空间效率太低,所以在Java语言中通过双引号创建的字符串都会在字符串常量池中存储一份,以后如果使用该字符串,会直接从字符串常量池中取出,这时一种提高程序执行效率的缓存机制

public class StringConstructor {
    public static void main(String[] args) {
        String s1 = "hello";
        String s2 = "hello";//s1和s2指向了字符串中的同一个对象,其值是相同的
    }
}

程序执行到第三行的时候,检测到双引号括起来的hello字符串,此时会去字符串常量池中查找,如果没有找到就会创建hello对象,并将hello存储在字符串常量池中; 在程序执行到第六行的时候,检测到hello字符串,同样会去字符串常量池中查找。由于一个字符串可能会被多个引用指向,为了保证数据的安全性,字符串被设计为不可变的。

java8之前字符串常量池存储在方法区中(java8之后方法区也没有了,叫做metaspace元空间),Java8之后把字符串常量池挪到了堆内存当中

创建了几个对象

public class StringConstructor {
    public static void main(String[] args) {
        String s1 = new String("hello");
        String s2 = new String("hello");
    }
}

使用new运算符必然导致堆内存当中开辟新的存储空间,所以以上程序创建了三个对象,堆内存中两个String对象,字符串常量池中一个hello对象。(实际上三个对象都在堆内存中)

字符串拼接

+ 两边都是常量

分析:

public class StringAppend {
    public static void main(String[] args) {
        String s = "abc" + "def";
    }
}

大家可能此处会创建三个字符串常量;但对以上class文件进行反编译:

//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by FernFlower decompiler)
//

public class StringAppend {
    public StringAppend() {
    }

    public static void main(String[] args) {
        String s = "abcdef";
    }
}

可以看到第11行直接就是 “abcdef”,没有"abc",“def”;说明"abc" + "def"是在编译阶段进行了字符串拼接


+ 两边有变量

public class StringAppend {
    public static void main(String[] args) {
        String s1 = "abc";
        String s2 = s1 + "def";
    }
}

反编译结果是:

public class StringAppend {
    public StringAppend() {
    }

    public static void main(String[] args) {
        String s1 = "abc";
        String s2 = s1 + "def";
    }
}

可以看出,“abc”,"def"都是有的,只是没有”abcdef“;这是因为在拼接的时候new了一个StringBuilder对象,通过StringBuilder对象的append方法进行了字符串拼接

不要频繁使用 + 进行字符串拼接

  • 如果两边都是字符串常量,可以使用加号进行拼接,因为这是编译阶段进行拼接
  • 如果加号两边有任意一个变量,必然会导致底层new一个StringBuffer对象进行字符串拼接

如果循环拼接的话,每循环一次都要new一个StringBuilder对象,所以效率极低,例如以下代码:

public class StringAppend {
     public static void main(String[] args) {
        long begin = System.currentTimeMillis();
        String s = "";
        for (int i = 0; i < 10_000; i++) {
            s = s + i;
        }
        long end = System.currentTimeMillis();
        System.out.println(end - begin);
    }
}	//执行了47ms

在短时间内就会new出来一万个StringBuilder对象,效率较低;建议在外部手动创建一个StringBuilder对象,然后在for循环中进行append拼接:

public class StringAppend {
    public static void main(String[] args) {
        long begin = System.currentTimeMillis();
        StringBuilder s = new StringBuilder("");
        for (int i = 0; i < 10_000; i++) {
            s.append(i);
        }
        long end = System.currentTimeMillis();
        System.out.println(end - begin);
    }
}  //执行了0ms

String类的equals方法

== 比较的是引用保存的内存地址,String类的equals经过重写,比较的是对象的内容

使用的时候建议用字符串常量调用equals方法,可以避免空指针异常

String类构造方法

  • String(original)
String s = "";
String s = new String();
  • 将byte数组转换为字符串
String s = new String(byte数组);
String s = new String(byte数组,起始下标,长度);
  • 将char数组转换为字符串
String s = new String(char数组);
String s = new String(char数组,起始下标,长度);

将byte数组转换为字符串

    byte[] bytes = {97,98,99};//97 a   98 b   99 c
    String s1 = new String(bytes);
    String s2 = new String(bytes,1,2);//byte数组,起始下标,长度

    System.out.println(s1); //abc
    System.out.println(s2);	//bc

会把byte数组转换为字符串输出

将char数组转换为字符串

        char[] chars = {'a','b','c','d','e','f'};
        String s3 = new String(chars);
        String s4 = new String(chars,1,4);

        System.out.println(s3);//abcdef
        System.out.println(s4);//bcde

String类常用方法

public char charAt(int index)

返回指定索引index处的字符

String s = "abc";
System.out.println(s.charAt(2));//c

public int compareTo(String anotherString)

按照字典顺序比较两个字符串大小,相等返回0;结果小于零表示:a < b;结果大于零表示:a > b;

System.out.println("abc".compareTo("abc"));
System.out.println("def".compareTo("dev"));//-16

public String concat(String str)

拼接字符串,注意参数只能是String类型,如果是null会出现空指针异常

System.out.println("a".concat("b")); //ab

public boolean contains(CharSequence s)

判断是否含有子字符串

System.out.println("helloWorld.java".contains("World"));//true

public boolean endsWith(String suffix)

判断是否以指定后缀结尾

System.out.println("helloWorld.java".endsWith(".java"));

public boolean equalsIgnoreCase(String anotherString)

忽略大小写判断字符串

System.out.println("helloWorld".equalsIgnoreCase("HELLOWORLD"));

public byte[] getBytes()

将字符串转换为byte数组

System.out.println(Arrays.toString("abcd".getBytes()));//[97, 98, 99, 100]

public int indexOf(String str)

获取str子字符串在当前字符串中第一次出现的索引

System.out.println("helloWorld".indexOf("lo"));

public int indexOf(String str, int fromIndex)

从fromIndex下标开始,获取str子字符串在当前字符串中第一次出现处的索引值

System.out.println("helloWorld".indexOf("lo",2));

public boolean isEmpty()

判断是否为空字符串

System.out.println("".isEmpty());
System.out.println(" ".isEmpty());//false

public int lastIndexOf(String str)

获取str子字符串在当前字符串中最后一次出现处的索引

System.out.println("helloWorld".lastIndexOf("l"));

public int lastIndexOf(String str, int fromIndex)

从fromIndex下标开始,获取str子字符串在当前字符串中最后一次出现处的索引值

System.out.println("helloWorld".lastIndexOf("l",3));

public int length()

获取字符串长度

System.out.println("java".length());

public String replace(CharSequence target, CharSequence replacement)

使用指定的 字面值替换序列replacement 替换当前字符串中所有 匹配字面值目标序列target 的子字符串

System.out.println("c++ c++ c++".replace("c++","java"));
System.out.println("http://www.baidu.com".replace("http://","https://"));

public String[] split(String regex)

将当前字符串以某个特定符号进行拆分,返回String[] 数组

System.out.println(Arrays.toString("1980-1-1".split("-"))); //[1980, 1, 1]

public boolean startsWith(String prefix)

判断当前字符串对象是否以某个子字符串开头

System.out.println("helloWorld".startsWith("hello"));

public String substring(int beginIndex)

从beginIndex开始截取字符串,返回截取到的字符串

System.out.println("helloWorld".substring(5));

public String substring(int beginIndex, int endIndex)

从beginIndex开始,到endIndex结束(不包含endIndex)

System.out.println("helloWorld".substring(5,"helloWorld".length()));

public char[] toCharArray()

将字符串转换为char[] 数组

System.out.println(Arrays.toString("helloWorld".toCharArray()));//[h, e, l, l, o, W, o, r, l, d]

public String toUpperCase()

转换大写

System.out.println("helloworld".toUpperCase());

public String toLowerCase()

转换小写

System.out.println("HELLOWORLD".toLowerCase());

public String trim()

去除前后的空白(中间不能去除)

System.out.println(" hello ".trim());

public static String valueOf(Object obj)

这是String类中唯一的静态方法,不需要字符串对象就能调用,作用是把非字符串的内容转换为字符串

System.out.println(String.valueOf(new Object()));//java.lang.Object@776ec8df

可以看出,参数是一个对象的时候会自动调用该对象的toString方法:

Object a1 = new Object();
System.out.println(a1);

println方法:

public void println(Object x) {
    String s = String.valueOf(x);//println调用valueOf
    if (getClass() == PrintStream.class) {
        // need to apply String.valueOf again since first invocation
        // might return null
        writeln(String.valueOf(s));
    } else {
        synchronized (this) {
            print(s);
            newLine();
        }
    }
}

valueOf:

public static String valueOf(Object obj) {
    return (obj == null) ? "null" : obj.toString();//valueOf调用了toString方法
}

所以说在控制台上输出的任何内容都是字符串

StringBuilder StringBuffer

java中的字符串是不可变的,每一次采用 + 进行String拼接,会占用大量方法区(堆内存)的内存,造成内存空间的浪费

StringBuffer buffer = new StringBuffer();

创建一个初始化容量为16的字符串缓冲区。

以StringBuilder为例:

public final class StringBuilder
    extends AbstractStringBuilder
    implements java.io.Serializable, Comparable<StringBuilder>, CharSequence
{

    /** use serialVersionUID for interoperability */
    @Serial
    static final long serialVersionUID = 4383685877147921099L;
        /**
     * Constructs a string builder with no characters in it and an
     * initial capacity of 16 characters.
     */
    @IntrinsicCandidate
    public StringBuilder() {
        super(16);//调用父类AbstractStringBuilder的构造方法,参数是16
    }

向上查找父类 AbstractStringBuilder:

abstract class AbstractStringBuilder implements Appendable, CharSequence {
    /**
     * The value is used for character storage.
     */
    byte[] value;//16传到这个参数的构造方法中的capacity

    /**
     * The id of the encoding used to encode the bytes in {@code value}.
     */
    byte coder;

说明StringBuilder的默认初始化容量是16

可变长字符串:

StringBuffer底层是一个byte[] value数组,如果进行字符串追加 本质上是将原来的数组通过ArrayCopy到一个新的数组中,原来的数组就被垃圾回收器回收了,再让value指向新的数组(String的value被final修饰了,不能指向新对象)

同时,append方法底层追加满了之后会自动扩容,不会产生新的对象,之前的对象都被垃圾回收期回收了

需要频繁拼接使用: java.lang.StringBuffer java.lang.StringBuilder

面试题

为什么String长度不可变?

因为String类中有一个final修饰的byte数组,而且数组的长度一旦确定就不可变;并且因为final的修饰使得数组引用value无法指向其他对象;StringBuffer/StringBuilder内部实际上是一个byte数组,但是这个数组没有被final修饰,StringBuffer的初始化容量是16,存满之后会自动调用System.arrayCopy()进行扩容

public class StringBuilderTest {
    public static void main(String[] args) {
        String s = "abc";
        s = "xyz";
    }
}

这样的操作是可行的:因为字符串不可变是指双引号中的内容不可变,也就是byte数组中的内容不可变;而s变量是可以指向其他的对象的;第四行代码新建了一个byte数组,把新的value值传递给s(String s 没有被final修饰)

append方法剖析

public StringBuilder append(String str) {
    super.append(str);
    return this;
}

向上查找父类append:

public AbstractStringBuilder append(String str) {
    if (str == null) {
        return appendNull();
    }
    int len = str.length();
    ensureCapacityInternal(count + len);//第二步
    putStringAt(count, str);//第三步
    count += len;//第四步
    return this;
}

这个方法的思路是:

第一步:第2行:如果被拼接的字符串是null,直接 return appendNull(); 这个方法的源码是:

private AbstractStringBuilder appendNull() {
    ensureCapacityInternal(count + 4);
    int count = this.count;
    byte[] val = this.value;
    if (isLatin1()) {
        val[count++] = 'n';
        val[count++] = 'u';
        val[count++] = 'l';
        val[count++] = 'l';
    } else {
        count = StringUTF16.putCharsAt(val, count, 'n', 'u', 'l', 'l');
    }
    this.count = count;
    return this;
}

这个方法中在第二行给char数组扩容了四个长度,然后在数组中添加了四个字符: n u l l

说明如果追加的字符串是null的话,会在原有字符串后面追加null;例如:lisanull

第二步:当str不为null时:append方法第五行:先获取str的长度,然后进入ensureCapacityInternal()方法,这个方法的源码如下:

private void ensureCapacityInternal(int minimumCapacity) {
    // overflow-conscious code
    if (minimumCapacity - value.length > 0) {
        expandCapacity(minimumCapacity);
    }
}

再进入expandCapacity方法:

void expandCapacity(int minimumCapacity) {
    int newCapacity = value.length * 2 + 2; // 原容量*2+2 作为备选新的容量
    if (newCapacity - minimumCapacity < 0) 
        newCapacity = minimumCapacity;
    if (newCapacity < 0) {
        if (minimumCapacity < 0) // overflow
            throw new OutOfMemoryError();
        newCapacity = Integer.MAX_VALUE;
    }
    value = Arrays.copyOf(value, newCapacity);
}

可以看到,新容量是原容量的2倍 + 2,这就是StringBuilder的扩容规则;得到新数组的长度之后value = Arrays.copyOf(value, newCapacity) ;copyOf返回了一个新的数组对象,也就是说把原数组中的数据拷贝到新数组当中,原数组会被GC管理回收掉

第三步:putStringAt方法:

private void putStringAt(int index, String str) {
    putStringAt(index, str, 0, str.length());
}

将str字符串中的字符拷贝到value数组之后进行追加

第四步:StringBuilder中的字符长度count加上被添加字符的长度,然后返回当前对象this。

所以说:StringBuilder底层扩容实际上就是对数组进行扩容,数组的扩容实际上就是数组拷贝,而数组拷贝效率较低;为了提高效率,建议在初始化对象的时候对字符串长度进行预估,给定合适的初始化容量,这样可以减少数组的扩容,提高程序执行效率。

StringBuilder StringBuffer的区别

StringBuffer的源码:

@Override
public synchronized int length() {
    return count;
}

@Override
public synchronized int capacity() {
    return super.capacity();
}

synchronized关键字表示StringBuffer是线程安全的,而StringBuilder是非线程安全的;虽然前者线程安全,但是效率较低;所以一般采用StringBuilder进行字符串拼接,线程安全会选择其他的策略来实现。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值