πŸ§ŠπŸ’§πŸ’¨ Java Generic

GunhoΒ·2024λ…„ 10μ›” 19일
1

Object Oriented Programming (OOP) & Java

λͺ©λ‘ 보기
6/29

πŸ§ŠπŸ’§πŸ’¨ Java Generic

πŸ’‘ Generics refers to a mechanism where a specific data type of classes, methods, or interfaces is defined at the time of instantiation rather than at the time of type definition.

Generics enables the compile-time data type check leading to the lowered risk of runtime error potentially induced from a type-casting. Generics also increases code flexibility, code reusability and type safety, as the data type is defined at instantiation.

πŸ“ Grammar

A generic class grammatically follows the below format:

class CLASS_NAME<T1, T2, ..., Tn> { /* ... */ }

The above format can be further elaborated as below where the type parameter, T, also becomes the type of a instance variable, and the return type of the two methods, a getter() and a setter().

Single Type Parameter

public class Box<T> {
    private T t;

    public void set(T t) { 
    	this.t = t; 
    }
    
    public T get() { 
    	return t; 
    }
}

Results

public class Main{
	public static void main(String[] args) {
    	Box<String> stringBox = new Box<>();
        Box<Integer> integerBox = new Box<>();
        
        System.out.println(stringBox.get());  // "";
        System.out.println(integerBox.get()); // 0
        
        stringBox.setter("hello");
        integerBox.setter(1);
        
        System.out.println(stringBox.get());  // "hello";
        System.out.println(integerBox.get()); // 1
        
        stringBox.setter(3240)     // compile error
        integerBox.setter("hello") // compile error
    }
}

The same principles strictly applies to the multiple type parameters where the below can be a good practical example.

Multiple Type Parameters

public class OrderedPair<K, V> {

    private K key;
    private V value;

    public OrderedPair(K key, V value) {
	this.key = key;
	this.value = value;
    }

    public K getKey()	{ return key; }
    public V getValue() { return value; }
}

Results

OrderedPair<String, Integer> p1 = new OrderedPair<>("Even", 8);
OrderedPair<String, String>  p2 = new OrderedPair<>("hello", "world");

🧾 Type Parameter Naming Conventions

Oracle Available at here


πŸ‚‘ Wildcard

πŸ’‘ Wildcard represents an unknown type and can be programmatically represented as the question mark (?).

Wildcard can be extensively used from a type parameter to field, local variable, or even as a return type. Wildcard, if appropriately used, enables developers to write flexible codes and increases code reusability by preventing the creation of duplicate code handling varying type definition cases.

Essentially, wildcard can be further specified by implementing its upper or lower boundaries. The upper boundaries define all the subclasses that either directly or indirectly inherit a given reference type. On the contrary, the lower boundaries require a provided type parameter to be a direct or an indirect parent class of a reference type.

In Java, programmatically, the upper boundaries can be implemented via the extends keyword while the lower boundaries can be achieved via the super keyword.

The below could be a good theoretical example for the upper bounded wildcard as the List in the method parameter only allows the List execution towards subclasses of the Hello class.

Upper Bounded Wildcard

public static void example(List<? extends Hello> list) { /* ... */ }

πŸ’‘ The upper & lower boundaries can be also applied to type parameters.

Upper Bounded Type Parameter

public class Sample<T super Hello> {
	...
}

β˜ƒοΈ Interface & Method

All the above principles of generic are well applicable to the interfaces and methods.

Interfaces

Generic Interfaces may be exactly the same to the generic classes where all the above discussed principles strictly apply to the generic interfaces.

A good example could be below:

Generic Interface

public interface Pair<K, V> {
    public K getKey();
    public V getValue();
}

public class OrderedPair<K, V> implements Pair<K, V> {

    private K key;
    private V value;

    public OrderedPair(K key, V value) {
      this.key = key;
      this.value = value;
    }

    public K getKey()	{ return key; }
    public V getValue() { return value; }
}

Oracle Available at here



Generic Method

Generic Methods are defined as the methods in which a single or multiple type parameters within the scope of the methods are independently specified.

The syntax for the generic methods are simply the typical method signature followed by the generic diamond, <>, where the diamond is placed after an access modifier.

The below could be a good example of a generic method:

Generic Method

public class Sample<T> {

	// the below T differs from the above class T
    // where the below T has an independent scope within the method
	public <T> void example(T value) {
		System.out.println("Provided Value is: " + value); 
  }
}

Given the above generic method, its invocation can be done either via explicit invocation where the type reference is explicitly provided or via inference where the Java infers the right data type from the provided parameters.

Results

public class Main {
	public static void main(String[] args) {
    	Main main = new Main();
        main.executeExample();
    }
    
    public void executeExample() {
    	Sample<String> sample = new Sample();
        sample.<Integer>example(1); // 1
        
        // or by inference
        sample.example(1); // 1
    }
}

Same applies to a static method where it can be invoked implicitly and explicitly.

Generic Static Method

public class Sample {
	public static <T> void example(T value) {
		System.out.println("Provided Value is: " + value); 
  }
}


✏️ Type Erasure

✏️ Type Erasure refers to a mechanism in which the Java Compiler removes all type parameters and replaces them with generic types with their bounds or Objects if unbounded.

Type Erasure introduces type casts where necessary to ensure type safety and generates bridge methods to maintain polymorphism in classes and interfaces that inherits or implements other classes and interfaces with generic types as well. Type Erasure ensures that no new classes are created for parameterised types; consequently, generics incur no runtime overhead.

For instance, the typical Type Erasure happens to be the following:

Type Erasure (Unbounded Type Parameter)

// source code
public class Node<T> {

    private T data;
    private Node<T> next;

    public Node(T data, Node<T> next) {
        this.data = data;
        this.next = next;
    }

    public T getData() { return data; }
    // ...
}

// type erased code
public class Node {

    private Object data;
    private Node next;

    public Node(Object data, Node next) {
        this.data = data;
        this.next = next;
    }

    public Object getData() { return data; }
    // ...
}

Type Erasure (Bounded Type Parameter)

// source code
public class Node<T extends Comparable<T>> {

    private T data;
    private Node<T> next;

    public Node(T data, Node<T> next) {
        this.data = data;
        this.next = next;
    }

    public T getData() { return data; }
    // ...
}

// type erased code
public class Node {

    private Comparable data;
    private Node next;

    public Node(Comparable data, Node next) {
        this.data = data;
        this.next = next;
    }

    public Comparable getData() { return data; }
    // ...
}

The above type erasure execution, however, can be problematic under the below case where a generic class is extended to another class.


// source code
public class Node<T> {

    public T data;

    public Node(T data) { this.data = data; }

    public void setData(T data) {
        System.out.println("Node.setData");
        this.data = data;
    }
}

public class MyNode extends Node<Integer> {
    public MyNode(Integer data) { super(data); }

    public void setData(Integer data) {
        System.out.println("MyNode.setData");
        super.setData(data);
    }
}

// type erased code
public class Node {

    public Object data;

    public Node(Object data) { this.data = data; }

    public void setData(Object data) {
        System.out.println("Node.setData");
        this.data = data;
    }
}

public class MyNode extends Node {

    public MyNode(Integer data) { super(data); }

    public void setData(Integer data) {
        System.out.println("MyNode.setData");
        super.setData(data);
    }
}

The type-erased Node and MyNode class now have a different method signature to the setData() method where a method overriding or a polymorphism with the generics fails to comply at the runtime.

To preserve the polymorphism, Java Compiler generates a bridge method in which it links the two methods where they no longer happen to comply with a polymorphism.

Below is the generated bridge method:

Bridge Method

class MyNode extends Node {

    // Bridge method generated by the compiler
    //
    public void setData(Object data) {
        setData((Integer) data);
    }

    public void setData(Integer data) {
        System.out.println("MyNode.setData");
        super.setData(data);
    }

    // ...
}

And given the above generated bridge method, the below code execution incurs ClassCastException followed by wrong type-casting within the bridge method ((Integer) "Hello").

MyNode mn = new MyNode(5);
Node n = mn;            // A raw type - compiler throws an unchecked warning
                        // Note: This statement could instead be the following:
                        //     Node n = (Node)mn;
                        // However, the compiler doesn't generate a cast because
                        // it isn't required.
n.setData("Hello");     // Causes a ClassCastException to be thrown.
Integer x = (Integer)mn.data; 

Oracle All Available at here


πŸ“š References

μžλ°”μ˜ μ‹ 
Oracle
G Market (1)
G Market (2)
F-Lab (1)
F-Lab (2)

profile
Hello

0개의 λŒ“κΈ€