4.2 使用预定义类
在Java中,没有类就无法做任何事情。然而,并不是所有的类都表现出面向对象的典型特征。例如,Math类,可以直接使用Math类的方法,如Math.random,而不必了解它具体是如何实现的,你只需要知道方法名和参数(如果有的话)。这正是封装的关键所在,当然所有类都是这样。但Math类只封装了功能,它不需要也不必隐藏数据。由于没有数据,因此也不必考虑创建对象和初始化它们的实例字段,因为根本没有实例字段!
对象与对象变量
构造器(constructor,或称构造函数)
要想使用对象,首先必须构造对象,并指定其初始状态。然后对对象应用方法。
在Java程序设计语言中,要使用构造器(constructor,或称构造函数)构造新实例。构造器是一种特殊的方法,用来构造并初始化对象。下面看一个例子。在标准Java库中包含一个Date类。它的对象可以描述一个时间点,例如,“December 31,1999,23:59:59 GMT”。
注释
你可能会感到奇怪:为什么用类描述时间,而不像其他语言那样用一个内置的(built-in)类型?例如,在Visual Basic中有一个内置的date类型,程序员可以采用#6/1/1995#的格式指定日期。从表面上看这似乎很方便,因为程序员只需要使用内置的date类型,而不必为设计类而操心。但实际上,Visual Basic这样设计的适应性如何呢?在有些地区,日期表示为月/日/年,而另一些地区则表示为日/月/年。语言设计者是否能够预见这些问题呢?如果没有处理好这类问题,语言就有可能陷入混乱,对此感到不满的程序员也会丧失使用这种语言的热情。如果使用类,这些设计任务就交给了类库的设计者。如果类设计得不完善,其他的程序员可以很容易地编写自己的类,以便增强或替代(replace)系统提供的类(作为印证:Java的日期类库开始时有些混乱,现在已经重新设计了两次)。
构造器的名字与类名相同
构造器的名字应该与类名相同。因此Date类的构造器名为Date。要想构造一个Date对象,需要在构造器前面加上new操作符,如下所示:
new Date()
这个表达式构造了一个新对象。这个对象被初始化为当前的日期和时间。
如果需要的话,也可以将这个对象传递给一个方法:
System.out.println(new Date());
程序示例
public class HuangZiHanTest
{
public static void main(String[] args)
{
new Date();
}
}
运行结果
Exception in thread "main" java.lang.Error: Unresolved compilation problem:
Date cannot be resolved to a type
at HuangZiHanTest.main(HuangZiHanTest.java:7)
程序示例
public class HuangZiHanTest
{
public static void main(String[] args)
{
new HuangZiHanTest();
System.out.println(new HuangZiHanTest());
}
}
运行结果
HuangZiHanTest@24d46ca6
toString方法
或者,也可以对刚刚创建的对象应用一个方法。Date类中有一个toString方法。这个方法将返回日期的字符串描述。下面的语句可以说明如何对新构造的Date对象应用toString方法。
String s=new Date().toString();
对象变量
在这两个例子中,构造的对象仅使用了一次。通常,你会希望构造的对象可以多次使用,因此,需要将对象存放在一个变量中:
Date birthday=new Date();
创建一个新对象
图(创建一个新对象)显示了对象变量birthday,它引用了新构造的对象。
程序示例
public class HuangZiHanTest
{
public static void main(String[] args)
{
new HuangZiHanTest();
System.out.println(new HuangZiHanTest());
System.out.println();
String huangzihan=new HuangZiHanTest().toString();
System.out.println(huangzihan);
System.out.println();
HuangZiHanTest Huangzihan=new HuangZiHanTest();
System.out.println(Huangzihan);
}
}
运行结果
HuangZiHanTest@24d46ca6
HuangZiHanTest@4517d9a3
HuangZiHanTest@372f7a8d
对象与对象变量
在对象与对象变量之间存在着一个重要的区别。例如,以下语句
Date deadline; //deadline doesn't refer to any object
定义了一个对象变量deadline,它可以引用Date类型的对象。但是,一定要认识到:变量deadline不是一个对象,而且实际上它也没有引用任何对象。此时还不能在这个变量上使用任何Date方法。下面的语句
s = deadline.toString(); //not yet
将产生编译错误。
程序示例
public class HuangZiHanTest
{
public static void main(String[] args)
{
HuangZiHanTest shuai_ge;
huangzihan = shuai_ge.toString();
}
}
运行结果
Exception in thread "main" java.lang.Error: Unresolved compilation problem:
huangzihan cannot be resolved to a variable
at HuangZiHanTest.main(HuangZiHanTest.java:8)
必须首先初始化变量deadline,这里有两个选择。当然,可以初始化这个变量,让它引用一个新构造的对象:
deadline = new Date();
也可以设置这个变量,让它引用一个已有的对象:
deadline = birthday;
程序示例
public class HuangZiHanTest
{
public static void main(String[] args)
{
HuangZiHanTest shuai_ge;
HuangZiHanTest Huangzihan=new HuangZiHanTest();
shuai_ge=new HuangZiHanTest();
String huangzihan = shuai_ge.toString();
System.out.println(huangzihan);
System.out.println();
System.out.println(Huangzihan);
}
}
运行结果
HuangZiHanTest@24d46ca6
HuangZiHanTest@4517d9a3
引用同一个对象的对象变量
现在,这两个变量都引用同一个对象(请参见图(引用同一个对象的对象变量))。
要认识到重要的一点:对象变量并没有实际包含一个对象,它只是引用一个对象。
在Java中,任何对象变量的值都是对存储在另外一个地方的某个对象的引用。new操作符的返回值也是一个引用。下面的语句:
Date deadline = new Date();
有两个部分。表达式
new Date()
构造了一个Date类型的对象,它的值是对新创建对象的一个引用。这个引用存储在变量deadline中。
程序示例
public class HuangZiHanTest
{
public static void main(String[] args)
{
HuangZiHanTest shuai_ge=new HuangZiHanTest();
HuangZiHanTest huangzihan=new HuangZiHanTest();
System.out.println(shuai_ge);
System.out.println(huangzihan);
}
}
运行结果
HuangZiHanTest@24d46ca6
HuangZiHanTest@4517d9a3
对象变量设置为null
可以显式地将对象变量设置为null,指示这个对象变量目前没有引用任何对象。
deadline = null;
. . .
if(deadline != null)
System.out.println(deadline);
程序示例
public class HuangZiHanTest
{
public static void main(String[] args)
{
HuangZiHanTest shuai_ge=new HuangZiHanTest();
HuangZiHanTest huangzihan=new HuangZiHanTest();
System.out.println(shuai_ge);
System.out.println(huangzihan);
System.out.println();
shuai_ge=null;
huangzihan=null;
System.out.println(shuai_ge);
System.out.println(huangzihan);
System.out.println();
if(shuai_ge==null)
{
System.out.println("黄子涵是帅哥!");
}
if(huangzihan==null)
{
System.out.println("黄子涵是帅哥!");
}
}
}
运行结果
HuangZiHanTest@24d46ca6
HuangZiHanTest@4517d9a3
null
null
黄子涵是帅哥!
黄子涵是帅哥!
Java类库中的LocalDate类
在前面的例子中,已经使用了Java标准类库中的Date类。Date类的实例有一个状态,即特定的时间点。
纪元
尽管在使用Date类时不必知道这一点,但时间是用距离一个固定时间点的毫秒数(可正可负)表示的,这个时间点就是所谓的纪元(epoch),它是UTC时间1970年1月1日00:00:00。UTC就是Coordinated Universal
Time(国际协调时间),与大家熟悉的GMT(即Greenwich Mean Time,格林尼治时间)一样,是一种实用的科学标准时间。
但是,Date类对于处理人类记录日期的日历信息并不是很有用,如“December 31,1999”。这种特定的日期描述遵循了世界上大多数地区使用的Gregorian阳历。但是,同样的这个时间点采用中国或希伯来的阴历来描述就很不一样了,倘若我们有来自火星的顾客,基于他们使用的火星历来描述这个时间点就更不一样了。
类库设计者决定将保存时间与给时间点命名分开。所以标准Java类库分别包含了两个类:一个是用来表示时间点的Date类;另一个是用大家熟悉的日历表示法表示日期的LocalDate类。
将时间度量与日历分开是一种很好的面向对象设计。通常,最好使用不同的类表示不同的概念。
静态工厂方法(factory method)
不要使用构造器来构造LocalDate类的对象。实际上,应当使用静态工厂方法(factory method),它会代表你调用构造器。下面的表达式:
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 aThousandDaysLater = newYearsEve.plusDays(1000);
year = aThousandDaysLater.getYear(); //2002
month = aThousandDaysLater.getMonthValue(); //09
day = aThousandDaysLater.getDayOfMonth(); //26
LocalDate类封装了实例字段来维护所设置的日期。如果不查看源代码,就不可能知道类内部的日期表示。当然,封装的意义就在于内部表示并不重要,重要的是类对外提供的方法。
程序示例
import java.time.LocalDate;
public class HuangZiHanTest
{
public static void main(String[] args)
{
LocalDate.now();
System.out.println(LocalDate.now());
System.out.println();
LocalDate.of(1949, 10, 01);
System.out.println(LocalDate.of(1949, 10, 01));
System.out.println();
LocalDate huangzihan_NationalDay=LocalDate.of(2021, 10, 01);
System.out.println(huangzihan_NationalDay);
System.out.println();
int huangzihan_year = huangzihan_NationalDay.getYear();
int huangzihan_month = huangzihan_NationalDay.getMonthValue();
int huangzihan_day = huangzihan_NationalDay.getDayOfMonth();
System.out.println(huangzihan_year);
System.out.println(huangzihan_month);
System.out.println(huangzihan_day);
System.out.println();
LocalDate aThousandDaysLater = huangzihan_NationalDay.plusDays(1000);
int Huangzihan_year = aThousandDaysLater.getYear();
int Huangzihan_month = aThousandDaysLater.getMonthValue();
int Huangzihan_day = aThousandDaysLater.getDayOfMonth();
System.out.println(Huangzihan_year);
System.out.println(Huangzihan_month);
System.out.println(Huangzihan_day);
System.out.println();
}
}
运行结果
2021-07-05
1949-10-01
2021-10-01
2021
10
1
2024
6
27
注释
实际上,Date类也有得到日、月、年的方法,分别是getDay、getMonth以及getYear,不过这些方法已经废弃。当类库设计者意识到某个方法最初就不该引入时,就把它标记为废弃,不鼓励使用。
类库设计者意识到应当单独提供类来处理日历,不过在此之前这些方法已经是Date类的一部分了。Java 1.1中引入较早的一组日历类时,Date方法被标为废弃不用。虽然仍然可以在程序中使用这些方法,不过如果这样做,编译时会出现警告。最好还是不要使用这些废弃的方法,因为将来的某个类库版本很有可能会将它们完全删除。
提示
JDK提供了jdeprscan工具来检查你的代码中是否使用了Java API已经废弃的特性。有关说明参见https://docs.oracle.com/javase/9/tools/jdeprscan.htm。
更改器方法与访问器方法
再来看上一节中的plusDays方法调用:
LocalDate aThousandDaysLater = newYearsEve.plusDays(1000);
这个调用之后newYearsEve会有什么变化?它会改为1000天之后的日期吗?事实上,并没有。plusDays方法会生成一个新的LocalDate对象,然后把这个新对象赋给aThousandDaysLater变量。原来的对象不做任何改动。我们说plusDays方法没有更改调用这个方法的对象。(这类似于String类的toUpperCase方法。在一个字符串上调用toUpperCase时,这个字符串仍保持不变,会返回一个将字符大写的新字符串。)
程序示例
import java.time.LocalDate;
public class HuangZiHanTest
{
public static void main(String[] args)
{
LocalDate.now();
System.out.println(LocalDate.now());
System.out.println();
LocalDate.of(1949, 10, 01);
System.out.println(LocalDate.of(1949, 10, 01));
System.out.println();
LocalDate huangzihan_NationalDay=LocalDate.of(2021, 10, 01);
System.out.println(huangzihan_NationalDay);
System.out.println();
int huangzihan_year = huangzihan_NationalDay.getYear();
int huangzihan_month = huangzihan_NationalDay.getMonthValue();
int huangzihan_day = huangzihan_NationalDay.getDayOfMonth();
System.out.println(huangzihan_year);
System.out.println(huangzihan_month);
System.out.println(huangzihan_day);
System.out.println();
LocalDate aThousandDaysLater = huangzihan_NationalDay.plusDays(1000);
int Huangzihan_year = aThousandDaysLater.getYear();
int Huangzihan_month = aThousandDaysLater.getMonthValue();
int Huangzihan_day = aThousandDaysLater.getDayOfMonth();
System.out.println(Huangzihan_year);
System.out.println(Huangzihan_month);
System.out.println(Huangzihan_day);
System.out.println(huangzihan_NationalDay);
System.out.println(aThousandDaysLater);
System.out.println();
}
}
运行结果
2021-07-05
1949-10-01
2021-10-01
2021
10
1
2024
6
27
2021-10-01
2024-06-27
GregorianCalendar
Java库的一个较早版本曾经有另一个处理日历的类,名为GregorianCalendar。可以如下为这个类表示的一个日期增加1000天:
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方法不同,GregorianCalendar.add方法是一个更改器方法(mutator method)。调用这个方法后,someDay对象的状态会改变。可以如下查看新状态:
year = someDay.get(Calendar.YEAR); //2002
month = someDay.get(Calendar.MONTH)+1; //09
day = someDay.get(Calendar.DAY_OF_MONTH); //26
正是因为这个原因,我们将变量命名为someDay而不是newYearsEve——调用这个更改器方法之后,它不再是新年前夜。
程序示例
import java.util.Calendar;
import java.util.GregorianCalendar;
public class HuangZiHanTest
{
public static void main(String[] args)
{
GregorianCalendar huangzihanDay = new GregorianCalendar(2021,07,05);
huangzihanDay.add(Calendar.DAY_OF_MONTH,1000);
int huangzihan_year = huangzihanDay.get(Calendar.YEAR);
int huangzihan_month = huangzihanDay.get(Calendar.MONTH)+1;
int huangzihan_day = huangzihanDay.get(Calendar.DAY_OF_MONTH);
System.out.println(huangzihan_year);
System.out.println(huangzihan_month);
System.out.println(huangzihan_day);
}
}
运行结果
2024
5
1
访问器方法(accessor method)
相反,只访问对象而不修改对象的方法有时称为访问器方法(accessor method)。例如,LocalDate.getYear和GregorianCalendar.get就是访问器方法。
下面用一个应用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
当前日期用一个*号标记。可以看到,这个程序需要知道如何计算某月份的天数以及一个给定日期相应是星期几。
下面看一下这个程序的关键步骤。首先构造一个对象,并用当前的日期初始化。
LocalDate date=LocalDate.now();
下面获得当前的月份和日期。
int month = date. getMonthValue();
int today = date.getDayOfMonth();
然后,将date设置为这个月的第一天,并得到这一天为星期几。
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。
注意,日历的第一行是缩进的,这样可使月份的第一天指向相应的星期几。下面的代码会打印表头和第一行的缩进:
现在我们来打印日历的主体。进入一个循环,其中date遍历一个月中的每一天。
每次迭代时,打印日期值。如果date是当前日期,这个日期则用一个 * 标记。接下来,把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天。实际上,只要date还在当月就要继续迭代。
可以看到,利用LocalDate类可以编写一个日历程序,能处理星期几以及各月天数不同等复杂问题。你并不需要知道LocalDate类如何计算月和星期几。只需要使用这个类的接口,如plusDays和getDayOfWeek等方法。
。
程序示例
运行结果
huangzihanDate=2021-07-14(当前日期)
huangzihanDate=2021-07-01(当前日期本月的第一天)
huangzihanMonth=7(当前日期的月份)
huangzihanToday=14(当前日期的号数)
huangzihanWeekDay=THURSDAY(当前日期的星期)
huangzihanValue=4(当前日期的星期值)
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