Let say there is such immutable class:
public class Foo {
private final Long id;
private final String name;
private final LocalDate date;
public Foo(Long id, String name, LocalDate date) {
this.id = id;
this.name = name;
this.date = date;
}
public Long getId() {
return id;
}
public String getName() {
return name;
}
public LocalDate getDate() {
return date;
}
#Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Foo foo = (Foo) o;
return Objects.equals(getId(), foo.getId()) &&
Objects.equals(getName(), foo.getName()) &&
Objects.equals(getDate(), foo.getDate());
}
#Override
public int hashCode() {
return Objects.hash(getId(), getName(), getDate());
}
}
There is a collection of objects of this class. In some cases, it is required to distinguish only by name and in some cases by name and date.
So pass collection to java.util.Set<Foo> or create java 8 Stream<Foo> calling .distinct() method is not working for this case.
I know it is possible to distinguish using TreeSet and Comparator. It looks like this:
private Set<Foo> distinct(List<Foo> foos, Comparator<Foo> comparator) {
TreeSet<Foo> treeSet = new TreeSet<>(comparator);
treeSet.addAll(foos);
return treeSet;
}
usage:
distinct(foos, Comparator.comparing(Foo::getName)); // distinct by name
distinct(foos, Comparator.comparing(Foo::getName).thenComparing(Foo::getDate)); // distinct by name and date
But I think that is not a good way to do it.
What's the most elegant way to solve this problem?
First, let's consider your current approach, then I'll show a better alternative.
Your current approach is succinct, yet uses a TreeMap when all you need is a TreeSet. If you are OK with the O(nlogn) complexity imposed by the red/black tree structure of TreeMap, I would only change your current code to:
public static <T> Set<T> distinct(
Collection<? extends T> list,
Comparator<? super T> comparator) {
Set<T> set = new TreeSet<>(comparator);
set.addAll(list);
return set;
}
Note that I've made your method generic and static, so that it can be used in a generic way for any collection, no matter the type of its elements. I've also changed the first argument to Collection, so that it can be used with more data structures.
Also, TreeSet still has O(nlogn) time complexity because it uses a TreeMap as its backing structure.
The usage of TreeSet has 3 disadvantages: first, it sorts your elements according to the passed Comparator (maybe you don't need this); second, time complexity is O(nlogn) (which might be way too much if all you require is to have distinct elements); and third, it returns a Set (which might not be the type of collection the caller needs).
So, here's another approach that returns a Stream, which you can then collect to the data-structure you want:
public static <T> Stream<T> distinctBy(
Collection<? extends T> list,
Function<? super T, ?>... extractors) {
Map<List<Object>, T> map = new LinkedHashMap<>(); // preserves insertion order
list.forEach(e -> {
List<Object> key = new ArrayList<>();
Arrays.asList(extractors)
.forEach(f -> key.add(f.apply(e))); // builds key
map.merge(key, e, (oldVal, newVal) -> oldVal); // keeps old value
});
return map.values().stream();
}
This converts every element of the passed collection to a list of objects, according to the extractor functions passed as the varargs argument.
Then, each element is put into a LinkedHashMap with this key and merged by means of preserving the initially put value (change this as per your needs).
Finally, a stream is returned from the values of the map, so that the caller can do whatever she wants with it.
Note: this approach requires that all the objects returned by the extractor functions implement the equals and hashCode methods consistently, so that the list formed by them can be safely used as the key of the map.
Usage:
List<Foo> result1 = distinctBy(foos, Foo::getName)
.collect(Collectors.toList());
Set<Foo> result2 = distinctBy(foos, Foo::getName, Foo::getDate)
.collect(Collectors.toSet());
Related
I would like to build a Sort object based on Map<Column, Direction>. I have a problem with the fact that the Sort class only has a private constructor, it just has to be created by the static method by() or and(), therefore I have a problem with initialising the sort object with the first element from the map.
private Sort buildSort(Map<WorklistColumn, Direction> columnsDirectionsmap){
Sort sort = by("wartość inicjalna której nie chcemy", Direction.Ascending);
for (Map.Entry<WorklistColumn, Direction> columnWithDirection : columnsDirectionsmap.entrySet()) {
sort.and(columnWithDirection.getKey().toString(), columnWithDirection.getValue());
}
return sort;
}
public class Sort {
private List<Column> columns = new ArrayList();
private Sort() {
}
public static Sort by(String column) {
return (new Sort()).and(column);
}
public static Sort by(String column, Direction direction) {
return (new Sort()).and(column, direction);
}
public Sort and(String name) {
this.columns.add(new Column(name));
return this;
}
public Sort and(String name, Direction direction) {
this.columns.add(new Column(name, direction));
return this;
}
Build a Sort object from a map
I think the question is about the fact that to fully configure a Sort object from your map, you need to use the first map entry in conjunction with Sort.by(), and then use all the other entries in conjunction with Sort.and(). That is, the first entry requires different handling than the rest.
There are lots of ways of dealing with that, but the one I'm going to suggest is to work directly with the iterator of the map's entry set. Something like this:
private Sort buildSort(Map<WorklistColumn, Direction> columnsDirectionsMap) {
if (columnsDirectionsMap.isEmpty()) {
throw new NoCriteriaException(); // or whatever
}
Iterator<Map.Entry<WorklistColumn, Direction>> criterionIterator =
columnsDirectionsMap.entrySet().iterator();
Map.Entry<WorklistColumn, Direction> criterion = criterionIterator.next();
Sort sort = Sort.by(criterion.key().toString(), criterion.value());
while (criterionIterator.hasNext()) {
criterion = criterionIterator.next();
sort.and(criterion.key().toString(), criterion.value());
}
return sort;
}
Do note that depending on the Map implementation involved, the order of the entries may not be easily predictable. I assume that you need control of that order for this approach to work as desired, so it's on you to choose a Map implementation that provides that. A LinkedHashMap might be suitable, for example, but probably not a HashMap.
I'm always confused about the Java Collections (set, map) remove "complex object", which I mean some self-defined class rather than just primitive type.
I'm experimenting like:
public class Main {
public static void main(String[] args) {
// set
Set<Node> set = new HashSet<>();
set.add(new Node(1,2));
set.add(new Node(3,4));
System.out.println(set);
set.remove(new Node(1,2));
System.out.println(set + "\n");
// tree set
TreeSet<Node> tset = new TreeSet<>((a, b) -> a.name - b.name);
tset.add(new Node(1,2));
tset.add(new Node(3,4));
System.out.println(tset);
tset.remove(new Node(1,2));
System.out.println(tset);
}
}
class Node {
int name;
int price;
Node(int name, int price) {
this.name = name;
this.price = price;
}
}
In the example above, the printout would be:
Set:
[Node#5ba23b66, Node#2ff4f00f]
[Node#5ba23b66, Node#2ff4f00f]
TreeSet:
[Node#48140564, Node#58ceff1]
[Node#58ceff1]
Obviously, the general Set can't remove new Node(1, 2), which is treated as a different object. But interestingly the TreeSet can remove, which I think because the hashing code is based on the lambda comparator I defined here?
And if I change to remove new Node(1, 6), interestingly it's the same printout, where obviously the remove in TreeSet is based on only the name value.
I think I still lack of deep understanding of how Set build up hashing and how comparator would affect this.
For HashMap and HashSet, you need to overwrite hashCode() and equals(Object), where if two objects are equal, they should have equal hash codes. E.g., in your case, you could implement it like this:
#Override
public boolean equals(Object o) {
if (o == null || getClass() != o.getClass()) {
return false;
}
Node node = (Node) o;
return name == node.name && price == node.price;
}
#Override
public int hashCode() {
return Objects.hash(name, price);
}
For TreeMap and TreeSet, the notion of equality is based on a comparison (whether the class implements Comparable, or you supply a custom Comparator). In the code you provided, you have a custom Comparator that only takes the name in to consideration, so it would consider any two Nodes with the same name as being equal, regardless of their price.
javadoc comes to rescue
https://docs.oracle.com/javase/7/docs/api/java/util/Set.html#remove(java.lang.Object)
Removes the specified element from this set if it is present (optional
operation). More formally, removes an element e such that (o==null ?
e==null : o.equals(e)), if this set contains such an element. Returns
true if this set contained the element (or equivalently, if this set
changed as a result of the call). (This set will not contain the
element once the call returns.)
So just change your Node class and override equals(Object o) method with your own.
I am trying to compare two objects of same class and the goal is to compare them as well as identify which fields didn't match.
Example of my domain class
#Builder(toBuilder=true)
class Employee {
String name;
int age;
boolean fullTimeEmployee;
}
Two objects
Employee emp1 = Employee.builder().name("john").age(25).fullTime(false).build();
Employee emp2 = Employee.builder().name("Doe").age(25).fullTime(true).build();
Comparing both objects
int result = Comparator.comparing(Employee::getName, Comparator.nullsFirst(Comparator.naturalOrder()))
.thenComparing(Employee::getAge, Comparator.nullsFirst(Comparator.naturalOrder()))
.thenComparing(Employee::isFullTimeEmployee, Comparator.nullsFirst(Comparator.naturalOrder()))
.compare(emp1, emp2);
result will be 0 because name & fullTime fields are not matching with each other.
But I also want to produce a list of fields which didn't match.. like below
List<String> unmatchedFields = ["name","fulltimeEmployee"];
Can I do it in a nicer way, other than bunch of if() else
Check out DiffBuilder. It can report which items are different.
DiffResult<Employee> diff = new DiffBuilder(emp1, emp2, ToStringStyle.SHORT_PREFIX_STYLE)
.append("name", emp1.getName(), emp2.getName())
.append("age", emp1.getAge(), emp2.getAge())
.append("fulltime", emp1.getFulltime(), emp2.getFulltime())
.build();
DiffResult has, among other things, a getDiffs() method that you can loop over to find what differs.
First of all, I don't think you understand how comparing().thenComparing() works. The result in your case won't be 0. The comparator compares the first statement, if they are equal it goes to the thenComparing() and so on. The result will be 0 if and only if all comparison()/thenComparing() are also equal. I actually wrote about this a few days ago: http://petrepopescu.tech/2021/01/simple-collection-manipulation-in-java-using-lambdas/
Anyway, back to your question, I think you are looking for an equal() where it also returns what fields were not equal. Here is a quick prototype.
public class Pair<T, U> {
private Function<? super T, ? extends Comparable> function;
private String fieldName;
public Pair(Function<? super T, ? extends Comparable> function, String fieldName) {
this.function = function;
this.fieldName = fieldName;
}
}
And the actual comparator:
public class MyComparator {
List<Pair> whatToCompare = Arrays.asList(
new Pair<Employee, String>(Employee::getName, "name"),
new Pair<Employee, String>(Employee::getAge, "age"),
new Pair<Employee, Double>(Employee::isFullTimeEmployee, "fullTimeEmployee")
);
public List<String> compare(Employee e1, Employee e2) {
List<String> mismatch = new ArrayList<>();
for(Pair pair:whatToCompare) {
int result = Comparator.comparing(pair.getFunction()).compare(e1, e2);
if (result != 0) {
mismatch.add(pair.getFieldName());
}
}
return mismatch;
}
}
Comparator itself has no ability to discover out the differences, it only compares two objects of the same type and returns -1, 0 or 1. This is the interface contract.
To discover the actual differences, you need to use Reflection API to get the fields, compare the equality of their real values of two passed objects and let the differences be listed. Here is a generic implementation:
public class Difference<T> {
T left;
T right;
//all-args constructor omitted
public Set<String> differences() throws IllegalAccessException {
// fieldName-field as a key-value for the left and right instances
Map<String, Field> leftFields = Arrays.stream(left.getClass().getDeclaredFields())
.peek(f -> f.setAccessible(true))
.collect(Collectors.toMap(Field::getName, Function.identity()));
Map<String, Field> rightFields = Arrays.stream(left.getClass().getDeclaredFields())
.peek(f -> f.setAccessible(true))
.collect(Collectors.toMap(Field::getName, Function.identity()));
// initialize Set as long as two fields cannot have a same name
Set<String> differences = new HashSet<>();
// list the fields of the left instance
for (Entry<String, Field> entry: leftFields.entrySet()) {
String fieldName = entry.getKey();
Field leftField = entry.getValue();
// find the right instance field matching the name
Field rightField = rightFields.get(fieldName);
// compare their actual values and add among the differences if they aren't equal
if (!leftField.get(left).equals(rightField.get(right))) {
differences.add(fieldName);
}
}
return differences;
}
}
Employee emp1 = Employee.builder().name("john").age(25).fullTime(false).build();
Employee emp2 = Employee.builder().name("Doe").age(25).fullTime(true).build();
Set<String> differences = new Difference<Employee>(emp1, emp2).differences();
System.out.println(differences);
[name, fullTimeEmployee]
I have a class called LineUp, it is an ArrayList of a class called Event. An Event has three values a String Act, a Venue (it's own class), and an int Session.
An Event might be declared like this.
Event e1 = new Event("Foo Fighters", northstage, "1")
LineUp is an ArrayList, Event being elements like e1.
In my LineUp class I have to make an invariant that checks that every Event contained within the ArrayList lineup has a unique Venue and Session. Because this assignment requires that I follow specification exactly, it is irrelevant whether the combination of Act, Venue and Session is unique, to follow specification I must /only/ ensure that Venue and Session are unique.
How do I check for duplicates but only of specific values within an ArrayList?
Thank-you.
If you only need to check if there are duplicates (considering venue-session pairs), you could create a helper Pair class with only the attributes that matter in this specific case. Then map the events to Pair objects, remove the duplicates and check if the size is the same.
You could, for example, create a nested class inside LineUp:
class LineUp {
private List<Event> events = new ArrayList<>();
private static final class Pair<U, V> {
final U first;
final V second;
Pair(U first, V second) {
this.first = first;
this.second = second;
}
#Override
public boolean equals(Object o) {
if (this == o) {
return true;
}
if (!(o instanceof Pair)) {
return false;
}
Pair<U, V> that = (Pair<U, V>) o;
return Objects.equals(this.first, that.first)
&& Objects.equals(this.second, that.second);
}
#Override
public int hashCode() {
return Objects.hash(this.first, this.second);
}
}
// rest of the LineUp class
}
Then create a method that return false if there are any duplicates:
public boolean duplicateVenueSessions() {
// Map each Event to a Pair<Venue, Integer> and remove the duplicates
long numDistinct = this.events.stream()
.map(e -> new Pair<>(e.venue, e.session))
.distinct()
.count();
// return false if the original number of events is different from the
// number of distinct events considering only venue and session values
return this.events.size() != numDistinct;
}
If can't use Java 8, you could use a Set instead:
public boolean duplicateVenueSessions() {
Set<Pair<String, Integer>> distinct = new HashSet<>();
for (Event e : this.events) {
Pair<String, Integer> venueSession = new Pair<>(e.venue, e.session);
if (distinct.contains(venueSession)) {
return true;
}
distinct.add(venueSession);
}
return false;
}
If I understand everything correctly you can use Map in a method to store values
Map<Map<Venue, Integer>, Act> lineup = new HashMap<>();
it incorporated uniqueness of Venue-Session pair.
However as Venue is your own class, you will have to implement equals() and hashCode() methods for Venue in order for this solution to work
EDIT:
what I meant wa something like this:
Map<Map<Integer, Venue>,String> uniqueMap = new HashMap<>();
for (Event event: events) { // assuming events is ArrayList
Map<Integer, Venue> sessionVenueMap = new HashMap<>();
sessionVenueMap.put(event.getSession(), event.getVenue());
//check if we stored this pair in our cool map
if (uniqueMap.get(sessionVenueMap) == null) {
//if not
//store this in our uniqieMap in our method
uniqueMap.put(sessionVenueMap, event.getAct);
sessionVenueMap.put(event.getSession(), event.getVenue);
} else {
// if map has this pair
// then it is not unique
return false;
}
venueSessionMap.put(.getVenue(); event.getSession();
}
return true;
code is not tested though, but you get the general idea, although it seems quite complex. probably there is a better solution
Sorry for my English.
I have something like this:
class SomeClass<T> {
private List<T> elements;
private List<Comparator<T>> comparators;
public SomeClass(List<T> elements) {
this.elements = elements;
this.comparators = new ArrayList<Comparator<T>>();
}
public void addComparator(Comparator<T> comparator) {
comparators.add(comparator);
}
public List<T> getSortedElements() {
return sorted(new ArrayList<T>(elements));
}
// !!! At the moment I use very inefficient way
// !!! to sort elements by several comparators.
private List<T> sorted(List<T> toSort) {
// I need to use stable sort.
// Collections.sort(...) is guaranteed to be stable
for (Comparator<T> comparator : comparators) {
Collections.sort(toSort, comparator);
}
return toSort;
}
}
Can I optimize sorted() method ? Maybe this can be done using a single sorting of collection by using combined comparator? (instead of sorting several times with each of comparators)
(But I have to use a stable sort, and last-added comparators have a higher priority than comparators added at the beginning)
UPDATE:
I use sorting by several comparators to sort elements by different attributes (for example, by priority, name, etc.)
The problem is that when you sort it the way you are doing, subsequent sorts completely discard the sorting done by previous sorts.
I think what you're wanting is to say something like "if the first comparator finds them equal, sort by the next". You can do that like this:
class SomeClass<T> {
private List<T> elements;
private List<Comparator<T>> comparators;
private final Comparator<T> combined = new Comparator<T>() {
#Override
public int compare(T e1, T e2) {
for(Comparator<T> c : comparators) {
int result = c.compare(e1, e2);
if(result != 0)
return result;
}
return 0;
}
};
...
private List<T> sorted(List<T> toSort) {
Collections.sort(toSort, combined);
return toSort;
}
}
Using Guava:
static <T> Comparator<T> compound(List<Comparator<T>> comparators)
{
return Ordering.compound(comparators);
}
Given a pair of elements, the comparators in the given list are tried one after the other, in order, until one produces a non-zero result, which is returned, or there are no comparators left, in which case zero is returned.