Introduction to Java Generics

Okay, here’s a comprehensive article on Java Generics, aiming for approximately 5000 words. This will cover a wide range of topics, from the basics to more advanced concepts and practical examples.

Introduction to Java Generics: A Comprehensive Guide

Java Generics, introduced in Java 5 (JDK 1.5), represent one of the most significant enhancements to the Java language. They provide a mechanism for type safety at compile time, eliminating the need for manual type casting and significantly reducing the risk of ClassCastException errors at runtime. Before generics, collections (like ArrayList, HashMap, etc.) could hold objects of any type, leading to potential type mismatches that were only detected when the program was running. Generics solve this problem by allowing you to specify the type of object a collection (or a class or method) can work with.

This article will delve into the details of Java Generics, covering the following topics:

  1. The Problem Before Generics (and Why We Need Them)
  2. Basic Syntax and Usage of Generics
    • Type Parameters
    • Generic Classes
    • Generic Interfaces
    • Generic Methods
  3. Bounded Type Parameters
    • Upper Bounded Wildcards (<? extends T>)
    • Lower Bounded Wildcards (<? super T>)
    • Unbounded Wildcards (<?>)
  4. Type Erasure
    • How Generics Work Under the Hood
    • Bridge Methods
    • Limitations of Generics Due to Type Erasure
  5. Generics and Inheritance
  6. Wildcards and Subtyping
  7. Generic Methods in Detail
    • Inference of Type Arguments
  8. Creating Generic Classes and Interfaces
  9. Best Practices and Common Patterns
  10. Advanced Topics
    • Reflection and Generics
    • Generic Varargs
    • Reifiable Types
  11. Comparison with C++ Templates
  12. Common Pitfalls and How to Avoid Them
  13. Practical Examples and Use Cases

Let’s begin!

1. The Problem Before Generics (and Why We Need Them)

Before the introduction of generics, working with collections in Java was often cumbersome and error-prone. Consider the following example using an ArrayList to store strings:

“`java
import java.util.ArrayList;
import java.util.List;

public class PreGenericsExample {
public static void main(String[] args) {
List myList = new ArrayList(); // No type specified!
myList.add(“Hello”);
myList.add(“World”);
myList.add(123); // Oops! We added an Integer.

    // We need to cast to String when retrieving elements.
    String str1 = (String) myList.get(0);
    String str2 = (String) myList.get(1);

    // This will cause a ClassCastException at runtime!
    try {
        String str3 = (String) myList.get(2);
    } catch (ClassCastException e) {
        System.out.println("Caught a ClassCastException: " + e.getMessage());
    }
}

}
“`

In this example, myList is a raw type ArrayList. This means it can hold objects of any type. We add two strings and then, accidentally, an integer. The problem is that the compiler doesn’t know that myList is intended to hold only strings. It doesn’t complain about adding the integer.

The real issue arises when we try to retrieve elements. We have to cast each element to String because the get() method of the raw ArrayList returns an Object. The compiler doesn’t know the specific type, so it relies on us to perform the cast correctly.

When we try to cast the integer 123 to a String, a ClassCastException is thrown at runtime. This is a major problem:

  • Lack of Type Safety: The compiler can’t help us prevent type errors. We only discover them when the program is running, potentially in production.
  • Tedious Casting: We have to manually cast every time we retrieve an element, making the code more verbose and prone to errors.
  • Runtime Errors: ClassCastException errors are runtime errors, meaning they can occur after the code has been deployed, leading to unexpected crashes.

Generics provide a solution to all these problems.

2. Basic Syntax and Usage of Generics

Generics introduce the concept of type parameters. These are placeholders for actual types that will be specified later. Let’s see how to use generics with the ArrayList example:

“`java
import java.util.ArrayList;
import java.util.List;

public class GenericsExample {
public static void main(String[] args) {
List myList = new ArrayList<>(); // Type parameter: String
myList.add(“Hello”);
myList.add(“World”);
// myList.add(123); // Compile-time error!

    // No casting needed!
    String str1 = myList.get(0);
    String str2 = myList.get(1);

    System.out.println(str1 + " " + str2);
}

}
“`

Key changes:

  • List<String>: We declare myList as a List that can hold only String objects. String is the type argument we’re providing for the type parameter of the List interface.
  • new ArrayList<>(): The diamond operator (<>) is used on the right-hand side. The compiler infers the type argument (String in this case) from the left-hand side. (This is called type inference). You could also write new ArrayList<String>(), but it’s redundant.
  • // myList.add(123);: If we uncomment this line, we get a compile-time error. The compiler now knows that myList should only hold strings, and it prevents us from adding an integer. This is the crucial advantage of generics: type safety at compile time.
  • No Casting: We no longer need to cast when retrieving elements. myList.get(0) knows it’s returning a String.

2.1 Type Parameters

Type parameters are typically single, uppercase letters, although you can use longer names if it improves readability. Common conventions include:

  • T: Type (a general-purpose type parameter)
  • E: Element (often used for collections)
  • K: Key (used in maps)
  • V: Value (used in maps)
  • N: Number
  • S, U, V (used when you need multiple type parameters)

2.2 Generic Classes

You can create your own generic classes. Here’s a simple example of a generic Box class that can hold an object of any type:

“`java
public class Box {
private T t;

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

public T get() {
    return t;
}

public static void main(String[] args) {
    Box<Integer> integerBox = new Box<>();
    integerBox.set(10);
    Integer someInteger = integerBox.get(); // No cast needed
    System.out.println(someInteger);

    Box<String> stringBox = new Box<>();
    stringBox.set("Hello Generics");
    String someString = stringBox.get(); // No cast needed
    System.out.println(someString);
}

}
“`

  • class Box<T>: We declare the class Box with a type parameter T. This means Box is a generic class.
  • private T t;: The member variable t is of type T. The actual type of t will be determined when we create an instance of Box.
  • set(T t) and get(): The methods use the type parameter T.
  • Box<Integer> integerBox = new Box<>();: We create a Box that can hold an Integer.
  • Box<String> stringBox = new Box<>();: We create a Box that can hold a String.

2.3 Generic Interfaces

You can also define generic interfaces. The List interface itself is a generic interface. Here’s a simple example:

“`java
interface Pair {
public K getKey();
public V getValue();
}

public class OrderedPair implements Pair {
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; }

public static void main(String[] args) {
    Pair<String, Integer> p1 = new OrderedPair<>("Even", 8);
    Pair<String, String>  p2 = new OrderedPair<>("hello", "world");

    System.out.println(p1.getKey() + ": " + p1.getValue());
    System.out.println(p2.getKey() + ": " + p2.getValue());
}

}
“`

  • interface Pair<K, V>: The interface Pair has two type parameters, K and V.
  • OrderedPair<K, V> implements Pair<K, V>: The OrderedPair class implements the Pair interface and uses the same type parameters.
  • Pair<String, Integer> p1 = ...: We create a Pair where the key is a String and the value is an Integer.

2.4 Generic Methods

You can define generic methods, even within non-generic classes. Generic methods have their own type parameters, declared before the return type.

“`java
public class Util {

// Generic method to compare two Pair objects
public static <K, V> boolean compare(Pair<K, V> p1, Pair<K, V> p2) {
    return p1.getKey().equals(p2.getKey()) &&
           p1.getValue().equals(p2.getValue());
}

public static void main(String[] args) {
    Pair<Integer, String> p1 = new OrderedPair<>(1, "apple");
    Pair<Integer, String> p2 = new OrderedPair<>(2, "pear");
    Pair<Integer, String> p3 = new OrderedPair<>(1, "apple");

    boolean same1 = Util.<Integer, String>compare(p1, p2); // Explicit type arguments
    boolean same2 = Util.compare(p1, p3); // Type arguments inferred

    System.out.println("p1 and p2 are same: " + same1); // false
    System.out.println("p1 and p3 are same: " + same2); // true
}

}
“`

  • public static <K, V> boolean compare(...): The <K, V> before boolean declares the type parameters for this method. These are independent of any type parameters the class might have (if it were a generic class).
  • Util.<Integer, String>compare(p1, p2): We can explicitly specify the type arguments when calling the method.
  • Util.compare(p1, p3): More commonly, we let the compiler infer the type arguments based on the arguments we pass to the method. This is called type argument inference.

3. Bounded Type Parameters

Sometimes, you want to restrict the types that can be used as type arguments. This is where bounded type parameters come in. There are three main types of bounds:

3.1 Upper Bounded Wildcards (<? extends T>)

An upper bounded wildcard restricts the unknown type to be a specific type or a subtype of that type. You use the extends keyword.

“`java
import java.util.List;
import java.util.ArrayList;

public class UpperBoundExample {

public static double sumOfList(List<? extends Number> list) {
    double s = 0.0;
    for (Number n : list) {
        s += n.doubleValue();
    }
    return s;
}

public static void main(String[] args) {
    List<Integer> integerList = new ArrayList<>();
    integerList.add(1);
    integerList.add(2);
    integerList.add(3);
    System.out.println("Sum of integers: " + sumOfList(integerList)); // Works fine

    List<Double> doubleList = new ArrayList<>();
    doubleList.add(1.1);
    doubleList.add(2.2);
    doubleList.add(3.3);
    System.out.println("Sum of doubles: " + sumOfList(doubleList)); // Works fine

    //List<String> stringList = new ArrayList<>();
    //sumOfList(stringList);  // Compile-time error: String is not a Number

}

}
“`

  • List<? extends Number> list: This means the list can be a List of Number, or a List of any subtype of Number (like Integer, Double, Float, etc.).
  • sumOfList(integerList) and sumOfList(doubleList): Both work because Integer and Double are subclasses of Number.
  • sumOfList(stringList): This would cause a compile-time error because String is not a subclass of Number.
  • Read-Only (Mostly): With an upper bounded wildcard, you can read elements from the list (as Number objects), but you generally cannot add elements. Think of it as a “producer” of values. You can’t add a Number because you don’t know if the list is a List<Integer>, List<Double>, etc. You could add null, as null is a valid value for any reference type.

3.2 Lower Bounded Wildcards (<? super T>)

A lower bounded wildcard restricts the unknown type to be a specific type or a supertype of that type. You use the super keyword.

“`java
import java.util.List;
import java.util.ArrayList;

public class LowerBoundExample {

public static void addNumbers(List<? super Integer> list) {
    for (int i = 1; i <= 10; i++) {
        list.add(i);
    }
}

public static void main(String[] args) {
    List<Integer> integerList = new ArrayList<>();
    addNumbers(integerList);
    System.out.println(integerList);

    List<Number> numberList = new ArrayList<>();
    addNumbers(numberList);
    System.out.println(numberList);

    List<Object> objectList = new ArrayList<>();
    addNumbers(objectList);
    System.out.println(objectList);

   // List<Double> doubleList = new ArrayList<>();
   // addNumbers(doubleList); //Compile-time error

}

}
“`

  • List<? super Integer> list: This means the list can be a List of Integer, or a List of any supertype of Integer (like Number or Object).
  • addNumbers(integerList), addNumbers(numberList), addNumbers(objectList): All work. We can add Integer objects to a List<Integer>, List<Number>, or List<Object>.
  • addNumbers(doubleList): This would result in Compile-time error, Double is not a super class of Integer.
  • Write-Only (Mostly): With a lower bounded wildcard, you can add elements of type T (or subtypes of T) to the list, but you can only read elements as Objects. Think of it as a “consumer” of values. You can’t be sure of the exact type you’ll get when reading, only that it will be an Object.

3.3 Unbounded Wildcards (<?>)

An unbounded wildcard represents a list of an unknown type. You use a simple question mark (?).

“`java
import java.util.List;
import java.util.ArrayList;
import java.util.Arrays;

public class UnboundedExample {

public static void printList(List<?> list) {
    for (Object elem : list) {
        System.out.print(elem + " ");
    }
    System.out.println();
}

public static void main(String[] args) {
    List<Integer> integerList = Arrays.asList(1, 2, 3);
    List<String> stringList = Arrays.asList("one", "two", "three");

    printList(integerList);
    printList(stringList);
}

}
“`

  • List<?> list: This means the list can be a List of any type. We don’t know what type it is, and we can’t make any assumptions about it.
  • printList(integerList) and printList(stringList): Both work. We can pass a List of any type to printList.
  • Read-Only (Effectively): You can only read elements from the list as Objects. You cannot add anything to the list (except null), because you have no idea what the actual type is. This is the most restrictive type of wildcard. Use it when you only need to read from a list and the specific element type doesn’t matter.

Key Differences: List<?> vs. List<Object>

It’s important to distinguish between List<?> and List<Object>. They are not the same:

  • List<Object>: A list that can hold only objects of type Object. You can add any object to it.
  • List<?>: A list of some unknown type. You cannot add anything to it (except null), because you don’t know what type it’s supposed to hold. It is much more restrictive.

You can pass a List<String> to a method expecting a List<?>, but you cannot pass a List<String> to a method expecting a List<Object>. This is because List<String> is not a subtype of List<Object> (even though String is a subtype of Object). This is a crucial point about generics and subtyping, which we’ll discuss later.

4. Type Erasure

Type erasure is a fundamental aspect of how generics are implemented in Java. It’s essential to understand type erasure to grasp the capabilities and limitations of generics.

4.1 How Generics Work Under the Hood

When you compile Java code with generics, the compiler performs a process called type erasure. Here’s what happens:

  1. Type Checking: The compiler checks the code for type safety, ensuring that you’re using generics correctly (e.g., not adding a String to a List<Integer>).
  2. Erasure: The compiler removes all type parameter information. List<String> becomes simply List. Box<Integer> becomes Box. The type parameters are replaced with their bounds (or Object if there are no bounds).
  3. Casting: The compiler inserts casts where necessary to ensure type safety at runtime. These casts are based on the erased type.

Essentially, generics are a compile-time feature. At runtime, the JVM has no knowledge of the type parameters you used.

Example:

Consider our Box<T> class:

“`java
public class Box {
private T t;

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

public T get() {
    return t;
}

}
“`

After type erasure, it effectively becomes:

“`java
public class Box { // No more
private Object t; // T is replaced with Object

public void set(Object t) {
    this.t = t;
}

public Object get() { // Returns Object
    return t;
}

}
“`

When you use Box<Integer>, the compiler inserts casts:

java
Box<Integer> integerBox = new Box<>();
integerBox.set(10);
Integer someInteger = integerBox.get(); // Compiler inserts a cast here

The get() method still returns Object, but the compiler adds a (Integer) cast to make it type-safe. This cast is guaranteed to succeed because of the compile-time type checks.

4.2 Bridge Methods

Bridge methods are synthetic methods generated by the compiler to handle situations where type erasure would otherwise break polymorphism with generic types. They are most common when dealing with inheritance and generic methods.

Consider this example:

“`java
class Node {
public T data;

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

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

}

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

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

}

public class BridgeMethodExample {
public static void main(String[] args) {
MyNode mn = new MyNode(5);
Node n = mn; // This is allowed (upcasting)
n.setData(“Hello”); // This will call a bridge method
Integer x = mn.data; // This would cause a ClassCastException without the bridge method
System.out.println(x);
}
}
“`

After type erasure, Node becomes:

java
class Node {
public Object data;
public Node(Object data) { this.data = data; }
public void setData(Object data) { ... }
}

The setData method in Node now takes an Object. The setData method in MyNode takes an Integer. Without a bridge method, when we call n.setData("Hello"), the setData(Object) method in Node would be called directly. This could lead to mn.data (which is supposed to be an Integer) holding a String, breaking type safety.

To prevent this, the compiler generates a bridge method in MyNode:

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

This bridge method:

  1. Takes an Object as input (matching the erased signature of Node.setData).
  2. Casts the Object to an Integer.
  3. Calls the setData(Integer) method in MyNode.

This ensures that the correct method is called and that type safety is maintained, even though the type information has been erased.

4.3 Limitations of Generics Due to Type Erasure

Type erasure imposes several limitations on what you can do with generics in Java:

  • Cannot Create Instances of Type Parameters: You cannot do new T(). The compiler doesn’t know what T is at runtime.
  • Cannot Create Arrays of Type Parameters: You cannot do new T[10]. Arrays in Java need to know their component type at runtime, but T is erased.
  • Cannot Use Primitive Types as Type Arguments: You cannot use List<int>. Type arguments must be reference types. You must use wrapper classes like Integer, Double, etc. (List<Integer>).
  • Cannot Use instanceof with Parameterized Types: You cannot do if (obj instanceof List<String>). At runtime, it’s just a List, not a List<String>. You can only use the raw type or a wildcard: if (obj instanceof List).
  • Cannot Overload Methods Where Erasure Results in the Same Signature:

    java
    public class Example {
    // These methods have the same erasure (List), so they clash
    // public void process(List<String> list) { ... }
    // public void process(List<Integer> list) { ... }
    }

    * Cannot create, catch, or throw objects of parameterized types. Generic class cannot extend Throwable directly or indirectly.
    * Static members cannot use the class’s type parameters.

5. Generics and Inheritance

Generics interact with inheritance in specific ways. A key concept is that a generic class with one type argument is not a subtype of the same generic class with a different type argument, even if the type arguments themselves have an inheritance relationship.

“`java
public class InheritanceExample {
public static void main(String[] args) {
List intList = new ArrayList<>();
List numList = new ArrayList<>();

    // numList = intList;  // Compile-time error!

    //Even if Integer is a sub type of Number
    //ArrayList<Integer> is not a subtype of ArrayList<Number>

    Number n = new Integer(5); // Fine: Integer is a subtype of Number
}

}
“`

  • Number n = new Integer(5);: This is standard inheritance. An Integer is a Number.
  • numList = intList;: This causes a compile-time error. A List<Integer> is not a List<Number>. This is because allowing this could break type safety:

    java
    List<Integer> intList = new ArrayList<>();
    List<Number> numList = intList; // Imagine this were allowed
    numList.add(3.14); // We could add a Double to a List<Integer>!
    Integer x = intList.get(0); // ClassCastException!

    Generics prevent this by enforcing that List<Integer> and List<Number> are distinct, unrelated types.

6. Wildcards and Subtyping

Wildcards provide a way to express relationships between generic types that wouldn’t be possible with concrete type arguments. This is how you achieve a form of polymorphism with generics.

  • List<? extends Number>: This represents a family of types: List<Number>, List<Integer>, List<Double>, etc. You can assign any of these to a List<? extends Number> variable.
  • List<? super Integer>: This represents a family of types: List<Integer>, List<Number>, List<Object>. You can assign any of these to a List<? super Integer> variable.
  • List<?>: This represents a list of any type. It’s the most general wildcard.

The “PECS” principle (“Producer Extends, Consumer Super”) is a helpful mnemonic:

  • Producer Extends: If you have a generic collection that you’re only reading from (producing values), use <? extends T>.
  • Consumer Super: If you have a generic collection that you’re only writing to (consuming values), use <? super T>.
  • If you need to do both, don’t use a wildcard; use a specific type parameter.

7. Generic Methods in Detail

We’ve touched on generic methods, but let’s explore them further.

7.1 Inference of Type Arguments

One of the most convenient features of generic methods is type argument inference. The compiler can often figure out the type arguments for you, based on the types of the arguments you pass to the method.

“`java
public class InferenceExample {

public static <T> T pick(T a1, T a2) {
    return a2;
}

public static void main(String[] args) {
    String s = pick("d", "a"); // Compiler infers T as String
    Integer i = pick(1,2);    //Compiler infers T as Integer
    //Integer i = pick(1,2.0); //Compiler finds common ancestor which is Number
    Number n = pick(1, 2.2);  //Compiler infers T as Number

}

}
“`

The compiler analyzes the arguments passed to pick and determines the most specific type that satisfies the constraints of the type parameters. This significantly reduces the verbosity of using generic methods.

8. Creating Generic Classes and Interfaces

We’ve seen examples of creating generic classes and interfaces. The key is to declare the type parameters in angle brackets (<>) after the class or interface name. You can then use those type parameters within the class or interface definition.

“`java
//Generic interface
interface MinMax> {
T min();
T max();
}

//Generic class implementing the generic interface.
class MyClass> implements MinMax{
T[] vals;

MyClass(T[] o) {
    vals = o;
}

public T min() {
    T v = vals[0];
    for (int i = 1; i < vals.length; i++) {
        if (vals[i].compareTo(v) < 0) {
            v = vals[i];
        }
    }
    return v;
}
 public T max() {
    T v = vals[0];
    for (int i = 1; i < vals.length; i++) {
        if (vals[i].compareTo(v) > 0) {
            v = vals[i];
        }
    }
    return v;
}

}
public class GenIFDemo {
public static void main(String args[]) {
Integer inums[] = { 3, 6, 2, 8, 6 };
Character chs[] = { ‘b’, ‘r’, ‘p’, ‘w’ };
MyClass a = new MyClass(inums);
MyClass b = new MyClass(chs);
System.out.println(“Max value in inums: ” + a.max());
System.out.println(“Min value in inums: ” + a.min());
System.out.println(“Max value in chs: ” + b.max());
System.out.println(“Min value in chs: ” + b.min());
}
}
“`

In this example:

  • MinMax is a generic interface which takes a Type T which should implement Comparable interface.
  • MyClass is a generic class, which implements MinMax interface, the constructor takes an array of type T.
  • min and max methods return the minimum and maximum values in the array respectively. The compareTo method (from Comparable) is used to compare the elements.
  • GenIFDemo class demonstrates how to create instances of the MyClass with Integer and Character arrays.

9. Best Practices and Common Patterns

Here are some best practices for working with generics:

  • Use Generics Whenever Possible: Embrace generics to improve type safety and reduce casting.
  • Favor Generic Types over Raw Types: Avoid using raw types (like List instead of List<String>) unless you have a very specific reason (e.g., interacting with legacy code).
  • Use Descriptive Type Parameter Names: While single letters are common, use longer names if it improves clarity (e.g., <KeyType, ValueType>).
  • Understand and Apply PECS: Remember “Producer Extends, Consumer Super” when using wildcards.
  • Eliminate Unchecked Warnings: Pay attention to compiler warnings related to generics. Try to eliminate them by using generics correctly. If you must suppress a warning, use @SuppressWarnings("unchecked") on the narrowest possible scope and add a comment explaining why it’s safe.
  • Prefer Lists to Arrays: Arrays and generics don’t mix well due to type erasure. Lists are generally a better choice when working with generics.
  • Consider Using Bounded Type Parameters: Restrict the types that can be used with your generic classes or methods when appropriate.
  • Don’t create subtypes of parameterized types.

10. Advanced Topics

10.1 Reflection and Generics

Reflection in Java allows you to inspect and manipulate classes, methods, and fields at runtime. However, type erasure makes it challenging to work with generic type information using reflection.

You can obtain some generic type information using reflection, but it’s limited:

  • Class.getTypeParameters(): Returns an array of TypeVariable objects representing the type parameters declared by the class. However, this only gives you the declared type parameters, not the actual type arguments used at runtime.
  • ParameterizedType: If you have a field or method parameter that is a parameterized type (like List<String>), you can use reflection to get a ParameterizedType object. From this, you can get the raw type (List) and the actual type arguments (String). However, this only works if the type information is available at runtime (e.g., it’s not been completely erased).

“`java
import java.lang.reflect.*;
import java.util.List;
import java.util.ArrayList;

public class ReflectionGenerics {
private List stringList = new ArrayList<>();

public static void main(String[] args) throws NoSuchFieldException {
    Field field = ReflectionGenerics.class.getDeclaredField("stringList");
    Type fieldType = field.getGenericType();

    if (fieldType instanceof ParameterizedType) {
        ParameterizedType pType = (ParameterizedType) fieldType;
        System.out.println("Raw type: " + pType.getRawType()); // class java.util.List
        Type[] typeArgs = pType.getActualTypeArguments();
        for (Type typeArg : typeArgs) {
            System.out.println("Type argument: " + typeArg); // class java.lang.String
        }
    }
}

}
“`

10.2 Generic Varargs

Varargs methods (methods that take a variable number of arguments) can be used with generics. However, there are some subtleties due to the interaction between varargs and type erasure.

“`java
import java.util.Arrays;
import java.util.List;

public class GenericVarargs {

@SafeVarargs // Suppresses the warning
public static <T> List<T> asList(T... elements) {
    return Arrays.asList(elements);
}
public static <T> void heapPollution(List<T>... stringLists) {
    Object[] array = stringLists;
    List<Integer> tmpList = Arrays.asList

Leave a Comment

Your email address will not be published. Required fields are marked *

Scroll to Top