Niphin
Niphin

Reputation: 11

Join Custom lists in java

I have two custom lists as follows.

List<OfficeName> = [{id: 1, offname: "Office1"}{id: 2, offname: "Office2"}]
List<OfficeLocation> = [{id: 1, offlocation: "location1"}{id: 2, offlocation: "locatio2"}]

I want result as follows:

list<OfficeDetails> =  [{id: 1, offname: "Office1",offlocation: "location1" },
                        {id: 2, offname: "Office2", offlocation: "location2"}]

The first two lists needs to be joined on basis of "id" to give a new list which is equivalent to the join operation in sql tables.

My model classes are

public class OfficeName {
int id;
String  offname;
//getter and setter
}

.................

public class OfficeLocation{
int id;
String  offlocation;
//getter and setter
}

.........

Currently I am Iterating and manually adding as follows to a LinkedHashSet .

{
List<OfficeName> officeName =  new ArrayList<OfficeName>();
    onr.findById(id).forEach(officeName::add); // adding values from auto wired Repository
    
    
    List<OfficeLocation> officeLocation =  new ArrayList<OfficeLocation>();
    olr.findById(id).forEach(officeLocation::add); // adding values from auto wired Repository

  LinkedHashSet<LinkedHashSet<String>> lhs   = new LinkedHashSet<LinkedHashSet<String> >();
     OfficeName officeName1 =  new OfficeName();
     OfficeLocation officeLocation1 =  new OfficeLocation();
    
    Iterator<OfficeName> onIterator = officeName.iterator();
    Iterator<OfficeLocation> olIterator = officeLocation.iterator();
    
    while (onIterator.hasNext()) {
        officeName1 =onIterator.next();
        int idon =officeName1.getId();

        while(olIterator.hasNext()){
            officeLocation1 = olIterator.next();
            int idol = officeLocation1.getId();
        
            if(idon==idol)
            {               

                 lhs.add(new LinkedHashSet<String>(Arrays.asList( String.valueOf(officeName1.getId()),officeName1.getOffname(),officeLocation1.getOfflocation())));
                 olIterator.remove();
                 break;
            }
        }; 
    }

I am not sure whether this is correct way to achieve the same as I am new to java. In C#, this could able to achieve through data tables. Please suggest whether there is any faster way?

Upvotes: 1

Views: 87

Answers (2)

Nowhere Man
Nowhere Man

Reputation: 19565

One of the lists (e.g. locations) should be converted into a map (HashMap) by a key on which the joining should be made, in this case id field.

Then, assuming that OfficeDetails class has an all-args constructor, the resulting list may be retrieved by streaming the other list offices and mapping its contents into new OfficeDetails, filling the remaining location argument by looking up the map.

List<OfficeName> offices = Arrays.asList(
    new OfficeName(1, "Office1"), new OfficeName(2, "Office2"), new OfficeName(3, "Office3")
);
List<OfficeLocation> locations = Arrays.asList(
    new OfficeLocation(1, "Location 1"), new OfficeLocation(2, "Location 2"), new OfficeLocation(4, "Location 4")
);

Map<Integer, OfficeLocation> mapLoc = locations
        .stream()
        .collect(Collectors.toMap(
            OfficeLocation::getId,
            loc -> loc,
            (loc1, loc2) -> loc1 // to resolve possible duplicates
        ));
        
List<OfficeDetails> details = offices
        .stream()
        .filter(off -> mapLoc.containsKey(off.getId())) // inner join
        .map(off -> new OfficeDetails(
            off.getId(), off.getOffname(),
            mapLoc.get(off.getId()).getOfflocation() // look up the map
        ))
        .collect(Collectors.toList());
details.forEach(System.out::println);

Output (assuming toString is implemented in OfficeDetails):

{id: 1, offname: "Office1", offlocation: "Location 1"}
{id: 2, offname: "Office2", offlocation: "Location 2"}

If offices list is not filtered by condition mapLoc.containsKey, an implementation of LEFT JOIN is possible (when null locations are stored in the resulting OfficeDetails).

To implement RIGHT JOIN (with null office names and all available locations), a lookup map should be created for offices, and main iteration has to be run for locations list.

To implement FULL JOIN (where either name or location parts of OfficeDetails can be null), two maps need to be created and then joined:

Map<Integer, OfficeName> mapOff = offices
        .stream()
        .collect(Collectors.toMap(
            OfficeName::getId,
            off -> off,
            (off1, off2) -> off1, // to resolve possible duplicates
            LinkedHashMap::new
        ));
        
List<OfficeDetails> fullDetails = Stream.concat(mapOff.keySet().stream(), mapLoc.keySet().stream())
        .distinct()
        .map(id -> new OfficeDetails(
            id, 
            Optional.ofNullable(mapOff.get(id)).map(OfficeName::getOffname).orElseGet(()->null), 
            Optional.ofNullable(mapLoc.get(id)).map(OfficeLocation::getOfflocation).orElseGet(()->null) 
        ))
        .collect(Collectors.toList());
fullDetails.forEach(System.out::println);

Output:

{id: 1, offname: "Office1", offlocation: "Location 1"}
{id: 2, offname: "Office2", offlocation: "Location 2"}
{id: 3, offname: "Office3", offlocation: null}
{id: 4, offname: null, offlocation: "Location 4"}

Upvotes: 0

Basil Bourque
Basil Bourque

Reputation: 339382

Assuming both input lists:

  • Are distinct, with no duplicate id values in either, and…
  • Are complete, with a single object in both lists for each possible id value

… then we can get the work done with little code.

I use NavigableSet or SortedSet implementations to hold our input lists, the names and the locations. Though I have not verified, I assume being sorted will yield better performance when searching for a match across input collections.

To get the sorting done, we define a Comparator for each input collection: Comparator.comparingInt( OfficeName :: id ) & Comparator.comparingInt( OfficeLocation :: id ) where the double-colons make a method reference. To each NavigableSet we add the contents of our inputs, an unmodifiable list made with the convenient literals syntax of List.of.

To get the actual work done of joining these two input collections, we make a stream of either input collection. Then we produce a new object of our third joined class using inputs from each element of the stream plus its counterpart found via a stream of the other input collection. These newly produced objects of the third joined class are then collected into a list.

NavigableSet < OfficeName > officeNames = new TreeSet <>( Comparator.comparingInt( OfficeName :: id ) );
officeNames.addAll( List.of( new OfficeName( 1 , "Office1" ) , new OfficeName( 2 , "Office2" ) ) );

NavigableSet < OfficeLocation > officeLocations = new TreeSet <>( Comparator.comparingInt( OfficeLocation :: id ) );
officeLocations.addAll( List.of( new OfficeLocation( 1 , "location1" ) , new OfficeLocation( 2 , "locatio2" ) ) );

List < Office > offices = officeNames.stream().map( officeName -> new Office( officeName.id() , officeName.name() , officeLocations.stream().filter( officeLocation -> officeLocation.id() == officeName.id() ).findAny().get().location() ) ).toList();

Results:

officeNames = [OfficeName[id=1, name=Office1], OfficeName[id=2, name=Office2]]

officeLocations = [OfficeLocation[id=1, location=location1], OfficeLocation[id=2, location=locatio2]]

offices = [Office[id=1, name=Office1, location=location1], Office[id=2, name=Office2, location=locatio2]]

Our three classes, the two inputs and the third joined one, are all written as records here for their convenient brevity. This Java 16+ feature is a brief way to declare a class whose main purpose is to communicate data transparently and immutably. The compiler implicitly creates the constructor, getters, equals & hashCode, and toString. Note that a record can be defined locally as well as nested or separate.

public record OfficeName( int id , String name ) { }

public record OfficeLocation( int id , String location ) { }

public record Office( int id , String name , String location ) { }

Given the conditions outlined above, we could optimize by hand-writing loops to manage the matching of objects across the input collections, rather than using streams. But I would not be concerned about the performance impact unless you had huge amounts of data that had proven to be a bottleneck. Otherwise, using streams makes for less code and more fun.

Upvotes: 1

Related Questions