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