Category Archives: Java

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.

Java 8 – Thừa kế

Trong các bài trước, chúng ta đã làm việc sơ qua với tính năng thừa kế trong Java. Trong Java thì các lớp có thể thừa kế lại các lớp khác, tức là có chứa các thuộc tính và phương thức của các lớp khác.

Một lớp kế thừa từ lớp khác còn được gọi là subclass (hoặc lớp con). Lớp được các lớp khác kế thừa lại thì được gọi là superclass (hay lớp cha hoặc lớp cơ cở).

Mặc định trong Java thì toàn bộ các lớp, kể cả lớp được định nghĩa bởi lập trình viên, nếu không khai báo kế thừa từ lớp nào thì đều được kế thừa từ một lớp gốc có tên là Object.

Các lớp cũng có thể được kế thừa từ một lớp đã được kế thừa, cứ như vậy, và tất nhiên lớp cha cao nhất là lớp Object.

Tính năng thừa kế là một tính năng rất mạnh mẽ, nó cho phép chúng ta tạo thêm lớp mới nhưng không cần viết các đoạn code đã có sẵn trong các lớp khác, chúng ta chỉ cần “kế thừa” lại và viết thêm các đoạn code mới.

Một lớp con kế thừa toàn bộ các thuộc tính, phương thức và các lớp lồng nhau của lớp cha. Tuy nhiên không kế thừa phương thức khởi tạo (constructor), nhưng chúng ta có thể gọi phương thức khởi tạo của lớp cha trong lớp con.

Cây phân lớp Java

Lớp Object được định nghĩa trong gói java.lang định nghĩa tất cả các hành vi của toàn bộ các lớp, kể cả các lớp mà chúng ta tự viết. Trong Java thì có một số lớp kế thừa trực tiếp từ lớp Object, và một số lớp khác lại kế thừa từ các lớp này, tạo thành một cây phân cấp các lớp.

Ví dụ

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

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

    public Bicycle(int gear, int speed) {
        this.gear = gear;
        this.speed = 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;
    } 
}

Lớp Bicycle có 2 thuộc tính và 4 phương thức. Trong đó phương thức Bicycle() là phương thức khởi tạo, phương thức này nhận vào 2 tham số là speedgear dùng để gán cho 2 thuộc tính speedgear.

Sau đó chúng ta định nghĩa lớp MountainBike kế thừa lớp Bicycle như sau:

public class MountainBike extends Bicycle {
        
    public int seatHeight;

    public MountainBike(int startHeight,                        
                        int speed,
                        int gear) {
        super(seatHeight, speed, gear);
        seatHeight = startHeight;
    }   
        
    public void setHeight(int newValue) {
        seatHeight = newValue;
    }   
}

Chúng ta cho lớp MountainBike kế thừa từ lớp Bicycle bằng cách thêm từ khóa extends Bicycle vào sau tên MountainBike. Bên trong chúng ta định nghĩa thêm thuộc tính seatHeight, 2 phương thức MountainBike()setHeight(). Phương thức khởi tạo MountainBike()nhận vào các tham số seatHeight, speedgear, chúng ta không gán giá trị trực tiếp cho các thuộc tính speedgear của lớp MountainBike, mà thay vào đó chúng ta gọi phương thức khởi tạo của lớp cha là super() và truyền vào các giá trị speedgear. Phương thức super() sẽ gọi phương thức khởi tạo của lớp Bicycle(), và như thế thì các giá trị speedgear của lớp MountainBike sẽ được gán luôn trong đó

Quyền thừa kế của lớp con

  • Lớp con kế thừa toàn bộ các phương thức và thuộc tính có phạm vi publicprotected của lớp cha. Nếu lớp con nằm chung gói (package) với lớp cha thì sẽ kế thừa cả các thuộc tính và phương thức private.
  • Có thể định nhĩa thuộc tính và phương thức mới.
  • Các thuộc tính và phương thức được thừa kế được sử dụng trực tiếp như các thuộc tính và phương thức bình thường.
  • Có thể định nghĩa thuộc tính trong lớp con trùng tên với thuộc tính trong lớp cha, tuy nhiên thông thường chúng ta không nên làm thế.
  • Có thể định nghĩa phương thức trong lớp con trùng tên với phương thức trong lớp cha, việc này còn được gọi là ghi đè (overriding).
  • Có thể định nghĩa phương thức static trùng tên.

 

Các thành phần private

Một lớp con không được thừa kế các thành phần private của lớp cha. Tuy nhiên nếu lớp cha có phương thức public hoặc protected mà có sử dụng đến các thành phần private thì lớp con cũng có thể gọi các phương thức này.

 

Ép kiểu đối tượng

Các đối tượng thuộc lớp cha có thể sử dụng phương thức khởi tạo của các lớp con. Tức là chúng ta có thể tạo đối tượng Bicycle như sau:

Bicycle bike = new MountainBike();
Object obj = new Bicycle();
...

Trường hợp ngược lại thì không được phép trong Java. Nếu chúng ta viết:

MoutainBike bike = new Bibycle();

Thì trình biên dịch sẽ báo lỗi. Tuy nhiên chúng ta có thể ép kiểu:

MountainBike bike = (Bicycle)new Bicycle();

Java 8 – Phương thức default và static trong Interface

Trong phần trước chúng ta đã tìm hiểu về interface, chúng ta đã biết là khi viết thêm các phương thức và interface thì chúng ta phải code các phương thức đó cho toàn bộ các lớp đã implement interface đó, nếu không thì sẽ bị lỗi biên dịch.

Phương thức default

Phương thức default (mặc định) cho phép chúng ta viết thêm phương thức mới vào interface mà không lo bị lỗi biên dịch.

Giả sử chúng ta có đoạn code interface Blog như sau:

 

public interface Blog {
    void new_post();
    void delete_post();
}

Và chúng ta định nghĩa lớp PhoCode implement lại interface này như sau:

public class PhoCode implements Blog {
    String title = "phocode.com";

    void new_post() {
        // ...
    }

    void delete_post() {
        // ...
    }
}

Interface Blog có 2 phương thức là new_post()delete_post(). Giả sử chúng ta muốn thêm một phương thức mới vào interface này là update_post() như sau:

public interface Blog {
    void new_post();
    void delete_post();
    void update_post();
}

Chúng ta sẽ phải viết thêm phần code cho phương thức update_post() trong lớp PhoCode.

public class PhoCode implements Blog {
    String title = "phocode.com";
   
    void new_post() {
        // ...
    }

    void delete_post() {
        // ... 
    }

    void update_post() {
        // ...
    }
}

Việc viết lại các phương thức mới của interface vào toàn bộ các lớp có implement rất mệt mỏi. Do đó chúng ta có thể khai báo phương thức mặc định trong interface như sau:

public interface Blog {
    void new_post();
    void delete_post();
    default void update_post() {
        // ...
    }
}

Chúng ta khai báo một phương thức mà mặc định bằng cách đưa từ khóa default ra trước tên phương thức, tất cả các phương thức trong interface đều có phạm vi hoạt động là public. Đối với phương thức mặc định thì chúng ta viết phần code cho phương thức đó luôn, tức là có phần ngoặc nhọn {} thay vì dấu chấm phẩy phía sau tên phương thức.

Và khi khai báo một phương thức là mặc định thì các lớp implement lại interface đó không cần phải viết lại phần code cho phương thức mặc định và có thể gọi đến các phương thức mặc định như bình thường.

Kế thừa interface có phương thức mặc định

Các interface kế thừa lại interface có chứa phương thức mặc định có thể thực hiện các việc sau:

  • Không khai báo lại các phương thức mặc định, Java sẽ tự hiểu là kế thừa lại 100% phương thức mặc định
  • Kế thừa lại phương thức mặc định nhưng không khai báo phần thân phương thức {}, lúc này phương thức đó sẽ trở lại là phương thức trừu tượng.
  • Kế thừa lại và có khai báo phần thân phương thức, tức là override lại phương thức mặc định

Giả sử chúng ta có interface BlogV2 kế thừa Blog như sau:

public interface BlogV2 extends Blog {

}

Phương thức BlogV2 không khai báo phương thức update_post(), tức là mặc định sẽ kế thừa lại hoàn toàn phương thức này.

Nếu khai báo như sau:

public interface BlogV2 extends Blog {
    public void update_post();
}

Thì lúc này phương thức update_post() trong BlogV2 trở lại là phương thức trừu tượng, và tất cả các lớp implement interface BlogV2 sẽ phải code phương thức update_post().

Và cuối cùng chúng ta cũng có thể khai báo lại phương thức này là phương thức default, tức là ghi đè phương thức:

public interface BlogV2 extends Blog {
    default void update_post() {
        // ...
    }
}

Phương thức static

Cũng tương tự như các class, chúng ta có thể khai báo phương thức static (tĩnh) bên trong một interface. Ví dụ:

public interface Blog {
    String title = "phocode.com";

    static String getTitle() {
        return this.title;
    }
}

Chúng ta khai báo một phương thức static bằng cách ghi từ khóa static ra trước tên phương thức. Sau tên phương thức chúng ta khai báo phần thân phương thức trong cặp dấu ngoặc nhọn {}.

Phương thức static là phương thức dùng chung cho toàn bộ interface giống như với class, tức là tất cả các đối tượng implement interface có phương thức static đều kế thừa luôn 100% phương thức static đó, không thể override lại được.

Java 8 – Interface

Trong Java thì interface (giao diện) là một kiểu dữ liệu tham chiếu tương tự như class, nhưng chỉ có thể chứa hằng số và tên các phương thức, không chứa phần thân phương thức, tuy nhiên chúng ta vẫn có thể viết phần thân phương thức cho các phương thức tĩnh và phương thức mặc định. Interface không thể được khởi tạo như lớp mà chỉ có thể được mở rộng từ các lớp khác hoặc được kế thừa từ các interface khác.

Để định nghĩa một interface thì cũng tương tự như định nghĩa một lớp:

public interface Blog {
   const String title = "phocode.com";

   void newPost();
   void deletePost();
   void updatePost();
}

Bên trong định nghĩa interface chỉ có hằng số hoặc tên phương thức, phương thức không chứa phần thân, tức là không có phần ngoặc nhọn {}, mà kết thúc bằng dấu chấm phẩy ;.

Để sử dụng interface thì phải có một lớp code lại interface đó bằng từ khóa implements, và lớp đó phải code lại toàn bộ phương thức có trong interface. Ví dụ:

public class PhoCode implements Blog {
    void newPost() {     
    }
    void deletePost {
    }
    void updatePost {
    }   
}

Trong đoạn code trên, lớp PhoCode kế thừa interface Blog, và bên trong lớp PhoCode phải code lại các phương thức có trong interface này, tức là phải có phần ngoặc nhọn {} phía sau tên phương thức.

Định nghĩa một interface

Chúng ta định nghĩa một interface bằng cách ghi từ khóa phạm vi hoạt động, rồi đến từ khóa interface, sau đó là tên của interface mà chúng ta muốn đặt, tiếp theo là danh sách các interface kế thừa nếu có, cuối cùng là phần ngoặc nhọn {} khai báo các phương thức và hằng số. Ví dụ:

public interface Blog extends Http, Database {
    String title = "phocode.com";

    void newPost();
    void deletePost();
    void updatePost();
}

Từ khóa public cho biết interface này có thể sử dụng ở bất kỳ gói nào, nếu chúng ta không khai báo từ khóa phạm vi nào thì mặc định interface này chỉ có thể được sử dụng trong cùng một gói.

Một interface có thể được kế thừa từ nhiều interface khác. Khác với class là mỗi class chỉ có thể kế thừa từ một class thì interface có thể kế thừa từ nhiều interface khác, danh sách các interface kế thừa cách nhau bởi dấu phẩy.

Các phương thức của interface có thể là phương thức trừu tượng, phương thức mặc định và phương thức tĩnh. Phương thức ảo chính là phương thức mà bình thường mà chúng ta viết, không có gì đặc biệt, phương thức mặc định thì có từ khóa default (chúng ta sẽ tìm hiểu phương thức mặc định sau), phương thức tĩnh thì có từ khóa staticTất cả các phương thức trong interface đều mặc định là public nên chúng ta không cần khai báo public.

Tất cả các hằng số trong interface là public, staticfinal.

Implement interface

Chúng ta khai báo từ khóa implements sau tên lớp rồi ghi tên danh sách các interface ra (nếu implement nhiều interface).

Lưu ý là nếu lớp này có kế thừa từ lớp khác thì từ khóa extends sẽ luôn đứng trước từ khóa implements.

Chẳng hạn như chúng ta có interface chứa phương thức so sánh xem đối tượng nào “lớn hơn” đối tượng nào như sau:

public interface Comparison {        
    public int isGreaterThan(Comparison other);
}

Sau này giả sử dụng ta có lớp Blog, và chúng ta muốn so sánh 2 đối tượng Blog với nhau thì chúng ta cho lớp Blog implements interface trên, bên trong lớp Blog chúng ta code lại phương thức isLargerThan() là xong. Tất nhiên là với điều kiện chúng ta phải có cách để so sánh 2 đối tượng, bởi vì ở đây chúng ta không so sánh 2 giá trị thô như so sánh 2 số nguyên, chẳng hạn như so sánh 2 đối tượng Blog thì so sánh xem đối tượng nào có nhiều bài viết hơn, nhiều comment hơn… v.v Ví dụ:

public class Blog implements Comparison {
    private int post_count = 0;

    int getPostCount() {
        return this.post_count;
    }

    public int isLargerThan(Comparison otherBlog) {
        if (this.getPostCount() < otherRect.getPostCount())
            return -1;
        else if (this.getPostCount() > otherRect.getPostCount())
            return 1;
        else
            return 0;               
    }
}

Trong đoạn code trên chúng ta viết lớp Blog có phương thức getPostCount() là phương thức lấy về số lượng bài viết của Blog, rồi implements interface Comparison, trong đó chúng ta code lại phương thức isLargerThan(), phương thức này nhận vào tham số là một đối tượng Blog khác, chúng ta kiểm tra xem đối tượng nào có phương thức getPostCount() lớn hơn thì trả về 1, bé hơn là -1 và bằng nhau là 0. Trên đây chỉ là một cách để chúng ta so sánh, trên thực tế chúng ta có nhiều cách khác để so sánh 2 đối tượng.

Dùng Interface làm kiểu dữ liệu

Việc định nghĩa một interface giống như định nghĩa một kiểu dữ liệu tham chiếu mới vậy. Hầu như chỗ nào chúng ta đặt tên lớp thì chúng ta cũng có thể thay bằng tên interface.

Nếu chúng ta khai báo một đối tượng có kiểu dữ liệu của một interface thì trong đoạn code khởi tạo đối tượng đó phải là khởi tạo từ một lớp có implement interface đó. Ví dụ:

public interface Blog {
    //  ...
}

public class PhoCode implements Blog {
    public static void main(String[] args) {
        Blog phocode_blog = new PhoCode();
    }
}

Mở rộng interface

Giả sử chúng ta có interface Blog với 2 phương thức mẫu như sau:

public interface Blog {
    void newPost();
    void deletePost();
}

Giả sử interface này đã được rất nhiều class implement lại, nhưng bây giờ chúng ta lại muốn thêm vào interface này một phương thức như sau:

public interface Blog {
    void newPost();
    void deletePost();
    void updatePost();
}

Và kết quả là toàn bộ các lớp có implement interface này sẽ sụp đổ vì chúng chưa code lại phương thức updatePost() mới.

Để tránh tình trạng này thì chúng ta nên đoán trước các phương thức mà chúng ta sẽ code và khai báo toàn bộ trong interface ngay từ đầu. Một cách khác là chúng ta viết một interface mới kế thừa từ interface cũ, chẳng hạn như:

public interface BlogPlus extends Blog {
    void updatePost();   
}

Và các coder sẽ có 2 lựa chọn là dùng interface cũ của bạn hoặc chuyển qua interface mới có đầy đủ các tính năng hơn.

Hoặc chúng ta có một cách khác là khai báo phương thức mặc định với từ khóa default như sau:

public interface Blog {
    void newPost();
    void deletePost();
    default updatePost() {
        // ...
    }     
}

Phương thức mặc định sẽ được code ngay bên trong interface chứ không được kế thừa nữa. Chúng ta sẽ tìm hiểu phương thức mặc định trong bài sau.