用 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就更好了,方便以后扩展。