1.面向对象程序设计概述
面向对象程序设计(简称OOP),Java是完全面向对象的。
面向对象的程序是由对象组成的,每个对象包含对用户公开的特定功能部分和隐藏的实现部分。
在OOP中,不必关系对象的具体实现,只要能够满足用户的需求即可。
类
类(class)是构造对象的模版或蓝图。由类构造(construct)对象的过程被称为创建类的实例(instance)。
封装(encapsulation,有时称为数据隐藏),将数据和行为组合在一个包中,并对对象的使用者隐藏了数据的实现方式,对象的数据被称为实例域(instance field),操纵数据的过程成为方法(method)。
实现封装的关键在于绝对不能让类中的方法直接地访问其他类的实例域,程序仅通过对象的方法与对象数据进行交互。
OOP的另一个原则,可以通过扩展一个类来建立另一个新的类。事实上所有Java类都源自于一个超类,Object。扩展后的类具有所扩展的类的全部属性和方法。
对象
对象的三个特征:
对象行为(behavior),可以对对象施加哪些操作,或可以对对象施加哪些方法
状态(state),当施加那些方法时,对象如何响应
标识(identity),如何辨别具有相同行为与状态的不同对象
每个对象都保存着描述当前特征的信息,这就是对象的状态。对象的状态并不能完全描述一个对象,每个对象都有一个唯一的身份(identity)。做为一个类的实例,每个对象的标识永远是不同的,状态常常也存在着差异。
识别类
识别类的简单规则是在分析问题的过程中寻找名词,而方法对应着动词。
类之间的关系
常见关系有:
依赖(“uses-a”),一个类的方法操作另一个类的对象,我们就说一个类依赖于另一个类,尽可能将相互依赖的类减至最少,如果类A不知道B的存在,就不用关心B的任何变化,用软件工程术语来说就是,让类之间的耦合度最小。
聚合(“has-a”),聚合关系意味着类A包含类B的对象
继承(“is-a”)
2.使用预定义类
Math类只封装了功能,不需要也不必隐藏数据,由于没有数据,因此也不必担心生成对象以及初始化实例域。
对象与对象变量
想使用对象就必须构造对象,并制定其初始化状态。
Java中使用构造器(constructor)构造新实例,构造器是一种特殊的方法,用来构造并初始化对象。
在对象与对象变量之间存在着一个重要区别:
Data deadline;
定义了一个对象变量deadline,它可以引用Date类型的对象。但是变量deadline不是一个对象,实际上也没有引用对象,此时不能将任何Date方法应用于这个变量上。
必须初始化变量deadline,有两个选择:
1.用构造器的对象初始化这个变量
2.引用一个已存在的对象
一个对象变量并没有实际上包含一个对象,而仅仅引用一个对象。
在Java中,任何对象变量的值都是对存储在另外一个地方的一个对象的引用。new操作符的返回值也是一个引用。
Data deadline = new Date();
有两个部分。表达式new Date()构造了一个Date类型的对象,并且它的值是对新创建对象的引用,这个引用存储在变量deadline中。
可以显示地将对象变量设置为null,表明对象变量没有任何引用,如果将一个方法应用于一个值为null的对象上,那么就会产生运行时错误。
Date deadline = new Date();
deadline = null;
String s = deadline.toString(); // NullPointerException
Java类库中的LocalDate类
Date类是用距离一个固定时间点的毫米数(可正可负)表示,这个点就是所谓的纪元(epoch),它是UTC时间1970年1月1日00:00:00。UTC是Coordinated Universal Time的缩写,是一种具有实践意义的科学标准时间。
Date可以满足大多数地区的阳历表示法,但同一时间点采用中国的农历就不一样了,所以类库的设计者决定将保存时间与给时间点命名分开。一个表示时间点的Date类,另一个采用日历表示法LocalDate类。
不要使用构造器构造LocalDate类的对象,应当使用静态工厂方法(factory method)代表调用了构造器:
LocalDate.now();
可提供年、月、日来构造对应一个特定日期的对象:
LocalDate newYear = LocalDate.of(2019, 10, 23);
System.out.println(newYear.getYear());
System.out.println(newYear.getMonthValue());
System.out.println(newYear.getDayOfMonth());
看起来没多大意义,因为这正是构造对象使用的值,不过有时某个日期是计算出来的:
LocalDate aThousandDaysLater = newYear.plusDays(1000);
LocalDate类封装了实例域来维护所设置的日期。如果不查看源代码,就不可能知道类内部的日期表示。当然,封装的意义在于,类对外提供的方法。
实际上Date类页游getDay等方法,然而并不推荐使用,当类库设计者意识到某些方法不应该存在时,就会把它标记为不鼓励使用。
更改器方法与访问器方法
plusDays方法生成了一个新的LocalDate对象,然后赋值给aThousandDaysLater变量,原来的对象不做任何变动。我们说plusDays没有更改调用这个方法的对象。
GregorianCalendar someDay = new GregorianCalendar(2019, 10, 23);
someDay.add(Calendar.DAY_OF_MONTH, 1000);
System.out.println(someDay.get(Calendar.YEAR));
System.out.println(someDay.get(Calendar.MONTH) + 1);
System.out.println(someDay.get(Calendar.DAY_OF_MONTH));
GregorianCalendar.add方法是一个更改器方法(mutator method)。调用这个方法后,someDay对象的状态会改变。
相反的,只访问对象而不修改对象的方法称为访问器方法(accessor method)。
public class ConstructorTest {
public static void main(String[] args) {
LocalDate date = LocalDate.now();
int month = date.getMonthValue();
int today = date.getDayOfMonth();
date = date.minusDays(today - 1);
DayOfWeek weekday = date.getDayOfWeek();
int value = weekday.getValue();
System.out.println("Mon Tue Web Thu Fri Sat Sun");
for (int i = 1; i < value; i++) {
System.out.print(" ");
}
while (date.getMonthValue() == month) {
System.out.printf("%3d", date.getDayOfMonth());
if (date.getDayOfMonth() == today) {
System.out.print("*");
} else {
System.out.print(" ");
}
date = date.plusDays(1);
if (date.getDayOfWeek().getValue() == 1) {
System.out.println();
}
}
if (date.getDayOfWeek().getValue() != 1) {
System.out.println();
}
}
}
Mon Tue Web Thu Fri Sat Sun
1 2 3 4 5 6
7 8 9 10 11 12 13
14 15 16 17 18 19 20
21 22 23* 24 25 26 27
28 29 30 31
3.用户自定义类
主力类(workhorse class),这些类没有main方法,却有自己的实例域和实例方法。
Employee类
public class EmployeeTest {
public static void main(String[] args) {
Employee[] staff = new Employee[3];
staff[0] = new Employee("Carl Cracker", 75000, 1987, 12, 15);
staff[1] = new Employee("Harry Hacker", 50000, 1989, 10, 1);
staff[2] = new Employee("Tony Tester", 40000, 1990, 3, 15);
for (Employee e : staff) {
e.raiseSalary(5);
}
for (Employee e : staff) {
System.out.println("name=" + e.getName() + ",salary=" + e.getSalary() + ",hi reDay="
+ e.getHireDay());
}
}
}
class Employee {
private String name;
private double salary;
private LocalDate hireDay;
public Employee(String name, double salary, int year, int month, int day) {
this.name = name;
this.salary = salary;
hireDay = LocalDate.of(year, month, day);
}
public String getName() {
return name;
}
public double getSalary() {
return salary;
}
public LocalDate getHireDay() {
return hireDay;
}
public void raiseSalary(double byPercent) {
double raise = salary * byPercent / 100;
salary += raise;
}
}
源文件名是EmployleeTest.java,这是因为文件名必须与public类的名字相匹配。在一个源文件中,只能有一个公有类,但可以有任意数目的非公共类。
当编译这段源代码的时候,编译器将在目录下创建两个类文件:EmployeeTest.class和Employee.class。
将程序中包含main方法的类名提供给字节码解释器,以便启动程序:java EmployeeTest
字节码解释器开始运行EmployeeTest类的main方法中的代码。在这段代码中,先后构造了三个新Employee对象,并显示它们的状态。
多个源文件的使用
许多程序员习惯于将每一个类存在一个单独的源文件中。
这样可以有两张编译源程序的方法:
1.使用通配符调用Java编译器:javac Employee*.java
2.键入下列命令:javac EmployeeTest.java
当Java编译器发现EmployeeTest.java使用了Employee类时会查找名为Employee.class的文件。如果没有找到这个文件,就会自动搜索Employee.java,然后进行编译。更重要的是,如果Employee.java版本较已有的Employee.class文件版本新,Java编译器会自动地重新编译这个文件。
剖析Employee类
方法都被标记为public,意味着任何类的任何方法都可以调用这些方法。
三个实例域关键字private确保只有Employee类自身的方法能够访问。可以用public标记实例域,但是一种极为不提倡的做法,public数据域允许任何程序的方法访问和修改,这就完全破坏了封装。
类通常包括类型属于某个类类型的实例域。
从构造器开始
1.构造器与类同名
2.每个类可以有一个以上的构造器
3.构造器可以有0个、1个或多个参数
4.构造器没有返回值
5.构造器总是伴随着new操作一起调用
不要再构造器中定义与实例域重名的局部变量。这些变量只能在构造器的内部访问,屏蔽了同名的实例域。
隐式参数与显式参数
e.raiseSalary(5);
raiseSalary方法有两个参数,第一个参数为隐式(implicit)参数,是出现在方法名钱的Employee类对象。第二个参数方法后面括号的数值,是一个显式(explicit)参数。
在每一个方法中,关键字this表示隐式参数:
public void raiseSalary(double byPercent) {
double raise = this.salary * byPercent / 100;
this.salary += raise;
}
有些人偏爱这种风格,因为可以将实例域与局部变量明显区分开来。
封装的优点
1.可以改变内部实现,除了该类的方法之外,不影响其他代码
2.更改器方法可以执行错误检查,然而直接对域进行赋值将不会进行这些处理。
注意不要编写返回引用可变对象的访问器方法:
class Employee {
private Date hireDay;
...
public Date getHireDay() {
return hireDay;
}
...
}
Employee harry = new Employee("Carl Cracker", 75000);
// 时间使用Date(Date对象是可变的)
System.out.println(harry.getHireDay());
Date d = harry.getHireDay();
double t = 10 * 365.25 *24 * 60 * 60 * 1000;
d.setTime(d.getTime() - (long)t);
System.out.println(harry.getHireDay());
// Mon Oct 28 20:19:18 CST 2019
// Wed Oct 28 08:19:18 CST 2009
Date对象是可变的,这点破坏了封装。d和harry.hireDay引用同一对象。对d调用更改器方法就可以自动地改变这个雇员对象的私有状态。
如果需要返回一个可变对象的引用,应该首先对它进行克隆(clone),对象clone是指存放在另一个位置上的对象副本。
修改后的代码:
public Date getHireDay() {
return (Date) hireDay.clone();
}
如果需要返回一个可变数据域的拷贝,就应该使用clone。
基于类的访问权限
Employee类的方法可以访问Employee类的任何一个对象的私有域。
私有方法
在java中为了实现一个私有方法,只需将关键字public改为private即可。
只要方法是私有的,它就不会被外部的其他类操作调用,可以将其删去。如果是共有的,就不能删去,因为其他的代码可能依赖它。
final实例域
可以将实例域定义为final。构建对象时必须初始化这样的域。也就是说,必须确保在每一个构造器执行之后,这个域的值被设置,并且在后面的操作中,不能够再对它进行修改。
final修饰符大多都应用于基本类型域或不可变类的域(String类就是一个不可变类)。
对于可变的类,会造成混乱:
private final StringBuilder evaluations;
构造器中会初始化为:
evaluations = new StringBuilder();
final关键字只是表示存储在evaluations变量中的对象引用不会再指示其他StringBuilder对象,不过这个对象可以更改:
public void giveGoldStar () {
evaluations.append(LocalDate.now() + ": Gold star!\n");
}