I want to implement my own Pair template class which will be used as the key in a HashMap or possibly other collections. What I have done so far is this:
import java.util.Objects;
public class Pair<T1, T2> {
public T1 first;
public T2 second;
public Pair(T1 first, T2 second) {
this.first = first;
this.second = second;
}
@Override
public boolean equals(Object obj) {
if (obj == this) {
return true;
}
if (!(obj instanceof Pair)) {
return false;
}
Pair<T1, T2> other = (Pair<T1, T2>) obj;
return this.first.equals(other.first) && this.second.equals(other.second);
}
@Override
public int hashCode() {
return Objects.hash(first, second);
}
}
IntelliJ throws a warning that says:
Unchecked cast: 'java.lang.Object' to 'gr.uoa.di.madgik.kbt.utils.Pair<T1,T2>'
Is there something I can do about it?
Are there are any other problems with my implementation?
4 Answers 4
Only one nitpick:
return this.first.equals(other.first) && this.second.equals(other.second);
The above will throw NullPointerException
if this.first
or this.second
is null
.
Consider this, instead:
Pair<T1, T2> other = (Pair<T1, T2>) obj;
return Objects.equals(first, other.first) &&
Objects.equals(second, other.second);
The idea is that Objects.equals
deals with the null
values also.
DON'T DO IT!
You should know that the reason why Java standard libraries do not have a Pair
class is because people would abuse it and use it EVERYWHERE to represent anything that has two things associated to each other.
This would make code horrible to read and maintain. People would put pairs into pairs and create freakish binary trees where no name gives any indication whatsoever to what each piece of data represents.
Inasted of making the exact error that was rejected by quite a smart bunch of software architects, you should reject it too and instead make classes that represent your data.
So if your pair represents cartesian coordinates, name your class Coordinate
instead. If it's a key-value pair, name it KeyValuePair
. And so on.
Now that we have the important part out of the way, "a pair" is just a set with hard coded size of 2. If you implement your class so that it has an order between the values, you create something that is no longer just a pair and you should name it accordingly. If you are hell bent on implementing a pair, your equals method should take into account that the order of the elements should not matter.
-
\$\begingroup\$ (Considering the last sentence of your answer.) Point. \$\endgroup\$coderodde– coderodde2022年01月17日 13:52:00 +00:00Commented Jan 17, 2022 at 13:52
-
1\$\begingroup\$ Fully agree with the danger coming with misuse of pairs you described (I believe its called "primitive obsession" or "under-abstraction"). I still find pairs useful in a low-level, functional, stream-like processing of data, where pair is used to do some intermediary operation (so its not a result) - like when mapping between some structures, grouping them to do some filtering etc. Tuples would be better than pairs for this but pair is often good enough. \$\endgroup\$Azahe– Azahe2022年01月18日 09:22:25 +00:00Commented Jan 18, 2022 at 9:22
-
\$\begingroup\$ Especially now that records exist so that the overhead is less. \$\endgroup\$tgdavies– tgdavies2022年01月19日 04:48:14 +00:00Commented Jan 19, 2022 at 4:48
-
\$\begingroup\$ Everithing can be abused, so if somebody wants to use Pair class to represent anything that has two things associated to each other, you can´t do much agains it. \$\endgroup\$convert– convert2022年02月07日 16:57:15 +00:00Commented Feb 7, 2022 at 16:57
-
\$\begingroup\$ @convert Sure thing, but that's exactly why we are here in Code Review. To learn about the bad habits and stop doing them. \$\endgroup\$TorbenPutkonen– TorbenPutkonen2022年02月08日 05:17:14 +00:00Commented Feb 8, 2022 at 5:17
- You can compare your implementation with
java.util.KeyValueHolder
- Consider disallowing null keys and values - depends on the context if can be done, but if its sort of "clean slate" state I'd definitely go for it. A lot has been said about nulls, their pros and cons, but main benefit of removing them (for me) is that it makes the code simpler as it can express less states (e.g. you would entirely avoid having to check nulls in equals/hashcode).
- Consider eliminating mutability. Again, immutable code is generally simpler and easier to understand (personally I consider immutable solution default and only go to mutable solution when actually needed). Currently anyone holding reference to the pair object can modify it without any thread safety/consistency constrains.
- Consider how would the code be used, and, if additional abstraction layer is needed. E.g. if you want the pair to be mutable it is generally better to hide internal state behind abstraction layer (getter/setter) - it is not important tho if you are going to use it only in your project (then I would wait with adding getters/setters to the moment when they are needed as modern IDEs make it completely automatic), but, if it were part of library to be used by multiple projects (and
Pair
-like objects usually are due to them being so generic) then additional abstraction layer is a very good idea to not tie your hands as a maintainer (seeMap.Entry
interface andKeyValueHolder
implementing it) - Consider implementing
toString
- value holding classes tend to be logged while debugging etc. and it could make it easier (don't do it tho if you want to store secrets in it)
-
1\$\begingroup\$ KeyValue is not a Pair. KayValue has an implied direction of association. Pair is just a set of two values with no ordering. Representing a "pair" with a "key-value" class places unwanted restrictions on the data. \$\endgroup\$TorbenPutkonen– TorbenPutkonen2022年01月17日 12:54:02 +00:00Commented Jan 17, 2022 at 12:54
-
\$\begingroup\$ I think I have not stated that one is the other, nor that one should replace the other - I only think that comparison might be valuable. However I am not sure if I agree/fully understand the second part - in multiple languages the sheer need to access values stored in pair usually implies ordering e.g. Kotlin has pair.first and pair.second, in python when a tuple is a pair we do pair[0] pair[1] etc. So as much as I think ordering might be implied in pair - I do agree that key and value are logically connected in different way than paired values (having no hierarchy). \$\endgroup\$Azahe– Azahe2022年01月18日 09:03:40 +00:00Commented Jan 18, 2022 at 9:03
-
\$\begingroup\$ Of course there is ordering in the representation of the data type. This is because is not syntactically possible to define parameters in a text file (or physical computer memory to that matter) without defining one before the other. My point is that KeyValue pair assigns a meaning to the elements that breaks the concept of pair. A pair should be equal to another if there is a permutation of the ordering where the pairs are equal. I wish I could explain this in a more clear manner. :) \$\endgroup\$TorbenPutkonen– TorbenPutkonen2022年01月18日 09:25:24 +00:00Commented Jan 18, 2022 at 9:25
You can get rid of that warning by using pattern matching for instanceof:
if (obj instanceof Pair other) {
return this.first.equals(other.first) && this.second.equals(other.second);
} else {
return false;
}
-
\$\begingroup\$ This is the 'modern' and correct way :) \$\endgroup\$Rob Audenaerde– Rob Audenaerde2022年01月19日 15:44:56 +00:00Commented Jan 19, 2022 at 15:44
Pair
class, when there are already plenty of implementations? \$\endgroup\$