程序師世界是廣大編程愛好者互助、分享、學習的平台,程序師世界有你更精彩!
首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫知識
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
 程式師世界 >> 編程語言 >> JAVA編程 >> 關於JAVA >> 關於Java常用工具您不知道的5件事

關於Java常用工具您不知道的5件事

編輯:關於JAVA

Java 常用工具,如解析、計時和聲音

很多年前,當我還是高中生的時候,我曾考慮以小說作家作為我的職業追求,我訂閱了一本 Writer's Digest 雜志。我記得其中有篇專 欄文章,是關於 “太小而難以保存的線頭”,專欄作者描述廚房儲物抽屜中放滿了無法分類的玩意兒。這句話我一直銘記在心,它正好用 來描述本文的內容,本系列的最後一篇(至少目前是這樣)。

Java 平台就充滿了這樣的 “線頭” — 有用的命令行工具和庫,大多數 Java 開發人員甚至都不知道,更別提使用了。其中很多無法 劃分到之前的 5 件事 系列 的編程分類中,但不管怎樣,嘗試一下:有些說不定會在您編程的廚房抽屜中占得一席之地。

1. StAX

在千禧年左右,當 XML 第一次出現在很多 Java 開發人員面前時,有兩種基本的解析 XML 文件的方法。SAX 解析器實際是由程序員對 事件調用一系列回調方法的大型狀態機。DOM 解析器將整個 XML 文檔加入內存,並切割成離散的對象,它們連接在一起形成一個樹。該樹 描述了文檔的整個 XML Infoset 表示法。這兩個解析器都有缺點:SAX 太低級,無法使用,DOM 代價太大,尤其對於大的 XML 文件 — 整 個樹成了一個龐然大物。

幸運的是,Java 開發人員找到第三種方法來解析 XML 文件,通過對文檔建模成 “節點”,它們可以從文檔流中一次取出一個,檢查, 然後處理或丟棄。這些 “節點” 的 “流” 提供了 SAX 和 DOM 的中間地帶,名為 “Streaming API for XML”,或者叫做StAX。(此縮 寫用於區分新的 API 與原來的 SAX 解析器,它與此同名。)StAX 解析器後來包裝到了 JDK 中,在 javax.xml.stream 包。

使用 StAX 相當簡單:實例化 XMLEventReader,將它指向一個格式良好的 XML 文件,然後一次 “拉出” 一個節點(通常用 while 循 環),查看。例如,在清單 1 中,列舉出了 Ant 構造腳本中的所有目標:

清單 1. 只是讓 StAX 指向目標

import java.io.*;
import javax.xml.namespace.QName;
import javax.xml.stream.*;
import javax.xml.stream.events.*;
import javax.xml.stream.util.*;

public class Targets
{
   public static void main(String[] args)
     throws Exception
   {
     for (String arg : args)
     {
       XMLEventReader xsr =
         XMLInputFactory.newInstance()
           .createXMLEventReader(new FileReader(arg));
       while (xsr.hasNext())
       {
         XMLEvent evt = xsr.nextEvent();
         switch (evt.getEventType())
         {
           case XMLEvent.START_ELEMENT:
           {
             StartElement se = evt.asStartElement();
             if (se.getName().getLocalPart().equals("target"))
             {
               Attribute targetName =
                 se.getAttributeByName(new QName("name"));
               // Found a target! 
               System.out.println(targetName.getValue());
             }
             break;
           }
           // Ignore everything else
         }
       }
     }
   }
}

StAX 解析器不會替換所有的 SAX 和 DOM 代碼。但肯定會讓某些任務容易些。尤其對完成不需要知道 XML 文檔整個樹結構的任務相當 方便。

請注意,如果事件對象級別太高,無法使用,StAX 也有一個低級 API 在 XMLStreamReader 中。盡管也許沒有閱讀器有用,StAX 還有 一個 XMLEventWriter,同樣,還有一個 XMLStreamWriter 類用於 XML 輸出。

2. ServiceLoader

Java 開發人員經常希望將使用和創建組件的內容區分開來。這通常是通過創建一個描述組件動作的接口,並使用某種中介創建組件實例 來完成的。很多開發人員使用 Spring 框架來完成,但還有其他的方法,它比 Spring 容器更輕量級。

java.util 的 ServiceLoader 類能讀取隱藏在 JAR 文件中的配置文件,並找到接口的實現,然後使這些實現成為可選擇的列表。例如 ,如果您需要一個私僕(personal-servant)組件來完成任務,您可以使用清單 2 中的代碼來實現:

清單 2. IPersonalServant

public interface IPersonalServant
{
   // Process a file of commands to the servant
   public void process(java.io.File f)
     throws java.io.IOException;
   public boolean can(String command);
}

can() 方法可讓您確定所提供的私僕實現是否滿足需求。清單 3 中的 ServiceLoader 的 IPersonalServant 列表基本上滿足需求:

清單 3. IPersonalServant 行嗎?

import java.io.*;
import java.util.*;

public class Servant
{
   public static void main(String[] args)
     throws IOException
   {
     ServiceLoader<IPersonalServant> servantLoader =
       ServiceLoader.load(IPersonalServant.class);

     IPersonalServant i = null;
     for (IPersonalServant ii : servantLoader)
       if (ii.can("fetch tea"))
         i = ii;

     if (i == null)
       throw new IllegalArgumentException("No suitable servant found");

     for (String arg : args)
     {
       i.process(new File(arg));
     }
   }
}

假設有此接口的實現,如清單 4:

清單 4. Jeeves 實現了 IPersonalServant

import java.io.*;

public class Jeeves
   implements IPersonalServant
{
   public void process(File f)
   {
     System.out.println("Very good, sir.");
   }
   public boolean can(String cmd)
   {
     if (cmd.equals("fetch tea"))
       return true;
     else
       return false;
   }
}

剩下的就是配置包含實現的 JAR 文件,讓 ServiceLoader 能識別 — 這可能會非常棘手。JDK 想要 JAR 文件有一個 META- INF/services 目錄,它包含一個文本文件,其文件名與接口類名完全匹配 — 本例中是 META-INF/services/IPersonalServant。接口類名 的內容是實現的名稱,每行一個,如清單 5:

清單 5. META-INF/services/IPersonalServant

Jeeves  # comments are OK

幸運的是,Ant 構建系統(自 1.7.0 以來)包含一個對 jar 任務的服務標簽,讓這相對容易,見清單 6:

清單 6. Ant 構建的 IPersonalServant

<target name="serviceloader" depends="build">
     <jar destfile="misc.jar" basedir="./classes">
       <service type="IPersonalServant">
         <provider classname="Jeeves" />
       </service>
     </jar>
   </target>

這裡,很容易調用 IPersonalServant,讓它執行命令。然而,解析和執行這些命令可能會非常棘手。這又是另一個 “小線頭”。

3. Scanner

有無數 Java 工具能幫助您構建解析器,很多函數語言已成功構建解析器函數庫(解析器選擇器)。但如果要解析的是逗號分隔值文件 ,或空格分隔文本文件,又怎麼辦呢?大多數工具用在此處就過於隆重了,而 String.split() 又不夠。(對於正則表達式,請記住一句老 話:“ 您有一個問題,用正則表達式解決。那您就有兩個問題了。”)

Java 平台的 Scanner 類會是這些類中您最好的選擇。以輕量級文本解析器為目標,Scanner 提供了一個相對簡單的 API,用於提取結 構化文本,並放入強類型的部分。想象一下,如果您願意,一組類似 DSL 的命令(源自 Terry Pratchett Discworld 小說)排列在文本文 件中,如清單 7:

清單 7. Igor 的任務

fetch 1 head 
fetch 3 eye
fetch 1 foot
attach foot to head 
attach eye to head 
admire 

您,或者是本例中稱為 Igor的私僕,能輕松使用 Scanner 解析這組違法命令,如清單 8 所示:

清單 8. Igor 的任務,由 Scanner 解析

import java.io.*;
import java.util.*;

public class Igor 
   implements IPersonalServant
{
   public boolean can(String cmd)
   {
     if (cmd.equals("fetch body parts"))
       return true;
     if (cmd.equals("attach body parts"))
       return true;
     else
       return false;
   }
   public void process(File commandFile)
     throws FileNotFoundException
   {
     Scanner scanner = new Scanner(commandFile);
     // Commands come in a verb/number/noun or verb form
     while (scanner.hasNext())
     {
       String verb = scanner.next();
       if (verb.equals("fetch"))
       {
         int num = scanner.nextInt();
         String type = scanner.next();
         fetch (num, type);
       }
       else if (verb.equals("attach"))
       {
         String item = scanner.next();
         String to = scanner.next();
         String target = scanner.next();
         attach(item, target);
       }
       else if (verb.equals("admire"))
       {
         admire();
       }
       else
       {
         System.out.println("I don't know how to "
           + verb + ", marthter.");
       }
     }
   }

   public void fetch(int number, String type)
   {
     if (parts.get(type) == null)
     {
       System.out.println("Fetching " + number + " "
         + type + (number > 1 ? "s" : "") + ", marthter!");
       parts.put(type, number);
     }
     else
     {
       System.out.println("Fetching " + number + " more "
         + type + (number > 1 ? "s" : "") + ", marthter!");
       Integer currentTotal = parts.get(type);
       parts.put(type, currentTotal + number);
     }
     System.out.println("We now have " + parts.toString());
   }

   public void attach(String item, String target)
   {
     System.out.println("Attaching the " + item + " to the " +
       target + ", marthter!");
   }

   public void admire()
   {
     System.out.println("It'th quite the creathion, marthter");
   }

   private Map<String, Integer> parts = new HashMap<String, Integer>();
}

假設 Igor 已在 ServantLoader 中注冊,可以很方便地將 can() 調用改得更實用,並重用前面的 Servant 代碼,如清單 9 所示:

清單 9. Igor 做了什麼

import java.io.*;
import java.util.*;

public class Servant
{
   public static void main(String[] args)
     throws IOException
   {
     ServiceLoader<IPersonalServant> servantLoader =
       ServiceLoader.load(IPersonalServant.class);

     IPersonalServant i = null;
     for (IPersonalServant ii : servantLoader)
       if (ii.can("fetch body parts"))
         i = ii;

     if (i == null)
       throw new IllegalArgumentException("No suitable servant found");

     for (String arg : args)
     {
       i.process(new File(arg));
     }
   }
} 

真正 DSL 實現顯然不會僅僅打印到標准輸出流。我把追蹤哪些部分、跟隨哪些部分的細節留待給您(當然,還有忠誠的 Igor)。

4. Timer

java.util.Timer 和 TimerTask 類提供了方便、相對簡單的方法可在定期或一次性延遲的基礎上執行任務:

清單 10. 稍後執行

import java.util.*;

public class Later 
{
   public static void main(String[] args)
   {
     Timer t = new Timer("TimerThread");
     t.schedule(new TimerTask() {
       public void run() {
         System.out.println("This is later");
         System.exit(0);
       }
     }, 1 * 1000);
     System.out.println("Exiting main()");
   }
}

Timer 有許多 schedule() 重載,它們提示某一任務是一次性還是重復的,並且有一個啟動的 TimerTask 實例。TimerTask 實際上是一 個 Runnable(事實上,它實現了它),但還有另外兩個方法:cancel() 用來取消任務,scheduledExecutionTime() 用來返回任務何時啟 動的近似值。

請注意 Timer 卻創建了一個非守護線程在後台啟動任務,因此在清單 10 中我需要調用 System.exit() 來取消任務。在長時間運行的 程序中,最好創建一個 Timer 守護線程(使用帶有指示守護線程狀態的參數的構造函數),從而它不會讓 VM 活動。

這個類沒什麼神奇的,但它確實能幫助我們對後台啟動的程序的目的了解得更清楚。它還能節省一些 Thread 代碼,並作為輕量級 ScheduledExecutorService(對於還沒准備好了解整個 java.util.concurrent 包的人來說)。

5. JavaSound

盡管在服務器端應用程序中不常出現,但 sound 對管理員有著有用的 “被動” 意義 — 它是惡作劇的好材料。盡管它很晚才出現在 Java 平台中,JavaSound API 最終還是加入了核心運行時庫,封裝在 javax.sound * 包 — 其中一個包是 MIDI 文件,另一個是音頻文件 示例(如普遍的 .WAV 文件格式)。

JavaSound 的 “hello world” 是播放一個片段,如清單 11 所示:

清單 11. 再放一遍,Sam

public static void playClip(String audioFile)
{
   try
   {
     AudioInputStream inputStream =
       AudioSystem.getAudioInputStream(
         this.getClass().getResourceAsStream(audioFile));
     DataLine.Info info =
       new DataLine.Info( Clip.class, audioInputStream.getFormat() );
     Clip clip = (Clip) AudioSystem.getLine(info);
     clip.addLineListener(new LineListener() {
         public void update(LineEvent e) {
           if (e.getType() == LineEvent.Type.STOP) {
             synchronized(clip) {
               clip.notify();
             }
           }
         }
       });
     clip.open(audioInputStream);

     clip.setFramePosition(0);

     clip.start();
     synchronized (clip) {
       clip.wait();
     }
     clip.drain();
     clip.close();
   }
   catch (Exception ex)
   {
     ex.printStackTrace();
   }
}

大多數還是相當簡單(至少 JavaSound 一樣簡單)。第一步是創建一個文件的 AudioInputStream 來播放。為了讓此方法盡量與上下文 無關,我們從加載類的 ClassLoader 中抓取文件作為 InputStream。(AudioSystem 還需要一個 File 或 String,如果提前知道聲音文件 的具體路徑。)一旦完成, DataLine.Info 對象就提供給 AudioSystem,得到一個 Clip,這是播放音頻片段最簡單的方法。(其他方法提 供了對片段更多的控制 — 例如獲取一個 SourceDataLine — 但對於 “播放” 來說,過於復雜)。

這裡應 該和對 AudioInputStream 調用 open() 一樣簡單。(“應該” 的意思是如果您沒遇到下節描述的錯誤。)調用 start() 開始 播放,drain() 等待播放完成,close() 釋放音頻線路。播放是在單獨的線程進行,因此調用 stop() 將會停止播放,然後調用 start() 將會從播放暫停的地方重新開始;使用 setFramePosition(0) 重新定位到開始。

沒聲音?

JDK 5 發行版中有個討厭的小錯 誤:在有些平台上,對於一些短的音頻片段,代碼看上去運行正常,但就是 ... 沒聲音。顯然媒體播放器在應該出現的位置之前觸發了 STOP 事件。

這個錯誤 “無法修復”,但解決方法相當簡單:注冊一個 LineListener 來監聽 STOP 事件,當觸發時, 調用片段對象的 notifyAll()。然後在 “調用者” 代碼中,通過調用 wait() 等待片段完成(還調用 notifyAll())。在沒出 現錯誤的平台上,這些錯誤是多余的,在 Windows® 及有些 Linux® 版本上,會讓程序員 “開心” 或 “憤怒 ”。

結束語

現在您都了解了,廚房裡的工具。我知道很多人已清楚了解我此處介紹的工具,而我的職業經驗告訴我,很多人將從這篇 介紹文章,或者說是對長期遺忘在凌亂的抽屜中的小工具的提示中受益。

我在此系列中做個簡短的中斷,讓別人能加入分享他們各 自領域的專業經驗。但別擔心,我還會回來的,無論是本系列還是探索其他領域的新的 5 件事。在那之前,我鼓勵您一直探索 Java 平台 ,去發現那些能讓編程更高效的寶石。

  1. 上一頁:
  2. 下一頁:
Copyright © 程式師世界 All Rights Reserved