【Java基础】String详解

打印 上一主题 下一主题

主题 531|帖子 531|积分 1593

String

一、String 概述

1、根本特性

  1. public final class String implements java.io.Serializable, Comparable<String>, CharSequence {
  2.     // jdk1.8及之前
  3.     private final char value[];
  4.     public String() {
  5.         this.value = "".value;
  6.     }
  7. }
复制代码


  • String 实现了 Serializable 接口:表现字符串是支持序列化的。
  • String 实现了 Comparable 接口:表现 String 可以比较大小
  • String 声明为 final 的,不可被继续,一旦被创建就不会改变,对字符串的操作只是产生了一个新的字符串。
  • String 在 jdk8及从前 定义了 final char[] value 存储字符串数据,jdk9时 改为 final byte[] value
2、不可变性

String 声明为 final 的,一旦被创建就不会改变。String的每次操作都是生成一个新的对象,不改变原来的对象。
例1:重新赋值
  1. @Test
  2. public void test1() {
  3.     String s1 = "abc";
  4.     String s2 = s1;
  5.     System.out.println(s1.hashCode()); // 96354
  6.     System.out.println(s2.hashCode()); // 96354
  7.     // 不会改变原来的对象("abc"),只是新生成一个对象("hello"),并指向新对象
  8.     s2 = "hello";
  9.     System.out.println(s1.hashCode()); // 96354
  10.     System.out.println(s2.hashCode()); // 99162322
  11.     System.out.println(s1); // abc
  12.     System.out.println(s2); // hello        
  13. }
复制代码
例2:拼接操作
  1. @Test
  2. public void test2() {
  3.     String s1 = "abc";
  4.     String s2 = s1 + "def";
  5.     System.out.println(s1); // abc
  6.     System.out.println(s2); // abcdef
  7. }
复制代码
例3:replace() 方法
  1. @Test
  2. public void test3() {
  3.     String s1 = "abc";
  4.     String s2 = s1.replace('a', 'm');
  5.     System.out.println(s1); // abc
  6.     System.out.println(s2); // mbc
  7. }
复制代码
例4:方法参数传递
  1. @Test
  2. public void test4() {
  3.     String str = "old";
  4.     char[] ch = {'t', 'e', 's', 't'};
  5.    
  6.     change(str, ch);
  7.    
  8.     System.out.println(str); // old
  9.     System.out.println(ch);  // best
  10. }
  11. public void change(String str, char ch[]) {
  12.     // 拼接和replace同理
  13.     str = "new";
  14.     ch[0] = 'b';
  15. }
复制代码
3、String、StringBuilder、StringBuffer

可变性:


  • String:
    利用字符数组 private final char value[] 生存字符串,因此String不可变
  • StringBuilder 与 StringBuffer:
    继续于 AbstractStringBuilder,利用字符数组 char[] value 生存字符串,因此这两种对象都是可变的
线程安全性:


  • String:String对象是不可变的,一旦创建后其内容不能更改,线程安全
  • StringBuffer:加了 synchronized 同步锁,线程安全
  • StringBuilder:非线程安全
性能:


  • String:每次对 String 范例进行改变的时间,都会生成一个新的String对象。
  • StringBuffer:每次都会对StringBuffer对象自己进行操作,而不是生成新的对象并改变对象引用。
  • StirngBuilder:没有加锁操作,比 StringBuffer 能得到 10%~15% 左右的性能提升。
利用场景:


  • 操作少量数据:String
  • 单线程操作大量数据:StringBuilder
  • 多线程操作大量数据:StringBuffer
二、字符串 创建与内存分配

1、字面量 / 双引号

   直接由双引号""给出声明的字符串,存储在字符串常量池中(并且雷同的字符串只会存在一份)
  1. public static void test() {
  2.     String str = "ab";
  3. }
复制代码
  1. 0 ldc #2 <ab>
  2. 2 astore_0
  3. 3 return
复制代码
利用双引号""创建字符串时,JVM会先去常量池中查找是否存在这个字符串对象。


  • 不存在:在 字符串常量池 创建这个字符串对象,并返回地址。
  • 存在:直接返回 字符串常量池 中 字符串对象的地址。
2、new关键字

   new 关键字声明的字符串,先在堆内存中创建一个字符串对象(new),然后在字符串常量池中创建一个字符串常量(ldc)。
  1. public static void test() {
  2.     String s1 = new String("ab");
  3.     String s2 = "ab";
  4. }
复制代码
  1. 0 new #3 <java/lang/String>
  2. 3 dup
  3. 4 ldc #2 <ab>
  4. 6 invokespecial #4 <java/lang/String.<init> : (Ljava/lang/String;)V>
  5. 9 astore_0
  6. 10 ldc #2 <ab>
  7. 12 astore_1
  8. 13 return
复制代码
利用 new 创建字符串时,JVM也会先去常量池中查找是否存在这个字符串对象。


  • 不存在:先在堆内存中创建一个字符串对象,然后在字符串常量池中创建一个字符串常量。
  • 存在:直接在堆内存中创建另一个字符串对象。
注意:末了返回的是堆内存中字符串对象的地址,不是常量池中的字符串对象的地址。
  1. public static void test() {
  2.     String s1 = new String("ab");
  3.     String s2 = "ab";
  4.     System.out.println(s1 == s2); // false
  5. }
复制代码
3、StringBuilder.toString()

从下面的源码可以看到,StringBuilder的toString()其实会new一个String对象
  1. public final class StringBuilder extends AbstractStringBuilder implements Serializable, CharSequence {
  2.     @Override
  3.     public String toString() {
  4.         // Create a copy, don't share the array
  5.         return new String(value, 0, count);
  6.     }
  7. }
复制代码
需要注意的是,StringBuilder.toString() 不会在常量池中创建对象,下面写个例子分析一下。
  1. public static void test() {
  2.     StringBuilder stringBuilder = new StringBuilder("a");
  3.     stringBuilder.append("b");
  4.     String str = stringBuilder.toString();
  5. }
复制代码
  1. 0 new #5 <java/lang/StringBuilder>
  2. 3 dup
  3. 4 ldc #6 <a>
  4. 6 invokespecial #7 <java/lang/StringBuilder.<init> : (Ljava/lang/String;)V>
  5. 9 astore_0
  6. 10 aload_0
  7. 11 ldc #8 <b>
  8. 13 invokevirtual #9 <java/lang/StringBuilder.append : (Ljava/lang/String;)Ljava/lang/StringBuilder;>
  9. 16 pop
  10. 17 aload_0
  11. 18 invokevirtual #10 <java/lang/StringBuilder.toString : ()Ljava/lang/String;>
  12. 21 astore_1
  13. 22 return
复制代码
可以看到没有出现 ldc #x <ab>,可见StringBuilder.toString() 只在堆内存创建了一个字符串,并没有放到字符串常量池
4、intern() 方法

   intern()判断常量池中是否存在该字符串,存在,则返回常量池中的地址;不存在,则在常量池中加载一份并返回地址
  1. @Test
  2. public void test() {
  3.     String s1 = "nb";
  4.     String s2 = "n";
  5.     String s3 = s2 + "b";
  6.     String s4 = s3.intern(); // 常量池中存在,返回常量池中的地址
  7.     System.out.println(s1 == s3); // false
  8.     System.out.println(s1 == s4); // true
  9. }
复制代码
关于intern()方法下面会详细睁开。
5、小结

什么情况下,字符串会被放入 字符串常量池 呢?

  • 直接由双引号""给出声明的字符串,会直接放在字符串常量池中。
  • 利用new创建的字符串,也会有一份放在字符串常量池中。
  • 调用intern()方法的字符串,也会被放到字符串常量池中。
注意:StringBuilder.toString()生成的字符串,是不会放到字符串常量池中的,只会在堆中创建一份。
三、字符串 拼接

1、常量+常量

   场景1:常量与常量拼接,拼接结果在字符串常量池,原理是编译期优化
  1. public class AppendTest {
  2.     public void test() {
  3.         String s1 = "a" + "b" + "c";
  4.         String s2 = "abc";
  5.         System.out.println(s1 == s2); // true
  6.     }
  7. }
复制代码
从 class文件 的 反编译结果 可以看出:编译器做了优化,将 "a" + "b" + "c" 优化成了 "abc"
  1. 0 ldc #2 <abc>
  2. 2 astore_0
  3. 3 ldc #2 <abc>
  4. 5 astore_1
  5. 6 getstatic #3 <java/lang/System.out : Ljava/io/PrintStream;>
  6. 9 aload_0
  7. 10 aload_1
  8. 11 if_acmpne 18 (+7)
  9. 14 iconst_1
  10. 15 goto 19 (+4)
  11. 18 iconst_0
  12. 19 invokevirtual #4 <java/io/PrintStream.println : (Z)V>
  13. 22 return
复制代码
从 IDEA 的 AppendTest.class 也可以直接看出来
  1. public class AppendTest {
  2.     public AppendTest() {}
  3.     public void test() {
  4.         String s1 = "abc";  // 显示 String s1 = "abc"; 说明做了代码优化
  5.         String s2 = "abc";
  6.         System.out.println(s1 == s2);
  7.     }
  8. }
复制代码
2、变量 拼接

   场景2:拼接中只要有一个是变量,拼接结果就在堆中,原理是StringBuilder的append操作。
  1. public void test2() {
  2.     String s1 = "n";
  3.     String s2 = "b";
  4.     String s3 = "nb";
  5.     String s4 = "n" + "b"; // 编译期优化
  6.     String s5 = s1 + "b";
  7.     String s6 = "n" + s2;
  8.     String s7 = s1 + s2;
  9.     System.out.println(s3 == s4); // true
  10.     System.out.println(s3 == s5); // false
  11.     System.out.println(s3 == s6); // false
  12.     System.out.println(s3 == s7); // false
  13.     System.out.println(s5 == s6); // false
  14.     System.out.println(s5 == s7); // false
  15.     System.out.println(s6 == s7); // false
  16.     // 这里使用intern(),会返回常量池中"nb"的地址并赋给s8(这里先了解,具体用法后续会详细展开)
  17.     String s8 = s7.intern();
  18.     System.out.println(s3 == s8); // true
  19. }
复制代码
下面我们从 class文件 的 反编译结果 进行分析
  1.   0 ldc #5 <n>
  2.   2 astore_1
  3.   3 ldc #6 <b>
  4.   5 astore_2
  5.   6 ldc #7 <nb>
  6.   8 astore_3
  7.   9 ldc #7 <nb>
  8. 11 astore 4
复制代码
s4之前都是【例1】的内容,这里就不赘述了,主要看一下 s5、s6、s7 这三行
可以看出,都是先 new 了一个 StringBuilder 对象,然后利用 append() 拼接,末了调用了 toString() 创建 String对象 并赋值
  1. 13 new #8 <java/lang/StringBuilder>
  2. 16 dup
  3. 17 invokespecial #9 <java/lang/StringBuilder.<init> : ()V>
  4. 20 aload_1
  5. 21 invokevirtual #10 <java/lang/StringBuilder.append : (Ljava/lang/String;)Ljava/lang/StringBuilder;>
  6. 24 ldc #6 <b>
  7. 26 invokevirtual #10 <java/lang/StringBuilder.append : (Ljava/lang/String;)Ljava/lang/StringBuilder;>
  8. 29 invokevirtual #11 <java/lang/StringBuilder.toString : ()Ljava/lang/String;>
  9. 32 astore 5
复制代码
  1. 34 new #8 <java/lang/StringBuilder>
  2. 37 dup
  3. 38 invokespecial #9 <java/lang/StringBuilder.<init> : ()V>
  4. 41 ldc #5 <n>
  5. 43 invokevirtual #10 <java/lang/StringBuilder.append : (Ljava/lang/String;)Ljava/lang/StringBuilder;>
  6. 46 aload_2
  7. 47 invokevirtual #10 <java/lang/StringBuilder.append : (Ljava/lang/String;)Ljava/lang/StringBuilder;>
  8. 50 invokevirtual #11 <java/lang/StringBuilder.toString : ()Ljava/lang/String;>
  9. 53 astore 6
复制代码
  1. 55 new #8 <java/lang/StringBuilder>
  2. 58 dup
  3. 59 invokespecial #9 <java/lang/StringBuilder.<init> : ()V>
  4. 62 aload_1
  5. 63 invokevirtual #10 <java/lang/StringBuilder.append : (Ljava/lang/String;)Ljava/lang/StringBuilder;>
  6. 66 aload_2
  7. 67 invokevirtual #10 <java/lang/StringBuilder.append : (Ljava/lang/String;)Ljava/lang/StringBuilder;>
  8. 70 invokevirtual #11 <java/lang/StringBuilder.toString : ()Ljava/lang/String;>
  9. 73 astore 7
复制代码
3、final变量 拼接

   场景3:final修饰的String变量,视作String常量。
  1. public static void test3() {
  2.     final String s1 = "n";
  3.     final String s2 = "b";
  4.     String s3 = "nb";
  5.     String s4 = "n" + "b"; // 编译期优化
  6.     String s5 = s1 + "b";
  7.     String s6 = "n" + s2;
  8.     String s7 = s1 + s2;
  9.     System.out.println(s3 == s4); // true
  10.     System.out.println(s3 == s5); // true
  11.     System.out.println(s3 == s6); // true
  12.     System.out.println(s3 == s7); // true
  13.     System.out.println(s5 == s6); // true
  14.     System.out.println(s5 == s7); // true
  15.     System.out.println(s6 == s7); // true
  16. }
复制代码
可以看到,我们只是在String变量前加上final,结果就完全不同了。
下面我们看一下 class文件 的 反编译结果
  1.   0 ldc #6 <n>
  2.   2 astore_0
  3.   3 ldc #7 <b>
  4.   5 astore_1
  5.   6 ldc #8 <nb>
  6.   8 astore_2
  7.   9 ldc #8 <nb>
  8. 11 astore_3
  9. 12 ldc #8 <nb>
  10. 14 astore 4
  11. 16 ldc #8 <nb>
  12. 18 astore 5
  13. 20 ldc #8 <nb>
  14. 22 astore 6
复制代码
可以看出,String变量被final修饰之后,所有的拼接操作都在编译期优化了,而没有利用StringBuilder
4、拼接小结



  • 常量与常量拼接:

    • 拼接结果在字符串常量池,原理是编译期优化

  • 拼接中只要有一个是变量:

    • 拼接结果在堆中,原理是 先new 一个StringBuilder,然后用append()拼接,末了调用 toString() 返回结果
    • 补充说明:在 jdk5 之前利用的是 StringBuffer,在 jdk5 之后,改为了 StringBuilder,

  • final修饰的String变量拼接:

    • 拼接结果在字符串常量池,仍然利用编译期优化,而非StringBuilder。

因此,在开发中能利用上final的时间照旧发起利用
5、+拼接 vs append拼接

  1. public class StringAppendTest {
  2.     public static void main(String[] args) {
  3.         long start = System.currentTimeMillis();
  4. //        String s1 = append1(100000); // 1670ms
  5.         String s2 = append2(100000);   // 4ms
  6.         long end = System.currentTimeMillis();
  7.         System.out.println("拼接花费的时间为:" + (end - start));
  8.     }
  9.     public static String append1(int highLevel) {
  10.         String str = "";
  11.         for (int i = 0; i < highLevel; i++) {
  12.             str = str + "a";  // 每次循环都会创建一个StringBuilder、String
  13.         }
  14.         return str;
  15.     }
  16.     public static String append2(int highLevel) {
  17.         StringBuilder strBuilder = new StringBuilder();
  18.         for (int i = 0; i < highLevel; i++) {
  19.             strBuilder.append("a"); // 只需要创建一个StringBuilder
  20.         }
  21.         return strBuilder.toString();
  22.     }
  23. }
复制代码
结论:通过StringBuilder的append()的方式拼接字符串的服从,远远高于 + 拼接
缘故原由:


  • StringBuilder的append()的方式:

    • 自始至终中只创建过一个StringBuilder的对象。

  • + 拼接的方式:

    • 每一次 字符串变量 拼接的过程,都会new一个StringBuilder对象(这从之前的反编译结果中也可以看出来)

因此利用字符串变量+拼接会占用更大的内存,产生大量垃圾字符串,如果发生了GC,也会花费额外的时间。
6、StringBuilder的扩容

StringBuilder 空参构造器的初始化大小为16,超过该大小会进行扩容,涉及数组的copy操作
  1. public StringBuilder() {   
  2.     super(16);
  3. }
复制代码
如果提前知道需要拼接 String 的长度,就应该直接利用带参构造器指定capacity,以减少扩容的次数
  1. public StringBuilder(int capacity) {   
  2.     super(capacity);
  3. }
复制代码
7、优化小结



  • 允许的情况下只管利用final。如许拼接操作会在编译期优化,而不会创建StringBuilder对象去append。
  • 利用StringBuilder的append()服从要高于利用+拼接。
  • 如果知道最终的字符串长度,应该利用带容量的构造器创建StringBuilder,制止频繁扩容。
四、new String() 会创建几个对象

1、单独new

   场景1:new String("ab") 会创建几个对象?(答案是2个)
  1. 0: new                   #2                  // class java/lang/String
  2. 3: dup
  3. 4: ldc                   #3                  // String ab
  4. 6: invokespecial         #4                  // Method java/lang/String."<init>":(Ljava/lang/String;)V
  5. 9: astore_1
  6. 10: return
复制代码
  1. 对象1  new String("ab")
  2. 对象2  常量池中的"ab"
复制代码
2、常量 拼接 new

   场景2:"a" + new String("b") 会创建几个对象?(答案是5个)
  1. 0: new                    #2                  // class java/lang/StringBuilder
  2. 3: dup
  3. 4: invokespecial          #3                  // Method java/lang/StringBuilder."<init>":()V
  4. 7: ldc                    #4                  // String a
  5. 9: invokevirtual          #5                  // Method java/lang/StringBuilder.append:
  6. 12: new                   #6                  // class java/lang/String
  7. 15: dup
  8. 16: ldc                   #7                  // String b
  9. 18: invokespecial         #8                  // Method java/lang/String."<init>":(Ljava/lang/String;)V
  10. 21: invokevirtual         #5                  // Method java/lang/StringBuilder.append:
  11. 24: invokevirtual         #9                  // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
  12. 27: astore_1
  13. 28: return
复制代码
  1. 对象1         new StringBuilder()
  2. 对象2  常量池中的"a"
  3. 对象3  new String("b")
  4. 对象4  常量池中的"b"   
  5. 对象5  StringBuilder.toString() 会 new String("ab")
复制代码
注意:StringBuilder.toString 不会在 字符串常量池中 生成 "ab"。
3、new 拼接 new

   场景3:new String("a") + new String("b") 会创建几个对象?(答案是6个)
  1. 0: new                   #2                  // class java/lang/StringBuilder
  2. 3: dup
  3. 4: invokespecial         #3                  // Method java/lang/StringBuilder."<init>":()V
  4. 7: new                   #4                  // class java/lang/String
  5. 10: dup
  6. 11: ldc                   #5                  // String a
  7. 13: invokespecial         #6                  // Method java/lang/String."<init>":(Ljava/lang/String;)V
  8. 16: invokevirtual         #7                  // Method java/lang/StringBuilder.append:
  9. 19: new                   #4                  // class java/lang/String
  10. 22: dup
  11. 23: ldc                   #8                  // String b
  12. 25: invokespecial         #6                  // Method java/lang/String."<init>":(Ljava/lang/String;)V
  13. 28: invokevirtual         #7                  // Method java/lang/StringBuilder.append:
  14. 31: invokevirtual         #9                  // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
  15. 34: astore_1
  16. 35: return
复制代码
  1. 对象1         new StringBuilder()
  2. 对象2  new String("a")
  3. 对象3  常量池中的"a"
  4. 对象4  new String("b")
  5. 对象5  常量池中的"b"   
  6. 对象6  StringBuilder.toString() 会 new String("ab")
复制代码
注意:StringBuilder.toString 不会在 字符串常量池中 生成 "ab"。
五、intern()

intern是一个native方法,底层调用的是C的方法
  1. public final class String implements java.io.Serializable, Comparable<String>, CharSequence {
  2.     public native String intern();
  3. }
复制代码
1、intern() 的作用

调用intern()时,会判断 字符串常量池 中 是否已存在当前字符串(通过equals()方法判断)


  • 已存在:返回 字符串常量池 中 已存在的 该字符串对象的地址;
  • 不存在:将 该字符串 放入 字符串常量池,并返回 字符串对象的地址(这里jdk7前后略有不同)

    • JDK1.6中:会把 调用对象复制一份(新的引用地址),放入常量池,并返回 新的引用地址。
    • JDK1.7起:会把 调用对象的引用地址 复制一份(雷同的引用地址),放入常量池,并返回 调用对象的引用地址。

也就是说,任意字符串调用intern() 返回结果所指向的那个类实例,必定和直接以常量情势出现的字符串实例完全雷同。
  1. ("a"+"b"+"c").intern() == "abc" // true;
复制代码
intern()可以确保字符串在内存里只有一份(即字符串常量池中的),可以节省内存空间,加快字符串操作使命的执行速度。
2、案例分析

1)案例1

  1. public void test() {
  2.     String s1 = new String("a");
  3.     s1.intern();
  4.     String s2 = "a";
  5.     System.out.println(s1 == s2);  // jdk6/7/8 false
  6. }
复制代码


  • s1记录的是堆中new String("a")的地址
  • s2记录的是字符串常量池中"a"的地址
2)案例2

  1. public void test() {
  2.     String s1 = new String("a") + new String("b");
  3.     s1.intern();
  4.     String s2 = "ab";
  5.     System.out.println(s1 == s2);  // jdk6 false ; jdk7/8 true
  6. }
复制代码


  • s1记录的是堆中"ab"的地址(注意,这个"ab"是StringBuilder.toString()生成的,没有往常量池里放)
  • s1.intern()调用这个方法之前,字符串常量池中并不存在"ab",所以要把"ab"放入字符串常量池

    • jdk6中:字符串常量池中的"ab"指向新的地址。
    • jdk7起:字符串常量池中的"ab"指向的是调用intern()的s1的地址

  • s2记录的是字符串常量池中的"ab"指向的地址
3)案例3

  1. public void test() {
  2.     String s1 = new String("a") + new String("b");
  3.     String s2 = s1.intern();                 // 常量池没有"ab",会放入
  4.     System.out.println(s1 == "ab");  // jdk6 false ; jdk7/8 true
  5.     System.out.println(s2 == "ab");  // jdk6 true  ; jdk7/8 true      
  6. }
复制代码
  1. public void test() {
  2.     String s1 = "ab";                           // 常量池中创建一个新的对象"ab"
  3.     String s2 = new String("a") + new String("b");
  4.     String s3 = s2.intern();           // 常量池已有"ab",不会再放入
  5.     System.out.println(s1 == s2);  // jdk6/7/8  false
  6.     System.out.println(s1 == s3);  // jdk6/7/8  true     
  7. }
复制代码
3、intern() 的服从:空间角度

  1. public class StringInternTest {   
  2.     static final int MAX_COUNT = 1000 * 10000;   
  3.     static final String[] arr = new String[MAX_COUNT];   
  4.    
  5.     public static void main(String[] args) {        
  6.         Integer [] data = new Integer[]{1,2,3,4,5,6,7,8,9,10};      
  7.         
  8.         long start = System.currentTimeMillis();  
  9.         for (int i = 0; i < MAX_COUNT; i++) {            
  10. //                         arr[i] = new String(String.valueOf(data[i%data.length]));                     // 不用intern   7256ms   
  11.             arr[i] = new String(String.valueOf(data[i%data.length])).intern();   // 使用intern   1395ms
  12.         }      
  13.         long end = System.currentTimeMillis();   
  14.         System.out.println("花费的时间为:" + (end - start));   
  15.         
  16.         try {           
  17.             Thread.sleep(1000000);      
  18.         } catch (Exception e) {      
  19.             e.getStackTrace();   
  20.         }  
  21.         System.gc();
  22.     }
  23. }
复制代码


  • 直接new:堆 和 字符串常量池 大概会存在雷同的字符串的两个对象。
  • 利用intern():保证内存中雷同的字符串对象只会有一个。
结论:对于步伐中大量利用存在的字符串时,尤其存在许多已经重复的字符串时,利用 intern()方法能够节省内存空间。
4、运行时内存案例

  1. class Memory {
  2.     public static void main(String[] args) {
  3.         int i = 1;
  4.         Object obj = new Object();
  5.         Memory mem = new Memory();
  6.         mem.foo(obj);
  7.     }
  8.     private void foo(Object param) {
  9.         String str = param.toString().intern();
  10.         System.out.println(str);        // java.lang.Object@42a57993
  11.     }
  12. }
复制代码


免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有账号?立即注册

x
回复

使用道具 举报

0 个回复

正序浏览

快速回复

您需要登录后才可以回帖 登录 or 立即注册

本版积分规则

光之使者

金牌会员
这个人很懒什么都没写!

标签云

快速回复 返回顶部 返回列表