Chap4对象与类
预定义类
对象与对象变量
构造器是一种特殊的方法,用来构造类的一个实例,
构造器名与类名是一致的,
如new Date()
就构造出Date类的一个实例,也就是一个对象,Date deadline = new Date();
中,deadline
是一个对象变量而不是对象,它仅仅起一个引用的作用
因此Date deadline;
的deadline是不能被直接使用的
在Java中,对象都是存储在堆中的
GregorianCalendar
类
Date
类用来保存时间点,GregorianCalendar
类用来给时间点命名
这使得中国阴历、希伯来阴历、泰国佛历等历法得以实现
new GregorianCalendar() //构造日历类的一个实例
new GregorianCalendar(1999, 11, 31) //还可以给出时间,但是月份是从0记起的,也就是说这里表示的是1999-12-31而不是11月
new GregorianCalendar(1999, Calendar.DECEMBER, 31) //直接用常量来表示更为直观
new GregorianCalendar(1999, Calendar.DECEMBER, 31, 23, 59, 59) //还可以给出具体的时间点
更改器方法与访问器方法
更改器方法(mutator method):对实例域作出修改的方法
访问器方法(accessor method):仅访问实例域而不进行修改的方法
如GregorianCalendar
类中的get
方法为访问器方法,set
、add
方法为更改器方法
GregorianCalendar now = new GregorianCalendar(); //构造实例
int month = now.get(Calendar.MONTH); //用get方法访问now实例的月份
now.set(Calendar.DAY_OF_MONTH, 15); //用set方法修改now实例的日
now.set(1999, 11, 31); //也可以直接设置年月日甚至时分秒
now.add(Calendar.MONTH, 3); //用add方法修改now实例的月份使之增加三个月
GregorianCalendar
类的其他方法:
int getFirstDayOfWeek() //获取当前用户所在地区的一星期中的第一天,如Calendar.SUNDAY
Date getTime() //获取当前值所表达的时间点
日期格式规范的几个方法
/*java.text.DateFormatSymbols*/
String[] getShortWeekdays()
String[] getShortMonths()
String[] getWeekdays()
String[] getMonths()
//分别获取当前地区的星期几、月份的缩写和完整名,分别以Calendar的星期和月份常量作为数组索引值
用户自定义类
主力类(workhorse class)没有main方法
一个程序中只能有一个类有main方法,这个类必须是共有类(public),而且这个类的类名必须与.java
文件名相同
编译时,每个类都会单独生成.class
文件
运行程序时,只需要指向包含main方法的类名
定义示例
类的定义包含实例域、构造器、方法(访问器方法、更改器方法)
class Employee
{
//instance fieds实例域
private String name;
private double salary;
private Date hireDay;
/*
*私有变量,只有自身能够使用,外部无法访问
*注意:为了保证类的“封装”性,应尽量使实例域私有化,外部只能通过访问器方法来访问
*/
...
//constructor构造器
public Employee(String n, double s, int year, int month, int day) //构造器与类名相同
{
name = n;
salary = s;
GregorianCalendar calendar = new GregorianCalendar(year, month - 1, day);
hireDay = calendar.getTime();
}
/*
*当一个实例被创建时,会自动运行构造器
*每个类可以有一个以上的构造器
*每个构造器可以带任意数量的参数
*构造器没有返回值
*构造器总是伴随new操作符使用,不能用来对一个实例进行重新设置
*注意不要在构造器中定义与实例域重名的变量
*/
...
//methods方法(访问器方法)
public String getName()
{
return name;
}
public double getSalary()
{
return salary;
}
public Date getHireDay()
{
return hireDay.clone(); //返回引用可变对象的一个副本,防止外部直接修改对象的设置,以保证封装性
}
...
//methods方法(更改器方法)
public void raiseSalary(double byPercent)
{//薪资增长
double raise = salary * byPercent / 100;
salary += raise;
}
//public表明构造器和方法公有,即除了自身可以调用外,外部也可以调用
}
多个源文件的使用
不同的类可以放在不同在源文件,只要分别编译了就能够运行
还可以每个类单独使用一个源文件(文件名与类名相同)
当编译包含main方法的源文件时,编译器会查找是否存在调用的自定义类对应的.class
文件,否则则会查找对应的.java
文件并主动进行编译
如果存在.java
文件且比对应的.class
文件新,那么该类也会被重新编译
隐式参数与显式参数
以上面的raseSalary
方法为例
public void raiseSalary(double byPercent)
{//薪资增长
double raise = salary * byPercent / 100;
salary += raise;
}
其中的double byPercent
为显式参数
而对象本身为隐式参数
如调用number007.raiseSalary(5)
相当于
double raise = number007.salary * 5 /100;
number007.salary += raise;
//注意到salary相当于number007.salary
//但是实例域是私有的,实际上在外部不能如此处理
隐式参数用关键字this
来表示,上面的调用也相当于
double raise = this.salary * 5 /100;
this.salary += raise;
有的程序员也习惯直接在方法定义中使用this,来显示表明这是类内的实例域
封装的优点
- 可以改变内部实现,如对数据做一定处理后通过访问器方法返回出来
- 更改器方法可以添加错误检查,而直接访问实例域无法自动进行检查
注意:尽量不要编写返回引用可变对象的访问器方法,否则如果对该访问器返回的对象使用更改器方法会直接修改类内的私有实例域,从而破坏了封装性;如果需要返回,可以clone这个对象(即生成一个副本)后返回
基于类的访问权限
一个对象不仅可以访问自己的私有域,还可以访问同类对象的私有域
如:
class Employee
{
...
public boolean equals(Employee other)
{
return name.equals(other.name); //这里访问了同类(Employee)的其他对象(other)的私有域(name)
}
}
私有方法
将方法定义的public
修改为private
即可使方法私有化而不被外部调用
通常用于编写实现其他公有方法的辅助方法
final实例域
对实例域使用final
关键字可以使实例域被构造器初始化后就不可修改final
关键字常用于:基本类型域、不可变类的域
不应对可变类使用final
关键字,如:private final Date hiredate;
hiredate是一个不可变的引用变量,但他引用的Date对象却是可变的,此处的final
会给读者造成混乱
静态域和静态方法
static
修饰符
静态域
静态域属于类而不属于对象,故又称为类域
每个对象都共用一个静态域
静态常量
修饰符static final
实例域通常是私有的,但静态常量却是公有的,反正外部也无法改变常量
静态常量是属于类的,无需通过对象,而可以直接通过类来访问
如Math.PI
静态方法
静态方法不能向对象实施操作(即不能使用隐式参数this),只能访问类的静态域(而不能访问对象的实例域)
如Math.pow()
常用情形:
- 不需要访问对象状态,参数都是显示提供的
- 不需要访问对象的实例域,只需要类的静态域
通过对象来访问静态域、静态常量、静态方法也不是不可以,只是这样会造成混淆
C、C++、JAVA中的
static
修饰符
第一种含义(C):变量一直存在,当再次进入该块时依然存在
第二种含义(C):不能被其他文件访问的全局变量和函数
第三种含义(C++、JAVA):属于类且不属于对象的变量和函数
main方法
每个类都可以定义一个共用静态的main方法
其中一个用来执行程序的主体结构
其他用来分别对类进行单独的测试
方法参数
- Java的方法参数都是按值调用,也就是说调用一个方法,它对外部的变量不会做任何的修改
但是,当传入的是一个对象引用变量时,修改的是对象的设置,而并没有拷贝一个副本 - 方法参数的两种类型:基本数据类型、对象引用
注意:即使是对象引用,其传参方式依旧是值传递,JAVA是不支持地址传递的
C++中引用调用的参数标有&
符号
对象构造
重载
- 类的方法可以名字相同,但参数不同
- 重载解析(overloading resolution):
编译器会根据各个方法给出的参数类型与特定方法调用所使用的值类型进行匹配 - 如果编译器找不到匹配的参数,或者找除多个可能的匹配,就会产生编译错误
默认域初始化
默认情况下,如果没有给域赋初值,会被自动初始化为0
或false
或null
但这不是一种良好的编程习惯,最好能够显式地初始化域
无参数的构造器
如果编写类时没有给出任何构造器
那么系统会自动提供一个给每个域赋予默认初始化值的无参数构造器
如:
public Employee()
{
name = "";
salary = 0;
hireDay = new Date();
}
如果编写类时给出了构造器
那么至少必须提供一个无参数构造器,否则被视为不合法
提供无参数构造器时可以直接给出一个空的无参数构造器,它将表示采取默认初始化值的无参数构造器
如:
public Employee()
{
}
显式域初始化
如果某些实例域无论在哪个构造器中都赋予相同的初值,那么可以直接在实例域的定义时赋值
而且初值无需是常量
如:
class Employee
{
private static int nextId;
private int id = assignId();
...
private static int assignId()
{
int r = nextId;
nextId++;
return r;
}
...
}
在C++中,不能够直接初始化类的实例域,但是可以通过特殊的“初始化器列表”语法来进行实例域的初始化
参数名
如果参数名与实例域重复,那么实例域名会被覆盖,但是可以通过隐式参数来使用
public Employee(String name, double salary)
{
this.name = name;
this.salary = salary;
}
调用另一个构造器
构造器可以通过隐式参数this来调用另一个构造器
如:
public Employee(double s)
{
//调用Employee(String n, double s)
this("Employee #" + nextId, s);
nextId++;
}
此时调用new Employee(123)
就相当于调用new Employee("Employee #123", 123)
意义:提高了代码的重用性
在C++中,构造器是不能调用另一个构造器的
初始化块
在类的声明中可以包含多个代码块,当一个对象被构建时,这些代码块会被自动执行
如:
class Employee
{
private static int nextId;
private int id;
private String name;
private double salary;
//初始化块
{
id = nextId;
nextId++;
}
...
}
- 初始化块放在域定义之后是合法的
但这样容易引发循环定义,因此
尽量将初始化块放在域定义之后
静态域的初始化块
static
{
Random generator = new Random();
nextId = generator.nextInt(10000);
}
初始化细节
初始化有三种方法:定义时初始化、初始化块、构造器初始化
当构造一个对象时,初始化的顺序是这样的——
- 所有数据域被初始化位默认值(0、false、null)
- 依次执行所有域初始化语句和初始化块
- 执行构造器的主体
对象析构和finalize方法
C++中,通常支持显示的析构器方法,用于回收分配给对象的存储空间
但是JAVA有垃圾回收机制,所以Java不需要也不支持析构器
Java支持名为finalize的方法
它将在垃圾回收期清楚对象之前被调用,用于回收内存以外的资源
但是不要依赖它来回收任何短缺的资源,因为很难知道这个方法被调用的具体时刻
包
Java允许使用包(package)将类组织起来
- 包可以使用嵌套层次
- 所有的标准包都处于java或javax包层次中
- 从编译器的角度看,嵌套的包之间没有任何关系,每一个都拥有独立的类集合
类的导入
一个类可以使用所属包的所有类 和 其他包的公有类
使用类的时在类名前添加完整的包名
如:
java.util.Date today = new java.util.Date();
利用import语句导入类
如:
import java.util.Date;
Date today = new Date();
也可以导入整个包:
import java.util.*;
但绝对不要导入所有标准包的所有类,如import java.*.*;
如果导入多个包的所有类,而且他们有重复的类名时,编译器会报错:
import java.util.*; //包含Date类
import java.sql.*; //也包含Date类
此时可以显示地导入特定类,来指明来源的包:
import java.util.*;
import java.sql.*;
import java.util.Date; //告知编译器,导入的是java.util包内的Date类
如果两个类都要同时使用,那就只能通过完整名来使用类啦:
java.util.Date deadline = new java.util.Date();
java.sql.Date today = new java.sql.Date(...);
静态导入
可以导入类的静态方法和静态域
如:
import static java.lang.System.*; //导入System类的所有静态方法和静态域
out.println("Goodbye, World!"); //无需再指明System类
也可以导入特定静态方法或特定静态域
如:
import static java.lang.System.out; //只导入System类的out方法
将类放入包中
在源文件开头添加package语句,如
package com.horstmann.corejava;
public class Empoyee
{
...
}
如果不在源文件中添加package语句
那么源文件中的类都会放入一个默认包中(default package),默认包是没有名字的
包中的文件必须放到与包名相匹配的目录中
以主体结构的源文件为基目录com.horstmann.corejava
的源文件应放在com/horstmann/corejava
目录下
编译包含主体结构的源文件时,编译器就会自动到相应的目录查找文件并进行编译
当然,也可以单独编译源文件并运行它的类,但参数给出的形式不同
# 通过指定路径来编译
javac com/mycompany/PayrollApp.java
# 通过指定包来运行
java com.mycompany.PayrollApp
注意:编译器在编译源文件时不会检查目录结构,如果对应包的源文件没有在指定目录,那么编译不会出错,但最终程序却无法运行
包作用域
public
修饰的部分,可以被任意类使用private
修饰的部分,只能被定义它们的类使用- 既没有public也没有private修饰的部分,可以被同一包内的任何类使用
用户无法自定义java.
的类
类路径
JAR文件
类文件也可以存储在JAR(Java归档)文件中,文件中可以包含多个压缩形式的类文件和子目录,可以节省空间并改善性能
JAR文件使用ZIP格式组织文件和子目录,可以使用ZIP使用程序查看
使类能被多个程序共享的几个要求
- 把类都放到一个目录中
如:/home/user/classdir
- 把JAR文件放在一个目录中
如:/home/user/archives
- 设置类路径(class path)
类路径
类路径通常包含基目录、当前目录、jar文件目录
类路径格式
- UNIX环境中,以冒号
:
为分隔符
如:/home/user/classdir:.:/home/user/archives/archive.jar
其中句点.
表示当前目录
javac编译器总在当前目录中查找文件,但java虚拟机仅在类路径有句点.
时才查找当前目录 - Windows环境中,以分号
;
为分隔符
如:c:\classdir;.;c:\archives\archive.jar
- 运行时库文件会被自动搜索,无需显式地将它们列在类路径中
文件定位顺序
- 对于虚拟机
- 查看
jre/lib
和jre/lib/ext
目录下的归档文件中所存放的系统类文件 - 按照类路径的顺序依次查找各个目录和归档文件
- 查看
- 对于编译器
- 如果引用了一个类但没有指定包
那么会查询所有import命令导入的包的所有源文件
如果找不到这个类或者找到多个类则会报错 - 如果引用了一个类且指定了包
那么会查询这个包下的所有源文件
如果找不到这个类或者找到多个类则会报错
- 如果引用了一个类但没有指定包
设置类路径
为虚拟机添加参数
采用-classpath
或-cp
选项为虚拟机指定类路径
如:java -cp /home/user/classdir:.:/home/user/archives/archive.jar MyProg
设置环境变量CLASSPATH
以bash为例export CLASSPATH=/home/user/classdir:.:/home/user/archives/archive.jar
文档注释
- 用
/**
和*/
注释的内容会自动由javadoc工具生成一个HTML文档 - 文档从以下特性中抽取信息
- 包
- 公有类与接口
- 公有的与受保护的构造器方法
- 公有的和受保护的域
- 注释内容可以包含HTML标签
- 但不要使用
<h1>
和<hr>
,防止和文档格式冲突 - 如果文档中包含其他文件(如图像)的链接
那么文件需要放在子目录doc-files
中,javadoc将从源拷贝其中的文件到文档目录中
如<img src="doc-files/uml.png" alt="UML diagram">
- 但不要使用
类注释
类注释放在import语句之后,类定义之前
方法注释
方法注释放在所描述的方法之前
可以使用以下特殊的标记——
@param
变量描述
为“参数”部分添加一个条目,该描述可以占多行@return
描述
对“返回”部分添加条目,可以占多行@throws
类描述
表示该方法可能出现的异常
如:
/**
* Raises the salary of an employee.
* @param byPercent the percentage by which to raise the salary (e.g. 10 means 10%)
* @return the amount of the raise
*/
public double raiseSalary(double byPercent)
{
...
}
域注释
只对公有域(通常指的是静态常量)建立文档
如:
/**
* The "Hearts" card suit
*/
public static final int HEARTS = 1;
通用注释
可以使用以下标记
@author
姓名
产生一个“作者”条目,可以使用多个@version
文本
产生一个“版本”条目,为对当前版本的描述@since
文本
产生一个“始于”条目,对引入特性的版本的描述@deprecated
文本
对类、方法、变量添加一个不再使用的注释,并给出取代的建议
如:@deprecated Use <code> setVisible(true) </code> instead
@see
引用
在see also
部分添加一个超链接@see com.horstmann.corejava.Employee#raiseSalary(double)
- 包名和类名可以省略,此时表示当前包或当前类
- 类名和方法名必须用井号
#
隔开,因为javadoc无法自动区分类名和方法名
@see <a href="www.horstmann.com/corejava.html">The Core Java home page</a>
可以使用a标签提供一个超链接@see "Core Java 2 volume 2"
可以添加双引号""
使双引号的内容显示到文档上- 可以为一个特性添加多个
@see
标记,但是必须放在一起
@link
引用
指向其他类或方法的超链接
如{@link package.class#feature label}
包与概述注释
有两种方法书写包注释
- 提供一个
package.html
文件
<body>
标签内的内容都会被提取出来形成文档 - 提供一个
package-info.java
文件
一个javadoc注释紧跟在包语句后
不该添加其他的代码或注释
注释的抽取
(假设HTML文件放在目录docDirectory下)
- 切换到包含想要生成文档的源文件目录
- 调用javadoc命令生成文档
- 如果是一个包
javadoc -d docDirectory nameOfPackage
- 如果有多个包
javadoc -d docDirectory nameOfPackage1 nameOfPackage2 ...
- 如果文件在默认包中
javadoc -d docDirectory *.java
- 如果是一个包
类设计技巧
- 一定要保证数据私有
- 防止破坏封装性
- 访问器方法、更改器方法
- 实例域私有化
- 一定要对数据初始化
- Java系统只会对对象的实例域进行初始化,而不会对局部变量进行初始化
- 不要依赖系统的初始化,应该显式的初始化所有数据
- 不要在类中使用过多的基本类型
- 不是所有的域都需要独立的域访问器和域更改器
如常量 - 将职责过多的类进行分解
- 类名和方法名要能够体现它们的职责
一般以形容词(动名词)+名词来命名
访问器以get
为前缀,更改器以set
为前缀