目录
一、创建字符串
常见的构造 String 的方式.
String str1 = "hello";
String str2 = new String("hello");
char[] array = {'a','b','c'};
String str3 = new String(array);
注意事项
- “hello” 这样的字符串字面值常量,类型也是 String.
- String 也是引用类型,
String str = "hello";
这样的代码内存布局如下
引用类型
引用类似于 C 语言中的指针,只是在栈上开辟了一小块内存空间保存地址。
但是引用和指针又不太相同,指针能进行各种数学运算(指针+1)之类的,引用不能,没这么灵活。
也可以把引用想象成一个标签,“贴” 到一个对象上。
如果一个对象上没有一个标签,就会被 JVM 当做垃圾对象回收掉。
Java 中数组,String,自定义的类都是引用类型。
由于 String 是引用类型,因此对于以下代码.
String str1 = "hello";
String str2 = str1;
内存布局如图.
如果我们修改 str1,str2也会随之改变吗?
str1 = "world";
System.out.println(str2);
// 执行结果
Hello
我们发现,“修改” str1 之后,str2 也没发生变化,还是 Hello?
事实上,str1 = "world"
这样的代码并不算 “修改” 字符串,而是让 str1 这个引用指向了一个新的 String 对象。
二、字符串比较相等
如果现在有两个int型变量,判断其相等可以使用 “==” 完成
int x = 10;
int y = 10;
System.out.println(x == y);
//执行结果
true
如果 String 类对象上使用 “==” ?
代码一
String str1 = "Hello";
String str2 = "Hello";
System.out.println(str1 == str2);
// 执行结果
true
看起来貌似没啥问题,再换个代码试试,发现情况不太妙.
代码二
String str1 = new String("Hello");
String str2 = new String("Hello");
System.out.println(str1 == str2);
// 执行结果
false
我们来分析两种创建 String 方式的差异.
代码一内存布局
我们发现,str1 和 str2 是指向同一个对象的。
此时如 “Hello” 这样的字符串常量在字符串常量池中。
关于字符串常量池
如 “Hello” 这样的字符串字面值常量,也是需要内存空间来存储的。
这样的常量有一个特点,就是不需要修改,
所以如果代码中有多个地方引用都需要使用 “Hello” 的话,
就直接引用到常量池这个位置就行了,没必要把 “Hello” 在内存中存储两次。
代码二内存布局
通过String str1 = new String("Hello");
这样的方式创建的 String 对象相当于在堆上另外开辟了空间来存储 “Hello” 的内容,也就是内存中存在两份空间,都指向了 “Hello”。
String 使用 == 比较并不是在比较字符串内容,而是比较两个引用是否指向同一个对象
Java 中想要比较字符串的内容,必须采用 String 类提供的 equals 方法。
String str1 = new String("Hello");
String str2 = new String("Hello");
System.out.println(str1.equals(str2));
// System.out.println(str2.equals(str1)); // 或者这样写也行
// 执行结果
true
equals 使用注意事项:
现在需要比较 str 和 “Hello” 两个字符串是否相等,我们应该如何来写?
String str = new String("Hello");
// 方式一
System.out.println(str.equals("Hello"));
// 方式二
System.out.println("Hello".equals(str));
在上面的代码中,哪种方式更好呢?
我们更推荐使用 “方式二”。一旦 str 是 null,方式一的代码会抛出异常,而方式二不会。
String str = null;
// 方式一
System.out.println(str.equals("Hello")); // 执行结果 抛出 java.lang.NullPointerException 异常
// 方式二
System.out.println("Hello".equals(str)); // 执行结果 false
“Hello” 这样的字面值常量,本质上也是一个 String 对象,完全可以使用 equals 等 String 对象的方法。
三、字符串常量池
在上面的例子中, String类的两种实例化操作, 直接赋值和 new 一个新的 String。
3.1 直接赋值的内存情况
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
为什么没有开辟新的堆内存空间呢?
因为 String 类的设计使用了共享设计模式
在JVM底层实际上会自动维护一个对象池(字符串常量池)
- 如果采用了直接赋值的模式进行 String 类的对象实例化操作,那么该实例化对象(字符串内容)将会自动保存到这个对象池中。
- 下次继续使用直接赋值的模式声明 String 类对象,此时对象池中如果有该常量,将直接进行引用。
- 如果没有,则开辟新的字符串对象,并且将其保存在对象池之中以供下次使用。
理解 “池”
"池" 是编程中的一种常见的,重要的提升效率的方式。
我们会在未来的学习中遇到各种 "内存池","线程池","数据库连接池"。
3.2 采用构造方法的内存情况
类对象使用构造方法实例化是标准做法。分析如下程序:
String str = new String("hello");
这样的做法有两个缺点:
- 使用 String 构造方法就会开辟两块堆内存空间,并且其中一块堆内存将成为垃圾空间(字符串常量 “hello” 也是一个匿名对象,用了一次之后就不再使用了,就成为垃圾空间,会被 JVM 自动回收掉)。
- 字符串共享问题,同一个字符串可能会被存储多次,浪费空间。
我们可以使用 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
面试题:请解释 String 类中两种对象实例化的区别.
- 直接赋值,只会开辟一块堆内存空间,并且该字符串对象可以自动保存到字符串常量池中以供下次使用。
- 构造方法,会开辟两块堆内存空间,不会自动保存在对象池中,可以使用 intern 方法手工入池。
综上,我们一般采用直接赋值的方式创建 String 对象。
四、理解字符串不可变
字符串是一种不可变对象,它的内容不可改变。
String 类的内部实现也是基于 char[ ] 来实现的,但是 String 类并没有提供 set 方法之类的来修改内部的字符数组。
感受如下代码:
String str = "hello";
str = str + " world";
str += "!!!";
System.out.println(str);
形如 += 这样的操作,表面上好像是修改了字符串,其实不是。内存变化如下:
“ += ”之后的 str 打印的结果却变了,不是 String 对象本身发生了改变,而是 str 引用到了其它对象。
需要修改字符串,我们有两种方法比较好。
1) 常见方法:借助原字符串,创建新字符串
String str = "Hello";
str = "h" + str.substring(1);
System.out.println(str);
//执行结果
hello
2) 特殊方法:使用 “反射” 操作可以破坏 String 的封装,访问类的内部 private 成员。
IDEA 中 ctrl + 左键 跳转到 String 类的定义,可以看到内部包含了一个 char[] ,保存了字符串的内容。
String str = "Hello";
// 获取 String 类中的 value 字段. 这个 value 和 String 源码中的 value 是匹配的.
Field valueField = String.class.getDeclaredField("value");
// 将这个字段的访问属性设为 true
valueField.setAccessible(true);
// 把 str 中的 value 属性获取到.
char[] value = (char[]) valueField.get(str);
// 修改 value 的值
value[0] = 'h';
System.out.println(str);
// 执行结果
hello
关于反射
反射是面向对象编程的一种重要特性,有些编程语言也称为 “自省”。
指的是程序运行过程中,获取/修改某个对象的详细信息(类型信息,属性信息等),相当于让一个对象更好的 “认清自己”。
为什么 String 不可变?
我们刚刚看源码,发现 String 被 final 修饰表示不可变。
为什么 String 不可变?(不可变的好处是什么?)
1. 方便实现字符串对象池。
2. 不可变对象的线程是安全的。
3. 不可变对象更方便缓存 hash code,作为 key 时可以更高效的保存到 HashMap 中。
注意事项:这种代码不应该出现在你的程序中,会产生大量的临时对象,效率低。
String str = "hello";
for(int x = 0; x < 1000; x++){
str += x;
}
System.out.println(str);
五、字符、字节与字符串
5.1 字符与字符串
字符串内部包含一个字符数组,String 可以和 char[ ] 相互转换。
相关方法
构造方法:public String(char value[])
将字符数组中的所有内容变成字符串
构造方法:public String(char value[], int offset, int count)
将部分字符数组中的内容变成字符串
普通方法:public char charAt(int index)
取得指定索引位置的字符,索引从 0 开始
普通方法:public char[] toCharArray()
将字符串变为字符数组返回
代码示例 :获取指定位置的字符
String str = "hello" ;
System.out.println(str.charAt(0)); // 下标从 0 开始
// 执行结果
h
System.out.println(str.charAt(10));
// 执行结果
产生 StringIndexOutOfBoundsException 异常
代码示例 :字符串与字符数组的转换
String str = "helloworld" ;
// 将字符串变为字符数组
char[] data = str.toCharArray() ;
for (int i = 0; i < data.length; i++) {
System.out.print(data[i]+" ");
}
// 字符数组转为字符串
System.out.println(new String(data)); // 全部转换
System.out.println(new String(data,5,5)); // 部分转换
代码示例 :给定字符串,判断其是否全部由数字所组成。
public static void main(String[] args) {
String str = "1a23456" ;
System.out.println(isNumber(str)? "字符串由数字所组成!" : "字符串中有非数字成员!");
}
public static boolean isNumber(String str) {
char[] data = str.toCharArray() ;
for (int i = 0; i < data.length; i++) {
if (data[i]<'0' || data[i]>'9') {
return false ;
}
}
return true ;
}
5.2 字节与字符串
字节常用于数据传输以及编码转换的处理之中,String 也能方便的和 byte[] 相互转换。
相关方法
构造方法:public String(byte bytes[])
将字节数组变为字符串
构造方法:public String(byte bytes[], int offset, int length)
将部分字节数组中的内容变为字符串
普通方法:public byte[] getBytes()
将字符串以字节数组的形式返回
普通方法:public byte[] getBytes(String charsetName) throws
UnsupportedEncodingExcption
编码转换处理
代码示例 :实现字符串与字节数组的转换处理
String str = "helloworld" ;
// String 转 byte[]
byte[] data = str.getBytes() ;
for (int i = 0; i < data.length; i++) {
System.out.print(data[i]+" ");
}
// byte[] 转 String
System.out.println(new String(data));
5.3 小结
- byte[ ] 是把 String 按照一个字节一个字节的方式处理,这种适合在网络传输,数据存储这样的场景下使用,更适合针对二进制数据来操作。
- char[ ] 是把 String 按照一个字符一个字符的方式来处理,更适合针对文本数据来操作,尤其是包含中文的时候。
文本数据 vs 二进制数据
一个简单粗暴的区分方式就是用记事本打开能不能看懂里面的内容。
如果看的懂,就是文本数据(例如 .java 文件),如果看不懂,就是二进制数据(例如 .class 文件)。