对象与类
使用预定义类
- 在 Java 中, 没有类就无法做任何事情,但是并非所有的类都具有面向对象的特征。封装是只需要知道方法名和参数,而不必了解内部具体实现过程,有些类之封装了功能,不需要也没有必要隐藏数据。由于没有数据,因此也不必担心生成对象以及初始化实例域。
- 以Date类为例,展示构造、调用等方法
- 对象和对象变量
- 要想使用对象,就必须首先构造对象, 并指定其初始状态。然后,对对象应用方法。在 Java 程序设计语言中,使用构造器(constructor ) 构造新实例。构造器是一种特殊的方法, 用来构造并初始化对象。在标准 Java 库中包含一个 Date 类。它的对象将描述一个时间点,
December 31, 1999, 23:59:59 GMT
; - 构造器的名字应该与类名相同,要想构造一个对象,需要在构造器前面加上 new 操作符
new Date()
,即初始化为当前的日期和时间; - 如果需要可以将这个对象传递给一个方法
System.out.println(new Date());
- 也可以将一个方法应用与刚刚创建的对象
String s = new Date().toString();
,toString方法应用于新构造的Date对象上,将返回日期的字符串描述。 - 将构造的对象存放在一个变量中,便于以后多次使用
Date birthday = new Date();
- 在对象与对象变量之间存在着一个重要的区别。例如, 语句
Date deadline; // deadline doesn't refer to any object
定义了一个对象变量 deadline, 它 可 以 引 用 Date 类型的对象。但是,一定要认识到: 变量deadline 不是一个对象, 实际上也没有引用对象。此时,不能将任何 Date 方法应用于这个变量上。语句s = deadline.toString(); // not yet
将产生编译错误。 - 首先初始化变量 deadline,可以新构造的对象初始化:
deadline = new date();
,也可以让这个变量引用一个已经存在的对象:deadline = birthday;
,两个变量引用同一个对象。
一个对象变量并没有实际包含一个对象,而仅仅引用一个对象。在 Java 中,任何对象变量的值都是对存储在另外一个地方的一个对象的引用。new 操作符的返回值也是一个引用。例如:Date deadline = new Date();
有两个部分。表达式 new Date() 构造了一个 Date 类型的对象, 并且它的值是对新创建对象的引用。这个引用存储在变量 deadline 中。
可以显式地将对象变量设置为 null,表明这个对象变量目前没有引用任何对象。
如果将一个方法应用于一个值为 null 的对象上,那么就会产生运行时错误。局部变量不会自动地初始化为 null ,而必须通过调用 new 或者将他们设置为 null 进行初始化。deadline = null; ... if (deadline != null) Sysem.out.println(deadline);
- 要想使用对象,就必须首先构造对象, 并指定其初始状态。然后,对对象应用方法。在 Java 程序设计语言中,使用构造器(constructor ) 构造新实例。构造器是一种特殊的方法, 用来构造并初始化对象。在标准 Java 库中包含一个 Date 类。它的对象将描述一个时间点,
很多人错误地认为 Java 对象变量与 C++ 的引用类似。然而,在 C++ 中没有空引用, 并且引用不能被赋值。可以将 Java 的对象变量看作 C++ 的对象指针。例如,
Date birthday; // Java
实际上,等同于
Date* birthday; // C++
一旦理解了这一点, 一切问题就迎刃而解了。 当然,一个 Date* 指针只能通过调用 new 进行初始化。就这一点而言,C++ 与 Java 的语法几乎是一样的。
Date* birthday = new Date(); // C++
如果把一个变量的值賦给另一个变量, 两个变量就指向同一个日期,即它们是同一个对象的指针。 在 Java 中的 null 引用对应 C++ 中的 NULL 指针。
所有的 Java 对象都存储在堆中。 当一个对象包含另一个对象变量时, 这个变量依然包含着指向另一个堆对象的指针。
在 C++ 中, 指针十分令人头疼, 并常常导致程序错误。稍不小心就会创建一个错误的指针,或者造成内存溢出。在 Java 语言中,这些问题都不复存在。 如果使用一个没有初始化的指针, 运行系统将会产生一个运行时错误, 而不是生成一个随机的结果, 同时,不必担心内存管理问题,垃圾收集器将会处理相关的事宜。
C++ 确实做了很大的努力, 它通过拷贝型构造器和复制操作符来实现对象的自动拷贝。 例如,一个链表( linked list) 拷贝的结果将会得到一个新链表, 其内容与原始链表相同, 但却是一组独立的链接。这使得将同样的拷贝行为内置在类中成为可能。在 Java中,必须使用 clone 方法获得对象的完整拷贝 „
- Java 类库中的 LocalDate 类
- Date类有局限性,不能很好地表示出其他历法下的日期来,为此,类库设计者决定将保存时间与给时间点命名分开。所以标准 Java 类库分别包含了两个类:一个是用来表示时间点的 Date 类;另一个是用来表示大家熟悉的日历表示法的LocalDate 类。
- 使用静态工厂方法 (factory method) 代表所调用构造器。例如
LocalDate.now()
构造一个新对象,表示构造对象时的日期。 - 提供年、月和日来构造一个特定日期的对象:
LocalDate.of(1999, 12 ,19)
,通常将构造的对象保存在一个对象变量中:LocalDate newYearEve = LocalDate.of(1999, 12, 19);
- 一旦有了LocalDate对象,可以用方法getYear,getMonthValue和getDayOfMonth得到年、月、日;有时可能某个日期是计算得到的,你希望调用这些方法来得到更多信息。例如, plusDays 方法会得到一个新的 LocalDate, 如果把应用这个方法的对象称为当前对象,这个新日期对象则是距当前对象指定天数的一个新日期。LocalDate 类封装了实例域来维护所设置的日期。
- 更改器方法与访问器方法
- plusDays 方法调用:
LocalDate aThousandDaysLater = newYearsEve.plusDays(1000);
plusDays 方法会生成一个新的 LocalDate 对象,然后把这个新对象赋给 aThousandDaysLater变量。原来的对象不做任何改动。与 LocalDate.plusDays 方法不同,GregorianCalendar.add 方法是一个更改器方法 ( mutator method ) 调用这个方法后,someDay 对象的状态会改变。 - 只访问对象而不修改对象的方法有时称为访问器方法(accessor method),相反,访问对象后并且修改即称为更改器方法(mutator method)
- plusDays 方法调用:
用户自定义类
- 自定义类
在Java中,最简单的类定义形式为:
例如:class ClassName { field1 field2 ... construct1 construct2 ... method1 method2 }
class Employee { // instance fields private String name; private double salary; private LocalDate hireDay; // constructor public Employee(String n,double s,int year,int month, int day) { name = m; salary = s; hireDay = LocalDate.of(year,month,day); } // a method public String getName() { return name; } // more methods ... }
- 多个源文件的实用
- 一个源文件包含了两个类。许多程序员习惯于将每一个类存在一个单独的源文件中。
- 这样有两种编译原程序的方法。一种是使用通配符调用Java编译器
javac Employee*.java
所有与通配符匹配的源文件都将被编译成类文件。第二种为javac EmployeeTest.java
使用第二种方式,并没有显式地编译 Employee.java 然而,当 Java 编
译器发现 EmployeeTest.java 使用丫 Employee 类时会查找名为 Employee.class 的文件。如果没有找到这个文件, 就会自动地搜索 Employee.java, 然后,对它进行编译。更重要的是:如果 Employee.java 版本较已有的 Employee.class 文件版本新,Java 编译器就会自动地重新编译这个文件。
- 类的方法与实力域
- 关键字 public 意味着任何类的任何方法都可以调用这些方法。关键字 private 确保只有当前类自身的方法能够访问这些实例域, 而其他类的方法不能够读写这些域。
- 可以用 public 标记实例域,但这是一种极为不提倡的做法。public 数据域允许程序中的任何方法对其进行读取和修改。这就完全破坏了封装。 任何类的任何方法都可以修改 public 域, 从我们的经验来看,某些代码将使用这种存取权限,而这并不我们所希望的,因此,这里强烈建议将实例域标记为 private。
- 类通常包括类型属于某个类类型的实例域。
- 构造器
// 构造器 public Employee(String n,double s,int year,int month, int day) { name = m; salary = s; hireDay = LocalDate.of(year,month,day); }
- 构造器与类同名。在构造类的对象时,构造器会运行,以便将实例域初始化为所希望的状态。
例如:
new Employee("James Bond",100000,1950,1,1);
会把实例域设置为
name = "James Bond"; salary = 100000; hireDay = LocalDate.of(1950,1,1);
- 构造器与其他的方法有一个重要的不同。构造器总是伴随着 new 操作符的执行被调用,而不能对一个已经存在的对象调用构造器来达到重新设置实例域的目的。
- 构造器基本规则
- 构造器与类同名
- 每个可以有一个以上的构造器
- 构造器可以有0个、1个或多个参数
- 构造器没有返回值
- 构造器总是伴随着 new 操作一起用
- 请注意, 不要在构造器中定义与实例域重名的局部变量。Java 构造器的工作方式与 C++—样。但是, 要记住所有的 Java 对象都是在堆中构造的, 构造器总是伴随着 new 操作符一起使用。
- 构造器与类同名。在构造类的对象时,构造器会运行,以便将实例域初始化为所希望的状态。
- 隐式参数与显式参数
- 方法用于操作对象以及存取它们的实例域。
public void raiseSalary(double byPercent) { double raise = salary * byPercent / 100; salary += raise; }
- 将调用这个方法的对象的 salary 实例域设置为新值。调用:
number007.raiseSalary(5);
它的结果将 number007.salary 域的值增加 5%。 - raiseSalary 方法有两个参数。第一个参数称为隐式(implicit)参数,是出现在方法名前的 Employee 类对象。第二个参数位于方法名后面括号中的数值,这是一个显式(explicit)参 数。(有些人把隐式参数称为方法调用的目标或接收者。)
- 可以看到,显式参数是明显地列在方法声明中的, 例如 double byPercent。隐式参数没有
出现在方法声明中。在每一个方法中, 关键字 this 表示隐式参数。这样可以将实例域与局部变量明显地区分开来。
public void raiseSalary(double byPercent) { double raise = this.salary * byPercent / 100; this.salary += raise; }
- 封装的优点
public String getName() { return name; } public double getSalary() { return salary; } public LocalDate getHireDay() { return hireDay; }
- 这些都是典型的访问器方法。由于它们只返回实例域值, 因此又称为域访问器。
- 有些时候, 需要获得或设置实例域的值
- 一个私有的数据域
- 一个公有的域访问器方法
- 一个公有的域更改器方法
- 这样做要比提供一个简单的公有数据域复杂些, 但是却有着下列明显的好处:首先, 可以改变内部实现,除了该类的方法之外,不会影响其他代码。当然, 为了进行新旧数据表示之间的转换,访问器方法和更改器方法有可能需要做许多工作。但是, 这将为我们带来了第二点好处:更改器方法可以执行错误检查, 然而直接对域进行赋值将不会进行这些处理。
- 注意不要编写返回引用可变对象的访问器方法。如果需要返回一个可变对象的引用, 应该首先对它进行克隆(clone )。对象 clone 是指存放在另一个位置上的对象副本。
- 基于类的访问权限
- 一个方法可以访问所属类的所有对象的私有数据
- C++ 也有同样的原则。方法可以访问所属类的私有特性( feature ), 而不仅限于访问隐式参数的私有特性
- 私有方法
- 在实现一个类时,由于公有数据非常危险, 所以应该将所有的数据域都设置为私有的。然 而,方法又应该如何设计呢?尽管绝大多数方法都被设计为公有的,但在某些特殊情况下,也可能将它们设计为私有的。有时,可能希望将一个计算代码划分成若干个独立的辅助方法。通常,这些辅助方法不应该成为公有接口的一部分,这是由于它们往往与当前的实现机制非常紧密,或者需要一个特别的协议以及一个特别的调用次序。最好将这样的方法设计为 private 的。
- 在 Java 中,为了实现一个私有的方法, 只需将关键字 public 改为 private 即可。
- 对于私有方法, 如果改用其他方法实现相应的操作, 则不必保留原有的方法。如果数据的表达方式发生了变化,这个方法可能会变得难以实现, 或者不再需要。然而,只要方法是私有的,类的设计者就可以确信:它不会被外部的其他类操作调用,可以将其删去。如果方法是公有的, 就不能将其删去,因为其他的代码很可能依赖它。
- final 实例域
- 可以将实例域定义为 final。构建对象时必须初始化这样的域。也就是说,必须确保在每一个构造器执行之后,这个域的值被设置,并且在后面的操作中,不能够再对它进行修改。
- final 修饰符大都应用于基本 (primitive ) 类型域,或不可变(immutable) 类的域(如果类中的每个方法都不会改变其对象, 这种类就是不可变的类。例如,String类就是一个不可变的类。)
- 对于可变的类, 使用 final 修饰符可能会对读者造成混乱。