1. 构造器
初始化和清理是涉及安全的两个问题,许多C程序的错误都源于忘记初始化变量,当使用完一个元素时,也很容易忘记清理它。C++引入了构造器的概念,这是一个在创建对象时被自动调用的特殊方法,Java中也采用了构造器,并额外提供了垃圾回收器,对于不再使用的内存资源,垃圾回收器能自动将其释放。
在Java中,通过提供构造器,类的设计者可确保每个对象都会得到初始化。创建对象时,如果类具有构造器,就会自动调用相应的构造器。构造器采用与类相同的名称,也避免名称冲突的问题,例如:
class Test {
Test() {
System.out.print("Initialize");
}
}
public class Constructor {
public static void main(String[] args) {
new Test();
}
}
现在在创建Test的对象时,将会为对象分配存储空间,并调用相应的构造器,这就确保在操作对象前已经被初始化。不接受任何参数的构造器叫默认构造器,它也能带有形式参数, 以便指定如何创建对象,例如:
class Test {
Test(int i) {
System.out.print("Initialize i");
}
}
public class Constructor {
public static void main(String[] args) {
new Test(10);
}
}
有了构造器形式参数,就可以在初始化对象时提供实际参数。构造器是一种特殊类型的方法,它没有返回值(这与void不同),并且别无选择,如果没有定义任何构造器,编译器将会自动创建一个默认构造器。
2. 重载
大多数程序语句要求为每个方法提供一个独一无二的标识符,在Java里,构造器是强制重载方法名的另一个原因。如果需要创建一个类,可以用不同的方法初始化,这就需要多个构造器。由于它们都是构造器,所以必须有相同的名字,即类名。为了让方法名相同而形式参数不同的构造器同时存在,必须用到方法重载,它也可应用于其他方法,例如:
class Test {
Test() {
system.out.println("Defalut");
}
Test(int i) {
system.out.println("Initialize i");
}
}
public class Overloading {
public static void main(String[] args) {
new Test();
new Test(10);
}
}
每个重载的方法必须有一个独一无二的参数类型列表,甚至参数顺序的不同,但是使用不同的返回值来区分是不可行的。此外,基本类型能从一个较小的类型自动提升至一个较大的类型,此过程一旦涉及重载,可能会造成一些混淆。
3. this
假如希望在方法的内部获得对当前对象的引用,有专门的关键字:this,它只能在方法内部使用,表示对调用方法的对象的引用,this的用法和其他对象引用相同,但如果在方法内部调用同一个类的另一个方法,就不必使用this,直接调用即可,例如:
class Test {
void a() {return this;}
void b() {a();}
}
如果为类写了多个构造器,可以使用this在一个构造器中调用另一个构造器。在构造器中,如果为this添加了参数列表,那么将产生对符合此参数列表的某个构造器的明确调用,例如:
class Test {
Test(int i) {}
Test(String s) {
this(10);
}
}
尽管可以用this调用一个构造器,但却不能同时调用两个,此外必须将构造器调用置于最起始处,否则编译器会报错。除构造器外,编译器禁止在任何方法中调用构造器。
static方法是没有this的方法,在static方法的内部不能调用非静态方法,这正是static方法的主要用途。
4. 垃圾回收
Java有垃圾回收器负责回收无用对象占据的内存资源。由于垃圾回收器只知道释放经由new分配的内存,所以它不知道如何释放并非使用new获得的特殊内存。
为了应对这种情况,Java允许在类中定义一个名为finalize()的方法,一旦垃圾回收器准备好释放对象占用的存储空间,将首先调用其finalize()方法,并且在下一次垃圾回收动作发生时,才会真正回收对象占用的内存。这与C++中的析构函数有所不同,当程序员调用delete操作符时,就会调用相应的析构函数,而Java虚拟机如果并未面临内存耗尽的情形,它不会浪费时间去执行垃圾回收以恢复内存,这意味着finalize()并不保证一定会发生。
之所有要有finalize(),是由于在分配内存时可能采用了类似C语言中的做法,而非Java中的通常做法,这种情况主要发生在使用本地方法的情况下。本地方法是一种在Java中调用非Java代码的方式,目前只支持C和C++,在非Java代码中,也许会调用malloc()函数来分配存储空间,所以需要在finalize()中用本地方法调用free()来释放。
5. 初始化
Java尽力保证所有变量在使用前都能得到恰当的初始化。对于方法的局部变量,Java以编译时错误的形式来体现。如果类的数据成员是基本类型,那么它们都保证会有一个初始值,如果定义一个对象引用并且不将其初始化,此引用就会获得一个特殊值null,例如:
class Test {
char c;
int i;
String s;
}
可以通过调用某个方法来提供初始值,也可以带有参数,但这些参数必须是已经被初始化的,例如:
class Test {
int i = a(10);
int j = a(i);
int a(int n) {return n;}
}
静态数据成员的初始化只有在必要时才会进行。如果不创建类的对象,或不引用类的静态数据成员,那么静态数据成员永远都不会被初始化。初始化的顺序是先静态对象,而后是非静态对象。变量定义的先后顺序决定了初始化的顺序,但是不论它们定义在什么位置,都会在任何方法(包括构造器)被调用之前得到初始化。
Java允许将多个静态初始化动作组织成一个特殊的静态子句,它看起来像个方法,但实际只是一段跟在static关键字后面的代码,这段代码仅执行一次,例如:
class Test {
static int i;
static {
i = 10;
}
}
6. 数组
数组是相同类型、用一个标识符名称封装到一起的一个对象序列或基本类型数据序列。数组是通过方括号下标操作符"[ ]"来定义和使用的,要定义一个数组,只需在类型名后加上一对空方括号,例如:
int[] a1;
int a2[];
编译器不允许指定数组的大小,定义数组拥有的只是对数组的一个引用,而且没有分配任何存储空间。为了给数组分配存储空间,必须写初始化表达式,例如:
int[] a = {1, 2, 3};
int[] b = new int[]{1, 2, 3};
可以将一个数组赋值给另一个数组,但真正做的只是复制了一个引用,例如:
int[] a1 = {1, 2, 3};
int[] a2;
a2 = a1;
所有数组都有一个固有成员length,可以通过它获知数组内包含了多少个元素,但不能对其修改。Java数组计数也是从第0个元素开始,所以能使用的最大下标数是length-1,与C++不同的是,Java中一旦下标访问过界,就会出现运行时错误,例如:
int[] a = {1, 2, 3};
System.out.print(a.length);
如果在编写程序时,并不能确定数组里需要多少个元素,可以直接用new在数组里创建元素,尽管创建的是基本类型数组,new仍然可以工作,例如:
int[] a;
Random rand = new Random(50);
a = new int[rand.nextInt(20)];
数组元素中的基本数据类型值会自动初始化。
Arrays.toString()方法属于java.util标准类库,它将产生一维数组的可打印版本,例如:
System.out.print(Arrays.toString(new int[]{1, 2, 3}));
7. 枚举类型
在Java SE5中添加了enum关键字,它使我们在需要群组并使用枚举类型集时,可以很方便地处理。在此之前,需要创建一个整型常量集,例如:
public enum Num {
ONE, TWO, THREE, FOUR, FIVE
}
由于枚举类型的实例是常量,因此按命名惯例使用大写字母表示。为了使用enum,需要创建一个该类型的引用,并将其赋值给某个实例,例如:
Num n = Num.ONE;
System.out.print(n);
在创建enum时,编译器会自动添加一些有用的特性。尽管enum看起来像是一种新的数据类型,但事实上enum确实是类,并且具有自己的方法。例如,编译器会创建toString()方法,以便显示某个enum实例的名字,还会创建ordinal()方法,用来表示某个特定enum常量的声明顺序,以及static values()方法,用来按照enum常量的声明顺序,产生由这些常量值构成的数组。