一文读懂字符串String

小伙伴儿们,如果觉得文章干货满满,欢迎加入公众号【编程识堂】,更多干货等你们来哦!String类的值是保存在value数组中的,并且是被private final修饰的。

小伙伴儿们,如果觉得文章干货满满,欢迎加入公众号【编程识堂】,更多干货等你们来哦!

前言

String在java中特别常用,但小伙伴儿们对String真的彻底了解了吗?今天跟着小堂我一起盘它、弄懂它。

为什么说字符串是不可变的

在工作中,我们经常要在代码中对字符串进行赋值和改变他的值,但是,为什么我们说字符串是不可变的呢?

源码解析

public final class String
implements java.io.Serializable, Comparable<String>, CharSequence {
    /** The value is used for character storage. */
    private final char value[];

     /**
     * Initializes a newly created {@code String} object so that it represents
     * an empty character sequence.  Note that use of this constructor is
     * unnecessary since Strings are immutable.
     */
    public String() {
        this.value = "".value;
    }

    /**
     * Initializes a newly created {@code String} object so that it represents
     * the same sequence of characters as the argument; in other words, the
     * newly created string is a copy of the argument string. Unless an
     * explicit copy of {@code original} is needed, use of this constructor is
     * unnecessary since Strings are immutable.
     *
     * @param  original
     *         A {@code String}
     */
    public String(String original) {
        this.value = original.value;
        this.hash = original.hash;
    }
}

String类的值是保存在value数组中的,并且是被private final修饰的

  1. private修饰,表明外部的类是访问不到value的,同时⼦类也访问不到,当然String类不可能有⼦类,因为类被final修饰了。
  2. final修饰,表明value的引⽤是不会被改变的,⽽value只会在String的构造函数中被初始化,⽽且并没有其他⽅法可以修改value数组中的值,保证了value的引⽤和值都不会发⽣变化。

final关键字的作⽤有如下⼏种:

  1. final修饰类时,表明这个类不能被继承
  2. final修饰⽅法,表明⽅法不能被重写
  3. final修饰变量,如果是基本数据类型的变量,则其数值⼀旦在初始化之后便不能改变;如果是对象类型的变量,只能保证它的引⽤不变,但对象的内容是可以改变的

Java中数组也是对象,数组即使被final修饰,内容还是可以改变的。
所以我们说String类是不可变的。⽽很多⽅法,如substring并不是在原来的String类上进⾏操作,⽽是⽣成了新的String类

public String substring(int beginIndex) {
 if (beginIndex < 0) {
 throw new StringIndexOutOfBoundsException(beginIndex);
 }
 int subLen = value.length - beginIndex;
 if (subLen < 0) {
 throw new StringIndexOutOfBoundsException(subLen);
 }
 return (beginIndex == 0) ? this : new String(value, beginIndex, subLen);
}

什么是不可变对象

首先,我们需要知道什么是不可变对象?

不可变对象是在完全创建后其内部状态保持不变的对象。这意味着,一旦对象被赋值给变量,我们既不能更新引用,也不能通过任何方式改变内部状态

原因

可是有人会有疑惑,String为什么不可变,我的代码中经常改变String的值啊,如下:

String s = "abcd";
s = s.concat("ef");

这样,操作,不就将原本的”abcd”的字符串改变成”abcdef”了么?

但是,虽然字符串内容看上去从”abcd”变成了”abcdef”,但是实际上,我们得到的已经是一个新的字符串了。

一文读懂字符串String

如上图,在堆中重新创建了一个”abcdef”字符串,和”abcd”并不是同一个对象

所以,一旦一个string对象在内存(堆)中被创建出来,他就无法被修改。而且,String类的所有方法都没有改变字符串本身的值,都是返回了一个新的对象

如果我们想要一个可修改的字符串,可以选择StringBuffer 或者 StringBuilder这两个代替String。

为什么String要设计成不可变

在知道了”String是不可变”的之后,大家是不是一定都很疑惑:为什么要把String设计成不可变的呢?有什么好处呢?

这个问题,困扰过很多人,甚至有人直接问过Java的创始人James Gosling。

在一次采访中James Gosling被问到什么时候应该使用不可变变量,他给出的回答是:

I would use an immutable whenever I can.

那么,他给出这个答案背后的原因是什么呢?是基于哪些思考的呢?

其实,主要是从缓存、安全性、线程安全和性能等角度触发的

Q:缓存、安全性、线程安全和性能?这有都是啥
A:你别急,听我一个一个给你讲就好了。

缓存

字符串是使用最广泛的数据结构。大量的字符串的创建是非常耗费资源的,所以,Java提供了对字符串的缓存功能,可以大大的节省堆空间。

JVM中专门开辟了一部分空间来存储Java字符串,那就是字符串常量池

通过字符串常量池,两个内容相同的字符串变量,可以从池中指向同一个字符串对象,从而节省了关键的内存资源

String s = "abcd";
String s2 = s;

对于这个例子,s和s2都表示”abcd”,所以他们会指向字符串池中的同一个字符串对象:

一文读懂字符串String

但是,之所以可以这么做,主要是因为字符串的不变性。试想一下,如果字符串是可变的,我们一旦修改了s的内容,那必然导致s2的内容也被动的改变了,这显然不是我们想看到的。

面试的时候我们经常被问到:

String str1 = "abc";
String str2 = "abc";
String str3 = new String("abc");
String str4 = new String("abc");
// true
System.out.println(str1 == str2);
// false
System.out.println(str1 == str3);
// false
System.out.println(str3 == str4);

内存中的结构如下:

一文读懂字符串String

其中常量池中存的是引⽤

解释⼀下上⾯代码的输出,Java中有2种创建字符串对象的⽅式

String str1 = "abc"; 
String str2 = "abc"; 
// true 
System.out.println(str1 == str2); 

采⽤字⾯值的⽅式创建⼀个字符串时,JVM⾸先会去字符串池中查找是否存在”abc”这个对象的引⽤

如果不存在,则在堆中创建”abc”这个对象,并将其引⽤添加到字符串常量池(实际上是将引⽤放到哈希

表中),随后将引⽤赋给str1

如果存在,则不创建任何对象,直接将池中”abc”对象的引⽤返回,赋给str2。因为str1、str2指向同⼀

个对象,所以结果为true。

String str3 = new String("abc"); 
String str4 = new String("abc"); 
// false 
System.out.println(str3 == str4); 

采⽤new关键字新建⼀个字符串对象时,JVM⾸先在字符串池中查找有没有”abc”这个字符串对象的引⽤,

如果没有,则先在堆中创建⼀个”abc”字符串对象,并将引⽤添加到字符串常量池,随后将引⽤赋给str3。

如果有,则不往池中放”abc”对象的引⽤,直接在堆中创建⼀个”abc”字符串对象,然后将引⽤赋给 str4。这样,str4就指向了堆中创建的这个”abc”字符串对象;

因为str3和str4指向的是不同的字符串对象,结果为false。

安全性

字符串在Java应用程序中广泛用于存储敏感信息,如用户名、密码、连接url、网络连接等。JVM类加载器在加载类的时也广泛地使用它。

因此,保护String类对于提升整个应用程序的安全性至关重要。

当我们在程序中传递一个字符串的时候,如果这个字符串的内容是不可变的,那么我们就可以相信这个字符串中的内容。

但是,如果是可变的,那么这个字符串内容就可能随时都被修改。那么这个字符串内容就完全不可信了。这样整个系统就没有安全性可言了。

线程安全

不可变会自动使字符串成为线程安全的,因为当从多个线程访问它们时,它们不会被更改。

因此,一般来说,不可变对象可以在同时运行的多个线程之间共享。它们也是线程安全的,因为如果线程更改了值,那么将在字符串池中创建一个新的字符串,而不是修改相同的值,不会发生竞争条件,也不需要进行额外的同步操作。因此,字符串对于多线程来说是安全的。

hashcode缓存

由于字符串对象被广泛地用作数据结构,它们也被广泛地用于哈希实现,如HashMap、HashTable、HashSet等。在对这些散列实现进行操作时,经常调用hashCode()方法。

不可变性保证了字符串的值不会改变。因此,hashCode()方法在String类中被重写,以方便缓存,这样在第一次hashCode()调用期间计算和缓存散列,并从那时起返回相同的值。

在String类中,有以下代码:

private int hash;//this is used to cache hash code.

性能

前面提到了的字符串池、hashcode缓存等,都是提升性能的提现

因为字符串不可变,所以可以用字符串池缓存,可以大大节省堆内存。而且还可以提前对hashcode进行缓存,更加高效

由于字符串是应用最广泛的数据结构,提高字符串的性能对提高整个应用程序的总体性能有相当大的影响。

字符串拼接

字符串拼接是我们在Java代码中比较经常要做的事情,就是把多个字符串拼接到一起。

我们都知道,String是Java中一个不可变的类,所以他一旦被实例化就无法被修改。但是,既然字符串是不可变的,那么字符串拼接又是怎么回事呢?

字符串不变性与字符串拼接

其实,所有的所谓字符串拼接,都是重新生成了一个新的字符串。下面一段字符串拼接代码

String s = "abcd";
s = s.concat("ef");

其实我们最后得到的是一个新字符串,如下图:

一文读懂字符串String

s是保存的是一个重新创建出来的String对象的引用。

那么我们在工作过程中会遇到很多种拼接方式,小伙伴儿们,平时是怎么使用的呢?今天跟着小堂来一起回顾下比较常用的方式。

使用+拼接字符串

演示

拼接字符串最简单的方式就是直接使用+号拼接,如:

String wechat = "编程识堂";
String introduce = "每日更新Java相关技术文章,关注我不迷路";
String bcst = wechat + "," + introduce;
System.out.println(bcst);
//结果为:编程识堂,每日更新Java相关技术文章,关注我不迷路

原理

通过反编译成字节码后我们发现,主要是通过StringBuilder的append方法实现的。

一文读懂字符串String

concat

演示

使用String类中的concat方法来拼接字符串。如:

String wechat = "编程识堂";
String introduce = "每日更新Java相关技术文章,关注我不迷路";
System.out.println(wechat.concat(introduce));

原理

源码

wechat.concat(introduce);
/**
str="每日更新Java相关技术文章,关注我不迷路"; otherLen=21
this:wechat =  "编程识堂";
value: "编程识堂"  length:4;

**/
public String concat(String str) {
        if (str.isEmpty()) {
            return this;
        }
        int len = value.length;
        int otherLen = str.length();
        char buf[] = Arrays.copyOf(value, len + otherLen);
        str.getChars(buf, len);
        return new String(buf, true);
    }

/**
 Arrays.copyOf 方法 
 original:{编,程,识,堂} 源数组
 newLength: 25
**/
 public static char[] copyOf(char[] original, int newLength) {
        char[] copy = new char[newLength];
      /**
      original:{编,程,识,堂} 源数组
      srcPos:0 从源数组中0位置开始复制元素到目标数组中
      copy:{,,......,,,} length=25 目标数组
      destPos:0 目标数组中从0位置开始存储源数组的中的元素
      Math.min(original.length4, newLength) 要复制的元素个数 original.length=4个
      **/
        System.arraycopy(original, 0, copy, 0,
                         Math.min(original.length, newLength));
        /**
        copy:{'编','程','识','堂','\u0000',......,'\u0000'} length=25
        **/
        return copy;
 }

 /**    str.getChars方法
       dst[] 目标数组:{'编','程','识','堂','\u0000',......,'\u0000'} length=25
       dstBegin: 4 目标数组中从4位置开始存储源数组的中的元素
     * Copy characters from this string into dst starting at dstBegin.
     * This method doesn't perform any range checking.
     */
 void getChars(char dst[], int dstBegin) {
     /**
       value:源数组:{'每','日','更','新','J','a','v','a','相','关','技','术',
       '文','章',',','关','注','我','不','迷','路'}
       value.length:21
      **/
      System.arraycopy(value, 0, dst, dstBegin, value.length);
 }
一文读懂字符串String

一文读懂字符串String

一文读懂字符串String

一文读懂字符串String

一文读懂字符串String

System.arraycopy

System.arraycopy是Java语言中一个用于数组复制的方法。它可以将一个数组的部分或全部元素复制到另一个数组中。

方法签名: public static void arraycopy(Object src, int srcPos, Object dest, int destPos, int length)

参数解释:

  • src:源数组
  • srcPos:源数组中复制元素的起始位置
  • dest:目标数组
  • destPos:目标数组中复制元素的起始位置
  • length:要复制的元素个数

使用示例:

int[] src = {1, 2, 3, 4, 5}; 
int[] dest = new int[5]; 
System.arraycopy(src, 0, dest, 0, 5);
//结果 dest: {1, 2, 3, 4, 5}

上述代码将src数组中的所有元素复制到dest数组中,两个数组中的元素值相同。

StringBuffer和StringBuilder

演示

关于字符串,Java中除了定义了一个可以用来定义字符串常量的String类以外,还提供了可以用来定义字符串变量StringBuffer和StringBuilder类,它的对象是可以扩充和修改的。

使用StringBuffer可以方便的对字符串进行拼接。如:

 StringBuffer sb = new StringBuffer("编程识堂");
 sb.append("每日更新Java相关技术文章,关注我不迷路");
 System.out.println(sb.toString());
//编程识堂每日更新Java相关技术文章,关注我不迷路

原理

一文读懂字符串String

根据类图,我们知道StringBuffer和StringBuilder都继承⾃AbstractStringBuilder类。

源码

AbstractStringBuilder

abstract class AbstractStringBuilder implements Appendable, CharSequence {
    /**
     * The value is used for character storage.
     */
    char[] value;

    /**
     * The count is the number of characters used.
    与String不同,字符数组中不一定所有位置都已经被使用,它有一个实例变量,表示数组中
   已经使用的字符个数,定义如下    
     */
    int count;

    /**
     * This no-arg constructor is necessary for serialization of subclasses.
     */
    AbstractStringBuilder() {
    }


   public AbstractStringBuilder append(String str) {
        if (str == null)
            return appendNull();
        int len = str.length();
        //⾃动扩容机制
        ensureCapacityInternal(count + len);
        str.getChars(0, len, value, count);
        count += len;
        return this;
    }
}

看到区别了吗?value数组没有⽤private和final修饰,说明了StringBuffer和StringBuilder是可变的。

String不同,字符数组中不一定所有位置都已经被使用,它有一个实例变量,表示数组中已经使用的字符个数(count)。

抽象类AbstractStringBuilder内部提供了⼀个⾃动扩容机制,当发现⻓度不够的时候,会⾃动进⾏

扩容⼯作(具体扩容可以看源码,很容易理解),会创建⼀个新的数组,并将原来数组的数据复制

到新数组,不会创建新对象,拼接字符串的效率⾼.

StringBuilder

public final class StringBuilder extends AbstractStringBuilder implements java.io.Serializable, CharSequence{
  public StringBuilder append(String str) {
        super.append(str);
        return this;
  }

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

StringBuffer

 public final class StringBuffer extends AbstractStringBuilder
    implements java.io.Serializable, CharSequence{
  public StringBuffer(String str) {
        super(str.length() + 16);
        append(str);
  }
  public synchronized StringBuffer append(String str) {
        toStringCache = null;
        super.append(str);
        return this;
 }
} 

StringBufferStringBuilder类似,最大的区别就是StringBuffer是线程安全的

String、 StringBuffer 、StringBuilder的区别

1.都是final类,不允许被继承
2. String⻓度是不可变的,StringBuffer,StringBuilder⻓度是可变的
3. StringBuffer是线程安全的,StringBuilder不是线程安全的。但它们⽅法实现类似,StringBuffer在
⽅法之上添加了synchronized修饰,保证线程安全
4. StringBuilder⽐StringBuffer拥有更好的性能
5. 如果⼀个String类型的字符串,在编译时可以确定是⼀个字符串常量,则编译完成之后,字符串会
⾃动拼接成⼀个常量,此时String的速度⽐StringBuffer和StringBuilder的性能好的多

用例子解释下第五条:

 public static void main(String[] args) {
        String a = "a";
        String b = "b";
        String c = a + b;
        String d = "a" + "b" + "c";
    }
一文读懂字符串String

看String d ,理解了吧?编译后直接为abc.

同时看string c的拼接过程,先⽣成⼀个StringBuilder对象,再调⽤2次append⽅法,最后再返回⼀个

String对象,知道String⽐StringBuilder慢的原因了吧。

那么也就是说,Java中的+对字符串的拼接,其实现原理是使用StringBuilder.append。

但是,String的使用+字符串拼接也不全都是基于StringBuilder.append,还有种特殊情况,那就是如果是两个固定的字面量拼接,如:

String d = “a” + “b” + “c”;

编译器会进行常量折叠(因为两个都是编译期常量,编译期可知),直接变成 String d = “abc”

StringUtils.join

除了JDK中内置的字符串拼接方法,还可以使用一些开源类库中提供的字符串拼接方法名,如apache.commons中提供的StringUtils类,其中的join方法可以拼接字符串。

 String a = "a";
 String b = "b";
 String result = StringUtils.join(a, ",", b);
System.out.println(result);//a,b

这里简单说一下,StringUtils中提供的join方法,最主要的功能是:将数组或集合以某拼接符拼接到一起形成新的字符串,如:

 public static void main(String[] args) {
        List<String> list = Arrays.asList("编程识堂","每日更新Java相关技术文章,关注我不迷路");
        System.out.println( StringUtils.join(list,","));
     //编程识堂,每日更新Java相关技术文章,关注我不迷路
    }

原理

public static String join(Object[] array, char separator, int startIndex, int endIndex) {
        if (array == null) {
            return null;
        } else {
            int noOfItems = endIndex - startIndex;
            if (noOfItems <= 0) {
                return "";
            } else {
                StringBuilder buf = new StringBuilder(noOfItems * 16);

                for(int i = startIndex; i < endIndex; ++i) {
                    if (i > startIndex) {
                        buf.append(separator);
                    }

                    if (array[i] != null) {
                        buf.append(array[i]);
                    }
                }

                return buf.toString();
            }
        }
    }

过查看StringUtils.join的源代码,我们可以发现,其实他也是通过StringBuilder来实现的

StringJoiner

StringJoiner是java.util包中的一个类用于构造一个由分隔符分隔的字符序列(可选),并且可以从提供的前缀开始并以提供的后缀结尾。虽然这也可以在StringBuilder类的帮助下在每个字符串之后附加分隔符,但StringJoiner提供了简单的方法来实现,而无需编写大量代码。

StringJoiner类共有2个构造函数,5个公有方法。其中最常用的方法就是add方法和toString方法,类似于StringBuilder中的append方法和toString方法。

演示

 public static void main(String[] args) {
        List<String> list = Arrays.asList("编程识堂","每日更新Java相关技术文章,关注我不迷路");
        StringJoiner stringJoiner = new StringJoiner(":","[","]");
        stringJoiner.add("编程识堂").add("每日更新Java相关技术文章,关注我不迷路");
        System.out.println( stringJoiner.toString());//[编程识堂:每日更新Java相关技术文章,关注我不迷路]

        String result = list.stream().collect(Collectors.joining("-", "{", "}"));
        System.out.println(result);//{编程识堂-每日更新Java相关技术文章,关注我不迷路}
    }

原理

主要看一下add方法:

 public StringJoiner(CharSequence delimiter,
                        CharSequence prefix,
                        CharSequence suffix) {
        Objects.requireNonNull(prefix, "The prefix must not be null");
        Objects.requireNonNull(delimiter, "The delimiter must not be null");
        Objects.requireNonNull(suffix, "The suffix must not be null");
        // make defensive copies of arguments
        this.prefix = prefix.toString();
        this.delimiter = delimiter.toString();
        this.suffix = suffix.toString();
        this.emptyValue = this.prefix + this.suffix;
    }

  public StringJoiner add(CharSequence newElement) {
        prepareBuilder().append(newElement);
        return this;
    }

  private StringBuilder prepareBuilder() {
        if (value != null) {
            value.append(delimiter);
        } else {
            value = new StringBuilder().append(prefix);
        }
        return value;
    }

看到了一个熟悉的身影——StringBuilder ,没错,StringJoiner其实就是依赖StringBuilder实现的。

当我们发现StringJoiner其实是通过StringBuilder实现之后,我们大概就可以猜到,他的性能损耗应该和直接使用StringBuilder差不多!

为什么要用StringJoiner

在了解了StringJoiner的用法和原理后,可能很多读者就会产生一个疑问,明明已经有一个StringBuilder了,为什么Java 8中还要定义一个StringJoiner呢?到底有什么好处呢?

如果读者足够了解Java 8的话,或许可以猜出个大概,这肯定和Stream有关。

 public static Collector<CharSequence, ?, String> joining(CharSequence delimiter,
                                                             CharSequence prefix,
                                                             CharSequence suffix) {
        return new CollectorImpl<>(
                () -> new StringJoiner(delimiter, prefix, suffix),
                StringJoiner::add, StringJoiner::merge,
                StringJoiner::toString, CH_NOID);
    }

我们通过Collectors.joining源码发现是通过StringJoiner实现的

总结

如果日常开发中中,需要进行字符串拼接,如何选择?

1、如果只是简单的字符串拼接,考虑直接使用”+”即可。

2、如果是在for循环中进行字符串拼接,考虑使用StringBuilder和StringBuffer。

3、如果是通过一个List进行字符串拼接,则考虑使用StringJoiner。

StringJoiner其实是通过StringBuilder实现的,所以他的性能和StringBuilder差不多,他也是非线程安全的。

字符串有没有长度限制

想要搞清楚这个问题,首先我们需要翻阅一下String的源码,看下其中是否有关于长度的限制或者定义。

String类中有很多重载的构造函数,其中有几个是支持用户传入length来执行长度的:

   public String(byte bytes[], int offset, int length) {
        checkBounds(bytes, offset, length);
        this.value = StringCoding.decode(bytes, offset, length);
    }

可以看到,这里面的参数length是使用int类型定义的,那么也就是说,String定义的时候,最大支持的长度就是int的最大范围值。

根据Integer类的定义,java.lang.Integer#MAX_VALUE的最大值是2^31 – 1;

那么,我们是不是就可以认为String能支持的最大长度就是这个值了呢?

其实并不是,这个值只是在运行期,我们构造String的时候可以支持的一个最大长度,而实际上,在编译期,定义字符串的时候也是有长度限制的

如以下代码:

String s = “11111…1111″;//其中有10万个字符”1”

当我们使用如上形式定义一个字符串的时候,当我们执行javac编译时,是会抛出异常的,提示如下:

错误: 常量字符串过长

那么,明明String的构造函数指定的长度是可以支持2147483647(2^31 – 1)的,为什么像以上形式定义的时候无法编译呢?

其实,形如String s = “xxx”;定义String的时候,xxx被我们称之为字面量,这种字面量在编译之后会以常量的形式进入到Class常量池

那么问题就来了,因为要进入常量池,就要遵守常量池的有关规定。

所以字符串有长度限制,在编译期,要求字符串常量池中的常量不能超过65535,并且在javac执行过程中控制了最大值为65534。

在运行期,长度不能超过Int的范围,否则会抛异常。

免责声明:本站所有文章内容,图片,视频等均是来源于用户投稿和互联网及文摘转载整编而成,不代表本站观点,不承担相关法律责任。其著作权各归其原作者或其出版社所有。如发现本站有涉嫌抄袭侵权/违法违规的内容,侵犯到您的权益,请在线联系站长,一经查实,本站将立刻删除。 本文来自网络,若有侵权,请联系删除,如若转载,请注明出处:https://yundeesoft.com/91452.html

(0)

相关推荐

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注

关注微信