jdk源码读到现在这里,重要的集合类也读了一部分了。
集合类再往下读的话,就要涉及到两个方向。
第一,是比较典型的但是不常用的数据结构,这部分我准备将数据结构复习、回顾后再继续阅读。
第二,是并发相关的集合,这部分我准备留到和并发相关的类一起阅读。
所以,今天就读些轻松的。
Object
作为单根继承的Object
java的对象系统设计是采用单根继承,所有的对象往上追溯,Object都是它们共同的祖先。
有了这个假设,我忽然想起java中一个有趣的事实:
List list = new ArrayList();
list.toString();
这段代码能正常编译、运行吗?经验告诉我,当然可以。
可是从类型系统的角度仔细思考,list引用的类型为List
,其为List
接口。
然而,List
接口中并没有toString
方法,为什么能调用?
这是由于,在java中,会让接口类型也拥有Object的所有方法。一个接口对象,也是一个Object对象。因为单根继承这一总体设计,所以这样设计接口是合理的。
这里有关于该问题的有趣讨论,所以这里就不详细展开了。
作为锁的Object
在java中,除了最基本的单根继承的祖先类之外,Object还内置了很多机制。如:
Object o = new Object();
synchronized(o) {
/* ... */
}
在其它语言中,锁这一机制都是标准库中提供的函数,成对使用。一个lock函数
用于获取锁,一个release函数
函数用于释放锁。
然而,java直接将锁机制作为语法的一部分,还给它一个专属关键字synchronized
。每个Object对象,都内嵌了一个锁。java称之监视器锁。
这样设计有什么好处呢?一种观点是,将锁机制内置为语法的一部分,有利于jvm对其进行深度优化提升性能,如java的锁升级机制。
作为条件变量的Object
java的Object不仅可以认为内嵌了一把锁,还内嵌了一个条件变量。操作条件变量的函数:
public final native void notify();
public final native void notifyAll();
public final native void wait(long timeout) throws InterruptedException;
wait
将当前线程在条件变量上阻塞,一般是为了等待其他线程的某件事情执行完成。当其他线程的事情执行完成后,在条件变量上调用notify
或notifyAll
来唤醒阻塞的线程。
可以看到,这三个方法都是native
,jvm原生实现。
wait
还有两个重载形式:
public final void wait() throws InterruptedException {
wait(0);
}
public final void wait(long timeout, int nanos) throws InterruptedException {
if (timeout < 0) {
throw new IllegalArgumentException("timeout value is negative");
}
if (nanos < 0 || nanos > 999999) {
throw new IllegalArgumentException(
"nanosecond timeout value out of range");
}
if (nanos > 0) {
timeout++;
}
wait(timeout);
}
比较有意思的是第二个。
原生实现的wait(long timeout)
,只能设置毫秒级别的超时时间。但是这个wait(long timeout, int nanos)
却能设置纳秒级别的超时时间。怎么实现的?
if (nanos > 0) {
timeout++;
}
笑哭了。。。。难道是我下载的jdk平台不对?
hashCode、equals、toString
Object类提供了这三个函数的默认实现。来看一下:
public native int hashCode();
public String toString() {
return getClass().getName() + "@" + Integer.toHexString(hashCode());
}
public boolean equals(Object obj) {
return (this == obj);
}
可以看到,hashCode的默认方法是原生实现,到底是不是指针不清楚。
equals方法的默认实现仅仅简单比较了是否为同一引用。
toString()
方法打印出的是类名及十六进制的hash值。
装箱拆箱
装箱拆箱机制的存在的原因是:
- java中的泛型是类型擦除,类似集合等泛型类中实际存放的必须是Object的子类,也即引用类型。
- java的8种基本类型都是值类型,不是对象。因此无法直接放入泛型类对象中。
为了解决这个冲突,只好设计一组对象,中间包裹基本类型,并且语法层次内建装箱类与基本类型的自动转换机制,也即自动装箱拆箱。
下面以Integer为例分析装箱拆箱类的源码。
Integer
大致看一下Integer中的组成。可以发现有三个不同的部分:
- Integer类本身作为装箱容器。
- Integer类的static属性定义了大量和int有关的常量。
- Integer类的static方法定义了和int有关的工具函数。
属性和构造函数
先来看属性。
private final int value;
public Integer(int value) {
this.value = value;
}
对的,Integer对象中,只有包含这么一个数据,被装箱的原始值。
简单到不能再简单。
工厂方法和缓存
我们知道,一般来说,在java中,使用工厂方法代替构造函数是更好的设计。在Integer里,就体现了它的好处之一。
Integer提供了一组静态工厂方法:
public static Integer valueOf(String s) throws NumberFormatException {
return Integer.valueOf(parseInt(s, 10));
}
public static Integer valueOf(String s, int radix) throws NumberFormatException {
return Integer.valueOf(parseInt(s,radix));
}
public static Integer valueOf(int i) {
if (i >= IntegerCache.low && i <= IntegerCache.high)
return IntegerCache.cache[i + (-IntegerCache.low)];
return new Integer(i);
}
前两个工厂方法都利用最后一个工厂方法实现。最重要的是最后一个。
非常明显,当被装箱的原始类型i
在IntegerCache.low
和IntegerCache.high
之间时,则返回缓存的Integer对象。
来看IntegerCache
:
private static class IntegerCache {
static final int low = -128;
static final int high;
static final Integer cache[];
static {
// high value may be configured by property
int h = 127;
String integerCacheHighPropValue =
sun.misc.VM.getSavedProperty("java.lang.Integer.IntegerCache.high");
if (integerCacheHighPropValue != null) {
try {
int i = parseInt(integerCacheHighPropValue);
i = Math.max(i, 127);
// Maximum array size is Integer.MAX_VALUE
h = Math.min(i, Integer.MAX_VALUE - (-low) -1);
} catch( NumberFormatException nfe) {
// If the property cannot be parsed into an int, ignore it.
}
}
high = h;
cache = new Integer[(high - low) + 1];
int j = low;
for(int k = 0; k < cache.length; k++)
cache[k] = new Integer(j++);
// range [-128, 127] must be interned (JLS7 5.1.7)
assert IntegerCache.high >= 127;
}
private IntegerCache() {}
}
可发现:
- 默认缓存的值是
-128
到127
。 - 缓存的范围可以通过
java.lang.Integer.IntegerCache.high
来设置。这样,如果在某些场景下Integer影响性能,可以通过jvm手动修改该参数空间换时间。
总结一下,由于Integer是对象,而对整数的操作是代码里非常频繁的地方。装箱机制会导致程序产生大量的Integer对象,这导致:
- 对象会占据额外空间(如对象头),造成内存浪费。
- 频繁创建销毁对象,给gc造成压力。
因此,采用缓存机制,尽量降低装箱对性能的影响。
其它装箱类
其它装箱类的代码这里就不分析了。重点关注下各装箱类的缓存范围。
首先,Boolean,只有两个值,当然可以都缓存。
浮点类型,Double和Float,没有缓存:
public static Float valueOf(float f) {
return new Float(f);
}
public static Double valueOf(double d) {
return new Double(d);
}
Short,缓存范围为-128到127,和默认的Integer一样。最重要的是,这个范围无法修改。
public static Short valueOf(short s) {
final int offset = 128;
int sAsInt = s;
if (sAsInt >= -128 && sAsInt <= 127) { // must cache
return ShortCache.cache[sAsInt + offset];
}
return new Short(s);
}
private static class ShortCache {
private ShortCache(){}
static final Short cache[] = new Short[-(-128) + 127 + 1];
static {
for(int i = 0; i < cache.length; i++)
cache[i] = new Short((short)(i - 128));
}
}
同样:
- Byte缓存范围也是-128到127,全部缓存。
- 而Character缓存范围为0到127.
- Long的缓存范围为-128到127。
可以发现,只有Integer的缓存范围能够修改,其它的装箱类型都不行。