Problem Set 9 - Classes and Objects

Assigned: March 11, 2026

Due: March 25, 2026 at 11:59 PM EST

Objectives

Instructions

Because we're introducing classes with this problem set, it is no longer appropriate to use the class name ProblemX. Instead, design classes with the given specification in each problem, along with the appropriate test suite. Each method in a class (including accessors!) must include proper Javadoc comments describing the (1) purpose of the method, (2) parameters, and (3) return value.

For problems containing multiple parts, write the methods inside the same class.

Do not round your solutions. For example, do not use Math.round or float your output to, e.g., 2 decimal places, unless specified.

What You Cannot Use

You may not use anything beyond Chapter 4.1. This includes but is not limited to:

Any violation results in a score of 0 on the problem set.

Anything that trivializes a problem is disallowed. This problem set is designed to get you acclimated with immutable classes and data representation. Therefore, mutation and aliasing are off the table. As such, all instance variables should be declared as final.

Please contact a staff member if you are unsure as to whether you're allowed to use something.

Notes and Version Changes:

You do not need to write tests for any hashCode implementations. We will check these manually.

Important Warning 1: You must write the method headers for all methods that we list below in order for your code to compile in the autograder. We will not award partial points for non-compiling code. So, we strongly recommend that, before you implement the methods, you first write the method headers and Javadocs.

Important Warning 2: When writing the equals method, IntelliJ will likely suggest that you use Objects.equals or ask if you want to override the method in a way that uses reflection through the getClass() method. Both of these are disallowed by the rules of the lab and problem set, and if you use them, you will earn an automatic zero. Write the method in the way that we teach you! The golden rule is that, if you don't understand an IntelliJ suggestion or a suggestion made by Google/online, don't use it!

Update 03/12: Removed "constant time" requirement for size().

Problems

  1. (15 points) Design the RgbColor class to represent a color that has three channels: red, green, and blue. This type of class is generally associated with pixels on a display, which use three integers (also sometimes with an opacity for transparency), values ranging between 0 and 255, to represent the three values of color. Combining these integers creates a color. For example, a color whose red channel is 255, whose green channel is 0, and whose blue channel is 255 creates magenta. The constructor should receive these three integer values and assign them to instance variables. Design the relevant accessor methods. Call them getRed(), getGreen(), and getBlue(). Additionally, design the following methods:

    1. RgbColor invert(), which returns a new RgbColor instance that represents the inversion of this.

    2. RgbColor grayscale(), returns a new RgbColor instance that represents the grayscale color of this. To compute the grayscale color, take the average of the three channels (casting to int) and set all three channels to that average.

    3. boolean isGrayscale(), which returns whether this color is grayscale, i.e., whether all three of the channels are the same.

    Files to Submit: RgbColor.java, RgbColorTest.java

  2. (15 points) In this exercise you will design the Duration class for working with units of a duration. Programming languages often support operations for handling dates, times, and durations to varying degrees of success. Java provides a few classes and methods of its own, and you cannot use these in your implementation, as that would defeat the point of the exercise.

    1. Design the Duration class. Its constructor should receive the following parameters:

      • Duration(int h, int m, int s) receives three integers \(h \geq 0\), \(0 \leq m < 60\), and \(0 \leq s < 60\) representing durations in hours, minutes, and seconds respectively.
      • Duration(int s) receives a single integer \(s \geq 0\) representing the number of seconds.
      • Duration(String t) receives a string of the form "hh:mm:ss" with three components: hours, minutes, and seconds. You can assume that the string is always in the correct format, and that the hours, minutes, and seconds are valid according to the same rules as the previous two constructors.

      Hint: It may be tempting to store three separate instance variables for hours, minutes, and seconds, but consider the implications of this approach! You may have a harder time implementing the methods that need to work with the duration as a whole.

    2. Design the int getNumberOfSeconds() method, which returns the number of seconds that this Duration object represents.

    3. Design the int getNumberOfMinutes() method, which returns the number of minutes that this Duration object represents. If there are an inexact number of minutes, return only the number of full minutes. As an example, new Duration("02:30:45").getNumberOfMinutes() returns 150 and not 151.

    4. Design the int getNumberOfHours() method, which returns the number of hours that this Duration object represents. Return inexact hours in the same way as getNumberOfMinutes.

    5. Override the toString method to return a stringified version of the duration where the hours, minutes, and seconds are separated by colons. Single-digit units of time must contain a leading zero.

    6. Override the equals method to return whether a given Duration object represents the same duration as this instance.

    7. Design the Duration addDuration(Duration d) that adds the given Duration object \(d\) to this Duration object. Do not mutate this or the passed Duration object; instead, return a new Duration object that represents the result of the addition.

    8. Design the Optional<Duration> subtractDuration(Duration d) that subtracts the given Duration object \(d\) from this Duration object. Like addDuration, you should not mutate this or the passed Duration object; instead, return a new Duration object, wrapped in an Optional, that represents the result of the subtraction. If the result of a subtraction is a negative duration amount, return an empty Optional.

    Files to Submit: Duration.java, DurationTest.java

  3. (25 points) In this exercise, you will design a class that represents a two-dimensional vector. Vectors have a direction and a magnitude, and are represented by two components: \(x\) and \(y\).

    1. Design the Vector class, which stores two double values representing the \(x\) and \(y\) components of the vector. The constructor should receive these two values and assign them to the instance variables.

    2. Design the double getX() and double getY() methods that return the respective components of the vector.

    3. Design the double getMagnitude() method that returns the magnitude of the vector, which is defined as \(\sqrt{x^2 + y^2}\).

    4. Design the Vector add(Vector v) method that adds a given vector \(v\) to this vector. The result is a new Vector instance whose components are the sum of the respective components of both vectors.

    5. Design the Vector subtract(Vector v) method that subtracts a given vector \(v\) from this vector. The result is a new vector whose components are the difference of the respective components of both vectors.

    6. Design the Vector scale(double s) method that scales this vector by a given scalar \(s\). The result is a new vector whose components are multiplied by \(s\).

    7. Design the Vector normalize() method that normalizes this vector to the unit vector, i.e., a vector with a magnitude of \(1\). The result is a new vector whose components are divided by the magnitude of this vector.

    8. Design the double dot(Vector v) method that computes the dot product of this vector with a given vector \(v\). The dot product is defined as \(x_1 \cdot x_2 + y_1 \cdot y_2\), where \((x_1, y_1)\) and \((x_2, y_2)\) are the components of the two vectors.

    9. Finally, override the equals, hashCode, and toString methods. Two vectors are equal if their components are equal. The hash code of a vector is the sum of the hash codes of its components. The toString representation of a vector is the stringified version of the vector, e.g., "(x, y)".

    Files to Submit: Vector.java, VectorTest.java

  4. (30 points) The bag, or multi-set, data structure is a simple set-like data structure, with the exception that elements can be inserted multiple times into a bag. Order does not matter with a bag, nor does its internal structure. With bags, we can add items, remove items, query how many of an item there are, and return the number of items in the bag.

    1. Design the generic Bag<T> class. It would be inefficient to simply store a list of all the items in the bag. So, your bag will employ a Map<T, Integer>, which associates an item \(T\) with its count in the map. Instantiate the map as an instance variable as a HashMap.

    2. Design the void insert(T t) method, which inserts an item \(t\) into the bag. If the item already exists, its associated count is incremented by one.

    3. Design the boolean remove(T t) method, which removes an item \(t\) from the bag, meaning its associated count is decremented by one. If \(t\) does not exist, the method returns false, and otherwise returns true. If decrementing the count results in \(0\), then remove the key \(t\).

    4. Design the int count(T t) method that returns the respective quantity of item \(t\) in the bag.

    5. Design the int size() method that returns the number of items in the bag.

    6. Design the boolean contains(T t) method that returns whether the bag contains t.

    7. Design the boolean isSubBagOf(Bag b) method that determines this is a sub-bag of b. A bag \(b_1\) is a sub-bag of \(b_2\) if, for every element \(i \in b_1\), then \(i \in b_2\) and count\((b_1[i]) \leq \)count\((b_2[i])\).

    Files to Submit: Bag.java, BagTest.java

  5. (15 points) Java's type system (i.e., its ability to tell us information about the type of a variable) is, unfortunately, not expressive enough to do specific things. For example, one thing we cannot do is write a method whose parameter is an array of some specific length. Instead, we have to state that the parameter is an Object, then perform a runtime check on the type via instanceof. In this exercise you will reimplement the behavior of Arrays.deepToString: a method to convert an array of any dimension into a string representation.

    The algorithm is recursive and straightforward: if the parameter \(A\) is null, return "null". Otherwise, we know that \(A\) is an array of some dimension. Traverse over the elements of \(A\), and if \(A[i]\) is an instance of Object[], then recursively call deepToString on it. Otherwise, just call toString on it.

    Create a class called DeepToString, and then design the static String deepToString(Object[] A) method, which implements this algorithm. The output string should follow the same format as Arrays.deepToString, which encloses its elements in brackets, separated by commas and a space.

    For example, we can invoke DeepToString.deepToString(new Integer[][]{{1, 2}, {3, 4}}), which returns the string "[[1, 2], [3, 4]]".

    Warning: You cannot use Arrays.deepToString or any similar method that trivializes the problem.

    Hint: The problem states this, but the instanceof, which checks whether an object is an instance of a particular type, is your friend here.

    Note 1: This method does not support arrays of primitive types, e.g., int[], double[], and so forth. If you want to test an array of primitive types, you will have to box the elements into their wrapper classes, e.g., Integer[], Double[], etc.

    Note 2: As the problem states, your method should be able to process arrays of any dimension, hence the recursive decomposition.

    Files to Submit: DeepToString.java, DeepToStringTest.java