If the most common thing we do after cast with CompositeType is get "types" field then we could add Collection<AbstractType<?>> getComponentTypes() method
So 100% of the time, when we access the "types" field the code is specific to compositeType, so we would still have to test if the type is composite and we would never call the getComponentTypes for non-composite type. Hence the only thing that would change is that we would replace "if (type instanceof CompositeType)" by "if (type.isCompositeType())" and "((CompositeType)type).types" by "type.getComponentTypes()" which is imho exactly the same thing (allergic reaction to instanceof/casts put aside). Except that in the meantime you'll have polluted the interface of AbstractType with 2 new methods. And it is less safe because if someone writes a custom type that happens to change the implementation of those methods, it will almost surely be a bug. So I'm not convinced at all by that idea.
But don't get me wrong, if I were to redesign C* from the ground up, I would probably make composite a more "native" notion and everything would be a composite (with potentially only 1 component) and composites wouldn't be a subclass of AbstractType at all. But this is not really an option, at least not on the short term.
On a more general "design" level, in the current code, an AbstractType in general don't have "components types" so I don't think we should add a getComponentTypes() method. But since all that has nothing to do with this ticket, if you think it would be a good idea, I can only encourage you to open a separate ticket on which we could have a more focused discussion.
And why do we need the common ancestor
Because we want Operation.Set and 'List<Value> columnValues' in UpdateStatement to be able to contain both Term and collection Literal (i.e. Value is a marker interface basically).
List/Set/Map don't have anything in common with Term
That's not what I said. And in fact Value defines the asList() method that both Term and List/Set/Map implement. What I said is that List/Set/Map also have most of their methods that don't apply to Term and Term have most of his method that don't apply to List/Set/Map. Having the intersection non-empty don't mean one is contain in the other.
Note that I'm not saying it's the only design. We probably could duplicate Operation.Set to have one applying to Term and one applying to collection literals and split UpdateStatement.columnValues into two lists, but imho having the Value interface to avoid the code duplication is cleaner.