📜 ⬆️ ⬇️

10 things you didn't know about java

So, have you been working on Java since its inception? Do you remember the days when it was called “Oak”, when OOP was spoken at every corner, when the supporters thought that Java had no chance, and applets were considered cool?

I bet you didn't know at least half of what I was going to tell you. Let's discover some amazing facts about the internal features of Java.

1. There are no checked (checked) exceptions.


Yes Yes! The JVM knows nothing about them, only Java knows.

Today, anybody agrees that verifiable exceptions were a bad idea. As Bruce Ekkel said in his final speech at GeeCON in Prague , not a single language after Java was associated with checked exceptions, and even in the new Streams API in Java 8 they were abandoned ( which can cause difficulties when your lambda uses I / O or data ).
')
Want to make sure the JVM doesn't know anything about them? Run this code:

public class Test { //  throws:    public static void main(String[] args) { doThrow(new SQLException()); } static void doThrow(Exception e) { Test.<RuntimeException> doThrow0(e); } @SuppressWarnings("unchecked") static <E extends Exception> void doThrow0(Exception e) throws E { throw (E) e; } } 

This program is not only compiled, but in fact throws a SQLException . You don't even need @SneakyThrows from Lombok.

You can read more about this here , or here on Stack Overflow .

2. You can create two methods that differ only in the return type.


Such code will not compile, right?

 class Test { Object x() { return "abc"; } String x() { return "123"; } } 

Right. The Java language does not allow two “equivalently overloaded” methods in one class, even if they differ in return type or declared exceptions.

But wait a minute. Let's read the documentation for Class.getMethod (String, Class ...) . It is written there:
Note that a class may contain several suitable methods, because although the Java language does not allow declaring several methods with the same signature, the Java virtual machine still allows this if the return type is different. This flexibility of the virtual machine can be used to implement some of the language features. For example, a covariant return type can be implemented using a bridge method, which differs from the actual overloaded method only by the return type.
Oh how! Yes, it sounds reasonable. In fact, this will happen if you write:

 abstract class Parent<T> { abstract T x(); } class Child extends Parent<String> { @Override String x() { return "abc"; } } 

Here such a bytecode will be generated for the Child class:

 // Method descriptor #15 ()Ljava/lang/String; // Stack: 1, Locals: 1 java.lang.String x(); 0 ldc <String "abc"> [16] 2 areturn Line numbers: [pc: 0, line: 7] Local variable table: [pc: 0, pc: 3] local: this index: 0 type: Child // Method descriptor #18 ()Ljava/lang/Object; // Stack: 1, Locals: 1 bridge synthetic java.lang.Object x(); 0 aload_0 [this] 1 invokevirtual Child.x() : java.lang.String [19] 4 areturn Line numbers: [pc: 0, line: 1] 

It is clear that generic type T in bytecode simply turns into Object . The synthetic bridge method is generated by the compiler, because in some places where the method is invoked, Object can be expected as the return type of Parent.x() . It would be difficult to add generic types without bridge methods and to provide binary compatibility. It turned out to be a lesser evil to modify the JVM so that it would support this possibility (and covariant return types appeared as a side effect). Clever, yes?

Interested in overloading by return type? Read this discussion on stack overflow.

3. These are all two-dimensional arrays!


 class Test { int[][] a() { return new int[0][]; } int[] b() [] { return new int[0][]; } int c() [][] { return new int[0][]; } } 

Yes it's true. The return type of these methods is the same, even if the parser in your head did not immediately understand it! And here is a similar piece of code:

 class Test { int[][] a = {{}}; int[] b[] = {{}}; int c[][] = {{}}; } 

Say madness? And if you add to this annotation types of Java 8 ? The number of options increases at times!

 @Target(ElementType.TYPE_USE) @interface Crazy {} class Test { @Crazy int[][] a1 = {{}}; int @Crazy [][] a2 = {{}}; int[] @Crazy [] a3 = {{}}; @Crazy int[] b1[] = {{}}; int @Crazy [] b2[] = {{}}; int[] b3 @Crazy [] = {{}}; @Crazy int c1[][] = {{}}; int c2 @Crazy [][] = {{}}; int c3[] @Crazy [] = {{}}; } 

Type annotations. Mysterious and powerful mechanism . Steeper than its mysteriousness except its power .

Or in other words:

My last commit before the monthly leave
My last commit before the monthly leave

Find the real scenario of using these constructs, I leave to you as an exercise.

4. You do not understand the conditional constructions.


It seems to you that you know all about conditional expressions? I will disappoint you. Most programmers believe that the following code fragments are equivalent:

 Object o1 = true ? new Integer(1) : new Double(2.0); 

Is this the same thing?

 Object o2; if (true) o2 = new Integer(1); else o2 = new Double(2.0); 

And no. Let's check:

 System.out.println(o1); System.out.println(o2); 

The program will issue the following:

 1.0 1 

Aha The conditional operator performs the casting of numerical types when “necessary” , and “necessary” in very fat quotes. After all, you do not expect that this program will throw a NullPointerException ?

 Integer i = new Integer(1); if (i.equals(1)) i = null; Double d = new Double(2.0); Object o = true ? i : d; // NullPointerException! System.out.println(o); 

More details on this topic here .

5. Composite assignment operator, you also do not understand


Do not believe? Consider two lines of code:

 i += j; i = i + j; 

Intuitively, they should be equivalent, right? Surprise! They differ. As stated in the JLS :

The compound assignment operator of the form E1 op = E2 is equivalent to the expression E1 = (T) ((E1) op (E2)), where T is the type of E1, except that E1 is calculated only once.

This is so beautiful that I would like to quote Peter Laurie ’s response to Stack Overflow :

An example of such a type conversion can be shown on * = or / =

  byte b = 10;
 b * = 5.7;
 System.out.println (b);  // displays 57 

or

  byte b = 100;
 b = 2.5;
 System.out.println (b);  // displays 40 

or

  char ch = '0';
 ch * = 1.1;
 System.out.println (ch);  // displays '4' 

or

  char ch = 'A';
 ch * = 1.5;
 System.out.println (ch);  // will print 'a' 

See what a useful feature? Now I will multiply my characters with automatic type casting. Because, you know ...

6. Random integers


It is rather a mystery. Do not peek into the solution, try it yourself. When I run this code:

 for (int i = 0; i < 10; i++) { System.out.println((Integer) i); } 

“In some cases” I get the following result:

 92 221 45 48 236 183 39 193 33 84 

How is this possible ??

Solution
The answer is given here and consists in rewriting the JDK integer cache using reflection and in using autoboxing. Do not try this at home! Well, or remember the picture above about the last commit before the release.

7. GOTO


But my favorite. Java has GOTO! Try:

 int goto = 1; 

And you will get:

 Test.java:44: error: <identifier> expected int goto = 1; ^ 

That's because goto is an unused keyword . Just in case, when it comes in handy.

But this is not the most interesting. Most impressive is that you can actually implement goto with the help of break , continue and blocks with labels:

Jump forward:

 label: { // ... - ... if (check) break label; // ... - ... } 

In bytecode:

 2 iload_1 [check] 3 ifeq 6 //   6 .. 

Jump back

 label: do { // ... - ... if (check) continue label; // ... - ... break label; } while(true); 

In bytecode:

 2 iload_1 [check] 3 ifeq 9 6 goto 2 //   9 .. 


8. Java has type aliases.


In other languages, for example, in ( Ceylon ), we can easily declare an alias for the type:

 interface People => Set<Person>; 

The People type is created in such a way that it can be used anywhere instead of Set<Person> :

 People? p1 = null; Set<Person>? p2 = p1; People? p3 = p2; 

In Java, we cannot declare type aliases globally. But it is possible to do this within a class or method. Suppose we do not like long names Integer , Long , etc., we want to use short ones instead: I and L Easy:

 class Test<I extends Integer> { <L extends Long> void x(I i, L l) { System.out.println( i.intValue() + ", " + l.longValue() ); } } 

In this code, I is the alias for Integer within the Test class, and L is the alias for Long within the x() method. We can safely call this method:

 new Test().x(1, 2L); 

Of course, this technique can not be taken seriously. In our case, the types Integer and Long declared final, and this means that generic types I and L effectively aliases (well, almost: compatibility with assignment works only in one direction). If we used types that were not declared final (for example, Object ), then these would be ordinary generics.

Enough foolish tricks. The time has come for something more serious!

9. Some relationships between types are not computable!


Okay, now it will be really cool, so pour yourself some coffee and concentrate. Consider the following types:

 //  .     List interface Type<T> {} class C implements Type<Type<? super C>> {} class D<P> implements Type<Type<? super D<D<P>>>> {} 

What do C and D types really mean?

They are in some sense recursive and somewhat similar (though not completely) to the type declaration java.lang.Enum . See:

 public abstract class Enum<E extends Enum<E>> { ... } 

In fact, the enum declaration is syntactic sugar:

 //  enum MyEnum {} //       class MyEnum extends Enum<MyEnum> { ... } 

Remember this and return to our original types. Does this code compile?

 class Test { Type<? super C> c = new C(); Type<? super D<Byte>> d = new D<Byte>(); } 

The question is complex and Ross Tate has the answer. It is generally impossible to determine this:

Is C a subtype of Type <? super C>?
Step 0) C is the Type<? super C> Type<? super C>
Step 1) Type<Type<? super C>> Type<Type<? super C>> is Type<? super C> Type<? super C> (inheritance)
Step 2) Is C a Type<? super C> Type<? super C> (check mask? super C)
Step ... (endless loop)

If with C we just get hung up, then with D is even more fun:
Is D <Byte> Type <subtype? super D <Byte >>?
Step 0) D<Byte> is Type<? super D<Byte>> Type<? super D<Byte>>
Step 1) Type<Type<? super D<D<Byte>>>> Type<Type<? super D<D<Byte>>>> is Type<? super D<Byte>> Type<? super D<Byte>>
Step 2) D<Byte> is Type<? super D<D<Byte>>> Type<? super D<D<Byte>>>
Step 3) Type<Type<? super D<D<Byte>>>> Type<Type<? super D<D<Byte>>>> is Type<? super D<D<Byte>>> Type<? super D<D<Byte>>>
Step 4) D<D<Byte>> Is Type<? super D<D<Byte>>> Type<? super D<D<Byte>>>
Step ... (endless growth)

Try compiling this into Eclipse, and it will crash with a stack overflow! ( do not worry, I have already reported to the bugtracker )
We'll have to accept:
Some relationships between types are not computable !

If you are interested in generic-type problems, read Ross Tate’s article “ Taming Patterns in the Java Type System ” (in collaboration with Alan Löng and Sorin Lerner), or our thoughts on the topic.

10. Type Intersection


In the Java language there is a very peculiar thing - the intersection of types. You can declare a generic type that is the intersection of two types. For example:

 class Test<T extends Serializable & Cloneable> { } 

The type to which T corresponds to specific instances of the class Test must implement both the Serializable and Cloneable . For example, the String will not work, but the Date is fine:

 //   Test<String> s = null; //  Test<Date> d = null; 

This feature has been developed in Java 8, where you can convert a type to an intersection. Where can it come in handy? Almost nowhere, but if you need to bring a lambda expression to this type, you have no other options. Suppose your method has such an insane type restriction:

 <T extends Runnable & Serializable> void execute(T t) {} 

You are only satisfied with Runnable , which is also Serializable in case you want to transfer it over the network and run somewhere else. In principle, lambda can be serialized:
You can serialize a lambda expression if its target type and its captured arguments are serializable.
But even if it is observed, the lambda will not automatically implement the Serializable marker interface. You will need a cast. But if you only lead to Serializable :

 execute((Serializable) (() -> {})); 

then the lambda will no longer be Runnable .

Eh ...

It remains ...

Lead to two types at once:

 execute((Runnable & Serializable) (() -> {})); 

Conclusion


I usually say this only about SQL, but it's time to finish the article like this:
Java is a mysterious and powerful mechanism. Steeper than its mysteriousness except its power.

Source: https://habr.com/ru/post/253787/


All Articles