|
使用Windows操作系统的朋友对Excel(电子表格)一定不会陌生,但是要使用Java语言来操纵Excel文件并不是一件容易的事。在Web应用日益盛行的今天,通过Web来操作Excel文件的需求越来越强烈,目前较为流行的操作是在JSP或Servlet 中创建一个CSV (comma separated values)文件,并将这个文件以MIME,text/csv类型返回给浏览器,接着浏览器调用Excel并且显示CSV文件。这样只是说可以访问到 Excel文件,但是还不能真正的操纵Excel文件,本文将给大家一个惊喜,向大家介绍一个开放源码项目,Java Excel API,使用它大家就可
以方便地操纵Excel文件了。
Java Excel API简介
Java Excel是一开放源码项目,通过它Java开发人员可以读取Excel文件的内容、创建新的Excel文件、更新已经存在的Excel文件。使用该 API非Windows操作系统也可以通过纯Java应用来处理Excel数据表。因为是使用Java编写的,所以我们在Web应用中可以通过JSP、 Servlet来调用API实现对Excel数据表的访问。
现在发布的稳定版本是V2.0,提供以下功能:
从Excel 95、97、2000等格式的文件中读取数据;
读取Excel公式(可以读取Excel 97以后的公式);
生成Excel数据表(格式为Excel 97);
支持字体、数字、日期的格式化;
支持单元格的阴影操作,以及颜色操作;
修改已经存在的数据表;
现在还不支持以下功能,但不久就会提供了:
不能够读取图表信息;
可以读,但是不能生成公式,任何类型公式最后的计算值都可以读出;
应用示例
1、从Excel文件读取数据表
Java Excel API既可以从本地文件系统的一个文件(.xls),也可以从输入流中读取Excel数据表。读取Excel数据表的第一步是创建Workbook(术语:工作薄),下面的代码片段举例说明了应该如何操作:(完整代码见ExcelReading.java)
import java.io.*;
import jxl.*;
… … … …
try
{
//构建Workbook对象, 只读Workbook对象
//直接从本地文件创建Workbook
//从输入流创建Workbook
InputStream is = new FileInputStream(sourcefile);
jxl.Workbook rwb = Workbook.getWorkbook(is);
}
catch (Exception e)
{
e.printStackTrace();
}
一旦创建了Workbook,我们就可以通过它来访问Excel Sheet(术语:工作表)。参考下面的代码片段:
//获取第一张Sheet表
Sheet rs = rwb.getSheet(0);
我们既可能通过Sheet的名称来访问它,也可以通过下标来访问它。如果通过下标来访问的话,要注意的一点是下标从0开始,就像数组一样。
一旦得到了Sheet,我们就可以通过它来访问Excel Cell(术语:单元格)。参考下面的代码片段:
//获取第一行,第一列的值
Cell c00 = rs.getCell(0, 0);
String strc00 = c00.getContents();
//获取第一行,第二列的值
Cell c10 = rs.getCell(1, 0);
String strc10 = c10.getContents();
//获取第二行,第二列的值
Cell c11 = rs.getCell(1, 1);
String strc11 = c11.getContents();
System.out.println("Cell(0, 0)" + " value : " + strc00 + "; type : " + c00.getType());
System.out.println("Cell(1, 0)" + " value : " + strc10 + "; type : " + c10.getType());
System.out.println("Cell(1, 1)" + " value : " + strc11 + "; type : " + c11.getType()); 如果仅仅是取得Cell的值,我们可以方便地通过getContents()方法,它可以将任何类型的Cell值都作为一个字符串返回。示例代码中Cell(0, 0)是文本型,Cell(1, 0)是数字型,Cell(1,1)是日期型,通过getContents(),三种类型的返回值都是字符型。
如果有需要知道Cell内容的确切类型,API也提供了一系列的方法。参考下面的代码片段:
String strc00 = null;
double strc10 = 0.00;
Date strc11 = null;
Cell c00 = rs.getCell(0, 0);
Cell c10 = rs.getCell(1, 0);
Cell c11 = rs.getCell(1, 1);
if(c00.getType() == CellType.LABEL)
{
LabelCell labelc00 = (LabelCell)c00;
strc00 = labelc00.getString();
}
if(c10.getType() == CellType.NUMBER)
{
NmberCell numc10 = (NumberCell)c10;
strc10 = numc10.getvalue();
}
if(c11.getType() == CellType.DATE)
{
DateCell datec11 = (DateCell)c11;
strc11 = datec11.getDate();
}
System.out.println("Cell(0, 0)" + " value : " + strc00 + "; type : " + c00.getType());
System.out.println("Cell(1, 0)" + " value : " + strc10 + "; type : " + c10.getType());
System.out.println("Cell(1, 1)" + " value : " + strc11 + "; type : " + c11.getType());
在得到Cell对象后,通过getType()方法可以获得该单元格的类型,然后与 API提供的基本类型相匹配,强制转换成相应的类型,最后调用相应的取值方法getXXX(),就可以得到确定类型的值。API提供了以下基本类型,与 Excel的数据格式相对应,如下图所示:
每种类型的具体意义,请参见Java Excel API document.
当你完成对Excel电子表格数据的处理后,一定要使用close()方法来关闭先前创建的对象,以释放读取数据表的过程中所占用的内存空间,在读取大量数据时显得尤为重要。参考如下代码片段:
//操作完成时,关闭对象,释放占用的内存空间
rwb.close();
Java Excel API提供了许多访问Excel数据表的方法,在这里我只简要地介绍几个常用的方法,其它的方法请参考附录中的Java Excel API document.
Workbook类提供的方法
1. int getNumberOfSheets()
获得工作薄(Workbook)中工作表(Sheet)的个数,示例:
jxl.Workbook rwb = jxl.Workbook.getWorkbook(new File(sourcefile));
int sheets = rwb.getNumberOfSheets();
2. Sheet[] getSheets()
返回工作薄(Workbook)中工作表(Sheet)对象数组,示例:
jxl.Workbook rwb = jxl.Workbook.getWorkbook(new File(sourcefile));
Sheet[] sheets = rwb.getSheets();
3. String getVersion()
返回正在使用的API的版本号,好像是没什么太大的作用。
jxl.Workbook rwb = jxl.Workbook.getWorkbook(new File(sourcefile));
String apiVersion = rwb.getVersion();
Sheet接口提供的方法
1) String getName()
获取Sheet的名称,示例:
jxl.Workbook rwb = jxl.Workbook.getWorkbook(new File(sourcefile));
jxl.Sheet rs = rwb.getSheet(0);
String sheetName = rs.getName();
2) int getColumns()
获取Sheet表中所包含的总列数,示例:
jxl.Workbook rwb = jxl.Workbook.getWorkbook(new File(sourcefile));
jxl.Sheet rs = rwb.getSheet(0);
int rsColumns = rs.getColumns();
3) Cell[] getColumn(int column)
获取某一列的所有单元格,返回的是单元格对象数组,示例:
jxl.Workbook rwb = jxl.Workbook.getWorkbook(new File(sourcefile));
jxl.Sheet rs = rwb.getSheet(0);
Cell[] cell = rs.getColumn(0);
4) int getRows()
获取Sheet表中所包含的总行数,示例:
jxl.Workbook rwb = jxl.Workbook.getWorkbook(new File(sourcefile));
jxl.Sheet rs = rwb.getSheet(0);
int rsRows = rs.getRows();
5) Cell[] getRow(int row)
获取某一行的所有单元格,返回的是单元格对象数组,示例子:
jxl.Workbook rwb = jxl.Workbook.getWorkbook(new File(sourcefile));
jxl.Sheet rs = rwb.getSheet(0);
Cell[] cell = rs.getRow(0);
6) Cell getCell(int column, int row)
获取指定单元格的对象引用,需要注意的是它的两个参数,第一个是列数,第二个是行数,这与通常的行、列组合有些不同。
jxl.Workbook rwb = jxl.Workbook.getWorkbook(new File(sourcefile));
jxl.Sheet rs = rwb.getSheet(0);
Cell cell = rs.getCell(0, 0); 2、生成新的Excel工作薄
下面的代码主要是向大家介绍如何生成简单的Excel工作表,在这里单元格的内容是不带任何修饰的(如:字体,颜色等等),所有的内容都作为字符串写入。(完整代码见ExcelW
riting.java)
与读取Excel工作表相似,首先要使用Workbook类的工厂方法创建一个可写入的工作薄(Workbook)对象,这里要注意的是,只能通过API提供的工厂方法来创建Workbook,而不能使用 WritableWorkbook的构造函数,因为类WritableWorkbook的构造函数为protected类型。示例代码片段如下:
import java.io.*;
import jxl.*;
import jxl.write.*;
… … … …
try
{
//构建Workbook对象, 只读Workbook对象
//Method 1:创建可写入的Excel工作薄
jxl.write.WritableWorkbook wwb = Workbook.createWorkbook(new File(targetfile));
//Method 2:将WritableWorkbook直接写入到输出流
/*
OutputStream os = new FileOutputStream(targetfile);
jxl.write.WritableWorkbook wwb = Workbook.createWorkbook(os);
*/
}
catch (Exception e)
{
e.printStackTrace();
}
API提供了两种方式来处理可写入的输出流,一种是直接生成本地文件,如果文件名不带全路径的话,缺省的文件会定位在当前目录,如果文件名带有全路径的话,则生成的Excel文件则会定位在相应的目录;另外一种是将Excel对象直接写入到输出流,例如:用户通过浏览器来访问Web服务器,如果HTTP头设置正确的话,浏览器自动调用客户端的Excel应用程序,来显示动态生成的 Excel电子表格。
接下来就是要创建工作表,创建工作表的方法与创建工作薄的方法几乎一样,同样是通过工厂模式方法获得相应的对象,该方法需要两个参数,一个是工作表的名称,另一个是工作表在工作薄中的位置,参考下面的代码片段:
//创建Excel工作表
jxl.write.WritableSheet ws = wwb.createSheet("Test Sheet 1", 0);
"这锅也支好了,材料也准备齐全了,可以开始下锅了!",现在要做的只是实例化API所提供的Excel基本数据类型,并将它们添加到工作表中就可以了,参考下面的代码片段:
//1.添加Label对象
jxl.write.Label labelC = new jxl.write.Label(0, 0, "This is a Label cell");
ws.addCell(labelC);
//添加带有字型Formatting的对象
jxl.write.WritableFont wf = new jxl.write.WritableFont(WritableFont.TIMES, 18, WritableFont.BOLD, true);
jxl.write.WritableCellFormat wcfF = new jxl.write.WritableCellFormat(wf);
jxl.write.Label labelCF = new jxl.write.Label(1, 0, "This is a Label Cell", wcfF);
ws.addCell(labelCF);
//添加带有字体颜色Formatting的对象
jxl.write.WritableFont wfc = new jxl.write.WritableFont(WritableFont.ARIAL, 10, WritableFont.NO_BOLD, false,
Underlinestyle.NO_UNDERLINE, jxl.format.Colour.RED);
jxl.write.WritableCellFormat wcfFC = new jxl.write.WritableCellFormat(wfc);
jxl.write.Label labelCFC = new jxl.write.Label(1, 0, "This is a Label Cell", wcfFC);
ws.addCell(labelCF);
//2.添加Number对象
jxl.write.Number labelN = new jxl.write.Number(0, 1, 3.1415926);
ws.addCell(labelN);
//添加带有formatting的Number对象
jxl.write.NumberFormat nf = new jxl.write.NumberFormat("#.##");
jxl.write.WritableCellFormat wcfN = new jxl.write.WritableCellFormat(nf);
jxl.write.Number labelNF = new jxl.write.Number(1, 1, 3.1415926, wcfN);
ws.addCell(labelNF);
//3.添加Boolean对象
jxl.write.Boolean labelB = new jxl.write.Boolean(0, 2, false);
ws.addCell(labelB);
//4.添加DateTime对象
jxl.write.DateTime labelDT = new jxl.write.DateTime(0, 3, new java.util.Date());
ws.addCell(labelDT);
//添加带有formatting的DateFormat对象
jxl.write.DateFormat df = new jxl.write.DateFormat("dd MM yyyy hh:mm:ss");
jxl.write.WritableCellFormat wcfDF = new jxl.write.WritableCellFormat(df);
jxl.write.DateTime labelDTF = new jxl.write.DateTime(1, 3, new java.util.Date(), wcfDF);
ws.addCell(labelDTF); 这里有两点大家要引起大家的注意。第一点,在构造单元格时,单元格在工作表中的位置就已经确定了。一旦创建后,单元格的位置是不能够变更的,尽管单元格的内容是可以改变的。第二点,单元格的定位是按照下面这样的规律(column, row),而且下标都是从0开始,例如,A1被存储在(0, 0),B1被存储在(1, 0)。
最后,不要忘记关闭打开的Excel工作薄对象,以释放占用的内存,参见下面的代码片段:
//写入Exel工作表
wwb.write();
//关闭Excel工作薄对象
wwb.close();
这可能与读取Excel文件的操作有少少不同,在关闭Excel对象之前,你必须要先调用write()方法,因为先前的操作都是存储在缓存中的,所以要通过该方法将操作的内容保存在文件中。如果你先关闭了Excel对象,那么只能得到一张空的工作薄了。
3、拷贝、更新Excel工作薄
接下来简要介绍一下如何更新一个已经存在的工作薄,主要是下面二步操作,第一步是构造只读的Excel工作薄,第二步是利用已经创建的Excel工作薄创建新的可写入的Excel工作薄,参考下面的代码片段:(完整代码见ExcelModifying.java)
//创建只读的Excel工作薄的对象
jxl.Workbook rw = jxl.Workbook.getWorkbook(new File(sourcefile));
//创建可写入的Excel工作薄对象
jxl.write.WritableWorkbook wwb = Workbook.createWorkbook(new File(targetfile), rw);
//读取第一张工作表
jxl.write.WritableSheet ws = wwb.getSheet(0);
//获得第一个单元格对象
jxl.write.WritableCell wc = ws.getWritableCell(0, 0);
//判断单元格的类型, 做出相应的转化
if(wc.getType() == CellType.LABEL)
{
Label l = (Label)wc;
l.setString("The value has been modified.");
}
//写入Excel对象
wwb.write();
//关闭可写入的Excel对象
wwb.close();
//关闭只读的Excel对象
rw.close(); 之所以使用这种方式构建Excel对象,完全是因为效率的原因,因为上面的示例才是 API的主要应用。为了提高性能,在读取工作表时,与数据相关的一些输出信息,所有的格式信息,如:字体、颜色等等,是不被处理的,因为我们的目的是获得行数据的值,既使没有了修饰,也不会对行数据的值产生什么影响。唯一的不利之处就是,在内存中会同时保存两 个同样的工作表,这样当工作表体积比较大时,会占用相当大的内存,但现在好像内存的大小并不是什么关键因素了。
一旦获得了可写入的工作表对象,我们就可以对单元格对象进行更新的操作了,在这里我们不必调用API提供的add()方法,因为单元格已经于工作表当中,所以我们只需要调用相应的setXXX()方法,就可以完成更新的操作了。
尽单元格原有的格式化修饰是不能去掉的,我们还是可以将新的单元格修饰加上去,以使单元格的内容以不同的形式表现。
新生成的工作表对象是可写入的,我们除了更新原有的单元格外,还可以添加新的单元格到工作表中,这与示例2的操作是完全一样的。
最后,不要忘记调用write()方法,将更新的内容写入到文件中,然后关闭工作薄对象,这里有两个工作薄对象要关闭,一个是只读的,另外一个是可写入的。
jxl的一些总结
要往xls文件里面写入数据的时候需要注意的是第一要新建一个xls文件 OutputStream os=new FileOutputStream("c:\\excel2.xls");
再建完这个文件的时候再建立工作文件 jxl.write.WritableWorkbook wwb = Workbook.createWorkbook(new File(os));
如果这个文件已经存在,那么我们可以在这个文件里面加入一个sheet为了和以前的数据进行分开; jxl.write.WritableSheet ws = wwb.createSheet("Test Sheet 1", 0); 在createSheet方法里前面的参数是sheet名,后面是要操作的sheet号
接下来就可以往这个文件里面写入数据了
写入数据的时候注意的格式
(1)添加的字体样式 jxl.write.WritableFont wf = new jxl.write.WritableFont(WritableFont.TIMES, 18, WritableFont.BOLD, true); WritableFont()方法里参数说明: 这个方法算是一个容器,可以放进去好多属性 第一个: TIMES是字体大小,他写的是18 第二个: BOLD是判断是否为斜体,选择true时为斜体 第三个: ARIAL 第四个: UnderlineStyle.NO_UNDERLINE 下划线 第五个: jxl.format.Colour.RED 字体颜色是红色的
jxl.write.WritableCellFormat wcfF = new jxl.write.WritableCellFormat(wf);
jxl.write.Label labelC = new jxl.write.Label(0, 0, "This is a Label cell",wcfF); ws.addCell(labelC); 在Label()方法里面有三个参数 第一个是代表列数, 第二是代表行数, 第***要写入的内容 第四个是可选项,是输入这个label里面的样式 然后通过写sheet的方法addCell()把内容写进sheet里面。
(2)添加带有formatting的Number对象 jxl.write.NumberFormat nf = new jxl.write.NumberFormat("#.##");
(3)添加Number对象 (3.1)显示number对象数据的格式
jxl.write.NumberFormat nf = new jxl.write.NumberFormat("#.##"); jxl.write.WritableCellFormat wcfN = new jxl.write.WritableCellFormat(nf);
jxl.write.Number labelNF = new jxl.write.Number(1, 1, 3.1415926, wcfN); ws.addCell(labelNF); Number()方法参数说明: 前两上表示输入的位置 第三个表示输入的内容
(4)添加Boolean对象 jxl.write.Boolean labelB = new jxl.write.Boolean(0, 2, false); ws.addCell(labelB);
(5)添加DateTime对象 jxl.write.DateTime labelDT = new jxl.write.DateTime(0, 3, new java.util.Date()); ws.addCell(labelDT); DateTime()方法的参数说明 前两个表示输入的位置 第三个表示输入的当前时间
(6)添加带有formatting的DateFormat对象 这个显示当前时间的所有信息,包括年月日小时分秒 jxl.write.DateFormat df = new jxl.write.DateFormat("dd MM yyyy hh:mm:ss"); jxl.write.WritableCellFormat wcfDF = new jxl.write.WritableCellFormat(df); jxl.write.DateTime labelDTF = new jxl.write.DateTime(1, 3, new java.util.Date(), wcfDF); ws.addCell(labelDTF);
(7)添加带有字体颜色Formatting的对象 jxl.write.WritableFont wfc = new jxl.write.WritableFont(WritableFont.ARIAL, 10, WritableFont.NO_BOLD, false,UnderlineStyle.NO_UNDERLINE, jxl.format.Colour.RED); jxl.write.WritableCellFormat wcfFC = new jxl.write.WritableCellFormat(wfc);
import="jxl.format.* jxl.write.WritableFont wfc = new jxl.write.WritableFont(WritableFont.ARIAL,20,WritableFont.BOLD,false,UnderlineStyle.NO_UNDERLINE,jxl.format.Colour.GREEN);
(8)设置单元格样式
jxl.write.WritableCellFormat wcfFC = new jxl.write.WritableCellFormat(wfc); wcfFC.setBackGround(jxl.format.Colour.RED);//设置单元格的颜色为红色 wcfFC = new jxl.write.Label(6,0,"i love china",wcfFC);
Jxl在写excel文件时使用的方法比较怪,也可以说jxl不支持修改excel文件。它的处理方式是每次打开旧excel文件,然后创建一个该excel文件的可写的副本,所有的修改都是在这个副本上做的。下面是一个例子。
Java中static、this、super、final用法 |
作者:未知 来源:未知 发布时间:2006-7-14 21:33:56 发布人:sany |
减小字体
增大字体
一、static 请先看下面这段程序: public class Hello{ public static void main(String[] args){ //(1) System.out.println("Hello,world!"); //(2) } } 看过这段程序,对于大多数学过Java 的从来说,都不陌生。即使没有学过Java,而学过其它的高级语言,例如C,那你也应该能看懂这段代码的意思。它只是简单的输出“Hello,world”,一点别的用处都没有,然而,它却展示了static关键字的主要用法。 在1处,我们定义了一个静态的方法名为main,这就意味着告诉Java编译器,我这个方法不需要创建一个此类的对象即可使用。你还得你是怎么运行这个程序吗?一般,我们都是在命令行下,打入如下的命令(加下划线为手动输入): javac Hello.java java Hello Hello,world! 这就是你运行的过程,第一行用来编译Hello.java这个文件,执行完后,如果你查看当前,会发现多了一个Hello.class文件,那就是第一行产生的Java二进制字节码。第二行就是执行一个Java程序的最普遍做法。执行结果如你所料。在2中,你可能会想,为什么要这样才能输出。好,我们来分解一下这条语句。(如果没有安装Java文档,请到Sun的官方网站浏览J2SE API)首先,System是位于java.lang包中的一个核心类,如果你查看它的定义,你会发现有这样一行:public static final PrintStream out;接着在进一步,点击PrintStream这个超链接,在METHOD页面,你会看到大量定义的方法,查找println,会有这样一行: public void println(String x)。 好了,现在你应该明白为什么我们要那样调用了,out是System的一个静态变量,所以可以直接使用,而out所属的类有一个println方法。 静态方法 通常,在一个类中定义一个方法为static,那就是说,无需本类的对象即可调用此方法。如下所示: class Simple{ static void go(){ System.out.println("Go..."); } } public class Cal{ public static void main(String[] args){ Simple.go(); } } 调用一个静态方法就是“类名.方法名”,静态方法的使用很简单如上所示。一般来说,静态方法常常为应用程序中的其它类提供一些实用工具所用,在Java的类库中大量的静态方法正是出于此目的而定义的。 静态变量 静态变量与静态方法类似。所有此类实例共享此静态变量,也就是说在类装载时,只分配一块存储空间,所有此类的对象都可以操控此块存储空间,当然对于final则另当别论了。看下面这段代码: class Value{ static int c=0; static void inc(){ c++; } } class Count{ public static void prt(String s){ System.out.println(s); } public static void main(String[] args){ Value v1,v2; v1=new Value(); v2=new Value(); prt("v1.c="+v1.c+" v2.c="+v2.c); v1.inc(); prt("v1.c="+v1.c+" v2.c="+v2.c); } } 结果如下: v1.c=0 v2.c=0 v1.c=1 v2.c=1 由此可以证明它们共享一块存储区。static变量有点类似于C中的全局变量的概念。值得探讨的是静态变量的初始化问题。我们修改上面的程序: class Value{ static int c=0; Value(){ c=15; } Value(int i){ c=i; } static void inc(){ c++; } } class Count{ public static void prt(String s){ System.out.println(s); } Value v=new Value(10); static Value v1,v2; static{ prt("v1.c="+v1.c+" v2.c="+v2.c); v1=new Value(27); prt("v1.c="+v1.c+" v2.c="+v2.c); v2=new Value(15); prt("v1.c="+v1.c+" v2.c="+v2.c); } public static void main(String[] args){ Count ct=new Count(); prt("ct.c="+ct.v.c); prt("v1.c="+v1.c+" v2.c="+v2.c); v1.inc(); prt("v1.c="+v1.c+" v2.c="+v2.c); prt("ct.c="+ct.v.c); } } 运行结果如下: v1.c=0 v2.c=0 v1.c=27 v2.c=27 v1.c=15 v2.c=15 ct.c=10 v1.c=10 v2.c=10 v1.c=11 v2.c=11 ct.c=11 这个程序展示了静态初始化的各种特性。如果你初次接触Java,结果可能令你吃惊。可能会对static后加大括号感到困惑。首先要告诉你的是,static定义的变量会优先于任何其它非static变量,不论其出现的顺序如何。正如在程序中所表现的,虽然v出现在v1和v2的前面,但是结果却是v1和v2的初始化在v的前面。在static{后面跟着一段代码,这是用来进行显式的静态变量初始化,这段代码只会初始化一次,且在类被第一次装载时。如果你能读懂并理解这段代码,会帮助你对static关键字的认识。在涉及到继承的时候,会先初始化父类的static变量,然后是子类的,依次类推。非静态变量不是本文的主题,在此不做详细讨论,请参考Think in Java中的讲解。 静态类 通常一个普通类不允许声明为静态的,只有一个内部类才可以。这时这个声明为静态的内部类可以直接作为一个普通类来使用,而不需实例一个外部类。如下代码所示: public class StaticCls{ public static void main(String[] args){ OuterCls.InnerCls oi=new OuterCls.InnerCls(); } } class OuterCls{ public static class InnerCls{ InnerCls(){ System.out.println("InnerCls"); } } } 输出结果会如你所料: InnerCls 和普通类一样。内部类的其它用法请参阅Think in Java中的相关章节,此处不作详解。 二、this & super 在上一篇拙作中,我们讨论了static的种种用法,通过用static来定义方法或成员,为我们编程提供了某种便利,从某种程度上可以说它类似于C语言中的全局函数和全局变量。但是,并不是说有了这种便利,你便可以随处使用,如果那样的话,你便需要认真考虑一下自己是否在用面向对象的思想编程,自己的程序是否是面向对象的。好了,现在开始讨论this&super这两个关键字的意义和用法。 在Java中,this通常指当前对象,super则指父类的。当你想要引用当前对象的某种东西,比如当前对象的某个方法,或当前对象的某个成员,你便可以利用this来实现这个目的,当然,this的另一个用途是调用当前对象的另一个构造函数,这些马上就要讨论。如果你想引用父类的某种东西,则非super莫属。由于this与super有如此相似的一些特性和与生俱来的某种关系,所以我们在这一块儿来讨论,希望能帮助你区分和掌握它们两个。 在一般方法中 最普遍的情况就是,在你的方法中的某个形参名与当前对象的某个成员有相同的名字,这时为了不至于混淆,你便需要明确使用this关键字来指明你要使用某个成员,使用方法是“this.成员名”,而不带this的那个便是形参。另外,还可以用“this.方法名”来引用当前对象的某个方法,但这时this就不是必须的了,你可以直接用方法名来访问那个方法,编译器会知道你要调用的是那一个。下面的代码演示了上面的用法: public class DemoThis{ private String name; private int age; DemoThis(String name,int age){ setName(name); //你可以加上this来调用方法,像这样:this.setName(name);但这并不是必须的 setAge(age); this.print(); } public void setName(String name){ this.name=name;//此处必须指明你要引用成员变量 } public void setAge(int age){ this.age=age; } public void print(){ System.out.println("Name="+name+" Age="+age);//在此行中并不需要用this,因为没有会导致混淆的东西 } public static void main(String[] args){ DemoThis dt=new DemoThis("Kevin","22"); } } 这段代码很简单,不用解释你也应该能看明白。在构造函数中你看到用this.print(),你完全可以用print()来代替它,两者效果一样。下面我们修改这个程序,来演示super的用法。 class Person{ public int c; private String name; private int age; protected void setName(String name){ this.name=name; } protected void setAge(int age){ this.age=age; } protected void print(){ System.out.println("Name="+name+" Age="+age); } } public class DemoSuper extends Person{ public void print(){ System.out.println("DemoSuper:"); super.print(); } public static void main(String[] args){ DemoSuper ds=new DemoSuper(); ds.setName("kevin"); ds.setAge(22); ds.print(); } } 在DemoSuper中,重新定义的print方法覆写了父类的print方法,它首先做一些自己的事情,然后调用父类的那个被覆写了的方法。输出结果说明了这一点: DemoSuper: Name=kevin Age=22 这样的使用方法是比较常用的。另外如果父类的成员可以被子类访问,那你可以像使用this一样使用它,用“super.父类中的成员名”的方式,但常常你并不是这样来访问父类中的成员名的。 在构造函数中 构造函数是一种特殊的方法,在对象初始化的时候自动调用。在构造函数中,this和super也有上面说的种种使用方式,并且它还有特殊的地方,请看下面的例子: class Person{ public static void prt(String s){ System.out.println(s); } Person(){ prt("A Person."); } Person(String name){ prt("A person name is:"+name); } } public class Chinese extends Person{ Chinese(){ super(); //调用父类构造函数(1) prt("A chinese.");//(4) } Chinese(String name){ super(name);//调用父类具有相同形参的构造函数(2) prt("his name is:"+name); } Chinese(String name,int age){ this(name);//调用当前具有相同形参的构造函数(3) prt("his age is:"+age); } public static void main(String[] args){ Chinese cn=new Chinese(); cn=new Chinese("kevin"); cn=new Chinese("kevin",22); } } 在这段程序中,this和super不再是像以前那样用“.”连接一个方法或成员,而是直接在其后跟上适当的参数,因此它的意义也就有了变化。super后加参数的是用来调用父类中具有相同形式的构造函数,如1和2处。this后加参数则调用的是当前具有相同参数的构造函数,如3处。当然,在Chinese的各个重载构造函数中,this和super在一般方法中的各种用法也仍可使用,比如4处,你可以将它替换为“this.prt”(因为它继承了父类中的那个方法)或者是“super.prt”(因为它是父类中的方法且可被子类访问),它照样可以正确运行。但这样似乎就有点画蛇添足的味道了。 最后,写了这么多,如果你能对“this通常指代当前对象,super通常指代父类”这句话牢记在心,那么本篇便达到了目的,其它的你自会在以后的编程实践当中慢慢体会、掌握。另外关于本篇中提到的继承,请参阅相关Java教程。 三、final final在Java中并不常用,然而它却为我们提供了诸如在C语言中定义常量的功能,不仅如此,final还可以让你控制你的成员、方法或者是一个类是否可被覆写或继承等功能,这些特点使final在Java中拥有了一个不可或缺的地位,也是学习Java时必须要知道和掌握的关键字之一。 final成员 当你在类中定义变量时,在其前面加上final关键字,那便是说,这个变量一旦被初始化便不可改变,这里不可改变的意思对基本类型来说是其值不可变,而对于对象变量来说其引用不可再变。其初始化可以在两个地方,一是其定义处,也就是说在final变量定义时直接给其赋值,二是在构造函数中。这两个地方只能选其一,要么在定义时给值,要么在构造函数中给值,不能同时既在定义时给了值,又在构造函数中给另外的值。下面这段代码演示了这一点: import java.util.List; import java.util.ArrayList; import java.util.LinkedList; public class Bat{ final PI=3.14; //在定义时便给址值 final int i; //因为要在构造函数中进行初始化,所以此处便不可再给值 final List list; //此变量也与上面的一样 Bat(){ i=100; list=new LinkedList(); } Bat(int ii,List l){ i=ii; list=l; } public static void main(String[] args){ Bat b=new Bat(); b.list.add(new Bat()); //b.i=25; //b.list=new ArrayList(); System.out.println("I="+b.i+" List Type:"+b.list.getClass()); b=new Bat(23,new ArrayList()); b.list.add(new Bat()); System.out.println("I="+b.i+" List Type:"+b.list.getClass()); } } 此程序很简单的演示了final的常规用法。在这里使用在构造函数中进行初始化的方法,这使你有了一点灵活性。如Bat的两个重载构造函数所示,第一个缺省构造函数会为你提供默认的值,重载的那个构造函数会根据你所提供的值或类型为final变量初始化。然而有时你并不需要这种灵活性,你只需要在定义时便给定其值并永不变化,这时就不要再用这种方法。在main方法中有两行语句注释掉了,如果你去掉注释,程序便无法通过编译,这便是说,不论是i的值或是list的类型,一旦初始化,确实无法再更改。然而b可以通过重新初始化来指定i的值或list的类型,输出结果中显示了这一点: I=100 List Type:class java.util.LinkedList I=23 List Type:class java.util.ArrayList 还有一种用法是定义方法中的参数为final,对于基本类型的变量,这样做并没有什么实际意义,因为基本类型的变量在调用方法时是传值的,也就是说你可以在方法中更改这个参数变量而不会影响到调用语句,然而对于对象变量,却显得很实用,因为对象变量在传递时是传递其引用,这样你在方法中对对象变量的修改也会影响到调用语句中的对象变量,当你在方法中不需要改变作为参数的对象变量时,明确使用final进行声明,会防止你无意的修改而影响到调用方法。 另外方法中的内部类在用到方法中的参变量时,此参变也必须声明为final才可使用,如下代码所示: public class INClass{ void innerClass(final String str){ class IClass{ IClass(){ System.out.println(str); } } IClass ic=new IClass(); } public static void main(String[] args){ INClass inc=new INClass(); inc.innerClass("Hello"); } } final方法 将方法声明为final,那就说明你已经知道这个方法提供的功能已经满足你要求,不需要进行扩展,并且也不允许任何从此类继承的类来覆写这个方法,但是继承仍然可以继承这个方法,也就是说可以直接使用。另外有一种被称为inline的机制,它会使你在调用final方法时,直接将方法主体插入到调用处,而不是进行例行的方法调用,例如保存断点,压栈等,这样可能会使你的程序效率有所提高,然而当你的方法主体非常庞大时,或你在多处调用此方法,那么你的调用主体代码便会迅速膨胀,可能反而会影响效率,所以你要慎用final进行方法定义。 final类 当你将final用于类身上时,你就需要仔细考虑,因为一个final类是无法被任何人继承的,那也就意味着此类在一个继承树中是一个叶子类,并且此类的设计已被认为很完美而不需要进行修改或扩展。对于final类中的成员,你可以定义其为final,也可以不是final。而对于方法,由于所属类为final的关系,自然也就成了final型的。你也可以明确的给final类中的方法加上一个final,但这显然没有意义。 下面的程序演示了final方法和final类的用法: final class final{ final String str="final Data"; public String str1="non final data"; final public void print(){ System.out.println("final method."); } public void what(){ System.out.println(str+"\n"+str1); } } public class FinalDemo { //extends final 无法继承 public static void main(String[] args){ final f=new final(); f.what(); f.print(); } } 从程序中可以看出,final类与普通类的使用几乎没有差别,只是它失去了被继承的特性。final方法与非final方法的区别也很难从程序行看出,只是记住慎用。 final在设计模式中的应用 在设计模式中有一种模式叫做不变模式,在Java中通过final关键字可以很容易的实现这个模式,在讲解final成员时用到的程序Bat.java就是一个不变模式的例子。如果你对此感兴趣,可以参考阎宏博士编写的《Java与模式》一书中的讲解。 到此为止,this,static,supert和final的使用已经说完了,如果你对这四个关键字已经能够大致说出它们的区别与用法,那便说明你基本已经掌握。然而,世界上的任何东西都不是完美无缺的,Java提供这四个关键字,给程序员的编程带来了很大的便利,但并不是说要让你到处使用,一旦达到滥用的程序,便适得其反,所以在使用时请一定要认真考虑。
|
Java中this、super用法简谈 通过用static来定义方法或成员,为我们编程提供了某种便利,从某种程度上可以说它类似于C语言中的全局函数和全局变量。但是,并不是说有了这种便利,你便可以随处使用,如果那样的话,你便需要认真考虑一下自己是否在用面向对象的思想编程,自己的程序是否是面向对象的。好了,现在开始讨论this&super这两个关键字的意义和用法。 在Java中,this通常指当前对象,super则指父类的。当你想要引用当前对象的某种东西,比如当前对象的某个方法,或当前对象的某个成员,你便可以利用this来实现这个目的,当然,this的另一个用途是调用当前对象的另一个构造函数,这些马上就要讨论。如果你想引用父类的某种东西,则非super莫属。由于this与super有如此相似的一些特性和与生俱来的某种关系,所以我们在这一块儿来讨论,希望能帮助你区分和掌握它们两个。 在一般方法中 最普遍的情况就是,在你的方法中的某个形参名与当前对象的某个成员有相同的名字,这时为了不至于混淆,你便需要明确使用this关键字来指明你要使用某个成员,使用方法是“this.成员名”,而不带this的那个便是形参。另外,还可以用“this.方法名”来引用当前对象的某个方法,但这时this就不是必须的了,你可以直接用方法名来访问那个方法,编译器会知道你要调用的是那一个。下面的代码演示了上面的用法: public class DemoThis{ private String name; private int age; DemoThis(String name,int age){ setName(name); //你可以加上this来调用方法,像这样:this.setName(name);但这并不是必须的 setAge(age); this.print(); br> } public void setName(String name){ this.name=name;//此处必须指明你要引用成员变量 } public void etAge(int age){ this.age=age; } public void print(){ System.out.println("Name="+name+" ge="+age); //在此行中并不需要用this,因为没有会导致混淆的东西 } public static void main(String[] args){ DemoThis dt=new DemoThis("Kevin","22"); 这段代码很简单,不用解释你也应该能看明白。在构造函数中你看到用this.print(),你完全可以用print()来代替它,两者效果一样。下面我们修改这个程序,来演示super的用法。 class Person{ public int c; private String name; private int age; protected void setName(String name){ this.name=name; } protected void setAge(int age){ this.age=age; } protected void print(){ System.out.println("Name="+name+" Age="+age); } } public class DemoSuper extends Person{ public void print(){ System.out.println("DemoSuper:"); super.print(); } public static void main(String[] args){ DemoSuper ds=new DemoSuper(); ds.setName("kevin"); ds.setAge(22); ds.print(); } } 在DemoSuper中,重新定义的print方法覆写了父类的print方法,它首先做一些自己的事情,然后调用父类的那个被覆写了的方法。输出结果说明了这一点: DemoSuper: Name=kevin Age=22
这样的使用方法是比较常用的。另外如果父类的成员可以被子类访问,那你可以像使用this一样使用它,用“super.父类中的成员名”的方式,但常常你并不是这样来访问父类中的成员名的。 在构造函数中构造函数是一种特殊的方法,在对象初始化的时候自动调用。在构造函数中,this和super也有上面说的种种使用方式,并且它还有特殊的地方,请看下面的例子:
class Person{
public static void prt(String s){ System.out.println(s); } Person(){ prt("A Person."); } Person(String name){ prt("A person name is:"+name);
} } public class Chinese extends Person{ Chinese(){ super(); //调用父类构造函数(1) prt("A chinese.");//(4) } Chinese(String name){ super(name);//调用父类具有相同形参的构造函数(2) prt("his name is:"+name); } Chinese(String name,int age){ this(name);//调用当前具有相同形参的构造函数(3) prt("his age is:"+age); } public static void main(String[] args){ Chinese cn=new Chinese(); cn=new Chinese("kevin"); cn=new Chinese("kevin",22); } } 在这段程序中,this和super不再是像以前那样用“.”连接一个方法或成员,而是直接在其后跟 上适当的参数,因此它的意义也就有了变化。super后加参数的是用来调用父类中具有相同形式的 构造函数,如1和2处。this后加参数则调用的是当前具有相同参数的构造函数,如3处。当然,在 Chinese的各个重载构造函数中,this和super在一般方法中的各种用法也仍可使用,比如4处,你 可以将它替换为“this.prt”(因为它继承了父类中的那个方法)或者是“super.prt”(因为它 是父类中的方法且可被子类访问),它照样可以正确运行。但这样似乎就有点画蛇添足的味道 了。 最后,写了这么多,如果你能对“this通常指代当前对象,super通常指代父类”这句话牢记在 心,那么本篇便达到了目的,其它的你自会在以后的编程实践当中慢慢体会、掌握。另外关于本 篇中提到的继承,请参阅相关Java教程。
泊船瓜洲
@ 2005-08-30 18:08
Java中的类反射机制 (转帖) 一、反射的概念 : 反射的概念是由Smith在1982年首次提出的,主要是指程序可以访问、检测和修改它本身状态或行为的一种能力。这一概念的提出很快引发了计算机科学领域关于应用反射性的研究。它首先被程序语言的设计领域所采用,并在Lisp和面向对象方面取得了成绩。其中LEAD/LEAD++ 、OpenC++ 、 MetaXa和OpenJava等就是基于反射机制的语言。最近,反射机制也被应用到了视窗系统、操作系统和文件系统中。
反射本身并不是一个新概念,它可能会使我们联想到光学中的反射概念,尽管计算机科学赋予了反射概念新的含义,但是,从现象上来说,它们确实有某些相通之处,这些有助于我们的理解。在计算机科学领域,反射是指一类应用,它们能够自描述和自控制。也就是说,这类应用通过采用某种机制来实现对自己行为的描述(self-representation)和监测(examination),并能根据自身行为的状态和结果,调整或修改应用所描述行为的状态和相关的语义。可以看出,同一般的反射概念相比,计算机科学领域的反射不单单指反射本身,还包括对反射结果所采取的措施。所有采用反射机制的系统(即反射系统)都希望使系统的实现更开放。可以说,实现了反射机制的系统都具有开放性,但具有开放性的系统并不一定采用了反射机制,开放性是反射系统的必要条件。一般来说,反射系统除了满足开放性条件外还必须满足原因连接(Causally-connected)。所谓原因连接是指对反射系统自描述的改变能够立即反映到系统底层的实际状态和行为上的情况,反之亦然。开放性和原因连接是反射系统的两大基本要素。13700863760
Java中,反射是一种强大的工具。它使您能够创建灵活的代码,这些代码可以在运行时装配,无需在组件之间进行源代表链接。反射允许我们在编写与执行时,使我们的程序代码能够接入装载到JVM中的类的内部信息,而不是源代码中选定的类协作的代码。这使反射成为构建灵活的应用的主要工具。但需注意的是:如果使用不当,反射的成本很高。
二、Java中的类反射: Reflection 是 Java 程序开发语言的特征之一,它允许运行中的 Java 程序对自身进行检查,或者说“自审”,并能直接操作程序的内部属性。Java 的这一能力在实际应用中也许用得不是很多,但是在其它的程序设计语言中根本就不存在这一特性。例如,Pascal、C 或者 C++ 中就没有办法在程序中获得函数定义相关的信息。
1.检测类:
1.1 reflection的工作机制
考虑下面这个简单的例子,让我们看看 reflection 是如何工作的。
import java.lang.reflect.*; public class DumpMethods { public static void main(String args[]) { try { Class c = Class.forName(args[0]); Method m[] = c.getDeclaredMethods(); for (int i = 0; i < m.length; i++) System.out.println(m.toString()); } catch (Throwable e) { System.err.println(e); } } }
按如下语句执行:
java DumpMethods java.util.Stack
它的结果输出为:
public java.lang.Object java.util.Stack.push(java.lang.Object)
public synchronized java.lang.Object java.util.Stack.pop()
public synchronized java.lang.Object java.util.Stack.peek()
public boolean java.util.Stack.empty()
public synchronized int java.util.Stack.search(java.lang.Object)
这样就列出了java.util.Stack 类的各方法名以及它们的限制符和返回类型。
这个程序使用 Class.forName 载入指定的类,然后调用 getDeclaredMethods 来获取这个类中定义了的方法列表。java.lang.reflect.Methods 是用来描述某个类中单个方法的一个类。
1.2 Java类反射中的主要方法
对于以下三类组件中的任何一类来说 -- 构造函数、字段和方法 -- java.lang.Class 提供四种独立的反射调用,以不同的方式来获得信息。调用都遵循一种标准格式。以下是用于查找构造函数的一组反射调用:
l Constructor getConstructor(Class[] params) -- 获得使用特殊的参数类型的公共构造函数,
l Constructor[] getConstructors() -- 获得类的所有公共构造函数
l Constructor getDeclaredConstructor(Class[] params) -- 获得使用特定参数类型的构造函数(与接入级别无关)
l Constructor[] getDeclaredConstructors() -- 获得类的所有构造函数(与接入级别无关)
获得字段信息的Class 反射调用不同于那些用于接入构造函数的调用,在参数类型数组中使用了字段名:
l Field getField(String name) -- 获得命名的公共字段
l Field[] getFields() -- 获得类的所有公共字段
l Field getDeclaredField(String name) -- 获得类声明的命名的字段
l Field[] getDeclaredFields() -- 获得类声明的所有字段
用于获得方法信息函数:
l Method getMethod(String name, Class[] params) -- 使用特定的参数类型,获得命名的公共方法
l Method[] getMethods() -- 获得类的所有公共方法
l Method getDeclaredMethod(String name, Class[] params) -- 使用特写的参数类型,获得类声明的命名的方法
l Method[] getDeclaredMethods() -- 获得类声明的所有方法
1.3开始使用 Reflection:
用于 reflection 的类,如 Method,可以在 java.lang.relfect 包中找到。使用这些类的时候必须要遵循三个步骤:第一步是获得你想操作的类的 java.lang.Class 对象。在运行中的 Java 程序中,用 java.lang.Class 类来描述类和接口等。
下面就是获得一个 Class 对象的方法之一:
Class c = Class.forName("java.lang.String");
这条语句得到一个 String 类的类对象。还有另一种方法,如下面的语句:
Class c = int.class;
或者
Class c = Integer.TYPE;
它们可获得基本类型的类信息。其中后一种方法中访问的是基本类型的封装类 (如 Integer) 中预先定义好的 TYPE 字段。
第二步是调用诸如 getDeclaredMethods 的方法,以取得该类中定义的所有方法的列表。
一旦取得这个信息,就可以进行第三步了——使用 reflection API 来操作这些信息,如下面这段代码:
Class c = Class.forName("java.lang.String");
Method m[] = c.getDeclaredMethods();
System.out.println(m[0].toString());
它将以文本方式打印出 String 中定义的第一个方法的原型。
2.处理对象:
如果要作一个开发工具像debugger之类的,你必须能发现filed values,以下是三个步骤:
a.创建一个Class对象 b.通过getField 创建一个Field对象 c.调用Field.getXXX(Object)方法(XXX是Int,Float等,如果是对象就省略;Object是指实例).
例如: import java.lang.reflect.*; import java.awt.*;
class SampleGet {
public static void main(String[] args) { Rectangle r = new Rectangle(100, 325); printHeight(r);
}
static void printHeight(Rectangle r) { Field heightField; Integer heightvalue; Class c = r.getClass(); try { heightField = c.getField("height"); heightvalue = (Integer) heightField.get(r); System.out.println("Height: " + heightvalue.toString()); } catch (NoSuchFieldException e) { System.out.println(e); } catch (SecurityException e) { System.out.println(e); } catch (IllegalAccessException e) { System.out.println(e); } } }
三、安全性和反射: 在处理反射时安全性是一个较复杂的问题。反射经常由框架型代码使用,由于这一点,我们可能希望框架能够全面接入代码,无需考虑常规的接入限制。但是,在其它情况下,不受控制的接入会带来严重的安全性风险,例如当代码在不值得信任的代码共享的环境中运行时。
由于这些互相矛盾的需求,Java编程语言定义一种多级别方法来处理反射的安全性。基本模式是对反射实施与应用于源代码接入相同的限制:
n 从任意位置到类公共组件的接入
n 类自身外部无任何到私有组件的接入
n 受保护和打包(缺省接入)组件的有限接入
不过至少有些时候,围绕这些限制还有一种简单的方法。我们可以在我们所写的类中,扩展一个普通的基本类 java.lang.reflect.AccessibleObject 类。这个类定义了一种setAccessible方法,使我们能够启动或关闭对这些类中其中一个类的实例的接入检测。唯一的问题在于如果使用了安全性管理器,它将检测正在关闭接入检测的代码是否许可了这样做。如果未许可,安全性管理器抛出一个例外。
下面是一段程序,在TwoString 类的一个实例上使用反射来显示安全性正在运行:
public class ReflectSecurity {
public static void main(String[] args) {
try {
TwoString ts = new TwoString("a", "b");
Field field = clas.getDeclaredField("m_s1");
// field.setAccessible(true);
System.out.println("Retrieved value is " +
field.get(inst));
} catch (Exception ex) {
ex.printStackTrace(System.out);
}
}
}
如果我们编译这一程序时,不使用任何特定参数直接从命令行运行,它将在field .get(inst)调用中抛出一个 IllegalAccessException异常。如果我们不注释field.setAccessible(true)代码行,那么重新编译并重新运行该代码,它将编译成功。最后,如果我们在命令行添加了JVM参数-Djava.security.manager以实现安全性管理器,它仍然将不能通过编译,除非我们定义了ReflectSecurity类的许可权限。
四、反射性能: 反射是一种强大的工具,但也存在一些不足。一个主要的缺点是对性能有影响。使用反射基本上是一种解释操作,我们可以告诉JVM,我们希望做什么并且它满足我们的要求。这类操作总是慢于只直接执行相同的操作。
下面的程序是字段接入性能测试的一个例子,包括基本的测试方法。每种方法测试字段接入的一种形式 -- accessSame 与同一对象的成员字段协作,accessOther 使用可直接接入的另一对象的字段,accessReflection 使用可通过反射接入的另一对象的字段。在每种情况下,方法执行相同的计算 -- 循环中简单的加/乘顺序。
程序如下:
public int accessSame(int loops) {
m_value = 0;
for (int index = 0; index < loops; index++) {
m_value = (m_value + ADDITIVE_value) *
MULTIPLIER_value;
}
return m_value;
}
public int accessReference(int loops) {
TimingClass timing = new TimingClass();
for (int index = 0; index < loops; index++) {
timing.m_value = (timing.m_value + ADDITIVE_value) *
MULTIPLIER_value;
}
return timing.m_value;
}
public int accessReflection(int loops) throws Exception {
TimingClass timing = new TimingClass();
try {
Field field = TimingClass.class.
getDeclaredField("m_value");
for (int index = 0; index < loops; index++) {
int value = (field.getInt(timing) +
ADDITIVE_value) * MULTIPLIER_value;
field.setInt(timing, value);
}
return timing.m_value;
} catch (Exception ex) {
System.out.println("Error using reflection");
throw ex;
}
}
在上面的例子中,测试程序重复调用每种方法,使用一个大循环数,从而平均多次调用的时间衡量结果。平均值中不包括每种方法第一次调用的时间,因此初始化时间不是结果中的一个因素。下面的图清楚的向我们展示了每种方法字段接入的时间:
图 1:字段接入时间 :
我们可以看出:在前两副图中(Sun JVM),使用反射的执行时间超过使用直接接入的1000倍以上。通过比较,IBM JVM可能稍好一些,但反射方法仍旧需要比其它方法长700倍以上的时间。任何JVM上其它两种方法之间时间方面无任何显著差异,但IBM JVM几乎比Sun JVM快一倍。最有可能的是这种差异反映了Sun Hot Spot JVM的专业优化,它在简单基准方面表现得很糟糕。反射性能是Sun开发1.4 JVM时关注的一个方面,它在反射方法调用结果中显示。在这类操作的性能方面,Sun 1.4.1 JVM显示了比1.3.1版本很大的改进。
如果为为创建使用反射的对象编写了类似的计时测试程序,我们会发现这种情况下的差异不象字段和方法调用情况下那么显著。使用newInstance()调用创建一个简单的java.lang.Object实例耗用的时间大约是在Sun 1.3.1 JVM上使用new Object()的12倍,是在 IBM 1.4.0 JVM的四倍,只是Sun 1.4.1 JVM上的两部。使用Array.newInstance(type, size)创建一个数组耗用的时间是任何测试的JVM上使用new type[size]的两倍,随着数组大小的增加,差异逐步缩小。
结束语: Java语言反射提供一种动态链接程序组件的多功能方法。它允许程序创建和控制任何类的对象(根据安全性限制),无需提前硬编码目标类。这些特性使得反射特别适用于创建以非常普通的方式与对象协作的库。例如,反射经常在持续存储对象为数据库、XML或其它外部格式的框架中使用。 Java reflection 非常有用,它使类和数据结构能按名称动态检索相关信息,并允许在运行着的程序中操作这些信息。Java 的这一特性非常强大,并且是其它一些常用语言,如 C、C++、Fortran 或者 Pascal 等都不具备的。
但反射有两个缺点。第一个是性能问题。用于字段和方法接入时反射要远慢于直接代码。性能问题的程度取决于程序中是如何使用反射的。如果它作为程序运行中相对很少涉及的部分,缓慢的性能将不会是一个问题。即使测试中最坏情况下的计时图显示的反射操作只耗用几微秒。仅反射在性能关键的应用的核心逻辑中使用时性能问题才变得至关重要。
许多应用中更严重的一个缺点是使用反射会模糊程序内部实际要发生的事情。程序人员希望在源代码中看到程序的逻辑,反射等绕过了源代码的技术会带来维护问题。反射代码比相应的直接代码更复杂,正如性能比较的代码实例中看到的一样。解决这些问题的最佳方案是保守地使用反射——仅在它可以真正增加灵活性的地方 ——记录其在目标类中的使用。
利用反射实现类的动态加载
Bromon原创 请尊重版权
最近在成都写一个移动增值项目,俺负责后台server端。功能很简单,手机用户通过GPRS打开Socket与服务器连接,我则根据用户传过来的数据做出响应。做过类似项目的兄弟一定都知道,首先需要定义一个类似于MSNP的通讯协议,不过今天的话题是如何把这个系统设计得具有高度的扩展性。由于这个项目本身没有进行过较为完善的客户沟通和需求分析,所以以后肯定会有很多功能上的扩展,通讯协议肯定会越来越庞大,而我作为一个不那么勤快的人,当然不想以后再去修改写好的程序,所以这个项目是实践面向对象设计的好机会。
首先定义一个接口来隔离类:
package org.bromon.reflect;
public interface Operator
{
public java.util.List act(java.util.List params)
}
根据设计模式的原理,我们可以为不同的功能编写不同的类,每个类都继承Operator接口,客户端只需要针对Operator接口编程就可以避免很多麻烦。比如这个类:
package org.bromon.reflect.*;
public class Success implements Operator
{
public java.util.List act(java.util.List params)
{
List result=new ArrayList();
result.add(new String(“操作成功”));
return result;
}
}
我们还可以写其他很多类,但是有个问题,接口是无法实例化的,我们必须手动控制具体实例化哪个类,这很不爽,如果能够向应用程序传递一个参数,让自己去选择实例化一个类,执行它的act方法,那我们的工作就轻松多了。
很幸运,我使用的是Java,只有Java才提供这样的反射机制,或者说内省机制,可以实现我们的无理要求。编写一个配置文件emp.properties:
#成功响应
1000=Success
#向客户发送普通文本消息
2000=Load
#客户向服务器发送普通文本消息
3000=Store
文件中的键名是客户将发给我的消息头,客户发送1000给我,那么我就执行Success类的act方法,类似的如果发送2000给我,那就执行Load 类的act方法,这样一来系统就完全符合开闭原则了,如果要添加新的功能,完全不需要修改已有代码,只需要在配置文件中添加对应规则,然后编写新的类,实现act方法就ok,即使我弃这个项目而去,它将来也可以很好的扩展。这样的系统具备了非常良好的扩展性和可插入性。
下面这个例子体现了动态加载的功能,程序在执行过程中才知道应该实例化哪个类:
package org.bromon.reflect.*;
import java.lang.reflect.*;
public class TestReflect
{
//加载配置文件,查询消息头对应的类名
private String loadProtocal(String header)
{
String result=null;
try
{
Properties prop=new Properties();
FileInputStream fis=new FileInputStream("emp.properties");
prop.load(fis);
result=prop.getProperty(header);
fis.close();
}catch(Exception e)
{
System.out.println(e);
}
return result;
}
//针对消息作出响应,利用反射导入对应的类
public String response(String header,String content)
{
String result=null;
String s=null;
try
{
/*
* 导入属性文件emp.properties,查询header所对应的类的名字
* 通过反射机制动态加载匹配的类,所有的类都被Operator接口隔离
* 可以通过修改属性文件、添加新的类(继承MsgOperator接口)来扩展协议
*/
s="org.bromon.reflect."+this.loadProtocal(header);
//加载类
Class c=Class.forName(s);
//创建类的事例
Operator mo=(Operator)c.newInstance();
//构造参数列表
Class params[]=new Class[1];
params[0]=Class.forName("java.util.List");
//查询act方法
Method m=c.getMethod("act",params);
Object args[]=new Object[1];
args[0]=content;
//调用方法并且获得返回
Object returnObject=m.invoke(mo,args);
}catch(Exception e)
{
System.out.println("Handler-response:"+e);
}
return result;
}
public static void main(String args[])
{
TestReflect tr=new TestReflect();
tr.response(args[0],”消息内容”);
}
}
测试一下:java TestReflect 1000
这个程序是针对Operator编程的,所以无需做任何修改,直接提供Load和Store类,就可以支持2000、3000做参数的调用。
有了这样的内省机制,可以把接口的作用发挥到极至,设计模式也更能体现出威力,而不仅仅供我们饭后闲聊。
array(数组)和Vector是十分相似的Java构件(constructs),两者全然不同,在选择使用时应根据各自的功能来确定。
1、数组:Java arrays的元素个数不能下标越界,从很大程度上保证了Java程序的安全性,而其他一些语言出现这一问题时常导致灾难性的后果。 Array可以存放Object和基本数据类型,但创建时必须指定数组的大小,并不能再改变。值得注意的是:当Array中的某一元素存放的是Objrct reference 时,Java不会调用默认的构造函数,而是将其初值设为null,当然这跟Java对各类型数据赋默认值的规则是一样的,对基本数据类型同样适用。
2、Vector:对比于Array,当更多的元素被加入进来以至超出其容量时,Vector的size会动态增长,而Array容量是定死的。同时,Vector在删除一些元素后,其所有下标大于被删除元素的元素都依次前移,并获得新下标比原来的小了)。注意:当调用Vector的size()方法时,返回Vector中实际元素的个数。 Vector内部实际是以Array实现的,也通过元素的整数索引来访问元素,但它只能存放java.lang.Object对象,不能用于存放基本类型数据,比如要存放一个整数10,得用new Integer(10)构造出一个Integer包装类对象再放进去。当Vector中的元素个数发生变化时, 其内部的Array必须重新分配并进行拷贝,因此这是一点值得考虑的效率问题。 Vetor同时也实现了List接口,所以也可以算作Colletion了,只是它还特殊在:Vector is synchronized。即Vetor对象自身实现了同步机制。
3、ArrayList:实现了List接口,功能与Vetor一样,只是没有同步机制,当然元素的访问方式为从List中继承而来,可存放任何类型的对象。
4、HashMap:继承了Map接口,实现用Keys来存储和访问Values,Keys和Values都可以为空,它与Hashtable类的区别在于Hashtable类的Keys不能为null,并Hashtable类有同步机制控制,而HashMap类没有。 在Struts类库中实现了一个LableValueBean,用Lable(Key)来存储和访问Value,很方便。
本文提供一个项目中的错误实例,提供对其观察和分析,揭示出 Java语言实例化一个对象具体过程,最后总结出设计Java类的一个重要规则。通过阅读本文,可以使Java程序员理解Java对象的构造过程,从而设计出更加健壮的代码。本文适合Java初学者和需要提高的Java程序员阅读。 程序掷出了一个异常 作者曾经在一个项目里面向项目组成员提供了一个抽象的对话框基类,使用者只需在子类中实现基类的一个抽象方法来画出显示数据的界面,就可使项目内的对话框具有相同的风格。具体的代码实现片断如下(为了简洁起见,省略了其他无关的代码): public abstract class BaseDlg extends JDialog { public BaseDlg(Frame frame, String title) { super(frame, title, true); this.getContentPane().setLayout(new BorderLayout()); this.getContentPane().add(createHeadPanel(), BorderLayout.NORTH); this.getContentPane().add(createClientPanel(), BorderLayout.CENTER); this.getContentPane().add(createButtonPanel(), BorderLayout.SOUTH); }
private JPanel createHeadPanel() { ... // 创建对话框头部 }
// 创建对话框客户区域,交给子类实现 protected abstract JPanel createClientPanel();
private JPanel createButtonPanel { ... // 创建按钮区域 } } |
这个类在有的代码中工作得很好,但一个同事在使用时,程序却掷出了一个NullPointerException违例!经过比较,找出了工作正常和不正常的程序的细微差别,代码片断分别如下: 一、正常工作的代码: public class ChildDlg1 extends BaseDlg { JTextField jTextFieldName; public ChildDlg1() { super(null, "Title"); } public JPanel createClientPanel() { jTextFieldName = new JTextField(); JPanel panel = new JPanel(new FlowLayout()); panel.add(jTextFieldName); ... // 其它代码 return panel; } ... } ChildDlg1 dlg = new ChildDlg1(frame, "Title"); // 外部的调用 |
二、工作不正常的代码: public class ChildDlg2 extends BaseDlg { JTextField jTextFieldName = new JTextField(); public ChildDlg2() { super(null, "Title"); } public JPanel createClientPanel() { JPanel panel = new JPanel(new FlowLayout()); panel.add(jTextFieldName); ... // 其它代码 return panel; } ... } ChildDlg2 dlg = new ChildDlg2(); // 外部的调用 |
你看出来两段代码之间的差别了吗?对了,两者的差别仅仅在于类变量jTextFieldName的初始化时间。经过跟踪,发现在执行panel.add(jTextFieldName)语句之时,jTextFieldName确实是空值。 我们知道,Java允许在定义类变量的同时给变量赋初始值。系统运行过程中需要创建一个对象的时候,首先会为对象分配内存空间,然后在“先于调用任何方法之前”根据变量在类内的定义顺序来初始化变量,接着再调用类的构造方法。那么,在本例中,为什么在变量定义时便初始化的代码反而会出现空指针违例呢? 对象的创建过程和初始化 实际上,前面提到的“变量初始化发生在调用任何方法包括构造方法之前”这句话是不确切的,当我们把眼光集中在单个类上时,该说法成立;然而,当把视野扩大到具有继承关系的两个或多个类上时,该说法不成立。 对象的创建一般有两种方式,一种是用new操作符,另一种是在一个Class对象上调用newInstance方法;其创建和初始化的实际过程是一样的: 首先为对象分配内存空间,包括其所有父类的可见或不可见的变量的空间,并初始化这些变量为默认值,如int类型为0,boolean类型为false,对象类型为null; 然后用下述5个步骤来初始化这个新对象: 1)分配参数给指定的构造方法; 2)如果这个指定的构造方法的第一个语句是用this指针显式地调用本类的其它构造方法,则递归执行这5个步骤;如果执行过程正常则跳到步骤5; 3)如果构造方法的第一个语句没有显式调用本类的其它构造方法,并且本类不是Object类(Object是所有其它类的祖先),则调用显式(用super指针)或隐式地指定的父类的构造方法,递归执行这5个步骤;如果执行过程正常则跳到步骤5; 4)按照变量在类内的定义顺序来初始化本类的变量,如果执行过程正常则跳到步骤5; 5)执行这个构造方法中余下的语句,如果执行过程正常则过程结束。 这一过程可以从下面的时序图中获得更清晰的认识: 对分析本文的实例最重要的,用一句话说,就是“父类的构造方法调用发生在子类的变量初始化之前”。可以用下面的例子来证明: // Petstore.java class Animal { Animal() { System.out.println("Animal"); } } class Cat extends Animal { Cat() { System.out.println("Cat"); } } class Store { Store() { System.out.println("Store"); } } public class Petstore extends Store{ Cat cat = new Cat(); Petstore() { System.out.println("Petstore"); } public static void main(String[] args) { new Petstore(); } } |
运行这段代码,它的执行结果如下: Store Animal Cat Petstore 从结果中可以看出,在创建一个Petstore类的实例时,首先调用了它的父类Store的构造方法;然后试图创建并初始化变量cat;在创建cat时,首先调用了Cat类的父类Animal的构造方法;其后才是Cat的构造方法主体,最后才是Petstore类的构造方法的主体。 寻找程序产生例外的原因
现在回到本文开始提到的实例中来,当程序创建一个ChildDlg2的实例时,根据super(null, “Title”)语句,首先执行其父类BaseDlg的构造方法;在BaseDlg的构造方法中调用了createClientPanel()方法,
这个方法是抽象方法并且被子类ChildDlg2实现了,因此,实际调用的方法是ChildDlg2中的createClientPanel()方法(因为Java里面采用“动态绑定”来绑定所有非final的方法);createClientPanel()方法使用了ChildDlg2类的实例变量jTextFieldName,而此时ChildDlg2的变量初始化过程尚未进行,jTextFieldName是null值!所以,ChildDlg2的构造过程掷出一个NullPointerException也就不足为奇了。
再来看ChildDlg1,它的jTextFieldName的初始化代码写在了createClientPanel()方法内部的开始处,这样它就能保证在使用之前得到正确的初始化,因此这段代码工作正常。
解决问题的两种方式
通过上面的分析过程可以看出,要排除故障,最简单的方法就是要求项目组成员在继承使用BaseDlg类,实现createClientPanel()方法时,凡方法内部要使用的变量必须首先正确初始化,就象ChildDlg1一样。然而,把类变量放在类方法内初始化是一种很不好的设计行为,它最适合的地方就是在变量定义块和构造方法中。
在本文的实例中,引发错误的实质并不在ChildDlg2上,而在其父类BaseDlg上,是它在自己的构造方法中不适当地调用了一个待实现的抽象方法。
从概念上讲,构造方法的职责是正确初始化类变量,让对象进入可用状态。而BaseDlg却赋给了构造方法额外的职责。
本文实例的更好的解决方法是修改BaseDlg类:
public abstract class BaseDlg extends JDialog { public BaseDlg(Frame frame, String title) { super(frame, title, true); this.getContentPane().setLayout(new BorderLayout()); this.getContentPane().add(createHeadPanel(), BorderLayout.NORTH); this.getContentPane().add(createButtonPanel(), BorderLayout.SOUTH); }
/** 创建对话框实例后,必须调用此方法来布局用户界面 */ public void initGUI() { this.getContentPane().add(createClientPanel(), BorderLayout.CENTER); }
private JPanel createHeadPanel() { ... // 创建对话框头部 }
// 创建对话框客户区域,交给子类实现 protected abstract JPanel createClientPanel();
private JPanel createButtonPanel { ... // 创建按钮区域 } } |
新的BaseDlg类增加了一个initGUI()方法,程序员可以这样使用这个类:
ChildDlg dlg = new ChildDlg(); dlg.initGUI(); dlg.setVisible(true); |
总结
类的构造方法的基本目的是正确初始化类变量,不要赋予它过多的职责。
设计类构造方法的基本规则是:用尽可能简单的方法使对象进入就绪状态;如果可能,避免调用任何方法。在构造方法内唯一能安全调用的是基类中具有final属性的方法或者private方法(private方法会被编译器自动设置final属性)。final的方法因为不能被子类覆盖,所以不会产生问题。 | |
所以那些有子类实现的方法尽量不要放在父类的构造函数中,一定要注意设计好构造函数。
这个问题一直困扰我很久,一直没有完全的理解: abstractclass和interface是Java语言中对于抽象类定义进行支持的两种机制,正是由于这两种机制的存在,才赋予了Java强大的面向对象能力。
abstractclass和interface之间在对于抽象类定义的支持方面具有很大的相似性,甚至可以相互替换,因此很多开发者在进行抽象类定义时对于abstractclass和interface的选择显得比较随意。其实,两者之间还是有很大的区别的,对于它们的选择甚至反映出对于问题领域本质的理解、对于设计意图的理解是否正确、合理。本文将对它们之间的区别进行一番剖析,试图给开发者提供一个在二者之间进行选择的依据。理解抽象类 abstractclass和interface在Java语言中都是用来进行抽象类(本文中的抽象类并非从abstractclass翻译而来,它表示的是一个抽象体,而abstractclass为Java语言中用于定义抽象类的一种方法,请读者注意区分)定义的,那么什么是抽象类,使用抽象类能为我们带来什么好处呢? 在面向对象的概念中,我们知道所有的对象都是通过类来描绘的,但是反过来却不是这样。并不是所有的类都是用来描绘对象的,如果一个类中没有包含足够的信息来描绘一个具体的对象,这样的类就是抽象类。抽象类往往用来表征我们在对问题领域进行分析、设计中得出的抽象概念,是对一系列看上去不同,但是本质上相同的具体概念的抽象。比如:如果我们进行一个图形编辑软件的开发,就会发现问题领域存在着圆、三角形这样一些具体概念,它们是不同的,但是它们又都属于形状这样一个概念,形状这个概念在问题领域是不存在的,它就是一个抽象概念。正是因为抽象的概念在问题领域没有对应的具体概念,所以用以表征抽象概念的抽象类是不能够实例化的。 在面向对象领域,抽象类主要用来进行类型隐藏。我们可以构造出一个固定的一组行为的抽象描述,但是这组行为却能够有任意个可能的具体实现方式。这个抽象描述就是抽象类,而这一组任意个可能的具体实现则表现为所有可能的派生类。模块可以操作一个抽象体。由于模块依赖于一个固定的抽象体,因此它可以是不允许修改的;同时,通过从这个抽象体派生,也可扩展此模块的行为功能。熟悉OCP的读者一定知道,为了能够实现面向对象设计的一个最核心的原则OCP(Open-ClosedPrinciple),抽象类是其中的关键所在。 从语法定义层面看abstractclass和interface 在语法层面,Java语言对于abstractclass和interface给出了不同的定义方式,下面以定义一个名为Demo的抽象类为例来说明这种不同。 使用abstractclass的方式定义Demo抽象类的方式如下: abstractclassDemo{ abstractvoidmethod1(); abstractvoidmethod2(); … } 使用interface的方式定义Demo抽象类的方式如下: interfaceDemo{ voidmethod1(); voidmethod2(); … } 在abstractclass方式中,Demo可以有自己的数据成员,也可以有非abstarct的成员方法,而在interface方式的实现中,Demo只能够有静态的不能被修改的数据成员(也就是必须是staticfinal的,不过在interface中一般不定义数据成员),所有的成员方法都是abstract的。从某种意义上说,interface是一种特殊形式的abstractclass。 从编程的角度来看,abstractclass和interface都可以用来实现"designbycontract"的思想。但是在具体的使用上面还是有一些区别的。 首先,abstractclass在Java语言中表示的是一种继承关系,一个类只能使用一次继承关系。但是,一个类却可以实现多个interface。也许,这是Java语言的设计者在考虑Java对于多重继承的支持方面的一种折中考虑吧。 其次,在abstractclass的定义中,我们可以赋予方法的默认行为。但是在interface的定义中,方法却不能拥有默认行为,为了绕过这个限制,必须使用委托,但是这会增加一些复杂性,有时会造成很大的麻烦。 在抽象类中不能定义默认行为还存在另一个比较严重的问题,那就是可能会造成维护上的麻烦。因为如果后来想修改类的界面(一般通过abstractclass或者interface来表示)以适应新的情况(比如,添加新的方法或者给已用的方法中添加新的参数)时,就会非常的麻烦,可能要花费很多的时间(对于派生类很多的情况,尤为如此)。但是如果界面是通过abstractclass来实现的,那么可能就只需要修改定义在abstractclass中的默认行为就可以了。 同样,如果不能在抽象类中定义默认行为,就会导致同样的方法实现出现在该抽象类的每一个派生类中,违反了"onerule,oneplace"原则,造成代码重复,同样不利于以后的维护。因此,在abstractclass和interface间进行选择时要非常的小心。 从设计理念层面看abstractclass和interface 上面主要从语法定义和编程的角度论述了abstractclass和interface的区别,这些层面的区别是比较低层次的、非本质的。本小节将从另一个层面:abstractclass和interface所反映出的设计理念,来分析一下二者的区别。作者认为,从这个层面进行分析才能理解二者概念的本质所在。 前面已经提到过,abstarctclass在Java语言中体现了一种继承关系,要想使得继承关系合理,父类和派生类之间必须存在"isa"关系,即父类和派生类在概念本质上应该是相同的(参考文献〔3〕中有关于"isa"关系的大篇幅深入的论述,有兴趣的读者可以参考)。对于interface来说则不然,并不要求interface的实现者和interface定义在概念本质上是一致的,仅仅是实现了interface定义的契约而已。为了使论述便于理解,下面将通过一个简单的实例进行说明。 考虑这样一个例子,假设在我们的问题领域中有一个关于Door的抽象概念,该Door具有执行两个动作open和close,此时我们可以通过abstractclass或者interface来定义一个表示该抽象概念的类型,定义方式分别如下所示: 使用abstractclass方式定义Door: abstractclassDoor{ abstractvoidopen(); abstractvoidclose(); } 使用interface方式定义Door: interfaceDoor{ voidopen(); voidclose(); } 其他具体的Door类型可以extends使用abstractclass方式定义的Door或者implements使用interface方式定义的Door。看起来好像使用abstractclass和interface没有大的区别。 | |
如果现在要求Door还要具有报警的功能。
我们该如何设计针对该例子的类结构呢(在本例中,主要是为了展示abstractclass和interface反映在设计理念上的区别,其他方面无关的问题都做了简化或者忽略)?下面将罗列出可能的解决方案,并从设计理念层面对这些不同的方案进行分析。解决方案一: 简单的在Door的定义中增加一个alarm方法,如下: abstractclassDoor{ abstractvoidopen(); abstractvoidclose(); abstractvoidalarm(); } 或者 interfaceDoor{ voidopen(); voidclose(); voidalarm(); } 那么具有报警功能的AlarmDoor的定义方式如下: classAlarmDoorextendsDoor{ voidopen(){…} voidclose(){…} voidalarm(){…} } 或者 classAlarmDoorimplementsDoor{ voidopen(){…} voidclose(){…} voidalarm(){…} } 这种方法违反了面向对象设计中的一个核心原则ISP(InterfaceSegregationPriciple),在Door的定义中把Door概念本身固有的行为方法和另外一个概念"报警器"的行为方法混在了一起。这样引起的一个问题是那些仅仅依赖于Door这个概念的模块会因为"报警器"这个概念的改变(比如:修改alarm方法的参数)而改变,反之依然。 解决方案二: 既然open、close和alarm属于两个不同的概念,根据ISP原则应该把它们分别定义在代表这两个概念的抽象类中。定义方式有:这两个概念都使用abstractclass方式定义;两个概念都使用interface方式定义;一个概念使用abstractclass方式定义,另一个概念使用interface方式定义。 显然,由于Java语言不支持多重继承,所以两个概念都使用abstractclass方式定义是不可行的。后面两种方式都是可行的,但是对于它们的选择却反映出对于问题领域中的概念本质的理解、对于设计意图的反映是否正确、合理。我们一一来分析、说明。 如果两个概念都使用interface方式来定义,那么就反映出两个问题:1、我们可能没有理解清楚问题领域,AlarmDoor在概念本质上到底是Door还是报警器?2、如果我们对于问题领域的理解没有问题,比如:我们通过对于问题领域的分析发现AlarmDoor在概念本质上和Door是一致的,那么我们在实现时就没有能够正确的揭示我们的设计意图,因为在这两个概念的定义上(均使用interface方式定义)反映不出上述含义。 如果我们对于问题领域的理解是:AlarmDoor在概念本质上是Door,同时它有具有报警的功能。我们该如何来设计、实现来明确的反映出我们的意思呢?前面已经说过,abstractclass在Java语言中表示一种继承关系,而继承关系在本质上是"isa"关系。所以对于Door这个概念,我们应该使用abstarctclass方式来定义。另外,AlarmDoor又具有报警功能,说明它又能够完成报警概念中定义的行为,所以报警概念可以通过interface方式定义。如下所示: abstractclassDoor{ abstractvoidopen(); abstractvoidclose(); } interfaceAlarm{ voidalarm(); } classAlarmDoorextendsDoorimplementsAlarm{ voidopen(){…} voidclose(){…} voidalarm(){…} } 这种实现方式基本上能够明确的反映出我们对于问题领域的理解,正确的揭示我们的设计意图。其实abstractclass表示的是"isa"关系,interface表示的是"likea"关系,大家在选择时可以作为一个依据,当然这是建立在对问题领域的理解上的,比如:如果我们认为AlarmDoor在概念本质上是报警器,同时又具有Door的功能,那么上述的定义方式就要反过来了。 | |
好多同志对 iframe 是如何控制的,并不是十分了解,基本上还处于一个模糊的认识状态.
注意两个事项,ifr 是一个以存在的 iframe 的 ID 和 NAME 值:  document.getElementById(“ifr”);  window.frames[“ifr”];
要想使用iframe内的函数,变量就必须通过第二种方法.因为它取的是一个完整的DOM模型(不知道这样说对不对).第一种方法只是取出了一个OBJECT而已.
如果只想改变iframe的 src 或者 border , scrolling 等 attributes(与property不是一个概念,property是不能写在标签内的,比如:scrollHeight,innerHTML等),就需要用到第一种方法.
如果想取得iframe的页面(不是iframe本身),就需要使用第二种方法,因为它取得的是一个完整的DOM模型,比如想得到iframe的document.body的内容,就只能用第二种方法.
还要注意的是,如果在iframe的页面未完全装入的时候,调用iframe的DOM模型,会发生很严重的错误,所以,你要准备一个容错模式.
下面是示
网页头部标签meta详解
关键词: 收集meta资料
网页头部标签meta详解 网页头部有两个标签titel和meta, title比较简单,就是网页标题,meta的内容还是蛮多了,为了自己今后能够比较好的参考,我收集了些资料,也希望对大家有用。 一、meta标签的组成 meta标签共有两个属性,它们分别是http-equiv属性和name属性,不同的属性又有不同的参数值,这些不同的参数值就实现了不同的网页功能。 1、name属性 name属性主要用于描述网页,与之对应的属性值为content,content中的内容主要是便于搜索引擎机器人查找信息和分类信息用的。meat标签的name属性语法格式是:<meta name="参数" content="具体的参数值"> 。 其中name属性主要有以下几种参数: A、Keywords(关键字) 说明:keywords用来告诉搜索引擎你网页的关键字是什么。 举例:<meta name ="keywords" content="science, education,culture,politics,ecnomics,relationships, entertaiment, human"> B、description(网站内容描述) 说明:description用来告诉搜索引擎你的网站主要内容。 举例:<meta name="description" content="This page is about the meaning of science, education,culture."> C、robots(机器人向导) 说明:robots用来告诉搜索机器人哪些页面需要索引,哪些页面不需要索引。 content的参数有all,none,index,noindex,follow,nofollow。默认是all。 举例:<meta name="robots" content="none"> D、author(作者) 说明:标注网页的作者 举例:<meta name="author" content="***@yahoo.com.con"> 2、http-equiv属性 http-equiv顾名思义,相当于http的文件头作用,它可以向浏览器传回一些有用的信息,以帮助正确和精确地显示网页内容,与之对应的属性值为content,content中的内容其实就是各个参数的变量值。 meat标签的http-equiv属性语法格式是:<meta http-equiv="参数" content="参数变量值"> ;其中http-equiv属性主要有以下几种参数: A、Expires(期限) 说明:可以用于设定网页的到期时间。一旦网页过期,必须到服务器上重新传输。 用法:<meta http-equiv="expires" content="Fri, 12 Jan 2001 18:18:18 GMT"> 注意:必须使用GMT的时间格式。 B、Pragma(cache模式) 说明:禁止浏览器从本地计算机的缓存中访问页面内容。 用法:<meta http-equiv="Pragma" content="no-cache"> 注意:这样设定,访问者将无法脱机浏览。 C、Refresh(刷新) 说明:自动刷新并指向新页面。 用法:<meta http-equiv="Refresh" content="2;URL=http://phigo.bokee.com"> 注意:其中的2是指停留2秒钟后自动刷新到URL网址。 D、Set-Cookie(cookie设定) 说明:如果网页过期,那么存盘的cookie将被删除。 用法:<meta http-equiv="Set-Cookie" content="cookievalue=xxx; expires=Friday, 8-Aug -2008 18:18:18 GMT; path=/"> 注意:必须使用GMT的时间格式。 E、Window-target(显示窗口的设定) 说明:强制页面在当前窗口以独立页面显示。 用法:<meta http-equiv="Window-target" content="_top"> 注意:用来防止别人在框架里调用自己的页面。 F、content-Type(显示字符集的设定) 说明:设定页面使用的字符集。 用法:<meta http-equiv="content-Type" content="text/html; charset=gb2312"> G、Page-Exit /Page-Enter(进入页面、离开页面动画效果) 使用meta标签,我们还可以在进入网页或者离开网页的一刹那实现动画效果,我们只要在页面的html代码中的<head></head>标签之间添加如下代码就可以了: <meta http-equiv="Page-Enter" content="revealTrans(duration=5, transition=23)"> <meta http-equiv="Page-Exit" content="revealTrans(duration=5, transition=23)"> 一旦上述代码被加到一个网页中后,我们再进出页面时就会看到一些特殊效果,这个功能其实与FrontPage2000中的Format/Page Transition一样,但我们要注意的是所加网页不能是一个Frame页;Duration的值为网页动态过渡的时间,单位为秒。Transition是过渡方式,它的值为0到23,分别对应24种过渡方式。如下表: 0 盒状收缩 | 1 盒状放射 | 2 圆形收缩 | 3 圆形放射 | 4 由下往上 | 5 由上往下 | 6 从左至右 | 7 从右至左 | 8 垂直百叶窗 | 9 水平百叶窗 | 10 水平格状百叶窗 | 11垂直格状百叶窗 | 12 随意溶解 | 13从左右两端向中间展开 | 14从中间向左右两端展开 | 15从上下两端向中间展开 | 16从中间向上下两端展开 | 17 从右上角向左下角展开 | 18 从右下角向左上角展开 | 19 从左上角向右下角展开 | 20 从左下角向右上角展开 | 21 水平线状展开 | 22 垂直线状展开 | 23 随机产生一种过渡方式 |
|