Generics
- The term "generic" means "pertaining or appropriate to large groups of classes."
- While using someone else’s generic type is fairly easy, when creating your own you will encounter a number of surprises.
Comparison with C++
- Understanding certain aspects of C++ templates will help you understand the foundations of the concept, as well as the limitations of what you can do with Java generics and why.
- The ultimate goal is to give you a clear understanding of where the boundaries lie.
Simple generics
- There are some cases where you want a container to hold multiple types of objects, but typically you only put one type of object into a container.
- One of the primary motivations for generics is to specify what type of object a container holds, and to have that specification backed up by the compiler.
- That’s the core idea of Java generics: You tell it what type you want to use, and it takes care of the details.
A tuple library
- One of the things you often want to do is return multiple objects from a method call.
- A tuple is simply a group of objects wrapped together into a single object.
- Each object in the tuple can be of a different type.
- The longer-length tuples can be created with inheritance.
- Because of generics, you can easily create any tuple to return any group of types, just by writing the expression.
Generic interfaces
- The generator knows how to create new objects without any extra information.
- Typically, a generator just defines one method, the method that produces new objects.
- Using generics with interfaces is no different than using generics with classes.
- One of the limitations of Java generics is that you cannot use primitives as type parameters.
Generic methods
- A generic method allows the method to vary independently of the class.
- If it’s possible to make a method generic rather than the entire class, it’s probably going to be clearer to do so.
- With a generic method, you don’t usually have to specify the parameter types, because the compiler can figure that out for you.
Leveraging type argument inference
- Type argument inference in a generic method can produce some simplification.
- Type argument inference eliminates the need to repeat the generic parameter list.
Explicit type specification
- It is possible to explicitly specify the type in a generic method, although the syntax is rarely needed.
Anonymous inner classes
- Generics can also be used with inner classes and anonymous inner classes.
The mystery of erasure
- All you find out is the identifiers that are used as the parameter placeholders.
- There’s no information about generic parameter types available inside generic code.
- You just can’t know the actual type parameter(s) used to create a particular instance.
- Any specific type information is erased when you use a generic.
- Understanding erasure and how you must deal with it will be one of the biggest hurdles you will face when learning Java generics
The C++ approach
- Writing this kind of code in C++ is straightforward because when a template is instantiated, the template code knows the type of its template parameters.
- We must assist the generic class by giving it a bound that tells the compiler to only accept types that conform to that bound.
- A generic type parameter erases to its first bound.
- The compiler actually replaces the type parameter with its erasure.
- Generics are only useful when you want to use type parameters that are more "generic" than a specific type.
- The type parameters and their application in useful generic code will usually be more complex than simple class replacement.
Migration compatibility
- It is a compromise in the implementation of Java generics, necessary because generics were not made part of the language from the beginning.
- Erasure reduces the "genericity" of generics.
- The generic types are present only during static type checking, after which every generic type in the program is erased by replacing it with a non-generic upper bound.
- The core motivation for erasure is that it allows generified clients to be used with non-generified libraries, and vice versa.
- Erasure enables this migration towards generics by allowing non-generic code to coexist with generic code.
- To achieve migration compatibility, each library and application must be independent of all the others regarding whether generics are used.
The problem with erasure
- Erasure allows existing nongeneric client code to continue to be used without change, until clients are ready to rewrite code for generics.
- You must constantly be reminding yourself that it only appears that you have type information about a parameter.
The action at the boundaries
- Using Array.newInstance( ) is the recommended approach for creating arrays in generics.
- Even though erasure removes the information about the actual type inside a method or class, the compiler can still ensure internal consistency in the way that the type is used within the method or class.
- What matters at run time is the boundaries: the points where objects enter and leave a method.
Compensating for erasure
- Sometimes you must compensate for erasure by introducing a type tag. This means you explicitly pass in the Class object for your type so that you can use it in type expressions.
- The compiler ensures that the type tag matches the generic argument.
Creating instances of types
- If you use a type tag, you can use newlnstance( ) to create a new object of that type.
Arrays of generics
- You can’t create arrays of generics. The general solution is to use an ArrayList everywhere that you are tempted to create an array of generics.
- You can define a reference in a way that makes the compiler happy. This does in fact compile, but it won’t run.
- The only way to successfully create an array of a generic type is to create a new array of the erased type, and cast that.
- There’s no way to subvert the type of the underlying array, which can only be Object[].
- It’s best to not issue any kind of message from the compiler unless the programmer must do something about it.
- If certain idioms appear in the Java library sources, that’s not necessarily the right way to do it.
Bounds
- Bounds allow you to place constraints on the parameter types that can be used with generics.
- This allows you to enforce rules about the types that your generics can be applied to.
- A potentially more important effect is that you can call methods that are in your bound types.
- If you are able to constrain that parameter to be a subset of types, then you can call the methods in that subset.
- It’s important for you to understand that extends has a significantly different meaning in the context of generic bounds than it does ordinarily.
Wildcards
- It’s clear that the array objects can preserve the rules about the type of objects they contain.
- Because arrays are completely defined in the language and can thus have both compile-time and runtime checks built in.
- But with generics, the compiler and runtime system cannot know what you want to do with your types and what the rules should be.
How smart is the compiler?
- It’s up to the generic class designer to decide which calls are "safe," and to use Object types for their arguments.
- To disallow a call when the type is used with wildcards, use the type parameter in the argument list.
Contravariance
- You say that the wildcard is bounded by any base class of a particular class, by specifying <? super MyClass> or even using a type parameter: <? super T>
- You can thus begin to think of subtype and supertype bounds in terms of how you can "write" (pass into a method) to a generic type, and "read" (return from a method) from a generic type.
- If you can get away with a static generic method, you don’t necessarily need covariance if you’re just reading.
Unbounded wildcards
- The unbounded wildcard <?> appears to mean "anything," and so using an unbounded wildcard seems equivalent to using a raw type.
- can be thought of as a decoration.
- When you are dealing with multiple generic parameters, it’s sometimes important to allow one parameter to be any type while establishing a particular type for the other parameter.
- List actually means "a raw List that holds any Object type," whereas List<?> means "a non-raw List of some specific type, but we just don’t know what that type is."
- So anytime you have a raw type, you give up compile-time checking.
- The benefit of using exact types instead of wildcard types is that you can do more with the generic parameters.
- Using wildcards allows you to accept a broader range of parameterized types as arguments.
Capture conversion
- The unspecified wildcard type is captured and converted to an exact type.
- Capture conversion only works in situations where, within the method, you need to work with the exact type.
Issues
No primitives as type parameters
- Autoboxing doesn’t apply to arrays.
Implementing parameterized interfaces
- A class cannot implement two variants of the same generic interface.
Casting and warnings
- Using a cast or instanceof with a generic type parameter doesn’t have any effect.
Overloading
- Overloading the method produces the identical type signature because of erasure.
Self-bounded types
Curiously recurring generics
- You can’t inherit directly from a generic parameter. However, you can inherit from a class that uses that generic parameter in its own definition.
- Your class appears, rather curiously, in its own base class.
- "I’m creating a new class that inherits from a generic type that takes my class name as its parameter."
- Generics in Java are about arguments and return types, so it can produce a base class that uses the derived type for its arguments and return types.
- This is the essence of CRG: The base class substitutes the derived class for its parameters.
Self-bounding
- Self-bounding takes the extra step of forcing the generic to be used as its own bound argument.
- The type parameter must be the same as the class being defined.
- The self-bounding idiom is not enforceable.
- The self-bounding constraint serves only to force the inheritance relationship.
- If you use self-bounding, you know that the type parameter used by the class will be the same basic type as the class that’s using that parameter.
Argument covariance
- A derived type method should be able to return a more specific type than the base type method that it’s overriding.
- Without self-bounding, you overload on argument types. If you use self-bounding, you only end up with one version of a method, which takes the exact argument type.
Dynamic type safety
- Because you can pass generic containers to pre-Java SE5 code, there’s still the possibility that old-style code can corrupt your containers.
- A checked container will throw a ClassCastException at the point you try to insert an improper object, as opposed to a pre-generic (raw) container which would inform you that there was a problem when you pulled the object out.
- It’s fine to put derived-type objects into a checked container that is checking for the base type.
Exceptions
- A catch clause cannot catch an exception of a generic type, because the exact type of the exception must be known at both compile time and run time.
- A generic class can’t directly or indirectly inherit from Throwable.
- Type parameters may be used in the throws clause of a method declaration.
Mixins
- The fundamental concept is that of mixing in capabilities from multiple classes in order to produce a resulting class that represents all the types of the mixins.
- One value of mixins is that they consistently apply characteristics and behaviors across multiple classes.
- Mixins have part of the flavor of aspect-oriented programming (AOP), and aspects are often suggested to solve the mixin problem.
Mixins in C++
- A more interesting and elegant approach to mixins is using parameterized types, whereby a mixin is a class that inherits from its type parameter.
- You can think of a mixin as a function that maps existing classes to new subclasses.
- Java generics don’t permit this. Erasure forgets the base-class type, so a generic class cannot inherit directly from a generic parameter.
Mixing with interfaces
- A commonly suggested solution is to use interfaces to produce the effect of mixins.
Using the Decorator pattern
- Decorators are often used when, in order to satisfy every possible combination, simple subclassing produces so many classes that it becomes impractical.
- Decorator specifies that all objects that wrap around your initial object have the same basic interface.
- Decorators are implemented using composition and formal structures, whereas mixins are inheritance-based.
- You could think of parameterized-type-based mixins as a generic decorator mechanism that does not require the inheritance structure of the Decorator design pattern.
- A significant drawback to Decorator is that it only effectively works with one layer of decoration (the final one), and the mixin approach is arguably more natural.
Mixins with dynamic proxies
- With a dynamic proxy, the dynamic type of the resulting class is the combined types that have been mixed in.
Latent typing
- Code that doesn’t care what type it works with can indeed be applied everywhere, and is thus quite "generic."
- A language with latent typing loosens the constraint (and produces more generic code) by only requiring that a subset of methods be implemented, not a particular class or interface.
- By not requiring a specific type, your code can be more generic.
- Latent typing is a code organization and reuse mechanism. With it you can write a piece of code that can be reused more easily than without it.
- Latent typing does not require either static or dynamic type checking.
- It initially seems that Java’s generic mechanism is "less generic" than a language that supports latent typing.
Using function objects as strategies
- Strategy design pattern, which produces more elegant code because it completely isolates "the thing that changes" inside of a function object.
- A function object is an object that in some way behaves like a function.
- They can be passed around, and they can also have state that persists across calls.
- We are creating function objects which perform adaptation, and they are being passed into methods to be used as strategies.
Summary: Is casting really so bad?
- Without the Java SE5 generic version of the container, you put Objects in and you get Objects out.
- Type-safe containers come as a side effect of the ability to create more general purpose code.
- It is fairly easy to write truly generic "holder" classes (which the Java containers are), but to write generic code that manipulates its generic types requires extra effort, on the part of both the class creator and the class consumer, who must understand the concept and implementation of the Adapter design pattern.
- Introducing any kind of generic mechanism in a later version of a language, after that language has come into general use, is a very, very messy proposition, and one that cannot be accomplished without pain.