摘要

这章包含两个大主题,一个对象初始化,一个垃圾回收。初始化的话题在第三章已经总结过,传送门 - 《Think in Java 读书笔记:第三章 - 操作符》(一开始就总结了对象初始化)。然后垃圾回收内容,以为比较有趣,又独立出来成一篇《Java垃圾回收初探》。所以剩下的是一些比较零碎的问题,比如this关键字,可变参数列表,枚举型,以及很少用到的finalize()清理函数。

构造函数

对象初始化,包括构造函数在之前《Think in Java 读书笔记:第三章 - 操作符》这一章已经分析过。直接练习题。

对于无参数的默认构造函数,以及以参数类型区分的重载构造函数,设计上说不上完美,我完全可以需要好几个有相同参数类型,但完全不同的构造函数,这在java里就无法实现。但实际用起来,这样的设定还挺实用,目前为止,我没遇到过构造函数打架的问题。

练习1,2

Exercise 1: (1) Create a class containing an uninitialized String reference. Demonstrate that this reference is initialized by Java to null. Exercise 2: (2) Create a class with a String field that is initialized at the point of definition, and another one that is initialized by the constructor. What is the difference between the two approaches?

指出一点,如果这里的构造函数是空的:public Initialize (){},那不必显式的写出来。

package com.ciaoshen.thinkinjava.chapter5;
import java.util.*;

public class Initialize {
    //fields
    //exercise 1,2
    public String nonInitialized = new String();
    public String initialized  = "I am auto-initialized!";
    public String constructorInitialized = new String();

    //default constructor
    public Initialize (){
        this.constructorInitialized = "Constructor initialize me now!";
    }

    /**
     *  Our main method
     *  @param args void
     */
    public static void main(String args[]){
        Initialize myObject = new Initialize();

        //exercise 1,2
        System.out.println(myObject.nonInitialized);
        System.out.println(myObject.initialized);
        System.out.println(myObject.constructorInitialized);
    }
}

//output:
//
//	I am auto-initialized!
//	Constructor initialize me now!

方法的重载

成员方法可以重名,只要参数类型不一样,就以不同的方法。甚至参数放的顺序不一样,也能区分方法。比如method(int i, String s)method(String s, int i)是不同的两个方法。但写代码的时候别这么干。会把自己搞糊涂。

另外,想要以不同的返回值区分方法,达到重载,是行不通的!这是因为光是返回值不同,编译器是无法区分不同的。比如我们忽略返回值,直接调用:

f();	//忽略返回值的调用

这时候如果几个参数相同,只是返回值不同的重载方法f(),,编译器没法知道到底用哪个。

参数的“自动提升”和“窄化转型”

java函数传递参数的时候不是一定需要完全符合的类型。对基本型参数,我们传入的参数基本型在我们定义的另一个基本型里放得下,java会做基本型的“自动提升”。比如只有这一个构造函数setI(int inputInt),如果我构造时传入的是short,系统会自动找到setI(int inputInt)。但如果我构造是传入long型,因为int里放不下long,我必须显式地把long先“窄化转型”成int,再调用构造函数setI(int inputInt),不然系统会抛出异常。

public class Initialize {
    //primitive type field
    public int i;

	//成员方法参数int型
    public void setI (int inputInt){
        this.i=inputInt;
    }

    public static void main(String args[]){  
        Initialize myObject = new Initialize();
        int i = 10;
        short s = 10;
        long l = 10L;
        mObject.setI(i);   //函数参数就是int,int当然没问题。
        myObject.setI(s);   //传short,java会自动调用带int参数的函数,因为int放得下short。
        myObject.setI(l);  //传long就不行了。必须显式地窄化转型成int。
        System.out.println(intObject.i);
        System.out.println(shortObject.i);
        System.out.println(longObject.i);
    }
}

练习 3,4

Exercise 3: (1) Create a class with a default constructor (one that takes no arguments) that prints a message. Create an object of this class. Exercise 4: (1) Add an overloaded constructor to the previous exercise that takes a String argument and prints it along with your message.

    //default constructor
    public Initialize (){
        this.constructorInitialized = "Hello World!";
        System.out.println(this.constructorInitialized);
    }

    //constructor with param String
    public Initialize (String name){
        this.constructorInitialized = "Hello "+name+"!";
        System.out.println(this.constructorInitialized);
    }

    public static void main(String args[]){
        //exercise 3
        Initialize defaulObject = new Initialize();
        Initialize shenObject = new Initialize("shen");      
    }

//output:
//Hello World!
//Hello shen!

练习 5,6

Exercise 5: (2) Create a class called Dog with an overloaded bark( ) method. This method should be overloaded based on various primitive data types, and print different types of barking, howling, etc., depending on which overloaded version is called. Write a main( ) that calls all the different versions. Exercise 6: (1) Modify the previous exercise so that two of the overloaded methods have two arguments (of two different types), but in reversed order relative to each other. Verify that this works.

package com.ciaoshen.thinkinjava.chapter5;

import java.util.*;
import com.ciaoshen.thinkinjava.chapter3.*;


/**
 *  Inherit the Dog class in Chapter 3
 */
public class HowlingDog extends com.ciaoshen.thinkinjava.chapter3.Dog {

    public void bark(){
        System.out.println("barking");
    }

    public void bark(String who){
        System.out.println("howling to "+who);
    }

    public void bark(String who, int times){
        for(int i=0;i<times;i++){
            System.out.println("howling to "+who);
        }
    }

    public void bark(int times, String who){
        for(int i=0;i<times;i++){
            System.out.print("howling ");
        }
        System.out.println("to "+who);
    }

        public static void main(String args[]){
        HowlingDog puppy = new HowlingDog();
        puppy.bark();
        puppy.bark("shen");
        System.out.println("");
        puppy.bark("shen",5);
        puppy.bark(5,"shen");
    }
}

//	output:
barking
howling to shen

howling to shen
howling to shen
howling to shen
howling to shen
howling to shen
howling howling howling howling howling to shen

练习 7

Exercise 7: (1) Create a class without a constructor, and then create an object of that class in main( ) to verify that the default constructor is automatically synthesized.

把第5,6题答案中HowlingDog类对com.ciaoshen.thinkinjava.chapter3.Dog类的继承去掉,就没有构造函数了。但程序照样能跑。

this关键字

this关键字,代表在类的内部,对当前对象的引用。static静态类是没有this的。

把this当参数传递

this关键字我以前没试过的一种用法:当参数传递给别的方法。下面的代码从设计的角度说还不错,

class Person {
  public void eat(Apple apple) {
    Apple peeled = apple.getPeeled();
    System.out.println("Yummy");
  }
}
class Peeler {
  static Apple peel(Apple apple) {
    // ... remove peel
    return apple; // Peeled
  }
}
class Apple {
  Apple getPeeled() { return Peeler.peel(this); }
}
public class PassingThis {
  public static void main(String[] args) {
    new Person().eat(new Apple());
  }
} /* Output:
Yummy

在构造器中调用另一个构造器

在构造函数里,用this关键字,调用另一个构造器,java也能认识。

 Flower(String ss) {
    print("Red");
s = ss; }
  Flower(String s, int petals) {
    this(petals);
	//!    this(s); // Can’t call two!
  }

编程风格: 不必到处用this

Some people will obsessively put this in front of every method call and field reference, arguing that it makes it “clearer and more explicit.” Don’t do it. There’s a reason that we use high-level languages: They do things for us. If you put this in when it’s not necessary, you will confuse and annoy everyone who reads your code, since all the rest of the code they’ve read won’t use this everywhere. People expect this to be used only when it is necessary. Following a consistent and straightforward coding style saves time and money.

关于编程风格,这段注脚让我们避免在不必要的时候也全加上this关键字。我一般在字段前面加this,但调用类中成员方法就不用。

finalize()函数

C++需要手动释放内存,Java有垃圾回收器。我们不能决定什么东西在什么时候被清理。但Java还是给了我们一些工具,一定程度上控制垃圾的回收。

首先就是 System.gc(),建议虚拟机现在执行一次垃圾回收。

然后就是这个 finalize() 函数。他用来定义当对象被回收的时候所需要做的操作。我们可以通过对基类finalize函数的重载,达到这个目的。

但Java仍然不保证当前对象一定会被回收,因为回收对象的扫描过程的控制权不在我们手上。由Java垃圾回收器决定哪些垃圾需要被回收。

练习 10,11

Exercise 10: (2) Create a class with a finalize( ) method that prints a message. In main( ), create an object of your class. Explain the behavior of your program. Exercise 11: (4) Modify the previous exercise so that your finalize( ) will always be called.

还是利用前面几题的的HowlingDog类,在垃圾回收处理的时候,在finalize()里检查小狗是否狂吠。如果不狂吠,提醒我它不是恶犬,不要清除。

package com.ciaoshen.thinkinjava.chapter5;
import java.util.*;
import com.ciaoshen.thinkinjava.chapter3.*;

/**
 *  Inherit the Dog class in Chapter 3
 *  they have name and says
 */
public class HowlingDog extends com.ciaoshen.thinkinjava.chapter3.Dog {

    //tag to notice if the dog howls
    public boolean itHowls = false;

    //constructor inherit from Dog class in Chapter3
    public HowlingDog(String inName, String inSays){
        super(inName,inSays);
    }

    public void bark(){
        System.out.println(this.name+" is only barking");
    }

    public void bark(int times, String who){
        System.out.print(this.name+" ");
        for(int i=0;i<times;i++){
            System.out.print(this.says);
        }
        System.out.println(" to "+who);
        this.itHowls=true;
    }

    // only clean the howling dogs
    public void finalize(){
        if(! this.itHowls){
            System.out.println("Error, please do not kill "+this.name+", it doesn't howl!");
            //super.finalize();     //在基类finalize()中加入一些异常处理流程。在异常处理的一章专门练习。
        }
    }

    /**
     *  main
     *  @param args void.
     */
    public static void main(String args[]){
        HowlingDog puppy = new HowlingDog("spots","Ruff!");	//恶犬
        puppy.bark(5,"shen");
        puppy = new HowlingDog("titi","JOJOJO");
        System.gc();

        HowlingDog nicePuppy = new HowlingDog("cruffy","Wurf!");	//小受犬
        nicePuppy.bark();
        nicePuppy = new HowlingDog("tata","BiuBiu");
        System.gc();
    }
}

output:

spots Ruff!Ruff!Ruff!Ruff!Ruff! to shen
cruffy is only barking
Error, please do not kill cruffy, it doesn't howl!

练习 12

package com.ciaoshen.thinkinjava.chapter5;
import java.util.*;

public class Tank {
    //check field
    protected boolean isEmpty = false;
    protected int bullet = 100;
    protected String name = new String();

    //default constructor
    public Tank(String name){
        this.name=name;
    }

    //shot one bullet
    public void shot(){
        this.bullet --;
        if (this.bullet==0){
            this.isEmpty=true;
        }
    }

    //finish the bullet
    public void showHand(){
        this.bullet=0;
        this.isEmpty=true;
    }

    //check if the tank is empty
    public void finalize(){
        if(!this.isEmpty){
            System.out.println("Error! "+this.name+" still has bullet!!");
        }
        //super.finalize();
    }

    /**
     *  main method
     *  @param args void
     */
    public static void main(String args[]){
        Tank tank1 = new Tank("Tank1");
        Tank tank2 = new Tank("Tank2");

        tank1.shot();
        tank2.showHand();
        //free the reference
        tank1 = new Tank("Tank3");
        tank2 = new Tank("Tank4");
        //force to run garbage collection
        System.gc();
    }
}

//Output:	Error! Tank1 still has bullet!!

垃圾回收

本章的垃圾回收内容,为了方便查阅,独立出来成一篇《Java垃圾回收初探》。作为一个系统的了解。

成员变量初始化

成员变量初始化的方法,主要有

  1. 直接声明
  2. 构造函数
  3. block
  4. 调用函数 这四种方法,在第三章的时候已经做过总结,当时没想到这是在第五章讲的内容。这里我就不重复了。传送门 - 《Think in Java 读书笔记:第三章 - 操作符》(一开始就总结了对象初始化)

练习 14,15

Exercise 14: (1) Create a class with a static String field that is initialized at the point of definition, and another one that is initialized by the static block. Add a static method that prints both fields and demonstrates that they are both initialized before they are used.

Exercise 15: (1) Create a class with a String that is initialized using instance initialization.

public class Initialize {
    public String constructorInitialized = new String();

    //exercise 14
    protected static int autoIni;
    protected static String staticBlockIni;
    static {
        System.out.println("Hey autoIni!  "+"autoIni="+autoIni);
        staticBlockIni="I was born!";
        System.out.println("Hello staticBlockIni!  staticBlockIni:"+staticBlockIni);
    }

    //exercise 15
    protected String exercise15;
    //ini block
    {
        exercise15="ini block gives me life";
        System.out.println(exercise15);
    }

    //default constructor
    public Initialize (){
        this.constructorInitialized = "constructor gives me life";
        System.out.println(this.constructorInitialized);
    }

    public static void main(String args[]){
        //exercise 14
        Initialize.salutStatic();

        //exercise 15
        Initialize exercise15=new Initialize();
    }

output:

Hey autoIni!  autoIni=0
Hello staticBlockIni!  staticBlockIni:I was born!
ini block gives me life
constructor gives me life

数组的初始化

关于数组,有两点我以前的误区,

  1. 练习16为了说明,除了在声明数组的时候,Java不允许像Initialize.fiveStrArray = {"one","two","three","four","five"};这样直接为数组赋值。而是必须显式地重新创建对象Initialize.fiveStrArray = new String[] {"one","two","three","four","five"};。或者一个一个赋值,像这样Initialize.fiveStrArray[0]=new ArrayNode("one");。Java的这个特性并不怎么合理。
  2. 练习17,18点出了另一个误区。当我们创建一个新数组的时候,比如ArrayNode[] nodeArray = new ArrayNode[10];。实际JAVA逻辑堆区什么对象也没有创建。只是JVM Stack里创建了一个包含10个null引用的数组:{null,null,null,null,null,null,null,null,null,null}

练习 16

Create an array of String objects and assign a String to each element. Print the array by using a for loop.

public class ArrayIni {

    //for exercise 16
    //this form is only allowed in initializing block
    protected static String[] fiveStrArray = {"1","2","3","4","5"};

    //exercise 16
    public static void exercise16(){
        for(String ele : Initialize.fiveStrArray){
            System.out.println(ele);
        }
        //the following line is wrong, we can not asign an array like that
        //Initialize.fiveStrArray = {"one","two","three","four","five"};
        Initialize.fiveStrArray = new String[] {"one","two","three","four","five"};
        for(String ele : Initialize.fiveStrArray){
            System.out.println(ele);
    }
    /**
     *  main method
     *  @param args void
     */
    public static void main(String args[]){
        ArrayIni.exercise16();
    }
}

output:

1
2
3
4
5
one
two
three
four
five

练习 17,18

Exercise 17: (2) Create a class with a constructor that takes a String argument. During construction, print the argument. Create an array of object references to this class, but don’t actually create objects to assign into the array. When you run the program, notice whether the initialization messages from the constructor calls are printed.

Exercise 18: (1) Complete the previous exercise by creating objects to attach to the array of references.

public class ArrayNode {

    //nothing in fields

    //constructor only print the parameter
    public ArrayNode(String inParam){
        System.out.println("I am "+inParam+"!");
    }

    /**
     *  main method
     *  @param args void
     */
    public static void main(String args[]){
        ArrayNode[] nodeArray = new ArrayNode[10];  //nothing happend here
        nodeArray[0]=new ArrayNode("toto");     //output->toto
        nodeArray[1]=new ArrayNode("titi");     //output->titi
        nodeArray[2]=new ArrayNode("tata");     //output->tata
    }
}

可变参数列表

因为Java所有类都继承自一个Object基类。所以,要是我们为函数传递一个Object数组Object[],就可以接受所有类型的参数。这就是所谓的可变参数列表。看下面这个例子:

class A {}
public class VarArgs {
  //Object[] args 基类数组,由于泛型,可以接受任何类型参数
  static void printArray(Object[] args) {
    for(Object obj : args)
      System.out.print(obj + " ");
    System.out.println();
  }
  public static void main(String[] args) {
    //任何类型参数,都游刃有余。甚至基本型,也会触发Auto boxing
    printArray(new Object[]{
      new Integer(47), new Float(3.14), new Double(11.11)
    });
    printArray(new Object[]{"one", "two", "three" });
    printArray(new Object[]{new A(), new A(), new A()});
  }
}
// Output:
//	47 3.14 11.11
//	one two three
//	A@1a46e30 A@3e25a5 A@19821f		//空对象,print()只打印方法名,后跟@他的地址。

但可变参数列表可能的一个问题是:会导致重载的麻烦。因为可变参数列表会覆盖其他所有特定参数类型的重载方法。

  static void f(float i, Character... args) {
    System.out.println("first");
  }
  //可变参数列表会覆盖其他所有特定参数类型的重载方法
  static void f(Character... args) {
    System.out.print("second");
  }

解决的办法是,只在需要的特定参数类型后面附加可变参数类型。千万不要愣头愣脑直接一个可变参数类型贴上去。

  static void f(float i, Character... args) {
    System.out.println("first");
  }
  static void f(char c, Character... args) {
    System.out.println("second");
  }

J2SE5以后引入一个新特性,可以更简便地使用可变参数列表:

public class NewVarArgs {
  //定义参数的时候这样写Object... args
  static void printArray(Object... args) {
    for(Object obj : args)
      System.out.print(obj + " ");
    System.out.println();
  }
  public static void main(String[] args) {
    //后面调用方法的时候就不用显式地声明基类数组**`new Object[]`**了。可以直接给参数。
    printArray(new Integer(47), new Float(3.14),
      new Double(11.11));
    printArray(47, 3.14F, 11.11);
    printArray("one", "two", "three");
    printArray(new A(), new A(), new A());
    // Or an array:
                                      Initialization & Cleanup 137
     printArray((Object[])new Integer[]{ 1, 2, 3, 4 });
    printArray(); // Empty list is OK
  }
}

//Output:
//	47 3.14 11.11
//	47 3.14 11.11
//	one two three
//	A@1bab50a A@c3c749 A@150bd4d 1234

我认为这个特性多此一举,原来声明数组不是很清楚吗,现在反而容易引起混乱。不知道老程序猿怎么看。

练习 19

Write a method that takes a vararg String array. Verify that you can pass either a comma-separated list of Strings or a String[] into this method.

public class ArrayIni {

    //exercise 19: Variable arguments list
    public static void exercise19(Object[] args){
        for(Object ele : args){
            System.out.print(ele+" ");
        }
    }

    /**
     *  main method
     *  @param args void
     */
    public static void main(Object[] args){
        ArrayIni.exercise19(new String[] {"one","two","three","four"});		//output: one two three four
        String[] myStr = new String[] {"a","b","c","d"};
        ArrayIni.exercise19(myStr);		//output: a b c d
        //exercise 20

    }

枚举型

定义枚举类型:用enum关键字开始,不同的枚举在大括号里用逗号隔开。

enum RainbowColor { RED, ORANGE, YELLOW, GREEN, CYAN, BLUE, PURPLE }

例子中定义了一个名为RainbowColor的枚举型,他是由赤橙黄绿青蓝紫,七个具名值组成的有限集合。要使用RainbowColor这个枚举型,需要创建指向它具体值的一个引用:

//枚举型有套嵌的特性
//引用指向集合中的某个成员,但引用具有相同的类型RainbowColor
RainbowColor firstColor = RainbowColor.RED;
RainbowColor secondColor = RainbowColor.ORANGE;

System.out.println(firstColor);
System.out.println(secondColor);

枚举型和switch语句是绝配,它的有限常量集合可以用在switch语句里。解决了switch语句只接受基本型数据的尴尬。

 // 定义一周七天的枚举类型			
 public enum WeekDayEnum { Mon, Tue, Wed, Thu, Fri, Sat, Sun }

 // 读取当天的信息
 WeekDayEnum today = readToday();

 // 根据日期来选择进行活动
 switch(today) {
  Mon: do something; break;
  Tue: do something; break;
  Wed: do something; break;
  Thu: do something; break;
  Fri: do something; break;
  Sat: play sports game; break;
  Sun: have a rest; break;
 }

练习 21,22

这题因为不知道钞票的面值是多少,我稍微改了下,改成游戏的难度等级。
Exercise 21: (1) Create an enum of the least-valuable six types of paper currency. Loop through the values( ) and print each value and its ordinal( ).
Exercise 22: (2) Write a switch statement for the enum in the previous example. For each case, output a description of that particular currency.

从这道题能看出,枚举型本身就是一个类。比如我创建了一个包含EASY, NORMAL, HARD, VERYHARD四个常量的枚举型,名叫Difficulty。声明的语句,完全不用放在class Enum的大括号里。而是可以独立地放在外面。编译完成以后,会在Java包里多出一个Difficulty.class的类文件。 enumDifficulty

//由Difficulty.class反向编译生成的代码
Compiled from "Enum.java"
final class com.ciaoshen.thinkinjava.chapter5.Difficulty extends java.lang.Enum<com.ciaoshen.thinkinjava.chapter5.Difficulty> {
  public static final com.ciaoshen.thinkinjava.chapter5.Difficulty EASY;
  public static final com.ciaoshen.thinkinjava.chapter5.Difficulty NORMAL;
  public static final com.ciaoshen.thinkinjava.chapter5.Difficulty HARD;
  public static final com.ciaoshen.thinkinjava.chapter5.Difficulty VERYHARD;
  private static final com.ciaoshen.thinkinjava.chapter5.Difficulty[] $VALUES;
  public static com.ciaoshen.thinkinjava.chapter5.Difficulty[] values();
  public static com.ciaoshen.thinkinjava.chapter5.Difficulty valueOf(java.lang.String);
  private com.ciaoshen.thinkinjava.chapter5.Difficulty();
  static {};
}

包里的Enum.java是我写的源代码文件,Enum.class是生成的字节码文件。多出来的两个东西,一个Difficulty.class就是我们关心的枚举型Difficulty的字节码,还有一个Enum$1.class是switch语句产生的另一个字节码文件。用javap -private反向编译,可以看到java.lang.Enum<T>实际是个泛型集合,Difficulty类型实际是从java.lang.Enum<Difficulty>继承下来的子类。这是一个套嵌的结构。Difficulty型本身就是Difficulty型组成的枚举集合的子类。也就是它自己的集合就是他本身。有点像LinkedList每个节点都包含一个指向另一个节点的指针的感觉。下图是Java Doc里Enum的说明,Class Enum<E extends Enum<E>>很明显是个套嵌结构。21题用到的ordinal()就是其中一个成员方法。 enum1 enum2

下面是21,22题的代码:

/**
 *  Enum type
 *  @author wei.shen@iro.umontreal.ca
 *  @version 1.0
 */

package com.ciaoshen.thinkinjava.chapter5;
import java.util.*;

//enum class
enum Difficulty {EASY, NORMAL, HARD, VERYHARD}

/**
 *  our class
 */
public class Enum {

    //exercise 22
    public static void interpreter(Difficulty diff){
        switch(diff){
            case EASY:
                System.out.println("Easy is for the beginner!"); break;
            case NORMAL:
                System.out.println("Normal is for those who have some experience!"); break;
            case HARD:
                System.out.println("Hard is for good players!"); break;
            case VERYHARD:
                System.out.println("Very hard is for the super master!"); break;
        }
    }

    //exercise 21
    public static void printEnumValue(){
        for(Difficulty d : Difficulty.values()){
            System.out.println("Value: "+d+";   Order: "+d.ordinal());
        }
    }

    /**
     *  main method
     *  @param args void
     */
    public static void main(String[] args){
    	//exercise 22
        Enum.interpreter(Difficulty.EASY);
        Enum.interpreter(Difficulty.NORMAL);
        Enum.interpreter(Difficulty.HARD);
        Enum.interpreter(Difficulty.VERYHARD);
        //exercise 21
        Enum.printEnumValue();
    }
}

output:

//exercise 22
Easy is for the beginner
Normal is for those who have some experience
Hard is for good players
Very hard is for the super master

//exercise 21
Value: EASY;   Order: 0
Value: NORMAL;   Order: 1
Value: HARD;   Order: 2
Value: VERYHARD;   Order: 3