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– ElementK– KeyV– ValueT– TypeN– 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 anyList. - Upper-bounded:
List<? extends Number>— accepts lists ofNumberor its subclasses. - Lower-bounded:
List<? super Integer>— accepts lists ofIntegeror 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
instanceofchecks 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.