Kotlin/Java 数据类型的底层逻辑

语言: CN / TW / HK

基本数据类型与引用类型

Java 遵循 JVM 规范,常见的说法是 Java 中存在两种对象的类型:基本数据类型和引用类型。而这个实际上是指所有 JVM 平台上的语言,都遵循的底层原理。

Java

基本数据类型

Java 中的基本数据类型包括 8 种:

markdown 1. byte —— 整型,1 字节 2. boolean —— 布尔型,1 字节 3. short —— 整型,2 字节 4. char —— 字符型,2 字节 5. int —— 整型,4 字节 6. float —— 浮点型,4 字节 7. long —— 整型,8 字节 8. double——浮点型,8 字节

一个字节等于 8 位二进制数,所以 int 的最大最小值是二进制的 32 位数的值:

java // A constant holding the maximum value an int can have, 2^31-1. @Native public static final int MAX_VALUE = 0x7fffffff;

引用数据类型

引用数据类型通常内存较大,类型复杂。最常见的引用类型是 String 、 Array 等。 引用的含义是,你所创建引用类型的对象,一般是一个类似指针的概念,它会指向真正对象的内存地址。而基本数据类型的对象,则是直接表示值。

两者的区别

要理解它们的区别,需要从 Java 虚拟机的层面上去分析。 JVM 的运行时数据区域包括:

  • 程序计数器:用来记录指令执行到的行号,每个线程都有自己的计数器,所以是线程私有的。

  • Java 虚拟机栈:线程私有,生命周期与线程相同,是用来描述 Java 方法执行的内存模型。

    • 每个方法在执行时都会创建一个栈帧,栈帧里面存储了局部变量表、操作数栈等信息,方法从调用到执行完成,就是对应着一个栈帧在虚拟机栈种入栈到出栈到过程。
    • 栈帧的局部变量表中存放了编译期可知的各种基本数据类型、引用类型的对象等数据。局部变量表所需的内存在编译期完成分配,在方法运行期间不会改变其内存大小。
  • 本地方法栈:线程私有,从概念上与 Java 虚拟机栈用途相似,区别是用于执行 Native 方法。

  • Java 堆:所有线程共享的数据区域,用来存放对象和数组,几乎所有的对象都是在这里分配内存并进行存放的。

  • 方法区:所有线程共享的数据区域。用来存储已加载的类信息、常量、静态变量等数据。

从虚拟机运行时的内存模型上看,关于存储普通对象的(这里指除了一些静态和常量的情况)内存空间有 Java 堆和JVM 栈中的栈帧中的局部变量表。两种的描述是:

虚拟机所管理的内存中最大的一块,在虚拟机启动时创建。此内存唯一的目的就是存放对象。

局部变量表的容量以变量槽(Variable Slot)为最小单位,《Java虚拟机规范》中并没有明确指出一个变量槽应占用的内存空间大小,只是很有引导性地说到每个变量槽都应该能存放一个 boolean、byte、char、short、int、float、reference 或 returnAddress 类型的数据。 Reference类型表示对一个对象实例的引用,至少能通过这个引用做到两件事情: 一是从根据引用直接或间接地查找到对象在Java堆中的数据存放的起始地址或索引。 二是根据引用直接或间接地查找到对象所属数据类型在方法区中存储的类型信息。

暂时将这两者简称为 “堆内存” 和 “栈内存”。堆内存指 Java 堆;栈内存指 JVM 栈的栈帧中的局部变量表。

首先不同的对象在不同的位置上,存储的位置是不同的。栈内存强调 “方法运行时” 、 “局部变量表“ 等关键词都在提醒我们,方法中的局部变量的情况下,是在栈内存中的。

类的成员变量的情况

举例说明:

java public class A {    private int number = 1;    private String string = "A"; }

在类中声明的变量是成员变量,他们没有在方法运行时创建,不会随着某个方法的执行结束而销毁。所以这种情况下不管是基本数据类型还是引用类型都是存放在 Java 堆中的。 在这种情况下,基本数据类型属性 number 的值是值 1 ,它存放在 Java 堆中;引用数据类型 string 变量的值是真实对象 "A" 的内存地址。

方法的局部变量的情况

java public class A {    void log(int no, String content) {        System.out.println(no + content);   } }

这是一个简单的方法,方法需要两个参数,一个基本数据类型和一个引用数据类型。

此时,随着方法的执行,这个方法以一个栈帧的形式进入 JVM 栈中。它的参数 nocontent 是局部变量,存储在局部变量表中。 基本数据类型 no 的值,直接存储在局部变量表中;引用类型 content的值以真实对象的内存地址的形式,存储在局部变量表中。

为什么引用类型要以内存地址的形式存值呢? 从内存大小的角度看,因为内存地址是一个类似 int 或者 long 的值,占用内存小。而如果引用类型把对象本身存到方法运行时的局部变量表的话,那么如果对象很大的情况,就容易造成局部变量表内存不足,而局部变量表的大小在一开始就分配好了,运行时不会发生改变。所以某种角度来说,就是为了更好的利用内存空间,提高效率。

== 运算符

== 运算符在 Java 中比较的是真正的对象实例的值,不管是引用还是基本数据类型。 常见的误区是 == 比较的是引用类型的内存地址,基本数据类型的值。这个说法是不精确的,因为不管是基本数据类型还是引用类型,应该有一个统一的理解。 == 比较的是,不管是基本数据类型还是引用数据类型,它们都指向了一个真实的对象实例。比较的是这个实例是否相同。

基本数据类型比较

在基本数据类型层面,方法中的局部变量和存在堆上的成员变量,很明显他们的内存地址一定不相同,但如果它们的值是相同的,那么它们也相等:

public class A {    int a = 100;    int b = 100; ​    void compare() {        int c = 100;        System.out.println(a == b);        System.out.println(a == c);   }

不管是 a == b 还是 a == c 结果都是 true 。 假设基本数据类型实际上也是指向了一个对象实例,不管是在堆内存中的对象 100 还是在栈内存中的对象 100 ,两个对象一模一样,所以相等。

引用数据类型比较

== 比较引用类型时,比较的是内存地址中的对象实例。

public class A {    String java1 = "java";    String java2 = "java";    String java3 = new String("java");    String java4 = new String("java"); ​    void print() {        System.out.println(java1 == java2);        System.out.println(java2 == java3);        System.out.println(java3 == java4);        System.out.println(java1.equals(java4));   }

这里用对照实验的方式,分为四种情况。

java1java2 ,引用了同一个对象实例 "java" 的内存地址。 java3java4 ,分别创建一个新的对象实例,并引用了各自的内存地址。

结果是:

情况1:java1 == java2 = true 情况2:java2 == java3 = false 情况3:java3 == java4 = false 情况4:java1.equals(java4) = true

情况1,因为 java1java2 都引用了同一个对象实例,所以一定相等。 情况2,因为 java2java3 引用了不同的对象,尽管对象的值相同,但是他们是两个对象,两者引用的内存地址也一定不同,所以不相等。 情况3,与情况2 相同,这里是为了证明, java3java4 创建了不同的对象。 情况4,equals 方法内部比较了对象的值,所以相等。

// in String public boolean equals(Object anObject) {  // 先比较两个对象是否相同 ==    if (this == anObject) {        return true;   }  // 对对象的字符值逐个字符比较    if (anObject instanceof String) {        String anotherString = (String)anObject;        int n = value.length;        if (n == anotherString.value.length) {            char v1[] = value;            char v2[] = anotherString.value;            int i = 0;            while (n-- != 0) {                if (v1[i] != v2[i])                    return false;                i++;           }            return true;       }   }    return false; }

这里也说明了 equals 方法和 == 的区别,equals 方法可以自定义实现判断相等的逻辑; == 只是单纯的比较对象实例本身。

Kotlin

from https://www.kotlincn.net/docs/reference/basic-types.html

在 Kotlin 中,所有东西都是对象,在这个意义上讲我们可以在任何变量上调用成员函数与属性。 一些类型可以有特殊的内部表示——例如,数字、字符以及布尔值可以在运行时表示为原生类型值,但是对于用户来说,它们看起来就像普通的类。 在本节中,我们会描述 Kotlin 中使用的基本类型:数字、字符、布尔值、数组与字符串。

官方文档的描述的含义是,在 Kotlin 中一切对象类型都是相同的,不存在基本数据类型和引用数据类型的概念,但在运行时,一些类型可以与 Java 的基本数据类型对应上,也就是运行时和 int 等基本数据类型的概念是一样的。

数字类型的大小

整数类型:

| 类型 | 大小(位) | 最小值 | 最大值 | | ------- | ----- | ------------------------------------ | ------------------------------------- | | Byte | 8 | -128 | 127 | | Short | 16 | -32768 | 32767 | | Int | 32 | -2,147,483,648 (-2 ^31 ) | 2,147,483,647 (2 ^31 - 1) | | Long | 64 | -9,223,372,036,854,775,808 (-2 ^63 ) | 9,223,372,036,854,775,807 (2 ^63 - 1) |

浮点数类型:

| 类型 | 大小(位) | 有效位 | 指数位 | 十进制数字 | | -------- | ----- | --- | --- | ----- | | Float | 32 | 24 | 8 | 6-7 | | Double | 64 | 53 | 11 | 15-16 |

在 JVM 上的呈现

Kotlin 的数字类型在 JVM 平台上会存储为 Java 对应的基本数据类型,但是有一种特殊的情况:Kotlin 可以将数字类型声明为可空类型:

kotlin val a: Int? = 100

这种情况下,数字类型会在 JVM 平台下表现为 包装类型例如 Integer等。

另外一点不同是,以下情况会出现一个神奇的现象:

```kotlin val a: Int = 100 val boxedA: Int? = a val anotherBoxedA: Int? = a

val b: Int = 10000 val boxedB: Int? = b val anotherBoxedB: Int? = b

println(boxedA === anotherBoxedA) // true println(boxedB === anotherBoxedB) // false ```

从代码上讲, A 和 B 应该结果都是 true ,但是测试了一些值后,发现当数比较小时,是相等的,数变大时,就出现了 false 的情况。 这是因为 JVM 对在 -128 ~ 127 之间的 Integer 类型进行了内存优化,所有对 a 的可空引用实际上都指向了同一个对象实例。而超出这个范围都会创建新的对象实例。

Kotlin 中的 == 和 === 运算符

在 Kotlin 中,相等的类型有两种:

  • 结构相等(== ,检查 equals 方法)
  • 引用相等(===,两个引用是否指向同一个对象)

结构相等

表达式 a == b 实际上被翻译为:

kotlin a?.equals(b) ?: (b === null)

含义是:a 不为空,调用 a 的 equals 函数,否则a 为空时,检查 b 是不是也为空。

也就是说, == 实际上等于调用 equals(Any?): Boolean 函数。如果重写了这个方法自定义相等条件,那么会影响 == 的结果:

```kotlin class B {    override fun equals(other: Any?): Boolean {        return false   } }

fun main() {    val b = B()    val c = b    println(b == c) // false    println(b === c) // true    println(b.equals(c)) // false } ```

引用相等

=== 对应 !==a === b 当且仅当 a 和 b 指向同一个对象时,结果为 true 。

总结

所有运行在 JVM 都遵循基本数据类型与引用数据类型的规则,但是单独从语言层面上来说,Kotlin 屏蔽了底层的概念,更方便开发者去理解。这也是 Kotlin 语言相比 Java 的更高级和更现代的一个体现。 但是从学习知识的角度去看,我们还是要搞清楚虚拟机底层的运行原理,才能更好的使用语言本身的特性。