Java String 源码分析

时间:2022-07-28
本文章向大家介绍Java String 源码分析,主要内容包括其使用实例、应用技巧、基本知识点总结和需要注意事项,具有一定的参考价值,需要的朋友可以参考一下。

Java String 源码分析

定义

Java 8 中 String 源码

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

String 是final 类型不能被继承,同时实现了 java.io.serializable Comparable charSequence 三个接口。

String类 官方的说法是:

String 字符串常量,在实例化后不能被修改,但是字符串缓冲区支持可变的字符串,因为缓存区里面的不可变字符串对象可被共享。

属性

/** The value is used for character storage. */
private final char value[];

一个字符数组,并且是 final 类的,用于存储字符串内容。final 字符数组可看出,String 经过定义后,不能被修改。

可能会有疑问,String 初始化化之后,可以被修改啊

String str = "hello";
str = "World"

这里的复制并不是对 str 内容的修改,而是 str 指向了新的字符串。

/** use serialVersionUID from JDK 1.0.2 for interoperability */
private static final long serialVersionUID = -6849794470754667710L;

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

构造方法

空构造方法

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

该构造方法,指挥创建空的字符串,构造方法不必要的字符串对象是不变的。

不建议使用如下方式创建对象:会产生空字符串。

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

使用字符串类型对象初始化

 /**
     * 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 和 hash 两个属性直接赋值给目标 String . 因为 String 一旦定义之后是不可变的,所以也就不用担心,改变源 String 的值会影响到目标 String 的值。

使用字符串来构造

   /**
     * Allocates a new {@code String} so that it represents the sequence of
     * characters currently contained in the character array argument. The
     * contents of the character array are copied; subsequent modification of
     * the character array does not affect the newly created string.
     *
     * @param  value
     *         The initial value of the string
     */
    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);
    }

使用字符数组构建字符串时,会用到 Arrays.copyOf 方法,或者使用 Arrays.copyOfRange() 方法,这两个方法会将原有的字符串中的字符串数组中的内容赋值到 String 中的字符数组中,会创建一个新的字符串对象。随后修改字符数组不影响新创建的字符串。

使用字节数组来构建 String

Java 中,String 实例中报错一个字符数组,char[] 字符数组时以 unicode 码来存储的。

byte 是网络传输或者存储的序列化形式,在很多传输和存储过程中将 byte[] 数组和 String 进行相互转换。

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

如果使用 byte[] 数组构造 String 的时候,如果没有指明使用的字符集的话,那么StringCoding 的 decode 方法

public String(byte bytes[]) {
    this(bytes, 0, bytes.length);
}
public String(byte bytes[], String charsetName)
        throws UnsupportedEncodingException {
    this(bytes, 0, bytes.length, charsetName);
}
static char[] decode(String charsetName, byte[] ba, int off, int len)
        throws UnsupportedEncodingException
    {
        StringDecoder sd = deref(decoder);
        String csn = (charsetName == null) ? "ISO-8859-1" : charsetName;
        if ((sd == null) || !(csn.equals(sd.requestedCharsetName())
                              || csn.equals(sd.charsetName()))) {
            sd = null;
            try {
                Charset cs = lookupCharset(csn);
                if (cs != null)
                    sd = new StringDecoder(cs, csn);
            } catch (IllegalCharsetNameException x) {}
            if (sd == null)
                throw new UnsupportedEncodingException(csn);
            set(decoder, sd);
        }
        return sd.decode(ba, off, len);
    }

使用 byte[] 构造 String , 如果没有指定编码格式,默认使用 ISO-8895-1 编码。

使用 StringBuffer 和 StringBuilder 构造一个 String 。

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

关于效率问题,Java 官方文档中有提到 StringBuilder 的 toString 方法会更快一些,原因是 StringBuffer 中的toString 方法是 synchronized,有同步的开销。

StringBuilder 的 toString()方法

@Override
public String toString() {
    // Create a copy, don't share the array
    return new String(value, 0, count);
}

SringBuffer 的 toString() 方法

public synchronized String toString() {
    if (toStringCache == null) {
        toStringCache = Arrays.copyOfRange(value, 0, count);
    }
    return new String(toStringCache, true);
}

主要方法

substring

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 longStr = "....averylongstring";
String partStr= longStr.substring(10,30);

longStr 是临时的,要用的就是 partStr, 长度截取20个字符,但是他的内部数组是 longStr 共享的,虽然 longStr 可以被回收,但是内部数组不能释放。这样就出现了内存泄漏。Java 8 中采用的是 Array.copy 方法,避免了这个问题

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

length() 返回字符串长度

public int length() {
      return value.length;
  }

isEmpty() 返回字符为空

public boolean isEmpty() {
    return value.length == 0;
}

chartAt 方法

charAt(int index) 返回字符串中第 index+1 个字符

public char charAt(int index) {
    if ((index < 0) || (index >= value.length)) {
        throw new StringIndexOutOfBoundsException(index);
    }
    return value[index];
}

char[] toCharArray() 转化为字符数组

trim() 去掉两端空格

toUpperCase() 转换为大写

toLowerCase() 转换为小写

String concat(String str) //拼接字符串

String replace(char oldChar, char newChar) //将字符串中的oldChar字符换成newChar字符

boolean matches(String regex) //判断字符串是否匹配给定的regex正则表达式

boolean contains(CharSequence s) //判断字符串是否包含字符序列s

String[] split(String regex, int limit) 按照字符regex将字符串分成limit份

String[] split(String regex) 按照字符regex将字符串分段

getBytes

在创建 String 的时候,可以使用 byte[] 数组,将一个字节数组转换成字符串,同样,可以将一个字符串转换成字节数组,那么 String 提供了多种重载 getBytes 方法。

String s = "Hello World!";
byte[] bytes = s.getBytes();

上面这段代码没有指定编码方式,在该方法对字符串进行编码的时候默认使用系统编码,中文操作系统中可能会使用 GBK,英文操作系统中使用 ISO-8859-1.

boolean equals(ObjectanObject);

boolean contentEquals(StringBuffersb);

boolean contentEquals(CharSequencecs);

boolean equalsIgnoreCase(StringanotherString);

int compareTo(StringanotherString);

int compareToIgnoreCase(Stringstr);

boolean regionMatches(inttoffset,Stringother,intooffset,intlen) //局部匹配

boolean regionMatches(booleanignoreCase,inttoffset,Stringother,intooffset,intlen) //局部匹配

contentEquals

StringBuffer 考虑线程安全问题,加锁之后再调用 contentEquals(CharSequence sb) 方法。

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

equalsIgnoreCase 方法

 public boolean equalsIgnoreCase(String anotherString) {
       return (this == anotherString) ? true
               : (anotherString != null)
               && (anotherString.value.length == value.length)
               && regionMatches(true, 0, anotherString, 0, value.length);
   }                              

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

数学公式:s[0]*31^(n-1) + s[1]*31^(n-2) + … + s[n-1]

replaceFirst、replaceAll replace 区别

Sring replaceFirst(String replacement)
String replaceAll(String regex,String replacement)
String replace(CharSequence target,CharSequence replacement)

replace 的参数是 char,支持字符的替换,也支持字符串的替换 replaceAll 和 replaceFirst 的参数是 regex ,基于正则表达式替换 replaceAll("%d",“”) 把一个字符串所有的数字字符都换成 replace 方法

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

intern 方法

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

switch 对字符串的支持

public class switchDemoString {
    public static void main(String[] args) {
        String str = "world";
        switch (str) {
            case "hello":
                System.out.println("hello");
                break;
            case "world":
                System.out.println("world");
                break;
            default:
                break;
        }
    }
}                              

javap 编译之后

public static void main(String args[]) {
        String str = "world";
        String s;
        switch ((s = str).hashCode()) {
            case 99162322:
                if (s.equals("hello"))
                    System.out.println("hello");
                break;
            case 113318802:
                if (s.equals("world"))
                    System.out.println("world");
                break;
            default:
                break;
        }
    }                               

字符串的switch 是通过equals 和 hashCode 方法来实现的,switch 支持整型 byte ,short char int, 也可以看到 hashcode 返回的是int 。

为啥 String 定义成 final 的?

  • 为了线程安全 字符串不可变,所以是多线程安全,同一个字符串实例可以被多个线程共享,这样不会因为线程安全问题而使用同步,字符串便是线程安全的。
  • 为实现 String 可以创建 hashcode 不可变 字符串不可变,在创建的时候 hashCode 被缓存了,不㔿重新机损这样可以使得字符串作为 Map的键,字符串处理快。
  • 为了实现字符串常量池 字符串不可变,可以放在字符串常量池中,因为不同的字符串遍历都可以指向池中的同一个字符串,如果字符串可变,那么 String intern 不能实现。

总结

  • string 对象在内存对中被创建后,就无法修改 - 如果需要一个可修改的字符串,应该使用 StringBuffer 或者 StringBuilder
  • 如果只需要创建一个字符串,可以使用引号的方式,如果在堆中创建一个新的对象,可以选择构造函数。

欢迎关注公众号:程序员开发者社区

微信号:程序员开发者社区

博客:CSDN 王小明

关注我们,了解更多