Lập trình chức năng (functional programing), biểu thức lambda trong Java
- 22-10-2023
- Toanngo92
- 0 Comments
Một trong những tính năng mới được giới thiệu trong Java & là lập trình chức năng. Lập trình chức năng là một phương pháp lập trình. Nó nhấn mạnh việc sử dụng các hàm và việc viết mã không thay đổi trạng thái. Sử dụng lập trình chức năng, bạn có thể truyền các hàm như tham số cho các hàm khác và trả lại chúng như giá trị. Lập trình chức năng mang lại nhiều lợi ích cho các lập trình viên – nó làm cho việc kiểm thử chương trình dễ dàng hơn, nó bảo đảm tính an toàn trong đa luồng và làm cho chương trình trở nên mô đun hóa hơn.
Mục lục
Biểu thức Lambda
Một bổ sung quan trọng cho Java 8 là sự ra mắt của biểu thức lambda. Chúng giúp thúc đẩy lập trình chức năng và làm cho việc phát triển ứng dụng dễ dàng hơn.
Biểu thức lambda (còn được gọi tắt là lambdas) là một hàm chấp nhận tham số đầu vào và có thể trả về đầu ra. Nó là một thành phần của một giao diện chức năng (functional interface). Giao diện chức năng là một giao diện có một phương thức và được sử dụng như kiểu của một biểu thức lambda.
Biểu thức lambda tạo ra khả năng truyền các hàm trong mã, tương tự như việc truyền tham số và dữ liệu thông thường. Biểu thức lambda tương tự như một hàm ẩn danh (anonymous function) cho phép thực thi một phương thức chính xác tại nơi nó được yêu cầu. Biểu thức lambda loại bỏ nhu cầu khai báo một phương thức mới cũng như một phương thức riêng biệt cho một lớp chứa.
Cú pháp:
paramerters -> body
trong đó:
paramerters : các biến
-> : toán tử lambda
body : giá trị các tham số
Quy tắc cho Biểu thức Lambda
Một số quy tắc về biểu thức lambda như sau:
- Khai báo kiểu là tùy chọn: Khai báo kiểu tham số là tùy chọn.
- Dấu ngoặc đơn xung quanh tham số có thể bị bỏ qua: Sử dụng dấu ngoặc đơn là tùy chọn trong trường hợp một tham số.
- Dấu ngoặc nhọn có thể có hoặc không: Tương tự, dấu ngoặc nhọn là tùy chọn trong trường hợp một tham số.
- Từ khóa return có thể có hoặc không: Việc sử dụng từ khóa return là tùy chọn trong trường hợp một tham số.
- Thân câu lệnh có thể chứa một số câu lệnh khác nhau: Thân của biểu thức lambda có thể chứa không, một hoặc nhiều câu lệnh.
Giao diện Phương Thức Đơn (Single Method Interface) và Biểu thức Lambda
Lập trình chức năng có thể được sử dụng để tạo bộ lắng nghe sự kiện (event listener). Trong Java, bộ lắng nghe sự kiện thường được định nghĩa dưới dạng giao diện Java với một phương thức duy nhất. Ví dụ:
Định nghĩa class State
/*
* Click nbfs://nbhost/SystemFileSystem/Templates/Licenses/license-default.txt to change this license
* Click nbfs://nbhost/SystemFileSystem/Templates/Classes/Class.java to edit this template
*/
package com;
/**
*
* @author toan1
*/
public class State {
public String var1;
public Integer var2;
public State() {
this.var1 = "val1";
}
public State(String var1) {
this.var1 = var1;
this.var2 = 0;
}
public String getVar1() {
return var1;
}
public void setVar1(String var1) {
this.var1 = var1;
}
public Integer getVar2() {
return var2;
}
public void setVar2(Integer var2) {
this.var2 = var2;
}
}
Định nghĩa interface StateChangeListener
public interface StateChangeListener {
void onStateChange(State previousState, State presentState);
}
File App.java:
/*
* Click nbfs://nbhost/SystemFileSystem/Templates/Licenses/license-default.txt to change this license
* Click nbfs://nbhost/SystemFileSystem/Templates/Project/Maven2/JavaApp/src/main/java/${packagePath}/${mainClassName}.java to edit this template
*/
package com;
import java.util.ArrayList;
import java.util.List;
/**
*
* @author toan1
*/
// Define the StateTest class
class App {
private List<StateChangeListener> listeners = new ArrayList<>();
private State currentState;
public void addStateListener(StateChangeListener listener) {
listeners.add(listener);
}
public void changeState(State newState) {
State previousState = currentState;
currentState = newState;
for (StateChangeListener listener : listeners) {
listener.onStateChange(previousState, currentState);
}
}
public static void main(String[] args) {
App objStateTest = new App();
// Create some sample states
State state1 = new State("State 1");
state1.setVar1("after");
State state2 = new State("State 2");
// Add a state change listener
objStateTest.addStateListener(new StateChangeListener() {
public void onStateChange(State previousState, State presentState) {
System.out.println("Su kien thay doi trang thai xay ra");
try{
System.out.println("Previous State: " + previousState.getVar1());
System.out.println("Present State: " + presentState.getVar1());
}catch(Exception ex){
System.out.println(ex.getMessage());
}
}
});
// Change the state to trigger the listener
objStateTest.changeState(state1);
objStateTest.changeState(state2);
}
}
Ví dụ khi sử dụng biểu thức lambda:
/*
* Click nbfs://nbhost/SystemFileSystem/Templates/Licenses/license-default.txt to change this license
* Click nbfs://nbhost/SystemFileSystem/Templates/Project/Maven2/JavaApp/src/main/java/${packagePath}/${mainClassName}.java to edit this template
*/
package com;
import java.util.ArrayList;
import java.util.List;
/**
*
* @author toan1
*/
// Define the StateTest class
class App {
private List<StateChangeListener> listeners = new ArrayList<>();
private State currentState;
public void addStateListener(StateChangeListener listener) {
listeners.add(listener);
}
public void changeState(State newState) {
State previousState = currentState;
currentState = newState;
for (StateChangeListener listener : listeners) {
listener.onStateChange(previousState, currentState);
}
}
public static void main(String[] args) {
App objStateTest = new App();
// Create some sample states
State state1 = new State("State 1");
state1.setVar1("after");
State state2 = new State("State 2");
objStateTest.addStateListener((previousState, presentState) -> {
System.out.println("State change event occurred");
try {
System.out.println("Previous State: " + previousState.getVar1());
System.out.println("Present State: " + presentState.getVar1());
} catch (Exception ex) {
System.out.println(ex.getMessage());
}
});
// Change the state to trigger the listener
objStateTest.changeState(state1);
objStateTest.changeState(state2);
}
}
Đây là một cách đơn giản và gọn gàng hơn để thêm một bộ lắng nghe sự kiện. Biểu thức lambda trong phương thức addStateListener() như sau: (previousState, presentState) -> System.out.println(“Sự kiện thay đổi trạng thái đã xảy ra”).
Ở đây, previousState và presentState là các tham số của sự kiện. Biểu thức lambda và kiểu của tham số của phương thức addStateListener() phải khớp nhau. Nếu kiểu và biểu thức khớp nhau, biểu thức lambda trở thành một hàm, tạo ra cùng một giao diện với một tham số. Giao diện này chứa một phương thức duy nhất. Do đó, biểu thức lambda được khớp thành công.
Tham số biểu thức Lambda
Biểu thức lambda trong Java tương tự với các phương thức ẩn danh, chúng cũng có thể xử lý tham số tương tự như phương thức. ví dụ phía trên đã giải thích (previousState, presentState) là một phần của biểu thức lambda. Những tham số này phải khớp với tham số của giao diện có một phương thức duy nhất.
Không có tham số
Ví dụ:
() -> System.out.println("hello");
Có một tham số
Ví dụ:
(param) -> System.out.println("hello "+param);
// hoac khong co dau ngoac tron
param -> System.out.println("hello "+param);
Có nhiều tham số
Nếu phương thức trả về nhiều tham số , thì tham số phải được thêm vào trong dấu ngoặc tròn.
Ví dụ:
(param1, param2) -> System.out.println("hello "+param1 + " "+param2);
Ở đây, dấu ngoặc tròn chứa hai giá trị, cho biết biểu thức lambda sẽ chấp nhận hai tham số.
Kiểu tham số
Việc xác định kiểu tham số cho biểu thức lambda là cần thiết trong một số trường hợp, khi trình biên dịch không rõ ràng về sự khớp giữa loại tham số của phương thức giao diện chức năng và biểu thức lambda.
Ví dụ:
(Product prd) -> System.out.println("Chiếc điện thoại thông minh là: " + prd.getName()
Trong ví dụ trên, kiểu (Product) của tham số prd được xác định. Điều này tương tự như việc khai báo một tham số trong một phương thức hoặc tạo một triển khai ẩn danh của một giao diện.
Thân hàm lambda phải được bao quanh bởi dấu ngoặc nhọn {} giống như mã Java thông thường trong trường hợp của biểu thức lambda có nhiều dòng.
Ví dụ:
(previousState, presentState) -> {
// Sử dụng ngoặc nhọn cho nhiều dòng
System.out.println("Kết quả của trạng thái trước: " + previousState);
System.out.println("Kết quả của trạng thái hiện tại: " + presentState);
}
Trả về giá trị (return value)
Quy trình trả giá trị từ biểu thức lambda Java tương tự như trong một phương thức Java thông thường.
Ví dụ:
(param) -> {
System.out.println("hello "+param);
return "value";
}
Một câu lệnh return cho các tính toán cụ thể có thể được thêm vào dưới dạng rút gọn. Xem xét ví dụ, Đoạn mã dưới hiển thị cùng một tình huống trả về dưới dạng thông thường và dưới dạng rút gọn.
(param1, param2) -> { return param1 > param2; }
(param1, param2) -> param1 > param2;
Sử dụng Lambdas như đối tượng
Biểu thức lambda trong Java cũng là một loại đối tượng. Nó có thể được gán như một đối tượng thông thường vào một biến và có thể được truyền đi. Ví dụ:
/*
* Click nbfs://nbhost/SystemFileSystem/Templates/Licenses/license-default.txt to change this license
* Click nbfs://nbhost/SystemFileSystem/Templates/Classes/Interface.java to edit this template
*/
package lambdasobj;
/**
*
* @author toan1
*/
public interface SampleComparator {
public boolean compare(int iA, int iB);
}
Đoạn mã dưới biểu diễn triển khai dưới dạng biểu thức lambda:
/*
* Click nbfs://nbhost/SystemFileSystem/Templates/Licenses/license-default.txt to change this license
* Click nbfs://nbhost/SystemFileSystem/Templates/Classes/Class.java to edit this template
*/
package lambdasobj;
/**
*
* @author toan1
*/
public class Main {
public static void main(String[] args) {
SampleComparator sampleComp = (iA, iB) -> iA > iB;
boolean res = sampleComp.compare(10, 5);
System.out.println("Ket qua: "+res);
}
}
Đoạn mã trên minh họa cách đối tượng lambda được gán cho một biến và truyền như một đối tượng.
Hoặc đoạn mã dưới mô tả một biểu thức lambda được sử dụng để sắp xếp chuỗi theo độ dài của chúng.
Arrays.sort(sampleStrArr, (strA, strB) -> strB.length() - strA.length());
Lợi ích khi sử dụng biểu thức Lambda
- Mã nguồn dễ đọc hơn: Biểu thức lambda có thể làm cho mã nguồn của bạn ngắn gọn và dễ đọc hơn. Chúng đặc biệt hữu ích khi làm việc với các giao diện chức năng, nơi bạn có thể thay thế các lớp ẩn danh bằng các biểu thức lambda ngắn gọn và diễn đạt.
- Lập trình nhanh chóng đặc biệt trong Collections: Biểu thức lambda đặc biệt hữu ích khi làm việc với các tập hợp trong Java. Chúng cho phép bạn thực hiện các thao tác trên các phần tử của tập hợp với ít mã nguồn hơn, làm cho mã của bạn hiệu quả hơn và dễ viết hơn.
- Xử lý song song dễ dàng hơn: Biểu thức lambda rất phù hợp cho việc xử lý song song và có thể được sử dụng với Java Streams API để viết mã đồng thời và song song dễ dàng hơn. Điều này có thể dẫn đến hiệu suất cải thiện trên các hệ thống đa nhân.
Đoạn mã dưới thể hiện một chương trình hoàn chỉnh sử dụng biểu thức lambda:
public class SampleLambda {
public static void main(String[] args) {
SampleLambda perform = new SampleLambda();
// To receive results with type declaration
MathOperation add = (int ab, int xy) -> ab + xy;
// To receive results without type declaration
MathOperation subtr = (ab, xy) -> ab - xy;
// To receive results with return statement along with curly braces
MathOperation multi = (int ab, int xy) -> {
return ab * 2 * xy;
};
// To receive results without return statement and curly braces
MathOperation div = (int ab, int xy) -> ab / xy;
System.out.println("Addition operation with type declaration: 20 + 5 = " + perform.operate(20, 5, add));
System.out.println("Subtraction operation without type declaration: 20 - 5 = " + perform.operate(20, 5, subtr));
System.out.println("Multiplication with return statement: 20 * 5 = " + perform.operate(20, 5, multi));
System.out.println("Division operation without return statement: 20 / 5 = " + perform.operate(20, 5, div));
}
interface MathOperation {
int operation(int ab, int xy);
}
private int operate(int ab, int xy, MathOperation mathOperation) {
return mathOperation.operation(ab, xy);
}
}
Ví dụ 2:
/*
* Click nbfs://nbhost/SystemFileSystem/Templates/Licenses/license-default.txt to change this license
* Click nbfs://nbhost/SystemFileSystem/Templates/Classes/Class.java to edit this template
*/
package vdlambda;
/**
*
* @author toan1
*/
public class SampleLambda2 {
public static void main(String[] args) {
SampleLambda2 testLambda = new SampleLambda2();
// Passing without parentheses
GreetingService greetService = message -> System.out.println("Hi " + message);
// Passing with parentheses
GreetingService greetService2 = (message) -> System.err.println("Hi " + message);
greetService.sayMessage("James");
greetService2.sayMessage("Mary");
}
interface GreetingService {
void sayMessage(String message);
}
}
Phạm vi biểu thức lambda
Ví dụ sử dụng biểu thức lambda vớ Runnable interface:
import static java.lang.System.out;
public class MyWishes {
Runnable da = () -> out.println(this);
Runnable db = () -> out.println(this.toString());
@override
public String toString() {
return "Happy New Year!";
}
public static void main(String[] args) {
new MyWishes().da.run(); // Happy New Year
new MyWishes().db.run(); // Happy New Year
}
}
Trong Đoạn mã trên, cả hai biểu thức lambda da và db đều gọi phương thức toString() của lớp MyWishes. Điều này cho thấy phạm vi mà biểu thức lambda có sẵn.
Biến final hoặc biến “effectively final” cũng có thể được tham chiếu trong một biểu thức lambda. Một biến final là một biến chỉ được gán một lần trong toàn bộ mã nguồn được gọi là biến “effectively final”.
Tham chiếu phương thức
Tham chiếu phương thức (method references) cho phép chúng ta tham chiếu đến các constructor hoặc phương thức mà không cần thực hiện chúng. Đây là một tính năng mới trong Java 8. Việc sử dụng tham chiếu phương thức tương tự như việc sử dụng biểu thức lambda, cả hai đều yêu cầu một kiểu mục tiêu chứa một giao diện chức năng tương thích.
Tham chiếu phương thức đại diện cho các biểu thức lambda dễ đọc hơn cho các phương thức đã có tên. Dưới đây là sáu loại tham chiếu phương thức:
- TypeName::static : Loại này tham chiếu đến một phương thức tĩnh của một lớp hoặc enum.
- TypeName.super::instance : Loại này tham chiếu đến một phương thức thể hiện từ kiểu cha của một đối tượng.
- ObjectReference::instance : Loại này tham chiếu đến một phương thức thể hiện trên một đối tượng cụ thể.
- ClassName::instance : Loại này tham chiếu đến một phương thức thể hiện của một lớp.
- ClassName::new : Loại này tham chiếu đến constructor từ một lớp.
- ArrayTypeName::new : Loại này tham chiếu đến constructor của một kiểu mảng cụ thể.
Ví dụ, xét một trường hợp mà yêu cầu việc lọc thường xuyên của một danh sách các tệp dựa trên loại tệp. Đoạn mã dưới hiển thị một tập hợp các phương thức để xác định loại tệp:
/*
* Click nbfs://nbhost/SystemFileSystem/Templates/Licenses/license-default.txt to change this license
* Click nbfs://nbhost/SystemFileSystem/Templates/Classes/Class.java to edit this template
*/
package methodref;
import java.io.File;
/**
*
* @author toan1
*/
public class FileFilters {
// Filter for JPEG files
public static boolean filterJpeg(File file) {
// Sample code for filtering JPEG files
return file.getName().toLowerCase().endsWith(".jpeg") || file.getName().toLowerCase().endsWith(".jpg");
}
// Filter for TIFF files
public static boolean filterTiff(File file) {
// Sample code for filtering TIFF files
return file.getName().toLowerCase().endsWith(".tiff") || file.getName().toLowerCase().endsWith(".tif");
}
// Filter for PNG files
public static boolean filterPng(File file) {
// Sample code for filtering PNG files
return file.getName().toLowerCase().endsWith(".png");
}
}
Tham chiếu phương thức có thể hữu ích trong trường hợp lọc tệp, như được thể hiện trong dưới. Ở đây, một phương thức được xác định trước là getFiles() trả về một Stream.
/*
* Click nbfs://nbhost/SystemFileSystem/Templates/Licenses/license-default.txt to change this license
* Click nbfs://nbhost/SystemFileSystem/Templates/Classes/Class.java to edit this template
*/
package methodref;
import java.io.File;
/**
*
* @author toan1
*/
public class FileFilters {
// Filter for JPEG files
public static boolean filterJpeg(File file) {
// Sample code for filtering JPEG files
return file.getName().toLowerCase().endsWith(".jpeg") || file.getName().toLowerCase().endsWith(".jpg");
}
// Filter for TIFF files
public static boolean filterTiff(File file) {
// Sample code for filtering TIFF files
return file.getName().toLowerCase().endsWith(".tiff") || file.getName().toLowerCase().endsWith(".tif");
}
// Filter for PNG files
public static boolean filterPng(File file) {
// Sample code for filtering PNG files
return file.getName().toLowerCase().endsWith(".png");
}
}
Trong ví dụ này, phần tham chiếu phương thức FileFilters::filterJpeg, FileFilters::filterTiff, và FileFilters::filterPng được sử dụng để lọc danh sách các tệp dựa trên loại tệp.
Vai trò của phần tham chiếu phương thức là trỏ đến các phương thức bằng tên phương thức. Hai dấu hai chấm (::) được sử dụng để mô tả phần tham chiếu phương thức. Có các loại phương thức có thể được tham chiếu như sau:
- Phương thức tĩnh (Static methods).
- Phương thức thể hiện (Instance methods).
- Phương thức trên các thể hiện cụ thể (Methods on particular instances).
- Constructors (Các hàm khởi tạo).
Tham chiếu phương thức tĩnh (static)
Tham chiếu phương thức tĩnh (Static method reference) cho phép sử dụng một phương thức tĩnh như một biểu thức lambda. Phương thức tĩnh có thể được định nghĩa trong một enum, một lớp hoặc một giao diện. Đoạn mã dưới thể hiện ví dụ tham chiếu phương thức tĩnh.
import java.util.function.Function;
public class MainTest {
public static void main(String[] args) {
// To retrieve the result with a lambda expression
Function<Integer, String> funcA = (i) -> Integer.toBinaryString(i);
System.out.println(funcA.apply(11));
// To retrieve the result with a method reference
Function<Integer, String> funcB = Integer::toBinaryString;
System.out.println(funcB.apply(11));
}
}
Output:
1011
1011
Biểu thức lambda đầu tiên funcA được tạo ra bằng cách xác định một giá trị đầu vào x và cung cấp một thân biểu thức lambda. Đây là cách thông thường để tạo một biểu thức lambda.
Biểu thức lambda thứ hai funcB được tạo ra bằng cách tham chiếu đến một phương thức tĩnh từ lớp Integer.
Tham chiếu phương thức thể hiện (instance)
Ví dụ:
import java.util.function.Supplier;
public class MainTest2 {
public static void main(String[] args) {
Supplier<Integer> sampleSupA = () -> "Toanngo92".length();
System.out.println(sampleSupA.get()); // Display the result
Supplier<Integer> sampleSupB = "Toanngo92"::length;
System.out.println(sampleSupB.get()); // Display the result
}
}
Output:
8
8
Giao diện chức năng (Functional Interface)
Trong Java 8, một số giao diện chức năng mới đã được thêm vào gói java.util.function. Các giao diện này được thiết kế để hỗ trợ biểu thức lambda và lập trình hàm. Dưới đây là một số giao diện chức năng và các ví dụ về cách sử dụng chúng:
- Predicate: Trả về một giá trị Boolean dựa trên đầu vào kiểu T. Ví dụ:
Predicate<Integer> isPositive = n -> n > 0;
- Supplier: Trả về một đối tượng kiểu T. Ví dụ:
Supplier<String> greetingSupplier = () -> "Hello, World!";
- Consumer: Thực hiện một hành động trên đối tượng kiểu T. Ví dụ:
Consumer<String> printUpperCase = str -> System.out.println(str.toUpperCase());
- Function<T,R>: Nhận một đối tượng kiểu T và trả về một đối tượng kiểu R. Ví dụ:
Function<String, Integer> strLength = name -> name.length();
- BiFunction: Tương tự như Function, nhưng có hai đối số kiểu T và U. Ví dụ:
BiFunction<String, Integer, String> repeat = (str, count) -> str.repeat(count);
Giao diện chức năng trong Java 8 cũng bao gồm các phiên bản tương ứng cho kiểu dữ liệu nguyên thủy để tối ưu hóa hiệu suất khi làm việc với chúng. Dưới đây là các giao diện chức năng cho các kiểu dữ liệu nguyên thủy:
- IntSupplier: Trả về một giá trị kiểu nguyên nguyên (int).
- IntFunction<R>: Nhận một giá trị kiểu nguyên (int) và trả về một giá trị kiểu tổng quát R.
- IntPredicate: Kiểm tra một giá trị kiểu nguyên (int) và trả về một giá trị Boolean.
- IntConsumer: Thực hiện một hành động trên một giá trị kiểu nguyên (int).
Các phiên bản này cho phép bạn làm việc trực tiếp với kiểu dữ liệu nguyên thủy mà không cần chuyển đổi sang kiểu tổng quát, điều này có thể giúp tối ưu hóa hiệu suất trong trường hợp bạn đang xử lý dữ liệu nguyên thủy.
Giao diện chức năng (functional interface) có khả năng xử lý gần như bất kỳ loại tham số nào và trả về một loại khác. Chúng cho phép bạn thực hiện các biến đổi hoặc tính toán trên các giá trị tham số và trả về kết quả trong một loại dữ liệu khác. Ví dụ:
- Một Function<String, Integer> có thể chuyển đổi một chuỗi thành một số nguyên.
- Một Function<String, String> có thể thực hiện một biến đổi trên một chuỗi và trả về một chuỗi khác.
Các giao diện chức năng cung cấp linh hoạt trong việc xử lý và biến đổi dữ liệu, cho phép bạn sử dụng chúng để thực hiện nhiều nhiệm vụ khác nhau trong ứng dụng Java.
Ví dụ sử dụng functional interface:
import java.util.function.Function;
public class MainTest {
public static void main(String[] args) {
Function<String, Integer> sampleLengthA = (name) -> name.length();
Function<String, String> sampleTransformation = (name) -> "Transformed: " + name;
Function<String, Integer> sampleLengthB = String::length;
// Usage examples:
String input = "Hello, World!";
int lengthA = sampleLengthA.apply(input);
System.out.println("Length using lambda expression: " + lengthA);
String transformed = sampleTransformation.apply(input);
System.out.println("Transformation using lambda expression: " + transformed);
int lengthB = sampleLengthB.apply(input);
System.out.println("Length using method reference: " + lengthB);
}
}
Ví dụ 2:
import java.util.Arrays;
import java.util.List;
import java.util.function.Predicate;
public class SampleFuncInter {
public static void main(String[] args) {
List<Integer> list = Arrays.asList(5, 10, 15, 20, 25, 30, 35, 40, 45);
// In this example, a functional interface is used to filter numbers less than 45.
System.out.println("Display numbers less than 45:");
eval(list, n -> n < 45);
}
public static void eval(List<Integer> list, Predicate<Integer> predicate) {
for (Integer n : list) {
if (predicate.test(n)) {
System.out.println(n);
}
}
}
}
Phương thức mặc định (Default Method)
Default methods là một tính năng mới trong Java 8 cho phép triển khai mặc định cho các phương thức trong một giao diện. Nó cho phép các giao diện cung cấp triển khai mặc định cho các phương thức. Kết quả là các lớp hiện có triển khai một giao diện sẽ tự động kế thừa các triển khai mặc định. Ví dụ, giao diện List hoặc Collection chứa mặc định khai báo phương thức foreach (). Do đó, việc triển khai các phương thức như vậy sẽ phá vỡ triển khai của framework của bộ sưu tập. Sử dụng tính năng ‘default method’ mới, giao diện List/Collection có thể có một triển khai mặc định của phương thức foreach () và các lớp triển khai giao diện này không cần phải triển khai các phương thức đó.
Default methods còn được gọi là phương thức mở rộng ảo hoặc phương thức Defender.
Dưới đây là các đặc điểm của default methods:
- Default methods giảm sự khác biệt giữa giao diện và lớp trừu tượng.
- Những phương thức này loại bỏ nhu cầu của các lớp tiện ích.
- Một trong những lý do hàng đầu để giới thiệu default methods trong giao diện là tăng cường API bộ sưu tập và làm cho chúng thân thiện với biểu thức lambda.
- Default methods có thể hỗ trợ trong việc loại bỏ lớp triển khai cơ sở. Bạn có thể cung cấp triển khai mặc định và cho phép các lớp triển khai tự động quyết định phải ghi đè phương thức nào.
- Default methods trở nên vô ích nếu bất kỳ lớp nào trong hệ thống chứa một phương thức có chữ ký giống nhau.
- Một default method không thể ghi đè một phương thức từ java.lang.Object. Lý do là Object là lớp cơ sở cho tất cả các lớp Java khác.
- Những phương thức này mở rộng các giao diện mà không làm vỡ các lớp triển khai.
Ví dụ:
Tạo interface Book:
interface Book {
default void print() {
System.out.println("This is a book");
}
static void turnPages() {
System.out.println("Turning pages");
}
}
Tạo interface Journal:
interface Journal {
default void print() {
System.out.println("This is a journal");
}
}
Tạo class Novel:
class Novel implements Book, Journal {
public void print() {
Book.super.print();
Journal.super.print();
Book.turnPages();
System.out.println("This is a novel");
}
}
Tạo class Test:
class Test {
public static void main(String[] args) {
Book novel = new Novel();
novel.print();
Book.turnPages();
}
}
Ghi đè các phương thức mặc định là một tùy chọn hiệu quả có thể áp dụng bất cứ khi nào cần. Ghi đè phương thức mặc định cho phép bạn cung cấp một triển khai tùy chỉnh cho một phương thức trong giao diện.
Dưới đây là một ví dụ về việc ghi đè các phương thức mặc định trong giao diện Iterable:
import java.util.Iterator;
public class CustomIterable<T> implements Iterable<T> {
private T[] elements;
public CustomIterable(T[] elements) {
this.elements = elements;
}
@Override
public Iterator<T> iterator() {
return new Iterator<T>() {
private int index = 0;
@Override
public boolean hasNext() {
return index < elements.length;
}
@Override
public T next() {
return elements[index++];
}
@Override
public void remove() {
throw new UnsupportedOperationException("Phương thức xóa không được hỗ trợ.");
}
};
}
public static void main(String[] args) {
Integer[] numbers = {1, 2, 3, 4, 5};
CustomIterable<Integer> customIterable = new CustomIterable<>(numbers);
for (Integer number : customIterable) {
System.out.print(number + " ");
}
}
}
Phương thức mặc định (Default Method) và phương thức thông thường (Regular Method)
Phương thức mặc định và phương thức thông thường
Phương thức mặc định chứa từ khóa “default” – đó là sự khác biệt chính giữa một phương thức thông thường và phương thức mặc định. Ngoài ra, các phương thức trong các lớp có thể truy cập và sửa đổi tham số của phương thức và cũng các trường (fields) của lớp của họ. Tuy nhiên, một phương thức mặc định chỉ có thể truy cập các tham số của nó, giao diện không có trạng thái nào.
Cuối cùng, phương thức mặc định cho phép thêm chức năng mới vào các giao diện hiện có mà không ảnh hưởng đến việc triển khai hiện tại của các giao diện này. Một giao diện có thể có một hoặc nhiều phương thức mặc định và vẫn hoạt động.
Đoạn mã dưới thể hiện một ví dụ khác về phương thức mặc định.
Tạo interface Gadget
interface Gadget {
default void print() {
System.out.println("This is a Gadget!");
}
static void call() {
System.out.println("with Calling feature!");
}
}
Tạo interface TextMesage
interface TextMessage {
default void print() {
System.out.println("With Text Messaging feature!");
}
}
Tạo class SmartGadget
class SmartGadget implements Gadget, TextMessage {
public void print() {
Gadget.super.print();
TextMessage.super.print();
Gadget.call();
System.out.println("It is a Smartphone!");
}
}
Tạo class JavaTester
public class JavaTester {
public static void main(String[] args) {
Gadget gadget = new SmartGadget();
gadget.print();
}
}
Kết quả:
This is a Gadget!
With calling feature!
With Text Messaging feature!
It is a Smartphone!
Trong ví dụ này, chúng ta có một giao diện Gadget và một giao diện TextMessage, cả hai đều có phương thức mặc định print(). Lớp SmartGadget triển khai cả hai giao diện và ghi đè phương thức print() để gọi cả hai phương thức mặc định và phương thức call() của giao diện Gadget. Điều này cho phép chúng ta sử dụng các phương thức mặc định từ cả hai giao diện trong một lớp triển khai.
Multiple Defaults
Một lớp Java có thể triển khai một hoặc nhiều giao diện và mỗi giao diện có thể định ra phương thức mặc định thông qua cùng một chữ ký phương thức. Cuối cùng, các phương thức được kế thừa xung đột với nhau và gây ra lỗi. Do đó, Java sẽ ném một lỗi biên dịch nếu nó không chắc chắn rằng lớp triển khai hai hoặc nhiều giao diện định ra cùng một phương thức mặc định. Trong trường hợp đó, bạn nên ghi đè phương thức và chọn một trong các phương thức đó.
Ví dụ
public interface Green {
default void defaultMethod() {
System.out.println("Green default method");
}
}
public interface Red {
default void defaultMethod() {
System.out.println("Red default method");
}
}
public class Impl implements Green, Red {
// todo
}
Biên dịch ví dụ sẽ dẫn đến một lỗi:
Java: class Imp inherits unrelated defaults for defaultMethod() from types Green and Red
Để sửa lỗi này, chúng ta cần cung cấp triển khai phương thức mặc định rõ ràng như được hiển thị trong mã:
public interface Green {
default void defaultMethod() {
System.out.println("Green default method");
}
}
public interface Red {
default void defaultMethod() {
System.out.println("Red default method");
}
}
public class Impl implements Green, Red {
public void defaultMethod() {
Green.super.defaultMethod(); // Calling the default method from Green
Red.super.defaultMethod(); // Calling the default method from Red
}
public static void main(String[] args) {
Impl impl = new Impl();
impl.defaultMethod();
}
}
Phương thức tĩnh trong giao diện
Ngoài phương thức mặc định, trong các giao diện, chúng ta có thể định ra các phương thức tĩnh. Điều này giúp tổ chức và truy cập các phương thức trợ giúp trong thư viện dễ dàng hơn; các phương thức này loại bỏ sự cần thiết của một lớp riêng biệt, do đó phương thức tĩnh cụ thể cho một giao diện có thể được đặt trong cùng một giao diện.
Ví dụ, giao diện Stream mới chứa nhiều phương thức tĩnh. Do đó, việc truy cập các phương thức “helper” dễ dàng hơn, vì chúng được định ra trực tiếp trên giao diện, thay vì một lớp khác như StreamUtil hoặc Streams.
Tương tự như phương thức tĩnh trong các lớp, một định nghĩa phương thức trong một giao diện được đánh dấu là một phương thức tĩnh bằng cách sử dụng từ khóa static ở đầu của chữ ký phương thức. Theo mặc định, tất cả các khai báo phương thức trong một giao diện, bao gồm cả phương thức tĩnh, đều tự động là công cộng, do đó, từ khóa public không cần phải được cung cấp. Mã nguồn dưới thể hiện một ví dụ đơn giản về một phương thức tĩnh trong một giao diện.
public interface Productinfo{
static ProductId getProductid (String Productstring) {
// todo
}
}
Cú pháp biến cục bộ cho tham số Lambda
Trong Java 11, cú pháp Biến Cục bộ cho Tham số Lambda đã được cải thiện. Theo cải thiện này, từ khóa var có thể được sử dụng tương tự như biến cục bộ khi khai báo tham số chính thức của các biểu thức lambda được gán kiểu ngầm định.
Trong các phiên bản trước của Java, như Java 10, một biểu thức lambda có thể được viết như sau:
(n1, n2) -> n1.compute(n2)
Ở đây, biểu thức này có kiểu ngầm định, điều này có nghĩa là kiểu của tất cả các tham số chính thức của nó được suy ra tự động.
Trong Java 10, bạn cũng có thể sử dụng kiểu ngầm định cho biến cục bộ. Xem xét một số ví dụ:
1. var n1 = new DisplayText();
2. try (var n1 = // logic ) { // todo } catch { // todo }
Trong cả hai trường hợp này, var được sử dụng để gán kiểu ngầm định cho các biến cục bộ.
Vì vậy, trong Java 11, để đảm bảo tính nhất quán với biến cục bộ, quyết định cho phép sử dụng var cho các tham số chính thức của biểu thức lambda được gán kiểu ngầm định.
(var n1, var n2) -> n1.compute(n2) // biểu thức lambda có kiểu ngầm định
Lợi ích của điều này là nó giúp cho các nhà phát triển dễ dàng đánh dấu các tham số ngầm định của một lambda hoặc thêm các trình điều khiển truy cập cho các biến cục bộ lambda có kiểu ngầm định. Lợi ích khác là những người phát triển sẽ trải qua ít lỗi hơn do việc sử dụng var ở những nơi không đúng.
Việc sử dụng chú thích trong các câu lệnh lambda có thể giúp các nhà phát triển sử dụng phản chiếu và thu thập thông tin về đối tượng được chú thích tại thời gian chạy cũng như mô tả các hành vi như mã nguồn được tạo ra hoặc các gợi ý khác cho các công cụ tại thời gian biên dịch.
Lưu ý: Bạn không thể sử dụng var cho một số tham số và bỏ qua cho các tham số khác. Nó phải được sử dụng đồng nhất.
Xét một ví dụ:
Trước Java 11, bạn có thể đã viết:
IDisplay dis = (@ADisplay SomeVeryLongClassName n1, final SomeVeryLongClassName n2, final SomeVeryLongClassName n3) -> ...
Từ Java 11 trở đi, bạn có thể viết:
IDisplay dis = (@ADisplay var n1, final var n2, final var n3) -> ...
Ví dụ:
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
class Book {
int id;
String title;
float price;
public Book(int id, String title, float price) {
this.id = id;
this.title = title;
this.price = price;
}
}
public class VarDemo {
public static void main(String[] args) {
List<Book> list = new ArrayList<Book>();
// Adding Books
list.add(new Book(1, "Harry Potter and the Chamber of Secrets", 250.0f));
list.add(new Book(3, "Keyboard Ninjas", 300.0f));
list.add(new Book(2, "The Three Investigators Club", 150.0f));
System.out.println("Sorting on the basis of title...");
// Implementing a lambda expression for sorting
Collections.sort(list, (p1, p2) -> p1.title.compareTo(p2.title));
for (Book book : list) {
System.out.println(book.id + " " + book.title + " " + book.price);
}
}
}
Giới thiệu về Giao diện Hàm (Functional Interfaces)
Giao diện hàm (Functional Interfaces) là một trong những thành phần quan trọng để thực hiện lập trình hàm trong Java. Trong Java 8, gói java.util.function mới được giới thiệu, và nó định nghĩa các giao diện hàm có thể cung cấp loại mục tiêu cho các yếu tố như biểu thức lambda và tham chiếu phương thức. Một giao diện hàm chỉ có một phương thức trừu tượng duy nhất (gọi là phương thức hàm) và không hoặc nhiều phương thức mặc định.
Function
Function<T, R> là một trong những giao diện hàm (functional interface) tích hợp trong gói java.util.function. Mục đích chính của Function<T, R> là ánh xạ các tình huống, ví dụ, khi một đối tượng của một loại cụ thể được cung cấp làm đầu vào và nó được chuyển đổi (hoặc ánh xạ) thành các loại khác. Một trong những cách sử dụng phổ biến của Function là trong luồng (stream) trong đó hàm map() của một luồng sử dụng một thể hiện của Function để chuyển đổi luồng từ một loại thành một loại khác.
Vì Function<T, R> là một giao diện hàm, nó có thể được sử dụng làm mục tiêu gán cho một biểu thức lambda hoặc một tham chiếu phương thức.
“Mô tả Hàm” (Function Descriptor) là một thuật ngữ mô tả chữ ký của phương thức trừu tượng trong giao diện hàm. Điều quan trọng cần lưu ý là chữ ký của phương thức này có cú pháp tương tự chữ ký của biểu thức lambda.
Function Descriptor của Function<T,R>:
Mô tả Hàm (Function Descriptor) cho Function<T, R> sẽ là T -> R.
Ở đây, một đối tượng kiểu T
được đưa làm đầu vào cho lambda và nó sản xuất một đối tượng kiểu R
là giá trị đầu ra. Dưới đây là những điểm quan trọng cần lưu ý về giao diện Function
:
- Phương thức apply() là phương thức chức năng trừu tượng chính của giao diện
Function
. Nó nhận một tham số kiểuT
làm đầu vào và trả về một đối tượng đầu ra kiểuR
. Function<T, R>
chứa hai phương thức mặc định.- Phương thức mặc định đầu tiên là
compose()
, kết hợp hàm trên đó nó được kích hoạt (còn được gọi là hàm hiện tại) với một hàm khác được gọi làbefore()
. Nếu hàmbefore()
được áp dụng sau khi hàm kết hợp đã được áp dụng, thì kiểu đầu vào sẽ thay đổi từ kiểuV
thành kiểuT
. Sau đó, hàm hiện tại chuyển đổi đối tượng kiểuT
thành đối tượng kiểuR
làm đầu ra. Kết quả là hàm kết hợp được trả về, trong khicompose()
sử dụng cả hai hàm trong quá trình chuyển đổi từ kiểuV
thành kiểuR
. - Phương thức mặc định thứ hai là
andThen()
, kết hợp hàm trên đó nó được kích hoạt (hàm hiện tại) với các hàm khác có tên làafter()
, để khi hàm kết hợp được gọi, vào lúc đó ban đầu hàm hiện tại được kích hoạt, thay đổi kiểu đầu vàoT
thành kiểuB
. Sau đó, hàmafter()
được áp dụng, thay đổi kiểu từB
thànhV
. Kết quả là hàm kết hợp được đạt được bằng cách sử dụng phương thức mặc địnhandThen()
, nó sử dụng cả hai hàm trong quá trình chuyển đổi từ kiểuT
thành kiểuV
. Function<R, R>
cũng bao gồm một phương thức tĩnhidentity()
, đó là một hàm đơn giản, nó trả về đầu vào dưới dạng đầu ra. Như tên gợi ý, lập trình hàm được tạo dựa trên hàm như một đặc điểm quan trọng đầu tiên.- Giao diện Function (và các giao diện liên quan khác như IntFunction, DoubleFunction, LongFunction, BiFunction, và các giao diện khác được định nghĩa trong gói java.util.function) cho phép các hàm được chấp nhận dưới dạng đối số, được lưu trữ dưới dạng biến, và được trả về bởi các phương thức.
Tóm lại, các phương thức mặc định của giao diện Function như sau:
andThen(Function)
: Trả về một hàm kết hợp mà ban đầu áp dụng hàm này vào đầu vào của nó và sau đó áp dụng hàm đã chỉ định vào đầu ra. Cú pháp:
default <V> Function<T, V> andThen(Function<? super R, ? extends V> after)
Trong đó:
T
là kiểu đầu vào của hàm.R
là kiểu kết quả của hàm.V
là kiểu đầu ra của hàmafter
.after
là hàm sẽ áp dụng sau khi hàm này đã được áp dụng.
compose(Function)
: Tương tự nhưandThen()
, tuy nhiên, thực hiện theo thứ tự ngược lại (trước tiên áp dụng hàm đã chỉ định vào đầu vào của nó, sau đó áp dụng hàm này). Cú pháp:
default <V> Function<V, R> compose(Function<? super V, ? extends T> before)
Trong đó:
R
là kiểu đầu vào của hàm.V
là kiểu đầu ra của hàmbefore
và đầu vào của hàm này.- Kết quả là kiểu đầu ra của hàm đã kết hợp.
before
là hàm sẽ áp dụng trước khi áp dụng hàm này.
identity()
: Trả về một hàm luôn trả về đối số đầu vào. Cú pháp:
static <T> Function<T, T> identity()
Trong đó:
T
là kiểu đầu vào và đầu ra của hàm.
Ví dụ:
import java.util.function.Function;
public class FunctionDemo {
public static void main(String[] args) {
Function<Integer, String> sample = Function.<Integer>identity()
.andThen(i -> 2 + i)
.andThen(i -> "samplestr" + i);
// Example usage:
String result = sample.apply(3);
System.out.println(result); // Prints "samplestr5"
}
}
Hàm kết quả sẽ nhận vào một số nguyên, nhân nó với 2 và cuối cùng, thêm chuỗi “samplestr” vào đầu kết quả.
Để tạo một hàm duy nhất, phương thức andThen có thể được áp dụng nhiều lần. Tương tự, các hàm có thể được chuyển đối như đối số và cũng có thể được trả về từ các phương thức. Đoạn mã dưới thể hiện cách sử dụng API Ngày-Thời gian mới với Function.
Ví dụ:
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.util.function.Function;
public class DateTimeFunctionDemo {
public static void main(String[] args) {
Function<LocalDate, LocalDateTime> displayDateToTime = test -> test.andThen(d -> d.atTime(5, 4));
// Example usage:
LocalDate inputDate = LocalDate.now();
LocalDateTime result = displayDateToTime.apply(inputDate);
System.out.println(result);
}
}
Phương thức này sẽ nhận một hàm làm việc với LocalDate và biến đổi nó thành một hàm LocalDateTime (với thời gian 5:04 sáng) làm kết quả.
Chương trình hoàn chỉnh định nghĩa và sử dụng phương thức displayDateTime():
import java.util.function.Function;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import java.time.LocalDate;
import java.time.LocalDateTime;
public class FunctionDemo {
public static void main(String[] args) {
Function<LocalDate, LocalDateTime> plusTwo = Function.identity()
.andThen(displayDateTime(d -> d.plusMonths(2)));
Stream<LocalDate> dateStream = Stream.iterate(LocalDate.now(), d -> d.plusDays(1))
.limit(10)
.map(plusTwo)
.map(Object::toString);
String result = dateStream.collect(Collectors.joining(", "));
System.out.println(result);
}
public static Function<LocalDate, LocalDateTime> displayDateTime(final Function<LocalDate, LocalDate> test) {
return test.andThen(d -> d.atTime(2, 2));
}
}
Currying
Currying là một khái niệm liên quan đến lập trình hàm, trong đó một hàm f
có hai đối số (ví dụ, a
và b
) được tạo ra như một phiên bản thay thế cho một hàm g
có một đối số trả về một hàm. Giá trị trả về của hàm sau cùng là giống như giá trị của hàm ban đầu.
Đơn giản, currying là quá trình biến đổi một hàm có nhiều đối số thành một hàm có một đối số duy nhất. Đối số này chính là giá trị của đối số đầu tiên từ hàm ban đầu. Hàm trả về một hàm khác chỉ có một đối số. Hàm này sẽ nhận đối số thứ hai ban đầu và trả về một hàm khác chỉ có một đối số. Chuỗi này tiếp tục qua số lượng đối số của hàm gốc. Hàm cuối cùng trong chuỗi sẽ truy cập vào tất cả các đối số và có thể thực hiện bất kỳ công việc nào cần thiết.
Ví dụ:
f(a,b) = (g(a)) (b)
Ở đây f là function, a,b là tham số
Nếu một số lượng đối số ít hơn (không phải tất cả) được truyền vào một hàm, đây được gọi là hàm được áp dụng một phần (partially applied function). Điều này có thể làm ví dụ hoàn hảo cho việc chuyển đổi đơn vị.
Chuyển đổi đơn vị thường bao gồm một hệ số chuyển đổi từ thời gian này sang thời gian khác và một hệ số điều chỉnh cơ sở. Ví dụ, công thức để chuyển đổi Kilogram sang Pound, tức là, kg sang lbs(x) = x * 2.2046.
Dưới đây là mẫu cơ bản của tất cả các chuyển đổi đơn vị:
- Nhân với hệ số chuyển đổi.
- Điều chỉnh cơ sở (nếu cần).
Đoạn mã dưới thể hiện mẫu cơ bản của việc chuyển đổi đơn vị:
public static double converter(double x, double e, double y) {
return x * e + y;
}
Các phương pháp chuyển đổi đòi hỏi nhiều chuyển đổi giữa cùng một cặp đơn vị, ví dụ, từ độ Celsius sang độ Fahrenheit. Tuy nhiên, các phương pháp này phải thực hiện tính toán với ba giá trị giữa hai đơn vị. Điều này có thể gây ra lỗi nhập liệu thủ công trong một số trường hợp.
Tuy nhiên, một cách dễ dàng để tận dụng logic hiện có là tối ưu hóa nó với một bộ chuyển đổi curry. Đoạn mã dưới thể hiện việc sử dụng bộ chuyển đổi curry. Trong Java 8, currying có thể sử dụng biểu thức lambda:
static DuoCurryConverter(double e, double b){
return (double a) -> a*e + b;
}
Ở đây, mã nguồn trở nên linh hoạt hơn và tái sử dụng logic chuyển đổi hiện có. Thay vì truyền tất cả các đối số a, e và b cùng một lúc vào phương thức chuyển đổi, các đối số e và b được gọi để trả về một hàm khác, khi được cung cấp một đối số, sẽ trả về a * e + b. Phương pháp này cho phép tái sử dụng logic chuyển đổi và tạo ra các hàm khác nhau với các hệ số chuyển đổi khác nhau.
Hệ số chuyển đổi và cơ sở (e và b) được truyền vào mã nguồn một cách rõ ràng, cho phép mã thể hiện chính xác những gì được mong đợi.
Ví dụ, người dùng có thể sử dụng các chuyển đổi như sau:
DoubleUnaryOperatorconvert convertKgToLbs = curriedConverter(0, 2.2046);
DoubleUnaryOperatorconvert convertGEPEOFUR = curriedConverter(1.268, 0);
DoubleUnaryOperatorconvert convertCtoF = curriedConverter(9.0 / 5, 32);
Trong Đoạn mã dưới, DoubleUnaryOperator
định nghĩa một phương thức applyAsDouble()
, được sử dụng để thực hiện các chuyển đổi.
double gbp = convertUSDToGBP.applyAsDouble(1000);
Trong Ví dụ 2, một phương thức compose()
được định nghĩa, nó lấy hai thể hiện Function
và trả về một thể hiện Function
khác. Bên trong phương thức, phương thức apply()
được gọi trên đối số g
, sau đó trên h
bằng cách sử dụng một biểu thức lambda. Trong phương thức main()
, hàm compose()
được gọi.
public static <X, Y, Z> Function<X, Z> compose(Function<Y, Z> g, Function<X, Y> h) {
return o -> g.apply(h.apply(o));
}
public static void main(String[] args) {
// Tạo một thể hiện có tên là sin_asin
Function<Double, Double> sin_asin = compose(Math::sin, Math::asin);
// Để hiển thị kết quả, gọi apply trên thể hiện
System.out.println(sin_asin.apply(0.6));
}
Phương thức compose() được định nghĩa là một phương thức tĩnh, nhận hai hàm (g
và h
) và trả về một hàm mới. Trong ví dụ này, nó được sử dụng để kết hợp các hàm sin
và asin
. Kết quả của mã nguồn là 0.6
, đó là kết quả của việc áp dụng hàm sin_asin
lên đầu vào 0.6
.
Ví dụ hooàn chỉnh phương thức compose():
import java.util.function.BiFunction;
import java.util.function.Function;
public class JavaCurry {
public void curryFunction() {
// Create a function that adds two integers
BiFunction<Integer, Integer, Integer> adder = (x, y) -> x + y;
// Create a curried function
Function<Integer, Function<Integer, Integer>> currier = x -> y -> adder.apply(x, y);
// Display results
System.out.println("Curry:");
System.out.println(currier.apply(5).apply(2)); // Should print 7
}
public void compose() {
// Function to add 4
Function<Integer, Integer> addFour = x -> x + 4;
// Function to multiply by 5
Function<Integer, Integer> timesFive = x -> x * 5;
// Compose functions to first multiply by 5 and then add 4
Function<Integer, Integer> compose1 = addFour.compose(timesFive);
// Compose functions to first add 4 and then multiply by 5
Function<Integer, Integer> compose2 = timesFive.compose(addFour);
// Display results
System.out.println("Times then add: " + compose1.apply(7)); // Should print 39
System.out.println("Add then times: " + compose2.apply(7)); // Should print 55
}
public static void main(String[] args) {
JavaCurry curryDemo = new JavaCurry();
curryDemo.curryFunction();
curryDemo.compose();
}
}
Immutability (tính bất biến)
Lập trình hàm đặt ra một trong những yêu cầu quan trọng là sử dụng cấu trúc dữ liệu không thay đổi (immutable). Không thay đổi (immutable) là khả năng của một đối tượng để chống lại hoặc ngăn chặn sự thay đổi. Trong lập trình hàm, nếu trạng thái của một đối tượng không thay đổi sau khi nó được xây dựng, thì đối tượng đó được coi là không thay đổi. Ví dụ, kiểu dữ liệu String là một kiểu không thay đổi (immutable) trong Java.
Điều này là một trong những khái niệm cốt lõi trong lập trình hàm. Đối tượng không thay đổi đặc biệt quý giá trong các ứng dụng đồng thời. Chúng không thể bị nhiễu bởi xung đột luồng hoặc thấy trong trạng thái không thể đoán trước, vì chúng không thay đổi trạng thái.
Ví dụ:
public class Main {
public static void main(String[] args) {
String sample = "immutable";
System.out.println(sample); // immutable
change(sample);
System.out.println(sample); // immutable
}
public static void change(String str) {
str = "mutable";
}
}
Chương trình trong Ví dụ 4 hiển thị cùng một chuỗi ngay cả sau khi phương thức change()
đã được gọi, vì vậy chuỗi vẫn không thay đổi. Điều này xảy ra vì một đối tượng String trong Java được coi là không thay đổi (immutable).
Các đối tượng không thay đổi (immutable) không thể thay đổi trạng thái sau khi chúng đã được tạo ra.
Dưới đây là ba mục tiêu chính để sử dụng đối tượng không thay đổi thường xuyên, mục tiêu này cũng có thể được áp dụng để giảm số lượng lỗi được giới thiệu trong mã nguồn:
- Nếu biết rằng trạng thái của một đối tượng không thể bị thay đổi bởi một phương thức khác, thì sẽ dễ dàng hiểu cách chương trình của bạn hoạt động.
- Các đối tượng không thay đổi mặc định là an toàn đối với luồng.
- Các đối tượng không thay đổi có thể được sử dụng làm khóa trong một HashMap (hoặc tương tự), vì chúng có mã băm giống nhau mãi mãi. Mục nhập trong bảng băm của một bảng băm sẽ bị mất hiệu quả nếu mã băm của một phần tử trong bảng thay đổi, vì nỗ lực để tìm nó trong bảng sẽ dẫn đến việc tìm kiếm ở vị trí không chính xác. Đây là lý do chính mà các đối tượng String không thay đổi – chúng thường được sử dụng làm khóa trong HashMap.
Từ Java 8 trở đi, các lớp Date-Time đều không thay đổi và thực tế hầu hết mọi thứ được thêm vào từ Java 8 trở đi cũng là không thay đổi (ví dụ như Optional và Streams).
Immutable class
Một lớp không thay đổi (immutable class) là một lớp trong đó trạng thái của các thể hiện của nó không thay đổi sau khi nó được xây dựng.
Dưới đây là một số lớp không thay đổi trong Java như java.lang.String
, java.lang.Integer
, java.lang.Float
, và java.math.BigDecimal
.
Lớp không thay đổi loại bỏ khả năng dữ liệu trở nên không thể truy cập khi được sử dụng làm khóa trong Map và Set. Đối tượng không thay đổi không được phép thay đổi trạng thái của nó khi nó nằm trong một tập hợp.
Cách triển khai một lớp không thay đổi:
Đây là một số hướng dẫn về cách triển khai một lớp không thay đổi một cách đúng đắn:
- Lớp cần phải được đánh dấu là một lớp cuối cùng (final class). Lớp cuối cùng không thể được mở rộng.
- Tất cả các trường (fields) trong lớp phải được xác định là riêng tư (private) và không thay đổi (final).
- Không được định nghĩa bất kỳ phương thức nào có thể thay đổi trạng thái của đối tượng không thay đổi. Ngoài các phương thức Setter, điều này cũng áp dụng cho bất kỳ phương thức nào khác có thể thay đổi trạng thái của đối tượng.
Những hướng dẫn này phải được tuân theo khi tạo một đối tượng không thay đổi. Đừng trả về các tham chiếu có thể thay đổi tới người gọi.
Bạn phải cẩn thận khi sử dụng các function pattern trong Java để đảm bảo rằng mã nguồn không bất ngờ chuyển sang cách tiếp cận có thể thay đổi. Ví dụ cách viết như đoạn mã dưới cần được tránh:
int[] sampleCount = new int[1];
List<Item> sampleItems = ...;
list.forEach(sampleItems -> { if(SampleItems.isRed()) myCount++;})
Ví dụ mã giải quyết:
list.stream().filter(SampleItems::isRed).count();
Concurency (đồng thời)
Concurrency là khả năng cho phép nhiều quy trình có thể bắt đầu, chạy và kết thúc trong các khoảng thời gian trùng lắp. Tuỳ chọn, các quy trình có thể kết thúc cùng lúc. Ví dụ, máy tính với một lõi (single-core) có thể thực hiện nhiệm vụ đa nhiệm (multitasking).
Lập trình hàm tạo nên một cơ sở mạnh mẽ cho lập trình đồng thời (concurrent programming), bên cạnh đó, Java cũng hỗ trợ đồng thời trong nhiều cách khác nhau. (Một trong những cách đó là phương thức parallelStream()
trên Collection. Nó cung cấp một cách nhanh chóng để sử dụng một Stream đồng thời. Tuy nhiên, nên sử dụng cẩn thận vì độ đồng thời quá mức có thể làm chậm ứng dụng).
Một cách khác mà Java 8 hỗ trợ đồng thời là thông qua lớp CompletableFuture mới. Một trong các phương thức của nó là phương thức tĩnh supplyAsync()
, chấp nhận một thể hiện của giao diện hàm Supplier. Nó cũng có phương thức thenAccept()
, chấp nhận một Consumer quản lý việc hoàn thành công việc. Lớp CompletableFuture gọi phương thức cung cấp được chỉ định trong một luồng khác và thực hiện Consumer khi nó hoàn thành.
Recursion (đệ quy)
Đệ quy là một tính năng trong lập trình được hỗ trợ bởi nhiều ngôn ngữ, bao gồm cả Java. Việc mô tả một điều gì đó liên quan đến chính nó được gọi là đệ quy. Trong lập trình Java, đệ quy là tính năng cho phép một phương thức gọi chính nó. Một phương thức mà gọi chính nó được gọi là phương thức đệ quy.
Ví dụ:
Các ngôn ngữ lập trình hàm điển hình không đi kèm với cấu trúc lặp như while
và for
loop. Lý do là các cấu trúc như vậy thường là một lời mời tiềm ẩn để sử dụng thay đổi trạng thái (mutation).
Ví dụ, điều kiện trong một vòng lặp while
phải được cập nhật; nếu không, vòng lặp sẽ thực thi không hoặc vô số lần. Tuy nhiên, trong nhiều trường hợp, các vòng lặp hoạt động tốt. Việc thay đổi các biến cục bộ là chấp nhận được. Sử dụng vòng lặp while
với một bộ trình lặp (iterator) trong Java được thể hiện trong Đoạn mã dưới:
Iterator<Product> th = prds.iterator();
while(th.hasNext()){
prd = th.next();
// todo
}
Ở đây, điều này không phải là một vấn đề vì các thay đổi trạng thái (cả việc thay đổi trạng thái của Iterator bằng phương thức next và gán cho biến prd bên trong thân vòng lặp while) không ảnh hưởng tới danh ssách bên ngooài
Tuy nhiên, việc sử dụng vòng lặp for-each, chẳng hạn như một thuật toán tìm kiếm, là vấn đề vì thân vòng lặp đang cập nhật một cấu trúc dữ liệu được chia sẻ với biến danh sách liên quan, như được thể hiện trong Đoạn mã dưới:
public void searchForPlatinum(List<String> list, StatsBunch bunch) {
for (String p : list) {
if ("platinum".equals(p)) {
bunch.incrementFor("platinum");
}
}
}
hân vòng lặp có một tác động phụ không thể bỏ qua trong phong cách lập trình hàm; nó thay đổi trạng thái của đối tượng, mà được chia sẻ với các phần khác của chương trình.
Đây chính là lý do chính mà các ngôn ngữ lập trình hàm tinh khiết tránh hoàn toàn các hoạt động có vấn đề như vậy. Trong Java, mọi chương trình có thể được viết lại để tránh việc lặp bằng cách sử dụng đệ quy thay vì sử dụng thay đổi trạng thái. Sử dụng đệ quy loại bỏ nhu cầu về các biến lặp.
Ví dụ tính hàm giai thừa theo cách tiếp cận lặp, và thực hiện kết quả tương tự theo cách tiếp cận đệ quy:
public class FactorialIterative {
public static long factorialIterative(int n) {
long result = 1;
for (int i = 1; i <= n; i++) {
result *= i;
}
return result;
}
public static void main(String[] args) {
int n = 5; // Thay đổi n để tính giai thừa cho số khác
long result = factorialIterative(n);
System.out.println("Giai thừa của " + n + " là " + result);
}
}
public class FactorialRecursive {
public static long factorialRecursive(int n) {
if (n == 0 || n == 1) {
return 1;
} else {
return n * factorialRecursive(n - 1);
}
}
public static void main(String[] args) {
int n = 5; // Thay đổi n để tính giai thừa cho số khác
long result = factorialRecursive(n);
System.out.println("Giai thừa của " + n + " là " + result);
}
}
Bài tập
Để quản lý khách hàng đến thuê phòng của một khách sạn, người ta cần các thông tin sau: Số ngày thuê, loại phòng, thông tin cá nhân của những người thuê phòng.
Biết rằng phòng loại A có giá 500$, phòng loại B có giá 300$ và loại C có giá 100$.
Với mỗi cá nhân cần quản lý các thông tin sau: Họ tên, tuổi, số chứng minh nhân dân.
Yêu cầu 1:
Xây dựng abstract class PersonAbs gồm 2 các thuộc tính name,age, 1 phương thức trừu tượng là goToWork()
Hãy xây dựng lớp Person để quản lý thông tin cá nhân của những người thuê phòng kế thừa từ PersonAbs và có thêm thuộc tính quản lý CMND.
Trong phương thức goToWork, in ra dòng lệnh “go to work”
Yêu cầu 2:
Xây dựng interface Ihotel gồm 3 phương thức addCustomer, deleteCustomer, calculatePrice để thể hiện các nghiệp vụ của khách sạn
Xây dựng lớp Hotel triển khai từ Ihotel để quản lý các thông tin về các phòng trong khách sạn.
Yêu cầu 3: Xây dựng các phương thức thêm mới, xoá khách theo số chứng minh nhân dân. Tính tiền thuê phòng cho khách(xác định khách bằng số chứng minh nhân dân) dựa vào công thức: (số ngày thuê * giá của từng loại phòng)