对象和类
OOP简介
作者首先举著名PC生产商,如Compaq、Dell,引入了面向对象(OOP)的概念。OOP只在意对象(Object)的功能,而不关心对象的具体实现;好比PC生产商购入能满足某种需求的配件,不细究其中的实现过程。OOP将功能细分,让每个对象实现其中的一部分,对象间可以通过方法调用(Method calls)完成其所需的任务。但是OOP有良好的数据封装性(encapsulation),某个对象不能直接操作其他对象的内部数据,所有通信的唯一方式就是方法调用,比如访问对象a的一个数据aData,可以a.getData()。
Pascal语言发明者Niklaus Wirth提出了著名的0公式Algorithms + Data Structures = Programs。与传统的结构化编程(Structured Programming)先设计算法(如何操作数据),再考虑数据存储的做法(如上面公式,算法在前)相反,OOP将数据结构放到了算法实现的前面。
关于OOP的术语
类(Class):可以看作待创建的对象的一个模板,或者蓝图。
构造类的一个对象称作创建类的一个实例(Instance)。
封装(Encapsulation,有时也叫做数据隐藏,data hiding):将数据(data)和行为(behavior)一起放入某个黑盒子中,用户却看不到数据的实现。
对象中的数据称为实例域(Instance Field),而对这些数据操作的函数和过程称为方法(Method)。
类的每个实例,即对象,它们的实例域的值可能是彼此不同的,这些值叫做对象的当前状态。对象的方法可能会改变这些状态值。
继承(Inheritance):通过扩展父类实现一个子类。
Java中Object是一个“Cosmic Superclass”,其它的所有类都继承自Object。
对象
对象的三个关键特征:
行为(Behavior),有类的方法定义,所以同一个类的实例有相同的行为。
状态(State),对象状态的改变只能是方法调用的结果,否则认为封装已经被破坏。
标识(Identity),区别不同的对象。
同一个类的不同对象间总是(always)有不同的标识,并且状态经常(usually)互不相同(即有相同的可能)。
类间关系
最通常的类间关系有:
依赖Dependence ("uses–a"):类A的方法要操作类B的某个对象,称类A依赖类B。最常见。注意:尽量减少类间的互相依赖。
聚合Aggregation ("has–a")
继承Inheritance ("is–a")
可以使用统一建模语言(UML)提供的Connector画类图(class diagram)来表示类间的各种关系(参考UML手册)。
OOP和传统过程化编程的比较
两种策略:
由顶而下(Top-down):将问题细化。
自底而上(bottom-up):将完成简单任务的过程组装成程序。
通过细化过程能很好地解决较小的问题,但对于大型的项目,OOP的类和方法的组织方式就能体现它的巨大优势:便于管理、小组分工,提高debug效率。
OOP和模块化(modularization)的区别:
模块间通过过程调用(procedure calls)互相通信,而非共享数据。
类是创建具有相同行为的多个对象的”工厂”,但我们却无法获得一个模块(module)的多个拷贝。
使用已存在的类
对象和对象变量
构造方法(Constructor):构造和初始化对象。
对象变量(Object variable)和对象是不同的,对象变量的值是存储在其他地方的对象的一个引用(reference)。声明一个对象变量后,若要使用其方法,必须用new constructor()初始化。可以给对象变量赋值null,表示没有引用任何对象,这时如果使用某个对象方法,将引发一个运行时(run-time)错误。
本地对象变量(local object variable)不会自动初始化为null,所以必须或者调用new初始化,或者设为null。
书本关于时间和相关的Java两个类的一点描述:Date类表示一个特殊的时间点,时间点的参考基准是协调世界时(UTC)1970年1月1号00:00:00,用两者之间相差的毫秒数(有正负)来表示,当然在使用Date的时候无需知道这些。Calendar类描述了一般的历法属性,可以通过继承这个类实现具体的历法,如我们的阴历。GregorianCalendar就是它的一个子类,用以实现公历表示。在引入Calendar后,Date的方法就不再建议使用(deprecated)。
GregorianCalendar的具体使用参考文档,但有一点注意:在构造它的一个对象时,参数月份是从0开始的,所以11就表示12月份。
Mutator method:改变对象实例域的方法;Accessor method:访问实例域,而不改变它们的值。
自定义类
在一个源文件中只能有一个public类,但可以有多个非public类,源文件名必须和public类同名。
也可以为每个类单独创建一个源文件。书本举例:Employee.java和EmployeeTest.java,在编译时使用通配符,
javac Employee*.java
或者直接
javac EmployeeTest.java
Java编译器会去搜索Employee.class 文件,若没找到,自动编译Employee.java到Employee.class。而且Java编译器还带有类似于Unix make自动重编译的功能:检查Employee.java的时间戳,若比Employee.class要新,则自动重新编译该文件。
若类A中的方法用public修饰,则任何类的任何方法中都能调用类A的public方法。
若类A中的实例域用private修饰,则只有类A中的方法才能访问它们,其它任何类的任何方法都无法读写这些域。
构造方法只能和new操作符合用,不能给已经存在的对象再使用构造方法来重置实例域,否则引发编译时(Compile-time)错误。
对于构造方法,要牢记:
构造方法与类同名
一个类可以有多个构造方法
一个构造方法可以有0个,1个或多个参数
构造方法没有返回值
构造方法总是用new操作符来调用
注意:在方法中引入局部变量时不要与实例域同名,否则会隐藏同名的实例域。在使用实例域的时候可以使用this关键字,以和局部变量区分。
在使用类A的一个对象a的某个方法method(arg)时,即a.method(arg),a称作隐式参数(implicit parameter),arg称作显式参数(explicit parameter)。在方法声明时,显式参数显式地列出,隐式参数不出现。可以用this关键字表示隐式参数。
Field accessor:简单地返回实例域的值的方法。
Fidld mutator:简单地修改实例域的值的方法。
使用private实例域和public的Field accessor 和Fidld mutator与直接使用public实例域有两个好处:
1. 内部实现的改变不影响其它代码
2. Mutator方法可以作错误检查
注意:在写返回易变对象(mutable object)的accessor方法时要十分小心,易变对象很可能被改变,所以用clone()返回易变数据域的一个拷贝。
访问private数据的方法
类的方法可以访问该类的所有对象的private数据。
私有(Private)方法
Private方法只能被同一个类中的其它的方法调用,因此一个没有被类中其它方法调用的private方法,可以简单地丢弃(public方法就不可以了,因为可能有其他代码的依赖)。
最终(Final)实例域
Final实例域必须在对象被创建的时候初始化,而且其值不能再被改变。
如果一个类的所有实例域都是final类型的,则称这个类是不可变的(immutable),也就是说它的对象在创建后不会被改变。Immutable类的一个好处是:不用担心数据共享时的不一致。
静态(Static)域
对于普通的实例域,类的每个对象都有自己的一份拷贝;而static域则是这些对象之间共享的。Static域是属于类的,即使没有对象,static域照样存在。
常量
静态变量(static variable)很少使用,常见的是静态常量(static constant)。如Math.PI
public static final double PI = 3.14159265358979323846;
还有System.out
Public static final PrintStream out = …
静态(Static)方法
静态方法不对具体对象操作,没有隐式参数。静态方法中不能访问实例域,但可以访问静态域。
main方法也是一个静态方法。
工厂方法(factory method)
我们常用某些静态方法来产生某个类的一个对象,比如
NumberFormat formatter = NumberFormat.getPercentInstance();
这种方法就称作工厂方法。
为什么不使用构造方法来创建对象?1.无法给出构造方法的名字;2.工厂方法可以得到类的对象,或者该类的子类对象,而构造方法就没有这种灵活性。
方法参数
传值调用(call by value):方法得到的参数是调用者提供的值,即是参数的一个副本。因此,方法可以修改的只是参数的副本值,而不影响原参数。
引用调用(call by reference):方法得到的是调用者提供的变量的位置,这种方式可以改变传给方法的参数。
Java的方法参数可以是原始类型(数字,Boolean值等),也可以是对象引用(Object reference)。但Java语言并没有使用对对象的引用调用,相反地,对象引用也是通过值传递的。
Java中方法参数的能够和不能够,
1.方法不能够修改原始类型的参数
2.方法能够改变对象参数的状态(用对象饮用作方法参数)
3.方法不能够让对象参数指向一个新的对象。
对象构造
重载(overloading)
几个方法具有相同的方法名,但有不同的参数(数量或者类型),称之为重载。
编译器执行重载解析(overloading resolution):它会从一组方法中寻找与指定方法相匹配的,最后如果有参数不匹配或者有多个匹配,将发生一个编译时错误(compile-time error)。
默认的域初始化
如果没有在构造方法中显式地设置域的值,那么数字默认初始化为0,Boolean型初始化为false,对象引用为null。
和域相比,本地变量(local variables)就必须在方法中显式地初始化。
默认的构造方法
如果某个类不带构造方法,则将使用默认的构造方法,它不带参数,又称为无参构造方法(no-arg constructor),它将对域进行默认的初始化。
如果一个类提供了至少一个构造方法,但是没有提供默认的构造方法,这时如果不带任何参数地构造一个对象将是非法的。
注:默认构造方法只在没有其它构造方法的类中存在。如果想在一个具有构造方法的类中使用无参的默认构造方法,那么必须提供这样的一个默认构造方法。
显式的域初始化(Explicit field initialization)
如果类的某个域在其所有的构造方法中初始为同一个值,则可以直接在类中给该域赋值,而且可以赋给变量值。
参数名
防止参数变量隐藏(shadow)与其同名的实例域,最好在实例域前加上this关键字。
调用另一个构造方法
可以在一个构造方法中使用this(…)调用另一个构造方法,this语句必须出现在第一行。
如书本上的例子,
public Employee(double s)
{
// calls Employee(String, double)
this("Employee #" + nextId, s);
nextId++;
}
这样做的好处就是只需要写一遍通用的构造代码。
初始化块(Initialization blocks)
初始化一个数据域的三种方法:
1. 在构造方法中初始化
2. 在该域声明时初始化
3. 在初始化块中初始化
在类声明中可以包含任意的代码块,这些代码块在构造该类的对象时被执行。如
class Employee
{
public Employee(String n, double s)
{
name = n;
salary = s;
}
public Employee()
{
name = "";
salary = 0;
}
. . .
//必须在初始化块中使用之前定义
private static int nextId;
private int id;
. . .
// 初始化块的代码在构造方法之前执行
{
id = nextId;
nextId++;
}
private String name;
private double salary;
}
构造方法被调用时的细节
1. 所有数据被初始化成它们的默认值(0,false或null)
2. 按照类声明中的顺序,域被逐个初始化(可能在初始化块中)
3. 如果构造方法的第一行调用了另一个构造方法,则执行被调用的构造方法
4. 构造方法的方法体被执行
初始化静态(static)域的时候,可以直接提供一个初始化值,也可以在静态初始化块(用static声明)中实现。后者一般实现复杂的初始化代码。
当类第一次被装载的时候静态初始化就发生了。
对象析构(Destruction)和finalize方法
C++有析构方法(destructor),对不再使用的对象进行清除,收回分配的内存。Java不支持这种手工的内存回收机制,内建自动垃圾回收机制(automatic garbage collection)。
可以给任何类加上finalize方法,该方法在GC清除对象之前被调用。
如果希望在使用某个资源结束后立即关闭它,则可以使用dispose()。类声明中包含的dispose()将在该类的对象处理结束后被调用。特别的,如果类中某实例域有dispose(),则应该提供dispose代码来除去该实例域。
包(Package)
使用包的一个主要原因是保证类名的唯一性(uniqueness)。为保证包名的独特性,Sun推荐用反向的国际域名表示法,如com.urname.urpackage。
使用包嵌套的唯一目的是管理类名的唯一性。在编译器看来,嵌套的两个包之间完全没有关系,每个都有自己的独立的类集合。
使用包
一个类可以使用同一包中的所有类和其它包中的public类。
使用其它包中的类有两个方法,一是在使用的类名前加上完整的包名;二是使用import关键字。
使用import时,可以导入一个包中的所有类,这不会影响代码的大小。但是不能使用java前缀导入所有的包,如import java.*; import java.*.*;。如果导入的两个包中有相同的类名,则在使用该类时要在类名前加上完整的包名,以互相区别。
静态导入(Static Imports)
从JDK1.5开始,import语句可以导入静态方法和静态域,如在源文件加上,
import static java.lang.System.*;
则可以直接使用System的静态方法,如exit(),out.println(),而无需在方法前加上System这个类名前缀.
虚拟机如何定位类文件(classes)
在文件系统中到类文件的路径必须和包名相一致。可以用JAR工具对类文件进行归档,这样可以节省空间和访问时间。如在jre/lib下的运行时库rt.jar包含了数千个类文件。
Classpath是基目录的集合,每个基目录的子目录下可以包含类文件。使用JDK时有两种方法指定classpath:1.给编译器和字节码解释器用-classpath选项2.通过设置CLASSPATH环境变量。
Classpath包括1.一个基目录2.当前目录3.JAR文件。
系统类文件:jre/lib, jre/lib/ext
包范围(Package scope)
如果没有指定public或者private等访问控制符,类、方法或变量能够被同一包中的所有方法访问,要特别注意变量的访问权限。
包封装(package sealing)后,就不能再向其中添加类文件。
文档注释
插入注释
每个/** …*/文档注释包括了标签(tags)和自由格式的文本,其中标签以@开头,如@author,@param。
类注释
位于import语句之后,类定义之前。
方法注释
首先描述方法的作用,然后可用下面的通用标签:
@param variable description
@return description
@throws class description
域注释
只需要对公共域给出注释,通常是静态常量。
OOP类设计经验
总是让数据私有
总是初始化数据
不要在一个类中使用太多的基本类型
并不是每个域都需要它自己的accessor和mutator方法的
类定义时使用标准的形式
类的内容
public features
package scope features
private features
对每个部分
instance methods
static methods
instance fields
static fields
将复杂类的功能分派到多个较小类
让类名和方法名有望文生义的效果