重学Java-基本数据类型及其包装类

还记得去年公司的一次知识地图认证,我报了一个“Java基础”。下面是我答题过程:

考官:Java有哪些数据类型?
我:有八大基本类型,分别是:byte、int、long、float、double、char、boolean...还有一种是啥来着?
考官:这种类型很少用,平时开发基本上看不到,你再想想。
我(想了一会儿):我确实想不起来了
考官:short,一种整型数据类型。

作为一个使用java近十年的老兵没有回答出这个问题,我感到非常汗颜,也因此将这个作为了我重学Java路线中第一课。

接下来正式开始学习java的基本类型和他们的包装类型,里面会有一些工作过程中遇到的和学习到的一些内容,还有本次学习到的一些新内容。

基本类型

基本类型作为java中类的基础,几乎所有的类追根溯源后都是基本类型的组合,这个C语言中的结构体有些相似——通过基本类型+数据结构组合成为结构体以达到某些特殊功能。与其他某些开发语言不同的是,java的基本类型位数是固定的,这也是java能跨平台运行的一大保障。

Java语言提供了八种基本类型。六种数字类型(四个整数型,两个浮点型),一种字符类型,还有一种布尔型:

基本类型分类默认值位数取值范围包装类型
byte整型08-27 ~ 27-1Byte
short整型016-215 ~ 215-1Short
int整型032-231 ~ 231-1Integer
long整型0L64-263 ~ 263-1Long
float浮点类型0.0f3232位单精度 IEEE 754标准Float
double浮点类型0.0d6464位双精度 IEEE 754标准Double
char字符类型空格符16\u0000 ~ \uffffCharacter
boolean布尔类型false1ture 和 falseBoolean

char类型

java中char类型长度为16位,采用的是Unicode编码。Unicode编码包含了ASCII的所有码值,对应的整型数值与ASCII一致(0-127)。

ASCII编码对照表

其实在English,Spanish,German, French中只需要ASCII码即可,但是考虑到其他语言,比如中文、日语、汉语、俄语等ASCII码无法表示的语言,做出了一些妥协。

赋值方法

char类型在java中有三种初始化方法:

// 字符:汉字、符号、数字、转义字符等
char c = '你';

// 整型数值赋值:十进制、八进制、十六进制均可。
char c = 23;

// unicode码值赋值
char c = '\uffff';

单引号在java语法中唯一合法的地方就是char类型复制。

运算

由于char使用的Unicode码,可以使用整型方式赋值,因此在java中char可以进行‘+、-、* 、/ 、%、>、<、^’等运算,例如:

char a = 'a';
char a = a + 'a';
int i = a + 'a';
char a = a + 1;
int i = a + 1;
......

总之,char + char 和 char + int 均为提升为int运算,赋值给char类型后,输出后就是对应的字符了。

类型提升问题

其实在char+char, char+int的过程中,并没有发生实际上的类型提升:

package com.xxxx.t3;

public class BaseType {
    public static void main(String[] args) {
        int i=0;
        float f = 1;
        byte b = 1;
        char c = 0;
        System.out.println(c >> b);
        System.out.println(c + b);
        System.out.println(i+f);
        System.out.println(i + b);
    }
}

编译后使用javap -c -l BaseType输出汇编指令集:

public class com.xxxx.t3.BaseType {
  public com.xxxx.t3.BaseType();
    Code:
       0: aload_0
       1: invokespecial #1                  // Method java/lang/Object."<init>":()V
       4: return
    LineNumberTable:
      line 3: 0

  public static void main(java.lang.String[]);
    Code:
       0: iconst_0
       1: istore_1
       2: fconst_1
       3: fstore_2
       4: iconst_1
       5: istore_3
	
	// 从这儿可以看出,Java中生命char的时候就是以整型数据存储的
       6: iconst_0
       7: istore        4
       9: getstatic     #2                  // Field java/lang/System.out:Ljava/io/PrintStream;

	// char类型的位运算,直接取了两个整型数值进行运算
      12: iload         4
      14: iload_3
	// 位运算
      15: ishr

      16: invokevirtual #3                  // Method java/io/PrintStream.println:(I)V
      19: getstatic     #2                  // Field java/lang/System.out:Ljava/io/PrintStream;

	// char + int 的指令,其实就是整数相加
      22: iload         4
      24: iload_3
      25: iadd

      26: invokevirtual #3                  // Method java/io/PrintStream.println:(I)V
      29: getstatic     #2                  // Field java/lang/System.out:Ljava/io/PrintStream;

	// int + float的指令, 执行了i2f执行,将整型转为浮点类型,然后相加
      32: iload_1
      33: i2f
      34: fload_2
      35: fadd

      36: invokevirtual #4                  // Method java/io/PrintStream.println:(F)V
      39: getstatic     #2                  // Field java/lang/System.out:Ljava/io/PrintStream;

	// int + byte的指令, 也是两个整型数值相加
      42: iload_1
      43: iload_3
      44: iadd
      45: invokevirtual #3                  // Method java/io/PrintStream.println:(I)V
      48: return
    LineNumberTable:
      line 5: 0
      line 6: 2
      line 7: 4
      line 8: 6
      line 9: 9
      line 10: 19
      line 11: 29
      line 12: 39
      line 13: 48
}

更多类型提升问题,后面专门做一个课来研究这个。

相等性判断

相同基本类型判断相等性的时候是直接使用==符号进行判断,例如:

int i1 = 1;
int i2 = 2;
System.out.println(i1 == i2);

不相同的基本类型也是可以进行相等性判断,例如:

byte b = 1;
int i = 1;
System.out.println(b == i); // true 

不同基本类型判断相等性是,遵循一定的原则:

  • 数值型可以相互进行相等性判断,判断过程中会进行类型提升,提升顺序为:byte -> short -> int -> long -> float -> double。即短类型转长类型,整型转浮点型。
  • 字符类型可以与数值型进行相等性判断,字符型类型在java中可以使用int类型表示,所以不需要在代码中进行强制类型转换。判断过程中取字符类型变量对应的整型数值与之进行判断。
  • 布尔型不可与其他类型进行相等性判断。

整型数值范围计算

我们拿byte这个8位的数据类型来举例,取值范围是怎么算出来的呢?

首先byte只有8位,那么二进制补码取值就在:11111111 ~ 01111111 之间了。

  • 第一位是符号位,1表示负数,0表示这个数。
  • 0开始的数值总共有27个,去除00000000,还有27-1个,所以最大值为27-1。
  • 1开始的数值也有27个,全部为负数,所以最小是为-27

由此可以算出byte的取值范围:-27 ~ 27-1。

其他整形数据类型取值范围计算方式类似。

取值范围估算

  • int 32位:可以估算为:210 * 210 * 210 * 2 ≈ 1000 * 1000 * 1000 = 1000000000 (十亿级)
  • long 64位:十亿 * 十亿 ,姑且称为“百亿亿级”数据

浮点型取值范围计算

浮点数取值范围计算咱们以floatl类型举例,double的计算方法几乎一致。

参考文档:IEEE Standard 754 Floating Point Numbers

变量存储

基本类型

包装类

包装类(Wrapper Class): Java是一个面向对象的编程语言,但是Java中的八种基本数据类型却是不面向对象的,为了使用方便和解决这个不足,在设计类时为每个基本数据类型设计了一个对应的类进行代表,这样八种基本数据类型对应的类统称为包装类(Wrapper Class),包装类均位于java.lang包。

对于包装类说,用途主要包含两种:

  • 作为 和基本数据类型对应的类 类型存在,方便涉及到对象的操作。
  • 包含每种基本数据类型的相关属性如最大值、最小值等,以及相关的操作方法。

自动装箱和拆箱

先看一下下面的代码, 还是以int为例子:

public class BaseType {
    public static void main(String[] args) {
        Integer i = 1;
        int b = i;
    }
}

编译后执行javap -c -l BaseType的结果:

Compiled from "BaseType.java"
public class BaseType {
  public BaseType();
    Code:
       0: aload_0
       1: invokespecial #1                  // Method java/lang/Object."<init>":()V
       4: return
    LineNumberTable:
      line 1: 0

  public static void main(java.lang.String[]);
    Code:
       0: iconst_1
       1: invokestatic  #2                  // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
       4: astore_1
       5: aload_1
       6: invokevirtual #3                  // Method java/lang/Integer.intValue:()I
       9: istore_2
      10: return
    LineNumberTable:
      line 3: 0
      line 4: 5
      line 5: 10
}

从上面的执行结果可以得出一下结论:

  • Integer的自动装箱是使用Integer.valueOf(int i)实现的。
  • Integer的自动拆箱是使用Integer.intValue()实现的。

那么还有一个问题,这个自动装箱拆箱是在什么时候进行的呢?

反编译了一下BaseType.class(不要用idea的反编译工具,idea这个太智能了):

public class BaseType
{
  public static void main(String[] paramArrayOfString)
  {
    Integer localInteger = Integer.valueOf(1);
    int i = localInteger.intValue();
  }
}

可以看出自动装箱拆箱在编译阶段就完成了,也就是说这就是一个“语法糖”。

其他基本类型的自动装箱和拆箱原理基本都是如此。

热值缓存

java包装类中针对一些热点数据进行了缓存,主要目的是用于自动装箱的时候直接获取缓存对象,不创建新对象。这个地方是享元模式的一种使用场景。浮点类型数据没有较为特别的热点数据,所以没有进行缓存。

所以,通过自动装箱或者valueOf方法产生的包装对象,在其基本类型值相同的时候,可以使用==判等。但是绝不建议这么做,毕竟我们无法保证两个对象都是自动装箱或者valueOf,也许是new出来的也不一定。

各种包装类型热值缓存情况如下:

包装类型缓存数据范围
Byte所有byte范围
Short-128 ~ 127
Integer-128 ~ 127(可配置)
Long-128 ~ 127
CharacterASCII码值
Boolean直接声明TRUE和FALSE两个变量
Float无缓存
Double无缓存

需要特别注意的是Integer的缓存范围可以通过参数配置,有两种方式:

  • 添加JVM配置-XX:AutoBoxCacheMax=<size>配置项
  • 启动参数设置java.lang.Integer.IntegerCache.high属性

但是,如果手动设置的值如果小于127,则不会生效。

包装类型初始化

由于热值缓存的存在,所以更建议非浮点类型包装类型使用valueOf的方式进行初始化。

针对浮点类型FloatDouble初始化使用valueOfnew是等效的,从某方面讲new可能更加高效一些。

不可变对象

所有包装类中只有一个final的基本类型成员变量,这意味着,包装类实例一旦初始化,其成员变量就变得不可更改了。

这里面有两个点:

  1. 成员变量全是基本类型
  2. final 不可修改

不可变对象保证了所有包装类实例的线程安全。

可以参考《Effective Java》中对不可变对象的定义和描述。

包装类实例运算

包装类实例本身是一个“对象”,对象本身是不支持+、-、* 、/....等运算符的(除String支持+符号以外)。所以,包装类实例运算实际上就是:拆箱后使用基本类型运算。

那么,这个地方就有一个咱们经常犯的错误:在为对实例判空,就直接进行运算。比如:

Integer count = computeValue(); // maybe null
if(count < 1) {
    // do something!
}

如果computeValue方法直接返回null,在执行count < 1的时候就会NPE了。

至于原因就是拆箱的时候,实际上就是在调用实例自身的xxValue()方法,实例为空自然会NPE。

Your browser is out-of-date!

Update your browser to view this website correctly. Update my browser now

×