您的位置:首页 > 职场人生

黑马程序员——7K面试

2015-09-16 19:59 429 查看
------Java培训、Android培训、iOS培训、.Net培训、期待与您交流! -------

先解释一下这篇博客的题目。之所以称为7K面试题,就是用人单位承诺如果做出这道面试题目就为面试者提供月薪7000元的工作,因此从薪水的角度来说是非常有吸引力的,但从另一方面来说,这道面试题必然具有一定的难度,而这样的面试方式更能够考察面试者的综合开发能力。
那么这一篇博客中,我们将介绍两个面试题目,由于这两道面试题需要设计两个完整的系统,因此需要详细介绍题意、设计思路,以及相关代码的编写。在此之前,我们通过几个例子来加强面向对象的设计思想,而这一思想对于完成前述系统设计面试题目是非常必要的。

1 面向对象设计思想加强

题目一:“人在黑板上画圆”,应该将画圆的方法定义在谁身上?
思路:上述过程中,涉及到3个对象,分别是人、黑板和圆。面向对象的设计思想中一个非常重要的原则就是谁拥有数据谁就要提供操作该数据的方法。想要画出一个圆,需要两个数据,一个是圆心坐标,另一个是半径。而这两个数据都是属于圆对象的,因此操作这两个数据画出圆的方法就要定义在圆自己身上。
题目二:列车司机刹车。刹车的方法应该定义在哪个对象身上?
思路:从表面上看,是司机把列车停了下来,但实际上司机并没有那么大的力气,把正在高速行驶的列车刹住。那么司机做的事情只不过是给列车下达了停车的命令,而真正使列车刹车的是列车本身,因为只有列车自己才清楚如何抱闸、如何停止引擎的运转等等。
题目三:售货员统计收获小票上的总金额。那么计算总金额的方法又应该定义在哪个对象身上呢?
思路:现实生活中总金额的计算确实是售货员利用计算器或者通过心算计算出来的,但是当我们需要用面向对象的对象进行描述时,就要遵循面向对象设计的原则。每个商品的价格是依次打印在售货小票上的,因此商品价格数据是属于售货小票这个对象的,那么计算总金额的方法当然也是要定义在收获小票对象上。
题目四:在“人关门”的情景中,“关闭”方法应该定义在哪个对象上?
思路:这个题目与“列车司机刹车”是类似的。人对于门来说,只是一个命令的下达者,也就是说,关门的动作实际是门自己完成的,比如,门轴是如何旋转的、门锁是如何自动锁上的等等,这些都是门自己最清楚,因此“关闭”方法自然而然就要定义在“门”对象身上。那么最终实现关门的效果,当然还需要“关闭”方法的调用者,而这个调用者就是“人”。
题目五:“两块石头磨成一把石刀,石刀可以砍树,砍下来的树就变成木材,最终可以将木材做成椅子”。将上述过程通过面向对象的思想,转换为代码编写出来。
思路:上述表述中涉及到多个对象,包括石头、石刀、树、木材,以及椅子。这些对象之间是相互转换的关系,这些转换的动作可以定义在这些对象本身,比如石刀砍树的动作肯定要定义在石刀本身,而有些动作就需要第三方对象的参与,比如石头转换为石刀,这一转换动作不应该定义在石头上,因为石头不能自己把自己变成石刀,这不符合现实规律,这一转换过程对于石头来说是一个被动的过程,因此需要第三方对象。这里我们可以定义一个石刀工厂(KnifeFactory),专门对外提供将石头转换为石刀的方法等等。
依照上述设计思路我们给出如下伪代码例程。
代码1:
Stone firstStone = new Stone();
Stone secondStone = new Stone();
//石刀工厂,将石头转换为石刀
Knife knife =KnifeFactory.createNewKnife(stone);

Tree tree = new Tree();
//石刀具备砍树功能
Woods wood = knife.cut(tree);

//椅子工厂,将木材转换为椅子
Chair chair = ChairFactory.createNewChair(wood);
代码说明:
(1) 由于石刀工厂并不涉及该类的特有数据,因此可以将产生一个新石刀的方法定义为一个静态方法,接收一个石头对象,经方法内部转换(这里不给出具体代码),返回一个石刀对象。按照现实情况石头、树木等原材料并不是创建出来的,但这里为了突出重点,将石头和树木的获取过程简化为创建对象。此外,椅子的创建同样通过椅子工厂类将木材转换为椅子的静态方法来实现。
(2) 对于木材的产生,可以调用石刀对象的cut方法,方法需要传递树木对象作为参数,并返回木材对象。
题目六:“一个小球从一根绳子的一端移动到另一端”。同样将上述过程通过面向对象的思想,转换为代码编写出来。
思路:小球和绳子之间本身是没有任何关系的,但现在题目要求这两个事物之间产生一个关系,那么就要思考这是一个怎样的关系。一个物体的运动总是通过物体所处位置的变换来体现的。小球从位置A变换到位置B就是一次移动。假设我们将小球所处的位置简化为一个点,那么从题意可知这个点是由绳子提供的,因为小球是沿着绳子运动的。那么这个时候,小球和绳子之间就产生了一个关系——小球按照绳子提供的位置移动,或者说绳子为小球的移动提供了方向。在这个关系中,位置就是数据,根据谁拥有数据就要提供对数据进行操作方法的思想,绳子应该对外提供返回下一个位置的方法。由于小球要移动,因此应具备一个移动方法,而方法参数就是绳子提供的位置。
根据上述设计思路,给出如下伪代码例程。
代码2:
//将“位置”这类事物抽象为一个类
ppublic class Point {}
//Rope类
public class Rope {
//绳子的起始位置,以及结束位置
private Point start;
private Point end;
//在构造函数中初始化起始位置和结束位置
public Rope(Point start, Point end) {
this.start = start;
this.end = end;
}

public Point nextPoint(Point currentPoint) {
/*
* 通过某种算法给出相对当前位置的下一个位置
* */
return new Point();
}
}
//小球类
public class Ball {
//将绳子内化为小球对象的成员变量
private Rope rope;
//currentPoint记录小球对象的当前位置
private Point currentPoint;

public Ball(Rope rope, Point point) {
this.rope = rope;
this.currentPoint = point;
}

//“移动”方法,方法参数为小球的当前的位置
//将小球移动到Rope对象返回的相对当前位置给出的下一个位置
//将下一个位置打印到控制台表示移动
public void move(Point currentPoint) {
currentPoint = rope.nextPoint(currentPoint);
System.out.println("小球移动到了"+currentPoint+"位置处");
}
}
代码说明:
(1) Point:将“位置”这一类事物抽象为一个类,称为Point。此类的主要作用就是对外提供一个位置坐标,比如X轴坐标值,以及Y轴坐标值等等,由于这里仅作演示,因此不给出具体的代码。
(2) Rope:将绳子这类事物定义为Rope类。Rope中定义两个Point类型的成员变量,分别表示绳子的起始位置,以及结束位置,这两个成员变量可以在Rope的构造方法中进行初始化,这样就创建了一个新的绳子对象。而Rope的主要方法就是给出相对当前位置的下一个位置,方法参数是由外部传递的当前位置,同样不给出具体的计算方法。
(3) Ball:分别定义Rope类型,以及Point类型的成员变量,这两者均在构造方法中进行初始化。其中Point对象表示当前位置,而Rope对象的作用就是根据当前位置计算下一个位置,然后Ball的move方法就根据外部传入的当前位置,得到下一个位置,并将下一个位置打印到控制台表示小球的移动。

2 7K面试题之一——交通灯系统

2.1 题目

模拟实现十字路口的交通灯管理系统逻辑,具体需求如下:
(1) 异步随机生成按照各个路线行驶的车辆。
例如:
由南向而来去往北向的车辆——直行车辆
由西向而来去往南向的车辆——右转车辆
由东向而来去往南向的车辆——左转车辆

(2) 信号灯忽略黄灯,只考虑红灯和绿灯。
(3) 应考虑左转车辆控制信号灯,右转车辆不受信号灯控制。
(4) 具体信号灯控制逻辑与现实生活中普通交通灯控制逻辑相同,不考虑特殊情况下的控制在逻辑。注:南北向车辆与东西向车辆交替放行,同方向等待车辆应先放行直行车辆而后放行左转车辆。
(5) 每辆车通过路口时间为1秒(提示,可通过线程Sleep的方法模拟)。
(6) 随机生成车辆时间间隔以及红绿灯交换时间间隔自定,可以设置。
(7) 不要求实现GUI,只考虑系统逻辑实现,可通过Log方式展现程序运行结果。

2.2 题意分析

(1) 路线分析

任何一个十字路口,总是分为东南西北4个方向,这4个方向之间进行两两组合总共可以产生3种行走方式,12条路线,3中行走方式分别是:直行,左转和右转。其中直行和左转需要受到交通灯的控制,而右转则不需要。直行路线分别是:南到北、北到南、东到西、西到东;左转路线分别是:南到西、东到南、北到东、西到北;右转路线分别是:南到东、东到北、北到西、西到南。上述12条路线如下图所示。



关于上图我们首先解释一下每个路线的命名方式,以“S2N”为,2的英文单词“two”与“to”同音,因此代表了汉字“到”,整个意思就是从南到北的这条路线,其他路线的名称也是相同的命名方式。以上就是一个普通十字路口通常所具有的12条路线。路线分析完毕,我们接着根据路线分析一下,路线对应红绿灯的变化情况。

(2) 红绿灯变化分析

上图虽然乍看起来非常复杂混乱,但是可以进行一定的简化。比如,“S2N”与“N2S”路线的红绿灯是同时变化的,因此我们只需考虑其中一个路线红绿灯的亮灭情况即可。同理,“S2W”与“N2E”这两条左转路线红绿灯也是同时变化的,因此只需考虑其中一条路线。这样一来,除去右转线路,原来的8条直行、左转路线,现在只需要控制其中4条就可以就可以实现十字路口的正常运行。这里我们选择以下4个方向作为主要控制路线,直行方向:“S2N”和“E2W”;左转方向:“S2W”和“E2S”(这4条路线的选择,只是个人喜好,大家可以选择自己习惯的4个方向,只要方便分析思考即可)。
现在我们来分析右转路线的控制。可能有朋友认为右转路线由于不受红绿灯的控制,因此就不设置红绿灯,这就需要为12条路线设计两种红绿灯模型——有灯模型与无灯模型,较为繁琐。为了简化设计,将模型统一起来,只需将右转路线的红绿灯设置为常绿即可,而其他路线的红绿灯设置为随时间变换。
还有一点非常重要的是,我们需要搞清楚红绿灯的变化顺序,上图中为4条路线标上了序号,这一序号是路线对应红绿灯的变化顺序。比如,两个南北向路线(“S2N”和“N2S”)的红绿灯由绿变为红以后,下一个变灯的路线并非是两个东西向路线(“E2W”和“W2E”),而是左转路线,分别是“S2W”和“N2E”,之后才是两个东西向直行路线,接着又是一对左转路线的红绿灯变换,就此完成了一个红绿灯变化循环,那么这一变化循环的不断重复就是十字路口交通灯正常的运行过程。上述内容可以简单总结为:某个方向的车都走完了,无论直行还是拐弯,才轮到另外方向的车。

2.3 面向对象的分析与设计

根据上述题意分析的内容,我们可以从中提取出涉及其中的若干事物。首先是红绿灯对象,这个不必多说;其次是用于控制红绿灯变换的一个控制装置,这一控制装置的存在也是符合实际情况的,因为红绿灯是不会自己变化的;再次,还要设计上述12条路线;最后,就是在路面上行驶的车辆。下面我们就逐一分析上述4个事物的设计思路,并给出相应的代码。

(1) 红绿灯——TrafficLamp

红绿灯与12条路线是一一对应的,有几条路线就有几个红绿灯对象,因此应该设计12个红绿灯对象,分别控制不同路线上车辆的放行与等待。这可能与现实生活中不太一样,真实的十字路口通常只有4个红绿灯柱,一个灯柱上有两个不停变化的灯——一个负责控制直行方向,另一个控制左转方向。之所以将两种灯合并在一个灯柱上,是为了节省空间,同时也方便司机观察与判断(个人理解),而在灯的内部,直行灯与左转灯也是分开控制的,这与我们的设计思路是一样的。至于4个右转灯,为了统一模型,同样将其设计为一个独立的红绿灯,只不过是常绿的,上文有作说明。
这12个红绿灯对象对于一个指定的十字路口是固定,不能多也不能少,每一个灯对象是相对独立的存在,更不能随外力的意愿而随意改变,因此根据这一设计需求,可以将红绿灯对象设计为一个枚举类型,其中包含12个常量元素。
一个红绿灯通常具有两个基本行为:变红与变绿,而红与绿实际也代表了一个灯的两个状态,相当于是灯所具有的数据,这里我们可以将灯的状态设计为一个布尔变量,false表示红灯,true表示绿灯,而所有红绿灯对象在初始化时默认为红灯。由此可知,红绿灯对象还应对外提供变红与变绿的方法。
我们在前述内容中说道,12条路线最终可以简化为控制其中4条,就可以控制整个十字路口的正常运行。那么如何体现这一点呢?可以在每个红绿灯对象内定义两个成员变量,分别代表对面的灯,以及下一个灯。以“S2N”为例,对面的灯就是“N2S”,而下一个灯就是“S2W”,而“S2W”对面的灯就是“N2E”。当“S2N”对象的变红方法被调用时,该方法内部还应调用对面灯的变红方法,与此同时,还要调用下一个灯的变绿方法。而在变绿方法内部,除了调用对面灯的变绿方法以外,不需要调用下一个灯的变红方法,原因有二:第一,除了当前灯和对面灯以外,其他灯默认是红色的;第二,如果设计成变红方法内调用其他灯变绿方法,而在变绿方法内又调用其他灯变红方法,不同对象之间就会出现方法的无穷调用现象,最终抛出内存溢出异常。
此外,红绿灯对象还要对外提供返回当前此灯红绿状态的方法,返回值类型就是表示红绿状态的false和true。通过该方法,可以判断能否令某个路线上的车辆通行。
由此总结,一个红绿灯对象中需要定义3个成员变量,表示红绿状态的布尔变量,分别代表对面灯和下一个灯红绿灯类型的引用,并对外提供返回红绿状态的方法,以及变红和变绿的方法。以下给出TrafficLamp的相关代码。
代码3:
//将红绿灯定义为一个枚举
public enum TrafficLamp {
//4个主灯
S2N("N2S","S2W", false), S2W("N2E", "E2W", false),E2W("W2E", "E2S", false), E2S("W2N","S2N", false),
//4个副灯
N2S(null, null, false), N2E(null, null, false), W2E(null, null, false),W2N(null, null, false),
//4个右转灯
S2E(null, null, true), E2N(null, null, true), N2W(null, null, true),W2S(null, null, true);

//三个成员变量:灯的当前状态、对面灯,以及下一个灯
private boolean lampState;
private String opposite;
private String next;

private TrafficLamp(String opposite, String next, boolean lampState) {
this.opposite = opposite;
this.next = next;
this.lampState = lampState;
}

public boolean isGreen() {
return lampState;
}
public void turnGreen() {
this.lampState = true;
System.out.println(name()+" is green");

if(opposite != null) {
TrafficLamp oppositeLamp = TrafficLamp.valueOf(opposite);
oppositeLamp.turnGreen();
}
//turnGreen方法中,仅把对面灯同时变绿
//而不调用下一个灯的变红方法,避免方法的无穷调用
}
public TrafficLamp turnRed() {
this.lampState = false;
System.out.println(name() + " is red");

if(opposite != null) {
TrafficLamp oppositeLamp =TrafficLamp.valueOf(opposite);
oppositeLamp.turnRed();
}

TrafficLamp nextLamp = null;
if(next != null) {
nextLamp = TrafficLamp.valueOf(next);
nextLamp.turnGreen();
}
//将下一个灯变绿以后,返回下一个灯对象
return nextLamp;
}
}
代码说明:
a) TrafficLamp枚举中定义有12个常量元素,分别对应上图中所绘制的12条行驶路线,包括4个主灯,4个副灯,以及4个右转灯。其中主灯就是红绿灯控制器主要需要控制的那4个灯,主灯可以按照自己的喜好进行选择,但一定要注意两相对方向的一对红绿灯中只能选择其中一个作为主灯,比如“S2N”和“N2S”这一对灯中,只能选择其中之一作为主灯,另一个自然就成为了副灯。并且,副灯一定要与主灯绑定,也就是主灯对象中一定要有指向对面灯(副灯)对象的TrafficLamp类型引用(成员变量)。
TrafficLamp默认的构造方法需要指定对面灯(副灯)、下一个受控红绿灯,以及灯的初始化状态,除了4个右转灯以外,其他灯默认为红灯。在以上代码中,只为4个主灯指定了对面灯和下一个灯,而初始化其他8个灯的时候,这两个参数则指定为了null。对此,右转灯是显然的,而对于副灯,这样做的目的是为了避免变绿/变红方法的无穷调用。
b) 由于副灯和右转灯的对面灯以及下一个灯均为null,因此调用turnGreen和turnRed方法时,需要进行非null判断。
c) 大家可能会有这样的疑问:为什么不把对面灯和下一个灯对象直接在构造方法中通过valueOf方法获得,而是每次在调用turnGreen/turnRed方法之前临时获取到这两个对象呢?因为12个TrafficLamp类型的常量元素是在加载TrafficLamp枚举时才逐一进行初始化的。大家要注意要逐一这两个字,以“S2N”元素为例,它的对面灯是“N2S”,而下一个灯是“S2W”,那么在初始化“S2N”元素完毕以前,“N2S”以及“S2W”元素实际都是不存在的,还未被创建出来,那么这个时候调用valueOf方法尝试获取这两个TrafficLamp类型常量元素必然抛出异常。因此表示对面灯和下一个灯的成员变量,其类型为字符串。

(2) 红绿灯控制器——TrafficLampController

红绿灯控制器,顾名思义,它的主要作用就是控制红绿灯的变化。具体的说,就是每隔一个固定长度的时间(每个红绿灯都是相同的),就变换某个方向上的红绿灯,具体来说就是调用某个红绿灯对象的变红或变绿方法。为了使得整个红绿灯系统正常运转起来,需要为控制器指定一个初始红绿灯对象,作为当前红绿灯,比如可以指定“S2N”为初始当前受控红绿灯,并且在初始化控制器对象时,调用当前灯的变绿方法。而控制器的主要方法就是每隔一个固定时间以后(比如10秒),控制器就调用当前红绿灯对象的变红方法。需要注意的是,变红方法应返回当前红绿灯的下一个红绿灯对象,并将其设置为当前红绿灯,如此循环往复,即可实现十字路口红绿灯的正常运转。以下给出红绿灯控制器类的相关代码。
代码4:
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;

public class LampController {
//TrafficLamp类型成员变量,用于指向当前受控红绿灯对象
privateTrafficLamp currentLamp;

public LampController() {
//将当前受控红绿灯对象,默认初始化为“S2N”,并将其置为绿灯
this.currentLamp = TrafficLamp.valueOf("S2N");
currentLamp.turnGreen();

start();
}

public void start() {
ScheduledExecutorService threadPool =Executors.newScheduledThreadPool(1);
threadPool.scheduleAtFixedRate(new Runnable() {

@Override
public void run() {
//当前受控红绿灯对象,在每次调用其变红方法后得到更新
currentLamp =currentLamp.turnRed();
}

},
10,
10,
TimeUnit.SECONDS);

}
}
代码说明:
a) 当前受控红绿灯对象的初始化值可以根据喜好选择,但一定要在4个主灯中选择,并且初始化以后,一定要将其状态置为绿灯,表示从此灯开始红绿灯变换循环。
b) 控制器对象的作用非常简单,就是定时将当前受控红绿灯对象变为红灯,这里我们利用调用线程池实现需求,也可以利用Timer和TimerTask来完成。
c) 变灯间隔时间可以任取,但是要符合实际情况。

(3) 马路——Road

由于有12条路线,因此相应的要创建12个Road对象。Road对象最重要的功能就是在每条路线中存储一定数量的车辆,这可以通过一个List集合来实现,因为必须要保证每辆车要按照进入路口的顺序排列等待。同时对外提供增加车辆以及减少车辆数目的方法。之所以要将这两个方法定义在路线对象中,同样是根据谁拥有数据,谁就提供操作这一数据方法的原则。这样做是显然的,因为车辆永远是在马路上行驶的,那么某条路线上车辆的个数肯定要作为这条路线对象上的数据,进而就应该将增减车辆个数的方法定义在马路对象上。
对于一个路口来说,车辆总是从四面八方源源不断的汇入车辆队列的,不同车辆进入到队列中的时间不同,具有很大的随机性,为了模拟这一效果,可以将增加车辆的方法,设计为每隔一段随机长度的时间,就产生一个新的车辆并存储到集合中。该方法的调用不受任何外力的限制,并且一经调用,就没有必要结束调用。
关于减少车辆的方法,车辆的放行总是要在当前路口红绿灯由红变为绿以后发生,并且并非是当前路线上的所有车全部同时放行,而是按顺序从路口第一辆车开始依次放行,因此在绿灯期间可以循环调用车辆集合的remove(0),不断删除第一辆车,实现车辆依次驶离路口的效果。由此可知在放行车辆以前需要进行两次判断,首先判断当前路口是否是绿灯,其次判断路线集合中是否包含有车辆。

(4) 车辆——vehicles

车辆,更准确的说是车辆的个数,作为上述情景中唯一的数据,是没有必要专门为其设计一个类的,因为本题的核心问题,是红绿灯的变化逻辑,捕捉某个路线上减少一辆车的瞬间,并不需要模拟车辆由某个路口行驶到另一个路口的过程,因此对于车辆只需要体现其个数简单的增减效果即可。这也就是定义一个List集合,以此实现车辆增减的原因。而车辆本身可以以一个字符串的形式体现,字符串中包含车辆编号(按照创建顺序),以及路线名称等信息,这样在打印Log时可以更为直观的观察车辆的等待与放行信息。
以下给出Road类的相关代码,这其中也包含了车辆集合的定义与操作相关代码。
代码5:
import java.util.ArrayList;
import java.util.List;
import java.util.Random;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class Road {
//roadName与对应红绿灯的名称相同,比如“S2N”
private String roadName;
//车辆集合
private List<String> vehicles = new ArrayList<String>();

public Road(String roadName) {
this.roadName = roadName;

//初始化Road对象的同时,
//先后调用创建新车到队列中,以及定时判断当前路线红绿灯状态的方法
generateNewVehicle();
checkLamp();
}

//每隔一个随机时间就添加一个新的车辆到车辆集合中
//该方法显然需要一个单独的线程控制,利用线程池实现
public void generateNewVehicle() {
ExecutorService threadPool = Executors.newSingleThreadExecutor();
threadPool.execute(new Runnable() {
Random rand = new Random();

@Override
public void run() {
//随机创建1000辆车
for(int i=0; i<1000; i++) {
try {
//1到10秒之内出现一个新的车辆
Thread.sleep((rand.nextInt(10)+1)*1000);
} catch (InterruptedException e){
e.printStackTrace();
}
//车辆信息中包含序号、路线名称等信息
vehicles.add(roadName+" : No."+(i+1)+" vehicles ismoving.");
}
}

});
}
//每隔一秒检查当前路线红绿灯是否为绿灯
public void checkLamp() {
ScheduledExecutorServicethreadPool = Executors.newScheduledThreadPool(1);
threadPool.scheduleAtFixedRate(new Runnable() {

@Override
public void run() {
//首先判断车辆集合中是否包含等待车辆
if(vehicles.size()> 0) {
//获取到当前路线对应的红绿灯对象
//进而获取到该红绿灯对象的状态
TrafficLamp currentLamp =TrafficLamp.valueOf(roadName);
boolean lampState =currentLamp.isGreen();
//判断当前灯是否为绿
if(lampState)
System.out.println(vehicles.remove(0));
}
}
},
1,
1,
TimeUnit.SECONDS);
}
}
代码说明:
a) 为了体现面向接口编程,以上代码中将成员变量vehicles的类型定义为了List,具有更好的扩展性。
b) 一个路线对应一个红绿灯,因此成员变量roadName的值与对应红绿灯对象的名称相同,需要在构造方法中初始化。
c) 不定义具体的车辆类,而是以包含有车辆信息的字符串代替,因此车辆集合的实际类型参数为String。
d) 以上代码中,创建新车的方法以及检查当前路口红绿灯颜色方法,均是定时执行方法,但用了两种不同的方式实现,前者利用线程对象的sleep方法控制执行节奏,而后者直接创建了一个调度线程池,更为方便直观的设置方法执行周期。这里也可以利用Timer和TimerTask来实现相同的功能,留给大家自行尝试。
e) 关于remove(0),由于车辆的等待队列遵循先进先出的原则,因此总是从集合的头部移除元素,而remove方法的将会返回被删除元素本身,因此可以方便的打印车辆的等待/行驶信息。
至此,就将实现红绿灯功能的业务逻辑代码编写完毕了,下面我们再编写一个测试代码,来测试以上代码能够正常执行。

2.4 测试

测试类的主要作用就是创建12个路线对象,然后再创建一个红绿灯控制器对象,如下代码所示。
代码6:

import java.io.FileOutputStream;
import java.io.ObjectOutputStream;
import java.io.Serializable;

public class Test implements Serializable{

public static void main(String[] args) {
String[] roads = {"S2N", "S2W", "E2W","E2S",
"N2S","N2E", "W2E", "W2N",
"S2E","E2N", "N2W", "W2S"};

for(int i=0; i<roads.length; i++) {
new Road(roads[i]);
}

new LampController();
}

}
代码说明:
a) 测试类之所以这么简单的原因是,很多方法的调用是直接在Road类、TrafficLampController类的构造方法中完成的,因此都被隐藏了起来。
b) 若手动编写12个创建Road对象的代码,看起来非常繁琐,还容易出错,因此可以利用代码6的方法,半自动的完成这一操作。

小知识点1:
这里有一个小技巧告诉大家。如果需要将某段代码中重复出现的内容替换为别的内容,比如将所有的“,”替换为“.”,可以进行如下操作:在eclipse界面顶部的菜单栏中,选择“Edit”——>Find/Replace,打开“Find/Replace”对话框,Find栏中键入“,”,Replace with栏中键入“.”。如果想要从前往后进行替换就选择“Direction”的“Forward”,如果全文替换可以选择“Scope”的“All”,也可以先选择一行内容,然后选择“Selected
lines”,最后既可以通过不断点击“Find”按钮将所有内容进行替换,也可以点击“ReplaceAll”一次性替换所有内容。

部分执行结果如下:
S2N is green
N2S is green
S2N : No.1 vehicles is moving.
N2S : No.1 vehicles is moving.
N2S : No.2 vehicles is moving.
S2E : No.1 vehicles is moving.
S2N : No.2 vehicles is moving.
W2S : No.1 vehicles is moving.
W2S : No.2 vehicles is moving.
S2E : No.2 vehicles is moving.
E2N : No.1 vehicles is moving.
N2W : No.1 vehicles is moving.
S2N is red
N2S is red
S2W is green
N2E is green
S2W : No.1 vehicles is moving.
N2E : No.1 vehicles is moving.
N2E : No.2 vehicles is moving.
S2E : No.3 vehicles is moving.
W2S : No.3 vehicles is moving.
N2W : No.2 vehicles is moving.
S2W : No.2 vehicles is moving.
W2S : No.4 vehicles is moving.
E2N : No.2 vehicles is moving.
E2N : No.3 vehicles is moving.
S2W is red
N2E is red
E2W is green
W2E is green
E2W : No.1 vehicles is moving.
W2E : No.1 vehicles is moving.
E2W : No.2 vehicles is moving.
W2E : No.2 vehicles is moving.
E2W : No.3 vehicles is moving.
W2E : No.3 vehicles is moving.

从执行结果来看,首先是“S2N”和“N2S”两个方向的灯变绿,随后这两个方向以及4个右转方向的车陆续放行,10秒钟以后,这两个方向的灯变红,“S2W”和“N2E”这两个方向的灯变绿,因此这两个路线以及4个右转方向的车被放行,紧接着是“E2W”和“W2E”方向的灯变绿,就此循环往复,实现了十字路口红绿灯系统的功能。

3 7K面试题之二——银行业务调度系统

3.1 题目

模拟实现银行业务调度系统逻辑,具体需求如下:
(1) 银行内有6个业务窗口,1-4号窗口为普通窗口,5号窗口为快速窗口,6号为VIP窗口。
(2) 有三种对应力类型的客户:VIP客户、普通客户、快速客户(办理如交水电费、电话费之类业务的客户)。
(3) 异步随机生成各种类型的客户,生成各类型用户的概率比例为:
VIP客户:普通用户:快速客户 = 1:6:3。
(4) 客户办理业务所需时间有最大值和最小值,在该范围内随机设定每个VIP客户以及普通客户办理业务所需的时间,快速客户办理业务所需时间为最小值(提示:办理业务的过程可通过线程Sleep的方式模拟)。
(5) 各类型客户在其对应窗口按顺序依次办理业务。
(6) 当VIP(6号)窗口和快速业务(5号)窗口没有客户等待办理业务的时候,这两个窗口可以优先处理普通客户的业务,而一旦有对应的客户等待办理业务的时候,则优先处理对应客户的业务。
(7) 随机生成客户时间间隔以及业务办理时间最大值和最小值自定,可以设置。
(8) 不要求实现GUI,只考虑系统逻辑实现,可通过Log方式展现程序运行结果。

3.2 题意分析

去过银行的朋友肯定知道,通常在银行门口会有一个取号机器,客户来到银行以后,首先要在取号机器取到号码,如果等待客户较多,那么可以先在客户休息区等待。经过一段时间以后,若某个业务窗口叫到号码,那么此号码对应的客户就可以到相应的窗口办理业务。那么从以上情景中,我们可以抽取出以下几个需要描述的事物:客户、取号机,以及业务窗口。我们先来说说取号机。

(1) 号码生成器 & 管理器

通常银行取号机主页面会有若干选项,不同的选项代表了不同类型的业务,以此来区分不同的客户。将大量客户进行分流,分到不同的等待队伍中去。题目中提到的三种客户,实际就是三种不同的业务,由于三类客户的排队等待是互相独立,互不干扰的,那么对应的就有三个不同的选项。那么这三种不同的选项落实到程序设计中,就应设计3种号码生成器,分别用于产生三类不同的号码,每产生一个号码就相当于出现了一个新的客户。而这3个号码生成器并非是独立工作的,而应该由一个号码生成器管理器(以下简称管理器)来统一管理,而这个管理器就可以理解为银行门口的取号机,由于这个管理器的唯一性,因此在程序设计中可以考虑使用单例设计模式。

(2) 业务窗口

业务窗口的作用当然是为客户办理业务,但是这道题目中,我们并不需要模拟业务的办理过程(仅通过线程的Sleep方法替代这一过程),只需要实现其叫号的过程即可。由于存在3种客户,对应3种号码生成器,自然也就有3种对应的不同业务窗口。按照题意每种业务窗口,优先获取对应类型等待客户号码,比如VIP窗口优先获取VIP等待客户号码等等,如果没有对应类型的等待客户,则自动尝试获取普通等待客户号码。由于等待客户号码全部存储在号码生成器中,而号码生成器又由管理器同一管理,因此业务窗口就需要通过管理器来访问号码生成器,进而获取到等待客户号码,这再一次体现了谁拥有数据谁就要提供操作该数据的方法。

(3) 客户

与交通灯管理系统类似,不必专门设计客户类,它只需要作为一个数据存在即可(具体说就是号码生成器的成员变量),表示客户号码。那么客户来到银行后使用取号机取号的过程,可以简化为号码生成器对象的一个方法——生成新号码的方法,在该方法内此客户号码将自增,并存储到一个客户号码集合(List集合)中。而业务窗口从取号机取号的过程,就是号码生成器的另一个方法——从号码集合头部移除号码,并返回。下图表示了号码生成器、管理器以及业务窗口三者之间相互的调用关系。



上图中NumberManager表示管理器,或称为取号机,内部包含三种号码生成器对象(分别是普通客户号码生成器、VIP客户号码生成器,以及快速客户号码生成器),对外提供获取这三种号码生成器的方法,由于管理器的设计将利用单例设计模式,因此需要对外提供获取管理器实例的静态方法——getInstance(),静态方法名称标记下划线;NumberGenerator表示号码生成器,对外提供生成新号码的方法——generateNumber,该方法面向客户,此外还提供业务窗口获取等待号码的方法fetchNumber,该方法面向业务窗口。ServiceWindow,表示业务窗口。
从上图三者的关系可知,业务窗口需要同时与管理器和客户打交道。一方面需要访问管理器,通过管理器访问号码生成器,进而获取等待客户号码;另一方面,获取到等待客户号码后,就要着手为持有该号码的客户提供服务。

3.3 面向对象的设计与相关代码

(1) 号码生成器——NumberGenerator

正如前文所述,号码生成器中定义有整型成员变量,用于记录该号码生成器中最新生成的号码。此外还需定义一个用于存储新生成号码的List集合。如前所述,该类应提供两个方法,一个是面向客户生成新号码的方法,另一个是面向业务窗口获取等待号码的方法。下面给出相关代码。
代码7:

import java.util.ArrayList;
import java.util.List;

public class NumberManager {
//起始号码从1开始
private int lastNumber = 1;
private List<Integer> queueNumber = newArrayList<Integer>();

public synchronized Integer generateNewNumber() {
queueNumber.add(lastNumber);
return lastNumber++;
}

public synchronized Integer fetchServiceNumber() {
if(queueNumber.size() > 0)
return queueNumber.remove(0);
else
return null;
}
}
代码说明:
a) generateNewNumber方法需要返回最新生成的号码,以便于打印等待客户的信息,观察程序的执行情况。
b) 由于新号码的生成,以及业务窗口获取等待号码的操作是相互独立,互不干扰的,因此这两个方法需要被两个线程所调用,由于这两个方法要同时操作同一个共享资源——List集合,因此这两个需要同步。
c) 业务窗口从等待队列中获取等待客户号码的方式,与上一个例子中绿灯后车辆放行的方式是一样的——均通过调用remove(0)的方式,从队列头部移除元素,并获取被移除元素,打印相关信息。
d) 无论是generateNewNumber还是fetchServiceNumber方法,返回值类型一定要定义为Integer,而不能是int,因为有可能remove(0)的返回值为null,而null是不能自动拆箱为int值而抛出NumberFormatException异常。

(2) 管理器——NumberManager

号码生成器管理器的主要作用就是将底层的3个号码生成器隐藏起来,并分别面向客户和业务窗口提供访问这三个号码生成器的途径。因此需要在NumberManager类的成员位置创建3个NumberGenerator对象,对应3种不同的号码生成器,并对外提供访问这3个号码生成器对象的方法。最后,NumberManager应被设计成单例。以下给出相关代码。
代码8:
public class NumberMachine {
//3中不同的号码生成器对象
private NumberManager commonManager = new NumberManager();
private NumberManager expressManager = new NumberManager();
private NumberManager vipManager = new NumberManager();

//单利设计模式
private static NumberMachine instance = new NumberMachine();

private NumberMachine() {}

public NumberManager getCommonManager() {
return commonManager;
}
public NumberManager getExpressManager() {
return expressManager;
}
public NumberManager getVIPManager() {
return vipManager;
}

public static NumberMachine getInstance() {
return instance;
}
}

(3) 业务窗口——ServiceWindow & CustomerType &Constants

业务窗口分为三种,不同窗口接待不同类型的客户,因此当我们创建业务窗口对象时,需要为他们贴上标签,标明它们是哪种窗口。我们可以从客户角度出发定义一个代表客户类型的枚举,内含3种常量元素,分别是表示普通用户的COMMON、表示VIP用户的VIP,以及表示快速用户的EXPRESS。下面给出相关代码。
代码9:
public enum CustomerType {
COMMON("普通"),VIP("VIP"),EXPRESS("快速");

private String typeName;
private CustomerType(String typeName) {
this.typeName = typeName;
}

public String toString() {
return typeName;
}
}
代码说明:
为每个元素初始化对应的中文名称,方便打印程序的运行信息。随后在定义ServiceWindow类时,在成员位置上定义一个CustomerType类型的成员变量,用以标明此ServiceWindow对象是3种窗口类型中的哪一种。标明窗口类型的目的在于,窗口对象本身要根据自己的窗口类型从管理器中获取对应类型的号码生成器对象,进而获取对应类型的等待客户号码。
ServiceWindow类的主要功能就是根据自己的窗口类型,从管理器中获取对应类型号码生成器,进而获取对应类型等待客户号码。如果没有对应类型的等待客户,就尝试获取普通等待客户。在获取到等待客户号码后,就为客户提供相应服务,根据题意,处理快速客户时间为固定值1秒钟,而处理其他两种客户的时间为1-10秒钟之间的随机值,因此同样需要窗口类型来判断服务时间。以下给出相关代码。
代码10:
import java.util.Random;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class ServiceWindow {
private CustomerType type = CustomerType.COMMON;
private int windowID = 1;

public void setType(CustomerType type) {
this.type = type;
}
public void setWindowID(int windowID) {
this.windowID = windowID;
}

public void start() {
ExecutorService threadPool = Executors.newSingleThreadExecutor();
threadPool.execute(new Runnable() {

@Override
public void run() {
while(true) {
provideService(type);
}
}

});
}
private void provideService(CustomerType type){
//标明本窗口类型及编号
String windowName = "第" + windowID + "号" + this.type + "窗口";
//根据客户类型参数,获取相应类型的等待客户号码
Integer serviceNumber = getServiceNumber(type);

System.out.println(windowName+"尝试获取"+type+"任务...");
//若获取到相应类型客户,则开始提供服务
if(serviceNumber != null) {
System.out.println(windowName+"已获取到第"+
serviceNumber+"个"+type+"客户");
//记录服务所耗费的时间
long beginTime = System.currentTimeMillis();
analogServicing(type);//模拟提供服务
long costTime = System.currentTimeMillis() - beginTime;
System.out.println(windowName+"为第"+serviceNumber+"个"+type+"客户完成服务,耗时"+costTime/1000+"秒");
} else {//若未能获取到相应类型等待客户,则尝试服务普通客户
System.out.println(windowName+"没有获取到"+type+"任务");

if(type != CustomerType.COMMON)
provideService(CustomerType.COMMON);
else {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}

}
private Integer getServiceNumber(CustomerType type) {
switch(type){
case COMMON :
return NumberMachine.getInstance().getCommonManager().fetchServiceNumber();
case VIP :
return NumberMachine.getInstance().getVIPManager().fetchServiceNumber();
case EXPRESS :
return NumberMachine.getInstance().getExpressManager().fetchServiceNumber();
default :
return null;
}
}
private void analogServicing(CustomerType type) {
long serverTime = 0;
//若客户类型为非快速类型客户,服务时间为1-10s中的随机值
if(type != CustomerType.EXPRESS) {
int maxRandomTime = Constants.MAX_SERVICE_TIME -Constants.MIN_SERVICE_TIME;
serverTime = new Random().nextInt(maxRandomTime) + 1 +Constants.MIN_SERVICE_TIME;
} else {
//若为快速客户,服务时间则为固定为1s
serverTime = Constants.MIN_SERVICE_TIME;
}
try {
Thread.sleep(serverTime);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
代码说明:
a) 名为windowID的整型变量,用于区分4个普通窗口,初始化值为1。而CustomerType类型变量type的初始化值为COMMON,为方便创建另外两种业务窗口对象,为windowID和type变量提供设置方法,当然这两个成员变量也可以在构造方法中进行初始化。
b) 这里给出的ServiceWindow代码与张孝祥老师给出的代码有所不同。张老师的代码中,分别为3种类型的窗口给出了3种提供服务的方法,分别是commonService、expressService,以及vipService。但是3种服务提供方法中有很多重复代码,因此代码复用率不高。我给出的代码是根据传递的窗口类型参数来获取相应类型的等待客户号码和服务相应类型客户所需的时间。
c) 关于当未能获取本窗口类型客户时的处理方式:在未能获取到本窗口类型相同的客户后,首先判断本窗口类型是否是普通类型,若不是普通类型,则继续调用provideService方法,而传递的客户类型为COMMON,尝试服务普通客户;如果本窗口类型就是COMMON,则休息1s种后继续尝试获取等待客户号码。
d) provideService方法是在start()方法内的无限循环中不断被调用的,并且开启了一个单独的线程执行,以此来模拟业务员在业务窗口中不断叫号的过程。
e) getServiceNumber方法中使用了Switch语句来获取与窗口类型对应的等待客户号码,可以说枚举与Switch语句是绝配,在case关键词后面甚至都不需要给出枚举名,直接编写元素名即可。
f) 在analogServicing方法内,为了计算服务时间使用到了Constants类的两个常量字段——MAX_SERVICE_TIME以及MIN_SERVICE_TIME,分别表示最长服务时间和最短服务时间,以这样的方式编写代码,比起直接在代码中编写10000(表示10s)和1000(表示1s)具有更好的阅读性,而且一次编写完毕后,后期如果有需要可以反复使用。这里给出Constants类的代码。
代码11:
public class Constants {
public static int MAX_SERVICE_TIME = 10000;
public static int MIN_SERVICE_TIME = 1000;
public static int BASETIME = 1;
}
代码说明:
代码11中BASETIME表示出现一个新普通客户的所需时间为1s,这是一个基本时间,快速客户为基本时间的2倍,而出现一个VIP客户的所需时间为基本时间的6倍,这是根据题意推出的。题目中提到VIP客户、普通用户、快速客户的出现概率比例为1:6:3,也就是说出现一个VIP客户时,出现6个普通客户和3个快速客户,那么出现单个VIP客户、普通客户,以及快速客户所需时间的比值为1:1/6:1/3,转换为整数就是6:1:2。
至此,完成题目需求所需的所有类就均设计完毕,下面我们通过一个测试类来测试以上类的代码执行情况。

3.4 测试

测试类的主要工作分为两部分,第一部分是创建6个业务窗口对象,并分别调用这些对象的start()方法,使它们开始工作——不断尝试从号码生成器中获取对应类型等待客户号码;第二部分是开启一个容量为1的调度线程池,向线程池中先后提交3个任务,这3个任务分别用于定时创建3种客户,并将新客户号码不断添加到客户队列中(也就是号码生成器对象内的List集合中)。以下给出测试代码。
代码12:
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;

public class MainClass {
public static void main(String[] args) {
ServiceWindow window = null;
//创建4个普通窗口
for(inti=0; i<4; i++) {
window = new ServiceWindow();
window.setType(CustomerType.COMMON);
window.setWindowID(i+1);
window.start();
}
//创建VIP窗口
window = new ServiceWindow();
window.setType(CustomerType.VIP);
window.setWindowID(1);
window.start();
//创建快速窗口
window = new ServiceWindow();
window.setType(CustomerType.EXPRESS);
window.setWindowID(1);
window.start();

ScheduledExecutorService threadPool =Executors.newScheduledThreadPool(1);
threadPool.scheduleAtFixedRate(new Runnable() {
//添加普通客户
@Override
public void run() {
Integer customerNumber = NumberMachine.getInstance().getCommonManager().generateNewNumber();
System.out.println("第"+customerNumber+"号普通客户正在等待...");
}

},
Constants.BASETIME,
Constants.BASETIME,
TimeUnit.SECONDS);

threadPool.scheduleAtFixedRate(new Runnable() {
//添加VIP客户
@Override
public void run() {
Integer customerNumber = NumberMachine.getInstance().getVIPManager().generateNewNumber();
System.out.println("第"+customerNumber+"号VIP客户正在等待...");
}

},
Constants.BASETIME * 6,
Constants.BASETIME * 6,
TimeUnit.SECONDS);

threadPool.scheduleAtFixedRate(new Runnable() {
//添加快速客户
@Override
public void run() {
Integer customerNumber = NumberMachine.getInstance().getExpressManager().generateNewNumber();
System.out.println("第"+customerNumber+"号快速客户正在等待...");
}

},
Constants.BASETIME * 2,
Constants.BASETIME * 2,
TimeUnit.SECONDS);
}
}
代码说明:
a) 由于ServiceWindow类type成员变量的初始值就是CustomerType.COMMON,因此创建4个普通窗口时,不必再设置type的值,而只需设置windowID即可。对于VIP窗口和快速窗口,需要设置type值,而windowID只需沿用默认值1即可。
b) 正如前文所述,每当需要创建一个新客户时,首先要通过管理器对象获取到对应类型的号码生成器对象,进而调用generateNewNumber方法生成一个新的号码,也就相当于创建了一个新的客户。
部分执行代码如下:
第1号VIP窗口尝试获取VIP任务...
第1号VIP窗口没有获取到VIP任务
第1号VIP窗口尝试获取普通任务...

第1号VIP窗口尝试获取普通任务...
第1号VIP窗口没有获取到普通任务

第4号普通窗口没有获取到普通任务
第1号普通窗口已获取到第1个普通客户
第1号快速窗口尝试获取普通任务...
第1号快速窗口没有获取到普通任务

第2号普通客户正在等待...
第1号快速客户正在等待...

第3号普通客户正在等待...
第4号普通窗口尝试获取普通任务...
第4号普通窗口已获取到第3个普通客户
第1号快速窗口为第1个快速客户完成服务,耗时1秒
第1号快速窗口尝试获取快速任务...
第1号快速窗口没有获取到快速任务

第2号普通窗口尝试获取普通任务...
第1号VIP窗口没有获取到VIP任务

第1号快速窗口尝试获取普通任务...
第1号快速窗口已获取到第5个普通客户
第1号VIP窗口尝试获取VIP任务...
第1号VIP窗口没有获取到VIP任务
第1号VIP窗口尝试获取普通任务...
第1号VIP窗口没有获取到普通任务
第6号普通客户正在等待...
第1号VIP客户正在等待...
第3号快速客户正在等待...
第1号VIP窗口尝试获取VIP任务...
第1号VIP窗口已获取到第1个VIP客户
第7号普通客户正在等待...
第3号普通窗口为第2个普通客户完成服务,耗时4秒
第3号普通窗口尝试获取普通任务...
第3号普通窗口已获取到第6个普通客户
第8号普通客户正在等待...
第4号快速客户正在等待...
第1号普通窗口为第1个普通客户完成服务,耗时6秒
第1号普通窗口尝试获取普通任务...
第1号普通窗口已获取到第7个普通客户
第9号普通客户正在等待...
第1号普通窗口为第7个普通客户完成服务,耗时1秒
第1号普通窗口尝试获取普通任务...
第1号普通窗口已获取到第8个普通客户
第10号普通客户正在等待...
第5号快速客户正在等待...
第4号普通窗口为第3个普通客户完成服务,耗时6秒
第4号普通窗口尝试获取普通任务...
第4号普通窗口已获取到第9个普通客户
第11号普通客户正在等待...
第2号普通窗口为第4个普通客户完成服务,耗时6秒
第2号普通窗口尝试获取普通任务...
第2号普通窗口已获取到第10个普通客户
第12号普通客户正在等待...
第2号VIP客户正在等待...
第6号快速客户正在等待...
第1号快速窗口为第5个普通客户完成服务,耗时6秒
第1号快速窗口尝试获取快速任务...
第1号快速窗口已获取到第3个快速客户
第1号VIP窗口为第1个VIP客户完成服务,耗时5秒
第1号VIP窗口尝试获取VIP任务...
第1号VIP窗口已获取到第2个VIP客户
第13号普通客户正在等待...
第1号快速窗口为第3个快速客户完成服务,耗时1秒
第1号快速窗口尝试获取快速任务...
第1号快速窗口已获取到第4个快速客户
第3号普通窗口为第6个普通客户完成服务,耗时5秒
第3号普通窗口尝试获取普通任务...
第3号普通窗口已获取到第11个普通客户
第14号普通客户正在等待...
第7号快速客户正在等待...
第1号快速窗口为第4个快速客户完成服务,耗时1秒
第1号快速窗口尝试获取快速任务...
第1号快速窗口已获取到第5个快速客户
第15号普通客户正在等待...
第1号快速窗口为第5个快速客户完成服务,耗时1秒
第1号快速窗口尝试获取快速任务...
第1号快速窗口已获取到第6个快速客户
第1号普通窗口为第8个普通客户完成服务,耗时5秒
第1号普通窗口尝试获取普通任务...
第1号普通窗口已获取到第12个普通客户
第16号普通客户正在等待...
第8号快速客户正在等待...
第1号快速窗口为第6个快速客户完成服务,耗时1秒
第1号快速窗口尝试获取快速任务...
第1号快速窗口已获取到第7个快速客户
第17号普通客户正在等待...
第1号VIP窗口为第2个VIP客户完成服务,耗时4秒
第1号VIP窗口尝试获取VIP任务...
第1号VIP窗口没有获取到VIP任务

第3号VIP客户正在等待...
第9号快速客户正在等待...
第1号快速窗口为第8个快速客户完成服务,耗时1秒
第1号快速窗口尝试获取快速任务...
第1号快速窗口已获取到第9个快速客户
第19号普通客户正在等待...

第1号VIP窗口为第13个普通客户完成服务,耗时5秒
第1号VIP窗口尝试获取VIP任务...
第1号VIP窗口已获取到第3个VIP客户
第2号普通窗口为第14个普通客户完成服务,耗时4秒
第2号普通窗口尝试获取普通任务...
第2号普通窗口已获取到第19个普通客户
第3号普通窗口为第15个普通客户完成服务,耗时5秒
第3号普通窗口尝试获取普通任务...
第3号普通窗口已获取到第20个普通客户
第23号普通客户正在等待...

以上执行结果表明,3种业务窗口可以分别办理对应类型客户的业务,而两行加粗和加下划线的执行信息可知,当快速窗口和VIP窗口空闲时,还可以为普通客户服务,至此完成了题目的既定需求。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: