Lesson 4 - Object Orientated Design
We explored String
s in previous lessons, which was your first look at an object in Java. In fact, you've been writing programs inside of a main
function, which is a method inside a function! In this lesson, we'll see what an object really is and why they're so useful.
Why objects?
Objects have both state, variables that remain constant until you modify them, and behavior, which are seen through callable methods that either feed your main method information about that object or operate on its state. As programmers, you need to design objects so that their state and behavior are appropriate for what you're trying to accomplish, while breaking down code into testable and understandable building blocks.
It's probably easiest to think of an object like an object in the real world, such as a person or a dog. It could have states like a name, age, favorite color, and friends. Similarly, a Person
object could have behaviors like befriending another person if they have the same favorite color, increasing its age every year, or saying its name. Objects may also want to keep some state variables private
, such as a credit card number. These are just examples, but you'll see as we go about modeling a person in this lesson.
In essence, objects make programming real-world mechanisms and representing real-world entities a lot easier. You could have a Robot
object drive a certain distance, deposit and earn interest in a BankAccount
, or read the state of an Actuator
on a Robot
.
Constructors and member variables
A class
is the blueprint behind an object, which is something you should already be familiar with from writing programs. Each class should generally be in its own .java
file, called ClassName.java
. It's possible to have multiple classes within a file, but we'll get to that later.
Inside Person.java
:
//Import statements come first import java.util.Scanner; public class Person { public static void main(String[] args){ //do stuff here } }
This is probably what you're familiar with right now. Now, we'll add in some fields, called member variables, to the Person
class that will help identify it:
public class Person { String name, favoriteColor; int age, numFriends; }
To initialize these attributes, we'll also need a constructor, which is essentially a method that returns an instance of that class while assigning values to as many member variables as you'd like.
public class Person { String name, favoriteColor; int age, numFriends; public Person(String name) { this.age = 0; this.numFriends = 0; this.name = name; } }
Note that the identifier for a constructor is simply the name of the class, though the parameters can be any datatype, and there is no return
statement. Like other methods, constructors can also be overloaded with arguments of different datatypes. An empty constructor is automatically created if no constructor is declared for a class, which simply doesn't set any member variables.
To access member variables, we use the keyword this
, referring to the new instance of the object, followed by the dot operator and the identifiers of the variables you want to modify. This also distinguishes the name
variable in the parameter of the constructor with the name
variable that's part of the object's state.
Member variables must be assigned a value, either explicitly through the code of the class or via a constructor, before their value can be accessed. They can also either be objects or primitive values.
Declaring objects
So, how exactly do you create an object with the constructor? Well, it's similar to declaring any other variable in Java, complete with a datatype and identifier, except you'll need to use the new
keyword:
public static void main(String[] args) { Person mentor = new Person("Cokeley"); }
This creates an instance of a Person
, which is stored in mentor
. Now, if you want to access or modify attributes of mentor
, you'll use the same dot operator that was used in the constructor:
mentor.age = 25; System.out.println(mentor.name + " is " + mentor.age + " years old");
This will print out Cokeley is 25 years old
.
Class methods
While having objects as a simple data structure with various attributes is useful, sometimes you want a little more interactivity. That's when we can create our own methods in a class. We can spice up the Person
class by adding in the following:
public class Person { String name, favoriteColor; int age, numFriends; public Person(String name){ this.age = 0; this.numFriends = 0; this.name = name; } boolean makeFriends(Person possibleFriend){ if (possibleFriend.favoriteColor.equals(this.favoriteColor)) { this.numFriends++; possibleFriend.numFriends++; return true; } return false; } }
This makeFriends
method mutually has two people become friends if their favoriteColor
is the same, then returns true
if they've become friends, or false otherwise. Another example can be seen with this RightTriangle
class:
class RightTriangle { double legOne, legTwo, hypotenuse; RightTriangle(double side1, double side2, double side3) { this.hypotenuse = Math.max(Math.max(side1, side2), side3); this.legOne = Math.min(Math.min(side1, side2), side3); this.legTwo = side1 + side2 + side3 - this.legOne - this.hypotenuse; } double getArea() { return 0.5 * this.legOne * this.legTwo; } double getPerimeter() { return this.legOne + this.legTwo + this.hypotenuse; } //Returns smaller acute angle in radians double getSmallerAngle() { return Math.atan(this.legOne / this.legTwo); } //Returns larger acute angle in radians double getLargerAngle() { return Math.PI/2 - this.getSmallerAngle(); } //Extends the length of the shorter leg and //adjusts the length of the hypotenuse accordingly void extendLegOne(double newLength) { if(newLength > this.legTwo){ this.legOne = this.legTwo; this.legTwo = newLength; } else this.legOne = newLength; this.hypotenuse = Math.sqrt(Math.pow(this.legOne, 2) + Math.pow(this.legTwo, 2)); } }
Here, after initializing a RightTriangle
object, we can use its different instance methods to get and modify its properties. For instance, executing the following in the main
method
RightTriangle tri = new RightTriangle(4,3,5); System.out.println("Perimeter: " + tri.getPerimeter()); System.out.println("Area: " + tri.getArea()); System.out.println("Smaller Angle: " + tri.getSmallestAngle() + " rad"); tri.extendLegOne(10); System.out.println("New Perimeter: " + tri.getPerimeter());
will print
Perimeter: 12.0 Area: 6.0 Smallest Angle: 0.6435011087932844 rad New Perimeter: 24.77032961426901
Visibility
Sometimes it is desirable to reduce the access and visibility of object methods or instance variables. For example, you generally wouldn't want a main
method to be able to change a Person
object's name
explicitly, or perhaps you want to modify a BankAccount
object's balance only through withdrawal and deposit methods, each requiring a PIN code. In that case, it's best to leave member variables private
instead of public
. To do this, simply put the visibility modifier in front of the variable identifier, like so:
public class Person { private String name, favoriteColor; private int age, numFriends; public Person(String name) { this.age = 0; this.numFriends = 0; this.name = name; } }
That way, private
level access only allows those variables to be accessed and modified within the same class (such as within the constructor or other class methods). On the other hand, public
level access allows all classes to modify a variable or call a method, while default access (not putting any modifier) only allows classes of the same package to access variables/methods. We'll get into packages later on, but just know that they're ways of grouping similar classes for now.
So, how are we supposed to modify and access attributes like favoriteColor
so the method makeFriends
can still function properly? For the first case, we'll use a special type of method called a setter, while we create a getter to do the latter:
private public class Person { private String name, favoriteColor; private int age, numFriends; public Person(String name) { this.age = 0; this.numFriends = 0; this.name = name; } public void setFavoriteColor(String favoriteColor) { this.favoriteColor = favoriteColor; } public String getFavoriteColor() { return this.favoriteColor; } }
It's pretty simple: a setter has an argument datatype that's the same as the member variable, sets the member variable, then returns nothing. Meanwhile, a getter has the same return type as the member variable, has no parameters, and simply returns the value of the member variable. In fact, these methods are so common (as it's good practice to set variables as private
) that most IDEs (including Eclipse) have a way to automatically generate setters and getters for a class.
Finally, static
methods and variables are called directly on a class, rather than on an instance of a class. You've already seen examples of these in the Math
class: the variable Math.PI
and the method Math.pow(base,exp)
can be called without declaring an instance of a Math
object via the new
keyword.
It's important to note that static
variables are shared by all instances of a given class (and thus can be used as temporary storage). Also, static
methods cannot access non-static
variables or methods. You can make a static
method private
or public
by putting the visibility modifier in front of the word static
, such as in public static void main(String[] args)
.
Scope
Our last topic is something that we've used throughout this lesson without realizing it; variable identifiers often come in conflict with each other. In that case, which value do we use?
The smallest scope is in a loop:
for (int i = 1; i < 5; i++) { double area = Math.pow(i, 2); }
In this case, both i
and area
have block scope and cannot be modified or accessed outside of the for
loop. You'll see that a set of curly braces generally denotes scope. Note that variables with the identifier i
or area
cannot exist outside of the loop either.
Next up are local variables, also known as method scope:
public void setSideLength(int sideLength) { double squareArea = Math.pow(sideLength, 2); this.sideLength = sideLength; }
Both sideLength
and squareArea
can be accessed/modified only within the method setSideLength
. However, using the keyword this
and the dot operator will refer to the member variable sideLength
of the class, rather than the one passed in as an argument.
All variables other than member variables cannot be accessed outside of a class, which then follow the visibility rules discussed before.
HW: Modeling Bank Accounts
We've discussed bank accounts throughout this lesson. Now it's time to implement one!
Each BankAccount
object has the following attributes, which should all be private
:
balance
: adouble
indicating the amount of money in the bank account. Cannot be negative.pin
: a four-digit code that authenticates a user. May contain leading zeroes (i.e. 0000 is a valid PIN)name
: the name of the bank account ownerinterestRate
: the percent of interest earned by a user in a given year, shared across all bank accounts- Challenge:
id
: a random, five-digit code that's generated upon the creation of the bank account to identify it. Does not contain leading zeroes. You may want to refer back to theMath.random
method from Lesson 2.
Methods should be provided to do the following:
- Construct a
BankAccount
with a PIN, owner, and initial balance - Challenge: Construct a
BankAccount
with a PIN, owner, and initial balance of $0.00 - Get the
name
of the owner of an account - Get the
id
of the account - Change the PIN of the account, given the old PIN and the new PIN. Should return some form of feedback to determine if the PIN was successfully changed
- Withdraw money from an account, given its PIN and the amount desired. Should indicate if withdrawal was successful and should not allow it if the account doesn't have enough money
- Deposit a given amount of money into the account
- Earn interest for the year, based on the
interestRate
- Challenge: Transfer money from one account to another, given another
BankAccount
object and its PIN
You do not need to submit a main
method as a driver for your class, though you will need to write one to test if your class works. Next lesson, you'll write one to complete specific tasks when you learn about arrays and HashMaps
to create a network of bank accounts.
Please email me your completed class or DM me via Slack.