码迷,mamicode.com
首页 > 编程语言 > 详细

[Thinking in Java]第5章-初始化与清理

时间:2015-05-07 14:03:01      阅读:287      评论:0      收藏:0      [点我收藏+]

标签:

 


5.1 用构造器确保初始化

构造器有什么作用?用来创建对象?但new操作符才是用来创建对象的。试想一下,一个婴儿出生时,TA的基因和TA的性别等就已经初始化了,一个圆一旦画出来,它的半径就已经初始化了,因此一旦用new创建了一个对象,就必须要初始化。像C++那样,Java中也用构造器来初始化对象,而且new一个对象时就自动调用构造器。对构造器的要求如下

  1. 构造器的名字必须与类名完全相同(区分大小写);
  2. 构造器没有返回值,连void都没有(void的意思是返回空值,而构造器连返回值都没有);
  3. 如果自己不写任何构造器,编译器会自动提供一个无参构造器,也称为默认构造器,它没有任何参数,也没有任何内容。

既然构造器没有返回值,那么可以在构造器中使用return吗?

 

 1 public class Person {
 2     int age;
 3     
 4     public static void main(String[] args) {
 5         Person zhangSan = new Person(-1);// new一个Person对象,并传入-1初始化这个对象
 6         println(zhangSan.age);
 7     }
 8     
 9     public Person(int newAge) {
10         if (newAge < 0)
11             return;// 如果年龄是负数,就退出构造器
12        //return -1;// 错误,不能返回任何值
13        //return void;// 语法错误
14         else
15             age = newAge;// 如果年龄是非负数,就赋值
16     }
17 }

 

上面的程序是可以编译运行的,因为return的意思不是返回空值,而是退出方法,但这时age还是默认值0。同时我们也看到,第12行和第13行注释的代码是不能通过编译的,因此构造器中不能return任何值,更不能return void。当然,上面的程序设计是有问题的,如果传入的参数是非负数,可以采用异常机制阻止创建对象。

重点理解第5行代码的意思,先创建(new)一个Person对象,于是这个对象放在堆中,然后使用第9行的构造器初始化对象,试图使Person对象的年龄是-1,但构造器发现参数小于0,因而年龄还是默认值0,完成初始化对象后,就返回这个Person对象的引用给Person类型的引用变量zhangSan,有点像C语言的指针,由于是Person类中使用,故而zhangSan变量可以使用这个对象的所有成员和方法

 

需要注意的是,一旦自己定义了构造器,编译器就不会自动提供一个默认构造器了,下面的程序不能通过编译

public class Person {
    int age;
    
    public static void main(String[] args) {
        Person zhangSan = new Person();// 错误,类中没有无参构造器
    }
    
    public Person(int newAge) {
    }
}

但是,自己既要有参构造器,也同时要有无参构造器,那该怎么办呢?见下一节“方法重载”

 

5.2 方法重载

先展示原书中方法重载的程序

 1 public class Tree {
 2     int height;
 3     
 4     public static void main(String[] args) {
 5         for (int i = 0; i < 5; i++) {
 6             Tree t = new Tree(i);
 7             t.info();
 8             t.info("方法重载");
 9         }
10         // 重载构造器
11         new Tree();
12     }
13     
14     Tree() {// 无参构造器
15         println("种下一个种子");
16         height = 0;
17     }
18     
19     Tree(int initialHeight) {// 有参构造器
20         height = initialHeight;
21         println("种下一棵新的树,其高度为" + height + "英尺");
22     }
23     
24     void info() {// 无参方法
25         println("树高" + height + "英尺");
26     }
27     
28     void info(String s) {// 有参方法
29         println(s + ":树高" + height + "英尺");
30     }
31 }

如上所示,同样一个名字,形式参数不同,但构造器和其他方法可以做不同的事情。就像动作“清洗”,是清洗什么呢?如何清洗?事实上,可以表示为清洗车子、清洗房子、清洗衬衫等等意思,映射到编程语言,就出现了方法重载。方法重载的语法规则如下

  1. 方法名或构造器的名字相同;
  2. 参数列表独一无二,参数的顺序不同也是允许的;
  3. 返回值可以不同;
  4. 修饰符可以不同(*****)

涉及基本类型的重载

基本数据类型能从“较小”的类型自动提升为“较大”的类型,但是这一过程遇到方法重载时,容易造成混淆。比如有两个方法fun(int x)和fun(double x),这时传入int类型的变量,那该调用哪个方法呢?其实编译器会选择最合适的方法,当然就是fun(int x)。

如果重载的方法只有两个fun(short x)和fun(int x),但这时传入了long类型的变量,那该调用哪个方法?这时或者编译器报错,或者自己强制类型转换,如果自己想要调用 fun(int x)的话,就写成fun((int) x)

参数顺序不同遇到的烦恼o(︶︿︶)o 

 1 public class Example {
 2     int x;
 3     double y;
 4     
 5     public static void main(String[] args) {
 6         Example ex = new Example();
 7         ex.fun(1, 2);// 错了???
 8     }
 9     
10     void fun(int xx, double yy) {
11         x = xx;
12         y = yy;
13     }
14     
15     void fun(double yy, int xx) {
16         y = yy;
17         x = xx;
18     }
19 }

上面的程序的第7行,编译器压根不知道该调用第7行的方法,还是调用第15行的方法,就连博主我都不知道:-),因为有歧义嘛!对这种问题,无可奈何,设计程序应当本着严谨的思维去设计,尽量避免bug的发生

 

5.3 默认构造器

 如前所述,如果没有在类中定义任何构造器的话,编译器就会暗暗地提供一个默认构造器,它没有任何参数,方法体没有任何内容。这样做的好处就是当程序员忘了定义构造器时,编译器会帮忙定义一个,当初始化对象的时候就派上用场。但尤其注意的是,一旦自己定义了构造器的话,编译器就不会再提供默认构造器了。引用原书的话:要是你没有提供任何构造器,编译器会认为,“你需要一个构造器,让我给你制造一个吧”;但加入你已写了一个构造器,编译器则会认为“啊,你已写了一个构造器,所以你知道你在做什么;你是可以省略了默认构造器。”

 

5.4 this关键字

 先看一下这个小程序

 1 class Banana {
 2     void peel(int i) {/* ...... */}
 3 }
 4 
 5 public class BananaPeel {
 6     public static void main(String[] args) {
 7         Banana a = new Banana();
 8         Banana b = new Banana();
 9         a.peel(1);
10         a.peel(2);
11     }
12 }

现在我们都知道mian方法中,先创建了Banana类型的a和b两个对象,然后这两个对象分别调用了peel方法,But,有没有想过这个问题?Banana类的peel方法怎么知道是被谁调用了?是被a调用了还是被b调用了?我初次见到这个问题还以为想多了,但却因这个问题引出了重要的this关键字:

为了能用简便、面向对象的语法来编写代码——即“发送消息给对象”,编译器做了一些幕后工作:它暗自把“所操作对象的引用”作为第一个参数传递给peel(),所以上述两个方法的调用就变成了这样:

Banana.peel(a, 1);
Banana.peel(b, 2);

这是内部的表示形式,我们不能这样书写代码,并试图通过编译,class文件反编译后也不是这样写的。我很佩服作者渊博的知识。

假设我们希望在方法内部获得对当前对象的引用,那该怎么做?this关键字因此登上代码舞台

 1 public class This {
 2     int i = 0;
 3     
 4     public static void main(String[] args) {
 5         This obj = new This();
 6         obj.increment().increment().increment().getI();
 7     }
 8     
 9     void getI() {
10         print(i);
11     }
12     
13     This increment() {
14         i++;
15         return this;
16     }
17 }/*输出结果
3
*/

为什么 i 不是0反而是3?原因在于第15行的return this;这行代码每次执行前 i 就自动加1,返回的又是这个对象的引用,就这样第15行代码经过三次执行,i 的值变成了3

既然this代表这个对象的引用,那岂不是可以这样写,this.i和this.method()了?这样写没错,但是不推荐,这就好比对别人说北京的北京大学,武汉的武汉大学。这样子说话太冗长了,因此程序应当这样子写

public class This {
    int i;
    double d;

    void f1() {
        f2();// NOT this.f2();
        i++;// NOT this.i++;
        d = d + i;// NOT this.d = this.d + this.i;
    }

    void f2() {}
}
5.4.1 在构造器中调用构造器
 1 public class Flower {
 2     int petalCount = 0;
 3     String s = "initial value";
 4     
 5     Flower(int petals) {
 6         petalCount = petals;
 7         println("构造器,只有int类型参数, petalCount = " + petalCount);
 8     }
 9     
10     Flower(String ss) {
11         println("构造器,只有String类型参数, s = " + ss);
12         s = ss;
13     }
14     
15     Flower(String s, int petals) {
16         this(petals);
17         //! this(s);//只能写一个
18         this.s = s;// this的另一种用法,this.s为这个对象的成员,不带this的s为参数
19         println("String & int 参数");
20     }
21     
22     Flower() {
23         this("Hi", 47);
24         println("默认构造器(无参)");
25     }
26     
27     void printPetalCount() {
28         //! this(11);// 不准在普通方法中用
29         println("petalCount = " + petalCount + " s = " + s);
30     }
31     
32     public static void main(String[] args) {
33         Flower x = new Flower();
34         x.printPetalCount();
35     }
36 }/*输出结果
37 构造器,只有int类型参数, petalCount = 47
38 String & int 参数
39 默认构造器(无参)
40 petalCount = 47 s = Hi
41 */

从上面的程序可以归纳this的用法:

  1. 尽管可以用this调用一个构造器,但却不能调用两个或两个以上;
  2. 必须将构造器调用放在第一行;
  3. 不准在普通方法中用this调用构造器;
  4. 可以用this调用成员。

第4点用法原来没必要,但在特殊情况下又显得非常有用:当参数s的名称恰好与类的成员s的名称相同时,参数s覆盖了类成员s,于是导致不能直接在方法内使用成员s,所以这时this就起到了关键作用。事实上参数s的名称可以改一下,避免与成员s的名称发生冲突,而大多程序员还是喜欢写成 this.s=s;

5.4.2 static的含义

static方法就是没有this的方法。在static方法中不能直接调用实例方法,但可以间接调用,可以在static方法创建一个对象,然后让对象调用实例方法,也可以在参数列表中传入对象。反过来可以让实例方法直接调用static方法。可以在没有创建任何对象的情况下,直接使用static方法。

 

5.5 清理:终结处理和垃圾回收

 

5.6 成员初始化

 首先千万要注意,局部变量一定要自己初始化,因为这更有可能是程序员的疏忽,否则编译器要报错。

 关于类的成员,如果自己不初始化,那它们的默认值是什么呢?

 1 public class InitialValues {
 2     boolean           t;
 3     char              c;
 4     byte              b;
 5     short             s;
 6     int               i;
 7     long              l;
 8     float             f;
 9     double            d;
10     InitialValues     reference;
11     
12     void printInitialValues() {
13         println("类型\t\t默认值");
14         println("boolean\t\t" + t);
15         println("char\t\t[" + c + "]");
16         println("byte\t\t" + b);
17         println("short\t\t" + s);
18         println("int\t\t" + i);
19         println("long\t\t" + l);
20         println("float\t\t" + f);
21         println("double\t\t" + d);
22         println("InitialValues\t" + reference);
23     }
24     
25     public static void main(String[] args) {
26         new InitialValues().printInitialValues();
27     }
28 }/*输出结果
29 类型            默认值
30 boolean         false
31 char            [ ]
32 byte            0
33 short           0
34 int             0
35 long            0
36 float           0.0
37 double          0.0
38 InitialValues   null
39 */

 char值为0时,所以打印结果是空白了。

 如何初始化成员,我在第7章-复用类已经写得够详细了,这里就不再赘述了......

 

5.7 构造器初始化

 学Java前,我先学C++,刚开始以为初始化一定是从构造器开始的,可如今发现还有静态初始化块和实例初始化块

class Test {
    int n;
    Test() { n = 5; }
}

上面的程序中,初始化是先n=0,然后在构造器中变成5

5.7.1 初始化顺序

先看看原书的程序

 1 class Bowl {
 2     Bowl(int marker) {
 3         println("Bowl(" + marker + ")");        
 4     }
 5     void f1(int marker) {
 6         println("f1(" + marker +")");
 7     }
 8 }
 9 
10 class Table {
11     static Bowl bowl1 = new Bowl(1);
12     Table() {
13         println("Table()");
14         bowl2.f1(1);
15     }
16     void f2(int marker) {
17         println("f2(" + marker + ")");
18     }
19     static Bowl bowl2 = new Bowl(2);
20 }
21 
22 class Cupboard {
23     Bowl bowl3 = new Bowl(3);
24     static Bowl bowl4 = new Bowl(4);
25     Cupboard() {
26         println("Cupboard()");
27         bowl4.f1(2);
28     }
29     void f3(int marker) {
30         println("f3(" + marker + ")");
31     }
32     static Bowl bowl5 = new Bowl(5);
33 }
34 
35 public class StaticInitialization {
36     public static void main(String[] args) {
37         println("Creating new Cupboard() in main");
38         new Cupboard();
39         println("Creating new Cupboard() in main");
40         new Cupboard();
41         table.f2(1);
42         cupboard.f3(1);
43     }
44     static Table table = new Table();
45     static Cupboard cupboard = new Cupboard();
46 }/*输出结果
47 Bowl(1)
48 Bowl(2)
49 Table()
50 f1(1)
51 Bowl(4)
52 Bowl(5)
53 Bowl(3)
54 Cupboard()
55 f1(2)
56 Creating new Cupboard() in main
57 Bowl(3)
58 Cupboard()
59 f1(2)
60 Creating new Cupboard() in main
61 Bowl(3)
62 Cupboard()
63 f1(2)
64 f2(1)
65 f3(1)
66 */

 这个程序运行的流程是这样的:

step1 : 加载public类StaticInitialization,在进入main()方法前发现有2个static成员,

step2 : 于是先执行第44行,初始化Table类型的table,这一过程就有2个步骤,先加载Table类,再创建table对象,

step3 : 程序跳转到第10行,加载类Table,发现有2个static成员,

step4 : 于是先执行第11行,初始化第1个static成员,Bowl类型的bowl1,这一过程就有2个步骤,先加载类Bowl,再创建bowl1对象,

step5 : 程序跳转到第1行,加载类Bowl,没有发现任何static成员,因而一下子加载完毕,

step6 : 接下来要创建并初始化bowl1对象,程序跳转到第2行的构造器,输出Bowl(1)

step7 : 继续初始化第2个static成员,程序跳转到第19行,发现又是Bowl类型,因为类Bowl已经加载过了,所以像step6一样,输出Bowl(2)

step8 : 类Table已经加载完毕,就继续创建并初始化table对象(C++和Java一样,对象的创建和初始化是同时进行的),程序跳转到第12行,先输出Table(),然后bowl2调用f1(1),输出f1(1)

step9 : 继续step1,执行第45行,初始化第2个static成员,初始化Cupboard类型的cupboard,因为类Cupboard还没有加载,于是也要包括2个步骤,

step10 : 程序跳转到第22行,加载类Cupboard,发现有2个static成员,都是Bowl类型的,因为类Bowl已经加载过了,于是直接创建Bowl类型的成员bowl4和bowl5,跟step6一样,依次输出Bowl(4)Bowl(5)

step11 : 类Cupboard加载完毕后,还要创建并初始化cupboard对象,但在使用第25行的构造器前,发现有个实例成员需要初始化,于是跳转到第23行去执行,因而先输出Bowl(3)

step12 : 类Cupboard已经加载完毕,就继续创建并初始化cupboard对象,因此依次输出Cupboard()f1(2)

step13 : 类StaticInitialization已经加载完毕,开始main()方法,先输出Creating new Cupboard() in main

step14 : 然后new Cupboard();调用第25行的构造器,像step11~12一样,依次输出Bowl(3)Cupboard()f1(2)

step15 : 重复step14, 依次输出Bowl(3)、Cupboard()、f1(2)

step16 : table对象调用f2(1),输出f2(1)

step17 : cupboard对象调用f3(1),输出f3(1)

 

5.7.2 显示初始化

显示初始化其实就是静态初始化块,它会在第一次使用类时就执行,而且仅执行一次,比如

public class Test {
    static int i;
    static {
        i = 47;
    }
}

 这里有个问题,能不能在静态初始化块定义一个变量呢?可以,但它将是一个局部变量,比如

public class Test {
    static {
        int i = 10;
        System.out.println(i);// 10
    }
    
    public static void main(String[] args) {
        System.out.println(Test.i);// 错误,i 不是成员,而是局部变量
    }
}

 

5.7.3 非静态实例初始化

也是初始化块,但这是实例初始化块,它在每次初始化对象时使用,比如

public class Test {
    char c;    
    {
        c = ‘Z‘;
        System.out.println(c);
    }
    
    public static void main(String[] args) {
        new Test();
        new Test();
    }
}/*输出
Z
Z
*/

 

5.8 数组

什么数组?面试时就被问到一个超经典的问题:数组是对象吗?

还是回到原书找答案:数组只是相同类型的、用一个标识符名称封装到一起的一个对象序列或基本类型数据序列。

英文是:An array is simply a sequence of either objects or primitives that are all the same type and are packaged together under one identifier name.

我认为,数组是对象,它是new出来的,它是在运行时就确定好的对象,它有固定的属性length,也可以直接调用类Object的方法toString()和equals()等方法,在内存中数组是放在堆里面的,而不是放在栈里面,数组名是一个引用,而不是一个基本类型的变量

public class Test {    
    public static void main(String[] args) {
        int[] intArray = new int[] {1, 3, 5, 7, 8, 10, 12};
        int[] numberArray;
        
        for (int i = 0; i < intArray.length; i++)
            print(intArray[i] + " ");
        System.out.println();
        
        numberArray = intArray;
        numberArray[0] = 0;
        numberArray[numberArray.length - 1] = 13;
        
        for (int i = 0; i < intArray.length; i++)
            print(intArray[i] + " ");
        System.out.println();
    }
}/*输出结果
1 3 5 7 8 10 12 
0 3 5 7 8 10 13
*/

可见数组名是一个引用,numberArray改变了数组的内容,intArray也会发生相应的变化,因为numberArray和intArray就是引用同一个数组的

数组的初始化有这么几种方式

// 初始化:第一种方式
int[] intArray1 = new int[3];
for (int i = 0; i < intArray1.length; i++)
    intArray1[i] = i;

// 初始化:第二种方式
int[] intArray2 = new int[] {1, 5, 9};

// 初始化:第三种方式
int[] intArray3 = {2, 4, 8};

 

可变参数列表

由于目前可变参数列表用得少,这里就不像原书那样子讲得那么详细了

用过C语言的printf函数就知道,它的参数个数要多少就有多少,Java也可以实现这样的可变参数列表,只要在类型后面加3个点就行了,比如

public class Test {
    public static void main(String[] args) {
        printArray("I", "Love", "Java");
    }
    
    public static void printArray(String... args) {
        for (String s : args)
            System.out.print(s + " ");
    }
}

这里的String...就像String数组一样使用了,不过传入的实际参数,只要是String类型的,个数要多少都行。

实际上mian方法中也可以这样写 main(String... args)

 

5.9 枚举类型

在Java中,枚举也是类,可以像类一样使用枚举

enum Week {
    SUNDAY, MONDAY, TUESDAY, WEDNESDAY, THURSDAY, FRIDAY, SATURDAY    
}

public class SimpleEnumUse {
    public static void main(String[] args) {
        Week myWeek = Week.SUNDAY;
        System.out.println("I love " + myWeek);
    }
}

 既然enum也是类,必然有它的方法,这里主要介绍toString(),ordinal()和values()

toString()用来显示某个enum实例的名字;ordinal()表示某个特定enum常量的声明顺序;values()是static方法,用来按照enum常量的声明顺序,产生由这些常量值构成的数组

enum Week {
    SUNDAY, MONDAY, TUESDAY, WEDNESDAY, THURSDAY, FRIDAY, SATURDAY    
}

public class EnumOrder {
    public static void main(String[] args) {
        for (Week w : Week.values())
            System.out.println(w.toString() + ", ordinal " + w.ordinal());
    }
}/*输出结果
SUNDAY, ordinal 0
MONDAY, ordinal 1
TUESDAY, ordinal 2
WEDNESDAY, ordinal 3
THURSDAY, ordinal 4
FRIDAY, ordinal 5
SATURDAY, ordinal 6
*/

之前讲到switch的判断因子是整型数值,其实还可以判断enum类型。Java中,switch的判断因子只能是整型数值或enum

enum Week {
    SUNDAY, MONDAY, TUESDAY, WEDNESDAY, THURSDAY, FRIDAY, SATURDAY    
}

public class Today {
    Week today;
    
    public static void main(String[] args) {
        Today td = new Today(Week.SUNDAY);
        switch (td.today) {
            case SUNDAY :
            case SATURDAY :
                System.out.println("双休日");
                break;
            case MONDAY :
            case TUESDAY :
            case WEDNESDAY :
            case THURSDAY :
            case FRIDAY :
                System.out.println("工作日");
                break;
            default :
                System.out.println("WRONG DAY");
        }
    }
    
    public Today(Week today) {
        this.today = today;
    }
}/*输出结果
双休日
*/

 

[Thinking in Java]第5章-初始化与清理

标签:

原文地址:http://www.cnblogs.com/cyninma/p/4471426.html

(0)
(0)
   
举报
评论 一句话评论(0
登录后才能评论!
© 2014 mamicode.com 版权所有  联系我们:gaon5@hotmail.com
迷上了代码!