0%

简单记录下常用的调试工具,以及常用的方法。

gdb

常用命令用法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
gdb <program> <corefile>  #调试core dump file

# 下面是交互命令

r/run 执行
c/continue 继续执行
n/next 单步执行
until 循环直到条件满足,或者跟一个行号
p/pvector/pstack... 各种打印,尤其可以打印一些STL容器
call 调用函数
b n 在第n行设置断点
l/list 打印源码
bt 打印栈
up/down 在栈上向上/下层移动
frame 直接到某层栈
watch 设置监视点

这些命令大概可以覆盖90%以上的场景。

其他辅助工具

lsof

如果直接加目录文件名,就是查看读写目录的进程情况。

常用参数

1
2
3
-p <pid> 列出进程文件读写情况
-c <name> 列出name开头的进程读写文件的情况
-i [46][protocol][@hostname|hostaddr][:service|port] 列出符合条件的网络连接

ps

一般只用ps -efx

strace

一般只用strace -Ttt定位程序耗时

gcore

gcore <pid> 对正在执行的进程生成core文件。

pstack

pstack <pid> 查看正在执行进程的栈。

ipcs

直接使用即可。

iostat

watch -n 1 iostat 或者 iostat 1 每秒统计一次IO信息。

vmstat

直接使用即可。

valgrind

这个工具非常复杂。入门可以看下边这2个:

  • https://stackoverflow.com/questions/5134891/how-do-i-use-valgrind-to-find-memory-leaks。
  • https://web.stanford.edu/class/archive/cs/cs107/cs107.1212/resources/valgrind.html

手册:https://valgrind.org/docs/manual/manual.html

htop

升级版的top,可视化做的好。尤其可以尝试tree模式(按F5),显示进程树关系。

iotop

类top工具,直接打开用就可以。但是感觉不太好用。。。

很长时间没有写博客了。工作三年期间学到不少东西,作为一个新人来说这可能是都会经历的。但写在博客上,能为他人带来多大的启发呢?毕竟不是学生了,如果还是写xxx教程,记录一些感想(年轻人都有的那种),无非是增加了整个互联网上一些无用的信息罢了:)

前段时间一直在思考“博客写什么”这个问题。甚至上个月把原来的博客删掉了,但是今天又把博客恢复了过来。某种意义上,这是一种完美主义者的一次“发病”。但是这种思考是值得的,这个问题目前有了解答,至于对不对就要先用实践检验下了。

写什么

想了下没什么可写的原因,大概有以下几个: 1. 工作中见到的牛逼的东西多了,以至于觉得自己想写的东西没那么高端。 2. 有些能写的东西是公司的项目,不一定适合发在博客这种外部平台上。 3. 个人学习和总结,有更好的方式(思维导图,云笔记,甚至写到本上),写博客性价比不太高。 4. 很多东西写下来只对自己有用,别人懒得看。

怎么改变

  1. 自己觉得最牛逼的项目,如果是开源的,就争取主动参加进去。另外有意识的每次拔高一下写的内容。
  2. 写点自己学的新东西和踩的坑,多折腾就有更多的内容写。
  3. 不要指望写博客提高自己,而是要用博客提高影响力和督促自己进步。
  4. 写点技术外的东西,能让大家开心下也挺好。

实施计划

  1. 先找找看看有哪些牛逼项目。
  2. 把前段时间学的新东西拿出来写写。
  3. 在微信朋友圈和qq空间里打一波广告。
  4. 写一些非技术文章,做个有趣的人。

注:如果赶时间可以直接跳到下一小结阅读。

今天一个朋友问我一个问题,由此引发了我对C++对象初始化方式的深入研究。他问我这段代码有什么问题没有?

tm *t = new tm;

t->tm_year = 2016;
t->tm_mon = 5;
t->tm_min = 9;
t->tm_hour = 9;
t->tm_sec = 30;
cout << mktime(t)<<endl;

我第一反应是time_t格式不能直接cout,直接cout得不到格式化的时间。当然这个不是重点,他告诉我每次结果都是随机的!我这才反应过来是new的问题。因为new在堆上分配的空间是没有初始化的。

然后我打算告诉他在main函数外的全局部分定义一个变量,但是在我消息发送出去前,他又告诉我他把代码的第一行修改成了下面这个形式,然后就初始化成固定值了:

tm t1;

他问我是不是这样做就总是固定值呢?我认为这个对象是栈上的,印象里这个是没有初始化的。所以回答只是编译器的问题。然后我提供了一个使用指针的的方法:

tm *t = new tm();

之后他问了一个关键的问题:“如果不加括号是不是一定就不会去自动的调用相关的初始化部分?”

我开始回答的是:如果是个类的话,会调用。基本类型不会。

但是后来想想不太对,因为我印象中确实有的在函数里定义的临时类是没有经过初始化的。但当时在外边没有电脑。回去后在 Windows 环境下用 gcc(codeblocks) 和 VS2015 都试了一下,然后查了cppreference,大概明白是怎么回事了。

首先经验证,new的时候加括号可以得到想要的结果: pic1 pic2

但是,直接在函数内部定义结构体变量,gcc和vs得到了不同的结果: pic3

这说明结构体内部的成员没有初始化。

为了搞清所有的情况,查了下cppreference,并按照C++11的标准写了下面的内容:

下面介绍C++对象两种重要的初始化方式:默认初始化和值初始化。主要参考cppreference写成的。并且删除了其中过时的部分(主要都是C++03的特性)。

默认初始化

默认初始化的语法:

T object;
new T;

在以下情形,会使用默认初始化:

  1. 当变量具有自动,静态,线程生存周期,并且没有初始化时。
  2. 使用new动态分配的对象没有初始化
  3. 在一个类中,当某个基类或者非静态成员在构造函数初始化列表中没有出现,并且该构造函数被调用的时候。

默认初始化的效果:

  • 如果T是一个非POD (Plain Old Data)类型,就会在列表为空的构造函数,以及重载的构造函数中选择一个(注:可能有构造函数参数列表非空且所有的参数都有默认值)。被选中的这个构造函数会初始化变量。
  • 如果T是数组类型,每个元素都被默认初始化。
  • 否则,什么也不做:自动生存周期的对象(包括他们的子对象)值都是未定义的。

#值初始化

值初始化的语法:

T();    (1) 
new T ();   (2) 
Class::Class(...) : member() { ... }    (3) 
T object {};    (4) (since C++11)
T{};    (5) (since C++11)
new T {};   (6) (since C++11)
Class::Class(...) : member{} { ... }    (7) (since C++11)

在以下情形使用值初始化:

  • 1,5) 当使用内部为空的圆括号或花括号创建一个无名的临时对象时。
  • 2,6) 当用new创建一个动态对象,且后面使用了内部为空的圆括号或花括号时。
  • 3,7) 在某个类中,当某个非静态成员或基类使用了空的花括号或圆括号初始化时。

初始化的效果:

  1. 如果类型T是有至少1个用户定义的构造函数,那么调用默认构造函数。
  2. 如果T没有默认构造函数,或者有用户定义的构造函数,或者默认构造函数是删除的,那么使用默认初始化。
  3. 如果T不是Union且无用户的构造函数,那么所有的非静态成员和基类采用值初始化。
  4. 如果T是个有默认构造函数的类,且这个构造函数不是用户定义的也不是删除的,那么该对象被零初始化。然后,如果它有一个non-trivial的默认构造函数,它将被默认初始化。
  5. 如果T是数组,每个元素都被值初始化。
  6. 否则都执行零初始化

关于第3条很难理解。首先,trivial的定义参考这个链接。明确了这个定义,看看下面的代码:

#include <bits/stdc++.h>
#include <type_traits>

using namespace std;

class A{//non-trivial
    int a;
    int b;
    char c;
public:
    A(){
        cout << "it works!"<<endl;
    };
};

class B{//trivial
    int a;
    int b;
    char c;
};

int main()
{

    A *a = new A();
    B *b = new B();

    cout<<is_pod<A>::value<<endl;

    return 0;
}

运行结果表明a指向的对象的成员是随机的,并且输出了"it works",而b指向的成员的对象全部是0。

A的构造函数尽管是空的,但是它是用户定义的,因此是non-trivial,进行默认初始化。参考默认初始化条的效果,由于A不是POD类型,那么就会执行A的默认构造函数(我们定义的那个)。由于我们定义的构造函数没有初始化任何数据成员,因此A成员的值是随机的,并且输出了信息"it works"。

B的构造函数有编译器合成,是trivial的,它符合第三条,执行零初始化。

总结

根据以上内容,我总结了2点帮助记忆:

第一点

第一点是关于两种初始化发生的条件的:

只要使用了括号(圆括号或花括号),就是值初始化。可以简单理解为括号提醒编译器你想要用某个值赋给对象。

没有使用括号,就是默认初始化。可以简单理解成,你不加任何东西,编译器就会使用默认的行为。

第二点

第二点是关于两种初始化的效果的:

默认初始化:总是试图使用默认构造函数初始化对象。但是它对于POD类型则不这么做。比如:C基本类型,聚合类型,POD类型的数组。C语言的struct以及基本类型如果不初始化也是随机的值,和这个POD类型在C++类似。我们可以简单理解为:总使用默认构造函数,同时兼容C。

值初始化:我们观察几个条件可以发现,大部分情况下(效果2里包含默认构造器被删除的情况),只要用户指定了默认构造函数,那么就执行默认初始化。并且,如果编译器合成了构造函数,执行零初始化。另外,对于数组和其它情况是值初始化。而且排除掉数组后,基本类型的值初始化都是零初始化。综上,可以简单理解为:有用户定义构造函数,就执行用户定义的构造函数,否则都使用零初始化。

暑假实习回来了。学到不少东西,有不少感想,除了技术上的,更多是其它方面的。

最大的一点感触就是要会表现自己,有自信,要做好宣传和推广的工作。很多时候默默工作,成绩是很难被别人看到的,必须要会总结,会汇报,会通过各种渠道宣传。当然这个前提是真要做出好的工作来。很多时候埋头技术,技术做的很厉害,但是没什么存在感,这是对个人发展很不利的。对一个团队来说也是如此,要选择好的方法把自己的技术积累展示出来。

展示自己的方法有很多。比如,分享自己学到的东西,或者整理自己的工作,再或者写一点有意思的个人项目。这些对个人也有好处,毕竟总结所学也是一个很重要的技能。说实话在学校里除了考试复习和偶尔写点博客之外,还真少有系统总结个人所学的时候。另外就是掌握做ppt的正确姿势。ppt不要花哨,但要做出震撼力,把核心的东西展示出来,用重点内容打动听众。参加了几次技术分享以后真心觉得这很重要。

第二,多问业务上的问题。作为一个新人,就算技术基础还可以,业务上的经验也是0。之所以要掌握这个技能是因为:很多时候技术资料是全的,但是组里和部门解决怎样的问题确实没有资料和文档可查。而且,业务和架构也是很长时间积累来的,一时半会儿肯定摸不清,与其自己满世界找资料,不如请教专门负责这些部分的人。这里问问题是有方法的:

  • 1.搞清楚组里每个人都在做什么,负责什么内容。这个是一个大的前提。我这里做得一开始不好,到来了很长一段时间才意识到这个严重性。幸运的是意识到问题的时候组里的人我都认识了,而且也零散的请教了很多问题,对他们的工作有了基本了解。如果时间倒流的话,第一周我就要把这个全弄清楚。

  • 2.找个组里的老员工,听他扯扯组里项目的发展史。我进入组里后,和几个新人听前辈讲了组里业务的发展,技术的改换过程,感觉很受益。

  • 3.如果有运维的工作,争取做一下。通过维护系统,可以对整个系统有个更直观的理解。

第三,要制定自我提高的计划,时刻保持学习的状态。毕竟工作和学校学习有很大不同。工作以后学习时间减少,而且要花更多的时间在管理自己的生活上。以前那种想学就去学的时间和机会都没有了,学习必须挤出时间来。这个方面目前还没有很好的方法,将来还是要多请教其他人。

最后,坚持锻炼。我体能并不怎么好,也不是很壮。但是来实习以后组里人觉得我看上去体力很好。。。这只能说明工作后大部分人体能都退化了。坚持锻炼其实很难做到,因为人总是懒。我很佩服我的导师,在我来实习这段时间他也开始了个人的锻炼计划。两个月最后减掉十几斤。其实他就是每天晚上都去跑步,然后不吃晚饭。锻炼这个和上面制定计划这个,都需要有执行力。锻炼也有很多,除了专门去跑步外还可以多爬楼梯,多参加其它体育活动。

行为模式

这次总结行为模式。行为模式主要涉及算法涉及对象的职责分配问题。

迭代器模式

先介绍这个模式是因为它太常见了。各种编程语言的容器都有自己的迭代器。比如:Java中的List是个接口。只要符合List接口的各种List都可以使用它的迭代器来按照某种顺序遍历访问List的元素,而不必关心具体List的实现(是ArrayList还是LinkedList之类的)。

迭代器模式的主要用来:

  • 访问一个聚合对象的内容而不必暴露它的内部表示。

  • 支持聚合对象的各种遍历。

  • 为遍历不同的聚合结构提供一个统一的接口(多态迭代)。

参与者

  • Iterator

    --迭代器定义访问和遍历元素的接口。在Java中List的iterator()方法返回一个实现了Iterator接口的迭代器。这是典型的Iterator。

  • ConcreteIterator

    --具体的迭代器实现了Iterator接口。并能在遍历容器时跟踪当前位置。

  • Aggregate

    --定义创建响应迭代器对象的接口。也就是它可以返回Iterator。比如:Java的List。

  • ConcreteAggregate

    --实现了创建响应迭代器对象的接口,返回迭代器实例。

推荐看Java中LinkedList的代码。

职责链模式

这个模式用来避免请求的发送者和接受者之间的耦合关系。如果有多个对象,它们都有机会处理请求(但是何时处理的条件不同),就可以把对象连成一条链,沿着该链传递请求,知道有一个对象处理它为止。

举例:我们看龙珠或者其它战斗类型的漫画的时候会发现一个问题:主角去挑战大Boss的时候总是先打小Boss,再打大Boss,也就是说小Boss挂了或搞不定的时候大 Boss才会亲自动手。这就形成了一个责任链!可以把主角的挑战看成是请求,职责链上的对象(各种Boss)负责处理请求。如果职责链上的对象发现自己无法处理请求(小Boss打不过挂掉了),这个请求就会转发到下一对象(更高级的Boss),否则就处理这个请求(就是Boss赢了)。可以把打的过程看成是判断是否可以处理请求,输赢看成转发和处理。

这里需要注意,职责链上的对象在转发请求的时候是不知道最终会由谁处理请求的(小Boss打输后不是很确定后面的Boss会不会赢)。

上面这个例子的参与者

  • Handler(Boss 类)

    --定义一个处理请求的接口。在这里可以抽象出一个Boss类。 --(可选)实现后继链。

  • ConcreteHandler(Boss类的子类,各种Boss)

    --处理它负责的请求。

    --可以访问后继者。

    --如果能处理请求就处理,否则转发请求给后继者。

  • Client(主角)

    --向链上的具体处理者ConcreteHandler提交请求。

例子的代码:

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
#include <iostream>

using namespace std;

class Kakarotto;

class Boss{//handler
Boss *successor;
protected:
int power;
public:
Boss(int p, Boss *s):power(p), successor(s){}
virtual void handleChallenge(Kakarotto *k){
if(successor==NULL){
cout<<"Challenger win!"<<endl;
return;
}
successor->handleChallenge(k);
}
};


class Kakarotto{//client
int power;
public:
Kakarotto(int p):power(p){}
void Challenge(Boss *b){
cout<<"Challenging"<<endl;
b->handleChallenge(this);
}
int getPower(){return power;}
};

class Vegeta: public Boss{//concrete handler 1
public:
Vegeta(int p, Boss *s):Boss(p, s){
}

virtual void handleChallenge(Kakarotto* k){
cout<<"Vegeta is challenged!"<<endl;
if(k->getPower()<power){
cout<<"Vegeta win!"<<endl;
}else{
cout<<"Vegeta failed"<<endl;
Boss::handleChallenge(k);
}
}
};

class Freeze: public Boss{//concrete handler 2
public:
Freeze(int p, Boss *s):Boss(p, s){}

virtual void handleChallenge(Kakarotto* k){
cout<<"Freeze is challenged!"<<endl;
if(k->getPower()<power){
cout<<"Freeze win!"<<endl;
}else{
cout<<"Freeze failed"<<endl;
Boss::handleChallenge(k);
}
}
};

int main(){
Kakarotto * k = new Kakarotto(10000);
Freeze *f = new Freeze(1000000, NULL);
Vegeta *v = new Vegeta(5000, f);
k->Challenge(v);
return 0;
}

命令模式

将请求封装为对象,可用不同的请求对客户进行参数化。用于“行为请求者”与“行为实现者”解耦。

举例:

现在有一个文档处理程序中用到了菜单。菜单中有许多菜单项。每个菜单项执行一个或一系列命令。程序用某种UI框架开发。显然,框架开发者是不知道菜单具体会执行哪些功能的,它只知道何时执行(即用户触发了Click事件它可以捕捉到,然后去执行具体功能)。具体的命令,将被封装成对象,以供调用。

在这个例子中,要执行的命令都实现了Command接口中的方法。菜单项MenuItem就是负则调用Command接口中方法的类,从而在用户点击菜单项时执行操作(准确说操作是Command类的子类里的接受者Receiver执行的)。应用程序类为App,它持有具体的命令对象(Command的子类)并绑定了接受者,建立了MenuItem具体Item类的联系。Receiver是文档类Document,它知道如何如何对文档实施具体的操作。

上面这个例子的参与者

  • Command

    --声明操作的接口

  • ConcreteCommand(复制、粘贴等等)

    --将一个接受者对象绑定于一个动作。

    --调用接受者相应的操作,从而实现execute()。

  • Client(App)

    --创建具体命令对象并设置接受者。

  • Invoker(菜单项MenuItem)

    --要求该命令执行这个请求。注意一个Invoker可以对应多个Command。

  • Receiver(Document)

    --知道如何实施与执行一个请求相关的操作。

这里有个疑问:为什么需要Command类,而不是直接使用Invoker?这是因为Command类的execute()方法中可以做点别的工作,比如记录指令日志,从而支持撤销命令等等。

命令模式和过程式语言的回调函数机制十分类似。

下面是一个例子代码:

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
package com.vbill;

class Doc{//receiver
String name;

public Doc(String name){
this.name = name;
}

public void edit(){
System.out.println("edit "+name);
}

public void open(){
System.out.println("open "+name);
}
}

abstract class Command{//command
protected Doc doc;

public Command(Doc doc){ this.doc = doc; }

abstract public void execute();
}

class EditCommmand extends Command{//concrete command
public EditCommmand(Doc doc){
super(doc);
}

@Override
public void execute(){
doc.edit();
}
}

class OpenCommmand extends Command{//concrete command
public OpenCommmand(Doc doc){
super(doc);
}

@Override
public void execute(){
doc.open();
}
}

class Invoker extends Thread{//invoker
private Command com;
@Override
public void run() {
System.out.println("waiting for invoke!");
try{
sleep(1000);
}catch(InterruptedException e){
e.printStackTrace();
}
com.execute();
}

public void setCom(Command com){ this.com = com; }
}

public class Main {
public static void main(String[] args) {//client
Command coms[] = new Command[]{ new OpenCommmand(new Doc("1.txt")),
new EditCommmand(new Doc("1.txt"))};
Invoker inv = new Invoker();
for(Command com : coms) {
inv.setCom(com);
inv.run();
try {
inv.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}

策略模式

定义了一系列的算法的封装,使得它们可以相互替换。这使得算法可以独立于使用它的客户而变化。

显然,因为算法之间是可以替换的,所以它们有相同的接口,我们管这个接口叫做策略(Strategy)。

举个例子:现在有一个程序提供了一个搜索条,根据文本的长短和各个字符出现的频率,它可以调整自己的字符串搜索算法。

以上例子中的参与者

  • Strategy

    --定义了所有算法的公共接口。Context使用这个接口来调用ConcreteStrategy定义的算法。上面这个例子中,可以定义一个只含1个方法的接口,它接收2个参数,一个是正文的指针,另一个是待搜索串的指针。

  • ConcreteStrategy

    --以Strategy接口实现的算法。在这个例子中就是各种字符串搜索算法。

  • Context

    --上下文。用1个ConcreteStrategy对象来配置。维护一个ConcreteStrategy对象的引用。并且可定义一个接口让Strategy访问它的数据。

下面是一个配置了不同的字符串搜索算法的策略模式的例子:

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
package com.vbill;

interface StrfindStrategy{
int strFind(String dest, String pattern);
}

class KMPFinder implements StrfindStrategy{
int next[];

private void calcNext(String pattern){
next = new int[pattern.length()];
next[0] = -1;
for(int i=1, j = -1; i<pattern.length(); i++){
while (j>=0 && pattern.charAt(j+1) != pattern.charAt(i))
j = next[j];
if(pattern.charAt(j+1) == pattern.charAt(i)) j++;
next[i] = j;
}
}

@Override
public int strFind(String dest, String pattern){
if(pattern.length()>dest.length())return -1;

calcNext(pattern);
for(int i=0, j=-1; i<dest.length(); i++){
while(j>=0 && pattern.charAt(j+1)!=dest.charAt(i))
j = next[j];

if(pattern.charAt(j+1)==dest.charAt(i)) j++;

if(j==pattern.length()-1)
return i-pattern.length()+1;
}
return -1;
}
}

class DefaultFinder implements StrfindStrategy{
@Override
public int strFind(String dest, String pattern){
return dest.indexOf(pattern);
}
}

class Context{
private StrfindStrategy strategy;
public Context(StrfindStrategy s){
this.strategy = s;
}

public int execute(String dest, String pattern){
return strategy.strFind(dest, pattern);
}

public void setStrategy(StrfindStrategy s){
this.strategy = s;
}
}

public class Main {
public static void main(String[] args) {//client
String dest = "xbaadabcabab";
String pattern = "abab";

Context con = new Context(new DefaultFinder());
System.out.println(con.execute(dest, pattern));
con.setStrategy(new KMPFinder());
System.out.println(con.execute(dest, pattern));
}
}

观察者模式

定义对象间的一对多的依赖关系,当一个对象的状态发生改变时,所有依赖于它的对象都得到通知并自动更新。

举个例子:现在有许多个统计图(柱状图、折现图、扇形图),这些图都反映了同一个表格的数据,如果表格中的数据改变了,图中的内容也要自动发生响应的变化。

模式的参与者

  • Subject

    --目标知道它的观察者,可以有多个观察者观察同一个目标。

    --提供注册和删除观察者的接口。

  • Observer

    --为那些在目标发生改变时需要获得通知的对象定义一个更新接口。

  • ConcreteSubject

    --将有关状态存入各ConcreteObserver对象。

    --当它的状态发生改变时,向它的各个观察者发出通知。

  • ConcreteObserver

    --维护一个指向ConcreteSubject对象的引用

    --存储有关状态,这些状态应与目标的状态保持一致。

    --实现Observer的更新接口以使自身状态与目标的状态保持一致。

在Java语言中自带了该模式相关的接口,下面是例子:

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
package com.vbill;

import java.util.*;

class MyObservable extends Observable{
//concrete subject
//Observable is subject
private int data = 0;

public int getData(){
return data;
}

public void setData(int i){
this.data = i;
setChanged();
notifyObservers();
}
}

class MyObserver implements Observer{
//concrete observer
//Observer is observer

@Override
public void update(Observable o, Object arg) {
MyObservable myObservable = (MyObservable) o;
System.out.println("Data changed to "+ myObservable.getData());
}
}

public class Main {
public static void main(String[] args) {//client
MyObservable d = new MyObservable();
d.addObserver(new MyObserver());
d.addObserver(new MyObserver());
d.setData(12);
d.setData(1);
d.setData(3);
}
}

在jdk源代码里,Observable接口(相当于Subject)是这样实现的:

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
package java.util;

public class Observable {
private boolean changed = false;
private Vector<Observer> obs;

public Observable() {
obs = new Vector<>();
}

public synchronized void addObserver(Observer o) {
if (o == null)
throw new NullPointerException();
if (!obs.contains(o)) {
obs.addElement(o);
}
}

public synchronized void deleteObserver(Observer o) {
obs.removeElement(o);
}

public void notifyObservers() {
notifyObservers(null);
}

public void notifyObservers(Object arg) {
Object[] arrLocal;

synchronized (this) {
if (!changed)
return;
arrLocal = obs.toArray();
clearChanged();
}

for (int i = arrLocal.length-1; i>=0; i--)
((Observer)arrLocal[i]).update(this, arg);
}

public synchronized void deleteObservers() {
obs.removeAllElements();
}

protected synchronized void setChanged() {
changed = true;
}

protected synchronized void clearChanged() {
changed = false;
}

public synchronized boolean hasChanged() {
return changed;
}

public synchronized int countObservers() {
return obs.size();
}
}

jdk中Observer接口的源代码:

1
2
3
public interface Observer {
void update(Observable o, Object arg);
}

备忘录模式

在不破坏封装性的前提下,捕获一个对象的内部状态,并在对象之外保存这个状态,这样就可以将该对象恢复到原先保存的状态。

在这个模式中一个备忘录就是一个用来记录状态的对象,它记录的是某个对象在某个时刻的状态。被记录的对象称为Originator。当需要进行记录的时候,Originator请求一个备忘录,并用自己的当前状态设置备忘录对象。只有Originator能够存取备忘录的信息,其它对象则不可以。

参与该模式的参与者

  • Memento

    --备忘录存储原发器对象的内部状态。原发器根据需要决定存储哪些内部状态。

    --防止原发器外的其它对象访问备忘录。备忘录实际有2个接口,一个是Caretaker看到的窄接口,它只能将备忘录传递给其它对象。原发器只能看到宽接口,允许它访问返回到先前状态所需要的数据。理想的情况是只允许生成本备忘录的那个原发器访问本备忘录的状态。

  • Originator

    --原发器创建一个备忘录记录当前自己的状态。

    --使用备忘录恢复内部状态。

  • Caretaker

    --负责保存备忘录

    --不能对备忘录的内容进行检查操作或检查。

下面是一份Java代码的例子:

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
package com.vbill;

import java.util.*;

interface MementoNarrow{

}

class Memento implements MementoNarrow{
private String state;

public Memento(String state){
setState(state);
}

public void setState(String state){
this.state = state;
}

public String getState(){
return state;
}
}

class Originator{
private String state;

public void setState(String state){
this.state = state;
}

public String getState(){
return state;
}

public Memento createMemento(){
return new Memento(state);
}

public void getStateFromMemento(Memento memento){
this.state = memento.getState();
}
}

class CareTaker{
private LinkedList<MementoNarrow> list = new LinkedList<MementoNarrow>();

public void add(Memento memento){
list.add(memento);
}

public Memento get(){
return (Memento)list.getLast();
}

public void remove(){
list.removeLast();
}
}

public class Main {
public static void main(String[] args) {//client
Originator org = new Originator();
CareTaker ct = new CareTaker();

org.setState("1");
ct.add(org.createMemento());

org.setState("2");
ct.add(org.createMemento());

org.setState("3");
System.out.println(org.getState());

org.getStateFromMemento(ct.get());
ct.remove();
System.out.println(org.getState());

org.getStateFromMemento(ct.get());
ct.remove();
System.out.println(org.getState());
}
}

状态模式

允许对象在内部状态发生改变时改变它的行为,对象看起来好像修改了它的类。

也就是说这个对象的行为随着状态改变!

举个例子:现在有个2状态状态机,状态分别叫0和1。状态机只有2个输入0和1。在0状态下输入0状态始终为0,在0状态下输入1会切换到1。在1状态下输入1状态始终为1,输入0则切换到0状态。并且你有1个LED灯,当状态为1时灯亮,否则灯灭。

参与者

  • Context

    --定义用户感兴趣的接口。

  • State

    --定义一个接口以封装与Context的一个特定状态相关的行为。

  • ConcreteState

    --每一个子类实现一个与Context的一个状态相关的行为。

需要注意的是这个模式和策略模式非常相似。如果画出UML类图,会发现两者几乎是一样的。那么问题来了,究竟它们有啥区别?

在状态模式中,Context或ConcreteState子类都可以决定哪个状态是另外一个的后继者,以及是在何种条件下进行状态转换。

在策略模式中,Context将它的客户的请求转发给它的Strategy。客户通常创建并传递一个ConcreteStrategy对象给该Context;这样客户仅与Context交互。通常有一系列的ConcreteStrategy类可供客户从中选择。

上面的状态机的例子的Java代码:

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
package com.vbill;

import java.util.Scanner;

interface State{
void LED(Context context) throws Exception;
}

class State0 implements State{
@Override
public void LED(Context context) throws Exception{
System.out.println("0");

Scanner scanner = new Scanner(System.in);
int i = scanner.nextInt();
if(i==0){
context.setState(this);
}else if(i==1){
context.setState(new State1());
}else{
throw new Exception("Unknown state");
}
}
}

class State1 implements State{
@Override
public void LED(Context context) throws Exception{
System.out.println("1");

Scanner scanner = new Scanner(System.in);
int i = scanner.nextInt();
if(i==1){
context.setState(this);
}else if(i==0){
context.setState(new State0());
}else{
throw new Exception("Unknown state");
}
}
}

class Context{
private State state;

public Context(State state){
setState(state);
}

public void setState(State state){
this.state = state;
}

public void requestLED(){
try{
state.LED(this);
}catch (Exception e){
e.printStackTrace();
}
}
}

public class Main {
public static void main(String[] args) {
Context con = new Context(new State0());
int i = 7;
while(i-->0){
con.requestLED();
}
}
}

中介者模式

用一个中介对象来封装一些列的对象交互。中介者使各个对象不需要显式地相互引用,从而使其松散耦合,而且可以独立地改变它们之间的交互。

举例:现在有个对话框,上面有很多部件。比如:有文本框、列表框、一个确定按钮,仅当文本框里有内容时,按钮才能按下,按下按钮后文本框的内容将会被发送给程序的后台处理。在用户懒得输入文本框内容时,可以从列表框里选择几个预置的内容填入文本框。

如果直接让各个对象之间进行通信,耦合度是很高的。如果把各种集体行为封装在一个中介者(mediator)中就可以避免这个问题。中介者用来协调一组对象的交互,它使得对象之间不再显式的相互调用,这些对象只知道中介者。中介者就是对象通信的中转中心。

参与者

  • Mediator

    --中介者定义一个接口用于与各个同事对象通信。

  • ConcreteMediator

    --具体中介者通过协调各个同事对象实现协作行为。

    --了解并维护它的各个同事。

  • Colleague Class

    --每个同事类都知道它的中介者对象。

    --每一个同事对象在需要与其它同事通信的时候都与它的中介者通信。

上面提到的例子的Java代码如下(不含测试类)

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
import java.util.*;

abstract class Widget{
private DialogDirector director;

public void changed(){
director.WidgetChanged(this);
}

public Widget(DialogDirector dialogDirector){
this.director = dialogDirector;
}
}

interface DialogDirector{
void WidgetChanged(Widget w);
}

class ListBox extends Widget{
private ArrayList<String> list = new ArrayList<String>();
private int chosen = 0;

{
list.add("1"); list.add("2"); list.add("3");
}

public ListBox(DialogDirector dialogDirector){
super(dialogDirector);
}

public void setChosen(int chosen){
this.chosen = chosen;
}

public String getChosenElement(){
return list.get(chosen);
}
}

class TextField extends Widget{
private String content;

public void setContent(String content){
this.content = content;
changed();
}

public String getContent(){
return this.content;
}

public TextField(DialogDirector dialogDirector){
super(dialogDirector);
}
}

class Button extends Widget{
private boolean avail = false;

public boolean getAvail(){
return this.avail;
}

public void setAvail(boolean avail){
this.avail = avail;
changed();
}

public Button(DialogDirector dialogDirector){
super(dialogDirector);
}
}

class MyDialogDirector implements DialogDirector{
private Button okButton = new Button(this);
private ListBox listBox = new ListBox(this);
private TextField textField = new TextField(this);

public void WidgetChanged(Widget w){
if(w.equals(okButton)){
if(okButton.getAvail()==false)
System.out.println("Not available!");
else
System.out.println("ok" + textField.getContent());
}else if(w.equals(textField)){
okButton.setAvail(!textField.getContent().equals(""));
}else{
textField.setContent(listBox.getChosenElement());
}
}
}

访问者模式

表示一个作用于某对象结构中的各元素的操作。它使你可以在不改变各元素类的前提下定义作用于这些元素的新操作。

举例:这里有一个完整的例子。

模式的参与者

  • Visitor

    --为该对象结构中的ConcreteElement的每一个类声明一个visit操作。该操作的名字和特征标示了发送visit请求给该访问者的那个类。这使得访问者可以确定被访问元素的具体的类。这样访问者就可以通过该元素的特定接口直接访问它。

  • ConcreteVisitor

    --实现每个由Visitor声明的操作。每个操作实现本算法的一部分,而该算法片段乃是对应于结构中对象的类。ConcreteVisitor为该算法提供了上下文并存储它的局部状态。这一状态常常在遍历该结构的过程中基类结果。

  • Element

    --定义一个Accept操作,它以一个访问者为参数。

  • ConcreteElement

    --实现Accept操作,该操作以一个访问者为参数。

  • ObjectStructure

    --能枚举它的元素。

    --可以提供一个高层的接口以允许该访问者访问它的元素。‘

    --可以是一个符合(Composite)或是一个集合,如一个列表或一个无序集合。

解释器模式

给定一个语言,定义它的文法表示,并定义一个解释器,将这个解释器使用该表示来解释语言中的句子。

这个模式应用的情景比较特殊(个人感觉),比如:HTML解释器,正则表达式等等,甚至解释型语言的解释器。

模式参与者

  • AbstractExpression

    --声明一个抽象的解释操作,这个接口为抽象语法树中所有节点所共享。

  • TerminalExpression

    --实现与文法中的终结符相关的解释操作。

    --一个句子中的每个终结符需要该类的一个实例。

  • NonterminalExpression

    --对文法中的每一条文法规则R::R1R2...Rn都需要一个NonterminalExpression类。

    --为从R1到到Rn的每个符号都维护一个AbstractExpression类型的实例变量。

    --为文法中的非终结符实现解释操作。解释一般要递归地调用表示R1到Rn的那些对象的解释操作。

  • Context

    --包含解释器之外的一些全局信息。

  • Client

    --构建(或被给定)表示该文法定义的语言中一个特定的句子的抽象语法树。该抽象语法树由NonterminalExpression和TerminalExpreession的实例装配而成。

    --调用解释操作。

结构型模式

这次主要说结构型模式。这类模式还是在各种场合比较常见的。

适配器模式

这个模式主要用来解决一些接口不相互兼容的情况。举个例子:现在有一类电器,都是插在110V的电压的电源上。然而电源现在只有220V的。为了让电器能够用上电,必须在220V电源和110V用电器之间弄一个转换器把电压转成110V。

在这里需要明确一下这个模式的几个参与者:

参与者

  • Client:使用者,与符合Target接口的对象协同工作。这个例子里就是用电器。
  • Target:与Client使用的特定领域相关的接口。这里就是符合110V电压标准的那类电源插孔。
  • Adaptee:已经存在的接口,它需要适配。这里就是220V电源。
  • Adapter:适配器,对Adaptee的接口与Target的接口进行适配。这个例子里就是110V转换器。

这里有个问题:为什么需要一个Target,直接用Adapter不可以么?回答:Client需要的是符合某个接口规范的一类类,而不是具体的某个类。Adapter是一定实现了Target的接口的。就像转换器可以有很多种类和品牌,有些甚至可以带一些无关的花哨的功能,但是一定都具有转换功能并提供一个符合要求的输出插孔。

这个设计模式有两种实现方法:

  • 使用多重继承,Adapter继承Target和Adaptee。
  • 使用对象组合。Adapter仍然继承Target且拥有Adaptee对象实例。

对于多重继承的方法,Adapter应该私有继承Adaptee(防止公有接口暴露)。

这个例子的代码写下来应该是这样的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class PowerSupport110{
public:
virtual Power110 getPower110();
};//Target

class ChinaPowerSupport220{
public:
virtual Power220 getPower220(){
//......
return /*......*/;
}
};//Adaptee

class MyAdapter110:public PowerSupport110, private ChinaPowerSupport220 {
public:
virtual Power110 getPower110(){
return convert110(getPower220());
}
};//Adapter

代理模式

这个模式为访问某个对象提供了控制手段。举2个例子:

  • 1.打开网页的时候某些大的图片、动画可能要过一段时间才能加载出来,但是网页提供了一个占位符,即使没加载出来,我们也可以查看图片的信息,控制重新加载内容,或者另存为到本地。
  • 2.C++里的智能指针(比如shared_ptr)。

参与者

这个模式的参与者有如下的几个(以第1个网站的例子说明):

  • Subject:声明了RealSubject和Proxy的共同接口,这样任何使用RealSubject的地方都可以使用Proxy。这个例子中就是网页里的图片(准确说是图片的接口,因为操作都是针对接口的)。
  • Proxy:内部含有RealSubject的引用,从而可以操作RealSubject对象;提供一个与Subject接口相同的接口,这样就可以替代实体;控制实体的存取,并可能负责创建和删除它。还有其它功能依赖于代理的类型。
  • RealSubject:定义了Proxy代表的实体。

代理的类型:

  • 1.远程代理:为一个对象在不同的地址空间提供局部代表。
  • 2.虚代理:根据需求创建开销很大的对象。(占位符的例子)
  • 3.保护代理:控制对原始对象的访问(比如希望它们有不同的访问权限)。
  • 4.智能指引:在访问对象的同时执行一些附加的操作(如:C++的shared_ptr)。

以图片加载的例子为例的代码:

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
#include <iostream> 
#include <string>
using namespace std;

class Image{};

class Graphic{
protected:
string imgName;
public:
Graphic(const char* s):imgName(s){}
virtual void display()=0;
};

class Picture: public Graphic{
Image *img;

bool LoadFromDisk(){
cout<<imgName;
cout<<" loaded!"<<endl;
img = new Image;
}
public:
Picture(const char* s):Graphic(s){
LoadFromDisk();
}

virtual void display(){
cout<<"Displaying "<<imgName<<endl;
}
};

class PicProxy: public Graphic{
Picture *pic;
public:
PicProxy(const char* s):Graphic(s){
pic = NULL;
}

virtual void display(){
if(pic==NULL){
pic = new Picture(imgName.c_str());
}
pic->display();
}
};

int main(){
PicProxy pp("pic1.jpg");
pp.display();
pp.display();
return 0;
}

装饰器模式

有时候我们希望动态的给对象添加职责(不是整个类,而是某个对象),就可以用这个模式。

整个模式中的参与者:

  • Component:定义了一个对象接口,可以给这些对象动态添加职责。它是抽象的。
  • ConcreteComponent:定义了对象,可以给这些对象动态添加职责。它继承自Component。可以给这个对象添加职责。
  • Decorator:维持一个指向Component对象的指针,且定义了与Component接口一致的接口。
  • ConcreteDecorator:维持一个指向Component对象的指针,且定义了与Component接口一致的接口。

应用举例

比较典型的装饰器的用法是Java里各种I/O流。比如:InputStream这个抽象类是Component。FileInputStream是ConcreteComponent,它实现了InputStream的read接口,然而它除了简单的read一些东西外,并没有什么高级功能。

如果希望扩展出高级功能就需要Decorator。Java中比较典型的是FilterInputStream(相当于Decorator)。它的子类:BufferedInputStream、DataInputStream就是ConcreteDecorator。这两个ConcreteDecorator的构造函数的参数都是InputStream类型。

BufferedInputStream是带缓冲的,DataInputStream的作用是从流中读出不同的Java数据类型。那么,如果用BufferedInputStream修饰FilterInputStream,再用DataInputStream修饰刚才的BufferedInputStream,就可以从带缓冲的文件流中读各种Java数据类型。 即:

1
DataInputStream inputstream = new DataInputStream(new BufferedInputStream(new FileInputStream("Data.txt")));

下面是一份Java代码, 代码中View对象被滚动条修饰后又被边框Border修饰:

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
package com.vbill;

interface VisualComponent{
void Draw();
}

class View implements VisualComponent{
@Override
public void Draw(){
System.out.println("Drawing view!");
}
}

abstract class Decorator implements VisualComponent{
private VisualComponent comp;

public Decorator(VisualComponent comp){
this.comp = comp;
}

@Override
public void Draw(){
comp.Draw();
}
}

class Border extends Decorator{
public Border(VisualComponent comp){
super(comp);
}

@Override
public void Draw(){
super.Draw();
System.out.println("Drawing border!");
}
}


class ScrollBar extends Decorator{
public ScrollBar(VisualComponent comp){
super(comp);
}

@Override
public void Draw(){
super.Draw();
System.out.println("Drawing scrollbar!");
}
}


public class Main {
public static void main(String[] args) {
Border b = new Border(new ScrollBar(new View()));
b.Draw();
}
}

外观模式

如果现在有一个很复杂的系统(编译器之类的),但是用户不关心这个复杂系统的内部各个子系统的工作细节,那么可以引入一个Facade类,由它提供我们关心的部分,并负责将我们的请求代理给合适的子系统对象(比如我们要编译程序,编译器的lexer, parser等等部件我们不关心,我们只要一系列编译指令和能设置的参数选项)。

举个例子:现在有个工厂,某客户要进货。工厂有3个部门分别负责订单处理,生产,出货。

在没有外观模式之前可能是这样的(以下是Java代码):

1
2
3
4
5
6
7
8
9
10
// 以下代码写在客户代码里
// 依次创建三个部门实例
Order order = new Order();
Producer producer = new Producer();
Sender sender = new Sender();

// 依次调用三个部门实例的方法
order.order(1000); //订1000件
Goods goods = producer.produce(order);
sender.send(goods);

使用外观模式:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public class Factory {
//定义被封装的三个部门
Order order;
Producer producer;
Sender sender;

// 构造器
public Factory() {
this.order = new Order();
this.producer = new producer();
this.sender = new Sender();
}

public Goods buyThings(int num) {
//把整个流程封装
order.order(num); //订num件
Goods goods = producer.produce(order);
sender.send(goods);
return goods;
}
}

然后就可以使用了:

1
2
Factory f = new Factory();
Goods g = f.buyThings(1000);

桥接模式

这个模式可以实现抽象与实现分离,使得它们可以各自独立变化。

举例:现在有一个Window类,它提供了基本的绘图功能(绘制文字,矩形)。其它各种具体的Window里的绘图功能都是基于Window里这些基本操作实现的。现在我们希望Window类跨平台,或者运行时修改基本绘图功能的实现。如果使用继承机制,那么必须位每个平台或实现继承出一个类。因为继承是静态的,所以每增加一个平台Window的每种子类都要继承出一个对应平台的类,这是很麻烦的。

改进的方法是:把平台相关的部分操作独立出来。因为这些操作接口相同,因此可以独立成一个抽象类(比如叫WindowImp,它包含绘制文字、矩形的接口)。那么这些独立出来的每种平台的具体实现就继承自这个独立出的抽象类。然后在原来的Window类里维护一个抽象类的指针。原来用到的平台相关的操作,现在都通过指针指向的对象来实现。当需要改变这部分实现的时候,只让这个指针指向其它版本的实现就可以了。

模式的参与者

  • Abstraction:抽象部分的接口。维护一个Implementor的对象指针。
  • RefinedAbstraction:扩展Abstraction的接口。
  • Implementor:定义实现类的接口,该接口不用和Abstraction的完全一致;事实上它们可以完全不同。一般Implementor接口提供基本操作,而Abstraction里定义了基于这些基本操作的较高层次的操作。
  • ConcreteImplementor:真正实现Implementor接口的对象。

这个模式之所以叫”桥接“,就是因为Abstraction和Implementor(本例中的WindowImp是Implementor,Abstraction是Window)起到了桥梁作用,将各个平台具体的实现ConcreteImplementor和RefinedAbstraction(各类具体的Window)联系了起来。

下面是例子的Java代码:

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
package com.vbill;

interface WindowImp{
void DevDrawText();
void DevDrawLine();
}

class LinuxWindowImp implements WindowImp{
@Override
public void DevDrawText() {
System.out.println("Drawing text on linux");
}

@Override
public void DevDrawLine() {
System.out.println("Drawing line on linux");
}
}

class WindowsWindowImp implements WindowImp{
@Override
public void DevDrawText() {
System.out.println("Drawing text on Windows");
}

@Override
public void DevDrawLine() {
System.out.println("Drawing line on Windows");
}
}

abstract class Window{//声明为abstract 防止实例化
private WindowImp imp;

protected void DrawText(){
imp.DevDrawText();
}

protected void DrawRect(){
imp.DevDrawLine();
imp.DevDrawLine();
imp.DevDrawLine();
imp.DevDrawLine();
System.out.println("Rect ok!");
}

public Window(){imp = new WindowsWindowImp();}

public void ChangeAPI(){
if(imp instanceof WindowsWindowImp){
imp = new LinuxWindowImp();
}else{
imp = new WindowsWindowImp();
}
}
}

class BigWindow extends Window{
public void DrawBigWindow(){
System.out.println("Big window ok!");
DrawRect();
DrawText();
}
}

class SmallWindow extends Window{
public void DrawSmallWindow(){
System.out.println("Small window ok!");
DrawRect();
}
}

public class Main {
public static void main(String[] args) {
BigWindow b = new BigWindow();
b.DrawBigWindow();
b.ChangeAPI();
b.DrawBigWindow();

SmallWindow s = new SmallWindow();
s.DrawSmallWindow();
s.ChangeAPI();
s.DrawSmallWindow();
}
}

组合模式

将对象组合成树形结构以表示”整体-部分“的层次结构。Composite使得用户对单个对象和组合对象的使用具有一致性。

举个最简单的例子:做PPT的时候,Powerpoint提供了许多基本图形。我们可以画几个基本图形,然后点击”组合“。这样就行成了组合图形。而且软件对新的组合图形还有基本图形的操作基本是一样的。这种方式在其它许多绘图软件里也很常见。

这个模式的关键在于提供了一个抽象类。基本的图形类继承自它,图形的组合容器类也继承自它。这样用户操纵抽象类的时候,基本图形和容器看上去就是一样的。

这样看上取,对象的组合关系就像一棵树一样,是递归的。

模式的参与者

  • Component

    --为组合中的对象声明接口。

    --在适当情况下实现所有类共有接口的缺省行为。

    --声明一个接口,用于访问和管理Component的子组件

    --(可选)在递归结构中定义一个接口,用于访问一个父部件,并在合适的情况下实现它。

    在上面例子中,把图形抽象成Graphic类,就是Component。

  • Leaf

    --在组合中表示叶节点对象。叶节点没有子节点

    显然基本的图形就是Leaf。

  • Composite

    --定义有子部件的那些部件的行为。

    --存储子部件。

    --在Component接口中实现与子部件有关的操作。

    显然容器类就是Composite。它可以存放多个基本图形和容器的组合。本例中可以命名它为Pic。

  • Client

    --通过Component的接口操纵组合部件。

这里有个问题:模式中所有的类均继承自Component。但是Component提供了许多对Leaf而言不必要的接口,比如添加/删除组合部件,返回孩子部件的代码。解决这个问题的方法是,在Component中提供默认的操作(什么都不做,返回无效值,抛出异常等等),然后在Leaf型的对象中不要重写这些无关的方法,在用到这些接口方法的Composite类型中重写这些方法。

下面是例子的Java代码:

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
package com.vbill;

import java.util.*;

abstract class PicComponent{
abstract void show();
void add(PicComponent p){}
void remove(PicComponent p){}
PicComponent getChild(int c){ return null; }
}

class Rectangle extends PicComponent{
@Override
public void show() {
System.out.println("Rectangle");
}
}

class Text extends PicComponent{

@Override
public void show() {
System.out.println("Text");
}
}

class Triangle extends PicComponent{

@Override
public void show() {
System.out.println("Triangle");
}
}

class Picture extends PicComponent{
private LinkedList<PicComponent> comps;

public Picture(){
comps = new LinkedList<PicComponent>();
}

@Override
public void show() {
System.out.println("{");
for(PicComponent p : comps){
p.show();
}
System.out.println("}");
}

@Override
public void add(PicComponent p) {
comps.add(p);
}

@Override
public void remove(PicComponent p) {
comps.remove(p);
}

@Override
public PicComponent getChild(int c) {
return comps.get(c);
}
}

public class Main {
public static void main(String[] args) {
Picture bigpic = new Picture();

Text txt = new Text();
Triangle tri = new Triangle();
Rectangle rect = new Rectangle();
Picture smallpic = new Picture();
smallpic.add(txt);
smallpic.add(tri);
smallpic.add(rect);

Rectangle rect2 = new Rectangle();

bigpic.add(rect2);
bigpic.add(smallpic);

bigpic.show();
}
}

享元模式

该模式运用共享技术,支持大量细粒度对象。

比如:现在需要开发一个文本编辑器。其中文本框内每个字符都有自己的字形、位置等等信息。如果为每个字符都创建一个对象实例,那么内存空间的开销将会非常巨大。

实际上在创建对象的时候有写信息是可以共享的。比如,文字可能是ASCII字符集里的。那么每个对象就不必维护自己的字形,只需要记录自己的位置就可以了。

到这里就涉及2个概念:

  • 外部状态:会随着不同的场景变化,这些信息不能被共享。
  • 内部状态:独立于具体场景的信息,这些信息使得flyweight可以被共享。

那么,这个例子里字形就是内部状态,位置就是外部状态。

模式的参与者

  • Flyweight

    --描述一个接口,通过它flyweight可以接受并作用于外部状态。

  • ConcreteFlyweight: --实现Flyweight接口,并为内部状态(如果有)增加存储空间。ConcreteFlyweight对象必须是可共享的。它所存储的状态必须是内部的(独立于ConcreteFlyweight)对象的场景。

  • UnsharedConcreteFlyweight

    --Flyweight接口使得共享成为可能,但是也可以有一些对象不被共享。比如:在文本编辑器里,多个字符可以组成一个行对象。这样ConcreteFlyweight对象(字符)就是UnsharedConcreteFlyweight的子节点。

  • FlyweightFactory

    --创建并管理Flyweight对象。当用户请求一个Flyweight的时候,它提供一个已经创建的实例,或创建一个(如果不存在的话)。

  • Client

    --维持对Flyweight的引用,计算/存储多个Flyweight的外部状态。

如果外部状态是算出来的而不是存储起来的,空间节约将达到最大。比如:字符在屏幕中的位置是根据窗口尺寸和它是第几个字符算出来的。

说明:这是一个个人学习的入门性总结,而且其中多是一些个人的理解,没有什么抄书的内容。所以如果有问题请指出。

常见的设计模式分为3类:创建型,结构型,行为型。分别用来完成对象的:创建、组合、交互和职责分配。

创建型模式

抽象工厂

这个模式用来创建一个“系列”产品,简化创建过程,并让客户类不再操心具体的创建细节。比如假设现在你在玩一个游戏,游戏主人公可以用手头的材料合成成套的盔甲。盔甲的材质有皮,铁,黄金,钻石。在不同的环境下,你希望使用不同的盔甲套装(比如一般情况下穿皮甲方便行动,外出穿铁甲加强防护,打怪的时候穿金套装)。显然每次根据不同的情况"new"一堆装备是很麻烦的。为了让不再每次都操心具体的创建过程,并且保证每次创建的装备的材质都一样,可以声明一个抽象工厂类,在其中声明一些抽象方法用来创建一套完整的装备。到具体要创建东西的时候,调用实现了这个抽象类中抽象方法的具体类就可以了。抽象工厂的每个具体类负责创建具体某种材质的套装。显然为了让套装中的每种装备(比如头盔,外套)有不同的材质,每种装备都应该声明为抽象类,这样不同材质的装备就可以从基类派生出。

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
#include<iostream>
using namespace std;

class Helmet{
public:
virtual void use()=0;
};

class Coat{
public:
virtual void use()=0;
};

class GoldHelmet: public Helmet{
public:
void use(){ cout<<"use GoldHelmet"<<endl; }
};

class GoldCoat: public Coat{
public:
void use(){ cout<<"use GoldCoat"<<endl; }
};

class IronHelmet: public Helmet{
public:
void use(){ cout<<"use IronHelmet"<<endl; }
};

class IronCoat: public Coat{
public:
void use(){ cout<<"use IronCoat"<<endl; }
};

class ArmorFactory{
public:
virtual Helmet* MakeHelmet()=0;
virtual Coat* MakeCoat()=0;
};

class GoldArmorFactory : public ArmorFactory{
public:
virtual Helmet* MakeHelmet(){return new GoldHelmet;}
virtual Coat* MakeCoat(){return new GoldCoat;}

};

class IronArmorFactory : public ArmorFactory{
public:
virtual Helmet* MakeHelmet(){return new IronHelmet;}
virtual Coat* MakeCoat(){return new IronCoat;}
};

int main(){
Helmet *hforest, *hmonster;
Coat *cforest, *cmonster;
//当你在树林里用铁甲
ArmorFactory *ia = new IronArmorFactory;
hforest = ia->MakeHelmet();
cforest = ia->MakeCoat();

hforest->use();
cforest->use();

//当你打怪用金甲
ArmorFactory *ia2 = new GoldArmorFactory;
hmonster = ia2->MakeHelmet();
cmonster = ia2->MakeCoat();

hmonster->use();
cmonster->use();

return 0;
}

以上的例子有两个工厂,一个负责创建金装备,另一个负责创建铁装备。另外从代码中还可以看出,工厂的指针可以作为一个参数传递,这样就可以在程序的其它地方使用工厂了。

另外也可以把工厂作为一个单例,因为一般来说程序中没有必要反复创建工厂类的实例。

最后抽象工厂有个缺点:如果在产品系列中增加新的类型(比如套装中增加了斧头),那么抽象工厂的基类和各个子类都要被修改。

单例模式

有时候需要全局只有一个对象实例,比如刚才的工厂,当需要某种材质的套装时只需要调用唯一的工厂实例创建套装就行了。

解决方法是:构造函数不要声明为public。并且在类声明内定义private的该类型的静态指针变量。这样就解决了唯一性问题。为了让外部访问唯一的对象实例,还要提供一个public方法,返回唯一实例的指针。

代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
class Singleton{
public:
static Singleton* getInstance(){
if(_instance==NULL) _instance = new Singleton;
return _instance;
}
protected:
Singleton();
private:
static Singleton* _instance;
};

Singleton* Singleton::_instance = NULL;
以上属于“懒汉式”单例。它在首次需要用到实例时才创建实例,而"饿汉式"则在程序开始就初始化实例。如果初始化依赖某些程序运行时的信息,“懒汉式”单例就派上了用场。

工厂方法模式

首先要介绍一下简单工厂模式。 #### 简单工厂 假设现在某个程序需要建立一个链接访问系统的某个资源。我们假设这个链接类的名子叫Connection。如果系统为该资源提供了多种访问方式,那么这个链接类也对应有很多子类(子类和访问方式对应)。如果这个资源的每种访问方式都涉及很多其它的类,操作,那么最好把这些东西都封装起来,让客户不关心Connection如何创建的。这样客户只要指定访问方式,然后使用Connection对象访问资源就行了。

比如:

1
Connection con = ConFactory().createConnection(way);
这样就把方式当做参数传入,根据不同参数返回不同的Connection子类。ConFactory代码可能是这个样子的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class ConFactory{
public:
//。。。。
Connection* createConnection(int way){
switch(way){
case 1:
ConnectionA *con1 = new ConnectionA;
//一些准备工作
return con1;
case 2:
ConnectionB *con2 = new ConnectionB;
//一些准备工作
return con2;
//。。。
}
}
};

这个设计模式虽然解决了客户创建对象的问题,使得客户不再需要关心对象创建的细节,但是很明显,如果增加了Connection类型,那么类内switch部分的代码又要修改。这违背了“开放-封闭原则”。

工厂方法

简单工厂模式违背“开放-封闭原则”主要是因为内部的逻辑判断。为了克服这个问题,逻辑判断可以放在客户代码里。以前为了创建Connection对象要做很多准备工作,现在代码改了,怎么办?这里采取的方法是:把工厂类弄成抽象的,然后每种Connection子类对应一个抽象工厂的具体实现类。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class ConFactory{
public:
virtual Connection* createConnection()=0;
};

class ConFactoryA{
public:
virtual Connection* createConnection(){
ConnectionA *con1 = new ConnectionA;
//一些其它工作。。。
return con1;
}
};

class ConFactoryB{//。。。。。。

需要用某个Connection类的时候可以这样:

1
2
3
4
5
6
7
Connection con;
switch(way){
case 1:
con = ConFactoryA().createConnection();
break;
//。。。
}
或者如果事先知道了需要何种Connection,也可以直接用相应的工厂创建对象。

从上面也可以看出,抽象工厂就是工厂方法中每种工厂类可以创建一批对象的情形。

生成器模式

如果需要构建一个复杂的对象,甚至这个对象还需要进行一定的“组装”(各个部件之间有些联系),那么如果把生成和装配的代码分离出来会比较好。比如:现在有一个房屋生成的程序,它建造房子的流程是固定的几项(先造地基,然后是盖一定的层数,安装房顶,内部装修)。如果希望给房子选一些不同风格的部件,可以把这些盖房流程抽象成一个Builder接口。然后用不同的ConcreteBuilder类实现这些接口,这样每种具体的Builder的实现就能够盖出一个不同的房子。

Builder的组装受一个Director类的控制。Director调用Builder中的接口方法实现构造对象。显然Director不关心究竟怎么构建对象的。只要增加一个Builder的实现,不用修改代码,就可以让Director造一个新类型的房子。Director调用Builder里方法的顺序也不是固定的,可以根据情况进行一些动态的调整(比如GOF书里RTF文档阅读器的例子)。

下面的代码制造一个木头房子。

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

class House{};
class WoodHouse : public House{};

class HouseBuilder{
public:
virtual void buildBase()=0;
virtual void buildWalls()=0;
virtual void buildRoof()=0;
virtual void buildFurniture()=0;

virtual House* getHouse()=0;
};

class WoodHouseBuilder{
WoodHouse* whouse;
public:
virtual void buildBase(){ //..... }
virtual void buildWalls(){ //..... }
virtual void buildRoof(){ //..... }
virtual void buildFurniture(){ //..... }

virtual House* getHouse(){return whouse;}
};

House* HouseDirector(const HouseBuilder& builder){
builder.buildBase();
builder.buildWalls();
builder.buildRoof();
builder.buildFurniture();
return builder.getHouse();
}

int main(){
WoodHouseBuilder b;
House* house = HouseDirector(b);
return 0;
}

如果需要别的房子,只要相应地增加HouseBuilder和House的子类就可以。然后调用HouseDirector方法完成组装。

原型模式

这是本文介绍的最后一个设计模式。

这个设计模式主要通过“拷贝”实现对象的构造。比如,现在有个画板程序。其中有许多预置图形(继承自同一基类Shape)。为了实现快速生成一个图形对象,可以采用实现构造一个对象实例,然后复制它的方法。这样:第一,如果需要增加新的图形,那么只要继承图形基类Shape,其它代码不变即可。第二,因为只是复制对象,所以创建图形的代价会减小。为了复制对象,可以为要复制的类定义clone方法。如果在创建复制对象的时候想对复制的对象进行一些修改,可以定义Initialize方法。

附一个GOF书上迷宫生成器的例子:

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
class MazePrototypeFactory(Maze*, Wall*, Room*, Door*){
Maze* _prototypeMaze;
Room* _prototypeRoom;
Wall* _prototypeWall;
Door* _prototypeDoor;
public:
MazePrototypeFactory(Maze*, Wall*, Room*, Door*);

virtual Maze* MakeMaze() const;
virtual Room* MakeRoom(int) const;
virtual Wall* MakeWall() const;
virtual Door* MakeDoor(Room*, Room*) const;
};

MazePrototypeFactory::MazePrototypeFactory(
Maze* m, Wall* w, Room* r, Door* d)
{
_prototypeMaze = m;
_prototypeRoom = r;
_prototypeWall = w;
_prototypeDoor = d;
}

Wall* MazePrototypeFactory::MakeWall()const{
return _prototypeWall->clone();
}

Door* MazePrototypeFactory::MakeDoor(Room* r1, Room* r2)const{
Door* door = _prototypeDoor->clone();
door->Init(r1, r2);
return door;
}

MazeGame game;
MazePrototypeFactory SimpleMazeFactory(
new Maze, new Wall, new Room, new Door
);

Maze* maze = game. createMaze(SimpleMazeFactory);

之前因为学校考试时间原因拒绝了阿里巴巴的实习机会。几天后腾讯那边通知笔试过了,要去面试。腾讯的面试经历也是相当宝贵的经历,因为是第一次参加现场的面试,所以记录一下(4月底面的,问的问题可能不止这些,时间久记不住所有问题)。

一面

一面面试问了一些基础问题,因为投的是后台开发,问了很多网络方面的问题,但是都十分基础。比如:HTTP协议是TCP还是UDP之上的?讲一下HTTPS协议。对Linux套接字编程了解如何,发送和读取数据有哪些方法(我不知道什么意思,答的是那几个API:read/write, send/recv, sento/recvfrom,然后扯了一下udp的发送和接受)。阻塞和非阻塞通信的区别(中间有个概念搞错了,设置超时也是属于阻塞的)?还有套接字怎么关闭的(四次挥手),为什么有等待时延,套接字关闭后还能收数据么(没答出来)?算法问题比较简单:从两个链表中找出重复数字。我一开始用哈希表,就着优化哈希表扯了半天,后经提醒换用位图法,用01位标记数字是否出现。不过答题前我问面试官数字范围,他说int范围。我觉得这样用位图的话空间开销很大,如果链表不长还是哈希好点,但是我猜这是正确答案吧,所以没用开始没用位图的理由我就没说。最后问C和C++,static关键字的作用(忘了答在C++类里的作用了),static变量在内存的布局。没问项目经历和参加的比赛。一面问了二十多分钟。

可能是早上六点多就从学校赶到市区面试的原因,平时睡眠时间本来就不是很多,一早起脑袋有点晕。好多会的问题没回答好。。。答题前一定要想好再说,要点要说全。。。

二面

二面还是没怎么问项目。。。问了比赛:介绍数学建模是什么比赛,你负责干什么?我就介绍了一下,然后讲了下15年国赛题(作了个死,说这个比赛和计算机关系不大。。。),我说我负责编程辅助写作,LaTex比较熟。然后问最近在干什么,我说学校让组队做个大课设,我和另外两个同学写一套ftp服务器和客户端,我负责整个项目。之后就是让深入介绍服务器的架构,以及你们怎么学习、了解、实现ftp协议的。我大概讲了一下我们怎么用ftp和FileZilla与vsftpd通信看报文,怎么看RFC文档。我们写的服务器是如何用单进程和select实现并发。大概讲了select在我们的服务器里怎么用的。中途被问知道epoll不?我说知道,但是面试官没继续问,让我接着讲服务器。又问为什么不用多线程,我说另外两个同学不熟悉pthread(糊涂了,应该讲讲单进程并发的好处)。。。然后就是问整个服务器性能怎么优化,第一反应什么都没想到。。。经提醒后回答了可以根据文件大小情况预读一部分文件到内存里,这样就不会因为fread的buffer太小反复读硬盘(服务器五一节的时候写好了,没加这个功能,传了个上G的文件,感觉可能有必要加这个功能)。中间扯优化的时候不知道怎么说起加锁上了,于是说了pthread_mutex的用法,又说了pthread_rwlock。最后问了算法,问1亿条短信中怎么找重复出现频率最高的100条短信(也就是这100条其实内容一样)。面试官挺好的,说这个问题可以多想想,没有标准答案,想你觉得最好的方法。最后我说的是数据随机分到多台机器上,每台机器找用hash找100个重复率最高的数据,然后直接把统计结果汇总一下,在一台机器上加起来,汇总后出现最多的100个就是答案了。面试完了想想貌似不对,如果某条前100的短信被比较“平均”地分发到所有机器上,那么可能它不会进入这些机器的前100。。。所以应该就直接每个返回这个机器上所有的统计结果,最后汇总。虽然麻烦,但不会错。最后一个问题是如何存储和查询一个短信是否存在于这1亿条短信中。开始直接说的哈希,然后觉得太low了。。。又说了如果短信内容有由多片段重复组成的,可以采用类似字典树的方法,还能省些空间,然后画了一下示意图。现在想想直接哈希肯定不行的,因为数据量太大(后来知道了一个东西叫Hash Tree,感觉用来解决这个问题或许可行)。二面大概半小时。

感觉就是最后的开放性问题回答不太好。平时这类问题没看过,学校课程和项目中也不出现这类问题,是个短板。问最近干什么居然说的做学校课设也是low爆了,应该说在学些什么技术之类的。。。不过这也是实话是说吧,这学期专业必修课开太多了,光是作业和实验就挺忙了,加上找实习,确实就没学新东西。

HR面

HR面主要就是问问爱好,家在哪里,有女朋友么,聊点学校学习的事情,喜欢哪个城市。可能因为以前在高考人口大省上学,又聊了学校和为什么学计算机。HR居然还问了简历上的比赛经历,这个我很意外。HR挺友好,知道我们学校在郊区,进来面试时候给了我一瓶宾馆的矿泉水。

HR面没什么感觉。。。不知道刷人的标准是什么,我猜测是主要依据是前两面表现,另外看性格和交流能力吧。

最后过了一周,在五一前通知面试过了,分配到了TEG做后台开发。也总算是告一段落了。运气不错的是我认识的另外两个人也过了面试。

今年3月初大概6号的时候在校园BBS论坛上找到了一个学姐帮忙内推阿里巴巴。过了约10天开始进入了内推面试流程。人生头一次面试一个公司并且一路面到了HR面,按照HR的意思是最后一面了,据说面试结果要过一段时间才会通知(该不会挂了吧-_-||)。不管怎样,这是人生一次宝贵的经历,记下这次经历,以时刻警醒自己。

大致情况


在学校情况大概是这样:之前只做过1个项目(安卓恶意APP批量检测工具,用了机器学习方法),参加过数学建模得了两个校内赛奖和省三等奖,和同学5人参加过15年和16年ASC竞赛(不过都没进决赛),大一得了国奖,大二得了学校的一等奖学金。技术上兴趣还是自认为比较广泛的,虽然之前搞过的安卓弃坑了。比较擅长C/C++,Java与python略懂。我比较赞同在理论知识掌握牢固的前提下进行技术学习(技术指某门具体的应用技术),多看书,不要“无脑”的写代码。

总之技术经历挺平淡的,挺像一个读书读多的“书呆子”,并没有一堆吊炸天的项目和开源的东西。

我投的是C/C++研发。我没选意向部门,简历被支付宝拿到了(但简历里有个问题是你想到阿里哪个部门,我写的阿里云,因为知道他们用C/C++,而且了解到貌似他们主要做云计算和大数据平台这块)。

初面

初面面试官问的问题都很偏向基础的技术知识。正因如此,我对初面的印象最深刻。听声音面试官应该比较年轻。第一次打电话是周三,当时在上课,幸好坐在教室中间周围全是人,装作捡东西接听了一下,告诉他我在上课(当时傻了,没约下次电话时间)。第二次就是一周后的周五晚上快十点了(应该在加班吧)。那个时候打电话我挺惊讶的,也挺感动的(那么忙还要面试实习生)。

问题有这么几个:

  1. 自我介绍。我主要把我上面的大致情况讲了一下,但没说我对学习技术的看法,只谈了获奖和项目经历。
  2. 谈谈傅里叶变换。不会。其实之前在知乎和推酷上看过一个带动图的回答,还是有些印象,但是仅仅听说过的话还是就说不会好点。然后面试官问我你们专业都学过哪些数学课,我说微积分,线性代数,概率论与数理统计、离散数学,然后就没再问数学了。
  3. 听说过或用过C++的NIO网络模型没。我说我只知道Java有NIO一个包,C++下好像没这个东西,我不知道。然后就没再问了。我总怀疑是不是听错了,因为后来查了下,确实没C++的NIO。
  4. 讲讲快速排序。这个答的自我认为比较好,因为以前还专门写了博客。然后让在collabedit.com上手写快排。事后才知道这个网站是专门用来面试的。不过可能晚上打游戏看视频的人太多,网抽风了。我敲好了代码对面说没看见,过了几秒网页就显示链接断开,然后一刷新什么都没了,所以最后面试官还是没看见我写的代码。。。
  5. 用过数据库没。我说mysql。然后让谈谈对mysql锁的认识。不会。
  6. 网络协议栈几层?介绍下。我答的5层(主要好举每层协议的例子)因为做过一个课程设计是从抓的数据包里还原HTTP报文信息,所以答的还可以。
  7. 介绍简历中的项目(比赛中负责什么,C语言套接字编程学到什么程度,课程设计HTTP协议还原,安卓恶意app批量检测工具项目介绍一下)。
  8. 问随机森林算法,怎么训练的,知道过拟合么?我说随机森林不会过拟合。然后问为什么,讲了一通。然后问做项目的时候你们怎么衡量机器学习算法的好坏,我说的是交叉验证+测试准确率,另外可以看TPR,FPR,TNR,FNR,外加ROC曲线和AUC值(顺便解释了ROC曲线是干啥的)。面试官问ROC曲线怎么画的,解释了一下。

面试时间大概40分钟多一点,大概面完15分钟,简历状态就是“面试中”了。

我的感受是,只要项目在一年内做的,没有夸大成分,简历项目问题应该都能答出来。一面偏向基础,常见算法要会。同学有一面问mysql索引的,B+树的,HashMap和ArrayList的,当然他面的Java。只是说做项目要深挖知识点,多思考,注重基础和常考的面试问题。

人生第一次面试,一开始说话都没力气。后面好了一些。

二面

二面面试感觉是最有意思的,面试官也很有意思,问的问题也很不一样(不是那种背书问题)。一开始还是自我介绍。让我说了下OpenMP是什么(主要因为竞赛的时候用了)。快速排序原理(估计一面面试官跟他说了没写代码的事)。怎么并行化快速排序(一时没想起来,有一个专门的并行算法,并不是递归的时候创建线程递归就行),因为不知道怎么答,所以我说并行快速排序容易造成负载不均衡,一般归并排序并行好点。然后就被要求并行化归并排序(我说了一下外部排序和归并过程)。最后又问机器学习。其中一个问题没答出来:ROC曲线如果相交,AUC值也差不多,你怎么判断哪个方法好?

大概时间还是用了40分钟,最后被问实习时间,我说7月1到9月1。然后让我提问,我问了下一共要面试几次(官网上说3次,但网上说4和5次的都有)。

三面

三面是最短的一次,只有20多分钟,面完以为挂了(间隔了一周多才到HR面)。主要问简历项目。面试官很喜欢质疑,比如:你们做的项目学校在项目答辩后看代码么?我说看,因为文档和代码让提交。他说:如果只是走走形势呢?我说做项目时要发论文,老师总要看看东西吧。其实有一点我没说,面试官估计也清楚吧,就是现在学校里有些老师都是到处接活,至于做成啥样,能用,能发文章就行,代码的话很多老师自己都不怎么写了,所以也不关心。反正这个问题个人感觉没答好。

之前还问了:你学技术的动力是什么?我说就是觉得好玩,解决问题很有意思,搞技术挺酷的(实话,当年高考报专业全是计算机和电子信息类的)。貌似面试官表示很怀疑。我相信大部分长期写代码的都是最初觉得好玩才学写代码的。但是,我感觉我理解错他意思了,他应该是想强调学技术是长期的事情,你怎么坚持下来的,这恐怕是他真正关心的。

还有:看你经历挺丰富,万一你哪天对别的东西感兴趣了,那你之前的技术怎么办?我说我并不是见什么都感兴趣,我会对技术有个大致了解再决定是否继续深入。我说了我搞安卓的例子,说不打算搞移动端开发了。又被问为什么?我说我偏向解决基础性的问题,移动端还是比较面向业务和需求,不太符合我解决问题这个兴趣点。看来我是傻了,现在想想,对方是怕你太随性,不能坚持。但我觉得一个程序员应该对新技术始终保持跟进,感兴趣是动力啊。了解一个新技术,不代表你整个技术领域都改变。而是其它技术或许可以为解决问题提供参考和新思路。比如:机器学习技术可以跨界应用到安全领域,一个人做安全的人如果视野很窄不愿意学习,那么很难把这个新兴技术应用到自己的领域。

总之,搞技术的人在非技术的阅读理解和表达上是很容易产生偏差的,要想想再说

HR面

面试官是个大姐。第一次电话是上午上课,幸好坐中间了,假装捡东西,接了电话,约了中午。我以为一点多会打过来,就没睡觉。前一天熬夜了,困得不行,就躺着玩手机。大概是HR午休或者考虑到学生午休吧,两点半打过来了(早知道睡觉了T_T)。

先让自我介绍。然后问我实习动机,我说增加大企业实习经历,因为以前没实习过,也希望获得转正式员工的机会。

之后问技术上关注擅长什么。我说C/C++还有机器学习。于是就问为什么是这2个技术。我说能解决问题,偏向基础,我想做偏基础的东西,偏向业务和应用的不太感兴趣。作了个大死又说了安卓,我说框架什么的这些老是在变,不想被Google这种大公司牵着鼻子走,也不想老是根据需求不停地改app之类的东西。然后被批评了,说技术要面向市场需求。然后问知道阿里做服务器用什么?中间件知道么?我说Java(今年年初发现了阿里好几个团队的博客,所以这个还是知道的)。又问为什么投C/C++?我说这是我擅长的,参加的ASC比赛也是这方面的,兴趣也在这里。结果面试官说你这不是擅长,你是只投你会的,因为你们学校里做项目老师让用什么你们就用什么,学校作业也是C/C++,你的技术视野太窄了,因为你接触的只有这些,所以就只投这些,阿里做服务器的都是Java,C++都是做挖掘用的(但是他们网站上写的分布式,云计算和系统内核)。我就只好说:擅长的肯定是我会的,不会的东西也就谈不上擅长,所以您这么说没错。而且我才大三,就这几年也不可能把所有的技术都了解一遍(我想就算是工作了的人也不会每种技术都懂)。语言只是解决问题的工具,最重要还是能够解决问题的知识和思路。结束时候让我提问,我说了下自己估计的实习时间,问我的岗位转正可以么?回答是表现好才可以,没转正的话校招可以直接开始面试环节,而且更沾光。如果实习时间不能达到2个月,那还不如别来。

技术视野不够这个问题很难界定,因为了解太多会被扣上“多而不精”的帽子,了解太少又成了“不愿学习”,“没有学习新技术的动力”。所以这个问题我就没继续回答。

当时还是被HR的问题弄懵逼了。应该说:招聘网站上写的是做分布式的,然后顺便提一下自己简历上有除了C++外的项目(总感觉他们想找个写Java的)。

对于学校里只用C++这点,完全不对。至少在我们学校,学生工作室全是做Web开发的,都是Java, js, python一类的。学校老师的项目用Java绝对是主流,甚至还有python的(上离散的时候授课老师亲自说他们都用python)。可以说,这几年我是完全凭个人兴趣在学C++。我学Java的时间是大一寒假,虽然比C++晚,但是我并没有只学C++。甚至我的博客里,技术文章中用到Java的占到了一半以上。只是C++是个很大的主题,至今还有很多东西没有学习到,所以没有写很多博客。我为什么学C/C++?原因很简单:它偏向基础,偏向底层。现在的计算机科学世界就是C/C++上搭建起来的。看看Linux/Unix,mysql,nginx,Java虚拟机,各种语言的解释器编译器,深度学习(GPU编程)。如果你不懂C/C++,那么上面所有这些你都别想深入研究。

总结

基本技术要过关。我列个单子,以后继续努力:

  • sql的所有指令都要看一遍,然后要有意识的搜集mysql各种面试问题。
  • 不看书能够手写各种常见算法数据结构,刷题技术过关。我一个同学跟我说程序员怎么能背代码呢?我觉得代码确实不是靠背的,但是作为一个基础有待考察的在校生,还是去准备这些东西吧。
  • C/C++基础。没问的东西不代表以后不考或可以不看。当然C++11/14就不必看了。
  • Linux网络编程,多线程:挑选APUE的部分章节看,看man手册,动手做东西。
  • 网络协议:学校里学的差不多够了。经常问问自己为什么协议设计成了这样,这么做是干什么?据一些同学说《TCP/IP详解1》是好书。
  • 操作系统的知识,课本内容差不多也够了。

还有就是技术兴趣点和学习动机这个问题,真是要想好再回答。我觉得很多人学技术的动机和我差不多,但是真要答好这个问题还是要仔细准备,不然就成了给自己找麻烦了。

Update: 最后拿到了实习,但是因为学校和时间的原因拒绝了这个offer,确实十分可惜。

这学期选上了人工智能这门选修课,最后课程设计里大量用到了随机排列功能(打乱数据用的)。然而自己从来没实现过这类功能。昨天晚上偶然想起这个遗憾的事,于是自己想到一个实现,感觉很好理解也好写,在这里写一下。

道理很简单,只有3步,是个迭代过程。设要打乱的数据有n条。

  1. 判断n是否小于1。如果小于1退出。

  2. 生成1个0到n-1的随机数t,作为下标。

  3. 交换最后一条记录和第t条记录。n=n-1。回到第一步。

代码(C++):

void RandomPermu(int a[], int n){
    while(n>0){
        swap(a[rand()%n], a[n-1]);
        n--;
    }
}