OOP Basics ·
‹ Previous Next ›
⏱ 5 min read Modified: 2026-08-09

Object-Oriented Programming in Java (OOP Concepts). Practical Tasks

Seventeen hands-on tasks cover core Java OOP: classes and objects, constructors, the this keyword, method overloading, the stack/heap memory model, passing objects to methods, varargs, recursion, garbage collection, inheritance, super, access modifiers, JavaBeans, overriding, abstract classes, and final. Work through them in order — later tasks assume the classes built in earlier ones.

1. The Phone Class

  1. Create a class Phone with the fields number, model, and weight.
  2. Create three instances of the class.
  3. Print the values of their fields to the console.
  4. Add the methods receiveCall and getNumber to Phone. receiveCall takes one parameter — the caller's name — and prints "Incoming call from {name}" to the console. getNumber returns the phone number. Call both methods for every object.
  5. Add a constructor to Phone that takes three parameters to initialize number, model, and weight.
  6. Add a constructor that takes two parameters to initialize number and model.
  7. Add a no-argument constructor.
  8. From the three-parameter constructor, call the two-parameter one.
  9. Add an overloaded receiveCall method that takes two parameters — the caller's name and phone number. Call it.
  10. Create a sendMessage method with varargs. The method takes any number of phone numbers and prints them to the console.
  11. Rework Phone to follow the JavaBean convention.

2. The Person Class

Create a class Person that has:

  1. the fields fullName and age;
  2. the methods move() and talk(), each simply printing a message such as "{Person} is talking" to the console;
  3. two constructors — Person() and Person(fullName, age);
  4. two objects of the class, one built with Person(), the other with Person(fullName, age);
  5. calls to move() and talk() for both objects.

3. The Matrix Class

Create a class Matrix. It should have the following fields:

  1. a two-dimensional array of floating-point numbers;
  2. the number of rows and columns.

It should have the following methods:

  1. addition with another matrix;
  2. multiplication by a number;
  3. printing the matrix;
  4. multiplication by another matrix.

4. Library Readers

Define a class Reader that stores the following information about a library patron:

  1. full name,
  2. library card number,
  3. department,
  4. date of birth,
  5. phone number.
  6. The methods takeBook() and returnBook().
  7. Write a program that creates an array of objects of this class.
  8. Overload takeBook() and returnBook():
    – a takeBook version that accepts a count of books taken and prints, for example, "J. Smith took 3 books".
    – a takeBook version that accepts a variable number of book titles and prints, for example, "J. Smith took: Adventures, Dictionary, Encyclopedia".
    – a takeBook version that accepts a variable number of Book objects (create a new class holding a book's title and author) and prints the same kind of message.
  9. Overload returnBook() the same way, printing either the list of returned titles or the count of books returned.

5. Print a Range of Numbers Recursively

Given two integers A and B, print every number from A to B inclusive — in increasing order if A < B, or decreasing order otherwise. Use recursion.

6. Inheritance: Student and Aspirant

  1. Build an inheritance example with a Student class and an Aspirant class (a graduate student), where an Aspirant differs from a Student by having ongoing research work.
  2. Student holds the fields String firstName, lastName, group, and double averageMark for the GPA.
  3. Add a method getScholarship() to Student that returns the scholarship amount: 2000 if the average mark is 5, otherwise 1900. Override this method in Aspirant: 2500 if the average mark is 5, otherwise 2200.
  4. Create an array of type Student holding both Student and Aspirant objects. Call getScholarship() for every element.

7. Cars

  1. Create the class Car in package com.company.vehicles, Engine in com.company.details, and Driver in com.company.professions.
  2. Driver holds fields for full name and years of driving experience.
  3. Engine holds fields for horsepower and manufacturer.
  4. Car holds fields for make, class, weight, a Driver, and an Engine. Add the methods start(), stop(), turnRight(), turnLeft(), each printing a matching message ("Driving off", "Stopping", "Turning right", "Turning left"), plus a toString() that prints the full car, driver, and engine details.
  5. Create a class Lorry derived from Car, adding a cargo capacity field.
  6. Create a class SportCar derived from Car, adding a top-speed field.
  7. Make Driver extend Person (see task 2).

Class hierarchy diagram

8. Animals

  1. Create a class Animal and the abstract classes Dog, Cat, and Bear extending it.
  2. Animal holds the field name and the abstract methods makeNoise, eat, and getDescription. makeNoise prints the animal's sound; eat prints what the animal eats; getDescription returns a description of the animal.
  3. Dog, Cat, and Bear each override makeNoise, eat, and getDescription.
  4. Create a class Vet with a method void treatAnimal(Animal animal) that prints the animal's name and description.
  5. In main, build an array of type Animal containing every animal type you have. In a loop, send each one to the vet; in a separate loop, call makeNoise and eat for each animal.

9. Access Levels

  1. Fill in the table.
    Access levels
    # Situation private default protected public
    1 Same class        
    2 Subclass in the same package as the superclass        
    3 Class in the same package, not a subclass        
    4 Subclass in a different package        
    5 Class in a different package, not a subclass        
  2. Create classes that let you verify each cell of the table.

10. Shapes

  1. Create an abstract class Shape and its subclasses Circle and Rectangle.
  2. Shape holds an abstract method draw() and a field for color.
  3. Circle and Rectangle hold coordinates.
  4. Create an array containing these shapes.
  5. Draw them in a loop (call draw()).

11. Online Store, Part 1

  1. Create a class Product with the fields name, price, and rating.
  2. Create a class Category with a name field and an array of Product. Create several Category objects.
  3. Create a class Basket holding an array of purchased products.
  4. Create a class User holding a login, a password, and a Basket object. Create several User objects.
  5. Print the product catalog to the console.
  6. Print every visitor's purchases to the console.

About tasks 12-17

These six tasks close gaps that plain "build a class" exercises don't cover: this, passing objects to methods, the stack/heap, garbage collection, super, and final. Whenever a step says "confirm it does not compile," actually try it — these are some of the most common Java interview questions, and they test understanding, not syntax memorization.

12. The this Keyword: BankAccount

  1. Create a class BankAccount with the fields String owner and double balance.
  2. Add a constructor BankAccount(String owner, double balance) whose parameter names match the field names. Inside it, use this.owner = owner; and this.balance = balance; to tell the field apart from the local parameter.
  3. Add a method deposit(double balance) — the parameter name shadows the field again. Inside it, add the amount to the object's balance with this.balance += balance;.
  4. Add a method BankAccount richerThan(BankAccount other) that compares this.balance with other.balance and returns whichever object (this or other) has the larger balance. This step shows that this is not just a way to reach a field — it is a reference to the whole current object.
  5. In main, create two BankAccount objects, call deposit on each, then call richerThan and print the owner with the larger balance.

13. Passing Objects to Methods: Counter

  1. Create a class Counter with a single field int value.
  2. Write a method increment(Counter c) that increases c.value by 1. Call it from main and confirm the change is visible outside the method — the object is passed by reference, so the method mutates the same object the caller holds.
  3. Write a method reset(Counter c) that assigns a brand-new object to the parameter: c = new Counter();. Call it and confirm the variable in main did NOT change — the reference itself is passed by value, so reassigning the parameter inside the method never touches the caller's variable.
  4. Print value before and after each call, and explain in a code comment why increment and reset behave differently.

14. Stack, Heap, and StackOverflowError

  1. Write a method printCountdown(int n) that prints n and recursively calls itself with n - 1, but with NO base case (no stopping condition).
  2. Run it and trigger a java.lang.StackOverflowError. In a comment, explain why: every call gets its own stack frame holding local variables and a return address, and a thread's stack has a fixed, non-negotiable size.
  3. Fix the method by adding a base case (for example, if (n <= 0) return;) and confirm it now finishes normally.
  4. Create a class Point with fields x and y, and in main create several Point objects in a loop, storing them in an array. In a comment, state where the Point objects themselves live (the heap) and where the reference variable holding the array lives (the stack frame of main).

15. Garbage Collection and finalize

  1. Create a class Resource with a field String name and an overridden method protected void finalize() throws Throwable that prints "Released resource: " + name and calls super.finalize().
  2. In main, create several Resource objects in a loop without keeping references to them past the loop body (or explicitly set the variable to null), then call System.gc().
  3. In a comment, explain that a finalize() call is neither guaranteed nor timely — it is only a hint to the garbage collector, not a command to free memory immediately.
  4. Also note in a comment that finalize() has been deprecated since Java 9 (and is disabled by default starting with Java 18), and that real code releases resources with try-with-resources and the AutoCloseable interface instead of finalize().

16. The super Keyword: Vehicle and Bicycle

  1. Create a class Vehicle with a field String name, a constructor Vehicle(String name), and a method printInfo() that prints "Vehicle: " + name.
  2. Create a class Bicycle extends Vehicle with an extra field int gearsCount. In the constructor Bicycle(String name, int gearsCount), call super(name) as the first line to initialize the parent's field.
  3. Override printInfo() in Bicycle: call super.printInfo() first, then print the gear count.
  4. In main, create a Bicycle object and call printInfo() — confirm both lines print: the parent's and the one added in the subclass.

17. The final Keyword: Constants, Classes, Methods, Parameters

  1. Create a class MathConstants with a field public static final double PI = 3.14159;. Try assigning a new value to it elsewhere in the code, confirm the code does not compile, comment out that line, and explain the compilation error in a comment.
  2. Create a final class SecurityUtils with a single static method. Try writing a class that extends SecurityUtils, confirm the compiler rejects it, and explain why a class might need to be final in the first place (for example, to guarantee its behavior can't change, the way String does).
  3. In the Vehicle class from task 16, declare printInfo() as final. Try overriding it in Bicycle and confirm it is impossible.
  4. Write a method with a final parameter, such as void process(final int value), and try reassigning value inside the method — confirm it is a compilation error.

Frequently Asked Questions

What's the difference between calling this(...) and super(...) in a constructor?

this(...) calls another constructor of the SAME class — for example, the three-parameter constructor in task 1 calling the two-parameter one. super(...) calls the parent class's constructor, as in task 16. Whichever one appears has to be the first line of the constructor, and a single constructor can never use both — it's this(...) or super(...), never both.

Why can I change an object passed into a method, but not replace the variable itself?

Java always passes arguments by value, but for objects that value is a reference (an address) into the heap. The method receives a copy of the reference to the same object, so changing its fields — as increment does in task 13 — is visible to the caller. Assigning a new object to the parameter, as reset does, only changes the local copy of the reference inside the method; the caller's variable never sees it.

Should I use finalize() to release resources in real code?

No. finalize() guarantees neither when it runs nor that it runs at all, has been deprecated since Java 9, and is disabled by default starting with Java 18+. For files, database connections, and sockets, use try-with-resources with the AutoCloseable interface — it releases the resource deterministically, the moment the block exits.

If a reference variable is final, can I still change the fields of the object it points to?

Yes. final on a reference variable only stops you from reassigning the reference itself to point to a different object — it does not make the object immutable. Its fields can still change normally, if they have public setters or are not themselves final. True immutability is a separate design decision (final fields, no setters), not an automatic side effect of a final variable.

Comments

Please log in or register to have a possibility to add comment.