Tuesday, October 7, 2008

CRITICISM OF JAVA

The Java programming language was intended to serve as a novel way to manage software complexity, though a number of criticisms have been leveled at the language.

Class path
Running a Java program originally required all third-party supporting libraries to be in the class path.

Prior to Java 6 it was necessary for each .jar or .zip archive required to be explicitly named in the class path. Java 6 provides a way around this by allowing directories listed in the class path to end in an asterisk (*), which will be expanded to the names of all files ending in .jar or .JAR within the directory. Such an entry does not, however, match .zip or .class files within that directory.

Resource management

Java performs garbage collection, so memory management is automatic, which makes allocation and deallocation errors such as memory leaks less likely and, barring errors in the VM, eliminates segmentation violations. But it does not manage all resources, such as file handles, JDBC database connections, and network connections; these must be released just as memory would need to be in C++. Leaks are particularly likely when exceptions are thrown, if try/catch/final are not used properly.

Java ensures the execution of "finalization" methods before garbage collection occurs, without guaranteeing any particular order of execution between related resources, and without guaranteeing when or even if automatic garbage collection of any given object will occur.

Conceptually, Java always allocates objects on the heap. Only the compiler may optimize this to faster stack-based allocation. In this respect Java is less flexible than C++, which allows (requires) the programmer to control where objects are allocated. In both languages, values of primitives type are allocated on the stack, and the compiler decides how to use the available registers.


Language choices

Primitives vs. objects

Java designers decided not to implement certain features present in other languages (including multiple inheritance, operator overloading, and tuples). Java permits multiple inheritance of interfaces but not of implementations.

Java's primitive types are not objects. Primitive types hold their values in the stack rather than being references to values. Because of this, Java is not considered to be a pure object-oriented programming language and this makes reflection more complicated. The motivation for primitive types being non-object oriented was performance considerations.

Java 5.0 and later supports automatic conversion (autoboxing) of primitive data types to corresponding object form wherever required. This is not without problems, however:

Special care must be taken when comparing two boxed primitive types as the boxed instances may have identity as reference objects.
When autounboxing, a null pointer exception may be thrown. Since this operation occurs implicitly (without a cast or method call), this unchecked exception may not be obvious by inspection of the line of code to an unfamiliar reader.
Boxed primitives do not allow for operator based arithmetic; either the programmer must ensure an unbox operation, or she must use member methods which does not lend itself to be read as fluently as operator based arithmetic.

Generics
When generics were added to Java 5.0, there was already a large framework of classes (many of which were already deprecated), so generics were chosen to be implemented using erasure to allow for migration compatibility and re-use of these existing classes. This limited the features that could be provided by this addition as compared to other languages.

There is a common misconception that type erasure was necessary for backwards compatibility in the sense that old code could run on a new JVM and new code could call old code. However, this was entirely possible without type erasure (i.e. with reified generics), as has been demonstrated with the addition of generics to the very similar C# programming language[citation needed]. Java 5.0 could have implemented new generic collections classes with reified generics and have those collection classes implement the non-generic collection interfaces such as IList, ISet etc.

Type erasure was only necessary because of a requirement for a temporary migration compatibility; a requirement that the collection classes be the same classes, so that code which did not insulate itself through use of interfaces (thus breaking best practices) could co-exist in the same JVM with new generic style code without the use of wrapping techniques.

Type erasure has several drawbacks:

Typecasting overhead when inserting and removing from a collection and when calling any other method of a generic type which takes a parameter of a parameterized type.
Inability to use primitive types as type parameters. Because all types must be of reference type and must be "erased" to their upper bounds, it was not possible to allow the use of primitive types. Instead Java 5.0 features autoboxing which "boxes" primitive type on insertion into collections, thus incurring extra overhead and making the code more opaque.
What looks like separate classes at development time is really a single class at runtime. Thus static (class-level) members are shared among all classes realized from the generic class.
Inability to create new instances or new arrays of types defines through a type parameter.
Disparity with arrays which means that in general the developer should never let a method return an array of a parametric type.
The type erasure process may produce clashing method signatures.
Inability to implement different realizations of the same generic interface.
Inability to use generic exceptions

Non-Virtual methods

Java provides no way to make methods non-virtual (although they can be "sealed" by using the final modifier to disallow overriding). This means that there is no way to let derived classes define a new, unrelated method with the same name. This can be a problem when a base class is designed by a different person, and a new version introduces a method with the same name and signature as some method already present in the derived class. This means that the method in the derived class will implicitly override the method in the base class, even though that was not the intent of the designers of either class. To partially accommodate for these versioning problems, Java 5.0 introduced the @Override annotation, but to preserve backwards compatibility it could not be made compulsory by default.


Single paradigm

Java is predominantly a single-paradigm language. The addition of static imports in Java 5.0 accommodates the procedural paradigm better than earlier versions of Java. It is expected that the addition of new language features like closures (or lambdas) in version 7 will allow a more functional style (but the language specification has not yet been finalised).


Exception handling

Java embraced the concept of exception specifications from C++ where they were optional, but made throws clauses mandatory for any checked exception. While this can be a benefit for small systems, there is no universal agreement that using checked exceptions is a benefit for larger systems. In particular, "higher level" code is often not interested in errors thrown by "lower level" code (eg: NamingException). The coder of the naming classes must make a choice: either force higher level code to deal with naming exceptions as checked exceptions, or allow them to "bubble up" through his own low-level code without compile-time checks.


Closure

Finally, while anonymous inner classes provide a basic form of closures, they are not complete and require referenced variables to either be class fields or declared "final". The rationale behind this is that it allows JVM implementors to choose a stack model for variable lifetimes, so that a variable scope is removed when exited, thus preventing real closures. In addition, when using - for instance - "Runnable" as a closure, one has to declare the "run" method and put the code in that: you cannot simply put some code in braces and pass it around (however the Java 7 language specification is aiming to address this with first class closures).


Floating point arithmetic
While Java's floating point arithmetic is largely based on IEEE 754 (Standard for Binary Floating-Point Arithmetic), certain features are not supported even when using the strictfp modifier, such as Exception Flags and Directed Roundings — capabilities mandated by IEEE Standard 754. Many so-called "Java gotchas" are not problems with Java per se, but problems that are inevitable whenever using floating point arithmetic.


Look and feel
look and feel of GUI applications written in Java using the Swing platform may look different from native applications. While programmers can choose to use the AWT toolkit that displays native widgets (and thus look like the operating platform), the AWT toolkit is unable to meet advanced GUI programming needs by wrapping around advanced widgets and not sacrificing portability across the various supported platforms, each of which have vastly different APIs especially for higher-level widgets. If an alternative GUI toolkit is used, such as SWT, it is possible for a Java application to have native look and feel whilst also having access to advanced widgets.

The Swing toolkit – written completely in Java – both creates the problem of having a different look and feel from native applications, and avoids the problem of being limited by native toolkit capabilities because it reimplements widgets using only the most basic drawing mechanisms that are guaranteed available on all platforms. Unfortunately, the default installations of the JRE (as of August 2006) do not use the system's "native" look and feel, instead defaulting to the built-in Metal Look and Feel. If the programmer doesn't take care to set the native look and feel, users will have applications whose appearance is vastly different from that of their native applications. Apple Computer's own optimized version of the Java Runtime, which is included within the Mac OS X distribution, by default does set the default and implements its "Aqua" look-and-feel, giving Swing applications on the Macintosh a similar appearance to native software. Even in this environment, the programmer must still do some extra work to ensure that that application looks like an Aqua one (for example, they must set system properties to ensure the menubar is rendered in the OS X menubar and not in the application window as it would be on other platforms).


Performance

Java's performance has improved substantially since the early versions, and performance of JIT compilers relative to native compilers has in some tests been shown to be quite similar. The performance of the compilers does not necessarily indicate the performance of the compiled code; only careful testing can reveal the true performance issues in any system.

In a paper written in 1999 by Lutz Prechelt it is outlined that, statistically, programmer efficiency and experience has a bearing many standard deviations greater on run-time and memory usage than language choice. This paper specifically uses Java as a basis for the comparison, due to its then bad reputation.[19] Sun Microsystems have taken considerable trouble to address these problems, and regularly produce white papers on this topic. A more recent study (2003–4) gives Java a comparable performance to C++.


General
Java bytecode can either be interpreted at run time by a virtual machine, or it can be compiled at load time or runtime into machine code which runs directly on the computer's hardware. Interpretation is slower than native execution, and compilation at load time or runtime has an initial performance penalty for the compilation.


Language constraints
There are a few language requirements which incur an unavoidable time penalty, although these features are not unique to Java. Among these are array bounds checking, run-time type checking, and virtual function indirection (although each of these can in some situations be avoided by an optimizing compiler). Also the lack of features can affect performance. For example, Java does not have arrays of structures or a true multi-dimensional array, but only an array of references to objects or further arrays. Nor does Java allow returning more than one value from a function without using an object. The net result is that Java code makes more heap allocations than some other languages.


Garbage collection
The garbage collector controls when objects are deleted from memory. Java does not allow the programmer to control when garbage collection occurs - it cannot be delayed, or exercised on a particular object. While this makes programming much simpler and reduces memory leaks, it lacks the flexibility that can, in some cases, result in a more efficient handling of memory. The programmer can call the function System.gc() as a hint that this is a suitable time to run the garbage collector, but the JVM is not obliged to honour this suggestion. Lower-level languages provide more flexibility over memory management, and while garbage collection is not part of the language specification and therefore its use is not mandated, in some cases (notably C and C++) a number of third-party garbage collectors are available.

The use of a garbage collector to automatically delete objects can add overhead compared to manual deallocation and can have a positive or negative impact on performance depending upon the garbage collector implementation and the characteristics of the application's use of objects. With the modern generational garbage collectors used in many JVMs, many applications actually experience greater performance because of faster allocation and deallocation algorithms.

Because the garbage collector may run at any time, Java is unsuitable for real time programming. In older JVM implementations, collection paused the program's own threads. The current implementation, using concurrent collection, does not need to pause the program but will typically slow its execution. This is problematic in a real-time environment because making scheduling guarantees is impossible with arbitrary interruptions, and deadlines may be missed while the garbage collector is running.


Byte code vs. native compilation
Relative performance of code produced by JIT compilers as compared to AOT compilers can be quite close, and is often a subject of debate. The JIT compilation stage may be time consuming, which is inconvenient for applications that are short-lived and/or contain large amounts of code. Once compiled to native code, however, the performance of the program can be comparable to that achieved by a native compiler, even on numerical tasks. Although Java does not support manual inlining of method calls, many JIT compilers perform this optimization at load time and can exploit information from the runtime environment to guide more effective transformations, such as profile-directed inlining. Dynamic recompilation, as provided by Sun's HotSpot JVM, can exceed the performance of the static compilation available in most other languages by exploiting information that is only available at runtime.


Hardware interfacing

Because Java was designed with an emphasis on security and portability, it does not support direct access to the machine architecture and address space. This means working directly with a specific piece of hardware such as a scanner, digital camera, audio recorder, video capture, or any hardware that requires direct memory space control (typically those pieces or hardware installed with drivers), cannot easily be accomplished with Java. An illustration of this issue is seen in version 1.0 of Java as it was not possible to access a printer because the interface code to the various printer drivers was not included in this first JVM.


Interfacing with native code

Client side or server systems that need to "talk" to the hardware must implement a hybrid solution using Java and C/C++ or assembly language via the Java Native Interface (JNI) libraries to link native code to the Java libraries. An alternate solution is to code the hardware software component in its native C/C++/assembler language and then pass the data via files, databases or a shared memory interface, although this is not an ideal solution.

Using the JNI technique introduces many possible inconsistencies such as: machine dependency, potential deadlock situations, memory allocation leaks, and possibly poor application performance, not to mention code complexity of needing to maintain two different code bases.

However, other simpler solutions have been developed since JNI. Java Native Access is comparable to .NET P/Invoke, in that no boilerplate C glue code have to be written to be able to access the native library.

No comments: