Author Archives: Phở Code

Rust – Hello World

Trong phần này chúng ta sẽ tìm hiểu cách viết một chương trình bằng Rust. File code Rust có phần mở rộng là .rs nên đầu tiên chúng ta tạo một file có tên hello_world.rs và gõ vào đoạn code sau:

fn main() {
println!("Hello World");
}

Tiếp theo chúng ta biên dịch file hello_world.rs này bằng cách mở command prompt (cmd) lên, chuyển thư mục hiện hành (bằng lệnh cd) đến thư mục chứa file hello_world.rs và gõ lệnh rustc hello_world.rs để biên dịch:

Lệnh rustc sẽ biên dịch và nếu biên dịch thành công thì tạo ra 2 file có tên hello_world.pdbhello_world.exe trên Windows hoặc hello_world trên Linux. Nếu có lỗi thì Rust sẽ báo lỗi biên dịch và không tạo 2 file trên.

Sau đó chúng ta có thể chạy bằng cách gõ tên file hello_world hoặc ./hello_world (trên Linux) là được:

Chúng ta sẽ nhận được kết quả là một dòng chữ “Hello World”. 

Nếu bạn đã từng làm việc với C/Java/C# thì bạn sẽ thấy đoạn code trên khá quen thuộc. Một chương trình viết bằng Rust bắt đầu chạy ở một điểm xuất phát có tên là hàm main(), tất cả mọi thứ nằm trong cặp dấu {} phía sau fn main() sẽ chạy trước:

fn main() {

}

Chúng ta sẽ tìm hiểu thêm về hàm sau.

Bên trong hàm main() này chỉ có một dòng code duy nhất là println!("Hello World"); chuỗi “Hello World” được đặt bên trong một macro có tên println!(); lưu ý dấu chấm than ! ngay phía sau println cho biết đây là một macro chứ không phải một hàm, chúng ta sẽ tìm hiểu về macro sau, macro printlln!() có chức năng in chuỗi nằm bên trong nó ra màn hình console.

Rust – Cài đặt

Để có thể cài đặt Rust thì chúng ta cần có Microsoft Visual C++ Build Tools – trình biên dịch C++ của Visual Studio ít nhất là phiên bản 2015. Nếu trong máy bạn không có thì khi cài đặt, Rust sẽ báo cho bạn biết là máy bạn không có rồi đưa đường link cho bạn tải, hoặc bạn cũng có thể cài C++ Build Tools bằng cách cài C++ từ Visual Studio cũng được.

Windows

Đối với Windows thì chúng ta lên trang chủ của Rust là https://www.rust-lang.org, sau đó nhấn vào nút install để tải về file cài đặt Rust có tên rustup-init.exe.

Sau đó click chuột vào file rustup-init.exe để cài đăt, Rust sẽ hiển thị một màn hình command prompt và đưa ra 3 lựa chọn để chúng ta cài đặt, ba lựa chọn này cũng không có gì đặc sắc hết nên chúng ta cứ bấm enter để rust cài đặt như bình thường luôn là được.

Linux và Mac OS X

Đối với các hệ điều hành Linux và Mac OS thì chúng ta có thể cài bằng cách chạy lệnh sau:

curl -sSL https://static.rust-lang.org/rustup.sh | sh

Xem phiên bản Rust

Hiện tại khi viết bài viết này Rust đang có phiên bản là 1.20. Bạn có thể xem phiên bản Rust trong máy mình bằng cách chạy lệnh rustc -V

Sau khi cài Rust xong thì đường dẫn đến thư mục cài đặt Rust trong Windows là C:\Users\<tên_user>\.cargo còn trong Linux là /usr/local/lib/rustlib.

Rust – Giới thiệu

Rust là một ngôn ngữ lập trình được phát triển bởi Mozilla Research và sau đó chủ yếu được phát triển bởi cộng đồng mã nguồn mở. Cha đẻ của Rust là một nhà thiết kế ngôn ngữ Graydon Hoare, Rust được giới thiệu lần đầu vào năm 2010. Rust được xây dựng dựa trên những nguyên lý rõ ràng và vững chắc, Rust chứa đựng những khả năng cũng có trong C và C++, do đó Rust có tốc độ chạy ngang hàng với C++, nhưng lại cho phép lập trình viên viết code an toàn hơn vì không phải đụng chạm nhiều đến xử lý bộ nhớ. Ngoài ra Rust còn có các chức năng hỗ trợ chạy nhiều tiến trình song song trên các máy tính đa lõi.

Những ưu điểm của Rust

Một trong những ưu điểm của C++ là tốc độ thực thi nhanh bởi vì C++ cho phép lập trình viên khả năng điều khiển bộ nhớ của máy tính, tuy nhiên điều này lại tăng khả năng crash của ứng dụng do vì việc cấp phát, thu hồi và sử dụng bộ nhớ là một công việc phức tạp.

Đối lập với các ngôn ngữ như C++ là các ngôn ngữ như Python, Ruby…v.v đây vốn là các ngôn ngữ đơn giản, dễ sử dụng và an toàn cho lập trình viên, bởi vì các ngôn ngữ này hầu như đã làm tất cả mọi thứ cho người lập trình, tuy nhiên lại không cho phép lập trình viên thực hiện các thao tác ở cấp độ thấp như điều khiển bộ nhớ, do đó thường thì các ứng dụng viết bằng các ngôn ngữ này chạy khá chậm.

Rust là một ngôn ngữ được tạo ra để giải quyết cả hai vấn đề trên với các tính năng:

  • Độ ổn định cao nhờ vào hệ thống kiểm tra kiểu dữ liệu chặt chẽ
  • Cho phép truy cập sâu vào bên trong hệ thống nhưng vẫn nằm trong tầm kiểm soát

Rust cho phép chúng ta quy định chính xác cách mà các giá trị được lưu trong bộ nhớ và cách bộ nhớ được sử dụng bởi hệ điều hành như thế nào.Chính vì vậy mà Rust làm cho các ứng dụng vừa chạy với tốc độ cao vừa nằm trong vùng an toàn.

Ngoài ra Rust loại bỏ tính năng “máy thu dọn rác” xuất hiện trong các ngôn ngữ cấp cao như Java, C#, Python… mặc dù trên thực tế người ta đã từng định thiết kế một cỗ máy thu gom rác cho Rust.

Các điểm mạnh của Rust

Rust không phải là một ngôn ngữ mang tính cách mạng với các tính năng hiện đại, tuy nhiên Rust vẫn mang trong mình những kĩ thuật đã được chứng minh trong các ngôn ngữ lập trình xưa cũ mà phần lớn là được phát triển dựa trên C++.

Rust được phát triển là một ngôn ngữ lập trình đa mục đích, đa mô hình, bao gồm mô hình lập trình mệnh lệnh, lập trình cấu trúc và lập trình hướng đối tượng. Ngoài ra ngôn ngữ này cũng kế thừa khá nhiều từ các ngôn ngữ lập trình chức năng và các kỹ thuật cấp cao trong lập trình song song.

Tính kiểm tra kiểu dữ liệu của Rust rất chặt chẽ. Trong một số trường hợp, lập trình viên không bị gò bó trong việc khai báo kiểu dữ liệu ngay khi khai báo biến như trong C++/Java, thay vào đó trình biên dịch có thể tự động nội suy ra kiểu dữ liệu dựa trên giá trị khởi tạo.

Một vấn đề khá lớn của C/C++ là hay bị leak bộ nhớ, do đó người lập trình C/C++ hay gặp những lỗi về con trỏ, tràn bộ nhớ đệm, con trỏ null…v.v Tất cả những vấn đề đó đều sẽ được giải quyết tự động trong trình biên dịch của Rust bằng cách tự động phát hiện và báo lỗi trước.

Rust sử dụng một kiến trúc khá tương đồng với Erlang để thực thi các tác vụ song song. Nói sơ qua thì Rust sẽ chạy các tiến trình trong các luồng, các luồng này sẽ không sử dụng chung bộ nhớ heap mà trao đổi thông qua các channel.

Rust cũng được thiết kế để có tính portable như C++, tức là có thể chạy trên nhiều nền tảng phần mềm và phần cứng. Hiện tại Rust có thể chạy trên Linux, Mac OS X, Windows, FreeBSD, Android và iOS. Thậm chí Rust cũng có thể gọi các đoạn code viết bằng C, và bản thân C cũng có thể gọi code viết bằng Rust.

Ứng dụng của Rust

Mặc dù Rust được thiết kế để viết ứng dụng hệ thống là chính, tuy nhiên khả năng ứng dụng của ngôn ngữ này cũng rất rộng, Rust có thể được dùng để viết các loại ứng dụng sau đây:

  • Trình biên dịch
  • Các hệ thống cần có hiệu suất cao và độ trễ thấp như thiết bị lái xe, game, thiết bị phát sóng
  • Ứng dụng phân tán và song song
  • Ứng dụng thời gian thực
  • Hệ thống nhúng
  • Web framework
  • Các hệ thống lớn và phức tạp

Servo

Mozilla sử dụng Rust để viết nên Servo, một trình duyệt web song song

(https://github.com/servo/servo)

Năm 2013, Samsung đã tham gia phát triển Servo bằng cách đưa lên Android và các bộ xử lý ARM. Sau đó Servo trở thành một dự án mã nguồn mở với hơn 200 người tham gia đóng góp phát triển.

Java 8 – Phương thức Generic

Phương thức Generic là các phương thức cho phép sử dụng tham số kiểu dữ liệu tương tự như với kiểu dữ liệu Generic.

Cú pháp

Cú pháp của phương thức generic giống như với phương thức bình thường, khác ở chỗ chúng ta thêm cặp dấu <> và danh sách các tham số kiểu dữ liệu vào giữa cặp dấu đó được đặt trước kiểu dữ liệu trả về.

Ví dụ chúng ta khai báo lớp Util và phương thức compare() theo kiểu Generic như sau:

public class Util {
    public static <K, V> boolean compare(Pair<K, V> p1, Pair<K, V> p2) {
        return p1.getKey().equals(p2.getKey()) &&
               p1.getValue().equals(p2.getValue());
    }
}

public class Pair<K, V> {

    private K key;
    private V value;

    public Pair(K key, V value) {
        this.key = key;
        this.value = value;
    }

    public void setKey(K key) { this.key = key; }
    public void setValue(V value) { this.value = value; }
    public K getKey()   { return key; }
    public V getValue() { return value; }
}

Trong đoạn code trên, chúng ta định nghĩa lớp Pair là một lớp generic lưu 2 thuộc tính keyvalue có kiểu dữ liệu bất kì, lớp Util có một phương thức generic là compare() nhận vào 2 đối tượng Pair và so sánh xem 2 đối tượng này có trùng keyvalue hay không.

Cú pháp gọi phương thức generic compare() như sau:

Pair<Integer, String> p1 = new Pair<>(1, "apple");
Pair<Integer, String> p2 = new Pair<>(2, "berry");
boolean same = Util.<Integer, String>compare(p1, p2);

Ở đây chúng ta khai báo tên các kiểu dữ liệu của các đối tượng được truyền vào phương thức compare() ngay trước tên phương thức.

Tuy nhiên chúng ta vẫn có thể không cần khai báo và để Java tự nhận biết các kiểu này:

Pair<Integer, String> p1 = new Pair<>(1, "apple");
Pair<Integer, String> p2 = new Pair<>(2, "pear");
boolean same = Util.compare(p1, p2);

Giới hạn các kiểu dữ liệu được phép truyền

Chúng ta có thể giới hạn chỉ cho phép một số kiểu dữ liệu nào đó được truyền vào khi khai báo kiểu dữ liệu generic hoặc phương thức generic. Để làm việc này thì chúng ta thêm

extends <tên lớp> 

vào sau tham số biến, chẳng hạn <T extends Integer>, và như thế chúng ta sẽ chỉ có thể truyền vào tham số biến là lớp Integer và các lớp con của lớp Integer.

Chẳng hạn chúng ta có đoạn code như sau:

public class Box { 
    public <T> void print(T t) { 
        System.out.println(t + " is a " + t.getClass().getName()); 
    } 

    public static void main(String[] args) { 
        Box box = new Box(); 
        box.<Integer>print(new Integer(10));
    }
}

Trong đoạn code trên chúng ta định nghĩa lớp Box có một phương thức generic nhận vào một tham số, bên trong phương thức này chúng ta in ra giá trị của tham số đó và tên lớp của tham số.

10 is a java.lang.Integer

Đoạn code trên rất đơn giản, nhưng bây giờ chúng ta sửa lại để lớp Box chỉ nhận các đối tượng Number và các lớp con của Number như sau:

public class Box {     
    public <T extends Number> void print(T t) {         
        System.out.println(t + " is a " + t.getClass().getName());     
    } 
    
    public static void main(String[] args) {         
        Box box = new Box();         
        box.<Integer>print(new Integer(10));             // OK
        box.<String>print(new String("Pho Code"));       // error
    }
}

Và như thế là chúng ta chỉ có thể truyền vào lớp Number hoặc các lớp con của lớp Number. Nếu truyền vào các kiểu dữ liệu khác thì Java sẽ báo lỗi.

10 is a java.lang.Integer
Box.java:9: error:  method print in class Box cannot be applied to given types; 
    box.<String>print(new String("Pho Code"));   
     ^  
 required: T  
 found: String  
 reason: explicit type argument String does not conform to declared bound(s) Number  
 where T is a type-variable:    
  T extends Number declared in method <T>print(T)
1 error

Và cũng tương tự chúng ta cũng có thể giới hạn các kiểu dữ liệu được phép dùng cho lớp generic chứ không chỉ có phương thức generic.

Giới hạn nhiều lớp

Nếu muốn giới hạn cho nhiều lớp hơn thì chúng ta ghi tên các lớp cơ sở cách nhau bởi dấu '&':

<T extends Integer & Boolean & Byte>

Sự thừa kế trong các kiểu generic

Như chúng ta đã biết, chúng ta có thể gán các đối tượng có kiểu dữ liệu là kiểu cha cho một đối tượng khác có kiểu dữ liệu là kiểu con. Chẳng hạn như chúng ta có thể gán một đối tượng Object cho một đối tượng IntegerObject là cha của Integer:

Object someObject = new Object();
Integer someInteger = new Integer(10);
someObject = someInteger;   // OK

Đây còn được gọi là “mối quan hệ is a” trong lập trình hướng đối tượng. Khái niệm này cũng được áp dụng cho kiểu dữ liệu generic:

Box<Number> box = new Box<Number>();
box.add(new Integer(10));   // OK
box.add(new Double(10.1));  // OK

Bây giờ chúng ta xem đoạn code sau:

public void print(Box<Number> n) { 
    /* ... */ 
}

Đối với phương thức trên thì chúng ta phải truyền vô một đối tượng có kiểu chính xác là

Box<Number>

Các kiểu khác như Box<Integer> hay Box<Double> đều sẽ bị báo lỗi bởi vì các kiểu Box<Integer> hay Box<Double>… đều không phải là kiểu con của Box<Number> mặc dù Integer hay Double là kiểu con của Number.

Đây là một đặc điểm rất dễ bị nhầm lẫn khi lập trình generic. Để có thể truyền các kiểu Box<Integer> hay Box<Double> thì chỉ có một cách duy nhất là thực hiện việc kế thừa lại các lớp cha.

Java 8 – Kiểu dữ liệu Generic

Generic là một tính năng cho phép chúng ta truyền các kiểu dữ liệu cao cấp như lớp và interface vào phương thức, lưu ý là kiểu truyền này khác với việc truyền đối tượng lớp hoặc interface vào phương thức mà chúng ta thường dùng.

Generic có các đặc điểm sau:

  • Hỗ trợ trình biên dịch kiểm tra tính chặt chẽ của kiểu dữ liệu
  • Loại bỏ việc ép kiểu dữ liệu
  • Cho phép lập trình viên viết các thuật toán generic

Ví dụ

Chúng ta định nghĩa lớp Box như sau:

public class Box {
    private Object object;

    public void set(Object object) { this.object = object; }
    public Object get() { return object; }
}

Lớp này có một thuộc tính kiểu Object, 2 phương thức get()set() để gán và lấy đối tượng Object đó.

Do kiểu dữ liệu ở đây là Object nên chúng ta có thể truyền vào bất cứ kiểu dữ liệu nào cũng được vì Object là lớp gốc trong cây phân lớp, ngoại trừ các kiểu dữ liệu cơ bản như int, float, double….  Nhưng khi chúng ta muốn lấy ra đúng kiểu dữ liệu mà chúng ta đã truyền vào thì chúng ta phải ép kiểu, ví dụ:

Object obj = new Object();
Integer n = new Integer();
obj.set(n);

Integer m = (Integer)obj.get();

Và giả sử ở một đoạn code khác chúng ta lại truyền vào một đối tượng khác như String, Float..v.v và nếu nhầm lẫn, chúng ta có thể ép kiểu sai và gây ra lỗi exception.

Để giải quyết thì chúng ta có thể viết lại lớp Box theo kiểu Generic.

Cú pháp Generic

Cú pháp của Generic như sau:

class name<T1, T2, ..., Tn> {

}

Chúng ta định nghĩa lớp như bình thường, và theo sau tên lớp là cặp dấu <>, rồi đến các tham số biến là các kí tự T1, T2,… Tn. Bên trong khai báo lớp, tất cả các kiểu dữ liệu đều có tên là T1, T2Tn chứ không còn là các tên cụ thể như Object nữa.

Lớp Box được viết lại như sau:

public class Box<T> {
    private T object; 

    public T get() { 
        return this.object; 
    } 

    public void set(T object) { 
        this.object = object; 
    }
}

Tham số biến ở đây có thể hiểu là tham số cho các kiểu dữ liệu (trừ các kiểu cơ bản như int, float, char…), và khi khởi tạo một đối tượng thì chúng ta thay tế T bằng tên các lớp cụ thể như sau:

Box<Integer> integerBox = new Box<Integer>();

Và như thế thì thuộc tính object bên trong lớp Box giờ đây sẽ có kiểu dữ liệu là Integer, và khi gọi phương thức get() thì chúng ta không cần phải ép kiểu nữa.

Một số lưu ý:

  • Có thể sử dụng bất kì kí tự nào cho tham số biến và có thể viết hoa hay viết thường tùy ý, không nhất thiết phải là T.
  • Kể từ phiên bản Java SE 7 trở lên, chúng ta có thể không truyền vào tham số biến khi khởi tạo , tức là chỉ ghi dấu <> trống không, ví dụ new Box<>(), và trình biên dịch sẽ tự suy ra kiểu dữ liệu phù hợp.

Sử dụng nhiều tham số biến

Trong cú pháp ở trên chúng ta thấy là có thể sử dụng nhiều tham số biến khi khai báo (T1, T2, … Tn). Để ví dụ thì chúng ta xem đoạn code sau:

public class MyPair<K, V> {

    private K key;
    private V value;

    public MyPair(K key, V value) {
	this.key = key;
	this.value = value;
    }

    public K getKey()	{ return key; }
    public V getValue() { return value; }
}

Để khai báo nhiều tham số biến thì chúng ta chỉ cần ghi tên chúng cách nhau bởi dấu phẩy. Khi sử dụng thì chúng ta cũng khai báo danh sách các tên lớp hoặc interface cách nhau bởi dấu phẩy.

MyPair<String, Integer> myPair1 = new MyPair<String, Integer>("PhoCode", 2017);
MyPair<String, String> myPair2 = new MyPair<String, String>("PhoCode", "Blog");

Như đã lưu ý ở trên, chúng ta có thể không điền tên lớp trong đoạn code khởi tạo:

MyPair<String, Integer> myPair1 = new MyPair<>("PhoCode", 2017);
MyPair<String, String> myPair2 = new MyPair<>("PhoCode", "Blog");

Kiểu nguyên thủy

Khi chúng ta không khai báo tham số biến cho lớp thì lớp đó bây giờ sẽ được gọi là “kiểu nguyên thủy”, chẳng hạn như chúng ta có đoạn code định nghĩa lớp Box như sau:

public class Box<T> {
    // ...
}

Như bình thường thì chúng ta sẽ truyền tên lớp khi khởi tạo lớp Box:

Box<Integer> intBox = new Box<>();

Tuy nhiên chúng ta có thể không khai báo cả tên lớp và lúc này lớp Box được gọi là kiểu nguyên thủy.

Box rawBox = new Box();

Chúng ta có thể tham chiếu đối tượng kiểu nguyên thủy tới đối tượng có khai báo rõ ràng tên lớp như sau:

Box<String> stringBox = new Box<>();
Box rawBox = stringBox;               // OK

Nếu tham chiếu theo chiều ngược lại thì Java sẽ in câu cảnh báo:

Box rawBox = new Box();           
Box<Integer> intBox = rawBox;     // warning: unchecked conversion

Và đôi khi sẽ là lỗi biên dịch luôn nếu việc chuyển đổi kiểu không phù hợp. Để ẩn câu cảnh báo thì chúng ta có thể dùng annotation @SuppressWarnings("unchecked"), tuy nhiên chúng ta cũng không nên dùng cách này mà nên làm mọi thứ minh bạch thì hơn.

Java 8 – Package

Gói (Package) là một tính năng hỗ trợ coder tìm kiểu dữ liệu nhanh chóng, tránh trùng lắp tên, tiện dụng trong truy vấn.

Một gói (package) là một nhóm các kiểu dữ liệu có liên quan tới nhau hỗ trợ truy cập và quản lý các kiểu dữ liệu đó, kiểu dữ liệu ở đây có thể là lớp, interface, enum, annotation, tuy nhiên trong bài này chúng ta sẽ chủ yếu làm việc với lớp.

Tất cả các lớp có sẵn trong Java đều được nhóm vào một gói nào đó, chẳng hạn như các lớp cơ bản thì được nhóm vào trong gói java.lang, các lớp hỗ trợ đọc/ghi dữ liệu thì nằm trong gói java.io, chúng ta cũng có thể tự định nghĩa gói riêng và nhóm các lớp của chúng ta lại vào gói này.

Tạo một gói

Để tạo một gói thì chúng ta ghi từ khóa package theo sau là tên gói do chúng ta tự nghĩ vào đầu mỗi file .java có chứa code định nghĩa kiểu (như lớp, interface, annotation…). Lưu ý là câu lệnh package này phải được ghi đầu tiên trong mỗi file code Java, và trong mỗi file chỉ được phép có duy nhất một dòng này thôi.

Thông thường thì chúng ta sẽ định nghĩa mỗi kiểu dữ liệu mới trong mỗi file riêng biệt, chẳng hạn như lớp public class Blog thì trong file Blog.java, lớp public class PhoCodeBlog trong file PhoCodeBlog.java…v.v Và trong mỗi file này chúng ta chèn thêm câu lệnh package (chẳng hạn package blog;) vào đầu mỗi file để quy định package cho chúng.

package blog;
public class Blog {
    // ...
}
package blog;
public class PhoCodeBlog {
    // ...
}

Nếu chúng ta không ghi dòng lệnh package nào thì các kiểu mà chúng ta định nghĩa ra sẽ không có package.

Quy tắc đặt tên

Tên package được viết thường toàn bộ các chữ cái để tránh nhầm lẫn với tên lớp hoặc interface.

Thông thường thì các package được viết bởi các công ty sẽ có tên bắt đầu bằng tên miền do công ty đó mua. Chẳng hạn như công ty FPT viết package sẽ có tên dạng như com.fpt.mypackage.

Các package có sẵn trong Java có tên bắt đầu bằng java. hoặc javax.

Truy xuất các phần tử trong package

Các kiểu dữ liệu được định nghĩa bên trong một package được gọi là các phần tử của package. Chúng ta có thể truy xuất phần tử của một package bằng một trong các cách sau:

  • Viết đầy đủ tên của lớp và package
  • Import tên phần tử của package
  • Import tất cả các phần tử của package

Nếu bạn đã từng làm việc với C++ hay C# thì có thể hiểu import giống như include hoặc using vậy

Viết đầy đủ tên lớp và package

Chúng ta sử dụng cách viết này bằng cách viết tên đầy đủ của gói và tên của kiểu dữ liệu, ngăn cách nhau bởi dấu chấm. Ví dụ:

blog.PhoCodeBlog;

Tuy nhiên nếu gói mà chúng ta định sử dụng đã được import trước rồi (tìm hiểu thêm ở dưới) thì chúng ta không cần phải ghi tên gói ra nữa mà chỉ cần ghi tên kiểu dữ liệu là được.

Để tạo một đối tượng lớp PhoCodeBlog thì chúng ta cũng phải ghi đầy đủ tên gói và tên lớp ra:

blog.PhoCodeBlog blog = new blog.PhoCodeBlog();

Thông thường chúng ta sử dụng cách ghi đầy đủ này nếu như kiểu dữ liệu đó ít được dùng. Còn nếu đây là lớp thường dùng thì chúng ta nên sử dụng cách import package.

Import phần tử của package

Chúng ta import một kiểu dữ liệu (hay một phần tử) trong một package bằng cách ghi từ khóa import vào đầu file, theo sau là tên đầy đủ của package và lớp, ngăn cách nhau bởi dấu chấm. Ví dụ:

import blog.PhoCodeBlog;

Các dòng import luôn luôn đứng đầu file nhưng phải đứng sau dòng khai báo package.

Sau khi đã import thì chúng ta có thể tạo đối tượng lớp bình thường mà không cần ghi tên package:

PhoCodeBlog blog = new PhoCodeBlog();

Cách import trực tiếp như thế này thường được dùng khi kiểu dữ liệu được gọi đi gọi lại nhiều lần. Nếu một package có nhiều kiểu dữ liệu và chúng ta cũng thường xuyên sử dụng các kiểu dữ liệu đó thì chúng ta nên sử dụng cách import toàn bộ phần tử của package.

Import tất cả các phần tử của package

Để import toàn bộ phần tử trong một package thì chúng ta ghi dấu ngôi sao (*) theo sau tên package thay vì ghi rõ tên các phần tử. Ví dụ:

import blog.*;

Và chúng ta vẫn có thể sử dụng các phần tử của package một cách bình thường.

PhoCodeBlog blog = new PhoCodeBlog();

Package không có tính thừa kế hay gộp nhóm

Package không có tính thừa kế hay gộp nhóm như class. Ví dụ trong Java có package java.awt, một package khác có tên là java.awt.color, và java.awt.font, java.awt.xxx… thì đây là các package riêng biệt, tức là java.awt.color không liên quan gì tới java.awt cả, mà đây thực ra chỉ là quy tắc đặt tên package trong Java cho dễ quản lý. Khi chúng ta ghi import java.awt.* thì dòng này chỉ đọc các phần tử có trong package java.awt chứ không liên quan gì tới các package có trong java.awt.color hay java.awt.font…v.v

Để sử dụng tất cả các phần tử của cả 2 package java.awtjava.awt.color thì chúng ta phải import cả 2 package:

import java.awt.*;
import java.awt.color.*;

Package có các phần tử trùng tên

Nếu có 2 package khác nhau có các phần tử trùng tên, và cả 2 đều được import thì khi sử dụng, chúng ta phải chỉ rõ tên package của phần tử mà chúng ta định sử dụng. Chẳng hạn như chúng ta định nghĩa một lớp có tên Integer trong package blog, thì lớp này trùng với lớp Integer trong package java.lang và do đó khi sử dụng chúng ta phải ghi rõ tên package của lớp mà chúng ta muốn sử dụng.

import blog.Integer;
import java.lang.Integer;

Integer a = new Integer();                     // lỗi
java.lang.Integer b = new java.lang.Integer(); // đúng
blog.Integer c = new blog.Integer();           // đúng

Static Import

Trong Java có câu lệnh static import dùng để import các thành phần static của các lớp có trong các package, thường thì chúng ta sẽ import những phần tử hay được dùng nhiều nhất.

Chẳng hạn như lớp Math trong package java.lang có hằng số staticPI, khi dùng thì chúng ta sẽ gọi tên lớp và tên hằng số một cách rõ ràng là java.lang.Math.PI:

public class Circle {                        //  Hình tròn
    private double r;
    public static double area()              //  Tính diện tích
    {
        return java.lang.Math.PI * this.r * this.r;
    }
}

Chúng ta có thể import lớp Math bằng cách ghi câu lệnh import java.lang.Math; sau đó mỗi lần dùng PI thì ghi Math.PI. Hoặc nếu muốn gọn hơn thì chúng ta có thể import luôn hằng số PI như sau:

import static java.lang.Math.PI;

Hoặc dùng cách import toàn bộ phần tử:

import static java.lang.Math.*;

Bằng cách đó chúng ta có thể ghi PI đứng một mình là được:

import static java.lang.Math.PI;
public class Circle {                  // Hình tròn
    private double r; 
    public static double area()        // Tính diện tích 
    { 
        return PI * this.r * this.r; 
    }
}

Java 8 – Lớp và phương thức trừu tượng

Lớp trừu tượng là lớp được khai báo với từ khóa abstract và có thể có hoặc không có các phương thức trừu tượng. Lớp trừu tượng không được dùng để tạo ra các đối tượng mà chỉ có thể được kế thừa. Ví dụ:

public abstract class Number() {

}

Phương thức trừu tượng là các phương thức chỉ có phần tên chứ không có phần thân, phía sau tên phương thức là dấu chấm phẩy, ví dụ:

abstract double add(double a, double b);

Nếu một lớp có chứa bất kì một phương thức trừu tượng nào thì lớp đó phải là lớp trừu tượng.

public abstract class Number {
   abstract double add(double a, double b);
}

Khi một lớp trừu tượng được kế thừa thì các lớp con luôn luôn phải code lại tất cả các phương thức trừu tượng của lớp cha, nếu không thì lớp con cũng phải khai báo là trừu tượng – abstract.

Lưu ý là tính năng này cũng khá giống với interface, bởi vì trong interface thì một phương thức cũng không có phần thân, và các lớp implement lại interface này cũng phải code lại các phương thức đó.

So sánh giữa lớp trừu tượng và interface

Lớp trừu tượng và interface khá giống nhau ở chỗ là chúng ta không thể tạo ra đối tượng từ chúng, và mỗi loại đều có thể khai báo phương thức vừa có phần thân và vừa không có phần thân. Điểm khác nhau là trong phương thức trừu tượng thì không thể khai báo các thuộc tính static hoặc final, có thể định nghĩa các phương thức public, protectedprivate. Đối với interface thì mặc định tất cả các thuộc tính đều là public static final, tất cả các phương thức được định nghĩa đều là public. Một điểm khác nữa là chúng ta chỉ có thể thừa kế 1 lớp (cho dù là có trừu tượng hay không), nhưng lại có thể implement vô số interface.

Chúng ta nên dùng lớp trừu tượng khi:

  • Các lớp mà chúng ta sẽ định nghĩa sẽ gần như giống hoàn toàn với nhau và với lớp cha – tức là ít thay đổi
  • Các lớp con sẽ chủ yếu dùng các phương thức và thuộc tính như nhau, hoặc sử dụng phạm vi hoạt động khác với public.
  • Chúng ta muốn khai báo các trường không phải staticfinal.

Nên dùng interface khi:

  • Các lớp mà chúng ta sẽ định nghĩa không liên quan đến nhau.
  • Chúng ta định nghĩa các phương thức nhưng không quan tâm đến lớp nào sẽ định nghĩa phương thức đó.
  • Chúng ta muốn thực hiện đa thừa kế.

 

Cũng có trường hợp rất nhiều thư viện sử dụng cả lớp trừu tượng và interface.

Ví dụ

Giả sử chúng ta muốn viết một chương trình vẽ các hình như hình tròn, hình chữ nhật, đường thẳng, đường cong…v.v Các đối tượng này thường có chung một số trạng thái như vị trí, hướng, màu, nền…v.v và các hành động như di chuyển, xoay, thay đổi kích thước, vẽ…v.v Một trong số các hành động sẽ khác nhau tùy thuộc vào từng hình, chẳng hạn như vẽ – vẽ hình tròn sẽ khác với vẽ hình chữ nhật. Chúng ta sẽ định nghĩa lớp có tên như GraphicObject là trừu tượng, và các lớp cụ thể hơn kế thừa lại lớp này như Rectangle (hình chữ nhật), Circle (hình tròn), Curve (đường cong), Line (đường thẳng). Lớp GraphicObject sẽ lưu các thuộc tính dùng chung và các lớp con sẽ kế thừa lại, các lớp con cũng sẽ override lại các phương thức để dùng cho riêng chúng.

Classes Rectangle, Line, Bezier, and Circle Inherit from GraphicObject
 

Ví dụ chúng ta khai báo lớp trừu tượng GraphicObject như sau:

abstract class GraphicObject {
    int x, y;

    void moveTo(int newX, int newY) {
        this.x = newX;
        this.y = newY;
    }
    abstract void draw();
}

Lớp GraphicObject có 2 thuộc tính là xy. Một phương thức thường là moveTo() và một phương thức trừu tượng là draw().

Tiếp theo chúng ta khai báo các lớp không trừu tượng là CircleRectangle kế thừa lại lớp GraphicObject và các lớp này sẽ phải code lại phương thức draw().

class Circle extends GraphicObject {
    void draw() {
        // ...
    }
}

class Rectangle extends GraphicObject {
    void draw() {
        // ...
    }
}

Lớp trừu tượng implement giao diện

Trong bài interface chúng ta đã biết là tất cả các lớp implement một interface thì phải code lại toàn bộ các phương thức trong interface đó. Tuy nhiên, một lớp có khai báo abstract – trừu tượng mà có implement một interface thì không cần phải code lại toàn bộ các phương thức trong interface đó

abstract class X implements Y {
    // Có thể không cần code một phương thức nào của interface Y
}

 

Java 8 – Lớp Object

Lớp Object trong gói java.lang là lớp nằm tại gốc của cây phân cấp lớp. Tất cả các lớp khác được tạo ra đều là hậu duệ trực tiếp hoặc gián tiếp của lớp Object. Và do đó sẽ kế thừa tất cả các phương thức của lớp Object, mặc dù chúng ta cũng ít khi dùng các phương thức của lớp này, tuy nhiên nếu phải dùng thì chúng ta nên override lại các phương thức đó. Một trong các phương thức của lớp Object là:

  • protected Object clone() throws CloneNotSupportedException
    Tạo và trả về một đối tượng bản sao của đối tượng này.
  • public boolean equals(Object obj)
    Cho biết một đối tượng có “bằng” đối tượng này hay không.
  • protected void finalize() throws Throwable
    Trình thu dọn rác sẽ gọi phương thức này khi không còn bất cứ một tham chiếu nào đến đối tượng này.
  • public final Class getClass()
    Lấy lớp của đối tượng.
  • public int hashCode()
    Lấy mã băm của đối tượng
  • public String toString()
    Trả về một chuỗi String mô tả về đối tượng

Ngoài ra còn có các phương thức notify(), notifyAll() và wait() dùng trong các hoạt động đồng bộ luồng chương trình, chúng ta sẽ không tìm hiểu về các phương thức này.

Phương thức clone()

Nếu một lớp hoặc các lớp cha của lớp đó có implement giap diện Cloneable thì chúng ta có thể sử dụng phương thức clone() để tạo ra một bản sao của đối tượng đó, để làm việc này thì chúng ta chỉ cần gọi phương thức clone() là được:

object.clone();

Phương thức này sẽ kiểm tra xem đối tượng gọi phương thức này có implement giao diện Cloneable hay không, nếu không thì sẽ giải phóng lỗi CloneNotSupportedException, chúng ta sẽ tìm hiểu về lỗi exception sau.

Nếu đối tượng có implement giao diện Cloneable thì phương thức clone() sẽ tạo một đối tượng có cùng tên lớp, cùng các thuộc tính và giá trị của đối tượng gốc.

Trong hầu hết các trường hợp thì chúng ta có thể gọi phương thức clone() mà không có bất cứ chuyện gì xảy ra. Tuy nhiên nếu trong đối tượng đó có tham chiếu tới một đối tượng khác thì chúng ta nên override lại phương thức clone(), vì khi chúng ta thay đổi giá trị của đối tượng được tham chiếu thì các đối tượng gốc và đối tượng được tạo ra từ clone() sẽ có cùng giá trị của đối tượng được tham chiếu, tức là đối tượng gốc và đối tượng được tạo ra từ clone() vẫn không thật sự là 2 đối tượng khác nhau 100%.

Phương thức equals()

Phương thức equals() sẽ so sánh xem 2 đối tượng có giống nhau hay không và trả về giá trị true nếu giống.

Cách thức hoạt động của phương thức này sẽ khác nhau tùy theo kiểu dữ liệu, nếu kiểu dữ liệu là các kiểu cơ bản như int, float, String…v.v thì phương thức này sẽ trả về true hoặc false bằng cách đơn giản là so sánh theo toán tử ==. Tuy nhiên khi so sánh 2 đối tượng với nhau thì phương thức này sẽ so sánh địa chỉ tham chiếu của chúng, tức là 2 đối tượng sẽ “bằng nhau” khi chúng cùng tham chiếu vê một địa chỉ bộ nhớ.

Trên thực tế thì chúng ta nên override lại phương thức equals() để nó hoạt động theo như ý chúng ta muốn, và khi override phương thức equals() thì chúng ta cũng phải override phương thức hashCode().

Phương thức finalize()

Lớp Object có một phương thức thuộc dạng callback là finalize(), phương thức này sẽ được gọi khi đối tượng trở thành “rác”. Phương thức finalize() trong lớp Object có công dụng là… chẳng làm gì cả, chúng ta có thể override lại trong các lớp con để thực hiện các công việc dọn dẹp tài nguyên theo ý chúng ta thay vì để việc đó cho trình dọn rác của Java.

Và bản thân phương thức finalize() khi được gọi cũng thực hiện các công việc rất không ổn định, do đó chúng ta cũng không nên quá dựa dẫm vào phương thức này mà nên tự xử lý các công việc dọn dẹp bằng tay, chẳng hạn như đóng file, đóng kết nối mạng…v.v.

Phương thức getClass()

Phương thức getClass() có một điểm đặc biệt là không thể override được.

Phương thức này sẽ trả về một đối tượng Class có chứa các phương thức mà chúng ta có thể sử dụng để lấy thông tin về lớp đó, chẳng hạn như getSimpleName(), getSuperclass(), getInterfaces()…v.v Ví dụ:

void printClassName(Object obj) {
    System.out.println("Class name: " + obj.getClass().getSimpleName());
}

Đoạn code trên sẽ in tên lớp của đối tượng obj bằng cách lấy đối tượng Class từ phương thức getClass(), sau đó lấy tên lớp thông qua phương thức getSimpleName(). Lớp Class trong gói java.lang chứa khoảng hơn 50 phương thức hỗ trợ chúng ta lấy gần như toàn bộ thông tin về một lớp cụ thể.

Phương thức hashCode()

Phương thức hashCode() sẽ trả về mã băm của một đối tượng, đó là địa chỉ bộ nhớ của đối tượng đó trong RAM, lưu theo dạng số thập lục phân.

Như bình thường thì khi 2 đối tượng “bằng nhau” thì mã băm của chúng cũng bằng nhau, khi chúng ta thay đổi phương thức equals() thì chúng ta cũng nên thay đôi phương thức hashCode(), bởi vì phương thức equals() sẽ lấy giá trị từ hashCode() và so sánh 2 đối tượng với nhau.

Phương thức toString()

Hầu như chúng ta sẽ luôn luôn cần override phương thức toString().

Phương thức toString() sẽ trả về một đối tượng String chứa chuỗi mô tả về đối tượng đó, đây là phương thức rất hữu ích khi debug. Chuỗi mô tả của đối tượng nào thì sẽ mô tả đối tượng đó, do dó thường chúng ta sẽ phải override phương thức toString() để cho ra chuỗi theo ý chúng ta.

Thông thường chúng ta có thể truyền giá trị của toString() vào trong phương thức System.out.println() luôn.

System.out.println(obj.toString());

Java 8 – Từ khóa super

Nếu các phương thức của chúng ta ghi đè lên các phương thức của lớp cha, chúng ta có thể gọi lại chính các phương thức đã bị ghi đè đó ở lớp cha thông qua từ khóa super. Ngoài ra chúng ta cũng có thể tham chiếu trực tiếp tới các thuộc tính bị che giấu. Ví dụ:

public class Superclass {
    public void print() {
        System.out.println("Printed in Superclass.");
    }
}

Đoạn code trên định nghĩa lớp Superclass có một phương thức print(). Tiếp theo chúng ta định nghĩa lớp con như sau:

public class Subclass extends Superclass {

    @Override
    public void print() {
        super.print();
        System.out.println("Printed in Subclass");
    }
    public static void main(String[] args) {
        Subclass s = new Subclass();
        s.printMethod();    
    }
}

Chúng ta định nghĩa lớp Subclass kế thừa lớp Superclass, chúng ta cũng định nghĩa phương thức print() ghi đè phương thức print() của lớp Superclass. Tuy nhiên chúng ta vẫn có thể gọi tới phương thức print() của lớp Superclass từ lớp Subclass thông qua từ khóa super.

Printed in Superclass.
Printed in Subclass

Gọi phương thức khởi tạo

Chúng ta cũng có thể gọi lại phương thức khởi tạo của lớp cha bằng cách dùng từ khóa super. Giả sử chúng ta có lớp Blog với phương thức khởi tạo như sau:

public class Blog {
    private String title;
    public Blog(String title) {
        this.title = title;
    }
}

Phương thức khởi tạo của lớp Blog nhận vào một biến String. Tiếp theo chúng ta định nghĩa lớp PhoCodeBlog kế thừa từ lớp Blog như sau:

public class PhoCodeBlog extends Blog {
    private DateTime createdAt;
    public PhoCodeBlog(DateTime createdAt, String title) {
        super(title);
        this.createdAt = createdAt;   
    }
}

Phương thức khởi tạo của lớp PhoCodeBlog cũng nhận vào một đối tượng String, ngoài ra còn có một dối tượng DateTime dành riêng cho lớp này. Bên trong chúng ta gọi tới phương thức khởi tạo của lớp cha là Blog bằng cách gọi super() và truyền vào tham số là giá trị của đối tượng String title.

Lưu ý là nếu chúng ta có gọi tới phương thức super() thì luôn luôn đặt super() làm dòng đầu tiên.

Một lưu ý khác là bản thân các lớp sẽ có một phương thức khởi tạo không có tham số nếu chúng ta không khai báo phương thức khởi tạo nào. Nếu trong lớp con chúng ta không gọi tới các phương thức super() thì trình biên dịch sẽ tự động gọi tới phương thức super() không có tham số ở lớp cha, và do đó ở lớp cha phải có định nghĩa một phương thức khởi tạo không có tham số, nếu không trình biên dịch sẽ báo lỗi.

Và tất nhiên là điều đó có nghĩa là trình biên dịch sẽ lần lượt gọi ngược lại các phương thức khởi tạo của các lớp cha, dần dần quay về lớp gốc là lớp Object, trong Java thì quy trình này còn được gọi là chuỗi khởi tạo (constructor chaining).

Java 8 – Ghi đè và che giấu phương thức

Ghi đè phương thức

Trong một lớp con có chứa các phương thức có cùng tên, cùng kiểu dữ liệu trả về và danh sách các tham số với một phương thức nào đó trong lớp cha thì lớp con đó đã ghi đè (override) phương thức của lớp cha.

Ghi đè phương thức là một cách thay đổi cách hoạt động của một phương thức trong lớp cha. Phương thức ghi đè có thể trả về kiểu dữ liệu được kế thừa từ kiểu dữ liệu trả về của phương thức trong lớp cha, kiểu dữ liệu được trả về kiểu này còn được gọi là kiểu hiệp biến (covariant).

Khi ghi đè một phương thức thì chúng ta dùng annotation @Override trước phương thức đó để thông báo cho trình biên dịch biết là chúng ta đang ghi đè phương thức. Nếu trình biên dịch không tìm ra phương thức bị ghi đè trong lớp cha thì sẽ báo lỗi.

Che giấu phương thức

Chúng ta đã biết là phương thức tĩnh (static) là các phương thức dùng chung trong các lớp từ lớp cha tới lớp con. Tuy nhiên khi chúng ta khai báo lại một phương thức tĩnh trùng tên, kiểu dữ liệu trả về và danh sách tham số trong lớp con thì phương thức trong lớp con sẽ che giấu (hide) phương thức đó trong lớp cha.

Sự khác nhau

Sự khác nhau giữa ghi đè phương thức bình thường và che giấu phương thức tĩnh là:

  • Phương thức ghi đè sẽ chỉ có thể được gọi từ lớp con.
  • Phương thức che giấu hay bị che giấu sẽ được gọi tùy vào việc nó được gọi từ lớp cha hay lớp con.

Chẳng hạn chúng ta có đoạn code định nghĩa lớp Animal như sau:

public class Animal {
    public static void staticEx() {
        System.out.println("This is a static method");
    }

    public void ex() {
        System.out.println("This is a normal method");
    }
}

Lớp này chứa một phương thức bình thường là ex() và một phương thức tĩnh là staticEx().

Tiếp theo chúng ta định nghĩa lớp Cat kế thừa lại như sau:

public class Cat extends Animal {
    public static void staticEx() {
        System.out.println("This is Cat class's static method");  
    }

    public void ex() {
        System.out.println("This is Cat class's normal method");
    }

    public static void main(String[] args) {
        Cat cat = new Cat();
        Animal animal = cat;
        Animal.staticEx();
        animal.ex();
    }
}

Lớp Cat sẽ ghi đè phương thức ex() và che giấu phương thức staticEx() của lớp Animal. Trong hàm main() chúng ta tạo 2 đối tượng AnimalCat, đối tượng animal tham chiếu thẳng tới đối tượng cat.

This is a static method
This is Cat class's normal method

Chúng ta gọi phương tức staticEx() của lớp Animal.Sau đó gọi phương thức ex() từ đối tượng animal, do animal là đối tượng tham chiếu từ lớp Cat nên phương thức ex() được gọi sẽ là phương thức trong lớp Cat.

Phương thức Interface

Trong interface thì các phương thức mặc định và phương thức trừu tượng được thừa kế giống như phương thức bình thường. Tuy nhiên nếu ở các lớp cha của một lớp hay interface định nghĩa nhiều phương thức mặc định có cùng tên thì trình biên dịch sẽ dựa vào một số quy tắc để giải quyết các xung đột này. Các quy tắc đó như sau:

1. Phương thức bình thường sẽ được ưu tiên sử dụng so với phương thức mặc định. Ví dụ:
public class Horse {
    public String print() {
        return "I am a horse.";
    }
}
public interface Flyer {
    default public String print() {
        return "I am a fly.";
    }
}
public interface Mythical {
    default public String print() {
        return "I am a mythical creature.";
    }
}
public class Pegasus extends Horse implements Flyer, Mythical {
    public static void main(String[] args) {
        Pegasus pega = new Pegasus();
        System.out.println(pega.print());
    }
}

Đối tượng pega sẽ gọi phương thức print() từ lớp Horse thay vì từ 2 interface.

I am a horse.
2. Các phương thức đã bị ghi đè sẽ bị bỏ qua.

Trường hợp này áp dụng với các kiểu dữ liệu có chung kiểu dữ liệu cha. Ví dụ:

public interface Animal {
    default public String print() {
        return "I am an animal.";
    }
}
public interface Chicken extends Animal {
    default public String print() {
        return "I am a chicken.";
    }
}
public interface Dog extends Animal { }
public class Dragon implements Chicken, Dog {
    public static void main (String[] args) {
        Dragon dragon = new Dragon();
        System.out.println(dragon.print());
    }
}

Đối tượng dragon sẽ gọi phương thức print() từ interface Chicken.

Nếu có nhiều phương thức mặc định trùng tên với nhau hoặc trùng tên với phương thức trừu tượng thì trình biên dịch sẽ báo lỗi. Cách duy nhất để mất lỗi là ghi đè trong các lớp/interface con.

Giả sử chúng ta có 2 interface như sau:

public interface OperateCar {
    default public int startEngine(int key) {
        System.out.println("Starting OperateCar...");
    }
}
public interface FlyCar {
    default public int startEngine(int key) {
        System.out.println("Starting FlyCar...");
    }
}

Một lớp có implement cả 2 interface trên đều phải ghi đè phương thức startEngine(). Bên trong phương thức ghi đè chúng ta có thể gọi lại phương thức của lớp cha ở bất cứ interface nào với từ khóa super.

public class FlyingCar implements OperateCar, FlyCar {
    public int startEngine(int key) {
        FlyCar.super.startEngine(key);
        OperateCar.super.startEngine(key);
    }
}

Từ khóa super tham chiếu trực tiếp đến interface cha (của từng interface OperateCar hay FlyCar).

Khi một lớp kế thừa lại một lớp khác có chứa phương thức trùng tên với một phương thức trong interface thì phương thức đó có thể ghi đè phương thức trong interface, do đó chúng ta không cần phải ghi đè lại khi implement interface đó. Ví dụ:

public interface Mammal {
    String print();
}
public class Horse {
    public String print() {
        return "I am a horse.";
    }
}
public class Mustang extends Horse implements Mammal {
    public static void main(String[] args) {
        Mustang mustang = new Mustang();
        System.out.println(mustang.print());
    }
}

Trong đoạn code trên, nếu chúng ta không kế thừa lớp Horse thì chúng ta phải override lại phương thức print() từ interface Mammal trong lớp Mustang, nhưng do chúng ta đã kế thừa lớp Horse, do đó phương thức print() trong lớp Horse đã override lại phương thức print() của interface Mammal nên chúng ta không cần phải override một lần nữa.

Tuy nhiên có một lưu ý là các phương thức static sẽ không bao giờ được override theo cách này.