+8

Tại sao 1 == 1 lại đúng trong khi 1000 == 1000 thì sai với Integer wrappers trong Java?

Mayfest2023

Hi folks!

Tuần này mình lại sml với bọn Java (unit test), thiết nghĩ chỉ viết mỗi unit test sẽ sớm bị "out trình" Java nên mình lân la trên Google, StackOverflow tìm vài món Java hack não để luyện tập, và mình vô tình lướt qua 1 câu hỏi thú vị (như tiêu đề). Bài viết này mình sẽ chia sẻ với bạn đọc những thứ hay ho của Java mà mình học được được sau khi giải ngố câu hỏi trên.

1. Đặt vấn đề

Để làm rõ vấn đề trong câu hỏi chính của bài viết, cùng mình xem qua đoạn code Java bên dưới

public static void main(String[] args) {
        Integer a = 1;
        Integer b = 1;
        Integer e = new Integer(1);
        Integer c = 1000;
        Integer d = 1000;
        System.out.println(a == b);
        System.out.println(a == e);
        System.out.println(c == d);
    }

Và kết quả sau khi chạy màn main

true
false
false

Để có thể giải thích kết quả trên, trước tiên cùng mình ôn lại một số kiến thức Java căn bản

2. So sánh Object trong Java

Để so sánh 2 objects trong Java, chắc hẳn chúng ta đều biết không nên sử dụng toán tử ==, toán tử này so sánh tham chiếu của 2 objects thay vì giá trị của chúng.

Tham chiếu là địa chỉ bộ nhớ mà tại đó các objects sẽ được lưu trữ

Ví dụ:

Integer a = 1000;
Integer b = 1000;
System.out.println(a == b); // false, compares their references
System.out.println(a.equals(b)); // true, compare their values

Đoạn code đầu bài sử dụng toán tử ==. Dangerous!!!

3. Wrapper classes & Auto boxing

Trong Java, chúng ta có 2 cách để thể hiện giá trị integer: sử dụng primitive types hoặc wrapper classes.

integer a = 100; // primitive types
Integer b = 100; // wrapper class: Integer

primitive types sẽ đơn giản là lưu giá trị một cách trực tiếp, như int, float, double,...

wrapper classes thì lại là Object trong Java, nó sẽ bọc giá trị của chúng ta trong 1 object, như Integer là object và bên trong sẽ là một giá trị int.

Một trong những lợi ích của wrapper classes mà mình hay trải nghiệm đó là "đẻ" ra NullPointerException 🤣 Đùa 1 tí, primitive types và wrapper classes sẽ giúp chúng ta giải quyết vấn đề "tham chiếu, tham trị" khi truyền đối số vào method (và nhiều lợi ích khác, các bạn có thể Google để tìm hiểu thêm).

Auto boxing là một cơ chế của Java compilier, compilier sẽ tự động convert primitive types sang wrapper class tương ứng. Ví dụ integer thành Integer, double thành Double, cách dùng auto boxing đơn giản là gán trực tiếp primitive types value cho wrapper classes

Integer a = 200;
// behind the scenes
// Integer a = Integer.valueOf(200);

Đến đây, chúng ta cùng quay lại đoạn code đầu bài, phân tích một chút

public static void main(String[] args) {
        Integer a = 1; // autoboxing
        Integer b = 1; // autoboxing
        Integer e = new Integer(1); // using new keyword, not autoboxing
        Integer c = 1000; // autoboxing
        Integer d = 1000; // autoboxing
        System.out.println(a == b); // compare their references
        System.out.println(a == e); // compare their references
        System.out.println(c == d); // compare their references
    }

Dựa vào kiến thức đầu bài đến giờ, chúng ta dễ dàng xác định được a == b, c == d đều cho kết quả FALSE, nhưng kết quả của chương trình hơi "cấn cấn" nhỉ. Đó là vì mình thiếu 1 kiến thức thú vị bên dưới.

4. Integer caching

"Object creation is expensive" bởi vậy nên Integer class có thứ gọi là integer caching, một kĩ thuật để optimize performance, giúp chúng ta tái sử dụng lại các Integer objects thay vì phải tạo mới object mỗi lần sử dụng. Trước tiên, cùng xem implement của method Integer.valueOf (method behind the scenes của autoboxing)

    /**
     * Returns an {@code Integer} instance representing the specified
     * {@code int} value.  If a new {@code Integer} instance is not
     * required, this method should generally be used in preference to
     * the constructor {@link #Integer(int)}, as this method is likely
##      * to yield significantly better space and time performance by
     * caching frequently requested values.
     *
     * This method will always cache values in the range -128 to 127,
     * inclusive, and may cache other values outside of this range.
     *
     * @param  i an {@code int} value.
     * @return an {@code Integer} instance representing {@code i}.
     * @since  1.5
     */
    @HotSpotIntrinsicCandidate
    public static Integer valueOf(int i) {
        if (i >= IntegerCache.low && i <= IntegerCache.high)
            return IntegerCache.cache[i + (-IntegerCache.low)];
        return new Integer(i);
    }

Sự xuất hiện của class IntegerCache và javaDoc đã thể hiện rõ ràng, Integer dùng class IntegerCache để cache lại các objects, cụ thể có range value từ -128 đến 127. Vì vậy, ab - 2 objects dùng autoboxing sẽ trỏ đến cùng 1 object (chứa value int 1) vì giá trị int của chúng nằm trong range value caching, do đó khi so sánh == cho ra giá trị TRUE.

Đối với e, do sử dụng new keyword, nó sẽ được tạo riêng 1 object mới và vì thế nên so sánh == với a sẽ cho ra FALSE, tương tự khi so sánh b == e

Chúng ta có thể dùng
-Djava.lang.Integer.IntegerCache.high=<size>
Hoặc
-XX:AutoBoxCacheMax=<size>
Để tăng giới hạn trên cho cache, ví dụ size = 1000, Integer sẽ cách object từ -127 đến 1000. Hiện tại chưa có config để tăng giới hạn dưới. 

5. Kết

Vậy là chúng ta vừa ôn lại một vài kiến thức thú vị trong Java, đặc biệt là kiến thức về so sánh 2 objects, chúng ta (luôn) nên dùng equals thay vì ==, trừ khi giá trị của chúng nằm trong vùng cache đối với Integer. Hiện tại Double, Float chưa có cơ chế cache này.

6. Tham khảo

Autoboxing: https://docs.oracle.com/javase/tutorial/java/data/autoboxing.html

Integer caching: https://howtodoinjava.com/java-examples/internal-cache-wrapper-classes/

Hẹn gặp lại các bạn trong những bài sau. Cheerr!


All rights reserved

Viblo
Hãy đăng ký một tài khoản Viblo để nhận được nhiều bài viết thú vị hơn.
Đăng kí