如果看过 《Thinking in Java》 就应该对作者的风格有所了解,tij 被捧为 Java 圣经,却又被称为新手劝退书,就是因为作者本身已经有非常丰富的编程经验了,也累积了很多对于编程的想法,所以一开始上来就给你来了一大堆高维度的概念,这些概念可能会引发同样经验丰富的编程老手的共鸣,却也会让零基础刚接触编程的新手不知所谓,就像小时候家长给你说了一堆道理,等你长大碰见事儿之后才真正明白那些话背后的意义。

而这本书 《On Java8》 作为事实上的 TIJ 第五版,作者延续了他的风格,第一章就提出了面向对象编程的最核心的概念 —— 对象的概念

同时也提出了 Java 中最核心的特性:

  • 抽象
  • 接口
  • 封装
  • 复用
  • 继承
  • 多态
  • 单继承接口
  • 集合容器
  • 对象的创建与生命周期
  • 异常处理

这些几乎是一本书的内容了,所以一章更多的是对全书的内容做一个大概的总结,这篇读书笔记是重构的,看完之后再回过头总览,发现了作者的写作脉络。

这一次的重构我大幅度总结了每个小节比较核心的内容,同时添加了一些自己的理解,之前的读书笔记风格是大段的摘抄,并在其中加入自己的一些想法作为批注的形式,后来发现这一的话并没有办法一眼看到最核心的东西,效率并不高,于是改为核心点总结的方式。

这一章是从宏观的角度来阐述对象以及面向对象的编程的几个核心概念:

  • 接口
  • 封装
  • 继承
  • 多态

以及 Java 中的一些基本处理机制:

  • 异常处理
  • 对象的创建与生命周期
  • Collections 集合框架
  • Java 的单继承结构与 C++ 进行对比的优势

但是这些东西吧,如果是零基础刚入门的人看毫无意义,因为太空了,每个点都是后面的一个章节的内容,而对应的章节存在着大量的示例代码,那是我认为真正有意义的东西,有具体的代码支持,再去看作者说的思想,才能理解一部分。

而"思想"这玩意,我认为见仁见智,没有绝对的正确思想,我个人认为作者的思想是比较绕的,以前我觉得是翻译的问题,后来这本书我有一部分是照着原文看的,也是属于技术书籍中比较晦涩的那种,我觉得价值有限。

真要学习编程的思想的话更建议去看 《Code Complete2》 代码大全2 这本书。


"我们没有意识到管用语言的机构有多大的力量。可以毫不夸张的说,它通过语义反应机制奴役我们。语言表现出来并在无意识中给我们留下深刻的印象的结构会自动投射到我们周围的世界。"

计算机革命的起源来自机器。编程语言就像是那台机器。它不仅是我们思维放大的工具与另一种表达媒介,更像是我们思想的一部分。

面向对象编程是一种编程思维方式和编码架构。本章讲述 OOP 的基本概述。

抽象

编程语言本身就是不断的抽象,汇编语言是对底层机器码的抽象,C 语言是对汇编语言的抽象,我们通过抽象不断封装复杂度,也让编程语言更加容易上手。

早期程序员要在机器模型与实际解决问题模型之间建立关联,这消耗了额外精力,并且这项工作脱离了编程语言本身。

面向对象的程序设计思想在此基础上前进了一大步,程序员将问题空间中的元素以及解决方案中的表示都称为 「对象 Object」 OOP 让我们根据问题来描述问题,而不是根据与运行解决方案的计算机,剥离出了与业务无关的部分。

Alan Kay 总结了 面向对象的五大特征 ,我们可理解纯粹的面向对象程序设计方法是什么样的:

  1. 万物皆对象。你可以将对象想象成一种特殊的变量。它存储数据,但可以在你对其发出请求时执行本身的操作。【理论上讲,你总是可以从要解决的问题身上抽象出概念性的组件,然后在程序中将其表示为一个对象。】
  2. 程序是一组对象,通过消息传递来告知彼此该做什么。 要求调用一个对象的方法,你需要向该对象发送消息。【参数的传递,方法的调用】
  3. 每个对象都有一种类型。 根据语法,每个对象都是某个类的一个实例。其中类(Class)是类型(Type) 的同义词,一个类最重要的特征就是"能将什么消息发给它"
  4. 每个对象都有自己的存储空间,可容纳其他对象。 或者说,通过封装现有对象,可制作出新型对象。【组合】所以尽管对象的概念非常简单,但在程序中却可达到任意高的复杂程度。
  5. 同一类所有对象都能接收相同的消息。 这实际是别有含义的一种说法。由于类型“圆(Cricle)”的一个对象也属于类型为 形状(shape)的一个对象,所以一个圆完全能接收发送给“形状”的消息。这意味着可以让程序代码统一指挥“形状“ ,令其自动控制所有符合 “形状” 描述的对象,其中自然包括“圆”。这一对象称为对象的可替换性,是 OOP 最重要的概念之一。【也就是多态

Grady Booch 提供了 对象 更简洁的描述:

  • 一个对象具有自己的 状态行为标识 。这意味着对象有自己的内部数据(提供状态)、方法产生行为)并彼此区分(每个对象在内存中有唯一的地址

个人认为上面这个定义更加直指核心,并且更加准确。

接口

有了对象就存在对象的类型。

Java 中使用 class 关键字来描述类型,被称为类,类作为对象创建的模板,一个类可以根据情况生成许多对象、

但是对象想要完成工作就必须具备某些功能,就像电灯可以被点亮,用来照明,如果将电灯抽象为类,那么它就具备以下几个方法:

  • 开灯
  • 关灯
  • 调节亮度
  • RGB 灯效(如果有的话)

image-20201108220044672

这里的灯作为对象,同时它也作为一个 接口 , 接口从更广范围上定义了对象的功能。

比如灯这个接口分为了相当多的种类:

  • 台灯
  • 车灯
  • 屏幕挂灯

它们是 "灯" 这个 接口 的具体实例,都实现了 "开灯" 这个方法,但是具体的实现又各自不同:

  • 台灯可以接入 220V 的交流电压,亮度可能更大
  • 车灯则接入汽车的电瓶,同时还具备远光、近光 这些子类特有的方法
  • 屏幕挂灯则是接入5V的充电器,同时具备"非对称光源" 这种特性

服务提供

面向对象的程序,我们将对象看作 服务的提供者

  • 从宏观来看,你写的整个应用程序为用户提供了相应的服务。
  • 从微观来看,程序中的一个个对象提供提供了不同的服务,将它们组合在一起,形成了整个应用程序。

我们开发程序就是将功能分解,抽象成一组服务,根据软件开发的基本原则: 高内聚、低耦合,将各个组件有机的结合在一起,提供不同的功能,形成一个整体,同时组件又可以重用,为别的组件提供对应的服务。

在良好的面向对象设计中,每个对象的功能单一且高效。

封装

程序员可以分为应用开发程序员,和基础设施开发程序员。后者为前者提供基础组件或框架供前者使用,二者分工合作,共同开发程序。

基础组件或类库一般都希望仅提供接口,而隐藏实现。 就像你买个电视或者空调,你只需要知道点击遥控器上的开机键就可以打开设备,而并不需要知道其内部的具体开机流程。

这种细节的隐藏在程序世界中就称为封装,封装存在的意义非常重要:

  • 减少程序出错的可能,将细节屏蔽之后也就杜绝了细节被修改的可能
  • 职责划分更加清晰,只通过接口甚至网络 Rest API 进行协同

Java 中有对应的关键字提供了类访问权限的定义:

  • public —— 公开级别,任何人都可以访问 public 的字段 或调用 public 的方法。
  • protected —— 只有子类可以访问。
  • private —— 私有,只有类中可以访问。

一般建议只要是类中自己使用的数据或方法,都定位 private 的,这样就避免暴露给别人,也就可以在后续时放心的修改,否则如果修改了 public 的方法,则会影响所有调用了该方法的类,可能影响到别的开发者。

复用

类是可以复用,比如一个类实现了将文件传输到 AWS 中 S3 桶里的功能,那么只要需要用到这个功能的地方,都可以调用这个类的方法,这也是面向对象的优点之一。

而在代码级别的复用,则可以使用 "组合" 这个方式,即类中持有要使用类的引用,然后只要实例化一个对象赋值给这个引用,就可以使用这个对象对应的方法了。

继承

子类可以继承父类的方法与字段,这是 Java 中的核心机制,同时子类可以覆盖父类的方法,并且添加自己的方法,这就为程序的复用提供了相当大的遍历,并且也为扩展留下了可能。

多态

同样是 Java 中的核心机制,子类通常被看作和父类是同一个类型。 而我们在写方法操作对象时,定义的参数可以定义父类型,这样就具有相当大的灵活性,任何该父类的子类都可以适用于整个方法。

这种能力减少了软件维护的代价,也产生了一条准则: 面向接口编程 ,因为任何实现了接口的类都可以视作是这个接口,而你编写一个参数为接口类型的方法,则

所有实现了接口的类都适用这个方法,我们并不知道要操作哪个具体类,只需要知道这一系列的类都适用就行了。

如下图的例子

BirdController 对象和通用 Bird 对象中,BirdController 不知道 Bird的具体确切类型却还能正常工作

BirdConroller 的角度来看,这是很方便的,因为它不需要编写特别的代码来确定 Bird 的具体确定类型或行为。 那么在调用 move(); 方法时是如何保证发生正确的行为(鹅走路,飞,或游泳;企鹅走路或游泳)的呢?

这个问题的答案,是面向对象设计的妙诀

  • 在传统意义上,编译器不能进行函数调用。

由非 OOP 编译器产生的函数调用会引起所谓的 早期绑定, 这个术语你可能从未听说过,不会想过其他的函数调用方式。 这意味着编译器生成对特定函数名的调用,该调用会被解析为将执行的代码的绝对地址。【也就是编译的时候具体的调用方法就已经被确定】

通过继承,程序直到运行时才能确定代码的地址。【因为父类引用可以指向子类对象】,因此发消息给对象时,还需要其他一些方案。为了解决这个问题,面向对象语言使用 后期绑定的概念。当向对象发送消息时,被调用的代码直到运行时才确定。

编译器确保方法存在,并对参数和返回值执行类型检查,但是它不知道要执行的确切代码。

为了执行后期绑定,Java 使用了一个特殊的代码位来代替绝对调用。 这段代码使用对象中存储的信息来计算方法主体的位置。因此每个对象的行为根据特定代码位的内容而不同。当你向对象发送消息时,对象知道该如何处理这条消息。

在某些语言中,必须显式地授予方法后期绑定属性的灵活性。例如 C++ 使用 virtual 关键字。 在这些语言中,默认情况下方法不是动态绑定的。

在Java中,动态绑定是默认行为,不需要额外的关键字来实现多态性。

以上内容想要深入学习建议阅读《深入理解 JVM 虚拟机》中相关章节。

单继承接口

Java 中的类只能继承一个类,即 extends 后面只能跟一个类,所以继承结构是单一的,但是可以实现多个接口,这带来了灵活性。

集合

真正的工程中很少操作单个对象,对象被放在不同的容器中,JDK 中的 Collection Framework 相关类 比如:

  • ArrayList
  • HashMap

可能是我们使用的最多的类,这些不同的容器有着不同的底层数据结构,有着不同的特性,合理根据场景使用对应的容器也是一个合格的 Java 开发者必备的修养,有的集合很适合只读场景,有的集合则修改起来非常快速。

集合又与泛型结合在一起使用,进一步的提升了开发效率。

对象创建与生命周期

面向对象的核心是对象,则必须关注对象的创建与销毁,具体的内容可以看 《深入学习JVM》中 Java 内存模型相关的,了解 Java 是如何划分对象的存储空间,以及不同的空间有着怎样的对象回收机制。

不过大多数开发者可能记得最牢的就是 「对象在堆中,引用在栈中」 这是一个非常粗粒度的划分,却也是最多人牢记的一个法则。

异常处理

程序中无法避免出现错误,错误可能来自于很多方面:

  • 前端输入的参数错误 【所以永远要对前端传来的参数进行校验,这也被称为防御性编程】
  • 程序运行时某个流程出现错误 —— 例如读取某文件,结果文件并不存在,这对应了 Java 的运行时异常,这种异常一般是不显示捕获的。
  • 某种处理可能导致异常的发生,例如线程的 sleep 可能导致 线程被 interrupt 终止时产生异常,这种被定义的 Exception 需要程序员手动处理,这种异常称为 被检查异常。

我们写程序可能正确的流程相关代码只占20%,剩下都是异常流程的处理,写出一个健壮安全的程序也是我们的追求,毕竟程序哪怕慢一点也比出错要强太多了。

小结

这些概念如果是刚接触编程的新人,真的不用纠结,留个印象就好。

Q.E.D.

知识共享署名-非商业性使用-相同方式共享 4.0 国际许可协议

最是人间留不住,曾是惊鸿照影来。