1. 比特和字节

比特作为信息技术的最基本存储单位,非常小,但大名鼎鼎的比特币就是以此命名的,它的简写为小写字母“b”。

大家都知道,计算机是以二进制存储数据的,二进制的一位,就是 1 比特,也就是说,比特要么为 0 要么为 1。

通常来说,一个英文字符是一个字节,一个中文字符是两个字节。字节与比特的换算关系是:1 字节 = 8 比特。

在往上的单位就是 KB,并不是 1000 字节,因为计算机只认识二进制,因此是 2 的 10 次方,也就是 1024 个字节。

2. 基本数据类型

1. 布尔

boolean hasMoney = true;
boolean hasGirlFriend = false;

布尔(boolean)仅用于存储两个值:true 和 false,也就是真和假,通常用于条件的判断。

2. byte

byte b; // 声明一个 byte 类型变量
b = 10; // 将值 10 赋给变量 b
byte c = -100; // 声明并初始化一个 byte 类型变量 c,赋值为 -100

byte 类型变量用于存储整数,其范围是 -128 到 127,通常用于存储小范围整数。在网络传输、大文件读写时,为了节省空间,常用字节来作为数据的传输方式。

3. short

short s; // 声明一个 short 类型变量
s = 1000; // 将值 1000 赋给变量 s
short t = -2000; // 声明并初始化一个 short 类型变量 t,赋值为 -2000

short 的取值范围在 -32,768 和 32,767 之间,包含 32,767。

4. int

int i; // 声明一个 int 类型变量
i = 1000000; // 将值 1000000 赋给变量 i
int j = -2000000; // 声明并初始化一个 int 类型变量 j,赋值为 -2000000

int 的取值范围在 -2,147,483,648(-2 ^ 31)和 2,147,483,647(2 ^ 31 -1)(含)之间。如果没有特殊需求,整型数据就用 int。

5. long

long l; // 声明一个 long 类型变量
l = 100000000000L; // 将值 100000000000L 赋给变量 l(注意要加上 L 后缀)
long m = -20000000000L; // 声明并初始化一个 long 类型变量 m,赋值为 -20000000000L

long 的取值范围在 -9,223,372,036,854,775,808(-2^63) 和 9,223,372,036,854,775,807(2^63 -1)(含)之间。如果 int 存储不下,就用 long。为了和 int 作区分,long 型变量在声明的时候,末尾要带上大写的“L”。不用小写的“l”,是因为小写的“l”容易和数字“1”混淆。

6. float

float f; // 声明一个 float 类型变量
f = 3.14159f; // 将值 3.14159f 赋给变量 f(注意要加上 f 后缀)
float g = -2.71828f; // 声明并初始化一个 float 类型变量 g,赋值为 -2.71828f

float 是单精度的浮点数(单精度浮点数的有效数字大约为 6 到 7 位),32 位(4 字节),遵循 IEEE 754(二进制浮点数算术标准),取值范围为 1.4E-45 到 3.4E+38。float 不适合用于精确的数值,比如说金额。为了和 double 作区分,float 型变量在声明的时候,末尾要带上小写的“f”。不需要使用大写的“F”,是因为小写的“f”很容易辨别。

7. double

double myDouble = 3.141592653589793;

double 是双精度浮点数(双精度浮点数的有效数字大约为 15 到 17 位),占 64 位(8 字节),也遵循 IEEE 754 标准,取值范围大约 ±4.9E-324 到 ±1.7976931348623157E308。double 同样不适合用于精确的数值,比如说金额。

在进行金融计算或需要精确小数计算的场景中,可以使用 BigDecimal 类来避免浮点数舍入误差。BigDecimal 可以表示一个任意大小且精度完全准确的浮点数。

在实际开发中,如果不是特别大的金额(精确到 0.01 元,也就是一分钱),一般建议乘以 100 转成整型进行处理。

8. char

char letterA = 'A'; // 用英文的单引号包裹住。

注意,字符字面量应该用单引号('')包围,而不是双引号(""),因为双引号表示字符串字面量。

3. 单精度和双精度

单精度(single-precision)和双精度(double-precision)是指两种不同精度的浮点数表示方法。

单精度是这样的格式,1 位符号,8 位指数,23 位小数。

双精度是这样的格式,1 位符号,11 位指数,52 位小数。

计算精度取决于小数位(尾数),则能表示的数越大,那么计算的精度就越高

浮点数遵循 IEEE754 标准

IEEE 754标准的浮点数转换流程主要包括以下几个步骤:

  1. 确定浮点数的符号位:首先,需要确定浮点数的正负性。如果浮点数是正数,则符号位为0;如果是负数,则符号位为1。
  2. 将浮点数转换为二进制表示:将浮点数的整数部分和小数部分分别转换为二进制数。整数部分可以通过连续除以2并取余数的方式得到其二进制表示,余数从低位到高位排列;小数部分则通过连续乘以2并取整数部分的方式得到其二进制表示,整数部分从高位到低位排列。
  3. 确定指数位:根据IEEE 754标准,指数位有一个固定的偏移值(对于单精度是127,对于双精度是1023)。将浮点数的实际指数加上这个偏移值,然后将结果转换为二进制数,即为指数位的值。
  4. 确定尾数位:尾数位用于表示浮点数的精度。将浮点数的二进制表示中除去符号位和指数位后的部分作为尾数位。如果尾数位的位数不足(例如,对于单精度浮点数,尾数位有23位),则需要在尾数前补零至足够的位数。
  5. 组合符号位、指数位和尾数位:将符号位、指数位和尾数位按照规定的顺序组合在一起,即得到IEEE 754标准下的浮点数的二进制表示。

请注意,实际的转换过程中还需要考虑一些特殊情况,比如无穷大、非数值(NaN)以及零的表示等。此外,对于不同精度的浮点数(如单精度和双精度),其指数位和尾数位的位数也会有所不同。

4. int 与 char 类型转换

int 和 char 之间比较特殊,可以互转,也会在以后的学习当中经常遇到

  1. 可以通过强制类型转换将整型 int 转换为字符 char
int value_int = 65;
char value_char  = (char) value_int;
System.out.println(value_char);
  1. 可以使用 Character.forDigit() 方法将整型 int 转换为字符 char,参数 radix 为基数,十进制为 10,十六进制为 16。
int radix = 10;
int value_int = 6;
char value_char = Character.forDigit(value_int , radix);
System.out.println(value_char );
  1. 可以使用 int 的包装器类型 Integer 的 toString() 方法+String 的 charAt() 方法转成 char。
int value_int = 1;
char value_char = Integer.toString(value_int).charAt(0);
System.out.println(value_char );
  1. char 转 int。

当然了,如果只是简单的 char 转 int,直接赋值就可以了

int a = 'a';

不过,如果字符本身就是数字,这种方法就行不通了。

int a = '1';

那么,怎么才能把字符 '1' 转成数字 1 呢?

可以使用 Character.getNumericValue() 方法。

int a = Character.getNumericValue('1');

这样的话,a 的值就是 1 了。

除此之外,还可以使用 Character.digit() 方法。

int a = Character.digit('1', 10);

这样的话,a 的值也是 1。

因为这两个方法的内部实现都大差不差,大家可以研究一下源码。

那还有一种更直观的方法,就是 - '0' 方法。

int a = '1' - '0';

这样的话,a 的值也是 1。这是因为在 ASCII 编码和 Unicode 编码(Java 使用 Unicode 编码)中,数字字符 '0' 到 '9' 是连续排列的,并且它们的编码值是顺序递增的。

字符 '0' 的编码值是 48,字符 '1' 的编码值是 49,依此类推,字符 '9' 的编码值是 57。

当从一个字符的编码值中减去字符 '0' 的编码值(即 48),结果就是该字符所表示的数字值。例如,对于字符 '5',其编码值是 53。计算 53 - 48 得到 5,这就是字符 '5' 所表示的数字值。

5. 包装器类型

包装器类型(Wrapper Types)是 Java 中的一种特殊类型,用于将基本数据类型(如 int、float、char 等)转换为对应的对象类型。

Java 提供了以下包装器类型,与基本数据类型一一对应:

  • Byte(对应 byte)
  • Short(对应 short)
  • Integer(对应 int)
  • Long(对应 long)
  • Float(对应 float)
  • Double(对应 double)
  • Character(对应 char)
  • Boolean(对应 boolean)

包装器类型允许我们使用基本数据类型提供的各种实用方法,并兼容需要对象类型的场景。例如,我们可以使用 Integer 类的 parseInt 方法将字符串转换为整数,或使用 Character 类的 isDigit 方法检查字符是否为数字,还有前面提到的 Character.forDigit() 方法。

下面是一个简单的示例,演示了如何使用包装器类型:

// 使用 Integer 包装器类型
Integer integerValue = new Integer(42);
System.out.println("整数值: " + integerValue);

// 将字符串转换为整数
String numberString = "123";
int parsedNumber = Integer.parseInt(numberString);
System.out.println("整数值: " + parsedNumber);

// 使用 Character 包装器类型
Character charValue = new Character('A');
System.out.println("字符: " + charValue);

// 检查字符是否为数字
char testChar = '9';
if (Character.isDigit(testChar)) {
System.out.println("字符是个数字.");
}

上面的示例中,我们创建了一个 Integer 类型的对象 integerValue 并为其赋值 42。然后,我们将其值打印到控制台。

我们有一个包含数字的字符串 numberString。我们使用 Integer.parseInt() 方法将其转换为整数 parsedNumber。然后,我们将转换后的值打印到控制台。

比如说 parseInt() 用于将字符串转换为整数,这也是非常常用的一个方法,尤其是遇到“数字字符串”转整数的时候。

String text = "123";
int number = Integer.parseInt(text);
System.out.println(number);

可以简单看一下 parseInt() 的源码

public static int parseInt(String s, int radix) throws NumberFormatException {
    // 如果字符串为空或基数不在有效范围内,抛出 NumberFormatException
    if (s == null || radix < Character.MIN_RADIX || radix > Character.MAX_RADIX) {
        throw new NumberFormatException();
    }

    int result = 0; // 用于存储解析结果的变量
    boolean negative = false; // 标记数字是否为负数
    int i = 0, len = s.length(); // i 是字符索引,len 是字符串长度
    int limit = -Integer.MAX_VALUE; // 溢出检查的上限

    if (len > 0) {
        char firstChar = s.charAt(0); // 获取字符串的第一个字符
        if (firstChar == '-') { // 如果是负号
            negative = true; // 设置负数标记
            limit = Integer.MIN_VALUE; // 调整溢出上限为 Integer 的最小值
            i++;
        } else if (firstChar == '+') { // 如果是正号
            i++; // 仅跳过,不做额外操作
        }

        int multmin = limit / radix; // 计算溢出检查的临界值
        while (i < len) {
            // 将字符转换为对应的数字值
            int digit = Character.digit(s.charAt(i++), radix);
            if (digit < 0 || result < multmin || result * radix < limit + digit) {
                // 如果字符不是有效数字或者结果溢出,抛出 NumberFormatException
                throw new NumberFormatException();
            }
            // 累积结果
            result = result * radix - digit;
        }
    } else {
        // 如果字符串为空,抛出 NumberFormatException
        throw new NumberFormatException();
    }

    // 根据正负号返回最终结果
    return negative ? result : -result;
}

简单解释一下:

空值检查:首先检查输入字符串是否为 null,如果是,则抛出 NumberFormatException。

符号处理:检查字符串的第一个字符以确定数字的符号(正或负)。如果字符串以“-”开头,则数字为负数,以“+”或数字开头则为正数。

数字转换:遍历字符串中的每个字符,将字符转换为对应的数字。这是通过从字符中减去 '0' 的 ASCII 值来实现的。

结果计算:计算最终的数字值。这是通过将每个数字乘以其位置权重(10 的幂)并累加到结果中来完成的。

溢出检查:在整个转换过程中,代码会检查是否有溢出的风险。如果检测到溢出,将抛出 NumberFormatException。

返回结果:根据数字的符号返回最终结果。

6. 引用数据类型

基本数据类型在作为成员变量和静态变量的时候有默认值,引用数据类型也有的(学完数组&字符串,以及面向对象编程后会更加清楚,这里先简单过一下)。

String 是最典型的引用数据类型,所以我们就拿 String 类举例,看下面这段代码:

/**
 * @author 微信搜「沉默王二」,回复关键字 PDF
 */
public class LocalRef {
    private String a;
    static String b;

    public static void main(String[] args) {
        LocalRef lv = new LocalRef();
        System.out.println(lv.a);
        System.out.println(b);
    }
}

输出结果如下所示:

null
null

null 在 Java 中是一个很神奇的存在,在你以后的程序生涯中,见它的次数不会少,尤其是伴随着令人烦恼的“空指针异常”,也就是所谓的 NullPointerException。

也就是说,引用数据类型的默认值为 null,包括数组和接口。

那你是不是很好奇,为什么数组和接口也是引用数据类型啊?

先来看数组:

int [] arrays = {1,2,3};
System.out.println(arrays);

arrays 是一个 int 类型的数组,对吧?打印结果如下所示:

[I@2d209079

[I 表示数组是 int 类型的,@ 后面是十六进制的 hashCode——这样的打印结果太“人性化”了,一般人表示看不懂!为什么会这样显示呢?查看一下 java.lang.Object 类的 toString() 方法就明白了。

public String toString() {
	return getClass().getName() + "@" + Integer.toHexString(hashCode());
}

数组虽然没有显式定义成一个类,但它的确是一个对象,继承了祖先类 Object 的所有方法。那为什么数组不单独定义一个类来表示呢?就像字符串 String 类那样呢?

一个合理的解释是 Java 将其隐藏了。假如真的存在一个 Array.java,我们也可以假想它真实的样子,它必须要定义一个容器来存放数组的元素,就像 String 类那样。

public final class String
    implements java.io.Serializable, Comparable<String>, CharSequence {
    /** The value is used for character storage. */
    private final char value[];
}

数组内部定义数组?没必要的!

再来看接口:

List<String> list = new ArrayList<>();
System.out.println(list);

List 是一个非常典型的接口:

public interface List<E> extends Collection<E> {}

而 ArrayList 是 List 接口的一个实现:

public class ArrayList<E> extends AbstractList<E>
        implements List<E>, RandomAccess, Cloneable, java.io.Serializable
{}

对于接口类型的引用变量来说,你没法直接 new 一个:

只能 new 一个实现它的类的对象——那自然接口也是引用数据类型了。

来看一下基本数据类型和引用数据类型之间最大的差别。

基本数据类型:

1、变量名指向具体的数值。 2、基本数据类型存储在栈上。 引用数据类型:

1、变量名指向的是存储对象的内存地址,在栈上。 2、内存地址指向的对象存储在堆上。

7. 堆和栈

堆是在程序运行时在内存中申请的空间(可理解为动态的过程);切记,不是在编译时;因此,Java 中的对象就放在这里,这样做的好处就是:

当需要一个对象时,只需要通过 new 关键字写一行代码即可,当执行这行代码时,会自动在内存的“堆”区分配空间——这样就很灵活。

栈,能够和处理器(CPU,也就是脑子)直接关联,因此访问速度更快。既然访问速度快,要好好利用啊!Java 就把对象的引用放在栈里。为什么呢?因为引用的使用频率高吗?

不是的,因为 Java 在编译程序时,必须明确的知道存储在栈里的东西的生命周期,否则就没法释放旧的内存来开辟新的内存空间存放引用——空间就那么大,前浪要把后浪拍死在沙滩上啊。

用图来表示一下,左侧是栈,右侧是堆。

这里再补充一些额外的知识点,能看懂就继续吸收,看不懂可以先去学下一节,以后再来补,没关系的。学习就是这样,可以跳过,可以温故。

举个例子。

String a = new String("沉默王二")

这段代码会先在堆里创建一个 沉默王二的字符串对象,然后再把对象的引用 a 放到栈里面。这里面还会涉及到字符串常量池,后面会讲。

那么对于这样一段代码,有基本数据类型的变量,有引用类型的变量,堆和栈都是如何存储他们的呢?

public void test()
{
    int i = 4;
    int y = 2;
    Object o1 = new Object();
}

画个图表示一下:

8. 小结

本文详细探讨了 Java 数据类型,包括比特与字节、基本数据类型、单精度与双精度、int 与 char 互转、包装器类型、引用数据类型以及堆与栈的内存模型。通过阅读本文,你将全面了解 Java 数据类型的概念与使用方法,为 Java 编程打下坚实基础。

菜菜,捞捞~