What we miss in Java

Original author: Ben Evans
  • Transfer

In this article, we will look at some of the “missing” features in Java. But we must immediately emphasize that some things will be deliberately omitted, which are either actively discussed or require too much work at the virtual machine level. For example:

There are no materialized generics . Only the lazy did not write about this, and most of the comments indicate a lack of understanding of the essence of type mashing. If a Java developer says, “I don't like type overwriting,” then in most cases it means “I need to List int.” The question of the primitive specialization of generics is only indirectly related to mashing, and the benefits of generics that are visible during execution are greatly exaggerated by rumors.

Unsigned arithmetic calculations at the virtual machine level.The absence of unsigned arithmetic types in Java has been frustrating developers for many years. But this is a deliberate decision of the creators of the language. The presence of only iconic calculations greatly simplifies the language. If today we start to implement unsigned types, this will entail a very serious reworking of Java, which is fraught with a lot of big and small bugs that will be difficult to catch. At the same time, the risk of destabilizing the entire platform increases greatly.

Long pointers for arrays . Again, the introduction of this functionality will require too deep processing of the JVM with possible unpleasant consequences, and not only in terms of behavior and semantics of garbage collectors. Although it should be noted that Oracle is looking for ways to implement such functionality using the VarHandles project.

Here we will not go into details of the possible Java syntax for the functionality discussed. Unfortunately, such discussions generally slide into debate over syntax, although semantics are far more important.

More expressive import syntax

The import syntax in Java provides us with not so many possibilities, only two options are available: import of one class or the whole package. And if you need to import only part of the package, then you have to pile up a bunch of lines. Also, poor syntax makes IDE features such as import folding for the largest Java files necessary.

Our life would be easier if we could import several classes from one package in one line:

import java.util.{List, Map};

Code readability would be enhanced by the possibility of local type renaming (or creating an alias). At the same time, there would be less confusion with types having the same short class name:

import java.util.{Date : UDate};
import java.sql.{Date : SDate};
import java.util.concurrent.Future;
import scala.concurrent.{Future : SFuture};

The implementation of extended wildcards would also be beneficial:

import java.util.{*Map};

This is a small but useful change that can be fully implemented with javac.

Collection Literals

Java has syntax (albeit limited) for declaring array literals. For instance:

int[] i = {1, 2, 3};

There are a number of disadvantages to this syntax. For example, literals should only be used in initializers.

Arrays in Java are not collections. The “bridge methods” presented in the helper class Arraysalso have flaws. Let's say the method Arrays.asList()returns ArrayList, which upon closer examination turns out to be Arrays.ArrayList. This inner class does not contain alternative methods List, and similar methods throw an exception OperationNotSupportedException. As a result, an ugly seam appears in the API that makes it difficult to transition between arrays and collections.

There is no reason to refuse the syntax for declaring literals in an array; in one form or another, it is present in many languages. For example, in Perl you can write this:

my $primes = [2, 3, 5, 7, 11, 13];
my $capitals = {'UK' => 'London', 'France' => 'Paris'};
На Scala — так:
val primes = Array(2, 3, 5, 7, 11, 13);
val m = Map('UK' -> 'London', 'France' -> 'Paris');

Unfortunately, there are no useful collection literals in Java. This question has been raised repeatedly, but neither Java 7 nor Java 8 has this functionality. Object literals are also of interest, but in Java they are much more difficult to implement.

Structural typing

Naming plays a very large role in the type system in Java. All variables must refer to named types, and it is impossible to express a type only by defining its structure. In other languages, for example, in Scala, you can express a type without declaring it when implementing the interface (or the Scala trait), but simply confirming that it contains a certain method:

def whoLetTheDucksOut(d: {def quack(): String}) {

In this case, any type containing the method will be accepted quack(), regardless of whether there is inheritance or whether the types use the common interface.

It is no coincidence that an example was chosen quack()- structural typing has much in common with duck typing in the same Python. However, in Scala, typing is done during compilation, which indicates the flexibility of the language in terms of expressing types that would be difficult or impossible to express in Java. Unfortunately, the type system here has very little structural typing capability. You can specify a local anonymous type with additional methods, and if one of them is called immediately, then Java will allow you to compile the code.

The possibilities end there: we can create only one “structural” method. You cannot return a type from it that contains the additional information we need. All structural methods are valid, expressed in bytecode and support reflective access. They simply cannot be expressed using the Java type system. Perhaps this should not surprise anyone, since structural methods are actually implemented using an additional class file that corresponds to an anonymous local type.

Algebraic Data Types

Thanks to generics, Java is a language with parameterized types, which are reference types with type parameters. When they are substituted into some types, others are formed. That is, the resulting types consist of “containers” (generalized types) and “payload” (values ​​of type parameters).

In some languages, supported composite types are very different from Java generics. As an example, tuples are immediately suggested, although sum type, sometimes also called “disjoint union of types” or “tagged union”, is of much greater interest.

The type-sum is an unambiguous type, that is, at each moment of time, variables can have only one value. But at the same time it can be any valid value related to the specified range of various types. This is true even if the disconnected types that are the values ​​are not related in any way from the point of view of inheritance. For example, in the F # language, you can specify the Shape type, the instances of which can be rectangles or circles:

type Shape =
| Circle of int
| Rectangle of int * int

F # is very different from Java, but in Scala these types are implemented with restrictions: sealed types are used with case classes. A sealed class cannot be extended beyond the current compilation unit. This is practically an analogue of the terminal class in Java, but in Scala, the basic compilation unit is a file, and numerous high-level public classes can be declared in a single file.

This leads us to a pattern in which a sealed abstract base class is declared along with several subclasses that correspond to possible disconnected types from the type-sum. The Scala standard library contains many examples of using this pattern, including Option[A]one that is similar to the type Optional Tin Java 8.

In Scala, a disjoint union of two possibilities are Option and Someand None and Option type.

If we implemented the same mechanism in Java, we would encounter a restriction when the compilation unit is, in fact, a class. It doesn't turn out as convenient as in Scala, but you can still come up with solutions. For example, you could use javac to handle the new syntax for the classes we want to seal:

final package example.algebraic;

A similar syntax would mean that the compiler should allow the extension of the class taking into account the final packaging within the current folder, rejecting all other extension attempts. This change could also be implemented using javac, but without checks during execution, it cannot be fully protected from the reflective code. In addition, a Java implementation would be less useful than Scala, since Java lacks advanced match expressions.

Dynamic Call Sites

Starting with version 7, a surprisingly useful tool has appeared in Java: a bytecode invokedynamic, designed to serve as the main calling mechanism. This allows you to execute dynamic languages ​​on top of the JVM, as well as expand the type system in Java by adding built-in methods and changing the interface, while previously this was not possible. Paying for this has a slightly increased complexity. But with skillful handling invokedynamic is a powerful tool.

True, he has one strange limitation. Despite the announced support in Java 7, for some reason, direct access to dynamic call methods is still not provided. Although the whole point of dynamic dispatching is to allow developers to decide for themselves which method to call from a specific point in the call, moreover, the decision can be delayed until the code is executed.

Note: do not confuse this dynamic linking method with a keyword dynamic from C #. In our case, an object is introduced that dynamically determines its bindings during execution; this will not work if the object does not support the requested method calls. Instances of such dynamic objects during execution are indistinguishable from “ordinary” objects, and the mechanism itself is unsafe.

While Java is used to implement lambda expressions and built-in methods invokedynamic, developers do not have direct access and cannot dispatch at run time. In other words, in Java there is no keyword or other design for creating invokedynamic general-purpose dial peers. The javac compiler simply does not translate instructions invokedynamic beyond the framework of the language infrastructure.

You can simply add this functionality to Java. For example, using a keyword or annotation. An additional library and build support are also required.

Glimmers of hope?

The development of language architecture and its implementation is the art of achieving the possible. There are many examples where important changes take their way for a very long time. For example, in C ++, lambda expressions appeared only in version 14.

Many don't like the slow development of Java. But James Gosling adheres to the position that it is impossible to implement functionality until it is fully understood and realized. Although the conservatism of the Java architecture is one of the reasons for the success of this language, at the same time, many eager young developers who are eager for quick changes do not like it. Are any of the above possibilities being implemented? One can cautiously suggest this.

Some of the ideas described can be implemented using the sameinvokedynamic. As you remember, it should fulfill the role of the main call mechanism, postponed until execution. According to the suggestion for improving the JEP276 language , you can standardize the Dynalink library, which was originally created by Attila Szegedi to implement the “meta-object protocol” in the JVM. Later, the author of the library switched to Oracle, which used Dynalink in Nashorn, a JavaScript implementation in the JVM. The library description is on Github, but it is removed from there.

Essentially, Dynalink allows you to talk about object-oriented operations - “get the value of a property”, “assign a value to a property”, “create a new object”, “call a method” - without the need to implement their semantics using the corresponding statically typed, low-level JVM operations.

This binding technology can be used to implement dynamic linkers, whose behavior will differ from the standard. In addition, it can act as a kind of draft for the implementation of new properties of the type system in Java.

Some key Scala developers considered this mechanism as a possible replacement for the implementation of structural types in this language. Although the current version relies on reflection, the appearance of Dynalink on the stage can change everything.

Also popular now: