说明

这是一个完善了但又不完善的笔记,或许以后会更新

可以参考但请务必超越

源文件


Tools


Typora
PicGo

认识String类

什么是字符串?使用双引号引起来的内容。"abc" ,"a" - 只有一个字符的字符串

什么是字符?使用单引号引起来的内容。'a'

注意,在Java当中没有所谓的以\0结尾。

在C语言里面, 是没有字符串这种数据类型的。在C++和Java里面有了

1.创建字符串

//方式一
String str1 = "ababcabcd";
//调用构造方法进行构造对象

//方式二
String str2 = new String("hello");


//方式三
char[] chars = {'a', 'b', 'c'};
String str3 = new String(chars);

查看方法

可以在Java开发手册中搜索,也可以在idea中点开String查看

在开发手册中写好了构造方法

在idea中快捷键alt+7查看

2.字符串底层代码及内存图

现在可以看到String的构造方法,还有字段,这里我们要注意其中2个字段

private final char value[];
private int hash;

当我们new一个新的String对象时,就会在逻辑上产生这样一个类似链表节点的东西

2.1 查看构造方法

接下来的内容可能会有点烧脑,但是不影响正常的理解

能看懂理解最好,看不懂理解不来也没关系

那么,我们将利用上面的信息,来逐步查看String底层的内容,以及内存图等

方式一的创建方式应该不用多讲,直接就通过构造方法创建并初始化嘛。接下来看看剩下两种

String str2 = new String("hello");

点开这个对象String

我们可以看到生成了一个value和一个hash,现在还看不懂,后面画图就可以理解了

还有一个字符数组

char[] chars = {'a', 'b', 'c'};

把他转换成字符串

String str3 = new String(chars);

点开这个新的对象,可以看到这是一个char []的构造方法,当然在Java中规定了写法是char value[],可以不用管

意思是把所有的数组内容通过Arrays.copyOf拷贝一份新的给value

点开copyOf可以看到是有新生成一个数组的

2.2 字符串引用指向

我们再来看一个代码

String str1 = "hello";
String str2 = str1;

这个意思很好理解吧,内存布局就是这样的

说明str1str2在引用同一个对象

现在str2指向别处,当然是不会影响str1

str2 = "world";
System.out.println(str1 + str2);

问题一

既然str1str2都指向一个"hello",那么可不可以通过修改str2来修改"hello"

答案是不可以的

双引号引起来的内容属于字面值常量,str2 = "world"只是修改了指向

不是说传引用就能改变实参的值。要看这个引用做了什么

更深层次的内容接下来再看

2.3 字符串常量池

上面的问题有一个点,双引号引起来的内容属于字面值常量

那么我们来看下面的问题

现在让str1str2各自新建对象

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

这个时候str1str2相等吗?

根据上面引用指向的图,可以知道str1str2是2个引用,2个引用当然是不相等的

注意,虽然引用不相等,但是我们看双引号引起来的hello

字符串常量嘛。那这2个"hello",是不是相等呢?

常量池简述

通常会听到3个常量池。会涉及到JVM的内容,JVM以后或许会讲,现在先简单讲述一下常量池

  • Class文件常量池:程序编译好后会产生字节码文件,比如int a = 10;这里的10就在Class文件常量池中。存放在磁盘上。
  • 运行时常量池:当程序把编译好的字节码文件加载到JVM后,会生成一个运行时常量池[方法区]、实际上就是Class文件常量池从磁盘上加载到方法区。
  • 字符串常量池:主要存放字符串常量,本质上是一个哈希表。StringTable。双引号引起来的字符串常量。

从JDK1.8开始,字符串常量池放在了堆里面。也就是说以后由堆来维护这个哈希表。

什么是池?

以后或许会接触到数据库连接池,线程池

他的意义是提高效率

池子嘛,当用的时候就用现成的,抽象出一个池子,仓库一般不流动,需要自己去找。池子就自动流动,自动存储,所以就会自动帮我们完成一些固定的内容。

哈希表

哈希表:是一种数据结构

如何去查找到一个关键字,一般有2种方法

  1. 顺序查找:通过关键字一个一个来比较,时间复杂度可能高达O(n)
  2. 排序+二分查找

哈希表呢,在存储数据的时候,会根据一个映射关系进行存储。如何映射,需要设计一个函数,也就是哈希函数。

假设现在有这么一组数据,要存放到长度为10的一个空间。

1、11、2、22、4、44、5、55、9、99

那么就可以设计一个函数,假设就根据key % len来存储

当放进去第一个值,第二个值会和第一个值出现冲突时,就会使用链表顺序挂到后面,每一个数据就是一个节点。取的时候也就是这样取的

这里借鉴百度百科的内容,可以自行查看下百度百科给出的图片

散列表(Hash table,也叫哈希表),是根据关键码值(Key value)而直接进行访问的数据结构。也就是说,它通过把关键码值映射到表中一个位置来访问记录,以加快查找的速度。这个映射函数叫做散列函数,存放记录的数组叫做散列表

在数据结构中,还会再讲相关内容

使用哈希表,最快可以达到O(1)

2.4 验证存储

上面的一大堆概念,足以证明2个"hello"其实是一个存放在哈希表里的数据

我们现在已知2个引用str1str2是不相等的

那么,我们就来看看2个"hello"在内存中的存储吧

前面有说,从JDK1.8开始,字符串常量池放在了堆里面。

那么我们首先可以把哈希表画出来

String str1 = "hello";

那么第一个"hello"在哈希表中应当是这样存储的

  1. 会生成哈希值
  2. 会有next域来链接下一个节点
  3. 新产生了一个对象

根据上面3个点,可以画出这样的str1

注意虽然比较绕,但是还是很好理解的

str1新创建的对象由哈希表通过节点来管理,有下一个节点就接着链接

那么str2就较为简单了

首先检查哈希表中存在"hello",那么就直接给到str2的对象就好了。但是可以看到str2就需要产生2个对象

调式

我们直接打个断点调试一下

可以看到value值是一样的,也验证了上面画的图是对的

2.5 openJDK

关于最底层的JVM源代码,实际上应该是没有方法可查看的,因为不开源

如果想查看,可以查看openJDK的内容

具体方法这里就不讲了,网上搜索多得是,也很简单

2.6 其他比较(重要)

我们前面用str1str2的比较来讲了常量池,哈希表

str1str2的比较当然可以有其他的几种情况

String str1 = "hello";
String str2 = "hello";

这个很容易理解,就是把"hello"直接给到2个引用

关键来了,还有这样一种情况

String str1 = "hello";
String str2 = "he" + "llo";

这时str1还等于str2

答案是相等

把双引号中的内容分成两部分,这两部分也都是常量。编译的时候就已经确定好了是一个整体

也很好验证,我们查看一下字节码文件

既然前面分开也是2个常量,那么

String str3 = "he";
String str4 = str3 + "llo";
System.out.println(str1 == str4);

这种情况自然也就是不相等了,因为str3是变量,在编译的时候不知道是什么

最后,还有一种情况

String str1 = "11";
String str2 = new String("1") + new String("1");

结合前面的内容,可以知道str1str2不相等

看这里的字节码文件,有一个StringBuilder.append方法,实际上就是通过这个方法来新创建了一个对象来保存2个1

StringBuilder后面还会讲

先来把这个图画一下加深理解

因为不知道StringBuilder,所以看起来很复杂,其实也确实很复杂。但一步一步顺着来,逐渐理解就能明白了

  1. 哈希表会存一个1
  2. str2创建了2个对象,每个对象的value都引用一个1
  3. StringBuilder把2个1合成11,使用val指向11
  4. StringBuilder通过toString方法再创建一个对象,其中的value也指向11,让str2引用这个对象

一步一步理解后,发现其实还是很简单。当然也完全不用研究这么深入,str1str2就是2个引用,他们不相等

手动入池

前面最后一种情况,StringBuilder虽然合成了一个11,但是并没有入池,直接用toString创建了一个对象让str2引用

那么接下来可以用这样一个方法

intern();//当常量池没有的时候就会入池

这个方法就是手动入池

意思就是我们手动的把他放到哈希表里嘛

假设现在str1str2换了位置,池子里没有11

String str2 = new String("1") + new String("1");
str2.intern();
String str1 = "11";

这个时候,str1就和str2相等了

注意,如果不换顺序,池子里已经有了11,自然就不会再入池了,那必然还是false

3.理解字符串不可变

前面刚啃下一大块难搞的内容

接着就又来一大块

String str1 = "hello";
String str2 = "world";

一般情况,其实是无法修改字符串中的内容的,比如把str1"hello"改成"Hello"

还有str1str2的拼接

String str = str1 + str2;

虽然打印出来是拼接在了一起,但是其实并没有改变任何一个字符串

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

String类的内部实现也是基于char[]来实现的,但是String类并没有提供set方法之类的来修改内部的字符数组。

错误的拼接

经常会见到这样的代码

String str = "abc";
for (int i = 0; i < 10; i++) {
    str += i;
}
System.out.println(str);

在abc后面拼接上0123456789,虽然使用for循环做到了,但是每一次的循环,都会new一个新的对象

我们可以查看一下字节码文件

12-42是循环,虽然现在还没讲StringBuilder,但是我们现在就可以知道,所有的拼接,都会优化为StringBuilder。和前面的11一样,调用append方法,toString方法

每一次new对象,时间和空间都是有花销的

现在我们再来想,想要改变一个字符串"hello"的首字母,或者给字符串拼接,其实并不是很容易。那有没有什么办法可以解决这个问题呢?

常见办法

我们可以借助原字符串,创建新的字符串

str1 = "H" + str1.substring(1);

一般来说使用Java提供的substring方法即可修改

还有一种方法,要比较厉害

反射

我们前面看到过Stringvalue字段

他是private封装起来的

反射,可以破坏封装,访问一个类内部的private成员

我们在过安检的时候,安检机器能透视看到行李箱中的RPG-7火箭筒,反射就是能获取到类中的属性

public static void main(String[] args) throws NoSuchFieldException, IllegalAccessException {
    String str = "hello";
    Class<?> c1 = String.class;
    // 获取 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';
}

可以看到,反射就相当于是一个“bug”存在,当然不是真的bug。这里也只是作为了解内容,了解即可

关于反射

反射是面向对象编程的一种重要特性,有些编程语言也称为“自省”。

指的是程序运行过程中,获取/修改某个对象的详细信息(类型信息,属性信息等),相当于让一个对象更好的“认清自己”。

Java中使用反射比较麻烦一些。后面或许会详细介绍反射的具体用法。

为什么String要不可变?(不可变对象的好处是什么?)

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

关于字符串拼接

前面涉及到了这么多的StringBuilder,也看到了使用for循环拼接其实很不合适

到本篇文章后面,就会详细的讲,到底如何来进行拼接

4.字符与字符串

字符与字符串的转换,可以通过构造方法和普通方法来转换

构造方法

public String(char value[])
public String(char value[], int offset, int count)

两种构造方法

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

第一个就不用多说了,直接传进去就好了

第二个还有offsetcount2个参数

一个是偏移量,一个是个数

char[] val = {'a', 'b', 'c' };
String str = new String(val, 1, 1);

charAt

public char charAt(int index)

获取某个下标的字符。当然记得不要越界,否则就会抛异常

很好理解

String str = "hello";
char ch = str.charAt(2);

toCharArray

String str = "hello";
char[] chs = str.toCharArray();

把字符串变成字符数组嘛

判断字符

给定一个字符串,判断其中是否包含数字(或字母)

很简单,普通的判断方法就不多说了,这里讲一个Character.isDigit()方法,之后可以自己查一下Character

public static boolean Func(String str) {
    for (int i = 0; i < str.length(); i++) {
        char c = str.charAt(i);
        boolean flg = Character.isDigit(c);
        //判断某个字符是不是数字
        if (flg == false) {
            return false;
        }
    }
    return true;
}

5.字节与字符串

不用多说,自然还是有构造方法和普通方法

构造方法

byte[] bytes = {97, 98, 99, 100, 101};
String str = new String(bytes, 1, 1);

最直接的构造方法就不说了,因为很简单,下面都会讲带参数的

这里有一个点需要注意,如果我们把后面2个参数中间的逗号去掉,就会变成调用2个参数的构造方法,而恰巧,String是有这样的方法的

image-20211128164625666

可以看到String被划了横线,我们点进去看看

@Deprecated

注意这个注解,这个注解的意思是过时,意思是这个构造方法已经过时了,没用了

getBytes

String str = "abc";
byte[] bytes = str.getBytes();

就很简单,字符串转字节,abc就转换ASCII码值

byte[] bytes = str.getBytes(StandardCharsets.UTF_8);
byte[] bytes = str.getBytes("utf-8");

可以给一个参数,指定编码

注意下面给编码的方式需要声明异常

throws UnsupportedEncodingException

当然还可以有其他的编码方式,比如GBK。注意,同样的字符串不同的编码个数甚至都不相同

小结

那问题来了,什么时候用byte[]?什么时候用char[]呢?

  • byte[]是把String按照一个字节一个字节的方式处理,这种适合在网络传输,数据存储这样的场景下使用。更适合针对二进制数据来操作。
  • char[]是把String按照一个字符一个字符的方式处理,更适合针对文本数据来操作,尤其是包含中文的时候。

6.字符串常见操作

大多都是重载方法,这可以理解吧

String有那么多的构造方法,都也是重载

字符串比较

字符串的比较,前面做例子,对于引用都用的是==

其实这是不太好的,一些公司就强制规定了必须使用equals

字符串的比较,一般只有2种比较

  1. 真假比较(内容)
  2. 大小比较

equals

点开Stringequals看一下嘛

比较规则:先比较地址,地址相同那就直接返回true。地址不同再看类型,长度,最后再看内容是否相同

idea在前面有显示这样一个图标

点击他可以跳转到这里

那么意思就很明确了,如果String没有重写equals的话,那么比较的就是地址,而不是其中的字符

举个栗子可以看到返回的一定是布尔类型

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

如果地址相同那就直接返回true

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

当然,还要注意空指针异常。任何一个引用调用方法一定要预防空指针异常。

equalsIanoreCase

前面这是真假比较,还有大小的比较

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

忽略大小写比较,那么他们就是一样的

如果去掉IgnoreCase,那自然是不一样的

compareTo

在面向对象的时候讲到过Comparable接口

现在查看一下String

可以看到也是实现了Comparable接口的

String str1 = "abc";
String str2 = "Abc";
str1.compareTo(str2);

那就是str1-str2嘛,a的码值是97,A的码值是65

返回值有三种情况,因为相减嘛

大于0、等于0和小于0

字符串查找

字符串查找就只有普通方法了,肯定不能new对象的时候就查找吧,什么都没有找什么

contains

public boolean contains(CharSequence s)

看一下参数,CharSequence

String str = "ababcabcd";
String tmp = "abc";
str.contains(tmp);

现在传一个字符串发现他是没有报错的,这是为什么?

还是看String

哦,实现了CharSequence这个接口,那就可以接收了

contains返回值是一个布尔类型,那么我们接收打印一下

说明str是有包含tmp

indexOf

类似于C的strstr - KMP算法

关于KMP算法,这里推荐可以看博哥的视频,非常详细易懂

【完整版】终于有人讲清楚了KMP算法,Java语言C语言实现

如果存在的话,返回起始下标

String str = "ababcabcd";
String tmp = "abc";
str.indexOf(tmp);

也可以给一个参数从指定下标开始找

lastIndexOf

字面意思,从后向前找

还是用上面indexOf的例子

那要是从下标4开始找

startsWith

判断开头

意思是判断是否是以某个字符开头

这就很简单了

也可以给个偏移量参数,开始找

endsWith

那自然就是判断结尾

注意结尾就没有其他方法了,都结尾了嘛,开头可以从半路开头,结尾不能半路结束

字符串替换

只有这4种情况,一一解读一下

首先

只要是返回String的,那一定是返回了新的对象,而不是在原本的对象做操作

他一定是new了一个新对象

replace

String ret = str.replace('b', 'd');

注意一定要接收

所有的b都被替换成了d

当然还有第二种

CharSequence接口刚在前面讲了

那这里意思就是可以传字符串

ad全部替换成了cc

replaceAll

字面意思,替换所有

那就和不带All的版本一样嘛,只不过看参数只能传字符串。当然单个字符的字符串也算字符

replaceFirst

字面意思那就是只替换第一个呗,单个和多个字符的字符串都可以

字符串拆分

字符串的拆分只有一个sqlit,可以将一个完整的字符串按照指定的分隔符划分为若干个子字符串

我们在查看网页的时候,很多网页网址的组成会有很多的参数

这个时候我们就可以通过拆分来取得其中的关键字

split

现在来拆分一行信息

public static void main(String[] args) {
    String str = "name=kirito&age=18";
    String[] strings = str.split("&");
    for (String s : strings) {
        String[] ss = s.split("=");
        for (String sss : ss) {
            System.out.println(sss);
        }
    }
}

先通过&nameage拆分,再通过=把其中的内容也拆分。foreach循环拆分最后打印

注意,有返回值,是数组

比较特殊的情况,拆分IP地址

127.0.0.1

如果我们直接按.来拆分,就会出现转义的问题,具体问题需要具体计算。

  1. 字符".""|""*""+"都得加上转义字,前面加上"\"。
  2. 而如果是"\",那么就得写成"\\\\",就得数着凑齐。
  3. 如果一个字符串中有多个分隔符,可以用"|"作为连字符。
public static void main(String[] args) {
        String str = "127.0.0.1";
        String[] strings = str.split("\\.");
        for (String s : strings) {
            System.out.println(s);
        }
    }

split还可以分组拆分

输入想要分成的组数即可,但是不是均匀分组的。只要分好了就算完成,分的多了也没用,最多能分几组就几组

字符串截取

(提取子串)

截取就很简单了嘛,要么就直接截到尾也就是截成两半,要么就选取中间部分截取

substring

一个参数就是截到尾,两个参数就是截一部分

public static void main(String[] args) {
    String str = "abcdefg";
    String sub = str.substring(3);
    System.out.println(sub);
}

截一部分,注意Java左闭右开(前闭后开)

public static void main(String[] args) {
    String str = "abcdefg";
    String sub = str.substring(3, 5);
    System.out.println(sub);
}

其他操作方法

trim():去掉左右空格,保留中间空格

toUpperCase():字符串转大写

toLowerCase():字符串转小写

以上三种其实在一些情况以及见过了,比如输入验证码不区分大小写等

intern():字符串入池操作,前面手动入池就是用的这个

concat():字符串拼接,拼接后的字符串不会入池

length():获取字符串长度,就不用多说了吧。注意哦使用字符串的方法时是需要带括号的!

isEmpty():判断是否为空字符串,注意不是null,是长度为0。""是空字符串,null就是null" "注意这是有空格的字符串。

注意!返回值!有返回值为String的,基本都是new了新的,注意接收!

7.StringBufferStringBuilder

注意,这两个数据类型和String,这是三种不同的数据类型

从内部的方法上来说其实是大同小异的

append

先从StringBuilder讲起

StringBuilder sb = new StringBuilder();

初始化赋值都一样,那我们来打印下

StringBuilder sb = new StringBuilder("abc");
System.out.println(sb.toString());

注意,这里我们通过append来添加一些内容

sb.append(123);

注意这个方法的返回值,在前面String的众多方法当中,他们的返回值基本都是new了一个新的String

append不同

他返回的是thisthis是当前对象的引用嘛,那就是把要添加的内容直接返回给了当前对象,改变了当前对象,而不是像String的方法new了一个新的对象不改变原对象

这就很好理解了

甚至append还能连用

StringStringBuilder

现在知道了StringBuilderappend,那前面String做比较时源代码出现的StringBuilderappend就可以理解了

现在我们写个简单的代码来举栗

public static void main(String[] args) {
    String str = "abc";
    for (int i = 0; i < 10; i++) {
        str += i;
    }
    System.out.println(str);
}

这个代码的意思是String类型的字符串添加数字0到9

我们可以javap -c看看他的字节码文件

在for循环里,可以看到对字符串String类型的"abc"进行了如下操作

  1. new了一个新的StringBuilder对象
  2. 调用无参数的构造方法
  3. 将当前String字符串的内容append添加进去
  4. append(i)添加数字
  5. toString()返回给String对象

好家伙,原来String靠的就是StringBuilder来进行操作

那如果直接写成StringBuilder的形式,应该是这样子

public static void main(String[] args) {
    String str = "abc";
    for (int i = 0; i < 10; i++) {
        StringBuilder sb = new StringBuilder(str);
        sb.append(str).append(i);
        str = sb.toString();
    }
    System.out.println(str);
}

这是不是每次都要new一个StringBuilder对象呀,而且每次还要再进行append(str),也太繁琐了

那优化一下应该是这样子

public static void main(String[] args) {
    String str = "abc";
    StringBuilder sb = new StringBuilder(str);
    for (int i = 0; i < 10; i++) {
        StringBuilder sb = new StringBuilder(str);
        sb.append(i);
    }
    str = sb.toString();
    System.out.println(str);
}

直接就把str先赋值进去,然后循环内部只使用append添加,最后再把添加好的值给str

小结

如果是在循环里面,进行字符串的拼接,尽量不要使用String。优先使用StringbufferStringBuilder

那问题来了

StringbufferStringBuilder有什么区别,前面已经讲了一个,那现在就剩下Stringbuffer需要讲

StringBuffer

从前面的内容,可以看到String其实没有很全面,比如append这个方法String就没有

还有字符串逆序

reverse()

String也没有

但是StringbufferStringBuilder就有,可以自行查看

那么既然方法都有,区别在哪里呢?

随便找一个append方法

StringBuffer.append(String)

StringBuilder.append(String)

synchronized

首先,基本上所有的方法,Stringbuffer都要比StringBuilder多一个关键字synchronized

保证线程安全

什么意思呢?

当程序在运行的时候,synchronized就像一把锁。用的时候打开资源,不用的时候关闭资源。

举个不太恰当的栗子(没别的意思别乱想)

有个厕所,只有一个位置。一个名叫VR的老哥在里面解急,synchronized就像门锁保护了他的隐私。外面有再多的人也没关系。如果没有这道门,那其实很尴尬。

  • 所以Stringbuffer一般都是多线程使用。StringBuilder是单线程使用。在大型的项目工作中,一般都是多线程。
  • 在单线程的时候,不建议使用Stringbuffer。因为你就一个人写代码没事加锁解锁,其实也很费资源。

其他区别

除了前面提到String没有appendreverse,还有其他的方法String也没有

比如delete删除,insert等等

StringStringBuffer也不能直接转换。如果想要转换,可以采用如下原则:

  • String变为StringBuffer:利用StringBuffer的构造方法或append()方法。
  • StringBuffer变为String:调用toString()方法。

面试题

请解释String、StringBuffer、StringBuilder的区别:

(老经典了)

  • String的内容不可修改,StringBuffer与StringBuilder的内容可以修改。
  • StringBuffer与StringBuilder大部分功能是相似的。
  • StringBuffer采用同步处理,属于线程安全操作;StringBuilder未采用同步处理,属于线程不安全操作。
广告位招租
最后修改:2021 年 12 月 09 日 08 : 04 PM
如果觉得我的文章对你有用,请喂饱我!(理直气壮)