以下内容为学习张孝祥交通灯管理系统视频教程时所做的笔记
网页显示版面不正常 请下载原笔记文件
交通灯管理系统
模拟实现十字路口的交通灯管理系统逻辑,具体需求如下:
Ø 异步随机生成按照各个路线行驶的车辆。
例如:
由南向而来去往北向的车辆----直行车辆
由西向而来去往南向的车辆 ----右转车辆
由东向而来去往南向的车辆 ----左转车辆
。。。
Ø 信号灯忽略黄灯,只考虑红灯和绿灯。
Ø 应考虑左转车辆控制信号灯,右转车辆不受信号灯控制。
Ø 具体信号灯控制逻辑与现实生活中普通交通灯控制逻辑相同,不考虑特殊情况下的控制逻辑。
注:南北向车辆与东西向车辆交替放行,同方向等待车辆应先放行直行车辆而后放行左转车辆。
Ø 每辆车通过路口时间为1秒(提示:可通过线程Sleep的方式模拟)。
Ø 随机生成车辆时间间隔以及红绿灯交换时间间隔自定,可以设置。
Ø 不要求实现GUI,只考虑系统逻辑实现,可通过Log方式展现程序运行结果。
1、线路分析:
注意,共有12条线路 由于相对方向的红绿灯变化情况一致,所以只具体分析四条线路即可:水平和垂直方向的直行路线两条、水平和垂直方向的左转线路两条、右转线路另算
每条线路都设置一个红绿灯,根据灯的状态来控制车辆通行,右转线路的灯为常绿状态。为防止线路过多导致分析时混乱,可将各条线路单独进行分析,如只分析同一个方向到另外3个方向时的问题(右转方向可忽略,只分析2个方向即可)。
2、对象分析与设计:红绿灯、红绿灯控制器、汽车、路线
汽车看到自己所在路线为绿灯时才能穿过路口,但是穿过路口前还要判断自己前面是否还有其他汽车,只有当自己是当前路线上的第一辆车时才可穿过路口。怎么判断路上还有没有其它车呢?问路呀。所以路上存着车辆的集合,并且,路这个对象中还要有添加、删除汽车的方法。题目只要求展现车辆穿过路口的过程,所以不单独设计车对象,只用字符串表示不同的车辆对象。
面向对象设计的经验:谁拥有数据,谁就提供操作这些数据的方法。典型例子:
a、人在黑板上画圆b、人把门关上 c、司机紧急刹车
上面的例子中都是人调用了对象的一些方法,这些方法并不是人具有的方法。
A、两块石头磨成一把石刀,石刀可以砍树,砍成木材,木材做成椅子
工厂:返回石刀 加工石头(石头1,石头2)
石刀:返回木材 砍树(树)
工厂:返回椅子 加工木材(木材)
StoneKnife = KnifeFactory.createKnife(stone)
mucai = StoneKnife.cut(tree)
chair = ChairFactory.makeChair(mucai)
B、球从一根绳子的一端移动到另一端
小球要移动,移动到哪里?问绳子,移动到绳子上的下一个点
小球内部设置定时器,每隔一段时间move一次
绳子:属性(长度,即两个端点)
下一个点的方法,接收小球的当前位置,返回小球将要移动到的位置
球:移动方法,将球的当前位置移到绳子的下一个位置
class Rope
{
private Point start, end;
public Rope(Point start, Point end)
{
this.start = start;
this.end = end;
}
public Point nextPoint(Point currentPoint)
{
通过当前点计算出绳子的下一个点,
如果当前点是终点则放回null
如果当前点不是绳子上的点抛出异常
}
}
class Ball
{
private Rope rope;
private Point currentPoint;
public Ball(Rope rope, Point start)
{
this.rope = rope;
this.currentPoint = start;
}
public void move()
{
currentPoint = rope.nextPoint(currentPoint);
SOP(“小球移动到了”+currentPoint);
}
}
3、交通灯及控制器类的实现细节分析
12条路线上各有一个灯,且灯固定不变就这12个,所以使用枚举。灯有两种状态(亮绿、灭红),右转灯常绿,其余八条路线两两相对,所以只考虑四个灯即可。
四个灯中的逻辑关系:当某一个灯变绿时,需要它相对方向上的灯也变绿,并且将其下一个方向上(与其90°方向)的灯变红;它变红时,其对应方向上的灯也要变红,并且将它下一个方向(与其90°方向)上的灯变绿。所以灯中需要两个变量:对应方向上的灯和下一个方向上的灯。灯中还需一个标记变量,标示灯有没有亮,当路要放行车时,需要进行判断。
综上所述,灯需要三个成员变量
灯(对应方向的灯,灯的状态,下一个方向的灯)
控制器需要一个变量来记录当前亮着的灯是哪一个灯(currentLamp);还需要一个定时器,每隔一定时间将当前灯变红,变红方法返回下一个方向的灯(用currentLamp记录),再将下一个方向的灯变绿,currentLamp记录的一直都是当前绿着的灯
控制器就是定时将当前绿着的灯变红,并记录下一个灯(下一个灯置为当前灯)
4、路线类分析及汽车上路代码实现:
首先要有一个存放当前路上车辆的集合,一共有12条路线,所以路线的构造方法中要用路线名字进行区分,在构造方法中随机产生路上的车辆。
class Road
{
private List<String> vehicles = new ArrayList<String>();
private String name = null;
public Road(String name)
{
this.name = name;
在当前路线上随机添加车辆
}
}
不能一下子将所有车辆一下放到路上,所以在产生车辆时要将这个线程暂停一下。这里要注意,构造方法中可不能让线程sleep,构造线程中sleep就相当于难产,所以要单独创建一个线程,随机往路上增加车辆。
ExecutorService pool = Executors.newSingleTheadExecutor();
产生线程后,让这个线程执行随机添加车辆的方法
pool.execute(new Runnable(){//这里创建一个Runnable接口的匿名实例对象
public void run()
{
for (int i=1; i<1000; i++)
{
Thread.sleep(1000); //休息1秒
随机1—10秒: (new Random().nextInt(10)+1)*1000
车辆集合中添加一辆车
vehicles.add(name+”_”+i);//标示一下哪条路上的几号车
这里直接用name会导致类中的成员变量name和构造时传入的局部变量名name重复,两种解决办法:将name用final修饰或者,在此处用Road.this.name引用类外部的name即可
}
}
});
线程池:一下产生好多线程,要运行任务时,并不直接将任务交给某一个线程,而是直接将任务交给线程池,由线程池从所有的线程中挑选一个空闲线程来执行任务。用Executors执行器工具产生线程池java.util.concurrent.Executors
static ExecutorService 返回的ExecutorService就是一个线程池,相当于ThreadPool | newFixedThreadPool(int nThreads) |
| newSingleThreadExecutor |
static ScheduledExecutorService | newScheduledThreadPool(int corePoolSize) |
static ScheduledExecutorService | newSingleThreadScheduledExecutor() |
接口 ExecutorService
public interface ExecutorService
extends Executor
Executor
提供了管理终止的方法,以及可为跟踪一个或多个异步任务执行状况而生成Future
的方法。
可以关闭 ExecutorService,这将导致其拒绝新任务。提供两个方法来关闭ExecutorService。shutdown()
方法在终止前允许执行以前提交的任务,而shutdownNow()
方法阻止等待任务启动并试图停止当前正在执行的任务。在终止时,执行程序没有任务在执行,也没有任务在等待执行,并且无法提交新任务。应该关闭未使用的ExecutorService 以允许回收其资源。
void | shutdown() | |
List<Runnable> | shutdownNow() | |
从接口 java.util.concurrent.Executor继承的方法 | ||
execute | ||
void | execute(Runnable command) | |
下面给出了一个网络服务的简单结构,这里线程池中的线程作为传入的请求。它使用了预先配置的Executors.newFixedThreadPool(int)工厂方法:
class NetworkService implements Runnable {
private final ServerSocket serverSocket;
private final ExecutorService pool;
public NetworkService(int port, int poolSize)
throws IOException {
serverSocket = new ServerSocket(port);
pool = Executors.newFixedThreadPool(poolSize);
}
public void run() { // run the service
try {
for (;;) {
pool.execute(new Handler(serverSocket.accept()));
}
} catch (IOException ex) {
pool.shutdown();
}
}
}
class Handler implements Runnable {
private final Socket socket;
Handler(Socket socket) { this.socket = socket; }
public void run() {
// read and service request on socket
}
}
java.util.Random
如果用相同的种子创建两个Random
实例,则对每个实例进行相同的方法调用序列,它们将生成并返回相同的数字序列。
很多应用程序会发现Math.random()
方法更易于使用。
Random() | |
Random(long seed) | |
方法摘要 | |
protected int | next(int bits) |
boolean | nextBoolean() |
void | nextBytes(byte[] bytes) |
double | nextDouble() |
float | nextFloat() |
double | nextGaussian() |
int | nextInt() |
int | nextInt(int n) |
long | nextLong() |
void | setSeed(long seed) |
java.lang.Math
public static doublerandom()
返回带正号的 double值,该值大于等于 0.0且小于 1.0。返回值是一个伪随机选择的数,在该范围内(近似)均匀分布。
第一次调用该方法时,它将创建一个新的伪随机数生成器,与以下表达式完全相同
new java.util.Random
之后,新的伪随机数生成器可用于此方法的所有调用,但不能用于其他地方。
此方法是完全同步的,可允许多个线程使用而不出现错误。但是,如果许多线程需要以极高的速率生成伪随机数,那么这可能会减少每个线程对拥有自己伪随机数生成器的争用。
5、路线类中定时器及汽车穿过路口分析与代码实现:
每个一段时间就有汽车从路口通过,需要设置一个定时器。
ScheduledExecutorService timer = Executors.newScheduledThreadPool(1);
定时器每隔一定时间就执行放行汽车的动作
a、定时执行一次
timer.schedule(command,
1,//delay//过多长时间后执行一次command中的方法,
unit);
b、定时循环执行
timer.scheduleAtFixedRate(
new Runnable() //command,定时执行的任务移除路上的一辆车
{
if(vehicles.size()>0)路上有车才移除
{
boolean lighted = true;假设现在的灯为绿灯
灯的状态通过Lamp的isLighted方法获得,灯对象和路是绑定的,通过路的名字(也就是灯的名字)即可获得当前的灯对象
lighted = Lamp.valueOf(Road.this.name).isLighted;
if (lighted)绿灯才能移车
{
vehicles。remove(0);返回一个值
将返回值打印表明车已通过路口了
SOP(vehicles.remove(0)+”通过了”);
}
}
},
1,//initialDelay,过多长时间后执行任务
1,//period,做完后再过多长时间继续做
TimeUnit.SECONDS,//unit上面定义的时间是以什么为单位的
);
接口 ScheduledExecutorService
public interface ScheduledExecutorService
extends ExecutorService
一个 ExecutorService
,可安排在给定的延迟后运行或定期执行的命令。
以下是一个带方法的类,它设置了 ScheduledExecutorService,在 1小时内每 10 秒钟蜂鸣一次:
import static java.util.concurrent.TimeUnit.*;
class BeeperControl {
private final ScheduledExecutorService scheduler =
Executors.newScheduledThreadPool(1);
public void beepForAnHour() {
final Runnable beeper = new Runnable() {
public void run() { System.out.println("beep"); }
};
final ScheduledFuture<?> beeperHandle =
scheduler.scheduleAtFixedRate(beeper, 10, 10, SECONDS);
scheduler.schedule(new Runnable() {
public void run() { beeperHandle.cancel(true); }
}, 60 * 60, SECONDS);
}
}
方法摘要 | ||
| schedule(Callable<V> callable, long delay, TimeUnit unit) | |
ScheduledFuture<?> | schedule(Runnable command, long delay, TimeUnit unit) | |
ScheduledFuture<?> | scheduleAtFixedRate(Runnable command, long initialDelay, long period, TimeUnit unit) | |
ScheduledFuture<?> | scheduleWithFixedDelay(Runnable command, long initialDelay, long delay, TimeUnit unit) |
枚举 TimeUnit
java.util.concurrent.TimeUnit
public enum TimeUnit
extends Enum<TimeUnit>
TimeUnit表示给定单元粒度的时间段,它提供在这些单元中进行跨单元转换和执行计时及延迟操作的实用工具方法。TimeUnit不维护时间信息,但是有助于组织和使用可能跨各种上下文单独维护的时间表示形式。毫微秒定义为千分之一微秒,微秒为千分之一毫秒,毫秒为千分之一秒,一分钟为六十秒,一小时为六十分钟,一天为二十四小时。
枚举常量摘要 | |||||||
DAYS | HOURS | MICROSECONDS | MILLISECONDS | MINUTES | NANOSECONDS | SECONDS | |
方法摘要 | |||||||
long | convert(long sourceDuration,TimeUnit sourceUnit) | ||||||
void | sleep(long timeout) | ||||||
void | timedJoin(Thread thread, long timeout) | ||||||
void | timedWait(Object obj, long timeout) | ||||||
long | toDays(long duration) | ||||||
convert
public long convert(long sourceDuration,
TimeUnit sourceUnit)
将给定单元的时间段转换到此单元。从较细粒度到较粗粒度的舍位转换,这样会失去精确性。例如,将999毫秒转换为秒的结果为0。使用参数从较粗粒度到较细粒度转换,如果参数为负,则在数字上溢出至Long.MIN_VALUE,如果为正,则为Long.MAX_VALUE。
例如,要将 10分钟转换为毫秒,请使用:TimeUnit.MILLISECONDS.convert(10L, TimeUnit.MINUTES)
6、灯Lamp类的实现:JDK1.5中出现了枚举Enum
public Enum Lamp
{
a、12条路对应12个灯,右转灯常绿,其余两两相对,这里4个主要灯具有逻辑关系
南2北(”北2南”,”南2西”,false),南2西,东2西,东2南,//直行、左转
北2南,北2东,西2东,西2北,//对应方向
南2东,东2北,北2西,西2南;//右转
i、根据构造方法传递对应的参数,补全12个灯的参数
南2北(”北2南”,”南2西”,false),南2西(”北2东”,”东2西”,false),
东2西(”西2东”,”东2南”,false),东2南(”西2北”,”南2北”,false),
北2南(null, null, false),北2东(null, null, false),
西2东(null, null, false),西2北(null, null, false),
南2东(null, null, true),东2北(null, null, true),
北2西(null, null, true),西2南(null, null, true);
b、灯有一个状态,红(灭,黑表示)还是绿(亮),需要一个标志变量
private boolean lighted;
e、当前灯状态改变后,与其对应方向上的灯也要改变,又需要一个成员变量
privateLamp String opposite;对应方向上的灯
g、对应方向上的灯怎么得到呢,通过构造方法,构造时传入对应方向的灯
当前灯绿,下一个方向灯要红,需要指导对应的下一个灯和当前灯的状态
private String next;
private Lamp(Lamp String opposite,String next,boolean lighted)
{ 枚举构造方法要私有
this.opposite = opposite;
this.next = next;
this.lighted = lighted;
}这里又出现一个问题,构造时如果传入的是对应方向的灯的对象变量,因为变量要先定义后使用,南2北(北2南)这里会出现问题,所以构造时传入灯的名字,将对应方向上的灯也用String表示,将对应方向的灯变凉要获取灯对象时使用枚举的alueOf(name)即可
c、要知道灯是不是亮着的,需要一个方法
public boolean isLighted()
{
return lighted;
}
d、 灯要变红变绿,又需要两个方法
public void light()
{
this.lighted = true;
f、这个灯变亮后,其对应方向的灯也要跟着变量
opposite.light();直接这样会陷入死循环,我亮你亮、我亮你亮……
所以要进行判断,当前灯有对应方向的灯时,才让对应方向的灯亮
南向的灯对应西向的灯,西向就没有对应的灯了
if(opposite!=null)
h、opposite。light();根据名字获取对应方向灯对象
Lamp.valueOf(opposite).light();
}
publicvoid Lamp blackout()控制器中需要修改返回值,返回变黑灯的下一个方向灯
{
this.lighted = false;
j、当前灯灭,对应方向灯也要跟着灭
并且当前灯灭后,如果有下一个灯,则将其变亮
if(opposite!=null)
Lamp.valueOf(opposite). blackOut ();
Lamp nextLamp = Lamp.valueOf(next);控制器中需要修改返回值
if (next!=null)
Lamp.valueOf(next).light(); 控制器中需要修改返回值
{
nextLamp.light();
}
return nextLamp;控制器中要求有返回值
}
}
7、灯控制器类的实现:
控制器用来控制灯变绿或变红,所以需要一个成员变量表示当前控制的灯。控制器构造时指定一个当前控制的灯,并让其变绿,隔一段时间后再让其变红
public class LampController
{
private Lamp currentLamp;
public LampController()
{
this.currentLamp = Lamp.南2北;控制器启动就控制一个灯
currentLamp.light(); 让控制的灯变绿
产生定时器,定时将当前灯变红
ScheduledExecutorService timer = Executors.newScheduledThreadPool(1);
timer.ScheduledAtFixedRate(
new Runnable(){
public void run()
{
currentLamp.blackOut(); 将当前灯变黑后,需要将当前灯的下一个灯置为当前灯,控制器等10秒后会再将当前灯变黑,怎么在当前灯变黑的同时将当前灯的下一个方向上的灯置为当前灯呢?在灯变黑的方法中直接返回下一个灯即可,修改一下Lamp中的blackOut方法
currentLamp = currentLamp.blackOut();
}},
10, //10秒后执行任务
10, //每隔10秒执行一次
TimeUnit.SECONDS
);
}
}
8、总成测试: 产生12条路线及一个控制器即可
直接在main方法中new12条路线出来,要写12个new语句吗?为了简化书写,将12条路线的名字封装进一个字符串数组中,通过for循环来new路线
string [] directions = new string[]{‘南2北’,南2西,东2西,东2南,北2南,北2东,西2东,西2北,南2东,东2北,北2西,西2南};
for (int i=0; i<directions.length; i++)
new Road(directions[i]);
new LampController();
为便于看到测试结果,在Lamp类的变亮方法中显示出当前是哪个灯变亮了
SOP(name()+”方向的灯亮了”)
在当前灯变黑,下一个灯变亮时也显示切换信息
SOP(“绿灯从”+name()+”方向切换为”+next)
测试中产生的问题:灯不会切换,问题出在变黑方法中
Lamp nextLamp = Lamp.valueOf(next);如果next为空就没法获取下一个灯了
if (next!=null)
{
nextLamp.light();
}
return nextLamp;
问题代码改为:
Lamp nextLamp = null;
if(next!= null)
{
SOP(“绿灯从”+name()+”方向切换为”+next)
nextLamp = Lamp.valueOf(next);
nextLamp.light();
}
return nextLamp;