之前通过记笔记的方法,对于《Java核心技术》这本书的前一章进行了重新的复习,感觉效果很好,比单独看书带来了更好的复习效果,了解了很多以前不是很注意的一些细节,但是在一些自己较为熟悉的地方,比如数组,循环等,复习的时候显得稍微没有耐心,不认真看书,这点也是需要以后继续进步的地方。
以前刚开始学习Java的时候,对于面向对象、类的一些知识总是不太了解,但是这一块知识真的是非常重要,不了解之后对于整个Java的提高也是非常有帮助的,因此我也希望在这次复习中能够继续提高自己的认识。
1. 面向对象程序设计概述
面向对象程序设计,简称OOP,是当今主流的程序设计规范,已经取代了“结构化”过程化结构设计开发技术。
面向对象的程序是由对象组成的,每个对象包含对用户公开的特定功能部分和隐藏的实现部分。程序中很多对象来自于标准库,以及一些自定义的。从根本上说,只要对象能够满足要求,就不必关心其功能的具体实现过程。
传统的结构化程序设计通过设计一些列的过程来解决问题。这些过程一旦被确定,就要开始考虑存储数据的方式。这就是为什么数据结构课程一直很重要,但是我可能无法理解,因为我开始学习的时候,就接触了面向对象的程序设计方法,而非传统结构。以前的程序第一个考虑的是算法,即如何操作数据,第二个是数据结构,即如何组织数据。而现在数据却是第一位的,然后再考虑如何操作数据。并且面向对象适合大型项目,也比较简洁。
1.1 类
类是构造对象的模板或蓝图。由类构造对象的过程称为创建类的实例。类相当于图纸,而实例相当于一个个具体的产品。
封装,有时候称为数据隐藏,是与对象有关的一个重要概念。从形式上看,封装是将数据和行为组合在一个包中,并对对象的使用者隐藏了数据的实现方式。对象中的数据成为实例域,操纵数据的过程称为方法。对于每个特定的类实例都有一组特定的实例域值。这些值的集合就是这个对象的当前状态,无论何时,只要向对象发送一个消息,它的状态就有可能改变。
实现封装的关键在于绝对不能让类中的方法直接地访问其他类的实例域。程序仅通过对象的方法与对象进行交流。封装给予了对象黑盒的特性,这是提高重用性和可靠性的关键。
OOP的另一个原则就是:类可以通过扩展另一个类来建立。Java所有的类都来自Object这个类,这个类是所有的类的父亲(因为在设计者看来,万物皆是对象,所以都来自于Object)。对于一个已有的类扩展时,这个扩展后的新类具有所扩展类的全部属性和方法。在新类中,只需要提供一些仅仅适用于新的类的方法和数据域就可以。这个过程被称作继承。
1.2 对象
对象的三个主要特性:对象的行为,对象的状态,对象标志。
同一个类的所有对象实例,由于支持相同的行为而有家族相似性。每个对象都保存着当前特征的信息,就是对象的状态(如果没有通过调用方法而改变了状态,说明封装性遭到了破坏)。对象的状态并不能描述一个对象,每个对象都有唯一的身份。
1.3 识别类
传统的程序设计,应当从main函数开始,而OOP的程序首先应当从类开始设计,再在类中添加方法。比如设计人这个类,人的身高,体重等就是实例域,而行走,说话等就是类的行为。当然,具体如何设计一个类,并不能严格按照实际条件来设计,而需要根据具体的情况来具体对待。对于类的设计,才是OOP中最精华的部分之一。
1.4 类之间的关系
类之间,最常见的关系有:1.依赖(users-a)2.聚合(has-a)3.继承(is-a)。
依赖关系,是一种最明显的,最常见的关系。例如,人这个类需要查看个人银行信息这个类的一些具体内容,而动物这个类则不需要,因为和银行信息无关。因此,如果一个类的方法操纵另一个类的对象,我们就说一个类依赖于另一个类。应当将类之间的依赖关系减少到最少,如果A类不知道B类的存在,它就不用关心B的任何改变,这样B的改变不会导致A产生任何BUG,也就是我们常说的,耦合度最小。
聚合关系,是一种易于理解的关系。比如,浴室这个对象包含了浴缸,洗脸盆等对象。意味着A类的对象包含B类的对象。
继承,是一种用于表示特殊与一般关系的。例如我们设计了一个动物类,继承自生物类。一般来说,如果A类扩展B类,那么A类不但继承了B的方法,还可能会拥有一些自己的二外功能。
很多程序员采用UML描述类之间的关系。有很多UML的工具,比如Rational Rose和Together,ArgoUML,Violet等。
2. 使用现有类
Java中,没有类就没办法做任何的事情。然而并不是所有的类都具有面向对象特征。Math类只有功能,没有参数,没有数据,因此不用担心生成对象以及初始化实例域。
2.1 对象与对象变量
在Java中,使用构造器构造新实例,构造器是一种特殊的方法,用来构造并初始化对象。构造器的名字与类名相同,通过new操作符实施,new的返回值也是一个引用。
// 定义了一个对象变量deadline,引用Date类型的对象Date deadline = new Date();
一个对象变量并没有实际包含一个对象,而仅仅引用一个对象。可以显示的将对象变量设置为null,表明这个对象变量目前没有引用任何对象。
2.2 Java类库中的GregorianCalendar类
Date类的实例有一个状态,即特定的时间点,距离一个固定时间点的毫秒数,这个点就是所谓的纪元,UTC时间1970年1月1日 00:00:00。但是Date类所提供的日期处理并没有太大的用处,Java的类库设计者认为:像December 31, 1999,23:59:59这样的日期表示法只是阳历的固有习惯。但是,同一时间点采用中国的农历表示就不太一样了。
类库设计者决定将保存时间与给时间点命名分开(这一点是一个优良的设计,将变化的部分和不变化的部分分开,类似于MVC模式,时间是永恒不变的,但是给时间点命名如同view一样,是一种表示方式而已,所以采用了这样的分离)。所以Java包含了两个类:一个是用来表示时间点的Date类;另一个是日历表示法的GregorianCalendar类。GregorianCalendar类扩展自一个更加通用的类Calendar类,这个类描述了日历的一般属性。理论上可以扩展Calandar类来实现阴历等。使用不同的类表示不同的概念,是一个很好的设计。
// Date类只提供了少量的方法表示时间,如before和after表示时间点的限售if (today.before(birthday)) System.out.println("Still time to shop for a gift");
实际上,Date类还有getDay等方法,然而不推荐使用这些(Eclipse中我们能看到有一些方法上面画有横线)。当类的设计者认为某个方法不应该存在时,就把它标记为不鼓励使用(@Deprecated)。因为设计着认为把Date时间和表示分离会更好,但是之前的Date版本中已经有了这样的方法,为了向后兼容,必须保留这些方法(因为可能世界上很多重要的软件已经在使用这些方法,不能贸然的去要求别人修改程序,可能引入新的错误)。但是我们在编写程序的时候,尽量不要使用这些方法,一来设计上会有一些不好的地方,二来这些方法说不准哪天就被删除了。。。
// 构造新对象,用于表示对象构造时的日期和时间GregorianCalendar gregorianCalendar = new GregorianCalendar();// 通过提供年月日构造特定时间午夜的日历GregorianCalendar gregorianCalendar = new GregorianCalendar(1991, 5, 26); // 月份从0开始,因此5代表6月// 为了避免上面的问题,可以使用常量GregorianCalendar gregorianCalendar = new GregorianCalendar(1991, Calendar.JULY, 26);// 也可以设置时间GregorianCalendar gregorianCalendar = new GregorianCalendar(1991, Calendar.JULY, 26, 16, 26, 26);
2.3 更改器方法与访问器方法
日历的作用是提供某个时间点的年、月、日等信息。要想查询这些信息,应该使用get方法。同时为了希望得到项,需要借助Calendar类的一些常量,如Calandar.MONTH等(其实我认为枚举更好,而不是用这种int型常量)。
GregorianCalendar gregorianCalendar = new GregorianCalendar();int month = gregorianCalendar.get(Calendar.MONTH); // 输出的是8(我测试的时候是9月,所以是8,之前介绍过)int weekly = gregorianCalendar.get(Calendar.DAY_OF_WEEK); // 输出的是3(我测试的时候是周二,周日是1,依次类推)
// 可以调用set方法,改变对象的状态,设置为2012年4月15日now.set(Calendar.YEAR, 2012);now.set(Calendar.Month, Calendar.APRIL);now.set(Calendar.DAY_OF_MONTH, 15);// 还可以便捷的设置年月日now.set(2012, Calendar.JULY, 9);// 还可以为给定的日期增加天数,星期等now.add(Calendar.DATE, 1); // 这时是2012年6月10日
get方法与set和add放啊是由区别的。get方法仅仅查看并返回对象的状态,而set和add方法却对状态进行了修改。修改的方法被称为更改器方法,访问而不该变的方法被称为访问器方法。C++中的访问器方法要有const后缀,Java无明显区别。通常的习惯是在访问其方法前面加get前缀,更改器加上set前缀。可以通过getTime方法获得一个Date对象,也可以通过Date对象构造一个Calander对象。
2.4 简单的日历程序设计
本书提供了一个简单的日历程序的设计的例子,用来描述如何使用GregorianCalendar对象,打印出当月的日历,并用*号标注当前的日子,效果图大致如下:
书本上提供了一套设计的思路和最终成型的代码,但我想既然是一个学习或者再学习的过程,我还是应当以自己的方式编写这样的一个程序,下面是我的一些思路,大家可以指正。
程序先获得当前日期的信息以及当前地理位置下的日历表示的习惯(美国是周日是每周的第一天,英国周一是每周的第一天),也就是我们说的国际化,输出整个日历头,接下来,由于每个月的第一天并不一定是在第一个日子下,所以需要有格式的缩进,接下来再打印整个月份的日子的信息,并在当前日子上标注*号。
下面提供了每一个步骤实现的关键部分的代码
// 获得当年地区每周的第一天是周几int firstDayOfWeek = calendar.getFirstDayOfWeek();// 得到星期几的头的缩写格式String[] weekDayNames = new DateFormatSymbols().getShortWeekdays();// 打印日历头for (int i = 1; i < weekDayNames.length; i++) { System.out.printf("%5s", weekDayNames[i]);}
// 日历设置到当月的第一天,并确定星期几calendar.set(Calendar.DAY_OF_MONTH, 1);int firstDayWeek = calendar.get(Calendar.DAY_OF_WEEK);// 确定缩进的日子int indent = 0;while (firstDayWeek != firstDayOfWeek) { indent++; calendar.add(Calendar.DAY_OF_MONTH, -1); firstDayWeek = calendar.get(Calendar.DAY_OF_WEEK);}// 打印缩进位置for (int i = 0; i < indent; i++) { System.out.printf("%5s", " ");}
// 打印整个日历内容do { int day = calendar.get(Calendar.DAY_OF_MONTH); day = calendar.get(Calendar.DAY_OF_MONTH); System.out.printf("%3d", day); if (day == todayDay) { System.out.print("* "); } else { System.out.print(" "); } calendar.add(Calendar.DAY_OF_MONTH, 1); if (calendar.get(Calendar.DAY_OF_WEEK) == firstDayOfWeek) { System.out.println(); System.out.print(" "); }} while (calendar.get(Calendar.MONTH) == todayMonth);
通过这三部操作,基本能够实现这样的一些需求。程序有几个关键点:日历头不能用System.out.println("Sun Mon ......");这样的方法输出,因为每一个人使用的地区都不一样,我们应当尽可能的调用系统的方法,通用的,考虑了国际化的方法去设计程序,这样,如果这些方法本身出现了问题,只要类库的设计者就行修改,我们的程序就能恢复正常。而我们自己设计这样的地方,难免会有考虑不足,而且这样会提高编程效率,所以,尽量重用已经存在的类库和方法,而不是自己编写。可以通过设置默认地址来测试不同国家的效果:Locale.setDefault(Local.US);(这就是设置默认地址为美国)
附上我的整体程序(如果大家看到觉得有什么不合适的地方,欢迎指正):
import java.text.DateFormatSymbols;import java.util.Calendar;import java.util.GregorianCalendar;import java.util.Locale;/** * 制作一个日历程序,显示当月的日历,并且标注当天 * @author S.R.ZHANG * */public class MyCalendar { public static void main(String[] args) { Locale.setDefault(Locale.US); // 通过当天的日期信息构建一个日历对象 GregorianCalendar calendar = new GregorianCalendar(); // 得到今天的月、日信息 int todayDay = calendar.get(Calendar.DAY_OF_MONTH); int todayMonth = calendar.get(Calendar.MONTH); // 日历设置到当月的第一天,并确定星期几 calendar.set(Calendar.DAY_OF_MONTH, 1); int firstDayWeek = calendar.get(Calendar.DAY_OF_WEEK); // 获得当年地区每周的第一天是周几 int firstDayOfWeek = calendar.getFirstDayOfWeek(); // 确定缩进的日子 int indent = 0; while (firstDayWeek != firstDayOfWeek) { indent++; calendar.add(Calendar.DAY_OF_MONTH, -1); firstDayWeek = calendar.get(Calendar.DAY_OF_WEEK); } // 得到星期几的头的缩写格式 String[] weekDayNames = new DateFormatSymbols().getShortWeekdays(); // 打印日历头 for (int i = 1; i < weekDayNames.length; i++) { System.out.printf("%5s", weekDayNames[i]); } System.out.println(); // 重置当前日子 calendar = new GregorianCalendar(); calendar.set(Calendar.DAY_OF_MONTH, 1); System.out.print(" "); // 打印缩进位置 for (int i = 0; i < indent; i++) { System.out.printf("%5s", " "); } // 打印整个日历内容 do { int day = calendar.get(Calendar.DAY_OF_MONTH); day = calendar.get(Calendar.DAY_OF_MONTH); System.out.printf("%3d", day); if (day == todayDay) { System.out.print("* "); } else { System.out.print(" "); } calendar.add(Calendar.DAY_OF_MONTH, 1); if (calendar.get(Calendar.DAY_OF_WEEK) == firstDayOfWeek) { System.out.println(); System.out.print(" "); } } while (calendar.get(Calendar.MONTH) == todayMonth); }}
并且附上书本上的程序:
import java.text.DateFormatSymbols;import java.util.Calendar;import java.util.GregorianCalendar;public class CalendarTest { public static void main(String[] args) { GregorianCalendar d = new GregorianCalendar(); int today = d.get(Calendar.DAY_OF_MONTH); int month = d.get(Calendar.MONTH); d.set(Calendar.DAY_OF_MONTH, 1); int weekday = d.get(Calendar.DAY_OF_WEEK); int firstDayOfWeek = d.getFirstDayOfWeek(); int indent = 0; while (weekday != firstDayOfWeek) { indent++; d.add(Calendar.DAY_OF_MONTH, -1); weekday = d.get(Calendar.DAY_OF_WEEK); } String[] weekdayNames = new DateFormatSymbols().getShortWeekdays(); do { System.out.printf("%4s", weekdayNames[weekday]); d.add(Calendar.DAY_OF_MONTH, 1); weekday = d.get(Calendar.DAY_OF_WEEK); } while (weekday != firstDayOfWeek); System.out.println(); for (int i = 1; i <=indent; i++) System.out.print(" "); d.set(Calendar.DAY_OF_MONTH, 1); do { int day = d.get(Calendar.DAY_OF_MONTH); System.out.printf("%3d", day); if (day == today) System.out.print("* "); else System.out.print(" "); d.add(Calendar.DAY_OF_MONTH, 1); weekday = d.get(Calendar.DAY_OF_WEEK); if (weekday == firstDayOfWeek) System.out.println(); } while (d.get(Calendar.MONTH) == month); if (weekday != firstDayOfWeek) System.out.println(); }}
3. 用户自定义类
之前我们所编写的类,都是有main方法的,但是这并不是实际的情况,现实生活中的程序,是没有main方法的。程序就是这些类的不断地交互所组成的。书上编写了一个简单的Employee类来说明一些最基本的东西。
3.1 Employee类和这个类的测试类
import java.util.Date;import java.util.GregorianCalendar;public class Employee { private String name; private double salary; private Date hireDay; public Employee(String name, double salary, int year, int month, int day) { this.name = name; this.salary = salary; GregorianCalendar calendar = new GregorianCalendar(year, month - 1, day); hireDay = calendar.getTime(); } public void raiseSarlary(double byPercent) { double raise = salary * byPercent / 100; salary += raise; } public String getName() { return name; } public double getSalary() { return salary; } public Date getHireDay() { return hireDay; }}
我们编写了一个测试类去测试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.raiseSarlary(5); } for (Employee e : staff) { System.out.println("name=" + e.getName() + ",salary=" + e.getSalary() + ",hireDay=" + e.getHireDay()); } }}
这个类通过构造器,构造了3个Employee对象,并把他们的工资都上调了5%,并输出最终的结果:
使用javac命令编译的时候,可以通过javac Employee*.java就可以编译这两个类,因为这两个类的名字都符合这样的一个格式。
3.2 分析Employee这个类
我们首先分析这个类的方法。这个类包含了一个构造器和四个方法。这个类的所有方法都被标记为了public,意味着任何的类的任何的方法都能够调用这些方法。并且这个类有三个实例域,private这个关键字确保了只有Employee这个类自身能够访问。
我们首先分析这个构造器,构造器的名字必须与类同名,构造对象的时候,构造器被运行,以便实例化对象的初始状态。使用new Employee("S.R.ZHANG", 10000, 2013, 6, 9); 会把实例域设置。构造器必须通过new操作符调用,不能直接调用构造器如jams.Employee(...)这样是不对的。构造器主要有以下几点:
- 构造器与类同名
- 每个类可以有一个以上的构造器
- 构造器可以有0个或多个参数
- 构造器没有返回值
- 构造器总是伴随着new操作一起调用
- Java所有的对象都是在堆中构造的,并且不要在构造器中定义与实例域重名的局部变量。
如果我们操作raiseSalary(double byPercent)这个方法,如:number001.raiseSalary(5);就等于是double raise = number001.salary * 5 / 100、number001.salary += raise;这个方法有两个参数,第一个被称为隐式参数,就是出现在方法前的Employee对象(number001);第二个是显式参数。在每一个方法中,关键字this表示隐式参数,所以在构造器中,我使用this.name = name;用来区别这两个name,第一个是对象的实例域name,第二个是这个构造方法中的name。
我们还提供了三个访问器方法,getName,getSalary,getHireDay,又被称作域访问器。一般情况下,我们对于设置一个实例域,通常应该提供三个内容:
- 一个私有的数据域 例如:private String name;
- 一个公有的域访问器方法 例如:public String getName();
- 一个公有的域更改器方法 例如:public void setNmae(String name);
这样做的话相对于提供一个简单的公有数据域更复杂一点,因此很多人不能理解这样做的意义,但其实这种做法有着明显的好处:
1. 可以改变内部的实现,除了类本身之外,不会影响到其他的代码
// 原始程序public String name; // 调用程序的话System.out.println(name);// 如果此时程序改变,将name变成firstName和lastName的话public String firstName;public String lastName; // 此时外部的调用都要改变成System.out.println(firstName + " " + lastName);// 如果采用了访问器方法的话private String name;public String getName() { return name;}// 此时程序进行了修改private String firstName;private String lastName;public String getName() { return firstName + " " + lastName; // 只需修改这个方法和类本身,外部调用不受影响,仍然是getName方法}
2. 更改器方法可以执行错误检查,而直接对域操作不会进行处理
public int age; // 如果想让这个age必须大于18岁,则必须重写一个validate方法去验证// 使用修改器private int age;public void setAge(int age) { if (age >= 18) { this.age = age; } else { System.out.println("必须超过18岁!"); }}
不要编写返回引用可变对象的访问器方法。Employee这个类就违反了这个原则:
Employee harry = ...;Date d = harry.getHireDay();double tenYearsInMilliSeconds = 10 * 365 * 24 * 60 * 60 * 1000;d.setTime(d,getTime() - (long)tenYearsInMilliSeconds );
因为d和harry.hireDay引用了同一个对象,对d调用更改其方法就可以偷偷改变这个私有的构造器方法了。因此,如果要返回一个可变对象的引用,首先要进行克隆。
class Employee { ... public Date getHireDay() { return (Date) hireDay.clone(); }}
这样,d的值仅仅是一个和harry.hireDay本身相同的值,但是引用的是不同的地方,这样对d的操作就不会改变私有的数据域。
3.3 私有方法和final实例域
实现一个类的时候,由于公有数据很危险(无法别人对这个数据域的操作做任何的检测和限制),所以应该设为私有。同样的,方法在有些情况下也该设为私有的。比如,这个方法,提供了一个公共的接口,这个方法是由一系列流程构成的,因此,这些流程的方法,都应该被设置私有的,将public关键字改为private。
class Waiter { // 对外暴露的接口,招呼一个新的客人,包括找座位和上茶水两个步骤 public void welcomeANewVisitor() { searchTheSeat(); provideTea(); } private void searchTheSeat() { System.out.println("您好,请坐!"); } private void provideTea() { System.out.println("您好,这是你的茶,请慢用!"); }}
如果这个provideTea方法了修改,那么当这个类处理完时,我们可以直接删除这个方法,因为私有的方法我们可以确定别的地方不会有调用,然而如果这个方法是public的,我们则永远也无法保证这一点,这个方法将永远存在在这个类中,给后来的人造成困扰(每一个设置为public的方法都是对用户的承诺)。
可以将实例域定义为final。构建对象时必须初始化这样的域。也就是说,必须确保在每一个构造器执行之后,这个域的值被设置,并且以后不能对他作出修改。final修饰符大都应用于基本数据类型或不可变类的域。如果类中每个方法都不会改变其对象,这种类就是不可变的类(如String类)。
对于可变的类,使用final修饰符让人很难理解,比如:private fianl Date hireDay;仅仅意味着存储在hireDay中的对象引用在对象构造之后不能被改变,而并不意味着hireDay对象是一个常量,任何方法都可以对hireDay引用的对象调用setTime更改器。