你的位置:首页 > Java教程

[Java教程][Thinking in Java]第5章


5.1 用构造器确保初始化5.2 方法重载5.3 缺省构造器5.4 this关键字5.5 清理:终结处理和垃圾回收5.6 成员初始化5.7 构造器初始化5.8 数组初始化5.9 枚举类型

目录

 


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     else15       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 = 4738 String & int 参数39 默认构造器(无参)40 petalCount = 47 s = Hi41 */

从上面的程序可以归纳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     false31 char      [ ]32 byte      033 short      034 int       035 long      036 float      0.037 double     0.038 InitialValues  null39 */

 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 main57 Bowl(3)58 Cupboard()59 f1(2)60 Creating new Cupboard() in main61 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();  }}/*输出ZZ*/

 

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 0MONDAY, ordinal 1TUESDAY, ordinal 2WEDNESDAY, ordinal 3THURSDAY, ordinal 4FRIDAY, ordinal 5SATURDAY, 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;  }}/*输出结果双休日*/