ONJava.com    
 Published on ONJava.com (http://www.onjava.com/)
 See this if you're having trouble printing code examples


Teaching Java the Extreme Way

by Daniel H. Steinberg
10/09/2002

This is the second in a series of articles that looks at how programming in Java is taught to new programmers. In the first article, I looked at the traditional first program, HelloWorld. I argued that although it was a good "systems check," it was not a good first program for a course in Java or in object-oriented programming. Despite the C-like syntax, we should not teach Java as if it were C with objects. Objects really need to come first.

Your feedback was wonderful. Take a look back at last month's article and the accompanying talkback and feel free to join in any of the threads. Many people provided suggestions for tools and approaches that were thought-provoking. There were some who thought that we shouldn't be teaching objects first (you need to teach pointers and memory allocation) and some who suggested that Java was a toy language that is fine for teaching but not for serious development. Students and developers should learn about memory and they should learn more than one language and more than one approach. OOP in Java provides a solid introduction to programming. This series will continue to explore taking an extreme programming approach to this introductory course.

A group of responses felt that OOP is just a layer on top of procedural programming, and that method calls are, at their core, the same as procedure calls. In a future article, we'll focus more on the concept of "an object is at least a certain type." A polymorphic method call means that you are making a procedure call -- you're just making it without knowing which object or even which type of object will respond. At compile time, the object that you end up calling at runtime may not exist.

In This Series

Cooking with Java XP, Part 3
Here are more sample recipes from Java Extreme Programming Cookbook. From Chapter 8 ("JUnitPerf"), learn how to create a load test; and from Chapter 9 ("XDoclet"), find out how to execute a custom template.

Cooking with Java XP, Part 2
In these two recipes, excerpted from Chapter 6 of Java Extreme Programming Cookbook, learn how to create a mock implementation of an event listener interface, and how to avoid duplicated validation logic in your tests. And check back here next week for recipes on creating a load test and executing a custom template.

Cooking with Java XP
In this recipe from Chapter 5 of Java Extreme Programming Cookbook, learn how to configure your development environment to support test-first development with HttpUnit, JUnit, Tomcat, and Ant.

Top 12 Reasons to Write Unit Tests
Most programmers do not write tests. This is unfortunate, because testing improves software quality and design, reduces bugs, and provides accurate documentation. But if those reasons don't sway you, here are 12 more on the importance of writing tests, brought to you by Eric M. Burke and Brian M. Coyner, coauthors of Java Extreme Programming Cookbook.

Rethinking the Java Curriculum: Goodbye, HelloWorld!
How do we train students in the ways of object-oriented programming? Teaching them C first and then porting to Java is obviously the wrong thing to do. And HelloWorld gets very few OO points. In this series, Daniel Steinberg applies Extreme Programming concepts to the problem of teaching Java.

In this article, we'll start looking at a test-first approach to programming and to learning. In extreme programming you have a task you want to accomplish. Within that task, you choose a small measurable result. You think about how you would verfiy that you've accomplished that result. You write the test and then you write the code that will pass the test.

In this article, I'll use an ad hoc framework for this process. Next, I'll use JUnit, but I want to separate what's being done from the tools used to do it. Please join the discussion at the end of each article and feel free to suggest future topics in the forum or by emailing me at DSteinberg@core.com.

HelloWorld: Your First Java Program

Fundamentally, object-oriented programming is all about objects communicating with each other through their visible interfaces. You will learn to be stingy about what you expose to the outside world. Ideally, you will expose very little about the object's structure and only reveal those behaviors you are allowing others to invoke.

In a first programming assignment, I would like to see the newbie accomplish the following tasks:

As an example, consider the following two lines of code:

Friend friend = new Friend(); 
String yourName = friend.getName(); 
// somehow I will display yourName once I have it

Your first programming task is to write the code that makes those two lines work. Sure, this seems trivial to you now. You quickly whip up a file, Friend.java, that looks like this:

public class Friend { 
   public String getName(){

       return "Daniel";
   }
}

You compile it and somehow run the program and you expect to be greeted by name. Again, it's hard to remember back to when you didn't know anything. There's a lot here. I taught freshman calculus for a dozen years or so; on the last day of each semester I would write all that we had learned that semester on the board and step back. There, on a pair of blackboards at the front of the room, was everything we had covered over the past fifteen weeks. Looking back, it didn't look like much -- but it had taken us an entire semester and countless blackboards to get to this point. I'm asking you to go back to when you didn't know how to program and think about this assignment.

Learning Java

Related Reading

Learning Java
By Patrick Niemeyer, Jonathan Knudsen

Extreme Teaching

Now that you're remembering back to your early days, perhaps you used to type in -- I mean, perhaps you knew people who would type in -- long passages of a program and then save and try to compile it. They spent a lot of time debugging the program. Maybe they put print statements here or there and commented out sections that weren't working, rather than use an actual debugger. In any case, they spent a lot of time trying to track down what usually amounted to little typos.

In extreme programming, you take very small steps. The goal is for the code to always compile, and for all of the tests to pass. We might write a couple of lines of code and then compile and run all of the tests. If something goes wrong, we have a good idea where to look for our mistake. Here's a pseudo-code explanation of how we might proceed with the current program.

Running the Harness

First, before any tests are written, you should be able to run the test harness and get some indication that it is up and running. In this case, there will be an executable .jar file that you can run by double-clicking it. When it starts up, you will see this:


Testing For the Existence of the Friend Class

Go ahead and click the button. Until the student has successfully created the shell for a Friend class, they will see something like this:

Let's write a test to make sure we can create an object of type Friend. We'll do something like this:

public void testFriendExisits(){
   Friend friend = new Friend();
   checkExistenceOf(friend);
}

This test shouldn't compile, because we haven't created a Friend class yet. So we create a Friend class.

public class Friend(){

}

The code compiles and the test passes. Note that this exercise required that we do the following.

  1. Use a text editor to create the code for the class Friend.
  2. Save the code as Friend.java in a location in our classpath.
  3. Compile Friend.java using javac or another compiler.
  4. Run the test harness that checks the code.

This means that your two-line shell for the Friend class already serves as a system check, just like the classic version of HelloWorld, without bogging you down in a ton of details. You typed in Java code and you receive some sort of feedback that your work was successful.

Testing For the Signature of the getName() Method

Once a Friend class exists in the correct location, the student will see this feedback:

The next step is to write a test to make sure that the Friend class contains a method, getName(), that returns a String that is treated as the user's name. Our pseudo-code for the test might look like this:

public void testFriendReturnsName(){
   Friend friend = new Friend();
   checkThisMethodReturnsAString(friend.getName());
}

Now you can see the benefit of working step by step. Once the first test passes, we know that we can create an object of type Friend. Even though the test seems trivial, we always run it as part of our test suite. If, down the road, someone changes the source code and breaks this test, we'll immediately know what went wrong and we'll know what needs fixing. These tests protect us against future changes.

In this second test, we create a Friend object, invoke its getName() method and check that it returns a String. The instantiation is repeated code from the first step. When we look at JUnit, we'll use its facility for refactoring this common code to a single setUp() method. With this test method, the student's code can't compile if the signature of getName() isn't correct. This is a bit restrictive for a first assignment. In our actual harness code, you'll see that we test for a correctly-named method and a correct return type separately, and give helpful feedback in either case. The student can get the test to pass with code like this:

public class Friend { 
   public String getName(){
      return "Any old String you want. " +
         "It might be a name or it might be a meaningless sentence or two.";
   }
}

Testing the Actual Value Returned

At this point, everything is syntactically correct in the student code. You may have noticed so far that getting the code to compile led to the test being passed. We'll write one more test that is a bit far-fetched in this example, but will serve to make a point. Suppose that I know the student's name is Elena. I may want to test that the value that is returned by the getName() method matches "Elena." This test might look like this:

public void testFriendReturnsName(){
   Friend friend = new Friend();
   checkThisStringHasGivenValue(friend.getName(), "Elena");
}

This code immediately compiles, because the class and the method exist and the method returns a variable of the correct type. The test, however, will fail until the student changes the getName() method to this:

public String getName(){
   return "Elena";
}

Often, the purpose of unit tests is to test the return value of different methods in different situations. In the current example, this is a bit silly, and so our test harness won't do it. Once the signature of getName() is correct, our harness will display whatever the student has used as the value being returned. They will see a screen like this:


Summary

In our revised HelloWorld assignment, the students have had to read existing code and craft an appropriate response. From the start, the return type of a method has meaning to them, because they are required to use it. They have been required to create from scratch a class and a method that conform to someone else's specifications. From the start, they understand that they may be only writing part of an application and that they have to be able to understand, conform to, and later, possibly contribute to the public interface.

The Harness

Like all teachers, I'm sure that I learn more when I teach than my students do. In constructing this example, I needed to learn about how classes are loaded in Java. The original version of the test harness was a double-clickable .jar file that the students needed to quit and restart each time they made adjustments to the Friend class. I added the button and found that I didn't really understand ClassLoaders. I thank Malcolm Davis and Bill Venners for their helpful suggestions -- a lot of what I know about programming in Java is the result of ongoing conversations with Malcolm and Bill. I've told my editor that any mistakes with the code are theirs -- but I'm sure he knows better. Check out Bill's Web site, www.artima.com, for more information on the Java Virtual Machine and class loading. In this section, I'll present the code for the harness.

The Main Frame

As you saw in the figures, the application is a JFrame with a text area of some sort and a button. Here's the code for the JFrame.

import javax.swing.JFrame;
import java.awt.BorderLayout;

public class HelloFrame extends JFrame{

   private HelloFrame(){
      setUpLookOfFrame();
      setUpFramesComponents();
      setVisible(true);
   }

   private void setUpFramesComponents() {
      MessagePane reporter = new MessagePane();
      getContentPane().add(reporter,BorderLayout.CENTER);
      getContentPane().add(new TestButton(reporter),BorderLayout.SOUTH);
   }

   private void setUpLookOfFrame() {
      setTitle("Hello World");
      getContentPane().setLayout(new BorderLayout());
      setSize(350,330);
      setDefaultCloseOperation(EXIT_ON_CLOSE);
   }

   public static void main(String[] args){
      new HelloFrame();
   }
}

In setUpFramesComponents(), the text area and button are created and added to the frame. The rest of the code just tweaks the look of the application.

The Button

The button doesn't do very much. It sits around and waits to be pressed. When it is pressed, it tells the text area to figure out what message to display.

import javax.swing.JButton;
import java.awt.event.ActionListener;
import java.awt.event.ActionEvent;

public class TestButton extends JButton implements ActionListener {
   private MessagePane reporter;

   public TestButton(MessagePane reporter){
      super("Press here to check progress");
      addActionListener(this);
      this.reporter = reporter;
      setSelected(true);
   }
   public void actionPerformed(ActionEvent actionEvent){
      reporter.testProgress();
   }
}

The Text Area

The text area is a JEditorPane, so that HTML can be displayed. The testProgress() method contains a call to the custom class loader. There is an attempt to create an object of type Friend and then to invoke the getName() method. If the return type of getName() is a String, then we can go ahead and display the "congratulations" message. There are many places in this process where things could go wrong. For each case we can anticipate, we display an appropriate message. It is still possible that something we haven't anticipated will go wrong. We also display an appropriate message and provide a stack trace so that the instructor can try to figure out what went wrong.

import javax.swing.JEditorPane;

public class MessagePane extends JEditorPane{

   public MessagePane(){

      super("text/html","");
      welcomeUser();
   }

   public void testProgress(){

      try {
         Class friend = (new FriendLoader()).getFriend();
         String name = (String)friend.getMethod("getName",null).invoke
		               (friend.newInstance(),null);
         if (name == null){
            stringNotReturned();
         } else greetFriend(name);
      }
      catch (NoSuchMethodException e) {
         methodNotFound();
      }
      catch(ClassCastException e){
         stringNotReturned();
      }
      catch (NullPointerException e){
         classNotFound();
      }
      catch (Exception e){
         e.printStackTrace();
         unexpectedHappened();
      }
   }

   private void welcomeUser(){ //...
   }

   private void greetFriend(String name){ //...
   }

   private void classNotFound(){ //...
   }

   private void methodNotFound(){ //...
   }

   private void stringNotReturned(){ //...
   }

   private void unexpectedHappened(){ //...
   }
}

You can actually stop here and not create the class loader -- just replace the call to the class loader with a call to Class.forName(). Your application would be smaller, but it would require restarting it every time students want to check their work. This isn't a big deal, but you should probably get rid of the button and just call testProgress() in the constructor for HelloFrame.

The Class Loader

The only public method in the FriendLoader class is getFriend(). The getFriend() method calls the overridden findClass() method to try to find and load the Friend class. The key to creating your own class loader is in overriding this findClass() method. If you can find the class file, you then copy the byte code into an array that you pass, along with the class name, to the defineClass() method. Here's what this looks like:

import java.io.*;

public class FriendLoader extends ClassLoader {

   public Class getFriend() throws ClassNotFoundException {
      return findClass("Friend");
   }

   protected Class findClass(String className) throws ClassNotFoundException {
      byte classData[]= getFileData();
      if (classData == null) {
         throw new ClassNotFoundException();
      }
      return defineClass(className, classData,0,classData.length);
   }

   private byte[] getFileData()  {
      BufferedInputStream bufferedInputStream = getBufferedInputStream();
      ByteArrayOutputStream byteArrayOutputStream = 
	           getByteOutputFromFileInput(bufferedInputStream);
      return byteArrayOutputStream.toByteArray();
   }

   private BufferedInputStream getBufferedInputStream(){
      BufferedInputStream bufferedInputStream = null;
      try {
         File friend = new File("Friend.class");
         FileInputStream fileInputStream = 
		        new FileInputStream(friend.getPath());
         bufferedInputStream = new BufferedInputStream(fileInputStream);
      }
      catch (FileNotFoundException e){
         e.printStackTrace();
      }
      return bufferedInputStream;
   }

   private ByteArrayOutputStream getByteOutputFromFileInput
                (BufferedInputStream bufferedInputStream){
      ByteArrayOutputStream byteArrayOutputStream = 
	            new ByteArrayOutputStream();
      try {
         int c = bufferedInputStream.read();
         while(c != -1) {
            byteArrayOutputStream.write(c);
            c = bufferedInputStream.read();
         }
      }
      catch (IOException e){
         return null;
      }
      return byteArrayOutputStream;
   }
}

The Challenge

As a next step, I'd like the students to write to an interface. In this case, I don't care what they call their class; in fact, I don't care how they store their name information. This will, of course, require some changes to the harness. All of this is a topic for another day. For now, if your goal is to teach object-oriented programming and you plan to use Java as your language in an introductory course, how might you change or extend this proposed initial assignment? Next month, we'll continue to look at the test first approach to programming.

Daniel H. Steinberg is the editor for the new series of Mac Developer titles for the Pragmatic Programmers. He writes feature articles for Apple's ADC web site and is a regular contributor to Mac Devcenter. He has presented at Apple's Worldwide Developer Conference, MacWorld, MacHack and other Mac developer conferences.


Return to ONJava.com.

Copyright © 2009 O'Reilly Media, Inc.