初始化和清理

初始化和清理

初始化和清理正是涉及安全的两个问题。在之前的程序中一大部分错误都源自于不正确的初始化以及清理工作。在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中垃圾回收问题存在以下特点:

  • 对象可能不被垃圾回收
  • 垃圾回收并不等于析构
  • 垃圾回收只与内存有关

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操作生成的而不再使用的对象所占用的内存空间,从而简化编程。而垃圾回收面临三个问题:

  • 何时回收
  • 回收哪些对象的内存
  • 如何回收

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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 213,099评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,828评论 3 387
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 158,540评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,848评论 1 285
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,971评论 6 385
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,132评论 1 291
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,193评论 3 412
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,934评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,376评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,687评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,846评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,537评论 4 335
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,175评论 3 317
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,887评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,134评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,674评论 2 362
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,741评论 2 351

推荐阅读更多精彩内容

  • 初始化和清理正是涉及安全的两个问题 一、用构造器确保初始化构造器:在java中提供构造器,确保每个对象都会得到初始...
    whyshang阅读 380评论 0 0
  • *构造器是特殊的方法,它没有返回值。这个和返回值为空(void)明显不同。 *区分重载的方法是必须有个独一无二的参...
    e条虫阅读 213评论 0 0
  • 1. Java基础部分 基础部分的顺序:基本语法,类相关的语法,内部类的语法,继承相关的语法,异常的语法,线程的语...
    子非鱼_t_阅读 31,604评论 18 399
  • 从三月份找实习到现在,面了一些公司,挂了不少,但最终还是拿到小米、百度、阿里、京东、新浪、CVTE、乐视家的研发岗...
    时芥蓝阅读 42,220评论 11 349
  • Sunny飞镜阅读 92评论 0 0