1. Giao diện

Để hiểu chức năng lambda là gì, trước tiên bạn cần hiểu giao diện là gì. Vì vậy, hãy nhớ lại những điểm chính.

Một giao diện là một biến thể của khái niệm về một lớp. Giả sử một lớp bị cắt ngắn nhiều. Không giống như một lớp, một giao diện không thể có các biến riêng của nó (ngoại trừ các biến tĩnh). Bạn cũng không thể tạo các đối tượng có kiểu là một giao diện:

  • Bạn không thể khai báo các biến của lớp
  • Bạn không thể tạo đối tượng

Ví dụ:

interface Runnable
{
   void run();
}
Ví dụ về giao diện tiêu chuẩn

Sử dụng một giao diện

Vậy tại sao lại cần một giao diện? Các giao diện chỉ được sử dụng cùng với sự kế thừa. Các lớp khác nhau có thể kế thừa cùng một giao diện, hay như người ta vẫn nói - các lớp triển khai giao diện .

Nếu một lớp cài đặt một giao diện, thì nó phải cài đặt các phương thức được khai báo nhưng không được cài đặt bởi giao diện. Ví dụ:

interface Runnable
{
   void run();
}

class Timer implements Runnable
{
   void run()
   {
      System.out.println(LocalTime.now());
   }
}

class Calendar implements Runnable
{
   void run()
   {
      var date = LocalDate.now();
      System.out.println("Today: " + date.getDayOfWeek());
   }
}

Lớp Timerthực hiện Runnablegiao diện, vì vậy nó phải khai báo bên trong chính nó tất cả các phương thức có trong Runnablegiao diện và thực hiện chúng, tức là viết mã trong phần thân phương thức. Đối với lớp học cũng vậy Calendar.

Nhưng bây giờ Runnablecác biến có thể lưu trữ các tham chiếu đến các đối tượng thực hiện Runnablegiao diện.

Ví dụ:

Mã số Ghi chú
Timer timer = new Timer();
timer.run();

Runnable r1 = new Timer();
r1.run();

Runnable r2 = new Calendar();
r2.run();

Phương run()thức trong Timerlớp sẽ được gọi


Phương run()thức trong Timerlớp sẽ được gọi


Phương run()thức trong Calendarlớp sẽ được gọi

Bạn luôn có thể gán một tham chiếu đối tượng cho một biến thuộc bất kỳ loại nào, miễn là loại đó là một trong các lớp tổ tiên của đối tượng. Đối với các lớp TimerCalendar, có hai loại như vậy: ObjectRunnable.

Nếu bạn gán một tham chiếu đối tượng cho một Objectbiến, bạn chỉ có thể gọi các phương thức được khai báo trong Objectlớp. Và nếu bạn gán một tham chiếu đối tượng cho một Runnablebiến, bạn có thể gọi các phương thức của Runnablekiểu đó.

Ví dụ 2:

ArrayList<Runnable> list = new ArrayList<Runnable>();
list.add (new Timer());
list.add (new Calendar());

for (Runnable element: list)
    element.run();

Mã này sẽ hoạt động, bởi vì các đối tượng TimerCalendarcó các phương thức chạy hoạt động hoàn hảo. Vì vậy, gọi cho họ không phải là một vấn đề. Nếu chúng ta vừa thêm một phương thức run() vào cả hai lớp, thì chúng ta sẽ không thể gọi chúng theo cách đơn giản như vậy.

Về cơ bản, Runnablegiao diện chỉ được sử dụng làm nơi đặt phương thức chạy.



2. Sắp xếp

Hãy chuyển sang một cái gì đó thiết thực hơn. Ví dụ, hãy xem việc sắp xếp các chuỗi.

Để sắp xếp một tập hợp các chuỗi theo thứ tự bảng chữ cái, Java có một phương thức tuyệt vời được gọi làCollections.sort(collection);

Phương thức tĩnh này sắp xếp bộ sưu tập đã qua. Và trong quá trình sắp xếp, nó thực hiện so sánh theo cặp các phần tử của nó để hiểu liệu các phần tử có nên hoán đổi hay không.

Trong quá trình sắp xếp, các so sánh này được thực hiện bằng compareTophương thức (), mà tất cả các lớp tiêu chuẩn đều có: Integer, String, ...

Phương thức so sánh () của lớp Số nguyên so sánh các giá trị của hai số, trong khi phương thức so sánh () của lớp Chuỗi xem xét thứ tự bảng chữ cái của chuỗi.

Vì vậy, một tập hợp các số sẽ được sắp xếp theo thứ tự tăng dần, trong khi một tập hợp các chuỗi sẽ được sắp xếp theo thứ tự bảng chữ cái.

Các thuật toán sắp xếp thay thế

Nhưng nếu chúng ta muốn sắp xếp các chuỗi không theo thứ tự bảng chữ cái mà theo độ dài của chúng thì sao? Và nếu chúng ta muốn sắp xếp các số theo thứ tự giảm dần thì sao? Bạn làm gì trong trường hợp này?

Để xử lý các tình huống như vậy, Collectionslớp có một sort()phương thức khác có hai tham số:

Collections.sort(collection, comparator);

Trong đó bộ so sánh là một đối tượng đặc biệt biết cách so sánh các đối tượng trong một bộ sưu tập trong quá trình sắp xếp . Thuật ngữ so sánh xuất phát từ từ so sánh tiếng Anh , do đó bắt nguồn từ so sánh , có nghĩa là "so sánh".

Vậy đối tượng đặc biệt này là gì?

Comparatorgiao diện

Chà, tất cả đều rất đơn giản. Loại sort()tham số thứ hai của phương thức làComparator<T>

Trong đó T là một tham số kiểu cho biết kiểu của các phần tử trong tập hợpComparatorlà một giao diện có một phương thức duy nhấtint compare(T obj1, T obj2);

Nói cách khác, một đối tượng so sánh là bất kỳ đối tượng nào của bất kỳ lớp nào thực hiện giao diện Bộ so sánh. Giao diện Bộ so sánh trông rất đơn giản:

public interface Comparator<Type>
{
   public int compare(Type obj1, Type obj2);
}
Mã cho giao diện Bộ so sánh

Phương compare()thức so sánh hai đối số được truyền cho nó.

Nếu phương thức trả về một số âm, điều đó có nghĩa là obj1 < obj2. Nếu phương thức trả về một số dương, điều đó có nghĩa là obj1 > obj2. Nếu phương thức trả về 0, điều đó có nghĩa là obj1 == obj2.

Đây là một ví dụ về một đối tượng so sánh so sánh các chuỗi theo độ dài của chúng:

public class StringLengthComparator implements Comparator<String>
{
   public int compare (String obj1, String obj2)
   {
      return obj1.length() – obj2.length();
   }
}
mã của StringLengthComparatorlớp

Để so sánh độ dài của chuỗi, chỉ cần trừ độ dài này cho độ dài kia.

Mã hoàn chỉnh cho một chương trình sắp xếp các chuỗi theo độ dài sẽ như sau:

public class Solution
{
   public static void main(String[] args)
   {
      ArrayList<String> list = new ArrayList<String>();
      Collections.addAll(list, "Hello", "how's", "life?");
      Collections.sort(list, new StringLengthComparator());
   }
}

class StringLengthComparator implements Comparator<String>
{
   public int compare (String obj1, String obj2)
   {
      return obj1.length() – obj2.length();
   }
}
Sắp xếp chuỗi theo độ dài


3. Đường cú pháp

Bạn nghĩ gì, mã này có thể được viết gọn hơn không? Về cơ bản, chỉ có một dòng chứa thông tin hữu ích — obj1.length() - obj2.length();.

Nhưng mã không thể tồn tại bên ngoài một phương thức, vì vậy chúng tôi phải thêm một compare()phương thức và để lưu trữ phương thức đó, chúng tôi phải thêm một lớp mới — StringLengthComparator. Và chúng ta cũng cần chỉ định các loại biến... Mọi thứ dường như đều đúng.

Nhưng có nhiều cách để làm cho mã này ngắn hơn. Chúng tôi đã có một số đường cú pháp cho bạn. Hai muỗng!

Lớp bên trong ẩn danh

Bạn có thể viết mã so sánh ngay bên trong main()phương thức và trình biên dịch sẽ thực hiện phần còn lại. Ví dụ:

public class Solution
{
    public static void main(String[] args)
    {
        ArrayList<String> list = new ArrayList<String>();
        Collections.addAll(list, "Hello", "how's", "life?");

        Comparator<String> comparator = new Comparator<String>()
        {
            public int compare (String obj1, String obj2)
            {
                return obj1.length() – obj2.length();
            }
        };

        Collections.sort(list, comparator);
    }
}
Sắp xếp chuỗi theo độ dài

Bạn có thể tạo một đối tượng cài đặt Comparatorgiao diện mà không cần tạo một lớp rõ ràng! Trình biên dịch sẽ tự động tạo và đặt tên tạm thời cho nó. Hãy so sánh:

Comparator<String> comparator = new Comparator<String>()
{
    public int compare (String obj1, String obj2)
    {
        return obj1.length() – obj2.length();
    }
};
Lớp bên trong ẩn danh
Comparator<String> comparator = new StringLengthComparator();

class StringLengthComparator implements Comparator<String>
{
    public int compare (String obj1, String obj2)
    {
        return obj1.length() – obj2.length();
    }
}
StringLengthComparatorlớp học

Màu giống nhau được sử dụng để biểu thị các khối mã giống hệt nhau trong hai trường hợp khác nhau. Sự khác biệt là khá nhỏ trong thực tế.

Khi trình biên dịch gặp khối mã đầu tiên, nó chỉ cần tạo khối mã thứ hai tương ứng và đặt cho lớp một số tên ngẫu nhiên.


4. Biểu thức Lambda trong Java

Giả sử bạn quyết định sử dụng một lớp bên trong ẩn danh trong mã của mình. Trong trường hợp này, bạn sẽ có một khối mã như thế này:

Comparator<String> comparator = new Comparator<String>()
{
    public int compare (String obj1, String obj2)
    {
        return obj1.length() – obj2.length();
    }
};
Lớp bên trong ẩn danh

Ở đây chúng tôi kết hợp việc khai báo một biến với việc tạo ra một lớp ẩn danh. Nhưng có một cách để làm cho mã này ngắn hơn. Ví dụ, như thế này:

Comparator<String> comparator = (String obj1, String obj2) ->
{
    return obj1.length() – obj2.length();
};

Dấu chấm phẩy là cần thiết bởi vì ở đây chúng ta không chỉ có khai báo lớp ẩn mà còn có việc tạo một biến.

Ký hiệu như thế này được gọi là biểu thức lambda.

Nếu trình biên dịch gặp ký hiệu như thế này trong mã của bạn, thì nó chỉ tạo ra phiên bản dài dòng của mã (với một lớp bên trong ẩn danh).

Lưu ý rằng khi viết biểu thức lambda, chúng tôi đã bỏ qua không chỉ tên của lớp mà còn cả tên của phương thức.Comparator<String>int compare()

Quá trình biên dịch sẽ không gặp vấn đề gì khi xác định phương thức , bởi vì biểu thức lambda chỉ có thể được viết cho các giao diện có một phương thức duy nhất . Nhân tiện, có một cách để vượt qua quy tắc này, nhưng bạn sẽ tìm hiểu về điều đó khi bắt đầu nghiên cứu sâu hơn về OOP (chúng ta đang nói về các phương pháp mặc định).

Hãy xem lại phiên bản dài dòng của mã, nhưng chúng ta sẽ tô xám phần có thể bỏ qua khi viết biểu thức lambda:

Comparator<String> comparator = new Comparator<String>()
{
    public int compare (String obj1, String obj2)
   {
      return obj1.length() – obj2.length();
   }
};
Lớp bên trong ẩn danh

Có vẻ như không có gì quan trọng đã được bỏ qua. Thật vậy, nếu Comparatorgiao diện chỉ có một compare()phương thức, trình biên dịch hoàn toàn có thể khôi phục mã bị mờ từ mã còn lại.

Sắp xếp

Nhân tiện, bây giờ chúng ta có thể viết mã sắp xếp như thế này:

Comparator<String> comparator = (String obj1, String obj2) ->
{
   return obj1.length() – obj2.length();
};
Collections.sort(list, comparator);

Hoặc thậm chí như thế này:

Collections.sort(list, (String obj1, String obj2) ->
   {
      return obj1.length() – obj2.length();
   }
);

Chúng tôi chỉ cần ngay lập tức thay thế comparatorbiến bằng giá trị được gán cho comparatorbiến.

Loại suy luận

Nhưng đó không phải là tất cả. Mã trong các ví dụ này thậm chí có thể được viết gọn hơn. Đầu tiên, trình biên dịch có thể tự xác định rằng các biến obj1obj2Strings. Và thứ hai, dấu ngoặc nhọn và câu lệnh trả về cũng có thể được bỏ qua nếu bạn chỉ có một lệnh duy nhất trong mã phương thức.

Phiên bản rút gọn sẽ như thế này:

Comparator<String> comparator = (obj1, obj2) ->
   obj1.length() – obj2.length();

Collections.sort(list, comparator);

Và nếu thay vì sử dụng comparatorbiến, chúng tôi ngay lập tức sử dụng giá trị của nó, thì chúng tôi sẽ nhận được phiên bản sau:

Collections.sort(list, (obj1, obj2) ->  obj1.length() — obj2.length() );

Vâng, bạn nghĩ gì về điều đó? Chỉ một dòng mã không có thông tin thừa — chỉ các biến và mã. Không có cách nào để làm cho nó ngắn hơn! Hay là có?



5. Cách thức hoạt động

Trên thực tế, mã có thể được viết gọn hơn nữa. Nhưng nhiều hơn về điều này sau.

Bạn có thể viết một biểu thức lambda trong đó bạn sẽ sử dụng một loại giao diện với một phương thức duy nhất.

Ví dụ: trong mã , bạn có thể viết biểu thức lambda vì chữ ký của phương thức giống như sau:Collections.sort(list, (obj1, obj2) -> obj1.length() - obj2.length());sort()

sort(Collection<T> colls, Comparator<T> comp)

Khi chúng tôi chuyển ArrayList<String>bộ sưu tập làm đối số đầu tiên cho phương thức sắp xếp, trình biên dịch có thể xác định rằng loại của đối số thứ hai là . Và từ đó, kết luận rằng giao diện này có một phương thức duy nhất. Mọi thứ khác là một kỹ thuật.Comparator<String>int compare(String obj1, String obj2)