- 初始化和清理是涉及安全的两个问题。
- Java中采用了构造器,一个在创建对象时被自动调用的特殊方法,并额外提供了“垃圾回收器”。
1 用构造器确保初始化
- 创建对象时,如果其类具有构造器,Java就会在用户有能力操作对象之前自动调用相应的构造器,从而保证了初始化的进行。
- 构造器采用与类相同的名称。
- 不接受任何参数的构造器叫做默认构造器,Java文档中通常使用术语无参构造器。
- 在Java中,“初始化”和“创建”捆绑在一起,两者不能分离。
- 构造器是一种特殊类型的方法,因为它没有返回值。
2 方法重载
- 当创建一个对象时,也就给此对象分配到的存储空间取了一个名字。所谓方法则是给某个动作取的名字。每个方法都有个独一无二的标识符。
- 方法重载,让方法名相同而形式参数不同的构造器和方法同时存在。
2.1 区分重载方法
- 每个重载的方法都必须有一个独一无二的参数类型列表。
- 参数顺序的不同也可以区分两个方法。
2.2 涉及基本类型的重载
- 基本类型能从一个“较小”的类型自动提升至一个“较大”的类型。
- 如果传入的数据类型(实际参数类型)小于方法中声明的形式参数类型,实际数据类型就会被提升。char型略有不同,如果无法找到恰好接受char参数的方法,就会把char直接提升至int型。
- 如果传入的实际参数较大,就得通过类型转换来执行窄化。如果不这样做,编译器就会报错。
2.3 以返回值区分重载方法
- 可能会调用方法而忽略其返回值,根据方法的返回值来区分重载方法是行不通的。
3 默认构造器
- 默认构造器是没有形式参数的,作用是创建一个“默认对象”。
- 如果你写的类中没有构造器,则编译器会自动帮你创建一个默认构造器。
- 如果已经定义了一个构造器(无论是否有参数),编译器就不会帮你自动创建默认构造器。
4 this关键字
- “发送消息给对象”时把“所操作对象的引用”作为第一个参数传递给方法。
- this关键字只能在方法内部使用,表示对“调用方法的那个对象”的引用。this的用法和其他对象引用并无不同。
- 如果在方法内部调用同一个类的另一个方法,就不必使用this,直接调用即可。
- 只有当需要明确指出对当前对象的引用时,才需要使用this关键字,最好不要把this放在没必要地方。
- this关键字可以用于将当前对象传递给其他方法。
4.1 在构造器中调用构造器
- 通常写this的时候,都是指“这个对象”或者“当前对象”,而且它本身表示对当前对象的引用。
- 尽管可以用this调用一个构造器,但却不能调用两个。
- 必须将构造器调用置于最起始处,否则编译器会报错。
- 形参和数据成员名称相同时,用
this.名称
来代表数据成员。 - 除了构造器之外,编译器禁止在其他任何方法中调用构造器。
4.2 static的含义
- static方法是没有this的方法,在static方法的内部不能直接调用非静态方法。
- java中禁止使用全局方法,但你在类中置入static方法就可以访问其他static方法和static域。
5 清理:终结处理和垃圾回收
- 垃圾回收器只知道释放那些经由new分配的内存,不知道如何释放一些对象(并非使用new)获得的“特殊”的内存区域
- Java允许在类中定义一个名为finalize()的方法,一旦垃圾回收器准备好释放对象占用的存储空间,将首先调用其finalize()方法。
- Java里的对象并非总是被垃圾回收。
- 对象可能不被垃圾回收。
- 垃圾回收并不等于“析构”。
- 垃圾回收只与内存有关。
-只要程序没有濒临存储空间用完的那一刻,对象占用的空间就总也得不到释放。
5.1 finalize()的用途何在
- 使用垃圾回收器的唯一原因是为了回收程序不再使用的内存。
- finalize()的需求限制到一种特殊情况,即通过某种创建对象方式以外的方式为对象分配了存储空间。
- 这种特殊情况,是由于在分配内存时可能采用了类似C语言中的做法。
- 本地方法是一种在Java中调用非Java代码的方式。本地方法目前只支持C和C++,但它们可以调用其他语言写的代码,实际上可以调用任何代码。
5.2 你必须实施清理
- Java不允许创建局部对象,必须使用new创建对象。
- 垃圾回收器的存在并不能完全代替析构函数。(绝对不能直接调用finalize())
- 如果希望进行除释放存储空间之外的清理工作,得明确调用某个恰当的Java方法。
- 无论是“垃圾回收”还是“终结”,都不保证一定会发生。如果Java虚拟机(JVM)并未面临内存耗尽的情形,它是不会浪费时间去执行垃圾回收以恢复内存的。
5.3 终结条件
- finalize()可以用于对象终结条件的验证,确保释放实例占用的全部资源。
- 当对象可以被清理时,这个对象应该处于某种状态,使它占用的内存可以被安全地释放。
- System.gc()用于强制进行终结动作。告诉垃圾回收器打算进行垃圾收集,而垃圾回收器进不进行收集是不确定的。
- System.runFinalization()强制调用已经失去引用的对象的finalize方法。
5.4 垃圾回收器如何工作
- Java从堆分配空间的速度,可以和其他语言从堆栈上分配空间的速度相媲美。
- Java的“堆指针”只是简单地移动到尚未分配的区域,其效率比得上C++在堆栈上分配空间的效率。
- 引用计数是一种简单但速度很慢的垃圾回收技术。
- 对任何“活”的对象,一定能最终追溯到其存活在堆栈或静态存储区之中的引用。
- Java虚拟机采用一种自适应的垃圾回收技术。
- 停止-复制(stop-and-copy),暂停程序,将所有存活的对象从当前堆复制到另一个堆,没有被复制的全部都是垃圾。
- 标记-清扫(mark-and-sweep),从堆栈和静态存储区出发,遍历所有的引用,进而找出所有存活的对象,没有标记的对象将被释放。
- 在Java虚拟机中,内存分配以较大的“块”为单位、如果对象较大,它会占用单独的快。
- “即时”(Just-In—Time,JIT)编译器技术,与加载器操作有关
- 即时编译器编译所有代码。
- 惰性评估,即时编译器只在必要的时候才编译代码。
6 成员初始化
- Java尽力保证:所有变量在使用前都能得到恰当的初始化。对于方法的局部变量,Java以编译时错误的形式来贯彻这种保证。
- 类的每个基本类型数据成员保证都会有一个初始值。
- 在类里定义一个对象引用时,如果不将其初始化,此引用就会获得一个特殊值null。
6.1 指定初始化
- 在定义类成员变量的地方为其赋值(在C++里不能这样做),也可以用同样的方法初始化非基本类型的对象。
- 可以通过调用某个方法来提供初值。这个方法也可以带有参数,这些参数必须是已经被初始化了的。编译器会恰当地对“向前引用”发出警告。
- 类的每个对象都会具有相同的初值。
7 构造器初始化
- 无法阻止自动初始化的进行,它将在构造器被调用之前发生。
- 初始化早已得到保证,编译器不会强制你一定要在构造器的某个地方或在使用它们之前对元素进行初始化。
7.1 初始化顺序
- 在类的内部,变量定义的先后顺序决定了初始化的顺序,变量会在任何方法(包括构造器)被调用之前得到初始化。
7.2 静态数据的初始化
- 无论创建多少个对象,静态数据都只占用一份存储空间。
- static关键字不能应用于局部变量,只能作用于域。
- 如果一个域是静态的基本类型域,且也没有对它进行初始化,那么它就会获得基本类型的标准值;如果它是一个对象引用,那么它的默认初始化化值就是null。
- 静态初始化只有在必要时刻才会进行,只有在类第一次被访问时,才会被初始化。静态对象不会再次被初始化。
- 初始化的顺序是先静态对象(如果它们尚未因前面的对象创建过程而被初始化),而后是“非静态”对象。
- 对象创建过程,假设有个Dog类。
- 即使没有显示地使用static关键字,构造器实际上也是静态方法。首次创建Dog对象时,或Dog类的静态方法/静态域首次被访问时,Java解释器将查找类路径,定位Dog.class。
- 载入Dog.class,有关静态初始化的所有动作都会被执行。静态初始化只在Class对象首次加载的时候进行一次。
- 当用new Dog()创建对象的时候,首先将在堆上为Dog对象分配足够的存储空间。
- 这块存储空间会被清零,这就自动地将Dog对象中的所有基本数据类型和引用设置成了默认值。
- 执行所有出现于字段定义处的初始化条件。
- 执行构造器。
7.3 显示的静态初始化
- Java允许将多个静态初始化动作组织成一个特殊的“静态子句”(又称“静态块”)。
- 静态块实际只是一段跟在static关键字后面的代码,与其他静态初始化一样,这段代码仅执行一次。静态初始化动作只进行一次。
7.4 非静态实例初始化
- 实例初始化,用来初始化每一个对象的非静态变量。
- 实例初始化子句是在两个构造器之前执行的。
8 数组初始化
- 数组是相同类型的、用一个标识符名称封装到一起的一个对象序列或基本类型数据序列。
- 数组是通过方括号下标操作符[ ]来定义和使用的。要定义一个数组,只需在类型名后加上一对方括号即可,或者将方括号置于标识符后面。
- 编译器不允许指定数组的大小。为了给数组创建相应的存储空间,必须写初始化表达式。
- 数组初始化动作可以出现在代码的任何地方,可以使用一种特殊的初始化表达式,由一对花括号括起来的值组成的,必须在创建数组的地方出现,等价于使用new。
- 可以将一个数组赋值给另一个数组,实际只是复制了一个引用。
- 数组成员length,代表数组元素个数,不可修改。
- 数组计数从第0个元素开始,能使用的最大下标数是length-1。
- 数组尽量在定义的同时进行初始化,可以用new创建基本类型数组,但不能使用new创建单个的基本类型数据。
- 数组的创建是在运行时刻进行的,元素中的基本数据类型值会自动初始化成空值。
- Arrays.toString()方法将产生一维数组的可打印版本。
- 使用对象数组中的空引用,会在运行时产生异常。
- 可以在new数组,并且不指定数组大小的情况下,后面跟花括号括起来的列表来初始化对象数组。
- 初始化列表的最后一个逗号都是可选的。
8.1 可变参数列表
- 所有的类都直接或间接继承于Object类。
- toString()方法的默认行为就是打印类的名字和对象的地址。
- 可变参数列表,形如
TypeName... args
。 - 如果你有一组事物,可以把它们当作列表传递,而如果你已经有了一个数组,该方法可以把它们当作可变参数列表来接受。
- 将0个参数传递给可变参数列表是可行的。
- 在可变参数列表中可以使用任何类型的参数,包括基本类型。
- getClass()方法将产生对象的类,打印该类将显示该类类型的编码字符串。
- 编译器使用自动包装机制来匹配重载方法,然后调用最明确匹配的方法。
- 当有多个重载可以变参数列表方法,调用方法不添加参数时,编译器无法知道应该调用哪一个方法,可以给每个方法都添加一个非可变参数来解决该问题。
- 执行函数入口main()中的
String[] args
可以改为可变参数列表String... args
9 枚举类型
- 可以用
enum 标识符 { 字符常量 }
声明字符类型。 - 枚举类型的实例是常量,使用enum,需要创建一个该类型的引用。
- 创建enum时,编译器会自动添加一些有用的特性。toString()方法显示某个enum实例的名字,ordinal()表示某个特定enum常量的声明顺序。static values()方法,返回由enum常量值构成的数组。
- enum是类。
- enum可以在switch语句内使用。
- 可以将enum用作另外一种创建数据类型的方式,然后直接将所得到的类型拿来使用。
10 总结
- 初始化在Java中占有至关重要的地位。
疑问和总结
1 定义时初始化的域和构造器初始化的域有什么区别?
- 定义时的初始化优先于构造器初始化进行。
2 两个重载方法f(float i, Character… c)和f(Character… c),调用f(‘c’)编译器无法判断重载哪个方法。
- 未解决。
3 带可变参数和数组参数的方法重载时,怎么判断?
- 无法重载,会直接报错。
static void t(String[] args) { System.out.println("形参是字符串数组"); }
static void t(String... args) { System.out.println("形参是可变字符串数组"); }
- 结论:JDk不允许,相同类型的带可变参数的方法和带数组参数的方法在同一类中重载。在带可变参数的方法体时,读取可变参数列表时,就是以数组的方式来读取;
带可变参数的方法可以传入一个数组参数(还能为空),但带数组参数的方法却不能传入可变参数(也不能为空)。
4 可变参数方法与无参数方法重载时,怎么判断?
- 当可变参数方法与不带参数的方法重载时,JDK默认调用的是无参数的方法。若类中没有定义无参数的方法,则会调用可变参数的方法。
static void empty(String... args) { System.out.println("可变参数列表函数"); }
static void empty() { System.out.println("无参函数"); }
5 可变参数方法与可变参数方法重载时,怎么判断?
- 可以重载,但是在调用方法时会报错,只有调用无参方法才能通过编译。
static void g(String s1, String... args) { System.out.println("1"); }
static void g(String... args) { System.out.println("2"); }
6 可变参数方法与多个参数的方法重载时,怎么判断?
- 当两个参数调用非可变参数函数,其它情况调用可变参数方法。
static void y(String s1, String s2) { System.out.println("1"); }
static void y(String... args) { System.out.println("2"); }