From the title "Java language from the perspective of Kotlin and C #," I think some people who are familiar with Kotlin and C # would expect something like a report of the results of experiencing Java. Not a sentence. It was written by a person who had played with both Java and C #, inspired by the book "Kotlin In Actin" (and also read one C # book for business reasons). This book was a very interesting book, and I was able to deepen my knowledge of the Java language as well as my knowledge of Kotlin.
I think that this article will be a material for thinking about programming languages, so I will leave it.
Why studying Kotlin and C # gives you insight into Java? That's because both languages are designed with Java as a reference and to make it easier to use. Therefore, if you look at the parts supplemented by both languages, you can see what Java recognized as a weakness.
So what are the weaknesses of Java? There are many, but here I will focus on three things: (1) lack of value type definition function, (2) inheritance of virtual functions, and (3) lack of NULL safety. The three are closely related to each other.
A value type is a type represented by its actual value, and a variable of value type directly holds its value. Value types exist only in basic data types such as boolean, int, and double in Java. The opposite is a reference type, and variables of that type do not hold a direct value, but have that reference as a value. In Java, all types except basic data types are reference types.
In Java, all types other than basic data types, both pre-prepared types and user-defined types, are reference types. That is, the user cannot define the value type.
In contrast, C #, which was created with reference to Java, had a language function called a structure for the user to define a value type from the initial version.
The value type makes it easy to confirm that the two values are the same (called equivalence) and make a copy of them. For example, in the Java basic type, the equality operator (==) can be used to confirm that two values are equal, and the assignment operator (=) can be used to make a copy of the value.
In the case of a reference type, the equality operator checks that the reference values are equal, that is, they refer to the same thing (in this case, they refer to the same thing), and the assignment operator checks the reference value. Make a copy, not a copy of the actual value.
In order to check the equivalence in Java reference type, it is necessary to override the equals method (and hashCode at the same time), and if you need to copy the object, you need to override the clone method or prepare a copy constructor. There is.
The weakness of the lack of value types is that applications have a lot of opportunities to use these two features. For this reason, when creating an application in Java, a large amount of boilerplate (a standard code required by language specifications) can be embedded in the source code (actually, Java's The large number of boilerplates embedded in the source code is largely due to the lack of language features called properties in Kotlin and C #, but it's complicated to talk about, so I'll omit it here).
So why did Java use all types except basic data types as reference types? The reason is to ask the Java author James Gosling, but I can imagine it.
Java is a much touted language as an object-oriented language. When Java was born, the following three functions were listed as the functions that object-oriented languages should have.
Java realizes polymorphism by inheriting and overriding virtual functions, but this is a function that can only be realized by reference type in Java. For this reason, Java, which is oriented toward object-oriented languages, may have tried to eliminate value types as much as possible. Rather, it can be said that using the basic data type as the value type was an unavoidable choice in consideration of performance and other factors.
In addition, Java has the feature that "operators cannot be overloaded", which seems to be related to the feature that user-defined value types cannot be created.
Java author James Gosling says about operator overloading:
Perhaps 20-30% of people consider operator overloading to be the root of all evil. This is marked with a big cross because someone somewhere used an operator overload, for example assigning a "+" to insert a list, which messed up life terribly. It must have been. Many of the problems are that there are at most half a dozen operators that can be overloaded in a sensible way, but there are thousands or millions of operators that you'll want to define. I have to choose, but the choice itself is against my intuition.
That is, James Gosling hates operator overloading.
However, if Java had adopted user-defined value types as language features, it would not have been possible to choose not to adopt operator overloading. At least it should have been noticeable as a language flaw.
For example, one of the typical value types is the complex number type. If this could be defined, it would be a linguistic flaw that the plus and minus operators for this type could not be defined. But if it can only be defined as a reference type, even if the operator cannot be overloaded, it will not be noticeable as a linguistic flaw.
By eliminating the function of defining value types, it is possible to omit the overload function of operators that I hate. I think this was very convenient for Java.
As mentioned earlier, Java is a language that has been widely touted as an object-oriented language, and the language is designed to actively support polymorphism by inheriting and overriding virtual functions. That is, if nothing is specified, the class becomes an inheritable class, and the function defined in the class becomes an overrideable virtual function.
So what's wrong with this language specification?
In fact, there is a big problem. Virtual functions cause a now-known problem called the "fragile base class". For this reason, it is now recommended to refrain from using it.
The "vulnerable base class problem" is the problem that when the code of a base class is changed, the change is no longer what the subclass expects, causing malicious behavior in the subclass. (For details, please refer to https://en.wikipedia.org/wiki/Fragile_base_class etc.)
To avoid this problem, the well-known Java masterpiece, Effective Java, recommends a method of "designing and documenting for inheritance, or prohibiting inheritance." That is, when using polymorphism by inheritance, it is recommended to publish the implementation contents of the base class as a document (otherwise, inheritance is not used in the first place). This means abandoning "information hiding," which is said to be an important object-oriented element.
There is a language flaw that stands out because all types other than the basic type are reference types. That is the lack of null safety.
A Java reference type is a type that references a value, but can have a value (NUL) that does not reference anything. At this time, assuming that a normal value is referenced, if a member (method or field) of that type is referenced, an exception called NullPointerException (so-called nullpo) will occur. To put it simply, NULL safety is a mechanism that does not generate this NullPointerException.
The fact that Java is not null-safe is a linguistic flaw because Java is a type-safe language (for example, C is not a type-safe language, and null references are also a problem. Not done).
Type safety is a mechanism to prevent illegal operations on types by detecting compilation errors. For example, if the type of a variable is of type String, Java will cause a compile error if you try to perform any operation on that variable other than that allowed for type String. Null references are the only exception to Java's type safety.
The problem with lack of null safety in Java is that Java is a very widespread language and many programmers have been plagued by this problem, but all but the basic data types are reference types. Is also pushing this.
To add to the honor of Java, I don't think there was a word null safe when Java was born. At least it wasn't a commonly known feature. Therefore, it can be said that Java is not null-safe.
So how is Java trying to overcome these weaknesses? Also, let's see how Kotlin and C # are taking measures against them.
In Java, all user-defined types are reference types, so it is not possible to define a value type strictly, but it is possible to define a type that behaves like a value. In other words, it is a type that allows equivalence comparison with the equals method and makes it easy to make a copy. However, in this case, many boilerplates will have to be added to the source, as mentioned above.
The first solution to this problem is the automatic generation of boilerplates by the IDE. However, while this reduces the burden of creating the source, it does not reduce the burden of reviewing it later.
That's where the library called Lombok came out. If you install this library, you can just add Data annotation to the class and it will automatically generate boilerplates such as equals in a place that is not visible from the source code.
In addition, Java 14 introduced a new mechanism called Recod, which further complemented Java's weakness of having no value type.
Kotlin, like Java, is a language designed to run on the JVM. Therefore, all types other than the basic data type have the property of being reference types, and value types cannot be defined. However, it is possible to define a class that behaves like a value type, just like Java.
Just by adding the modifier data to the class, methods such as equals, hashCode, toString, and copy are automatically generated from the beginning (these methods are not visible from the source).
As mentioned earlier, C # has a mechanism for defining a value type called a structure.
Furthermore, a type that can be used like a literal called an anonymous type can also be used like a value type because the Equals function for equivalence comparison is automatically assigned.
From C # 7.0, a type called tuple can be used. This is also a type that is easy to compare and copy equivalence like the value type.
If you try to inherit a class with the final modifier or override a method with the same modifier, you will get a compile error. This is a feature that Java has had from the beginning. Basically, you can avoid the "vulnerable base class" problem by adding final to all classes and methods.
With the annotation introduced in Java 5.0, it is now recommended to add the Override annotation when overriding a method. This is a feature added to avoid the mistake of unintentionally creating a new method that does not override, but it has the effect of making overlaying a bit more cumbersome.
The "Effective Java" introduced earlier recommends the method of "choosing composition over inheritance." Instead of inheriting an existing class, this gives the new class a private field that references an instance of the existing class, and each method in the new class calls the corresponding method in the existing class it holds. The method is to return the result (this is called delegation). This avoids the "vulnerable base class" problem. Also, most IDEs have features that support this method. However, this method produces a large number of boilerplates.
Kotlin treats classes as final by default. That is, a class without any qualifiers cannot create a derived class. The open modifier must be added to allow inheritance.
Similarly, the method is treated as final by default. That is, methods without any qualifiers cannot be overridden. The open modifier must be added to allow overrides. Also, when overriding the method in a derived class, you must add the override modifier (the Override annotation of the corresponding function in Java is optional).
Kotlin has an active support for the "choose composition over inheritance" method. The by keyword makes it easy to delegate methods inherited from an interface to another class.
Kotlin has a mechanism called extension functions as a way to extend the functionality of classes without inheritance. It's not a replacement for inheritance, and it's more restrictive than a regular method definition, but it's a powerful way to add methods to an existing class later.
In C #, you can prohibit inheritance of a class by adding the sealed modifier to the class. However, unlike Kotlin, this is not the default.
Like Kotlin, methods are not overrideable by default. To be overridable, you must add the virtual modifier, and when you override the method in a derived class, you must add the override modifier.
There is no class delegation mechanism like Kotlin. When doing class delegation, you have to write a boilerplate just like in Java.
Similar to Kotlin, there is a mechanism called extension method as a method to add a method to an existing class. However, since this function was added in C # 3.0, Kotlin used it as a reference to incorporate a mechanism called an extension function into the language specification.
If you install a library such as JSR305 or Lombok introduced earlier, you can use Nullable annotation and NonNull annotation. These help detect NullPointerException (specifications vary from library to library).
Starting with Java8, the Optional class can be used as a wrapper class for potentially null reference types. The methods of this class distinguish between Null and other cases, which can reduce the occurrence of NullPointerException.
Kotlin's type system distinguishes between nullable types (potentially null types) and non-nullable types (types that do not allow nulls), making it easier to avoid NullPointerExceition. If you declare a type (such as a String) in Kotlin using the type name as is, it will be a nullable type. A question mark (such as String?) Must be added after the type name to make it a nullable type.
The null-free type does not have null, so it does not generate a NullPointerExceition. In addition, Kotlin provides the following syntax for nullable types to make it difficult for NullPointerExceition to occur.
Starting with C # 8.0 released in September 2019, nullable reference types are available. Like Kotlin, this is a feature that allows nulls in the return value of a variable or function only when you put a question mark after the type name. The difference with Kotlin is that you can only use nullable reference types when you enable the nullable annotation context for compatibility with previous versions.
Each C # has a different introduction time, but there are the following operators that consider NULL.
Stroustrup, the designer of the C ++ language, stated in his book "Design and Evolution of C ++":
"As I predicted, Java has acquired new features over the years, and as a result has negated the" original advantage "of simplicity, but it didn't improve performance. Languages always sell "simplicity" and then survive, increasing in size and complexity for real-world applications. "
I think that's exactly the case. Java and C # are now complex enough languages, and Kotlin, which is still in its infancy, will soon grow into a complex language. That's what a living language is.