第4章 对象与类
面向对象程序设计(Object-Oriented Programming,OOP)。
实现一个简单的Web浏览器可能2000个过程,采用面向对象设计风格大约100个类,每个类平均包含20个方法。
类
类(class)是构造对象的模板或蓝图,我们可以将类想象成制作小甜饼的切割机,将对象想象称为小甜饼。
由类构造器(construct)对象的过程称为创建类的实例(instance)。
封装(encapsulation,有时称为数据隐藏)是与对象有关的一个重要概念,将数据和行为组合在一个包中,并对对象使用者隐藏了数据的实现方式。对象中数据称为实例域(instance field),操纵数据的过程称为方法(method)。对于每个特定的类实例(对象)都有一组特定的实例域值,这些值的集合就是这个对象的当前状态(state)。无论何时,只要向对象发送一个消息,他的状态就有可能发生改变。
实现封装的关键在于绝对不能让类中的方法直接地访问其他类的实例域。程序仅通过对象的方法与对象数据进行交互。封装给对象赋予了黑盒特征,这是提高重用性和可靠性的关键。
通过拓展一个类来建立另外一个类的过程称为继承(inheritance)。
对象
对象三个主要特性:
- 对象的行为(behavior):可以对对象施加哪些操作或方法?
- 对象的状态(state):当施加这些方法时,对象如何响应?
- 对象标识(identity):如何辨别具有相同行为与状态的不同对象?
每个对象都有一个唯一的身份(identity)。例如订单系统中,即使两个商品都一样,订单号也要不同。
识别类
传统过程化程序设计必须从顶部的main函数开始编写程序。面向对象则没有所谓的顶部。
对于OOP初学者会感到无从下手,答案是:首先从设计类开始,然后再往每个类中添加方法。
识别类的简单规则是在分析问题的过程中寻找名词,而方法对应着动词。
例如:订单处理系统中有这些名词
- 项目(Item)
- 订单(Order)
- 送货地址(Shipping address)
- 付款(Payment)
- 账户(Account)
这些名词可能成为类Item,Order等。
接下来查看动词,项目被添加到订单中,订单被发送或取消,订单货款被支付,对于每一个动词,添加,发送,取消以及支付都要标识处主要负责完成的相应动作的对象。
类之间的关系
- 依赖(uses-a)
- 聚合(has-a)
- 继承(is-a)
依赖(dependence),一个类的方法操纵另一个类的对象,我们就说一个类依赖于另一个类。Order类使用Account类,订单要操作账户,应该尽可能将相互依赖的类减至最少,让类之间的耦合度最小。
聚合(aggregation),是一种具体且易于理解的关系,例如Order对象包含了一些Item对象,聚合关系意味着类A的对象包含类B的对象。
继承(inheritance),表示特殊与一般的关系。
表示类关系的UML(Unified Modeling Language,统一建模语言)符号:
对象与对象变量
Java的日期类库有些混乱,重新设计参看:http://jcp.org/en/jsr/detail?id=310
一定要认识到:一个对象变量并没有实际包含一个对象,而仅仅引用一个对象。
在Java中,任何对象变量的值都是对存储在另外一个地方的对象的引用。new操作符的返回值也是一个引用。
可以将Java对象变量看做C++的对象指针。
所有的Java对象都存在堆中,当一个对象包含另一个对象变量时,这个变量依然包含着指向另一个堆对象的指针。
Date类中,有一个毫秒数时1970年1月1日开始。
日历表示法用GregorianCalendar类。格里高利历
案例:打印当前月的日历
未完待续。。。
用户自定义类
开始学习如何设计主力类(workhorse class)。这些类没有main方法,却又自己的实例域和实例方法。
要想创建一个完整的程序,其中只有一个类main方法。
Employee类
编写薪金管理时可能会用到,创建EmployeeTest类
package corejava;
import java.util.Date;
import java.util.GregorianCalendar;
public class EmployeeTest {
public static void main(String[] args) {
Employee[] staff = new Employee[3];
staff[0] = new Employee("孙悟空", 75000,1987,12,15);
staff[1] = new Employee("通背猿猴", 50000,1989,10,1);
staff[2] = new Employee("赤尻马猴", 40000,1990,3,15);
//涨工资5%
for (Employee e : staff) {
e.raiseSalary(5);
}
//打印信息
for (Employee e : staff) {
System.out.println("name="+e.getName()+",salary="+e.getSalary()+",hireDay="+e.getHireDay());
}
}
}
class Employee {
//实例字段
private String name;
private double salary;
private Date hireDay;
//构造器
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();
}
public Employee() {}
//
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public double getSalary() {
return salary;
}
public void setSalary(double salary) {
this.salary = salary;
}
public Date getHireDay() {
return hireDay;
}
public void setHireDay(Date hireDay) {
this.hireDay = hireDay;
}
public void raiseSalary(double byPercent) {
double raise = salary * byPercent /100;
salary += raise;
}
}
//输出
//name=孙悟空,salary=78750.0,hireDay=Tue Dec 15 00:00:00 CST 1987
//name=通背猿猴,salary=52500.0,hireDay=Sun Oct 01 00:00:00 CST 1989
//name=赤尻马猴,salary=42000.0,hireDay=Thu Mar 15 00:00:00 CST 1990
注释:可以用public标记实例域,但是极为不提倡,任何方法都可以修改public的域,破坏了封装,强烈建议标记为private。
构造器伴随着new操作一起调用,不要在构造器里定义与域重名的局部变量。
在每一个方法中,关键字this表示隐式参数。
如果需要返回一个可变对象的引用,应该首先对它进行克隆(clone)。对象clone是指存放在另一个位置上的对象副本。
静态域
将域定义为static,每个类中只有一个这样的域,每个对象对于所有的实例域都有自己的一份拷贝。
1000个Employee类就有1000个实例域id,静态域nextId(静态变量)只有一个,所有对象共享。
静态常量
静态常量用的比较多,也是java中表示常量的方法。Math.PI.
静态方法
静态方法不能操作对象,不能在静态中访问实例域,可以访问自身类中的静态域。静态方法调用静态变量。类名直接调。
解释为:属于类且不属于类对象的变量和函数。
方法参数
按值调用(call by value)表示方法接收的是调用者提供的值。
按引用调用(call by reference)表示方法接收的是调用者提供的变量地址。
Java程序设计语言总是采用按值调用,也就是说方法得到的是所有参数值的一个拷贝,特别是,方法不能修改传递给它的任何参数变量的内容。
- 一个方法不可能修改一个基本数据类型的参数,基本数据类型(数字,布尔)。
- 一个方法可以改变一个对象参数的状态。
- 一个方法不能让对象参数引用一个新的对象。
重载
如果多个方法有相同的名字,不同的参数,便是重载(overloading)。
完整描述一个方法,指出方法名以及参数类型,叫做方法的签名(signature)。
参数名
可以在每个参数前面加上一个前缀"a":
public Employee(String aName,double aSalary){...}
调用另一个构造器
有参构造器调用其他构造器,用this(nextId,s);
对象析构与finalize方法
Java不支持析构器,使用了内存之外的资源,可以为任何一个类添加finalize方法。
此方法将在垃圾回收器清除对象之前调用,实际应用中不要依赖于此方法回收任何短缺的资源,很难知道什么时候调用。
注释:System.runFinalizersOnExit(true)可以确保finalizer方法在Java关闭前被调用,但不安全,不鼓励大家使用。
替代方法是Runtime.addShutdownHook添加关闭钩(shutdown hook)。
包
sun公司建议将公司的因特网域名(独一无二)以逆序的形式作为包名,并且对于不同项目使用不同的子包。
com.clgao
静态导入
import static java.lang.System.*;
out.println("hello,world!")
exit(0);
例如:sqrt(pow(x,2)+pow(y,2))比Math点来点去清晰的多。
设置类路径
java -classpath c:\classdir;.;c:\archives\archive.jar MyProg
shell
export CLASSPATH=/....
Windows shell
set CLASSPATH=c:\......
注释
方法注释:
- @param 变量描述,对参数添加一个条目,描述可多行
- @return 描述,返回值进行描述
- @throws类描述,异常描述
通用注释:
- @author 姓名,多个作者
- @version 文本,某个版本
- @since 文本,始于某个时间或版本
- @deprecated 文本,不在使用,给出取代的建议
- @see 引用,增加一个超链接
- @link 引用,类似于see
注释的抽取,javadoc -d 目录
类设计技巧
- 一定要保证数据私有
- 一定要对数据初始化
- 不要在类中使用过多的基本类型
- 不是所有的域都需要独立的域访问器和域更改器
- 将职责过多的类进行分解
- 类名和方法名要能够体现它们的职责