第4章 对象与类
4.1 面向对象程序设计概述
4.1.1 类
类(class) 构造(construct)对象的过程称为创建类的实例(instance)
封装(encapsulation,有时称为数据隐藏)
实例域(instance field): 对象中的数据
方法(method)
每个特定的类实例(对象)都有一组特定的实例域值。这些值的集合就是这个对象的当前状态(state)。
实现封装的关键在于绝对不能让类中的方法直接访问其他类的实例域。程序仅通过对像的方法与对象数据进行交互。“黑盒”特征 提高重用性和可靠性。
继承(inheritance):扩展一个类来建立另外一个新类。Java所有的类来自于一个“神通广大的超类”Object。(下一章介绍Object)
4.1.2 对象
三个主要特性:
- 对象的行为(behavior)
- 对象的状态(state)
- 对象标识(identity)
4.1.3 识别类
简单规则: 分析问题的过程中寻找名词,方法对应着动词。
4.1.4 类之间的关系
- 依赖(uses-a)
- 聚合(has-a)
- 继承(is-a)
依赖(dependence,即uses-a关系) 应该尽可能地将相互依赖的类减至最少。软件工程的术语来说,让类之间的耦合度最小。
聚合(aggregation,即has-a关系)
表达关系的UML符号:
继承(inheritance,即is-a关系)
UML(Unified Modeling Language,统一建模语言)
4.2 使用预定义类
并不是所有类都具有面向对象特征,例如, Math类。Math类只封装了功能,它不需要也不必隐藏数据。由于没有数据,因此也不必担心生成对象以及初始化实例域。
下一节会给出一个更典型的类Date类,从中可以看到如何构造对象,以及如何调用类的方法。
4.2.1 对象与对象变量
构造器(constructor)
Date类,它的对象将描述一个时间点。
注释:为什么用类描述时间,而不像其他语言那样用一个内置的(built-in)类型?例如,VB中有一个内置的date类型,而不必为设计类而操心。但适应性不佳。如果使用类,这些设计任务就交给了类库的设计者。如果类设计的不完善,其他操作员可以很容易地编写自己的类,以便增强或替代(replace)系统提供的类
Date类中有一个toString方法,返回日期的字符描述。
一个对象变量并没有实际包含一个对象,仅仅引用一个对象。
在Java中,任何对象变量的值都是对存储在另外一个地方的一个对象的引用。new操作符返回值也是一个引用。
可以显式地将对象变量设置为null,表明这个对象变量目前没有引用任何对象。
局部变量不会自动地初始化为null,则必须通过调用new或将它们设置为null进行初始化。
C++注释:很多人错误地认为Java的对象变量与C++的引用类似。然而,在C++中没有空引用,并且引用不能赋值。可以将Java的对象变量看做C++的对象指针。
所有Java对象都存储在堆中。当一个对象包含另一个对象变量时,这个变量依然包含着指向另一个堆对象的指针
在C++中,指针十分让人头疼。Java语言中,如果使用一个没有初始化的指针,运行系统将会产生一个运行时错误,而不是一个随机的结果。也不用担心内存管理问题,有垃圾收集器。
C++通过拷贝型构造器和复制操作符来实现对象的自动拷贝。可以得到一个内容相同却独立的对象。在Java中必须使用clone方法获得对象的完整拷贝。
4.2.2 Java类库中GregorianCalendar类
Date类实例有一个状态,即特定的时间点。
时间是用距离一个固定时间点的毫秒数(可正可负)表示的,这个点就是纪元(epoch),它是UTC时间1970年1月1日00:00:00.
UTC是Coordinated Universal Time的缩写,与大家熟悉的GMT(即Greenwich Mean Time格林威治时间)一样,是一种具有实践意义的科学标准时间。
类设计者决定将保存时间和给时间点命名分开。所以一个是用来表示时间点的Date类;另一个是用大家熟悉日历表示法的GregorianCalendar类。事实上,GregorianCalendar类扩展了一个更加通用的Calendar类,这个类描述了日历的一般属性。标准Java库还包含泰国佛历和日本黄历的实现。
将时间与日历分开是一种很好的面向对象设计。
Date类只提供了少量方法用来比较两个时间点。例如before和after方法分别表示一个时间点是否早于另一个时间点或晚于。
注释:Date类还有getDay、getMonth以及getYear等方法,但不推荐使用。这些是在单独设计日历类之前,就是Date类的一部分了。
GregorianCalendar类有几个很有用的构造器表达式:表示对象构造时的日期和时间new GregorianCalendar()
,年月日构造特定日期午夜的日历对象new GregorianCalendar(1999,11,31)
有些怪异的是,月份从0开始计数,因此11表示十二月。可以使用常量Calendar.DECEMBER代替。
还可以设置时间new GregorianCalendar(1999,Calendar.DECEMBER,31,23,59,59)
GregorianCalendar类封装了实例域,这些实例域保存着设置的日期信息。
4.2.3 更改器方法与访问器方法
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方法,可以改变对象的状态。
还可以用add方法增加天数、星期数、月份等。给负数就前移。
get方法与set和add方法在概念上不同。get仅仅查看并返回对象状态,而set和add却对状态进行修改。对实例域进行修改的方法称为更改器方法(mutator method),仅访问实例域而不进行修改的方法称为访问器方法(accessor method)
C++注释:C++中const后缀的方法是访问器方法;默认为更改器方法。但是Java中,两者没有语法上的明显区别。
通常习惯是在访问器方法前加前缀get,在更改器方法加上前缀set。GregorianCalendar类有getTime和setTime,分别获得和设置日历对象所表示的时间点
提示:如果想看到不同地区程序的输出,应该在main方法的第一行中添加下列代码:Locale.setDefault(Locale.ITALY);
getFirstDayOfWeek方法获得当前地区星期的起始日。
DateFormatSymbols类方法实现输出表示星期几名称的前几个字母。
getShortWeekdays方法返回用户语种所命名的表示星期几的缩写字符串(例如:英语将返回“Sun”、“Mon”等)。
【API】java.util.GregorianCalendar 1.1:
GregorianCalendar()
构造一个日历对象,用来表示默认地区、默认时区的当前时间。GregorianCalendar(int year, int month, int 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.DAY_OF_MONTH、Calendar.WEEK_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_OFFSETvoid set(int field, int value)
设置特定域的值。参数:field是get接受的常量之一;value是新值。void set(int year, int month, int 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 域被改变的数量(可以是负值)int getFirstDayOfWeek()
获得当前用户所在地区,一个星期中的第一天。例如:在美国一个星期中第一天是Calendar.SUNDAY。void setTime(Date time)
将日历设置为指定的时间点。 参数:time 时间点Date getTime()
获得这个日历对象当前值所表示的时间点。
【API】java.text.DateFormatSymnbols 1.1:
String[] getShortWeekdays()
String[] getShortMonths()
String[] getWeekdays()
String[] getMonths()
获得当前地区的星期几或月份的名称。利用Calendar的星期和月份常量作为数组索引值。
4.3 用户自定义类
主力类(workhorse class)。通常这些类没有main方法,却有自己的实例域和实例方法。要像创建一个完整的程序,应该将若干类组合在一起,其中只有一个类有main方法。
4.3.1 Employee类
源文件名是EmployeeTest.java,这是因为文件名必须与public类的名字相匹配。在一个源文件中,只能有一个公有类,但可以有任意数目的非公有类。
4.3.2 多个源文件的使用
许多程序员习惯于将每一个类存在一个单独的源文件中。例如,将Employee类存放在Employee.java中,将EmployeeTest类存放在文件EmployeeTest.java中。
如果喜欢这样组织文件,将可以有两种编译源程序的方法。一种是使用通配符调用Java编译器:javac Employee*.java
于是,所以与通配符匹配的源文件都将被编译成类文件。或者键入下列命令:javac EmployeeTest.java
第二种方式,并没有显式地编译Employee.java。然而,当Java编译器发现EmployeeTest.java使用了Employee类时会查找名为Employee.class的文件。如果没有找到这个文件,就会自动地搜索Employee.java,然后,对它进行编译。更重要的是:如果Employee.java版本较已有的Employee.class文件版本新,Java编译器就会自动地重新编译这个文件。
注释:如果熟悉UNIX的“make”工具(或者是Windows中的“nmake”等工具),可以认为Java编译器内置了“make”功能。
4.3.3 剖析Employee类
注释:可以用public标记实例域,但这是一种极为不提倡的做法。public数据域允许程序中的任何方法对其进行读取和修改。这就完全破坏了封装。因此,这里强烈建议将实例域标记为private
4.3.4 从构造器开始
构造器和其他方法有一个重要的不同。构造器总是伴随着new操作符的执行被调用,而不能对一个已经存在的对象调用构造器来达到重新设置实例域的目的。
- 构造器与类同名
- 每个类可以有一个以上的构造器
- 构造器可以有0个、1个或多个参数
- 构造器没有返回值
- 构造器总是伴随着new操作一起调用
C++注释:Java构造器的工作方式与C++一样。但是,要记住所有的Java对象都是在堆中构造的,构造器总是伴随着new操作符一起使用。C++程序员最易犯的错误就是忘记new操作符
警告:请注意,不要在构造器中定义与实例域重名的局部变量。例如,下面的构造器将无法设置salary。这个构造器声明了局部变量name和salary。这些变量只能在构造器内部访问。这些变量屏蔽了同名的实例域。有些程序设计者(例如,本书的作者)常常不假思索地写出这类代码,因为它们已经习惯增加这类数据类型。这种错误很难被检查出来,因此,必须注意在所有的方法中不要命名与实例域同名的变量。
4.3.5 隐式参数与显式参数
方法用于操作对象以及存取它们的实例域。例如,方法:
public void raiseSalary(double byPercent){
double raise = salary * byPercent / 100;
salary +=raise;
}
将调用这个方法的对象的salary实例域设置为新值。看看这个调用:
number007.raiseSalary(5);
它的结果将number007.salary域的值增加5%。具体地说,这个调用将执行下列指令:
double raise = number007.salary*5/100;
number007.salary += raise;
raiseSalary 方法有两个参数。第一个参数称为**隐式(implicit)参数,是出现在方法名前的Employee类对象。第二个参数位于方法名后面括号中的数值,这是一个显式(explicit)**参数。
每一个方法中,关键字this表示隐式参数。
C++注释:在C++中,通常在类的外面定义方法。如果在类的内部定义方法,这个方法将自动地成为内联(inline)方法。在Java中,所有方法必须在类的内部定义,但并不表示它们是内联方法。是否将某个方法设置为内联方法是Java虚拟机的任务。即时编译器会监视调用那些简洁、经常被调用、没有被重载以及可优化的方法。
4.3.6 封装的优点
getName方法、getSalary方法和getHireDay方法。这些都是典型的访问器方法。由于它们只返回实例域值,因此又称为域访问器。
首先,可以改变内部实现,除了该类的方法外,不会影响其他代码。
第二,更改器方法可以执行错误检查。
警告:注意不要编写返回引用可变对象的访问器方法。这样会破坏封装性!出错的原因很微妙,d和harry.hireDay引用同一个对象后,对d调用更改器方法就可以自动地改变这个雇员对象的私有状态!
如果需要返回一个可变对象的引用,应该首先对它进行克隆(clone)。对象clone是指存放在另一个位置上的对象副本。有关对象clone的详细内容将在第6章讨论。
4.3.7 基于类的访问权限
方法可以访问所调用对象的私有数据。一个方法可以访问所属类的所有对象的私有数据,这令很多人感动奇怪!
C++注释:C++也有同样的原则。方法可以访问所属类的私有特性(feature),而不仅访问隐式参数的私有特性。
4.3.8 私有方法
4.3.9 final实例域
可以将实例域定义为final。构建对象时必须初始化这样的域。也就是说,必须确保在每一个构造器执行之后,这个域的值被设置,并且在后面的操作中,不能够再对它进行修改。例如,可以将Employee类中的name域声明为final,因为在对象构建之后,这个值不会再被修改,即没有setName方法。
final修饰符大都应用于基本(primitive)类型域,或不可变(immutable)类的域(如果类中的方法都不会改变其对象,这种类就是不可变的类。例如,String类就是一个不可变的类)。例如,private final Date hiredate;
仅仅意味着存储在hiredate变量中的对象引用在对象构造后不能被改变,而不意味着hiredate对象是一个常量。任何方法都可以对hiredate引用的对象调用setTime更改器。
4.4 静态域与静态方法
4.4.1 静态域
如果将域定义为static,每个类中只有一个这样的域。而每个对象对于所有的实例域却都有自己的一份拷贝。
注释:在绝大多数的面向对象程序设计语言中,静态域被称为类域。术语“static”只是沿用了C++的叫法,并无实际意义。
4.4.2 静态常量
一个静态常量:Math.PI
另外一个多次使用的静态常量是System.out
前面提到过,由于每个类对象都可以对公有域进行修改,所以,最好不要将域设计为public。然而,公有常量(即final域)却没问题。
注释:如果查看一下System类,就会发现有一个setOut方法,它可以将System.out设置为不同的流。原因在于,setOut是一个本地方法,而不是Java语言实现的。本地方法可以绕过Java语言的存取控制机制。这是一种特殊的方法,在自己编写程序时,不应该这样处理。
4.4.3 静态方法
静态方法是一种不能向对象实施操作的方法。例如,Math类中的pow方法就是一个静态方法。
可以认为静态方法是没有this参数的方法(在一个非静态方法中,this参数表示这个方法的隐式参数,参见4.3.5节)
因为静态方法不能操作对象,所以不能在静态方法中访问实例域。但是静态方法可以访问自身类中的静态域。下面是使用这种静态方法的一个示例:
public static int getNextId(){
return nextId;
}
可以通过类名调用这个方法:int n=Employee.getNextId();
这个方法可以省略关键字static吗?答案是肯定的。但是,需要通过Employee类对象的引用调用这个方法。
注释:可以使用对象调用静态方法。不过,这种方式很容易造成混淆,其原因是getNextId方法计算的结果与harry毫无关系。我们建议使用类名,而不是对象来调用静态方法。
在下面两种情况下使用静态方法:
- 一个方法不需要访问对象状态,其所需参数都是通过显式参数提供(例如:Math.pow)。
- 一个方法只需要访问类的静态域(例如:Employee.getNextId)
C++注释:Java中的静态域与静态方法在功能上与C++相同。但是,语法书写上却稍有所不同。在C++中,使用::操作符访问自身作用域之外的静态域和静态方法,如Math::PI。
术语“static”有一段不寻常的历史。起初,C引入关键字static是为了表示退出一个块后依然存在的局部变量。在这种情况下,术语“static”是有意义的:变量一直存在,当再次进入该块时仍然存在。随后,static在C中有了第二种含义,表示不能被其他文件访问的全局变量和函数。为了避免引入一个新的关键字,关键字static被冲用了。最后,C++第三次重用了这个关键字,与前面赋予的含义完全不一样,这里将解释为:属于类且不属于类对象的变量和函数。这个含义与Java相同。
4.4.4 工厂方法
静态方法还有一种常见的用途。NumberFormat类使用工厂方法产生不同风格的格式对象。
NumberFormat currencyFormatter = NumberFormat.getCurrencyInstance();
NumberFormat percentFormatter = NumberFormat.getPercentInstance)();
double x = 0.1;
System.out.println(currencyFormatter.format(x));
System.out.println(percentFormatter.format(x));
为什么NumberFormat类不利用构造器完成这些操作呢?这主要有两个原因:
- 无法命名构造器。构造器的名字必须与类名相同。但是,这里希望将得到的货币实例和百分比实例采用不用的名字。
- 当使用构造器时,无法改变所构造的对象类型。而Factory方法将返回一个DecimalFormat类对象,这是NumberFormat的子类(有关继承的详细内容请参看第5章)。
4.4.5 main方法
需要注意,不需要使用对象调用静态方法。
main方法不对任何对象进行操作。事实上,在启动程序时还没有任何一个对象。静态的main方法将执行并创建程序所需要的对象。
提示:每一个类可以有一个main方法。这是一个常用于对类进行单元测试的技巧。运行大型类的时候,除了它自己的main方法,下面组成部分的main方法不会执行
4.5 方法参数
按值调用(call by value)表示方法接受的是调用者提供的值。而按引用调用(call by reference)表示方法接收的是调用者提供的变量地址。一个方法可以修改传递引用所对应的变量值,而不能修改传递值调用所对应的变量值。(事实上,还有一种按名称调用(call by name),Algol程序设计语言使用的就是这种参数传递方式。不过现在,这种传递方式已经成为历史)
Java程序设计语言总是采用按值调用。也就是说,方法得到的是所有参数值的一个拷贝,特别是,方法不能修改传递给它的任何参数变量的内容。
一个方法不能修改一个基本数据类型的参数。而对象引用作为参数就不同了,可以很容易利用下面这个方法实现将一个雇员的薪金提高两倍的操作:
public static void tripleSalary(Employee x){
x.raiseSalary(200);
}
当调用
harry = new Employee(...);
tripleSalary(harry);
时,具体的执行过程为:
1) x被初始化为harry值的拷贝,这里是一个对象的引用。
2) raiseSalary方法应用于这个对象引用。x和harry同时引用的那个Employee对象的薪金提高了200%。
3) 方法结束后,参数变量x不再使用。当然,对象变量harry继续引用那个薪金增至3被的雇员对象。
读者已经看到,实现一个改变对象参数状态的方法并不是一件难事。理由很简单,方法得到的是对象引用的拷贝,对象引用及其他的拷贝同时引用同一个对象。
很多程序设计语言(特别是,C++和Pascal)提供了两种参数传递的方式:值调用和引用调用。==有些程序员(甚至本书作者)认为Java对对象采用的是引用调用,实际上,这种理解是不对的。==举个反例:
首先编写一个交换两个雇员对象的方法:
public static void swap(Employee x, Employee y){
Employee temp = x;
x=y;
y=temp;
}
但是,swap(a,b);
并没有改变存储在变量a和b中的对象引用。swap方法的参数x和y被初始化为两个对象引用的拷贝,这个方法交换的是这两个拷贝。
这个过程说明:Java程序设计语言对对象采用的不是引用调用,实际上,对象引用进行的是值传递。
- 一个方法不能修改一个基本数据类型的参数(即数值型和布尔型)。
- 一个方法可以改变一个对象参数的状态。
- 一个方法不能让对象参数引用一个新的对象。
C++注释:C++有值调用和引用调用。引用参数标有&符号。
4.6 对象构造
4.6.1 重载
重载(overloading):多个方法有相同的名字、不同的参数,便产生了重载。
重载解析(overloading resolution)
注释:方法签名(signature)——方法名以及参数类型。返回类型不是方法签名的一部分。
4.6.2 默认域初始化
构造器中没有显式给域赋予初值的话,那么就会被自动地赋予默认值:数值为0,布尔值为false,对象引用为null。
4.6.3 无参数的构造器
如果在编写一个类时没有编写构造器,那么系统就会提供一个无参数构造器。这个构造器将所有实例域设置为默认值。
如果类中提供了至少一个构造器,但是没有提供无参数的构造器,则在构造对象时如果没有提供参数就会被视为不合法。
4.6.4 显式域初始化
在类定义中,直接将一个值赋给任何域。
初始值不一定是常量。
C++注释:在C++中,不能直接初始化类的实例域。所有的域必须在构造器中设置。有一个特殊的初始化器列表语法:
Employee::Employee(String n, double s, int y, int n, int d):name(n),
salary(s),
hireDay(y,m,d)
{
}
C++使用这种特殊的语法来调用域构造器。在Java中没有这个必要,因为对象没有子对象,只有指向其他对象的指针。
4.6.5 参数名
在编写很小的构造器时,常常在参数命名上出现错误。
通常,参数用单个字符命名,但这样有个缺陷:只有阅读代码才能够了解参数n和参数s的含义。于是有些程序员在每个参数前面加一个前缀“a”,这样很清晰。
还有一种常用技巧,它基于这样的事实:参数变量用同样的名字将实例域屏蔽起来。例如,如果将参数命名为salary,salary将引用这个参数,而不是实例域。但是可以采用this.salary的形式访问实例域。回想一下,this指示隐式参数,也就是被构造的对象。示例:
public Employee(String name,double salary){
this.name=name;
this.salary=salary;
}
C++注释:在C++中,经常用下划线或某个固定字母(一般用m或x)作为实例域的前缀。例如,salary域可能被命名为_salary、mSalary或xSalary。Java程序员通常不这样做
4.6.6 调用另一个构造器
关键字this引用方法的隐式参数。然而,这个关键字还有另外一个含义。
如果构造器的第一个语句形如this(…),这个构造器将调用同一个类的另一个构造器。例子:
public Employee(double s){
this("Employee #"+nextId,s);
nextId++;
}
当调用newEmployee(60000)时,Employee(double)构造器将调用Employee(String,double) 构造器。
采用这种方式使用this关键字非常有用,这样对公共的构造器代码部分只编写一次即可。
C++注释:在Java中,this引用等价于C++的指针。但是在C++中一个构造器不能调用另一个构造器。在C++中必须抽取出公共初始化代码编写成一个独立的方法。
4.6.7 初始化块
前面浆果两种初始化数据域的方法:
- 在构造器中设置值
- 在声明中赋值
实际上,Java还有第三种机制,称为初始化块(initialization block)。在一个类的声明中可以包含多个代码块。只要构造类的对象,这些块就会被执行。例如
class Employee{
private static int nextId;
private int id;
private String name;
private double salary;
//object initialization block
{
id=nextId;
nextId++;
}
public Employee(String n, double s){
name=n;
salary=s;
}
public Employee(){
name="";
salary=0;
}
}
在这个实例中,无论使用哪个构造器构造对象,id域都在对象初始化中被初始化。首先运行初始化块,然后才运行构造器的主体部分。
这种机制不是必须的,也不常见。通常直接将初始化代码放在构造器中。
注释:即使在类后面定义,仍然可以在初始化块中设置域。但是为了避免循环定义,不要读取在后面初始化的域。建议将初始化块放在域定义之后。
由于初始化数据域有多种途径,所以列出构造过程的所有路径可能相当混乱。下面是调用构造器的具体处理步骤:
1)所有数据域被初始化为默认值(0、false或null)。
2)按照在类声明中出现的次序,依次执行所有域初始化语句和初始化块。
3)如果构造器第一行调用了第二个构造器,则执行第二个构造器主体。
4)执行这个构造器的主体。
当然,应该精心地组织好初始化代码,这样有利于其他程序员的理解。例如,如果让类构造器行为依赖于数据域声明的顺序,那就回显得奇怪并且容易引起错误。
可以通过提供一个初始化值,或者使用一个静态的初始化块来对静态域进行初始化。
如果对类的静态域进行初始化的代码比较复杂,那么可以使用静态的初始化块。将代码放在一个块中,并标记关键字static。
在类第一次加载的时候,就会进行静态域的初始化。与实例域一样,除非它们显式地被设置为其他值,否则默认的初始值是0、false或null。所有的静态初始化语句以及静态初始化块都将按照类定义的顺序执行。
注释:使用下面这种方式,可以使用Java编写一个没有main方法的“HelloWorld”程序
public class SublimeTextTest{
static{
System.out.println("Hello,World");
}
}
当用java Hello调用这个类时,这个类就会加载,静态初始化块将会打印“HelloWorld”。在此之后,会得到一个“main is not defined(没有定义)”的错误信息。不过可以在静态初始化块的尾部调用System.exit(0)避免这一缺陷。
【API】java.util.Random 1.0:
Random()
构造一个新的随机数生成器。int nextInt(int n)
1.2 返回一个0~n-1之间的随机数。
4.6.8 对象析构和finalize方法
有些面向对象的程序设计语言,特别是C++,有显式的析构器方法,其中放置一些当对象不再使用时需要执行的清理代码。在析构器中,最常见的是回收分配给对象的存储空间。由于Java有自动垃圾回收器,所以Java不支持析构器。
某些对象使用了内存之外的其他资源。在这种情况下,资源不再需要时,将其回收和再利用就非常重要。
可以为任何一个类添加finalize方法。其将在垃圾回收器清除对象前调用。不要依赖于finalize方法回收任何短缺的资源,这是因为很难知道这个方法什么时候才能够调用。
注释:有个名为System.runFinalizersOnExit(true)的方法能够确保finalize方法在Java关闭前被调用。不过,这个方法并不安全,也不鼓励大家使用。有一种代替的方法是使用方法Runtime.addShutdownHook添加“关闭钩”(shutdown hook),详细内容请参看API文档。
如果某个资源需要在使用完毕后立刻被关闭,那么就需要由人工来管理。对象用完时,可以应用一个close方法来完成相应的清理操作。11.2.4节会介绍如何确保这个方法自动得到调用。
4.7 包
Java允许使用包(package)将类组织起来。借助于包可以方便地组织自己的代码,并将自己的代码与别人提供的代码库分开管理。
标准的Java类库分布在多个包中,包括java.lang、java.util和java.net等。标准的Java包具有一个层次结构。如同硬盘的目录嵌套一样,也可以使用嵌套层次组织包。所有标准的Java包都处于Java和javax包层次中。
使用包的主要原因是确保类名的唯一性。假如两个程序员不约而同地建立了Employee类。只要将这些类放置在不同的包中,就不会产生冲突。事实上,为了保证包名的绝对唯一性,Sun公司建议将公司的因特网域名(这显然是独一无二的)以逆序的形式作为包名,并且对于不同的项目使用不同的子包。例如,horstmann.com是本书作者之一注册的域名。逆序形式为com.horstmann。
从编译器的角度来看,嵌套的包之间没有任何关系。例如,java.util包与java.util.jar包毫无关系。每一个都拥有独立的类集合。
4.7.1 类的导入
一个类可以使用所属包的所有类,以及其他包中的公有类(public class)。我们可以采用两种方式访问另一个包中的公有类。第一种方式是在每个类名前添加完整的包名。例如:
java.util.Date today = new java.util.Date();
这显然很令人生厌。更简单而更常用的方式是使用import语句。import语句是一种引用包含在包中的类的简明描述。一旦使用了import语句,在使用类时,就不必写出包的全名了。
可以使用import语句导入一个特定的类或者整个包。import语句应该位于源文件的顶部(但位于package语句的后面)。例如,可以使用下面这条语句导入java.util包中所有的类。
import java.util.*;
然后,就可以使用
Date today = new Date();
而无须在前面加上包前缀。还可以导入一个包中的特定类:
import java.util.Date;
提示:在Eclipse中,可以使用菜单选项Source->Organize Imports. Package语句,如import java.util.* 将会自动地扩展指定的导入列表。是一个十分便捷的特性。
但是,需要注意的是,只能使用星号(*)导入一个包,而不能使用import java.*或import java.*.* 导入以java为前缀的所有包。
在大多数情况下,只导入所需的包,并不必过多地理睬他们。但在发生命名冲突时,就不得不注意包的名字了。
可以采用添加一个特定的import语句来解决这个问题
如果这两个同名类都需要使用,则在每个类名前加上完整的包名。
C++注释:C++程序员经常将import与#include弄混。实际上,这两者之间并没有共同之处。在C++中,必须使用#include将外部特性的声明加载进来,这是因为C++编译器无法查看任何文件的内部,除了正在编译的文件以及在头文件中明确包含的文件。Java编译器可以查看其他文件的内部,只要告诉它到哪里去查看就可以了。
在Java中,通过显式地给出包名,就可以不使用import;而在C++中,无法避免使用#include。
import语句的唯一好处就是简捷。在C++中,与包机制类似的是命名空间(namespace)。在Java中,package与import语句类似于C++中的namespace和using指令(directive)。
4.7.2 静态导入
import还增加了导入了静态方法和静态域的功能。例如:import static java.lang.System.*;
就可以使用System类的静态方法和静态域,而不必加类名前缀。
另外,还可以导入特定的方法或域:import static java.lang.System.out;
4.7.3 将类放入包中
要想将一个类放入包中,就必须将包的名字放在源文件的开头,包中定义类的代码之前。例如:package com.horstmann.corejava;
如果没有在原文件中放置package语句,这个源文件中的类就被放置在一个**默认包(default package)**中。
需要注意,编译器对文件(带有文件分隔符和扩展名.java的文件)进行操作。而Java解释器加载类(带有.分隔符)。
提示:从下一章开始,我们将对源代码使用包。这样一来,就可以为各章建立一个IDE工程,而不是各小节分别建立工程。
警告:编译器在编译源文件的时候不检查目录结构。假如源文件没在包指定的子目录下,也可以进行编译。如果它不依赖于其他包,就不会出现编译错误。但是,最终程序将无法运行,因为虚拟机找不到类文件。
4.7.4 包作用域
变量必须显式地标记为private,不然的话将默认为包可见。这样会破坏封装性。
包密封(package sealing)机制来解决各种包混杂在一起的问题。如果将一个包密封起来,就不能再向这个包添加类了。在第10章中,将介绍制作包含密封包的JAR文件的方法。
4.8 类路径
类存储在文件系统的子目录中。类的路径必须与包名匹配。
另外,类文件也可以存储在JAR(Java归档)文件中。
提示:JAR文件使用ZIP格式组织文件和子目录。可以使用所有ZIP实用程序查看内部的rt.jar以及其他的JAR文件。
为了使类能够被多个程序共享:
1) 把类放到一个目录中。需要注意,这个目录是包树状结构的基目录。
2)将JAR文件放在一个目录中。
3)设置类路径(class path)。类路径是所有包含类文件的路径的集合。
类路径包括:
- 基目录/home/user/classdir或c:\classes;
- 当前目录(.);
- JAR文件/home/user/archives/archive.jar或c:\archives\archive.jar
从Java SE 6开始,可以在JAR文件目录中指定通配符。
在归档目录中的所有JAR文件(但不包括.classs文件)都包含在类路径中。
警告:javac编译器总是在当前的目录中查找文件,但Java虚拟机仅在类路径有“.”目录的时候才查看当前目录。如果没有设置类路径,那也不会产生什么问题,默认的类路径包含“.”目录。然而如果设置了类路径却忘记包含“.”目录,则程序仍然可以通过编译,但不能运行。
类路径所列出的目录和归档文件是搜寻类的起始点。
编译器定位文件要比虚拟机复杂得多。如果引用一个类,而没指出这个类所在的包,那么编译器将首先查找包含这个类的包,并询查所有的import指令,确定其中是否包含了被引用的类。如果找到了一个以上的类,就会产生编译错误(因为类必须是唯一的,而import语句的次序却无关紧要)。
编译器的任务不止这些,它还要查看源文件(source files)是否比类文件新。如果是这样的话,那么源文件就会自动地重新编译。在前面已经知道,仅可以导入其他包中的公有类。一个源文件只能包含一个公有类,并且文件名必须与公有类匹配。因此,编译器很容易定位公有类所在的源文件。当然,也可以从当前包中导入非公有类。这些类有可能定义在与类名不同的源文件中。如果从当前包中导入一个类,编译器就要搜索当前包中的所有源文件,以便确定哪个源文件定义了这个类。
设置类路径
最好采用-classpath(或-cp)选项指定类路径。
也可以通过设置CLASSPATH环境变量完成这个操作。
警告:有人建议将CLASSPATH环境变量设置为永久不变的值。总的来说这是一个很糟糕的主意。
警告:有人建议绕开类路径,将所有文件放在jre/lib/ext路径。这是一个极坏的主意,原因有二:当手工地加载其他的类文件时,如果将它们存放在扩展路径上,则不能正常地工作(有关类加载器的详细信息,请参看卷II第9章)。此外,程序员经常会忘记3个月前所存放文件的位置。当类加载器忽略了曾经仔细设计的类路径时,程序员会毫无头绪地在头文件中查找。事实上,加载的是扩展路径上已长时间遗忘的 类。
4.9 文档注释
JDK包含一个很有用的工具,叫做javadoc,它可以由源文件生成一个HTML文档。事实上,在第3章讲述的联机API文档就是通过对标准Java类库的源代码运行javadoc生成的。
如果在源代码中添加专用的定界符/**
开始的注释,那么可以很容易地生成一个看上去具有专业水准的文档。
4.9.1 注释的插入
javadoc实用程序(utility)从下面几个特性中抽取信息:
- 包
- 公有类与接口
- 公有的和受保护的构造器及方法
- 公有的和受保护的域
在第5章将介绍受保护特性,在第6章介绍接口。
应该为上面几部分编写注释。注释应该放置在所描述特性的前面。注释以/**
开始,并以*/
结束。
每个/** ... */
文档注释在标记之后紧跟着自由格式文本(free-form text)。标记由@开始,如@author或@param。
自由格式文本的第一句应该是一个概要性的句子。javadoc实用程序自动地将这些句子抽取出来形成概要页。
自由格式文本中,可以使用HTML修饰符,例如,用于强调的<em>...</em>
,用于设置等宽“打字机”字体的<code>...</code>
,用于着重强调的<strong>...</strong>
以及包含图像的<img ...>
等。不过,一定不要使用<h1>...</h1>
,因为它们会与文档的格式产生冲突。
注释:如果文档中有到其他文件的链接,例如,图像文件(用户界面的组件的图表或图像等),就应该将这些文件放到子目录doc-files中。javadoc实用程序将从源目录拷贝这些目录及其中的文件到文档目录中。在链接中需要使用doc-files目录,例如:<img src = "doc-file/uml.png" alt = "UML diagram">
。
4.9.2 类注释
类注释必须放在import语句之后,类定义之前。
注释:没必要在每一行开始用星号,然而大部分IDE都提供了自动添加星号,并且当注释行改变时,自动重新排列这些星号的功能。
4.9.3 方法注释
- @param 变量描述 这个标记将对当前方法的“param”(参数)部分添加一个条目。这个描述可以占据多行,并可以使用HTML标记。一个方法的所有@param标记必须放在一起。
- @return 描述 这个标记将对当前方法添加“return”(返回)部分。这个描述可以跨越多行,并可以使用HTML标记。
- @throws 类描述 这个标记添加一个注释,用于表示这个方法有可能抛出异常。有关异常的详细内容将在第11章中讨论。
4.9.4 域注释
只需要对公有域(通常指的是静态常量)建立文档。
4.9.5 通用注释
下面的标记可以用在类文档的注释中。
- @author 姓名 这个标记将产生一个“author”(作者)条目。可以使用多个@author标记,每个@author标记对应一名作者。
- @version 文本 这个标记将产生一个“version”(版本)条目。这里的文本可以是对当前版本的任何描述。下面的标记可以用于所有的文档注释中。
- @since 文本 这个标记将产生一个“since”(始于)条目。这里的text可以是对引入特性的版本描述。例如,@since verison 1.7.1。
- @deprecated 文本 这个标记将对类、方法或变量添加一个不再使用的注释。文本中给出了取代的建议。通过@see和@link标记,可以使用超级链接,链接到javadoc文档的相关部分或外部文档。
- @see 引用 这个标记将在“see also”部分增加一个超级链接。它可以用于类中,也可以用于方法中。这里的引用可以选择下列情形之一:
package .class#feature label
、<a href="...">label</a>
、"text"
第一种情况是最常见的。只要提供类、方法或变量的名字,javadoc就在文档中插入一个超链接。
4.9.6 包与概述注释
可以直接将类、方法和变量的注释放置在Java源文件中,只要用/** ... */
文档注释界定就可以了。但是要产生包注释,就需要在每一个包目录中添加一个单独的文件。有两个选择:
1)提供一个以package.html命令的HTML文件。在标记<body>...</body>
之间所有的文本都会被抽取出来。
2) 提供一个以package-info.java 命令的Java文件。这个文件必须包含一个初试的以/**
和*/
界定的Javadoc注释,跟随在一个包语句之后。它不应该包含更多的代码或注释。
还可以为所有的源文件提供一个概述性的注释。这个注释将放置在一个名叫overview.html的文件中,这个文件位于包含所有源文件的父目录中。标记<body>...</body>
之间的所有文本将被抽取出来。当用户从导航栏中选择“Overview”时,就会显示出这些注释内容。
4.9.7 注释的抽取
这里,假设HTML文件将被存放在目录docDirectory下。执行以下步骤:
1)切换到包含想要生成文档的源文件目录。如果有嵌套的包要生成文档,就必须切换到包含子目录的根目录。
2) 如果是一个包或多个包,应该运行:javadoc -d docDirectory nameOfPackage1 nameOfPackage2 ...
如果文件在默认包中,就应该运行:javadoc -d docDirectory *.java
如果省略了-d docDirectory选项,那HTML文件就会被提取到当前目录下。这样有可能带来混乱,因此不提倡这种做法。
可以使用命令行选项对javadoc程序进行调整。例如-author和-version选项在文档中包含@author 和 @version标记(默认情况下,这些标记会被省略)。另一个很有用的选项是-link,用来为标准类添加超链接。例如,使用
javadoc -link http://docs.oracle.com/javase/7/docs/api *.java
那么,所有的标准类库类都会自动地链接到Oracle网站的文档。
如果使用-linksource选项,则每个源文件被转换为HTML(不对代码着色,但包含行编号)。并且每个类和方法名将转变为指向源代码的超链接。
注释:如果需要进一步的定制,例如,生成非HTML格式的文档,可以提供自定义的doclet,以便生成想要的任何输出形式。
4.10 类设计技巧
1)一定要保证数据私有。
这是最重要的:绝对不要破坏封装性。有时候,需要编写一个访问器方法或更改器方法,但是最好还是保持实例域的私有性。
2)一定要对数据初始化。
Java不对局部变量进行初始化,但是会对对象的实例域进行初始化。最好不要依赖于系统的默认值,而是应该显式地初始化所有的数据,具体的初始化方式可以是提供默认值,也可以是在所有构造器中设置默认值。
3)不要在类中使用过多的基本类型。
就是说,用其他的类代替多个相关的基本类型的使用。这样会使类更加易于理解且易于修改。
4)不是所有的域都需要独立的域访问器和域更改器。
5)将职责过多的类进行分解。
6)类名和方法名要能够体现它们的职责。