Equality, the URI class, and form encoding
◈ 4 cardsWhy URL.equals blocks on DNS and must never key a hash structure, what the pure-syntax URI class gives you instead, and how to build an x-www-form-urlencoded query one component at a time.
equals() on a URL goes to the network
Two URL objects are equal when they point at the same resource on the same host, port and path, with the same query and fragment. The surprise is in same host: to decide it, URL.equals() resolves both hostnames through DNS, so http://ibiblio.org/ and http://www.ibiblio.org/ compare equal when they resolve to the same address.
That makes equals() — and therefore hashCode(), which must agree with it — a blocking network operation. A HashSet<URL> built from the links on one page can stall for as long as the resolver takes, behave differently on a different network, and hang entirely behind a broken DNS server. Never put a URL in a HashMap, HashSet, or anything else that hashes. Store URI and convert on the way out.
sameFile() is the same comparison, DNS and all, minus the fragment: …/page.html#a and …/page.html#b are sameFile but not equals. And URL does not implement Comparable, so you cannot sort a list of them without a comparator.
URI: pure syntax
java.net.URI is built from strings and throws the checked URISyntaxException when they break the grammar. It needs no protocol handler, so urn:, tel: and schemes invented tomorrow all parse. In exchange it does no I/O at all.
Its comparison behaviour is everything URL's is not. URI.equals() is a string comparison — scheme and authority case-insensitive, the rest case-sensitive — with no lookup, and escapes are not decoded first, so …/A and …/%41 are unequal. URI implements Comparable, so URIs sort. Three methods do the syntax work: resolve(String) merges a relative reference against this URI, relativize(URI) reverses that, and normalize() removes . and .. segments. Cross over with toURI() and toURL() at the boundary.
x-www-form-urlencoded
A query component may carry only a restricted ASCII set, so form data is encoded before it goes into the URL. URLEncoder.encode leaves letters, digits and the four characters . - * _ alone, turns a space into +, and converts everything else — including every non-ASCII character — into percent escapes of its UTF-8 bytes. UTF-8 is the only charset you should ever pass.
The modern signature takes a Charset and throws nothing:
URLEncoder.encode(s, StandardCharsets.UTF_8);
The older encode(String, String) throws UnsupportedEncodingException for a charset name the VM does not know, and the one-argument encode(String) is deprecated because it silently uses the platform default — the classic bug that works on your laptop and mangles data on the marker's machine.
The critical property: the encoder is blind. It cannot tell a slash that is structure from a slash that is data, so it encodes both. Encode one component at a time and assemble the URL yourself.
Worked example — building a query string
You want to search the course site for the phrase I/O & streams, filtered to topic=urls. Encoding each value separately gives:
topic -> urls
q -> I%2FO+%26+streams
http://data.example.org:8080/comp348/search?topic=urls&q=I%2FO+%26+streams
The slash became %2F and the ampersand %26 inside the value, while the ?, the = and the & that separate the pairs stayed literal — because you wrote those, not the encoder. Hand the whole URL to URLEncoder.encode instead and you get http%3A%2F%2Fdata.example.org…: a string that is no longer a URL at all, only a very long, very encoded value.
source JDK javadoc java.net.URLEncoder