第一章 对象导论
面向对象语言的五个基本特性
-
万物皆对象
-
程序是对象的集合,它们通过发送消息来告诉彼此所要做的。
想要请求一个对象,就必须对该对象发送一条消息。更具体的说,可以把消息想象为对某个特定对象的方法的调用请求。
-
每个对象都有自己的由其他对象所构成的存储。
换句话说,可以通过创建包含现有对象的包的方式来创建新类型的对象。因此可以在程序中构建复杂的体系,同时将其复杂性隐藏在对象的简单性背后。
-
每个对象都拥有其类型。
-
某一特定类型的所有对象都可以接收同样的消息。
每个对象都有一个接口
对象通过暴露的接口来提供服务和接收消息。
被隐藏的具体实现
复杂的逻辑实现可以隐藏在接口之后,即使逻辑改变,接口不发生改变仍可以正常使用。
复用具体实现
最简单地复用某个类的方式就是直接使用该类的一个对象,此外也可以将那个类的一个对象置于某个新的类中,成为一个成员对象。
继承
当继承现有类型时,也就创造了新的类型。这个新的类型不仅包括现有类型的所有成员(尽管private成员被隐藏了起来,并且不可访问,但它可能需要被父类的方法调用),而且更重要的是它复制了基类的接口。所有可以发送给基类对象的消息同时也可以发送给导出类对象。这也就意味着导出类与基类具有相同的类型。
伴随多态的可互换对象
在Java中动态绑定是默认行为,不需要添加额外的关键字来实现多态。
单根继承结构
在Java中所有的类最终都继承自单一的基类Object。事实上除C++以外的所有OPP语言都继承自单一的基类。
单根继承结构保证所有对象都具备某些功能,因此可以在每个对象上执行某些基本操作。所有对象都可以很容易地在堆上创建,而参数传递也得到了极大的简化。单根继承结构也使得垃圾回收器的实现变得容易得多。
容器
一种对象类型,可以持有其他对象的引用。用来解决不知道在不知道解决某个特定问题时需要多少个对象,或者他们将存活多久,如何存储这些对象的问题。
参数化类型
在Java SE5之前,容器存储的对象都只具有Java中的通用类型:Object。因此将对象引用置入容器时,它必须被向上转型为Object。因此它会丢失其身份。
取回时会获得一个对Object对象的引用,因此需要向下转型。但除非确切知道所要处理的对象类型,否则向下转型几乎是不安全的。并且向下转型和运行时的检查需要额外的程序运行时间。
在Java SE5中增加了参数化类型,在Java中它成为泛型。一对尖括号,中间包含类型信息,通过这些特征就可以识别对泛型的使用。
对象的创建和生命周期
-
C++
C++认为效率控制是最重要的议题,对象的存储空间和生命周期可以在编写程序时确定,可以通过将对象置于堆栈(它们有时被称为自动变量或限域变量)或静态存储区域内来实现。 -
Java
另一种方式实在被称为堆的内存池中动态地创建对象。在这种方式中,直到运行时才知道需要多少对象,他们的生命周期如何,以及它们的具体类型是什么。如果需要一个新的对象,可以在需要的时刻直接在堆中创建。 因为存储空间是在运行时被动态管理的,所以需要大量的时间在堆中分配存储空间,这可能要远远大于在堆栈中创建存储空间的时间。 动态方式有这样一个一般性的逻辑假设:对象趋于变得复杂,所以查找和释放存储空间的开销不会对对象的创建造成重大冲击。动态方式所带来的更大的灵活性正式解决一般化编程问题的要点所在。
-
生命周期
对于允许在堆栈上创建对象的语言,编译器可以确定对象存活的时间,并可以自动销毁它。然后在堆上创建对象,编译器就会对他的生命周期一无所知。
异常处理:处理错误
异常是一种对象,它从出错地点被“抛出”,并被专门设计用来处理特定类型错误的相应的异常处理器“捕获”。
异常不能被忽略,所以它保证一定会在某处得到处理。异常处理提供了一种从错误状况进行可靠恢复的途径。
第二章 一切都是对象
用引用操纵对象
Java中一切都被视为对象,因此可采用单一固定的语法。尽管一切都看作对象,但操纵的标识符实际上是对象的一个引用。Java中的引用在语法上更接近C++的引用而不是指针。
创建对象
存储位置
- 寄存器:这是最快的存储区,因为它位于不同于其他存储区的地方——处理器内部。但是寄存器的数量极其有限,所以寄存器根据需求进行分配,我们无法直接控制,也不能在程序中感觉到寄存器存在的任何迹象(另一方面,C和C++允许向编译器建议寄存器的分配方式)。
- 堆栈:位于通用RAM(随机访问存储器)中,但通过堆栈指针可以从处理器那里获得直接支持。堆栈指针若向下移动,则分配新的内存;若向上移动,则释放那些内存。这是一种快速有效的分配存储方法,仅次于寄存器。创建程序时,Java系统必须知道存储在堆栈内所有项的确切生命周期,以便上下移动堆栈指针。这一约束限制了程序的灵活性,所以虽然某些Java数据存储于堆栈中——特别是对象引用,但是Java对象并不存储于其中。
- 堆:一种通用的内存池(也位于RAM区),用于存放所有的Java对象。堆不同于堆栈的好处是:编译器不需要知道存储的数据在堆里存活多长时间。因此在堆里分配有很大的灵活性。相应的代价是:用堆进行存储分配和清理可能比用堆栈进行存储分配需要更多的时间。
- 常量存储:常量值通常直接存放在程序代码内部。
- 非RAM存储:如果数据完全存活于程序之外,那么它可以不受程序的任何控制,在程序没有运行时也可以存在。其中两个基本的例子是流对象和持久化对象。
特例:基本类型
在程序设计中经常用到一系列类型,它们需要特殊对待。可以把它们想象成“基本”l类型。之所以特殊对待,是因为 new 将对象存储在“堆”里,故用 new 创建一个对象——特别是小的、简单的变量,往往不是很有效。因此,对于这些类型,Java采取和C++相同的方法。也就是说,不用 new 来创建变量,而是创建一个并非是引用的“自动变量。这个变量直接存储”值“,并置于堆栈中,因此更加高效。
Java每种基本类型所占存储空间的大小是确定的,并不随机器硬件架构的变化而变化,因此Java具有更高的可移植性。
对象的销毁
作用域
大多数过程型语言都有作用域(scope)的概念。作用域决定了在其内定义的变量名的可见性和生命周期。在C、C++和Java中,作用域由花括号的位置决定。例如:
int x = 12;
// Only x available
{
int q = 96;
// Both x & q available
}
// Only x available
// q is "out of acope"
尽管一下代码在C和C++中是合法的,但是在Java中却不能这样书写:
{
int x = 12;
{
int x = 96 //Illegal
}
}
在C和C++里将一个较大作用域的变量“隐藏”起来的做法,在Java里是不允许的。
对象的作用域
Java对象不具备和基本类型一样的生命周期。当用 new 创建一个Java对象时,它可以存活于作用于之外。在作用域之外该对象在此作用域创建的引用便消失了,但如果该对象还有其他引用则会继续占据内存空间。Java有一个垃圾回收器,用来监视用 new 创建的所有对象,并辨别那些不会再被引用的对象,随后释放这些对象的内存空间。
类
字段和方法
字段(有时被称作数据成员)可以是任何类型的对象,可以通过其引用与其进行通讯;也可以是基本类型中的一种。如果字段是某个对象的引用,那么必须初始化该引用,以便使其与一个实际的对象相关联。
方法有时也被称为成员函数。
方法、参数和返回值
第三章 操作符
关系操作符
关系操作符生成的是一个**boolean**(布尔)结果。
测试对象的等价性
public class Equivalence{
public static void main(String[] args){
Integer n1 = new Integer(47);
Integer n2 = new Integer(47);
System.out.println(n1 == n2); // false
System.out.println(n1 != n2); // true
}
}
==和!=会比较对象的引用,而不是比较对象的实际内容。如果想要比较对象的实际内容是否相同,需要调用特殊方法**equals()**。并需要在中重写该方法,因为该方法默认行为是比较引用。
直接常量
直接常量后面的后缀字符标志了它的类型。若为大写(或小写)的L,代表long(但是,使用小写字母l容易造成混淆,因为它看起来很像数字1)。大写(或小写)字母F,代表float;大写(或小写)字母D,则代表double。
十六进制数适用于所有整数数据类型,以前缀0x(或0X),后面跟随0-9或小写(或大写)的a-f来表示。
八进制数由前缀0以及后续的0~7的数字来表示。
在C、C++或者Java中,二进制数没有直接常量表示方法。但是,在使用十六进制和八进制记数法时,以二进制形式显示结果将非常有用。通过使用 Integer和Long类的静态方法toBinaryString() 可以很容易地实现这一点。如果将比较小的类型传递给Integer.toBinaryString() 方法,则该类型将自动被转换为int。
指数记数法
在Java中**1.39-43**真正的含义是1.39 * 10-43,而不是科学与工程领域中的1.39 * e-43。
第四章 控制执行流程
迭代
逗号操作符
Java里唯一用到逗号操作符的地方就是for循环的控制表达式。在控制表达式的初始化和步进控制部分,可以使用一系列由逗号分隔的语句,而且那些语句均会独立执行。通过使用逗号操作符,可以在for语句内定义多个变量,但是它们必须具有相同的类型。
for(int i = 1, j = i + 10; i < 5; i++, j = i * 2)
for语句中的int定义涵盖了i和j,在初始化部分实际上可以拥有任意数量的具有相同类型的变量定义。在一个控制表达式中,定义多个变量的这种能力只限于for循环适用,在其他任何选择或迭代语句中都不能使用这种方式。
这里的逗号是逗号操作符而不是逗号分隔符,逗号用作分隔符时用来分隔函数的不同参数。
Foreach语法
foreach可以用于任何Iterable对象。
public class ForeachString{
public static void main(String[] args){
for(char c : "An African Swallow".toCharArray()){
System.out.print(c + "");
}
}
}
goto和label
label1
outer-iteration{
inner-iteration{
//../
break; //(1)
//...
continue; //(2)
//...
continue label1; //(3)
//...
break label1; //(4)
}
}
在(1)中,break中断内部迭代,回到外部迭代。在(2)中,continue使执行点回到内部迭代的起始处。在(3)中,continue label1同时中断内部迭代以及外部迭代,直接转到label1处;随后,它实际上是继续迭代过程,但却从外部迭代开始。在(4)中,break label1也会中断所有迭代,并回到label1处,但并不重新进入迭代。
第五章 初始化与清理
this关键字
在构造器中调用构造器
尽管可以用this调用一个构造器,但却不能调用两个。此外必须将构造器调用置于最起始处,否则编译器会报错。另外编译器禁止在其他任何方法中调用构造器。
static关键字
static方法就是没有this的方法。在static方法的内部不能调用非静态方法,反过来倒是可以。而且可以在没有创建任何对象的前提下,仅仅通过类本身调用static方法。这实际上正是static方法的主要用途。它很像全局方法。Java中禁止使用全局方法,但在类中置入static方法就可以访问其他static方法和static域。
清理:终结处理和垃圾回收
Java有垃圾回收器负责回收无用对象占据的内存资源,但也有特殊情况:假定对象(并非使用new)获得了一块“特殊”的内存区域,由于垃圾回收器只知道释放那些经由new分配的内存,所以它不知道该如何释放该对象的这块“特殊”内存。为了应对这种情况,Java允许在类中定义一个名为 finalize() 的方法。它的工作原理“假定”是这样的:一旦垃圾回收器准备好释放对象占用的存储空间,将首先调用其 finalize() 方法,并且在下次垃圾回收动作发生时,才会回收对象占用的内存。 finalize() 与C++中的析构函数并不相同,在C++中,对象一定会被销毁(如果程序中没有缺陷的话);而Java里的对象却并非总是被垃圾回收。因为C++的析构函数是可以主动调用的,而Java的垃圾回收是自动处理的,而finalize() 方法仅在垃圾回收前调用。
只要程序没有濒临存储空间用完的那一刻,对象占用的空间就总也得不到释放。如果程序执行结束,并且垃圾回收器一直都没有释放创建的任何对象的存储空间,则随着程序的退出,那些资源也会全部交还给操作系统。因为垃圾回收本身也有开销,要是不使用它,那就不用支付这部分开销了。
finalize()的用途何在
Java中一切皆为对象,那么上文提到的”特殊“情况是怎么回事呢?之所以要用finalize(),是由于在分配内存时可能采用了类似C语言中的做法,而非Java中的通常做法。这种情况主要发生在使用本地方法的情况下,本地方法是一种在Java中调用非Java代码的方式。本地方法目前只支持C和C++,但它们可以调用其他语言写的代码,所以实际上可以调用任何代码。在非Java代码中,也许会调用C的malloc() 函数系列来分配存储空间,而且除非调用了free() 函数,否则存储空间将得不到释放,从而造成内存泄漏。当然free() 是C和C++中的函数,所以需要在finalize() 中用本地方法调用它。
构造器初始化
无法阻止自动初始化的进行,它将在构造器被调用之前发生。因此,假如使用下述代码:
public class Counter{
int i;
Counter(){
i = 7;
}
}
那么i首先会被置为0,然后变成7。对于所有基本类型和对象引用,包括在定义时已经指定初值的变量,这种情况都是成立的;因此,编译器不会强制一定要在构造器的某个地方或在使用他们之前堆元素进行初始化——因为初始化早已得到了保证。
初始化顺序
在类的内部,变量定义的先后顺序决定了初始化的顺序。即使变量定义散布于方法定义之间,它们仍旧会在任何方法(包括构造器)被调用之前得到初始化。
静态初始化只有在必要时刻才会进行,静态方法内的对象只有在第一个对象被创建(或者第一次访问静态数据)的时候,它们才会被初始化。此后静态对象不会再次被初始化。
非静态实例初始化
public class Mugs{
Mug mug1;
Mug mug2;
{
mug1 = new Mug(1);
mug2 = new Mug(2);
}
}
实例初始化子句是在所有构造器之前执行,即无论调用哪个构造器,实例化子句都会在此之前执行。