Java学习笔记---初始化与清理
“不安全”的编程方式已逐渐成为编程代价高昂的主因之一;初始化和清理正是涉及安全的两个问题;许多C程序的错误都源于程序员忘记初始化变量;清理也是一个特殊问题,当使用完一个元素时,它对你也不会有什么影响,很容易把它忘记,但如果不清理,这个元素占用的资源就会一直得不到释放,结果资源用尽。C语言引入了构造器的概念,在创建对象时被自动调用的特殊方法;Java中也采用了构造器,并额外提供了“垃圾回收器”。对于不用的内存资源,垃圾回收器能自动将其释放。
- 用构造器确保初始化
- 方法重载
- 默认构造器
- this关键字
- 清理:终结处理和垃圾回收
- 成员初始化
- 构造器初始化
- 数组初始化
- 枚举类型
1.用构造器确保初始化
通过构造器,类的设计者可以确保每个对象都会得到初始化。创建对象时,如果其类具有构造器,Java就会在用户有能力操作对象之前自动调用相应的构造器,从而确保了初始化的进行。
构造器名称与类名相同,原因:构造器若随便取名字,取的名字都可能与类的某个成员名称相冲突;调用构造器是编译器的责任,所以必须让编译器知道应该调用哪个方法。
创建对象时:new Rock()。将会为对象分配存储空间,并调用相应的构造器。这就确保了在你能操作对象之前,他已经被恰当地初始化了。
注:由于构造器的名称必须与类名完全相同,所以每个方法“首字母小写”的编码风格并不适用于构造器。
不接受任何参数的构造器叫做默认构造器,术语:无参构造器
构造器也可以带有参数:
有了构造器形式参数,就可以在初始化对象时提供实际参数;例如,假设类Tree有一个构造器,它接受了一个整形变量来表示树的高度,就可以这样构建一个Tree对象:Tree t = new Tree(12);如果,Tree(int)是唯一的构造器,那么编译器将不会允许你以其他任何方式创建Tree对象。
构造器有助于减少错误,并使代码更易于阅读;从概念上讲,“初始化”和“创建”是彼此独立的,然而在java的对象创建中,却找不到对initialize()方法的明确调用。在Java中,“初始化”与“创建”捆绑在一起,两者不能分离。
构造器是一种特殊的类型的方法,因为它没有返回值。这与返回值为空(void)不同,对于空返回值,尽管方法本身不会自动返回什么,但仍可选择让它返回别的东西。构造器则不会返回任何东西,(new表达式却是返回了对新建对象的引用,但构造器本身并没有返回任何值)。假设构造器具有返回值,并且允许人们自行选择返回类型,那么势必得让编译器只改该如何处理此返回值。
2.方法重载
当创建一个对象时,也就给此对象分配到的存储空间取了一个名字。所谓方法则是给某个动作取的名字。通过使用名字,可以引用所有的对象和方法。
假设想要以多种方式创建一个对象该怎么办?假设需要构建一个类,既可以用标准方式进行初始化,也可以从文件里读取信息来进行初始化。这就需要两个构造器:一个默认构造器,另一个取字符串作为形式参数--该字符串表示初始化对象所需的文件名称。由于都是构造器,所以它们必须有相同的名字,即类名。为了让方法名相同而形式参数不同的构造器同时存在,必须用到方法重载。尽管方法重载是构造器所必需的,但它亦可应用于其他方法,且用法同样方便。
结果:
创建Tree对象的时候,既可以不含参数,也可以用树的高度当参数。前者表示一颗树苗,后者表示已有一定高度的树木。要支持这种创建方式:得有一个默认构造器和一个采用现有高度作为参数的构造器。
要是对明显相同概念使用了不同的名字,那一定让人很纳闷,好在有了方法重载,可以为两者使用相同的名字。
区分重载方法:
规则:每个重载的方法都必须有一个独一无二的参数类型列表。因为唯一标志一个方法的是:方法名+参数列表;甚至参数的顺序不同也足以区分两个方法,但最好不要这么做,这么做会使代码难以维护。
这里两个f()方法虽然声明了相同的参数,但顺序不同,因此得以区分。
涉及基本类型的重载:
基本类型能从一个“较小”的类型自动提升至一个“较大”的类型,此过程一旦牵涉到重载,可能会造成一些混淆。
常值5被当做int值处理,所以如果有某个重载方法接受int型参数,它就会被调用。至于其他情况,如果传入的数据类型(实际参数类型)小于方法中声明的形式参数类型,实际数据类型就会提升。char型有所不同,如果无法找到恰好接受char参数的方法,就会把char直接提升至int型。
那么,如果传入的实际参数大于重载方法声明的形式参数,会出现什么情况呢?
在这里,方法接受较小的基本类型作为参数。如果传入的实际参数较大,就得通过类型转换来执行窄化转换。如果不这么做,编译器就会报错。
以返回值区分重载方法:
这两个方法虽然具有同样的名字和形式参数,但却很容易区分它们。
只要编译器可以根据语境明确判断出语义,比如在int x = f()中,那么的确可以根据此区分重载方法。不过,有时并不关心方法的返回值,想要的是方法调用的其他效果(为了副作用而调用),这时可能会忽略其返回值。所以,像下卖弄这样调用方法:
f();
此时,Java该如何才能判断调用哪一个f()呢?别人该如何理解这种代码?因此,根据方法的返回值来区分重载方法是行不通的。
3.默认构造器
默认构造器又名无参构造器,是没有形式参数的--它的作用是创建一个“默认对象”。如果你写的类中没有构造器,则编译器会自动帮你创建一个默认构造器。
new Bird()创建了一个新对象,并调用默认构造器--即使没有明确定义它。若没有默认构造器,就没有方法可调用,就无法创建对象。但是,如果已经定义了一个构造器(无论是否有参),编译器就不会帮你自动帮你创建默认构造器。
如果这样写:new Bird2();编译器就会报错:没有找到匹配的构造器。
4.this关键字
如果有同一类型的两个对象,分别是a和b。如何才能让这两个对象都能调用peel()方法?
如果只有一个peel()方法,它如何知道是被a调用还是b调用呢?
为了能用简便、面向对象的语法来编写代码---即“发送消息给对象”,编译器做了一些幕后工作。它暗自把“所操作对象的引用”作为第一个参数传递给peel()。所以上述两个方法的调用就变成了这样:
这是内部的表示形式,我们并不能这样书写代码,并试图通过编译,但这种写法的确能够帮你了解实际所发生的事情。
假设希望在方法的内部获得对当前对象的引用。由于这个引用是由编译器“偷偷”传入的,所以没有标示符可用。但是,为此有个专门的关键字:this关键字只能在方法内部使用,表示对“调用方法的那个对象”的引用。this的用法和其他对象引用并无不同。但要注意,如果方法内部调用同一个类的另一个方法,就不必使用this,直接调用即可。当前方法中的this引用会自动应用于同一类中的其他方法。所以可以这样写代码:
在pit()内部,你可以写this.pick(),但没有必要,编译器能自动给你添加。只有当需要明确指出对当前对象的引用时,才需要使用this关键字。例如:当需要返回对当前对象的应用时,就常常在return语句中这样写:
由于increment()通过this关键字返回了对当前对象的引用,所以很容易在一条语句里对同一个对象执行多次操作。
分析操作流程:x.increment():i=1,因为return this返回x;x.increment().increment()相当于x.increment():i=2,返回x;x.increment().increment().increment()相当有x.increment(),此时i=3,且返回x,最后调用x.print()打印x的真实值:3。
this关键字对于将当前对象传递给其他方法也很有用:
Apple需要调用Peeler.peel()方法,它是一个外部的工具方法,将执行由于某种原因而必须放在Apple外部的操作(也许是因为该外部方法要应用于许多不同的类,而你却不想重复这些代码)。为了将自身传递给外部方法,Apple必须使用this关键字。
分析:return Peeler.peel(this)是将apple传递给Peeler的peel()方法,并返回其自身:apple。
在构造器中调用构造器:
可能为一个类写了多个构造器,有时可能想在一个构造器中调用另一个构造器,以避免重复代码。可用this关键字做到这一点。
通常写this的时候,都是指“这个对象”或“当前对象”,而且它本身表示对当前对象的引用。在构造器中,如果为this添加了参数列表,那么就有了不同的含义。这将产生对符合此参数列表的某个构造器的明确调用;这样,调用其他构造器就有了直接的途径:
构造器Flower(String s,int petals)表明:尽管可以调用一个构造器,但却不能调用两个。此外,必须将构造器调用置于最起始处,否则编译会报错。
这里展示了this的另一种用法,由于参数s的名称和数据成员s的名字相同,所以会产生歧义,必须将构造器调用置于最起始处,否则编译器会报错。
除构造器之外,编译器禁止在其他任何方法中使用构造器。
static的含义:
static就是没有this的方法。在static方法的内部不能调用非静态的方法,反过来倒是可以的。原因是因为,在没有创建任何对象的前提下就可以使用类名直接调用static方法,如果static方法内部包含类中非静态方法,显然这是矛盾的;static方法很像全局方法,Java中禁止使用全局方法,但你在类中置入static方法就可以访问其他static方法和static域。
有人认为static方法不是“面向对象”的,因为它们的确具有全局函数的语义;使用static方法时,由于不存在this,所以不是通过“向对象发送消息”的方式来完成的。
5.清理:终结处理和垃圾回收
使用程序库时,把一个对象用完后就“弃之不顾”的做法并非总是安全的。Java有垃圾回收器负责回收无用对象占据的内存资源。但是,垃圾回收器只会释放那些经由new分配的内存,对于不是使用new获得的“特殊”内存区域,垃圾回收器并不知道该如何释放该对象的这块“特殊”内存。
为了应对这种情况,Java允许在类中定义一个名为finalize()的方法。工作原理:一旦垃圾回收器准备好释放对象占用的存储空间,将首先调用其finalize()方法,并且在下一次垃圾回收动作发生时,才会真正回收对象占用的内存。所以,如果使用finalize(),就能在垃圾回收时刻做些重要的清理工作。
finalize()不是C++中的析构函数,在C++中,对象一定会被销毁;而Java里的对象却并非总是被垃圾回收。
1).对象可能不被垃圾回收
2).垃圾回收并不等于“析构”
意味着不再需要某个对象之前,如果必须执行某些动作,那么你得自己去做。Java并未提供“析构函数”或相似的概念,要做类似的清理工作,必须自己动手创建一个执行清理工作的普通方法。例子:假设某个对象在创建过程中将自己绘制到屏幕上,如果不是明确地从屏幕上将其擦除,他可能永远得不到清理。如果在finalize()里加入某种擦除功能,当“垃圾回收”发生时,finalize()得到了调用,图像就会被擦除。要是“垃圾回收”没有发生,图像就会一直保留下来。
但是,只要程序没有频临存储空间用完的那一刻,对象占用的空间就总也得不到释放。如果程序执行结束,并且垃圾回收器一直都没有释放你创建的任何对象的存储空间,则随着程序的退出,那些资源也会全部交还给操作系统。这个策略是恰当的,因为垃圾回收本身也有开销,要是不使用它,那就不用支付这部分开销了。
总结:不该将finalize()作为通用的清理方法。
finalize()的用途:
3).垃圾回收只与内存有关
使用垃圾回收器的唯一原因是为了回收程序不再使用的内存。所以对于与垃圾回收有关的任何行为来说(尤其是finalize()方法),它们必须同内存及其回收有关。
无论对象如何创建的,垃圾回收器都会负责释放对象占据的所有内存。这将对finalize()的需求限制到一种特殊情况,即通过某种创建对象以外的方式为对象分配了存储空间。但是,Java中一切皆对象,这中特殊情况是怎么回事?
之所以要有finalize(),是由于在分配内存时可能采用了类似C语言中的做法,而非Java代码的方式。
必须实施清理:
在C++中创建了一个局部对象(也就是在堆栈上创建,这在Java中行不通),此时的销毁动作发生在以“右花括号”为边界的,此对象作用域的末尾处。如果此对象是用new创建的(类似于Java),那么当程序员调用C++的delete操作符时(Java没有这个命令),就会调用相应的析构函数。如果程序员忘记调用delete,那么永远不会调用析构函数,这样就会出现内存泄露,对象的其他部分也不会得到清理。这种缺陷很难跟踪,这也是让C++程序员转向Java的一个主要因素。
Java不允许创建局部对象,必须使用new创建对象。在Java中也没有释放对象的delete,因为垃圾回收器会帮你释放存储空间。Java没有析构函数。然而,垃圾回收器的存在并不能完全替代析构函数。(而且绝对不能直接调用finalize(),所以这也不是一种解决方案)。如果希望进行出释放存储空间之外的清理工作,还是得明确调用某个恰当的方法。这就等同于使用了析构函数,只是没它方便。
注意:无论是“垃圾回收”还是“终结”,都不保证一定会发生。如果java虚拟机(JVM)并未面临内存耗尽的情形,它是不会浪费时间去执行垃圾回收以恢复内存的。
终结条件:
通常,不能指望finalize(),必须创建其他的清理方法,并且明确地调用他们。看来。finalize()只能应用于很难用到的一些晦涩难懂的用法里了。但是,finalize()有一个有趣的用法,它并不依赖于每次都要对finalize()进行调用,这就是对象终结条件的验证。
当对某个对象不再感兴趣--也就是它可以被清理了,这个对象应该处于某种状态,使它占用的内存可以被安全地释放。例:一个对象打开了一个文件,在对象被回收前程序员应该关闭这个文件。只要对象中存在没有被适当清理地部分,程序就存在很隐晦地缺陷。finalize()可以用来最终发现这种情况---尽管它并不总是会被调用。如果某次finalize()的动作使得缺陷被发现,那么就可据此找出问题所在。
finalize()用法用例:
本例的终结条件是:所有的Book对象在被当作垃圾回收前都应该被签入(check in)。但在main()方法中,由于程序员的错误,有一本书未被签入。要是没有finalize()来验证终结条件,将很难发现这样的错误。
System.gc()用于强制进行终结动作。即使不这么做,通过重复地执行程序(假设程序将分配大量的存储空间而导致垃圾回收动作的执行),最终也能找出错误的Book对象。
你应该总是假设基类版本的finalize()也要做某些终要的事请,因此要使用super来调用它,就像在Book.finalize()中看到的那样。在本例中,它被注释掉了,因为它需要进行异常处。
垃圾回收器如何工作:
java之前的语言,在堆上分配对象的代价非常昂贵,但是,垃圾回收器对于提高对象的创建速度,却具有明显的效果---存储空间的释放会影响存储空间的分配,虽然很奇怪,但这确实是Java虚拟机的工作方式。
C++里,每个对象都负责管理自己的地盘,一段时间后,对象可能被销毁,但地盘必须加以重用。在某些Java虚拟机中,堆的实现截然不同:它更像一个传送带,每分配一个新对象,它就往前移动一格,这意味着对象存储空间分配速度非常快。Java的“堆指针”只是简单地移动到尚未分配的区域,其效率比得上C++在堆栈上分配空间的效率。当然,实际过程中在簿记工作方面还有少量额外开销,但比不上查找可用空间开销大。
Java若真像传送带那样工作,势必会导致频繁的内存页面调度---将其移进移出硬盘,因此会显得需要拥有比实际需要更多的内存。页面调度会显著地影响性能,最终,在创建了足够多的对象之后,内存资源将耗尽。其中的秘密在于垃圾回收器的介入。当它在工作时,将一面回收空间,一面使堆中的对象紧凑排列,这样“堆指针”就可以很容易移动到更靠近传送带的开始处,也就尽量避免了页面错误。通过垃圾回收对对象重新排列,实现了一种高速的、有无限空间可供分配的堆模型。
引用计数是一种简单但速度很慢的垃圾回收基数。每个对象都含有一个引用计数器,当有引用连接至对象时,引用计数加1。当引用离开作用域或被置为null时,引用计数减1。虽然管理引用计数的开销不大,但这项开销在整个程序生命周期中将持续发生。垃圾回收器会在含有全部对象的列表上遍历,当发现某个对象的引用计数为0时,就释放其占用的空间(一般引用计数模式会在计数值变为0时立即释放对象)。
在一些更快的模式中,垃圾回收器并非基于引用计数技术。依据思想:对任何“活”的对象,一定能最终追溯其存活的堆栈或静态存储区之中的引用。这个引用链条可能会穿过数个对象层次。由此,如果从堆栈和静态存储区开始,遍历所有的引用,就能找到所有“活”的对象。对于发现的每个引用,必须追踪它所引用的对象,然后是此对象包含的所有引用,如此反复进行,直到“根源于堆栈和静态存储区的引用”所形成的网络全部被访问为止。你所访问过的对象必须都是“活”的。注意,这就解决了“交互自引用的对象组”的问题--这种现象根本不会被发现,因此也就被自动回收了。
Java虚拟机采用一种自适应的垃圾回收技术。如何处理找到的存活对象取决于不同的虚拟机实现。有一种做法名为停止-复制。显然,这意味着,先暂停程序的运行(所以它不属于后台回收模式),然后将所有存活的对象从当前堆复制到另一个堆,没有被复制的全部都是垃圾。当对象被复制到新堆时,它们是一个挨着一个的,所以新堆保持紧凑排列,然后就可以按前述方法简单、直接地分配新空间了。
当把对象从一处搬到 另一处时,所有指向它的那些引用都必须修正。位于堆或静态存储区的引用可以直接被修正,但可能还有其他指向这些对象的引用,它们在遍历的过程中才能被找到(可以想象成有个表格,将旧地址映射至新地址)。
对于这种复制式回收器而言,效率会降低,这有两个原因。首先,得有两个堆,然后得在这两个分离的堆之间来回倒腾,从而维护比实际需要多一倍的空间。某些Java虚拟机对此问题的处理方式是,按需从堆中分配几块大的内存,复制动作发生在这些大块内存之间。
第二个问题在于复制,程序进入稳定状态之后,可能只会产生少量垃圾,甚至没有垃圾。尽管如此,复制式回收期仍会将所有内幕才能自一处复制到另一处。为避免这种情况,一些Java虚拟机会进行检查:要是没有垃圾产生,就会转换到另一种工作模式("即自适应")。这种模式称为标记-清扫。对一般用途而言,“标记-清扫”方式速度相当慢,但是当你知道只会产生少量垃圾甚至不会产生垃圾时,他的速度就很快。
“标记-清扫”所依据的思路同样是从堆栈和静态存储区出发,遍历所有的引用,进而找出所有存活的对象,每当它找到一个存活对象,就会给对象设一个标记,这个过程中不会回收任何对象。只有全部标记工作完成的时候,清理动作才会开始。在清理过程中,没有标记的对象将被释放,不会发生任何复制动作。所以剩下的堆空间是不连续的,垃圾回收器要是希望得到连续空间的话,就得重新整理剩下的对象。
“停止-复制”的意思是这种垃圾回收动作不是在后台进行的,相反,垃圾回收动作发生的同时,程序降会被暂停。
内存分配以较大的“块”为单位,如果对象较大,他会占用单独的块。严格来说,“停止-复制”要求在释放旧有对象之前,必须先把所有存活对象从旧堆复制到新堆,这将导致大量内存复制行为。有了块之后,垃圾回收器在回收的时候就可以往废弃的块里拷贝对象。每个块都用相应的代数来记录它是否还存活。通常,如果块在某处被引用,其代数会增加;垃圾回收器将对上次回收动作之后新分配的块进行整理。这对处理大量短命的临时对象很有帮助。垃圾回收器会定期进行完成的清理动作--大型对象仍然不会被复制,内含小型对象的那些块则被复制并整理。Java虚拟机会进行监视,如果所有对象都很稳定,垃圾回收器的效率降低的话,就切换到“标记-清扫”方式;同样,Java虚拟机会跟踪“标记-清扫”的效果,要是堆空间出现很多碎片,就会切换回“停止-复制”方式。这就是“自适应”技术。
6.成员初始化
Java尽力保证:所有变量在使用前都能得到恰当的初始化。对于方法的局部变量,Java以编译时错误的形式来贯彻这种保证。
对于:
会得到一条出错消息,告诉你i可能尚未初始化。前面说过,编译器也可以为i默认赋一个初始值,但是未初始化的局部变量更有可能是程序员的疏忽,所以采用默认值反而会掩盖这种失误。
要是类的数据成员(即字段)是基本类型,情况就会变得有些不同。正如在“一切皆对象”中看到的,类的每个基本类型数据成员都会有一个初始值。例如:
尽管数据成员初值没有给出,但他们确实有初值。
在类里定义一个对象引用,如果不将其初始化,此引用就会获得一个特殊值null。
指定初始化:
为某个变量赋初值,有两种方法:
1).定义类成员变量的地方赋值
也可以使用相同的方法初始化非基本类型的对象:
如果没有为d指定初始值就尝试使用它,就会出现运行时错误。
2).调用方法提供初值:
该方法也可以带有参数,但这些参数必须是已经初始化了的:
这里g(i)中的i已经被初始化了,i=f();
下面是一个错误例子:
该程序取决于初始化的顺序,与其编译方式无关,所以编译器适当地对“向前引用”发出了警告。
这样初始化的限制:类InitialValues的每个对象都会具有相同的初始值,有时这是所希望的,但有时需要更大的灵活性。
7.构造器初始化
可用构造器来进行初始化。在运行时刻,可以调用方法或执行某些动作来确定初值,这为编程带来了更大的灵活性。但是,自动初始化的进行是没法阻止的,它将在构造器被调用之前发生。因此:
这种情况下,i首先会被置为0,然后变成7。编译器不会强制你一定要在构造器的某个地方或在使用它们之前对元素进行初始化---因为初始化早已得到了保证。
初始化顺序:
在类的内部,变量定义的先后顺序决定了初始化的顺序。即使变量定义散布于方法定义之间,它们仍旧会在任何方法(包括构造器)被调用之前得到初始化。例如:
这里,对Window对象的创建虽然分布在方法和构造器的各处,结果足以证明它们全都会在调用构造器或其他方法之前得到初始化。此外,w3在构造器内再次被初始化。
w3这个引用会被初始化两次:一次在调用构造器之前,一次在调用期间(其中,第一次引用的对象将被丢弃,并作为垃圾回收)
静态数据的初始化:
无论创建多少个对象,静态数据都只占用一份存储区域。static关键字不能应用于局部变量,因此它只能作用于域。如果一个域是静态的基本类型域,且也没有对他进行初始化,那么它就会获得基本类型的标准初值;如果它是一个对象引用,那么它的默认初始化值就是null(这个时候别尝试使用它,会出错)。
如果想在定义处进行初始化,采取的方法与非静态数据没什么不同。
例子:(静态存储区域何时初始化)
结果为:分析:
由上图可见,静态初始化只有在必要时刻(创建了类,或者使用类名对某个static方法进行调用)才会进行。如果不创建Tabel对象,也不引用Table.b1或Table.b2,那么静态的Bowel b1和b2永远都不会创建。只有在第一个Table对象被创建(或者第一次访问静态数据)的时候,它们才会被初始化。此后,静态对象不会再此被初始化。
初始化的顺序是先静态对象(如果它们尚未因前面的对象创建过程而被初始化),而后是“非静态”对象。比如上面程序:要执行main()(静态方法),必须加载StaticInitialization类,然后其静态域table和cupboard被初始化,这将导致它们对应的类也被加载,并且由于它们也都包含静态的Bowel对象,因此Bowel随后也被加载。于是,所有类都在main()开始之前就都被加载了。
总结:
1).即使没有显式地使用static关键字,构造器实际上也是static方法。因此,当首次创建类型为Dog的对象时,或者Dog类的静态方法/静态域首次被访问时,Java解释器必须查找类路径,以定位Dog.class文件。
2).然后载入Dog.class,有关静态初始化的所有动作都会执行。因此,静态初始化只在class对象首次加载的时候进行一次。
3).当用new Dog()创建对象的时候,首先将在堆上为Dog对象分配足够的存储空间
4).这块存储空间会被清零,这就自动地将Dog对象中的所有基本类型数据都设置成了默认值,而引用则被设置成了null
5).执行所有出现于字段定义处的初始化动作
6).执行构造器。
显式的静态初始化:
Java允许将多个静态初始化动作组织成一个特殊的“静态子块”,如下:
这里i=47,实际上只是一段跟在static关键字后面的代码,而并非是一个函数。与其他静态初始化动作一样,这段代码仅执行一次:当首次生成这个类的一个对象时,或者首次访问属于那个类的静态数据成员时。
无论是通过标为(1)的那行代码访问静态的cup1对象,还是把标为(1)的行注释掉,让它去运行标为(2)的那行代码,Cups的静态初始化动作都会得到执行。如果把标为(1)和(2)的行同时注释掉,Cups的静态初始化动作就不会进行。此外,静态初始化动作只会进行一次。
这里只要求执行cup1,但cup1和cup2初始化过程包含在同一个static块中,所以会同时执行。
非静态实例初始化:
Java中存在实例初始化的类似语法,用来初始化每一个对象的非静态变量。如:
看起来与静态初始化子句一模一样,只不过少了static关键字。这种语法对于支持“匿名内部类”的初始化是必须的,需要注意,这种非静态实例化使得无论你调用了哪个显式构造器,这些操作都会发生。实例化子句是在两个构造器之前执行的。
总结:初始化顺序
1).类中字段(有静态先静态,且只执行一次;然后是非静态)
2).类构造器方法
8.数组初始化
数组是相同类型的、用一个标识符封装到一起的一个对象序列或基本类型数据序列。数组是通过方括号下标操作符[]来定义和使用的。定义一个数组,只需在类型名后加上一对空方括号即可:
方括号也可以置于标识符后面:
这是C++程序员的习惯,前一种更为合理,毕竟它表明类型是"一个int型数组".
编译器不允许指定数组大小,现在拥有的只是对数组的一个引用,而且也没给数组对象本身分配任何空间。为了给数组创建相应的存储空间,必须写初始化表达式。对于数组,初始化动作可以出现在代码的任何地方,但也可以使用一种特殊的初始化表达式,它必须在创建数组的地方出现。这种特殊的初始化是由一对花括号括起来的值组成的。在这种情况下,存储空间的分配(相当于new)将由编译器负责。例如:
那么,为什么还要在没有数组的时候定义一个数组引用呢?
在Java中可以将一个数组赋值给另一个数组,可以这样:
其实真正做的只是复制了一个引用,就像下面演示的那样:
虽然这里只对a2进行操作,但打印a1的结果发现已经发生了改变。
所有数组都有一个固有成员,可以通过它获知数组内包含了多少个元素,但不能对其进行修改。这个成员是:length。Java数组从第0个元素开始,能使用的最大下标数是length-1。超出这个边界便会报错。
如果不能确定数组里需要多少个元素,该如何做?可以直接用new在数组里创建元素。尽管创建的是基本类型属猪,new仍然可以工作(但是,不能用new创建单个的基本类型数据)。即int a = new int(1)是不允许的
数组大小通过Random.nextInt()方法随机决定,该方法返回0到输入参数之间的一个值。这表明数组的创建确实是在运行时刻进行的。该程序表明,数组元素中的基本数据类型值会自动初始化成空值(对于数字和字符,就是0;对于布尔型,就是false)
Arrays.toString()方法属于java.util标准类库,它将产生一维数组的可打印版本。
在本例中,数组也可以在定义的同时进行初始化:
如果创建了一个非基本类型的数组,那么就创建了一个引用数组。以整形的Integer为例,它是一个类而不是基本类型:
这里,即使使用new创建数组之后:Integer[] a = new Integer[rand.nextInt(20)],这还是只是一个引用数组,并且直到通过创建新的Integer对象,并把对象赋值给引用,初始化进程才算结束:a[i]=rand.nextInt(500);
前面提到,编译器不允许指定数组的大小,那么Integer[] a = new Integer[rand.nextInt(20)]是否在给数组指定大小,其实是有区别的,这里使用new Integer是在创建对象。指定数组大小是:int a[20],这在Java中是不允许的。
如果忘记了创建对象,并且师徒使用数组中的空引用,就会在 运行时产生异常。
另一种初始化数组方式是:使用花括号括起来的列表来初始化数组。
初始化列表的最后一个逗号是可选的。
虽然第一种形式很有用,但是它更加受限制,因为它只能用于数组被定义之处(Integer[] a = {})。第二种和第三种形式可以在任何地方使用,甚至方法调用的内部也可以使用。
创建一个String对象数组,将其传递给另一个main()方法,以提供参数,用来替换传递给该main()方法的命令行参数。
为other.main()的参数而创建的数组是在方法调用处创建的,因此你甚至可以在调用时提供可替换参数。
可变参数列表:
可变参数列表可以应用于参数个数和类型未知的场合。由于所有的类都直接或间接继承于Object类,所以可以创建以Object数组为参数的方法,并像下面这样调用:
可以看到print()方法使用Object数组作为参数,然后使用foreach语法遍历数组,打印每个对象。
通过数组语法Object[] args,可以获得可变参数列表。Java SE5后加进了可变参数列表:
结果:
有了可变参数,就不需要显式地编写数组语法了,当指定参数时,编译器实际上会自动为其填充数组。获取的仍然是一个数组,这就是为什么print()可以使用foreach来迭代该数组。
这不仅仅只是从元素列表到数组的自动转换,程序倒数第二行,一个Integer数组(通过使用自动包装而创建)被转型为一个Object数组,并且传递给了printArray()。很明显,编译器会发现它已经不是一个数组了,所以不会在其上执行任何转换。因此,如果你有一组事物,可以把它们当做列表传递,而如果你已经有了一个数组,该方法可以把它们当做可变参数列表来接受。
程序的最后一行表明将0个参数传递给可变参数列表是可行的,当具有可选的尾随参数时,这一特性就会很有用:
该程序展示了如何使用具有Object之外类型的可变参数列表,这里所有的可变参数都必须是String。在可变参数列表中可以使用任何类型的参数,包括基本类型。
下面展示可变参数列表为数组的清醒,并且如果在该列表中没有任何元素,那么转变成的数据的尺寸为0:
结果为:
getClass()方法属于Object的一部分,它将产生对象的类,并且在打印该类时,可以看到表示该类类型的编码字符串。
前导的“[”表示这是一个后面紧随的类型的数组,而紧随的“I”表示基本类型int。
最后一行创建了一个int数组,并打印了其类型,验证了使用可变参数列表不依赖于自动包装机制而实际上使用的是基本类型。
然而,可变参数列表与自动包装机制可以和谐共处,例如:
可以在单一的参数列表中将类型混合在一起,而自动包装机制将有选择地将int参数提升为Integer。
可变参数列表使得重载过程变得复杂了:
结果为:,当输入参数明确是long行时才调用f(Long... args).
在每一种情况中,编译器都会使用自动包装机制来匹配重载的方法,然后调用最明确的匹配的方法。
但是,在不使用参数调用f()时,编译器就无法知道应该调用哪一个方法了。
解决方法,在某个方法中增加一个非可变参数来解决该问题:
这个程序编译是会报错的,{CompileTimeError}注释标签把该文件排除在了本书的Ant构建之外。如果你手动编译它,就会得到下面的错误消息:
原因就是,方法重载如果是可变参数,则要求每个方法都是可变参数;如果可变参数中含有一个非可变参数,则要求都有一个非可变参数,所以解决方法就是:给这两个方法都添加一个非可变参数:
9.枚举类型
在Java SE5以后添加了一个看似很小的特性,即enum关键字,应用于需要群组并使用枚举类型集时,可以方便地处理。
使用过程:
首先需要创建一个整形常量集,但是这些枚举值并不会必然地将其自身的取值限制在这个常量集的范围之内,因此它们显得更有风险,且难以使用。
例子:
这里创建了一个名为Spiciness的枚举类型,它具有5个具名值,由于枚举类型的实例是常量,因此按照命名惯例它们都用大写字母表示(如果名字中存在多个单词,用下划线将它们分开):
为了使用enum,需要创建一个该类型的引用,并将其赋值给某个实例:
在你创建enum时,编译器会自动添加一些有用的特性。例如,它会创建toString()方法,方便显示某个enum实例的名字;编译器还会创建ordinal()方法,用来表示某个特定enum常量的声明顺序;编译器还会创建static values()方法,用来按照enum常量的声明顺序,产生由这些常量值构成的数组。
尽管enum看起来是一种新的数据类型,但是这个关键字只是为enum生成对应的类时,产生了某些编译器行为,在很大程度上,可以将enum当做其他任何类来处理。事实上,enum确实是类,并且具有自己的方法。
enum有一个特别实用的特性,即它可以在switch语句内实用:
由于switch是要在有限的可能值集合中进行选择,因此它与enum正是绝佳的组合,enum的名字是如何能够倍加清楚地表明程序意欲何为的。
大体上可以将enum用作另外一种创建数据类型的方式,然后直接将所得到的类型拿来使用。