首页 > 编程语言 >《java解惑》——类谜题

《java解惑》——类谜题

时间:2023-11-09 12:02:51浏览次数:38  
标签:初始化 java int 谜题 class static Test 解惑 public


1.方法重载:

问题:

下面的程序演示方法重载,代码如下:



public class Test{  
      
public static void main(String[] args) {  
new Test(null);  
    }  
  
private Test(Object o){  
"Object");  
    }  
      
private Test(double[] doubleArray){  
"Double array");  
    }  
}


很多人看到这个程序都不能很确定输出结果,因为构造方法重载了,但是对于传递进来的null,两个构造方法都可以,所以也有人觉得程序模棱两可,不能通过编译。

程序的真实运行结果是“Double array”,即调用参数为double数组的构造方法。

原因:

java中方法重载必须以方法签名:方法名称和参数列表来区分,在源码层面方法的返回值不作为方法重载的判断(在字节码文件中方法返回值也作为重载依据),方法重载是静态早绑定,即在编译器要调用哪个方法就是已经确定的了,java的重载解析过程是以下面两个阶段运行的:

阶段一:

选取所有可获得并且可应用的方法或者构造函数。

阶段二:

在第一阶段选取的方法或构造函数中选取最精确的一个。

如果方法或者构造函数A可以接受传递给方法或者构造函数B的任何参数,那么我们就可以说方法A比方法B缺乏精确性。

上述程序中,两个构造函数都是可获得并且可应用的,构造函数Test(Object o)可以接受任何传递给Test(double[] doubleArray)的参数,因为每一个double数组都是一个Object类型,但是并不一定每个Object类型都是一个double数组,因此程序会选择最精确匹配的构造方法。

结论:

如果想让上述的程序调用Object类型参数的构造方法,只需要进行如下修改即可:


public class Test{  
      
public static void main(String[] args) {  
new Test((Object)null);  
    }  
  
private Test(Object o){  
"Object");  
    }  
      
private Test(double[] doubleArray){  
"Double array");  
    }  
}


即如果想要强制要求编译器选择一个精确的重载版本,需要将实际的参数转型为目标重载方法中形式参数所声明的类型,但是这种方式选择重载版本体验非常的不好,更好的建议为:

(1).避免使用重载,为不同方法取不同名字。

(2).对于构造方法,使用静态工厂模式或者建造者模式来减少对重载版本的需求量。

(3).如果必须要使用重载,请确保所有的重载版本所接受的参数类型都互不兼容,这样任何两个重载版本都不会同时是可应用的。

2.静态变量共享问题:

问题:

我们经常使用静态变量做计数器,下面的程序使用一个Counter类类记录每种宠物的叫唤次数,代码如下:


class Counter{  
private static int count = 0;  
public static final synchronized void increment(){  
        count++;  
    }  
public static final synchronized int getCount(){  
return count;  
    }  
}  
  
class Dog extends Counter{  
public Dog(){}  
public void woof(){  
        increment();  
    }  
}  
  
class Cat extends Counter{  
public Cat(){}  
public void meow(){  
        increment();  
    }  
}  
  
public class Test{  
      
public static void main(String[] args) {  
new Dog(), new Dog()};  
for(Dog dog : dogs){  
            dog.woof();  
        }  
new Cat(), new Cat(), new Cat()};  
for(Cat cat : cats){  
            cat.meow();  
        }  
" woofs and ");  
" meows");  
    }  
}


我们期望上述程序打印出“2 woofs and 3 meows”,但是程序真实运行结果为”5 woofs and 5 meows“。

原因:

之所以出现了计数器不对的问题是因为Dog和Cat都从共同的父类计数器Counter那里继承了count域,而count是一个静态域,每个静态域在声明它的类及其所有子类中共享一份单一的拷贝,因此Dog和Cat使用的相同的count域,每一个对woof()或meow()的调用都在递增这个域,因此总共增加了5次,所以造成最后打印输出5的问题。

结论:

知道了问题的原因,就容易解决问题了,静态变量是声明它的类及其所有子类共享的,因此如果每个子类都需要一个单独的拷贝,则需要在每个类中声明自己的静态域(也可以声明非静态域),代码如下:


class Dog{  
private static int count = 0;  
public Dog(){}  
public void woof(){  
        count++;  
    }  
public static int getCount(){  
return count;  
    }  
}  
  
class Cat{  
private static int count = 0;  
public Cat(){}  
public void meow(){  
        count++;  
    }  
public static int getCount(){  
return count;  
    }  
}


另外从面向对象程序设计来看,Dog和Cat继承Counter也不符合要求,在设计一个类的时候,如果该类构建于另一个类的行为之上,那么有两种选择:

继承:一个类扩展另一个类,适用于一个类的每一个实例都是另一个类的一个实例。

组合:在一个类中包含另一个类的一个实例,适用于一个类的每一个实例都有另一个类的一个实例。

Dog和Cat显然不是计数器,但是可以拥有计数器,在有些不确定的情况下优选组合而非继承。

3.当静态方法遇到覆盖:

问题:

下面程序企图使用方法覆盖演示面向对象的多态特性:


class Dog{  
public static void bark(){  
"woof ");  
    }  
}  
  
class Basenji extends Dog{  
public static void bark(){}  
}  
  
public class Test{  
      
public static void main(String[] args) {  
new Dog();  
new Basenji();  
        woofer.bark();  
        nipper.bark();  
    }  
}


很多人认为上述程序是方法覆盖,因此结果应该是一个woof,可惜程序真实运行结果是两个woof。

原因:

方法重载和静态方法从严格意义上来说都不能算做是多态,因为它们都是静态单分派的而非动态多分派。

当一个程序调用一个静态方法时,被调用的静态方法在编译时就被选定了,它的选定是基于修饰符的编译期类型而做出的,修饰符的编译器类型就是我们给出的方法调用表达式中圆点左边部分的名字。

在上述程序代码中,两个方法调用的修饰符分别是变量woofer和nipper,他们都被声明为Dog类型,因此它们具有相同的编译期类型,因此都调用了Dog类中的静态方法。

结论:

多态中的方法覆盖是针对非静态方法的,若想让上述程序只打印输出一个woof,只需要把方法变成非静态即可,代码如下:


class Dog{  
public void bark(){  
"woof ");  
    }  
}  
  
class Basenji extends Dog{  
public void bark(){}  
}  
  
public class Test{  
      
public static void main(String[] args) {  
new Dog();  
new Basenji();  
        woofer.bark();  
        nipper.bark();  
    }  
}


在java中,如果子类和父类具有相同签名的静态方法,称之为子类对父类静态方法的隐藏,只有非静态方法才能被覆盖。

4.类初始化的循环:

问题:

下面的程序使用单例模式计算1970年出生的人的年龄,代码如下:


public class Test{  
public static final Test INSTANCE = new Test();  
private final int age;  
private static final int CURRENT_YEAR = Calendar.getInstance().get(Calendar.YEAR);  
private Test(){  
1970;  
    }  
public int getAge(){  
return age;  
    }  
public static void main(String[] args) {  
"Age is " + INSTANCE.getAge());  
    }  
}


第一眼看去,这个程序是在计算当前的年份减去1970的值,如果它是正确的,那么在2014年,该程序应该打印出Age is 44,但是程序的真实运行结果是Age is -1970。

原因:

改程序的问题是由类初始化顺序中的循环而引起的,Test类的初始化是有java虚拟机对其main()方法的调用而触发的,顺序如下:

首先,静态域被设置为缺省值,其中INSTANCE域被设置为null,CURRENT_YEAR被设置为0.

其次,静态域初始器按照其出现的顺序执行,第一个静态域是INSTANCE,它的值是通过调用Test()构造函数而计算出来的。在构造方法中会用一个涉及静态域CURRENT_YEAR的表达式来初始化age变量,通常读取一个静态域是会引起一个类被初始化的事件之一,但是我们已经在初始化Test类了,递归的初始化尝试会直接被忽略掉,因此CURRENT_YEAR的值仍旧是其缺省值0,因此年龄被计算出来为-1970.

最后,从构造函数返回以完成Test类的初始化,假设我们是在2014年运行改程序,那么我们就将静态域CURRENT_YEAR初始化成了2014,遗憾的是,这个常量现在所具有的正确值对于Test类的age计算已经太迟了,age的值已经被计算为-1970了,这正是后续INSTANCE.getAge()方法调用返回的值。

结论:

该程序表明,在final类型的静态域被初始化之前,存在着读取它的值的可能,而此时该静态域包含的还只是其所属类型的缺省值。

我们通常将final类型的域看作是常量,但是final类型的域只有在其初始化表达式是常量表达式市才是常量。

修正上述程序方法很简单,只需要重新对静态域的初始器进行排序,使的每一个初始器都出现在任何依赖域它的初始器之前,对于上述程序中CURRENT_YEAR的声明放在INSTANCE声明和初始化之前,代码如下:


public class Test{  
private static final int CURRENT_YEAR = Calendar.getInstance().get(Calendar.YEAR);  
public static final Test INSTANCE = new Test();  
private final int age;  
private Test(){  
1970;  
    }  
public int getAge(){  
return age;  
    }  
public static void main(String[] args) {  
"Age is " + INSTANCE.getAge());  
    }  
}


单例设计模式,服务提供者框架和类型安全的枚举模式本质上都是初始化循环。

5.类型比较和转换:

问题:

下面的程序展示java中类型比较和转换,代码如下:



public class Test{  
public static void main(String[] args) {  
null;  
instanceof String);  
new Test() instanceof String);  
new Object();  
    }  
}


对于第一个打印语句,很多人不确定instanceof应用于一个空对象引用时的行为。

对于第二个打印语句,很多人认为毫无疑问应该输出false。

对于第三个类型转换语句,很多人认为无法通过编译。

程序真实情况是:

对于第一个打印语句,结果应为false。

对于第二个打印语句,会有一个“Incompatible conditional operand types Test and String”编译错误。

对于第三个类型转换语句,如果注释掉编译错误的第二条打印语句,运行时会报“java.lang.ClassCastException”。



原因:

对于第一个打印语句,展示了instanceof操作符应用于一个空对象引用时的行为,尽管null对于每一个引用类型来说都是其子类型,但是instanceof操作符被定义为在其左操作数为null时返回false,因此如果instanceof告诉你一个对象引用是某个特定类型的实例,那么你就可以将其转型为该类型,并调用该类型的方法,而不用担心会抛出类型转换或者空指针异常。

对于第二个打印语句,展示了instanceof操作符在测试一个类的实例,以查看它是否是某个不相关的类型时所表现出来的行为。instanceof操作符规定:如果两个操作数的类型都是类,其中一个必须是另一个的子类型,否则将会导致编译器失败。

第三个类型转换语句,展示了当要被转型的表达式的静态类型是转型类型的超类时,转型操作符的行为。与instanceof操作符类似,如果在一个转型操作中的两种类型都是类,那么其中一个必须是另一个的子类型,但是编译器在编译期无法确定表达式new Object()的运行期类型不可能是Test的一个子类型,因此可以通过编译,只有在运行期才会抛出类型转换异常。

结论:

instaceof对于左操作数为null时,一律返回false。

当instanceof操作符的两个操作数都是类时,这两个类必须是父子类型关系。

6.实例初始化顺序:

问题:

下面的程序使用不可变的值类来演示实例初始化顺序问题,代码如下:



class Point{  
protected final int x, y;  
private final String name;  
int x, int y){  
this.x = x;  
this.y = y;  
        name = makeName();  
    }  
      
protected String makeName(){  
return "[" + x + "," + y + "]";  
    }  
      
public final String toString(){  
return name;  
    }  
}  
  
public class ColorPoint extends Point{  
protected final String color;  
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){  
new ColorPoint(4, 2, "Blue"));  
    }  
}


很多人认为程序的运行结果应该为:[4,2]:Blue,但是程序的真实运行结果为:[4,2]:null。

原因:

之所以出现令人意想不到的结果是因为上述代码中有个实例初始化顺序问题,详细分解上述代码的执行过程如下:

(1).执行ColorPoint类中main方法时,调用ColorPoint的构造方法。

(2).调用ColorPoint类构造方法时调用父类Point类的构造方法。

(3).调用Point类构造方法时调用其makeName方法,Point类的makeName方法调用子类ColorPoint的makeName方法.

(4).调用ColorPoint类的makeName方法时,color属性还未被赋值,因此使用默认值null,所以Point类的name属性被赋值为[4,2]:null。

(5).父类Point构造完成,返回子类ColorPoint构造方法对color属性进行赋值,但是此时赋值已经太晚,赋值Point的name属性值已经被缓存,

(6).ColorPoint实例调用父类Point的toString方法打印输出我们看到的[4,2]:null结果。

通过分析我们看到一个final实例域color在被赋值之前,存在着读取其值的可能性,而此时final域包含的仍旧是其所属类型的默认值,这种实例初始化顺序循环问题经常引起程序的混乱。

结论:

当一个构造器调用了一个已经被其子类覆盖的方法,总会引起实例初始化循环问题,因为在父类构造器中所调用的覆盖方法总是在实例被初始化之前执行。

想要避免这个问题,就千万不要在构造方法,实例初始器和伪构造器(readObject/clone)中直接或间接调用可覆盖的方法。

可以通过延迟初始化name域来修正上述程序中的问题,即当Point对象第一次被使用时初始化,以此取代积极初始化,代码如下:



class Point{  
protected final int x, y;  
private String name;  
int x, int y){  
this.x = x;  
this.y = y;  
    }  
      
protected String makeName(){  
return "[" + x + "," + y + "]";  
    }  
      
public final synchronized String toString(){  
if(name == null){  
            name = makeName();  
        }  
return name;  
    }  
}


尽管延迟初始化name域可以修正这个问题,但是对于让一个值类去扩展另一个值类,并且在其中添加一个会对equals方法产生影响的域仍旧不是一个好主要,因为无法在超类和子类上都提供一个基于值的equals方法,而同时又不违反Object中关于equals方法的同样约定。

因此,请千万注意不要在构造方法,实例初始器或伪构造器中调用可覆盖的方法,因为在实例初始化中产生的循环将是致命的。

7.类初始化顺序:

问题:

为了计算从0到99的整数之和,下面的程序中同时使用积极初始化和延迟初始化确保程序在任何情况下都能正常运行,代码如下:


class Cache{  
static{  
        initializelIfNecessary();  
    }  
private static int sum;  
public static int getSum(){  
        initializelIfNecessary();  
return sum;  
    }  
private static boolean initialized = false;  
private static synchronized void initializelIfNecessary(){  
if(!initialized){  
for(int i = 0; i < 100; i++){  
                sum += i;  
            }  
true;  
        }  
    }  
}  
  
public class Test{  
public static void main(String[] args){  
        System.out.println(Cache.getSum());  
    }  
}


本程序期望打印出4950,但是程序真实运行结果为9900,是我们期望的两倍,即重复计算了一次。

原因:

我们通过对程序执行过程的分析,来找出问题的根本原因:

(1).在Test类的main方法中调用Cache.getSum()方法时,由于是静态方法,因此首先java虚拟机加载Cache类。

(2).在java虚拟机加载Cache类的时候,执行类初始化方法(静态初始化块和静态变量赋值),静态初始化块中调用initializelIfNecessary()方法时initialized域还没有被初始化,因此被赋值为默认值false,sum也没有被初始化,默认值为0,在循环中计算出sum的值为4950并且缓存在sum变量中,initialized变量被赋值为true。

(3).静态初始化块执行完毕顺序执行静态变量赋值,此时sum没有被赋值,因此缓存了4950的计算结果,而initialized又被赋值为false。

(4).Cache类加载和初始化完成之后调用getSum方法时initialized还是false,因此又执行了initializelIfNecessary中运行,sum值被累加了4940,最后打印输出9900.

结论:

由于不能在延迟初始化和积极初始化中做出选择,上述代码同时使用了二者,结果产生了初始化的顺序问题,千万不要同时使用延迟初始化和积极初始化。

我们可以使用静态初始化顺序重排和积极初始化来避免上面的重复计算问题,代码如下:



class Cache{  
private static final int sum = computeSum();  
private static int computeSum(){  
int result = 0;  
for(int i = 0; i < 100; i++){  
            result += i;  
        }  
return result;  
    }  
public static int getSum(){  
return sum;  
    }  
}  
  
public class Test{  
public static void main(String[] args){  
        System.out.println(Cache.getSum());  
    }  
}


只有当性能方面考虑,或者需要打破初始化循环时才使用延迟初始化,否则积极初始化是更加常用的选择。

另外在类初始化时请特别注意静态变量声明引起的初始化顺序问题。

标签:初始化,java,int,谜题,class,static,Test,解惑,public
From: https://blog.51cto.com/u_809530/8275281

相关文章

  • 《java解惑》--字符串之谜
    1.字符拼接:问题:程序员几乎在每天编程中都遇到和处理字符串拼接的问题,但是是否对其了解的足够深入,且看下面的程序:publicstaticvoidmain(String[]args){"H"+"a");'H'+'a');}很多人觉得输出结果应是:HaHa,但是真实的程序运行结果是:Ha169。原因:程序第一......
  • JavaScript实现完整的表单验证对邮箱用户名和密码一致性检测并拦截提交-----前端
    完整的表单验证HTML网页使用JS完成用户名密码一致性和邮箱验证<!DOCTYPEhtml><!--这是HTML的注释--><htmllang="en"id="myHtml"> <head> <!--这里不是设置了编码,而是告诉浏览器,用什么编码方式打开文件避免乱码--> <metacharset="UTF-8"> <metaname......
  • 《java解惑》——库谜题
    1.不可变类:问题:下面的程序计算5000+50000+500000值,代码如下:importjava.math.BigInteger;publicclassTest{publicstaticvoidmain(String[]args){newBigInteger("5000");newBigInteger("50000");newBigInteger("500000");......
  • Java-Script 编程
    Java-Script编程目录Java-Script编程一.Js概念1.1简介1.2语法结构二.变量使用2.1定义变量2.2定义常量三.数据类型3.1数值类型(number)3.2字符类型(string)3.3.字符类型常用方法3.4布尔值(boolean)3.5null与undefined3.6数组3.7数组常用的方法3.8运算符四.流程......
  • java ip地址转换成int
    数据库存放,提高数据库使用性能我们将字符串IP转换成int类型保存!java代码如下://将127.0.0.1形式的IP地址转换成十进制整数,这里没有进行任何错误处理publicstaticlongipToLong(StringstrIp){long[]ip=newlong[4];//先找到IP地址字符串中.的......
  • Java类加载机制
    类加载机制将class文件中的二进制数据读取到内存中,并对其进行校验,解析和初始化,将类型数据存放在方法区,实例对象存放在堆,作为方法区该类的数据访问接口。这就是类加载。加载通过全限定名获取二进制字节流将字节流代表的静态存储结构转化为方法区的运行时数据结构在堆中生成......
  • 【深入理解Java虚拟机】内存分配策略
    本文内容来源于《深入理解Java虚拟机》一书,非常推荐大家去看一下这本书。本系列其他文章:【深入理解Java虚拟机】Java内存区域模型、对象创建过程、常见OOM【深入理解Java虚拟机】垃圾回收机制垃圾收集器与内存分配策略Java技术体系中所提倡的自动内存管理最终可以归结为自动......
  • 【JAVA】智慧工地信息管理系统源码 智慧大屏、手机APP、SaaS模式
    一、智慧工地可以通过安全八要素来提升安全保障,具体措施包括:  1.安全管理制度:建立科学完善的安全管理制度,包括安全标准规范、安全生产手册等,明确各项安全管理职责和要求。  2.安全培训教育:对工地人员进行安全培训和教育,提高他们的安全意识和安全素质,使其掌握必要的安全知......
  • Java实现截图和录屏
    一、截图,Javax提供的能力。importjavax.imageio.ImageIO;importjava.awt.*;importjava.awt.image.BufferedImage;importjava.io.File;publicclassDemo1{publicstaticvoidmain(String[]args)throwsException{//创建一个Robot对象......
  • Java获取Windows或Linux下的IP地址
    Java获取Linux或Windows下的IP地址,详情如下importlombok.extern.slf4j.Slf4j;importjava.net.InetAddress;importjava.net.NetworkInterface;importjava.net.SocketException;importjava.net.UnknownHostException;importjava.util.Enumeration;@Slf4jpubli......