首页 > 技术 > Java的几个术语(覆写, 重载, 隐藏, 遮蔽, 遮掩)

Java的几个术语(覆写, 重载, 隐藏, 遮蔽, 遮掩)

这几天看Java Puzzler,被里面的题目折腾得死去活来,特别是标题里的几个概念。于是决定发篇小文澄清下几个概念。

覆写(Override)

即子类的实例方法覆写了父类的实例方法,即函数的多态。

陷阱1:覆写仅仅对于实例方法有效,对于属性和静态方法不适用。后两者的情况属于隐藏(hide)的范畴。覆写的函数调用时动态绑定的,根据实际类型进行调用;而隐藏的调用则是静态绑定,编译期间就确定了。

陷阱2:覆写要求父类函数的参数和子类函数的参数类型相同。举两个例子:

1
2
3
4
5
6
7
8
9
10
        public class Base{
                public void foo(Object a){}
                public void bar(int a){}
        }
        public class Derived extends Base{
                @Override
                public void foo(String a){}
                @Override
                public void bar(Integer a){}
        }

上面的代码能通过编译吗?结论是都不行。Java要求类型严格的一致,所以上面的例子实际上表现的是函数的重载。

正如例子中写的那样,对于Java5以上的版本,确定子类函数是否覆写了父类函数,只需要给子类函数添加一个@Override注释,如果子类函数并未覆写任何父类函数,则无法通过编译。

陷阱3:覆写要求父类函数的访问限制对子类可见(public/protected/package)。上例子:

1
2
3
4
5
6
7
8
        public class Base{
                private void foo(){System.out.println("base foo");}
                public void bar(){foo();}
        }
        public class Derived extends Base{
                public void foo(){System.out.println("derived foo");}
                public void static main(String[] args){new Derived().bar();}
        }

上面的例子将会输出base foo。子类并没有覆写父类的函数。

在Java虚拟机中,多态的调用通过invokevirtual和invokeinterface两条指令进行调用,前者用于类引用,后者用于接口引用。上面两个方法均是动态绑定。而invokestatic和invokespecial指令使用静态绑定。前者用于静态方法的调用,后者用于实例方法的调用,分为三种情况:第一种用于构造函数(<init>),显然构造函数不需要动态绑定;第二种情况用于调用私有的实例方法,也是上面这个例子里的情况,原因在于,既然这个方法不可能被子类方法覆写,所以直接使用静态绑定(不能推广到final关键字上);第三种情况用于super关键字,在子类方法中强行指定调用父类方法时使用super指代父类,但此类的调用JVM使用动态绑定,具体不再赘述,参见《深入Java虚拟机》第19章的内容。

重载(Overload)

重载这个技术,仅仅是针对语言而言,而在JVM中其实并没有重载的概念。函数的调用匹配本身是通过函数名+函数参数确定的,在匹配函数名后,再匹配函数参数。在选择相同名字的方法时,编译器会选择一个最精确的重载版本。举个例子

1
2
3
4
5
6
7
8
9
10
11
public class Main {
        public int a = 1;
        public static void main(String[] args){
                new Main().test(3);
                new Main().test(new Integer(3));
                new Main().test(new ArrayList<int>());
        }
        public void test(int a){System.out.println("int");}
        public void test(Object a){System.out.println("object");}
        public void test(List<int> a){System.out.println("intlist");}
}

上面的方法会打印int int intlist。第一个调用匹配了同为int,Integer类型的函数;第二个调用在寻找Integer类型的参数未果的情况下,把Integer自动拆箱,匹配到了int类型;第三个调用则匹配了最精确的类型List,而不是不精确的Object。如果我们增加一个Integer类型作为参数的函数:

1
2
3
4
5
6
7
8
9
10
11
12
public class Main {
        public int a = 1;
        public static void main(String[] args){
                new Main().test(3);
                new Main().test(new Integer(3));
                new Main().test(new ArrayList&lt;int&gt;());
        }
        public void test(int a){System.out.println("int");}
                public void test(Integer a){System.out.println("integer");}
        public void test(Object a){System.out.println("object");}
        public void test(List&lt;int&gt; a){System.out.println("intlist");}
}

则会精确匹配类型,打印int integer intlist。

再看一个例子,思想来自于Java Puzzlers #46

1
2
3
4
5
6
7
8
public class Main {
        public int a = 1;
        public static void main(String[] args){
                new Main().test(null);
        }
        public void test(int[] a){System.out.println("intarray");}
        public void test(Object a){System.out.println("object");}
}

这次的输出会是什么?答案是intarray。如果我们调用的是new Main().test(new int[0]),那么结果总是很好理解;而如果传递一个null,则就有违直觉了。关键在于,编译器总是需要寻找更加精确的类型,而匹配的测试并没有使用实参(参数的实际类型)进行匹配。对于编译器来说,null都可以是Object类型变量和int[]类型变量的合法值,而int[]变量可以是Object类型,但Object类型变量不一定是int[]类型,相较之下int[]更为精确,所以编译器选择了int[]。如果一定要让编译器选择Object,那我们只需要通过new Main().test((Object)null)的形式即可。编译器在重载的时候只认形式参数,不认实际参数。

如何匹配重载函数的代码,Spring里有一个查找constructor的例子,在org.springframework.beans.factory.support.ConstructorResolver的autowireConstructor()函数,以前曾经在Spring配置的时候发生过问题Trace到这里。这个匹配算法使用了贪心的思想(代码里这么注释的),对于每个候选的构造函数都通过一个评估函数(getTypeDifferenceWeight())评估与调用参数类型(constructor-arg里配置的)的相似度,最后取最相似的候选函数。有兴趣的朋友可以自己看看。

隐藏(Hide)

隐藏的概念应用于父类和子类之间。子类的域、静态方法和类型声明可以隐藏父类具有相同名称或函数签名的域、静态方法和类型声明。同final方法不能被覆写类似,final的方法也不能被隐藏。考虑下面的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class Point {
        int x = 2;
}
class Test extends Point {
        double x = 4.7;
        void printBoth() {
                System.out.println(x + " " + super.x);
        }
        public static void main(String[] args) {
                Test sample = new Test();
                sample.printBoth();
                System.out.println(sample.x + " " + ((Point)sample).x);
        }
}

上面的代码将打印4.7 2 \n 4.7 2。子类中的x变量隐藏了父类中的x变量,尽管两个变量类型不同。要访问父类中的变量必须使用super关键字或者进行类型强制转换。下面的代码展示了实例变量隐藏静态变量:

1
2
3
4
5
6
7
8
9
class Base{
        public static String str = "Base";
}
public class Main extends Base{
    public String str = "Main";
    public  static void main(String[] args){
        System.out.println(Main.str);
    }
}

上面的代码不能正确编译,原因是子类中的实例域把父类中的静态域给隐藏了。如果去掉子类中str的声明就可以正确编译。

静态方法和类型声明的隐藏的情况,和域隐藏的情况大同小异,不再给出具体的例子。

遮蔽(shadow)

遮蔽(shadow)指的是在一个范围内(比如{}之间),同名的变量,同名的类型声明,同名的域之间的名称遮蔽。我们最常见到的遮蔽就是IDE为我们自动生成的setter:

1
2
3
4
5
6
public class Pojo{
        private int x;
        public void setX(int x){
                this.x = x;
        }
}

即使是变量类型不相同也可以遮蔽。在上个例子中把setX(int x)的参数类型改为short,也能通过编译。函数的遮蔽常见于匿名类里的函数遮蔽原先类的函数等情况。

上面的几种遮掩比较常见,下面情况就比较特殊了(来源于Java Puzzlers#71)。其中Arrays.toString()方法提供了多个重载版本,可以方便的把基本类型数组转换为字符串。

1
2
3
4
5
6
7
8
9
import static java.util.Arrays.toString;
public class ImportDuty{
        public static void main(String[] args){
                printArgs(1, 2, 3, 4, 5);
        }
        static void printArgs(Object... args){
                System.out.println(toString(args));
        }
}

结果是不能编译,并且返回的信息告诉我们,Object.toString()方法不适用。这是为什么呢?原因在于,Object.toString()方法把Arrays.toString()方法给遮蔽了。下面的代码虽然可以编译,但运行时会提示找不到main方法入口:

1
2
3
4
5
6
7
class String{
}
public final class Main{
        public static void main(String[] args){
 
        }
}

原因在于我们自定义的String类型把java.lang.String类型遮蔽了。而如果强行加入import java.lang.String;语句,则不能通过编译。

遮掩(obscure)

遮掩很容易与遮蔽(shadow)混淆。其最重要的区别在于,遮蔽是相同元素之间的遮蔽,变量遮蔽变量,类型声明遮蔽类型声明,函数遮蔽函数。而遮掩却是变量遮掩类型和包声明,类型声明遮掩包声明。

遮掩的例子不多,看下面的例子,来自Java Puzzlers#68:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class ShadesOfGray{
        public static void main(String[] args){
                System.out.println(X.Y.Z);
        }
}
class X{
        static class Y{
                static String Z = "Black";
        }
        static C Y = new C();
}
class C{
        String Z = "White";
}

上面的例子不仅能通过编译,还将打印White。原因就在于当一个变量和一个类型具有相同名称,且具有相同作用域时,变量名优先级大。而类型的声明又具有比包声明更高的优先级。下面一个例子也不能通过编译:

1
2
3
4
5
6
7
public class Obscure{
        static String System;//变量遮掩了java.lang.System类型
 
        public static void main(String[] args){
                System.out.println("hello, obscure world!");
        }
}

小结

只有覆写是运行时的技术,另外其他的技术都是编译期的技术。

除了重载、覆写和作为setter的遮蔽,其他特性都强烈不推荐使用,这只会给你和你的继任者带来无穷无尽的麻烦,事实上,重载我认为也少用为妙,特别是结合了使用不确定参数个数的”…”函数的重载,会使你回头看代码时晕头转向。

p.s. 准备技术笔试/面试的语言类题目,C/C++的话,程序员面试宝典里有一些题目,另外就是C++的那些effective系列的书,具体还请C++达人列举(被点名的同学请自觉回帖)。Java的话,首推Joshua Bloch的Effecitve Java;第二就是Java Puzzlers,而后者其实更适合要参加技术笔试面试的同学。剩下的书,应该还有Practical Java,不过我没看过。

分类: 技术 标签: , , , , ,
  1. 小翼
    2009年6月5日23:16 | #1

    记得当年看Java解惑的时候也很头大,现在差不多都忘了,这几种情况C++里也都有,但分得没这么细
    C++里hide和遮蔽是一样的,但C++并里没有“遮掩”这个术语,标准里说明了编译时的按照类似的优先级来解释名称,并用typename显示告诉编译器后面的是类型

    [回复]

    marshall 回复:

    @小翼, 多谢1楼指出几处错误。不过MS你还没忘了列出基本C++的reference吧。
    遮掩,遮蔽这些东西都是JLS里说的,都挺confusing的

    [回复]

  2. 小翼
    2009年6月6日00:00 | #2

    看完effective+exceptional系列共六本,一般笔试面试上的c++基本题就都不怕了
    至于其它的书,可以看刘未鹏的这篇http://blog.csdn.net/pongba/archive/2007/05/16/1611593.aspx

    [回复]

  3. 不认识
    2009年6月6日19:54 | #3

    弱问你贴代码的那个插件是怎么实现的,用wordpress? 貌似是php
    C#开发的blog,能加什么插件,贴代码时侯保留格式的?
    哪里有下

    [回复]

    marshall 回复:

    @不认识, codebox,wordpress的插件

    [回复]

  1. 本文目前尚无任何 trackbacks 和 pingbacks.

*
To prove you're a person (not a spam script), type the security word shown in the picture. Click on the picture to hear an audio file of the word.
Click to hear an audio file of the anti-spam word


Warning: fsockopen() has been disabled for security reasons in /home/onlymars/public_html/wp/wp-includes/class-snoopy.php on line 1148