第5章 初始化与清理
编程的代价和编程方式的安全息息相关,初始化和清理正是涉及安全的两个问题。
Java采用构造器作为创建对象时自动调用的特殊方法,并额外提供垃圾回收器,清理垃圾,释放内存。 本章将讨论初始化和清理的相关问题,以及Java对它们提供的支持。
5.1 用构造器确保初始化
在Java中,通过提供构造器,类的设计者可确保每个对象都会得到初始化,创建对象时,如果其类具有构造器,Java就会在用户有能力操作对象之前自动调用相应的构造器,从而保证初始化的进行。
构造器的命名需满足以下两点:
- 所取名字不能与成员名称冲突。
- 编译器需要知道应该调用哪个方法。
所以Java采取与C++中一致的解决方案:构造器采用与类相同的名称。
下面是一个带有构造器的简单类:
class Rock {
public Rock() {
System.out.print("Rock ");
}
}
public class SimpleConstructor {
public static void main(String[] args) {
for (int i = 0; i < 10; i++) {
new Rock();
}
}
}
现在,创建对象时,将会为对象分配内存空间,并调用相应的构造器,即确保了在操作对象之前,它已经被恰当地初始化了。
没有任何参数的构造器叫作默认构造器或无参构造器。
构造器也可具有形式参数,如下例所示:
class Rock2 {
public Rock2(int i) {
System.out.print("Rock " + i + " ");
}
}
public class SimpleConstructor2 {
public static void main(String[] args) {
for (int i = 0; i < 8; i++) {
new Rock2(i);
}
}
}
从概念上讲,"初始化"与"创建"是彼此独立的,但在Java中,"初始化"和"创建"捆绑在一起,两者不可分离。
构造器是一种特殊的方法,它没有返回值。
5.2 方法重载
任何程序设计语言都具的一项重要特性就是对名字的运用。
- 当创建一个对象时,也就给此对象分配到的存储空间取了一个名字。
- 所谓方法,则是给某个动作所取的名字。
在Java里,构造器的名字由类名决定,当我们需要以多种方式创建一个对象,即让方法名相同而形式参数不同的构造器同时存在,就必须用到方法重载。所以,构造器是强制重载方法的重要原因。
下例示范了重载的构造器和方法:
class Tree {
int height;
Tree() {
System.out.println("Planting a seedling");
}
Tree(int initialHeight){
height = initialHeight;
System.out.println("Creating new Tree that is " + height + " feet tall ");
}
void info(){
System.out.println("Tree is " + height + " feet tall");
}
void info(String s){
System.out.println(s + ": Tree is "+ height + " feet tall" );
}
}
public class Overloading {
public static void main(String[] args) {
for (int i = 0; i < 5; i++) {
Tree tree = new Tree(i);
tree.info();
tree.info("overloading method");
}
new Tree();
}
}
5.2.1 区分重载方法
区分重载方法的规则是:每个重载的方法都必须有一个独一无二的参数类型列表。
参数顺序的不同也可使方法进行重载,但这会使代码难以维护:
public class OverloadingOrder {
static void f(String s, int i) {
System.out.println("String: " + s + ", int: " + i);
}
static void f(int i, String s) {
System.out.println("int: " + i + ",String: " + s);
}
public static void main(String[] args) {
f("String first", 11);
f(99, "Int first");
}
}
上例中两个f()方法虽然声明了相同的参数,但顺序不同,因此得以重载。
5.2.2 涉及基本类型的重载
对于重载方法的形式参数类型为基本类型时:
- 通常情况下,传入的数据类型小于方法中声明的形参类型,实际数据类型就会被提升。
- 传入类型为char时,如果无法找到恰好接收char的方法,就会把char直接提升为int。
- 传入的实际参数类型较大,就必须通过强制类型转换,否则,编译器报错。
5.2.3 以返回值区分重载方法
通常,我们会误以为可以通过返回值来重载方法。如下面的两个方法:
void f(){}
int f(){ return 1; }
如果我们在调用方法时,不关心该方法的返回值,即:
f();
编译器则无法得知我们需要调用的是何方法。因此,根据方法返回值无法重载方法。
5.3 默认构造器
默认构造器(又名无参构造器)是没有形式参数的,如果一个类中没有任何构造器,则编译器会自动创建一个默认构造器。 例如:
class Bird{}
public class DefaultConstructor {
public static void main(String[] args) {
Bird bird = new Bird();
}
}
但是,如果已经定义了一个构造器(无论是否具有参数),编译器就不会自动创建默认构造器了,此时使用默认构造器,编译器就会报错,没有找到匹配的构造器:
class Bird2{
Bird2(int i) {}
Bird2(double d) {}
}
public class NoSynthesis {
public static void main(String[] args) {
//! Bird2 b1 = new Bird2(); // No default
Bird2 b2 = new Bird2(1);
Bird2 b3 = new Bird2(1.0);
}
}
5.4 this关键字
同一类型的两个对象调用该类的方法,该方法如何得知是哪个对象调用的它。 例如:
class Banana{
void peel(int i){}
}
public class BananaPeel {
public static void main(String[] args) {
Banana a = new Banana();
Banana b = new Banana();
a.peel(1);
b.peel(2);
}
}
事实上,编译器在对象调用方法时,将该对象引用作为第一个参数传递给了方法,我们可以将其理解为:
Banana.peel(a,1);
Banana.peel(b,2);
如果希望在方法内部获得对当前对象的引用,关键字this则可满足需求。 this关键字只能在方法内部使用,表示调用该方法的对象引用。 在一个方法内部调用同一个类的另一个方法时,不必使用this,因为,当前方法中的this引用会自动应用于同一个类中的其他方法。例如:
public class Apricot {
void pick(){}
void pit(){
pick();
}
}
在pit()内部,编译器会自动添加this,即this.pick()。
当需要明确指出对当前对象的引用时,才需要使用this关键字,例如:在需要返回对当前对象的引用时:
public class Leaf {
int i = 0;
Leaf increment() {
i++;
return this;
}
void print(){
System.out.println("i = " + i);
}
public static void main(String[] args) {
Leaf x = new Leaf();
x.increment().increment().increment().print();
}
}
我们发现,由于increment()通过this关键字返回了对当前对象的引用,所以很容易地在一条语句里对同一个对象执行多次操作。
this关键字对于将当前对象传递给其他方法也十分有用。
5.4.1 在构造器内调用构造器
有时,一个类可能有多个构造器,当需要在一个构造器中调用另一个构造器,以更好地复用代码时,this关键字亦可满足需求。
一般情况下,this代表对当前对象的引用,但在构造器中,当this后跟随参数列表,那就代表对符合此参数列表的某个构造器的明确调用。 如下例所示:
public class Flower {
int petalCount = 0;
String s = "initial value";
public Flower(String s) {
this.s = s;
}
public Flower(int petalCount) {
this.petalCount = petalCount;
}
public Flower(String s, int petalCount) {
this(petalCount);
//! this(s); //Can't call two!
this.s = s;
}
public Flower() {
this("hi", 47);
}
void printPetalCount() {
//! this(11); //Not inside non-constructor
System.out.println("petalCount = " + petalCount + " s = " + s);
}
public static void main(String[] args) {
Flower f = new Flower();
f.printPetalCount();
}
}
从上例中,我们发现使用this调用构造器有以下限制:
- 尽管在构造器中可以用this调用另一个构造器,但却不能调用两个。
- 除构造器方法之外,编译器禁止在其他任何方法中使用this去调用构造器。
并且,当参数名称和数据成员名称产生歧义时,this可以很好地解决问题。
5.4.2 static的含义
static方法就是没有this的方法。 static的主要用途就是:在不创建对象的前提下,仅仅通过类本身来调用static方法。
我们可以认为:static方法不是面向对象的。 因为,使用static方法时,不存在this,即不是通过向对象发送消息的方法的方式来处理方法的。 当代码中出现大量的static方法时,需重新考虑架构设计了。
5.5 清理:终结处理和垃圾回收
Java垃圾回收器可以帮助我们回收无用对象占据的内存资源,但也有特殊情况:该对象并非使用new创建而获得了一块特殊的内存区域(由本地方法分配的内存),垃圾回收器则无法回收该内存。 为了应对这种情况,Java允许在类中定义一个名为finalize()的方法:一旦垃圾回收器准备好释放对象的内存,将首先调用其finalize()方法,并且在下一次垃圾回收动作发生时,才会真正回收对象占用的内存。
我们会发现,只要程序没有濒临内存资源用完的那一刻,对象占用的空间就总得不到释放。如果程序执行结束,并且垃圾回收器一直都没有释放内存资源,则随着程序的退出,这些资源也会全部交换给操作系统。 这样做是由于垃圾回收本身也有开销,如果不使用它,即不用支付这部分开销。
5.5.1 finalize()的用途何在
关于垃圾回收,我们需要知道最重要的一点:垃圾回收只与内存有关。无论对象是如何创建的,垃圾回收器都会负责释放对象占据的所有内存。
当使用了本地方法为对象分配了存储空间时,例如调用C的malloc()函数系列来分配内存空间,除非调用free()函数,否则该存储空间将得不到释放,而free()是C和C++中的函数,所以需要在finalize()中用本地方法调用它。
5.5.2 你必须实现清理
在Java中,垃圾回收器会帮助我们释放存储空间。但是,无论是垃圾回收还是终结,都不一定会发生。如果Java虚拟机(JVM)并未面临内存耗尽的情形,它不会浪费时间去执行垃圾回收以修复内存。
如果希望进行除释放存储空间外的清理工作,还是得明确调用某个恰当的Java方法。
5.5.3 终结条件
当某个对象需要被清理时,它应该处于某种状态,使它占用的内存可以安全地被释放。 例如,如果对象代表了一个打开的文件,在对象被回收前程序员应该关闭这个文件。finalize()即可以用来发现对象中是否有未被适当清理的部分:
class Book{
boolean checkedOut = false;
Book(boolean checkOut){
this.checkedOut = checkOut;
}
void checkIn(){
checkedOut = false;
}
protected void finalize() {
if(checkedOut)
System.out.println("Error: checked out");
super.finalize();
}
}
public class TerminationCondition {
public static void main(String[] args) {
Book novel = new Book(true);
novel.checkIn();
new Book(true);
System.gc();
}
}
本例中的对象终结条件是:所有的Book对象在当作垃圾回收前都应该被签入(check in)。但在main()方法中,由于程序员的错误,有一本书未被签入。如果没有finalize()验证终结条件,将难以发现这种缺陷。
System.gc()用于强制进行终结动作。
如果存在继承关系,由于父类中的finalize()方法可能也要做某些重要的事情,我们应当在导出类的finalize()方法中调用父类的finalize()方法,
5.5.4 垃圾回收器如何工作
垃圾回收器对于提高对象的创建速度具有明显的效果。
当垃圾回收器工作时,它一方面回收空间,另一方面使堆中的对象紧密排列。通过垃圾回收器对对象重新排列,实现了一种高速的、有无限空间可供分配的堆模型。
下面我们介绍几种垃圾回收机制:
-
引用记数:每个对象都含有一个引用记数器。当有引用连接至对象时,引用计数加1,当引用离开作用域或被置为null时,引用减1。垃圾回收器会在含有全部对象的列表上遍历,当发现某个对象的引用计数为0时,就释放其占用的内存资源。缺陷是:如果对象之间存在循环引用,则会出现对象应该被回收,但引用计数却不为0的情况,垃圾回收器想要定位这样的交互自引用的对象组也十分困难。
-
停止-复制:从堆栈和静态存储区出发,遍历所有的引用,以此可以发现活的对象。并追溯到每个引用所引用的对象,遍历该对象所包含的所有引用,如此反复进行,最终发现所有存活的对象。在清理时,先暂停程序的运行,然后将所有存活的对象从当前堆复制到另一个堆,没有复制的全部是垃圾。
-
标记-清扫:发现存活对象的思路同时。但每当它找到一个存活对象,就会给对象设一个标记,在此过程中不会回收任何对象。在全部标记工作完成后,没有标记的对象将被释放。不过剩下的堆空间是不连续的,垃圾回收器需要重新整理剩下的对象。
5.6 成员初始化
Java尽量保证了:所有变量在使用前都能得到恰当的初始化。
-
对于方法的局部变量:无论是对象引用还是基本类型变量,如果没有初始化便使用的话,Java会以编译时错误的形式告知程序员。
-
对于类的数据成员,分为基本类型和对象引用两种情况:
- 基本类型:编译器会给类的每个基本类型数据成员一个初始值0。
- 对象引用:编译器会将对象引用赋值为null。
下面的程序可以验证这类情况:
public class InitialValues {
boolean t;
char c;
byte b;
short s;
int i;
long l;
float f;
double d;
InitialValues reference;
void printInitialValues(){
print("Data type Inital value");
print("boolean " + t);
print("char " + c);
print("byte " + b);
print("short " + s);
print("int " + i);
print("long " + l);
print("float " + f);
print("double " + d);
}
public static void main(String[] args) {
InitialValues values = new InitialValues();
values.printInitialValues();
}
}
5.6.1 指定初始化
如果想要为某个数据成员变量赋初值,无论该变量是基本类型还是对象引用,都可以在定义类成员变量的地方为其赋值。 如下所示:
public class InitialValues {
boolean bool = true;
char ch = 'x';
byte b = 66;
short s = 0xff;
int i = 999;
long lng = 1;
float f = 3.14f;
double d = 3.14159;
InitialValues values = new InitialValues();
}
5.7 构造器初始化
在构造器中,可以调用方法或执行某些动作来为变量确定初值。但是,这无法阻止自动初始化的进行,它将在构造器被调用之前发生。 如下述代码:
public class Counter {
int i;
public Counter() {
System.out.println(i);
i = 7;
}
public static void main(String[] args) {
new Counter();
}
}
可以发现,数据成员变量i首先会被置0,然后再给赋值为7。因此,类数据成员的初始化是由编译器保证的,不会强制程序员一定要在构造器或使用前对元素进行初始化。
对于对象引用,在使用前没有进行初始化,编译可以通过,但在运行时会发生异常。
5.7.1 初始化顺序
在类的内部,变量定义的先后顺序决定了初始化的顺序。即使变量定义散布于方法定义之间,它们仍旧会在任何方法(包括构造器)被调用之前得到初始化。 例如:
class Window{
Window(int marker) {
System.out.println("Window(" + marker +")");
}
}
class House{
Window w1 = new Window(1);
House() {
System.out.println("House()");
w3 = new Window(33);
}
Window w2 = new Window(2);
void f(){
System.out.println("f()");
}
Window w3 = new Window(3);
}
public class OrderOfInitialization {
public static void main(String[] args) {
House h = new House();
h.f();
}
}
5.7.2 静态数据的初始化
无论创建多少个对象,静态数据都只占用一份存储区域。static关键字不能应用于局部变量,只能作用于域。如果类数据成员没有进行初始化,静态数据和非静态数据一样:基本类型赋予标准初值,对象引用赋予null。
通过下面的例子,我们可以进一步了解静态存储区域是何时初始化的:
class Bowl{
Bowl(int marker) {
print("Bowl(" + marker +")");
}
void f1(int marker){
print("f1(" + marker + ")");
}
}
class Table{
static Bowl bowl1 = new Bowl(1);
Table(){
print("Table()");
bowl1.f1(1);
}
void f2(int marker){
print("f2(" + marker + ")");
}
static Bowl bowl2 = new Bowl(2);
}
class Cupboard{
Bowl bowl3 = new Bowl(3);
static Bowl bowl4 = new Bowl(4);
Cupboard() {
print("Cupboard()");
bowl4.f1(2);
}
void f3(int marker){
print("f3(" + marker + ")");
}
static Bowl bowl5 = new Bowl(5);
}
public class StaticInitialization {
public static void main(String[] args) {
print("Creating new Cupboard() in main");
new Cupboard();
print("Creating new Cupboard() in main");
new Cupboard();
table.f2(1);
cupboard.f3(1);
}
static Table table = new Table();
static Cupboard cupboard = new Cupboard();
}
从输出可见:当首次生成这个类的对象,或者首次访问该类的静态成员时,该类的静态数据成员都会被初始化,并且只初始化一次。 初始化的顺序是:先静态成员,再非静态成员。
5.7.3 显式的静态初始化
Java允许将多个静态初始化动作组织成一个特殊的静态子句(或静态代码块)。 如下所示:
class Cup {
Cup(int marker) {
System.out.println("Cup(" + marker + ")");
}
void f(int marker) {
System.out.println("f(" + marker + ")");
}
}
class Cups {
static Cup cup1;
static Cup cup2;
static {
cup1 = new Cup(1);
cup2 = new Cup(2);
}
Cups() {
System.out.println("Cups()");
}
}
public class ExplicitStatic {
public static void main(String[] args) {
System.out.println("Inside main()");
Cups.cup1.f(99);
}
}
5.7.4 非静态实例初始化
Java中也有类似静态子句的语法,名为实例初始化:用来初始化每一个对象的非静态变量。 例如:
class Mug {
Mug(int marker) {
print("Mug(" + marker + ")");
}
void f(int marker) {
print("f(" + marker + ")");
}
}
public class Mugs {
Mug mug1;
Mug mug2;
{
mug1 = new Mug(1);
mug2 = new Mug(2);
print("mug1 & mug2 initialized");
}
Mugs() {
print("Mugs()");
}
Mugs(int i) {
print("Mugs(int)");
}
public static void main(String[] args) {
print("Inside main()");
new Mugs();
print("new Mugs() completed");
new Mugs(1);
print("new Mugs(1) completed");
}
}
从输出可以看出,实例初始化子句是在两个构造器之前执行的,它保证了无论调用哪个显式构造器,某些操作都会发生。
5.8 数组初始化
数组的概念为:相同类型的、用一个标识符名称封装到一起的一个对象引用序列或基本类型数据的序列。 定义数组的格式为:
int[] array;
此时得到的是该数组的引用,我们需要对其进行初始化,创建数组的方式有两种:
- 直接赋值:在创建数组的地方,由一对花括号括起来的和数组类型一致的值组成的。
- 使用new创建数组:通过使用new关键字,并为该数组指定大小。
首先,我们使用直接赋值的方式初始化一个数组:
public class ArraysOfPrimitives {
public static void main(String[] args) {
int[] array1 = { 1, 2, 3, 4, 5 };
int[] array2;
array2 = array1;
for (int i = 0; i < array2.length; i++)
array2[i] = array2[i] + 1;
for (int i = 0; i < array2.length; i++)
System.out.println("array[" + i + "] = " + array1[i]);
}
}
可以看出,array2和array1是相同数组的别名。所有的数组都有一个固定成员length,可以通过它获知数组内包含了多少个元素,但不能对其修改。 Java数组计数是从0开始,即最大下标为length-1。并且一旦访问下标过界,就会出现允许时异常。
下面,通过new来创建数组:
public class ArrayNew {
public static void main(String[] args) {
int[] array;
Random random = new Random(66);
array = new int[random.nextInt(20)];
System.out.println("length of array = " + array.length);
System.out.println(Arrays.toString(array));
}
}
在上述代码中,数组的大小由Random.nextInt()方法随机决定,这也表明了:数组的创建是在运行时进行的。并且,我们可以发现,数组元素中的基本类型值会自动初始化为空值。(对于数字和字符就是0,对于布尔型则是false)
当创建的是一个非基本类型的数组时,我们得到的是一个引用数组。 例如:
public class ArrayClassObj {
public static void main(String[] args) {
Random rand = new Random(66);
Integer[] arr = new Integer[rand.nextInt(20)];
System.out.println("length of arr = " + arr.length);
for (int i = 0; i < arr.length; i++)
arr[i] = rand.nextInt(100);
System.out.println(Arrays.toString(arr));
}
}
在上例中,我们创建了一个Integer类型的数组,并在对其初始化时使用了装箱机制。如果没有对其进行初始化,则会有一个默认值null,此时直接使用则会得到运行时异常。
使用花括号括起来的列表也可用来初始化对象数组,并有两种方式:
public class ArrayInit {
public static void main(String[] args) {
Integer[] arr1 = {
new Integer(1),
new Integer(2),
3
};
Integer[] arr2 = new Integer[] {
new Integer(1),
new Integer(2),
3
};
System.out.println(Arrays.toString(arr1));
System.out.println(Arrays.toString(arr2));
}
}
使用花括号创建数组的方式有时很灵活,例如:
public class DynamicArray {
public static void main(String[] args) {
Other.main(new String[] { "fiddle", "de", "dum" });
}
}
class Other {
public static void main(String[] args) {
for (String s : args) {
System.out.println(s);
}
}
}
5.8.1 可变参数列表
Java SE5添加了一个新特性:可变参数列表。 例如:
class A {}
public class NewVarArgs {
static void printArray(Object... args) {
for (Object obj : args)
System.out.print(obj + " ");
System.out.println();
}
public static void main(String[] args) {
printArray(new Integer(66), new Float(3.14), new Double(11.11));
printArray(66, 3.14, 11.11);
printArray("one", "two", "three");
printArray((Object[]) new Integer[] { 1, 2, 3, 4 });
printArray(new A(), new A());
printArray();
}
}
在上述代码中,我们可以发现:System.out.print()方法首先调用该类的toString()方法,如果该类没有定义toString()方法,则会打印类的名字和对象的地址。
有了可变参数,就不必再显式地编写数组语法了,当指定参数后,编译器会自动填充数组,在方法中获取的实参实际上仍旧是数组。并且,当编译器发现你传递的是一个数组时,就不会再进行任何转换了。因此,你可以传递一组事物或一个数组,甚至什么都不传,可变参数列表都能很好地工作。
并且,当具有可选的尾随参数时,可变参数列表可以接收0个参数的特性会很有用:
public class OptionalTrailingArguments {
static void f(int required, String... trailing) {
System.out.print("required: " + required + " ");
for (String s : trailing)
System.out.print(s + " ");
System.out.println();
}
public static void main(String[] args) {
f(1, "one");
f(2, "two", "three");
f(0);
}
}
可变参数列表可以使用任何类型的参数,包括基本类型:
public class VarargType {
static void f(Character... args) {
System.out.println(args.getClass() + " length " + args.length);
}
static void g(int... args) {
System.out.println(args.getClass() + " length " + args.length);
}
public static void main(String[] args) {
f('a');
f();
g(1);
g(new Integer(1));
g();
System.out.println("int[]: " + new int[0].getClass());
}
}
getClass()方法用于获取该对象所属的类。并且,我们发现:如果传递0个元素,那么转换成数组的尺寸为0。可变参数列表与自动装箱和拆箱机制可以和谐共处。
但是,可变参数使得重载过程变得复杂了许多,如:
public class OverloadingVarargs {
static void f(float i, Character... args) {
System.out.println("first");
}
static void f(Character... args) {
System.out.println("second");
}
public static void main(String[] args) {
f(1, 'a');
// f('a', 'b'); Error
}
}
此时,由于float的范围大于char,char型可以被自动提升至float型。所以编译器无法得知应该调用哪个方法。 所以,当重载具有可变参数的方法时,总是要在方法上都添加一个非可变参数:
public class OverloadingVarargs2 {
static void f(float b, Character... args) {
System.out.println("first");
}
static void f(char c, Character... args) {
System.out.println("second");
}
public static void main(String[] args) {
f(1, 'a');
f('a', 'b');
}
}
5.9 枚举类型
在Java SE5添加的新特性还有:enum关键字。 它的定义方法如下所示:
public enum Spiciness {
NOT,
MILD,
MEDIUM,
HOT,
FLAMING
}
由于枚举类型的实例是常量,按照命名惯例,需要使用大写字母表示,当一个名字中有多个单词时,用下划线将它们隔开。
下面介绍如何使用枚举:
public class SimpleEnumUse {
public static void main(String[] args) {
Spiciness howHot = Spiciness.MEDIUM;
System.out.println(howHot);
for (Spiciness s : Spiciness.values()) {
System.out.println(s + ", ordinal " + s.ordinal());
}
}
}
从上例我们可以看出,当创建enum时,编译器会自动添加一些有用的特性:
- toString()方法:方便显示某个enum实例的名字。
- ordinal()方法:表示某个特定enum常量的声明顺序。
- static values():按照enum常量的声明顺序,产生由这些常量值构成的数组。
enum还有一个特别实用的特性:在switch语句内使用。 例如:
public class Burrito {
Spiciness degree;
public Burrito(Spiciness degree) {
this.degree = degree;
}
public void describe() {
System.out.println("This burrito is ");
switch (degree) {
case NOT:
System.out.println("not spicy at all.");
break;
case MILD:
case MEDIUM:
System.out.println("a little hot.");
break;
case HOT:
case FLAMING:
default:
System.out.println("maybe to hot.");
}
}
public static void main(String[] args) {
Burrito plain = new Burrito(Spiciness.NOT),
greenChile = new Burrito(Spiciness.MEDIUM),
jalapeno = new Burrito(Spiciness.HOT);
plain.describe();
greenChile.describe();
jalapeno.describe();
}
}
5.10 总结
经过调查发现,大量的编程错误都源于不正确的初始化和不恰当的清理,所以,初始化和清理在编程语言中具有至关重要的地位。
在Java中,编译器可以保证所有的变量都要被初始化:
- 局部变量如果没有初始化,则无法使用。
- 数据成员变量会有一个默认值,基本类型0,引用变量null。
Java中的垃圾回收器会自动清理不再使用的对象,释放内存资源,但同时也增加了运行时的开销。