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