javacollectionsjava-8

Storing Data of custom class in HashSet in sorted order


I have below bean class

public class ElectricityReading {

    private Instant time;
    private BigDecimal reading; // kW

    public ElectricityReading() { }

    public ElectricityReading(Instant time, BigDecimal reading) {
        this.time = time;
        this.reading = reading;
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        ElectricityReading that = (ElectricityReading) o;
        return Objects.equals(time, that.time);
    }

    @Override
    public int hashCode() {
        return Objects.hash(time);
    }

    public BigDecimal getReading() {
        return reading;
    }

    public Instant getTime() {
        return time;
    }

    public void setReading(BigDecimal reading){
        this.reading=reading;
    }
}

I need to store these result in HashSet.Actually i dont want to store time twice. if it repeats I need to either ignore or overwrite(Both will work ,its POC). Thats why I took time in equals and hashCode method. I am writing HashSet code like below.

public List<ElectricityReading> generate(int number) {
        Set<ElectricityReading>reading= new HashSet<>();

        Instant now = Instant.now();

        Random readingRandomiser = new Random();
        for (int i = 0; i < number; i++) {
            double positiveRandomValue = Math.abs(readingRandomiser.nextGaussian());
            BigDecimal randomReading = BigDecimal.valueOf(positiveRandomValue).setScale(4, RoundingMode.CEILING);
            ElectricityReading electricityReading = new ElectricityReading(now.minusSeconds(i * 10), randomReading);
            if(!reading.contains(electricityReading.getTime())){
                reading.add(electricityReading);
            }else {
                electricityReading.setReading(electricityReading.getReading());
            }
        }
        List<ElectricityReading> readings = new ArrayList<>(reading);
        readings.sort(Comparator.comparing(ElectricityReading::getTime));
        return readings;
    }

I am storing element in List because I need data in sorted order. is there anyway to improve this. in below output last 2 values are getting repeated.

[
    {
        "time": "2021-09-15T20:13:51.268560800Z",
        "reading": 2.5574
    },

    {
        "time": "2020-11-29T08:00:00Z",
        "reading": 1.7
    },
    {
        "time": "2020-11-29T08:00:00Z",
        "reading": 1.7
    }
]

Solution

  • tl;dr

    You are working too hard.

    Key code:

    NavigableSet < ElectricityReading > readings = 
        new TreeSet <>( 
            Comparator.comparing( ElectricityReading :: time ) 
        )
    ;
    

    Details

    For brevity, I will use the records feature in Java 16+ to briefly write the class. You could just as well use a conventional class.

    record ElectricityReading( Instant time , BigDecimal readingKwh ) { }
    

    NavigableSet

    If you want a set to keep its values in a certain order, use a NavigableSet (or SortedSet). Java comes with a few implementations of NavigableSet, one of which is TreeSet.

    When constructing the TreeSet, pass Comparator so the navigable set knows how you want the sorting performed. Fortunately, modern Java makes creating a comparator quite easy with the Comparator.comparing method that takes a method reference. We use our getter method for accessing our Instant field, ElectricityReading#time (implicitly created by the compiler in a record), as our method reference for comparisons.

    NavigableSet < ElectricityReading > readings = new TreeSet <>( Comparator.comparing( ElectricityReading :: time ) );
    

    Let's try that out with some sample data. We set up the data intentionally to be out-of-order to verify our set is sorting properly. Notice both the Instant values and the readingKwh values are increasing in parallel.

    I modified your input data, for easier reading.

    readings.add( new ElectricityReading( Instant.parse( "2021-09-05T08:00:00Z" ) , new BigDecimal( "1.7" ) ) );
    readings.add( new ElectricityReading( Instant.parse( "2021-09-05T08:00:00Z" ) , new BigDecimal( "-666" ) ) );    // Repeated `Instant` value.
    readings.add( new ElectricityReading( Instant.parse( "2021-11-05T09:00:00Z" ) , new BigDecimal( "3.1" ) ) );     // Out-of-order.
    readings.add( new ElectricityReading( Instant.parse( "2021-10-05T09:00:00Z" ) , new BigDecimal( "2.5574" ) ) );
    

    You also asked to prevent elements in the set that have same Instant value, without regard to the BigDecimal value. So let's repeat that first data element. We use a special value for readingKwh, -666, so we can observe the behavior of TreeSet#add.

    Verify by dumping to console.

    System.out.println( "readings = " + readings );
    

    When run.

    readings = [ElectricityReading[time=2021-09-05T08:00:00Z, readingKwh=1.7], ElectricityReading[time=2021-10-05T09:00:00Z, readingKwh=2.5574], ElectricityReading[time=2021-11-05T09:00:00Z, readingKwh=3.1]]

    We see two effects, both desired:

    Pulling all that code together for your copy-paste convenience.

    record ElectricityReading( Instant time , BigDecimal readingKwh ) { }
    NavigableSet < ElectricityReading > readings = new TreeSet <>( Comparator.comparing( ElectricityReading :: time ) );
    
    readings.add( new ElectricityReading( Instant.parse( "2021-09-05T08:00:00Z" ) , new BigDecimal( "1.7" ) ) );
    readings.add( new ElectricityReading( Instant.parse( "2021-09-05T08:00:00Z" ) , new BigDecimal( "-666" ) ) );    // Repeated `Instant` value.
    readings.add( new ElectricityReading( Instant.parse( "2021-11-05T09:00:00Z" ) , new BigDecimal( "3.1" ) ) );     // Out-of-order.
    readings.add( new ElectricityReading( Instant.parse( "2021-10-05T09:00:00Z" ) , new BigDecimal( "2.5574" ) ) );
    
    System.out.println( "readings = " + readings );