Hi! In this lesson, we'll talk about how abstract classes differ from interfaces and consider some examples with common abstract classes. The difference between abstract classes and interfaces - 1We've devoted a separate lesson to the differences between an abstract class and an interface, because this topic is very important. You'll be asked about the difference between these concepts in 90% of future interviews. That means you should be sure to figure out what you're reading. And if you don't fully understand something, read additional sources. So, we know what an abstract class is and what an interface is. Now we'll go over their differences.
  1. An interface only describes behavior. It has no state. But an abstract class includes state: it describes both.

    For example, take the Bird abstract class and the CanFly interface:

    public abstract class Bird {
    
       private String species;
       private int age;
    
       public abstract void fly();
    }
    
    
    public interface CanFly {
    
       public void fly();
    }

    Let's create a MockingJay bird class and make it inherit Bird:

    public class MockingJay extends Bird {
    
       @Override
       public void fly() {
           System.out.println("Fly, bird!");
       }
    
       public static void main(String[] args) {
    
           MockingJay someBird = new MockingJay();
           someBird.setAge(19);
           System.out.println(someBird.getAge());
       }
    }

    As you can see, we can easily access the abstract class's state — its species and age variables.

    But if we try to do the same with an interface, the picture is different. We can try to add variables to it:

    public interface CanFly {
    
       String species = new String();
       int age = 10;
    
       public void fly();
    }
    
    public interface CanFly {
    
       private String species = new String(); // Error
       private int age = 10; // Another error
    
       public void fly();
    }

    We can't even declare private variables inside an interface. Why? Because the private modifier was created to hide the implementation from the user. And an interface has no implementation inside it: there isn't anything to hide.

    An interface only describes behavior. Accordingly, we can't implement getters and setters inside an interface. This is the nature of interfaces: they are needed to work with behavior, not state.

    Java 8 introduced default methods for interfaces that have an implementation. You already know about them, so we won't repeat ourselves.

  2. An abstract class connects and unites classes that are very closely related. In the same time, a single interface can be implemented by classes that have absolutely nothing in common.

    Let's return to our example with birds.

    Our Bird abstract class is needed for creating birds that are based on that class. Just birds and nothing else! Of course, there will be different kinds of birds.

    The difference between abstract classes and interfaces - 2

    With the CanFly interface, everybody gets on in their own way. It only describes the behavior (flying) associated with its name. Many unrelated things 'can fly'.

    The difference between abstract classes and interfaces - 3

    These 4 entities are not related to each other. They aren't even all living. However, they all CanFly.

    We couldn't describe them using an abstract class. They don't share the same state or identical fields. To define an aircraft, we would probably need fields for the model, production year, and maximum number of passengers. For Carlson, we would need fields for all the sweets he ate today, and a list of the games he'll play with his little brother. For a mosquito, ...uh... I don't even know... Maybe, an 'annoyance level'? :)

    The point is that we can't use an abstract class to describe them. They are too different. But they do have shared behavior: they can fly. An interface is perfect for describing everything in the world that can fly, swim, jump, or exhibit some other behavior.

  3. Classes can implement as many interfaces as you want, but they can only inherit one class.

    We've already mentioned this more than once. Java doesn't have multiple inheritance of classes, but it does support multiple inheritance of interfaces. This point follows in part from the previous one: an interface connects many different classes that often have nothing else in common, while an abstract class is created for a group of very closely related classes. Therefore, it makes sense that you can only inherit one such class. An abstract class describes an 'is-a' relationship.

Standard interfaces: InputStream and OutputStream

We've already gone over various classes responsible for input and output streams. Let's consider InputStream and OutputStream. In general, these aren't interfaces at all, but rather entirely genuine abstract classes. Now you know what that means, so it will be much easier to work with them :) InputStream is an abstract class responsible for byte input. Java has several classes that inherit InputStream. Each of them is designed to receive data from different sources. Because InputStream is the parent, it provides several methods that make it easy to work with data streams. Each descendant of InputStream has these methods:
  • int available() returns the number of bytes available for reading;
  • close() closes the input stream;
  • int read() returns an integer representation of the next available byte in the stream. If the end of the stream has been reached, -1 will be returned;
  • int read(byte[] buffer) tries to read bytes into buffer, and returns the number of bytes read. When it reaches the end of the file, it returns -1;
  • int read(byte[] buffer, int byteOffset, int byteCount) writes part of a block of bytes. It is used when the byte array may not have been filled entirely. When it reaches the end of the file, it returns -1;
  • long skip(long byteCount) skips byteCount bytes in the input stream, and returns the number of bytes ignored.
I recommend that you study the complete list of methods. There are actually more than ten child classes. For example, here are a few:
  1. FileInputStream: the most common type of InputStream. It is used to read information from a file;
  2. StringBufferInputStream: Another helpful type of InputStream. It converts a string into an InputStream;
  3. BufferedInputStream: A buffered input stream. It is used most often to increase performance.
Remember when we went over BufferedReader and said that you don't have to use it? When we write:
BufferedReader reader - new BufferedReader(new InputStreamReader(System.in))
…you don't have to use BufferedReader: An InputStreamReader can do the job. But BufferedReader improves the performance and can also read whole lines of data rather than individual characters. The same thing applies to BufferedInputSteam! The class accumulates input data in a special buffer without constantly accessing the input device. Let's consider an example:
import java.io.BufferedInputStream;
import java.io.FileInputStream;
import java.io.InputStream;

public class BufferedInputExample {

   public static void main(String[] args) throws Exception {
       InputStream inputStream = null;
       BufferedInputStream buffer = null;

       try {

           inputStream = new FileInputStream("D:/Users/UserName/someFile.txt");

           buffer = new BufferedInputStream(inputStream);

           while(buffer.available()>0) {

               char c = (char)buffer.read();

                System.out.println("Character read: " + c);
           }
       } catch(Exception e) {

           e.printStackTrace();

       } finally {

           inputStream.close();
           buffer.close();
       }
   }
}
In this example, we read data from a file located on a computer at 'D:/Users/UserName/someFile.txt'. We create 2 objects — a FileInputStream and a BufferedInputStream that 'wraps' it. Then we read bytes from the file and convert them into characters. And we do that until the file ends. As you can see, there's nothing complicated here. You can copy this code and run it on a real file on your computer :) The OutputStream class is an abstract class that represents an output stream of bytes. As you already know, this is the opposite of an InputStream. It isn't responsible for reading data from somewhere, but rather for sending data somewhere. Like InputStream, this abstract class gives all of its descendants a set of convenient methods:
  • int close() closes the output stream;
  • void flush() clears all output buffers;
  • abstract void write(int oneByte) writes 1 byte to the output stream;
  • void write(byte[] buffer) writes a byte array to the output stream;
  • void write(byte[] buffer, int offset, int count) writes a range of count bytes from an array, starting at the offset position.
Here are some of the descendants of the OutputStream class:
  1. DataOutputStream. An output stream that includes methods for writing standard Java data types.

    A very simple class for writing primitive Java data types and strings. You'll probably understand the following code even without an explanation:

    import java.io.*;
    
    public class DataOutputStreamExample {
    
       public static void main(String[] args) throws IOException {
    
           DataOutputStream dos = new DataOutputStream(new FileOutputStream("testFile.txt"));
    
           dos.writeUTF("SomeString");
           dos.writeInt(22);
           dos.writeDouble(1.21323);
           dos.writeBoolean(true);
    
       }
    }

    It has separate methods for each type — writeDouble(), writeLong(), writeShort(), and so on.


  2. FileOutputStream. This class implements a mechanism for sending data to a file on disk. By the way, we already used it in the last example. Did you notice? We passed it to DataOutputStream, which acted as a 'wrapper'.

  3. BufferedOutputStream. A buffered output stream. There's also nothing complicated here. It's purpose is analogous to BufferedInputStream (or BufferedReader). Instead of the usual sequential reading of data, it writes data using a special 'cumulative' buffer. The buffer makes it possible to reduce the number of times the data sink is accessed, thereby increasing performance.

    import java.io.*;
    
    public class DataOutputStreamExample {
    
         public static void main(String[] args) throws IOException {
    
               FileOutputStream outputStream = new FileOutputStream("D:/Users/Username/someFile.txt");
               BufferedOutputStream bufferedStream = new BufferedOutputStream(outputStream);
    
               String text = "I love Java!"; // We'll convert this string to a byte array and write it to a file
    
               byte[] buffer = text.getBytes();
    
               bufferedStream.write(buffer, 0, buffer.length);
         }
    }

    Again, you can play around with this code yourself and verify that it will work on real files on your computer.

We'll have a separate lesson about FileInputStream, FileOutputStream and BuffreredInputStream, so this is enough information for a first acquaintance. That's it! We hope you understand the differences between interfaces and abstract classes and are ready to answer any question, even trick questions :)