这是源码解析系列的第二篇,依然来学习java.lang包下常用的类–8种基本数据类型对应的包装类。将从概念到源码进行学习,并对其缓存、自动拆装箱的语法糖等注意点进行分析。
在开始之前,今天看到一个有趣问题:Java到底是纯面向对象语言吗?有人认为是,有人认为不是。认为不是的因为:(1)类的static修饰的静态内容(变量和方法)不属于任何对象,所以这是非对象的东西;(2)所有的8种基本数据类型都不是对象,因为无法做类似正常对象所具有的操作(如:用“.”来访问对象和方法)。但是JVM角度分析来分析了:Java到底是不是一种纯面向对象语言?。
我们可以思考一下JDK为什么要给每个基本数据类型都提供一个对应的包装类?我们可以理解成包装类将java的所有东西都抽象成对象,可以更方便的控制和使用。包装类的存在,方便进行相关操作以及类型的相关属性(如最大最小值)。
基本数据类型与包装类的描述
Java 中的基本数据类型有8种,分别是布尔类型boolean,字符类型char,数值类型:整数类型byte,short,int,long;浮点数类型float,double。
- boolean类型:基本数据类型中唯一没有给出具体占用字节数的,因为对于虚拟机来说,不存在boolean类型,boolean类型编译后是由其他类型表示的。boolean类型没有给出精确的定义,《Java虚拟机规范》给出了4个字节,和boolean数组1个字节的定义,具体还要看虚拟机实现是否按照规范来,所以1个字节、4个字节都是有可能的。boolean类型的值只有:true和false,且不能像c++那样用0和非0来替代。其对应的包装类为:Boolean。
- char:char 在Java中是2个字节。Java采用unicode编码,2个字节(16位)来表示一个字符,说明它也可以表示汉字。实际存储的是字符的码值,所以可以进行相关算术运算。其对应得包装类为:Character。
数值类型都是采用补码来表示的:正数:补码=原码,负数:补码=反码+1。注意正负零的概念及处理 - byte:1字节,8位,表示范围:-128~127。包装类–Byte
- short :2字节,16位,无符号位时最大存储65536,范围:-32768~32767。包装类–Short
- int:4字节, 32位,无符号位时最大存储2的32次方减1,表示范围:-2147483648(0x8000 0000)-2147483647(0x7fff ffff)。包装类–Integer。直接赋值时默认的类型。
- long:8字节,64位,无符号位时最大存储2的64次方减1,范围:负的2的63次方到正的2的63次方减1。直接赋值时需要在后边加L,例如:21455444454L,否则会认为是int型。包装类–Long。
- float:4字节, 32位,数据范围在3.4e-45~1.4e38,直接赋值时必须在数字后加上f或F,否则认为是默认类型double。包装类–Float。有效位6~7位
- double:8字节,64位,数据范围在4.9e-324~1.8e308,赋值时可以加d或D也可以不加。包装类–Double。有效位:15~16位。关于float和double的精度计算方法,可参看:float与double的范围和精度。
包装类源码解析
8种包装类,重点分析最常用的Integer类,其他类的实现大同小异。首先我们明确:这8中包装类都实现了Comparable接口和Serializable接口,表示表示可以比较大小以及进行序列化。对于数值类型的包装类:我们可以看一下类图:
从上图中可以看到,数值类型的6种包装类都继承了Number类。重点分析的是Integer类,先看一下Integer类的相关属性:
Integer类有int属性,其实就是用对象封装了这个属性而已。另外定义了最大最小值,长度等属性。
构造方法有连个就是:
public Integer(int value) {
this.value = value;
}
public Integer(String s) throws NumberFormatException {
this.value = parseInt(s, 10);
}
parseInt(s, 10);方法涉及字符串的转化为10进制整数方法,这是一个非常重要的方法,我们下边来看其源码:
//默认按10进制
public static int parseInt(String s) throws NumberFormatException {
return parseInt(s,10);
}
/**
* Parses the string argument as a signed integer in the radix
* specified by the second argument. The characters in the string
* must all be digits of the specified radix (as determined by
* whether {@link java.lang.Character#digit(char, int)} returns a
* nonnegative value), except that the first character may be an
* ASCII minus sign {@code '-'} ({@code '\u005Cu002D'}) to
* indicate a negative value or an ASCII plus sign {@code '+'}
* ({@code '\u005Cu002B'}) to indicate a positive value. The
* resulting integer value is returned.
*
* <p>An exception of type {@code NumberFormatException} is
* thrown if any of the following situations occurs:
* <ul>
* <li>The first argument is {@code null} or is a string of
* length zero.
*
* <li>The radix is either smaller than
* {@link java.lang.Character#MIN_RADIX} or
* larger than {@link java.lang.Character#MAX_RADIX}.
*
* <li>Any character of the string is not a digit of the specified
* radix, except that the first character may be a minus sign
* {@code '-'} ({@code '\u005Cu002D'}) or plus sign
* {@code '+'} ({@code '\u005Cu002B'}) provided that the
* string is longer than length 1.
*
* <li>The value represented by the string is not a value of type
* {@code int}.
* </ul>
*
* <p>Examples:
* <blockquote><pre>
* parseInt("0", 10) returns 0
* parseInt("473", 10) returns 473
* parseInt("+42", 10) returns 42
* parseInt("-0", 10) returns 0
* parseInt("-FF", 16) returns -255
* parseInt("1100110", 2) returns 102
* parseInt("2147483647", 10) returns 2147483647
* parseInt("-2147483648", 10) returns -2147483648
* parseInt("2147483648", 10) throws a NumberFormatException
* parseInt("99", 8) throws a NumberFormatException
* parseInt("Kona", 10) throws a NumberFormatException
* parseInt("Kona", 27) returns 411787
* </pre></blockquote>
*
* @param s the {@code String} containing the integer
* representation to be parsed
* @param radix the radix to be used while parsing {@code s}.
* @return the integer represented by the string argument in the
* specified radix.
* @exception NumberFormatException if the {@code String}
* does not contain a parsable {@code int}.
*/
public static int parseInt(String s, int radix)
throws NumberFormatException
{
/*
* WARNING: This method may be invoked early during VM initialization
* before IntegerCache is initialized. Care must be taken to not use
* the valueOf method.
*/
if (s == null) {
throw new NumberFormatException("null");
}
if (radix < Character.MIN_RADIX) {
throw new NumberFormatException("radix " + radix +
" less than Character.MIN_RADIX");
}
if (radix > Character.MAX_RADIX) {
throw new NumberFormatException("radix " + radix +
" greater than Character.MAX_RADIX");
}
int result = 0;
boolean negative = false;
int i = 0, len = s.length();
int limit = -Integer.MAX_VALUE;
int multmin;
int digit;
if (len > 0) {
char firstChar = s.charAt(0);
if (firstChar < '0') { // Possible leading "+" or "-"
if (firstChar == '-') {
negative = true;
limit = Integer.MIN_VALUE;
} else if (firstChar != '+')
throw NumberFormatException.forInputString(s);
if (len == 1) // Cannot have lone "+" or "-"
throw NumberFormatException.forInputString(s);
i++;
}
multmin = limit / radix;
while (i < len) {
// Accumulating negatively avoids surprises near MAX_VALUE
digit = Character.digit(s.charAt(i++),radix);
if (digit < 0) {
throw NumberFormatException.forInputString(s);
}
if (result < multmin) {
throw NumberFormatException.forInputString(s);
}
result *= radix;
if (result < limit + digit) {
throw NumberFormatException.forInputString(s);
}
result -= digit;
}
} else {
throw NumberFormatException.forInputString(s);
}
return negative ? result : -result;
}
上述方法就是字符串转为任意进制int的算法。且注释还给出了很多例子,看一下即可。
toString( )方法,可用来将int类型转化为String类型。
public static String toString(int i) {
if (i == Integer.MIN_VALUE)
return "-2147483648";
int size = (i < 0) ? stringSize(-i) + 1 : stringSize(i);
char[] buf = new char[size];
getChars(i, size, buf);
return new String(buf, true);
}
public static String toString(int i, int radix) {
if (radix < Character.MIN_RADIX || radix > Character.MAX_RADIX)
radix = 10;
/* Use the faster version */
if (radix == 10) {
return toString(i);
}
char buf[] = new char[33];
boolean negative = (i < 0);
int charPos = 32;
if (!negative) {
i = -i;
}
while (i <= -radix) {
buf[charPos--] = digits[-(i % radix)];
i = i / radix;
}
buf[charPos] = digits[-i];
if (negative) {
buf[--charPos] = '-';
}
return new String(buf, charPos, (33 - charPos));
}
另外还要一些方法:
其中valueOf方法很重要,我们将在下一小节结合自动拆装箱和缓存重点讲。图中下边继承了Number类的几个方法,在Integer实现了其抽象方法,就是进行类型装换。比如:
public short shortValue() {
return (short)value;
}
hashcode方法就是int数值本身:
public static int hashCode(int value) {
return value;
}
其他还有一些方法,我用的不是太多,需要时可以直接查看api使用即可。
其他包装类与Integer包装类的实现大同小异,大家可以自己查看源码。
缓存及自动拆装箱讨论
缓存机制
这是 Java 5 中引入的一个有助于节省内存、提高性能的特性,整型对象在内部实现中通过使用相同的对象引用实现了缓存和重用。其实现是定义了缓存内部类:
/**
* Cache to support the object identity semantics of autoboxing for values between
* -128 and 127 (inclusive) as required by JLS.
*
* The cache is initialized on first usage. The size of the cache
* may be controlled by the {@code -XX:AutoBoxCacheMax=<size>} option.
* During VM initialization, java.lang.Integer.IntegerCache.high property
* may be set and saved in the private system properties in the
* sun.misc.VM class.
*/
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() {}
}
当此静态内部类被使用时,缓存就就被加载到JVM(jdk1.8之前中的方法区的常量池)。它是怎么被调用的呢:
public static Integer valueOf(String s) throws NumberFormatException {
return Integer.valueOf(parseInt(s, 10));
}
/**
* Returns an {@code Integer} instance representing the specified
* {@code int} value. If a new {@code Integer} instance is not
* required, this method should generally be used in preference to
* the constructor {@link #Integer(int)}, as this method is likely
* to yield significantly better space and time performance by
* caching frequently requested values.
*
* This method will always cache values in the range -128 to 127,
* inclusive, and may cache other values outside of this range.
*
* @param i an {@code int} value.
* @return an {@code Integer} instance representing {@code i}.
* @since 1.5
*/
public static Integer valueOf(int i) {
if (i >= IntegerCache.low && i <= IntegerCache.high)
return IntegerCache.cache[i + (-IntegerCache.low)];
return new Integer(i);
}
它是通过valueOf( )来调用的,这说明我们通过构造方法来创建Integer对象时不经过缓存的。
缓存机制+自动拆装箱配合
我们可以看下边的代码:
public class Tets {
public static void main(String[] args) {
Integer i1 = new Integer(100);
Integer i2 = new Integer(100);
System.out.println(i1 == i2); //false
Integer i3 = 100;
Integer i4 = 100;
Integer i5 = Integer.valueOf(100);
System.out.println(i3 == i1); //false
System.out.println(i3 == i4); //true
System.out.println(i3 == i5); //true
Integer i6 = 200;
Integer i7 = 200;
System.out.println(i6 == i7); //false
}
}
从上边的程序中,再结合上边的缓存讨论,我们不难猜到,Integer i3 = 100;的自动装箱操作,肯定是调用了valueOf() 方法,从而调用了缓存。我们可以反编译一下class文件,看自动装箱编译后的结果:
public class Tets {
public Tets() {
}
public static void main(String[] args) {
Integer i1 = new Integer(100);
Integer i2 = new Integer(100);
System.out.println(i1 == i2);
Integer i3 = Integer.valueOf(100);
Integer i4 = Integer.valueOf(100);
Integer i5 = Integer.valueOf(100);
System.out.println(i3 == i1);
System.out.println(i3 == i4);
System.out.println(i3 == i5);
Integer i6 = Integer.valueOf(200);
Integer i7 = Integer.valueOf(200);
System.out.println(i6 == i7);
}
}
果然和 我们猜想的一样,自动装箱语法糖在编译后,帮我们调用了valueOf方法来创建对象,在缓存范围内,所以指向的地址一样,都是同一个对象。
jdk1.5 加入的针对基本数据类型和包装类的自动拆装箱的语法糖编译器帮我们做的工作是:
自动装箱:是编译器帮我们调用了包装类的valueOf( )方法;
自动拆箱:是编译器帮我们调用了包装类的intValue( )方法(继承于Number类)。
public int intValue() {
return value;
}
这种缓存行为不仅适用于Integer对象。我们针对所有整数类型的类都有类似的缓存机制。
有 ByteCache 用于缓存 Byte 对象
有 ShortCache 用于缓存 Short 对象
有 LongCache 用于缓存 Long 对象
有 CharacterCache 用于缓存 Character 对象
Byte,Short,Long 有固定范围: -128 到 127。对于 Character, 范围是 0 到 127。除了 Integer 可以通过参数改变范围外,其它的都不行。
只有Double,Float和Boolean包装类没有提供缓存,它们的valueOf方法就是直接创建包装类对象。
一般推荐用自动拆装箱的方式来创建包装类。
BigDecimal、BigInteger
在刚开始看Number继承图时,其子类还有 BigDecimal、BigInteger这两个类,专门用于进行高精度运算BigInteger 和 BigDecimal 。它们在java.math包下。拿BigInteger来说,其是不可变的任意精度的整数。所有操作中,都以二进制补码形式表示 BigInteger(如 Java 的基本整数类型)。BigInteger 提供所有 Java 的基本整数操作符的对应物,并提供 java.lang.Math 的所有相关方法。另外,BigInteger 还提供以下运算:模算术、GCD 计算、质数测试、素数生成、位操作以及一些其他操作。可参考此文章:Java 7 源码学习系列(三)——BigInteger 看一下其原理。在使用时,查看api即可。这里不再详细介绍了。
最后一个小点:关于类型转换:自动类型转换:
强制类型转换-视图把表数范围大的类型转换为表数范围小的类型时,容器引起信息丢失。另:字符串不能直接转换为基本类型,但可通过基本类型对应的包装类实现转换成基本类型。
自动提升规则:当一个算术表达式中包含多个基本类型的值时,所有的byte类、short和char类型会被提示到int类型。
至此,第二篇关于包装类的源码解析就到此结束。