Oh you mean HASHTABLES. Well, duh, of COURSE they're not typed, because you should be able to put ANYTHING in them...
What?
The value of universal containers far outweighed the costs of dealing with the early early Java type system.
Code:
List list = new ArrayList();
OtherClass o = new OtherClass();
list.add(o);
MyClass m = (MyClass)list.get(0); // <-- Runtime failure with Class Cast Exception
The Java Generic system is essentially a large construct of syntax sugar within the compiler to make working with such data structures easier, but the underlying runtime itself was essentially unchanged.
Code:
List<MyClass> list = new ArrayList<MyClass>(); // This can now be abbreviated: new ArrayList<>();
OtherClass o = new OtherClass();
list.add(o); // <-- Compile time failure, 'o' is not 'MyClass'
MyClass m = list.get(0); // no manual casting needed here as before
However, when compiled, the code for these routines is identical. You could use runtime constructs such as reflection (among other things) and stuff an "OtherClass" in to the list, internally, List is still a collection of Objects, and you'd still get the runtime exception on the get(0).
C would have the same problem as early Java were you to have a set of collections on void pointers (void *). However, you would NEVER know that you'd had the wrong "class" (struct, whatever), in your list, as the casts necessary to make it work in the source code are purely compile time, and not enforced at runtime. You'd find out by some awful core dump, if you're lucky.
C++ worked around it with its templating system. Here, you have universal source code, created for each different type (since the effect of invoking a template internally compiles a new version of the source code for the specific type), rather than a single set of universal code that's type agnostic as you have in Java.
Python suffers from the same problems as Java when it comes to generic data structures, but it does not enforce typing at runtime save through dispatch. You'll discover you have the wrong object when you try to invoke a method that does not exist on the wrong instance.
Java, too, of course, will fail if you try to invoke a method on an object that doesn't support it (yes, this is possible). But Java's casting also has a runtime effect of ensuring it's can be cast, vs simply a compile time effect. In Python, of course, you don't cast at all.
The Java model works. Has it's warts, but it works. I personally prefer typed code to untyped code, especially on large codebases. Having some faith that "if it compiles, it'll work (to a point)" when doing scary refactoring is a nice bedrock. Dynamic typed systems don't offer that.