Mặc dù Java là một ngôn ngữ lập trình kiểu tĩnh (statically-typed), nó vẫn có những đặc điểm của một ngôn ngữ lập trình dynamic, khiến nó linh hoạt hơn trong một số tình huống nhất định. Điều này đôi khi gây nhầm lẫn cho các lập trình viên mới, nhưng chúng ta sẽ xem xét chi tiết các yếu tố làm cho Java có thể được coi là dynamic, bất chấp bản chất kiểu tĩnh của nó.

Đặc điểm kiểu tĩnh của Java

Trước khi đi sâu vào lý do tại sao Java có các tính năng dynamic, chúng ta cần hiểu rằng Java được thiết kế là một ngôn ngữ lập trình kiểu tĩnh. Điều này có nghĩa là kiểu dữ liệu của các biến được xác định tại thời điểm biên dịch (compile-time) và không thể thay đổi trong quá trình chạy (run-time).

Ví dụ, khi khai báo biến trong Java, kiểu dữ liệu của biến đó phải được xác định rõ ràng:

int number = 10;  // Biến number có kiểu dữ liệu int

Kiểu của biến number được xác định tại thời điểm biên dịch và không thể thay đổi thành kiểu khác, như String, trong quá trình chạy.

Các yếu tố dynamic của Java

Reflection API

Reflection là một tính năng mạnh mẽ của Java cho phép kiểm tra và thao tác với các lớp, phương thức, và trường dữ liệu của chúng trong thời gian chạy. Thông qua Reflection API, bạn có thể tạo các đối tượng, gọi phương thức, hoặc truy cập các trường của một lớp mà không cần biết trước về chúng tại thời điểm biên dịch.

Điều này mang lại tính linh hoạt tương tự như trong các ngôn ngữ dynamic, nơi bạn có thể thay đổi hành vi của chương trình trong runtime mà không cần tuân thủ hoàn toàn vào cấu trúc tĩnh.

Ví dụ, bạn có thể tạo một đối tượng của một lớp mà tên của nó chỉ biết trong thời gian chạy:

Class<?> clazz = Class.forName("com.example.MyClass");
Object obj = clazz.getDeclaredConstructor().newInstance();

Tính năng này cho phép Java trở nên rất linh hoạt, đặc biệt trong các ứng dụng như frameworks hoặc các công cụ cần tải và thực thi mã động, chẳng hạn như Spring và Hibernate.

Dynamic Class Loading

Java có khả năng tải các lớp (classes) trong thời gian chạy, thay vì phải tải tất cả mọi thứ trong thời điểm khởi tạo chương trình. Điều này cho phép bạn thay đổi hoặc thêm mới các lớp mà không cần phải biên dịch lại toàn bộ chương trình.

Ví dụ, JVM có thể sử dụng các lớp được tải từ bên ngoài như các thư viện jar động trong thời gian chạy. Điều này mang lại tính linh hoạt và khả năng mở rộng mà các ngôn ngữ lập trình dynamic thường có.

ClassLoader classLoader = ClassLoader.getSystemClassLoader();
Class<?> clazz = classLoader.loadClass("com.example.DynamicClass");

Dynamic class loading rất hữu ích trong việc xây dựng các hệ thống mô-đun, nơi các thành phần có thể được tải động khi cần thiết, hoặc trong các ứng dụng web, nơi các thư viện có thể được tải động trong runtime.

Giao diện và tính đa hình

Java hỗ trợ tính đa hình (polymorphism), cho phép các đối tượng được truy cập thông qua giao diện hoặc lớp cha mà không cần biết đối tượng thực sự thuộc lớp con nào tại thời điểm biên dịch. Điều này mang lại tính năng linh hoạt trong runtime, một trong những đặc điểm của các ngôn ngữ dynamic.

Ví dụ, bạn có thể sử dụng một biến kiểu interface để lưu trữ bất kỳ đối tượng nào thực hiện interface đó mà không cần quan tâm đến kiểu cụ thể của đối tượng:

List<String> myList = new ArrayList<>();
myList.add("Hello");

Ở đây, myList được khai báo là kiểu List, nhưng thực tế nó được gán cho đối tượng thuộc lớp ArrayList. Điều này cho phép bạn thay đổi cách triển khai của List mà không phải thay đổi mã nguồn sử dụng myList.

Dynamic Proxy

Java cung cấp tính năng Proxy trong java.lang.reflect.Proxy, cho phép tạo các đối tượng proxy động trong runtime mà không cần biết trước về các lớp cụ thể. Điều này mang lại khả năng thay đổi hoặc thêm mới hành vi vào các đối tượng trong runtime một cách linh hoạt.

Ví dụ, bạn có thể sử dụng InvocationHandler để tạo các đối tượng proxy thực hiện một giao diện mà không cần lớp cụ thể nào:

MyInterface proxyInstance = (MyInterface) Proxy.newProxyInstance(
    MyInterface.class.getClassLoader(),
    new Class<?>[] { MyInterface.class },
    (proxy, method, args) -> {
        System.out.println("Method " + method.getName() + " is called");
        return null;
    }
);

Proxy động là một công cụ mạnh mẽ cho các ứng dụng cần xử lý các logic phức tạp trong runtime, chẳng hạn như việc triển khai các mô hình lập trình AOP (Aspect-Oriented Programming).

Thực thi mã động (Dynamic Code Execution)

Java hỗ trợ tính năng biên dịch và thực thi mã trong thời gian chạy thông qua các thư viện như javax.tools.JavaCompiler. Điều này cho phép biên dịch mã từ chuỗi ký tự hoặc tệp tin trong thời gian chạy và thực thi nó, mang lại khả năng rất linh hoạt cho các ứng dụng cần tính năng động.

JavaCompiler compiler = ToolProvider.getSystemJavaCompiler();
compiler.run(null, null, null, "Test.java");

Điều này hữu ích trong các ứng dụng như hệ thống plugin hoặc môi trường phát triển tích hợp (IDE), nơi mã có thể được viết, biên dịch và thực thi ngay lập tức.

Scripting API (Tích hợp với ngôn ngữ kịch bản)

Java từ phiên bản 6 đã hỗ trợ Scripting API, cho phép tích hợp với các ngôn ngữ dynamic như JavaScript, Python, Ruby, và nhiều ngôn ngữ khác. Điều này mang lại khả năng thực thi mã kịch bản (script) trong thời gian chạy mà không cần biên dịch trước.

Ví dụ, bạn có thể sử dụng ScriptEngine để thực thi mã JavaScript ngay trong ứng dụng Java:

ScriptEngineManager manager = new ScriptEngineManager();
ScriptEngine engine = manager.getEngineByName("JavaScript");
engine.eval("print('Hello, world!');");

Tính năng này cho phép Java tương tác với các ngôn ngữ dynamic và mang lại nhiều lợi ích cho các ứng dụng cần xử lý mã động hoặc tích hợp với các ngôn ngữ khác.

Tóm lại, Java mặc dù là một ngôn ngữ kiểu tĩnh, nhưng vẫn có nhiều tính năng dynamic thông qua cơ chế Reflection, dynamic class loading, Proxy, scripting, và tính đa hình. Những yếu tố này giúp Java có tính linh hoạt cao, mang lại lợi ích của các ngôn ngữ dynamic mà không từ bỏ sự an toàn về kiểu dữ liệu và hiệu suất của ngôn ngữ kiểu tĩnh.