所有局部变量都必须显式给一个初始值,使用未初始化的局部变量会得到编译错误。(C++中未初始的局部变量会是一个未定义的值。)
- void f()
- {
- int i;
- i++; // error - i is not initialized
- }
类的成员变量在未显式初始化时会得到默认的初始值,如int会初始化为0,而boolean初始化为false。
同样你也可以显式地初始化成员变量:
- public class MyClass
- {
- int a = 5;
- bool b = true;
- char c = 'c';
- SomeClass sc = new SomeClass();
- }
更有甚者你可以通过把一个方法的返回值作为一个成员变量的初始值:
- public class MyClass
- {
- int a = f();
- int b = g(a);
- int f() { return 10; }
- int g(int n) { return n * 3; }
- }
成员变量初始化的顺序取决于它们定义的顺序。上述代码中,a会先于b初始化。如果改变它们的顺序会出现编译错误:
- public class MyClass
- {
- int b = g(a); // error - a is not defined yet
- int a = f();
- }
当然成员变量也可以在构造器中初始化。
static成员变量的初始方法与非静态成员变量的初始化相同。
- public class MyClass
- {
- int a = 5;
- bool b = true;
- static char c = 'c';
- }
以上述代码为例,一个Java类的初始化顺序为:1. 当MyClass对象被创建(new MyClass)或MyClass的静态方法被调用时,Java解释器会定位MyClass.class文件;
2.载入MyClass.class,初始化所有静态成员变量;
3.在new MyClass调用时,在堆上分配足够存储空间;
4.分配出的存储空间全部清零,这也是为什么成员变量在未显式初始化时会有默认的初始值;
5.执行所有成员变量的显式初始化;
6.执行构造器。
多个静态初始化可以组织成“静态子句”
- public class MyClass
- {
- int a = 5;
- bool b = true;
- static char c = 'c';
- static {
- double d = 4.0;
- int e = 7;
- }
- }
实例初始化和静态子句相似,用来初始化非静态成员,并且可以保证在构造器之前调用,这在有多个构造器的时候是非常方便的:
- public class MyClass
- {
- int a;
- boolean b;
- {
- a = 5;
- b = true;
- System.out.println("look!");
- }
- }
数组的定义可以是"int a[]"也可以是"int[] a"。你可以用“int a[] = new int[6];”的方式来初始化一个数组。
- Integer[] i = {
- new Integer(1),
- new Integer(2),
- 3 // Autoboxing
- };
等价于:
- Integer[] i = new Integer{
- new Integer(1),
- new Integer(2),
- 3, // the last comma is also valid!
- };
所以花括号的定义仍然是在堆中分配内存。
可变参数列表是非常有趣的特性:
- void f(Object... args)
- {
- for (Object o: args)
- System.out.println(o);
- }
- // caller
- f(1, 'c', false);
枚举不是一些简单的常数,它是可以用打印的。
- public enum E { A, B, C };
- //caller
- E e = E.C;
- System.out.println(e);
Java中的对象是不需要手动清理的,垃圾回收器会(在适当的时候)清理所有没被引用的对象。当然,你也可以强制让垃圾回收器进行清理:
- void f()
- {
- MyClass mc = new MyClass();
- mc = null;
- System.gc();
- }
垃圾回收器的机制:
有一种所谓的“引用计数机制”,当对象被引用时计数器加一,当取消引用时计数器减一。当计数器为0时则释放该对象。这种模式非常慢,JVM实际上用一些更快的模式。
根据当前所有的引用(指针),肯定可以能找到它在堆中所指的对象。通过这种方法可以在堆中找到所有存活的对象。在清理时把这些“活”的对象复制到新堆中并紧密排列,并把所有的引用更新到新堆的地址,再把旧堆弃置。在处理过程中,当前的程序会停止,所以这种操作称为“停止-复制”(Stop-and-copy)。由于分配新内存时,堆里的对象是紧密排列的、连续的,所以只需要移动堆顶指针就可以得到一块新的内存,这样和在栈中分配内存的相似,所在java中的new操作非常高效。
由于stop-and-copy需要额外一个堆,所以要维护比实际需要大一倍的内存空间,此外复制操作也是很耗时的,当程序进入稳定状态时,只有少量的垃圾产生,但仍然复制所有的对象无疑是种浪费。所以有另一种模式“标记-清扫”(mark-and-sweep)。在这种模式下,仍然通过引用找到所有存活的对象,然后把所有死掉的对象释放掉,再把剩下的堆空间整理成连续的空间。这种方式仍然需要中止当前程序。
JVM综合上述两种模式,按需在堆中分配几块较大的内存,复制操作就发生在这些内存块之间。每个块都有表示它存活时间的“代数(generation count)”,当一个块被清理过一遍后它的代数会增加,一般清理会从代数最低的块开始,因为里面可能包含大量的临时变量。当“停止-复制”变得低效时,JVM会切换到“标记-清扫”模式。当后一个模式产生大量内存碎片,就切换回前一个模式。这就是“自适应技术”。
Java的对象可以覆写一个叫finalize的方法。这个方法会在GC释放一个对象之前被调用,且最多只能调一次。但是,“但是”,这个方法并不保证何时被调用,也不保证一定会调用。不能把它当成C++里的析构函数使用。如果需要做一些析构操作,需要定义其它方法,并且进行显式调用。finalize这种东西基本靠不住,不靠谱的,最好别用。
阅读(884) | 评论(0) | 转发(0) |