String 表示 字符串类型,属于 引用数据类型 。Java 中 String 是 不可变 的。
在 Java 当中 双引号 括起来的字符串,是直接存储在“方法区”的“字符串常量池”当中的。
1. 构造方法
1.1 String()
源码:
/**
初始化新创建的字符串对象,使其表示空字符序列。
请注意,由于字符串是不可变的,因此不需要使用此构造函数。
*/
public String() {
this.value = "".value;
}
1.2 String(String original)
源码:
/**
初始化新创建的字符串对象,使其表示与参数相同的字符序列;
换句话说,新创建的字符串是参数字符串的副本。
除非需要original的显式副本,否则不需要使用此构造函数,因为字符串是不可变的。
*/
public String(String original) {
this.value = original.value;
this.hash = original.hash;
}
1.3 String(char[] chars)
源码:
/**
分配一个新字符串,使其表示字符数组参数中当前包含的字符序列。
复制字符数组的内容;对字符数组的后续修改不会影响新创建的字符串。
参数:
value–字符串的初始值
*/
public String(char value[]) {
this.value = Arrays.copyOf(value, value.length);
}
1.4 String(char数组,起始下标,长度)
源码:
/**
分配一个新字符串,该字符串包含字符数组参数子数组中的字符。
offset参数是子数组第一个字符的索引,count参数指定子数组的长度。
复制子数组的内容;对字符数组的后续修改不会影响新创建的字符串。
参数:
value–作为字符源的数组
偏移量–初始偏移量
计数–长度
抛出:
IndexOutOfBoundsException–如果偏移量和计数参数索引字符超出值数组的边界
*/
public String(char value[], int offset, int count) {
if (offset < 0) {
throw new StringIndexOutOfBoundsException(offset);
}
if (count <= 0) {
if (count < 0) {
throw new StringIndexOutOfBoundsException(count);
}
if (offset <= value.length) {
this.value = "".value;
return;
}
}
// 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);
}
1.5 String(byte数组)
源码:
/**
通过使用平台的默认字符集对指定的字节数组进行解码来构造新字符串。
新字符串的长度是字符集的函数,因此可能不等于字节数组的长度。
当给定字节在默认字符集中无效时,此构造函数的行为未指定。
当需要对解码过程进行更多控制时,应使用CharsetDecoder类。
参数:
字节–要解码为字符的字节
Since: JDK1.1
*/
public String(byte bytes[]) {
this(bytes, 0, bytes.length);
}
1.6 String(byte数组,起始下标,长度)
源码:
/**
通过使用平台的默认字符集对指定的字节子数组进行解码,构造一个新字符串。
新字符串的长度是字符集的函数,因此可能不等于子数组的长度。
当给定字节在默认字符集中无效时,此构造函数的行为未指定。
当需要对解码过程进行更多控制时,应使用CharsetDecoder类。
参数:
字节–要解码为字符的字节
偏移量–要解码的第一个字节的索引
长度–要解码的字节数
抛出:
IndexOutOfBoundsException–如果偏移量和长度参数索引字符超出字节数组的边界
Since: JDK1.1
*/
public String(byte bytes[], int offset, int length) {
// checkBounds(bytes, offset, length);
if (length < 0)
throw new StringIndexOutOfBoundsException(length);
if (offset < 0)
throw new StringIndexOutOfBoundsException(offset);
if (offset > bytes.length - length)
throw new StringIndexOutOfBoundsException(offset + length);
this.value = StringCoding.decode(bytes, offset, length);
}
1.7 String(StringBuffer buffer)
源码:
/**
分配一个新字符串,该字符串包含字符串缓冲区参数中当前包含的字符序列。
复制字符串缓冲区的内容;
字符串缓冲区的后续修改不会影响新创建的字符串。
*/
synchronized(buffer) {
this.value = Arrays.copyOf(buffer.getValue(), buffer.length());
}
1.8 String(StringBuilder builder)
源码:
/**
分配一个新字符串,该字符串包含字符串生成器参数中当前包含的字符序列。
复制字符串生成器的内容;
字符串生成器的后续修改不会影响新创建的字符串。
提供此构造函数是为了方便迁移到StringBuilder。
通过toString方法从字符串生成器获取字符串可能运行得更快,通常是首选方法。
Since:JDK1.5
*/
public String(StringBuilder builder) {
this.value = Arrays.copyOf(builder.getValue(), builder.length());
}
2. 普通方法
https://blog.csdn.net/qq_44715943/article/details/116308837
2.1 char charAt(int index)
源码:
/**
返回指定索引处的字符值。索引的范围从0到length()-1。
序列的第一个字符值在索引0处,下一个字符值在索引1处,依此类推,就像数组索引一样。
如果索引指定的char值是代理项,则返回代理项值。
参数:index–字符值的索引。
返回:此字符串指定索引处的char值。第一个char值位于索引0处。
抛出:IndexOutOfBoundsException–如果索引参数为负或不小于此字符串的长度。
*/
public char charAt(int index) {
if ((index < 0) || (index >= value.length)) {
throw new StringIndexOutOfBoundsException(index);
}
return value[index];
}
2.2 int compareTo(String anotherString)
源码:
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;
}
2.3 int indexOf(String str, int fromIndex)
源码:
/**
返回指定子字符串第一次出现的字符串内的索引,从指定的索引开始。
返回的索引是最小的值k,其中:
k>=fromIndex&&this。startsWith(str,k)
如果不存在这样的k值,则返回-1。
参数:
str–要搜索的子字符串。
fromIndex–开始搜索的索引。
返回:指定子字符串第一次出现的索引,从指定的索引开始,如果没有出现,则为-1。
*/
public int indexOf(String str, int fromIndex) {
return indexOf(value, 0, value.length,
str.value, 0, str.value.length, fromIndex);
}
2.4 int indexOf(String str)
源码:
/**
返回指定子字符串第一次出现时该字符串内的索引。
返回的索引是最小的值k,其中:
这startsWith(str,k)
如果不存在这样的k值,则返回-1。
参数:str–要搜索的子字符串。
返回:指定子字符串第一次出现的索引,如果没有出现,则为-1。
*/
public int indexOf(String str) {
return indexOf(str, 0);
}
2.5 boolean contains(CharSequence s)
源码:
/**
当且仅当此字符串包含指定的字符值序列时,返回true。
参数:s–要搜索的序列
返回:如果此字符串包含s,则为true,否则为false
Since:1.5
*/
public boolean contains(CharSequence s) {
return indexOf(s.toString()) > -1;
}
2.6 boolean startsWith(String prefix, int toffset)
源码:
/**
测试此字符串中从指定索引开始的子字符串是否以指定前缀开始。
参数:
prefix–前缀。
toffset–从何处开始查看此字符串。
返回:
如果参数所表示的字符序列是从索引toffset开始的该对象的子字符串的前缀,则为true;否则就错了。
如果toffset为负值或大于该字符串对象的长度,则结果为false;否则,结果与表达式的结果相同
这子串(toffset)。startsWith(前缀)
*/
public boolean startsWith(String prefix, int toffset) {
char ta[] = value;
int to = toffset;
char pa[] = prefix.value;
int po = 0;
int pc = prefix.value.length;
// Note: toffset might be near -1>>>1.
if ((toffset < 0) || (toffset > value.length - pc)) {
return false;
}
while (--pc >= 0) {
if (ta[to++] != pa[po++]) {
return false;
}
}
return true;
}
2.7 boolean startsWith(String prefix)
源码:
public boolean startsWith(String prefix) {
return startsWith(prefix, 0);
}
2.8 boolean endsWith(String suffix)
源码:
public boolean endsWith(String suffix) {
return startsWith(suffix, value.length - suffix.value.length);
}
2.9 boolean equals(Object anObject)
源码:
/**
*/
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;
}
2.10 boolean equalsIgnoreCase(String anotherString)
源码:
public boolean equalsIgnoreCase(String anotherString) {
return (this == anotherString) ? true
: (anotherString != null)
&& (anotherString.value.length == value.length)
&& regionMatches(true, 0, anotherString, 0, value.length);
}
2.11 char[] toCharArray()
2.12 String substring(int beginIndex)
从传入的索引处截取,截取到末尾,得到新的字符串
2.13 String substring(int beginIndex, int endIndex)
根据开始和结束索引进行截取,得到新的字 符串(包含头,不包含尾)
2.14 String replace(CharSequence target, CharSequence replacement)
使用新值,将字符串中 的旧值替换,得到新的字符串
2.15 String[] split(String regex)
根据传入的规则切割字符串,得到字符串数组
3. 常见面试题
Java 程序中所有的双引号字符串,都是 String 类的对象
字符串不可变,它们的值在创建后不能被更改
虽然 String 的值是不可变的,但是它们可以被共享
问题:下列代码的运行结果是?
public class Test1{
public static void main(String[] args){
String s1="abc";
String s2="abc";
System.out.println(s1==s2);
}
}
分析:都在常量池中
问题:下列代码的运行结果是?
public class Test2{
public static void main(String[] args){
String s1="abc";
String s2=new String("abc"); //创建两个对象
System.out.println(s1==s2);
}
}
分析: s1在常量池 , s2 在 堆内存中
问题:下列代码的运行结果是?
public class Test3 {
public static void main(String[] args) {
String s1 = "abc";
String s2 = "ab";
String s3 = s2 + "c";
System.out.println(s1 == s3);
}
}
字符串对象使用"+" 拼接 会先转成StringBuildder 在进行append方法 最后toString方法 转成String对象
问题:下列代码的运行结果是?
public class Test4 {
public static void main(String[] args) {
String s1 = "abc";
String s2 = "a" + "b" + "c";
System.out.println(s1 == s2);
}
}
分析
- 常量优化机制
- 常量与常量之间运算 直接算结果 再赋值
举例:
String s1 = "abc";
String s2 = "abc";
// s1 == s2 // true
String s1 = "abc";
String s2 = new String("abc");
// s1 == s2 // false
String s1 = "java";
String s2 = "jav";
String s3 = s2 + "a"; // 变量 + 常量
// s1 == s3 // false 字符串之间使用+号拼接的时候,系统底层会自创建一个StringBuilder对
//象,拼接后再转String
String s1 = "java";
String s2 = "jav" + "a"; //常量 + 常量
// s1 == s2 // true 虽然是+拼接,但是是常量在拼接,常量优化机制
4. 深入理解
:::info
在Java语言中,所有类似“ABC”的字面值,都是String类的实例;String类位于java.lang包下,是Java语言的核心类,提供了字符串的比较、查找、截取、大小写转换等操作;Java语言为“+”连接符(字符串连接符)以及对象转换为字符串提供了特殊的支持,字符串对象可以使用“+”连接其他对象。String类的部分源码如下:
:::
public final class String
implements java.io.Serializable, Comparable<String>, CharSequence {
/** The value is used for character storage. */
private final char value[];
/** Cache the hash code for the string */
private int hash; // Default to 0
...
}
从上面可以看出
1)String类被final关键字修饰,意味着String类不能被继承,并且它的成员方法都默认为final方法;字符串一旦创建就不能再修改。
2)String类实现了Serializable、CharSequence、 Comparable接口。
3)String实例的值是通过字符数组实现字符串存储的。
4.1“+”连接符
4.1.1“+”连接符的实现原理
Java语言为“+”连接符以及对象转换为字符串提供了特殊的支持,字符串对象可以使用“+”连接其他对象。其中字符串连接是通过 StringBuilder(或 StringBuffer)类及其append 方法实现的,对象转换为字符串是通过 toString 方法实现的,该方法由 Object 类定义,并可被 Java 中的所有类继承。
我们可以通过反编译验证一下
/**
* 测试代码
*/
public class Test {
public static void main(String[] args) {
int i = 10;
String s = "abc";
System.out.println(s + i);
}
}
/**
* 反编译后
*/
public class Test {
public static void main(String args[]) { //删除了默认构造函数和字节码
byte byte0 = 10;
String s = "abc";
System.out.println((new StringBuilder()).append(s).append(byte0).toString());
}
}
由上可以看出,Java中使用"+"连接字符串对象时,会创建一个StringBuilder()对象,并调用append()方法将数据拼接,最后调用toString()方法返回拼接好的字符串。由于append()方法的各种重载形式会调用String.valueOf方法,所以我们可以认为:
//以下两者是等价的
s = i + ""
s = String.valueOf(i);
//以下两者也是等价的
s = "abc" + i;
s = new StringBuilder("abc").append(i).toString();
4.1.2“+”连接符的效率
使用“+”连接符时,JVM会隐式创建StringBuilder对象,这种方式在大部分情况下并不会造成效率的损失,不过在进行大量循环拼接字符串时则需要注意。
String s = "abc";
for (int i=0; i<10000; i++) {
s += "abc";
}
/**
* 反编译后
*/
String s = "abc";
for(int i = 0; i < 1000; i++) {
s = (new StringBuilder()).append(s).append("abc").toString();
}
这样由于大量StringBuilder创建在堆内存中,肯定会造成效率的损失,所以在这种情况下建议在循环体外创建一个StringBuilder对象调用append()方法手动拼接(如上面例子如果使用手动拼接运行时间将缩小到1/200左右)。
/**
* 循环中使用StringBuilder代替“+”连接符
*/
StringBuilder sb = new StringBuilder("abc");
for (int i = 0; i < 1000; i++) {
sb.append("abc");
}
sb.toString();
与此之外还有一种特殊情况,也就是当"+"两端均为编译期确定的字符串常量时,编译器会进行相应的优化,直接将两个字符串常量拼接好,例如:
System.out.println("Hello" + "World");
/**
* 反编译后
*/
System.out.println("HelloWorld");
/**
* 编译期确定
* 对于final修饰的变量,它在编译时被解析为常量值的一个本地拷贝存储到自己的常量池中或嵌入到它的字节码流中。
* 所以此时的"a" + s1和"a" + "b"效果是一样的。故结果为true。
*/
String s0 = "ab";
final String s1 = "b";
String s2 = "a" + s1;
System.out.println((s0 == s2)); //result = true
/**
* 编译期无法确定
* 这里面虽然将s1用final修饰了,但是由于其赋值是通过方法调用返回的,那么它的值只能在运行期间确定
* 因此s0和s2指向的不是同一个对象,故上面程序的结果为false。
*/
String s0 = "ab";
final String s1 = getS1();
String s2 = "a" + s1;
System.out.println((s0 == s2)); //result = false
public String getS1() {
return "b";
}
综上,“+”连接符对于直接相加的字符串常量效率很高,因为在编译期间便确定了它的值,也就是说形如"I"+“love”+“java”; 的字符串相加,在编译期间便被优化成了"Ilovejava"。对于间接相加(即包含字符串引用,且编译期无法确定值的),形如s1+s2+s3; 效率要比直接相加低,因为在编译器不会对引用变量进行优化。
4.2 字符串常量池
在Java的内存分配中,总共3种常量池,分别是 Class常量池、运行时常量池、字符串常量池。
字符串的分配和其他对象分配一样,是需要消耗高昂的时间和空间的,而且字符串使用的非常多。JVM为了提高性能和减少内存的开销,在实例化字符串的时候进行了一些优化:使用字符串常量池。每当创建字符串常量时,JVM会首先检查字符串常量池,如果该字符串已经存在常量池中,那么就直接返回常量池中的实例引用。如果字符串不存在常量池中,就会实例化该字符串并且将其放到常量池中。由于String字符串的不可变性,常量池中一定不存在两个相同的字符串。
/**
* 字符串常量池中的字符串只存在一份!
* 运行结果为true
*/
String s1 = "hello world!";
String s2 = "hello world!";
System.out.println(s1 == s2);
4.2.1 内存区域
在HotSpot VM中字符串常量池是通过一个StringTable类实现的,它是一个Hash表,默认值大小长度是1009;这个StringTable在每个HotSpot VM的实例中只有一份,被所有的类共享;字符串常量由一个一个字符组成,放在了StringTable上。要注意的是,如果放进String Pool的String非常多,就会造成Hash冲突严重,从而导致链表会很长,而链表长了后直接会造成的影响就是当调用String.intern时性能会大幅下降(因为要一个一个找)。
在JDK6及之前版本,字符串常量池是放在Perm Gen区(也就是方法区)中的,StringTable的长度是固定的1009;在JDK7版本中,字符串常量池被移到了堆中,StringTable的长度可以通过**-XX:StringTableSize=66666**参数指定。至于JDK7为什么把常量池移动到堆上实现,原因可能是由于方法区的内存空间太小且不方便扩展,而堆的内存空间比较大且扩展方便。
4.2.2 存放的内容
在JDK6及之前版本中,String Pool里放的都是字符串常量;在JDK7.0中,由于String.intern()发生了改变,因此String Pool中也可以存放放于堆内的字符串对象的引用。
String s1 = "AB";
String s2 = "AB";
String s3 = new String("AB");
System.out.println(s1 == s2);
System.out.println(s1 == s3);
由于常量池中不存在两个相同的对象,所以s1和s2都是指向JVM字符串常量池中的"AB"对象。new关键字一定会产生一个对象,并且这个对象存储在堆中。所以String s3 = new String(“AB”);产生了两个对象:保存在栈中的s3和保存堆中的String对象。
当执行String s1 = "AB"时,JVM首先会去字符串常量池中检查是否存在"AB"对象,如果不存在,则在字符串常量池中创建"AB"对象,并将"AB"对象的地址返回给s1;如果存在,则不创建任何对象,直接将字符串常量池中"AB"对象的地址返回给s1。
4.2.3 intern 方法
直接使用双引号声明出来的String对象会直接存储在字符串常量池中,如果不是用双引号声明的String对象,可以使用String提供的intern方法。intern 方法是一个native方法,intern方法会从字符串常量池中查询当前字符串是否存在,如果存在,就直接返回当前字符串;如果不存在就会将当前字符串放入常量池中,之后再返回。
JDK1.7的改动:
- 将String常量池 从 Perm 区移动到了 Java Heap区
- String.intern() 方法时,如果存在堆中的对象,会直接保存对象的引用,而不会重新创建对象。
intern的用法
static final int MAX = 1000 * 10000;
static final String[] arr = new String[MAX];
public static void main(String[] args) throws Exception {
Integer[] DB_DATA = new Integer[10];
Random random = new Random(10 * 10000);
for (int i = 0; i < DB_DATA.length; i++) {
DB_DATA[i] = random.nextInt();
}
long t = System.currentTimeMillis();
for (int i = 0; i < MAX; i++) {
//arr[i] = new String(String.valueOf(DB_DATA[i % DB_DATA.length]));
arr[i] = new String(String.valueOf(DB_DATA[i % DB_DATA.length])).intern();
}
System.out.println((System.currentTimeMillis() - t) + "ms");
System.gc();
}
未使用intern,耗时 826 ms
使用intern,耗时 2160 ms
通过上述结果,我们发现不使用 intern 的代码生成了1000w 个字符串,占用了大约640m 空间。 使用了 intern 的代码生成了1345个字符串,占用总空间 133k 左右。其实通过观察程序中只是用到了10个字符串,所以准确计算后应该是正好相差100w 倍。虽然例子有些极端,但确实能准确反应出 intern 使用后产生的巨大空间节省。
细心的同学会发现使用了 intern 方法后时间上有了一些增长。这是因为程序中每次都是用了 new String 后,然后又进行 intern 操作的耗时时间,这一点如果在内存空间充足的情况下确实是无法避免的,但我们平时使用时,内存空间肯定不是无限大的,不使用 intern 占用空间导致 jvm 垃圾回收的时间是要远远大于这点时间的。 毕竟这里使用了1000w次intern 才多出来1秒钟多的时间。
5. String、StringBuilder和StringBuffer
5.1 继承结构
5.2 主要区别
1)String是不可变字符序列,StringBuilder和StringBuffer是可变字符序列。
2)执行速度StringBuilder > StringBuffer > String。
3)StringBuilder是非线程安全的,StringBuffer是线程安全的
6. 总结
public static void main(String[] args) {
String s1 = "AB";
String s2 = new String("AB");
String s3 = "A";
String s4 = "B";
String s5 = "A" + "B";
String s6 = s3 + s4;
System.out.println(s1 == s2);
System.out.println(s1 == s5);
System.out.println(s1 == s6);
System.out.println(s1 == s6.intern());
System.out.println(s2 == s2.intern());
}
解析:真正理解此题目需要清楚以下三点
1)直接使用双引号声明出来的String对象会直接存储在常量池中;
2)String对象的intern方法会得到字符串对象在常量池中对应的引用,如果常量池中没有对应的字符串,则该字符串将被添加到常量池中,然后返回常量池中字符串的引用;
3) 字符串的+操作其本质是创建了StringBuilder对象进行append操作,然后将拼接后的StringBuilder对象用toString方法处理成String对象,这一点可以用javap -c命令获得class文件对应的JVM字节码指令就可以看出来。
运行结果:
7. 注意:
字符串 的比较 应该 使用 equals() 方法。
8. 参考文献
https://docs.oracle.com/javase/8/docs/api/
https://blog.csdn.net/sinat_19425927/article/details/38663461
https://www.cnblogs.com/xiaoxi/p/6036701.html
https://tech.meituan.com/in_depth_understanding_string_intern.html
文章来源:https://www.toymoban.com/news/detail-464641.html
文章来源地址https://www.toymoban.com/news/detail-464641.html
到了这里,关于【JavaSE】Java基础语法(三十七):Java 中的 String 类(源码级别)的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!