对象的初始化
随着计算机革命的发展,“不安全”的编程方式已经逐渐成为了编程代价高昂的主因之一。
正确的初始化和清理过程对于程序的安全性和性能至关重要,不同的编程语言对此具有不同的处理机制。C++中利用构造器完成初始化,当对象不再被使用时调用析构函数完成销毁,程序员需要手动完成内存释放管理过程;Java也采用了构造器完成初始化,同时提供了垃圾会收器对不再使用的内存资源进行自动管理,减轻了程序员手动管理内存的编程负担。
1 重载
Java(和C++)中,构造器是强制重载方法名的原因之一。因为构造器常用于对类中的域完成初始化,当类中存在自定义参数列表的构造器时,系统将调用对应的构造器完成初始化;而当类中不存在自定义参数列表的构造器时,将自动调用类的默认无参构造器,而构造器必须保持与类名相同,所以为了调用的灵活性,需要将构造器重载。
重载可以视为程序实现多态的一种方式,运行时根据参数列表的不同最终确定调用哪个重载函数。重载既可以是构造函数也可以是普通的成员函数【注:编译器区分重载方法时,只能以类名和方法的形参列表(参数个数和类型的不同)作为标准,仅有返回类型不同的两个函数不是重载函数。】
对象初始化时,常根据不同的重载函数完成域的初始化,因此理解重载是正确理解初始化过程的前提与基础。下面的例子同时示范了重载构造器和重载方法。
/** * Created by One on 2016/11/15. */ public class Tree { int height;//树高 int age;//树龄 Tree() {//无参构造器 System.out.println("调用无参构造器" + "\t" + this.height + "\t" + this.age); } Tree(int height, int age) { this.height = height; this.age = age; System.out.println("调用自定义构造器" + "\t" + this.height + "\t" + this.age); } void info() { System.out.println("调用无参重载方法1"); } void info(String message) { System.out.println("调用含参重载方法2" + "\t" + message); } }
public class OverloadTest {
public static void main(String[] args) {
Tree t1=new Tree();
t1.info();
t1.info("对象t1调用");
Tree t2=new Tree(20,3);
t2.info();
t2.info("对象t1调用");
}
}
输出结果:
调用无参构造器 0 0
调用无参重载方法1
调用含参重载方法2 对象t1调用
调用自定义构造器 20 3
调用无参重载方法1
调用含参重载方法2 对象t1调用
该例子表明,可利用构造器和成员函数的重载完成对象域初始化,大大提高程序的灵活性。每一个类均存在一个默认的构造器,当类定义中无显示构造器时系统会调用该默认无参构造器完成初始化工作,其中域会被自动赋予默认值:数字为0,布尔值而false,对象引用为null,但并不建议程序开发人员非明确的对域进行初始化,否则可能会影响程序代码的可读性。
2 this关键字
Java中并不存在指针的概念,它摈弃了C/C++指针难以控制的噩梦,转而以引用代替,对象的引用可以理解为对象在内存中的地址,其中this表示对当前对象的引用。当明确需要指明当前对象的引用时才需用到this关键字,利用this关键字可以区分参数名称和域名称的混淆,同时this关键字在将当前对象传递给其他方法时很有用。下面的例子展示了this将当前对象传递给其他方法的过程。
/** * Created by One on 2016/11/15. */ public class Person { public void eat(Apple apple) { Apple peeled = apple.getPeeled(); System.out.println("苹果很好吃!"); } } public class Apple { Apple getPeeled() { return Peeler.peel(this);//将当前对象传递给其他方法 } } public class Peeler { static Apple peel(Apple apple) { return apple; } } public class Test { public static void main(String[] args) { new Person().eat(new Apple()); } }
输出:
苹果很好吃!
类的初始化过程中常会出现构造器调用构造器的现象,若为this添加上了参数列表,那么就有了不同的含义,这将产生对符合此参数列表的某个构造器的明确调用。这是this关键字的又一重要应用。下面的例子表明了构造器调用构造器的过程。
/** * Created by One on 2016/11/15. */ public class Flower { int petalCount = 0;//花瓣数 String s = "An initial flower"; Flower(int petalCount) { this.petalCount = petalCount; System.out.println("构造器只含整型参数,花瓣数为:\t" + this.petalCount); } Flower(String str) { this.s = str; System.out.println("构造器只含字符串型参数,参数为:\t" + this.s); } Flower(String s, int petals) { this(s);//调用构造器 //this(petals);//仅能调用一次 this.petalCount = petals; System.out.println("字符串和整型参数"); } Flower() { this("test", 100); System.out.println("默认无惨构造函数!"); } void displayPetalCount() { System.out.println("花瓣数为:" + this.petalCount + "\t参数为:" + this.s); } public static void main(String[] args) { Flower flower = new Flower(); flower.displayPetalCount(); } }
输出:
构造器只含字符串型参数,参数为: test
字符串和整型参数
默认无惨构造函数!
花瓣数为:100 参数为:test
程序调用顺序为Flower()->Flower(String s,int petals)->Flower(String s),由输出结果可看出当this添加上了参数列表,将产生对符合此参数列表的某个构造器的明确调用。这是this关键字的又一重要用法,对于代码复用具有重要作用。
3 初始化顺序
Java尽量保证所有的变量在使用前都能够得到恰当的初始化。类的每个基本类型数据成员都会有一个默认的初始值,但我们一般不建议这样做,开发时更多会指定值进行初始化。例如:
public class IntialValues{
//初始化基本类型 boolean bool=true; char ch='A'; byte b=100; short s=0xff; int i=3; long lng=1; float f=3.14f double d=3.14159
//初始化非基本类型(对象等) Person person=new Person(); }
当然,我们也可以利用构造器进行初始化。
对象的初始化包括单个类的初始化和含有父类、子类继承层次的初始化。充分理解类内部的成员、静态数据域和数据块以及非静态数据的初始化顺序是理解含有继承关系的父类、子类初始化顺序的基础。所以,接下来先分别介绍初始化过程。
(1)类内部成员初始化
类内部域的初始化总是早于其他方法甚至是构造器,下面这个例子说明了这个机制。
/** * Created by One on 2016/11/8. */ public class Window { Window(int marker) { System.out.println("Window(" + marker + ")"); } } public class House { Window w1 = new Window(1); Window w2 = new Window(2); Window w3 = new Window(3); House() { w3 = new Window(3); System.out.println("House() is invoking......"); } void f1() { System.out.println("f1() is invoking......"); } } public class OrderOfInitializaiton { public static void main(String[] args) { House h=new House(); h.f1(); } }
输出:
Window(1)
Window(2)
Window(3)
Window(3)
House() is invoking......
f1() is invoking......
由以上代码可知,成员函数总是在调用构造器和其他方法前初始化,对象w3被引用2次,第一次在调用House()之前,第二次在House中被引用。成员变量的初始化顺序与其在类中的声明顺序有关,而与其他因素无关。
(2)静态数据的初始化
【注:无论创建了多少个对象,静态数据都只占用一份存储区域。static关键字不能用于修饰局部变量,只能作用于域。】
初始化的顺序是先静态对象,然后“非静态”对象,并且静态初始化仅在Class对象首次加载的时候进行一次,下面的代码例子很好的说明了静态存储区域的初始化过程。
/** * Created by One on 2016/11/8. */ public class Bowl { Bowl(int marker) { System.out.println("Bow1(" + marker + ")"); } void f1(int marker) { System.out.println("f1(" + marker + ")"); } } public class Table { static Bowl bowl = new Bowl(1); static Bowl bow2 = new Bowl(2); Table() { System.out.println("Table()"); bow2.f1(1); } void f2(int marker) { System.out.println("f2(" + marker + ")"); } } public class Cupboard { static Bowl bowl4 = new Bowl(4); static Bowl bowl5 = new Bowl(5); Bowl bowl3 = new Bowl(3); Cupboard() { System.out.println("Cupboard()"); bowl4.f1(2); } void f3(int maker) { System.out.println("f3(" + maker + ")"); } }
public class StaticIntializaiton {
static Table table = new Table();
static Cupboard cupboard = new Cupboard();
public static void main(String[] args) {
System.out.println("Creating new Cupbord in main");
new Cupboard();
System.out.println("Creating new Cupbord in main");
new Cupboard();
table.f2(1);
cupboard.f3(1);
}
}
输出:
Bow1(1)
Bow1(2)
Table()
f1(1)
Bow1(4)
Bow1(5)
Bow1(3)
Cupboard()
f1(2)
Creating new Cupbord in main
Bow1(3)
Cupboard()
f1(2)
Creating new Cupbord in main
Bow1(3)
Cupboard()
f1(2)
f2(1)
f3(1)
由程序输出结果可以看出,在执行StaticIntializaiton类的Main方法之前,先加载并初始化table、cupboard静态成员,对应的Table类和Cupboard类按照先初始化静态域和静态成员然后初始化普通类成员最后初始化类构造器的顺序完成整个过程。
因此,不含继承层次的单个类的初始化顺序为: 1 静态成员及Static块(块内部初始化顺序由定义的顺序决定)--> 2 普通成员初始化-->3 构造函数初始化
当程序中存在父类和子类的继承体系时,类的初始化顺序为: 1 继承体系中所有静态成员及Static块(块内部初始化顺序由定义的顺序决定,先父类后子类)->父类初始化(普通成员初始化、父类构造函数初始化)->子类初始化(普通成员初始化、子类构造函数初始化)
下面的示例程序说明了含继承关系类间的初始化过程。
//测试类 public class Sample { Sample() { System.out.println("默认构造函数被调用!"); } Sample(String s) { System.out.println(s); } } //父类 public class Father { static { System.out.println("父类静态块1正在执行!"); } static Sample sample1 = new Sample("父类静态成员1正在执行!"); Sample sam1 = new Sample("父类成员1正在初始化!"); static Sample sample2 = new Sample("父类静态成员2正在执行!"); static { System.out.println("父类静态块2正在执行!"); } Father() { System.out.println("父类构造函数正在执行!"); } Sample sam2 = new Sample("父类成员2正在初始化!"); } //子类 public class Children extends Father { static { System.out.println("子类static块1执行"); } static Sample staticSamSub = new Sample("子类静态成员staticSamSub初始化"); Children() { System.out.println("子类构造函数正在执行!"); } Sample sam1 = new Sample("子类 sam1成员初始化"); static Sample staticSamSub1 = new Sample("子类静态成员staticSamSub1初始化"); static { System.out.println("子类static块2执行"); } Sample sam2 = new Sample("子类 sam2成员初始化"); public static void main(String[] args) { Children children = new Children(); } }
输出:
父类静态块1正在执行!
父类静态成员1正在执行!
父类静态成员2正在执行!
父类静态块2正在执行!
子类static块1执行
子类静态成员staticSamSub初始化
子类静态成员staticSamSub1初始化
子类static块2执行
父类成员1正在初始化!
父类成员2正在初始化!
父类构造函数正在执行!
子类 sam1成员初始化
子类 sam2成员初始化
子类构造函数正在执行!
有输出结果可看出:初始化的顺序总是先静态后非静态,先父类后子类。
综上可得Java对象初始化顺序如下图:
总结:Java对象的正确初始化对于程序设计而言十分重要,重载可以使用户根据需求自定义参数列表,对象初始化总是遵循一定的顺序,先静态后非静态,先父类后子类。