类 1、参数兼容的方法重载 Java代码 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)。 如果你确实进行了重载,那么请确保所有的重载版本所接受的参数类型都互不兼容,这样,任何两个重载版本都不会 同时是可应用的。 2、静态方法不具有多态特性 Java代码 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() } } 对静态方法的调用不存在任何动态的分派机制。当一个程序调用了一个静态方法时,要被调用的方法都是在编译时就 被选定的,即调用哪个方法是根据该引用被声明的类型决定的。上面程序中a1与a2引用的类型都是A1类型,所以调用 的是A1中的f()方法。 3、属性只能被隐藏 Java代码 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 } } 属性的调用与静态方式的调用一样,只与前面引用类型相关,与具体的实例没有任何关系。 当你在声明一个域、一个静态方法或一个嵌套类型时,如果其名与基类中相对应的某个可访问的域、方法或类型相同 时,就会发生隐藏。 4、属性对嵌套类的遮掩 Java代码 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 } } 当一个变量和一个类型具有相同的名字,并且它们位于相同的作用域时,变量名具有优先权。变量名将遮掩类型名。 相似地,变量名和类型名可以遮掩包名。 5、不能重写不同包中的defualt访问权限方法 Java代码 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)的方法不能被位于另一个包中的某个方法直接重写。 关于私有方法,还有属性能否被重写,请参考 方法能覆写(重写),属性能覆写吗? 6、重写、隐藏、重载、遮蔽、遮掩 重写:一个实例方法可以重写在其超类中可访问到的具有相同签名的所有实例方法,从而能动态分派,换句话说,VM 将基于实例的运行期类型来选择要调用的重写方法。重写是面向对象编程技术的基础。 Java代码 public class P{ public void f(){} } class S extends P{ public void f(){}//重写 } 重写时异常要求: ? 如果父类方法抛出的是捕获型异常,则子类也只能抛出同类的捕获型异常或其子类,或不抛出。 ? 父类抛出捕获型异常,子类却抛出运行时异常,这是可以,因为抛出运行时就相当于没有抛出任何异常。 ? 如果父类抛出的是非捕获型异常,则子类可以抛出任意的非捕获型异常,没有扩大异常范围这一问题。 ? 如果父类抛出的是非捕获异常,子类也可以不用抛出,这与父类为捕获型异常是一样的。 ? 如果父类抛出的是非捕获异常,子类就不能抛出任何捕获型异常,因为这样会扩大异常的范围。 返回类型的协变:从Java SE5开始子类方法可以返回比它重写的基类方法更具体的类型,但是这在早先的Java版本是 不允许——重写时子类的返回类型一定要与基类相同。但要注意的是:子类方法返回类型要是父类方法返回类型的子 类,而不能反过来。 方法参数类型协变:如果父子类同名方法的参数类型为父子关系,则为参数类型协变,此时不属于重写,而是方法的 重载,以前版本就是这样。 如果父类的方法为private时,子类同名的方法的方法名前可以使用任何修饰符来修饰。我们可以随意地添加一个新的 私有成员(方法、域、类型),或都是修改和删除一个旧的私有成员,而不需要担心对该类的客户造成任何损害。换 而言之,私有成员被包含它们的类完全封装了。 父与子类相同签名方法不能一静一动的,即父类的方法是静态的,而子类不是,或子类是静态的,而父类不是,编译 时都不会通过。 父与子相同签名方法都是静态的方法时,方法名前的修饰符与非静态方法重写的规则一样,但不属于重写,因为静态 方法根本就不具有多态性。 最后,属于成员也不具有多态特性,相同名的域属于隐藏,而不管域前面的修饰符为什么: Java代码 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 } } 隐藏:一个域、静态方法或成员类型可以分别隐藏在其超类中可访问到的具有相同名字(对方法而言就是相同的方法 签名)的所有域、静态方法或成员类型。隐藏一个成员将阻止其被继承。 Java代码 public class P{ public static void f(){} } class S extends P{ //隐藏,不会继承P.f() public static void f(){} } 重载:在某个类中的方法可以重载另一个方法,只要它们具有相同的名字和不同的签名。由调用所指定的重载方法是 在编译期选定的。 Java代码 public class T{ public static void f(int i){} public static void f(String str){}//重载 } 遮蔽:一个变量、方法或类型可以分别遮蔽在一个闭合的文本范围内的具有相同名字的所有变量、方法或类型。如果 一个实体被遮蔽了,那么你用它的简单名是无法引用到它的;根据实体的不同,有时你根本就无法引用到它。 Java代码 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; } } 遮掩:一个变量可以遮掩具有相同名字的一个类型,只要它们都在同一个范围内:如果这个名字被用于变量与类型都 被许可的范围,那么它将引用到变量上。相似地,一个变量或一个类型可以遮掩一个包。遮掩是唯一一种两个名字位 于不同的名字空间的名字重用形式,这些名字空间包括:变量、包、方法或类型。如果一个类型或一个包被遮掩了, 那么你不能通过其简单名引用到它,除非是在这样一个上下文环境中,即语法只允许在其名字空间中出现一种名字: Java代码 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"); } } 7、构造器中静态常量的引用问题 Java代码 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方法的调用而触发的。首先,其静态域被设置缺省值,其中y、j被初始化为0,而t被初始化为null。接下来,静态域初始器按照其声明的顺序执行赋值动作。第一个静态域是y,它的值是通过调用getY获取的,赋值操作完后结果为2。第一个初始化完成后,再进行第二个静态域的赋值操作,第二个静态域为t,它的值是通过调用T()构造函数来完成的。这个构造器会用二个涉及静态域y、j来初始化非静态域i。通常,读取一个静态域是会引起一个类被初始化,但是我们又已经在初始化T类。JavaVM规范对递归的初始化尝试会直接被忽略掉(按理来说在创建出实例前需初始化完所有的静态域后再来创建实例),这样就导致在静态域被初始化之前就调用了构造器,后面的静态域j将得不到正常的初始化前就在构造器中被使用了,使用时的值为内存分配清零时的,即0。当t初始化完后,再初始化j,此时j得到的值为2,但此时对i的初始化过程来说已经晚了。 在final类型的静态域被初始化之前,存在着读取其值的可能,而此时该静态域包含的还只是其所属类型的缺省值。这 是与直觉想违背的,因为我们通常会将final类型的域看作是常量,但final类型的域只有在其初始化表达式是字面常 量表达式时才是真正的常量。 再看看另一程序: Java代码 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类型的静态域,可能会在它们被初始化之前,被读走其缺省值。 另,类初始化规则请参考《惰性初始化》一节 8、instanceof与转型 Java代码 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告诉你一个对象引用是某个特定类型的实例,那么你就可以将其转型为该类型,并调用该类型的方法 ,而不用担心会抛出ClassCastException或NullPointerException异常。 instanceof操作符有这样的要求:左操作数要是一个对象的或引用,右操作数是一个引用类型,并且这两个操作数的 类型是要父子关系(左是右的子类,或右是左的子类都行),否则编译时就会出错。 9、 父类构造器调用已重写的方法 Java代码 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 } } 在一个构造器调用一个已经被其子类重写了的方法时,可能会出问题:如果子类重写的方法要访问的子类的域还未初 始化,因为这种方式被调用的方法总是在实例初始化之前执行。要想避免这个问题,就千万不要在父类构造器中调用 已重写的方法。 10、静态域与静态块的初始顺序 Java代码 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。 11、请使用引用类型调用静态方法 Java代码 public class Null { public static void greet() { System.out.println("Hello world!"); } public static void main(String[] args) { ((Null) null).greet(); } } 上面程序运行时不会打印NullPointerException异常,而是输出"Hello world!",关键原因是:调用静态方法时将忽略前面的调用对象或表达示,只与对象或表达式计算结果的类型有关。 12、循环中的不能声明局部变量 Java代码 for (int i = 0; i < 1; i++) Object o ; //!! 编译不能通过 for (int i = 0; i < 1; i++) Object o = new Object(); //!! 编译不能通过 一个本地变量声明看起来像是一条语句,但是从技术上来说不是。 Java语言规范不允许一个本地变量声明语句作为一条语句在for、while或do循环中重复执行。 一个本地变量声明作为一条语句只能直接出现在一个语句块中(一个语句块是由一对花 括号以及包含在这对花括号中的语句和声明构成的): Java代码 for (int i = 0; i < 1; i++) { Object o = new Object(); // 编译OK } 13、内部类反射 Java代码 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: Java代码 Constructor c = Inner.class.getConstructor(Outer.class);//获取带参数的内部类构造函数 System.out.println(c.newInstance(Outer.this));//反射时还需传进外围类
相关推荐
文档《java解惑 PDF版》中列举了95个这样的谜题,每个谜题都旨在帮助开发者理解并纠正一些常见的错误理解。以下是根据提供的部分内容解析的几个相关知识点。 ### 表达式谜题与取余操作符(%)的行为 在Java中,...
Java是一种广泛使用的面向对象的编程语言,以其跨平台性、高效性和丰富的类库而闻名。...在学习过程中,不断实践和总结是掌握任何编程语言的关键,"java解惑"提供的资源无疑是这样的一个实践和学习的优秀工具。
### JAVA解惑中的关键知识点解析 #### 谜题1:奇数性的判断 在《JAVA解惑》一书中,作者通过一系列实例介绍了Java编程语言中的一些不易掌握的知识点。其中一个例子是关于如何正确判断一个整数是否为奇数。 **原始...
3. **java解惑.pdf**:这很可能与博客主题相呼应,详细解答了Java编程中的疑惑,比如异常处理、多线程、集合框架、内存管理等复杂话题。 4. **网络安全防护措施百分百.ppt**:网络安全是任何应用开发都不可忽视的...
**谜题背景**: 在《JAVA解惑》这本书中提到了第一个谜题:如何判断一个整数是否为奇数。该谜题提供了一个看似合理的解决方案,但实际运行时会出现问题。 **原方法实现**: ```java public static boolean isOdd(int ...
标题《Java解惑中文》很可能是某本专注于解决Java编程中常见疑惑、误区或陷阱的书籍的中文版。这类书籍通常由经验丰富的Java开发者编写,目的是帮助其他开发者深入理解Java语言和相关类库的内在工作原理,同时学习...
Java编程语言中有许多微妙而有趣的细节,这些细节可能会在开发过程中造成困扰,这就是"Java解惑"系列试图解决的问题。本文将深入探讨PPT8中提及的三个Java谜题:Puzzle 76 乒乓、Puzzle 77 搞乱锁的妖怪和Puzzle 78 ...
刚刚由本人收集整理,共享给各位java爱好者。 本书深入研究Java编程... 本书以轻松诙谐的语言,寓教于乐的方式,由浅入深、总结归纳Java编程语言的知识点,适合具有Java知识的学习者和有编程经验的Java程序员阅读。
### Java解惑中文版(带索引)知识点详解 #### 一、理解Java中的奇数检测方法 在Java中,判断一个整数是否为奇数是常见的编程需求。本章节介绍了一个具体的例子来探讨如何正确地实现这一功能。 **原始方法实现**:...
在《JAVA解惑》文档中的第一个谜题关注的是一个简单的逻辑判断:一个整数是否为奇数。虽然这个问题看似简单,但在Java语言中却隐藏着一个容易被忽视的陷阱。 **谜题描述** 代码片段提供了一个名为`isOdd`的方法,...
### Java解惑:深入理解Java中的谜题与陷阱 #### 表达式谜题:奇数判断误区 在探讨Java编程中的谜题时,我们首先遇到的是关于奇数判断的一个常见陷阱。根据“Java谜题”一书的中文版描述,一个简单的函数`public ...
在Java中,判断一个整数是否为奇数的常见方法是通过取余操作符(%)来实现。例如,判断方法`isOdd(int i)`使用`return i % 2 == 1;`来判断传入的整数`i`是否为奇数。这个方法看起来直观且正确,但在处理负数时会出现...
在"Java解惑PPT6"中,我们探讨了几个关键的Java特性,特别是关于不变性、equals()和hashCode()方法的约定以及它们在HashSet中的应用。 首先,让我们来看一下Library Puzzle(Puzzle 56)中的Big Problem。这个例子...