建站教程

建站教程

Products

当前位置:首页 > 建站教程 >

String对象的存储、拼接和比较(JS 定义用字符串拼接的变量的解析)

GG网络技术分享 2025-03-18 16:14 2


String对象的存储、拼接和比较


  • 一、String类型介绍
  • 二、String类型的存储
  • 虚拟机运行时内存(JDK1.8以后)常量池String对象的创建
  • 三、String类型的拼接
  • 通过concat方法拼接通过+号拼接
  • 四、字符串的比较
  • equals方法"=="运算符


( 以下源码都基于jdk11)


一、String类型介绍

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

@Stable

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

12

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

//String的构造函数

public String(char value[]) { this(value, 0, value.length, null);//调用另一个构造函数,代码在下方 }

12

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);

}

1234567891011121314151617

二、String类型的存储

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


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

常量池

通过常量池的使用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"

123

下面来看看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);

}

12345678910111213141516171819

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

通过+号拼接

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

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

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

public class Test{

public static void main(String[] args){

String str1 = "111111";

String str2 = "222222";

String str = str1 + str2;

System.out.println(str);

}

}

12345678

然后进入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

12

动态指令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);

}

1234567891011

下面是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));

12345678910111213141516171819

下面是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 StringConcatException

throw e;

} catch (Throwable t) {

throw new StringConcatException("Generator failed", t);

}

}

12345678910111213141516171819202122232425

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

}

1234567891011121314151617181920212223242526272829303132333435

主要就是针对不同的情况,使用不同的策略值,共六种策略,从而能调用适用于当前情况的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());

}

}

}

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465

然后是生成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 resulting

String.length(). If these values disagree, that means StringBuilder had to perform

storage 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);

}

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161

下面是该方法中末尾的几行代码,主要就是调用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);

}

12345678910111213141516171819202122232425

所以,总结一下,有字符串变量参与拼接的过程:首先调用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;

}

1234567891011121314

下面是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;

}

123456789101112

然后是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;

}

12345678910111213

可以看出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 array

return isLatin1() ? StringLatin1.newString(value, 0, count)

: StringUTF16.newString(value, 0, count);

}

1234567

//StringLatin1对象的newString方法

public static String newString(byte[] val, int index, int len) {

return new String(Arrays.copyOfRange(val, index, index + len),

LATIN1);

}

12345

//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);

}

JS 定义用字符串拼接的变量的解析

今天在写js的时候碰到了难题,我又一个页面需要生成很多的变量。但是变量的名称是根据参数的不同而区分的。

例如可能需要生成date_1,date_2,datet_3...(后面的数字是根据参数来的)。 所以我的函数名 应该由var name = \"test_\"+num;生成函数名 但这问题就来了。

1可以使用 window[name] = \" \" 这种方式来定义变量:

所以 var \"test_\"+num = 100; 这中肯定是错误的。后来问了大哥后才知道。可以使用 window[name] = 100 这种方式来定义变量。看代码

function create_variable(num){

var name = \"test_\"+num;  //生成函数名

window[name] = 100;

window[\'name\'] = 200;  //注意看中括号里的内容加引号和不加引号的区别

}

create_variable(2);

alert(test_2); // 100;

alert(name); //200;

总结

window用中括号的方式定义 变量时,中括号里的内容应该是字符串。如果是一个变量的话,他就会解析这个变量找到具体的值。

他和点语法区别就是这里,点语法后面跟的内容就是要定义的变量名。而不会在解析他是否为变量之类的。例如

var name = \"test\"

window.name = 200

alert(name); // 200

alert(test);  ReferenceError: test is not defined

2用对象的形式

var test = {};

for(var i = 0; i < 3; i++){

test[\'test_\'+i]=\'我是字符串\'+i;

console.log(test[\'test_\'+i]); //输出:我是字符串0, 我是字符串1, 我是字符串2

}

console.log(test_0); //输出:ReferenceError: test_0 is not defined

console.log(test[\'test_0\']); //输出:我是字符串0

3用数组的形式

var test = [];

for(var i = 0; i < 3; i++){

test[i]=\'我是字符串\'+i;

console.log(test[i]); //输出:我是字符串0, 我是字符串1, 我是字符串2

}

console.log(test[0]); //输出:我是字符串0

以上就是JS 定义用字符串拼接的变量的解析的详细内容,更多请关注网站的其它相关文章!

JS 定义用字符串拼接的变量的解析 (https://www.wpmee.com/) javascript教程 第1张

标签:

提交需求或反馈

Demand feedback