面向对象(下)[枚举类、对象与垃圾回收]

1 枚举类

在某些情况下,一个类的对象是有限而且固定的,比如季节类,它只有4个对象;再比如行星类,目前只有9个对象。这种实例有限而且固定的类,在Java里被称为枚举类。

1.1 手动实现枚举类

  • 通过private将构造器隐藏起来。
  • 把这个类的所有可能实例都使用public static final修饰的类变量来保存。
  • 如果有必要,可以提供一些静态方法,允许其他程序根据特定参数来获取与之匹配的实例。
        public class Season
        {
            //把Season类定义成不可变的,将其Field也定义成final
            private final String name;
            private final String desc;
            public static final Season SPRING
                =new Season("春天" , "趁春踏青");
            public static final Season SUMMER
                =new Season("夏天" , "夏日炎炎");
            public static final Season FALL
                =new Season("秋天" , "秋高气爽");
            public static final Season WINTER
                =new Season("冬天" , "围炉赏雪");
            public static Season getSeason(int seasonNum)
            {
            switch(seasonNum)
            {
                  case 1 :
                        return SPRING;
                  case 2 :
                        return SUMMER;
                  case 3 :
                        return FALL;
                  case 4 :
                        return WINTER;
                  default :
                        return null;
                  }
            }
            //将构造器定义成private访问权限
            private Season(String name , String desc)
            {
                  this.name=name;
                  this.desc=desc;
            }
            //只为name和desc提供getter方法
            public String getName()
            {
                  return this.name;
            }
            public String getDesc()
            {
                  return this.desc;
            }
        }

上面的Season类是一个不可变类,在上面的Season类中包含了4个static final常量Field,这4个常量Field就代表了该类所能创建的对象。当其他程序需要使用Season对象时,既可通过如Season.SPRING的方式来取得Season对象,也可通过getSeason()静态工厂方法来获得Season对象。

在更早以前,程序员喜欢使用简单的静态常量来表示这种情况。

        public static final int SEASON_SPRING=1;
        public static final int SEASON_SUMMER=2;
        public static final int SEASON_FALL=3;
        public static final int SEASON_WINTER=4;

这种定义方法简单明了,但存在如下几个问题。

  • 类型不安全:因为上面的每个季节实际上是一个int整数,因此完全可以把一个季节当成一个int整数使用,例如进行加法运算SEASON_SPRING+SEASON_SUMMER,这样的代码完全正常。
  • 没有命名空间:当需要使用季节时,必须在SPRING前使用SEASON_前缀,否则程序可能与其他类中的静态常量混淆。
  • 打印输出的意义不明确:当我们打印输出某个季节时,例如打印SEASON_SPRING,实际上输出的是1,这个1很难猜测它代表了春天。

1.2 枚举类入门

Java 5新增了一个enum关键字(它与class、interface关键字的地位相同),用以定义枚举类。正如前面看到的,枚举类是一种特殊的类,它一样可以有自己的Field、方法,可以实现一个或者多个接口,也可以定义自己的构造器。一个Java源文件中最多只能定义一个public访问权限的枚举类,且该Java源文件也必须和该枚举类的类名相同。

枚举类终究不是普通类,它与普通类有如下简单区别

  • 枚举类可以实现一个或多个接口,使用enum定义的枚举类默认继承java.lang.Enum类,而不是继承Object类。其中java.lang.Enum类实现java.lang.Serializablejava.lang.Comparable两个接口。
  • 使用enum定义、非抽象的枚举类默认会使用final修饰,因此枚举类不能派生子类
  • 枚举类的构造器只能使用private访问控制符,如果省略了构造器的访问控制符,则默认使用private修饰;如果强制指定访问控制符,则只能指定private修饰符。
  • 枚举类的所有实例必须在枚举类的第一行显式列出,否则这个枚举类永远都不能产生实例。列出这些实例时,系统会自动添加public static final修饰,无须程序员显式添加

所有的枚举类都提供了一个values方法,该方法可以很方便地遍历所有的枚举值

前面已经介绍过,所有的枚举类都继承了java.lang.Enum类,所以枚举类可以直接使用java.lang.Enum类中所包含的方法。java.lang.Enum类中提供了如下几个方法。

  • int compareTo(Eo):该方法用于与指定枚举对象比较顺序,同一个枚举实例只能与相同类型的枚举实例进行比较。如果该枚举对象位于指定枚举对象之后,则返回正整数;如果该枚举对象位于指定枚举对象之前,则返回负整数,否则返回零。
  • Stringname():返回此枚举实例的名称,这个名称就是定义枚举类时列出的所有枚举值之一。与此方法相比,大多数程序员应该优先考虑使用toString()方法,因为toString()方法返回更加用户友好的名称。
  • int ordinal():返回枚举值在枚举类中的索引值(就是枚举值在枚举声明中的位置,第一个枚举值的索引值为零)。
  • String toString():返回枚举常量的名称,与name方法相似,但toString()方法更常用。
  • public static <T extends Enum> T valueOf(Class enumType,String
    name):这是一个静态方法,用于返回指定枚举类中指定名称的枚举值。名称必须与在该枚举类中声明枚举值时所用的标识符完全匹配,不允许使用额外的空白字符。

1.3 枚举类的Field、方法和构造器

枚举类也是一种类,只是它是一种比较特殊的类,因此它一样可以定义Field、方法。下面程序将定义一个Gender枚举类,该枚举类里包含了一个name实例变量。

        public enum Gender
        {
            MALE,FEMALE;
            //定义一个public修饰的实例变量
            public String name;
        }

上面的Gender枚举类里定义了一个名为name的实例变量,并且将它定义成一个public访问权限的。下面通过如下程序来使用该枚举类。

        public class GenderTest
        {
            public static void main(String[] args)
            {
                  //通过Enum的valueOf方法来获取指定枚举类的枚举值
                  Gender g=Enum.valueOf(Gender.class , "FEMALE");
                  //直接为枚举值的Field赋值
                  g.name="女";
                  //直接访问枚举值的Field值
                  System.out.println(g + "代表:" + g.name);
            }
        }

上面程序使用Gender枚举类时与使用一个普通类没有太大的差别,差别只是产生Gender对象的方式不同,枚举类的实例只能枚举值,而不是随意地通过new来创建枚举类对象。

正如前面提到的,Java应该把所有类设计成良好封装的类,所以不应该允许直接访问Gender类的name成员变量,而是应该通过方法来控制对name的访问。否则可能出现很混乱的情形,例如上面程序,恰好我们设置了g.name=“女”,要是采用g.name=“男”,那程序就会非常混乱了,可能出现FEMALE代表男的局面。为此我们改进了Gender类的设计。

        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;
            }
        }

实际上这种做法依然不够好,枚举类通常应该设计成不可变类,也就是说,它的Field值不应该允许改变,这样会更安全,而且代码更加简洁。为此,我们应该将枚举类的Field都使用private final修饰。

因为我们将所有的Field都使用了final修饰符来修饰,所以必须在构造器里为这些Field指定初始值(或者在定义Field时指定默认值,或者在初始化块中指定初始值,但这两种情况并不常见),因此应该为枚举类显式定义带参数的构造器

一旦为枚举类显式定义了带参数的构造器,列出枚举值时就必须对应地传入参数

        public enum Gender
        {
            //此处的枚举值必须调用对应的构造器来创建
            MALE("男"),FEMALE("女");
            private final String name;
            //枚举类的构造器只能使用private修饰
            private Gender(String name)
            {
                  this.name=name;
            }
            public String getName()
            {
                  return this.name;
            }
        }

从上面程序中可以看出,当为Gender枚举类创建了一个Gender(String name)构造器之后,列出枚举值就应该采用粗体字代码来完成。也就是说,在枚举类中列出枚举值时,实际上就是调用构造器创建枚举类对象,只是这里无须使用new关键字,也无须显式调用构造器。前面列出枚举值时无须传入参数,甚至无须使用括号,仅仅是因为前面的枚举类包含无参数的构造器。

不难看出,上面程序中粗体字代码实际上等同于如下两行代码:

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

1.4 实现接口的枚举类

        public interface GenderDesc
        {
            void info();
        }

在上面GenderDesc接口中定义了一个info方法,下面的Gender枚举类实现了该接口,并实现了该接口里包含的info方法。

        public enum Gender implements GenderDesc
        {
            //其他部分与codes\06\6.8\best\Gender.java中的Gender类完全相同
            ...
            //增加下面的info方法,实现GenderDesc接口必须实现的方法
            public void info()
            {
                  System.out.println(
                        "这是一个用于定义性别Field的枚举类");
            }
        }

如果由枚举类来实现接口里的方法,则每个枚举值在调用该方法时都有相同的行为方式(因为方法体完全一样)。如果需要每个枚举值在调用该方法时呈现出不同的行为方式,则可以让每个枚举值分别来实现该方法,每个枚举值提供不同的实现方式,从而让不同的枚举值调用该方法时具有不同的行为方式。

        public enum Gender implements GenderDesc
        {
            //此处的枚举值必须调用对应的构造器来创建
            MALE("男")
            //花括号部分实际上是一个类体部分
            {
                  public void info()
                  {
                        System.out.println("这个枚举值代表男性");
                  }
            },
            FEMALE("女")
            {
                  public void info()
                  {
                        System.out.println("这个枚举值代表女性");
                  }
            };
            //枚举类的其他部分与codes\06\6.8\best\Gender.java中的Gender类完全相同
            ...
        }

如果还记得匿名内部类语法的话,则可能对这样的语法有点印象了,花括号部分实际上就是一个类体部分,在这种情况下,当创建MALE、FEMALE枚举值时,并不是直接创建Gender枚举类的实例,而是相当于创建Gender的匿名子类的实例。因为粗体字括号部分实际上是一个匿名内部类的类体部分,所以这个部分的代码语法与前面介绍的匿名内部类语法大致相似,只是它依然是枚举类的匿名内部子类。

枚举类不是用final修饰了吗?怎么还能派生子类呢?

并不是所有的枚举类都使用了final修饰!非抽象的枚举类才默认使用final修饰。对于一个抽象的枚举类而言——只要它包含抽象方法,它就是抽象枚举类,系统会默认使用abstract修饰,而不是使用final修饰。

编译上面的程序,可以看到生成了Gender.class、Gender$1.class和Gender$2.class三个文件,这样的三个class文件正好证明了上面的结论:MALE和FEMALE实际上是Gender匿名子类的实例,而不是Gender类的实例。

1.5 包含抽象方法的枚举类

假设有一个Operation枚举类,它的4个枚举值PLUS, MINUS, TIMES, DIVIDE分别代表加、减、乘、除4种运算。

        public enum Operation
        {
            PLUS, MINUS, TIMES, DIVIDE;
            //为枚举类定义一个方法,用于实现不同的运算
            double eval(double x, double y)
            {
                  switch(this)
                  {
                        case PLUS: return x + y;
                        case MINUS: return x - y;
                        case TIMES: return x * y;
                        case DIVIDE: return x / y;
                        default: return 0;
                  }
            }
            public static void main(String[] args)
            {
                  System.out.println(Operation.PLUS.eval(3, 4));
                  System.out.println(Operation.MINUS.eval(5, 4));
                  System.out.println(Operation.TIMES.eval(5, 4));
                  System.out.println(Operation.DIVIDE.eval(5, 4));
            }
        }

仔细观察上面的Operation类不难发现,实际上PLUS,MINUS,TIMES,DIVIDE 4个值对eval方法各有不同的实现。为此,我们可以采用前面介绍的方法,让它们分别为4个枚举值提供eval的实现,然后在Operation类中定义一个eval的抽象方法。

        public enum Operation2
        {
            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(Operation2.PLUS.eval(3, 4));
                  System.out.println(Operation2.MINUS.eval(5, 4));
                  System.out.println(Operation2.TIMES.eval(5, 4));
                  System.out.println(Operation2.DIVIDE.eval(5, 4));
            }
        }

编译上面程序会生成5个class文件,其实Operation2对应一个class文件,它的4个匿名内部子类分别各对应一个class文件。

枚举类里定义抽象方法时不能使用abstract关键字将枚举类定义成抽象类(因为系统自动会为它添加abstract关键字),但因为枚举类需要显式创建枚举值,而不是作为父类,所以定义每个枚举值时必须抽象方法提供实现,否则将出现编译错误。

1 对象与垃圾回收

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

  • 垃圾回收机制只负责回收堆内存中的对象,不会回收任何物理资源(例如数据库连接、网络IO等资源)。
  • 程序无法精确控制垃圾回收的运行,垃圾回收会在合适的时候进行。当对象永久性地失去引用后,系统就会在合适的时候回收它所占的内存。
  • 在垃圾回收机制回收任何对象之前,总会先调用它的finalize()方法,该方法可能使该对象重新复活(让一个引用变量重新引用该对象),从而导致垃圾回收机制取消回收。

1.1 对象在内存中的状态

当一个对象在堆内存中运行时,根据它被引用变量所引用的状态,可以把它所处的状态分成如下三种。

  • 可达状态:当一个对象被创建后,若有一个以上的引用变量引用它,则这个对象在程序中处于可达状态,程序可通过引用变量来调用该对象的Field和方法。
  • 可恢复状态:如果程序中某个对象不再有任何引用变量引用它,它就进入了可恢复状态。在这种状态下,系统的垃圾回收机制准备回收该对象所占用的内存,在回收该对象之前,系统会调用所有可恢复状态对象的finalize()方法进行资源清理。如果系统在调用finalize()方法时重新让一个引用变量引用该对象,则这个对象会再次变为可达状态;否则该对象将进入不可达状态。
  • 不可达状态:当对象与所有引用变量的关联都被切断,且系统已经调用所有对象的finalize()方法后依然没有使该对象变成可达状态,那么这个对象将永久性地失去引用,最后变成不可达状态。只有当一个对象处于不可达状态时,系统才会真正回收该对象所占有的资源。
    在这里插入图片描述

下面程序简单地创建了两个字符串对象,并创建了一个引用变量依次指向两个对象。

        public class StatusTranfer
        {
            public static void test()
            {
                  String a=new String("轻量级Java EE企业应用实战"); //①
                  a=new String("疯狂Java讲义");   //②
            }
            public static void main(String[] args)
            {
                  test();    //③
            }
        }

当程序执行test方法的①代码时,代码定义了一个a变量,并让该变量指向"轻量级Java EE企业应用实战"字符串,该代码执行结束后,"轻量级Java EE企业应用实战"字符串对象处于可达状态。

当程序执行了test方法的②代码后,代码再次定义了"疯狂Java讲义"字符串对象,并让a变量指向该对象。此时,"轻量级Java EE企业应用实战"字符串对象处于可恢复状态,而"疯狂Java讲义"字符串对象处于可达状态。

一个对象可以被一个方法的局部变量引用,也可以被其他类的类变量引用,或被其他对象的实例变量引用。当某个对象被其他类的类变量引用时,只有该类被销毁后,该对象才会进入可恢复状态;当某个对象被其他对象的实例变量引用时,只有当该对象被销毁后,该对象才会进入可恢复状态。

2.2 强制垃圾回收

当一个对象失去引用后,系统何时调用它的finalize()方法对它进行资源清理,何时它会变成不可达状态,系统何时回收它所占有的内存,对于程序完全透明。程序只能控制一个对象何时不再被任何引用变量引用,绝不能控制它何时被回收。

程序无法精确控制Java垃圾回收的时机,但我们依然可以强制系统进行垃圾回收——这种强制只是通知系统进行垃圾回收,但系统是否进行垃圾回收依然不确定。大部分时候,程序强制系统垃圾回收后总会有一些效果。强制系统垃圾回收有如下两个方法。

  • 调用System类的gc()静态方法:System.gc()
  • 调用Runtime对象的gc()实例方法:Runtime.getRuntime().gc()
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值