Chapter 8.J

Java Objects

Objects in Java

Subsections of Java Objects

Instance Classes

Now that we understand the basics of what classes and objects are in object-oriented programming, let’s look at how to create these items in Java.

Creating an Instance Class

Creating a class in Java is very similar to creating a method. The syntax is <access modifier> class <ClassName> {<body>}. We will use public for all our class-access modifiers. The class definition (body), like all Java code-bodies, is enclosed in {}.

Java requires just a single public-class in each file, with the filename matching the name of the class, followed by the .java file extension. By convention, class names in Java should be nouns, in mixed case (Pascal-case) with the first letter of each internal word capitalized1.

So, to create an empty class named Ingredient, we would place the following code in a file named Ingredient.java:

public class Ingredient {

}

As we’ve already learned, each class declaration in Python includes these parts:

  1. public - an access modifier enabling other parts of the code to “see” the class
  2. class - this keyword says that we are declaring a new class.
  3. Ingredient - this is an identifier that gives us the name of the class we are declaring.
  4. {}- an empty body that does nothing.

Following the declaration, we see a curly brace { marking the start of a new block, inside of which will be all of the fields and methods stored in this class. We should indent all items inside of this class, just like we do with other blocks in Java.

In order for Java to allow this code to compile, we must have a body. The { } can be empty but cannot be missing.

Objects & Initialization

Of course, our classes are not very useful at this point because they don’t include any attributes or methods. Including instance attributes in an object class is one of the their basic uses, so let’s start there.

Types of Attributes

There are two types of attributes: Class and Instance.

Instance attributes are variables that “belong” to the instance. It makes sense that a Student object owns its own name.

We will defer discussion of class attributes to a later module.

Instance Attributes and the Constructor

To add instance attributes to a class, we can simply place a variable declarations with access modifiers inside the class but outside of any methods.

public class Ingredient{
    
    public String name;
    public double amount;
    public String units;
}

This tells the compiler that each instance of “Ingredient” will have three variables. This is one of the ways that objects store their data. It is possible to assign values at this point, i.e. public int amount= 2;, but this stylistically bad. Default values should be assigned in the constructors.

The Constructor

The Constructor is a method named after the class. It called each time an object is instantiated; it gets triggered by use of the new keyword. Typically the constructor sets default values for the instance attributes.

The Java compiler creates a default, no parameter constructor for every class.

public Ingredient(){} // provided by the complier if not overwritten

But it is normal to override this definition with a constructor if your own.

public Ingredient(){
    this.name = "flour";
    this.amount = 2.0;
    this.units = "cup";
}

Typically, all instance variables should be given a value in the constructor. This gives programmers one place to look for all the names and values of all the instance variables in the object. Also, the constructor(s) should always be the first method(s) inside an object class’s definition.

this

Java uses this to refer to the specific object used when calling a method. It is the mechanism that ensures the Ingredient object first sees first’s data and Ingredient object second sees second’s data. It is typically used for clarity.

public Ingredient(String name, double amount, String units){
    this.name = name;
    this.amount = amount;
    this.units = units;
}

Here the parameter names obscure (or shadow) the instance variable names, and the use of this clarifies the code. We assign the values of the parameters to the instance attributes of the same name. “Shadowing” instance/class names with parameter names is considered bad coding style anywhere except in constructors. Even in constructors is is easy to avoid, such as using public Ingredient(String nameIn, int amountIn, String unitsIn).

YouTube Video

Feel free to refer to the UML diagram below to find the correct instance attributes for the Ingredient class so far.

UML Class Diagram showing Ingredient UML Class Diagram showing Ingredient

Instances & Testing

Your class should now look something like this, although your default values may be different:

public class Ingredient{
    public String name;
    public double amount;
    public String units;

    public Ingredient(){
        this.name = "flour";
        this.amount = 2.0;
        this.units = "cup";
    }
}

As classes grow large you will want to test methods as you add them. In CC 410 we will learn about test frameworks and will write formal tests in parallel with project development.

For now we will follow the convention of a main method in each class which can be used for testing. The basic flow would be:

  1. Create class
  2. Write the constructor, it may be blank
  3. Create main(args), instantiate an object and test (print out) the initialized value to test for correctness.
  4. Compile and run the class as a program
  5. Repeat until done:
    1. Create new method
    2. Update main(), usually involves instantiating and object and using it to call the new method
    3. Run and check output

Instantiation

To instantiate, or create, an object in Java, we use the keyword new and call the class constructor.

Ingredient ingr1 = new Ingredient();

This will create a new Ingredient object, and then store it in a variable of type Ingredient named ingr1. While this may seem a bit confusing at first, it is very similar to how we’ve already been working with variables of types like int and double.

Accessing Attributes

Once we’ve created a new object, we can access the instance attributes as defined in the class from which it is created.

For example, to access the name attribute in the object stored in ingr1, we could put the following code in main()1

Ingredient ingr1 = new Ingredient();
String n = ingr1.name; // n is assigned the current value of
                       // ingr1's name attribute
System.out.println(n == ingr1.name); // prints true they are equal

Java uses what is called dot notation to access attributes and methods within instances of a class. So, we start with an object created from that class and stored in a variable, and then use a period or dot . directly after the variable name followed by the attribute or method we’d like to access. Therefore, we can easily access all of the attributes in an Ingredient object using this notation:

Ingredient ingr1 = new Ingredient();
System.out.println(ingr1.name);
System.out.println(ingr1.amount);
System.out.println(ingr1.units);

We can then treat each of these attributes just like any normal variable, allowing us to use or change the value stored in it:

Ingredient ingr1 = new Ingredient();
System.out.println(ingr1.name);
System.out.println(ingr1.amount);
System.out.println(ingr1.units);

n = ingr1.name;
String ingr1.name = "cardamom";
System.out.println (n == ingr1.name); // False they are not equal we changed ingr1.name

How to Test

When testing it is important to avoid “feature creep” in the class. We want to avoid adding add attributes or methods that are not called for by the UML class diagram. In software development you will drive up test and maintenance cost2.

In this class it is always acceptable to add a private static void main() method, even if it is not on the UML, to facilitate testing. We will put all our test code for instance classes in main().

public class Ingredient{
    public String name;
    public double amount;
    public String units;

    public Ingredient(){
        this.name = "flour";
        this.amount = 2.0;
        this.units = "cup";
    }
    private static void main(String[] args){
        Ingredient ingr1 = new Ingredient();
        System.out.println(ingr1.name);
        System.out.println(ingr1.amount);
        System.out.println(ingr1.units);    
    }
}

This is how one might test a constructor method, we check to see all object variables have the correct values.

Things to keep in mind for this course:

  • YOU need to know what the answer (print out) should be
  • Never use input (keyboard or file) in your test code
    • Hard code the values you want to test with
  • Consider deleting main() from instance classes before submitting a Project
  • Consider deleting this code after testing each method
    • You can continue to append to the end but it makes for a long program
What if the Class Needs a Main Method?

Write main()’s actual functionality last, and possibly move your testing code to the test() method. Consider this structure:

private static void test(){  put you test code here}
public static void main(String[] args){
    test(); // delete this line when you are done testing
            // and ready to start writing main
}

It encapsulates all the test code in test and keeps main pretty clean. When you are satisfied that everything but main() works, delete the call and work on main().

The only way to test main()’s functionality will be from the terminal.


  1. Recall that java always looks for a public static void main (String[] args) method to run: ↩︎

  2. Software maintenance is estimated to be 60 - 75% of the total cost of ownership for a software project. ↩︎

Instance Methods

We can also add additional methods to our classes. These methods are used either to modify the attributes of the class or to perform actions based on the attributes stored in the class. Finally, we can even use those methods to perform actions on data provided as arguments. In essence, the sky is the limit with methods in classes, so we’ll be able to do just about anything we need to do in these methods. Let’s see how we can add methods to our classes.

As with attributes, there are class and instance categories. Class methods are underlined on the UML diagram and will be discussed in a later module.

Adding Methods

To add a method to our class, we can simply add a method declaration inside of our class declaration. So, let’s add the methods we need to our Ingredient class:

public class Ingredient{
    public String name;
    public double amount;
    public String units;

    public Ingredient(){
        this.name = "flour";
        this.amount = 2.0;
        this.units = "cup";
    }

    public String toString(){
        return String.format("%.2f",amount) + " " + units + " of " + name;
    }
}

The toString() method is pretty straightforward. When that method is called, we simply return a string of <amount> <units> of <name>. toString() has a special meaning in Java, it is the method called whenever an object is coerced (automatically converted) to a String.

So that we get consistent results, we used the format specifier%.2f in String.format(), to round the amount off to two decimal places. Used in this manner:

String.format("<format specifier>", <variable>)

String.format() returns a string representation of variable formatted according to the format specifier. See geeks for geeks for a specifier list.

Variable Scope

We’ve already discussed variable scope earlier in this course. Recall that two different functions may use the same local variable names without affecting each other because they are in different scopes.

The same applies to classes. A class may have an attribute named age, but a method inside of the class may also use a local variable named age. Therefore, we must be careful to make sure that we access the correct variable, using the this reference if we intend to access the object’s attribute’s value in the current instance. Here’s a short example:

public class Test{
  public int age;

  public Test(){
     this.age = 15;
  }
  
  public void foo(){
    int age = 12;
    System.out.println(age);     // 12
    System.out.println(this.age);// 15
  }

  public static void main(String[] a){
    Test temp = new Test();
    temp.foo();
 }
}

As we can see, in the method foo() we must be careful to use this.age to refer to the attribute, since there is another variable named age declared in that method.

For the convert() method, lets specify objects of type Ingredient must have units of “cups” or “ml” (milliliters). As parameters, it accepts a reference to the current instance named self, and value for conversion units. It must determine if a conversion is necessary. If so, it must update the attributes units and amount. There are 236.588 milliliters in a cup.

public void convert(String units){
    if (this.units.equals("cups") && units.equals("ml")){
        this.units = "ml";
        amount *= 236.588;
    }else if(this.units.equals("ml") && units.equals("cups")){
        this.units = "cups";
        amount /= 236.588;
    }
}

Let’s go ahead and add the scale() method as well. That method should accept a single double as a parameter It should: (1) create a new object, (2) copy its object attributes to the new object, (3) scale the new object’s amount by the scaling factor.

public Ingredient scale(double factor){
    Ingredient output = new Ingredient();
    output.name = this.name;
    output.units = this.units;
    output.amount = this.amount * factor;
    return output;
}

UML UML

Driver Class

The Driver class is much simpler. It has one feature, a class method public static void main(String[] args)1. Everything that needs to be done will be done in the main method. A template might be:

public class Driver{
    
   public static void main(String[] args){

   }
    
}
Class vs. Static Features

A class feature belongs to the class, no object is necessary to use it. Examples include math methods we write

double five = Math.sqrt(25.); 

to access the class function sqrt(). We do not have to create a Math object first:

Math mo = new Math();       // Incorrect
double five = mo.sqrt(25.); // Incorrect

Java uses the keyword static as a function modifier to create class methods and attributes. Not all languages use the static keyword in the same way. Be careful when using static in discussing class-level features. In Java they are virtually synonymous, but in general they are not.

Accessing the Instance Class

Lets add an instance Ingredient as i1.

public static void main(String[] args){
    Ingredient i1 = new Ingredient();
}
How Does the Compiler Handle User-Defined Classes?

When the java compiler (javac) sees an identifier it does not understand, it looks in

  • the rest of the file being compiled
  • the classes covered by the import statements
  • the directory the .java file is in
    • If it finds a .java file it also compiles it
    • If it finds a .class file it uses it

So when our Driver.java uses Ingredient, the java compiler (re)compiles Ingredient.java.


  1. In Java, class methods have the modifier static ↩︎

A Worked Example

Let’s put this all together by finishing our project with a full worked example. We want to use our Ingredient class to help us bake some sugar cookies. Unfortunately, the recipe uses both cups and milliliters interchangeably, and we want to be able to scale the recipe up or down depending on how many batches we need to make.

So, let’s build a program that performs that task for us. It should first ask the user to choose a desired unit of measurement by selecting a number, with 1 representing “cups” and 2 representing “ml”. Then, it should also ask for a scaling factor as a decimal number. Finally, it will print out the required ingredients in the correct units and scaled to the correct scaling factor.

The recipe that we have is as follows:

  • 2.75 cups of flour
  • 1 cup of butter
  • 1.5 cups of sugar
  • 2.6 ml of baking powder
  • 15.0 ml of baking soda
  • 15.0 ml of vanilla extract

First, let’s start with our existing Ingredient class from this module:

UML UML

public class Ingredient{
    public String name;
    public double amount;
    public String units;

    public Ingredient(){
        this.name = "flour";
        this.amount = 2.0;
        this.units = "cup";
    }

    public Ingredient(String name, double amount, String units){
        this.name = name;
        this.amount = amount;
        this.units = units;
    }

    public String toString(){
        return String.format("%.2f",amount) + " " + units + " of " + name;
    }

    public void convert(String units){
        if (this.units.equals("cups") && units.equals("ml")){
            this.units = "ml";
            amount *= 236.588;
        }else if(this.units.equals("ml") && units.equals("cups")){
            this.units = "cups";
            amount /= 236.588;
        }
    }

    public Ingredient scale(double factor){
        Ingredient output = new Ingredient();
        output.name = this.name;
        output.units = this.units;
        output.amount = this.amount * factor;
        return output;
    }

}

To complete this example, we want to write a Driver class that uses two user inputs - a value for units and a scaling factor. It should then print out the ingredient list for sugar cookies using those units, and scaled by the given scaling factor.

Setting up the Driver Program

First we write the generic driver program and class skeleton:

import java.util.Scanner;

public class Driver{

   public static void main(String[] args){

   }
    
}

Handling the Input

Our program takes 2 inputs. First, we should ask the user which units should be used. Since we only have two options, it makes sense to just offer those options and allow the user to input a number to select from them. For the scaling factor, we can just ask for any decimal value:

public static void main(String[] args){
    Scanner scanner = new Scanner(System.in);
    System.out.println("Enter 1 for cups or 2 for ml: ");
    int option = scanner.nextInt();
    while(!(option == 1 || option == 2)){
        System.out.println("Error! Unrecognized option");
        System.out.println("Enter 1 for cups or 2 for ml: ");
        option = scanner.nextInt();
    }
    String units = "";
    if (option == 1){
        units = "cups";
    }else {
        units = "ml";
    }

    System.out.println("Enter a scaling factor as a decimal number: ");
    double scaleFactor = scanner.nextDouble();

    // more code here. 
        
}

Handling an Ingredient

Now that we have our input, we can handle the first ingredient. We need to create an object that represents 2.75 cups of flour, then convert it, scale it, and print it.

    // Create the object
    Ingredient flour = new Ingredient("flour", 2.75, "cups");

    // Scale the ingredient - this returns a new object
    Ingredient scaledFlour = flour.scale(scaleFactor);

    // Convert the units - this updates the object
    scaledFlour.convert(units);

    // Print the output
    System.out.println(scaledFlour);

To finish the program, we can repeat the same process for all ingredients. This is left as an exercise for the reader!

What's Next

There is no “project” for this module. Instead the basics of object classes as well as the use of objects will be reinforced in the next few modules covering aggregate data, strings, exceptions and file system interactions.