Search icon CANCEL
Subscription
0
Cart icon
Your Cart (0 item)
Close icon
You have no products in your basket yet
Arrow left icon
Explore Products
Best Sellers
New Releases
Books
Videos
Audiobooks
Learning Hub
Conferences
Free Learning
Arrow right icon
Arrow up icon
GO TO TOP
Java Coding Problems

You're reading from   Java Coding Problems Become an expert Java programmer by solving over 250 brand-new, modern, real-world problems

Arrow left icon
Product type Paperback
Published in Mar 2024
Publisher Packt
ISBN-13 9781837633944
Length 798 pages
Edition 2nd Edition
Languages
Arrow right icon
Author (1):
Arrow left icon
Anghel Leonard Anghel Leonard
Author Profile Icon Anghel Leonard
Anghel Leonard
Arrow right icon
View More author details
Toc

Table of Contents (16) Chapters Close

Preface 1. Text Blocks, Locales, Numbers, and Math 2. Objects, Immutability, Switch Expressions, and Pattern Matching FREE CHAPTER 3. Working with Date and Time 4. Records and Record Patterns 5. Arrays, Collections, and Data Structures 6. Java I/O: Context-Specific Deserialization Filters 7. Foreign (Function) Memory API 8. Sealed and Hidden Classes 9. Functional Style Programming – Extending APIs 10. Concurrency – Virtual Threads and Structured Concurrency 11. Concurrency ‒ Virtual Threads and Structured Concurrency: Diving Deeper 12. Garbage Collectors and Dynamic CDS Archives 13. Socket API and Simple Web Server 14. Other Books You May Enjoy
15. Index

47. Exemplify erasure vs. overloading

Before we join them in an example, let’s quickly tackle erasure and overloading separately.

Erasure in a nutshell

Java uses type erasure at compile time in order to enforce type constraints and backward compatibility with old bytecode. Basically, at compilation time, all type arguments are replaced by Object (any generic must be convertible to Object) or type bounds (extends or super). Next, at runtime, the type erased by the compiler will be replaced by our type. A common case of type erasure implies generics.

Erasure of generic types

Practically, the compiler erases the unbound types (such as E, T, U, and so on) with the bounded Object. This enforces type safety, as in the following example of class type erasure:

public class ImmutableStack<E> implements Stack<E> {
  private final E head;
  private final Stack<E> tail;
  ...

The compiler applies type erasure to replace E with Object:

public class ImmutableStack<Object> implements Stack<Object> {
  private final Object head;
  private final Stack<Object> tail;
  ...

If the E parameter is bound, then the compiler uses the first bound class. For instance, in a class such as class Node<T extends Comparable<T>> {...}, the compiler will replace T with Comparable. In the same manner, in a class such as class Computation<T extends Number> {...}, all occurrences of T would be replaced by the compiler with the upper bound Number.

Check out the following case, which is a classical case of method type erasure:

public static <T, R extends T> List<T> listOf(T t, R r) {
  List<T> list = new ArrayList<>();
  list.add(t);
  list.add(r);
  return list;
}
// use this method
List<Object> list = listOf(1, "one");

How does this work? When we call listOf(1, "one"), we are actually passing two different types to the generic parameters T and R. The compiler type erasure has replaced T with Object. In this way, we can insert different types in the ArrayList and the code works just fine.

Erasure and bridge methods

Bridge methods are created by the compiler to cover corner cases. Specifically, when the compiler encounters an implementation of a parameterized interface or an extension of a parameterized class, it may need to generate a bridge method (also known as a synthetic method) as part of the type erasure phase. For instance, let’s consider the following parameterized class:

public class Puzzle<E> {
  public E piece;
  public Puzzle(E piece) {
    this.piece = piece;
  }
  public void setPiece(E piece) { 
    this.piece = piece;
  }
}

And, an extension of this class:

public class FunPuzzle extends Puzzle<String> {
  public FunPuzzle(String piece) {
    super(piece);
  }
  @Override
  public void setPiece(String piece) { 
    super.setPiece(piece);
  }
}

Type erasure modifies Puzzle.setPiece(E) as Puzzle.setPiece(Object). This means that the FunPuzzle.setPiece(String) method does not override the Puzzle.setPiece(Object) method. Since the signatures of the methods are not compatible, the compiler must accommodate the polymorphism of generic types via a bridge (synthetic) method meant to guarantee that sub-typing works as expected. Let’s highlight this method in the code:

/* Decompiler 8ms, total 3470ms, lines 18 */
package modern.challenge;
public class FunPuzzle extends Puzzle<String> {
   public FunPuzzle(String piece) {
      super(piece);
   }
   public void setPiece(String piece) {
      super.setPiece(piece);
   }
   // $FF: synthetic method
   // $FF: bridge method
   public void setPiece(Object var1) {
      this.setPiece((String)var1);
   }
}

Now, whenever you see a bridge method in the stack trace, you will know what it is and why it is there.

Type erasure and heap pollution

Have you ever seen an unchecked warning? I’m sure you have! It’s one of those things that is common to all Java developers. They may occur at compile-time as the result of type checking, or at runtime as a result of a cast or method call. In both cases, we talk about the fact that the compiler cannot validate the correctness of an operation, which implies some parameterized types. Not every unchecked warning is dangerous, but there are cases when we have to consider and deal with them.

A particular case is represented by heap pollution. If a parameterized variable of a certain type points to an object that is not of that type, then we are prone to deal with a code that leads to heap pollution. A good candidate for such scenarios involves methods with varargs arguments.

Check out this code:

public static <T> void listOf(List<T> list, T... ts) {
  list.addAll(Arrays.asList(ts));    
}

The listOf() declaration will cause this warning: Possible heap pollution from parameterized vararg type T. So, what’s happening here?

The story begins when the compiler replaces the formal T... parameter into an array. After applying type erasure, the T... parameter becomes T[], and finally Object[]. Consequently, we opened a gate to possible heap pollution. But, our code just added the elements of Object[] into a List<Object>, so we are in the safe area.

In other words, if you know that the body of the varargs method is not prone to generate a specific exception (for example, ClassCastException) or to use the varargs parameter in an improper operation, then we can instruct the compiler to suppress these warnings. We can do it via the @SafeVarargs annotation as follows:

@SafeVarargs
public static <T> void listOf(List<T> list, T... ts) {...}

The @SafeVarargs is a hint that sustains that the annotated method will use the varargs formal parameter only in proper operations. More common, but less recommended, is to use @SuppressWarnings({"unchecked", "varargs"}), which simply suppresses such warnings without claiming that the varargs formal parameter is not used in improper operations.

Now, let’s tackle this code:

public static void main(String[] args) {
  List<Integer> ints = new ArrayList<>();
  Main.listOf(ints, 1, 2, 3);
  Main.listsOfYeak(ints);
}
public static void listsOfYeak(List<Integer>... lists) {
  Object[] listsAsArray = lists;     
  listsAsArray[0] = Arrays.asList(4, 5, 6); 
  Integer someInt = lists[0].get(0);   
  listsAsArray[0] = Arrays.asList("a", "b", "c"); 
  Integer someIntYeak = lists[0].get(0); // ClassCastException
}

This time, the type erasure transforms the List<Integer>... into List[], which is a subtype of Object[]. This allows us to do the assignment: Object[] listsAsArray = lists;. But, check out the last two lines of code where we create a List<String> and store it in listsAsArray[0]. In the last line, we try to access the first Integer from lists[0], which obviously leads to a ClassCastException. This is an improper operation of using varargs, so it is not advisable to use @SafeVarargs in this case. We should have taken the following warnings seriously:

// unchecked generic array creation for varargs parameter 
// of type java.util.List<java.lang.Integer>[]
Main.listsOfYeak(ints);
// Possible heap pollution from parameterized vararg
// type java.util.List<java.lang.Integer>
public static void listsOfYeak(List<Integer>... lists) { ... }

Now, that you are familiar with type erasure, let’s briefly cover polymorphic overloading.

Polymorphic overloading in a nutshell

Since overloading (also known as “ad hoc” polymorphism) is a core concept of Object-Oriented Programming (OOP), I’m sure you are familiar with Java method overloading, so I’ll not insist on the basic theory of this concept.

Also, I’m aware that some people don’t agree that overloading can be a form of polymorphism, but that is another topic that we will not tackle here.

We will be more practical and jump into a suite of quizzes meant to highlight some interesting aspects of overloading. More precisely, we will discuss type dominance. So, let’s tackle the first quiz (wordie is an initially empty string):

static void kaboom(byte b) { wordie += "a";}   
static void kaboom(short s) { wordie += "b";}   
kaboom(1);

What will happen? If you answered that the compiler will point out that there is no suitable method found for kaboom(1), then you’re right. The compiler looks for a method that gets an integer argument, kaboom(int). Okay, that was easy! Here is the next one:

static void kaboom(byte b) { wordie += "a";}   
static void kaboom(short s) { wordie += "b";}  
static void kaboom(long l) { wordie += "d";}   
static void kaboom(Integer i) { wordie += "i";}   
kaboom(1);

We know that the first two kaboom() instances are useless. How about kaboom(long) and kaboom(Integer)? You are right, kaboom(long) will be called. If we remove kaboom(long), then kaboom(Integer) is called.

Important note

In primitive overloading, the compiler starts by searching for a one-to-one match. If this attempt fails, then the compiler searches for an overloading flavor taking a primitive broader domain than the primitive current domain (for instance, for an int, it looks for int, long, float, or double). If this fails as well, then the compiler checks for overloading taking boxed types (Integer, Float, and so on).

Following the previous statements, let’s have this one:

static void kaboom(Integer i) { wordie += "i";} 
static void kaboom(Long l) { wordie += "j";} 
kaboom(1);

This time, wordie will be i. The kaboom(Integer) is called since there is no kaboom(int/long/float/double). If we had a kaboom(double), then that method has higher precedence than kaboom(Integer). Interesting, right?! On the other hand, if we remove kaboom(Integer), then don’t expect that kaboom(Long) will be called. Any other kaboom(boxed type) with a broader/narrow domain than Integer will not be called. This is happening because the compiler follows the inheritance path based on an IS-A relationship, so after kaboom(Integer), it looks for kaboom(Number), since Integer is a Number.

Important note

In boxed type overloading, the compiler starts by searching for a one-to-one match. If this attempt fails, then the compiler will not consider any overloading flavor taking a boxed type with a broader domain than the current domain (of course, a narrow domain is ignored as well). It looks for Number as being the superclass of all boxed types. If Number is not found, the compiler goes up in the hierarchy until it reaches the java.lang.Object, which is the end of the road.

Okay, let’s complicate things a little bit:

static void kaboom(Object... ov) { wordie += "o";}   
static void kaboom(Number n) { wordie += "p";}   
static void kaboom(Number... nv) { wordie += "q";}  
kaboom(1);

So, which method will be called this time? I know, you think kaboom(Number), right? At least, my simple logic pushes me to think that this is a common-sense choice. And it is correct!

If we remove kaboom(Number), then the compiler will call the varargs method, kaboom(Number...). This makes sense since kaboom(1) uses a single argument, so kaboom(Number) should have higher precedence than kaboom(Number...). This logic reverses if we call kaboom(1,2,3) since kaboom(Number) is no longer representing a valid overloading for this call, and kaboom(Number...) is the right choice.

But, this logic applies because Number is the superclass of all boxed classes (Integer, Double, Float, and so on).

How about now?

static void kaboom(Object... ov) { wordie += "o";}   
static void kaboom(File... fv) { wordie += "s";}   
kaboom(1);

This time, the compiler will “bypass” kaboom(File...) and will call kaboom(Object...). Based on the same logic, a call of kaboom(1, 2, 3) will call kaboom(Object...) since there is no kaboom(Number...).

Important note

In overloading, if the call has a single argument, then the method with a single argument has higher precedence than its varargs counterpart. On the other hand, if the call has more arguments of the same type, then the varargs method is called since the one-argument method is not suitable anymore. When the call has a single argument but only the varargs overloading is available, then this method is called.

This leads us to the following example:

static void kaboom(Number... nv) { wordie += "q";}   
static void kaboom(File... fv) { wordie += "s";}   
kaboom();

This time, kaboom() has no arguments and the compiler cannot find a unique match. This means that the reference to kaboom() is ambiguous since both methods match (kaboom(java.lang.Number...) in modern.challenge.Main and method kaboom(java.io.File...) in modern.challenge.Main).

In the bundled code, you can play even more with polymorphic overloading and test your knowledge. Moreover, try to challenge yourself and introduce generics in the equation as well.

Erasure vs. overloading

Okay, based on the previous experience, check out this code:

void print(List<A> listOfA) {
  System.out.println("Printing A: " + listOfA);
}
void print(List<B> listofB) {
  System.out.println("Printing B: " + listofB);
}

What will happen? Well, this is a case where overloading and type erasure collide. The type erasure will replace List<A> with List<Object> and List<B> with List<Object> as well. So, overloading is not possible and we get an error such as name clash: print(java.util.List<modern.challenge.B>) and print (java.util.List<modern.challenge.A>) have the same erasure.

In order to solve this issue, we can add a dummy argument to one of these two methods:

void print(List<A> listOfA, Void... v) {
  System.out.println("Printing A: " + listOfA);
}

Now, we can have the same call for both methods:

new Main().print(List.of(new A(), new A()));
new Main().print(List.of(new B(), new B()));

Done! You can practice these examples in the bundled code.

You have been reading a chapter from
Java Coding Problems - Second Edition
Published in: Mar 2024
Publisher: Packt
ISBN-13: 9781837633944
Register for a free Packt account to unlock a world of extra content!
A free Packt account unlocks extra newsletters, articles, discounted offers, and much more. Start advancing your knowledge today.
Unlock this book and the full library FREE for 7 days
Get unlimited access to 7000+ expert-authored eBooks and videos courses covering every tech area you can think of
Renews at $19.99/month. Cancel anytime
Banner background image