静态分派(Method Overload Resolution)
所有依赖静态类型来定位方法执行版本的分派动作称为静态分派。典型应用方法是重载。
静态分派发生在编译阶段,因此确定静态分派动作是由编译器来完成的,在很多情况下,重载版本并不是唯一的,而是“当前最合适的”版本。
举一个静态分派的极端例子:
上面的代码输出hello char。’a‘是一个char类型,会自然寻找char的重载方法。
但如果注释掉char方法,则输出变为:
hello int。此时发生了一次自动类型转换,'a'除了可以代表字符串,也可以代表数字97(unicode数值)。
如果继续注释掉int方法,则输出会变为:
hello long。
这时发生了两次自动类型转换,'a'转型为整数97后,进一步转型为97L,重载了long类型方法。
事实上重载还能继续发生很多次,按照char>int>long>float>double进行,但不会匹配到byte和short,因为转型不安全。
如果继续注释掉long,则输出会变为:
hello Character。
这时发生了一次自动装箱。'a'被包装为它的封装类型java.lang.Character,匹配到Character的重载。
继续注释掉Character方法,输出会变为:
hello Serializable。
因为java.lang.Serializable是java.lang.Character实现的一个接口。
当自动装箱后还找不到装箱类,但是找到了装箱类实现了的接口类型,所以又发生一次自动转型。
char可以转型为int,但Character绝对不会转型为Integer。它只能安全地转型为它实现的接口或父类。
Character还实现了另外一个java.lang.Comparable<Character>如果同时出现,他们的优先级是一样的。编译器无法确定时会提示类型模糊,拒绝编译。
继续注释掉Serializable,则输出会变为:
hello Object
这时是char装箱后转型为父类了。
再次注释后,输出变为:
hello char...
变长参数重载优先级最低。
包装类型
Java语言是一个面向对象的语言,虽然每一个引用类型都和Object相容,但8种基本类型是不能的,这在实际使用时存在很多的不便。于是,Java为这8种基本类型提供了包装类。(Wrapper Class)。
每一个包装对象都是不可变的(状态绝不能改变,即不能被类型转换,见下文类型转换例子)
包装类均位于java.lang包,包装类和基本数据类型的对应关系如下表所示:
基本数据类型 | 包装类 |
byte | Byte |
double | Double |
short | Short |
char | Character |
int | Integer |
long | Long |
float | Float |
double | Double |
为什么需要包装类?
比如,在集合类中,我们是无法将int 、double等类型放进去的。因为集合的容器要求元素是Object类型。
为了让基本类型也具有对象的特征,就出现了包装类型,它相当于将基本类型“包装起来”,使得它具有了对象的性质,并且为其添加了属性和方法,丰富了基本类型的操作。
自动拆箱与装箱
举例来说:
- 如果一个int型量被传递到一个需要Integer对象的地方,那么编译器将在幕后插入一个对Integer构造方法的调用,这就叫做自动装箱。
- 如果一个Integer对象被放到需要int型的地方,则编译器会插入一个intValue方法的调用,这叫自动拆箱。
当表格中基础类型与它们的包装类有如下几种情况时,编译器会自动帮我们进行装箱或拆箱.
- 进行 = 赋值操作(装箱或拆箱)
- 进行+,-,*,/混合运算 (拆箱)
- 进行>,<,==比较运算(拆箱)
- 调用equals进行比较(装箱)
- ArrayList,HashMap等集合类 添加基础类型数据时(装箱)
例如:
一个典型的自动拆装箱示例
public static void main(String[]args){
Integer integer=1; //装箱
int i=integer; //拆箱
}
反编译后:
public static void main(String[]args){
Integer integer=Integer.valueOf(1);
int i=integer.intValue();
}
自动装箱是通过包装类的valueOf()
方法来实现的.自动拆箱都是通过包装类对象的xxxValue()
来实现的。
自动拆装箱可能产生的问题:
- 用一个例子来解释自动装箱和包装类的类型转换限制(包装类不可变原则):
我们设置一个类,其变量可以被设置为Object的任意类型:
编写一个测试类:
在这个例子中,MemoryCell的实例m,先设置了一个String类型的变量,可以正常读取和打印。
然后,写入一个int类型123时,报了类型转换ClassCastException。
这里发生了两个操作:
1.由于m是Object泛型实例,write时,调用了自动装箱操作,将123转换为Integer类型,写入操作正常。
2.由于包装类型是不能互相转换的,在(String)强制转换过程中,抛出类型转换异常。
- Integer cache的上下限问题:
1.基础类型a与包装类b进行==比较,这时b会拆箱,直接比较值,所以会打印true。
2.二个包装类型,都被赋值了100,所以根据我们之前的解析,这时会进行装箱,调用Integer的valueOf方法,生成2个Integer对象,引用类型==比较,直接比较对象指针,这里我们先给出结论,最后会分析原因,打印 true。
3.跟上面第2段代码类似,只不过赋值变成了200,打印 false。
默认Integer cache 的下限是-128,上限默认127,可以配置,所以到这里就清楚了,我们上面当赋值100给Integer时,刚好在这个range内,所以从cache中取对应的Integer并返回,所以二次返回的是同一个对象,所以==比较是相等的,当赋值200给Integer时,不在cache 的范围内,所以会new Integer并返回,当然==比较的结果是不相等的。
包装对象的数值比较,不能简单的使用==,虽然-128到127之间的数字可以,但是这个范围之外还是需要使用equals比较。
- 小心空指针异常
有这么一段代码:
public static void main(String[] args) throws Exception
{
Object obj = getObj(null);
int i = (Integer)obj;
}
public static Object getObj(Object obj)
{
return obj;
}
如果运行的话:
Exception in thread "main" java.lang.NullPointerException
at main.Test7.main(Test7.java:8)
这种使用场景很常见,我们把一个int数值放在session或者request中,取出来的时候就是一个类似上面的场景了。所以,小心自动拆箱时候的空指针异常。
有些场景会进行自动拆装箱,同时也说过,由于自动拆箱,如果包装类对象为null,那么自动拆箱时就有可能抛出NPE。
另外的例子参见:Java中的三目运算符及拆装箱产生的异常