How to figure out what object is in a abstract list?

Stream the sessions list and use instanceof to filter the Lectures type objects

List<Lecture> l = sessions.stream()
                           .filter(Lecture.class::isInstance) 
                           .map(Lecture.class::cast)                               
                           .collect(Collectors.toList());

By using for loop use two different lists for each type

List<Lecture> l = new ArrayList<>();
List<Tutorial> t = new ArrayList<>();
for (Session s : sessions) {
    if (s instanceof Lecture) {
        l.add((Lecture) s);
    }
      else if(s instanceof Tutorial) {
        t.add((Tutorial) s);
    }
}

Maybe you should store in two lists, just like:

public class Enrolment {

    private List<Lecture> lectures;
    private List<Tutorial> tutorials;

    public Enrolment() {
        this.lectures = new ArrayList<>();
        this.tutorials = new ArrayList<>();
    }

    public void addSession(Session session) {
        if (session instanceof Lecture) {
            lectures.add((Lecture) session);
        } else if (session instanceof  Tutorial) {
            tutorials.add((Tutorial) session);
        }
    }

    public List<Lecture> getLectures() {
        return lectures;
    }

    public List<Tutorial> getTutorials() {
        return tutorials;
    }

    public List<Session> getAllSessions() {
        ArrayList<Session> sessions = new ArrayList<>(lectures);
        sessions.addAll(tutorials);
        return sessions;
    }
}

Is that what you need?


My next question is should I instead store 2 lists. One list of Lectures and one list of Tutorials, instead of 1 session list? This is because the sessions list is useless to me and I have to loop through it each time to get information about lectures and tutorials. Is there a way I am missing to get all the lectures objects?

You answered yourself to your problem.
When you start to write too complex/boiler plate code to make things that should be simple such as iterating on a list of objects that you have just added, it is a sign that you should step back and redesign the thing.

By introducing Enrolment.addSession(Session session), you introduced an undesirable abstraction :

public class Enrolment {

    private List<Session> sessions;

    public Enrolment() {
        this.sessions = new ArrayList<>();
    }

    public addSession(Session session) {
        this.sessions.add(session);
    }
}

You don't want to handle uniformally Lecture and Tutorial from the Enrolment point of view, so just don't merge them in the same List only because these rely on the same interface (Session).
Abstraction has to be used when it is required and not systematically because that is possible.
Don't you add all objects in a List of Object because all is Object ? No.

Instead of, create this distinction both from the API method and from its implementation :

public class Enrolment {

    private List<Conference> conferences = new ArrayList<>();
    private List<Tutorial> tutorials = new ArrayList<>();


    public addConference(Conference conference) {
        this.conferences.add(conference);
    }

    public addTutorial(Tutorial tutorial) {
        this.tutorials.add(tutorial);
    }
}

And use it :

Lecture morningLec = new Lecture(900, "Dr. Mike");
newEnrolment.addLecture(morningLec);

Tutorial afternoonTut = new Tutorial(1400, "John Smith", 3);
newEnrolment.addTutorial(afternoonTut);

Note that you could have a scenario where you need to manipulate uniformally Tutorial and Lecture for some processings but that for others you want to distinguish them.
In this case, you have some common ways :

  • instanceOf : easy to use but also easy to make a code brittle. For example, later you could add a new subclass in the Session hierarchy and without be aware of it, instances of this subclass could be included or excluded in some processing without that the compiler warns you.

  • provide a abstract method that returns a boolean or an enum to convey the nature of the object (ex: isLecture()). More robust than instanceOf since the compiler constraints you to override the method but it may also lead to error prone code if multiple subclasses are added and that the filters are not only on Lecture but Lecture and another type. So I would favor this way while the filtering condition stays simple.

  • define three lists : one for lectures, another for conferences and another that contains all of these that should be handled uniformally. More complex way but more robust way too. I would favor it only for cases with complex/changing filtering conditions.