3. 对象与类
3.1. 面向对象程序设计概述
算法+数据结构=程序
结构化程序设计:算法是第一位,数据结构第二位
面向对象程序设计:数据结构第一位,算法第二位
-
类
-
由类构造(construct)对象的过程称为创建类的实例(instance)
封装(encapsulation):将数据和行为组合在一个包里,而对对象使用者隐藏了数据的实现方式。
对象中的数据称为实例域(instance field),操纵数据的过程叫方法(method)
封装的关键在于绝对不能让类中的方法直接地访问其他类的实例域,程序仅通过对象的方法与对象数据交互。
继承(inheritance):通过扩展一个类建立另外一个类
对象
-
三个基本特性
对象的行为:可以对对象施加哪些方法?
对象的状态:当施加那些方法时,对象如何响应?
对象的标识:如何辨别具有相同行为和状态的不同对象?
识别类
- 编写程序时,从设计类开始,再往每个类中添加方法 类之间的关系
- 依赖(uses-a):如果一个类的方法操纵另一个类的对象,则一个类依赖于另一个类。
- 聚合(has-a):类A的对象包含类B的对象(Order类包含Item类)
- 继承(is-a)
3.2. 使用预定义类
-
对象与对象变量
- 要想使用对象,必须先构造对象,指定初始状态,然后对对象应用方法。
-
Java中使用构造器(constructor)构造新实例。构造器是一种特殊的方法,用来构造并初始化对象。
new Date() //构造了一个新对象
-
Date deadline;
定义了一个对象变量,可以引用Date类型的对象。但是,变量deadline不是一个对象,实际上也没有引用对象。不能将任何Date方法应用于这个变量。 -
初始化对象变量。可以用新构造的对象初始化这个变量
deadline=new Date();
也可以让这个变量引用一个已存在的对象deadline=birthday;
- 一个对象变量并没有实际包含一个对象,而仅仅引用一个对象。 GregorianCalendar类
-
Date类用来表示时间点,GregorianCalendar类用来表示日历表示法
具体用法见另一篇博客《》
更改器方法和访问器方法
-
更改器方法(mutator method):对实例域做出修改
访问器方法(accessor method):仅访问实例域而不做出修改
3.3. 用户自定义类
主力类(workhorse class):没有main方法,但有自己的实例域及方法
-
自定义类
-
最简单的类定义形式为:
class ClassName
{
field1
field2
…
constructor1
constructor2
…
method1
method2
…
}
构造器
-
构造器总是伴随着new操作符的执行被调用,而不能对一个已经存在的对象调用构造器来达到重新设置实例域的目的
james.Employee("James",25000) //error
-
构造器的特点
构造器与类同名
每个类可以有一个以上的构造器
构造器可以有0个、1个或多个参数
构造器没有返回值
构造器总是伴随着new操作一起调用
隐式参数与显式参数
-
number007.raiseSalary(5);
raiseSalary方法有两个参数。
第一个参数成为隐式(implicit)参数,时出现在方法名前面的Employee类对象。
第二个参数是方法名后面括号中的数值,这是一个显式(explicit)参数。
显式参数是明显地列在方法声明中的,隐式参数没有。 - 在每个方法中,关键词this代表隐式参数。 封装的好处
-
除了该类的方法之外,不会影响其他的代码
更改器方法可以执行错误检查 - ! 不要编写返回引用可变对象的访问器方法,会破坏封装性
Employee harry=...;
Date d=harry.getHireDay();
double tenYearsInMillSeconds=10*365025*24*60*60*1000;
//改变了hireDay
d.setTime(d.getTime()-(long)tenYearsInMillSeconds);
因为d和harry.hireDay引用同一个对象。对d调用更改器方法就可以自动地改变这个对象的私有状态!
如需返回一个可变对象的引用,应该先对它进行克隆(clone)。对象克隆是指存放在另一个位置上的对象副本。return hireDay.clone()
final实例域
private final String name;
- 将实例域定义为final,构造对象时必须初始化这样的域。也就是说,必须确保在每一个构造器执行之后,这个域的值被设置,并且在后面的操作中,不能够再对它进行修改。
- final修饰符大都应用于基本(primitive)类型域,或不可变(immutable)类的域(如果类中的每个方法都不会改变其对象,这种类就是不可变的类,如String类)。
- 对于可变的类,使用final修饰仅仅意味着变量的对象引用在对象构造后不能被改变。
3.4. 静态域与静态方法
静态域
private static int nextId=1;
- 如果将域定义为static,每个类中只有一个这样的域。就是说,不管这个类有没有对象,对象是多少个,静态与只有一个。它属于类,而不属于任何一个独立的对象。
静态常量
public static final double PI=3.14;
- 可以直接通过Math.PI获得常量
- 如果省略static,PI就变成了Math类的一个实例域。需要通过Math类的对象访问PI。
- 另一个常用的静态常量是System.out。
静态方法
- 静态方法是一种不能向对象实施操作的方法。
Math.pow(x,a);
在运算时,不适用任何Math对象。换句话说,没有隐式的参数。(不用实例化就可以使用)- 可以认为静态方法是没有this参数的方法。(在一个非静态的方法中,this参数表示这个方法的隐式参数)
- 静态方法不能访问实例域,因为不能操作对象。但可以访问自身类中的静态域(前面定义的nextId)。
- 在下面两种情况下使用静态方法:1.一个方法不需要访问对象状态,其所需参数都是通过显式参数提供。2.一个方法只需要访问类的静态域。
工厂方法
- 静态方法另一种的常见用途。
- NumberFormat类使用工厂方法产生不同风格的格式对象。
NumberFormat currencyFormatter=NumberFormat.getCurrencyInstance();
NumberFormat percentFormatter=NumberFormat.getPercentInstance();
double x=0.1;
System.out.println(currencyFormatter.format(x));//print $0.10
System.out.println(percentFormatter.format(x));//print 10%
main方法
- 不需要使用对象调用静态方法。main方法也是一个静态方法。
- main方法不对任何对象进行操作。
3.5. 方法参数
- 按值调用(call by value):方法接收的是调用者提供的值。按引用调用(call by reference):方法接收的是调用者提供的变量地址。
- Java中总是采用按值调用。也就是说,方法得到的是参数值的一个拷贝,特别是,方法不能修改传递给它的任何参数变量的内容。
- 方法参数有两种类型:基本数据类型 & 对象引用。方法不可能修改一个基本数据类型的参数。但可以改变一个对象参数的状态。(因为改变了被引用的对象的状态,所以变量引用被改变后的对象。)
- 一个方法不能让对象参数引用一个新的对象。
3.6. 对象构造
重载(overloading)
- 如果多个方法有相同的名字,不同的参数,便产生了重载。
- Java允许重载任何方法,而不只是构造器方法。因此,要完整地描述一个方法,需要指出方法名以及参数类型。这叫做方法的签名(signature)。如,indexOf(int).
- 返回值不是方法签名的一部分。也就是说,不能有两个名字相同、参数类型也相同,但返回不同类型值的方法。
无参构造器
- 在编写一个类时如果没有编写构造器,系统就会提供一个无参构造器。这个构造器将所有实例域设置为默认值。
- 如果类中提供了至少一个构造器,但没有提供无参构造器,则在构造对象时如果没有提供参数就会被视为不合法。(即如果提供了至少一个构造器,系统不会再提供无参构造器。)
显式域初始化
- 在执行构造器之前,先执行赋值操作。当一个类的所有构造器都希望把相同的值赋予某个特定的实例域时,这种方式特别有用。
- 初始值不一定是常量。也可以调用方法初始化域。
private int id=assignId();
调用另一个构造器
- 如果构造器的第一个语句形如this(…),这个构造器将调用同一个类的另一个构造器。
public Employee(double s)
{
//calls Employee(String,double)
this("Employee #"+nextId,s);
nextId++;
}
初始化块
- 在一个类的声明中,可以包含多个代码块。只要构造类的对象,这些块就会被执行。
- 无论使用哪个构造器构造对象,对象初始化块中的实例域会被初始化。首先运行初始化块,然后才运行构造器的主体部分。
...
private double salary;
//object initialization block
{
id=nextId;
nextId++;
}
...
调用构造器的具体处理步骤:
- 所有数据域被初始化为默认值(0,false或null)
- 按照在类声明中出现的次序,一次执行所有域初始化语句和初始化块
- 如果构造器第一行调用了第二个构造器,则执行第二个构造器主体
- 执行这个构造器的主体
静态的初始化块
//static initialization block
static
{
Random generator=new Random();
nextId=generator.nextInt(1000);
}
3.7. 包
- 所有标准的Java包都处于java和javax包层次中
使用包的主要原因是确保类名的唯一性
类的导入
一个类可以使用所属包中的所有类,以及其他包中的公有类。
- 可以使用import语句导入一个特定的类或整个包。import语句位于源文件的顶部(但位于package语句后面)。
- 只能使用*导入一个包。
import java.*;//error
import java.*.*; //error
静态导入
- import语句不仅可以导入类,还增加了导入静态方法和静态域的功能。
import static java.lang.System.*;
可以使用System类的静态方法和静态域。
包作用域
- public:被任意的类使用
- private:只能比定义它们的类使用
- default:可以被同一个包中的所有方法调用
3.8. 文档注释
JDK中包含javadoc,可以由源文件生成一个HTML文档。
插入注释
- 每个/*…./文档注释在标记之后紧跟着自由格式文本(free-form text)。标记由@开始,如@author或@param。
- 自由格式文本的第一句应该是一个概要性的句子。javadoc实用程序自动地将这些句子抽取出来形成概要页。
类注释
类注释必须放在import语句之后,类定义之前。
方法注释
- 每一个方法注释都必须放在所描述的方法之前
- 除了通用标记外,可以使用:@param变量描述 @return描述 @throws类描述
域注释
只需要对公有域(通常指静态常量)建立文档。
通用注释
- @author 作者
- @version 版本
- @since 始于
- @deprecated 不再使用
- @see 引用
注释的抽取
(假设HTML文件将被存放在目录docDirectory下)
1.切换到包含想要生成文档的源文件目录
2.如果是一个包,运行命令javadoc -d docDirectory nameOfPackage
如果是多个包,运行命令javadoc -d docDirectory nameOfPackage1 nameOfPackage2
如果是默认包,运行命令javadoc -d docDirectory *.java
3.9. 类设计技巧
- 一定要保证数据私有。
- 一定要对数据初始化。
- 不要在类中使用过多的基本类型。 用其他的类代替多个相关的基本类型的使用。使类更易于理解且易于修改。
- 不是所有的域都需要独立的域访问器和域更改器。
- 将职责过多的类进行分解。
- 类名和方法名要能够体现他们的职责。