前面几次一直都在讲静物,做了乌龟,做了星星,有想法的coder们就在想怎么样才能让这些东西动起来呢,这也就是推动技术发展的动力,好在这次总算是给大家带来了能让物体动起来的技术,我们现在先不已前面的乌龟和星星为案例继续讲如何写运动的物体,还是先从最简单的物体开始,反正原理都一样,掌握了这个做其他的也会动,这次用一个小球作为案例的切入口
好了,按照上一次讲的做法,首先我们要列出一个步骤出来,看了标题就知道现在要做一个可以飞行的小球。好的,让我们来大概写一下步骤:
1.先做一个窗体
2.然后就是要画一个小球
3.最后一步就是要实现让小球落下来
按照步骤先画出一个圆,这个代码已经写了这么多次,应该很熟熟练了,我就不详细说了,直接上代码了
import java.awt.*;
public class MyBall {
public static void main(String[] args) {
// TODO Auto-generated method stub
Frame w = new Frame();
w.setSize(300, 400);
MyOval mp = new MyOval();
w.add(mp);
w.show();
}
}
class MyOval extends Panel{
public void paint(Graphics g){
g.fillOval(30, 30, 20, 20);
}
}
出现如图所示的这样一个黑的实心圆
下一步,我们来看看如何实现动画,想想看怎么可以看到运动的小球。我们假设小球是从上往下运动,这也符合物理原理,一个小球自由落体的掉下来,那么我们试想一下,小球的运动轨迹就是一条垂直于你的窗体下边界的一条直线,我说过窗体里其实有一个笛卡尔坐标系,所有的点都有对应的坐标,既然运动轨迹是一条垂直于窗体下边界的直线,也就意味着改变的就是Y轴的数据,我告诉你屏幕的左上方是坐标原点,向下是正方向的Y坐标轴,向右是正方向的X坐标轴,这样一来,那小球下落的轨迹的点的Y值是不断增加的,搞清楚了原理,我们在想一个问题,怎么表示小球是自由落体呢,其实很简单,及时不断的重画小球,这种机械的事情人类不擅长做,直接交给电脑去完成。小球的Y坐标不断的增加,每增加一下,都抹去原有的小球,重画一个。还记得之前将的循环吗?循环可以不断实现增加,至于重画怎么实现,其实也简单,这次案例的最后我会讲述。先跟着我看一下下面这段代码
//主函数省略,和第一段代码的主函数一样
class MyOval extends Panel{
public void paint(Graphics g){
int x=30,y=30;
while(true){
g.fillOval(x, y, 20, 20);
y++;
}
}
}
现在拉分析一下,这里展现的是paint方法里的代码,我们现实看见了int x=30,int y=30(也可写成int x=30,y=30其实是一个意思,只是博主偷懒)这两句话是用来声明变量的,又来了一个新词,到底什么是变量?变量从字面上理解就是能变化的量,上一段代码把小球的坐标给写死了30,30,不能变就没有办法动,现在换成了可以改变的x和y。问题是在Java里,x和y需要事先声明,说未来我需要一个可以变化的量,叫做x,以后我要在x里放整数,所以之前加上int,这是必须的,规定好的语法格式,这里我事先给x一个初值30。
下面是while是循环,之前我们看到的是一个叫for的循环,如果通常知道需要循环多少次,就用for,若是不知道要循环多少次,就用while。奇怪了,怎么就不知道要循环多少次呢?举个例子,老师体罚你去操场跑10圈,这个是知道循环次数的,所以就用for循环循环10次。若老师就让你一直跑着,你知道你能跑几圈吗?所以这个时候就要用while循环了,里面的true是什么意思呢?就是说你如果还有力气那只能一直跑下去。Java里能用true表示,想法不能就用相反对应的词false来表示。true和false都是基本数据类型boolean的值,这个类型也就这两个值,通过boolean值来判断循环是否退出。
这里的while其实比较特别,一直都是true,也就是说,这个循环永远不会停,除非程序不运行了,我们通常把这样的循环叫做死循环,这里是希望动画一直继续下去,大多数情况下是要避免死循环的。
在看循环里有一个y++;就是让y这个变量加1的意思,实际上y++就是y=y+1的意思
这段代码加上主函数我们来运行一下看看结果是什么?
我们看到了一条向下的黑线,这是因为虽然能够不断的在新的位置画圆,但是过去画的圆并没有消失啊,这不符合我们的要求啊,有些聪明的人到这儿而去找到了擦除的办法,也做出来动画,但是一直有一个问题得不到很好的解决——小球飞行的时候用户什么操作都做不了,程序完全被循环占用,要是隔一会儿让y++一下,才能解决问题,目前最好的办法是线程
什么线程?又来了一个概念,我去翻了一下教科书,书上这么写:“线程是通过利用CPU的轮转,让程序中不同的代码同时执行的机制。”唉,反正这就是教科书,就整一些让人蒙圈的定义。那我就竟让说的通俗一些让大家都可以理解。还是要举一个例子,这个例子是我老师和我说的
大家都知道在城市里生活人与人之间都比较冷漠,你也常常不知道对门或者隔壁住的是谁,但是有孩子的就不同了,因为你每天都要带着孩子到楼下玩,这样就能遇到很多带孩子的人,孩子在一旁玩,大人无聊起来就会聊天,一来二去聊熟了。假设有一天天不好,于是我提议大家到我家来玩玩,结果来了4个孩子还有他们的家长,这么多人在一起我得找点儿事儿做,大家提议打麻将 ,可是无奈我不会啊,于是我被分配到看孩子,这样假设我面前有5个哇哇哭的孩子,我的任务是哄他们不让他们哭,我试了很多吃的和玩具,只有这个玩具放到孩子手里他们都不哭了,下一步我就要找5个这样的玩具,但是问题是我只有一个啊,咋办?于是我想到了一个办法打,将玩具给一个宝宝,不哭了之后抢下来,假设宝宝不会立刻哭,然后再给下一个宝宝,这个宝宝不哭之后再抢下来再 给下一个,就这样不断的重复着,如果技术高超这样用一个玩具就能让5个宝宝都不哭。对照一下定义,玩具是我们的CPU,我们没有5个COU只有一个,但是我们5个程序都要同时运行,怎么办?只里面叫做轮转,就是这个CPU轮着给每个CPU用,如果每个程序轮到10毫秒,从宏观上看好像每个程序都在运行,其实微观上来看,每个时刻都只有一个程序在运行。问题是,到目前为止的描述叫做进程,是不同的程序,又是我们希望一个程序里有两段并列的代码同时运行,也使用类似的机制,不同的是这是一个程序。早看一遍:“线程是利用CPU的轮转,让程序中不同的代码同时执行机制。”其实同时执行是假的,只是人这么认为。
好了,了解了线程的道理,我们看Java是怎么写线程的。在这个阶段,我们不深究线程的细节,可以使用就好了,我们在MyOval的类里面加上一个run方法:
class MyOval extends Panel{
public void paint(Graphics g){
g.fillOval(30,30,20,20);
}
public void run(){
}
}
这个run就是一个新来的宝宝,CPU在核实的情况下会轮到run那里,要说这个run也是程序的入口。问题是你写个run在MyOval这个类里面,操作系统也不知道啊,你问我为什么当时paint系统就知道,这是因为Panel里面就有这个机制,你一继承Panel,就相当于告诉操作系统,我有重画机制了,而线程不仅仅应用在Panel这个类上,几乎什么类都可以包含run,所以需要更新机制,我们在类的后面加上implements Runnable,叫做实现Runnable接口。接口又是一个复杂的概念,这次先记住怎么写吧,这样写是为了让系统知道里面有一个run,后续的博客更新我也会来详细的介绍接口这个概念。
class MyOval extends Panel implements Runnable{
public void paint(Graphics g){
g.fillOval(20,20,20,20);
}
public void run(){
}
}
到这里还不行,你还要告诉系统,这里有一个宝宝也要玩具
import java.awt.*;
public class MyBall {
public static void main(String[] args) {
// TODO Auto-generated method stub
Frame w = new Frame();
w.setSize(300, 400);
MyOval mp = new MyOval();
w.add(mp);
Thread t = new Thread(mp);
t.start();
w.show();
}
}
增加的
Thread t = new Thread(mp);
t.start();
用了一个新的独享来包装这个宝宝,就是t,他是Thread的对象,特别是在new这个Thread对象的时候就放进去mp。mp是什么?它是MyOval对象的引用,再看看MyOval,里面有一个run,这样就关联起来了,经过这个讨论这个讨厌的线程八股文代码,我们终于可以开始了让小球动起来的代码了。
问题是我们不能再run里面画图,一个重要的理由是在run方法里,访问不到变量g,因为大括弧限定了变量的范围,意思是你在一个大括弧开始和结束的范围内声明变量,就在这个大括弧里有效,离开这个大括弧就无效了。这么设计是有道理的,但是现在我们先不管这个道理是什么,写的代码多了,就自然而然的会发现这样设计的好处。现在我们的思路是,把小球的坐标改成变量,声明在paint方法的外面,或者说是MyOval类中,这样变量就定义成了MyOval类的成员,变量处于类的大括弧中,在整个类的作用域中有效,那么paint方法和run方法都能使用坐标变量,我们在paint方法中取坐标值,在run方法里改变坐标变量。
注意下面代码的顺序发生了变化,因为MyOval越来越长
import java.awt.*;
public class MyBall {
public static void main(String[] args) {
// TODO Auto-generated method stub
Frame w = new Frame();
w.setSize(300, 400);
MyOval mp = new MyOval();
w.add(mp);
Thread t = new Thread(mp);
t.start();
w.show();
}
}
class MyOval extends Panel implements Runnable{
int x=30,y=30;
public void paint(Graphics g){
g.fillOval(x, y, 20, 20);
}
public void run(){
y++;
}
}
有些代码顺序实在没办法表达了,比如画圆的时候不能用x和y,因为这个时候还没有声明x和y,而且写到这里,你还不知道将坐标变成可变的,所以这个地方还用30和30,写完这个再将坐标改成变量,另外实现的Runnable这个接口也是后来才加上去的,原则有两个:一是一直在写没有语法错误的代码:二是符合正常人的逻辑思维。
现在运行一下,看看小球没有动吧,没动,为什么?明明是y++了,哦!原来y坐标虽然变了,但是图像没有重画。问题是怎么重画图像?要知道重画是系统的事情,你不能在程序中自己调用,Java提供了一个repaint方法,发出repaint()的调用,这个请求将发送回系统,系统见到后便会调用paint()方法,还是系统重画,你发出这个请求。下面我只截取MyOval类的代码,前面的代码没有区别。
class MyOval extends Panel implements Runnable{
int x=30;
int y=30;
public void paint(Graphics g){
g.fillOval(x,y,20,20);
}
public void run(){
y++;
repaint();
}
}
这下子该有动画了吧!还没有?不可能,一定是有动画了,只是你没有看出来,小球确实是动了,只不过移动了一个像素点,肉眼很难识别,然后重画,线程就结束了,要想看到动画,你得看到动画,你得让y坐标不断的向下移动,不断的重画,用循环。
class MyOval extends Panel implements Runnable{
int x=30,y=30;
public void paint(Graphics g){
g.fillOval(x, y, 20, 20);
}
public void run(){
while(true){
y++;
repaint();
}
}
}
这下看一下结果,是不是连小球都没了,因为小球目前正飞往地球的另一面,电脑太快了,y坐标很快就比窗口的高度还大,我们的y坐标超出窗口边缘的时候,将他拉回窗口的上边缘。
class MyOval extends Panel implements Runnable{
int x=30,y=30;
public void paint(Graphics g){
g.fillOval(x, y, 20, 20);
}
public void run(){
while(true){
y++;
if(y>400){
y=0;
}
repaint();
}
}
}
今天的讲解总算也是让小球动了起来,还算是没有百忙下面我还有几点东西要补充
## 线程和进程的区别##
进程是一个独立运行的程序,比如一个正在运行的记事本和一个正在运行的浏览器,这是两个进程,加入我们启动了两次记事本,那么也得到了两个进程,程序在运行的时候,系统会在内存中分配一块独立的空间给这个程序,两个运行的记事本就有两块独立的空间,儿线程是在一个进程中,能够运行两段代码。
即便你不写多线程的程序,Java程序本身就是多线程的,main方法被调用的时候多线程机制早就已经存在,我们将程序所处的线程称为前台线程。在Java中,为前台线程提供服务的后台线程会伴随着运行起来,所以后台线程也就称为“守护线程”或“精灵线程”,JVM的垃圾回收机制就是由后台线程完成的。当前台线程都死亡后,后台线程会自动死亡。
故事的最后还好是要说拜拜,今天的内容就到这里了,还是要写个20遍,就算不明白写得多了自然就懂了