Thinking in Java-初始化与清理

随着计算机革命的发展,“不安全”的编程方式已逐渐称为变成代价高昂的主因之一
初始化清理(cleanup)正是设计线程安全的两个问题。许多C程序错误都源于程序员忘记初始化变量。清理也是一个特殊问题,当时用完一个元素时,很容易把它忘记,这样一来,这个元素占用的资源就会一直得不到释放,结果是资源(尤其是内存)用尽。
C++引入了构造器(constructor)的概念,这是一个在创建对象时被自动调用的特殊方法。Java也采用了构造器,并额外提供了“垃圾回收器”。

用构造器确保初始化

在Java中,通过提供构造器,类的设计者可确保每个对象都会得到初始化。命名有两个问题:第一,所取的任何名字都可能与类的某个成员相冲突;第二,调用构造器是编译器的责任,所以必须让编译器知道应该调用哪个方法。C++语言中采用的解决方案看来最简单且更符合逻辑,所以在Java中也采用了这种方案:即构造器采用与类相同的名称。
不接受任何参数的构造器叫默认构造器,Java中通常使用术语无参构造器。假如Constructor(int)是类中唯一的构造器,那么编译器将不会允许你以其他任何方式创建其对象。
从概念上将,“初始化”与“创建”是彼此独立的。在Java中,“初始化”和“创建”捆绑在一起,两者不能分离。构造器市一中特殊类型的方法,因为它没有返回值。

方法重载

大多数程序设计语言(尤其是C)要求为每个方法都提供一个独一无二的标识符——每个函数都要有唯一的名称。而人类语言存在细微差别的概念“映射”到程序设计语言中时,相同的词可以表达多种不同的含义——它们被重载了。硬要区分这样的差异显得很愚蠢:以洗衬衣的方式洗衬衣,以洗车的方式洗车。
Java(和C++)里,构造器是强制重载方法名的另一个原因。可以使用两个构造器:一个默认构造器,另一个取任意类型作为形式参数的构造器。为了让方法名相同而形式参数不同的构造器同时存在,必须用到方法重载

区分重载方法

每一个重载方法都必须有一个独一无二的参数类型列表。甚至参数的顺序的不同也足以区分两个方法,不过最好不要这么做。

涉及基本类型的重载

基本类型能从一个“较小”的类型自动提升至一个“较大”的类型,如果有某个重载方法接受int型参数,它就会被调用。如果传入的数据类型(实际参数类型)小于方法中声明的形式参数类型,实际数据类型就会被提升。char型略有不同,如果无法找到恰好接受char参数的方法,就会把char直接提升至int型。所以针对这种情况,可以在实际参数类型加上“指导”,如5L15d
如果传入的实际参数大于重载方法声明的形式参数,则必须执行窄化转换,否则编译器会报错。

以返回值区分重载方法

看上去定义时编译器可以根据语境判断出语义void f(){} int f(){return 1;},不过有时你想要的是方法调用的其他效果(这常被称为“为了副作用而调用”),这时你可能会调用方法而忽略返回值,f();,那Java如何才能判断该调用哪一个f()呢?所以,根据返回值来区分重载方法是行不通的。

默认构造器

即使你没有明确定义它,编译器也会调用其默认的构造器,但是如果已经定义了一个构造器(无论是否有参数),编译器就不会帮你自动创建默认构造器。

this关键字

this关键字只能在方法内部使用,表示对“调用方法的那个对象”的引用。如果在方法内部调用同一个类的另一个方法,就不必使用this,直接调用即可。只有当需要明确指出当前对象的引用时,才需要使用this关键字。为了将自身传递给外部方法,必须使用this关键字。

在构造器中调用构造器

Main(int a){
    this.a=a;
}
Main(int a,String s){
    this(a);
    print(s);
}

尽管可以用this调用一个构造器,但是却不能调用两个。此外必须将构造器调用置于最起始处,否则编译器会报错。由于参数名称a与数据成员a的名字相同,可以使用this.a来解决歧义问题。

static的含义

static方法就是没有this的方法。在static方法的内部不能调用非静态方法,在不创建对象的前提下,仅仅通过类本身来调用static方法。

清理:终结处理和垃圾回收

Java有垃圾回收器负责回收无用对象占据的内存资源。假定你的对象(并非使用new)获得了一块“特殊”的内存区域,由于垃圾回收器只知道释放哪些经由new分配的内存,所以它不知道该如何释放该对象的这块“特殊”内存。Java允许在类中定义一个名为finalize()的方法。一旦垃圾回收器准备好释放对象占用的存储空间,将首先调用其finalize()方法,并且在下一次垃圾回收动作发生时,才会真正回收对象占用的内存。
finalize()不能等同于C++中的析构函数。在C++中,对象一定会被销毁;而Java里的对象却并非总是被垃圾回收。
1. 对象可能不被垃圾回收
2. 垃圾回收并不等于“析构”
这意味着在你不再需要某个对象之前,如果必须执行某些动作,那么你得自己去做。如果在finalize()里加入某种擦除功能,当“垃圾回收”发生时(不保证一定会发生),finalize()得到了调用,图像就会被擦除。要是“垃圾回收”没有发生,图像就会一直保留下来。
如果程序执行结束,并且垃圾回收器一直都没有释放你创建的任何对象的存储空间,则随着程序的退出,那些资源也会全部交还给操作系统。

finalize()的用途何在

3. 垃圾回收只与内存有关
使用垃圾回收器的唯一原因是为了回收程序不再使用的内存。无论对象是如何创建的,垃圾回收器都会负责释放对象占据的所有内存。finalize()之所以存在的一点是,由于在分配内存时可能采用了类似C语言中的做法,而非Java中的通常做法。这种情况主要发生在使用“本地方法”的情况下,本地方法是一种在Java中调用非Java代码的方式。本地方法目前只支持C和C++,但它们可以调用其他语言写的代码,所以实际上可以调用任何代码。也许会调用C的malloc()函数系列来分配存储空间,而除非调用free()函数,否则存储空间将得不到释放,从而造成内存泄漏。需要在finalize()中用本地方法调用它。

你必须实施清理

如果在C++中创建了一个局部对象(也就是在堆栈上创建,Java中行不通),此时销毁的动作发生在以“}”为便捷的、此对象作用域的末尾处。如果使用new创建的对象,未使用C++的delete操作符调用析构函数时,就会造成内存泄漏。
相反,Java中不允许创建局部对象,必须使用new创建对象。正是垃圾回收器的存在,使得java没有析构函数,但垃圾回收器不能完全替代析构函数(而且绝对不能直接调用finalize())。如果希望进行除释放存储空间之外的清理工作,还是得明确调用某个恰当的Java方法。
记住,无论是“垃圾回收”还是“终结”,都不保证一定会发生。如果Java虚拟机并未面临内存耗尽的情形,它是不会浪费时间去执行垃圾回收以恢复内存的。

终结条件

通常,不能指望finalize(),不过还有一个有趣的用法,它并不依赖于每次都对finalize()进行调用,这就是对象终结条件的验证。如果一个对象在被清理之前,存在没有被适当执行的部分,程序就存在很隐晦的缺陷,finalize()就可以用来最终发现这种情况。

Sign sign=new Sign(true);
sign.checkIn();  //每一个sign都应该被检查
new Sign(true);

System.gc();

由于程序员的错误,有一个Sign没有被检查,所以在Sign类中重写的finalize()方法会检查出错误的Sign对象。

垃圾回收器如何工作

垃圾回收器对于提高对象的创建速度,却具有明显的效果。Java从堆分配空间的速度,可以和其他语言从堆栈上分配空间的速度相媲美。
打个比方,C++的堆是一个院子,里面每个对象都负责管理自己的地盘,销毁后地盘必须加以重用。在某些Java虚拟机中,堆的实现截然不同:它更像一个传送带,每分配一个新对象,它就往前移动一格。这意味着对象在堆上的分配速度非常块。Java的“堆指针”只是简单地移动到尚未分配的区域,其效率比得上C++在堆栈上分配空间的效率。当然,实际过程中的簿记工作方面还有少量额外开销,但比不上查找可用空间开销大。
Java中的堆未必完全像传送带那样工作。要真是那样会导致频繁的内存页面调度——将其移进移出硬盘。页面调度会显著地影响性能。垃圾回收器的介入,将一面回收空间,一面使堆中的对象紧凑排列,这样“堆指针”就可以很容易移动到更靠近传送带的开始出,也就避免了页面错误。
引用记数是一种简单但是速度很慢的垃圾回收技术。每个对象含有一个引用计数器,当有引用连接至对象时,引用计数加1.当引用离开作用于或被置为null时,引用计数减1。当发现某个对象的引用计数为0时,就释放其占用的的空间。这个方法有个缺陷,如果对象之间存在循环引用,可能会出现“对象应该被回收,但引用计数却不为零”的情况。对垃圾回收器而言,定位这样的对象租所需的工作量极大。引用记数常用来说明垃圾收集的工作方式,但未被应用于任何一种Java虚拟机实现中。
在一些更快的模式中,它们依据的思想是:对任何“活”的对象,一定能最终追溯到其存活在堆栈或静态存储区之中的引用。由此,如果从堆栈或静态存储区开始,遍历所有的引用,就能找到所有“活”的对象,直到所形成的网络全部被访问。你所访问的对象必须都是“活”的,“交互自引用的对象组”根本不会被发现就被自动回收了(因为它们是“死”的,或者说不可达)。
在这种方式下,Java虚拟机将采用一种自适应的垃圾回收技术。有一种做法名为停止-复制(stop-and-copy)。显然这意味着,先暂停程序的运行(所以不属于后台回收模式),然后将所有存活的对象从当前堆复制到另一个堆,没有被复制的全部都是垃圾。当对象被复制到新堆时,它们是紧凑排列的,所有指向它的引用必须修正。位于堆或静态存储区的引用可以直接被修正,但还有些其他指向这些对象的引用,它们在遍历的过程中才能被找到。
对于这种所谓的“复制式回收期”而言,效率会降低。这有两个原因,首先得有两个堆,然后在这两个分离的堆之间来回倒腾,从而维护比实际多一倍的空间。某些Java虚拟机会按需从堆中分配几块较大的内存,复制动作在这些大内存块之间。
第二个问题在于复制。程序进入稳定状态之后,可能产生很少垃圾,甚至没有垃圾。尽管如此,复制式回收器仍会进行复制,这很浪费。一些Java虚拟机会检查:要是没有新垃圾,就转换到另一种工作模式(即“自适应”)。这种模式称为标记-清扫(mark-and-sweep)。一般而言,“标记-清扫”方式速度相当慢,但是在少量垃圾甚至没有垃圾时,它的速度就很快了。
“标记-清扫”所依据的思路同样是从堆栈和静态存储区出发,遍历所有引用,每当找到一个存货对象,就给对象一个标记。只有全部标记工作完成时,清理动作才开始,没有标记的对象将被释放,不会发生任何复制动作,所以剩下的堆空间是不连续的。
“停止-复制”的意思是这种垃圾回收动作不是在后台进行的,垃圾回收发生的同时,程序会被暂停。同样“标记-清扫”工作也必须在程序暂停的情况下才能进行。
Java虚拟机中,内存分配以较大的“块”为单位。如果对象较大,它会占用单独的块。每个块都用相应的代数(generation count)来记录它是否还存活。通常,如果块在某处被引用,其代数会增加,垃圾回收器将对上次回收动作之后新分配的块进行整理。垃圾回收器会定期进行完整的清理动作——大型对象仍然不会被复制(只是其代数会增加),内含小型对象的那些块则被复制并整理。Java虚拟机会进行监视,“自适应”的切换工作方式,可以称为“自适应的、分代的、停止-复制、标记-清扫”式垃圾回收器。
Java虚拟机中有许多附加技术提升速度。尤其是与加载器操作有关的,被称为“即时(Just-In-Time,JIT)”编译器技术。这种技术可以把程序全部或部分翻译成本地机器码(这本是Java虚拟机的工作)。当需要装载某个类(通常是在为该类创建第一个对象)时,编译器会先找到其.class文件,然后将该类的字节码装入内存。有两种方法可选。一种是就让即时编译器编译所有代码。但这样做有两个缺陷:这种加载动作散落在整个程序生命周期内,累加起来要花更多时间;并且会增长可执行代码的长度(字节码要比即时编译器展开后的本地机器码小很多),这将导致页面调度,从而降低程序速度。另一种做法称为惰性评估(lazy evaluation),意思是即时编译器只在必要的时候才编译代码。这样,从不会被执行的代码也许压根就不会被JIT所编译。

成员初始化

对于局部变量,未被初始化会报错,毕竟这有可能是程序员的疏忽。类的数据成员是基本类型的话,会被默认赋予初值,对象引用未初始化时,会获得一个特殊值null。

指定初始化

很直接的办法是在定义类成员变量的地方为其赋值(C++可不能这样做),也可以直接初始化引用。甚至可以通过调用某个方法来提供初值,但是一定要注意,方法里的参数一定是被初始化了的,因此,程序的正确性取决于初始化的顺序,而与其编译的方式无关。所以,非法顺序会被编译器给出“向前引用”的错误。

构造器初始化

无法阻止自动初始化的进行(先初始化默认值或指定值,后构造器初始化),编译器不会强制你一定要在构造器的某个地方或在使用它们之前对元素进行初始化——因为初始化早已得到了保证。

初始化顺序

在类的内部,变量定义的先后顺序决定了初始化的顺序。即使变量定义散布在方法定义之间,它们仍旧会在任何方法(包括构造器)被调用之前得到初始化(使用指定初始化的话)。如果构造器初始化了这个引用,那么之前引用的对象会被垃圾回收。

静态数据的初始化

无论创建多少个对象,静态数据都只占用一份存储区域。static关键字不能应用于局部变量,因为它只作用于域。如果一个域是静态的基本类型域,且也没有对它进行初始化,那么它就会获得基本类型的标准初值,如果是一个对象引用,那么它默认是null。
静态初始化只有在必要时刻才会进行。只有在第一个对象被创建(第一次访问静态数据)的时候,它们才会被初始化。此后静态对象不会再次进行初始化。
初始化的顺序是先静态对象(如果尚未因前门的对象创建过程而被初始化),而后是“非静态”对象(指定初始化优先于构造器初始化)。要执行Main()静态方法,则一定会先加载其所在的类,也就意味着会加载类中的静态域。
总结一下对象的创建过程:

  1. 即使没有显式地使用static关键字,构造器实际上也是静态方法。当首次创建类的对象时,或者类的静态方法/静态域首次被访问时,Java解释器必须查找类路径,以定位.class文件。
  2. 载入.class文件(创建一个Class对象),有关静态初始化的所有动作都会执行。因此静态初始化只在Class对象首次加载时进行一次。
  3. 当用new创建对象时,首先将在堆上为对象分配足够的存储空间。
  4. 这块存储空间会被清零,自动地将对象中所有的基本类型数据都设置成了默认值,而引用则被设置成了null。
  5. 执行所有出现于字段定义处的初始化动作。
  6. 执行构造器。

显式的静态初始化

Java中允许将多个静态初始化动作组织称一个特殊的“静态子句”(静态块)。实际只是一段跟在static关键字后面的代码。与其他静态初始化动作一样,这段代码仅执行一次:当首次生成这个类的一个对象时,或者首次访问属于那个类的静态数据成员(即便从未生成过那个类的对象)。静态块和指定初始化的先后顺序决定了类变量的静态初始化值

非静态实例初始化

实例初始化子句(初始化块)是在构造器之前执行的。使用“块”赋初始值无所谓顺序,但是要小心“向前引用”的错误,即不能引用定义在之后的变量。

数组的初始化

int[] a;int a[];都可以定义一个数组,编译器不允许直接指定数组的大小。现在拥有的只是对数组的一个引用(你已经为该引用分配了足够的存储空间),而且也没给数组对象本身分配任何空间。为了给数组创建相应的存储空间,必须写初始化表达式。对于数组,初始化动作可以出现在代码的任何地方,也可以用{ }括起来的值组成初始化。所有数组对象都有一个固定成员,可以获得包含多少个元素,但不能修改,就是length。C和C++会允许你访问超出边界,但是Java则不允许。可以使用new动态的创建数组大小,数组会自动初始化为类型的默认值。使用{ }初始化列表的最后一个逗号是可选的。

可变参数列表

方法中如f(int i,String... args)和C语言的可变参数列表(varagrs)是一样的。可以应用于参数个数或类型未知的场合。如上面的f()方法可以传入f(0); f(1,"one"); f(2,"one","two");都可以,0个参数传递也是可以的。使用Object可以传入任何类型的参数。在可变参数列表中可以使用任何类型的参数,包括基本类型。实际上,可变参数列表不依赖自动包装机制,使用的仍然是基本类型。如果在该列表中没有任何元素,那么转换成的数据的尺寸为0。你可以在单一的参数列表中将类型混合在一起,自动包装机制会有选择的将int参数提升为Ineteger。
可变参数列表使重载变得复杂,如果不使用参数调用方法时f(),编译器可能不知道该使用哪一个。可以在某个方法中添加一个非可变参数来解决该问题。最好只在重载方法的一个版本上使用可变参数列表,或者压根就不用。

枚举类型

enum关键字一般用作定义常量集。编译器会创建ordinal()方法表明某个enum常量的声明顺序,可以通过static的values()得到这些常量值顺序构成的数组。enum是一个类,有自己的方法,可以在switch语句中使用。

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

推荐阅读更多精彩内容

  • 1. Java基础部分 基础部分的顺序:基本语法,类相关的语法,内部类的语法,继承相关的语法,异常的语法,线程的语...
    子非鱼_t_阅读 31,531评论 18 399
  • (一)Java部分 1、列举出JAVA中6个比较常用的包【天威诚信面试题】 【参考答案】 java.lang;ja...
    独云阅读 7,048评论 0 62
  • 从三月份找实习到现在,面了一些公司,挂了不少,但最终还是拿到小米、百度、阿里、京东、新浪、CVTE、乐视家的研发岗...
    时芥蓝阅读 42,157评论 11 349
  • 最近,在读史蒂芬·柯维的《高效能人士的七个习惯》,发现一个有趣的现象,这本书里面讲的很多概念,有一种似曾相识的感觉...
    践行而生阅读 319评论 1 2
  • 当有字符串常量的时候,苹果推荐我们使用const,苹果推荐使用const。 区别: 编译时间:宏是预编译(编译之前...
    西瓜_fdce阅读 222评论 0 0