资讯专栏INFORMATION COLUMN

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

VioletJack / 848人阅读

摘要:作为条件变量的的不仅可以认为内嵌了一把锁,还内嵌了一个条件变量。操作条件变量的函数将当前线程在条件变量上阻塞,一般是为了等待其他线程的某件事情执行完成。其它装箱类其它装箱类的代码这里就不分析了。重点关注下各装箱类的缓存范围。

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值。

装箱拆箱

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

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);
}

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

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

可发现:

默认缓存的值是-128127

缓存的范围可以通过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的缓存范围能够修改,其它的装箱类型都不行。

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/77093.html

相关文章

  • 深入浅出了解“装箱与拆箱”

    摘要:本章部分内容从源码中解读一些自动装箱与拆箱的原理,以及会出现的一些陷阱已经性能等。例题分析我们通过几个经典的问题,来看看大家到底理解了装箱与拆箱的知识点没。 showImg(https://img-blog.csdnimg.cn/20190426221838971.gif);showImg(https://img-blog.csdnimg.cn/20190426221918208.pn...

    FullStackDeveloper 评论0 收藏0
  • 源码|jdk源码栈、队列ArrayDeque分析

    摘要:栈队列双端队列都是非常经典的数据结构。结合了栈和队列的特点。因此,在中,有栈的使用需求时,使用代替。迭代器之前源码源码之与字段中分析过,容器的实现中,所有修改过容器结构的操作都需要修改字段。 栈、队列、双端队列都是非常经典的数据结构。和链表、数组不同,这三种数据结构的抽象层次更高。它只描述了数据结构有哪些行为,而并不关心数据结构内部用何种思路、方式去组织。本篇博文重点关注这三种数据结构...

    ZHAO_ 评论0 收藏0
  • Java学习路线总结,搬砖工逆袭Java架构师(全网最强)

    摘要:哪吒社区技能树打卡打卡贴函数式接口简介领域优质创作者哪吒公众号作者架构师奋斗者扫描主页左侧二维码,加入群聊,一起学习一起进步欢迎点赞收藏留言前情提要无意间听到领导们的谈话,现在公司的现状是码农太多,但能独立带队的人太少,简而言之,不缺干 ? 哪吒社区Java技能树打卡 【打卡贴 day2...

    Scorpion 评论0 收藏0
  • Java快速扫盲指南

    摘要:不相等的对象要具有不相等的哈希码为了哈希表的操作效率,这一点很重要,但不是强制要求,最低要求是不相等的对象不能共用一个哈希码。方法和方法协同工作,返回对象的哈希码。这个哈希码基于对象的身份生成,而不是对象的相等性。 本文面向 刚学完Java的新手们。这篇文章不讲语法,而是一些除了语法必须了解的概念。 将要去面试的初级工程师们。查漏补缺,以免遭遇不测。 目前由于篇幅而被挪出本文的知识...

    Tony_Zby 评论0 收藏0
  • java.lang.Integer 源码深入解读

    摘要:最近算是比较深入的了解了一下的源码,就想着写点东西记录一下,一来可以加深理解,再来也算是为我刷了那么久平台贡献一点自己的绵薄之力。这两个方法都是给当前的实例的属性赋值,参数为类型的构造器直接将参数赋值给属性,参数为是将方法的返回值赋值。 最近算是比较深入的了解了一下Integer的源码,就想着写点东西记录一下,一来可以加深理解,再来也算是为我刷了那么久segmentfault平台贡献一...

    mingzhong 评论0 收藏0

发表评论

0条评论

VioletJack

|高级讲师

TA的文章

阅读更多
最新活动
阅读需要支付1元查看
<