在某些情况下,一个类的对象是有限而且固定的,比如季节类,它只有4个对象;再比如行星类,目前只有8个对象。这种实例有限而且固定的类,在Java里被称为枚举类。
在早期代码中,可能会直接使用简单的静态常量来表示枚举,例如如下代码:
这种定义方法简单明了,但存在如下几个问题。
但枚举又确实有存在的意义,因此早期也可采用通过定义类的方式来实现,可以采用如下设计方式。
但通过定义类来实现枚举的代码量比较大,实现起来也比较麻烦,Java 从JDK 1.5后就增加了对枚举类的支持。
Java 5新增了一个enum关键字(它与class、interface关键字的地位相同),用以定义枚举类。正如前面看到的,枚举类是一种特殊的类,它一样可以有自己的成员变量、方法,可以实现一个或者多个接口,也可以定义自己的构造器。一个Java源文件中最多只能定义一个public访问权限的枚举类,且该Java源文件也必须和该枚举类的类名相同。
但枚举类终究不是普通类,它与普通类有如下简单区别。
枚举类默认提供了一个values()方法,该方法可以很方便地遍历所有的枚举值。
定义枚举类时,需要显式列出所有的枚举值,如上面的SPRING,SUMMER,FALL,WINTER;所示,所有的枚举值之间以英文逗号(,)隔开,枚举值列举结束后以英文分号作为结束。这些枚举值代表了该枚举类的所有可能的实例。
如果需要使用该枚举类的某个实例,则可使用EnumClass.variable的形式,如SeasonEnum.SPRING。
上面程序测试了SeasonEnum枚举类的用法,该类通过values()方法返回了SeasonEnum枚举类的所有实例,并通过循环迭代输出了SeasonEnum枚举类的所有实例。
不仅如此,上面程序的switich表达式中还使用了SeasonEnum对象作为表达式,这是JDK 1.5增加枚举后对 switch的扩展: switch 的控制表达式可以是任何枚举类型。不仅如此,当 switch控制表达式使用枚举类型时,后面case表达式中的值直接使用枚举值的名字,无须添加枚举类作为限定。
前面已经介绍过,所有的枚举类都继承了java.lang.Enum类,所以枚举类可以直接使用java.lang.Enum类中所包含的方法。java.lang.Enum类中提供了如下几个方法。
- int compareTo(E o):该方法用于与指定枚举对象比较顺序,同一个枚举实例只能与相同类型的枚举实例进行比较。如果该枚举对象位于指定枚举对象之后,则返回正整数;如果该枚举对象位于指定枚举对象之前,则返回负整数,否则返回零。
- String name():返回此枚举实例的名称,这个名称就是定义枚举类时列出的所有枚举值之一。与此方法相比,大多数程序员应该优先考虑使用toString()方法,因为 toString()方法返回更加用户友好的名称。
- int ordinal():返回枚举值在枚举类中的索引值(就是枚举值在枚举声明中的位置,第一个枚举值的索引值为零)。
- String toString():返回枚举常量的名称,与name方法相似,但 toString()方法更常用。
- public static
> T valueOf(Class enumType, String name):这是一个静态方法,用于返回指定枚举类中指定名称的枚举值。名称必须与在该枚举类中声明枚举值时所用的标识符完全匹配,不允许使用额外的空白字符。 正如前面看到的,当程序使用System.out.printIn(s)语句来打印枚举值时,实际上输出的是该枚举值的toString()方法,也就是输出该枚举值的名字。
枚举类也是一种类,只是它是一种比较特殊的类,因此它一样可以定义成员变量、方法和构造器。下面程序将定义一个Gender枚举类,该枚举类里包含了一个name实例变量。
上面程序使用Gender枚举类时与使用一个普通类没有太大的差别,差别只是产生Gender对象的方式不同,枚举类的实例只能是枚举值,而不是随意地通过new来创建枚举类对象。
正如前面提到的,Java应该把所有类设计成良好封装的类,所以不应该允许直接访问Gender类的name成员变量,而是应该通过方法来控制对name的访问(上面的不严谨)。否则可能出现很混乱的情形,例如上面程序恰好设置了g.name= "女",要是采用g.name="男",那程序就会非常混乱了,可能出现FEMALE代表男的局面。可以按如下代码来改进Gender类的设计。
上面程序把name 设置成private,从而避免其他程序直接访问该name成员变量,必须通过setName()方法来修改Gender 实例的name变量,而setName()方法就可以保证不会产生混乱。上面程序中粗体字部分保证FEMALE 枚举值的name变量只能设置为"女",而MALE枚举值的name变量则只能设置为"男"。看如下程序。
实际上这种做法依然不够好,枚举类通常应该设计成不可变类,也就是说,它的成员变量值不应该允许改变,这样会更安全,而且代码更加简洁。因此建议将枚举类的成员变量都使用private final修饰。
如果将所有的成员变量都使用了final修饰符来修饰,所以必须在构造器里为这些成员变量指定初始值(或者在定义成员变量时指定默认值,或者在初始化块中指定初始值,但这两种情况并不常见),因此应该为枚举类显式定义带参数的构造器。
一旦为枚举类显式定义了带参数的构造器,列出枚举值时就必须对应地传入参数。
从上面程序中可以看出,当为Gender枚举类创建了一个 Gender(String name)构造器之后,列出枚举值就应该采用粗体字代码来完成。也就是说,在枚举类中列出枚举值时,实际上就是调用构造器创建枚举类对象,只是这里无须使用new关键字,也无须显式调用构造器。前面列出枚举值时无须传入参数,甚至无须使用括号,仅仅是因为前面的枚举类包含无参数的构造器。
不难看出,上面程序中粗体字代码实际上等同于如下两行代码:
如果想每个枚举值的info()方法相同,就跟普通类调用接口一样;
如果需要每个枚举值在调用该方法时呈现出不同的行为方式,则可以让每个枚举值分别来实现该方法,每个枚举值提供不同的实现方式,从而让不同的枚举值调用该方法时具有不同的行为方式。在下面的Gender枚举类中,不同的枚举值对 info()方法的实现各不相同(程序清单同上)。
上面代码的粗体字部分看起来有些奇怪:当创建MALE和FEMALE两个枚举值时,后面又紧跟了一对花括号,这对花括号里包含了一个info()方法定义。如果读者还记得匿名内部类语法的话,则可能对这样的语法有点印象了,花括号部分实际上就是一个类体部分,在这种情况下,当创建MALE、FEMALE枚举值时,并不是直接创建 Gender 枚举类的实例,而是相当于创建Gender 的匿名子类的实例。因为粗体字括号部分实际上是匿名内部类的类体部分,所以这个部分的代码语法与前面介绍的匿名内部类语法大致相似,只是它依然是枚举类的匿名内部子类。
编译上面的程序,可以看到生成了Gender.class、Gender$1.class和 GenderS2.class三个文件,这样的三个class文件正好证明了上面的结论:MALE和 FEMALE 实际上是Gender 匿名子类的实例,而不是Gender类的实例。当调用MALE和 FEMALE两个枚举值的方法时,就会看到两个枚举值的方法表现不同的行为方式。
假设有一个Operation枚举类,它的4个枚举值PLUS,MINUS,TIMES, DIVIDE分别代表加、减、乘、除4种运算,该枚举类需要定义一个eval()方法来完成计算。
编译上面程序会生成5个class文件,其实Operation对应一个class 文件,它的4个匿名内部子类分别各对应一个class文件。
枚举类里定义抽象方法时不能使用abstract 关键字将枚举类定义成抽象类(因为系统自动会为它添加abstract关键字),但因为枚举类需要显式创建枚举值,而不是作为父类,所以定义每个枚举值时必须为抽象方法提供实现,否则将出现编译错误。
垃圾回收机制的特点:
对象在内存中的状态:
当一个对象在堆内存中运行时,根据它被引用变量所引用的状态,可以把它所处的状态分成如下三种。
- 可达状态:当一个对象被创建后,若有一个以上的引用变量引用它,则这个对象在程序中处于可达状态,程序可通过引用变量来调用该对象的实例变量和方法。
- 可恢复状态:如果程序中某个对象不再有任何引用变量引用它,它就进入了可恢复状态。在这种状态下,系统的垃圾回收机制准备回收该对象所占用的内存,在回收该对象之前,系统会调用所有可恢复状态对象的 finalize()方法进行资源清理。如果系统在调用finalize()方法时重新让一个引用变量引用该对象,则这个对象会再次变为可达状态;否则该对象将进入不可达状态。
- 不可达状态:当对象与所有引用变量的关联都被切断,且系统已经调用所有对象的 finalize()方法后依然没有使该对象变成可达状态,那么这个对象将永久性地失去引用,最后变成不可达状态。只有当一个对象处于不可达状态时,系统才会真正回收该对象所占有的资源。
当程序执行test方法的①代码时,代码定义了一个a变量,并让该变量指向"轻量级Java EE企业应用实战"字符串,该代码执行结束后,"轻量级Java EE 企业应用实战"字符串对象处于可达状态。
当程序执行了test方法的②代码后,代码再次创建了"疯狂Java讲义"字符串对象,并让a变量指向该对象。此时,"轻量级Java EE企业应用实战"字符串对象处于可恢复状态,而"疯狂Java讲义"字符串对象处于可达状态。
一个对象可以被一个方法的局部变量引用,也可以被其他类的类变量引用,或被其他对象的实例变量引用。当某个对象被其他类的类变量引用时,只有该类被销毁后,该对象才会进入可恢复状态;当某个对象被其他对象的实例变量引用时,只有当该对象被销毁后,该对象才会进入可恢复状态。
程序无法精确控制Java垃圾回收的时机,但依然可以强制系统进行垃圾回收—─这种强制只是通知系统进行垃圾回收,但系统是否进行垃圾回收依然不确定。大部分时候,程序强制系统垃圾回收后总会有一些效果。强制系统垃圾回收有如下两种方式。
下面程序创建了4个匿名对象,每个对象创建之后立即进入可恢复状态,等待系统回收,但直到程序退出,系统依然不会回收该资源。
编译、运行上面程序,看不到任何输出,可见直到系统退出,系统都不曾调用GcTest对象的 finalize()方法。但如果将程序修改成如下形式(程序清单同上):
finalize方法是定义在 Object类里的实例方法,方法原型为:
当finalize()方法返回后,对象消失,垃圾回收机制开始执行。方法原型中的throws Throwable表示它可以抛出任何类型的异常。
任何Java类都可以重写Object类的 finalize()方法,在该方法中清理该对象占用的资源。如果程序终止之前始终没有进行垃圾回收,则不会调用失去引用对象的finalize()方法来清理资源。垃圾回收机制何时调用对象的finalize()方法是完全透明的,只有当程序认为需要更多的额外内存时,垃圾回收机制才会进行垃圾回收。因此,完全有可能出现这样一种情形:某个失去引用的对象只占用了少量内存,而且系统没有产生严重的内存需求,因此垃圾回收机制并没有试图回收该对象所占用的资源,所以该对象的finalize()方法也不会得到调用。
finalize()方法具有如下4个特点。
- 永远不要主动调用某个对象的finalize()方法,该方法应交给垃圾回收机制调用。
- finalize()方法何时被调用,是否被调用具有不确定性,不要把 finalize()方法当成一定会被执行的方法。
- 当JVM执行可恢复对象的finalize()方法时,可能使该对象或系统中其他对象重新变成可达状态。
- 当JVM执行finalize()方法时出现异常时,垃圾回收机制不会报告异常,程序继续执行。
下面程序演示了如何在 finalize()方法里复活自身,并可通过该程序看出垃圾回收的不确定性。
上面程序中定义了一个 FinalizeTest类,重写了该类的finalize()方法,在该方法中把需要清理的可恢复对象重新赋给ft引用变量,从而让该可恢复对象重新变成可达状态。
上面程序中的main()方法创建了一个FinalizeTest类的匿名对象,因为创建后没有把这个对象赋给任何引用变量,所以该对象立即进入可恢复状态。进入可恢复状态后,系统调用①号粗体字代码通知系统进行垃圾回收,②号粗体字代码强制系统立即调用可恢复对象的finalize()方法,再次调用ft对象的info()方法。编译、运行上面程序,看到ft的info0方法被正常执行。
如果删除①行代码,取消强制垃圾回收。再次编译、运行上面程序,将会看到如图6.9所示的结果。
从图6.9所示的运行结果可以看出,如果取消①号粗体字代码,程序并没有通知系统开始执行垃圾回收(而且程序内存也没有紧张),因此系统通常不会立即进行垃圾回收,也就不会调用FinalizeTest对象的finalize()方法,这样FinalizeTest 的ft类变量将依然保持为null,这样就导致了空指针异常。
上面程序中②号代码和③号代码都用于强制垃圾回收机制调用可恢复对象的 finalize()方法,如果程序仅执行System.gc();代码,而不执行②号或③号粗体字代码——由于JVM垃圾回收机制的不确定性,JVM往往并不立即调用可恢复对象的 finalize()方法,这样FinalizeTest 的ft类变量可能依然为null,可能依然会导致空指针异常
对大部分对象而言,程序里会有一个引用变量引用该对象,这是最常见的引用方式。除此之外,java.lang.ref包下提供了3个类:SoftReference、PhantomReference和 WeakReference,它们分别代表了系统对对象的3种引用方式:软引用、虚引用和弱引用。因此,Java语言对对象的引用有如下4种方式。
1.强引用(StrongReference)
这是Java程序中最常见的引用方式。程序创建一个对象,并把这个对象赋给一个引用变量,程序通过该引用变量来操作实际的对象,前面介绍的对象和数组都采用了这种强引用的方式。当一个对象被一个或一个以上的引用变量所引用时,它处于可达状态,不可能被系统垃圾回收机制回收。
2.软引用(SoftReference)
软引用需要通过SoftReference类来实现,当一个对象只有软引用时,它有可能被垃圾回收机制回收。对于只有软引用的对象而言,当系统内存空间足够时,它不会被系统回收,程序也可使用该对象;当系统内存空间不足时,系统可能会回收它。软引用通常用于对内存敏感的程序中。
3.弱引用(WeakReference)
弱引用通过WeakReference类实现,弱引用和软引用很像,但弱引用的引用级别更低。对于只有弱引用的对象而言,当系统垃圾回收机制运行时,不管系统内存是否足够,总会回收该对象所占用的内存。当然,并不是说当一个对象只有弱引用时,它就会立即被回收——正如那些失去引用的对象一样,必须等到系统垃圾回收机制运行时才会被回收。
4.虚引用(PhantomReference)
虚引用通过 PhantomReference类实现,虚引用完全类似于没有引用。虚引用对对象本身没有太大影响,对象甚至感觉不到虚引用的存在。如果一个对象只有一个虚引用时,那么它和没有引用的效果大致相同。虚引用主要用于跟踪对象被垃圾回收的状态,虚引用不能单独使用,虚引用必须和引用队列(ReferenceQueue)联合使用。
上面三个引用类都包含了一个get()方法,用于获取被它们所引用的对象。
引用队列由java.lang.ref.ReferenceQueue类表示,它用于保存被回收后对象的引用。当联合使用软引用、弱引用和引用队列时,系统在回收被引用的对象之后,将把被回收对象对应的引用添加到关联的引用队列中。与软引用和弱引用不同的是,虚引用在对象被释放之前,将把它对应的虚引用添加到它关联的引用队列中,这使得可以在对象被回收之前采取行动。
软引用和弱引用可以单独使用,但虚引用不能单独使用,单独使用虚引用没有太大的意义。虚引用的主要作用就是跟踪对象被垃圾回收的状态,程序可以通过检查与虚引用关联的引用队列中是否已经包含了该虚引用,从而了解虚引用所引用的对象是否即将被回收。
下面程序示范了弱引用所引用的对象被系统垃圾回收过程。
当程序执行到③号粗体字代码时,由于本程序不会导致内存紧张,此时程序通常还不会回收弱引用wr所引用的对象,因此在③号代码处可以看到输出"疯狂Java 讲义"字符串。
执行到③号粗体字代码之后,程序调用了System.gc();和System.runFinalization();通知系统进行垃圾回收,如果系统立即进行垃圾回收,那么就会将弱引用wr所引用的对象回收。接下来在④号粗体字代码处将看到输出null。
下面程序与上面程序基本相似,只是使用了虚引用来引用字符串对象,虚引用无法获取它引用的对象。下面程序还将虚引用和引用队列结合使用,可以看到被虚引用所引用的对象被垃圾回收后,虚引用将被添加到引用队列中。
因为系统无法通过虚引用来获得被引用的对象,所以执行①处的输出语句时,程序将输出null(即使此时并未强制进行垃圾回收)。当程序强制垃圾回收后,只有虚引用引用的字符串对象将会被垃圾回收,当被引用的对象被回收后,对应的虚引用将被添加到关联的引用队列中,因而将在②代码处看到输出true。
必须指出:要使用这些特殊的引用类,就不能保留对对象的强引用;如果保留了对对象的强引用,就会浪费这些引用类所提供的任何好处。
由于垃圾回收的不确定性,当程序希望从软、弱引用中取出被引用对象时,可能这个被引用对象已经被释放了。如果程序需要使用那个被引用的对象,则必须重新创建该对象。这个过程可以采用两种方式完成,下面代码显示了两种方式。
一、
二、
上面两段代码采用的都是伪码,其中 recreatelt()方法用于生成一个obj对象。这两段代码都是先判断obj对象是否已经被回收,如果已经被回收,则重新创建该对象。如果弱引用引用的对象已经被垃圾回收释放了,则重新创建该对象。但第一段代码存在一定的问题:当if块执行完成后,obj还是有可能为null。因为垃圾回收的不确定性,假设系统在①和②行代码之间进行垃圾回收,则系统会再次将 wr所引用的对象回收,从而导致 obj依然为null。第二段代码则不会存在这个问题,当if块执行结束后,obj一定不为null。
就拿6.10来讲:(这里的obj就相当于图中的str)(这里的recreatelt()相当于"疯狂Java讲义")
第一种方式:
第二种方式:
在表6.3中,包访问控制符是一个特殊的修饰符,不用任何访问控制符的就是包访问控制。对于初始化块和局部成员而言,它们不能使用任何访问控制符,所以看起来像使用了包访问控制符。
strictfp 关键字的含义是FP-strict,也就是精确浮点的意思。在Java虚拟机进行浮点运算时,如果没有指定strictfp关键字,Java的编译器和运行时环境在浮点运算上不一定令人满意。一旦使用了strictfp来修饰类、接口或者方法时,那么在所修饰的范围内Java 的编译器和运行时环境会完全依照浮点规范IEEE-754来执行。因此,如果想让浮点运算更加精确,就可以使用strictfp关键字来修饰类、接口和方法。
native关键字主要用于修饰一个方法,使用native修饰的方法类似于一个抽象方法。与抽象方法不同的是,native方法通常采用C语言来实现。如果某个方法需要利用平台相关特性,或者访问系统硬件等,则可以使用native修饰该方法,再把该方法交给C去实现。一旦Java程序中包含了native方法,这个程序将失去跨平台的功能。
其他修饰符如 synchronized、transient将在后面章节中有更详细的介绍,此处不再赘述。
在表6.3列出的所有修饰符中,4个访问控制符是互斥的,最多只能出现其中之一。不仅如此,还有abstract和 final永远不能同时使用; abstract和 static不能同时修饰方法,可以同时修饰内部类;abstract和private 不能同时修饰方法,可以同时修饰内部类。private和 final同时修饰方法虽然语法是合法的,但没有太大的意义——由于private修饰的方法不可能被子类重写,因此使用final修饰没什么意义。
JAR文件的全称是Java Archive File,意思就是Java档案文件。通常JAR文件是一种压缩文件,与常见的ZIP压缩文件兼容,通常也被称为JAR包。JAR文件与ZIP文件的区别就是在JAR文件中默认包含了一个名为META-INF/MANIFEST.MF的清单文件,这个清单文件是在生成JAR文件时由系统自动创建的。
当开发了一个应用程序后,这个应用程序包含了很多类,如果需要把这个应用程序提供给别人使用,通常会将这些类文件打包成一个JAR文件,把这个JAR文件提供给别人使用。只要别人在系统的CLASSPATH环境变量中添加这个JAR文件,则Java虚拟机就可以自动在内存中解压这个JAR包,把这个JAR文件当成一个路径,在这个路径中查找所需要的类或包层次对应的路径结构。
1.创建JAR文件: jar cf test.jar -C dist/ .
该命令没有显示压缩过程,执行结果是将当前路径下的 dist路径下的全部内容生成一个test.jar 文件。如果当前目录中已经存在 test.jar 文件,那么该文件将被覆盖。
2.创建JAR文件,并显示压缩过程: jar cvf test.jar -C dist/ .该命令的结果与第1个命令相同,但是由于v参数的作用,显示出了打包过程,如下所示:
3.不使用清单文件: jar cvfM test.jar -C dist/ .
该命令的结果与第2个命令类似,其中M选项表明不生成清单文件。因此生成的 test.jar中没有包含META-INF/MANIFEST.MF文件,打包过程的信息也略有差别。
4.自定义清单文件内容:jar cvfm test.jar manifest.mf -C dist .
运行结果与第⒉个命令相似,显示信息也相同,其中 m选项指定读取用户清单文件信息。因此在生成的JAR包中清单文件META-INF/MANIFEST.MF的内容有所不同,它会在原有清单文件基础上增加MANIFEST.MF文件的内容。
当开发者向MANIFEST.MF 清单文件中增加自己的内容时,就需要借助于自己的清单文件了,清单文件只是一个普通的文本文件,使用记事本编辑即可。清单文件的内容由如下格式的多个key-value对组成。5.查看JAR包内容: jar tf test.jar
在test.jar文件已经存在的前提下,使用此命令可以查看test.jar中的内容。例如,对使用第2个命令生成的test.jar执行此命令,结果如下:
当JAR包中的文件路径和文件非常多时,直接执行该命令将无法看到包的全部内容(因为命令行窗口能显示的行数有限),此时可利用重定向将显示结果保存到文件中。例如,采用如下命令:
6.查看JAR包详细内容: jar tvf test.jar
该命令与第5个命令基本相似,但它更详细。所以除显示第5个命令中显示的内容外,还包括包内文件的详细信息。例如:
7.解压缩: jar xf test.jar
将test.jar文件解压缩到当前目录下,不显示任何信息。假设将第﹖个命令生成的 test.jar解压缩,将看到如下目录结构:
8.带提示信息解压缩: jar xvf test.jar
解压缩效果与第7个命令相同,但系统会显示解压过程的详细信息。例如:
9.更新JAR文件: jar uf test.jar Hello.class
更新test.jar中的Hello.class 文件。如果test.jar中已有Hello.class文件,则使用新的Hello.class文件替换原来的 Hello.class 文件;如果 test.jar中没有Hello.class文件,则把新的Hello.class文件添加到test.jar文件中。
10.更新时显示详细信息: jar uvf test.jar Hello.class这个命令与第9个命令相同,也用于更新test.jar文件中的 Hello.class 文件,但它会显示详细的压缩信息。例如:
11.创建多版本JAR包: jar cvf test.jar -C dist7/ . --release 9 -C dist/ .
多版本JAR包是JDK9新增的功能,它允许在同一个JAR包中包含针对多个Java版本的class文件。JDK 9为jar命令增加了一个--release选项,用于创建多版本JAR包,该选项的参数值必须大于或等于9—也就是说,只有Java 9才能支持多版本JAR包。
在使用多版本JAR包之前,可以使用javac的--release选项针对指定Java进行编译。比如命令:
上面命令代表使用Java 7的语法来编译Test.java。如果你的Test.java中使用了Java 8或Java 9的语法,程序将会编译失败。假如将针对Java 7编译的所有class文件放在dist7目录下,针对Java 9编译的所有class文件放在dist目录下。接下来可用如下命令来创建多版本JAR包:
当一个应用程序开发成功后,大致有如下三种发布方式。
把应用程序压缩成JAR包来发布是比较典型的做法,如果开发者把整个应用制作成一个可执行的JAR包交给用户,那么用户使用起来就方便了。在Windows下安装JRE时,安装文件会将*.jar文件映射成由javaw.exe打开。对于一个可执行的JAR包,用户只需要双击它就可以运行程序了,和阅读*.chm文档一样方便(*.chm 文档默认是由 hh.exe打开的)。下面介绍如何制作可执行的JAR包。
创建可执行的JAR包的关键在于:让javaw命令知道JAR包中哪个类是主类,javaw命令可以通过运行该主类来运行程序。
jar命令有一个-e选项,该选项指定JAR包中作为程序入口的主类的类名。因此,制作一个可执行的JAR包只要增加-e选项即可。例如如下命令:
上面命令把test目录下的所有文件都压缩到test.jar包中,并指定使用test.Test类(如果主类带包名,此处必须指定完整类名)作为程序的入口。
运行上面的JAR包有两种方式。
当创建JAR包时,所有的类都必须放在与包结构对应的目录结构中,就像上面-e选项指定的Test类,表明入口类为Test。因此,必须在JAR包下包含Test.class文件。