Is there a way, algorithmically, to come up with more examples of distinct strings sharing hash codes without using brute force testing of many strings, given we know the exact hashing algorithm used in each language?
Sometimes. Let's take your Java example... "Ea and FB both having a hash code of 2236" and consider the hash function (here in C++, and only for ASCII strings):
int java_hash(const std::string& s) {
int hash = 0;
for (char c : s) (hash *= 31) += c;
return hash;
}
We can see it starts by taking the value of the first character. Then for two-character strings, it multiplies that by 31 and adds the next character.
Say we generalise and explain this: we have a two-character string KL (e.g. for "Ea", K == 'E' and L == 'a'); the hash value ends up being 31K + L. Then to find a colliding key, we could try incrementing K to the next character value (e.g. 'F') which increases the hash output by 31, so to counter-balance we decrease L's character value by 31 (i.e. 'a'==ASCII 97 back to 'B'==66).
We can find yet another collision by incrementing to 'G' and decreasing to '#'.
Or, we could move in the other direction: if the new L value had been out of the valid value range for character types, we could instead decrement K and increase L by 31, still having a net-0 effect on the character value, but in this particular case "Ea" would need 'E' to become 'D', and 'a' to become 128, which may or may not be valid in your string type.
If we add more characters, we can observe something interesting - the overall hash value gets more complicated, but to create a collision we can still just screw with the last two characters in the same way, making sure their impact on the overall hash value is net-zero for our colliding keys.
With better and better hash functions, this kind of analysis becomes more and more difficult, and with uncompromised cryptographic strength functions it should be impractical even for an expert mathematician. (That's why we see cryptocurrency mining having to use brute-force searches for "nonce" values in the key fields, seeking to create a desired overall hash value for the block.)