導航:首頁 > 編程語言 > javaaskto

javaaskto

發布時間:2023-05-05 23:20:23

Ⅰ 請教java編程高手一個問題,如何在面板上繪圖

用 Java繪圖一直都吸引著開發人員的注意。傳統上,Java 開發人員使用 java.awt.Graphics 或 Java 2D API 進行繪圖。一些開發人員甚至使用現成的開源工具箱(如 JSci)來繪圖。但很多時候,您的選擇被限定在了 AWT 或 Swing 上。為了最大限度地減少對第三方工具箱的依賴,或者為了簡化繪圖基礎,可以考慮使用 Draw2D,並編寫自己的代碼來制圖或繪圖。

Draw2D 簡介

Draw2D 是一個駐留在 SWT Composite 之上的輕量級窗口小部件系統。一個 Draw2D 實例 由一個 SWT Composite、一個輕量級系統及其內容的圖形組成。圖形 是 Draw2D 的構建塊。關於 Draw2D API 的所有細節,可以從 Draw2D Developer』s Guide 的 Eclipse 幫助文件中找到。因為本文不打算成為一篇講述 Draw2D 的教程,所以,為了簡便起見,只要您了解 Draw2D API 可以幫助您在 SWT Canvas 上進行繪圖就足夠了。您可以直接使用一些標準的圖形,比如 Ellipse、Polyline、RectangleFigure 和 Triangle,或者,您可以擴展它們來創建自己的圖形。此外,一些容器圖形,如 Panel,可以充當所有子圖形的總容器。

Draw2D 有兩個重要的包:org.eclipse.draw2d.geometry 和 org.eclipse.draw2d.graph,本文中使用了這兩個包。org.eclipse.draw2d.geometry 包有一些有用的類,比如 Rectangle、Point 和 PointList,這些類都是自我解釋的。另一個包 org.eclipse.draw2d.graph 開發人員使用的可能不是太多。這笑激盯個包提供了一些重要的類,比如 DirectedGraph、Node、Edge、NodeList 和 EdgeList,這些類有助於創建圖表。

在本文中,我將解釋如何使用 Draw2D 編寫代碼,幫助您鉛升以圖形的方式形象化您的數據。碰和我將從一項技術的描述開始,該技術將位於某一范圍內的數據值(比如,從 0 到 2048)按比例縮放成另一范圍內的等效數據值(例如,從 0 到 100)。然後,我將舉例說明如何繪制出任意個級數的 X-Y 坐標圖,每個級數都包含一組數據元素。在學習了本文中的概念之後,就可以很容易地繪制其他類型的圖表,比如餅圖和條形圖。

具體的繪圖過程

步驟 1:您想繪制什麼樣的圖形?

顯然,您想以圖形方式描繪來自數據源的數據。所以,您需要那些您想以圖形形式形象化的數據。為了簡便起見,我使用了一個名為 dataGenerator 的簡單函數生成的數據,而不是從 XML 文件或其他一些數據源讀取數據,該函數使用了一個 for(;;) 循環,並以數組列表的形式返回生成的值。
清單 1. 生成一些數據

private ArrayList dataGenerator() {
double series1[] = new double[5];
for(int i=0; i<series1.length; i++)
series1[i] = (i*10) + 10; // a linear
series containing 10,20,30,40,50

double series2[] = new double[9];
series2[0] = 20; series2[1] = 150; series2[2] = 5;
series2[3] = 90; series2[4] = 35; series2[5] = 20;
series2[6] = 150; series2[7] = 5; series2[8] = 45;

double series3[] = new double[7];
for(int i=0; i<series3.length; i++)
series3[i] = (i*20) + 15;

seriesData.add(series1);
seriesData.add(series2);
seriesData.add(series3);
return seriesData;
}

步驟 2:縮放技術 —— 從給定的數據生成 X 坐標和 Y 坐標

一些新的術語

FigureCanvas
Draw2D 中的 FigureCanvas 是 SWT Canvas 的一個擴展。FigureCanvas 可以包含 Draw2D 圖形。
Panel
Panel 是 Draw2D 中的一個通用容器圖形,它可以包含子圖形。您可以向一個 Panel 圖形中添加許多圖形,然後將這個 Panel 圖形提供給 FigureCanvas。
DirectedGraph
DirectedGraph 是一個 2-D 圖形,擁有有限數量的 Node,每個 Node 都位於一些 Point 中,相鄰的 Node 是通過 Edges 彼此連接在一起的。

當您想繪制一架 2-D 飛機上的點時,必須找出每個點的 X 坐標和 Y 坐標。繪圖的奇妙之處在於能夠將某一個給定數據值從一個范圍按比例縮放到另一個范圍中,也就是說,如果給定一組值,如 {10,20,30},那麼您應該能夠確定 2-D 飛機上具體哪些點(X 坐標和 Y 坐標)表示的是 10、20 和 30 這些數據值。

繪制總是在按照某一個限定縮放比例進行的。換句話說,在同一限定區域內,可以繪制任意數量的點。因為該區域是固定的,所以您總是可以找到 X 坐標軸的跨度(長度)和 Y 坐標軸的跨度(高度)。X 坐標軸和 Y 坐標軸的跨度只是等式的一部分。另一部分是找出數據值的范圍,並根據每個數據值在新范圍內的等效值來計算這些值的坐標。

計算 X 坐標和 Y 坐標

X 坐標:X 坐標是某一個點距離原點的水平距離。計算元素的數量,然後將 X 坐標軸的跨度分成 n 個區段,其中,n 是給定集合中的元素的數量,通過這種方式,可以計算某一集合中的所有點的橫向坐標。用這種分割方法可以獲得每個區段的長度。集合中的第一個點位於等於區段長度的第一段距離內。後續的每個點則位於區段長度加上原點到前一個點的距離的那一段距離內。

例如,給出一個集合 {10,20,30,40},您立刻就可以知道要繪制 4 個點,因為集合中包含 4 個元素。所以,應該將 X 坐標軸的跨度分成 4 個相等的區段,每個區段的長度 = 跨度/4。因此,如果 X 坐標軸的跨度是 800,那麼區段的長度將是 800/4,即 200。第一個元素(10)的 X 坐標將是 200,第二個元素(20)的 X 坐標將是 400,依此類推。
清單 2. 計算 X 坐標

private int[] getXCoordinates(ArrayList seriesData){
int xSpan = (int)GraFixConstants.xSpan;
int longestSeries = Utilities.getLongestSeries(seriesData);
int numSegments =
((double[])seriesData.get(longestSeries)).length;
int sectionWidth =
(int)xSpan / numSegments; //want to divide span of xAxis

int xPositions[] =
new int[numSegments]; // will contain X-coordinate of all dots.
for(int i=0; i<numSegments; i++){
xPositions[i]=
(i+1)*sectionWidth;//dots spaced at distance of sectionWidth
}
return xPositions;
}

Y 坐標:Y 坐標是某一個點距離原點的縱向距離。計算 Y 坐標要將某一個值按比例從一個范圍縮放到另一個范圍。例如,給出相同的集合 {10,20,30,40},您可以看出,數據的范圍是 0 到 40,新的范圍就是 Y 坐標軸的跨度(高度)。假設 Y 坐標軸的高度為 400,那麼第一個元素(10)的高度將是100,第二個元素的高度將是 200,依此類推。

通過以下例子,您可以更好地理解如何按比例將一個值從一個范圍縮放到另一個范圍:假定一個范圍的跨度是從 0 到 2048,而您打算將該范圍內的任意值(比如說 1024)縮放到另一個從 0 到 100 的范圍內,那麼您立刻就可以知道,等刻度值是 50。該縮放所遵循的三值線演算法是:

line 1---> 2048 / 1024 equals 2.
line 2---> 100 - 0 equals 100.
line 3---> 100 / 2 equals 50, which is the desired scaled value.

步驟 3:您想在哪兒進行繪圖?

您還需要進行繪圖的地方。可以通過擴展 Eclipse ViewPart 和使用 SWT Composite 來創建您自己的視圖。此外,也可以使用從 main() 函數中調用的 SWT shell。

在擴展 Eclipse ViewPart 時,至少必須實現兩個函數:createPartControl(Composite parent) 和 setFocus()。函數 createPartControl(Composite parent) 是在屏幕上繪制視圖時自動調用的。您的興趣只在所接收的 SWT Composite 上。因此,將它傳遞給某個類,然後通過對這個類進行編碼來繪制圖形。
清單 3. 使用 Eclipse ViewPart 繪圖

public class MainGraFixView extends ViewPart{
public void createPartControl(Composite parent) {

//create or get data in an arraylist
ArrayList seriesData = dataGenerator();

//instantiate a plotter, and provide data to it.
DirectedGraphXYPlotter dgXYGraph = new DirectedGraphXYPlotter(parent);
dgXYGraph.setData(seriesData);
dgXYGraph.plot(); //ask it to plot

}
public void setFocus() {
}
}

步驟 4;您需要繪制哪種圖形?

一旦擁有了數據以及想用來繪制圖形的區域,就必須確定您需要哪種類型的可視化。在本文中,我演示了如何編寫代碼來創建 X-Y 坐標圖和線形圖。一旦知道了繪制 X-Y 坐標圖的技術,就應該能夠繪制出其他圖形,比如條形圖和餅圖。要想更多地了解 X-Y 坐標圖,請參閱我為本文編寫的 DirectedGraphXYPlotter 類(參見所附源代碼中的 \src\GraFix\Plotters\DirectedGraphXYPlotter.java)。

步驟 5:創建自己的 X-Y 坐標圖

X-Y 坐標圖應該能夠繪制出 2-D 飛機上的任意數量的級數線。每個級數線都應該以圖形形式顯示出引用 X 和 Y 引用線的那些級數中的每個點的位置。每個點都應該通過一條線連接到級數中的下一個點上。通過使用表示一個點和一條線的 Draw2D 圖形,您應該能夠創建這樣一個坐標圖。例如,為了表示一個點,我通過擴展 Ellipse 圖形創建了一個 Dot 圖形,並使用 PolylineConnection 圖形來表示連接線。

DirectedGraphXYPlotter 類只有兩個公共函數:setData(ArrayList seriesData) 和 plot()。函數 setData(ArrayList seriesData) 接受您想要以圖形形式形象化的數據(參見步驟 1),而 plot() 函數則開始繪圖。

一旦調用了 plot() 函數,就必須依次採用以下步驟:

採用一個 SWT Composite,並將 FigureCanvas 放在它之上。然後,將一個類似 Panel 的通用容器圖放在畫布上。
計算將要繪制的級數的數量,然後填充創建 DirectedGraphs 所需數量的 NodeLists 和 EdgeLists。
在 Panel 圖上繪制 X 坐標軸和 Y 坐標軸。(參見所附源代碼中 \src\GraFix\Figure 目錄下的 XRulerBar.java 和 YRulerBar.java。)
創建和級數一樣多的 DirectedGraphs,以便進行繪圖。
在 Panel 圖上繪制點和連接線,同時採用步驟 d 中創建的 DirectedGraphs 中的圖形數據。
最後,通過提供 Panel 圖來設置畫布的內容,其中包括到目前為止您已經准備好的所有的點和連接線。

在以下代碼中:

第 6-11 行代碼對應於上述的步驟 a。

第 14 行,即函數 populateNodesAndEdges(),對應於上述的步驟 b。

第 16 行,即函數 drawAxis(),對應於上述的步驟 c。

第 17 行、第 18 行和第 19 行對應於上述的步驟 d 和步驟 e。

第 20 行對應於上述的步驟 f。
清單 4. plot() 函數

1. public void plot(){
2. //if no place to plot, or no data to plot, return.
3. if(null==_parent || null==_seriesData)
4. return;
5.
6. Composite composite = new Composite(_parent, SWT.BORDER);
7. composite.setLayout(new FillLayout());
8. FigureCanvas canvas = new FigureCanvas(composite);
9.
10. Panel contents = new Panel();//A Panel is a general purpose container figure
11. contents.setLayoutManager(new XYLayout());
12. initializeSpan(contents.getClientArea());
13.
14. populateNodesAndEdges();
15.
16. drawAxis(contents);
17. for(int i=0; i<_numSeries; i++){
18. drawDotsAndConnections(contents,getDirectedGraph(i)); //
draw points & connecting wires
19. }
20. canvas.setContents(contents);
21. }

plot() 調用了兩個重要內部函數來幫助繪制圖形中的點:populateNodesAndEdges() 和 drawDotsAndConnections()。在您發現這兩個函數到底完成什麼功能之前,讓我們來看一下 DirectedGraph。

DirectedGraph 是什麼?為了使用 Draw2D 進行繪圖,事實上您必須先創建一個圖形,定義將要繪制的點和線。一旦創建好這個圖形,就可以使用它實際在畫布上進行繪圖。您可以將 DirectedGraph 形象化為擁有有限數量的 Node 的一個 2-D 圖形,在該圖形中,每個 Node 都位於一些 Point 上,相鄰的 Node 是通過 Edges 連接在一起的。

您可以通過以下代碼行來了解創建 DirectedGraph 的關鍵所在。首先,創建一個 Node 列表和一個 Edges 列表。然後,創建一個新的 DirectedGraph,並通過剛才創建的 NodeList 和 EdgeList 設置其成員(Nodes 和 Edges)。現在,使用 GraphVisitor 來訪問這個 DirectedGraph。為了簡便起見,包 org.eclipse.draw2d.internal.graph 中有許多 GraphVisitor 實現,這些 GraphVisitor 有一些用來訪問圖形的特定演算法。

因此,創建 DirectedGraph 的示例代碼類似於下面這樣:
清單 5. 示例 DirectedGraph

//This is a sample, you will need to add actual Node(s) to this NodeList.
NodeList nodes = new NodeList(); //create a list of nodes.
//This is a sample, you will need to add actual Edge(s) to this EdgeList.
EdgeList edges = new EdgeList(); //create a list of edges.
DirectedGraph graph = new DirectedGraph();
graph.nodes = nodes;
graph.edges = edges;
new BreakCycles().visit(graph);//ask BreakCycles to visit the graph.
//now our "graph" is ready to be used.

現在,已經知道 DirectedGraph 包含許多 Node,其中,每個 Node 都可能包含一些數據,並且還存儲了這些數據的 X 坐標和 Y 坐標,以及一個 Edges 的列表,每個 Edge 都知道在自己的兩端分別有一個 Node,您可以通過以下技術,使用這些信息來繪圖,其中涉及兩個部分:部分 A —— 通過以下步驟填充 Node 和 Edge:

創建一個 NodeList,在該列表中,集合中的每個元素都有一個 Node,集合 {10,20,30,40} 需要 4 個 Node。

找出每個元素的 X 坐標和 Y 坐標,將它們存儲在 node.x 和 node.y 成員變數中。

創建一個 EdgeList,在該列表中,有 n -1 個 Edge,其中,n 是集合中的元素的數量。例如,集合 {10,20,30,40} 需要三個 Edge。

將 Node 與每個 Edge 的左右端相關聯,並相應地設置 edge.start 和 edge.end 成員變數。

部分 B —— 通過以下步驟繪製表示 Node 和 Edge 的圖形:

繪制一個 Dot 圖來表示每個 Node。
繪制一個 PolylineConnection 圖形來表示每個 Edge。
界定每個 PolylineConnection 圖形,以固定 Dot 圖的左右端。

現在,回到內部函數的工作上來:

函數 populateNodesAndEdges() 實現了該技術的部分 A,而函數 drawDotsAndConnections() 則實現了該技術的部分 B。

函數 populateNodesAndEdges() 計算將繪制多少級數。它為每個級數創建了一個 NodeList 和一個 EdgeList。

每個 NodeList 都包含一個用於特殊級數的 Node 的列表。每個 Node 都保存著關於應該在什麼地方繪制 X 坐標和 Y 坐標的信息。函數 getXCoordinates() 和 getYCoordinates() 分別用於檢索 X 坐標值和 Y 坐標值。使用步驟 2 中的相同演算法,這些函數也可以內部地將數據值按比例從一個范圍縮放到另一個范圍。

每個 EdgeList 都包含一個用於特殊級數的 Edges 的列表。每個 Edge 的左右端上都分別有一個 Node。
清單 6. populateNodesAndEdges() 函數

private void populateNodesAndEdges(){

_seriesScaledValues = new ArrayList(getScaledValues(_seriesData));
_nodeLists = new ArrayList();
_edgeLists = new ArrayList();

for(int i=0; i<_numSeries; i++){
_nodeLists.add(new NodeList());// one NodeList per series.
_edgeLists.add(new EdgeList());// one EdgeList per series.
}
//populate all NodeLists with the Nodes.
for(int i=0; i<_numSeries; i++){//for each series
double data[] = (double[])_seriesData.get(i);//get the series
int xCoOrds[] = getXCoordinates(_seriesData);
int yCoOrds[] = getYCoordinates(i, data);
//each NodeList has as many Nodes as points in a series
for(int j=0; j<data.length; j++){
Double doubleValue = new Double(data[j]);
Node node = new Node(doubleValue);
node.x = xCoOrds[j];
node.y = yCoOrds[j];
((NodeList)_nodeLists.get(i)).add(node);
}
}
//populate all EdgeLists with the Edges.
for(int i=0; i<_numSeries; i++){
NodeList nodes = (NodeList)_nodeLists.get(i);
for(int j=0; j<nodes.size()-1; j++){
Node leftNode = nodes.getNode(j);
Node rightNode = nodes.getNode(j+1);
Edge edge = new Edge(leftNode,rightNode);
edge.start = new Point(leftNode.x, leftNode.y);
edge.end = new Point(rightNode.x, rightNode.y);
((EdgeList)_edgeLists.get(i)).add(edge);
}
}
int breakpoint = 0;
}

一旦函數 populateNodesAndEdges() 完成了它的使命,為所有將要繪制的級數創建了 NodeLists 和 EdgeLists,另一個函數 drawDotsAndConnections() 就開始為每個 Node 繪制一個 Dot 圖形,並為每個 Edge 繪制一個 PolylineConnection 圖形。
清單 7. drawDotsAndConnections()、drawNode() 和 drawEdge() 函數

private void drawDotsAndConnections(IFigure contents, DirectedGraph graph){
for (int i = 0; i < graph.nodes.size(); i++) {
Node node = graph.nodes.getNode(i);
drawNode(contents, node);
}
for (int i = 0; i < graph.edges.size(); i++) {
Edge edge = graph.edges.getEdge(i);
drawEdge(contents, edge);
}
}

private void drawNode(IFigure contents, Node node){
Dot dotFigure = new Dot();
node.data = dotFigure;
int xPos = node.x;
int yPos = node.y;
contents.add(dotFigure);
contents.setConstraint(dotFigure, new Rectangle(xPos,yPos,-1,-1));
}

private void drawEdge(IFigure contents, Edge edge){
PolylineConnection wireFigure = new PolylineConnection();
//edge.source is the Node to the left of this edge
EllipseAnchor sourceAnchor = new EllipseAnchor((Dot)edge.source.data);

//edge.target is the Node to the right of this edge
EllipseAnchor targetAnchor = new EllipseAnchor((Dot)edge.target.data);
wireFigure.setSourceAnchor(sourceAnchor);
wireFigure.setTargetAnchor(targetAnchor);
contents.add(wireFigure);
}

繪圖結果

結束語

如果您想以圖形形式描繪將展示的數據,那麼 Draw2D 是一個好工具。可以使用 Draw2D 編寫自己的用來繪制圖形的 Java 代碼,這有助於您將精力集中於縮放代碼和繪制代碼上,把其他與繪制相關的工作留給 Draw2D 和 SWT。您還可以通過使用所選擇的 Draw2D 圖形來控制您的圖形的外觀。Draw2D 簡化了繪圖的基本步驟,並且可以最大限度地減少您對第三方工具箱的依賴。

Ⅱ 簡單的JAVA字元串長度計算的實現

簡單實現代碼如下:
public
class
stringlength
{
/**
*
獲取字元串的長度,如果有中文,則每個中文字元計為2位
*
@param
value
指定的字元串
*
@return
字元串的長度
*/
public
static
int
length(string
value)
{
int
valuelength
=
0;
string
chinese
=
"[\u0391-\uffe5]";
/*
獲取欄位值的長度,如果含中文字元,則每個中文字元長度為2,否則為1
*/
for
(int
i
=
0;
i
<
value.length();
i++)
{
/*
獲取一個字元
*/
string
temp
=
value.substring(i,
i
+
1);
/*
判斷是否為中文字元
*/
if
(temp.matches(chinese))
{
/*
中文字元長度為2
*/
valuelength
+=
2;
}
else
{
/*
其他字元長度為1
*/
valuelength
+=
1;
}
}
return
valuelength;
}
public
static
void
main(string
args[]){
string
str
=
"hello你好";
system.out.println(stringlength.length(str));
}
}

Ⅲ Java中對象作為傳遞參數的函數如何調用

NullPointerException說明有對象為空(null)。

person []ps=new person[3];//建立3個person類的對象

---上面這句話有問題
這句話是建立一個長度擾橋為兄拿3的person數組,但是數組內對象還是空的。
需要用ps[0]=new person();這種方式羨李搭逐個將數組內對象初始化。

Ⅳ java中int怎樣轉換成char

java將int類型的數字轉換成char型,主要是通過強制類型轉換,如下代碼:

public class Int2CharDemo { public static void main(String[] args) { // 將int類型數字8轉換為char類型數字8

int num1 = 8;

char ch1 = (char) (num1 + 48);

System.out.println("ch1 = " + ch1); // 將char類型數字8轉換為int類型數字8

// 方法一:

Character ch2 = '8'; // char是基本數據類型,Character是其包裝類型。

int num2 = Integer.parseInt(ch2.toString());

System.out.println("num2 = " + num2);

// 方法二:

char ch3 = '8';

int num3 = ch3 - 48;

System.out.println("num3 = " + num3); }



(4)javaaskto擴展閱讀:

當編輯並運行一個Java程序時,需要同時涉及到這四種方面。使用文字編輯軟體(例如記事本、寫字板、UltraEdit等)或集成開發環境(Eclipse、MyEclipse等)在Java源文件中定義不同的類。

C語言中如int、long、short等不指定signed或unsigned時都默認為signed,但char在標准中不指定為signed或unsigned,編譯器可以編譯為帶符號的,也可以編譯為不帶符號的。

Java看起來設計得很像C++,但是為了使語言小和容易熟悉,設計者們把C++語言中許多可用的特徵去掉了,這些特徵是一般程序員很少使用的。例如,Java不支持go to語句,代之以提供break和continue語句以及異常處理。

Java還剔除了C++的操作符過載(overload)和多繼承特徵,並且不使用主文件,免去了預處理程序。因為Java沒有結構,數組和串都是對象,所以不需要指針。Java能夠自動處理對象的引用和間接引用,實現自動的無用單元收集,使用戶不必為存儲管理問題煩惱,能更多的時間和精力花在研發上。

Ⅳ 哪本Java 面試書籍比較好

BooksThinking
in
Java
(Java
101,
must
read)
Java
Puzzlers
(適合面試官,
如果你找不到很好的java
面試題的話,
可以從這本書找些靈感,
其中很多"奇技淫巧",
小尺芹肆心走火入魔)
Java
Concurrency
In
practice,
對於java
伺服器開發,大並發,很適合。
Concurrent
Programming
in
Java:
Design
Principles
and
PatternsDoug
Lea語言和虛擬機方面可以參考
Inside
the
Java
2
Virtual
Machine
Inside
Java
2
Platform
Security
by
宮力.
Garbage
Collection:
Algorithms
for
Automatic
Dynamic
Memory
Management這些書的作者多數是當年Sun
Java
team
的成員。
另外Quora
上很多問題可以關注一下
What
questions
are
Java
Software
Engineers
seeing
the
most
of
on
technical
interviews?Java
(programming
language):
What
are
good
interview
questions
to
ask
JAVA
developers?作為面試官,當你的對手聲稱對Java
非常了解的時候,
可以就上述話題展開較深入的討論,
觀察其思維方式和見解。
個人更傾向的不是語首冊言層面上的東西,畢竟術業有專攻,
讓宮力去和Doug
Lea
討論並發問陵轎題並不一定能夠過關。

Ⅵ Java類的熱替換——概念、設計與實現

對於許多關鍵性業務或者龐大的 Java 系統來說 如果必須暫停系統服務才能進行系統升級 既會大大影響到系統的可用性 同時也增加了系統的管理和維護成本 因此 如果能夠方便地在不停止系統業務的情況下進行系統升級 則可以很好地解決上述問題 在本文中 我們將基於實例 對構建在線升級 Java 系統的基礎技術和設計原則進行了深入的講解 相信讀者能夠根據文中的技術構建出自己的在線升級系統來

Java ClassLoader 技術剖析

在本文中 我們將不對 Java ClassLoader 的細節進行過於詳細的講解 而是關注於和構建在線升級系統相關的基礎概念 關於 ClassLoader 的詳細細節許多資料可以參考 有興趣的讀者可以自行研讀

要構建在線升級系統 一個重要的技術就判如是能夠實現 Java 類的熱替換 —— 也就是在不停止正在運行的系統的情況下進行類(對象)的升級替換 而 Java 的 ClassLoader 正是實現這項技術的基礎

在 Java 中 類銷數的實例化流程分為兩個部分 類的載入和類的實例化 類的載入又分為顯式載入和隱式載入 大家使用 new 關鍵字創建類實例時 其實就隱式地包含了類的載入過程 對於類的顯式載入來說 比較常用的是 Class forName 其實 它們都是通過調用 ClassLoader 類的 loadClass 方法來完成類的實際載入工作的 直接調用 ClassLoader 的 loadClass 方法是另外一種不常用的顯式載入類的技術

圖 Java 類載入器層次結構圖

ClassLoader 在載入類時有一定的層次關系和規則 在 Java 中 有四種類型的類載入器 分別為 BootStrapClassLoader ExtClassLoader AppClassLoader 以及用戶自定義的 ClassLoader 這四種類載入器分別負責不同路徑的類的載入 並形成了一個類載入的層次結構

BootStrapClassLoader 處於類載入器層次結構的最高層 負責 sun boot class path 路徑下類的載入 默認為 jre/lib 目錄下的核心 API 或 Xbootclasspath 選項指定的 jar 包 ExtClassLoader 的載入路徑為 java ext dirs 默認為 jre/lib/ext 目錄或者 Djava ext dirs 指定目錄下的 jar 包載入 AppClassLoader 的載入路徑為 java class path 默認為環境變數 CLASSPATH 中設定的值 也可以通過 classpath 選型進行指定 用戶自定義 ClassLoader 可以根據用戶的需要定製自己的類載入過程 在運行期進行指定類的動態實時載入

這四種類載入器的層次關系圖如 圖 所示 一般來說 這四種類載入器會形成一種父子關系 高層為低層的父載入器 在進行類載入時 首先會自底向上挨個檢查是否已經載入了指定類 如果已經載入則直接返回該類的引用 如果到最高層也沒有載入過指定類 那麼會自頂向下挨個嘗試載入 直到用戶自定義類載入器 如果還不能成功 就會拋出異常 Java 類的載入過程如 圖 所示

圖 Java 類的載入過程

每個類載入器有自己的名字空間 對於同一個類載入器實例來說 名字相同的類只能存在一個 並且僅載入一次 不管該類有沒有變化 下次再需要載入時 它只是從自己的緩存中直接返回已經載入過的類引用

我們編寫的應用類默認情況下都是通過 AppClassLoader 進行載入的 當虧沖首我們使用 new 關鍵字或者 Class forName 來載入類時 所要載入的類都是由調用 new 或者 Class forName 的類的類載入器(也是 AppClassLoader)進行載入的 要想實現 Java 類的熱替換 首先必須要實現系統中同名類的不同版本實例的共存 通過上面的介紹我們知道 要想實現同一個類的不同版本的共存 我們必須要通過不同的類載入器來載入該類的不同版本 另外 為了能夠繞過 Java 類的既定載入過程 我們需要實現自己的類載入器 並在其中對類的載入過程進行完全的控制和管理

編寫自定義的 ClassLoader

為了能夠完全掌控類的載入過程 我們的定製類載入器需要直接從 ClassLoader 繼承 首先我們來介紹一下 ClassLoader 類中和熱替換有關的的一些重要方法

findLoadedClass 每個類載入器都維護有自己的一份已載入類名字空間 其中不能出現兩個同名的類 凡是通過該類載入器載入的類 無論是直接的還是間接的 都保存在自己的名字空間中 該方法就是在該名字空間中尋找指定的類是否已存在 如果存在就返回給類的引用 否則就返回 null 這里的直接是指 存在於該類載入器的載入路徑上並由該載入器完成載入 間接是指 由該類載入器把類的載入工作委託給其他類載入器完成類的實際載入

getSystemClassLoader Java 中新增的方法 該方法返回系統使用的 ClassLoader 可以在自己定製的類載入器中通過該方法把一部分工作轉交給系統類載入器去處理

defineClass 該方法是 ClassLoader 中非常重要的一個方法 它接收以位元組數組表示的類位元組碼 並把它轉換成 Class 實例 該方法轉換一個類的同時 會先要求裝載該類的父類以及實現的介面類

loadClass 載入類的入口方法 調用該方法完成類的顯式載入 通過對該方法的重新實現 我們可以完全控制和管理類的載入過程

resolveClass 鏈接一個指定的類 這是一個在某些情況下確保類可用的必要方法 詳見 Java 語言規范中 執行 一章對該方法的描述

了解了上面的這些方法 下面我們來實現一個定製的類載入器來完成這樣的載入流程 我們為該類載入器指定一些必須由該類載入器直接載入的類集合 在該類載入器進行類的載入時 如果要載入的類屬於必須由該類載入器載入的集合 那麼就由它直接來完成類的載入 否則就把類載入的工作委託給系統的類載入器完成

在給出示例代碼前 有兩點內容需要說明一下 要想實現同一個類的不同版本的共存 那麼這些不同版本必須由不同的類載入器進行載入 因此就不能把這些類的載入工作委託給系統載入器來完成 因為它們只有一份 為了做到這一點 就不能採用系統默認的類載入器委託規則 也就是說我們定製的類載入器的父載入器必須設置為 null 該定製的類載入器的實現代碼如下

class CustomCL extends ClassLoader {

private String basedir; // 需要該類載入器直接載入的類文件的基目錄

private HashSet dynaclazns; // 需要由該類載入器直接載入的類名

public CustomCL(String basedir String[] clazns) {

super(null); // 指定父類載入器為 null

this basedir = basedir;

dynaclazns = new HashSet();

loadClassByMe(clazns);

}

private void loadClassByMe(String[] clazns) {

for (int i = ; i < clazns length; i++) {

loadDirectly(clazns[i]);

dynaclazns add(clazns[i]);

}

}

private Class loadDirectly(String name) {

Class cls = null;

StringBuffer *** = new StringBuffer(basedir);

String classname = name replace( File separatorChar) + class ;

*** append(File separator + classname);

File classF = new File( *** toString());

cls = instantiateClass(name new FileInputStream(classF)

classF length());

return cls;

}

private Class instantiateClass(String name InputStream fin long len){

byte[] raw = new byte[(int) len];

fin read(raw);

fin close();

return defineClass(name raw raw length);

}

protected Class loadClass(String name boolean resolve)

throws ClassNotFoundException {

Class cls = null;

cls = findLoadedClass(name);

if(!ntains(name) && cls == null)

cls = getSystemClassLoader() loadClass(name);

if (cls == null)

throw new ClassNotFoundException(name);

if (resolve)

resolveClass(cls);

return cls;

}

}

在該類載入器的實現中 所有指定必須由它直接載入的類都在該載入器實例化時進行了載入 當通過 loadClass 進行類的載入時 如果該類沒有載入過 並且不屬於必須由該類載入器載入之列都委託給系統載入器進行載入 理解了這個實現 距離實現類的熱替換就只有一步之遙了 我們在下一小節對此進行詳細的講解

實現 Java 類的熱替換

在本小節中 我們將結合前面講述的類載入器的特性 並在上小節實現的自定義類載入器的基礎上實現 Java 類的熱替換 首先我們把上小節中實現的類載入器的類名 CustomCL 更改為 HotswapCL 以明確表達我們的意圖

現在來介紹一下我們的實驗方法 為了簡單起見 我們的包為默認包 沒有層次 並且省去了所有錯誤處理 要替換的類為 Foo 實現很簡單 僅包含一個方法 sayHello

清單 待替換的示例類

public class Foo{

public void sayHello() {

System out println( hello world! (version one) );

}

}

在當前工作目錄下建立一個新的目錄 swap 把編譯好的 Foo class 文件放在該目錄中 接下來要使用我們前面編寫的 HotswapCL 來實現該類的熱替換 具體的做法為 我們編寫一個定時器任務 每隔 秒鍾執行一次 其中 我們會創建新的類載入器實例載入 Foo 類 生成實例 並調用 sayHello 方法 接下來 我們會修改 Foo 類中 sayHello 方法的列印內容 重新編譯 並在系統運行的情況下替換掉原來的 Foo class 我們會看到系統會列印出更改後的內容 定時任務的實現如下(其它代碼省略 請讀者自行補齊)

public void run(){

try {

// 每次都創建出一個新的類載入器

HowswapCL cl = new HowswapCL( /swap new String[]{ Foo });

Class cls = cl loadClass( Foo );

Object foo = cls newInstance();

Method m = foo getClass() getMethod( sayHello new Class[]{});

m invoke(foo new Object[]{});

} catch(Exception ex) {

ex printStackTrace();

}

}

編譯 運行我們的系統 會出現如下的列印

圖 熱替換前的運行結果

好 現在我們把 Foo 類的 sayHello 方法更改為

public void sayHello() {

System out println( hello world! (version o) );

}

在系統仍在運行的情況下 編譯 並替換掉 swap 目錄下原來的 Foo class 文件 我們再看看屏幕的列印 奇妙的事情發生了 新更改的類在線即時生效了 我們已經實現了 Foo 類的熱替換 屏幕列印如下

圖 熱替換後的運行結果

敏銳的讀者可能會問 為何不用把 foo 轉型為 Foo 直接調用其 sayHello 方法呢?這樣不是更清晰明了嗎?下面我們來解釋一下原因 並給出一種更好的方法

如果我們採用轉型的方法 代碼會變成這樣 Foo foo = (Foo)cls newInstance(); 讀者如果跟隨本文進行試驗的話 會發現這句話會拋出 ClassCastException 異常 為什麼嗎?因為在 Java 中 即使是同一個類文件 如果是由不同的類載入器實例載入的 那麼它們的類型是不相同的 在上面的例子中 cls 是由 HowswapCL 載入的 而 foo 變數類型聲名和轉型里的 Foo 類卻是由 run 方法所屬的類的載入器(默認為 AppClassLoader)載入的 因此是完全不同的類型 所以會拋出轉型異常

那麼通過介面調用是不是就行了呢?我們可以定義一個 IFoo 介面 其中聲名 sayHello 方法 Foo 實現該介面 也就是這樣 IFoo foo = (IFoo)cls newInstance(); 本來該方法也會有同樣的問題的 因為外部聲名和轉型部分的 IFoo 是由 run 方法所屬的類載入器載入的 而 Foo 類定義中 implements IFoo 中的 IFoo 是由 HotswapCL 載入的 因此屬於不同的類型轉型還是會拋出異常的 但是由於我們在實例化 HotswapCL 時是這樣的

HowswapCL cl = new HowswapCL( /swap new String[]{ Foo });

其中僅僅指定 Foo 類由 HotswapCL 載入 而其實現的 IFoo 介面文件會委託給系統類載入器載入 因此轉型成功 採用介面調用的代碼如下

清單 採用介面調用的代碼

public void run(){

try {

HowswapCL cl = new HowswapCL( /swap new String[]{ Foo });

Class cls = cl loadClass( Foo );

IFoo foo = (IFoo)cls newInstance();

foo sayHello();

} catch(Exception ex) {

ex printStackTrace();

}

}

確實 簡潔明了了很多 在我們的實驗中 每當定時器調度到 run 方法時 我們都會創建一個新的 HotswapCL 實例 在產品代碼中 無需如此 僅當需要升級替換時才去創建一個新的類載入器實例

在線升級系統的設計原則

在上小節中 我們給出了一個 Java 類熱替換的實例 掌握了這項技術 就具備了實現在線升級系統的基礎 但是 對於一個真正的產品系統來說 升級本省就是一項非常復雜的工程 如果要在線升級 就會更加復雜 其中 實現類的熱替換只是最後一步操作 在線升級的要求會對系統的整體設計帶來深遠的影響 下面我們來談談在線升級系統設計方面的一些原則

在系統設計一開始 就要考慮系統的哪些部分是需要以後在線升級的 哪些部分是穩定的

雖然我們可以把系統設計成任何一部分都是可以在線升級的 但是其成本是非常高昂的 也沒有必要 因此 明確地界定出系統以後需要在線升級的部分是明智之舉 這些部分常常是系統業務邏輯規則 演算法等等

設計出規范一致的系統狀態轉換方法

替換一個類僅僅是在線升級系統所要做的工作中的一個步驟 為了使系統能夠在升級後正常運行 就必須保持升級前後系統狀態的一致性 因此 在設計時要考慮需要在線升級的部分所涉及的系統狀態有哪些 把這些狀態設計成便於獲取 設置和轉換的 並用一致的方式來進行

明確出系統的升級控制協議

這個原則是關於系統在線升級的時機和流程式控制制的 不考慮系統的當前運行狀態就貿然進行升級是一項非常危險的活動 因此在系統設計中 就要考慮並預留出系統在線升級的控制點 並定義清晰 明確的升級協議來協調 控制多個升級實體的升級次序 以確保系統在升級的任何時刻都處在一個確定的狀態下

考慮到升級失敗時的回退機制

即使我們做了非常縝密細致的設計 還是難以從根本上保證系統升級一定是成功的 對於大型分布式系統來說尤其如此 因此在系統設計時 要考慮升級失敗後的回退機制

好了 本小節我們簡單介紹了在線升級系統設計時的幾個重要的原則 下一小節我們將給出一個簡單的實例 來演示一下如何來實現一個在線升級系統

在線升級系統實例

首先 我們來簡單介紹一下這個實例的結構組成和要完成的工作 在我們的例子中 主要有三個實體 一個是升級控制實體 兩個是工作實體 都基於 ActiveObject 實現

升級控制實體以 RMI 的方式對外提供了一個管理命令介面 用以接收外部的在線升級命令 工作實體有兩個消息隊列 一個用以接收分配給它的任務(我們用定時器定時給它發送任務命令消息) 我們稱其為任務隊列 另一個用於和升級控制實體交互 協作完成升級過程 我們稱其為控制隊列 工作實體中的任務很簡單 就是使用我們前面介紹的 Foo 類簡單地列印出一個字元串 不過這次字元串作為狀態保存在工作實體中 動態設置給 Foo 類的實例的 升級的協議流程如下

當升級控制實體接收到來自 RMI 的在線升級命令時 它會向兩個工作實體的任務隊列中發送一條准備升級消息 然後等待回應 當工作實體在任務隊列中收到准備升級消息時 會立即給升級控制實體發送一條准備就緒消息 然後切換到控制隊列等待進一步的升級指令 升級控制實體收齊這兩個工作實體發來的准備就緒消息後 就給這兩個工作實體的控制隊列各發送一條開始升級消息 然後等待結果 工作實體收到開始升級消息後 進行實際的升級工作 也就是我們前面講述的熱替換類 然後 給升級控制實體發送升級完畢消息 升級控制實體收到來自兩個工作實體的升級完畢消息後 會給這兩個工作實體的控制隊列各發送一條繼續工作消息 工作實體收到繼續工作消息後 切換到任務隊列繼續工作 升級過程結束

主要的代碼片段如下(略去命令消息的定義和執行細節)

清單 主要的代碼片段

// 升級控制實體關鍵代碼

class UpgradeController extends ActiveObject{

int nready = ;

int nfinished = ;

Worker[] workers;

// 收到外部升級命令消息時 會觸發該方法被調用

public void askForUpgrade() {

for(int i= ; i<workers length; i++)

workers[i] getTaskQueue() enqueue(new PrepareUpgradeCmd(workers[i]));

}

// 收到工作實體回應的准備就緒命令消息時 會觸發該方法被調用

public void readyForUpgrade(String worker_name) {

nready++;

if(nready == workers length){

for(int i= ; i<workers length; i++)

workers[i] getControlQueue() enqueue(new

StartUpgradeCmd(workers[i]));

}

}

// 收到工作實體回應的升級完畢命令消息時 會觸發該方法被調用

public void finishUpgrade(String worker_name) {

nfinished++;

if(nfinished == workers length){

for(int i= ; i<workers length; i++)

workers[i] getControlQueue() enqueue(new

ContineWorkCmd(workers[i]));

}

}

}

// 工作實體關鍵代碼

class Worker extends ActiveObject{

UpgradeController ugc;

HotswapCL hscl;

IFoo foo;

String state = hello world! ;

// 收到升級控制實體的准備升級命令消息時 會觸發該方法被調用

public void prepareUpgrade() {

switchToControlQueue();

ugc getMsgQueue() enqueue(new ReadyForUpdateCMD(ugc this));

}

// 收到升級控制實體的開始升級命令消息時 會觸發該方法被調用

public void startUpgrade(String worker_name) {

doUpgrade();

ugc getMsgQueue() enqueue(new FinishUpgradeCMD(ugc this));

}

// 收到升級控制實體的繼續工作命令消息時 會觸發該方法被調用

public void continueWork(String worker_name) {

switchToTaskQueue();

}

// 收到定時命令消息時 會觸發該方法被調用

public void doWork() {

foo sayHello();

}

// 實際升級動作

private void doUpgrade() {

hscl = new HowswapCL( /swap new String[]{ Foo });

Class cls = hscl loadClass( Foo );

foo = (IFoo)cls newInstance();

foo SetState(state);

}

}

//IFoo 介面定義

interface IFoo {

void SetState(String);

void sayHello();

}

在Foo 類第一個版本的實現中 只是把設置進來的字元串直接列印出來 在第二個版本中 會先把設置進來的字元串變為大寫 然後列印出來 例子很簡單 旨在表達規則或者演算法方面的升級變化 另外 我們並沒有提及諸如 消息超時 升級失敗等方面的異常情況 這在實際產品開發中是必須要考慮的

lishixin/Article/program/Java/hx/201311/26326

Ⅶ 求助 OOP java 對象有時需要問其他對象在特定事件發生時向他們發送消息

第一句翻譯錯誤,objects有時會要求other objects在特定事件發生時向他們發送一個消息

我選第3種,舉個例子簡胡,例如一個button要求用戶在點擊的時候向它發送一個消息。普通裂爛的做法就是在這個button上注冊監聽器來監聽click事件。第3種方法創建的intermediate object就相當於監聽器,當然這個監聽器能監聽別的事件,如攔源攔mouseover就更好了,方便以後擴展。

閱讀全文

與javaaskto相關的資料

熱點內容
自家wifi怎麼能加密 瀏覽:642
紅米k40加密門禁卡 瀏覽:845
什麼樣的源碼好看 瀏覽:156
手機主伺服器有什麼用 瀏覽:610
程序編寫命令 瀏覽:597
android發送心跳包 瀏覽:385
指標源碼和原理 瀏覽:700
汽車空調壓縮吸盤 瀏覽:208
崽崽因app版本不同不能邀請怎麼辦 瀏覽:686
poa演算法得到的解為全局最優解 瀏覽:926
python符號表達式 瀏覽:34
威馳壓縮機繼電器 瀏覽:871
華為手機怎麼設置移動數據app 瀏覽:959
空調壓縮機哪的廠家多 瀏覽:390
手指速演算法24加7怎麼算 瀏覽:139
如何用python寫vlookup函數 瀏覽:798
社保加密狗廠商 瀏覽:216
php編譯運行說法 瀏覽:957
程序員說喂 瀏覽:258
抖音直播雲伺服器 瀏覽:629