转型 绑定 | 加载顺序(父子 动静 变量 构造) | 存储空间使用
Java入门记(二):向上转型与向下转型
在对Java学习的过程中,对于转型这种操作比较迷茫,特总结出了此文。例子参考了《Java编程思想》。
目录
几个同义词
首先是几组同义词。它们出现在不同的书籍上,这是造成理解混淆的原因之一。
父类/超类/基类
子类/导出类/继承类/派生类
静态绑定/前期绑定
动态绑定/后期绑定/运行时绑定
向上转型与向下转型
例一:向上转型,调用指定的父类方法
class Shape {
static void draw(Shape s) {
System.out.println("Shape draw.");
}
}
class Circle extends Shape {
static void draw(Circle c) {
System.out.println("Circle draw.");
}
}
public class CastTest {
public static void main(String args[]) {
Circle c = new Circle();
Shape.draw(c);
}
}
输出为
Shape draw.
这表明,draw(Shape s)方法本来被设计为接受Shape引用,但这里传递的是Circle引用。实际上draw(Shape s)方法可以对所有Shape类的导出类使用,这被称为向上转型。表现的行为,和方法所属的类别一致。换句话说,由于明确指出是父类Shape的方法,那么其行为必然是这个方法对应的行为,没有任何歧义可言。
“向上转型”的命名来自于类继承图的画法:根置于顶端,然后逐渐向下,以本例中两个类为例,如下图所示:
例二:向上转型,动态绑定
class Shape {
public void draw() {
System.out.println("Shape draw.");
}
}
class Circle extends Shape {
public void draw() {
System.out.println("Circle draw.");
}
}
public class CastTest {
public static void drawInTest(Shape s) {
s.draw();
}
public static void main(String args[]) {
Circle c = new Circle();
drawInTest(c);
}
}
输出为
Circle draw.
这样做的原因是,一个drawInTest(Shape s)就可以处理Shape所有子类,而不必为每个子类提供自己的方法。但这个方法能能调用父类和子类所共有的方法,即使二者行为不一致,也只会表现出对应的子类方法的行为。这是多态所允许的,但容易产生迷惑。
例三:向上转型,静态绑定
class Shape {
public static void draw() {
System.out.println("Shape draw.");
}
}
class Circle extends Shape {
public static void draw() {
System.out.println("Circle draw.");
}
}
public class CastTest {
public static void drawInTest(Shape s) {
s.draw();
}
public static void main(String args[]) {
Circle c = new Circle();
drawInTest(c);
}
}
输出为
Shape draw.
例三与例二有什么区别?细看之下才会发现,例三里调用的方法被static修饰了,得到了完全不同的结果。
这两例行为差别的原因是:Java中除了static方法和final方法(包括private方法),其他方法都是动态绑定的。对于一个传入的基类引用,后期绑定能够正确的识别其所属的导出类。加了static,自然得不到这个效果了。
了解了这一点之后,就可以明白为什么要把例一写出来了。例一中的代码明确指出调用父类方法,而例三调用哪个方法是静态绑定的,不是直接指明的,稍微绕了一下。
例四:向下转型
出自《Java编程思想》8.5.2节,稍作了修改,展示如何通过类型转换获得子类独有方法的访问方式。
这相当于告诉了编译器额外的信息,编译器将据此作出检查。
class Useful {
public void f() {System.out.println("f() in Useful");}
public void g() {System.out.println("g() in Useful");}
}
class MoreUseful extends Useful {
public void f() {System.out.println("f() in MoreUseful");}
public void g() {System.out.println("g() in MoreUseful");}
public void u() {System.out.println("u() in MoreUseful");}
}
public class RTTI {
public static void main(String[] args) {
Useful[] x = {
new Useful(),
new MoreUseful()
};
x[0].f();
x[1].g();
// Compile-time: method not found in Useful:
//! x[1].u();
((MoreUseful)x[1]).u(); // Downcast/RTTI
((MoreUseful)x[0]).u(); // Exception thrown
}
}
输出
Exception in thread "main" java.lang.ClassCastException: Useful cannot be cast to MoreUseful
at RTTI.main(RTTI.java:44)
f() in Useful
g() in MoreUseful
u() in MoreUseful
虽然父类Useful类型的x[1]接收了一个子类MoreUseful对象的引用,但仍然不能直接调用其子类中的u()方法。如果需要调用,需要做向下转型。这种用法很常见,比如一个通用的方法,处理的入参是一个父类,处理时根据入参的类型信息转化成对应的子类使用不同的逻辑处理。
此外,父类对象不能向下转换成子类对象。
向下转型的好处,在学习接口时会明显地体会出来(如果把实现接口看作多重继承)。可以参考9.4节的例子,这里不做详述:
interface CanFight {
void fight();
}
interface CanSwim {
void swim();
}
interface CanFly {
void fly();
}
class ActionCharacter {
public void fight() {}
}
class Hero extends ActionCharacter implements CanFight, CanSwim, CanFly {
public void swim() {}
public void fly() {}
}
public class Adventure {
static void t(CanFight x) { x.fight(); }
static void u(CanSwim x) { x.swim(); }
static void v(CanFly x) { x.fly(); }
static void w(ActionCharacter x) { x.fight(); }
public static void main(String[] args) {
Hero i = new Hero();
t(i); // Treat it as a CanFight
u(i); // Treat it as a CanSwim
v(i); // Treat it as a CanFly
w(i); // Treat it as an ActionCharacter
}
}
转型的误区
转型很方便,利用转型可以写出灵活的代码。不过,如果用得随心所欲而忘乎所以的话,难免要跌跟头。下面是几种看似可以转型,实际会导致错误的情形。
1.运行信息(RTTI)
/* 本例代码节选自《Java编程思想》14.2.2节 */
Class<Number> genericNumberClass = int.class
这段代码是无效的,编译不能通过,即使把int换为Integer也同样不通过。虽然int的包装类Integer是Number的子类,但Integer Class对象并不是Number Class对象的子类。
2.数组类型
/* 代码节改写《Java编程思想》15.8.2节,本例与泛型与否无关。 */
class Generic<T> {}
public class ArrayOfGeneric {
static final int SIZE = 100;
static Generic<Integer>[] gia;
@SuppressWarnings("unchecked")
public static void main(String[] args) {
//! gia = (Generic<Integer>[]) new Object[SIZE];
gia = (Generic<Integer>[]) new Generic[SIZE];
}
}
注释部分在去掉注释后运行会提示java.lang.ClassCastException。这里令人迷惑的地方在于,子类数组类型不是父类数组类型的子类。在异常提示的后面可以看到
[Ljava.lang.Object; cannot be cast to [LGeneric;
除了通过控制台输出的异常信息,可以使用下面的代码来看看gia究竟是什么类型:
Object[] obj = new Object[SIZE];
gia = (Generic<Integer>[]) new Generic[SIZE];
System.out.println(obj.getClass().getName());
System.out.println(gia.getClass().getName());
System.out.println(obj.getClass().getClass().getName());
System.out.println(gia.getClass().getSuperclass().getName());
控制台输出为:
[Ljava.lang.Object;
[LGeneric;
java.lang.Object
java.lang.Object
可见,由Generic<Integer>[] gia和Object[] obj定义出的gia和obj根本没有任何继承关系,自然不能类型转换,不管这个数组里是否放的是子类的对象。(子类对象是可以通过向上转型获得的,如果被转换的确实是一个子类对象,见例四)
3.Java容器
/* 代码节选自《Java编程思想》15.10节*/
class Fruit {}
class Apple extends Fruit {}
class Orange extends Fruit {}
public class Test {
public static void main(String[] args) {
// 无法编译
List<Fruit> fruitList = new ArrayList<Apple>();
}
}
明明Fruit的List是可以存放Apple对象的,为什么赋值失败?其实这根本不是向上转型。虽然可以通过getClass().getName()得知List<Fruit>和List<Apple>同属java.util.ArrayList类型,但是,假设这里可以编译通过,相当于允许向ArrayList<Apple>存放一个Orange对象,显然是不合理的。虽然由于泛型的擦除,ArrayList<Fruit>和ArrayList<Apple>在运行期是同一种类型,但是具体能持有的元素类型会在编译期进行检查。
Java入门记(三):初始化顺序
初始化顺序的规则
1.在一个类的对象实例化时,成员变量首先初始化,然后才调用构造器,无论书写顺序。如果调用构造器前,没有显式初始化,那么会赋默认值。
这样做法的原因可以理解为:构造器执行时可能会用到一些成员变量的初值。
2.static变量早于所有其他的类成员变量初始化,同样无论书写顺序。但是static变量仅在所在类第一次被使用时初始化一次。
3.基类构造器总是在导出类的构造过程中被调用,而且按照继承层级逐渐向上链接(调用顺序则是从基类开始向下)。可以理解为,这么做的逻辑关系是在一个类构建时可能会用到其父类的成员、方法。在清理时顺序相反。
4.成员的初始化方法(包括基本数据类型的赋值)在基类构造器调用之后才会被调用。最初时,分配给对象的存储空间初始化二进制的零。
例一出自《Java编程思想》第5.7.2节,为了便于演示初始化顺序,进行了缩减和重新编号。用构造器的参数标明执行顺序,演示1~2条规则:
class Bowl {
Bowl(int marker) {
System.out.println("Bowl(" + marker + ")");
}
}
class Cupboard {
Bowl bowl1 = new Bowl(3);
static Bowl bowl2 = new Bowl(1);
int i;
static int j = 5;
Cupboard() {
System.out.println("i:" + i);
bowl4 = new Bowl(j);
j = 6;
}
Bowl bowl3 = new Bowl(4);
static Bowl bowl4 = new Bowl(2);
}
public class ParaInitialization {
public static void main(String args[]) {
new Cupboard();
new Cupboard();
}
}
输出及对应注释:
Bowl(1) //第一个static变量
Bowl(2) //第二个static变量
Bowl(3) //第一个对象的第一个非static成员变量
Bowl(4) //第一个对象的第一个非static成员变量
i:0 //未显示初始化的成员变量
Bowl(5) //更改static变量的值
Bowl(3) //第二个对象的第一个非static成员变量
Bowl(4) //第二个对象的第二个非static成员变量
i:0
Bowl(6)
例二是一个演示第3条规则的简单示例。
class A {
A() {
System.out.println("A");
}
}
class B extends A {
B() {
System.out.println("B");
}
}
class C extends B {
C() {
System.out.println("C");
}
}
public class hrt {
public static void main(String args[]) {
new C();
}
}
输出
A
B
C
例三用于演示规则4。调用父类构造器时,构造器中的方法被子类方法覆盖。
class Glyph {
void draw() {
System.out.println("Glyph.draw(");
}
Glyph() {
System.out.println("Glyph() before draw()");
draw();
System.out.println("Glyph() after draw()");
}
}
class RoundGlyph extends Glyph {
int radius = 1;
RoundGlyph(int r) {
radius = r;
System.out.println("RoundGlyph.RoundGlyph(), radius = " + radius);
}
void draw() {
System.out.println("RoundGlyph.draw(), radius = " + radius);
}
}
public class PolyConstructors {
public static void main(String[] args) {
new RoundGlyph(5);
}
}
这么多条规则,记起来实在让人头大。将它们按顺序编排会易读很多。
对象初始化顺序,如果有对应成员/父类的才执行对应条目:
1.将分配给对象的存储空间初始化为二进制的零;
2.调用基类构造器,从最顶层/根的基类开始;
3.按照声明的顺序,使用直接的赋值或者初始化方法,先依次初始化static变量,再依次初始化非static变量;
4.调用本对象所属类的构造器。
一些小知识点:
[java] view plain copy
- float f=3.4;是否正确?
- 答:不正确。3.4是双精度数,将双精度型(double)赋值给浮点型(float)属于下转型(down-casting,也称为窄化)会造成精度损失,因此需要强制类型转换float f =(float)3.4; 或者写成float f =3.4F;。
[java] view plain copy
- 访问修饰符public,private,protected,以及不写(默认)时的区别?
- 答:
- 修饰符 当前类 同 包 子 类 其他包
- public √ √ √ √
- protected √ √ √ ×
- default √ √ × ×
- private √ × × ×
[java] view plain copy
- 按位与 a & b 相同位的两个数字都为1,则为1;若有一个不为1,则为0。
- 按位或 a | b 相同位只要一个为1即为1。
- 按位异或 a ^ b 相同位不同则为1,相同则为0
- 按位取反 ~a 内存中的0和1全部取反
- 左移 a << b 乘a * 2的b次方
- 带符号右移 a >> b a除以2的b次方(取整)</span>
- 无符号右移 a>>> b
[java] view plain copy
- 是否可以继承String类?
- 答:String 类是final类,不可以被继承。
- 补充:继承String本身就是一个错误的行为,对String类型最好的重用方式是关联关系(Has-A)和依赖关系(Use-A)而不是继承关系(Is-A)。
[java] view plain copy
- 重载(Overload)和重写(Override)的区别。重载的方法能否根据返回类型进行区分?
- 答:方法的重载和重写都是实现多态的方式,区别在于前者实现的是编译时的多态性,而后者实现的是运行时的多态性。重载发生在一个类中,同名的方法如果有不同的参数列表(参数类型不同、参数个数不同或者二者都不同)则视为重载;重写发生在子类与父类之间,重写要求子类被重写方法与父类被重写方法有相同的返回类型,比父类被重写方法更好访问,不能比父类被重写方法声明更多的异常(里氏代换原则)。重载对返回类型没有特殊的要求。
[java] view plain copy
- String s = new String("xyz");创建了几个字符串对象?
- 答:两个对象,一个是静态区的"xyz",一个是用new创建在堆上的对象。
[java] view plain copy
- 解释内存中的栈(stack)、堆(heap)和静态区(static area)的用法。
- stack: 通常我们定义一个基本数据类型的变量,一个对象的引用,还有就是函数调用的现场保存都使用内存中的栈空间
- heap: 而通过new关键字和构造器创建的对象放在堆空间
- Math.round(11.5) 等于多少?Math.round(-11.5)等于多少?
- 答:Math.round(11.5)的返回值是12,Math.round(-11.5)的返回值是-11。四舍五入的原理是在参数上加0.5然后进行下取整。
I/O管理:http://www.cnblogs.com/javathread/archive/2012/02/05/2634806.html
并发机制示意图: