ThinkingInJava(一):对象导论、操作符、初始化与清理

第一章:对象导论

1.替代原则

继承应该只覆盖基类的方法,意味着导出类与基类是完全相同的类型,因为它们具有相同的接口。结果可以用一个导出类对象来完全替代一个基类对象。这可以视为纯粹替代,通常称之为替代原则

2.伴随多态的可互换对象

面向对象程序设计的最重要的妙诀:编译器不可能产生传统意义上的函数调用,一个非面向对象编程的编译器产生的函数调用会引起所谓的前期绑定,这么做将意味着编译器将产生对一个具体函数名字的调用,而运行时将这个调用解析到将要被执行的代码的绝对地址。然而在OOP中,程序直到运行时才能确定代码的地址,所以当消息发送到一个泛化对象时,必须采用其他的机制。

为了解决这个问题,面向对象程序设计语言使用了后期绑定的概念,当向对象发送消息时,被调用的代码直到运行时才能确定,即动态绑定。编译期确保被调用方法的存在,并对调用参数和返回值执行参数检查,但是并不知道将被执行的确切代码。为了执行后期绑定,Java使用一小段特殊的代码来替代绝对地址调用。这段代码使用在对象中存储的信息来计算方法体的地址。根据这一小段代码的内容,每一个对象都可以具有不同的行为表现。当向一个对象发送消息时,该对象就能够知道对这条消息应该做些什么。

3.单根继承结构

所有的类最终都继承自单一的基类,都具有一个公用接口。

单根继承结构保证所有对象都具备某些功能。因此在系统中可以在每个对象上执行某些基本操作,所有对象都可以很容易地在堆上创建,而参数传递也得到了极大的简化。

单根继承结构使垃圾回收器的实现变得容易得多,由于所有对象都保证具有其类型信息,因此不会因无法确定对象的类型而陷入僵局。

4.对象的创建和生命期

在堆的内存池中动态地创建对象。在这种方式中,直到运行时才知道需要多少对象,它们的生命周期如何,以及它们具体的类型是什么。这些问题的答案只能在程序运行时相关代码被执行到的那一刻才能确定。如果需要一个新对象,可以在需要的时刻直接在堆中创建。因为存储空间是在运行时被动态管理的,所以需要大量的时间在堆中分配存储空间,这可能远远大于在堆栈中创建存储空间的时间。在堆栈中创建存储空间和释放存储空间通常各需要一条汇编指令即可,分别对应将栈顶指针向下移动和将栈顶指针向上移动。创建堆存储空间的时间依赖于存储机制的设计。

动态方式有这样一个一般性的逻辑假设:对象趋向于变得复杂,所以查找和释放存储空间的开销不会对对象的创建造成重大冲击。动态方式所带来的更大的灵活性正是解决一般化编程问题的要点所在。

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

Java的垃圾回收器被设计用来处理内存释放问题(尽管它不包括清理对象的其他方面)。垃圾回收器“知道” 对象何时不再被使用,并自动释放对象占用的内存。这一点同所有对象都是继承自单根基类0bject以及只能以种方式创建对象(在堆上创建)这两个特性结合起来,使得用Java编程的过程较之用C++编程要简单得多,所要做出的决策和要克服的障碍也要少得多。

5.容器

存储对象?对于面向对象设计中的大多数问题而言,大多数解决方案:创建另一种对象类型。这种新的对象类型持有对其他对象的引用,称为容器。

6.异常机制

异常处理不是面向对象的特征,尽管在面向对象语言中异常常被表示称为一个对象。异常处理在面向对象语言出现之前就已经存在了。

第二章:一切都是对象

1.用引用操纵对象

可以拥有一种引用,并不一定需要有一个对象与它关联。

一种安全的做法就是,创建一个引用的同时便进行初始化。

2.数据存储方式
  • 寄存器:这是最快的存储区,因为它位于不同于其他存储区的地方----处理器内部,但是寄存器的数量极其有限,所以寄存器根据需求进行分配。你不能直接控制,也不能在程序中感觉到寄存器的存在的任何迹象。
  • 堆栈:位于通用RAM(随机访问存储器)中,通过堆栈指针可以从处理器获得直接支持。堆栈指针若向下移动,则分配新的内存,若向上移动,则释放那些内存。这是一种快速有效的分配存储方法,仅次于寄存器。创建程序时,java系统必须知道存储堆栈内所有项的确切的生命周期,以便上下移动堆栈指针。这一约束限制了程序的灵活性,所以虽然某些java数据存储于堆栈中----特别是对象的引用,但是java对象并不存储于其中
  • 堆:一种通用的内存池(也位于RAM区),用于存放所有的java对象。堆不同于堆栈的好处是:编译器不需要知道存储的数据在堆里存活多长时间。因此,在堆里面分配存储有很大的灵活性。当需要一个对象时,只需要用new写一行简单的代码,当执行这行代码时,会自动在堆里面进行存储分配。当然这种灵活行必须要付出相应的代价:用堆进行存储分配和清理可能比用堆栈进行存储分配需要更多的时间。
  • 静态存储(static storage):这里的“静态”是指“在固定的位置”(尽管也在 RAM 里)。静态存储里存放程序运行时一直存在的数据。你可用关键字 Static 来标识一个对象的特定元素是静态的,但 Java 对象本身从来不会存放在静态存储空间里。
  • 常量存储(constant storage):常量值通常直接存放在程序代码内部,这样做是安全的,因为它们永远不会被改变。有时,在嵌入式系统中,常量本身会和其他部分隔离开,所以在这种情况下,可以选择将其存放在ROM(只读存储器中)。
  • 非RAM存储:如果数据完全存活于程序之外,那么它可以不受程序的任何控制,**在程序没有运行时也可以存在。其中两个基本的例子是流对象和持久化对象。**在流对象中,对象转化成字节流,通常被发送给另一台机器。在"持久化对象"中,对象被存放于磁盘上,因此,即使程序终止,他们仍可以保持自己的状态。这种存储方式的技巧在于:**把对象转化成可以存放在其他媒介上的事物,在需要时,可以恢复成常规的,基于RAM的对象。**java提供了对轻量级持久化的支持,而诸如JDBC和Hibernate这样的机制提供了更加复杂的对在数据库中存储和读取对象信息的支持。
3.特例:基本类型

对于基本类型,不用new来创建变量,而是创建一个并非是引用的“自动”变量,这个变量直接存储值,并置于堆栈中,因此更加高效。

Java要确定每种基本类型的所占的存储空间的大小,它们的大小并不像其他大多数语言随机器硬件架构的变化而变化。这种所占存储空间大小的不变性是Java程序比用其他大多数语言编写的程序更具移植性的原因之一

4.Java中的数组

Java确保数组会被初始化,而且不能在它的范围之外被访问。这种范围检查,是以每个数组上少量的内存开销及运行时的下标检查为代价的。但由此换来的是安全性和效率的提高,因此付出的代价是值得的。

5.static关键字

当声明一个事物是static时,就意味着这个域或方法不会与包含它的那个类的任何对象实例关联在一起。所以,即使从未创建某个类的任何对象,也可以调用其static方法或访问其static域。通常,你必须创建一个对象,并用它来访问数据或方法。因为非static域和方法必须知道它们一起运作的特定对象

尽管当static作用于某个字段时,肯定会改变数据创建的方式(因为一个static字段对每个类来说都只有一份存储空间,而非static字段则是对每个对象都有一个存储空间),但是如果static作用于某个方法,差别却没那么大。static方法的一个重要用法就是在不创建任何对象的前提下,就可以调用它。

5.1 通过 static 关键字可以满足以下两方面情形的需要:
  • 只想为某一特定域分配单一存储空间,而不去考虑空间要创建多少对象,甚至根本就不创建任何对象。
  • 希望某个方法不与包含它的类的任何对象关联在一起。也就是说,即使没有创建对象,也能够调用这个方法
5.2 static方法
  • static作用于字段时,会改变数据的创建方式,但作用于方法时,差别却没有那么大。static方法的一个重要用法就是在不创建任何对象的前提下就可以调用它。这一点对定义main()方法很重要(所以main()方法是一个 static 方法),这个方法是运行一个应用时的入口点。
  • 和其它任何方法一样,static方法可以创建或使用与其类型相同的被命名对象,因此,static方法常常拿来做“牧羊人”的角色,负责看护与其隶属同一类型的实例群。
  • static方法的含义:static方法就是没有this的方法。关于static方法内部是否能调用非静态方法:因为没有this,就没有对象,所以不能直接调用非静态方法,但可以传递一个对象引用到静态方法里,然后通过这个引用(和this效果相同)来调用非静态方法和访问非静态数据成员。
  • 有些人认为static方法不是“面向对象”的,因为它们的确具有全局函数的语义;使用static方法时,由于不存在this,所以不是通过“向对象发送消息”的方式来完成的。

第三章:操作符

1.赋值

赋值操作符=表示取右值,把它复制给左值。右值可以是任何常数、变量或者表达式,左值必须是一个明确的、已命名的变量。也就是说,必须有一个物理空间可以存储等号右边的值。

基本类型的赋值:直接将一个地方的内容复制到了另一个地方。例如,对基本数据类型使用a=b,那么b的内容就复制给a。若接着又修改了a,而b根本不会受这种修改的影响。

对象的赋值:将一个对象赋值给另一个对象,实际是将“引用”从一个地方复制到另一个地方。即引用复制,指向相同的对象。原本的引用被覆盖,也就是丢失了,而不再被引用的对象会由gc自动清理。即假若对对象使用c=d,那么c和d都指向原本只有d指向的那个对象。

别名现象:是java操作对象的一种基本方式,当对引用型变量进行赋值操作时,可能出现别名现象,两个引用变量共同指向同一个对象,当一个引用对对象进行改变时,另一个引用变量的对象值也跟着改变。

2.窄化转型

窄化转换,将能容纳更多信息的数据类型转换成无法容纳那么多信息的类型,有面临信息丢失的危险。

扩展转换,则不必显式地进行类型转换,因为新类型肯定能容纳旧类型,不会造成任何信息的丢失。

如将float或double转型为整型值时,总是对该数字执行截尾。

转型:Java允许把任何基本数据类型转换成别的基本数据类型,除布尔类型外。对象可以在其所属类型的类族之间进行类型转换。

3.移位

如果对byte或short值进行移位运算,得到的可能不是正确的结果。它们会先被转换成int型,再进行右移操作,然后被截断,赋值给原来的类型。

4.Java没有sizeof()操作符

在C和C++中,sizeof()操作符可以告诉你为数据项分配的字节数。使用这个操作符的最大原因是为了进行一些与存储空间有关的运算,使程序可以在不同平台上“移植”。而Java不需要sizeof()操作符来满足这方面的需要,因为所有数据类型在所有机器中的大小是相同的。我们不必考虑移植问题——它已经被设计在语言中了。

第五章:初始化与清理

1.用构造器确保初始化
  • 创建对象时,如果其类具有构造器,Java就会在用户有能力操作对象之前自动调用相应的构造器,从而保证了初始化的进行。
  • 构造器采用与类相同的名称,构造器是一种特殊类型的方法,没有返回值。
  • 不接受任何参数的构造器叫默认构造器(无参构造器)。
  • 如果已经定义了一个构造器(无论是否有参数),编译器就不会帮你自动创建默认构造器。
2.方法重载

区分重载方法:独一无二的参数类型列表

涉及基本类型的重载:如果传入的数据类型(实际参数类型)小于方法中声明的形式参数类型,实际数据类型就会被提升。char型略有不同,如果无法找到恰好接受char类型的方法,就会把char型直接提升至int型。

传入的实际参数大于重载方法声明的形式参数:使方法接受较小的基本类型作为参数。如果传入的实际参数过大,就得通过类型转换来执行窄化转换。如果不这样做,编译器就会报错。

以返回值来区分重载方法是行不通的:有时并不关心方法的返回值,想要的是方法调用的其他效果,可能会调用方法而忽略其返回值。

3.this关键字
  • 在方法内部获得对当前对象的引用
  • 将当前对象传递给其他方法
  • 在构造器中调用构造器,但不能调用两个,且必须将构造器调用置于最起始处,否则编译会报错
4.清理:终结处理和垃圾回收

Java有垃圾回收器负责回收无用对象占据的内存资源,但垃圾回收器只知道释放那些经由new分配的内存,假如你的对象(并非使用new)获得一块“特殊”的内存区域,它不知道该如何释放该对象的这块“特殊”内存。为了应对这种情况,Java允许在类中定义一个名为finalize()的方法。

它的工作原理”假定”是这样的:一旦垃圾回收器准备好释放对象占用的存储空间,将首先调用finalize()方法,并且在下一次垃圾回收动作发生时,才会真正回收对象占用的内存。

所以要是你打算用finalize(),就能在垃圾回收时刻做一些重要的清理工作。

Java里的对象却并非总是被垃圾回收,即对象可能不被垃圾回收,垃圾回收并不等于"析构",垃圾回收只与内存有关。

只要程序没有濒临存储空间用完的那一刻,对象占用的空间就总也得不到释放。如果程序执行结束,并且垃圾回收器一直都没有释放你创建的任何对象的存储空间,则随着程序的退出,那些资源也会全部交还给操作系统。这个策略是恰当的,因为垃圾回收本身也有开销,要是不使用它,那就不用支付这部分开销了。

终结方法有一个致命的缺点就是:当一个对象变得不可达的时候,该对象的终结方法的执行时间是任意的,在大多数的情况下可能是不会执行的。

Java不允许创建局部对象,必须使用new创建对象。无论是垃圾回收还是终结,都不保证一定会发生。如果JVM并未面临内存耗尽的情形,它是不会浪费时间去执行垃圾回收以恢复内存的。

5.垃圾回收器如何工作

垃圾回收器对于提高对象的创建速度,具有明显的效果。

  • 分配新对象的时候,Java的堆指针只是简单地移动到尚未分配的区域。
  • gc工作的时候,将一面回收空间,一面使堆中的对象紧凑排列 。这样“堆指针”就可以很容易移动到更靠近传送带的开始处,也就尽量避免了页面错误。

垃圾回收技术:

  1. 引用记数(从未被应用)

简单但速度很慢的垃圾回收技术。每个对象都有一个引用计数器,当有引用连接至对象时,引用计数加1。当引用离开作用域或被置为null的时候,引用计数减1。虽然管理引用记数的开销不大,但这项开销在整个程序生命周期中将持续发生。gc会在含有全部对象的列表上遍历,当发现某个对象的引用记数为0时,就释放其占用的空间。这种方法的缺陷就是,如果对象之间存在循环引用,可能会出现“对象应该被回收,但引用计数却不为零”的情况。

  1. 自适应的垃圾回收技术

**对任何“活”的对象,一定能最终追溯到其存活在堆栈或静态存储区之中的引用。**这个引用链条可能会穿过数个对象层次。由此,如果从堆栈和静态存储区开始,遍历所有的引用,就能找到所有“活”的对象。对于发现的每个引用,必须追踪它所引用的对象,然后是此对象包含的所有引用,如此反复进行,直到“根源于堆栈和静态存储区的引用”所形成的网络全部被访问为止。至于如何处理找到的存活对象,取决于不同的虚拟机实现。

  • 停止-复制(stop-and-copy)

    先暂停程序的运行,然后将所有存活的对象从当前堆复制到另一个堆,没有被复制的都是垃圾。对象被复制到新堆的时候就会摆出紧凑排列。当把对象从一处移动到另一处时,所有指向它的引用全部被修正。效率低:①维护两个堆;②程序稳定后,垃圾少甚至没有,复制式回收器仍然会将所有内存自一处复制到另一处。

  • 标记-清扫(mark-and-sweep)

    程序进入稳定后,可能只会产生少量垃圾,如果还是复制到新堆中,就会很浪费。为避免这种情形,一些JVM会进行检查:如果没有新垃圾产生,就会转到另一种工作模式–标记清扫。对于一般用途而言,标记-清扫速度很慢,但是在垃圾很少的情况下,它的速度很快。

    工作思路:从堆栈和静态存储区出发,遍历所有的引用,进而找出所有存活的对象。每找到一个存活的对象,就会给对象设一个标记,这个过程不会回收任何对象。只有全部标记工作完成的时候,清理工作才会开始。在清理过程中,没有标记的对象将被释放,不会发生任何复制动作。所以剩下的堆空间是不连续的,gc要是希望空间连续的话,就得重新整理剩下的对象。

6.构造器初始化
  • 初始化顺序

在类的内部,变量定义的先后顺序决定了初始化的顺序。即使变量定义散布于方法定义之间,它们仍旧会在任何方法(包括构造器)被调用之前得到初始化。

  • 静态数据的初始化

1、无论创建多少个对象,静态数据都只占用一份存储区域。static关键字不能应用于局部变量,因此它只能作用于域。如果一个域是静态的基本类型域,且也没有对它进行初始化,那么它就会获得基本类型的标准初值;如果它是一个对象引用,那么它的默认初始化值就是null。

2、静态初始化只有在必要时刻才会进行。当对象被创建的时候,初始化才会开始。此后,静态对象不会再次被初始化。

3、初始化的顺序,先是静态对象,而后是非静态对象。

  • 显式的静态初始化

Java允许将对个静态初始化动作组织成一个特殊的静态子句(静态块),这段代码只执行一次。

  • 非静态实例初始化

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

7.数组初始化

将一个数组复制给另一个数组,只是复制了一个引用。

8.枚举类型

后,静态对象不会再次被初始化。

3、初始化的顺序,先是静态对象,而后是非静态对象。

  • 显式的静态初始化

Java允许将对个静态初始化动作组织成一个特殊的静态子句(静态块),这段代码只执行一次。

  • 非静态实例初始化

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

7.数组初始化

将一个数组复制给另一个数组,只是复制了一个引用。

8.枚举类型

当创建enum时,编译器会自动添加一些有用的特性。例如,它会自动创建toString()方法,以便你可以方便地显示某个enum实例的名字。还会创建ordinal()方法,用来表示某个特定enum常量的声明顺序,以及static value()方法,用来按照enum方法的声明顺序,产生有这些常量值构成的数组。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值