Java初始化和清理

前言

科学的不朽荣誉,在于它通过对人类心灵的作用,克服了人们在自己面前和在自然界面前的不安全感。—爱因斯坦
"不安全"的编程是造成编程代价昂贵的罪魁祸首之一。有两个安全性问题:初始化和清理。Java 除了沿用了C++构造器的概念,另外还使用了垃圾收集器(Garbage Collector, GC)自动回收不再被使用的对象所占的资源。
构造器保证初始化 在 Java 中,类的设计者通过构造器保证每个对象的初始化。如果一个类有构造器,那么 Java 会在用户使用对象之前(自动调用对象的构造器方法,从而保证初始化。一个无参构造器就是不接收参数的构造器,用来创建一个"默认的对象"。如果你创建一个类,类中没有构造器,那么编译器就会自动为你创建一个无参构造器。Java 构造器命名方式:构造器名称与类名相同。
class User {
//显式的无参构造 User() { // 这是一个构造器 System.out.print("User "); } public static void main(String[] args) { new User(); }

}
public class User {
public static void main(String[] args) { //隐式无参构造 new User(); }

【Java初始化和清理】}
输出:
User
构造器方法也可以传入参数来定义如何创建一个对象。
class User {
User(String name, int age) { // 这是一个带参构造器 System.out.print("name:"+name+", age:" + age); } public static void main(String[] args) { new User("码农洞见", 30); }

}
输出:
name:码农洞见, age:30
方法重载 将人类语言细微的差别映射到编程语言中会产生一个问题。通常,相同的词可以表达多种不同的含义——它们被"重载"了。方法是行为的命名。你通过名字指代所有的对象,属性和方法。良好命名的系统易于理解和修改。就好比写散文——目的是与读者沟通。在 Java中,还有一个因素也促使了必须使用方法重载:构造器。因为构造器方法名肯定是与类名相同,所以一个类中只会有一个构造器名。那么你怎么通过不同的方式创建一个对象呢?如果两个方法命名相同,每个被重载的方法必须有独一无二的参数列表。基本类型可以自动从较小的类型转型为较大的类型。如果传入的参数类型大于方法期望接收的参数类型,你必须首先做下转换,如果你不做的话,编译器就会报错。
成员初始化
Java 尽量保证所有变量在使用前都能得到恰当的初始化。类的每个基本类型数据成员保证都会有一个初始值。
public class User {
String name = "码农洞见"; int age; User() { // 这是一个带参构造器 System.out.print("name:"+name+", age:" + age); } public static void main(String[] args) { new User(); }

}
输出:
name:码农洞见, age:0
怎么给一个变量赋初值呢?一种很直接的方法是在定义类成员变量的地方为其赋值。
public class User {
String name = "码农洞见"; int age = 30; User() { // 这是一个带参构造器 System.out.print("name:"+name+", age:" + age); } public static void main(String[] args) { new User(); }

}
输出:
name:码农洞见, age:30
构造器初始化 可以用构造器进行初始化,这种方式给了你更大的灵活性,因为你可以在运行时调用方法进行初始化。但是,这无法阻止自动初始化的进行,它会在构造器被调用之前发生。因此,如果使用如下代码:
public class User {
int age; User() { // 这是一个带参构造器 age = 30; } public static void main(String[] args) { User user = new User(); System.out.println(user.age); }

}
输出:
30
age 首先会被初始化为 0,然后变为 30。对于所有的基本类型和引用,包括在定义时已明确指定初值的变量,这种情况都是成立的。因此,编译器不会强制你一定要在构造器的某个地方或在使用它们之前初始化元素——初始化早已得到了保证。
初始化的顺序 在类中变量定义的顺序决定了它们初始化的顺序。即使变量定义散布在方法定义之间,它们仍会在任何方法(包括构造器)被调用之前得到初始化。
垃圾回收器 程序员都了解初始化的重要性,但通常会忽略清理的重要性。毕竟,谁会去清理一个 int 呢?但是使用完一个对象就不管它并非总是安全的。Java 中有垃圾回收器回收无用对象占用的内存。但现在考虑一种特殊情况:你创建的对象不是通过 new 来分配内存的,而垃圾回收器只知道如何释放用 new 创建的对象的内存,所以它不知道如何回收不是 new 分配的内存。为了处理这种情况,Java 允许在类中定义一个名为 finalize() 的方法。它的工作原理"假定"是这样的:当垃圾回收器准备回收对象的内存时,首先会调用其 finalize() 方法,并在下一轮的垃圾回收动作发生时,才会真正回收对象占用的内存。也就是说,使用垃圾回收的唯一原因就是为了回收程序不再使用的内存。所以对于与垃圾回收有关的任何行为来说(尤其是 finalize() 方法),它们也必须同内存及其回收有关。
垃圾回收器工作原理 停止-复制(stop-and-copy)
顾名思义,这需要先暂停程序的运行(不属于后台回收模式),然后将所有存活的对象从当前堆复制到另一个堆,没有复制的就是需要被垃圾回收的。另外,当对象被复制到新堆时,它们是一个挨着一个紧凑排列,然后就可以按照前面描述的那样简单、直接地分配新空间了。当对象从一处复制到另一处,所有指向它的引用都必须修正。位于栈或静态存储区的引用可以直接被修正,但可能还有其他指向这些对象的引用,它们在遍历的过程中才能被找到(可以想象成一个表格,将旧地址映射到新地址)。
标记-清扫(mark-and-sweep)
"标记-清扫"所依据的思路仍然是从栈和静态存储区出发,遍历所有的引用,找出所有存活的对象。但是,每当找到一个存活对象,就给对象设一个标记,并不回收它。只有当标记过程完成后,清理动作才开始。在清理过程中,没有标记的对象将被释放,不会发生任何复制动作。"标记-清扫"后剩下的堆空间是不连续的,垃圾回收器要是希望得到连续空间的话,就需要重新整理剩下的对象。Sun 公司早期版本的 Java 虚拟机一直使用这种技术。
Java 虚拟机中有许多附加技术用来提升速度。尤其是与加载器操作有关的,被称为"即时"(Just-In-Time, JIT)编译器的技术。这种技术可以把程序全部或部分翻译成本地机器码,所以不需要 JVM 来进行翻译,因此运行得更快。当需要装载某个类(通常是创建该类的第一个对象)时,编译器会先找到其 .class 文件,然后将该类的字节码装入内存。你可以让即时编译器编译所有代码,但这种做法有两个缺点:一是这种加载动作贯穿整个程序生命周期内,累加起来需要花更多时间;二是会增加可执行代码的长度(字节码要比即时编译器展开后的本地机器码小很多),这会导致页面调度,从而一定降低程序速度。另一种做法称为惰性评估,意味着即时编译器只有在必要的时候才编译代码。这样,从未被执行的代码也许就压根不会被 JIT 编译。新版 JDK 中的 Java HotSpot 技术就采用了类似的做法,代码每被执行一次就优化一些,所以执行的次数越多,它的速度就越快。
总结 由于要保证所有对象被创建,实际上构造器比这里讨论得更加复杂。特别是当通过组合或继承创建新类的时候,这种保证仍然成立,并且需要一些额外的语法来支持。想了解更多相关内容,请关注后续文章!

    推荐阅读