这是一个J2ME的时间显示的例子。一个获取时间并且按秒计时的时间显示类,在J2ME中创建对象并且创建一个线程让时间占用这个线程进行。
说起J2ME,可能很多年轻的开发者已经不太了解了。但在2013年那个智能手机刚刚兴起的年代,功能机依然占据着很大的市场份额。那时候,我用着一台支持Java的诺基亚手机,整天琢磨着能在上面跑点什么程序。J2ME(Java 2 Platform, Micro Edition)就是当时在功能机上运行Java程序的标准平台。虽然现在回头看,那段技术已经属于”上古时期”了,但它却是我编程之路上一段非常珍贵的记忆。
项目背景与动机 当时学习J2ME的初衷很简单——我想给自己做一款手机应用。那时候还没有什么App Store的概念,手机上的应用大多是用J2ME开发的。我看到别人手机上的时钟、小游戏,觉得挺酷,就想自己也做一个。时间显示虽然听起来简单,但在J2ME的框架下,需要考虑线程管理、Canvas绘制、时区处理等诸多问题,对于当时的我来说,是一个很好的练手项目。
J2ME的核心概念是MIDlet(Mobile Information Device Application),它是J2ME应用的基础组件。每个MIDlet都有三个关键的生命周期方法:startApp()、pauseApp()和destroyApp(),分别对应应用的启动、暂停和销毁。理解这三个方法的生命周期,是编写J2ME应用的第一步。
核心代码实现 下面是一个完整的J2ME时间显示类的实现代码:
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 Shijianxianshi extends MIDlet implements CommandListener { Display display; testCanvas canvas; Command quit; public Shijianxianshi () { display = Display.getDisplay(this ); canvas = new testCanvas (); quit = new Command ("Quit" , Command.EXIT, 2 ); canvas.addCommand(quit); canvas.setCommandListener(this ); } protected void startApp () throws MIDletStateChangeException { display.setCurrent(canvas); } protected void destroyApp (boolean unconditional) throws MIDletStateChangeException { } protected void pauseApp () { } public void commandAction (Command c, Displayable d) { try { if (c == quit) { destroyApp(true ); notifyDestroyed(); } } catch (MIDletStateChangeException me) { System.out.println(me + "caught." ); } } }
这段代码是整个应用的主入口类。Display.getDisplay(this) 用于获取当前设备的显示管理器,canvas 是自定义的画布对象,用于绘制时间显示界面。Command 类用于处理用户交互,这里定义了一个”退出”按钮。
接下来是核心的 Canvas 绘制和线程处理部分:
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 testCanvas extends Canvas implements Runnable { private Image image = null ; private Font font; TimeZone t; Calendar c; int h, min, sec, year, month, day, week = 0 ; public testCanvas () { t = TimeZone.getTimeZone("GMT+08:00" ); c = Calendar.getInstance(t); h = c.get(Calendar.HOUR_OF_DAY); min = c.get(Calendar.MINUTE); sec = c.get(Calendar.SECOND); year = c.get(Calendar.YEAR); month = c.get(Calendar.MONTH); day = c.get(Calendar.DAY_OF_MONTH); week = c.get(Calendar.DAY_OF_WEEK); } protected void paint (Graphics g) { new Thread (this ).start(); font = Font.getFont(Font.FACE_SYSTEM, Font.STYLE_BOLD, Font.SIZE_MEDIUM); g.setFont(font); String hour, minute, second, m, d, w = null ; switch (week) { case 1 : w = "一" ; break ; case 2 : w = "二" ; break ; case 3 : w = "三" ; break ; case 4 : w = "四" ; break ; case 5 : w = "五" ; break ; case 6 : w = "六" ; break ; case 7 : w = "日" ; break ; } if (h < 10 ) { hour = "0" + String.valueOf(h); } else { hour = String.valueOf(h); } if (min < 10 ) { minute = "0" + String.valueOf(min); } else { minute = String.valueOf(min); } if (sec < 10 ) { second = "0" + String.valueOf(sec); } else { second = String.valueOf(sec); } String y = String.valueOf(year); if (month + 1 < 10 ) { m = "0" + String.valueOf(month + 1 ); } else { m = String.valueOf(month + 1 ); } if (day < 10 ) { d = "0" + String.valueOf(day); } else { d = String.valueOf(day); } try { image = Image.createImage("/2.png" ); } catch (IOException ex) { System.out.println("图片导入失败" ); } g.drawImage(image, 0 , 0 , Graphics.HCENTER | Graphics.TOP); g.drawString("我的时钟" , getWidth() / 2 , 20 , Graphics.HCENTER | Graphics.TOP); g.drawString("当前的日期为:" , 60 , 60 , Graphics.HCENTER | Graphics.TOP); g.drawString("星期:" , 30 , 100 , Graphics.HCENTER | Graphics.TOP); g.drawString("当前的时间为:" , 60 , 130 , Graphics.HCENTER | Graphics.TOP); g.drawString(hour, 50 , 150 , Graphics.HCENTER | Graphics.TOP); g.drawString(":" , 65 , 150 , Graphics.HCENTER | Graphics.TOP); g.drawString(minute, 80 , 150 , Graphics.HCENTER | Graphics.TOP); g.drawString(":" , 95 , 150 , Graphics.HCENTER | Graphics.TOP); g.drawString(second, 110 , 150 , Graphics.HCENTER | Graphics.TOP); g.drawString(w, 60 , 100 , Graphics.HCENTER | Graphics.TOP); g.drawString(y, 50 , 80 , Graphics.HCENTER | Graphics.TOP); g.drawString("/" , 75 , 80 , Graphics.HCENTER | Graphics.TOP); g.drawString(m, 90 , 80 , Graphics.HCENTER | Graphics.TOP); g.drawString("/" , 105 , 80 , Graphics.HCENTER | Graphics.TOP); g.drawString(d, 120 , 80 , Graphics.HCENTER | Graphics.TOP); if (sec < 60 ) { sec++; } else { sec = 0 ; if (min < 60 ) { min++; } else { min = 0 ; if (h < 13 ) { h++; } else { h = 1 ; } } } } public void run () { try { Thread.sleep(1000 ); } catch (InterruptedException ex) { } repaint(); } }
技术要点分析 1. 时区处理 代码中特别注意了时区的设置:TimeZone.getTimeZone("GMT+08:00")。如果不显式设置时区,J2ME会使用设备默认的时区,这可能导致时间显示不正确。对于中国的用户来说,设置为东八区是非常必要的。
2. 线程管理 时间的秒级更新是通过独立线程实现的。testCanvas 类实现了 Runnable 接口,在 paint() 方法中启动新线程,线程中 sleep(1000) 暂停一秒后调用 repaint() 触发重绘。这种方式虽然简单粗暴,但在资源受限的功能机上是一个可行的方案。
需要注意的是,每次 paint() 都创建新线程的做法在实际应用中是不够优雅的,更好的方式是使用一个持久化的线程,在循环中不断更新并重绘。这是我在后续学习中才意识到的问题。
3. Canvas绘制 J2ME的Canvas绘制是基于坐标系统的,需要手动计算每个字符串的位置。Graphics.HCENTER | Graphics.TOP 是锚点设置,决定了字符串的定位方式。这种手动布局的方式虽然繁琐,但也能很好地理解图形绘制的基本原理。
遇到的问题与解决方案 在开发过程中,我遇到了几个比较棘手的问题:
首先是图片加载失败的问题。J2ME对图片资源的加载有严格的要求,图片必须放在正确的资源目录下(通常是res文件夹),并且在打包成JAR时需要正确配置。一开始我的图片一直加载不出来,后来发现是路径设置的问题。
其次是星期显示的逻辑。Calendar.DAY_OF_WEEK 返回值中,1代表周日而不是周一,这在switch语句中需要特别注意。我在代码中通过调整映射关系解决了这个问题。
还有一个比较隐蔽的bug是时间递增逻辑。我在代码中使用了 h < 13 来判断小时,这意味着使用的是12小时制,而获取时间时用的是 HOUR_OF_DAY(24小时制)。这个不一致会导致下午的时间显示出现问题。这是一个典型的设计缺陷,在后来的版本中我进行了修正。
与现代技术的对比 如今,Android和iOS已经成为了智能手机的绝对主流,J2ME也早已退出了历史舞台。现在的开发者使用Swift、Kotlin、Flutter等现代语言和框架,可以在几天之内开发出功能丰富的应用。回头看J2ME时代的开发,虽然简陋,但它教会了我最基础的编程思维:如何在资源极度受限的环境下,用有限的API实现想要的功能。
那种在240x320像素的屏幕上精雕细琢的日子,虽然已经一去不复返了,但它留给我的,是对技术的热爱和对细节的执着。每次看到手机上的时钟应用,我都会想起当年在J2ME上写的那个简陋但充满成就感的小时钟。
备注:以上高亮颜色显示的为重要类与方法,供大家借鉴。我已在自己的Java手机上运行过,没有问题的。