Thứ Sáu, 20 tháng 11, 2009

Những hạn chế của Java

Mặc dù chưa thực sự gây một tác động nào đáng kể đối với vị trí độc quyền của C/C++ trong các ứng dụng đòi hỏi performance cao, lập trình games và lập trình hệ thống, nhưng dưới tầm ảnh hưởng của các tập đoàn lớn như Sun, Oracle, IBM, Java đã tìm được cho nó một sân chơi riêng: embedded devices và server-side programming.

Tuy nhiên, đến khi các ngôn ngữ linh động hơn, thích hợp hơn cho sân chơi này xuất hiện - như Python, Ruby, Scala - và ASP.NET ngày một trở nên chín chắn hơn, thì những điểm yếu của Java bắt đầu được bộc lộ. Những tín đồ Java lâu năm giờ đây quay lưng lại và chỉ trích nó, như Bruce Tale viết trong cuốn Beyond Java, và dư luận bắt đầu xôn xao xung quanh những tiên đoán về cái chết của Java.

Có lẽ phương châm bảo thủ, coi trọng an toàn hơn tốc độ, khống chế quyền tự do của lập trình viên nhằm đảm bảo độ tin cậy cho code, và việc Java chạy trên một VM đã khiến nó không thể cạnh tranh được với C/C++ trên vương quốc của hai ngôn ngữ này. Thế nhưng đâu là nguyên nhân khiến cho Java tự đánh mất thị phần vào tay các giải pháp mới nổi?

Chúng ta hãy cùng điểm qua một số hạn chế trong thiết kế của ngôn ngữ Java và thư viện chuẩn của nó:

1. Thiếu language support cho collections

Ta có mảng động các số nguyên a. Ta cần nhân đôi mỗi phần tử của nó. Dưới đây là cách làm chuẩn trong Java, đã áp dụng autoboxing của Java 5 và double brace initialization (thanks to anh Buu Nguyen):

ArrayList a = new ArrayList() {{
    add(1);
    add(2);
    add(3);
}};

for (int i = 0; i != a.size(); ++i)
{
    a.set(i, a.get(i) * 2);
}

Câu trả lời của Java cho thấy một số vấn đề:

  • Java không support collection initializer (như D, C# 3.0, Ruby, Python, Scala).
  • Không support automatic type inference (như D, C++0x, C# 3.0).
  • Không support operator overloading (như D, C++, C#, Ruby, Python, Scala).
  • Không cho phép vòng foreach hiệu chỉnh các phần tử của collection (như D, C++0x).

Các thao tác liên quan đến collections (như ArrayList, TreeMap, HashMap) diễn ra thường xuyên đến nỗi C# 3.0 đã giới thiệu LINQ để thu hút lập trình viên. Việc Java không đối xử đặc biệt với collections ở mức language là một nguyên nhân khiến cho code viết trên Java tỏ ra xấu xí và thô kệch hơn so với các ngôn ngữ mới.

Java 7 sẽ hỗ trợ type inference.
2. Thư viện chuẩn "không iterable"

Java 5 giới thiệu vòng foreach với mục đích rút gọn những đoạn code duyệt qua một đối tượng có thể duyệt được (iterable). Tuy nhiên, thư viện chuẩn thì hình như không được tái thiết kế để trở nên thân thiện hơn với cải tiến mới này, đơn cử như StringTokenizer:

StringTokenizer tokenizer = new StringTokenizer("this sucks");
while (tokenizer.hasMoreTokens())
{
    String token = tokenizer.nextToken();
    // Do something with token here
}

Trong khi lập trình viên kỳ vọng sẽ được áp dụng foreach như sau:

for (String token : new StringTokenizer("this sucks"))
{
    // Do something with token here
}

Nguyên nhân là vì StringTokenizer implements Enumeration mà không implement Iterable, trong khi foreach chỉ làm việc với Iterable.

Rất nhiều class trong thư viện chuẩn không implement Iterable mặc dù điều này là hợp lý, chẳng hạn:

Điều này thể hiện sự không nhất quán trong thư viện chuẩn của Java (và hiển nhiên là các thư viện thuộc bên thứ ba). So với Java Collection Framework, C++ STL được thiết kế chặt chẽ hơn rất nhiều.

3. Không hỗ trợ mixin hoặc đa kế thừa

Ta có interface Speakable. Class DefaultSpeakable cung cấp default implementation cho Speakable. Class SuperDog extends class Dog và implement Speakable. Ta muốn SuperDog sử dụng lại default implementation từ DefaultSpeakable, có thể override một số methods theo ý mình. Hiển nhiên Java không cho phép SuperDog kế thừa DefaultSpeakableSuperDog đã kế thừa Dog mất rồi. Khi này ta không có sự lựa chọn nào khác ngoài implement lại Speakable từ đầu.

Tình huống trên xảy ra khá thường xuyên. Lấy thư viện chuẩn làm ví dụ. Mỗi khi implement Iterator, ta buộc phải implement hasNext(), next(), remove(). Giả sử Java cho phép một trong hai biện pháp - mixin hoặc đa kế thừa - thì hẳn sẽ có một class DefaultIterator nào đó cung cấp sẵn remove() (throw new UnsupportedOperationException()) và hasNext() (return next() == null). Trong phần lớn trường hợp, người sử dụng sẽ chỉ phải tự viết lấy next().

Hạn chế này làm các interface trong Java luôn cố gắng giảm bớt số lượng methods một cách không tự nhiên. Thử so sánh Java Iterator với Ruby Enumerable (Ruby là một ngôn ngữ hỗ trợ mixin).

Như vậy có hai hậu quả chính khi Java quyết định loại bỏ đa kế thừa mà lại không cho phép interface có implementation (mixin):

  • Người sử dụng không thể dùng default implementation nếu class của anh ta đã kế thừa một class khác. Anh ta chỉ còn cách copy-paste từ default implementation.
  • Người viết thư viện không thể thoải mái cung cấp thêm tính năng vào trong interface. Điều này làm thiết kế của anh ta đôi khi trở nên gượng ép.
4. Không chấp nhận khái niệm function

Java chỉ có khái niệm method. Method thuộc về class. Đây là lý do tại sao HelloWorld trong Java tương đối dài hơn bình thường một cách không cần thiết:

public class HelloWorld
{
    public static void main(String[] args)
    {
        System.out.println("It takes some time but hey, hello!");
    }
}

Nhưng Java có static method. Liệu có thể coi static method như function chính quy, và class mà static method thuộc về có thể coi như namespace cho function? Không hẳn, ít nhất là so với các ngôn ngữ mà function (hay callable objects) được coi trọng (D, C, C++, Ruby, Python, Scala và hàng loạt các functional languages khác - C# có delegate):

  • Không thể trừu tượng hoá một function call. Cũng tức là:
  • Static methods trong Java không thể dùng làm argument cho các methods khác.

Java tìm cách trốn tránh hai hạn chế trên bằng cách khuyến khích sử dụng interface kết hợp với anonymous inner class. Chẳng hạn ta cần viết một method lọc ra các phần tử thoả mãn một điều kiện nhất định trong Java. Bởi lẽ không thể trừu tượng hoá "điều kiện nhất định" thành một con trỏ hàm đơn giản nên lập trình viên Java chỉ còn cách sử dụng interface Predicate<T> với một method bên trong là isSatisfied(T elem), và như vậy signature cho method ban đầu trở thành:

Iterable filter(Iterable source, Predicate condition); 

Và thông thường thì người sử dụng sẽ create một anonymous inner class implement Predicate<T> mỗi khi gọi method này. Giải pháp thiếu trực quan và làm code trở nên thô kệch này xuất hiện khắp mọi nơi trong thư viện chuẩn, có lẽ bởi vì function là một khái niệm không thể thiếu trong lập trình.

Trong bài viết nổi tiếng The Kingdom of nouns, Steve Yegge tranh luận rằng sự ép buộc phải thiết kế theo danh từ (object/classes) mà không tồn tại khái niệm động từ (function) của Java là một sai lầm lớn.

Java 7 dự định sẽ giới thiệu closure để khắc phục một phần. Nhưng có vẻ như đề xuất này đã bị loại bỏ.
Cùng nhiều hạn chế khác ...

Sau đây là một số nhược điểm nữa mà các tác giả khác đã đi sâu, tôi đồng ý và không cảm thấy cần thiết phải viết lại ở đây:

  • Checked exception: Java cho rằng một số exception nhất định bắt buộc phải được catch. Thêm một nguyên nhân khiến cho code trở nên dài dòng.
  • Type erasure: Java 5 implement generic programming bằng type erasure - nói đơn giản ArrayList<Person> sẽ trở thành ArrayList<Object> lúc runtime.
  • Không thể viết generic code vừa làm việc được với array vừa làm việc được với Java Collection Framework (C++/D khắc phục bằng iterator). Ví dụ.
  • Destructor trong Java (Object.finalize()) không chắc chắn sẽ được gọi trước khi process kết thúc. Điều này loại bỏ hoàn toàn khả năng áp dụng RAII vào trong thiết kế - nói đơn giản, nếu bạn đã open một FileStream thì phải explicitly close() nó.
  • Không có khái niệm property trong class: mỗi property tương ứng với một cặp get/set. Điều này làm code trở nên thô kệch cho cả người viết thư viện và người sử dụng. Xem thêm C# Property.

Kết luận

Trong những năm vừa qua, Java đã làm nền tảng cho rất nhiều thư viện và framework - trên thực tế, quá nhiều và chúng đều quá cồng kềnh. Nguyên nhân một phần do lỗi của người viết thư viện (lạm dụng XML và overdesign là hai bất cập hàng đầu), nhưng không thể phủ nhận những hạn chế trong ngôn ngữ Java và sự định hướng sai lệch của thư viện chuẩn cũng đã gây ảnh hưởng không nhỏ.

Với việc Java 7 từ chối hầu hết các đề xuất nhằm khắc phục những nhược điểm sâu sắc nhất thuộc về ngôn ngữ thì tương lai của Java lại càng trở nên đáng lo ngại hơn bao giờ hết. Cho đến nay, uy tín của Java trong giới hackers (hardcore programmers), startups, researchers đã giảm hẳn - nó vốn chưa bao giờ được họ coi trọng. Khi những bộ phận này thay đổi sở thích, các công ty lớn và các trường đại học rất có thể sẽ từ từ chuyển mình theo.