使用现有类
在Java中,没有类就无法做任何事情。前面曾经接触过几个类,但可惜的是,其中许多并不具有Java类的典型特征。例如,Math类。在程序中,可以使用Math类的方法,如Math.random,并只需要知道方法名和参数(如果有的话),而不必了解它的具体实现过程。这正是封装的关键所在,当然所有类都是这样。但遗憾的是,Math类只封装了功能,它不需要也不必隐藏数据。由于没有数据,因此也不必担心生成对象以及初始化实例域。
在下一节中,将会给出一个更加具有代表性的类—Date类。从中可以看到如何构造对象,以及如何调用类的方法。
对象与对象变量
要想使用对象,就必须首先构造对象,并指定其初始状态。然后,对对象施加方法。
在Java程序设计语言中,使用构造器(constructor)构造新实例。构造器是一种特殊的方法,用来构造并初始化对象。下面看一个例子。在标准Java类库中包含一个Date类。它的对象将描述一个时间点,例如“December 31, 1999, 23:59:59 GMT”。
构造器的名字应该与类名相同。因此Date类的构造器名为Date。要想构造一个Date对象,需要在构造器前面加上new操作符,如下所示:
new Date( )
这个表达式构造了一个新对象。这个对象被初始化为当前的日期和时间。
如果需要的话,也可以将这个对象传递给一个方法:
System.out.println(new Date( ));
相反,也可以将一个方法应用于刚刚构造的对象上。Date类中有一个toString方法。这个方法将返回日期的字符串描述。下面的语句可以说明如何将toString方法应用于新构造的Date对象上。
String s = new Date( ).toString( );
在这两个例子中,构造的对象仅使用了一次。通常,希望构造的对象可以被多次使用,因此,需要将对象存放在一个变量中:
Date birthday = new Date( );
图4-3展示了引用新构造的对象的对象变量birthday。
在对象与对象变量之间存在着一个重要的区别。例如,语句
Date deadline; // deadline doesn’t refer to any object
定义了一个对象变量deadline,它可以引用Date类型的对象。但是,一定要认识到:变量deadline不是一个对象,实际上也没有引用对象。此时,不能将任何Date方法应用于这个变量上。语句
s = deadline.toString( ); // not yet
将产生编译错误。
必须首先初始化变量deadline。这里有两个选择。当然可以用新构造的对象初始化这个变量:
deadline = new Date( );
或者让这个变量引用一个已存在的对象:
deadline = birthday;
现在,这两个变量引用同一个对象(请参见图4-4)。
一定要认识到:一个对象变量并没有实际包含一个对象,而仅仅引用一个对象。
在Java中,任何对象变量的值都是对存储在另外一处的一个对象的引用。new操作符的返回值也是一个引用。下列语句:
Date deadline = new Date( );
有两个部分。表达式new Date( )构造了一个Date类型的对象,并且它的值是对新创建对象的引用。
这个引用被存储在变量deadline中。
可以显式地将对象变量设置为空(null),表明这个对象变量目前没有引用任何对象。
deadline = null;
. . .
if (deadline != null)
System.out.println(deadline);
如果将一个方法应用于一个值为null的对象上,就会产生运行错误。
birthday = null;
String s = birthday.toString( ); // runtime error!
变量不会自动地初始化为null,而必须对它们进行初始化,调用new或将它们设置为null。
Java库中的GregorianCalendar类
在前面的例子中,已经使用了Java标准类库中的Date类。Date类的实例有一个状态,称为特定的时间点。
尽管在使用Date类时不必知道这一点,但时间是用距离一个固定时间点的毫秒数(可正可负)表示的,这个点就是所谓的纪元(epoch),它是UTC时间1970年1月1日00:00:00。UTC是Coordinated Universal Time的缩写,与大家熟悉的GMT(即Greenwich Mean Time/格林威治时间)一样,是一种具有实际目的的科学标准时间。
但是,事实上Date类对于日期的处理并没有太大的用途。Java类库的设计者认为:像“December 31, 1999, 23:59:59”这样的日期表示法只是阳历的固有习惯。这种特定的描述法遵循了世界上大多数地区使用的Gregorian阳历表示法。但是,同一时间若采用中国的农历,或者希伯来的阴历表示就大不一样,对于火星历来说就更不可想象了。
类库设计者决定将保存时间与给时间点命名分开。所以标准Java类库分别包含了两个类:一个是用来表示时间点的Date类;另一个是用来表示大家熟悉的日历表示法的GregorianCalendar类。
事实上,GregorianCalendar类扩展了一个更加通用的Calendar类,这个类描述了日历的一般属性。
理论上,可以通过扩展Calendar类来实现中国的阴历或者是火星日历。然而,标准类库中只实现了Gregorian日历。
将时间与日历分开是一种很好的面向对象设计。通常,最好使用不同的类表示不同的概念。
Date类只提供了少量的方法用来比较两个时间点。例如before和after方法分别返回一个时间点是否早于另一个时间点,或者晚于另一个时间点。
if (today.before(birthday))
System.out.println("Still time to shop for a gift.");
GregorianCalendar类所包含的方法要比Date类多很多。特别是有几个很有用的构造器。表达式
new GregorianCalendar( )
构造一个新对象,用于表示对象构造时的日期和时间。
另外,还可以通过提供年、月、日来构造一个表示某个特定日期午夜的日历对象:
new GregorianCalendar(1999, 11, 31)
有些怪异的是,月份从0开始计数。因此11表示12月。为了清晰起见,也可以使用常量,如:
Calendar.DECEMBER。
new GregorianCalendar(1999, Calendar.DECEMBER, 31)
还可以设置时间:
new GregorianCalendar(1999, Calendar.DECEMBER, 31, 23, 59, 59)
当然,常常希望将构造的对象存储于对象变量中。
GregorianCalendar deadline = new GregorianCalendar(. . .);
GregorianCalendar类封装了实例域,这些实例域保存着设置的日期信息。不查看源代码不可能知道日期在类中的具体表达方式。当然,这一点并不重要,重要的是要知道类向外界开放的方法。
更改器方法与访问器方法
现在,可能有人会问:如何从封装在某个特定的GregorianCalendar对象内部的日期中获得当前的日、月、年呢?如果希望对这些内容做一些修改,又该怎么做呢?在联机文档或本节末尾的API注释中可以找到答案。这里只讲解本节主题中最重要的方法。
日历的作用在于提供某个时间点的日、星期、月或年等属性。要想查询这些设置信息之一,应该使用GregorianCalendar类的get方法。为了表达希望得到的项,需要借助于Calendar类中定义的一些常量,如Calendar.MONTH或Calendar.DAY_OF_WEEK:
GregorianCalendar now = new GregorianCalendar( );
int month = now.get(Calendar.MONTH);
int weekday = now.get(Calendar.DAY_OF_WEEK);
API注释列出了可以使用的全部常量。
调用set方法,可以改变对象的状态:
deadline.set(Calendar.YEAR, 2001);
deadline.set(Calendar.MONTH, Calendar.APRIL);
deadline.set(Calendar.DAY_OF_MONTH, 15);
另外,还有一种便于设置年、月、日的方法,只需要像下面这样调用一个方法:
deadline.set(2001, Calendar.APRIL, 15);
最后,还可以为给定的日历对象增加天数、星期数、月数等:
deadline.add(Calendar.MONTH, 3); // move deadline by 3 months
如果所传递的数值是一个负数,那么日期就向后移。
get方法与set和add方法在概念上是有区别的。get方法仅仅查看并返回对象的状态,而set和add方法却对对象的状态进行修改。对实例域做出修改的方法被称为更改器方法,仅访问实例域而不加修改的方法被称为访问器方法。
通常的习惯是在访问器方法名前面加上前缀get,在更改器方法前面加上前缀set。例如,在GregorianCalendar类有getTime方法和setTime方法,它们分别用来获得和设置日历对象所表达的时间点。
Date time = calendar.getTime( );
calendar.setTime(time);
这些方法在进行GregorianCalendar和Date类之间的转换时特别有用。这里有一个例子。假定希望知道年、月、日,并且希望创建一个具有这样的时间值的Date对象。由于Date类不知道如何操作日历,所以需要首先构造一个GregorianCalendar对象,然后再调用getTime方法得到一个日期对象:
GregorianCalendar calendar = new GregorianCalendar(year, month, day);
Date hireDay = calendar.getTime( );
与之相反,如果希望得到Date对象中的年、月、日信息,需要构造一个GregorianCalendar对象,设置时间,然后再调用get方法:
GregorianCalendar calendar = new GregorianCalendar( );
calendar.setTime(hireDay);
int year = calendar.get(Calendar.YEAR);
下面用一个应用GregorianCalendar类的程序来结束本节内容的阐述。这个程序将显示当前月的日历,其格式为:
Sun Mon Tue Wed Thu Fri Sat
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 31
当前的日用一个 * 号标注,并且这个程序还能够计算星期几。
下面看一下这个程序的关键步骤。首先,构造一个日历对象,并用当前的日期和时间进行初始化。(对于这个应用程序,并不必在意实际的时间。)
GregorianCalendar d = new GregorianCalendar( );
经过两次调用get方法获得了当时的日、月。
int today = d.get(Calendar.DAY_OF_MONTH);
int month = d.get(Calendar.MONTH);
然后,将d设置为这个月的第一天,并获得这一天为星期几。
d.set(Calendar.DAY_OF_MONTH, 1);
int weekday = d.get(Calendar.DAY_OF_WEEK);
如果这个月的第一天是星期日,变量weekday被设置为1(即Calendar.SUNDAY);如果这个月的第一天是星期一,则设置为2(即Calendar.MONDAY),以此类推。
随后,输出标题和空格作为日历的第一行。
如果日期小于10,则先打印一个空格,再打印日期;如果是当前日,则打印日期之后再打印一个*。每一个星期天,重新换一行。
然后,将d设置为下一天:
d.add(Calendar.DAY_OF_MONTH, 1);
什么时候结束呢?我们并不知道这个月有31天、30天、29天,还是28天。其实,只要d还指示当月,就应该继续迭代。
do
{
. . .
}
while (d.get(Calendar.MONTH) == month);
一旦d进入了下一月,程序就终止执行。
例4-1给出了完整的程序。
正如所看到的那样,日历程序设计包含很多复杂问题,例如:某一天是星期几,每个月有多少天等。有了GregorianCalendar类,一切就变得简单了。我们并不必知道GregorianCalendar类是如何计算星期数和每月天数的,而只需要使用类提供的接口:get方法、set方法以及add方法就可以了。
这个例子程序的关键在于告诉我们:可以使用类的接口来解决复杂任务,而不必了解其中的实现细节。
例4-1 CalendarTest.java
java.util.GregorianCalendar 1.1
? GregorianCalendar( )
构造一个日历对象,用来表示默认地区、默认时区的当前时间。
? GregorianCalendar(int year, int month, int day)
用指定的日期信息构造一个Gregorian日历对象。
参数:year 该日期所在的年份
month 该日期所在的月份。此值以0为基准;例如,0表示一月
day 该月份中的日期
? GregorianCalendar(int year, int month, int day, int hour, int minutes, int seconds)
用给定的日期和时间信息构造一个Gregorian日历。
参数:year 该日期所在的年份
month 该日期所在的月份。此值以0为基准;例如,0表示一月
day 该月份中的日期
hour 小时(0~23)
minutes 分钟(0~59)
seconds 秒(0~59)
? int get(int field)
返回给定域的值。
参数:field
可以是下述选项之一:Calendar.ERA、Calendar.YEAR、Calendar.MONTH、
Calendar.WEEK_OF_YEAR、Calendar.WEEK_OF_MONTH、Calendar.DAY_
OF_MONTH、Calendar.DAY_OF_YEAR、Calendar.DAY_OF_WEEK、Calendar.DAY_
OF_WEEK_IN_MONTH、Calendar.AM_PM、Calendar.HOUR、Calendar.HOUR_
OF_DAY、Calendar.MINUTE、Calendar.SECOND、Calendar.MILLISECOND、
Calendar.ZONE_OFFSET、Calendar.DST_OFFSET
? void set(int field, int value)
设置特定域的值。
参数:field get接收的常量之一
value 新值
? void set(int year, int month, int day)
将日期域设置为新日期。
参数:year 日期所在年份
month 日期所在月份。此值是以0为基准的;例如,0表示一月
day 该月份中的日期
? void set(int year, int month, int day, int hour, int minutes, int seconds)
将日期和时间域设置为新值。
参数:year 日期所在年份
month 日期所在月份。此值是以0为基准的;例如,0表示一月
day 该月份中的日期
hour 小时(0~23)
minutes 分钟(0~59)
seconds 秒(0~59)
? void add(int field, int amount)
这是一个可以对日期信息实施算术运算的方法。对给定的时间域增加指定数量的时间。例如,可以通过调用c.add(Calendar.DAY_OF_MONTH, 7),将当前的日历日期加上7。
参数:field 需要修改的域(可以使用get方法文档中给出的一个常量)
amount 域被改变的数量(可以是负值)
? void setTime(Date time)
将日历设置为指定的时间点。
参数:time 时间点
? Date getTime( )
获取这个日历对象当前值所表达的时间点。