浅层次了解Java中的String

最近觉得自己学的东西常常会忘记,于是就想写一下笔记。总结一下String相关的一些内容,如果有什么理解有误的话欢迎大家指出。

目录

  • Unicode 编码
  • String属性
  • 构造方法
  • 常用函数
  • 一些常被问到的问题

Unicode

         计算机诞生的时候,老美为了存储他们的英文单词和一些符号,与计算机有一套约定,整数45表示百分号%,整数50代表数字2,51表示数字3,这些约定成为ASCII,他跟计算机约定好了从整数0-127所对应的字符。即1个字节就可以保存下来了。

         后来计算机被世界广泛应用,所以各个国家都需要与计算机做好约定,比如我们大中国也需要跟计算机约定好,但是128个字符明显就容不下我们中国五千年来的文化积累呀,于是我们和计算机说好,中文要占3个字节,比如0xB6A001表示中文字“丁”,这种数值与中文的约定成为GBK,那日本也需要与计算机做约定,韩国也需要。世界上那么多种语言,计算机在识别字符的时候还要先看看是什么国家跟他做了约定,非常麻烦。

         于是Unicode对这些编码进行了大一统,用6个字节表示所有的字符,范围从00x10FFFF,其中常用的字符放在00xFFFF的位置,每一个字符对应的数字叫做码点,

         后来人们发现,其实常用的字符用00xFFFF就可以表示了,后面那些字符用的很少,没必要每次都弄3个字节来存储,于是我们制定了可变长的UTF-8和·UTF16,在存储过程中,如果字符在00xFFFF之间就用2个字节,刚好一个char来存储,如果大于0xFFFF的就要用4个字节存储,刚好2个char。在java中就是以这种方式存储字符的。

属性:

/** 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 */
private static final long serialVersionUID = -6849794470754667710L;
  • char数组 被final修饰
  • hash
  • serialVersionUID 这个与序列化有关,以后再总结

构造方法

  • 默认构造函数

       public String() {
           this.value = "".value;
       }
  • 传入String

       public String(String original) {
           this.value = original.value;
           this.hash = original.hash;
       }
  • 传入char数组

       public String(char value[]) {
           this.value = Arrays.copyOf(value, value.length);
       }
  • 传入byte数组,需要传入字符集

       public String(byte bytes[], String charsetName)
               throws UnsupportedEncodingException {
           this(bytes, 0, bytes.length, charsetName);
       }
    平时我们的转码就需要依靠这个构造方法啦
  • 传入StringBuffer

       public String(StringBuffer buffer) {
           synchronized(buffer) {
               this.value = Arrays.copyOf(buffer.getValue(), buffer.length());
           }
       }
    这里上了锁,所以是线程安全滴
  • 传入StringBuilder

       public String(StringBuilder builder) {
               this.value = Arrays.copyOf(builder.getValue(), builder.length());
       }
    关于 StringBuilderStringBuffer等等会讲
  • 还有很多就不数啦

常用方法

equals

听说这个常常会被问到,本来equals方法是Object类里面的,String把他重写了一下,用来判断两字符串是否相等

public boolean equals(Object anObject) {
        //判断是不是同一个引用
        if (this == anObject) {
            return true;
        }
        //判断是否为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;
    }

写的很精炼,里面有些地方值得我去学习的,看到instanceof我突然想起反射里面的一个小知识点

equalsIgnoreCase

忽略大小写比较两字符串是否相等

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

这里调用了一个叫regionMatches的方法,我也把他沾上来

 public boolean regionMatches(boolean ignoreCase, int toffset,
            String other, int ooffset, int len) {
        char ta[] = value;
        int to = toffset;
        char pa[] = other.value;
        int po = ooffset;
        // Note: toffset, ooffset, or len might be near -1>>>1.
        if ((ooffset < 0) || (toffset < 0)
                || (toffset > (long)value.length - len)
                || (ooffset > (long)other.value.length - len)) {
            return false;
        }
        while (len-- > 0) {
            char c1 = ta[to++];
            char c2 = pa[po++];
            if (c1 == c2) {
                continue;
            }
            if (ignoreCase) {
                // If characters don't match but case may be ignored,
                // try converting both characters to uppercase.
                // If the results match, then the comparison scan should
                // continue.
                char u1 = Character.toUpperCase(c1);
                char u2 = Character.toUpperCase(c2);
                if (u1 == u2) {
                    continue;
                }
                // Unfortunately, conversion to uppercase does not work properly
                // for the Georgian alphabet, which has strange rules about case
                // conversion.  So we need to make one last check before
                // exiting.
                if (Character.toLowerCase(u1) == Character.toLowerCase(u2)) {
                    continue;
                }
            }
            return false;
        }
        return true;
    }

他在比较的时候还先转大写再转小写,为什么要这么做大家看他的注释把我其实没看懂。

我想了一下,String不是带有一个toUppaerCase的方法嘛?如果让我去写这个方法我可能就直接这样写了

s2.equals(s1.toUppaerCase());

不过看了看源码,我这样写Java会对字符串遍历两次,效率会比较低吧。

然后我又看了一下toUpperCase()方法里面的代码

     public String toUpperCase(Locale locale) {
        if (locale == null) {
            throw new NullPointerException();
        }

        int firstLower;
        final int len = value.length;

        /* Now check if there are any characters that need to be changed. */
        scan: {
            for (firstLower = 0 ; firstLower < len; ) {
                
                int c = (int)value[firstLower];
                int srcCount;
                if ((c >= Character.MIN_HIGH_SURROGATE)
                        && (c <= Character.MAX_HIGH_SURROGATE)) {
                    c = codePointAt(firstLower);
                    srcCount = Character.charCount(c);
                } else {
                    srcCount = 1;
                }
                int upperCaseChar = Character.toUpperCaseEx(c);
                if ((upperCaseChar == Character.ERROR)
                        || (c != upperCaseChar)) {
                    break scan;
                }
                firstLower += srcCount;
            }
            return this;
        }

值得注意的是其中两行代码

c = codePointAt(firstLower);
srcCount = Character.charCount(c);

在Java中有两个获取下表字符的Unicode编码,

  • charAt 获取的是0-0xFFFFFF的码点,3个字节,保存在int中
  • codePointAt 获取的是0-0xFFFF的码点,2个字节,保存在int中

大家顺着代码看,大概也能猜出来Character.charCount()其实是计算这个字符到底是占2个字节的还是3个字节的。

顺便提一下,String的length方法和codePointCount方法也是一样的道理

  • length 返回的是字符串的长度,如果有超出0xFFFF的字符会算两
  • codePointCount 返回真正的字符个数,一个字符对应一个码点

所以Java在大小写转换的时候还要考虑编码问题,学就是了。

compareTo

也是比较两个字符串,不过他与equals不同,equals传入的是Object,返回的是布尔值,compareTo传入的是字符串,返回的是整型

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

返回值表示第一个出现不同的字符的编码差值。如果是相同的字符串则返回0。大家回忆一下equals方法,他第一步是判断两个变量是否为同一个引用,这里为什么不去判断呢?解决这个问题我们要想一下字符串在内存中是怎么存储的。

其他函数

  • indexOf 可传入字符、字符数组,从左向右对入参进行索引,找不到返回-1
  • lastIndexOf 从右向左对入参进行索引,找不到返回-1
  • contains 判断是否包含子字符串,内部调用indexOf
  • toUpperCase 将字符串转换成大写
  • toLowerCase 将字符串转换成小写
  • trim 去除字符串前后的空格
  • replace 替换字符串,可传入正则表达式,内部真正做替换的是replaceAll
  • join 连接字符串,可以传入StringStringBuffer
注意 : 字符串是不可变的,操作完一定要拿个新的字符串存于下返回值,例如· trim函数,他会新建一个char数组存放结果
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;
}

一些常见问题

  • == equals有什么区别
  • final修饰的好处
  • String、StringBufferStringBuilder
  • String的创建方式

1. == equals有什么区别

==对于基本类型来说,是直接比较值的大小,而对于引用来说是比较地址是否相等,而String的·equals是比较两个字符串的内容是否相同。

可以发现Object也有equals方法,内部其实是调用==

public boolean equals(Object obj) {
    return (this == obj);
}

2. 为什么Stringfinal修饰

两个目的,安全高效

看下图,现在有两个引用指向字符串,我们的字符串是存储在常量池中的,如果字符串设计成可变的话,s1不小心对字符串的内容修改了,我们用s2取值的时候发现他变了,会引起不堪设想的灾难,关于常量池的内容晚点我也总结一下。

浅层次了解Java中的String_第1张图片

3. StringStringBufferStringBuilder的区别

SringBufferStringBuilder主要用于字符串的拼接。

字符串的拼接方法有很多,下面我们来分析一下:

  • +:
    由于String的不可变性,在循环体中对字符串拼接的时候每次都要创建一个对象

    String s = "";
    for(int i = 0; i < 100000; i++) {
       s = s + "test"
    }

    每次都需要在创建新的字符串变量,所以·+适用于常量字符串的拼接,编译器会在编译的时候帮我们拼接好。

  • Stringjoin方法

    其实内部是使用了StringJoin方法实现的,StringJoiner内部其实是使用StringBuilder的。

  • StringBufer

    我们可以看看他的append方法

    public synchronized StringBuffer append(CharSequence s) {
        toStringCache = null;
        super.append(s);
        return this;
    }

    加了同步锁,所以是线程安全的,返回的是this,所以支持链式操作

    由于加了同步锁,性能相对没有 StringBuilder
  • StringBuilder

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

    没有带锁,线程不安全,但是效率会皮较高,关于线程安全的知识我稍后也会总结

总结:因此,如果是常量或者一两个变量之间的拼接我们可以直接使用+,如果是需要重复拼接的话,不考虑线程安全就是用StringBuilder,考虑则使用StringBuffer.

关于StringJoiner

有点像Python里面的join,它可以帮我们在拼接的时候加入分隔符、前缀和后缀

构造函数

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

可以传入分隔符、前缀和后缀

两个StringJoiner可以使用merge拼接

public StringJoiner merge(StringJoiner other) {
    Objects.requireNonNull(other);
    if (other.value != null) {
        final int length = other.value.length();
        // lock the length so that we can seize the data to be appended
        // before initiate copying to avoid interference, especially when
        // merge 'this'
        StringBuilder builder = prepareBuilder();
        builder.append(other.value, other.prefix.length(), length);
    }
    return this;
}

4. String的创建方式

String有两种创建方式

  • String s1 = "Rhythm";

    s1在创建时,会在常量池中看看有没有这个字符串,有就直接返回句柄(地址),没有就创建在返回句柄(地址)

  • String s2 = new String("rHYTHM");

    直接在堆中创建,调用intern方法可以将其存入常量池

JDB1.7后的版本中,编译器会对字符串进行优化

String s1 = "AAA" + "BBB";
String s2 = "AAABBB";
System.out.println(s1 == s2);    //true

返回的是true,大家平时可以稍加注意一下


第一次写文章,写得不咋地,大家见谅,下一篇打算写一下JVM的内存模型,顺便交代一下String是怎么存储的。

你可能感兴趣的:(java,string,源码)