4.1. Variables, Primitive Types, Parameters, and Logic¶
In the previous chapters, you created custom subclasses, controlled execution
with conditional statements and loops, and designed automated unit test cases.
In this chapter, you will learn how to work with Java’s fundamental primitive data types,
evaluate arithmetic expressions, write methods that take parameters and return values,
formulate compound boolean expressions with logical operators (&&, ||, !),
and declare custom constructors in subclasses using super(...).
4.1.1. Java’s Primitive Types: Integers and Doubles¶
One of the most powerful features of any programming language is the ability to define and manipulate variables. A variable is a named storage location in computer memory that holds a value. Values may be numbers, characters, boolean truth values, or references to complex objects. To store and manipulate a value, you must first declare a variable.
int x = 7;
Every variable has a data type that determines what kinds of values it can store,
how much memory it occupies, and what operations can be performed on it. The statement
above is a variable declaration and initialization, because it declares that the variable
named x has type int (integer) and gives it an initial value of 7.
4.1.1.1. Variable Naming Conventions and Keywords¶
While x is an acceptable variable name for short mathematical formulas or loop indices,
you should generally choose descriptive names that indicate what the variable represents–either
the data it holds or the role it plays in your program:
int ageInYears = 21;
int flowersCollected = 14;
int studentId = 123456;
When a variable name consists of multiple words, Java style conventions dictate using
lowerCamelCase (lowercase first letter, with the first letter of each subsequent word capitalized).
Variable names always begin with a lowercase letter, whereas class names (like Jeroo or FlowerSweeper)
begin with an uppercase letter.
Variable names in Java are case-sensitive: ageInYears, ageinyears, and AgeInYears are
three entirely distinct identifiers. In addition, you cannot use any of Java’s approximately 50
reserved keywords (such as class, public, void, int, double, boolean, or return)
as identifier names, because the compiler reserves them to parse the structure of your code.
4.1.1.2. Local Variable Scope¶
A variable declared inside the body of a method is called a local variable.
The region of program text where a variable’s name is recognized and accessible is called its scope.
For local variables, scope extends from the point of declaration down to the closing brace (}) of the
block of statements where it was declared. A local variable declared inside one method cannot be seen or
accessed by any other method.
4.1.1.3. Variable Assignment and the ‘=’ Operator¶
After declaring a variable, you can update its value at any point using an assignment statement:
int flowersCollected = 0; // declared and initialized to 0
flowersCollected = 5; // assigned a new value of 5
flowersCollected = 8; // updated to 8
An assignment statement evaluates the expression on the right-hand side of the = symbol
and stores the resulting value into the storage location named on the left-hand side. Because
the value can vary throughout program execution, the named location is called a variable.
Warning
In mathematics, the = symbol indicates equality (a static statement of truth, such as \(a = b\)).
In Java, = is an action–an assignment operator that moves data from right to left.
Writing 5 = x; is illegal in Java because a literal number (5) is not a storage location.
Furthermore, equality in mathematics is symmetric and permanent. In Java, an assignment statement can make two variables hold the same value temporarily, but changing one later does not affect the other:
int a = 5;
int b = a; // a and b now both hold 5
a = 3; // a changes to 3, but b remains 5!
4.1.1.4. Floating-Point Numbers: The double Type¶
Java’s int type stores 32-bit signed whole numbers (ranging from approximately \(-2.14\) billion
to \(+2.14\) billion). However, many computations require real numbers with fractional components,
such as measurements, percentages, or geometric coordinates.
For real numbers, Java provides the double primitive type (a 64-bit double-precision floating-point number):
double accountBalance = 125.75;
double pi = 3.1415926535;
double distanceInMiles = 4.2;
You can assign an integer value directly to a double variable, and Java will automatically store
it as a floating-point number (e.g., double weight = 50; stores 50.0). However, you cannot assign
a double value directly to an int variable without an explicit cast, because discarding the
fractional part could lead to unintended data loss.
4.1.2. Characters, Booleans, and Literals¶
In addition to numeric types for integers and real numbers, Java provides primitive types for individual characters and boolean truth values.
4.1.2.1. The char Type and Character Literals¶
The char primitive type represents a single 16-bit character from the Unicode character set, which includes standard English letters, digits, punctuation, and symbols from world languages.
In Java, character literals are enclosed in single quotation marks (' '). This distinguishes
them from multi-character String literals, which are enclosed in double quotation marks (" "):
char letterGrade = 'A';
char currencySymbol = '$';
char digitChar = '7';
Notice that the character literal '7' is fundamentally different from the integer literal 7.
The integer 7 is a numeric value suitable for arithmetic computations, while the character '7'
is a text glyph represented internally by an encoding number (ASCII/Unicode code 55).
Special non-printable characters are written using an escape sequence preceded by a backslash (\):
'\n': Newline character (advances cursor to the next line)'\t': Tab character (horizontal spacing)'\'': Single quote character literal'\\': Backslash character literal
You can compare char values directly using relational equality operators:
char userChoice = 'y';
if (userChoice == 'y')
{
this.hop();
}
4.1.2.2. Booleans as Variables and Condition Storage¶
You have already encountered boolean conditions in if statements and while loops.
Java formalizes this concept with the boolean primitive type, which can hold only one of
two literal values: true or false.
Rather than calling a sensor query method repeatedly or building complicated nested logic,
you can capture the result of any boolean expression in a boolean variable:
boolean pathIsBlocked = this.isWater(AHEAD) || this.seesNet(AHEAD);
boolean hasAmmunition = this.hasFlower();
Once stored, a boolean variable can be used wherever a condition is expected:
if (pathIsBlocked)
{
this.turn(RIGHT);
}
if (hasAmmunition)
{
this.toss();
}
Tip
Avoid writing if (pathIsBlocked == true). Since pathIsBlocked already evaluates to
true or false, writing == true is completely redundant. Simply write if (pathIsBlocked).
Similarly, write if (!pathIsBlocked) instead of if (pathIsBlocked == false).
4.1.3. Arithmetic Operators & Integer Division Truncation¶
Java provides standard arithmetic operators to perform mathematical computations on numeric types:
+: Addition-: Subtraction*: Multiplication/: Division%: Remainder (Modulo)
4.1.3.1. Arithmetic Expressions and Precedence¶
An expression is a combination of variables, literals, operators, and method calls that evaluates to a single value:
int hours = 2;
int minutes = 35;
int totalMinutes = hours * 60 + minutes; // computes 155
Java follows standard mathematical operator precedence:
Multiplication (
*), division (/), and remainder (%) have higher precedence than addition (+) and subtraction (-).Operators with equal precedence evaluate from left to right.
You can use parentheses
( )to explicitly control the order of operations:
int result1 = 5 + 3 * 2; // result1 is 11 (multiplication first)
int result2 = (5 + 3) * 2; // result2 is 16 (parentheses first)
The remainder operator (%), also known as the modulo operator, computes the remainder left over after integer division:
int rem1 = 14 % 4; // rem1 is 2 (since 14 = 3 * 4 + 2)
int rem2 = 8 % 2; // rem2 is 0 (8 is evenly divisible by 2)
The remainder operator is commonly used to test whether a number is even (number % 2 == 0)
or to wrap values within a fixed range (such as compass headings or grid coordinates).
4.1.3.2. Integer Division and Truncation Pitfalls¶
When both operands of a division operator are integers (int), Java performs integer division.
Integer division discards any fractional portion and truncates toward zero:
int a = 7 / 2; // a is 3 (not 3.5!)
int b = 1 / 3; // b is 0 (not 0.333333!)
A frequent pitfall occurs when students attempt to store the result of an integer division into a double:
double fraction = 1 / 3; // BUG: fraction is 0.0, NOT 0.333333
Because both 1 and 3 are integer literals, Java evaluates 1 / 3 using integer division first,
yielding the integer 0. Only after the division is complete does Java widen 0 to the double
value 0.0.
4.1.3.3. Type Conversions: Implicit Widening and Explicit Casting¶
Java handles arithmetic expressions involving mixed types using widening conversions:
if either operand in an arithmetic operation is a double, Java automatically converts the
other operand to a double before performing the calculation:
double half = 1.0 / 2; // 1.0 is double, so 2 becomes 2.0; half is 0.5
double quarter = 1 / 4.0; // quarter is 0.25
What if you have two int variables and need their fractional quotient? You cannot simply append .0
to a variable name. Instead, you use an explicit cast:
int totalScore = 85;
int totalQuizzes = 10;
// Truncates to 8.0:
double wrongAverage = totalScore / totalQuizzes;
// Explicit cast converts totalScore to double before division:
double correctAverage = (double) totalScore / totalQuizzes; // yields 8.5
The cast operator consists of the desired target type inside parentheses: (double). It has higher
precedence than arithmetic operators, so (double) totalScore / totalQuizzes first converts totalScore
to 85.0, which then forces floating-point division by 10, producing 8.5.
Conversely, casting a double to an int truncates the decimal portion:
double tax = 8.875;
int wholeDollars = (int) tax; // wholeDollars is 8 (fractional part is discarded)
4.1.3.4. Rounding vs. Truncation¶
It is critical to distinguish between truncation and rounding:
Truncation simply chops off the fractional part and throws it away, moving toward zero. Both
(int) 8.1and(int) 8.99produce8. Java does not automatically round to the nearest whole number when casting.Rounding finds the closest whole number (so
8.1rounds to8, but8.9rounds to9).
If your calculation requires rounding rather than truncation, Java provides two common approaches:
1. The “+ 0.5” Technique (for positive numbers)
You can add 0.5 to a positive floating-point number before casting to int:
double score1 = 8.2;
int rounded1 = (int) (score1 + 0.5); // 8.2 + 0.5 = 8.7 -> truncates to 8
double score2 = 8.7;
int rounded2 = (int) (score2 + 0.5); // 8.7 + 0.5 = 9.2 -> truncates to 9
Because adding 0.5 pushes any fractional value of 0.5 or greater over to the next whole integer,
the subsequent truncation produces properly rounded results.
2. Using Math.round()
Java’s standard Math utility class includes a built-in Math.round() method:
double price = 19.85;
long nearestDollar = Math.round(price); // returns 20
Note that Math.round(double) produces a 64-bit integer of type long. If you need the result as
an int, you can cast the returned value:
int roundedPrice = (int) Math.round(price); // 20
4.1.4. Parameters and Pass-by-Value Semantics¶
So far, most methods you have declared were parameterless actions such as hop() or turnAround().
However, methods become far more powerful and reusable when they can accept input data to customize
their behavior.
4.1.4.1. Method Parameters: Formal Parameters vs. Actual Arguments¶
To allow a method to receive input, you define one or more formal parameters inside the parentheses of the method header. A parameter declaration specifies both the type and the name of the incoming data:
public void hopMultiple(int distance)
{
int count = 0;
while (count < distance)
{
this.hop();
count = count + 1;
}
}
In this method header, int distance is a formal parameter. It acts as a local variable initialized
automatically with whatever value the caller supplies.
When a caller invokes the method, the value provided in the call is called the actual argument:
jeroo.hopMultiple(4); // 4 is the actual argument
The compiler ensures that the type of each actual argument matches (or can be widened to) the type of the corresponding formal parameter.
4.1.4.2. Methods with Multiple Parameters¶
A method can accept multiple parameters separated by commas. The actual arguments must match the formal parameters in count, order, and type:
public void plantRectangle(int rows, int columns)
{
int r = 0;
while (r < rows)
{
int c = 0;
while (c < columns)
{
this.plant();
this.hop();
c = c + 1;
}
this.turnAround();
r = r + 1;
}
}
When invoking plantRectangle(3, 5), Java binds the value 3 to rows and 5 to columns.
4.1.4.3. Pass-by-Value Semantics in Java¶
One of the most important concepts in programming is understanding how parameters are passed to methods. Java is strictly pass-by-value.
When you pass an argument to a method, Java evaluates the argument expression and passes a copy of the value into the method’s formal parameter variable. The method operates exclusively on its own local copy:
public class ValueTester
{
public void tryToChange(int x)
{
x = x + 10; // modifies only the local parameter variable x
}
}
Consider what happens at the call site:
int original = 5;
ValueTester tester = new ValueTester();
tester.tryToChange(original);
// What is original now?
System.out.println(original); // Prints 5!
When tester.tryToChange(original) is executed, the value 5 stored in original is copied into the
parameter variable x. Inside tryToChange, x becomes 15. But original remains in the caller’s
stack frame completely untouched. A method cannot modify the value of a primitive variable passed to it by a caller.
4.1.5. Writing Non-Void Methods with Return Statements¶
Methods can be categorized by whether they produce a result:
void methods: Perform actions that change the state of the world or an object (e.g.
hop(),pick()), but return no answer.non-void methods: Compute and produce a value (an answer) that is returned to the caller.
4.1.5.1. The return Keyword and Non-Void Return Types¶
To write a method that returns a value, you replace the keyword void in the method header with the
return type of the value being produced:
public int convertHoursToMinutes(int hours)
{
int minutes = hours * 60;
return minutes;
}
The return statement terminates execution of the method immediately and passes the computed value
back to the caller. The type of the expression in the return statement must match the return type declared
in the method header.
4.1.5.2. Returning Calculated Values and Conditional Returns¶
Methods can compute values using conditional statements to return different answers based on inputs:
public int max(int a, int b)
{
if (a >= b)
{
return a;
}
else
{
return b;
}
}
Important
Every Path Must Return: When compiling a non-void method, Java checks every possible execution
path through your conditionals. If there is any path where a return statement is not reached,
the compiler will reject your code with the error: missing return statement.
You can also use a return statement to exit early once an answer is determined, or structure your code with a single return at the end using a local accumulator variable:
public double calculateTax(double subtotal, boolean isTaxExempt)
{
if (isTaxExempt)
{
return 0.0;
}
return subtotal * 0.053; // Virginia state sales tax rate
}
4.1.5.3. Using Method Results in Expressions¶
Because non-void methods return values, a call to a non-void method is an expression. You can:
Store the returned value in a variable:
int larger = this.max(12, 19);Use the returned value in arithmetic:
int total = this.convertHoursToMinutes(3) + 45;Pass the returned value as an argument to another method:
this.hopMultiple(this.max(2, 5));Use the returned boolean directly in a conditional:
if (this.hasFlower()) { ... }
4.1.6. Boolean Operators: AND, OR, NOT & Short-Circuiting¶
In previous chapters, you wrote simple conditions based on a single sensor query, such as this.isWater(AHEAD).
Real-world decision-making often requires checking multiple criteria simultaneously or comparing numeric values.
4.1.6.1. Relational Operators with Primitive Data Types¶
Java provides six relational operators to compare primitive values. Every relational expression evaluates
to a boolean result (true or false):
Operator |
Meaning |
Example |
Result (if x = 5) |
|---|---|---|---|
|
Equal to |
|
|
|
Not equal to |
|
|
|
Less than |
|
|
|
Less than or equal to |
|
|
|
Greater than |
|
|
|
Greater than or equal to |
|
|
4.1.6.2. Compound Boolean Operators: &&, ||, !¶
To combine multiple conditions into a single expression, Java provides three logical operators:
Operator |
Meaning |
Description |
|---|---|---|
|
Logical AND |
|
|
Logical OR |
|
|
Logical NOT |
Inverts the truth value (changes |
You can combine relational expressions and sensor queries using logical operators:
// Safe to step forward if clear AND facing the correct boundary:
if (this.isClear(AHEAD) && this.getHeading() == EAST)
{
this.hop();
}
// Handle an obstacle if blocked by water OR blocked by a net:
if (this.isWater(AHEAD) || this.seesNet(AHEAD))
{
this.turn(RIGHT);
}
// Act when NOT facing water:
if (!this.isWater(AHEAD))
{
this.hop();
}
4.1.6.3. Short-Circuit Evaluation Semantics¶
Java evaluates compound boolean expressions from left to right using short-circuit evaluation:
For && (AND): If the left operand evaluates to
false, the entire expression cannot possibly betrue. Java immediately stops evaluation and producesfalsewithout evaluating the right operand.For || (OR): If the left operand evaluates to
true, the entire expression is already guaranteed to betrue. Java immediately stops evaluation and producestruewithout evaluating the right operand.
Short-circuit evaluation is not just a performance optimization; it is a critical programming tool for writing guard conditions that prevent runtime errors:
// The second check (hasFlower) ensures toss() is only called when ammunition exists:
if (this.hasFlower() && this.seesNet(AHEAD))
{
this.toss();
}
4.1.6.4. Testing Compound Conditions: Condition Coverage and the N + 1 Rule¶
In Chapter 3, you learned that achieving condition coverage (also known as branch coverage)
requires writing automated unit test cases that evaluate every condition to both true and false.
For a simple condition (such as if (this.hasFlower())), two test cases are sufficient:
one where the condition evaluates to true, and one where it evaluates to false.
However, when you write compound conditions using && or ||, achieving full condition
coverage requires deeper analysis due to Java’s short-circuit evaluation.
4.1.6.4.1. The N + 1 Rule for Compound Conditions¶
Consider a method with a compound condition containing \(N\) simple boolean conditions joined by &&:
if (this.hasFlower() && this.seesNet(AHEAD))
{
this.toss();
}
Here, there are \(N = 2\) simple conditions:
Condition 1:
this.hasFlower()Condition 2:
this.seesNet(AHEAD)
You might initially assume you need \(2^N = 4\) test cases to test every possible combination
in a truth table. However, because Java uses short-circuit evaluation, it stops evaluating the moment
the first false operand is encountered in an && expression!
To achieve full condition coverage for a compound condition with \(N\) simple conditions, you need exactly N + 1 test cases:
Test Case |
Condition 1 ( |
Condition 2 ( |
Outcome & Evaluation Path |
|---|---|---|---|
Test 1 |
|
(not evaluated) |
Java short-circuits at Condition 1. Result: |
Test 2 |
|
|
Condition 1 evaluates to |
Test 3 |
|
|
Both conditions evaluate to |
For any compound && expression with \(N\) conditions:
You need 1 test case where every condition evaluates to
true(the overall expression istrue).You need N test cases where each condition \(k\) (from 1 to \(N\)) is the first condition to evaluate to
false.
The same \(N + 1\) rule applies to compound || expressions:
You need 1 test case where every condition evaluates to
false(the overall expression isfalse).You need N test cases where each condition \(k\) is the first condition to evaluate to
true(triggering short-circuit termination).
4.1.6.4.2. The “Lining Up Preceding Conditions” Trap¶
A very common mistake students make when writing unit tests for compound conditions is trying to test a specific sub-condition without setting up the environment so that execution actually reaches it.
For example, suppose you want to write a test case to verify how your code behaves when Condition 2
(seesNet(AHEAD)) is false. A student might write:
@Test
public void testSeesNetIsFalse()
{
// Create a Jeroo with NO flowers in its pouch, facing clear water
Jeroo jeroo = new Jeroo(0);
// ...
}
In this test setup, the student made sure there is no net ahead. But because the Jeroo was created with
0 flowers, hasFlower() evaluates to false!
Because Java evaluates hasFlower() && seesNet(AHEAD) from left to right, Java sees that
hasFlower() is false and short-circuits immediately. It never calls seesNet(AHEAD) at all!
As a result, this test case only tests Condition 1; it completely fails to test Condition 2.
Important
The Preceding Conditions Rule:
To test condition \(k\) in a compound && expression, all preceding conditions (1 through :math:`k - 1`) must evaluate to true.
If any earlier condition evaluates to false, Java will short-circuit before ever reaching condition \(k\).
Similarly, to test condition \(k\) in a compound || expression, all preceding conditions (1 through :math:`k - 1`) must evaluate to false.
If any earlier condition evaluates to true, Java will short-circuit before reaching condition \(k\).
Whenever you design unit tests for compound conditions in your programs and labs, always verify that your test fixture sets up the world state so that each target condition is reached and evaluated.
4.1.6.5. Clean Conditional Design: Avoiding Dead Code and Negation Traps¶
When writing complex conditional structures, following good design habits makes your code readable and defect-free:
Prefer Positive Conditions in if-else: Starting an
if-elsewith a negated condition (!) forces readers to perform mental gymnastics.// Hard to read: if (!this.isClear(AHEAD)) { this.turn(RIGHT); } else { this.hop(); } // Much easier to read: if (this.isClear(AHEAD)) { this.hop(); } else { this.turn(RIGHT); }
Beware of Unreachable (Dead) Code: If an earlier branch covers all cases, subsequent branches can never execute:
if (this.isClear(AHEAD)) { this.hop(); } else if (!this.isClear(AHEAD)) { this.turn(RIGHT); } else { // UNREACHABLE CODE! The area ahead is always either clear or not clear. this.toss(); }
Use Compound Conditions to Flatten Nested Logic: Instead of nesting multiple
ifstatements, join them with&&:// Nested: if (this.isClear(AHEAD)) { if (this.hasFlower()) { this.plant(); } } // Clean, flat compound condition: if (this.isClear(AHEAD) && this.hasFlower()) { this.plant(); }
4.1.7. Constructors in Subclasses & super(…)¶
When you create a subclass in Java, it inherits all accessible methods from its superclass.
For example, a FlowerSweeper or PlantingJeroo subclass inherits hop(), turn(),
pick(), and isWater() from Jeroo.
However, subclasses do not inherit constructors.
4.1.7.1. Why Subclasses Do Not Inherit Constructors¶
Constructors are special methods designed specifically to initialize the state of a newly created object. Because a subclass often adds new fields, new responsibilities, or specialized behavior that the superclass cannot anticipate, every subclass must define its own constructors.
If you do not define any constructor in a subclass, the Java compiler automatically attempts to insert a
default, parameterless constructor that calls super() (the superclass no-argument constructor).
If the superclass does not have a no-argument constructor (or if you need to pass parameters to configure
the superclass), your code will fail to compile unless you provide an explicit constructor.
4.1.7.2. Declaring Subclass Constructors & Invoking super(…)¶
A constructor declaration has two defining characteristics:
It has no return type (not even
void).Its name is identical to the name of the class.
Inside a subclass constructor, you can invoke a constructor of its superclass using the super(…) statement.
When used, super(...) must be the very first statement in the constructor body:
public class PlantingJeroo extends Jeroo
{
/**
* Create a new PlantingJeroo with a specified pouch of flowers.
* @param flowers The number of flowers to start with.
*/
public PlantingJeroo(int flowers)
{
super(flowers); // Invokes the Jeroo(int flowers) constructor
}
}
In this constructor, PlantingJeroo accepts an integer parameter flowers and delegates its initialization
to the Jeroo superclass via super(flowers). This ensures that the superclass initializes all of its
internal micro-world attributes (such as position, heading, and flower pouch) properly before any subclass
code executes.
You can also provide constructors that pass multiple arguments to the superclass:
public class PlantingJeroo extends Jeroo
{
public PlantingJeroo(int flowers)
{
super(flowers);
}
/**
* Create a PlantingJeroo at a specific coordinate facing east.
* @param x Initial x-coordinate.
* @param y Initial y-coordinate.
* @param flowers Initial flower count.
*/
public PlantingJeroo(int x, int y, int flowers)
{
super(x, y, EAST, flowers); // Invokes Jeroo(int, int, CompassDirection, int)
}
}
By defining custom subclass constructors with parameters and calling super(...), you gain complete
control over how your micro-world actors are initialized in your programs and test fixtures.
4.1.8. Building Your Skills Through Practice¶
Learning to program is a skill built through deliberate practice and experimentation. The following optional, ungraded resources are designed to help you reinforce key concepts, build fluency, and prepare for upcoming quizzes, labs, and programming assignments.
Java Syntax Practice 4 : Build confidence and muscle memory with rapid, bite-sized drills focused on syntax rules and basic language mechanics. Especially recommended if you are new to programming.
