面向对象程序设计概述
面向对象的程序是由对象组成的,每个对象包含对用户公开的特定功能部分和隐藏的实现部分。
从根本上说,只要对象满足要求,不必关心其功能的具体实现过程。
与传统的结构化程序区别:
- 传统的结构化程序通过一系列过程(算法)来求解问题,算法是第一位,数据结构第二位
- OOP则是数据第一位,然后考虑操作数据的方法
类
相关概念
-
类:构造对象的模板或蓝图
-
封装:将数据和行为组合在一个包中,并对对象的使用者隐藏数据的实现方式
封装的关键:绝不能让类中的方法直接访问其他类的实例域
-
继承:拓展一个类来建立另外一个类的过程
如何识别类
分析问题时找名词和动词,名词对应类,动词对应方法
对象
三个主要特性
- 对象的行为:由可调用的方法决定
- 对象的状态:状态的改变必须通过调用方法实现——封装的体现
- 对象的标识:同一个类创建的实例,标识永远是不同的
类与类之间的关系
- 依赖(uses-a):一个类的方法操纵另一个类的对象,应尽可能避免这种关系
- 聚合(has-a):类A的对象包含类B的对象
- 继承(is-a):表示特殊与一般的关系
使用预定义类
对象与对象变量
-
构造器:特殊的方法,名字与类名相同,用于构造对象
new Date()
-
对象变量:引用一个对象,并不是包含一个对象,类似于指针
Date birthday = new Date();
局部变量不会自动初始化为null,必须通过调用new或设置为null进行初始化
更改器方法和访问器方法
- 更改器方法:调用后对象的状态改变
- 访问器方法:调用后对象的状态不变,只访问不修改
用户自定义类
一个源文件中只能有一个公有类,但可以有任意数量的非公有类
如果将每个类单独放在一个源文件中时,可以用通配符一起编译,也可以只编译共有类所在的源文件
关键字public:任何类的任何方法都可以调用
关键字private:只有本类中的方法可以访问
构造器
- 构造器与类同名
- 构造器总是伴随着new操作符的执行被调用,而不能对一个已存在的对象调用
- 不要在构造器中定义与实例域重名的局部变量
隐式参数与显式参数
number007.raiseSalary(5);
- 隐式参数:方法的调用者,不出现在方法的声明中,即上面调用代码中的number007,在方法声明时可用关键字
this
表示隐式参数 - 显式参数:明显地列在方法声明中的参数
封装的优点
要获取或设置实例域的值,应提供:
- 一个私有的数据域
- 一个公有的访问器方法——可以改变方法的内部实现,而不会影响其他代码
- 一个公有的更改器方法——可以执行错误检测
注:不要编写返回引用可变对象的访问器方法
例:
class Employee{
private Date hireDay;
public Date getHireDay()
{
return hireDay;
}
}
其中Data类对象是可变的,如果返回的hireDay改变,则会同时改变类中的私有数据域,破坏了封闭性
如果要返回一个可变对象的引用,则首先对它进行克隆
final实例域
-
应用于基本类型域、不可变类的域:相当于是常量,其值不能改变
不可变类:类中的每个方法都不会改变其对象
-
应用于可变域:相当于c中
const int*
指针,不可以指向其他的对象,但是修改它指向的对象
静态域与静态方法
静态域
关键字static,类的所有实例共享静态域,属于类,不属于任何一个独立对象,可以通过类名来访问
静态常量
public class Math
{
public static final double PI = 3.14;
}
可采用Math.PI
访问
如果没有static,则变成Math的实例域,需要Math类对象访问,并且每个对象都有一份拷贝
静态方法
静态方法是一种不能向对象实施操作的方法,不能访问实例域,因为只有对象才能访问实例域,但可以访问自身类中的静态域
以下两种情况下使用静态方法:
- 一个方法不需要访问对象状态,其所需参数都是通过显式参数提供,
Math.pow(x,a)
- 一个方法只需要访问类的静态域
关键词static含义:属于类但不属于对象的变量和函数
工厂方法
也属于静态方法,直接使用类名调用,用于构造对象
为何不用构造器?
- 无法命名构造器,该类不同种类的构造方法,要加以区分则使用工厂方法
- 使用构造器时,无法改变所构造的对象类型
main方法
main方法也是静态方法
main方法不对任何对象进行操作
每个类都可以有一个main方法,常用于对类进行单元测试
方法参数
- 按值调用:方法接收的是调用者提供的值
- 按引用调用:方法接收的是调用者提供的变量地址
Java总是按值调用,即方法得到的时所有参数值的一个拷贝
注意Java的对象引用
public static void swap(Employee x,Employee y)
{
Employee tmp=x;
x=y;
y=tmp;
}
这是不能交换x,y所引用的对象,只是交换了x,y的拷贝
总结:
- 一个方法不能修改一个基本数据类型的参数(数值型或布尔型)
- 一个方法可以改变一个对象参数的状态
- 一个方法不能让一个对象参数引用一个新的对象
对象的构造
重载
如果多个方法有相同的名字、不同的参数,则产生重载,返回类型不同不可以,即不能有两个名字相同、参数类型相同,但返回类型不同的方法
默认域初始化
如果没有在构造器中显式给域赋予初值,则会自动被赋为默认值(数值0,布尔值false,对象引用null)
区别于局部变量,局部变量必须被明确的初始化
无参数的构造器
类中没有构造器时,系统会提供一个默认的无参数构造器
类中一旦有构造器,则系统提供的构造器无效
显式域初始化
可以给任何域显式的赋予初值,这样的赋值会在构造器之前执行,而且赋值不一定是常数,可以调用方法进行初始化
class Employee
{
private static int nextId;
private int id = assignId();
private static int assignId()
{
int r = nextId;
nextId++;
return r;
}
}
参数名
参数变量如果和实例域的名称相同,则会覆盖实例域,此时要想访问实例域需要通过this
关键词
好的习惯:
在参数前加前缀”a“
public Employee(String aName,double aSalary)
{
name = aName;
salary = aSalary;
}
或者通过this
调用另一个构造器
如果构造器的第一个语句形如this(...)
,这个构造器将调用同一个类的另一个构造器
public Employee(double s)
{
this("Employee #"+nextId,s);
nextId++;
}
初始化块
设置一个代码块,在其中放入域初始化语句,则可在构造器执行之前对域进行初始化
调用构造器的具体步骤:
- 所有数据域被初始化为默认值
- 按照在类中出现的次序,依次执行所有域初始化语句和初始化块
- 如果构造器第一行调用了第二个构造器,则执行第二个构造器
- 执行这个构造器主体
一般通过提供一个初始化值初始化静态域,如果初始化语句比较复杂,则可以使用静态初始化块
包
- Java允许用包(package)将类组织起来,方便组织自己的代码
- 使用包主要原因是确保类名的唯一性
- 从编译器的角度看,嵌套包之间没有任何联系
- 包的命名习惯:以域名的倒序作为包名——保证包名的唯一性
类的导入
一个类可以使用所属包中的所有类,以及其他包中的公有类
如何访问公有类:
-
在类名前加上完整的包名
-
使用import导入相关的类,然后就可以直接使用类中的方法
注:import 可以用
*
导入多个类,但是不能导入多个包
如果导入的包中含有同名的类怎么办:
- 如果只需使用其中一个类,则再次import这个特定的类
- 如果两个类都要使用,则在使用前加上完整的包名
静态导入
import 不仅可以导入类,还能导入静态方法和静态域
注意:不能静态导入类!!
import static java.lang.Math.*;
这样就不用加类名前缀了,如sqrt(pow(x,2)+pow(y,2))
将类放入包中
要想将一个类放入包中,就必须将包的名字放在源文件的开头
package com.horstmann.coreJava;
对应的源文件会被放在com/horstmann/coreJava
目录下
包作用域
类、方法、变量:
- public:可以被任意类使用
- private:只能被定义它们的类使用
- 没有指定:可以被同一个包中的所有方法访问
类设计技巧
-
一定要保证数据私有
-
一定要对数据初始化
-
不要在类中使用过多的基本类型
用其他类代替多个相关的基本类型
-
不是所有的域都需要独立的域访问器和域更改器
-
将职责过多的类进行分解
-
类型和方法名要能体现它们的职责
-
优先使用不可变的类
更改对象的问题在于:如果多个线程试图同时更新一个对象,就会发生并发更改,其结果不可预料