Java · 认识 String 类(上)· 创建字符串 · 字符串比较相等 · 字符串常量池 · 字符串不可变 · 字符字节与字符串

一、创建字符串

常见的构造 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底层实际上会自动维护一个对象池(字符串常量池)

  1. 如果采用了直接赋值的模式进行 String 类的对象实例化操作,那么该实例化对象(字符串内容)将会自动保存到这个对象池中。
  2. 下次继续使用直接赋值的模式声明 String 类对象,此时对象池中如果有该常量,将直接进行引用。
  3. 如果没有,则开辟新的字符串对象,并且将其保存在对象池之中以供下次使用。
理解 “池”

"池" 是编程中的一种常见的,重要的提升效率的方式。
我们会在未来的学习中遇到各种 "内存池","线程池","数据库连接池"。

3.2 采用构造方法的内存情况

类对象使用构造方法实例化是标准做法。分析如下程序:

String str = new String("hello");

这样的做法有两个缺点

  1. 使用 String 构造方法就会开辟两块堆内存空间,并且其中一块堆内存将成为垃圾空间(字符串常量 “hello” 也是一个匿名对象,用了一次之后就不再使用了,就成为垃圾空间,会被 JVM 自动回收掉)。
  2. 字符串共享问题,同一个字符串可能会被存储多次,浪费空间

我们可以使用 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 类中两种对象实例化的区别.

  1. 直接赋值,只会开辟一块堆内存空间,并且该字符串对象可以自动保存到字符串常量池中以供下次使用。
  2. 构造方法,会开辟两块堆内存空间,不会自动保存在对象池中,可以使用 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 文件)。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值