Chapter &

Objects

Modelling the Real World in Code!

Subsections of Objects

Modeling the Real World

Papercraft Image of Trees, Birds and Clouds Papercraft Image of Trees, Birds and Clouds1

Let’s take a look at the world around us. There are many things we might see. A computer. A keyboard. A chair. A desk. If we look outside, we may even see more things. A tree. A bird. A cloud.

From a certain point of view, the entire world is made up of things, each with unique features and actions that help define how it differs from other things.

As we continue to write more and more complex programs, it would be very useful to have a way to represent these things in our own software. Thankfully, we can! In this chapter, we’ll learn all about classes and objects, which form the basis of the object-oriented programming paradigm, one of the most common and popular programming paradigms today.

Let’s get started!

Object-Oriented Programming

Data Centric

At its heart, object-oriented programming is about the data. We may talk about how a class stands for a blueprint or outline of a real world item, but what we mean is the data that describes and defines that real world object. It is how we choose to model this data, how we allow access to it and manipulate it, that drives the object-oriented paradigm.

There are four pillars to object-oriented programming, all having to do with data and how it can be accessed and changed.

Encapsulation

Encapsulation refers to data and method hiding. The idea is things outside of the object should not directly access the object’s data. This ensures the data remains consistent with object we are modeling. A Car class might have a speed attribute, and there might maximum values for changing the speed–a max braking or acceleration. Additionally there might be a maximum speed.

Encapsulation says, when you ask the Car object how fast it is going, the object does not give you access to its data, but instead provides you a copy. When you want it to speed up, you don’t change the object’s speed directly; instead you use the object’s accelerate method, and the object changes its own speed.

Inheritance

Inheritance is the idea that like objects share traits, and can go from the generic to the specific. We might have Bird class, with generic sing and move methods, and a wingspan attribute. But then we might have a Parrot class, which is a kind of bird, so it too can sing and has a wingspan; but in this case a Parrot might also have a talk method, and an additional colorScheme variable. Inheritance is a way to link classes so that the the subclass is a super set of the base class – it has all the superclass’s stuff and more.

Abstraction

Abstraction deals with leaving things un-defined. In our Bird class, maybe there is no body (no code) to the move method, just the fact that such a method should exist. The concept that birds move is independent of a particular kind of bird. This would allow a Penguin class to code move as swimming, Ostrich to code it as running, and Parrot as riding on pirate shoulders.

Polymorphism

Polymorphism is the idea that the same method can give you different behaviors. Say we code the Bird class with the beautiful song of the musician wren. But the Eagle class, also a kind of Bird, might override this with a loud screech. Overriding is when a subclass replaces a superclass method. A Parrot and Eagle are both instances of the Bird superclass, but you get different behaviors (sounds) when you invoke their sing method.

That Seems Complicated

It is a lot, but it can be learned a little at a time. This later modules will deal with some basics of encapsulation, and introduce inheritance. Abstraction and polymorphism will be taught more deeply in later courses.

Instance & Driver Classes

Thus far in this course we have always had just one class, and in most cases one method. So, while we have been following some object-oriented conventions like starting in main(), our designs have not followed an object-oriented programming paradigm. This was deliberate, as the first few modules are necessary to cover the basics of program control.

We will begin bending our designs toward object-oriented programming with this module by introducing instance and driver classes. The instance class holds certain data and all the methods to access and manipulate that data. The driver class (typically only a main() method) holds the logic for how and when to use the data. The driver generally only has indirect accesses to the instance’s data through its methods1

Driver instance image Driver instance image

One of the things that makes OOP so powerful is this simple driver-instance idea can be used to model fairly complex real-world things. For example when you use a web browser for research. You act as the driver, you know how to uses the browser and the information you want. The browser knows how to interact with the internet and all the little details to fetch and display information.

Real world driver instance example Real world driver instance example


  1. this restriction is relaxed in this tutorial. ↩︎

Classes

The first step in creating a program that can represent things in the real world is to determine which things we’d like to include in our program, and then create classes that can describe the different types of objects.

Class

In programming, a class describes an individual entity or part of the program. In many cases, the class can be used to describe an actual thing, such as a person, a vehicle, or a game board, or a more abstract thing such as a set of rules for a game, or even an artificial intelligence engine for making business decisions.

In object-oriented programming, a class is the basic building block of a larger program. Typically each part of the program is contained within a class, representing either the main logic of the program or the individual entities or things that the program will use.

Every Class is a Type

Every time we define a new class, we create a new type.

public class Dog {
    // class definition for Dog
    String name;

    public Dog(String aName){
        this.name = aName;
    }
}

public class Driver {
    public static void main(String[] args) {
        Dog x = new Dog("rover");  // Dog is now a data type
        Dog y = new Dog("spot");   // We use it when declaring a variable
    }
}

Example

Teacher and Student Clipart Image Teacher and Student Clipart Image

For example, let’s consider a program that could be used to store the information about students and teachers at a school. For this program, we could create 2 instancs classes: Student, Teacher, and a driver class called Main. To help represent this program, we can use a UML Class Diagram like this:

UML Class Diagram showing Main, Student, and Teacher class UML Class Diagram showing Main, Student, and Teacher class

In the diagram above, we see three boxes, one labeled for each class. Below the names we see entries for the fields and methods in the class, which we’ll discuss on the next page.

Obviously, the Student class can be used to represent a single student in school. Likewise, we’ll use the Teacher class to represent a teacher. Finally, we’ve also included a Main class, which will store the actual logic for the program we’re creating. However, right now the classes are just names, and aren’t very useful in that form.

UML Diagrams

The Unified Modeling Language or UML is a standard way to visualize the structure and design of a software program. UML includes many different types of diagrams, including class diagrams, use case diagrams, sequence diagrams, and more.

In this course, we’ll use some simple class diagrams to help describe the structure and layout of classes in our programs. Those diagrams are very simple to read and understand, and you won’t be asked to create any diagrams of your own right now. If you take some later programming courses, we’ll cover more information about UML diagrams and how to work with them there.

If you want to learn more, here are a few helpful links:

Object Features

YouTube Video

Video Materials

To make our classes more useful, we must give them features to help define the properties and actions of that class. So, let’s look at each of those in turn and discuss how we might use those in our programs.

Object Attributes

First, each class can have a set of attributes, sometimes known as fields, to describe the data stored by that class. In programming, these would be the variables stored within the class itself. These attributes represent the different properties of the thing the class represents, helping to distinguish it from other things in the world.

For example, an Ingredient for a recipe might have a name of the ingredient, an amount and a units. These might be “flour”, 4 and “cups”.

Inside a Unified Modeling Language (UML) class diagram, instance attributes go in the section immediately under the class name. Typically both attribute’s proposed identifier and type are included.

UML Class Diagram showing Ingredient UML Class Diagram showing Ingredient

Object Methods

In addition, each class can have a set of methods or actions that it can perform. In programming, these are the methods stored available to the object to help manipulate or provide the object’s data. Lets assume we have and object ingrd1 of type Ingredient with the following attributes: name = "flour", amount = 3.0, unit = "cup".

These methods may represent actions taken directly on the attributes. Our Ingredient class has three methods:

  • toString(): returns a string describing the object; Something like amount + units + " of " + name
    • ingrd1.toString() would return the string “3.0 cup of flour”
  • scale(factor): returns a new ingredient object scaled to the provide factor
    • ingrd2 = ingrd1.scale(2.0) results in
      • ingrd2 with name = "flour", amount = 6.0, unit = "cup".
      • ingrd1 is unchanged
  • convert(units): returns nothing. Changes the object’s unit attribute to the provided value and adjusts the objects amount attribute so that is correct for the new units
    • ingrd1.convert("ml") results in ingrd1 now containing name = "flour", amount = 709.1, unit = "ml"

Here, each instance method is listed in the lower part of the UML class diagram. It is annotated with the types of its expected parameters and return value or void if the method does not return any value1

Designing Classes

When designing a class for a program, it is important to make sure that each class includes the attributes and methods needed to represent the object fully within the program. However, we also don’t need to include every single attribute and method we can think of. Sometimes it is best to be as simple as possible, only including the ones that will be used within the program. This helps make our code simple and easy to read.

A great way to start is to make a list. We can ask ourselves questions such as “what information is needed to identify a single student?” or “what actions can a teacher perform in this program?” Typically the answers to those questions will help us build our classes, and eventually build our entire program.

Modeling

Classes are not typically modeled in pseudo code. The design function usually creates and UML diagrams from which developers work. Developers usually use pseudo code to develop individual methods or work through the logic of complex method call-chains.


  1. Some UML diagrams may also use void instead of an empty parameter list if the method takes no parameters. ↩︎

Subsections of Object Features

Instantiable Classes

A class can serve many functions, the most basic of which is to serve as a blueprint for the data and methods used to access and manipulate that data. We will refer to these as “object” or “instance” classes. An object class is a class with the primary purpose of encapsulating and manipulating related data.

When you instantiate an object of a class, you create a variable of that type. The class definition is only a only the specification for each of those items. So, let’s look at the next step, which is to create objects based on the classes we’ve defined.

Objects

Once we’ve created a class, we can then use it to instantiate an object based on that class. Let’s break that statement down a bit.

The word instantiate comes from the word instance, which means “an example or single occurrence of something.” So, we’re creating a single example of a class, which we call an object.

Most high level programming languages create an object by calling a special function generically called a constructor, which usually has the same name as the class from which you are trying to create an instance. For example, we can define our Ingredient class:

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

  public Ingredient (String aName, double anAmount, String aUnit){
    this.name = aName;
    this.amount = anAmount;
    this.unit = aUnit;
  }
}

Then, elsewhere in our code, we can instantiate Ingredient objects by calling the constructor - effectively, we just call the data type itself as if it were a function along with the new keyword, which will return a new instance of that object!

Ingredient flour = new Ingredient("Flour", "1.0", "cups");
Ingredient sugar = new Ingredient("Sugar", "2.0", "cups");

Here flour and sugar are both variables of type Ingredient, but they represent different things and would each contain their own name, amount and unit.

Some Vocabulary

Object-oriented programming introduces a large number of new terms, each with very specific uses. Here is a quick overview of some of the new terms we’ve learned so far:

  • Object-Oriented Programming: A programming paradigm that uses objects as the basis of the program’s structure
  • Class: A specification of a new data type in a program, including fields and methods, that are used to describe a single part of a program
  • Object: A unique instance of a class, representing a single concrete item in the program
  • Instantiation: The process of creating an object based on a class
  • Attribute: A single variable defined in a class or stored in an object; also called a field
  • Method: An action that can be performed by a class or object; typically to access or change an Attribute; also called an action
Class Descriptions

Classes are a versatile programming constructs. Their combination of data and methods make them great containers for related information and procedures. Some generic groupings would be:

  • Object class – an instantiable class that contains data describing the object and methods to access and manipulate that data
  • Driver class – normally not instantiable, generally only contains a main() method
  • Utility class – not instantiable, generally contains methods and related constants (like the math library)

You may hear the terms abstract and concrete. Abstract classes are incomplete, in effect they are blueprints for blueprints. Concrete classes are complete. We will introduce abstract classes, and why you might use them, in a few modules.

Driver Classes

Instance class are not normally executable. That is they may contain lots of fields (attributes) and methods, but generally have no main() method. The technique we will use for the next few modules will be to have a companion driver class.

Driver and Ingredient UML Driver and Ingredient UML

In the UML class diagram above you will see we added a separate class Driver, which has one method and no attributes.

The first thing you may notice is that the method is underlined. In UML, underlined class element are class-level attributes or objects. Each language may implement these differently, but in general to access a class element all one needs is the class name, so Driver.main() will generally call the class-method main() in the class Driver. Class-features can only be called by using the class name.

Next you may observe the dotted line arrow. This indicates that the class Driver depends on the class Ingredient. For our purposes this means:

  • Ingredient must work correctly for Driver to work correctly
  • Driver must include, use, import or somehow have access to Ingredient
    • We will cover how to do this for you specific language later in this module
  • We anticipate that at least one method in Driver will make or access an Ingredient object.
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.

Summary

In this chapter, we learned about creating objects in our programs. We also learned about the two class “Instance” and “Driver” model we will use for the rest of this course.