【问题标题】:Immutability of Strings in JavaJava中字符串的不变性
【发布时间】:2010-12-05 20:32:23
【问题描述】:

考虑以下示例。

String str = new String();

str  = "Hello";
System.out.println(str);  //Prints Hello

str = "Help!";
System.out.println(str);  //Prints Help!

现在,在 Java 中,String 对象是不可变的。那么为什么对象str 可以被赋值为“Help!”。这不是与 Java 中字符串的不变性相矛盾吗?谁能解释一下不变性的确切概念吗?

编辑:

好的。我现在明白了,但只是一个后续问题。下面的代码呢:

String str = "Mississippi"; 
System.out.println(str); // prints Mississippi 

str = str.replace("i", "!"); 
System.out.println(str); // prints M!ss!ss!pp! 

这是否意味着再次创建了两个对象(“Mississippi”和“M!ss!ss!pp!”)并且引用strreplace() 方法之后指向不同的对象?

【问题讨论】:

标签: java string immutability


【解决方案1】:

str 不是对象,它是对对象的引用。 "Hello""Help!" 是两个不同的 String 对象。因此,str 指向一个字符串。您可以更改它指向的内容,但不能更改它指向的内容

以这段代码为例:

String s1 = "Hello";
String s2 = s1;
// s1 and s2 now point at the same string - "Hello"

现在,我们无法对s1 进行任何1 操作来影响s2 的值。它们引用同一个对象 - 字符串 "Hello" - 但该对象是不可变的,因此无法更改。

如果我们这样做:

s1 = "Help!";
System.out.println(s2); // still prints "Hello"

这里我们看到了改变对象和改变引用之间的区别。 s2 仍然指向与我们最初设置 s1 指向的对象相同的对象。将s1 设置为"Help!" 只会更改引用,而它最初引用的String 对象保持不变。

如果字符串可变的,我们可以这样做:

String s1 = "Hello";
String s2 = s1;
s1.setCharAt(1, 'a'); // Fictional method that sets character at a given pos in string
System.out.println(s2); // Prints "Hallo"

编辑以响应 OP 的编辑:

如果您查看source code for String.replace(char,char)(也可以在 JDK 安装目录中的 src.zip 中找到——一个专业提示是,当您想知道某些东西是如何工作时,请查看那里),您可以看到它的作用如下:

  • 如果在当前字符串中出现一个或多个oldChar,则复制当前字符串,其中所有出现的oldChar 都替换为newChar
  • 如果oldChar 不在当前字符串中,则返回当前字符串。

所以是的,"Mississippi".replace('i', '!') 创建了一个新的 String 对象。同样,以下成立:

String s1 = "Mississippi";
String s2 = s1;
s1 = s1.replace('i', '!');
System.out.println(s1); // Prints "M!ss!ss!pp!"
System.out.println(s2); // Prints "Mississippi"
System.out.println(s1 == s2); // Prints "false" as s1 and s2 are two different objects

你现在的作业是看看如果你把s1 = s1.replace('i', '!');改成s1 = s1.replace('Q', '!');,上面的代码会做什么:)


1 实际上, 可以改变字符串(和其他不可变对象)。它需要反射,非常非常危险,除非你真的有兴趣破坏程序,否则永远不要使用它。

【讨论】:

  • +1 为虚构方法,它展示了不可变对象与其他对象的区别。
  • 感谢 gustafc 提供正确的示例和清晰的解释....但是您能回答问题中已编辑的部分吗?这将使我的理解更加清晰。
  • 我以前从未见过这样的答案。讨论了每一个细节。
  • +1 这是一个想法,Java 中的不可变对象就像按值复制一样,您可以有 2 个对字符串的引用,但您应该将它们视为 2 个单独的字符串,因为它是不可变的,并且可以使用其中一个不会影响另一个
  • Java——你认为你知道的越多,你实际上知道的越少。
【解决方案2】:

str 引用的对象可以更改,但实际的String 对象本身不能。

包含字符串"Hello""Help!"String 对象不能更改它们的值,因此它们是不可变的。

String 对象的不变性并不意味着指向该对象的引用不能改变。

防止str 引用更改的一种方法是将其声明为final

final String STR = "Hello";

现在,尝试将另一个String 分配给STR 将导致编译错误。

【讨论】:

  • 但是在这种情况下,字符串对象是'str',它首先包含值'Hello',然后被分配新值'Help!'。 “包含字符串“Hello”和“Help!”的字符串对象不能更改它们的值,因此它们是不可变的。”?如果这是一个愚蠢的问题,请原谅我。但是我必须清除它...
  • 你有没有尝试过用 C 编程?只需阅读指针入门,您就会完全理解 coobird 的答案。
  • 看...这是我想避免的...我知道你是一个很棒的程序员...我只是想在这里学习java...所以如果你能回答我的问题正确然后请回答...
  • 您混淆了引用和对象 - str 不是“对象”,它是对对象的引用。如果您有String str = "Hello"; 后跟String anotherReference = str;,则您没有2 个String 对象,您有一个对象(文字“Hello”)和2 个对它的引用(stranotherReference)。
  • 我没有编辑权限,但如果我这样做了,我会将 coobirds 的第一句话编辑为:“str 引用可以更改,但实际的 String 对象本身不能。”
【解决方案3】:

Light_handle 我建议您阅读Cup Size -- a story about variablesPass-by-Value Please (Cup Size continued)。阅读上面的帖子时,这将有很大帮助。

你读过它们吗?是的。好的。

String str = new String();

这将创建一个名为“str”的新“远程控制”并将其设置为值new String()(或"")。

例如这会在内存中创建:

str --- > ""

str  = "Hello";

这会更改遥控器“str”,但不会修改原始字符串""

例如这会在内存中创建:

str -+   ""
     +-> "Hello"

str = "Help!";

这会更改遥控器“str”,但不会修改原始字符串"" 或遥控器当前指向的对象。

例如这会在内存中创建:

str -+   ""
     |   "Hello"
     +-> "Help!"

【讨论】:

【解决方案4】:

让我们把它分成几个部分

String s1 = "hello";

此语句创建包含 hello 的字符串并占用内存中的空间,即在 Constant String Pool 中,并将其分配给引用对象 s1

String s2 = s1;

此语句将相同的字符串 hello 分配给新引用 s2

         __________
        |          |
s1 ---->|  hello   |<----- s2
        |__________| 

两个引用都指向同一个字符串,所以输出相同的值如下。

out.println(s1);    // o/p: hello
out.println(s2);    // o/p: hello

虽然 String不可变的,但可以进行赋值,因此 s1 现在将引用新值 stack .

s1 = "stack";    
         __________
        |          |
s1 ---->|  stack   |
        |__________|

但是指向 hellos2 对象会保持原样。

         __________
        |          |
s2 ---->|  hello   |
        |__________|

out.println(s1);    // o/p: stack
out.println(s2);    // o/p: hello

由于 String 是不可变的 Java 虚拟机不允许我们通过它的方法修改字符串 s1。它将在池中创建所有新的 String 对象,如下所示。

s1.concat(" overflow");

                 ___________________
                |                   |
s1.concat ----> |  stack overflow   |
                |___________________|

out.println(s1);    // o/p: stack
out.println(s2);    // o/p: hello
out.println(s1.concat); // o/p: stack overflow

注意如果 String 是可变的,那么输出将是

out.println(s1);    // o/p: stack overflow

现在你可能会惊讶为什么 String 有像 concat() 这样的方法可以修改。跟随 sn-p 将消除您的困惑。

s1 = s1.concat(" overflow");

这里我们将修改后的字符串值分配回 s1 引用。

         ___________________
        |                   |
s1 ---->|  stack overflow   |
        |___________________|


out.println(s1);    // o/p: stack overflow
out.println(s2);    // o/p: hello

这就是为什么 Java 决定 String 是最终类 否则任何人都可以修改和更改字符串的值。 希望这会有所帮助。

【讨论】:

    【解决方案5】:

    str 第一次引用的字符串对象没有改变,你所做的只是让str 引用一个新的字符串对象。

    【讨论】:

      【解决方案6】:

      字符串不会改变,对它的引用会改变。您将不变性与 final 字段的概念混淆了。如果一个字段被声明为final,一旦被赋值,就不能重新赋值。

      【讨论】:

        【解决方案7】:

        关于问题的替换部分,试试这个:

        String str = "Mississippi"; 
        System.out.println(str); //Prints Mississippi 
        
        String other = str.replace("i", "!"); 
        System.out.println(str); //still prints Mississippi 
        System.out.println(other);  // prints M!ss!ss!pp!
        

        【讨论】:

          【解决方案8】:

          尽管 java 试图忽略它,str 只不过是一个指针。这意味着当你第一次写str = "Hello"; 时,你创建了一个str 指向的对象。当您通过编写 str = "Help!"; 重新分配 str 时,会创建一个新对象,并且只要 java 愿意,旧的 "Hello" 对象就会被垃圾回收。

          【讨论】:

            【解决方案9】:

            不变性意味着实例化对象的值不能改变,你永远不能把“Hello”变成“Help!”。

            变量 str 是对对象的引用,当您为 str 分配新值时,您并没有更改它所引用的对象的值,而是引用了不同的对象。

            【讨论】:

              【解决方案10】:

              String 类是不可变的,你不能改变不可变对象的值。 但是在字符串的情况下,如果您更改字符串的值,它将在字符串池中创建新字符串,而不是您对该值的字符串引用而不是旧字符串。所以通过这种方式字符串是不可变的。 让我们举个例子,

              String str = "Mississippi";  
              System.out.println(str); // prints Mississippi 
              

              它将创建一个字符串 "Mississippi" 并将其添加到字符串池中 所以现在 str 指向密西西比州。

              str = str.replace("i", "!");  
              System.out.println(str); // prints M!ss!ss!pp! 
              

              但是经过以上操作, 将创建另一个字符串 "M!ss!ss!pp!" 它将被添加到字符串池中。和 现在 str 指向 M!ss!ss!pp!,而不是密西西比州。

              所以当你改变字符串对象的值时,它会再创建一个对象并将其添加到字符串池中。

              再举一个例子

              String s1 = "Hello"; 
              String s2 = "World"; 
              String s = s1 + s2;
              

              以上三行会将三个字符串对象添加到字符串池中。
              1) 你好
              2) 世界
              3) 你好世界

              【讨论】:

                【解决方案11】:

                用途:

                String s = new String("New String");
                s.concat(" Added String");
                System.out.println("String reference -----> "+s); // Output: String reference -----> New String
                

                如果你看到这里我使用concat 方法来更改原始字符串,即带有字符串“Added String”的“New String”,但我仍然得到了之前的输出,因此证明你可以不改变 String 类对象的引用,但如果你通过 StringBuilder 类做这件事,它会起作用。如下所示。

                StringBuilder sb = new StringBuilder("New String");
                sb.append(" Added String");
                System.out.println("StringBuilder reference -----> "+sb);// Output: StringBuilder reference -----> New String Added String
                

                【讨论】:

                  【解决方案12】:

                  就像 Linus Tolvards 所说:

                  谈话很便宜。给我看代码

                  看看这个:

                  public class Test{
                      public static void main(String[] args){
                  
                          String a = "Mississippi";
                          String b = "Mississippi";//String immutable property (same chars sequence), then same object
                  
                          String c = a.replace('i','I').replace('I','i');//This method creates a new String, then new object
                          String d = b.replace('i','I').replace('I','i');//At this moment we have 3 String objects, a/b, c and d
                  
                          String e = a.replace('i','i');//If the arguments are the same, the object is not affected, then returns same object
                  
                          System.out.println( "a==b? " + (a==b) ); // Prints true, they are pointing to the same String object
                  
                          System.out.println( "a: " + a );
                          System.out.println( "b: " + b );
                  
                          System.out.println( "c==d? " + (c==d) ); // Prints false, a new object was created on each one
                  
                          System.out.println( "c: " + c ); // Even the sequence of chars are the same, the object is different
                          System.out.println( "d: " + d );
                  
                          System.out.println( "a==e? " + (a==e) ); // Same object, immutable property
                      }
                  }
                  

                  输出是

                  a==b? true
                  a: Mississippi
                  b: Mississippi
                  c==d? false
                  c: Mississippi
                  d: Mississippi
                  a==e? true
                  

                  所以,记住两件事:

                  • 字符串是不可变的,直到您应用一种操作和创建新字符串的方法(c 和 d 案例)。
                  • 如果两个参数相同,替换方法返回相同的字符串对象

                  【讨论】:

                    【解决方案13】:

                    对于那些想知道如何在 Java 中打破字符串不变性的人...

                    代码

                    import java.lang.reflect.Field;
                    
                    public class StringImmutability {
                        public static void main(String[] args) {
                            String str1 = "I am immutable";
                            String str2 = str1;
                    
                            try {
                                Class str1Class = str1.getClass();
                                Field str1Field = str1Class.getDeclaredField("value");
                    
                                str1Field.setAccessible(true);
                                char[] valueChars = (char[]) str1Field.get(str1);
                    
                                valueChars[5] = ' ';
                                valueChars[6] = ' ';
                    
                                System.out.println(str1 == str2);
                                System.out.println(str1);
                                System.out.println(str2);           
                            } catch (NoSuchFieldException e) {
                                e.printStackTrace();
                            } catch (SecurityException e) {
                                e.printStackTrace();
                            } catch (IllegalArgumentException e) {
                                e.printStackTrace();
                            } catch (IllegalAccessException e) {
                                e.printStackTrace();
                            }
                    
                        }
                    }
                    

                    输出

                    true
                    I am   mutable
                    I am   mutable
                    

                    【讨论】:

                      【解决方案14】:

                      字符串不可变的。这意味着我们只能更改reference


                      String a = "a";
                      System.out.println("String a is referencing to "+a); // Output: a
                      
                      a.concat("b");
                      System.out.println("String a is referencing to "+a); // Output: a
                      
                      a = a.concat("b");
                      System.out.println("String a has created a new reference and is now referencing to "+a); // Output: ab
                      

                      【讨论】:

                        【解决方案15】:

                        在 Java 中,对象通常通过引用来访问。在您的一段代码中,str 是一个引用,它首先分配给“Hello”(一个自动创建的对象或从constant pool 获取),然后您分配了另一个对象“Help!”到相同的参考。需要注意的一点是引用是相同的和修改的,但对象是不同的。您的代码中的另一件事是您访问了三个对象,

                        1. 调用 new String() 时。
                        2. 当您分配“你好”时。
                        3. 当您分配“帮助!”时。

                        调用 new String() 会创建一个新对象,即使它存在于字符串池中,所以一般不应该使用它。要将 new String() 创建的字符串放入字符串池中,可以尝试intern() 方法。

                        我希望这会有所帮助。

                        【讨论】:

                          【解决方案16】:

                          我可以说的不变性是你不能改变字符串本身。假设您有字符串 x,其值为“abc”。现在你不能改变字符串,也就是说,你不能改变“abc”中的任何字符。

                          如果您必须更改字符串中的任何字符,您可以使用字符数组并对其进行变异或使用 StringBuilder。

                          String x = "abc";
                          x = "pot";
                          x = x + "hj";
                          x = x.substring(3);
                          System.out.println(x);
                          
                          char x1[] = x.toCharArray();
                          x1[0] = 's';
                          String y = new String(x1);
                          System.out.println(y);
                          

                          输出:

                          hj
                          sj
                          

                          【讨论】:

                            【解决方案17】:

                            或者你可以试试:

                            public class Tester
                            {
                            public static void main(String[] args)
                            {
                             String str = "Mississippi"; 
                             System.out.println(str); // prints Mississippi 
                             System.out.println(str.hashCode());
                            
                             str = str.replace("i", "!"); 
                             System.out.println(str); // prints M!ss!ss!pp! 
                             System.out.println(str.hashCode());
                             }
                             }
                            

                            这将显示哈希码如何变化。

                            【讨论】:

                              【解决方案18】:

                              String 是不可变的意味着你不能改变对象本身,但是你可以改变对对象的引用。当您调用 a = "ty" 时,实际上是将 a 的引用更改为由字符串字面量 "ty" 创建的新对象。更改一个对象意味着使用它的方法来更改它的一个字段(或者这些字段是公共的而不是最终的,因此可以从外部更新它们而无需通过方法访问它们),例如:

                              Foo x = new Foo("the field");
                              x.setField("a new field");
                              System.out.println(x.getField()); // prints "a new field"
                              

                              在不可变类中(声明为final,以防止通过继承修改)(其方法不能修改其字段,并且字段始终是私有的,建议为final),例如String,您不能更改当前字符串,但你可以返回一个新的字符串,即:

                              String s = "some text";
                              s.substring(0,4);
                              System.out.println(s); // still printing "some text"
                              String a = s.substring(0,4);
                              System.out.println(a); // prints "some"
                              

                              【讨论】:

                                【解决方案19】:

                                这里的不变性意味着实例可以指向其他引用,但字符串的原始内容不会在原始引用处被修改。 让我通过你给出的第一个例子来解释。 第一个 str 指向“Hello”,到此为止。 第二次指向“帮助!”。 这里 str 开始指向“帮助!”并且“Hello”字符串的引用丢失了,我们无法找回。

                                事实上,当 str 尝试修改现有内容时,会生成另一个新字符串,并且 str 将开始指向该引用。 所以我们看到原始引用处的字符串没有被修改,但在它的引用处是安全的,并且对象实例开始指向不同的引用,因此保持不变性。

                                【讨论】:

                                  【解决方案20】:

                                  回答太晚了,但想从 Java 中的 String 类的作者那里得到一条简明的信息

                                  字符串是常量;它们的值在它们被更改后不能更改 创建的。字符串缓冲区支持可变字符串。因为字符串 对象是不可变的,它们可以共享。

                                  可以从这个文档中得出,任何改变字符串的东西都会返回不同的对象(可能是新的或实习的和旧的)。 关于这一点的不那么微妙的暗示应该来自函数签名。 想一想,“为什么他们让对象上的函数返回对象而不是状态?”。

                                  public String replace(char oldChar, char newChar) 
                                  

                                  还有一个使这种行为明确的来源(来自替换功能文档)

                                  返回一个新字符串,该字符串由替换所有出现的 此字符串中的 oldChar 与 newChar。

                                  来源:https://docs.oracle.com/javase/7/docs/api/java/lang/String.html#replace(char,%20char)

                                  • 作者李博因顿
                                  • 作者亚瑟·范霍夫
                                  • 作者 Martin Buchholz
                                  • 作者 Ulf Zibis

                                  来源:String 的 JavaDoc。

                                  【讨论】:

                                    【解决方案21】:

                                    对象字符串 - 方法本身是“不可变的”。 此操作不会产生任何更改:“letters.replace("bbb", "aaa");"

                                    但分配数据确实会导致字符串内容发生变化:

                                        letters = "aaa";
                                        letters=null;
                                        System.out.println(letters);
                                        System.out.println(oB.hashCode());
                                        System.out.println(letters);
                                        letters = "bbbaaa";
                                        System.out.println(oB.hashCode());
                                        System.out.println(letters);
                                    

                                    //字符串Object的hashcode不变。

                                    【讨论】:

                                      【解决方案22】:

                                      如果HELLO 是您的字符串,那么您不能将HELLO 更改为HILLO。此属性称为不变性属性。

                                      你可以有多个指针String变量来指向HELLO String。

                                      但是如果 HELLO 是 char 数组,那么你可以将 HELLO 更改为 HILLO。例如,

                                      char[] charArr = 'HELLO';
                                      char[1] = 'I'; //you can do this
                                      

                                      编程语言具有不可变的数据变量,因此它可以用作键值对中的键。

                                      【讨论】:

                                        【解决方案23】:

                                        我会用简单的例子来解释一下


                                        考虑任何字符数组:例如char a[]={'h','e','l','l','o'}; 和一个字符串: 字符串 s="你好";


                                        在字符数组上,我们可以使用迭代数组执行诸如只打印最后三个字母的操作; 但在字符串中,我们必须创建新的字符串对象并复制所需的子字符串,其地址将在新的字符串对象中。

                                        例如

                                        ***String s="hello";
                                        String s2=s.substrig(0,3);***
                                        

                                        所以 s2 会有“hel”;

                                        【讨论】:

                                          【解决方案24】:

                                          Java 中的 String 在 Immutable 和 Final 中只是意味着它不能被更改或修改:

                                          案例 1:

                                          class TestClass{  
                                           public static void main(String args[]){  
                                             String str = "ABC";  
                                             str.concat("DEF");  
                                             System.out.println(str);  
                                           }  
                                          } 
                                          

                                          输出:ABC

                                          原因:对象引用str并没有改变其实是一个新对象 “DEF”被创建在池中并且根本没有参考 (即丢失)。

                                          案例 2:

                                          class TestClass{  
                                           public static void main(String args[]){  
                                             String str="ABC";  
                                             str=str.concat("DEF");  
                                             System.out.println(str);  
                                           }  
                                          }  
                                          

                                          输出:ABCDEF

                                          原因:在这种情况下,str 现在指的是一个新对象“ABCDEF” 因此它打印 ABCDEF,即之前的 str 对象“ABC”在池中丢失,没有引用。

                                          【讨论】:

                                            【解决方案25】:

                                            因为字符串是不可变的,所以如果您不将函数的返回值分配给字符串,则不会发生更改。所以在您的问题中,将交换函数返回值的值分配给 s。

                                            s=swap(s, n1, n2) ;那么字符串s的值就会改变。

                                            当我编写程序以获得一些排列字符串时,我也得到了不变的值(虽然它没有给出所有排列,但这是为了回答你的问题)

                                            这是一个例子。

                                            > import java.io.*;  public class MyString { public static void
                                            > main(String []args)throws IOException {  BufferedReader br=new
                                            > BufferedReader(new InputStreamReader(System.in));  String
                                            > s=br.readLine().trim(); int n=0;int k=0;  while(n!=s.length()) {
                                            > while(k<n){  swap(s,k,n); System.out.println(s); swap(s,k,n); k++; }
                                            > n++; } }  public static void swap(String s,int n1,int n2) { char temp;
                                            > temp=s.charAt(n1); StringBuilder sb=new StringBuilder(s);
                                            > sb.setCharAt(n1,s.charAt(n2)); sb.setCharAt(n2,temp); s=sb.toString();
                                            > } }
                                            

                                            但我没有从上面的代码中得到字符串的置换值。所以我将交换函数的返回值分配给字符串并得到了字符串的改变值。分配返回值后,我得到了字符串的置换值。

                                            /import java.util.*; import java.io.*; public class MyString { public static void main(String []args)throws IOException{
                                            BufferedReader br=new BufferedReader(new InputStreamReader(System.in)); 
                                            String s=br.readLine().trim(); int n=0;int k=0; 
                                            while(n!=s.length()){ while(k<n){ s=swap(s,k,n); 
                                            System.out.println(s); s=swap(s,k,n); k++; } n++; } } 
                                            public static String swap(String s,int n1,int n2){
                                            char temp; temp=s.charAt(n1); StringBuilder sb=new StringBuilder(s); sb.setCharAt(n1,s.charAt(n2)); sb.setCharAt(n2,temp); s=sb.toString(); return s; } }
                                            

                                            【讨论】:

                                              【解决方案26】:
                                                  public final class String_Test {
                                              
                                                  String name;
                                                  List<String> list=new ArrayList<String>();
                                              
                                                  public static void main(String[] args) {
                                              
                                                      String_Test obj=new String_Test();
                                                      obj.list.add("item");//List will point to a memory unit- i.e will have one Hashcode value #1234
                                              
                                                      List<String> list2=obj.list; //lis1 also will point to same #1234
                                              
                                                      obj.list.add("new item");//Hashcode of list is not altered- List is mutable, so reference remains same, only value in that memory location changes
                                              
                                                      String name2=obj.name="Myname"; // name2 and name will point to same instance of string -Hashcode #5678
                                                      obj.name = "second name";// String is Immutable- New String HAI is created and name will point to this new instance- bcoz of this Hashcode changes here #0089
                                              
                                                      System.out.println(obj.list.hashCode());
                                                      System.out.println(list2.hashCode());
                                                      System.out.println(list3.hashCode());
                                              
                                                      System.out.println("===========");
                                                      System.out.println(obj.name.hashCode());
                                                      System.out.println(name2.hashCode());
                                                  }
                                              }
                                              

                                              会产生这样的东西

                                              1419358369 1419358369

                                              103056 65078777

                                              不可变对象的目的是它的值一旦被分配就不应改变。 每次您尝试根据实现更改它时,它都会返回新对象。 注意:可以使用 Stringbuffer 代替 string 来避免这种情况。

                                              对于你的最后一个问题:: 你将有一个引用,字符串池中有 2 个字符串.. 除了引用将指向 m!ss!ss!pp!

                                              【讨论】:

                                                猜你喜欢
                                                • 1970-01-01
                                                • 1970-01-01
                                                • 2015-04-15
                                                • 1970-01-01
                                                • 1970-01-01
                                                • 2011-04-05
                                                • 1970-01-01
                                                • 2012-10-06
                                                • 2018-12-17
                                                相关资源
                                                最近更新 更多