lots of exercises in java... from https://github.com/exercism/java

TUTORIAL.md 16KB

NOTE: You can also view the HTML version of this file here: https://github.com/exercism/java/blob/master/exercises/hello-world/TUTORIAL.md


Solving "Hello, World!"

Welcome to the first exercise on the Java track!

This is a step-by-step guide to solving this exercise.

Each exercise comes with a set of tests. The first pass through the exercise is about getting all of the tests to pass, one at a time.

If you have not installed the Java Development Kit and Gradle, you must do so now. For help with this, see: http://exercism.io/languages/java/installing


This guide picks-up where Running the Tests (in Java) left off. If you haven't reviewed those instructions, do so now.

The following instructions work equally well on Windows, Mac OS X and Linux.

Reading Gradle output

Use Gradle to run the tests:

$ gradle test

This command does a lot and displays a bunch of stuff. Let's break it down...

:compileJava
:processResources UP-TO-DATE
:classes

Each line that begins with a colon (like :compileJava) is Gradle telling us that it's starting that task. The first three tasks are about compiling the source code of our solution. We've done you a favor and included just enough code for the solution that it compiles.

When a task is successful, it generally does not output anything. This is why :compileJava and :classes do not produce any additional output. :processResources reports that it had nothing to do.

So far, so good...

The next three tasks are about compiling source code of the tests.

:compileTestJava
:processTestResources UP-TO-DATE
:testClasses

... with both sets of source code successfully compiled, Gradle turns to running the task you asked it to: executing the tests against the solution.

:test

HelloWorldTest > helloSampleName FAILED
    java.lang.UnsupportedOperationException: Delete this statement and write your own implementation.
        at HelloWorld.hello(HelloWorld.java:3)
        at HelloWorldTest.helloSampleName(HelloWorldTest.java:22)

HelloWorldTest > helloNoName FAILED
    java.lang.UnsupportedOperationException: Delete this statement and write your own implementation.
        at HelloWorld.hello(HelloWorld.java:3)
        at HelloWorldTest.helloNoName(HelloWorldTest.java:11)

HelloWorldTest > emptyStringIsComparedByValue FAILED
    java.lang.UnsupportedOperationException: Delete this statement and write your own implementation.
        at HelloWorld.hello(HelloWorld.java:3)
        at HelloWorldTest.emptyStringIsComparedByValue(HelloWorldTest.java:17)

HelloWorldTest > helloAnotherSampleName FAILED
    java.lang.UnsupportedOperationException: Delete this statement and write your own implementation.
        at HelloWorld.hello(HelloWorld.java:3)
        at HelloWorldTest.helloAnotherSampleName(HelloWorldTest.java:27)

4 tests completed, 4 failed
:test FAILED

FAILURE: Build failed with an exception.

* What went wrong:
Execution failed for task ':test'.
> There were failing tests. See the report at: file:///home/logan/hello-world/build/reports/tests/index.html

* Try:
Run with --stacktrace option to get the stack trace. Run with --info or --debug option to get more log output.

BUILD FAILED

Total time: 12.361 secs

Seeing the word "fail" TEN TIMES might give you the impression you've done something horribly wrong. You haven't. It's a whole lot of noise over a few tests not passing.

Let's focus in on the important bits:

HelloWorldTest > helloSampleName FAILED
    java.lang.UnsupportedOperationException: Delete this statement and write your own implementation.

...is read: "Within the test class named HelloWorldTest, the test method helloSampleName did not pass because an UnsupportedOperationException was thrown with the message Delete this statement and write your own implementation.."

The next line of the stack trace tells us where the exception was thrown:

        at HelloWorld.hello(HelloWorld.java:3)

Looks like it was on line 3 in the HelloWorld file.

We should remove this line from our HelloWorld.java file.

In your favorite text editor, open src/main/java/HelloWorld.java.

Delete the contents of that line and replace it with the following:

return null;

Now you can run gradle test again.

You should see a new error this time, don't worry though, you're making progress.

HelloWorldTest > helloNoName FAILED
    java.lang.AssertionError: expected:<Hello, World!> but was:<null>

...is read: "Within the test class named HelloWorldTest, the test method helloNoName did not pass because the solution did not satisfy an assertion. Apparently, we expected to see the string 'Hello, World!' but the value null was returned instead.

The last line of the stack trace tells us exactly where this unsatisfied assertion lives:

        at HelloWorldTest.helloNoName(HelloWorldTest.java:10)

Looks like the crime was discovered on line 10 in the test file.

Knowing these two facts,

  1. The return value was not what was expected, and
  2. The failure was on line 10 of the test,

We can turn this failure into success.

Fixing the first test

Open src/test/java/HelloWorldTest.java and go to line 10.

assertEquals("Hello, World!", HelloWorld.hello(""));

The test is expecting that hello(), when given an empty string (""), returns "Hello, World!". Instead, hello() is returning null. Let's fix that.

Open src/main/java/HelloWorld.java.

public class HelloWorld {
  public static String hello(String name) {
    return null;
  }
}

Let's change that to return the expected string:

public class HelloWorld {
  public static String hello(String name) {
    return "Hello, World!";
  }
}

Save the file and run the tests again:

$ gradle test
:compileJava
:processResources UP-TO-DATE
:classes
:compileTestJava
:processTestResources UP-TO-DATE
:testClasses
:test

HelloWorldTest > helloSampleName FAILED
    org.junit.ComparisonFailure: expected:<Hello, [Alice]!> but was:<Hello, [World]!>
        at org.junit.Assert.assertEquals(Assert.java:115)
        at org.junit.Assert.assertEquals(Assert.java:144)
        at HelloWorldTest.helloSampleName(HelloWorldTest.java:22)

HelloWorldTest > helloNoName PASSED

HelloWorldTest > emptyStringIsComparedByValue PASSED

HelloWorldTest > helloAnotherSampleName FAILED
    org.junit.ComparisonFailure: expected:<Hello, [Bob]!> but was:<Hello, [World]!>
        at org.junit.Assert.assertEquals(Assert.java:115)
        at org.junit.Assert.assertEquals(Assert.java:144)
        at HelloWorldTest.helloAnotherSampleName(HelloWorldTest.java:27)

4 tests completed, 2 failed
:test FAILED

FAILURE: Build failed with an exception.

* What went wrong:
Execution failed for task ':test'.
> There were failing tests. See the report at: file:///home/logan/hello-world/build/reports/tests/index.html

* Try:
Run with --stacktrace option to get the stack trace. Run with --info or --debug option to get more log output.

BUILD FAILED

Total time: 20.855 secs

Woohoo! :) You can see that helloNoName() and emptyStringIsComparedByValue() are now passing.

Let's tackle the next test...

Fixing the second test

Right now, we've hardcoded the greeting. This second test has unleashed a new expectation: that our program must incorporate a name given into that greeting. When given the name "Alice", that's who should be greeted instead of "World".

(Re)open src/main/java/HelloWorld.java.

public class HelloWorld {
  public static String hello(String name) {
    return "Hello, World!";
  }
}

While hello() does accept a reference to a string named name, it is not using it in the output. Let's change that:

public class HelloWorld {
  public static String hello(String name) {
    return "Hello, " + name + "!";
  }
}

... and rerun the tests ...

$ gradle test
:test

HelloWorldTest > helloSampleName PASSED

HelloWorldTest > helloNoName FAILED
    org.junit.ComparisonFailure: expected:<Hello, [World]!> but was:<Hello, []!>
        at org.junit.Assert.assertEquals(Assert.java:115)
        at org.junit.Assert.assertEquals(Assert.java:144)
        at HelloWorldTest.helloNoName(HelloWorldTest.java:11)

HelloWorldTest > emptyStringIsComparedByValue FAILED
    org.junit.ComparisonFailure: expected:<Hello, [World]!> but was:<Hello, []!>
        at org.junit.Assert.assertEquals(Assert.java:115)
        at org.junit.Assert.assertEquals(Assert.java:144)
        at HelloWorldTest.emptyStringIsComparedByValue(HelloWorldTest.java:17)

HelloWorldTest > helloAnotherSampleName PASSED

4 tests completed, 2 failed
:test FAILED

FAILURE: Build failed with an exception.

* What went wrong:
Execution failed for task ':test'.
> There were failing tests. See the report at: file:///home/logan/hello-world/build/reports/tests/index.html

* Try:
Run with --stacktrace option to get the stack trace. Run with --info or --debug option to get more log output.

BUILD FAILED

Total time: 12.466 secs

Wait... didn't we just fix the test? Why is it failing? Take a closer look...

In fact, helloSampleName() is passing. It's just that at the same time, we just inadvertently broke the first tests: helloNoName() and emptyStringIsComparedByValue().

This is one tiny example of the benefit of maintaining a test suite: if we use them to drive out our code, the second we break the program the tests say so. Since we saw them passing just before our latest change, whatever we just did most likely cause that regression.

Our latest change was making the greeting dependent on the name given. Our first two tests expects that if either a blank string or null are given as the name, then "World" should be substituted in. Let's implement that.

src/main/java/HelloWorld.java:

public class HelloWorld {
  public static String hello(String name) {
    if(name == null || "".equals(name)) {
      name = "World";
    }
    return "Hello, " + name + "!";
  }
}

... and re-run the tests ...

$ gradle test
...
:test

HelloWorldTest > helloSampleName PASSED

HelloWorldTest > helloNoName PASSED

HelloWorldTest > emptyStringIsComparedByValue PASSED

HelloWorldTest > helloAnotherSampleName PASSED

BUILD SUCCESSFUL

Total time: 11.717 secs

Oh, hello! Turns out, the solution we put into place didn't just apply for "Alice" but "Bob" equally well. In this case, the test succeeded with no additional code on our part.

Refactoring

Now that you've got all the tests passing, you might consider whether the code is in the most readable/maintainable/efficient shape. What makes for "good" design of software is a big topic. The pursuit of it underlies much of what makes up the more valuable conversations on Exercism.

For now, let's just take a quick review of our solution and see if there's any part of it we'd like to refactor. Refactoring is changing the way a bit of code reads without changing what it does.

Right now, the details of detecting whether the caller of hello() has given a name or not (i.e. name is either null or an empty string) is sitting right next to the core responsibility of the method: to produce a personalized greeting.

public class HelloWorld {
  public static String hello(String name) {
    if(name == null || "".equals(name)) {
      name = "World";
    }
    return "Hello, " + name + "!";
  }
}

How would things read if we extracted those details into a separate method and at the same time, replaced the if with a ternary expression?

public class HelloWorld {

  public static String hello(String name) {
    String whom = isBlank(name) ? "World" : name;
    return "Hello, " + whom + "!";
  }

  private static boolean isBlank(String string) {
    return string == null || "".equals(string);
  }
}

By extracting that logic into the isBlank() method, we've added a little abstraction to our program -- it's not as literal as it was before. Yet, it allows us to defer needing to understand how "blankness" is detected. If we can assume that isBlank() just works, we don't have to downshift in our head to those details. Instead, we can remain at the same level of thinking: to whom are we greeting?

The ternary operator allowed us to express that choice in a more compact form. Less to read without losing the intent.

Finally, we introduced another variable: whom. Doing so gives a name to the output of the ternary expression. We certainly could have continued to reuse name, but by introducing a second String to hold the calculated value this keeps crisp the two ideas: name is what's given and whom is what's been determined.

We made a bunch of changes, let's make sure we didn't break the program!

$ gradle test
...
HelloWorldTest > helloSampleName PASSED

HelloWorldTest > helloNoName PASSED

HelloWorldTest > emptyStringIsComparedByValue PASSED

HelloWorldTest > helloAnotherSampleName PASSED
...

This illustrates another benefit of writing tests: you can make significant changes to the structure of the program and very quickly restore your confidence that the program still works. These tests are a far cry from a "proof" of correctness, but well-written tests do a much better job of (very quickly) giving us evidence that it is. Without them, we manually run the program with different inputs and/or inspecting the code line-by-line -- time-consuming and error prone.

Submitting your first iteration

With a working solution that we've reviewed, we're ready to submit it to exercism.io.

$ exercism submit src/main/java/HelloWorld.java

Next Steps

From here, there are a number of paths you can take.

Move on to the next exercise

There are many more exercises you can practice with. Grab the next one!

$ exercism fetch java

Review (and comment on) others' submissions to this exercise

The heart of Exercism are the conversations about coding practices. It's definitely fun to practice, but engaging with others both in their attempts and your own is how you get feedback. That feedback can help point out what you're doing well and where you might need to improve.

Some submissions will be nearly identical to yours; others will be completely different. Seeing both kinds can be instructive and interesting.

Note that you can only view submissions of others for exercises you have completed yourself. This enriches the experience of reading others' code because you'll have your own experience of trying to solve the problem.

Here's an up-to-date list of submissions on the Java track:

http://exercism.io/tracks/java/exercises

Submit another iteration

You are also encouraged to consider additional "requirements" on a given exercise.

For example, you could add a test or two that requires that the greeting use the capitalized form on the person's name, regardless of the case they used.

In that situation, you'd write a test to set-up that new expectation and then implement that in the code (the same process we just went through together, above).

Contribute to Exercism

The entire of Exercism is Open Source and is the labor of love for over 100 maintainers and many more contributors.

A starting point to jumping in can be found here:

https://github.com/exercism/problem-specifications/blob/master/CONTRIBUTING.md


Regardless of what you decide to do next, we sincerely hope you learn and enjoy being part of this community. If at any time you need assistance do not hesitate to ask for help:

http://exercism.io/languages/java/help

Cheers!