一、定义
public final class String implements java.io.Serializable, Comparable<String>, CharSequence{}
从该类的声明中我们可以看出String是final类型的,表示该类不能被继承,同时该类实现了三个接口:java.io.Serializable
、 Comparable<String>
、 CharSequence
二、属性
/** The value is used for character storage. */
private final char value[];
这是一个字符数组,并且是final类型,他用于存储字符串内容,从fianl这个关键字中我们可以看出,String的内容一旦被初始化了是不能被更改的。 从这里我们也能知道,String其实就是用char[]实现的。
/** Cache the hash code for the string */
private int hash; // Default to 0
缓存hash code 的值,默认值是0;
/** use serialVersionUID from JDK 1.0.2 for interoperability */
private static final long serialVersionUID = -6849794470754667710L;
因为String
实现了Serializable
接口,所以支持序列化和反序列化支持。Java的序列化机制是通过在运行时判断类的serialVersionUID
来验证版本一致性的。在进行反序列化时,JVM会把传来的字节流中的serialVersionUID
与本地相应实体(类)的serialVersionUID
进行比较,如果相同就认为是一致的,可以进行反序列化,否则就会出现序列化版本不一致的异常(InvalidCastException
)。
三、构造方法
1.空的构造方法
public String() {
this.value = "".value;//初始化value数组,赋空值
}
2.使用一个字符串、字符数组去构造一个String
第一种使用字符串去创建另一个字符串:它做的是将 源String 的value和hash值 赋给 目标String,这样,你对源String的修改就不会影响目标String的值。
public String(String original) {
this.value = original.value;
this.hash = original.hash;
}
第二种使用字符数组创建一个String:这里执行时,都会先将传入的值使用Array.copyOf或者copyOfRange方法复制一份,赋给当前的char[] value,这样就可以避免我改变目标value,从而改变源value的问题,这也是一个编程小技巧。而第三个构造方法,是直接将数组的引用赋给当前value ,这样做的好处, 首先,性能好,直接将value指向传入char[] 数组,比一个一个的copy更快,其次,我们并没有开辟新的数组空间,节约内存。其中的offset(偏移量)和count参数,用来指定你想从传入数组截取的部分。
public String(char value[]) {}
public String(char value[], int offset, int count) {}
//参数share是为了确保String的不可变性,
String(char[] value, boolean share) {
// assert share : "unshared not supported";
this.value = value;
}
3.使用字节数组、整型数组构造一个String
这里提供了许多用字节数组来创建String的构造方法,主要是因为编码问题,String 中char[] 使用的是unicode码来存储,为了确保传入byte[] 不会乱码,所以可以设置编码,在通过StringCoding.decode方法进行解码,如果使用无编码设置的构造方法,默认编码格式是ISO-8859-1。
//参数codePoints表示unicode中字符所对应的十进制值
public String(int[] codePoints, int offset, int count) {}
public String(byte bytes[], int offset, int length, String charsetName)throws UnsupportedEncodingException {}
public String(byte bytes[], int offset, int length, Charset charset) {}
public String(byte bytes[], String charsetName)throws UnsupportedEncodingException {}
public String(byte bytes[], Charset charset) {}
public String(byte bytes[], int offset, int length) {}
public String(byte bytes[]) {}
4.使用StringBuffer或StringBuilder构造一个String
StringBuffer和StringBuilder也可以用于构造String,String和StringBuilder的区别是String 是不可变的,而StringBuilder是可变的,StringBuffer和StringBuilder的区别是StringBuffer是线程安全的,因为它几乎所有方法用synchronized修饰了,但效率低,StringBuilder线程不安全,但效率高。
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());
}
四、其他方法
常用方法
//获取字符串的长度
public int length() {}
//判断字符串的长度是否为0
public boolean isEmpty() {}
//返回目标位置的字符
public char charAt(int index) {}
//返回目标位置的字符的十进制数
public int codePointAt(int index) {}
//返回目标位置的字符前一个的十进制数
public int codePointBefore(int index) {}
//将目标字符数组赋值,无返回
public void getChars(int srcBegin, int srcEnd, char dst[], int dstBegin) {}
//获取当前字符串的byte数组,可以指定对应编码
public byte[] getBytes(String charsetName){}
public byte[] getBytes(Charset charset) {}
public byte[] getBytes() {}
//从第toffset个开始计算,判断是否是以指定字符串开头
public boolean startsWith(String prefix, int toffset) {}
//是否是以指定的字符串结尾(实际调用的还是startWith方法,第二参数默认传的是value.length - suffix.value.length)
public boolean endsWith(String suffix) {}
//返回字符在当前字符串中位置,没有返回-1
public int indexOf(int ch) {}
//返回当前字符最后一次出现位置,没有返回-1
public int lastIndexOf(int ch) {}
//返回目标字符串在当前字符串中的位置,没有返回-1
public int indexOf(String str) {}
//返回当前字符串最后一次出现位置,没有返回-1
public int lastIndexOf(String str) {}
//截取从beginIndex位置开始到endIndex结束,[beginIndex, endIndex);
public String substring(int beginIndex, int endIndex) {}
//将字符串添加到源字符串,通过getChars()来实现
public String concat(String str) {}
//用新字符来替所有老字符
public String replace(char oldChar, char newChar) {}
//判断是否包含当前字符序列
public boolean contains(CharSequence s) {}
//将字符串根据regex分成limit份,返回String数组
public String[] split(String regex, int limit) {}
public String[] split(String regex) {}
//去除前后空格
public String trim() {}
//将字符串转化成字符数组
public char[] toCharArray() {}
//转换成大写
public String toUpperCase(}
//转换成小写
public String toLowerCase() {}
//判断是否包含指定的字符序列
public boolean contains(CharSequence s) {}
//判断字符串是否与正则表达式匹配
public boolean matches(String regex) {}
比较方法
String中有很多比较方法,前四个都是比较两个对象的字符数组是否相同,相同返回true,不同为false
public boolean equals(Object anObject) {}
public boolean contentEquals(StringBuffer sb) {}
private boolean nonSyncContentEquals(AbstractStringBuilder sb) {}
public boolean contentEquals(CharSequence cs) {}
//忽略大小写,相同返回true,反之返回false
public boolean equalsIgnoreCase(String anotherString) {}
//比较两个字符串中每个字符的大小,不同,返回差值,如果都相同,返回它们长度的差值
public int compareTo(String anotherString) {}
//和上面一样,忽略大小写
public int compareToIgnoreCase(String str) {}
//局部比较
public boolean regionMatches(int toffset, String other, int ooffset, int len) {}
public boolean regionMatches(boolean ignoreCase, int toffset, String other, int ooffset, int len) {}
equals()
可以看出它是先比较这两个对象是否是拥有同一个对象引用,再判断前面的对象的类型是否是后面的类,或者其子类、实现类,如果是返回true,否则返回false,再比较它们各自的字符数组中值是否一致。
可以看出它们是先做“宏观”比较,在做“微观”比较。
public boolean equals(Object anObject) {
if (this == anObject) {
return true;
}
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;
}
hashCode()
s[0]*31^(n-1) + s[1]*31^(n-2) + ... + s[n-1]
s[i]是string的第i个字符,n是String的长度。那为什么这里用31,而不是其它数呢? 计算机的乘法涉及到移位计算。当一个数乘以2时,就直接拿该数左移一位即可!选择31原因是因为31是一个素数!
所谓素数:质数又称素数。指在一个大于1的自然数中,除了1和此整数自身外,没法被其他自然数整除的数。
在存储数据计算hash地址的时候,我们希望尽量减少有同样的hash地址,所谓“冲突”。如果使用相同hash地址的数据过多,那么这些数据所组成的hash链就更长,从而降低了查询效率!所以在选择系数的时候要选择尽量长的系数并且让乘法尽量不要溢出的系数,因为如果计算出来的hash地址越大,所谓的“冲突”就越少,查找起来效率也会提高。
31可以 由i*31= (i<<5)-1来表示,现在很多虚拟机里面都有做相关优化,使用31的原因可能是为了更好的分配hash地址,并且31只占用5bits!在java乘法中如果数字相乘过大会导致溢出的问题,从而导致数据的丢失.而31则是素数(质数)而且不是很长的数字,最终它被选择为相乘的系数的原因不过与此!
在Java中,整型数是32位的,也就是说最多有2^32= 4294967296个整数,将任意一个字符串,经过hashCode计算之后,得到的整数应该在这4294967296数之中。那么,最多有 4294967297个不同的字符串作hashCode之后,肯定有两个结果是一样的, hashCode可以保证相同的字符串的hash值肯定相同,但是,hash值相同并不一定是value值就相同。
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;
}
重写了equals()就需要重写hashCode()
hashCode的通用规定:
-
在应用程序的执行期间,只要对象的equals方法的比较操作所用到的信息没有被修改,那么对同一个对象的多次调用,hashCode方法都必须始终返回同一个值。在一个应用程序与另一个应用程序的执行过程中,执行hashCode方法所返回的值可以不一致。
-
如果两个对象根据equals(Object)方法比较是相等的,那么调用这两个对象中的hashCode方法都必须产生同样的整数结果
-
如果两个对象根据equals(Object)方法比较是不相等的,那么调用这两个对象中的hashCode方法,则不一定要求hashCode方法必须产生不用的结果。但是程序员应该知道,给不相等的对象产生截然不同的整数结果,有可能提高散列表的性能。
由上面三条规定可知,如果重写了equals方法而没有重写hashCode方法的话,就违反了第二条规定。相等的对象必须拥有相等的hash code。
这里篇幅有限不做扩展,有兴趣可以参考这篇文章:彻底理解 为什么重写equals()方法为什么要重写hashCode()方法
format 方法
他有两种重载形式,这两个唯一不同是多了一个Locale类型的参数,这个Locale类型参数的主要作用,可以根据指定国家或地区的语言来显示结果。
public static String format(Locale l, String format, Object... args) {}
public static String format(String format, Object... args) {}
可以常规的格式化,也可以进行时间格式化
常规类型格式化(图片来源于String.format()字符串常规类型格式化!_Andrew_Chenwq的博客-优快云博客的截图)
时间格式化,种类较多,可以参考(【Java基础】String.format()格式化日期_java之路-优快云博客_java string.format 日期)
intern()
public native String intern();
该方法返回一个字符串对象的内部化引用。当intern方法被调用时,如果字符串常量池中已经包含一个等于此String对象的字符串,则返回常量池中这个字符串的String对象;否则,将此String对象包含的字符添加到常量池中,并返回此String对象的引用。
题外话:
一、字符的个人理解
char表示的是字符,范围是-65536~65535之间,初始化可以使用数字,字符,和16进制的数。
Unicode是一种常用的字符编码方式,最初想用0~65535之间的数表示世界上所有的字符,但是现在看来显然不够,Unicode码空间为U+0000 ~ U+10FFFF,一共1114112个码位(216∗17=1114112)其中只有1112064个码位是合法的,有2048个码位不合法,要表示超出65536(U+0000到U+FFFF部分被称为基本多语言面)的部分(增补字符),所以就有了高代理部分,范围是0xD800~0xDBFF,低代理部分,范围是0xDC00~0xDFFF,高代理部分和低代理部分分别占据1024个不合法码位,他们用两个char表示一个字符,也就是说,增补字符占4个字节,它们可以组成1048576个合法码位,如果再加上65536,就是1112064个合法码位, 但目前只用到了九万多个。
在String中也常会调用Character类中的属性和方法,去判断字符是否在合法码位中。
如 public String[] split(String regex, int limit) {}方法中的代码片段
if (((regex.value.length == 1 &&
".$|()[{^?*+\\".indexOf(ch = regex.charAt(0)) == -1) ||
(regex.length() == 2 &&
regex.charAt(0) == '\\' &&
(((ch = regex.charAt(1))-'0')|('9'-ch)) < 0 &&
((ch-'a')|('z'-ch)) < 0 &&
((ch-'A')|('Z'-ch)) < 0)) &&
//判断字符不在不合法码位中
(ch < Character.MIN_HIGH_SURROGATE || ch > Character.MAX_LOW_SURROGATE)){}
贴上一个例子:
//高代理部分,范围是0xD800~0xDBFF,低代理部分,范围是0xDC00~0xDFFF
//用高代理部分,低代理部分两个字符(不合法码位),最终经过java处理之后
//会出现一个合法码位的字符'𐐀'
char a[] = {0xD801,0xDC00,97};
String string7 = new String(a);
System.out.println(string7.codePointAt(0));//返回string7中第一个字符在unicode中的对应的int值
System.out.println(string7.codePointAt(1));//返回string7中第二个字符在unicode中的对应的int值
System.out.println(string7.codePointAt(2));//返回string7中第三个字符在unicode中的对应的int值
System.out.println(string7);
输出结果:
66560
56320
97
𐐀a
二、正则表达式
String类中常用到正则表达式,使用方式我把它放到我的另一篇文章中,可作参考(java中使用正则表达式)。
参考资料: