初始化与清理
随着计算机革命的发展,“不安全”的编程方式已逐渐成为编程代价高昂的主因之一。
初始化和清理正是涉及安全的两个问题。
1. 用构造器确保初始化
在Java中,通过构造器,类的设计者可以确保每个对象都会得到初始化。创建对象时,如果类具有构造器,Java就会在用户操作对象之前自动调用相应的构造器,从而保证了初始化的进行。
构造器的特点:
- 构造器名称和类名完全相同
- 不接受任何参数的构造器叫做“默认构造器”或者“无参构造器”
- 如果没有创建任何构造器,编译器会默认创建一个“无参构造器”
- 可以自己创建构造器(有参无参都可以),但是这样编译器就不会创建“无参构造器”,如果需要使用,则必须自己创建
2. 方法重载
Java中构造器的名称必须和类名一致,也就是说只能有一个构造器名称,那么如果想用多种方式创建一个对象应该怎么办? 这就要用到方法重载。同时,尽管方法重载是构造器所必需的,但它也可用于其他方法。例如:
1 /** 2 * 重载 3 */ 4 public class Tree { 5 Tree() { 6 System.out.println("Tree"); 7 } 8 Tree(int height) { 9 System.out.println("Tree height = " + height); 10 } 11 12 void f() { 13 System.out.println("f()"); 14 } 15 16 void f(String s) { 17 System.out.println("f(String)"); 18 } 19 20 public static void main(String[] args) { 21 Tree t1 = new Tree(); 22 Tree t2 = new Tree(1); 23 t1.f(); 24 t1.f("test"); 25 26 } 27 } 28 输出: 29 Tree 30 Tree height = 1 31 f() 32 f(String)
2.1 区分重载的方法
通过参数列表:每个重载的方法都一个独一无二的参数类型列表,甚至不同的参数顺序也可以区分(这个要求所有的参数类型不能完全相同,尽量不要这么弄,会使代码难以维护)
疑问:是否可以通过返回值区分重载方法呢?
答案:不可以
如下面的重载函数,如果直接调用f(),编译器如何知道调用哪个方法。所以通过返回值区分重载方法是行不通的。
1 void f() { } 2 int f() {return 1;}
2.2 涉及基本类型的重载
如果传入的数据类型小于方法中声明的参数类型,实际数据类型会被提升。通过下面的代码可以看出基本类型向上转型的顺序:char->int->long->float->double;byte->short->int->long->float->double
1 /** 2 * 基本类型的重载 3 */ 4 public class PrimitiveOverloading { 5 6 void f1(char x) { System.out.print("f1(char) "); } 7 void f1(byte x) { System.out.print("f1(byte) "); } 8 void f1(short x) { System.out.print("f1(short) "); } 9 void f1(int x) { System.out.print("f1(int) "); } 10 void f1(long x) { System.out.print("f1(long) "); } 11 void f1(float x) { System.out.print("f1(float) "); } 12 void f1(double x) { System.out.print("f1(double) "); } 13 14 void f2(byte x) { System.out.print("f2(byte) "); } 15 void f2(short x) { System.out.print("f2(short) "); } 16 void f2(int x) { System.out.print("f2(int) "); } 17 void f2(long x) { System.out.print("f2(long) "); } 18 void f2(float x) { System.out.print("f2(float) "); } 19 void f2(double x) { System.out.print("f2(double) "); } 20 21 void f3(short x) { System.out.print("f3(short) "); } 22 void f3(int x) { System.out.print("f3(int) "); } 23 void f3(long x) { System.out.print("f3(long) "); } 24 void f3(float x) { System.out.print("f3(float) "); } 25 void f3(double x) { System.out.print("f3(double) "); } 26 27 void f4(int x) { System.out.print("f4(int) "); } 28 void f4(long x) { System.out.print("f4(long) "); } 29 void f4(float x) { System.out.print("f4(float) "); } 30 void f4(double x) { System.out.print("f4(double) "); } 31 32 void f5(long x) { System.out.print("f5(long) "); } 33 void f5(float x) { System.out.print("f5(float) "); } 34 void f5(double x) { System.out.print("f5(double) "); } 35 36 void f6(float x) { System.out.print("f6(float) "); } 37 void f6(double x) { System.out.print("f6(double) "); } 38 39 void f7(double x) { System.out.print("f7(double) "); } 40 41 void testChar() { 42 System.out.print("char: "); 43 char x = 'x'; 44 f1(x);f2(x);f3(x);f4(x);f5(x);f6(x);f7(x); 45 System.out.println(); 46 } 47 void testByte() { 48 System.out.print("byte: "); 49 byte x = 0; 50 f1(x);f2(x);f3(x);f4(x);f5(x);f6(x);f7(x); 51 System.out.println(); 52 } 53 void testShort() { 54 System.out.print("short: "); 55 short x = 0; 56 f1(x);f2(x);f3(x);f4(x);f5(x);f6(x);f7(x); 57 System.out.println(); 58 } 59 void testInt() { 60 System.out.print("int: "); 61 int x = 0; 62 f1(x);f2(x);f3(x);f4(x);f5(x);f6(x);f7(x); 63 System.out.println(); 64 } 65 void testLong() { 66 System.out.print("long: "); 67 long x = 0; 68 f1(x);f2(x);f3(x);f4(x);f5(x);f6(x);f7(x); 69 System.out.println(); 70 } 71 void testfloat() { 72 System.out.print("float: "); 73 float x = 0; 74 f1(x);f2(x);f3(x);f4(x);f5(x);f6(x);f7(x); 75 System.out.println(); 76 } 77 void testDouble() { 78 System.out.print("double: "); 79 double x = 0; 80 f1(x);f2(x);f3(x);f4(x);f5(x);f6(x);f7(x); 81 System.out.println(); 82 } 83 public static void main(String[] args) { 84 PrimitiveOverloading p = new PrimitiveOverloading(); 85 p.testChar(); 86 p.testByte(); 87 p.testShort(); 88 p.testInt(); 89 p.testLong(); 90 p.testfloat(); 91 p.testDouble(); 92 } 93 } 94 95 输出: 96 char: f1(char) f2(int) f3(int) f4(int) f5(long) f6(float) f7(double) 97 byte: f1(byte) f2(byte) f3(short) f4(int) f5(long) f6(float) f7(double) 98 short: f1(short) f2(short) f3(short) f4(int) f5(long) f6(float) f7(double) 99 int: f1(int) f2(int) f3(int) f4(int) f5(long) f6(float) f7(double) 100 long: f1(long) f2(long) f3(long) f4(long) f5(long) f6(float) f7(double) 101 float: f1(float) f2(float) f3(float) f4(float) f5(float) f6(float) f7(double) 102 double: f1(double) f2(double) f3(double) f4(double) f5(double) f6(double) f7(double)
如果传入的实际参数**大于**重装方法声明的形式参数,需要通过类型转换,否则编译器会报错:
1 void f8(short x) { System.out.print("f8(short) "); } 2 // p.f8(5); //编译报错 3 p.f8((short)5);
3. this关键字
this表示“调用方法的那个对象”的引用,只能在方法内部使用:
- 可以在构造器中使用,调用其他的构造器
- 当方法的参数和成员变量名字相同时,可使用this关键字区分成员变量和传入参数
3.1 在构造器中调用构造器
可能为了一个类写了多个构造器,有时想在一个构造器中调用另一个构造器,以避免重复代码,可使用this关键字做到这点。在构造器中,如果为this添加了参数列表(空列表也可以),那么就会对符合此参数列表的某个构造器进行调用。
使用this调用构造器限制条件:
- 只能在构造器中使用this调用其他的构造器,其他方法不行
- 只能调用一次
- 必须是第一个被执行的语句
1 public class Flower { 2 int petalCount = 0; 3 String s = "initial value"; 4 5 Flower(int petals) { 6 petalCount = petals; 7 System.out.println("Constructor int arg only, petalCount = " + petalCount); 8 } 9 10 Flower(String s, int petals) { 11 12 this(petals); 13 // this(s); // 编译报错,只能调用一次 14 this.s = s; // 使用this关键字代表数据成员,防止和参数s混淆 15 System.out.println("String & int args"); 16 } 17 18 Flower() { 19 this("hi", 47); 20 System.out.println("default constructor (no args)"); 21 } 22 23 void pringPetalCount() { 24 // this(11); // 编译报错,只能在构造器中使用 25 System.out.println("petalCount = " + petalCount + " s = " + s); 26 } 27 28 public static void main(String[] args) { 29 Flower x = new Flower(); 30 x.pringPetalCount(); 31 } 32 } 33 34 输出: 35 Constructor int arg only, petalCount = 47 36 String & int args 37 default constructor (no args) 38 petalCount = 47 s = hi
3.2 static方法的含义
static方法就是没有this的方法。在static方法内部不能调用非静态方法,反过来可以。并且在没有创建任何对象的前提下,仅仅通过类本身来调用static方法。
4. 清理
java中有垃圾回收器负责回收无用对象占据的内存,但是什么时候会执行垃圾清理不确定,有可能你创建的对象在程序运行过程中永远不会被清理:
- 对象可能不被垃圾回收
- 垃圾回收并不等于析构
- 垃圾回收只与内存有关
几种常见的垃圾清理机制:
| 垃圾清理机制 | 简要描述 | 缺陷 |
| 引用计数 | 简单但速度慢。每个对象都有一个引用计数,当有引用连接到对象时,引用计数加1。当引用离开作用域或者被置为null时,引用及时减一。当对象的引用计数为0时,释放其占用的空间。 | 对象之间存在循环引用,可能出现“对象应该被回收,但是引用计数却不为0” |
| 停止-复制 | 从堆栈和静态存储区出发,遍历所有对象引用,进而找出所有存活的对象。暂停程序的运行,将所有存活的对象拷贝到另一个堆,当对象被复制到新堆时,它们是一个挨着一个的。 | 效率低。首先需要两个堆,需要多维护一倍的空间。程序运行过程中可能只产生少量的垃圾,这种拷贝属于浪费 |
| 标记-清扫 | 从堆栈和静态存储区出发,遍历所有对象引用,进而找出所有存活的对象。每当找到一个存活对象会给对象一个标记,这个过程中不会回收任何对象,当全部标记工作方程时,才会清理。没有标记的对象被释放。 | 清理后剩余的空间是不连续的 |
5. 成员初始化
方法的局部变量不会给默认值,必须自己进行初始化;类的成员变量,会给默认值:
1 /** 2 * 3 */ 4 class Base { 5 6 } 7 public class DataInit { 8 boolean t; 9 char c; 10 byte b; 11 short s; 12 int i; 13 long l; 14 float f; 15 double d; 16 String ss; 17 DataInit reference; 18 19 void f() { 20 int j; 21 // System.out.println(j); // 编译报错 22 j = 1; 23 System.out.println("j = " + j); 24 } 25 26 void printInitValues() { 27 System.out.println("数据类型 默认初始化值"); 28 System.out.println("boolean " + t); 29 System.out.println("char #" + c + "#"); // char值为0,显示空白,可以使用(int)强制转化查看 30 System.out.println("byte " + b); 31 System.out.println("short " + s); 32 System.out.println("int " + i); 33 System.out.println("long " + l); 34 System.out.println("float " + f); 35 System.out.println("double " + d); 36 System.out.println("String " + ss); 37 System.out.println("reference " + reference); 38 } 39 public static void main(String[] args) { 40 DataInit d = new DataInit(); 41 d.printInitValues(); 42 } 43 } 44 45 输出: 46 数据类型 默认初始化值 47 boolean false 48 char # # 49 byte 0 50 short 0 51 int 0 52 long 0 53 float 0.0 54 double 0.0 55 String null 56 reference null
6. 构造器初始化
可以使用构造器初始化,即在运行时刻,可以调用方法或执行某些动作来确定初值。但要牢记:无法阻止自动初始化的进行,它将在构造器调用之前发生。如下代码,首先i会先被置0,然后变为7:
1 public class Counter {
2 int i;
3 Counter() {
4 i = 7;
5 }
6 }
6.1 初始化顺序
在类的内部,变量定义的先后顺序决定了初始化的顺序。即使变量定义散布于方法定义之间,它们仍旧会在任何方法(包括构造器)被调用之前得到初始化。例如:
在House类中,故意把几个Window对象定义散布到各处,以证明它们全都会在调用构造器或其他方法之前得到初始化
1 /**
2 * 初始化的顺序
3 */
4
5 class Window {
6 Window(int marker) {
7 System.out.println("Window(" + marker + ")");
8 }
9 }
10 class House {
11 Window w1 = new Window(1); // 构造器之前
12 House() {
13 System.out.println("House");
14 w3 = new Window(33); // 重新初始化
15 }
16 Window w2 = new Window(2); // 构造器之后
17 void f() {
18 System.out.println("f()");
19 }
20 Window w3 = new Window(3);
21 }
22 public class OrderOfInit {
23 public static void main(String[] args) {
24 House h = new House();
25 h.f();
26 }
27 }
28
29 输出:
30 Window(1)
31 Window(2)
32 Window(3)
33 House
34 Window(33)
35 f()
6.2 静态数据初始化
无论创建多少个对象,静态数据都只占用一份存储区域。static关键字不能用于局部变量。如果一个域是静态的基本类型域,且没有初始化,那么它就会获得基本类型的标准初值;如果它是一个对象引用,那么它的默认初值就是null。如果定义时初始化,和非静态数据没有区别。
1 /**
2 * 静态数据初始化
3 */
4 class Bowl {
5 Bowl(int marker) {
6 System.out.println("Bowl(" + marker + ")");
7 }
8 void f1(int marker) {
9 System.out.println("f1(" + marker +")");
10 }
11 }
12
13 class Table {
14 Bowl bowl3 = new Bowl(3);
15 static Bowl bowl1 = new Bowl(1);
16 Table() {
17 System.out.println("Table()");
18 bowl2.f1(1);
19 }
20 static void s() {
21 System.out.println("Table static method s()");
22 }
23 void f2(int marker) {
24 System.out.println("f2(" + marker +")");
25 }
26 static Bowl bowl2 = new Bowl(2);
27 }
28 //public class StaticInit {
29 // public static void main(String[] args) {
30 // System.out.println("Creating new Cupboard() in main");
31 // Bowl bowl1 = Table.bowl1;
32 // }
33 //}
34 //输出:
35 //Creating new Cupboard() in main
36 //Bowl(1)
37 //Bowl(2)
38
39 //public class StaticInit {
40 // public static void main(String[] args) {
41 // System.out.println("Creating new Cupboard() in main");
42 // Table.s();
43 // }
44 //}
45 //输出:
46 //Creating new Cupboard() in main
47 //Bowl(1)
48 //Bowl(2)
49 //Table static method s()
50 public class StaticInit {
51 public static void main(String[] args) {
52 System.out.println("Creating new Cupboard() in main");
53 table.f2(1);
54 }
55 static Table table = new Table();
56 }
57
58 输出:
59 Bowl(1)
60 Bowl(2)
61 Bowl(3)
62 Table()
63 f1(1)
64 Creating new Cupboard() in main
65 f2(1)
由输出可见静态初始化只有在必要时刻才会进行,只有在一个Table对象被创建(或者第一次访问静态数据或静态方法,见注释代码部分)的时候,它们才会被初始化。此后静态变量不会初始化。初始化的顺序是先静态对象,后非静态对象。
对象创建的过程,假设有个名为Dog的类:
- 即使没有显式的使用static关键字,构造器实际上也是静态方法。因此当首次创建为Dog的对象时,或者Dog类的静态方法/静态域(静态成员变量)首次被访问时,Java解释器必须查找类的路径,以定位Dog.class文件
- 然后载入Dog.class(后面会学到),有关静态初始化的动作都会执行。因此静态初始化只在Class对象首次加载的时候进行一次
- 当用new Dog()创建对象的时候,首先将在堆上为Dog对象分配足够的存储空间
- 这块存储空间会被清零,这就自动将Dog对象中的所有基本类型数据设置成了默认值,而引用则被设置成了null
- 执行所有出现于字段定义出的初始化动作
- 执行构造器
6.3 显示的静态初始化
Java允许将多个静态初始化组成一个特殊的“静态初始化子句”(也叫“静态块”),例如:
1 class Cups {
2 static Cup cup1;
3 static Cup cup2;
4 // 静态子句(静态块)
5 static {
6 cup1 = new Cup(1);
7 cup2 = new Cup(2);
8 }
9 }
与其他静态初始化一样,静态子句仅执行一次:当首次生成这个类的一个对象时,或者首次访问属于这个类的静态数据成员时。例如:
1 /**
2 * 显式的静态初始化
3 */
4 class Cup {
5 Cup(int marker) {
6 System.out.println("Cup(" + marker +")");
7 }
8 void f(int marker) {
9 System.out.println("f(" + marker +")");
10 }
11 }
12
13 class Cups {
14 static Cup cup1;
15 static Cup cup2;
16 // 静态子句(静态块)
17 static {
18 cup1 = new Cup(1);
19 cup2 = new Cup(2);
20 }
21 Cups(){
22 System.out.println("Cups()");
23 }
24 }
25
26 public class ExplicitStatic {
27 public static void main(String[] args) {
28 System.out.println("Inside main()");
29 Cups.cup1.f(99);
30 }
31 // static Cups cups1 = new Cups();
32 }
33
34 输出:
35 Inside main()
36 Cup(1)
37 Cup(2)
38 f(99)
6.4 非静态实例初始化
Java中也有“实例初始化子句”,用来初始化每一个对象的非静态变量,例如:
1 /**
2 * 非静态实例初始化
3 */
4 /**
5 * 非静态实例初始化
6 */
7 class Mug {
8 Mug(int marker) {
9 System.out.println("Mug(" + marker +")");
10 }
11 }
12 public class Mugs {
13 Mug mug1;
14 Mug mug2;
15 // 实例初始化子句
16 {
17 mug1 = new Mug(1);
18 mug2 = new Mug(2);
19 System.out.println("mug1 & mug2 init");
20 }
21 Mugs() {
22 System.out.println("Mugs()");
23 }
24
25 public static void main(String[] args) {
26 new Mugs();
27 new Mugs();
28 }
29 }
30 输出:
31 Mug(1)
32 Mug(2)
33 mug1 & mug2 init
34 Mugs()
35 Mug(1)
36 Mug(2)
37 mug1 & mug2 init
38 Mugs()
“实例初始化子句”相比“静态初始化子句”只少了static关键字,从输出可以看出实例初始化自己是在构造器之前执行的。
7. 数组初始化
定义:类型名+[],例如:
int[] a1;
方括号也可以放在标识符后面:
int a1[];
两种格式是一样的,推荐使用前一种,能明确表明定义的是“一个int类型的数组”,还有就是如果使用第二种定义有时与自己预期不符,比如想定义两个int数组,实际上只有a3是int数组,a4是整形变量:int a3[], a4;
编译器不允许指定数组的大小。因为创建的只是对数组的一个引用,并且没有给对象本身分配空间。数组初始化的几种方式:
- 定义的时候初始化: int[] a1 = {1, 2, 3}; // 这种方式只能在定义的时候使用
- 使用new创建,不赋初值: int[] a3 = new int[5]; // 这种方式可以先定义a3,在其他任何地方都可以初始化。因为没有赋初值,默认数组所有元素都为0
- 使用new创建,并赋初值: int[] a3 = new int[]{1, 2, 3, 4,5,}; // 这种方式同样可以先定义a3,在任何地方都可以初始化,其中初始化列表中的最后一个逗号可选(维护长列表方便,增加也方便)
所有数组都有length的成员,可以用来获取数组元素的个数: a.length
1 public class ListTest {
2
3 static String listToString(int[] a) {
4 String l = "";
5 for (int i = 0; i < a.length; i++) {
6 l += a[i] + " ";
7 }
8 return l;
9 }
10 public static void main(String[] args) {
11 int[] a1 = {1, 2, 3, 4, 5};
12 // int[] a1 = {1, 2, 3, 4, 5,}; //增加逗号也可以
13 int[] a2;
14 // a2 = {1, 2, 3, 4, 5}; // 编译报错,这种方式只能在定义时初始化
15 int[] a3 = new int[5]; // 使用new创建数组
16 int[] a4; // 先定义数组,在其他地方使用new初始化
17 int[] a5; // 先定义数组,在其他地方使用new初始化,并指定初值
18 a4 = new int[5];
19 a5 = new int[]{1, 2, 3, 4,5,};
20 System.out.println("a1 = " + listToString(a1));
21 System.out.println("a3 = " + listToString(a3));
22 System.out.println("a4 = " + listToString(a4));
23 System.out.println("a5 = " + listToString(a5));
24 }
25 }
26 输出:
27 a1 = 1 2 3 4 5
28 a3 = 0 0 0 0 0
29 a4 = 0 0 0 0 0
30 a5 = 1 2 3 4 5
7.1 可变参数
printArray函数可以接受0-n个任意对象, f函数至少接收一个整数,接收0-n个字符串:
1 public class NewArgs {
2 static void printArray(Object... args) {
3 for (Object obj : args) {
4 System.out.print(obj + " ");
5 }
6 System.out.println();
7 }
8 static void f(int required, String... strs) {
9 System.out.print("requied: " + required + " ");
10 for (String s : strs) {
11 System.out.print(s + " ");
12 }
13 System.out.println();
14 }
15 public static void main(String[] args) {
16 printArray(new Integer(47), new Float(3.14), new Double(11.11));
17 printArray(47, 3.14f, 11.11);
18 printArray("one", "two", "three");
19 printArray(); // 传空也可以
20 // f(); // 编译报错,至少传递一个参数
21 f(0);
22 f(1, "one");
23 f(2, "one", "two");
24 }
25 }
26 输出:
27 47 3.14 11.11
28 47 3.14 11.11
29 one two three
30
31 requied: 0
32 requied: 1 one
33 requied: 2 one two
8. 枚举类型
创建枚举类型Grade,具有A、B、C、D 4个值,枚举都是常量,使用大写字母表示(如果一个名字中有多个单词,用下划线分隔):
1 public enum Grade {
2 A, B, C, D
3 }
使用enum,需要创建一个该类型的引用,并将其赋值给某个实例:
1 Grade grade = Grade.A
创建enum时,编译器会自动增加一些有用的特性。例如,创建toString()方法,可以方便显示enum的实例名字;ordinal()方法,用来表示某个特定的enum常量的生命顺序;static values()方法,用enum常量的生命顺序,产生由这些常量构成的数组:
1 public class Ranking {
2 public static void main(String[] args) {
3 for (Grade grade : Grade.values()) {
4 System.out.println(grade + " ordinal " + grade.ordinal());
5 }
6 }
7 }
8 输出:
9 A ordinal 0
10 B ordinal 1
11 C ordinal 2
12 D ordinal 3
还可用于switch语句:
1 // Grade.java
2 public enum Grade {
3 A, B, C, D
4 }
1 //Ranking.java
2 public class Ranking {
3 Grade grade;
4 Ranking(Grade grade) {
5 this.grade = grade;
6 }
7 public void getRankingDes() {
8 switch (grade) {
9 case A:
10 System.out.println("优秀");
11 break;
12 case B:
13 System.out.println("良好");
14 break;
15 case C:
16 System.out.println("及格");
17 break;
18 case D:
19 System.out.println("不及格");
20 break;
21 default:
22 System.out.println("错误输入");
23 }
24 }
25 public static void main(String[] args) {
26 Ranking rank1 = new Ranking(Grade.A);
27 Ranking rank2 = new Ranking(Grade.D);
28 rank1.getRankingDes();
29 rank2.getRankingDes();
30 }
31 }
32
33 输出:
34 优秀
35 不及格

浙公网安备 33010602011771号