1. 概述

本文将深入探讨 URI 和 URL 的核心差异,并通过 Java 代码示例直观展示这些区别。对于有经验的开发者来说,这些概念虽然基础,但在实际开发中踩坑的情况并不少见。

2. URI 和 URL

先明确定义:

  • **URI (Uniform Resource Identifier)**:用于唯一标识任何抽象或物理资源的字符序列
  • **URL (Uniform Resource Locator)**:URI 的子集,除了标识资源位置外,还描述了访问该资源的主要机制

关键结论:所有 URL 都是 URI,但并非所有 URI 都是 URL

2.1. 语法结构

所有 URI(无论是否为 URL)都遵循统一格式:

scheme:[//authority][/path][?query][#fragment]

各组成部分解析: | 部分 | 说明 | |------|------| | scheme | URL 中指协议(如 http),其他 URI 中是标识符规范 | | authority | 可选部分,包含认证信息、主机和端口 | | path | 在 scheme 和 authority 范围内标识资源 | | query | 附加数据,与 path 共同标识资源(URL 中即查询字符串) | | fragment | 资源特定部分的可选标识符 |

⚠️ 快速判断技巧:检查 scheme 开头

  • 有效的 URL scheme:ftp, http, https, gopher, mailto, news, nntp, telnet, wais, file, prospero
  • 不符合上述规则的就不是 URL

示例对比(前三个是 URL,后三个不是):

ftp://ftp.is.co.za/rfc/rfc1808.txt
https://tools.ietf.org/html/rfc3986
mailto:john.doe@example.com

tel:+1-816-555-1212
urn:oasis:names:docbook:dtd:xml:4.1
urn:isbn:1234567890

3. Java 中 URI 和 URL 的 API 差异

3.1. 实例化方式

创建 URI 和 URL 实例的代码对比:

@Test
public void whenCreatingURIs_thenSameInfo() throws Exception {
    URI firstURI = new URI(
      "somescheme://theuser:thepassword@someauthority:80"
      + "/some/path?thequery#somefragment");
    
    URI secondURI = new URI(
      "somescheme", "theuser:thepassword", "someuthority", 80,
      "/some/path", "thequery", "somefragment");

    assertEquals(firstURI.getScheme(), secondURI.getScheme());
    assertEquals(firstURI.getPath(), secondURI.getPath());
}

@Test
public void whenCreatingURLs_thenSameInfo() throws Exception {
    URL firstURL = new URL(
      "http://theuser:thepassword@somehost:80"
      + "/path/to/file?thequery#somefragment");
    URL secondURL = new URL("http", "somehost", 80, "/path/to/file");

    assertEquals(firstURL.getHost(), secondURL.getHost());
    assertEquals(firstURL.getPath(), secondURL.getPath());
}

关键差异

  • URI 类提供便捷创建方法:
    @Test
    public void whenCreatingURI_thenCorrect() {
        URI uri = URI.create("urn:isbn:1234567890");
        assertNotNull(uri);
    }
    
  • URL 类没有类似方法

⚠️ 踩坑警告:尝试用无效 scheme 创建 URL 会抛异常:

@Test(expected = MalformedURLException.class)
public void whenCreatingURLs_thenException() throws Exception {
    URL theURL = new URL("otherprotocol://somehost/path/to/file");
    assertNotNull(theURL);
}

3.2. 实例转换

转换操作代码示例:

@Test
public void givenObjects_whenConverting_thenCorrect()
  throws MalformedURLException, URISyntaxException {
    String aURIString = "http://somehost:80/path?thequery";
    URI uri = new URI(aURIString);
    URL url = new URL(aURIString);

    URL toURL = uri.toURL();
    URI toURI = url.toURI();

    assertNotNull(url);
    assertNotNull(uri);
    assertEquals(toURL.toString(), toURI.toString());
}

转换限制:非 URL 的 URI 无法转换为 URL:

@Test(expected = MalformedURLException.class)
public void givenURI_whenConvertingToURL_thenException()
  throws MalformedURLException, URISyntaxException {
    URI uri = new URI("somescheme://someauthority/path?thequery");
    URL url = uri.toURL();
    assertNotNull(url);
}

3.3. 远程连接操作

URL 类的核心优势在于可直接访问远程资源:

@Test
public void givenURL_whenGettingContents_thenCorrect()
  throws MalformedURLException, IOException {
    URL url = new URL("http://courses.baeldung.com");
    String contents = IOUtils.toString(url.openStream());
    assertTrue(contents.contains("<!DOCTYPE html>"));
}

⚠️ 性能陷阱

  • URL 的 equals()hashCode() 可能触发 DNS 解析
  • 会导致网络延迟和结果不一致
  • ❌ 不建议在虚拟主机环境使用
  • ✅ 推荐改用 URI 进行标识操作

4. 总结

本文通过代码示例直观展示了 URI 和 URL 的核心差异:

  1. 本质区别:URL 是 URI 的子集,包含访问机制
  2. API 差异
    • URI 提供更灵活的创建方式
    • URL 支持直接网络连接
  3. 最佳实践
    • 使用 URI 进行资源标识
    • 仅在需要网络访问时使用 URL
    • 避免依赖 URL 的 equals()/hashCode()

完整源码可在 GitHub 获取。


原始标题:Difference Between URL and URI