--sunfruit
上图求一笔画的路径,利用图论的相关知识可以得到程序如下:
public class OnePath {
private static int[][]
links = { {0,1,1,0,0,0,1,0}, {1,0,0,1,0,0,0,1}, {1,0,0,1,1,1,0,0},
{0,1,1,0,1,1,0,0}, {0,0,1,1,0,1,1,0}, {0,0,1,1,1,0,0,1}, {1,0,0,0,1,0,0,0}, {0,1,0,0,0,1,0,0}
};
public OnePath() {
int sum = 0;
//存放每个点的度
int[] point = new int[links[0].length];
for (int i = 0; i < links[0].length; i++) {
int[] templink = links[i];
for (int j = 0; j < links[0].length; j++) {
point[i] += templink[j];
}
sum += point[i];
}
//计算度数是奇数点的个数,如果大于2则不能一笔画
int odt = 0;
int start = -1;
for (int i = 0; i < point.length; i++) {
int mod = point[i] % 2;
if (mod > 0) {
//if(start==-1)
start = i;
odt++;
}
}
if(odt>2)
{
System.out.println("该图不能一笔画");
return;
}
int r = 0;
//从一个奇数点开始计算
int nowd=start;
System.out.print(nowd+1);
while (sum > 0) {
r=0;
//对于起点nowd 检查当前的点r 是否合适
//links[nowd][r]==0 判断是否有可以走的没有用过的线路
//(point[r]<=1 && sum!=2) 判断是否是最后一次,如果不是最后一次,那么避开度数是1的点
while (links[nowd][r]==0 || (point[r]<=1 && sum!=2)) {
r++;
}
links[nowd][r]=0; //已经用过的线路
links[r][nowd]=0; //已经用过的线路 links[nowd][r] links[r][nowd]互为往返路线,用过1->2那么2->1也作废了
sum=sum-2; //总度数减2 因为从1->2 消耗了1的度和2的度
point[nowd]--; //起点和终点的度都减1 1->2 那么1的度和2的度都减1
point[r]--; //起点和终点的度都减1 1->2 那么1的度和2的度都减1
nowd =r; //设置新的起点
System.out.print("->"+(r+1));
}
}
public static void main(String[] args) {
new OnePath();
}
}
如果你使用JavaScript编程,你或许会怀疑它是否包含了面向对象(OO)的结构。实际上,JavaScript的确支持面向对象的架构――在某种程度上。本文将通过一个可扩展向量图形(SVG)的实例来说明JavaScript的OO结构。
我如何在类中定义方法和属性?
OO开发的一个基本方面是类及其相应的方法和/或属性的使用。JavaScript通过function关键字支持类(及其属性)的使用。下面的代码定义了一个叫做Figure的JavaScript类:
function Figure() {
this.centerX=0;
this.centerY=0;
this.area=0;
this.transform = transform; // methods are defined like this
function transform(moveX,moveY,angle) {
this.centerX += moveX;
this.centerY += moveY;
} }
这个Figure类有三个属性:centerX,centerY,和area。另外,它还有一个方法叫做transform()。前三行是这个类的构造器。
但是它看起来不像一个类
你会想Figure()看起来不像一个类,而更像一个JavaScript的函数。那么为什么Figure()定义的是个类?
严格的说,Figure()函数没有定义一个类,但是它仿造了一个。它实际上创建了一个对象,在括号里的代码使这个对象的构造器。JavaScript的对象支持是很基础的,它并不区分类和对象。
这就引到了问题为什么Figure()函数创建的是一个对象。对象是可以有属性和方法的。基本上,因为Figure()函数同时包含了属性和方法,它就是个对象。在JavaScript里,所有的函数即是对象又是可调用的代码块。这不像它听起来的那样容易被误解。要创建一个Figure()类/对象,你只用使用以下句法:
MyFigure = new Figure();
你也可以把Figure()函数当作代码块调用,就像这样:
figValue = Figure();
变量figValue没有被定义是因为代码块Figure()没有返回任何值。如果你把return(this.area)加到函数的最后一行,figValue就会有个值0。所以figValue是个类型数字,MyFigure是对象 Rectangle的实例。
为什么所有的变量前面都一个“this”?
这个关键字this表示这是对象的实例变量,可以使用MyFigure.centerX从对象外部访问。要让变量成为私有变量,去掉前缀this就行了。this.transform = transform这一行让方法成为公用方法。这个方法通过MyFigure.transform(100,100,0)调用。
这些类有层次之分吗?
另一个好问题的是JavaScript的类是否有层次之分。回答是肯定有。我们来仔细看看是怎么做到分层的。我们可以定义一个Rectangle子类,并把Figure作为父类:
function Rectangle(startX, startY, endX, endY) {
this.width = endX - startX;
this.height = endY - startY;
this.centerX = (endX + startX)/2;
this.centerY = (endY + startY)/2;
this.computeArea = computeArea;
function computeArea() {
this.area = this.width*this.height;
} }
Rectangle.prototype = new Figure();
Rectangle对象是用4个自变量创建的,前四行是构造器。 Rectangle类包含了一个方法: computeArea()。最后一行Rectangle.prototype = new Figure();,把Rectangle类定义为从Figure类继承来的子类。
然我来解释一下prototype(原型)。每个对象构造器都有prototype属性;这是用来给所有的对象增加新属性和方法的。这就是为什么原型被用来实现继承:child.prototype = new parent();。通过原型,父对象的所有属性和方法都被添加到子对象上。
要注意this.centerX,this.centerY,和area是Rectangle类中所使用的属性,但是它们是 Figure父类的属性。和Rectangle类相似,Circle类可以被定义成Figure类的原型。这种父子关系可以按你需要来定义深度;你可以创建另一个Rectangle的子类。
我如何创建一个类的实例?
在JavaScript里创建一个类的实例很容易:
rect = new Rectangle(100,100,900,800);
这就创建了Rectangle类型的一个对象。Rectangle的构造器在属性width, height, centerX, 和centerY中填入了值。rect.area属性的值是零(0)。使用这个命令就能调用area方法:
rect.computeArea();
rect.area的值现在是560,000。要调用transform方法使用:
rect.transform(100,200,0);
父和子对象的属性可以像这样访问到:
var ar = rect.area;
var wi = rect.width;
我能超越属性和方法吗?
就像你在Java中的一样,你可以超越属性和方法。在子类中定义的属性或者方法可以超越同名的父类的属性和方法。
和全局变量互动
JavaScript也支持全局变量的使用。在以下代码段中测试一下g_area变量的范围:
<HTML>
<SCRIPT>
var g_area = 20;
function Figure() {
…
this.area=g_area;
…
}
function Rectangle(){ … }
Rectangle.prototype = new Figure();
function test(){
g_area = 40;
rect = new Rectangle();
alert(rect.area);
}
</SCRIPT>
<BODY onLoad = 'test()'/>
</BODY>
</HTML>
rect.area的值是20(不是你预计的40),这是因为Rectangle对象是Figure对象的原型,这种关系在test()被调用以前就被定义了。要使用g_area的新值,你需要用以下的方法:
function test() {
g_area = 40;
Rectangle.prototype = new Figure();
rect = new Rectangle();
alert(rect.area);
}
对于所有的Rectangle的新实例,这将改变area属性的值。或者,你可以使用这种方法:function test() {
g_area = 40;
rect = new Rectangle();
Rectangle.prototype.area = g_area;
alert(rect.area);
}
这将改变Rectangle所有现存的以及新实例的area属性的值。
结论
为了效仿OO开发,JavaScript提供了必需的继承、封装和超越属性,尽管它不支持接口和方法的过载。如果你是刚接触到OO开发,用它试试。OO概念允许开发者将一组数据和相关操作集中入一个对象。这在管理浏览器事件和管理浏览器内SVG图时很有用。
--sunfruit
很多时候需要上传附件到服务器,一般采用在页面放置<input type="file" name="upload" value=""> 的方式让用户选择要上传的文件进行上传,使用的是HTTP协议,这样的方式很方便开发也简单,不过如果上传的附件比较大的时候,会出现IE响应很慢的情况,如果用户急性子,多点几下上传的按钮,那么就会导致IE不响应的情况,这个时候如果在文件上传得过程中,给用户一个动态的提示甚至是一个上传的进度条,效果就会好多了,这样就会用到Ajax技术了,让Ajax以一个固定的间隔时间检查上传情况然后在页面以文字或是图片的方式体现出来就行了。
在使用Ajax进行附件上传进度查询的时候也想过,直接使用Ajax进行附件上传,在实现过程中发现问题比较多,所以就使用了变通的方式:使用标准的附件上传方式,结合Ajax进行上传的进度检查
主要的代码如下:
Ajax的封装
/**
* 创建 XMLHttpRequest 对象
*/
function getXMLHttpRequest()
{
var http_request;
if (window.XMLHttpRequest) {
//非IE浏览器框架创建 XMLHttpRequest 对象
http_request = new XMLHttpRequest();
if(http_request.overrideMimeType)
{
http_request.overrideMimeType('text/xml');
}
}else if (window.ActiveXObject){
// 创建 XMLHttpRequest 对象
try {
http_request = new ActiveXObject("Msxml2.XMLHTTP");
} catch (e1) {
try {
http_request = new ActiveXObject("Microsoft.XMLHTTP");
} catch (e2) {
// 不能创建 XMLHttpRequest 对象
}
}
}
return http_request;
}
/**
* Get请求
*/
function sendGetDictate(http_request,url)
{
req.open("GET", url, true);
http_request.send(null);
}
以上是Ajax的的基础部分,下面说文件上传部分的检查部分,文件上传本身的流程不变,只是在提交上传以后,需要执行setTimeout(checkupload,500); 这样的方法 checkupload 方法要自己编写,例如
function checkupload()
{
req=getXMLHttpRequest();
req.onreadystatechange = setActiveContent;
sendGetDictate(req,"/manager/servlet/imageservlet?tag=ajaxuploadfilecheck&WARE_ID=609187669&nocache="+Math.random(),"name=111");
}
然后需要编写setActiveContent方法,例如
var count=0; //防止无限循环,并且在页面提交上传得时候设置为0
function setActiveContent()
{
if (req.readyState == 4) {
if (req.status == 200) {
var rettext=req.responseText; //这里是servlet返回的内容,检查上传得状态,可以在javabean或是servlet里面设置全局的静态变量来表明上传状态
if(rettext=="-1")
{
//停止循环
alert("服务器更新错误");
}
else if(rettext=="0")
{
//继续循环检查
if(count<6)
{
setTimeout("checkupload()",500);
count++;
}
else
{
alert("上传失败");
}
}
else if(rettext=="1")
{
alert("文件上传成功");
}
}
}
}
基本流程就是这样了,至于文字表现上传过程还是进度条表现,就看自己了
Painting in AWT and Swing
Good Painting Code Is the Key to App Performance
By Amy Fowler
在图形系统中, 窗口工具包(windowing toolkit)通常提供一个框架以便相对容易地创建一个图形用户接口(GUI),在正确的时间、正确的屏幕位置显示一个正确的图像位。
AWT (abstract windowing toolkit,抽象窗口工具包) 和Swing都提供这种框架。但是实现这种框架的APIs对一些开发人员来讲不是很好理解 -- 这就导致一些程序的运行达不到预期的效果。
本文详细地解释AWT和Swing的绘画机制,目的是帮助开发人员写出正确的和高率的GUI绘画代码。然而,这篇文章只包括一般的画图机制(即,在什么地方和什么时间去呈现),而不介绍Swing的图形API怎样去呈现图形。想学习怎样去显示漂亮的图形,请访问Java 2D 网站。
绘画系统的演变
当最初的、为JDK1.0使用的AWT API发布时,只有重量级(heavyweight)部件("重量级" 的意思是说该部件有它自己的、遮光(opaque)的、与生俱来的窗体)。这样就使得AWT在很大程度上依赖于本地平台的绘画系统。这样的安排需要开发人员写代码的时候要考虑到很多细节问题,象重画检测(damage detection)、剪切(clip)计算、以及Z轴次序等。随着JDK 1.1中轻量级(lightweight)部件的引入("轻量级" 部件重用了与它最接近的重量级祖先的本地窗体),需要AWT能在共享的代码里为轻量级部件实现绘画处理。因此,重量级和轻量级部件在它们各自的绘画处理方法有着微妙的差别。
在JDK 1.1之后,当发布了Swing工具的时候,引入了它自己的绘画风格。Swing的绘画机制在很大程度上类似并且依赖于AWT,但是,也有它自己的观点,还带来了新的API,使得应用程序可以容易地定制绘画工作。
在AWT中绘画
去理解AWT绘画API怎样工作,有助于我们搞明白是什么触发了窗口环境中的绘画操作。AWT中有两种绘画操作:系统触发的绘画,和程序触发的绘画
系统触发的绘画操作
在系统触发的绘画操作中,系统需要一个部件显示它的内容,通常是由于下列中的原因:
部件第一次在屏幕上显示
部件的大小改变了
部件显示的内容受损需要维护。(比如,先前挡住部件的其它物体移走了,于是部件被挡住的部分曝露出来。
程序触发的绘画操作
在程序触发的绘画操作,是部件自己决定要更新自身的内容,因为部件内部的状态改变了。(比如,监测到鼠标按钮已经按下,那么它就需要去画出按钮"被按下"时的样子>
画图的方法
不管是谁触发了画图请求,AWT都是利用"回调"机制来实现绘画,这个机制对于“重量级”和“轻量级”的部件都是相同的。这就意味着程序应该在一个特定的可覆盖的方法中放置那些表现部件自身的的代码,并且在需要绘画的时候,工具包就会调用这个方法。这个可覆盖的方法在java.awt.Component中声明:
public void paint(Graphics g)
当AWT调用这个方法时,作为参数的、负责在这个特定的部件上绘画的Graphics对象是在之前已经配置了的,拥有恰当的状态值。
Graphics的颜色 值被设置为部件的前景。
Graphics的字体 设置为部件的字体。
Graphics的平移(translation) 也给设定,使用坐标(0,0)定位部件的左上角。
Graphics的裁剪框(clip rectangle)设置为部件需要画图的区域。
程序必须使用这个Graphics(或者其派生类)对象来呈现绘画,并且可以根据自己的需要任意改变Graphics对象的属性值。
这里是一个回调绘画的简单例子,在部件的范围内呈现一个实体园:
public void paint(Graphics g) {
// 根据部件的范围,动态计算圆的尺寸信息。
Dimension size = getSize();
// 直径
int d = Math.min(size.width, size.height);
int x = (size.width - d)/2;
int y = (size.height - d)/2;
// 画圆(颜色已经预先设置为部件的前景颜色)
g.fillOval(x, y, d, d);
g.setColor(Color.black);
g.drawOval(x, y, d, d);
}
初次接触AWT的开发人员可以看看PaintDemo example,那里介绍了一个在AWT程序中怎样使用画图回调方法的例子。
一般情况下,程序应该避免把绘画代码放置在回调方法paint()的范围之外。为什么呢?因为paint方法之外的绘画代码可能会在不适合画图的时候被调用 -- 例如,在部件变为可见之前或者已经在使用一个有效的Graphics。同时,不推荐在程序中直接调用paint()。
为了使能够由程序触发绘画操作,AWT提供了下面的java.awt.Component的方法,这样程序就可以提出一个异步的绘画请求:
public void repaint()
public void repaint(long tm)
public void repaint(int x, int y, int width, int height)
public void repaint(long tm, int x, int y,
int width, int height)
下面的代码显示了一个简单的鼠标监听器的例子,当鼠标按下和抬起的时候,使用repaint()来触发“假想按钮”的更新操作。
MouseListener l = new MouseAdapter() {
public void mousePressed(MouseEvent e) {
MyButton b = (MyButton)e.getSource();
b.setSelected(true);
b.repaint();
}
public void mouseReleased(MouseEvent e) {
MyButton b = (MyButton)e.getSource();
b.setSelected(false);
b.repaint();
}
};
如果部件要呈现复杂的图形,就应该使用带参数的repaint()方法,通过参数来指定需要更新的区域。一个比较常见的错误是总是调用无参数的repaint()来提出重画请求,这个方法会重画整个部件,经常导致一些不必要的画图处理。
paint() vs. update()
为什么我们要区分绘画操作是"系统触发" 还是"程序触发"呢?因为在“重量级”部件上,AWT对这两种请求的在处理上稍有不同(“轻量级”的情况将在后面介绍),并且不幸的是与此相关的代码非常复杂,难以更改。
对于“重量级”部件,这两种方式的绘画产生于两条不同的途径,取决于是“系统触发”还是“程序触发”。
系统触发的绘画
下面介绍“系统触发”的绘画操作是怎么产生的:
AWT确定是一部分还是整个部件需要绘画。
AWT促使事件分派线程调用部件的paint()方法。
程序触发的绘画
由程序触发的绘画的产生如下所示:
程序确定是一部分还是全部部件需要重画以对应内部状态的改变。
程序调用部件的repaint(),该方法向AWT登记了一个异步的请求 -- 当前部件需要重画。
AWT促使事件分派线程去调用部件的update() 方法。
注意: 在最初的重画请求处理完成之前,如果在该部件上有多次对repaint()的调用,那么这些调用可以被合并成对update()的一次调用。决定什么时候应该合并多次请求的运算法则取决于具体的实现。如果多次请求被合并,最终被更新的区域将是所有这些请求所要求更新的区域的联合(union)。
如果部件没有覆盖(override)update()方法,update()的默认实现会清除部件背景(如果部件不是“轻量级”),然后只是简单地调用paint()方法。
因为作为默认的最终结果都是一样的(paint()方法被调用),很多开发人员完全不知道一个分离的update() 方法的意义。确实,默认的update()的实现最终会转回到对paint()方法的调用,然而,如果需要,这个更新操作的 "钩子(hook)"可以使程根据不同的情况来处理程序触发的绘画。程序必须这么设想,对paint()的调用意味着Graphics的裁剪区"损坏"了并且必须全部重画;然而对update()的调用没有这种含义,它使程序做增量的绘画。
如果程序希望只把要增加的内容敷盖于已存在于该部件的像素位之上,那么就使用增量画图操作。UpdateDemo example 示范了一个利用update()的优点做增量绘画的程序。
事实上,大多数GUI部件不需要增量绘画,所有大部分程序可以忽略update()方法,并且简单地覆盖(override)paint()来呈现部件的当前状态。这就意味着不管“系统触发”还是“程序触发”,在大多数部件上的表现从其本质上讲是是等价的。
绘画与轻量级部件
从应用开发人员的观点看,“轻量级”的绘画API基本上和“重量级”一样(即,你只需要覆盖paint()方法,同样,调用repaint()方法去触发绘图更新)。然而,因为AWT的“轻量级”部件的框架全部使用普通Java代码实现,在轻量级部件上绘画机制的实现方式有一些微妙的不同。
“轻量级”部件是怎样被绘制的
“轻量级”部件需要一个处在容器体系上的“重量级”部件提供进行绘画的场所。当这个“重量级”的“祖宗”被告知要绘制自身的窗体时,它必须把这个绘画的请求转化为对其所有子孙的绘画请求。这是由java.awt.Container的paint()方法处理的,该方法调用包容于其内的所有可见的、并且与绘画区相交的轻量级部件的paint()方法。因此对于所有覆盖了paint()方法的Container子类(“轻量级”或“重量级”)需要立刻做下面的事情:
public class MyContainer extends Container {
public void paint(Graphics g) {
// paint my contents first...
// then, make sure lightweight children paint
super.paint(g);
}
}
如果没有super.paint(),那么容器(container)的轻量级子孙类就不会显示出来(这是一个非常普遍的问题,自从JDK1.1初次引进“轻量级”部件之后)。
这种情况相当于注释掉了默认的Container.update()方法的执行,从而不能 使用递归去调用其轻量级子孙类的update()或者paint()方法。这就意味着任何使用update()方法实现增量绘画的重量级Container子类必须确保其轻量级子孙在需要时,能够被它的递归操作所调用从而实现重画。幸运的是,只有少数几个重量级的容器(Container)需要增量绘图,所以这个问题没有影响到大多数的程序。
轻量级与系统触发型的画图
为轻量级部件实现窗体行为(显示、隐藏、移动、改变大小等)的轻量级框架的代码全部用Java代码写成。经常的,在这些功能的Java实现中,AWT必须明确地吩咐各个轻量级部件执行绘画(实质上讲这也是系统触发的绘画,尽管它不是源于本地的 操作系统)。而轻量级框架使用repaint()方法来吩咐部件执行绘画,这是我们前面解释过的,将导致一个update()的调用而不是直接地对paint()的调用。因此,对于轻量级,系统触发型的画图操作可以遵循下面的两种途径:
系统触发的绘画要求产生于本地系统(例如,轻量级的重量级祖先第一次现身的时候),这导致对paint()的直接调用。
系统触发型的绘图要求产生于轻量框架(例如,轻量级部件的尺寸改变了),这导致对update()的调用,该方法进而默认地调用paint()。
简单地讲,这意味着轻量级部件在update()和paint()之间没有实质的差别,进一步讲这又意味着“增量的绘图技术”不能用到轻量级部件上。
轻量级部件与透明
因为轻量级部件"借用"了本属于其“重量级”祖先的屏幕,所以它们支持“透明”的特征。这样做是因为轻量级部件是从底往上绘画,因此如果轻量级部件遗留一些或者全部它们祖先的像素位而没有画,底层的部件就会"直接显示。"出来。这也是对于轻量级部件,update()方法的在默认实现将不再清除背景的原因。
LightweightDemo 例程示范了轻量级部件的透明特征。
"灵活巧妙地"绘画方法
当AWT尝试着使呈现部件的处理尽可能高效率时,部件自身paint()的实现可能对整体性能有重大的影响。影响这个处理过程的两个关键领域是:
使用裁剪区来缩小需要呈现的范围。
应用内部的版面布局信息来缩小对子部件的笼罩范围(仅适用于轻量级).。
如果你的部件很简单 -- 比如,如果是一个按钮 -- 那么就不值得花费气力去改善它的呈现属性,使它仅仅去绘画与修剪区相交的部分;不理会Graphics的裁剪区直接绘制整个部件反而更划算。然而,如果你创建的部件界面很复杂,比如文本部件,那么迫切需要你的代码使用裁剪信息来缩小需要绘图的范围。
更进一步讲,如果你写了一个容纳了很多部件的复杂的轻量级容器,其中的部件和容器的布局管理器,或者只是容器的布局管理器拥有布局的信息,那么就值得使用所知道的布局信息来更灵活地确定哪个子部件需要绘画。Container.paint()的默认实现只是简单地按顺序遍历子部件,检查它是否可见、是否与重换区域相交 -- 对于某几个布局管理这种操作就显得不必要的罗嗦。比如,如果容器在100*100的格子里布置部件,那么格子的信息就可以用来更快得确定这10,000个部件中哪个与裁剪框相交,哪个就确实需要绘制。
AWT绘画准则
AWT为绘制部件提供了一个简单的回调API。当你使用它是,要遵循下面的原则:
对于大多数程序,所有的客户区绘画代码应该被放置在部件的paint()方法中。
通过调用repaint()方法,程序可以触发一个将来执行的paint()调用,不能直接调用paint()方法。
对于界面复杂的部件,应该触发带参数的repaint()方法,使用参数定义实际需要更新的区域;而不带参数调用会导致整个部件被重画。
因为对repaint()的调用会首先导致update()的调用,默认地会促成paint()的调用,所以重量级部件应该覆盖update()方法以实现增量绘制,如果需要的话(轻量级部件不支持增量绘制) 。
覆盖了paint()方法的java.awt.Container子类应当在paint()方法中调用super.paint()以保证子部件能被绘制。
界面复杂的部件应该灵活地使用裁剪区来把绘画范围缩小到只包括与裁剪区相交的范围。
在Swing中的绘画
Swing起步于AWT基本绘画模式,并且作了进一步的扩展以获得最大化的性能以及改善可扩展性能。象AWT一样,Swing支持回调绘画以及使用repaint()促使部件更新。另外,Swing提供了内置的双缓冲(double-buffering)并且作了改变以支持Swing的其它结构(象边框(border)和UI代理)。最后,Swing为那些想更进一步定制绘画机制的程序提供了RepaintManager API。
对双缓冲的支持
Swing的最引人注目的特性之一就是把对双缓冲的支持整个儿的内置到工具包。通过设置javax.swing.JComponent的"doubleBuffered"属性就可以使用双缓冲:
public boolean isDoubleBuffered()
public void setDoubleBuffered(boolean o)
当缓冲激活的时候,Swing的双缓冲机制为每个包容层次(通常是每个最高层的窗体)准备一个单独的屏外缓冲。并且,尽管这个属性可以基于部件而设置,对一个特定的容器上设置这个属性,将会影响到这个容器下面的所有轻量级部件把自己的绘画提交给屏外缓冲,而不管它们各自的"双缓冲"属性值
默认地,所有Swing部件的该属性值为true。不过对于JRootPane这种设置确实有些问题,因为这样就使所有位于这个上层Swing部件下面的所有部件都使用了双缓冲。对于大多数的Swing程序,不需要作任何特别的事情就可以使用双缓冲,除非你要决定这个属性是开还是关(并且为了使GUI能够平滑呈现,你需要打开这个属性)。Swing保证会有适宜的Graphics对象(或者是为双缓冲使用的屏外映像的Graphics,或者是正规的Graphics)传递给部件的绘画回调函数,所以,部件需要做的所有事情仅仅就是使用这个Graphics画图。本文的后面,在绘制的处理过程这一章会详细解释这个机制。
其他的绘画属性
为了改善内部的绘画算法性能,Swing另外引进了几个JComponent的相互有关联的属性。引入这些属性为的是处理下面两个问题,这两个问题有可能导致轻量级部件的绘画成本过高:
透明(Transparency): 当一个轻量级部件的绘画结束时,如果该部件的一部分或者全部透明,那么它就可能不会把所有与其相关的像素位都涂上颜色;这就意味着不管它什么时候重画,它底层的部件必须首先重画。这个技术需要系统沿着部件的包容层次去找到最底层的重量级祖先,然后从它开始、从后向前地执行绘画。
重叠的部件(Overlapping components): 当一个轻量级部件的绘画结束是,如果有一些其他的轻量级部件部分地叠加在它的上方;就是说,不管最初的轻量级部件什么时候画完,只要有叠加在它上面的其它部件(裁剪区与叠加区相交),这些叠加的部件必须也要部分地重画。这需要系统在每次绘画时要遍历大量的包容层次,以检查与之重叠的部件。
遮光性
在一般情况下部件是不透明的,为了提高改善性能,Swing增加了读写javax.swing.JComponent的遮光(opaque)属性的操作:
public boolean isOpaque()
public void setOpaque(boolean o)
这些设置是:
true:部件同意在它的矩形范围包含的里所有像素位上绘画。
false:部件不保证其矩形范围内所有像素位上绘画。
遮光(opaque)属性允许Swing的绘图系统去检测是否一个对指定部件的重画请求会导致额外的对其底层祖先的重画。每个标准Swing部件的默认(遮光)opaque属性值由当前的视-感UI对象设定。而对于大多数部件,该值为true。
部件实现中的一个最常见的错误是它们允许遮光(opaque)属性保持其默认值true,却又不完全地呈现它们所辖的区域,其结果就是没有呈现的部分有时会造成屏幕垃圾。当一个部件设计完毕,应该仔细的考虑所控制的遮光(opaque)属性,既要确保透的使用是明智的,因为它会花费更多的绘画时间,又要确保与绘画系统之间的协约履行。
遮光(opaque)属性的意义经常被误解。有时候被用来表示“使部件的背景透明”。然而这不是Swing对遮光的精确解释。一些部件,比如按钮,为了给部件一个非矩形的外形可能会把“遮光”设置为false,或者为了短时间的视觉效果使用一个矩形框围住部件,例如焦点指示框。在这些情况下,部件不遮光,但是其背景的主要部分仍然需要填充。
如先前的定义,遮光属性的本质是一个与负责重画的系统之间订立的契约。如果一个部件使用遮光属性去定义怎样使部件的外观透明,那么该属性的这种使用就应该备有证明文件。(一些部件可能更合适于定义额外的属性控制外观怎样怎样增加透明度。例如,javax.swing.AbstractButton提供ContentAreaFilled属性就是为了达到这个目的。)
另一个毫无价值的问题是遮光属性与Swing部件的边框(border)属性有多少联系。在一个部件上,由Border对象呈现的区域从几何意义上讲仍是部件的一部分。就是说如果部件遮光,它就有责任去填充边框所占用的空间。(然后只需要把边框放到该不透明的部件之上就可以了)。
如果你想使一个部件允许其底层部件能透过它的边框范围而显示出来 -- 即,通过isBorderOpaque()判断border是否支持透明而返回值为false -- 那么部件必须定义自身的遮光属性为false并且确保它不在边框的范围内绘图。
"最佳的"绘画方案
部件重叠的问题有些棘手。即使没有直接的兄弟部件叠加在该部件之上,也总是可能有非直系继承关系(比如"堂兄妹"或者"姑婶")的部件会与它交叠。这样的情况下,处于一个复杂层次中的每个部件的重画工作都需要一大堆的树遍历来确保'正确地'绘画。为了减少不必要的遍历,Swing为javax.swing.JComponent增加一个只读的isOptimizedDrawingEnabled属性:
public boolean isOptimizedDrawingEnabled()
这些设置是:
true:部件指示没有直接的子孙与其重叠。
false: 部件不保证有没有直接的子孙与之交叠。
通过检查isOptimizedDrawingEnabled属性,Swing在重画时可以快速减少对交叠部件的搜索。
因为isOptimizedDrawingEnabled属性是只读的,于是部件改变默认值的唯一方法是在其子类覆盖(override)这个方法来返回所期望的值。除了JLayeredPane,JDesktopPane,和JViewPort外,所有标准Swing部件对这个属性返回true。
绘画方法
适应于AWT的轻量级部件的规则同样也适用于Swing部件 -- 举一个例子,在部件需要呈现的时候就会调用paint() -- 只是Swing更进一步地把paint()的调用分解为3个分立的方法,以下列顺序依次执行:
protected void paintComponent(Graphics g)
protected void paintBorder(Graphics g)
protected void paintChildren(Graphics g)
Swing程序应该覆盖paintComponent()而不是覆盖paint()。虽然API允许这样做,但通常没有理由去覆盖paintBorder()或者paintComponents()(如果你这么做了,请确认你知道你到底在做什么!)。这个分解使得编程变得更容易,程序可以只覆盖它们需要扩展的一部分绘画。例如,这样就解决先前在AWT中提到的问题,因为调用super.paint()失败而使得所有轻量级子孙都不能显示。
SwingPaintDemo例子程序举例说明了Swing的paintComponent()回调方法的简单应用。
绘画与UI代理
大多数标准Swing部件拥有它们自己的、由分离的观-感(look-and-feel)对象(叫做"UI代理")实现的观-感。这意味着标准部件把大多数或者所有的绘画委派给UI代理,并且出现在下面的途径:
paint()触发paintComponent()方法。
如果ui属性为non-null,paintComponent()触发ui.update()。
如果部件的遮光属性为true,ui.udpate()方法使用背景颜色填充部件的背景并且触发ui.paint()。
ui.paint()呈现部件的内容。
这意味着Swing部件的拥有UI代理的子类(相对于JComponent的直系子类),应该在它们所覆盖的paintComponent方法中触发super.paintComponent()。
public class MyPanel extends JPanel {
protected void paintComponent(Graphics g) {
// Let UI delegate paint first
// (including background filling, if I'm opaque)
super.paintComponent(g);
// paint my contents next....
}
}
如果因为某些原因部件的扩展类不允许UI代理去执行绘画(是如果,例如,完全更换了部件的外观),它可以忽略对super.paintComponent()的调用,但是它必须负责填充自己的背景,如果遮光(opaque)属性为true的话,如前面在遮光(opaque)属性一章讲述的。
绘画的处理过程
Swing处理"repaint"请求的方式与AWT有稍微地不同,虽然对于应用开发人员来讲其本质是相同的 -- 同样是触发paint()。Swing这么做是为了支持它的RepaintManager API (后面介绍),就象改善绘画性能一样。在Swing里的绘画可以走两条路,如下所述:
(A) 绘画需求产生于第一个重量级祖先(通常是JFrame、JDialog、JWindow或者JApplet):
事件分派线程调用其祖先的paint()
Container.paint()的默认实现会递归地调用任何轻量级子孙的paint()方法。
当到达第一个Swing部件时,JComponent.paint()的默认执行做下面的步骤:
如果部件的双缓冲属性为true并且部件的RepaintManager上的双缓冲已经激活,将把Graphics对象转换为一个合适的屏外Graphics。
调用paintComponent()(如果使用双缓冲就把屏外Graphics传递进去)。
调用paintChildren()(如果使用双缓冲就把屏外Graphics传递进去),该方法使用裁剪并且遮光和optimizedDrawingEnabled等属性来严密地判定要递归地调用哪些子孙的paint()。
如果部件的双缓冲属性为true并且在部件的RepaintManager上的双缓冲已经激活,使用最初的屏幕Graphics对象把屏外映像拷贝到部件上。
注意:JComponent.paint()步骤#1和#5在对paint()的递归调用中被忽略了(由于paintChildren(),在步骤#4中介绍了),因为所有在swing窗体层次中的轻量级部件将共享同一个用于双缓冲的屏外映像。
(B) 绘画需求从一个javax.swing.JComponent扩展类的repaint()调用上产生:
JComponent.repaint()注册一个针对部件的RepaintManager的异步的重画需求,该操作使用invokeLater()把一个Runnable加入事件队列以便稍后执行在事件分派线程上的需求。
该Runnable在事件分派线程上执行并且导致部件的RepaintManager调用该部件上paintImmediately(),该方法执行下列步骤:
使用裁剪框以及遮光和optimizedDrawingEnabled属性确定“根”部件,绘画一定从这个部件开始(处理透明以及潜在的重叠部件)。
如果根部件的双缓冲属性为true,并且根部件的RepaintManager上的双缓冲已激活,将转换Graphics对象到适当的屏外Graphics。
调用根部件(该部件执行上述(A)中的JComponent.paint()步骤#2-4)上的paint(),导致根部件之下的、与裁剪框相交的所有部件被绘制。
如果根部件的doubleBuffered属性为true并且根部件的RepaintManager上的双缓冲已经激活,使用原始的Graphics把屏外映像拷贝到部件。
注意:如果在重画没有完成之前,又有发生多起对部件或者任何一个其祖先的repaint()调用,所有这些调用会被折叠到一个单一的调用 回到paintImmediately() on topmostSwing部件 on which 其repaint()被调用。例如,如果一个JTabbedPane包含了一个JTable并且在其包容层次中的现有的重画需求完成之前两次发布对repaint()的调用,其结果将变成对该JTabbedPane部件的paintImmediately()方法的单一调用,会触发两个部件的paint()的执行。
这意味着对于Swing部件来说,update()不再被调用。
虽然repaint()方法导致了对paintImmediately()的调用,它不考虑"回调"绘图,并且客户端的绘画代码也不会放置到paintImmediately()方法里面。实际上,除非有特殊的原因,根本不需要超载paintImmediately()方法。
同步绘图
象我们在前面章节所讲述的,paintImmediately()表现为一个入口,用来通知Swing部件绘制自身,确认所有需要的绘画都能适当地产生。这个方法也可能用来安排同步的绘图需求,就象它的名字所暗示的,即一些部件有时候需要保证它们的外观实时地与其内部状态保持一致(例如,在JScrollPane执行滚定操作的时候确实需要这样并且也做到了)。
程序不应该直接调用这个方法,除非有合理实时绘画需要。这是因为异步的repaint()可以使多个重复的需求得到有效的精简,反之直接调用paintImmediately()则做不到这点。另外,调用这个方法的规则是它必须由事件分派线程中的进程调用;它也不是为能以多线程运行你的绘画代码而设计的。关于Swing单线程模式的更多信息,参考一起归档的文章"Threads and Swing."
RepaintManager
Swing的RepaintManager类的目的是最大化地提高Swing包容层次上的重画执行效率,同时也实现了Swing的'重新生效'机制(作为一个题目,将在其它文章里介绍)。它通过截取所有Swing部件的重画需求(于是它们不再需要经由AWT处理)实现了重画机制,并且在需要更新的情况下维护其自身的状态(我们已经知道的"dirty regions")。最后,它使用invokeLater()去处理事件分派线程中的未决需求,如同在"Repaint Processing"一节中描述的那样(B选项).
对于大多数程序来讲,RepaintManager可以看做是Swing的内部系统的一部分,并且甚至可以被忽略。然而,它的API为程序能更出色地控制绘画中的几个要素提供了选择。
"当前的"RepaintManager
RepaintManager设计 is designed to be dynamically plugged, 虽然 有一个单独的接口。下面的静态方法允许程序得到并且设置"当前的"RepaintManager:
public static RepaintManager currentManager(Component c)
public static RepaintManager currentManager(JComponent c)
public static void
setCurrentManager(RepaintManager aRepaintManager)
更换"当前的"RepaintManager
总的说来,程序通过下面的步骤可能会扩展并且更换RepaintManager:
RepaintManager.setCurrentManager(new MyRepaintManager());
你也可以参考RepaintManagerDemo ,这是个简单的举例说明RepaintManager加载的例子,该例子将把有关正在执行重画的部件的信息打印出来。
扩展和替换RepaintManager的一个更有趣的动机是可以改变对重画的处理方式。当前,默认的重画实现所使用的来跟踪dirty regions的内部状态值是包内私有的并且因此不能被继承类访问。然而,程序可以实现它们自己的跟踪dirty regions的机制并且通过超载下面的方法对重画需求的缩减:
public synchronized void
addDirtyRegion(JComponent c, int x, int y, int w, int h)
public Rectangle getDirtyRegion(JComponent aComponent)
public void markCompletelyDirty(JComponent aComponent)
public void markCompletelyClean(JComponent aComponent) {
addDirtyRegion()方法是在调用Swing部件的repaint()的之后被调用的,因此可以用作钩子来捕获所有的重画需求。如果程序超载了这个方法(并且不调用super.addDirtyRegion()),那么它改变了它的职责,而使用invokeLater()把Runnable放置到EventQueue ,该队列将在合适的部件上调用paintImmediately()(translation: not for the faint of heart).
从全局控制双缓冲
RepaintManager提供了从全局中激活或者禁止双缓冲的API:
public void setDoubleBufferingEnabled(boolean aFlag)
public boolean isDoubleBufferingEnabled()
这个属性在绘画处理的时候,在JComponent的内部检查过以确定是否使用屏外缓冲显示部件。这个属性默认为true,但是如果程序希望在全局范围为所有Swing部件关闭双缓冲的使用,可以按照下面的步骤做:
RepaintManager.currentManager(mycomponent).
setDoubleBufferingEnabled(false);
注意:因为Swing的默认实现要初始化一个单独的RepaintManager实例,mycomponent参数与此不相关。
Swing绘画准则
Swing开发人员在写绘画代码时应该理解下面的准则:
对于Swing部件,不管是系统-触发还是程序-触发的请求,总会调用paint()方法;而update()不再被Swing部件调用。
程序可以通过repaint()触发一个异步的paint()调用,但是不能直接调用paint()。
对于复杂的界面,应该调用带参数的repaint(),这样可以仅仅更新由该参数定义的区域;而不要调用无参数的repaint(),导致整个部件重画。
Swing中实现paint()的3个要素是调用3个分离的回调方法:
paintComponent()
paintBorder()
paintChildren()
Swing部件的子类,如果想执行自己的绘画代码,应该把自己的绘画代码放在paintComponent()方法的范围之内。(不要放在paint()里面)。
Swing引进了两个属性来最大化的改善绘画的性能:
opaque: 部件是否要重画它所占据范围中的所有像素位?
optimizedDrawingEnabled: 是否有这个部件的子孙与之交叠?
如故Swing部件的(遮光)opaque属性设置为true,那就表示它要负责绘制它所占据的范围内的所有像素位(包括在paintComponent()中清除它自己的背景),否则会造成屏幕垃圾。
把一个部件设置为遮光(opaque)同时又把它的optimizedDrawingEnabled属性设置为false,将导致在每个绘画操作中要执行更多的处理,因此我们推荐的明智的方法是同时使用透明并且交叠部件。
使用UI代理(包括JPanel)的Swing部件的扩展类的典型作法是在它们自己的paintComponent()的实现中调用super.paintComponent()。因为UI代理可以负责清除一个遮光部件的背景,这将照顾到#5.
Swing通过JComponent的doubleBuffered属性支持内置的双缓冲,所有的Swing部件该属性默认值是true,然而把Swing容器的遮光设置为true有一个整体的构思,把该容器上的所有轻量级子孙的属性打开,不管它们各自的设定。
强烈建议为所有的Swing部件使用双缓冲。
界面复杂的部件应该灵活地运用剪切框来,只对那些与剪切框相交的区域进行绘画操作,从而减少工作量。
总结
不管AWT还是Swing都提供了方便的编程手段使得部件内容能够正确地显示到屏幕上。虽然对于大多数的GUI需要我们推荐使用Swing,但是理解AWT的绘画机制也会给我们带来帮助,因为Swing建立在它的基础上。
关于AWT和Sing的特点就介绍到这里,应用开发人员应该尽力按照本文中介绍的准则来撰写代码,充分发挥这些API功能,使自己的程序获得最佳性能。
--sunfruit
空间数据库Oracle Spatial的建立过程如下:
-- 创建最基本的个人兴趣点表结构
drop table poi;
create table poi (gid INTEGER,
opid INTEGER,
gname VARCHAR2(256),
gshape MDSYS.SDO_GEOMETRY);
-- 更新用户空间数据对象视图(建立索引依赖她)
delete from USER_SDO_GEOM_METADATA where TABLE_NAME='POI' and COLUMN_NAME='GSHAPE';
insert into USER_SDO_GEOM_METADATA values ('poi',
'gshape',
MDSYS.SDO_DIM_ARRAY(MDSYS.SDO_DIM_ELEMENT('lon', -- lontitude
-64800000, -- min(china 26430867 73.41907434101486)
64800000, -- max(china 49679991 137.99997381765377)
1), -- scale (china abs 23249124)
MDSYS.SDO_DIM_ELEMENT('lat', -- latitude
-32400000, -- min(china -1677502 -4.6597267116858045)
32400000, -- max(china 21571819 59.92171939467364)
1)), -- scale (china abs 23249321)
NULL);
-- 插入一个个人兴趣点的SQL语句,使用标准点地物空间数据类型
delete from POI;
insert into POI values (20010001,
1,
'我的家',
MDSYS.SDO_GEOMETRY(2001, -- SDO_GTYPE
NULL, -- SDO_SRID
SDO_POINT_TYPE(41884696, 14377039, NULL), NULL, NULL));
-- 插入一个个人兴趣点的SQL语句,使用另一种点地物空间数据组织结构
delete from POI;
insert into POI values (20010001,
1,
'我的家',
MDSYS.SDO_GEOMETRY(2001, -- SDO_GTYPE
NULL, -- SDO_SRID
NULL, -- SDO_POINT
MDSYS.SDO_ELEM_INFO_ARRAY (1, -- SDO_STARTING_OFFSET
1, -- SDO_ETYPE
1), -- SDO_INTERPRETATION
MDSYS.SDO_ORDINATE_ARRAY (41884696,14377039)));
-- 创建缺省的R-tree空间索引
drop index POI_IDX;
CREATE INDEX POI_IDX on poi(gshape)
INDEXTYPE is MDSYS.SPATIAL_INDEX;
-- PARAMETERS('SDO_LEVEL=10000'); -- 180*60*60*1000*2/100/100*90*60*60*1000*2/100/100 = 8398080000
-- 索引粗滤矩形窗口选择SQL语句(对于点地物对象,索引粗滤的结果是精确的)
SELECT * FROM POI P
WHERE sdo_filter(P.gshape,
mdsys.sdo_geometry(2003,NULL,NULL,
mdsys.sdo_elem_info_array(1,1003,3),
mdsys.sdo_ordinate_array(41883696,14376039, 41885696,14378039)),
'querytype=window') = 'TRUE';
-- 精确匹配矩形窗口选择SQL语句(计算非常耗时)
SELECT * FROM POI P
WHERE sdo_relate(P.gshape,
mdsys.sdo_geometry(2003,NULL,NULL,
mdsys.sdo_elem_info_array(1,1003,3),
mdsys.sdo_ordinate_array(41883696,14376039, 41885696,14378039)),
'mask=INSIDE querytype=window') = 'TRUE';
Java虚拟机的深入研究
作者:刘学超
1 Java技术与Java虚拟机
说起Java,人们首先想到的是Java编程语言,然而事实上,Java是一种技术,它由四方面组成: Java编程语言、Java类文件格式、Java虚拟机和Java应用程序接口(Java API)。它们的关系如下图所示:
图1 Java四个方面的关系
运行期环境代表着Java平台,开发人员编写Java代码(.java文件),然后将之编译成字节码(.class文件)。最后字节码被装入内存,一旦字节码进入虚拟机,它就会被解释器解释执行,或者是被即时代码发生器有选择的转换成机器码执行。从上图也可以看出Java平台由Java虚拟机和Java应用程序接口搭建,Java语言则是进入这个平台的通道,用Java语言编写并编译的程序可以运行在这个平台上。这个平台的结构如下图所示:
在Java平台的结构中, 可以看出,Java虚拟机(JVM) 处在核心的位置,是程序与底层操作系统和硬件无关的关键。它的下方是移植接口,移植接口由两部分组成:适配器和Java操作系统, 其中依赖于平台的部分称为适配器;JVM 通过移植接口在具体的平台和操作系统上实现;在JVM 的上方是Java的基本类库和扩展类库以及它们的API, 利用Java API编写的应用程序(application) 和小程序(Java applet) 可以在任何Java平台上运行而无需考虑底层平台, 就是因为有Java虚拟机(JVM)实现了程序与操作系统的分离,从而实现了Java 的平台无关性。
那么到底什么是Java虚拟机(JVM)呢?通常我们谈论JVM时,我们的意思可能是:
- 对JVM规范的的比较抽象的说明;
- 对JVM的具体实现;
- 在程序运行期间所生成的一个JVM实例。
对JVM规范的的抽象说明是一些概念的集合,它们已经在书《The Java Virtual Machine Specification》(《Java虚拟机规范》)中被详细地描述了;对JVM的具体实现要么是软件,要么是软件和硬件的组合,它已经被许多生产厂商所实现,并存在于多种平台之上;运行Java程序的任务由JVM的运行期实例单个承担。在本文中我们所讨论的Java虚拟机(JVM)主要针对第三种情况而言。它可以被看成一个想象中的机器,在实际的计算机上通过软件模拟来实现,有自己想象中的硬件,如处理器、堆栈、寄存器等,还有自己相应的指令系统。
JVM在它的生存周期中有一个明确的任务,那就是运行Java程序,因此当Java程序启动的时候,就产生JVM的一个实例;当程序运行结束的时候,该实例也跟着消失了。下面我们从JVM的体系结构和它的运行过程这两个方面来对它进行比较深入的研究。
2 Java虚拟机的体系结构
刚才已经提到,JVM可以由不同的厂商来实现。由于厂商的不同必然导致JVM在实现上的一些不同,然而JVM还是可以实现跨平台的特性,这就要归功于设计JVM时的体系结构了。
我们知道,一个JVM实例的行为不光是它自己的事,还涉及到它的子系统、存储区域、数据类型和指令这些部分,它们描述了JVM的一个抽象的内部体系结构,其目的不光规定实现JVM时它内部的体系结构,更重要的是提供了一种方式,用于严格定义实现时的外部行为。每个JVM都有两种机制,一个是装载具有合适名称的类(类或是接口),叫做类装载子系统;另外的一个负责执行包含在已装载的类或接口中的指令,叫做运行引擎。每个JVM又包括方法区、堆、Java栈、程序计数器和本地方法栈这五个部分,这几个部分和类装载机制与运行引擎机制一起组成的体系结构图为:
图3 JVM的体系结构
JVM的每个实例都有一个它自己的方法域和一个堆,运行于JVM内的所有的线程都共享这些区域;当虚拟机装载类文件的时候,它解析其中的二进制数据所包含的类信息,并把它们放到方法域中;当程序运行的时候,JVM把程序初始化的所有对象置于堆上;而每个线程创建的时候,都会拥有自己的程序计数器和Java栈,其中程序计数器中的值指向下一条即将被执行的指令,线程的Java栈则存储为该线程调用Java方法的状态;本地方法调用的状态被存储在本地方法栈,该方法栈依赖于具体的实现。
下面分别对这几个部分进行说明。
执行引擎处于JVM的核心位置,在Java虚拟机规范中,它的行为是由指令集所决定的。尽管对于每条指令,规范很详细地说明了当JVM执行字节码遇到指令时,它的实现应该做什么,但对于怎么做却言之甚少。Java虚拟机支持大约248个字节码。每个字节码执行一种基本的CPU运算,例如,把一个整数加到寄存器,子程序转移等。Java指令集相当于Java程序的汇编语言。
Java指令集中的指令包含一个单字节的操作符,用于指定要执行的操作,还有0个或多个操作数,提供操作所需的参数或数据。许多指令没有操作数,仅由一个单字节的操作符构成。
虚拟机的内层循环的执行过程如下:
do{
取一个操作符字节;
根据操作符的值执行一个动作;
}while(程序未结束)
由于指令系统的简单性,使得虚拟机执行的过程十分简单,从而有利于提高执行的效率。指令中操作数的数量和大小是由操作符决定的。如果操作数比一个字节大,那么它存储的顺序是高位字节优先。例如,一个16位的参数存放时占用两个字节,其值为:
第一个字节*256+第二个字节字节码。
指令流一般只是字节对齐的。指令tableswitch和lookup是例外,在这两条指令内部要求强制的4字节边界对齐。
对于本地方法接口,实现JVM并不要求一定要有它的支持,甚至可以完全没有。Sun公司实现Java本地接口(JNI)是出于可移植性的考虑,当然我们也可以设计出其它的本地接口来代替Sun公司的JNI。但是这些设计与实现是比较复杂的事情,需要确保垃圾回收器不会将那些正在被本地方法调用的对象释放掉。
Java的堆是一个运行时数据区,类的实例(对象)从中分配空间,它的管理是由垃圾回收来负责的:不给程序员显式释放对象的能力。Java不规定具体使用的垃圾回收算法,可以根据系统的需求使用各种各样的算法。
Java方法区与传统语言中的编译后代码或是Unix进程中的正文段类似。它保存方法代码(编译后的java代码)和符号表。在当前的Java实现中,方法代码不包括在垃圾回收堆中,但计划在将来的版本中实现。每个类文件包含了一个Java类或一个Java界面的编译后的代码。可以说类文件是Java语言的执行代码文件。为了保证类文件的平台无关性,Java虚拟机规范中对类文件的格式也作了详细的说明。其具体细节请参考Sun公司的Java虚拟机规范。
Java虚拟机的寄存器用于保存机器的运行状态,与微处理器中的某些专用寄存器类似。Java虚拟机的寄存器有四种:
- pc: Java程序计数器;
- optop: 指向操作数栈顶端的指针;
- frame: 指向当前执行方法的执行环境的指针;。
- vars: 指向当前执行方法的局部变量区第一个变量的指针。
在上述体系结构图中,我们所说的是第一种,即程序计数器,每个线程一旦被创建就拥有了自己的程序计数器。当线程执行Java方法的时候,它包含该线程正在被执行的指令的地址。但是若线程执行的是一个本地的方法,那么程序计数器的值就不会被定义。
Java虚拟机的栈有三个区域:局部变量区、运行环境区、操作数区。
局部变量区
每个Java方法使用一个固定大小的局部变量集。它们按照与vars寄存器的字偏移量来寻址。局部变量都是32位的。长整数和双精度浮点数占据了两个局部变量的空间,却按照第一个局部变量的索引来寻址。(例如,一个具有索引n的局部变量,如果是一个双精度浮点数,那么它实际占据了索引n和n+1所代表的存储空间)虚拟机规范并不要求在局部变量中的64位的值是64位对齐的。虚拟机提供了把局部变量中的值装载到操作数栈的指令,也提供了把操作数栈中的值写入局部变量的指令。
运行环境区
在运行环境中包含的信息用于动态链接,正常的方法返回以及异常捕捉。
动态链接
运行环境包括对指向当前类和当前方法的解释器符号表的指针,用于支持方法代码的动态链接。方法的class文件代码在引用要调用的方法和要访问的变量时使用符号。动态链接把符号形式的方法调用翻译成实际方法调用,装载必要的类以解释还没有定义的符号,并把变量访问翻译成与这些变量运行时的存储结构相应的偏移地址。动态链接方法和变量使得方法中使用的其它类的变化不会影响到本程序的代码。
正常的方法返回
如果当前方法正常地结束了,在执行了一条具有正确类型的返回指令时,调用的方法会得到一个返回值。执行环境在正常返回的情况下用于恢复调用者的寄存器,并把调用者的程序计数器增加一个恰当的数值,以跳过已执行过的方法调用指令,然后在调用者的执行环境中继续执行下去。
异常捕捉
异常情况在Java中被称作Error(错误)或Exception(异常),是Throwable类的子类,在程序中的原因是:①动态链接错,如无法找到所需的class文件。②运行时错,如对一个空指针的引用。程序使用了throw语句。
当异常发生时,Java虚拟机采取如下措施:
- 检查与当前方法相联系的catch子句表。每个catch子句包含其有效指令范围,能够处理的异常类型,以及处理异常的代码块地址。
- 与异常相匹配的catch子句应该符合下面的条件:造成异常的指令在其指令范围之内,发生的异常类型是其能处理的异常类型的子类型。如果找到了匹配的catch子句,那么系统转移到指定的异常处理块处执行;如果没有找到异常处理块,重复寻找匹配的catch子句的过程,直到当前方法的所有嵌套的catch子句都被检查过。
- 由于虚拟机从第一个匹配的catch子句处继续执行,所以catch子句表中的顺序是很重要的。因为Java代码是结构化的,因此总可以把某个方法的所有的异常处理器都按序排列到一个表中,对任意可能的程序计数器的值,都可以用线性的顺序找到合适的异常处理块,以处理在该程序计数器值下发生的异常情况。
- 如果找不到匹配的catch子句,那么当前方法得到一个"未截获异常"的结果并返回到当前方法的调用者,好像异常刚刚在其调用者中发生一样。如果在调用者中仍然没有找到相应的异常处理块,那么这种错误将被传播下去。如果错误被传播到最顶层,那么系统将调用一个缺省的异常处理块。
操作数栈区
机器指令只从操作数栈中取操作数,对它们进行操作,并把结果返回到栈中。选择栈结构的原因是:在只有少量寄存器或非通用寄存器的机器(如Intel486)上,也能够高效地模拟虚拟机的行为。操作数栈是32位的。它用于给方法传递参数,并从方法接收结果,也用于支持操作的参数,并保存操作的结果。例如,iadd指令将两个整数相加。相加的两个整数应该是操作数栈顶的两个字。这两个字是由先前的指令压进堆栈的。这两个整数将从堆栈弹出、相加,并把结果压回到操作数栈中。
每个原始数据类型都有专门的指令对它们进行必须的操作。每个操作数在栈中需要一个存储位置,除了long和double型,它们需要两个位置。操作数只能被适用于其类型的操作符所操作。例如,压入两个int类型的数,如果把它们当作是一个long类型的数则是非法的。在Sun的虚拟机实现中,这个限制由字节码验证器强制实行。但是,有少数操作(操作符dupe和swap),用于对运行时数据区进行操作时是不考虑类型的。
本地方法栈,当一个线程调用本地方法时,它就不再受到虚拟机关于结构和安全限制方面的约束,它既可以访问虚拟机的运行期数据区,也可以使用本地处理器以及任何类型的栈。例如,本地栈是一个C语言的栈,那么当C程序调用C函数时,函数的参数以某种顺序被压入栈,结果则返回给调用函数。在实现Java虚拟机时,本地方法接口使用的是C语言的模型栈,那么它的本地方法栈的调度与使用则完全与C语言的栈相同。
3 Java虚拟机的运行过程
上面对虚拟机的各个部分进行了比较详细的说明,下面通过一个具体的例子来分析它的运行过程。
虚拟机通过调用某个指定类的方法main启动,传递给main一个字符串数组参数,使指定的类被装载,同时链接该类所使用的其它的类型,并且初始化它们。例如对于程序:
class HelloApp
{
public static void main(String[] args)
{
System.out.println("Hello World!");
for (int i = 0; i < args.length; i++ )
{
System.out.println(args[i]);
}
}
}
编译后在命令行模式下键入: java HelloApp run virtual machine
将通过调用HelloApp的方法main来启动java虚拟机,传递给main一个包含三个字符串"run"、"virtual"、"machine"的数组。现在我们略述虚拟机在执行HelloApp时可能采取的步骤。
开始试图执行类HelloApp的main方法,发现该类并没有被装载,也就是说虚拟机当前不包含该类的二进制代表,于是虚拟机使用ClassLoader试图寻找这样的二进制代表。如果这个进程失败,则抛出一个异常。类被装载后同时在main方法被调用之前,必须对类HelloApp与其它类型进行链接然后初始化。链接包含三个阶段:检验,准备和解析。检验检查被装载的主类的符号和语义,准备则创建类或接口的静态域以及把这些域初始化为标准的默认值,解析负责检查主类对其它类或接口的符号引用,在这一步它是可选的。类的初始化是对类中声明的静态初始化函数和静态域的初始化构造方法的执行。一个类在初始化之前它的父类必须被初始化。整个过程如下:
图4:虚拟机的运行过程
4 结束语
本文通过对JVM的体系结构的深入研究以及一个Java程序执行时虚拟机的运行过程的详细分析,意在剖析清楚Java虚拟机的机理。