源码解读:包装类型

源码解读:包装类型

因为java是面向对象的,很多时候我们需要用到的是对象而非基本数据类型。因此我们就在每个基本数据类型上都建了一个包装类型,他们具有对象的性质,并添加有属性和方法。这里我们直接开始对包装类源码中的一些独特之处做出解析。

在解读包装类的源码之前我们不妨先谈一谈java中与包装类型相关的语法糖[语法糖就是对现有语法的一个封装,通过编译器实现,可通过javap/jad 等反编译工具观察到]

反编译:执行java之前,我们首先需要将java文件编译为class字节码文件(javac xx.java),此时编译生成的字节码文件其实就是一个二进制文件,我们用文本编辑器将它打开可以观察到,里面存放了许多重要数据信息,具体可以参考相关文档了解这些二进制信息代表的含义。但问题是我们要想阅读这些二进制信息无疑是困难且繁琐的,这时候就可以通过反编译工具将字节码文件转为我们可以理解的信息。其中JDK有给我们自带的反编译工具javap,但它生成的信息更接近字节码,可以让我们更专业的了解到class文件内部的具体细节,jad则是直接生成我们熟知的java高级语法,在后文可以看到。

拆装箱

拆装箱就是一个简单常见的语法糖。我们在程序中直接对包装类以及基础数据类型操作时,在程序编译过程中编译器就会自动进行拆装箱操作。我们这里可以先通过jad工具进行反编译以更加简单直观的方式观察的拆装箱结果。

/**
* valueOf():装箱
* intValue:拆箱
*/
Integer x1 = new Integer(101);
int x2 = 101;
Integer x3 = 101;
// 反编译结果:// Method java/lang/Integer.intValue:()I;包装类自动拆箱
System.out.println(x1+x2);
// Method java/lang/Integer.intValue:()I
System.out.println(x1==x2);
// Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer;基本类型自动装箱
System.out.println(x1.equals(x2));
System.out.println(x1==x3);

jad反编译结果:

Integer x1 = new Integer(101);
int x2 = 101;
Integer x3 = Integer.valueOf(101);
System.out.println(x1.intValue() + x2);
System.out.println(x1.intValue() == x2);
System.out.println(x1.equals(Integer.valueOf(x2)));
System.out.println(x1 == x3);

观察发现java编译过程确实为我们的代码添加了不少的"额外信息"。这里其实就是所谓的自动拆装箱了。

字段分析

在看源码中具体的方法实现之前,我们先看一下里面包含的一些重要字段,先从简单的Integer开始

每个保证类中都存有一个基本数据类型值,它是实现拆箱操作的关键。

在其他包装类中都能发现类似的字段:最大最小值,类对象,对应基本数据类型值,位大小等。

    /**
     * A constant holding the minimum value an {@code int} can
     * have, -2<sup>31</sup>.
     */
    @Native public static final int   MIN_VALUE = 0x80000000;

    /**
     * A constant holding the maximum value an {@code int} can
     * have, 2<sup>31</sup>-1.
     */
    @Native public static final int   MAX_VALUE = 0x7fffffff;

    /**
     * The {@code Class} instance representing the primitive type
     * {@code int}.
     *
     * @since   JDK1.1
     */
    @SuppressWarnings("unchecked")
    public static final Class<Integer>  TYPE = (Class<Integer>) Class.getPrimitiveClass("int");

    /**
     * All possible chars for representing a number as a String
     */
    final static char[] digits = {
        '0' , '1' , '2' , '3' , '4' , '5' ,
        '6' , '7' , '8' , '9' , 'a' , 'b' ,
        'c' , 'd' , 'e' , 'f' , 'g' , 'h' ,
        'i' , 'j' , 'k' , 'l' , 'm' , 'n' ,
        'o' , 'p' , 'q' , 'r' , 's' , 't' ,
        'u' , 'v' , 'w' , 'x' , 'y' , 'z'
    };
    /**
     * The value of the {@code Integer}.
     *
     * @serial
     */
    private final int value;
    /**
     * The number of bits used to represent an {@code int} value in two's
     * complement binary form.
     *
     * @since 1.5
     */
    @Native public static final int SIZE = 32;

再比如Short类:

image-20210307125534741

包装类的缓存机制

我们先来看一下这个方法:

public static Integer valueOf(int i) {
    if (i >= IntegerCache.low && i <= IntegerCache.high)
        return IntegerCache.cache[i + (-IntegerCache.low)];
    return new Integer(i);
}

这个方法相信我们大家都不陌生,在装箱过程中使用的就是该方法。但在实际装箱之前还做了一个判断,其中的关键类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() {}
}

代码并不难懂,前面的静态代码块做了个范围的调节功能,重点观察一下代码段

sun.misc.VM.getSavedProperty("java.lang.Integer.IntegerCache.high")

它的意思我们仔细想想就能明白无非就是从"java.lang.Integer.IntegerCache.high"这个“地方”获取到要用的参数。它与我们熟悉的System.getProperties()方法类似,但要说出它们的具体区别还是得从源码角度深入分析。

首先我们要知道JVM的执行逻辑,程序启动时JVM会自动调用initializeSystemClass方法来初始化System类

java.lang.System中我们可以看到它的执行逻辑:

/**
     * Initialize the system class.  Called after thread initialization.
     */
    private static void initializeSystemClass() {

        // VM might invoke JNU_NewStringPlatform() to set those encoding
        // sensitive properties (user.home, user.name, boot.class.path, etc.)
        // during "props" initialization, in which it may need access, via
        // System.getProperty(), to the related system encoding property that
        // have been initialized (put into "props") at early stage of the
        // initialization. So make sure the "props" is available at the
        // very beginning of the initialization and all system properties to
        // be put into it directly.
        props = new Properties();
        initProperties(props);  // initialized by the VM

        // There are certain system configurations that may be controlled by
        // VM options such as the maximum amount of direct memory and
        // Integer cache size used to support the object identity semantics
        // of autoboxing.  Typically, the library will obtain these values
        // from the properties set by the VM.  If the properties are for
        // internal implementation use only, these properties should be
        // removed from the system properties.
        //
        // See java.lang.Integer.IntegerCache and the
        // sun.misc.VM.saveAndRemoveProperties method for example.
        //
        // Save a private copy of the system properties object that
        // can only be accessed by the internal implementation.  Remove
        // certain system properties that are not intended for public access.
        sun.misc.VM.saveAndRemoveProperties(props);


        lineSeparator = props.getProperty("line.separator");
        sun.misc.Version.init();

        FileInputStream fdIn = new FileInputStream(FileDescriptor.in);
        FileOutputStream fdOut = new FileOutputStream(FileDescriptor.out);
        FileOutputStream fdErr = new FileOutputStream(FileDescriptor.err);
        setIn0(new BufferedInputStream(fdIn));
        setOut0(newPrintStream(fdOut, props.getProperty("sun.stdout.encoding")));
        setErr0(newPrintStream(fdErr, props.getProperty("sun.stderr.encoding")));

        // Load the zip library now in order to keep java.util.zip.ZipFile
        // from trying to use itself to load this library later.
        loadLibrary("zip");

        // Setup Java signal handlers for HUP, TERM, and INT (where available).
        Terminator.setup();

        // Initialize any miscellenous operating system settings that need to be
        // set for the class libraries. Currently this is no-op everywhere except
        // for Windows where the process-wide error mode is set before the java.io
        // classes are used.
        sun.misc.VM.initializeOSEnvironment();

        // The main thread is not added to its thread group in the same
        // way as other threads; we must do it ourselves here.
        Thread current = Thread.currentThread();
        current.getThreadGroup().add(current);

        // register shared secrets
        setJavaLangAccess();

        // Subsystems that are invoked during initialization can invoke
        // sun.misc.VM.isBooted() in order to avoid doing things that should
        // wait until the application class loader has been set up.
        // IMPORTANT: Ensure that this remains the last initialization action!
        sun.misc.VM.booted();
    }

具体实现细节开发者已经在代码中给出了详细的解释,这里在对细节作简要说明:

initProperties(props);初始化参数,我们点进该方法可以看到它是个本地方法,本地方法实现从操作系统中获取全局属性

native 关键字作用?

首先我们知道java是跨平台的,作为跨平台的代价就是java对于底层操作系统的控制远不如c、c++这些。这个时候java必须开放一个“通道”实现java与操作系统之间的“沟通”,native便是该入口。当JVM要调用一个native方法时,JVM会从本地库中调用该方法,同时该方法通过c语言实现,c语言实现时调用JNI(java native interface)

private static native Properties initProperties(Properties props);

接下来是sun.misc.VM.saveAndRemoveProperties(props); 同样点进去看一下源码

public static void saveAndRemoveProperties(Properties var0) {
    if (booted) {
        throw new IllegalStateException("System initialization has completed");
    } else {
        savedProps.putAll(var0);
        String var1 = (String)var0.remove("sun.nio.MaxDirectMemorySize");
        if (var1 != null) {
            if (var1.equals("-1")) {
                directMemory = Runtime.getRuntime().maxMemory();
            } else {
                long var2 = Long.parseLong(var1);
                if (var2 > -1L) {
                    directMemory = var2;
                }
            }
        }

        var1 = (String)var0.remove("sun.nio.PageAlignDirectMemory");
        if ("true".equals(var1)) {
            pageAlignDirectMemory = true;
        }

        var1 = var0.getProperty("sun.lang.ClassLoader.allowArraySyntax");
        allowArraySyntax = var1 == null ? defaultAllowArraySyntax : Boolean.parseBoolean(var1);
        var0.remove("java.lang.Integer.IntegerCache.high");
        var0.remove("sun.zip.disableMemoryMapping");
        var0.remove("sun.java.launcher.diag");
        var0.remove("sun.cds.enableSharedLookupCache");
    }
}

认真看一下这串代码,它的实际实现的是不是这样一个过程:

  1. savedProps.putAll(var0);:系统参数备份到VM类中
  2. var0.remove() : 将系统类中某些指定参数移除
  3. directMemory|pageAlignDirectMemory|allowArraySyntax:对VM类中的特定属性赋值

我们想一想这样做的目的是什么?

开发者告诉我们的是:保存只能由内部实现访问的系统属性对象的私有副本,删除某些不用于公共访问的系统属性

// Save a private copy of the system properties object that
// can only be accessed by the internal implementation.  Remove
// certain system properties that are not intended for public access.
sun.misc.VM.saveAndRemoveProperties(props);

它这样设计主要还是为了安全考虑和隔离角度考虑,避免JVM的内部行为受到运行时用户代码对System.properties的修改所干扰。

所以我们在给JVM设置参数的时候往往写在运行时的 VM options 上。例如这里的java.lang.Integer.IntegerCache.high

我们可以在VM options 上写-Djava.lang.Integer.IntegerCache.high=xxx

这里可以自己试一试,看一下有没有效果:

image-20210307141759023

Integer x1 = 222;
Integer x2 = 222;
System.out.println(VM.getSavedProperty("java.lang.Integer.IntegerCache.high"));
System.out.println(System.getProperty("java.lang.Integer.IntegerCache.high"));
System.out.println(x1 == x2);
//返回结果:
//	300
//	null
//	true

initializeSystemClass 方法看到这里其实已经可以解释我们一开始的遇到的问题了,即sun.misc.VM.getSavedProperty() 方法与 System.getProperties()方法的区别。

至于后面代码块的内容与本篇内容无关,在这里不做过多解释。

我们继续回到IntegerCache类,后面的内容果然和我们一开始想的一致,它的实现就是类似于“缓存”的机制。该类在加载之初就已经自动实例化了默认范围[-128,127]的Integer对象。分别存储在cache[]数组中。

for(int k = 0; k < cache.length; k++)
	cache[k] = new Integer(j++);

这样一来我们在该范围内所获取的同一个指定值,是不是都是同一个对象了。

可是这么做又有什么好处呢?

它的作用其实就是类似于缓存的作用。我们在创建大量Integer对象时可以减少内存的分配(大量Integer对象就在指定范围内创建)。而内存分配少了,JVM gc 次数是不是就少了,gc 次数少了,程序执行效率自然也就提高了。

回过头来我们再看一下Short、Long、Byte、Character类,他们同样都有缓存机制,然而并不能手动调节缓存的范围

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

public static Long valueOf(long l) {
    final int offset = 128;
    if (l >= -128 && l <= 127) { // will cache
        return LongCache.cache[(int)l + offset];
    }
    return new Long(l);
}
public static Byte valueOf(byte b) {
    final int offset = 128;
    return ByteCache.cache[(int)b + offset];
}
public static Character valueOf(char c) {
    if (c <= 127) { // must cache
        return CharacterCache.cache[(int)c];
    }
    return new Character(c);
}
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值