Generics let the compiler know what type of values a class or collection is supposed to hold. They turn a collection that could otherwise accept many kinds of objects into a collection designed for a particular type, with incompatible operations detected during compilation rather than appearing as runtime failures.
For example, a shopping cart that stores product names can be declared as List<String>. The compiler then checks that only strings are added, and values retrieved from the list can be used as strings without explicit casting.
This lesson covers the problem generics solve, how parameterized types work, the diamond operator, type-parameter naming conventions, and the use of standard generic types such as List, Map, and Optional. Defining custom generic classes, methods, and interfaces is a separate topic.
Why generics matter
They provide compile-time type checking.
They reduce the need for explicit casts when retrieving values.
They make APIs clearer because the expected value types are visible in declarations.
They help prevent runtime errors caused by putting one type into a collection intended for another.
Generics are primarily a compile-time feature. Java implements them through type erasure, meaning generic type arguments generally are not retained as separate runtime types in the same way the compiler checks them. Generics do not add extra method dispatch merely because a collection is parameterized. Wrapper types are still used when primitive values are stored in generic collections.
2. Life Before Generics
Generics were introduced in Java 5. Before that, collections in java.util commonly stored values as Object. Because Object is the root of Java's reference-type hierarchy, a raw collection could accept a string, an integer, or another object without expressing which type the program actually expected.
That flexibility could cause problems when values were read back. A caller often had to cast the result, and an incorrect cast could compile successfully but fail at runtime.
Example: a list without type safety
The first cast succeeds because the value at index 0 is a String. The third cast fails with ClassCastException because the value at index 2 is an Integer. The compiler cannot verify the intended element type because the raw list does not declare one.
The underlying problem is not that a collection contains objects; it is that the program has lost the information about which object type is valid for that particular collection.
3. How Generics Solve the Problem
A parameterized collection declares its expected element type in angle brackets. For instance, List<String> indicates that the list is intended to contain strings. The compiler can reject incompatible add() calls, and get() returns a String directly.
Example: the same list with generics
The compiler rejects cart.add(100) because Integer is not compatible with String. The retrieved item is already typed as String, so no explicit cast is needed. The mistake that previously appeared at runtime is now detected while compiling.
How generics provide type safety
The flowchart shows the role of the declared type argument and the compiler's check.
Whiteboard
Loading diagram...
4. What “Parameterized Type” Actually Means
A type parameter is a placeholder for a type that gets filled in when a generic type is used. List<E> is the generic declaration: E represents the element type. List<String> fills E with String, while List<Integer> fills it with Integer.
The declaration is reusable, but each parameterized use has its own compile-time type rules. The type argument influences the methods exposed to the caller: adding to a List<String> must supply a string, while adding to a List<Integer> must supply an integer wrapper.
Example: two different parameterized types
productNames accepts strings, while stockCounts accepts integers. A method expecting List<String> can accept productNames, but not stockCounts, because List<Integer> is not a subtype of List<String>.
One generic declaration, several type arguments
Whiteboard
Loading diagram...
The generic declaration is what a library author defines. Application code chooses a type argument to create the parameterized type it needs. This lets the compiler check operations according to the declared type.
Generic types with more than one parameter
A generic type can have multiple type parameters. Map<K, V> uses K for keys and V for values. Filling them in with Map<String, Integer> means that keys must be strings and values must be integers.
The same Map<K, V> declaration can be used for Map<String, Product> in a product catalog or Map<Integer, String> for order IDs and statuses. The generic structure remains the same while the selected key and value types change.
5. The Diamond Operator and Type Inference
Early generic code often repeated the type argument in the variable declaration and in the constructor:
Java 7 introduced the diamond operator, <>, allowing the compiler to infer the constructor's type argument from the surrounding context.
Example: using the diamond operator
Because the left side declares List<String>, the compiler can infer that the new ArrayList uses String as its element type. Similarly, it infers the key and value types for the HashMap.
The empty angle brackets are important. new ArrayList() omits the type argument and creates a raw type, which weakens type checking. Prefer the diamond operator when context makes the intended type clear.
Type inference with method return values
When a method already returns a parameterized type, the caller can store the result in a compatible variable without repeating type information unnecessarily.
The return type List<String> communicates the element type to the caller. The caller can use the returned list without a cast.
6. Type Parameter Naming Conventions
Java does not enforce a particular naming convention for type parameters, but the standard library uses familiar single-letter names. These conventions make generic declarations easier to recognize.
Letter
Stands for
Where it appears
T
Type
General-purpose parameter, such as Optional<T>
E
Element
Element type in List<E> or Set<E>
K
Key
Map key in Map<K, V>
V
Value
Map value in Map<K, V>
N
Number
Numeric type parameter
R
Return
Result type in functional interfaces such as Function<T, R>
Optional<T> uses T because it can represent a value of many possible types. List<E> uses E because a list is a sequence of elements. Map<K, V> uses two names because keys and values have different roles. Function<T, R> uses T for the input and R for the result.
Following these conventions in custom generic types makes the code more familiar to Java developers. The letters are conventions, not keywords, but unusual names can make generic declarations harder to read.
7. Using the Standard Library’s Generic Types
Most generic code uses types already provided by Java rather than defining new generic classes. Collections such as List and Map, Optional, CompletableFuture, Class, and functional interfaces in java.util.function all use generic type parameters.
The general pattern is consistent: choose the type argument that describes the data, and let the compiler check operations that use it.
A shopping cart with List<Product>
A cart can be represented by a list of product objects. Once the element type is declared as Product, a for-each loop can retrieve each item as a product without casting.
The local variable product is statically typed as Product, so methods such as price() can be checked at compile time. An attempt to add a string directly to the cart is rejected by the compiler.
A product catalog with Map<String, Product>
A map is useful when products are looked up by an identifier such as a SKU. Map<String, Product> specifies that each key is a string and each value is a product.
The return type of catalog.get() is CatalogProduct, so the result can be used directly after checking whether it is null. The type argument determines the expected value type.
Optional<T> for a value that may be absent
Optional<T> represents a value that may or may not be present. Its type parameter identifies the type of value held when one exists.
Optional<String> is distinct from Optional<Integer> or Optional<Product>. If a value is present, the generic type tells the caller what kind of value it is. Optional.empty() represents the absence of a value.
List<Integer> and the cost of boxing
Java collections store reference types, not primitive values. Therefore, a List<Integer> stores Integer objects. When an int is added, Java automatically boxes it into an Integer; when the value is used as an int, Java can unbox it.
The conversions are convenient, but wrapper objects have memory and processing costs. For a list with a modest number of stock counts, this is usually acceptable. For large numeric workloads, a primitive array such as int[] or an IntStream may avoid some boxing overhead.
8. Raw Types: What They Are and Why to Avoid Them
A raw type is a generic class used without its type argument. ArrayList is raw, while ArrayList<String> is parameterized. Raw types remain available for backward compatibility with code written before generics, but they give up much of the compiler's type checking and commonly produce warnings.
Example: a raw list
This code can compile with raw-type or unchecked-operation warnings. The loop uses Object, so it can print both values, but the compiler cannot guarantee that the list contains only product names. When code later assumes a particular type, a cast failure can occur.
How raw and parameterized types can interact dangerously
A raw reference can point to a list that is also referenced through a parameterized variable. Using the raw reference can bypass the type checks associated with the parameterized reference.
The raw reference inserts an Integer into the same list. The enhanced for loop expects each item to be a String, so the program can throw ClassCastException when it reaches the integer. The compiler may warn about the raw operation, but it does not reject it.
The practical rule is to avoid raw types in new code. If the element type is unknown at a particular point, List<?> is generally safer than a raw List.
Raw type vs parameterized type vs wildcard
Form
Meaning
Practical effect
ArrayList
Raw type
Element type is not declared; type checks are weakened and warnings are possible
ArrayList<String>
Parameterized type
Element type is fixed; incompatible additions are rejected
ArrayList<?>
Wildcard type
Element type is unknown at this use site; values can be read as Object, but arbitrary non-null values cannot be added
The three forms may look similar, but they express different levels of type information. Parameterized types provide the strongest checks when the element type is known. Wildcards express an unknown type safely. Raw types are mainly a legacy compatibility feature.
Whiteboard
Loading diagram...
9. Putting It Together: A Small Checkout Flow
A small checkout program can use three generic types together: Map<String, Double> for product prices, List<String> for cart entries, and Optional<Double> for a price lookup that may not find a match.
Each type parameter has a specific job. The map requires string keys and double-valued wrapper objects. The cart accepts product names as strings. The optional represents a price that may be present or absent.
If a future change tries to add an integer to the cart or swaps the key and value types in the catalog, the compiler rejects the operation. These are the kinds of mistakes generics are designed to catch before the program runs.
Whiteboard
Loading diagram...
10. Common Mistakes and Best Practices
Using a raw type instead of a parameterized type
Avoid declarations such as List names = new ArrayList(). Prefer List<String> names = new ArrayList<>() when the list stores strings. This lets the compiler reject incompatible additions and avoids unnecessary casts.
Assuming List<String> and List<Integer> are interchangeable
Both are based on the same generic declaration, but they are different parameterized types. A method that accepts List<String> cannot be passed a List<Integer>.
Removing the diamond operator
new ArrayList<>() retains generic type information through inference. new ArrayList() is a raw type and weakens type checking. Do not omit the diamond merely to shorten the expression.
Expecting generics to store primitives directly
Generic collections require reference types. Use wrapper types such as Integer and Double when working with collections, and remember that boxing and unboxing may occur. Use primitive arrays or specialized primitive APIs when avoiding boxing is important.
Treating List<?> as a list that accepts any type
List<?> means the actual element type is unknown, not that every kind of value can safely be inserted. Apart from null, arbitrary additions are rejected because the list might actually be a List<String>, List<Product>, or another specific type.
Assuming generics remove every runtime type issue
Generics catch many type mismatches at compile time, but raw types, unchecked operations, and unsafe casts can bypass those checks. Keep new code parameterized and pay attention to compiler warnings.
Using Optional without considering absence
An Optional<T> represents a value that may not be present. Use methods such as ifPresent(), orElse(), or other suitable optional operations to handle the absence case deliberately.
11. Quick Revision Summary
Generics specify the kind of values a class or collection is intended to hold.
List<String> provides compile-time type checking and avoids explicit casts when reading strings.
List<E> is a generic declaration; List<String> and List<Integer> are parameterized types.
Map<K, V> uses separate type parameters for keys and values.
Java 7 introduced the diamond operator <> for constructor type inference.
T, E, K, V, N, and R are common type-parameter naming conventions.
Optional<T> represents a value that may or may not be present.
List<Integer> stores Integer objects, so primitive int values are boxed and unboxed.
Raw types weaken compile-time checks and can lead to runtime ClassCastException.
List<?> is a safer way to refer to a list when the element type is unknown.
Generics are mainly a compile-time feature implemented using type erasure.
12. Conclusion
Generics make Java programs clearer and safer by expressing type expectations directly in declarations. They reduce explicit casts, let the compiler catch incompatible operations, and make collection APIs easier to understand. Parameterized types such as List<String> and Map<String, Product> describe how data is expected to flow through an application, while Optional<T> expresses a value that may be absent.
Use the diamond operator when the type can be inferred, follow familiar naming conventions, and avoid raw types in new code. Together, these practices make Java collection code more maintainable and help prevent many type-related bugs before execution.
Author & Technical Reviewer
B
Written by:Bhavya Hasini Cheede
Technically reviewed by:ExamAdda Technical Review Team
Technical Reviewers, ExamAdda
Software engineers at ExamAdda who check every article's definitions, complexity claims and code examples before and after publishing.
Published
Oct 27, 2026
Last updated
Oct 27, 2026
Content Verification Methodology
Definitions and complexity claims were checked against authoritative computer-science references. Code examples were compiled and tested with standard, boundary and edge-case inputs.