源码解读:包装类型,xxx看完大呼过瘾

源码解读:包装类型

因为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, -231.
  */
  @Native public static final int   MIN_VALUE = 0x80000000;
 ​
  /**
  * A constant holding the maximum value an {@code int} can
  * have, 231-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  TYPE = (Class) 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);
 }

你可能感兴趣的:(java程序员源码后端源码分析)