说明
这是一个完善了但又不完善的笔记,或许以后会更新
可以参考但请务必超越
源文件
Tools
认识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;
这个意思很好理解吧,内存布局就是这样的
说明str1
和str2
在引用同一个对象
现在str2
指向别处,当然是不会影响str1
str2 = "world";
System.out.println(str1 + str2);
问题一
既然str1
和str2
都指向一个"hello"
,那么可不可以通过修改str2
来修改"hello"
?
答案是不可以的
双引号引起来的内容属于字面值常量,str2 = "world"
只是修改了指向
不是说传引用就能改变实参的值。要看这个引用做了什么
更深层次的内容接下来再看
2.3 字符串常量池
上面的问题有一个点,双引号引起来的内容属于字面值常量
那么我们来看下面的问题
现在让str1
和str2
各自新建对象
String str1 = "hello";
String str2 = new String("hello");
System.out.println(str1 == str2);
这个时候str1
和str2
相等吗?
根据上面引用指向的图,可以知道str1
和str2
是2个引用,2个引用当然是不相等的
注意,虽然引用不相等,但是我们看双引号引起来的hello
字符串常量嘛。那这2个"hello"
,是不是相等呢?
常量池简述
通常会听到3个常量池。会涉及到JVM的内容,JVM以后或许会讲,现在先简单讲述一下常量池
- Class文件常量池:程序编译好后会产生字节码文件,比如
int a = 10;
这里的10就在Class文件常量池中。存放在磁盘上。 - 运行时常量池:当程序把编译好的字节码文件加载到JVM后,会生成一个运行时常量池[方法区]、实际上就是Class文件常量池从磁盘上加载到方法区。
- 字符串常量池:主要存放字符串常量,本质上是一个哈希表。StringTable。双引号引起来的字符串常量。
从JDK1.8开始,字符串常量池放在了堆里面。也就是说以后由堆来维护这个哈希表。
池
什么是池?
以后或许会接触到数据库连接池,线程池
他的意义是提高效率
池子嘛,当用的时候就用现成的,抽象出一个池子,仓库一般不流动,需要自己去找。池子就自动流动,自动存储,所以就会自动帮我们完成一些固定的内容。
哈希表
哈希表:是一种数据结构
如何去查找到一个关键字,一般有2种方法
- 顺序查找:通过关键字一个一个来比较,时间复杂度可能高达O(n)
- 排序+二分查找
哈希表呢,在存储数据的时候,会根据一个映射关系进行存储。如何映射,需要设计一个函数,也就是哈希函数。
假设现在有这么一组数据,要存放到长度为10的一个空间。
1、11、2、22、4、44、5、55、9、99
那么就可以设计一个函数,假设就根据key % len
来存储
当放进去第一个值,第二个值会和第一个值出现冲突时,就会使用链表顺序挂到后面,每一个数据就是一个节点。取的时候也就是这样取的
这里借鉴百度百科的内容,可以自行查看下百度百科给出的图片
散列表(Hash table,也叫哈希表),是根据关键码值(Key value)而直接进行访问的数据结构。也就是说,它通过把关键码值映射到表中一个位置来访问记录,以加快查找的速度。这个映射函数叫做散列函数,存放记录的数组叫做散列表。
在数据结构中,还会再讲相关内容
使用哈希表,最快可以达到O(1)
2.4 验证存储
上面的一大堆概念,足以证明2个"hello"
其实是一个存放在哈希表里的数据
我们现在已知2个引用str1
和str2
是不相等的
那么,我们就来看看2个"hello"
在内存中的存储吧
前面有说,从JDK1.8开始,字符串常量池放在了堆里面。
那么我们首先可以把哈希表画出来
String str1 = "hello";
那么第一个"hello"
在哈希表中应当是这样存储的
- 会生成哈希值
- 会有
next
域来链接下一个节点 - 新产生了一个对象
根据上面3个点,可以画出这样的str1
注意虽然比较绕,但是还是很好理解的
str1
新创建的对象由哈希表通过节点来管理,有下一个节点就接着链接
那么str2
就较为简单了
首先检查哈希表中存在"hello"
,那么就直接给到str2
的对象就好了。但是可以看到str2
就需要产生2个对象
调式
我们直接打个断点调试一下
可以看到value值是一样的,也验证了上面画的图是对的
2.5 openJDK
关于最底层的JVM源代码,实际上应该是没有方法可查看的,因为不开源
如果想查看,可以查看openJDK的内容
具体方法这里就不讲了,网上搜索多得是,也很简单
2.6 其他比较(重要)
我们前面用str1
和str2
的比较来讲了常量池,哈希表
str1
和str2
的比较当然可以有其他的几种情况
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");
结合前面的内容,可以知道str1
和str2
不相等
看这里的字节码文件,有一个StringBuilder.append
方法,实际上就是通过这个方法来新创建了一个对象来保存2个1
StringBuilder
后面还会讲
先来把这个图画一下加深理解
因为不知道StringBuilder
,所以看起来很复杂,其实也确实很复杂。但一步一步顺着来,逐渐理解就能明白了
- 哈希表会存一个1
str2
创建了2个对象,每个对象的value
都引用一个1StringBuilder
把2个1合成11,使用val
指向11StringBuilder
通过toString
方法再创建一个对象,其中的value
也指向11,让str2
引用这个对象
一步一步理解后,发现其实还是很简单。当然也完全不用研究这么深入,str1
和str2
就是2个引用,他们不相等
手动入池
前面最后一种情况,StringBuilder
虽然合成了一个11,但是并没有入池,直接用toString
创建了一个对象让str2
引用
那么接下来可以用这样一个方法
intern();//当常量池没有的时候就会入池
这个方法就是手动入池
意思就是我们手动的把他放到哈希表里嘛
假设现在str1
和str2
换了位置,池子里没有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"
还有str1
和str2
的拼接
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
方法即可修改
还有一种方法,要比较厉害
反射
我们前面看到过String
的value
字段
他是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要不可变?(不可变对象的好处是什么?)
- 方便实现字符串对象池。如果String可变。那么对象池就需要考虑何时深拷贝字符串的问题了。
- 不可变对象是线程安全的。
- 不可变对象更方便缓存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);
第一个就不用多说了,直接传进去就好了
第二个还有offset
和count
2个参数
一个是偏移量,一个是个数
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是有这样的方法的
可以看到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种比较
- 真假比较(内容)
- 大小比较
equals
点开String
的equals
看一下嘛
比较规则:先比较地址,地址相同那就直接返回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);
}
}
}
先通过&
把name
和age
拆分,再通过=
把其中的内容也拆分。foreach
循环拆分最后打印
注意,有返回值,是数组
比较特殊的情况,拆分IP地址
127.0.0.1
如果我们直接按.
来拆分,就会出现转义的问题,具体问题需要具体计算。
- 字符
"."
,"|"
,"*"
,"+"
都得加上转义字,前面加上"\"。 - 而如果是
"\"
,那么就得写成"\\\\"
,就得数着凑齐。 - 如果一个字符串中有多个分隔符,可以用
"|"
作为连字符。
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.StringBuffer
和StringBuilder
注意,这两个数据类型和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
不同
他返回的是this
,this
是当前对象的引用嘛,那就是把要添加的内容直接返回给了当前对象,改变了当前对象,而不是像String
的方法new
了一个新的对象不改变原对象
这就很好理解了
甚至append
还能连用
String
和StringBuilder
现在知道了StringBuilder
和append
,那前面String
做比较时源代码出现的StringBuilder
和append
就可以理解了
现在我们写个简单的代码来举栗
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"
进行了如下操作
new
了一个新的StringBuilder
对象- 调用无参数的构造方法
- 将当前
String
字符串的内容append
添加进去 append(i)
添加数字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
。优先使用Stringbuffer
和StringBuilder
那问题来了
Stringbuffer
和StringBuilder
有什么区别,前面已经讲了一个,那现在就剩下Stringbuffer
需要讲
StringBuffer
从前面的内容,可以看到String
其实没有很全面,比如append
这个方法String
就没有
还有字符串逆序
reverse()
String
也没有
但是Stringbuffer
和StringBuilder
就有,可以自行查看
那么既然方法都有,区别在哪里呢?
随便找一个append
方法
StringBuffer.append(String)
:
StringBuilder.append(String)
:
synchronized
首先,基本上所有的方法,Stringbuffer
都要比StringBuilder
多一个关键字synchronized
保证线程安全
什么意思呢?
当程序在运行的时候,synchronized
就像一把锁。用的时候打开资源,不用的时候关闭资源。
举个不太恰当的栗子(没别的意思别乱想)
有个厕所,只有一个位置。一个名叫VR的老哥在里面解急,synchronized
就像门锁保护了他的隐私。外面有再多的人也没关系。如果没有这道门,那其实很尴尬。
- 所以
Stringbuffer
一般都是多线程使用。StringBuilder
是单线程使用。在大型的项目工作中,一般都是多线程。 - 在单线程的时候,不建议使用
Stringbuffer
。因为你就一个人写代码没事加锁解锁,其实也很费资源。
其他区别
除了前面提到String
没有append
和reverse
,还有其他的方法String
也没有
比如delete
删除,insert
等等
String
和StringBuffer
也不能直接转换。如果想要转换,可以采用如下原则:
String
变为StringBuffer
:利用StringBuffer
的构造方法或append()
方法。StringBuffer
变为String
:调用toString()
方法。
面试题
请解释String、StringBuffer、StringBuilder的区别:
(老经典了)
- String的内容不可修改,StringBuffer与StringBuilder的内容可以修改。
- StringBuffer与StringBuilder大部分功能是相似的。
- StringBuffer采用同步处理,属于线程安全操作;StringBuilder未采用同步处理,属于线程不安全操作。
《那不是抄袭吗?(特别加长版 )》日本剧高清在线免费观看:https://www.jgz518.com/xingkong/141848.html
《闪婚老公竟是我上司》短片剧高清在线免费观看:https://www.jgz518.com/xingkong/14790.html
你的才华让人瞩目,期待你的更多文章。 http://www.55baobei.com/ywC0pN6M11.html
想想你的文章写的特别好www.jiwenlaw.com