源码|jdk源码之Object及装箱类型分析

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将当前线程在条件变量上阻塞,一般是为了等待其他线程的某件事情执行完成。当其他线程的事情执行完成后,在条件变量上调用notifynotifyAll来唤醒阻塞的线程。

可以看到,这三个方法都是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值。

装箱拆箱

装箱拆箱机制的存在的原因是:

  1. java中的泛型是类型擦除,类似集合等泛型类中实际存放的必须是Object的子类,也即引用类型。
  2. java的8种基本类型都是值类型,不是对象。因此无法直接放入泛型类对象中。

为了解决这个冲突,只好设计一组对象,中间包裹基本类型,并且语法层次内建装箱类与基本类型的自动转换机制,也即自动装箱拆箱。

下面以Integer为例分析装箱拆箱类的源码。

Integer

大致看一下Integer中的组成。可以发现有三个不同的部分:

  1. Integer类本身作为装箱容器。
  2. Integer类的static属性定义了大量和int有关的常量。
  3. 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);
}

前两个工厂方法都利用最后一个工厂方法实现。最重要的是最后一个。

非常明显,当被装箱的原始类型iIntegerCache.lowIntegerCache.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() {}
    }

可发现:

  1. 默认缓存的值是-128127
  2. 缓存的范围可以通过java.lang.Integer.IntegerCache.high来设置。这样,如果在某些场景下Integer影响性能,可以通过jvm手动修改该参数空间换时间。

总结一下,由于Integer是对象,而对整数的操作是代码里非常频繁的地方。装箱机制会导致程序产生大量的Integer对象,这导致:

  1. 对象会占据额外空间(如对象头),造成内存浪费。
  2. 频繁创建销毁对象,给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));
        }
    }

同样:

  1. Byte缓存范围也是-128到127,全部缓存。
  2. 而Character缓存范围为0到127.
  3. Long的缓存范围为-128到127。

可以发现,只有Integer的缓存范围能够修改,其它的装箱类型都不行。

你可能感兴趣的:(jdk8,java)