1. This关键字
1.1 引子
如果一个类Test的两个对象a和b,都调用类中的一个方法f(),f()如何知道是被a还是
被b调用?在这里编译器做了一些幕后操作,它暗自把 “所操作对象的引用”作为第一个参数传递给f()。所以,“a.f(1);”就变成了“Test.f(a,1);”。(说明:在类的存储中,静态变量为类所有,非静态变量在各个对象中都分配有存储空间,这些值用来标志各个对象本身,至于类中的方法,应该是只有一份方法列表,为各个对象所公用。)
this关键字只能在方法内部使用,表示“调用方法的那个对象”的应用。只有当需要明确指出对当前对象的引用时,才需要使用this关键字,例如,当需要返回对当前对象的引用时,需要“return this;”。
1.2 this在构造器中的使用
这种用法适合于一个类中有多个类构造器的情况。
在构造器中,如果为this添加了参数列表,那么就有了不同的含义:这将产生对符合此参数列表的某个构造器的明确调用。这样就可以直接调用其他构造器。
例如:
public class Flower {
int petalCount = 0;
String s = new String("null"); //类中有两个属性
Flower(int petals) { //构造器1
petalCount = petals;
}
Flower(String ss) { //构造器2
s = ss;
}
Flower(String s, int petals) { //构造器3
this(petals); //this使用,必须在构造器的起始处
//! this(s); // Can't call two!//不能调用两次
this.s = s; // Another use of "this"//this的另一种用法
}
Flower() { //构造器4
this("hi", 47);
}
void print() {
//! this(11); // Not inside non-constructor! //不能在非构造器中使用
}
public static void main(String[] args) {
}
} ///:~
说明:这种带参数列表的this只能在构造器中使用,而且只能调用一次,并且在构造器的起始处。
2. Finalize()方法
Finalize()方法的工作原理:当垃圾回收器准备释放对象占用的存储空间时,它会首先调
用要释放对象的finalize() 方法。finalize() 方法定义在Object类中。
finalize() 方法和c++中的析构函数截然不同:
l 垃圾收集不等于析构,析构函数是程序员主宰的,GC是系统主宰的。
l 对象可能不被垃圾回收
l 垃圾回收只与内存有关
例如:假设某个对象在创建过程中会将自己绘制到屏幕上,如果不是明确地从屏幕上将
其从屏幕上擦除,它可能永远得不到清理。如果在finalize() 方法中加入擦除功能,当“垃圾回收”时(不能保证一定会发生),finalize() 将得到调用,图像将会被擦除,如果“垃圾回收”没有发生,图像就会一直保留下来。
如果程序结束,并且垃圾回收器一直没有释放你创建的任何对象的存储空间,随着程序的退出,那些资源也会全部交还给操作系统。这个策略是恰当的,因为GC本身也有开销,要是不使用它,那就不用支付这部分开销了。
注意(编程技巧):在构造函数只做最基本的初始化,例如new一个对象等,所有有关内存之外的其他初始化都不要在构造函数中编写,应单独写,且名称要有含义,如open(),claose()。
3. 垃圾回收器
3.1 什么是垃圾
引子:“引用记数”是一种简单但速度很慢的垃圾回收技术,它常用来说明垃圾回收器的工作方式,但似乎从未被应用于任何一种Java虚拟机中。每个对象都有一个引用记数器,当有引用连接至对象时,引用记数加1。当引用离开作用域或被置为null时,引用记数减1。当发现某个对象的引用记数为0时,就释放其占用的空间。这个方法有个缺陷:如果对象之间存在循环引用,可能会出现“对象应该被回收,但引用记数却不为零”的情况。
没有任何引用指向某个对象,那这个对象是垃圾;由堆栈和静态变量存储区的引用指向的对象一定不是垃圾,这句话是正确的,但不能包含所有情况。
其实当有引用指向某个对象时,这个对象也可能是垃圾。如何来判断这种情况呢?
如果是垃圾且有引用指向它,那这个垃圾一定被堆内存中一个对象中的引用所指向(而且,这个引用所属对象同样是垃圾)。但同样不能说:如果一个对象没有由堆栈或静态存储区中的引用指向,只有某对象中的引用指向,就说这个对象是垃圾。这种说法同样是片面的。因为如果一个对象不是垃圾,它包含的引用所指向的对象同样不是垃圾。
最好的评判标准:从堆栈和静态存储区中的引用出发,它们所指向的对象不是垃圾,这些不是垃圾的对象中所引用的对象同样不是垃圾,以此类推,直到结束。这样就可以标识出所有不是垃圾的对象,剩余的就是垃圾了。
例1:
有两个类A和B(虽然它们的关系比较复杂,但并不属于交互引用的情况):
class A
{
B b=new B();
}
class B
{
A a=new A();
}
在main()函数中:
public static void main(String[] args)
{
A aa=new A();
B bb=new B();
aa=null;
bb=null;
}
图释(分上图和下图):
上图是例1的情况,1是B对象,2是A对象,3是A对象,4是B对象,其中1和4不是同一个对象,2和3不是同一个对象。虽然情况比较复杂,但不属于互相引用的情况,所以即使使用“引用记数”的方式也能回收,只不过有一定的依赖关系,并不构成死循环,即2和4的回收得依赖于1和3先被回收。
下图是例2的情况,1是B对象,2是A对象,1和2相互引用,构成死循环,如果仅仅依靠“引用记数”的方式好象解决不了这个问题。
例2:交互引用的情况
public class TestCycleRef {
public static void main(String[] args) {
A aa = new A();
B bb = new B();
aa.setB(bb);
bb.setA(aa);
aa = null;
bb = null;
System.gc();
}
}
class A {
B b ;
public void finalize() {
System.out.println( " A is finalize" );
}
/**
* @return Returns the b.
*/
public B getB() {
return this.b;
}
/**
* @param b The b to set.
*/
public void setB(B b) {
this.b = b;
}
}
class B {
A a ;
public B() {
a = new A();
}
public void finalize() {
System.out.println( " B is finalize" );
}
/**
* @return Returns the a.
*/
public A getA() {
return this.a;
}
/**
* @param a The a to set.
*/
public void setA(A a) {
this.a = a;
}
}
3.2 C++和Java堆内存处理方式简单比较
可以把C++里的堆想象成一个院子,里面每个对象都负责管理自己的地盘,一段时间后,对象可能被销毁,但地盘必须加以重用。我认为(因为没有找到相关资料):堆这部分内存都由程序员分配和销毁,当申请一块空间时,要查看每一块空余空间,看是否有大小合适的,如果要分配的空间比其他所有空间都大,编译器可能要整理堆内存,以满足分配。
在某些Java虚拟机中,堆的实现截然不同:它更象一个传送带,每分配一个新对象,它就往前移动一格。这意味着对象存储空间的分配速度非常快。Java的“堆指针”只是简单地移动到尚未分配的区域,其效率比得上C++在堆栈上分配空间的效率。当然,实际过程中在簿记工作方面还有少量额外开销,但比不上查找可用空间开销大。其实,Java中的堆未必完全像传送带那样工作,要是那样的话,势必会导致频繁的内存页面调度,这将极大影响性能,并最终耗尽资源。其中的秘密在于垃圾回收器的介入。当它工作的时候,将一边回收空间,一边使堆中的对象紧凑排列,这样“堆指针”就可以容易移动到更靠近传送带的开始处,也就尽量避免了页面错误。通过垃圾回收器对对象重新排列,实现了一种高速的、有无限空间可供分配的堆模型。
4. 成员初始化
4.1区分局部变量和类中的数据成员
l 局部变量:方法内部的变量是局部变量,java以编译时错误的形式来保证,在使用这个局部变量之前必须初始化。
l 类中的数据成员:有一个默认初始值,“0”。
4.2 初始化顺序
初始化包括,自动初始化、定义初始化、构造器初始化和静态变量初始化。
假设有个Dog的类:
l 当首次创建类型为Dog的对象时(构造器可以看成静态方法),或者Dog类的静态方法或静态字段首次被访问时,Java解释器必须查找类路径,以定位Dog.class文件。
l 然后载入Dog.class(这将创建一个Class对象),有关静态初始化的所有动作都会执行。因此,静态初始化只在Class对象首次加载的时候进行一次。
l 当用new Dog()创建对象的时候,首先在堆上为Dog对象分配足够的存储空间。
l 这块存储空间会被清零,这就自动将Dog对象中的所有基本类型数据都设置成了缺省值(对数字来说是0,其他类型与此对应),而引用则被设置成null。
l 执行所有出现与字段定义处的初始化动作(定义初始化)。
l 执行构造器(这里未考虑继承问题)。
5. 数组初始化
5.1 length属性
对于这个属性需要注意一点:length属性只能说明这个数组所能容纳的最多元素个数,对于数组元素而言有缺省值(“0”或null),其中有多少是通过自己赋值的元素是不得而知的。
5.2多维数组
举例:
int[][] a={{1,2,3},{1,2}};
说明:有2个最大元素是3的一维数组。即:int[2][3]。
int[][][] b={
{ {1,2,3,4}, {1,2} ,{1,} },
{ {4,5,6,7}, {7,8} }
};
说明:有两个二维数组,每个二维数组最多包含三个一维数组,一维数组包含最多的元素为4,即:int[2][3][4]。
遍历程序:
for(int i=0;i<a.length;i++)
for(int j=0;j<a[i].length;j++)
System.out.println(a[i][j]);
5.3 实例说明数组初始化
class A{} //定义一个类A
public void test()
{
A[] arrayObjectA;
System.out.println(arrayObjectA); //编译会报错:局部变量没有初始化。arrayObjectA只是堆栈中的一个局部变量,此时没有指向任何对象,“A[]”只是说明arrayObjectA可以指向A类型的数组,仅此而已。
arrayObjectA=new A[3];
System.out.println(arrayObjectA[i]); //编译不会报错。
for(int i=0;i<arrayObjectA.length;i++)
{ System.out.println(arrayObjectA[i]);} //会打印出null。这相当于对局部变量arrayObjectA已经初始化,但arrayObjectA所指向的数组对象中的各个数组元素只有缺省初始化:null。所以,此时如果对数组arrayObjectA中的任何一个元素进行操作(除打印操作)的话,就会有空指针异常。
for(int i=0;i<arrayObjectA.length;i++)
{ arrayObjectA [i]=new A();} //这才是对数组“有意义”的初始化,之后就可以对数组元素进行一些相关正确操作了。
}
发表于 @
2006年05月13日 09:18:00 | | 编辑|
举报| 收藏