4.4 静态域和方法
在您看到的所有示例程序中,主方法都用static
修饰符标记。我们现在准备讨论这个修饰语的含义。
4.4.1 静态域
如果将字段定义为静态字段,那么每个类只有一个这样的字段。相反,每个对象都有自己的非静态实例字段副本。例如,假设我们要为每个员工分配一个唯一的标识号。我们将实例字段id和静态字段nextId添加到Employee类中:
class Employee
{
private static int nextId = 1;
private int id;
. . .
}
每个Employee对象现在都有自己的id字段,但类的所有实例之间只有一个共享的nextId字段。换个说法吧。如果Employee类有1000个对象,那么有1000个实例字段id,每个对象一个。但是只有一个静态字段nextId。即使没有Employee对象,也存在静态字段nextId。它属于类,而不是任何单个对象。
注意
在一些面向对象的编程语言中,静态字段称为类字段。“static”一词是C++的无意义的保留。
让我们实现一个简单的方法:
public void setId()
{
id = nextId;
nextId++;
}
假设您为harry设置了员工身份号码:
harry.setId();
然后,将harry的id字段设置为静态字段nextId的当前值,并递增静态字段的值:
harry.id = Employee.nextId;
Employee.nextId++;
4.4.2 static常量
静态变量非常罕见。然而,静态常量更常见。例如,Math类定义了一个静态常量:
public class Math
{
. . .
public static final double PI = 3.14159265358979323846;
. . .
}
您可以在程序中以Math.PI的形式访问这个常量。
如果省略了关键字static
,那么PI
将是Math
类的一个实例字段。也就是说,您需要这个类的一个对象来访问PI
,并且每个Math
对象都有自己的PI
副本。
另一个您使用过多次的静态常量是System.out
。它在System
类中声明如下:
public class System
{
. . .
public static final PrintStream out = . . .;
. . .
}
正如我们多次提到的,拥有public字段从来不是一个好主意,因为每个人都可以修改它们。但是,public常量(即最终字段)是可以的。由于out已声明为final,因此不能将另一个打印流重新分配给它:
System.out = new PrintStream(. . .); // ERROR--out is final
注意
如果查看System类,您会注意到一个方法setOut将System.out设置为不同的流。您可能想知道该方法如何更改final变量的值。但是,setOut方法是原生方法,而不是在Java编程语言中实现的。本地方法可以绕过Java语言的访问控制机制。这是一个非常不寻常的解决方案,您不应该在程序中模仿它。
4.4.3 static方法
静态方法是不在对象上操作的方法。例如,Math类的pow方法是静态方法。表达式
Math.pow(x, a)
计算乘方x^a。它不使用任何数学对象来执行它的任务。换句话说,它没有隐式参数。
可以将静态方法视为没有this参数的方法。(在非静态方法中,该参数指方法的隐式参数,见第150页第4.3.7节“隐式和显式参数”。)
Employee类的静态方法无法访问id实例字段,因为它不在对象上操作。但是,静态方法可以访问静态字段。下面是这样一个静态方法的示例:
public static int getNextId()
{
return nextId; // returns static field
}
若要调用此方法,请提供类的名称:
int n = Employee.getNextId();
你能省略这个方法的关键字static吗?是的,但是您需要一个Employee类型的对象引用来调用该方法。
注意
使用对象调用静态方法是合法的。例如,如果harry是
Employee
对象,则可以调用harry.getNextId()
而不是Employee.getNextId()
。然而,我们发现这个符号令人困惑。getNextId方法根本不看Harry来计算结果。我们建议您使用类名而不是对象来调用静态方法。
在两种情况下使用静态方法:
- 当一个方法不需要访问对象状态时,因为所有需要的参数都作为显式参数提供(例如:Math.pow)。
- 当方法只需要访问类的静态字段时(例如:
Employee.getNextId
)。
C++注意
静态字段和方法在Java和C++中具有相同的功能。但是,语法略有不同。在C++中,使用::运算符访问其范围之外的静态字段或方法,如
Math::PI
。“static”术语有着奇怪的历史。首先,在C语言中引入了关键字static来表示退出块时不会消失的局部变量。在这种情况下,术语“static”是有意义的:当再次输入块时,变量仍然存在,并且仍然存在。然后,static在c中得到了第二个含义,表示不能从其他文件访问的全局变量和函数。关键字static被简单地重用以避免引入新的关键字。最后,C++重用关键字作为第三个无关的解释,表示属于类的变量和函数,而不是类的任何特定对象。这就是java关键词的意思。
4.4.4 工厂方法
下面是静态方法的另一个常见用法。诸如LocalDate
和NumberFormat
之类的类使用构造对象的静态工厂方法。您已经看到工厂方法LocalDate.now
和LocalDate.of
。下面是NumberFormat
类如何为各种样式生成格式化程序对象:
NumberFormat currencyFormatter = NumberFormat.getCurrencyInstance();
NumberFormat percentFormatter = NumberFormat.getPercentInstance();
double x = 0.1;
System.out.println(currencyFormatter.format(x)); // prints $0.10
System.out.println(percentFormatter.format(x)); // prints 10%
为什么NumberFormat类不使用构造函数?有两个原因:
- 不能给构造函数命名。构造函数名称始终与类名相同。但我们需要两个不同的名称来获取货币实例和百分比实例。
- 使用构造函数时,不能更改所构造对象的类型。但是工厂方法实际上返回
DecimalFormat
类的对象,DecimalFormat
是继承自NumberFormat
的子类。(更多关于继承的信息,请参见第5章。)
4.4.5 main
方法
注意,您可以调用静态方法而不需要任何对象。例如,您从不构造Math
类的任何对象来调用Math.pow。
出于同样的原因,main方法是静态方法。
public class Application
{
public static void main(String[] args)
{
// construct objects here
. . .
}
}
main方法不在任何对象上操作。事实上,当程序启动时,还没有任何对象。静态main方法执行,并构造程序需要的对象。
提示
每个类都有一个main方法。对于类的单元测试来说,这是一个方便的技巧。例如,可以向Employee类添加一个main方法:
class Employee { public Employee(String n, double s, int year, int month, int day) { name = n; salary = s; hireDay = LocalDate.of(year, month, day); } . . . public static void main(String[] args) // unit test { var e = new Employee("Romeo", 50000, 2003, 3, 31); e.raiseSalary(10); System.out.println(e.getName() + " " + e.getSalary()); } . . . }
如果要单独测试Employee类,只需执行
java Employee
如果Employee类是较大应用程序的一部分,则使用
java Application
并且永远不会执行Employee类的main方法。
清单4.3中的程序包含一个具有静态字段nextId和静态方法getNextId的Employee类的简单版本。我们用三个雇员对象填充一个数组,然后打印雇员信息。最后,我们打印下一个可用的标识号,以演示静态方法。
请注意,Employee类还具有用于单元测试的静态main
方法。试着运行
java Employee
和
java StaticTest
来执行main方法。
清单4.3 StaticTest/StaticTest.java
/**
* This program demonstrates static methods.
* @version 1.02 2008-04-10
* @author Cay Horstmann
*/
public class StaticTest
{
public static void main(String[] args)
{
// fill the staff array with three Employee objects
var staff = new Employee[3];
staff[0] = new Employee("Tom", 40000);
staff[1] = new Employee("Dick", 60000);
staff[2] = new Employee("Harry", 65000);
// print out information about all Employee objects
for (Employee e : staff)
{
e.setId();
System.out.println("name=" + e.getName() + ",id=" + e.getId() + ",salary="
+ e.getSalary());
}
int n = Employee.getNextId(); // calls static method
System.out.println("Next available id=" + n);
}
}
class Employee
{
private static int nextId = 1;
private String name;
private double salary;
private int id;
public Employee(String n, double s)
{
name = n;
salary = s;
id = 0;
}
public String getName()
{
return name;
}
public double getSalary()
{
return salary;
}
public int getId()
{
return id;
}
public void setId()
{
id = nextId; // set id to next available id
nextId++;
}
public static int getNextId()
{
return nextId; // returns static field
}
public static void main(String[] args) // unit test
{
var e = new Employee("Harry", 50000);
System.out.println(e.getName() + " " + e.getSalary());
}
}
java.util.Object 7
- static <T> void requireNonNull(T obj)
- static <T> void requireNonNull(T obj, String message)
- static <T> void requireNonNull(T obj, Supplier<String> messageSupplier) 8
如果obj为空,则这些方法抛出一个无消息或给定消息的NullPointerException。(第6章解释了如何延时从供应者地方获取一个值。第8章解释了<T
>语法。) - static <T> T requireNonNullElse(T obj, T defaultObj)
- static <T> T requireNonNullElseGet(T obj, Supplier<T> defaultSupplier)
如果obj不为空,则返回obj;如果obj为空,则返回默认对象。