Java 基础语法万字解析 Java 的 String 类

Posted 吞吞吐吐大魔王

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Java 基础语法万字解析 Java 的 String 类相关的知识,希望对你有一定的参考价值。

前言:

在 C 语言中并没有字符串类型,而 Java 中却有着 String。之前就介绍过 Java 的数据类型,但是 String 类型还有着很多的知识没有介绍到。今天这节就是深度解析一下 Java 中的字符串类型,让我们更加了解它,更容易玩转字符串。


学习 Java 时我总是离不开它的 api 文档,因为很多知识都不一定要全部记得,有些方法随用随查就行了。而今天我们剖析 String 类型之前,我们就可以借助 Java 的 api 文档,让我们对 String 类型有个整体的认识。先看我在 api 中对 String 的一张截图

从这张图中我们就可以获取到关于 String 的几个知识点

  • String 是在 Java.lang 包中的,我们之前说过使用这个包的一些类(含 String 类)前不需要手动导包
  • String 是一个类
  • String 类继承了这三个接口:Serializable(下面会介绍)、CharSequence(下面会介绍)、Comparable< String >(在万字解析 Java 的多态、抽象类和接口这章介绍了)
  • String 类被 final 修饰,故它不可以被继承,是密封类

1. 创建字符串

为什么要借用 api 开头呢?

因为我们既然了解了 String 是一个类,那么用这个类去创建对象的时候我们就要清楚它有哪些构造方法

因此我就又截了 api 中关于 String 构造方法的图片,我们来看看

虽然 String 的构造方法比较多,但是我们常用的就三种

  • 方式一:

    String str1 = "Hello Java";
    
  • 方式二:

    String str2 = new String("Hello Java");
    
  • 方式三:

    char[] array = {'a', 'b', 'c'};
    String str3 = new String(array);
    

    至于想要知道方法三的原理的小伙伴,可以通过 ctrl + 鼠标点击 String 跳到 String 的定义,去寻找有数组相关的成员变量和构造方法,之后相信理解起来就很简单

注意:

“hello” 这样的字符串字面值常量,类型也是 String

问题: String 是引用类型,println 打印的应该是一个地址,为什么结果可以正常打印字符串呢?

  1. 通过找到 String 的重写方法,我们可以看到
  2. 再通过找到 println 的定义,我们可以看到
  3. 然后我就先不解释了,哈哈。其实我们知道 String 返回的是 this,但在 println 中又被转换成字符串。如果你想知道怎样转换的话,可以在 println 的定义中一直跟踪到整个过程

2. 字符串常量池

在我们实例化字符串的时候,其实就分为了两种方式

  • 直接赋值
  • 采用构造方法(new 一个新的 String)

a)直接赋值

我们可以先看一个代码,猜猜结果是啥

String str1 = "Hello Java";
String str2 = "Hello Java";
System.out.println(str1 == str2);

结果其实就是:true。因为我们知道 str1 和 str2 的值(这里指的是他俩的引用值)是一样的。

但是为什么在创建字符串的时候没有开辟出新的内存空间呢?为什么内存是这样存储的呢?

因为 String 类的设计使用了共享设计模式

在 JVM 底层实际上会自动维护一个对象池,这里指的是字符串常量池

什么是字符串常量池呢?

在堆中有一个区域存储着字符串常量,即字符串常量池。但是在 JVM 当中并没有划分区域指定哪里是字符串常量池。它的本质其实就是一个哈希表

那么字符串常量池有什么作用呢?

  • 如果对 String 类的对象实例化,并且之前字符串常量池中没有该实例化对象,那么该实例化对象将自动保存到这个字符串常量池中,如

    String str1 = "Hello Java";
    

    此时 "Hello Java" 这个字符串,将保存到字符串常量池中

  • 如果对 String 类的对象实例化,但字符串常量池之前就已经含有该实例化对象,那么将直接对其进行引用

String str1 = "Hello Java";
String str2 = "Hello Java";
System.out.println(str1 == str2);

故这个代码先是 str1 实例化对象,并将 “Hello Java” 保存到了字符串常量池中。之后 str2 实例化时,则可以直接对这个字符串进行引用,因此它们两个的引用值是相同的

2)采用构造方法

我们来看一个代码

String str2 = new String("Hello Java");

按照上述字符串常量池的概念,我们思考下实例化时在内存中是怎样的

我们知道 String 类有一个含数组的构造方法,数组名就是 Value。我们 new 的时候,相当于在堆上开辟一块空间,里面有一个变量是 Value,而构造时出现了字符串常量 “Hello Java”,所以他会自动保存到字符串常量中,而 Value 存的参数就是 “Hello Java”,并且此时他存的应该是这个字符串的地址,即相当于 Value 的引用又指向 “Hello Java”

因此我们会发现如果采用构造方法实例化 String 的对象,则会有下面的缺点:

  • 如果使用 String 构造方法会开辟两块堆内存空间,并且其中一块堆内存空间将成为垃圾空间
  • 同一个字符串可能会被多次次存储,比较浪费空间

问题: 请解释 String 类中两种对象实例化的区别

  • 直接赋值:只会开辟一块堆内存空间
  • 构成方法:会开辟两块堆内存空间

3. 字符串比较相等(包含 equals、intern)

首先我先介绍字符串比较时会出现的各类情况,在各种复杂的情况中,让你理解 String 类实例化时在内存中的情况

3.1 情况一(含 equals):

String str1 = "Hello Java";
String str2 = new String("Hello Java");
System.out.println(str1 == str2);

我们知道他们的输出是一样的,那么下面这个代码的答案是什么呢?按理说是:true

但结果是:false,为什么呢?

== 其实就是比较的是值相不相等,但是对于上述代码的值不是数值而是引用值,而 new 的对象相当于在堆上新开辟了一块空间,引用值肯定不同,所以这样比较的结果是错误的

正确的比较字符串的方式是使用 equals

equals 是比较引用所指向的对象是否相同

故将上述代码改成

System.out.println(str1.equals(str2));

结果就变成了:true,因为两个引用是不同的但是它们所指向的对象相等

3.2 情况二:

String str1 = "Hello Java";
String str2 = "Hello " + "Java";
System.out.println(str1 == str2);

结果是:ture,为什么呢?

因为常量在编译的时候就已将被运算了,即上述代码中的 "Hello " + "Java" 在编译时就已经被拼接成了 “Hello Java”。

3.3 情况三:

String str1 = "Hello Java";
String str2 = "Hello ";
String str3 = str2 + "Java";
System.out.println(str1 == str3);

结果是:false,为什么呢?

因为 str2 是变量(变量在编译的时候是不知道里面的值的,只有在运行时才知道),所以 str3 不能在编译时确定值是什么,我们可以用一张图看一下上述代码的存储

3.4 情况四:

String str1 = "Hello Java";
String str2 = "Hello " + new String("Java");
System.out.println(str1 == str2);

结果是:false,为什么呢?

我们直接上内存看看

结果就是引用不同

3.5 情况五:

String str1 = "Hello Java";
String str2 = new String("Hello ") + new String("Java");
System.out.println(str1 == str2);

结果是:false,至于为什么其实和上述情况类似,最终比较的两个对象引用值不同

3.6 情况六(含 intern 讲解):

String str1 = "Hello Java";
String str2 = new String("Hello Java");
str2.intern();
System.out.println(str1 == str2);

结果是:false,为什么呢?

这个情况和情况一类似,就是多了 intern 那行代码。

intern 就是手动将字符串入池

我们这个情况属于 str2 实例化的对象在字符串常量池已经存在了,故其实就不做处理了,和情况一是一样的,内存图就是这样

那么如果实例化之前字符串常量池不存在又会出现什么结果呢?看情况七

3.7 情况七:

String s1 = new String("1") + new String("1");
s1.intern();
String s2 = "11";
System.out.println(s1 == s2);

结果是:true,为什么呢?

我们先直接上一个图

从 s1 看起,先是两个字符串对象实例化,并将 “1” 存入了字符串变量池。之后拼接成一个新的对象,此时的数值为 “11”。后面由于有 intern,并且字符串 “11” 在变量池中不存在,所以进行入池,故字符串池中存了字符串 “11”(注意:此时存的是字符串 “11” 的地址)。再 s2 进行实例化,因为字符串变量池已经存在 “11”,所以就直接引用,故最终 s1 和 s2 的引用相同

3.8 情况八:

String str1 = new String("Hello Java");
str1.intern();
String str2 = "Hello Java";
System.out.println(str1 == str2);

结果是:false,为什么呢?

其实写看这种题目就是要细心,我们直接上图理清思绪

3.9 情况九:

String s1 = new String("1") + new String("1");
String s2 = "11";
s1.intern();
System.out.println(s1 == s2);

结果是:false,为什么呢?

这个情况其实和情况七就一点不同,直接上图理解

3.10 情况十:

String str1 = "Hello Java";
String str2 = str1;
str2 = "Hello World";
System.out.println(str1 == "Hello Java");

结果其实是:true,为什么呢?

str1 = "Hello World" 其实是将 str1 这个引用指向了一个新的 String 对象,即上述代码的整个过程可以理解为这个图

3.11 情况十一:

public static void func(String str1){
    str1="abc"
}
public static void main(String[] args){
    String str = "Hello Java";
	func(str);
	System.out.println(str == "Hello Java");
}

结果其实还是:true,这是为啥呢?

这个其实和情况十是一种情形,虽然加了个函数,但函数里的形参的引用是指向实参引用的地址,然后将形参指向一个新的对象,对实参其实没有影响

介绍了这么多种情况我们再回顾下 == 和 equals 的用法吧!

3.12 == 和 equals

String 使用 == 比较并不是在比较字符串的内容,而是比较两个引用是否指向同一个对象(即引用值)

这两种有什么不同吗?

面向对象编程语言中,涉及到对象的比较有三种方式:比较身份(即比较引用值)、比较值、比较类型

一般编程语言中 == 是用来比较值的,但是 Java 中是用来比较身份的(不需要记,看内存的存储就行)

比较值我们好理解,那么这个身份什么意思呢?我们来看下面一张图

诶对,大魔王现在去取快递,它的快递就放在那个红框框里。

我们可以把那个柜子的位置看看成“第二行,从左数第四个”或者是“第二行,从右数第第三个”,由于这两个位置都指向一个柜子,所以就表示身份相同,是我大魔王装快递的柜子

我们也可以在“第一行从左数第一个”柜子和“第一行从左数第二个“柜子都放入同样的物品,虽然它们不是同一个柜子,但是打开都是相同的物品,这就叫值相同,但是这

大家可以自细细感悟下,并且我们还能得到这样的结论:身份相同值一定相同,值相同身份不一定相同

因此比较字符串的时候,如果我们用 == 比较,可能明明它们的对象其实都是同一个字符串,但因为引用值的不同使结果出现错误。

故我们就要使用 equlas 去比较字符串,因为它是比较引用所指向的对象

注意:

当我们使用 equals 去比较字符串的时候,这样写代码要注意

String str1 = null;
String str2 = "abc";
System.out.println(str1.equals(str2));

上述代码会抛出 java.lang.NullPointerException 异常,所以我们要注意 str1 的引用是非为空,故最好直接使用字面常量字符串的形式去比较,例如

String str1 = "abc";
String str2 = "abc";
System.out.println("abc".equals(str2));

4. 理解字符串不可变

4.1 分析

字符串是一种不可变的对象,它的内容不可以改变

定义 String 类中的数组我们其实可以看到它是被 final 修饰的,无法修改

这是什么意思嘞,我们先看一段代码

String str = "hello ";
str += "java";
str += "!!!";
System.out.println(str);

结果是:"hello java!!!" ,就是你会感觉好像字符串被修改了对吧,就像是小时候的神奇宝贝进化一样,本来是小火龙,后来变成火恐龙,最好进化成喷火龙。始终都是这一只,但小火龙进化后,原本的小火龙就没有了

但是字符串是不会向上述那样,虽然最终输出的是:"hello java!!!",但是 "hello ""java""!!!" 这几个字符串都没有改变,还存在着。我们可以通过内存去理解一下

因为不是动态的所以看起来没那么顺畅,首先是1号线,表示代码的第一行,代码的第二行就是加了一个 “java”,由于常量是不可以改变的,所以不可能直接加在原有的 “hello” 后面,就新开辟一个内存,将拼接的新的字符串存入,此时 str1 的地址应该变成 0x678。3号线也是和2号线一样的步骤。所以最后其实是开辟了五个内存。原有的字符串没有改变,而是增加了新的字符串

4.2 修改字符串方式(含反射简单介绍)

那么如果我们想要修改字符串该怎么办呢?

注意:

字符串是不可以修改的,我这里说的修改其实是得到和原字符串上进行改变的新的字符串,但原字符串是不会改变的,例如将字符串 str = "hello" 改成 str = "Hello"

方式一:借助原字符串,创建新的字符串

采用 substring 方法来提取字串

String str = "hello";
str = "H" + str.substring(1);
System.out.println(str);

结果为:Hello,但是我们要知道

这样修改并没有改变原字符串,只是提取了它的字串,并进行了新的拼接

那么我就是想将字符串真正的改变而不是创建新的字符串有办法吗?有啊!这里需要用到反射

那什么是反射呢?

反射是 Java 类的一种自省的方式。通常情况下:类的内部细节有时候在类外是看不到的,但是通过反射就可以看到。

这其实就可以形象的理解为我们的行李箱接受安检的时候,行李箱通过检查的机器就可以直接看到行李箱内部的物品。

因此如果我们可以拿到 String 内部的字符串将它直接修改,就 🆗。那么怎么做呢?

String str = "hello";
// 拿到字节码对象
Class c = String.class;
// 获取 String 类的 value 字段
Field field = c.getDeclareField("value");
// 修改该字段的权限,使其访问属性为 true
field.setAccessible(true);
// 将 str 中的 value 属性获取到
char[] vals = (char[]) field.get(str);
// 将第一个字符修改成 'H'
vals[0] = 'H';
System.out.println(str);

结果为:“Hello”,并且是直接将原字符串修改

但是我们为什么要让 String 不可变呢?

  • 方便实现字符串常量池。如果可变那么对象池就需要考虑何时深拷贝字符串的问题
  • 不可变对象是线程安全的
  • 不可变对象更方便缓存 hash code,作为 key 时可以更高效的保存到 HashMap 中

5. 字符、字节、字符串

5.1 字符串与字符

之前我们就介绍到字符串内部含一个数组,即 String 应该可以和 char[] 相互转换

我搜集了下列方法

No.方法名称类型描述
1public String(char value[])构造将字符数组中的所有内容变为字符串
2public String(char value[], int offset, int count)构造将部分字符数组中的内容变为字符串,offset 为偏移量,从0开始
3public char charAt(int index)普通取得指定索引的字符,索引从0开始
4public char[] toCharArray()普通将字符串变为字符数组返回

接下来进行一一演示

示例一: 将字符数组中的所有内容变为字符串

char[] value = {'a', 'b', 'c', 'd', 'e'};
String str = new String(value);
System.out.println(str);

结果为:abcde

示例二: 将部分字符数组中的内容变为字符串

char[] value = {'a', 'b', 'c', 'd', 'e'};
String str = new String(value, 1, 3);
System.out.println(str);

结果为:bcd

示例三: 取得指定索引的字符,索引从0开始

String str = "abcde";
char c = str.charAt(2);
System.out.println(c);

结果为:c

示例四: 将字符串变为字符数组返回

String str = "abcde";
char[] value = str.toCharArray();
System.out.println(Arrays.toString(value));

结果为:[a, b, c, d, e]

练习: 判断一个字符串是否都由数字组成

String str = "12213";
char[] value = str.toCharArray();
for(int i=0; i<value.length; i++){
    if(value[i]<'0' || value[i]>'9'){
        System.out.println("不是都由字母组成");
        return;
    }
}
System.out.println("都是由字母组成");

思路:

将字符串变为字符数组,然后判断每一位是否为数字

5.2 字符串与字节

字节常用于数据传输以及编码转换的处理,字符串 String 也能和字节数组 byte[] 相互转换

我搜集了下列方法

No.方法名称类型描述
1public String(byte bytes[])构造将字节数组变成字符串
2public String(byte bytes[], int offset, int length)构造将部分字节数组中的内容变为字符串
3public byte[] getBytes()普通将字符串以字节数组的形式返回
4public byte[] getBytes(String charsetName)throws java.io.UnsupportedEncodingException普通编码转换处理

接下来进行一一演示

示例一: 将字节数组变成字符串

byte[] bytes = {97, 98 ,99 ,100};
String str = new String(bytes);
System.out.println(str);

结果为:abcd

示例二: 将部分字节数组中的内容变为字符串

byte[] bytes = {97, 98 ,99 ,100};
String str = new String(bytes, 1, 2);
System.out.println(str);

结果为:bc

示例三: 将字符串以字节数组的形式返回

public static void main(String[] args) {
    String str = "abcde";
    byte[] bytes = str.getBytes();
    System.out.println(Arrays.toString(bytes));
}

结果为:[97, 98, 99, 100, 101]

示例四: 编码转换处理

public static void main(String[] args)throws java.io.UnsupportedEncodingException {
    String str = "魔王";
    byte[] bytes = str.getBytes("GBK");
    System.out.println(Arrays.toString(bytes));
}

结果为:[-60, -89, -51, -11]

如果我们将编码方式 “GBK” 改成 “utf-8”,则会有不同的结果

public static void main(String[] args)throws java.io.UnsupportedEncodingException {
    String str = "魔王";
    byte[] bytes = str.getBytes("utf-8");
    System.<

以上是关于Java 基础语法万字解析 Java 的 String 类的主要内容,如果未能解决你的问题,请参考以下文章

万字长文!java读取json文件数据给对象

3.5万字 JavaSE温故而知新!(结合jvm 基础+高级+多线程+面试题)

坚持原创 绝不注水爆肝万字长文Java 语言的基本特性(喂饭式教程)

Java 基础语法爆肝两万字解析 Java 的多态抽象类和接口

java中的增强for循环,是啥?语法结构和使用得条件?详细解答就加高分。

Java Executor源码解析—ThreadPoolExecutor线程池submit方法以及FutureTask源码一万字