《Java编程思想》读书笔记01-初始化与清理

来源:互联网 发布:惠阳政府网络问政 编辑:程序博客网 时间:2024/05/23 13:25

开篇

最近在看Java编程思想,想把Java SE部分重新巩固一下。由于之前看过两遍遍《Java疯狂讲义》,但是看过以后很多东西就忘记了,所以想记录一下学习Java SE的过程与重点。初步计划每章会有一篇博客作为总结,因为前几章比较简单,所以从第五章开始记录笔记。

序言

随着计算机革命的发展,“不安全”的编程方式已逐渐成为编程代价高昂的主因之一。初始化和清理正是设计安全的两个问题。许多错误来源于忘记初始化变量,而没有清理会导致资源用尽。Java采用构造器和垃圾回收器来解决相关问题。

1. 用构造器确保初始化

Java提供构造器,使类的设计者可以确保每个对象都被初始化。
构造器命名有两个问题:
    1. 所取的任何名字都可能与类的某个成员的名字重复
    2. 调用构造器是编译器的责任,所以必须让编译器知道应该调用哪个方法。
解决办法:让构造器采用与类相同的名字。因此“每个方法的首字母小写”的编码风格不适用于构造器。
从概念上将:“初始化”与“创建”是彼此分离的,然而在Java中,两者被捆绑在了一起,不能分离。
构造器没有返回值。

2. 方法重载

所谓方法,就是给某个动作取名字,通过起名字,你可以引用所有的对象和方法。名字起到好有助于系统的理解和修改。但是有时候相同的动作需要传入不同的对象,如果为每一个方法都起一个名字,会很难维护。例如清洗()这个方法,如果穿进去的对象是衬衫,那么执行的操作就是清洗衬衫,如何传进去的对象是汽车,那么执行的操作就是清洗汽车。我们当然可以定义两个方法,清洗汽车()和清洗衬衫(),但是这会很难维护。因此Java提供了方法的重载,使得可以用同一个方法名字,根据传入参数的不同,执行不用的操作。

很显然,Java的构造器是强制方法重载的一个重要原因。因为类名决定构造器名,所以要用多种方式创建一个类的对象时,只能根据传入参数的不同来区分。为了让方法名相同而形参不同的构造器同时存在,就必须用到方法重载。

例:

class Tree {    int height;    Tree() {        prt("Planting a seedling");        height = 0;    }    Tree(int i) {        prt("Creating new Tree that is " + i + " feet tall");        height = i;    }    void info() {        prt("Tree is " + height + " feet tall");    }    void info(String s) {        prt(s + ": Tree is " + height + " feet tall");    }    static void prt(String s) {        System.out.println(s);    }}public class Overloading {    public static void main(String[] args) {        for (int i = 0; i < 5; i++) {            Tree t = new Tree(i);            t.info();            t.info("overloaded method");        }        // Overloaded constructor:        new Tree();    }}
如上所示,对象tree有两个构造器,传入不同的参数,会生成不同的tree对象。构造器通过形参不同产生方法重载。

2.1 区分方法重载

通过形参列表来区分重载的方法,每个方法都有独一无二的形参列表。注意:参数顺序的不同也可以区分两个不同的方法。
例:
public class OverloadingOrder {    static void print(String s, int i) {        System.out.println("String: " + s + ", int: " + i);    }    static void print(int i, String s) {        System.out.println("int: " + i + ", String: " + s);    }    public static void main(String[] args) {        print("String first", 11);        print(99, "Int first");    }} 
上述代码中print方法的参数都为String与int,但参数顺序不同,所以为两个不同的方法。但请注意,尽量不要这样做,这会使代码难易维护。

2.2 涉及基本类型的重载

基本类型能从“较小”的类型自动提升至“较大”的类型。如果传入的参数类型小于方法中声明的形式参数类型,实际数据类型就会被提升。char类型略有不同,如果无法找到恰好能接受char参数的方法,就会把char直接提升至int类型。
然而如果方法接受较小的类型作为参数。如果传入较大类型的参数,就得通过窄化才能正常调用方法,否则编译器会报错。
Java数据类型自动提升的顺序如下图所示:


图1 Java数据类型自动提升顺序
涉及基本类型重载的例子如下:
public class Test {    public static void main(String[] args) {        // 字面量默认作为int处理        PrimitiveTypeOverload.primitive(5); // 输出结果:int        byte b = 1;        PrimitiveTypeOverload.primitive(b); // 输出结果:byte        // char 在没有确切接收char类型的重载方法的时候,char类型作为int类型处理(65535)        char c = 'c';        PrimitiveTypeOverload.primitive(c); // 输出结果:int        long l = 5L;        // 传入的参数类型大于声明类型, 需要窄化        PrimitiveTypeOverload.primitive((char) l); // 输出结果:int        PrimitiveTypeOverload.primitive((short) l); // 输出结果:short        PrimitiveTypeOverload.primitive((int) l); // 输出结果:int    }}class PrimitiveTypeOverload {    public static void primitive(int i) {        System.out.println("int ");    }    public static void primitive(byte b) {        System.out.println("byte");    }    public static void primitive(short s) {        System.out.println("short");    }}

2.3 以返回值区分重载方法

有人会想当然认为,可以通过返回值区分方法。例如如下两个方法:
void f() {}int f() {}
如果用int x = f(),可以很明确的知道调用的是第二个方法。但如果我不关心方法的返回值,指是想要方法调用的效果,那么如下的例子就不能确定是调用的哪个方法:
f();
结论:根据返回值区分重载方法不可行!

3. 默认构造器

如果你写的类中没有构造器,系统会自动帮你创建一个没有参数的默认构造器。例:
class Bird {    int i;}public class DefaultConstructor {    public static void main(String[] args) {        Bird nc = new Bird(); // default!    }}
如果你已经定义了一个构造器(无论有无参数),系统都不在帮你自动创建默认构造器。例:
class Bush {    Bush(int i) {    }    Bush(double d) {    }}
 new Bird(); //此时会报错,因为没有默认构造器。

4. this关键字

调用一个对象的方法时需要这个对象的名字,即引用。如a,b两个对象,他们都有方法peel()。调用时的代码分别是
a.peel();b.peel();
面向对象的语法编写代码就是发送消息给对象。编译器在此暗自把“所操作对象的引用”发送给了方法peel()。实际情况应该是这样的:
Banana.peel(a,1);Banana.peel(b,2);
但不能这样写,编译器无法通过。这说明,调用某个对象的方法一定需要该对象的名字,即引用。但如果要在一个方法的内部获取当前对象的引用时,这种方式就无法完成任务。Java提供了this关键字来代表“调用该方法的当前对象”的引用,注意this只能在方法内部使用。特别:如果在一个方法内部调用另一个方法,不必使用this,直接调用即可。只在必要时使用this,遵循一种一直而直观的编程峰哥能节省时间和金钱。

4.1 在构造器中调用构造器

通常this指当前对象,代表一个当前对象的引用。但如果在this后添加了参数列表,将调用符合这个参数列表的构造器。
注意:
  1. 构造器调用要放在最起始处,否则编译器会报错。
  2. this调用构造器不能调用两次,只能用一次。
  3. 非构造器不能用this调用构造器。
public class Flower {    private int petalCount = 0;    private String s = new String("null");    Flower(int petals) {        petalCount = petals;        System.out.println("Constructor w/ int arg only, petalCount= " + petalCount);    }    Flower(String ss) {        System.out.println("Constructor w/ String arg only, s=" + ss);        s = ss;    }    Flower(String s, int petals) {        this(petals);        // ! this(s); //  1.不能调用两次this        this.s = s; // Another use of "this"        System.out.println("String & int args");    }    Flower() {        this("hi", 47);        System.out.println("default constructor (no args)");    }    void print() {        // ! this(11); 2.只能在构造器内使用this调用构造器        System.out.println("petalCount = " + petalCount + " s = " + s);    }    public static void main(String[] args) {        Flower x = new Flower();        x.print();    }}

4.2 static含义

static方法就是没有this的方法,在static方法的内部不能调用非静态方法,反过来却可以。因为可以使用类本身来调用static方法而无需创建类对象,如果static方法调用了非静态方法,而非静态方法需要依赖于对象的实例,可是此时对象还没有创建,因此就找不到该对象的非静态方法,会出错。
当然,如果代码中出现了大量static方法,那一定需要重新考虑自己的设计。

5. 清理:终结处理和垃圾回收

Java有一个垃圾回收器,用来监视new创建的所有对象,并辨别那些不会再被引用的对象。使用垃圾回收器的唯一是为了回收不再使用的内存。垃圾回收的特点:
  1. 对象可能不被垃圾回收。
  2. 垃圾回收不等于“析构”。
  3. 垃圾回收只与内存有关。

将一个对象用完之后就“弃之不顾”是不完全的。Java的垃圾回收机制只与内存相关,负责回收new出来的对象。然而对使用本地方法调用c或c++等语言分配的内存,Java垃圾回收无法起作用。因此出现了finalize()方法,负责执行垃圾回收前的收尾工作。

5.1 finalize()的用途

finalize()工作原理:一旦垃圾回收器准备好释放对象所占用的存储空间,将首先调用其finalize()方法,并且在下一次垃圾回收动作发生时,才会真正回收对象占用的内存。Java的垃圾回收器只负责回收对象所占用的内存,如果用过某种创建对象以外的方式为对象分配了内存,垃圾回收器就毫无作用,这时就需要finalize()方法。例如Java调用C语言的malloc()函数分配了存储空间,此时除非使用free()函数,否则存储空间得不到释放,会放生内存泄漏。这种情况下就需要在finalize()方法中调用free()函数。
当然,不能过多的使用finalize()方法。

5.2 你必须实施清理

Java不允许创建局部对象,必须使用new创建对象。无论是“垃圾回收”还是“终结”,都不一定会发生。如果Java虚拟机没有面临内存耗尽的情况,它是不会浪费时间去执行垃圾回收以恢复内存的。

5.3 终结条件

finalize()可以用于对象的终结条件的验证。例如:
public class Tank {      boolean status=false;//false为空,true为满            Tank(boolean stat){          status=stat;      }            void empty(){          status=false;      }            protected void finalize(){          if(status)              System.out.println("error");          }  }  
public class Test {      public static void main(String args[]){          Tank tk1=new Tank(true);                    tk1.empty();                    //Tank tk2=new Tank(true);          new Tank(true); //对象一创建就成为垃圾,因为没有引用指向它                      System.gc();  //强制执行终结操作    }  }  
输出为error。

本例终止的条件为:Tank()的status值为false,即tank为空。当Tank的status值为true时,会输出error。如果没有finalize()来验证终止条件,很难发现这种缺陷。gc()函数的作用是提醒虚拟机,程序员希望进行一次垃圾回收,但并不保证一定会执行垃圾回收。具体什么时候取决于虚拟机,不同虚拟机有不同对策。

5.4垃圾回收器如何工作

待完成

6. 成员初始化

Java尽力保证所有变量在使用前都能得到初始化。对于局部变量,Java以编译时错误的形势贯彻这种保证, 也就是说局部变量没有默认值。其余默认值如下所示:

boolean        false

char              '/uoooo'(null)

byte              (byte)0

short             (short)0

int                  0

long               0L

float               0.0f

double           0.0d

引用类型默认值为null。

6.1 指定初始化

指定初始化是指在定义类成员变量的时候为其赋值。例:
class Measurement {    boolean b = true;    char c = 'x';    byte B = 47;    short s = 0xff;    int i = 999;    long l = 1;    float f = 3.14f;    double d = 3.14159;}
也可以以同样方法初始化非基本类型的对象。
class Measurement {    Depth o = new Depth();    boolean b = true;}
如果没有初始化d的值就是用它,会出现运行时错误。

7. 构造器初始化 

可以用构造器进行初始化。即在运行时执行某些动作为变量赋初值。但要注意:无法阻止自动初始化的执行, 它将在构造器被调用之前发生。即默认值最先出现。例:
class Counter {    int i;    Counter() {        i = 7;    }}
i的值首先是0,然后变成7。对于所有的基本类型和对象引用,包括在定义时已经指定初始值的变量,都是如此。

7.1 初始化顺序

在类的内部,成员变量定义的先后顺序决定了初始化的顺序,先于方法的执行,即使变量定义散布于方法之间,仍旧会在任何方法执行前得到初始化。
class Tag {    Tag(int marker) {        System.out.println("Tag(" + marker + ")");    }}class Card {    Tag t1 = new Tag(1); // Before constructor    Card() {        // Indicate we're in the constructor:        System.out.println("Card()");        t3 = new Tag(33); // Re-initialize t3    }    Tag t2 = new Tag(2); // After constructor    void f() {        System.out.println("f()");    }    Tag t3 = new Tag(3); // At end}public class OrderOfInitialization {    public static void main(String[] args) {        Card t = new Card();        t.f(); // Shows that construction is done    }}
其输出结果如下:
Tag(1)Tag(2)Tag(3)Card()Tag(33)f()
可以看出,即使tag的定义散布于方法之间,仍然先于任何方法得到了初始化。

7.2 静态数据的初始化

无论创建多少个对象,静态数据都只占用一份存储区域。静态代码只执行一次。static关键字不能用于局部变量。静态初始化只在必要时进行,两个时刻:
  1. 第一次new创建对象时。
  2. 第一次访问静态数据时。
初始化的顺序是:
静态对象----->非静态对象
构造器在初始化完成后执行。例:
class Bowl {    Bowl(int marker) {        System.out.println("Bowl(" + marker + ")");    }    void f(int marker) {        System.out.println("f(" + marker + ")");    }}class Table {    static Bowl b1 = new Bowl(1);    Table() {        System.out.println("Table()");        b2.f(1);    }    void f2(int marker) {        System.out.println("f2(" + marker + ")");    }    static Bowl b2 = new Bowl(2);}class Cupboard {    Bowl b3 = new Bowl(3);    static Bowl b4 = new Bowl(4);    Cupboard() {        System.out.println("Cupboard()");        b4.f(2);    }    void f3(int marker) {        System.out.println("f3(" + marker + ")");    }    static Bowl b5 = new Bowl(5);}public class StaticInitialization {    public static void main(String[] args) {        System.out.println("Creating new Cupboard() in main");        new Cupboard();        System.out.println("Creating new Cupboard() in main");        new Cupboard();        t2.f2(1);        t3.f3(1);    }    static Table t2 = new Table();    static Cupboard t3 = new Cupboard();}
输出:
Bowl(1)Bowl(2)Table()f(1)Bowl(4)Bowl(5)Bowl(3)Cupboard()f(2)Creating new Cupboard() in mainBowl(3)Cupboard()f(2)Creating new Cupboard() in mainBowl(3)Cupboard()f(2)f2(1)f3(1)



7.3 显示的静态初始化

即为将多个静态初始化语句组成一个特殊的“静态子句”,成为静态代码块。例:
class Spoon {    static int i;    static {        i = 47;    }}

7.4 非静态实例初始化

实例初始化用来初始化每一个实例对象的非静态变量。

8. 数组初始化

待完成

9. 枚举类型

待完成


原创粉丝点击