4.2 使用预定义类
在没有Java的情况下,你不能用Java做任何事情,而且你已经看到了几个类在工作。然而,并非所有这些都显示了对象方向的典型特征。以Math类为例。您已经看到,您可以使用Math.random
等Math
类的方法,而不需要知道它们是如何实现的,您只需要知道名称和参数(如果有的话)。这就是封装的要点,对于所有类来说,这都是正确的。但是Math
类只封装了功能;它既不需要也不隐藏数据。因为没有数据,所以您不必担心生成对象和初始化它们的实例字段——没有!
在下一节中,我们将看到一个更典型的类,日期类。您将看到如何构造这个类的对象和调用方法。
4.2.1 对象和对象变量
要处理对象,首先要构造它们并指定它们的初始状态。然后对对象应用方法。
在Java编程语言中,使用构造函数构造新实例。构造函数是一种特殊的方法,其目的是构造和初始化对象。让我们来看一个例子。标准Java库包含一个Date
类。它的对象描述时间点,如December
31, 1999, 23:59:59 GMT 。
注意
您可能想知道:为什么使用类来表示日期而不是(在某些语言中)内置类型?例如,VisualBasic有一个内置的日期类型,程序员可以用6/1/1995格式指定日期。从表面上看,这听起来很方便——程序员可以简单地使用内置的日期类型,而不用担心类。但实际上,VisualBasic设计有多适合?在某些地区,日期指定为月/日/年,而在其他地区指定为日/月/年。语言设计者真的有能力预见这些问题吗?如果他们做得不好,语言就会变成令人不快的混乱,但是不快乐的程序员却无能为力。通过类,设计任务被卸载到库设计器中。如果类不完美,其他程序员可以轻松地编写自己的类来增强或替换系统类。(证明了这一点:Java数据库开始有点混乱,而且已经被重新设计了两次。)
构造函数总是与类名同名。因此,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
。
图4.3 创建一个新对象
对象和对象变量之间有一个重要的区别。例如,语句
Date deadline; // deadline doesn't refer to any object
定义一个对象变量,deadline,它可以引用Date类型的对象。重要的是要认识到变量deadline
不是一个对象,事实上,甚至还没有涉及到对象。此时不能对此变量使用任何日期方法。声明
s = deadline.toString(); // not yet
将会引发一个编译时错误。
你必须首先初始化deadline
变量。你有两个选择。当然,你能初始化变量,因此它指向一个新构造的对象:
deadline = new Date();
或者你能设置变量指向一个已经存在的对象:
deadline = birthday;
现在两个变量都指向相同的对象了(见图4.4)
图4.4 对象变量指向相同的对象
意识到一个对象变量可能不会包含一个对象是很重要的。它仅仅指向一个对象。
在Java,对象变量的值是引用到一个对象,该对象存储在其它地方。new
操作符的返回值也是一个引用。一个语句,例如
Date deadline = new Date();
有两部分。表达式new Date()
让一个对象的类型为Date
,并且它的值是引用到新创建的对象。引用然后存储在deadline
变量。
你能够明确设置一个对象变量为null
,来指示它当前没有引用任何对象。
deadline = null;
. . .
if (deadline != null)
System.out.println(deadline);
我们将在第148页的4.3.6节“使用null引用”讨论null的更多细节
C++注意
一些人错误相信Java对象变量行为像C++引用。但是在C++中,没有null引用,并且引用不能被重新赋值。你应该考虑Java对象变量类似于C++中的对象指针。例如
Date birthday; // Java
等同于
Date* birthday; // C++
一旦你做了这个关联,所有东西都容易理解了。当然,一个
Date*
指针不会初始化直到你初始化它通过调用new
。这个语法在C++和Java中非常相同。Date* birthday = new Date(); // C++
如果你拷贝一个变量到另一个变量,然后两个变量都指向相同的日期——他们是指向相同的对象。Java的null引用等价于C++的NULL指针。
所有的Java对象都存活在堆上面。当一个对象包含另一个对象变量,它仅仅包含一个指向堆上对象的一个指针。
在C++中,指针会使你紧张,因为它们容易引起错误。很容易就创建了一个错误的指针,或者使内存管理变得混乱。在Java中,这些问题很容易就没有了。如果你使用一个未初始化的指针,运行时系统将生成一个运行时错误而不是制造一个随机结果。你不用担心内存管理,因为垃圾回收器会管理它。
C++做了很大努力,来支持拷贝构造函数和赋值运算符,来允许对象的实现能够自动拷贝它们自己。例如,链表的拷贝,是一个新的链表,它们有相同的内容,但是链接关系是不同的。它也可能像内建类型行为一样设计类。在Java中,你必须使用
clone
方法来得到一个对象的完整拷贝。
4.2.2 Java库中的LocalDate
类
在前面的例子中,我们使用Date
类,它是标准Java库的一部分。Date
类的实例有一个状态——特定的时间点。
尽管你不需要知道这个当你使用Date
类的时候,时间是由毫秒的数量(正数或者负数)呈现,有特殊的点,叫做epoch
,它是00:00:00 UTC, January 1, 1970 。UTC是协调世界时,特定的时间标准,它和GMT或者格林尼治时间的意义相似。
但事实证明,日期类对于处理人类用于日期的日历信息(如“December
31, 1999”)并不是很有用。对一天的这种特殊描述遵循公历,公历是世界上大多数国家使用的日历。同样的时间点在中国或希伯来的农历中会有很大的不同,更不用说火星客户使用的农历了。
注意
在整个人类历史中,文明都在努力设计历法,将名字附在日期上,并为太阳和月球周期带来秩序。有关世界各地历法的迷人解释,从法国革命历法到玛雅历法,请参阅纳鸿·德绍维茨和爱德华·M·雷因戈德的历法计算(剑桥大学出版社,2007年第3版)。
库设计者决定将保留时间和在时间点上附加名称的问题分开。因此,标准Java库包含两个单独的类:Date
类、表示时间点和LocalDate
类,它们表示熟悉的日历符号中的日期。Java 8引入了许多其他类来处理日期和时间的各个方面,参见第二卷第6章。
将时间度量与日历分离是一种很好的面向对象设计。一般来说,使用不同的类来表达不同的概念是一个好主意。
不使用构造函数来构造LocalDate类的对象。相反,使用您的行为调用构造函数的静态工厂方法。表达式
LocalDate.now()
构造一个新对象,该对象表示该对象的构造日期。
可以通过提供年、月和日来构造特定日期的对象:
LocalDate.of(1999, 12, 31)
当然,您通常希望将构造的对象存储在一个对象变量中:
LocalDate newYearsEve = LocalDate.of(1999, 12, 31);
一旦有了LocalDate对象,就可以使用getYear、getMonthValue和getDayOfMonth方法查找年份、月份和日期:
int year = newYearsEve.getYear(); // 1999
int month = newYearsEve.getMonthValue(); // 12
int day = newYearsEve.getDayOfMonth(); // 31
这可能看起来毫无意义,因为它们与您刚构造对象时使用的值非常相同。但有时,您有一个已经计算好的日期,然后您将希望调用这些方法来了解更多关于它的信息。例如,plusDays方法生成一个新的LocalDate,该LocalDate与应用它的对象相距给定的天数:
LocalDate aThousandDaysLater = newYearsEve.plusDays(1000);
year = aThousandDaysLater.getYear(); // 2002
month = aThousandDaysLater.getMonthValue(); // 09
day = aThousandDaysLater.getDayOfMonth(); // 26
LocalDate类已封装实例字段以维护其设置的日期。如果不看源代码,就不可能知道类在内部使用的表示形式。但是,当然,封装的要点是这并不重要。重要的是类公开的方法。
注意
实际上,Date类还具有获取日、月和年的方法,称为getDay、getMonth和getYear,但这些方法已被弃用。当库设计者意识到一开始就不应该引入方法时,就不推荐使用该方法。
在库设计人员意识到提供单独的类来处理日历更有意义之前,这些方法是日期类的一部分。当在Java 1.1中引入了早先的日历类集合时,日期方法被标记为弃权。您仍然可以在程序中使用它们,但是如果这样做,编译器会发出难看的警告。最好不要使用不推荐使用的方法,因为它们可能在库的未来版本中被删除。
提示
JDK提供了jdeprscan工具,用于检查代码是否使用Java API的不受欢迎的特性。有关说明,请参阅https://docs.oracle.com/javase/9/tools/jdeprscan.htm。
4.2.3 变换器和访问器方法
再看一下您在前一节中看到的plusDays方法调用:
LocalDate aThousandDaysLater = newYearsEve.plusDays(1000);
调用完之后,新年又怎么样了?一千天后又改了吗?事实证明,事实并非如此。plusDays方法生成一个新的LocalDate对象,然后将其分配给aThousandDaysLater变量。原始对象保持不变。我们说plusDays方法不会改变调用它的对象。(这类似于您在第3章中看到的String类的toUpperCase方法。当您对一个字符串调用toUpperCase时,该字符串保持不变,并返回一个包含大写字符的新字符串。)
Java库的早期版本有一个不同的类来处理日历,称为GregorianCalendar。以下是您如何在该类表示的日期中添加一千天:
GregorianCalendar someDay = new GregorianCalendar(1999, 11, 31); // odd feature of that class: month numbers go from 0 to 11
someDay.add(Calendar.DAY_OF_MONTH, 1000);
与Localdate.plusDays方法不同,GregoriCalendar.add方法是一种变异方法。在调用它之后,某天对象的状态发生了变化。以下是您了解新状态的方法:
year = someDay.get(Calendar.YEAR); // 2002
month = someDay.get(Calendar.MONTH) + 1; // 09
day = someDay.get(Calendar.DAY_OF_MONTH); // 26
这就是为什么我们称变量为someDay,而不是newYearsEve。在调用了mutator方法之后,它不再是新年前夜了。
相反,只访问对象而不修改对象的方法有时称为访问器方法。例如,LocalDate.getYear和GregorianCalendar.get是访问器方法。
C++注意
在C++中,const后缀表示访问器方法。一个未声明为const的方法被假定为一个变元。然而,在Java编程语言中,没有特殊的语法区分访问器和变异器。
我们使用一个程序来完成这一部分,该程序使用LocalDate类。程序显示当前月份的日历,如下所示:
Mon Tue Wed Thu Fri Sat Sun
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
当天用星号(*)标记。如您所见,程序需要知道如何计算一个月的长度和一天的工作日。
让我们来介绍一下这个项目的关键步骤。首先,我们构造一个用当前日期初始化的对象。
LocalDate date = LocalDate.now();
我们捕获当前的月份和日期。
int month = date.getMonthValue();
int today = date.getDayOfMonth();
然后我们将日期设置为该月的第一个,并获取该日期是星期几。
date = date.minusDays(today - 1); // set to start of month
DayOfWeek weekday = date.getDayOfWeek();
int value = weekday.getValue(); // 1 = Monday, . . . , 7 = Sunday
变量weekday设置为DayOfWeek类型的对象。我们调用该对象的getValue
方法来获取工作日的数值。这将生成一个整数,该整数遵循国际惯例,即周末在一周的最后,周一返回1,周二返回2,依此类推。星期日的值为7。
请注意,日历的第一行是缩进的,因此一个月的第一天是适当的工作日。下面是打印标题和第一行缩进的代码:
System.out.println("Mon Tue Wed Thu Fri Sat Sun");
for (int i = 1; i < value; i++)
System.out.print(" ");
现在,我们准备打印日历的正文。我们进入一个循环,其中日期遍历一个月中的几天。
在每个迭代中,我们打印日期值。如果date是今天,则日期用*标记。然后,我们把日期提前到第二天。当我们到达每个新星期的开始时,我们会打印一行新行:
while (date.getMonthValue() == month)
{
System.out.printf("%3d", date.getDayOfMonth());
if (date.getDayOfMonth() == today)
System.out.print("*");
else
System.out.print(" ");
date = date.plusDays(1);
if (date.getDayOfWeek().getValue() == 1) System.out.println();
}
我们什么时候停止?我们不知道这个月是31天、30天、29天还是28天。相反,当日期仍在当月时,我们将继续迭代。
清单4.1显示了完整的程序。
如您所见,LocalDate
类使您能够编写一个日历程序,来处理工作日和不同月份长度等复杂情况。您不需要知道LocalDate
类如何计算月份和工作日。您只需使用类的接口,比如plusDays和getDayofWeek。
这个示例程序的要点是向您展示如何使用类的接口来执行相当复杂的任务,而不必知道实现细节。
清单4.1 CalendarTest/CalendarTest.java
import java.time.*;
/**
* @version 1.5 2015-05-08
* @author Cay Horstmann
*/
public class CalendarTest
{
public static void main(String[] args)
{
LocalDate date = LocalDate.now();
int month = date.getMonthValue();
int today = date.getDayOfMonth();
date = date.minusDays(today - 1); // set to start of month
DayOfWeek weekday = date.getDayOfWeek();
int value = weekday.getValue(); // 1 = Monday, . . . , 7 = Sunday
System.out.println("Mon Tue Wed Thu Fri Sat Sun");
for (int i = 1; i < value; i++)
System.out.print(" ");
while (date.getMonthValue() == month)
{
System.out.printf("%3d", date.getDayOfMonth());
if (date.getDayOfMonth() == today)
System.out.print("*");
else
System.out.print(" ");
date = date.plusDays(1);
if (date.getDayOfWeek().getValue() == 1) System.out.println();
}
if (date.getDayOfWeek().getValue() != 1) System.out.println();
}
}
java.time.LocalDate 8
- static LocalDate now()
构造表示当前日期的对象。 - static LocalDate of(int year, int month, int day)
构造表示给定日期的对象。 - int getYear()
- int getMonthValue()
- int getDayOfMonth()
获取此日期的年、月和日。 - DayOfWeek getDayOfWeek
获取此日期的WeekDay作为DayOfWeek类的实例。调用GetValue以获取1(星期一)到7(星期日)之间的工作日。 - LocalDate plusDays(int n)
- LocalDate minusDays(int n)
生成在此日期之后或之前n天的日期。