1. Overview
In this tutorial, we’ll first understand what a triple is and then discuss how to store triple elements in a Java ArrayList.
2. What Is a Triple?
We may have heard about the Pair type, which always contains two values, for example, a key-value association. A triple is pretty similar to a pair. The only difference is a triple always has three values instead of two. For example, a 3D coordinate can be considered a triple structure: x=-100L, y=0L, z=200L.
In the 3D coordinate example, the three values in the triple have the same type: Long. However, the three value types in a triple aren’t necessarily the same. For example, name=”Lionel Messi”, birthday=24 June 1987 (Date), number=10 is another triple structure of football players. In this example, the three values in the triple have different types: String, Date, and Integer.
Next, we’ll see a more detailed triple example and discuss the proper way to store the triple objects in an ArrayList.
3. The Example: an Arithmetic Question Generator
Let’s say we want to build an arithmetic question generator for pupils. For example, “*100 + 200 = ?*” is one question. It’s composed of the first number, the second number, and an operator. Thus, we have three values. We’ll store these three parts as triples.
Also, we’ve defined the accepted operators as an Enum:
enum OP {
PLUS("+"), MINUS("-"), MULTIPLY("x");
final String opSign;
OP(String x) {
this.opSign = x;
}
}
As we can see, we only accept three operators.
The question generation logic is pretty straightforward. But first, let’s create a method to generate the questions from the three parts:
String createQuestion(Long num1, OP operator, Long num2) {
long result;
switch (operator) {
case PLUS:
result = num1 + num2;
break;
case MINUS:
result = num1 - num2;
break;
case MULTIPLY:
result = num1 * num2;
break;
default:
throw new IllegalArgumentException("Unknown operator");
}
return String.format("%d %s %d = ? ( answer: %d )", num1, operator.opSign, num2, result);
}
If we have values in the triple structure in a list, we can pass the three values to the method above and create questions. For simplicity, we’ll use unit test assertions to verify if the expected questions can be created. Let’s say we expect to generate three questions by triples:
List<String> EXPECTED_QUESTIONS = Arrays.asList(
"100 - 42 = ? ( answer: 58 )",
"100 + 42 = ? ( answer: 142 )",
"100 x 42 = ? ( answer: 4200 )");
Now, it’s time to consider the central problem: how to store the triple structure in a list?
Of course, if we only focus on this problem, we could create a QuestionInput class containing the three arguments. However, our goal is to generally store triple structures in a list, which means our solution should solve the “question generation” problem and work for “3D coordinate” and “football player” examples too.
Usually, two ideas may come up to store triples in a list:
- List
- List<Triple<…>> – Creating a generic Triple class
So next, let’s see them in action. Further, we’ll discuss their pros and cons.
4. Triples as Lists
We know we can add elements of any type to a raw list. So next, let’s see how to store triples as lists.
4.1. Storing a Triple Structure as a List of Three Elements
For each triple structure, we can create a list. Then add the three values to the list. So next, let’s store the math question triples as lists:
List myTriple1 = new ArrayList(3);
myTriple1.add(100L);
myTriple1.add(OP.MINUS);
myTriple1.add(42L);
List myTriple2 = new ArrayList(3);
myTriple2.add(100L);
myTriple2.add(OP.PLUS);
myTriple2.add(42L);
List myTriple3 = new ArrayList(3);
myTriple3.add(100L);
myTriple3.add(OP.MULTIPLY);
myTriple3.add(42L);
List<List> listOfTriples = new ArrayList<>(Arrays.asList(myTriple1, myTriple2, myTriple3));
As the code above shows, we create three raw ArrayList objects to carry the three triples. In the end, we add the three raw lists to the outer list listOfTriples.
4.2. A Word About Type Safety
Raw usage of the lists allows us to put values in different types to the list, for example, Long and OP. Therefore, this approach can be used for any triple structure.
However, on the other hand, we lost the type safety when we used a list in raw. An example can explain it quickly:
List oopsTriple = new ArrayList(3);
oopsTriple.add("Oops");
oopsTriple.add(911L);
oopsTriple.add("The type is wrong");
listOfTriples.add(oopsTriple);
assertEquals(4, listOfTriples.size());
As we can see, we’ve created the oopsTriple list carrying a different triple structure. Also, listOftriples accepts it without any complaints when we add oopsTriple to it.
So now, listOfTriples contains two different kinds of triples: Long, OP, Long and String, Long, String. Therefore, when we use the triples in the listOfTriples list, we must check if the triple is in the expected type.
4.3. Using Triples in the List
Now that we understand the pros and cons of the “triples as lists” approach, let’s see how to use the triples in listOfTriples to generate arithmetic questions:
List<String> questions = listOfTriples.stream()
.filter(
triple -> triple.size() == 3
&& triple.get(0) instanceof Long
&& triple.get(1) instanceof OP
&& triple.get(2) instanceof Long
).map(triple -> {
Long left = (Long) triple.get(0);
String op = (String) triple.get(1);
Long right = (Long) triple.get(2);
return createQuestion(left, op, right);
}).collect(Collectors.toList());
assertEquals(EXPECTED_QUESTIONS, questions);
As the code snippet above shows, we’ve used Java Stream API‘s map() to transform the list of triples into a list of generated questions. However, as the type of the inner raw lists isn’t guaranteed, we must check if the elements in each raw list conform to Long, OP, and Long. So, before we call the map() method, we’ve called filter() to skip unrecognized triples, such as the String, Long, and String one.
Further, because of the list’s raw usage, no contract can guarantee that the three elements in the raw list are in types: Long, OP, and Long. Therefore, we must explicitly cast the list elements to the desired types before passing them to the createQuestion() method.
If we run the test, it passes. So this approach solves our problem. Its advantage is obvious. We can store any triple structures in a list without creating new classes. But we lost the type safety. Therefore, we must perform type checks before we use the raw values. Moreover, we must cast the values to the desired types.
Imagine that we use this approach on many different triple structures in our application, then we have to put a lot of effort into type-checking and casting. It makes the code less readable, hard to maintain, and error-prone. Therefore, this approach isn’t recommended.
5. Creating a Generic Triple Class
Now that we understand the pros and cons of the “triples as lists” approach, let’s try to find a straightforward and type-safe way to store triples in a list.
5.1. The Generic Triple Class
One benefit Java generics brings is type-safety. So next, let’s create a generic Triple class:
public class Triple<L, M, R> {
private final L left;
private final M middle;
private final R right;
public Triple(L left, M middle, R right) {
this.left = left;
this.middle = middle;
this.right = right;
}
public L getLeft() {
return left;
}
public M getMiddle() {
return middle;
}
public R getRight() {
return right;
}
}
The class is pretty straightforward. In this example, we’ve made Triple immutable. If it’s required to be mutable, we can remove the final keywords and add the corresponding setters.
5.2. Initializing Triples and Storing in a List
Next, let’s create three triple objects and add them to the listOfTriples List:
Triple<Long, OP, Long> triple1 = new Triple<>(100L, OP.MINUS, 42L);
Triple<Long, OP, Long> triple2 = new Triple<>(100L, OP.PLUS, 42L);
Triple<Long, OP, Long> triple3 = new Triple<>(100L, OP.MULTIPLY, 42L);
List<Triple<Long, OP, Long>> listOfTriples = new ArrayList<>(Arrays.asList(triple1, triple2, triple3));
As we can see, as our generic Triple class has type parameters, there are no raw usages in the code above. Furthermore, the type is safe.
Next, let’s test what happens if we create an invalid Triple object and attempt to add it to the list:
Triple<String, Long, String> tripleOops = new Triple<>("Oops", 911L, "The type is wrong");
listOfTriples.add(tripleOops);
If we add the two lines above, the code doesn’t compile:
java: incompatible types:
com...Triple<...String, ...Long, ...String> cannot be converted to com...Triple<...Long, ...OP, ...Long>
Therefore, this type-safe approach protects us from falling into the type-error trap.
5.3. Using the Triple Elements
As the type is safe, we can directly use the values without performing any type-checking and type-casting:
List<String> questions = listOfTriples.stream()
.map(triple -> createQuestion(triple.getLeft(), triple.getMiddle(), triple.getRight()))
.collect(Collectors.toList());
assertEquals(EXPECTED_QUESTIONS, questions);
The test passes if we give it a run. Compared to the “triples as lists” approach, the code above is cleaner and much more readable.
6. Conclusion
In this article, we’ve explored how to store triples in a list by examples. We’ve discussed why we should create a generic Triple type instead of using triples as lists.
As always, all code snippets presented in the article are available over on GitHub.