Category Archives: Java

Java 8 – Annotation – Phần 2

Các annotation có sẵn trong Java

Java có sẵn một số annotation được định nghĩa sẵn, thông thường được dùng cho trình biên dịch hoặc dùng cho các annotation khác.

Có 3 annotation được định nghĩa sẵn trong gói java.lang@Deprecated, @Override@SuppressWarnings.

@Deprecated cho biết một phần tử nào đó không còn được sử dụng nữa. Trình biên dịch sẽ in các câu cảnh báo khi chương trình sử dụng các thuộc tính, lớp hoặc phương thức có gắn với @Deprecated.

public class MyClass {
    @Deprecated
    static void deprecated-method() {
        // ...
    }
}

@Override cho biết phần tử đó ghi đè lên một phần tử khác đã được định nghĩa trong lớp cha. Chúng ta sẽ làm việc với các phương thức ghi đè khá nhiều.

public class MyClass extends MySuperClass {
    @Override
    public int override-method() {
        // ...
    }
}

Chúng ta không nhất thiết phải sử dụng @Override khi ghi đè phương thức, nhưng annotation này sẽ giúp chúng ta tránh lỗi dễ dàng hơn.

@SupressWarnings thông báo cho trình biên dịch biết là không được in các câu cảnh báo nào đó. Chẳng hạn như chặn câu cảnh báo deprecated ở trên.

public class MyClass {
    @Deprecated
    static void deprecated-method() {
        //  ...
    }

    @SupressWarnings("deprecation")
    public void supress-method() {
        this.deprecated-method();
    }
}

Tất cả các loại câu cảnh báo trong Java đều thuộc về một trong 2 loại là deprecation hoặc unchecked. Chúng ta có thể loại bỏ cả 2 loại cảnh báo bằng cách truyền vào @SupressWarnings như sau:

@SuppressWarnings({"unchecked", "deprecation"})

Ngoài ra còn có 2 annotation là @SafeVarargs và @FunctionalInterface. 

@SafeVarargs được đặt trước phương thức. có tác dụng kiểm tra xem đoạn code phía sau nó có tạo ra bất cứ một hành động nguy hiểm nào đó hay không, chẳng hạn như kiểm tra xem biến sắp được sử dụng có thể giải phóng lỗi exeption hay không… khi dùng @SafeVarargs thì các câu cảnh báo không được hiện ra.

@FunctionalInterface là annotation có trong Java 8, được đặt trước các giao diện hàm và thông báo cho trình biên dịch biết sau đó là phần khai báo một giao diện của một hàm, chúng ta sẽ tìm hiểu sau.

Lớp annotation

Như đã giới thiệu trong phần trước, đối với phiên bản Java 8 thì các annotation còn có thể được dùng giống như một lớp, tức là có thể dùng trong việc tạo đối tượng, ép kiểu, kế thừa…v.v

Lớp annotation được sử dụng với mục đích hỗ trợ cho việc phân tích chương trình, nhằm tăng tính type-checking, tức là đảm bảo cho biến hoặc đối tượng luôn lưu trữ một dữ liệu trong một ngữ cảnh nào đó. Chẳng hạn như chúng ta có thể quy định một biến không bao giờ được gán giá trị null, nhờ đó mà không bao giờ xảy ra lỗi NullPointerException, bằng cách sử dụng một annotation nào đó mà chúng ta tự định nghĩa, chẳng hạn như:

@NonNull String str;

Khi chúng ta biên dịch thì trình biên dịch sẽ in những câu thông báo lỗi nếu các biến có thể gây ra một lỗi nào đó, nhờ đó mà chúng ta sửa lại kịp thời trước khi chạy.

Đây là một tính năng khá mới, bạn có thể tìm hiểu thêm trên mạng, ở đây chúng ta không đi sâu.

Khai báo nhiều annotation trùng tên

Sẽ có trường hợp chúng ta muốn sử dụng một annotation nhiều lần tại một vị trí, Java 8 hỗ trợ tính năng này.

Ví dụ như chúng ta muốn viết một chương trình tự động thực hiện một công việc nào đó tại một thời điểm nào đó tương tự như dịch vụ cron. Giả sử chúng ta muốn đặt thời gian thực hiện một việc vào cuối tuần, rồi lại muốn đặt thời gian thực hiện một việc khác vào đầu tuần, chúng ta có thể định định nghĩa và khai báo 2 annotation dạng như sau:

@Schedule(date="Mon")
@Schedule(date="Sun")
public void someCron() {
    // ...
}

Để có thể sử dụng một annotation nhiều lần thì chúng ta phải khai báo một annotation khác dùng để chứa annotation lặp theo 2 bước dưới đây.

Bước 1: Khai báo kiểu annotation có thể lặp

Để khai báo một annotation có thể lặp thì chúng ta khai báo một annotation khác cho nó là @Repeatable. Ví dụ:

import java.lang.annotation.Repeatable;

@Repeatable(Schedules.class)
public @interface Schedule {
     String date() default "Mon";
}

Bên trong @Repeatable chúng ta phải truyền vào class của một annotation khác dùng để chứa @Schedule, ở đây là Schedules.class (lưu ý khác với Schedule). Tức là @Schedules sẽ chứa các @Schedule.

Bước 2: Khai báo annotation dùng để chứa

Kiểu annotation dùng để chứa annotation khác phải có một thuộc tính có tên là value. Kiểu dữ liệu được chứa phải là kiểu mảng. Ví dụ:

 

public @interface Schedules {
    Schedule[] value();
}

Java 8 – Annotation – Phần 1

Annotations là một kiểu metadata (hay siêu dữ liệu) cung cấp thông tin thêm cho một chương trình, nhưng lại không đóng một vai trò nào trong quá trình chạy của chương trình cả. Annotation không ảnh hưởng hay điều chỉnh các đoạn code mà chương trình chạy.

Một số mục đích của annotation là:

  • Cung cấp thông tin cho trình biên dịch, trình biên dịch có thể dùng annotation để phát hiện lỗi.
  • Xử lý một số công việc trong quá trình biên dịch và khởi chạy, chẳng hạn như có thể dùng để tạo ra các file code, XML…v.v
  • Xử lý một số công việc trong quá trình chạy.

Cơ bản về annotation trong Java

Annotation có dạng như sau:

@Entity

Kí tự @ cho trình biên dịch biết theo sau là một annotation, ở trên annotation có tên Entity. Annotation mà chúng ta thường dùng nhất là @Override.

@Override
void mySuperMethod() { ... }

Annotation có thể mang theo các thuộc tính và giá trị của riêng nó, chẳng hạn như:

@Blog(
   name = "Pho Code",
   site = "phocode.com"
)
class MyClass() { ... }

Hay một kiểu khai báo khác là:

@Blog(name = "Pho Code")
void myMethod() { ... }

Nếu annotation chỉ có một thuộc tính thì không cần ghi tên thuộc tính ra cũng được, ví dụ:

@Blog("Pho Code")
void myMethod() { ... }

Nếu không có thuộc tính thì không cần đến cặp dấu () như trong @Override.

Có thể khai báo nhiều annotation tại một vị trí:

@Blog
@Site
class MyClass { ... }

Và cũng có thể khai báo nhiều annotation cùng tên tại một vị trí, đây là tính năng chỉ được hỗ trợ trong Java 8, chúng ta sẽ tìm hiểu thêm sau, ví dụ:

@Blog(name = "Pho Code")
@Blog(name = "Java 8")
class MyClass { ... }

Annotation có thể đứng trước khai báo lớp, thuộc tính, phương thức và các thành phần khác của chương trình. Khi khai báo thì annotation được đặt trên dòng riêng của nó.

Đặc biệt trong Java 8 thì annotation có thể dùng giống như một lớp, tức là có thể tạo thành đối tượng, ép kiểu dữ liệu, kế thừa giao diện, giải phóng lỗi ngoại lệ…v.v

//  Tạo đối tượng
new @Blog MyObject();

//  Ép kiểu
str1 = (@Name String) str2;

//  Kế thừa giao diện
class MyClass implements @Blog AnotherClass { ... }

//  Giải phóng lỗi exception
void myMethod() throws @Blog Exception { ... }

Gần như annotation được dùng để thay thế cho tính năng comment trong các đoạn code.

Ví dụ như chúng ta có đoạn code khai báo lớp như sau:

public class Generation3List extends Generation2List {

   // Author: J.K. Rowling
   // Date: 3/17/2002
   // Current revision: 6
   // Last modified: 4/12/2004
   // By: phocode.com
}

Chúng ta có thể thay đoạn code comment ở trên bằng đoạn annotation như sau:

@ClassPreamble (
   author = "J.K. Rowling",
   date = "3/17/2002",
   currentRevision = 6,
   lastModified = "4/12/2004",
   lastModifiedBy = "phocode.com",
)
public class Generation3List extends Generation2List {

}

Định nghĩa annotation

Ví dụ về một đoạn code định nghĩa annotation:

public @interface AnnotationExample {
   String author();
   String date();
   int currentRevision() default 1;
   String lastModified() default "N/A";
   String lastModifiedBy() default "N/A";
}

Chúng ta định nghĩa annotation bằng cách ghi từ khóa @interface, rồi đến tên annotation, sau đó là cặp dấu ngoặc nhọn {}, rồi đến danh sách các thuộc tính.

Ở đây các thuộc tính được khai báo bằng cách khai báo kiểu dữ liệu, rồi đến tên thuộc tính và cặp dấu ngoặc tròn (). Nếu muốn chúng ta có thể đưa giá trị mặc định cho thuộc tính bằng cách ghi từ khóa default, theo sau là giá trị mặc định đó.

Java 8 – Lớp cục bộ

Lớp cục bộ (local class) là các lớp được định nghĩa trong một khối lệnh.

Thường thì chúng ta hay định nghĩa lớp cục bộ trong khối lệnh của một phương thức, một câu lệnh for hoặc một câu lệnh if. Ví dụ:

public class OuterClass {

    public void printMessage(String msg) {
        final String parent = "Outer Class";        

        class LocalClass {
            String msg;
            LocalClass(String msg) {
                this.msg = msg;
            }

            public void print() {
                System.out.println(this.msg + " from " + parent);
            }
        }
  
        LocalClass lc = new LocalClass(msg);
        lc.print();
    }

    public static void main(String[] args) {
        OuterClass oc = new OuterClass();
        oc.printMessage("Hello World");
    }
}

Trong đoạn code trên thì chúng ta có lớp OuterClass là lớp cha ngoài cùng, bên trong có phương thức printMessage(), trong phương thức này chúng ta định nghĩa lớp LocalClass có phương thức print(). Ở đây lớp LocalClass chính là một lớp cục bộ, vì được định nghĩa trong một khối lệnh.

Hello World from Outer Class

Các lớp cục bộ có thể truy xuất đến các phương thức và thuộc tính của lớp cha của nó, tuy nhiên với điều kiện là thuộc tính đó phải được khai báo với từ khóa final. Trong đoạn code trên phương thức print() của lớp LocalClass có thể đọc thuộc tính parent của lớp OuterClass là vì thuộc tính parent được khai báo là final.

Tuy nhiên, kể từ phiên bản Java 8 trở đi thì lớp cục bộ cũng có thể truy xuất đến các thuộc tính “gần như final” nữa, ở đây Java quy định thuộc tính “gần như final” là các thuộc tính không khai báo từ khóa final nhưng không bao giờ thay đổi giá trị sau khi đã được khai báo và khởi tạo.

Do đó trong đoạn code trên chúng ta có thể bỏ từ khóa final ở thuộc tính parent. Nhưng nếu chúng ta gán lại giá trị mới cho thuộc tính parent thì Java sẽ báo lỗi khi biên dịch.

public class OuterClass {

    public void printMessage(String msg) {
        String parent = "Outer Class";        

        class LocalClass {
            String msg;
            LocalClass(String msg) {
                parent = "Changed";
                this.msg = msg;
            }

            public void print() {
                System.out.println(this.msg + " from " + parent);
            }
        }
  
        LocalClass lc = new LocalClass(msg);
        lc.print();
    }

    public static void main(String[] args) {
        OuterClass oc = new OuterClass();
        oc.printMessage("Hello World");
    }
}

Trong đoạn code trên chúng ta bỏ từ khóa final cho biến parent, sau đó thay đổi giá trị của biến này trong phương thức khởi tạo của lớp LocalClass. Khi biên dịch thì Java sẽ thông báo lỗi:

local variables referenced from an inner class must be final or effectively final
     parent = "Changed";
     ^

Lớp cục bộ và lớp nội khá giống nhau vì chúng đều không thể khai báo và định nghĩa các thuộc tính và phương thức static. Lớp cục bộ được định nghĩa trong một phương thức static thì chỉ có thể truy xuất các thuộc tính static của lớp chứa phương thức đó. Nói chung là việc truy xuất các thuộc tính static của lớp cục bộ khá hạn chế.

Chúng ta cũng không thể định nghĩa interface trong một khối lệnh bởi vì vốn dĩ interface cũng là static rồi.

public class OuterClass {

    public void printMessage(String msg) {
        String parent = "Outer Class"; 

        class LocalClass {
            String msg;
            LocalClass(String msg) {
                parent = "Changed";
                this.msg = msg;
            }

            public void print() {
                System.out.println(this.msg + " from " + parent);
            }
 
            public void greetInEnglish() {
                interface HelloThere {
                    public void greet();
                }
                class EnglishHelloThere implements HelloThere {
                    public void greet() {
                        System.out.println("Hello ");
                    }
                }
                HelloThere myGreeting = new EnglishHelloThere();
                myGreeting.greet();
            }
        }
 
        LocalClass lc = new LocalClass(msg);
        lc.print();
    }

    public static void main(String[] args) {
        OuterClass oc = new OuterClass();
        oc.printMessage("Hello World");
    }
}

Đoạn code trên sẽ báo lỗi.

interface not allowed here
     interface HelloThere {
     ^

Ngoài ra chúng ta cũng không thể khai báo phương thức static trong một lớp cục bộ:

public class OuterClass {
    public void outerClassMethod() {
        class LocalClass {
            public static void doNothing() {
                //...
            }
 
            LocalClass() {
                //...   
            }
        }
    }

    public static void main(String[] args) {
        OuterClass oc = new OuterClass();
        oc.outerClassMethod();
    }
}

Đoạn code trên sẽ báo lỗi:

Illegal static declaration in inner class LocalClass
     public static void doNothing() {
                        ^

Tuy nhiên có một điều kì dị là Java cho phép chúng ta khai báo một thuộc tính final static trong một lớp cục bộ:

public class OuterClass {

    public void outerClassMethod() {
        class LocalClass {
            public static final float PI = 3.14159f;
            LocalClass() {
                //...
            }
        }
    }

    public static void main(String[] args) {
        OuterClass oc = new OuterClass();
        oc.outerClassMethod();
    }
}

Java 8 – Lớp lồng nhau

Java cho phép chúng ta định nghĩa một lớp trong một lớp khác, những lớp như thế được gọi là lớp lồng nhau (nested class).

class OuterClass {
    ...
    class NestedClass {
        ...    
    }
}

Lớp lồng nhau mà không phải là static thì còn được gọi là lớp nội (inner class).

Lớp lồng không static (tức lớp nội) có thể truy xuất đến tất cả các thuộc tính và phương thức của lớp lồng bên ngoài, kể cả các thành phần private, nhưng lớp lồng static thì lại không thể.

Lớp lồng có các công dụng như:

  • Gom nhóm các lớp có chung mục đích lại với nhau
  • Tăng tính đóng gói
  • Tăng tính dễ đọc và dễ bảo trì cho code

Lớp lồng static – Static Nested Class

Giống như phương thức và thuộc tính, lớp lồng static được gắn kết với đối tượng thuộc lớp bao bọc lấy nó nhưng không thể truy xuất tới các thuộc tính và phương thức của đối tượng đó.

Chúng ta truy xuất tới lớp lồng static bằng cách thêm dấu chấm và tên lớp:

OuterClass.StaticNestedClass nestedObject = new OuterClass.StaticNestedClass();

Lớp nội – Inner Class

Lớp nội được gắn với đối tượng thuộc lớp bao bọc lấy nó và có thể truy xuất tới các phương thức và thuộc tính thuộc đối tượng đó, nhưng không thể định nghĩa các thuộc tính và đối tượng static.

Đối tượng của lớp nội chỉ tồn tại trong đối tượng của lớp bao bọc lấy nó.

class OuterClass {
    ...
    class InnerClass {
        ...
    }
}

Để tạo một đối tượng lớp nội thì chúng ta phải tạo lớp ngoài trước. Sau đó tạo đối tượng lớp nội theo cú pháp như sau:

OuterClass.InnerClass innerObject = outerObject.new InnerClass();

Lớp nội còn có 2 loại nữa là lớp cục bộ và lớp ẩn, chúng ta sẽ tìm hiểu sau.

Biến trùng tên

Khi chúng ta khai báo các thuộc tính nằm trong các khu vực con mà có tên giống nhau, thì các biến nằm trong cùng sẽ được dùng trước tiên. Ví dụ:

public class LevelOne {
    public int x = 0;

    class LevelTwo {
        public int x = 1;
 
        void methodLevelTwo(int x) {
            System.out.println("x = " + x);
            System.out.println("this.x = " + this.x);
            System.out.println("LevelOne.this.x = " + LevelOne.this.x);
        }
    }

    public static void main(String[] args) {
        LevelOne lo = new LevelOne();
        LevelOne.LevelTwo lt = lo.new LevelTwo();
        lt.methodLevelTwo(23);
    }
}

Trong đoạn code trên, chúng ta có 3 biến tên là x, một biến là thuộc tính của lớp LevelOne, một biến là thuộc tính của lớp LevelTwo, một biến là tham số của phương thức methodLevelTwo(). Khi chúng ta sử dụng đến biến trong phương thức methodLevelTwo() thì Java sẽ tự động hiểu là dùng biến trong tham số, muốn truy xuất đến biến x của lớp LevelTwo thì chúng ta phải dùng từ khóa this, muốn truy xuất đến biến của lớp LevelOne thì chúng ta phải ghi rõ tên lớp rồi tới từ khóa this.

Đoạn code trên sẽ in ra các kết quả sau:

x = 23
this.x = 1
LevelOne.this.x = 0

Java 8 – Thao tác trên lớp

Phần này sẽ tìm hiểu thêm một số đặc điểm của lớp.

Trả về giá trị trong phương thức

Một phương thức quay trở lại nơi đã gọi nó khi:

  • Hoàn thành mọi câu lệnh, hoặc
  • Có câu lệnh return, hoặc
  • Giải phóng lỗi ngoại lệ (sẽ đề cập sau)

Khi chúng ta khai báo kiểu dữ liệu trả về trong phần khai báo phương thức thì chúng ta phải có câu lệnh return để “trả về” một giá trị nào đó dạng như thế này:

return value;

Nếu kiểu dữ liệu là void thì không cần, hoặc có thể ghi return không cũng được.

Kiểu dữ liệu của giá trị được trả về phải trùng với kiểu dữ liệu đã khai báo ở tên phương thức.

Ví dụ:

public class Rectangle {
    private int width = 1280;
    private int height = 720;
    public int area() {
        return width * height;
    }
}

Trong đoạn code trên thì phương thức area() có kiểu dữ liệu trả về là int, do đó trong câu lệnh return chúng ta phải ghi một biểu thức hoặc một biến nào đó có kiểu dữ liệu là int, ở đây là width * height.

Từ khóa this

Trong phần định nghĩa các phương thức của một lớp, chúng ta có thể sử dụng từ khóa this để tham chiếu đến đối tượng hiện tại – tức là đối tượng sẽ được gọi các phương thức của nó, để đọc/ghi tới tất cả các thuộc tính và phương thức của đối tượng đó.

Thông thường công dụng của từ khóa this là để phân biệt thuộc tính của lớp và tham số trong các phương thức. Ví dụ:

public class Point {
    public int x = 0;
    public int y = 0;

    public Point(int x, int y) {
        this.x = x;
        this.y = y;
    }
}

Trong phương thức khởi tạo trên thì tên các biến tham số cũng trùng với tên các thuộc tính, do đó để phân biệt rõ biến nào là tham số, biến nào là phương thức thì chúng ta dùng từ khóa this.

Từ khóa phạm vi

Có tất cả 4 loại phạm vi, chúng ta đã biết 2 loại là publicprivate, các từ khóa này quy định việc các lớp khác có được truy xuất vào các biến và phương thức của lớp này hay không.

Bảng dưới đây mô tả các loại phạm vi của lớp:

TÊN

CÙNG LỚP

CÙNG PACKAGE

LỚP CON

MỌI NƠI KHÁC

public
protected Không
không có từ khóa Không Không
private Không Không Không

Theo như bảng trên thì các biến và phương thức của một lớp lúc nào cũng có thể được truy xuất trong chính lớp đó, các biến và phương thức có phạm vi là private thì chỉ có thể truy xuất được ở bên trong lớp đó, ở các nơi khác thì không.

Biến class

Biến class có tên khác là biến static, biến tĩnh…v.v là biến được dùng chung trong toàn bộ đối tượng, tức là khi một đối tượng thuộc một lớp thay đổi giá trị của biến này thì giá trị đó cũng sẽ được thay đổi trong các đối tượng cùng lớp khác.

Để khai báo biến static thì chúng ta chỉ cần thêm từ khóa static vào trước kiểu dữ liệu:

class Point {
    private static int width = 1280;
    private static int height = 720;
    
    public int getWidth() {
        return this.width;
    }

    public int getHeight() {
        return this.height;
    }

    public void changeWidth(int width) {
        this.width = width;
    }

    public void changeHeight(int height) {
        this.height = height;
    }
}
class StaticVar {
    public static void main(String[] args) {
        Point p1 = new Point();
        System.out.println(p1.getWidth());
        p1.changeWidth(1600);
        Point p2 = new Point();
        System.out.println(p2.getWidth());
    }
}
1280
1600

Và tương tự với thuộc tính, chúng ta cũng có thể có phương thức static, tuy nhiên phương thức static phải được gọi bằng tên lớp chứ không được gọi bằng tên đối tượng. Thông thường phương thức static được dùng để đọc/ghi các thuộc tính static là chính.

Ngoài ra trong Java còn có từ khóa final, biến nào có từ khóa final sẽ không thể chỉnh sửa được nữa, do đó từ khóa final thường được dùng để định nghĩa hằng số kết hợp với từ khóa static.

static final double PI = 3.141592653589793;

Java 8 – Đối tượng

Một chương trình viết bằng Java sẽ tạo ra các đối tượng có thể gọi các phương thức. Thông qua việc thao tác với các đối tượng này, chương trình có thể thực hiện nhiều công việc khác nhau như hiển thị giao diện, chạy hoạt hình, gửi và nhận thông tin qua mạng. Khi đã xong việc thì các tài nguyên này sẽ được tái chế để sử dụng bởi các đối tượng khác.

Tạo đối tượng

Chúng ta đã biết lớp chính là một bản thiết kế để tạo ra các đối tượng, để tạo mới một đối tượng thì chúng ta sử dụng toán tử new:

Point center = new Point(23, 94);
Rectangle rect1 = new Rectangle(center, 100, 200);

Một câu lệnh tạo đối tượng gồm có 2 phần:

  • Khai báo: là tên lớp/kiểu dữ liệu và tên đối tượng, ở đoạn code trên thì phần khai báo là Point centerRectangle rect1.
  • Khởi tạo: là toán tử new, theo sau là phương thức khởi tạo của lớp đó.

Việc khai báo đối tượng cũng giống như việc khai báo biến thông thường vậy, chúng ta cũng có thể gọi một đối tượng là một biến có kiểu dữ liệu là lớp của nó. Tuy vậy việc khai báo biến với kiểu dữ liệu thông thường và khai báo đối tượng cũng có hơi khác nhau, biến thông thường khi được khai báo nhưng không được khởi tạo (không có phần sau dấu =) thì vẫn có một giá trị mặc định nào đó, chẳng hạn như int thì là 0, String thì là ""… nhưng đối tượng đối tượng thì chúng lại không có một giá trị mặc định nào cả. Việc khai báo một đối tượng mà không khởi tạo sẽ không thực sự tạo ra một đối tượng, chúng ta phải khởi tạo cho chúng thì mới có thể sử dụng được.

Khi chúng ta sử dụng toán tử new để khởi tạo một đối tượng, thì hệ điều hành sẽ cấp phát bộ nhớ cho đối tượng đó và gửi về địa chỉ bộ nhớ cho đối tượng đó.

Theo sau toán tử new là phương thức khởi tạo của lớp đó, và địa chỉ bộ nhớ của đối tượng đó, nếu muốn biết chúng ta có thể in ra như thường:

Point center = new Point(50, 50);
System.out.println(center);

Và kết quả sẽ tương tự như thế này:

Point@15db9742

Sử dụng đối tượng

Khi đã tạo đối tượng thì chúng ta có thể làm một số thứ với nó, chúng ta có thể sử dụng giá trị trong thuộc tính của chúng, hoặc gọi các phương thức để thực hiện một công việc nào đó.

Chúng ta có thể trích xuất (tức đọc) dữ liệu từ các trường trong đối tượng thông qua tên của chúng:

class Rectangle {
    private int width = 1280;
    private int height = 720;
    public void display() {
        System.out.println("Width and height are: " + width + ", " + height);
    }
}
Width and height are: 1280, 720

Nếu chúng ta truy xuất các trường của đối tượng ở bên ngoài thì chúng ta phải ghi rõ ra tên đối tượng, theo sau là dấu chấm rồi tới tên trường:

class Rectangle {
    public int width = 1280;
    public int height = 720;
}

class DisplayRectangle {
    void display() {
        Rectangle rect = new Rectangle();
        System.out.println("Width and height are: " + rect.width + ", " + rect.height);
    }
}

Và tất nhiên là các trường phải có phạm vi truy xuất là public.

Để gọi một phương thức của đối tượng thì chúng ta cũng làm giống như truy xuất trường, là ghi tên đối tượng, tiếp theo là dấu chấm rồi tới tên phương thức, và danh sách các tham số nếu có:

class Rectangle {
    private int width = 1280;
    private int height = 720;
    public void area() {
      return width * height;
    }
}

class RectangleArea {
    private Rectangle rect1;
    public static void main(String[] args) {
        System.out.println("Rectangle area: " + rect1.area());
    }
}
Rectangle are: 921600

Trình dọn rác – Garbage collector

Một số ngôn ngữ lập trình hướng đối tượng yêu cầu chúng ta phải theo dõi quá trình tồn tại của các đối tượng, phải xóa các đối tượng sau khi dùng xong vì sẽ có trường hợp chúng ta thoát chương trình nhưng các đối tượng vẫn còn tồn tại và sẽ gây ra lãng phí bộ nhớ.

Tuy nhiên việc theo dõi thủ công này rất mệt mỏi và dễ sinh lỗi, do đó Java có sẵn trình thu dọn tài nguyên, và sẽ xóa toàn bộ các đối tượng khi chương trình kết thúc, chúng ta không cần phải quan tâm đến việc thu dọn rác đó nữa.

Java 8 – Lớp

Chúng ta đã tìm hiểu qua về lớp trong các phần trước, trong phần này chúng ta sẽ tìm hiểu kĩ hơn.

Khai báo lớp

Chúng ta đã biết cú pháp khai báo lớp như sau:

class MyClass {
    // thuộc tính và phương thức
}

Cú pháp trên là cú pháp khai báo lớp. Phần thân lớp nằm giữa cặp dấu ngoặc nhọn sẽ chứa đoạn code mô tả vòng đời của các đối tượng được tạo ra từ lớp đó, chẳng hạn như phương thức khởi tạo để khởi tạo các đối tượng, khai báo các thuộc tính để lưu giữ trạng thái của lớp và các đối tượng, khai báo phương thức để mô tả các hành vi của lớp.

Cú pháp trên là cú pháp cơ bản, chúng ta có thể khai báo lớp này kế thừa từ một lớp khác hay code một giao diện nào đó với cú pháp đầy đủ như sau:

class MyClass extends MySuperClass implements YourInterface {
    // thuộc tính và phương thức
}

Cú pháp trên có ý nghĩa là lớp MyClass kế thừa từ lớp MySuperClass, và code giao diện YourInterface.

Ngoài ra chúng ta có thể thêm các từ khóa phạm vi hoạt động như public hay private và trước từ khóa class, chúng ta sẽ tìm hiểu sau về phạm vi hoạt động.

Khai báo biến

Biến có các loại sau đây:

  • Biến thành viên của một lớp – có tên khác là trường hoặc thuộc tính
  • Biến nằm trong một phương thức hoặc khối lệnh – có tên khác là biến cục bộ
  • Biến nằm trong phần khai báo tên phương thức – có tên khác là tham số

Chẳng hạn như:

class Bicycle {
    public int speed;
    public int gear;
}

Trong đoạn code trên thì speedgear là các trường (hoặc thuộc tính), cú pháp khai báo trường gồm có:

  • Có thể có từ khóa khai báo phạm vi như public hoặc private
  • Kiểu dữ liệu (bắt buộc)
  • Tên trường (bắt buộc)

Từ khóa phạm vi hoạt động cho biết lớp nào có quyền được đọc/ghi trực tiếp lên thuộc tính. Bây giờ thì chúng ta chỉ xem qua 2 từ khóa chính là publicprivate thôi:

  • public – lớp nào cũng có thể đọc/ghi trực tiếp vào thuộc tính
  • private – chỉ có thể đọc/ghi từ chính lớp đó

Thông thường thì tất cả các thuộc tính nên được để private, tức là chỉ có thể truy xuất từ chính lớp chủ của nó, việc truy xuất từ các lớp khác nên được làm thông qua phương thức. Ví dụ:

public class Bicycle {
    private int gear;
    private int speed;

    public int getGear() {
        return gear;
    }

    public void setGear(int newValue) {
        gear = newValue;
    }

    public int getSpeed() {
        return speed;
    }

    public void speedUp(int increment) {
        speed += increment;
    }    
}

Kiểu dữ liệu của thuộc tính có thể là bất cứ kiểu dữ liệu nào, từ các kiểu dữ liệu cơ bản như int, float, boolean… cho đến các kiểu lớp và phức tạp hơn như String, mảng, đối tượng…v.v

Định nghĩa phương thức

Ví dụ về cú pháp của một phương thức như sau:

public double calculate(double first, double second) {
    // tính toán
}

Một phương thức cần ít nhất là kiểu dữ liệu trả về, cặp dấu ngoặc tròn () và khối lệnh trong cặp dấu ngoặc nhọn {}.

Ngoài ra phương thức còn có thể có từ khóa phạm vi (public, private) tên phương thức, danh sách các tham số trong cặp dấu ngoặc nhọn, tên phương thức, danh sách các lỗi ngoại lệ (sẽ tìm hiểu sau), phần thân phương thức trong cặp dấu ngoặc nhọn {}.

Chúng ta có thể có nhiều phương thức cùng tên nhưng khác danh sách tham số hoặc kiểu dữ liệu trả về, và được gọi là ghi đè phương thức, ví dụ:

public class Data {
    public void calculate(String s) {
        ...
    }

    public void calculate(int i) {
        ...
    }

    public void calculate(double f) {
        ...
    }    
}

Chúng ta không thể khai báo 2 phương thức có cùng tên, cùng kiểu dữ liệu trả về và cùng danh sách các phương thức.

Phương thức khởi tạo

Phương thức khởi tạo là các phương thức được gọi khi đối tượng được tạo ra, dùng để khai báo giá trị cho các thuộc tính, ví dụ:

public Bicycle(int startGear, int startSpeed) {
    gear = startGear;
    speed = startSpeed;
}

Để khởi tạo một đối tượng thì chúng ta dùng toán tử new như thường:

Bicycle myBike = new Bicycle(0, 8);

Toán tử new sẽ gọi phương thức khởi tạo Bicycle() ở trên.

Phương thức khởi tạo cũng giống như phương thức bình thường, chỉ khác là tên phương thức chính là tên lớp, và không có kiểu dữ liệu trả về. Chúng ta có thể có nhiều phương thức khởi tạo, miễn là chúng khác nhau danh sách tham số.

Phương thức khởi tạo không bắt buộc phải có, khi biên dịch thì trình biên dịch sẽ tự tạo ra một phương thức khởi tạo rỗng nếu chúng ta không khai báo.

Truyền tham số

Các phương thức khi được khai báo có thể nhận vào các biến, chúng ta gọi chúng là các tham số.  Ví dụ:

public double calculate(double a, double b) {
    ...
}

Trong đoạn code trên thì phương thức là calculate, có 2 biến tham số là ab có kiểu dữ liệu là double.

Cũng giống như các biến bình thường, kiểu dữ liệu của tham số cũng có thể là bất kì kiểu nào. Từ các kiểu cơ bản cho đến các kiểu phức tạp hơn.

Thông thường chúng ta hay khai báo số lượng các biến tham số một cách rõ ràng, tức là phương thức nhận vào 3 tham số thì chính xác là 3 tham số. Tuy nhiên Java có một cú pháp cho phép chúng ta truyền vào số lượng tham số bất kì. Ví dụ:

public double sum(double... values) {
    double result = 0;
    int n = values.length;
    for(int i = 0 ; i < n ; i++) {
        result += values[i];
    }
    return result;
}

Cú pháp này thực ra cũng chỉ là một dạng khác của mảng thôi, để khai báo thì chúng ta thêm 3 dấu chấm vào sau kiểu dữ liệu, và tham số lúc này sẽ là một mảng, chúng ta có thể thao tác như mảng bình thường. Khi truyền tham số thì chúng ta truyền từng giá trị đơn lẻ:

System.out.println(sum(2.3, 4.6, 1.0, 3.2, 9.8));

Các biến tham số cũng có phạm vi tương tự như các biến cục bộ, tức là chỉ có hiệu lực trong phương thức đó, do đó tên biến tham số không được trùng với tên các biến cục bộ khác. Tuy nhiên biến tham số có thể trùng tên với tên của thuộc tính, để có thể sử dụng 2 biến này với nhau chúng ta phải dùng đến con trỏ this, tuy nhiên con trỏ this sẽ được bàn đến trong các bài sau, và tốt hơn hết là không nên đặt tên 2 biến đó giống nhau.

Java 8 – Vòng lặp

Trong Java có các câu lệnh có tác dụng thực thi các câu lệnh khác một cách lặp đi lặp lại.

Câu lệnh while và do-while

Câu lệnh while sẽ thực hiện một khối lệnh trong khi một điều kiện nào đó vẫn còn đúng, cú pháp của câu lệnh while có dạng:

while(<biểu_thức>) {
    <câu_lệnh>
}

Đầu tiên là từ khóa while rồi đến một biểu thức có trả về true hoặc false, tiếp theo là khối lệnh. Nếu biểu thức cho ra kết quả true thì các câu lệnh trong khối lệnh sẽ được thực hiện. Ví dụ:

class WhileDemo {
    public static void main(String[] args) {
        int count = 1;
        while(count < 11) {
            System.out.println("Count to " + count);
            count++;
        }
    }
}

Đoạn code trên sẽ in các số từ 1 đến 10, vòng lặp while kiểm tra biến count, nếu biến count vẫn còn bé hơn 11 thì in dòng chữ ra màn hình và tăng biến count lên 1.

Count to 1
Count to 2
Count to 3
Count to 4
Count to 5
Count to 6
Count to 7
Count to 8
Count to 9
Count to 10

Nếu biểu thức luôn luôn cho kết quả true thì có thể tạo ra một vòng lặp chạy vô tận:

while(true) {
    ...
}

Ngoài câu lệnh while thì còn có một câu lệnh khác là do-while, cú pháp như sau:

do {
    <câu_lệnh>;
} while(<biểu_thức>);

Sự khác nhau giữa while và do-while là câu lệnh trong do-while sẽ được thực hiện rồi biểu thức mới được đánh giá, còn trong while thì biểu thức được đánh giá trước rồi mới thực hiện câu lệnh. Ví dụ:

public class DoWhile {
    public static void main(String[] args) {
        int count = 1;
        do {
            System.out.println("Count is: " + count);
            count++;
        } while(count < 11);
    }
}

Câu lệnh for

Câu lệnh for cho phép chúng ta thực hiện vòng lặp trong một khoảng giá trị nào đó, cú pháp như sau:

for ( <khởi_tạo_biến_lặp> ; <điều_kiện_lặp> ; <tăng_biến_lặp>) {
    <câu_lệnh>;
}

Đầu tiên là từ khóa for, sau đó là câu lệnh khởi tạo biến lặp, câu lệnh này sẽ được thực thi một lần, sau đó vòng lặp sẽ bắt đầu chạy, cứ mỗi lần chạy thì vòng lặp sẽ kiểm tra xem điều kiện lặp có đúng hay không, nếu đúng rồi thì thực hiện các câu lệnh trong khối lệnh rồi chạy câu lệnh tăng biến lặp lên, không thì dừng vòng lặp. Ví dụ:

class ForDemo {
    public static void main(String[] args) {
        for(int i = 1 ; i < 11 ; i++) {
            System.out.println("Count to: " + i);
        }
    }
}

Đoạn code trên sẽ in các số từ 1 đến 10.

Đầu tiên câu lệnh for sẽ khởi tạo biến lặp là i có giá trị là 1, sau đó kiểm tra xem i có bé hơn 11 hay không, nếu có thì in dòng chữ ra, rồi tăng i lên 1 và tiếp tục kiểm tra lại….

Count to 1
Count to 2
Count to 3
Count to 4
Count to 5
Count to 6
Count to 7
Count to 8
Count to 9
Count to 10

Biến lặp khi được khởi tạo chỉ có thể đọc/ghi trong khối lệnh của vòng lặp for. Nếu muốn sử dụng biến thì chúng ta có thể khai báo biến này ở ngoài. Ví dụ:

int i;
for(i = 1 ; i < 11 ; i++) {
    ...
}

Tên biến cũng không nhất thiết phải là i, có thể dùng a, b, c… đều được.

Ba thành phần trong câu lệnh cũng không nhất thiết phải có, chúng ta có thể bỏ cả 3 và tạo ra một vòng lặp vô tận:

for( ; ; ) {
    ...
}

Ngoài ra vòng lặp for còn có một cú pháp dùng để lặp qua các kiểu dữ liệu dạng danh sách như mảng, cú pháp như sau:

for(<kiểu_dữ_liệu> <tên_biến> : <tên_mảng) {
    <câu_lệnh>
}

Ví dụ:

class ForArray { 
    public static void main(String[] args) {
        int[] numbers = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
        for(int i : numbers) {
            System.out.println("Count to: " + i);
        }
    }
}

Trong đoạn code trên, thì biến i sẽ lần lượt có giá trị là các phàn tử của mảng numbers.

Count to 1
Count to 2
Count to 3
Count to 4
Count to 5
Count to 6
Count to 7
Count to 8
Count to 9
Count to 10

Java 8 – Câu lệnh điều kiện

Các câu lệnh trong một file .java được thực thi theo thứ tự từ trên xuống dưới, từ trái sang phải. Tuy nhiên có một số câu lệnh sẽ điều khiển hướng đi này theo một hướng khác, có thể là theo một điều kiện nào đó hoặc lặp đi lặp lại.

Câu lệnh if

Câu lệnh if là câu lệnh cơ bản nhất trong số các câu lệnh điều khiển hướng đi của chương trình. Câu lệnh này cho chương trình biết chỉ thực hiện một số câu lệnh chỉ khi có một điều kiện nào đó cho kết quả là true. Ví dụ:

class Bicycle {
    int speed = 0;
    void accelerate(int value) {
        speed = speed + value;
        if(speed > 10) {
            speed = 10;
        }
    }    
}

Trong đoạn code trên, chúng ta có lớp Bicycle có phương thức accelerate() nhận vào một giá trị, sau đó gán giá trị này cho thuộc tính speed, rồi chúng ta sử dụng câu lệnh if(speed > 10), tức là nếu thuộc tính speed có giá trị lớn hơn 10 thì thực hiện các câu lệnh trong khối lệnh sau đó, là gán giá trị cho speed10.

Câu lệnh if-else

Câu lệnh if-else là mở rộng từ câu lệnh if, tức là thêm một trường hợp nếu điều kiện cho kết quả false (điều kiện sai) thì thực hiện một số câu lệnh nào đó. Ví dụ:

class Bicycle {
    int speed = 0;
    void accelerate(int value) {
        if(speed + value <= 10) {
            speed = speed + value;
        } else {
            speed = 10;
        }
    }
}

Trong đoạn code trên, chúng ta lại có lớp Bicycle và phương thức accelerate(), trong đó chúng ta kiểm tra xem nếu speed + value cho ra kết quả bé hơn hoặc bằng 10 thì cộng speed bằng value, để kiểm tra trường hợp ngược lại thì chúng ta dùng từ khóa else và ghi các lâu lệnh phía sau đó, ở đây là gán giá trị 10 cho thuộc tính speed.

Ngoài ra chúng ta có thể dùng câu lệnh else if nhiều lần để kiểm tra nhiều điều kiện khác nhau, ví dụ:

class Score {
    int score = 8;
    String result = "";
    void grade) {
        if(score >= 9) {
            result = "Exellent";
        } else if(score >= 8) {
            result = "Good";
        } else if(score >= 5) {
            result = "Average";
        } else {
            result = "Below average";
        }
    }
}

Trong đoạn code trên chúng ta sử dụng câu lệnh if-else if-else nhiều lần, mỗi lần chúng ta kiểm tra giá trị của thuộc tính score và gán giá trị cho thuộc tính result tương ứng, ở đây thuộc tính score thỏa rất nhiều điều kiện nhưng khi chương trình gặp một điều kiện đúng thì chỉ thực hiện các câu lệnh trong điều kiện đó thôi.

Câu lệnh switch

Câu lệnh switch cũng tương tự như các câu lệnh ifif-else, tuy nhiên cách kiểm tra là xem một biến nào đó có bằng một giá trị nào đó hay không, chứ không thể kiểm tra nhiều điều kiện như if được, cũng vì cách kiểm tra đó cho nên switch chỉ kiểm tra được một số kiểu dữ liệu cơ bản nhất định, bao gồm char, byte, short, int và một số lớp nhất định là String, Character, Byte, Short, Interger. Ví dụ:

class SwitchDemo {
    public static void main(String[] args) {
        int month = 8;
        String monthString;
        switch(month) {
            case 1:
                monthString = "January";
                break; 
            case 2:
                monthString = "February";
                break;            
            case 3:
                monthString = "March";
               break;
            case 4:
                monthString = "April";
               break;
            case 5:
                monthString = "May";
               break;
            case 6:
                monthString = "June";
               break;
            case 7:
                monthString = "July";
               break;
            case 8:
                monthString = "August";
               break;
            case 9:
                monthString = "September";
               break;
            case 10:
                monthString = "October";
               break;
            case 11:
                monthString = "November";
               break;
            case 12:
                monthString = "December";
               break;
            default:
                monthString = "Invalid month";
                break;
        }
    }
}

Trong đoạn code trên thì monthString sẽ có giá trị là August.

Khối lệnh phía sau câu lệnh switch sẽ kiểm tra các giá trị của thuộc tính month, để kiểm tra một giá trị thì chúng ta ghi từ khóa case, rồi đến giá trị đó và dấu hai chấm. Theo sau là các câu lệnh cần thực hiện. Ngoài ra còn có một trường hợp đặc biệt là default, các câu lệnh sau default sẽ được thực hiện nếu không có case nào thỏa điều kiện.

Cuối cùng là câu lệnh break, câu lệnh này có tác dụng dừng thực hiện các câu lệnh điều kiện và vòng lặp (sẽ tìm hiểu sau). Lý do chúng ta sử dụng câu lệnh break trong switch là vì sau khi tìm được một giá trị phù hợp thì các giá trị phía sau case đó vẫn được thực thi. Trong đoạn code trên nếu chúng ta bỏ hết các câu lệnh break, thì các case 9. 10, 11, 12default vẫn sẽ được thực thi.

Java 8 – Biểu thức, câu lệnh, khối lệnh

Toán tử được dùng để tạo nên các biểu thức (expression), dùng để tính toán ra các giá trị, các biểu thức là các thành phần tạo nên các câu lệnh (statement), các câu lệnh gộp lại tạo nên khối lệnh (block).

Biểu thức

Một biểu thức được tạo lên từ các biến, toán tử và lời gọi phương thức và tính toán cho ra một kết quả nào đó. Chúng ta đã làm việc với biểu thức trong các bài trước rồi, ví dụ:

int speed = 0; 
int[] arr = new int[100];
System.out.println("Element 10 at index 9: " + arr[9]);

int result = 1 + 2;
System.out.println(result);    

Tất cả các dòng trên đều có biểu thức cả, từ toán tử gán, toán tử cộng cho tới phép lấy phần tử mảng, tất cả đều cho ra một giá trị nào đó, kiểu dữ liệu của giá trị này sẽ tùy thuộc vào các phần tử có trong biểu thức. Chẳng hạn như biểu thức speed = 0 cho ra kết quả kiểu int, bởi vì chúng ta đã khai báo biến speedint.

Chúng ta có thể thực hiện các biểu thức phức tạp bằng cách ghép nhiều biểu thức lại với nhau, như trong toán vậy:

1 * 2 * 3
x + y / 100

Nhiều khi cách viết như trên sẽ gây nhầm lẫn ở một số trường hợp, chẳng hạn như chúng ta không muốn chia trước và cộng sau, trong trường hợp này chúng ta có thể sử dụng cặp dấu ngoặc tròn () để báo cho Java biết biểu thức nào được thực hiện trước, biểu thức nào được thực hiện sau như trong toán.

(x + y) / 100
x + (y / 100)

Câu lệnh

Câu lệnh trong Java là các dòng chỉ thị yêu cầu thực hiện một việc gì đó, mỗi câu lệnh được kết thúc bởi một dấu chấm phẩy ';'.

Thực chất thì các biểu thức chính là các câu lệnh, bởi vì chúng đều thực hiện một công việc gì đó, câu lệnh thì có ý nghĩa rộng hơn một tí, vì câu lệnh có thể là lời gọi tạo đối tượng, lời gọi phương thức nữa…v.v

int value = 8933;
value++;
System.out.println("Hello World");
Bicycle bike = new Bicycle();

Khối lệnh

Khối lệnh là một nhóm các câu lệnh được bọc trong cặp dấu ngoặc nhọn {}. Ví dụ:

class Block {
    public static void main(String[] args) {
        boolean pass = true;
        if(pass == true) {
            System.out.println("Condition is true");
            System.out.println("You are passed");
        } else {
            System.out.println("Condition is false");
            System.out.println("You are not passed");
        }
    }
}

Trong đoạn code trên có rất nhiều khối lệnh, đầu tiên là khối lệnh sau Block, gồm có một câu lệnh khai báo phương thức main(). Theo sau là một khối lệnh, khai báo biến pass, rồi tới câu lệnh điều kiện if-else lại có 2 khối lệnh nữa… Chúng ta sẽ tìm hiểu câu lệnh điều kiện sau.