您的位置:首页 > 编程语言 > Java开发

Java编程思想:第5章 初始化与清理

2015-08-07 17:39 603 查看
随着计算机革命的发展,“不安全”的编程方式已经逐渐成为编程代价高昂的主因之一。

初始化和清理,正是涉及安全的俩个问题。

5.1 用构造器确保初始化

构造器名与类名相同,没有返回值

5.2 方法重载

构造器的重载与方法重载

5.2.1 区分重载的方法

参数列表的不同(不同顺序也是重载,但不建议这么做)

5.2.2 涉及基本类型的重载

  void print(char c){System.out.println("The character is "+c); }
void print(byte b){System.out.println("The byte is "+b);}
void print(short s){System.out.println("The short is "+s);}
void print(int i){System.out.println("The int is "+i);}
void print(long l){System.out.println("The long is "+l);}
void print(float f){System.out.println("The float is "+f);}
void print(double d){System.out.println("The double is "+d); }

1)如果直接传入数值,如 print(5)则5会被当做 int x = 5; print(x)去寻找最合适的print(int)

2)常量之外的类型会去寻找最合适的类型,没有同类型,但是有更大类型时会类型提升

3)char是一个特例,如果没有print(char)则char当做int,执行print(int)

4)需要用cast才能放入小类型的方法里

5.2.3 用返回值区分重载方法

void f(){}

int f(){return 1;}

这样是不可以的,因为很多时候我们不需要返回值,只是调用而已,那么我们会写:

f();这样是无法区分它是哪个方法的

5.3 默认构造器

如果类里没有构造器,编译器会帮你弄一个。

有了就不会帮你造一个了。

5.4 this关键字

可以用于指代调用对象方法时的“当前对象”,可以解决参数和域重名问题。

5.4.1 构造器中调用构造器

this(参数);

必须放于第一行,且最多只能调用一个。

5.4.2 static的含义

static方法就是没有this的方法,它属于类,不属于对象。

5.5 清理:最终处理和垃圾回收

Java垃圾回收器负责回收由new创建并且不再被使用的对象。

特殊情况是:如果一个对象以非new方式获得了一块内存区域,GC就不知道怎么回收这块特殊的内存。Java提供了finalize()方法,它的工作原理“假定”是这样的:一旦垃圾回收器准备好释放对象占用的存储空间,将首先调用finalize()方法, 并且在下一次垃圾回收动作发生时,才会真正回收对象内存。所以finalize()方法可以在垃圾回收时做一些重要的清理工作。

//此处疑惑感谢@Launcher的细心解答。

//---------------解答开始

也就是你有这样一个 class,伪码:

class A{

IntPtr m_pBuffer;

A(){

m_pBuffer = malloc(1000);

}

void finalize(){

free(_M_pBuffer);

}
}

A 对象没有用 new ,而是使用 malloc 获得了一块特殊的内存区 m_pBuffer。

A a = new A(); // 还是使用 new 获得对象 A

但是对象 a 本身不用 new 获得的特殊内存区 m_pBuffer,不会被 GC 释放,所以你需要实现 finalize,在里面手动释放。

//---------------解答结束

这里有一个误区:

finalize() 与 C++中的析构函数(销毁对象时必须要用到这个函数)相同。

这是不正确的,C++中对象一定会被销毁,即析构函数一定被调用,而Java里的对象并不总是被垃圾回收。换句话说:

1)对象可能不被垃圾回收

2)垃圾回收不等于析构

5.5.1 finalize()用途何在

3)垃圾回收只与内存有关。

Java中无论对象是如何创建的,GC都会回收它的内存,对finalize()的需要限制到一种特殊情况:用了创建对象以外的方式,为对象分配了存储空间。

之所以要有finalize(),是由于在分配内存时可能采用了类似C语言的做法,这种情况主要发生在使用Java调用本地代码,如用C的malloc()函数系列来分配存储空间,所以需要在finalize()里调用本地代码,使用free()来释放这些内存。

所以不要过多使用finalize(),它不是进行普通清理工作的合适场所。

5.5.2 你必须实施清理

C++中所有对象都会被销毁。包括局部对象(在栈上创建的)和用new创建的对象(delete调用析构函数)。

Java不允许创建局部对象,必须使用new创建,也没有delete可用,释放对象内存只能由GC来完成,但是它并不保证一定去回收。如果希望在进行释放存储空间之外的清理操作,得明确在finalize()里使用,这等于析构了,但是没析构方便。

如果JVM没有面临内存耗尽的情况,它是不会浪费时间和资源去执行垃圾回收以恢复内存的。

5.5.3 终结条件

因为finalize()方法是在垃圾回收前执行的,所以复写finalize方法可以检测"逻辑上"的这个对象是否应该被终结。借此来发现程序中是否有缺陷。如:某对象代表了一个打开着的文件,而我们规定这种对象回收时必须为关闭状态。为了检测程序是否有缺陷,我们可以复写finalize方法检测是否打开状态,然后在某次finalize中查看是否存在问题,虽然finalize并不一定会调用,但是只要调用了一次我们就知道是否有问题,这才是关键。

我们可以利用System.gc()来建议做GC操作,但JVM有可能不听我们的。

5.5.4 垃圾回收器如何工作

在以前所用过的程序语言中,在堆上分配对象的代价十分高昂,因此容易有JAVA中的对象在堆上分配方式也非常高昂。但实际上垃圾回收器对提高对象创建的速度有非常明显的效果。Java中堆内存的分配方式就像传送带一样,通过移动“堆指针”来完成内存分配,而其中最关键的是垃圾回收器在工作时,一边回收空间,一边使堆中对象紧密排列,从而保证了“堆指针”可以很容易分配内存,实现了高速的无限空间的可供分配堆模型。

先看其他系统的垃圾回收机制模型:

1.引用计数,简单但很慢的垃圾回收技术。且难以处理循环引用问题。

2.更快的模式,从堆栈和静态存储区开始,用引用向下搜索所有对象,以及对象间引用,保证每个活的对象都被搜索到且避免了循环引用问题。

1和2都是寻找“活”着对象的方式,在基于2的方式之下,Java虚拟机采用“自适应”技术。

如何处理寻找到的存活对象,取决于不同Java虚拟机的实现。

A.停止-复制 stop-and-copy, 暂停程序的运行,然后将所有存活的对象从当前堆复制到新的堆里,并且紧密排列,然后就可以简单直接的分配新空间了。当把对象从一处搬到另一处时,所有指向它的那些引用都必须修正。问题一:对于这种“复制式”回收器而言,效率会降低,需要在2个堆里倒腾,还要维护比实际需要多一倍的空间。某些Java虚拟机的解决方法是,按需从堆里分配几块较大的内存,复制动作发生在这些大块内存之前。问题二:对于复制,程序进入稳定状态之后,只会产生少量垃圾,甚至没有垃圾,尽管如此,复制式回收器仍然会把所有内存从一处复制到另一处,很浪费。一些Java虚拟机会进行检查:要是没有新垃圾产生,就会转换到另一种工作模式这种模式叫 标记-清扫。一般用途而言,标记清扫的效率很低,但是如果垃圾很少甚至没有,它的速度就很快了。

B.标记清扫 mark-and-clean, 所依据的思路同样是从堆栈和静态存储区出发,遍历所有引用,进而找出所有存活对象。每当它找到一个存活对象,就会给这个对象一个标记,这个过程不会回收任何对象。只有当标记工作全部完成之后,清理动作才会开始。清理过程中没有标记的对象将被释放,不会发生任何复制动作。所以剩下的堆空间是不连续的,垃圾回收器希望得到连续空间的话就得重新整理剩下的对象。

这里讨论内存分配以较大的“块”为单位的Java虚拟机。严格意义来说,停止-复制是要求释放垃圾对象之前,必须把对象从一个堆复制到新堆里的,这将导致大量的内存复制。此处有了块之后,垃圾回收器就可以往“废弃”的块里复制对象了,每个块有自己的代数记录它是否存活(废弃)。如果块在某处被引用,代数增加,垃圾回收器会对上次回收动作之后新分配的块进行整理。这对处理大量短命的临时对象很有帮助。垃圾回收器会定期进行完整的清理动作-大型对象仍然不会被复制,内含小型对象的那些块则被复制并整理。Java虚拟机会进行监视,如果所有对象都很稳定,垃圾回收器效率很低的话,就会切换到“标记-清扫”方式;同样虚拟机会跟踪“标记-清扫”的效果,要是堆空间出现很多碎片,就会切换回“停止-复制”方式。这就是自适应技术。

Java虚拟机还有很多其他用于提升速度的附加技术。尤其是与加载器操作有关的,被称为“即时Just-In-Time,JIT”编译器技术。这种技术可以把程序部分或全部翻译成本地机器码(这本来是Java虚拟机的工作),程序运行速度因此得以提升。当需要装载某个类时,编译器会先找到class文件,然后把字节码装入内存。此时有两种方案可供选择,一是让即时编译器编译所有代码,缺点是这种加载动作散落在整个程序生命周期内,累加起来要花费更多时间;并且会增加可执行代码长度,导致页面调度,从而降低程序运行速度。二是采用惰性评估,执行频繁的方法和代码会采用即时编译,执行次数越多速度越快。

5.6 成员初始化

Java尽力保证:所有变量在使用前都得到恰当的初始化。对于方法中的局部变量,以编译错误形式贯彻这种保证。对类成员以默认值执行初始化。

5.6.1 指定初始化

可以在申明变量时直接以赋值符号完成初始化。在针对类成员变量初始化时,如果直接给出值,会使得所有对象有相同的值,有时是我们希望的,但有时显得不够灵活。

5.7 构造器初始化

利用构造器来初始化可以得到更大的灵活性。但要牢记:无法阻止自动初始化的进行,它将在构造器被调用前发生。在构造器赋值成员变量之前,变量已经被赋予了默认值。----编译器不会强制我们在构造器初始化,因为初始化早已得到了保证。

5.7.1 初始化顺序

变量定义的顺序决定了初始化顺序。即使变量可能定义在构造器或其他方法之后,但仍旧会在任何方法(包括构造器)被调用之前得到初始化。

5.7.2 静态数据的初始化

动作执行于类加载时,且只执行一次。总结对象创建的过程,假设有个Dog类

1.构造器是静态方法,只是没有static。所以当首次调用构造器,或首次使用Dog类的静态域或方法时,Java解释器必须找到类路径,定位Dog.class文件。

2.然后载入class文件,有关静态初始化的所有动作都会被执行。

3.当用new Dog()创建对象的时候,首先将堆上为Dog对象分配足够的存储空间。

4.这块存储空间会被清零,这就自动的将Dog对象中所有基本类型设置为默认值0和false,引用类型设置为null

5.按申明顺序执行所有出现于字段定义处的初始化动作。

6.执行构造器。

5.7.3 显式的静态初始化

即在static块里完成初始化

5.7.4 非静态实例的初始化

即无static的块里完成初始化

总结:

参照Java 类加载、调用构造器、执行方法的过程

5.8 数组初始化

int[] a1;

如何初始化?

1. Integer[] a1 = {1,2,3};//只能在申明处用这种方式初始化

2. Integer[] a1 = new Integer[]{1,2,3};

3. Integer[] a1;

 a1 = a2;

可以用赋值让2个数组引用指向同一个数组对象。所以允许存在Integer[] a1;这种只有引用没有指向的情况

注意:所有数组都有一个固定成员,可以通过它获取数组里包含了多少元素,但是不能修改它。这个成员就是length,我们只能访问0-(length-1)范围,下标检查是自动进行的,相比于C和C++用这些开销来获得安全是值得的。

5.8.1 可变参数列表

在参数列表的末尾可以选择传入0-N个参数,会自动变成数组,可以作为可选参数存在

5.9 枚举类型

Java SE5开始添加了enum关键字用于创建枚举类型。功能比C、C++完善得多。

public enum Spiciness{

  NOT, MILD, MEDIUM, HOT, FLAMING

}

示例创建了一个名为Spiciness的枚举类型,它有5个常量实例。

使用时:

Spiciness howHot = Spiciness.MEDIUM;

System.out.println(howHot);

--> MEDIUM

当创建了enum时,编译器会自动添加一些有用的特性:

1.toString()用于显示实例名字

2.ordinal()显示该实例常量声明顺序,从0开始

3.静态的values() 方法,按照声明顺序产生一个全值数组

注意:

A.enum本身并不是新的数据类型,它属于编译器行为,本质上还是类,有自己的方法

用类分析器分析后:

public final class study.core.反射.Spiciness extends java.lang.Enum{
//域
public static final study.core.反射.Spiciness NOT;
public static final study.core.反射.Spiciness MILD;
public static final study.core.反射.Spiciness MEDIUM;
public static final study.core.反射.Spiciness HOT;
public static final study.core.反射.Spiciness FLAMING;
private static final study.core.反射.Spiciness[] ENUM$VALUES;
//构造器
private Spiciness(java.lang.String, int);
//方法
public static study.core.反射.Spiciness[] values( );
public static study.core.反射.Spiciness valueOf(java.lang.String);
}
B.enum可以配合switch使用
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: