ChangeSet.getChanges is read only because the code has not necessarily been tested with dynamic changes to the list of changes in it. There is a ChangeSet.addChanges method to add a change but not anything to remove it currently. The list returned is not modifiyable but the Changes contained in it can be edited.
Changes do not have identifiers because they have not been needed so far. ChangeSets have a unique identifier and are tracked by liquibase, but the change implementations are just anonymously contained in the ChangeSet. If they had a unique identifeir, it would probably just be ChangeSet.id+ChangeSet.author+ChangeSet.filePath+indexOfChange
Preconditions are only at the changeSet level because that is the level liquibase works at and tries to run as a transaction. If you want preconditions per change you will need to have just one change per changeSet (most of the time you want this anyway because DDL statements autocommit which can get liquibase into a bad state if a second change fails).
There are several ways to preview SQL generation. If you have a Change object, calling change.generateStatements(database) will return an array of statements, each of which can be passed to SqlGeneratorFactory.getInstance().generateSql(statement, database)