ID #12978

使用泛型和并发改善集合

自从最初的 Java 2 platform, Version 1.2 发布以后,Java Collections Framework 一直在不断发展。在 Java SE 5 中,泛型的引入增强了框架, java.util.concurrent 的引入添加了对并发的直接支持(请参阅 参考资料)。 在 Java SE 6 中,框架中添加了更好的双向集合访问特性。本文将向您介绍集 合库的所有这些方面,并帮助您利用与并发相关的流行功能。

本文的高级任务是创建一个 Web crawler:给定一个网站的基 URL,从该网 站收集可以用作某种用途的元素。您将从单个网页收集一系列链接,然后蔓延到 整个网站。把高级任务分解为子任务,这些子任务可以转化为自己的作业。您将 了解并使用泛型和线程池。为了使任务更加简单,我们将任务作为独立的客户端 应用程序实现。(解释如何部署 Web 应用程序并不是本文的中心目的。但是可 以随意创建一个 Web 应用程序,将任务作为附加的练习在此应用程序中启动。 )

您应该熟悉 Java 平台上的程序开发。本文假设您熟悉连网和 I/O 库,这两 方面知识将分别用于 socket 连接和读取流。您需要安装一个开发人员版本的 Java SE 6 平台。它至少应该是来自 Sun Microsystems 的 Update 5 of JDK 6 或来自 IBM 的 最新的 SDK for Java, Version 6。

了解泛型

从 Java SE 5 版本开始,泛型的概念就成为了 Java 平台的一部分(请参阅 参考资料)。简单来说,泛型为集合提供了编译时类型安全。在早期的 Java 平 台版本中,您创建一个集合,并向其中添加项,如清单 1 所示:

清单 1. 向集合添加项 — 旧方法

List buttonList = new LinkedList();
buttonList.add(new JButton("One"));
buttonList.add(new JButton("Two"));
buttonList.add(new JButton("Three"));
buttonList.add(new JButton("Four"));

要从集合中提取元素,您必须知道集合中对象的类型,以将其强制转换为合 适的局部变量:

JButton first = (JButton)buttonList.get(0);

您并不需要 将其强制转换为正确的类型,但是如果您想要对某个特定类类型 进行操作,则需要这么做。这种方法运行得很好,除非您不小心向集合中添加了 错误的类型对象:

buttonList.add(new JLabel("Five"));

现在,如果您尝试将最后一个元素作为 JButton 来提取,则在运行时会出现 一个类转换异常:

Line 13: JButton last = (JButton)buttonList.get(4);
>java GetIt
Exception in thread "main" java.lang.ClassCastException:
  javax.swing.JLabel cannot be cast to javax.swing.JButton
     at GetIt.main(GetIt.java:13)

在本质上,将 JLabel 放入集合并没有任何问题,但是如果提取代码希望集 合中的所有元素都是同一类型(这里为 JButton),那么从集合中提取一个 JLabel 就会生成 ClassCastException。这个异常只会在运行时出现;如果没有 进行足够的测试,那么也许直到部署之后才会出现该异常。

泛型集合的使用

现在进入泛型的世界。泛型可以帮助您在开发周期的早期解决编码问题。不 只是拥有一个集合并向其中添加 JButton 对象,您可以拥有一个 JButton 对象 的集合。然后,如果想要将 JLabel 添加到集合,则编译器会在编译时发现差异 和并抛出异常。

在尝试向泛型集合(本例中为 List<JButton>)添加错误类型的元素 时,清单 2 中的程序会生成编译时错误消息:

清单 2. 使用泛型的示例代码(没有编译)

import java.util.*;
import javax.swing.*;
public class GetIt {
  public static void main(String args[]) {
   List<JButton> buttonList = new LinkedList<JButton> ();
   buttonList.add(new JButton("One"));
   buttonList.add(new JButton("Two"));
   buttonList.add(new JButton("Three"));
   buttonList.add(new JButton("Four"));
   JButton first = buttonList.get(0);
   buttonList.add(new JLabel("Five"));
   JButton last = buttonList.get(4);
  }
}

当您保存并编译该应用程序时,您将注意到对 add() 的最后调用失败了:

>javac GetIt.java
GetIt.java:12: cannot find symbol
symbol : method add(javax.swing.JLabel)
location: interface java.util.List<javax.swing.JButton>
         buttonList.add(new JLabel("Five"));
              ^
1 error

错误消息的第二行表明,您尝试将一个 JLabel 添加到第三个错误行报告的 JButton 对象的 List。然后您必须决定该集合是否必须为 Component 对象(或 者 JComponent,如果您想要使用 Swing 平台组件)的集合,或者您是否不应该 尝试在第一个位置添加 JLabel。

注意,在 清单 2 中,从集合中提取项并不需要将其强制转换为正确的类型 。因为您已经说明了集合为某种类型,从集合中提取项的所有调用都返回给定的 类型。

泛型的使用使您的代码库更容易维护,尤其当代码库不断增长,以及将代码 元素转换为可重复使用的库时。库的用户不用担心对集合中对象的类型有任何限 制。正确定义的方法应该在其定义中包含这些类型。并且如果您的类型不符合该 类型,编译器会给出警告。

泛型编译器问题

当一个类使用的集合的定义中缺少泛型类型时,在编译这个类时,编译器就 会报错,比如编译 清单 1 中的代码就会出现这种情形。例如,假设您想要编译 一个包含以下行的类:

List buttonList = new LinkedList();

编译器会发出一个警告:

>javac GetIt.java
Note: GetIt.java uses unchecked or unsafe operations.
Note: Recompile with -Xlint:unchecked for details.

您可以忽略该警告,不予理会。假设您并没有意外添加错误的数据类型到集 合中,那么一切都会运行良好。

查看详细的警告

要了解编译器警告您的特定问题的详细信息,可以向编译器发出 - Xlint:unchecked 命令。您将看到如清单 3 所示的输出:

清单 3. 使用 Xlint 进行编译的详细信息

>javac -Xlint:unchecked GetIt.java
GetIt.java:7: warning: [unchecked] unchecked call to add(E) as a member of
     the raw type java.util.List
   buttonList.add(new JButton("One"));
          ^
GetIt.java:8: warning: [unchecked] unchecked call to add(E) as a member of
     the raw type java.util.List
   buttonList.add(new JButton("Two"));
          ^
GetIt.java:9: warning: [unchecked] unchecked call to add(E) as a member of
     the raw type java.util.List
   buttonList.add(new JButton("Three"));
          ^
GetIt.java:10: warning: [unchecked] unchecked call to add(E) as a member of
     the raw type java.util.List
   buttonList.add(new JButton("Four"));
          ^
4 warnings

在清单 3 中可以看到,编译器并不特别关心 List 未定义数据类型。它显示 的是,每个对 add() 的调用都存在问题,因为该 List 未定义数据类型。

另外,这些都是警告,所以您可以忽略它们。但是,修复该集合以显式地指 定类型,将会避免在编译时遇到这些警告引起的一个真正的错误。

禁止编译器警告

如果您使用的是一个不能或不想更改的库,那么您可以禁止编译器警告。 @SuppressWarnings 注释会告诉编译器您知道代码生成了警告,但是您不想看到 它们。如果您将下面这行代码添加到想要忽略其警告的方法前面,则编译器不再 显示该方法的警告:

@SuppressWarnings("unchecked")

现在,当您编译该类时,将不会看到警告消息或错误消息。如果处理未预料 到的数据类型,您仍然有可能获得 ClassCastException。选择权在您手中。

读取网页

现在您应该深刻了解了泛型的用途,以及它们如何使您的程序更容易维护。 下一步是创建一个程序来收集某个特定网页上的所有链接。尽管您可以自己写一 个程序来读取网页并解析其内容,但是不必这么做。Swing 组件库提供了这项功 能。您需要做的就是查找与页面上的锚(<a>)标记相关联的 href 属性 。

获取文档

javax.swing.text.html 包包含一个 HTMLEditorKit。如果您向它提供一个 流,它会解析相关的网页。根据这个解析的流,您可以告诉工具箱遍历所有可用 的标记,并获得锚标记的 href 属性。程序的功能还可以更加丰富,可以收集图 像标记或 Flash 影片,但是它只会收集 <a href="...">xxx</a> 形式的内容。

您需要做的只是创建一个新的 HTMLEditorKit 实例,并将一个 Reader 传入 到内容中。因为想法是从远程网站获得内容,所以您必须使用在命令行输入的 http:// 字符串来获得 Reader,该字符串随后被传递到 URL 构造器,您可以从 中获得一个 URLConnection。这个过程听起来很复杂,其实并不是这样。清单 4 显示了它的工作原理:

清单 4. 读取网页

HttpURLConnection.setFollowRedirects(false);
EditorKit kit = new HTMLEditorKit();
Document doc = kit.createDefaultDocument();
doc.putProperty("IgnoreCharsetDirective", Boolean.TRUE);
String uri = args[0];
Reader reader = null;
if (uri != null && uri.startsWith("http")) {
  URLConnection conn = new URL(uri).openConnection();
  reader = new InputStreamReader(conn.getInputStream());
} else {
  System.err.println(
  "Usage: java ListUrls http://example.com/startingpage");
  System.exit(-1);
}
kit.read(reader, doc, 0);

与连接相关联的输入流被提供给 EditorKit 的 read() 方法。read() 的其 他参数包括一个 Document 和一个开始读取的位置,前者是您通过调用工具箱的 createDefaultDocument() 方法创建的,后者通常为 0,表示流的起点。

清单 4 添加了两个有帮助的附加任务。调用 HttpURLConnection 类的 setFollowRedirects() 方法可禁止后面的重定向请求。而设置 Document 的 IgnoreCharsetDirective 属性是因为,当页面的 <meta> 标记中包含一 个 charset 属性时,HTMLEditorKit 中明显存在一个 bug。

遍历元素

您将使用的下一个 Swing 类是 ElementIterator,可以在 javax.swing.text 包中找到。使用 Document(与刚创建的 Document 类似), 您可以遍历其中所有的元素:

ElementIterator it = new ElementIterator(doc);
javax.swing.text.Element elem;
while ((elem = it.next()) != null) {
  // ...
}

通过搜索 <a> 标记,您可以获得相关的 href 属性并添加到发现的链 接集合中。这里使用的集合是一个 Set,因为没有必要收集重复的内容:

Set<String> uriList = new TreeSet<String>();
// Below is inside of while loop
AttributeSet s = (AttributeSet)
  elem.getAttributes().getAttribute(HTML.Tag.A);
if (s != null) {
  String href = (String)
   s.getAttribute(HTML.Attribute.HREF);
  uriList.add(href);
}

尽管到目前为止执行的步骤已经足够收集所有链接,您也可以处理一些特殊 的情况。比如,在发现的 href 为空的地方不需要添加链接 — 格式良好的文档 不应该出现这种情况,但是有时候确实会出现。另外,内部链接没有前导的 http://。最好将这些内部链接附加到文档的基 URL 之后,这样如果您需要再次 遍历该列表(比如在下一任务中),您可以拥有完整的 URL。而且,最好不要使 用 javascript: 标记。还可以进行其他更多增强。清单 5 显示了完整的程序:

清单 5. 列出单个页面的 URL 的代码

import java.io.*;
import java.net.*;
import java.util.*;
import javax.swing.text.*;
import javax.swing.text.html.*;
public class ListUrls {
  public static void main(String args[]) throws Exception {
   Set<String> uriList = new TreeSet<String>();
   HttpURLConnection.setFollowRedirects(false);
   EditorKit kit = new HTMLEditorKit();
   Document doc = kit.createDefaultDocument();
   doc.putProperty("IgnoreCharsetDirective", Boolean.TRUE);
   String uri = args[0];
   Reader reader = null;

   if (uri != null && uri.startsWith("http")) {
    URLConnection conn = new URL(uri).openConnection();
    reader = new InputStreamReader(conn.getInputStream());
   } else {
    System.err.println(
     "Usage: java ListUrls http://example.com/startingpage");
    System.exit(-1);
   }
   kit.read(reader, doc, 0);
   ElementIterator it = new ElementIterator(doc);
   javax.swing.text.Element elem;
   while ((elem = it.next()) != null) {
    AttributeSet s = (AttributeSet)
     elem.getAttributes().getAttribute(HTML.Tag.A);
    if (s != null) {
     String href = (String)s.getAttribute (HTML.Attribute.HREF);
     if (href == null) {
      continue;
     } else if (href.startsWith("javascript:")) {
      continue; // skip it
     } else if (href.startsWith("https:")) {
      // add as is
     } else if (!href.startsWith("http:")) {
      href = uri + href;
     }
     uriList.add(href);
    }
   }
   for (String element: uriList) {
    System.out.printf(">>%s<<%n", element);
   }
  }
}

该程序打印出了收集的 URL 集合。下载并编译 ListUrls 程序,通过在命令 行传入一个 URL 来运行该程序(要获取本文的完整源代码,请参阅 下载 部分 的链接)。确切的结果取决于您收集的页面。

.

线程池

清单 5 中的 ListUrls 程序收集某个特定页面上的所有外出链接。要改进此 程序,使其作用到整个网站,最好将其分解为小一些的任务。尽管可以在一个线 程中完成所有工作,但是应用程序肯定会受到 I/O 延迟的阻碍,因为它必须首 先读取完整的网页,然后才对其进行处理。网络延迟是将工作分解为多个线程另 一个原因。在单独的线程内处理 Set 的每个元素应该可以显著提高整个工作的 处理速度。当然,您需要限制线程的数量,否则将会执行太多的任务,这些任务 之间的交换将会花费更多的时间。

Executor

Java SE 5 引入了 java.util.concurrent 库和泛型(请参阅 参考资料)。 Executor 接口接受 Runnable 对象并执行。这类似于将一个 Runnable 对象传 递到 Thread 构造器中,但是借助 Executor,当一个 Thread 处理完一个 Runnable 后,可以重新使用它获得新的 Runnable。因此,该程序避免了不断丢 弃并重新创建线程的过程。Executor 接口有一个 execute() 方法,它接受 Runnable 参数。具体结果取决于 Executor 接口的特定实现。

Executor 的一个实现就是 ThreadPoolExecutor。使用 Executors 工具类来 创建线程池,而不是直接调用 ThreadPoolExecutor 构造器来创建。对于固定大 小的线程池,使用 newFixedThreadPool(int maxThreads);或者使用 newFixedThreadPool(int maxThreads, ThreadFactor factory),它允许您提供 一个用于创建底层线程的工厂。

创建线程池之后,使用 service(Runnable) 方法添加要运行的任务。对于您 创建的 Web crawler,可以调用 awaitTermination() 方法来确定所有任务何时 完成,或者至少确定出线程池何时终止,如清单 6 所示:

清单 6. 使用线程池

String uri =...
ExecutorService service = Executors.newFixedThreadPool(5);
service.execute(new Crawler(service, uri, uri));
service.awaitTermination(300, TimeUnit.SECONDS);
for (String element: allUriList) {
  System.out.printf(">>%s<<%n", element);
}

awaitTermination() 方法接受一个超时。清单 6 中的程序被设置为五分钟 后超时。根据想要让程序运行多久、网络连接速度和想对网站进行处理的深度, 您可以使用更长或更短的超时。

另外请注意,只有基 URI 字符串被添加到了 crawler。读取每个页面时,会 将新的 URI 添加到作业队列。

Runnable

该服务执行的 Runnable 任务是 清单 5 中的大量代码。我添加了一些额外 的检查,以改进构建下一个页面的 URL 的过程。execute() 方法末尾的检查确 定服务是否应该终止。通常情况下,线程池会运行到程序结束,但是在线程池完 成时这个程序才会结束,所以这个检查非常必要。

下载 CollectUrls 程序,并在一个相对较小的网站上运行它,最好是您自己 的网站,以从该网站获得所有的链接。您也可以修改该程序以保持一个多重映射 :如果您知道每个链接的源,您可以自动生成网站层次和互连的映射。

其他线程池

CollectUrls Web crawler 程序利用一个固定大小的线程池。但它不是惟一 的选择。可以使用 Executors 工具类创建其他三种线程池:

newCachedThreadPool() 可创建极大的线程池,但是当线程空闲太久时,它 会终止线程。如果您有很多短期的异步任务,可以考虑使用它。如果线程池中有 可用的线程,它就会被使用。如果没有可用的线程,则会创建一个新线程,然后 如果线程池中的线程空闲了 60 秒,该线程就会消失。当没有进行任何任务时, 不会使用任何资源。相反,当没有任务要完成时,固定大小的线程池会让所有的 线程等待。

newSingleThreadExecutor() 创建的线程池对需要按顺序执行的作业非常有 用。如果底层的线程终止了,它会被重新创建。这类似于创建一个固定大小的线 程池,但是固定大小的线程池无法更改大小。

newScheduledThreadPool() 可以创建像 Timer 对象一样工作的线程池,但 是能够更好地处理未捕获的异常和线程饥饿。借助 Timer 类,您可以拥有一个 长时间运行的 TimerTask,阻止其他任务运行。一个线程池中包含多个线程可以 防止其他任务被阻止,并仍然保持线程的计划。

也可以使用其他集合类型。作为计划的线程池的备选方法,可以考虑使用 DelayQueue。它允许您向集合中添加那些在延迟时间失效之后才能提取的项目。 它是一种特定类型的 BlockingQueue:如果一个项不可用,则从队列获取该项会 受到阻止,直到延迟失效。

结束语

本文向您介绍了创建 Web crawler 的过程:

收集泛型 Set 中的 URI 字符串的集合

生成 Runnable 任务,在网站的页面上找到更多的 URI。

使用线程池来完成 Runnable 操作

要扩展该 Web crawler,可以考虑收集图像引用或搜索特定的文本字符串。 您可以改进该程序,增强其功能,并学习更多使用并发集合技术的知识。

本文配套源码


2011-07-02 11:30
阅读:
I'm VC , Just U know Y
本站部分文章来源于互联网,版权归原作者所有。

延伸阅读:

myeclipse 8下载和注册码

浅析android下如何通过jni监控wifi网络连接、dhcpcd执行和power电源控制

Java和.NET继续争斗的四大相关问题

大智慧超赢手机版(JAVA)详细使用说明

使用maven构建struts2在jdk1.4.2上的开发环境