String
今天我们介绍 Java 中的字符串类型。
目录
首先需要清楚,Java 中的字符串和 C 语言不同,和 char[] 没有任何关系,Java 是通过一个专门的类 String 来实现,是 Java 标准库中的一个类,在 Java.lang 这个包中,在使用这个类的时候会自动导入。
1. 创建字符串
基本上有两种方式
a)
String str = "abcd";
b)
String str = new String("abcd");
这两者虽然结果相同,但是创建的过程有区别,方式 b) 中先在字符串常量池中创建了一个字符串 "abcd",然后通过该字符串又 new 出一个新的字符串,这个字符串的内容也是 "abcd" ,然后将常量池中的 "abcd" 通过垃圾回收机制删除。
2. 字符串比较相等
看下面一个代码:
String str1 = new String("Hello");
String str2 = new String("Hello");
System.out.println(str1 == str2);
结果为:false
这是为什么呢?因为实际上在非内部类里 == 并不是比较内容,而是比较引用是否指向了同一个对象,我们给出上面代码的内存布局图一切都明了了。
很明显,这里的 str1 和 str2 指向的不是一个对象,所以结果为 false。
所以如果要比较字符串的内容需要借助 equals 函数。
String str1 = new String("Hello");
String str2 = new String("Hello");
System.out.println(str1.equals(str2));
// System.out.println(str2.equals(str1)); // 或者这样写也行
此时的执行结果就为 :true。
需要注意一个小细节:
String str = new String("Hello");
// 方式一
System.out.println(str.equals("Hello"));
// 方式二
System.out.println("Hello".equals(str));
更建议使用方法二,因为当 str = null 时,方式一会抛出异常。
3. 字符串常量池
上面我们频繁提到字符串常量池,其实字符串常量池是 jvm 底层实现的一个自动维护从而减少内存浪费的对象池。
看下面一段代码:
String str1 = "hello" ;
String str2 = "hello" ;
String str3 = "hello" ;
System.out.println(str1 == str2); // true
System.out.println(str1 == str3); // true
System.out.println(str2 == str3); // true
这里的结果都为 true 因为实际上在堆上只有一份 "hello" ,三个引用同时指向该内存区域,我们给出内存分布图。
字符串常量池遵循下面的原则:
a) 如果现在采用了直接赋值的模式进行 String 类的对象实例化操作,那么该实例化对象(字符串内 容)将自动保存到这个对象池之中。
b) 如果下次继续使用直接赋值的模式声明String类对象,此时对象池之中如若有指定内容,将直接进行引用,如若没有,则开辟新的字符串对象而后将其保存在对象池之中以供下次使用。
上面是直接赋值,下面我们看使用构造方法的情况:
String str = new String("hello");
正如 1 中所说,方式 b) 中先在字符串常量池中创建了一个字符串 "abcd",然后通过该字符串又 new 出一个新的字符串,这个字符串的内容也是 "abcd" ,然后将常量池中的 "abcd" 通过垃圾回收机制删除。
这样的做法有两个缺点:
a) 如果使用 String 构造方法就会开辟两块堆内存空间,并且其中一块堆内存将成为垃圾空间(字符串常量 "hello" 也是一个匿名对象,用了一次之后就不再使用了, 就成为垃圾空间,会被 JVM 自动回收掉。
b) 字符串共享问题,同一个字符串可能会被存储多次,比较浪费空间。
4. 字符串主动入池
我们可以使用 String 的 intern 方法来手动把 String 对象加入到字符串常量池中。
// 该字符串常量并没有保存在对象池之中
String str1 = new String("hello");
String str2 = "hello";
System.out.println(str1 == str2);
// 执行结果
false
String str1 = new String("hello").intern();
String str2 = "hello";
System.out.println(str1 == str2);
// 执行结果
true
可以看出来,使用 intern 来手动入池的结果和直接赋值类似。
5. 字符串不可变,反射
Java 中的关键字 final 如果修饰引用代表引用的指向不可变,但是在字符串的操作中我们经常看见 String str = "hello"; str = "你好"; 这样的操作,可见 String 中的不可变和 final 关键字的引用指向不可变不是一个意思。
看下面一段代码:
String str = "hello";
str = str + " world";
str += "!!!";
System.out.println(str);
// 执行结果
hello world!!!
对字符串使用 + 或者 += 的操作,看似改变了字符串内容,实则不是。我们给出上面代码的内存图。
可以看见,在代码执行的过程中,引用 str 的指向不断被改变,最后指向的是 "hello world !!!",前面的 "hello","hello world",等字符串常量由于没有引用指向都被垃圾回收机制回收了。
由上面的例子可以看出,字符串不可变是指字符串的内容不可变。
如果我们想改变字符串内容可以借助反射机制来改变,但是不建议这样因为它会破坏封装,这和我们封装的思想是相违背的,下面给出反射机制的代码演示。
注释写的很详细,想要了解的朋友可以看看。
final Test t = new Test();
Test t1 = new Test();
//下面这句不可行,final修饰引用,引用的指向不可变
//t = t1;
//可以改变引用指向对象的内容
t.a = 100;
System.out.println(t.a);
String str1 = "hello";
str1 = str1 + "hi";
System.out.println(str1);
//进入String的源码,可以看见我们的String是借助于char[]类型实现的
//字符串内容不可变但是可以通过特殊手段改变内容
//这种特殊手段叫 自省 或者 反射
//下面给出例子
String string = "hello";
//我们把类看成图纸,把类下面的对象看成根据图纸制造出来的零件
//1.获取到String的类对象,类对象:站在更高到的角度把每一个类看成一个对象,可以理解为图纸对象
//对应的操作为 String.class
//2.根据String中的value这个属性名,在类对象中拿到对应的字段(仍然是图纸的一个部分)
//对应的操作为 getDeclaredField("value")
Field valueField = String.class.getDeclaredField("value");
//3.让value这个private的成员也可以被访问到
valueField.setAccessible(true);
//4.根据得到的局部图纸把对象拆开,取出对应局部图纸的零件部分
char[] value = (char[])valueField.get(string);
//5.修改内容
value[4] = 'a';
//查看修改后的内容
System.out.println(string);
代码运行的结果为:
通过上面的代码我们可以看出,final 修饰的引用,引用的指向的对象不可变,但是可以修改引用所指对象的内容;通过反射机制,可以改变字符串的内容。
今天的内容就分享到这里,希望大家多多评论,一同提高。