一、数据类型

1.1 什么是数据类型

数据类型是编程语言中用来定义数据的属性和操作的规范。它确定了数据的存储方式、可操作的范围以及可进行的操作类型。数据类型指定了数据的大小、内存布局和数据值的范围。

1.2 java中的数据类型

在Java等静态类型语言中,数据类型在编译时就需要被明确地指定,并且在程序运行过程中保持不变。

Java数据类型(type)可以分为两大类:基本类型(primitive types)和引用类型(reference types)。基本数据类型是java预定义的,无需new(new出来的对象存放在堆里),基本数据类型存放在栈中。

graph LR 数据类型 --> 基本数据类型 数据类型 --> 引用数据类型 基本数据类型 --> 数值型 基本数据类型 --> c["字符型 (char)"] 基本数据类型 --> b["布尔型 (boolean)"] 数值型 --> i["整数类型 (byte,short,int,long)"] 数值型 --> f["浮点类型 (float,double)"] 引用数据类型 --> clazz["类 (class)"] 引用数据类型 --> inter["接口 (interface)"] 引用数据类型 --> arr["数组 (array)"]

二、基本数据类型

2.1 什么是基本数据类型

在Java中,基本数据类型(Primitive Data Types)是指用于存储基本数据值的数据类型,它们是语言内置的预定义类型。基本数据类型存在栈中,在内存中直接存储数据值。具有较高的执行效率和较小的内存占用。在大规模数据处理和性能要求较高的场景中,选择适当的数据类型可以提高程序的效率。

1
2
int x = 1;
int y = 2;
graph subgraph 栈 direction TB i1["int x = 1;"] i2["int y = 2;"] end

以下是java中的基本数据类型及其默认值和取值范围

数据类型 默认值 包装类 大小 范围
byte 0 Byte 1字节(8bits) -128 ~ +127
short 0 Short 2字节(16bits) -2^15 ~ +2^15-1
int 0 Integer 4字节(32bits) -2^31 ~ +2^31-1
long 0L Long 8字节(64bits) -2^63 ~ +2^63-1
char \u0000 Character 2字节(16 位 Unicode 字符) \u0000(十进制等效值为0) ~ \uffff(即为65535)
float 0.0f Float 4字节(32bits) 见 IEEE 754
double 0.0d Double 8字节(64bits) 见 IEEE 754
boolean false Boolean 一般为1 字节(实际实现可能依赖于 JVM,但通常会占用1 字节) 取值为true或false
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
public class Test {

    static char c;
    static byte b;
    static short s;
    static int i;
    static long l;
    static float f;
    static double d;
    static boolean bl;

    public static void main(String[] args) {
        System.out.println(c); // \u0000 也可以一个空格
        System.out.println(b); // 0
        System.out.println(s); // 0
        System.out.println(i); // 0
        System.out.println(l); // 0
        System.out.println(f); // 0.0
        System.out.println(d); // 0.0
        System.out.println(bl); // false
    }
}

2.2 自动类型转换

Java支持自动类型转换,即将一个数据类型转换为另一个兼容的数据类型,如将int类型赋值给long类型。在进行类型转换时,需要注意数据范围和精度的变化,避免数据丢失或不准确。通常的浮点型数据在不声明的情况下都是double型的,如果要表示一个数据时float 型的,可以在数据后面加上 “F” 。浮点型的数据是不能完全精确的,有时候在计算时可能出现小数点最后几位出现浮动,这是正常的。

graph LR byte --> short --> int --> long; char --> int; int --> float; int --> double; float --> double; long --> double; long --> float
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
public class Test {

    public static void main(String[] args) {
        int x = 100;
        long y = x;
        System.out.println(y); // 100
        
        int b =2;
        float c  = 6f;
        System.out.println(b/c); // 0.33333334 b会转化为float类型  
    }
}

char本质上是一种特殊的int

1
2
3
4
5
6
7
8
public class Test {
    public static void main(String[] args) {
        char ch1,ch2;
        ch1 = 'Y';
        ch2 = 89 ;     // 可以将一个整数值赋给字符型变量
        System.out.println(ch1==ch2); // true
    }
}

2.3 强制类型转换

有时需要将一个数据类型强制转换为另一个不兼容的数据类型,如将long类型转换为int类型。在进行强制类型转换时,需要注意可能的数据丢失和精度问题,确保转换操作的正确性。

1
2
3
4
5
6
7
8
public class Test {

    public static void main(String[] args) {
        long y = 100;
        int x = (int) y;
        System.out.println(x); // 100
    }
}

2.4 包装类和自动装箱

基本类型都有对应的包装类型,基本类型与其对应的包装类型之间的赋值使用自动装箱与拆箱完成。基本数据类型自动装箱过程都是调用其对应的valueOf() 方法。

  • new Integer(2) 与 Integer.valueOf(2) 的区别在于:new Integer(2) 每次都会新建一个对象,Integer.valueOf(123) 会先使用缓存池中的对象(多次调用会取得同一个对象的引用),缓存中没有才会使用new 创建一个新对象。

  • 在 Java 8 中,Integer 缓存池的大小默认为 -128~127。除 Integer 类型,其他基本类型也维护了缓冲池,用于缓存一定范围内的对象。下面是各个基本类型的缓冲池范围:

    1. Byte:缓冲池范围是 -128 到 127。
    2. Short:缓冲池范围是 -128 到 127。
    3. Long:缓冲池范围是 -128 到 127。
    4. Character:缓冲池范围是 0 到 127。
    5. Boolean:Boolean 类型没有缓冲池,只有两个常量对象:Boolean.TRUE 和 Boolean.FALSE。

    需要注意的是,浮点数类型(float 和 double)以及字符类型(char)没有实现缓冲池机制。

    这些基本类型的缓冲池的作用与 Integer 缓冲池类似,即在创建对象时优先从缓冲池中获取,而不是每次都创建新的对象。这样可以节省内存,并提高性能。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
public class PrimitiveCacheExample {
    public static void main(String[] args) {
        Integer x = 2;     // 装箱
        int y = x;        // 拆箱

        Integer i1 = 100;
        Integer i2 = 100;
        System.out.println(i1 == i2); // true

        Integer i3 = 129;
        Integer i4 = 129;
        System.out.println(i3 == i4); // false

        Byte b1 = 127;
        Byte b2 = 127;
        System.out.println(b1 == b2); // true

        Short s1 = 100;
        Short s2 = 100;
        System.out.println(s1 == s2); // true
        Short s3 = 200;
        Short s4 = 200;
        System.out.println(s3 == s4); // false

        Long l1 = 100L;
        Long l2 = 100L;
        System.out.println(l1 == l2); // true
        Long l3 = 200L;
        Long l4 = 200L;
        System.out.println(l3 == l4); // false

        Character c1 = 'A';
        Character c2 = 'A';
        System.out.println(c1 == c2); // true

        Boolean bo1 = true;
        Boolean bo2 = true;
        System.out.println(bo1 == bo2); // true
    }
}

三、引用数据类型

3.1 什么是引用数据类型

引用类型均继承Object类(Objec也是引用类型)。使用Java内存堆和内存栈来进行这种类型的数据存储,简单地讲,“引用”(存储对象在内存堆上的地址)是存储在有序的内存栈上的,而对象本身的值存储在内存堆上的。

3.2 对象实例化及赋值过程

实例化一个对象可以分为三个步骤:

  • 分配内存空间。
  • 初始化对象。
  • 将内存空间的地址赋值给对应的引用。

java中无论是基本类型还是引用类型,都是值传递

  • 对于基本类型而言,传递的是具体的值———-是在赋值
  • 对于引用类型而言,传递的是具体的地址值—-是在指向一个对象

例:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
package packages;

public class Test {
    public static void main(String[] args) {
        Dog dog = new Dog("小黑");
        System.out.println(dog);
        System.out.println(dog.getName());
        new Test().updateDog(dog);
        System.out.println(dog);
        System.out.println(dog.getName());
    }

    void updateDog(Dog dog){
        System.out.println(dog);
        System.out.println(dog.getName());
        dog.setName("小白");
        System.out.println(dog);
        System.out.println(dog.getName());
    }
}

class Dog {
    private String name;

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public Dog(String name){
        this.name = name;
    }
}

/** 
输出结果:
packages.Dog@6536e911
小黑
packages.Dog@6536e911
小黑
packages.Dog@6536e911
小白
packages.Dog@6536e911
小白 
*/

上述结果比较好理解,不做解释。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
package packages;

public class Test {
    public static void main(String[] args) {
        Dog dog = new Dog("小黑");
        System.out.println(dog);
        System.out.println(dog.getName());
        new Test().updateDog(dog);
        System.out.println(dog);
        System.out.println(dog.getName());
    }

    void updateDog(Dog dog1){
        System.out.println(dog1);
        System.out.println(dog1.getName());
        dog1 = new Dog("小白");
        System.out.println(dog1);
        System.out.println(dog1.getName());
    }
}

class Dog {
    private String name;

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public Dog(String name){
        this.name = name;
    }
}
/** 
输出结果:
packages.Dog@6536e911
小黑
packages.Dog@6536e911
小黑
packages.Dog@520a3426
小白
packages.Dog@6536e911
小黑 
*/
  • 第一步创建了一个Dog引用并指向了一个Dog对象,假设Dog对象是0x111,那么Dog的引用就指向了这个0x111的内存地址。
graph LR direction LR subgraph 栈 direction LR i1["Dog dog"] -->f1["0x111"] end subgraph 堆 direction LR i2["0x111 new Dog(“小黑”)"] end f1 --> i2
  • 第二步调用updateDog方法,需要传一个Dog类型的对象,因为java是值传递,值传递传递的是真实内容的一个副本,对副本的操作不影响原内容,也就是形参怎么变化,不会影响实参对应的内容。

    所以updateDog的引用dog1刚开始是指向0x111这个内存地址的,然后执行new Dog(“小白”); 指向了新的内存地址,并不影响外面这个引用dog的指向。所以dog没有变化。

graph LR direction LR subgraph 栈 direction LR i1["Dog dog"] --> f1["0x111"] end subgraph s2["updateDog()栈区"] direction LR i2["Dog dog1"] -- 取消 --> f2["0x111"] i2["Dog dog1"] --> f3["0x222"] end subgraph 堆 direction LR i3["0x111 new Dog(“小黑”)"] i4["0x222 new Dog(“小白”)"] end f1 --> i3 f2 --> i3 f3 --> i4