Quiz Yourself

More intermediate and advanced test questions

If you’re a regular reader of this quiz, you know these questions simulate the level of difficulty of two different certification tests. Those marked “intermediate” correspond to questions from the Oracle Certified Associate exam, which contains questions for a preliminary level of certification. Questions marked “advanced” come from the 1Z0-809 Programmer II exam, which is the certification test for developers who have been certified at a basic level of Java 8 programming knowledge and now are looking to demonstrate more-advanced expertise.

Question 1 (intermediate).

The objective is to create methods with arguments and return values, including overloaded methods. Given the following classes:

class GenericEngine { public String engType="GE-001"; }

class CombustionEngine extends GenericEngine { 
    public String engType="CE-002"; }

class JetEngine extends CombustionEngine { 
    public String engType="JE-003"; }

public class Car {
    public void setEngine(Object o) {
        System.out.print("I have unknown engine");
    }

    public void setEngine(GenericEngine ge) {
        System.out.printf(
            "I have generic engine: %s", ge.engType);
    }

    public void setEngine(CombustionEngine ce) {
        System.out.printf(
            "I have combustion engine: %s", ce.engType);
    }
}

And this code fragment:

JetEngine e = new JetEngine();
new Car().setEngine(e);

What is the result? Choose one.

  1. I have unknown engine
  2. I have generic engine: GE-001
  3. I have combustion engine: CE-002
  4. I have generic engine: CE-002
  5. I have combustion engine: JE-003

Question 2 (advanced).

The objective is to use the Path interface to operate on files and directory paths. Given this code fragment:

Path defaultRoot =
    Paths.get(System.getProperty("user.dir")).getRoot();
Path path = Paths.get("tmp", "john", "..", "doe");
path = defaultRoot.resolve(path);
System.out.print(path.getName(2)); // line n1

Assume that the file system containing the current working directory has an empty directory, tmp, in its root.

What is the result? Choose one.

  1. john
  2. ..
  3. doe
  4. Execution completes without exceptions, producing output that depends on the host operating system.
  5. An exception is thrown at line n1.

Question 3 (advanced).

The objective is to create and use lambda expressions. Given the following:

@FunctionalInterface // line n1
interface SwissKnife {
    default int compare(int i1, int i2) {
        return i1 - i2;
    }

    static void run() {
        System.out.println("Running !");
    }

    String welcomify(String name);
}

Which is true? Choose one.

  1. The following code compiles:
    SwissKnife sni = (int a, int b) -> a - b;
    
  2. The following code compiles:
    SwissKnife sni = () -> System.out.print("Running fast !");
    
  3. The following code compiles:
    SwissKnife sni = (a) -> "Welcome, " + a;
    
  4. Compilation fails at line n1.

Question 4 (intermediate).

The objective is to define the scope of variables. Given the following two methods, which are declared in the same class:

public static float divide(float arg1, float arg2) 
    throws ArithmeticException { // line n1
        return arg1/arg2;
}
    
public static void main(String[] args) {
    try {
        int arg1 = 10;
        int arg2 = 0;
        System.out.printf("Result: %f", divide(arg1, arg2));
    } catch (RuntimeException e) {
        System.out.printf(
            "Bad arguments: %d and %d", arg1, arg2); // line n2
    }
}

What is the result? Choose one.

  1. Result: Infinity
  2. Result: NaN
  3. Bad arguments: 10 and 0
  4. Compilation fails at line n1.
  5. Compilation fails at line n2.

Question 5 (advanced).

The objective is to create and manage date-based and time-based events, including creating a combination of date and time in a single object using LocalDate, LocalTime, LocalDateTime, Instant, Period, and Duration. A senior Java developer is traveling from Chicago O’Hare (ORD) in the United States to Warsaw (WAW) in Poland. She is trying to calculate the duration of the flight but thinks that Poland might be shifting to daylight saving time on the weekend of her journey. The information printed on her ticket is as follows:

Depart Chicago O’Hare
Scheduled: March 24, 2018 5:30 PM (17:30)
Arrive Warsaw
Scheduled: March 25, 2018 9:35 AM (09:35)

She has written the following incomplete code:

ZonedDateTime ord = 
    ZonedDateTime.of(2018, 3, 24, 17, 30, 0, 0,
                     ZoneId.of("America/Chicago"));

ZonedDateTime waw = 
    ZonedDateTime.of(2018, 3, 25, 9, 35, 0, 0,
                     ZoneId.of("Europe/Warsaw"));

// line n1

System.out.print("Time in plane is: " + timeInPlane);

What code does she insert at line n1 to calculate the elapsed time expected between departure and arrival? Choose one.

  1. long timeInPlane = ChronoUnit.HOURS.between(ord, waw);
  2. Period timeInPlane = Period.between(
          ord, ZoneId.of("America/Chicago"),
          waw, ZoneId.of("Europe/Warsaw"));
  3. Period timeInPlane = Period.between(ord, waw); 
  4. Duration timeInPlane = Duration.between(ord, waw); 

Question 6 (intermediate).

The objective is to explain an object’s lifecycle (creation, dereference by reassignment, and garbage collection). Given the following GCDemo class:

public class GCDemo {
    public static ArrayList<Object> l = new ArrayList<>();    
    public void doIt() {
        HashMap<String, Object> m = new HashMap<>();     
        Object o1 = new Object(); // line n1
        Object o2 = new Object();
        m.put("o1", o1);
        o1 = o2; // line n2
        o1 = null; // line n3
        l.add(m);
        m = null; // line n4
        System.gc();// line n5
    }
}

And given this code fragment:

GCDemo demo = new GCDemo();
demo.doIt();
demo = null;  // line n6

When does the object created at line n1 become eligible for garbage collection?

  1. At line n2
  2. At line n3
  3. At line n4
  4. At line n5
  5. At line n6
  6. None of the above

Answer 1.

The correct option is C. This question investigates how methods are selected for invocation and also how variables are resolved. The relevant rules for overloaded methods and for field access are documented in Java Language Specification sections 15.12.2 and 15.11.1.

The code fragment constructs a JetEngine and initializes a variable of that same type to refer to the object. It then calls a setEngine method, using the variable as the argument. There are three overloaded methods called setEngine, and although none of them takes an argument of exactly the type JetEngine, the argument of each overload is a parent of JetEngine. Therefore, any of them could accept the argument. But one method must be selected, and the compiler performs that selection. The specification describes how the method is selected from the candidate overloads in as many as three stages.

The first stage (which happens to be where the decision is made in this example) is to try to identify the target method based on the provided argument types, without using any autoboxing/unboxing or variable argument list-handling rules.

If the first stage were to fail, a second stage would look for a target method by applying autoboxing/unboxing, and finally, the third stage would look for a match by applying variable argument list-handling rules.

So, the first stage selects the “most specific” method based on the types of the parameters. Java Language Specification section 15.12.2.5 says the following:

If more than one member method is both accessible and applicable to a method invocation, it is necessary to choose one to provide the descriptor for the run-time method dispatch. The Java programming language uses the rule that the most specific method is chosen.

There’s a fairly long and detailed definition of “most specific” in the specification, but in this context, it simply means “nearest to the actual argument type.” Given a JetEngine as an actual parameter, the CombustionEngine is the “most specific” formal parameter type, and Object is the least specific. Therefore (and given that no setEngine(JetEngine e) method is defined), the compiler will generate code to invoke the setEngine(CombustionEngine ce) method.

In light of this discussion, you know that the output will start with the message I have combustion engine. Consequently, options A, B, and D are incorrect.

Next, let’s consider which engine type message is printed. Each of three engine variants has the same engType variable, and each subclass “hides” the parent’s class variable. This represents highly dubious style, and this question illustrates fairly convincingly why it’s considered bad.

It might seem as if the three variables called engType are “overrides” in the same sense that occurs in the definitions of methods with the same name in a hierarchy of classes; however, this is not the case. A single instance of the JetEngine object actually contains three independent variables called engType, each with different values and each visible from different scopes. Java Language Specification section 15.11.1 states the following:

Note that only the type of the [p]rimary expression, not the class of the actual object referred to at run time, is used in determining which field to use.

The “primary expression” is the part that comes before the last dot. In the following setEngine method, that means the primary expression is ce, and its type is CombustionEngine. Consequently, the engType variable that is printed is the one embedded in the CombustionEngine part of the object, and that has the value CE-002. Because of this, you can see that option C is correct and option E is incorrect:

public void setEngine(CombustionEngine ce) {
    System.out.printf("I have combustion engine: %s", ce.engType);
}

This behavior might be surprising, but the central point is that the late-binding effect applies only to the invocation of a nonprivate, nonfinal instance method on an object, not to direct field access. The behavior can perhaps be improved in several ways:

  • You could render this behavior less surprising if you simply avoided using the same variable name.
  • You could change this behavior to print a more expected message if you avoided making direct reference to the variable engType and instead invoked a method getType() and ensured that this method is overridden in all three classes. However, this is a cumbersome solution, duplicating an identical getType method in every class.
  • Another possibility would be to have a single engType variable, defined in the base Generic- Engine class. This variable is configured appropriately via a chain of constructors in the three classes. The following example implements this approach by using a final field.

class GenericEngine {
    public final String engType;
    protected GenericEngine(String engType) { 
        this.engType = engType; }
    public GenericEngine() { this("GE-001"); }
}

class CombustionEngine extends GenericEngine {
    protected CombustionEngine(String engType) { super(engType); }
    public CombustionEngine() { this("CE-002"); }
}

class JetEngine extends CombustionEngine {
    protected JetEngine(String engType) { super(engType); }
    public JetEngine() { this("JE-003"); }
}

However, the best solution might be simply to avoid using class inheritance in this situation entirely. If the protected constructor of the GenericEngine class in the last code block were made public, that would allow all three engine types to be handled directly by that class. Of course, there might be other constraints on a more complete design, but a common mantra in modern software engineering is to “prefer delegation over inheritance.” Using delegation for code reuse is an approach you should understand, but it’s more complex than can be discussed in the context of this question.

Answer 2.

The correct option is B. The Path class represents the idea of a path on the file system—that is, an optional sequence of hierarchical directory names, perhaps ending in a filename. A Path object itself is not directly linked with the physical file system. Such a connection is created when some action is taken using the Path—for example, listing the contents of a directory or creating a file. The reason for avoiding such a hard connection is fairly compelling: If you could use a Path only to represent something that already exists, a Path could not be used in the process of creating a new directory or file.

Given this background, the question revolves around four inquiries. What do the Paths.get operations do? What does the resolve operation do? Can you extract getName(2) from the path after resolve has done its work? And if getName doesn’t throw an exception, what value does it provide in this situation?

You could be forgiven for wondering what the first two lines do in general, and to be fair, some of that code is beyond the scope of the exam. That first line in particular is likely beyond the scope of the real exam, but we left it in to show how you can answer a question even if you don’t necessarily have a perfect understanding of everything. Given that none of the options admits the possibility of code other than the last line failing in any way, you can safely assume that these first lines compile and run without crashing. It’s also reasonable—and accurate—to assume that this opening code does what it seems to suggest.

Java’s APIs give you the tools to work with hierarchical directories without ever making explicit literal references to root directories and path separators.

The first line extracts a Path object that represents the root of the file system that contains the user’s current working directory. So, for example, if the current working directory is C:\users\simon\javaprojects\examproj1, the extracted Path object represents C:\.

The second line extracts another Path—which is likely to be a relative path—representing a path hierarchy that might be represented in a UNIX-like format as either tmp/john/../doe or tmp/doe.

Which of those two relative paths do you get? From the perspective of Paths.get, the .. is just an element of the path; it is not treated as anything special in the basic creation process. Therefore, the effective path has four elements: tmp, john, .., and doe.

Why do you make the path this way rather than by simply specifying a String such as tmp/john/../doe? The reason is that Java seeks to allow you to create platform-independent programs. It’s important to be able to describe and manipulate paths on a file system without hardcoding things such as forward slashes or backslashes. Allowing a variable-length argument list of Strings and joining them at runtime in whatever way is appropriate for the current execution platform is much more flexible than defining literal strings with separators. You can access the system property file.separator to find the local character if you want, but why bother? So yes, this approach does work, and it looks as if it should. Therefore, option D is incorrect.

As a side note, in literal path strings, forward slashes (UNIX style) work properly on Microsoft Windows-based JVMs, but backslashes (Windows style) fail on UNIX-based JVMs. The flexibility on Windows JVMs is possible because a forward slash is not a legal character at the operating system level in Windows paths or filenames. Therefore, if the JVM sees the forward slash, it can safely swap that character for a backslash before handing the path to the Windows system. But if a backslash shows up in a literal path string in UNIX, it’s a legal—if rather odd—character in a UNIX pathname, so simply swapping would change a valid meaning.

This entire question of forward slashes and backslashes is made even more interesting because there are other systems that use different path schemes entirely. On the OpenVMS operating system, local paths have a structure such as sys$disk:[dir.path.elements]myfile.txt;1. Clearly, making simple assumptions about slashes would not work with this. The lesson here is that Java’s APIs give you the tools to work with hierarchical directories without ever making explicit literal references to root directories and path separators. Clearly, it’s a good habit to use them, because it will protect your code if it’s ever run on unfamiliar operating systems.

The next question is what does resolve do? There are a couple of corner cases, but the most commonly used behavior is the one used here. If the invocation path is not empty and the argument path is not absolute, the effect is to concatenate the two paths. As a result, this takes the relative path (tmp/john/../doe in UNIX-like format) and anchors it to the root of the current file system. Again in UNIX-like format, this becomes /tmp/john/../doe. Note that resolve still does not eliminate the .. part; that is the job of the method normalize.

So, now you need to know whether getName(2) is successful and, if it is, what it returns. It turns out that Path effectively treats the elements of a path (in this case tmp, john, .., and doe) like a list with a zero-based indexing system. Further, the root part of the path is not included in that list nor in its indexing scheme. Importantly, this exclusion of the root is consistent even if the path were on a Windows system where this example would likely represent C:\tmp\john\..\doe. So, in a platform-independent way, the indexing starts at zero with tmp (and never with C:\ or something similar). This again shows that option D is incorrect.

Further, you can see that the index 2 should be within the valid range, so option E is incorrect, because no exception is thrown. Further, you will get .. regardless of the host operating system. This tells you that option B is correct and options A and C are both incorrect.

Answer 3.

The correct option is C. In the functional programming style, functions can be arguments and return values of other functions. Many languages support this idea directly, but most object-oriented programming (OOP) languages allow passing only objects to and from functions (and, of course, you usually call functions in your object system’s methods).

To address this seeming limitation, OOP design patterns (such as the Command pattern) suggest creating an object (function or method—or whatever you want to call it) that contains the desired behavior and passing that around. This works perfectly well, but the syntax tends to be cumbersome, because all the “syntactic scaffolding” necessary to define a class, and then create and instantiate an object from it, really has no immediate relevance to the point of the source code—which is, in such a situation, simply to describe a function.

To ameliorate this, Java 1.1 provided anonymous classes, which reduce the syntactic scaffolding a little, but more importantly allow the definition, instantiation, and usage to be done all in the same place—for example, as a parameter to a method invocation. Passing an object that implements SwissKnife might look like this when written using the anonymous syntax:

doStuffWithASwissKnife(new SwissKnife() {
    public String welcomify(String a) {
        return "Welcome, " + a;
    }
});

Note that while the anonymous syntax shown here is consistent with any version of Java from 1.1 onward, the static and default methods in the SwissKnife interface require at least Java 8.

As stated earlier, the behavior definition is colocated with the use, which is good, but the syntax is still very cluttered.

Java 8 addressed this clutter and, in specific situations, allows a better syntax, known as lambdas. Lambdas are essentially expressions that define a class and instantiate an object from that class, but they do it in a way that allows you to write code that addresses only the definition of a single function. Because of this restriction of defining a single function, the syntax can be used only to implement interfaces that have a single abstract method (SAM). Java’s documentation generally refers to this type of interface as a functional interface, and the annotation with the same name can be used to verify that an interface does indeed have exactly one abstract method.

So, the SwissKnife interface is a functional interface, because it has a SAM named welcomify. The other two methods provide implementations, so they are not abstract and don’t present a problem for the lambda syntax rules. The job of the @FunctionalInterface annotation is to cause a compiler error if it is attached to an interface that has zero abstract methods or more than one. Because the code given compiles correctly, option D is incorrect.

By now, it might be clear that the relationship between an interface and a lambda expression that implements it centers on the SAM in that interface. The lambda provides an implementation for that method, and the lambda’s arguments and return type must match those of the interface’s abstract method. By the way, the Java compiler’s ability to determine the applicable type for a particular context is called type inferencing.

Therefore, a lambda that implements SwissKnife must take a single parameter of type String, and it must return a String. The only matching lambda is option C, so C is the correct answer. Options A and B are incorrect because their parameter lists do not match the requirement of the SwissKnife’s single abstract method.

As a side note, lambda expressions may include or omit the types of all their parameters; it is probably better, in general, to omit parameter types and allow them to be inferred from the context. However, in some situations, perhaps with overloaded methods, the target interface might be ambiguous. In such a situation, providing the argument type might be necessary to allow compilation to succeed. Sometimes, including the argument type might make a program easier to read. Option A includes the argument types, but it fails because the argument list is incorrect in type (int, rather than String) and in number (two instead of one), not because the argument types are specified. For illustration, the following would also be correct:

SwissKnife sni = (String a) -> "Welcome, " + a;

Answer 4.

The correct option is E. This is one of those uncomfortable questions. Because compilation fails and in daily coding the problem would be reported immediately by the development environment, it can seem like an unreasonable question to ask. But, exams are not daily coding; they attempt to probe your knowledge. As such, a little care and attention to detail should lead you to the right answer and, in the process, allow you to demonstrate an element of core knowledge that is being legitimately tested. If you’re still uneasy by the end of the discussion, know that the exam creators try hard to limit the number of questions that fall into this category, and the information in the question—specifically “fails at line n2”—should be used to help you spot the right answer.

Consider what happens if you perform a division by zero. It turns out that it depends on the type of the expression.

Let’s look at the question. The setup has all the hallmarks of being about the way Java performs arithmetic and, in particular, how it handles division by zero—but it’s not. It’s about the scope of variables.

In general, a local variable, such as arg1 and arg2 in this sample, is visible from the point of declaration to the end of the immediately enclosing block; that’s the region bounded by curly braces. As a result, arg1 and arg2 are not accessible in the catch block, and line n2 fails to compile. This—along with the assurance that there’s only one correct answer—tells you that option E is correct.

An important note is that the description of visibility just given isn’t complete and, therefore, isn’t fully correct. Formal parameters (such as the variables in the argument list of a method) will be visible from the point of declaration to the end of the block that is associated with whatever those variables are formal parameters to. By way of examples, the variable args, which is the formal parameter of the main method, is visible throughout the main method body. The variable e, which is the formal parameter of the catch block, is visible throughout that catch block. Similar rules apply to similar situations, including variables declared in the resources section of a try-with-resources structure, and those declared in for loops.

So, you could fix this particular compilation error simply by moving the declarations of the two variables further up in the source code so that they are directly above the try keyword. In that case, the code would compile and run. Now, to make this question and its discussion more interesting, consider what would happen if that were the case; after all, the distractors (the wrong answers) were chosen to be at least tempting, which should be true of all multiple-choice exam questions.

Perhaps the best starting point is to consider what happens if you perform a division by zero. It turns out that it depends on the type of the expression. If an integer division expression has zero in the divisor (the bottom part of a fraction), the code throws an ArithmeticException. In practice, this means that both the divisor and the dividend (the top) must be of integral types; if either has a floating-point type, the expression has a floating-point type, and no exceptions are possible. In fact, floating-point expressions with division by zero produce one of three special values: Infinity, -Infinity, and NaN (“Not a Number”). If the dividend is nonzero, and it has the same sign as the zero divisor (floating-point arithmetic distinguishes positive and negative zero), you get Infinity. If the signs are different, you get -Infinity, and if both the dividend and divisor are zero, you get NaN. We hope this information is interesting, but it’s not needed for either of the Java exams. However, because the code could not possibly produce NaN, option B must be incorrect.

The next consideration is that the variables arg1 and arg2 are declared as int, but the divide method takes two float arguments. So, would this division be handled in floating-point or integer arithmetic format? The arguments are promoted to float for the method invocation, so a floating-point expression is evaluated and, again, no exception will be thrown. That tells you that option C cannot be correct, even if the scope problem were fixed. Also, from the previous discussion, you can tell that if the variable scope issue were fixed and the code were compiled, the output would be in the form of option A.

You could then ask, if the expression cannot throw an exception, is it an error that the method declares an exception that will definitely not arise? The answer is no, and in fact, it’s a general rule that methods are permitted to declare exceptions that they never throw. One reason this is important is that overriding methods are not permitted to throw checked exceptions that are not permissible from the method being overridden. On this basis, abstract methods in interfaces regularly declare exceptions that, given that they have no implementation, they obviously cannot throw. It’s also worth noting that ArithmeticException is an unchecked exception, so there’s never a requirement to declare it on any method. However, it’s also perfectly permissible to do so, even if it’s unusual and not recommended style. From this, you can determine that line n1 does not cause a compilation error and option D is incorrect.

Answer 5.

The correct answer is option D. In fact, the intrepid programmer is correct: Poland did move its clocks forward an hour at 02:00 on the morning of March 25, 2018. However, the passenger’s task is actually easy. The ZonedDateTime class specifically addresses the representation of not only time zone but also of legally prescribed changes of time, such as daylight saving time. Consequently, the various means of calculating the offset between two ZonedDateTime objects take any such shifts into account automatically.

Since Java 8 introduced the Date and Time API, Java contains a database of all the historical daylight saving time changes from governments around the world, and it is updated with new information as it becomes available.

Let’s review the options and compare what they produce. Option A is incomplete; it will print Time in plane is: 9 to the console, which is the correct number of full hours, but it will entirely ignore the minutes. The departure time ends with 30 minutes and the arrival time ends with 35 minutes, so the number of hours is not exact. If the arrival time were at the same number of minutes past the hour, the output might appear correct, but it would be by luck, not because of technical validity. Consider also if our developer’s flight had been to India or another country that has time zones that vary from those of other countries by an amount that is not a whole numbers of hours. Because of this, option A is incorrect.

You might improve this option by rewriting the code like this, which would give a result that is a bit “better”—9.083333333333334—although the format is not very helpful:

double timeInPlane = 
    ChronoUnit.MINUTES.between(ord, waw) / 60.0;

The Period class measures time differences in days, months, and years, and its between method accepts only two arguments, which must be LocalDate objects. Because of this, both options B and C are incorrect.

Option D is correct; it accurately denotes the time spent in the plane, and it takes into account the time zones the flight passes through and the daylight saving time change in Poland that occurs midflight. The output is in the rather odd form PT9H5M. This cryptic presentation specifies a “period of time of 9 hours and 5 minutes,” which is, in fact, the scheduled time for the flight.

The PT part of the output is hardcoded in the Duration.toString() method, and as you might expect, it’s possible to extract the hours, minutes, and so on for a nicer presentation. The Java Date and Time API is very full featured, and it’s worth spending a little time browsing Oracle’s tutorial on the topic as well as the Javadoc so you can get the best out of this powerful API.

As a side note, since Java 8 introduced the Date and Time API, Java contains a database of all the historical daylight saving time changes from governments around the world, and it is updated with new information as it becomes available.

There’s a final observation to make here about the nature of test questions and what you need to answer them. While we were preparing this question, we pondered whether options B and C were bad because they appeared to require rote learning of the API. Questions that test the simple learning of facts, particularly facts that an IDE will tell you, are considered to be a bad idea (although a few exist in the real test, and you’ll see many on the free sample tests that are abundant on the web). However, we decided to keep these options because although they can be rejected by rote knowledge, they are best answered from a position of understanding. So, let’s explore how you would address options B and C from a position of understanding the API, rather than from simple learning of that API’s contents.

It’s a key feature of the API as a whole that the Duration and Period classes represent different concepts. Specifically, Duration represents “physics time” and Period represents a human-calendar type of time in days—strictly in days, months, and years, but expressly without hours, minutes, seconds, and the like. From that, it’s a simple deduction that Period cannot be appropriate to the task of extracting hours and minutes. Therefore, you can eliminate options B and C immediately, without concern about whether the specific methods exist or not.

With a little luck, this persuades you that understanding will beat, or at least greatly augment, knowledge, even in a multiple-choice test.

Answer 6.

The correct option is F. This question investigates how objects become eligible for garbage collection and, at the risk of spoiling the story by giving away the outcome, one way that memory leaks are possible in Java.

It turns out that the object created on line n1 never becomes eligible for garbage collection unless a new value is written to the static variable l or the class GCDemo remains loaded in the JVM. However, class unloading is not a topic of either the exams, so from an exam perspective, the object is effectively collectable only if more code is added. If the object never becomes eligible for collection, the memory is simply reclaimed by the operating system after the JVM process exits. But because the question never addresses when the JVM exits, and that is not an option you can select, the correct answer is F: “None of the above.”

By the way, a while ago, the Oracle exams adopted a general policy of avoiding “None of the above” and similar variations as options. However, because this rule has not always been in place, we can’t be completely certain you won’t ever see it, and it suits the learning purpose of this question to use it here.

So, how does an object become eligible for garbage collection? This happens when there are no live references to the object left in the program. What does that mean? Well, when the object of interest is created on line n1, the variable o1 is assigned to point at it. The variable o1 is a reference (a reference is a kind of pointer, but one to which you cannot make arbitrary changes). In other words, o1 is not the data; it’s how to find the data. Because o1 is a variable that the thread can use, it’s a live reference and the object is not eligible for garbage collection, because the program can still find the object.

The garbage collector never makes anything eligible for collection; all it ever does is collect things that are already eligible.

Importantly, anytime you take a copy of the value of o1, you duplicate the instructions on how to find the object (you don’t duplicate the data). For the sake of a colorful analogy, imagine the object is buried treasure. If a pirate has a treasure map (a reference), he can find the treasure and use it if he wants to. Further, if another pirate makes a copy of that treasure map, either pirate can find the treasure. Now, if the first pirate’s map sinks with his ship, the second pirate can still find the treasure and use it. But if all the copies of the map go down in sinking ships, nobody can find the treasure. (The assumption is that the pirates don’t remember the map details in their heads.) This is equivalent to the situation in which an object becomes eligible for garbage collection.

Now, let’s get back to real life. The line right before line n2—m.put("o1", o1)—puts a copy of the reference to the object into a Map (the data structure Map, not the pirate map, although the analogy holds there, too). This means that the Map structure can be used to reach the object.

Next, line n2 overwrites the pointer value in o1 with the value of o2. This action, in effect, turns the pirate map for finding the object into a pirate map for finding another object. But the original object can still be reached by using the variable m. The variable m lets you find the Map data structure, and the Map data structure still lets you find the original object. So, at this point, the original object is still not lost and is not eligible for collection. That means that option A is incorrect.

At line n3, the value of the o1 reference variable is changed again, but because it no longer refers to the original object, that doesn’t change the picture. You still can reach the object, so option B is also incorrect.

The next line (between lines n3 and n4) makes a copy of the reference value currently in variable m. The copy is placed in the List referred to by the static variable l. That means that you could follow the reference in l to find the Map (which at this point is still also referred to by the variable m), and then from the Map you can find the object. So, you now have another route for finding that object. Importantly, the variable l is static, so unlike method local variables m, o1, and o2, which cease to exist when the method doIt returns to its caller, the variable l (that is, the reference variable l, which is distinct from the List to which it refers) will not disappear unless the class GCDemo is unloaded (or the JVM shuts down).

Next, line n4 nulls out the direct reference to the Map (that’s the variable m). But it’s still possible to find the Map because you have the reference to it stored in the List from the previous line. By following the chain from that List to the Map, and then from the Map to the object, you can still reach the object. So, even now, the object is still reachable and still not eligible for garbage collection. Therefore, option C is incorrect.

At line n5, the code invokes the System.gc() method, which encourages the garbage collector to invest some time cleaning up. This method has been the subject of much commentary regarding why it might be best avoided, but that isn’t relevant here. The garbage collector never makes anything eligible for collection; all it ever does is collect things that are already eligible. As you saw, the object of interest wasn’t eligible on line n4, so the call on line n5 changes nothing, and option D is also incorrect.

After the doIt method returns to its caller, the local variables o1, o2, and m cease to exist. Those pirate maps go down with the sinking ship that is the doIt method. But the static variable l in the GCDemo class still exists, it is still accessible, and the transitive series of references still leads to the object. So, there is no change at this point.

Line n6 nulls out the reference demo that refers to an instance of the GCDemo class, incidentally rendering that object eligible for collection. But the variable l that still lets you reach the object is static, so unless something changes the value of l or the class GCDemo is unloaded, the object remains reachable. From this you can see that option E is also incorrect and, by elimination, option F must be correct.

As we hinted at the start, this kind of behavior is a good candidate for creating a memory leak. Of course, it’s possible this reference chain was kept deliberately, and there might be other code that cleans up the List contents, and the contents of the Maps that are in that List, at intervals. In that case, everything would be OK, but if this were overlooked, you would likely find that the program consumes ever more memory as it runs, creating a memory leak.

What actions could you take to avoid having this become a memory leak? Several options exist, and the right one depends on the real purpose of the code. Note that there are two potential leaks in the current code. Every call to doIt puts another Map into the List, and in that Map there’s another Object. These instances of Map and Object must be kept under control. Actions that might be involved in keeping memory allocation under control include the following:

  • Explicitly removing object references from the Map(s)
  • Explicitly removing Map references from the List
  • Explicitly overwriting the value of the static variable l, perhaps with null

Also in This Issue

Javalin: A Simple, Modern Web Server Framework
Building Microservices with Micronaut
Helidon: A Simple Cloud Native Framework
The Proxy Pattern
Loop Unrolling
Size Still Matters
Book Review: Modern Java in Action

Simon Roberts

Simon Roberts joined Sun Microsystems in time to teach Sun’s first Java classes in the UK. He created the Sun Certified Java Programmer and Sun Certified Java Developer exams. He wrote several Java certification guides and is currently a freelance educator who publishes recorded and live video training through Pearson InformIT (available direct and through the O’Reilly Safari Books Online service). He remains involved with Oracle’s Java certification projects.

Mikalai Zaikin

Mikalai Zaikin is a lead Java developer at IBA IT Park in Minsk, Belarus. During his career, he has helped Oracle with development of Java certification exams, and he has been a technical reviewer of several Java certification books, including three editions of the famous Sun Certified Programmer for Java study guides by Kathy Sierra and Bert Bates.

Share this Page