Thinking in java 第5章 初始化与清理
5.0 序章
1. C中容易忽略初始化和清理,导致内存泄漏;C++中引入了构造器的概念;Java中也采用了构造器,并额外提供了“垃圾回收器”。
5.2 方法重载
1. 如果传入数据类型小于方法中声明的形式参数类型,实际数据类型就会被提升。char类型若没有恰好接受char参数的方法,就会被直接提升为int类型。
2. 如果传入数据类型大于方法中声明的形式参数类型,就必须采用强制转换,不然编译器会报错。
5.4 this关键字
1. 当需要返回对当前对象的引用时,可以写成return this; 同理也可写成传递参数。
2. static方法就是没有this的方法,在static方法的内部不能调用非静态方法(虽然可以在里面创建一个新对象再调用静态方法,不过没必要直接写一个非静态方法就好),反过来可以。
5.5 清理:终结处理和垃圾回收
1. 垃圾回收只与内存有关。无论是垃圾回收还是终结,都不保证一定会发生。如果JVM并为面临内存耗尽的情形,他是不会浪费时间去执行垃圾回收以恢复内存的。
2. 垃圾回收器的工作方式:
- 引用计数(仅用作说明):每个对象都含有一个引用计数器,当有引用连接至该对象时,引用计数加1。当引用离开作用域或被设置成null时减1。垃圾收集器会在含有全部对象的列表上遍历,当某个对象引用计数为0时,是放弃占用空间。
- 停止-复制:暂停程序,将所有存活的对象从当前堆复制到另一个堆,没有被复制的全是垃圾。新堆没有碎片。在处理大量短命的临时对象时很有帮助。如果空间碎片太多也会切换回此方式。
- 标记-清扫:没有复制工作。相当于对无向图进行遍历,清扫不可达的点。如果所有对象都很稳定,垃圾回收期的效率降低的话,就切换到此方式。
5.7 构造器初始化
1. 在类的内部,变量定义的先后顺序决定了初始化的顺序。即使变量定义散布于方法定义之间,它们仍旧会在任何方法(包括构造器)被调用之前得到初始化。
2. 初始化的顺序是先静态对象(如果它们尚未因前面的对象创建过程而被初始化),而后是“非静态”对象。
3. 一旦类中的静态方法/静态域被访问时(main也是),Java解释器必须查找类路径,以定位.class文件然后载入,此时有关静态初始化的所有动作都会执行。因此,静态初始化只在Class对象首次加载的时候进行一次。(例P95-96)
4. 去掉static的初始化为非静态实例初始化,这种语法对于支持“匿名内部类”的初始化时必须的,使得无论调用哪个显示构造器,某些操作都会发生。
5.8 数组初始化
1. 可变参数列表让重载过程变得复杂,如f(int... args){}与f(double... args){}同时存在时,调用f()会编译失败。可以在方法中加入一个非可变参数来解决该问题。
5.9 枚举类型
public enum Spiciness{
NOT, MILD, MEDIUM, HOT, FLAMING
}
1. 由于枚举类型的实例是常量,因此按照命名惯例都用大写字母表示。
2. ordinals()方法可以返回某个特定的enum常量的声明顺序;staic values()方法会按照声明顺序生成这些常量构成的数组。
3. 可以配合switch使用。
Spiciness degree;
switch(degree) {
case NOT:
...
case FLAMING:
default:
}
习题:
练习1:创建一个类,它包含一个未初始化的String引用。验证该引用被Java初始化成了null。
public class E5_1 {
String s;
}
class Main{
public static void main(String[] args) {
E5_1 e = new E5_1();
System.out.println(e.s);
}
}
/*
null
*/
练习2:创建一个类,它包含一个在定义时就被初始化了的String域,以及另一个通过构造器初始化的String域。这两种方式有何差异?
public class E5_2 {
String s1;
String s2 = "111";
}
public class Main {
public static void main(String[] args) {
E5_2 e = new E5_2();
System.out.println("s1 = " + e.s1 + ", s2 = " + e.s2);
e.s1 = "222";
System.out.println("s1 = " + e.s1 + ", s2 = " + e.s2);
}
}
/*
s1 = null, s2 = 111
s1 = 222, s2 = 111
*/
差异:s1先被初始化为null再被初始化为222,初始化了2次;而s2只被初始化了一次。
练习3:创建一个带默认构造器(即无参构造器)的类,在构造器中打印一条消息。为这个类创建一个对象。
public class E5_3 {
int a;
public E5_3() {
System.out.println("一个带默认构造器的类");
}
}
public class Main {
public static void main(String[] args) {
E5_3 e = new E5_3();
}
}
/*
一个带默认构造器的类
*/
练习4:为前一个练习中的类添加一个重载构造器,令其接受一个字符串参数,并在构造器中把你自己的信息和接受的参数一起打印出来。
public class E5_3 {
String s;
public E5_3() {
System.out.println("一个带默认构造器的类");
}
public E5_3(String s) {
this.s = s;
System.out.println("一个重载构造器 "+ s);
}
}
public class Main {
public static void main(String[] args) {
E5_3 e = new E5_3("aaa");
}
}
/*
一个重载构造器 aaa
*/
练习5:创建一个名为Dog的类,它具有重载的bark()方法。此方法应根据不同的基本数据类型进行重载,并根据被调用的版本,打印出不同类型的狗吠(barking)、咆哮(howling)等信息。编写main()来调用所有不同版本的方法。
public class Dog {
void bark() {
System.out.println("barking!");
}
void bark(String s) {
System.out.println("howling! "+s);
}
}
public class Main {
public static void main(String[] args) {
Dog dog = new Dog();
dog.bark();
dog.bark("tnk");
}
}
/*
barking!
howling! tnk
*/
练习6:修改前一个练习的程序,让两个重载方法各自接受两个类型的不同的参数,但二者顺序相反。验证其是否工作。
会正常工作。略。
练习7:创建一个没有构造器的类,并在main()中创建其对象,用以验证编译器是否真的自动加入了默认构造器。
意义不明。略。
练习8:编写具有两个方法的类,在第一个方法内调用第二个方法两次:第一次调用时不使用this关键字,第二次调用时使用this关键字——这里只是为了验证它是起作用的,你不应该在实践中使用这种方法。
public class E5_8 {
void f1() {
f2();
this.f2(); // 不推荐
}
void f2() {
System.out.println("this is f2");
}
}
练习9:编写具有2个(重载)构造器的类,并在第一个构造器中通过this调用第二个构造器。
public class E5_9 {
public E5_9() {
this("aaa");
}
public E5_9(String s) {
System.out.println("using " + s);
}
}
练习10:编写具有finalize()方法的类,并在方法中打印消息。在main()中为该类创建一个对象。试解释这个程序的行为。
方法已被取消,了解即可。
public class E5_10 {
boolean checkout=true;
void checkin(){
checkout=true;
}
protected void finalize(){
if(checkout) {
System.out.println("Error:checked out");
super.finalize();\\报错java.lang.Object中的finalize()已过时
}
}
}
public class Main {
public static void main(String[] args) {
E5_10 e=new E5_10();
e.checkin();
System.gc();
}
}
练习11:修改前一个练习的程序,让你的finalize()总会被调用。
同上。
练习12:编写名为Tank的类,此类的状态可以是“满的”或“空的”。其终结条件是:对象被清理时必须处于空状态。请编写finalize()以检验终结条件是否成立。在main()中测试Tank可能发生的几种使用方式。
修改一下boolean cheackout的判断即可。略。
练习13:验证前面段落中的语句。
class Cup{
Cup(int maker){
System.out.println("Cup("+maker+")");
}
void f(int maker){
System.out.println("f("+maker+")");
}
}
class Cups{
static Cup cup1;
static Cup cup2;
static {
cup1=new Cup(1);
cup2=new Cup(2);
}
Cups(){
System.out.println("Cups()");
}
}
public class Main {
public static void main(String[] args) {
System.out.println("Inside main()");
// Cups.cup1.f(99);
}
static Cups cup1 = new Cups();
static Cups cup2 = new Cups();
}
/*
Cup(1)
Cup(2)
Cups()
Cups()
Inside main()
*/
练习14:编写一个类,拥有两个静态字符串域,其中一个在定义处初期化,另一个在静态块中初期化。现在加入一个静态方法用于打印出两个字段的值。请证明他们都会在被使用之前完成初始化动作。
public class E5_14 {
static String a = "aaa";
static String b;
static{
b = "bbb";
}
void print() {
System.out.println("a = " + a);
System.out.println("b = " + b);
}
}
public class Main {
public static void main(String[] args) {
E5_14 e = new E5_14();
e.print();
}
}
/*
a = aaa
b = bbb
*/
练习15:编写一个含有字符串域的类,并采用实例初始化的方法初始化它。
public class E5_15 {
String s;
{
s = "aaa";
System.out.println("非静态实例初始化");
}
}
public class Main {
public static void main(String[] args) {
E5_15 e = new E5_15();
System.out.println(e.s);
}
}
/*
非静态实例初始化
aaa
*/
练习16:创建一个String对象数据(数组?),并为每一个元素赋值一个String,用for循环来打印该数组。
public class E_16 {
public static void main(String[] args) {
String[] strings = new String[] {"aaa", "bbb", "ccc"};
for(String s:strings) {
System.out.println(s);
}
}
}
/*
aaa
bbb
ccc
*/
练习17:创建一个类,它有一个接受一个String参数的构造器。在构造阶段,打印该参数。创建一个该类的引用数组,但是不实际创建对象赋值给该数组。当运行程序时,请注意来自对该构造器的调用中的初始化信息是否打印出来。
public class E5_17 {
String s;
public E5_17 (String s) {
this.s = s;
System.out.println("this is " + s);
}
}
public class Main {
public static void main(String[] args) {
E5_17[] es;
}
}
/*
*/
练习18:通过创建对象赋值给引用数组,从而完成前一个练习。
public class E5_17 {
String s;
public E5_17 (String s) {
this.s = s;
System.out.println("this is " + s);
}
}
public class Main {
public static void main(String[] args) {
E5_17[] es = new E5_17[3];
es[0] = new E5_17("aaa");
es[1] = new E5_17("bbb");
es[2] = new E5_17("ccc");
}
}
/*
this is aaa
this is bbb
this is ccc
*/
练习19:写一个类,他接受一个可变参数 String数组。 验证你可以向他传递一个用逗号分隔的String 列表, 或者String[]。
public class E5_19 {
static void print(String... strings) {
for(String s:strings) {
System.out.print(s + " ");
}
System.out.println();
}
}
public class Main {
public static void main(String[] args) {
String[] strings = new String[] {"aaa", "bbb", "ccc"};
E5_19.print(strings);
E5_19.print("ddd","eee","fff");
}
}
/*
aaa bbb ccc
ddd eee fff
*/
练习20:创建一个使用可变参数列表而不是普通main()语法的main().打印所产生的args数组的所有元素,并用不同的数量行参数来测试他。
意义不明。略。
练习21:创建一个enum,它包含纸币中最小面值的6种类型。通过values()循环并打印每一个值及其ordinal()。
public class E5_21 {
public enum Money{
YI_YUAN, WU_YUAN, SHI_YUAN, ER_SHI_YUAN, WU_SHI_YUAN
}
public static void main(String[] args) {
for(Money m:Money.values()) {
System.out.println(m + " : " + m.ordinal());
}
}
}
/*
YI_YUAN : 0
WU_YUAN : 1
SHI_YUAN : 2
ER_SHI_YUAN : 3
WU_SHI_YUAN : 4
*/
练习22:在前面的例子中,为enum写一个switch语句,对于每一个case,输出该特定货币的描述。
public class E5_22 {
public enum Money{
YI_YUAN, WU_YUAN, SHI_YUAN, ER_SHI_YUAN, WU_SHI_YUAN
}
public static void main(String[] args) {
for(Money m: Money.values()) {
switch (m) {
case YI_YUAN:
System.out.println("一元人民币");
break;
case WU_YUAN:
System.out.println("五元人民币");
break;
case SHI_YUAN:
System.out.println("十元人民币");
break;
case ER_SHI_YUAN:
System.out.println("二十元人民币");
break;
case WU_SHI_YUAN:
System.out.println("五十元人民币");
break;
}
}
}
}
/*
一元人民币
五元人民币
十元人民币
二十元人民币
五十元人民币
*/