这章来说,主要是来讲解面对对象程序设计,如何创建标准Java类库中的类对象,如何编写自己的类
面对对象的程序是由对象组成的,每一个对象都包含对用户公开的特定功能部分和隐藏的实现部分,程序中的对象大多来自标准库,也有自己定义的,opp是将数据放在第一步,然后再考虑操作数据的算法,面对对象的思想用于解决规模比较大的问题,要实现一个问题,可能是用到100个类,每个类里面有着20个方法,这样来做,我们程序员容易掌握,也容易找到bug
(1)类
类是构造对象的模板或是蓝天,我们可以把类看成饼干制造机器,而对象是看出饼干,类构造对象的过程就叫做创建类的实例。
Java编写的代码都位于某个类的内部
封装:它是与对象有关的一个重要概念,它把数据和行为组合再一个包里面,并对对象的使用者隐藏了数据的实现方式
实例域 :对象的数据
方法:操作数据的过程
对于每个特定的对象都有一组特定的实例域,这些值的集合就是当前对象的状态
完成封装的关键就是不能让类中的方法去直接访问其他类的实例域,程序仅仅通过对象的方法来和对象的数进行交互,封装赋予了对象黑盒的特征,这是提高重用性和可靠性的关键(这意味着一个类可以改变存储数据的方式,只要使用通用的方法,没那么其他对象就不会知道也不介意它发生变化)
继承:OOP的另一个原则就是会让用户自定义Java类变得容易,那就是可以通过扩展一个类来建立另一个类,事实上,所有的类都是来自于一个超类“Object”类,在扩展一个类的时候,这个扩展的类具有扩展的类的全部属性和方法,在新类中只要提供适用于这个新类的方法的数据域就可以
(2)对象
要使用OOP,那么就需要清除对象的三个主要特性
对象的行为:可以对对象添加哪些操作
对象的状态:当施加那些方法后,对象是如何响应的
对象的标识:如何辨别具有相同行为和状态不同的不同对象
对象状态的改变必须通过调用方法实现,如果不通过调用方法而状态改变的话,那么说明封装性遭到了破坏
对象的标识永远是不一样的,状态也常常存在着差异,每一个对象都拥有着唯一的身份,而对象的状态又影响着它的行为(例如订单:“已经送货的订单就应该拒绝那些具有增删条目的方法”)
(3)识别类
在设计面对对象的程序的时候,应该从设计类开始,然后再向每一个列里面添加方法
识别类的简单规则就是在分析问题的时候寻找名词,然后方法对应着动词
就像在订单处理系统中,有这样的名词 商品,订单,送货地址,这些就可能成为一个类,而动词 “发送”,“支付”,就可能成为一个方法
(4)类之间的关系
在类之间常有下面的关系:
依赖:uses - a 如果一个类的方法操控着另一个类的对象,那么我们就说一个类依赖于另一个类,就像Order类使用Account类,是因为Order对象需要访问Account类查看其信用,但是我们应该让相互依赖的类减少到最少,如果A不知道B的存在,那么它就不会关心B的任何改变
聚合:has - a 就像一个Order对象里面包含一些Item对象,这种关系意味着类A的对象包含类B的对象
继承:is - a 表示一种特殊与普通的关系,如RushOrder类由Order类继承而来,RushOrder类用于优先处理货物,以及计算运费不同方法,而其他方法都是从Order类继承来的
(5)使用预定类
在Java中,没有类就无法做成任何事情,但是不是所有的类都拥有面对对象的特征,就像Math类只封装了功能,它也不需要去隐藏数据,因此也不必担心生成对象和初始化实例域
1.对象与对象变量
要想使用对象,就必须先构造对象,并指定其初始值,然后对对象应用方法
在Java语言中,使用构造器来构造新实例,构造器是一种特殊的方法,用来构造和初始化对象
构造器的名字应和类名一样,因此Date类的构造器名为Date,要构造一个Date对象,需要在构造器前面加上new 操作符,如下面
new Date()//这个表达式构造了一个新的对象,这个对象被初始化为当前的日期和时间
需要的话可以将这个对象传给一个方法,或是可以将一个方法应用于这个对象
通常我们也希望构造的对象可以多次使用,因此需要将对象放在一个变量中 Date birthday = new Date();我们得区别对象和对象变量的区别,定义一个对象变量,它可以引用着Date类型的对象,如下面
Date deadline;//此时它不是一个对象,实际上没有引用对象,所以此时不能将任何Date方法应用于这个变量上面
想要让它可以使用方法那么就需要去初始化这个变量,有两个选择(1)用新构造的对象初始化这个变量:deadline = new Date(); (2)让这个变量引用一个已经存在的对象:deadline = birthday;现在两个变量引用同一个对象
注意:一个对象变量并没有实际包含一个对象,而只是仅仅引用一个对象而已,在Java中任何对象变量的值都是对存储在另一个地方的一个对象的引用,new操作符的返回值也是一个引用
Date deadline = new Date();//表达式new Date()构造了一个Date类型的对象,并且它的值是对新创建对象的引用,这个引用放在变量deadline中,因此可以将对象变量设置为 null,表明这个对象变量目前没有引用任何对象
deadline = null;
如果将一个方法作用于一个值为null的对象上,那么就会产生运行时错误,局部变量不会自动初始化为null,而必须通过new或是将他们设置为null进行初始化
2.java类库中的LocalDate类
Date类实例有一个状态,也就是特定的时间点,这里的时间指的是距离一个固定时间点的毫秒数,这个点就是 纪元,它是UTC时间1970年1月1日00:00:00,Date类提供的日期处理并没有太大的用途
类库的设计者决定将保存时间和给时间点命名分开,所以类库里面有两个类,一个是用来表示时间点的Date类,一个用来表示大家自己熟悉的日历表示法的LocalDate类
将时间与日历分开是一种很好的面对对象的设计,通常最后使用不同的类来表示不同的概念
不要用构造器来构造LocalDate类的对象,实际上应该使用静态工厂方法代表你使用构造器
如LocalDate.now()就会表示构造这个对象时的日期, LocalDate newYearsEve = LocalDate.of(1999,12,31)表示一个特定的日期的对象
有了对象,就可以使用方法来获得年月日,int year = newYearDate.getYear();\
在这边LocalDate封装了实例域来维护所设定的日期,类只对外面提供方法来服务
3.更改器方法和访问器方法
更改器方法:访问对象和修改对象的状态
访问器方法:访问对象但不修改对象的状态
(6)用户自定义类
之前我们所学的类都是带有一个main方法的这边我们来学习如何设置复杂的主力类,这些类没有main方法,却有自己的实例域和实例方法,然后如果想要创建一个完整的程序,那么就需要将若干个类组合在一起,其中却只有一个类有main方法
1在源文件里面,只有那个带有public类的类名和文件名一样,在源文件里面只能有一个公有类,但是了可以有任意的非公有类
2.多个源文件
一个源文件包含两个类,许多人都习惯将每一个类存在单独的文件中,如果喜欢这样组织文件的话,那么就有两种编译程序的方法:
一种是使用通配符调用Java的编译器
javac Employee*.java,这样所有与通配符比配的源文件都将被编译成类文件
一种是javac EmployeeTest.java
使用这种,虽然没有显示地编译Employee.java文件,但是当Java编译器发现EmployeeTest.java使用了Employee类时,那么就会查找名为Employee.class文件,如果没有找到,那么就会去找Employee.java,然后对它进行编译,更重要:如果Employee.java有更新,那么Java编译器就会自动地重新编译这个文件
3.解析类的内容
如果一个方法标为 public ,那么就是表示着任何类的任何方法都可以调用这些方法,而实例域正常都是标记为private这就意味着只有这个类本身的方法才可以来访问这些实例域
4构造器:
对于构造器来说构造器与类同名,在构造某个类的对象的时候,构造器就会运行,以便把实例域初始化为所希望的状态,构造器总是伴随着new操作符的执行被调用的,因此不能对一个已经存在的对象调用构造器来达到重新设置实例域的目的,构造器没有返回值,每个类可以有一个以上的构造器
警告:不要在在构造器中定义与实例域重名的局部变量,定义的话会导致无法设置那些局部变量
5.隐式参数和显示参数
方法用于操作对象以存取他们的实例域,下面是调用一个方法来设置新值
number007.raiseSalary(5)
对于上面的操作来说,将调用这个方法的对象的salary实例域设置为新值,raiseSalary方法有两个参数,第一个参数称为隐式参数(也有人把隐式参数称为方法的调用的目标或是接收者),是出现在方法名前面的Employee类对象,第二个参数是位于方法名后面的括号中的数值,这是一个显式参数
我们可以看出显式参数是明显列在方法声明中的,隐式参数没有出现在方法声明中,在每一个方法中都可以使用关键字this表示隐式参数如下面
public void raiseSalary(double byPercent)
{
double raise = this.salary*byPercent/100;
this.salary+=raise;
}
这样就可以将实例域和局部变量明显区别出来了
(在Java中,所有的方法必须在类的内部中定义,但是不表示他们是内联方法,是不是内联方法是Java虚拟机的任务)
6.封装的优点
如果一个域只是一个只读域,那么一旦在构造器中设置完成,那么就没有任何方法可以对它进行修改了,这样就可以确保这个域不会受到外界的破坏,如果某个域不是只读域,那么如果这个域出现问题,那么就可以去使用更改器方法去修改就可以
所以有时候需要获得或设置实例域的值,那么就应该提供下面
(
一个私有的数据域,
一个公有的域访问器方法
一个公有的域更改器方法
)
做到上面的行为后,就拥有下面的好处:(1)我们可以更改内部的实现,除了该类的方法之外不会影响其他的代码
如将存储名字的域改为 String firstName;String lastName;那么getName方法就应改为 返回first + " " + lastName;
上面的改变,其他程序都完全不可以看见 (2)更改器方法可以去执行错误检查,然而直接对域赋值将不会进行这些处理,如setSalary方法可以检查薪酬是不是会小于0
注意:不要编写返回引用可变对象的访问器方法,如下面会是一个错误
public Date getHireDay() {
return hireDay;
}
这边Date对象是可以变化(它拥有一个更改器)的,这一点就破坏了封装性
Employee harry = ...;
Date d = harry.getHireDay();
double tenYear = 10*365.25*24*60*60*1000;
d.setTime(d.getTime() - (long)tenYear);
出错的原因是:这边d和harry.hireDay引用同一个对象,更改器方法就会自动地更改这个雇员的私有状态
解决方法:
如果需要返回一个可变对象的引用,那么就首先需要对它进行克隆,对象clone是指存放在另一个位置上的对象的副本,下面是修改后的代码:
public Date getHireDay(){
return (Date)hireDay.clone();//如果需要返回一个可变数据的拷贝,那么应该使用clone
}
7.私有方法
在实现一个类的时候,由于公有数据非常危险,所以应将所有的数据都设置为私有的,然后对于方法来说,大多数方法都设置为公有的,但是在一些特殊的情况下面(如可能希望将一个计算代码划分为若干个独立的辅助方法,这些辅助方法不应该成为公有的接口的一部分,这是因为他们往往和当前的实现机制非常紧密,或是需要一个特别的协议,以及一个特别的调用顺序),哪儿买就应该将此设置为私有的方法 private
8.final实例域
可以将实例域设置为final ,构建对象时必须初始化这样的域,也就是说必须确保在每一个构造器执行后,这个域的值就被设置,并在后面的操作中不能对它进行修改,也就是没有修改器方法,final修饰符大都应用于基本类型域或是不可变的域(如String,每个方法都不会改变其对象)
9.静态域和静态方法
静态域:
如果将域设置为 static ,每一个类都只有一个这样的域,也就是说每一个对象对于所有的实例域都有自己的一份拷贝
class Employee {
private static int nextId = 1;//访问时可以直接使用Employee.nextId
private int id;
}
这边每一个雇员的对象都有一个自己的id域,但是这个类的所有实例都将共享一个nextId域,静态域是属于类的,不属于任何独立的对象
静态常量:
private static final double PI = 3.145926;
如果static被省了,那么PI就变成一个实例域,那么就需要通过Math类的对象来访问PI了,并每一个Math的对象,都有它自己的一份拷贝,前面说过每个类的对象都可以对共有域进行修改,所以就不要将域设置为public,但是公有常量却可以,因为设置为final,所以就不允许对它进行修改了
静态方法(一种不能向对象实行操作的方法):
如 Math.pow(x,a);
上面就没有使用到任何Math对象,也就是说没有隐式参数,可以认为静态方法是没有this参数的,Employee类的静态方法不能访问Id实例域,因为它不能操作对象,但是静态方法可以访问自身的静态域
public static int getNextId(){
return nextId;
}
可以通过类名来调用这个方法 Employee.getNext();(我们可以通过使用对象来调用静态方法,但是这种方式很容易产生误解,因为这个方法的计算结果和对象无关,我们我们最好使用类名来调用静态方法)
在下面的两种情况下使用静态方法:
(1)一个方法不需要访问对象的状态,其所需要的参数都是通过显式参数提供的(Math.pow())
(2)一个方法只需要访问类的静态域
10.工厂方法
静态方法还有一种用处,就是使用静态工厂方法来构造对象,如上面我们所了解的 LocalDate.now()和LocalDate.of()
不利用构造器来完成这种操作的原因:
无法命名构造器,因为构造器的名字需要和类名相同,但是我们有时需要使用不同的名字
当使用构造器时,我们无法改变所构造的对象类型,而使用构造方法,我们有时可以选择返回他们的子类类型
11.main方法
main方法不对任何对象进行操作,事实上,在启动程序的时候还没有任何对象,静态main方法将执行并创建程序所需要的对象(每一个类都可以有一个main方法,这是一个常用于对类进行单位测试的技巧)
(7)方法参数
在程序设计中,有一些关于参数传递给方法的一些术语
按值传递(call by value)表示方法接受到的是调用者提供的值
按引用传递(call by reference)表示方法接受到的是调用者提供的变量地址
一个方法可以修改传递引用所对应的变量值,但是不能修改传递值调用的所对应的变量值
Java总是按值调用的,也就是说方法得到的是所有参数值的一个拷贝,特别是方法不能修改传递给它的任何参数变量的内容
如
double percent = 10;
harry.raiseSalary(percent);
方法调用后,percent的值还是10
方法参数有两种类型:
基本数据类型和对象引用类型
从上面我们可以知道一个方法不可能修改一个基本数据类型的参数,但是对象引用作为参数就不一样了,如下面的操作就可以把薪金升高为两倍
public static void tripleSalary(Employee x) {
x.raiseSalary(200);
}
调用时
harry = new Employee(..);
tripleSalary(harry);
具体执行过程为:
1)x被初始化为harry值得拷贝,这里是一个对象得引用
2)raiseSalary方法应用于这个对象引用,x和harry同时引用得那个Employee对象得薪金都提高两倍
3)方法结束后,参数x不再使用,对象harry继续引用那个对象
这样实现一个改变对象参数状态的方法就完成了
但是Java对 对象 采用的是值调用,就像如果我们编写一个交换两个雇员对象的方法,如果Java对对象采用的是按引用调用,那么就可以交换达到交换数据的结果,但实际上没有,原来的变量还是引用在这个方法调用前的对像
总结java中方法参数的使用情况:
1)一个方法不能修改一个基本数据类型的参数
2)一个方法可以改变一个对象参数的状态
3)一个方法不能让对象参数引用一个新的对象
(8)方法构造
前面已经学习了编写简单的构造器来定义对象的初始化状态,但是由于对象构造十分重要,Java提供了多种编写构造器的机制
1.重载
如果多个方法有相同的名字,不同的参数,那么就产生了重载,编译器必须找出具体执行那个方法的参数类型与特定的方法来调用的值类型进行比配来选出相应的方法,找不到相对应的参数,那么就会产生编译时错误
方法签名:需要指出方法名和参数类型,返回类型不是方法签名的一部分,也就是说不能有名字相同,参数类型相同但是却返回不同类型值的方法
2.默认域初始化
如果在域中没有显式地给域赋值,那么就会自动地设置为默认值:数值0,布尔值flase,对象引用null,但是我们最好去初始化它,不然会影响可读性(如null,我们没初始化它,在后面去引用它的话就会报错)
域和局部变量的主要不同点:局部变量必须明确地去初始化它,不然会报错
3无参数的构造器
如果在编写一个类时没有编写构造器,那么系统就会自动提供一个无参数的构造器,这个构造器把所有的实例域设置为默认值,如果类至少提供了一个构造器,但是没有提供无参数的构造器,那么在构造对象时没有提供参数是不合法的,此时会报错
4.显式域初始化
通过重载类的构造方法可以采用多种形式设置类的实例域的初始化状态,确保不管怎样调用构造器,每个实例域都可以设置为一个有意义的值,这是一种很好的设计方法,但是我们也可以直接将一个值赋给任何域
private String name = " ";
在执行构造器之前,先执行赋值操作,当一个类的所有构造器都希望把相同的值赋给某个特定的实例域时,这种方法特别有效
初始值不一定就是常量值,可以调用方法来对域进行初始化
private int id = assignId();
private static int assignId(){
}
5.参数名
一种技巧:参数变量用同样的名字将实例域屏蔽起来。如果将参数名命名为salary,那么salary就将会引用这个参数,而不是实例域,但是可以使用this.salary的形式来访问实例域,this表示隐式参数,也就是所构造的对象
public Employee( String name,double salary) {
this.name = name;
this.salary = salary;
}
6.调用另外一个构造器
关键字this引用方法的隐式参数外,这个this还有另外一个含义
如果在构造器的第一个语句形如this(..)那么这个构造器就会调用另一个构造器
public Employee(double s) {
this("sss" + nextId,s);
nextId++;
}
使用这种方法很好用,这样对公共的构造器代码部分只编写一次就可以
7.初始化块
前面已经说了两种初始化数据域的方法1)在构造器中设置2)在声明中赋值,其实Java还有第三种机制,那就是初始化块
在一个类的声明中可以含有多个代码块,只要构造类的对象,这些块就会执行
如
class Emoloyee{
private static int nextId;
{
id = nextId;
nextId++;
}
}
在这个例子中,无论使用哪个构造器来构造对象,id的域都会在对象初始块中被初始化,先运行初始化块,然后才运行构造器的主体部分,不过这种机制通常没有必要,通常会把他们放在构造器中
下面是调用构造器处理的具体步骤:
1)所有数据域被初始化为默认值
2)按照在类声明中出现的顺序,依次执行所有的域初始化语句和初始化块
3)如果构造器的第一行调用第二个构造器,那么就执行第二个构造器的主题
4)执行这个构造器的主题
如果对类的静态域进行初始化的代码比较复杂,那么就可以使用静态的初始化块,把代码放入一个块中,并加上一个static
static{
Romdom generation = new Random();
nextId = generator.nextInt(100);
}
在类的第一次加载时就会进行静态域的初始化,所有的静态初始化语句和静态初始化块都将按照定义的顺序执行
注意:在Java7后,Java程序会先检查有没有一个main()
8.对象析构和finalize方法
Java有自动的垃圾回收器,不需要人工回收内存,所以Java不支持析构器
当然如果某些对象使用了内存以外的其他资源,那么当资源不再需要时将其回收和再利用就很重要了,我们可以为任何一个类添加finalize()方法,这个方法将在垃圾回收器清除对象之前调用,在实际上不要依赖这个方法来回收短缺的资源,因为我们不知道这个方法什么时候会被使用
如果某个资源需要在使用后就立刻关闭,那么就需要人工来管理,对象用完后就使用这个close()方法来完成相应的清除操作
(9)包
Java允许使用包来将类组织起来,标准的Java类库分布在多个包中,所有的标准的Java包都在Java和Javax包层次中,使用包的主要原因是保证类名的唯一性
建议用公司的因特网的域名以逆序的形式作为包名,并在不同的项目中使用不同的子包
1.类的导入
我们可以采用两种方式来访问另外一个包的公有类
1)第一种就是在每个类名之前添加完整的包名,如下面
java.time.LocalDate today = java.time.LocalDate.now();//这就太麻烦了
2)第二种是使用import语句
import语句是一种引用包含在包中的类的简明描述,一旦使用了import语句,使用类时就不需要写出整个包名了
这条语句是导入java.util包中所有的类 import java.util.*
然后就可以直接使用 LocalDate today = LocalDate.now();
还可以导入一个包中的特定类:import java.time.LocalDate;
注意:只能使用星号(*)来导入一个包,而不能使用import java.*来导入以java为前缀的所有包
大多数情况下,只要导入所需要的包,不必去过分理睬他们,但是在发生命名冲突时,就不能不注意包的名字了,如java.util和java.sql包中就都有日期类,如果使用 import java.util.* import java.sql
然后使用Date类时就会报错:Date date;
当无法确定程序用哪一个时,可以添加一个特定的import java.util.Date;
但是如果两个类都需要使用,那么就需要在每个类名前面添加上完整的包名了
java.util.Date dedline = new java.util.Date();
java.sql.Date today = new java.sql.Date();
2.静态导入
import语句不仅可以导入类,也可以增加导入静态方法和静态域的功能(但不推荐使用)
3.将类放入包中
要将一个类放入包中,就必须将包的名字放入源头件的开头,如下面
package com.horstmans,corejava//那么就会放置在com/horstmans/corejava目录中
public class Employee {
}
如果没有放置在package语句,那么这个源文件中的类就会被放置在一个默认包中(defaulf package)
编译器在编译源文件的时候不检查目录的结构,但是如果当包和目录不比配的话,那么虚拟机就找不到类,然后无法运行
4.包作用域
前面已经接触修饰符和private,标记public的部分可以被任意的类使用,标记private的部分可以只能被定义他们的类使用,如果没有指定public和private,那么这个部分就可以被同一个包中的所有方法访问(这对于类来说符合情理2的,但是变量就是不符合了,因为在之前如果将某些类放在 package java.awt;中的话,那么就会很容易将其他东西混入这个包中,不过现在jdk已经明确地表示用户不能自定义以Java开头的包)
可以使用包封装来解决这个问题
(10)类的设计技巧
1.一定要保证数据私有,有时候可能需要编写一个访问器方法或更改器方法,但是最好还是需要保证实例域的私有性
2.一定要对数据进行初始化,java不对局部变量进行初始化,但是会对对象的实例域进行初始化,但是最好不要依赖于系统的默认值,而是选择显示初始化所有的数据,具体的方式可以是提供默认值,或是在构造器中去设置默认值
3.不要在类中使用过多的基本类型,也就是说要用其他类来替代相关的基本类型的使用,这样会使得类更加容易理解和容易修改
如 用Address来替代下面的实例域
private String street;
private String city;
private String state;
private int zip;
4.不是所有的域都需要独立的域访问器和域更改器,如一旦构造了雇员的雇定时间,就不能再去修改它了
5.将职责过多的类进行分解,但也不要走极端,分出太多类
6.类名和方法名能够体现出他们的职责
命名类名最好使用一个名词,前面有形容词修饰的名词,对于方法来说,习惯是访问器方法用小写的get开头,更改器方法用set开头
7.优先使用不可变的类
不可变的:没有方法能够修改对象的状态,类似plugDays的方法并不是更改对象,而是返回状态已经修改的新对象
更改对象的问题在于,如果多个多个线程视图同时同时更新一个对象,那么就会发生并发修改更改,其结果是不能预测的,如果类是不可以变得,那么就可以安全地在多个线程中共享对象。