有些特殊的类的对象是由固定个数的,例如四季,只有四个。这就是枚举类。这篇就来讲讲枚举类以及垃圾管理。

枚举类

手动实现枚举类

 在以前如果想用枚举类,偷懒的办法是用静态常量来表示,四季用四个数字表示,但是存在许多问题,例如类型不安全,意义不明确等。或者用类来实现,那有太麻烦,需要用private将构造器隐藏起来,把四个季节全都用public static final保存,还要提供一些静态方法使用四个季节。这样就比较麻烦。而在JDK1.5之后就有了枚举类。

枚举类入门

 enum关键字与class,interface地位相同,用于定义枚举类。(static不能修饰外部类,只有静态内部类,final可以修饰外部类,表示不能继承)enum是一种特殊的类,一样可以有自己的成员变量,方法,也可实现一个或多个接口,也可以定义自己的构造器。一个Java源文件只能由一个public的enum类。但是枚举类和普通了之间还是存在非常多的区别:

  • enum类继承的是java.lang.Enum类,不是Object类,因此不能显示的继承其他父类。
  • enum实现了java.lang.Serializable和java.lang.Comparable接口(序列化和可比较)
  • 使用enum定义非抽象的枚举类会默认使用final修饰,因此不能继承
  • 枚举类的构造器只能是private修饰
  • 枚举类的所有实例必须在枚举类的第一行显式的列出,系统会自动添加public static final
  • 枚举类默认提供一个values()方法,用于遍历枚举值
1
2
3
public enum SeasonEnum{
SPRING,SUMMER,FALL,WINTER;
}

 如果需要使用,则通过EnumClass.variable的形式,如Season.SPRING。下面使用values()方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public class EnumTest {
public void judge(SeasonEnum s) {
switch (s) {
case SPRING:
System.out.println("春天");
break;
case SUMMER:
System.out.println("夏天");
break;
case FALL:
System.out.println("秋天");
break;
case WINTER:
System.out.println("冬天");
}
}

public static void main(String[] args) {
for (SeasonEnum s : SeasonEnum.values()) {
System.out.println(s);
}
new EnumTest().judge(SeasonEnum.SPRING);
}
}

 foreach循环中可以接受数组和集合,经过调试,values()方法返回的是数组。

返回值
 在上面的代码中,switch控制表达式可以是任意枚举类,而且可以直接用枚举值作为case的值。

 由于所有的枚举类继承了java.lang.Enum类,所以可以直接使用改类中的方法:

  • int compareTo(E o):与参数比较,若该枚举实例在参数前则返回负整数;若该枚举实例在参数后则返回正整数,否则为0。要求参数必须是与该枚举实例相同的的枚举类实例。
  • String name():返回枚举实例的名称,名称就是定义枚举类时所列出的所有枚举值之一。
  • String toString():与上一个方法类似,优先使用该方法。
  • int ordinal():返回枚举值的索引值

枚举值的成员变量、方法和构造器

 枚举值也是一个类,所以可以有自己的成员变量,方法和构造器。

1
2
3
4
public enum Gender{
MALE,FEMALE;
public String name;
}
1
2
3
4
5
6
7
public class GenderTest{
public static void main(String[] args){
Gender g = Enum.valueOf(Gender.class , "FEMALE");
g.name = "女";
System.out.println(g + "代表:" + g.name);
}
}

Enum.valueOf(Gender.class , "FEMALE")这句的意思是将指定名称(“FEMALE”)的指定枚举类型的枚举常量返回给Gender g。简而言之,有“FEMALE”枚举值的这个枚举类实例给了g。但是这里的代码写的很不好,g.name = "女"直接对对象的成员变量修改是不好的,可能出现FEMALE代表了男的情况。应该将name改为private的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
public enum Gender {
MALE, FEMALE;
private String name;

public void setName(String name) {
switch (this) {
case MALE:
if (name.equals("男")){
this.name = name;
} else {
System.out.println("参数错误");
return;
}
break;
case FEMALE:
if (name.equals("女")){
this.name = name;
} else {
System.out.println("参数错误");
return;
}
break;
}
}
public String getName(){
return this.name;
}
}

 这样就避免了直接对name操作,也避免了将“男”传给FEMALE这种事。但是这样的代码还是存在问题,枚举类通常应该设计成不可变类,其成员变量值不应该允许改变,因此最好用private final修饰成员变量。联系以前的知识,如果变量都用final修饰,则应在构造器中指定初始值,或者定义成员变量时指定初始值,或者在初始化块中指定初始值。

1
2
3
4
5
6
7
8
9
10
public enum Gender{
MALE("男"),FEMALE("女");
private final String name;
private Gender(String name){
this.name = name;
}
public String getName(){
return this.name;
}
}

 这段代码相当于下面这段:

1
2
public static final Gender MALE = new Gender("男");
public static final Gender FEMALE = new Gender("女");

实现接口的枚举类

 与普通方法类似,枚举类实现一个或多个接口时,也需要实现该接口所包含的方法。

1
2
3
public interface GenderDesc{
void info();
}
1
2
3
4
5
6
7
8
9
10
11
12
13
public enum Gender implements GenderDesc{
MALE("男"),FEMALE("女");
private final String name;
private Gender(String name){
this.name = name;
}
public String getName(){
return this.name;
}
public void info(){
System.out.println("这是一个定义性别的枚举类");
}
}

 但是这样子写有个问题,“男”和“女”的类体是一样的,不论哪个调用info()方法结果都是一样的。因为它们具有同一个方法题,所以需要拆分开。

1
2
3
4
5
6
7
8
9
10
11
12
public enum Gender implements GenderDesc{
MALE("男"){
public void info(){
System.out.println("这个枚举值代表男性");
}
},
FEMALE("女"){
public void info(){
System.out.println("这个枚举值代表女性");
}
};
}

 这个代码和匿名内部类语法类似,花括号实际就是类体的一部分。编译上面的程序会产生Gender.class,Gender$1.class,Gender$2.class三个文件,MALE和FEMALE实际是Gender匿名子类的实例,不是Gender类的实例。

包含抽象方法的枚举类

 假设有个Operation枚举类,有四个枚举值分别代表加,减,乘,除,再定义一个eval方法完成计算。但是不同的枚举值会有不同的操作,所以可以将eval定义为抽象方法,让四个枚举值分别为eval提供不同的实现。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
public enum Operation {
PLUS {
public double eval(double x, double y) {
return x + y;
}
},
MINUS {
public double eval(double x, double y) {
return x - y;
}
},
TIMES {
public double eval(double x, double y) {
return x * y;
}
},
DIVIDE {
public double eval(double x, double y) {
return x / y;
}
};

public abstract double eval(double x,double y);
public static void main(String[] args) {
System.out.println(Operation.PLUS.eval(3,4));
System.out.println(Operation.MINUS.eval(3,4));
System.out.println(Operation.TIMES.eval(3,4));
System.out.println(Operation.DIVIDE.eval(3,4));
}
}

 编译上面的代码会产生5个class文件。枚举类里定义抽象方法时不能用abstract将枚举类定义为抽象类,因为系统会自动添加。同时,每个枚举值都要为抽象方法提供实现。

 对于理解枚举类,最关键的就是就是将第一行(到’;’为止)的枚举值先剔除不看,把枚举值作为枚举类的实例,当作一个普通类,再来理解上面的代码。

对象与垃圾回收

 当程序创建对象,数组等引用实体类型时,系统都会在堆内存分配一块内存区,对象就保存在这里,当这块内存不再有任何引用变量引用时,这块内存就变为垃圾,等待垃圾回收机制回收,垃圾回收机制具有如下机制:

  • 垃圾回收机制只负责回收堆内存中的对象
  • 程序无法精确控制垃圾回收的运行
  • 垃圾回收机制回收任何对象之前,总会先调用它的finalize()方法,该方法可能使该对象重新复活(让一个引用变量重新引用该对象),从而导致垃圾回收机制取消回收

对象在内存中的状态

 根据一个对象被引用变量所引用的状态,可以把它所处的状态分为三个状态:

  • 可达状态:当一个对象被创建后,若有一个以上的引用变量引用它,则这个对象处于可达状态,程序可以通过引用变量来使用该对象的实例变量和方法
  • 可恢复状态:如果某个对象不再有引用变量引用它,它就进入了可恢复状态,垃圾回收机制会准备回收内存,在回收之前会先调用所有finalize()方法进行资源清理,如果finalize()让一个引用变量引用该对象,则该对象重新进入可达状态,否则进入不可达状态
  • 不可达状态:当finalize()被调用后仍然没有使对象恢复为可达状态,这是就是不可达状态,系统就会回收对象所占用的资源

强制垃圾回收

 当一个对象失去引用后,系统何时调用finalize()方法,何时回收资源是不可控的,能控制的只有对某个对象不再引用。虽然存在强制回收的方式:调用System类的gc()静态方法System.gc()和Buntime对象的gc()实例方法Runtime.getRuntime.gc()。但是这种方式只是通知系统回收,系统是否进行垃圾回收仍然是不可确定的。

1
2
3
4
5
6
7
8
9
10
public class Gctest{
public static void main(String[] args) {
for (int i = 0; i < 4; i++) {
new Gctest();
}
}
public void finalize(){
System.out.println("系统正在清理");
}
}

 这里重写了finalize()方法,如果说进行了垃圾回收,那么就会调用到重写的方法,但是什么都没出现,那么代表没有进行回收。

1
2
3
4
5
6
7
8
9
10
11
public class Gctest{
public static void main(String[] args) {
for (int i = 0; i < 4; i++) {
new Gctest();
Runtime.getRuntime().gc();
}
}
public void finalize(){
System.out.println("系统正在清理");
}
}

 这样子后发现就进行了回收。而且当在Eclipse的Run As>Run Configurations>Arguments>VM arguments中添加参数-verbose:gc-XX:+PrintGC可以看到垃圾回收的简略信息,而参数-XX:+PrintGCDetails可以看到详细信息。

finalize方法

 finalize()方法的原型为protected void finalize() throws Throwable,是定义在Object类中的实例方法,具有如下特点:

  • 永远不要主动调用某个对象的finalize方法,而应交给垃圾回收机制
  • 该方法何时被调用,是否被调用都是不确定的
  • 当该方法遇到异常时,垃圾回收机制不会报告任何异常,程序继续执行

 由于该方法不一定会被执行,所以想清理类里的资源不能用这个方法,有专门的方法做这个。

对象的软,弱和虚引用

 对于大部分对象,程序里都会有引用变量引用到该对象。除此之外,java.lang.ref包还提供了三个类:SoftReference,PhantomReference,WeakReference。四种对应了强引用,软引用,虚引用,弱引用。

强引用(StrongReference)

 最常见的引用方式,当一个对象有一个或多个引用变量引用到它时,这就是强引用,是不可能被回收的。

软引用(SoftReference)

 软引用需要通过SoftReference类实现,当一个对象只有软引用时,有可能被回收,特别是当内存空间不足时。

弱引用(WeakReference)

 弱引用需要通过WeakReference类实现比软引用更低的引用,对于只有弱引用的对象,系统总是会回收,不管内存够不够。但是不是立刻回收,具体什么时候还是要看垃圾回收机制。

虚引用(PhantomReference)

 虚引用通过PhantomReference类实现,虚引用完全类似没有引用,虚引用主要用于跟踪垃圾回收状态,虚引用不能单独使用,必须和引用队列(ReferenceQueue)联合使用。


 三个引用类都包含了一个get()方法,用于获取被它们引用的对象。

 引用队列由java.lang.ref.ReferenceQueue类表示,它用于保存被回收后的对象的引用。当联合使用软引用,弱引用和引用队列时,系统在回收被引用的对象后,将把被回收对象的引用添加到关联的引用队列。而对于虚引用,它实在对象被释放之前就被添加到关联的引用队列中,这使得可以在对象被回收之前采取行动。软引用和弱引用可以单独使用,但是虚引用不能。单独使用虚引用没什么意义,虚引用的作用就是跟踪对象被垃圾回收的状态,程序可以通过检查与虚引用关联的引用队列中是否已经包含了该虚引用,从而了解虚引用所引用的对象是否即将被回收。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import java.lang.ref.WeakReference;

public class ReferenceTest {
public static void main(String[] args) throws Exception {
String str = new String("算法导论");
WeakReference wr = new WeakReference(str);
str = null;
System.out.println(wr.get());
System.gc();
System.runFinalization();
System.out.println(wr.get());
}

}

 上面的代码创建了一个字符串,str强引用到字符串,wr弱引用到字符串,然后将str与对象的引用切断。结果会输出字符串和null。不要使用String str="算法导论";的方式,这样产生的字符串将有常量池管理,系统不会回收这个字符串直接量。

 如果要使用这种由软引用或弱引用引用的对象,在使用get()方法后要注意是否成功取出了对象,因为有可能已近被释放了。

使用jar文件

 jar类似于压缩文件,与zip文件兼容,但是多了一个META-INF/MANIFEST.MF的清单文件。当把jar文件给别人使用时,只用把jar的位置添加到CLASSPATH中,JVM就会自动在内存中解压这个包,把这个包作为CLASSPATH的一部分,可以直接调用里面的文件。

jar命令详解

 生成jar的工具是由bin目录下的jar.exe工具来做,会用到lib目录下的tools.jar,这个通常会自动加载。

创建JAR文件

jar cf test.jar test表示将当前文件夹下的test文件夹中所有内容生成test.jar。如果加上参数v:cvf,就会显式详细压缩过程。

不添加清单文件

jar cvfM test.jar test这样写表示不生成清单文件。

自定义清单文件内容

jar cvfm test.jar manifest.mf test注意manifest.mf文件只是意味着需要一个清单文件,不是说非要这个名字,也不是非要这个格式。虽然格式和名字不受限制,但是内容的格式有限制,依照key:<空格>value,有如下要求:

  • 每行只能写一个key-value对,且必须顶头写
  • 文件开头不能有空行
  • 文件以一个空行结尾

 保存在当前文件夹,名字随意,用txt格式。假如添加了如下内容,且保存为a.txt。

1
2
Manifest-Version: 1.0txt
Created-By: 9.0.4 (Oracle Corporation)

 运行命令jar cvfm test.jar a.txt test,打开jar文件,test.jar>META-INF>MANIFEST.MF,里面写的就是上面的内容。

其他命令

  • jar tf test.jar,查看jar包中的内容。若内容过多,可用jar tf test.jar > b.txt将结果重定向至b.txt中
  • jar tvf test.jar,查看详细内容,包括大小,时间等
  • jar xf test.jar,解压test.jar到当前目录
  • jar xvf test.jar,带提示信息解压
  • jar uf test.jar Hello.class,将Hello.class添加到包中,.java文件也可,都不会添加到test文件夹中,而是在上一层级
  • jar uvf test.jar Hello.class,带提示信息添加

创建可执行的jar文件

jar cvfe test.jar test.Test test,用-e参数指定主类,要注意在jar文件中的test文件夹下有Test这个竹类,且要是.class文件。前面,所有的添加操作都是如此,一定要把.java和.class文件都包含进去。

 运行这个jar包的命令是java -jar test.jarjavaw test.jar