Inheritance and Interfaces.pdf
Document Details
Uploaded by Deleted User
Tags
Full Transcript
Inheritance and Interfaces From the Inheritance and Interfaces slides. Contents 1. Overview of Inheritance 2. A Hierarchy of Animals 3. The Animal Class 4. Extending Animal: The Eagle Class 5. Abstract Classes 6. The Object Class 7. Interfaces 8. Uses of Interfaces: IFlyable 9. Why Interfa...
Inheritance and Interfaces From the Inheritance and Interfaces slides. Contents 1. Overview of Inheritance 2. A Hierarchy of Animals 3. The Animal Class 4. Extending Animal: The Eagle Class 5. Abstract Classes 6. The Object Class 7. Interfaces 8. Uses of Interfaces: IFlyable 9. Why Interfaces? 10. Practice Problems Overview of Inheritance Inheritance is the idea of inheriting the attributes of your parents/ancestors. You inherit the qualities of your parents (either genetically or from the environment). We can apply that same concept to Objects. The idea is to create a hierarchy of classes that relate to each other in an “is a” relationship. Here is a quick summary of terminology that will be used throughout the notes: Parent Class: The existing class that is inherited from. ○ AKA the Super Class, the Base Class Child Class: The new class that is inheriting the qualities of the parent class. ○ AKA the Sub Class, the Derived Class ○ A child class “is a” type of its parent class In Java, every child class can only inherit one direct parent class. We will discuss why that is later. A Hierarchy of Animals To illustrate the concept of inheritance, let’s start by designing a hierarchy of animals. At the very base of our hierarchy, we have an Animal that encompasses all animals. The children of the Animal are Eagle and Insect. The children of Insect are Ant and Butterfly. So, in OOP terms: Animal is the parent class of the Eagle and Insect class Eagle is the child class of Animal ○ An Eagle “is an” Animal Insect is a child class of Animal, and the parent class of Ant and Butterfly ○ An Insect “is an” Animal Ant and Butterfly classes are children classes of the Insect parent class. ○ An Ant “is an” Insect ○ A Butterfly “is an” Insect The Animal Class To start, let's list some characteristics that all animals have. Every animal has the following attributes: name species (should not be changed) prey - a list of names that the current animal preys upon Every animal has the following behaviors: eat(String preyName) - if the name is in the prey list, this animal can eat it. makeSound() addPrey() - add a new prey to the list of prey Let’s also have a tracker that tracks how many Animal instances we have created: totalAnimalsCreated So, let’s put these into the Animal class: import java.util.*; public class Animal { public String name; public final String species; public ArrayList prey; public static int totalAnimalsCreated = 0; public static void main(String[] args) { Animal animal = new Animal("Red-Tailed Hawk", "Buteo jamaicensis"); animal.addPrey("Squirrel"); animal.addPrey("Mouse"); animal.eat("Mouse"); animal.eat("Boar"); } public Animal(String name, String species) { this.name = name; this.species = species; this.prey = new ArrayList(); totalAnimalsCreated++; } public void eat(String preyName) { if (prey.contains(preyName)) { System.out.println("Om nom nom"); } else { System.out.println("Yuck"); } } public void addPrey(String name) { if (!prey.contains(name)) { prey.add(name); } } public void makeSound() { System.out.println("Animal Noises"); } } Om nom nom Yuck Extending Animal: The Eagle Class Now that we have an Animal class defined, let’s move on to the Eagle. From the hierarchy diagram, we know that an Eagle “is an” Animal. Eagles should also have a name, species, and a list of prey. It should also be able to eat and make a sound. However, it wouldn’t make sense to redefine all of these characteristics specifically for the Eagle. In other words, an Eagle should “inherit” these characteristics from the Animal. In Java, to inherit from the Animal class, we use the extends keyword on the Eagle class signature. It would look like this: public class Eagle extends Animal {... } Now, Eagle inherits the fields and methods of the Animal class. In the Eagle class, we can access the fields and methods as if they are in the Eagle class itself: import java.util.*; public class Eagle extends Animal { public static void main(String[] args) { Eagle eagle = new Eagle("Eagle", "Haliaeetus leucocephalus"); eagle.addPrey("Snake"); eagle.eat("Snake"); eagle.makeSound(); } public Eagle(String name, String species) { super(name, species); } } Om nom nom Animal Noises Using and Extending the Animal Constructor with super() Now let’s take a look at the Eagle constructor. The Eagle constructor takes in a name and species. We want to initialize the same stuff as in the Animal constructor (name, species, prey). Therefore, we use the super() method. This method will pass the given arguments to the matching constructor in the super class. So effectively, the Eagle constructor is given a name and a species. The Eagle constructor says “call my parent’s (Animal) constructor”. Constructors are called in a chain based on the hierarchy from most general to least general. So in the case of an Eagle, Java will first call the Animal constructor, then the Eagle constructor. Some important notes on super(): If you do not use super(), Java will by default call the no argument Animal constructor. ○ Try removing the super() line from the Eagle constructor. Since Animal does not have a no argument constructor, we will get a compilation error. If you do use super(), it must be the first line of code in the Eagle constructor. Adding New Functionality for Eagles An Eagle can do its own things that other animals cannot. For example, an Eagle can fly, but not all Animals can fly. Therefore, we can create a fly() method that is specific to Eagles only. import java.util.*; public class Eagle extends Animal { public static void main(String[] args) { Eagle eagle = new Eagle("Eagle", "Haliaeetus leucocephalus"); eagle.fly(); } public Eagle(String name, String species) { super(name, species); } public void fly() { System.out.println("Flap Flap Glide"); } } Flap Flap Glide This fly() method is only available for Eagles and any class that extends from Eagle. An Animal cannot use the fly() method. Overriding Superclass Methods Finally, notice what happened when we called makeSound() with an Eagle. We printed out “Animal Noises” just like inside the Animal class. However, we should be more specific as to what sound an Eagle makes. So let’s override the Animal’s makeSound() method by defining makeSound() inside the Eagle class: import java.util.*; public class Eagle extends Animal { public static void main(String[] args) { Eagle eagle = new Eagle("Eagle", "Haliaeetus leucocephalus"); eagle.makeSound(); } public Eagle(String name, String species) { super(name, species); } public void makeSound() { System.out.println("CAWWWW"); } } CAWWWW By overriding the makeSound() method, we are telling Java to use the Eagle class’s makeSound() method instead of the Animal class’s makeSound() method. Abstract Classes So far, we’ve been avoiding a glaring error in the Animal class. What sound does an Animal make? Well, an animal does not necessarily have a definitive/concrete sound. However, in the Animal class, we have a makeSound() method that prints out “Animal Noises”. This doesn’t really make sense. So how can we fix it? Think about what an animal actually is. It is not some concrete thing in itself. There are different types of animals (Eagles, Ants, etc.), but an animal itself is more of an abstract concept that groups together a bunch of species. Therefore, we should not be able to instantiate an instance of an Animal. To do this, we make the Animal class abstract. import java.util.*; public abstract class Animal { public String name; public final String species; public ArrayList prey; public static int totalAnimalsCreated = 0; public static void main(String[] args) { Animal animal = new Animal("Red-Tailed Hawk", "Buteo jamaicensis"); animal.addPrey("Squirrel"); animal.addPrey("Mouse"); animal.eat("Mouse"); animal.eat("Boar"); } public Animal(String name, String species) { this.name = name; this.species = species; this.prey = new ArrayList(); totalAnimalsCreated++; } public void eat(String animalName) { if (prey.contains(animalName)) { System.out.println("Om nom nom"); } else { System.out.println("Yuck"); } } public void addPrey(String name) { if (!prey.contains(name)) { prey.add(name); } } public abstract void makeSound(); } Animal.java:13: error: Animal is abstract; cannot be instantiated Animal animal = new Animal("Red-Tailed Hawk", "Buteo jamaicensis"); ^ By marking the Animal class as abstract, we do the following: Important Note: We can no longer create an instance of an Animal (as shown in the error message above) We can mark makeSound() as abstract. By marking a method like makeSound() as abstract, we remove the implementation of makeSound() in the Animal class (there is no {...} block, only a semicolon). This also means that any non-abstract class that is a descendant of Animal is required to make an implementation of makeSound(). Therefore, Eagle must provide an implementation of makeSound(). public class Eagle extends Animal { public static void main(String[] args) { Eagle eagle = new Eagle(); eagle.makeSound(); } public Eagle() { super("Eagle", "Haliaeetus leucocephalus"); } public void makeSound() { System.out.println("CAWWWWW"); } } CAWWWW Note the following: Unlike the concrete Eagle class, because Insect is an abstract class, it does not require an implementation of makeSound(). However, because Insects are Animals, any class that inherits from Insects requires an implementation of makeSound(). Important Note: Abstract classes do not require an abstract method. You can create an abstract class and have implementations for every field and method within that class. However, if you have an abstract method, the class must be marked as abstract. Important Note: An abstract class can have a constructor method even though you cannot construct an instance of that class. This constructor should provide initialization steps that can be called via the super() method in a child class. The Object Class Every class in Java, built-in or your own, extends from the Object class. The Object class contains some built in methods that you are already familiar with: toString() - Convert this object to a String representation. By default, in the Object class, this prints out a reference to the Object in memory equals(Object o) - Determines if the given object is equal to the argument o. By default, equals() compares the references between objects. But remember how we override the functionality of makeSound() for an Eagle. Well we do the exact same thing when we redefine what toString() and equals() does: The Quirks of Inheriting Static, Final, and Private Methods Here is a quick recap of each of the keywords: Static: Belonging to the class. The same across all instances of a class. Final: Once initialized, cannot change Private: Only the class it resides in can access this method. Here is a quick summary of how static, final, and private keywords affect inheritance: private Methods: A private method in a class is not inherited by any subclasses. These methods are only accessible within the class where they are declared. As a result, subclasses cannot override or access these methods directly. static Methods: A static method belongs to the class itself rather than to any specific instance. Static methods are not inherited in the traditional sense, as they are called on the class rather than on instances of the class. However, if a subclass defines a static method with the same signature as a static method in its superclass, it hides the superclass method (this is known as method hiding, not method overriding). final Classes: A final class cannot be subclassed. This means that no class can extend or inherit from a class marked as final. Methods: A final method cannot be overridden by subclasses. This ensures that the method's implementation remains unchanged in any subclass. Interfaces Interfaces are essentially contracts for classes. It is a collection of fields and methods that a class must implement. If the class does not implement the fields/methods, then the class is not following the contract, and therefore the compiler will yell at you. Here’s the syntax for creating an interface. Interfaces are defined inside their own.java file (like classes): public interface INTERFACE_NAME { // Interface fields and methods signatures } For a class to use an interface in Java, we use the implements keyword in the class signature. public class CLASS_NAME implements INTERFACE_NAME { } So, since the CLASS_NAME class implements the INTERFACE_NAME interface, the CLASS_NAME class “signs” the interface’s contract specifying that it will implement all fields and methods in the interface. A class can implement multiple interfaces. However, classes can only extend one class. public class CHILD_CLASS extends PARENT_CLASS implements INTERFACE_1, INTERFACE_2, INTERFACE_3 { } Style Convention An interface in Java should be some adjective/adverb (describe a noun/action, usually ending in -able or -ible). Here are some examples: Flyable Comparable ← built-in Java interface Breathable Also, some programmers will prepend an I to the interface name to make it clear that it is an interface while developing. So IFlyable, IComparable, IBreathable, etc. Extending an Interface An interface can also be extended. public interface INTERFACE_A { public void foo(); } public interface INTERFACE_B extends INTERFACE_A { public void bar(); } By implementing INTERFACE_B, we also require implementing INTERFACE_A without explicitly stating it. So any class that implements INTERFACE_B must implement both foo() and bar() Note: This is the exact same thing as saying ClassName implements INTERFACE_A, INTERFACE_B. By extending interfaces, you are relating them by a hierarchy. Uses of Interfaces: IFlyable As an example, let’s create a new interface called IFlyable that has the method fly(). public interface IFlyable { public void fly(); } Now, any class that implements IFlyable will need to provide an implementation of the fly() method. Going back to our Animal hierarchy, the Eagle and the Butterfly can fly. So, we can place this interface on the Eagle and Butterfly classes like so: public class Eagle extends Animal implements IFlyable { public static void main(String[] args) { Eagle eagle = new Eagle(); eagle.fly(); } public Eagle() { super("Eagle", "Haliaeetus leucocephalus"); } public void makeSound() { System.out.println("CAWWWWW"); } public void fly() { System.out.println("Flap flap glide..."); } } public class Butterfly extends Insect implements IFlyable { public static void main(String[] args) { Butterfly butterfly = new Butterfly(); } public Butterfly() { super("Butterfly", "Danaus plexippus"); } public void makeSound() { System.out.println("BZZZZ"); } public void fly() { System.out.println("Flap flap flap"); } } By implementing the IFlyable interface, we ensure that Eagle and Butterfly have a fly() method set up. So now our diagram looks like this: Why Interfaces? So we made this IFlyable interface that guarantees that Eagle and Butterfly have a fly() method.But what is the point of interfaces? There are four reasons why interfaces are so powerful. Interfaces as a Data Type Since we can guarantee that any class that implements the IFlyable interface, we can just reference those classes based on the interface. Similar to how we can use a class like a data type, we can also use an interface like a data type: import java.util.*; public class FlyTest { public static void main(String[] args) { ArrayList flyableObjects = new ArrayList(); flyableObjects.add(new Eagle()); flyableObjects.add(new Butterfly()); flyableObjects.get(0).fly(); flyableObjects.get(1).fly(); } } Flap flap glide... Flap flap flap In the above code, we create an ArrayList of objects that implement IFlyable. Therefore, any objects we add to the ArrayList must implement IFlyable. However, by referencing Eagle and Butterfly as IFlyable objects, we cannot access any field or method that is not the fly() method. IFlyable only asserts that fly() is implemented. It has no access or control over other characteristics like the name, species, etc. For example, although we know that the first flyableObject is an Eagle, we cannot access its name or species. Side Note: To access the specific Eagle fields/methods, we would have to cast flyableObjects.get(0) to an Eagle. Avoid Modifying The Existing Hierarchy Suppose that we did not have the IFlyable interface. How would we guarantee that an animal has a fly() method? One possible solution is to modify the hierarchy like so: However, this leads to multiple issues: Creating a new class in a large enough hierarchy is tiresome and difficult to keep track of. More classes = more dependencies, which makes the system more complex. Butterfly now inherits from two classes: Flying Animals and Insect, which is not allowed in Java (explained more down below) Interfaces solve these issues by removing the need to create a new class, simplifying the hierarchy while allowing for single inheritance. Single Inheritance vs Multiple Inheritance We’ve mentioned that Java classes can only extend one class. This is called Single Inheritance. Alternatively, Multiple Inheritance is when a class can extend multiple classes. Multiple Inheritance allows us to inherit the fields and methods from multiple classes. This makes sense logically, as an object can inherit qualities from multiple sources. For example, as a child, you inherit qualities from both your mother and father. The problems that can arise with multiple inheritance is when a child class inherits from two parent classes, and both parent classes have the same field/method definition. To illustrate this point: Both parents have someMethod(). Well what if the child class wants to use someMethod()? Which version of someMethod does it choose: Parent Class 1 or Parent Class 2? Java deals with this ambiguity by preventing it entirely. That is why a child class can only inherit from one parent class. But how do we get all of the benefits of multiple inheritance in Java? We use interfaces! A class can implement any number of interfaces that describe qualities it has. Therefore, we (kind of) inherit qualities from multiple sources like in multiple inheritance, but without the drawbacks of the ambiguity. Hierarchy Independence Eagles and Butterflies are not the only things that can fly. Other birds and insects can also fly. Therefore, we can place the IFlyable interface on all flying animals. Also, animals are not the only things that can fly. What about a plane? It is not a part of the Animal hierarchy, but it should also have a fly() method. The IFlyable interface can also be placed on the Plane class. Practice Problems 1. Complete the Animal hierarchy as illustrated above. Create the following: a) Insect - an abstract class that extends from Animal with the following members: int numLegs - How many legs the insect have Insect Constructor - taking in name, species, numLegs abstract void layEggs() b) Ant - A class that extends from Insect with the following members: String role - The role of the ant (worker, queen, etc) Ant Constructor - taking in name, species, numLegs, role void crawl() - Method that prints out “Crawling” void layEggs() - Method that prints “Ant laying eggs” void makeSound() - Method that prints “Rubbing abdomens” c) Butterfly - A class that extends Insect and implements IFlyable with the following members: Butterfly Constructor - taking in name, species, numLegs fly() - Method that prints out “Flap Flap Flap” void layEggs() - Method that prints “Butterfly laying eggs” void makeSound() - Method that prints “Flapping wings” Solution a) Insect public abstract class Insect extends Animal { public int numLegs; public Insect(String name, String species, int numLegs) { super(name, species); this.numLegs = numLegs; } public abstract void layEggs(); } b) Ant public class Ant extends Insect { public String role; public Ant(String name, String species, int numLegs, String role) { super (name, species, numLegs); this.role = role; } public void crawl() { System.out.println("Crawling"); } public void layEggs() { System.out.println("Ant laying eggs"); } public void makeSound() { System.out.println("Rubbing abdomen"); } } c) Butterfly public class Butterfly extends Insect implements IFlyable { public Butterfly(String name, String species, int numLegs) { super(name, species, numLegs); } public void fly() { System.out.println("Flap Flap Flap"); } public void layEggs() { System.out.println("Butterfly laying eggs"); } public void makeSound() { System.out.println("Flapping Wings"); } } Note: Remember that Animal has an abstract method makeSound(). Since Insect is also abstract, it does not require an implementation of makeSound(). However, since Ant and Butterfly are Insects, and an Insect is an Animal, Ant and Butterfly require an implementation of makeSound() 2. What are the errors with the following code? public class ChildClass extends ParentClass1, ParentClass2, Interface1, Interface2 { public abstract void foo(); } Solution: ChildClass can only extend one parent class in Java. It either extends ParentClass1 or ParentClass2. A ChildClass implements Interface1 and Interface2. So after extend ParentClass, follow up with implements Interface1, Interface2 ○ Remember that a class can implement any number of interfaces. In order for foo() to be abstract, ChildClass must also be abstract. If ChildClass is not meant to be abstract, then foo() must have an implementation and cannot be abstract. 3. What are the errors with the following code? public abstract class InheritanceTest { public int value; public static void main(String[] args) { InheritanceTest test = new InheritanceTest(10); test.foo(); test.bar(); } public InheritanceTest(int value) { this.value = value; } public void foo() { value++; } public void bar() { value--; } } Solution: InheritanceTest is an abstract class, which means it cannot be instantiated. Therefore, creating test in main will lead to a compile time error. ○ Remember that abstract classes can have a defined constructor ○ Remember that abstract classes do not require abstract methods. However, abstract methods do require being in an abstract class.