这章包含两个大主题,一个对象初始化,一个垃圾回收。初始化的话题在第三章已经总结过,传送门 - 《Think in Java 读书笔记:第三章 - 操作符》(一开始就总结了对象初始化)。然后垃圾回收内容,以为比较有趣,又独立出来成一篇《Java垃圾回收初探》。所以剩下的是一些比较零碎的问题,比如this关键字,可变参数列表,枚举型,以及很少用到的finalize()清理函数。
对象初始化,包括构造函数在之前《Think in Java 读书笔记:第三章 - 操作符》这一章已经分析过。直接练习题。
对于无参数的默认构造函数,以及以参数类型区分的重载构造函数,设计上说不上完美,我完全可以需要好几个有相同参数类型,但完全不同的构造函数,这在java里就无法实现。但实际用起来,这样的设定还挺实用,目前为止,我没遇到过构造函数打架的问题。
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);
}
}
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!
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
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关键字,代表在类的内部,对当前对象的引用。static静态类是没有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!
}
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,但调用类中成员方法就不用。
C++需要手动释放内存,Java有垃圾回收器。我们不能决定什么东西在什么时候被清理。但Java还是给了我们一些工具,一定程度上控制垃圾的回收。
首先就是 System.gc()
,建议虚拟机现在执行一次垃圾回收。
然后就是这个 finalize()
函数。他用来定义当对象被回收的时候所需要做的操作。我们可以通过对基类finalize函数的重载,达到这个目的。
但Java仍然不保证当前对象一定会被回收,因为回收对象的扫描过程的控制权不在我们手上。由Java垃圾回收器决定哪些垃圾需要被回收。
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!
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垃圾回收初探》。作为一个系统的了解。
成员变量初始化的方法,主要有
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
关于数组,有两点我以前的误区,
Initialize.fiveStrArray = {"one","two","three","four","five"};
这样直接为数组赋值。而是必须显式地重新创建对象Initialize.fiveStrArray = new String[] {"one","two","three","four","five"};
。或者一个一个赋值,像这样Initialize.fiveStrArray[0]=new ArrayNode("one");
。Java的这个特性并不怎么合理。ArrayNode[] nodeArray = new ArrayNode[10];
。实际JAVA逻辑堆区什么对象也没有创建。只是JVM Stack里创建了一个包含10个null
引用的数组:{null,null,null,null,null,null,null,null,null,null}
。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
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
我认为这个特性多此一举,原来声明数组不是很清楚吗,现在反而容易引起混乱。不知道老程序猿怎么看。
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;
}
这题因为不知道钞票的面值是多少,我稍微改了下,改成游戏的难度等级。
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
的类文件。
//由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()
就是其中一个成员方法。
下面是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