一文看完String的前世今生,内容有点多,请耐心看完!
写在开头
String字符串作为一种引用类型,在Java中的地位举足轻重,也是代码中出现频率最高的一种数据结构,因此,我们需要像分析Object一样,将String作为一个topic,单独拿出来总结,这里面涉及到字符串的不可变性,字符串拼接、存储、比较、截取以及StringBuffer,StringBuilder区别等。
String类的源码
源码解读
想要真切的去了解Java中被定义好的一个类,读源码是最直接的方式,以经典的Java8为例(Java9之后,内部的实现数组类型从char改为了byte,目的用来节省内存空间),我们来看看Java中对于String是如何设计的。
public final class String implements java.io.Serializable,
Comparable<String>, CharSequence {
private final char value[];
//...
}
我们从源码中可以总结出如下几点内容:
1. String类被final关键字修饰,意味着它不可被继承;;
2. String的底层采用了final修饰的char数组,意味着它的不可变性;
3. 实现了Serializable接口意味着它支持序列化;
4. 实现了Comparable接口,意味着字符串的比较可以采用compareTo()方法,而不是==号,并且Sring类内部也重写了Object的equals()方法用来比较字符串相等。
String如何实现不可变得性?
从过源码我们可以看到类和char[]数组均被final关键字修饰,且数组的访问修饰符为private,访问权限仅限本类中。
final关键字修饰的类不能被继承,修饰的方法不能被重写,修饰的变量是基本数据类型则值不能改变,修饰的变量是引用类型则不能再指向其他对象。但光用final修饰只能保证不被子类继承,不存在子类的破坏,char数组中的字符串仍然是可以改变的。
但,当底层实现的这个char[]被private修饰后,代表着它的私有化,且String没有对外提供修改这个字符串数组的方法,这才导致了它的不可变!
String如为什么要不可变?
那么问题来了,String为什么要设计成不可变的呢?我们都知道,不可变意味着,每次赋值其实就是创建一个新的字符串对象进行存储,这无疑带来了诸多不便。但相比于以下2点,那些不便似乎无关紧要了
1、String 类是最常用的类之一,为了效率,禁止被继承和重写
2、为了安全。String 类中有很多调用底层的本地方法,调用了操作系统的API,
如果方法可以重写,可能被植入恶意代码,破坏程序。其实Java 的安全性在这里就有一定的体现啦。
String类的方法
因为使用频率非常高,所以String内部提供很多操作字符串的方法,常用的如下:
equals:字符串是否相同
equalsIgnoreCase:忽略大小写后字符串是否相同
compareTo:根据字符串中每个字符的Unicode编码进行比较
compareToIgnoreCase:根据字符串中每个字符的Unicode编码进行忽略大小写比较
indexOf:目标字符或字符串在源字符串中位置下标
lastIndexOf:目标字符或字符串在源字符串中最后一次出现的位置下标
valueOf:其他类型转字符串
charAt:获取指定下标位置的字符
codePointAt:指定下标的字符的Unicode编码
concat:追加字符串到当前字符串
isEmpty:字符串长度是否为0
contains:是否包含目标字符串
startsWith:是否以目标字符串开头
endsWith:是否以目标字符串结束
format:格式化字符串
getBytes:获取字符串的字节数组
getChars:获取字符串的指定长度字符数组
toCharArray:获取字符串的字符数组
join:以某字符串,连接某字符串数组
length:字符串字符数
matches:字符串是否匹配正则表达式
replace:字符串替换
replaceAll:带正则字符串替换
replaceFirst:替换第一个出现的目标字符串
split:以某正则表达式分割字符串
substring:截取字符串
toLowerCase:字符串转小写
toUpperCase:字符串转大写
trim:去字符串首尾空格
方法有很多,无法一一讲解,我们挑选几个聊一聊哈
方法1、hashCode
由于Object中有hashCode()方法,所以所有的类中都有对应的方法,在String中做了如下的实现:
private int hash; // 缓存字符串的哈希码
public int hashCode() {
int h = hash; // 从缓存中获取哈希码
// 如果哈希码未被计算过(即为 0)且字符串不为空,则计算哈希码
if (h == 0 && value.length > 0) {
char val[] = value; // 获取字符串的字符数组
// 遍历字符串的每个字符来计算哈希码
for (int i = 0; i < value.length; i++) {
h = 31 * h + val[i]; // 使用 31 作为乘法因子
}
hash = h; // 缓存计算后的哈希码
}
return h; // 返回哈希码
}
先检核是否已计算哈希,若已计算则直接返回,否则根据31倍哈希法进行计算并缓存计算后的哈希值。String中重写后的hashCode方法,计算效率高,随机性强,哈希碰撞概率小,所以常被用作HashMap中的Key。
方法2、equals
我们在之前的文章中曾提到过重写equals方法往往也需要重写hashCode方法,这一点String就做到了,我们来看看String中equals()方法的实现:
public boolean equals(Object anObject) {
if (this == anObject) {
return true;
}
if (anObject instanceof String) {
String anotherString = (String)anObject;
int n = value.length;
if (n == anotherString.value.length) {
char v1[] = value;
char v2[] = anotherString.value;
int i = 0;
while (n-- != 0) {
if (v1[i] != v2[i])
return false;
i++;
}
return true;
}
}
return false;
}
这是Java8中的实现,逻辑清晰易懂,首先,通过==判断是否是同一个对象,如果是则直接返回true,否则进入下一轮判断逻辑:判断对象是否为String类型,再判断两个字符串长度是否相等,再比较每个字符是否相等,全部为true最后返回true,其中有任何一个为flase则返回false。
方法3、substring
该方法在日常开发中时常被用到,主要用来截取字符串,源码:
public String substring(int beginIndex) {
// 检查起始索引是否小于 0,如果是,则抛出 StringIndexOutOfBoundsException 异常
if (beginIndex < 0) {
throw new StringIndexOutOfBoundsException(beginIndex);
}
// 计算子字符串的长度
int subLen = value.length - beginIndex;
// 检查子字符串长度是否为负数,如果是,则抛出 StringIndexOutOfBoundsException 异常
if (subLen < 0) {
throw new StringIndexOutOfBoundsException(subLen);
}
// 如果起始索引为 0,则返回原字符串;否则,创建并返回新的字符串
return (beginIndex == 0) ? this : new String(value, beginIndex, subLen);
}
使用案例:
注意源码中注释提到的:如果 beginIndex 为 0,说明子串与原字符串相同,直接返回原字符串。否则,使用 value 数组(原字符串的字符数组)的一部分 new 一个新的 String 对象并返回。
String str = "Hello, world!";
String pre = str.substring(0);
System.out.println(pre);
String prefix = str.substring(0, 5);
System.out.println(prefix);
String suffix = str.substring(7);
System.out.println(suffix);
输出:
Hello, world!
Hello
world!
方法4、indexOf
indexOf的主要作用是获取目标字符或字符串在源字符串中位置下标,看源码:
/**
* 由 String 和 StringBuffer 共享的用于执行搜索的代码。这
* source 是正在搜索的字符数组,目标
* 是要搜索的字符串。
*
* @param正在搜索的字符的来源。
* @param源字符串的 sourceOffset 偏移量。
* @param源字符串的 sourceCount 计数。
* @param定位正在搜索的字符。
* @param目标字符串的 targetOffset 偏移量。
* @param目标字符串的 targetCount 计数。
* @param fromIndex 要开始搜索的索引。
*/
static int indexOf(char[] source, int sourceOffset, int sourceCount,
char[] target, int targetOffset, int targetCount,
int fromIndex) {
if (fromIndex >= sourceCount) {
return (targetCount == 0 ? sourceCount : -1);
}
if (fromIndex < 0) {
fromIndex = 0;
}
if (targetCount == 0) {
return fromIndex;
}
char first = target[targetOffset];
int max = sourceOffset + (sourceCount - targetCount);
for (int i = sourceOffset + fromIndex; i <= max; i++) {
/* Look for first character. */
if (source[i] != first) {
while (++i <= max && source[i] != first);
}
/* Found first character, now look at the rest of v2 */
if (i <= max) {
int j = i + 1;
int end = j + targetCount - 1;
for (int k = targetOffset + 1; j < end && source[j]
== target[k]; j++, k++);
if (j == end) {
/* Found whole string. */
return i - sourceOffset;
}
}
}
return -1;
}
使用案例一
String str = "Hello, world!";
int index = str.indexOf("wor"); // 查找 "world" 子字符串在 str 中第一次出现的位置
System.out.println(index); // 输出 7,字符串下标从0开始,空格也算一位
使用案例二
String str = "Hello, world!";
int index1 = str.indexOf("o"); // 查找 "o" 子字符串在 str 中第一次出现的位置
int index2 = str.indexOf("o", 5); // 从索引为5的位置开始查找 "o" 子字符串在 str 中第一次出现的位置
System.out.println(index1); // 输出 4
System.out.println(index2); // 输出 8
方法五、replace与replaceAll
话不多说,直接看码
///replace是字符和字符串的替换操作,基于字符匹配
public String replace(char oldChar, char newChar)
public String replace(CharSequence target, CharSequence replacement)
//replaceAll是基于正则表达式的字符串匹配与替换
public String replaceAll(String regex, String replacement)
使用案例:
String str = "Hello Java. Java is a language.";
//查找原字符串中所有Java子串,并用c进行替换
System.out.println(str.replace("Java", "c"));
//根据正则表达式匹配规则,.代表是任意字符 可以匹配任何单个字符
//所以经过正则匹配后,找出原字符串中所有“Java”+”任意一个字符”的子串,用c进行替换!
System.out.println(str.replaceAll("Java.", "c"));
输出:
Hello c. c is a language.
Hello c cis a language.
String类的使用
学以致用,学习的最终目的就是使用!
字符串常量池
搞清楚字符串常量池之前,我们先看如下这条语句,考你们一下,这行代码创建了几个对象?
String s1 = new String("abc");
这个答案并不是唯一的
第一种情况:若字符串常量池中不存在“abd”对象的引用,则语句会在堆中闯将2个对象,其中一个对象的引用保存到字符串常量池中。
这种情况下的字节码(JDK1.8)
ldc 命令用于判断字符串常量池中是否保存了对应的字符串对象的引用,如果保存了的话直接返回,如果没有保存的话,会在堆中创建对应的字符串对象并将该字符串对象的引用保存到字符串常量池中。
第二种情况: 如果字符串常量池中已存在字符串对象“abc”的引用,则只会在堆中创建 1 个字符串对象“abc”。
// 字符串常量池中已存在字符串对象“abc”的引用
String s1 = "abc";
// 下面这段代码只会在堆中创建 1 个字符串对象“abc”
String s2 = new String("abc");
看到这里我们大致可以明白什么时字符串常量池,以及它的作用了:
字符串常量池是JVM 为了提升性能和减少内存消耗针对字符串(String 类)专门开辟的一块区域,主要目的是为了避免字符串的重复创建。
字符串引用的存储
在上面的内容中,我们了解了字符串常量池,那么Java中是怎么将字符串的引用保存到常量池中的呢,这里我们需要提到String的intern()方法。
String.intern() 是一个native(本地)方法
其作用是将指定的字符串对象的引用保存在字符串常量池中。
若字符串常量池中保存了对应的字符串对象的引用,就直接返回该引用;
若字符串常量池中没有保存了对应的字符串对象的引用,那就在常量池中创建一个指向该字符串对象的引用并返回。
我们看下面一段代码:
String s1 = new String("Hello") + new String("World");
String s2 = s1.intern();
System.out.println(s1 == s2);
你们觉得返回的是false还是true?如果还不明白,那么请看一下美团团队发布的一篇文章
美团技术团队深入解析 String.intern()
字符串的拼接
你是不是曾用过“+”进行字符串的拼接操作,比如说String res = "str" + "ing"; 。
,最终输出的就是string
出现这样的效果的原因是Java编译器的优化功能-常量折叠!
对于编译期可以确定值的字符串,也就是常量字符串 ,jvm 会将其存入字符串常量池。并且,字符串常量拼接得到的字符串常量在编译阶段就已经被存放字符串常量池,这个得益于编译器的优化。
优化前:String res = "str" + "ing";
优化后:String res = "string";
但像对象引用这种情况,无法在编译其进行优化,我们看下面这段
String str1 = "str";
String str2 = "ing";
System.out.println(str1+str2);
字节码(JDK1.8)
通过字节码我们可以分析出,通过+号将几个对象引用进行拼接,实际上是调用StringBuilder().append(str1).append(str2).toString();
来实现的。
但有几个对象引用拼接,就会创建几个StringBuilder对象,浪费资源,因此,在做字符串拼接时直接采用StringBuilder实现!
String、StringBuffer,StringBuilder区别
相同点:
1、都可以储存和操作字符串
2、都使用 final 修饰,不能被继承
3、提供的 API 相似
异同点:
1、String 是只读字符串,String 对象内容是不能被改变的
2、StringBuffer 和 StringBuilder 的字符串对象可以对字符串内容进行修改,在修改后的内存地址不会发生改变
3、StringBuilder 线程不安全;StringBuffer 线程安全
一文看完String的前世今生,内容有点多,请耐心看完!的更多相关文章
- 从字符串到常量池,一文看懂String类设计
从一道面试题开始 看到这个标题,你肯定以为我又要讲这道面试题了 // 这行代码创建了几个对象? String s3 = new String("1"); 是的,没错,我确实要从这里 ...
- 通过反编译看Java String及intern内幕--费元星站长
通过反编译看Java String及intern内幕 一.字符串问题 字符串在我们平时的编码工作中其实用的非常多,并且用起来也比较简单,所以很少有人对其做特别深入的研究.倒是面试或者笔试的时候,往 ...
- C# 复制一个Word文档的部分或全部内容到另一个Word文档
C# 复制一个Word文档的部分或全部内容到另一个Word文档 我最近喜欢折腾Office软件相关的东西,想把很多Office软件提供的功能用.NET来实现,如果后期能把它用来开发一点我自己的小应用程 ...
- dzzoffice教程、文档、开发手册等内容地址
dzzoffice教程.文档.开发手册等内容全部都存放在DzzOffice开发者社区的文集中.搜索引擎收录不到DzzOffice中的应用内容,这里将文集地址提供在这里. 地址:http://dev.d ...
- html5权威指南:嵌入另一张HTML文档、通过插件嵌入内容、嵌入数字表现形式
嵌入另一张HTML文档.通过插件嵌入内容.嵌入数字表现形式:http://www.cnblogs.com/yc-755909659/p/5928125.html
- python模块之httplib(在py3中功能进一步强大,请详看文档)
# -*- coding: utf-8 -*-#python 27#xiaodeng#python模块之httplib(在py3中功能进一步强大,请详看文档) import httplib#是较为底层 ...
- String s="hello";s+="world";s变化了吗?原始的String对象的内容变了吗?
分析: String s="hello";s+="world"; 引用变量s 一开始指向String对象("hello" :0x001); ...
- IntelliJ idea鼠标移动到类上显示文档document(javadoc)内容
IntelliJ idea鼠标移动到类上显示文档document(javadoc)内容 Step 1:设置鼠标移动到类上自动显示Javadoc文档 step2:为jdk下载javadoc Step3: ...
- Elasticsearch学习,请先看这一篇!
原文:Elasticsearch学习,请先看这一篇! 版权声明:本文为博主原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接和本声明. 本文链接:https://blog.csdn ...
- activiti怎么实现用户自定义流程?请先看这里
最近一两个星期收到了好几个qq好友添加的请求和csdn的私信,里面基本都是询问activiti相关的问题. 尤其是今天有个朋友给我发了私信,内容如下: 你好,请问你关于activiti工作流的问题:怎 ...
随机推荐
- ElasticSearch之Delete index API
删除指定的索引. 同时删除索引关联的数据.分片.元数据等相关的资源,因此执行前需要慎重. 命令样例如下: curl -X DELETE "https://localhost:9200/tes ...
- MySQL|空间碎片化问题处理
一.空间碎片化严重案例分享 1.1 问题描述 实例磁盘空间近1个月上涨趋势明显,主要是个别日志表存储较大且部分表存在空间碎片化的现象. 1.2 处理流程 1.通过日常巡检以及监控发现某实例磁盘空间近1 ...
- spring自定义session分布式session
spring实现自定义session.springboot实现自定义session.自定义sessionid的key.value.实现分布式会话 一.原始方案 自定义生成sessionid的值 修改t ...
- MyBatis中使用#{}和${}占位符传递参数的各种报错信息处理
在Mapper层使@Select注解进行SQL语句查询时,往往需要进行参数传入和拼接,一般情况下使用两种占位符#{参数名}和${参数名},两者的区别为: 一.两种占位符的区别 1.参数传入方式的区别 ...
- Provider的八种提供者
代码 class Example extends StatelessWidget { @override Widget build(BuildContext context) { return Sca ...
- 案例解析关于ArkUI框架中ForEach的潜在陷阱与性能优化
本文分享自华为云社区<深入解析ForEach的潜在陷阱与性能优化:错误用法与性能下降的案例分析>,作者:柠檬味拥抱 . 在ArkUI框架中,ForEach接口是基于数组类型数据进行循环渲染 ...
- 1024 | 9位开发者分享生涯“最”时刻,文武状元大PK等你来
本文分享自华为云社区<1024程序员节,和华为云一起做不被定义的开发者>,作者:华为云社区精选 . 1024,祝所有开发者们节日快乐 "代码有注释,程序无bug, 需求不改动,永 ...
- 从零开始学python | 使用Python映射,过滤和缩减函数:所有您需要知道的
摘要:在本文中,您将学习Python中的三个令人印象深刻的函数,即map(),filter和reduce(). Python提供了许多预定义的内置函数,最终用户可以通过调用它们来使用它们.这些功能不仅 ...
- 看FusionInsight Spark如何支持JDBCServer的多实例特性
摘要:采用多主实例模式的HA方案,不仅可以规避主备切换服务中断的问题,实现服务不中断或少中断,还可以通过横向扩展集群来提高并发能力. 本文分享自华为云社区<FusionInsight Spark ...
- ImproperlyConfigured('SQLite 3.8.3 or later is required Centos升级SQLite
遇到这个错误可以选择给django降级,不建议 这里选择升级SQLite 1.查看版本 sqlite3 --version 2.Centos7安装最新的sqlite3 wget https://www ...