Lecture 03: Java review: Classes, Objects, and Methods
Announcements
A reminder: all questions/answers about course material and assignments should go on Piazza. Feel free to remain anonymous if you like, or to ask privately questions that you think are not relevant to the class. Please only email me or Garrett if you have an administrative issue (for example, you attended class but we didn’t give you credit for the in-class exercise).
A note about the optional textbooks for this course: Yes, the books and reading are optional. But optional does not automatically equal “I don’t need to get the books or do the reading.” I’m sensitive to making you buy books you don’t need to (and you’ll note that many of the texts are available online for free). But if you need reference material, or an alternative to my presentation of the material, or a reference, the texts may help.
If you’re a late add, you must contact me about what you need to do to catch up. SPIRE does not notify me about late adds, and the CS main office only knows about overrides into the course. If you used SPIRE to add yourself and haven’t been in touch with me, I have no idea that you’ve added late.
Methods
In Java, methods are always attached to a class. A method consists of a declaration and a body, which is just a sequence of statements. Let’s look at a declaration more closely:
public static void main(String[] args) {
...
}
WTF is going on here!?!?! is usually the reaction we get in 121 when people first start learning Java. But now, you probably know enough to understand most of it. Let’s tackle it inside-out.
String[] args
is the parameter to this method. One or more parameters are passed in, and they look like (and in many respects behave as) variable declarations. The difference is that their values are provided by the calling method (or in the very special case of main
, by the JVM). Here, main
gets an array of Strings, which are exactly what is passed on the command line to the java interpreter, e.g.,
public class PrintArgs {
public static void main(String[] args) {
int i = 0;
for (String a : args) {
System.out.println(i + ": " + a);
}
}
}
> javac PrintArgs.java
> java PrintArgs Hello Marc!
0: Hello
1: Marc!
Next is the name of the method, which is by convention “camelCased,” starting with a lowercase letter. Next is the method’s return type, or void
if it does not return anything.
Important note: methods that return void
typically do something, like print a value, or delete an item from a list, or the like. They affect state, or are stateful. Sometimes but not always, methods that return a value don’t do something (they are more like mathematical functions). The only way to be sure is to read the documentation (or the method code itself)! But a method’s public API should describe what it does.
Next comes one or more method modifiers: either abstract
or one or more of static
, final
, and synchronized
. static
methods are associated with a class, but not a particular instance of that class (in other words, not with an object). We’ll talk more about the other modifiers later as they come up.
Finally there is at most one of public
, protected
, or private
, which are member access modifiers. Which objects can invoke this method is determined by this modifier. public
and private
are probably familiar to you (any object and only objects of this class, respectively). protected
and no modifier (“default access” or “package access”) have special meanings we’ll skip for now.
Classes and objects
Java is said to be “object-oriented”, which means that the language strongly encourages the use of objects as a paradigm for problem solving.
What’s an object? A collection of related state and behavior. In Java, this means objects are typically variables and associated methods. The “blueprint” for objects is a class. Classes are also the place where we stick stuff that’s not necessarily part of an object. (This falls out of Java’s design: everything is part of a class.)
Classes in the JVM
(Note that like our discussion of the call stack, this is a simplified version to give you a mental model of how things work; the implementation in the JVM differs somewhat.)
A single copy of the class (note: the class! not an object of class!) lives in memory, annotated with its name, each method, and space for each static
variable. There also some other stuff: a pointer to the class’s superclass, and a list of interfaces it implements, and the like.
Objects that are instances of the class will be able to refer to these methods and variables here.
Instantiating objects
Objects are defined by a class. But an object does not exist until you instantiate it, that is, “make a new one” using the class as a template for the new object. For example:
Bus bus44 = new Bus();
There’s a class called Bus
, and we’ve instantiated an object of type Bus
. The object is named by the variable bus44
.
Thinking back to our first class, what’s going on in memory when this happens?
Unlike primitive types, which are stored directly in the variable’s allocated space, the variable “holding” an object isn’t really the object’s value. It’s a pointer or reference to the place where the object really lives.
This is really important to understand, so I’ll say it again: what Java stores in primitive type variables and object variables is different, conceptually: The former stores the value itself, the latter stores as its value the memory address of the object. Weird but true!
Let’s do an example. What happens here?
String message = new String("Hi!");
String wassup = message;
First a new String
is allocated. Then it’s initialized on the heap. Then a new variable of type String
, named message
, is created. Its value is set to the address of the actual String
object we created.
Note that this String
object implicitly refers to a String
class somewhere else. When you call methods on an object, it uses this reference to find the method – only one copy of the code for a method exists at a time – all objects of a class share it.
Next, another new variable, again of type String
, named wassup
is created. The address of message
is looked up, and is then assigned to wassup
. Both variables now “point to” or “refer to” the same object, which is a String
object containing the data "Hi!"
.
Having two variables refer to the same object is called “aliasing”; it is a common source of program bugs. Always think carefully about the value stored in a variable, not just the name of the variable.
So this leads to an important thing: ==
vs .equals()
. With primitive types (int
and so on), you only have one option: ==
. What does it do? It looks up the values stored in the variables, and returns true
if and only if they are the same. x = 3; y = 3; x==y;
But what happens when we use ==
on variables that refer to objects? Exactly the same thing! Which might or might not be what we mean. Following from above, let’s add String hello = new String("Hi!");
, and ask, does message == wassup
? Yes, because they refer to the same object.
Does message == hello
? No. Even though the two String
objects represent the same value (“Hi!”), they are stored in separate objects at different addresses. So the variables store the value of two different addresses.
But oftentimes we don’t actually want to know if two variables point to the same objects. Instead, we want to know if the two objects’ values are equivalent. In the case of String
s. this means that they hold the same text; you can imagine that for more complicated objects, we might need a more complicated comparison of the two objects’ instance variables. There is a method .equals()
that a class can implement to provide a class-specific test of equality. (If you don’t implement it, the Object
class’s equals()
method, which defaults to ==
, is used.)
So we can write message.equals(hello)
to check if the two objects store equivalent String
s, rather than the two variables storing the same address as a value.
In-class exercise
Bus busA = new Bus();
Bus busB = new Bus();
Bus busC = busA;
busA.setNumber(44);
busB.setNumber(44);
System.out.println(busA == busB);
System.out.println(busA.equals(busB));
busC.setNumber(13);
System.out.println(busA == busC);
System.out.println(busA.equals(busC));
What are the four lines of output (true/false
)?
Using objects and methods
If the flow of control is executing within an object, and we access a variable, say x
, what happens?
First the local scope is checked, inside out, for example:
class Bar {
int x = 0;
private void foo(int j) {
for (int i = 0; i<x.length; i++) {
x[i] = j;
}
}
}
x
is not declared in the for
loop, nor in the method body, nor as a parameter, so the next place that’s checked is the object itself, where it’s found. (The superclasses, etc.) This all happens at compile-time; you can’t fail this lookup in a source that correctly compiles.
Relatedly, when methods are called on an object, the type of the object is examined, and the method is looked up in the corresponding class. If it’s not found, the class’s superclass is checked, and so on. Again, this is checked at compile-time. This is how the default “equals” method works; it’s implemented in Object
, which is by default the superclass of all classes that don’t otherwise declare a superclass. In other words, it “inherits” the method from its superclass.
We’ll do (slightly) more on inheritance, but best practices over the years have moved toward a relatively flat class hierarchy. Usually you won’t see (or use) deep inheritance, with some exceptions for older codebases and large libraries (like, say, the Java Platform API).