`
7454103
  • 浏览: 128892 次
  • 性别: Icon_minigender_1
  • 来自: 西安
社区版块
存档分类
最新评论

java1.5新特性详解!java1.5新特性详解!

阅读更多

      java1.5新特性详解!   java1.5新特性详解!  java1.5新特性详解!

1.3.4  可变参数

C和C++是支持函数可变长度参数列表的两种语言。Java决定引入这方面的优势。只在必要时才使用可变参数列表。如果滥用它们,就很容易创建出制造混乱的源代码。C语言在函数声明中使用省略号(…)来代表“任意数量的参数(0个或者多个)”。Java也使用省略号,但是将它同类型和标识符一起使用。这里的类型可以是任意内容,如任意类、任意基本类型,甚至是数组类型。然而,当在一个数组中使用它时,省略号必须出现在类型描述之前和方括号之后。由于可变参数的自然属性,每个方法只能有一个类型作为可变参数,同时它必须出现在参数列表的最后。

下面这个例子中的方法以任意数量的基本整数作为参数,并返回它们的总和:

public int sum(int... intList)

{

     int i, sum;

     sum=0;

     for(i=0; i<intList.length; i++)  {

          sum += intList[i];

     }

    return(sum);

}

从被标记为可变的参数位置算起,所有传入的参数都被组合成一个数组。这使得测试传入了多少参数变得简单。需要做的事情就是引用数组的length属性,同时数组还提供对每个参数的便捷访问。

以下是一个对任意数量元素的数组中的所有值求和的完整示例程序:

public class VarArgsExample {

     int sumArrays(int[]...  intArrays}

     {

          int sum,  i, j;

          sum=0;

          for(i=0;  i<intArrays.length;  i++)  {

               for(j=0;  j<intArrays[i].length;  j++)  {

                    sum += intArrays[i] [j];

               }

          }

          return(sum);

     }

     public static void main(String args[])

     {

          VarArgsExample va = new VarArgsExample();

          int sum=0;

          sum = va.sumArrays(new int[]{1,2,3},

                                 new int[]{4,5,6},

                                 new int[]{10,16});

          System.out.println("The sum of the numbers is:  " + sum);

     }

}

这段代码跟在已建立的方法之后用来定义和使用一个可变参数。省略号出现在方括号之后(也就是说,在可变参数的类型之后)。在该方法中,参数intArrays只是一个数组的数组。

1.3.5  装箱/拆箱转换

以前的Java语言存在着一种冗长乏味的处理方式,就是将基本类型(例如int和char)转换为它们的对应引用类型(例如,int对应的Integer,以及char对应的Character)时需要进行手工操作。摆脱这种经常性打包/拆包操作的解决方法就是使用装箱/拆箱转换。

1.装箱转换

装箱转换是一个隐式操作,它将基本类型(例如int)自动地放置到它对应的引用类型(在本例中是Integer)的一个实例中。拆箱则是相反的操作,即将一个引用类型(例如Integer)转换为它的基本类型(int)。如果没有装箱,就需要按如下方式将int基本类型添加到一个集合(它容纳Object类型)中:

Integer intObject;

int intPrimitive;

ArrayList arrayList = new ArrayList();

intPrimitive = 11;

intObject = new Integer(intPrimitive);

arrayList.put (intObject);  // cannot add intPrimitive directly

尽管此代码很简单,但是完全没必要这么冗长。随着装箱转换的引入,以上代码可以重新编写如下:

int intPrimitive;

ArrayList arrayList = new ArrayList();

intPrimitive = 11;

// here intPrimitive is automatically wrapped in an Integer

arrayList.put(intPrimitive);

这样,将不再需要创建一个Integer对象来将一个int放置到集合中。在结果引用类型的value()方法(例如Integer的intValue())等于源基本类型的值时,装箱转换将会发生。可以参考下面的表1-1来查看所有有效的装箱转换。如果存在任意其他的类型,那么装箱转换将是一种恒等转换(将类型转换为其自身类型)。注意,由于引入了装箱转换,一些引用基本类型的被禁转换也不再被禁止,因为基本类型现在也可以被转换成某些引用类型。

表1-1

 

基本类型

引用类型

基本类型

引用类型

boolean

Boolean

int

Integer

byte

Byte

long

Long

char

Character

float

Float

short

Short

double

Double

2.拆箱转换

Java也引入了拆箱转换,它将一个引用类型(例如Integer或者Float)转换成其自身的基本类型(例如int或者float)。可以参考下面的表1-2来查看所有有效的拆箱转换。在引用类型的value方法等于结果基本类型的值时,拆箱转换将会发生。

表1-2

 

引用类型

基本类型

引用类型

基本类型

Boolean

boolean

Integer

int

Byte

byte

Long

long

Character

char

Float

float

Short

short

Double

double

3.装箱/拆箱转换的有效上下文

因为装箱和拆箱操作是一种转换操作,所以它们无需程序员的特定指令即可自动发生(与类型转换(casting)不一样,类型转换是一种显式操作)。存在几种可以出现装箱/拆箱转换的上下文环境。

(1)赋值

当表达式的值被赋予一个变量时,会发生赋值转换。当表达式的类型与变量的类型不匹配,并且不会存在数据丢失的危险时,转换将会自动发生。发生转换的优先级首先是恒等转换,接着是扩展基本类型转换,然后是扩展引用类型转换,最后才是新的装箱(或者拆箱)转换。如果这些转换都无效,那么编译器将会发出一个错误。

(2)方法调用

进行方法调用时,如果参数类型没有精确匹配那些传入的参数,那么就可能发生一些转换。这些转换被认为是方法调用转换。每个在类型上没有精确匹配方法签名中对应参数的参数都将会被转换。可能发生的转换依次是恒等转换、扩展基本类型转换、扩展引用类型转换,然后是新的装箱(或者拆箱)转换。

在多个方法都匹配特定方法调用时,要选择最精确的方法。匹配最精确方法的规则外加装箱转换只需稍作修改。如果所有用于解决方法歧义的标准检查都失败了,那么装箱/拆箱转换将不会用于解决歧义。这样,在执行装箱转换检查时,方法调用将会被认为有歧义并且失败。

将装箱和泛型联合使用,可以编写如下代码:

import java.util.*;

public class BoxingGenericsExample {

     public static void main(String args[])

     {

          HashMap<String, Integer> hm = new HashMap<String,Integer>();

          hm.put("speed",  20);

     }

}

基本类型整数20自动转换成为一个Integer类型,然后按照指定关键字被放入到HashMap中。

1.3.6  静态导入

Java语言中引入了导入静态数据,以简化静态属性和方法的使用。在导入静态信息后,就可以使用方法/属性,而不需要限制方法/属性到所属类名称。例如,通过导入Math类的静态成员,就可以编写abs或者sqrt,而不用写成Math.abs和Math.sqrt。

这种机制同时还阻止了一种危险的编码实践,即将一组静态属性放入一个接口中,然后在每个需要使用这些属性的类中实现该接口。为了能够使用不受限制的属性,不应该实现下面的接口:

interface ShapeNumbers {

     public static int CIRCLE = 0;

     public static int SQUARE = 1;

     public static int TRIANGLE = 2;

}

实现这个接口会对ShapeNumbers接口产生不必要的依赖性。更糟糕的是,随着类的进化,特别是在其他类也需要访问这些常量,并且实现这个接口的情况下,对其进行维护会变得很困难。如果包含这些属性的接口进行了修改并且只有一些类被重新编译,那么已编译类相互之间很容易遇到同步问题。

为了更清楚地理解这点,将静态成员放入到一个类(而不是放入一个接口)中,然后通过一个已修改的导入指令语法导入。ShapeNumbers将修订如下:

package MyConstants;

class ShapeNumbers {

     public static int CIRCLE = 0;

     public static int SQUARE = 1;

     public static int TRIANGLE = 2;

}

然后,一个客户端类从ShapeNumbers类中导入静态信息,接着就能够使用CIRCLE、SQUARE和TRIANGLE属性,而不需要为它们加上ShapeNumbers和成员操作符前缀。

为了导入类中的静态成员,请在Java源程序文件的导入部分(顶部)指定如下代码:

import static MyConstants. ShapeNumbers.*;  // imports all static data

这行语法只是根据标准的导入语句格式进行了稍许修改。关键字static添加在import关键字之后,因为静态信息正在从一个特定的类中被导入,所以现在不是导入包,而总是添加类名称。关键字static添加到导入语句的主要原因是为了清晰地向那些读源代码的人显示静态信息的导入。

也可通过以下语法单独导入常量:

import static MyConstants.ShapeNumbers.CIRCLE;

import static MyConstants. ShapeNumbers. SQUARE;

这种语法也是所希望的。关键字static被包含进来,因为这是一个静态导入,并且要导入的静态信息片段被各自分开显式指定。

不能从默认包的一个类中静态地导入数据。类必须位于一个指定的包中。同时,静态属性和方法可能会产生冲突。例如,下面是包含静态常量的两个类(分别位于Colors.java和Fruits.java中):

package MyConstants;

public class Colors {

     public static int white = 0;

     public static int black = 1;

     public static int red = 2;

     public static int blue = 3;

     public static int green = 4;

     public static int orange = 5;

     public static int grey = 6;

}

package MyConstants;

public class Fruits {

     public static int apple = 500;

     public static int pear = 501;

     public static int orange = 502;

     public static int banana = 503;

     public static int strawberry = 504;

}

如果编写一个类,试图同时对这两个类进行静态导入,在使用一个同时在上述两个类中定义的静态变量之前,一切进展都很正常:

import static MyConstants. Colors.*;

import static MyConstants. Fruits.*;

public class StaticTest {

     public static void main(String args[])

     {

          System.out.println("orange = " + orange);

          System.out.println("color orange = " + Colors.orange);

          System.out.println("Fruity orange = " + Fruits.orange);

     }

}

上述程序的第七行将导致如下编译器错误。由于标识符orange在Colors和Fruits中都定义了,因此编译器无法解决这种分歧:

StaticTest.java:7:  reference to orange is ambiguous,  both variable orange in

MyConstants.Colors and variable orange in MyConstants. Fruits match

           System.out.println("orange = " + orange);

在本例中,就应该使用定义静态数据的类来显式限制这种冲突名称。不是直接编写orange,而是编写Colors.orange或者Fruits.orange。

1.3.7  枚举

Java在JDK 5版本中从语言级别上引入了枚举支持。枚举是指一个经过排序的、被打包成一个单一实体的项列表。一个枚举的实例可以使用枚举项列表中任意单一项的值。可能的最简单的枚举是下面所显示的Colors枚举:

public enum Colors { red, green, blue }

它们给出了将一个任意项同另一个项相比较的能力,并且可以在一个已定义项列表中进行迭代。枚举(在Jave中简称为enum)是一个特定类型的类。所有枚举都是Java中的新类java.lang.Enum的隐式子类。此类不能手工进行子类定义。

在Java中内置枚举支持有许多好处。枚举是类型安全的,性能可与使用常量相媲美。枚举中的常量名称无需用枚举名称进行限定。客户端不需要建立对枚举中常量的了解,因此可以容易地对枚举进行修改,而无需修改客户端。如果常量从枚举中被删除了,那么客户端将会失败并且将会收到一个错误消息。枚举中的常量名称可以被打印,因此除了仅仅得到列表中项的序号外还可以获取更多信息。这也意味着常量可用作集合的名称,例如HashMap。

因为在Java中一个枚举就是一个类,它也可以有域和方法,并且实现接口。枚举在switch语句中可以一种直接的方式被使用,并且可以便于程序员相对简单地去理解和使用。

下面是一个基本的枚举声明,以及它在switch语句中的使用。如果希望追踪某个用户正在使用什么操作系统,就可以使用一个操作系统的枚举,即OperatingSystems enum。注意,因为一个枚举实际上是一个类,那么如果在同一个文件中的其他类是公用的,则枚举就不能是公用的了。同时也要注意,在switch语句中,常量名称不能用常量所在枚举的名称限制。这些细节将由编译器基于switch子句中使用的enum类型来自动处理:

import java.util.*;

enum OperatingSystems {

     windows,  unix,  linux,  macintosh

}

public class EnumExample1 {

     public static void main(String args[])

     {

          OperatingSystems os;

          os = OperatingSystems.windows;

          switch(os)  {

               case windows:

                    System.out.println("You chose Windows!");

                    break;

               case unix:

                    System.out.println("You chose Unix!");

                    break;

              case linux:

                    System.out.println("You chose Linux!"};

                    break;

              case macintosh:

                    System.out.println{"You chose Macintosh!");

                    break;

              default:

                    System.out.println{"I don't know your OS."};

                   break;

         }

    }

}

Java.lang.Enum类实现了Comparable和Serializable接口。在该类中已提供了比较枚举以及将它们序列化成为一个数据源的细节。不能将一个enum标记为abstract,除非每一个常量都有一个类主体,并且这些类主体覆盖了enum中的抽象方法。同时也要注意,枚举不能使用new进行实例化。编译器将会告知“enum types may not be instantiated.”(枚举类型不可以被实例化)。

Java引入了两个新集合:EnumSet和EnumMap,它们只是想在使用enum时优化集和映射的性能。枚举可以与当前已存在的集合类一起使用,或者在需要剪裁成枚举的优化活动时与新集合一起使用。

方法可声明在一个enum中。但是,对一个正在定义的构造方法来说会有所限制。构造函数不能链接超类构造方法,除非超类是另一个enum。enum中的每个常量都可以有一个类主体,但是因为这实际上是一个匿名类,因此不能定义一个构造函数。

也可以向枚举和单个enum常量添加属性。enum常量后面也可以跟有参数,这些参数被传递给在enum中定义的构造函数。

以下是一个具有域和方法的枚举例子:

enum ProgramFlags {

     showErrors(0x01),

     includeFileOutput(0x02),

     useAlternateProcessor{0x04};

     private int bit;

     ProgramFlags(int bitNumber)

     {

          bit = bitNumber;

     }

     public int getBitNumber()

     {

          return(bit);

     }

}

public class EnumBitmapExample {

     public static void main(String args[])

     {

          ProgramFlags flag = ProgramFlags.showErrors;

          System.out.println("Flag selected is:  "+

                                       flag.ordinal() +

                                 "which is" +

                                      flag.name());

     }

}

ordinal()方法返回列表中常量的位置。因为它在列表的第一个位置出现,而且序号值是从0开始的,所以showErrors的值是0。Name()方法可以用来获取常量的名称,这使得可以获取有关枚举的更多信息。

1.3.8  元数据

Sun公司决定在Java的JDK 5版本中包括的另一个特性是元数据功能。这使得人们可以使用工具能够分析的额外信息来标记类,同时也可以自动向类应用特定代码块。元数据功能在java.lang. annotation包中被引入。注释是指Java中一个tag与构造(例如类,在注释术语中被称为目标)之间的关联。能够被注释的构造的类型在java.lang.annotation.ElementType枚举中列出,同时在下面的表1-3中列出。甚至注释也可以被注释。TYPE覆盖了类、接口和enum声明。

表1-3

 

ElementType常量

ElementType常量

ANNOTATION_TYPE

METHOD

CONSTRUCTOR

PACKAGE

FIELD

PARAMETER

LOCAL_VARIABLE

TYPE

另一个引入的概念是注释的生命周期,被称为保持(retention)。某些注释也许只是在Java源代码级别上才有用,例如javadoc工具的注释。其他的注释也许在程序正在执行时才需要。RetentionPolicy枚举列出了一个注释的3个type生命周期。SOURCE策略表示注释应该由编译器丢弃,也就是说,该注释只在源代码级别上有用。CLASS策略表示注释应该在类文件中出现,但是在运行时可能被丢弃。RUNTIME策略则表示注释在整个程序执行期间都应该有效,并且这些注释可以通过反射来查看。

在此包中定义了几个注释类型。表1-4中列出了这些类型。这些注释中的每一个都是从Annotation接口继承而来,该接口定义了equals方法和toString方法。

表1-4

 

注释类名称

描述

Target

指定一个注释类型可以应用的程序元素。每个程序元素只能出现一次

Documented

指定注释应该通过javadoc或者其他存档工具存档。这只能应用于注释

Inherited

从超类继承注释,而不是接口。作用于此注释的策略是RUNTIME,它只能应用于注释

Retention

表示作用于程序元素上的注释应该多长时间有效。请参见前面讨论的RetentionPolicy。作用于此注释的策略是RUNTIME,它只能应用于注释

Deprecated

标记一个不赞成使用的程序元素,告诉开发人员他们不应该再使用该元素。保持策略是SOURCE

Overrides

表示一个方法将要覆盖一个父类中的方法。如果确实不存在覆盖,那么编译器将会生成一个错误消息。这只能应用于方法

JDK 5引入了两个有用的源代码级别注释,即@deprecated和@overrides。@deprecated注释用来标记一个不赞成使用的方法,也就是说,客户端程序员不应该使用它。当在一个类方法中遇到程序员使用这种注释时,编译器将会发出一个警告。另一个注释@overrides,用来标记一个覆盖父类中方法的方法。编译器将会确认标记为@overrides的方法真正是覆盖了父类中的一个方法。如果子类中的方法没有覆盖父类中的方法,那么编译器将会发送一个错误,警告程序员方法签名不匹配父类中的方法。

开发一个自定义注释并不困难。下面创建一个CodeTag注释,它可以存储基本的作者信息和修改日期信息,同时也存储任意应用到该段代码的bug修复。注释将被限制到类和方法上:

import java.lang.annotation.*;

@Retention(RetentionPolicy. SOURCE)

@Target({ElementType. TYPE, ElementType.METHOD})

public @interface CodeTag {

     String authorName();

     String lastModificationDate();

     String bugFixes() default "";

}

Retention被设置为SOURCE,这意味着此注释在编译时和运行时是无效的。doclet API用来访问源代码级别上的注释。Target(对于类/接口/枚举)被设置为TYPE,对于方法则被设置为METHOD。如果CodeTag注释被应用到任意其他的源代码元素上,那么就会生成一个编译器错误。前面的两个注释元素是authorName和lastModificationDate,两者都是必要的。bugFixes元素如果没有指定,那么默认是空字符串。下面是一个使用CodeTag注释的示例类:

import java.lang.annotation.*;

@CodeTag(authorName="Dilbert",

           lastModificationDate="May 7,  2006")

public class ServerCommandProcessor {

     @CodeTag(authorName="Dilbert",

                lastModificationDate="May 10,  2006",

                bugFixes="BUG0170")

     public void setParams(String serverName)

     {

          //…

     }

     public void executeCommand(String command,  Object... params)

     {

          //…

     }

}

注意注释是如何用来标记谁对源代码进行了修改以及是何时修改的。由于bug的修复从而使得方法在类修改之后一天被修改。自定义的注释可以用来追踪关于源代码修改的信息。为了查看或者处理这些源代码注释,就必须使用doclet API。

doclet API(也叫做Javadoc API)已经扩展到可以支持源代码中的注释处理。要使用doclet API,就必须在类路径中包括tools.jar文件(对于版本5或更高的版本,位于默认JDK安装的库文件目录中)。通过编写一个扩展了com.sun.javadoc.Doclet的Java类就可以使用doclet API。必须实现start方法,因为这是Javadoc调用doclet以执行自定义处理的方法。下面这个简单的doclet打印一个Java源文件中的所有类和方法:

import com.sun.javadoc.*;

public class ListClasses extends Doclet {

     public static boolean start(RootDoc root)  {

          ClassDoc[] classes = root.classes();

           for  (ClassDoc cd :  classes)  {

                 System.out.println("Class  [" + cd + "]  has the following methods");

                 for(MemberDoc md :  cd.methods())  {

                     System.out.println(" "+ md);

                 }

           }

           return true;

     }

}

start方法将RootDoc作为一个参数,该参数通过javadoc工具被自动传入。从RootDoc开始可以访问源代码中的所有元素,以及关于命令行(例如增加的包和类)的信息。

针对注释添加到doclet API中的接口是AnnotationDesc、AnnotationDesc.Element ValuePair、AnnotationTypeDoc、AnnotationTypeElementDoc和AnnotationValue。

Java源代码中可以有注释的任意元素都具有一个annotations()方法,该方法与doclet API同源代码元素的对应部分相关联。这些元素是AnnotationTypeDoc、AnnotationTypeElementDoc、ClassDoc、ConstructorDoc、ExecutableMemberDoc、FieldDoc、MethodDoc以及MemberDoc。Annotations()方法返回一个AnnotationDesc数组。

1.AnnotationDesc

此类代表一个注释,它包括一个注释类型(AnnotationTypeDoc)和一个同它们的值相配对的注释类型元素数组。AnnotationDesc定义了表1-5中的方法。

表1-5

 

方法

描述

AnnotationTypeDoc annotationType()

返回这个注释的类型

AnnotationDesc.ElementValuePair[] elementValues()

返回一个注释元素的数组以及它们的值。只返回被显式列出的元素。没有被显式列出的元素(已经假设了它们的默认值),将不会被返回,因为此方法只处理被列出的元素。如果没有元素,那么将返回一个空数组

2.AnnotationDesc.ElementValuePair

它代表了一个注释类型的元素及其值之间的关联。它定义了表1-6中的方法。

表1-6

 

方法

描述

AnnotationTypeElementDoc element()

返回注释类型元素

AnnotationValue value()

返回注释类型元素的值

3.AnnotationTypeDoc

此接口代表了源代码中的一个注释,就如同ClassDoc代表一个Class一样。它只定义了一个方法,见表1-7。

表1-7

 

方法

描述

AnnotationTypeElementDoc[] elements()

返回此注释类型的元素的数组

4.AnnotationTypeElementDoc

此接口代表了一个注释类型的元素,它定了一个方法,见表1-8。

表1-8

 

方法

描述

AnnotationValue defaultValue()

返回与此注释类型相关联的默认值;如果没有默认值,则返回null

5.AnnotationValue

此接口代表了一个注释类型元素的值,它定义的方法见表1-9。

表1-9

 

方法

描述

String toString()

返回该值的一个字符串表示形式

Object value()

返回值。此值所表示的对象可以是下面的任意类型:
q      基本类型(例如Integer或者Float)的一个包装器类
q      一个String
q   一个Type(代表一个类、一个泛型类、一个类型变量、一个通配符类,或者一个基本数据类型)
q      一个FieldDoc(代表一个enum常量)
q      一个AnnotationDesc
q   一个AnnotationValue数组

下面是一个使用doclet API所提供的注释支持的示例。此doclet显示它在一个源文件中发现的所有注释及其值:

import com.sun.javadoc.*;

import java.lang.annotation.*;

public class AnnotationViewer {

     public static boolean start(RootDoc root)

     {

          ClassDoc[] classes = root.classes();

          for (ClassDoc cls : classes)  {

               showAnnotations(cls);

          }

          return(true);

     }

     static void showAnnotations(ClassDoc cls)

     {

          System.out.println("Annotations for class [" + cls + "]");

          process(cls.annotations());

          System.out.println();

          for(MethodDoc m : cls.methods())  {

               System.out.println("Annotations for method [" + m + "]");

               process(m.annotations());

               System.out.println();

         }

    }

    static void process(AnnotationDesc[] anns)

    {

         for (AnnotationDesc ad :anns)  {

               AnnotationDesc.ElementValuePair evp[]  = ad.elementValues();

               for(AnnotationDesc.ElementValuePair e : evp)  {

                    System.out.println("   NAME:  " + e.element()  +

                                           ",  VALUE="  + e.value()) ;

               }

          }

    }

}

Start方法迭代了在源文件中发现的所有类(和接口)。因为在源代码元素上的所有注释都是与AnnotationDesc接口相关联的,所以可以编写一个方法来处理注释,而不用考虑该注释关联的是哪个代码元素。showAnnotations方法打印出同当前类相关联的所有注释,然后处理该类中的所有方法。doclet API使得处理这些源代码元素很容易。为了执行doclet,可以按照如下方式向命令行的程序传递doclet名称和类名称:

javadoc -doclet AnnotationViewer ServerCommandProcessor.java

doclet在屏幕上显示如下内容:

Loading source file ServerCommandProcessor.java...

Constructing Javadoc information...

Annotations for class  [ServerCommandProcessor]

   NAME: CodeTag.authorName(), VALUE="Dilbert"

   NAME: CodeTag.lastModificationDate(), VALUE="May 7,  2006"

Annotations for method [ServerCommandProcessor.setParams(java fang. String)]

   NAME: CodeTag.authorName(), VALUE="Dilbert"

   NAME: CodeTag.lastModificationDate(), VALUE="May 10, 2006"

   NAME: CodeTag.bugFixes(), VALUE="BUG0170"

Annotations for method [ServerCommandProcessor.executeCommand (java.lang. Strinq,

java.lang.Object[])]

要在运行时访问注释,就必须使用反射API。通过AnnotatedElement接口已经内置了这种支持,该接口是通过反射类AccessibleObject、Class、Constructor、Field、Method和Package来实现的。所有这些元素都可以有注释。AnotatedElement

分享到:
评论

相关推荐

Global site tag (gtag.js) - Google Analytics