Java 魔法Part 1:java.net.URL
原文 http://mishadoff.com/blog/java-magic-part-1-java-dot-net-dot-url/
序
这篇文章比较老了,也有很多人翻译,但是好像都是google翻译的,没有体现出作者的风趣。感觉作者还是很皮的,他的这系列文章都很有意思。
最近, 我在reddit上发现了一个非常有趣的Java代码片段(有一点修改)
HashSet set = new HashSet();
set.add(new URL("http://google.com"));
set.contains(new URL("http://google.com"));
Thread.sleep(60000);
set.contains(new URL("http://google.com"));
你认为第三代码和第五行代码的结果会是什么?
既然问了这个问题,那很明显不是true, true。先思考两分钟。
好了,在大多数时候结果是true, false 这是因为你连接了互联网(否则你怎么能看到这篇文章呢?)关闭你的网络连接或者WiFi你将会得到true, true。
问题的原因在于该类方法hashCode() 和 equals()的实现逻辑。
让我们看下它是如何计算hashCode的:
public synchronized int hashCode() {
if (hashCode != -1)
return hashCode;
hashCode = handler.hashCode(this);
return hashCode;
}
我们可以看到hashCode是一个实例变量并且只计算一次。这是有意义的,因为URL是不可变的。handler是什么?它是URLStreamHandler子类的实例,具体依赖于不同的协议类型(file,http,ftp)。看下java.net.URL的Java文档说明:
The hash code is based upon all the URL components relevant for URL comparison. As such, this operation is a blocking operation.
等一下! 阻塞式操作?!
对不起我昨天没有收新邮件,因为hashCode计算阻塞了
或者更好的例子:
不是的,妈妈,我不能看X片。你知道的,在做hashCode计算呢(这个实在是太皮了)
好的就当他是阻塞操作吧。另一个奇葩的地方,当计算hashCode的时候这个handler竟然会解析ip地址。更准确的说法是会尝试去解析ip地址,如果无法解析ip地址的话,会根据host地址去计算hashCode。我们拿google.com举个例子。当host的ip是动态的时候,或者说有一个域名解析的负载均衡的时候,不好的事情就发生了。在这种情况下同一个域名会得到不同的hashCode值,如果用在HashSet就会有两个(或者多个)实例在集合列表里。这一点也不好。顺便说一下,hashCode 和 equals 的性能也是很不好的,因为URLStreamHandler会开启一个URLConnection,不过这是另外一个话题了。
附Java里URLStreamHandler代码实现
protected int hashCode(URL u) {
int h = 0;
// Generate the protocol part.
String protocol = u.getProtocol();
if (protocol != null)
h += protocol.hashCode();
// Generate the host part.
InetAddress addr = getHostAddress(u);
if (addr != null) {
h += addr.hashCode();
} else {
String host = u.getHost();
if (host != null)
h += host.toLowerCase().hashCode();
}
// Generate the file part.
String file = u.getFile();
if (file != null)
h += file.hashCode();
// Generate the port part.
if (u.getPort() == -1)
h += getDefaultPort();
else
h += u.getPort();
// Generate the ref part.
String ref = u.getRef();
if (ref != null)
h += ref.hashCode();
return h;
}
好消息是Android源码里对此作了更改
protected int hashCode(URL u) {
// Android-changed: Avoid network I/O
// Hash on the same set of fields that we compare in equals().
return Objects.hash(
u.getRef(),
u.getQuery(),
u.getProtocol(),
u.getFile(),
u.getHost(),
u.getPort());
}