??? --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虛擬機的機理。