走进 JDK 之 Byte

整理一下前面几篇文章,按顺序阅读效果更好。java

走进 JDK 之 Integer数组

走进 JDK 之 Long缓存

走进 JDK 之 Floatbash

今天来讲说 Byte微信

类声明

public final class Byte extends Number implements Comparable<Byte> 复制代码

和以前的如出一辙,不可变类,继承了抽象类 Number,实现了 Comparable 接口。数据结构

字段

private final byte value; // 包装的 byte 值
public static final byte   MIN_VALUE = -128; // 最小值是 -128
public static final byte   MAX_VALUE = 127; // 最大值是 127
public static final Class<Byte>     TYPE = (Class<Byte>) Class.getPrimitiveClass("byte");
public static final int SIZE = 8; // byte 占 8 bits
public static final int BYTES = SIZE / Byte.SIZE; // byte 占一个字节
private static final long serialVersionUID = -7183698231559129828L;
复制代码

都是很熟悉的属性,再也不过多分析了。这里提第一个问题,为何最大值是 127,最小值是 -128,最小值的绝对值能够比最大值的绝对值大 1 呢 ?这里先不说,看完代码再来解答。函数

构造函数

public Byte(byte value) {
    this.value = value;
}

public Byte(String s) throws NumberFormatException {
    this.value = parseByte(s, 10);
}
复制代码

两个构造函数。第一个传入 byte 直接给 value 赋值,第二个传入字符串,调用 parseByte() 方法转换为 bytepost

方法

parseByte()

public static byte parseByte(String s, int radix) throws NumberFormatException {
        int i = Integer.parseInt(s, radix);
        if (i < MIN_VALUE || i > MAX_VALUE)
            throw new NumberFormatException(
                "Value out of range. Value:\"" + s + "\" Radix:" + radix);
        return (byte)i;
    }
复制代码

调用 Integer.parseInt() 方法转换为 int,再强转 byteInteger.parseInt() 详细解析见 走进 JDK 之 Integer 。不光是 parseInt() 方法,Byte.java 中还有好几个地方都是当作 int 来处理,后面的分析中将会看到。this

这里再提一个问题,做为方法内部局部变量的 byte 在内存中占几个字节 ?spa

valueOf()

public static Byte valueOf(String s, int radix) throws NumberFormatException {
    return valueOf(parseByte(s, radix));
}

public static Byte valueOf(byte b) {
    final int offset = 128;
    return ByteCache.cache[(int)b + offset];
}
复制代码

再来看一下 ByteCache :

private static class ByteCache {
    private ByteCache(){}

    static final Byte cache[] = new Byte[-(-128) + 127 + 1];

    static {
        for(int i = 0; i < cache.length; i++)
            cache[i] = new Byte((byte)(i - 128));
    }
}
复制代码

一样也是缓存了 -128127,也就是说缓存了 byte 的全部可取值。

toString()

public String toString() {
    return Integer.toString((int)value);
}
复制代码

toString() 方法直接调用了 Integer.toString()

其余的确没啥好说的了,Byte 类源码比较简单,认真度过 Integer 源码的同窗,大概浏览一下就有数了。后面来回答一下前面提问的两个问题。

为何 Byte 最小值的绝对值比最大值的绝对值大 1 呢 ?

其实不光是 Byte,Java 的因此基本整数类型都是这样(固然不包括 char) :

基本类型 最大值 最小值
byte 127 -128
short 215-1 - 215
int 231-1 231
long 263-1 -263

能够看到取值范围都是不对称的,负数的范围比正数的范围都大 1。解释这个问题以前,先来看几个基本概念:

  • 原码:最高位是符号位,后面表示具体数值。
  • 反码:原码的符号位不变,其他取反
  • 补码:反码加 1
  • 以上仅针对负数,正数的原码、反码、补码都是其自己

例如 -8 ,其原码是 1000 1000,反码是 1111 0111,补码是 1111 1000。那么计算机中到底存储的是哪一种形式呢?这就要涉及到减法运算了。相比减法运算,计算机是更乐意作加法运算的,若是遇到 1 - 8 这道题目,它就会想我计算 1 + (-8) 不是一个道理吗,最好我还能不把符号位当符号位,一块儿做加法,还能提升一点运算效率。那么,负数的加法运算怎么作的,咱们来尝试一下。

首先,咱们按原码计算:

1 + (-8) = (0000 0001)(原) + (1000 1000)(原) = (1000 1001)(原) = -9
复制代码

显然不正确。再看反码:

1 + (-8) = (0000 0001)(反) + (1111 0111)(反) = (1111 1000)(反) = (1000 0111)(原) = -7
复制代码

好像没什么毛病,计算结果很正确。再换个例子看看:

1 + (-1) = (0000 0001)(反) + (1111 1110)(反) = (1111 1111)(反) = (1000 0000)(原) = -0
复制代码

上篇文章解析 Float 时说过,浮点数是区分 +0.0-0.0 的。可是整数的 0 是没有正负之分的,用反码无法解决 -0 的问题。最后来看一下补码运算是否会存在 -0

1 + (-1) = (0000 0001)(补) + (1111 1111)(补) = (0000 0000)(补) = (0000 0000)(原) = 0
复制代码

经过进位把符号位的 1 给溢出了,从而避免产生了 -0

综上所述,补码是比较适合在计算机中来表示整数的,实际上大多数计算机也正是这么作的。再回到这个 -0,二进制表示为 1000 0000,总不能把它丢掉吧,多点表示范围老是好的,就把它定为了 -128,而且它没有反码,也没有补码,它就是 -128

如今咱们知道了 -128 其实就是替代了 -0 的存在。再来讲一个知识点,你会更加直观的了解 -128。如何快速的将补码转换为十进制数?其实不论正数仍是负数,补码二进制转换为咱们熟悉的十进制都遵循相同的规律。看看下面几个转换:

15 = (0000 1111)(补)
    = - 0*2^8 + (1*2^3 + 1*2^2 + 1*2^1 +1*2^0)
    = 0 + 8 + 4 + 2 + 1
   
-15 = (1111 0001)(补)
    = - 1*2^8 + (1*2^6 + 1*2^5 + 1*2^4 +1*2^0)
    = -128 + 64 + 32 + 16 + 1

复制代码

不须要转换为源码,直接按补码计算。正数符号位表示为 0,负数符号位表示为 -128。显而易见,最小的负数确定是 10000 0000 = -128 + 0 + 0 + ... 。如今你应该对个问题很清楚了吧。下面看第二个问题:

做为方法内部局部变量的 byte 在内存中占几个字节 ?

乍看之下我在问一个废话,byte 那不愿定是 1 个字节吗 !没错,byte 是一个字节,可是我这个问题有特定的条件,做为方法内部局部变量的 byte。咱们一般所说的 byte 占一个字节,指的是若是在 java 堆上分配一个 byte,那么就是一个字节。同理,int 就是四个字节。那么,方法内的局部变量 是存储在堆上的吗?显然不是的,它是存储在栈中的。若是不理解的话,咱们先来回顾一下 Java 的运行时数据区域。

Java 的运行时数据区包含一下几块:

  • 程序计数器:当前线程所执行的字节码的行号指示器
  • Java 虚拟机栈:描述的是 Java 方法执行的内存模型
  • 本地方法栈:为 native 方法服务
  • Java 堆:全部的对象实例以及数组都在这里分配
  • 方法区:存储已被虚拟机加载的类信息、常量、静态常量、即时编译器编译后的代码等数据
  • 运行时常量池:方法区的一部分,存放编译期生成的各类字面量和符号引用

咱们一般所说的栈就是指 Java 虚拟机栈。每个线程都有本身的 Java 虚拟机栈,用于存储栈帧。栈帧是用于支持虚拟机进行方法调用和方法执行的数据结构。每一个方法在执行的同时都会建立一个栈帧,用于存储局部变量表、操做数栈、动态连接、方法出口等信息。每个方法从调用直至执行完成的过程,就对应一个栈帧在虚拟机栈中入栈到出栈的过程。因此,方法内的局部变量 byte 不出意外应该就是存储在局部变量中了。那么,局部变量表的结构又是怎么样的呢?

局部变量表是一组变量值存储空间,用于存放方法参数和方法内部定义的变量。在 Java 程序编译 Class 文件时,就在方法的 Code 属性的 max_locals 数据项中肯定了该方法所需分配的局部变量表的最大容量。在我以前一篇文章 Class 文件格式详解 中,详细解析了 Class 文件结构,咱们再来回顾一下它的 main() 方法的 Code 属性:

max_stack 表明了操做数栈深度的最大值。在方法执行的任意时刻,操做数栈都不会超过这个深度。虚拟机运行的时候须要根据这个值来分配栈帧中的操做栈深度。

max_locals 表明了局部变量表所需的存储空间,以 slot 为单位。Slot 是虚拟机为局部变量分配内存所使用的最小单位。简而言之,栈帧就是一个 Slot[],利用下标来访问数组元素。那么,对于不一样的数据类型是如何处理的呢?这里就是典型的以空间换时间。除了 longdouble 占用两个 Slot 之外,其余基本类型 booleanbytecharshortintfloat 等都占用一个 Slot。这样就而已快速的利用下标索引来进行定位了。因此,在局部变量表中,byteint 占用的内存是同样的。

总结

Byte 源码没有说的不少,不少方法都是直接调用 Integer 类的方法。后面主要说了两个知识点:

  • 补码表示法更加利用运算,把减法当加法算,且能够多表示一个 -128,也就是 1000 0000
  • 基本类型做为方法局部变量是存储在栈帧上的,除了 longdouble 占两个 Slot,其余都占用一个 Slot

文章同步更新于微信公众号: 秉心说 , 专一 Java 、 Android 原创知识分享,LeetCode 题解,欢迎关注!

相关文章
相关标签/搜索