Skip to content

Instantly share code, notes, and snippets.

@Scrappers-glitch
Last active April 8, 2023 09:18
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save Scrappers-glitch/2fe95035aab7f116e868fb01a2f1305e to your computer and use it in GitHub Desktop.
Save Scrappers-glitch/2fe95035aab7f116e868fb01a2f1305e to your computer and use it in GitHub Desktop.
Generics in java
public class Wrapper {
private Object object;
public Wrapper(Object object) {
this.object = object;
}
public void setObject(Object object) {
this.object = object;
}
public Object getObject() {
return object;
}
}
@Scrappers-glitch
Copy link
Author

Scrappers-glitch commented Apr 8, 2023

Problems with the above syntax

Problems with this code: 1) Runtime type-casting unsafety. 2) Hard time reading on large code scales.
// type l-value = r-value, where r-value > l-value
// Object str = new String("Hello") -> OK!
final Wrapper wrapper = new Wrapper(new String("Hello")); 

// type l-value = r-value, where r-value < l-value
// Type incompatibility -> Compile-Time error
final String text = wrapper.getObject();

// To overcome this error, casting is a must!
if (wrapper.getObject() instanceof String) {
    final String text = (String) wrapper.getObject();
}

// A ClassCastException CCE is fired!
final Integer age = (Integer) wrapper.getObject();

@Scrappers-glitch
Copy link
Author

Scrappers-glitch commented Apr 8, 2023

Refactoring

Refactoring code to use Class Generics:

public class Wrapper<T> {
     private T object;
     
     public Wrapper(T object) {
         this.object = object;
     }

     public void setObject(T object) {
         this.object = object;
     }

     public T getObject() {
         return object;
     }
}

Now to utilize:

final Wrapper<String> text = new Wrapper<>("Hello");

// no casting and eventually no CCEs at Runtime anymore!
final String text = wrapper.getObject();

// Compile-time error due to type incompatibilities and so this avoids runtime CCEs too!
final Integer age = wrapper.getObject();

@Scrappers-glitch
Copy link
Author

Upper Bounds V.S. Lower Bounds AND Compile-time Type-Erasures

Generic-Type Upper bounds restrictions, this is an example that restricts the Class-Generic types to Numbers only, all objects extending Numbers are permitted:

public class Wrapper<T extends Number> { /* LEGAL! The compiler finds Type-Erasure for this, Number */
     private T object;
     
     public Wrapper(T object) {
         this.object = object;
     }

     public void setObject(T object) {
         this.object = object;
     }

     public T getObject() {
         return object;
     }
}

Generic-Type Upper bounds restrictions, this is an example that restricts the Class-Generic types to Numbers only, all superclasses of FileOutputStream are permitted, including AutoCloseable and Closeable down to the class hierarchy to the FileOutputStream:

public class Wrapper<T super FileOutputStream> { /* ILLEGEAL! Because the compiler cannot find a Type-Erasure for this! */
     private T object;
     
     public Wrapper(T object) {
         this.object = object;
     }

     public void setObject(T object) {
         this.object = object;
     }

     public T getObject() {
         return object;
     }
}

@Scrappers-glitch
Copy link
Author

Scrappers-glitch commented Apr 8, 2023

Upper Bounded Wildcards V.S. Lower Bounded Wildcards

The (?) is called the wildcard operator, it provides the compiler with the ability to capture type compatibilities and replace them at compile-time.
The wildcard is never used as a type argument for a generic method invocation, a generic class instance creation, or a supertype.

Upper bounded wildcard:

public static <T extends Number> void addAll(List<? extends T> list) {
   // add all items from a list
}

final List<Integer> integers =  Arrays.asList(1, 2, 3, 4);
addAll(integers); // OK!

final List<Double> doubles =  Arrays.asList(1.2d, 2.3d, 3.3d, 4.5d);
addAll(doubles); // OK!

final List<String> strs =  Arrays.asList("Hello", "world!");
addAll(strs); //compile-time error, out of lower bounds (out of sub-types)!
/*
 addAll(strs);
^
  required: List<? extends T>
  found: List<String>
  reason: inference variable T has incompatible bounds
    lower bounds: Number
    lower bounds: String
  where T is a type-variable:
    T extends Number declared in method <T>addAll(List<? extends T>)
1 error
*/

Lower bounded wildcard:

// Example from Lemur-API

public static <S extends Button> void addCommand(Command<? super S> command) {
   // add all items from a list
}
// recall, Button extends Label extends Box extends Panel
// and Card extends Button
// and so, the compatible upper bounds (super-interfaces) are [Button], [Label], [Box] and [Panel]
// and the incompatible upper bounds (super-interfaces) are [Card] class in this case and its subclasses.

final Panel card = new Card(); // OK! A Card is a Panel.
addCommand(new Command<Panel>() { // Within bounds!
     @Override 
     public Panel onClick() {
            // command-state execution with the source in return 
            return card; // Panel = new Card, GOOD!
     }
});

final Button click = new Button(); // OK! 
addCommand(new Command<Button>() { // Within bounds!
     @Override 
     public Button onClick() {
            // command-state execution with the source in return 
            return click; // Button = new Button, GOOD!
     }
});


final Card poster = new Card(); // OK! 
addCommand(new Command<Card>() { // Out of the upper bounds!
     @Override 
     public Card onClick() {
            // command-state execution with the source in return 
            return poster; // Incompatible bounds!
     }
});

@Scrappers-glitch
Copy link
Author

Scrappers-glitch commented Apr 8, 2023

API Design Tips

When designing your API, you should be aware of the following rules:

For class type generics and generic method type parameters:

  • Lower bounds are not allowed.
  • Instantiation of a variable of this type isn't allowed.
  • Returning a parameterized type object will restrict the implementation to utilize generics and thus won't be backward compatible.

For wildcard generics:

  • Upper bounds are used to define the type for the method returning values.
  • Lower bounds give the developer the flexibility to use the abstraction over the l-value.

Last, but not least, in order to decide the best type of boundaries to use, think of the final type-erasure you want to have on your API:

  1. Declare the type-erasure you want to finally reach.
  2. Consider optimizing your code using Type-Generics.

Here is a nice tip from the Java tutorials:

In general, if you have an API that only uses a type parameter T as an argument, its uses should take advantage of lower bounded wildcards (? super T). Conversely, if the API only returns T, you'll give your clients more flexibility by using upper bounded wildcards (? extends T).

@Scrappers-glitch
Copy link
Author

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment