1.Java中的隐藏(hide):
问题:
下面的小程序用来演示java中的隐藏,代码如下:
class Base{
public String className = "Base";
}
class Derived extends Base{
private String className = "Derived";
}
public class Test{
public static void main(String[] args){
new Derived().className);
}
}
有人觉得应该打印输出Base,也有觉得Derived类编译报错,因为同名的变量访问控制权限比父类更严格了。
真实情况确实是程序编译有错,但是不是Derived类编译报错,而是在main函数的打印语境中报错,因为Derived类的className属性是私有的,无法访问。
原因:
java中,子类和父类拥有相同方法签名的方法被称为方法覆盖,在方法覆盖中子类方法的访问控制权限不能被父类更严格,同时子类方法也不能抛出被父类方法更多的异常。
而子类和父类拥有同名的变量被称为变量隐藏(hide),变量隐藏没有访问控制权限的限制,因此虽然Base类中的className属性是共有的,子类Derived类可以继承,但是由于子类Derived中拥有同名的className变量,即使是私有的,Derived类的className变量还是会隐藏了父类Base类的className变量。
结论:
如果想要解决上述程序中变量隐藏带来的编译错误,有以下两种解决方法:
方法1:使用类型转换将子类转换父类,代码如下:
class Base{
public String className = "Base";
}
class Derived extends Base{
private String className = "Derived";
}
public class Test{
public static void main(String[] args){
new Derived()).className);
}
}
此时程序的打印输出结果为Base。
方法2:使用方法覆盖代替变量隐藏,代码如下:
class Base{
public String getClassName() {
return "Base";
}
}
class Derived extends Base{
public String getClassName() {
return "Derived";
}
}
public class Test{
public static void main(String[] args){
new Derived().getClassName());
}
}
此时程序的打印结果为Derived。
java中允许在程序中隐藏域,成员类型,甚至是静态方法,但是最好不要这么做,因为隐藏带来的问题通常让人混乱(被隐藏的域会被阻止继承),同时隐藏也违反了程序设计原则中的里氏替换原则。
2.java中的遮掩(Obscure):
问题:
下面的小程序用来演示java中的遮掩,代码如下:
class X{
static class Y{
static String Z = "Black";
}
static C Y = new C();
}
class C{
"White";
}
public class Test{
public static void main(String[] args){
System.out.println(X.Y.Z);
}
}
对于程序运行到底打印输出Black还是White,相信很多人都无法确定,通常编译器会拒绝模棱两可的程序,改程序看起来模棱两可,因此应该被编译器拒绝,但是真实情况是该程序可以正常运行,打印输出结果为White。
原因:
java中一个变量可以遮掩具有相同名字的一个类型,只要她们都在同一作用范围内:如果这个名字被用于变量与类型都被许可的范围,那么它将引用到变量上,即变量遮掩类型,类似地,一个变量或者一个类型可以遮掩一个包。遮掩是唯一一种两个名字位于不同的名字空间的名字重用形式。
上述程序代码刚好为我们演示了变量遮掩类型这一原则,由于Y既是类型名又是变量名,并且处于同一作用范围,虽然类型声明在变量声明之前,变量名还是会遮掩类型名。
结论:
如果遵循java的命名规范,可以有效规避遮掩,java命名规范如下:
(1).变量名通常是以首字母小写的驼峰命名法.
(2).类型名通常是以首字母大写的驼峰命名法;包名应该是全小写。
(3).常量名应该是全部大写(多个单词直接使用下划线连接)。
(4).单个的大写字母只能用于类型参数,就像泛型接口Map<K, V>中那样。
因此,遵循命名规范重写上面的程序就可以毫无歧义地打印输出Black,代码如下:
class Ex{
static class Why{
static String Z = "Black";
}
static See y = new See();
}
class See{
"White";
}
public class Test{
public static void main(String[] args){
System.out.println(Ex.Why.Z);
}
}
遵循java命名规范是解决遮掩的最好方式,但是除了该方式以外, 如果不允许修改X,Y和C这3个类型名,且不允许使用反射也可以使用如下3种反射打印输出Black:
方法1:
巧用类型转换,代码如下:
class X{
static class Y{
static String Z = "Black";
}
static C Y = new C();
}
class C{
"White";
}
public class Test{
public static void main(String[] args){
null).Z);
}
}
由于在类型转换的时候转型表达式对象前面的只能是类型不能是变量或对象,因此编译器会自动做出合适的选择,但是该例子中的该方法只能对要访问的静态域生效,有一定局限性。
方法2:
使用继承,代码如下:
class X{
static class Y{
static String Z = "Black";
}
static C Y = new C();
}
class C{
"White";
}
public class Test{
static class Xy extends X.Y{}
public static void main(String[] args){
System.out.println(Xy.Z);
}
}
只能针对类进行继承,因此编译器就可以区别出所继承的不是变量,绕过遮掩。
方法3:
使用泛型,代码如下:
class X{
static class Y{
static String Z = "Black";
}
static C Y = new C();
}
class C{
"White";
}
public class Test{
public static <T extends X.Y> void main(String[] args){
System.out.println(T.Z);
}
}
泛型的上边界和下边界可以起到extends类似的作用。
3.java中的遮蔽(shadow):
问题:
下面的代码展示java中的遮蔽,代码如下:
import static java.util.Arrays.toString;
public class Test{
public static void main(String[] args){
1, 2, 3, 4, 5);
}
static void printArgs(Object... args){
System.out.println(toString(args));
}
}
上述代码中通过静态导入java.util.Arrays类的toString(Object[])方法,期望打印输出给定的数组,所以应该期望程序打印输出[1, 2, 3, 4, 5]。
但是真实情况是程序编译报错The method toString() in the type Object is not applicable for the arguments (Object[])。
原因:
在java中遮蔽是指一个变量、方法或者类型可以分别遮蔽在一个闭合的文本范围内的具有相同名字的所有变量、方法或类型。java中的同名局部变量优先同名全局变量就是遮蔽的最常见例子。
编译器在选择运行期将被调用的方法时,所作的第一件事就是在肯定能找到该方法的范围内挑选,编译器将在包含了具有恰当名字的方法的最小闭合范围内进行挑选,上述程序中的选择方法的最小范围就是Test类,它包含了从Object继承而来的toString方法,而静态导入的toString方法恰好被从Object继承而来的同名方法所遮蔽。
当一个声明遮蔽了另一个声明时,简单名称就爱那个引用到遮蔽声明中的实体,即本身就属于某个范围的成员在该范围内与静态导入相比具有优先权。
结论:
明白了上述程序问题是有java遮蔽引起之后,解决起来就比较容易了,使用普通导入声明代替静态导入,代码如下:
import java.util.Arrays;
public class Test{
public static void main(String[] args){
1, 2, 3, 4, 5);
}
static void printArgs(Object... args){
System.out.println(Arrays.toString(args));
}
}
java中遮蔽(shadow)和遮掩(obscure)非常类似,很多人经常搞混淆,二者的区别如下:
遮蔽:一个声明只能遮蔽类型相同的另一个声明:一个类型声明可以遮蔽另一个类型声明;一个方法声明可以遮蔽另一个方法声明;一个变量声明可以遮蔽另一个变量声明、
遮掩:变量声明可以遮掩类型和包声明;类型声明可以遮掩包声明。
4.条件操作符:
问题:
下面的程序演示条件操作符在JDK1.4和JDK1.5之后的变化,代码如下:
import java.util.Random;
public class Test{
private static Random rnd = new Random();
public static Test flip(){
return rnd.nextBoolean() ? Heads.INSTANCE : Tails.INSTANCE;
}
public static void main(String[] args){
System.out.println(flip());
}
}
class Heads extends Test{
private Heads(){}
public static final Heads INSTANCE = new Heads();
public String toString(){
return "heads";
}
}
class Tails extends Test{
private Tails(){}
public static final Tails INSTANCE = new Tails();
public String toString(){
return "tails";
}
}
上述程序没有使用任何JDK1.5的新特性,在JDK1.5之后的版本可以正常编译和运行。
在编译时如果使用“-source 1.4”参数让JDK使用JDK1.4对其进行编译,发现会报编译错误:incompatible types for ?: neither is a subtype of the other......
原因:
条件操作符(?:)的行为在JDK1.5之前是非常受限制的,当第二个和第三个操作数是引用类型时,条件操作符要求它们其中的一个必须是另一个的子类型,由于Heads和Tails彼此都不是对方的子类型,因此产生了第二个和第三个操作数类型不兼容编译错误。
结论:
明白错误的原因之后,想让上述代码在JDK1.4中顺利编译通过可以将其中一个操作数类型转换为公共超类类型,代码如下:
public static Test flip(){
return rnd.nextBoolean() ? (Test)Heads.INSTANCE : Tails.INSTANCE;
}
在JDK1.5之后,条件操作符在第二个和第三个操作数是引用类型时总是合法的,其结果类型是这两种类型的最小公共超类,即它等效于T choose(T a, T b)。
在JDK1.4和更早版本中条件操作符这种限制带来的问题非常普遍和频繁,经常使用如下的类型安全枚举模式来避免该问题,代码如下:
import java.util.Random;
public class Test{
public static final Test HEADS = new Test("heads");
public static final Test TAILS = new Test("tails");
private final String name;
private Test(String name){
this.name = name;
}
public String toString(){
return name;
}
private static Random rnd = new Random();
public static Test flip(){
return rnd.nextBoolean() ? HEADS : TAILS;
}
public static void main(String[] args){
System.out.println(flip());
}
}
在JDK1.5之后,可以直接使用java的枚举来编写,代码如下:
import java.util.Random;
public enum Test{
HEADS, TAILS;
public String toString(){
return name().toLowerCase();
}
private static Random rnd = new Random();
public static Test flip(){
return rnd.nextBoolean() ? HEADS : TAILS;
}
public static void main(String[] args){
System.out.println(flip());
}
}