OOP基础
类
class
是构造对象的模板或者说蓝图。由类构造的对象称为类的实例(instance
)。
类封装是与对象有关的一个重要概念。从形式上来看,封装不过是将数据和行为组合在一个包中,并对对象的使用者隐藏了数据的实现方式。对象中的数据称为实例域,操纵数据的过程称为方法。对于每个特定的类实例,都有一组特定的实例域值。这些值的几何就是这个对象的当前状态。无论何时,只要想对象发送一个消息,它的状态就有可能发生改变。
实现封装的关键在于,绝对不能让类中的方法直接地访问其他类的实例域。程序仅仅通过对象的方法与对象数据进行交互。封装给对象赋予了黑盒特征,这是提高重用性和可靠性的关键。这意味着一个类可以全面地改变存储数据的方式,只要仍旧使用同样的方法操作数据,其他的对象就不会知道或介意所发生的变化。
OOP的另一个原则会让用户自定义Java类变得轻而易举:可以通过扩展一个类来建立一个新的类。
扩展一个已有的类时,新类就会具有所扩展的类的全部方法和属性。在新类中,只需要提供适用于这个新类的新方法和数据域就可以了。通过扩展一个类来建立新的类的过程称为继承。
传统的过程化程序设计,必须从顶部的main函数开始编写程序。在OOP程序设计时没有所谓的顶部,方法是:首先从设计类开始,然后再往每个类中添加方法。
对象
对象有三个主要特性:
- 对象的行为——可以对对象施加那些操作,或者哪些方法。
- 对象的状态——当施加那些方法时,对象如何相应。
- 对象标识——如何比阿尼额具有相同行为与状态的不同对象。
同一个类的所有对象实例,由于支持相同的行为而具有家族式的相似性。对象的行为是用可调用的方法定义的。
此外,每个对象都保存着描述当前特征的信息。这就是对象的状态。对象的状态可能会随着时间而发生改变,但是这种改变不是自发的。对象状态的改变必须通过调用方法实现。
但是,对象的状态并不能完全描述一个对象。每个对象都有一个唯一的身份。例如,在一个订单处理系统中,任何两个订单都存在着不同之处。即使所订购的货物完全相同也是如此。需要注意的是,作为一个类的实例,每个对象的标识永远是不同的,状态也常常存在着差异。
预定义类
Java中,没有类就无法做任何事情。也不是所有的类都具有面向对象特征。
对象与对象变量。
要想使用对象,就必须首先构造对象,并制定其初始状态。然后对对象应用方法。
在Java中,使用构造器构造新实例。构造器是一种特殊的方法,用来构造并初始化对象。
Date类
构造器的名字应当与类名相同:new Date()
。
这个表达式构造了一个新对象。这个对象被初始化为当前的日期和时间。
我们需要储存这个构造出来的对象:Date birthday = new Date();
。
注意对象变量和对象是不同的,对象变量可以引用变量,在引用变量之前它什么都不是。
可以显式地将对象变量赋值为null
:Date deadline = null;
。
GregorianCalendar类
在前面的例子中,已经使用了Java类库的Date类。Date类的实例有一个状态,即特定的时间点。
尽管在使用Date类时不必知道这一点,但时间是用距离一个固定时间点的毫秒数(时间戳)来表示的。
但是Date类所提供的日期处理并没有太大的用途。Java类库的设计者认为,通用日期表示法只适用于我们一般的固有习惯。
类库设计者决定将保存时间与给时间点命名分开。所以Java类库分别包含了两个类,一个是表示时间点的Date
类,一个是日历表示法GregorianCalendar
类。将时间与日历分开是一种很好的面向对象设计。
通常,最好使用不同的类表示不同的概念。
GregorianCalendar
类中有更多的构造器和方法。
可以提供年月日来构造一个表示某个特定日期午夜的日历对象:new GregorianCalendar(1998, 11, 31)
。
比较怪异的是,月份从0开始计数。为了清晰,可以用常量:new GregorianCalendar(1998, Calendar.DECEMBER, 31)
。
还可以设置时间:new GregorianCaldendar(1998, 11, 31, 23, 59, 59)
。
自定义类
从构造器开始
可以看到,构造器与类同名。在构造对象时,构造器会运行,以便将对象初始化为所希望的状态。
- 构造器与类同名。
- 每个类可以有多个构造器。
- 构造器可以有0、1或者多个参数。
- 构造器没有返回值。
- 构造器总是伴随着new操作一起调用。
隐式参数和显式参数
方法用于操作对象和存取他们的实例域。
class a{
int x;
void b(int y){
int x = y;
this.x = x;
}
}
上面是一个类的示例,类中有一个变量x,方法b中也有,this关键字就用来调用类中的参数,称为隐式参数。
静态域和静态方法
static
是静态修饰符。
静态域
如果将域定义为static
,每个类中只有一个这样的域。而每个对象对于所有的实例域却都有自己的一份拷贝。
即static
域是属于类的,所有对象共用一个。
静态方法
静态方法是不能向对象实施操作的方法。
可以认为static
方法是没有this
参数的方法。
使用静态方法主要在以下两种情况:
- 一个方法不需要访问对象状态,其所需的参数都是通过显示参数提供。
- 一个方法只需要访问类的静态域。
工厂方法
静态方法还有一种常见用途:采用不同的工厂方法产生不同风格的实例。
使用工厂方法主要有以下两种情况:
- 无法命名构造器,构造器的名字必须与类名相同。但是,这里希望得到的货币实例和百分比实例采用不同的名字。
- 当使用构造器时,无法改变所构造的对象类型。
方法参数
一般传参有两种形式:传引用、传值。
Java中总是采用按值调用的方式。方法得到的是所有参数值的一个拷贝。
特别地,方法不能修改传递给它的任何参数变量的内容。
然而,方法参数共有两种类型:基本数据类型、对象引用。
一个方法不能修改一个基本数据类型的参数,而对象引用作为参数就不同了。
可以在方法中修改对象的状态。
但这不是说对象引用可以改变对象变量的值,对象采用的不是引用调用,而是值传递。
对象构造
重载
多个方法有相同的名字,不同的参数列表,就产生了重载。
编译器根据方法的参数类型来判断具体执行哪个方法。
默认域初始化
如果再构造器中没有显式地给域赋予初值,那么就会自动地赋为默认值:数值为0,布尔值为false,对象引用为null。
无参数的构造器
如果再编写一个类是没有编写构造器,那么系统就会提供一个无参数构造器。
如果类中提供了至少一个构造器,但是没有提供无参数的构造器,则在构造对象时如果没有提供参数就会被视为不合法。
显示域初始化
由于类的构造方法可以重载,所以可以采用多种形式设置类的实例域的初始状态。
确保不管怎么样调用构造器,每个实例域都可以被设置为一个有意义的初值。
在执行构造器之前,先执行复制操作。当一个类的所有构造器都希望把相同的值富于某个特定的实例域是,这种方法特别有用。
包
Java使用包(package
)来将类组织起来。借助包可以方便地组织自己的代码,并将自己的代码与别人提供的代码库分开管理。
标注你的Java类库分布于多个包中,包括java.lang``java.util``java.net
等。标准的Java包都处于java``javax
包层次中。
试用包的主要原因是确保类名的唯一性。加入两个程序员建立了同样类名的类,只要将这些类放置在不同的包中,就不会发生冲突。
类的导入
一个类可以使用所属包中的所有类,和其他包中的公共类。我们可以曹勇两种方式访问另一个包中的所有公共类。
第一种方式是在每个类名之前添加完整的包名:java.util.Date today = new java.util.Date()
。
这显然很繁琐。
更简单更常用的是使用import
语句。
注意,只能用*
导入一个包,不能导入所有包。
将类放入包中
要想将一个类放进包中,就必须将包的名字放在源文件的开头。
如果没有在源文件中放置package
语句,这个源文件中的类就被放置在一个默认包中。默认包没有名字。
我们应当将包中的文件放到与完整的包名匹配的子目录中。
包作用域
前面已经接触过访问修饰符public
和private
。标记为public
的部分可以被任意类使用,标记为private
的部分职能被定义它们的类使用。
如果没有指定,这个部分可以被同一个包中的所有方法访问。
类路径
类的路径必须与包名匹配。
另外,类文件也可以存储在JAR文件中,在一个JAR文件中,可以包含多个压缩形式的类文件和子目录。这样既可以节省又可以改善性能。
在程序中用到第三方的库文件时,通常会给出一个或多个需要包含的JAR文件。JDK也提供了许多的JAR文件,例如,在jre/lib/re.jar
中包含数千个类库文件。
为了使类能够被多个程序共享,需要做到以下几点:
- 把类放到一个目录中。需要注意,这个目录是包树状结构的基目录。如果希望将一个类添加到其中,这个类文件就必须位于子目录中。
- 在JAR文件放在一个目录中。
- 设置类路径,类路径是所有包含类文件的路径的集合。
在UNIX环境中,类路径中的不同项目之间采用冒号分隔。
而Windows环境中,类路径中的不同项目之间采用分号分隔。
类路径包括:
- 基目录
- 当前目录
- JAR文件
文档注释
JDK包含一个很有用的工具,叫做javadoc,它可以由源文件生成一个HTML文档。
如果在源码中添加以专用的定界符开始的注释,那么可以很容易地生成一个看上去具有专业水准的文档。
这是一种很好的方式,因为这种方式可以将源码和注释保存在一个地方。如果将文档存入一个独立的文件中,就可能会随着时间的推移,出现代码和注释不一致的问题。
注释的插入
javadoc实用程序从下面几个特性中抽取信息:
- 包
- 公用类和接口
- 公有的和受保护的构造器和方法
- 公有的和受保护的域
应该为上面几部分编写注释。注释应该放置在所描述的特性的前面。注释以/**
开始,*/
结束。
每个/**...*/
文档注释在标记之后紧跟着自由格式文本。标记由@开始。
自由格式文本的第一句应该是一个概要性的句子。javadoc实用程序自动地将这些句子抽取出来形成概要页。
类设计技巧
下面简单记录一些OOP的类设计技巧。
1. 一定要保证数据私有
这是最重要的;绝对不要破坏封装性。有时候,需要编写一个访问器方法或更改器方法,但是最好还是保持实例域的私有性。
很多惨痛的经验告诉我们,数据的表示形式很可能会改变,但它们的使用方式却不会经常发生变化。当数据保持私有时,它们的表示形式的变化不会对类的使用者产生影响。
2. 一定要对数据初始化
Java不对局部变量进行初始化,但是会对对象的实例域进行初始化。最好不要依赖于系统的默认值,而是应该显式地初始化所有数据。
具体的初始化方式可以是提供默认值,也可以是在所有构造器中设置默认值。
3. 不要在类中使用过多的基本类型
就是说,用其他的类代替多个相关的基本类型的使用。这样会使类更加易于理解且已与修改。
例如,我们可以用一个新的类来替换类中的一些实例域。
这样可以更加容易地理解和处理变量。
4. 不是所有的域都需要独立的域访问器和域更改器
或许,需要获取或设置雇员的薪资。而一旦构造了雇员对象,就应该禁止更改雇用日期,并且在对象中,常常包含一些不希望别人获得或设置的实例域。
5. 将职责过多的类进行分解
这样说似乎有点含糊不清,究竟多少算是“过多”?
每个人的看法不同,但是如果明显地可以将一个复杂的类分解为两个更为简单的类,就应该将其分解。
6. 类名和方法名要能够体现它们的职责
与变量应该有一个能够反映其含义的名字一样,类也应该如此。
命名类名良好的习惯是采用一个名词、前面有形容词修饰的名词或动名词修饰的名词。
对于方法来说,习惯是访问器方法用小写get开头,更改器方法用小写set开头。