|
|
摘要
泛型是J2SE 5.0最重要的特性。他们让你写一个type(类或接口)和创建一个实例通过传递一个或多个引用类型。这个实例受限于只能作用于这些类型。比如,在java 5,java.util.List 已经被泛化。当建立一个list对象时,你通过传递一个java类型建立一个List实例,此list实例只能作用于所传递的类型。这意味着如果你传递一个String ,此List实例只能拥有String对象;如果你传递一个Integer,此实例只能存贮Integer对象。除了创建参数化的类型,你还能创建参数化的函数。
泛型的第一个好处是编译时的严格类型检查。这是集合框架最重要的特点。此外,泛型消除了绝大多数的类型转换。在JDK 5.0之前,当你使用集合框架时,你不得不进行类型转换。
本文将教你如何操作泛型。它的第一部分是“没有泛型的日子”,先让我们回忆老版本JDK的不便。然后,举一些泛型的例子。在讨论完语法以及有界泛型的使用之后,文章最后一章将解释如何写泛型。
没有泛型的日子
所有的java类都源自java.lang.Object,这意味着所有的JAVA对象能转换成Object。因此,在之前的JDK的版本中,很多集合框架的函数接受一个Object参数。所以,collections是一个能持有任何对象的多用途工具,但带来了不良的后果。
举个简单的例子,在JDK 5.0的之前版本中,类List的函数add接受一个Object参数:
public boolean add(java.lang.Object element)
所以你能传递任何类型给add。这是故意这么设计的。否则,它只能传递某种特定的对象,这样就会出现各种List类型,如,StringList, EmployeeList, AddressList等。
add通过Object传递能带来好处,现在我们考虑get函数(返回List中的一个元素).如下是JDK 5之前版本的定义:
public java.lang.Object get(int index) throws IndexOutOfBoundsException
get返回一个Object.不幸的事情从此开始了.假如你储存了两个String对象在一个List中:
List stringList1 = new ArrayList();stringList1.add("Java 5");stringList1.add("with generics");
当你想从stringList1取得一个元素时,你得到了一个Object.为了操作原来的类型元素,你不得不把它转换为String。
String s1 = (String) stringList1.get(0);
但是,假如你曾经把一个非String对象加入stringList1中,上面的代码会抛出一个ClassCastException. 有了泛型,你能创建一个单一用途的List实例.比如,你能创建一个只接受String对象的List实例,另外一个实例只能接受Employee对象.这同样适用于集合框架中的其他类型.
泛型入门
像一个函数能接受参数一样,一个泛型也能接受参数.这就是一个泛型经常被称为一个参数化类型的原因.但是不像函数用()传递参数,泛型是用<>传递参数的.声明一个泛型和声明一个普通类没有什么区别,只不过你把泛型的变量放在<>中.
比如,在JDK 5中,你可以这样声明一个java.util.List : List<E> myList;
E 称为类型变量.意味着一个变量将被一个类型替代.替代类型变量的值将被当作参数或返回类型.对于List接口来说,当一个实例被创建以后,E 将被当作一个add或别的函数的参数.E 也会使get或别的参数的返回值.下面是add和get的定义:
boolean add<E o>E get(int index)
NOTE:一个泛型在声明或例示时允许你传递特定的类型变量: E.除此之外,如果E是个类,你可以传递子类;如果E是个接口,你可以传递实现接口的类;
-----------------------------译者添加--------------------
List<Number> numberList= new ArrayList<Number>();
numberList.add(2.0);
numberList.add(2);
-----------------------------译者添加--------------------
如果你传递一个String给一个List,比如:
List<String> myList;
那么mylist的add函数将接受一个String作为他的参数,而get函数将返回一个String.因为返回了一个特定的类型,所以不用类型转化了。
NOTE:根据惯例,我们使用一个唯一的大写字目表示一个类型变量。为了创建一个泛型,你需在声明时传递同样的参数列表。比如,你要想创建一个ArrayList来操作String ,你必须把String放在<>中。如:
List<String> myList = new ArrayList<String>();
再比如,java.util.Map 是这么定义的:
public interface Map<K,V>
K用来声明map键(KEY)的类型而V用来表示值(VALUE)的类型。put和values是这么定义的:
V put(K key, V value)Collection<V> values()
NOTE:一个泛型不准直接的或间接的是java.lang.Throwable的子类。因为异常是在运行时抛出的,所以它不可能预言什么类型的异常将在编译时抛出.
列表1的例子将比较List在JDK 1.4 和JDK1.5的不同
package com.brainysoftware.jdk5.app16;
import java.util.List;
import java.util.ArrayList;
public class GenericListTest {
public static void main(String[] args) {
// in JDK 1.4
List stringList1 = new ArrayList();
stringList1.add("Java 1.0 - 5.0");
stringList1.add("without generics");
// cast to java.lang.String
String s1 = (String) stringList1.get(0);
System.out.println(s1.toUpperCase());
// now with generics in JDK 5
List<String> stringList2 = new ArrayList<String>(); stringList2.add("Java 5.0"); stringList2.add("with generics"); // no need for type casting String s2 = stringList2.get(0); System.out.println(s2.toUpperCase()); }}
在列表1中,stringList2是个泛型。声明List<String>告诉编译器List的实例能接受一个String对象。当然,在另外的情况中,你能新建能接受各种对象的List实例。注意,当从List实例中返回成员元素时,不需要对象转化,因为他返回的了你想要的类型,也就是String.
NOTE:泛型的类型检查(type checking)是在编译时完成的.
最让人感兴趣的事情是,一个泛型是个类型并且能被当作一个类型变量。比如,你想你的List储存lists of Strings,你能通过把List<String>作为他的类型变量来声明List。比如:
List<List<String>> myListOfListsOfStrings;
要从myList中的第一个List重新取得String,你可以这么用:
String s = myListOfListsOfStrings.get(0).get(0);
下一个列表中的ListOfListsTest类示范了一个List(命名为listOfLists)接受一个String List作为参数。
package com.brainysoftware.jdk5.app16;import java.util.ArrayList;import java.util.List;public class ListOfListsTest { public static void main(String[] args) { List<String> listOfStrings = new ArrayList<String>(); listOfStrings.add("Hello again"); List<List<String>> listOfLists = new ArrayList<List<String>>(); listOfLists.add(listOfStrings); String s = listOfLists.get(0).get(0); System.out.println(s); // prints "Hello again" }}
另外,一个泛型接受一个或多个类型变量。比如,java.util.Map有两个类型变量s。第一个定义了键(key)的类型,第二个定义了值(value)的类型。下面的例子讲教我们如何使用个一个泛型Map.
package com.brainysoftware.jdk5.app16;import java.util.HashMap;import java.util.Map;public class MapTest { public static void main(String[] args) { Map<String, String> map = new HashMap<String, String>(); map.put("key1", "value1"); map.put("key2", "value2"); String value1 = map.get("key1"); }}
在这个例子中,重新得到一个key1代表的String值,我们不需要任何类型转换。
没有参数的情况下使用泛型
既然在J2SE 5.0中收集类型已经泛型化,那么,原来的使用这些类型的代码将如何呢?很幸运,他们在JAVA 5中将继续工作,因为你能使用没有参数的泛型。比如,你能继续像原来一样使用List接口,正如下面的例子一样。
List stringList1 = new ArrayList();stringList1.add("Java 1.0 - 5.0");stringList1.add("without generics");String s1 = (String) stringList1.get(0);
一个没有任何参数的泛型被称为原型(raw type)。它意味着这些为JDK1.4或更早的版本而写的代码将继续在java 5中工作。
尽管如此,一个需要注意的事情是,JDK5编译器希望你使用带参数的泛型。否则,编译器将提示警告,因为他认为你可能忘了定义类型变量s。比如,编译上面的代码的时候你会看到下面这些警告,因为第一个List被认为是原型。
Note: com/brainysoftware/jdk5/app16/GenericListTest.java
uses unchecked or unsafe operations.
Note: Recompile with -Xlint:unchecked for details.
当你使用原型时,如果你不想看到这些警告,你有几个选择来达到目的:
1.编译时带上参数-source 1.4
2.使用@SupressWarnings("unchecked")注释
3.更新你的代码,使用List<Object>. List<Object>的实例能接受任何类型的对象,就像是一个原型List。然而,编译器不会报错。
使用 ? 通配符
前面提过,如果你声明了一个List<aType>, 那么这个List对aType起作用,所以你能储存下面这些类型的对象:
1.一个aType的实例
2.它的子类的实例(如果aType是个类)
3.实现aType接口的类实例(如果aType是个接口)
但是,请注意,一个泛型本身是个JAVA类型,就像java.lang.String或java.io.File一样。传递不同的类型变量给泛型可以创建不同的JAVA类型。比如,下面例子中list1和list2引用了不同的类型对象。
List<Object> list1 = new ArrayList<Object>();List<String> list2 = new ArrayList<String>();
list1指向了一个类型变量s为java.lang.Objects 的List而list2指向了一个类型变量s为String 的List。所以传递一个List<String>给一个参数为List<Object>的函数将导致compile time错误。下面列表可以说明:
package com.brainysoftware.jdk5.app16;import java.util.ArrayList;import java.util.List; public class AllowedTypeTest { public static void doIt(List<Object> l) { } public static void main(String[] args) { List<String> myList = new ArrayList<String>(); // 这里将产生一个错误 doIt(myList); }}
上面的代码无法编译,因为你试图传递一个错误的类型给函数doIt。doIt的参数是List<Object>二你传递的参数是List<String>。
可以使用 ? 通配符解决这个难题。List<?> 意味着一个对任何对象起作用的List。所以,doIt可以改为:
public static void doIt(List<?> l) {}
在某些情况下你会考虑使用 ? 通配符。比如,你有一个printList函数,这个函数打印一个List的所有成员,你想让这个函数对任何类型的List起作用时。否则,你只能累死累活的写很多printList的重载函数。下面的列表引用了使用 ? 通配符的printList函数。
package com.brainysoftware.jdk5.app16;import java.util.ArrayList;import java.util.List; public class WildCardTest { public static void printList(List<?> list) { for (Object element : list) { System.out.println(element); } } public static void main(String[] args) { List<String> list1 = new ArrayList<String>(); list1.add("Hello"); list1.add("World"); printList(list1); List<Integer> list2 = new ArrayList<Integer>(); list2.add(100); list2.add(200); printList(list2); }}
这些代码说明了在printList函数中,List<?>表示各种类型的List对象。然而,请注意,在声明的时候使用 ? 通配符是不合法的,像这样:
List<?> myList = new ArrayList<?>(); // 不合法
如果你想创建一个接收任何类型对象的List,你可以使用Object作为类型变量,就像这样:
List<Object> myList = new ArrayList<Object>();
在函数中使用界限通配符
在之前的章节中,你学会了通过传递不同的类型变量s来创建不同JAVA类型的泛型,但并不考虑类型变量s之间的继承关系。在很多情况下,你想一个函数有不同的List参数。比如,你有一个函数getAverage,他返回了一个List中成员的平均值。然而,如果你把List<Number>作为getAverage的参数,你就没法传递List<Integer> 或List<Double>参数,因为List<Number>和List<Integer> 和List<Double>不是同样的类型。
你能使用原型或使用通配符,但这样无法在编译时进行安全类型检查,因为你能传递任何类型的List,比如List<String>的实例。你可以使用List<Number>作为参数,但是你就只能传递List<Number>给函数。但这样就使你的函数功能减少,因为你可能更多的时候要操作List<Integer>或List<Long>,而不是List<Number>。
J2SE5.0增加了一个规则来解决了这种约束,这个规则就是允许你定义一个上界(upper bound) 类型变量.在这种方式中,你能传递一个类型或它的子类。在上面getAverage函数的例子中,你能传递一个List<Number>或它的子类的实例,比如List<Integer> or List<Float>。
使用上界规则的语法这么定义的:GenericType<? extends upperBoundType>. 比如,对getAverage函数的参数,你可以这么写List<? extends Number>. 下面例子说明了如何使用这种规则。
package com.brainysoftware.jdk5.app16;import java.util.ArrayList;import java.util.List;public class BoundedWildcardTest { public static double getAverage(List<? extends Number> numberList) { double total = 0.0; for (Number number : numberList) total += number.doubleValue(); return total/numberList.size(); } public static void main(String[] args) { List<Integer> integerList = new ArrayList<Integer>(); integerList.add(3); integerList.add(30); integerList.add(300); System.out.println(getAverage(integerList)); // 111.0 List<Double> doubleList = new ArrayList<Double>(); doubleList.add(3.0); doubleList.add(33.0); System.out.println(getAverage(doubleList)); // 18.0 }}
由于有了上界规则,上面例子中的getAverage函数允许你传递一个List<Number> 或一个类型变量是任何java.lang.Number子类的List。
下界规则
关键字extends定义了一个类型变量的上界。通过使用super关键字,我们可以定义一个类型变量的下界,尽管使用的情况不多。比如,如果一个函数的参数是List<? super Integer>,那么意味着你可以传递一个List<Integer>的实例或者任何java.lang.Integer的超类(superclass)。
创建泛型
前面的章节主要说明了如何使使用泛型,特别是集合框架中的类。现在我们开始学习如何写自己的泛型。
基本上,除了声明一些你想要使用的类型变量s外,一个泛型和别的类没有什么区别。这些类型变量s位于类型后面的<>中。比如,下面的Point就是个泛型。一个Point对象代表了一个系统中的点,它有横坐标和纵坐标。通过使Point泛型化,你能定义一个点实例的精确程度。比如,如果一个Point对象需要非常精确,你就把Double作为类型变量。否则,Integer 就够了。
package com.brainysoftware.jdk5.app16;public class Point<T> { T x; T y; public Point(T x, T y) { this.x = x; this.y = y; } public T getX() { return x; } public T getY() { return y; } public void setX(T x) { this.x = x; } public void setY(T y) { this.y = y; }}
在这个例子中,T是Point的类型变量 。T是getX和getY的返回值类型,也是setX和setY的参数类型。此外,构造函数结合两个T参数。
使用point类就像使用别的类一样。比如,下面的例子创建了两个Point对象:ponint1和point2。前者把Integer作为类型变量,而后者把Double作为类型变量。
Point<Integer> point1 = new Point<Integer>(4, 2);point1.setX(7);Point<Double> point2 = new Point<Double>(1.3, 2.6);point2.setX(109.91);
总结
泛型使代码在编译时有了更严格的类型检查。特别是在集合框架中,泛型有两个作用。第一,他们增加了对集合类型在编译时的类型检查,所以集合类所能持有的类型对传递给它的参数类型起了限制作用。比如你创建了一个持有strings的java.util.List实例,那么他就将不能接受Integers或别的类型。其次,当你从一个集合中取得一个元素时,泛型消除了类型转换的必要。
泛型能够在没有类型变量的情况下使用,比如,作为原型。这些措施让Java 5之前的代码能够运行在JRE 5中。但是,对新的应用程序,你最好不要使用原型,因为以后Java可能不支持他们。
你已经知道通过传递不同类型的类型变量给泛型可以产生不同的JAVA类型。就是说List<String>和List<Object>的类型是不同的。尽管String是java.lang.Object。但是传递一个List<String>给一个参数是List<Object>的函数会参数会产生编译错误(compile error)。函数能用 ? 通配符使其接受任何类型的参数。List<?> 意味着任何类型的对象。
最后,你已经看到了写一个泛型和别的一般JAVA类没有什么区别。你只需要在类型名称后面的<>中声明一系列的类型变量s就行了。这些类型变量s就是返回值类型或者参数类型。根据惯例,一个类型变量用一个大写字母表示。
Budi Kurniawan是一个高级 J2EE 架构师和作家。
|
Linux下jdk1.5的安装及中文显示问题解决方案
本文未经本人许可切勿转载!
作者:terry
个人主页:http://infected.533.net
电子邮件:imterry@gmail.com
文档版本:v1.0 2004-10-30
图文版本:http://infected.html.533.net/download/linux.swf
操作系统:Fedora Core 2 (Kernel 2.6.7)
jdk版本:jdk 1.5.0
摘要:随着Sun发布J2SE 5.0,即jdk1.5,众多新特性的加入和JVM性能的改善让人心动。Windows下安装和环境设置以及中文显示自然没什么好说的,简直可以说是傻瓜式的。但是Linux下就不会那么顺利了,尤其是中文字体的显示一直以来总是困扰着初级用户。Jdk 1.5.0的中文显示设置和j2sdk 1.4.x时相比已经有了很大的改变。本文着重介绍jdk1.5在Linux操作系统下的安装、环境变量设置和中文显示问题的解决。
1. 首先,从sun下载jdk1.5的安装文件,一般有两种。
下载地址:http://java.sun.com/j2se/1.5.0/download.jsp
Linux RPM in self-extracting file (jdk-1_5_0-linux-i586-rpm.bin, 42.50 MB)
Linux self-extracting file (jdk-1_5_0-linux-i586.bin, 43.95 MB)
简单说一下两个安装文件的区别,Linux RPM in self-extracting file是一个把rpm安装包封装在压缩包内的二进制格式,在linux操作系统的命令行下,给文件加上执行的权限chmod a+x jdk-1_5_0-linux-i586-rpm.bin,然后./jdk-1_5_0-linux-i586-rpm.bin之后可以得到jdk-1_5_0-linux-i586.rpm这个rpm包,之后安装rpm包还用多说么?rpm ?ivh package-name。前者的好处是容易安装和维护,傻瓜式的。而后者则适合高级的用户,相当于一个zip版本的“绿色”jdk,没有安装程序,安装完之后可能需要做一些链接(具体可以参考jdk安装指南文档)。同样地给后者加上执行权限之后直接执行二进制文件即可得到jdk 1.5.0,一般用户的习惯都是将jdk放在/usr/java下,rpm安装后默认是在/usr/java下。
2. 安装好之后要做的便是环境变量的设置,也可分为两种不同的情况。
第一种是对于单个用户的的环境变量设置。需要修改用户所对应的.bash_profile文件。我们可以通过vi ~/.bash_profile来编辑当前用户的bash_profile文件,当然你也可以用你自己喜欢的编辑器。在开头或者适当的地方插入一下代码:
export JAVA_HOME=/usr/java/jdk1.5.0
export CLASSPATH=.
export PATH=$JAVA_HOME/bin:$PATH
然后保存文件,注销重新登陆之后在控制台下输入java ?version,如果出现一下信息则说明安装设置已经成功。
注意:此代码是为了看起来简单,所以没有加上复杂的变量引用,熟悉shell的用户可以参 照第二种方法的代码进行修改。
java version "1.5.0"
Java(TM) 2 Runtime Environment, Standard Edition (build 1.5.0-b64)
Java HotSpot(TM) Client VM (build 1.5.0-b64, mixed mode, sharing)
第二种是设置Linux的全局环境变量(需要root权限),修改/etc/profile,在适当的地方加入以下代码:
JAVA_HOME=/usr/java/jdk1.5.0
PATH=$JAVA_HOME=$JAVA_HOME/bin:$PATH
CLASSPATH=.:$JAVA_HOME/lib/tools.jar:$JAVA_HOME/lib/dt.jar
export PATH JAVA_HOME CLASSPATH
修改完成后保存,注销当前用户重新登陆后按照第一种同样的方法进行测试查看是否成功。
至此,基本的安装和环境变量设置已经完成了。
至于正确显示applet的浏览器的插件安装请查看Sun的文档:
http://java.sun.com/j2se/1.5.0/install-linux.html#plugin
http://java.sun.com/j2se/1.5.0/manual_install_linux.html
接下来要说如何修改配置来正确显示中文字体。
3. 最后来解决一下中文字体的显示问题。Jdk1.5.0的字体配置和1.4.x的时候相比有了很大的差别。先来看一下不作任何修改的情况下ControlPanel中文字体的显示情况,如图:
虽然能显示一下中文,但是有一大半是无法正常显示的方块,很不爽。下面我们就来解决这个问题。
先来看一下Sun公司对JRE1.5.0版的中文字体设置的官方说明中的一些资料:
介绍一下其字体配置文件的加载方案。其加载顺序为(JavaHome指JRE的根目录,下同):JavaHome/lib/fontconfig.OS.Version.properties
JavaHome/lib/fontconfig.OS.Version.bfc
JavaHome/lib/fontconfig.OS.properties
JavaHome/lib/fontconfig.OS.bfc
JavaHome/lib/fontconfig.Version.properties
JavaHome/lib/fontconfig.Version.bfc
JavaHome/lib/fontconfig.properties
JavaHome/lib/fontconfig.bfc
其中,OS字段可以是:
Windows: "98", "2000", "Me", "XP", "2003"。
Solaris:空。
Linux: "Sun", "RedHat", "Turbo", "SuSE"。
而Version字段指该OS的版本号。
在这些配置文件中,仅.properties文件是文本文件。其格式跟JRE1.4.2版的不同。文件分为几段,并在每段开始时标明。
我们需要关注的仅有两段:Component Font Mappings和Search Sequences。前者指定字体,后者指定要使用的properties的搜索顺序。
知道了这些,配置起来就简单了。
看了是不是有点晕,而且觉得没用?不要急下面会用到的。
先把字体文件Simsun.ttf(可以从windows下拷贝一份到linux下)拷贝到JRE的字体目录$JAVA_HOME/jre/lib/fonts下,接着编辑字体目录下的文件fonts.dir,把第一行的数字加1,然后在最后加一行加上:
simsun.ttf -SungtiL GB-medium-r-normal--0-0-0-0-c-0-gb2312.1988-0
然后保存文件。
最后把字体配置文件改名为fontconfig.RedHat.properties这种形式(Fedora Core x也是Redhat的衍生物),把$JAVA_HOME/jre/lib下多余的fontconfig开头的文件全部移到新建的bak目录下,当然你不备份也可以只是除了问题不要找我^_^
注意:如果你的Linux不是Redhat和Fedora Core系列,请按照自己的发行版本对字体配置文件做相应的修改,参照Sun公司的资料,jds、Suse和Turbo应该没问题了,至于Debian和Mandrake我就不得而知了,如果大家按照自己的方式修改成功了请告诉我详细情况以便于我完善这个文档。
然后再在控制台下执行ControlPanel看看,是不是很爽!如图:
还有自己的程序也能完美的显示中文字体了!
4. 参考文献
http://addone.blogchina.com/blog/article_17203.216643.html
Sun公司对JRE1.5.0版的中文字体设置的官方说明:
http://java.sun.com/j2se/1.5.0/docs/guide/intl/fontconfig.html
|
可以使用标准窗口小部件工具箱(Standard Widget Toolkit,SWT)和 JFace 库来开发用于 Eclipse 环境的图形用户界面,而且还可以将它们用于开发单独的 GUI 本机应用程序。在本文中,我将介绍一些基本的 SWT(基本 GUI 对象的名称)类型,并展示如何综合使用它们来创建有用的应用程序。
关于 Eclipse、SWT 和 JFace
正如 Eclipse 的 Web 站点上所提到的,Eclipse 是一种通用工具平台。它是一个开放的、可用于任何东西的可扩展 IDE,没什么特别之处,它为工具开发人员提供了灵活性以及对软件技术的控制。
Eclipse 为开发人员提供了生产大量 GUI 驱动的工具和应用程序的基础。而这项功能的基础就是 GUI 库 SWT 和 JFace。
SWT 是一个库,它创建了Java 版的本地主机操作系统 GUI 控件。它依赖于本机实现。这意味着基于 SWT 的应用程序具有以下几个关键特性:
它们的外观、行为和执行类似于“本机”应用程序。
所提供的窗口小部件(widget)反映了主机操作系统上提供的窗口小部件(组件和控件)。
主机 GUI 库的任何特殊行为都在 SWT GUI 中得到反映。
这些目标使得 SWT 不同于 Java 技术的 Swing,Swing 的设计目标是消除操作系统的差异。
SWT 库反映了主机操作系统的基本窗口小部件。在许多环境下,这种方法太低级。JFace 库有助于向 SWT 应用程序中添加大量服务。JFace 并没有隐藏 SWT,它只是扩展了 SWT。正如您将在这一系列的后面部分中看到的,SWT 最重要的扩展之一是,将应用程序的数据模型与显示及更改它的 GUI 隔离开来。
在开始之前,我需要介绍一些 SWT 术语:
Widget —— 基本的 SWT GUI 组件(类似于 Java AWT 中的 Component 和 Swing 中的 JComponent)。Widget 是一个抽象类。
Control —— 拥有操作系统的对等物的窗口小部件(换句话说,在操作系统中具有同一身份)。Control 是一个抽象类。
Composite —— 包含其他控件的控件(类似于 Java AWT 中的 Container 和 Swing 中的 JPanel)。
Item —— 其他控件包含的窗口小部件(该控件可能不是复合控件),比如列表和表。注意,包含一些项的控件很少包含其他控件,反之亦然。Item 是一个抽象类。
注意,Eclipse 具有跨平台特性(因此可以在许多操作平台上运行),本文基于 Eclipse 的 Microsoft? Windows? 版本。因此,本文包含的每个例子都应该能够不加任何更改地在其他平台上使用。还要注意的是,本文是基于 Eclipse V3.0 的。Eclipse V3.1 中添加了少许 GUI 窗口小部件类型和特性。
基本控件
几乎所有 SWT GUI 都是从某些基础部分开始创建的。所有 SWT 窗口小部件都可以在 org.eclipse.swt.widget 或 org.eclipse.swt.custom 包中找到。(一些 Eclipse 插件还在其他包中提供了定制的窗口小部件。)窗口小部件包中包含一些基于操作系统控件的控件,而定制包中则包含一些超出操作系统控件集之外的控件。一些定制的软件包控件类似于窗口小部件包中的控件。为了避免命名冲突,定制控件的名称都是以“C”开始的(例如,比较 CLabel 与 Label)。
在 SWT 中,所有控件(除了一些高级控件,比如 shell,将在后面进行讨论)在创建的时候都必须有一个父控件(一个复合实例)。在创建的时候,这些控件被自动“添加”到父控件中,这与必须明确添加到父控件中的 AWT/Swing 中的控件有所不同,自动添加产生了一种“自上而下”地构造 GUI 的方法。这样,所有控件都可以采用一个复合父控件(或者一个子类)作为构造函数的参数。
大多数控件都有一些必须在创建时设置的标记选项。因此,大多数控件还有另外一个构造函数参数,我们通常称之为样式,该参数提供了设置这些选项的标记。所有这些参数值都是 static final int,并且都是在 org.eclipse.swt 包的 SWT 类中定义的。如果不需要任何参数,则可以使用 SWT.NONE 值。
标签
标签可能是最简单的控件,标签 被用于显示纯文本(没有颜色、特殊字体或样式的文本)或称为图标的小图像。标签不接受焦点(换句话说,用户不能通过 Tab 键或鼠标移动到标签),因此,标签无法产生输入事件。
清单 1 展示了如何创建一个简单的文本标签。
清单 1. 创建一个带文本的标签
import org.eclipse.swt.widget.*;
:
Composite parent = ...;
:
// create a center aligned label
Label label = new Label(parent, SWT.CENTER);
label.setText("This is the label text");
注意,该文本是采用不同于构造函数的单独的方法设置的。这是所有 SWT 控件的一个典型象征。只有父控件和样式是在构造函数中设置的,其他所有属性都是在已创建的对象上设置的。
由于平台的限制,标准标签控件不能同时拥有文本和图标。为了支持同时拥有文本和图标,可以使用 CLabel 控件,如清单 2 中所示。
清单 2. 创建一个包含文本和图像的标签
import com.eclipse.swt.graphics.*;
import org.eclipse.swt.widget.*;
import org.eclipse.swt.custom.*;
:
Composite parent = ...;
Image image = ...;
:
// create a left aligned label with an icon
CLabel Clabel = new CLabel(parent, SWT.LEFT);
label.setText("This is the imaged label text"");
label.setImage(image);
文本
在标签显示文本的同时,您时常还想允许用户插入文本。文本 控件就是用于此目的的。文本可以是单行的(一个文本字段),也可以是多行的(一个文本区域)。文本还可以是只读的。文本字段中没有描述,因此,常常通过标签控件处理它们,以确定它们的用途。文本控件还可以包含一个“工具提示”,提供关于控件用途的信息(所有控件都支持这一特性)。
清单 3 显示了如何使用允许使用的有限数量的特性来创建一个简单的文本字段。选择默认文本是为了便于擦除。
清单 3. 创建一个包含选定的默认文本和一个限制条件的文本
import org.eclipse.swt.widget.*;
:
Composite parent = ...;
:
// create a text field
Text name = new Text(parent, SWT.SINGLE);
name.setText("<none>");
name.setTextLimit(50);
name.setToolTipText("Enter your name -- Last, First");
name.selectAll(); // enable fast erase
按钮
通常,您希望用户指出应该何时进行某项操作。最常见的做法是使用按钮 控件。存在以下几种样式的按钮:
ARROW —— 显示为一个指向上、下、左、右方向的箭头。
CHECK —— 已标记的复选标记。
FLAT —— 没有凸起外观的按钮。
PUSH —— 瞬时按钮(最常见的事件源)。
RADIO —— 具有排他性的粘性标记(sticky mark),其他所有单选按钮都在相同的组中。
TOGGLE —— 一个粘性按钮。
清单 4 创建了一个“Clear”按钮:
import org.eclipse.swt.widget.*;
:
Composite parent = ...;
:
// create a push button
Button clear = new Button(parent, SWT.PUSH);
clear.setT名称中的 & 导致利用紧接着的一个字母创建一个加速器,允许通过 Ctrl+<字母> 顺序的方式按下按钮(控件顺序由主机操作系统决定)。 ext("Clea&r");
事件监听器
通常,您可能想在选择按钮(特别是某种推式按钮)的时候执行一些操作。您可以通过向该按钮添加一个 SelectionListener(在 org.eclipse.swt.events 包中)做到这一点。当按钮状态发生改变时(通常是按钮被按下),就会生成事件。清单 5 在单击 Clear 按钮时输出一条消息。
清单 5. 按钮事件处理程序
import org.eclipse.swt.events.*;
:
// Clear button pressed event handler
clear.addSelectionListener(new SelectionListener() {
public void widgetelected(selectionEvent e) {
System.out.println("Clear pressed!");
}
public void widgetDefaultSelected(selectionEvent e) {
widgetelected(e);
}
});
此代码使用了一个匿名的内部类,但您还可以使用指定的内部类或单独的类作为监听器。多数包含两个或更多方法的 ...Listener 类还有一个类似的 ...Adapter 类,这个类提供了一些空的方法实现,并且可以减少您需要编写的代码数量。例如,还有一个 SelectionAdapter 类,这个类实现了 SelectionListener。
注意,在这些方法中执行的操作必须快速完成(通常不足一秒时间),或者说 GUI 的反应将是迟钝的。更长时间的操作(比如访问文件)需要单独的线程,但那是以后某期文章的主题。
复合控件
至此,我们已经讨论了一些单独的控件。在多数 GUI 中,许多控件被组合在一起以提供丰富的用户体验。在 SWT 中,这种组合是通过 Composite 类实现的。
复合控件可以在任何级别上进行嵌套,并且可以混合和匹配控件,将它们作为子控件进行组合。这样做可以极大地减少 GUI 开发的复杂性,并为 GUI 代码重用(通过封装内部 GUI)创造了机会。复合控件可以是有边界的,并且这些边界很容易在视觉上产生混淆,或者它们也可以是无边界的,无缝集成到更大的组中。
清单 6. 创建一个有边界的复合控件。
单 6. 创建一个有边界的复合控件
import org.eclipse.swt.widget.*;
:
Composite parent = ...;
:
Composite border = new Composite(parent, SWT.BORDER);
除了边界之外,Group 复合子类还支持标题。在定义排他性按钮集合时,组通常被用来包含单选类型的按钮。
清单 7 创建了一个有边界的组。
清单 7. 创建一个有边界的组
import org.eclipse.swt.widget.*;
:
Composite parent = ...;
:
Group border = new Group(parent, SWT.SHADOW_OUT);
border.setText("Group Description");
shell
shell 是一种可能没有父复合控件的复合控件(框架或窗口);此外,它还有一个作为父控件的 Display,这通常也是默认设置。shell 有很多种样式,但最常见的样式是 SWT.SHELL_TRIM 或 SWT.DIALOG_TRIM。shell 可以是模态的,也可以是非模态的。模态 shell 常常用于对话框,防止父 GUI(如果有的话)在关闭子 shell 之前被处理。
清单 8 创建了一个框架样式的顶级非模态 shell。
清单 8. 创建一个顶级 shell
import org.eclipse.swt.widget.*;
:
Shell frame = new Shell(SWT.SHELL_TRIM);
:
shell 可以有子 shell。这些子 shell 是与父 shell 相关的独立桌面窗口(也就是说,如果父 shell 关闭,那么其所有子 shell 也将关闭)。
清单 9 创建了一个对话框样式的子 shell。
清单 9. 创建一个对话框 shell
hell dialog = new Shell(frame, SWT.DIALOG_TRIM);
布局管理器
复合控件常常包含多个控件。可以使用以下两种方法安排这些控件:
绝对定位 —— 为每个控件设置明确的 X 和 Y 位置,并通过代码设置一定的宽度和高度。
托管定位 —— 每个控件的 X、Y、宽度和高度都是通过LayoutManager 设置的。
在多数情况下,应该选择使用 LayoutManagers,因为很容易调整它们来适应可变大小的 GUI。SWT 也提供了一些布局管理器供您使用;在这一期的系列文章中,我们将讨论两种基本的布局管理器:FillLayout 和 GridLayout。在这两种情况下,每当重新设置复合控件的大小,都需要进行定位。
一些布局管理器常常是专为某一个复合控件分配的。一些布局管理器只使用它们自身的参数就可以控制,而另一些布局管理器还需要其他参数 —— LayoutData,该参数是在它们管理的复合控件中的每个控件上指定的。
FillLayout
FillLayout 以行或列的形式安排控件。每个控件所设置的大小将与填充该复合控件所需的宽度和高度相同,在这些控件之间,空间是平均分配的。一种特殊情况是:在仅有一个子控件时,该控件的大小被设置为填充整个父复合控件的大小。
清单 10. 使用 FillLayout 创建一列控件
import org.eclipse.swt.widget.*;
import org.eclipse.swt.layouts.*;
:
Composite composite = ...;
FillLayout fillLayout = new FillLayout(SWT.VERTICAL);
composite.setLayout(fillLayout);
GridLayout
GridLayout 提供了一个功能更强大的布局方法,该方法类似于使用 HTML 表的方法。它创建了 2-D 网格的单元格。可以将控件放置在一个或多个单元格中(可以称之为单元格跨越)。单元格的大小可以是相等的,或者是网格宽度或高度的某个给定可变百分比。可以将控件添加到某一行的下一个可用列中,如果这一行中没有更多的列,那么该控件将移动到下一行的第一列中。
清单 11 创建了一个复合控件,该控件有两行和两个列,其中包含两个已标记的文本字段。这些列可以有不同的宽度。
清单 11. 创建一个控件表
import org.eclipse.swt.widget.*;
import org.eclipse.swt.layouts.*;
:
Composite composite = ...;
GridLayout gridLayout = new GridLayout(2, false);
composite.setLayout(gridLayout);
Label l1 = new Label(composite, SWT.LEFT);
l1.settext("First Name: ");
Text first = new Text(composite, SWT.SINGLE);
Label l1 = new Label(composite, SWT.LEFT);
l2.setText("Last Name: ");
Text last = new Text(composite, SWT.SINGLE);
GridData
考虑一下这种情况:您需要指定每个控件如何使用其单元格中的剩余空间。为了给每个单元格提供这种精确控制,添加到 GridLayout 的托管复合控件的控件可以拥有 GridData 实例(LayoutData 的子类)。
清单 12 设置了这些文本字段,以便采用所有可用的剩余空间(根据前面的清单)。
清单 12. 配置一个扩展到所有可用空间的布局
first.setLayoutData(new GridData(SWT.FILL, SWT.FILL, true, true));
last.setLayoutData(new GridData(SWT.FILL, SWT.FILL, true, true));
构建一个运行程序
现在是时候来看一下我们已经在简单的可执行例子 Basic1 中讨论过的所有 SWT 控件了。请参阅 参考资料,以获得该应用程序的完整源代码。
SWT GUI 需要一个已配置好的环境来运行。这个环境是通过一个显示实例提供的,该实例提供了对主机操作系统显示设备的访问。这个显示实例允许您处理每个用户输入(鼠标或键盘)来处理您的 GUI。
清单 13 创建了一个环境和一个 GUI,并显示了这个 GUI。
清单 13. 创建一个 GUI 应用程序并启动它
import org.eclipse.swt.widget.*;
:
Display display = new Display();
Shell shell = new Shell(display);
shell.setText("Shell Title");
// *** construct Shell children here ***
shell.open(); // open shell for user access
// process all user input events
while(!shell.isDisposed()) {
// process the next event, wait when none available
if(!display.readAndDispatch()) {
display.sleep();
}
}
display.dispose(); // must always clean up
在 SWT 和 JFace 系列的第一期中,我们介绍了大多数基本 SWT 窗口小部件控件:标签、文本、按钮、复合控件和 shell。这些控件,与显示类(display class)相结合,允许创建全功能的 GUI。
|
Java跨平台的特性使Java越来越受开发人员的欢迎,但也往往会听到不少的抱怨:用Java开发的图形用户窗口界面每次在启动的时候都会跳出一个控制台窗口,这个控制台窗口让本来非常棒的界面失色不少。怎么能够让通过Java开发的GUI程序不弹出Java的控制台窗口呢?其实现在很多流行的开发环境例如JBuilder、Eclipse都是使用纯Java开发的集成环境。这些集成环境启动的时候并不会打开一个命令窗口,因为它使用了JNI(Java Native Interface)的技术。通过这种技术,开发人员不一定要用命令行来启动Java程序,可以通过编写一个本地GUI程序直接启动Java程序,这样就可避免另外打开一个命令窗口,让开发的Java程序更加专业。
JNI允许运行在虚拟机的Java程序能够与其它语言(例如C和C++)编写的程序或者类库进行相互间的调用。同时JNI提供的一整套的API,允许将Java虚拟机直接嵌入到本地的应用程序中。
本文将介绍如何在C/C++中调用Java方法,并结合可能涉及到的问题介绍整个开发的步骤及可能遇到的难题和解决方法。本文所采用的工具是Sun公司创建的 Java Development Kit (JDK) 版本 1.3.1,以及微软公司的Visual C++ 6开发环境。
环境搭建
为了让本文以下部分的代码能够正常工作,我们必须建立一个完整的开发环境。首先需要下载并安装JDK 1.3.1,其下载地址为“http://java.sun.com”。假设安装路径为C:\JDK。下一步就是设置集成开发环境,通过Visual C++ 6的菜单Tools→Options打开选项对话框。
将目录C:\JDK\include和C:\JDK\include\win32加入到开发环境的Include Files目录中,同时将C:\JDK\lib目录添加到开发环境的Library Files目录中。这三个目录是JNI定义的一些常量、结构及方法的头文件和库文件。集成开发环境已经设置完毕,同时为了执行程序需要把Java虚拟机所用到的动态链接库所在的目录C:\JDK \jre\bin\classic设置到系统的Path环境变量中。这里需要提出的是,某些开发人员为了方便直接将JRE所用到的DLL文件直接拷贝到系统目录下。这样做是不行的,将导致初始化Java虚拟机环境失败(返回值-1),原因是Java虚拟机是以相对路径来寻找所用到的库文件和其它一些相关文件的。至此整个JNI的开发环境设置完毕,为了让此次JNI旅程能够顺利进行,还必须先准备一个Java类。在这个类中将用到Java中几乎所有有代表性的属性及方法,如静态方法与属性、数组、异常抛出与捕捉等。我们定义的Java程序(Demo.java)如下,本文中所有的代码演示都将基于该Java程序,代码如下:
package jni.test;
/**
* 该类是为了演示JNI如何访问各种对象属性等
* @author liudong
*/
public class Demo {
//用于演示如何访问静态的基本类型属性
public static int COUNT = 8;
//演示对象型属性
public String msg;
private int[] counts;
public Demo() {
this("缺省构造函数");
}
/**
* 演示如何访问构造器
*/
public Demo(String msg) {
System.out.println("<init>:" + msg);
this.msg = msg;
this.counts = null;
}
/**
* 该方法演示如何访问一个访问以及中文字符的处理
*/
public String getMessage() {
return msg;
}
/**
* 演示数组对象的访问
*/
public int[] getCounts() {
return counts;
}
/**
* 演示如何构造一个数组对象
*/
public void setCounts(int[] counts) {
this.counts = counts;
}
/**
* 演示异常的捕捉
*/
public void throwExcp() throws IllegalAccessException {
throw new IllegalAccessException("exception occur.");
}
}
初始化虚拟机
本地代码在调用Java方法之前必须先加载Java虚拟机,而后所有的Java程序都在虚拟机中执行。为了初始化Java虚拟机,JNI提供了一系列的接口函数Invocation API。通过这些API可以很方便地将虚拟机加载到内存中。创建虚拟机可以用函数 jint JNI_CreateJavaVM(JavaVM **pvm, void **penv, void *args)。对于这个函数有一点需要注意的是,在JDK 1.1中第三个参数总是指向一个结构JDK1_ 1InitArgs, 这个结构无法完全在所有版本的虚拟机中进行无缝移植。在JDK 1.2中已经使用了一个标准的初始化结构JavaVMInitArgs来替代JDK1_1InitArgs。下面我们分别给出两种不同版本的示例代码。
在JDK 1.1初始化虚拟机:
#include <jni.h>
int main() {
JNIEnv *env;
JavaVM *jvm;
JDK1_1InitArgs vm_args;
jint res;
/* IMPORTANT: 版本号设置一定不能漏 */
vm_args.version = 0x00010001;
/*获取缺省的虚拟机初始化参数*/
JNI_GetDefaultJavaVMInitArgs(&vm_args);
/* 添加自定义的类路径 */
sprintf(classpath, "%s%c%s",
vm_args.classpath, PATH_SEPARATOR, USER_CLASSPATH);
vm_args.classpath = classpath;
/*设置一些其他的初始化参数*/
/* 创建虚拟机 */
res = JNI_CreateJavaVM(&jvm,&env,&vm_args);
if (res < 0) {
fprintf(stderr, "Can't create Java VM\n");
exit(1);
}
/*释放虚拟机资源*/
(*jvm)->DestroyJavaVM(jvm);
}
JDK 1.2初始化虚拟机:
/* invoke2.c */
#include <jni.h>
int main() {
int res;
JavaVM *jvm;
JNIEnv *env;
JavaVMInitArgs vm_args;
JavaVMOption options[3];
vm_args.version=JNI_VERSION_1_2;//这个字段必须设置为该值
/*设置初始化参数*/
options[0].optionString = "-Djava.compiler=NONE";
options[1].optionString = "-Djava.class.path=.";
options[2].optionString = "-verbose:jni"; //用于跟踪运行时的信息
/*版本号设置不能漏*/
vm_args.version = JNI_VERSION_1_2;
vm_args.nOptions = 3;
vm_args.options = options;
vm_args.ignoreUnrecognized = JNI_TRUE;
res = JNI_CreateJavaVM(&jvm, (void**)&env, &vm_args);
if (res < 0) {
fprintf(stderr, "Can't create Java VM\n");
exit(1);
}
(*jvm)->DestroyJavaVM(jvm);
fprintf(stdout, "Java VM destory.\n");
}
为了保证JNI代码的可移植性,建议使用JDK 1.2的方法来创建虚拟机。JNI_CreateJavaVM函数的第二个参数JNIEnv *env,就是贯穿整个JNI始末的一个参数,因为几乎所有的函数都要求一个参数就是JNIEnv *env。
访问类方法
初始化了Java虚拟机后,就可以开始调用Java的方法。要调用一个Java对象的方法必须经过几个步骤:
1.获取指定对象的类定义(jclass)
有两种途径来获取对象的类定义:第一种是在已知类名的情况下使用FindClass来查找对应的类。但是要注意类名并不同于平时写的Java代码,例如要得到类jni.test.Demo的定义必须调用如下代码:
jclass cls = (*env)->FindClass(env, "jni/test/Demo"); //把点号换成斜杠
然后通过对象直接得到其所对应的类定义:
jclass cls = (*env)-> GetObjectClass(env, obj);
//其中obj是要引用的对象,类型是jobject
2.读取要调用方法的定义(jmethodID)
我们先来看看JNI中获取方法定义的函数:
jmethodID (JNICALL *GetMethodID)(JNIEnv *env, jclass clazz, const char *name,
const char *sig);
jmethodID (JNICALL *GetStaticMethodID)(JNIEnv *env, jclass class, const char
*name, const char *sig);
这两个函数的区别在于GetStaticMethodID是用来获取静态方法的定义,GetMethodID则是获取非静态的方法定义。这两个函数都需要提供四个参数:env就是初始化虚拟机得到的JNI环境;第二个参数class是对象的类定义,也就是第一步得到的obj;第三个参数是方法名称;最重要的是第四个参数,这个参数是方法的定义。因为我们知道Java中允许方法的多态,仅仅是通过方法名并没有办法定位到一个具体的方法,因此需要第四个参数来指定方法的具体定义。但是怎么利用一个字符串来表示方法的具体定义呢?JDK中已经准备好一个反编译工具javap,通过这个工具就可以得到类中每个属性、方法的定义。下面就来看看jni.test.Demo的定义:
打开命令行窗口并运行 javap -s -p jni.test.Demo 得到运行结果如下:
Compiled from Demo.java
public class jni.test.Demo extends java.lang.Object {
public static int COUNT;
/* I */
public java.lang.String msg;
/* Ljava/lang/String; */
private int counts[];
/* [I */
public jni.test.Demo();
/* ()V */
public jni.test.Demo(java.lang.String);
/* (Ljava/lang/String;)V */
public java.lang.String getMessage();
/* ()Ljava/lang/String; */
public int getCounts()[];
/* ()[I */
public void setCounts(int[]);
/* ([I)V */
public void throwExcp() throws java.lang.IllegalAccessException;
/* ()V */
static {};
/* ()V */
}
我们看到类中每个属性和方法下面都有一段注释。注释中不包含空格的内容就是第四个参数要填的内容(关于javap具体参数请查询JDK的使用帮助)。下面这段代码演示如何访问jni.test.Demo的getMessage方法:
/*
假设我们已经有一个jni.test.Demo的实例obj
*/
jmethodID mid;
jclass cls = (*env)-> GetObjectClass (env, obj); //获取实例的类定义
mid=(*env)->GetMethodID(env,cls,"getMessage"," ()Ljava/lang/String; ");
/*如果mid为0表示获取方法定义失败*/
jstring msg = (*env)-> CallObjectMethod(env, obj, mid);
/*
如果该方法是静态的方法那只需要将最后一句代码改为以下写法即可:
jstring msg = (*env)-> CallStaticObjectMethod(env, cls, mid);
*/
3.调用方法
为了调用对象的某个方法,可以使用函数Call<TYPE>Method或者CallStatic<TYPE>Method(访问类的静态方法),<TYPE>根据不同的返回类型而定。这些方法都是使用可变参数的定义,如果访问某个方法需要参数时,只需要把所有参数按照顺序填写到方法中就可以。在讲到构造函数的访问时,将演示如何访问带参数的构造函数。
访问类属性
访问类的属性与访问类的方法大体上是一致的,只不过是把方法变成属性而已。
1.获取指定对象的类(jclass)
这一步与访问类方法的第一步完全相同,具体使用参看访问类方法的第一步。
2.读取类属性的定义(jfieldID)
在JNI中是这样定义获取类属性的方法的:
jfieldID (JNICALL *GetFieldID)
(JNIEnv *env, jclass clazz, const char *name, const char *sig);
jfieldID (JNICALL *GetStaticFieldID)
(JNIEnv *env, jclass clazz, const char *name, const char *sig);
这两个函数中第一个参数为JNI环境;clazz为类的定义;name为属性名称;第四个参数同样是为了表达属性的类型。前面我们使用javap工具获取类的详细定义的时候有这样两行:
public java.lang.String msg;
/* Ljava/lang/String; */
其中第二行注释的内容就是第四个参数要填的信息,这跟访问类方法时是相同的。
3.读取和设置属性值
有了属性的定义要访问属性值就很容易了。有几个方法用来读取和设置类的属性,它们是:Get<TYPE>Field、Set<TYPE>Field、GetStatic<TYPE>Field、SetStatic<TYPE>Field。比如读取Demo类的msg属性就可以用GetObjectField,而访问COUNT用GetStaticIntField,相关代码如下:
jfieldID field = (*env)->GetFieldID(env,obj,"msg"," Ljava/lang/String;");
jstring msg = (*env)-> GetObjectField(env, cls, field); //msg就是对应Demo的msg
jfieldID field2 = (*env)->GetStaticFieldID(env,obj,"COUNT","I");
jint count = (*env)->GetStaticIntField(env,cls,field2);
访问构造函数
很多人刚刚接触JNI的时候往往会在这一节遇到问题,查遍了整个jni.h看到这样一个函数NewObject,它应该是可以用来访问类的构造函数。但是该函数需要提供构造函数的方法定义,其类型是jmethodID。从前面的内容我们知道要获取方法的定义首先要知道方法的名称,但是构造函数的名称怎么来填写呢?其实访问构造函数与访问一个普通的类方法大体上是一样的,惟一不同的只是方法名称不同及方法调用时不同而已。访问类的构造函数时方法名必须填写“<init>”。下面的代码演示如何构造一个Demo类的实例:
jclass cls = (*env)->FindClass(env, "jni/test/Demo");
/**
首先通过类的名称获取类的定义,相当于Java中的Class.forName方法
*/
if (cls == 0)
<error handler>
jmethodID mid = (*env)->GetMethodID(env,cls,"<init>","(Ljava/lang/String;)V ");
if(mid == 0)
<error handler>
jobject demo = jenv->NewObject(cls,mid,0);
/**
访问构造函数必须使用NewObject的函数来调用前面获取的构造函数的定义
上面的代码我们构造了一个Demo的实例并传一个空串null
*/
数组处理
创建一个新数组
要创建一个数组,我们首先应该知道数组元素的类型及数组长度。JNI定义了一批数组的类型j<TYPE>Array及数组操作的函数New<TYPE>Array,其中<TYPE>就是数组中元素的类型。例如,要创建一个大小为10并且每个位置值分别为1-10的整数数组,编写代码如下:
int i = 1;
jintArray array; //定义数组对象
(*env)-> NewIntArray(env, 10);
for(; i<= 10; i++)
(*env)->SetIntArrayRegion(env, array, i-1, 1, &i);
访问数组中的数据
访问数组首先应该知道数组的长度及元素的类型。现在我们把创建的数组中的每个元素值打印出来,代码如下:
int i;
/* 获取数组对象的元素个数 */
int len = (*env)->GetArrayLength(env, array);
/* 获取数组中的所有元素 */
jint* elems = (*env)-> GetIntArrayElements(env, array, 0);
for(i=0; i< len; i++)
printf("ELEMENT %d IS %d\n", i, elems);
中文处理
中文字符的处理往往是让人比较头疼的事情,特别是使用Java语言开发的软件,在JNI这个问题更加突出。由于Java中所有的字符都是Unicode编码,但是在本地方法中,例如用VC编写的程序,如果没有特殊的定义一般都没有使用Unicode的编码方式。为了让本地方法能够访问Java中定义的中文字符及Java访问本地方法产生的中文字符串,我定义了两个方法用来做相互转换。
方法一,将Java中文字符串转为本地字符串
/**
第一个参数是虚拟机的环境指针
第二个参数为待转换的Java字符串定义
第三个参数是本地存储转换后字符串的内存块
第三个参数是内存块的大小
*/
int JStringToChar(JNIEnv *env, jstring str, LPTSTR desc, int desc_len)
{
int len = 0;
if(desc==NULL||str==NULL)
return -1;
//在VC中wchar_t是用来存储宽字节字符(UNICODE)的数据类型
wchar_t *w_buffer = new wchar_t[1024];
ZeroMemory(w_buffer,1024*sizeof(wchar_t));
//使用GetStringChars而不是GetStringUTFChars
wcscpy(w_buffer,env->GetStringChars(str,0));
env->ReleaseStringChars(str,w_buffer);
ZeroMemory(desc,desc_len);
//调用字符编码转换函数(Win32 API)将UNICODE转为ASCII编码格式字符串
//关于函数WideCharToMultiByte的使用请参考MSDN
len = WideCharToMultiByte(CP_ACP,0,w_buffer,1024,desc,desc_len,NULL,NULL);
//len = wcslen(w_buffer);
if(len>0 && len<desc_len)
desc[len]=0;
delete[] w_buffer;
return strlen(desc);
}
方法二,将C的字符串转为Java能识别的Unicode字符串
jstring NewJString(JNIEnv* env,LPCTSTR str)
{
if(!env || !str)
return 0;
int slen = strlen(str);
jchar* buffer = new jchar[slen];
int len = MultiByteToWideChar(CP_ACP,0,str,strlen(str),buffer,slen);
if(len>0 && len < slen)
buffer[len]=0;
jstring js = env->NewString(buffer,len);
delete [] buffer;
return js;
}
异常
由于调用了Java的方法,因此难免产生操作的异常信息。这些异常没有办法通过C++本身的异常处理机制来捕捉到,但JNI可以通过一些函数来获取Java中抛出的异常信息。之前我们在Demo类中定义了一个方法throwExcp,下面将访问该方法并捕捉其抛出来的异常信息,代码如下:
/**
假设我们已经构造了一个Demo的实例obj,其类定义为cls
*/
jthrowable excp = 0; /* 异常信息定义 */
jmethodID mid=(*env)->GetMethodID(env,cls,"throwExcp","()V");
/*如果mid为0表示获取方法定义失败*/
jstring msg = (*env)-> CallVoidMethod(env, obj, mid);
/* 在调用该方法后会有一个IllegalAccessException的异常抛出 */
excp = (*env)->ExceptionOccurred(env);
if(excp){
(*env)->ExceptionClear(env);
//通过访问excp来获取具体异常信息
/*
在Java中,大部分的异常信息都是扩展类java.lang.Exception,因此可以访问excp的toString
或者getMessage来获取异常信息的内容。访问这两个方法同前面讲到的如何访问类的方法是相同的。
*/
}
线程和同步访问
有些时候需要使用多线程的方式来访问Java的方法。我们知道一个Java虚拟机是非常消耗系统的内存资源,差不多每个虚拟机需要内存大约在20MB左右。为了节省资源要求每个线程使用的是同一个虚拟机,这样在整个的JNI程序中只需要初始化一个虚拟机就可以了。所有人都是这样想的,但是一旦子线程访问主线程创建的虚拟机环境变量,系统就会出现错误对话框,然后整个程序终止。
其实这里面涉及到两个概念,它们分别是虚拟机(JavaVM *jvm)和虚拟机环境(JNIEnv *env)。真正消耗大量系统资源的是jvm而不是env,jvm是允许多个线程访问的,但是env只能被创建它本身的线程所访问,而且每个线程必须创建自己的虚拟机环境env。这时候会有人提出疑问,主线程在初始化虚拟机的时候就创建了虚拟机环境env。为了让子线程能够创建自己的env,JNI提供了两个函数:AttachCurrentThread和DetachCurrentThread。下面代码就是子线程访问Java方法的框架:
DWORD WINAPI ThreadProc(PVOID dwParam)
{
JavaVM jvm = (JavaVM*)dwParam; /* 将虚拟机通过参数传入 */
JNIEnv* env;
(*jvm)-> AttachCurrentThread(jvm, (void**)&env, NULL);
.........
(*jvm)-> DetachCurrentThread(jvm);
}
时间
关于时间的话题是我在实际开发中遇到的一个问题。当要发布使用了JNI的程序时,并不一定要求客户要安装一个Java运行环境,因为可以在安装程序中打包这个运行环境。为了让打包程序利于下载,这个包要比较小,因此要去除JRE(Java运行环境)中一些不必要的文件。但是如果程序中用到Java中的日历类型,例如java.util.Calendar等,那么有个文件一定不能去掉,这个文件就是[JRE]\lib\tzmappings。它是一个时区映射文件,一旦没有该文件就会发现时间操作上经常出现与正确时间相差几个小时的情况。下面是打包JRE中必不可少的文件列表(以Windows环境为例),其中[JRE]为运行环境的目录,同时这些文件之间的相对路径不能变。
文件名 目录
hpi.dll [JRE]\bin
ioser12.dll [JRE]\bin
java.dll [JRE]\bin
net.dll [JRE]\bin
verify.dll [JRE]\bin
zip.dll [JRE]\bin
jvm.dll [JRE]\bin\classic
rt.jar [JRE]\lib
tzmappings [JRE]\lib
由于rt.jar有差不多10MB,但是其中有很大一部分文件并不需要,可以根据实际的应用情况进行删除。例如程序如果没有用到Java Swing,就可以把涉及到Swing的文件都删除后重新打包。
|
Oracle的ROWID用来唯一标识表中的一条记录,是这条数据在数据库中存放的物理地址。
Oracle的ROWID分为两种:物理ROWID和逻辑ROWID。索引组织表使用逻辑ROWID,其他类型的表使用物理ROWID。其中物理ROWID在Oracle的8版本中进行了扩展,Oracle7及以下版本使用约束ROWID,Oracle8及以上版本使用扩展ROWID。本文描述物理扩展ROWID,由于约束ROWID仅仅是为了兼容早期版本,因此不做讨论。
SQL> create table test_rowid (id number, row_id rowid);
表已创建。
SQL> insert into test_rowid values (1, null);
已创建 1 行。
SQL> update test_rowid set row_id = rowid where id = 1;
已更新 1 行。
SQL> commit;
提交完成。
SQL> select rowid, row_id from test_rowid;
ROWID ROW_ID
------------------ ------------------
AAABnRAAGAAAACWAAA AAABnRAAGAAAACWAAA
Oracle的物理扩展ROWID有18位,每位采用64位编码,分别用A~Z、a~z、0~9、+、/共64个字符表示。A表示0,B表示1,……Z表示25,a表示26,……z表示51,0表示52,……,9表示61,+表示62,/表示63。
ROWID具体划分可以分为4部分。
1.OOOOOO:前6位表示DATA OBJECT NUMBER,将起转化位数字后匹配DBA_OBJECTS中的DATA_OBJECT_ID,可以确定表信息。
如上面例子中的DATA OBJECT NUMBER是AAABnR,转化位数字是1×64×64 +39×64 + 17。
SQL> select owner, object_name from dba_objects
2 where data_object_id = 1*64*64 + 39*64 + 17;
OWNER OBJECT_NAME
------------------------------ -----------------------------
YANGTK TEST_ROWID
2.FFF:第7到9位表示相对表空间的数据文件号。
上面的例子中是AAG,表示数据文件6。
SQL> select file_name, tablespace_name from dba_data_files where relative_fno = 6;
FILE_NAME TABLESPACE_NAME
--------------------------------------------- ---------------
E:ORACLEORADATATESTYANGTK01.DBF YANGTK
3.BBBBBB:第10到15位表示这条记录在数据文件中的第几个BLOCK中。
上面的例子是AAAACW,转化位数字是2×64+22,表示这条记录在数据文件中的第150个BLOCK。
4.RRR:最后3位表示这条记录是BLOCK中的第几条记录。
上面的例子是AAA,表示第0条记录(总是从0开始计数)。
SQL> alter system dump datafile 6 block 150;
系统已更改。
SQL> select row_id, dump(row_id, 16) dump_rowid from test_rowid;
ROW_ID DUMP_ROWID
------------------ -------------------------------------------------
AAABnRAAGAAAACWAAA Typ=69 Len=10: 0,0,19,d1,1,80,0,96,0,0
找到对应的dump文件,可以发现类型的信息
*** 2004-12-21 17:58:26.000
*** SESSION ID:(13.91) 2004-12-21 17:58:26.000
Start dump data blocks tsn: 6 file#: 6 minblk 150 maxblk 150
buffer tsn: 6 rdba: 0x01800096 (6/150)
scn: 0x0000.2e389c16 seq: 0x01 flg: 0x06 tail: 0x9c160601
frmt: 0x02 chkval: 0xc97d type: 0x06=trans data
Block header dump: 0x01800096
Object id on Block? Y
seg/obj: 0x19d1 csc: 0x00.2e389c0f itc: 2 flg: O typ: 1 - DATA
fsl: 0 fnx: 0x0 ver: 0x01
Itl Xid Uba Flag Lck Scn/Fsc
0x01 0x0003.009.00000057 0x0080004b.0042.56 --U- 1 fsc 0x0000.2e389c16
0x02 0x0000.000.00000000 0x00000000.0000.00 ---- 0 fsc 0x0000.00000000
data_block_dump,data header at 0x651105c
===============
tsiz: 0x3fa0
hsiz: 0x14
pbl: 0x0651105c
bdba: 0x01800096
76543210
flag=--------
ntab=1
nrow=1
frre=-1
fsbo=0x14
fseo=0x3f89
avsp=0x3f7b
tosp=0x3f7b
0xe:pti[0] nrow=1 offs=0
0x12:pri[0] offs=0x3f89
block_row_dump:
tab 0, row 0, @0x3f89
tl: 17 fb: --H-FL-- lb: 0x1 cc: 2
col 0: [ 2] c1 02
col 1: [10] 00 00 19 d1 01 80 00 96 00 00
end_of_block_dump
End dump data blocks tsn: 6 file#: 6 minblk 150 maxblk 150
有时需要查看表的DUMP信息,但是很难准确定位表中数据开始于哪个BLOCK,根据ROWID中包含的信息就可以方便的找到起始BLOCK。
下面简单描述一下ROWID类型是如何存储的。
SQL> select row_id, dump(row_id, 16) dump_rowid from test_rowid;
ROW_ID DUMP_ROWID
------------------ -------------------------------------------------
AAABnRAAGAAAACWAAA Typ=69 Len=10: 0,0,19,d1,1,80,0,96,0,0
前4位表示ROWID的前6位,也就是DATA_OBJECT_ID信息。数据以数值的格式保存。
SQL> select to_number('19d1', 'xxxxxx') from dual;
TO_NUMBER('19D1','XXXXXX')
--------------------------
6609
SQL> select 1*64*64 + 39*64 + 17 from dual;
1*64*64+39*64+17
----------------
6609
这里存在一个问题,根据ROWID的取值范围,OBJECT_DATA_ID最大的值是64的6次方,而根据DUMP,oracle只用了4位保存,因此取值范围是256的4次方。
SQL> set numwid 12
SQL> select power(64, 6), power(256, 4), power(64, 6)/power(256, 4) from dual;
POWER(64,6) POWER(256,4) POWER(64,6)/POWER(256,4)
------------ ------------ ------------------------
68719476736 4294967296 16
可见,OBJECT_DATA_ID的最大值是4294967296,当超过这个值时会出现重复的情况。(当然,现实中不大可能)。
后面4位比较特殊,是数据文件号和BLOCK数的“和”值构成。
数据文件的数值乘64后保存在5、6位上。
SQL> select to_number('0180', 'xxxx') from dual;
TO_NUMBER('0180','XXXX')
------------------------
384
SQL> select 6*64 from dual;
6*64
------------
384
同时,6位BLOCK的值,也保存在这4位上,并与数据文件转存结果相加。仍然是以数字格式存放。
SQL> select to_number('96', 'xxx') from dual;
TO_NUMBER('96','XXX')
---------------------
150
SQL> select 2*64 + 22 from dual;
2*64+22
----------
150
由于采用两位保存数据文件的值,且最小单位是64,因此,ROWID中可以保存的数据文件数是1024,超过1024会造成ROWID的重复。
SQL> select 256*256/64 from dual;
256*256/64
----------
1024
由于BLOCK的值和数据文件共用这4位,因此BLOCK的第3位最大值应小于64,这样才能保证ROWID的不重复。因此BLOCK值的最大值应该是4194304。
SQL> select 64*256*256 from dual;
64*256*256
----------
4194304
最后两位保存BLOCK中记录的值。这个值的最大值是65536。
SQL> select 256*256 from dual;
256*256
----------
65536
Oracle的文档上没有介绍逻辑ROWID的编码规则,而且通过DUMP的结果也很难反推出编码规则。因此,本文只简单讨论一下逻辑ROWID的存储。
下面来看例子。
SQL> create table test_index (id number primary key, name varchar2(20)) organization index;
表已创建。
SQL> insert into test_index values (1, 'a');
已创建 1 行。
SQL> commit;
提交完成。
SQL> col dump_rowid format a60
SQL> select rowid, dump(rowid) dump_rowid from test_index;
ROWID DUMP_ROWID
--------------------------- ----------------------------------------
*BAFAB4wCwQL+ Typ=208 Len=10: 2,4,1,64,7,140,2,193,2,254
逻辑ROWID的DUMP结果前两位都是2和4,最后一位都是254,(我还没有发现其他的情况),由于逻辑ROWID和主键的值有关,所以长度是不定的,因此应该是用来表示开始和结束的。
第3、4位和物理ROWID一样,表示的是相对表空间的数据文件号乘以64的值。
第5、6位表示这条记录在数据文件的第几个BLOCK中。
从第7位开始到DUMP结果的倒数第二位,表示主键的值。首先是主键中第一个字段的长度,这里是2,然后是主键的值,由于是NUMBER类型,因此193,2表示数值1。如果是多个字段组成的主键,第一个字段之后是第二个字段的长度,然后是第二个字段的值……。
SQL> select (1*256 + 64)/64 from dual;
(1*256+64)/64
-------------
5
SQL> select 7*256 + 140 from dual;
7*256+140
----------
1932
SQL> alter system dump datafile 5 block 1932;
系统已更改。
找到相应的dump文件,可以发现刚才插入的记录。
Dump file f:oracleadmintest4udumptest4_ora_3828.trc
Thu Dec 23 00:17:53 2004
ORACLE V9.2.0.4.0 - Production vsnsta=0
vsnsql=12 vsnxtr=3
Windows 2000 Version 5.1 Service Pack 1, CPU type 586
Oracle9i Enterprise Edition Release 9.2.0.4.0 - Production
With the Partitioning, Oracle Label Security, OLAP and Oracle Data Mining options
JServer Release 9.2.0.4.0 - Production
Windows 2000 Version 5.1 Service Pack 1, CPU type 586
Instance name: test4
Redo thread mounted by this instance: 1
Oracle process number: 9
Windows thread id: 3828, image: ORACLE.EXE
*** 2004-12-23 00:17:53.361
*** SESSION ID:(8.82) 2004-12-23 00:17:53.301
Start dump data blocks tsn: 5 file#: 5 minblk 1932 maxblk 1932
buffer tsn: 5 rdba: 0x0140078c (5/1932)
scn: 0x0000.00e9f122 seq: 0x01 flg: 0x02 tail: 0xf1220601
frmt: 0x02 chkval: 0x0000 type: 0x06=trans data
Block header dump: 0x0140078c
Object id on Block? Y
seg/obj: 0x1e48 csc: 0x00.e9f113 itc: 2 flg: E typ: 2 - INDEX
brn: 0 bdba: 0x1400789 ver: 0x01
inc: 0 exflg: 0
Itl Xid Uba Flag Lck Scn/Fsc
0x01 0x0000.000.00000000 0x00000000.0000.00 ---- 0 fsc 0x0000.00000000
0x02 0x0005.008.000000e7 0x00800226.005c.24 --U- 1 fsc 0x0000.00e9f122
Leaf block dump
===============
header address 71963236=0x44a1264
kdxcolev 0
KDXCOLEV Flags = - - -
kdxcolok 0
kdxcoopc 0x90: opcode=0: iot flags=I-- is converted=Y
kdxconco 1
kdxcosdc 0
kdxconro 1
kdxcofbo 38=0x26
kdxcofeo 8026=0x1f5a
kdxcoavs 7988
kdxlespl 0
kdxlende 0
kdxlenxt 0=0x0
kdxleprv 0=0x0
kdxledsz 0
kdxlebksz 8036
row#0[8026] flag: K----, lock: 2
col 0; len 2; (2): c1 02
tl: 5 fb: --H-FL-- lb: 0x0 cc: 1
col 0: [ 1]
Dump of memory from 0x044A31C7 to 0x044A31C8
44A31C0 61010100 [...a]
----- end of leaf block dump -----
End dump data blocks tsn: 5 file#: 5 minblk 1932 maxblk 1932
可以看到,根据DUMP结果的3、4、5、6位可以定位记录的物理位置。
需要注意的是,索引组织表以主键的顺序存储数据,因此插入、更新和删除数据都可能造成一条记录的物理位置发生变化,这时通过ROWID中的DATAFILE和BLOCK的信息可能就无法正确定位到记录的物理位置。当根据逻辑ROWID访问索引组织表时,首先会根据DATAFILE和BLOCK信息去找到相应的BLOCK,检查数据是否在这个BLOCK中,如果不在,就通过逻辑ROWID中的主键信息去通过索引扫描,找到这条记录。这就是Oracle文档在提到的physical guess。
下面看一个由字符串和日期组成联合主键的例子。
SQL> create table test_index2 (id char(4), time date,
2 constraint pk_test_index2 primary key (id, time)) organization index;
表已创建。
SQL> insert into test_index2 values ('1', sysdate);
已创建 1 行。
SQL> col dump_rowid format a75
SQL> select rowid, dump(rowid) dump_rowid from test_index2;
ROWID DUMP_ROWID
---------------------------- ------------------------------------------------------------------
*BAFAB5QEMSAgIAd4aAwXASMT/g Typ=208 Len=20: 2,4,1,64,7,148,4,49,32,32,32,7,120,104,12,23,1,35,19,254
可以看出,第7位是字段id的长度4,然后是字符串1和三个空格的ASCII码,这是字符串的存储格式,后面跟着的7是字段time长度,后面七位是日期的存储格式。在逻辑ROWID中,数值、字符和日期类型的存储格式都和它们本身的存储格式一致,这里不在赘述。
一般情况下,使用一位来表示长度,但是如果长度超过了127(16进制DUMP的结果是7F),则长度开始用两位表示。第一位以8开头,这个8只是标识位,表明长度字段现在由两位来表示。例如长度128表示位8080,而支持的最大值3800表示为8ED8。
|
|
|
|