Java编程的逻辑

马俊昌

第1章 编程基础

  • 所谓程序,基本上就是告诉计算机要操作的数据和执行的指令序列,即对什么数据做什么操作

1.1 数据类型和变量

  • ❑ 整数类型:有4种整型byte/short/int/long,分别有不同的取值范围; ❑ 小数类型:有两种类型float/double,有不同的取值范围和精度; ❑ 字符类型:char,表示单个字符; ❑ 真假类型:boolean,表示真假。
  • 对象是由基本数据类型、数组和其他对象组合而成的一个东西,以方便对其整体进行操作。
  • 变量就是给数据起名字,方便找不同的数据,它的值可以变,但含义不应变。

1.2 赋值

  • 声明变量之后,就在内存分配了一块位置,但这个位置的内容是未知的,赋值就是把这块位置的内容设为一个确定的值。
  • 整数类型有byte、short、int和long,分别占1、2、4、8个字节
  • 之所以需要加L或l,是因为数字常量默认为是int类型。
  • 但数组有两块:一块用于存储数组内容本身,另一块用于存储内容的位置。

1.3 基本运算

  • 取模运算适用于整数和字符类型,其他算术运算适用于所有数值类型和字符类型。
  • 取模(%)就是数学中的求余数,例如,5%3是2,10%5是0。
  • 整数相除不是四舍五入,而是直接舍去小数位
  • 放在变量后(a++)是先用原来的值进行其他操作,然后再对自己做修改,而放在变量前(++a)是先对自己做修改,再用修改后的值进行其他操作。
  • 对于数组,==判断的是两个变量指向的是不是同一个数组,而不是两个数组的元素内容是否一样,即使两个数组的内容是一样的,但如果是两个不同的数组,==依然会返回false
  • 则b的值还是0,因为||会“短路”,即在看到||前面部分就可以判定结果的情况下,忽略||后面的运算。

1.4 条件执行

  • if/else if/else陷阱:需要注意的是,在if/else if/else中,判断的顺序是很重要的,后面的判断只有在前面的条件为false的时候才会执行。
  • 表达式值的数据类型只能是byte、short、int、char、枚举和String(Java 7以后)
  • 每条case语句后面都应该跟break语句,否则会继续执行后面case中的代码直到碰到break语句或switch结束。
  • 单一条件满足时,执行某操作使用if;根据一个条件是否满足执行不同分支使用if/else;表达复杂的条件使用if/else if/else;条件赋值使用三元运算符,根据某一个表达式的值不同执行不同的分支使用switch
  • 如果分支比较多,使用条件跳转会进行很多次的比较运算,效率比较低,可能会使用一种更为高效的方式,叫跳转表。

1.5 循环

  • foreach不是一个关键字,它使用冒号:,冒号前面是循环中的每个元素,包括数据类型和变量名称,冒号后面是要遍历的数组或集合(第9章介绍),每次循环element都会自动更新。对于不需要使用索引变量,只是简单遍历的情况,foreach语法上更为简洁。
  • break用于提前结束循环。
  • continue语句会跳过循环体中剩下的代码,然后执行步进操作。
  • 解决复杂问题的基本策略是分而治之,将复杂问题分解为若干相对简单的子问题,然后子问题再分解为更小的子问题……程序由数据和指令组成,大程序可以分解为小程序,小程序接着分解为更小的程序。

1.6 函数的用法

  • 使用函数来减少重复代码和分解复杂操作
  • 返回值:函数可以没有返回值,如果没有返回值则类型写成void,如果有则在函数代码中必须使用return语句返回一个值,这个值的类型需要和声明的返回值类型一致。
  • String[] args表示从控制台接收到的参数
  • 定义函数时声明参数,实际上就是定义变量,只是这些变量的值是未知的,调用函数时传递参数,实际上就是给函数中的变量赋值。
  • 程序从main函数开始执行,碰到函数调用的时候,会跳转进函数内部,函数调用了其他函数,会接着进入其他函数,函数返回后会继续执行调用后面的语句,返回到main函数并且main函数没有要执行的语句后程序结束。
  • 数组作为参数与基本类型是不一样的,基本类型不会对调用者中的变量造成任何影响,但数组不是,在函数内修改数组中的元素会修改调用者中的数组内容。
  • 一个数组变量有两块空间,一块用于存储数组内容本身,另一块用于存储内容的位置,给数组变量赋值不会影响原有的数组内容本身,而只会让数组变量指向一个不同的数组内容空间。
  • 可变长度参数的语法是在数据类型后面加三个点“... ”,在函数内,可变长度参数可以看作是数组。可变长度参数必须是参数列表中的最后一个,一个函数也只能有一个可变长度的参数。
  • 同一个类中函数名相同但参数不同的现象,一般称为函数重载。

1.7 函数调用的基本原理

  • 栈一般是从高位地址向低位地址扩展,换句话说,栈底的内存地址是最高的,栈顶的是最低的。
  • 对于数组和对象类型,我们介绍过,它们都有两块内存,一块存放实际的内容,一块存放实际内容的地址,实际的内容空间一般不是分配在栈上的,而是分配在堆(也是内存的一部分,后续章节会进一步介绍)中,但存放地址的空间是分配在栈上的。

2.2 小数的二进制表示

  • 为什么要叫浮点数呢?这是由于小数的二进制表示中,表示那个小数点的时候,点不是固定的,而是浮动的。
  • 32位格式中,1位表示符号,23位表示尾数,8位表示指数。64位格式中,1位表示符号,52位表示尾数,11位表示指数。

2.3 字符的编码与乱码

  • 编码有两大类:一类是非Unicode编码;另一类是Unicode编码。
  • GB2312固定使用两个字节表示汉字,在这两个字节中,最高位都是1,如果是0,就认为是ASCII字符。
  • 乱码有两种常见原因:一种比较简单,就是简单的解析错误;另外一种比较复杂,在错误解析的基础上进行了编码转换。

2.4 char的真正含义

  • char本质上是一个固定占用两个字节的无符号正整数,这个正整数对应于Unicode编号,用于表示那个Unicode编号对应的字符。

第3章 类的基础

  • Java定义了8种基本数据类型:4种整型byte、short、int、long,两种浮点类型float、double,一种真假类型boolean,一种字符类型char。其他类型的数据都用类这个概念表达。

3.1 类的基本概念

  • 类也确实只是函数的容器,但类更多表示的是自定义数据类型。
  • static表示类方法,也叫静态方法,与类方法相对的是实例方法。实例方法没有static修饰符,必须通过实例或者对象调用,而类方法可以直接通过类名进行调用,不需要创建实例。
  • 类变量和实例变量都叫成员变量,也就是类的成员,类变量也叫静态变量或静态成员变量。类方法和实例方法都叫成员方法,也都是类的成员,类方法也叫静态方法。
  • 类型本身具有的属性通过类变量体现,经常用于表示一个类型中的常量。
  • final在修饰变量的时候表示常量,即变量赋值后就不能再修改了。
  • ❑ 类方法只能访问类变量,不能访问实例变量,可以调用其他的类方法,不能调用实例方法。 ❑ 实例方法既能访问实例变量,也能访问类变量,既可以调用实例方法,也可以调用类方法。
  • 方法要执行需要被调用,而实例方法被调用,首先需要一个实例。实例也称为对象,我们可能会交替使用。
  • 通过对象来访问和操作其内部的数据是一种基本的面向对象思维。
  • STATIC_TWO=2;语句外面包了一个static {},这叫静态初始化代码块。静态初始化代码块在类加载的时候执行,这是在任何对象创建之前,且只执行一次。
  • this表示当前实例

3.2 类的组合

  • 字符串常量用双引号括起来(注意与字符常量区别,字符常量是用单引号)
  • 在设计线时,我们考虑的层次是点,而不考虑点的内部细节。每个类封装其内部细节,对外提供高层次的功能,使其他类在更高层次上考虑和解决问题,是程序设计的一种基本思维方式。
  • 想想现实问题有哪些概念,这些概念有哪些属性、哪些行为,概念之间有什么关系,然后定义类、定义属性、定义方法、定义类之间的关系。概念的属性和行为可能是非常多的,但定义的类只需要包括那些与现实问题相关的就行了。
  • 两个类之间可以互相引用,MyFile引用了MyFolder,而MyFolder也引用了MyFile

3.3 代码的组织机制

  • 为避免命名冲突,Java中命名包名的一个惯例是使用域名作为前缀,因为域名是唯一的,一般按照域名的反序来定义包名,比如,域名是apache.org,包名就以org.apache开头
  • 有一种特殊类型的导入,称为静态导入,它有一个static关键字,可以直接导入类的公开静态方法和成员。
  • import是编译时概念,用于确定完全限定名,在运行时,只根据完全限定名寻找并加载类

第4章 类的继承

  • 使用继承一方面可以复用代码,公共的属性和行为可以放到父类中,而子类只需要关注子类特有的就可以了;另一方面,不同子类的对象可以更为方便地被统一处理。

4.1 基本概念

  • 在new的过程中,父类的构造方法也会执行,且会优先于子类执行。
  • super的使用与this有点像,但super和this是不同的,this引用一个对象,是实实在在存在的,可以作为函数参数,可以作为返回值,但super只是一个关键字,不能作为参数和返回值,它只是用于告诉编译器访问父类的相关变量和方法。

4.2 继承的细节

  • 子类可以通过super调用父类的构造方法,如果子类没有通过super调用,则会自动调动父类的默认构造方法
  • 这个类只有一个带参数的构造方法,没有默认构造方法。这个时候,它的任何子类都必须在构造方法中通过super调用Base的带参数构造方法
  • 父类构造方法中调用可被子类重写的方法,是一种不好的实践,容易引起混淆,应该只调用private的方法。
  • public变量和方法,则要看如何访问它。在类内,访问的是当前类的,但子类可以通过super.明确指定访问父类的。在类外,则要看访问变量的静态类型:静态类型是父类,则访问父类的变量和方法;静态类型是子类,则访问的是子类的变量和方法。
  • 静态绑定在程序编译阶段即可决定,而动态绑定则要等到程序运行时。实例变量、静态变量、静态方法、private方法,都是静态绑定的。
  • 当有多个重名函数的时候,在决定要调用哪个函数的过程中,首先是按照参数类型进行匹配的,换句话说,寻找在所有重载版本中最匹配的,然后才看变量的动态类型,进行动态绑定。
  • 一个父类的变量能不能转换为一个子类的变量,取决于这个父类变量的动态类型(即引用的对象类型)是不是这个子类或这个子类的子类。
  • 这种思路和设计是一种设计模式,称之为模板方法。action方法就是一个模板方法,它定义了实现的模板,而具体实现则由子类提供。模板方法在很多框架中有广泛的应用,这是使用protected的一种常见场景。
  • 重写时,子类方法不能降低父类方法的可见性
  • 为什么要这样规定呢?继承反映的是“is-a”的关系,即子类对象也属于父类,子类必须支持父类所有对外的行为,将可见性降低就会减少子类对外的行为,从而破坏“is-a”的关系,但子类可以增加父类的行为,所以提升可见性是没有问题的。
  • 一个Java类,默认情况下都是可以被继承的,但加了final关键字之后就不能被继承了
  • 其中的public/protected实例方法默认情况下都是可以被重写的,但加了final关键字后就不能被重写了

4.3 继承实现的基本原理

  • 在Java中,类是动态加载的,当第一次使用这个类的时候才会加载,加载一个类时,会查看其父类是否已加载,如果没有,则会加载其父类。
  • 动态绑定,而动态绑定实现的机制就是根据对象的实际类型查找要执行的方法,子类型中找不到的时候再查找父类。
  • 对变量的访问是静态绑定的,无论是类变量还是实例变量。

4.4 为什么说继承是把双刃剑

  • 继承为什么会有破坏力呢?主要是因为继承可能破坏封装,而封装可以说是程序设计的第一原则
  • 封装就是隐藏实现细节,提供简化接口
  • 如果子类不知道基类方法的实现细节,它就不能正确地进行扩展
  • 子类和父类之间是细节依赖,子类扩展父类,仅仅知道父类能做什么是不够的,还需要知道父类是怎么做的,而父类的实现细节也不能随意修改,否则可能影响子类。
  • 对于子类而言,通过继承实现是没有安全保障的,因为父类修改内部实现细节,它的功能就可能会被破坏;而对于基类而言,让子类继承和重写方法,就可能丧失随意修改内部实现的自由。
  • 给类加final修饰符,父类就保留了随意修改这个类实现的自由,使用者也可以放心地使用它,而不用担心一个父类引用的变量,实际指向的却是一个完全不符合预期行为的子类对象。

5.1 接口的本质

  • 针对接口而非具体类型进行编程,是计算机程序的一种重要思维方式。
  • 与类一样,接口也可以使用instanceof关键字,用来判断一个对象是否实现了某接口
  • Java 8允许在接口中定义两类新方法:静态方法和默认方法,它们有实现体
  • 引入默认方法主要是函数式数据处理的需求,是为了便于给接口增加功能。
  • 针对接口编程是一种重要的程序思维方式,这种方式不仅可以复用代码,还可以降低耦合,提高灵活性,是分解复杂问题的一种重要工具。

5.2 抽象类

  • 抽象类就是抽象的类。抽象是相对于具体而言的,一般而言,具体类有直接对应的对象,而抽象类没有,它表达的是抽象概念
  • 这种只有子类才知道如何实现的方法,一般被定义为抽象方法。
  • 抽象方法和抽象类都使用abstract这个关键字来声明
  • 抽象类不能创建对象(比如,不能使用new Shape()),而具体类可以。
  • 每个人都可能会犯错,减少错误不能只依赖人的优秀素质,还需要一些机制,使得一个普通人都容易把事情做对,而难以把事情做错。抽象类就是Java提供的这样一种机制。
  • 抽象类和接口是配合而非替代关系,它们经常一起使用,接口声明能力,抽象类提供默认实现,实现全部或部分方法,一个接口经常有一个对应的抽象类。
  • 抽象类和接口经常相互配合,接口定义能力,而抽象类提供默认实现,方便子类实现接口。

5.3 内部类的本质

  • 内部类可以方便地访问外部类的私有变量,可以声明为private从而实现对外完全隐藏,相关代码写在一起,写法也更为简洁,这些都是内部类的好处。
  • 它可以访问外部类的静态变量和方法,如innerMethod直接访问shared变量,但不可以访问实例变量和方法。
  • 静态内部类的使用场景是很多的,如果它与外部类关系密切,且不依赖于外部类实例,则可以考虑定义为静态内部类
  • 与静态内部类不同,成员内部类中不可以定义静态变量和方法(final变量例外,它等同于常量)
  • 方法内部类还可以直接访问方法的参数和方法中的局部变量,不过,这些变量必须被声明为final
  • 因为实际上,方法内部类操作的并不是外部的变量,而是它自己的实例变量,只是这些变量的值和外部一样,对这些变量赋值,并不会改变外部的值,为避免混淆,所以干脆强制规定必须声明为final。
  • 匿名内部类是与new关联的,在创建对象的时候定义类,new后面是父类或者父接口,然后是圆括号(),里面可以是传递给父类构造方法的参数,最后是大括号{},里面是类的定义。

5.4 枚举的本质

  • 枚举类型都有一个方法int ordinal(),表示枚举值在声明时的顺序,从0开始
  • 枚举的好处体现在以下几方面。 ❑ 定义枚举的语法更为简洁。 ❑ 枚举更为安全。一个枚举类型的变量,它的值要么为null,要么为枚举值之一,不可能为其他值,但使用整型变量,它的值就没有办法强制,值可能就是无效的。 ❑ 枚举类型自带很多便利方法(如values、valueOf、toString等),易于使用。
  • 枚举类型本质上也是类,但由于编译器自动做了很多事情,因此它的使用更为简洁、安全和方便。
  • 枚举值的定义需要放在最上面,枚举值写完之后,要以分号(; )结尾,然后才能写其他代码。

6.1 初识异常

  • Java的默认异常处理机制是退出程序,异常发生点后的代码都不会执行
  • throw关键字可以与return关键字进行对比。return代表正常退出,throw代表异常退出;return的返回位置是确定的,就是上一级调用者,而throw后执行哪行代码则经常是不确定的,由异常处理机制动态确定。
  • 捕获异常后,程序就不会异常退出了,但try语句内异常点之后的其他代码就不会执行了,执行完catch内的语句后,程序会继续执行catch花括号外的代码。
  • 异常是相对于return的一种退出机制,可以由系统触发,也可以由程序通过throw语句触发,异常可以通过try/catch语句进行捕获并处理,如果没有捕获,则会导致程序退出并输出异常栈信息

6.2 异常类

  • 所有异常类都有一个共同的父类Throwable
  • Throwable类有两个主要参数:一个是message,表示异常消息;另一个是cause,表示触发该异常的其他异常。异常可以形成一个异常链,上层的异常由底层异常触发,cause表示底层异常。
  • RuntimeException比较特殊,它的名字有点误导,因为其他异常也是运行时产生的,它表示的实际含义是未受检异常(unchecked exception),相对而言,Exception的其他子类和Exception自身则是受检异常(checked exception), Error及其子类也是未受检异常。
  • 如果父类是RuntimeException或它的某个子类,则自定义异常也是未受检异常;如果是Exception或Exception的其他子类,则自定义异常是受检异常。

6.3 异常处理

  • 为什么要重新抛出呢?因为当前代码不能够完全处理该异常,需要调用者进一步处理。
  • finally语句有一个执行细节,如果在try或者catch语句内有return语句,则return语句在finally语句执行结束后才执行,但finally并不能改变返回值
  • 资源r的声明和初始化放在try语句内,不用再调用finally,在语句执行完try语句后,会自动调用资源的close()方法。
  • 对于未受检异常,是不要求使用throws进行声明的,但对于受检异常,则必须进行声明,换句话说,如果没有声明,则不能抛出。
  • 未受检异常和受检异常的区别如下:受检异常必须出现在throws语句中,调用者必须处理,Java编译器会强制这一点,而未受检异常则没有这个要求。

6.4 如何使用异常

  • 真正出现异常的时候,应该抛出异常,而不是返回特殊值

7.1 包装类

  • 将基本类型转换为包装类的过程,一般称为“装箱”,而将包装类型转换为基本类型的过程,则称为“拆箱”。

7.2 剖析String

  • String类内部用一个字符数组表示字符串
  • String类也声明为了final,不能被继承,内部char数组value也是final的,初始化后就不能再变了。
  • 实际上,这些常量就是String类型的对象,在内存中,它们被放在一个共享的地方,这个地方称为字符串常量池,它保存所有的常量字符串,每个常量只会保存一份,被所有使用者共享。当通过常量的形式使用一个字符串的时候,使用的就是常量池中的那个对应的String类型的对象
  • 如果不是通过常量直接赋值,而是通过new创建,==就不会返回true了
  • Java 9对String的实现进行了优化,它的内部不是char数组,而是byte数组,如果字符都是ASCII字符,它就可以使用一个字节表示一个字符,而不用UTF-16BE编码,节省内存。

7.3 剖析StringBuilder

  • 唯一的不同就在于StringBuffer类是线程安全的,而StringBuilder类不是。
  • 在不知道最终需要多长的情况下,指数扩展是一种常见的策略,广泛应用于各种内存分配相关的计算机程序中。
  • 既然直接使用+和+=就相当于使用StringBuilder和append,那还有什么必要直接使用StringBuilder呢?在简单的情况下,确实没必要。不过,在稍微复杂的情况下,Java编译器可能没有那么智能,它可能会生成过多的StringBuilder,尤其是在有循环的情况下

7.4 剖析Arrays

  • 传递比较器Comparator给sort方法,体现了程序设计中一种重要的思维方式。将不变和变化相分离,排序的基本步骤和算法是不变的,但按什么排序是变化的,sort方法将不变的算法设计为主体逻辑,而将变化的排序方式设计为参数,允许调用者动态指定,这也是一种常见的设计模式,称为策略模式,不同的排序方式就是不同的策略。
  • 需要注意的是,binarySearch针对的必须是已排序数组,如果指定了Comparator,需要和排序时指定的Comparator保持一致。另外,如果数组中有多个匹配的元素,则返回哪一个是不确定的。
  • 在创建数组时,除了第一维的长度需要指定外,其他维的长度不需要指定,甚至第一维中每个元素的第二维的长度可以不一样

7.5 剖析日期和时间

  • DateFormat/SimpleDateFormat不是线程安全的。

7.6 随机

  • 种子决定了随机产生的序列,种子相同,产生的随机数序列就是相同的。
  • 指定种子是为了实现可重复的随机。
  • 随机数基于一个种子,种子固定,随机数序列就固定,默认构造方法中,种子是一个真正的随机数。

8.1 基本概念和原理

  • 泛型就是类型参数化,处理的数据类型不是固定的,而是可以作为参数传入。
  • ❑ 更好的安全性。 ❑ 更好的可读性。
  • >是一种令人费解的语法形式,这种形式称为递归类型限制,可以这么解读:T表示一种数据类型,必须实现Comparable接口,且必须可以与相同类型的元素进行比较。
  • 泛型是计算机程序中一种重要的思维方式,它将数据结构和算法与数据类型相分离,使得同一套数据结构和算法能够应用于各种数据类型,而且可以保证类型安全,提高可读性。
  • 在Java中,泛型是通过类型擦除来实现的,它是Java编译器的概念,Java虚拟机运行时对泛型基本一无所知,理解这一点是很重要的,它有助于我们理解Java泛型的很多局限性。

8.2 解析通配符

  • 1)用于定义类型参数,它声明了一个类型参数T,可放在泛型类定义中类名后面、泛型方法返回值前面。 2)用于实例化类型参数,它用于实例化泛型变量中的类型参数,只是这个具体类型是未知的,只知道它是E或E的某个子类型。
  • 形如DynamicArray,称为无限定通配符。
  • Java容器类中就有类似这样的用法,公共的API是通配符形式,形式更简单,但内部调用带类型参数的方法。
  • 如果返回值依赖于类型参数,也不能用通配符
  • 1)通配符形式都可以用类型参数的形式来替代,通配符能做的,用类型参数都能做。 2)通配符形式可以减少类型参数,形式上往往更为简单,可读性也更好,所以,能用通配符的就用通配符。 3)如果类型参数之间有依赖关系,或者返回值依赖类型参数,或者需要写操作,则只能用类型参数。 4)通配符形式和类型参数往往配合使用,比如,上面的copy方法,定义必要的类型参数,使用通配符表达依赖,并接受更广泛的数据类型。

8.3 细节和局限性

  • 对于泛型类声明的类型参数,可以在实例变量和方法中使用,但在静态变量和静态方法中是不能使用的。

9.1 剖析ArrayList

  • 迭代器表示的是一种关注点分离的思想,将数据的实际组织方式与数据的迭代遍历相分离,是一种常见的设计模式。
  • Collection表示一个数据集合,数据间没有位置或顺序的概念
  • 这种没有任何代码的接口在Java中被称为标记接口,用于声明类的一种属性。
  • 它会重新分配一个数组,大小刚好为实际内容的长度。调用这个方法可以节省数组占用的空间。
  • 对于ArrayList,它的特点是内部采用动态数组实现,这决定了以下几点。 1)可以随机访问,按照索引位置进行访问效率很高,用算法描述中的术语,效率是O(1),简单说就是可以一步到位。 2)除非数组已排序,否则按照内容查找元素效率比较低,具体是O(N), N为数组内容长度,也就是说,性能与数组长度成正比。 3)添加元素的效率还可以,重新分配和复制数组的开销被平摊了,具体来说,添加N个元素的效率为O(N)。 4)插入和删除元素的效率比较低,因为需要移动元素,具体为O(N)。
  • ArrayList不是线程安全的

9.2 剖析LinkedList

  • 除了实现了List接口外,LinkedList还实现了Deque和Queue接口,可以按照队列、栈和双端队列的方式进行操作

10.1 剖析HashMap

  • keySet()、values()、entrySet()有一个共同的特点,它们返回的都是视图,不是复制的值,基于返回值的修改会直接修改Map自身
  • Java 8对HashMap的实现进行了优化,在哈希冲突比较严重的情况下,即大量元素映射到同一个链表的情况下(具体是至少8个元素,且总的键值对个数至少是64), Java 8会将该链表转换为一个平衡的排序二叉树,以提高查询的效率
  • 根据哈希值存取对象、比较对象是计算机程序中一种重要的思维方式,它使得存取对象主要依赖于自身Hash值,而不是与其他对象进行比较,存取效率也与集合大小无关,高达O(1),即使进行比较,也利用Hash值提高比较性能。

10.2 剖析HashSet

  • HashSet内部是用HashMap实现的,它内部有一个HashMap实例变量

10.4 剖析TreeMap

  • TreeMap的实现基础是排序二叉树

10.8 剖析EnumSet

  • 对于只有两种状态,且需要进行集合运算的数据,使用位向量进行表示、位运算进行处理,是计算机程序中一种常用的思维方式。

11.1 堆的概念与算法

  • 堆是一种比较神奇的数据结构,概念上是树,存储为数组,父子有特殊顺序,根是最大值/最小值,构建/添加/删除效率都很高,可以高效解决很多问题

15.1 线程的基本概念

  • 线程表示一条单独的执行流,它有自己的程序执行计数器,有自己的栈
  • 在Java中创建线程有两种方式:一种是继承Thread;另外一种是实现Runnable接口。
  • start表示启动该线程,使其成为一条单独的执行流,操作系统会分配线程相关的资源,每个线程会有单独的程序执行计数器和栈,操作系统会把这个线程作为一个独立的个体进行调度,分配时间片让它执行,执行的起点就是run方法。
  • 无论是通过继承Thead还是实现Runnable接口来创建线程,启动线程都是调用start方法。
  • 前面我们提到,启动线程会启动一条单独的执行流,整个程序只有在所有线程都结束的时候才退出,但daemon线程是例外,当整个程序中剩下的都是daemon线程的时候,程序就会退出。
  • Thread有一个join方法,可以让调用join的线程等待该线程结束
  • 竞态条件(race condition)是指,当多个线程访问和操作同一个对象时,最终执行结果与执行时序有关,可能正确也可能不正确。
  • 如果执行的任务都是CPU密集型的,即主要消耗的都是CPU,那创建超过CPU数量的线程就是没有必要的,并不会加快程序的执行。

15.2 理解synchronized

  • 共享内存有两个重要问题,一个是竞态条件,另一个是内存可见性
  • synchronized可以用于修饰类的实例方法、静态方法和代码块
  • 多个线程是可以同时执行同一个synchronized实例方法的,只要它们访问的对象是不同的即可
  • synchronized实例方法实际保护的是同一个对象的方法调用
  • synchronized保护的是对象而非代码,只要访问的是同一个对象的synchronized方法,即使是不同的代码,也会被同步顺序访问
  • 一般在保护变量时,需要在所有访问该变量的方法上加上synchronized。
  • 任意对象都有一个锁和等待队列
  • synchronized有一个重要的特征,它是可重入的,也就是说,对同一个执行线程,它在获得了锁之后,在调用其他需要同样锁的代码时,可以直接调用。
  • 可重入是通过记录锁的持有线程和持有数量来实现的,当调用被synchronized保护的代码时,检查对象是否已被锁,如果是,再检查是否被当前线程锁定,如果是,增加持有数量,如果不是被当前线程锁定,才加入等待队列,当释放锁时,减少持有数量,当数量变为0时才释放整个锁。
  • synchronized除了保证原子操作外,它还有一个重要的作用,就是保证内存可见性,在释放锁时,所有写入都会写回内存,而获得锁后,都会从内存中读最新数据。
  • 加了volatile之后,Java会在操作对应变量时插入特殊的指令,保证读写到内存最新值,而非缓存的值。
  • 应该尽量避免在持有一个锁的同时去申请另一个锁,如果确实需要多个锁,所有代码都应该按照相同的顺序去申请锁

15.3 线程的基本协作机制

  • 除了用于锁的等待队列,每个对象还有另一个等待队列,表示条件队列,该队列用于线程间的协作
  • notify做的事情就是从条件队列中选一个线程,将其从队列中移除并唤醒,notifyAll和notify的区别是,它会移除条件队列中所有的线程并全部唤醒。
  • 它们被不同的线程调用,但共享相同的锁和条件等待队列(相同对象的synchronized代码块内),它们围绕一个共享的条件变量进行协作

16.1 原子变量和CAS

  • compareAndSet是一个非常重要的方法,比较并设置,我们以后将简称为CAS。
  • 它的声明带有volatile,这是必需的,以保证内存可见性。
  • CAS是Java并发包的基础,基于它可以实现高效的、乐观、非阻塞式数据结构和算法,它也是并发包中锁、同步工具和各种容器的基础。

16.2 显式锁

  • park不同于Thread.yield(), yield只是告诉操作系统可以先让其他线程运行,但自己依然是可运行状态,而park会放弃调度资格,使线程进入WAITING状态。
  • 保证公平整体性能比较低,低的原因不是这个检查慢,而是会让活跃线程得不到锁,进入等待状态,引起频繁上下文切换,降低了整体的效率

17.1 写时复制的List和Set

  • CopyOnWriteArrayList和CopyOnWriteArraySet适用于读远多于写、集合不太大的场合,它们采用了写时复制,这是计算机程序中一种重要的思维和技术。

17.2 ConcurrentHashMap

  • ConcurrentHashMap的迭代器创建后,就会按照哈希表结构遍历每个元素,但在遍历过程中,内部元素可能会发生变化,如果变化发生在已遍历过的部分,迭代器就不会反映出来,而如果变化发生在未遍历过的部分,迭代器就会发现并反映出来,这就是弱一致性。

21.1 Class类

  • Class有一个静态方法forName,可以根据类名直接加载Class,获取Class对象

22.6 注解的应用:DI容器

  • 注解提升了Java语言的表达能力,有效地实现了应用功能和底层功能的分离,框架/库的程序员可以专注于底层实现,借助反射实现通用功能,提供注解给应用程序员使用,应用程序员可以专注于应用功能,通过简单的声明式注解与框架/库进行协作。

23.1 静态代理

  • 适配器是提供了一个不一样的新接口,装饰器是对原接口起到了“装饰”作用,可能是增加了新接口、修改了原有的行为等,代理一般不改变接口

23.3 cglib动态代理

  • Java SDK动态代理的局限在于,它只能为接口创建代理,返回的代理对象也只能转换到某个接口类型
  • cglib的实现机制与Java SDK不同,它是通过继承实现的,它也是动态创建了一个类,但这个类的父类是被代理的类,代理类重写了父类的所有public非final方法,改为调用Callback中的相关方法

26.1 Lambda表达式

  • 当主体代码只有一条语句的时候,括号和return语句也可以省略
  • 函数式接口也是接口,但只能有一个抽象方法