目录
静态域与静态对象
静态域与静态常量
在绝大多数的面向对象程序设计语言中,静态域被称为类域。如果将域定义为 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
)。
工厂方法
静态方法还有另外一种常见的用途。类似 LocalDate
和 NumberFormat
的类使用静态工厂方法 (factory method 来构造对象。你已经见过工厂方法 LocalDate.now
和 LocalDate.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);
注意: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
小结:
- 一个方法不能修改一个基本数据类型的参数(即数值型或布尔型)。
- 一个方法可以改变一个对象参数的状态。
- 一个方法不能让对象参数引用一个新的对象。
对象构造
重载
多个方法具有相同的方法名,但传入参数不同,这种现象称为“重载”(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;
}
...
}
调用构造器的具体执行顺序:
- 所有数据域被初始化为默认值 (
0
、false
或null
)。 - 按照在类声明中出现的次序,依次执行所有域初始化语句和初始化块。
- 如果构造器第一行调用了第二个构造器,则执行第二个构造器主体。
- 执行这个构造器的主体。
参数名
在构造对象时,应避免用一个简单的字符表示参数名,否则会影响代码的可读性,推荐使用如下方法进行参数命名:
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.lang
、java.util
和 java.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 实用程序就没有这么聪明了,因此必须对它提供帮助。
类设计的技巧
-
一定要保证数据私有
这是最重要的,绝对不要破坏封装性。有时候, 需要编写一个访问器方法或更改器方法,但是最好还是保持实例域的私有性。数据的表示形式很可能会改变, 但它们的使用方式却不会经常发生变化。当数据保持私有时,它们的表示形式的变化不会对类的使用者产生影响,即使出现 bug 也易于检测。
-
一定要对数据初始化
Java 不对局部变量进行初始化,但是会对对象的实例域进行初始化。最好不要依赖于系统的默认值,而是应该显式地初始化所有的数据,具体的初始化方式可以是提供默认值,也可以是在所有构造器中设置默认值。
-
不要在类中使用过多的基本类型
就是说,用其他的类代替多个相关的基本类型的使用。这样会使类更加易于理解且易于修改。
-
不是所有的域都需要独立的域访问器和域更改器
或许,需要获得或设置雇员的薪金。而一旦构造了雇员对象,就应该禁止更改雇用日期,并且在对象中,常常包含一些不希望别人获得或设置的实例域,例如, 在 Address 类中,存放州缩写的数组。
-
将职责过多的类进行分解
如果明显地可以将一个复杂的类分解成两个更为简单的类,就应该将其分解(但另一方面,也不要走极端。设计 10 个类,每个类只有一个方法,显然有些矫枉过正了)。
-
类名和方法名要能够体现它们的职责
与变量应该有一个能够反映其含义的名字一样,类也应该如此(在标准类库中,也存在着一些含义不明确的例子,如:
Date
类实际上是一个用于描述时间的类)。命名类名的良好习惯是采用一个名词 (Order)、前面有形容词修饰的名词 (RushOrder) 或动名词 (有 “-ing” 后缀) 修饰名词 (例如,BillingAddress)。对于方法来说,习惯是访问器方法用小写 “get” 开头 (getSalary),更改器方法用小写的 set 开头 (setSalary)。
-
优先使用不可变的类
LocalDate
类以及 java.time 包中的其他类是不可变的——没有方法能修改对象的状态。类似plusDays
的方法并不是更改对象,而是返回状态已修改的新对象。更改对象的问题在于,如果多个线程试图同时更新一个对象,就会发生并发更改。其结果是不可预料的。如果类是不可变的,就可以安全地在多个线程间共享其对象。
因此,要尽可能让类是不可变的,这是一个很好的想法。对于表示值的类,如一个字符串或一个时间点,这尤其容易。计算会生成新值,而不是更新原来的值。
当然,并不是所有类都应当是不可变的。如果员工加薪时让
raiseSalary
方法返回一个新的Employee
对象,这会很奇怪。
参考资料:
- 《Java核心技术 卷1 基础知识》
- 《关于 Java 的静态工厂方法,看这一篇就够了!》