Java编程自学之路:数据类型


数据类型

Java数据类型可分为两大类:

  • 基本类型:又叫内置数据类型,或值类型;

    • 变量名指向具体数值;
    • 变量声明后立刻分配内存空间;
    • 使用时需要赋值,判等时可使用运算符==;
  • 引用类型:除值类型之外,都是引用类型,如数组、字符串、对象引用;

    • 变量名指向对象的内存地址;
    • 变量声明后不会分配分配内存,只存储了一个内存地址;
    • 使用时可以赋值为null,判等需使用equals()方法;

基本类型

基本类型分类

Java语言提供了8种基本类型,大致分为4类:

类型名称 分类 比特位 默认值 取值范围 说明
boolean 布尔型 8bit false {false,true}
char 字符型 16bit \u0000 [0,2^16-1] 存储Unicode码,用单引号赋值
byte 整形 8bit 0 [-2^7, 2^7] -128到127
short 整形 16bit 0 [-2^15, 2^15-1] -32768到32767
int 整形 32bit 0 [-2^31, 2^31-1] 上限大约为21亿
long 整形 64bit 0L [-2^63, 2^63-1] 赋值需在数字后面加上lL
float 浮点型 32bit +0.0F (-2^-126, 2^127) 赋值需在数字后加上fF
double 浮点型 64bit +0.0D (2^-1022, 2^1023 ) 赋值需在数字后加上dD

1字节=8比特位,或者说 1byte=8bit

整形及浮点型都是有符号数据

各种基础类型默认值显示不一样,但内存中实际都是0

单精度浮点型取值需去除非规格数-(2-2^-23)*2^1272^-126

双精度浮点型取值需去除非规格数-2(2-2^-52)* 2^10232^-1022

Java中的数值类型不存在无符号的,取值范围也是固定的,不会随着硬件改变而变动;

实际上,Java中还存在另外一种基本类型Void,对应包装类为java.lang.Void,但无法对其进行操作;

基本类型的好处

我们都知道在Java语言中,new一个对象是存储在堆里的,我们通过栈中的引用来使用这些对象,对象本身是比较消耗资源的。

对于经常用到的类型,如int等,如果每次使用这种变量都要new一个Java对象的话,就会比较“笨重”。所以,与C++一样,Java提供了基本数据类型,这种类型的变量不需要使用new来创建,所以它们不会在堆上创建,而是直接在栈内存中存储,因而更加高效。

什么是浮点型

计算机的数字存储与运算都是通过二进制进行的;

其表示形式如下:

类型 数符(m) 阶码(E) 尾数(M) 指数范围
单精度 1位 8位 23位 [-126,127]
双精度 1位 11位 52位 [-1022,1023]
临时 1位 15位 64位 [-16382,16383]

其中:

  • 数符:尾数的符号位;1为负,0为正;
  • 阶码:表示数的幂,基数为2;用移码表示;
  • 尾数:表示数的小数部分,基为2;用原码表示;

二进制转十进制

除二取余法(整数)

  • 用2整除十进制数,得到商和余数;
  • 循环往复,直到商小于2时为止;
  • 将所有得到的余数按倒序排列即为对应的二进制数;
125转换为二进制,计算流程如下:

被除数			除数			商			 余数
125				2					62		  1
62				2					31		  0
31				2					15		  1
15				2					7			  1
7					2					3			  1
3					2					1			  1

最后转换的二进制数为:111101;

乘二取整法(小数)

  • 用2乘以十进制小数,得到乘积,取出整数部分;
  • 循环往复,直到得到的乘积小数部分为零为止;
  • 将所有取出的整数部分按正序排列作为转换后的二进制小数部分;
将0.1转换为二进制小数,计算流程如下:

被乘数				乘数				积				取整
0.1					2						0.2			 0
0.2					2						0.4			 0
0.4					2						0.8			 0
0.8					2						1.6			 1
0.6					2						1.2			 1
0.2					2						0.4			 0
……

转换后二进制小数为:0.000110……

0.1的二进制小数为无限循环小数,所以计算机无法精确表示0.1;

所以计算机科学中,使用浮点数来表示实数的近似值;

IEEE754标准

IEEE二进制浮点数算术标准(IEEE 754)是20世纪80年代以来最广泛使用的浮点数运算标准,为许多CPU与浮点运算器所采用。用以解决部分小数无法使用二进制精确表示的问题;

IEEE 754规定了四种表示浮点数值的方式:单精确度(32位)、双精确度(64位)、延伸单精确度(43比特以上,很少使用)与延伸双精确度(79比特以上,通常以80位实现);

其中最常用的就是32位单精度浮点数和64位双精度浮点数;

IEEE并没有解决小数无法精确表示的问题,只是提出了一种使用近似值表示小数的方式,并且引入了精度的概念;

计算机中保存的小数其实是十进制小数的近似值,并不是精确值;所以业务系统千万不要使用浮点数来表示金额等重要指标;建议使用BigDecimalLong来表示金额;

类型转换

Java中,数据类型转换有两种方式:

  • 自动转换
  • 强制转换

自动转换

一般情况下,定义了某种类型的变量,就不能随意转换。但Java允许用户对基本类型做有限度的类型转换;

以下情况会自动进行类型转换:

  • 由小数据转换为大数据

    显而易见的,“小”数据类型的数值表示范围小于“大”数据类型的数值表示范围,即精度小于“大”数据类型;

    所以,如果“大”数据类型向“小”数据类型转换,可能会丢失精度,导致结果失真;反之,“小”数据类型向“大”数据类型转换,则不会存在数据丢失风险;这种类型转换也称为扩大转换

  • 数值运算

    不同精度的数值进行运算后,结果为“大”数据类型;例如,整形与浮点型计算后结果为浮点型;

  • 类型转换兼容

上文所说的“小”数据类型和“大”数据类型,指的是表示值的范围与精度大小;

基础类型的大小顺序为:byte、char、short<int<long<float<double

强制转换

在不符合自动转换条件或根据用户的需要,可以对数据类型做强制转换;

通过小括号()来进行类型强制转换,引用类型也可以使用强制类型转换;

数值溢出

常用基本类型计算时,都有可能超出表达范围的可能性,且数值溢出时不会抛出任何异常,所以这类问题非常容易被忽略。改进的方法为:

  • 使用Math类的addExactsubtractExact等方法进行数值运算,这样在产生溢出时主动抛出异常;
  • 使用大数类BigDecimalBigIntegerBigDecimal是处理浮点数专家,BigInteger是处理整数专家;

数据类型判等

Java中,通常使用equals()方法或者==运算符进行判等操作;

  • ==
    • 操作符,用于对字面量进行判等,主要适用于基本类型;
  • equals()
    • 方法,可对对象内容进行判等,主要适用于引用类型;
    • 自定义实现技巧:
      • 考虑性能:先进行指针判等,如果相同则直接返回true
      • 控制判断:优先对乙方判空,空对象与自身比较,则直接返回false
      • 类型判断:如果类型不同,则直接返回false
      • 值判断:类型相同的情况下,逐一判断所有字符

引用类型判定注意事项:

  1. hashCode()equals()需要配对实现
  2. compareToequals()的实现逻辑需一致

Lombok注解避坑:

Lombok@Data注解会自动实现equals()hashCode()方法,但存在继承关系时,注解自动生成的方法可能不符合开发者预期;

@EqualAndHashCode默认实现没有使用父类属性,需要手动设置callSuper开关为true来覆盖默认行为;

开发技巧

  • 包装类不能使用操作符==进行判等,必须使用equals()进行判等;

  • 字符串正确的判定方式是使用equals()方法;但字符串内容存储在常量池中,也可以使用操作符==进行判等;

引用类型

引用类型是指Java中除了基本类型之外的所有类型(比如通过class定义的类型),它的值是指向内存空间的引用,也就是内存地址;

Java引用类型分类如下:

强引用

在Java中最常见的就是强引用,把一个对象赋给一个引用变量,这个引用变量就是一个强引用;当一个对象被强引用变量引用时,它处于可达状态,它是不可能被垃圾回收机制回收的,即使该对象以后一直不会被使用到,JVM也不会进行回收;

强引用是造成Java内存泄露的主要原因之一;

软引用

软引用需要通过SoftReference类来实现,对于只有软引用的对象来说,当系统内存足够时它不会被回收,当系统内存不足时它就会被回收;

软引用通常用在对内存敏感的程序中;

弱引用

弱引用需要用WeakReference类来实现,它比软引用的生存期更短,对于只有弱引用的对象来说,只要垃圾回收触发,不管JVM的内存空间是否足够,总会回收该对象占用的内存;

虚引用

虚引用需要用PhantomReference类来实现,它不能单独使用,必须和引用队列联合使用;

虚引用的主要作用是跟踪对象被垃圾回收的状态;

包装类型

Java语言是一个面向对象的语言,但是Java中的基本数据类型却不是面向对象的,这在实际使用中存在很多的不便,为了解决这个不足,在设计类时为每个基本数据类型设计了一个对应的类进行代表,这样8个和基本数据类型对应的类统称为包装类(Wrapper Class)。

包装类均位于java.lang包中,包装类与基本类型的对应关系如下:

基本类型 包装类
byte Byte
short Short
int Integer
long Long
float Float
double Double
boolean Boolean
char Character

在这8个类名中,除了IntegerCharacter类以后,其他6个类的类名与基本数据类型一致,只是类名的第一个字母大写;

为什么需要包装类

既然Java为了提高效率,提供了8种基本数据类型,为什么还要提供包装类?

因为Java是一种面向对象语言,很多地方都需要使用对象而不是基本数据类型。比如,集合类中,我们无法将intdouble等基本类型放进去。因为集合的容器要求元素是Object类型。

为了让基本类型也具有对象的特征,就出现了包装类型,相当于将基本类型“包装起来”,使得基本类型具备对象的属性,并且为其添加了属性和方法,丰富基本类型的操作。

拆箱与装箱

有了基本类型与包装类,就需要在两者之间进行转换。比如把一个基本类型的int转换成一个包装类型Integer对象。

包装类是对基本类型的包装,所以,把基本类型转换成包装类的过程就是boxing,中文翻译为装箱;反之,把包装类转换为基本类型的过程就是unboxing,中文译为拆箱。

自动拆箱与自动装箱

JavaJava SE5中为了减少开发人员的工作量,提供了自动拆装箱的能力。

自动装箱:即将基本类型自动转换为包装类;

自动拆箱:即将包装类自动转换为基本类型;

Integer i = 10;   //自动装箱

int b = i;   // 自动装箱

Integer i =10实际对应的代码应该是Integer i = new Integer(10),得益于Java自动装箱功能,让开发者可以省略了手动去new一个对象。

自动装箱是通过包装类的valueOf方法来实现的,自动拆箱功能是通过包装类对象的xxxValue()来实现的。

自动拆装箱应用场景

除了最简单基础的变量初始化与赋值外,以下场景均会进行自动拆装箱:

场景一:将基本类型放入集合类

List<Integer>  list = new ArrayList<>();

for( int i=1; i<10; i++) {
  list.add(Integer.valueOf(i))
}

场景二:包装类型与基本类型比较

Integer i1 = 1;
System.out.println( i1 == 1 ? "true" : "false");

Boolean bool = false;
System.out.println(bool ? "True" : "False");

反编译后代码如下:

Integer i1 = Integer.valueOf(1);
System.out.println(a.intValue() == 1 ? "true" : "false")
Boolean bool = Boolean.valueOf(false);
System.out.println(bool.booleanValue ? "True" : "False");

场景三:包装类运算

Integer i1 = 10;
Integer i2 = 5;
System.out.println( i1 + i2);

反编译代码如下:

Integer i = Integer.valueOf(10);
Integer j = Integer.valueOf(5);
System.out.println(i.intValue() + j.intValue());

场景四:三目运算符

boolean flag = true;
Integer i = 0;
int j =1;
int k = flag ? i : j;

以上代码反编译后如下:

boolean  flag = true;
Integer i = Integer.valueOf(0);
int j = 1;
int k = flag ? i.intValue() : j;
System.out.println(k);

在三目运算表达式中,当第二、第三位操作数分别为基本类型与对象时,其中的对象会拆箱为基本类型。

场景五:函数参数与返回值

//自动拆箱
public  int getNum1(Integer num) {
  return num;
}

//自动装箱
public Integer getNum2(int num) {
  return num;
}

自动拆装箱与缓存

Java SE自动拆装箱提供了一个和缓存相关的功能;

public static void main(String... String) {
  
  Integer int1 = 3;
  Integer int2 = 3;
  
  if (int1 == int2) {
    System.out.println("int1 == int2");
  }
  else {
    System.out.println("int1 != int2");
  }
  
  Integer int3 = 300;
  Integer int4 = 300;
  
  if (int3 == int4) {
    System.out.println("int3 == int4");
  }
  else {
    System.out.println("int3 != int4");
  }
}

输出结果为:

int1 == int2
int3 !=  int4

原因与Integer中的缓存机制有关,在Java SE5中,在Integer的操作上引入了一个新功能来节省内存和提高性能。整型对象通过使用相同的对象引用实现了缓存和重用。

  1. 当需要进行自动装箱时,如果数字在 -128 至 127 之间时,会直接使用缓存中的对象,而不是重新创建一个对象。
  2. 只适用于自动装箱,使用构造函数创建对象不适用;
  3. Javadoc 详细的说明了缓存支持 -128 到 127 之间的自动装箱过程。最大值 127 可以通过 -XX:AutoBoxCacheMax=size 修改。
  4. 在 Java 6 中,可以通过 java.lang.Integer.IntegerCache.high 设置最大值。

自动拆装箱带来的问题

自动拆箱装是一个很好的功能,大大节省开发人员的精力,但是也会引入一些问题:

  1. 包装对象的数值比较,不能简单的使用==,虽然-128到127之间的整数可以,但这个范围之外还是需要使用equals比较。
  2. 有些场景会进行自动拆装箱,如果包装类对象为null,那么自动拆装箱时就可能抛出NPE。
  3. for循环内部进行大量拆装箱,会浪费大量资源;

文章作者: Semon
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 Semon !
评论
  目录