java基础巩固-浅析String源码及其不可变性
字符串可以说是广泛应用在日常编程中,jdk从1.0就提供了String类来创建和操作字符串。同时它也是不可改变类(基本类型的包装类都不可改变)的典型代表。 源码查看(基于1.8) public final class String implements java.io.Serializable, Comparable<String>, CharSequence { private final char value[]; //这边只是引用并不是真正对象 ... } //首先string类创建的对象是不可变的(一个对象在创建完成后不能再改变它状态,说明是不可变的,并发程序最喜欢不可变量了), //里面最主要的成员为char类型的数组 几个构造方法 //空的构造方法 例如 String a = new String(); a为""空字符 public String() { this.value = new char[0]; } //带参构造方法 将源的hash和value赋给目标String public String(String original) { this.value = original.value; this.hash = original.hash; } 几个常用经典的String类方法 1.equals //如果引用指向的内存值都相等 直接返回true public boolean equals(Object anObject) { if (this == anObject) { return true; } //instanceof判断是否属于或子类 但Stringfinal修饰不可继承 只考虑是否为String类型 //上面说过String的成员为char数组,equals内则比较char类数组元素是否一一相等 if (anObject instanceof String) { String anotherString = (String)anObject; int n = value.length; //长度不相等返回false if (n == anotherString.value.length) { char v1[] = value; char v2[] = anotherString.value; int i = 0; //从后往前单个字符判断,如果有不相等,返回false while (n-- != 0) { if (v1[i] != v2[i]) return false; i++; } return true; } } return false; } 2.subString(beginIndex,endIndex) subString这边存在一个小插曲 在jdk1.7以前,该方法存在内存泄漏问题。之所以存在是因为在此之前,String三个参数的构造方法是这么写的。成员变量为这三个,jdk7以后取消掉了offset和count加入了hash,虽然原来的构造方法简洁高效但存在gc问题。所以7以后放弃了性能采取了更为保守的写法。 /** The value is used for character storage. */ private final char value[]; /** The offset is the first index of the storage that is used. */ private final int offset; /** The count is the number of characters in the String. */ private final int count; ... public String substring(int beginIndex, int endIndex) { if (beginIndex < 0) { throw new StringIndexOutOfBoundsException(beginIndex); } if (endIndex > count) { throw new StringIndexOutOfBoundsException(endIndex); } if (beginIndex > endIndex) { throw new StringIndexOutOfBoundsException(endIndex - beginIndex); } //虽然这边返回的是新的String对象,但构造方法中还引用着原先的value return ((beginIndex == 0) && (endIndex == count)) ? this : new String(offset + beginIndex, endIndex - beginIndex, value); } ... String(int offset, int count, char value[]) { this.value = value; this.offset = offset; this.count = count; } //这边开始this.value = value; 出现问题,这三个个原来为String类中的三个私有成员变量,因为这种实现还在引用原先的字符串变量value[] 通过offset(起始位置)和count(字符所占个数)返回一个新的字符串,这样可能导致jvm认为最初被截取的字符串还被引用就不对其gc,如果这个原始字符串很大,就会占用着内存,出现内存泄漏等gc问题。 jdk1.7以后的写法变化 //虽然这边还是有offse和count参数 但不是成员变量了 private final char value[]; /** Cache the hash code for the string */ private int hash; // Default to 0 ... 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);//构造函数参数顺序也有所变化 } ... public String(char value[], int offset, int count) { if (offset < 0) { throw new StringIndexOutOfBoundsException(offset); } if (count < 0) { throw new StringIndexOutOfBoundsException(count); } // 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); } 3.hashcode public int hashCode() { int h = hash; //如果hash没有被计算过,并且字符串不为空,则进行hashCode计算 if (h == 0 && value.length > 0) { char val[] = value; //计算过程 //s[0]*31^(n-1) + s[1]*31^(n-2) + ... + s[n-1] for (int i = 0; i < value.length; i++) { h = 31 * h + val[i]; } //hash赋值 hash = h; } return h; } //String重写了Object类的hashcode方法,根据值来计算hashcode值,不过Object的该方法为native本地方法。 //该方法设计十分巧妙,它先从0判断字符大小,如果 //hashcode其实就是散列码 这种算法的话 同样的字符串的值一定相等 但不同的字符串其实也有可能得到同样的hashcode值 n=3 i=0 -> h = 31 * 0 + val[0] i=1 -> h = 31 * (31 * 0 + val[0]) + val[1] i=2 -> h = 31 * (31 * (31 * 0 + val[0]) + val[1]) + val[2] //以字符串 "123"为例 1的的ascil码为49 所以 "1".hashcode()的值为49,2为50... h = 31 * (31 * (31 * 0 + val[0]) + val[1]) + val[2] = 31 * (31 * 49 + 50) + 51 = 48690 详细算法参考这里 String的不可变性 //这边可能存在一个疑问 s对象是否发生了改变 String s = "hello"; s = "world"; //String不可变不是在原内存地址上修改数据,而是重新指向一个新对象,新地址,所以这里的hello对象并没有被改变。 //同样的类似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);//这边返回的是新的一个String对象 而不改变原来的对象 } } return this; //如果都一样 就指向同一个对象了 } String对象不可变 String对象为什么不可变 String用final修饰是为了防止被继承进而破坏,而让String对象不可变主要也是为了安全。 这里原来我有一个误区,仅仅认为说final修饰了String类,所以String对象不可变,想法很天真,,final修饰类的话主要是让类不可被继承,final修饰基本类型变量不能对基本类型对象重新赋值,但对于引用类型的变量(如数组),它保存的只是一个引用,final只需保证这个引用的地址不改变,即一直引用同一个对象即可,但是这个对象还是能改变的。 比如 final int[] arr = {1}; arr[0] = 3; //改变 arr = new int[]{3}; //因为这个是个新的数组 这样改变了数组的地址 编译不通过 除非去掉final //String类源码中的成员变量 (jdk1.8) /** The value is used for character storage. */ private final char value[]; /** Cache the hash code for the string */ private int hash; // Default to 0 String.png 思考 1.String对象是否真的不可变 可以通过反射改变 String的成员变量为final修饰,就是初始化之后不可改变,但是这几个成员中value比较特殊,因为他是个引用变量而不是真正的对象,value[]是final修饰的,也就是说不能再指向其他数组对象,但是可以改变数组内部的结构来改变。 注:简单来说就是final修饰数组,指定数组所指向的内存空间固定,数组内部值还能改。因为数组是引用类型,内存地址是不可改变的。 实例代码 public static void testReflection() throws Exception { String s = "Hello,World"; System.out.println("s = " + s); //Hello World //获取String类中的value字段 Field valueOfString = String.class.getDeclaredField("value"); //改变value属性的访问权限 valueOfString.setAccessible(true); //获取s对象上的value属性的值 char[] value = (char[]) valueOfString.get(s); //改变value所引用的数组中的第5个字符 value[5] = '_'; System.out.println("s = " + s); //Hello_World } String为什么要设计成不可变 1.允许String对象缓存hashcode: Java中String对象的哈希码被频繁地使用, 比如在hashMap 等容器中。字符串不变性保证了hash码的唯一性,因此可以放心地进行缓存。 2.安全性 String被许多的Java类(库)用来当做参数,例如 网络连接地址URL,文件路径path,还有反射机制所需要的String参数等, 假若String不是固定不变的,将会引起各种安全隐患。 对于一个方法而言,参数是为该方法提供信息的,而不是让方法改变自己。 3.字符串常量池的需要 字符串常量池(String pool, String intern pool, String保留池) java堆内存放了一个特殊的区域用于常量池, 当创建一个String对象时,假如此字符串值已经存在于常量池中,则不会创建一个新的对象,而是引用已经存在的对象。