0%

备忘一下 Testlink API 调用实现。Testlink 是有实现自己的 API 接口的,你可以通过它来操作 project, test plan 等对象。

API 示例

安装 pip lib: pip install TestLink-API-Python-client, project repo: Github, Testlink clint - Python

登陆 Testlink -> My Settings -> My personal access key 拿到 API 授权的 key, 尝试链接

1
2
3
4
5
6
7
8
9
10
11
12
13
import testlink
url = "https://<hostname>/lib/api/xmlrpc/v1/xmlrpc.php"
# for this key, you can get forom Testlink -> click My Settings → my personal access key field
key = "api_auth_key" # test link personal key
tlk = testlink.TestLinkHelper(url, key).connect(testlink.TestlinkAPIClient)
print(tlk.tlk.countProjects())
# 39

# get project info
projects = tlk.getTestProjects()
for sub in projects:
print("id: %s, prefix: %s, name: %s" % (sub['id'], sub['prefix'], sub['name']))
# id: 5182, prefix: PLT#, name: Platform Foundations and Integrations

其他的 API 都和差不多类似的,通过使用 Ipython + tab 基本都可以找到

注意点

python 提供的接口中有一个很有意思:tlk.whatArgs('getTestProjects') 他可以给出对应 API 的调用方式,参数列表的信息,很有用

client 项目的 example 路径下,有几个示例,写的很清楚,基本上把所有支持的命令都写了,值得参考

PS:记得查看 Testlink 版本,就我自己的情况,公司内部用的还是 13 年的版本(1.9.7) 而最新的都已经是 1.9.20 了,好多 API 都不支持,可以 blame 以下 example,查看对应的例子是什么时候加进去的,看你想要的功能是否支持

Command is a behavioral design pattern that turns a request into a stand-alone object that contains all information about the request. This transformation lets you parameterize methods with different requests, delay or queue a request’s execution, and support undoable operations.

命令模式是行为模式的一种,它将请求包裹在一个单独的对象里面,包含了一个请求的所有信息,通过这种方式,它让你的处理方法有一个统一的参数格式,并且这种处理方式为处理延迟,队列和回滚等操作提供可行性

优点:

  • 易于实现一个命令队列
  • 易于整合日志
  • 允许请求方取消
  • 易于实现撤销和重做操作
  • 加入新的命令不会影响已有的实现
  • 将 command 从执行类中剥离出来,独立存在

解释

角色定义:

  • Client: 终端,初始化对象,类似执行环境,可以代表一个人
  • Command: 代表一个命令的接口
  • Concrete Command: 具体的命令,会持有一个 Receiver 引用
  • Receiver: 有能力执行具体命令的主体
  • Invoker(Sender): 持有命令的类似容器的东西

PS: 感觉上,写 demo 的时候可以把 Client 和 Invoker 合并成一个东西

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
public class Receiver {
public void doSomething() { System.out.println("Receiver do something ..."); }
}

public interface Command {
void execute();
}

public class ConcreteCommand implements Command {
Receiver receiver;

public ConcreteCommand(Receiver receiver) { this.receiver = receiver; }

@Override
public void execute() { receiver.doSomething(); }
}

public class Invoker {
private Command cmd;

public Invoker(Command cmd) { this.cmd = cmd; }

public void action() { cmd.execute(); }
}

public class Client {
public static void main(String[] args) {
Receiver receiver = new Receiver();
Command cmd = new ConcreteCommand(receiver);
Invoker invoker = new Invoker(cmd);

invoker.action();
}
}

// Receiver do something ...

案例分析

TIJ4 中 inner class 下的 Inner classes & control frameworks 小结说的 GreenHouse 采用了 Command Pattern,说实话我不是理解,应该是我对这个模式的使用太刻板了,不够变通吧

大话设计模式

用烧烤摊和烧烤店来举例子,烧烤摊客户直接和摊主紧耦合,混乱。烧烤店中有服务员做中介,效率高

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
public abstract class Command {
protected Barbecuer barbecuer;
public Command(Barbecuer barbecuer) {
this.barbecuer = barbecuer;
}

public abstract void execute() ;
}

public class BakeChickenWingCommand extends Command {
public BakeChickenWingCommand(Barbecuer barbecuer) {
super(barbecuer);
}

@Override
public void execute() {
barbecuer.bakeChickenWing();
}
}

public class BakeMuttonCommand extends Command {
public BakeMuttonCommand(Barbecuer barbecuer) {
super(barbecuer);
}

@Override
public void execute() {
barbecuer.bakeMutton();
}
}

public class Barbecuer {
public void bakeMutton() {
System.out.println("Bake mutton...");
}

public void bakeChickenWing() {
System.out.println("Bake chicken wing...");
}
}

public class Waiter {
private List<Command> cmds = new ArrayList<>();

public void setOrder(Command cmd) {
cmds.add(cmd);
System.out.println("Set order: " + cmd.getClass().getSimpleName() + ", Time: " + new Date());
}

public void cancelOrder(Command cmd) {
cmds.remove(cmd);
System.out.println("Cancel order: " + cmd.getClass().getSimpleName() + ", Time: " + new Date());
}

public void noteBaker() {
for (Command cmd : cmds) {
System.out.println("Process order: " + cmd.getClass().getSimpleName() + ", Time: " + new Date());
}
}
}

public class Client {
public static void main(String[] args) {
Barbecuer baker = new Barbecuer();
Command bakeWing = new BakeChickenWingCommand(baker);
Command bakeMutton = new BakeMuttonCommand(baker);

Waiter waiter = new Waiter();
waiter.setOrder(bakeMutton);
waiter.setOrder(bakeWing);
waiter.setOrder(bakeWing);
waiter.setOrder(bakeMutton);
waiter.cancelOrder(bakeMutton);

waiter.noteBaker();
}
}

Head First Design Pattern

支持宏模式的 command pattern 它管这种模式叫 Meta Command Pattern

资料

  • Predicate pre = Boolean::valueOf; compile failed, 提示说: Cannot resolve method 'valueOf' 改为 Predicate<Boolean> pre = Boolean::valueOf; works
  • Predicate<Boolean> pre = Boolean::valueOf; pre.test(null); 会抛出 NPE
  • Predicate<Boolean> pre2 = Objects::isNull; pre2.test(null); 类似的调用 Objects 的 isNull 等方法却不会跑错

貌似无解,根据这个 StackOverflow lambda 相关的问题来看,JVM 解析 lambda 的时候,直接将我们写的表达式编译成字节码,然后 JVM 通过 InvokeDynamic 指令就执行了,如果是这样的还,我是我发看到他的中间状态的,上面的那些问题看来只能通过经验来解决了 (; ̄ェ ̄)

Intro

Interfaces and abstract classes provide more structured way to
separate interface from implementation.

Abstract classes and methods

描述了前几章乐器的例子,其中使用 abstract class 会更符合题意

Interfaces

interface 是 abstract 在抽象上的进一步体现,他允让你决定方法名,参数列表和返回值,并且你不用实现它,结果只做了规范,但不需要实现。

通过 interface 你仿佛在说:所有实现了这个接口的类都应该长这样。其他编程语言中也叫protocol

除此之外,interface 变相的让你的类实现了多重继承,你的类可以转化为多个基类。

interface 可以用 public 修饰,也可以不写(包可见). 在 interface 中声明的 field 都是默认 static + final 的。interface 中声明的方法即使你没有显示的指定访问修饰符也是默认是 public 的。

// TODO,这里也给了例子,不过补上要等看了前面的章节再说了

Complete decoupling

设想这么一种场景,我们创建一个处理器类,他有两个方法 name() 和 process()。process() 可以接收字符串并处理。我们再声明一个 Apply 类作为 client 端声明方法接收 Processor 类,调用 Processor 方法。这个没记错的话就是策略模式了。作者的行为中也这么指出。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
class Processor {
public String name() {
return getClass().getSimpleName();
}
Object process(Object input) { return input; }
}

class Upcase extends Processor {
String process(Object input) { // Covariant return
return ((String)input).toUpperCase();
}
}

class Downcase extends Processor {
String process(Object input) {
return ((String)input).toLowerCase();
}
}

class Splitter extends Processor {
String process(Object input) {
// The split() argument divides a String into pieces:
return Arrays.toString(((String)input).split(" "));
}
}

public class Apply {
public static void process(Processor p, Object s) {
System.out.println("Using Processor " + p.name());
System.out.println(p.process(s));
}
public static String s =
"Disagreement with beliefs is by definition incorrect";
public static void main(String[] args) {
process(new Upcase(), s);
process(new Downcase(), s);
process(new Splitter(), s);
}
}

// Using Processor Upcase
// DISAGREEMENT WITH BELIEFS IS BY DEFINITION INCORRECT
// Using Processor Downcase
// disagreement with beliefs is by definition incorrect
// Using Processor Splitter
// [Disagreement, with, beliefs, is, by, definition, incorrect]

现在我们有另一批过滤器类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
public class Waveform {
private static long counter;
private final long id = counter++;

public String toString() {
return "Waveform " + id;
}
}

public class Filter {
public String name() {
return getClass().getSimpleName();
}

public Waveform process(Waveform input) {
return input;
}
}

public class LowPass extends Filter {
double cutoff;

public LowPass(double cutoff) {
this.cutoff = cutoff;
}

public Waveform process(Waveform input) {
return input; // Dummy processing
}
}

public class HighPass extends Filter {
double cutoff;

public HighPass(double cutoff) {
this.cutoff = cutoff;
}

public Waveform process(Waveform input) {
return input;
}
}

public class BandPass extends Filter {
double lowCutoff, highCutoff;

public BandPass(double lowCut, double highCut) {
lowCutoff = lowCut;
highCutoff = highCut;
}

public Waveform process(Waveform input) {
return input;
}
}

他的行为模式和前面的 Processor 是很相似的,理论上来说我们可以将 Filter 看作是一个算法的集合,然后 Apply 中接收 Filter 这族算法,同时接收一个 Waveform 作为输入,process() 方法产生输出即可。但是由于 Apply 定义的方法指定了 Processor 为参数,导致兼容 Filter 失败了

这时我们改一下 Processor 的代码,将其定义为一个接口

1
2
3
4
5
6
7
8
9
10
11
public interface Processor {
String name();
Object process(Object input);
}

public class Apply {
public static void process(Processor p, Object s) {
System.out.println("Using Processor " + p.name());
System.out.println(p.process(s));
}
}

同时包装一个处理 String 的 Processor 基类,并实现各种具体的处理器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
// 虽然它这种 abstract 类中直接写具体调用的做法我总感觉很飘逸,但是理解不难
public abstract class StringProcessor implements Processor{
public String name() {
return getClass().getSimpleName();
}
public abstract String process(Object input);
public static String s = "If she weighs the same as a duck, she’s made of wood";
public static void main(String[] args) {
Apply.process(new Upcase(), s);
Apply.process(new Downcase(), s);
Apply.process(new Splitter(), s);
}
}
class Upcase extends StringProcessor {
public String process(Object input) {
// Covariant return
return ((String)input).toUpperCase();
}
}
class Downcase extends StringProcessor {
public String process(Object input) {
return ((String)input).toLowerCase();
}
}
class Splitter extends StringProcessor {
public String process(Object input) {
return Arrays.toString(((String)input).split(" "));
}
}

// Using Processor Upcase
// IF SHE WEIGHS THE SAME AS A DUCK, SHE’S MADE OF WOOD
// Using Processor Downcase
// if she weighs the same as a duck, she’s made of wood
// Using Processor Splitter
// [If, she, weighs, the, same, as, a, duck,, she’s, made, of, wood]

现在轮到重构 Filter 部分了,和我原本料想的直接改代码不同,作者直接假定,这部分 Filter 类的代码就是第三方方法,你无权改动,这个时候怎么办?他又引入了 Adaptor 模式。好在这两个模式我都还挺熟悉,不过没记录过,明天花点时间写一下 ╮( ̄▽ ̄””)╭

通过实现 Processor 实现一个 Filter 的包装类, 然后将 Filter 传给包装类,并把包装类作为 Apply.process() 的方式实现了曲线救国,秀啊,小老弟。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
class FilterAdapter implements Processor {
Filter filter;

public FilterAdapter(Filter filter) {
this.filter = filter;
}

public String name() {
return filter.name();
}

public Waveform process(Object input) {
return filter.process((Waveform) input);
}
}

public class FilterProcessor {
public static void main(String[] args) {
Waveform w = new Waveform();
Apply.process(new FilterAdapter(new LowPass(1.0)), w);
Apply.process(new FilterAdapter(new HighPass(2.0)), w);
Apply.process(new FilterAdapter(new BandPass(3.0, 4.0)), w);
}
}

// Using Processor LowPass
// Waveform 0
// Using Processor HighPass
// Waveform 0
// Using Processor BandPass
// Waveform 0

“Multiple inheritance” in Java

TBD

Nesting interfaces

Interfaces 可以内嵌到 class 或者其他 interface 内部,这种做法可以引入一些有趣的特性。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
class A {
interface B {
void f();
}

public class BImp implements B {
public void f() {
}
}

private class BImp2 implements B {
public void f() {
}
}

public interface C {
void f();
}

class CImp implements C {
public void f() {
}
}

private class CImp2 implements C {
public void f() {
}
}

private interface D {
void f();
}

private class DImp implements D {
public void f() {
}
}

public class DImp2 implements D {
public void f() {
}
}

public D getD() {
return new DImp2();
}

private D dRef;

public void receiveD(D d) {
dRef = d;
dRef.f();
}
}

interface E {
interface G {
void f();
}

// Redundant "public":
public interface H {
void f();
}

void g();
// Cannot be private within an interface:
// ! private interface I {}
}

public class NestingInterfaces {
public class BImp implements A.B {
public void f() {
}
}

class CImp implements A.C {
public void f() {
}
}

// Cannot implement a private interface except
// within that interface’s defining class:
// ! class DImp implements A.D {
// ! public void f() {}
// ! }
class EImp implements E {
public void g() {
}
}

class EGImp implements E.G {
public void f() {
}
}

class EImp2 implements E {
public void g() {
}

class EG implements E.G {
public void f() {
}
}
}

public static void main(String[] args) {
A a = new A();
// Can’t access A.D:
// ! A.D ad = a.getD();
// Doesn’t return anything but A.D:
// ! A.DImp2 di2 = a.getD();
// Cannot access a member of the interface:
// ! a.getD().f();
// Only another A can do anything with getD():
A a2 = new A();
a2.receiveD(a.getD());
}
}

这种嵌套 interface 的语法是合理的,和普通的 interface 一下,所有访问修饰符都用在嵌套接口上。

内嵌接口在使用上和 内部类并没有什么不同. 那么 private 的 interface 有什么价值呢?如果你以为 nested private interface 的实现只能是 private 的,那么你就错了。看看 DImp2 就可知,其实现可以是任意访问类型的。

1
2
3
4
5
6
7
8
9
! 后面的这段感觉翻译不过去,模模糊糊,看了中文版,貌似没有相关的章节。。。。汗

but A.DImp2 shows that it can also be implemented as a public class. However,
A.DImp2 can only be used as itself. You are not allowed to mention the fact that it
implements the private interface D, so implementing a private interface is a way to force
the definition of the methods in that interface without adding any type information (that is,
without allowing any upcasting).

个人理解为 private 接口将接口的实现和定义限制在了定义类里面。而且一般使用的时候都是会返回接口类,像上面的 D getD(), 而 D 又是 private 的,限制了他的使用,这应该就是原文中 'A.DImp2 can only be used as itself' 的意思吧

getD() 方法是 private 修饰的嵌套接口的更特殊的使用方式,在 main() 中,我们 comment 了很多对 D 接口的引用,但是这些用法都有编译错误。唯一的使用方式是新建一个 A 对象,调用以 D 为参数的方法。

Interface E 的例子想要说明的事,接口内部也能声明接口,但是秉承接口内部元素必须都是规则,内嵌的接口也必须只能是 public 的

Nestinglnterfaces 类中给出了嵌套接口更多的实现,当我们实现一个嵌套接口的外部接口(E)时,是不需要我们实现对应的嵌套接口的。

private interface 在外部是不能访问的,只能在声明他的类内部做实现

今天再看一个 defect 的时候涉及到 HashMap 存储的问题,回头想一下发现自己只对 HashMap 以 key 的 Hash 作为依存储依据这点比较清楚外,其他的印象很模糊,写这篇文章记录一下。想要了解的问题如下:

  • HashMap 再存储时是否只用 key 的 hash 做依据,和 value 有关系吗 - 只和 key 有关系,只用 key 的 hashCode 做 hash 后的结果作为判断依据
  • HashMap 底层使用什么数据结构存储的 - array + list/tree(红黑树)
  • HashMap 的类继承关系

以后对 HashMap 的知识点都可以考虑在这篇中做扩展,做成一个总集篇

有趣的知识点:

  • 为什么因子选在 0.75? - 在头部注释中给出了解释,根据统计学的结果,hash 冲突符合泊松分布,在 7-8 之间时冲突概率最低

HashMap 图示

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
 +-------------------------------------------+ 
| | | | | |
| Node | Node | Tree | Node | ... |
| | | | | |
| | | | | |
|------|------| |------ | |
| next | next | | next | |
+------|------|-------|-------|-------------+
.
.
.
+------+
| Node |
| |
| |
|----- |
| next |
+------+

HashMap 的类继承关系

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

I I
+--------+ +----------------------------+
| Map | | Map/Cloneable/Serializable |
+--------+ +----------------------------+
^ ^
| |
C | |
+-------------+ |
| AbstractMap | |
+-------------+ |
^ |
| |
| |
| |
| |
+-------------------------------+
| HashMap |
| |
+-------------------------------+

JDK8 中对 HashMap 的实现做了改动,原先是 Array + link list, 时间复杂度 O(1)+O(n) 当哈希冲突严重时,性能就取决于后者了。新的实现采用 Array + list/tree, 当 list 长度大于 8 时就会将 list 转化为红黑树,即 O(1)+O(logn) 比原先会有提升

基本数据结构

Map.Entry: 定义了基本 get/set 方法的接口

HashMap.Node: 单项可延伸的链表结构

1
2
3
4
final int hash;
final K key;
V value;
Node<K,V> next;

LinkedHashMap.Entry: 增加了 before,after 属性,但是没有调用,好奇怪

HashMap.TreeNode: 红黑树实现,extends LinkedHashMap.Entry, 但是在我看来直接继承 HashMap.Node 不是更好?

put 方法实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
               +----------------+                             
| Start |
| Give node info |
+----------------+
|
|
v
+-----------+
|If Conflict|
+-----------+
No / \ Yes
/ \
v v
+------------------+ +------------------+
| Creae new node | | Check key & node |
+------------------+ +------------------+
| / | \
| / | \
| / | \
| / | \
| / | \
| v v v
| +---------------+ +------------------+ +-----------+
| | key same with | |Node is tree type | |List type |
| | first element | +------------------+ +-----------+
| +---------------+ | /
| | | /
| | | /
| v v v
| +-----------------------------------+
|--------> | Check if resize |
+-----------------------------------+
|
|
v
+--------+
| End |
+--------+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
// 代码实现

final V putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict) {
Node<K,V>[] tab; Node<K,V> p; int n, i;
if ((tab = table) == null || (n = tab.length) == 0) // 如果 tab 是空的,给一个初始 size
n = (tab = resize()).length;
if ((p = tab[i = (n - 1) & hash]) == null) // 如果 bucket 位置没有值,直接填充一个
tab[i] = newNode(hash, key, value, null);
else { // 如果 bucket 位置上有值,则再看
Node<K,V> e; K k;
if (p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k)))) // 如果 key 已经存在,将值放入 e 在后面做 value 替换
e = p;
else if (p instanceof TreeNode) // 如果是 tree, 则在 tree 后面添加节点
e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
else { // 处理 linked list 的情况
for (int binCount = 0; ; ++binCount) {
if ((e = p.next) == null) { // 如果是末尾节点,直接 append
p.next = newNode(hash, key, value, null);
if (binCount >= TREEIFY_THRESHOLD - 1) // 如果达到转化阀值,将链表转化为树
treeifyBin(tab, hash);
break;
}
if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k)))) // 如果 key 重复,跳出循环
break;
p = e; // 给 p 赋值,继续循环
}
}
if (e != null) { // 对已经存在的 node 做值替换
V oldValue = e.value;
if (!onlyIfAbsent || oldValue == null)
e.value = value;
afterNodeAccess(e);
return oldValue;
}
}
++modCount;
if (++size > threshold)
resize();
afterNodeInsertion(evict);
return null;
}

get 方法实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
/**
* Implements Map.get and related methods.
*
* @param hash hash for key
* @param key the key
* @return the node, or null if none
*/
final Node<K,V> getNode(int hash, Object key) {
Node<K,V>[] tab; Node<K,V> first, e; int n; K k;
if ((tab = table) != null && (n = tab.length) > 0 && (first = tab[(n - 1) & hash]) != null) {
if (first.hash == hash && ((k = first.key) == key || (key != null && key.equals(k)))) // always check first node
return first;
if ((e = first.next) != null) {
if (first instanceof TreeNode) // 如果是 tree 类型,调用 tree 的 get 方法
return ((TreeNode<K,V>)first).getTreeNode(hash, key);
do { // 否则遍历链表
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
return e;
} while ((e = e.next) != null);
}
}
return null;
}

resize

可以参考这篇内容: https://segmentfault.com/a/1190000015812438 , 现在状态不太好,看不进去。。。

Idea 调试优化

当调试 HashMap 时,默认设置下 Node 只显示 K,V 值,对其他细节,比如静态变量,Node 的 next 都是忽略的,可以通过以下方式查看

临时方案

在底部 debug 界面,选中需要查看的 entry, 右键 View as -> Object 即可,但是下次调试是会重制,需要再次设置

永久方案

Debug 是选中 tab 下的元素,右键 View as -> Create… -> Apply render to object of type 中输入 java.util.HashMap$Node 再 Apply 以下就行了。我这边是自动填充好了的

查看静态变量

Customize Data Views -> 勾选 static fields, static final fields

PS: 我本地设置了貌似没什么效果 (; ̄ェ ̄)

参考

Python 的 re 包里面的 search 和 match 经常搞不清楚,特意整理记录下 re 包支持的方法

re.search(pattern, string, flags=0) 字符串任意位置匹配

Scan through string looking for the first location where the regular expression pattern produces a match, and return a corresponding match object. Return None if no position in the string matches the pattern; note that this is different from finding a zero-length match at some point in the string.

1
2
3
4
ret = re.search(r'[\d]+', 'aaa123bbbb')
ret.group(0)

# '123'

re.match(pattern, string, flags=0) 从头开始匹配

If zero or more characters at the beginning of string match the regular expression pattern, return a corresponding match object. Return None if the string does not match the pattern; note that this is different from a zero-length match.

Note that even in MULTILINE mode, re.match() will only match at the beginning of the string and not at the beginning of each line.

If you want to locate a match anywhere in string, use search() instead (see also search() vs. match()).

1
2
3
4
5
re.match(r'[\d]+', 'aaa123aaa')
# null

re.match(r'[\d]+', '123aaa')
<re.Match object; span=(0, 3), match='123'>

Chrome + SwitchyOmega 已经配置了无数遍了,抽出来单独写一篇详细教程

配置步骤

外网限制,安装 Chrome 扩展极度不便。所有的第一步是安装 SwithyOmega 的扩展来翻墙。

  1. SwitchyOmega 官网下载 crx 文件。
  2. 重命名,将后缀改为 zip,然后直接拖到浏览器 - 直接将 crx 文件拖进去会报错 CRX_HEADER_INVALID
  3. 左键点击插件 icon -> Options -> New profile 添加 Profile Proxy 类型配置文件
  4. Protocol, Server, Port 一次填入 SOCKS5, 127.0.0.1, 1086
  5. 选择 auto switch, Rule List Config 下选择 AutoProxy 类型
  6. Rule List URL 填入 https://raw.githubusercontent.com/gfwlist/gfwlist/master/gfwlist.txt 点击下载更新文件
  7. Apply Change, 打完收工

PS: 公司里面有提供 PAC 解决方案,很方便,规则都给你定好了 New Profile 选择 PAC 类型,填入指定的 URL, 再点击 download 即可

Resources

刚好这几天要修一个异常处理的问题,回顾以下异常相关的知识点

想要解决的问题:

[x] runtime 异常和其他的异常有什么区别
[x] 异常处理的最佳实践是什么
[x] catch 里加一个 return,final 还会执行吗

Answers:

  1. RuntimeException 可以不用写 try-catch 处理,而 checked exception 你必须添加 try-catch block
  2. 本章的后半截有介绍,首先是根据当前节点是否有能力处理对应的异常,倒数第二章还有介绍一些规则。
  3. final 还是会执行

前述

最理想的异常捕捉点是编译期,即代码运行之前。但是并不是所有的 errors 都能在编译器就被捕捉到。剩下的这些问题,我们需要在运行时让异常产生者将错误信息提交给接受者做适当的处理。

提升异常处理是增加代码健壮性的重要方式。异常恢复是每个码农都需要考虑的问题,对于 Java 这种旨在为他人提供接口的语言来说,这点尤为重要。通过提供 error-reporting 错误处理模式,Java 允许组件代码将异常传给客户端代码处理。

Java 中的异常处理机制旨在以尽量少的代码完成经可能多的功能的同时让你的程序覆盖尽可能多的异常情况。异常机制不难学,并且学会了他能马上对你的项目产生益处,他是唯一官方指定的处理异常的方式并且编译器强制检查的。

本章将介绍一些必须要用到异常的代码以及遇到异常时的处理方案。

Concepts

C 和其他一些早期语言经常会有多中处理异常的方式,这些方式基本都是便宜形式,并且不是语言的一部分。典型的案例就是放回一个特殊的值或者设置一个特殊的 flag,接收方根据这个返回值来进行相应的处理。但是渐渐的码农们突然意识到,自己定义的异常情况可能是不充分的,并且为了覆盖这些没有覆盖到的情况,代码会变得越来越难以维护。

解决方案是将异常处理正规化,这种思路的出现是一个很长过的过程,可以追溯到 1960 年。

exception 表示:我处理这个异常是为了。。。当异常发生时,你可能并不清楚你需要做什么,但是你知道,你不能在正常的执行下去了,你应该做点什么。你就可以将这个异常提交给上层处理,那里可能具备足够的知识处理它。

异常处理机制的另一个明显的好处是,它可以简化你的异常处理代码。如果没有他你必须在多个地方编写检测代码。有了它之后,就不必这么做了,exception 会保证,有人在合适的地方做检查。你只需要在一个地方处理即可。这种机制简化了你的代码,代码被分为两个分之,正常的分支和异常分支,同时你阅读,书写,调试代码也会变的更方便。

Basic exceptions

发生异常的条件下,程序会停止执行当前方法。当代码发生异常时,程序由于缺少某些信息,已经不能继续执行程序了,他能做的就是跳出当前的上下文并且将执行权交给上层。

1\0 就是这么一种情况,当代码中出现了除 0 的情况,就需要检查一下了。可能你知道为什么要除 0,可能是你业务逻辑的需要,你知道在这种情况下需要做什么。但是如果这是一个意外情况,你必须停止当前方法并且抛出一个异常。

当你跑出一个异常时,有几件事情会发生。首先一个异常对象会在堆上通过 new 的方式被创建出来。然后当前的执行路径被阻断,异常对象被当前上下文弹出。exception-handling 处理机制开始接收这个异常,并试图妥善的处理它。

Exception handler 就是处理异常的地方,他会判断是否继续执行或者寻求其他解决路径。

想象一个简单的场景,比如你有一个对象引用叫做 t,他可能没有被初始化过,所以你想在使用前检查一下。你可以将检查的错误通过一个对象包裹起来并 thorwing 出去,这种做法就叫做抛出异常,代码如下:

1
2
if(t == null)
throw new NullPointerException();

这种做法可以让你为以后做打算,他会在之后的什么地方被处理,你很快能看到。

Exceptions 让你能够以 事务(transaction) 为单位处理问题, 你也可以将它想象成一个 undo 系统,你可以设置多个恢复点,当你的程序抛出异常时,他可以将程序恢复到某个稳定的节点。

Exceptions 最重要的一个点就是,当异常发生时,他阻止程序继续执行下去。C 语言在这方面就很糟糕,C 语言中是没有打断机制的,这发生异常时,你都不能预期它会执行到什么状态。

Exception arguments

和其他 Java 中的对象一样,你可以通过 new 关键字创建一个 exception 对象,它有两种构造函数,一种是无参的,另一种是带字符串的,比如:

1
throw new NullPointerException("t = null");

当然这个信息字符串也可以在之后通过调用 set 方法设置,之后有该种示例。

throw 这个关键词可以产生几种很有趣的结果。当你使用 new 创建一个 exception 的时候,你指定了 throw 的对象。虽然这个对象和你方法的返回值类型不一样,但是它还是会被这个方法返回。由此,你可以将异常处理看作一种特殊的 return 机制。返回的同时,方法和 scope 将会弹出(出栈)。

和普通方法的共同点到此为止了,接下来的处理方式将迥异于普通方法。异常将会在 exception handler 中被处理。

虽然你可以在处理异常时抛出任何 Throwable 的子类,但是一般来说,我们本会更具 error 的具体类型来指定它。error 的信息可以从他的名字和内容体现出来,但是通常来说异常只包含类名而没有其他什么内容。

Catching an exception

在理解异常捕捉之前,你先得理解守护区域的概念,就是被 try 包裹的部分。它代表了一段可能抛出异常的代码段,这段代码之后会紧接着一段异常处理代码。

The try block

如果你在方法体中抛出一个异常(或者方法体中调用的其他方法抛出异常),那么这个方法体在执行完 throwing 之后就结束了。如果你不想就这个结束,你可以在这些代码外面加一个 try block

1
2
3
try {
// Code that might generate exceptions
}

在不提供异常处理机制的语言中,如果你写代码很仔细的话,你可能需要为每一个方法添加异常处理,但是通过 try block 你只需要将他们全部包裹起来即可。这样你的代码会更容易阅读。

Exception handlers

当然,被抛出的异常都需要有一个地方来处理,这个地方就是 exception handler。它紧跟着 try block 通过关键字 catch 引出

1
2
3
4
5
6
7
8
9
10
try {
// Code that might generate exceptions
} catch(Type1 id1)|{
// Handle exceptions of Type1
} catch(Type2 id2) {
// Handle exceptions of Type2
} catch(Type3 id3) {
// Handle exceptions of Type3
}
// etc...

每一个 catch 就是一个小的方法体,只接收一个参数。有时你甚至不需要这个参数,仅仅根据异常的名字就可以写完处理逻辑。

如果异常被抛出,exception-handling 机制会搜寻第一个匹配的 catch 分支并进入,当 catch 分支走完后,异常处理被视为结束。不像 switch,catch 分支不需要 break 关键字,执行完直接返回。

Termination vs. resumption (中断还是继续?)

异常处理有两种模型,Java 采用的是中断,他认为当异常发生后,你不能在回到异常发生的节点。

另一种是 resumption(继续),他表示异常发生后,我们可以做一些补救措施,并且尝试重新执行失败的方法。采用这个方式意味着你在异常产生后依旧希望继续执行程序。

如果你想要 resumption 的处理方法,你不能在 error 发生的地方抛异常,或者你可以把你抛异常的代码放到一个循环中,多次运行,知道结果符合你预期。

历史上,码农们有尝试过使用 resumption 机制的操作系统,但最终回归到了 termination 机制。虽然 resumption 机制乍一听上去很美,但是并不是这么实用。可能是因为这种机制下你写的代码不能很通用,难以维护,特别是在写一些大型项目的时候,最后没有保留下来。

Creating your own exceptions

Java 允许你自己定制异常,你需要做的只是继承一个已有的异常类即可。当然继承的时候如果可能的话,选一个最贴近你异常类的,那是极好的。创建时只需要用它的默认构造函数即可,代码很简单:

自定义个一个异常 SimpleException 继承自 Exception,其他什么都没有。这是很常见的定义异常的方式,它调用默认的无参构造函数。定义异常时,取一个见名知意的名字显得尤为重要。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class SimpleException extends Exception {}

public class InheritingExceptions {
public void f() throws SimpleException {
System.out.println("Throw SimpleException from f()");
throw new SimpleException();
}

public static void main(String[] args) {
InheritingExceptions sed = new InheritingExceptions();
try {
sed.f();
} catch (SimpleException e) {
System.out.println("Caught it!");
}
}
}
// output:
// Throw SimpleException from f()
// Caught it!

下面是调用带参构造的例子,只需要少量的新加 code 就能实现带参构造,声明的时候用上 super 关键字即可。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
class MyException extends Exception {
public MyException() {
}

public MyException(String msg) {
super(msg);
}
}

public class FullConstructors {
public static void f() throws MyException {
System.out.println("Throwing MyException from f()");
throw new MyException();
}

public static void g() throws MyException {
System.out.println("Throwing MyException from g()");
throw new MyException("Originated in g()");
}

public static void main(String[] args) {
try {
f();
} catch (MyException e) {
e.printStackTrace(System.out);
}
try {
g();
} catch (MyException e) {
e.printStackTrace(System.out);
}
}
}
// output
// Throwing MyException from f()
// reading.container.MyException
// at reading.container.FullConstructors.f(FullConstructors.java:15)
// at reading.container.FullConstructors.main(FullConstructors.java:25)
// Throwing MyException from g()
// reading.container.MyException: Originated in g()
// at reading.container.FullConstructors.g(FullConstructors.java:20)
// at reading.container.FullConstructors.main(FullConstructors.java:30)

在 exception handler 中你可以看到一个方法调用叫做 e.printStackTrace(System.out)。他可以将异常信息输出。

Exercises

Exercise 1: (2) Create a class with a main( ) that throws an object of class Exception
inside a try block. Give the constructor for Exception a String argument. Catch the
exception inside a catch clause and print the String argument. Add a finally clause and
print a message to prove you were there.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class Exe1Exception extends Exception {
Exe1Exception(String msg) {
super(msg);
}
}

public class Exercise1 {
public static void main(String[] args) {
try {
throw new Exe1Exception("my exception msg...");
} catch (Exe1Exception e) {
System.out.println(e.getMessage());
} finally {
System.out.println("Into final cluster...");
}
}
}

// output
// my exception msg...
// Into final cluster...

Exercise 2: (1) Define an object reference and initialize it to null. Try to call a method
through this reference. Now wrap the code in a try-catch clause to catch the exception.

1
2
3
4
5
6
7
8
9
10
11
12
public class Exercise1 {
public static void main(String[] args) {
try {
String str = null;
System.out.println(str.isEmpty());
} catch (NullPointerException e) {
System.out.println("Invoked object is null...");
}
}
}
// output
// Invoked object is null...

Exercise 3: (1) Write code to generate and catch an
ArraylndexOutOfBoundsException.

1
2
3
4
5
6
7
8
9
10
public class Exercise1 {
public static void main(String[] args) {
try {
String[] strArr = new String[0];
strArr[0] = "str";
} catch (ArrayIndexOutOfBoundsException e) {
System.out.println("ArrayIndexOutOfBoundsException caught...");
}
}
}

Exercise 4: (2) Create your own exception class using the extends keyword. Write a
constructor for this class that takes a String argument and stores it inside the object with a
String reference. Write a method that displays the stored String. Create a try-catch clause
to exercise your new exception.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class ExeException extends Exception {
ExeException(String msg) {
super(msg);
}

public String getExeExceptionMsg() {
return getMessage();
}
}

public class Exercise1 {
public static void main(String[] args) {
try {
throw new ExeException("exe exception...");
} catch (ExeException e) {
System.out.println("Caught ExeException, msg: " + e.getExeExceptionMsg());
}
}
}
// output
// Caught ExeException, msg: exe exception...

Exercise 5: (3) Create your own resumption-like behavior using a while loop that
repeats until an exception is no longer thrown.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
class ExeException extends Exception {}

public class Exercise1 {
public static void main(String[] args) {
int index = 0;
while (index < 3) {
try {
System.out.println(index / 0);
} catch (ArithmeticException e) {
System.out.println("Arithmetic exception when index is " + index);
index ++;
}
}
System.out.println("end program...");
}
}

// output
// Arithmetic exception when index is 0
// Arithmetic exception when index is 1
// Arithmetic exception when index is 2
// end program...

Exceptions and logging

使用 logging 工具类记录信息,Logger.getLogger() 接收一个字符串作为参数创建 Logger 对象,如果没有其他设置,他回把对应的信息输出到 System.err 中去。printStackTrace() 接收一个 PrintWriter 作为参数,再通过调用 logger.severe() 将信息输出。

上面这种处理方式将所有的 logging 相关动作封装在了异常中,所以很简便,但是更常见的处理方式是将你要处理的 log 在 exception handler 中进行封装。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
import java.util.logging.*;
import java.io.*;

class LoggingException extends Exception {
private static Logger logger = Logger.getLogger("LoggingException");

public LoggingException() {
StringWriter trace = new StringWriter();
printStackTrace(new PrintWriter(trace));
logger.severe(trace.toString());
}
}

public class LoggingExceptions {
public static void main(String[] args) {
try {
throw new LoggingException();
} catch (LoggingException e) {
System.err.println("Caught " + e);
}
try {
throw new LoggingException();
} catch (LoggingException e) {
System.err.println("Caught " + e);
}
}
}
// output
// Jan 25, 2021 10:57:56 AM reading.container.LoggingException <init>
// SEVERE: reading.container.LoggingException
// at reading.container.LoggingExceptions.main(LoggingExceptions.java:20)

// Caught reading.container.LoggingException
// Jan 25, 2021 10:57:56 AM reading.container.LoggingException <init>
// SEVERE: reading.container.LoggingException
// at reading.container.LoggingExceptions.main(LoggingExceptions.java:25)

// Caught reading.container.LoggingException

下面是在 catch 中 log 异常信息的例子。自定义一个异常 MyException2,重写三种构造函数,分别是默认,带一个字符串,带字符串和数字三种形式。三种方式会分别在异常对象中多设置一个属性。

ExtraFeatures 中声明三个方法,调用三种异初始化函数,并抛出。主函数中,捕获异常并处理。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
class MyException2 extends Exception {
private int x;
public MyException2() {}
public MyException2(String msg) { super(msg); }
public MyException2(String msg, int x) {
super(msg);
this.x = x;
}
public int val() { return x; }
public String getMessage() {
return "Detail Message: " + x + " " + super.getMessage();
}
}

public class ExtraFeatures {
public static void f() throws MyException2 {
System.out.println("Throwing MyException2 from f()");
throw new MyException2();
}
public static void g() throws MyException2 {
System.out.println("Throwing MyException2 from g()");
throw new MyException2("Originated in g()");
}
public static void h() throws MyException2 {
System.out.println("Throwing MyException2 from h()");
throw new MyException2("Originated in h()", 47);
}
public static void main(String[] args) {
try {
f();
} catch(MyException2 e) {
e.printStackTrace(System.out);
}
try {
g();
} catch(MyException2 e) {
e.printStackTrace(System.out);
}
try {
h();
} catch(MyException2 e) {
e.printStackTrace(System.out);
System.out.println("e.val() = " + e.val());
}
}
}
// output
// Throwing MyException2 from f()
// reading.container.MyException2: Detail Message: 0 null
// at reading.container.ExtraFeatures.f(ExtraFeatures.java:20)
// at reading.container.ExtraFeatures.main(ExtraFeatures.java:32)
// Throwing MyException2 from g()
// reading.container.MyException2: Detail Message: 0 Originated in g()
// at reading.container.ExtraFeatures.g(ExtraFeatures.java:24)
// at reading.container.ExtraFeatures.main(ExtraFeatures.java:37)
// Throwing MyException2 from h()
// reading.container.MyException2: Detail Message: 47 Originated in h()
// at reading.container.ExtraFeatures.h(ExtraFeatures.java:28)
// at reading.container.ExtraFeatures.main(ExtraFeatures.java:42)
// e.val() = 47

exception 也是一个 Java 对象,你可以继续扩展这个类,但是值得注意的是,你的包装可能被其他人忽略,因为他们在使用的时候可能只想找一个贴切的异常并丢出去。

The exception specification

在 Java 中,你需要告知调用者你的方法可能会抛出什么异常,而且这是强制的。这种语法使用 throw 作为关键字,后面接需要 catch 的异常 void f() throws TooBig, TooSmall, DivZero { //...

如果方法声明只是简单的 void f() { //... 这表示没有异常从这个方法中抛出。 {except 表示异常继承自 RuntimeException,这个异常可以在任何地方抛出。在异常声明中,你不能作弊。如果你方法中有抛出异常,但是你没有处理的话,编译器就会监测到并给你提示你要么抛出它要么在方法签名上给出提示。通过自顶向下的约束异常声明,Java 保证了在编译期的异常检测。

有一个特别的地方是,你可以在没有对应实现的情况下抛出异常。这种处理方式可以看作是一个预先打桩,为你将来的实现做准备,而且省去了以后改应用代码的麻烦。

在编译期强制做检测的这种异常叫做 Checked Exception(受检异常)。

Catching any exception

在异常处理中,声明一个 catch 来捕获 Exception 以达到捕获几乎所有异常的基类的目的,这样做是可行的而且很常见。

1
2
3
catch(Exception e) {
System.out.println("Caught an exception");
}

他会处理几乎所有的受检异常,所以确保将它放到你的 catch 列表的末位。由于他是一个基类,所以你一般不能得到什么很特殊的信息,但是你还是可以调用那些基于 Throwable 的方法,比如

1
2
String getMessage( )  
String getLocalizedMessage( )

获取 message,或者是基于本地化的 message。

String toString( ) 返回一个简短的关于 Throwable 类的描述,如果这个类有详细信息的话,也会包含在其中。

1
2
3
void printStackTrace( )  
void printStackTrace(PrintStream)
void printStackTrace(java.io.PrintWriter)

打印 Throwable 以及对应的调用栈信息。栈信息会告诉你异常发生的点。第一种方式会将异常输出到 standard error, 第二和三种方式会输出到对应的流。

Throwable 还有很多其他的方法可以调用,比如 getClass(), 它能返回一个异常对象,getName() 返回类信息,包含路径名,getSimpleName() 只含有类名。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public class ExceptionMethods {
public static void main(String[] args) {
try {
throw new Exception("My Exception");
} catch (Exception e) {
System.out.println("Caught Exception");
System.out.println("getMessage():" + e.getMessage());
System.out.println("getLocalizedMessage():" +
e.getLocalizedMessage());
System.out.println("toString():" + e);
System.out.println("System.out.printlnStackTrace():");
e.printStackTrace(System.out);
}
}
}
// output
// Caught Exception
// getMessage():My Exception
// getLocalizedMessage():My Exception
// toString():java.lang.Exception: My Exception
// System.out.printlnStackTrace():
// java.lang.Exception: My Exception
// at reading.container.ExceptionMethods.main(ExceptionMethods.java:6)

The stack trace

printStackTrace( ) 中的信息也可以通过 getStackTrace( ) 得到,他会返回一个信息栈。下面是一个示例,可以看到,root cause 是在第一行打印的,最外层的异常点在最后打印。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
public class WhoCalled {
static void f() {
// Generate an exception to fill in the stack trace
try {
throw new Exception();
} catch (Exception e) {
for (StackTraceElement ste : e.getStackTrace())
System.out.println(ste.getMethodName());
}
}

static void g() {
f();
}

static void h() {
g();
}

public static void main(String[] args) {
f();
System.out.println("--------------------------------");
g();
System.out.println("--------------------------------");
h();
}
}

// output
// f
// main
// --------------------------------
// f
// g
// main
// --------------------------------
// f
// g
// h
// main

Rethrowing an exception

有时你在捕捉到异常之后会想要再一次 throw 它,比如之前提到的,通过 Exception 捕捉到异常的情况。这时你只需要在 handler 里面再 throw 即可

1
2
3
4
catch(Exception e) {
System.out.println("An exception was thrown");
throw e;
}

Rethrowing 会将异常交由更高的 context 处理,这个过程中,异常对象的所有信息都会被保存下来,如果你想要创建一个新的异常对象,你可以使用 fillInStackTrace( ) 方法,示例如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
public class Rethrowing {
public static void f() throws Exception {
System.out.println("originating the exception in f()");
throw new Exception("thrown from f()");
}

public static void g() throws Exception {
try {
f();
} catch (Exception e) {
System.out.println("Inside g(),e.printStackTrace()");
e.printStackTrace(System.out);
throw e;
}
}

public static void h() throws Exception {
try {
f();
} catch (Exception e) {
System.out.println("Inside h(),e.printStackTrace()");
e.printStackTrace(System.out);
throw (Exception) e.fillInStackTrace();
}
}

public static void main(String[] args) {
try {
g();
} catch (Exception e) {
System.out.println("main: printStackTrace()");
e.printStackTrace(System.out);
}
try {
h();
} catch (Exception e) {
System.out.println("main: printStackTrace()");
e.printStackTrace(System.out);
}
}
}

// originating the exception in f()
// Inside g(),e.printStackTrace()
// java.lang.Exception: thrown from f()
// at reading.container.Rethrowing.f(Rethrowing.java:6)
// at reading.container.Rethrowing.g(Rethrowing.java:11)
// at reading.container.Rethrowing.main(Rethrowing.java:31)
// main: printStackTrace()
// java.lang.Exception: thrown from f()
// at reading.container.Rethrowing.f(Rethrowing.java:6)
// at reading.container.Rethrowing.g(Rethrowing.java:11)
// at reading.container.Rethrowing.main(Rethrowing.java:31)
// /--------------------- Dash --------------------/
// originating the exception in f()
// Inside h(),e.printStackTrace()
// java.lang.Exception: thrown from f()
// at reading.container.Rethrowing.f(Rethrowing.java:6)
// at reading.container.Rethrowing.h(Rethrowing.java:21)
// at reading.container.Rethrowing.main(Rethrowing.java:38)
// main: printStackTrace()
// java.lang.Exception: thrown from f()
// at reading.container.Rethrowing.h(Rethrowing.java:25)
// at reading.container.Rethrowing.main(Rethrowing.java:38)

f() 中通过 fillInStackTrace( ) 改变了异常原点,相比于之前的调用 g() 的方法信息没有了。当然你也可以用 throw 新的 Exception 来实现和 fillInStackTrace() 同样的功能

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
class OneException extends Exception {
public OneException(String s) {
super(s);
}
}

class TwoException extends Exception {
public TwoException(String s) {
super(s);
}
}

public class RethrowNew {
public static void f() throws OneException {
System.out.println("originating the exception in f()");
throw new OneException("thrown from f()");
}

public static void main(String[] args) {
try {
try {
f();
} catch (OneException e) {
System.out.println(
"Caught in inner try, e.printStackTrace()");
e.printStackTrace(System.out);
throw new TwoException("from inner try");
}
} catch (TwoException e) {
System.out.println(
"Caught in outer try, e.printStackTrace()");
e.printStackTrace(System.out);
}
}
}

// originating the exception in f()
// Caught in inner try, e.printStackTrace()
// reading.container.OneException: thrown from f()
// at reading.container.RethrowNew.f(RethrowNew.java:18)
// at reading.container.RethrowNew.main(RethrowNew.java:24)
// Caught in outer try, e.printStackTrace()
// reading.container.TwoException: from inner try
// at reading.container.RethrowNew.main(RethrowNew.java:29)

最后的 exception handler 只知道异常来源于 inner try block 而不知道任何关于 f() 的信息。你完全不用关心异常的清理问题,他们都是基于堆创建的对象,垃圾回收机制会负责清理他们。

Exception chaining

通常来说,当你抛出自己的异常时,你都会希望这个异常带有原始异常的信息。在 Java 1.4 以前,码农们需要自己处理这个问题,但是之后的版本中,你可以通过在构造函数中传入异常类来实现这个功能。

Throwable 的子类中只有三个提供这个功能,分别是 Error(用于记录 JVM 异常),Exception 和 RuntimeException。如果其他类型的异常,你也想串联起来的话,你可以调用 initCause() 方法.

示例说明:

  • 自定义一个异常 DynamicFieldsException
  • DynamicFields 为测试类,包含一个需要处理的 field 叫做 fields,他是一个二维数组
  • fields 初始化时可以给定长度,宽度为固定值 2, 也就是 n*2 的矩阵
  • fields 的子单元值为对象,不能填充原始类型的值
  • 自定义 toString 方法可以打印矩阵值
  • setField 可以设置一行的值,如果超出容量,自动 copy + append, 设置的值不能为 null,否则报错
  • getField 返回对应行的值,如果没有抛异常
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
class DynamicFieldsException extends Exception {}

public class DynamicFields {
private Object[][] fields;

public DynamicFields(int initialSize) {
fields = new Object[initialSize][2];
for (int i = 0; i < initialSize; i++)
fields[i] = new Object[]{null, null};
}

public String toString() {
StringBuilder result = new StringBuilder();
for (Object[] obj : fields) {
result.append(obj[0]);
result.append(": ");
result.append(obj[1]);
result.append("\n");
}
return result.toString();
}

private int hasField(String id) {
for (int i = 0; i < fields.length; i++)
if (id.equals(fields[i][0]))
return i;
return -1;
}

private int
getFieldNumber(String id) throws NoSuchFieldException {
int fieldNum = hasField(id);
if (fieldNum == -1)
throw new NoSuchFieldException();
return fieldNum;
}

private int makeField(String id) {
for (int i = 0; i < fields.length; i++)
if (fields[i][0] == null) {
fields[i][0] = id;
return i;
}
// No empty fields. Add one:
Object[][] tmp = new Object[fields.length + 1][2];
for (int i = 0; i < fields.length; i++)
tmp[i] = fields[i];
for (int i = fields.length; i < tmp.length; i++)
tmp[i] = new Object[]{null, null};
fields = tmp;
// Recursive call with expanded fields:
return makeField(id);
}

public Object getField(String id) throws NoSuchFieldException {
return fields[getFieldNumber(id)][1];
}

public Object setField(String id, Object value)
throws DynamicFieldsException {
if (value == null) {
// Most exceptions don’t have a "cause" constructor.
// In these cases you must use initCause(),
// available in all Throwable subclasses.
DynamicFieldsException dfe = new DynamicFieldsException();
dfe.initCause(new NullPointerException());
throw dfe;
}
int fieldNumber = hasField(id);
if (fieldNumber == -1)
fieldNumber = makeField(id);
Object result = null;
try {
result = getField(id); // Get old value
} catch (NoSuchFieldException e) {
// Use constructor that takes "cause":
throw new RuntimeException(e);
}
fields[fieldNumber][1] = value;
return result;
}

public static void main(String[] args) {
DynamicFields df = new DynamicFields(3);
System.out.println(df);
try {
df.setField("d", "A value for d");
df.setField("number", 47);
df.setField("number2", 48);
System.out.println(df);
df.setField("d", "A new value for d");
df.setField("number3", 11);
System.out.println("df: " + df);
System.out.println("df.getField(\"d\") : " + df.getField("d"));
Object field = df.setField("d", null); // Exception
} catch (NoSuchFieldException e) {
e.printStackTrace(System.out);
} catch (DynamicFieldsException e) {
e.printStackTrace(System.out);
}
}
}

// output
// null: null
// null: null
// null: null

// d: A value for d
// number: 47
// number2: 48

// df: d: A new value for d
// number: 47
// number2: 48
// number3: 11

// df.getField("d") : A new value for d
// reading.container.DynamicFieldsException
// at reading.container.DynamicFields.setField(DynamicFields.java:68)
// at reading.container.DynamicFields.main(DynamicFields.java:98)
// Caused by: java.lang.NullPointerException
// at reading.container.DynamicFields.setField(DynamicFields.java:69)
// ... 1 more

setField() 方法中,我们我们为 DynamicFieldsException 通过调用 initCause 设置了 NPE 为 root

Standard Java exceptions

Java 的 Throwable 类代表了所有可 throw 类,有两个常用子类 Error 和 Exception。Error 表示 compile-time 和系统错误,这些是你不需要关心的。另一类是 Exception,这些是码农需要关心的。

想要对 Exception 有一个概览,最好就去看一下 JDK 文档,这可以给你找找感觉,但是当你看了之后,你会发现,这些异常,除了名字不同外,其他基本都是一样的。如果你是用第三方包,那么很大概率会遇到他们自定义的异常。所以最重要的事是了解他的定义,还有就是知道当你遇到它时你需要做什么。

异常的名字就代表了它处理的场景,异常的命名要求贴切明了。异常并不是全都定义在 java.lang 下,其他一些包,比如 util, net 和 io 也都有自己的异常类。你可以通过查看他们的包路径知道这些信息。比如所有的 I/O 异常都是继承于 java.io.IOException。

Special case: RuntimeException

下面是第一个示例

1
2
3
if (t == null) {
throw new NullPointException();
}

如果代码中每个可能有 null 引用的地方都需要做 NPE 检测,那想象就很刺激。所幸,这个检测 Java 会替你完成,所以上述的代码中的 NPE 检查是多余的。

JDK 中有一族异常处理类似的问题,Java 代码中会自动抛出,自动处理这些异常签名。他们有一个基类叫做 RuntimeException, 由它派生出来的异常都不需要在声明中特别指出来。他们也被叫做 unchecked exceptions(非受检异常)。虽然你不需要检测 RuntimeException, 但是你在写代码的过程中可能会想要抛出这个异常。

下面是一个没有捕获 RuntimeException 的例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class NeverCaught {
static void f() {
throw new RuntimeException("From f()");
}

static void g() {
f();
}

public static void main(String[] args) {
g();
}
}

// output
// Exception in thread "main" java.lang.RuntimeException: From f()
// at reading.container.NeverCaught.f(NeverCaught.java:5)
// at reading.container.NeverCaught.g(NeverCaught.java:9)
// at reading.container.NeverCaught.main(NeverCaught.java:13)

你可以看到,即使你在 f() 中 throw 了这个异常,但是你在调用它的位置也不需要用异常签名标识它。

时刻牢记,只有 运行时异常 可以这么处理, checked exception 不行,因为 Java 语法中,将运行时异常当作系统错误处理,系统错误的定义:

  1. 那些你不能预料的异常,比如 null reference
  2. 那种作为作者,你在程序中应该检查的错误,比如 ArraylndexOutOfBoundsException

Performing cleanup with finally

在你的代码中总有一些动作是你无论如何都要做的,不管是否有异常发生, 为了应对这些问题,我们在 catch 结束后引入了 finally 这个关键字。表现形式如下:

1
2
3
4
5
6
7
8
9
10
11
12
try {
// The guarded region: Dangerous activities
// that might throw A, B, or C
} catch(A a1) {
// Handler for situation A
} catch(B b1) {
// Handler for situation B
} catch(C c1) {
// Handler for situation C
} finally {
// Activities that happen every time
}

为了表明 final 是一个必定执行的分支,我们创建了一下示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
class ThreeException extends Exception {}

public class FinallyWorks {
static int count = 0;

public static void main(String[] args) {
while (true) {
try {
// Post-increment is zero first time:
if (count++ == 0)
throw new ThreeException();
System.out.println("No exception");
} catch (ThreeException e) {
System.out.println("ThreeException");
} finally {
System.out.println("In finally clause");
if (count == 2) break; // out of "while"
}
}
}
}

// output
// ThreeException
// In finally clause
// No exception
// In finally clause

从输出的 log 我们可以看到,无论是否有异常抛出,finally 里面的内容都会被执行。

这个代码段同时也提示我们,Java 是不允许我们回到异常点的,如果你将你的 try block 放到一个循环中,你可以设定条件来重复执行他。

What’s finally for?

在一个没有垃圾回收和和自动解构的语言中,finally 是很重要的,但是 Java 语言体系中已经默认给你提供了这些功能,那么 fianlly 又是用来做什么的呢?

finally 可以用于重制除内存以外的对象,比如关闭文件,或者网络链接之类的东西。

示例说明:

下面这个例子,我们想要达到的效果是无论如何要在程序结束时将开关关闭。

我们声明了两个异常 OnOffException1, OnOffException2,但是如果要将关闭的动作放到 catch 中,会出现很多重复的代码。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
class OnOffException1 extends Exception {}

class OnOffException2 extends Exception {}

public class Switch {
private boolean state = false;

public boolean read() {
return state;
}

public void on() {
state = true;
System.out.println(this);
}

public void off() {
state = false;
System.out.println(this);
}

public String toString() {
return state ? "on" : "off";
}
}

public class OnOffSwitch {
private static Switch sw = new Switch();

public static void f()
throws OnOffException1, OnOffException2 {
}

public static void main(String[] args) {
try {
sw.on();
// Code that can throw exceptions...
f();
sw.off();
} catch (OnOffException1 e) {
System.out.println("OnOffException1");
sw.off();
} catch (OnOffException2 e) {
System.out.println("OnOffException2");
sw.off();
}
}
}

// on
// off

我们可以加一个 finally 来统一处理,去除重复代码。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public class WithFinally {
static Switch sw = new Switch();

public static void main(String[] args) {
try {
sw.on();
// Code that can throw exceptions...
OnOffSwitch.f();
} catch (OnOffException1 e) {
System.out.println("OnOffException1");
} catch (OnOffException2 e) {
System.out.println("OnOffException2");
} finally {
sw.off();
}
}
}

// on
// off

现在无论异常是否抛出,switch 都会被 turn off。

下面是一个更加深入的例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
class FourException extends Exception {}

public class AlwaysFinally {
public static void main(String[] args) {
System.out.println("Entering first try block");
try {
System.out.println("Entering second try block");
try {
throw new FourException();
} finally {
System.out.println("finally in 2nd try block");
}
} catch (FourException e) {
System.out.println("Caught FourException in 1st try block");
} finally {
System.out.println("finally in 1st try block");
}
}
}

// Entering first try block
// Entering second try block
// finally in 2nd try block
// Caught FourException in 1st try block
// finally in 1st try block

即使有 break 或者 continue 关键字,finally 还是会被执行,它的出现结束了 goto 关键字的使用。

Using finally during return

由于 finally 是保证会被执行的,这就使得一段程序中有两个返回点变为可能,而且一些重要的 cleanup 必定会被执行。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
public class MultipleReturns {
public static void f(int i) {
System.out.println("Initialization that requires cleanup");
try {
System.out.println("Point 1");
if (i == 1) return;
System.out.println("Point 2");
if (i == 2) return;
System.out.println("Point 3");
if (i == 3) return;
System.out.println("End");
return;
} finally {
System.out.println("Performing cleanup");
}
}

public static void main(String[] args) {
for (int i = 1; i <= 4; i++)
f(i);
}
}

// Initialization that requires cleanup
// Point 1
// Performing cleanup
// Initialization that requires cleanup
// Point 1
// Point 2
// Performing cleanup
// Initialization that requires cleanup
// Point 1
// Point 2
// Point 3
// Performing cleanup
// Initialization that requires cleanup
// Point 1
// Point 2
// Point 3
// End
// Performing cleanup

我们可以看到,不管 return 在哪里,finally 都会被执行到。

Pitfall: the lost exception

Java 异常处理终有一个缺陷可能导致我们漏掉异常。这种情况和 finally 有关,示例如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
class VeryImportantException extends Exception {
public String toString() {
return "A very important exception!";
}
}

class HoHumException extends Exception {
public String toString() {
return "A trivial exception";
}
}


public class LostMessage {
void f() throws VeryImportantException {
throw new VeryImportantException();
}

void dispose() throws HoHumException {
throw new HoHumException();
}

public static void main(String[] args) {
try {
LostMessage lm = new LostMessage();
try {
lm.f();
} finally {
lm.dispose();
}
} catch (Exception e) {
System.out.println(e);
}
}
}
// A trivial exception

我们可以看到在上面的例子中 VerylmportantException 被吞了,只有 HoHumException 被捕获了。这种缺陷很严重,异常被完全抹去了,而且很难找到 root cause。

下面是一个更直接的例子, 如果我们在 final 中加了一个 return,那么所有的 try 中的异常都会被 skip 掉。

1
2
3
4
5
6
7
8
9
10
11
public class ExceptionSilencer {
public static void main(String[] args) {
try {
throw new RuntimeException();
} finally {
// Using ‘return’ inside the finally block
// will silence any thrown exception.
return;
}
}
}

Exception restrictions

当你 重写 一个方法的时候,你只能抛出基类方法中规定的异常,这个限制很有用,通过这样的限制,就能使重写的方法可以在原方法出现的地方生效。下面的方法展示了异常在继承体系中的限制情况

示例说明:

Inning 为基类, Storm 为接口, StormyInning 为测试类,继承 Inning 并且实现 Storm 接口。接口和基类中抛出的异常不同,并且接口和异常中有一个方法是同名的。总结规则如下

  1. 构造函数和一般方法相比,比较特别,它要求必须抛出和基类构造函数一致的异常的同时,可以新增异常
  2. 方法只在一个超类(接口或者基类)中出现,那么子类中的异常 <= 超类,甚至可以不抛出异常
  3. 如果方法在接口和基类中都出现,则一基类为准
  4. 从 main 方法中可以看出,程序会根据你的类信息来处理对应的异常。StormyInning 时处理一类,转化为 Inning 时需要处理的异常就改变了。

子类不能抛出基类没有定义的异常的理由:比如在框架层级的代码中,你写了一个 flow, 将所有的一族类统一到一个流程中,如果允许子类可以抛出基类没有的异常,那么就有可能 flow 中没有子类的异常处理逻辑。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87

class BaseballException extends Exception {}

class Foul extends BaseballException {}

class Strike extends BaseballException {}

abstract class Inning {
public Inning() throws BaseballException {}

public void event() throws BaseballException {
// Doesn’t actually have to throw anything
}

public abstract void atBat() throws Strike, Foul;

public void walk() {} // Throws no checked exceptions
}

class StormException extends Exception {}

class RainedOut extends StormException {}

class PopFoul extends Foul {}

interface Storm {
public void event() throws RainedOut;

public void rainHard() throws RainedOut;
}

public class StormyInning extends Inning implements Storm {
// OK to add new exceptions for constructors, but you
// must deal with the base constructor exceptions:
public StormyInning()
throws RainedOut, BaseballException {
}

public StormyInning(String s)
throws Foul, BaseballException {
}

// Regular methods must conform to base class:
// !public void walk() throws PopFoul {} //Compile error
// Interface CANNOT add exceptions to existing
// methods from the base class:
// !public void event() throws RainedOut {}
// If the method doesn’t already exist in the
// base class, the exception is OK:
public void rainHard() throws RainedOut {}

// You can choose to not throw any exceptions,
// even if the base version does:
public void event() {}

// Overridden methods can throw inherited exceptions:
public void atBat() throws PopFoul {}

public static void main(String[] args) {
try {
StormyInning si = new StormyInning();
si.atBat();
} catch (PopFoul e) {
System.out.println("Pop foul");
} catch (RainedOut e) {
System.out.println("Rained out");
} catch (BaseballException e) {
System.out.println("Generic baseball exception");
}
// Strike not thrown in derived version.
try {
// What happens if you upcast?
Inning i = new StormyInning();
i.atBat();
// You must catch the exceptions from the
// base-class version of the method:
} catch (Strike e) {
System.out.println("Strike");
} catch (Foul e) {
System.out.println("Foul");
} catch (RainedOut e) {
System.out.println("Rained out");
} catch (BaseballException e) {
System.out.println("Generic baseball exception");
}
}
}

虽然异常签名会在继承时对你做一些语法上的要求,但是它并不是方法的一部分。方法签名只和 方法名,方法参数有关。所以你在重写方法的时候是不能基于异常类型的。

Constructors

时常问自己一句,“如果有异常发生,是不是所有的东西都会被清理干净” 是很重要的。大部分情况下你是安全的,但是在构造函数中,这就是个问题了。构造函数中,对象一开始是安全的,但是随着程序的进行,比如打开了一个文件。但是只有在完成读写后他才会关闭这个流。如果在构造函数中抛出异常,那么这个清理工作可能不能顺利完成。这就意味着,在处理构造函数的时候,你必须格外小心。

你可能会想,我们可以用 finally 来处理这种情况,但是情况可能并没有这么简单,因为 finally 是每次都会执行的。如果构造函数中途挂了,一些对象可能没有被正确的创建出来,那么对应的 finally 执行也可能出问题。

下面的例子中,我们用一个 I/O 的例子做示范:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
public class InputFile {
private BufferedReader in;

public InputFile(String fname) throws Exception {
try {
in = new BufferedReader(new FileReader(fname));
// Other code that might throw exceptions
} catch (FileNotFoundException e) {
System.out.println("Could not open " + fname);
// Wasn’t open, so don’t close it
throw e;
} catch (Exception e) {
// All other exceptions must close it
try {
in.close();
} catch (IOException e2) {
System.out.println("in.close() unsuccessful");
}
throw e; // Rethrow
} finally {
// Don’t close it here!!!
}
}

public String getLine() {
String s;
try {
s = in.readLine();
} catch (IOException e) {
throw new RuntimeException("readLine() failed");
}
return s;
}

public void dispose() {
try {
in.close();
System.out.println("dispose() successful");
} catch (IOException e2) {
throw new RuntimeException("in.close() failed");
}
}
}

构造函数里,InputFile 用 String 表示文件名,并在一个 try block 创建一个 FileReader。InputFile 并没有什么特别的地方,它最大的作用是将 FileReader 和 BufferedReader 结合在了一起。

如果 FileReader 的构造失败了,就会抛出 FileNotFoundException。这种情况下,IO 流并没有被正常的开启,我们在异常处理时不需要关闭这个流。除这个异常外的其他异常,则要求我们关闭文件流。因为如果是其他的异常,则当时文件流已经被打开了,我们就需要在处理异常后关闭文件流。close() 方法也会抛出异常,对应的,我们在 catch 中也添加一个 try-catch 处理。处理完这些异常后我们再将这个异常抛出去。

上面的示例中,由于 finally 是每次比执行的,所以不是一个处理 close() 的好地方。

getLine() 会返回下一行内容。底层是调用了 readLine() 方法,它会抛出一个异常,但是这个异常被捕获并转化为 RuntimeException了,所以方法签名中不需要处理该异常。

当 InputFile 对象使用完毕后,调用 dispose() 方法释放资源。你可能想要将这个动作放到 finalize() 方法中,但是 Java 语言体系是不支持这种操作的,算是 Java 的缺陷之一。

像这种例子,最安全的做法应该是在原来的 try block 中再嵌套一个 try block。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public class Cleanup {
public static void main(String[] args) {
try {
InputFile in = new InputFile("Cleanup.java");
try {
String s;
int i = 1;
while ((s = in.getLine()) != null)
; // Perform line-by-line processing here...
} catch (Exception e) {
System.out.println("Caught Exception in main");
e.printStackTrace(System.out);
} finally {
in.dispose();
}
} catch (Exception e) {
System.out.println("InputFile construction failed");
}
}
}
// dispose() successful

上例中,我们有通过两个 try block 进行嵌套,一个处理文件流的构造,一个处理文件流读写。构造方法失败无需调用 dispose(),读写失败则需要调用读写。

这种通用的 cleanup 即使是那些不会抛异常的构造函数,也可以适用。基本规则是:当你创建了需要 cleanup 的对象,你就可以开始适用 try-finally 了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
class NeedsCleanup { // Construction can’t fail
private static long counter = 1;
private final long id = counter++;

public void dispose() {
System.out.println("NeedsCleanup " + id + " disposed");
}
}

class ConstructionException extends Exception {}

class NeedsCleanup2 extends NeedsCleanup {
// Construction can fail:
public NeedsCleanup2() throws ConstructionException {
}
}

public class CleanupIdiom {
public static void main(String[] args) {
// Section 1:
NeedsCleanup nc1 = new NeedsCleanup();
try {
// ...
} finally {
nc1.dispose();
}

// Section 2:
// If construction cannot fail you can group objects:
NeedsCleanup nc2 = new NeedsCleanup();
NeedsCleanup nc3 = new NeedsCleanup();
try {
// ...
} finally {
nc3.dispose(); // Reverse order of construction
nc2.dispose();
}

// Section 3:
// If construction can fail you must guard each one:
try {
NeedsCleanup2 nc4 = new NeedsCleanup2();
try {
NeedsCleanup2 nc5 = new NeedsCleanup2();
try {
// ...
} finally {
nc5.dispose();
}
} catch (ConstructionException e) { // nc5 constructor
System.out.println(e);
} finally {
nc4.dispose();
}
} catch (ConstructionException e) { // nc4 constructor
System.out.println(e);
}
}
}
// NeedsCleanup 1 disposed
// NeedsCleanup 3 disposed
// NeedsCleanup 2 disposed
// NeedsCleanup 5 disposed
// NeedsCleanup 4 disposed

在 main() 函数中, section 1 的内容很直截了当,如果构造失败,就不需要 try-finally 了。

section 2 中,如果构造成功,我们可以在一个 try-finally 中同时关闭两个对象。

section 3 中,由于构造函数本身会跑出异常,所以每执行一个对象的初始化,你就需要有一个 try-finally 处理它,对应的代码会变得混乱。在这种情况下,强烈建议你将初始化的代码段也纳入 try 中,虽然有点容于,但是更好维护。

如果 dispose() 也会抛出异常,你需要额外的 try 来处理它,总之你必须处理所有可能出现的情况。

Exception matching

当抛出异常时,异常处理系统会查找最近的处理器。如果找到一个匹配的,那么就视作异常已经被处理,不会找下一个了。

搜索异常是并不会精确匹配,子类异常可以匹配到基类异常处理器。

在下面的例子中,第一个 try block 中 Sneeze 异常被第一个 catch block 捕获,这个合理。但是当我们将第一个 catch 移除时,异常也可以被 Annoyance 这个基类异常捕获。

如果你将第一个示例中的 Annoyance 提前,会有编译错误指出 Sneeze 已经被捕获,不需要再处理了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
class Annoyance extends Exception {}

class Sneeze extends Annoyance {}

public class Human {
public static void main(String[] args) {
// Catch the exact type:
try {
throw new Sneeze();
} catch (Sneeze s) {
System.out.println("Caught Sneeze");
} catch (Annoyance a) {
System.out.println("Caught Annoyance");
}
// Catch the base type:
try {
throw new Sneeze();
} catch (Annoyance a) {
System.out.println("Caught Annoyance");
}
}
}
// output
// Caught Sneeze
// Caught Annoyance

Alternative approaches

异常处理系统提供了一个在程序异常时的分支。异常状态表示当前程序不能被处理,异常系统开发的初衷是为了给程序员处理异常情况提供便利性。

异常处理的准则之一:不要捕获那些你不知道怎么处理的异常。其实,异常处理的主要目标之一是将异常处理代码从当前节点移除。这样你就可以将你的主要逻辑集中到一个地方,而在不远处的 catch 中集中处理异常代码。这样代码更容易理解和维护。

一个 handler 可以处理多种异常,减少了处理代码的量

Checked exception 会强制你写 catch block 这个有违于 ‘harmful if swallowed’ 原则。

1
2
3
try {
// ... to do something useful
} catch(ObligatoryException e) {} // Gulp!

码农们仅仅做了捕获,并不处理。但是编译器视这种做法合理,所以除非你重新回顾这段代码,不然这个异常就丢失了。当异常发生时,它被吞了。这是最简单但也可能是最糟糕的一种处理方式了。

第二版中,做了一些改进,我们在处理异常时打印了对应的 log。但是我们在那个时间点还是不知道应该怎么处理它。这个章节我们将提供几个处理异常时的可选项。

History

各种语言的异常发展历史,pass

Perspectives

各种语言的异常发展历史,pass

Passing exceptions to the console

在简单的代码段中,最简单的异常处理可能就是将异常抛出不处理。比如我们要打开/关闭一个文件,我们就会需要处理一些 IO Exception。

1
2
3
4
5
6
7
8
9
10
public class MainException {
// Pass all exceptions to the console:
public static void main(String[] args) throws Exception {
// Open the file:
FileInputStream file = new FileInputStream("MainException.java");
// Use the file ...
// Close the file:
file.close();
}
}

main() 也是一个可以携带异常签名的方法,上面的例子中,它带有一个异常 Exception,是所有受检异常的基类。通过把它在方法签名中抛出我们就不用在代码段中写 try-catch 了。

Converting checked to unchecked exceptions

从 main() 中抛出异常很方面,但是不实用。很多时候,你在调用其他方法的时候,你会想,我不知道怎么处理该异常,但是我又不想只是打印一条信息。这时我们可以简单的在这个异常外面套一个壳变成 Runtimexception。

1
2
3
4
5
try {
// ... to do something useful
} catch(IDontKnowWhatToDoWithThisCheckedException e) {
throw new RuntimeException(e);
}

这中处理方式看上去很美好,他可以让你从 checked exception 中解放出来,你没有吞掉它。并且你通过异常链串起来,起初的异常也没有丢失。

虽然不用再写 try-catch 了,但是你还是可以通过 getCause() 处理。

下例中,WrapCheckedException.throwRuntimeException() 可以抛出不同的异常。他们被包裹在 RuntimeException 异常中的 cause 中。TurnOffChecking 中你在调用 throwRuntimeException 时可以不处理 try。

但是如果你想要处理,你也可以在方法调用外面包裹 try 并通过 getCause() 得到原始异常并处理。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
class WrapCheckedException {
void throwRuntimeException(int type) {
try {
switch (type) {
case 0:
throw new FileNotFoundException();
case 1:
throw new IOException();
case 2:
throw new RuntimeException("Where am I?");
default:
return;
}
} catch (Exception e) { // Adapt to unchecked:
throw new RuntimeException(e);
}
}
}

class SomeOtherException extends Exception {}

public class TurnOffChecking {
public static void main(String[] args) {
WrapCheckedException wce = new WrapCheckedException();
// You can call throwRuntimeException() without a try
// block, and let RuntimeExceptions leave the method:
wce.throwRuntimeException(3);
// Or you can choose to catch exceptions:
for (int i = 0; i < 4; i++)
try {
if (i < 3)
wce.throwRuntimeException(i);
else
throw new SomeOtherException();
} catch (SomeOtherException e) {
System.out.println("SomeOtherException: " + e);
} catch (RuntimeException re) {
try {
throw re.getCause();
} catch (FileNotFoundException e) {
System.out.println("FileNotFoundException: " + e);
} catch (IOException e) {
System.out.println("IOException: " + e);
} catch (Throwable e) {
System.out.println("Throwable: " + e);
}
}
}
}
// FileNotFoundException: java.io.FileNotFoundException
// IOException: java.io.IOException
// Throwable: java.lang.RuntimeException: Where am I?
// SomeOtherException: reading.container.SomeOtherException

当然你也可以包装一个 RuntimeException 的子类,并用它包装你捕获的受检异常。

Exception guidelines

  1. 在恰当的地方处理异常(Avoid catching exceptions unless you know what to do with them.)
  2. Fix the problem and call the method that caused the exception again.
  3. 修复问题,不要用 retry
  4. Calculate some alternative result instead of what the method was supposed to produce.
  5. Do whatever you can in the current context and rethrow the same exception to a higher context.
  6. Do whatever you can in the current context and throw a different exception to a higher context.
  7. Terminate the program.
  8. Simplify. (If your exception scheme makes things more complicated, then it is painful and annoying to use.)
  9. Make your library and program safer. (This is a short-term investment for debugging, and a long-term investment for application robustness.)

Summary

Exception 是 Java 的一部分,如果你不了解他,那么你能做的事情就非常有限了。

异常处理的一大好处是,你可以将你的业务逻辑和异常处理代码分开。已经异常处理涵盖两部分,报告异常和修复程序,但是从遗忘的经验中来看,修复这个功能貌似都是难以实现的,或者压根不可能实现。

不管怎么说,我始终相信,报告异常才是异常处理的主要职责。通过异常机制,你可以将更多的精力集中到更有趣,有挑战的部分。

前述

这个章节将深入讲解 Arrays 的使用

Why arrays are special

array 持有对象的特点:高效,可指定类型,可以存储原始类型的数据

  • 数组是最高效的存储结构,代价是容量固定,不可变
  • ArrayList 可以自动扩容,但是每次扩容都需要重新拷贝引用
  • 泛型没出来前,Array 有类型检查的优势
  • 数组可以装载原始数据类型,容器则需要通过自动开箱,装箱实现

以下示例对比数组和容器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;


class BerylliumSphere {
private static long counter;
private final long id = counter++;

public String toString() { return "Sphere " + id; }
}

public class ContainerComparison {
public static void main(String[] args) {
BerylliumSphere[] spheres = new BerylliumSphere[10];
for (int i = 0; i < 5; i++)
spheres[i] = new BerylliumSphere();
System.out.println(Arrays.toString(spheres));
System.out.println(spheres[4]);

List<BerylliumSphere> sphereList = new ArrayList<>();
for (int i = 0; i < 5; i++)
sphereList.add(new BerylliumSphere());
System.out.println(sphereList);
System.out.println(sphereList.get(4));

int[] integers = {0, 1, 2, 3, 4, 5};
System.out.println(Arrays.toString(integers));
System.out.println(integers[4]);

List<Integer> intList = new ArrayList<>(Arrays.asList(0, 1, 2, 3, 4, 5));
intList.add(97);
System.out.println(intList);
System.out.println(intList.get(4));
}
}

两种方式都会做类型检测,唯一的不同体现在,使用数组时我们通过 [ ] 访问元素,使用容器时我们通过 add( )get( ) 来访问。两者很类似是有意为之,为了减少两者之间 migration 的effort。

容器支持的功能更多,现在使用数组的唯一理由就是。但是当你需要应付一些复杂的情况时,数组的这种严格限制就会制约你,你可能需要用容器来代替它。

Arrays are first-class objects

抛开数组的类型不说,数组修饰符是指向对内对象的引用。它存储了其他对象的引用,你可以通过数组初始化语句隐式创建它,也可以通过 new 的方式显示的创建。整个数组对象基本上只提供一个只读属性(length)给你使用,你还可以通过 [ ] 语法访问数组元素。

下面的例子展示了初始化数组的各种方式,和各种赋值方法。存储原始类型和对象类型的数据基本上一致的,唯一的不同是,如果存储的是对象,那么数组持有的是对象的引用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
import java.util.Arrays;

class BerylliumSphere {
private static long counter;
private final long id = counter++;

public String toString() {
return "Sphere " + id;
}
}

public class ArrayOptions {
public static void main(String[] args) {
// Arrays of objects:
BerylliumSphere[] a; // Local uninitialized variable
BerylliumSphere[] b = new BerylliumSphere[5];
// The references inside the array are
// automatically initialized to null:
System.out.println("b: " + Arrays.toString(b));
BerylliumSphere[] c = new BerylliumSphere[4];
for (int i = 0; i < c.length; i++)
if (c[i] == null) // Can test for null reference
c[i] = new BerylliumSphere();
// Aggregate initialization:
BerylliumSphere[] d = {new BerylliumSphere(),
new BerylliumSphere(), new BerylliumSphere()
};
// Dynamic aggregate initialization:
a = new BerylliumSphere[]{
new BerylliumSphere(), new BerylliumSphere(),
};
// (Trailing comma is optional in both cases)
System.out.println("a.length = " + a.length);
System.out.println("b.length = " + b.length);
System.out.println("c.length = " + c.length);
System.out.println("d.length = " + d.length);
a = d;
System.out.println("a.length = " + a.length);
// Arrays of primitives:
int[] e; // Null reference
int[] f = new int[5];
// The primitives inside the array are
// automatically initialized to zero:
System.out.println("f: " + Arrays.toString(f));
int[] g = new int[4];
for (int i = 0; i < g.length; i++)
g[i] = i * i;
int[] h = {11, 47, 93};
// Compile error: variable e not initialized:
//!System.out.println("e.length = " + e.length);
System.out.println("f.length = " + f.length);
System.out.println("g.length = " + g.length);
System.out.println("h.length = " + h.length);
e = h;
System.out.println("e.length = " + e.length);
e = new int[]{1, 2};
System.out.println("e.length = " + e.length);
}
}

// output
// b: [null, null, null, null, null]
// a.length = 2
// b.length = 5
// c.length = 4
// d.length = 3
// a.length = 3
// f: [0, 0, 0, 0, 0]
// f.length = 5
// g.length = 4
// h.length = 3
// e.length = 3
// e.length = 2
  • 数组 a 未初始化,编译器在你赋值之前会阻止你做任何操作

最近打算新加一个命令到项目,突然发现项目启动不了了,查了一下是 setup.py 和 toml 文件的兼容性有问题,找到了解决方案,顺便结合找到的资料,将使用 poetry 和 click 自定义命令重新记录一下。

版本信息

Python: 3.7.3
pip: 19.3.1
OS: posix

创建工程

新建项目 poetry new --name greet --src clickgreet,如过想名字保持一致,那直接 poetry new greet 即可。命令执行完后会在当前目录下生成项目,结构如下

1
2
3
4
5
6
7
8
9
clickgreet
├── README.rst
├── pyproject.toml
├── src
│ └── greet
│ └── __init__.py
└── tests
├── __init__.py
└── test_greet.py

安装依赖包 poetry add click, 首次运行 add 命令时,poetry 会帮你创建一个虚拟环境,并将包安装进去。安装完后你可以在项目的 toml 文件中看到 tool.poetry.dependencies 下有了 click 的依赖

在 src/greet 文件夹下新建 greet.py 添加逻辑代码, 代码实现如下功能:接受两个参数 name, count 后在终端输出对应次数的名字

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import click

@click.command()
@click.option('--count', default=1, help='Number of greetings.')
@click.option('--name', prompt='Your name',
help='The person to greet.')
def greet(count, name):
"""Simple program that greets NAME for a total of COUNT times."""
for x in range(count):
click.echo('Hello %s!' % name)


if __name__ == '__main__':
greet()

在 toml 中添加程序入口

1
2
[tool.poetry.scripts]
greet = "greet.greet:greet"

终端输入 poetry install 将代码安装到虚拟环境,之后输入 poetry run greet 试运行脚本,可以看到终端给出提示

1
2
3
mypc ~/tmp/clickgreet > poetry run greet
Your name: jack
Hello jack!

至此,主题部分结束,下面开始编写测试部分,在目录的 test_greet.py 中添加测试代码如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
from click.testing import CliRunner

from greet.greet import greet

from greet import __version__


def test_version():
assert __version__ == '0.1.0'

def test_greet_cli():
runner = CliRunner()
result = runner.invoke(greet, ['--name', 'jack'])
assert result.exit_code == 0
assert "Hello jack!" in result.output

终端输人 poetry run pytest 运行测试用例, 至此教程主体结束。

PS: 可以在 toml 中添加配置使用 douban 镜像加速下载

1
2
3
[[tool.poetry.source]]
name = "douban"
url = "https://pypi.doubanio.com/simple/"

Debug Click Command

测试代码,接收 count, name 参数并在终端输出

1
2
3
4
5
6
7
8
9
10
11
12
13
import click

@click.command()
@click.option('--count', default=1, help='Number of greetings.')
@click.option('--name', prompt='Your name',
help='The person to greet.')
def hello(count, name):
"""Simple program that greets NAME for a total of COUNT times."""
for x in range(count):
click.echo('Hello %s!' % name)

if __name__ == '__main__':
hello()

配置 launch.json 运行文件

1
2
3
4
5
6
7
8
9
10
{
"name": "click",
"type": "python",
"request": "launch",
"program": "${file}",
"console": "integratedTerminal",
"args": [
"--count", "3", "--name", "jaaack"
]
}

点击菜单栏的 debug 按钮,选择配置的 ‘click’ run config,点击这个配置左边的运行按钮,直接运行即可。需要注意的点:

  1. 别点右上角那个,那个是直接运行当前文件的,不会接收配置的参数!!
  2. 当断点生效时,VSCode 还提供了一个 DEBUG CONSOLE 来给你操作运行时的变量,真是太酷了
  3. 如果你想要输入多行,使用 Shift + Enter 实现换行

如果要调试带有 argumnet 注解的代码,比如

1
2
3
4
5
6
7
8
9
10
11
12
13
@click.command()
@click.argument('input', type=click.File('rb'))
@click.argument('output', type=click.File('wb'))
def inout(input, output):
"""Copy contents of INPUT to OUTPUT."""
while True:
chunk = input.read(1024)
if not chunk:
break
output.write(chunk)

if __name__ == '__main__':
inout(["input_path", "output_path"])

只需要将 argument 直接写在最后的函数入口中就行了,这里有一个设定不是很理解,在最后一行,按理说我设置的参数列表应该是 "arg1", "arg2" 才对,但是执行的时候会出问题,设置成 list type 的就没问题。。。

集成 setup.py

以上的命令行运行时有一个限制,它必须在对应的文件夹下才能工作,pip 是支持将脚本安装到本地的。如何操作?步骤如下:

poetry 是没有 setup.py 文件的,运行 poetry add dephell 安装 dephell 来自动生成 setup.py 文件

toml 文件中添加生成 setup.py 的配置

1
2
3
[tool.dephell.main]
from = {format = "poetry", path = "pyproject.toml"}
to = {format = "setuppy", path = "setup.py"}

同时你还要修改 [build-system] 配置,在 requires 中添加 setuptools 的依赖 requires = ["setuptools", "poetry>=0.12"],这是个 pip 的 bug 但是到 2020-1 为止还没有修复

运行 dephell deps convert 生成 setup.py 然后运行 pip install -E . 安装到本地。cd 到其他目录直接在终端输入 greet 测试通过,脚本正常工作,不需要什么 hack 的代码,棒棒哒 ╮( ̄▽ ̄””)╭

资料白嫖