JAVA学习笔记——面向对象编程:类与对象2

静态域与静态对象

静态域与静态常量

在绝大多数的面向对象程序设计语言中,静态域被称为类域。如果将域定义为 static, 每个类中只有一个这样的域。而每一个对象对于所有的实例域却都有自己的一份拷贝。

静态域属于类,而不属于任何一个对象。类定义了静态域后,所有的实例对象共享同一个静态域。换句话说,当任何一个对象修改了它的静态域后,所有同类的对象的静态域也会一同改变,即所有对象的静态域共用同一块内存空间。

基于这个特性,静态域一般使用静态常量,很少使用静态变量。例如 Math 类中,就定义了一个静态常量 public static final double PI = 3.14159265358979323846;

静态方法

静态方法是一种不能对对象实施操作的方法,即不存在隐式参数 this,不能对对象的实例域进行操作。

不过,静态方法可以访问类的静态域,且直接用类名来调用静态方法。例:

// 静态方法定义
public static int getNextId()
{
return nextId; // returns static field
}
// 调用静态方法
int n = Employee.getNextId();

注意:可以使用对象调用静态方法。例如,如果 harry 是一个 Employee 对象,可以用 harry.getNextId() 代替 Employee.getNextId() 不过,这种方式很容易造成混淆,其原因是 getNextld 方法计算的结果与 harry 毫无关系。建议使用类名,而不是对象来调用静态方法。

以下两种情况可考虑使用静态方法:

  • 一 方法不需要访问对象状态,其所需参数都是通过显式参数提供(例如:Math.pow) 。
  • 一个方法只需要访问类的静态域(例如:Employee.getNextId)。

工厂方法

静态方法还有另外一种常见的用途。类似 LocalDateNumberFormat 的类使用静态工厂方法 (factory method 来构造对象。你已经见过工厂方法 LocalDate.nowLocalDate.of
NumberFormat 类如下使用工厂方法生成不同风格的格式化对象:

NumberFormat currencyFormatter = NumberFormat.getCurrencylnstance();
NumberFormat percentFormatter = NumberFormat.getPercentlnstance();
double x = 0.1;
System.out.println(currencyFormatter.format(x)); // prints $O.10
System.out.println(percentFomatter.format(x)); // prints 10%

为什么 NumberFormat 类不利用构造器完成这些操作呢? 这主要有两个原因:

  • 无法命名构造器。构造器的名字必须与类名相同。但是, 这里希望将得到的货币实例和百分比实例采用不用的名字。
  • 当使用构造器时,无法改变所构造的对象类型。而 Factory 方法将返回一个 DecimalFormat 类对象,这是 NumberFormat 的子类。

工厂方法详细介绍

main 方法

需要注意,不需要使用对象调用静态方法。例如,不需要构造 Math 类对象就可以调用 Math.pow。同理,main 方法也是一个静态方法,main 方法不对任何对象进行操作。

事实上,在启动程序时还没有任何一个对象。静态的 main 方法将执行并创建程序所需要的对象。

每一个类可以有一个 main 方法,这是一个常用于对类进行单元测试的技巧。

方法参数

Java 总是按值传递参数的。

  • 当传入参数为基本数据类型时,与一般的按值传参的过程相同。
  • 当传入参数为引用类型时:
    • 方法内部会构造一个新变量,并将其初始化为传入参数的拷贝。
    • 此时新变量与原变量引用的是同一个对象。
    • 新变量对对象进行操作后,由于二者引用同一对象,因此原变量所引用的对象状态也发生改变。

例:

public static void tripleSalary (Employee x) // works
{
	x.raiseSa1ary(200);
}

harry = new Employee(. . .);
tripleSalary(harry);

按值传参

图1 对对象参数的修改保留了下来

注意:Java 总是按值传递参数,因此在方法内部对于对象状态的修改会保留下来,但是在方法外部,变量和参数的引用关系依然保持不变。见下例:

public static void swap(Employee x , Employee y) // doesn't work
{
	Employee temp = x;
	x = y;
	y = temp;
}
// 调用方法
Employee a = new Employee("Alice", . . .);
Employee b = new Employee("Bob", . . .);
swap(a, b);
// does a now refer to Bob, b to Alice?
// 在方法内部,显然经过 swap 后
// x refer to Bob, y refer to Alice
// 但是由于按值传参,在方法外部,a 依然引用 Alice,b 依然引用Bob

交换对象参数

图2 交换对象参数的结果未被保存下来

小结:

  • 一个方法不能修改一个基本数据类型的参数(即数值型或布尔型)。
  • 一个方法可以改变一个对象参数的状态。
  • 一个方法不能让对象参数引用一个新的对象。

对象构造

重载

多个方法具有相同的方法名,但传入参数不同,这种现象称为“重载”(overloading)。编译器通过传入的参数,来确定调用哪一个方法,若找不到匹配的方法,则会产生错误。

Java 允许重载任何方法,因此,要完整地描述一个方法,
需要指出方法名以及参数类型。这叫做方法的签名 (signature)。例如,String 类有 4 个称为 indexOf 的公有方法。它们的签名是

indexOf(int)
indexOf(int, int)
indexOf(String)
indexOf(String, int)

注意:返回类型不是方法签名的一部分。也就是说,不能有两个名字相同、参数类型也相同却返回不同类型值的方法。

无参构造函数

如果在编写一个类时没有编写构造器,那么系统就会提供一个无参数构造器。(当构造器存在时,系统将不提供)这个构造器将所有的实例域设置为默认值。于是,实例域中的数值型数据设置为 0、布尔型数据设置为 false、所有对象变量将设置为 null

初始化

初始化对象的实例域时,有如下方法:

  • 直接对实例域进行初始化:private String name="";
  • 利用构造器,在构造器中初始化实例域
  • 使用初始化块
  • 构造器同样也可以重载,调用不同的构造器可以初始化不同的对象
// 初始化块
class Employee
{
	private static int nextld;
	private int id;
	private String name;
	private double salary;
	// object initialization block
	{
		id = nextld;
		nextld++;
	}
	public Employee(String n, double s)
	{
		name = n;
		salary = s;
	}
	public Employee()
	{
	name = "";
	salary = 0;
	}
	...
}

调用构造器的具体执行顺序:

  1. 所有数据域被初始化为默认值 (0falsenull)。
  2. 按照在类声明中出现的次序,依次执行所有域初始化语句和初始化块。
  3. 如果构造器第一行调用了第二个构造器,则执行第二个构造器主体。
  4. 执行这个构造器的主体。

参数名

在构造对象时,应避免用一个简单的字符表示参数名,否则会影响代码的可读性,推荐使用如下方法进行参数命名:

public Employee(String aName, double aSalary)
{
	name = aName;
	salary = aSalary;
}

调用另一个构造器

this 关键字除了作为方法的隐式参数外,还有另一个含义。如果构造器的第一个语句形如 this(...),这个构造器将调用同一个类的另一个构造器。

采用这种方式使用 this 关键字非常有用, 这样对公共的构造器代码部分只编写一次即可。

对象析构与 finalize 方法

有些面向对象的程序设计语言,特别是 C++,有显式的析构器方法,其中放置一些当对象不再使用时需要执行的清理代码。在析构器中,最常见的操作是回收分配给对象的存储空间。由于 Java 有自动的垃圾回收器,不需要人工回收内存,所以 Java 不支持析构器。

当然,某些对象使用了内存之外的其他资源,例如,文件或使用了系统资源的另一个对象的句柄。在这种情况下,当资源不再需要时,将其回收和再利用将显得十分重要。

可以为任何一个类添加 finalize 方法。finalize 方法将在垃圾回收器清除对象之前调用。在实际应用中,不要依赖于使用 finalize 方法回收任何短缺的资源,这是因为很难知道这个方法什么时候才能够调用。

包(package)

Java 允许使用包 (package) 将类组织起来。借助于包可以方便地组织自己的代码,并将自己的代码与别人提供的代码库分开管理。

标准的 Java 类库分布在多个包中,包括 java.langjava.utiljava.net 等。标准的 Java 包具有一个层次结构。如同硬盘的目录嵌套一样,也可以使用嵌套层次组织包。所有标准的 Java 包都处于 java 和 javax 包层次中。

从编译器的角度来看, 嵌套的包之间没有任何关系。例如,java.util 包与 java.util.jar 包毫无关系。每一个都拥有独立的类集合。

包的导入

导入一个包中的类、静态方法、静态域,均使用 import 关键字,当需要导入一个包中的所有类、方法时,可以使用例如:import java.util.*;

但需要注意的是,只能使用星号(*) 导入一个包,而不能使用 import java.*import java.*.* 导入以 java 为前缀的所有包。

当用 * 导入的两个包内有相同的类名时,直接使用类名会产生错误。一种解决方法是在使用时,写出完整的类的路径(见下例);另一种方法是单独定义导入了哪个包中的类。

import java.util .*;
import java.sql .*;
// 均包含 Date 类

Date today; // Error java.util.Date or java.sql.Date?

// 方法 1
java.util.Date deadline = new java.util.Date();
java.sql.Date today = new java.sql.Date(...);

// 方法 2
import java.util.Date;

文档注释

类注释

类注释必须放在 import 语句之后,类定义之前。

/**
* A {©code Card} object represents a playing card , such
* as "Queen of Hearts". A card has a suit (Diamond, Heart,
* Spade or Club) and a value (1 = Ace , 2 . . . 10, 11 = Jack,
* 12 = Queen , 13 = King)
*/
public class Card
{
	...
}

方法注释

每一个方法注释必须放在所描述的方法之前。除了通用标记之外,还可以使用下面的标记:

  • @param 变量描述:这个标记将对当前方法的 “param”(参数)部分添加一个条目。这个描述可以占据多行,并可以使用 HTML 标记。一个方法的所有 @param 标记必须放在一起。
  • @return 返回值描述:这个标记将对当前方法添加 “return”(返回)部分。这个描述可以跨越多行,并可以使用 HTML 标记。
  • @throws 异常描述:这个标记将添加一个注释,用于表示这个方法有可能抛出异常。
/**
 * Raises the salary of an employee.
 * @param byPercent the percentage by which to raise the salary (e.g. 10 means 10%)
 * ©return the amount of the raise
 */
public double raiseSal ary(double byPercent)
{
	double raise = salary * byPercent / 100;
	salary += raise;
	return raise;
}

域注释

只需要对公有域(通常指的是静态常量)建立文档。

/**
 * The "Hearts" card suit
 */
public static final int HEARTS = 1;

通用注释

下面的标记可以用在类文档的注释中。

  • @author 作者:这个标记将产生一个 “author” (作者)条目。可以使用多个 @author 标记,每个 @author 标记对应一个作者。
  • @version 版本:这个标记将产生一个“ version”(版本)条目。这里的文本可以是对当前版本的任何描述。

下面的标记可以用于所有的文档注释中。

  • @since 始于:这个标记将产生一个“ since”(始于)条目。这里的 text 可以是对引入特性的版本描述。例如,@since version 1.7.1
  • @deprecated:这个标记将对类、方法或变量添加一个不再使用的注释。文本中给出了取代的建议。例如,@deprecated Use <code> setVisible(true)</code> instead,通过 @see@link 标记,可以使用超级链接,链接到 javadoc 文档的相关部分或外
    部文档。
  • @see 引用:这个标记将在 “see also” 部分增加一个超级链接。它可以用于类中,也可以用于方法中。例如:@see oom.horstraann.corejava.Employee#raiseSalary(double)

需要注意,一定要使用井号(#)而不要使用句号(.)分隔类名与方法名,或类名与变量名。Java 编译器本身可以熟练地断定句点在分隔包、子包、类、内部类与方法和变量时的不同含义。但是 javadoc 实用程序就没有这么聪明了,因此必须对它提供帮助。

类设计的技巧

  1. 一定要保证数据私有

    这是最重要的,绝对不要破坏封装性。有时候, 需要编写一个访问器方法或更改器方法,但是最好还是保持实例域的私有性。数据的表示形式很可能会改变, 但它们的使用方式却不会经常发生变化。当数据保持私有时,它们的表示形式的变化不会对类的使用者产生影响,即使出现 bug 也易于检测。

  2. 一定要对数据初始化

    Java 不对局部变量进行初始化,但是会对对象的实例域进行初始化。最好不要依赖于系统的默认值,而是应该显式地初始化所有的数据,具体的初始化方式可以是提供默认值,也可以是在所有构造器中设置默认值。

  3. 不要在类中使用过多的基本类型

    就是说,用其他的类代替多个相关的基本类型的使用。这样会使类更加易于理解且易于修改。

  4. 不是所有的域都需要独立的域访问器和域更改器

    或许,需要获得或设置雇员的薪金。而一旦构造了雇员对象,就应该禁止更改雇用日期,并且在对象中,常常包含一些不希望别人获得或设置的实例域,例如, 在 Address 类中,存放州缩写的数组。

  5. 将职责过多的类进行分解

    如果明显地可以将一个复杂的类分解成两个更为简单的类,就应该将其分解(但另一方面,也不要走极端。设计 10 个类,每个类只有一个方法,显然有些矫枉过正了)。

  6. 类名和方法名要能够体现它们的职责

    与变量应该有一个能够反映其含义的名字一样,类也应该如此(在标准类库中,也存在着一些含义不明确的例子,如:Date 类实际上是一个用于描述时间的类)。

    命名类名的良好习惯是采用一个名词 (Order)、前面有形容词修饰的名词 (RushOrder) 或动名词 (有 “-ing” 后缀) 修饰名词 (例如,BillingAddress)。对于方法来说,习惯是访问器方法用小写 “get” 开头 (getSalary),更改器方法用小写的 set 开头 (setSalary)。

  7. 优先使用不可变的类

    LocalDate 类以及 java.time 包中的其他类是不可变的——没有方法能修改对象的状态。类似 plusDays 的方法并不是更改对象,而是返回状态已修改的新对象。

    更改对象的问题在于,如果多个线程试图同时更新一个对象,就会发生并发更改。其结果是不可预料的。如果类是不可变的,就可以安全地在多个线程间共享其对象。

    因此,要尽可能让类是不可变的,这是一个很好的想法。对于表示值的类,如一个字符串或一个时间点,这尤其容易。计算会生成新值,而不是更新原来的值。

    当然,并不是所有类都应当是不可变的。如果员工加薪时让 raiseSalary 方法返回一个新的 Employee 对象,这会很奇怪。


参考资料

  1. 《Java核心技术 卷1 基础知识》
  2. 《关于 Java 的静态工厂方法,看这一篇就够了!》
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值