今天我们一起看一下Java基础类:String
public final class String implements java.io.Serializable, Comparable<String>, CharSequence { /**用来存储字符串 */ private final char value[]; /** 缓存字符串的哈希码 */ private int hash; // Default to 0 /** 实现序列化的标识 */ private static final long serialVersionUID = -6849794470754667710L;}
这是一个用 final 声明的常量类,不能被任何类所继承,而且一旦一个String对象被创建, 包含在这个对象中的字符序列是不可改变的, 包括该类后续的所有方法都是不能修改该对象的,直至该对象被销毁,这是我们需要特别注意的(该类的一些方法看似改变了字符串,其实内部都是创建一个新的字符串,下面讲解方法时会介绍)。
通过上述代码可以发现,一个 String 字符串实际上是一个 char 数组。
//注意这种字面量声明的区别String str1 = "abc";String str2 = new String("abc");
那么这两种声明方式有什么区别呢?在讲解之前,我们先介绍 JDK1.7(不包括1.7)以前的 JVM 的内存分布:
图片
在 JDK1.7 以后,方法区的常量池被移除放到堆中了,如下:
图片
常量池:Java运行时会维护一个String Pool(String池), 也叫“字符串缓冲区”。String池用来存放运行时中产生的各种字符串,并且池中的字符串的内容不重复。
public class stringclass { public static void main(String[] args) { String str="Hello"; String str2="Hello"; System.out.println(str==str2); str="World"; }}//输出结果:true
图片
public class stringclass { public static void main(String[] args) { String str= new String("Hello"); String str2= new String("Hello"); String str3 = "Hello"; System.out.println(str==str2); System.out.println(str==str3); }} //输出结果:false false
public class stringclass { public static void main(String[] args) { //当一个字符串由多个字符串常量连接而成时,它自己肯定也是字符串常量。 //该字符串是在编译期就能确定。先是在池里生成“a”和“b”,再通过拼接的方式在池里生成"ab"。 String str="Hello" + "World"; }}
图片
当使用了变量字符串的拼接(+, sb.append)都只会在堆区创建该字符串对象, 并不会在常量池创建新生成的字符串
public class stringclass { public static void main(String[] args) { String str=new String("Hello") + new String("World"); }}
图片
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;}
String 类重写了 equals 方法,比较的是组成字符串的每一个字符是否相同,如果都相同则返回true,否则返回false。
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;}
String 类的 hashCode 算法很简单,主要就是中间的 for 循环,计算公式如下:
s[0]*31^(n-1) + s[1]*31^(n-2) + ... + s[n-1]
s 数组即源码中的 val 数组,也就是构成字符串的字符数组。这里有个数字 31 ,为什么选择31作为乘积因子,而且没有用一个常量来声明?主要原因有两个:
public char charAt(int index) { //如果传入的索引大于字符串的长度或者小于0,直接抛出索引越界异常 if ((index < 0) || (index >= value.length)) { throw new StringIndexOutOfBoundsException(index); } return value[index];//返回指定索引的单个字符}
我们知道一个字符串是由一个字符数组组成,这个方法是通过传入的索引(数组下标),返回指定索引的单个字符。
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;}
源码也很好理解,该方法是按字母顺序比较两个字符串,是基于字符串中每个字符的 Unicode 值。当两个字符串某个位置的字符不同时,返回的是这一位置的字符 Unicode 值之差,当两个字符串都相同时,返回两个字符串长度之差。
compareToIgnoreCase() 方法在 compareTo 方法的基础上忽略大小写,我们知道大写字母是比小写字母的Unicode值小32的,底层实现是先都转换成大写比较,然后都转换成小写进行比较。
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);}
该方法是将指定的字符串连接到此字符串的末尾。
首先判断要拼接的字符串长度是否为0,如果为0,则直接返回原字符串。如果不为0,则通过 Arrays 工具类的copyOf方法创建一个新的字符数组,长度为原字符串和要拼接的字符串之和,前面填充原字符串,后面为空。接着在通过 getChars 方法将要拼接的字符串放入新字符串后面为空的位置。
注意:返回值是 new String(buf, true),也就是重新通过 new 关键字创建了一个新的字符串,原字符串是不变的。这也是前面我们说的一旦一个String对象被创建, 包含在这个对象中的字符序列是不可改变的。
public int indexOf(int ch) { return indexOf(ch, 0);//从第一个字符开始搜索 } public int indexOf(int ch, int fromIndex) { final int max = value.length;//max等于字符的长度 if (fromIndex < 0) {//指定索引的位置如果小于0,默认从 0 开始搜索 fromIndex = 0; } else if (fromIndex >= max) { //如果指定索引值大于等于字符的长度(因为是数组,下标最多只能是max-1),直接返回-1 return -1; } if (ch < Character.MIN_SUPPLEMENTARY_CODE_POINT) {//一个char占用两个字节,如果ch小于2的16次方(65536),绝大多数字符都在此范围内 final char[] value = this.value; for (int i = fromIndex; i < max; i++) {//for循环依次判断字符串每个字符是否和指定字符相等 if (value[i] == ch) { return i;//存在相等的字符,返回第一次出现该字符的索引位置,并终止循环 } } return -1;//不存在相等的字符,则返回 -1 }else {//当字符大于 65536时,处理的少数情况,该方法会首先判断是否是有效字符,然后依次进行比较 return indexOfSupplementary(ch, fromIndex); }}
indexOf(int ch),参数 ch 其实是字符的 Unicode 值,这里也可以放单个字符(默认转成int),作用是返回指定字符第一次出现的此字符串中的索引。其内部是调用 indexOf(int ch, int fromIndex),只不过这里的 fromIndex =0 ,因为是从 0 开始搜索;而 indexOf(int ch, int fromIndex) 作用也是返回首次出现的此字符串内的索引,但是从指定索引处开始搜索。
public String substring(int beginIndex) { if (beginIndex < 0) {//如果索引小于0,直接抛出异常 throw new StringIndexOutOfBoundsException(beginIndex); } int subLen = value.length - beginIndex;//subLen等于字符串长度减去索引 if (subLen < 0) {//如果subLen小于0,也是直接抛出异常 throw new StringIndexOutOfBoundsException(subLen); } //1、如果索引值beginIdex == 0,直接返回原字符串 //2、如果不等于0,则返回从beginIndex开始,一直到结尾 return (beginIndex == 0) ? this : new String(value, beginIndex, subLen);}
返回一个从索引 beginIndex 开始一直到结尾的子字符串。
String 类为什么要这样设计成不可变呢?我们可以从性能以及安全方面来考虑:
引发安全问题,譬如,数据库的用户名、密码都是以字符串的形式传入来获得数据库的连接,或者在socket编程中,主机名和端口都是以字符串的形式传入。因为字符串是不可变的,所以它的值是不可改变的,否则黑客们可以钻到空子,改变字符串指向的对象的值,造成安全漏洞。
保证线程安全,在并发场景下,多个线程同时读写资源时,会引竞态条件,由于 String 是不可变的,不会引发线程的问题而保证了线程。
HashCode,当 String 被创建出来的时候,hashcode也会随之被缓存,hashcode的计算与value有关,若 String 可变,那么 hashcode 也会随之变化,针对于 Map、Set 等容器,他们的键值需要保证唯一性和一致性,因此,String 的不可变性使其比其他对象更适合当容器的键值。
本文链接:http://www.28at.com/showinfo-26-82039-0.htmlJava中的String,这一篇就够了
声明:本网页内容旨在传播知识,若有侵权等问题请及时与本网联系,我们将在第一时间删除处理。邮件:2376512515@qq.com