Java学习笔记下


Java 学习笔记(二) From Gossip@caterpillar 语言技术:Java Gossip(二) 在学习Java的过程中,您会接触到很多框架(Framework), 而第一步就是从J2SE的框架开始学习,框架让您不用重新打造您需要的功能元件,它也 是经验的集成,即使您要重新打造,参考框架的实作方式,就可以让您获 得不少宝贵的经验。 事实上以下的每个主题区都有专书介绍,这边所介绍的内容,都只是某个主题的入门,若有进阶的应用需求,查阅相关书籍是必要的。 物件容器(Container) 物件容器可以帮您持有对象,在Java中分作两大类:Collection 与 Map。前者可持有各自独立的物件,后者持有成对的key-value物 件。 ● Collection 类 Collection 类包括了 List 类与 Set 类,List 以放置物件至容器中的顺序来排列物件,Set 类不接受重覆的物件,并有自己的一套 排序规则。 ❍ ArrayList ❍ LinkedList ❍ HashSet ❍ TreeSet ❍ EnumSet ● Map 类 在将物件存入 Map类时,需要配合一把key,您要取回物 件时就是根据这把key,Map中的key是唯一的,Map拥有自己的排序机 制。 ❍ HashMap ❍ TreeMap ❍ EnumMap 输入输出(I/O) 为了要将程式运行结果储存下来,以供下一次开启时使用,档案输入输出是一个应用程式所必备的,藉由档案的输入输 出,也可以 一同了解Java的I/O处理。 ● 档案处理 File类别是档 案的抽象表示,也是处理档案输入输出时所 必备的。 ❍ File ❍ RandomAccessFile ● 位元串流 电脑中的资料都是 以 0 与 1 的方式来储存,如果您要在两个装置之间进行资料的存取,当然也是以 0 与 1 位元的方式来进行, Java将资料目的地与来源之间的流动抽象化为一个串流(Stream),而当中流动的则是位元资料。 ❍ InputStream、 OutputStream ❍ FileInputStream、 FileOutputStream http://caterpillar.onlyfun.net/GossipCN/JavaGossip-V2/JavaGossip2.htm(第 1/5 页)2008-7-30 20:04:23 Java 学习笔记(二) ❍ BufferedInputStream、 BufferedOutputStream ❍ DataInputStream、 DataOutputStream ❍ ObjectInputStream、 ObjectOutputStream ❍ SequenceInputStream ❍ PrintStream ❍ ByteArrayInputStream、 ByteArrayOutputStream ❍ PushbackInputStream ● 字元串流 Reader、 Writer等是处理字元串流 (Character Stream)的相关类别,简单的说,就是对串流资料以一个字元(16bit)的长度为 单位来处理(0~65535、0x0000-0xffff),并进 行适当的字元转换处理。 ❍ Reader、 Writer ❍ InputStreamReader、 OutputStreamWriter ❍ FileReader、 FileWriter ❍ BufferedReader、 BufferedWriter ❍ PrintWriter ❍ CharArrayReader、 CharArrayWriter ❍ PushbackReader 执行绪(Thread) 一个执行绪是进程(Process)中的一个执行流程,一个进程中可以同时包括多个执行绪,CPU会分配时间片段来处理这些子流程, 这使得一个程式可以像 是同时间处理多个事务。 ● 执行绪入门 想要实作执行绪功 能,只要实作Runnable介面,单就 表面上来看,执行绪并不困难。 ❍ 实 作 Runnable 介面 ❍ Daemon 执行绪 ❍ 执 行绪生命周期 ❍ 执 行绪的加入(join) ❍ 执 行绪的停止 ❍ ThreadGroup ● 同步化(synchronized) 您只要实作 Runnable介面,就可以实作执行绪功能, 入门是简单的,但资料一被共用,事情就变得复杂。 ❍ 执 行绪的同步化 ❍ wait ()、notify() ❍ 容 器类的执行绪安全 (Thread-safe) ● JDK 5.0 新增类别 来看看 JDK 5.0 为多执行绪新增的几个好用类别。 ❍ UncaughtExceptionHandler ❍ Lock 与 Condition ❍ BlockingQueue ❍ Callable 与 Future ❍ Excutors 反射(Reflection) 反射机制允许执行时期动态检视与操作类别,一般应用程式开发较不常用到,但是在一些框架或检视工具程式中,就经常使用到反 ● StarSuite (包含 Java™) ● Google桌面 (桌面小帮手) ● Picasa (相簿管理程式) ● Norton Security Scan (病毒扫描) ● Spyware Doctor (反间碟软体) ● 各种实用软体工具 http://caterpillar.onlyfun.net/GossipCN/JavaGossip-V2/JavaGossip2.htm(第 2/5 页)2008-7-30 20:04:23 Java 学习笔记(二) 射机制。 ● 检视类别 即使您拿到一个类 别并对它一无所知,但其实它本身就包括了 许多资讯,从 Class 开始,您可以获得这个类的许多讯息。 ❍ 简 介 Class ❍ 从 Class 中获取资讯 ❍ 简 介 ClassLoader ❍ 自 订 ClassLoader ● 生成与操作 使用反射机制,您 可以于执行时期动态载入类别并生成物件, 操作物件上的方法甚至改变类别成员的值。 ❍ 生 成物件 ❍ 呼 叫方法 ❍ 修 改成员值 ❍ 生 成阵列 metadata metadata 简单的说就是资料的资料(Data about data),J2SE 5.0 中对 metadata 的支援是 Annotation,目的在对程式码作出说明以利分 析工具使用。 ● Annotation Annotation 对程式运行没有影响,它的目的在对编译器或分析工具说明程式的某些资讯,您可以在package、class、method、 field等上加上 Annotation。 ❍ 限 定 Override 父类方法 - Override ❍ 标 示方法为 Deprecated - Deprectated ❍ 抑制编译 器警讯 - SuppressWarnings ❍ 自 订 Annotation 型态 ● meta-annotation 在定义 Annotation 型态的时候,为 Annotation 型态加上 Annotation 并不奇怪,这为处理 Annotation 型态的工具提供资讯。 ❍ 告 知编译器如何处理 annotaion - Retention ❍ 限 定 annotation 使用对象 - Target ❍ 要 求为 API 文件的一部份 - Documented ❍ 子 类是否继承父类的 annotation - Inherited 资料库(JDBC) JDBC是用于执行SQL的Java API,它将资料库存取的API与SQL陈述分开,实现资料库无关的API介面,藉由JDBC统一的介面,开发 人员只要专注于SQL陈述,而可以不必理会 底层的资料库驱动程式与相关介面。 ● 简 介 JDBC ● 连接资料 库 ● 连 接资料库 - JDBC 4.0 ● Statement、 ResultSet ● Statement 批次处理 ● PreparedStatement ● 将档 案存入资料库 ● 将 档案存入资料库 - Oracle 9i ● ResultSet 游标控制 ● ResultSet 新增、更新、删除资料 http://caterpillar.onlyfun.net/GossipCN/JavaGossip-V2/JavaGossip2.htm(第 3/5 页)2008-7-30 20:04:23 Java 学习笔记(二) ● ResultSetMetaData 类别 ● 交易 (Transaction) 网路 这边介绍一下简单的网路程式设计,并使用 Socket 与 ServerSocket 设计一些简单的实例。 ● 入门 首先从认识 java.net 套件下的几个类别认识起。 ❍ InetAddress 类别 ❍ URL 类别 ❍ Socket 类别 ❍ ServerSocket 类别 ● 程式实例 以 Socket 及 ServerSocket 所实作的一些简单网路程式范例。 ❍ 档 案传送 ❍ HTTP 重新导向 ❍ 代 理伺服器 ❍ 简 单 HTTP 伺服器 ❍ 简 单 SMTP 送信(图形介面) ❍ 一 对一网路聊天(图形介面) ❍ 多人连 线聊天(图形介面) ❍ 远 端监视程式(图形介面) ❍ TicTacToe 井字游戏(Applet) 舍遗补缺 前面都是有系统的介绍某个主题,然而整个 Java SE 要详细说明是不可能的,这边就随时补充一些可能使用到的类别。 ● 属性设定、讯息绑定 您可以将一些属性 设定独立于一个.properties档 中,由程式读取,若要修改属性则直接修改.properties档案即可。也可以将讯息 另外定义在一个文字档案中,而不是写死在程式中,日后想要更改讯息 时,只要更改文字档案内容,而不用重新编译程式。 ❍ 使 用 Properties ❍ 使 用 ResourceBundle ❍ 国 际化讯息 ● 日期、时间 表面上看来,要取 得系统的时间只要使用 Date 类别就可以了,但查阅 Date 后,发现很多方法都被标示为 Deprecated? ❍ 使用 Date、DateFormat ❍ 使 用 Calendar ❍ 使 用 Calendar - JDK6 ● 记录(Logging) 如果您只是要作一 些简单的文件记录,可以考虑内建在JDK 中的Logging API,好处是它从JDK 1.4之后就成为J2SE的一员。 ❍ 简 介 Logging ❍ Logging 的层级 ❍ Handler、 Formatter ❍ 自 订 Formatter ❍ 自 订 Handler http://caterpillar.onlyfun.net/GossipCN/JavaGossip-V2/JavaGossip2.htm(第 4/5 页)2008-7-30 20:04:23 Java 学习笔记(二) ❍ 自 订 Filter ❍ Logger 阶层关系 ● 工作排程 如果您想要排定任 务在某些时间点运行,那么您可以使用标准 API中的Timer与TimerTask。 ❍ TimerTask 与 Timer ❍ Fixed-delay Execution 与 Fixed-rate Execution 视窗程式 ● 文 字编辑器制作 ● 制 作 Executable JAR ● 启 动画面与工具列图示 - Java SE 6 http://caterpillar.onlyfun.net/GossipCN/JavaGossip-V2/JavaGossip2.htm(第 5/5 页)2008-7-30 20:04:23 Framework From Gossip@caterpillar Struts Gossip: Framework 在 Gof 書中,Gamma等人為框架(Framework)給出了一個定義:”框架就是一組協同工作的類,它們為特定類型 的軟體構築了一個可重用的設計"。 http://caterpillar.onlyfun.net/Gossip/Struts/FrameWork.htm2008-7-30 20:04:35 ArrayList From Gossip@caterpillar Java Gossip: ArrayList ArrayList类别实作了List介面,List介面是Collection介面的子介面,主要增加了根据索引取得物件的方法。 ArrayList使用阵列实作List介面,所以对于快速的随机取得物件来说,使用ArrayList可以得到较好的效能,不过在移除物件或插入物 件时,ArrayList就比较慢(使用 LinkedList 在这方面就好的多)。 来看看一个ArrayList的范例: ● ArrayListDemo.java package onlyfun.caterpillar; import java.util.*; public class ArrayListDemo { public static void main(String[] args) { Scanner scanner = new Scanner(System.in); List list = new ArrayList(); System.out.println("输入名称(quit结束)"); while(true) { System.out.print("# "); String input = scanner.next(); if(input.equals("quit")) break; list.add(input); } System.out.print("显示输入: "); for(int i = 0; i < list.size(); i++) System.out.print(list.get(i) + " "); System.out.println(); } } 在 J2SE 5.0 之后新增了泛型(Generic)的功能,使用物件容器时建议容器中将储存的物件型态,如此您的物件在存入容器会被限定 为您所宣告的型态,而取出时,也不至于失去原来的型态资讯,可以避免型态转换时的问题。 使用add()方法可以将一个物件加入ArrayList中,使用size()方法可以传回目前的ArrayList的长度,使用get()可以传回指定索引处的物 件,使用toArray()可以将ArrayList中的物件转换为物件阵列。 http://caterpillar.onlyfun.net/GossipCN/JavaGossip-V2/ArrayList.htm(第 1/3 页)2008-7-30 20:04:38 ArrayList 以下是执行结果: 输入名称(quit结束) # Justin # caterpillar # momor # quit 显示输入: Justin caterpillar momor 您可以使用get()方法指定索引值取出物件,然而如果您的目的是要循序取出容器中所有的物件,则您可以使用Iterator类,Iterator类 实作 Iterator 模式,实际来看个例子: ● ArrayListDemo.java package onlyfun.caterpillar; import java.util.*; public class ArrayListDemo { public static void main(String[] args) { Scanner scanner = new Scanner(System.in); List list = new ArrayList(); System.out.println("输入名称(quit结束)"); while(true) { System.out.print("# "); String input = scanner.next(); if(input.equals("quit")) break; list.add(input); } Iterator iterator = list.iterator(); while(iterator.hasNext()) { System.out.print(iterator.next() + " "); } System.out.println(); } } iterator()方法会传回一个Iterator物件,这个物件提供的遍访的方法,hasNext()方法测试Iterator中是否还有物件,如果 有的话,可以 使用next()取出。 http://caterpillar.onlyfun.net/GossipCN/JavaGossip-V2/ArrayList.htm(第 2/3 页)2008-7-30 20:04:38 ArrayList 事实上,在J2SE 5.0您也不必须使用iterator()了,使用增强的for回圈可以直接遍访List的所有元素,例如: ● ArrayListDemo.java package onlyfun.caterpillar; import java.util.*; public class ArrayListDemo { public static void main(String[] args) { Scanner scanner = new Scanner(System.in); List list = new ArrayList(); System.out.println("输入名称(quit结束)"); while(true) { System.out.print("# "); String input = scanner.next(); if(input.equals("quit")) break; list.add(input); } for(String s : list) { System.out.print(s + " "); } System.out.println(); } } http://caterpillar.onlyfun.net/GossipCN/JavaGossip-V2/ArrayList.htm(第 3/3 页)2008-7-30 20:04:38 LinkedList From Gossip@caterpillar Java Gossip: LinkedList List类是以物件加入(add)容器的顺序来排列它们,如果您的物件加入之后大都是为了取出,而不会常作移除或插入(Insert)的动 作,则使用ArrayList,如果您会经常从容器中作移除或插入物件的动作,则使用LinkedList会获得较好的效能。 LinkedList实作了List介面,并增加了一些移除与插入物件的特定方法,像是addFirst()、addLast()、 getFirst()、getLast()、 removeFirst( )、removeLast()等等,由于在插入与移除时有较好的效能,适合拿来实作堆叠(Stack)与伫列(Queue)。 以下实作一个简单的FILO(First-In, Last-Out)堆叠,可以存入字串: ● StringStack.java package onlyfun.caterpillar; import java.util.*; public class StringStack { private LinkedList linkedList; public StringStack() { linkedList = new LinkedList(); } public void push(String name) { linkedList.addFirst(name); } public String top() { return linkedList.getFirst(); } public String pop() { return linkedList.removeFirst(); } public boolean isEmpty() { return linkedList.isEmpty(); } } 而对于FIFO(First-In, First-Out)的伫列,我们也可以使用LinkedList来实作: ● StringQueue.java http://caterpillar.onlyfun.net/GossipCN/JavaGossip-V2/LinkedList.htm(第 1/2 页)2008-7-30 20:04:42 LinkedList package onlyfun.caterpillar; import java.util.*; public class StringQueue { private LinkedList linkedList; public StringQueue() { linkedList = new LinkedList(); } public void put(String name) { linkedList.addFirst(name); } public String get() { return linkedList.removeLast(); } public boolean isEmpty() { return linkedList.isEmpty(); } } 事实上,如果您要使用伫列的功能,您也不用亲自实作,在J2SE 5.0中,LinkedList也实作了新加入的java.util.Queue介面,这个介面 有五个必须实作的方法: element() 取得但不移除伫列第一个元件,伫列为空时会丢出例外 offer() 加入一个元素至伫列中 peek() 取得但不移除伫列第一个元件 poll() 取得并移去伫列第一个元件,伫列为空时传回null remove() 取得并移除伫列第一个元件 要使用伫列的功能,您只要类似这样的宣告: Queue queue = new LinkedList(); http://caterpillar.onlyfun.net/GossipCN/JavaGossip-V2/LinkedList.htm(第 2/2 页)2008-7-30 20:04:42 HashSet From Gossip@caterpillar Java Gossip: HashSet HashSet实作Set介面,Set介面继承Collection介面,Set容器中的物件都是唯一的,加入 Set容器中的物件都必须重新定义equals()方 法,作为唯一性的识别,Set容器有自己的一套排序规则。 HashSet的排序规则是利用Hash Table,所以加入HashSet容器的物件还必须重新定义hashCode()方法,利用Hash的方式,可以让您快 速的找到容器中的物件,在比较两个加入Set容器中的物件是否相同时,会先比较hashCode()方法传回的值是否相同,如果相同,则 再使用equals()方法比较,如果两者都相同,则视为相同的物件。 事实上,在撰写新的类别时,最好总是重新定义equals()与hashCode()方法,以符合Java的设计规范,您可以参考 Object 类别 中的介 绍了解如何重新定义equals()与hashCode()。 来看一个例子: ● HashSetDemo.java package onlyfun.caterpillar; import java.util.*; public class HashSetDemo { public static void main(String[] args) { Set set = new HashSet(); set.add("caterpillar"); set.add("justin"); set.add("momor"); set.add("justin"); Iterator iterator = set.iterator(); while(iterator.hasNext()) { System.out.print(iterator.next() + " "); } System.out.println(); } } 执行结果: momor justin caterpillar 如上所示的,即使重覆加入了"justin"字串,HashSet中仍只有一个"justin"字串物件,另一个要注意的是,选代所有的值时,其顺序 与您加入的顺序是不一样的,选代所有值时的顺序是HashSet排序过后的顺序。 http://caterpillar.onlyfun.net/GossipCN/JavaGossip-V2/HashSet.htm(第 1/2 页)2008-7-30 20:04:46 HashSet LinkedHashSet是HashSet的子类,它在内部实作使用Hash Code进行排序,然而允许您在列举时行为像是LinkedList,简单的改写上面 的程式即可了解: ● LinkedHashSetDemo.java package onlyfun.caterpillar; import java.util.*; public class LinkedHashSetDemo { public static void main(String[] args) { Set set = new LinkedHashSet(); set.add("caterpillar"); set.add("justin"); set.add("momor"); set.add("justin"); Iterator iterator = set.iterator(); while(iterator.hasNext()) { System.out.print(iterator.next() + " "); } System.out.println(); } } 执行结果: caterpillar justin momor 可以在执行结果中看到的,选代时的顺序正是您加入值的顺序。 http://caterpillar.onlyfun.net/GossipCN/JavaGossip-V2/HashSet.htm(第 2/2 页)2008-7-30 20:04:46 TreeSet From Gossip@caterpillar Java Gossip: TreeSet TreeSet实作Set介面与SortedSet介面,提供相关的方法让您有序的取出对应位置的物件,像是 first()、last()等方法,TreeSet是J2SE中 唯一实作SortedSet介面的类别,它使用红黑树结构来对加入的物件进行排序。 看个简单的例子: ● TreeSetDemo.java package onlyfun.caterpillar; import java.util.*; public class TreeSetDemo { public static void main(String[] args) { Set set = new TreeSet(); set.add("justin"); set.add("caterpillar"); set.add("momor"); set.add("justin"); Iterator iterator = set.iterator(); while(iterator.hasNext()) { System.out.print(iterator.next() + " "); } System.out.println(); } } 由于加入的是String物件,执行结果会自动依字典顺序进行排序的动作: caterpillar justin momor 依字典顺序排序String物件是TreeSet预设的,如果您对物件有自己的一套排序顺序,您要实作一个 Comparator 物件,您要实作 compare()方法,它必须传回整数值,如果物件顺序相同则传回0,传回正整数表示compare()方法的第一个物件大于第二个物件,反 之则传回负整数。 举个实际的例子,假设您想要改变TreeSet依字典顺序排列加入的物件为相反的顺序: ● CustomComparator.java package onlyfun.caterpillar; import java.util.Comparator; http://caterpillar.onlyfun.net/GossipCN/JavaGossip-V2/TreeSet.htm(第 1/3 页)2008-7-30 20:04:52 TreeSet public class CustomComparator implements Comparator { public int compare(T o1, T o2) { if (((T) o1).equals(o2)) return 0; return ((Comparable) o1).compareTo((T) o2) * -1; } } 在自订的Comparator中,如果两个物件的顺序相同会传回0,这在TreeSet中表示两个物件是同一个物件,TreeSet要求传入的物件必须 实 作java.lang.Comparable介面,范例中只是简单的将原来compareTo()传回的值乘以负一,如此在TreeSet中就可以简单的 让排列顺 序相反。 在建构TreeSet实例时一并指定自订的Comparator,例如: ● TreeSetDemo2.java package onlyfun.caterpillar; import java.util.*; public class TreeSetDemo2 { public static void main(String[] args) { // 自订Comparator Comparator comparator = new CustomComparator(); Set set = new TreeSet(comparator); set.add("justin"); set.add("caterpillar"); set.add("momor"); // 使用 enhanced for loop 显示物件 for(String name : set) { System.out.print(name + " "); } System.out.println(); } } 执行的结果是相反的: momor justin caterpillar http://caterpillar.onlyfun.net/GossipCN/JavaGossip-V2/TreeSet.htm(第 2/3 页)2008-7-30 20:04:52 TreeSet http://caterpillar.onlyfun.net/GossipCN/JavaGossip-V2/TreeSet.htm(第 3/3 页)2008-7-30 20:04:52 EnumSet From Gossip@caterpillar Java Gossip: EnumSet EnumSet的名称说明了其作用,它是在J2SE 5.0后加入的新类别,可以协助您建立列举值的集合,它提供了一系列的静态方法,可以 让您指定不同的集合建立方式,例如: ● EnumSetDemo.java package onlyfun.caterpillar; import java.util.*; enum FontConstant { Plain, Bold, Italic } public class EnumSetDemo { public static void main(String[] args) { EnumSet enumSet = EnumSet.of(FontConstant.Plain, FontConstant.Bold); showEnumSet(enumSet); showEnumSet(EnumSet.complementOf(enumSet)); } public static void showEnumSet( EnumSet enumSet) { Iterator iterator = enumSet.iterator(); while(iterator.hasNext()) { System.out.print(iterator.next() + " "); } System.out.println(); } } 您可以指定列举值来加入EnumSet中,of()方法会返回一个EnumSet的实例,当中包括您所指定的列举值,您也可以使complementOf ()指定一个EnumSet的互补集,以下是执行的结果: Plain Bold Italic EnumSet实作了Set介面,所以您可以使用Set介面的所有方法来测试它所包括的列举值,例如测试一个集合中是否包括 FontConstant. Bold: if(enumSet.contains(FontConstant.Bold)) { .... } 您也可以建立一个空的EnumSet,然后自己逐个加入列举值,例如: http://caterpillar.onlyfun.net/GossipCN/JavaGossip-V2/EnumSet.htm(第 1/2 页)2008-7-30 20:04:58 EnumSet ● EnumSetDemo.java package onlyfun.caterpillar; import java.util.*; enum FontConstant { Plain, Bold, Italic } public class EnumSetDemo { public static void main(String[] args) { EnumSet enumSet = EnumSet.noneOf(FontConstant.class); enumSet.add(FontConstant.Bold); enumSet.add(FontConstant.Italic); showEnumSet(enumSet); } public static void showEnumSet( EnumSet enumSet) { Iterator iterator = enumSet.iterator(); while(iterator.hasNext()) { System.out.print(iterator.next() + " "); } System.out.println(); } } 执行结果: Bold Italic 您也可以由一个容器物件中建立EnumSet: List list = new ArrayList(); list.add(FontConstant.Bold); list.add(FontConstant.Italic); showEnumSet(EnumSet.copyOf(list)); 更多EnumSet相关的方法,您可以参考 EnumSet 线上API文件。 http://caterpillar.onlyfun.net/GossipCN/JavaGossip-V2/EnumSet.htm(第 2/2 页)2008-7-30 20:04:58 HashMap From Gossip@caterpillar Java Gossip: HashMap HashMap实作Map介面,内部实作使用Hash Table,让您在常数时间内可以寻得key/value对。 所谓的key/value对,简单的说,您将Map容器物件当作一个有很多间房间的房子,每个房间的门有一把钥匙,您将物件储存至房间 中时,要顺便拥有一把钥匙,下次要取回物件时,就是根据这把钥匙取得。 以一个简单的例子来作说明: ● HashMapDemo.java package onlyfun.caterpillar; import java.util.*; public class HashMapDemo { public static void main(String[] args) { Map map = new HashMap(); map.put("caterpillar", "caterpillar's message!!"); map.put("justin", "justin's message!!"); System.out.println(map.get("justin")); System.out.println(map.get("caterpillar")); } } 在宣告Map型态时,您指定了key/value各自的型态,这边都是宣告为String,也就是以String物件作为key物件的型态,而 value也是以 String物件作为其型态。 使用Map的put()方法将物件存入,必须同时指定key/value,而要取回物件时,则指定key,程式的执行结果如下: justin's message!! caterpillar's message!! HashMap是个被经常使用的物件,您可以参考下面几个例子中HashMap的应用: ● Command 模式 ● Thread-Specific Storage 模式 ● 控 制器(Servlet) 可以使用values()方法返回一个Collection物件,如果您需要一次选代Map中所有的物件,这会很有用,例如: ● HashMapDemo.java http://caterpillar.onlyfun.net/GossipCN/JavaGossip-V2/HashMap.htm(第 1/3 页)2008-7-30 20:05:02 HashMap package onlyfun.caterpillar; import java.util.*; public class HashMapDemo { public static void main(String[] args) { Map map = new HashMap(); map.put("justin", "justin's message!!"); map.put("momor", "momor's message!!"); map.put("caterpillar", "caterpillar's message!!"); Collection collection = map.values(); Iterator iterator = collection.iterator(); while(iterator.hasNext()) { System.out.println(iterator.next()); } } } 执行结果: momor's message!! justin's message!! caterpillar's message!! HashMap使用Hash Table,因而它有自己的排序方式,如果您想要在选代所有的物件时,依照插入的顺序来排序,则可以使用 LinkedHashMap,它是HashMap 的子类,使用values()所返回的Collection物件,其内含物件之顺序即为当初您加入物件之顺序,例 如: ● LinkedHashMapDemo.java package onlyfun.caterpillar; import java.util.*; public class LinkedHashMapDemo { public static void main(String[] args) { Map map = new LinkedHashMap(); map.put("justin", "justin's message!!"); map.put("momor", "momor's message!!"); map.put("caterpillar", "caterpillar's message!!"); Collection collection = map.values(); Iterator iterator = collection.iterator(); http://caterpillar.onlyfun.net/GossipCN/JavaGossip-V2/HashMap.htm(第 2/3 页)2008-7-30 20:05:02 HashMap while(iterator.hasNext()) { System.out.println(iterator.next()); } } } 执行结果: justin's message!! momor's message!! caterpillar's message!! http://caterpillar.onlyfun.net/GossipCN/JavaGossip-V2/HashMap.htm(第 3/3 页)2008-7-30 20:05:02 TreeMap From Gossip@caterpillar Java Gossip: TreeMap TreeMap实作Map介面与SortedMap介面,提供相关的方法让您有序的取出对应位置的物件,像是 firstKey()、lastKey()等方法, TreeMap是J2SE中唯一实作SortedMap介面的类别,它使用红黑树结构来对加入的物件进 行排序。 看个简单的例子: ● TreeMapDemo.java package onlyfun.caterpillar; import java.util.*; public class TreeMapDemo { public static void main(String[] args) { Map map = new TreeMap(); map.put("justin", "justin's message!!"); map.put("momor", "momor's message!!"); map.put("caterpillar", "caterpillar's message!!"); Collection collection = map.values(); Iterator iterator = collection.iterator(); while(iterator.hasNext()) { System.out.println(iterator.next()); } } } 由于您加入的是String物件,执行结果会自动依key的字典顺序进行排序的动作: caterpillar's message!! justin's message!! momor's message!! 依字典顺序排序String物件是TreeMap预设的,如果您对物件有自己的一套排序顺序,您要实作一个 Comparator 物件,它有两个必 须实作的方法,compare()与equals(),前者必须传回整数值,如果物件顺序相同则传回0,传回正整数表示compare ()方法的第一个 物件大于第二个物件,反之则传回负整数,后者则是定义两个物件是否相等。 http://caterpillar.onlyfun.net/GossipCN/JavaGossip-V2/TreeMap.htm2008-7-30 20:05:05 EnumMap From Gossip@caterpillar Java Gossip: EnumMap EnumMap是个专为列举型别设计的类别,方便您使用列举型别及Map物件,直接来举个实例: ● EnumMapDemo.java package onlyfun.caterpillar; import java.util.*; enum Action {TURN_LEFT, TURN_RIGHT, SHOOT} public class EnumMapDemo { public static void main(String[] args) { Map map = new EnumMap(Action.class); map.put(Action.TURN_LEFT, "向左转"); map.put(Action.TURN_RIGHT, "向右转"); map.put(Action.SHOOT, "射击"); for(Action action : Action.values( ) ) { System.out.println(map.get(action)); } } } 执行结果: 向左转 向右转 射击 与单纯的使用HashMap比较起来的差别是,在上面的程式中,EnumMap将根据列举的顺序来维护物件的排列顺序,从下面这个程式 可以看个大概: ● EnumMapDemo2.java package onlyfun.caterpillar; import java.util.*; enum Action {TURN_LEFT, TURN_RIGHT, SHOOT} public class EnumMapDemo2 { http://caterpillar.onlyfun.net/GossipCN/JavaGossip-V2/EnumMap.htm(第 1/2 页)2008-7-30 20:05:09 EnumMap public static void main(String[] args) { Map map = new EnumMap(Action.class); map.put(Action.SHOOT, "射击"); map.put(Action.TURN_RIGHT, "向右转"); map.put(Action.TURN_LEFT, "向左转"); for(String value : map.values( )) { System.out.println(value); } } } 执行结果: 向左转 向右转 射击 从遍访的结果可以看出,物件的顺序是根据列举顺序来排列的。 http://caterpillar.onlyfun.net/GossipCN/JavaGossip-V2/EnumMap.htm(第 2/2 页)2008-7-30 20:05:09 File From Gossip@caterpillar Java Gossip: File 不同的作业系统对于档案系统路径的设定各有差别,例如在Windows中,一个路径的表示法可能是: "c:\\Windows\\Fonts\\" 而在Linux下的路径设定可能是: "/home/justin/" Windows的路径指定是使用UNC(Universal Naming Convention)路径名,以\\开始表示磁碟根目录,如果没有以\\开始表示相对路 径,c是可选的磁碟指定,后面跟随着 : 字元。而UNIX-Like系统的路径指定以 / 开始表示绝对路径,不以 / 开始表示相对路径。 因而在程式中设定路径时会有系统相依性的问题,File类别提供一个抽象的、与系统独立的路径表示,您给它一个路径字串,它会 将它转换为与系统无关的抽象路径表示,这个路径可以指向一个档案、目录或是URI,您可以用以下四种方式来建构File的实例: File(File parent, String child) File(String pathname) File(String parent, String child) File(URI uri) 一个File的实例被建立时,它就不能再被改变内容;File类别除了用来表示一个档案或目录的抽象表示之外,它还提供了不少相关操 作方法,您可以用它来对档案系统作一些查询与设定的动作。 来看个简单的程式: ● FileDemo.java package onlyfun.caterpillar; import java.io.*; import java.util.*; public class FileDemo { public static void main(String[] args) { try { File file = new File(args[0]); if(file.isFile()) { // 是否为档案 System.out.println(args[0] + " 档案"); System.out.print( file.canRead() ? "可读 " : "不可读 "); System.out.print( file.canWrite() ? "可写 " : "不可写 "); System.out.println( file.length() + "位元组"); } http://caterpillar.onlyfun.net/GossipCN/JavaGossip-V2/FileClass.htm(第 1/3 页)2008-7-30 20:05:16 File else { // 列出所有的档案及目录 File[] files = file.listFiles(); ArrayList fileList = new ArrayList(); for(int i = 0; i < files.length; i++) { // 先列出目录 if(files[i].isDirectory()) { //是否为目录 // 取得路径名 System.out.println("[" + files[i].getPath() + "]"); } else { // 档案先存入fileList,待会再列出 fileList.add(files[i]); } } // 列出档案 for(File f: fileList) { System.out.println(f.toString()); } System.out.println(); } } catch(ArrayIndexOutOfBoundsException e) { System.out.println( "using: java FileDemo pathname"); } } } 执行结果: http://caterpillar.onlyfun.net/GossipCN/JavaGossip-V2/FileClass.htm(第 2/3 页)2008-7-30 20:05:16 File java onlyfun.caterpillar.FileDemo C:\ [C:\WINDOWS] [C:\Documents and Settings] [C:\Program Files] [C:\System Volume Information] [C:\Recycled] C:\A3N_A3L.10 C:\bootfont.bin C:\ntldr C:\NTDETECT.COM C:\boot.ini C:\CONFIG.SYS C:\AUTOEXEC.BAT C:\IO.SYS C:\MSDOS.SYS C:\Finish.log C:\pagefile.sys C:\VIRTPART.DAT http://caterpillar.onlyfun.net/GossipCN/JavaGossip-V2/FileClass.htm(第 3/3 页)2008-7-30 20:05:16 RandomAccessFile From Gossip@caterpillar Java Gossip: RandomAccessFile 档案存取通常是“循序的”,每在档案中存取一次,读取档案的位置就会相对于目前的位置前进,然而有时候您必须对档案的某个区 段进行读取或写入的动作,也就是进行“随机存取”(Random access),也就是说存取档案的位置要能在档案中随意的移动,这时您 可以使用RandomAccessFile,使用seek()方法来指定档案存取的位置,指定的单位是位元组,藉由它您就可以对档案进行随机存取 的动作。 为了方便,通常在随机存取档案时会固定每组资料的长度,例如一组学生个人资料,Java中并没有像C/C++中可以直接写入一个固定 长度结构(Structure)的方法,所以在固定每组长度的方面您必须自行设计。 下面这个程式示范了如何使用RandomAccessFile来写入档案,并随机读出一笔您所想读出的资料: ● Student.java package onlyfun.caterpillar; public class Student { private String name; // 固定 15 字元 private int score; public Student() { setName("noname"); } public Student(String name, int score) { setName(name); this.score = score; } public void setName(String name) { StringBuilder builder = null; if(name != null) builder = new StringBuilder(name); else builder = new StringBuilder(15); builder.setLength(15); this.name = builder.toString(); } public void setScore(int score) { this.score = score; http://caterpillar.onlyfun.net/GossipCN/JavaGossip-V2/RandomAccessFile.htm(第 1/3 页)2008-7-30 20:05:20 RandomAccessFile } public String getName() { return name; } public int getScore() { return score; } // 每笔资料固定写入34位元组 public static int size() { return 34; } } ● RandomAccessFileDemo.java package onlyfun.caterpillar; import java.io.*; import java.util.*; public class RandomAccessFileDemo { public static void main(String[] args) { Student[] students = { new Student("Justin", 90), new Student("momor", 95), new Student("Bush", 88), new Student("caterpillar", 84)}; try { File file = new File(args[0]); // 建立RandomAccessFile实例并以读写模式开启档案 RandomAccessFile randomAccessFile = new RandomAccessFile(file, "rw"); for(int i = 0; i < students.length; i++) { randomAccessFile.writeChars(students[i].getName()); randomAccessFile.writeInt(students[i].getScore()); } Scanner scanner = new Scanner(System.in); System.out.print("读取第几笔资料?"); http://caterpillar.onlyfun.net/GossipCN/JavaGossip-V2/RandomAccessFile.htm(第 2/3 页)2008-7-30 20:05:20 RandomAccessFile int num = scanner.nextInt(); randomAccessFile.seek((num-1) * Student.size()); Student student = new Student(); student.setName(readName(randomAccessFile)); student.setScore(randomAccessFile.readInt()); System.out.println("姓名:" + student.getName()); System.out.println("分数:" + student.getScore()); randomAccessFile.close(); } catch(ArrayIndexOutOfBoundsException e) { System.out.println("请指定档案名称"); } catch(IOException e) { e.printStackTrace(); } } private static String readName( RandomAccessFile randomAccessfile) throws IOException { char[] name = new char[15]; for(int i = 0; i < name.length; i++) name[i] = randomAccessfile.readChar(); return new String(name).replace('\0', ' '); } } 在实例化一个RandomAccessFile物件时,要设定档案开启的方式,设定"r"表示只供读取,设定"rw"表示可读可写;为了让每组资料 长度固 定,在写入name时,我们使用 StringBuilder 并设定其长度固定为15个字元,而读回name时则直接读回15个字元,然后再去 掉空白字元传回。 http://caterpillar.onlyfun.net/GossipCN/JavaGossip-V2/RandomAccessFile.htm(第 3/3 页)2008-7-30 20:05:20 InputStream、 OutputStream From Gossip@caterpillar Java Gossip: InputStream、 OutputStream 电脑中的资料都是以0与1的方式来储存,如果您要在两个装置之间进行资料的存取,当然也是以0与1位元的方式来进行,实际上资 料的流动是透过电路,而上面 的资料则是电流,而在程式上来说,将资料目的地与来源之间抽象化为一个串流(Stream),而当中 流动的则是位元资料。 01010101 Stream --> 来源地 ===================== 目的地 在Java中有两个类别用来作串流的抽象表示:InputStream与OutputStream。 InputStream是所有表示位元输入串流的类别之父类别,它是一个抽象类别,子类会重新定义它当中所定义的方法, InputStream用于 从装置来源地读取资料的抽象表示,例如System中的标准输入串流 in 物件就是一个 InputStream,在程式开始之后,这个串流物件就 会开启,以从标准输入装置中读取资料,这个装置通常是键盘或是其它使用者定义的装置。 OutputStream是所有表示位元输出串流的类别之父类别,它是一个抽象类别,子类会重新定义它当中所定义的方法, OutputStream是 用于将资料写入目的地的抽象表示,例如System中的标准输出串流物件 out ,out 的类型是PrintStream, 这个类别是OutputStream 的子类别(FilterOutputStream继承OutputStream, PrintStream再继承FilterOutputStream),在程式开始之后,这个串流物件就会开 启,您可以将资料透过它来写入目的地装置,这 个装置通常是荧幕或其它使用者定义的装置。 下面程式可以读取键盘输入串流,并将资料以10进位方式显示在荧幕上: ● StreamDemo.java package onlyfun.caterpillar; import java.io.*; public class StreamDemo { public static void main(String[] args) { try { System.out.print("输入字元: "); System.out.println("输入字元十进位表示: " + System.in.read()); System.out.println("换行字元十进位表示: " + System.in.read()); } catch(IOException e) { e.printStackTrace(); } } } 执行结果: http://caterpillar.onlyfun.net/GossipCN/JavaGossip-V2/InOutputStream.htm(第 1/2 页)2008-7-30 20:05:24 InputStream、 OutputStream 输入字元: A 输入字元十进位表示: 65 换行字元十进位表示: 10 字元A输入后被标准输入串流读取,A的位元表示以十进位来看就是65,这是A字元的编码(查查ASCII编码表就知道了),在这边 要注意的是read()只读取一个位元组的资料,而当输入A并按Enter键时,实际上在串流中会有A的位元资料与换行字元的位元资料, 换行字元的位元资料以十进位来表示的话就是10。 作业系统之间的换行字元各不相同,Windows 为"\r\n",Linux 为'\n',而 Mac 为'\r'。 http://caterpillar.onlyfun.net/GossipCN/JavaGossip-V2/InOutputStream.htm(第 2/2 页)2008-7-30 20:05:24 FileInputStream、 FileOutputStream From Gossip@caterpillar Java Gossip: FileInputStream、 FileOutputStream FileInputStream是InputStream的子类,由名称上就可以知道, FileInputStream主要就是从指定的档案中读取资料至目的地。 FileOutputStream是OutputStream的子类,顾名思义,FileInputStream主要就是从来源地写入资料至指定的档案中。 标准输入输出串流物件在程式一开始就会开启,但只有当您建立一个FileInputStream或FileOutputStream的实例时,实际的串流才会 开启,而不使用串流时,也必须自行关闭串流,以释放与串流相依的系统资源。 下面这个程式可以复制档案,程式先从来源档案读取资料至一个位元缓冲区中,然后再将位元阵列的资料写入目的档案: ● FileStreamDemo.java package onlyfun.caterpillar; import java.io.*; public class FileStreamDemo { public static void main(String[] args) { try { byte[] buffer = new byte[1024]; FileInputStream fileInputStream = new FileInputStream(new File(args[0])); FileOutputStream fileOutputStream = new FileOutputStream(new File(args[1])); System.out.println("复制档案:" + fileInputStream.available() + "位元组"); while(true) { // 从来源档案读取资料至缓冲区 int length = fileInputStream.read(buffer); if(length == buffer.length) { // 将阵列资料写入目的档案 fileOutputStream.write(buffer); } else { byte[] buf = new byte[length]; System.arraycopy(buffer, 0, buf, 0, length); fileOutputStream.write(buf); break; } } http://caterpillar.onlyfun.net/GossipCN/JavaGossip-V2/FileInOutStream.htm(第 1/2 页)2008-7-30 20:05:29 FileInputStream、 FileOutputStream // 关闭串流 fileInputStream.close(); fileOutputStream.close(); System.out.println("复制完成"); } catch(ArrayIndexOutOfBoundsException e) { System.out.println( "using: java FileStreamDemo src des"); e.printStackTrace(); } catch(IOException e) { e.printStackTrace(); } } } 这个程式示范了两个 read() 方法,一个可以读入指定长度的资料至阵列,一个一次可以读入一个位元组,每次读取之后,读取的指 标都会往前进,您使用available()方法获得还有多少位元组可以读取;除了使用File来建立FileInputStream、FileOutputStream的实例 之外,您也可以直接使用字串指定路径来建立。 不使用串流时,记得使用close()方法自行关闭串流,以释放与串流相依的系统资源。 http://caterpillar.onlyfun.net/GossipCN/JavaGossip-V2/FileInOutStream.htm(第 2/2 页)2008-7-30 20:05:29 BufferedInputStream、 BufferedOutputStream From Gossip@caterpillar Java Gossip: BufferedInputStream、 BufferedOutputStream 在介绍 FileInputStream、 FileOutputStream的 例子中,您使用了一个阵列来作为资料读入的缓冲区,以档案存取为例的话,您知 道磁碟存取的速度是远低于记忆体中的资料存取速度,为了减少对磁碟的存 ,您一次读入一定长度的资料,如上一个主题范例中的 1024位元组,而写入时也是一次写入一定长度的资料,这可以增加资料存取的效率。 BufferedInputStream与BufferedOutputStream可以为InputStream类的物件增加缓冲区功能,使用它们,您无需自行设计缓冲区。 BufferedInputStream的资料成员buf是个位元阵列,预设为2048位元组大小,当读取资料来源时,例如档案, BufferedInputStream会 尽量将buf填满,当使用read()方法时,实际上是先读取buf中的资料,而不是直接对资料来源作读取,当buf中的资料不足时, BufferedInputStream才会再从资料来源中提取资料。 BufferedOutputStream的资料成员buf是个位元阵列,预设为512个位元组,当写入资料时,会先将资料存至buf中,当buf已满时才会 一次将资料写至目的地,而不是每次写入都对目的地作写入。 将上一个主题的范例作个改写,这次不用自行设定缓冲区并进行判断了,使用BufferedInputStream、 BufferedOutputStream让程式看 来简单一些,也比较有效率: ● BufferedStreamDemo.java package onlyfun.caterpillar; import java.io.*; public class BufferedStreamDemo { public static void main(String[] args) { try { byte[] data = new byte[1]; File srcFile = new File(args[0]); File desFile = new File(args[1]); BufferedInputStream bufferedInputStream = new BufferedInputStream( new FileInputStream(srcFile)); BufferedOutputStream bufferedOutputStream = new BufferedOutputStream( new FileOutputStream(desFile)); System.out.println("复制档案:" + srcFile.length() + "位元组"); while(bufferedInputStream.read(data) != -1) { bufferedOutputStream.write(data); } http://caterpillar.onlyfun.net/GossipCN/JavaGossip-V2/BufferedInOutStream.htm(第 1/2 页)2008-7-30 20:05:33 BufferedInputStream、 BufferedOutputStream // 将缓冲区中的资料全部写出 bufferedOutputStream.flush(); // 关闭串流 bufferedInputStream.close(); bufferedOutputStream.close(); } catch(ArrayIndexOutOfBoundsException e) { System.out.println( "using: java UseFileStream src des"); e.printStackTrace(); } catch(IOException e) { e.printStackTrace(); } } } 为了确保缓冲区中的资料一定被写出,建议最后执行flush()将缓冲区中的资料全部写出目的串流中。 BufferedInputStream、BufferedOutputStream并没有改变来源InputStream或目的 OutputStream的行为,读入或写出时的动作还是 InputStream、OutputStream负责, BufferedInputStream、BufferedOutputStream只是在这之前动态的为它们加上一些功能(像是缓冲区 功能),在这边是 以档案存取串流为例,实际上您可以在其它串流物件上加上BufferedInputStream、BufferedOutputStream功能。 http://caterpillar.onlyfun.net/GossipCN/JavaGossip-V2/BufferedInOutStream.htm(第 2/2 页)2008-7-30 20:05:33 DataInputStream、 DataOutputStream From Gossip@caterpillar Java Gossip: DataInputStream、DataOutputStream DataInputStream、DataOutputStream可提供一些对Java基本资料型态写入的方法,像是读写int、double、 boolean等的方法,由于Java 的资料型态大小是规定好的,在写入或读出这些基本资料型态时,就不用担心不同平台间资料大小不同的问题。 这边还是举档案存取来进行说明,有时候您只是要储存一个物件的成员资料,而不是整个物件的资讯,成员资料的型态假设都是 Java的基本资料型态,您不必要 使用Object输入、输出相关串流物件,而可以使用DataInputStream、DataOutputStream来写入或读出 资料,下面这个程式 是个简单的示范: ● Student.java package onlyfun.caterpillar; public class Student { private String name; private int score; public Student() { name = "N/A"; } public Student(String name, int score) { this.name = name; this.score = score; } public void setName(String name) { this.name = name; } public void setScore(int score) { this.score = score; } public String getName() { return name; } public int getScore() { return score; } http://caterpillar.onlyfun.net/GossipCN/JavaGossip-V2/DataInOutStream.htm(第 1/3 页)2008-7-30 20:05:37 DataInputStream、 DataOutputStream public void showData() { System.out.println("name: " + name); System.out.println("score: " + score); } } ● DataStreamDemo.java package onlyfun.caterpillar; import java.io.*; public class DataStreamDemo { public static void main(String[] args) { Student[] students = {new Student("Justin", 90), new Student("momor", 95), new Student("Bush", 88)}; try { DataOutputStream dataOutputStream = new DataOutputStream( new FileOutputStream("data.dat")); for(Student student : students) { dataOutputStream.writeUTF(student.getName()); dataOutputStream.writeInt(student.getScore()); } dataOutputStream.flush(); dataOutputStream.close(); DataInputStream dataInputStream = new DataInputStream( new FileInputStream("data.dat")); for(int i = 0; i < students.length; i++) { String name = dataInputStream.readUTF(); int score = dataInputStream.readInt(); students[i] = new Student(name, score); students[i].showData(); } dataInputStream.close(); } catch(IOException e) { e.printStackTrace(); } http://caterpillar.onlyfun.net/GossipCN/JavaGossip-V2/DataInOutStream.htm(第 2/3 页)2008-7-30 20:05:37 DataInputStream、 DataOutputStream } } 这个程式在写入档案时,只提取物件的成员资料,而在读出时将这些资料读出,并将读回的资料设定给一个实例,是物件资料还原 的一种方式。 http://caterpillar.onlyfun.net/GossipCN/JavaGossip-V2/DataInOutStream.htm(第 3/3 页)2008-7-30 20:05:37 ObjectInputStream、ObjectOutputStream From Gossip@caterpillar Java Gossip: ObjectInputStream、ObjectOutputStream 在Java这样支援物件导向的程式中撰写程式,很多资料都是以物件的方式存在,在程式运行过后,您会希望将这些资料加以储存, 以供下次执行程式时使用,这时您可以使用ObjectInputStream、ObjectOutputStream来进行这项工作。 要被储存的物件必须实作Serializable介面,说是实作,其实Serializable中并没有规范任何必须实作的方法,所以这边所谓实作的意 义,其实像是对物件贴上一个标志,代表该物件是可以序列化的(Serializable)。 一个实作的例子如下所示: ● Student.java package onlyfun.caterpillar; import java.io.*; public class Student implements Serializable { private static final long serialVersionUID = 1L; private String name; private int score; public Student() { name = "N/A"; } public Student(String name, int score) { this.name = name; this.score = score; } public void setName(String name) { this.name = name; } public void setScore(int score) { this.score = score; } public String getName() { return name; } public int getScore() { return score; } public void showData() { System.out.println("name: " + name); http://caterpillar.onlyfun.net/GossipCN/JavaGossip-V2/ObjectInOutStream.htm(第 1/5 页)2008-7-30 20:05:41 ObjectInputStream、ObjectOutputStream System.out.println("score: " + score); } } 您要注意到serialVersionUID,这代表了可序列化物件的版本, 如果您没有提供这个版本讯息,则会自动依类名称、实现的介面、 成员等讯息来产生,如果是自动产生的,则下次您更改了Student类,则自动产生的 serialVersionUID也会跟着变更,当反序列化时两 个serialVersionUID不相同的话,就会丢出 InvalidClassException,如果您想要维持版本讯息的一致,则要显式宣告serialVersionUID。 ObjectInputStream、ObjectOutputStream为InputStream、OutputStream加上了可以让使用者写入 物件、读出物件的功能,在写入物件 时,我们使用writeObject()方法,读出物件时我们使用readObject()方法,被读出的物件都是以 Object的型态传回,您必须将之转换为 物件原来的型态,才能正确的操作被读回的物件,下面这个程式示范了如何简单的储存物件至档案中,并将之再度读 回 : ● ObjectStreamDemo.java package onlyfun.caterpillar; import java.io.*; import java.util.*; public class ObjectStreamDemo { public static void writeObjectsToFile( Object[] objs, String filename) { File file = new File(filename); try { ObjectOutputStream objOutputStream = new ObjectOutputStream( new FileOutputStream(file)); for(Object obj : objs) { objOutputStream.writeObject(obj); } objOutputStream.close(); } catch(IOException e) { e.printStackTrace(); } } public static Object[] readObjectsFromFile( String filename) throws FileNotFoundException { File file = new File(filename); if(!file.exists()) throw new FileNotFoundException(); http://caterpillar.onlyfun.net/GossipCN/JavaGossip-V2/ObjectInOutStream.htm(第 2/5 页)2008-7-30 20:05:41 ObjectInputStream、ObjectOutputStream List list = new ArrayList(); try { FileInputStream fileInputStream = new FileInputStream(file); ObjectInputStream objInputStream = new ObjectInputStream(fileInputStream); while(fileInputStream.available() > 0) { list.add(objInputStream.readObject()); } objInputStream.close(); } catch(ClassNotFoundException e) { e.printStackTrace(); } catch(IOException e) { e.printStackTrace(); } return list.toArray(); } public static void appendObjectsToFile( Object[] objs, String filename) throws FileNotFoundException { File file = new File(filename); if(!file.exists()) throw new FileNotFoundException(); try { ObjectOutputStream objOutputStream = new ObjectOutputStream( new FileOutputStream(file, true)) { protected void writeStreamHeader() throws IOException {} }; for(Object obj : objs) { objOutputStream.writeObject(obj); } objOutputStream.close(); } http://caterpillar.onlyfun.net/GossipCN/JavaGossip-V2/ObjectInOutStream.htm(第 3/5 页)2008-7-30 20:05:41 ObjectInputStream、ObjectOutputStream catch(IOException e) { e.printStackTrace(); } } public static void main(String[] args) { Student[] students = {new Student("caterpillar", 90), new Student("justin", 85)}; // 写入新档 writeObjectsToFile(students, "data.dat"); try { // 读取档案资料 Object[] objs = readObjectsFromFile("data.dat"); for(Object obj : objs) { ((Student) obj).showData(); } System.out.println(); students = new Student[2]; students[0] = new Student("momor", 100); students[1] = new Student("becky", 100); // 附加至档案 appendObjectsToFile(students, "data.dat"); // 读取档案资料 objs = readObjectsFromFile("data.dat"); for(Object obj : objs) { ((Student) obj).showData(); } } catch(FileNotFoundException e) { e.printStackTrace(); } } } 物件被写出时,会写入物件的类别型态、类别署名(Class signature),static与被标志为transient的成员则不会被写入。 在这边注意到以附加的形式写入资料至档案时,在试图将物件附加至一个先前已写入物件的档案时,由于ObjectOutputStream在 写入 资料时,还会加上一个特别的标示头,而读取档案时会检查这个标示头,如果一个档案中被多次附加物件,那么该档案中会有多个 标示头,如此读取检查时就会 发现不一致,这会丢出StreamCorrupedException,为此,您重新定义ObjectOutputStream的 writeStreamHeader()方法,如果是以附加的方式来写入物件,就不写入标示头: http://caterpillar.onlyfun.net/GossipCN/JavaGossip-V2/ObjectInOutStream.htm(第 4/5 页)2008-7-30 20:05:41 ObjectInputStream、ObjectOutputStream ObjectOutputStream objOutputStream = new ObjectOutputStream( new FileOutputStream(file, true)) { protected void writeStreamHeader() throws IOException {} }; 将物件写出或读入并不仅限于档案存取,您也可以用于网路的资料传送,例如传送整个物件资料或是影像档案。 http://caterpillar.onlyfun.net/GossipCN/JavaGossip-V2/ObjectInOutStream.htm(第 5/5 页)2008-7-30 20:05:41 SequenceInputStream From Gossip@caterpillar Java Gossip: SequenceInputStream 您将一个档案分割为数个档案,接下来要将之再度组合还原为原来的档案,最基本的作法是使用数个 FileInputStream来开启分割后 的档案,然后一个一个档案的读取,并连续写入至同一个FileOutputStream中,在这中间,您必须 要自行判断每一个分割档案的读取 是否完毕,如果完毕就换读取下一个档案。 如果您使用SequenceInputStream就不用这么麻烦,SequenceInputStream可以看作是数个 InputStream物件的组合,当一个InputStream 物件的内容读取完毕后,它就会取出下一个InputStream物件,直到所有的 InputStream物件都读取完毕为止。 下面这个程式是SequenceInputStream的使用示范,它可以将指定的档案进行分割,也可以将分割后的档案还原为一个档案: ● SequenceStreamDemo.java package onlyfun.caterpillar; import java.util.*; import java.io.*; public class SequenceStreamDemo { public static void main(String[] args) { try { // args[0]: 指定分割(s)或连接(c) switch (args[0].charAt(1)) { case 's': // args[1]: 每个分割档案的大小 int size = Integer.parseInt(args[1]); // args[2]: 指定要被分割的档案名称 seperate(args[2], size); break; case 'c': // args[1]: 指定要被组合的档案个数 int number = Integer.parseInt(args[1]); // args[2]: 组合后的档案名称 concatenate(args[2], number); break; } } catch(ArrayIndexOutOfBoundsException e) { System.out.println( "Using: java UseSequenceStream [-s/-c]" + " (size/number) filename"); System.out.println("-s: 分割档案\n-c: 组合档案"); http://caterpillar.onlyfun.net/GossipCN/JavaGossip-V2/SequenceInputStream.htm(第 1/4 页)2008-7-30 20:05:45 SequenceInputStream } catch(IOException e) { e.printStackTrace(); } } // 分割档案 public static void seperate(String filename, int size) throws IOException { FileInputStream fileInputStream = new FileInputStream(new File(filename)); BufferedInputStream bufInputStream = new BufferedInputStream(fileInputStream); byte[] data = new byte[1]; int count = 0; // 从原档案大小及指定分割的大小 // 决定要分割为几个档案 if(fileInputStream.available() % size == 0) count = fileInputStream.available() / size; else count = fileInputStream.available() / size + 1; // 开始进行分割 for(int i = 0; i < count; i++) { int num = 0; // 分割的档案加上底线与编号 File file = new File(filename + "_" + (i + 1)); BufferedOutputStream bufOutputStream = new BufferedOutputStream( new FileOutputStream(file)); while(bufInputStream.read(data) != -1) { bufOutputStream.write(data); num++; if(num == size) { // 分割出一个档案 bufOutputStream.flush(); bufOutputStream.close(); break; } } if(num < size) { bufOutputStream.flush(); http://caterpillar.onlyfun.net/GossipCN/JavaGossip-V2/SequenceInputStream.htm(第 2/4 页)2008-7-30 20:05:45 SequenceInputStream bufOutputStream.close(); } } System.out.println("分割为" + count + "个档案"); } // 连接档案 public static void concatenate(String filename, int number) throws IOException { // 收集档案用的List List list = new ArrayList(); for(int i = 0; i < number; i++) { // 档案名必须为底线加上编号 File file = new File(filename + "_" + (i+1)); list.add(i, new FileInputStream(file)); } final Iterator iterator = list.iterator(); // SequenceInputStream 需要一个Enumeration物件来建构 Enumeration enumation = new Enumeration() { public boolean hasMoreElements() { return iterator.hasNext(); } public InputStream nextElement() { return iterator.next(); } }; // 建立SequenceInputStream // 并使用BufferedInputStream BufferedInputStream bufInputStream = new BufferedInputStream( new SequenceInputStream(enumation), 8192); BufferedOutputStream bufOutputStream = new BufferedOutputStream( new FileOutputStream(filename), 8192); byte[] data = new byte[1]; http://caterpillar.onlyfun.net/GossipCN/JavaGossip-V2/SequenceInputStream.htm(第 3/4 页)2008-7-30 20:05:45 SequenceInputStream // 读取所有档案资料并写入目的地档案 while(bufInputStream.read(data) != -1) bufOutputStream.write(data); bufInputStream.close(); bufOutputStream.flush(); bufOutputStream.close(); System.out.println("组合" + number + "个档案 OK!!"); } } 分割档案时的范例如下: java onlyfun.caterpillar.SequenceStreamDemo -s 1048576 test.zip 分割为6个档案 组合档案时的范例如下: java onlyfun.caterpillar.SequenceStreamDemo -c 6 test.zip 组合6个档案 OK!! http://caterpillar.onlyfun.net/GossipCN/JavaGossip-V2/SequenceInputStream.htm(第 4/4 页)2008-7-30 20:05:45 PrintStream From Gossip@caterpillar Java Gossip: PrintStream 之前所介绍过的Stream输出物件,都是直接将记忆体中的资料写出至目的地(例如一个档案),举个例子来说,如果您将 int 整数 1 使用之前介绍的Stream物件输出至档案,则档案中所储存的是 int 整数 1 在记忆体中的值,例如: ● FileStream.java package onlyfun.caterpillar; import java.io.*; public class FileStreamDemo { public static void main(String[] args) throws IOException { FileOutputStream file = new FileOutputStream( new File("test.txt")); file.write(1); file.close(); } } 由于您使用write()方法,这会将 1 在记忆体中的值之低位元组0000001写入档案中,所以如果您使用文字编辑软体(像vi或 UltraEdit)观看test.txt的16进位表示,其结果会显示 01(16进位表示)。 有时候您所想要储存的结果是转换为字元之后的结果,例如若程式的执行结果是3.14159,您会希望使用字元来储存3.14159,也就是 俗称的储存为纯文字档案,如此当您使用简单的纯文字编辑器观看时,就可以直接看到程式执行的结果。 例如您若想使用纯文字档案看到test.txt的显示结果是1,则必须先将记忆体中的整数1,也就是二进位00000000 00000000 00000000 00000001转换为对应的字元编码,也就是0x31(十进位表示49)并加以储存。 使用PrintStream可以自动为您进行字元转换的动作,它会使用作业系统的预设编码来处理对应的字元转换动作,直接使用下面这个 例子来作示范: ● PrintStreamDemo.java package onlyfun.caterpillar; import java.io.*; public class PrintStreamDemo { public static void main(String[] args) throws FileNotFoundException { PrintStream printStream = new PrintStream( new FileOutputStream( http://caterpillar.onlyfun.net/GossipCN/JavaGossip-V2/PrintStream.htm(第 1/2 页)2008-7-30 20:05:49 PrintStream new File("pi.txt"))); printStream.print("PI = "); printStream.println(Math.PI); printStream.close(); } } 执行程式之后使用纯文字编辑器开启pi.txt,其内容会是PI = 3.141592653589793,print()或println()接受int、char、String、double等等 资料型态, println()会在输出之后加上换行字元,而print()则不会。 注意在档案储存上实际并没有二进位档案或是纯文字档案的分别,所有的档案所储存的都是二进位的资料,您俗称的纯文字档案, 其实正确的说,是指储存的结果是经过字元转换,例如将 int 整数 1转换为字元 '1' 的编码结果并加以储存。 http://caterpillar.onlyfun.net/GossipCN/JavaGossip-V2/PrintStream.htm(第 2/2 页)2008-7-30 20:05:49 ByteArrayInputStream、ByteArrayOutputStream From Gossip@caterpillar Java Gossip: ByteArrayInputStream、ByteArrayOutputStream 串流的来源或目的地不一定是档案,也可以是记忆体中的一个空间,例如一个位元阵列, ByteArrayInputStream、 ByteArrayOutputStream即是将位元阵列当作串流输入来源、输出目的地的工具类别。 ByteArrayInputStream可以将一个阵列当作串流输入的来源,而ByteArrayOutputStream则可以将一个位元阵列当作串流输出的目的 地,这两个类别基本上比较少使用,在这边举一个简单的档案位元编辑程式作为例子。 您开启一个简单的文字档案,当中有简单的ABCDEFG等字元,在读取档案之后,您可以直接以程式来指定档案的位元位置,以修 改您所指定的字元,程式的作法是将档案读入阵列中,修改位置的指定被用作阵列的指针,在修改完阵列内容之后,您重新将阵列 存回档案,范例如下: ● ByteArrayStreamDemo.java package onlyfun.caterpillar; import java.io.*; import java.util.*; public class ByteArrayStreamDemo { public static void main(String[] args) { try { File file = new File(args[0]); BufferedInputStream bufferedInputStream = new BufferedInputStream( new FileInputStream(file)); // 将档案读入位元阵列 ByteArrayOutputStream arrayOutputStream = new ByteArrayOutputStream(); byte[] bytes = new byte[1]; while(bufferedInputStream.read(bytes) != -1) { arrayOutputStream.write(bytes); } arrayOutputStream.close(); bufferedInputStream.close(); // 显示位元阵列内容 bytes = arrayOutputStream.toByteArray(); for(byte b : bytes) { System.out.print((char) b); } System.out.println(); http://caterpillar.onlyfun.net/GossipCN/JavaGossip-V2/ByteArrayInOutStream.htm(第 1/2 页)2008-7-30 20:05:53 ByteArrayInputStream、ByteArrayOutputStream // 让使用者输入位置与字元修改位元阵列内容 Scanner scanner = new Scanner(System.in); System.out.print("输入修改位置:"); int pos = scanner.nextInt(); System.out.print("输入修改字元:"); bytes[pos-1] = (byte) scanner.next().charAt(0); // 将位元阵列内容存回档案 ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(bytes); BufferedOutputStream bufOutputStream = new BufferedOutputStream( new FileOutputStream(file)); byte[] tmp = new byte[1]; while(byteArrayInputStream.read(tmp) != -1) bufOutputStream.write(tmp); byteArrayInputStream.close(); bufOutputStream.flush(); bufOutputStream.close(); } catch(ArrayIndexOutOfBoundsException e) { e.printStackTrace(); } catch(IOException e) { e.printStackTrace(); } } } 执行结果: java onlyfun.caterpillar.ByteArrayStreamDemo test.txt ABCDEFG 输入修改位置:2 输入修改字元:K 再开启test.txt,您会发现B已经被覆盖为K。 http://caterpillar.onlyfun.net/GossipCN/JavaGossip-V2/ByteArrayInOutStream.htm(第 2/2 页)2008-7-30 20:05:53 PushbackInputStream From Gossip@caterpillar Java Gossip: PushbackInputStream PushbackInputStream拥有一个PushBack缓冲区,您从这个物件读出资料后,如果愿意的话,只要PushBack缓冲区没有满,就可以使 用unread()将资料回推回串流的前端。 举个使用PushbackInputStream的例子,假设一个文字档案中同时储存有ASCII码与BIG5中文字,您希望判断出那些位置是 ASCII而那 些位置是BIG5中文字的位置,BIG5中文字使用两个位元组来表示,而ASCII只使用一个。 BIG5中文为了与ASCII相容,采低位元组范围为0xA4至0xF9,而高位元组为0x40到0x7E以及0xA1至0xFE,储存时低位元组先存, 再存高位元组,所以读取时只要先读到位元组是在0xA4至0xF9,就表示它可能是一个中文字的前半。 为了说明PushbackInputStream的功能,您的范例则是一次从档案中读取两个位元组,并检查其是否在0xA440与 0xFFFF之间,以简单 的判断其是否为BIG5码,如果不在这个范围之内,则可能是个ASCII范围内的字元,您显示它,并将读出的第二个位元组推回串 流,以待下一次可以重新读取 。 ● PushbackStreamDemo.java package onlyfun.caterpillar; import java.io.*; public class PushbackStreamDemo { public static void main(String[] args) { try { PushbackInputStream pushbackInputStream = new PushbackInputStream( new FileInputStream(args[0])); byte[] array = new byte[2]; int tmp = 0; int count = 0; while((count = pushbackInputStream.read(array)) != -1) { // 两个位元组转换为整数 tmp = (short)((array[0] << 8) | (array[1] & 0xff)); tmp = tmp & 0xFFFF; // 判断是否为BIG5,如果是则显示BIG5中文字 if(tmp >= 0xA440 && tmp < 0xFFFF) { System.out.println("BIG5: " + new String(array)); } else { http://caterpillar.onlyfun.net/GossipCN/JavaGossip-V2/PushbackInputStream.htm(第 1/2 页)2008-7-30 20:05:57 PushbackInputStream // 将第二个位元组推回串流 pushbackInputStream.unread(array, 1, 1); // 显示ASCII范围的字元 System.out.println("ASCII: " + (char)array[0]); } } pushbackInputStream.close(); } catch(ArrayIndexOutOfBoundsException e) { System.out.println("请指定档案名称"); } catch(IOException e) { e.printStackTrace(); } } } 假设我们的文字档案中有以下的文字:"这T是e一s个t测试" 执行结果会是: BIG5: 这 ASCII: T BIG5: 是 ASCII: e BIG5: 一 ASCII: s BIG5: 个 ASCII: t BIG5: 测 BIG5: 试 http://caterpillar.onlyfun.net/GossipCN/JavaGossip-V2/PushbackInputStream.htm(第 2/2 页)2008-7-30 20:05:57 Reader、Writer From Gossip@caterpillar Java Gossip: Reader、Writer Reader、Writer支援Unicode标准字元集(Character set)(位元组串流则只支援ISO-Latin-1 8-bit),在处理串流时,会根据系统预 设的字元编码来进行字元转换,它们是抽象类别,真正您会使用其子类别,子类别通常会重新定义相关的方法。 在 PushbackInputStream 中,您读入一个含BIG5中文字及ASCII字元的文字档案,这边改写一下这个例子,使用Reader的子类别 InputStreamReader来转换读入的两个位元组为中文字元,并显示在荧幕上: ● ReaderWriterDemo.java package onlyfun.caterpillar; import java.io.*; public class ReaderDemo { public static void main(String[] args) { try { PushbackInputStream pushbackInputStream = new PushbackInputStream( new FileInputStream(args[0])); byte[] array = new byte[2]; ByteArrayInputStream byteArrayStream = new ByteArrayInputStream(array); // reader会从已读的位元阵列中取出资料 InputStreamReader reader = new InputStreamReader(byteArrayStream); int tmp = 0; int count = 0; while((count = pushbackInputStream.read(array)) != -1) { // 两个位元组转换为整数 tmp = (short)((array[0] << 8) | (array[1] & 0xff)); tmp = tmp & 0xFFFF; // 判断是否为BIG5,如果是则显示BIG5中文字 if(tmp >= 0xA440 && tmp < 0xFFFF) { System.out.println("BIG5: " + (char)reader.read()); // 重置ArrayInputStream的读取游标 http://caterpillar.onlyfun.net/GossipCN/JavaGossip-V2/ReaderWriter.htm(第 1/2 页)2008-7-30 20:06:02 Reader、Writer // 下次reader才会再重头读取资料 byteArrayStream.reset(); } else { // 将第二个位元组推回串流 pushbackInputStream.unread(array, 1, 1); // 显示ASCII范围的字元 System.out.println("ASCII: " + (char)array[0]); } } pushbackInputStream.close(); } catch(ArrayIndexOutOfBoundsException e) { System.out.println("请指定档案名称"); } catch(IOException e) { e.printStackTrace(); } } } 假设的文字档案中有以下的文字:"这T是e一s个t测试" ,执行结果会是: BIG5: 这 ASCII: T BIG5: 是 ASCII: e BIG5: 一 ASCII: s BIG5: 个 ASCII: t BIG5: 测 BIG5: 试 ASCII: ! EOF InputStreamReader可以用位元组串流中取出位元组资料,并进行字元处理动作,关于Reader、Writer相关子类别,之后会于各相关主 题进行介绍。 http://caterpillar.onlyfun.net/GossipCN/JavaGossip-V2/ReaderWriter.htm(第 2/2 页)2008-7-30 20:06:02 InputStreamReader、OutputStreamWriter From Gossip@caterpillar Java Gossip: InputStreamReader、OutputStreamWriter 若想要对 InputStream、 OutputStream 进行字元处理,您可以使用InputStreamReader、OutputStreamWriter为它们加上字元处理的功 能,举个例子来说,若想要显示纯文字档案的内容,您不用费心的自行判断 字元编码(例如之前范例中要费心的自行判断是ASCII 英文字母或Big5中文字),只要将InputStream、OutputStream的实例作为建构InputStreamReader、OutputStreamWriter时的引数,之后 就可以操作InputStreamReader、OutputStreamWriter来进行文字档案的读取,让它们为您作字元判断与转换的动作。 下面这个例子可以用来开启文字档案,并将其备份为"原档名.bak": ● StreamReaderWriterDemo.java package onlyfun.caterpillar; import java.io.*; public class StreamReaderWriterDemo { public static void main(String[] args) { try { FileInputStream fileInputStream = new FileInputStream(args[0]); InputStreamReader inputStreamReader = new InputStreamReader(fileInputStream); FileOutputStream fileOutputStream = new FileOutputStream(args[0] + ".bak"); OutputStreamWriter outputStreamWriter = new OutputStreamWriter(fileOutputStream); int ch; // 以字元方式显示档案内容 while((ch = inputStreamReader.read()) != -1) { System.out.print((char)ch); outputStreamWriter.write(ch); } System.out.println(); inputStreamReader.close(); outputStreamWriter.close(); } catch(ArrayIndexOutOfBoundsException e) { e.printStackTrace(); } catch(IOException e) { http://caterpillar.onlyfun.net/GossipCN/JavaGossip-V2/StreamReaderWriter.htm(第 1/2 页)2008-7-30 20:06:05 InputStreamReader、OutputStreamWriter e.printStackTrace(); } } } 在这边是使用FileInputStream、FileOutputStream为例,但InputStreamReader、 OutputStreamWriter可以分别以InputStream、 OutputStream作为建构物件时的参数。 InputStreamReader、OutputStreamWriter在存取时是以系统的预设字元编码来进行字元转换,您也可以自行指定字元编码,例如 new InputStreamReader(fileInputStream, "GB2312"); http://caterpillar.onlyfun.net/GossipCN/JavaGossip-V2/StreamReaderWriter.htm(第 2/2 页)2008-7-30 20:06:05 FileReader、FileWriter From Gossip@caterpillar Java Gossip: FileReader、FileWriter 如果您想要存取的是一个文字档案,您可以直接使用FileReader、FileWriter类别,它们分别继承自InputStreamReader与 OutputStreamWriter,您可以直接指定档案名称或File物件来开启指定的文字档案,并读入串流转换后的字元,字元的转换会根据系 统预设 的编码(若要指定编码,则还是使用InputStreamReader与OutputStreamWriter)。 FileReader、FileWriter的使用非常简单,直接举个例子。在Linux下撰写的文字档案,其断行字元是'\n',而在 Windows下撰写的文字 档案其断行是'\r'与'\n'两个连续字元,如果您在Windows下使用记事本开启一个Linux下撰写的文字档案,其在显示上并不会有断行 的效果,且'\n'字元会被用一个黑色方块来显示。 我们在这边撰写一个简单的程式,读入Linux下撰写的文字档案,并写入另一个档案,在读取到'\n'字元时,就取代为'\r'与'\n'两个连 续字元,如此新的档案在Windows的记事本程式中,就可以有断行显示的效果。 ● FileReaderWriterDemo.java package onlyfun.caterpillar; import java.io.*; public class FileReaderWriterDemo { public static void main(String[] args) { try { FileReader fileReader = new FileReader(args[0]); FileWriter fileWriter = new FileWriter(args[0] + ".txt"); int in = 0; char[] wlnChar = {'\r', '\n'}; while((in = fileReader.read()) != -1) { if(in == '\n') fileWriter.write(wlnChar); else fileWriter.write(in); } fileReader.close(); fileWriter.close(); } catch(ArrayIndexOutOfBoundsException e) { e.printStackTrace(); } catch(IOException e) { http://caterpillar.onlyfun.net/GossipCN/JavaGossip-V2/FileReaderWriter.htm(第 1/2 页)2008-7-30 20:06:10 FileReader、FileWriter e.printStackTrace(); } } } http://caterpillar.onlyfun.net/GossipCN/JavaGossip-V2/FileReaderWriter.htm(第 2/2 页)2008-7-30 20:06:10 BufferedReader、BufferedWriter From Gossip@caterpillar Java Gossip: BufferedReader、BufferedWriter BufferedReader与BufferedWriter类别各拥有8192个字元的缓冲区,当读入或写出字元资料时,会先尽量从缓冲区读取。例如 BufferedReader在读取文字档案时,会先将字元资料读入缓冲区,而之后若使用read()方法时,会先从缓冲区中进行读取,如果缓冲 区资料不足,才会再从档案中读取,藉由缓冲区,可以减少对磁碟的I/O动作,藉以提高程式的效率。 而使用BufferedWriter时,写出的资料并不会先输出至目的地,而是先储存至缓冲区中,如果缓冲区中的资料满了,才会一次对目的 地进行写出,例如一个目标档案,藉由缓冲区,可以减少对磁碟的I/O动作,藉以提高程式的效率。 其实之前就常使用BufferedReader,您从标准输入串流System.in中直接读取使用者输入时,每当使用者输入一个字元,则 System.in 就读取一个字元,之前为了要能一次读取一行使用者的输入,使用了BufferedReader来对使用者输入的字元进行缓冲, readLine()方 法会在读取到使用者的换行字元时,再一次将整行字串传入。 System.in是个位元串流,为了转换为字元串流,您使用InputStreamReader为其进行字元转换,然后再使用BufferedReader为其增加缓 冲功能,例如: BufferedReader bufReader = new BufferedReader(new InputStreamReader(System.in)); 下次这个程式示范了BufferedReader与BufferedWriter的使用,您可以在文字模式下输入字元,程式会将您输入的文字储存至指定的档 案中,如果要结束程式,输入quit字串即可: ● BufferedReaderWriterDemo.java package onlyfun.caterpillar; import java.io.*; public class BufferedReaderWriterDemo { public static void main(String[] args) { try { BufferedReader bufReader = new BufferedReader( new InputStreamReader(System.in)); BufferedWriter bufWriter = new BufferedWriter(new FileWriter(args[0])); String keyin = null; while(!(keyin = bufReader.readLine()).equals("quit")) { bufWriter.write(keyin); bufWriter.newLine(); } bufReader.close(); http://caterpillar.onlyfun.net/GossipCN/JavaGossip-V2/BufferedReaderWriter.htm(第 1/2 页)2008-7-30 20:06:17 BufferedReader、BufferedWriter bufWriter.close(); } catch(ArrayIndexOutOfBoundsException e) { e.printStackTrace(); } catch(IOException e) { e.printStackTrace(); } } } http://caterpillar.onlyfun.net/GossipCN/JavaGossip-V2/BufferedReaderWriter.htm(第 2/2 页)2008-7-30 20:06:17 PrintWriter From Gossip@caterpillar Java Gossip: PrintWriter 之前曾经介绍过 PrintStream, 它可以将Java的基本资料型态等资料,直接转换为系统预设编码下对应的字元,再输出至 OutputStream中,而这边要介绍的 PrintWriter其功能上与PrintStream类似,除了接受OutputStream之外,它还可以接受Writer物件作为 输出的对象,当 您原先是使用Writer物件在作处理 ,而现在想要套用println()之类的方法时,使用PrintWriter会是比较方便的作法。 下面这个程式显示了PrintStream与PrintWriter两个物件在处理相同输出目的时的作法,程式将会在荧幕上显示 "简体中文" 四个字 元: ● StreamWriterDemo.java package onlyfun.caterpillar; import java.io.*; public class StreamWriterDemo { public static void main(String[] args) { try { byte[] sim = {(byte)0xbc, (byte)0xf2, // 简 (byte)0xcc, (byte)0xe5, // 体 (byte)0xd6, (byte)0xd0, // 中 (byte)0xce, (byte)0xc4}; // 文 InputStreamReader inputStreamReader = new InputStreamReader( new ByteArrayInputStream(sim), "GB2312"); PrintWriter printWriter = new PrintWriter( new OutputStreamWriter(System.out, "GB2312")); PrintStream printStream = new PrintStream(System.out, true, "GB2312"); int in; while((in = inputStreamReader.read()) != -1) { printWriter.println((char)in); printStream.println((char)in); } inputStreamReader.close(); printWriter.close(); printStream.close(); } http://caterpillar.onlyfun.net/GossipCN/JavaGossip-V2/PrinteWriter.htm(第 1/2 页)2008-7-30 20:06:22 PrintWriter catch(ArrayIndexOutOfBoundsException e) { e.printStackTrace(); } catch(IOException e) { e.printStackTrace(); } } } 要能正确看到执行的结果,您的终端机程式必须支援GB2312简体中文编码的显示。 http://caterpillar.onlyfun.net/GossipCN/JavaGossip-V2/PrinteWriter.htm(第 2/2 页)2008-7-30 20:06:22 CharArrayReader、CharArrayWriter From Gossip@caterpillar Java Gossip: CharArrayReader、CharArrayWriter ByteArrayInputStream、ByteArrayOutputStream 即是将位元阵列当作串流输入来源、输出目的地的工具类别,与其类似的是 CharArrayReader与CharArrayWriter,使用它们可以将 字元阵列当作字元资料输出或输入的来源。 通常很少会对文字档案进行随机存取的动作,因为为了与ASCII相容,一个文字档案中可能会有ASCII与双位元组字元,也就是说每 个字元的长度不一定相同,所以对文字档案进行随机存取容易发生错误。 由于Java中的Char是Unicode字元,藉由这个特性,您可以将文字档案的内容读入字元阵列,对字元阵列作随机存取,然后再将之写 回档案,这么一来对于文字档案您也可以达到类似的随机存取功能,这样的程式在以文字模式为主的编辑器可以应用上。 您开启一个简单的文字档案,当中有简单的ABCDEFG与中文等字元,在读取档案之后,您可以直接以程式来指定文字档案的字元 位置,以修改您所指定的字 元,程式的作法是将档案读入阵列中,修改位置的指定被用作阵列的指针,在修改完阵列内容之后,重 新将阵列存回档案,范例如下: ● CharArrayReaderWriterDemo.java package onlyfun.caterpillar; import java.io.*; import java.util.*; public class CharArrayReaderWriterDemo { public static void main(String[] args) { try { File file = new File(args[0]); BufferedReader bufInputReader = new BufferedReader( new FileReader(file)); // 将档案读入字元阵列 CharArrayWriter charArrayWriter = new CharArrayWriter(); char[] array = new char[1]; while(bufInputReader.read(array) != -1) charArrayWriter.write(array); charArrayWriter.close(); bufInputReader.close(); // 显示字元阵列内容 array = charArrayWriter.toCharArray(); for(int i = 0; i < array.length; i++) http://caterpillar.onlyfun.net/GossipCN/JavaGossip-V2/CharArrayReaderWriter.htm(第 1/3 页)2008-7-30 20:06:27 CharArrayReader、CharArrayWriter System.out.print(array[i] + " "); System.out.println(); // 让使用者输入位置与字元修改字元阵列内容 Scanner scanner = new Scanner(System.in); System.out.print("输入修改位置:"); int pos = scanner.nextInt(); System.out.print("输入修改字元:"); char ch = scanner.next().charAt(0); array[pos-1] = ch; // 将字元阵列内容存回档案 CharArrayReader charArrayReader = new CharArrayReader(array); BufferedWriter bufWriter = new BufferedWriter( new FileWriter(file)); char[] tmp = new char[1]; while(charArrayReader.read(tmp) != -1) bufWriter.write(tmp); charArrayReader.close(); bufWriter.flush(); bufWriter.close(); } catch(ArrayIndexOutOfBoundsException e) { e.printStackTrace(); } catch(IOException e) { e.printStackTrace(); } } } 假设文字档案中为:一 个Test!一个测试 ! 一个执行程式的范例如下: 一 个 T e s t ! 一 个 测 试 ! 输入修改位置:3 输入修改字元:t http://caterpillar.onlyfun.net/GossipCN/JavaGossip-V2/CharArrayReaderWriter.htm(第 2/3 页)2008-7-30 20:06:27 CharArrayReader、CharArrayWriter 修改过后,文字档案的内容会是:一个test!一个测试! http://caterpillar.onlyfun.net/GossipCN/JavaGossip-V2/CharArrayReaderWriter.htm(第 3/3 页)2008-7-30 20:06:27 PushbackReader From Gossip@caterpillar Java Gossip: PushbackReader PushbackReader与 PushbackInputStream 类似,都拥有一个PushBack缓冲区,只不过PushbackReader所处理的是字元,我们从这个 物件读出资料后,如果愿意的话,只要 PushBack缓冲区没有满,就可以使用unread()将资料回推回串流的前端。 下面这个程式可以将文字档案中的一些数学符号:<、>、<=、>=、!=、=转换为Big5码中的 < 、>、≦、≧、≠、=等全形符号并 另存新档: ● PushbackReaderDemo.java package onlyfun.caterpillar; import java.io.*; public class PushbackReaderDemo { public static void main(String[] args) { char[] symbols = {'<', '>', '≦', '≧', '≠', '='}; try { PushbackReader pushbackReader = new PushbackReader( new FileReader(args[0])); FileWriter fileWriter = new FileWriter(args[0] + ".math"); int c = 0; while((c = pushbackReader.read()) != -1) { int poss = -1; switch(c) { case '<': poss = 0; break; case '>': poss = 1; break; case '!': poss = 2; break; case '=': poss = 5; break; default: http://caterpillar.onlyfun.net/GossipCN/JavaGossip-V2/PushbackReader.htm(第 1/2 页)2008-7-30 20:06:32 PushbackReader fileWriter.write(c); } if(poss != -1) { if((c = pushbackReader.read()) == '=') { fileWriter.write(symbols[poss + 2]); fileWriter.write(' '); } else { pushbackReader.unread(c); fileWriter.write(symbols[poss]); } } } pushbackReader.close(); fileWriter.close(); } catch(ArrayIndexOutOfBoundsException e) { e.printStackTrace(); } catch(IOException e) { e.printStackTrace(); } } } 假设您的文字档案中有这些内容: x + y <= 3 y + z != w 1 + x >= 4 x + y > 3 x - y < 4 则执行程式后的另存新档内容会是: x + y ≦ 3 y + z ≠ w 1 + x ≧ 4 x + y > 3 x - y < 4 http://caterpillar.onlyfun.net/GossipCN/JavaGossip-V2/PushbackReader.htm(第 2/2 页)2008-7-30 20:06:32 实作 Runnable 介面 From Gossip@caterpillar Java Gossip: 实作 Runnable 介面 一个进程(Process)是一个包括有自身执行位址的程式,在一个多工的作业系统中,可以分配CPU时间给每一个进程,CPU在片段 时间中执行某个进程,然后下一个时间片段跳至另一个进程去执行,由于转换速度很快,这使得每个程式像是在同时进行处理一 般。 一个执行绪是进程中的一个执行流程,一个进程中可以同时包括多个执行绪,也就是说一个程式中同时可能进行多个不同的子流 程,这使得一个程式可以像是同时间 处理多个事务,例如一方面接受网路上的资料,另一方面同时计算资料并显示结果,一个多执 行绪程式可以同时间处理多个子流程。 在Java中要实现执行绪功能,可以实作Runnable介面,Runnable介面中只定义一个run()方法,然后实例化一个 Thread物件时,传入 一个实作Runnable介面的物件作为引数,Thread物件会调用Runnable物件的run()方法,进而执行当中所定义的流程。 下面这个程式是个简单的Swing程式,您可以看到如何实作Runnable介面及如何启动执行绪: ● ThreadDemo.java package onlyfun.caterpillar; import javax.swing.*; import java.awt.BorderLayout; import java.awt.Graphics; import java.awt.event.*; public class ThreadDemo extends JFrame { public ThreadDemo() { // 配置按钮 JButton btn = new JButton("Click me"); // 按下按钮后绘制圆圈 btn.addActionListener(new ActionListener() { public void actionPerformed(ActionEvent e) { Thread thread1 = new Thread(new Runnable() { public void run() { Graphics g = getGraphics(); for(int i = 10; i < 300; i+=10) { try { Thread.sleep(500); g.drawOval(i, 100, 10, 10); } catch(InterruptedException e) { e.printStackTrace(); http://caterpillar.onlyfun.net/GossipCN/JavaGossip-V2/RunnableInterface.htm(第 1/3 页)2008-7-30 20:06:37 实作 Runnable 介面 } } } }); Thread thread2 = new Thread(new Runnable() { public void run() { Graphics g = getGraphics(); for(int i = 10; i < 300; i+=10) { try { Thread.sleep(500); g.drawOval(i, 150, 15, 15); } catch(InterruptedException e) { e.printStackTrace(); } } } }); thread1.start(); thread2.start(); } }); getContentPane().add(btn, BorderLayout.NORTH); // 取消按下视窗关闭钮预设动作 setDefaultCloseOperation( WindowConstants.EXIT_ON_CLOSE); setSize(320, 200); setVisible(true); } public static void main(String[] args) { new ThreadDemo(); } } 将程式编译并执行时,您可以看到一个视窗,按下上面的按钮,您会看到两个圆在“同时”绘制,虽说是同时,其实也只是错觉而 已,其实是CPU往来两个流程之间不断的进行绘制圆的动作而已。 Thread类别也实作了Runnable介面,您也可以继承Thread类别并重新定义它的run()方法,好处是可以使用Thread上的一些继承下来的 方法,例如yield(),然而继承了Thread就表示您不能让您的类别再继承其它的类别。 http://caterpillar.onlyfun.net/GossipCN/JavaGossip-V2/RunnableInterface.htm(第 2/3 页)2008-7-30 20:06:37 实作 Runnable 介面 http://caterpillar.onlyfun.net/GossipCN/JavaGossip-V2/RunnableInterface.htm(第 3/3 页)2008-7-30 20:06:37 Daemon 执行绪 From Gossip@caterpillar Java Gossip: Daemon 执行绪 您想要设计一个程式,除了主执行绪之外,其运用了一个执行绪于背景进行相关运算工作,您的程式可能像是这样: ● SimpleThread.java package onlyfun.caterpillar; public class SimpleThread { public static void main(String[] args) { Thread thread = new Thread(new Runnable() { public void run() { while(true) { System.out.print("T"); } } }); thread.start(); // 主执行绪继续进行其它工作........ // such...such.... // 现在主执行绪执行到这边了,工作应该结束了 } } 您的执行绪已经运行到最后一个陈述了,这时应该是工作结束的时候,但您的另一个执行绪还在运作,您怎么停止它?在最后加上 一行使用System.exit ()?这虽然也可以,但这只是强迫程式结束,而且这个方法并不是随时可以适用! 一个Daemon执行绪是一个在背景执行服务的执行绪,例如网路伺服器倾听连接埠的服务、隐藏的系统执行绪如垃圾收集执行绪或 其它JVM 建立的执行绪,如果所有的非Daemon的执行绪都结束了,则Daemon执行绪自动就会终止。 从Main函式开始的是一个非Daemon执行绪,如果您希望某个执行绪在非Daemon执行绪都结束后也跟着终止,那么您要将它设定为 Daemon执行 绪,下面这个程式是个简单的示范: ● DaemonTest.java package onlyfun.caterpillar; public class DaemonTest { public static void main(String[] args) { Thread thread = new Thread(new Runnable() { public void run() { http://caterpillar.onlyfun.net/GossipCN/JavaGossip-V2/DaemonThread.htm(第 1/2 页)2008-7-30 20:06:41 Daemon 执行绪 while(true) { System.out.print("T"); } } }); thread.setDaemon(true); thread.start(); } } 这个程式在主执行绪结束之后,Daemon也就跟着结束了,所以它不会像上面这个程式,不断的列印T字元;您可以使用setDaemon() 方法来设定一 个执行绪是否为Daemon执行绪,使用isDaemon()方法则可以判断该执行绪是否为Daemon执行绪。 基本上,Java预设所有从Daemon执行绪产生的执行绪也是Daemon执行绪,这很直觉,由一个背景服务执行绪衍生出来的执行绪, 也应该是为了在背 景服务而产生的,所以在该停止时也应该停止。 http://caterpillar.onlyfun.net/GossipCN/JavaGossip-V2/DaemonThread.htm(第 2/2 页)2008-7-30 20:06:41 执行绪生命周期 From Gossip@caterpillar Java Gossip: 执行绪生命周期 执行绪的四个主要周期状态如下所示: 当您实例化一个Thread物件并执行start()之后,执行绪进入Runnable状态并开始执行run()方法。 虽然执行绪看起来像是同时执行,但事实上同一时间点上,还是只有一个执行绪在动作,只是执行绪之间切换的动作很快,所以看 来像是同时执行。 执行绪有其优先权,由1(Thread.MIN_PRIORITY)到10(Thread.MAX_PRIORITY),预设是 Thread.NORM_PRIORITY (5),您可以使用Thread的setPriority()方法来设定执行绪的优先权,设定必须在1到10之间,否则会丢出 IllegalArgumentException。 优先权高的执行绪会先被执行完毕,然后才会轮到优先权低的执行绪,如果优先权相同,则输流执行(Round-robin方式)。 决大多数的作业系统都支援timeslicing,简单的说就是作业 系统会为每个执行绪分配一小段CPU时间(quantum),时间一到就换下 一个执行绪,即使现有的执行绪还没结束。对于不支援timeslicing的 作业系统,每一个执行绪必须完成后,才能轮到下一个执行 绪,在这样的作业系统中,如果您想要让目前执行绪礼让一下其它执行绪,让它们有机会取得执行权,您 可以在呼叫绪行绪的yield ()方法,例如: public class SomeClass { // ..... Thread thread = new Thread(new Runnable() { public void run() { // .... while(true) { // .... Thread.yield(); // 暂时让出执行权 } } }); thread.start(); // .... } http://caterpillar.onlyfun.net/GossipCN/JavaGossip-V2/ThreadLife.htm(第 1/3 页)2008-7-30 20:06:46 执行绪生命周期 yield()方法让同样优先权的执行绪有被执行的机会,当执行绪执行yield()方法让出执行权时,它会再度加入执行绪的 排班,等待再 度取得执行权,对于支援timeslicing的作业系统,呼叫yield()是不太需要的,因为作业系统会自动分配时间给执行绪轮流执行。 有几种状况会让执行绪进入Not Runnable状态(或是blocked状态): 1. 呼叫sleep() 2. 呼叫wait() 3. 等待I/O完成 当执行绪在Not Runnable状态时,执行绪是可以被执行的,但有某些原因阻止它执行(例如等待使用者的输入),执行绪排班器将 不分配执行时间给这个执行绪,直到以下 的几个情况让执行绪回到Runnable状态: 1. 执行绪呼叫notify() 2. 执行绪呼叫notifyAll() 3. 执行绪呼叫interrupt() 当执行绪因为I/O而进入blocked状态,它必须等到I/O完成才可以离开这个状态。 最后,如果执行的工作完成(或发生例外)而离开run()方法,则执行绪执行完毕,进入Dead状态,您可以使用isAlive()方法来测试 执行绪是否 存活。 如果您查询Java的线上API文件,您会发现有suspend()、resume()、stop()等方法,这些方法Java并不建议您使用,而且已经 被标示 为"deprecated",这些方法建议您在需要的时候自行实作。 这边举个简单的例子,当您使用Thread.sleep()让执行绪暂停执行进入Not Runnable状态,您可以使用interrupt()让它离开Not Runnable状态,当使用sleep()暂时进入Not Runnable状态而您interrupt()时,会丢出InterruptedException例外物件,例如: ● InterruptDemo.java package onlyfun.caterpillar; public class InterruptDemo { public static void main(String[] args) { Thread thread1 = new Thread(new Runnable() { public void run() { try { Thread.sleep(99999); } catch(InterruptedException e) { System.out.println("I'm interrupted!!"); //e.printStackTrace(); } } }); thread1.start(); thread1.interrupt(); // interrupt it right now } } http://caterpillar.onlyfun.net/GossipCN/JavaGossip-V2/ThreadLife.htm(第 2/3 页)2008-7-30 20:06:46 执行绪生命周期 执行结果: I'm interrupted! 关于执行绪的wait()、notify()、notifyAll()等方法,在之后的文章中会陆续介绍。 http://caterpillar.onlyfun.net/GossipCN/JavaGossip-V2/ThreadLife.htm(第 3/3 页)2008-7-30 20:06:46 执行绪的加入(join) From Gossip@caterpillar Java Gossip: 执行绪的加入(join) 如果有一个A执行绪正在运行,您希望插入一个B执行绪,并要求B执行绪先执行完毕,然后再继续A执行绪的流程,您可以使用 join()方法来完成这个需求,这就好比您手头上正有一个工作在进行,老板插入一个工作要求您先作好,然后再进行您原先正进行的 工作。 当执行绪使用join()加入至另一个执行绪时,另一个执行绪会等待这个被加入的执行绪工作完毕,然后再继续它的动作,使用下面这 个简单的例子就可以 说明: ● ThreadA.java package onlyfun.caterpillar; public class ThreadA { public static void main(String[] args) { System.out.println("Thread A 执行"); Thread threadB = new Thread(new Runnable() { public void run() { try { System.out.println("Thread B 开始.."); for(int i = 0; i < 5; i++) { Thread.sleep(1000); System.out.println("Thread B 执行.."); } System.out.println("Thread B 即将结束.."); } catch(InterruptedException e) { e.printStackTrace(); } } }); threadB.start(); try { // Thread B 加入 Thread A threadB.join(); } catch(InterruptedException e) { e.printStackTrace(); } http://caterpillar.onlyfun.net/GossipCN/JavaGossip-V2/JoinThread.htm(第 1/2 页)2008-7-30 20:06:50 执行绪的加入(join) System.out.println("Thread A 执行"); } } 程式启动后主执行绪就开始,在主执行绪中您新建threadB,并在启动threadB后,将之加入(join)主执行绪的流程之中,threadB必 须 先执行完毕,主执行绪才会再继续它原本的流程, 执行结果如下: Thread A 执行 Thread B 开始.. Thread B 执行.. Thread B 执行.. Thread B 执行.. Thread B 执行.. Thread B 执行.. Thread B 即将结束.. Thread A 执行 如果程式中没有将threadB使用join()将之加入主执行绪的流程中,则最后一行显示"Thread A 执行"的陈述会先执行完毕(因为threadB 使用了sleep(),这让主执行绪有机会取得时间来执行)。 有时候这个加入的执行绪有可能处理太久,您不想无止境的等待这个执行绪的工作完毕,则您可以在join()上指定时间,例如 join (10000),表示至多等待10000毫秒,也就是10秒,如果加入的执行绪还没执行完毕就不管它,目前的执行绪可以继续执行工作。 http://caterpillar.onlyfun.net/GossipCN/JavaGossip-V2/JoinThread.htm(第 2/2 页)2008-7-30 20:06:50 执行绪的停止 From Gossip@caterpillar Java Gossip: 执行绪的停止 如果您想要停止一个执行绪的执行,当您查看API时,您会发现Thread的stop()方法已经被标示为 "deprecated",使用这个方法来停 止一个执行绪是不被建议的。 请见:Why Are Thread.stop, Thread.suspend, Thread.resume and Runtime.runFinalizersOnExit Deprecated? 如果您想要停止一个执行绪,您最好自行实作。 一个执行绪要进入Dead状态,就是执行完run()方法,简单的说,如果您想要停止一个执行绪的执行,就要提供一个方式让执行绪可 以执行完run(),而这也是您自行实作执行绪停止的基本概念。 例如,如果执行绪的run()方法中执行的是一个重复执行的回圈,您可以提供一个flag来控制回圈是否执行,藉此让回圈有可能终 止、执行绪可以离开 run()方法以终止执行绪: public class SomeThread implements Runnable { private boolean isContinue = true; public void terminate() { isContinue = false; } public void run() { while(isContinue) { // ... some statements } } } 如果执行绪因为执行sleep()或是wait()而进入Not Runnable状态,而您想要停止它,您可以使用interrupt(),而程式会丢出 InterruptedException例外,因而使得执行绪 离开run()方法,例如: ● SomeThread.java package onlyfun.caterpillar; public class SomeThread implements Runnable { public void run() { System.out.println("sleep....going to not runnable"); try { Thread.sleep(9999); } catch(InterruptedException e) { System.out.println("I am interrupted...."); } } } http://caterpillar.onlyfun.net/GossipCN/JavaGossip-V2/StopThread.htm(第 1/3 页)2008-7-30 20:06:55 执行绪的停止 ● Main.java package onlyfun.caterpillar; public class Main { public static void main(String[] args) { Thread thread = new Thread(new SomeThread()); thread.start(); thread.interrupt(); } } 如果程式因为I/O而停滞,进入Not Runnable状态,基本上您必须等待I/O完成才能离开Not Runnable,您无法使用interrupt()来使得执 行绪离开run()方法,您要提供替代的方法,基本上的概念也是引发一个例外,而这个例外要 如何引发,要看您所使用的I/O而定, 例如您使用readLine()在等待网路上的一个讯息,此时执行绪进入Not Runnable直到读到一个讯息,您要让它离开run()的方法就是使 用close()关闭它的串流,这时会引发一个IOException例外而使得 执行绪离开run()方法,例如: public class Client implements Runnable { private Socket skt; // ..... public void terminate() { skt.close(); } public void run() { // ..... try { BufferedReader buf = new BufferedReader( new InputStreamReader(skt.getInputStream())); // 读取客户端讯息 // 执行readLine()会进入Not runnable状态 // 直到读到客户端讯息 while((userMessage = buf.readLine()) != null) { // .... } } catch(IOException e) { System.out.println("执行绪被终止......."); } } } 上面这个程式是个简单的架构示范,实际的设计必须视您的程式功能与I/O类型而定。 除了stop()之外,suspend()、resume()方法也被标示为"deprecated",这些方法如果您要达成相同的功能,您都必须自行实作,在将 来新的Java版本中如果这些功能被实现,它也可能是新的介面,而不是使用现有的方法。 http://caterpillar.onlyfun.net/GossipCN/JavaGossip-V2/StopThread.htm(第 2/3 页)2008-7-30 20:06:55 执行绪的停止 有关于执行绪的终止,还可以参考 Two-phase Termination 模式。 http://caterpillar.onlyfun.net/GossipCN/JavaGossip-V2/StopThread.htm(第 3/3 页)2008-7-30 20:06:55 ThreadGroup From Gossip@caterpillar Java Gossip: ThreadGroup 在Java中每个执行绪都属于某个“执行绪群组”(ThreadGroup)管理的一员,例如若您是在main ()主工作流程中产生一个执行绪, 则产生的执行绪属于main这个执行绪群组管理的一员,您可以使用下面的指令来取得目前执行绪所属的执行绪群组名称:: Thread.currentThread().getThreadGroup().getName(); 每一个执行绪产生时,都会被归入某个执行绪群组,这视您的执行绪是在哪个群组中产生,如果没有指定,则归入产生该子执行绪 的执行绪群组中,您也可以自行指定执行绪群组,执行绪一但归入某个群组,就无法更换群组。 ThreadGroup正如其名,可以统一管理整个群组中的执行绪,您可以使用以下的方式来产生群组,并在产生执行绪的时候,一并指定 其群组: ThreadGroup threadGroup1 = new ThreadGroup("group1"); ThreadGroup threadGroup2 = new ThreadGroup("group2"); Thread thread1 = new Thread(threadGroup1, "group1's member"); Thread thread2 = new Thread(threadGroup2, "group2's member"); ThreadGroup中的某些方法,可以对所有的执行绪产生作用,例如interrupt()可以interrupt群组中所有的执行绪, setMaxPriority()可以 设定群组中执行绪所能拥有的最大优先权(本来就拥有更高优先权的执行绪不受影响),这方面您可以查询线上API文件 来了解有 哪些方法可以使用。 如果我们想要一次取得群组中所有的执行绪进行操作,您可以使用enumerate()方法,例如: Thread[] threads = new Thread[threadGroup1.activeCount()]; threadGroup1.enumerate(threads); activeCount()方法取得群组中作用中的执行绪数量,enumerate()方法要传入一个Thread阵列物件,它会将执行绪物件设定至每个阵列 栏位中,之后您就可以指定阵列索引来操作这些执行绪。 ThreadGroup中有个uncaughtException()方法,这是当群组中某个执行绪发生unchecked例外时,由执行环境呼叫此方法进行处理, 如果有必要,您可以重新定义此方法,一个例子如下: ● ThreadGroupDemo.java package onlyfun.caterpillar; import java.io.*; public class ThreadGroupDemo { public static void main(String[] args) { ThreadGroup threadGroup1 = // 这是匿名类别写法 new ThreadGroup("group1") { http://caterpillar.onlyfun.net/GossipCN/JavaGossip-V2/ThreadGroup.htm(第 1/2 页)2008-7-30 20:07:00 ThreadGroup // 继承ThreadGroup并重新定义以下方法 // 在执行绪成员丢出unchecked exception // 会执行此方法 public void uncaughtException(Thread t, Throwable e) { System.out.println(t.getName() + ": " + e.getMessage()); } }; // 这是匿名类别写法 Thread thread1 = // 这个执行绪是threadGroup1的一员 new Thread(threadGroup1, new Runnable() { public void run() { // 丢出unchecked例外 throw new RuntimeException("测试例外"); } }); thread1.start(); } } 在uncaughtException()方法的参数中,第一个参数可以取得发生例外的执行绪实例,而第二个参数可以取得例外物件,范例中显示了 执行绪的名称及例外讯息,结果如下所示: Thread-0: 测试例外 http://caterpillar.onlyfun.net/GossipCN/JavaGossip-V2/ThreadGroup.htm(第 2/2 页)2008-7-30 20:07:00 执行绪的同步化 From Gossip@caterpillar Java Gossip: 执行绪的同步化 如果您的程式只是一个单执行绪,单一流程的程式,那么通常您只要注意到程式逻辑的正确,您的程式通常就可以正确的执行您想 要的功能,但当您的程式是多执行绪程式,多流程同时执行时,那么您就要注意到更多的细节,例如在多执行绪共用同一物件的资 料时。 如果一个物件所持有的资料可以被多执行绪同时共享存取时,您必须考虑到“资料同步”的 问题,所谓资料同步指的是两份资料的整 体性一致,例如物件A有 name与id两个属性,而有一份A1资料有name与id的资料要更新物件A的属性,如果A1的name与id设定给A 物件完成,则称A1与A同步,如 果A1资料在更新了物件的name属性时,突然插入了一份A2资料更新了A物件的id属性,则显然的A1 资料与A就不同步,A2资料与A也不同步。 资料在多执行绪下共享时,就容易因为同时多个执行绪可能更新同一个物件的资讯,而造成物件资料的不同步,因为资料的不同步 而可能引发的错误通常不易察觉, 而且可能是在您程式执行了几千几万次之后,才会发生错误,而这通常会发生在您的产品已经上 线之后,甚至是程式已经执行了几年之后。 这边举个简单的例子,考虑您设计这么一个类别: ● PersonalInfo.java package onlyfun.caterpillar; public class PersonalInfo { private String name; private String id; private int count; public PersonalInfo() { name = "nobody"; id = "N/A"; } public void setNameAndID(String name, String id) { this.name = name; this.id = id; if(!checkNameAndIDEqual()) { System.out.println(count + ") illegal name or ID....."); } count++; } private boolean checkNameAndIDEqual() { return (name.charAt(0) == id.charAt(0)) ? http://caterpillar.onlyfun.net/GossipCN/JavaGossip-V2/ThreadSynchronized.htm(第 1/4 页)2008-7-30 20:07:05 执行绪的同步化 true : false; } } 在这个类别中,您可以设定使用者的名称与缩写id,并简单检查一下名称与id的第一个字是否相同,单就这个类别本身而言,它并 没有任何的错误,但如果它被 用于多执行绪的程式中,而且同一个物件被多个执行存取时,就会"有可能"发生错误,来写个简单的 测试程式: ● SynchronizedDemo.java package onlyfun.caterpillar; public class SynchronizedDemo { public static void main(String[] args) { final PersonalInfo person = new PersonalInfo(); Thread thread1 = new Thread(new Runnable() { public void run() { while(true) person.setNameAndID("Justin Lin", "J.L"); } }); Thread thread2 = new Thread(new Runnable() { public void run() { while(true) person.setNameAndID("Shang Hwang", "S.H"); } }); System.out.println("Start testing....."); thread1.start(); thread2.start(); } } 来看一下执行时的一个例子: Start testing..... 822949) illegal name or ID..... 1443074) illegal name or ID..... 1750512) illegal name or ID..... 2587632) illegal name or ID..... 2805877) illegal name or ID..... 3705555) illegal name or ID..... 4000077) illegal name or ID..... http://caterpillar.onlyfun.net/GossipCN/JavaGossip-V2/ThreadSynchronized.htm(第 2/4 页)2008-7-30 20:07:05 执行绪的同步化 看到了吗?如果以单执行绪的观点来看,上面的讯息在测试中根本不可能出现,然而在这个程式中却出现了错误,而且重点是,第 一次错误是发生在第822949 次的设定(您的电脑上可能是不同的数字),如果您在程式完成并开始应用之后,这个时间点可能是几 个月甚至几年之后。 问题出现哪?在于这边: public void setNameAndID(String name, String id) { this.name = name; this.id = id; if(!checkNameAndIDEqual()) { System.out.println(count + ") illegal name or ID....."); } count++; } 虽然您设定给它的参数并没有问题,在某个时间点时,thread1设定了"Justin Lin", "J.L"给name与id,在进行测试的前一刻,thread2可 能此时刚好呼叫setNameAndID("Shang Hwang", "S.H"),在name被设定为"Shang Hwang"时,checkNameAndIDEqual()开始执行,此时 name等于"Shang Hwang",而id还是"J.L",所以checkNameAndIDEqual()就会传回false,结果就显示了错误讯息。 您必须同步资料对物件的更新,也就是在有一个执行绪正在设定person物件的资料时,不可以又被另一个执行绪同时进行设定,您 可以使用"synchronized"关键字来进行这个动作。 "synchronized"的一个使用方式是用于方法上,让方法作用范围内都成为被同步化区域,例如: public synchronized void setNameAndID(String name, String id) { this.name = name; this.id = id; if(!checkNameAndIDEqual()) { System.out.println(count + ") illegal name or ID....."); } count++; } 每个物件内部都会有一个锁定(lock),当执行绪执行某个物件的同步化方法时,它会在物件上得到这个锁定,只有取得锁定的执 行绪才可进入同步区,未取得锁定的执行绪则必须等待,直到有机会取得锁定,其它执行绪必须等目前执行绪先执行完同步化方 法,并解除对物件的锁定,才有机会取得物件上的锁定。 就这个例子来说,简单的说,就是有执行绪在执行setNameAndID()时,会从物件上取得锁定,其它执行绪必须等待它执行完毕,释 放锁定之后,才会有机会竞争锁定,取得锁定的执行绪才可以执行setNameAndID ()。 以上所介绍的是实例方法同步化(instance method synchronized),同步化的设定不只可用于方法上,也可以用于某个程式区块上, 称之为实例区块同步化(instance block synchronized),例如: public void setNameAndID(String name, String id) { synchronized(this) { this.name = name; this.id = id; if(!checkNameAndIDEqual()) { System.out.println(count + ") illegal name or ID....."); http://caterpillar.onlyfun.net/GossipCN/JavaGossip-V2/ThreadSynchronized.htm(第 3/4 页)2008-7-30 20:07:05 执行绪的同步化 } count++; } } 上面的意思就是在执行绪执行至"synchronized"设定的区块时取得物件的锁定,这么一来其它执行绪暂时无法取得锁定,因此无法执 行物件同步化区块,这个方式可以应 用于您不想锁定整个方法区块,而只是想在共享资料在被执行绪存取时确保同步化时,由于只 锁定方法中的某个区块,在执行完区块后即释放对物件的锁定,以便让 其它执行绪有机会取得锁定,对物件进行操作,在某些时候 会比较有效率。 实例区块同步化的好处是,您也可以对某个物件进行同步化,而像实例方法同步化只针对this,例如在多执行绪存取同一个ArrayList 物件时,ArrayList并没有实作资料存取时的同步化,所以它使用于多执 行绪时,必须注意是否必须对它进行同步化,多个执行绪存 取同一个ArrayList时,有可能发生两个以上的执行绪将资料存入 ArrayList的同一个位置,造成资料的相互覆盖,为了确保资料存入 时的正确性,您可以在存取ArrayList物件时对它进行同步化,例如: // arraylist参考至一个ArrayList的一个实例 synchronized(arraylist) { arraylist.add(new SomeClass()); } 除了针对物件同步之外,您还可以针对静态方法同步化(static method synchronized),例如某个static成员会被多执行绪存取时,则 可以如下设定: public class Some { private static int value; public synchronized static void some() { value++; .... } } 进行锁定时,会锁定Some.class,因而static成员也受到保护。类似于实例区块同步化,您也可以在区块中锁定整个类别,称之为类 别字面同步化(class literals synchronized),例如: ... public void doSomething() { synchronized(Some.class) { .... } } ... 事实上,您也可以使用Collections的synchronizedXXX()等方法来传回一个同步化的容器物件,例如传回一个同步化的List: List list = Collections.synchronizedList(new ArrayList()); 同步化所牺性的自然就是在于执行绪等待时的延迟,所以同步化的手法不应被滥用,您不用将整个物件的方法都加 上"synchronized",有些方法只是单纯的传回某些数值,它并没有对共用资料进行修改的动作,那么它就不需要被同步化。 http://caterpillar.onlyfun.net/GossipCN/JavaGossip-V2/ThreadSynchronized.htm(第 4/4 页)2008-7-30 20:07:05 wait()、notify() From Gossip@caterpillar Java Gossip: wait()、notify() wait()、notify()与notifyAll()是由 Object所提供的方法,您在定义自己的类别时会继承下来(记得Java中所有的物件最顶层都继承自 Object),wait()、notify()与 notifyAll()都被宣告为"final",所以您无法重新定义它们,透过这三个方法您可以控制执行绪是否为 Runnable状态。 您必须在同步化的方法或区块中呼叫wait()方法,当物件的wait()方法被调用,目前的执行绪会被放入物件的“等待集合”(Wait set) 中, 执行绪会释放物件的锁定,其它的执行绪可以竞争锁定,取得锁定的执行绪可以执行同步化区块;被放在等待集中的执行绪将 不参与执行绪的排班,wait()可 以指定等待的时间,如果指定时间的话,则时间到之后执行绪会再度加入排班,如果指定时间0或不 指定,则执行绪会持续等待,直到有被中断(interrupt)或是被告知(notify)可以参与排班。 当物件的notify()被调用,它会从物件的等待集中选出“一个”执行绪加入排班,被选出的执行绪是随机的,被选出的执行绪会与其它 正在执行的执行绪共 同竞争对物件的锁定;如果您呼叫notifyAll(),则“所有”在等待集中的执行绪都会被唤醒,这些执行绪会与其它 正在执行的执行绪共同竞争对物件的 锁定。 简单的说,当执行绪呼叫到物件的wait()方法时,表示它要先让出物件的被同步区使用权并等待通知,或是等待一段指定的时间,直 到被通知或时间到时再从 等待点开始执行,这就好比您要叫某人作事,作到一半时某人叫您等候通知(或等候1分钟之类的),当 您被通知(或时间到时)某人会继承为您服务。 说明wait()、notify()或notifyAll()的应用最常见的一个例子,就是生产者(Producer)与消费者(Consumer)的 例子,如果生产者会 将产品交给店员,而消费者从店员处取走产品,店员一次只能持有固定数量产品,如果生产者生产了过多的产品,店员叫生产者等 一下 (wait),如果店中有空位放产品了再通知(notify)生产者继续生产,如果店中没有产品了,店员会告诉消费者等一下 (wait),如果店中有产品 了再通知(notify)消费者来取走产品。 以下举一个最简单的:生产者每次生产一个int整数交给在店员上,而消费者从店员处取走整数,店员一次只能持有一个整数。 以程式实例来看,首先是生产者: ● Producer.java package onlyfun.caterpillar; public class Producer implements Runnable { private Clerk clerk; public Producer(Clerk clerk) { this.clerk = clerk; } public void run() { System.out.println( "生产者开始生产整数......"); // 生产1到10的整数 for(int product = 1; product <= 10; product++) { http://caterpillar.onlyfun.net/GossipCN/JavaGossip-V2/WaitNotify.htm(第 1/5 页)2008-7-30 20:07:09 wait()、notify() try { // 暂停随机时间 Thread.sleep((int) (Math.random() * 3000)); } catch(InterruptedException e) { e.printStackTrace(); } // 将产品交给店员 clerk.setProduct(product); } } } 再来是消费者: ● Consumer.java package onlyfun.caterpillar; public class Consumer implements Runnable { private Clerk clerk; public Consumer(Clerk clerk) { this.clerk = clerk; } public void run() { System.out.println( "消费者开始消耗整数......"); // 消耗10个整数 for(int i = 1; i <= 10; i++) { try { // 等待随机时间 Thread.sleep((int) (Math.random() * 3000)); } catch(InterruptedException e) { e.printStackTrace(); } // 从店员处取走整数 clerk.getProduct(); } } } http://caterpillar.onlyfun.net/GossipCN/JavaGossip-V2/WaitNotify.htm(第 2/5 页)2008-7-30 20:07:09 wait()、notify() 生产者将产品放至店员,而消费者从店员处取走产品,所以店员来决定谁必须等待并等候通知。 ● Clerk.java package onlyfun.caterpillar; public class Clerk { // -1 表示目前没有产品 private int product = -1; // 这个方法由生产者呼叫 public synchronized void setProduct(int product) { if(this.product != -1) { try { // 目前店员没有空间收产品,请稍候! wait(); } catch(InterruptedException e) { e.printStackTrace(); } } this.product = product; System.out.printf("生产者设定 (%d)%n", this.product); // 通知等待区中的一个消费者可以继续工作了 notify(); } // 这个方法由消费者呼叫 public synchronized int getProduct() { if(this.product == -1) { try { // 缺货了,请稍候! wait(); } catch(InterruptedException e) { e.printStackTrace(); } } int p = this.product; System.out.printf( "消费者取走 (%d)%n", this.product); http://caterpillar.onlyfun.net/GossipCN/JavaGossip-V2/WaitNotify.htm(第 3/5 页)2008-7-30 20:07:09 wait()、notify() this.product = -1; // 通知等待区中的一个生产者可以继续工作了 notify(); return p; } } 使用这么一个程式来测试: ● WaitNotifyDemo.java package onlyfun.caterpillar; public class WaitNotifyDemo { public static void main(String[] args) { Clerk clerk = new Clerk(); Thread producerThread = new Thread( new Producer(clerk)); Thread consumerThread = new Thread( new Consumer(clerk)); producerThread.start(); consumerThread.start(); } } 执行结果: 生产者开始生产整数...... 消费者开始消耗整数...... 生产者设定 (1) 消费者取走 (1) 生产者设定 (2) 消费者取走 (2) 生产者设定 (3) 消费者取走 (3) 生产者设定 (4) 消费者取走 (4) 生产者设定 (5) 消费者取走 (5) 生产者设定 (6) 消费者取走 (6) 生产者设定 (7) 消费者取走 (7) 生产者设定 (8) http://caterpillar.onlyfun.net/GossipCN/JavaGossip-V2/WaitNotify.htm(第 4/5 页)2008-7-30 20:07:09 wait()、notify() 消费者取走 (8) 生产者设定 (9) 消费者取走 (9) 生产者设定 (10) 消费者取走 (10) 生产者会生产10个整数,而消费者会消耗10个整数,由于店员处只能放置一个整数,所以每生产一个就消耗一个,其结果如上所示 是无误的。 如果一个执行绪进入物件的等待集中,您可以中断它的等待,这时将会发生InterruptedException例外物件,interrupt()方法可用来 进行这项工作。 http://caterpillar.onlyfun.net/GossipCN/JavaGossip-V2/WaitNotify.htm(第 5/5 页)2008-7-30 20:07:09 容器类的执行绪安全(Thread-safe) From Gossip@caterpillar Java Gossip: 容器类的执行绪安全(Thread-safe) 容器类预设没有考虑执行绪安全问题,您必须自行实作同步以确保共用资料在多执行绪存取下不会出错,例如若您使用 List物件 时,您可以这样实作: // arraylist参考至一个ArrayList的一个实例 synchronized(arraylist) { arraylist.add(new SomeClass()); } 事实上,您也可以使用Collections的synchronizedXXX()等方法来传回一个同步化的容器物件,例如传回一个同步化的List: List list = Collections.synchronizedList(new ArrayList()); 以这种方式返回的List物件,在存取资料时,会进行同步化的工作,不过在您使用Iterator遍访物件时,您仍必须实作同步化,因为 这样的List使用iterator()方法返回的Iterator物件,并没有保证执行绪安全(Thread-safe),一个实作遍访的例子如下: List list = Collections.synchronizedList(new ArrayList()); ... synchronized(list) { Iterator i = list.iterator(); while (i.hasNext()) foo(i.next()); } 在J2SE 5.0之后,新增了 java.util.concurrent 这个 package,当中包括了一些确保执行绪安全的 Collection 类,例如 ConcurrentHashMap、CopyOnWriteArrayList、CopyOnWriteArraySet等等,这些新增的 Collection 类基本行为与先前介绍的 Map、List、Set等物件是相同的,所不同的是增加了同步化的功能,而且依物件的特性不同而有不同的同步化实作,以确保效率与 安全性。 例如ConcurrentHashMap,它针对Hash Table中不同的区段(segment)进行同步化,而不是对整个物件进行同步化,预设上HashMap 有16个区段,当有执行绪在存取第一个区段时, 第一个区域进入同步化,然而另一个执行绪仍可以存取第一个区段以外的区段,而 不用等待第一个执行绪存取完成,所以与同步化整个物件来说,新增的这些同步化 物件,在效率与安全性上取得了较好的平衡。 http://caterpillar.onlyfun.net/GossipCN/JavaGossip-V2/ContainerThreadSafe.htm2008-7-30 20:07:13 UncaughtExceptionHandler From Gossip@caterpillar Java Gossip: UncaughtExceptionHandler 在J2SE 5.0之前,如果您想要统一处理某些执行绪的unchecked例外,您可以使用一个ThreadGroup来管理,您继承ThreadGroup,并 重 新定义其uncaughtException()方法,而在J2SE 5.0之后,您不用这么麻烦,您可以让您的例外处理类别实作Thread. UncaughtExceptionHandler,并实现其 uncaughtException()方法,例如: ● ThreadExceptionHandler.java package onlyfun.caterpillar; public class ThreadExceptionHandler implements Thread.UncaughtExceptionHandler { public void uncaughtException(Thread t, Throwable e) { System.out.println(t.getName()); e.printStackTrace(); } } 您可以使用Thread的setUncaughtExceptionHandler()方法来设定例外处理物件,例如: ● ThreadExceptionDemo.java package onlyfun.caterpillar; public class ThreadExceptionDemo { public static void main(String[] args) { Thread thread = new Thread(new Runnable() { public void run() { // 除 0 的错误 int n = 1/0; } }, "testThread"); thread.setUncaughtExceptionHandler( new ThreadExceptionHandler()); thread.start(); } } 在这个程式中,我们故意产生除零的例外,例外丢出后会由ThreadExceptionHandler物件来处理,执行结果如下: http://caterpillar.onlyfun.net/GossipCN/JavaGossip-V2/UncaughtExceptionHandler.htm(第 1/2 页)2008-7-30 20:07:17 UncaughtExceptionHandler testThread java.lang.ArithmeticException: / by zero at onlyfun.caterpillar.ThreadExceptionDemo$1.run( ThreadExceptionDemo.java:8) at java.lang.Thread.run(Unknown Source) http://caterpillar.onlyfun.net/GossipCN/JavaGossip-V2/UncaughtExceptionHandler.htm(第 2/2 页)2008-7-30 20:07:17 Lock 与 Condition From Gossip@caterpillar Java Gossip: Lock 与 Condition 对于初学者来说,执 行绪的同步化 并非是个容易理解的议题,在synchronized中隐含着物件锁定与释放锁定的观念,程式中并没有 明显的语意来告知这一点,而必须靠程式设计人员本身记忆物件的锁定与释放锁定问题。 在java.util.concurrent.locks套件中新增了Lock与Condition等类别,可以让您明确的在程式中进行明确的锁定与释放锁定。 Lock是一个介面,其中规范了lock()、unclock()与newCondition()三种方法: ● lock() 用来取得物件的锁定。 ● unlock() 用来释放物件的锁定,通常由同一个Lock物件来呼叫lock()与unlock()。 ● newCondition() 建立一个与Lock物件相关联的Conditon物件。 Condition是一个介面,作用是在执行绪之间进行沟通,就如其名称所示,告知执行绪目前的状况为何,是要等待?还是通知?其规 范的几个重要方法为: ● await() 告知目前的执行绪等待,直到被通知或中断(interrupted)。 ● signal() 通知目前等待中的一个执行绪,从上次的等待点继续执行,类似物件的notify()方法 ● signalAll() 通知目前等待中的所有执行绪参与锁定竞争,而后从上次的等待点继续执行,类似物件的notifyAll()方法。 在这边直接改写wait ()、notify() 中的Clerk类别,不使用synchronized、wait()、notify(),而改用Lock与Condition,其中ReentrantLock 为Lock介面的一个实作类别: ● Clerk.java import java.util.concurrent.locks.*; public class Clerk { private Lock lock = new ReentrantLock(); private Condition threadCond = lock.newCondition(); // -1 表示目前没有产品 private int product = -1; // 这个方法由生产者呼叫 public void setProduct(int product) { lock.lock(); try { if(this.product != -1) { try { http://caterpillar.onlyfun.net/GossipCN/JavaGossip-V2/LockAndCondition.htm(第 1/3 页)2008-7-30 20:07:24 Lock 与 Condition // 目前店员没有空间收产品,请稍候! threadCond.await(); } catch(InterruptedException e) { e.printStackTrace(); } } this.product = product; System.out.printf("生产者设定 (%d)%n", this.product); // 通知等待区中的一个消费者可以继续工作了 threadCond.signal(); } finally { lock.unlock(); } } // 这个方法由消费者呼叫 public int getProduct() { lock.lock(); int p = 0; try { if(this.product == -1) { try { // 缺货了,请稍候! threadCond.await(); } catch(InterruptedException e) { e.printStackTrace(); } } p = this.product; System.out.printf( "消费者取走 (%d)%n", this.product); this.product = -1; // 通知等待区中的一个生产者可以继续工作了 threadCond.signal(); } finally { http://caterpillar.onlyfun.net/GossipCN/JavaGossip-V2/LockAndCondition.htm(第 2/3 页)2008-7-30 20:07:24 Lock 与 Condition lock.unlock(); } return p; } } http://caterpillar.onlyfun.net/GossipCN/JavaGossip-V2/LockAndCondition.htm(第 3/3 页)2008-7-30 20:07:24 BlockingQueue From Gossip@caterpillar Java Gossip: BlockingQueue 伫列(Queue)是个先前先出(First In First Out, FIFO)的资料结构。在JDK 5.0中新增了Blocking Queue,在多执行绪的情况下,如 果Blocking Queue的内容为空,而有个执行绪试图从Queue中取出元素,则该执行绪会被Block,直到Queue有元素时才解除Block,反 过来说,如果 Blocking Queue满了,而有个执行绪试图再把资料填入Queue中,则该执行绪会被Block,直到Queue中有元素被取走后 解除Block。 BlockingQueue的几个主要操作为下: 方法 说明 add 加入元素,如果伫列是满的,则丢出IllegalStateException remove 传回并从伫列移除元素,如果伫列是空的,则丢出NoSuchElementException element 传回元素,如果伫列是空的,则丢出NoSuchElementException offer 加入元素并传回true,如果伫列是满的,则传回false poll 传回并从伫列移除元素,如果伫列是空的,则传回null peek 传回元素,如果伫列是空的,则传回null put 加入元素,如果伫列是满,就block take 传回并移除元素,如果伫列是空的,就block 在java.util.concurrent下提供几种不同的Blocking Queue,ArrayBlockingQueue要指定容量大小来建构,LinkedBlockingQueue预设没有容 量上限,但也可以指定容量上限,PriorityBlockingQueue严格来说不是Queue,因为它是根据优先权(Priority)来移除元素。 在这边以 wait()、notify() 中的生产者、消费者程式为例,使用BlockQueue来加以改写,好处是我们不用亲自处理wait、notify的细 节,首先生产者改写如下: ● Producer.java package onlyfun.caterpillar; import java.util.concurrent.BlockingQueue; public class Producer implements Runnable { private BlockingQueue queue; public Producer(BlockingQueue queue) { this.queue = queue; } public void run() { for(int product = 1; product <= 10; product++) { try { // wait for a random time Thread.sleep((int) Math.random() * 3000); http://caterpillar.onlyfun.net/GossipCN/JavaGossip-V2/BlockingQueue.htm(第 1/3 页)2008-7-30 20:07:33 BlockingQueue queue.put(product); } catch(InterruptedException e) { e.printStackTrace(); } } } } 消费者类别改写如下: ● Consumer.java package onlyfun.caterpillar; import java.util.concurrent.BlockingQueue; public class Consumer implements Runnable { private BlockingQueue queue; public Consumer(BlockingQueue queue) { this.queue = queue; } public void run() { for(int i = 1; i <= 10; i++) { try { // wait for a random time Thread.sleep((int) (Math.random() * 3000)); queue.take(); } catch(InterruptedException e) { e.printStackTrace(); } } } } 可以使用下面这个程式来简单的测试一下: ● BlockingQueueDemo.java package onlyfun.caterpillar; import java.util.concurrent.ArrayBlockingQueue; import java.util.concurrent.BlockingQueue; public class BlockingQueueDemo { public static void main(String[] args) { BlockingQueue queue = new ArrayBlockingQueue(1); http://caterpillar.onlyfun.net/GossipCN/JavaGossip-V2/BlockingQueue.htm(第 2/3 页)2008-7-30 20:07:33 BlockingQueue Thread producerThread = new Thread( new Producer(queue)); Thread consumerThread = new Thread( new Consumer(queue)); producerThread.start(); consumerThread.start(); } } http://caterpillar.onlyfun.net/GossipCN/JavaGossip-V2/BlockingQueue.htm(第 3/3 页)2008-7-30 20:07:33 Callable 与 Future From Gossip@caterpillar Java Gossip: Callable 与 Future Callable与Future类别可以协助您完成 Future 模式 。 Callable是个介面,与Runnable类似,有个必须实作的方法,可以启动为另一个执行绪来执行,不过Callable工作完成后,可以传回结 果物件,Callable介面的定义如下: public interface Callable { V call() throws Exception; } 例如您可以使用Callable来完成某个费时的工作,工作结束后传回结果物件,例如求质数: ● PrimeCallable.java package onlyfun.caterpillar; import java.util.ArrayList; import java.util.List; import java.util.concurrent.Callable; public class PrimeCallable implements Callable { private int max; public PrimeCallable(int max) { this.max = max; } public int[] call() throws Exception { int[] prime = new int[max+1]; List list = new ArrayList(); for(int i = 2; i <= max; i++) prime[i] = 1; for(int i = 2; i*i <= max; i++) { // 这边可以改进 if(prime[i] == 1) { for(int j = 2*i; j <= max; j++) { if(j % i == 0) prime[j] = 0; } } } for(int i = 2; i < max; i++) { if(prime[i] == 1) { list.add(i); http://caterpillar.onlyfun.net/GossipCN/JavaGossip-V2/CallableFuture.htm(第 1/3 页)2008-7-30 20:07:38 Callable 与 Future } } int[] p = new int[list.size()]; for(int i = 0; i < p.length; i++) { p[i] = list.get(i).intValue(); } return p; } } 假设现在求质数的需求是在启动PrimeCallable后的几秒之后,则我们可以搭配Future来取得Callable执行的结果,在未来的时间点取 得结果,例如: ● FutureDemo.java package onlyfun.caterpillar; import java.util.concurrent.Callable; import java.util.concurrent.ExecutionException; import java.util.concurrent.FutureTask; public class FutureDemo { public static void main(String[] args) { Callable primeCallable = new PrimeCallable(1000); FutureTask primeTask = new FutureTask(primeCallable); Thread t = new Thread(primeTask); t.start(); try { // 假设现在做其它事情 Thread.sleep(5000); // 回来看看质数找好了吗 if(primeTask.isDone()) { int[] primes = primeTask.get(); for(int prime : primes) { System.out.print(prime + " "); } System.out.println(); } } catch (InterruptedException e) { e.printStackTrace(); } catch (ExecutionException e) { http://caterpillar.onlyfun.net/GossipCN/JavaGossip-V2/CallableFuture.htm(第 2/3 页)2008-7-30 20:07:38 Callable 与 Future e.printStackTrace(); } } } http://caterpillar.onlyfun.net/GossipCN/JavaGossip-V2/CallableFuture.htm(第 3/3 页)2008-7-30 20:07:38 Executors From Gossip@caterpillar Java Gossip: Executors 有时候您需要建立一堆Thread来执行一些小任务,然而频繁的建立Thread有时会是个开销,因为Thread的建立必须与作业系统互动, 如果能建立一个Thread pool来管理这些小的Thread并加以重复使用,对于系统效能会是个改善的方式。 您可以使用Executors来建立Thread pool,Executors有几个static方法,列出如下: 方法 说明 newCachedThreadPool 建立可以快取的Thread,每个Thread预设可idle 60秒 newFixedThreadPool 包括固定数量的Thread newSingleThreadExecutor 只有一个Thread,循序的执行指定给它的每个任务 newScheduledThreadPool 可排程的Thread newSingleThreadScheduledExecutor 单一可排程的Thread 举个简单的实例,下面的程式使用newFixedThreadPool方法建立Thread pool,当中包括五个可以重复使用的Thread,您可以指定 Runnable物件给它,程式中会产生十个Runnable物件,由于Thread pool中只有五个可用的Thread,所以后来建立的五个Runnable必须 等待有空闲的Thread才会被执行: ● ExecutorDemo.java package onlyfun.caterpillar; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; public class ExecutorDemo { public static void main(String[] args) { ExecutorService service = Executors.newFixedThreadPool(5); for(int i = 0; i < 10; i++) { final int count = i; service.submit(new Runnable() { public void run() { System.out.println(count); try { Thread.sleep(2000); } catch (InterruptedException e) { e.printStackTrace(); } http://caterpillar.onlyfun.net/GossipCN/JavaGossip-V2/Excutors.htm(第 1/2 页)2008-7-30 20:07:42 Executors } }); } service.shutdown(); // 最后记得关闭Thread pool } } submit()方法也接受实作Callable介面的物件,最后传回Future物件,可以取得Callable执行过后的传回结果。 如果想利用Executors进行排程,例如排定某个工作30秒后执行: ScheduledExecutorService scheduler = Executors.newSingleThreadScheduledExecutor( ); scheduler.schedule(new Runnable( ) { public void run() { // 排程工作 } }, 30, TimeUnit.SECONDS); 或排定某个工作5秒后执行,之后每30秒执行一次: ScheduledExecutorService scheduler = Executors.newSingleThreadScheduledExecutor( ); final ScheduledFuture future = scheduler.scheduleAtFixedRate(new Runnable( ) { public void run() { // 排程工作 System.out.println("t"); } }, 0, 5, TimeUnit.SECONDS); // 排定 60 秒后取消future scheduler.schedule(new Runnable( ) { public void run( ) { future.cancel(false); } }, 60, TimeUnit.SECONDS); 如上所示,想要取消排程任务,可以呼叫ScheduledFuture的cancel()方法。 http://caterpillar.onlyfun.net/GossipCN/JavaGossip-V2/Excutors.htm(第 2/2 页)2008-7-30 20:07:42 简介 Class From Gossip@caterpillar Java Gossip: 简介 Class 一个Class物件代表了Java应用程式在运行时所载入的类别或介面,也用来表达enum(属于类别的一种)、 annotation(属于介面的 一种)、阵列、原生型态(primitive type)、void。 Class没有公开(public)的建构函式,Class物件是由JVM自动产生,每当一个类别被载入时,JVM就自动为其生成一个Class物件。 您可以透过Object的getClass()来取得每一个物件对应的Class物件,或者是透过class常量(Class literal),下面这个程式简单的使用 getClass()方法来取得String类别的一些基本资讯: ● ClassDemo.java package onlyfun.caterpillar; public class ClassDemo { public static void main(String[] args) { String name = "caterpillar"; Class stringClass = name.getClass(); System.out.println("getName: " + stringClass.getName()); System.out.println("isInterface: " + stringClass.isInterface()); System.out.println("isPrimitive: " + stringClass.isPrimitive()); System.out.println("isArray: " + stringClass.isArray()); System.out.println("SuperClass: " + stringClass.getSuperclass().getName()); } } 执行结果: getName: java.lang.String isInterface: false isPrimitive: false isArray: false SuperClass: java.lang.Object 您也可以直接使用以下的方式来取得String类别的Class物件: Class stringClass = String.class; 在Java中,阵列也属于一个类别,也有对应的Class物件,这个 物件是由所有具相同元素与维度的阵列所共用,而原生型态像是 http://caterpillar.onlyfun.net/GossipCN/JavaGossip-V2/ClassObject.htm(第 1/4 页)2008-7-30 20:07:45 简介 Class boolean, byte, char, short, int, long, float, double以及关键字void,也都有对应的Class物件,您也都可以用类别常量(Class literal)来取 得这些物件,例如: ● ClassDemo.java public class ClassDemo { public static void main(String[] args) { System.out.println(boolean.class.getName()); System.out.println(boolean.class.isPrimitive()); System.out.println(void.class.getName()); } } 执行结果: boolean true void 在一些应用中,您无法事先知道使用者将载入什么类别,您可以使用Class的forName()方法实现动态加载类别,下面这个例子让您 可以指定类别名称 来获得类别的相关资讯: ● ClassDemo.java package onlyfun.caterpillar; public class ClassDemo { public static void main(String[] args) { try { Class c = Class.forName(args[0]); System.out.println("getName: " + c.getName()); System.out.println("isInterface: " + c.isInterface()); System.out.println("isPrimitive: " + c.isPrimitive()); System.out.println("isArray: " + c.isArray()); System.out.println("SuperClass: " + c.getSuperclass().getName()); } catch(ClassNotFoundException e) { e.printStackTrace(); } } } 执行结果: http://caterpillar.onlyfun.net/GossipCN/JavaGossip-V2/ClassObject.htm(第 2/4 页)2008-7-30 20:07:45 简介 Class java ClassDemo java.util.ArrayList getName: java.util.ArrayList isInterface: false isPrimitive: false isArray: false SuperClass: java.util.AbstractList Class物件的讯息是在编译时期就被加入至.class档案中,它是Java支援执行时期型别辨识(RTTI,Run- Time Type Information或 Run-Time Type Identification)的一种方式,在编译时期编译器会先检查对应的.class档案,而执行时期JVM在没有用到该类别时, 就不会载入它,如果 要用到时,JVM会先检查对应的Class物件是否已经载入,如果没有载入,则会寻找对应的.class档案并载入 它。 您可以使用Class.forName()方法动态加载类别,之后使用Class的newInstance()方法产生实例,下面这个程式是个简单的示 范,您可 以动态载入实现List的类别 : ● ClassDemo.java package onlyfun.caterpillar; import java.util.*; public class ClassDemo { public static void main(String[] args) { try { Class c = Class.forName(args[0]); List list = (List) c.newInstance(); for(int i = 0; i < 10; i++) { list.add("element " + i); } for(Object o: list.toArray()) { System.out.println(o); } } catch(ClassNotFoundException e) { e.printStackTrace(); } catch (InstantiationException e) { e.printStackTrace(); } catch (IllegalAccessException e) { e.printStackTrace(); } } } 执行结果: http://caterpillar.onlyfun.net/GossipCN/JavaGossip-V2/ClassObject.htm(第 3/4 页)2008-7-30 20:07:45 简介 Class java ClassDemo java.util.ArrayList element 0 element 1 element 2 element 3 element 4 element 5 element 6 element 7 element 8 element 9 http://caterpillar.onlyfun.net/GossipCN/JavaGossip-V2/ClassObject.htm(第 4/4 页)2008-7-30 20:07:45 从 Class 中获取资讯 From Gossip@caterpillar Java Gossip: 从 Class 中获取资讯 Class物件表示所载入的类别,取得Class物件之后,您就可以取得与类别上相关联的资讯,像是 package、建构函式、方法、资料成 员等等的讯息,而每一个讯息,也会有相应的物件作为其代表。 例如下面这个程式可以让您取得类别所属的package名称: ● ClassDemo.java package onlyfun.caterpillar; public class ClassDemo { public static void main(String[] args) { try { Class c = Class.forName(args[0]); Package p = c.getPackage(); System.out.println(p.getName()); } catch(ClassNotFoundException e) { e.printStackTrace(); } } } Package物件是package的代表,一个执行的结果如下: java ClassDemo java.util.ArrayList java.util 同样的道理,您可以分别取回Field、Constructor、Method等物件,分别代表资料成员、建构子与方法,下面这个程式简单的示范 了一 些类别基本资讯的取得: ● SimpleClassViewer.java package onlyfun.caterpillar; import java.lang.reflect.*; public class SimpleClassViewer { public static void main(String[] args) { try { Class c = Class.forName(args[0]); Package p = c.getPackage(); System.out.printf("package %s;%n", p.getName()); http://caterpillar.onlyfun.net/GossipCN/JavaGossip-V2/InfoFromClass.htm(第 1/4 页)2008-7-30 20:07:50 从 Class 中获取资讯 int m = c.getModifiers(); System.out.print(Modifier.toString(m) + " "); if(Modifier.isInterface(m)) { System.out.print("interface "); } else { System.out.print("class "); } System.out.println(c.getName() + " {"); Field[] fields = c.getDeclaredFields(); for(Field field : fields) { System.out.print("\t" + Modifier.toString(field.getModifiers())); System.out.print(" " + field.getType().getName() + " "); System.out.println(field.getName() + ";"); } Constructor[] constructors = c.getDeclaredConstructors(); for(Constructor constructor : constructors) { System.out.print("\t" + Modifier.toString( constructor.getModifiers())); System.out.println(" " + constructor.getName() + "();"); } Method[] methods = c.getDeclaredMethods(); for(Method method : methods) { System.out.print("\t" + Modifier.toString( method.getModifiers())); System.out.print(" " + method.getReturnType().getName() + " "); System.out.println(method.getName() + "();"); } System.out.println("}"); } catch(ArrayIndexOutOfBoundsException e) { http://caterpillar.onlyfun.net/GossipCN/JavaGossip-V2/InfoFromClass.htm(第 2/4 页)2008-7-30 20:07:50 从 Class 中获取资讯 System.out.println("没有指定类别"); } catch(ClassNotFoundException e) { System.out.println("找不到指定类别"); } } } 执行结果: java onlyfun.caterpillar.SimpleClassViewer java.util.ArrayList package java.util; public class java.util.ArrayList { private static final long serialVersionUID; private transient [Ljava.lang.Object; elementData; private int size; public java.util.ArrayList(); public java.util.ArrayList(); public java.util.ArrayList(); public boolean add(); public void add(); public java.lang.Object clone(); public void clear(); public boolean contains(); public int indexOf(); public int lastIndexOf(); public boolean addAll(); public boolean addAll(); public java.lang.Object get(); public int size(); public [Ljava.lang.Object; toArray(); public [Ljava.lang.Object; toArray(); public boolean remove(); public java.lang.Object remove(); private void writeObject(); public boolean isEmpty(); private void readObject(); public java.lang.Object set(); public void ensureCapacity(); protected void removeRange(); public void trimToSize(); private void RangeCheck(); private void fastRemove(); } 一些类别检视器的实作原理基本上就是上面这个程式所示范的,当然还可以取得更多的资讯,您可以参考Class的线上API文件得到 更多的讯息。 http://caterpillar.onlyfun.net/GossipCN/JavaGossip-V2/InfoFromClass.htm(第 3/4 页)2008-7-30 20:07:50 从 Class 中获取资讯 http://caterpillar.onlyfun.net/GossipCN/JavaGossip-V2/InfoFromClass.htm(第 4/4 页)2008-7-30 20:07:50 简介 ClassLoader From Gossip@caterpillar Java Gossip: 简介 ClassLoader Java在需要使用类别的时候,才会将类别载入,Java的类别载入是由类别载入器(Class loader)来达到的,预设上,在程式启动之 后,主要会有三个类别载入器:Bootstrap Loader、ExtClassLoader与AppClassLoader。 Bootstrap Loader是由C++撰写而成,预设上它负责搜寻JRE所在目录的classes或lib目录下的.jar档案中(例如rt.jar)是否有指定的类 别并载入(实际上是由系统参数sun.boot.class.path指定);预设上ExtClassLoader负责搜寻JRE所在目录的lib/ext 目录下的classes或. jar中是否有指定的类别并载入(实际上是由系统参数java.ext.dirs指定);AppClassLoader则搜寻 Classpath中是否有指定的classes并 载入(由系统参数java.class.path指定)。 Bootstrap Loader会在JVM启动之后载入,之后它会载入ExtClassLoader并将ExtClassLoader的parent设为Bootstrap Loader,然后 BootstrapLoader再载入AppClassLoader,并将AppClassLoader的parent设定为 ExtClassLoader。 在载入类别时,每个类别载入器会先将载入类别的任务交由其parent,如果parent找不到,才由自己负责载入,如果自己也找不到, 就会丢出 NoClassDefFoundError。 每一个类别被载入后,都会有一个Class的实例来代表它,每个Class的实例都会记得是哪个ClassLoader载入它的,可以由Class的 getClassLoader()取得载入该类别的ClassLoader。 来撰写一个简单的程式测试类别载入器,首先写一个测试类别: ● ClassLoaderTest.java public class ClassLoaderTest { static { System.out.println("类别被载入..."); } } 将这个类别先放在Classpath的路径下,然后撰写以下的程式: ● ClassPathDemo.java public class ClassLoaderDemo { public static void main(String[] args) { ClassLoaderTest test = new ClassLoaderTest(); ClassLoader loader = test.getClass().getClassLoader(); System.out.println(loader); System.out.println(loader.getParent()); System.out.println(loader.getParent().getParent()); } } http://caterpillar.onlyfun.net/GossipCN/JavaGossip-V2/IntroduceClassLoader.htm(第 1/2 页)2008-7-30 20:07:54 简介 ClassLoader 执行程式之后,会出现以下的讯息: 类别被载入... sun.misc.Launcher$AppClassLoader@82ba41 sun.misc.Launcher$ExtClassLoader@923e30 null 这表示ClassLoaderTest类别是由sun.misc.Launcher产生的AppClassLoader所载入,而 AppClassLoader的parent是ExtClassLoader (AppClassLoader与ExtClassLoader都是内部类 别),ExtClassLoader的parent则是Bootstrap Loader,最后一个显示null并不是表示没 有ClassLoader,而是因为Bootstrap Loader是由C++撰写而成,在Java中并没有一个实际代表它的类别,因而没有类别实例来表示 Bootstrap Loader。 如果把ClassLoaderTest的class档案放到JRE目录下的lib/ext/classes下,并重新执行程式,您会看到以下的讯息: 类别被载入... sun.misc.Launcher$ExtClassLoader@923e30 null Exception in thread "main" java.lang.NullPointerException at ClassLoaderDemo.main(ClassLoaderDemo.java:9) 这次ClassLoaderTest是由ExtClassLoader找到了,由于Bootstrap Loader并没有代表它的实例,所以是null,因而最后一行试图再使用 getParent()时会出现NullPointException。 如果您将ClassLoaderTest的class档案移至JRE目录下的classes目录下,执行程式的话会出现以下的讯息: 类别被载入... null Exception in thread "main" java.lang.NullPointerException at ClassLoaderDemo.main(ClassLoaderDemo.java:8) 出现null讯息,表示这次ClassLoaderTest是由Bootstrap Loader载入的。 这边只是对类别载入器作个简单的描述,想更深入了解类别载入器,可参考这篇 深入类别载入器。 http://caterpillar.onlyfun.net/GossipCN/JavaGossip-V2/IntroduceClassLoader.htm(第 2/2 页)2008-7-30 20:07:54 自订 ClassLoader From Gossip@caterpillar Java Gossip: 自订 ClassLoader ExtClassLoader与AppClassLoader都是 java.net.URLClassLoader的子类别,您可以在使用java启动程式时,使用以下的指令来指定 ExtClassLoader的搜寻路径: java -Djava.ext.dirs=c:\workspace\ YourClass 可以在使用java启动程式时,使用-classpath或-cp来指定AppClassLoader的搜寻路径,也就是设定Classpath: java -classpath c:\workspace\ YourClass ExtClassLoader与AppClassLoader在程式启动后会在虚拟机器中存在一份,您在程式运行过程中就无法再改变它的搜寻路径,如果在 程式运行过程中,打算动态决定从其它的路径载入类别,就要产生新的类别载入器。 您可以使用URLClassLoader来产生新的类别载入器,它需要java.net.URL作为其参数来指定类别载入的搜寻路径,例如: URL url1 = new URL("file:/d:/workspace/"); URLClassLoader urlClassLoader1 = new URLClassLoader(new URL[] {url1}); Class c1 = urlClassLoader1.loadClass("ClassDemoTest"); 在新增了ClassLoader后,您可以使用它的loadClass()方法来指定要载入的类别名称,新增 ClassLoader时,会自动将新增的 ClassLoader的parent设定为AppClassLoader,并在每次载入类别时,先委托 parent代为搜寻,所以上例中搜寻ClassDemoTest类别时, 会一路往上委托至Bootstrap Loader先开始搜寻,接着是ExtClassLoader、AppClassLoader,如果都找不到,才使用新增的ClassLoader 搜寻。 由同一个ClassLoader载入的类别档案,会只有一份Class实例,如果同一个类别档案是由两个不同的ClassLoader载入,则会有两份不 同的Class实例,注意这个说法,如果有两个不同的ClassLoader搜寻同一个类别,如果在parent的 AppClassLoader搜寻路径中就可以 找到,则Class实例就只会有一个,如果是由各自的ClassLoader搜寻到,则Class的实例会 有两份。 下面这个实例是个简单的示范,其中"file:/d:/workspace/"不在ExtClassLoader或AppClassLoader的搜寻路径中,所以同一个类别会分由 两个ClassLoader载入,因而会有两份Class实例: ● ClassLoaderDemo.java import java.net.MalformedURLException; import java.net.URL; import java.net.URLClassLoader; public class ClassLoaderDemo { public static void main(String[] args) throws MalformedURLException, ClassNotFoundException { URL url1 = new URL("file:/d:/workspace/"); URLClassLoader urlClassLoader1 = new URLClassLoader(new URL[] {url1}); Class c1 = http://caterpillar.onlyfun.net/GossipCN/JavaGossip-V2/CustomClassLoader.htm(第 1/2 页)2008-7-30 20:07:58 自订 ClassLoader urlClassLoader1.loadClass("ClassLoaderTest"); System.out.println(c1); URL url2 = new URL("file:/d:/workspace/"); URLClassLoader urlClassLoader2 = new URLClassLoader(new URL[] {url2}); Class c2 = urlClassLoader2.loadClass("ClassLoaderTest"); System.out.println(c2); System.out.println(c1 == c2); } } 执行结果会显示false;如果将ClassLoaderTest移至Classpath下,也就是 AppClassLoader可以搜寻到的路径下,结果是ClassLoaderTest 会被AppClassLoader载入,即使我们使用两个自订的 ClassLoader,但载入的Class实例也只有一个,再次执行同一个程式,则结果会 显示true。 http://caterpillar.onlyfun.net/GossipCN/JavaGossip-V2/CustomClassLoader.htm(第 2/2 页)2008-7-30 20:07:58 生成物件 From Gossip@caterpillar Java Gossip: 生成物件 您可以使用Class的newInstance()方法来实例化一个物件,例如: Class c = Class.forName(args[0]); Object obj = c.newInstance(); 如果载入的类别定义有无参数的建构函式,则可以使用这种方式来建构一个不指定初始参数的物件,如果您要在动态载入及生成物 件时指定参数,则要先指定参数类别、取得Constructor物件、使用Constructor的newInstance()并指定参数值。 以一个例子来说明,首先我们定义一个Student类: ● Student.java package onlyfun.caterpillar; public class Student { private String name; private int score; public Student() { name = "N/A"; } public Student(String name, int score) { this.name = name; this.score = score; } public void setName(String name) { this.name = name; } public void setScore(int score) { this.score = score; } public String getName() { return name; } public int getScore() { return score; } public String toString() { return name + " " + score; } http://caterpillar.onlyfun.net/GossipCN/JavaGossip-V2/NewInstance.htm(第 1/3 页)2008-7-30 20:08:03 生成物件 } 然后以动态载入的方式来动态生成物件: ● NewInstanceDemo.java package onlyfun.caterpillar; import java.lang.reflect.Constructor; import java.lang.reflect.InvocationTargetException; public class NewInstanceDemo { public static void main(String[] args) { Class c = null; try { c = Class.forName(args[0]); // 指定参数型态 Class[] params = new Class[2]; params[0] = String.class; params[1] = Integer.TYPE; Constructor constructor = c.getConstructor(params); // 指定参数内容 Object[] paramObjs = new Object[2]; paramObjs[0] = "caterpillar"; paramObjs[1] = new Integer(90); // 实例化 Object obj = constructor.newInstance(paramObjs); System.out.println(obj); } catch (ClassNotFoundException e) { e.printStackTrace(); } catch (SecurityException e) { e.printStackTrace(); } catch (NoSuchMethodException e) { e.printStackTrace(); } catch (IllegalArgumentException e) { e.printStackTrace(); } catch (InstantiationException e) { e.printStackTrace(); http://caterpillar.onlyfun.net/GossipCN/JavaGossip-V2/NewInstance.htm(第 2/3 页)2008-7-30 20:08:03 生成物件 } catch (IllegalAccessException e) { e.printStackTrace(); } catch (InvocationTargetException e) { e.printStackTrace(); } } } 执行的例子如下: java onlyfun.caterpillar.NewInstanceDemo onlyfun.caterpillar.Student caterpillar 90 http://caterpillar.onlyfun.net/GossipCN/JavaGossip-V2/NewInstance.htm(第 3/3 页)2008-7-30 20:08:03 呼叫方法 From Gossip@caterpillar Java Gossip: 呼叫方法 如果您会动态载入类别并使用有参数的建构函式(参考 生成物件),则动态呼叫所生成物件之方法并不是难事,直接以实例说明, 首先写一个 Student类别: ● Student.java package onlyfun.caterpillar; public class Student { private String name; private int score; public Student() { name = "N/A"; } public Student(String name, int score) { this.name = name; this.score = score; } public void setName(String name) { this.name = name; } public void setScore(int score) { this.score = score; } public String getName() { return name; } public int getScore() { return score; } public void showData() { System.out.println("name: " + name); System.out.println("score: " + score); } } 再来写个动态载入与呼叫方法的程式: ● InvokeMethod.java http://caterpillar.onlyfun.net/GossipCN/JavaGossip-V2/InvokeMethod.htm(第 1/3 页)2008-7-30 20:08:07 呼叫方法 package onlyfun.caterpillar; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; public class InvokeMethodDemo { public static void main(String[] args) { try { Class c = Class.forName(args[0]); Object targetObj = c.newInstance(); Class[] param1 = {String.class}; Method setName = c.getMethod("setName", param1); Object[] paramObjs1 = {"caterpillar"}; setName.invoke(targetObj, paramObjs1); Class[] param2 = {Integer.TYPE}; Method setScore = c.getMethod("setScore", param2); Object[] paramObjs2 = {new Integer(90)}; setScore.invoke(targetObj, paramObjs2); Method showData = c.getMethod("showData", new Class[0]); showData.invoke(targetObj, new Object[0]); } catch (ClassNotFoundException e) { e.printStackTrace(); } catch (SecurityException e) { e.printStackTrace(); } catch (NoSuchMethodException e) { e.printStackTrace(); } catch (IllegalArgumentException e) { e.printStackTrace(); } catch (IllegalAccessException e) { e.printStackTrace(); } catch (InvocationTargetException e) { e.printStackTrace(); } catch (InstantiationException e) { e.printStackTrace(); } } } http://caterpillar.onlyfun.net/GossipCN/JavaGossip-V2/InvokeMethod.htm(第 2/3 页)2008-7-30 20:08:07 呼叫方法 执行结果: java InvokeMethodDemo onlyfun.caterpillar.Student name: caterpillar score: 90 在很少的情况下,您会需要突破Java的存取限制来呼叫受护的或私有的方法(例如您拿到一个组件,但您没法修改它的原始码,而 您又一定要呼叫某个私有方法),这时候您可以使用反射机制来达到您的目的,一个存取私有方法的例子如下: Method privateTest = c.getDeclaredMethod("privateTest", new Class[0]); privateTest.setAccessible(true); privateTest.invoke(targetObj, new Object[0]); 所以在Java中即使宣告为私有方法或成员,仍可以透过反射机制存取私有方法,要突破权限仍是可以的,但该如何用则取决于您自 己。 http://caterpillar.onlyfun.net/GossipCN/JavaGossip-V2/InvokeMethod.htm(第 3/3 页)2008-7-30 20:08:07 修改成员值 From Gossip@caterpillar Java Gossip: 修改成员值 尽管直接存取Field成员是不被鼓励的,但您仍是可以直接操作公开的(public)Field成员,而您也可 以透过动态载入的方式来操作 Field成员,这边以一个实例来说明,首先撰写个TestedField类别: ● TestedField.java package onlyfun.caterpillar; public class TestedField { public int testInt; public String testString; public String toString() { return testInt + " " + testString; } } 再来看看如何透过动态载入来存取Field成员: ● AssignFieldDemo.java package onlyfun.caterpillar; import java.lang.reflect.Field; public class AssignFieldDemo { public static void main(String[] args) { try { Class c = Class.forName(args[0]); Object targetObj = c.newInstance(); Field testInt = c.getField("testInt"); testInt.setInt(targetObj, 99); Field testString = c.getField("testString"); testString.set(targetObj, "caterpillar"); System.out.println(targetObj); } catch (ClassNotFoundException e) { e.printStackTrace(); } catch (SecurityException e) { e.printStackTrace(); } catch (NoSuchFieldException e) { http://caterpillar.onlyfun.net/GossipCN/JavaGossip-V2/ModifyField.htm(第 1/2 页)2008-7-30 20:08:12 修改成员值 e.printStackTrace(); } catch (InstantiationException e) { e.printStackTrace(); } catch (IllegalAccessException e) { e.printStackTrace(); } } } 执行结果: java onlyfun.caterpillar.AssignFieldDemo onlyfun.caterpillar.TestedField 99 caterpillar 在 呼叫方法 中介绍了如何存取私有的(private)方法,同样的道理,如果必要,仍是可以透过反射机制来存取私有的Field成员,例 如: Field privateField = c.getDeclaredField("privateField"); privateField.setAccessible(true); privateField.setInt(targetObj, 99); 当然即使类别在定义时被宣告为私有,就表示不希望您存取它,若要透用反射机制来突破这个限制,则您要清楚您在作些什么。 http://caterpillar.onlyfun.net/GossipCN/JavaGossip-V2/ModifyField.htm(第 2/2 页)2008-7-30 20:08:12 生成阵列 From Gossip@caterpillar Java Gossip: 生成阵列 要使用反射机制动态载入类别并生成阵列的话,可以使用java.lang.reflect.Array来协助,下 面这个例子简单的示范了如何生成String 阵列: ● NewArrayDemo.java package onlyfun.caterpillar; import java.lang.reflect.Array; public class NewArrayDemo { public static void main(String[] args) { Class c = String.class; Object objArr = Array.newInstance(c, 5); for(int i = 0; i < 5; i++) { Array.set(objArr, i, i+""); } for(int i = 0; i < 5; i++) { System.out.println(Array.get(objArr, i)); } } } 执行结果: 0 1 2 3 4 二维以上阵列的话,可以透过一个int阵列来宣告其维度,例如下面这个程式示范如何设定与取得二维阵列的值: ● NewArrayDemo.java package onlyfun.caterpillar; import java.lang.reflect.Array; public class NewArrayDemo { public static void main(String[] args) { Class c = String.class; http://caterpillar.onlyfun.net/GossipCN/JavaGossip-V2/ArrayInstance.htm(第 1/2 页)2008-7-30 20:08:18 生成阵列 int[] dim = new int[]{3, 4}; Object objArr = Array.newInstance(c, dim); for(int i = 0; i < 3; i++) { Object row = Array.get(objArr, i); for(int j = 0; j < 4; j++) { Array.set(row, j, "" + (i+1)*(j+1)); } } for(int i = 0; i < 3; i++) { Object row = Array.get(objArr, i); for(int j = 0; j < 4; j++) { System.out.print(Array.get(row, j) + " "); } System.out.println(); } } } 执行结果: 1 2 3 4 2 4 6 8 3 6 9 12 http://caterpillar.onlyfun.net/GossipCN/JavaGossip-V2/ArrayInstance.htm(第 2/2 页)2008-7-30 20:08:18 限定 Override 父类方法 - Override From Gossip@caterpillar Java Gossip: 限定 Override 父类方法 - Override java.lang.Override是J2SE 5.0中标准的Annotation型态之一,它对编译器说明某个方法必须是重新定义父类别中的方法,编译器得知 这项资讯后,在编译程式时如果发现该方法 并非重新定义父类别中的方法,就会回报错误。 举个例子来说,如果您在定义新类别时想要重新定义toString()方法,您可能会写成这样: ● CustomObject.java public class CustomObject { public String ToString() { return "customObject"; } } 在撰写toString()方法时,您因为打字错误或其它的疏忽,将之打成了ToString(),但您编译这个类别时并不会出现任何的错误,编译 器只当 您是定义了一个新的ToString()。 您可以使用java.lang.Override这个Annotation型态,在方法上加上一个@Override的Annotation,告诉编译器 您现在定义的这个类别是 重新定义父类别中的同名方法,例如: ● CustomObject.java public class CustomObject { @Override public String ToString() { return "customObject"; } } 在编译程式时,编译器看到@Override这个Annotation,了解它必须检查这个方法是不是重新定义父类别的ToString()方法,但父类 别中并没有这个方法,所以它会回报错误: CustomObject.java:2: method does not override a method from its superclas @Override ^ 1 error 重新修改一下程式,编译时就不会有问题了: ● CustomObject.java public class CustomObject { @Override public String toString() { return "customObject"; } http://caterpillar.onlyfun.net/GossipCN/JavaGossip-V2/AnnotationOverride.htm(第 1/2 页)2008-7-30 20:08:25 限定 Override 父类方法 - Override } java.lang.Override是个Marker annotation,简单的说就是用于标示,annotation名称本身即包括了要给工具程式的资讯。 http://caterpillar.onlyfun.net/GossipCN/JavaGossip-V2/AnnotationOverride.htm(第 2/2 页)2008-7-30 20:08:25 标示方法为 Deprecated - Deprectated From Gossip@caterpillar Java Gossip: 标示方法为 Deprecated - Deprectated java.lang.Deprectated是J2SE 5.0中标准的Annotation型态之一,它对编译器说明某个方法已经不建议使用,如果有人试图使用或重新 定义该方法,必须提出警示讯息。 举个例子来说,您可能定义一个CustomObject类别,并在当中定义有getSomething()方法,而在一段时间之后,您不建议使用这个方 法 了,并要将这个方法标示为deprectated,您可以这么作 : ● CustomObject.java public class CustomObject { @Deprecated public String getSomething() { return "something"; } } 如果有人试图在继承这个类别后重新定义getSomething(),或是在程式中呼叫使用getSomething()方法,则进行编译时,就会出现这 个警讯: Note: SubCustomObject.java uses or overrides a deprecated API. Note: Recompile with -Xlint:deprecation for details. 想要知道详细的警讯内容的话,可以在编译时加上-Xline:deprecation引数,例如: >javac -Xlint:deprecation SubCustomObject.java SubCustomObject.java:5: warning: [deprecation] getSomething() in CustomObject ha s been deprecated object.getSomething(); ^ 1 warning java.lang.Deprecated是个Marker annotation,简单的说就是用于标示,annotation名称本身即包括了要给工具程式的资讯。 http://caterpillar.onlyfun.net/GossipCN/JavaGossip-V2/AnnotationDeprecated.htm2008-7-30 20:08:29 抑制编译器警讯 - SuppressWarnings From Gossip@caterpillar Java Gossip: 抑制编译器警讯 - SuppressWarnings java.lang.SuppressWarnings是J2SE 5.0中标准的Annotation型态之一,它对编译器说明某个方法中若有警示讯息,则加以抑制,不用 在编译完成后出现警讯。 在这边到说明SuppressWarnings的功能,考虑下面这个类别: ● SomeObject.java import java.util.*; public class SomeObject { public void doSomething() { Map map = new HashMap(); map.put("some", "thing"); } } 在J2SE 5.0中加入了集合物件的Generics支援,并建议您明确的指定集合物件将内填的物件之型态,在上面这个类别中使用Map时并 没有指定内填物件之型 别,在编译时会出现以下的讯息: Note: SomeObject.java uses unchecked or unsafe operations. Note: Recompile with -Xlint:unchecked for details. 在编译时一并指定-Xlint:unchecked可以看到警示的细节: SomeObject.java:7: warning: [unchecked] unchecked call to put(K,V) as a member o f the raw type java.util.Map map.put("some", "thing"); ^ 1 warning 如果您想让编译器忽略这些细节,则可以如下使用SuppressWarnings这个Annotation: ● SomeObject.java import java.util.*; public class SomeObject { @SuppressWarnings(value={"unchecked"}) public void doSomething() { Map map = new HashMap(); map.put("some", "thing"); } } 这么一来,编译器将忽略掉"unckecked"的警讯,您也可以指定忽略多个警讯: @SuppressWarnings(value={"unchecked", "deprecation"}) http://caterpillar.onlyfun.net/GossipCN/JavaGossip-V2/AnnotationSuppressWarnings.htm(第 1/2 页)2008-7-30 20:08:33 抑制编译器警讯 - SuppressWarnings http://caterpillar.onlyfun.net/GossipCN/JavaGossip-V2/AnnotationSuppressWarnings.htm(第 2/2 页)2008-7-30 20:08:33 自订 Annotation 型态 From Gossip@caterpillar Java Gossip: 自订 Annotation 型态 您可以自订Annotation型别,并使用这些自订的Annotation型别在程式码中使用 Annotation,这些Annotation将提供资讯给您的程式码 分析工具。 首先来看看如何定义Marker Annotation,也就是Annotation名称本身即提供资讯,对于程式分析工具来说,主要是检查是否有Marker Annotation的出现,并作出对应的动作。 要定义一个Annotation所需的动作,就类似于定义一个介面(interface),只不过您使用的是@interface,例如定义一个 Debug Annotation型态: ● Debug.java package onlyfun.caterpillar; public @interface Debug {} 由于是个Marker Annotation,所以没有任何的成员在里头,编译完成后,您就可以在程式码中使用这个Annotation了,例如: ● SomeObject.java package onlyfun.caterpillar; import java.util.*; public class SomeObject { @Debug public void doSomething() { // .... } } 接着来看看如何定义一个Single-value Annotation,它只有一个成员,名称为value,例如您可以提供Debug Annotation更多资讯: ● Debug.java package onlyfun.caterpillar; public @interface Debug { String value(); } 实际上您定义了value()方法,编译器在编译时会自动帮您产生一个value的变数成员,接着在使用Debug Annotation时要指定值,例 如: ● SomeObject.java package onlyfun.caterpillar; import java.util.*; http://caterpillar.onlyfun.net/GossipCN/JavaGossip-V2/CustomAnnotation.htm(第 1/3 页)2008-7-30 20:08:36 自订 Annotation 型态 public class SomeObject { @Debug("unit test") public void doSomething() { // .... } } @Debug("unit test")实际上是@Debug(value="unit test")的简便写法,如果是阵列值,例如: ● Debug.java package onlyfun.caterpillar; public @interface Debug { String[] value(); } 则使用Annotation时,可以写成@Debug({"value1", "value2"}),或是@Debug(value={"value1", "value2"})。 您也可以对成员设定预设值,使用default关键字即可,例如: ● Debug.java package onlyfun.caterpillar; public @interface Debug { String value() default "none"; } 这么一来如果您使用@Debug时没有指定值,则预设就是"none"。 您也可以为Annotation定义额外的成员,以提供额外的资讯给分析工具,例如: ● Debug.java package onlyfun.caterpillar; public @interface Debug { public enum Level {NONE, UNIT, INTEGRATION, FUNCTION}; Level level() default Level.NONE; String name(); String tester(); } 则您可以如下使用这个Annotation: ● SomeObject.java package onlyfun.caterpillar; import java.util.*; public class SomeObject { @Debug( level = Debug.Level.UNIT, http://caterpillar.onlyfun.net/GossipCN/JavaGossip-V2/CustomAnnotation.htm(第 2/3 页)2008-7-30 20:08:36 自订 Annotation 型态 name = "some test", tester = "justin" ) public void doSomething() { // .... } } @interface实际上是自动继承自java.lang.annotation.Annotation,所以定义Annotation时不能继承其它 Annotation或是interface。 http://caterpillar.onlyfun.net/GossipCN/JavaGossip-V2/CustomAnnotation.htm(第 3/3 页)2008-7-30 20:08:36 告知编译器如何处理 annotaion - Retention From Gossip@caterpillar Java Gossip: 告知编译器如何处理 annotaion - Retention java.lang.annotation.Retention可以在您定义Annotation型态时,指示编 译器如何对待您的自定义 Annotation,预设上编译器会将 Annotation资讯留在class档案中,但不被虚拟机器读取,而仅用于编译器或工具程式运行时提供资讯。 在使用Retention型态时,需要提供java.lang.annotation.RetentionPolicy的列举型态: package java.lang.annotation; public enum RetentionPolicy { SOURCE, // 编译器处理完Annotation资讯后就没事了 CLASS, // 编译器将Annotation储存于class档中,预设 RUNTIME // 编译器将Annotation储存于class档中,可由VM读入 } RetentionPolicy为SOURCE的例子是SuppressWarnings,这个资讯的作用仅在告知编译器抑制警讯,所以不必将这个资讯储 存于class 档案。 RetentionPolicy为RUNTIME的时机,可像是您使用Java设计一个程式码分析工具,您要VM读出Annotation资讯,以在分析 程式中使 用,搭配Reflection机制,就可以达到这个目的。 在J2SE 5.0中新增了java.lang.reflect.AnnotatedElement这个介面,当中定义有四个方法: public Annotation getAnnotation(Class annotationType); public Annotation[] getAnnotations(); public Annotation[] getDeclaredAnnotations(); public boolean isAnnotationPresent(Class annotationType); Class、Constructor、Field、Method、Package等类别,都实作了 AnnotatedElement这个介面,所以您可以从这些类别的实例上,分 别取得标示于其上的Annotation与其资讯,如果 RetentionPolicy为RUNTIME的话。 举个例子来说,假设您设计了以下的Debug Annotation: ● Debug.java package onlyfun.caterpillar; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; @Retention(RetentionPolicy.RUNTIME) public @interface Debug { String value(); String name(); } 由于RetentionPolicy为RUNTIME,编译器在处理Debug Annotation时,会将之编译至class档中,并可以VM读出Annotation资讯,接着 我们将Debug用于程式中: ● SomeObject.java http://caterpillar.onlyfun.net/GossipCN/JavaGossip-V2/AnnotationRetention.htm(第 1/3 页)2008-7-30 20:08:39 告知编译器如何处理 annotaion - Retention package onlyfun.caterpillar; public class SomeObject { @Debug( value = "unit", name = "debug1" ) public void doSomething() { // .... } } 可以设计一个工具程式来读取Annotation资讯: ● DebugTool.java package onlyfun.caterpillar; import java.lang.annotation.Annotation; import java.lang.reflect.Method; public class DebugTool { public static void main(String[] args) throws NoSuchMethodException { Class c = SomeObject.class; Method method = c.getMethod("doSomething"); if(method.isAnnotationPresent(Debug.class)) { System.out.println("@Debug is found."); Debug debug = method.getAnnotation(Debug.class); System.out.println("\tvalue = " + debug.value()); System.out.println("\tname = " + debug.name()); } else { System.out.println("@Debug is not found."); } Annotation[] annotations = method.getAnnotations(); for(Annotation annotation : annotations) { System.out.println( annotation.annotationType().getName()); } } } 程式的执行结果如下: http://caterpillar.onlyfun.net/GossipCN/JavaGossip-V2/AnnotationRetention.htm(第 2/3 页)2008-7-30 20:08:39 告知编译器如何处理 annotaion - Retention @Debug is found. value = unit name = debug1 onlyfun.caterpillar.Debug http://caterpillar.onlyfun.net/GossipCN/JavaGossip-V2/AnnotationRetention.htm(第 3/3 页)2008-7-30 20:08:39 限定 annotation 使用对象 - Target From Gossip@caterpillar Java Gossip: 限定 annotation 使用对象 - Target 在定义Annotation型态时,您使用java.lang.annotation.Target可以定义其适 用之时机,在定义时可以指定java.lang.annotation. ElementType的列举值: package java.lang.annotation; public enum ElementType { TYPE, // 适用 class, interface, enum FIELD, // 适用 field METHOD, // 适用 method PARAMETER, // 适用 method 上之 parameter CONSTRUCTOR, // 适用 constructor LOCAL_VARIABLE, // 适用区域变数 ANNOTATION_TYPE, // 适用 annotation 型态 PACKAGE // 适用 package } 举个例子来说,假设您定义Annotation只能适用于constructor与method: ● Debug.java package onlyfun.caterpillar; import java.lang.annotation.Target; import java.lang.annotation.ElementType; @Target({ElementType.CONSTRUCTOR, ElementType.METHOD}) public @interface Debug {} 如果您尝试将Debug用于class上: ● SomeObject.java package onlyfun.caterpillar; @Debug public class SomeObject { public void doSomething() { // .... } } 则在编译时会发生以下的错误: SomeObject.java:3: annotation type not applicable to this kind of declaration @Debug ^ http://caterpillar.onlyfun.net/GossipCN/JavaGossip-V2/AnnotationTarget.htm(第 1/2 页)2008-7-30 20:08:43 限定 annotation 使用对象 - Target 1 error http://caterpillar.onlyfun.net/GossipCN/JavaGossip-V2/AnnotationTarget.htm(第 2/2 页)2008-7-30 20:08:43 要求为 API 文件的一部份 - Documented From Gossip@caterpillar Java Gossip: 要求为 API 文件的一部份 - Documented 在制作JavaDoc文件时,预设上并不会将Annotation的资料加入到文件中,例如您设计了以下的 Annotation型别: ● Debug.java package onlyfun.caterpillar; public @interface Debug {} 然后将之用在以下的程式中: ● SomeObject.java package onlyfun.caterpillar; public class SomeObject { @Debug public void doSomething() { // .... } } 您可以试着产生Java Doc文件,文件中并不会有Annotation的讯息。 Annotation用于为程式码作注解,有时它包括了重要的讯息,您也许会想要使用者制作文件的同时,也一并将Annotation的讯息加 入,您可以 使用java.lang.annotation.Documented,例如: ● Debug.java package onlyfun.caterpillar; import java.lang.annotation.Documented; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; @Documented @Retention(RetentionPolicy.RUNTIME) public @interface Debug {} http://caterpillar.onlyfun.net/GossipCN/JavaGossip-V2/AnnotationDocumented.htm(第 1/2 页)2008-7-30 20:08:46 要求为 API 文件的一部份 - Documented 使用java.lang.annotation.Documented时,同时的您必须使用Retention指定编译器将讯 息加入class档案,并可以由VM读取,即设定 RetentionPolicy为RUNTIME,接着您可以重新产生Java Doc文件,这次可以看到文件中包括了@Debug的讯息。 http://caterpillar.onlyfun.net/GossipCN/JavaGossip-V2/AnnotationDocumented.htm(第 2/2 页)2008-7-30 20:08:46 子类是否继承父类的 annotation - Inherited From Gossip@caterpillar Java Gossip: 子类是否继承父类的 annotation - Inherited 在您定义Annotation型态后并使用于程式码上时,预设上父类别中的Annotation并不会被继承至子 类别中,您可以在定义 Annotation 时加上java.lang.annotation.Inherited的Annotation,这让您定义的Annotation型 别被继承下来。 例如: ● Debug.java package onlyfun.caterpillar; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Inherited; @Retention(RetentionPolicy.RUNTIME) @Inherited public @interface Debug { String value(); String name(); } 在下面这个程式中使用它: ● SomeObject.java package onlyfun.caterpillar; public class SomeObject { @Debug( value = "unit", name = "debug1" ) public void doSomething() { // .... } } 如果您有一个类别继承自SomeObject,则理想上@Debug也会被继承下来,不过事实上Inherited在Sun JDK 5.0中还没有作用。 http://caterpillar.onlyfun.net/GossipCN/JavaGossip-V2/AnnotationInherited.htm2008-7-30 20:08:50 简介 JDBC From Gossip@caterpillar Java Gossip: 简介 JDBC JDBC是用于执行SQL的Java API,它将资料库存取的API与SQL陈述分开,实现资料库无关的API介面,藉由JDBC统一的介面,开发 人员只要专注于SQL陈述,而可以不必理会底层的资料库驱动程式与相关介面。 使用JDBC,由厂商实作资料库的介面,而SQL的操作部份由Java程式设计人员负责,如果要更换驱动程式,则只要载入新的驱动程 式来源即可,Java 程式的部份则无需改变。 简单的说,JDBC让Java程式设计人员在撰写资料库程式的时候,可以“写一个程式,适用所有的资料库”。 下图JDBC API、资料库驱动程式与资料库之间的关系: JDBC资料库驱动程式依实作方式可以分为四个类型: ● Type 1:JDBC-ODBC Bridge 使用者的电脑上必须事先安装好ODBC驱动程式,Type 1驱动程式利用Bridge的方式将JDBC的呼叫方式转换为ODBC的呼叫 方式,用于Microsoft Access之类的资料库存取: Application <--> JDBC-ODBC Bridge <--> ODBC Driver <--> Database ● Type 2:Native-API Bridge http://caterpillar.onlyfun.net/GossipCN/JavaGossip-V2/IntroduceJDBC.htm(第 1/2 页)2008-7-30 20:08:54 简介 JDBC 驱动程式上层包装Java程式以与Java应用程式作沟通,将JDBC呼叫转为原生程式码的呼叫,下层为原生语 言(像是C、C+ +)来与资料库作沟通,下层的函式库是针对特定资料库设计的,不若Type 1可以对ODBC架构的资料库作存取: Application <--> Native-API Bridge <--> Native Driver <--> Database ● Type 3:JDBC-middleware 透过中间件来存取资料库,使用者不必安装特定的驱动程式,而是由驱动程式呼叫中间件,由中间件来完成所有的资料 库 存取动作,然后将结果传回给驱动程式: Application <--> JDBC-middleware <--> middleware <--> Database ● Type 4:Pure Java Driver 使用纯Java撰写驱动程式与资料库作沟通,而不透过桥接或中间件来存取资料库: Application <--> Pure Java Driver <--> Database MySQL的JDBC驱动程式属于Type 4,称之为Connector/J,目前有支援JDBC 2.0与JDBC 3.0的版本,您可以在以下的网站取得: http://www.mysql.com/products/connector-j/index.html http://caterpillar.onlyfun.net/GossipCN/JavaGossip-V2/IntroduceJDBC.htm(第 2/2 页)2008-7-30 20:08:54 连接资料库 From Gossip@caterpillar Java Gossip: 连接资料库 为了要连线MySQL资料库,您必须要有MySQL JDBC驱动程式,请将下载的mysql-connector-java-*.jar加入至CLASSPATH中,这 边先以Java Class来示范如何连线MySQL,Java类别中与资料库操作相关的类别都位于java.sql套件中。 要连线MySQL,必须经由几个动作: ● 载入与注册JDBC驱动程式 透过java.lang.Class类别的forName()来载入并向DriverManager注册JDBC驱动程式(驱动程式会自动透过DriverManager. registerDriver()方法注册), MySQL的驱动程式类别是com.mysql.jdbc.Driver,您如下载入与注册JDBC驱动程式: try { Class.forName("com.mysql.jdbc.Driver"); } catch(ClassNotFoundException e) { System.out.println("找不到驱动程式类别"); } 如果找不到com.mysql.jdbc.Driver类别,就会丢出 ClassNotFoundException,这时请确定您的CLASSPATH中是否包括了 mysql-connector-java-*.jar的位置。 ● 提供JDBC URL JDBC URL定义了连接资料库时的协定、子协定、资料来源职别: 协定:子协定:资料来源识别 协定在JDBC中总是jdbc开始;子协定是桥接的驱动程式或是资料库管理系统名称,使用MySQL的话是 mysql;资料来源识 别标出找出资料库来源的位址;MySQL的JDBC URL撰写方式如下: jdbc:mysql://主机名称:连接埠/资料库名称?参数1=值1&参数2=值2 主机名称可以是本机localhost或是其它连线主机,连接埠为3306,假如我们要连线GUESTBOOK 资料库,并指明使用者名称 与密码,可以如下指定: jdbc:mysql://localhost:3306/GUESTBOOK?user=caterpillar&password=123456 如果要使用中文存取的话,还必须给定参数userUnicode及characterEncoding,表明是否使用Unicode,并指定字元编码方 式,例如: jdbc:mysql://localhost:3306/GUESTBOOK?user=caterpillar&password=123456 &useUnicode=true&characterEncoding=Big5 中文的编码方式除了Big5之外,还可以使用UTF8,这可以避免Big5中文字“许”、“功”、“盖”等存入 MySQL所发生的问题。 ● 从DriverManager取得Connection 要连线资料库,我们可以从DriverManager要求并取得Connection物件,它代表资料库连线物件,您可以直接给它JDBC URL 作为参数并取回Connection物件: try { String url = "jdbc:mysql://localhost:3306/GUESTBOOK?" + "user=caterpillar&password=123456"; Connection conn = DriverManager.getConnection(url); http://caterpillar.onlyfun.net/GossipCN/JavaGossip-V2/ConnectDB.htm(第 1/3 页)2008-7-30 20:08:59 连接资料库 if(!conn.isClosed()) System.out.println("资料库连线成功"); conn.close(); } catch(SQLException e) { .... } 取得Connection物件之后,您可以测试与资料库的连线是否关闭,即使用isClosed(),在操作完资料库之后,必须使用close() 来关闭与资料库的连线。 getConnection()方法也提供给定使用者名称与密码的方式来给定参数,例如: String url = "jdbc:mysql://localhost:3306/GUESTBOOK"; String user = "caterpillar"; String password = "123456"; Connection conn = DriverManager.getConnection(url, user, password); 下面这个程式是用来测试与资料库的连线是否成功的一个完整范例: ● DBConnectionDemo.java package onlyfun.caterpillar; import java.sql.*; public class DBConnectionDemo { public static void main(String[] args) { String driver = "com.mysql.jdbc.Driver"; String url = "jdbc:mysql://localhost:3306/GUESTBOOK"; String user = "caterpillar"; String password = "123456"; try { Class.forName(driver); Connection conn = DriverManager.getConnection(url, user, password); if(conn != null && !conn.isClosed()) { System.out.println("资料库连线测试成功!"); conn.close(); } } catch(ClassNotFoundException e) { http://caterpillar.onlyfun.net/GossipCN/JavaGossip-V2/ConnectDB.htm(第 2/3 页)2008-7-30 20:08:59 连接资料库 System.out.println("找不到驱动程式类别"); e.printStackTrace(); } catch(SQLException e) { e.printStackTrace(); } } } http://caterpillar.onlyfun.net/GossipCN/JavaGossip-V2/ConnectDB.htm(第 3/3 页)2008-7-30 20:08:59 连接资料库 - JDBC 4.0 From Gossip@caterpillar Java Gossip: 连接资料库 - JDBC 4.0 在JDBC 4.0之前,如果您要连接资料库的话,必须使用Class.forName()并指定驱动程式类别名称,以载入JDBC驱动程式,例如: String url = …; String username = …; String password = …; String driver = …; Class.forName(driver); Connection conn = DriverManager.getConnection(url, username, password); 在JDBC 4.0之中,不需要再呼叫Class.forName()并指定驱动程式了,也就是说,只要一行就可以了: String url = …; String username = …; String password = …; Connection conn = DriverManager.getConnection(url, username, password); 那么JVM如何得知要载入哪个驱动程式呢?JVM会自动在Classpath中寻找适当的驱动程式,在包装有JDBC驱动程式的JAR档案中, 必须有一个 "META-INF/services/java.sql.Driver"档案,当中撰写驱动程式类别名称,以JDK6所附的 Apache Derby 为例,在JDK目 录/db/lib/derbyclient.jar中的"META-INF/services/java.sql.Driver"档案 中,撰写的是"org.apache.derby.jdbc.ClientDriver "。 http://caterpillar.onlyfun.net/GossipCN/JavaGossip-V2/ConnectDB-JDBC4.htm2008-7-30 20:09:03 Statement、 ResultSet From Gossip@caterpillar Java Gossip: Statement、 ResultSet Connection物件是代表Java与资料库的连线,接下来我们要执行SQL的话,必须取得 Statement物件,它代替您执行SQL叙述并取得 执行之后的结果,您可以使用Connection的createStatement()来建立Statement物件: Connection conn = DriverManager.getConnection( url, user, password); Statement stmt = conn.createStatement(); 取得Statement物件之后,我们可以使用executeUpdate()、executeQuery()等方法来执行 SQL,executeUpdate()主要是用来执行 CREATE TABLE、INSERT、DROP TABLE、ALTER TABLE等会改变资料库内容的SQL,例如: stmt.executeUpdate("INSERT INTO message VALUES('良葛格', " + "'caterpillar@mail.com', '留言吧', '2004-5-26'," + "'到此一游')"); executeQuery()方法则是用于SELECT等查询资料库的SQL,executeUpdate()与 executeQuery()都会传回ResultSet物件,代表变更或查 询的结果,查询的结果会是一笔一笔的资料,您使用next()来移动至下一笔资料,它会传回 true 或 false表示是否有下一笔资料,接 着可以使用getXXX()来取得资料,例如getString()、getFloat()、getDouble()等方法,分别取得相对应的栏位型态资料,getXXX()方 法都提供有依栏位名称取得资料,或是依栏位顺序取得资料的方法,一个例子如下,您指定栏位名称来取得资料: ResultSet result = stmt.executeQuery("SELECT * FROM message"); while(result.next()) { System.out.print(result.getString("name") + "\t"); System.out.print(result.getString("email") + "\t"); System.out.print(result.getString("subject") + "\t"); System.out.print(result.getString("time") + "\t"); System.out.println(result.getString("memo") + "\t"); } 使用查询到的结果之栏位顺序来显示结果的方式如下: ResultSet result = stmt.executeQuery("SELECT * FROM message"); while(result.next()) { System.out.print(result.getString(1) + "\t"); System.out.print(result.getString(2) + "\t"); System.out.print(result.getString(3) + "\t"); System.out.print(result.getString(4) + "\t"); System.out.println(result.getString(5) + "\t"); } Statement的execute()可以用来执行SQL,并可以测试所执行的SQL是执行查询或是更新,传回 true的话表示SQL执行将传回ResultSet 表示查询结果,此时可以使用getResultSet()取得ResultSet物件,如果 execute()传回false,表示SQL执行会传回更新笔数或没有结果, 此时可以使用getUpdateCount()取得更新笔数。如果事先无法 得知是进行查询或是更新,就可以使用execute()。 下面的程式是个完整的范例,注意我们在查询结束后,可以使用Statement的close()方法来释放Statement的资料库资源与JDBC资源, http://caterpillar.onlyfun.net/GossipCN/JavaGossip-V2/StatementResultSet.htm(第 1/3 页)2008-7-30 20:09:08 Statement、 ResultSet 而最后不使用连线时也使用Connection的close()来关闭连线: ● DBTest.java package onlyfun.caterpillar; import java.sql.*; public class DBTest { public static void main(String[] args) { String driver = "com.mysql.jdbc.Driver"; String url = "jdbc:mysql://localhost:3306/GUESTBOOK?" + "useUnicode=true&characterEncoding=Big5"; String user = "caterpillar"; String password = "123456"; Connection conn = null; Statement stmt = null; try { Class.forName(driver); conn = DriverManager.getConnection( url, user, password); stmt = conn.createStatement(); stmt.execute("INSERT INTO message VALUES('良葛格" + "', 'caterpillar@mail.com', '留言吧', "+ "'2004-5-26', '到此一游')"); ResultSet result = stmt.executeQuery( "SELECT * FROM message"); while(result.next()) { System.out.print(result.getString(1) + "\t"); System.out.print(result.getString(2) + "\t"); System.out.print(result.getString(3) + "\t"); System.out.print(result.getString(4) + "\t"); System.out.println(result.getString(5) + "\t"); } } catch(ClassNotFoundException e) { System.out.println("找不到驱动程式"); e.printStackTrace(); } catch(SQLException e) { e.printStackTrace(); } finally { http://caterpillar.onlyfun.net/GossipCN/JavaGossip-V2/StatementResultSet.htm(第 2/3 页)2008-7-30 20:09:09 Statement、 ResultSet if(stmt != null) { try { stmt.close(); } catch(SQLException e) { e.printStackTrace(); } } if(conn != null) { try { conn.close(); } catch(SQLException e) { e.printStackTrace(); } } } } } 最后注意到的是,Connection物件预设为自动“认可”(Commit),也就是Statement执行SQL叙述完 后,马上对资料库进行操作变 更,如果想要对Statement要执行的SQL进行除错,可以使用setAutoCommit(false)来将自动认可取 消,在执行完SQL之后,再呼叫 Connection的commit()方法认可变更,使用Connection的getAutoCommit()可以测 试是否设定为自动认可。 不过无论是否有无执行commit()方法,只要SQL没有错,在关闭Statement或Connection前,都会执行认可动作,对资料库进行变 更。 http://caterpillar.onlyfun.net/GossipCN/JavaGossip-V2/StatementResultSet.htm(第 3/3 页)2008-7-30 20:09:09 Statement 批次处理 From Gossip@caterpillar Java Gossip: Statement 批次处理 Statement的execute等方法一次只能执行一个SQL叙述,如果有多个SQL叙述要执行的话,可以使用executeBatch()方法,在一次方 法 呼叫中执行多个SQL叙述,以增加执行的效能,我们先使用addBatch()方 法将要执行的SQL叙述加入,然后执行 executeBatch()即 可: conn.setAutoCommit(false); Statement stmt = conn.createStatement(); stmt.addBatch("INSERT INTO message VALUES('良葛格'," + " 'caterpillar@mail.com', '留言吧', " + "'2004-5-26', '到此一游')"); stmt.addBatch("INSERT INTO message VALUES('米小狗'," + " 'dog@mail.com', '留言耶', " + "'2004-5-26', '啦啦啦')"); stmt.addBatch("INSERT INTO message VALUES('毛妹妹'," + " 'momor@mail.com', '留言吧', " + "'2004-5-26', '到此一游')"); stmt.executeBatch(); conn.commit(); 在执行executeBatch()时而SQL有错误的情况下,会丢出BatchUpdateException例 外,您可以由这个例外物件的 getUpdateCounts() 方 法取得正确的SQL句数。 http://caterpillar.onlyfun.net/GossipCN/JavaGossip-V2/StatementBatch.htm2008-7-30 20:09:12 PreparedStatement From Gossip@caterpillar Java Gossip: PreparedStatement Statement主要用于执行静态的SQL陈述,也就是在执行 executeQuery()、executeUpdate()等方法时,指定内容固定不变的SQL语句字 串,每一句SQL只适用于当时的执行,如果您有 些操作只是SQL语句当中某些参数会有所不同,其余的SQL子句皆相同,则您可以 使用java.sql.PreparedStatement。 您可以使用Connection的preparedStatement()方法建立好一个预先编译(precompile)的SQL语句,当中参数会变动的部份,先指 定"?"这个占位字元,例如: PreparedStatement stmt = conn.prepareStatement( "INSERT INTO message VALUES(?, ?, ?, ?, ?)"); 要将参数指定给每一个栏位,我们可以使用setInt()、setString()等等方法,例如: stmt.setString(1, "米小狗"); stmt.setString(2, "dog@mail.com"); stmt.setString(3, "留言吧"); stmt.setString(4, "2004-5-26"); stmt.setString(5, "到此一游"); stmt.executeUpdate(); stmt.clearParameters(); setXXX()方法的第一个参数指定 ? 的位置,而第二个参数为要加入至资料表栏位的值,要让SQL执行生效,要执行executeQuery()或 executeUpdate()方法,使用 setXXX()来设定的参数会一直有效,可以于下一次使用,如果想要清除设定好的参数,可以执行 clearParameters()方法。 使用PreparedStatement也可以进行批次处理,直接来看个例子就知道如何使用: PreparedStatement stmt = conn.prepareStatement( "INSERT INTO Users VALUES(?,?, ?)"); User[] users = ...; for(int i=0; i 0) { os.write(buf); } fin.close(); os.close(); // 用更新的方式新增Blob/Clob资料 pstmt = conn.prepareStatement("UPDATE files set des=?, image=? where id = ?"); pstmt.setClob(1, clob); pstmt.setBlob(2, blob); pstmt.setInt(3, 1); pstmt.executeUpdate(); pstmt.close(); conn.commit(); } catch(SQLException e) { e.printStackTrace(); } http://caterpillar.onlyfun.net/GossipCN/JavaGossip-V2/BLOBAndCLOB_Oracle.htm(第 2/3 页)2008-7-30 20:09:24 将档案存入资料库 - Oracle catch(IOException e) { e.printStackTrace(); } finally { if(pstmt != null) { try { pstmt.close(); } catch(SQLException e) { e.printStackTrace(); } } } } catch(ClassNotFoundException e) { System.out.println("找不到驱动程式"); e.printStackTrace(); } } } http://caterpillar.onlyfun.net/GossipCN/JavaGossip-V2/BLOBAndCLOB_Oracle.htm(第 3/3 页)2008-7-30 20:09:24 ResultSet 游标控制 From Gossip@caterpillar Java Gossip: ResultSet 游标控制 之前在建立Statement或 PreparedStatement,您所使用的是Connection无参数的createStatement()与 preparedStatement(),这样取得的 Statement其执行SQL后得到的ResultSet,将只能使用next()方法逐笔取得查 询结果。 您可以在建立Statement物件时指定resultSetType,可指定的参数有 ResultSet.TYPE_FORWARD_ONLY、ResultSet. TYPE_SCROLL_INSENSITIVE与 ResultSet.TYPE_SCROLL_SENSITIVE,在不指定的情况下,预设是第一个,也就是只能使用 next()来逐笔取得资料,指定第二个或第三个,则可以使用ResultSet的afterLast()、previous()、absolute()、relative()等方法。 ResultSet.TYPE_SCROLL_INSENSITIVE与ResultSet.TYPE_SCROLL_SENSITIVE 的差别在于能否取得ResultSet改变值后的资料,另 外您还必须指定resultSetConcurrency,有 ResultSet.CONCUR_READ_ONLY与ResultSet.CONCUR_UPDATABLE两个参数可以设 定,前者表示只能读取 ResultSet的资料,后者表示可以直接使用ResultSet来操作资料库,这会在下一个主题后说明。 createStatement()不给定参数时,预设是ResultSet.TYPE_FORWARD_ONLY、 ResultSet.CONCUR_READ_ONLY。 这边先示范如何控制ResultSet的读取游标,在建立Statement时,您使用 ResultSet.TYPE_SCROLL_INSENSITIVE及ResultSet. CONCUR_READ_ONLY即可,下面这个例子示范从 查询到的资料最后一笔开始往前读取: Statement stmt = conn.createStatement( ResultSet.TYPE_SCROLL_INSENSITIVE, ResultSet.CONCUR_READ_ONLY); ResultSet result = stmt.executeQuery( "SELECT * FROM message"); result.afterLast(); while(result.previous()) { System.out.print(result.getString("name") + "\t"); System.out.print(result.getString("email") + "\t"); System.out.print(result.getString("subject") + "\t"); System.out.print(result.getString("time") + "\t"); System.out.println(result.getString("memo") + "\t"); } stmt.close(); conn.close(); afterLast()会将ResultSet的读取游标移至最后一笔资料之后,您使用previous()方法往前移动读取游标。 您也可以使用absolute()方法指定查询到的资料之位置,例如absolute(4)表示第四笔资料,absoulte(10)则是第十笔资料,如果指定负 数,则从最后往前数,例如absolute(-1)则是最后一笔资料,若有100笔资料,absoulte(-4)则是第97笔资料。 relative()方法则从目前游标处指定相对位置,例如若目前在第25笔资料,则relative(-2)则表示第23笔资料,而relative (4)则表示第29 笔资料。 另外还有beforeFirst(),可以将游标移至资料的第一笔之前,first()可以将游标移至第一笔资料,而last()可以将游标移至最后一笔资 料。 http://caterpillar.onlyfun.net/GossipCN/JavaGossip-V2/ResultSetCursor.htm(第 1/2 页)2008-7-30 20:09:29 ResultSet 游标控制 http://caterpillar.onlyfun.net/GossipCN/JavaGossip-V2/ResultSetCursor.htm(第 2/2 页)2008-7-30 20:09:29 ResultSet 新增、更新、删除资料 From Gossip@caterpillar Java Gossip: ResultSet 新增、更新、删除资料 之前要进行新增、更新或删除资料,都必须要撰写SQL,然后使用 executeUpdate()来执SQL,将SQL写在executeUpdate ()之中,其实 是一件麻烦又容易出错的动作,如果您只是想要针对查询到的资料进行一些简单的新增、更新或删除资料,您可以藉由ResultSet的 一些方法 来执行,而不一定要撰写SQL并执行。 想要使用ResultSet直接进行新增、更新或删除资料,在建立Statement时必须在createStatement()上指定 ResultSet. TYPE_SCROLL_SENSITIVE(或ResultSet.TYPE_SCROLL_INSENSITIVE,如果不想取 得更新后的资料的话)与ResultSet. CONCUR_UPDATABLE,例如: Statement stmt = conn.createStatement(ResultSet.TYPE_SCROLL_SENSITIVE, ResultSet.CONCUR_UPDATABLE); 假如我们想要针对查询到的资料进行更新的动作,我们先移动游标至想要更新的资料位置,然后使用updateXXX()等对应的方法即 可,最后记得使用 updateRow()让更新生效,例如: ResultSet result = stmt.executeQuery( "SELECT * FROM message WHERE name='caterpillar'"); result.last(); result.updateString("name", "justin"); result.updateString("email", "justin@mail.com"); result.updateRow(); 使用updateXXX()等方法之后,并不会马上对资料库生效,而必须执行完updateRow()方法才会对资料库进行操作,如果在 updateRow ()前想要取消之前的updateXXX()方法,则可以使用cancelRowUpdates()方法取消。 如果想要新增资料,则先使用moveToInsertRow()移至新增资料处,执行相对的updateXXX()方法,然后再执行insertRow ()即可新增 资料,例如: ResultSet result = stmt.executeQuery( "SELECT * FROM message WHERE name='caterpillar'"); result.moveToInsertRow(); result.updateString("name", "caterpillar"); result.updateString("email", "caterpillar@mail.com"); result.updateString("subject", "test"); result.updateString("memo", "This is a test!"); result.insertRow(); 如果想要删除查询到的某笔资料,则可以将游标移至该笔资料,然后执行deleteRow()方法即可: ResultSet result = stmt.executeQuery( "SELECT * FROM message WHERE name='caterpillar'"); result.last(); http://caterpillar.onlyfun.net/GossipCN/JavaGossip-V2/ResultMore.htm(第 1/2 页)2008-7-30 20:09:35 ResultSet 新增、更新、删除资料 result.deleteRow(); http://caterpillar.onlyfun.net/GossipCN/JavaGossip-V2/ResultMore.htm(第 2/2 页)2008-7-30 20:09:35 ResultSetMetaData 类别 From Gossip@caterpillar Java Gossip: ResultSetMetaData 类别 Meta Data即“资料的资料”(Data about data),ResultSet用来表示查询到的资料,而ResultSet资料的资料,即描述查询到的资料背 后的资料描述,即用来表示表格名称、栏位名称、栏位型态等等,这些讯息您可以透过ResultSetMetaData来取得。 下面这个范例直接示范如何取得查询到的资料栏位数、表格名称、栏位名称与栏位资料型态: ● ResultSetMetaDataDemo.java package onlyfun.caterpillar; import java.sql.*; public class ResultSetMetaDataDemo { public static void main(String[] args) { String driver = "com.mysql.jdbc.Driver"; String url = "jdbc:mysql://localhost:3306/GUESTBOOK?" + "useUnicode=true&characterEncoding=Big5"; String user = "caterpillar"; String password = "123456"; Connection conn = null; Statement stmt = null; try { Class.forName(driver); conn = DriverManager.getConnection( url, user, password); stmt = conn.createStatement(); ResultSet result = stmt.executeQuery( "SELECT * FROM message"); ResultSetMetaData metadata = result.getMetaData(); for(int i = 1; i <= metadata.getColumnCount(); i++) { System.out.print( metadata.getTableName(i) + "."); System.out.print( metadata.getColumnName(i) + "\t|\t"); System.out.println( http://caterpillar.onlyfun.net/GossipCN/JavaGossip-V2/ResultSetMetaData.htm(第 1/2 页)2008-7-30 20:09:39 ResultSetMetaData 类别 metadata.getColumnTypeName(i)); } } catch(ClassNotFoundException e) { System.out.println("找不到驱动程式"); e.printStackTrace(); } catch(SQLException e) { e.printStackTrace(); } finally { if(stmt != null) { try { stmt.close(); } catch(SQLException e) { e.printStackTrace(); } } if(conn != null) { try { conn.close(); } catch(SQLException e) { e.printStackTrace(); } } } } } http://caterpillar.onlyfun.net/GossipCN/JavaGossip-V2/ResultSetMetaData.htm(第 2/2 页)2008-7-30 20:09:39 交易(Transaction) From Gossip@caterpillar Java Gossip: 交易(Transaction) 交易是一组原子(Atomic)操作(一组SQL执行)的工作单元,这个工作单元中的所有原子操作在进行期间,与其它交易隔离,免 于数据来源的交相更新而 发生混乱,交易中的所有原子操作,要嘛全部执行成功,要嘛全部失败(即使只有一个失败,所有的原子 操作也要全部撤消)。 举个简单的例子,一个客户从A银行转帐至B银行,要作的动作为从A银行的帐户扣款、在B银行的帐户加上转帐的金额,两个动作 必须成功,如果有一个动作失败,则此次转帐失败。 在JDBC中,可以操作Connection的setAutoCommit()方法,给定它false引数,在下达一连串的SQL语句后,自行呼叫Connection的 commit()来送出变更,如果中间发生错误,则呼叫rollback()来撤消所有的执行,例如: ● TransactionDemo.java package onlyfun.caterpillar; import java.sql.*; public class TransactionDemo { private static String driver = "com.mysql.jdbc.Driver"; private static String url = "jdbc:mysql://localhost:3306/demo"; private static String user = "root"; private static String password = "123456"; private static void loadDriver() { try { Class.forName(driver); } catch (ClassNotFoundException e) { e.printStackTrace(); } } public static void main(String[] args) { loadDriver(); Connection conn = null; Statement stmt = null; try { conn = DriverManager.getConnection( url, user, password); conn.setAutoCommit(false); stmt = conn.createStatement(); http://caterpillar.onlyfun.net/GossipCN/JavaGossip-V2/Transaction.htm(第 1/3 页)2008-7-30 20:09:44 交易(Transaction) stmt.execute("...."); // SQL stmt.execute("...."); stmt.execute("...."); conn.commit(); } catch(SQLException e) { try { conn.rollback(); } catch (SQLException e1) { e1.printStackTrace(); } e.printStackTrace(); } finally { if(stmt != null) { try { stmt.close(); } catch(SQLException e) { e.printStackTrace(); } } if(conn != null) { try { conn.close(); } catch(SQLException e) { e.printStackTrace(); } } } } } 如果您在交易管理时,仅想要rollback回某个SQL执行点,则您可以设定save point,例如: conn.setAutoCommit(false); Statement stmt = conn.createStatement(); stmt.executeUpdate("...."); stmt.executeUpdate("...."); Savepoint savepoint = conn.setSavepoint(); // 设定save point http://caterpillar.onlyfun.net/GossipCN/JavaGossip-V2/Transaction.htm(第 2/3 页)2008-7-30 20:09:44 交易(Transaction) stmt.executeUpdate("...."); // 如果因故rollback conn.rollback(savepoint); . . . conn.commit(); // 记得释放save point stmt.releaseSavepoint(savepoint); 在 Statement 批次处理 中介绍过批次执行SQL,批次处理前设定auto commit为 false,如果中间有个SQL执行错误,则应该rollback整 个批次处理。 http://caterpillar.onlyfun.net/GossipCN/JavaGossip-V2/Transaction.htm(第 3/3 页)2008-7-30 20:09:44 InetAddress 类别 From Gossip@caterpillar Java Gossip: InetAddress 类别 网路程式的第一步通常是从网址资讯的处理开始,这很容易理解,如果连网址都无法取得,更别谈网路连线了。 java.net.InetAddress类别可用来包装与进行网址处理的相关操作,它要有几个静态方法传回InetAddress物件: public static InetAddress InetAddress.getLocalHost() public static InetAddress InetAddress.getByName(String hostname) public static InetAddress[] InetAddress.getAllByName(String hostname) InetAddress主要包括两个栏位(field),即名称与位址,名称即像是www.caterpillar.onlyfun.net这样的名称, 而位址则是IP位址,我 们可以使用getHostName()与getHostAddress()方法分别取得这两个资讯。 getLocalhost()可以取得本机网址资讯,下面这个简单的程式即可显示本机名称与位址: ● Host.java package onlyfun.caterpillar; import java.net.*; public class Host { public static void main(String[] args) { try { InetAddress address = InetAddress.getLocalHost(); System.out.println(address); System.out.printf("HostName: %s%n", address.getHostName()); System.out.printf("HostAddress: %s%n", address.getHostAddress()); } catch (UnknownHostException e) { e.printStackTrace(); } } } 执行结果: caterpillar-PC/192.168.1.23 HostName: caterpillar-PC HostAddress: 192.168.1.23 下面的程式也很简单,可以指定查询远端主机的名称与IP位址: ● Host.java package onlyfun.caterpillar; import java.net.*; public class Host { public static void main(String[] args) { http://caterpillar.onlyfun.net/GossipCN/JavaGossip-V2/InetAddress.htm(第 1/3 页)2008-7-30 20:09:48 InetAddress 类别 try { InetAddress address = InetAddress.getByName(args[0]); System.out.println(address); System.out.printf("HostName: %s%n", address.getHostName()); System.out.printf("HostAddress: %s%n", address.getHostAddress()); } catch (UnknownHostException e) { e.printStackTrace(); } } } 执行结果: java onlyfun.caterpillar.Host caterpillar.onlyfun.net caterpillar.onlyfun.net/64.22.69.61 HostName: caterpillar.onlyfun.net HostAddress: 64.22.69.61 有的网站上可能拥有不止一个的IP位址,可以使用getAllByName()方法取回所有的网址资讯,这会传回InetAddress物件阵列,可以使 用回圈将这些物件一一取出,下面的程式是一个简单的示范: ● Host.java package onlyfun.caterpillar; import java.net.*; public class Host { public static void main(String[] args) { try { InetAddress[] addresses = InetAddress.getAllByName(args[0]); for(int i = 0; i < addresses.length; i++) System.out.println(addresses[i]); } catch(UnknownHostException e) { e.printStackTrace(); } } } 执行结果: java onlyfun.caterpillar.Host cnn.com cnn.com/64.236.16.20 cnn.com/64.236.16.52 cnn.com/64.236.24.12 cnn.com/64.236.29.120 http://caterpillar.onlyfun.net/GossipCN/JavaGossip-V2/InetAddress.htm(第 2/3 页)2008-7-30 20:09:48 InetAddress 类别 http://caterpillar.onlyfun.net/GossipCN/JavaGossip-V2/InetAddress.htm(第 3/3 页)2008-7-30 20:09:48 URL 类别 From Gossip@caterpillar Java Gossip: URL 类别 URL 类别可以提供URL(Uniform Resource Locator)的Protocol、Host、Port、File、named anchor与URLStreamHandler等资讯,它拥 有几个建构函式,它们皆需处理MalformedURLException: public URL(String url) public URL(String protocol, String host, String file) public URL(String protocol, String host, int port, String file) public URL(URL u,String s) URL类别可以由以下几个方法取得资讯: public String getProtocol() public String getHost() public int getPort() public String getFile() public String getRef() 其中getFile()会包括从主机名称后至档案名称的字串,包括/,而getRef()则是取回参考点名称,中文俗称网页中的“书签”,下面这个 程式示范这几个方法的作用: ● UrlInfo.java package onlyfun.caterpillar; import java.net.*; public class UrlInfo { public static void main(String[] args) { try { URL url = new URL(args[0]); System.out.printf("URL: %s%n", url); System.out.printf("Protocal: %s%n", url.getProtocol()); System.out.printf("Host: %s%n", url.getHost()); System.out.printf("Port: %d%n", url.getPort()); System.out.printf("File: %s%n", url.getFile()); System.out.printf("REF: %s%n", url.getRef()); } catch(MalformedURLException e) { e.printStackTrace(); } } } 当执行程式时给定的引数为http://caterpillar.onlyfun.net:8080/admin/setup.html#justin,执行结果如下: http://caterpillar.onlyfun.net/GossipCN/JavaGossip-V2/URL.htm(第 1/3 页)2008-7-30 20:09:55 URL 类别 URL: http://caterpillar.onlyfun.net:8080/admin/setup.html#justin Protocal: http Host: caterpillar.onlyfun.net Port: 8080 File: /admin/setup.html REF: justin URL类别有三个方法可以取得指定的URL资料,这三个方法必须处理IOException: public final InputStream openStream() public URLConnection openConnection() public final Object getContent() 在这边先示范openStream(),它会自动处理连线之间的协定动作,并传回一个InputStream物件,所以可以将它塞入BufferedReader或 BufferedInputStream等I/O类别,再透过它来读取伺服器传来的资料。 下面这个程式即利用openStream()取得指定网址的资料,并自动将资料储存在对应档案名称之中,由于是使用BufferedInputStream, 您可以用它来储存HTML网页,也可以储存图片: ● UrlInfo.java package onlyfun.caterpillar; import java.io.*; import java.net.*; public class Download { public static void main(String[] args) { try { URL url = new URL(args[0]); String fileName = url.getFile().substring( url.getFile().lastIndexOf('/') + 1); BufferedInputStream inputStream = new BufferedInputStream(url.openStream()); BufferedOutputStream outputStream = new BufferedOutputStream( new FileOutputStream(fileName)); int read = 0; while((read = inputStream.read()) != -1) { outputStream.write(read); } inputStream.close(); outputStream.flush(); outputStream.close(); } catch(Exception e) { e.printStackTrace(); } http://caterpillar.onlyfun.net/GossipCN/JavaGossip-V2/URL.htm(第 2/3 页)2008-7-30 20:09:55 URL 类别 } } http://caterpillar.onlyfun.net/GossipCN/JavaGossip-V2/URL.htm(第 3/3 页)2008-7-30 20:09:55 Socket 类别 From Gossip@caterpillar Java Gossip: Socket 类别 在TCP/IP 底层的运作必须处理封包、标头、格式、交握等的细节,这实在不是什么好差事,为此Berkeley UNIX提出Socket的概念, 将网路连线简化为资料流(data stream)的概念,这个资料流在客户端与伺服端各有一个接口(port),而资料流就像是在一个连接 两接口的缆线中传递,程式设计人员使用 Socket的概念来撰写网路连线程式,只要处理主机资讯与连接埠,而不用关心底层的琐碎 运作。 简而言之,就如同档案输出入一样,Socket将网路连线也视作一种输出入的动作,资料的传递就像是将资料写入与读入。 在Java中提供Socket类别来支援Socket概念,这边介绍四个建构式: public Socket(String host, int port) public Socket(InetAddress host, int port) public Socket(String host, int port, InetAddress interface, int localPort) public Socket(InetAddress host, int port, InetAddress interface, int localPort) 除了第一个建构函式必须同时处理UnknownHostException(无法识别主机)与IOException(无法建立连线时)之外,其它的建构式 只需处理IOException。 第一与第二个建构式让您指定远端主机与连接时所使用的连接埠,而本机的部份则交由程式自行决定,第三与第四个建构函式可以 让您指定远端与本身的资讯。 您可以直接指定主机名称来建立Socket物件,然而使用InetAddress会比较有效率,在真正进行Socket连线之前,如果在建立 InetAddress物件时无法取得主机资讯,则可以提前进行相关的处理。 下面这个程式可以让您扫描指定主机上所开启的连接埠(0~1023),这边指定本机为对象建立Socket连线,如果某个连接埠有开 启,就会建立连线,此时显示该连接埠开启的讯息: ● ScanPort.java package onlyfun.caterpillar; import java.io.*; import java.net.*; public class ScanPort { public static void main(String[] args) { try { String hostname = "localhost"; InetAddress address = InetAddress.getByName(hostname); for(int i = 0; i < 1024; i++) { try { Socket skt = new Socket(address, i); // 连线表示有开启 Port System.out.printf("%nPort: %d Opened..", i); skt.close(); } http://caterpillar.onlyfun.net/GossipCN/JavaGossip-V2/Socket.htm(第 1/5 页)2008-7-30 20:10:00 Socket 类别 catch(IOException e) { System.out.print("."); // 无法建立连线,没有开启 Port } } } catch(UnknownHostException e) { e.printStackTrace(); } } } 在建立了Socket物件之后,可以取得Socket物件的相关资讯,例如: public InetAddress getInetAddress() public int getPort() public int getLocalPort() public inetAddress getLocalAddress() 以上的方法由上而下分别为取得Socket连接对象位址、连接对象连接埠、本机连接埠、本机位址。 如果要取过Socket物件接受或输出资讯,可以使用getInputStream()与getOutputStream()两个方法,就如同档案I/O 一样,您只要将它 当作串流资料来处理即可,至于网路上的资讯是如何交换的,您并不用得知,Java会自动帮您完成相关的协定确认。 下面这个程式模拟Telnet程式,您可以用它来与远端主机进行“以行为主”的文字或指令沟通,也就是每下一行文字或指令就按Enter 键,然后程式会将您的指令传送出去,并显示远端主机的回应讯息,为了同时处理远端主机的回应与本机使用者的输入,程式使用 多执行绪: ● SocketToStdout.java package onlyfun.caterpillar; import java.io.*; import java.net.*; public class SocketToStdout implements Runnable { private Socket skt; public SocketToStdout(Socket skt) { this.skt = skt; } public void run() { BufferedReader sktReader; try { sktReader = new BufferedReader( new InputStreamReader(skt.getInputStream())); String sktMessage = null; while((sktMessage = sktReader.readLine()) != null) { http://caterpillar.onlyfun.net/GossipCN/JavaGossip-V2/Socket.htm(第 2/5 页)2008-7-30 20:10:00 Socket 类别 System.out.println(sktMessage); } skt.close(); } catch(IOException e) { System.out.println(e.toString()); } } } ● StdInToSocket.java package onlyfun.caterpillar; import java.io.*; import java.net.Socket; public class StdInToSocket implements Runnable { private Socket skt; public StdInToSocket(Socket skt) { this.skt = skt; } public void run() { String userInput; BufferedReader stdInReader; PrintStream socketStream; try { stdInReader = new BufferedReader( new InputStreamReader(System.in)); socketStream = new PrintStream(skt.getOutputStream()); while(true) { if(skt.isClosed()) { break; } userInput = stdInReader.readLine(); socketStream.println(userInput); } } catch(IOException e) { e.printStackTrace(); } } } http://caterpillar.onlyfun.net/GossipCN/JavaGossip-V2/Socket.htm(第 3/5 页)2008-7-30 20:10:00 Socket 类别 ● ConnectDemo.java package onlyfun.caterpillar; import java.io.*; import java.net.*; public class ConnectDemo { public static void main(String[] args) { String hostname = "localhost"; int port = 23; InetAddress address; BufferedReader buf; String read; if(args.length > 1) { hostname = args[0]; port = Integer.parseInt(args[1]); } try { address = InetAddress.getByName(hostname); try { Socket skt = new Socket(address, port); Thread sktToStd = new Thread(new SocketToStdout(skt)); Thread stdToSkt = new Thread(new StdInToSocket(skt)); sktToStd.start(); stdToSkt.start(); } catch (IOException e) { e.printStackTrace(); } } catch(UnknownHostException e) { System.out.println(e.toString()); } } } 下面的执行结果是连接至FTP站台的测试,可以输入简单的ASCII指令跟FTP站台互动(使用FTP协定指令): http://caterpillar.onlyfun.net/GossipCN/JavaGossip-V2/Socket.htm(第 4/5 页)2008-7-30 20:10:00 Socket 类别 java onlyfun.caterpillar.ConnectDemo ftp.isu.edu.tw 21 220-欢迎光临义守大学档案伺服器 220- 220-本站提供以下软体可供下载: 220-******************************************************************************* 220-/pub/BeOS/ BeOS 作业系统 220-/pub/CPAN/ Perl 程式语言 (Comprehensive Perl Archive Network) 220-/pub/CPatch/ 中文化软体 (收集大量的 Windows 共享软体与中文化程式) 220-/pub/Documents/ 各类文件收集 220-/pub/FreeBSD/ FreeBSD 作业系统 220-/pub/Game/ 免费游戏软体 220-/pub/Hardware/ 硬体驱动程式 220-/pub/Linux/ Linux 作业系统 220-/pub/MsDownload/ 微软相关软体更新 (例如 Service Pack 等) 220-/pub/RFC/ Request for Comments (RFC 文件) 220-/pub/Solaris/ Solaris 作业系统 220-/pub/Yesterday/ 昨日小筑完整 mirror (收集大量 Windows 相关软体) 220-******************************************************************************* 220- 220-另外,欢迎使用者多多利用 HTTP 的方式登入,一来有较佳的 220-传输效能,介面功能也较为完善,您还可以利用档案搜寻引擎 220-快速找到您所需求的档案,网址如下: 220- 220-http://ftp.isu.edu.tw 220 QUIT 221 Goodbye. http://caterpillar.onlyfun.net/GossipCN/JavaGossip-V2/Socket.htm(第 5/5 页)2008-7-30 20:10:00 ServerSocket 类别 From Gossip@caterpillar Java Gossip: ServerSocket 类别 Socket类别主要在处理客户端的Socket连线,如果要实作一个伺服器,可以使用ServerSocket类别,它包括了伺服器倾听与客户端连 线的方法,您可以用数种方式来指定ServerSocket建构函式: public ServerSocket(int port) public ServerSocket(int port, int queuelength) public ServerSocket(int port, int queuelength, InetAddress bindAddress) 以上的建构式皆需处理IOException, SecurityException,port是所指定要系结(bind)的连接埠,而queuelength用来指定外来连线的伫 列长度,bindAddress指定要系结至哪一个网路介面。 ServerSocket拥有Socket类别取得相关资讯的能力,例如: public InetAddress getInetAddress() public int getLocalPort() 当要倾听连线或关闭连线时,可以使用accept()与close()方法: public Socket accept() public void close() 这两个方法需处理IOException,其中accept()传回的是有关连线客户端的Socket物件资讯,可以用它来取得客户端的连线资讯,或关 闭客户端的连线。 下面这个程式是个简单的Echo伺服器,您可以使用Telnet程式,或是 Socket 类别 所实作的程式来测试它,它会将客户端的文字指令 再传回客户端,客户端输入/bye可结束连线: ● EchoServer.java package onlyfun.caterpillar; import java.io.*; import java.net.*; public class EchoServer { public static void main(String[] args) { final int port = 7; ServerSocket serverSkt; Socket skt; BufferedReader sktReader; String message; PrintStream sktStream; try { serverSkt = new ServerSocket(port); try { while(true) { System.out.printf("连接埠 %d 接受连线中......%n", port); http://caterpillar.onlyfun.net/GossipCN/JavaGossip-V2/ServerSocket.htm(第 1/3 页)2008-7-30 20:10:04 ServerSocket 类别 skt = serverSkt.accept(); System.out.printf("与 %s 建立连线%n", skt.getInetAddress().toString()); sktReader = new BufferedReader(new InputStreamReader(skt.getInputStream())); while((message = sktReader.readLine()) != null) { if(message.equals("/bye")) { System.out.println("Bye!"); skt.close(); break; } System.out.printf("Client: %s%n", message); sktStream = new PrintStream(skt.getOutputStream()); sktStream.printf("echo: %s%n", message); } } } catch(IOException e) { System.out.println(e.toString()); } } catch(IOException e) { System.out.println(e.toString()); } } } 假设使用Telnet程式连线至Echo伺服器,并输入以下的内容: $ telnet localhost 7 Trying 127.0.0.1... Connected to localhost. Escape character is '^]'. Hello! Echo Test! echo: Hello! Echo Test! 哈啰!中文测试! echo: 哈啰!中文测试! /bye Connection closed by foreign host. 以下是Echo伺服器的回应: http://caterpillar.onlyfun.net/GossipCN/JavaGossip-V2/ServerSocket.htm(第 2/3 页)2008-7-30 20:10:04 ServerSocket 类别 $ java echoServer 连接埠 7 接受连线中...... 与/127.0.0.1建立连线 client say: Hello! Echo Test! client say: 哈啰!中文测试! Bye! 连接埠 7 接受连线中...... http://caterpillar.onlyfun.net/GossipCN/JavaGossip-V2/ServerSocket.htm(第 3/3 页)2008-7-30 20:10:04 档案传送 From Gossip@caterpillar Java Gossip: 档案传送 之前的几个范例都是传送文字指令或资讯给连线的另一端,适当的改写一下,就可以作档案传送,主要是使用PrintStream的write()方 法,它负责将位元或int传送给连线的另一端。 程式分作两个,一个伺服端与一个客户端,伺服端会倾听连线,而客户端在连线传送一个档案之后就会结束程式,这是档案传送的 一个简单例子,首先是伺服端程式: ● Server.java package onlyfun.caterpillar; import java.io.*; import java.net.*; public class Server { public static void main(String[] args) { try { int port = Integer.parseInt(args[0]); System.out.println("简易档案接收..."); System.out.printf("将接收档案于连接埠: %d%n", port); ServerSocket serverSkt = new ServerSocket(port); while(true) { System.out.println("倾听中...."); Socket clientSkt = serverSkt.accept(); System.out.printf("与 %s 建立连线%n", clientSkt.getInetAddress().toString()); // 取得档案名称 String fileName = new BufferedReader( new InputStreamReader( clientSkt.getInputStream())).readLine(); System.out.printf("接收档案 %s ...", fileName); BufferedInputStream inputStream = new BufferedInputStream(clientSkt.getInputStream()); BufferedOutputStream outputStream = new BufferedOutputStream(new FileOutputStream(fileName)); int readin; http://caterpillar.onlyfun.net/GossipCN/JavaGossip-V2/FileTransfer.htm(第 1/4 页)2008-7-30 20:10:08 档案传送 while((readin = inputStream.read()) != -1) { outputStream.write(readin); Thread.yield(); } outputStream.flush(); outputStream.close(); inputStream.close(); clientSkt.close(); System.out.println("\n档案接收完毕!"); } } catch(Exception e) { e.printStackTrace(); } } } 再来是客户端程式: ● Client.java package onlyfun.caterpillar; import java.io.*; import java.net.*; public class Client { public static void main(String[] args) { try { System.out.println("简易档案传送..."); String remoteHost = args[0]; int port = Integer.parseInt(args[1]); File file = new File(args[2]); System.out.printf("远端主机: %s%n", remoteHost); System.out.printf("远端主机连接埠: %d%n", port); System.out.printf("传送档案: %s%n", file.getName()); Socket skt = new Socket(remoteHost, port); System.out.println("连线成功!尝试传送档案...."); PrintStream printStream = new PrintStream(skt.getOutputStream()); printStream.println(file.getName()); System.out.print("OK! 传送档案...."); http://caterpillar.onlyfun.net/GossipCN/JavaGossip-V2/FileTransfer.htm(第 2/4 页)2008-7-30 20:10:08 档案传送 BufferedInputStream inputStream = new BufferedInputStream( new FileInputStream(file)); int readin; while((readin = inputStream.read()) != -1) { printStream.write(readin); Thread.yield(); } printStream.flush(); printStream.close(); inputStream.close(); skt.close(); System.out.println("\n档案传送完毕!"); } catch(Exception e) { e.printStackTrace(); } } } 为简化程式范例,程式是单一流程,不使用多执行绪,下面是伺服端的执行范例: $ java onlyfun.caterpillar.Server 9393 简易档案接收... 将接收档案于连接埠: 9393 倾听中.... 与 /127.0.0.1 建立连线 接收档案 caterpillar.jpg ... 档案接收完毕! 倾听中.... 下面是对应的客户端执行范例: $ java onlyfun.caterpillar.Client localhost 9393 e:\caterpillar.jpg 简易档案传送... 远端主机: localhost 远端主机连接埠: 9393 传送档案: caterpillar.jpg 连线成功!尝试传送档案.... OK! 传送档案.... 档案传送完毕! http://caterpillar.onlyfun.net/GossipCN/JavaGossip-V2/FileTransfer.htm(第 3/4 页)2008-7-30 20:10:08 档案传送 http://caterpillar.onlyfun.net/GossipCN/JavaGossip-V2/FileTransfer.htm(第 4/4 页)2008-7-30 20:10:08 HTTP 重新导向 From Gossip@caterpillar Java Gossip: HTTP 重新导向 如果您的网站移站了,而您只是要作一个重新导向的动作,则可以撰写一个可以丢出重新导向标头的简单伺服器,当浏览器接收到重 新导向标头时,会重新导向您指定的网站。 HTTP 协定中,重新导向标头是由Location: 控制,这个标头需浏览器支援HTTP/1.0以上才有作用,所以最好再指定一个备用的HTML网 页,如果使用者的浏览器不支援HTTP/1.0以上,可以直接显示HTML网页告知讯息,例如: ● moved.html This site has been moved to: http://caterpillar.onlyfun.net 下面这个程式可以让浏览器重新导向至指定的网址,您可以自行指定连接埠、重新导向网页与重新导向网址: ● HttpRedirectServer.java package onlyfun.caterpillar; import java.io.*; import java.net.*; public class HttpRedirectServer { public static void main(String[] args) { int port = Integer.parseInt(args[0]); String movedHtml = args[1]; String redirectUrl = args[2]; try { System.out.println("HTTP 重新导向..."); ServerSocket serverSkt = new ServerSocket(port); while(true) { System.out.printf("倾听连线于 %d ...%n", port); Socket clientSkt = serverSkt.accept(); System.out.printf("%s 连线....", clientSkt.getInetAddress().toString()); PrintStream printStream = new PrintStream(clientSkt.getOutputStream()); BufferedReader protocolReader = new BufferedReader( http://caterpillar.onlyfun.net/GossipCN/JavaGossip-V2/HTTPLocation.htm(第 1/3 页)2008-7-30 20:10:12 HTTP 重新导向 new InputStreamReader( clientSkt.getInputStream())); String readin = protocolReader.readLine(); String[] tokens = readin.split("[ \t]"); // 是否支援HTTP/1.0以上 if(tokens.length >= 3 && tokens[2].startsWith("HTTP/")) { while(true) { if(protocolReader.readLine().trim().equals("")) { break; // 空白行,接下来header部份,不处理 } } // 送出 HTTP header printStream.print("HTTP/1.0 302 FOUND\r\n"); printStream.print("Date: " + (new java.util.Date()) + "\r \n"); printStream.print("Server: httpRedirect v0.1\r\n"); printStream.print("Location: " + redirectUrl + "\r\n"); printStream.print("Content-type: text/html\r\n\r\n"); } else { BufferedReader reader = new BufferedReader( new FileReader(movedHtml)); String htmlContent = null; while((htmlContent = reader.readLine()) != null) { printStream.println(htmlContent); } reader.close(); } printStream.flush(); clientSkt.close(); } } catch(IOException e) { e.printStackTrace(); } } } 伺服器只会显示客户连线端的IP位址,可以在客户端使用Telnet来测试看看结果: http://caterpillar.onlyfun.net/GossipCN/JavaGossip-V2/HTTPLocation.htm(第 2/3 页)2008-7-30 20:10:12 HTTP 重新导向 $ telnet localhost 80 Trying 127.0.0.1... Connected to localhost. Escape character is '^]'. GET / HTTP/1.0 HTTP/1.0 302 FOUND Date: Wed Jul 16 01:06:55 CST 2003 Server: httpRedirect v0.1 Location: http://www.caterpillar.onlyfun.net/phpBB2/ Content-type: text/html Connection closed by foreign host. $ telnet localhost 80 Trying 127.0.0.1... Connected to localhost. Escape character is '^]'. GET / This site has been moved to: http://caterpillar.onlyfun.net Connection closed by foreign host. http://caterpillar.onlyfun.net/GossipCN/JavaGossip-V2/HTTPLocation.htm(第 3/3 页)2008-7-30 20:10:12 代理伺服器 From Gossip@caterpillar Java Gossip: 代理伺服器 代理伺服器的作用,就是作为连线来源端与连线目的端之间的桥梁,代理伺服器的功能有很多种,有作为网页快取的代理伺服器, 有作为防火墙功能的代理伺服器,有作为讯息过滤的代理伺服器等等。 客户端 <----> 代理伺服器 <----> 目的伺服器 其实将代理伺服器的功能简化至最基本时,其功能就是将连线来源端的讯息转接至连线目的端,而连线目的端的讯息转接至连线来 源端,对连线来源端而言,代理伺服器像是伺服端,对连线目的端而言,代理伺服器像是客户端。 讯息在代理伺服器时所作的处理,决定了代理伺服器的种类,如果它将网页暂存在伺服器本身的储存装置,并供客户端直接比对下 载,它的作用就是网页代理伺服 器,如果它的作用在过滤进出代理伺服器的讯息,它的作用就有些像是防火墙的功能(当然必须实 作低阶的封包过滤才算真正是),您也可以设计一个代理伺服器, 专门过滤掉网页上的广告部份。 下面这个程式即实作一个最简单的代理伺服器功能,它将连线来源端的讯息转接至连线目的端,而连线目的端的讯息转接至连线来 源端,为了简化程式逻辑,这个程式一次只能服务一个连线: ● SimpleProxyServer.java package onlyfun.caterpillar; import java.io.*; import java.net.*; public class SimpleProxyServer { public static void main(String[] args) { String host; // 代理的对象主机 int remotePort; // 代理对象连接埠 int localPort; // 本机连接埠 BufferedReader reader; try { reader = new BufferedReader( new InputStreamReader(System.in)); System.out.println("SimpleProxyServer v0.1"); System.out.print("代理的对象主机: "); host = reader.readLine(); System.out.print("代理对象连接埠: "); remotePort = Integer.parseInt(reader.readLine()); System.out.print("本机连接埠: "); localPort = Integer.parseInt(reader.readLine()); runServer(host, remotePort, localPort); } catch(IOException e) { http://caterpillar.onlyfun.net/GossipCN/JavaGossip-V2/ProxyServer.htm(第 1/4 页)2008-7-30 20:10:19 代理伺服器 System.err.println(e.toString()); } } public static void runServer(String host, int remotePort, int localPort) { try { System.out.printf("Proxy伺服器启动...Port %d%n", localPort); ServerSocket proxyServerSkt = new ServerSocket(localPort); System.out.println("OK!"); while(true) { System.out.println("倾听客户端....."); Socket clientSkt = proxyServerSkt.accept(); System.out.printf("%s 连线..%n", clientSkt.getInetAddress().toString()); // 客户端的来往讯息 final BufferedInputStream clientInputStream = new BufferedInputStream(clientSkt.getInputStream()); PrintStream clientPrintStream = new PrintStream(clientSkt.getOutputStream()); // 伺服端的来往讯息 final Socket serverSkt = new Socket(host, remotePort); BufferedInputStream fromServerMsg = new BufferedInputStream( serverSkt.getInputStream()); final PrintStream serverPrintStream = new PrintStream(serverSkt.getOutputStream()); // 由客户端至伺服器的讯息沟通执行绪 Thread client = new Thread() { public void run() { int read; try { while((read = clientInputStream.read()) != -1) { serverPrintStream.write(read); serverPrintStream.flush(); } } catch(IOException e) {} // 中断至伺服器的连线 try { serverSkt.close(); } catch(IOException e) { System.err.println(e.toString()); http://caterpillar.onlyfun.net/GossipCN/JavaGossip-V2/ProxyServer.htm(第 2/4 页)2008-7-30 20:10:19 代理伺服器 } } }; client.start(); // 主执行绪为由伺服器至客户端的讯息 int read; try { while((read = fromServerMsg.read()) != -1) { clientPrintStream.write(read); clientPrintStream.flush(); } } catch(IOException e) { e.printStackTrace(); }; // 中断与客户端的连线 try { clientSkt.close(); } catch(IOException e) { e.printStackTrace(); } } } catch(UnknownHostException e) { e.printStackTrace(); } catch(IOException e) { e.printStackTrace(); } } } 您有几个方法可以测试这个程式,首先启动程式: SimpleProxyServer v0.1 代理的对象主机: ptt.cc 代理对象连接埠: 23 本机连接埠: 23 Proxy伺服器启动...Port 23..OK! 倾听客户端..... 范例在本机8888连接埠启动了代理伺服器,代理的对象是ptt.cc,接下来您可以直接telnet localhost 23,本机所启动的代理伺服器会帮 您连线至ptt.cc,接下来的一切操作就与BBS操作相同了。 http://caterpillar.onlyfun.net/GossipCN/JavaGossip-V2/ProxyServer.htm(第 3/4 页)2008-7-30 20:10:19 代理伺服器 您也可以让它代理一个Web伺服器,假设本机代理伺服器连接埠也设定为8080,当使用浏览器键入http://localhost:8080/时,您会发现 浏览器自动连接至代理的网页伺服器。 http://caterpillar.onlyfun.net/GossipCN/JavaGossip-V2/ProxyServer.htm(第 4/4 页)2008-7-30 20:10:19 简单 HTTP 伺服器 From Gossip@caterpillar Java Gossip: 简单 HTTP 伺服器 这个程式改写自O'reilly的Java网路程式设计书中的jhttp.java程式,我将启动的介面作了一些改变,并服务客户端的执行绪部份独立出 来,使这个程式的服务类别更具一般性,不受限制于伺服器介面的管理方式。 这个程式可以判别客户端请求的资源型态,并传回适当的标头,因为这是一个最简单的HTTP伺服器,它不处理客户端的标头,并只 接受基本的GET方法。 基本上这个程式一点都不难,执行绪与Socket的使用都很基本,只要您稍微懂得HTTP协定,都可以看得懂这个程式,有兴趣的可以 参照HTTP协定继续完成功能更丰富的HTTP伺服器。 以下为程式码内容: ● HttpClient.java package onlyfun.caterpillar; import java.io.*; import java.net.*; import java.util.Date; class HttpClient implements Runnable { private Socket clientSkt; private File docRoot; private String defaultFile;; public HttpClient(Socket s, String root, String indexFile) { clientSkt = s; docRoot = new File(root); defaultFile = indexFile; } // 根据要求的档名判断传回型态 public String returnContentType(String fileName) { if(fileName.endsWith(".html") || fileName.endsWith(".htm")) return "text/html"; // HTML网页 else if(fileName.endsWith(".txt") || fileName.endsWith(".java")) return "text/plain"; // 文字型态 else if(fileName.endsWith(".gif")) return "image/gif"; // GIF图档 else if(fileName.endsWith(".jpg") || fileName.endsWith(".jpeg")) return "image/jpeg"; // JPEG图档 else if(fileName.endsWith(".class")) return "application/octec-stream"; // 二进位应用?{式档案 else // 其它不可判别的档案一律视为文字档型态 http://caterpillar.onlyfun.net/GossipCN/JavaGossip-V2/SimpleHTTPServer.htm(第 1/5 页)2008-7-30 20:10:24 简单 HTTP 伺服器 return "text/plain"; } public void run() { String request; // 要求的方法 String contentType; // 档案型态 String httpVersion = ""; // HTTP协定版本 File requestedFile; // 要求的档案 try { PrintStream printStream = new PrintStream( clientSkt.getOutputStream()); BufferedReader clientReader = new BufferedReader( new InputStreamReader(clientSkt.getInputStream())); String get = clientReader.readLine(); // 分离Request的内容 String[] tokens = get.split("[ \t]"); request = tokens[0]; if(request.equals("GET")) { String file = tokens[1]; if(file.endsWith("/")) // GET / file += defaultFile; contentType = returnContentType(file); if(tokens.length >= 3) { httpVersion = tokens[2]; } // 我们不处理客户端的标头 while((get = clientReader.readLine()) != null) { if(get.trim().equals("")) break; } try { requestedFile = new File( docRoot, file.substring(1, file.length())); FileInputStream fileInputStream = new FileInputStream(requestedFile); // 读入请求的档案 int fileLength = (int)requestedFile.length(); byte[] requestedData = new byte[fileLength]; fileInputStream.read(requestedData); fileInputStream.close(); // 写出标头至客户端 http://caterpillar.onlyfun.net/GossipCN/JavaGossip-V2/SimpleHTTPServer.htm(第 2/5 页)2008-7-30 20:10:24 简单 HTTP 伺服器 if(httpVersion.startsWith("HTTP/")) { printStream.print("HTTP/1.0 200 OK\r\n"); Date now = new Date(); printStream.print("Date: " + now + "\r\n"); printStream.print("Server: TinyHttpd v0.1\r\n"); printStream.print( "Content-length: " + fileLength + "\r\n"); printStream.print( "Content-type: " + contentType + "\r\n\r\n"); } // 将档案传给客户端 printStream.write(requestedData); printStream.close(); } catch(IOException e) { // 找不到档案 if(httpVersion.startsWith("HTTP/")) { printStream.print("HTTP/1.0 404 File Not Found\r\n"); Date now = new Date(); printStream.print("Date: " + now + "\r\n"); printStream.print("Server: TinyHttpd v0.1\r\n"); printStream.print("Content-type: text/html\r\n\r\n"); } } // 显示错误讯息网页 printStream.println( "File Not Found" + "

HTTP Error 404: File Not Found" + "

"); printStream.close(); } else { // 客户端请求了一个没有实作的方法,例如POST等 if(httpVersion.startsWith("HTTP/")) { printStream.print("HTTP/1.0 501 Not Implemented\r\n"); Date now = new Date(); printStream.print("Date: " + now + "\r\n"); printStream.print("Server: TinyHttpd v1.0\r\n"); printStream.print("Content-type: text/html\r\n\r\n"); } } printStream.println( "Not Implemented" http://caterpillar.onlyfun.net/GossipCN/JavaGossip-V2/SimpleHTTPServer.htm(第 3/5 页)2008-7-30 20:10:24 简单 HTTP 伺服器 + "

HTTP Error 501: Not Implemented" + "

"); printStream.close(); } catch(IOException e) { e.printStackTrace(); } try { clientSkt.close(); } catch(IOException e) { e.printStackTrace(); } } } ● SimpleHttpServer.java package onlyfun.caterpillar; import java.io.*; import java.net.*; public class SimpleHttpServer { public static void main(String args[]) { String docRoot; String indexFile; int port; ServerSocket serverSkt; Socket clientSkt; try { BufferedReader buf = new BufferedReader(new InputStreamReader(System.in)); System.out.println("TinyHttpd v0.1"); System.out.print("伺服器文件根目录: "); docRoot = buf.readLine(); System.out.print("索引文件: "); indexFile = buf.readLine(); System.out.print("伺服器连接埠: "); port = Integer.parseInt(buf.readLine()); if(port < 0 || port > 65535) { port = 80; } } http://caterpillar.onlyfun.net/GossipCN/JavaGossip-V2/SimpleHTTPServer.htm(第 4/5 页)2008-7-30 20:10:24 简单 HTTP 伺服器 catch(Exception e) { System.err.println("错误: " + e.toString()); System.out.print("采用内定选项"); docRoot = "/var/www/html/"; indexFile = "index.html"; port = 80; System.out.println("文件根目录: " + docRoot + "\n索引文件: " + indexFile + "\n连接埠: " + port); } try { serverSkt = new ServerSocket(port); System.out.println("倾听客户端于 " + serverSkt.getLocalPort() + " 连接埠...."); while(true) { clientSkt = serverSkt.accept(); System.out.println("客户端连线: " + clientSkt.getInetAddress()); // 启动一个客户端执行绪 Thread clientThread = new Thread( new HttpClient(clientSkt, docRoot, indexFile)); clientThread.start(); } } catch(IOException e) { e.printStackTrace(); } } } http://caterpillar.onlyfun.net/GossipCN/JavaGossip-V2/SimpleHTTPServer.htm(第 5/5 页)2008-7-30 20:10:24 简单 SMTP 送信(图形介面) From Gossip@caterpillar Java Gossip: 简单 SMTP 送信(图形介面) 这个程式是使用Socket来作简单的SMTP协定,用来送出一帮简单的纯文字信件。 下载程式范例 http://caterpillar.onlyfun.net/GossipCN/JavaGossip-V2/SimpleSMTP.htm2008-7-30 20:10:27 一对一网路聊天(图形介面) From Gossip@caterpillar Java Gossip: 一对一网路聊天(图形介面) 简单的一对一聊天程式,Lucky Number 就是 Port 号码啦!Orz.... 下载程式范例 http://caterpillar.onlyfun.net/GossipCN/JavaGossip-V2/OneToOneChat.htm2008-7-30 20:10:30 多人连线聊天(图形介面) From Gossip@caterpillar Java Gossip: 多人连线聊天(图形介面) 启动 Server 程式时,需要设定 Port 号码,例如: java -jar SimpleMultiChatServerDemo.jar 9393 Client 程式的 Lucky Number 就是 Port 号码 Orz... 下载程式范例 http://caterpillar.onlyfun.net/GossipCN/JavaGossip-V2/ChatRoom.htm(第 1/2 页)2008-7-30 20:10:34 多人连线聊天(图形介面) http://caterpillar.onlyfun.net/GossipCN/JavaGossip-V2/ChatRoom.htm(第 2/2 页)2008-7-30 20:10:34 远端监视程式(图形介面) From Gossip@caterpillar Java Gossip: 远端监视程式(图形介面) 这个程式可以监视远端电脑的画面,启动 Server 程式时,需要设定 Port 号码,例如: java -jar SimpleMultiChatServerDemo.jar 9393 执行客户端程式 RemoteScreenCaptureClientUI.jar ,按选单的“Run/Connect”输入主机位址与Port号码,就可以开始监看主机了。 下载程式范例 http://caterpillar.onlyfun.net/GossipCN/JavaGossip-V2/ScreenCaptureServerClient.htm2008-7-30 20:10:37 TicTacToe 井字游戏(Applet) From Gossip@caterpillar Java Gossip: TicTacToe 井字游戏(Applet) 这个程式不是原创,我只是修改与结合而已。。。。 Applet与连线部份的程式是从Deitel & Deitel的Java How to Program中取材,原程式仅限于一对游戏者连线,我将之改为可以多对连 线,每两个玩家连续的连线会被配成一对进行游戏。 除了可连线对战之外,还可以与电脑对战,与网路上找到的Java AI package结合,Java AI package的部份并作了一些修改。 与电脑对战部份,并没有进行网路连线,只是将applet抓下来,然后在本机上执行。 AI的部份可以使用MiniMax法或是AlphaBeta法,从程式码中直接改!可以设定搜寻深度,搜寻越深越难赢,最多平手吧! Java Applet介面的程式还可以与网友进行连线对战,您必须先启动一个伺服器程式,您可以NetworkTicTacToe的上一层目录使用以 下的指令进行启动: java NetworkTicTacToe.server.TicTacToeServer 下载程式范例 http://caterpillar.onlyfun.net/GossipCN/JavaGossip-V2/TicTacToe.htm2008-7-30 20:10:43 使用 Properties From Gossip@caterpillar Java Gossip: 使用 Properties 以 连接资料库 中的程式为例,如下: ● DBConnectionDemo.java package onlyfun.caterpillar; import java.sql.*; public class DBConnectionDemo { public static void main(String[] args) { String driver = "com.mysql.jdbc.Driver"; String url = "jdbc:mysql://localhost:3306/GUESTBOOK"; String user = "caterpillar"; String password = "123456"; try { Class.forName(driver); Connection conn = DriverManager.getConnection(url, user, password); if(conn != null && !conn.isClosed()) { System.out.println("资料库连线测试成功!"); conn.close(); } } catch(ClassNotFoundException e) { System.out.println("找不到驱动程式类别"); e.printStackTrace(); } catch(SQLException e) { e.printStackTrace(); } } } 其中的driver、url、user与password等设定,我们并不用撰写在程式之中,而可以将之撰写在一个.properties档案中,例如: ● config.properties http://caterpillar.onlyfun.net/GossipCN/JavaGossip-V2/Properties.htm(第 1/3 页)2008-7-30 20:10:47 使用 Properties driver=com.mysql.jdbc.Driver url=jdbc:mysql://localhost:3306/GUESTBOOK user=caterpillar password=123456 =左边设定的是key,右边是value,我们可以使用java.util.Properties来读取这个属性设定档,根据key来取得value,例如: ● DBConnectionDemo.java package onlyfun.caterpillar; import java.util.Properties; import java.sql.*; public class DBConnectionDemo { private static Properties props; private static void loadProperties() { props = new Properties(); try { props.load(new FileInputStream("config.properties")); } catch (FileNotFoundException e) { e.printStackTrace(); } catch (IOException e) { e.printStackTrace(); } } private static String getConfig(String key) { return props.getProperty(key); } public static void main(String[] args) { loadProperties(); String driver = getConfig("driver"); String url = getConfig("url"); String user = getConfig("user"); String password = getConfig("password"); try { Class.forName(driver); Connection conn = DriverManager.getConnection(url, user, password); if(conn != null && !conn.isClosed()) { System.out.println("资料库连线测试成功!"); conn.close(); http://caterpillar.onlyfun.net/GossipCN/JavaGossip-V2/Properties.htm(第 2/3 页)2008-7-30 20:10:48 使用 Properties } } catch(ClassNotFoundException e) { System.out.println("找不到驱动程式类别"); e.printStackTrace(); } catch(SQLException e) { e.printStackTrace(); } } } 如此一来,将来若想改变属性设定,则直接修改.properties档案的内容即可,而不用修改原始码再重新编译。 http://caterpillar.onlyfun.net/GossipCN/JavaGossip-V2/Properties.htm(第 3/3 页)2008-7-30 20:10:48 使用 ResourceBundle From Gossip@caterpillar Java Gossip: 使用 ResourceBundle 在程式中有很多字串讯息会被写死在程式中,如果您想要改变某个字串讯息,您必须修改程式码然后重新编译,例如简单 的"Hello! World!"程式就是如此: ● Hello.java package onlyfun.caterpillar; public class Hello { public static void main(String[] args) { System.out.println("Hello!World!"); } } 如果日后想要改变"Hello!World!"为"Hello!Java!",您就要修改程式并重新编译。 对于日后可能变动的文字讯息,您可以考虑将讯息移至程式之外,方法是使用Java的java.util.ResourceBundle来作讯息绑定,首先 您要先准备一个.properties,例如: ● messages.properties onlyfun.caterpillar.welcome=Hello onlyfun.caterpillar.name=World .properties中撰写的是key、value,之后在程式中您可以使用key来取得对应的value值,接着改写Hello类别: ● ResourceBundleDemo.java package onlyfun.caterpillar; import java.util.ResourceBundle; public class ResourceBundleDemo { public static void main(String[] args) { ResourceBundle resource = ResourceBundle.getBundle("messages"); System.out.print(resource.getString( "onlyfun.caterpillar.welcome") + "!"); System.out.println(resource.getString( "onlyfun.caterpillar.name") + "!"); } } ResourceBundle的getBundle()方法会取得一个ResourceBundle的实例,所给定的参数名称是讯息档案的主档名,取得 ResourceBundle http://caterpillar.onlyfun.net/GossipCN/JavaGossip-V2/ResourceBundle.htm(第 1/2 页)2008-7-30 20:10:53 使用 ResourceBundle 实例后,可以使用getString()指定key值来取得value值,执行结果如下: Hello!World! 如果您日后想要改变显示的讯息,只要改变.properties的内容就可以了,例如可以改为: ● messages.properties onlyfun.caterpillar.welcome=Oh onlyfun.caterpillar.name=Java 则直接执行程式就会显示新的讯息: Oh!Java! http://caterpillar.onlyfun.net/GossipCN/JavaGossip-V2/ResourceBundle.htm(第 2/2 页)2008-7-30 20:10:53 国际化讯息 From Gossip@caterpillar Java Gossip: 国际化讯息 国际化的英文是Internationalization,因为单字中总共有18个字母,简称I18N,目的是让应用程式可以应地区不同而显示不同的讯 息,最基本的就是让不同语系的使用者可以看到属于自己语系的讯息,像是英文语系的看到英文内容,而中文语系的可以看到中文 的内容。 为了在应用程式中表示一个区域,Java提供有java.util.Locale类,一个Locale实例包括了语系资讯与区域资讯,例如说"en"表示英文 语系的国家,这个字母组合是在 ISO 639 中定义的,而区域资讯则是像"US"表示美国,这个字母组合则是在 ISO 3166 中定义的。 您可以这么新增一个Locale的实例: Locale locale = new Locale("zh", "TW"); 如何将Locale用于讯息绑定呢?当您使用ResourceBundle.getBundle()方法时,预设就会自动取得电脑上的语系与区域讯息,而事实上 讯息档案的名称由basename加上语系与地区来组成,例如: ● basename.properties ● basename_en.properties ● basename_zh_TW.properties 没有指定语言与地区的basename是预设的资源档名称,当没有提供专用的语系、区域讯息档案时,就会找寻预设的资源档案。 如果您想要提供繁体中文的讯息,由于讯息资源档必须是ISO-8859-1编码,所以对于非西方语系的处理,必须先将之转换为Java Unicode Escape格式,例如您可以先在讯息资源档中写下以下的内容: ● messages_zh_TW.txt onlyfun.caterpillar.welcome=哈啰 onlyfun.caterpillar.name=世界 然后使用JDK的工具程式native2ascii来转换,例如: native2ascii -encoding Big5 messages_zh_TW.txt messages_zh_TW.properties 转换后的内容会如下: ● messages_zh_TW.properties onlyfun.caterpillar.welcome=\u54c8\u56c9 onlyfun.caterpillar.name=\u4e16\u754c 将这个档案放于classpath可以存取的到的位置,您也可以提供预设的讯息档案: ● messages.properties onlyfun.caterpillar.welcome=Hello onlyfun.caterpillar.name=World 来测试一下讯息档案,我所使用的作业系统是语系设定是中文,区域设定是台湾,当我使用下面的程式时: ● ResourceBundleDemo.java http://caterpillar.onlyfun.net/GossipCN/JavaGossip-V2/I18NMessage.htm(第 1/2 页)2008-7-30 20:11:02 国际化讯息 package onlyfun.caterpillar; import java.util.ResourceBundle; public class ResourceBundleDemo { public static void main(String[] args) { ResourceBundle resource = ResourceBundle.getBundle("messages"); System.out.print(resource.getString( "onlyfun.caterpillar.welcome") + "!"); System.out.println(resource.getString( "onlyfun.caterpillar.name") + "!"); } } 会使用预设的语系"zh"与区域设定"TW",所以就会找寻messages_zh_TW.properties的内容,所以会显示以下的讯息: 哈啰!世界! 在使用ResourceBundle.getBundle()时可以给定Locale实例作为参数,例如若您想提供 messages_en_US.properties,并想要 ResourceBundle.getBundle()取得这个档案的内容,则可以如下撰写: Locale locale = new Locale("en", "US"); ResourceBundle resource = ResourceBundle.getBundle("messages", locale); 则取得的讯息会是messages_en_US.properties的内容。 http://caterpillar.onlyfun.net/GossipCN/JavaGossip-V2/I18NMessage.htm(第 2/2 页)2008-7-30 20:11:02 使用 Date、DateFormat From Gossip@caterpillar Java Gossip: 使用 Date、DateFormat 如果想要取得系统的时间,可以使用System.currentTimeMillis()方法,例如: ● DateDemo.java package onlyfun.caterpillar; public class DateDemo { public static void main(String[] args) { System.out.println(System.currentTimeMillis()); } } 执行结果会显示从1970年1月1日开始到取得系统时间为止所经过的毫秒数,例如1115346430703这个数字,但这样的数字没有人确切 了解它的意 义是什么,您可以使用Date类别来让这个数字变的更有意义一些,例如: ● DateDemo.java package onlyfun.caterpillar; import java.util.Date; public class DateDemo { public static void main(String[] args) { Date date = new Date(); System.out.println(date.toString()); System.out.println(date.getTime()); } } 执行的结果如下: Fri May 06 10:31:13 GMT+08:00 2005 1115346673531 当您生成Date物件时,实际上它会使用System.currentTimeMillis()来取得系统时间,而您使用 toString()方法时,会将取得的1970年1 月1日至今的毫秒数转为dow mon dd hh:mm:ss zzz yyyy的格式,分别是:“星期 月 日 时:分:秒 西元”;使用Date的getTime()方法 则可以取得毫秒数。 如果您想要对日期时间作格式设定,则可以使用DateFormat来作格式化,先来看看它的子类SimpleDateFormat如何使用: ● DateDemo.java package onlyfun.caterpillar; import java.text.DateFormat; import java.text.SimpleDateFormat; http://caterpillar.onlyfun.net/GossipCN/JavaGossip-V2/UseDate.htm(第 1/4 页)2008-7-30 20:11:10 使用 Date、DateFormat import java.util.Date; public class DateDemo { public static void main(String[] args) { Date date = new Date(); DateFormat dateFormat = new SimpleDateFormat("EE-MM-dd-yyyy"); System.out.println(dateFormat.format(date)); } } 执行结果: 星期五-05-06-2005 DateFormat会依电脑上的区域设定显示时间格式,EE表示星期,MM表示月份、dd表示日期,而yyyy是西元,每个字元的设定都各 有其意义,您 可以参考 SimpleDateFormat 的API说明了解每个字元设定的意义。 您也可以直接从DateFormat指定格式生成DateFormat的实例,例如: ● DateDemo.java package onlyfun.caterpillar; import java.text.DateFormat; import java.util.Date; public class DateDemo { public static void main(String[] args) { Date date = new Date(); DateFormat shortFormat = DateFormat.getDateTimeInstance( DateFormat.SHORT, DateFormat.SHORT); DateFormat mediumFormat = DateFormat.getDateTimeInstance( DateFormat.MEDIUM, DateFormat.MEDIUM); DateFormat longFormat = DateFormat.getDateTimeInstance( DateFormat.LONG, DateFormat.LONG); DateFormat fullFormat = DateFormat.getDateTimeInstance( DateFormat.FULL, DateFormat.FULL); System.out.println(shortFormat.format(date)); System.out.println(mediumFormat.format(date)); System.out.println(longFormat.format(date)); System.out.println(fullFormat.format(date)); } http://caterpillar.onlyfun.net/GossipCN/JavaGossip-V2/UseDate.htm(第 2/4 页)2008-7-30 20:11:10 使用 Date、DateFormat } 在使用getDateTimeInstance()取得DateFormat实例时,可以指定的参数是日期格式与时间格式,以上所指定的格式依讯息详细度 区 分,执行结果如下: 2005/5/6 上午 10:45 2005/5/6 上午 10:45:25 2005年5月6日 上午10时45分25秒 2005年5月6日 星期五 上午10时45分25秒 GMT+08:00 您也可以使用getDateInstance()取得DateFormat实 例,并同时指定日期的区域显示方式,例如: ● DateDemo.java package onlyfun.caterpillar; import java.text.DateFormat; import java.util.Date; import java.util.Locale; public class DateDemo { public static void main(String[] args) { Date date = new Date(); Locale locale = new Locale("en", "US"); DateFormat shortFormat = DateFormat.getDateInstance( DateFormat.SHORT, locale); DateFormat mediumFormat = DateFormat.getDateInstance( DateFormat.MEDIUM, locale); DateFormat longFormat = DateFormat.getDateInstance( DateFormat.LONG, locale); DateFormat fullFormat = DateFormat.getDateInstance( DateFormat.FULL, locale); System.out.println(shortFormat.format(date)); System.out.println(mediumFormat.format(date)); System.out.println(longFormat.format(date)); System.out.println(fullFormat.format(date)); } } 这边指定了美国的时间显示方式,执行结果如下: http://caterpillar.onlyfun.net/GossipCN/JavaGossip-V2/UseDate.htm(第 3/4 页)2008-7-30 20:11:10 使用 Date、DateFormat 5/6/05 May 6, 2005 May 6, 2005 Friday, May 6, 2005 http://caterpillar.onlyfun.net/GossipCN/JavaGossip-V2/UseDate.htm(第 4/4 页)2008-7-30 20:11:10 使用 Calendar From Gossip@caterpillar Java Gossip: 使用 Calendar 您可以使用Date来取得完整的日期时间显示,但如果您想要单独取得某个时间或日期资讯的话该如何?例如您想知道现在是5月的第 几天? 您要使用Calendar类别,在这之前,您要先知道Calendar的一些方法取回的数字是对应于Calendar本身的常数,也就是说并不是您 取回1 这个数字,就表示今天是星期一。 想要取得现在的时间,首先使用Calendar的getInstance()取得一个Calendar的实例,例如: Calendar rightNow = Calendar.getInstance(); 如果现在您想知道现在是西元几年,则可以使用get()方法并指定常数,例如: System.out.println(rightNow.get(Calendar.YEAR)); 如果现在是2005年,则上例会显示2005的数字,依照这个例子,假设撰写本文的时间是5月份,而您现在想使用程式取得现在的月 份,则下例您可能会有些困惑: System.out.println(rightNow.get(Calendar.MONTH)); 程式会显示4这个数字,而不是您预期的5,因为传回的4并不是代表月份,而是对应于Calendar.MAY常数的值, Calendar在月份上 的常数值从Calendar.JANUARY开始是0,到Calendar.DECEMBER的11,所以您如果想要显示传回值 的真正意涵,可以如下撰写: String[] months = {"一月", "二月", "三月", "四月", "五月", "六月", "七月", "八月", "九月", "十月", "十一月", "十二月"}; Calendar rightNow = Calendar.getInstance(); System.out.println(months[rightNow.get(Calendar.MONTH)]); 同样的,如果您想要取得星期资讯,要记得常数从Calendar.SUNDAY是1,到Calendar.SATURDAY是7,由于对应的数并不是从0 开 始,所以如果要使用如上的阵列来对应的话,第一个阵列值就不包括资讯,例如: String[] dayOfWeek = {"", "日", "一", "二", "三", "四", "五", "六"}; Calendar rightNow = Calendar.getInstance(); System.out.println(dayOfWeek[ rightNow.get(Calendar.DAY_OF_WEEK)]); 总之您要记得传回的值是对应于Calendar的某个常数,会这样设计的原因在于实际上英文中对于星期或月份并不附以数字上的意 涵,如Sunday、 Monday等,幸而除了月份、星期之外,传回的值都对应于真正的时间意涵,毕竟像西元、小时、分等等本身就是 以数字表示。 如果怕搞混,使用switch来比对是个不错的作法,例如: http://caterpillar.onlyfun.net/GossipCN/JavaGossip-V2/UseCalendar.htm(第 1/4 页)2008-7-30 20:11:16 使用 Calendar ● CalendarDemo.java package onlyfun.caterpillar; import java.util.Calendar; public class CalendarDemo { public static void main(String[] args) { Calendar rightNow = Calendar.getInstance(); System.out.println("现在时间是:"); System.out.println("西元:" + rightNow.get(Calendar.YEAR)); System.out.println("月:" + getChineseMonth(rightNow)); System.out.println("日:" + rightNow.get(Calendar.DAY_OF_MONTH)); System.out.println("星期:" + getChineseDayOfWeek(rightNow)); } public static String getChineseMonth(Calendar rightNow) { String chineseMonth = null; switch(rightNow.get(Calendar.MONTH)) { case Calendar.JANUARY: chineseMonth = "一"; break; case Calendar.FEBRUARY: chineseMonth = "二"; break; case Calendar.MARCH: chineseMonth = "三"; break; case Calendar.APRIL: chineseMonth = "四"; break; case Calendar.MAY: chineseMonth = "五"; break; case Calendar.JUNE: chineseMonth = "六"; break; case Calendar.JULY: http://caterpillar.onlyfun.net/GossipCN/JavaGossip-V2/UseCalendar.htm(第 2/4 页)2008-7-30 20:11:16 使用 Calendar chineseMonth = "七"; break; case Calendar.AUGUST: chineseMonth = "八"; break; case Calendar.SEPTEMBER: chineseMonth = "九"; break; case Calendar.OCTOBER: chineseMonth = "十"; break; case Calendar.NOVEMBER: chineseMonth = "十一"; break; case Calendar.DECEMBER: chineseMonth = "十二"; break; } return chineseMonth; } public static String getChineseDayOfWeek( Calendar rightNow) { String chineseDayOfWeek = null; switch(rightNow.get(Calendar.DAY_OF_WEEK)) { case Calendar.SUNDAY: chineseDayOfWeek = "日"; break; case Calendar.MONDAY: chineseDayOfWeek = "一"; break; case Calendar.TUESDAY: chineseDayOfWeek = "二"; break; case Calendar.WEDNESDAY: chineseDayOfWeek = "三"; break; case Calendar.THURSDAY: chineseDayOfWeek = "四"; http://caterpillar.onlyfun.net/GossipCN/JavaGossip-V2/UseCalendar.htm(第 3/4 页)2008-7-30 20:11:16 使用 Calendar break; case Calendar.FRIDAY: chineseDayOfWeek = "五"; break; case Calendar.SATURDAY: chineseDayOfWeek = "六"; break; } return chineseDayOfWeek; } } 在取得一个Calendar的实例后,您可以使用setTime()方法给它一个Date物件,将之改变为Date目前的时间,例如: Calendar c = Calendar.getInstance(); Date date = new Date(10000000000L); c.setTime(date); 还可以使用add()方法,来改变Calendar的时间,例如: c.add(Calendar.MONTH, 1); // 目前时间加1个月 c.add(Calendar.HOUR, 3); // 目前时间加3小时 c.add(Calendar.YEAR, -2); // 目前时间减2年 c.add(Calendar.DAY_OF_WEEK, 3); // 目前的时间加3天 如果打算只针对日期中某个栏位加减,则可以使用roll()方法,例如: c.roll(Calendar.MONTH, 10); // 只增加月的栏位值为10个月 在上例中,假设是2006/10/1,则执行过后的Calendar实例,其中的时间将是2006/8/1,也就是只改变月份的栏位。 http://caterpillar.onlyfun.net/GossipCN/JavaGossip-V2/UseCalendar.htm(第 4/4 页)2008-7-30 20:11:16 使用 Calendar - JDK6 From Gossip@caterpillar Java Gossip: 使用 Calendar - JDK6 在 使用 Calendar 中的范例,使用switch进行判断以显示中文的日期格式,在Java SE 6中,您可以直接使用getDisplayNames()或 getDisplayName()方法取得区域化的日期格式显示,例如可以改写使用switch的那个范例为以下的程式: ● ClassLoaderDemo.java package onlyfun.caterpillar; import java.util.*; import static java.util.Calendar.*; public class CalendarDemo { public static void main(String[] args) { Calendar rightNow = Calendar.getInstance(); Locale locale = Locale.getDefault(); System.out.println("现在时间是:"); System.out.printf("%s:%d %n", rightNow.getDisplayName(ERA, LONG, locale), rightNow.get(YEAR)); System.out.println( rightNow.getDisplayName(MONTH, LONG, locale)); System.out.printf("%d 日%n", rightNow.get(DAY_OF_MONTH)); System.out.println( rightNow.getDisplayName(DAY_OF_WEEK, LONG, locale)); } } 只要指定Locale物件,就可以适当的显示区域化日期讯息,执行的结果如下所示: 现在时间是: 西元:2006 十一月 23 日 星期四 http://caterpillar.onlyfun.net/GossipCN/JavaGossip-V2/CalendarJDK6.htm2008-7-30 20:11:22 简介 Logging From Gossip@caterpillar Java Gossip: 简介 Logging 程式中不免会出现错误,当错误发生时,您可以使用System.err.println()或是 System.out.println()在主控台 (Console)显示讯息给使 用者,如果是在视窗程式中,可能是使用讯息方块,如果是在网页程式中,则显示一个错误讯息页面,除了提供错误讯息之外,您 还可能想将错误讯息以某种方式储存下来,以供使用者或是程式人员debug时使用。 在Java中的java.util.logging套件提供了一系列的logging工具,如果您只是要简单的记录一些讯息,就可以使用 它,这些logging工具 在J2SE 1.4之后加入了Java标准中,不必额外配备logging元件就可以运行于标准的Java平台上是它的好处。 像是简单的主控台讯息显示或是记录档案的输出,java.util.logging下都提供了一些预设的工具,您不必亲自撰写这些工具。 来看一个简单的例子,假设您的程式在启动时必须提供参数,如果使用者没有提供参数,则必须显示警示讯息: ● LoggingDemo.java package onlyfun.caterpillar; import java.util.logging.*; public class LoggingDemo { public static void main(String[] args) { Logger logger = Logger.getLogger("LoggingDemo"); try { System.out.println(args[0]); } catch(ArrayIndexOutOfBoundsException e) { logger.warning("没有提供执行时的引数!"); } } } 执行结果: 2005/2/3 上午 10:50:54 onlyfun.caterpillar.LoggingDemo main 警告: 没有提供执行时的引数! 如上所示的,您简单的透过Logger的静态方法getLogger()取得Logger物件,之后就可以运用它的方法进行讯 息输出,预设是从主控 台输出讯息,除了您提供的讯息之外,Logger还自动帮您收集了相关的讯息,像是类别与套件名称,执行该段程式的执行绪名称以 及讯 息产生的时间等,这比下面这个程式所提供的讯息丰富的多了。 ● LoggingDemo.java package onlyfun.caterpillar; public class LoggingDemo { http://caterpillar.onlyfun.net/GossipCN/JavaGossip-V2/LoggingAPI.htm(第 1/2 页)2008-7-30 20:11:27 简介 Logging public static void main(String[] args) { try { System.out.println(args[0]); } catch(ArrayIndexOutOfBoundsException e) { System.err.println("没有提供执行时的引数!"); } } } 您不用亲自实作,直接使用 java.util.logging 工具,就可以拥有一些预设的记录功能,作为中大型系统来说,java.util.logging 工具可 能不足,但作为一个简单的记录工具,java.util.logging 是个可以考虑的对象。 http://caterpillar.onlyfun.net/GossipCN/JavaGossip-V2/LoggingAPI.htm(第 2/2 页)2008-7-30 20:11:27 Logging 的层级 From Gossip@caterpillar Java Gossip: Logging 的层级 在进行讯息的记录时,依情节的不同,我们会设定不同等级的讯息输出,您可以透过操作 Logger 的几个方法来得到不同等级的讯息 输出,例如: ● LoggingDemo.java package onlyfun.caterpillar; import java.util.logging.*; public class LoggingDemo { public static void main(String[] args) { Logger logger = Logger.getLogger("loggingTest"); logger.severe("严重讯息"); logger.warning("警示讯息"); logger.info("一般讯息"); logger.config("设定方面的讯息"); logger.fine("细微的讯息"); logger.finer("更细微的讯息"); logger.finest("最细微的讯息"); } } 执行结果: 2005/2/3 下午 07:10:43 onlyfun.caterpillar.LoggingDemo main 严重的: 严重讯息 2005/2/3 下午 07:10:43 onlyfun.caterpillar.LoggingDemo main 警告: 警示讯息 2005/2/3 下午 07:10:43 onlyfun.caterpillar.LoggingDemo main 资讯: 一般讯息 依照不同的等级,您可以采用不同的讯息等级方法,程式中依程度高低撰写,注意到 config 以下的讯息并没有显示出来,这是因为 Logger 的预设等级是 INFO,比这个等级更低的讯息,Logger 并不会理会它。 Logger 的预设等级是定义在执行环境的属性档logging.properties中,这个档案位于执行环境安装目录的lib目录下,部份内容如下: handlers= java.util.logging.ConsoleHandler .level= INFO java.util.logging.ConsoleHandler.level = INFO Logger 预设的 Handler 是 ConsolerHandler, 也就是将讯息输出至主控台,一个 Logger 可以维护多个 Handler,每个 Handler 可以 有自己的讯息等级,在通过 Logger 的等级限制后,实际上还要再经过 Handler 的等级限制,所以上面的例子中您如果想要看到所有 http://caterpillar.onlyfun.net/GossipCN/JavaGossip-V2/LoggingLevel.htm(第 1/3 页)2008-7-30 20:11:31 Logging 的层级 的讯息,则必须同时设定 Logger 与 ConsolerHandler 的等级,例如: ● LoggingDemo.java package onlyfun.caterpillar; import java.util.logging.*; public class LoggingDemo { public static void main(String[] args) { Logger logger = Logger.getLogger("loggingTest"); logger.setLevel(Level.ALL); ConsoleHandler consoleHandler = new ConsoleHandler(); consoleHandler.setLevel(Level.ALL); logger.addHandler(consoleHandler); logger.severe("严重讯息"); logger.warning("警示讯息"); logger.info("一般讯息"); logger.config("设定方面的讯息"); logger.fine("细微的讯息"); logger.finer("更细微的讯息"); logger.finest("最细微的讯息"); } } 执行结果: 2005/2/3 下午 07:22:59 onlyfun.caterpillar.LoggingDemo main 严重的: 严重讯息 2005/2/3 下午 07:22:59 onlyfun.caterpillar.LoggingDemo main 严重的: 严重讯息 2005/2/3 下午 07:22:59 onlyfun.caterpillar.LoggingDemo main 警告: 警示讯息 2005/2/3 下午 07:22:59 onlyfun.caterpillar.LoggingDemo main 警告: 警示讯息 2005/2/3 下午 07:22:59 onlyfun.caterpillar.LoggingDemo main 资讯: 一般讯息 2005/2/3 下午 07:22:59 onlyfun.caterpillar.LoggingDemo main 资讯: 一般讯息 2005/2/3 下午 07:22:59 onlyfun.caterpillar.LoggingDemo main 配置: 设定方面的讯息 2005/2/3 下午 07:22:59 onlyfun.caterpillar.LoggingDemo main 细致: 细微的讯息 2005/2/3 下午 07:22:59 onlyfun.caterpillar.LoggingDemo main 更细致: 更细微的讯息 http://caterpillar.onlyfun.net/GossipCN/JavaGossip-V2/LoggingLevel.htm(第 2/3 页)2008-7-30 20:11:31 Logging 的层级 2005/2/3 下午 07:22:59 onlyfun.caterpillar.LoggingDemo main 最细致: 最细微的讯息 Level.ALL 表示显示所有的讯息,如果您想要关闭所有的讯息,可以设定为Level.OFF。 Logger 的server()、warning()、info()等等的方法,实际上是个便捷的方法,您也可以直接使用log()方法并指定等级来执行相同的作 用,例如下面这个程式的执行结果是一样的: ● LoggingDemo.java package onlyfun.caterpillar; import java.util.logging.*; public class LoggingDemo { public static void main(String[] args) { Logger logger = Logger.getLogger("loggingTest"); logger.setLevel(Level.ALL); ConsoleHandler consoleHandler = new ConsoleHandler(); consoleHandler.setLevel(Level.ALL); logger.addHandler(consoleHandler); logger.log(Level.SEVERE, "严重讯息"); logger.log(Level.WARNING, "警示讯息"); logger.log(Level.CONFIG, "一般讯息"); logger.log(Level.CONFIG, "设定方面的讯息"); logger.log(Level.FINE, "细微的讯息"); logger.log(Level.FINER, "更细微的讯息"); logger.log(Level.FINEST, "最细微的讯息"); } } http://caterpillar.onlyfun.net/GossipCN/JavaGossip-V2/LoggingLevel.htm(第 3/3 页)2008-7-30 20:11:31 Handler、Formatter From Gossip@caterpillar Java Gossip: Handler、Formatter Logger预设的Handler是ConsolerHandler,而ConsolerHandler的讯息等级是INFO,这可以在logging.properties下看到: handlers= java.util.logging.ConsoleHandler java.util.logging.ConsoleHandler.level = INFO J2SE 提供了五个预设的 Handler: ● ConsoleHandler 以 System.err 输出记录。 ● FileHandler 将讯息输出至档案 ● StreamHandler 以 OutputStream 输出记录。 ● SocketHandler 将讯息透过Socket传送至远端主机。 ● MemoryHandler 将讯息暂存在记忆体中。 下面这个例子示范如何使用FileHandler将讯息输出至档案中: ● HandlerDemo.java package onlyfun.caterpillar; import java.io.IOException; import java.util.logging.*; public class HandlerDemo { public static void main(String[] args) { Logger logger = Logger.getLogger("handlerDemo"); try { FileHandler fileHandler = new FileHandler("%h/myLogger.log"); logger.addHandler(fileHandler); logger.info("测试讯息"); } catch (SecurityException e) { e.printStackTrace(); } catch (IOException e) { e.printStackTrace(); } http://caterpillar.onlyfun.net/GossipCN/JavaGossip-V2/HandlerFormatter.htm(第 1/2 页)2008-7-30 20:11:35 Handler、Formatter } } 执行的结果会在主控台显示讯息,并将结果输出至档案中,在指定输出的档案名称时,我们可以使用%h来表示使用者的home目 录,您还可以使用%t取得系统的暂存目录,或者是加入%g,例如可以设定为"%h/myLogger%g.log"表示自动为档案名称编号,档 案输出的内 容如下: 2005-02-03T19:49:39 1107431379203 0 handlerDemo INFO onlyfun.caterpillar.HandlerDemo main 10 测试讯息 Handler负责输出讯息的目的地,而输出的格式则是由Formatter控制,预设上FileHandler的输出格式是XML格式,输出格式是由 Formatter来控制,例如FileHandler的预设格式是XMLFormatter,而ConsolerHandler的预设格式是SimpleFormatter,您可以使用 Handler的setFormatter()方法来设定讯息的输出格式,例如: fileHandler.setFormatter(new SimpleFormatter()); 如果FileHandler设定为SimpleFormatter,则输出的记录档内容与主控台看到的就是相同的内容。 http://caterpillar.onlyfun.net/GossipCN/JavaGossip-V2/HandlerFormatter.htm(第 2/2 页)2008-7-30 20:11:35 自订 Formatter From Gossip@caterpillar Java Gossip: 自订 Formatter 除了XMLFormatter与SimpleFormatter之外,您也可以自订自己的记录输出格式,只要继承抽象类别Formatter,并重新定义其format ()方法即可,format()方法会传入一个LogRecord物件作为参数,您可以使用它来取得一些与程式执行有关的资讯。 下面这个程式是个简单的示范,自订一个简单的TableFormatter: ● TableFormatter.java package onlyfun.caterpillar; import java.util.logging.*; public class TableFormatter extends Formatter { public String format(LogRecord logRecord) { return "LogRecord info: " + logRecord.getSourceClassName() + "\n" + "Level\t|\tLoggerName\t|\tMessage\t|\n" + logRecord.getLevel() + "\t|\t" + logRecord.getLoggerName() + "\t|\t" + logRecord.getMessage() + "\t|\n\n"; } } 再来就是使用Handler的setFormatter()方法设定Formatter物件,例如: ● FormatterDemo.java package onlyfun.caterpillar; import java.util.logging.*; public class FormatterDemo { public static void main(String[] args) { Logger logger = Logger.getLogger("formatterDemo"); try { for(Handler h : logger.getParent().getHandlers()) { if(h instanceof ConsoleHandler) { h.setFormatter(new TableFormatter()); } } logger.info("测试讯息1"); logger.warning("测试讯息2"); http://caterpillar.onlyfun.net/GossipCN/JavaGossip-V2/CustomFormatter.htm(第 1/2 页)2008-7-30 20:11:38 自订 Formatter } catch (SecurityException e) { e.printStackTrace(); } } } 您取得预设的root Logger,并取得其ConsoleHandler,之后设定它的Formatter为自订的TableFormatter,其执行结果如下: LogRecord info: onlyfun.caterpillar.FormatterDemo Level | LoggerName | Message | INFO | formatterDemo | 测试讯息1 | LogRecord info: onlyfun.caterpillar.FormatterDemo Level | LoggerName | Message | WARNING| formatterDemo | 测试讯息2 | http://caterpillar.onlyfun.net/GossipCN/JavaGossip-V2/CustomFormatter.htm(第 2/2 页)2008-7-30 20:11:38 自订 Handler From Gossip@caterpillar Java Gossip: 自订 Handler Handler负责日志讯息的输出目的地,在Java SE中预设有五个Handler: ● ConsoleHandler 以 System.err 输出记录。 ● FileHandler 将讯息输出至档案 ● StreamHandler 以 OutputStream 输出记录。 ● SocketHandler 将讯息透过Socket传送至远端主机。 ● MemoryHandler 将讯息暂存在记忆体中。 如果这五个Handler还不符合您的需求,则可以自订Handler,方法是继承java.util.logging.Handler类别,这个类别有三个抽象方法必须 重新定义: public void publish(LogRecord logRecord); public void flush(); public void close(); publish()方法可取得LogRecord实例,您可以使用它来取得一些与程式执行有关的日志资讯,flush()方法用来将缓冲区中的日志讯息 出 清(如果有的话),close()方法用来关闭输出讯息的物件(例如档案开启的话,可以在这边关闭档案),以释放所有的相关资 源。 Handler通常搭配Formatter,Handler负责输出,而Formatter负责格式,Formatter的自订可以参考自订 Formatter,java.util.logging. Handler 预设并没有设置Formatter,在实作Handler时,Handler的setFormatter()是用来设置Formatter的,您可以在 publish()中透过 getFormatter()来取得Formatter实例,进行格式化输出,例如: import java.util.logging.*; public class CustomLogHandler extends Handler { ... public void publish(LogRecord logRecord) { String logMsg = getFormatter().format(logRecord); out.print(logMsg); // out 是您自订的输出目的地物件 } public void flush() { .... } public void close() { ... } http://caterpillar.onlyfun.net/GossipCN/JavaGossip-V2/CustomHandler.htm(第 1/2 页)2008-7-30 20:11:42 自订 Handler } http://caterpillar.onlyfun.net/GossipCN/JavaGossip-V2/CustomHandler.htm(第 2/2 页)2008-7-30 20:11:42 自订 Filter From Gossip@caterpillar Java Gossip: 自订 Filter 预 设上,Handler的输出是根据log的层级来过滤、决定要不要输出讯息的,如果您想要自订输出讯息的层级或方法,则可以实作 java.util.logging.Filter介面: package java.util.logging; public interface Filter { public boolean isLoggable(LogRecord record); } 一个使用的例子如下: handler.setFilter(new Filter() { public boolean isLoggable(LogRecord logRecord) { .... return true; } }); Logger的log()方法会呼叫isLoggable()方法,传回true表示讯息必须输出,传回false表示讯息无需输出: if (filter != null && !filter.isLoggable(record)) { return; } http://caterpillar.onlyfun.net/GossipCN/JavaGossip-V2/CustomFilter.htm2008-7-30 20:11:46 Logger 阶层关系 From Gossip@caterpillar Java Gossip: Logger 阶层关系 在取得Logger的时候,给getLogger()方法的名称是有 意义的,如果您给定"onlyfun",实际上您将从root logger继承一些特性,像是记 录等级(Level)以及root logger的Handler,如果您再取得一个Logger,而给定名称"onlyfun.caterpillar",则这次取得的Logger将继承 命名为"onlyfun"这个Logger的特性。 下面这个程式可以看出,三个Logger(包括root logger)在名称上的继承关系: ● LoggerHierarchyDemo.java package onlyfun.caterpillar; import java.util.logging.*; public class LoggerHierarchyDemo { public static void main(String[] args) { Logger loggerOnlyfun = Logger.getLogger("onlyfun"); Logger loggerCaterpillar = Logger.getLogger("onlyfun.caterpillar"); System.out.println(loggerOnlyfun.getParent()); System.out.println( loggerCaterpillar.getParent().getName()); System.out.println(loggerCaterpillar.getName()); loggerOnlyfun.setLevel(Level.WARNING); loggerCaterpillar.info("caterpillar' info"); loggerCaterpillar.setLevel(Level.INFO); loggerCaterpillar.info("caterpillar' info"); } } getParent()方法可以取得Logger的上层父Logger,root logger并没有名称,所以直接呼叫它的toString()以取得字串,当Logger没有设 定等级时,则使用父Logger的等级设定,所以在 上例中,loggerOnlyfun设定等级为WARNING时,loggerCaterpillar呼叫info()时并不 会有讯息显示,只有在 loggerCaterpillar设定了自己的等级为INFO之后,才会显示讯息,执行结果如下: java.util.logging.LogManager$RootLogger@757aef onlyfun onlyfun.caterpillar 2005/2/4 上午 09:07:32 onlyfun.caterpillar.LoggerHierarchyDemo main 资讯: caterpillar' info 在每一个Handler方面,当每一个Logger处理完自己的记录动作之后,它会向父节点传播,让父节点的Handler也可以处理记录,例如 root logger的Handler是ConsoleHandler,所以您的子节点Logger加入了FileHandler来处理完记录之后,向上传播至父 节点时,会再由 http://caterpillar.onlyfun.net/GossipCN/JavaGossip-V2/LoggerHierarchy.htm(第 1/2 页)2008-7-30 20:11:50 Logger 阶层关系 ConsolerHandler来处理,所以一样会在主控台上显示讯息。 http://caterpillar.onlyfun.net/GossipCN/JavaGossip-V2/LoggerHierarchy.htm(第 2/2 页)2008-7-30 20:11:50 TimerTask 与 Timer From Gossip@caterpillar Java Gossip: TimerTask 与 Timer 如果您想要在某个时间点之后,执行某项排定的任务,您可以定义一个TimerTask,再将之排定给Timer,Timer会依指定的时间来执 行TimerTask。 举个实例来说,您想在程式启动后5秒钟执行报时,则您可以继承TimerTask,并重新定义其run()方法。 ● DateTask.java package onlyfun.caterpillar; import java.util.*; public class DateTask extends TimerTask { public void run() { System.out.println("任务时间:" + new Date()); } } 在run()方法中,只简单的显示run()方法被执行的时间,TimerTask的排程执行要交给Timer物件,例如: ● Main.java package onlyfun.caterpillar; import java.util.*; public class Main { public static void main(String[] args) { Timer timer = new Timer(); timer.schedule(new DateTask(), 5000); System.out.println("现在时间:" + new Date()); try { Thread.sleep(8000); } catch(InterruptedException e) { } timer.cancel(); } } Timer物件的schedule()方法接受TimerTask实例,并可以给它一个毫秒数,表示schedule()方法执行后,几毫秒之后执行 TimerTask中 的run()方法,Timer物件在启动排程后,如果要停止,则要执行cancel()方法,这个程式的执行结果如下: 现在时间:Fri Jan 19 10:47:10 CST 2007 任务时间:Fri Jan 19 10:47:15 CST 2007 http://caterpillar.onlyfun.net/GossipCN/JavaGossip-V2/TimerTaskAndTimer.htm(第 1/2 页)2008-7-30 20:11:54 TimerTask 与 Timer 除了指定几毫秒之后执行任务之外,您还可以要求Timer物件,每隔多少时间就执行一次TimerTask的run()方法,例如一秒后开始执 行TimerTask,而后每隔3秒执行一次: ● Main.java package onlyfun.caterpillar; import java.util.*; public class Main { public static void main(String[] args) { Timer timer = new Timer(); timer.schedule(new DateTask(), 1000, 3000); System.out.println("现在时间:" + new Date()); try { Thread.sleep(20000); } catch(InterruptedException e) { } timer.cancel(); } } 执行结果如下: 现在时间:Fri Jan 19 10:49:43 CST 2007 任务时间:Fri Jan 19 10:49:44 CST 2007 任务时间:Fri Jan 19 10:49:47 CST 2007 任务时间:Fri Jan 19 10:49:50 CST 2007 任务时间:Fri Jan 19 10:49:53 CST 2007 任务时间:Fri Jan 19 10:49:56 CST 2007 任务时间:Fri Jan 19 10:49:59 CST 2007 任务时间:Fri Jan 19 10:50:02 CST 2007 Timer的schedule()方法也可以接受Date实例,例如您想直接排定执行日为一个月之后,则可以透过Calendar来计算,再取得Date时 间: Calendar c = Calendar.getInstance(); c.add(Calendar.MONTH, 1); System.out.println("任务排定时间:" + c.getTime()); Timer timer = new Timer(); timer.schedule(new DateTask(), c.getTime(), 3000); Timer与TimerTask是独立的两个物件,当使用Timer的schedule()方法排定TimerTask之后,则必须等执行Timer的 cancel()执行之后, 让TimerTask与Timer脱离关系,TimerTask才可以重新加入其它Timer的排程。 http://caterpillar.onlyfun.net/GossipCN/JavaGossip-V2/TimerTaskAndTimer.htm(第 2/2 页)2008-7-30 20:11:54 Fixed-delay Execution 与 Fixed-rate Execution From Gossip@caterpillar Java Gossip: Fixed-delay Execution 与 Fixed-rate Execution 对于Timer,要将排定时间与任务真正执行时间分别看待。 Timer的schedule()方法在排程时,是采用Fixed- delay execution的方式。也就是如果这次run()方法在周期时间内执行完毕,则下次run() 就如期排程。如果这次run()方法无法在周期时间内完 成,则接下来的任务排定就会被拖延,等到上次任务完成后立即排定并执行。 如果想取得TimerTask的排定时间,则可以使用scheduledExecutionTime(),例如: ● DateTask.java package onlyfun.caterpillar; import java.util.*; public class DateTask extends TimerTask { public void run() { System.out.println("任务排定时间:" + new Date(scheduledExecutionTime())); System.out.println("任务执行时间:" + new Date() + "\n"); try { Thread.sleep(5000); } catch(InterruptedException e) { } } } 如果您使用以下的程式片段来进行排程: Timer timer = new Timer(); timer.schedule(new DateTask(), 1000, 3000); 由于在run()方法中,执行会超出所排定的周期,因此每次的下一次工作都被延迟,在上次工作完成后立即执行: http://caterpillar.onlyfun.net/GossipCN/JavaGossip-V2/FixedDelayOrFixedRate.htm(第 1/2 页)2008-7-30 20:11:59 Fixed-delay Execution 与 Fixed-rate Execution 任务排定时间:Fri Jan 19 11:21:32 CST 2007 任务执行时间:Fri Jan 19 11:21:32 CST 2007 任务排定时间:Fri Jan 19 11:21:37 CST 2007 任务执行时间:Fri Jan 19 11:21:37 CST 2007 任务排定时间:Fri Jan 19 11:21:42 CST 2007 任务执行时间:Fri Jan 19 11:21:42 CST 2007 任务排定时间:Fri Jan 19 11:21:47 CST 2007 任务执行时间:Fri Jan 19 11:21:47 CST 2007 任务排定时间:Fri Jan 19 11:21:52 CST 2007 任务执行时间:Fri Jan 19 11:21:52 CST 2007 Timer还有另一个scheduleAtFixedRate()方法,可以让您采用Fix-rate Execution。也就是如果这次run()方法在周期时间内执行完毕,则 下次run()就如期排程。如果这次run()方法无法在周期时间内完成,则 接下来的任务排定不会被拖延,但真正的执行要等到上次任务 完成后执行。 如果您使用以下的程式片段来进行排程: Timer timer = new Timer(); timer.scheduleAtFixedRate(new DateTask(), 1000, 3000); 在run()方法中,执行会超出所排定的周期,但Timer不管,仍旧照周期排程,而真正的执行则等到上次任务完成后执行: 任务排定时间:Fri Jan 19 11:20:06 CST 2007 任务执行时间:Fri Jan 19 11:20:06 CST 2007 任务排定时间:Fri Jan 19 11:20:09 CST 2007 任务执行时间:Fri Jan 19 11:20:11 CST 2007 任务排定时间:Fri Jan 19 11:20:12 CST 2007 任务执行时间:Fri Jan 19 11:20:16 CST 2007 任务排定时间:Fri Jan 19 11:20:15 CST 2007 任务执行时间:Fri Jan 19 11:20:21 CST 2007 任务排定时间:Fri Jan 19 11:20:18 CST 2007 任务执行时间:Fri Jan 19 11:20:26 CST 2007 http://caterpillar.onlyfun.net/GossipCN/JavaGossip-V2/FixedDelayOrFixedRate.htm(第 2/2 页)2008-7-30 20:11:59 制作 Executable JAR From Gossip@caterpillar Java Gossip: 制作 Executable JAR 撰写Java程式到这边,相信您一定会有所疑问的是,编出来的.class档案越来越多,难道要将这一堆.class档案直接给想要执行程式的人吗?在Windows下的 话,有没有办法按一下档案,就可以执行程式呢? 当然,实际上要交付程式时,并不是给一堆.class档案,而是会将编译好的.class档包装为一个Java Archive File,也就是副档名为.jar的档案,在JDK的bin目录 下,附带有一个jar工具程式,您可以直接执行jar程式,看看它的提示讯息: 直接执行jar工具程式,提示讯息中已清楚的说明如何使用jar程式,在这边使用 文字编辑器制作 完成的文字编辑器为例,来示范如何将程式包装为.jar的档 案,首先请建立一个jar目录,并在其下建立bin与classes目录,将您完成的文字编辑器程式放入classes中(包括套件的资料夹结构),待会将会产生的.jar则将 放入bin中。 http://caterpillar.onlyfun.net/GossipCN/JavaGossip-V2/ExecutableJAR.htm(第 1/3 页)2008-7-30 20:12:18 制作 Executable JAR 接着开启文字模式,切换工作目录至jar目录下,然后键入以下的指令,表示将建立一个JNotePad.jar放到bin目录中,来源是classes中的档案,被放入的档案将 以/作为.jar档案中的根目录: 接着您的bin目录中就会产生一个JNotePad.jar,要如何使用这个.jar档案呢?.jar档案中包括.class,基本上可以将.jar看作是一个特别的目录,所以要使用.jar档 案中的.class档案时,基本上也是指定Classpath,例如: java -cp ./bin/JNotePad.jar onlyfun.caterpillar.JNotePad 接着您的文字编辑器就会启动了,现在您不用将一堆.class档案交付出去,只要交付这个JNotePad.jar就可以了。 然而,真的要指定Classpath这么麻烦吗?其实还有更方便的做法,制作一个Executable Jar档案,指定读取.jar档案时要执行的Main-Class就可以了,这需要准备 一个manifest.txt,当中写下: http://caterpillar.onlyfun.net/GossipCN/JavaGossip-V2/ExecutableJAR.htm(第 2/3 页)2008-7-30 20:12:18 制作 Executable JAR 注意写完Main-Class之后,要按下Enter键新增一行,在Windows下这个动作是必要的,否则会发生无法读取Main-Class属性的错误。假设manifest.txt放在jar目 录下,接着如下执行指令: 在.jar档案制作出来后,您可以在执行java时指定-jar引数,以及您的.jar档案,java程式会自动寻找Main-Class并执行,例如下达以下的指令: java -jar bin/JNotePad.jar 接着您的文字编辑器就会启动了,如果您的作业系统是Windows,由于安装完JRE之后,会将.jar预设由javaw程式开启,所以您可以直接在JNotePad.jar档案 上,使用滑鼠左键按两下直接开启程式来执行。 http://caterpillar.onlyfun.net/GossipCN/JavaGossip-V2/ExecutableJAR.htm(第 3/3 页)2008-7-30 20:12:18 启动画面与工具列图示 - Java SE 6 From Gossip@caterpillar Java Gossip: 启动画面与工具列图示 - Java SE 6 有些视窗程式在启动时,会有个启动画面,在Java SE 6之前,您要自己实作才可以拥有这个功能,现在您可以直接在使用"java"程式 执行程式时下达"-splash"引数指定启动画面的图片,就可以拥有这个功能,例如若执行 制作 Executable JAR 所制作出来的 Executable Jar档时,可以如下指定图片: java -splash:caterpillar.jpg -jar JNotePad.jar 其中caterpillar.jpg是启动画面的图片,支援的图片可以是JPG、GIF或PNG,GIF若有动画效果则可以呈现出来。 您也可以在制作Executable JAR档案时,于manifest档案中指定"SplashScreen-Image"为启动画面的图片,并在使用jar程式进行包装时 一并包装图片,如 此启动JAR档案时,就会自动展现启动画面,一个manifest档案的写法如下所示: ● manifest.txt Manifest-Version: 1.0 Main-Class: onlyfun.caterpillar.JNotePad SplashScreen-Image: caterpillar.jpg 如果您对于启动画面更进一步的控制感兴趣,例如在不同的启动阶段显示不同的图片,或者是在启动图片上显示进度列,则可以看 看java.awt. SplashScreen的API文件说明。 在Java SE 6中加入了系统工具列图示的支援,您可以使用SystemTray类别的isSupported()方法,测试看看目前的系统是否支援系统工 具列图示,如 果支援的话,可以使用getSystemTray()取得SystemTray实例,使用add()方法加入TrayIcon实例,如此就可以加入一个 系 统工具列图示,例如: ● SystemTrayDemo.java package onlyfun.caterpillar; import java.awt.*; public class SystemTrayDemo { public static void main(String[] args) { if(SystemTray.isSupported()) { SystemTray tray = SystemTray.getSystemTray(); Image image = Toolkit.getDefaultToolkit() .getImage("musical_note_smile.gif"); TrayIcon trayIcon = new TrayIcon(image, "JNotePad 1.0"); try { tray.add(trayIcon); } catch (AWTException e) { System.err.println("无法加入系统工具列图示"); e.printStackTrace(); } } else { http://caterpillar.onlyfun.net/GossipCN/JavaGossip-V2/SystemTray-JavaSE6.htm(第 1/3 页)2008-7-30 20:12:23 启动画面与工具列图示 - Java SE 6 System.err.println("无法取得系统工具列"); } } } 一个执行的结果画面如下所示: 如果想在系统工具列图示上按右键时,可以出现蹦现视窗,则可以在建构TrayIcon实例时,指定一个PopupMenu实例给它,例如: ● SystemTrayDemo2.java package onlyfun.caterpillar; import java.awt.*; import javax.swing.*; public class SystemTrayDemo2 { public static void main(String[] args) { if(SystemTray.isSupported()) { SystemTray tray = SystemTray.getSystemTray(); Image image = Toolkit.getDefaultToolkit() .getImage("musical_note_smile.gif"); PopupMenu popup = new PopupMenu(); MenuItem item = new MenuItem("开启JNotePad 1.0"); popup.add(item); TrayIcon trayIcon = new TrayIcon(image, "JNotePad 1.0", popup); try { tray.add(trayIcon); } catch (AWTException e) { System.err.println("无法加入系统工具列图示"); e.printStackTrace(); } } else { System.err.println("无法取得系统工具列"); }工具列图示 } } 执行以上程式,并在出现的图示上按滑鼠右键,将会出现以下的画面: http://caterpillar.onlyfun.net/GossipCN/JavaGossip-V2/SystemTray-JavaSE6.htm(第 2/3 页)2008-7-30 20:12:23 启动画面与工具列图示 - Java SE 6 如果打算在系统工具列图示上主动显示讯息,则可以使用TrayIcon的displayMessage()方法,例如: trayIcon.displayMessage("哈啰", "该休息了吗?", TrayIcon.MessageType.WARNING); TrayIcon可以显示的MessageType包括有NONE、INFO、WARNING及ERROR,分别会显示不同的图示,上面这段程式片段执行结果 画面如下所示: 如果要移除系统工具列中的图示,则可以使用SystemTray实例的remove()方法,指定要移除的图示,例如: tray.remove(trayIcon); http://caterpillar.onlyfun.net/GossipCN/JavaGossip-V2/SystemTray-JavaSE6.htm(第 3/3 页)2008-7-30 20:12:23
还剩247页未读

继续阅读

下载pdf到电脑,查找使用更方便

pdf的实际排版效果,会与网站的显示效果略有不同!!

需要 15 金币 [ 分享pdf获得金币 ] 5 人已下载

下载pdf

pdf贡献者

丨风影丨

贡献于2011-07-21

下载需要 15 金币 [金币充值 ]
亲,您也可以通过 分享原创pdf 来获得金币奖励!
下载pdf