Generics & Inner Classes in Java. Practical Tasks
Add the static modifier to the inner class Query and the line that prints the user’s login stops compiling: non-static variable login cannot be referenced from a static context. Differences like this between the kinds of nested classes, and between bounded type parameters, stick faster when you solve problems than when you only read the theory.
This page has 10 Java generics and inner classes exercises with hints. The first five practice nested classes: a member inner class, a local class, a static nested class and two anonymous classes. The other five practice generics: a generic class with bounded type parameters, a generic matrix, a generic class for the minimum and maximum of an array, a calculator with generic static methods, and wildcards. Five tasks have a video walkthrough (narrated in Russian), and the tasks on the matrix, minimum and maximum, calculator and wildcards include the expected output. The theory is in the lessons Inner Classes in Java: Types and Examples and Java Generics: Type Parameters, Wildcards, Erasure.
Member Inner Class: User and Query
- Create a class
Userwithprivatefieldsloginandpasswordand a constructor that initializes them. - Create a member inner class
QueryinsideUser. The methodprintToLog()ofQueryprints a message saying that the user with this login and password sent a request. - The class
Userhas a methodcreateQuery()that creates aQueryobject and callsprintToLog(). - In
main(): create aUserobject and callcreateQuery(); create aQueryobject withuser.new Query()and callprintToLog(); create anotherQueryobject withnew User(...).new Query()and callprintToLog().
Example output for new User("anna", "qwerty"):
User anna with password qwerty sent a requestHint
printToLog() needs no getters: an inner class sees the private fields of its outer class directly. A call to new Query() in main() without a User object does not compile: main() is static, so there is no implicit reference to an outer object.
Local Class
Rewrite the task Member Inner Class so that Query becomes a local class.
- Declare the class
Queryin the body of the methodcreateQuery()ofUser; create its object there and callprintToLog(). - Add a parameter
String requestTexttocreateQuery()and print it inprintToLog()together with the login. - Try to assign a new value to
requestTextinsidecreateQuery(). Explain why the code no longer compiles. - Which lines of
main()from the original task stop compiling now, and why?
What to look for
A local class is visible only inside its own method, so you cannot create it from main() with user.new Query(). It sees the parameters and local variables of the method only if they are final or effectively final; otherwise the compiler reports local variables referenced from an inner class must be final or effectively final.
Static Nested Class
Rewrite the class Query from the task Member Inner Class as a static nested class.
- Declare
QueryinsideUserwith thestaticmodifier. The methodprintToLog()still prints the login and the password of the user. - In
main(), create aQueryobject withoutuser.new Query()— withnew User.Query(...)— and callprintToLog(). - The method
createQuery()ofUsermust work as before.
Hint
A static nested class is not tied to a User object, so accessing login directly gives the error non-static variable login cannot be referenced from a static context. Pass a reference to the user into Query through its constructor: the private fields are accessible through that reference.
Anonymous Class: Printable
- Create an interface
Printablewith a methodvoid print()(or reuse one from your earlier solutions on interfaces). - Create an anonymous class that implements
Printable. Call itsprint()method. - Replace the anonymous class with a lambda expression. Explain why this is possible for
Printablebut not for an interface with two abstract methods.
A detail
After you compile the version with the anonymous class, a file like Main$1.class appears next to your class (named after the outer class): the compiler numbers anonymous classes. A lambda does not get a separate .class file.
Anonymous Class: Cookable
- Create an interface
Cookablewith a methodvoid cook(String str). - Create a class
Foodwith the methodpublic void prepare(Cookable c, String str) { c.cook(str); } - Create a
Foodobject and call its methodprepare(). - Pass an instance of an anonymous class that implements
Cookabletoprepare(). - Call
prepare()once more with a different anonymous class that prints the string in capital letters. Create it right in the argument of the call, without an intermediate variable.
Hint
An anonymous class can be declared right in the argument list: food.prepare(new Cookable() { ... }, "potato"); — the same way as the comparator in the section Anonymous class as a method argument.
Generic Class with Three Type Parameters
- Create a generic class with three type parameters (
T,V,K). - The class has three fields of types
T,V,K, a constructor that takes values of these types, and methods that return the three fields. Add a method that prints the class names of the three fields. - Bound the type parameters:
Tmust implementComparable(wrapper classes,String),Vmust implementSerializableand extend the classAnimal,Kmust extend the classNumber. - Create the class
Animaland its subclassCatthat implementsSerializable. Inmain(), create an object of your class with the type argumentsString,Cat,Integerand print the class names of the fields. - Check that a declaration with
Stringin place ofKdoes not compile.
Hint
When a bound includes both a class and an interface, the class goes first and the interfaces are joined with & (see Bounded type parameters). Give Comparable a type argument, for example Comparable<T>, or it stays a raw type.
Generic Matrix
Write a generic class Matrix<T extends Number> for a rectangular matrix of numbers. If you have already written a matrix class for int, rewrite it.
- The elements are stored in a two-dimensional array
T[][]passed to the constructor. If the rows of the array have different lengths, the constructor throwsIllegalArgumentException. - Methods:
T get(int row, int col),int rows(),int cols()andvoid print(), which prints each row of the matrix with elements separated by a space. - The method
Matrix<Double> add(Matrix<? extends Number> other)adds two matrices of the same size; the element type of the second matrix may differ. For different sizes it throwsIllegalArgumentException. - The method
Matrix<Double> multiply(Matrix<? extends Number> other)multiplies two matrices. If the number of columns of the first matrix is not equal to the number of rows of the second, it throwsIllegalArgumentException. - Test the class on these matrices:
aisInteger[[1, 2], [3, 4]],bisInteger[[5, 6], [7, 8]],cisDouble[[0.5, 0.5], [0.5, 0.5]].
The output of a.add(c).print() and a.multiply(b).print():
1.5 2.5
3.5 4.5
19.0 22.0
43.0 50.0A tricky point
Inside Matrix you cannot write new T[rows][cols] — the compiler answers generic array creation. That is why it is convenient to return the sum and the product as Matrix<Double>: you can create a Double[][] array, and the value of an element of any type is available through doubleValue().
Minimum and Maximum of an Array
Write a generic class MinMax with methods that find the minimum and the maximum element of an array. The array is a field of the class and is passed to it through the constructor.
- Bound the type parameter so that the class works with any comparable elements:
Integer,Double,String. - The methods
T min()andT max()return an element of the array. Compare elements withcompareTo(), not by converting them todouble. - For an empty array the methods throw
NoSuchElementExceptionwith a clear message, and anullarray causesNullPointerExceptionin the constructor (Objects.requireNonNull()). - Test the class on the arrays
Integer[] {3, -7, 12, 0}andString[] {"pear", "apple", "plum"}.
Expected output:
min = -7, max = 12
min = apple, max = plumWhy not Number
The bound T extends Number does not help here: Number has no compareTo() method, and the operator > does not work with objects (bad operand types for binary operator). You need a bound through the Comparable interface with a type argument — see Bounded type parameters.
Calculator with Generic Static Methods
Write a non-generic class Calculator with generic static methods sum(), multiply(), divide() and subtract(). The parameters of each method are two numbers, possibly of different types (for example, Integer and Double), and the method performs the operation on them. The result is a double.
- Give each method its own type parameters bounded by
Number. - Test the methods with the calls from the example below.
- Check that the call
Calculator.sum("1", 2)does not compile. - In a comment to the class, explain how your signature differs from
double sum(Number a, Number b)and when a type parameter in a method signature is really needed.
System.out.println(Calculator.sum(5, 2.5)); // 7.5
System.out.println(Calculator.multiply(3, 4L)); // 12.0
System.out.println(Calculator.subtract(10, 0.25f)); // 9.75
System.out.println(Calculator.divide(7, 2)); // 3.5
System.out.println(Calculator.divide(1, 0)); // InfinityA detail
The division is done on double values, so division by zero does not throw ArithmeticException: it returns Infinity (or NaN for 0 / 0). If this is an error in your program, check the divisor explicitly.
Wildcard: Copy a List and Find the Maximum
Write a utility class ListUtils with two generic static methods and find out which calls the compiler accepts.
static <T> void copy(List<? super T> dest, List<? extends T> src)appends all elements ofsrcto the end ofdest.static <T extends Comparable<? super T>> T max(List<? extends T> list)returns the largest element; for an empty list it throwsNoSuchElementException.- Copy a
List<Integer>[1, 2, 3] into an emptyList<Number>and into aList<Object>that already contains the string"x". Print both lists. Then find themaxofArrays.asList(3, 9, 4)andArrays.asList("pear", "apple", "plum"). - Try to copy a
List<Number>into aList<Integer>. Explain why the compiler rejects this call and which error it protects you from. - Write a method
static void addZero(List<? extends Number> list)that callslist.add(0). Explain the compilation error and fix the signature so that the method acceptsList<Integer>,List<Number>andList<Object>.
Expected output of step 3:
[1, 2, 3]
[x, 1, 2, 3]
9
plumHow to remember
A parameter you only read elements from (a producer) is declared with ? extends, and a parameter you only write elements to (a consumer) is declared with ? super. All three forms of the wildcard are summarized in the section Wildcard arguments.
What Each Exercise Practices
Every task checks one rule from the module lessons: how an object of a nested class is created and what it can access, or how a bound on a type parameter restricts the arguments and methods. If a task does not work out, open the lesson section from the last column — the rule is explained there with an example.
| Exercise | What it checks | What to review |
|---|---|---|
| Member Inner Class | access to private fields of the outer class, outer.new Inner() | Member (Regular) Inner Class |
| Local Class | scope, effectively final variables | Local Class, Effectively final variables |
| Static Nested Class | creation without an outer object, access to fields through a reference | Static Nested Class |
| Anonymous Class: Printable | implementing an interface in place, replacing it with a lambda | Anonymous class that implements an interface, Anonymous class vs lambda |
| Anonymous Class: Cookable | an anonymous class passed as a method argument | Anonymous class as a method argument |
| Generic Class with Three Type Parameters | several type parameters, class and interface bounds | Bounded type parameters |
| Generic Matrix | T[][] in a generic class, no new T[], Matrix<? extends Number> | Generic classes, Restrictions on generics |
| Minimum and Maximum of an Array | the Comparable bound, compareTo(), an empty array | Bounded type parameters |
| Calculator with Generic Static Methods | generic static methods in a non-generic class | Generic methods and constructors |
| Wildcard: Copy a List and Find the Maximum | ? extends and ? super, which calls do not compile | Wildcard arguments |
Where Solutions Usually Go Wrong
A solution often compiles and even works on one example, yet breaks the rule the task is about: it uses a raw type, compares numbers through double, or never checks edge cases. Go through this list before you call a task done.
- new Query() in a static main(). An object of an inner class needs an object of the outer class:
user.new Query(). If the class does not need the outer object, make it a static nested class — see Where Developers Get Tripped Up in the lesson. - Raw types.
Comparablewithout a type argument orMatrixwithout angle brackets switches off type checking. Compile with-Xlint:unchecked: a correct solution produces no warnings. - Comparing with doubleValue() instead of compareTo(). Such a
MinMaxdoes not work with strings and compares largelongvalues wrongly: values above 253 lose precision when converted todouble. - Testing only convenient data. An empty array, matrices of different sizes and division by zero are exactly the cases where the task texts require specific behavior.
Check your code
Before you compare your work with a video walkthrough, go through the code style checklist: meaningful class names and type parameter names (T, E, K, V), private fields, and a static nested class wherever it does not need an object of the outer class.
Frequently Asked Questions
In what order should I solve the Java generics and inner classes exercises?
Solve the five nested class tasks in order: the local class and static nested class tasks rewrite the solution of Member Inner Class. The generics tasks are independent of each other; a good route is from the class with three type parameters to the matrix and MinMax, leaving the wildcard task for last.
How do I check my solution to a task without a video walkthrough?
The matrix, MinMax, calculator and wildcard tasks give the expected output — your program must print the same. The task texts also list calls that must not compile: if the compiler accepts them, the type bound is wrong.
Can I solve these exercises on Java 8?
Yes, no task needs a newer language feature. But if you use the diamond operator with an anonymous class (new Comparator<>() { ... }, Java 9), var (Java 10) or static members in an inner class (Java 16), the solution will not compile on Java 8.
What interview questions are asked about inner classes and generics?
Why you cannot create an inner class with new Inner() in main() and how a static nested class differs; why new T() and new T[10] are not allowed; what ? extends and ? super mean. All three topics appear in the tasks Static Nested Class, Generic Matrix and Wildcard: Copy a List and Find the Maximum.
Comments