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

39次阅读

共计 9500 个字符,预计需要花费 24 分钟才能阅读完成。

源码解读:包装类型

因为 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 类:

包装类的缓存机制

咱们先来看一下这个办法:

 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

这里能够本人试一试,看一下有没有成果:

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

正文完
 0