Java中的初始化和清理

更新时间:2021-10-19 14:28:26 点击次数:4440次
初始化和清理
初始化和清理正是涉及安全的两个问题。在之前的程序中一大部分错误都源自于不正确的初始化以及清理工作。在Java中具有一系列的初始化机制保证数据对象的合理初始化,并且采用垃圾回收器机制保证对象内存的回收问题。

1. Java中的初始化机制
在Java的类定义中主要涉及到类属性(即静态域),对象属性以及方法中的局部变量,对它们进行初始化主要有两种方法:

在变量定义时提供初始值;(基本数据类型初始化为false或0, 对象类型初始化为null)
在变量定义时不提供初始值,并强制其在定义时就初始化(否则出现编译期错误);
第一种方式的优点是提供初始值使得编程更加灵活,而缺点是如果忘记对其正确初始化,使用变量的默认初始值引起错误而且不易察觉。第二种方式的优点是强制对其正确初始化,缺点是在变量定义时就进行初始化,缺乏灵活性,而且定义时就初始化,后期变量在使用时再做变化,就会引起两次初始化的开销。在Java中,类属性和对象属性采用第一种方式,尤其是对象属性,在实例化不同对象时,变量值需要不同值,采用第一种方式并引入构造器的方式在构造器中对对象属性进行初始化,增加了编程的灵活性(实例化不同对象,其对象属性不同,代表着不同对象的不同状态),同时避免了第二种方式中两次初始化的开销(为变量提供初始值的初始化开销较小)。而方法中的局部变量则采用第二种方式,局部变量在方法被调用时才会使用并初始化,并不代表对象的状态,因此不需要多样性,只需要满足方法的逻辑即可,因此采用第二种方式还可以避免不正确的初始化带来的错误。

方法的重载
方法重载是指方法名称相同而参数列表不同,他们代表着同一类的业务逻辑,但作用对象可能不同。为什么引入方法的重载,是因为在程序语言中,名称(变量或方法)代表一个内存地址或者函数的入口地址,是我们对内存操作的一个代号,由于内存地址不易记忆和识别,因此使用带有一定含义的名称替代,而在编程过程中经常出现相似的一类逻辑,逻辑大致相同而作用对象不同,需要定义不同的方法,为了区分则需要较长的方法名称造成一定的编码负担,因此引入方法重载机制,使用相同方法名,参数列表不同即可区分不同函数,至于内部的方法签名则是使用的方法名称加参数列表,而编程过程中无需考虑减少了负担。
引入方法重载的同时也带来一定的问题,就是在方法调用时传入参数,系统需要根据参数类型解析选择正确的方法,如果参数类型与方法的参数列表中的类型一一对应时不会发生错误,选择也很容易,但是如果出现需要类型转换时,则会出现一定问题,而系统选择的原则时,对方法调用的参数进行向上转型,选择最近可用方法进行调用。

最后需要注意的问题,参数列表中类型相同,但顺序不同也可以作为重载,但是需要尽量避免此类的行为。

构造器
Java引入构造器保证对对象属性的初始化。构造器用于定义的类进行实例化不同的对象,因此可以在对象属性默认初始化以后,在构造器中传入不同参数的方式对对象属性进行不同的初始化,从而实例化出不同状态的对象。构造器可以理解为一种静态方法,没有返回值,其名称与类名相同,所以需要不同的构造器时必须重载,这也是引入重载的另一个原因(也是为什么在这里介绍方法的重载)。构造器的主要作用就是对象的初始化,主要针对对象属性。
类的定义中,如果没有定义构造器,则编译器会为类构造一个默认构造器(即无参构造器,其内没有任何初始化行为),而类中如果定义了构造器,则不再构造默认构造器(不代表没有无参构造器,可以自己定义)。在构造器中,其逻辑为首先调用其父类构造器(没有指明则调用父类无参构造器,如果父类没有无参构造器则出现编译期错误,也可以指定调用父类的哪一个构造器,使用super(args),且该句位于构造器的第一行,否则也会出现编译期错误),然后编写对象属性进行初始化的逻辑,通过构造器的参数列表定义对象状态的多样性。

这里尤其注意父类构造器的调用问题,如果自定义的类继承某个类,而该类没有无参构造器,则自定义的类必须定义构造器,因为编译期给添加的默认构造器为无参的且没有任何执行逻辑的构造器,所以其内部必然默认调用父类的无参构造器,而父类不存在无参构造器,需要明确调用,因此自定义类必须定义一个构造器,至于参数列表是什么类型没有限制,但是构造器内部的第一行必须明确调用父类的某个存在的构造器。

为了简化编程在构造器的逻辑编写过程中还可以调用重载的其他构造器,使用this(args)的方式,这个调用的位置没有限定。

最后注意构造器可以理解为类的静态方法,没有返回值(即void),用于实例化对象,而实例化的对象并不是构造器的返回值。

初始化顺序
对于初始化问题可以分为两个过程,即类的初始化和对象的初始化。类的初始化即类实例的加载,就是在虚拟机中实例化该类的class对象,这个过程主要执行类属性的初始化,即静态域和静态块的初始化,其顺序为首先父类的类初始化,然后按照声明顺序执行静态域或者块的初始化,类的初始化触发条件包括两个,一个是使用类的静态域或静态方法(这一种情况不会触发对象初始化),第二个是对象实例化(即触发对象的初始化);对象的初始化主要执行对象属性的初始化,即属性中的非静态域,其顺序为按照声明首先执行父类的对象初始化,然后顺序执行对象属性的默认初始化或者显示声明的初始化(这里可以理解为在子类实例化一个对象时,需要首先实例化一个父类作为它的一个属性包含其中),最后调用构造器。

因此,初始化的顺序可以总结为:父类的类初始化,子类的类初始化,父类的对象初始化(先初始化对象属性,然后调用构造器),子类的对象初始化(顺序同样也是先初始化属性,在调用构造器)

代码样例:

package initialization;

public class OrderTest {


    //static int a = Child.sField1;   //执行语句1
    public static void main(String[] args) {
        System.out.println("before new child");
        //Child child = new Child();    //执行语句2

    }

}

class Parent{
    private int parentField = getParentField();
    static int sParentField =  getSParentField();
    static{
        System.out.println("parent static block");
    }


    public Parent(){
        System.out.println("parent constructor");
    }

    private static int getSParentField() {
        System.out.println("getSParentField");
        return 0;
    }

    private int getParentField() {
        System.out.println("getParentField");
        return 0;
    }
}

class Child extends Parent{
    private int field1 = getField1();
    static int sField1 =  getSField1();
    static{
        System.out.println("static block");
    }   
    static int sField2 =  getSField2();

    public Child(){
        System.out.println("Child constructor");
    }

    private int field2 = getField2();
    private static int getSField1() {
        System.out.println("getSField1");
        return 0;
    }

    private static int getSField2() {
        System.out.println("getSField2");
        return 0;
    }

    private int getField1(){
        System.out.println("getField1");
        return 0;
    }
    private int getField2(){
        System.out.println("getField2");
        return 0;
    }


}

在只有执行语句1时,即只会触发类的初始化,其输出结果为:

getSParentField
parent static block
getSField1
static block
getSField2
before new child

在只有执行语句2时,即实例化对象时,其输出结果为:

before new child
getSParentField
parent static block
getSField1
static block
getSField2
getParentField
parent constructor
getField1
getField2
Child constructor

可以看出即使在构造器之后声明的属性也会在构造器之前初始化。

最后注意类的初始化,即静态域和静态块的初始化只执行一次,后续的使用静态域以及实例化对象都不会再次出发类的初始化,只执行后续的对象初始化。

2. Java中的终结处理和垃圾回收
在Java中,通过new操作生成的对象其内存都分配在堆空间中,Java通过垃圾回收器负责回收程序中不再使用的对象所占的内存。垃圾回收器只负责回收程序中废弃对象占用的内存,然而程序运行过程中还会占用一些其他资源需要释放,比如打开的文件,连接的网络等。

不可使用的终结方法finalize()方法
类似于C++中的析构函数,Java中引入了finalize()方法,其工作原理假定为:一旦垃圾回收器准备好释放对象所占用的内存空间,首先调用对象的finalize()方法,并且在下一次垃圾回收动作发生时释放对象占用的内存空间。但是该方法并不一定得到调用,所以在该方法中清理除内存以外的资源,是不合理的。

Java中垃圾回收问题存在以下特点:
- 1. 对象可能不被垃圾回收
- 2. 垃圾回收并不等于析构
- 3. 垃圾回收只与内存有关

Java中垃圾回收器的工作是在内存面临不够用的时候才会触发GC,此时回收不再使用对象的内存,而当一个对象不再使用时,并不能保证一定会被回收,也就不能保证finalize()函数会被调用。

public class FinalizeTest {

    public static void main(String[] args) {
        // TODO Auto-generated method stub
        new FinalizeTest();
        //System.gc();
    }

    @Override
    protected void finalize() throws Throwable {
        // TODO Auto-generated method stub
        super.finalize();
        System.out.println("finalize");
    }

}

如果不显式调用Sysetem.gc(),此时就不会触发GC,finalize()方法也不会被调用。垃圾回收只与内存有关,因此对象相关的其他资源需要我们通过合理的方式显式清理。

垃圾回收器的工作方式
Java中使用垃圾回收器回收通过new操作生成的而不再使用的对象所占用的内存空间,从而简化编程。而垃圾回收面临三个问题:
- 1. 何时回收
- 2. 回收哪些对象的内存
- 3. 如何回收

对于第一个问题就是之前所说的在Java虚拟机的内存不够用时就会触发一次GC,回收不再使用对象的内存。而回收的自然是不再使用对象的内存,如何搜寻不再使用的对象,Java中没有使用引用计数法,而是使用追溯其存活堆栈或静态存储区之中的引用,不再被引用的对象自然是待处理的“对象”,而对于回收算法则较为复杂,主要包括“停止-复制”和“标记-清扫”算法,前者可以使得回收之后的内存相对整齐,便于内存分配,但是复制过程的消耗较大,而“标记-清扫”则相对轻量级,但是一段时间以后内存则会变得碎片化,因此Java考虑到不同时期不同对象的内存分配以及分布特点,采用了自适应的,分代的,停止-复制,标记-清扫式垃圾回收器,具体的垃圾回收过程可以参考相关书籍。

本站文章版权归原作者及原出处所有 。内容为作者个人观点, 并不代表本站赞同其观点和对其真实性负责,本站只提供参考并不构成任何投资及应用建议。本站是一个个人学习交流的平台,网站上部分文章为转载,并不用于任何商业目的,我们已经尽可能的对作者和来源进行了通告,但是能力有限或疏忽,造成漏登,请及时联系我们,我们将根据著作权人的要求,立即更正或者删除有关内容。本站拥有对此声明的最终解释权。

回到顶部
嘿,我来帮您!