一、String类型介绍

String类型是引用数据类型,表示字符串类型。String底层使用byte[]数组来存储char[]数组。(JDK1.9及以后的版本,JDK1.9之前是使用char数组保存,1.9为了节省空间,开始使用byte数组保存)

@Stable
private final byte[] value;//定义byte数组用于存储构造函数传进的char数组,最下方的代码中有用到。

从上方的代码中可以看出,String用于保存数据的数组是private、final的,因此String类型是不可变的。

//String的构造函数
public String(char value[]) {  this(value, 0, value.length, null);//调用另一个构造函数,代码在下方     }
String(char[] value, int off, int len, Void sig) {if (len == 0) {this.value = "".value;this.coder = "".coder;return;}if (COMPACT_STRINGS) {byte[] val = StringUTF16.compress(value, off, len);if (val != null) {this.value = val;this.coder = LATIN1;return;}}this.coder = UTF16;this.value = StringUTF16.toBytes(value, off, len);
}

二、String类型的存储

虚拟机运行时内存(JDK1.8以后)

JVM内存中与String类型存储相关的结构主要有堆和虚拟机栈。

常量池

常量池在java用于保存在编译期已确定的,已编译的class文件中的一份数据。它包括了关于类,方法,接口等中的常量,也包括字符串常量,如String s = "java"这种申明方式;当然也可扩充,执行器产生的常量也会放入常量池,故认为常量池是JVM的一块特殊的内存空间。

通过常量池的使用String实现了多个引用指向同一个常量池中的对象,大大的节省了内存空间的开销。
JDK1.8之后,常量池存放于JVM运行时内存中的堆内存中。

String对象的创建

主要有以下两种创建String对象的方式
1、String a="abcd";
使用这种创建方式时,若常量池中不存在"abcd"这个String对象,则会创建2个对象:在常量池中创建String类型的对象"abcd",常量池位于上图所示的堆内存中、在栈中创建引用a保存"abcd"的内存地址,从而指向常量池中的"abcd"对象,栈既上图所示的虚拟机栈。

若常量池中已存在"abcd"对象,则会直接返回这个对象,只在栈中创建一个引用a指向该对象。

2、String a=new String("abcd");
使用这种创建方式时,若常量池中不存在值为"abcd"的String对象,则会先在常量池中创建一个值为“abcd”的String对象,然后将其复制一份到堆内存中(常量池外,堆内存中,地址不同),然后在栈中创建一个引用a保存"abcd"在堆中的地址,从而指向堆内存中的该对象。共创建了三个对象
若常量池重已存在对象“abcd”,则省去在常量池中创建对象的这一步,共创建两个对象

三、String类型的拼接

通过concat方法拼接

String a="a";
String b="b";
System.out.println(a.concat(b));//通过a对象concat方法连接b对象,结果为"ab"

下面来看看concat方法的源码

public String concat(String str) {int olen = str.length();if (olen == 0) {return this;}if (coder() == str.coder()) {//coder来标识字符串的编码格式是LATIN1还是UTF16,若两个字符串的编码格式相等,则不用进行编码格式转换byte[] val = this.value;byte[] oval = str.value;int len = val.length + oval.length;//拼接后字符串的长度byte[] buf = Arrays.copyOf(val, len);//创建一个新数组存放拼接后的字符串System.arraycopy(oval, 0, buf, val.length, oval.length);return new String(buf, coder);}int len = length();byte[] buf = StringUTF16.newBytesFor(len + olen);getBytes(buf, 0, UTF16);str.getBytes(buf, len, UTF16);return new String(buf, UTF16);}

从concat源码中容易得出,concat方法通过创建一个长度为两字符串长度之和的byte数组来存放两字符串,然后将两个字符串依次放入数组中,实现了字符串的拼接。
至于为什么使用byte数组,上面讲过,String类型底层使用byte数组存储char数组,因此concat使用byte数组来存储字符串,如果用其他类型的数组就要进行类型转换。
注意:concat方法并不会对原对象进行改变,而是会返回一个新的String对象。

通过+号拼接

通过+号的拼接主要分为两种情况:有字符串变量(既在栈中创建的引用)参与的拼接无字符串变量参与,只有字符串常量(常量池中的String对象)参与的拼接有字符串变量(既在栈中创建的引用)参与的拼接:

在网上找了下有字符串变量参与+号拼接的实现原理,大部分说的都是:

运行时, 两个字符串str1, str2的拼接首先会调用String.valueOf(obj),这个Obj为str1,而String.valueOf(Obj)中的实现是return obj ==null ? “null” : obj.toString()。
然后产生StringBuilder, 调用的StringBuilder(str1)构造方法, 把StringBuilder初始化,长度为str1.length()+16,并且调用append(str1)!接下来调用StringBuilder.append(str2), 把第二个字符串拼接进去, 然后调用StringBuilder.toString返回结果。

下面我就得从底层中看看它们是如何实现拼接的。
打以下代码:

public class Test{public static void main(String[] args){String str1 = "111111";String str2 = "222222";String str = str1 + str2;System.out.println(str);}
}

然后进入dos界面,在dos界面中进入文件所在文件夹,使用javac Test.java命令生成字节码,再使用javap -verbose Test命令进行反编译,可以看到以下结果。(JDK1.9及以后的版本才能看到如下结果,JDK1.8及以前的可参考这篇博文:Java String + 拼接字符串原理)

容易看出以下两行代码 ,对应的是String str = str1 + str2;语句

8: invokedynamic #4,  0              // InvokeDynamic #0:makeConcatWithConstants:(Ljava/lang/String;Ljava/lang/String;)Ljava/lang/String;
13: astore_3

动态指令invokedynamic指令会调用makeConcatWithConstants方法进行字符串的连接。
该方法位于java.lang.invoke.StringConcatFactory类中。
下面是源码,容易看出这个方法里如果没出问题,是直接调用doStringConcat方法

public static CallSite makeConcatWithConstants(MethodHandles.Lookup lookup,String name,MethodType concatType,String recipe,Object... constants) throws StringConcatException {if (DEBUG) {System.out.println("StringConcatFactory " + STRATEGY + " is here for " + concatType + ", {" + recipe + "}, " + Arrays.toString(constants));}return doStringConcat(lookup, name, concatType, false, recipe, constants);
}

下面是doStringConcat方法的部分源码,多的就省略了。可以看到返回值中,mh调用asType方法适配得到MethodHandle对象,返回值的逻辑就是单纯的返回一个结果,字符串拼接是在mh对象生成的时候进行的,也就是在generate方法中进行。

private static CallSite doStringConcat(MethodHandles.Lookup lookup,String name,MethodType concatType,boolean generateRecipe,String recipe,Object... constants) throws StringConcatException {
......
MethodHandle mh;
if (CACHE_ENABLE) {Key key = new Key(className, mt, rec);mh = CACHE.get(key);if (mh == null) {mh = generate(lookup, className, mt, rec);CACHE.put(key, mh);}
} else {mh = generate(lookup, className, mt, rec);
}
return new ConstantCallSite(mh.asType(concatType));

下面是generate方法的源码

private static MethodHandle generate(Lookup lookup, String className, MethodType mt, Recipe recipe) throws StringConcatException {try {switch (STRATEGY) {case BC_SB:return BytecodeStringBuilderStrategy.generate(lookup, className, mt, recipe, Mode.DEFAULT);case BC_SB_SIZED:return BytecodeStringBuilderStrategy.generate(lookup, className, mt, recipe, Mode.SIZED);case BC_SB_SIZED_EXACT:return BytecodeStringBuilderStrategy.generate(lookup, className, mt, recipe, Mode.SIZED_EXACT);case MH_SB_SIZED:return MethodHandleStringBuilderStrategy.generate(mt, recipe, Mode.SIZED);case MH_SB_SIZED_EXACT:return MethodHandleStringBuilderStrategy.generate(mt, recipe, Mode.SIZED_EXACT);case MH_INLINE_SIZED_EXACT:return MethodHandleInlineCopyStrategy.generate(mt, recipe);default:throw new StringConcatException("Concatenation strategy " + STRATEGY + " is not implemented");}} catch (Error | StringConcatException e) {// Pass through any error or existing StringConcatExceptionthrow e;} catch (Throwable t) {throw new StringConcatException("Generator failed", t);}
}

generate方法通过不同的STRATEGY(策略)值来调用不同对象的generate方法。那么,接下来看看Strategy类型,对文档中的英文进行了一些简单的翻译。

private enum Strategy {/*** 字节码生成器,调用{@link java.lang.StringBuilder}.*/BC_SB,/*** 字节码生成器,调用 {@link java.lang.StringBuilder};* 但要估计所需的存储空间。*/BC_SB_SIZED,/*** 字节码生成器,调用 {@link java.lang.StringBuilder};* 但需要精确地计算所需的存储空间。*/BC_SB_SIZED_EXACT,/***基于MethodHandle的生成器,最终调用 {@link java.lang.StringBuilder}.* 此策略还尝试估计所需的存储空间。*/MH_SB_SIZED,/*** 基于MethodHandle的生成器,最终调用 {@link java.lang.StringBuilder}.* 此策略也需要准确地计算所需的存储空间。*/MH_SB_SIZED_EXACT,/*** 基于MethodHandle的生成器, 基于MethodHandle的生成器,从参数构造自己的byte[]数组。它精确地计算所需的存储空间。*/MH_INLINE_SIZED_EXACT}

主要就是针对不同的情况,使用不同的策略值,共六种策略,从而能调用适用于当前情况的generate方法。上面五种策略的实现都是基于StringBuilder。
接下来以上面的BytecodeStringBuilderStrategy中的generate方法为例,来具体看一看是怎么实现字符串拼接的(套了一堆娃,终于到正题了)
首先,是调用String的ValueOf()方法

if (mode.isExact()) {
/*在精确模式下,我们需要将所有参数转换为字符串表示,因为这允许精确计算它们的字符串大小。我们不能在这里使用私有的原语方法,因此我们也需要转换它们。我们还记录了转换结果中保证为非null的参数。字符串.valueOf是否为我们检查空。唯一极端的情况是字符串.valueOf(对象)返回null本身。此外,如果发生任何转换,则传入参数中的插槽索引不等于最终的本地映射。唯一可能会中断的情况是将2-slot long/double转换为1-slot时。因此,我们可以跟踪修改过的偏移,因为没有转换可以覆盖即将到来的参数。
*/int off = 0;int modOff = 0;for (int c = 0; c < arr.length; c++) {Class<?> cl = arr[c];if (cl == String.class) {if (off != modOff) {mv.visitIntInsn(getLoadOpcode(cl), off);mv.visitIntInsn(ASTORE, modOff);}} else {mv.visitIntInsn(getLoadOpcode(cl), off);mv.visitMethodInsn(INVOKESTATIC,"java/lang/String","valueOf",getStringValueOfDesc(cl),false);mv.visitIntInsn(ASTORE, modOff);arr[c] = String.class;guaranteedNonNull[c] = cl.isPrimitive();}off += getParameterSize(cl);modOff += getParameterSize(String.class);}}if (mode.isSized()) {/*在调整大小模式(包括精确模式)下操作时,让StringBuilder附加链看起来熟悉优化StringConcat是有意义的。为此,我们需要尽早进行空检查,而不是使附加链形状更简单。*/int off = 0;for (RecipeElement el : recipe.getElements()) {switch (el.getTag()) {case TAG_CONST:// Guaranteed non-null, no null check required.break;case TAG_ARG:// Null-checks are needed only for String arguments, and when a previous stage// did not do implicit null-checks. If a String is null, we eagerly replace it// with "null" constant. Note, we omit Objects here, because we don't call// .length() on them down below.int ac = el.getArgPos();Class<?> cl = arr[ac];if (cl == String.class && !guaranteedNonNull[ac]) {Label l0 = new Label();mv.visitIntInsn(ALOAD, off);mv.visitJumpInsn(IFNONNULL, l0);mv.visitLdcInsn("null");mv.visitIntInsn(ASTORE, off);mv.visitLabel(l0);}off += getParameterSize(cl);break;default:throw new StringConcatException("Unhandled tag: " + el.getTag());}}}

然后是生成StringBuilder对象并使用append方法依次将字符串加入

// 准备StringBuilder实例
mv.visitTypeInsn(NEW, "java/lang/StringBuilder");
mv.visitInsn(DUP);if (mode.isSized()) {/*大小模式要求我们遍历参数,并估计最终长度。在精确模式下,这将仅在字符串上操作。此代码将在堆栈上累积最终长度。*/int len = 0;int off = 0;mv.visitInsn(ICONST_0);for (RecipeElement el : recipe.getElements()) {switch (el.getTag()) {case TAG_CONST:len += el.getValue().length();break;case TAG_ARG:/*如果一个参数是String,那么我们可以对它调用.length()。大小/精确模式为我们转换了参数。如果一个参数是原始的,我们可以猜测它的字符串表示大小。*/Class<?> cl = arr[el.getArgPos()];if (cl == String.class) {mv.visitIntInsn(ALOAD, off);mv.visitMethodInsn(INVOKEVIRTUAL,"java/lang/String","length","()",false);mv.visitInsn(IADD);} else if (cl.isPrimitive()) {len += estimateSize(cl);}off += getParameterSize(cl);break;default:throw new StringConcatException("Unhandled tag: " + el.getTag());}}// 常数具有非零长度,混合if (len > 0) {iconst(mv, len);mv.visitInsn(IADD);}mv.visitMethodInsn(INVOKESPECIAL,"java/lang/StringBuilder","<init>","(I)V",false);
} else {mv.visitMethodInsn(INVOKESPECIAL,"java/lang/StringBuilder","<init>","()V",false);
}// 此时,堆栈上有一个空的StringBuilder,用.append调用填充它。
{int off = 0;for (RecipeElement el : recipe.getElements()) {String desc;switch (el.getTag()) {case TAG_CONST:mv.visitLdcInsn(el.getValue());desc = getSBAppendDesc(String.class);break;case TAG_ARG:Class<?> cl = arr[el.getArgPos()];mv.visitVarInsn(getLoadOpcode(cl), off);off += getParameterSize(cl);desc = getSBAppendDesc(cl);break;default:throw new StringConcatException("Unhandled tag: " + el.getTag());}mv.visitMethodInsn(//调用append方法INVOKEVIRTUAL,"java/lang/StringBuilder","append",desc,false);}
}if (DEBUG && mode.isExact()) {/*Exactness checks compare the final StringBuilder.capacity() with a resultingString.length(). If these values disagree, that means StringBuilder had to performstorage trimming, which defeats the purpose of exact strategies.*//*The logic for this check is as follows:Stack before:     Op:(SB)              dup, dup(SB, SB, SB)      capacity()(int, SB, SB)     swap(SB, int, SB)     toString()(S, int, SB)      length()(int, int, SB)    if_icmpeq(SB)              <end>Note that it leaves the same StringBuilder on exit, like the one on enter.*/mv.visitInsn(DUP);mv.visitInsn(DUP);mv.visitMethodInsn(INVOKEVIRTUAL,"java/lang/StringBuilder","capacity","()I",false);mv.visitInsn(SWAP);mv.visitMethodInsn(INVOKEVIRTUAL,"java/lang/StringBuilder","toString","()Ljava/lang/String;",false);mv.visitMethodInsn(INVOKEVIRTUAL,"java/lang/String","length","()I",false);Label l0 = new Label();mv.visitJumpInsn(IF_ICMPEQ, l0);mv.visitTypeInsn(NEW, "java/lang/AssertionError");mv.visitInsn(DUP);mv.visitLdcInsn("Failed exactness check");mv.visitMethodInsn(INVOKESPECIAL,"java/lang/AssertionError","<init>","(Ljava/lang/Object;)V",false);mv.visitInsn(ATHROW);mv.visitLabel(l0);}

下面是该方法中末尾的几行代码,主要就是调用StringBuilder的toString()方法并返回该方法得到的对象。

mv.visitMethodInsn(//调用StringBuilder的toString()方法INVOKEVIRTUAL,"java/lang/StringBuilder","toString","()Ljava/lang/String;",false
);mv.visitInsn(ARETURN);mv.visitMaxs(-1, -1);
mv.visitEnd();
cw.visitEnd();byte[] classBytes = cw.toByteArray();
try {Class<?> hostClass = lookup.lookupClass();Class<?> innerClass = UNSAFE.defineAnonymousClass(hostClass, classBytes, null);UNSAFE.ensureClassInitialized(innerClass);dumpIfEnabled(innerClass.getName(), classBytes);return Lookup.IMPL_LOOKUP.findStatic(innerClass, METHOD_NAME, args);
} catch (Exception e) {dumpIfEnabled(className + "$$FAILED", classBytes);throw new StringConcatException("Exception while spinning the class", e);
}

所以,总结一下,有字符串变量参与拼接的过程:首先调用String的ValueOf方法,然后是生成一个StringBuilder对象并将用append方法将两个字符串依次加入,然后返回StringBuilder的toString()方法。

只有字符串常量(常量池中的String对象)参与的拼接:例如:String a=“ab”+cd;这种拼接,在编译时,编译器会自动将a变量编译为"abcd"
例如以下代码:
public class Test2{
public static void main(String[] args){
String str = “12”+“34”;
System.out.println(str);
}
}
用上述的方法同样查看反编译代码

可以看到编译器直接将str字符串编译为了”1234“.

四、字符串的比较

equals方法

String类型的对象有个equals方法,用于比较两个String对象的是否相等。

public boolean equals(Object anObject) {if (this == anObject) {return true;}if (anObject instanceof String) {String aString = (String)anObject;if (coder() == aString.coder()) {//判断编码格式是否相等return isLatin1() ? StringLatin1.equals(value, aString.value): StringUTF16.equals(value, aString.value);//根据编码格式调用不同的equals方法}}return false;
}

下面是StringLatin1对象(以Latin1为编码格式的String对象)的equals方法

@HotSpotIntrinsicCandidate
public static boolean equals(byte[] value, byte[] other) {if (value.length == other.length) {for (int i = 0; i < value.length; i++) {if (value[i] != other[i]) {return false;}}return true;}return false;
}

然后是StringUTF16对象的equals方法

@HotSpotIntrinsicCandidate
public static boolean equals(byte[] value, byte[] other) {if (value.length == other.length) {int len = value.length >> 1;for (int i = 0; i < len; i++) {if (getChar(value, i) != getChar(other, i)) {return false;}}return true;}return false;
}

可以看出equals方法的实现逻辑就是通过for循环遍历保存字符串的byte数组,一位一位地进行判断。

"=="运算符

“==”运算符用于比较两个对象的地址是否相等。用在字符串比较时,需要注意"abcd"与new String(“abcd”)所返回的地址值不相同,具体看上方String对象的创建。

注意:上面我们具体分析了有字符串变量参与的连接预算,最后的对象是由StringBuilder的toString()方法返回的,而toString()方法底层是返回的是new String()对象,存储的地址是在堆中,而不是在常量池中。

@Override
@HotSpotIntrinsicCandidate
public String toString() {//StringBuilder对象的toString方法// Create a copy, don't share the arrayreturn isLatin1() ? StringLatin1.newString(value, 0, count): StringUTF16.newString(value, 0, count);
}
//StringLatin1对象的newString方法
public static String newString(byte[] val, int index, int len) {return new String(Arrays.copyOfRange(val, index, index + len),LATIN1);
}
//StringUTF16的toString方法
public static String newString(byte[] val, int index, int len) {if (String.COMPACT_STRINGS) {byte[] buf = compress(val, index, len);if (buf != null) {return new String(buf, LATIN1);}}int last = index + len;return new String(Arrays.copyOfRange(val, index << 1, last << 1), UTF16);
}

原文作者:化灰发-悔挥发

原文链接:https://segmentfault.com/a/1190000023961648

原文出处:CSDN

android string拼接字符串_String对象的存储、拼接和比较相关推荐

  1. c++ string截取字符串_String类的常见用法

    字符串不变:字符串的值在创建后不能被更改.string对象是不可变的,所以它们可以被共享.string底层是靠字符数组实现的."abc"等效于char[] data={'a','b ...

  2. android string 转成json对象_Android开发:生成桌面快捷方式是这样做的

    关于生成桌面快捷方式,Android提供了原生的api方法.下边就介绍一下兼容8.0的具体做法. 一.首先当然是关于快捷方式的权限了. 我们需要在AndroidManifest文件中添加一下权限: 二 ...

  3. android string数字字符串如何使用科学计数法,JSONObject 偶遇 数字字符串变为科学计数法 如何变为普通数字字符串...

    和 XML 一样,JSON 也是基于纯文本的数据格式.由于 JSON 天生是为 JavaScript 准备的,因此,JSON 的数据格式非常简单,您可以用 JSON 传输一个简单的 String,Nu ...

  4. Python 循环拼接字符串_详解Python拼接字符串的七种方式

    更多优质内容请关注「AI 应用前沿」 拥抱人工智能,并没有想象的那么难,每天懂一点就已经上路了[给力] Value的比较符号用双等号"==",上例中比较l1和l2的Value要写成 ...

  5. 【Flutter】Dart 数据类型 字符串类型 ( 字符串定义 | 字符串拼接 | 字符串 API 调用 )

    文章目录 I . 字符串定义 I . 字符串拼接 III . 字符串 API 调用 IV . 字符串 Demo 示例 I . 字符串定义 使用单引号 ' ' 和 双引号 " " 都 ...

  6. php数组循环转为对象,php中循环实现(字符串,对象,或者数组)编码相互转换

    /** * 循环实现编码互转 * * @param string $param(字符串,对象,或者数组),$currCharset当前编码,$toCharset期望编码 * @return 参数类型 ...

  7. js 改写对象转字符串 字符串转对象(不丢失对象方法)

    js 改写对象(包括方法)转字符串 对象转字符串 字符串转对象 总结 js中将对象转化为字符串可以用JSON.stringify,字符串转对象可以用JSON.parse.这两个方法在大部分的情况下都很 ...

  8. php 拼接mysql 语句_MySQL执行拼接字符串语句实例

    -- 以下是一个MySQL执行拼接字符串语句实例:-- 为需要拼接的变量赋值SET @VARNAME= -- 以下是一个MySQL执行拼接字符串语句实例: -- 为需要拼接的变量赋值 SET @VAR ...

  9. android string拼接字符串_「JAVA」细述合理创建字符串,分析字符串的底层存储,你不该错过...

    Java基础之字符串操作--String 字符串 什么是字符串?如果直接按照字面意思来理解就是多个字符连接起来组合成的字符序列.为了更好的理解以上的理论,我们先来解释下字符序列,字符序列:把多个字符按 ...

最新文章

  1. RDKit:基于RECAP生成片段
  2. vbs禁用任务管理器
  3. 删除所有数据_批量删除空白单元格,只会Ctrl+G定位就out了,全部三种方法都在这...
  4. 分享我工作10年收藏的程序员技术网站
  5. 软考信息安全工程师备考笔记2:第二章密码学基础与应用备考要点
  6. 用ajax下载字节流形式的excel文件
  7. 伦敦大学学院计算机残疾,伦敦大学学院残疾、设计和创新理学硕士
  8. java 自定义注解 应用_浅谈自定义注解在Spring中的应用
  9. 今年中秋云遮月,来年元宵雨打灯
  10. Julia:last() 和first()
  11. ekho--TTS语音引擎
  12. 软件项目组织与管理期末考试复习要点整理翻译
  13. 无线联网常见问题[1]-搜不到无线网络(请先耐心看完)
  14. linux监控工具 go实现,Gotop:另一个 TUI 图形活动监视器,使用 Go 编写
  15. 英国议会上院AI报告AI in the UK-ready, willing and able附原文183页(赞赏后下载ZIP包)
  16. [笔记]攻防工具分享之 CobaltStrike框架 《二》生成后门
  17. C语言的起源、特点与应用
  18. MySQL ERROR 1064 (42000)
  19. 时间复杂度分析:递归算法
  20. 01excel空白一键填充

热门文章

  1. LOJ 2085: 洛谷 P1587: bzoj 4652: 「NOI2016」循环之美
  2. [转载]C# MemoryStream(内存流)
  3. popwindow+动画
  4. div+css与table布局
  5. Ctrl+F5和F5区别
  6. DAG最长路问题 hdu-1224
  7. unrecognized selector sent to instance问题的解决
  8. 数字化转型方法论_50+企业数字化转型、管理的方法论,这本书到底有什么干货?...
  9. java 输出中文_没见过的 Java 编程入门教程!例程使用中文标识符代码:问个好吧...
  10. java代码启动spring_从0开始学JAVA之《Spring框架-启动过程》