Kiểu dữ liệu (2) | VnCoding

Trong bài hướng dẫn này, chúng ta sẽ tiếp tục discuss về kiểu dữ liệu trong Java như: wrapper class, boxing và unboxing, giá trị default, conversion và promotion.

Wrapper class

Wrapper class là các biểu diễn các kiểu dữ liệu nguyên thủy thành các object. Wrapper class được sử dụng để mô tả các giá trị nguyên thủy khi object được yêu cầu. Ví dụ: Java collection chỉ làm việc với các object. Wrapper class cũng có các phương thức hữu ích. Ví dụ, chúng bao gồm các phương thức cho việc convert kiểu dữ liệu. Việc đặt kiểu dữ liệu nguyên thủy vào wrapper class được gọi là boxing. Quá trình ngược lại gọi là unboxing.
Về nguyên tắc chung, chúng ta sử dụng wrapper class khi chúng ta không thể sử dụng kiểu dữ liệu nguyên thủy. Ngược lại, chúng ta sử dụng kiểu dữ liệu nguyên thủy. Các class wrapper là bất biến. Chúng được tạo 1 lần và không thể thay đổi chúng. Kiểu dữ liệu nguyên thủy nhanh hơn kiểu box (wrapper class). Trong khoa học tính toán và xử lí số lớn, wrapper class có thể khiến cho đạt được 1 performance đáng kể.

Kiểu dữ liệu nguyên thủy
Wrapper Class
Đối số hàm tạo

byte
Byte
byte or String

short
Short
short or String

int
Integer
int or String

long
Long
long or String

float
Float
float, double or String

double
Double
double or String

char
Character
char

boolean
Boolean
boolean or String

Table: Kiểu dữ liệu nguyên thủy và wrapper class tương ứng

Class Integer đóng gói giá trị của kiểu dữ liệu nguyên thủy int thành object. Nó bao gồm các hằng số và các phương thức hữu ích liên quan việc xử lí kiểu int.

package net.vncoding;


public class IntegerWrapper {

    public static void main(String[] args) {
        
        int a = 55;       
        Integer b = new Integer(a);
        
        int c = b.intValue();
        float d = b.floatValue();
        
        String bin = Integer.toBinaryString(a);
        String hex = Integer.toHexString(a);
        String oct = Integer.toOctalString(a);        
        
        System.out.println(a);
        System.out.println(b);
        System.out.println(c);
        System.out.println(d);
        
        System.out.println(bin);
        System.out.println(hex);
        System.out.println(oct);            
    }
}

Ví dụ minh họa sử dụng Integer wrapper class.

int a = 55;

Dòng lệnh này tạo biến số nguyên kiểu nguyên thủy int.

Integer b = new Integer(a);

Đối tượng kiểu Integer wrapper được tạo từ kiểu dữ liệu nguyên thủy int.

int c = b.intValue();
float d = b.floatValue();

Phương thức intValue() convert object Integer thành kiểu dữ liệu nguyên thủy int. Phương thức floatValue() trả về kiểu dữ liệu float.

String bin = Integer.toBinaryString(a);
String hex = Integer.toHexString(a);
String oct = Integer.toOctalString(a); 

3 phương thức trả về số nguyên kiểu binary, hexa, octal.

Java - Integer wrapper class

Collection là công cụ rất mạnh giành cho làm việc với nhóm các object. Kiểu dữ liệu nguyên thủy không nằm trong Java collection. Sau khi chúng ta đóng gói giá trị nguyên thủy, chúng ta có thể đặt chúng vào collection.

package net.vncoding;


import java.util.ArrayList;
import java.util.List;

public class Numbers {

    public static void main(String[] args) {
        
        List<Number> ls = new ArrayList<>();
        
        ls.add(1342341);
        ls.add(new Float(34.56));
        ls.add(235.242);
        ls.add(new Byte("102"));
        ls.add(new Short("1245"));
       
        for (Number n : ls) {
            
            System.out.println(n.getClass());
            System.out.println(n);
        }
    }
}

Trong ví dụ này, chúng ta đặt nhiều số vào ArrayList. ArrayList là dữ liệu kiểu dynamic, có thể thay đổi được kích thước.

List<Number> ls = new ArrayList<>();

Đối tượng ArrayList được tạo. Trong dấu ngoặc nhọn <>, chúng ta chỉ định kiểu dữ liệu mà container lưu trữ.
Number là class cơ sở trừu tượng (abstract base class) cho 5 số kiểu dữ liệu nguyên thủy trong Java.

ls.add(1342341);
ls.add(new Float(34.56));
ls.add(235.242);
ls.add(new Byte("102"));
ls.add(new Short("1245"));

Chúng ta add 5 số vào collection. Chú ý rằng, giá trị nguyên và dấu phẩy động không được đóng gói. Do compiler có thể auto đóng gói kiểu dữ liệu int và double.

for (Number n : ls) {
    System.out.println(n.getClass());
    System.out.println(n);
}

Chúng ta duyệt container và in tên class và giá trị của mỗi phần tử.

Kết quả:

Java - Collection

Boxing

Convert từ kiểu dữ liệu nguyên thủy sang kiểu object, được gọi là boxing. Unboxing là việc convert ngược lại, từ object sang kiểu dữ liệu nguyên thủy.

package net.vncoding;


public class BoxingUnboxing {

    public static void main(String[] args) {
        
        long a = 124235L;        
        
        Long b = new Long(a);
        long c = b.longValue();
        
        System.out.println(c);
    }
}

Trong ví dụ này, chúng ta đóng gói giá trị kiểu long thành object long và ngược lại.

Long b = new Long(a);

Dòng này thực hiện boxing

long c = b.longValue();

Dòng này thực hiện unboxing.

Autoboxing

Java SE 5 đã giới thiệu autoboxing. Autoboxing là auto convert giữa kiểu dữ liệu nguyên thủy và các object wrapper class tương ứng. Autoboxing giúp cho việc lập trình dễ ràng hơn. Lập trình viên không cần thực hiện convert thủ công.
Autoboxing và unboxing được thực hiện khi 1 giá trị là kiểu dữ liệu nguyên thủy và còn lại là wrapper class trong các trường hợp sau:
– Phép gán
– Truyền tham số cho phương thức
– Trả về giá trị từ phương thức
– Phép toán so sánh
– Phép toán số học

Integer i = new Integer(50);

if (i < 100) {
   ...
}

Trong ngoặc () của câu lệnh if, đối tượng Integer được so sánh với kiểu int (100). Đối tượng Integer được auto convert thành kiểu dữ liệu nguyên thủy int. Quá trình này được gọi là auto unboxing.

Autoboxing.java

package net.vncoding;

public class Autoboxing {
    
    private static int cube(int x) {
        
        return x * x * x;
    }

    public static void main(String[] args) {
        
        Integer i = 10;
        int j = i;
        
        System.out.println(i);
        System.out.println(j);        
        
        Integer a = cube(i);
        System.out.println(a);    
    } 
}

Automatic boxing and automatic unboxing được mình họa như ví dụ trên.

Integer i = 10;

Trình biên dịch Java tự động thực hiện boxing dòng code này. Giá trị nguyên (10) được boxing tự động thành kiểu Integer.

int j = i;

Đây là quá trình unboxing. Convert từ kiểu Integer thành kiểu dữ liệu nguyên thủy int.

Integer a = cube(i);

Khi chúng ta truyền đôi số kiểu Integer vào phương thức cube(), auto unboxing được thực hiện. Còn khi chúng ta trả về giá trị, auto boxing được thực hiện bởi vì kiểu int được convert thành kiểu Integer.
Java không support chồng toán tử (Operator Overloading), khác với C++. Khi chúng ta thực hiện phép toán số học trên wrapper class, auto boxing được thực hiện bởi trình biên dịch.

Autoboxing2.java

package net.vncoding;

public class Autoboxing2 {

    public static void main(String[] args) {
        
        Integer a = new Integer(5);
        Integer b = new Integer(7);
        
        Integer add = a + b;
        Integer mul = a * b;
        
        System.out.println(add);
        System.out.println(mul);    
    }
}

Chúng ta có 2 giá trị Integer. Thực hiện phép cộng và nhân a và b.

Integer add = a + b;
Integer mul = a * b;

Khác với Ruby, C#, Phython, C++, Java không support chồng toán tử. Khi thực thi 2 dòng code này, trình biên dịch gọi phương thức intValue() và convert wrapper class thành kiểu int, sau đó convert int thành wrapper class bởi việc gọi phương thức valueOf().

Autoboxing and object interning

Object intering là lưu trữ chỉ có một bản sao của mỗi đối tượng khác biệt. Đối tượng phải không thay đổi. Các đối tượng khác biệt được lưu trữ trong intern pool. Trong Java, khi giá trị nguyên thủy được boxing vào một đối tượng wrapper, các giá trị nhất định (bất kỳ boolean, bất kỳ byte, bất kỳ char từ 0 đến 127, và bất kỳ short hoặc int giữa -128 và 127) được interned, và bất kỳ hai boxing conversion của một trong các giá trị này được đảm bảo sẽ dẫn đến cùng một đối tượng. Theo đặc tả ngôn ngữ Java, đây là các dải tối thiểu. Vì vậy, hành vi này phụ thuộc vào việc thực hiện. Đối tượng intering tiết kiệm thời gian và không gian. Các đối tượng thu được từ literals, autoboxing và Integer.valueOf () là các đối tượng được intern trong khi những object được xây dựng với toán tử new luôn là những object riêng biệt.

Object intering có một số kết quả quan trọng khi so sánh các wrapper class. Toán tử == so sánh định danh tham chiếu của các đối tượng trong khi phương thức equals() so sánh các giá trị.

Autoboxing3.java

package net.vncoding;

public class Autoboxing3 {

    public static void main(String[] args) {
        
        Integer a = 5; // new Integer(5);
        Integer b = 5; // new Integer(5);         
        
        System.out.println(a == b);
        System.out.println(a.equals(b));
        System.out.println(a.compareTo(b));
        
        Integer c = 155; 
        Integer d = 155;              
                
        System.out.println(c == d);
        System.out.println(c.equals(d));
        System.out.println(c.compareTo(d));    
    }
}

Ví dụ trên, compare 2 object Integer.

Integer a = 5; // new Integer(5);
Integer b = 5; // new Integer(5);

2 số nguyên được boxing vào wrapper class

System.out.println(a == b);
System.out.println(a.equals(b));
System.out.println(a.compareTo(b));

Có 3 cách khác nhau được sử dụng để so sánh các giá trị. Toán tử == so sánh định danh tham chiếu của hai loại boxing. Bởi vì các đối tượng interning, kết quả so sánh là true. Nếu chúng ta sử dụng toán tử new, sẽ tạo ra hai đối tượng riêng biệt và toán tử == sẽ trả về false. Phương thức equals() so sánh giá trị của hai đối tượng Integer. Nó trả về một kiểu boolean true hoặc false (trong ví dụ này, trả về true) Cuối cùng, phương thức compareTo() cũng so sánh giá trị của hai đối tượng. Nó trả về giá trị 0 nếu Integer a này bằng đối số Integer b; trả về giá trị nhỏ hơn 0 nếu Integer a nhỏ hơn số nguyên Integer b; và một giá trị lớn hơn 0 nếu Integer a lớn hơn số nguyên Integer b.

Integer c = 155; 
Integer d = 155; 

Chúng ta có hai loại boxing khác. Tuy nhiên, các giá trị này lớn hơn giá trị maximum (127); do đó, hai đối tượng khác biệt được tạo ra. Lần này, toán tử == trả về false.

Kết quả:
true
true
0
false
true
0

Kiểu Java Null

Java có một kiểu null đặc biệt. Kiểu không có tên. Do đó, không thể khai báo một biến kiểu null hoặc để ép kiểu thành kiểu null. Kiểu null đại diện cho một tham chiếu null, không tham chiếu đến bất kỳ đối tượng. Giá trị null là giá trị mặc định của các biến kiểu tham chiếu. Không thể được gán null cho các kiểu nguyên thủy (char, int, char,…)

Trong các ngữ cảnh khác nhau, null có nghĩa là sự vắng mặt của một đối tượng, một giá trị chưa biết, hoặc một trạng thái chưa được khởi tạo.

NullType.java

package net.vncoding;
import java.util.Random;

public class NullType {
    
    private static String getName() {
        
        Random r = new Random();
        boolean n = r.nextBoolean();
        
        if (n == true) {
        
            return "John";
        } else {
            
            return null;
        }
    }

    public static void main(String[] args) {
        
        String name = getName();
        
        System.out.println(name);

        System.out.println(null == null);
        
        if ("John".equals(name)) {
            
            System.out.println("His name is John");
        }        
    }
} 

Ví dụ này mô tả về cách sử dụng null trong Java.

private static String getName() {
    
    Random r = new Random();
    boolean n = r.nextBoolean();
    
    if (n == true) {
    
        return "John";
    } else {
        
        return null;
    }
}

Trong phương thức getName (), chúng ta mô phỏng một tình huống mà một phương thức có thể trả lại một giá trị null.

System.out.println(null == null);

Chúng ta so sánh hai giá trị null. Biểu thức trả về true.

if ("John".equals(name)) {
    
    System.out.println("His name is John");
}   

Chúng ta so sánh biến name với chuỗi “John”. Chú ý rằng chúng ta gọi phương thức equals() bằng chuỗi “John”. Điều này là bởi vì nếu biến tên bằng null, gọi phương thức này sẽ dẫn đến NullPointerException.

Kết quả:
John
true
His name is John

Giá trị defaul trong Java

Các trường chưa được khởi tạo được set giá trị default bởi trình biên dịch. Các trường cuối cùng và các biến local phải được khởi tạo bởi Developer.

Bảng dưới đây hiển thị các giá trị mặc định cho các loại khác nhau.

Kiểu dữ liệu
Giá trị default

byte
0

char
‘\u0000’

short
0

int
0

long
0L

float
0f

double
0d

Object
null

boolean
false

 

Ví dụ tiếp theo sẽ in các giá trị default của các biến instance chưa được khởi tạo. Biến instance là một biến được định nghĩa trong một class mà mỗi đối tượng được khởi tạo của class có một bản copy riêng biệt.

package net.vncoding;

public class DefaultValues {
    
    static byte b;
    static char c;
    static short s;
    static int i;
    static float f;
    static double d;
    static String str;
    static Object o;
    
    public static void main(String[] args) {
        
        System.out.println(b);
        System.out.println(c);
        System.out.println(s);
        System.out.println(i);
        System.out.println(f);
        System.out.println(d);
        System.out.println(str);
        System.out.println(o);        
    }
}

Trong ví dụ, chúng ta khai báo 8 biến trong class. Chúng không được khởi tạo. Trình biên dịch sẽ đặt một giá trị mặc định cho biến.

static byte b;
static char c;
static short s;
static int i;
...

Đây là những biến instance; chúng được khai báo bên ngoài các phương pháp. Các biến được khai báo static vì chúng được truy cập từ một phương thức static main(). (Sau này trong hướng dẫn chúng ta sẽ nói về các biến static và ví dụ.)

Kết quả:
0

0
0
0.0
0.0
null
null

Ép kiểu kiểu dữ liệu trong Java

Chúng tôi thường làm việc với nhiều loại dữ liệu cùng một lúc. Chuyển đổi một kiểu dữ liệu sang một kiểu dữ liệu khác là công việc phổ biến trong lập trình. Thuật ngữ chuyển đổi loại chỉ sự thay đổi của một thực thể của một loại dữ liệu thành kiểu dữ liệu khác. Trong phần này, chúng ta sẽ giải quyết các chuyển đổi của các kiểu dữ liệu nguyên thủy. Chuyển đổi loại tham chiếu sẽ được đề cập đến trong chương sau. Các quy tắc cho chuyển đổi phức tạp; chúng được chỉ định trong chương 5 của đặc tả ngôn ngữ Java.

Có hai loại chuyển đổi: ngầm định(implicit) và rõ ràng(explicit). Sự chuyển đổi kiểu ẩn (implicit type conversion), còn gọi là sự cưỡng ép (coercion), là sự chuyển đổi kiểu tự động bởi trình biên dịch. Trong chuyển đổi rõ ràng, lập trình viên trực tiếp xác định loại chuyển đổi bên trong một cặp dấu ngoặc tròn. Chuyển đổi rõ ràng được gọi là type casting.

Các chuyển đổi xảy ra trong các ngữ cảnh khác nhau: phép gán, biểu thức, hoặc lời gọi phương thức.

int x = 456;
long y = 34523L;
float z = 3.455f;
double w = 6354.3425d;

Trong 4 phép gán này, không có ép kiểu. Mỗi biến được gán với 1 giá trị.

int x = 345;
long y = x;

float m = 22.3354f;
double n = m;

Trong đoạn code này, 2 phép ép kiểu được trình biên dịch Java thực hiện một cách ngầm ẩn. Việc gán một biến kiểu nhỏ hơn cho một biến kiểu lớn hơn là an toàn, vì không bị mất dữ liệu. Kiểu chuyển đổi này được gọi là chuyển đổi ngầm định mở rộng(implicit widening conversion).

long x = 345;
int y = (int) x;

double m = 22.3354d;
float n = (float) m;

Gán các biến kiểu lớn hơn sang kiểu nhỏ hơn không hợp lệ trong Java. Ngay cả khi các giá trị của chúng phù hợp với phạm vi của loại nhỏ hơn. Trong trường hợp này có thể mất dữ liệu. Để thực hiện các phép gán này, chúng ta phải sử dụng ép kiểu casting. Khi sử dụng ép kiểu casting, cần nhận thức rõ về khả năng mất dữ liệu.Loại chuyển đổi này được gọi là chuyển đổi rõ ràng phạm vị hẹp (explicit narrowing conversion).

byte a = 123;
short b = 23532;

Trong trường hợp này, chúng ta giải quyết việc chuyển đổi dữ liệu cụ thể. 123 và 23532 là giá trị nguyên, các biến a, b có dạng byte và short. Có thể sử dụng casting, nhưng nó không phải là bắt buộc. Các giá trị nguyên có thể được mô tả trong các biến bên trái phép gán. Chúng ta giải quyết với implicit narrowing conversion.

private static byte calc(byte x) {
...
}
byte b = calc((byte) 5);

Quy tắc trên chỉ áp dụng cho các phép gán. Khi chúng tôi truyền một giá trị nguyên 5 cho phương thức calc() với tham số kiểu byte. Chúng ta phải thực hiện casting.

Java numeric promotions

Numeric promotion là kiểu đặc biệt của chuyển đổi dữ liệu ngầm định. Điều này diễn ra trong phép toán số học.
Các numeric promotion được sử dụng trong 1 phép toán có nhiều kiểu dữ liệu khác nhau, để convert các kiểu dữ liệu thấp thành thành kiểu cao hơn.

int x = 3;
double y = 2.5;
double z = x + y;

Trong dòng code thứ 3, chúng ta có một biểu thức cộng 2 số. Toán hạng x là kiểu int, toán hạng y là kiểu double. Trình biên dịch convert số int sang double và thực hiện cộng hai số. Kết quả của phép cộng là kiểu double. Đó là trường hợp của implicit widening primitive conversion.

byte a = 120;
a = a + 1; // compiler error

Dòng code này gây ra một lỗi compiler error khi biên dịch. Ở phía bên phải của dòng code thứ 2, chúng ta có một byte biến a và một giá trị nguyên 1. Biến a được convert thành số nguyên và thực hiện cộng 2 số nguyên (a + 1). Kết quả là một số nguyên. Sau đó, trình biên dịch cố gán kết quả nguyên cho một biến a (kiểu byte). Việc gán các kiểu dữ liệu lớn hơn cho loại nhỏ hơn là không được phép trong Java trừ khi sử dụng casting. Vì vậy, chúng ta nhận được lỗi complier error.

byte a = 120;
a = (byte) (a + 1);

Đoạn code này biên dịch không lỗi. Lưu ý cách sử dụng dấu ngoặc tròn cho biểu thức a + 1. Toán tử casting (byte) có độ ưu tiên cao hơn toán tử +. Việc này, giúp casting kiểu số nguyên thành kiểu byte.

byte a = 120;
a += 5;

Toán tử += convert dữ liệu ngầm định.

short r = 21;
short s = (short) -r;

Nếu sử dụng toán tử + hoặc -, phép toán -r được convert về kiểu int. Do đó, chúng ta phải sử dụng casting để convert int thành kiểu short nếu không compiler show error.

byte u = 100;
byte v = u++;

Trong trường hợp sử dụng toán tử ++ và –, việc convert dữ liệu không được thực hiện. Do đó, chúng ta không cần sử dụng casting.

Java boxing, unboxing conversions

– Boxing conversion convert dữ liệu kiểu nguyên thủy sang biểu thức tương ứng kiểu wrapper.
– UnBoxing conversion convert dữ liệu kiểu wrapper thành kiểu dữ liệu nguyên thủy.

Byte b = 124;
byte c = b;

Trong dòng code đầu tiên, boxing conversion tự động được trình biên dịch Java thực hiện. Trong dòng thứ 2, unboxing conversion được thực hiện.

private static String checkAge(Short age) {
...
}
String r = checkAge((short) 5);

Chúng ta có boxing conversion trong ngữ cảnh gọi method. Giá trị nguyên 5 được truyền vào phương thức với tham số kiểu wrapper Short. Chúng ta phải sử dụng casting (short)5 để ép kiểu từ số int thành short. Cuối cùng, trình biên dịch auto boxing để convert từ kiểu nguyên thủy short thành wrapper Short.

Boolean gameOver = new Boolean("true");
if (gameOver) {
    System.out.println("The game is over");
}

Đây là ví dụ unboxing conversion. Trong biểu thức if, wrapper Boolean được convert thành kiểu dữ liệu nguyên thủy boolean.

Java string conversions

Thực hiện string conversion giữa các số và string là rất phổ biến trong lập trình. Việc sử dụng casting là không được phép bởi vì các string và các kiểu nguyên thuỷ là những kiểu khác nhau. Có một số phương pháp để thực hiện chuyển đổi string. Ngoài ra string conversion tự động khi sử dụng toán tử +.

Thông tin thêm về string conversion sẽ được đề cập đến trong chương String của bài hướng dẫn học lập trình Java này.

String s = (String) 15; // compilation error
int i = (int) "25"; // compilation error

Không thể casting giữa các số và String. Thay vào đó, chúng ta có nhiều phương pháp để chuyển đổi giữa các số và string.

short age = Short.parseShort("35");
int salary = Integer.parseInt("2400");
float height = Float.parseFloat("172.34");
double weight = Double.parseDouble("55.6");

Phương thức parse convert wrapper thành kiểu dữ liệu nguyên thủy.

Short age = Short.valueOf("35");
Integer salary = Integer.valueOf("2400");
Float height = Float.valueOf("172.34");
Double weight = Double.valueOf("55.6");

Phương thức valueOf() trả về các lớp wrapper từ các loại nguyên thủy.

int age = 17;
double weight = 55.3;
String v1 = String.valueOf(age);
String v2 = String.valueOf(weight);

Class String có một phương thức valueOf() để convert các kiểu dữ liệu sang các string.

Việc auto conversion diễn ra khi sử dụng toán tử cộng (+) một toán hạng là một string, toán hạng kia không phải là một string. Toán hạng không phải là string được chuyển thành string.

AutomaticStringConversion.java

package net.vncoding;

public class AutomaticStringConversion {

    public static void main(String[] args) {

        String name = "VnCoding";
        short age = 4;

        System.out.println(name + " is " +  age + " years old.\n");
    }
}

Trong ví dụ, chúng ta có một kiểu dữ liệu String và một kiểu dữ liệu short. Hai loại được ghép bằng toán tử + vào một string.

System.out.println(name + " is " +  age + " years old.");

Trong biểu thức này, biến số age được convert thành string.

Kết quả:
VnCoding is 4 years old.

Trong phần này của hướng dẫn Java, mình đã giới thiệu lớp wrapper, boxing và unboxing, default value, conversion và promotion.