在《Java关键字分类解析》里介绍了所有Java关键字和保留字的基本用途,到《Java修饰符及其作用对象》又介绍了所有Java修饰符的应用。通过修饰符的表格看到final是唯一一个可以在类(Class)、方法Method、变量(Variable)上都能应用的,但前文也提到final
在这些作用对象上的效果是不太一样的,本文就详细展开介绍一下final
的具体效果。
final class
当一个类被定义成final class
,表示该类的不能被其他类继承,即不能用在extends之后。否则在编译期间就会得到错误。
[codesyntax lang=”java” lines=”normal”]
package com.iderzheng.finalkeyword; public final class FinalClass { } // Error: cannot inherit from final class PackageClass extends FinalClass { }
[/codesyntax]
Java支持把class定义成final
,似乎违背了面向对象编程的基本原则,但在另一方面,封闭的类也保证了该类的所有方法都是固定不变的,不会有子类的覆盖方法需要去动态加载。这给编译器做优化时提供了更多的可能,最好的例子是String,它就是final类,Java编译器就可以把字符串常量(那些包含在双引号中的内容)直接变成String
对象,同时对运算符+
的操作直接优化成新的常量,因为final
修饰保证了不会有子类对拼接操作返回不同的值。
对于所有不同的类定义—顶层类(全局或包可见)、嵌套类(内部类或静态嵌套类)都可以用final来修饰。但是一般来说final多用来修饰在被定义成全局(public)的类上,因为对于非全局类,访问修饰符已经将他们限制了它们的也可见性,想要继承这些类已经很困难,就不用再加一层final限制。
另外要提到的是匿名类(Anonymous Class)虽然说同样不能被继承,但它们并没有被编译器限制成final
。
[codesyntax lang=”java” lines=”normal”]
import java.lang.reflect.Modifier; public class Main { public static void main(String[] args) { Runnable anonymous = new Runnable() { @Override public void run() { } }; System.out.println(Modifier.isFinal(anonymous.getClass().getModifiers())); } } // Output: // false
[/codesyntax]
final Method
跟继承观念密切相关是多态(Polymorphism),其中牵扯到了覆盖(Overriding)和隐藏(Hiding)的概念区别(为方便起见,以下对这两个概念统一称为“重写”)。但不同于C++中方法定义是否有加virtual关键字会影响子类相同方法签名的方法是覆盖还是隐藏,在Java里子类用相同方法签名重写父类方法,对于类方法(静态方法)会形成隐藏,而对象方法(非静态方法)只发生覆盖。由于Java允许通过对象直接访问类方法,也使得Java不允许在同一个类中类方法和对象方法有相同的签名。
final
类限定了整个类不能被继承,进而也表示该类里的所有方法都不能被子类所覆盖和隐藏。当类不被final
修饰时,依然可以对部分方法使用final
进行修饰来防止这些方法被子类重写。
同样的,这样的设计破坏了面向对象的多态性,但是final方法可以保证其执行的确定性,从而确保了方法调用的稳定性。在一些框架设计中就会经常见到抽象类的一些已实现方法的方法被限制成final,因为在框架中一些驱动代码会依赖这些方法的实现了完成既定的目标,所以不希望有子类对它进行覆盖。
下边的例子展示了final修饰在不同类型的方法中起到的作用:
[codesyntax lang=”java” lines=”normal”]
package com.iderzheng.other; public class FinalMethods { public static void publicStaticMethod() { } public final void publicFinalMethod() { } public static final void publicStaticFinalMethod() { } protected final void protectedFinalMethod() { } protected static final void protectedStaticFinalMethod() { } final void finalMethod() { } static final void staticFinalMethod() { } private static final void privateStaticFinalMethod() { } private final void privateFinalMethod() { } }
[/codesyntax]
[codesyntax lang=”java” lines=”normal”]
package com.iderzheng.finalkeyword; import com.iderzheng.other.FinalMethods; public class Methods extends FinalMethods { public static void publicStaticMethod() { } // Error: cannot override public final void publicFinalMethod() { } // Error: cannot override public static final void publicStaticFinalMethod() { } // Error: cannot override protected final void protectedFinalMethod() { } // Error: cannot override protected static final void protectedStaticFinalMethod() { } final void finalMethod() { } static final void staticFinalMethod() { } private static final void privateStaticFinalMethod() { } private final void privateFinalMethod() { } }
[/codesyntax]
首先注意上边的例子里,FinalMethods
和Methods
是定义在不同的包(package)下。对于第一个publicStaticMethod
,子类成功重写了父类的静态方法,但因为是静态方法所以发生的其实是“隐藏”。具体表现为调用Methods.publicStaticMethod()
会执行Methods
类中的实现,调用FinalMethods.publicStaticMethod()
时执行并不会发生多态加载子类的实现,而是直接使用FinalMethods
的实现。所以在用子类去访问方法时,会隐藏了父类相同方法签名的方法的可见性。
对于全局方法publicFinalMethod
就像final
修饰方法描述的那样禁止子类定义相同的方法去覆盖它,在编译时就会抛出异常。不过在子类定义方法名字一样但是带有个参数,比如:publicFinalMethod(String x)
是可以的,因为这是同步的方法签名。
在Intellij里,IDE对publicStaticFinalMethod
显示了一个警告:'static' method declared 'final'
。在它看来这是多余的,但从实例中可以看出final
同样禁止了子类定义相同的静态方法去隐藏它。在实际开发中,子类和父类定义相同的静态方法的行为是极为不推荐的,因为隐藏方法需要开发者注意使用不同类名限定会有不同的效果,就很容易带来错误。而且在类的内部是可以不使用类名限定直接调用静态方法,开发者再度做继承时可能没有注意到隐藏的存在默认在使用父类的方法时就会发现不是预期的结果。所以对静态方法应该默认已经是final
而不该去隐藏他们,也因此IDE觉得是多余的修饰。
父类中protected
修饰和public
修饰的方法对于子类都是可见的,所以final
修饰protected
方法的情况和public方法是一样的。想提到的是在实际开发中一般很少定义protected
静态方法,因为这样的方法实用性太低。
对于父类package方法,处在不同的package下的子类是不可见的,private方法已经定制了只有父类自己可访问。所以编译器允许子类去定义相同的方法。但这不形成覆盖或隐藏,因为父类已经通过修饰符来隐藏了这些方法,而非子类的重写造成的。当然如果子类和父类在同一package下,那么情况也和之前的public
、protected
一样了。
final Variable
简单说,Java里的final
变量只能且必须被初始化一次,之后该变量就与该值绑定。但该次赋值不一定要在变量被定义时被立刻初始化,Java也支持通过条件语句给final
变量不同的结果,只是无论如何该变量都只能变赋值一次。
不过Java的final变量并非绝对的常量,因为Java的对象变量只是引用值,所以final
只是表示该引用不能改变,而对象的内容依然可以修改。对比C/C++的指针,它更像是type * const variable
而非type const * variable
。
Java的变量可以分为两类:局部变量(Local Variable)和类成员变量(Class Field)。下边还是用代码来分别介绍它们的初始化情况。
Local Variable
局部变量主要指定义在方法中的变量,出了方法它们就会消失不可访问。其中有可分出一种特殊情况:函数参数。对于这种情况,其初始化与函数被调用时传入的参数绑定。
对于其他的局部变量,它们被定义在方法中,其值就可以被有条件的初始化:
[codesyntax lang=”java” lines=”normal”]
public String method(final boolean finalParam) { // Error: final parameter finalParam may not be assigned // finalParam = true; final Object finalLocal = finalParam ? new Object() : null; final int finalVar; if (finalLocal != null) { finalVar = 21; } else { finalVar = 7; } // Error: variable finalVar might already have been assigned // finalVar = 80; final String finalRet; switch (finalVar) { case 21: finalRet = "me"; break; case 7: finalRet = "she"; break; default: finalRet = null; } return finalRet; }
[/codesyntax]
从上述例子中可以看出被final
修饰的函数参数无法被赋予新的值,但是其他final
的局部变量则可以在条件语句中被赋值。这样也给final
提供了一定的灵活性。
当然条件语句中的所有条件里都应该包含对final
局部变量的赋值,否则就会得到变量可能未被初始化的错误
[codesyntax lang=”java” lines=”normal”]
public String method(final Object finalParam) { final int finalVar; if (finalParam != null) { finalVar = 21; } final String finalRet; // Error: variable finalVar might not have been initialized switch (finalVar) { case 21: finalRet = "me"; break; case 7: finalRet = "she"; break; } // Error: variable finalRet might not have been initialized return finalRet; }
[/codesyntax]
理论上局部变量没有被定义成final
的必要,合理设计的方法应该可以很好的维护局部变量。只是在Java方法中使用匿名函数做闭包时,Java要求被引用的局部变量必须被定义为final
:
[codesyntax lang=”java” lines=”normal”]
public Runnable method(String string) { int integer = 12; return new Runnable() { @Override public void run() { // ERROR: needs to be declared final System.out.println(string); // ERROR: needs to be declared final System.out.println(integer); } }; }
[/codesyntax]
Class Field
类成员变量其实也能分成两种:静态和非静态。对于静态类成员变量,因为它们与类相关,所以除了在定义时直接初始化,还可以放在static block中,而使用后者可以执行更多复杂的语句:
[codesyntax lang=”java” lines=”normal”]
package com.iderzheng.finalkeyword; import java.util.HashSet; import java.util.LinkedHashSet; import java.util.Set; public class StaticFinalFields { static final int STATIC_FINAL_INIT_INLINE = 7; static final Set<Integer> STATIC_FINAL_INIT_STATIC_BLOCK; /** Static Block **/ static { if (System.currentTimeMillis() % 2 == 0) { STATIC_FINAL_INIT_STATIC_BLOCK = new HashSet<>(); } else { STATIC_FINAL_INIT_STATIC_BLOCK = new LinkedHashSet<>(); } STATIC_FINAL_INIT_STATIC_BLOCK.add(7); STATIC_FINAL_INIT_STATIC_BLOCK.add(21); } }
[/codesyntax]
Java中也有非静态的block可以对非静态的成员变量进行初始化,但是对于这些变量,更多的时候还是放在构造函数(constructor)里进行初始化。当然必须保证每个final变量在构造函数里都有被初始化一次,如果通过this()
调用了其他的构造函数,则这些final变量不能再在该构造函数里被赋值了。
[codesyntax lang=”java” lines=”normal”]
package com.iderzheng.finalkeyword; public class FinalFields { final long FINAL_INIT_INLINE = System.currentTimeMillis(); final long FINAL_INIT_BLOCK; final long FINAL_INIT_CONSTRUCTOR; /** Initial Block **/ { FINAL_INIT_BLOCK = System.nanoTime(); } FinalFields() { this(217); } FinalFields(boolean bool) { FINAL_INIT_CONSTRUCTOR = 721; } FinalFields(long init) { FINAL_INIT_CONSTRUCTOR = init; } }
[/codesyntax]
当final
用来修饰类(Class) 和方法(Method)时,它主要影响面向对象的继承性,没有了继承性就没有了子类对父类的代码依赖,所以在维护时修改代码就不用考虑会不会破坏子类的实现,就显得更加方便。而当它用在变量(Variable)上时,Java保证了变量值不会修改,更进一步设计保证类的成员也不能修改的话,那么整个变量就可以变成常量使用,对于多线程编程是非常有利的。所以final
对于代码维护有非常好的作用。
1 Comment