Encapsulation
Today we discuss one of the four pillars of object oriented programming: encapsulation. Encapsulation describes binding code (called methods in OOP) and data together into one thing called an object.
- Instance variables
- Getters and setters
- Constructors
- Other methods
- toString
- Encapsulation
- Objects vs. primitives
- Applications vs. classes
- Java notes
All the code files for today: Student0.java; Student01.java; Student02.java; Student03.java; Student04.java; Student05.java; Student06.java; Student07.java; StudentTrackerApp.java; MemoryAllocationPrimitives.java; MemoryAllocationObjects.java.
Instance variables
In Java a
Today we'll build a simple Student class to represent students at a college. We will progressively add more functionality to the class as we go, resulting classes Student0
through Student07
. From those classes we will
ppublic class Student0 {
String name;
int graduationYear;
public static void main(String[] args) {
Student0 alice = new Student0();
alice.name = "Alice";
alice.graduationYear = 2027;
System.out.println("Name: " + alice.name +
", Year: " + alice.graduationYear);
}
}
OUTPUT
Name: Alice, Year: 2027
Here, name
and graduationYear
are Student
; i.e., each student object (that is each instance) has its own name and year. Python programmers are used to objects having instance variables (though not declaring them up front like this); C programmers can think of them kind of like structs (though as we'll see, they're much more powerful). One of the biggest changes for Python programmers in moving to Java is that we have to declare variables and say what type each will hold. It isn't that Python doesn't have types. It's that Python checks them at run time (sometimes called duck typing — if it looks like a duck, quacks like a duck, ... it's probably a duck. Similarly, if it looks like an int
, acts like an int
, ... it's probably an int
). Java checks data types at compile time rather than at run time (Java is said to be statically typed). By saving all of those run time-checks, Java code is able to run significantly faster than Python code. It's also safer, in that once the compiler is happy, you know that you have the type of data at run-time that you expect to have. You'll get used to have IntelliJ telling you what you need to fix (code highlighted in red); while annoying, it's for your own good!
In the main
method of the code above, we first instantiate (create) an object called alice
from class Student0
. We use the keyword new
to tell Java to allocate memory for object alice
. alice
gets memory allocated for instance variables representing her name and graduation year. These instance variables are set using the alice.name
). Once the instance variables have been initialized, alice
is printed (remember, white space doesn't matter in Java, its stripped out by the compiler, so we can stretch one command over multiple lines for readability). Soon we will instantiate more objects, other than alice
, and each one (each instance of Student0
) will get their own name and graduation year instance variables. We can track multiple students by instantiating multiple objects.
Getters and setters
While setting instance variables using the dot operator (e.g., alice.graduationYear = 2027;
) works shown above, that approach is frowned upon in Java. Instead we use
Note: Student01.java andStudent02.java progressively add functionality to Student0.java.
public class Student03 {
protected String name;
protected int graduationYear;
/**
* Setters for instance variables
*/
public void setName(String name) { this.name = name; }
public void setYear(int year) {
//only accept valid years
if (year > 1769 && year < 2100) {
graduationYear = year;
}
}
/**
* Getters for instance variables
*/
public String getName() { return name; }
public int getGraduationYear() { return graduationYear; }
public static void main(String[] args) {
Student03 alice = new Student03();
alice.setName("Alice");
alice.setYear(2027);
System.out.println("Name: " + alice.name +
", Year: " + alice.graduationYear);
}
}
Student03
provides getter
and setter
methods for our basic Student class. getters
allow outside code to retrieve the value of an instance variable. Each getter method returns an instance variable's value. By convention, getters are named get<VariableName>
. We see getName
returns the student's name. Java does not enforce this naming convention, you can call your method anything you'd like. Other programmers will generally assume getters will have the same name as the variable, but Java doesn't care what you call your methods. In fact, Java doesn't know getName
is a getter. Java simply knows getName
is a method. Note that methods that return a variable provide the data type in the method declaration (String
and int
in the code above).
Setters generally do not return a value (use data type void
if a method does not return a value) but allow outside code to request a change to an instance variable's value. Setters follow the same naming convention as getters but use set
instead of get
. They typically take a parameter with the same data type as the instance variable (e.g., String
for name
and int
for graduationYear
.) Notice we called our setter setYear
not setGraduationYear
. Java doesn't know or care that this method is a setter, so it doesn't require the method to have the same name as the instance variable. Setters allow an object to provide error checking and refuse an update if the values provided are invalid. For example, the setYear
method only accepts years between Dartmouth's founding in 1769 and 2100. For now it ignores other years (we will soon throw an exception for invalid parameters to tell the caller that their input was invalid).
In the code above we instantiate a student named alice
in the main
method, and then set each instance variable one by one using the setters. This initializes alice
, getting that object ready for use. It turns out there is an easier way to intialize objects — using constructors.
Constructors
When an object is first instantiated (declared using keyword new
), Java immediately runs a special method called a constructor
. Constructors have the same name as the class and may take zero or more parameters.
public class Student04 {
protected String name;
protected int graduationYear;
public Student04() {
//default constructor: you get this by default
}
public Student04(String name, int year) {
this.name = name;
graduationYear = year;
}
public static void main(String[] args) {
Student04 abby = new Student04(); //calls first constructor
Student04 alice = new Student04("Alice", 2027); //calls second constructor
System.out.println("Name: " + abby.name +
", Year: " + abby.graduationYear);
System.out.println("Name: " + alice.name +
", Year: " + alice.graduationYear);
}
}
OUTPUT
Name: null, Year: 0
Name: Alice, Year: 2027
Class Student04 demonstrates constructors. Constructors are a way to initialize an object's instance variables. For example, an object named abby
is instantiated with Student04 abby = new Student04();
. Java will first look to see if there is a method with the same name as the class that takes no parameters. In this case it finds the constructor and runs it. This constructor does nothing. If you do not write your own constructor, by default Java creates one like this and initializes instance variables to 0 for numeric types, false
for boolean types, and null
for objects (objects are the topic of the next class). abby
's' instance variables are set to null and 0 by default. We see the result in the first line of the output.
Next, an object named alice
is instantiated with Student04 alice = new Student04("Alice", 2027);
. Here Java looks for a constructor (method with same name as the class) that takes two parameters, a String followed by an integer. It finds that method and immediately runs it after the keyword new
, setting alice
's name
and graduationYear
to the parameters provided. One the second line of output, we see alice
's instance variables are set to the parameters passed to the constructor.
One thing to note about the second constructor: it uses the keyword name
has the same name as the corresponding instance variables, Java doesn't know which variable we want if we don't specify (e.g., do we mean the instance variable or the parameter when we say name
?). To specify the instance variable, use the keyword this
and to specify the parameter, just use the parameter name. Java will choose the most local variable if you do not use this
. For example, this.name
means the instance variable name
, whereas just name
means the most local variable, the parameter.
There are two constructors in this code, but Java knows which constructor to run based on the abby
provided no parameters, Java knows to run the constructor that takes no parameters. Because alice
provided a String followed by an integer, Java finds a method with that signature and runs that code for alice
. Two or more methods with the same name (here Student04) but different signatures is called
Other methods
Code that operates on an object's instance variables are called methods
in Java. Getters, setters, and constructors are examples of methods, but objects can have other methods. For example, suppose we want to track how many hours each student spends in class and studying. We can add instance variables for studyHours
and classHours
and provide methods, study
and attendClass
to track the numbers of hours spent studying and in class.
public class Student05 {
protected String name;
protected int graduationYear;
double studyHours;
double classHours;
public double study(double hoursSpent) {
System.out.println("Hi Mom! It's " + name + ". I'm studying!");
studyHours += hoursSpent;
return studyHours;
}
public double attendClass(double hoursSpent) {
System.out.println("Hi Dad! It's " + name +". I'm in class!");
classHours += hoursSpent;
return classHours;
}
public static void main(String[] args) {
Student05 abby = new Student05(); //calls first constructor, default instance variables
Student05 alice = new Student05("Alice", 2027); //calls second constructor
alice.study(1.5);
alice.attendClass(1.1);
}
OUTPUT
Hi Mom! It's Alice. I'm studying!
Hi Dad! It's Alice. I'm in class!
toString
When we define a class, Java does not know its semantic meaning. Our Student classes above may make sense to a human, but Java doesn't know what a "student" is. If we print an object that was instantiated from one of our classes, because Java doesn't know what the class represents, by default it simply prints a value based on the object's memory address. We can, however, provide a toString
method that returns a String representation of the object that makes sense to a human. For example:
public class Student06 {
protected String name;
protected int graduationYear;
double studyHours;
double classHours;
public String toString() {
String s = "Name: " + name + ", graduation year: " + graduationYear + "\n";
s += "\tHours studying: " + studyHours + "\n";
s += "\tHours in class: " + classHours;
return s;
}
public static void main(String[] args) {
Student06 abby = new Student06(); //calls first constructor, default instance variables
Student06 alice = new Student06("Alice", 2027); //calls second constructor
System.out.println(abby);
alice.study(1.5);
alice.attendClass(1.1);
System.out.println(alice);
}
}
OUTPUT
Name: null, graduation year: 0
Hours studying: 0.0
Hours in class: 0.0
Hi Mom! It's Alice. I'm studying!
Hi Dad! It's Alice. I'm in class!
Name: Alice, graduation year: 2027
Hours studying: 1.5
Hours in class: 1.1
Now when an object of type Student06
is printed, behind the scenes Java calls the toString
method, which returns a String representing the object. Note: '\n' adds a newline character and '\t' adds a tab character to the String.
Encapsulation
Objects vs. primitives
Java keeps track of its variables in an area of memory called the double
are 8 bytes so Java allocates 8 bytes on the stack for each double
.
Java stores objects in another area of memory called the
An important note about primitive vs. object types: double
is a "primitive" type, as are int
, char
, and boolean
(lower-case type names), variables of those types do not refer to an object but rather just simple number (or character) stored directly in memory. If a variable is of a primitive type, the stack contains the actual data itself (the bit pattern representing the data). If a variable is a reference to an object, then it contains a reference, and the object's data is stored on the heap.
Why is this distinction important? My wife and I have a joint checking account. We each have an ATM card. The cards are different and have different names on them, but the refer to the same checking account. If I withdraw money with my ATM card, there is less money in the account, and if my wife then looks at the balance it will be smaller even though she did nothing with her ATM card. In this analogy, the account is the object (and bank account objects are a common example in textbooks). The ATM cards are the variables, each holding a reference to the same account. Any changes made by either of us to the account via our ATM card are seen by both. On the other hand, if my wife has her ATM card re-programmed so that it refers to her personal account (changes the reference stored in the variable), that won't affect my card or the account. She just will no longer be able to use that ATM card to withdraw money from our account, because it no longer refers to our account.
Consider this code:
Student06 alice = new Student06("Alice", 2027);
Student06 bob = new Student06("Bob", 2025);
Student06 abby = alice;
There are two different students here, created with the two new
statements. One of these students has two "names" (called "aliasing"), with variables abby
and alice
referring to the same object. So:
abby.setName("Ainsley");
System.out.println(alice.getName()); // => prints Ainsley
It doesn't matter whether we refer to the object by its abby
name or its alice
name; it's the same memory location on the heap. Recuse they both reference the same memory location on the heap, changing abby
's name also changes alice
's name. Now both have the name "Ainsley". The student that bob
refers to is an entirely separate object piece of memory on the heap. bob
's name is not changed in this example.
Applications vs. classes
Often we will create classes to model an entity such as a student, but will create a separate program that uses them. These programs are often called application or driver programs and they provide the business logic to accomplish a useful task. For example, an application might be called StudentTrackerApp
and might track several Student
objects. In that case, StrudentTrackerApp
with have a main
method and other associated methods to track students at a college, but the Student
class will not have a main
method. Student
doesn't need a main
method, because it is not meant to run on its own. Instead Student
is meant to be used by StudentTrackerApp
. In Java, we create one class to represent students (stored on disk as Student.java
) and another class called StudentTrackerApp
(stored on disk as StudentTrackerApp.java
) to provide the application's logic. We provide Student07.java as a Student
class that does not have a main
method. Other application programs (that do have a main
method can use this Student class.
Java notes
Again, this isn't a comprehensive reference to the language; the textbook and on-line references provide much more detail. But hopefully it gives sufficient intuition and an organizational structure. Give a yell if I've missed something important.
- class
- A class is like a blueprint; a set of instructions that describe how to build an object. It can contain instance variables and methods (code) that operates on the instance variables.
- object
- An instance of a class. Objects are instantiated (created) by using the keyword
new
. Multiple objects can be instantiated from the same class, just like multiple houses can be constructed from the same blueprint. - instantiation
- An object is created by the
new
operator, with the name of the class and any parameters needed to initialize the object as defined by the class. Instantiation allocates memory for the object and "brings it into being." - instance variables / fields
- These store data specific to an object; they are declared outside any method.
- local variables
- These hold values temporarily in a method. They are declared inside methods.
- parameters
- These carry values into a method.
- constructor
- Creating an object creates its instance variables. Java will initialize these (to 0 for numbers, false for boolean, null for references to objects). However, these are seldom what we want. Java provides a special type of method called a constructor, that has the same name as the class. It called via
new
, and is responsible for giving all of the instance variables appropriate values. It can take parameters, too. - method
- Code that operates on an object's instance variables. A method performs an operation for an object. It is defined inside the class, and includes a method name, list of parameters (parameter names and types), and return type (before the name), followed by a body, which is a code block to be executed. Other languages might call methods a function or subroutine.
- method invocation
- To ask "object" to perform "method" with "parameters" (comma separated values), we write
object.method(parameters);
- dot operator
- A way to access an object's instance variables. For example,
alice.name
. In Java we typically use getters and setters to get or update instance variable values. - getter
- A method that returns an instance variables value.
- setter
- A method that updates an instance variable to a value passed in as a parameter. Setters may perform error checking and may refuse to update an instance variable if an invalid value is passed as a parameter.
- encapsulation
- Bringing code (methods) and data (instance variables) into one thing called an object. C programmers, think
structs
but also with functions. - return
- This exits a method immediately, and if the method returns a value, specifies what to pass back. Methods that don't return values ("void" type) just have "return" all by itself to return early, or just naturally return at the end of the code block.
- return type
- If a method is to return a value, the type of that value is specified. If doesn't return a value, "void" is indicated as the return type.
- this
- Refers to the object itself. In constructors and setters, it can be helpful to distinguish a parameter that has the same name as the instance variable (e.g.,
this.x = x
wherethis.x
refers to the instance variable andx
is the parameter). - signature
- The signature of a method is the name of the method plus the number and type of parameter it takes along. A class may have multiple methods with the same name, but with different numbers and types of parameters.
- overloading
- When a class has multiple methods with the same name, but with different parameter lists, the method is said to be overloaded.