Java基础5:基本数据类型
一、前言
本文内容摘自《深入理解Java核心技术:写给Java工程师的干货笔记(基础篇)》一书,2022年出版,作者 张洪亮(@Hollis),阿里巴巴技术专家,著有《Java工程师成神之路》系列文章,《Java工程师成神之路》电子书已开源,可在阿里云开发者社区免费下载。书籍内容比电子书内容要丰富,内容有修改,有需要的读者可以购买正版书籍。
【如何成神:先搬砖,再砌砖,后造砖!】
本文由 @大白有点菜 原创,请勿盗用,转载请说明出处!如果觉得文章还不错,请点点赞,加关注,谢谢!
二、第6章 基本数据类型
1、基本类型
基本类型(或者叫作内置类型)是Java中不同于类(Class)的特殊类型,它们是编程中使用最频繁的类型。
Java是一种强类型语言,第一次声明变量时必须说明数据类型,第一次变量赋值称为变量的初始化。
【Java的基本类型共有8种,可以分为三类】:
- 字符类型
- 布尔类型
- 数值类型
【数值类型又可以分为整数类型byte、short、int、long和浮点数类型float、double】:
- 字符类型:
char
->包装类【Character】 - 布尔类型:
boolean
->包装类【Boolean】 - 数值类型:
byte
->包装类【Byte】、short
->包装类【Short】、int
->包装类【Integer】、long
->包装类【Long】、float
->包装类【Float】、double
->包装类【Double】
特别需要注意的是,虽然String在日常开发中也经常使用,但是String不是基本类型,而是引用类型
。
Java中的数值类型不存在无符号的,它们的取值范围是固定的,不会随着机器硬件环境或者操作系统的改变而改变。
实际上,Java还存在另外一种基本类型void,不过我们无法直接对它们进行操作。
(1)使用基本数据类型的好处
在Java中,新创建的对象是存储在堆里的(不考虑JIT优化的情况下),通过栈中的引用来使用这些对象。所以,对象本身来说是比较消耗资源的。
Java提供了基本数据类型,这种数据的变量不需要使用new创建,它们不会在堆上创建,而是直接在栈内存中存储,因此会更加高效。
(2)整型的取值范围
注:1字节=8位(bit)
。Java中的整型属于有符号数。
【8 bit可以表示的数字】:
- 最小值:10000000(-128)
- 最大值:011111111(127)
【整型的取值】:
byte
:1 字节
,范围为 -128(-27)到 127(27-1) ,在变量初始化时,byte 类型的默认值为 0
。short
:2 字节
,范围为 -32768(215) 到 32767(215-1),在变量初始化时,short 类型的默认值为 0
。int
:4 字节
,范围为 -2147483648(-231)到 2147483647(231-1),在变量初始化时,int 类型的默认值为 0
。long
:8 字节
,范围为 -9223372036854775808(-263)到 9223372036854775807 (263-1),在变量初始化时,long 类型的默认值为 0L 或 0l,也可直接写为 0
。
【示例代码】:
/**
* 基本数据类型
* @author 大白有点菜
*/
public class BasicDataTypeApp {
public static void main(String[] args) {
System.out.println("**********分割线1**********");
System.out.println("byte范围:" + Byte.MIN_VALUE + " 到 " + Byte.MAX_VALUE);
System.out.println("**********分割线2**********");
System.out.println("short范围:" + Short.MIN_VALUE + " 到 " + Short.MAX_VALUE);
System.out.println("**********分割线3**********");
System.out.println("int范围:" + Integer.MIN_VALUE + " 到 " + Integer.MAX_VALUE);
System.out.println("**********分割线4**********");
System.out.println("long范围:" + Long.MIN_VALUE + " 到 " + Long.MAX_VALUE);
}
}
【运行结果】:
**********分割线1**********
byte范围:-128 到 127
**********分割线2**********
short范围:-32768 到 32767
**********分割线3**********
int范围:-2147483648 到 2147483647
**********分割线4**********
long范围:-9223372036854775808 到 9223372036854775807
(3)超出范围怎么办
在整型中,每个类型都有一定的表示范围,但是,在程序中有些计算会导致结果超出表示范围,即溢出。
【示例代码】:
/**
* 基本数据类型
* @author 大白有点菜
*/
public class BasicDataTypeApp {
public static void main(String[] args) {
int i = Integer.MAX_VALUE;
int j = Integer.MAX_VALUE;
int k = i + j;
System.out.println("i(" + i + ") + j(" + j + ") = k(" + k + ")");
}
}
【运行结果】:
i(2147483647) + j(2147483647) = k(-2)
溢出时并不会抛出异常,也没有任何提示。
2、浮点数
计算机中的数字都是通过二进制数存储和运算的,对于十进制整数转换为二进制整数,采用“除2取余,逆序排列
”法。
- 用2整除十进制整数,可以得到一个商和余数。
- 再用2去除商,又会得到一个商和余数,如此循环,直到商小于1为止。
- 把先得到的余数作为二进制数的低位有效位,后得到的余数作为二进制数的高位有效位,依次排列起来。
例如,把127转换成二进制数,如图所示:
那么,十进制小数转换成二进制小数,又该如何计算呢?
十进制小数转换成二进制小数采用“乘2取整,顺序排列
”法。
- 用2乘十进制小数,可以得到积。
- 将积的整数部分取出,再用2乘余下的小数部分,又得到一个积。
- 再将积的整数部分取出,如此循环,直到积中的小数部分为零,此时 0 或 1 为二进制数的最后一位,或者达到所要求的的精度为止。
例如,把0.625转换成二进制数,如图所示:
但是0.625是一个特例,用同样的算法,计算0.1对应的二进制数是多少,如图所示。
0.1的二进制数中出现了无限循环的情况,也就是(0.1)10=(0.000110011001100…)2。
在这种情况下,计算机就无法用二进制数精确地表示0.1了。
为了解决部分小数无法使用二进制数精确表示的问题,于是就有了 IEEE 754(IEEE二进制浮点数算术标准) 规范。
浮点数和小数并不是完全一样的,计算机中小数的表示法有定点和浮点两种。因为在位数相同的情况下,定点数的表示范围要比浮点数小。所以在计算机科学中,使用浮点数表示实数的近似值。
IEEE 754 规定了4种表示浮点数值的方式:单精度(32位)、双精度(64位)、延伸单精度(43bit以上,很少使用)与延伸双精度(79bit以上,通常以80位实现)。单精度和双精度两种浮点数最常用。
一个单精度浮点数(float)在计算机存储器中占4字节(32bit),利用“浮点”(浮动小数点)的方法,可以表示一个范围很大的值。
相比单精度浮点数,一个双精度浮点数(double)占用8字节(64bit)。
需要注意的是,IEEE并没有解决小数无法精确表示的问题,只是提出了一种使用近似值表示小数的方式,并且引入了精度的概念。
一个浮点数a由两个数m和e来表示:a=m x be 。
在任意的系统中,我们选择一个基数b(记数系统的基)和精度p(即使用多少位来存储)。m(即尾数)是形如 ±d.ddd…ddd 的p位数(每一位是一个介于0到b-1之间的整数,包括0和b-1)。
如果m的第一位是非0整数,则称m是规格化的。有一些描述使用一个单独的符号位(s代表+或者-)来表示正负,这样m必须是正的。e是指数。
规格化又叫规格化数,是一种表示浮点数的规格化的表示方法,经过规格化的浮点数叫做规格化数。
最后,由于计算机中保存的小数其实是十进制的小数的近似值,并不是准确值,所以,千万不要在代码中使用浮点数来表示金额等重要的指标。建议使用 BigDecimal 或者 Long(单位为分)来表示金额。
3、Java中负数的绝对值并不一定是正数
绝对值是指一个数在数轴上所对应的点到原点的距离。在数学领域,正数的绝对值是这个数本身,负数的绝对值应该是其相反数。
Java中可以使用 java.lang.Math
中的 abs
方法获得一个数字的绝对值,一共有4个重载的 abs 方法。
public static int abs(int a) {
return (a < 0) ? -a : a;
}
public static long abs(long a) {
return (a < 0) ? -a : a;
}
public static float abs(float a) {
return (a <= 0.0F) ? 0.0F - a : a;
}
public static double abs(double a) {
return (a <= 0.0D) ? 0.0D - a : a;
}
以上4个方法分别返回 int、long、float、double 类型的绝对值,方法中的逻辑也简单,无非就是遇到整数就直接返回,遇到负数就取相反数返回。
我们基于订单号实现分库分表,但订单号是字符串类型,我们需要取得这个字符串的 hashCode,因为 hashCode 可能是负数,所以对 hashCode 取绝对值,再用这个值对分表数据取模:
Math.abs(orderId.hashCode()) % 1024;
但是,上面这个逻辑是有问题的!因为在极特殊情况下,上面的代码会得到一个负数的值。这种极特殊情况就是当 hashCode 是 Integer.MIN_VALUE 时
,即整数能表达的最小值时。通过代码来验证。
【abs测试绝对值为负数 - 代码】
/**
* 绝对值
* @author 大白有点菜
*/
public class AbsApp {
public static void main(String[] args) {
System.out.println("Math.abs(Integer.MIN_VALUE)的值是:" + Math.abs(Integer.MIN_VALUE));
}
}
【abs测试绝对值为负数 - 运行结果】
Math.abs(Integer.MIN_VALUE)的值是:-2147483648
很明显,这是一个负数!为什么会这样呢?其实,就是 Integer 的取值范围引起的, int 的取值范围为 -231 至 231 - 1,即 -2147483648
至 2147483647
。当我们使用 abs 取绝对值时,取得 -2147483648 的绝对值为 2147483648 。但是,2147483648 大于 2147483647,即超过了int的取值范围。这时就会发生越界。
2147483647 用二进制的补码表示如下:
01111111 11111111 11111111 11111111
这个数 + 1 后得到如下值:
10000000 00000000 00000000 00000000
这个二进制值就是 -2147483648 的补码。这种情况发生的概率很低,只有当要取绝对值的数字是 -2147483648 时,得到的数字才是一个负数。
如何解决这个问题呢?那就是要解决越界的问题,在取绝对值之前,把这个 int 类型的值转成 long 类型,这样就不会越界了。例如,将分表逻辑修改为:
Math.abs((long)orderId.hashCode()) % 1024;
【abs测试绝对值不再为负数 - 代码】
/**
* 绝对值
* @author 大白有点菜
*/
public class AbsApp {
public static void main(String[] args) {
System.out.println("Math.abs(Integer.MIN_VALUE)的值是:" + Math.abs((long)Integer.MIN_VALUE));
}
}
【abs测试绝对值不再为负数 - 运行结果】
Math.abs((long)Integer.MIN_VALUE)的绝对值是(int转换为long):2147483648
一定要记得,对 long 类型的数值取绝对值其实也可能存在这个情况,只不过发生的概率就更低了。