目录
面向对象程序设计概述
概述
面向对象程序设计(简称OOP)是当今主流的程序设计范型,它已经取代了20世纪70年代的“结构化”过程化程序设计开发技术。
面向对象的程序是由对象组成的,每个对象包含对用户公开的特定功能部分和隐藏的实现部分。程序中的很多对象来自标准库,还有一些是自定义的。
从根本上说,只要对象能够满足要求,就不必关心其功能的具体实现过程。在OOP中,不必关心对象的具体实现,只要能够满足用户的需求即可。
类
类(class)是构造对象的模版或蓝图。
我们可以将类想象成制作小甜饼的机器,将对象想象为小甜饼。由类构造(construct)对象的过程,称作创建类的实例/类的实例化。
标准的Java库提供了几千个类,可以用于用户界面设计、日期、日历和网络程序设计。尽管如此,还是需要自定义一些类,来描述客户的问题域中的对象。
封装(encapsulation,有时称为数据隐藏)是与对象紧密相关的一个概念。从形式上看,封装不过是将数据和行为组合在一起,并对对象的使用者隐藏了数据的实现方式。
对象中的数据成为实例域(instance field),操纵数据的过程称为方法(method)。对于每个特定的类实例(对象)都有一组特定的实例域值。这些值的集合就是这个对象的当前状态(state)。无论何时,只要向对象发送一个消息,它的状态就有可能发生改变。
实现封装的关键就是绝对不能让类中的方法直接访问其他类的实例域。程序仅仅通过对象的方法与对象数据进行交互。封装给对象赋予了“黑盒”特征,提高了重用性和可靠性。
在OOP的原则中,有一个会让用户自定义Java类变得轻而易举,这就是:可以通过扩展一个类来建立另外一个新的类。而事实上,在Java中,所有类都源自于一个超类”Object“。
在扩展一个已有类时,这个新类具备被扩展类的全部属性和方法。
通过扩展一个类来建立另外一个类的过程称为继承(inheritance)。
对象
对象的主要特性:
对象的行为(behavior):可以对对象施加哪些操作,或可以对对象施加哪些方法?
对象的状态(state):当施加那些方法时,对象如何响应?
对象标识(identity):如何辨别具有相同行为与状态的不同对象?
对象的行为是通过可调用的方法定义的。
需要注意:作为一个类的实例,每个对象的标识 永远 是不同的,状态 常常 也存在差异。
对象的主要特性相互影响着,例如状态影响行为。
识别类
传统的过程化程序设计,必须从顶部的main函数开始编写程序。在面形对象程序设计时没有所谓的“顶部”,而是首先从设计类开始,然后再往每个类中添加方法。
识别类的简单规则是在分析问题的过程中寻找名词,而方法对应着动词。然后根据编程经验和确定的名次自定义类,根据动词进行方法编程。
类之间的关系
类之间,最常见的关系有:
- 依赖(“uses-a”)
- 聚合(“has-a”)
- 继承(“is-a”)
依赖(dependence),是一种最明显、最常见的关系。如果一个类的方法操纵另一个类的对象,我们就说一个类依赖于另一个类。应该尽量将相互依赖的类减至最少。用软件工程的术语来说,就是让类之间的耦合度最小。
聚合(aggregation),是一种具体且易于理解的关系。聚合关系意味着类A的对象包含类B的对象。
继承(inheritance),是一种用于表示特殊与一般关系的。一般而言,如果类A扩展类B(A继承B),类A不但包含从类B继承的方法,还会拥有一额外的功能。
描述类的关系时,习惯使用UML(Unified Modeling Language 统一建模语言)绘制类图
使用预定义类
使用预定义类,即在Java标准库中实例化一个已定义的类对象。
在Java中,不是所有类都有面向对象特征,如Math类,在程序中可以直接使用Math类中的方法,而不必关心具体的实现过程(这正是封装的关键所在),但遗憾的是,Math类只封装了功能,它不需要也不必隐藏数据。由于没有数据,因此也不必担心生成对象以及初始化实例域(不用考虑实例化一个具体对象)。
对象与对象变量
要想使用对西那个,就必须首先构造对象,并指定其初始状态,然后对对象应用方法。
在Java程序设计语言中,使用构造器(constructor)构造新实例。
Date birthday = new Date();(birthday为对象变量,new Date()为对象)
一个对象变量并没有实际包含一个对象,而仅仅引用了一个对象。在Java中,任何对象变量的值都是对存储在另外一个地方的一个对象的引用。new操作符的返回值也是一个引用。
Java类库中的LocalDate类
上一节中实例化的Date对象,遵循了世界上大多数地区使用的Gregorian阳历表示法,但是,同一时间点采用中国的农历表示和采用希伯来的阴历表示就很不一样,对于火星历来说就更不可想像了。
于是类库设计者决定将保存时间(时间)与给时间点命名(日历)分开。所以标准Java类库分别包含了两个类:一个是用来表示时间点的Date类,一个是用来表示大家熟悉的日历表示法的LocalDate类。
不要使用构造器来构造LocalDate对象。实际上,应当使用静态工厂方法(factory method)代表你调用构造器。
LocalDate.now(); //构造了一个新对象,表示构造这个对象时的日期。
更改器方法与访问器方法
只访问对象而不修改对象的方法称为访问器方法(accessor method),如String的toUpperCase方法,仅返回一个新字符串,而不改变原字符串的大小写。
调用后会修改对象的方法称为更改器方法(mutator method)
用户自定义类
如何设计复杂应用程序所需要的各种主力类(workhorse class),通常,这些类没有main方法,却有自己的实例域和实例方法。
要想创建一个完整的程序,应该将若干类组合在一起,其中只有一个类有main方法。
源文件名必须与public类的类名相同,一个源文件中只能有一个公有类(public class),但可以有任意数目的非公有类。
多个源文件的使用
除了一个源文件中放置多个类以外,程序员大多习惯一个类放在一个源文件中,对于这种原文件的编译,有两种方法:
1、使用通配符:javac Employee*.java 将编译所有以Employee开头的Java文件
2、仅编译含main方法的Java文件:javac EmployeeTest.jaav 若其中使用了其他类,会自动搜索相应类的class文件,若不存在,则编译相应的Java文件,而且若系统发现Java文件比class文件版本更新,将自动重新编译。
解析构造器
- 构造器与类同名(public className)
- 每个类可以有一个以上的构造器
- 构造器可以有任意多个参数
- 构造器无返回值
- 构造器总是伴随着new操作符一起调用
- 不要声明与实例域同名的变量,因为会屏蔽实例域变量,所以容易产生错误,da bu da
隐式参数和显式参数
显式(explicit)参数:在方法定义中显式地声明的参数,即方法名后面括号中的数值。
隐式(implicit)参数:出现在方法名前的类对象,包括this。
封装的优点
在有些时候,需要获得或设置实例域的值,因此,应该提供下面三项内容:
- 一个私有的数据域
- 一个公有的域访问器方法
- 一个公有的域更改器方法
但是注意不要编写返回引用可变对象的访问器方法。如Date对象。
class Employee{
private Date HireDay;
...
public Date getHireDay(){
return this.HireDay;
}
}
public class Frame{
public static void main(String[] args){
Employee em = new Employee();
Date d = em.getHireDay();
double seconds = ...;
d.setTime(d.getTime() - (long)seconds);
}
}
此处d.setTime()就将em的HireDay进行了更改,因为引用的是同一个地址,破坏了封装性。所以应该尽量避免返回这种可变对象。
基于类的访问权限
一个类的实例化对象可以访问同一个类实例化的所有对象的私有域。
私有方法
若一个计算过程需要或者最优选择是将之拆解,则拆解下来的计算步骤应当设置为私有方法,因为不希望其成为对外的程序接口。
final实例域
域(field),可以理解为字段、对象的属性、变量/常量等。
可以讲实例域定义为final,构建对象时必须初始化这样的域(即在执行完构造方法后,该域必须背初始化,不论是在域定义中显式得初始化,还是在实例化时通过参数进行初始化),且在其后的操作中无法被修改。
final修饰符大多应用于基本(primitive)类型域,或不可变(immutable)类的域
不可变的类:类中的所有方法都不会改变其对象,如String。
对于可变的类,可能会对使用者造成混乱,例如:
private final StringBuilder evaluation;
此处的final指的是,evaluation变量对象中的对象引用不会再指示其他的StringBuilder对象,不过该对象可以进行更改。(即可以添加新内容,evaluation.append("..."))
静态域与静态方法
静态域
如上文所述,域可以理解为字段、变量、对象的属性。
如果该域别定义为静态的,则类中只有一个这样的域,即不论实例化多少个对象,都公用这一个域,而不会像实例域,各自都有一份拷贝可供唯一修改。
静态域属于类,而不属于任何实例/对象。
静态常量
在Math类中,定义了一个静态常量:
public static final double PI = 3.14159265358979323846;
如果去掉了static,PI就成了一个实例域,需要通过对象调用,而现在只需要Math.PI即可调用,因为其属于类,不属于对象。
静态方法
静态方法是一种不能向对象实施操作的方法,如Math.pow();
可以认为静态方法是没有this参数的方法,即没有隐式的参数
静态方法不能访问实例域,因为不能操作对象,但可以访问自身类的静态域。
调用静态方法,不需要声明/实例化类。如Math.pow();
在以下两种情况下使用静态方法:
1、一个方法不需要访问对象的状态,其所需参数都是通过显式参数提供。
2、一个方法只需要访问的类的静态域。
工厂方法
很多静态方法都是通过工厂方法来调用的。如LocalDate和NumberFormat类使用静态工厂方法来构造对象:
LocalDate.now()、LocalDate.of()
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%
为什么NumberFormat不使用构造器(new)来完成这些擦操作呢?
- 无法命名构造器。构造器的名字必须与类名相同,但是,这里希望将得到的货币实例和百分比实例采用不同的名字。
- 当使用构造器时,无法改变所构造的对象类型。Factory方法将返回一个DecimalFormat类对象,这是NumberFormat的子类
方法参数
按值调用(call by value):方法接收的是调用者提供的值。
按引用调用(call by reference):方法接收的是调用者提供的变量地址。
按名调用(call by name)Algol使用的参数传递方式。现在已经过时。
一个方法可以修改传递引用所对应的变量值,但不能修改传递值调用所对应的变量值。
Java程序设计语言总是采用按值调用。即方法得到的是所有参数的一个拷贝,不能改变原始变量的值。
Java中方法参数的使用情况:
- 一个方法不能修改一个基本数据类型的参数(即数值型和布尔型)
- 一个方法可以改变一个对象参数的状态
- 一个方法不能让对象参数引用一个新的对象
对象构造
重载
StringBuilder messages = new StringBuilder();
StringBuilder messages = new StringBuilder("...");
以上特征展示了类StringBuilder拥有不止一个构造函数,这种特征叫做重载。
如果有多个方法有相同的名字,不同的参数数量或类型,便产生了重载。
编译器必须挑选出具体执行哪一个方法,若找不到匹配的参数,就会编译时报错。这个过程被称为重载解析(overloading resolution)
Java允许任何方法的重载,不只是构造器方法。因此,要完整的描述一个方法,需要指出方法名以及参数类型。这叫做方法的签名(signature)。注意:返回类型不是方法的签名之一,所以不能有两个方法名相同,参数类型相同但返回类型的方法。
默认域初始化
若在构造器中没有显式得给域赋予初值,则会被自动的赋为默认值:数值为0、布尔值为false、对象引用为null。
但这不是一个好的编程习惯。
无参数的构造器
大多数类都是用的是无参数的构造方法。
如果在编写一个类时没有编写构造器,那么系统就会提供一个无参数构造器。该构造器为所有的实例域设置为默认值。
如果类中提供了至少一个构造器,但不是无参数构造器,那么在实例化对象时若没有提供参数,编译将不通过。
显式域初始化
通过重载类的构造器方法,可以采用多种形式设置类的实例域的初始状态。确保每个实例域都可以被设置为一个有意义的初值,这是一种好的设计习惯。
在执行构造器之前,先执行赋值操作。除了可以赋予常量值,还能调用方法进行赋值。
class Employee
{
private static int nextId;
private String name = "";
private int id = assignId();
...
private static int assignId()
{
int r = nextId;
nextId++;
return r;
}
...
}
参数名
在编写构造器时,最好
1、使用‘a’+单词的形式命名;
2、使用单词命名,在给域赋值时使用this关键字;
调用另一个构造器
如果构造器的第一个语句形如this(...),这个构造器将调用同一个类的另一个构造器。下面是一个例子:
public Employee(double s)
{
// calls Employee(String, double)
this("Employee #" + nextId, s);
nextId++;
}
采用这种方式使用this关键字非常有用,这样对公共的构造器代码部分就只用编写一次即可。
初始化块(initialization block)
在一个类的声明中,可以包含多个代码块。
class Employee
{
private static int nextId;
private int id;
...
//object initialization block
{
id = nextId++;
}
public Employee(String n, double s)
{
name = n;
...
}
...
}
初始化块会在执行构造器之前执行。
如果对静态域的初始化代码比较复杂,可以使用静态初始化块。
static
{
Tandom generator = new Random();
nextId = generator.nextInt(10000);
}
对象析构与finalize方法
在析构器中,最常见的操作时回收分配给对象的存储空间。由于Java有自动的垃圾回收器,不需要人工回收内存,所以Java不支持析构器。
某些对象使用了内存之外的其他资源时,例如文件或使用了系统资源的另一个对象的句柄。在这种情况下,当资源不在需要时,将其回收和再利用显得十分重要。
可以为任何一个类添加finalize方法。将在垃圾回收器清楚对象之前调用。在实际应用中,不要依赖于使用finalize方法,因为不确定该方法是在什么时候执行。
包
类的导入
import关键字
静态导入
import语句还能导入静态方法和静态域。
import static java.lang.System.*;
就可以直接使用System类的静态方法,而不用再添加类名前缀。
文档注释
JDK包含一个工具javadoc,可以由源文件生成一个HTML文档。
如果在源代码中添加以专用的定界符/**开始的注释,那么可以很容易生成一个看上去具有专业水准的文档。
注释的插入
javadoc实用程序(utility)从以下几个特征中抽取信息
- 包
- 公有类与接口
- 公有的和受保护的构造器及方法
- 公有的和受保护的域
每个/**。。。*/文档注释在标记之后紧跟着自由格式文本(free-form text)。标记由@开始,如@author、@param。
在自由格式文本中,可以使用HTML修饰符。如<em>、<strong>、<img>等。
包注释
要想产生包注释,需要在每一个包目录下添加一个单独的文件。有两个选择:
- 提供一个以package.html命名的HTML文件。在标记<body></body>之间的所有文本都会抽取出来
- 提供一个以package-info.java命名的Java文件。这个文件必须包含一个初始的以/**和*/定界的Javadoc注释,跟随在一个包语句之后。他不应该包含更多的代码或注释。
类设计技巧
1、一定要保证数据私有。绝对不要破坏封装性。
2、一定要对数据初始化。
3、不要在类中使用过多的基本类型。即用其他的类代替多个相关的基本类型的使用。
4、不是所有的域都需要独立的域访问器和域更改器
5、将职责过多的类进行分解
6、类名和方法名要嗯构体现他们的职责
7、优先使用不可变的类。