提交 754e088d 编写于 作者: W wizardforcel

2.1~5

上级 8ae10506
......@@ -2,15 +2,15 @@
每种编程语言都有自己的数据处理方式。有些时候,程序员必须时刻留意准备处理的是什么类型。您曾利用一些特殊语法直接操作过对象,或处理过一些间接表示的对象吗(C或C++里的指针)?
所有这些在Java里都得到了简化,任何东西都可看作对象。因此,我们可采用一种统一的语法,任何地方均可照搬不误。但要注意,尽管将一切都“看作”对象,但操纵的标识符实际是指向一个对象的“引用”(Handle)。在其他Java参考书里,还可看到有的人将其称作一个“引用”,甚至一个“指针”。可将这一情形想象成用遥控板(引用)操纵电视机(对象)。只要握住这个遥控板,就相当于掌握了与电视机连接的通道。但一旦需要“换频道”或者“关小声音”,我们实际操纵的是遥控板(引用),再由遥控板自己操纵电视机(对象)。如果要在房间里四处走走,并想保持对电视机的控制,那么手上拿着的是遥控板,而非电视机。
所有这些在Java里都得到了简化,任何东西都可看作对象。因此,我们可采用一种统一的语法,任何地方均可照搬不误。但要注意,尽管将一切都“看作”对象,但操纵的标识符实际是指向一个对象的“句柄”(Handle)。在其他Java参考书里,还可看到有的人将其称作一个“引用”,甚至一个“指针”。可将这一情形想象成用遥控板(引用)操纵电视机(对象)。只要握住这个遥控板,就相当于掌握了与电视机连接的通道。但一旦需要“换频道”或者“关小声音”,我们实际操纵的是遥控板(引用),再由遥控板自己操纵电视机(对象)。如果要在房间里四处走走,并想保持对电视机的控制,那么手上拿着的是遥控板,而非电视机。
此外,即使没有电视机,遥控板亦可独立存在。也就是说,只是由于拥有一个引用,并不表示必须有一个对象同它连接。所以如果想容纳一个词或句子,可创建一个String引用:
此外,即使没有电视机,遥控板亦可独立存在。也就是说,只是由于拥有一个引用,并不表示必须有一个对象同它连接。所以如果想容纳一个词或句子,可创建一个`String`引用:
```
String s;
```
但这里创建的只是引用,并不是对象。若此时向s发送一条消息,就会获得一个错误(运行期)。这是由于s实际并未与任何东西连接(即“没有电视机”)。因此,一种更安全的做法是:创建一个引用时,记住无论如何都进行初始化:
但这里创建的只是引用,并不是对象。若此时向`s`发送一条消息,就会获得一个错误(运行期)。这是由于`s`实际并未与任何东西连接(即“没有电视机”)。因此,一种更安全的做法是:创建一个引用时,记住无论如何都进行初始化:
```
String s = "asdf";
......
# 2.2 所有对象都必须创建
创建引用时,我们希望它同一个新对象连接。通常用new关键字达到这一目的。new的意思是:“把我变成这些对象的一种新类型”。所以在上面的例子中,可以说:
创建引用时,我们希望它同一个新对象连接。通常用`new`关键字达到这一目的。`new`的意思是:“把我变成这些对象的一种新类型”。所以在上面的例子中,可以说:
```
String s = new String("asdf");
```
它不仅指出“将我变成一个新字符串”,也通过提供一个初始字符串,指出了“如何生成这个新字符串”。
当然,字符串(String)并非唯一的类型。Java配套提供了数量众多的现成类型。对我们来讲,最重要的就是记住能自行创建类型。事实上,这应是Java程序设计的一项基本操作,是继续本书后余部分学习的基础。
当然,字符串(`String`)并非唯一的类型。Java配套提供了数量众多的现成类型。对我们来讲,最重要的就是记住能自行创建类型。事实上,这应是Java程序设计的一项基本操作,是继续本书后余部分学习的基础。
2.2.1 保存到什么地方
......@@ -20,29 +20,29 @@ String s = new String("asdf");
(3) 堆。一种常规用途的内存池(也在RAM区域),其中保存了Java对象。和栈不同,“内存堆”或“堆”(Heap)最吸引人的地方在于编译器不必知道要从堆里分配多少存储空间,也不必知道存储的数据要在堆里停留多长的时间。因此,用堆保存数据时会得到更大的灵活性。要求创建一个对象时,只需用new命令编制相关的代码即可。执行这些代码时,会在堆里自动进行数据的保存。当然,为达到这种灵活性,必然会付出一定的代价:在堆里分配存储空间时会花掉更长的时间!
(4) 静态存储。这儿的“静态”(Static)是指“位于固定位置”(尽管也在RAM里)。程序运行期间,静态存储的数据将随时等候调用。可用static关键字指出一个对象的特定元素是静态的。但Java对象本身永远都不会置入静态存储空间。
(4) 静态存储。这儿的“静态”(`Static`)是指“位于固定位置”(尽管也在RAM里)。程序运行期间,静态存储的数据将随时等候调用。可用`static`关键字指出一个对象的特定元素是静态的。但Java对象本身永远都不会置入静态存储空间。
(5) 常数存储。常数值通常直接置于程序代码内部。这样做是安全的,因为它们永远都不会改变。有的常数需要严格地保护,所以可考虑将它们置入只读存储器(ROM)。
(6) 非RAM存储。若数据完全独立于一个程序之外,则程序不运行时仍可存在,并在程序的控制范围之外。其中两个最主要的例子便是“流式对象”和“固定对象”。对于流式对象,对象会变成字节流,通常会发给另一台机器。而对于固定对象,对象保存在磁盘中。即使程序中止运行,它们仍可保持自己的状态不变。对于这些类型的数据存储,一个特别有用的技巧就是它们能存在于其他媒体中。一旦需要,甚至能将它们恢复成普通的、基于RAM的对象。Java 1.1提供了对Lightweight persistence的支持。未来的版本甚至可能提供更完整的方案。
2.2.2 特殊情况:主要类型
2.2.2 特殊情况:基本类型
有一系列类需特别对待;可将它们想象成“基本”、“主要”或者“主”(Primitive)类型,进行程序设计时要频繁用到它们。之所以要特别对待,是由于用new创建对象(特别是小的、简单的变量)并不是非常有效,因为new将对象置于“堆”里。对于这些类型,Java采纳了与C和C++相同的方法。也就是说,不是用new创建变量,而是创建一个并非引用的“自动”变量。这个变量容纳了具体的值,并置于栈中,能够更高效地存取。
有一系列类需特别对待;可将它们想象成“基本”、“主要”或者“主”(Primitive)类型,进行程序设计时要频繁用到它们。之所以要特别对待,是由于用`new`创建对象(特别是小的、简单的变量)并不是非常有效,因为`new`将对象置于“堆”里。对于这些类型,Java采纳了与C和C++相同的方法。也就是说,不是用`new`创建变量,而是创建一个并非引用的“自动”变量。这个变量容纳了具体的值,并置于栈中,能够更高效地存取。
Java决定了每种主要类型的大小。就象在大多数语言里那样,这些大小并不随着机器结构的变化而变化。这种大小的不可更改正是Java程序具有很强移植能力的原因之一。
| 基本类型 | 大小 | 最小值 | 最大值 | 包装器类型 |
|---------|---------|-----------|----------------|------------|
| boolean | 1-bit | – | – | Boolean |
| char | 16-bit | Unicode 0 | Unicode 216- 1 | Character |
| byte | 8-bit | -128 | +127 | Byte[11] |
| short | 16-bit | -215 | +215 – 1 | Short1 |
| int | 32-bit | -231 | +231 – 1 | Integer |
| long | 64-bit | -263 | +263 – 1 | Long |
| float | 32-bit | IEEE754 | IEEE754 | Float |
| double | 64-bit | IEEE754 | IEEE754 | Double |
| void | – | – | – | Void1 |
| `boolean` | 1-bit | – | – | `Boolean` |
| `char` | 16-bit | Unicode 0 | Unicode 216- 1 | `Character` |
| `byte` | 8-bit | -128 | +127 | `Byte`[11] |
| `short` | 16-bit | -215 | +215 – 1 | `Short`1 |
| `int` | 32-bit | -231 | +231 – 1 | `Integer` |
| `long` | 64-bit | -263 | +263 – 1 | `Long` |
| `float` | 32-bit | IEEE754 | IEEE754 | `Float` |
| `double` | 64-bit | IEEE754 | IEEE754 | `Double` |
| `void` | – | – | – | `Void`1 |
①:到Java 1.1才有,1.0版没有。
......@@ -65,12 +65,12 @@ Character C = new Character('x');
**1. 高精度数字**
Java 1.1增加了两个类,用于进行高精度的计算:BigInteger和BigDecimal。尽管它们大致可以划分为“包装器”类型,但两者都没有对应的“基本类型”。
Java 1.1增加了两个类,用于进行高精度的计算:`BigInteger``BigDecimal`。尽管它们大致可以划分为“包装器”类型,但两者都没有对应的“基本类型”。
这两个类都有自己特殊的“方法”,对应于我们针对基本类型执行的操作。也就是说,能对int或float做的事情,对BigInteger和BigDecimal一样可以做。只是必须使用方法调用,不能使用运算符。此外,由于牵涉更多,所以运算速度会慢一些。我们牺牲了速度,但换来了精度。
这两个类都有自己特殊的“方法”,对应于我们针对基本类型执行的操作。也就是说,能对`int``float`做的事情,对`BigInteger``BigDecimal`一样可以做。只是必须使用方法调用,不能使用运算符。此外,由于牵涉更多,所以运算速度会慢一些。我们牺牲了速度,但换来了精度。
BigInteger支持任意精度的整数。也就是说,我们可精确表示任意大小的整数值,同时在运算过程中不会丢失任何信息。
BigDecimal支持任意精度的定点数字。例如,可用它进行精确的币值计算。
`BigInteger`支持任意精度的整数。也就是说,我们可精确表示任意大小的整数值,同时在运算过程中不会丢失任何信息。
`BigDecimal`支持任意精度的定点数字。例如,可用它进行精确的币值计算。
至于调用这两个类时可选用的构造器和方法,请自行参考联机帮助文档。
......@@ -82,7 +82,7 @@ BigDecimal支持任意精度的定点数字。例如,可用它进行精确的
Java的一项主要设计目标就是安全性。所以在C和C++里困扰程序员的许多问题都未在Java里重复。一个Java可以保证被初始化,而且不可在它的范围之外访问。由于系统自动进行范围检查,所以必然要付出一些代价:针对每个数组,以及在运行期间对索引的校验,都会造成少量的内存开销。但由此换回的是更高的安全性,以及更高的工作效率。为此付出少许代价是值得的。
创建对象数组时,实际创建的是一个引用数组。而且每个引用都会自动初始化成一个特殊值,并带有自己的关键字:null(空)。一旦Java看到null,就知道该引用并未指向一个对象。正式使用前,必须为每个引用都分配一个对象。若试图使用依然为null的一个引用,就会在运行期报告问题。因此,典型的数组错误在Java里就得到了避免。
创建对象数组时,实际创建的是一个引用数组。而且每个引用都会自动初始化成一个特殊值,并带有自己的关键字:`null`(空)。一旦Java看到`null`,就知道该引用并未指向一个对象。正式使用前,必须为每个引用都分配一个对象。若试图使用依然为null的一个引用,就会在运行期报告问题。因此,典型的数组错误在Java里就得到了避免。
也可以创建基本类型数组。同样地,编译器能够担保对它的初始化,因为会将那个数组的内存划分成零。
......
......@@ -35,11 +35,11 @@
}
```
编译器会认为变量x已被定义。所以C和C++能将一个变量“隐藏”在一个更大的作用域里。但这种做法在Java里是不允许的,因为Java的设计者认为这样做使程序产生了混淆。
编译器会认为变量`x`已被定义。所以C和C++能将一个变量“隐藏”在一个更大的作用域里。但这种做法在Java里是不允许的,因为Java的设计者认为这样做使程序产生了混淆。
2.3.2 对象的作用域
Java对象不具备与基本类型一样的存在时间。用new关键字创建一个Java对象的时候,它会超出作用域的范围之外。所以假若使用下面这段代码:
Java对象不具备与基本类型一样的存在时间。用`new`关键字创建一个Java对象的时候,它会超出作用域的范围之外。所以假若使用下面这段代码:
```
{
......@@ -47,8 +47,8 @@ String s = new String("a string");
} /* 作用域的终点 */
```
那么引用s会在作用域的终点处消失。然而,s指向的String对象依然占据着内存空间。在上面这段代码里,我们没有办法访问对象,因为指向它的唯一一个引用已超出了作用域的边界。在后面的章节里,大家还会继续学习如何在程序运行期间传递和复制对象引用。
那么引用`s`会在作用域的终点处消失。然而,`s`指向的`String`对象依然占据着内存空间。在上面这段代码里,我们没有办法访问对象,因为指向它的唯一一个引用已超出了作用域的边界。在后面的章节里,大家还会继续学习如何在程序运行期间传递和复制对象引用。
这样造成的结果便是:对于用new创建的对象,只要我们愿意,它们就会一直保留下去。这个编程问题在C和C++里特别突出。看来在C++里遇到的麻烦最大:由于不能从语言获得任何帮助,所以在需要对象的时候,根本无法确定它们是否可用。而且更麻烦的是,在C++里,一旦工作完成,必须保证将对象清除。
这样造成的结果便是:对于用`new`创建的对象,只要我们愿意,它们就会一直保留下去。这个编程问题在C和C++里特别突出。看来在C++里遇到的麻烦最大:由于不能从语言获得任何帮助,所以在需要对象的时候,根本无法确定它们是否可用。而且更麻烦的是,在C++里,一旦工作完成,必须保证将对象清除。
这样便带来了一个有趣的问题。假如Java让对象依然故我,怎样才能防止它们大量充斥内存,并最终造成程序的“凝固”呢。在C++里,这个问题最令程序员头痛。但Java以后,情况却发生了改观。Java有一个特别的“垃圾收集器”,它会查找用new创建的所有对象,并辨别其中哪些不再被引用。随后,它会自动释放由那些闲置对象占据的内存,以便能由新对象使用。这意味着我们根本不必操心内存的回收问题。只需简单地创建对象,一旦不再需要它们,它们就会自动离去。这样做可防止在C++里很常见的一个编程问题:由于程序员忘记释放内存造成的“内存溢出”。
......@@ -3,23 +3,23 @@
2.4 新建数据类型:类
如果说一切东西都是对象,那么用什么决定一个“类”(Class)的外观与行为呢?换句话说,是什么建立起了一个对象的“类型”(Type)呢?大家可能猜想有一个名为“type”的关键字。但从历史看来,大多数面向对象的语言都用关键字“class”表达这样一个意思:“我准备告诉你对象一种新类型的外观”。class关键字太常用了,以至于本书许多地方并没有用粗体字或双引号加以强调。在这个关键字的后面,应该跟随新数据类型的名称。例如:
如果说一切东西都是对象,那么用什么决定一个“类”(Class)的外观与行为呢?换句话说,是什么建立起了一个对象的“类型”(Type)呢?大家可能猜想有一个名为`type`的关键字。但从历史看来,大多数面向对象的语言都用关键字`class`表达这样一个意思:“我准备告诉你对象一种新类型的外观”。`class`关键字太常用了,以至于本书许多地方并没有用粗体字或双引号加以强调。在这个关键字的后面,应该跟随新数据类型的名称。例如:
```
class ATypeName {/*类主体置于这里}
```
这样就引入了一种新类型,接下来便可用new创建这种类型的一个新对象:
这样就引入了一种新类型,接下来便可用`new`创建这种类型的一个新对象:
```
ATypeName a = new ATypeName();
```
ATypeName里,类主体只由一条注释构成(星号和斜杠以及其中的内容,本章后面还会详细讲述),所以并不能对它做太多的事情。事实上,除非为其定义了某些方法,否则根本不能指示它做任何事情。
`ATypeName`里,类主体只由一条注释构成(星号和斜杠以及其中的内容,本章后面还会详细讲述),所以并不能对它做太多的事情。事实上,除非为其定义了某些方法,否则根本不能指示它做任何事情。
2.4.1 字段和方法
定义一个类时(我们在Java里的全部工作就是定义类、制作那些类的对象以及将消息发给那些对象),可在自己的类里设置两种类型的元素:数据成员(有时也叫“字段”)以及成员函数(通常叫“方法”)。其中,数据成员是一种对象(通过它的引用与其通信),可以为任何类型。它也可以是基本类型(并不是引用)之一。如果是指向对象的一个引用,则必须初始化那个引用,用一种名为“构造器”(第4章会对此详述)的特殊函数将其与一个实际对象连接起来(就象早先看到的那样,使用new关键字)。但若是一种基本类型,则可在类定义位置直接初始化(正如后面会看到的那样,引用亦可在定义位置初始化)。
定义一个类时(我们在Java里的全部工作就是定义类、制作那些类的对象以及将消息发给那些对象),可在自己的类里设置两种类型的元素:数据成员(有时也叫“字段”)以及成员函数(通常叫“方法”)。其中,数据成员是一种对象(通过它的引用与其通信),可以为任何类型。它也可以是基本类型(并不是引用)之一。如果是指向对象的一个引用,则必须初始化那个引用,用一种名为“构造器”(第4章会对此详述)的特殊函数将其与一个实际对象连接起来(就象早先看到的那样,使用`new`关键字)。但若是一种基本类型,则可在类定义位置直接初始化(正如后面会看到的那样,引用亦可在定义位置初始化)。
每个对象都为自己的数据成员保有存储空间;数据成员不会在对象之间共享。下面是定义了一些数据成员的类示例:
......@@ -51,11 +51,11 @@ d.b = false;
myPlane.leftTank.capacity = 100;
```
除容纳数据之外,DataOnly类再也不能做更多的事情,因为它没有成员函数(方法)。为正确理解工作原理,首先必须知道“参数”和“返回值”的概念。我们马上就会详加解释。
除容纳数据之外,`DataOnly`类再也不能做更多的事情,因为它没有成员函数(方法)。为正确理解工作原理,首先必须知道“参数”和“返回值”的概念。我们马上就会详加解释。
**1. 成员的默认值**
**1. 基本类型的成员的默认值**
若某个主数据类型属于一个类成员,那么即使不明确(显式)进行初始化,也可以保证它们获得一个默认值。
若某个类成员属于基本类型,那么即使不明确(显式)进行初始化,也可以保证它们获得一个默认值。
基本类型 默认值
......@@ -78,4 +78,4 @@ double 0.0d
int x;
```
那么x会得到一些随机值(这与C和C++是一样的),不会自动初始化成零。我们责任是在正式使用x前分配一个适当的值。如果忘记,就会得到一条编译期错误,告诉我们变量可能尚未初始化。这种处理正是Java优于C++的表现之一。许多C++编译器会对变量未初始化发出警告,但在Java里却是错误。
那么`x`会得到一些随机值(这与C和C++是一样的),不会自动初始化成零。我们责任是在正式使用x前分配一个适当的值。如果忘记,就会得到一条编译期错误,告诉我们变量可能尚未初始化。这种处理正是Java优于C++的表现之一。许多C++编译器会对变量未初始化发出警告,但在Java里却是错误。
......@@ -13,7 +13,7 @@ Java的“方法”决定了一个对象能够接收的消息。通过本节的
返回类型是指调用方法之后返回的数值类型。显然,方法名的作用是对具体的方法进行标识和引用。参数列表列出了想传递给方法的信息类型和名称。
Java的方法只能作为类的一部分创建。只能针对某个对象调用一个方法(注释③),而且那个对象必须能够执行那个方法调用。若试图为一个对象调用错误的方法,就会在编译期得到一条出错消息。为一个对象调用方法时,需要先列出对象的名字,在后面跟上一个句点,再跟上方法名以及它的参数列表。亦即“对象名.方法名(参数1,参数2,参数3...)。举个例子来说,假设我们有一个方法名叫f(),它没有参数,返回的是类型为int的一个值。那么,假设有一个名为a的对象,可为其调用方法f(),则代码如下:
Java的方法只能作为类的一部分创建。只能针对某个对象调用一个方法(注释③),而且那个对象必须能够执行那个方法调用。若试图为一个对象调用错误的方法,就会在编译期得到一条出错消息。为一个对象调用方法时,需要先列出对象的名字,在后面跟上一个句点,再跟上方法名以及它的参数列表。亦即`对象名.方法名(参数1,参数2,参数3...)`。举个例子来说,假设我们有一个方法名叫`f()`,它没有参数,返回的是类型为`int`的一个值。那么,假设有一个名为`a`的对象,可为其调用方法`f()`,则代码如下:
```
int x = a.f();
......@@ -21,7 +21,7 @@ int x = a.f();
返回值的类型必须兼容x的类型。
象这样调用一个方法的行动通常叫作“向对象发送一条消息”。在上面的例子中,消息是f(),而对象是a。面向对象的程序设计通常简单地归纳为“向对象发送消息”。
象这样调用一个方法的行动通常叫作“向对象发送一条消息”。在上面的例子中,消息是`f()`,而对象是`a`。面向对象的程序设计通常简单地归纳为“向对象发送消息”。
③:正如马上就要学到的那样,“静态”方法可针对类调用,毋需一个对象。
......@@ -29,7 +29,7 @@ int x = a.f();
参数列表规定了我们传送给方法的是什么信息。正如大家或许已猜到的那样,这些信息——如同Java内其他任何东西——采用的都是对象的形式。因此,我们必须在参数列表里指定要传递的对象类型,以及每个对象的名字。正如在Java其他地方处理对象时一样,我们实际传递的是“引用”(注释④)。然而,引用的类型必须正确。倘若希望参数是一个“字符串”,那么传递的必须是一个字符串。
④:对于前面提及的“特殊”数据类型boolean,char,byte,short,int,long,,float以及double来说是一个例外。但在传递对象时,通常都是指传递指向对象的引用。
④:对于前面提及的“特殊”数据类型`boolean``char``byte``short``int``long`,,`float`以及`double`来说是一个例外。但在传递对象时,通常都是指传递指向对象的引用。
下面让我们考虑将一个字符串作为参数使用的方法。下面列出的是定义代码,必须将它置于一个类定义里,否则无法编译:
......@@ -39,10 +39,10 @@ return s.length() * 2;
}
```
这个方法告诉我们需要多少字节才能容纳一个特定字符串里的信息(字符串里的每个字符都是16位,或者说2个字节、长整数,以便提供对Unicode字符的支持)。参数的类型为String,而且叫作s。一旦将s传递给方法,就可将它当作其他对象一样处理(可向其发送消息)。在这里,我们调用的是length()方法,它是String的方法之一。该方法返回的是一个字符串里的字符数。
这个方法告诉我们需要多少字节才能容纳一个特定字符串里的信息(字符串里的每个字符都是16位,或者说2个字节、长整数,以便提供对Unicode字符的支持)。参数的类型为`String`,而且叫作`s`。一旦将`s`传递给方法,就可将它当作其他对象一样处理(可向其发送消息)。在这里,我们调用的是`length()`方法,它是`String`的方法之一。该方法返回的是一个字符串里的字符数。
通过上面的例子,也可以了解return关键字的运用。它主要做两件事情。首先,它意味着“离开方法,我已完工了”。其次,假设方法生成了一个值,则那个值紧接在return语句的后面。在这种情况下,返回值是通过计算表达式“s.length()*2”而产生的。
可按自己的愿望返回任意类型,但倘若不想返回任何东西,就可指示方法返回void(空)。下面列出一些例子。
通过上面的例子,也可以了解`return`关键字的运用。它主要做两件事情。首先,它意味着“离开方法,我已完工了”。其次,假设方法生成了一个值,则那个值紧接在`return`语句的后面。在这种情况下,返回值是通过计算表达式`s.length()*2`而产生的。
可按自己的愿望返回任意类型,但倘若不想返回任何东西,就可指示方法返回`void`(空)。下面列出一些例子。
```
boolean flag() { return true; }
......@@ -51,6 +51,6 @@ void nothing() { return; }
void nothing2() {}
```
若返回类型为void,则return关键字唯一的作用就是退出方法。所以一旦抵达方法末尾,该关键字便不需要了。可在任何地方从一个方法返回。但假设已指定了一种非void的返回类型,那么无论从何地返回,编译器都会确保我们返回的是正确的类型。
若返回类型为`void`,则`return`关键字唯一的作用就是退出方法。所以一旦抵达方法末尾,该关键字便不需要了。可在任何地方从一个方法返回。但假设已指定了一种非`void`的返回类型,那么无论从何地返回,编译器都会确保我们返回的是正确的类型。
到此为止,大家或许已得到了这样的一个印象:一个程序只是一系列对象的集合,它们的方法将其他对象作为自己的参数使用,而且将消息发给那些对象。这种说法大体正确,但通过以后的学习,大家还会知道如何在一个方法里作出决策,做一些更细致的基层工作。至于这一章,只需理解消息传送就足够了。
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册