String源码分析


Strings are constant; their values cannot be changed after they are created. String buffers support mutable strings.Because String objects are immutable they can be shared.

String 字符串是常量,其值在实例创建后就不能被修改,但字符串缓冲区(StringBuffer)支持可变的字符串,由于String对象是不可变的,因此可以共享它们。

String 的结构

源码分析

包名

package java.lang

定义

我们常常听人说,HashMap 的 key 建议使用不可变类,比如说 String 这种不可变类。这里说的不可变指的是类值一旦被初始化,就不能再被改变了,如果被修改,将会是新的类,比如有这样的例子: String s = “hello”; s = “world”,看到这里也许会有人疑惑,String 初始化以后好像可以被修改。其实这里的赋值并不是对 s内容的修改,而是将s指向了新的字符串。从 debug 的日志来看,其实已经把 s 的引用指向了新的 String,如下图所示:

image-20200619184230724

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

可以看出 String 是 final 类型的,表示该类不能被其他类继承,同时该类实现了三个接口Serializable, Comparable, CharSequence

属性

/** The value is used for character storage. */
      // 字符存储的值
    private final char value[];

    /** Cache the hash code for the string */
      // 缓存字符串的哈希码
    private int hash; // Default to 0

    /** use serialVersionUID from JDK 1.0.2 for interoperability */
        // 序列化 serialVersionUID
    private static final long serialVersionUID = -6849794470754667710L;
 /**
     * Class String is special cased within the Serialization Stream Protocol.
     */
    private static final ObjectStreamField[] serialPersistentFields =
        new ObjectStreamField[0];

String 是基于字符数组 char[] 实现的。

String 中保存数据的是一个 char 的数组 value。我们发现 value 也是被 final 修饰的,也就是说 value 一旦被赋值,内存地址是绝对无法修改的,而且 value 的权限是 private 的,外部绝对访问不到,String 也没有开放出可以对 value 进行赋值的方法,所以说 value 一旦产生,内存地址就根本无法被修改。

因为 String 实现了 Serializable 接口,所以支持序列化和反序列化支持。Java 的序列化机制是通过在运行时判断类的 serialVersionUID 来验证版本一致性的。在进行反序列化时,JVM 会把传来的字节流中的 serialVersionUID 与本地相应实体(类)的 serialVersionUID 进行比较,如果相同就认为是一致的,可以进行反序列化,否则就会出现序列化版本不一致的异常 (InvalidCastException)。

因为 String 具有不变性,所以 String 的大多数操作方法,都会返回新的 String,如下面这种写法是不对的:

String str ="hello world !!";
// 这种写法是替换不掉的,必须接受 replace 方法返回的参数才行,这样才行:str = str.replace("l","dd");
str.replace("l","dd");

构造方法

1、无参构造器

        //初始化一个新创建的{@code String}对象,使其代表空字符序列。 请注意,使用此构造函数是不必要,因为字符串是不可变的。
    public String() {
        this.value = "".value;
    }

因此不建议像如下方式创建String对象,会产生不必要的对象。

String str = new String();
  str = "hello";

2、带参构造器

字符串构造
    // 新创建的字符串是参数字符串的副本。此构造函数的使用是不必要,因为字符串是不可变的。
    public String(String original) {
        this.value = original.value;
        this.hash = original.hash;
    }
字符数组构造
    public String(char value[]) {
        this.value = Arrays.copyOf(value, value.length);
    }

    public String(char value[], int offset, int count) {
        if (offset < 0) {
            throw new StringIndexOutOfBoundsException(offset);
        }
        if (count <= 0) {
            if (count < 0) {
                throw new StringIndexOutOfBoundsException(count);
            }
            if (offset <= value.length) {
                this.value = "".value;
                return;
            }
        }
        // Note: offset or count might be near -1>>>1.
        if (offset > value.length - count) {
            throw new StringIndexOutOfBoundsException(offset + count);
        }
        this.value = Arrays.copyOfRange(value, offset, offset+count);
    }

使用字符数组创建 String 的时候,会用到Arrays.copyOf方法Arrays.copyOfRange方法。这两个方法是将原有的字符数组中的内容逐一的复制到 String 中的字符数组中。这将会创建一个新的字符串对象,随后修改的字符数组不影响新创建的字符串。

字节数组构造

byte 是网络传输或存储的序列化形式,所以在很多传输和存储的过程中需要将 byte[] 数组和 String 进行相互转化。所以 String 提供了一系列重载的构造方法来将一个字节数组转化成 String,提到 byte[] 和 String 之间的相互转换就不得不关注编码问题。

使用字节数组来构造 String 有很多种形式,按照是否指定解码方式分的话可以分为两种:

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

如果我们在使用 byte[] 构造 String 的时候,使用的构造方法中带有 charsetName 或者 charset参数的话,那么就会使用 StringCoding.decode 方法进行解码,使用的解码的字符集就是我们指定的 charsetName或者 charset

public String(byte bytes[], Charset charset) {
        this(bytes, 0, bytes.length, charset);
    }
public String(byte bytes[], String charsetName)
            throws UnsupportedEncodingException {
        this(bytes, 0, bytes.length, charsetName);
    }
public String(byte bytes[], int offset, int length, Charset charset) {
        if (charset == null)
            throw new NullPointerException("charset");
        checkBounds(bytes, offset, length);
        this.value =  StringCoding.decode(charset, bytes, offset, length);
    } 
public String(byte bytes[], int offset, int length, String charsetName)
            throws UnsupportedEncodingException {
        if (charsetName == null)
            throw new NullPointerException("charsetName");
        checkBounds(bytes, offset, length);
        this.value = StringCoding.decode(charsetName, bytes, offset, length);
    }

在使用 byte[] 构造 String 的时候,如果没有指明解码使用的字符集的话,那么 StringCoding 的 decode 方法首先调用系统的默认编码格式,如果没有指定编码格式则默认使用 ISO-8859-1 编码格式进行编码操作。代码如下:

static char[] decode(byte[] ba, int off, int len) {
        String csn = Charset.defaultCharset().name();
        try {
            // use charset name decode() variant which provides caching.
            return decode(csn, ba, off, len);
        } catch (UnsupportedEncodingException x) {
            warnUnsupportedCharset(csn);
        }
        try {
            return decode("ISO-8859-1", ba, off, len);
        } catch (UnsupportedEncodingException x) {
            // If this code is hit during VM initialization, MessageUtils is
            // the only way we will be able to get any kind of error message.
            MessageUtils.err("ISO-8859-1 charset not available: "
                             + x.toString());
            // If we can not find ISO-8859-1 (a required encoding) then things
            // are seriously wrong with the installation.
            System.exit(1);
            return null;
        }
    }
使用 StringBuffer 和 StringBuilder 构造
public String(StringBuffer buffer) {
        synchronized(buffer) {
            this.value = Arrays.copyOf(buffer.getValue(), buffer.length());
        }
  public String(StringBuilder builder) {
        this.value = Arrays.copyOf(builder.getValue(), builder.length());
    }

这两个构造方法是很少用到的,因为当我们有了 StringBuffer 或者 StringBuilfer 对象之后可以直接使用他们的 toString 方法来得到 String。

关于效率问题,使用StringBuilder 的 toString 方法会更快一些,原因是 StringBuffer 的 toString 方法是 synchronized 的,在牺牲了效率的情况下保证了线程安全。

一个特殊的保护类型的构造方法
/*
    * Package private constructor which shares value array for speed.
    * this constructor is always expected to be called with share==true.
    * a separate constructor is needed because we already have a public
    * String(char[]) constructor that makes a copy of the given char[].
    */
        // 封装私有构造函数,该结构共享值数组以提高速度。 始终希望使用share == true调用此构造方法。 需要一个单独的构造函数,因为我们已经有一个公共String(char [])构造函数,该构造函数复制给定char []
    String(char[] value, boolean share) {
        // assert share : "unshared not supported";
        this.value = value;
    }

该方法和 String(char[] value)有两点区别:

  • 该方法多了一个参数。其实这个参数在方法体中根本没被使用。加入这个 share 的只是为了区分于String(char[] value)方法,不加这个参数就没办法定义这个函数,只有参数是不同才能进行重载。

  • 具体的方法实现不同。 String(char[] value)方法在创建 String 的时候会用到 Arrays.copyOf方法将 value 中的内容逐一复制到 String 当中,而这个 String(char[] value, boolean share)方法则是直接将 value 的引用赋值给 String 的 value。也就是说,这个方法构造出来的 String 和参数传过来的 char[] value 共享同一个数组。

为什么 Java 会提供这样一个方法呢?

  • 性能好:这个很简单,一个是直接给数组赋值(相当于直接将 String 的 value 的指针指向char[]数组),一个是逐一拷贝,当然是直接赋值快了。

  • 节约内存:该方法之所以设置为 protected,是因为一旦该方法设置为公有,在外面可以访问的话,如果构造方法没有对 arr 进行拷贝,那么其他人就可以在字符串外部修改该数组,由于它们引用的是同一个数组,因此对 arr 的修改就相当于修改了字符串,那就破坏了字符串的不可变性。

  • 安全的:对于调用他的方法来说,由于无论是原字符串还是新字符串,其 value 数组本身都是 String 对象的私有属性,从外部是无法访问的,因此对两个字符串来说都很安全。

    参考链接:https://www.jianshu.com/p/799c4459b808

在 Java 7 之前有很多 String 里面的方法都使用上面说的那种“性能好的、节约内存的、安全”的构造函数。比如:substring replace concat valueOf 等方法,但是在 Java 7 中,substring 已经不再使用这种“优秀”的方法了

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

虽然这种方法有很多优点,但是他有一个致命的缺点,那就是很有可能造成内存泄露

看一个例子,假设一个方法从某个地方(文件、数据库或网络)取得了一个很长的字符串,然后对其进行解析并提取其中的一小段内容,这种情况经常发生在网页抓取或进行日志分析的时候。

String aLongString = "...averylongstring...";
String aPart = data.substring(20, 40);
return aPart;

在这里 aLongString 只是临时的,真正有用的是 aPart,其长度只有 20 个字符,但是它的内部数组却是从 aLongString 那里共享的,因此虽然 aLongString 本身可以被回收,但它的内部数组却不能释放。这就导致了内存泄漏。如果一个程序中这种情况经常发生有可能会导致严重的后果,如内存溢出,或性能下降。新的实现虽然损失了性能,而且浪费了一些存储空间,但却保证了字符串的内部数组可以和字符串对象一起被回收,从而防止发生内存泄漏,因此新的 substring 比原来的更健壮。

其他方法

常用的简单方法
// 返回字符串长度
public int length() {
        return value.length;
    }
// 返回字符串是否为空
public boolean isEmpty() {
        return value.length == 0;
    }
// 返回字符串中第index个字符(数组索引)
 public char charAt(int index) {
        if ((index < 0) || (index >= value.length)) {
            throw new StringIndexOutOfBoundsException(index);
        }
        return value[index];
    }
// 转换为字符数组
public char[] toCharArray() {
        // Cannot use Arrays.copyOf because of class initialization order issues
        char result[] = new char[value.length];
        System.arraycopy(value, 0, result, 0, value.length);
        return result;
    }
// 去掉两端空格
public String trim() {
        int len = value.length;
        int st = 0;
        char[] val = value;    /* avoid getfield opcode */

        while ((st < len) && (val[st] <= ' ')) {
            st++;
        }
        while ((st < len) && (val[len - 1] <= ' ')) {
            len--;
        }
        return ((st > 0) || (len < value.length)) ? substring(st, len) : this;
    }
// 转化为大写
public String toUpperCase() {
        return toUpperCase(Locale.getDefault());
    }
// 转化为小写
public String toLowerCase() {
        return toLowerCase(Locale.getDefault());
    }

如果我们的项目被 Spring 托管的话,有时候我们会通过 applicationContext.getBean(className); 这种方式得到 SpringBean,这时 className 必须是要满足首字母小写的,除了该场景,在反射场景下面,我们也经常要使类属性的首字母小写,这时候我们一般都会这么做:

name.substring(0, 1).toLowerCase() + name.substring(1);,使用 substring 方法,该方法主要是为了截取字符串连续的一部分,substring 有两个方法:

  1. public String substring(int beginIndex, int endIndex) beginIndex:开始位置,endIndex:结束位置;
  2. public String substring(int beginIndex)beginIndex:开始位置,结束位置为文本末尾。

substring 方法的底层使用的是字符数组范围截取的方法 :Arrays.copyOfRange(字符数组, 开始位置, 结束位置); 从字符数组中进行一段范围的拷贝。

相反的,如果要修改成首字母大写,只需要修改成 name.substring(0, 1).toUpperCase() + name.substring(1) 即可。

拼接替换字符串方法
// 拼接字符串
public String concat(String str) {
        int otherLen = str.length();
        if (otherLen == 0) {
            return this;
        }
        int len = value.length;
        char buf[] = Arrays.copyOf(value, len + otherLen);
        str.getChars(buf, len);
        return new String(buf, true);
    }
// 将字符串中的oldChar 字符换成 newChar 字符
public String replace(char oldChar, char newChar) {
        if (oldChar != newChar) {
            int len = value.length;
            int i = -1;
            char[] val = value; /* avoid getfield opcode */

            while (++i < len) {
                if (val[i] == oldChar) {
                    break;
                }
            }
            if (i < len) {
                char buf[] = new char[len];
                for (int j = 0; j < i; j++) {
                    buf[j] = val[j];
                }
                while (i < len) {
                    char c = val[i];
                    buf[i] = (c == oldChar) ? newChar : c;
                    i++;
                }
                return new String(buf, true);
            }
        }
        return this;
    }

以上两个方法都使用了 String(char[] value, boolean share) 方法,他们不会导致元数组中有大量空间不被使用,因为他们一个是拼接字符串,一个是替换字符串内容,不会将字符数组的长度变得很短,所以使用了共享的 char[] 字符数组来优化。

public String replaceFirst(String regex, String replacement) {
        return Pattern.compile(regex).matcher(this).replaceFirst(replacement);
    }
public String replaceAll(String regex, String replacement) {
        return Pattern.compile(regex).matcher(this).replaceAll(replacement);
    }
public String replace(CharSequence target, CharSequence replacement) {
        return Pattern.compile(target.toString(), Pattern.LITERAL).matcher(
                this).replaceAll(Matcher.quoteReplacement(replacement.toString()));
    }

replace 的参数是 char 和 CharSequence,即可以支持字符的替换, 也支持字符串的替换,需要注意的是, replace 并不只是替换一个,是替换所有匹配到的字符或字符串哦。

replaceAll 和 replaceFirst 的参数是 regex,即基于规则表达式的替换。

当然我们想要删除某些字符,也可以使用 replace 方法,把想删除的字符替换成 “” 即可。

正则表达匹配方法
// 判断字符串是否匹配给定的regex正则表达式
public boolean matches(String regex) {
        return Pattern.matches(regex, this);
    }
// 判断字符串是否包含字符序列 s
public boolean contains(CharSequence s) {
        return indexOf(s.toString()) > -1;
    }
// 按照字符 regex将字符串分成 limit 份
public String[] split(String regex, int limit) {
        /* fastpath if the regex is a
         (1)one-char String and this character is not one of the
            RegEx's meta characters ".$|()[{^?*+\\", or
         (2)two-char String and the first char is the backslash and
            the second is not the ascii digit or ascii letter.
         */
        char ch = 0;
        if (((regex.value.length == 1 &&
             ".$|()[{^?*+\\".indexOf(ch = regex.charAt(0)) == -1) ||
             (regex.length() == 2 &&
              regex.charAt(0) == '\\' &&
              (((ch = regex.charAt(1))-'0')|('9'-ch)) < 0 &&
              ((ch-'a')|('z'-ch)) < 0 &&
              ((ch-'A')|('Z'-ch)) < 0)) &&
            (ch < Character.MIN_HIGH_SURROGATE ||
             ch > Character.MAX_LOW_SURROGATE))
        {
            int off = 0;
            int next = 0;
            boolean limited = limit > 0;
            ArrayList<String> list = new ArrayList<>();
            while ((next = indexOf(ch, off)) != -1) {
                if (!limited || list.size() < limit - 1) {
                    list.add(substring(off, next));
                    off = next + 1;
                } else {    // last one
                    //assert (list.size() == limit - 1);
                    list.add(substring(off, value.length));
                    off = value.length;
                    break;
                }
            }
            // If no match was found, return this
            if (off == 0)
                return new String[]{this};

            // Add remaining segment
            if (!limited || list.size() < limit)
                list.add(substring(off, value.length));

            // Construct result
            int resultSize = list.size();
            if (limit == 0) {
                while (resultSize > 0 && list.get(resultSize - 1).length() == 0) {
                    resultSize--;
                }
            }
            String[] result = new String[resultSize];
            return list.subList(0, resultSize).toArray(result);
        }
        return Pattern.compile(regex).split(this, limit);
    }
// 按照字符 regex 将字符串分段
public String[] split(String regex) {
        return split(regex, 0);

如上边源码所示,拆分使用 split 方法,该方法有两个入参数。第一个参数是我们拆分的标准字符,第二个参数是一个 int 值,叫 limit,来限制我们需要拆分成几个元素。如果 limit 比实际能拆分的个数小,按照 limit 的个数进行拆分,我们演示一个 demo:

String s ="boo:and:foo";
// 我们对 s 进行了各种拆分,演示的代码和结果是:
s.split(":") 结果:["boo","and","foo"]
s.split(":",2) 结果:["boo","and:foo"]
s.split(":",5) 结果:["boo","and","foo"]
s.split(":",-2) 结果:["boo","and","foo"]
s.split("o") 结果:["b","",":and:f"]
s.split("o",2) 结果:["b","o:and:foo"]

从演示的结果来看,limit 对拆分的结果,是具有限制作用的,还有就是拆分结果里面不会出现被拆分的字段。

那如果字符串里面有一些空值呢,拆分的结果如下:

String a =",a,,b,";
a.split(",") 结果:["","a","","b"]

从拆分结果中,我们可以看到,空值是拆分不掉的,仍然成为结果数组的一员,如果我们想删除空值,只能自己拿到结果后再做操作,但 Guava(Google 开源的技术工具) 提供了一些可靠的工具类,可以帮助我们快速去掉空值,如下:

String a =",a, ,  b  c ,";
// Splitter 是 Guava 提供的 API 
List<String> list = Splitter.on(',')
    .trimResults()// 去掉空格
    .omitEmptyStrings()// 去掉空值
    .splitToList(a);
log.info("Guava 去掉空格的分割方法:{}",JSON.toJSONString(list));
// 打印出的结果为:
["a","b  c"]

从打印的结果中,可以看到去掉了空格和空值,这正是我们工作中常常期望的结果,所以推荐使用 Guava 的 API 对字符串进行分割。

合并我们使用 join 方法,此方法是静态的,我们可以直接使用。方法有两个入参,参数一是合并的分隔符,参数二是合并的数据源,数据源支持数组和 List,在使用的时候,我们发现有两个不太方便的地方:

  1. 不支持依次 join 多个字符串,比如我们想依次 join 字符串 s 和 s1,如果你这么写的话 String.join(",",s).join(",",s1) 最后得到的是 s1 的值,第一次 join 的值被第二次 join 覆盖了;
  2. 如果 join 的是一个 List,无法自动过滤掉 null 值。

而 Guava 正好提供了 API,解决上述问题,我们来演示一下:

// 依次 join 多个字符串,Joiner 是 Guava 提供的 API
Joiner joiner = Joiner.on(",").skipNulls();
String result = joiner.join("hello",null,"china");
log.info("依次 join 多个字符串:{}",result);

List<String> list = Lists.newArrayList(new String[]{"hello","china",null});
log.info("自动删除 list 中空值:{}",joiner.join(list));
// 输出的结果为;
依次 join 多个字符串:hello,china
自动删除 list 中空值:hello,china

从结果中,我们可以看到 Guava 不仅仅支持多个字符串的合并,还帮助我们去掉了 List 中的空值,这就是我们在工作中常常需要得到的结果。

字符串转换成字节数组
public byte[] getBytes(Charset charset) {
        if (charset == null) throw new NullPointerException();
        return StringCoding.encode(charset, value, 0, value.length);
    }
public byte[] getBytes() {
        return StringCoding.encode(value, 0, value.length);
    }

在生活中,我们经常碰到这样的场景,进行二进制转化操作时,本地测试的都没有问题,到其它环境机器上时,有时会出现字符串乱码的情况,这个主要是因为在二进制转化操作时,并没有强制规定文件编码,而不同的环境默认的文件编码不一致导致的。

我们通过一个 demo 来模仿一下字符串乱码:

String str  ="nihao 你好 喬亂";
// 字符串转化成 byte 数组
byte[] bytes = str.getBytes("ISO-8859-1");
// byte 数组转化成字符串
String s2 = new String(bytes);
log.info(s2);
// 结果打印为:
nihao ?? ??

打印的结果为??,这就是常见的乱码表现形式。这时候有同学说,是不是我把代码修改成 String s2 = new String(bytes,"ISO-8859-1"); 就可以了?这是不行的。主要是因为 ISO-8859-1 这种编码对中文的支持有限,导致中文会显示乱码。唯一的解决办法,就是在所有需要用到编码的地方,都统一使用 UTF-8,对于 String 来说,getBytes 和 new String 两个方法都会使用到编码,我们把这两处的编码替换成 UTF-8 后,打印出的结果就正常了。

比较方法

我们判断相等有两种办法,equals 和 equalsIgnoreCase。后者判断相等时,会忽略大小写,如果让你写判断两个 String 相等的逻辑,应该如何写,我们来一起看下 equals 的源码,整理一下思路:

// 比较对象
public boolean equals(Object anObject) {
              // 判断内存地址是否相同
        if (this == anObject) {
            return true;
        }
              // 待比较的对象是否是 String,如果不是 String,直接返回不相等
        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 int compareToIgnoreCase(String str) {
        return CASE_INSENSITIVE_ORDER.compare(this, str);
    }

从 equals 的源码可以看出,逻辑非常清晰,完全是根据 String 底层的结构来编写出相等的代码。这也提供了一种思路给我们:如果有人问如何判断两者是否相等时,我们可以从两者的底层结构出发,这样可以迅速想到一种贴合实际的思路和方法,就像 String 底层的数据结构是 char 的数组一样,判断相等时,就挨个比较 char 数组中的字符是否相等即可。

// 与StringBuffer对象比较
public boolean contentEquals(StringBuffer sb) {
        return contentEquals((CharSequence)sb);
    }
// 与CharSequence对象比较
public boolean contentEquals(CharSequence cs) {
        // Argument is a StringBuffer, StringBuilder
        if (cs instanceof AbstractStringBuilder) {
            if (cs instanceof StringBuffer) {
                synchronized(cs) {
                   return nonSyncContentEquals((AbstractStringBuilder)cs);
                }
            } else {
                return nonSyncContentEquals((AbstractStringBuilder)cs);
            }
        }
  private boolean nonSyncContentEquals(AbstractStringBuilder sb) {
        char v1[] = value;
        char v2[] = sb.getValue();
        int n = v1.length;
        if (n != sb.length()) {
            return false;
        }
        for (int i = 0; i < n; i++) {
            if (v1[i] != v2[i]) {
                return false;
            }
        }
        return true;
    }
        // Argument is a String
        if (cs instanceof String) {
            return equals(cs);
        }
        // Argument is a generic CharSequence
        char v1[] = value;
        int n = v1.length;
        if (n != cs.length()) {
            return false;
        }
        for (int i = 0; i < n; i++) {
            if (v1[i] != cs.charAt(i)) {
                return false;
            }
        }
        return true;
    }
// 忽略大小写比较字符串对象
public boolean equalsIgnoreCase(String anotherString) {
        return (this == anotherString) ? true
                : (anotherString != null)
                && (anotherString.value.length == value.length)
                && regionMatches(true, 0, anotherString, 0, value.length);
    }
// 比较字符串
public int compareTo(String anotherString) {
        int len1 = value.length;
        int len2 = anotherString.value.length;
        int lim = Math.min(len1, len2);
        char v1[] = value;
        char v2[] = anotherString.value;

        int k = 0;
        while (k < lim) {
            char c1 = v1[k];
            char c2 = v2[k];
            if (c1 != c2) {
                return c1 - c2;
            }
            k++;
        }
        return len1 - len2;
    }
hashCode方法
public int hashCode() {
        int h = hash;
        if (h == 0 && value.length > 0) {
            char val[] = value;

            for (int i = 0; i < value.length; i++) {
                h = 31 * h + val[i];
            }
            hash = h;
        }
        return h;
    }

hashCode 可以保证相同的字符串的 hash 值肯定相同,但是 hash 值相同并不一定是 value 值就相同。

valueOf()的多种形式的重载方法
public static String valueOf(boolean b) {
        return b ? "true" : "false";
    }
public static String valueOf(char c) {
        char data[] = {c};
        return new String(data, true);
    }
 public static String valueOf(int i) {
        return Integer.toString(i);
    }
public static String valueOf(long l) {
        return Long.toString(l);
    }
public static String valueOf(float f) {
        return Float.toString(f);
    }
public static String valueOf(double d) {
        return Double.toString(d);
    }

上述代码为将六种基本数据类型的变量转换成 String 类型。

intern()方法
public native String intern();

该方法返回一个字符串对象的内部化引用。 String 类维护一个初始为空的字符串的对象池,当 intern 方法被调用时,如果对象池中已经包含这一个相等的字符串对象则返回对象池中的实例,否则添加字符串到对象池并返回该字符串的引用。

总结

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

  • 如果你需要一个可修改的字符串,应该使用 StringBuffer 或者 StringBuilder。(否则会有大量时间浪费在垃圾回收上,因为每次试图修改都有新的String 对象被创建出来)

  • 如果你只需要创建一个字符串,你可以使用双引号的方式,如果你需要在堆中创建一个新的对象,你可以选择构造函数的方式。

Long

Long 最被我们关注的就是 Long 的缓存问题,Long 自己实现了一种缓存机制,缓存了从 -128 到 127 内的所有 Long 值,如果是这个范围内的 Long 值,就不会初始化,而是从缓存中拿,缓存初始化源码如下:

private static class LongCache {
    private LongCache(){}
    // 缓存,范围从 -128 到 127,+1 是因为有个 0
    static final Long cache[] = new Long[-(-128) + 127 + 1];

    // 容器初始化时,进行加载
    static {
        // 缓存 Long 值,注意这里是 i - 128 ,所以再拿的时候就需要 + 128
        for(int i = 0; i < cache.length; i++)
            cache[i] = new Long(i - 128);
    }
}

为什么使用 Long 时,大家推荐多使用 valueOf 方法,少使用 parseLong 方法

答:因为 Long 本身有缓存机制,缓存了 -128 到 127 范围内的 Long,valueOf 方法会从缓存中去拿值,如果命中缓存,会减少资源的开销,parseLong 方法就没有这个机制。

// valueOf源码
public static Long valueOf(long l) {
        final int offset = 128;
        if (l >= -128 && l <= 127) { // will cache
            return LongCache.cache[(int)l + offset];
        }
        return new Long(l);
    }

评论
评论
  目录