Characters, Strings, Autoboxing, and Generics in Java

In Java, single character values are typically represented using the primitive char type. However, when an object representation is required—such as when passing to methods expecting objects—the Character wrapper class is used. This class encapsulates a char value and provides numerous static utility methods for character manipulation.

Strings, which are sequences of characters, are represented as objects of the String class. A common way to create a string is through direct assignment:

String message = "Hello world!";

The String class offers over 60 methods, including split(), toLowerCase(), toUpperCase(), and valueOf(). The latter is especially useful for converting strings to numeric types. Complementary conversion methods exist in numeric wrapper classes like Integer and Double.

For scenarios involving frequent string modifications, StringBuilder is more efficient than String. It supports operations like reverse() and can be converted to a String via toString(). Conversely, a String can be used to initialize a StringBuilder.

Autoboxing and Unboxing

Java automatically converts between primitive types and their corresponding wrapper classes—a feature known as autoboxing (primitive → wrapper) and unboxing (wrapper → primitive). For example:

Character ch = 'a'; // autoboxing
List<Integer> numbers = new ArrayList<>();
for (int i = 1; i < 50; i += 2)
    numbers.add(i); // autoboxing: int → Integer

During compilation, the above loop is transformed to use Integer.valueOf(i). Similarly, in method bodies that operate on primitives, unboxing occurs automatically:

public static int sumEven(List<Integer> list) {
    int total = 0;
    for (Integer num : list)
        if (num % 2 == 0) // unboxing: Integer → int
            total += num; // unboxing again
    return total;
}

The compiler inserts calls to intValue() during unboxing. This mechanism applies when:

  • A wrapper object is passed where a primitive is expected.
  • A wrapper object is assigned to a primitive variable.

The following table maps primitive types to their wrapper classes:

Primitive Type Wrapper Class
boolean Boolean
byte Byte
char Character
double Double
float Float
int Integer
long Long
short Short

Generics Overview

Generics enable types (classes and interfaces) to be parameterized, improving type safety and eliminating the need for explicit casts. A generic class is declared with type parameters enclosed in angle brackets:

public class Box<T> {
    private T value;
    public void set(T value) { this.value = value; }
    public T get() { return value; }
}

Here, T is a type variable that can be replaced with any non-primitive type. Common naming conventions include:

  • E – Element
  • K – Key
  • V – Value
  • T – Type
  • N – Number

To use a generic class, specify actual types:

Box<Integer> box = new Box<>(); // diamond operator infers type

Multiple type parameters are also supported:

public class Pair<K, V> {
    private K key;
    private V value;
    // constructor and accessors...
}

Pair<String, Integer> score = new Pair<>("Alice", 95);

Raw Types

A raw type is a generic type used without type arguments (e.g., Box instead of Box<T>). While allowed for backward compatibility, raw types bypass compile-time type checks and should be avoided.

Generic Methods

Methods can declare their own type parameters, independent of the class:

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());
}

Type inference usually allows omitting explicit type arguments during invocation:

boolean same = Util.compare(pair1, pair2); // compiler infers types

Bounded Type Parameters

To restrict the types that can be used as type arguments, use bounded parameters:

public <U extends Number> void logValue(U number) {
    System.out.println(number.doubleValue());
}

This ensures only Number or its subclasses (e.g., Integer, Double) are accepted. Multiple bounds are possible, with the class (if any) listed first:

<T extends Comparable<T> & Serializable>

Subtyping and Inheritance with Generics

Evenif Integer extends Number, List<Integer> is not a subtype of List<Number>. This prevents unsafe operations. To express flexible subtyping, use wildcards.

Wildcards

The wildcard ? denotes an unknown type. Three forms exist:

  • Unbounded: List<?> — accepts any List.
  • Upper-bounded: List<? extends Number> — accepts lists of Number or its subclasses.
  • Lower-bounded: List<? super Integer> — accepts lists of Integer or its supertypes.

Guideline: Use ? extends T for producer ("in") parameters and ? super T for consumer ("out") parameters (PECS principle).

Type Erasure

At compile time, Java erases all generic type information, replacing type parameters with their bounds (or Object if unbounded). This ensures no runtime overhead but requires synthetic bridge methods to preserve polymorphism in inherited generic classes.

Restrictions on Generics

Key limitations include:

  • No instantiation of type parameters: new T() is invalid.
  • No primitive types as type arguments (use wrappers instead).
  • No static fields with type parameters.
  • No instanceof checks with concrete parameterized types (e.g., list instanceof List<String> fails).
  • No arrays of parameterized types: new List<String>[10] is illegal.
  • Generic classes cannot extend Throwable.
  • Method overloading based solely on different type parameters is disallowed if erasure results in identical signatures.

Additionally, varargs methods with generic types may cause heap pollution. Use @SafeVarargs to suppress warnings when the method implementation is safe.

Tags: java generics autoboxing StringBuilder TypeErasure

Posted on Wed, 26 Aug 2026 16:14:26 +0000 by NathanLedet