public class Ball{
public String color;
public int size;
public Ball(String c, int s){
color = c;
size = s;
}
public void roll(){
//code not shown
}
}
Here’s our very basic class called Ball. We will add more methods and properties later in the explanation.
Let’s go through each bit of code.
The first line declares the class and gives some basic information like the name of the class and the access level (we will discuss this later in Encapsulation).
The next two lines declares the fields, or instance variables. The first word declares the access level. The second word declares the data type of the field (the type of data it stores, like a string or an integer). The last word declares the field’s name.
The next 4 lines are known as a constructor, and it allows for an object to be created from the class. It will be discussed later in Methods.
The next 3 lines contains methods, and allows the object that the class creates to be interacted with and do things. The different types of methods and what they can do will be discussed later.
Now, let’s create a ball object.
Ball redBall = new Ball(“red”, 2);
The first part specifies the type of object this variable is. The second part specifies the variable name. The third section creates a Ball object using the specified parameters of the constructor(s).
Now we can also use a method on the object we have just created:
redBall.roll();
Now that I have discussed the basic features of classes and objects, we can discuss their capabilities in more detail.
Methods are a key component of OOP and its usefulness. There are many types of methods used in OOP, such as the mutators and accessors we have discussed earlier. In general, all methods have an access level, a return type, and a name. Methods may also be static or non static. All methods have either a certain data type or void as a return type. Methods returning a data type will give a value of that type at the end of their execution. Methods returning void may still perform actions but will not return any values at the end of their execution.
Let’s say I want to change the color of the ball, from red to blue. Currently, the only way to do that would be to do redBall.color = “blue”;
but that is generally considered bad programming style and would likely not work in most programs due to instance variables having a private access level. A similar problem arises when I want to get what the color of the ball is, as there is no other option but to use redBall.color. Therefore, we use mutators and accessors, otherwise known as “getters” and “setters”. They are simply methods which will modify or access the variables in a less direct sense, which allows for more control over the values that are allowed.
The updated code with set and get methods becomes:
public class Ball{
public String color;
public int size;
public Ball(String c, int s){
color = c;
size = s;
}
public String getColor(){
return color;
}
public int getSize(){
return size;
}
public void setColor(String c){
color = c;
}
public void setSize(int s){
size = s;
}
public void roll(){
//code not shown
}
}
The constructor is a method with no return type (not void) and does not have the static keyword. Its purpose is to outline how the class will create an object, and similar to other methods, may have parameters to accept input. Without parameters, the constructor becomes a default constructor, which outlines the default object created from the class. The updated code with a default constructor becomes:
public class Ball{
public String color;
public int size;
public Ball(){
color = "red"; //default color is red
size = 1; //default size is 1
}
public Ball(String c, int s){
color = c;
size = s;
}
public String getColor(){
return color;
}
public int getSize(){
return size;
}
public void setColor(String c){
color = c;
}
public void setSize(int s){
size = s;
}
public void roll(){
//code not shown
}
}
The purpose of static methods is to allow certain methods to be run without the necessity of creating an object first. An example could be the Math utility class bundled within java. When we want to square root a number, we do not do:
Math mathObject = new Math();
mathObject.sqrt(number);
This is unnecessary because the object does not need to be there; a square root should work regardless of any variation in the object. Instead, we see the method being run directly from the class:
Math.sqrt(number);
In general, static methods are associated with the class, and instance methods are associated with the object.
public static double sqrt(double number){
//code not shown
}
An instance method is a regular method with no special properties. It simply follows the access level, return type, name, and parameter header. It requires an object to use.
public void roll(){
//code not shown
}
Multiple methods with the same name but different parameter lists so that their method signature is different. This allows the compiler to tell which overloaded method is currently being used. For example, I can have both a default constructor, which will have no parameters, and a custom constructor, where each parameter represents an instance variable’s value. These two will have different method signatures due to the difference in parameters but will have the same name (class name).
public void roll(){
//code not shown
}
public void roll(int distance){
//code not shown
}
One of the major principles of OOP is to be able to control access of components of an object or class from separate areas of code. The major keywords that are used to control this are:
Public allows the class / class component to be used within any client program. However, an access level of public on a class does not mean that all the components within the class are public. Therefore, a client program may be able to use a class but not access specific methods or variables.
Private prevents the class component from being used in any other location but the class itself. The private keyword can not be applied to a class, as that would prevent the usage of that class. This access level is generally applied to instance variables in a class, as mutators and accessors should undertake the role of changing and getting the value of an instance variable.
Learning this, we change the Ball class once again to ensure the instance variables have a private access level.
public class Ball{
private String color;
private int size;
public Ball(){
color = "red"; //default color is red
size = 1; //default size is 1
}
public Ball(String c, int s){
color = c;
size = s;
}
public String getColor(){
return color;
}
public int getSize(){
return size;
}
public void setColor(String c){
color = c;
}
public void setSize(int s){
size = s;
}
public void roll(){
//code not shown
}
}
Another key aspect of OOP is the ability to share properties and reuse code between classes. In general, subclasses, otherwise known as child classes, will inherit or reuse methods and variables from superclasses, otherwise known as parent classes, while usually adding some new methods and instance variables to expand the behaviour of the new class. The relationship between the child to the parent is known as a “is-a” relationship and the relationship between the parent to the child is known as a “has-a” relationship.
public class BouncyBall extends Ball{
private double bounciness;
public BouncyBall(){
bounciness = 0.1;
}
public String getBounciness(){
return bounciness;
}
public void setBounciness(double b){
bounciness = b;
}
public void bounce(){
//code not shown
}
}
public class Ball{
private String color;
private int size;
public Ball(){
color = "red"; //default color is red
size = 1 //default size is 1
}
public Ball(String c, int s){
color = c;
size = s;
}
public String getColor(){
return color;
}
public int getSize(){
return size;
}
public void setColor(String c){
color = c;
}
public void setSize(int s){
size = s;
}
public void roll(){
//code not shown
}
}
In this situation, the BouncyBall class will be a subclass, and the Ball class will be a superclass. Notice that the BouncyBall class has an extra bit of code past the normal class header: extends Ball. This extends keyword is what expresses inheritance in Java. From this example, we can see the usefulness of inheritance. Instead of having to copy the entirety of the code from Ball into BouncyBall, we simply need to use the extends keyword and implement the extra functionality. In this case, we add the double bounciness, the void instance method bounce, and the mutator and accessor for bounciness.
There are some additional keywords that can be used to allow for additional functionality. The super keyword allows the class to use methods and constructors from the superclass. For example, to access the Ball constructor in the BouncyBall constructor, the code would look like this:
public BouncyBall(String c, int s, double b){
super(c, s);
bounciness = b;
}
We will talk about the use of super to overload methods in Polymorphism. Superclasses and Subclasses are also interconnected in a multitude of ways. A superclass variable, for example, can store objects of a subclass. In terms of our example, we could store a BouncyBall object in a Ball variable.
Ball bouncy = new BouncyBall(“red”, 10, 0.1);
We will discuss the implications and uses of this in the next section, Polymorphism.
Polymorphism comes from the greek prefix poly-, meaning many, and morphe, meaning form. This literally translates to many forms, which will make more sense as we go along. We begin with our example of BouncyBall and Ball. Say I want a regular ball to roll differently from a bouncy ball. However, I don’t want to go through the inconvenience of creating a completely separate method for it. Therefore, we decide to override the method roll in BouncyBall.
public void roll (){
//code different but not shown
}
Now from the previous section on Methods we have learned about method overloading and how the compiler processes and determines which method to use. We recall that a method’s signature is how the compiler can tell the difference between each overload. Yet in this case, the method signatures are identical! So how will the compiler deduce which method to use? In short, it doesn’t need to. The method is tied to a specific class, and since they are still instance methods, require an object to run. The method is simply determined by the type of object that the method runs from.
Ball bouncy = new BouncyBall(“red”, 10, 0.1);
Even the bouncy object, which appears to be of type Ball, will still use the BouncyBall override for roll because the object is still inherently of type BouncyBall.
Continuing with the bouncy object, we see that the variable is of type Ball. This means that any methods that can only be run from a BouncyBall object, like bounce(), will return an error.
bouncy.bounce(); //returns an error
Yet the object inherently stored in the variable is of type BouncyBall, and should be able to use the bounce() method. So what do we do? We introduce downcasting. Using casting we are able to turn the bouncy object back into a BouncyBall.
BouncyBall casted = (BouncyBall)bouncy;
This allows us to use the bounce method on casted.
casted.bounce(); //no error
However, you must be careful that the underlying object stored within the superclass variable is the same type that you are casting to. For example, if we had another subclass from Ball, called BowlingBall, we would not be able to cast bouncy to a BowlingBall object.
BowlingBall casted = (BowlingBall)bouncy; //returns a ClassCastException
Another major paradigm of OOP is Abstraction. It hides the code implementation of certain methods from the user. This is accomplished in Java via the use of abstract classes and interfaces.
Abstract classes are classes that are unable to be instantiated into objects, but can be inherited using the extend keyword. They may or may not contain abstract methods, which will be discussed below. These serve as a basic framework for shared code between classes that will likely be related and used in similar circumstances.
An abstract method does not have any implementation or code. Instead, the subclasses that inherit the abstract class containing this method are expected to implement the method there. In fact, an error will occur when a class inherits an abstract class but does not implement all methods fully.
For example, let’s make the Ball class abstract, as we will only need BowlingBalls and BouncyBalls from now on (Assume BowlingBalls and BouncyBalls). BouncyBall and BowlingBall will likely share a lot of code; for example, they will still both roll. Therefore, we will likely keep roll as a shared method between the two classes. However, let’s add a method, play(), which will likely not have a shared implementation between BowlingBall and BouncyBall. Yet it is still a key feature and trait of every Ball. Therefore, let’s add the method play() and make it abstract.
public abstract class Ball{
private String color;
private int size;
public Ball(){
color = "red"; //default color is red
size = 1 //default size is 1
}
public Ball(String c, int s){
color = c;
size = s;
}
public String getColor(){
return color;
}
public int getSize(){
return size;
}
public void setColor(String c){
color = c;
}
public void setSize(int s){
size = s;
}
public void roll(){
//code not shown
}
public abstract void play();
}
Interfaces are similar to abstract classes in that they are also used to simplify common functionality between classes. In this case, classes may not even need to be related in any other way. While an abstract class called Shape may have subclasses which are heavily related to each other (Triangle, Rectangle, Circle), an interface such as Comparable may be implemented by a large variety of classes, like ArrayLists, Strings, or the Integer wrapper class. In this way, interfaces will be different from abstract classes. They also use the keyword implements instead of extends, and can not have instance variables or methods that are not abstract or default. In other words, it is a purely abstract class, containing no additional shared methods or implementations.
For example, instead of using shared roll() code in the abstract class Ball, let’s make an interface known as Rollable. This may be implemented by a variety of classes, such as Pencil, RollingPin, and Barrel, which may have nothing to do with Balls. By creating this interface Rollable, we simply let Ball implement Rollable. This way, Ball can still have a common implementation for roll() for both BowlingBall and BouncyBall.
interface Rollable
{
void roll();
}