初始化与清理

随着计算机革命的发展,“不安全”的编程方式已逐渐成为编程代价高昂的主因之一。

初始化和清理正是涉及安全的两个问题。

1. 用构造器确保初始化

在Java中,通过构造器,类的设计者可以确保每个对象都会得到初始化。创建对象时,如果类具有构造器,Java就会在用户操作对象之前自动调用相应的构造器,从而保证了初始化的进行。

构造器的特点:

  1. 构造器名称和类名完全相同
  2. 不接受任何参数的构造器叫做“默认构造器”或者“无参构造器”
  3. 如果没有创建任何构造器,编译器会默认创建一个“无参构造器”
  4. 可以自己创建构造器(有参无参都可以),但是这样编译器就不会创建“无参构造器”,如果需要使用,则必须自己创建

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表示“调用方法的那个对象”的引用,只能在方法内部使用:

  1. 可以在构造器中使用,调用其他的构造器
  2. 当方法的参数和成员变量名字相同时,可使用this关键字区分成员变量和传入参数

3.1 在构造器中调用构造器

可能为了一个类写了多个构造器,有时想在一个构造器中调用另一个构造器,以避免重复代码,可使用this关键字做到这点。在构造器中,如果为this添加了参数列表(空列表也可以),那么就会对符合此参数列表的某个构造器进行调用。

使用this调用构造器限制条件:

  1. 只能在构造器中使用this调用其他的构造器,其他方法不行
  2. 只能调用一次
  3. 必须是第一个被执行的语句
 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. 对象可能不被垃圾回收
  2. 垃圾回收并不等于析构
  3. 垃圾回收只与内存有关

几种常见的垃圾清理机制:

垃圾清理机制 简要描述 缺陷
引用计数 简单但速度慢。每个对象都有一个引用计数,当有引用连接到对象时,引用计数加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的类:

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

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;

编译器不允许指定数组的大小。因为创建的只是对数组的一个引用,并且没有给对象本身分配空间。数组初始化的几种方式:

  1. 定义的时候初始化: int[] a1 = {1, 2, 3}; // 这种方式只能在定义的时候使用
  2. 使用new创建,不赋初值: int[] a3 = new int[5]; // 这种方式可以先定义a3,在其他任何地方都可以初始化。因为没有赋初值,默认数组所有元素都为0
  3. 使用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 不及格
posted @ 2019-02-15 08:55  暴躁的毛毛熊  阅读(256)  评论(0编辑  收藏  举报