每一章应该按照目录去组织知识脉络
父类:基类
子类:派生类、导出类
第一章 对象的概念
- 把子类当成父类来处理的过程叫做“向上转型”(upcasting)。
- 从 Object 转为具体的类型(父类转为子类)就是“向下转型”(强制类型转换)。cjzksrtq
- 面向对象的程序设计语言是通过“动态绑定”的方式来实现对象的多态性的。“动态绑定”即是程序运行时才能决定变量接收的实际对象是哪一个。
- Java 中所有类都默认继承于 Object。(除 C++ 以外的几乎所有OOP语言都像 Java 这样有一个默认基类)
- 每种语言都应该有ta的基础包,这些包通常被称为STL(Standard Template Library,标准模板库)。
第三章 万物皆对象
引用和对象,就像遥控器和电视机,用遥控器可以控制电视机,但是遥控器和电视机又是分别独立存在的。
String str
这里的 str 是引用,String str = new String("test");
这里的new String("test")
创建了一个对象。Java 的基本数据类型不用new创建,而是自动创建,并放到了栈内存中,所以很高效。
1 | { |
上例中,引用 s 在作用域终点就结束了。但是,引用 s 指向的字符串对象依然还在占用内存。在这段代码中,我们无法在这个作用域之后访问这个对象,因为唯一对它的引用 s 已超出了作用域的范围。
java中基本数据类型都有默认值,默认值是 Java 初始化类的时候赋予的。char类型的默认值是
\u0000
,即空值,但不是null。ta是二进制上的0,表示每个二进制位都为0的Unicode字符。如果基本类型是局部变量,那么这个局部变量是不会被 Java 初始化的,ta 需要程序显式赋值。
面向对象编程可以总结为:向对象发送消息。
- Java 创建者希望我们反向使用自己的网络域名,因为域名通常是唯一的。因此我的域名是 mindviewInc.com,所以我将我的 foibles 类库命名为 com.mindviewinc.utility.foibles。反转域名后,
.
用来代表子目录的划分。
第四章 运算符
优先级
- 乘除 > 加减
- 应该习惯性用小括号明确运算优先级
赋值
- 基本类型的赋值是copy一份,可以随便改而不影响其他值;
- 对象的赋值,只是赋予其内存的引用,修改会改变所有引用了该对象的变量;
算术运算符
- 整数除法会直接砍掉小数,而不是进位;
第五章 控制流
逗号操作符
在 Java 中逗号运算符(这里并非指我们平常用于分隔定义和方法参数的逗号分隔符)仅有一种用法:在 for 循环的初始化和步进控制中定义多个变量。
1 | for(int i = 1, j = i + 10; i < 5; i++, j = i * 2) { |
本书推荐使用 for-in 语法
“标签”是后面跟一个冒号的标识符。
标签只能写在循环语句的上一行,相当于给当前循环语句做标记,可以对被标记的语句做一些操作。
代码示例:
1 | label1: |
大家要记住的重点是:在 Java 里需要使用标签的唯一理由就是因为有循环嵌套存在,而且想从多层嵌套中 break 或 continue。
第六章 初始化和清理
方法重载
可以根据参数顺序、数量不同来区分重载方法
如果你自己在一个类中写了一个有参数的构造器,那么 Java 就不会为该类创建无参构造方法,也就不能通过 new className();
来实例化对象了。
this关键词应用场景:
- 当成员变量s与参数变量s同名的时候,可以用
this.s
来显示指定成员变量。 - 当方法需要返回对象本身的时候,可以直接
return this
方法里面的局部变量都需要初始化,Java不会赋予默认值。
类的每个基本类型数据成员保证都会有一个初始值。例子如下:
1 | public class Test { |
在类里定义一个对象引用时,如果不将其初始化,那么引用就会被赋值为 null。
类中变量的初始化顺序取决于定义的顺序,变量的初始化会在任何方法(包括构造方法)之前
垃圾回收器只知道如何释放用 new 创建的对象的内存。
java不允许创建局部对象?必须使用new创建对象。
无论对象是如何创建的,垃圾回收器都会负责释放对象所占用的所有内存
初始化顺序是先静态对象(如果它们之前没有初始化的话),然后是非静态对象
尽管没有显示使用 static,构造器也是静态方法。
静态代码块只执行一次,会在第一次实例化类对象或者第一次访问类的静态变量的时候执行;
1 | String[] a = new String[10]; // 创建10个长度的String类型数组 |
1 | // 初始化列表的最后一个逗号是可选的(这一特性使维护长列表变得更容易) |
第七章 封装
一个 Java 文件中,如果有多个类,那么经过Java解析器编译之后,每一个类都会生成一个class文件
可以使用 import 将程序中的代码从调试版改为发布版。
访问权限修饰符
public、protect、private和默认访问权限(又叫做包权限)。
声明为 private 可以让你以后随意的修改这个方法/属性,而不用担心其他地方使用到ta。
继承了来自另一个包的类,那么唯一能访问的就是被继承类的 public 成员,如果在同一个包中继承,就可以操作所有的包访问权限的成员。
为了清晰起见,你可以采用一种创建类的风格:public 成员(方法/属性)放在类的开头,接着是 protected 成员,包访问权限成员,最后是 private 成员。这么做的好处是类的使用者可以从头读起,首先会看到对他们而言最重要的部分(public 成员,因为可以从文件外访问它们),直到遇到非 public 成员时停止阅读
虽然不是很常见,但是编译单元内没有 public 类也是可能的。这时可以随意命名文件(尽管随意命名会让代码的阅读者和维护者感到困惑)。
注意,类既不能是 private 的(这样除了该类自身,任何类都不能访问它),也不能是 protected 的。所以对于类的访问权限只有两种选择:包访问权限或者 public。为了防止类被外界访问,可以将所有的构造器声明为 private,这样只有你自己能创建对象(在类的 static 成员中)
默认访问权限(包权限)
父目录中的java类,不能访问子目录里面包访问权限的东西。
那,子目录里面的java类,可以访问父目录里面包访问权限对象吗?(不行,还是要 import 导包)
protected
继承类可以访问,相同包下的其他类可以访问。
第八章 复用
代码复用是面向对象编程(OOP)最具魅力的原因之一。
当你初始化一个派生类(子类)的时候,Java 会自动调用基类(父类)的无参构造方法来创建基类的对象。
ps. 调用子类的有参构造方法也会自动调用父类的无参构造方法来创建父类对象。
注意:对父类构造函数的调用,必须放到子类构造函数中的第一行。
建议只有在需要“向上转型”的情况下,才使用继承。
final 关键字
可以用“断子绝孙”来形容 ta。
修饰基本类型的属性:该属性永远有用,赋值后不能再改变。
修饰引用类型:该引用不能改为指向其他对象,但是该引用指向的对象还是可以修改的。
ps. 数组也是对象。
『我们不能因为某数据被 final 修饰就认为在编译时可以知道它的值。由上例中的 i4 和 INT_5 可以看出,它们在运行时才会赋值随机数。』
final修饰的变量可以在构造器中进行赋值。
使用 final 方法的原因有两个。第一个原因是给方法上锁,防止子类通过覆写改变
方法的行为。这是出于继承的考虑,确保方法的行为不会因继承而改变;
第二个原因是效率,如果将一个方法指明为 final,就是同意编译器把对该方法的调用转化为内嵌调用。但是不推荐这样使用(好像目前的编译器也不会个这种方式的代码提高效率了)。我们应该让编译器和 JVM 处理性能问题.
类中所有的 private 方法都被隐式地指定为 final。
如果父类有一个private void A(),子类又“覆写”了private void A(),这其实不是覆写,编译器也不会报错。它只是隐藏在类内部的代码,且恰好有相同的命名而已。
第九章 多态
后期绑定也称为动态绑定或运行时绑定。当一种语言实现了后期绑定,就必须具有某种机制在运行时能判断对象的类型,从而调用恰当的方法。也就是说,编译器仍然不知道对象的类型,但是方法调用机制能找到正确的方法体并调用。
Java 中除了 static 和 final 方法(private 方法也是隐式的 final)外,其他所有方法都是后期绑定
陷阱1:“重写” 私有方法
1 | public class PrivateOverride { |
1 | 输出: |
你可能期望输出是 public f(),然而 private 方法可以当作是 fnal 的,对于派生
类来说是隐蔽的。因此,这里 Derived 的 f() 是一个全新的方法;因为基类版本的 f()
屏蔽了 Derived ,因此它都不算是重写方法。
陷阱2:属性访问
1 | class Super { |
1 | 输出: |
当 Sub 对象向上转型为 Super 引用时,任何属性访问都被编译器解析,因此不是多态的。在这个例子中, Super.feld和 Sub.feld 被分配了不同的存储空间,因此,Sub 实际上包含了两个称为 feld 的属性:它自己的和来自 Super 的。然而,在引用Sub 的 feld 时,默认的 feld 属性并不是 Super 版本的 feld 属性。为了获取 Super 的 feld 属性,需要显式地指明 super.feld。
对象的构造器调用顺序:
- 父类构造器被调用。
- 按编码顺序初始化本类中的成员变量
- 调用该构造器的方法体
协变返回类型:Java 5 中引入了协变返回类型,在子类中重写的方法的返回值的类型,可以是被重写方法的放回值的派生类型。
第十章 接口
接口中只有静态常量属性,final
和 static
关键词一般会省略。
使用接口的核心原因:
- 可以向上转型;
- 防止客户端程序员创建这个类的对象;
接口最吸引人的原因之一是相同的接口可以有多个实现。在简单情况下体现在一个方法接受接口作为参数,该接口的实现和传递对象给方法则交由你来做。
因此,接口的一种常见用法是前面提到的策略设计模式。编写一个方法执行某些操作并接受一个指定的接口作为参数。可以说:“只要对象遵循接口,就可以调用方法” ,这使得方法更加灵活,通用,并更具可复用性。
抽象类
包含抽象方法的类叫做抽象类,用 abstract 关键字来修饰。
1 | public abstract class ClassName {…} |
继承抽象类的子类,必须要为抽象的父类的所有抽象方法提供定义。(如果子类也是一个抽象方法,则可以不提供定义)
我们可以创建一个没有抽象方法的抽象类,这样做可以阻止产生这个类的对象。
Java8新特性:允许接口包含默认方法和静态方法。
增加默认方法的极具说服力的理由是它允许在不破坏已使用接口的代码的情况下,在接口中增加新的方法。默认方法有时也被称为守卫方法或虚拟扩展方法。
增加静态方法,可以把工具功能至于接口中,从而操作接口,或者成为通用的工具。而不像java8之前那样,“接口只是规范”。示例如下:
1 | package onjava; |
接口和抽象类的区别
特性 | 接口 | 抽象类 |
---|---|---|
组合 | 新类可以组合多个接口 | 只能继承单一抽象类 |
状态 | 不能包含属性(除了静态属性,不支持对象状态) | 可以包含属性,非抽象方法可能引用这些属性 |
默认方法 和 抽象方法 | 不需要在子类中实现默认方法。默认方法可以引用其他接口的方法 | 必须在子类中实现抽象方法 |
构造器 | 没有构造器 | 可以有构造器 |
可见性 | 隐式 public | 可以是 protected 或友元 |
多继承
Java不允许多继承。但是可以通过实现多个接口来达成“多继承”的效果。代码如下:
1 | interface Sam1 { |
如果继承的多个接口中有相同(方法签名一样)的方法,实现类需要重新这个相同的方法。否则编译器会报错。示例如下:
1 | import java.util.*; |
应该使用接口还是抽象类呢?如果创建不带任何方法定义或成员变量的基类,就选择接口而不是抽象类。事实上,如果知道某事物是一个基类,可以考虑用接口实现它。
extends 只能继承一个普通类,但是构建接口时可以引用多个基类接口,接口名之间用逗号分隔。如:interface Vampire extends DangerousMonster, Lethal{}
一个方法如果入参是接口类型,就意味着任何类都可以实现这个接口然后传入这个方法中。这就是使用接口而不是类的强大之处。(适配器模式也是用了这种方法)
几乎任何时候,创建类都可以替代为创建一个接口。恰当的原则是优先使用类而不是接口。从类开始,如果使用接口的必要性变得很明确,那么就重构。接口是一个伟大的工具,但它们容易被滥用。
第十一章 内部类
内部类拥有其外围类的所有元素的访问权(当某个外围类的对象创建了一个内部类对象时,此内部类对象必定会秘密地捕获一个指向那个外围类对象的引用。然后,在你访问此外围类的成员时,就是用那个引用来选择外围类的成员)。
当内部类是非 static 时,内部类对象只能在于其外部类相关联的情况下才能被创建。构建内部类对象时,需要一个指向其外围类对象的引用,如果编译器访问不到这个引用就会报错。(静态内部类(即嵌套类)不需要外部类对象可直接创建)
在内部类里面可以直接通过 外部类名.this
的形式获取外部类对象的引用;
创建内部类对象的方式:
1 | public class Order { |
在方法内或者任意作用域内定义内部类
这么做的理由:
- 如前所示,你实现了某类型的接口,于是可以创建并返回对其的引用。
- 你要解决一个复杂的问题,想创建一个类来辅助你的解决方案,但是又不希望这个类是公共可用的。
匿名内部类
如果要创建一个匿名内部类,且在匿名内部类里面使用 ta 外面的一个对象,那么这个对象一定要是final的(初始化之后不会改变),示例代码如下:(留意 final String dest
)
1 | public class Parcel9 { |
为什么需要内部类?
使用内部类最吸引人的原因是:内部类可以独立地继承某个接口的实现类,无论外部类是否也继承了这个接口。(也就是说可以实现“多继承”)
如果不需要解决“多重继承”的问题,那么自然可以用别的方式编码,而不需要使用内部类。但如果使用内部类,还可以获得其他一些特性:
闭包(closure)是一个可调用的对象,它记录了一些信息,这些信息来自于创建它的作用域。
局部内部类和匿名内部类具有相同的行为和能力,那什么情况下使用哪一个呢?
- 需要一个已命名的构造器,或者需要重载构造器。使用局部内部类;
- 需要不止一个该内部类对象的时候,使用局部内部类;
- 只需要实例初始化出来一个对象的情况下,使用匿名内部类;
内部类标识符
编译过后,内部类也会单独生成一个class文件,ta 的命名规则是:外围类的名字,加上“$”,再加上内部类的名字。
1 | 普通类名称 |
第十二章 集合
在 Java 7 之前使用泛型来创建集合需要在等号两端都进行类型声明:ArrayList<Apple> appleList = new ArrayList<Apple>();
也可以把泛型 Apple 的子类放到 appleList 中;
id:方法可以直接返回一个list,不用泛型,然后 returnList
中可以放入不同类型的元素,以应对不同的逻辑环境。
Collections.addAll()
运行得更快,不推荐使用 new ArrayList<Integer>().addAll()
可以这样写Arrays.<Order>asList()
,这叫显式类型参数说明(explicit type argument specification)。
有两种类型的 List :
- 基本的 ArrayList ,擅长随机访问元素,但在 List 中间插入和删除元素时速度较慢。
- LinkedList ,它通过代价较低的在 List 中间进行的插入和删除操作,提供了优化的顺序访问。 LinkedList 对于随机访问来说相对较慢,但它具有比 ArrayList 更大的特征集。
containsAll()
方法不受List里面元素顺序的影响,只判断列表A是否全部包含列表B。
Iterator 只能用来:
- 使用
iterator()
方法要求集合返回一个 Iterator。 Iterator 将准备好返回序列中的第一个元素。 - 使用
next()
方法获得序列中的下一个元素。 - 使用
hasNext()
方法检查序列中是否还有元素。 - 使用
remove()
方法将迭代器最近返回的那个元素删除。
如果只是想向前遍历 List ,并不打算修改 List 对象本身,那么使用 for-in 语法更加简洁。
迭代器比 for-in 语法更灵活!
ListIterator 是一个更强大的 Iterator 子类型,它只能由各种 List 类生成。 Iterator 只能向前移动,而 ListIterator 可以双向移动。
主要方法:.hasNext()
、.next()
、.nextIndex()
、.hasPrevious()
、.previousIndex()
、.previous()
、.listIterator(3)
LinkedList 中间插入和删除比 ArrayList 高效,随机访问操作效率差一点
LinkedList 相关方法说明:
getFirst()
和element()
是相同的,它们都返回列表的头部(第一个元素)而并不删除它,如果 List 为空,则抛出 NoSuchElementException 异常。peek()
方法与这两个方法只是稍有差异,它在列表为空时返回 null 。removeFirst()
和remove()
也是相同的,它们删除并返回列表的头部元素,并在列表为空时抛出 NoSuchElementException 异常。poll()
稍有差异,它在列表为空时返回 null 。addFirst()
在列表的开头插入一个元素。offer()
与add()
和addLast()
相同。 它们都在列表的尾部(末尾)添加一个元素。removeLast()
删除并返回列表的最后一个元素。
堆栈
Java 1.0 中附带了一个 Stack 类,结果设计得很糟糕(为了向后兼容,我们永远坚持 Java 中的旧设计错误)。Java 6 添加了 ArrayDeque ,其中包含直接实现堆栈功能的方法:
1 | // collections/StackTest.java |
尽管已经有了 java.util.Stack ,但是 ArrayDeque 可以产生更好的 Stack ,因此更推荐使用!
1 | // onjava/Stack.java |
Set
- 不保存重复元素
- 最常见用途是测试归属性。测试某个对象是否属于这个 Set。因此,查找通常是 Set 最重要的操作,因此通常会选择 HashSet 实现,该实现针对快速查找进行了优化。
- TreeSet 将元素存储在红-黑树数据结构中。如果想修改默认排序方式,如按字母排序,不区分大小写,可以向构造器中传入比较器:
Set<String> words = new TreeSet<>(String.CASE_INSENSITIVE_ORDER);
- HashSet 使用散列函数
- LinkedHashSet 因为查询速度的原因也使用了散列
队列 Queue
队列是一个先进先出的集合,参考排队。
队列通常被当做一种可靠的将对象从程序的某个区域传输到另一个区域的途径。在【并发编程】中尤为重要。
LinkedList 实现了 Queue 接口,因此 LinkedList 可以用作 Queue 的一种实现。
优先级队列 PriorityQueue
根据队列中元素的优先度来决定下一个弹出的对象。
当在 PriorityQueue 上调用 offer()
方法来插入一个对象时,该对象会在队列中被排序。5默认的排序使用队列中对象的自然顺序(natural order),但是可以通过提供自己的 Comparator 来修改这个顺序。 PriorityQueue 确保在调用 peek()
, poll()
或 remove()
方法时,获得的元素将是队列中优先级最高的元素。
Collections.reverseOrder()
(Java 5 中新添加的)产生的反序的 Comparator 。
Java 5 引入了一个名为 Iterable 的接口,该接口包含一个能够生成 Iterator 的 iterator()
方法。for-in 使用此 Iterable 接口来遍历序列。
1 | for(String s : new IterableClass()) |
如果已经有一个接口并且需要另一个接口时,则编写适配器就可以解决这个问题。
集合与迭代器
队列和堆栈的行为是通过 LinkedList 提供的(通过 LinkedList 实现)。
不要在新代码中使用遗留类 Vector ,Hashtable 和 Stack 。
第十三章 函数式编程
函数式编程就是用代码操纵代码。
OO(object oriented,面向对象)是抽象数据,FP(functional programming,函数式编程)是抽象行为。
下面这段代码值得好好看:
1 | interface Strategy { |
Strategy 接口提供了单一的 approach()
方法来承载函数式功能。通过创建不同的 Strategy 对象,我们可以创建不同的行为。
传统上,我们通过创建一个实现 Strategy 接口的类来实现此行为,比如在 Soft。
- [1] 在 Strategize 中,Soft 作为默认策略,在构造函数中赋值。
- [2] 一种略显简短且更自发的方法是创建一个匿名内部类。即使这样,仍有相当数量的冗余代码。你总是要仔细观察:“哦,原来这样,这里使用了匿名内部类。”
- [3] Java 8 的 Lambda 表达式。由箭头
->
分隔开参数和函数体,箭头左边是参数,箭头右侧是从 Lambda 返回的表达式,即函数体。这实现了与定义类、匿名内部类相同的效果,但代码少得多。 - [4] Java 8 的方法引用,由
::
区分。在::
的左边是类或对象的名称,在::
的右边是方法的名称,但没有参数列表。 - [5] 在使用默认的 Soft strategy 之后,我们逐步遍历数组中的所有 Strategy,并使用
changeStrategy()
方法将每个 Strategy 放入 变量s
中。 - [6] 现在,每次调用
communicate()
都会产生不同的行为,具体取决于此刻正在使用的策略代码对象。我们传递的是行为,而非仅数据。3
Java 8 方法引用没有历史包袱。方法引用组成:类名或对象名,后面跟 ::
,然后跟方法名称。
使用未绑定的引用时,函数方法的签名(接口中的单个方法)不再与方法引用的签名完全匹配。 理由是:你需要一个对象来调用方法。
构造函数引用
你还可以捕获构造函数的引用,然后通过引用调用该构造函数。
1 | // functional/CtorReference.java |
我们如何对 [1],[2] 和 [3] 中的每一个使用 Dog :: new
。 这 3 个构造函数只有一个相同名称::: new
,但在每种情况下都赋值给不同的接口。编译器可以检测并知道从哪个构造函数引用。
从 Lambda 表达式引用的局部变量必须是 final
或者是等同 final
效果的。lambda 表达式可以引用普通的全局变量。
第十四章流式编程
流方法预置的操作几乎已满足了我们平常所有的需求。流操作的类型有三种:创建流,修改流元素(中间操作, Intermediate Operations),消费流元素(终端操作, Terminal Operations)。最后一种类型通常意味着收集流元素(通常是到集合中)。
流创建
创建随机数的流
new Random(47).ints(10, 20)
Random 类只能生成基本类型 int, long, double 的流。幸运的是, boxed()
流操作将会自动地把基本类型包装成为对应的装箱类型。
创建整形流
创建10到20所有整数的流:range(10, 20)
使用Stream.generate()创建流
Stream.generate()
可以把任意 Supplier<T>
用于生成 T
类型的流。
1 | // 这里创建流的所有元素都会是 “duplicate” |
iterate() 迭代器的方式创建
Stream.iterate()
以种子(第一个参数)开头,并将其传给方法(第二个参数)。方法的结果将添加到流,并存储作为第一个参数用于下次调用 iterate()
,依次类推。
这可以看成是自定义规则创建你想要的流数据了,需要一个开头,然后再加一个规则就ok了。
Stream.iterate(0, i -> i*i);
流的建造者模式(没看懂)
使用数组创建流 Arrays
1 | // 后两个参数是可选的,表示截取的开始下标和结束下标 |
另一种应用:
1 | Arrays.stream(new Operations[] { |
正则表达式创建流
1 | String str = "今天,你起床的想到的第一件事是什么?是美好的么?还是不好的?"; |
中间操作
跟踪和调试 peek()
peek()
排序 sorted()
可不传参数,使用默认排序;
可传入一个 Comparator 参数:.sorted(Comparator.reverseOrder())
移除元素
distinct()
filter(Predicate)
应用函数到元素
map(Function)
:将函数操作应用在输入流的元素中,并将返回值传递到输出流中。mapToInt(ToIntFunction)
:操作同上,但结果是 IntStream。mapToLong(ToLongFunction)
:操作同上,但结果是 LongStream。mapToDouble(ToDoubleFunction)
:操作同上,但结果是 DoubleStream。
在 map() 中组合流
flatMap()
concat()
Optional类
一些标准流操作会返回 Optional 对象,如下:
findFirst()
返回一个包含第一个元素的 Optional 对象,如果流为空则返回 Optional.emptyfindAny()
返回包含任意元素的 Optional 对象,如果流为空则返回 Optional.emptymax()
和min()
返回一个包含最大值或者最小值的 Optional 对象,如果流为空则返回 Optional.empty
便利函数
有许多方便我们解包 Optional的函数,可以简化“对所包含的对象的检查和执行操作”的过程,如下:
ifPresent(Consumer)
:当值存在时调用 Consumer,否则什么也不做。orElse(otherObject)
:如果值存在则直接返回,否则生成 otherObject。orElseGet(Supplier)
:如果值存在则直接返回,否则使用 Supplier 函数生成一个可替代对象。orElseThrow(Supplier)
:如果值存在直接返回,否则使用 Supplier 函数生成一个异常。
创建 Optional
empty()
:生成一个空 Optional。of(value)
:将一个非空值包装到 Optional 里。如果把null装到 of() 方法里,运行时会报异常ofNullable(value)
:针对一个可能为空的值,为空时自动生成 Optional.empty,否则将值包装在 Optional 中。
Optional 对象操作
当我们的流管道生成了 Optional 对象,下面 3 个方法可使得 Optional 的后续能做更多的操作:
Optional.filter(Predicate)
:将 Predicate 应用于 Optional 中的内容并返回结果。当 Optional 不满足 Predicate 时返回空。如果 Optional 为空,则直接返回。Optional.map(Function)
:如果 Optional 不为空,应用 Function 于 Optional 中的内容,并返回结果。否则直接返回 Optional.empty。Optional.flatMap(Function)
:同map()
,但是提供的映射函数将结果包装在 Optional 对象中,因此flatMap()
不会在最后进行任何包装。
Optional 流
可以通过 Optional.get() 解包来获取原值。
终端操作
这些操作接收一个流并产生一个最终结果;它们不会向后端流提供任何东西。因此,终端操作总是你在管道中做的最后一件事情。
转化为数组
toArray()
:将流转换成适当类型的数组。toArray(generator)
:在特殊情况下,生成器用于分配自定义的数组存储。
应用最终操作
forEach(Consumer)
:你已经看到过很多次System.out::println
作为 Consumer 函数。forEachOrdered(Consumer)
: 保证forEach
按照原始流顺序操作。
第一种形式:显式设计为任意顺序操作元素,仅在引入 parallel()
操作时才有意义。
收集
collect(Collector)
:使用 Collector 收集流元素到结果集合中。collect(Supplier, BiConsumer, BiConsumer)
:同上,第一个参数 Supplier 创建了一个新结果集合,第二个参数 BiConsumer 将下一个元素包含到结果中,第三个参数 BiConsumer 用于将两个值组合起来。
组合所有流元素
reduce(BinaryOperator)
:使用 BinaryOperator 来组合所有流中的元素。因为流可能为空,其返回值为 Optional。(未定义初始值,从而第一次执行的时候第一个参数的值是Stream的第一个元素,第二个参数是Stream的第二个元素)reduce(identity, BinaryOperator)
:功能同上,但是使用 identity 作为其组合的初始值。因此如果流为空,identity 就是结果。(定义了初始值,从而第一次执行的时候第一个参数的值是初始值,第二个参数是Stream的第一个元素)reduce(identity, BiFunction, BinaryOperator)
:这个形式更为复杂(所以我们不会介绍它),在这里被提到是因为它使用起来会更有效。通常,你可以显式地组合map()
和reduce()
来更简单的表达它。
匹配
allMatch(Predicate)
:如果流的每个元素根据提供的 Predicate 都返回 true 时,结果返回为 true。这个操作将会在第一个 false 之后短路;也就是不会在发生 false 之后继续执行计算。anyMatch(Predicate)
:如果流中的任意一个元素根据提供的 Predicate 返回 true 时,结果返回为 true。这个操作将会在第一个 true 之后短路;也就是不会在发生 true 之后继续执行计算。noneMatch(Predicate)
:如果流的每个元素根据提供的 Predicate 都返回 false 时,结果返回为 true。这个操作将会在第一个 true 之后短路;也就是不会在发生 true 之后继续执行计算。
元素查找
findFirst()
:返回一个含有第一个流元素的 Optional,如果流为空返回 Optional.empty。findAny(
:返回含有任意流元素的 Optional,如果流为空返回 Optional.empty。
如果必须选择流中最后一个元素,那就使用 reduce()
:
1 | // streams/LastElement.java |
信息
count()
:流中的元素个数。max(Comparator)
:根据所传入的 Comparator 所决定的“最大”元素。min(Comparator)
:根据所传入的 Comparator 所决定的“最小”元素。
数字流信息
average()
:求取流元素平均值。max()
和min()
:因为这些操作在数字流上面,所以不需要 Comparator。sum()
:对所有流元素进行求和。summaryStatistics()
:生成可能有用的数据。目前还不太清楚他们为什么觉得有必要这样做,因为你可以使用直接的方法产生所有的数据。
第十五章 异常
异常的概念
“异常”这个词有“我对此感到意外”的意思。问题出现了,你也许不清楚该如何处理,但你的确知道不应该置之不理,你要停下来,看看是不是有别人或在别的地方,能够处理这个问题。
基本异常
异常情形(exceptional condition)是指阻止当前方法或作用域继续执行的问题。
异常参数
所有标准异常类都有两个构造器:一个是无参构造器;另一个是接受字符串作为参数,以便能把相关信息放入异常对象的构造器:throw new NullPointerException("t = null");
对于不同类型的错误,要抛出相应的异常。错误信息可以保存在异常对象内部或者用异常类的名称来暗示。上一层环境通过这些信息来决定如何处理异常。(通常,唯一的信息只有异常的类型名,而在异常对象内部没有任何有意义的信息。)
异常捕获
解监控区域(guarded region): 它是一段可能产生异常的代码,并且后面跟着处理这些异常的代码。
try 语句块
异常处理程序
终止与恢复
在过去,使用支持恢复模型异常处理的操作系统的程序员们最终还是转向使用类似“终止模型”的代码,并且忽略恢复行为。所以虽然恢复模型开始显得很吸引人,但不是很实用。其中的主要原因可能是它所导致的耦合:恢复性的处理程序需要了解异常抛出的地点,这势必要包含依赖于抛出位置的非通用性代码。这增加了代码编写和维护的困难,对于异常可能会从许多地方抛出的大型程序来说,更是如此。
自定义异常
要自己定义异常类,必须从已有的异常类继承,最好是选择意思相近的异常类继承(不过这样的异常并不容易找)
使用 e.printStackTrace();
异常信息会被输出到标准错误流。
异常与记录日志
异常声明
【异常声明】告诉调用方该方法可能抛出什么异常:void f() throws TooBig, TooSmall, DivZero { // ...
捕获所有异常
异常信息输出
1 | e.getMessage():My Exception |
可以发现每个方法都比前一个提供了更多的信息一一实际上它们每一个都是前一个的超集。
多重捕获
Java7 之后可以使用同一方法捕获多个异常:1
2
3
4
5
6
7
8
9
10
11
12
13// exceptions/MultiCatch.java
public class MultiCatch {
void x() throws Except1, Except2, Except3, Except4 {}
void process() {}
void f() {
try {
x();
} catch(Except1 | Except2 | Except3 | Except4 e) {
process();
}
}
}
栈轨迹
通过 getStackTrace() 方法获取栈轨迹,这个方法将返回一个由栈轨迹中的元素所构成的数组,其中每一个元素都表示栈中的一桢。
重新抛出异常
如果重新抛出的是e.fillInStackTrace()
方法,那么在上级捕获处,就只能知道这个异常是你当前这个代码点抛出的异常,而你catch到的异常信息栈,会丢失。
永远不必为清理前一个异常对象而担心,或者说为异常对象的清理而担心。它们都是用 new 在堆上创建的对象,所以垃圾回收器会自动把它们清理掉。
精准的重新抛出异常
1 | public class PreciseRethrow { |
Java7之前你只能这样写:void catcher() throws BaseException {
,Java7 之后你可以写成上面代码一样。
异常链
在Java1.4之后,如果想要在捕获一个异常之后抛出另一个异常,可以把原来的异常放到另一个异常的构造器中:1
2
3
4
5
6try {
// code
} catch(NoSuchFieldException e) {
// 把 NoSuchFieldException 放到 RuntimeException中
throw new RuntimeException(e);
}
在 Throwable 的子类中,只有三种基本的异常类提供了带 cause 参数的构造器。它们是 Error(用于 Java 虚拟机报告系统错误)、Exception 以及 RuntimeException。如果要把其他类型的异常链接起来,应该使用 initCause0 方法而不是构造器。
1 | class SimpleException extends Exception {} |
第二十二章 枚举
当创建枚举类型时,编译器会为你生成一个辅助类,这个类自动继承自java.lang.Enum。java.lang.Enum提供了下例所示的一些功能:
values()
:返回一个由枚举常量组成的数组;ordinal()
:返回当前枚举常量的声明顺序(0开始);getDeclaringClass()
:返回该枚举的类路径,如class cn.lenqq.OrderStatusEnum
;valueOf(String enumName)
:返回对应enumName的枚举实例,如果不存在则抛出异常;
values()
和 valueOf()
方法是编译器添加的静态方法,不是继承自Enum类。所以如果将具体的枚举类型转换成了Enum类,就没办法使用 values()
了。但是有另一个途径去实现 values()
的效果。就是通过 e.getClass().getEnumConstants()
。
EnumSet:高性能。元素必须来自某个枚举类型。
EnumSet.of() 方法有六种重载,分别是1~5个参数和可变参数版本,可变参数版本的性能会略慢(因为需要构建可变参数数组)。
EnumMap:相对于常量特定方法来讲,ta的优势是可以改变值对象。常量是不可变的。
常量特定方法
Java的枚举机制允许我们为每一个枚举实例编写不同的方法,这些方法就是常量特定方法。
【需要在枚举类中定义一个或多个抽象方法】
重写常量特定方法:
【不用实现一个抽象方法】
1 | // enums/OverrideConstantSpecific.java |
第二十三章 注解
基本语法
注解的语法十分简单,主要是在现有语法中添加 @ 符号。Java 5 引入了前三种定义在 java.lang 包中的注解:
- @Override:表示当前的方法定义将覆盖基类的方法。如果你不小心拼写错误,或者方法签名被错误拼写的时候,编译器就会发出错误提示。
- @Deprecated:如果使用该注解的元素被调用,编译器就会发出警告信息。
- @SuppressWarnings:关闭不当的编译器警告信息。
- @SafeVarargs:在 Java 7 中加入用于禁止对具有泛型varargs参数的方法或构造函数的调用方发出警告。
- @FunctionalInterface:Java 8 中加入用于表示类型声明为函数式接口
注解可以和任何修饰符共同用于方法,诸如 public、static 或 void。
定义注解
注解的定义也需要一些元注解(meta-annoation)
@Target
定义你的注解可以应用在哪里(例如是方法还是字段)。
@Retention
定义了注解在哪里可用,在源代码中(SOURCE),class文件(CLASS)中或者是在运行时(RUNTIME)。
不包含任何元素的注解称为标记注解(marker annotation)。
注解中的元素(属性)可以设置默认值:String description() default "no description";
注解的元素在使用时表现为 名-值 对的形式,并且需要放置在 @UseCase
声明之后的括号内。
元注解
Java 语言中目前有 5 种标准注解(前面介绍过),以及 5 种元注解。元注解用于注解其他的注解
注解 | 解释 |
---|---|
@Target | 表示注解可以用于哪些地方。可能的 ElementType 参数包括: CONSTRUCTOR:构造器的声明 FIELD:字段声明(包括 enum 实例) LOCAL_VARIABLE:局部变量声明 METHOD:方法声明 PACKAGE:包声明 PARAMETER:参数声明 TYPE:类、接口(包括注解类型)或者 enum 声明 |
@Retention | 表示注解信息保存的时长。可选的 RetentionPolicy 参数包括: SOURCE:注解将被编译器丢弃 CLASS:注解在 class 文件中可用,但是会被 VM 丢弃。 RUNTIME:VM 将在运行期也保留注解,因此可以通过反射机制读取注解的信息。 |
@Documented | 将此注解保存在 Javadoc 中 |
@Interited | 允许子类继承父类的注解 |
@Repeatable | 允许一个注解可以被使用一次或者多次(Java 8)。 |
大多数时候,程序员定义自己的注解,并编写自己的处理器来处理他们。
编写注解处理器
如果没有用于读取注解的工具,那么注解不会比注释更有用。使用注解中一个很重要的部分就是,创建与使用注解处理器。
注解元素
注解元素可用的类型如下所示:
- 所有基本类型(int、float、boolean等)
- String
- Class
- enum
- Annotation
- 以上类型的数组
默认值限制
元素不能有不确定的值。要么有默认值,要么在使用时提供元素的值。
这里有另外一个限制:任何非基本类型的元素, 无论是在源代码声明时还是在注解接口中定义默认值时,都不能使用 null 作为其值。
建议自定义一些特殊的值表达某个元素不存在,如:
1 | // annotations/SimulatingNull.java |
生成外部文件
如果注解中定义了名为 value 的元素,并且在使用该注解时,value 为唯一一个需要赋值的元素,你就不需要使用名—值对的语法,只需要在括号中给出 value 元素的值即可。
如果想将注解应用于所有的 ElementType,那么可以不写 @Target 元注解。
注解不支持继承
简单实现注解处理器处理注解
通过使用 forName() 方法加载类,然后使用 getDeclaredFields() 方法获取类中所有的字段,通过使用 field.getDeclaredAnnotations() 获取字段上的所有注解,然后判断是否有某个注解,然后做对应的操作即可;
使用 javac 处理注解
- 创建一个 编译时注解处理器 可以在 java 源文件上使用注解;
- 限制:不能通过这个处理器改变源码,只能创建新的文件;
- 如果通过 编译时注解处理器 创建了新的文件,工具会不断的循环检查源文件,直到不再有源文件产生。然后它会编译所有的源文件;
- 每一个自定义的注解都需要注解处理器;
- javac 可以将多个注解处理器合并在一起。可以添加监听器,用于接收注解处理完成后的通知(没懂);
最简单的处理器
@Retention(RetentionPolicy.SOURCE),注解只保留在源代码中,javac 编译之后会删除掉,javac 是唯一有机会处理注解的代理。
@SupportedAnnotationTypes:支持哪些注解;
@SupportedSourceVersion:支持的 Java 版本;
一个简单的注解处理器:继承 AbstractProcessor,重写 process(Set<? extends TypeElement> annotations, RoundEnvironment env) 方法。 关于参数 annotations 和 env 怎么操作,一定要看一下书本原文。
更复杂的处理器
一定要看一下原文的使用例子。
如下是一个命令行,可以在编译的时候使用处理器:
1 | javac -processor annotations.ifx.IfaceExtractorProcessor Multiplier.java |
第二十四章 并发编程
令人迷惑的术语
- 并发是指如何正确、高效地控制共享资源;(突出的是解决阻塞类型问题)
- 并行是指如何利用更多的资源来产生更快速的响应;(突出的是解决速度类型问题)
作者对于并发的定义:并发是一系列聚焦于如何减少等待并提升性能的技术。
如果只有一个处理器,并发会使系统变得更慢,因为任务切换会带来性能损耗。但是,某些情况下,并发模型能在开发上带给我们一些便利,这时候即使慢一点也是值得的。
并发为速度而生
可以的话,先换一台更高配置的机器,或者使用更快的算法,实在没办法了,才去考虑使用并发,并且只能在隔离的环境中使用。
Java并发四定律
- 不要使用并发。
- 一切都不可信,一切都很重要。
- 能运行不代表没有问题。
- 你终究要理解并发。
用了 parallel() 并不一定就能让程序跑得更快,要自己试过看行不行。
parallel() 并非灵丹妙药
处理器的缓存机制会导致耗时增加。
当你使用 long 类型数组时,因为ta是一段连续的内存,处理器会把ta里面的数据放到缓存中,所以要快很多。
当你使用 Long 类型数组时,实际上是一段连续的 Long 类型对象引用的数组,这些引用所指数据在缓存之外,所以要慢很多。
parallel() 和 limit() 的作用
parallel() 和 limit() 的搭配使用只适合于高手。
它们的搭配使用实际上就是在请求随机输出。
如果想要随机生成一定数量的int流,可以用 IntStream.range().limit(10).parallel()。
创建和运行任务
Java 8 版本实现并发的理想方式是 CompletableFuture。
Java5 开始,可以通过实现 Runnable 接口,创建一个任务,然后将该任务类交给 ExecutorService 的 execute(Runnable run) 方法执行。
ExecutorService为您管理线程,并且在运行任务后重新循环线程而不是丢弃线程。
ExecutorService.shutdown(): 它告诉ExecutorService完成已经提交的任务,但不接受任何新任务。调用了这个shutdown之后还给提交任务的话,会抛出RejectedExecutionException.
exec.shutdownNow(): 它除了不接受新任务外,还会中断当前运行的任何任务。中断是错误的,容易出错并且不鼓励。
Executors.newSingleThreadExecutor(): 线程封闭的。同时只会运行一项任务。相当于单线程,虽然慢了点但是安全。
Executors.newCachedThreadPool(): 非线程安全,每个任务都会获得自己的线程。
避免竞态条件的最好方法是避免使用可变共享状态。我们称其为自私儿童原则(selfish child principle):什么都不共享。
Future 不推荐使用。
CompletableFuture
基本用法
1 | // concurrent/Machina.java |
我们可以通过 completedFuture() 方法包装出一个对象:CompletableFuture<Machina> cf = CompletableFuture.completedFuture(new Machina(0));
然后利用 future 的 Apply 类型的方法,来调用 Machina 里面的方法:CompletableFuture<Machina> cf2 = cf.thenApply(Machina::work);
总结
应用并发的唯一正当理由是“你的程序跑得不够快”。
应该要首先尝试用最简单的方法,最简单的方法在大部分情况下已经跑得足够快了。
永远不能相信一个使用了共享内存的并发程序能够正确运行。
良好的编码规范
【p32】尽量不要在构造器中调用类的其他方法(final方法,private方法除外)。
【p35】使用继承表达行为的差异,使用属性表达状态的变化。
【问题】
程序运行的时候,java对象 是怎么存储的呢?特别是内存怎么分配的呢?
计算机中有五个可以存储数据的地方:寄存器(我们无权控制)、栈内存、堆内存、常量存储、非 RAM 存储。
switch循环没理解清楚!!!
case 语句不必须要包含 break 语句。如果没有 break 语句出现,程序会继续执行下一条 case 语句,直到出现 break 语句。
【todo】不知道如何回收不是 new 分配的内存,那什么对象不是通过new创建的呢?
【todo】静态对象、静态代码块、非静态对象、非静态代码块、构造方法,它们的执行顺序是怎么样的?
【todo】ArrayList 中间插入和删除元素的速度有多慢?
编译器通过方法签名区分方法(方法名和参数类型)
这么一行代码就读取到文件了?14章:随机数流
1 | List<String> lines = Files.readAllLines(Paths.get(fname)); |
Todo
- final 关键字的应用案例,可以写很多篇事例了。
小芝士
可以给 Random 对象一个种子(以便程序再次运行时产生相同的输出):new Random(47);
流是懒加载的。这代表着它只在绝对必要时才计算。你可以将流看作“延迟列表”。由于计算延迟,流使我们能够表示非常大(甚至无限)的序列,而不需要考虑内存问题。
Random 类只能生成基本类型 int, long, double 的流。但是可以通过boxed()方法把基本类型“装箱”。
可以通过静态导入的方式,直接使用静态方法:
1 | // streams/Ranges.java |