Chapter:Interfaces

From Juneday education
Jump to: navigation, search

This page is obsoleted (not maintained) and replaced by:


Meta information about this chapter

Expand using link to the right to see the full content.

Introduction

Interfaces are used a lot since it is a lightweight inheritance. Using interfaces you can, a bit simplified, get the benefits from inheritance (using extends) and not get the downsides. This chapter introduces interfaces. Interfaces also allow you to write abstraction of unrelated classes.

Purpose

Make the student familiar with the interface concept. Also introduce and learn how to use some of the interfaces in the Java API.

Goal

The student shall understand:

  • what an interface is
  • how to implement existing interfaces
  • how to write and implement own interfaces

This is an introductory book. For this reason, this chapter doesn't cover the theory behind choosing between an interface and abstract classes, or even choosing between interfaces and inheritance using "extends". That topic is left for another book. Interfaces are commonplace in Java's API and in Java projects, however, so knowing what an interface is and how it works is something that the student should be prepared for. This is what this chapter aims to do.

Instructions to the teacher

Common problems

Students may get confused about when to use interfaces and why. A typical question could be "How should I know when to use interfaces, and when to use normal inheritance?". This question is not trivial to answer. From a language technical perspective, the simple answer would be that a class can only extend one other class, but implement many interfaces. So if a class already extends some class, it is kind of stuck in an inheritance hierarchy. But such a class can take on another form (another type) using the "implements SomeInterface" syntax. But starting a design from scratch is a whole lot more complex and complicated.

Refer to the existing interfaces of the Java API such as Comparable and Comparator, List, Collection etc in order to give some real life examples of real uses of interfaces. Explain to the student that making design decisions and designing class relations is something that takes a lot of experience. This introductory book simply goes through the semantics and syntax of interfaces and gives a few examples of how interfaces could be used.

Videos

All videos in this chapter:

See below for individual links to the videos.

Introduction to interfaces

Description

Sometimes we have a need to express some common capabilities of objects from otherwise totally unrelated classes. Often the need is to be able to speak about the objects in a highly abstract way, focusing solely on their capabilities, what they can do, i.e. what methods they offer to us.

A good example is to be found in the standard Java API where the authors found it good to express one such capability - the capability of Objects to compare themselves with other objects of the same type. The authors created an interface to express this capability - Comparable. An interface is like a super-abstract class which focuses on methods. The Comparable interface (in java.lang) declares only one abstract method, compareTo. A class which wants to signal that its objects have this capability, can be declared using the keyword implements and the name of the interface and the class name again:.

public class Member implements Comparable<Member>{
  // instance methods, constructors
  @Override
  public int compareTo(Member other){
    // code which compares this object with the other object
    // and returns an int as the result
    // 0 means they are to be considered equal
    // a negative value means that this object is less than the other
    // a positive value means that this object is greater than the other
  }
}

Quite a few of the classes in the API (some 57 classes or so) are declared in this way. A telling example is the class java.lang.String. String is declared as implements Comparable<String> (and some other interfaces actually), so String objects can be asked to compare themselves with other String objects. The compareTo() implementation in String returns value comparing the contents of the strings lexicographically which is a lot like alphabetical comparisons. This allows us to sort collections of Strings according to the spelling.

The choice of the name for this interface also allows us to talk about Strings in a way which is close to how we speak in plain English. We say that String objects "are comparable".

This turns out to be very useful, because this means that the interface Comparable<SomeClass> works as a promise about objects which have this type. All objects of a class which implements the Comparable interface will have a functioning compareTo() method implementation! This fact is used by some static methods for sorting lists and arrays. In order to sort the elements of an array, we need to compare all the elements to each other in some way, which is where the compareTo method comes in play. The authors of such a sorting method can use the Comparable interface as a contract, saying I can sort arrays if the elements are Comparable (that is, have the compareTo() method). The clients to the sorting method will have to fulfill their part of the contract and implement the compareTo() method.

The thing which is so good about this, is that using the abstraction of "being comparable", the authors of the sorting method don't have to have any other information about the elements in the array to sort, other than that the elements have a compareTo() method! So a good use of interfaces is that it allows us to agree on some capabilities of a group of objects, without caring at all about all the other information about these objects.

Another good thing about this, is that it is very easy for us to fulfill the contract of "being comparable". We simply have to read the documentation of the Comparable interface, and see what methods do we have to implement. In this case, only one method, the compareTo() method. So we can very easily create totally new classes which implement Comparable and they will work just fine with the sorting method which was written long before we even came up with the idea for our classes.

Another useful thing about interfaces, is that we can implement more than one interface, while we can only extend one class at the time using inheritance. Interfaces don't have many of the problems of classes using inheritance, because the interfaces work on a very abstract layer. They only list what methods must exist in classes implementing the interfaces. There is no hassle with constructors and initialization of objects (which we've seen exist using inheritance), because interfaces are not used for creating new objects. They are only meant to be used as a reference type based on behavior (what methods must exist).

Videos

  1. What is an interface? (eng) (sv) (download presentation)

Exercises

Exercises for "what is an interface" - should focus on the basics like what interfaces exist in the API, what classes in the API implements some interfaces? Etc.

1. What class does String extend?

Hint: Look at the API specification for String.

2. What interfaces does String implement?

Hint: Look at the API specification for String.

3. What interfaces does File implement?

Hint: Look at the API specification for File.

4. What methods does the AudioClip specify?

Hint: Look at the API specification for AudioClip.

Solutions

Expand using link to the right to see the full content.

1. The String class in Java extends Object.

2. The String class in Java implements the following interfaces:

  • Serializable
  • CharSequence
  • Comparable<String>

3. The File class in Java implements the following interfaces:

  • Serializable
  • Comparable<File>

4. The AudioClip class in Java specifies the following methods:

  • void loop() - Starts playing this audio clip in a loop.
  • void play() - Starts playing this audio clip.
  • void stop() - Stops playing this audio clip.

Implementing an interface

Description

If we want to create a class, SomeClass, whose objects are comparable to each other, we use the keyword implements followed by Comparable<SomeClass>:

public class SomeClass implements Comparable<SomeClass>{
  // we now need to implement the compareTo(SomeClass o) method!
}

How would we know how to implement the compareTo() method, and whether we have to implement some more methods? Easy! We have to read the documentation for Comparable. It lists the abstract methods we need to implement (in this case, only one, luckily for us!) and what these methods should do. If we read about this only method, compareTo(), we'll see that it should return an int representing the result of the comparison. A negative value represents that the object is less than the other object (given as argument), a value of 0 means they are considered equal and a positive value means that the object is greater than the object given as argument.

Let's exemplify this using Strings (which we know are comparable, because reading the documentation for String tells us that String implements the Comparable<String> interface, and thus has a functioning implementation of compareTo(String o) ).

    String s =     "Ape";
    System.out.println(s.compareTo("Zebra"));  // -25
    System.out.println(s.compareTo("Ape"));    //   0
    System.out.println(s.compareTo("Abraham"));//  14

The comments show what would be printed if we ran this snippet of code (in a main method for instance). A negative number for the comparison of "Ape" to "Zebra" (because "Ape" is considered less than "Zebra" lexicographically), a 0 for the comparison of "Ape" to "Ape" (because they are equal) and a positive number for the comparison of "Ape" to "Abraham" (because lexicographically, "Ape" is greater than "Abraham" because of the second character).

So, in order to implement the Comparable<SomeClass> interface, we must write an instance method compareTo(SomeClass o) where we compare this object with the object referred to by o which we get as a parameter. Then we must return an int representing the result of the comparison - if this object and o are equal (in terms of comparisons) we should return 0. If this object is less than the object referred to by o, we should return a negative number. Otherwise we should return a positive number.

What the negative and positive numbers are doesn't matter. It is just the sign which is important.

Now, we can actually ask some method to sort an array (or list) of SomeClass references. The sorting method will use the fact that SomeClass objects are comparable, and call the compareTo method on each one of them in order to find out how to sort the array (or list). One such method we could use is the static method sort() in the java.util.Arrays class, which exists in a version which accepts an array reference. Note that the sort method would crash if we sent it an array whose elements are not Comparable.

What if we want to sort an array whose elements are not comparable? Maybe the elements are of some class which we cannot change into being Comparable (because we don't have access to the source code for instance). Don't worry! There is another version of sort in Arrays (and the similar class for lists, Lists) which take two arguments. This version takes as the first argument the reference to the array to be sorted, and as the second argument an object which indeed can compare two objects of the array element type.

How have they implemented this? Well, again with the help of interfaces (admit that you start to like interfaces!). The comparator object is of type Comparator which is an interface in the java.util package. The Comparator<T> interface has a method which you need to implement in order to claim that some class is a Comparator: public int compare(T first, T second).

As you see, a comparator object can compare two objects of some type. This means that we can retrofit comparability to a class using a comparator helper object.

Let's say that we have a class Member which is not Comparable. But we want to be able to sort an array of Member references. Now, we can create a Comparator by writing a new class MemberComparator implements Comparator<Member>. This class must implement the method int compare(Member first, Member second) and return an int as if we compared first to second.

Again, if first is less than second, we should return a negative value. If they are equal we should return 0 and otherwise return a positive number. Using this class, we can create a MemberComparator object and use that as the second argument to the version of sort() which took two arguments.

public class MemberComparator implements java.util.Comparator<Member>{
  @Override
  public int compare(Member first, Member other){
    return... // code which produces an int according to the rules...
  }
}

The call to sort would then be something like Arrays.sort(memberArray, new MemberComparator());


  • TODO: Anonymous inner/local class for a comparator

Exercises on implementing interfaces

We are going to look into some of the existing interfaces in Java. We'll start off with looking into how Java can help you sort objects. First we're looking at how Java sorts String objects and continue with sorting Member objects to learn about the beauty of interfaces and how relatively easy it is to write general methods and classes. One example is the sort function you get with the Arrays class. The method is able sort an array of objects of any type as long as all the objects in the array has the same type. As an example sort can sort String objects, Member objects and classes you'll create in the future. All you need to do is to help the sort method by helping sort to tell the order of two objects.

1. Let's start out with sorting String objects. Crete a class, Sorter, in the package net.sortex.strings. Write a main method that sorts the Strings passed as arguments to the program (the main method of the class). The sorting should be done using Arrays.sort(). Print the String objects after you've sorted them.

Hint: Arrays has a method called toString which you can pass an array of Object references (e g array of String refereces).

2. How could Java possibly know how to sort the array?

Hint: When you're sorting things in the real world you usually, or at least sometimes, compare two objects to see which one is biggest or they're of the same size. Java does the same thing. In the case of String objects, the Comparable interface is used.

3. Create a simple class, Book:

  • with the following instance variables:
    • private String name
  • a public constructor with which you can set the name of the book
  • a public method that returns name
  • which is in the package net.sortex.books

4. Add a test class TestBook in the package net.sortex.books.test. Add a main method to the class and create 4 Book instances and stores them in an Array. Sort the Book instances using the method sort in Arrays.

Compile and try to execute. ... Uh oh... didn't quite work. Why? Read the error message the compiler printed.

Note: you don't need the Sorter class anymore.

5. Add a method int compareTo(Object anotherBook) to the Book class. The actual implementation of the method is not important at the moment. Just make sure you return an int value.

Hint: compareTo is a method that is used by Arrays.sort() and Collections.sort() when sorting lists.

Compile.

Still can't sort :(

6. State that your class is implementing the Comparable interface.

Hard to see if it worked, isn't it?

7. Write a method in the Book class that:

  • can return a String representation of a Book object
  • is automatically called by for example System.out.println

Hint: implement public String toString().

Ok, it works kind of good now. So the method sort in Arrays can sort objects for you, as long as you define a method (compareTo) that sort can use to compare two objects. That's all you do and the already written method sort does the actual sorting. Nice, isn't it. But how does sort do the sorting?

Are we happy now? No, let's add some more stuff to Book.

8. Add, to Book, the instance variable

  • int year

This means you need to rewrite your constructor with two additional parameters. You also "may" need to update the toString method.

9. Imagine we want to sort Books according to this order:

  • name
  • year (if the same name)

Add code to the compareTo method to reflect this.

But imagine now that we would like to sort Books in order of year of publication and sometimes in order of name. This is not by any means a strange of uncommon requirement. In this case we need to use a better interface, Comparator. More on this in the coming exercises.

10.

Let's add some more Books. Here's some book you can copy/paste if you want:

        Book[] books = { new Book("ABC ... and more", 1980),
                         new Book("A book", 1980),
                         new Book("A book-3", 1982),
                         new Book("A book-2", 1982),
                         new Book("Book again", 1719),
                         new Book("Book again", 1898),
                         new Book("Book again", 2013),
                         new Book("Book again - 3", 1984),
                         new Book("Book again - 2", 1985),
                         new Book("Another book", 1990)
        };

Compile and execute to make sure it works.

11.

Write a new class, BookYearComparator, that implements Comparator.

This means you need to:

  • import Comparator
  • explicitly state you're implementing Comparator
  • choose the type (hint: if writing a Comparator for Member objects, you use Comparator<Member>... what type, instead of Member, do you think you should specify if you want to compare Book objects?).
  • implement the compare methos

The method compare should compare the year in two Book objects.

12. Extend your test class' main method to print out the Books after sorting using the BookYearComparator.

13. Note that our books now can be sorted either after name or year. ... but what about if two Books have the same year? Now we should change our compare method so that it compares the years and if these are the same it should compare the names.

Hint: when the years of the two objects are the same you can compare the names using the compareTo method.

Compile and check if it seem to work.


Solutions

Expand using link to the right to see the full content.


You can find complete source code to the suggested solutions below in the implementing-interfaces directory in this zip file or in the git repository.


1.

package net.sortex.strings;

import java.util.Arrays;

public class Sorter {

    public static void main(String[] args) {

        Arrays.sort(args);
        System.out.println(Arrays.toString(args));
        
    }
    
}


2. The sort method in the Arrays class sorts an array of Objects if it implements the Comparable interface. To implement this interface a class has to provide an implementation of the method int compareTo(T o), which in the case of String objects translates to int compareTo(String anotherString). If the class implements that method Java can easily sort the objects by simply asking them two-by-two which one is biggest.

Compare this to sorting Students in a class. You have no idea what criteria to use to sort (name, length, age...) but you can still sort the students in order if you had a chance to ask the students two-by-two which one is biggest. This is the way Java does it. By implementing the compareTo interface we can rest assured that Arrays.sort can do its job.

3. Suggested solution:

package net.sortex.books;

import java.util.Arrays;

public class Book {

    private String name;

    public Book(String name) {
        this.name = name;
    }
          
    public String name() {
        return name;
    }
    
}


4. Java (in runtime) cannot find a method to use to compare two Book instances so the class can't be executed properly.


5.

Exmaple of method implementation

    public int compareTo(Book anotherBook) {
        return this.name.compareTo(anotherBook.name());
    }

Java still can't sort the Array. It's because we're not stating that we implement the Comparable interface.


6. Suggested solution:

public class Book implements Comparable<Book> {


7. Suggested solution:

package net.sortex.books.test;

import net.sortex.books.Book;

import java.util.ArrayList;
import java.util.Collections;
import java.util.Arrays;

public class BookTest {

    public static void main(String[] args) {
        Book[] books = { new Book("ABC ... and more", 1980),
                         new Book("A book", 1980),
                         new Book("A book", 1981),
                         new Book("Book again", 1984),
                         new Book("Another book", 1990)
        };

        Arrays.sort(books);
        System.out.println(Arrays.toString(books));        
    }

}
package net.sortex.books;

import java.util.Arrays;

public class Book implements Comparable {

    private String name;

    public Book(String name) {
        this.name = name;
    }
          
    public String name() {
        return name;
    }

    public int compareTo(Object anotherBook) {
        return this.name.compareTo(((Book)anotherBook).name());
    }

    public String toString() {
        return name;
    }
    
}

sort can sort objects just as you can sort elements of any kind if you were allowed to check the order of two objects.

8. See next suggested solution

9. Suggested solution:

package net.sortex.books;

import java.util.Arrays;

public class Book implements Comparable<Book>  {

    private String name;
    private int    year;

    private static final String SEPARATOR = ",";

    public Book(String name, int year) {
        this.name   = name;
        this.year   = year;
    }
          
    public String name() {
        return name;
    }

    public int year() {
        return year;
    }

    public int compareTo(Book anotherBook) {
        // We shall now compare the names (both String objects).                      
        // We do this by using using String's own compareTo                           
        //                                                                            
        // this.name is a String, so we invoke compareTo on anotherBook's name        
        int nameCheck = this.name.compareTo(anotherBook.name());
        if (nameCheck==0) {
            // If the names are the same, check year                                  
            return this.year - anotherBook.year();
            /*                                                                        
             * Perhaps the above code is easier understood if presented this way:     
             *                                                                        
             * Get the two books' years and store them in separate variables:         
             *   int year        = this.year;                                         
             *   int anotherYear = anotherBook.year();                                
             *                                                                        
             * Ok, we have the two year variables. The                                
             *   return year - anotherYear;                                           
             *                                                                        
             */
        } else {
            return nameCheck;
        }
    }
    
    public String toString() {
        return "(" + name + SEPARATOR + year + ")\n";
    }
    
}

10. No solutions needed.

11. Suggested solution:

package net.sortex.books;

import java.util.Comparator;

public class BookYearComparator implements Comparator<Book> {

    public int compare(Book one, Book two) {
        return one.year()-two.year();
    }

}

Note: we don't need to import Book since Book and BookYearComparator are in the same package.


12. Suggested solution:

    public static void main(String[] args) {
        Book[] books = { new Book("ABC ... and more", 1980),
                         new Book("A book", 1980),
                         new Book("A book-3", 1982),
                         new Book("A book-2", 1982),
                         new Book("Book again", 1719),
                         new Book("Book again", 1898),
                         new Book("Book again", 2013),
                         new Book("Book again - 3", 1984),
                         new Book("Book again - 2", 1985),
                         new Book("Another book", 1990)
        };

        System.out.println("Unsorted");
        System.out.println(Arrays.toString(books));

        System.out.println("Sorted - using names and then year");
        Arrays.sort(books);
        System.out.println(Arrays.toString(books));

        System.out.println("Sorted using BookYearComparator - using year and then names");
        Arrays.sort(books, new BookYearComparator());
        System.out.println(Arrays.toString(books));



    }

13. Suggested solution:

package net.sortex.books;

import java.util.Comparator;

public class BookYearComparator implements Comparator<Book> {

    public int compare(Book one, Book two) {
        if (one.year()==two.year()) {
            // Ok, same year.  How do check the names? .. why don't                                                         
            // use the compareTo method we've already written :)                                                            
            return one.compareTo(two);
        }
        return one.year()-two.year();
    }

}


Videos

  1. The Comparable interface (eng) (sv) (download presentation)
  2. The Comparator interface pt 1 (eng) (sv) (download presentation)
  3. The Comparator interface pt 2 (eng) (sv) (download presentation)

Writing your own interfaces

Description

Sometimes we need to write our own interfaces in order to solve some problem. In this lecture we show you how you can write a simple interface in order to make an abstraction which creates less dependencies between different parts of the code in a media player project.

The idea in this example is that a media player should be able to focus on objects which can be played. How the actual playing of a media file is implemented, shouldn't be the problem of the media player part of the system.

An interface is used to create this abstraction. The interface we create is called Playable and defines only one abstract method play(). All classes which implement this interface must then implement the play() method. In return, objects from classes implementing the Playable interface can now be viewed as playable objects, since they have an instance method which knows how to play them.

The media player can now accept a reference to some Playable object and call the play() method on the object. This means that the media player doesn't have to care about the objects' real classes, but rather focus on the fact that they have a play() method!

In order to create some drama (just kidding), we have decided to design our classes for two types of media files, AudioFile and VideoFile using a common base class File. The File class in this example is pretty simple and just keeps the name of the file. Now, both AudioFile and VideoFile extend File. This means that they are first of all representations of special types of files. But we want them to also be playable and here comes the interface to the rescue.

The classes AudioFile and VideoFile both extend File but also implement Playable. Implementing Playable means that they must provide implementation for the play() method. This means that our design has put the responsibility of playing in the classes, so that the objects know how to play themselves, so to speak.

The test class we have written creates an array which can hold references to Playable objects (objects of some class which implement Playable). We populate the array with references to a mix of objects, some audio files and some video files. Both AudioFile and VideoFile implement Playable, so this is legal and fine.

Next, the test class loops through the array and calls the media player (a simple method which calls play() on the argument which is of type Playable) which each reference in the array as the argument.

The playFile(Playable) method (which simulates our media player) accepts a reference to a Playable as the single argument, and calls play() on the reference. Our hope is that this shows that the media player is very general and can be called with any type of playable file or object. It doesn't care about the actual class of the object to be played, it simply focuses on the fact that a Playable object knows how to play itself via its play() method.

In fact, the media player is future proof in the sense that if we create a new class for a new type of playable object, it will still work as long as that object implements the play() method and is of a class implementing the Playable interface.

What if we in the future decide, for instance, that it would be cool to play a stream from a network service? Then we could actually create a class MediaStream which implements Playable. The play() method in that class would create a connection to some service on the internet and stream and play from that connection. Could we send a MediaStream object to the media player? Yes, of course! Since it knows how to play itself (and since the media player doesn't really care how this is done) it will happily accept the MediaStream object and view it as a Playable in general, and call the play() method on it.

The good thing about that, is that we now have a system which can handle totally different types of objects (files and network streams) as long as they share some capabilities. The capability is to have a functional play() method and to be able to be viewed as being of type Playable. The interface Playable fixes that for us, so all we have to do is to write classes which implement that interface and offers a good version of the play() method.

Videos

  1. Writing your own interface (eng) (sv) (download presentation) (source code)

Exercises on writing your own interface

Exercises on writing a small interface and some classes which implements that interface.

We should now write a MediaPlayer - not a very fancy one. To do this we start by writing a new interface for files that are, in some way, Playable. Since we started, in the previous chapter on inheritance, to use FBFile etc we will stick to using that so let's call our interface FBPlayable.

What do we expect from a playable file? Well, since we call the interface FBPlayable we can expect it to be able to play itself ... so a method called play() sounds reasonable.

Before we start we want you to do any of the following:

  • copy the files from the previous section to a new directory
  • continue editing the files in the same directory as you used in the previous section

Ok, we're ready to start. Let's do some basics first - the interface and some classes.

1. Create an interface FBPlayable. It can use the same package as FBFile. The interface should specify the method play() as:

  • public
  • return type shall be void
  • have no parameters

2. Add a class FBMp3. This class should:

  • extend FBFile, since we want it to be a file
  • implement FBPlayable, since we want it to be able to "behave" as a Playable. The implementation of the play method can be done by simply outputting some words about the file being played.... it would be better to actually play the file but for now we're happy with this solution.
  • have a constructor with one parameter (of type String)

3. Add a class FBOgg in a similar way you did with the FBMp3 class.

4. In the class FileList you need to add code to handle FBPlayable. So, add the following methods to the class FileList:

    public static List<FBPlayable> listPlayable() {
        return listPlayable(".");
    }
    
    public static List<FBPlayable> listPlayable(String dir) {
        return listPlayable(new File(dir));
    }
    
    public static List<FBPlayable> listPlayable(File dir) {
        List<FBFile>     files = list(dir);
        List<FBPlayable> list  = new ArrayList<FBPlayable>();

        for (FBFile f : files) {
            if (f instanceof FBPlayable ) {
                list.add((FBPlayable)f);
            }
        }
        
        return list;
    }

Change the following method in the class FileList to look like this.

    public static List<FBFile> list(File dir) {

        if ( (dir==null) || dir.isFile() ) {
            return null;
        }
        
        // By returning a List we can chose any the subclasses (in
        // this case actually sub interfaces - more on that in the
        // coming chapter) and change it afterwards, since the users
        // of this method only uses the methods as "provided" by List
        List<FBFile> list = new ArrayList<FBFile>();
        
        // Note that we use File below. File is a class in Java.
        File[] files = dir.listFiles();
        if (files==null) {
            System.err.println("Woooops, list files returned null");
            System.err.println(" * dir: " + dir);
        }
        for (File file : files) {
            if (file.isFile()) {
                if (file.getPath().endsWith(".java")) {
                    list.add(new FBJavaFile(file.getPath()));
                } else if (file.getPath().endsWith(".mp3")) {
                    list.add(new FBMp3(file.getPath()));
                } else if (file.getPath().endsWith(".ogg")) {
                    list.add(new FBOgg(file.getPath()));
                } else {
                    list.add(new FBFile(file.getPath()));
                }
            } else if (file.isDirectory()) {
                list.add(new FBDir(file.getPath()));
            }
        }
        return list;
    }

Note: we check the suffix of the files and create objects using that information.

5. Ok, let's make sure we can list files as in the previous section. But first let's make it possible to pass a directory to the TextMain class. Pass this directory to the FileList's list method.

6. let's write a new class, MediaPlayer in the same package as TextMain. This class should have a main method that:

  • checks if the users supplied a directory (see previous exercise) and uses that when listing files
  • asks FileList for a list of FBPlayable objects.
  • loops through the objects and plays them one by one (using the play method).


Solutions

Expand using link to the right to see the full content.


You can find complete source code to the suggested solutions below in the writing-your-own-interfaces directory in this zip file or in the git repository.


1. Suggested solution:

package com.superpower.filebrowser;

public interface FBPlayable  {

    public void play();

}

2. Suggested solution:

package com.superpower.filebrowser;

public class FBMp3 extends FBFile implements FBPlayable {

    public FBMp3(String name) {
        super(name);
    }

    public String thumbnail() {
        return "[mp3 ]";
    }

    public void play() {
        System.out.println("faked playing of file: " + this);
    }
    
}


3. Suggested solution:

package com.superpower.filebrowser;

public class FBOgg extends FBFile implements FBPlayable {

    public FBOgg(String name) {
        super(name);
    }

    public String thumbnail() {
        return "[ogg ]";
    }

    public void play() {
        System.out.println("faked playing of file: " + this);
    }
    
}

4. Suggested solution:

We're listing the entire FileList class here to make it a bit easier if you want to copy/paste.

package com.superpower.filebrowser.utils;

import java.util.List;
import java.util.ArrayList;
import java.io.File;

import com.superpower.filebrowser.*;


public class FileList {

    public static List<FBFile> list() {
        return list(".");
    }

    public static List<FBFile> list(String dir) {
        return list(new File(dir));
    }

    public static List<FBPlayable> listPlayable() {
        return listPlayable(".");
    }
    
    public static List<FBPlayable> listPlayable(String dir) {
        return listPlayable(new File(dir));
    }
    
    public static List<FBPlayable> listPlayable(File dir) {
        List<FBFile>     files = list(dir);
        List<FBPlayable> list  = new ArrayList<FBPlayable>();

        for (FBFile f : files) {
            if (f instanceof FBPlayable ) {
                list.add((FBPlayable)f);
            }
        }
        
        return list;
    }
    
    public static List<FBFile> list(File dir) {

        if ( (dir==null) || dir.isFile() ) {
            return null;
        }
        
        // By returning a List we can chose any the subclasses (in
        // this case actually sub interfaces - more on that in the
        // coming chapter) and change it afterwards, since the users
        // of this method only uses the methods as "provided" by List
        List<FBFile> list = new ArrayList<FBFile>();
        
        // Note that we use File below. File is a class in Java.
        File[] files = dir.listFiles();
        if (files==null) {
            System.err.println("Woooops, list files returned null");
            System.err.println(" * dir: " + dir);
        }
        for (File file : files) {
            if (file.isFile()) {
                if (file.getPath().endsWith(".java")) {
                    list.add(new FBJavaFile(file.getPath()));
                } else if (file.getPath().endsWith(".mp3")) {
                    list.add(new FBMp3(file.getPath()));
                } else if (file.getPath().endsWith(".ogg")) {
                list.add(new FBOgg(file.getPath())); 
                } else {
                    list.add(new FBFile(file.getPath()));
                }
            } else if (file.isDirectory()) {
                list.add(new FBDir(file.getPath()));
            }
        }
        return list;
    }

}


5. Suggested solution:

    public static void main(String[] args) {

        String dir = ".";
        if ( args.length!=0) {
            dir = args[0];
        }
        
        List<FBFile> files = FileList.list(dir);

        for (FBFile f : files) {
            System.out.println(f.thumbnail() + " " + f.name());
        }
        
    }


6. Suggested solution:

package com.superpower.filebrowser.main;

import java.util.List;

import com.superpower.filebrowser.FBPlayable;

public class MediaPlayer {

    public static void main(String[] args) {

        String dir = ".";
        if ( args.length!=0) {
            dir = args[0];
        }
        
        System.out.println("Playing in " + dir + ":");
        for (FBPlayable p : FileList.listPlayable(dir)) {
            p.play();
        }
        
    }

}


Program against an interface

Description

A principle often recommended is to "program against an interface". In order to give an example of what that means, we'll start by telling you that java.util.List is an interface. This interface defines what we can do with different kinds of lists, such as ArrayList. Since ArrayList is to be considered "a List" ArrayList implements the List interface.

This means, that we can declare a list of String references as being of type List<String>, rather than being specific about what exact type of list we are using:

List<String> myStrings = new ArrayList<>();

From this point, we can only treat the list referenced by myStrings as a general List<String> as defined in the interface List. We limit ourselves to the methods defined in the interface List. Isn't this a drawback? No, it's a feature! Since we limit ourselves to only use the methods declared in the List interface, it is perfectly safe for us to change our mind at a later stage, and initialize myStrings to some other type of concrete list (such as LinkedList). Since both ArrayList and LinkedList implement the List interface, they share at least the methods declared in the List interface! So everything we can do with the myStrings reference, we can do regardless of the actual list is an ArrayList or a LinkedList.

In a real program, the intialization would typically come from some method, rather than an explicit call to the constructor of e.g. ArrayList. Let's pretend that we get our list object from a method called createList(). As long as the method createList() returns an object of type List<String>, we don't have to worry about what particular type of list we get. We'll just treat the list as a list as defined in the List interface:

List<String> myStrings = createList();
// We can now only use the instance methods declared in the List interface!

Now, the createList() method will create and return a reference to some real type of list, perhaps an ArrayList<String>. But the method will return this object as being of type List<String>, which will hide from us what the actual type is. This is actually a good thing, because we don't have to worry about the details! We'll just treat the object returned to us a some kind of List<String>. Why is it a good thing? Because the authors of the createList() method might change their mind for some reason, and return instead an object of class LinkedList<String> but still return it as of type List<String>. But for us, this doesn't matter, which is the good thing! We know that we get an object of the more general type List<String>, so our code works with any such object. So the authors of the createList() method can change the actual object as much they like (as long as they stick to the contract of returning something of the general type List<String>.

Some of the methods of the java.util.List interface include:

  • boolean add(E e) - adds an element of the list type to the list
  • E get(int index) - gets the element at index (as the type of the list elements)
  • boolean isEmpty() - returns true if the list is empty, otherwise false
  • int size() - returns the number of elements in the list

These (and the rest of the methods declared in the List interface) are methods we can call on every object whose class implements the List interface. As long as we stick to these methods, we're safe if we or someone else change their mind and chooses an alternative actual class for our list.

Videos

This section of the chapter is just for reading. We include it in order to be more complete.

Creating an anonymous class

Description

For interfaces which declare only one or a few methods, it is actually common to create an object of a class implementing the interface "on the fly" without bothering to create a class declaration with a class name etc.

Let's say that we want to create a Comparator<String> for comparing two String objects without caring about the case of the text. We want to be able to compare two String objects as if they were both only lower case, for instance. So, we'd like to be able to compare e.g. "ABBA" with "abba" and get 0 (equality), or "ABBA" with "baba" and get a negative number, because disregarding the case "abba" is less than "baba".

We'll just need to create a class which implements Comparator<String> and override the compare(String, String) method to reflect this behavior.

The thing we could do here is to skip the step of actually defining such a class in a source code file with a class name and all. We could instead actually use the syntax for creating a nameless (anonymous) class. With this syntax we create an object and define its type in one long statement. In order to do this, we use the following syntax:

new InterfaceName(){
  methodToOveride(){
    // code for the method
  }
};

This syntax also works for creating anonymous classes which uses inheritance. The following would create a new Object of a type which extends Object and overrides toString():

Object o = new Object(){
  public String toString(){
    return "We are anonymous. Fear us.";
  }
};
System.out.println(o); // would print the text returned from toString above

So, going back to our String comparator which doesn't care about case. This code would sort a list of Strings without regard for the case of the Strings in the list:

import java.util.Comparator;
import java.util.ArrayList;
import java.util.Collections;
public class TestAnonymous{
  public static void main(String[] args){
    ArrayList<String> strings = new ArrayList<>();
    strings.add("CEPA");
    strings.add("bepa");
    strings.add("APA");
    strings.add("ABBA");
    strings.add("DEPA");
    strings.add("Baba");
    System.out.println("Unsorted: ");
    System.out.println(strings);
    Collections.sort(strings, new Comparator<String>(){
        @Override
        public int compare(String first, String other){
          return first.toLowerCase().compareTo(other.toLowerCase());
        }
      });
    System.out.println("Sorted disregarding case:");
    System.out.println(strings);
    Collections.sort(strings);
    System.out.println("Sorted using String's default compareTo:");
    System.out.println(strings);
  }
}

A test run of the program would print:

$ javac TestAnonymous.java && java TestAnonymous
Unsorted: 
[CEPA, bepa, APA, ABBA, DEPA, Baba]
Sorted disregarding case:
[ABBA, APA, Baba, bepa, CEPA, DEPA]
Sorted using String's default compareTo:
[ABBA, APA, Baba, CEPA, DEPA, bepa]

Videos

This section of the chapter is just for reading. We include it in order to be more complete.

Some rules about interfaces

Description

General rules

Methods declared in an interface are abstract by default, meaning that even if you don't add the abstract keyword, they will be interpreted as abstract. This means that you have to implement all methods declared in an interface if your class says it implements some interface(s), or else you would have to declare your class as abstract and leave implementation to classes extending your class.

Interfaces differ from abstract classes in that interfaces do not declare constructors, and they cannot deal with state. Instance variables are for this reason something that cannot exist in an interface. Abstract classes work pretty much like ordinary classes with the addition of being able to declare abstract methods. Another important difference from abstract classes, is of course that you cannot extend more than one class (which could be abstract or not), but you can implement many interfaces.

Default methods

In Java version 8, default methods were introduced. In short, a default method in an interface uses the keyword default and allows the author to write a method with a concrete implementation. The main use for this is to allow for methods to be added to an interface without breaking existing implementing classes. Before Java 8, adding a method to an interface would break all implementing classes, since they would lack the implementation for the new method.

Extending an interface

Extending an interface using inheritance with the keyword extends would work fine but produce a new interface type. This it typically used to add a new subtype which declares a few more methods.

The only thing which can be overridden (in the sense of the word we use when talking about inheritance and classes) would be default methods (which were introduced in Java 8). If you don't override any default methods from the super interface, you will inherit the default implementations from the super interface. If you redifine the default methods as abstract methods, they will "forget" their default implementations from the super interface and become normal abstract methods as any other abstract methods in an interface.

If you are not using Java 8, you don't have default methods, so you cannot try this without installing Java 8 or later.

Videos

This section of the chapter is just for reading. We include it in order to be more complete.

Links

External links

Books this chapter is a part of

Programming with Java book

Book TOC | previous chapter | next chapter