...the narrow answer to your question is, when you upcast an object into Object, then necessarily you have to downcast it to the specific type later -- unless you're going to use the Object methods like toString, or else do an end-run around the compiler's type checking with runtime introspection (e.g. Reflection).
When you upcast an object (e.g. something.setAsObject("a string")
), you lose type information about that object. You can get it back in various ways, but generics is how to carry the type on for the compiler to know what to expect later, and doesn't require the downcast back to what it was before.
...
Java generics open the door for parametric polymorphism, which is a different type of polymorphism than we usually talk about in OO. It allows for creating "container" or "wrapper" types that add behaviors independent of underlying types. As wikipedia puts it, " a function or a data type can be written generically so that it can handle values identically without depending on their type"
For example, for a List<T>
, what the <T>
says is that a list works the same regardless of whether it's a List<String>
or a List<Integer>
. You can perform the same operations, and when you're done you can retrieve objects of whatever type the list contains.
So, in a funny way, generics allow a type to be unaware of the underlying type. A List<T>
doesn't know, doesn't care, what type it contains. An Observer<T>
doesn't know, doesn't care, what type it's observing. It just provides certain semantics on top of that type.
This allows the code to avoid a downcast. Downcasts are viewed as bad in OO, because they raises the possibility of a runtime class cast exception -- you lose the benefit of type-checking by the compiler.
In your example, making your Observer interfaces generic is saying, "I don't care what type I'm observing, I'm going to provide this Observer mechanism on top of it." Seems like an apt application of generics.
(For what it's worth, when you get into the business of combining parametric polymorphism (i.e. container types that add behaviors) with chained or fluent interfaces, you can create some really powerful and elegant programmatic APIs.)
Since no one else has answered the question, I think I'll give it a go myself. I'm going to have to get a bit philosophical.
Generic programming is all about abstracting over similar types, without the loss of type information (which is what happens with object-oriented value polymorphism). In order to do this, the types must necessarily share some sort of interface (a set of operations, not the OO term) that you can use.
In object-oriented languages, types satisfy an interface by virtue of classes. Each class has its own interface, defined as part of its type. Since all classes List<T>
share the same interface, you can write code that works no matter which T
you choose. Another way to impose an interface is an inheritance constraint, and although the two seem different, they are sort of similar if you think about it.
In most object-oriented languages, List<>
is not a proper type in itself. It has no methods, and thus has no interface. It is only List<T>
that has these things. Essentially, in more technical terms, the only types you can meaningfully abstract over are those with the kind *
. In order to make use of higher-kinded types in an object-oriented world, you have to phrase type constraints in a manner consistent with this restriction.
For example, as mentioned in the comments, we can view Option<>
and List<>
as "mappable", in the sense that if you have a function, you could convert an Option<T>
into an Option<S>
, or a List<T>
into a List<S>
. Remembering that classes cannot be used to abstract over higher-kinded types directly, we instead make an interface:
IMappable<K<_>, T> where K<T> : IMappable<K<_>, T>
And then we implement the interface in both List<T>
and Option<T>
as IMappable<List<_>, T>
and IMappable<Option<_>, T>
respectively. What we've done, is using higher-kinded types to place constraints on the actual (non-higher-kinded) types Option<T>
and List<T>
. This is how it's done in Scala, though of course Scala has features such as traits, type variables, and implicit parameters that make it more expressive.
In other languages, it is possible to abstract over higher-kinded types directly. In Haskell, one of the highest authorities on type systems, we can phrase a type class for any type, even if it has a higher kind. For example,
class Mappable mp where
map :: mp a -> mp b
This is a constraint placed directly on an (unspecified) type mp
which takes one type parameter, and requires it be associated with the function map
that turns an mp<a>
into an mp<b>
. We can then write functions that constrain higher-kinded types by Mappable
just like in object-oriented languages you could place an inheritance constraint. Well, sort of.
To sum things up, your ability to make use of higher-kinded types depends on your ability to constrain them or to use them as part of type constraints.
Best Answer
Using the parametric version gives
As a random example, suppose we have a method which calculates the roots of a quadratic equation
And then you want it to work on other sorts of number like things besides
int
. You can write something likeThe issue is that this doesn't say what you want it to. It says
We can't do something like
int sol = solve(a, b, c)
ifa
,b
, andc
areint
s because we don't know that the method is going to return anint
in the end! This leads to some awkward dancing with downcasting and praying if we want to use the solution in a larger expression.Inside the function, someone might hand us a float, a bigint, and degrees and we'd have to add and multiply them together. We'd like to statically reject this because the operations between these 3 classes is going to be gibberish. Degrees are mod 360 so it won't be the case that
a.plus(b) = b.plus(a)
and similar hilarities will arise.If we use the parametric polymorphism with subtyping we can rule all this out because our type actually says what we mean
Or in words "If you give me some type which is a number, I can solve equations with those coefficients".
This comes up in a lot of other places as well. Another good source of examples are functions which abstract over some sort of container, ala
reverse
,sort
,map
, etc.