[Think In Java]基础拾遗1 - 对象初始化、垃圾回收器、继承、组合、代理、接口、抽象类

时间:2022-04-09 19:41:26

目录

第一章 对象导论
第二章 一切都是对象
第三章 操作符
第四章 控制执行流程
第五章 初始化与清理
第六章 访问权限控制
第七章 复用类
第九章 接口

第一章 对象导论

1. 对象的数据位于何处?

有两种方式在内存中存放对象:

(1)为了追求最大的执行速度,对象的存储空间和生命周期可以在编写程序时确定,这可以通过将对象置于堆栈或者静态存储区域内来实现。这种方式牺牲了灵活性。

(2)在被称为堆的内存池中动态地创建对象。在这种方式,知道运行时才知道对象需要多少对象,它们的生命周期如何,以及它们的具体类型。

java完全采用了动态内存分配方式。每当想要创建新对象时,就要使用new关键字来构建此对象的动态实例。

2. 对象的生命周期

对于允许在堆栈上创建对象的语言,编译器可以确定对象的存活时间,并可以自动销毁它。对于在堆上创建的对象则不行。Java提供了垃圾回收器来销毁不再使用的对象。

第二章 一切都是对象

1. 用引用操纵对象

每种语言都有自己操纵内存元素的方式。(1)直接操作元素(2)间接操作(比如C和C++中的指针)。

在Java中如下的声明:String s; 只是表示在栈中声明了一个String对象的引用。类似于C++中的:string *s;

2. 特例:基本类型

对于基本类型,Java采用与C/C++一样的方法。不用new来创建变量,而是创建一个并非是引用的“自动”变量。这个变量直接存储“”(而不是对象的引用),并置于堆栈中

基本类型的包装器类,使得可以在堆中创建一个非基本对象,用来表示对于的基本类型。

Java中所有数值类型都有正负号,没有无符号的整数类型。

两个高精度计算的类:BigInteger和BigDecimal。

3. static

非static方法是与具体的对象关联的,这些方法隐含有this指针执行当前对象,所以非static方法要么会访问对象的域,要么会修改对象的域。而static方法由于没有this指针,不会访问对象的任何对象,不和具体对象关联。

4. java.lang包默认导入到每个java文件中。

5. ant命令和build.xml类似于make和makefile文件。

第三章 操作符

1. 赋值操作符与引用类型

将一个对象赋值给另一个对象,实际上是将引用从一个地方复制到另一个地方。这点不同于c++中的赋值操作符,C++中是由类来控制具体的语义的。

由此会带来一种特殊的现象:“别名现象”。

2. Random类

Random类对象如果在创建的时候没有传递参数,则默认会将当期时间作为随机数生成器的种子,这样任何使用运行都会产生不同的随机数序列。如果传递了具体的参数作为随机数生成器的种子,则每次运行都会产生相同的随机数序列。

3. 关系操作符与引用类型

关系操作符比较的是操作数的之间的关系。对于引用的话,那就是比较对象的引用,即使对象的值是相同的。要想比较对象的域的大小,覆盖Object类的equals方法,注意:equals的默认行为是比较引用。

4. 不同于C/C++,java中不可以将一个非布尔值当做布尔值在逻辑表达式中使用。

5. 移位操作

左移补0;右移有两种:(1)有符号右移(>>):遵循符号扩展(负数补1,正数补0);(2)无符号右移(>>>):无论正负,都在高位补0。

6.float和double在转型为整数的时候,总是不要小数部分。要想四舍五入,调用java.lang.Math.round()方法。

第四章 控制执行流程

1. Math库里的static方法random()的作用是产生0和1之间(包括0,不包括1)的一个double值。

2. foreach语法用于数组和容器,还可用于Iterable对象。

第五章 初始化与清理

初始化和清理正是涉及安全的两个问题

1. 构造器

创建对象时,构造器会被编译器调用,这样保证在操作对象(调用对象的方法)之前,它已经恰当的初始化了。

2. 方法重载

区分方法重载的方法:通过参数列表来区分(通过参数的类型,个数,顺序的不同来区分。)

注意:通过返回值来区分方法重载是行不通的。

3. this关键字

在实例方法中:表示对”调用方法的那个对象“的引用。

在构造器中调用构造器:

(1)在构造函数中,只能通过this调用一次其他构造函数,不能调用多次。

(2)必须将构造器调用置于最起始处。

4. finalize和垃圾回收

不是有垃圾回收器吗,为什么还要关注清理工作?

Java的垃圾回收器只知道释放那些由new分配的内存。有时候某个对象获得了一块”特殊“的内存区域(并非使用new),GC就不知道如何释放该对象的这块”特殊“内存。

解决方案:java允许在类中定义一个名为finalize()的方法。它的工作原理如下:一旦GC准备好释放对象占用的存储空间,将首先调用其finalize()方法,并在下一次垃圾回收动作发生时,才会真正回收对象占用的内存。

注意:finalize()不同与C++中的析构函数。C++中,对象一定会被销毁;而java里的对象并非总是被垃圾回收。也就是说:(1) 对象可能不被垃圾回收。(2) 垃圾回收不等于”析构“。(3)垃圾回收只与内存有关。

之所以要有finalize(),是由于在分配内存时可能采用了类似C语言中的做法,而非java中的通常做法。这种情况主要发生在使用”本地方法“的情况下。

记住:无论是”垃圾回收”还是”finalize“,都不保证一定会发生。

System.gc() 用于告诉JVM运行垃圾回收器。

finalize的可能用法之一:我们想保证某个对象在被清理之前应该处于某个状态(终结条件),那么可以通过在finalize方法中来验证该对象是否处于该状态。p89 练习12.

更多finalize的使用参考《effective java》。

5. 垃圾回收器如何工作

垃圾回收机制:

(1)引用计数

缺陷:处理循环引用的情况比较麻烦。

意义:只是用来说明垃圾收集的工作方式,从未被应用于任何一种JVM的实现中。

(2) 从堆栈和静态存储区出发,遍历所有的引用,进而找出所有存活的对象。

JVM中的垃圾回收器是:”自适应的,分代的,停止-复制,标记-清理“,式垃圾回收器。

6. 初始化顺序

总结一下对象的创建过程,假设有个名为Dog的类:

(1)即使没有显示地使用static关键字,构造器实际上也是静态方法。因此,当首次创建类型为Dog的对象时(构造器可以看成静态方法),或者Dog类的静态方法/静态域首次被访问时,java解释器必须查找类路径,以定位Dog.class文件。

(2)然后载入Dog.class(这里将创建一个Class对象),有关静态初始化的所有动作都会执行。因此,静态初始化只在Class对象首次加载的时候进行一次。

(3)当用new Dog()创建对象的时候,首先将在堆上为Dog对象分配足够的存储空间。

(4)这块存储空间会被清零,这就自动地将Dog对象中的所有基本数据类型都设置成了默认值(对数字来说就是0,对bool和char也相同),而引用则被设置成了null。

(5)执行所有出现于字段定义处的初始化动作。

(6)执行构造器。(涉及到继承的时候还会牵扯到很多动作)

tips:构造器也是static方法,尽管static关键字并没有显示地写出来。由此可以一句话总结类的加载时机:类是在其任何static成员被访问时加载的。

7. 数组

对数组的初始化可以使用一种特殊的表达式,如下:

int[] a = {1,2,3,4,5};

注意:在这种情况下,存储空间的分配将由编译器负责。这种方式是分配在堆空间的(等价于使用new),而不是栈空间的,这一点不同于C++。并且数组的复制仅仅是引用的复制。

8. enum

枚举类型其实是类,有自己的方法。比如toString(),static values(),ordinal()方法等,此外enum类型还可以用在swtich语句内。

第六章 访问权限控制

如何把变动的事物与保持不变的事物区分开来。

1. java中的访问权限等级从大到小是:public,protected,包访问权限(没有关键字)和private。

2. 每一个java文件成为一个编译单元,一个编译单元中只能有一个public class。

3. class文件的查找路径:从CLASSPATH环境变量指定的目录开始,JVM获取包的名称并将每个句点替换成反斜杠,以从CLASSPATH根中产生一个路径名称。

第七章 复用类

两种代码重用机制:组合 & 继承

1. 组合

组合:只需要将对象引用置于新类中即可。如果想初始化被置入对象的引用,可以在代码的下列位置进行:

(1)在定义对象的地方。这意味着他们总是能够在构造器被调用之前被初始化。

(2)在类的构造器中。

(3)就在正要使用这些对象之前,这种方式成为惰性初始化。在生成对象不值得以及不必每次都生成对象的情况下,这种方式可以减少额外的负担。

(4)使用实例初始化(与静态块初始化相对应的那种方式)。

2. 继承

当继承一个类的时候,子类会自动得到父类中的所有域和方法,只不过有的域或方法没有访问权限而已。

子类对象由两部分组成,继承得到的父类部分和子类特有的部分。所以在子类的构造器中应该先调用父类的构造器,然后在执行子类部分的构造。

如果我们没有显式调用父类的构造器,java编译器会自动在子类的构造器中插入对父类默认构造器的调用。如果父类没有默认的构造器或者想我们调用一个带参数的父类构造器,就必须用关键字super显式地编写调用父类构造器的语句。

3. 代理

java并没有提供对它的直接支持,这是继承和组合之间的中庸之道。因为我们将一个成员对象置于所要构造的类中(就像组合),但与此同时我们在新类中暴露了该成员对象的所有方法(就像继承)。

组合:强调的是复用被置入类的功能,而非被置入类的形式(也就是说不一定向外界暴露被置入类的接口)。

继承:调用的是继承已有类的形式(接口),并添加新的代码。

4. 名称屏蔽

如果java的父类拥有某个已经被多次重载的方法名称,那么在子类中重新定义该方法名称并不会屏蔽其在父类中的任何版本(这一点与C++不同)。

5. 在组合与继承之间选择

组合技术通常用于想在新类中使用现有类的功能而非它的接口这种情形。

在继承的时候,使用某个现有类,并开发一个它的特殊版本。

“has-a”的关系是用组合来表达的,“is-a”的关系是用继承来表达的。

到底是该使用组合还是继承,一个最清晰的办法就是确定是否需要从新类想基类进行向上转型。若是,则继承是必要的;否则,应当好好考虑是否需要继承。

6. final关键字

修饰数据时:

(1)一个永恒不变的编译时常量。在java中这类常量必须是基本数据类型,并且以final修饰(不一定有static修饰)。

(2)一个在运行时被初始化的值,而我们不希望它改变。

当对对象引用(而非基本类型)运用final时,表示final使引用不变,而非所引用的对象不变。另外,final修饰的数据不代表就是编译时常量,比如(2)中的。

修饰参数时:

如果参数还是一个引用类型的,则表示不能修改引用本身的值,而非引用所指向对象的值。也就是说,对于引用类型 的参数,即使是final修饰,依然可以改变引用所指向的对象的值。

7. 继承与初始化

构造器也是static方法,尽管static关键字并没有显示地写出来。由此可以一句话总结类的加载时机:类是在其任何static成员被访问时加载的。对象的创建过程:

(1)父类静态成员和静态初始化块,按在代码中出现的顺序依次执行。

(2)子类静态成员和静态初始化块,按在代码中出现的顺序依次执行。

至此类加载完毕,开始创建对象了:

(3)在堆上为对象分配足够的存储空间并初始化为二进制的零。

(4)父类的实例成员和实例初始化块,按在代码中出现的顺序依次执行。

(5)执行父类的构造方法。

此时堆空间的对象的父类部分初始化完毕了,接下来初始化子类部分:

(6)子类实例成员和实例初始化块,按在代码中出现的顺序依次执行。

(7)执行子类的构造方法。

注意:如果在(5)中的父类构造器中调用了某个多态方法,则此时会出现问题。因此避免在构造器中调用其他方法。详情p163

第九章 接口

1. 抽象类

包含抽象方法的类叫做抽象类。也可以不包含抽象方法(好像没什么意义),只要类用abstract修饰就好了。

抽象类是很有用的重用工具,使得我们可以很容易地要将公共方法沿着继承层次结构向上移动。

不能实例化抽象类,编译器会报错。

2. 接口

一个接口表示”所有实现了该特定接口的类看起来都像这样“。接口被用来建立类与类之间的协议。

接口中包含的域隐式的是static和final的。

如果一个方法操作的是类而非接口,那么就只能使用这个类及其子类,而不能使用不在此继承体系结构中的某个类。

3. Java中抽象类和接口的选择

  • 抽象类表示“IS-A”,接口表示“CAN-DO-THIS”,举例:Shape和Flyable;

  • Java不支持多继承,当需要继承多个类时,可以用接口,举例:Thread和Runnable;

  • 面向接口编程(Programming for interfaces)是面向对象设计原则之一,应尽量面向接口编程;

  • 抽象类中可以有普通方法,当子类有一些共同的功能时,可以在抽象类中实现;

  • 抽象类更适用于代码重用(Code reuse),避免写重复的代码,举例:List接口有一个AbstractList实现,避免ArrayList和LinkedList等实现List时有些代码要重复写;(与5一个意思)

  • 接口比抽象类更松耦合,因为抽象类里可以有公共方法的实现;

  • 接口有助于实现依赖注入(Dependency Injection)设计模式,举例:Spring框架。

参考:When to use interface and abstract class in Java