String类(自己的一些总结)
今天在CSDN上看到了一道关于String类的demo,让我意识到是时候好好的研究一下String类了,似乎对它的操作总能产生一些出乎意料的输出结果,于是在网上找了些资料并结合自己遇到的题目进行了一些总结,欢迎指正其中的错误。题目如下:
public static void main(String[] args) {
StringBuffer sb1 = new StringBuffer("Hello");
StringBuffer sb2 = new StringBuffer("World");
change(sb1,sb2);
System.out.println(sb1 + " " + sb2);
String s1 = "Hello";
String s2 = "World";
change(s1,s2);
String s3 = s1;
s3 = s2;
System.out.println(s1 + " " + s2);
StringBuilder ssss = new StringBuilder("123");
ssss.toString();
}
public static void change(StringBuffer sb1, StringBuffer sb2) {
sb1 = sb2;
sb1.append(sb2);
}
public static void change(String s1, String s2) {
s1 = "123";
s2 = s1 + s2;
}
最后的结果为:Hello WorldWorld
Hello World
如果你对这个结果感到很疑惑,那么请你看完这篇文章,因为你对String的理解真的还不够透彻。
String的不可变
大多数人都知道String是不可变的,但多数也仅停留于此,并不能深刻的理解这句话。请看如下代码:
String s = "Hello";
s = "hello";
System.out.println(s);
输出结果:hello
相信我们都能推测这个结果,但是String不是不可变的吗?而这里我们给输出s却变成了hello,那是不是就说明String是可变的呢?这里所说的不可变指的是创建的"Hello"字符串是在内存中不可变的。画一个简图:
当"Hello"在堆中被创建之后,就不可变,只能改变s所存的地址值,才能完成s="hello"的赋值。
String作为函数参数传递
而为初步理解上面的那道题,单纯的了解String的不可变性是不够的,我们还要知到String在作为函数值传递时发生了什么,其实,String与基本数据类型作为参数传递时,传递的只是值的一个副本,在函数中对数据进行操作,不会改变原有的值,改变的只是副本值。就以这道题为例子,它的简单内存分析图如下:
仅看Stirng类的操作
1.当为调用change方法时:
2.当开始运行change方法时:
函数参数传递,就是将main方法中的s1,s2的地址值,传递给change方法中的s1,s2的地址值。
3.change方法的操作:
s1="123";和s2=s1+s2;要特别注意是对change方法的s1,s2操作。
4.结束调用:
最后你会惊奇的发现change方法就像一个挑梁小丑一样,一顿操作但是,对main方法的s1,s2没有造成任何“伤害”。
对于StringBuffer的操作则不用多说,与正常对象一致,这里不做详细解释。sb1=sb2,并不会改变堆内存中村的值,sb1.append(sb2);会改变堆中的值,因此输出发生改变。
java只支持值传递(面试题)
值得注意的事,java机制是只支持值传递,java认为,即使传递的是引用,也把他看成地址值看待,所以就是值传递,实际上可以说是引用传递了,只不过传递的是地址值。
到此,我们应该已经对String不可变形,有了一定的了解,我也因此对String类产生了浓厚的兴趣,于是我开始查找String类为什么具有不可变性。
String类不可变性(源码如何实现)
要理解String的不可变性,首先看一下String类中都有哪些成员变量。 在JDK1.6中,String的成员变量有以下几个:
/** 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;
/** Cache the hash code for the string */
private int hash; // Default to 0
在JDK1.7中,String类做了一些改动,主要是改变了substring方法执行时的行为,这和本文的主题不相关。JDK1.7中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
在在JDK1.9后,String类又做了一些改动,将字符串使用字节数组存储了,当然这部影响研究其不可变性的原理
@Stable
private final byte[] value;
/*
* LATIN1
* UTF16
*/
private final byte coder;
private int hash; // Default to 0
private boolean hashIsZero; // Default to false;
static final boolean COMPACT_STRINGS;
static {
COMPACT_STRINGS = true;
}
由以上的代码可以看出, 在Java中String类其实就是对字符数组的封装。
value,offset和count这三个变量都是private的,并且没有提供setValue, setOffset和setCount等公共方法来修改这些值,所以在String类的外部无法修改String。也就是说一旦初始化就不能修改, 并且在String类的外部不能访问这三个成员。此外,value,offset和count这三个变量都是final的, 也就是说在String类内部,一旦这三个值初始化了, 也不能被改变。所以可以认为String对象是不可变的了。
总结:
设计 | 作用 |
---|---|
主要字段都是private的 | String类的外部无法直接修改String |
没有提供setXXX等公共方法来修改这些值 | String类的外部无法间接修改String |
主要字段都是final的 | String类内部也不能被改变 |
类被final修饰 | 无法继承覆写String类中的方法 |
当然上面的讨论,均是在不考率反射的前提下,其实我们可以利用反射来直接改变String类堆内存的值:
public class Stringdemo {
public static void main(String[] args) throws Exception {
//创建字符串"Hello World", 并赋给引用s
String s = "Hello World";
System.out.println("s = " + s); //Hello World
//获取String类的反射对象
Class<String> clazz = String.class;
//获取String类中的value字段
Field valueFieldOfString = clazz.getDeclaredField("value");
//改变value属性的访问权限
valueFieldOfString.setAccessible(true);
//获取s对象上的value属性的值
byte[] value = (byte[]) valueFieldOfString.get(s);
//改变value所引用的数组中的第5个字符
value[5] = '_';
System.out.println("s = " + s); //Hello_World
}
}
结果:
s = Hello World
s = Hello_World
可以通过结果看到,我们改变了String的值,笔者用的是JDK9以上,如果使用jDK9以下版本,应将byte[] value = (byte[]) valueFieldOfString.get(s); 改为char[] value = (char[]) valueFieldOfString.get(s);即可运行。
————————————————
引用了:
CSDN博主「昨夜星辰_zhangjg」的原创文章
原文链接:https://blog.csdn.net/zhangjg_blog/article/details/18319521
为什么java要将String设为不可变
1.字符串常量池的需要
字符串常量池(String pool, String intern pool) 是Java堆内存中一个特殊的存储区域, 当创建一个String对象时,假如此字符串值已经存在于常量池中,则不会创建一个新的对象,而是引用已经存在的对象。
字符串池的实现可以在运行时节约很多heap空间,因为不同的字符串变量都指向池中的同一个字符串。
如下面的代码所示,将会在堆内存中只创建一个实际String对象。
String s1 = "Hello";
String s2 = "Hello";
String s3 = "Hel"+"lo";
- 注:对于s3,在编译时就会将"Hel"+"lo"自动相连,并在常量池中查找
可画出内存图:
2.允许String对象缓存HashCode
Java中String对象的哈希码被频繁地使用, 比如在hashMap 等容器中。
字符串不变性保证了hash码的唯一性,因此可以放心地进行缓存,这也是一种性能优化手段,意味着不必每次都去计算新的哈希码。这就使得字符串很适合作为Map中的键,字符串的处理速度要快过其它的键对象。这就是HashMap中的键往往都使用字符串。
在String类的定义中有如下代码:
private int hash;//用来缓存HashCode
3.安全性
String被许多的Java类(库)用来当做参数,例如 网络连接地址URL,文件路径path,还有反射机制所需要的String参数等, 假若String不是固定不变的,将会引起各种安全隐患。
类加载器要用到字符串,不可变性提供了安全性,以便正确的类被加载。譬如你想加载java.sql.Connection类,而这个值被改成了其他类,那么会对你的数据库造成不可知的破坏。
引用了:
CSDN博主「不能说的秘密go」的原创文章
原文链接:https://blog.csdn.net/canot/article/details/103754370
其实除了不可变性,String类还有一些其他很有意思(面试题会考)的知识,如上面提到的字符串常量池。
(equals,==)针对于String
==: 比较的是两个String对象的地址值是否相同
equals:比较的是两个String对象的值是否相同
我们知道,对于Object类中的equals方法,equals(),==两者等价,之所以String类这么"典型"是因为String类重写了equals方法,上源码:
public boolean equals(Object anObject) {
if (this == anObject) {
return true;
}
if (anObject instanceof String) {
String aString = (String)anObject;
if (!COMPACT_STRINGS || this.coder == aString.coder) {
return StringLatin1.equals(value, aString.value);
}
}
return false;
}
//StringLatin1.equals()
public static boolean equals(byte[] value, byte[] other) {
if (value.length == other.length) {
for (int i = 0; i < value.length; i++) {
if (value[i] != other[i]) {
return false;
}
}
return true;
}
return false;
}
简单来讲,判断步骤为:
判断两者地址值是否相同
判断要比较的对象是否属于String类
-
if (!COMPACT_STRINGS || this.coder == aString.coder),这里是关于jdk9的新特性,String类存在了byte[]中,coder存的是字符串的编码集,COMPACT_STRINGS默认为true,表示这种特性为开启状态。
//源码 static final boolean COMPACT_STRINGS; static { COMPACT_STRINGS = true; }
扩展:
coder方法(源码)判断COMPACT_STRINGS为true的话,则返回coder值,否则返回UTF16;isLatin1方法(源码)判断COMPACT_STRINGS为true且coder为LATIN1(ISO-8859-1)则返回true。
诸如charAt、equals、hashCode、indexOf、substring等等一系列方法都依赖isLatin1方法来区分对待是StringLatin1还是StringUTF16。
判断内容长度是否相等
-
判断内容是否相等
注:除了第3条,这些通常也是我们重写equals的判断步骤。
字符串常量池
先上题目:
String s1 = "hello";
String s2 = "hello";
String s3 = new String("hello");
String s4 = "hel"+"lo";
String s5 = "hel"+new String("lo");
String s6a = "hel";
String s6b = "lo";
String s6 = s6a+s6b;
//下列判断结果分别为true还是false
/*
s1 == s2;
s1 == s3;
s1 == s4;
s1 == s5;
s1 == s6;
s1.equals(s3);
s1.equals(s5);
s1.equals(s6);
答:
true
false
true
false
false
true
true
true
*/
我感觉把这些搞懂,再来做考察字符串常量池的面试题只要认真思考都能拿下。
1.对于s1 == s2 == s4;在上面介绍为什么java要将String设为不可变时已经说过,在此不再缀叙。
2.对于s3,使用new方式初始化字符串,会在堆上分配一个存放字符串对象的空间,然后查找常量池中是否存在内容为 “hello” 的字符串对象,如果存在则返回引用,不存在则创建再返回引用,此时内存分配情况为:
番外知识:public String intern(); //手动入池。
3.再解释s5,s6我们先要了解一下“+”,理解它作为Java的字符串连接符,所做的操作。当我们看到String s5 = "hel"+new String("lo");时一定能知道s5 = hello,因为我们能非常直观的理解连接的意思,却往往没有想过,+是如何实现连接的。
让我们看看编译器是怎么处理 + 操作符的:
例:
String str0 = "a";
String str1 = str0 + "b";
下面是这一个程序片段编译后的字节码指令:
public static void main(java.lang.String[]);
Code:
0: ldc #2 // String a
2: astore_1
3: new #3 // class java/lang/StringBuilder
6: dup
7: invokespecial #4 // Method java/lang/StringBuilder."<init>":()V
10: aload_1
11: invokevirtual #5 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
14: ldc #6 // String b
16: invokevirtual #5 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
19: invokevirtual #7 // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
22: astore_2
23: return
(画外音)作为一个菜鸡,我是没太看懂。
如上可知:+拼接操作实际上被编译器理解成了这个样子:
String str0 = "a";
StringBuilder sb = new StringBuilder();
sb.append(str0).append("b");
String str1 = sb.toString();
再看StringBuilder的toString方法源码:
public String toString() {
// Create a copy, don't share the array
return isLatin1() ? StringLatin1.newString(value, 0, count)
: StringUTF16.newString(value, 0, count);
}
//以StringLatin1.newString(value, 0, count)为例
public static String newString(byte[] val, int index, int len) {
if (len == 0) {
return "";
}
return new String(Arrays.copyOfRange(val, index, index + len),
LATIN1);
}
看到newString方法里使用构造方法即new String()初始化字符串,我相信你应该明白s5,s6为何与s1做 == 判断会为false了,其实他们都隐式的调用了构造函数(个人的理解)生成字符串与s3一样会在堆内存中开辟了两处内存空间。
再此注意String s4 = "hel"+"lo";在编译时就会自动被理解为s4 = "hello";
字符串池的优缺点
优点就是避免了相同内容的字符串的创建,节省了内存,省去了创建相同字符串的时间,同时提升了性能;
缺点就是牺牲了JVM在常量池中遍历对象所需要的时间,不过其时间成本相比而言比较低。
这些是我通过看别人的博客,和结合自己的理解总结的知识点,以后关于String、StringBuffer、StringBuilder的相关知识还会持续更新在这里,未完待续。。。