码出高效Java开发手册
面向对象的理念(OOP)
面向过程编程让计算机有步骤地顺序地做一件事情,是一种过程化地叙事思维。但是在大型软件开发过程中,使用面向过程语言开发,软件维护,软件复用存在着巨大的困难。
面向过程是用来解决一个问题的最好的思想,面向过程最主要的是过程,好的过程就会让一个问题快速正确的获得正确的答案。
但在软件开发方面,会出现很多很多的功能,即出现很多很多的问题需要解决,对于面向对象思想无疑会产生很多问题。
- 无法复用代码,在不同问题中出现相同的子问题,就需要又去复用之前出现的代码,无疑是很麻烦的。
- 耦合度高。面向过程的代码不断穿插,就会让软件的耦合度过高,这样只要一个小问题出现了错误,就有可能导致整个大的问题发生错误,这是我们不想看到的。
面向对象思想的想法就是为了解决这样的问题。
面向对象的特性
传统意义上,面向对象有三大特性:封装,继承,多态。本书明确将“抽象”作为面向对象的特性之一,支持面向对象“四大特性”的说法。
Java之父Gosling设计的Object类,是任何类的默认父类,是对万事万物的抽象,是在哲学方向上的延伸思考,高度概括了事务的自然行为和社会行为。Object还对哲学的三个经典问题进行了隐约的解答。
- 我是谁? getClass()说明本质上是谁,而toString()是当前我的名片。
- 我从哪里来?Object()构造方法是生产对象的基本方式,clone()是繁殖对象的另一种方式。
- 我到哪里去?finalize()是在对象销毁时触发的方法。
Object还映射了社会科学领域的一些问题。
- 世界是否与你而不同?hashCode()和equals()就是判断与其他元素是否相同的一组方法。
- 与他人如何协调?wait()和notify()是对象间通信与协作的一组方法。
Object的类是抽象的核心体现。他将万物抽象成一个Object,之后每一个类都继承于Object,将Object类当成一个模板,去将事物抽象成一个类。
封装。封装是将一个类中的行为和属性进行的信息控制,决定是否要公开对类行为和属性的控制。外界只要知道调用这个行为方法,就可以实现一些功能,而不用知道如何去内部是怎样实现的。书中有句话说得很好,模块之间的协作只需要忠于接口,忠于功能实现即可。
继承。类将一些复用的行为和属性赋予子类,也就是子类继承了父类,从而复用了父类的代码,也为多态打下基础。
继承的成本很低,一个关键字就可以使用别人的方法,所以继承像抗生素一样容易被滥用,我们传递的理念是谨慎使用继承,认清继承滥用的危险性,即方法污染和方法爆炸。
方法污染是指父类具备的行为,通过继承传递给子类,子类并不具备执行此行为的能力,这就是方法污染。(因为父类有些方法不希望子类使用)
方法爆炸是指继承树不断扩大,底层类拥有的方法虽然都能执行,但是由于方法众多,其中部分方法并非于当前类功能定位相关,多次继承后达到上百个方法,造成了方法爆炸。
多态。根据运行时不同对象的类型,同一个方法产生不同的运行结果,使同一个行为具有不同的表现形式。
接口与抽象类
抽象类是模板式设计,而接口是契约式设计。
抽象类和接口是对一个事物更高级的抽象,抽象类通常包括抽象方法,实体方法和属性变量,而接口一般只是一组行为,没有具体实现和属性。
抽象类是模板,意义是给子类提供一个抽象的模板,接口是一个契约,继承该接口就要实现里面的所有方法,即遵循这个契约。
覆写
多态中的override,本书翻译成覆写。如果翻译成重写,那么与重构意思过于接近;如果翻译成覆盖,那么少了“写”这个核心动词。如果父类定义的方法达不到子类的预期,那么子类可以重新实现方法覆盖父类的实现。(实现多态的方式)父类引用执行子类方法时需要注意
- 无法调用到子类中存在而父类本身不存在的方法。
- 可以调用到子类覆写了父类的方法,这是一种多态的实现。
覆写父类的方法,需要满足以下四个条件。
- 访问权限不能变小 。
- 返回类型能够向上转型成父类的返回类型。这里的向上转型必须是严格的继承关系,数据类型基本不存在通过继承向上转型的问题。比如
int
和Integer
,不会自动给装箱,是非兼容返回类型。 - 异常也要能向上转型成为父类的异常。
- 方法名,参数类型及个数必须严格一致。
每一个覆写方法最好加上@Override
注解,编译器会自动检查覆写方法签名是否一致。
重载
在同一个类中,如果多个方法有相同的方法名称,不同的参数类型,参数个数,参数顺序,即为重载。(方法返回值不在考虑范围)
JVM在重载方法中,选择合适的目标方法的顺序如下:
- 精确匹配。
- 如果是基本数据类型,自动转换成更大表示范围的基本类型。
- 通过自动拆箱与装箱。
- 通过子类向上转型继承路线依次匹配。
- 通过可变参数匹配。
父类的公有实例方法与子类的公有实例方法可以存在重载关系。不管继承关系如何复杂,重载在编译时可以根据规则知道调用哪种目标方法。所以重载又称为静态绑定。
基本数据类型
虽然Java是面向对象编程语言,一切皆是对象,但是为了兼容人类根深蒂固的数据处理习惯,加快常规数据的处理速度,提供了9种基本数据类型,它们都不具备对象的特性,没有属性和行为。
9种数据类型包括:boolean,byte,char,short,int,long,float,double,refvar。
refvar是面向对象世界中的引用变量,也叫引用句柄。本书认为它也是一种基本数据类型。
其中的boolean是一个比较特殊的存在。它的值为false或者是true。在计算机的世界,0表示false,1表示true。所以常规思想会认为booealn的大小为1字节或1位。
Java虚拟机描述,虽然定义了Boolean数据类型,但是对它的支持有限,所以在编译时用int
数据类型来代替,所以会占用4个字节。但时Boolean数组在编译时会被编译成byte
数组,所以数组中Boolean占用1个字节。
refvar无论指向什么,都是占用4个字节的空间。
代码风格
书的第三章一章书都是在讲代码风格。
对于我这种有一点代码洁癖的人来说,还是很建议去学习一套规范的代码风格的。
这里讲的不是很详细,详细的可以去看一下《阿里巴巴Java开发手册》那本书,一本小册子写了记录了很多的代码规范。
IDEA也有一个插件,用阿里巴巴的代码规范来规范你的代码,多写些规范的代码,利于别人也利于自己。
字节码
如果某个程序因为不同的硬件平台需要编写多套代码,这是十分令人崩溃的。Java的使命就是一次编写,到处执行。在不同的操作系统,不同的硬件平台上,均可以不用修改代码即可顺畅地执行,如何实现跨平台?有一个声音在天空中回响:计算机工程领域的任何问题都可以通过一个中间层来解决。因此,中间码应运而生,即“字节码”(Bytecode)。
Java之所以会这么流行,离不开它的一次编写,到处执行的跨平台的这个特性。不同平台的Java文件(.java),都会编译产生字节码文件(.class),从字节码文件这一中间层屏蔽了上层操作系统(平台)的差异性,只要安装了Java的环境,都能运行出Java文件,能解决很多平台之间的运行问题。
Java内存结构
堆
堆分成了两大块,新生代和老年代(它们之间的占比为1:2)。新生代又分为三个区域Eden区和两个Survivor区(它们占比为8:1:1)。
首先新创建的对象会放在Eden区上,等Eden区满了放不下新创建的对象了,就会进行YGC,将Eden区中和其中一个Survivor区(from区)活着的对象放入另一个一个Survivor区(to区)。以此循环。
每一个对象都会有一个计数器,经历了一次YGC之后,对象的计数器就会加一,当到达了一定阈值,就会进入老年代(Java默认为15)。
新生代进入老年代的两种方法:
- 对象的计数器到达了阈值,进入老年代。
- Eden区放不下新创建的对象,进行了YGC之后也放不下,就会提前将该对象晋升为老年代(过早老化)。
方法区
方法区是JVM规定的一个区域,不是一个实在的区域,只是一个定义。
Java8之前是用永久代来实现了方法区,Java8之后是用元空间来实现方法区。
永久代包括了运行时常量池(比如String)和一些类元信息(类加载的文件,静态变量等)。
元空间只保存了类元信息,运行时常量池已经放入堆中进行存储。
不同与永久代,元空间在本地内存中分配。
虚拟机栈和本地方法栈
虚拟机栈是描述Java方法执行的内存区域,它是线程私有的。栈中的元素用与支持虚拟机进行方法调用,每个方法从开始调用到执行完成的过程,就是栈帧从入栈到出栈的过程。
可以说,栈帧就是一个方法执行过程的具体体现。
栈帧主要包括四个部分。
- 局部变量表。存放方法参数和局部变量的区域。
- 操作栈。操作栈是一个初始状态为空的桶式结构栈。在方法执行过程中,会有各种指令往栈中写入和提取信息。
- 动态连接。每个栈中中包含一个在常量池中对当前方法的引用,目的是支持方法调用过程的动态连接。
- 方法返回地址。方法执行有两种退出情况。一,正常退出。二,异常退出。无论何种退出情况,都将返回至方法被调用的位置。
虚拟机栈主内,本地方法栈主外。这个内外是针对JVM来说的,本地方法栈为Native方法服务的。
程序计数寄存器
每个线程在创建后,都会产生自己的程序计数器和栈帧,程序计数器用来存放执行指令的偏移量和行号指示器等,线程执行或恢复都要依赖程序计数器。程序计数器在各个线程之间互不影响,此区域也不会发生内存溢出的异常。
try-catch-finally
- try代码块
监视代码执行过程,一旦发现异常则直接跳转到catch,如果没有catch,就直接跳转到finally。catch代码块
可选执行的代码块,如果没有任何异常发生则不会执行;如果发现异常则进行处理或向上抛出。finally代码块
必选执行代码块,不管是否有异常产生,即使发售鞥OutOfMemoryError也会执行,通常用于处理善后清理工作。如果finally代码块没有执行,则会有三种情况。
- 没有进入try代码块
- 进入try代码块,但是代码运行中出现了死循环或死锁状态
- 进入try代码块,但是执行了System.exit()操作
fail-fast机制
fail-fast机制是几何世界中比较常见的错误检测机制,通常出现在遍历集合元素的过程中。
java.util下的所有集合类都是fail-fase机制的。
比如在这些集合中,使用foreach遍历元素时进行删除,就会出现异常。
ConcurrentHashMap
需要注意的时,无论是JDK7还是JDK8,ConcurrentHashMap的size()方法都只能返回一个大概数量,无法做到100%精确,因为已经统计过的槽在size()返回最终结果前可能又出现了变化,导致返回大小与实际大小存在些许差异。在多个槽的设计下,如果仅仅是为了统计元素数量而停下所有操作,又会显得因噎废食。因此,ConcurrentHashMap在涉及元素总数的相关更新和计算时,会最大限度地减少锁的使用,以减少线程间地竞争与互相等待。在这个设计思路上,JDK8的ConcurrentHashMap对元素总数的计算又做了进一步的优化,具体表现:在put(),remove()和size()方法中,涉及元素总数的更新和计算,都彻底避免了锁的使用,取而代之的是众多的CAS操作。
线程安全
线程安全问题只在多线程环境下才出现,单线程串行执行不存在此问题。保证高并发场景下的线程安全,可以从以下四个纬度考量
- 数据单线程内可见。
- 只读对象
- 线程安全类
- 同步与锁机制
线程安全的核心理念就是“要么只读,要么加锁”。
该注意的是线程安全的主体是对象,而不是线程。
一般我们说线程安全的主语都是对象。
Java并发编程实践中对线程安全的定义
当多个线程访问一个对象时,如果不用考虑这些线程在运行时环境下的调度和交替执行,也不需要进行额外的同步,或者在调用方进行任何其他的协调操作,调用这个对象的行为都可以获得正确的结果,那这个对象就是线程安全的。
volatile
volatile的英文本已是“挥发,不稳定的”,延申意义为敏感的。当使用volatile修饰变量时,意味着任何对此变量的操作都会在内存中进行,不会产生副本,以保证共享变量的可见性,局部阻止了指令重排的发生。
“volatile是轻量级的同步方式”这种说法是错误的。它只是轻量级的线程操作可见方式,并非同步方式,如果是多写场景,一定会产生线程安全问题。
volatile关键字的特性
- 保证可见性
- 不保证原子性
- 禁止指令重排
读书感谢
这本书的前面半部分讲的通俗易懂,讲的很好,但是到后面有几章通过源码来进行讲解部分类与方法,这里就开始接触到部分工具的底层,如果没有一定的基础去看的话会很累,而且也会很繁琐。这些知识点很重要,作者想尽量写全,但是书的篇幅是有限的,所以就会感觉后面讲源码的部分看的体验就不是很好,有些地方也没有更深入的去讲。总的来说还是一本很好的书,适合学习Java的人观看,但是后面的一些知识点需要更加了解的话,就还需要通过其他途径,作者只是给各位读者开个头,了解一下,也是查缺补漏。最后孤尽老师还写了段话送给大家。
最后,做一个有技术情怀的人。技术情怀总结成两个关键字:热爱,卓越。热爱是一种源动力,卓越是一种境界。兴趣是最好的老师,也是最好的动力。而热爱是一种信念,即使痛苦,也不会让你背离这份事业和内心的执着。对技术的热爱,让人用于归根究底,勇于坐冷板凳,勇于回馈别人。极致与卓越,似乎是一个意思,即出类拔萃,超出期望。技术情怀提倡我们追求极致式的卓越,把卓越在往前提升。不管一个人如何卓越与优秀,都要学会自我驱动,持续进步,追求个人内心的极致。因为卓越,所以经典,只有这样百尺竿头,才能更进一步。仰望星空的同时,是脚踏实地,这样才能不断地学习和打磨自己。