String源码初了解

简单介绍

String是Java常见的数据类型。常说的不可变类。通过源码来看一下比较重要的方法。

类名定义

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

可以看到,首先final修饰了String类,表明了String类是不可被继承的。实现了CharSequence接口,也就需要实现charAt方法.

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

    /** Cache the hash code for the string */
    private int hash; // Default to 0

final修饰的字符数组。说明了String实际储存是字符数组,且无法改变。

构造方法

从提示上可以看到非常多的构造方法。但都是对value[]进行初始化的不同操作。

public String() {
        this.value = "".value;
    }
public String(char value[]) {
        this.value = Arrays.copyOf(value, value.length);
    }

无参的构造方法就是给value[]进行了初始化,因为final修饰成员变量的初始化 要不直接赋值初始化,要不就在构造器里面初始化。

char[]参数的构造方法调用了Arrays.copyOf方法来进行拷贝,也说明了String在操作上的不可变。

常用方法

 equals

public boolean equals(Object anObject) {
     //1.如果内存地址相同,则返回true
if (this == anObject) { return true; }
     //2.只有比较的对象是String的实例才能进行比较。否则直接false
if (anObject instanceof String) {
       String anotherString
= (String)anObject; int n = value.length;
//3.比较各自String的字符数组的长度是否相同
if (n == anotherString.value.length) { char v1[] = value; char v2[] = anotherString.value; int i = 0;
//4.逐个比较字符是否相同
while (n-- != 0) { if (v1[i] != v2[i]) return false; i++; } return true; } } return false; }

chatAt:也是利用字符数组的下标

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

indexOf

public int indexOf(int ch) {
        return indexOf(ch, 0);
    }

当我们习惯于用单个字符作为参数去获得该字符在String的字符数组里面的下标的时候。

比如String s = "abc";   System.outprintln(s.indexOf('c'));  判断c是否在字符串中,在的话返回下标。

但是进到源码看到的是indexOf的参数是int而不是一开始想的那样是char,其实这里的int是目标字符的Unicode值,放单个字符的话也会转成字符在Unicode编码中对应的代码值。

所以下面的indexOf(int,fromIndex)就比较容易理解了。

public int indexOf(int ch, int fromIndex) {
        final int max = value.length;
        if (fromIndex < 0) {
            fromIndex = 0;
        } else if (fromIndex >= max) {
            // Note: fromIndex might be near -1>>>1.
            return -1;
        }
    
        if (ch < Character.MIN_SUPPLEMENTARY_CODE_POINT) {
            // handle most cases here (ch is a BMP code point or a
            // negative value (invalid code point))
            final char[] value = this.value;
            for (int i = fromIndex; i < max; i++) {
                if (value[i] == ch) {
                    return i;
                }
            }
            return -1;
        } else {
            return indexOfSupplementary(ch, fromIndex);
        }
    }

indexOf(int ch,int fromIndex)从指定索引开始搜索,返回在此字符串中第一次出现指定字符的索引。只不过上面的方法索引从0开始。

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) {
       //定义一个新的字符数组来装String的值。然后这buf[]里面进行替换
char buf[] = new char[len]; for (int j = 0; j < i; j++) { buf[j] = val[j]; }
        //替换的方法。逐个比较,相同则替换成新的,不同则还是c.三目运算
while (i < len) { char c = val[i]; buf[i] = (c == oldChar) ? newChar : c; i++; }
          //最后new了一个String返回。
return new String(buf, true); } } return this; }

常用的替换单个字符。大概的过程注释代码写了。主要是这个方法最后会返回一个新的字符串(代码注释有),而不是改原来的String,所以使用的时候记得用String来接收。

concat 

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

拼接字符串的方法

一般用的字符串拼接的方法就是  String 的” + “” ,concat,StringBuffer和StringBuilder的append方法。

一般使用建议应该都是 + <concat<StringBuffer<StringBuilder。

String 的+  2个字符串拼接在编译的时候进行了优化,会生成一个StringBuilder对象来进行拼接。2个的+效率并不会低,但是关键在于每一次 +都会生成一个StringBuider对象。会造成内存的浪费。

//一般+ 
String str=""; for(int i=0;i<10;i++){ str+=i; } //append StringBuilder sb = new StringBuilder(); for(int i=0;i<10;i++) { sb.append(i); }

所以建议使用StringBuilder进行字符串的操作。StringBuffer对方法进行了同步处理。所以多线程的情况下使用StringBuffer,单线程就使用StringBuilder比较高效。

字符串常量池

jdk1.7以后字符串常量池从方法区移动到堆内存中了。这里就简单介绍一个新建一个String的两种方法的区别。(具体的关于字符串常量池里面存储的到底是String对象还是对象的引用,我看了好几篇都没怎么看明白。暂且当初存储的是String对象)

        String s1 = "abc";
        String s2 = "abc";
        String s3 = new String("abc");
        System.out.println(s1==s2);//true
        System.out.println(s1==s3);//false

这是常见的关于字符串的题目了吧。涉及的就是字符串常量池的问题。

用双引号声明的字符串常量,会先字符串常量池是否存在该字符串进行进行判断。如果存在该字符串,会直接指向字符串常量池的String对象。不存在的话,将该String对象存储在字符串常量池中,然后指向它。

所以s1 = s2 为true 便是 2个String引用都指向了字符串常量池的对象。

而new方法的话,比较好理解,直接在堆中开辟了一个内存来存储该字符串。所以和前面的内存地址就不一样了。所以s1==s3 为false

String的不可变性

我们常说的String是不可变的。但是真的是不可以变的吗。可以通过源码看到,String的设计为final类,实际存储字符的定义为private final字符数组,而且不提供方法来对属性修改。

可以比较明显的看到,实际数据是存储在value[] 字符数组中,虽然定义private final,不提供方法修改。但是私有的成员变量也是可以访问的,那就是反射。。反射出String对象的value,然后通过获取value的引用来改变数组的结构

只不过一般操作String并不会使用这种方法。所以说String是不可变的。

//创建字符串"Hello World", 并赋给引用s
        String s = "Hello World";
         
        System.out.println("s = " + s); //Hello World
         
        //获取String类中的value字段
        Field valueFieldOfString = String.class.getDeclaredField("value");
         
        //改变value属性的访问权限
        valueFieldOfString.setAccessible(true);
         
        //获取s对象上的value属性的值
        char[] value = (char[]) valueFieldOfString.get(s);
         
        //改变value所引用的数组中的第5个字符
        value[5] = '_';
         
        System.out.println("s = " + s);  //Hello_World

参考文章:https://www.cnblogs.com/wuchanming/p/9201103.html

posted @ 2019-01-28 01:14  发包哥哥  阅读(792)  评论(0编辑  收藏  举报