...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.)
Let's first talk about pure parametric polymorphism and get into bounded polymorphism later.
What does parametric polymorphism mean? Well, it means that a type, or rather type constructor is parameterized by a type. Since the type is passed in as a parameter, you cannot know in advance what it might be. You cannot make any assumptions based on it. Now, if you don't know what it might be, then what is the use? What can you do with it?
Well, you could store and retrieve it, for example. That's the case you already mentioned: collections. In order to store an item in a list or an array, I need to know nothing about the item. The list or array can be completely oblivious to the type.
But what about the Maybe
type? If you are not familiar with it, Maybe
is a type which maybe has a value and maybe doesn't. Where would you use it? Well, for example, when getting an item out of a dictionary: the fact that an item might not be in the dictionary is not an exceptional situation, so you really shouldn't throw an exception if the item isn't there. Instead, you return an instance of a subtype of Maybe<T>
, which has exactly two subtypes: None
and Some<T>
. int.Parse
is another candidate of something that really should return a Maybe<int>
instead of throwing an exception or the whole int.TryParse(out bla)
dance.
Now, you might argue that Maybe
is kinda-sorta like a list which can only have zero or one elements. And thus kinda-sorta a collection.
Then what about Task<T>
? It's a type which promises to return a value at some point in the future, but doesn't necessarily have a value right now.
Or what about Func<T, …>
? How would you represent the concept of a function from one type to another type if you cannot abstract over types?
Or, more generally: considering that abstraction and reuse are the two fundamental operations of software engineering, why wouldn't you want to be able to abstract over types?
So, let's now talk about bounded polymorphism. Bounded polymorphism is basically where parametric polymorphism and subtype polymorphism meet: instead of a type constructor being completely oblivious about its type parameter, you can bind (or constrain) the type to be a subtype of some specified type.
Let's go back to collections. Take a hashtable. We said above that a list doesn't need to know anything about its elements. Well, a hashtable does: it needs to know that it can hash them. (Note: in C#, all objects are hashable, just like all objects can be compared for equality. That's not true for all languages, though, and is sometimes considered to be design mistake even in C#.)
So, you want to constrain your type parameter for the key type in the hashtable to be an instance of IHashable
:
class HashTable<K, V> where K : IHashable
{
Maybe<V> Get(K key);
bool Add(K key, V value);
}
Imagine if instead you had this:
class HashTable
{
object Get(IHashable key);
bool Add(IHashable key, object value);
}
What would you do with a value
you get out of there? You can't do anything with it, you only know it's an object. And if you iterate over it, all you get yielded is a pair of something you know is an IHashable
(which doesn't help you much because it only has one property Hash
) and something you know is an object
(which helps you even less).
Or something based on your example:
class Repository<T> where T : ISerializable
{
T Get(int id);
void Save(T obj);
void Delete(T obj);
}
The item needs to be serializable because it is going to be stored on disk. But what if you have this instead:
class Repository
{
ISerializable Get(int id);
void Save(ISerializable obj);
void Delete(ISerializable obj);
}
With the generic case, if you put a BankAccount
in, you get a BankAccount
back, with methods and properties like Owner
, AccountNumber
, Balance
, Deposit
, Withdraw
, etc. Something you can work with. Now, the other case? You put in a BankAccount
but you get back a Serializable
, which has just one property: AsString
. What are you going to do with that?
There are also some neat tricks you can do with bounded polymorphism:
F-bounded quantification is basically where the type variable appears again in the constraint. This can be useful in some circumstances. E.g. how do you write an ICloneable
interface? How do you write a method where the return type is the type of the implementing class? In a language with a MyType feature, that's easy:
interface ICloneable
{
public this Clone(); // syntax I invented for a MyType feature
}
In a language with bounded polymorphism, you can do something like this instead:
interface ICloneable<T> where T : ICloneable<T>
{
public T Clone();
}
class Foo : ICloneable<Foo>
{
public Foo Clone()
{
// …
}
}
Note that this is not as safe as the MyType version, because there is nothing stopping someone from simply passing the "wrong" class to the type constructor:
class EvilBar : ICloneable<SomethingTotallyUnrelatedToBar>
{
public SomethingTotallyUnrelatedToBar Clone()
{
// …
}
}
Abstract Type Members
As it turns out, if you have abstract type members and subtyping, you can actually get by completely without parametric polymorphism and still do all the same things. Scala is heading in this direction, being the first major language that started out with generics and then trying to remove them, which is exactly the other way around from e.g. Java and C#.
Basically, in Scala, just like you can have fields and properties and methods as members, you can also have types. And just like fields and properties and methods can be left abstract to be implemented in a subclass later, type members can also be left abstract. Let's go back to collections, a simple List
, that would look something like this, if it were supported in C#:
class List
{
T; // syntax I invented for an abstract type member
T Get(int index) { /* … */ }
void Add(T obj) { /* … */ }
}
class IntList : List
{
T = int;
}
// this is equivalent to saying `List<int>` with generics
Best Answer
Fourth time is the charm
This is the same as my second answer (the one you initially checked) and I got rid of the
<OfType>
call and the enumerator.The trick here was to define a contravariant interface that pairs components and their data. I have been meaning to learn more about contravariance so this was a fun exercise, thank you.
This may be a bit of messy code at this point, but I think I've proven it can be done, and you can use the same technique but maybe clean up the structure a bit.