第四章 对象和类
面向对象程序设计 OOP
面向对象的程序是由对象组成,每个对象包含对用户公开的特定功能部分和隐藏的实现部分。
对象 可以来自标准库 可以自定义
算法 + 数据结构 = 程序 要先确定如何操作数据,再决定如何组织数据(易于操作数据) 不是OOP
OOP 先考虑数据 再考虑操作数据的算法
适用解决规模较大的问题
类 类的实例化—对象
类 实例域(数据) + (操作数据的)方法
封装 绝不让类中的方法直接访问其他类的实例域 “黑盒“特征-----提高了重用性和可靠性
也就是在同样的处理方法下,只需要改变存储数据的方式就可以,不需要发生大的变化
类 可扩展 扩展后的新类包含原来类的全部属性和方法 只需再添加其他新方法和数据域就可以(原来类有的不必添加) ------------------------------- 继承(第五章)
对象
属性 + 方法
行为----------该对象可以做的动作、行为(通过方法调用)
状态----------“已送货“,”已付款“
标识----------标识永远不同
设计类: 类-----分析问题时,找名词即为类 找动词即为方法
例:订单处理系统中,有名词:项目、订单等------类 ,有动词:添加、取消、发送等------方法
类之间的关系
依赖 类A的方法操纵类S的对象 A依赖于S 应尽可能减少依赖(降低耦合度)
聚合 类A的对象包含类S的对象
继承 第五章 类A扩展类B 类A不但包含从类B继承的方法 还可以有一些新的方法、属性
可以用UML绘制关于类之间关系的类图
使用现有类
以Date类为例,学习一下 ,使用现有类,如何构造对象,如何调用类的方法
构造对象------>指定初始状态------>对对象施加方法------->使用对象
Date birthday = new Date(); //Date对象 构造+初始化
String s = new Date().toString(); //String对象
Date deadline; //构造 构造的,未进行初始化的对象不可以进行操作 .toString()等等
//初始化变量的两种方式:
deadline = new Date(); //用新构造的对象初始化变量
deadline = birthday; //引用一个已存在的变量
deadline = null; //将对象变量设置为null,表明该对象变量没有引用任何对象
//deadline.toString() 会运行出错
// 因为变量不会自动初始化为null,需调用new或将它们设置为nul进行初始化
注:任何对象变量的值都是对存储在另一地方对象的引用 对象变量也是,只是引用一个对象
Java类库中的GregorianCalendar类
有很多方法可参见API使用
Java类库包含:Date类(表示时间点的),GregorianCalendar类(日历表示法)
GregorianCalendar类扩展了一个通用的Calendar类(描述了日历的一般属性)
Date类中before和after方法分别表示一个时间点是否早于或晚于另一个时间点。today.before(birthday)或today.after(birthday)
Date类中的getDay、getMonth以及getYear等方法,不推荐使用
构造:GregorianCalendar gc =
new GregorianCalendar() 新对象
new GregorianCalendar(1999,11,31) 年月日特定日期午夜的日历对象
其中月份从0开始,11为12月;也可用常量Calendar.December
new GregorianCalendar(1999,Calendar.December,31,23,59,59)
更改器方法与访问器方法
日历:提供某个时间点的年、月、日等信息
查询时间点(日历)的信息-----------GregorianCalendar.get()
GregorianCalendar now = new GregorianCalendar();
int month = now.get(Calendar.MONTH);
int weekday = now.get(Calendar.DAY_OF_WEEK); //其他可参见API
设置时间点信息----------------GregorianCalendar.set()
deadline.set(Calendar.YEAR,2001); //年
deadline.set(Calendar.MONTH,Calendar.APRIL); //月
deadline.set(Calemdar.DAY_OF_MONTH,15); //日
deadline.set(2001,Calendar.APRIL,15); //年、月、日
//为给定的日期对象添加天数、星期数、月份等等
deadline.add(Calendar.MONTH,3); //若传递的是负数,日期就向后移
get方法仅查看应返回对象的状态 --------------访问器方法(仅访问)
set、add方法可对对象的状态进行修改 -----------变更器方法(修改了内容)
Date time = calendar.getTime(); //获取日历对象所表示的时间点
calendar.set(Time); //设置日历对象所表示的时间点
这些方法是在进行GregorianCalendar和Date类之间的转换。
GregorianCalendar calendar = new GregorianCalendar();
GregorianCalendar calendar1 = new GregorianCalendar();
Date hireDay = calendar1.getTime(); //将GregorianCalendar转换为Date
calendar.setTime(hireDay); //设置calendar中的时间点为Date类型的hireDay
int year = calendar.get(Calendar.YEAR); //获取calendar中的年份 赋值给year
EmployeeTest.java
import java.util.*;
public class EmployeeTest{
public static void main(String[] args){
//构造 Employee类的数组
Employee[] staff = new Emploee[3];
//对employee类的对象赋值
staff[0] = new Employee("Carl",75000,1987,12,15);
staff[1] = new Employee("Harry",50000,1989,10,1);
staff[2] = new Employee("Tony",40000,1990,3,15);
//用raiseSalary方法提高每个雇员的薪水5%
for(Employee e : staff)
e.raiseSalary(S);
//打印每个雇员的信息
for(Employee e: satff)
System.out.println("name = "+e.getName() +",salray = "+e.getSalary()+",hireDay = "+e.gatHireDay());
}
}
class Employee{
//构造Employee类
public Employee(String n, double s; int year, int month, int day){//构造器
name = n; salary = s;
GregorianCalendar calendar = new GregorianCalendar(year,month-1,day); hireDay = calendar.getTime();
}
//4个方法 public意味着任何类的任何方法都可以调用这些方法
public String getName(){
return name;
}
public double getSalary(){
return salary;
}
public Date getHireDay(){
return hiraDay;
}
public void raiseSalary(doble byPercent){
double raise = salary * byPercent/100;
salary += raise;
}
private String name; private double salary; private Date hireDay;}
构造器
public Employee(String n, double s; int year, int month, int day){//构造器
name = n;
salary = s;
GregorianCalendar calendar = new GregorianCalendar(year,month-1,day);
hireDay = calendar.getTime();
}
- 构造器与类同名
- 每个类可以有一个以上的构造器
- 构造器可以有0个、1个或1个以上的参数
- 构造器没有返回值
- 构造器总是伴随着new操作一起调用
隐式参数与显式参数
public void raiseSalary(doble byPercent){
double raise = salary * byPercent/100;
salary += raise;
}
salary为隐式参数 byPercent为显式参数
在每个方法中,关键字this表示隐式参数。因此上面事例可变为:
public void raiseSalary(doble byPercent){
double raise = this.salary * byPercent/100;
this. salary += raise;
}
封装优点
public String getName(){
return name;
}
public double getSalary(){
return salary; }
public Date getHireDay(){
return hiraDay;
}
只返回实例域值,称为域访问器
获取或设置实例域的值:
- 一个私有的数据域 private
- 一个公有的域访问器方法 getXXX
- 一个公有的与更改器方法 setXXX
一个方法可以访问所属类的所有对象的私有数据
私有方法
public -----> private 数据一般都为私有的 方法可以公有,也可以私有
Final实例域
final 基本数据类型域 或 不可变类的域
private final Date hiredate;
意味着存储在hiredate变量中的对象引用在对象构造之后不能被改变。
任何方法都可以对hiredate引用的对象调用setTime更改器。
静态域与静态方法
static
静态域
每个类中只有一个这样的域:假定给每个雇员赋予惟一的标识码
class Employee{
......
private int id; //实力域id
private static int nextInt = 1; //静态域nextId
}
也就是:每个雇员都有自己的id域,但这个类的所有实例将共享一个nextId域。即使没有一个雇员对象,静态域nextId也存在。它属于类,不属于任何独立的对象。
静态常量
Math类中定义一个静态常量:
public class Math{
......
public static final double PI =3.14159265358979323845; //静态常量PI
......
}
使用Math.PI获得该常量
静态方法
静态方法不能对对象进行操作。即无隐式参数(无this)。
虽无法访问实力域,但是可访问自身类的静态域。
例:
public static int getnextId(){ //可省略static 因为本身就是访问静态域
return nextId; // 访问静态域
}
需通过Employee类对象调用该方法 Employee.getNextId()
Factory方法-----NumberFormat类
产生不同风格的格式对象
NumberFormat currentFormatter = NumberFormat.getCurrencyInstance();
NumberFormat percentFormatter = NumberFormat.getPercentInstance();
double x = 0.1;
System.out.println(currencyFormatter.format(x)); //打印50.10
System.out,println(percentFormatter.format(x)); //打印 10%
不用构造器而用NumberFormat类的原因:
1、无法命名构造器 实例之间希望采用不同名字达不到要求
2、构造器无法改变所构造的对象类型 Factory方法返回一个NumberFormat子类–DecimalFormat类对象
Main方法 -------静态方法
启动程序时还没有任何一个对象,静态的main方法将执行并创建程序所需要的对象
在一个小的单元中可进行单元测试: 运行Employee
整个项目的测试:运行EmployeeTest
import java.util.*;
public class EmployeeTest{
public static void main(String[] args){
//构造 Employee类的数组
Employee[] staff = new Emploee[3];
//对employee类的对象赋值
staff[0] = new Employee("Carl",75000,1987,12,15);
staff[1] = new Employee("Harry",50000,1989,10,1);
staff[2] = new Employee("Tony",40000,1990,3,15);
//用raiseSalary方法提高每个雇员的薪水5%
for(Employee e : staff)
e.raiseSalary(S);
//打印每个雇员的信息
for(Employee e: satff)
System.out.println("name = "+e.getName() +",salray = "+e.getSalary()+",hireDay = "+e.gatHireDay());
}
}
class Employee{
//构造Employee类
public Employee(String n, double s; int year, int month, int day){//构造器
name = n; salary = s;
GregorianCalendar calendar = new GregorianCalendar(year,month-1,day);
hireDay = calendar.getTime();
}
//4个方法 public意味着任何类的任何方法都可以调用这些方法
public String getName(){
return name;
}
public double getSalary(){
return salary;
}
public Date getHireDay(){
return hiraDay;
}
public void raiseSalary(doble byPercent){
double raise = salary * byPercent/100;
salary += raise;
}
//单元测试 main
public static void main(){
Employee e = new Employee("Harry",50000);
System.out.println(e.getName()+" "+e.getSalary());
}
private String name;
private double salary;
private Date hireDay;
}
方法参数
参数传递:1、传递的是值(值调用)、布尔类型(true、false)
2、传递的是变量地址(引用调用)
传递的值不能改变其内容。 只是将值传递过来。
特别讲一下 交换两个雇员对象的方法
public static void swap(Employee x,Employee y){
Employee temp = x;
x = y;
y = temp;
}
//main方法:
public static void main(String[] args){
Employee a = new Employee("Alice",...);
Employee b = new Employee("Bob",...);
swap(a,b);//最终白费力气,什么都没变 a、b仍用之前的
}
原因在于Java程序设计语言对对象采用的不是引用调用,而是值传递
该方法只是改变了x和y(a、b的拷贝),没有改变变量a,b
在Java程序设计语言中,方法参数的使用情况:
- 一个方法不能修改一个基本数据类型的参数(即数值型和布尔型)
- 一个方法不能改变一个对象参数的状态
- 一个方法不能实现让对象参数引用一个新的对象
ParamTest.java
对象构造
多种编写构造器的方式:
重载
例如:GregorianCalendar有多个构造器:可以用
GregorianCalendar today = new GregorianCalendar();
//或者
GregorianCalendar deadline = new GregorianCalendar(2099.Calendar,DECEMBER,31);
这种叫做重载 :有相同的名字,但是参数不同(参数类型、个数不同,且都要指明) 指出方法名以及参数类型------方法的签名
编译器根据所提供的构造方法找到与其对应的;若编译器找不到匹配的或者找到多个,就会产生编译时错误(该过程为重载解析)
注意:返回类型不是方法签名的一部分(不能有两个名字相同、参数类型也相同却返回不同类型值的方法)
默认域初始化
若构造其中没有显式地进行赋值,那么会自动度为默认值:数值=0,布尔值=false,对象引用=null
不明确地对域进行初始化,会影响代码可读性
默认构造器
没有参数的构造器
例:
public employee(){
name ="";
salary =0;
hireDay = new Date();
}
若在类中没有提供任何一个构造器,则系统会自动提供一个默认构造器,所有实力域的值均为默认值
注意:若类中提供了一个及以上构造器,但是没有编写默认的构造器(也就是无参的构造器),而在构造对象时还想使用默认的,则不合法,会产生错误
若编程人员希望所有域被赋值为默认值 则可使用
public ClassName(){
}
显示域初始化
在调用构造器时,应到确保不管怎样调用,每个实例域都可以被设置为一个有意义地初值。
赋初值:初始值是常量的情况
class Employee{
......
private String name ="Lisa";
......
}
初始值可以不是常量。调用方法对域进行初始化
class Employee{
......
static int assignId(){
int r = nextId;
nextId++;
return r;
}
......
private int id = assignId(); //每个雇员有一个id域,都会调用assignId()
}
参数名
参数命名时技巧:
1、在参数前加上前缀或者后缀之类的 , _xxx,(固定字母)xsalary,msalary
2、参数变量用同样的名字将实例域屏蔽起来
例:
public Employee(String name,double salary){
this.name = name; //this是隐式参数
this.salary =salary;
}
调用另一个构造器
this除了是隐式参数以外,还可以调用同一个类的另一个构造器
例:
public Employee(double s){
this("Employee #"+nextId,s); //Employee()调用Employee(String,double)
nextId++;
}
当调用new Employee(60000)时,Employee(double)构造器将调用Employee(String,double)构造器。
初始化块
class Employee{
public Employee(){
}
......
private static int nextId;
private int id;
private String name;
private double salary;
......
//初始化块
{
id = nextId;
nextId++;
}
}
先运行初始化块,然后再运行构造器的主体部分
调用构造器的具体处理步骤:
1、所有数据域被初始化为默认值(0、false或null)
2、按照在类声明中出现的次序,依次执行所有域初始化语句和初始化块
3、若构造器第一行调用了第二个构造器,则执行第二个构造器的主体
4、执行这个构造器的主体
对类的静态域进行初始化的代码稍复杂,可使用静态的初始化块:
static{
Random generator = new Random();
nextId = generator.nextInt(10000);
}
对象析构与finalize方法
析构器最常见操作:回收分配给对象的存储空间
由于Java有自动的垃圾回收器,不需要人工回收内存,所以Java不支持析构器
若特殊情况(资源不再需要,将其回收和再利用),可为类添加finalize方法—将在垃圾回收器清除对象之前调用。实际应用中,因很难知道什么时候才能调用,故而不要用这个方法回收短缺资源 不鼓励使用
若某资源需使用完毕后立即关闭,则需要人工应用类似dispose或close的方法来完成相应的清理操作。
注意:若该类使用了这样的方法,对象不再使用时一定要调用它
包
包 用于存放类 确保类名的唯一性 每个包之间独立
在包中定位类 是 编译器的工作。
优点:便于组织代码,当与被人代码合并时方便分开管理
Java类库,包括java.lang、java.util和java.net等 具有层次结构
所有标准的Java包都处在java和javax包层次中
一个类可使用所属包中的所有类与其他包中的公有类
类导入-------访问另一个包中的公有类
方法一: 在每个类名之前添加完整的包名
java.util.Date today = new java.util.Date();
方法二: 使用import语句
import语句 : 一种引用(含有类的简明描述) 可导入一个特定的类或者整个包 位于源文件的顶部(但位于package语句的后面)
import java.util.*; //导入java.util包中的所有类
import java.util.Date; //导入一个包的特定类
指出特定的类方便代码理解,知道加在了哪些类
Eclipse中,菜单选项Source -> Organize Imports 自动扩展指定的导入列表
只能使用星号* 导入一个包
不能使用 import java.* 或者 import java.*.*导入以java为前缀的所有包
大多情况下,只导入所需要的包即可。但在发生命名冲突的时候,必须要注意包的名字
例:java.util和Java.sql包中都有日期Date类
import java.util.*;
import java.sql.*;
Date today; //编译错误 编译器无法确定程序使用的是哪一个Date类
解决办法: 增加一个特定的import语句
import java.util.Date;
若是这两个Date类都要使用,则在每个类名的前面加上完整的包名
java.util.Date deadline = new java.util.Date();
java.sql.Date today = new java.sql.Date(.......);
静态导入
导入静态方法和静态域 添加在源文件的顶部
import static java.lang.System.*;
//System类的静态方法和静态域
out.print("Goodbye!!"); //System.out.print
exit(0); //System.exit
导入特定的方法或域
import static java.lang.System.out;
静态导入的实际应用:
- 算术函数 Math类 使用算术函数
sqrt(pow(x,2)+pow(y,2)) //清晰一些
Math.sqrt(Math.pow(x,2)+Math.pow(域,2));
- 笨重的变量 大量带有冗长名字的常量
if(d.get(DAY_OF_WEEK) == WONDAY) //容易一些
if(d.get(Calendar,DAY_OF_WEEK) == Calendar.MONDAY)
将类放入包中
包名需要放在源文件的开头 若放在默认包中 则无需声明
com.horstmann.corejava包中所有原文件放在子目录com/horatmann/corejava中。编译器将类文件也放在相同的目录结构中。
Employee.java与Employee.class放在com.horstmann.corejava包中
PayrollApp.java与PayrollApp.class放在com.mycompany包中
两个在不同的包中
从基目录编译和运行类,包含com目录:
javac com/mycompany/PayrollApp.java
编译器对文件操作
java com.mycompany.PayrollApp
Java解释器加载类
包作用域
在默认情况下,包不是一个封闭的实体
任何人都可以向包中添加更多的类 在使用时要多加注意 正确使用
类路径CLASSPATH 告诉编译器package的位置
类的路径必须与包名匹配。
另外:类文件也可以存储在jar文件中。一jar文件中可包含多个压缩形式的类文件和子目录(节省空间有改善性能)
有关于创建jar文件的详细内容见第十章。。。。
为使类可被多个程序共享,要做到:
- 把类放到一个目录中 树状结构
- 将jar文件放在一个目录中
- 设置类路径 ----- 所有包含类文件的路径的集合
CLASSPATH环境变量 ---------告诉编译器package的位置
PATH环境变量 ---------指定命令搜索路径
例: javac编译程序时,到PATH所指路径下查找能否找到相应命令程序
JAVA_HOME环境变量 -------指向jdk安装目录
例:Eclipse/NetBeans/Tomcat等软件就是通过搜索JAVA_HOME变量来找到并使用安装好的jdk
详细设置 这里不再叙述
文档注释
/** 开始
这种注释的方式将代码和注释保存在同一文件中,在代码修改时,重新运行javadoc就可轻而易举保持二者的一致性
注释的插入
javadoc实用程序(utility)在下面特性中抽取信息:
- 包
- 公有类与接口(接口见第六章)
- 公有的和受保护的方法(受保护特性见第五章)
- 公有的和受保护的域
注释应当放在所描述特性的前面。以/**开始,*/结束
/**…*/中添加 @+自由格式文本 概要性语句
(自由格式文本可使用HTML修饰符,
例:强调<em>...</em>
设置“打印机”字体<code>...</code>
着重强调<strong>...</strong>
图像<img>...</img>
等) 切勿使用<h1>
或<hr>
,会与文档的格式产生冲突。
javadoc自动抽取这些概要性语句形成概要页。
类注释 ----- 放在import语句之后,类定义之前
/**
*
*
*/
public class Employee{
}
或者
/**
//没必要在每一行的开始都用星号*
*/
public class Employee{
}
但是: 大部分IDE提供了自动添加星号,并当注释行改变时自动重新排列这些星号的功能
方法注释-------放在所描述方法前
除通用标记外,还可使用:
- @param variable description
对当前方法的参数部分添加一个条目,可占据多行,可使用HTML标记
一个方法的所有@param标记必须放在一起 - @return description
对当前方法添加返回部分,可跨越多行,可使用HTML标记 - @throws class description
添加一个注释,表示该方法可能抛出异常(异常详细内容见十一章)
域注释
只需对公有域(通常指静态常量)建立文档
例:
/**
* The "Hearts" cars suit
* /
public static final int HEARTS = 1;
通用注释
可用在类文档的注释中:
- @author name
产生一个作者条目。可使用多个@author标记,每个@author标记对应一个作者 - @version text
产生一个版本条目。text可以是对当前版本的任何描述
可用于所有文档的注释中
- @since text产生一个始于条目。text可以是对引入特性的版本描述
例:@since version 1.7.1 - @deprecated text
对类、方法或变量添加一个不再使用的注释。text中给出取代意见
例:@deprecated Use<code>
setVisible(true)</code>
instead
超级链接:连接到javadoc文档的相关部分或外部文档
-
@see reference------------可用于类中,也可用于方法中
例1:
@see com.horstmann.corejava.Employee#raiseSalary(double)
提供类、方法或变量的名字。 可省略包名、类名,链接将定位于当前包、当前类。
注意: 一定要使用#,不要使用. javadoc实用程序容易混淆例2:
@see<a href="www.horstmann.com/corejava.html">
The core Java home page</a>
可超链接到任何URL
也可简写为
@see “Core Java 2 volume 2”
可为一个特性添加多个@see标记,但必须将其放到一起
- 可在注释中任何位置放指向其他类或方法的超级链接,以及插入个人专用标记
例:{@link package.class#feature label}
特性描述规则与@see一样
包与概述注释
包注释: 在每个包目录中添加一个单独的文件
- 方法一
给一个以package.html命名的HTML文件。在标记<body>
…<body>
之间的所有文本都会被抽取出来 - 方法二
给一个以package-info.java命名的Java文件,该文件必须包含一个/*和/界定的javadoc注释,跟随在一个包语句之后。不应该包含更多代码或注释。 - 方法三
为所有的源文件提供一个概述性的注释,放在一名为overview.html文件中,文件位于包含所有原文件的父目录中。<body>
…</body>
之间的文本将被抽出来。用户从导航栏中选择“overview”时,会显示注释。
注释的抽取
HTML文件存放于目录docDirectory下
- 切换到想要生成文档的源文件目录
- 若是一个包,运行javadoc -d docDirectory nameOfPackage
多个包生成文档,运行。。。。。。。。。。nameOfPackage1 nameOfPackage2 …
文件在默认包,运行javadoc -d docDirectory *.java
不可省略-d docDirectory,否则HTML文件会被提取到当前目录下,会造成混乱
可使用多种命令行对javadoc程序进行调整:
- -author 和-version在包含@author和@version的文档中调整
- -link :为标准类添加超链接
例:javadoc -link http://java.sun.com/javase/6/docs/api*.java
所有标准类库会自动连接到Sun网站的文档 - -linksource :每个源文件被转换为HTML,无颜色编码,有行编号,且每个类和方法都转变为指向源代码的超链接
类设计技巧
-
一定将数据设计为私有
不要破坏封装性
可编写访问器方法getXXX或者更改其方法setXXX,以保持实例域的私有性 -
一定要对数据初始化
Java不对局部变量进行初始化,但对对象的实例域进行初始化。
不要依赖于系统提供的默认值,程序员应设置默认值或在所有构造器中设置默认值 -
不要在类中使用过多的基本数据类型
用其他类代替多个相关的基本数据类型的使用。 -
不是所有的域都需要独立的域访问器和域更改器
-
使用标准格式进行类的定义
类内容顺序:
1 公有访问特性部分
2 包作用域访问特性部分
3 私有访问特性部分每一部分的顺序:
1 实例方法
2 静态方法
3 实例域
4 静态域
每一部分要保持一致 顺序可调换(域 和 方法之间可调换)
- 将职责过多的类进行分解
”复杂的“分解为多个”简单的“ 。不要走极端,太简单,太复杂
一个类实现两个独立的概念:一副牌(含有shuffle方法和draw方法)+一张牌(含有查看花色和面值的方法)Card类-----表示单张牌
public class CardDeck{
public CardDeck(){}
public void shuffle(){}
public Card getTop(){}
public void draw(){}
private Cad[] cards;
//可使用查看花色、面值(Card类中)的方法 以及在本类中的方法
}
public class Card{ //查看花色、面值
public Card(int aValue,int aSuit){}
public int getVaule(){}
public int getSuit(){}
private int value;
private int suit;
}
- 类名和方法名要能体现出其职责
与变量应该有一个能反映其含义的名字一样,类也应如此。
养成良好的命名习惯 :名词Order 、形容词+名词RushOrder、动名词+名词BillingAddress 等等
访问器方法 getXXX ,更改器方法 setXXX
2018/10/8 第四章结束。。。。