考虑下面这个例子:

Long l1 = 1L;
Long l2 = 2L;
Long l3 = 3L;
long l4 = 3L;
Long l5 = 1 + 2L;
System.out.println(l3 == 3);
System.out.println(l4 == 3);
System.out.println(l3.equals(3));
System.out.println(l3.equals(l4));
System.out.println(l3.equals(l5));

输出的结果是

true
true
false
true
true

相信这个例子很多初学者都犯过迷糊:l3l4 不都是 3 吗,怎么 l3.equals(3)false 呢。

这里面有很多点可以讲的,我们一个一个来看:

Longlong

Java 里只有两种类型: primitive types 原始类型 和 reference types 引用类型。

null 是一种特殊的类型

规范说明:4.1. The Kinds of Types and Values

原始类型里包括:boolean、byte、short、int、long、char、float、double
引用类型有四种:class、interface、type、array (其中 type 我们平时遇到过的就是泛型 T,详细内容可以查阅规范 4.4. Type Variables)

所以这里,long 是原始类型,Long 是引用类型,这很重要,是接下来讨论的基础。

Boxing ConversionUnboxing Conversion

其实这个就是拆箱装箱,这个知识点应该不陌生吧,就是 Java 会自动帮你把原始数值类型和原始浮点类型转换为对应的引用类型,如 long 转换为 Long

举个栗子:

public void func(Long l) {
    System.out.println(l);
}

func(1L);

这段代码是可以跑起来的,但是如果调用时是这样的 func(1),那么就会报错,因为 1 是整数型,它即便自动装箱也是 Integer 而不是 Long

Objects,变量里存的是什么

规范中,对于 Obejct 有这么一句话:

An object is a class instance or an array.
一个 Object 可以是一个类的实例或者是一个数组 (一个数组其实是一个 Object,不过这是另一个话题了。)
The reference values (often just references) are pointers to these objects, and a special null reference, which refers to no object.
引用值(通常是引用)是指向这些对象的指针和一个特殊的null引用,它不引用任何对象。

有些学过 C++ 的或是有 引用 这个概念的其他语言的同学,可能在这里要犯迷糊了:不是说 Java 里没有引用吗,怎么规范里又提到这个词了。

 注意,C 里面没有引用只有指针,它跟 Java 一样是值传递的。

其实可以这么不严谨地认为:C++ 里的 引用 是动词,Java 里的 引用 是名词。

C++ 里的引用是对一个变量定义一个别名:

int a = 2, int &ra = a;
// a为目标原名称,ra为目标引用名。给ra赋值:ra = 1; 等价于 a = 1;
C++ 引用

Java 里的引用就是一个值,一个指向 对象 的值。

public static void func(String s) {
    s = "bar";
}

String s1 = "foo";
func(s1);
System.out.println(s1); // "foo"

在这里,s1 的值并不是 foo,而是一个指向 其字段value值为 ['f', 'o', 'o']String 实例 的引用。

比如说,再声明一个 String s2 = "foo";,然后在 func(s1); 处下断点,可以看到:

clipboard.png

可以看到,String{@xxx}value:byte[]{@xxx} 都是一样的,因为它们就是同一个对象,s1s2 这两个变量的值是 指向了同一个对象(String{@674})引用

如果我们在 func(String s) 里打断点,会发现在 func(s1) 的情况下,ss1引用值 是一样的。

因为 Java 是值传递,只不过在引用类型的情况下,传递的这个值,就是 引用值

func 内部对这个 s 进行操作后,我们再来看看func内部断点的情况:

public static void func(String s) {
    s = "bar";
    // 断点处,此时 s 的引用值已经变为 String{@674}
    // 即此时的 s 的引用值已经不再是 s1 的引用值,自此它们已经指向的是不同的对象了。
}
由于 String 的设计是不可变的,在一个 String 实例上的任何增删操作都会产生一个新的 String 实例,效果与重新为变量设定新的引用值是一样的。

我们再看看一个原始类型的断点情况:

int i = 0;

clipboard.png

对于原始类型的变量而言,它们的值就是本身。

==equals

== 操作符在规范里其实分了三种情况:

equalsObject 的方法,但是任何类都可以覆写这个方法来实现自定义的实例间判断,比如 Long.equals 就改成了这个样子:

    /**
     * Compares this object to the specified object.  The result is
     * {@code true} if and only if the argument is not
     * {@code null} and is a {@code Long} object that
     * contains the same {@code long} value as this object.
     *
     * @param   obj   the object to compare with.
     * @return  {@code true} if the objects are the same;
     *          {@code false} otherwise.
     */
    public boolean equals(Object obj) {
        if (obj instanceof Long) {
            return value == ((Long)obj).longValue();
        }
        return false;
    }

也就是说,只要待判定对象不是 Long 或其子类,那么就直接返回 false

结合前边讲的 int 在方法调用时会被自动装箱成 Integer(如果参数不显式要求 int 类型),很显然,l3.equals(3) 会直接因为 Integer 3 不是 Long 而返回 false


krun
6.9k 声望11.3k 粉丝