How does a HashSet use a HashMap internally in Java, and what does that explain?
A HashSet does not have its own storage. Internally it holds a HashMap whose keys are the elements you add and whose values are a single shared dummy object. That is the whole trick, and it explains most of the behaviour people find surprising about sets.
Because the elements are the map's keys, membership is decided by equals and hashCode. Adding an element you have already added does not create a second entry, because the map's put finds an existing key that is equal to it. That is why the add method returns a boolean: true if the element was new, false if it was already there. No set implementation could offer that API without the same trick.
It also explains the ordering guarantees. A plain HashSet inherits the HashMap's lack of ordering, so iteration order depends on the hash codes. A LinkedHashSet wraps a LinkedHashMap, which keeps the buckets in insertion order. And a TreeSet is a different animal entirely — it uses a TreeMap, so it keeps its elements sorted rather than hashed, and it is the only one of the three whose membership test is O(log n) rather than O(1).
Two more consequences follow from the design. A HashSet permits exactly one null element, because a HashMap permits exactly one null key. And because the dummy value is shared, iterating a HashSet gives you the keys, not the values — there is no value to give you.