Skip to content

Classes and Objects

Classes

Classes in object-oriented programming are used to describe surrounding objects, events, activities, states and relations between the described objects. The classes defined in our applications can then be used to create variables of these types. Such types defined by classes are called complex or reference types.

Classes have two basic components:

  • fields - it's nothing else than the variables we've learned before. It is also a feature that describes the object of the class.
  • methods - by method we mean the operation that our class makes available to the outside world and it's use.

To sum up: a class is a schema which consists of fields and methods defined in it.

Suppose that we want to describe the movie as a class. So, what features (fields) can a class consist of to describe our movie? We all know this: the title, the year of production, the description, the actors, etc. All these features are nothing more than the fields in the film class. There are still activities (methods), e.g: "play the movie", etc. Below is an example of the definition of the Movie class:

public class Movie { // (1)
    private String title; // (2)
    private String description; // (3)
    private int productionYear; // (4)

    public void play() { // (5)
        // the body of the method
    }
}
In the example above we have defined a public class called Movie - this is indicated by the (1) line declaration: public class Movie. We have used the keyword class and the public modifier that defines the class's visibility range (this will be discussed in the following chapters). The lines (2) - (4) are field declarations representing the film title, description and year of production. They are also preceded by private modifiers, which define the range of these fields. The line (5) is a declaration of the void play() method, which will allow us to start playing our film. Thus, we see that our class includes fields that represent features and methods, i.e. operations that our class provides.

Let's look at a slightly more complicated example of a class:

public class Car {
    private String color;
    private int maxSpeed;
    private String brand;

    public void setColor(String color) {
        this.color = color;
    }

    public void setMaxSpeed(int maxSpeed) {
        this.maxSpeed = maxSpeed;
    }

    public void setBrand(String brand) {
        this.brand = brand;
    }

    public void printCarParameters() {
        System.out.println(String.format("Car color is: %s, max speed is: %d, car brand is: %s", color, maxSpeed, brand));
    }
}
In addition to field declarations, the class also has several methods: setColor, setMaxSpeed and setBrand - these are the so-called "field declarations". These are the so-called setters, i.e. single-argument methods that set the values of our fields (this will be mentioned in the following paragraphs).

In general, the class definition can be written as follows:

[access modifier] class nameClasses {
    // class field definitions

    // method definitions
}

Objects

What are objects? You can create copies (instances) of classes from the template (classes). These instances will contain fields defined within the class and you can call on them the methods that were defined in the class. Instances of the class that were created are called objects. Therefore, it is important to remember what is the main difference between the object and the class: if we use the example of the Car class, the class is nothing more than a set of features representing a car and actions that can be performed (methods). The object is a specific copy of this class, for example, a Mercedes car, which is white and reaches a maximum speed of 250km/h. We then move on to the process of creating an instance of a given class.

Creating class instances

Let's try to use the Car class defined earlier and create two objects of this class:

Car car1 = new Car();
Car car2 = new Car();

In the above example, we have created two instances of the Car class with names: car1 and car2. To create objects, we have used the keyword new, which invokes the so-called "car1andcar2. **constructor**, which we will talk about later. This example is very simple and just creating a class instance without setting the value of fields does not give us much. For this purpose, we can use methods to set the values of these fields, the so called **setters**, which we defined in the classCar`, so we can develop our example a little more:

Car car1 = new Car(); // (1)
car1.setBrand("Mercedes"); // (2)
car1.setColor("white"); // (3)
car1.setMaxSpeed(250); // (4)
car1.printCarParameters(); // (5)

Car2 = new Car();
car2.setBrand("Fiat");
car2.setColor("red");
car2.setMaxSpeed(210);
car2.printCarParameters();

In the example above we created two instances with names: car1 and car2 representing two copies of the Car class. The first of these is a Mercedes car. In this instance, we have set the values of the relevant fields giving them specific characteristics:

  • (2) - in this line, we have set the make (field brand) to the value of Mercedes calling the method setBrand.
  • (3) - we have similarly set the color (color field) to white.
  • (4) - here we have defined the maximum speed of the car as the numerical value of 250
  • (5) - this line is nothing more than a call to the printing method of the standard output (console) of all data (features) of the car1 instance.

As you can see in the above example, to call a method on an object, you have to refer to the name of this method on the object reference variables using the dotted notation, e.g. car1.setBrand("Mercedes"). In the same way, we refer to the instance fields to retrieve their value, e.g. car1.brand.

The following lines are a similar creation of an instance of car2, which represents a Fiat brand of a car. In the above example, each assignment of a value to a field was associated with calling the appropriate method (setter), which was used only to assign values. We can initialize our fields with the appropriate values using one instruction, namely constructors.

Access Modifiers

Access modifiers are used to determine the scope and visibility of methods, class fields, and are also used to set the visibility of the classes themselves. They allow us to determine how the classes will be used and who will be able to use the fields and methods defined inside them and in what situation. There are four basic modifiers:

  • public
  • protected
  • default
  • private

Let's discuss two of them first: public and private. When we define a field, method, or class with a public modifier, we give them access from anywhere in our application - anyone can refer to or call such a method. We then say that our field, method or class is public.

It may also happen that we don't want to give access to some fields or methods outside the definition of our class - they are supposed to be private for our class. We can manage such fields and methods (i.e. download and change field values or call methods) only from the level of that defined class. In this case, we use the access modifier private - we say that our fields and methods are then private.

In summary: access modifiers regulate access to fields and methods of classes when we refer to them from the level of other classes. Let's illustrate this with an example. Let's define the class describing the book:

public class book {
    public String title;
    public String author;
    private int numberOfPages;

    public void setNumberOfPages(int numberOfPages) {
        if (isNumberOfPagesCorrect(numberOfPages)) {
            this.numberOfPages = numberOfPages;
        } else {
            System.out.println("The provided number of pages is incorrect: " + numberOfPages);
        }
    }

    private boolean isNumberOfPagesCorrect(int numberOfPages) {
        return numberOfPages > 0;
    }
}

We defined two public fields using the public modifier: title and author and one private field numberOfPages. We have also defined two methods: the public method setNumberOfPages, which sets the value of the private field numberOfPages calling the private method isNumberOfPagesIsCorrect, which checks if the new value is greater than zero. What does this give us? First, it makes no sense for the isNumberOfPagesCorrect method to be public, since it is only invoked inside the Book class. Secondly, we wanted the method setting the value of the numberOfPages field to be available outside the defined class, so we made it public.

Now let's try to see how method calling and referring to fields outside the Book class works. So we will define a BookTest class that will test this behavior:

public class BookTest {
    public static void main(String[] args) {
        Book testBook = new Book();
        testBook.author = "Charles Dickens";
        testBook.title = "A Chrismas Carol";
        testBook.setNumberOfPages(250);

        System.out.println("Book title: " + testBook.title); // (1)
        System.out.println("Book author: " + testBook.author); // (2)

        System.out.println("Number of pages: " + testBook.numberOfPages); // Compilation error!
    }
}

In the BookTest test class, we created the testBookTest object, then referred to the public fields: author and title by dotted notation and set their values. We also checked the range of the setNumberOfPages method, which sets the value of the private numberOfPages field.

The next part of the exercise is an attempt to write down field values. Unfortunately, an attempt to refer to a private numberOfPages field outside of the Book class definition will result in the following compilation error:

Error:(14, 55) java: numberOfPages has private access in pl.sdacademy.Book
The (1) and (2) lines correctly refer to the public title and author fields.

To sum up, when should we use private modifiers, and when public? We should know that private modifiers hide from the user the inside, that is, the implementation of the class, that is, the way it does what it does. We should also apply the following few rules:

  • All class fields should be private if possible - the class should usually provide public methods for modifying and retrieving the values of the class fields (so-called getters and setters).
  • All methods that are used internally by the class should be private.
  • Only methods to be used by users of the class should be public.

We will talk about the default modifiers once protected in the chapter on packages.

Getters and setters

The concept of getter and setter is closely related to the concept of encapsulation. Encapsulation is a way of defining classes, where from the outside world (i.e. other classes and the rest of the application) we hide the internal implementation of classes. Instead, we define a set of methods to be used by users. To ensure this, the fields of our classes (as in the previous chapter we mentioned) should always be private, so we need to define a set of methods to modify and retrieve the values of the class fields. We call these methods accessors. We distinguish two groups of accordions because of the tasks they perform:

For the definition of the accessories we adopt a conventional naming convention: the getter methods take the prefix get in the name, and the setters in the setter: set**. The second part of the name should be the name of the field being set or downloaded. For example, if we define the accessors for a field called author, we could generate these method names:

  • getAuthor - for the getter
  • setAuthor - for setter

From now on, we don't define public class fields, but only the appropriate accessories, of course if we need them. So let's try to rebuild our Book class in such a way that it doesn't expose public fields to the outside world, but only create public accessories:

public class book {
    private String title;
    private String author;

    public String getTitle() {
        return title;
    }

    public void setTitle(String title) {
        this.title = title;
    }

    public String getAuthor() {
        return author;
    }

    public void setAuthor(String author) {
        this.author = author;
    }
}

In the example above, we see that the title and author fields have become private - you cannot easily refer to them by their name outside the classroom. We have defined the accessories for this purpose: * getTitle() * setTitle() * getAuthor() * setAuthor()

While the definitions of the getters are not complicated, the setters are a bit more complex because of the this construction. this indicates the current object, i.e. is the instance to which the accessor method was called. We use the this connotation so that the parameter of the method with the same name as the one given to the field that is set will not obscure each other.

The following example shows calling an accessor outside the class definition:

public class BookTest {
    public static void main(String[] args) {
        Book testBook = new Book();
        testBook.setAuthor("Charles Dickens");
        testBook.setTitle("A Chrismas Carol");

        System.out.println("Book title: " + testBook.getTitle());
        System.out.println("Book title: " + testBook.getAuthor());
    }
}
Note that attempts to refer to private fields using: testBook.title and testBook.author, will end up with compilation errors this time. The result of the above program is as follows:

Book title: A Chrismas Carol
Author: Charles Dickens

Packages

Given that many developers can create and use classes with the same names, sooner or later we will come across a situation where we have two classes with the same names in our application. We need a mechanism to do manage this. In Java we can specify in which package (package) our class is located. The package becomes an integral name of our class, so we minimize the potential problem of repeating the same class name.

Defining

We declare packages using the keyword package, for example

``java package pl.sdacademy.example;

The packages reflect the name of the reversed Internet domain. Let us now assume that we would like to have two classes with the same name `Book`. This is of course possible, but you should put them in two separate packages, e.g. `Book`:

```java
package pl.sdacademy.myfirstbookexample;

public class Book {
    // definitions
}

package pl.sdacademy.mysecondbookexample;

public class Book {
    // definitions
}

However, when creating packages and classes in these packages, we must remember a few rules:

  • The class belonging to a package (keyword package) must be at the beginning of the file - the only thing that can precede the use of package is the comments,
  • It is best that package names contain only letters and numbers. In addition, the package names are separated by dots and do not use the camelCase notation - we always use lower case letters,
  • The name of the package should correspond to the directory structure on the disk, so for example, when creating a package called en.sdacademy.myfirstbookexample, we should have this directory structure on disk:
en
 |
  --sdacademy
           |
            --myfirstbookexample

Importing classes

To use the classes defined in other packages, we must import them using the keyword import. It is also possible to import all classes from a given package - for this we use the * character instead of the name of the imported class. Suppose we have a Book class defined in the en.sdacademy.bookexample package, and we would like to use the Book Test in the BookTest class definition in the en.sdacademy package. We can see that both classes are in different packages, so it was necessary to import the Book class in the BookTest class:

package pl.sdacademy;

import pl.sdacademy.bookexample.Book;

public class BookTest {
    public static void main(String[] args) {
        Book testBook = new Book();
        // other instructions
    }
}

How about importing two other classes with the same name into one class?

There is, of course, such a possibility. Let's assume that we have two definitions of classes named Book: in the package en.sdacademy.bookexample.Book and en.sdacademy.secondbookexample.Book. In this case one of them should be openly imported and the other one should be referred to in the code after the full package name. Example below:

package pl.sdacademy;

import pl.sdacademy.bookexample.Book;

public class BookTest {
    public static void main(String[] args) {
        Book testBook = new Book(); // (1)
        pl.sdacademy.secondbookexample.Book secondTestBook = new pl.sdacademy.secondbookexample.Book(); // (2)
    }
}

The (1) line refers to the Book class defined in the en.sdacademy.bookexample package, while the (2) line refers to the Book class giving the full package path: en.sdacademy.secondbookexample.Book.

Static import

Sometimes in our applications it is necessary to use the same static (or fixed) method defined in class from another package many times. Unfortunately, we then have to refer to this method by the name of the class in which it is defined, e.g. "static":

System.out.println(Math.PI);
System.out.println(Math.round(Math.PI));

In order to make it easier for us to use such constants and static methods, we are helped by the so-called "static" methods. static import. All we need to do is to import our methods/constants together with the keyword static, e.g. **static:

import static java.lang.Math.PI;

public class StaticImportExample {
    public static void main(String[] args) {
        System.out.println(PI);
        System.out.println(Math.round(PI));
    }
}
In the example above, we made a static import of the fixed PI: import static java.lang.Math.PI. This allows us to refer to our constant now only by its name PI, and not by the name together with the class name.

Available by default

In the previous chapter, we learned several different access modifiers, namely: private, default, protected and public. We discussed two of these, private and public.

The default access is also called package-private. It is unique in that if we don't use any access modifier, our field, method, or class will just get default** access. It is almost as restrictive as the private modifier, but it also allows access to classes that are in the same package, i.e. have the same package defined by the keyword package.

An example of a class definition with a method with a default modifier:

package pl.sdacademy.myfirstbookexample;

public class Book {
    private String title;
    private String author;

    String getAuthorAndTitle() {
        return title + " " + author;
    }
}
We see in the above example that the getAuthorAndTitle() method has a defined default range, so it will only be available in classes from the en.sdacademy.myfirstbookexample package.

The protected modifier

The protected modifier will be thoroughly discussed on the occasion of the inheritance, but it should now be mentioned that it shares a feature with the default modifier, i.e. it allows access to fields and methods from classes in the same package. In addition, it should also be mentioned that elements of the class are made available to the class itself and its subclasses (the inheritance will be discussed in other chapters).

Constructors

Definition

Until now, we have created our objects using the keyword new, then to set the value of a given field in the object, we have defined setters, which we called on the instance as usual methods. This is a rather cumbersome way, especially when our class consists of multiple fields. The constructors are one solution to this. Java constructor is a special type of method that is used to initialize the state of an object, i.e. it sets the values of object fields. Constructors have two distinguishing features compared to ordinary methods:

  • The name of the constructor is identical to the name of the class in which we define it, e.g. inside the Car class we define a constructor also called Car.
  • When defining the constructor, we omit that part of the method signature in which we define the returned type - it is always "empty".

It should also be mentioned that constructors' methods, like normal methods, can take any number of input arguments. Let's check this with the example of the previously discussed class Car - let's redefine it using the constructor:

public class Car {
    private String color;
    private int maxSpeed;
    private String brand;

    public Car(String color, int maxSpeed, String brand) { // (1)
        this.color = color; // (2)
        this.maxSpeed = maxSpeed; // (3)
        this.brand = brand; // (4)
    }

    public void printCarParameters() {
        System.out.println(String.format("Car color is: %s, max speed is: %d, car brand is: %s", color, maxSpeed, brand));
    }
}
Compared to the previous definition, this one has the constructor method and there is no definition of the methods setters. By using the constructor, in this case they become redundant. Let's move on to the discussion of the constructor method: in the line (1) there is a signature declaration of the constructor method (without the returned type), having the same name as the class in which it is defined. This constructor takes three parameters which represent the types of individual fields of the Car class. The lines (2) - (4) are the assignments of parameter input values to the class fields. In the constructor we again use the keyword this which, as before, is a reference to the current instance of the class for which the method was called. The following assignment will set us the value of the color field:

 this.color = color;

The above line of code is nothing more than to assign a value from the color parameter given in the constructor to the color field of the current instance (reference by this.color).

The definition of the constructor we are analyzing is the definition of the so called "color". constructor with parameters - as the name suggests, the method takes the parameters. There is one more version of the constructor - default constructor.

Default constructor

In the first definition of the Car class, we did not define the constructor, but by creating an instance of the Car class, we called the constructor using the instruction:

Car car1 = new Car();

This is nothing more than creating an instance of the Car class using the valueless constructor or otherwise a default one. You can see that the constructor method does not take any parameters. How is it possible that we could use the constructor without defining it in the class? This is natural, because in Java, the non-parameter constructor is the default one - you don't need to define it, because you inherit (we will talk about the inheritance later) it from the Object class. The constructor is called the default constructor and is automatically generated for us by the Java compiler in one particular case: if we don't provide the constructor for the class ourselves (no definition of any constructor).

Let's analyze another example:

public class Car {
    private String color;
    private String brand;

    public Car(String color, String brand) {
        this.color = color;
        this.brand = brand;
    }
}

Now let's try to create a Car class instance using a valuless builder:

Car car1 = new Car();

We will get the following compilation error:

Error:(7, 20) java: constructor Car in class pl.sdacademy.Car cannot be applied to given types;
  required: java.lang.String,java.lang.String
  found: no arguments
  reason: actual and formal argument lists differ in length

This is because the default constructor has been replaced by the definition of the constructor with parametersCar(String color, String brand). For the compilation to be successful, a definition of the non-parameter constructor must be explicitly created. The Car class will then have a form:

private String color;
private String brand;

public Car() {
}

public Car(String color, String brand) {
    this.color = color;
    this.brand = brand;
}

Then a call to Car car1 = new Car(); will correctly create an instance of the Car class.

Overloading the constructors

Each class can have multiple constructors. Because constructors are a special kind of methods, in order to have more than one constructor, each of them must differ in number, type or order of arguments. Let's illustrate this with the example of the Car class - let's expand it with more constructors:

public class Car {
    private String color;
    private int maxSpeed = 180;
    private String brand = "Fiat";

    public Car() {
        this.color = "white";
        this.maxSpeed = 180;
        this.brand = "Fiat";
    }

    public Car(int maxSpeed, String brand) {
        this();
        this.maxSpeed = maxSpeed;
        this.brand = brand;
    }

    public Car(String color, int maxSpeed, String brand) {
        this(maxSpeed, brand);
        this.color = color;
    }

    public void printCarParameters() {
        System.out.println(String.format("Car color is: %s, max speed is: %d, car brand is: %s", color, maxSpeed, brand));
    }
}
A few sentences explaining the above example: the first constructor (with no arguments) sets the fields with default values. The next constructor (with two arguments) calls the valueless constructor calling the this() instruction, additionally it sets the values of the fields maxSpeed and brand with the values of parameters passed to the method. The last constructor (three-parameter one) calls the two-parameter constructor (the this(maxSpeed, brand) instruction) and additionally sets the value of the color field with the value passed on to the method.

Now let's try to call overloaded constructors in the example:

Car car1 = new Car();
car1.printCarParameters();

Car car2 = new Car(250, "Mercedes");
car2.printCarParameters();

Car car3 = new Car("Red", 320, "Ferrari");
car3.printCarParameters();

The result of the action is as follows:

Car color is: white, max speed is: 180, car brand is: Fiat
Car color is: white, max speed is: 250, car brand is: Mercedes
Car color is: Red, max speed is: 320, car brand is: Ferrari

To sum up, overloading of the constructors is the process of defining many of them in one class, bearing in mind that each of them must differ in number, type or order of arguments.

Overloading methods

In addition to overloading the constructors, we can also overload methods in a class. The overload of a method happens when you define in one class a method with a name that already exists in it. For such code to compile, each of the overloaded methods must differ in number, type or order of arguments. Below is an example of a class that correctly overloads the add method:

public class SimpleCalculator {

  public int add(int numA, int numB) {
    return numA + numB;
  }

  public int add(int numA, int numB, int numC) {
    return numA + numB + numC;
  }
}

It is worth noting that changing only the access modifier or returned type not is a correct overload of the method, e.g.

public class SimpleCalculator {

  public int add(int numA, int numB) {
    return numA + numB;
  }

  private long add(int numA, int numB) { // incorrect overload, difference is only in access modifier and type returned
    return numA + numB;
  }
}

Primitives and classes

Java offers several primitives types like int, double or float. In some cases using primitive types is impossible. For example trying to define a list of integers in a following way is impossible:

List<int> ints = new ArrayList(); // błąd kompilacji

For such cases java offers several wrapper classes for primitive types:

primitive type wrapper class
byte Byte
short Short
int Integer
long Long
float Float
double Double
boolean Boolean
char Character

Thanks to wrapper classes, in each place where an object type is required, and we would like to use a primitive type, we should use wrapper class, e.g.:

List<Integer> ints = List.of(1, 2, 3, 4);
Set<Long> longs = Set.of(2L, 3L , 4L);

Despite the fact wrapper classes are objects we do not have to use constructors to create a new instance of such an object. For wrapper references we can assign values as if those were primitive types, e.g.:

byte b = 3;
Byte bObj = 3;

int intNumber = 7;
Integer intObject = 19;

long longNumber = 19L;
Long longObject = 120L;

float floatNumber = 1.1F;
Float floatObject = 2.2F;

double doubleNumber = 3.0D;
Double doubleObject = 3.1D;

Using constructors is possible but not recommended:

new Integer(3); // niezalecane
new Long(2L);
new Double(3.1);

However, there are some minor differences when using primitives types and wrapper classes. Such an exception is e.g. declaration of long. In case a number that fits an int is assigned, then we do not have to add L letter to the declaration, but if we want to assign a value to a Long then we always have to do it (even for small numbers), e.g.:

long a = 2_000_000_000; // ok
long b = 30000000000; // za duża liczba - błąd kompilacji
long c = 40000000000L; // ok

Long x = 2000000000; // błąd kompilacji
Long y = 2000000000L; // ok

Methods

Wrapper classes have access to such methods as toString(), hashCode() or equals() which are described in the later chapter. They do offer also some methods (some of them static) which allow conversion to and from primitive type. There also exists an overload of valueOf method which allows getting a value from a String, e.g.:

int a = 3;
Integer intA = Integer.valueOf(3);
Integer intB = Integer.valueOf("4");
int primitiveInt = intA.intValue();
System.out.println(intA.toString());

long b = 3;
Long longB = Long.valueOf(b);
Long longC = Long.valueOf("17L");
long primitiveLong = longB.longValue();
System.out.println(longB.toString());