JavaRush /Blog Java /Random-VI /Phổ biến về biểu thức lambda trong Java. Với các ví dụ và...
Стас Пасинков
Mức độ
Киев

Phổ biến về biểu thức lambda trong Java. Với các ví dụ và nhiệm vụ. Phần 1

Xuất bản trong nhóm
Bài viết này dành cho ai?
  • Dành cho những người nghĩ rằng họ đã biết rõ về Java Core nhưng chưa biết gì về biểu thức lambda trong Java. Hoặc có lẽ bạn đã nghe điều gì đó về lambdas nhưng không có thông tin chi tiết.
  • dành cho những người đã có chút hiểu biết về biểu thức lambda nhưng vẫn còn e ngại và chưa quen khi sử dụng chúng.
Nếu bạn không thuộc một trong những loại này, bạn có thể thấy bài viết này nhàm chán, không chính xác và nói chung là “không hay”. Trong trường hợp này, bạn có thể thoải mái bỏ qua hoặc nếu bạn hiểu rõ về chủ đề này, hãy đề xuất trong phần nhận xét cách tôi có thể cải thiện hoặc bổ sung bài viết. Tài liệu không đòi hỏi bất kỳ giá trị học thuật nào, càng không có tính mới lạ. Đúng hơn thì ngược lại: trong đó tôi sẽ cố gắng mô tả những thứ phức tạp (đối với một số người) một cách đơn giản nhất có thể. Tôi được truyền cảm hứng để viết bởi một yêu cầu giải thích về luồng api. Tôi đã suy nghĩ về điều đó và quyết định rằng nếu không hiểu các biểu thức lambda, một số ví dụ của tôi về “luồng” sẽ không thể hiểu được. Vì vậy, hãy bắt đầu với lambdas. Phổ biến về biểu thức lambda trong Java.  Với các ví dụ và nhiệm vụ.  Phần 1 - 1Cần có kiến ​​thức gì để hiểu bài viết này:
  1. Hiểu biết về lập trình hướng đối tượng (sau đây gọi tắt là OOP), cụ thể:
    • kiến thức về các lớp và đối tượng là gì, sự khác biệt giữa chúng là gì;
    • kiến thức về giao diện là gì, chúng khác với các lớp như thế nào, mối liên hệ giữa chúng là gì (giao diện và lớp);
    • kiến thức về phương thức là gì, cách gọi nó, phương thức trừu tượng là gì (hoặc phương thức không cần triển khai), tham số/đối số của phương thức là gì, cách truyền chúng đến đó;
    • công cụ sửa đổi truy cập, phương thức/biến tĩnh, phương thức/biến cuối cùng;
    • kế thừa (lớp, giao diện, kế thừa nhiều giao diện).
  2. Kiến thức về Java Core: generic, bộ sưu tập (danh sách), luồng.
Vâng, hãy bắt đầu.

Một ít lịch sử

Các biểu thức Lambda đến với Java từ lập trình hàm và từ toán học. Vào giữa thế kỷ 20 ở Mỹ, một Giáo hội Alonzo nào đó làm việc tại Đại học Princeton, người rất yêu thích toán học và tất cả các loại trừu tượng. Chính Alonzo Church là người đã nghĩ ra phép tính lambda, ban đầu nó là một tập hợp một số ý tưởng trừu tượng và không liên quan gì đến lập trình. Đồng thời, các nhà toán học như Alan Turing và John von Neumann cũng làm việc tại cùng một trường Đại học Princeton. Mọi thứ kết hợp với nhau: Church nghĩ ra hệ thống tính toán lambda, Turing phát triển cỗ máy tính toán trừu tượng của mình, hiện được gọi là “máy Turing”. Chà, von Neumann đã đề xuất một sơ đồ kiến ​​trúc của máy tính, sơ đồ này hình thành nên nền tảng của máy tính hiện đại (và ngày nay được gọi là “kiến trúc von Neumann”). Vào thời điểm đó, những ý tưởng của Alonzo Church không nổi tiếng bằng công trình của các đồng nghiệp của ông (ngoại trừ lĩnh vực toán học “thuần túy”). Tuy nhiên, một thời gian sau, một John McCarthy nào đó (cũng tốt nghiệp Đại học Princeton, vào thời điểm xảy ra câu chuyện - nhân viên của Viện Công nghệ Massachusetts) bắt đầu quan tâm đến ý tưởng của Church. Dựa trên chúng, năm 1958 ông đã tạo ra ngôn ngữ lập trình hàm đầu tiên, Lisp. Và 58 năm sau, những ý tưởng về lập trình hàm đã lọt vào Java như con số 8. Chưa đầy 70 năm trôi qua... Trên thực tế, đây không phải là khoảng thời gian dài nhất để áp dụng một ý tưởng toán học vào thực tế.

Bản chất

Biểu thức lambda là một hàm như vậy. Bạn có thể coi đây là một phương thức thông thường trong Java, điểm khác biệt duy nhất là nó có thể được truyền cho các phương thức khác làm đối số. Có, không chỉ có thể truyền số, chuỗi và mèo cho các phương thức mà còn cả các phương thức khác! Khi nào chúng ta có thể cần nó? Ví dụ: nếu chúng tôi muốn chuyển một số lệnh gọi lại. Chúng ta cần phương thức mà chúng ta gọi để có thể gọi một số phương thức khác mà chúng ta chuyển cho nó. Nghĩa là, để chúng tôi có cơ hội truyền một cuộc gọi lại trong một số trường hợp và một cuộc gọi lại khác trong những trường hợp khác. Và phương thức của chúng tôi, phương thức sẽ chấp nhận lệnh gọi lại của chúng tôi, sẽ gọi chúng. Một ví dụ đơn giản là sắp xếp. Giả sử chúng ta viết một số cách sắp xếp phức tạp trông giống như thế này:
public void mySuperSort() {
    // ... do something here
    if(compare(obj1, obj2) > 0)
    // ... and here we do something
}
Ở đâu, ifchúng ta gọi phương thức compare(), truyền vào đó hai đối tượng mà chúng ta so sánh và chúng ta muốn tìm ra đối tượng nào trong số những đối tượng này “lớn hơn”. Chúng ta sẽ đặt cái "nhiều hơn" trước cái "nhỏ hơn". Tôi viết “more” trong dấu ngoặc kép vì chúng ta đang viết một phương thức phổ quát có thể sắp xếp không chỉ theo thứ tự tăng dần mà còn theo thứ tự giảm dần (trong trường hợp này, “more” sẽ là đối tượng về cơ bản nhỏ hơn và ngược lại) . Để đặt quy tắc cho chính xác cách chúng ta muốn sắp xếp, bằng cách nào đó chúng ta cần chuyển nó vào tệp mySuperSort(). Trong trường hợp này, bằng cách nào đó chúng ta có thể “kiểm soát” phương thức của mình trong khi nó được gọi. Tất nhiên, bạn có thể viết hai phương pháp riêng biệt mySuperSortAsc()để mySuperSortDesc()sắp xếp theo thứ tự tăng dần và giảm dần. Hoặc truyền một số tham số bên trong phương thức (ví dụ: booleanif true, sắp xếp theo thứ tự tăng dần và if falsetheo thứ tự giảm dần). Nhưng điều gì sẽ xảy ra nếu chúng ta không muốn sắp xếp một số cấu trúc đơn giản mà là một danh sách các mảng chuỗi chẳng hạn? Làm thế nào phương thức của chúng ta mySuperSort()biết cách sắp xếp các mảng chuỗi này? Kích thước? Bằng tổng độ dài của từ? Có lẽ theo thứ tự bảng chữ cái, tùy thuộc vào hàng đầu tiên trong mảng? Nhưng điều gì sẽ xảy ra nếu, trong một số trường hợp, chúng ta cần sắp xếp danh sách các mảng theo kích thước của mảng và trong trường hợp khác, theo tổng độ dài của các từ trong mảng? Tôi nghĩ bạn đã nghe nói về bộ so sánh và trong những trường hợp như vậy, chúng ta chỉ cần chuyển một đối tượng so sánh vào phương pháp sắp xếp của mình, trong đó chúng ta mô tả các quy tắc mà chúng ta muốn sắp xếp. Vì phương pháp tiêu chuẩn sort()được thực hiện theo nguyên tắc tương tự như nên mySuperSort()trong các ví dụ tôi sẽ sử dụng phương pháp tiêu chuẩn sort().
String[] array1 = {"Mother", "soap", "frame"};
String[] array2 = {"I", "Very", "I love", "java"};
String[] array3 = {"world", "work", "May"};

List<String[]> arrays = new ArrayList<>();
arrays.add(array1);
arrays.add(array2);
arrays.add(array3);

Comparator<String[]> sortByLength = new Comparator<String[]>() {
    @Override
    public int compare(String[] o1, String[] o2) {
        return o1.length - o2.length;
    }
};

Comparator<String[]> sortByWordsLength = new Comparator<String[]>() {
    @Override
    public int compare(String[] o1, String[] o2) {
        int length1 = 0;
        int length2 = 0;
        for (String s : o1) {
            length1 += s.length();
        }
        for (String s : o2) {
            length2 += s.length();
        }
        return length1 - length2;
    }
};

arrays.sort(sortByLength);
Kết quả:
  1. mẹ đã rửa khung ảnh
  2. hòa bình Lao động tháng năm
  3. Tôi thực sự yêu java
Ở đây các mảng được sắp xếp theo số từ trong mỗi mảng. Một mảng có ít từ hơn được coi là “nhỏ hơn”. Đó là lý do tại sao nó đến ngay từ đầu. Phần nào có nhiều từ hơn được coi là “nhiều hơn” và kết thúc ở cuối. Nếu sort()chúng ta chuyển một bộ so sánh khác vào phương thức (sortByWordsLength)thì kết quả sẽ khác:
  1. hòa bình Lao động tháng năm
  2. mẹ đã rửa khung ảnh
  3. Tôi thực sự yêu java
Bây giờ các mảng được sắp xếp theo tổng số chữ cái trong các từ của mảng đó. Trong trường hợp đầu tiên có 10 chữ cái, trong trường hợp thứ hai là 12 và trong trường hợp thứ ba là 15. Nếu chúng ta chỉ sử dụng một bộ so sánh, thì chúng ta không thể tạo một biến riêng cho nó mà chỉ cần tạo một đối tượng của một lớp ẩn danh ngay tại thời điểm gọi phương thức sort(). Như thế:
String[] array1 = {"Mother", "soap", "frame"};
String[] array2 = {"I", "Very", "I love", "java"};
String[] array3 = {"world", "work", "May"};

List<String[]> arrays = new ArrayList<>();
arrays.add(array1);
arrays.add(array2);
arrays.add(array3);

arrays.sort(new Comparator<String[]>() {
    @Override
    public int compare(String[] o1, String[] o2) {
        return o1.length - o2.length;
    }
});
Kết quả sẽ giống như trong trường hợp đầu tiên. Nhiệm vụ 1 . Viết lại ví dụ này để nó sắp xếp các mảng không theo thứ tự tăng dần của số từ trong mảng mà theo thứ tự giảm dần. Chúng tôi đã biết tất cả điều này. Chúng ta biết cách truyền đối tượng cho các phương thức, chúng ta có thể truyền đối tượng này hoặc đối tượng kia cho một phương thức tùy thuộc vào những gì chúng ta cần vào lúc này và bên trong phương thức mà chúng ta truyền một đối tượng như vậy, phương thức mà chúng ta đã viết phần triển khai sẽ được gọi . Câu hỏi đặt ra: biểu thức lambda có liên quan gì đến nó? Cho rằng lambda là một đối tượng chứa chính xác một phương thức. Nó giống như một đối tượng phương thức. Một phương thức được bao bọc trong một đối tượng. Chúng chỉ có một cú pháp hơi khác thường (nhưng sẽ nói thêm về điều đó sau). Chúng ta hãy xem xét lại mục này
arrays.sort(new Comparator<String[]>() {
    @Override
    public int compare(String[] o1, String[] o2) {
        return o1.length - o2.length;
    }
});
Ở đây, chúng tôi lấy danh sách của mình arraysvà gọi phương thức của nó sort(), trong đó chúng tôi chuyển một đối tượng so sánh bằng một phương thức duy nhất compare()(đối với chúng tôi, nó được gọi là gì không quan trọng, vì đó là phương thức duy nhất trong đối tượng này, chúng tôi sẽ không bỏ lỡ nó). Phương thức này có hai tham số mà chúng ta sẽ xử lý tiếp theo. Nếu bạn làm việc trong IntelliJ IDEA , bạn có thể đã thấy cách nó cung cấp cho bạn mã này để rút ngắn đáng kể:
arrays.sort((o1, o2) -> o1.length - o2.length);
Thế là sáu dòng biến thành một dòng ngắn. 6 dòng được viết lại thành một dòng ngắn. Một cái gì đó đã biến mất, nhưng tôi đảm bảo rằng không có gì quan trọng đã biến mất và mã này sẽ hoạt động giống hệt như với một lớp ẩn danh. Nhiệm vụ 2 . Tìm ra cách viết lại giải pháp cho vấn đề 1 bằng cách sử dụng lambdas (biện pháp cuối cùng là yêu cầu IntelliJ IDEA biến lớp ẩn danh của bạn thành lambda).

Hãy nói về giao diện

Về cơ bản, một giao diện chỉ là một danh sách các phương thức trừu tượng. Khi chúng ta tạo một lớp và nói rằng nó sẽ triển khai một loại giao diện nào đó, chúng ta phải viết trong lớp của mình cách triển khai các phương thức được liệt kê trong giao diện (hoặc, phương án cuối cùng là không viết nó mà làm cho lớp trở nên trừu tượng ). Có giao diện với nhiều phương thức khác nhau (ví dụ List), có giao diện chỉ có một phương thức (ví dụ cùng Comparator hoặc Runnable). Có những giao diện hoàn toàn không có một phương thức nào (cái gọi là giao diện đánh dấu, ví dụ như Serializable). Những giao diện chỉ có một phương thức còn được gọi là giao diện chức năng . Trong Java 8, chúng thậm chí còn được đánh dấu bằng chú thích @FunctionalInterface đặc biệt . Đó là các giao diện với một phương thức duy nhất phù hợp để sử dụng bởi các biểu thức lambda. Như tôi đã nói ở trên, biểu thức lambda là một phương thức được gói trong một đối tượng. Và khi chúng ta truyền một đối tượng như vậy đến đâu đó, trên thực tế, chúng ta truyền một phương thức duy nhất này. Hóa ra việc phương pháp này được gọi là gì không quan trọng đối với chúng tôi. Tất cả những gì quan trọng đối với chúng tôi là các tham số mà phương thức này lấy và trên thực tế, chính mã phương thức. Về cơ bản, một biểu thức lambda là. thực hiện một giao diện chức năng. Khi chúng ta thấy một giao diện với một phương thức, điều đó có nghĩa là chúng ta có thể viết lại một lớp ẩn danh như vậy bằng cách sử dụng lambda. Nếu giao diện có nhiều/ít hơn một phương thức thì biểu thức lambda sẽ không phù hợp với chúng ta và chúng ta sẽ sử dụng một lớp ẩn danh hoặc thậm chí là một lớp thông thường. Đã đến lúc đào sâu vào lambdas. :)

Cú pháp

Cú pháp chung là như thế này:
(параметры) -> {тело метода}
Tức là, các dấu ngoặc đơn, bên trong chúng là các tham số của phương thức, một “mũi tên” (đây là hai ký tự liên tiếp: trừ và lớn hơn), sau đó phần thân của phương thức luôn nằm trong dấu ngoặc nhọn. Các tham số tương ứng với các tham số được chỉ định trong giao diện khi mô tả phương thức. Nếu loại biến có thể được xác định rõ ràng bởi trình biên dịch (trong trường hợp của chúng tôi, chắc chắn rằng chúng tôi đang làm việc với mảng chuỗi, vì nó Listđược nhập chính xác bởi mảng chuỗi), thì loại biến String[]không cần thiết được viết.
Nếu bạn không chắc chắn, hãy chỉ định loại và IDEA sẽ đánh dấu nó bằng màu xám nếu không cần thiết.
Bạn có thể đọc thêm trong phần hướng dẫn của Oracle chẳng hạn. Điều này được gọi là "gõ mục tiêu" . Bạn có thể đặt bất kỳ tên nào cho các biến, không nhất thiết phải là tên được chỉ định trong giao diện. Nếu không có tham số thì chỉ cần dấu ngoặc đơn. Nếu chỉ có một tham số thì chỉ có tên biến không có dấu ngoặc đơn. Chúng ta đã sắp xếp các tham số, bây giờ là về phần nội dung của biểu thức lambda. Bên trong dấu ngoặc nhọn, viết mã như đối với phương pháp thông thường. Nếu toàn bộ mã của bạn chỉ bao gồm một dòng, bạn không cần phải viết dấu ngoặc nhọn (như với if và vòng lặp). Nếu lambda của bạn trả về một cái gì đó, nhưng nội dung của nó chỉ bao gồm một dòng, thì returnkhông cần thiết phải viết. Nhưng nếu bạn có dấu ngoặc nhọn thì, như trong phương pháp thông thường, bạn cần phải viết rõ ràng return.

Ví dụ

Ví dụ 1.
() -> {}
Tùy chọn đơn giản nhất. Và điều vô nghĩa nhất :).Bởi vì nó chẳng làm được gì cả. Ví dụ 2.
() -> ""
Cũng là một lựa chọn thú vị. Nó không chấp nhận gì và trả về một chuỗi trống ( returnđược bỏ qua vì không cần thiết). Tương tự nhưng với return:
() -> {
    return "";
}
Ví dụ 3. Xin chào thế giới bằng lambdas
() -> System.out.println("Hello world!")
Không nhận gì, không trả về gì (chúng ta không thể đặt returntrước lệnh gọi System.out.println(), vì kiểu trả về trong phương thức println() — void), chỉ hiển thị một dòng chữ trên màn hình. Lý tưởng để triển khai một giao diện Runnable. Ví dụ tương tự đầy đủ hơn:
public class Main {
    public static void main(String[] args) {
        new Thread(() -> System.out.println("Hello world!")).start();
    }
}
Hoặc như thế này:
public class Main {
    public static void main(String[] args) {
        Thread t = new Thread(() -> System.out.println("Hello world!"));
        t.start();
    }
}
Hoặc thậm chí chúng ta có thể lưu biểu thức lambda làm đối tượng thuộc loại Runnablevà sau đó chuyển nó cho hàm tạo thread’а:
public class Main {
    public static void main(String[] args) {
        Runnable runnable = () -> System.out.println("Hello world!");
        Thread t = new Thread(runnable);
        t.start();
    }
}
Chúng ta hãy xem xét kỹ hơn thời điểm lưu biểu thức lambda vào một biến. Giao diện Runnablecho chúng ta biết rằng các đối tượng của nó phải có một phương thức public void run(). Theo giao diện, phương thức chạy không chấp nhận bất cứ thứ gì làm tham số. Và nó không trả lại bất cứ điều gì (void). Do đó, khi viết theo cách này, một đối tượng sẽ được tạo bằng một phương thức nào đó không chấp nhận hoặc trả về bất cứ thứ gì. Điều này khá phù hợp với phương thức run()trong giao diện Runnable. Đó là lý do tại sao chúng tôi có thể đặt biểu thức lambda này vào một biến như Runnable. Ví dụ 4
() -> 42
Một lần nữa, nó không chấp nhận bất cứ thứ gì mà trả về số 42. Biểu thức lambda này có thể được đặt trong một biến loại Callable, bởi vì giao diện này chỉ định nghĩa một phương thức, trông giống như thế này:
V call(),
Vloại giá trị trả về ở đâu (trong trường hợp của chúng tôi int). Theo đó, chúng ta có thể lưu trữ biểu thức lambda như sau:
Callable<Integer> c = () -> 42;
Ví dụ 5. Lambda trong một số dòng
() -> {
    String[] helloWorld = {"Hello", "world!"};
    System.out.println(helloWorld[0]);
    System.out.println(helloWorld[1]);
}
Một lần nữa, đây là biểu thức lambda không có tham số và kiểu trả về của nó void(vì không có return). Ví dụ 6
x -> x
Ở đây chúng ta lấy một cái gì đó vào một biến хvà trả về nó. Xin lưu ý rằng nếu chỉ chấp nhận một tham số thì không cần phải viết dấu ngoặc đơn xung quanh nó. Tương tự, nhưng có dấu ngoặc:
(x) -> x
Và đây là tùy chọn rõ ràng return:
x -> {
    return x;
}
Hoặc như thế này, với dấu ngoặc và return:
(x) -> {
    return x;
}
Hoặc với dấu hiệu rõ ràng về loại (và theo đó, có dấu ngoặc đơn):
(int x) -> x
Ví dụ 7
x -> ++x
Chúng tôi chấp nhận nó хvà trả lại nó, nhưng để 1biết thêm. Bạn cũng có thể viết lại nó như thế này:
x -> x + 1
Trong cả hai trường hợp, chúng tôi không chỉ ra dấu ngoặc đơn xung quanh tham số, nội dung phương thức và từ return, vì điều này là không cần thiết. Các tùy chọn có dấu ngoặc và return được mô tả trong ví dụ 6. Ví dụ 8
(x, y) -> x % y
Chúng tôi chấp nhận một số ху, trả lại phần còn lại của phép chia xcho y. Dấu ngoặc đơn xung quanh các tham số đã được yêu cầu ở đây. Chúng chỉ là tùy chọn khi chỉ có một tham số. Giống như thế này với dấu hiệu rõ ràng về các loại:
(double x, int y) -> x % y
Ví dụ 9
(Cat cat, String name, int age) -> {
    cat.setName(name);
    cat.setAge(age);
}
Chúng tôi chấp nhận một đối tượng Cat, một chuỗi có tên và số nguyên. Trong chính phương thức này, chúng tôi đặt tên và tuổi đã truyền cho Cat. Vì biến catcủa chúng ta là kiểu tham chiếu nên đối tượng Cat bên ngoài biểu thức lambda sẽ thay đổi (nó sẽ nhận tên và tuổi được truyền vào bên trong). Một phiên bản phức tạp hơn một chút sử dụng lambda tương tự:
public class Main {
    public static void main(String[] args) {
        // create a cat and print to the screen to make sure it's "blank"
        Cat myCat = new Cat();
        System.out.println(myCat);

        // create lambda
        Settable<Cat> s = (obj, name, age) -> {
            obj.setName(name);
            obj.setAge(age);
        };

        // call the method, to which we pass the cat and the lambda
        changeEntity(myCat, s);
        // display on the screen and see that the state of the cat has changed (has a name and age)
        System.out.println(myCat);
    }

    private static <T extends WithNameAndAge>  void changeEntity(T entity, Settable<T> s) {
        s.set(entity, "Murzik", 3);
    }
}

interface WithNameAndAge {
    void setName(String name);
    void setAge(int age);
}

interface Settable<C extends WithNameAndAge> {
    void set(C entity, String name, int age);
}

class Cat implements WithNameAndAge {
    private String name;
    private int age;

    @Override
    public void setName(String name) {
        this.name = name;
    }

    @Override
    public void setAge(int age) {
        this.age = age;
    }

    @Override
    public String toString() {
        return "Cat{" +
                "name='" + name + '\'' +
                ", age=" + age +
                '}';
    }
}
Kết quả: Cat{name='null', age=0} Cat{name='Murzik', age=3} Như bạn thấy, lúc đầu đối tượng Cat có một trạng thái, nhưng sau khi sử dụng biểu thức lambda, trạng thái đã thay đổi . Biểu thức Lambda hoạt động tốt với generics. Và nếu chúng ta cần tạo một lớp Dogchẳng hạn, nó cũng sẽ triển khai WithNameAndAge, thì trong phương thức này, main()chúng ta có thể thực hiện các thao tác tương tự với Dog mà không cần thay đổi chính biểu thức lambda. Nhiệm vụ 3 . Viết giao diện chức năng với phương thức nhận vào một số và trả về giá trị Boolean. Viết cách triển khai giao diện như vậy dưới dạng biểu thức lambda trả về truenếu số đã truyền chia hết cho 13 mà không có số . Viết giao diện chức năng với một phương thức nhận vào hai chuỗi và trả về cùng một chuỗi. Viết cách triển khai giao diện như vậy dưới dạng lambda trả về chuỗi dài nhất. Nhiệm vụ 5 . Viết giao diện hàm với phương thức chấp nhận ba số phân số: a, b, cvà trả về cùng một số phân số. Viết cách triển khai giao diện như vậy dưới dạng biểu thức lambda trả về một giá trị phân biệt. Ai quên, D = b^2 - 4ac . Nhiệm vụ 6 . Sử dụng giao diện chức năng từ nhiệm vụ 5, viết biểu thức lambda trả về kết quả của phép toán a * b^c. Phổ biến về biểu thức lambda trong Java. Với các ví dụ và nhiệm vụ. Phần 2.
Bình luận
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION