Java编程思想读书笔记——第五章:初始化与清理

第五章 初始化与清理

初始化和清理正是涉及安全的两个问题,Java中也引入了构造器,并额外提供了垃圾回收器

5.1 用构造器确保初始化

  • 构造方法会在new对象的时候,也就是我们调用对象之前去执行一些初始化的操作
  • 那么我们也可以在构造方法中去传参
  • 如果某一种构造器是类中唯一的构造器,编译器不会允许你以其他任何方式创建该对象

练习1、创建一个类,初始化String为null

public class E01_StringRefInitialization {   
    String s;   
    public static void main(String args[]) {     
        E01_StringRefInitialization sri = new E01_StringRefInitialization();             
        System.out.println("sri.s = " + sri.s);   
    } 
}

练习2、创建一个类,它包含一个在定义时就被初始化了的String域,以及另一个通过构造器初始化的String域

public class E02_StringInitialization {   
    String s1 = "Initialized at definition";   
    String s2;   
    public E02_StringInitialization(String s2i) {     
        s2 = s2i;   
    }   
    public static void main(String args[]) {     
        E02_StringInitialization si = new E02_StringInitialization("Initialized at construction");     
        System.out.println("si.s1 = " + si.s1);     
        System.out.println("si.s2 = " + si.s2);   
    } 
}

5.2 方法重载

  • 相同的词可以表达多种不同的含义——它们被重载了
  • 比如我创建了一个类,它是可以有多个构造方法的

5.2.1 区分重载方法

  • 每个重载方法都必须有一个独一无二的参数类型列表,顺序不一样也是可以的

5.2.2 涉及基本类型的重载

  • 传入的数据类型小于方法中参数的数据类型,会被提升
  • 否则,编译器会报错

5.2.3 以返回值区分重载方法

  • 只根据返回值来区分是不可以的
  • void eat() { }
  • int eat() { return 1; }
  • 如果我这样调用方法:
  • eat();
  • 根本无法确定我调用的是哪个方法

5.3 默认构造器

1、如果你的类中没有构造器,编译器会为你创建一个默认构造器

2、如果已经定义了一个构造器,编译器就不会为你自动创建默认构造器

  • User(int i) { }
  • User(String s) { }
  • 如果这样写编译器就会报错:
  • User user = new User();
  • 如果真想这么写,自己在定义一个什么参数都没有的构造方法就行

练习3、创建一个带默认构造器的类,在构造器中打印一条消息,为这个类创建一个对象

public class E03_DefaultConstructor {   
    E03_DefaultConstructor() {     
        System.out.println("Default constructor");   
    }   
    public static void main(String args[]) {     
        new E03_DefaultConstructor();   
    } 
} 

练习4、为前一个练习添加一个重载构造器,接受一个字符串参数

public class E04_OverloadedConstructor {   
    E04_OverloadedConstructor() {     
        System.out.println("Default constructor");   
    }   
    E04_OverloadedConstructor(String s) {     
        System.out.println("String arg constructor");     
        System.out.println(s);   
    }   
    public static void main(String args[]) {     
        // Call default version:     
        new E04_OverloadedConstructor();     
        // Call overloaded version:     
        new E04_OverloadedConstructor("Overloaded");   
    } 
}

练习5、创建一个名为Dog的类,它具有重载的bark()方法

class Dog {   
    public void bark() {     
        System.out.println("Default bark!");   
    }   
    public void bark(int i) {     
        System.out.println("int bark = howl");   
    }   
    public void bark(double d) {     
        System.out.println("double bark = yip");   
    }   
    // Etc. ... 
} 
 
public class E05_OverloadedDog {   
    public static void main(String args[]) {     
        Dog dog = new Dog();     
        dog.bark();     
        dog.bark(1);     
        dog.bark(1.1);   
    } 
}

练习6、在前一个练习的基础上接受两个类型不同的参数,并且来个顺序相反的,验证一下是否正常工作

class Dog2 { 
    public void bark(int i, double d) {     
        System.out.println("int, double bark");   
    }   
    public void bark(double d, int i) {     
        System.out.println("double, int bark");   
    } 
} 
 
public class E06_SwappedArguments {   
    public static void main(String args[]) {     
        Dog2 dog = new Dog2();     
        dog.bark(1, 2.2);     
        dog.bark(2.2, 1);   
    } 
}
// 是正常工作的

练习7、创建一个没有构造器的类,并在main()中创建其对象

public class E07_SynthesizedConstructor {   
    public static void main(String args[]) {     
    // Call the synthesized default constructor     
    // for this class:     
    new E07_SynthesizedConstructor();   
    } 
}

5.4 this关键字

  • 我们常用this的情况就是将构造器参数的值赋给成员变量
  • this只能在方法内部使用,表示调用方法的那个对象的引用,其实说简单点代表的就是此类中的对象,这么说可能不太严谨
  • 通过return关键字返回this,那么就可以一直调用各种方法,点xx点xx点xx,一直点下去,那么就可以实现如下代码:
boy.setName()
   .setSex()
   .setHeight()
   .setBodyWeight();

练习8、调用方法,使用this,和不使用this的区别

public class E08_ThisMethodCall {   
    public void a() {     
        b();     
        this.b();   
    }   
    public void b() {     
        System.out.println("b() called");   
    }   
    public static void main(String args[]) {     
        new E08_ThisMethodCall().a();   
    } 
}
// 用不用this都一样,都是起作用的

5.4.1 在构造器中调用构造器

  • 在一个构造器中调用另一个构造器,避免重复代码,可以使用this关键字
  • 只能调用一个构造器
  • 必须将要调用的构造器置于最起始处,否则会报错
  • 除构造器外,编译器禁止在其他地方调用构造器
  • 参数名称和成员变量名称一模一样,为了避免歧义,使用this关键字可以解决问题
  • 不要把this放在不必要的地方,比如在方法中调用方法,成员变量和构造器参数的名称并不一样,那就根本不需要使用this

练习9、在构造器中调用构造器

public class E09_ThisConstructorCall {   
    public E09_ThisConstructorCall(String s) {     
        System.out.println("s = " + s); 
    }   
    public E09_ThisConstructorCall(int i) {    
        this("i = " + i);   
    }   
    public static void main(String args[]) {     
        new E09_ThisConstructorCall("String call");     
        new E09_ThisConstructorCall(47);   
    } 
}

5.4.2 static的含义

  • static方法不能调用非静态方法,起码是不能直接调用本类的非静态方法
  • 如果实在想要实现,那么也可以在static方法中创建其自身的对象,然后通过这个引用去调用

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

  • 垃圾回收不一定都会回收,垃圾回收只知道释放那些经由new分配的内存
  • Java允许在类中定义一个名为finalize(),Java中没有提供“析构函数”相似的概念,也就是直接销毁对象的方法
  • 在Java中要想做类似的清理工作,需要在finalize()中加入某些操作
  • 要记住的三点:
  1. 对象可能不被垃圾回收
  2. 垃圾回收并不等于“析构”
  3. 垃圾回收只与内存有关

5.5.1 finalize()的用途何在

  • finalize()尽量避免使用

5.5.2 你必须实施清理

  • 在C++中,如果在栈上创建对象(Java中不允许),此对象作用域的末尾处,比方说函数结尾处,需要手动调用delete方法去清理,如果忘记了,那么就内存泄漏了
  • Java中不允许在栈上创建对象,必须使用new创建对象,垃圾回收器会帮你回收
  • 如果JVM并未面临内存耗尽的情形,它是不会浪费时间去执行垃圾回收以恢复内存的

5.5.3 终结条件

举个例子来说吧,我们在打开某个视频文件的同时,想去删除这个文件,系统会提示你,正在使用该文件,请关闭后再试

finalize可以去发现这种情况

finalize,意为:使结束

练习10:编写具有finalize()方法的类

public class E10_FinalizeCall {   
    protected void finalize() {     
        System.out.println("finalize() called");   
    }   
    public static void main(String args[]) {     
        new E10_FinalizeCall();   
    } 
}
// 结果finalize并没有执行
// 您可能不会看到调用终结器,因为程序通常不会为收集器生成足够的垃圾来运行。

练习11、修改前一个练习,使得finalize总会被调用

public class E11_FinalizeAlwaysCalled {   
    protected void finalize() {     
        System.out.println("finalize() called");   
    }   
    public static void main(String args[]) {     
        new E11_FinalizeAlwaysCalled();     
        System.gc();     
        System.runFinalization();   
    } 
}

// 对这些方法的调用只是一个请求; 它不能确保终结器实际运行 
// 最终,没有任何保证会调用finalize()

练习12、编写Tank类,此类的状态时“满的”或者“空的”,对象被清理时必须处于空状态

class Tank {   
    static int counter;   
    int id = counter++;   
    boolean full;   
    public Tank() {     
        System.out.println("Tank " + id + " created");     
        full = true;   
    }
   
    public void empty() { 
        full = false; 
    }
   
    protected void finalize() {     
        if(full)       
            System.out.println("Error: tank " + id + " must be empty at cleanup");     
        else       
            System.out.println("Tank " + id + " cleaned up OK");   
    }
   
    public String toString() { 
        return "Tank " + id; 
    }
 
} 

 
public class E12_TankWithTerminationCondition {   
    public static void main(String args[]) {     
        new Tank().empty();     
        new Tank();     
        // Don't empty the second one     
        System.gc(); 
        // Force finalization?     
        System.runFinalization();   
    } 
}

// 最后修改条件为:
// 调用empty()将full置为false才会执行finalize()方法,为空状态才会被清理
// 实际情况也是这样的,只有对象没有被引用才会被回收

5.5.4 垃圾回收器如何工作

  1. Java中所有对象都会在堆上创建,基本类型是在栈中
  2. 释放会影响分配
  3. Java从堆分配空间的速度,可以和其他语言从栈上分配空间的速度向媲美

几种技术:

1、引用计数:常用来说明垃圾收集的工作方式,但似乎从未被应用于任何一种Java虚拟机实现中

缺点:

  1. 循环引用,会导致引用计数一直大于0,回收不了该对象
  2. 程序一直在不断的读取计数,造成了不必要的内存访问和修改
  3. 在多线程环境下,很难保证这些计数是线程安全的

2、停止—复制:去遍历栈中的引用,将活的对象复制到另一个堆

缺点:

  1. 在两个堆之间切换,多了一倍空间
  2. 在没有垃圾的时候也会去复制
  3. 不是在后台进行的,需要暂停运行程序

3、标记—清扫:一样去遍历栈中的引用,给活的对象做标记,全部标记结束,释放那些没被标记的对象

缺点:

  1. 剩下的堆空间是不连续的
  2. 同样也需要暂停程序

垃圾回收的知识,现在就大体先了解一些吧,之后肯定还会讲更多的

5.6 成员初始化

如果成员变量是基本类型,都会被默认初始化,如果定义一个对象引用未初始化,默认值为null

5.6.1 指定初始化

直接赋初值,这种方法会导致每个对象都会有相同的初值

5.7 构造器初始化

public class Pengboboer {
    int length;
    Pengboboer() {
        length = 18;
    }
}
// 那么length先被置为0,然后被置为18
// 无法阻止自动初始化的进行

5.7.1 初始化顺序

无论变量在什么位置,总是先初始化变量,变量会在任何方法(包括构造器)调用之前被初始化,然后是构造器,最后是调用的方法

5.7.2 静态数据的初始化

创建了三个类,碗、桌子、橱柜,运行之后

1、先加载StaticInitialization类,首先它的静态域table和cupboard先被初始化

2、初始化table的时候,Table的静态域bowl1和bowl2会先初始化,输出:Bowl(1)、Bowl(2)

3、然后开始执行Table的构造器,输出:Table()、f1(1)

4、初始化cupboard的时候,尽管bowl3在前,但是肯定要先加载静态域,所以输出Bowl(4)、Bowl(5)、Bowl(3)

5、然后开始执行Cupboard的构造器,输出:Cupboard()、f2(2)

6、执行main方法:输出:

  • 在main方法中创建、Bowl(3)、Cupboard()、f1(2)、
  • 在main方法中创建、Bowl(3)、Cupboard()、f1(2)、
  • f2(1)、f3(1)
public class Bowl {
	
	Bowl(int marker) {
		System.out.println("Bowl(" + marker + ")");
	}
	
	void f1(int marker) {
		System.out.println("f1(" + marker + ")");
	}
}

public class Table {
	static Bowl bowl1 = new Bowl(1);
	
	Table() {
		System.out.println("Table()");
		bowl2.f1(1);
	}
	
	void f2(int marker) {
		System.out.println("f2(" + marker + ")");
	}
	
	static Bowl bowl2 = new Bowl(2);
}

public class Cupboard {
	
	Bowl bowl3 = new Bowl(3);
	static Bowl bowl4 = new Bowl(4);
	
	Cupboard() {
		System.out.println("Cupboard()");
		bowl4.f1(2);
	}
	
	void f3(int marker) {
		System.out.println("f3(" + marker + ")");
	}
	
	static Bowl bowl5 = new Bowl(5);
}

public class StaticInitialization {

	public static void main(String[] args) {
		// TODO Auto-generated method stub
		System.out.println("在main方法中创建");
		new Cupboard();
		System.out.println("在main方法中创建");
		new Cupboard();
		table.f2(1);
		cupboard.f3(1);
		
	}
	
	static Table table = new Table();
	static Cupboard cupboard = new Cupboard();

}

// 运行结果:
Bowl(1)
Bowl(2)
Table()
f1(1)
Bowl(4)
Bowl(5)
Bowl(3)
Cupboard()
f1(2)
在main方法中创建
Bowl(3)
Cupboard()
f1(2)
在main方法中创建
Bowl(3)
Cupboard()
f1(2)
f2(1)
f3(1)

如果你是一个初级小白,从这个例子真的能学得到这些:

  • 如果不创建Table对象或者不引用Table.b1,那么静态的Bowl永远不会被创建
  • 只有在第一个Table被创建(或第一次访问静态数据的时候),它们才会被初始化,此后,静态对象不会再次初始化
  • 初始化顺序是:先静态对象,后非静态对象、然后构造器

5.7.3 显式的静态初始化

静态块和静态成员一样,只会执行一次

Cups.cpu1.f(99);

// 这一行代码的执行,一定要记住,先执行初始化,都初始化完毕了,最后才执行方法

练习13、略

练习14、定义一个类拥有两个静态字符串域,其中一个在定义处初始化,另一个在静态块中初始化

public class E14_StaticStringInitialization {   
    static String s1 = "Initialized at definition";   
    static String s2;   
    static { 
        s2 = "Initialized in static block"; 
    }   
    public static void main(String args[]) {     
        System.out.println("s1 = " + s1);     
        System.out.println("s2 = " + s2);   
    } 
} 

// 证明了它们都会在被使用之前完成初始化操作
// 即使static写在整个类的最后面,那也记住,会先执行静态块,再执行方法

5.7.4 非静态实例初始化

看起来和静态初始化挺一样,就是少了static,一个大括号包起来,这种语法:

Mug mug1;
Mug mug2;
{
    mug1 = new Mug(1);
    mug2 = new Mug(2);
}

// 不管写在上面还是类的最下面,肯定先于构造器执行

练习15、编写一个含有字符串域的类,采用实例初始化方式进行初始化

public class E15_StringInstanceInitialization { 
    String s;   
    { 
        s = "'instance initialization'"; 
    }   
    public E15_StringInstanceInitialization() {     
        System.out.println("Default constructor; s = " + s);   
    }   
    public E15_StringInstanceInitialization(int i) {     
        System.out.println("int constructor; s = " + s);   
    }   
    public static void main(String args[]) {     
        new E15_StringInstanceInitialization();     
        new E15_StringInstanceInitialization(1);   
    } 
}

// 运行结果:
Default constructor; s = 'instance initialization' 
int constructor; s = 'instance initialization' 

// 证明了会先于构造方法执行

5.8 数组初始化

  • 数组是相同类型的,用一个标识符名称封装到一起的一个对象的序列和基本类型数据序列
  • int[ ] a1;这种更合理,毕竟它表示:“一个int型的数组”
  • int a1[ ]; C++程序员的习惯
  • 将一个数组赋值给另一个数组,其实是赋值了一个引用而已
  • 可以通过new来确定数组的大小,基本类型数据会自动赋值,int型为0,boolean型为false
  • 可以直接给某一项赋值,也可以使用花括号来赋值

练习16、创建一个String对象的数组,并打印

public class E16_StringArray {   
    public static void main(String args[]) {     
        // Doing it the hard way:     
        String sa1[] = new String[4];     
        sa1[0] = "These";     
        sa1[1] = "are";     
        sa1[2] = "some";     
        sa1[3] = "strings";     
        for(int i = 0; i < sa1.length; i++)       
            System.out.println(sa1[i]);     
        // Using aggregate initialization to     
        // make it easier:     
        String sa2[] = {       
            "These", "are", "some", "strings"     
        };     
        for(int i = 0; i < sa2.length; i++)       
            System.out.println(sa2[i]); 
    } 
}

练习17、创建一个类,接受一个String参数的构造器

class Test {   
    Test(String s) {     
        System.out.println("String constructor; s = " + s);   
    } 
} 
public class E17_ObjectReferences {   
    // You can define the array as a field in the class:   
    Test[] array1 = new Test[5];   
    public static void main(String args[]) {     
        // Or as a temporary inside main:     
        Test[] array2 = new Test[5];   
    } 
}
// 此代码仅创建数组,而不是进入其中的对象。 您没有在Test的构造函数中看到初始化消息,因为不存在该类的实例。

// 只是声明了我要创建Test类型的数组,长度为5,但是里面的值是null的

练习18、通过创建对象赋值给引用数组,从而完成前一个练习

public class E18_ObjectArray {   
    public static void main(String args[]) {     
        Test[] array = new Test[5];     
        for(int i = 0; i < array.length; i++)       
            array[i] = new Test(Integer.toString(i));   
    } 
}
// 这个才是赋值,才会打印

5.8.1 可变参数列表

直接上练习吧

练习19、写一个类,它接受一个可变参数的String数组

public class E19_VarargStringArray { 
    static void printStrings(String... strs) {     
        for(String s : strs)         
            System.out.println(s);   
    }   
    public static void main(String args[]) {     
        printStrings("These", "are", "some", "strings");     
        printStrings(new String[] { "These", "are", "some", "strings" });   
    } 
}

练习20、创建一个使用可变参数列表而不是普通的main()语法的main(),打印arg数组

public class E20_VarargMain {   
    public static void main(String... args) {             
        E19_VarargStringArray.printStrings(args);   
    } 
}

5.9 枚举类型

enum关键字

其实枚举也是个类呀,这里不说了,在书的后面章节还会介绍枚举的知识

直接上练习

练习21、创建一个enum,包含纸币中最小面值的6种类型,通过values()循环并打印每一个值及其ordimal()

enum PaperCurrencyTypes {   
    ONE, TWO, FIVE, TEN, TWENTY, FIFTY 
} 
 
public class E21_PaperCurrencyTypesEnum {   
    public static void main(String args[]) {     
        for(PaperCurrencyTypes s : PaperCurrencyTypes.values())               
            System.out.println(s + ", ordinal " + s.ordinal());   
    } 
}

练习22、在上面的例子为enum写一个switch语句

public class E22_PaperCurrencyTypesEnum2 {   
    static void describe(PaperCurrencyTypes pct) {     
        printnb(pct + " has a portrait of ");     
        switch(pct) {       
            case ONE:
                print("George Washington");                    
                break; 
            case TWO:    
                print("Thomas Jefferson");                    
                break;       
            case FIVE:   
                print("Abraham Lincoln");                    
                break;       
            case TEN:    
                print("Alexander Hamilton");                    
                break;       
            case TWENTY: print("Andrew Jackson");                    
                break;       
            case FIFTY:  print("U.S. Grant");                    
                break;     
        }   
    }   
    public static void main(String args[]) {     
        for(PaperCurrencyTypes s : PaperCurrencyTypes.values())       
            describe(s);   
    } 
}

5.10 总结

构造器是一种很精巧的初始化机制

大部分的编程错误都源于不正确的初始化

垃圾回收也是很重要的,但确实增加了运行时的开销

Java在性能方面已经取得了长足的进步,但是速度问题仍然是它涉足某些编程领域的障碍

猜你喜欢

转载自blog.csdn.net/pengbo6665631/article/details/82143352
今日推荐