已经看过了万能的final关键字在Java中的不同用法和效果,其实static关键字的的用处也很广泛。它的用法更多涉及到面向对象设计(Object-Oriented Design)的思想,有时更加让人迷惑。许多是否应该使用static关键字的决定也常常基于工作经验中形成的约束和规范。本文就来帮助大家解锁static的正确使用姿势,写出更加清晰的代码。
为了更好的了解Java程序,就需要了解代码被编译后的样子,所以下边会涉及到javac
和javap
这些指令的使用,不用担心都是比较简单直接的用法。也需要借助一些反编译(decompile)工具来查看.class文件,这里用到的是JD-GUI。
- 静态变量 Static Variable
- 静态方法 Static Method
- 静态块 Static Block
- 静态嵌套类 Static Nested Class
- 静态引入 Static Import
Static Variable
静态变量(Static Variable)在Java中只有静态成员变量,不同于C++、PHP等,它不允许在方法内定义局部静态变量。静态成员变量也称为类变量,相对的是对象成员变量。这些变量是属于定义的类,而非某个对象实例,但是该类的所有实例都可分享该变量。
用个简单的例子会比较直观一些:
[codesyntax lang=”java” lines=”normal”]
package com.iderzheng.statickeyword; public class StaticField { public static int sField; public int mField; public void print() { System.out.println( String.format("%s{sField: %d, mField: %d}", this, sField, mField) ); } public static void main(String[] args) { StaticField one = new StaticField(); StaticField two = new StaticField(); one.sField = 7; one.mField = 7; two.sField = 21; two.mField = 21; one.print(); two.print(); StaticField.sField = 49; // Error: non-static variable mField cannot be referenced from a static context // StaticField.mField = 49; one.print(); two.print(); } }
[/codesyntax]
编译运行这段代码得到如下结果:
com.iderzheng.statickeyword.StaticField@34469729{sField: 21, mField: 7} com.iderzheng.statickeyword.StaticField@6d172f8f{sField: 21, mField: 21} com.iderzheng.statickeyword.StaticField@34469729{sField: 49, mField: 7} com.iderzheng.statickeyword.StaticField@6d172f8f{sField: 49, mField: 21}
很清楚的看到变量two
对sField
的修改影响了one
中的sField
值。而mField
在两个对象中是独立互补影响的。也看到可以用类的名字来引用静态变量,这是提倡调用方法,不应该直接用对象变量来引用静态变量,这样做不明确调用的是静态变量,很容易带来副作用。
Static Final Variable
一般在程序中不常定义静态变量,因为静态变量的可被访问和使用的地方比局部变量和对象变量要多,维护起来更加困难。更多时候是跟final结合起来定义常量,对于无法修改的只读变量,就不用担心维护问题了。
对象变量也可以用final
修饰定义为常量,只不过每个对象的常量只属于对象自己,而且每个对象都需要分配内存给这些常量。而属于类的静态变量则只需要一份内存甚至无需存储变量的值。所以如果对象变量在每个对象中都是一致的,那么就可以考虑转为静态常量。转成静态常量也能方便地用类名直接进行访问,省去创建多余的中间对象。
Java编译器对于常量会做优化,在引用它们的地方会将它们的值直接嵌入,这样就不需要内存来长期存储这些值,更不要从内存地址中寻找,所以效率要高很多。
还是做一个简单的例子:
[codesyntax lang=”java” lines=”normal”]
package com.iderzheng.statickeyword; public class StaticFinalFields { static final int CONSTANT_INT = 7; static final String CONSTANT_STRING = "iderzheng.com"; static final int[] CONSTANT_ARRAY = { 7, 21, 31 }; final String name; final int constantInt = 21; final boolean constantBoolean; { constantBoolean = true; } public StaticFinalFields(String name) { this.name = name; } public static void main(String[] args) { StaticFinalFields finalFields = new StaticFinalFields("Ider"); System.out.println(finalFields.name); System.out.println(finalFields.constantInt); System.out.println(finalFields.constantBoolean); System.out.println(CONSTANT_INT); System.out.println(CONSTANT_STRING); System.out.println(CONSTANT_ARRAY); } }
[/codesyntax]
对于上边的代码,通过javac进行编译后得到StaticFinalFields.class,再讲其通过JD-UI打开,会看到编译后代码的
[codesyntax lang=”java” lines=”normal”]
package com.iderzheng.statickeyword; import java.io.PrintStream; public class StaticFinalFields { static final int CONSTANT_INT = 7; static final String CONSTANT_STRING = "iderzheng.com"; static final int[] CONSTANT_ARRAY = { 7, 21, 31 }; final String name; final int constantInt = 21; final boolean constantBoolean = true; public StaticFinalFields(String paramString) { this.name = paramString; } public static void main(String[] paramArrayOfString) { StaticFinalFields localStaticFinalFields = new StaticFinalFields("Ider"); System.out.println(localStaticFinalFields.name); localStaticFinalFields.getClass(); System.out.println(21); System.out.println(localStaticFinalFields.constantBoolean); System.out.println(7); System.out.println("iderzheng.com"); System.out.println(CONSTANT_ARRAY); } }
[/codesyntax]
从编译后的代码可以看出对于原始类型的变量,在用final
进行修饰并内嵌初始化后,使用这些变量的地方就直接用这些值进行了替换。虽然也有非static
修饰的对象常量被智能优化后直接内嵌,但是为了代码的维护性,依然应该显示地申明成static
。
至于对象变量,它们并非真正意义上的常量,对象的创建后的地址也总会不同,而且对象的内容依然可以被修改,所以这些变量加上static final
也无法用其值在使用的地方进行替换。
另外还看到通过block进行初始化的对象变量虽然也是常量,但是因为放入了block,编译器觉得它的初始化比较复杂,所以就没有将其引用直接替换。
直接内嵌常量的可以提高运行的效率,但是也有一些弊端:比如项目A中使用了项目B中的静态常量,成功编译后的A会直接使用那些常量;要是B的常量值被人修改了之后只编译了B而没有重新编译A,那A就不会得到新的值。好在现代的编译器都很智能,可以发现这些依赖关系并正确编译。
Static Method
静态方法和静态变量类似,是属于类的方法。静态方法可以
- 直接听过类名来进行调用,虽然也可以通过对象来调用但不推荐这样做
- 在方法内直接访问同一个类的静态变量,这就可以为静态变量做封装提高维护性
- 直接调用同一个类其他静态方法
- 被对象方法直接调用
- 调用静态方法来为静态变量或对象变量做直接初始化
静态方法也有以下约束
- 不能直接调用对象变量,仍需通过创建的实例
- 不能直接方法非静态方法,那些是属于对象的
- 不能使用this或者supper,因为不存在多态性
[codesyntax lang=”java” lines=”normal”]
package com.iderzheng.statickeyword; public class StaticMethods { // Use static method to initialize static variable // Use class name "System" to reference the static method static long sInit = System.currentTimeMillis(); long mInit = System.nanoTime(); public static void staticMethod() { sInit = 21; // mInit = 21; // Error // method() // Error } public void method() { staticMethod(); } }
[/codesyntax]
静态方法的最大好处就是方便,因为可以不用创建对象而直接通过类名来调用。
缺点则在于不支持多态性,在直接调用静态方法的地方不能切换其它实现。这对代码测试带来很大的麻烦,常用的mockito框架就无法对静态方法进行模拟(mock)。
比如直接使用了System.currentTimeMillis()
来获取时间,那测试代码就无法模拟一个虚拟的测试时间,不得不被真实系统时间所绑定。所以很多时间都会将获取时间的方法做一层封装,虽然多了一次对象创建,但代码变得可测试很多。
这是Java语言强类型特性造成的弊端,如果它如同Object-C那样让静态方法支持多态,或者同Python那样任意方法都可被切换,就不需要不用担心测试的问题。但强类型带来了维护性的好处,这些弊端就通过使用不同的设计模式来抵消。
Static Block
静态变量,特别是静态的常量的初始化,除了可以是内嵌外,比较复杂的初始化过程还可以放在static block中。而且Java没有限制static block的定义数量,可以创建多个不同的来让代码职能更加清晰,这些static block会按照它们的定义顺序被有序的执行。
[codesyntax lang=”java” lines=”normal”]
package com.iderzheng.statickeyword; import java.util.ArrayList; import java.util.List; public class StaticBlocks { static final int[] INT_ARRAY; static final List<Integer> INT_LIST; static { INT_ARRAY = new int[3]; INT_ARRAY[0] = 7; INT_ARRAY[1] = 19; INT_ARRAY[2] = 21; } static { INT_LIST = new ArrayList<>(INT_ARRAY.length); INT_LIST.add(INT_ARRAY[2]); INT_LIST.add(INT_ARRAY[1]); INT_LIST.add(INT_ARRAY[0]); } public static void main(String[] args) { System.out.println(INT_LIST); } } /* Output: [21, 19, 7] */
[/codesyntax]
前面说过静态方法也可以用来为静态变量做初始化,所以上边的static block就好像是匿名的静态方法,它们也都可以转成静态方法:
[codesyntax lang=”java” lines=”normal”]
package com.iderzheng.statickeyword; import java.util.ArrayList; import java.util.List; public class StaticBlocks { static final int[] INT_ARRAY = arrayInit(); static final List<Integer> INT_LIST = listInit(); private static int[] arrayInit() { int[] array = new int[3]; array[0] = 7; array[1] = 19; array[2] = 21; return array; } private static List<Integer> listInit() { List<Integer> list = new ArrayList<>(INT_ARRAY.length); list.add(INT_ARRAY[2]); list.add(INT_ARRAY[1]); list.add(INT_ARRAY[0]); return list; } public static void main(String[] args) { System.out.println(INT_LIST); } }
[/codesyntax]
因为这些方法只需被使用一次,使用static block可以省去方法名的思考,也防止之后会有人误调用。再者一个static block内可以对多个静态变量进行初始化,而且静态方法需要一对一并匹配返回值,前者的方便性也显而易见。
Static Nested Class
Java允许将类定义在另一个类内部,这样的类被称之为嵌套类(Nested Class),根据是否有static
修饰还分成静态嵌套类(Static Nested Class)内部类(Inner Class)。
使用嵌套类是为了让相关的类能结合得更加紧密,它们与非内嵌类最大的区别在于:嵌套类可以访问其外部类的任何成员(变量和方法),包括私有成员。静态嵌套类和内部类最大的区别在于,前者对外部类没有依赖基本相当于顶层类(Top-level Class),后者依存于外部类存在。所以静态嵌套类需要获得具体外部类的实例对象来进行调用,而内部类则可以直接访问创建它的外部对象的内容。
[codesyntax lang=”java” lines=”normal”]
package com.iderzheng.statickeyword; public class OuterClass { private String mVariable; public OuterClass(String arg) { mVariable = arg; } public static class StaticClass { public StaticClass() { } public StaticClass(OuterClass outerClass) { System.out.println(getClass().getName() + " " + outerClass.mVariable); } } public class InnerClass { public InnerClass() { System.out.println(getClass().getName() + " " + mVariable); } public InnerClass(OuterClass outerClass) { // OuterClass.this is optional unless there is conflict System.out.println(OuterClass.this.mVariable); System.out.println(outerClass.mVariable); } } public InnerClass createInnerClass() { // this is optional return this.new InnerClass(); } public static void main(String[] args) { OuterClass outerClass = new OuterClass("Ider"); StaticClass staticClass = new StaticClass(outerClass); // InnerClass innerClass = new InnerClass(); // Error InnerClass innerClass = outerClass.new InnerClass(); } } /* Output: com.iderzheng.statickeyword.OuterClass$StaticClass Ider com.iderzheng.statickeyword.OuterClass$InnerClass Ider */
[/codesyntax]
从上述代码可以看到“对外部类依赖”的含义:静态嵌套类可以像平常使用普通类那样直接用new关键字来创建;内部类测需要用外部类的对象来修饰才能创建(outerClass.new InnerClass()
)。从运行结果可以看出嵌套类的名字会和外部类的名字用$放在拼接成其完全类名,此外虽然外部类的变量被定义成private
,内嵌类依然可以直接访问到该变量。
使用javac对上边定义的类进行编译会得到三个.class文件:OuterClass.class、OuterClass$InnerClass.class、OuterClass$StaticClass.class。
对这三个文件分别使用javap指令得到如下结果:
$ javap OuterClass.class Compiled from "OuterClass.java" public class com.iderzheng.statickeyword.OuterClass { public com.iderzheng.statickeyword.OuterClass(java.lang.String); public com.iderzheng.statickeyword.OuterClass$InnerClass createInnerClass(); public static void main(java.lang.String[]); static java.lang.String access$000(com.iderzheng.statickeyword.OuterClass); } $ javap OuterClass\$StaticClass.class Compiled from "OuterClass.java" public class com.iderzheng.statickeyword.OuterClass$StaticClass { public com.iderzheng.statickeyword.OuterClass$StaticClass(); public com.iderzheng.statickeyword.OuterClass$StaticClass(com.iderzheng.statickeyword.OuterClass); } $ javap OuterClass\$InnerClass.class Compiled from "OuterClass.java" public class com.iderzheng.statickeyword.OuterClass$InnerClass { final com.iderzheng.statickeyword.OuterClass this$0; public com.iderzheng.statickeyword.OuterClass$InnerClass(com.iderzheng.statickeyword.OuterClass); public com.iderzheng.statickeyword.OuterClass$InnerClass(com.iderzheng.statickeyword.OuterClass, com.iderzheng.statickeyword.OuterClass); }
看OuterClass$InnerClass.class的结果可以看了一个this$0
的变量,这就是编译器为其加上的外部类依赖,这个变量会在构造函数中被赋予。看到它的所有构造函数都多了OuterClass作为第一个参数,这也是编译器加上去的。
回看OuterClass$StaticClass.class则没有任何异常的地方。
最后来看OuterClass.class的输出,多了一个access$000
静态方法,它也是由编译器生成的,被称为Synthetic Method,用来辅助访问外部类的私有成员的。
嵌套类看起来可以方法外部类的私有成员,编译后还是独立的.class文件,所以不应该允许访问私有成员,否则就破坏了访问性约束。因此它们实际上还是通过这些静态方法实现的,毕竟静态方法才属于类自身。那为什么要用静态方法,不用对象方法呢?因为这样就不用担心会被子类重写了。
当嵌套类对外部类的私有成员(无论变量还是方法)进行访问时,就会自动对每一个成员生成对应的Synthetic Method,名字是$
后的数字自动往上加。要想防止这些方法被生成,最简单的方法就是不要使用private
修饰,使用包可见(无任何访问修饰符)是推荐的访问修饰,它既可以保护访问性,又允许嵌套类访问,因为嵌套类会与外部类编译后属于同一package下。
当定义的类出现多层嵌套,会对每个内嵌类添加包可见的直接外部类作为其成员,变量名也是随着层数递增的数字加到this$
后。比如用javac编译下边的代码
[codesyntax lang=”java” lines=”normal”]
public class Outer { public class Middle { public class Inner { public Inner() { } } } }
[/codesyntax]
再用javap查看Outer$Middle$Inner.class,就会得到
public class Outer$Middle$Inner { final Outer$Middle this$1; public Outer$Middle$Inner(Outer$Middle); }
因为Middle中this$0
也是包可见,所以不妨碍Inner通过this$1.this$0
来访问到Outer中的成员。
静态嵌套内嵌类相对于普通类唯一的优势就在于可以访问外部类的私有成员,如果没有访问私有成员的要求静态嵌套类都可以转成同一package下的顶层类(Top-level Class)。不过外部类在一定程度上也可当做namespace来约束,从而允许定义相同名字的静态嵌套类在不同的类中。比如生成器模式(Builder Pattern)。
内部类跟其外部类的对象有直接的联系,所以可以作为辅助类,比如List#iterator()的具体实现就是返回一个实现了Iterator的内部类,通过内部类可以直接访问List中的内容。内部类为其外部类提供扩展和辅助,两者直接互相作用互相影响实现更强大的功能。但是也因为互相的紧密牵连着,也会造成一些问题,比如ConcurrentModificationException就是比较常见的一种。还有一种是比较隐蔽的内存泄漏问题,这是由于内部类对象对外部类对象有引用造成的:内部类的对象在某处有强引用不能被GC自动销毁,也导致其外部类也要长期驻留在内存中。
Static Import
在Java中使用static就是希望能够用类名来方便的调用,但是当类名为了直观可读而像下边这个定义这样变态长的时候,简便性就无从谈起了:
[codesyntax lang=”java” lines=”normal”]
package com.iderzheng.statickeyword; public class IderImportStaticMethodsAndStaticVariablesAndNestedClassses { public static final int STATIC_VARIABLE = 7; public static int staticMethod() { return 21; } public class NestedClass { public static final int OTHER_VARIABLE = 21; } public static class StaticClass { } }
[/codesyntax]
此时就可以用Java的static import
引进来,然后无需任何修饰直接调用这些变量和方法:
[codesyntax lang=”java” lines=”normal”]
package com.iderzheng; import static java.lang.System.out; import com.iderzheng.statickeyword.IderImportStaticMethodsAndStaticVariablesAndNestedClassses; import com.iderzheng.statickeyword.IderImportStaticMethodsAndStaticVariablesAndNestedClassses.StaticClass; import com.iderzheng.statickeyword.IderImportStaticMethodsAndStaticVariablesAndNestedClassses.NestedClass; import static com.iderzheng.statickeyword.IderImportStaticMethodsAndStaticVariablesAndNestedClassses.STATIC_VARIABLE; import static com.iderzheng.statickeyword.IderImportStaticMethodsAndStaticVariablesAndNestedClassses.staticMethod; import static com.iderzheng.statickeyword.IderImportStaticMethodsAndStaticVariablesAndNestedClassses.NestedClass.OTHER_VARIABLE; public class Main { public static void main(String[] args) { IderImportStaticMethodsAndStaticVariablesAndNestedClassses iderImport = new IderImportStaticMethodsAndStaticVariablesAndNestedClassses(); NestedClass nestedClass = iderImport.new NestedClass(); out.println(STATIC_VARIABLE); out.println(staticMethod()); out.println(OTHER_VARIABLE); out.println(new StaticClass()); out.println(nestedClass); } } /* Output: 7 19 21 com.iderzheng.statickeyword.IderImportStaticMethodsAndStaticVariablesAndNestedClassses$StaticClass@775651df com.iderzheng.statickeyword.IderImportStaticMethodsAndStaticVariablesAndNestedClassses$NestedClass@441944ae */
[/codesyntax]
就好似这些方法是直接定义在该类之下,也像是在使用C/C++中的全局变量和方法。这显然是Java程序应该避免的使用,Java本身就不支持全局变量和方法的定义,也不符合面向对象编程的思想。使用static import也不明确这些变量到底是在哪里定义的,如果滥用了还容易造成不同同名不同定义的冲突。
因此只有在代码中最好只在比较确定引入不会造成困惑时才进行引用,且只对静态常量做static import,不要在静态方法上使用。
静态嵌套类和嵌套接口也可以被static import进来,但它们本来就可以通过普通import引入。
也存在一些情况对静态方法进行static import会让代码变得更加易读易用,大家也约定俗成这样使用,那就可以依从习惯来使用。比如在写单元测试时对于assertion
方法就会直接用static import进来使用,还有前面介绍的mockito中的用来模拟的方法也是如此。一来这些方法会在测试代码中被频繁使用,再者习惯写测试很容易知道这些方法来自哪个框架,最后测试代码主要也是内部使用。
[codesyntax lang=”java” lines=”normal”]
package com.iderzheng.statickeyword; import org.junit.Test; import java.util.List; import static org.junit.Assert.assertEquals; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; public class StaticImportTest { @Test public void testStaticImport() { List mockedList = mock(List.class); when(mockedList.get(0)).thenReturn(21); assertEquals(21, mockedList.get(0)); } }
[/codesyntax]
本文对static
关键字在Java中的使用做了详细的介绍,也对各种使用的利弊做了简单的分析,希望大家从中得到帮助,在以后写Java程序时更加明白使用static
的缘由。