Java开发笔记(五十五)关键字static的用法

前面介绍嵌套类的时候讲到了关键字static,用static修饰类,该类就变成了嵌套类。从嵌套类的用法可知,其它地方访问嵌套类之时,无需动态创建外层类的实例,直接创建嵌套类的实例就行。
其实static不光修饰类,还能用来修饰方法、修饰属性等等,例如大家学习Java一开始就遇到的main方法,便为static所修饰。当一个成员方法被static修饰之后,该方法就成为静态方法;当一个成员属性被static修饰之后,该属性就成为静态属性。静态方法和静态属性,它俩同嵌套类一样不依赖于所在类的实例。外部若要访问某个类的静态方法,只需通过“类名.静态方法名”即可;同理,通过“类名.静态属性名”就能访问该类的静态属性。由于静态方法和静态属性拥有独立调用的特性,因此它们常常出现在一些通用的工具场景,例如系统的数学函数库Math,便提供了大量的静态方法和静态属性。其中常见的静态方法包括四舍五入函数Math.round、取绝对值函数math.abs、求平方根函数Math.sqrt等,常见的静态属性则有圆周率近似值Math.PI等。
那么开发者自己定义一个新类,如何得知哪些属性需要声明为静态属性,哪些方法需要声明为静态方法呢?在多数情况下,静态属性的取值一般要求是固定不变的,而静态方法只允许对输入参数进行加工,不允许操作其它的成员变量(静态属性除外)。以树木类为例,凡是会动态变化着的性状与事情,显然不适合声明为静态成员;只有与生长过程无关的概念,才适合声明为静态成员。譬如树木可分为乔木与灌木两大类,可想而知乔木与灌木的类型取值,与每棵树木的生长情况没有关联,这两种树木类型就适合作为静态属性。根据树木的类型,推断该树木的类型名称是“乔木”还是“灌木”,这个类型名称的判断方法就适合作为静态方法。如此一来,Tree类便可添加下面的静态成员声明代码:

	// static的字面意思是“静态的”,意味着无需动态创建即可直接使用。
// 利用static修饰成员属性,外部即可通过“类名.属性名”直接访问静态属性。
public static int TYPE_ARBOR = 1;
public static int TYPE_BUSH = 2; // 利用static修饰成员方法,外部即可通过“类名.方法名”直接访问静态方法。
public static String getTypeName(int type) {
String type_name = "";
if (type == TYPE_ARBOR) {
type_name = "乔木";
} else if (type == TYPE_BUSH) {
type_name = "灌木";
}
return type_name;
}

外部访问树木类的静态成员,只要按照“类名.静态成员名”的格式就好,具体的调用代码如下所示:

	// 演示静态成员的调用方式
private static void testStaticMember() {
// 使用静态属性无需创建该类的实例,只要通过“类名.静态属性名”即可访问静态属性
System.out.println("类型TYPE_ARBOR的取值为"+TreeStatic.TYPE_ARBOR);
System.out.println("类型TYPE_BUSH的取值为"+TreeStatic.TYPE_BUSH);
// 使用静态方法无需创建该类的实例,只要通过“类名.静态方法名”即可访问静态方法
String arbor_name = TreeStatic.getTypeName(TreeStatic.TYPE_ARBOR);
System.out.println("类型TYPE_ARBOR对应的名称是"+arbor_name);
String bush_name = TreeStatic.getTypeName(TreeStatic.TYPE_BUSH);
System.out.println("类型TYPE_BUSH对应的名称是"+bush_name);
}

神通广大的static不仅可以修饰类、属性、方法,它居然还能修饰一段代码块!被static修饰的代码段样例如下:

	static {
// 这里是被static修饰的代码段内容
}

以上为static所包裹的代码段,又被称作“静态代码块”,其作用是在系统加载该类之时立即执行这部分代码。因为此处的代码被static包括,所以静态代码块内部只能操作同类的静态属性和静态方法,而不能操作普通的成员属性和成员方法。可是这里有个问题,早先提到构造方法才是创建实例之时的初始操作,那么静态代码块与构造方法比起来,它们的执行顺序孰先孰后?倘若从Java的运行机制来解答该问题,不但费口舌而且伤脑筋,都说实践出真知,接下来不如做个实验,看看它们究竟是怎样的先来后到。
首先在树木类中声明一个静态的整型变量leaf_count,之所以添加static修饰符,是因为要给静态代码块使用;接着在静态代码块内部对该变量做自增操作,并将变量值打印到日志;同时在树木类的构造方法里面也进行leaf_count的自增运算,以及往控制台输出它的变量值。修改后的相关代码片段示例如下:

	// 叶子数量,用来演示构造方法与初始静态代码块的执行顺序
public static int leaf_count = 0; // static还能用来包裹某个代码块,一旦当前类加载进内存,静态代码块就立即执行
static {
leaf_count++;
System.out.println("这里是初始的静态代码块,此时叶子数量为"+leaf_count);
} public TreeStatic(String tree_name) {
this.tree_name = tree_name;
leaf_count++;
System.out.println("这里是构造方法,此时叶子数量为"+leaf_count);
}

最后回到外部创建该树木类的新实例,对应代码如下所示:

	// 演示静态代码块与构造方法的执行顺序
private static void testStaticBlock() {
System.out.println("开始创建树木类的实例");
TreeStatic tree = new TreeStatic("月桂");
System.out.println("结束创建树木类的实例");
}

运行以上的演示代码,观察到下列的日志信息:

这里是初始的静态代码块,此时叶子数量为1
开始创建树木类的实例
这里是构造方法,此时叶子数量为2
结束创建树木类的实例

从日志结果可见,静态代码块的内部代码早早就得到执行了,而构造方法的内部代码要等到外部调用new的时候才会执行,这证明了静态代码块的执行时机确实先于该类的构造方法。

静态修饰符一边给开发者带来了便利,一边也带来了不大不小的困惑。为了说明问题的迷惑性,接下来照例做个代码实验。仍旧在树木类中先声明一个静态的整型变量annual_ring,再补充一个成员方法grow,该方法内部对annual_ring自增的同时也打印日志。依据上述步骤给树木类新增了如下代码:

	// 树木年轮,用来演示静态属性的持久性
public static int annual_ring = 0; // 注意每次读取静态属性,得到的都是该属性最近一次的数值
public void grow() {
annual_ring++;
System.out.println(tree_name+"的树龄为"+annual_ring);
}

然后其它地方先后创建这个树木类的两个实例,就像下面代码示范的那样:

	// 演示静态属性的持久性
private static void testStaticProperty() {
TreeStatic bigTree = new TreeStatic("大树");
bigTree.grow();
TreeStatic littleTree = new TreeStatic("小树");
littleTree.grow();
}

继续运行上面的测试代码,发现打印的日志如下:

这里是构造方法,此时叶子数量为3
大树的树龄为1
这里是构造方法,此时叶子数量为4
小树的树龄为2

虽然bigTree和littleTree是新创建的实例,但是从日志结果看它们的annual_ring数值竟然是递增的,这可真是咄咄怪事,两个实例分明都是通过new出来的呀!产生怪异现象的罪魁祸首,原来就是static这个始作俑者,凡是被static修饰的静态变量,它在内存中占据了一块固定的区域,不管所在类被创建了多少个实例,每个实例引用的静态变量依然是最初分配的那个。于是后面创建的树木实例littleTree,其内部的annual_ring与之前实例bigTree的annual_ring保持一致,无怪乎前后两实例的annual_ring数值是依序递增的了。
由此可见,静态属性总是保存最后一次的数值,倘若它的取值每次都发生变化,即使创建新实例也得不到静态属性最初的数值。这种后果显而易见违背了静态变量的设计初衷,在多数时候,开发者定义一个静态属性,原本是想作为取值不变的常量使用,而不希望它变来变去。对于此类用于常量定义的静态属性,可以在static前头再添加修饰符final,表示该属性只允许赋值一次,从而避免了多次赋值导致取值更改的尴尬。下面是联合修饰final和static的属性定义代码例子:

	// 若想静态属性始终如一保持不变,就得给该属性添加final修饰符,表示终态属性只能被赋值一次
public final static int FINAL_TYPE_ARBOR = 1;
public final static int FINAL_TYPE_BUSH = 2;

  

更多Java技术文章参见《Java开发笔记(序)章节目录

上一篇:JavaScript进阶系列04,函数参数个数不确定情况下的解决方案


下一篇:css制作对话框