Search code examples
javaspringspring-bootlambda

Is it possible to have a list of generic instance?


I have three objects that contain some fields, but two of the fields are start date and end date.

I have another Wrapper class that contains two fields (startDate and endDate) and a constructor.

I want to create a generic method that accepts ObjectA, ObjectB, ObjectC and returns me a list of times containing the start date, end date.

Is there a better way to make it more dynamic?

@Data
@AllArgsConstructor
@NoArgsConstructor
public class TimeIntervalObj {

    private Timestamp startDate;
    private Timestamp endDate;

    public static List<TimeIntervalObj> constructTimeIntervals(List<?> arrayList) {
        if (arrayList instanceof ObjectA) {
            return arrayList.stream()
                    .map(ObjectA.class::cast)
                    .map(time -> new TimeIntervalObj(time.getStartDate(), time.getEndDate())).collect(Collectors.toList());
        } else if (arrayList instanceof ObjectB) {
            return arrayList.stream()
                    .map(ObjectB.class::cast)
                    .map(time -> new TimeIntervalObj(time.getStartDate(), time.getEndDate())).collect(Collectors.toList());
        } else if (arrayList instanceof ObjectC) {
            return arrayList.stream()
                    .map(ObjectC.class::cast)
                    .map(time -> new TimeIntervalObj(time.getStartDate(), time.getEndDate())).collect(Collectors.toList());
        }
        throw new RuntimeException("Instance Error Type Incompatibility");
    }

}

Solution

  • The clean way would be to introduce a new interface that defines the getStartDate() and getEndDate() method and make ObjectA, ObjectB and ObjectC implement it:

    public interface HasStartAndEndDate {
        Timestamp getStartDate();
        Timestamp getEndDate();
    }
    

    Then your method could simply be:

    public static List<TimeIntervalObj> constructTimeIntervals(Collection<? extends HasStartAndEndDate> arrayList) {
        return arrayList.stream()
                .map(time -> new TimeIntervalObj(time.getStartDate(), time.getEndDate()))
                .collect(Collectors.toList());
    }
    

    If that's not feasible for some reason (most likely because any one of the classes are not under your control), then you might have to check each objects type individually:

    public static List<TimeIntervalObj> constructTimeIntervals(Collection<?> collection) {
        return collection.stream()
                .map(TimeIntervalObj::from)
                .collect(Collectors.toList());
    }
    
    public static TimeIntervalObj from(Object o) {
        if (o instanceof ObjectA a) {
            return new TimeIntervalObj(a.getStartDate(), a.getEndDate());
        } else if (o instanceof ObjectB b) {
            return new TimeIntervalObj(b.getStartDate(), b.getEndDate());
        } else if (o instanceof ObjectC c) {
            return new TimeIntervalObj(c.getStartDate(), c.getEndDate());
        }
        throw new IllegalArgumentException("Unsupported type: " + o.getClass());
    }
    

    The two approaches can also be combined, if you want all classes under your control to use the interface and other classes to also be supported (let's pretend ObjectA and ObjectB now implement the interface, but ObjectC still doesn't):

    public static List<TimeIntervalObj> constructTimeIntervals(Collection<?> collection) {
        return collection.stream()
                .map(TimeIntervalObj::from)
                .collect(Collectors.toList());
    }
    
    public static TimeIntervalObj from(HasStartAndEndDate hsed) {
        return new TimeIntervalObj(hsed.getStartDate(), hsed.getEndDate());
    }
    
    public static TimeIntervalObj from(Object o) {
        if (o instanceof HasStartAndEndDate hsed) {
            return from(hsed);
        } else if (o instanceof ObjectC c) {
            return new TimeIntervalObj(c.getStartDate(), c.getEndDate());
        }
        throw new IllegalArgumentException("Unsupported type: " + o.getClass());
    }
    

    Note that the extra from(HasStartEndDate) method is not strictly necessary (and won't be directly called in constructTimeIntervals as the compiler will understand that the argument is of type Object), but it's better to avoid the instanceof shenanigans in the cases where the static type is known (for example if you directly call TimeIntervalObj.from(someKnownObjectThatImplementsHasStartEndDate) elsewhere in your code).