首页 > 代码库 > [Java解惑]类

[Java解惑]类


... 22
34.      参数兼容的方法重载... 22
35.      静态方法不具有多态特性... 23
36.      属性只能被隐藏... 23
37.      属性对嵌套类的遮掩... 24
38.      不能重写不同包中的defualt访问权限方法... 24
39.      重写、隐藏、重载、遮蔽、遮掩... 25
40.      构造器中静态常量的引用问题... 27
41.      instanceof与转型... 29
42.      父类构造器调用已重写的方法... 30
43.      静态域与静态块的初始顺序... 31
44.      请使用引用类型调用静态方法... 31
45.      循环中的不能声明局部变量... 32
46.      内部类反射... 32

34. 参数兼容的方法重载


public class Confusing {
       private Confusing(Object o) {
              System.out.println("Object");
       }
       private Confusing(double[] dArr) {
              System.out.println("double array");
       }
       public static void main(String[] args) {
              new Confusing(null);
       }
}
上面的程序打印的是“double array”,为什么?
 
null可代表任何非基本类型对象。
 
Java的重载解析过程是分两阶段运行的。第一阶段选取所有可获得并且可应用的方法或构造器。第二阶段在第一阶段选取的方法或构造器中选取最精确的一个。如果一个方法或构造器可以接受传递给另一个方法或构造器的任何参数,那么我们就说第一个方法比第二个方法缺乏精确性,调用时就会选取第二个方法。
 
使用上面的规则来解释该程序:构造器Confusing(Object o)可以接受任何传递Confusing(double[] dArr)的参数,因此Confusing(Object o)相对缺乏精确性,所以Confusing(null)会调用Confusing(double[] dArr)构造器。
 
如果想强制要求编译器选择一个自己想要的重载版本,需要将实参强制转型为所需要的构造器或方法的参数类型:如这里要调用Confusing(Object o)本版,则这样调用:Confusing((Object)null)
 
如果你确实进行了重载,那么请确保所有的重载版本所接受的参数类型都互不兼容,这样,任何两个重载版本都不会同时是可应用的。

35. 静态方法不具有多态特性


class A1 {
       public static void f() {
              System.out.println("A1.f()");
       }
}
class A2 extends A1 {
       public static void f() {
              System.out.println("A2.f()");
       }
}
class T {
       public static void main(String[] args) {
              A1 a1 = new A1();
              A1 a2 = new A2();
              // 静态方法不具有多态效果,它是根据引用声明类型来调用
              a1.f();// A1.f()
              a2.f();// A1.f()
       }
}
 
对静态方法的调用不存在任何动态的分派机制。当一个程序调用了一个静态方法时,要被调用的方法都是在编译时就被选定的,即调用哪个方法是根据该引用被声明的类型决定的。上面程序中a1a2引用的类型都是A1类型,所以调用的是A1中的f()方法。

36. 属性只能被隐藏


class P {
       public String name = "P";
}
 
class S extends P {
       // 隐藏父类的name域,而不像方法属于重写
       private String name = "S";
}
 
public class Test {
       public static void main(String[] args) {
              // !! S.name is not visible
              // !! System.out.println(new S().name);
              // 属性不能被重写,只是被隐藏,所以不具有多态性为
              System.out.println(((P) new S()).name);// p
       }
}
 
属性的调用与静态方式的调用一样,只与前面引用类型相关,与具体的实例没有任何关系。
 
当你在声明一个域、一个静态方法或一个嵌套类型时,如果其名与基类中相对应的某个可访问的域、方法或类型相同时,就会发生隐藏。

37. 属性对嵌套类的遮掩


class X {
       static class Y {
              static String Z = "Black";
       }
       static C Y = new C();
}
class C {
       String Z = "White";
}
public class T {
       public static void main(String[] args) {
              System.out.println(X.Y.Z);// White
              System.out.println(((X.Y) null).Z);// Black
       }
}
当一个变量和一个类型具有相同的名字,并且它们位于相同的作用域时,变量名具有优先权。变量名将遮掩类型名。相似地,变量名和类型名可以遮掩包名。

38. 不能重写不同包中的defualt访问权限方法


package click;
public class P {
       public void f() {
              //因为子类没有重写该方法,所以调用的还是父类中的方法
              prt();
       }
       void prt() {
              System.out.println("P");
       }
}
 
package hack;
import click.P;
public class T {
       private static class S extends P {
              // 这里没有重写父类的方法,因为父类方法不可见
              void prt() {
                     System.out.println("S");
              }
       }
       public static void main(String[] args) {
              new S().f();// P
       }
}
 
一个包内私有(default)的方法不能被位于另一个包中的某个方法直接重写。
 

39. 重写、隐藏、重载、遮蔽、遮掩


重写:一个实例方法可以重写在其超类中可访问到的具有相同签名的所有实例方法,从而能动态分派,换句话说,VM将基于实例的运行期类型来选择要调用的重写方法。重写是面向对象编程技术的基础。
public class P{
       public void f(){}
}
class S extends P{
       public void f(){}//重写
}
 
重写时异常要求:
l  如果父类方法抛出的是捕获型异常,则子类也只能抛出同类的捕获型异常或其子类,或不抛出。
l  父类抛出捕获型异常,子类却抛出运行时异常,这是可以,因为抛出运行时就相当于没有抛出任何异常。
l  如果父类抛出的是非捕获型异常,则子类可以抛出任意的非捕获型异常,没有扩大异常范围这一问题。
l  如果父类抛出的是非捕获异常,子类也可以不用抛出,这与父类为捕获型异常是一样的。
l  如果父类抛出的是非捕获异常,子类就不能抛出任何捕获型异常,因为这样会扩大异常的范围。
 
返回类型的协变:从Java SE5开始子类方法可以返回比它重写的基类方法更具体的类型,但是这在早先的Java版本是不允许——重写时子类的返回类型一定要与基类相同。但要注意的是:子类方法返回类型要是父类方法返回类型的子类,而不能反过来。
 
方法参数类型协变:如果父子类同名方法的参数类型为父子关系,则为参数类型协变,此时不属于重写,而是方法的重载,以前版本就是这样。
 
如果父类的方法为private时,子类同名的方法的方法名前可以使用任何修饰符来修饰。我们可以随意地添加一个新的私有成员(方法、域、类型),或都是修改和删除一个旧的私有成员,而不需要担心对该类的客户造成任何损害。换而言之,私有成员被包含它们的类完全封装了。
 
父与子类相同签名方法不能一静一动的,即父类的方法是静态的,而子类不是,或子类是静态的,而父类不是,编译时都不会通过。
 
父与子相同签名方法都是静态的方法时,方法名前的修饰符与非静态方法重写的规则一样,但不属于重写,因为静态方法根本就不具有多态性。
 
最后,属于成员也不具有多态特性,相同名的域属于隐藏,而不管域前面的修饰符为什么:
class P {
       public static final String str = "P";
}
class S extends P {
       //编译能通过。可以是final,这里属于隐藏
       public static final String str = "S";
       public static void main(String[] args) {
              System.out.println(S.str);//s
       }
}
 
隐藏:一个域、静态方法或成员类型可以分别隐藏在其超类中可访问到的具有相同名字(对方法而言就是相同的方法签名)的所有域、静态方法或成员类型。隐藏一个成员将阻止其被继承。
public class P{
       public static void f(){}
}
class S extends P{
       //隐藏,不会继承P.f()
       public static void f(){}
}
 
重载:在某个类中的方法可以重载另一个方法,只要它们具有相同的名字和不同的签名。由调用所指定的重载方法是在编译期选定的。
public class T{
       public static void f(int i){}
       public static void f(String str){}//重载
}
 
遮蔽:一个变量、方法或类型可以分别遮蔽在一个闭合的文本范围内的具有相同名字的所有变量、方法或类型。如果一个实体被遮蔽了,那么你用它的简单名是无法引用到它的;根据实体的不同,有时你根本就无法引用到它。
public class T {
       private static String str = "feild";
       public static void main(String[] args) {
              String str = "local";// 遮蔽
              System.out.println(str);// local
              // 可以通过适当的方式来指定
              System.out.println(T.str);// feild
       }
}
 
public class T {
       private final int size;
       // 参数属于方法局部范围类变量,遮蔽了同名成员变量
       public T(int size) {
              //使用适当的引用来指定
              this.size = size;
       }
}
 
遮掩:一个变量可以遮掩具有相同名字的一个类型,只要它们都在同一个范围内:如果这个名字被用于变量与类型都被许可的范围,那么它将引用到变量上。相似地,一个变量或一个类型可以遮掩一个包。遮掩是唯一一种两个名字位于不同的名字空间的名字重用形式,这些名字空间包括:变量、包、方法或类型。如果一个类型或一个包被遮掩了,那么你不能通过其简单名引用到它,除非是在这样一个上下文环境中,即语法只允许在其名字空间中出现一种名字:
public class T {
       static String System;
       public static void main(String[] args) {
              // !!不能编译,遮掩 java.lang.System
              // !! System.out.println("Hello");
              // 可明确指定
              java.lang.System.out.println("Hello");
       }
}

40. 构造器中静态常量的引用问题


class T {
       // 先于静态常量t初始化,固可以在构造器中正常使用
       private static final int y = getY();
       /*
        * 严格按照静态常量声明的先后顺来初始化:即t初始
        * 化完后,才初始化后面的静态常量j,所以构造器中
        * 引用后面的静态常量j时,会是0,即内存清零时的值
        */
       public static final T t = new T();
       // 后于静态常量t初始化,不能在构造器中正常使用
       private static final int j = getJ();
       private final int i;
 
       static int getY() {
              return 2;
       }
 
       static int getJ() {
              return 2;
       }
 
       // 单例
       private T() {
              i = y - j - 1;
//为什么j不是2
              System.out.println("y=" + y + " j=" + j);// y=2 j=0
       }
 
       public int getI() {
              return i;
       }
 
       public static void main(String[] args) {
              System.out.println(T.t.getI());// 1
              System.out.println(T.j);// 2
       }
}
 
该程序所遇到的问题是由类初始化顺序中的循环而引起的:初始化t时需调用构造函数,而调用构造函数前需初始化所有静态成员,此时又包括对t的再次初始化。
 
T类的初始化是由虚拟机对main方法的调用而触发的。首先,其静态域被设置缺省值,其中yj被初始化为0,而t被初始化为null。接下来,静态域初始器按照其声明的顺序执行赋值动作。第一个静态域是y,它的值是通过调用getY获取的,赋值操作完后结果为2。第一个初始化完成后,再进行第二个静态域的赋值操作,第二个静态域为t,它的值是通过调用T()构造函数来完成的。这个构造器会用二个涉及静态域yj来初始化非静态域i。通常,读取一个静态域是会引起一个类被初始化,但是我们又已经在初始化T类。JavaVM规范对递归的初始化尝试会直接被忽略掉(按理来说在创建出实例前需初始化完所有的静态域后再来创建实例),这样就导致在静态域被初始化之前就调用了构造器,后面的静态域j将得不到正常的初始化前就在构造器中被使用了,使用时的值为内存分配清零时的,即0
t初始化完后,再初始化j,此时j得到的值为2,但此时对i的初始化过程来说已经晚了。
 
final类型的静态域被初始化之前,存在着读取其值的可能,而此时该静态域包含的还只是其所属类型的缺省值。这是与直觉想违背的,因为我们通常会将final类型的域看作是常量,但final类型的域只有在其初始化表达式是字面常量表达式时才是真正的常量。
 
再看看另一程序:
class T {
       private final static int i = getJ();
       private final static int j;
       static {
              j = 2;
       }
       static int getJ() {
              return j;
       }
       public static void main(String[] args) {
              System.out.println(T.j);// 2
              /*
               * 因为上面的语句已经初使完T类,所以下面语句是
               * 再引起类的初始化,这里的结果用的是第一
               *  次( 即上面语句)的初始化结果
               */
              System.out.println(T.i);// 0
       }
}
为什么第二个输出是0而不是2呢?这就是因为VM是严格按照你声明的顺序来初始化静态域的,所以前面的引用后面的静态域时,基本类型就是0,引用类型就会是null
 
所以要记住:静态域,甚至是final类型的静态域,可能会在它们被初始化之前,被读走其缺省值。
 
另,类初始化规则请参考《惰性初始化》一节

41. instanceof与转型


System.out.println(null instanceof String);//false
System.out.println(new Object() instanceof String);//false
//编译能通过
System.out.println((Object) new Date() instanceof String);//false
//!!程序不具有实际意义,但编译时不能通过
//!!System.out.println(new Date() instanceof String);
//!!运行时抛ClassCastException,这个程序没有任何意义,但可以编译
//!!System.out.println((Date) new Object());
 
null可以表示任何引用类型,但是instanceof操作符被定义为在其左操作数为null时返回false
 
如果instanceof告诉你一个对象引用是某个特定类型的实例,那么你就可以将其转型为该类型,并调用该类型的方法,而不用担心会抛出ClassCastExceptionNullPointerException异常。
 
instanceof操作符有这样的要求:左操作数要是一个对象的或引用,右操作数是一个引用类型,并且这两个操作数的类型是要父子关系(左是右的子类,或右是左的子类都行),否则编译时就会出错。

42. 父类构造器调用已重写的方法


public class P {
       private int x, y;
       private String name;
 
       P(int x, int y) {
              this.x = x;
              this.y = y;
              // 这里实质上是调用子类被重写的方法
              name = makeName();
       }
 
       protected String makeName() {
              return "[" + x + "," + y + "]";
       }
 
       public String toString() {
              return name;
       }
 
}
 
class S extends P {
       private String color;
 
       S(int x, int y, String color) {
              super(x, y);
              this.color = color;
       }
 
       protected String makeName() {
              return super.makeName() + ":" + color;
       }
 
       public static void main(String[] args) {
              System.out.println(new S(1, 2, "red"));// [1,2]:null
       }
}
在一个构造器调用一个已经被其子类重写了的方法时,可能会出问题:如果子类重写的方法要访问的子类的域还未初始化,因为这种方式被调用的方法总是在实例初始化之前执行。要想避免这个问题,就千万不要在父类构造器中调用已重写的方法。

43. 静态域与静态块的初始顺序


public class T {
       public static int i = prt();
       public static int y = 1;
       public static int prt() {
              return y;
       }
 
       public static void main(String[] args) {
              System.out.println(T.i);// 0
       }
}
上面的结果不是1,而是0,为什么?
 
类初始化是按照静态域或静态块在源码中出现的顺序去执行这些静态初始器的(即谁先定义,就先初始化谁),上现程序中由于i先于y声明,所以先初始化i,但由于i初始化时需要由y来决定,此时y又未初始化,实为初始前的值0,所以i的最后结果为0

44. 请使用引用类型调用静态方法


public class Null {
       public static void greet() {
              System.out.println("Hello world!");
       }
 
       public static void main(String[] args) {
              ((Null) null).greet();
       }
}
上面程序运行时不会打印NullPointerException异常,而是输出"Hello world!",关键原因是:调用静态方法时将忽略前面的调用对象或表达示,只与对象或表达式计算结果的类型有关。
 
在调用静态方法时,一定要使用类去调用,或是静态导入后直接使用。

45. 循环中的不能声明局部变量


for (int i = 0; i < 1; i++)
       Object o ; //!! 编译不能通过
 
for (int i = 0; i < 1; i++)
       Object o = new Object(); //!! 编译不能通过
 
一个本地变量声明看起来像是一条语句,但是从技术上来说不是。
 
Java语言规范不允许一个本地变量声明语句作为一条语句在forwhiledo循环中重复执行。
 
一个本地变量声明作为一条语句只能直接出现在一个语句块中(一个语句块是由一对花 括号以及包含在这对花括号中的语句和声明构成的):
for (int i = 0; i < 1; i++) {
       Object o = new Object(); // 编译OK
}

46. 内部类反射


public class Outer {
       public class Inner {
              public String toString() {
                     return "Hello world";
              }
       }
       public void getInner() {
              try {
                     // 普通方式创建内部类实例
                     System.out.println(new Outer().new Inner());// Hello world
                     //!! 反射创建内部类,抛异常:java.lang.InstantiationException:Outer$Inner
                     System.out.println(Inner.class.newInstance());
              } catch (Exception e) {
              }
       }
       public static void main(String[] args) {
               new Outer().getInner();
       }
}
上面因为构造内部类时外部类实例不存在而抛异常。
 
一个非静态的嵌套类的构造器,在编译的时候会将一个隐藏的参数作为它的第一个参数,这个参数表示它的直接外围实例。如果使用反射创建内部类,则要传递个隐藏参数的唯一方法就是使用java.lang.reflect.Constructor
Constructor c = Inner.class.getConstructor(Outer.class);//获取带参数的内部类构造函数
System.out.println(c.newInstance(Outer.this));//反射时还需传进外围类

[Java解惑]类