Part 9!

  1. Class inheritance
    • An essential idea behind object-oriented programming is that solutions rise from the interactions between objects which are created from classes. An object in object-oriented programming is an independent unit that has a state, which can be modified by using the methods that the object provides. Objects are used in cooperation; each has its own area of responsibility.

    • Every Java class extends the class Object, which means every class we create has all the methods defined in the Object class. If we want to change how these methods are defined in Object function, they must be overriden by defining a new implementation for them in the newly created class. The objects we create receive the methods equals and hashCode, among others, from the Object class.

    • Each class can directly extend only one class. However, a class indirectly inherits all the properties of the classes it extends. So the ArrayList class derives from the class AbstractList, and indirectly derives from the classes AbstractCollection and Object. So ArrayList has all the variables and methods of the classes AbstractList, AbstractCollection, and Object.

    • use keyword extends to inherit properties of class. The class that receives the properties is called the subclass, and the class whose properties are inherited is called the superclass.

    • We notice a significant amount of overlap between the contents of Engine and Part. It can confidently be said the Engine is a special case of Part. The Engine is a Part, but it also has properties that a Part does not have, which in this case means the engine type. Let’s recreate the class Engine and, this time, use inheritance in our implementation. We’ll create the class Engine which inherits the class Part: an engine is a special case of a part.

     public class Engine extends Part {
    
         private String engineType;
    
         public Engine(String engineType, String identifier, String manufacturer, String description) {
             super(identifier, manufacturer, description);
             this.engineType = engineType;
         }
    
         public String getEngineType() {
             return engineType;
         }
     }
    
    • public class Engine extends Part indicates that the class Engine inherits the functionality of the class Part. We also define an object variable engineType in the class Engine.

    • Use the keyword super to call the constructor of the superclass. The call super(identifier, manufacturer, description) calls the constructor public Part(String identifier, String manufacturer, String description) which is defined in the class Part. Through this process the object variables defined in the superclass are initiated with their initial values. After calling the superclass constructor, we also set the proper value for the object variable engineType.

    • The super call bears some resemblance to the this call in a constructor; this is used to call a constructor of this class, while super is used to call a constructor of the superclass. If a constructor uses the constructor of the superclass by calling super in it, the super call must be on the first line of the constructor. This is similar to the case with calling this (must also be the first line of the constructor).

    • Since the class Engine extends the class Part, it has all the methods that the class Part offers. You can create instances of the class Engine the same way you can of any other class.

    • If a method or variable has the access modifier private, it is visible only to the internal methods of that class. Subclasses will not see it, and a subclass has no direct means to access it. So, from the Engine class there is no way to directly access the variables identifier, manufacturer, and description, which are defined in the superclass Part. The programmer cannot access the variables of the superclass that have been defined with the access modifier private.

    • A subclass sees everything that is defined with the public modifier in the superclass. If we want to define some variables or methods that are visible to the subclasses but invisible to everything else, we can use the access modifier protected to achieve this.

    • use super to call the constructor of the superclass. The call receives the types of values that the superclass constructor requires as parameters. If there are multiple constructors in the superclass, the parameters of the super call dictate which of them is used.

    • When the constructor (of the subclass) is called, the variables defined in the superclass are initialized. The events that occur during the constructor call are practically identical to what happens with a normal constructor call. If the superclass doesn’t provide a non-parameterized constructor, there must always be an explicit call to the constructor of the superclass in the constructors of the subclass.

    • Calling a superclass method: call the methods defined in the superclass by prefixing the call with super, just as you can call the methods defined in this class by prefixing the call with this.

     public String toString() {
         return super.toString() + "\n  And let's add my own message to it!";
     }
    
    • Person and subclass exercise is useful to understand
     public class Main {
    
         public static void main(String[] args) {
             ArrayList<Person> persons = new ArrayList<Person>();
             persons.add(new Teacher("Ada Lovelace", "24 Maddox St. London W1S 2QN", 1200));
             persons.add(new Student("Ollie", "6381 Hollywood Blvd. Los Angeles 90028"));
    
             printPersons(persons);
         }
         public static void printPersons(ArrayList<Person> persons){
             for(Person i:persons){
                 System.out.println(i);
             }
         }
     }
    
     public class Person {
         private String name,address;
            
         public Person(String name,String address){
             this.name = name;
             this.address= address;
         }
         public String toString(){
             return this.name + "\n  " + this.address;
                
         }
            
     }
    
     public class Student extends Person {
         private int credit;
            
         public Student(String name, String address){
             super(name,address);
                
         }
         public int credits(){
             return credit;
         }
         public void study(){
             credit +=1;
         }
         public String toString(){
             return super.toString() + "  \n"+ "  Study credits  "+this.credit;
         }
            
     }
    
    
     public class Teacher extends Person {
         private int salary;
            
         public Teacher(String name, String address, int salary){
             super(name,address);
             this.salary=salary;
         }
         public String toString(){
             return super.toString() +"\n  " + "salary " + salary+" euro/month";
         }
     }
    
    • The actual type of an object dictates which method is executed: An object’s type decides what the methods provided by the object are. For instance, we implemented the class Student earlier. If a reference to a Student type object is stored in a Person type variable, only the methods defined in the Person class (and its superclass and interfaces) are available. Cannot use Student methods

    • In the last exercise we wrote a new toString implementation for Student to override the method that it inherits from Person. The class Person had already overriden the toString method it inherited from the class Object. If we handle an object by some other type than its actual type, which version of the object’s method is called?

    • The method to be executed is chosen based on the actual type of the object, which means the class whose constructor is called when the object is created. If the method has no definition in that class, the version of the method is chosen from the class that is closest to the actual type in the inheritance hierarchy.

    • Polymorphism: Regardless of the type of the variable, the method that is executed is always chosen based on the actual type of the object. Objects are polymorphic, which means that they can be used via many different variable types. The executed method always relates to the actual type of the object. This phenomenon is called polymorphism.

    • If there is no String toString method in subclass, code looks for toString method in superclass. Methods mentioned in toString method in superclass are looked in subclass and definitions are taken.

    • Inheritance is a tool for building and specializing hierarchies of concepts; a subclass is always a special case of the superclass. If the class to be created is a special case of an existing class, this new class could be created by extending the existing class. For example, in the previously discussed car part scenario an engine is a part, but an engine has extra functionality that not all parts have.

    • When inheriting, the subclass receives the functionality of the superclass. If the subclass doesn’t need or use some of the inherited functionality, inheritance is not justifiable. Classes that inherit will inherit all the methods and interfaces from the superclass, so the subclass can be used in place of the superclass wherever the superclass is used. It’s a good idea to keep the inheritance hierarchy shallow, since maintaining and further developing the hierarchy becomes more difficult as it grows larger. (not more than 2 or 3 levels deep)

    • Inheritance is not useful in every scenario. For instance, extending the class Car with the class Part (or Engine) would be incorrect. A car includes an engine and parts, but an engine or a part is not a car. If an object owns or is composed of other objects, inheritance should not be used.

    • When using inheritance, take care to ensure that the Single Responsibility Principle holds true. There should only be one reason for each class to change. If you notice that inheriting adds more responsibilities to a class, you should form multiple classes of the class.

    • in subclass, dont declare variables metioned in superclass (important)

    • string representation provided by the ArrayList class is .toString()

    • Abstract class: Combines interfaces and inheritance. You cannot create instances of them — you can only create instances of subclasses of an abstract class. They can include normal methods which have a method body, but it’s also possible to define abstract methods that only contain the method definition. Implementing the abstract methods is the responsibility of subclasses.

    • The greatest difference between interfaces and abstract classes is that abstract classes can contain object variables and constructors in addition to methods. Since you can also define functionality in abstract classes, you can use them to define e.g. default behavior.

  2. Interface
    • The classes that implement the interface decide how the methods defined in the interface are implemented. A class implements the interface by adding the keyword implements after the class name followed by the name of the interface being implemented.

    • Implementations of methods defined in the interface must always have public as their visibility attribute.

    • When a class implements an interface, it signs an agreement. The agreement dictates that the class will implement the methods defined by the interface. If those methods are not implemented in the class, the program will not function.

    • The interface defines only the names, parameters, and return values ​​of the required methods. The interface, however, does not have a say on the internal implementation of its methods. It is the responsibility of the programmer to define the internal functionality for the methods.

    • .put(some key, some value) adds key-value pair to HashMap object

    • .get(some key) gets value of that key, returns null if there is not such key. This method passes key as parameter and returns its value.

    • .values() gets set of values of that key
     public ArrayList<Book> getBookByPart(String titlePart) {
         titlePart = sanitizedString(titlePart);
    
         ArrayList<Book> books = new ArrayList<>();
    
         for(Book book : this.directory.values()) {
             if(!book.getName().contains(titlePart)) {
                 continue;
             }
    
             books.add(book);
         }
    
         return books;
     }
    
    • .keySet(some value) gets key of that value

    • .containsKey(some key) is a boolean method that checks existence of some key

    • Going through Hash map’s values using keySet() method (but inefficient):

     public ArrayList<Book> getBookByPart(String titlePart) {
         titlePart = sanitizedString(titlePart);
    
         ArrayList<Book> books = new ArrayList<>();
    
         for(String bookTitle : this.directory.keySet()) {
             if(!bookTitle.contains(titlePart)) {
                 continue;
             }
    
             // if the key contains the given string
             // we retrieve the value related to it
             // and add it tot the set of books to be returned
    
             books.add(this.directory.get(bookTitle));
         }
    
         return books;
     }
    
    • Two type parameters are required when creating a hash map - the type of the key and the type of the value added. Like HashMap<String, Integer> hashmap = new HashMap<>();

    • The hash map has a maximum of one value per key. If a new key-value pair is added to the hash map, but the key has already been associated with some other value stored in the hash map, the old value is deleted from the hash map.

    • A Reference Type Variable as a Hash Map Value:

     Book senseAndSensibility = new Book("Sense and Sensibility", 1811, "...");
     HashMap<String, Book> directory = new HashMap<>();
     directory.put(senseAndSensibility.getName(), senseAndSensibility);
     // directory.get("Sense and Sensibility").getContent() or some method in object
    
     Book match = null;
     //or just match=null;
     for (Book book: books) {
         if (book.getName().equals("Sense and Sensibility") {
             match = book;
             break;
         }
     }
    
    • The difference in performance is due to the fact that when a book is searched for in a list, the worst-case scenario involves going through all the books in the list. In a hash map, it isn’t necessary to check all of the books as the key determines the location of a given book in a hash map.

    • Limitations of HashMap are that although they work well when we know exactly what we are looking for but if we wanted to identify books whose title contains a particular string, the hash map cannot. The hash maps also have no internal order, and it is not possible to search the hash map based on the indexes. The items in a list are in the order they were added to the list.

    • .toLowerCase() creates new string of all letters to lower case

    • .trim() creates new string that removes space at start and end

    • Library class encapsulating hash map containing books

     public class Library {
         private HashMap<String, Book> directory;
    
         public Library() {
             this.directory = new HashMap<>();
         }
    
         public void addBook(Book book) {
             String name = sanitizedString(book.getName());
    
             if (this.directory.containsKey(name)) {
                 System.out.println("Book is already in the library!");
             } else {
                 directory.put(name, book);
             }
         }
    
         public Book getBook(String bookTitle) {
             bookTitle = sanitizedString(bookTitle);
             return this.directory.get(bookTitle);
         }
    
         public void removeBook(String bookTitle) {
             bookTitle = sanitizedString(bookTitle);
    
             if (this.directory.containsKey(bookTitle)) {
                 this.directory.remove(bookTitle);
             } else {
                 System.out.println("Book was not found, cannot be removed!");
             }
         }
    
         public static String sanitizedString(String string) {
             if (string == null) {
                 return "";
             }
    
             string = string.toLowerCase();
             return string.trim();
         }
     }
    
    • A hash map expects that only reference-variables are added to it (in the same way that ArrayList does). Java converts primitive variables to their corresponding reference-types when using any Java’s built in data structures (such as ArrayList and HashMap). Although the value 1 can be represented as a value of the primitive int variable, its type should be defined as Integer when using ArrayLists and HashMaps.
     HashMap<Integer, String> hashmap = new HashMap<>(); // works
     hashmap.put(1, "Ole!");
     HashMap<int, String> map2 = new HashMap<>(); // doesn't work
    
    • A hash map’s key and the object to be stored are always reference-type variables. Java converts primitive variables to reference-types automatically as they are added to either a HashMap or an ArrayList. This automatic conversion to a reference-type variable is called auto-boxing in Java, i.e. putting something in a box automatically.

    • When performing automatic conversion, ensure that the value to be converted is not null. Or there will be java.lang.reflect.InvocationTargetException error.

    • .getOrDefault(some key to be found, if key is not found, return this value normally 0) method of the HashMap searches for the key passed to it as a parameter from the HashMap. If the key is not found, it returns the value of the second parameter passed to it.

  3. Similarity of objects
    • .equals() was used to check similarity of strings but can also be used for references. It checks whether the object given as a parameter has the same reference as the object it is being compared to. In other words, the default behaviour checks whether the two objects are the same.
     Book bookObject = new Book("Book object", 2000, "...");
     Book anotherBookObject = bookObject;
    
     if (bookObject.equals(anotherBookObject)) {
         System.out.println("The books are the same");
     } else {
         System.out.println("The books aren't the same");
     }
    
     // we now create an object with the same content that's nonetheless its own object
     anotherBookObject = new Book("Book object", 2000, "...");
    
     if (bookObject.equals(anotherBookObject)) {
         System.out.println("The books are the same");
     } else {
         System.out.println("The books aren't the same");
     }
    
    • The internal structure of the book objects (i.e., the values of their instance variables ) in the previous example is the same, but only the first comparison prints “The books are the same”. This is because the references are the same in the first case, i.e., the object is compared to itself. The second comparison is about two different entities, even though the variables have the same values and thus prints “aren’t the same”.

    • If we want to compare our own classes using the equals method, then it must be created and defined inside the class. The method created accepts an Object-type reference as a parameter, which can be any object. The comparison first looks at the references. This is followed by checking the parameter object’s type with the instanceof operation - if the object type does not match the type of our class, the object cannot be the same. We then create a version of the object that is of the same type as our class, after which the object variables are compared against each other.

     public boolean equals(Object comparedObject) {
         // if the variables are located in the same place, they're the same
         if (this == comparedObject) {
             return true;
         }
    
     // if comparedObject is not of type Book, the objects aren't the same
     if (!(comparedObject instanceof Book)) {
             return false;
         }
    
         // let's convert the object to a Book-olioksi
         Book comparedBook = (Book) comparedObject;
    
         // if the instance variables of the objects are the same, so are the objects
         if (this.name.equals(comparedBook.name) &&
             this.published == comparedBook.published &&
             this.content.equals(comparedBook.content)) {
             return true;
         }
    
         // otherwise, the objects aren't the same
         return false;
     }
    
    • Similarly for ArrayList:
     ArrayList<Book> books = new ArrayList<>();
     Book bookObject = new Book("Book Object", 2000, "...");
     books.add(bookObject);
    
     if (books.contains(bookObject)) {
         System.out.println("Book Object was found.");
     }
    
     bookObject = new Book("Book Object", 2000, "...");
    
     if (!books.contains(bookObject)) {
         System.out.println("Book Object was not found.");
     }
    
    • In addition to equals, the hashCode method can also be used for approximate comparison of objects. The method creates from the object a “hash code”, i.e, a number, that tells a bit about the object’s content. If two objects have the same hash value, they may be equal. On the other hand, if two objects have different hash values, they are certainly unequal.

    • HashMap’s internal behavior is based on the fact that key-value pairs are stored in an array of lists based on the key’s hash value. Each array index points to a list. The hash value identifies the array index, whereby the list located at the array index is traversed. The value associated with the key will be returned if, and only if, the exact same value is found in the list (equality comparison is done using the equals method). This way, the search only needs to consider a fraction of the keys stored in the hash map.

    • redo notes here and exercises

  4. Grouping data using hash maps

    • HashMap can only have 1 value per key. But we sometimes want to assign multiple values to 1 key. Since keys and values ​​in a hash map can be any variable, it is also possible to use lists as values in a hash map. You can add more values ​​to a single key by attaching a list to the key.
     HashMap<String, ArrayList<String>> phoneNumbers = new HashMap<>();
    
    • Each key of the hash map now has a list attached to it. Although the new command creates a hash map, the hash map initially does not contain a single list. They need to be created separately as needed as so:
     HashMap<String, ArrayList<String>> phoneNumbers = new HashMap<>();
    
     // let's initially attatch an empty list to the name Pekka
     phoneNumbers.put("Pekka", new ArrayList<>());
    
     // and add a phone number to the list at Pekka
     phoneNumbers.get("Pekka").add("040-12348765");
     // and let's add another phone number
     phoneNumbers.get("Pekka").add("09-111333");
    
     System.out.println("Pekka's numbers: " + phoneNumbers.get("Pekka"));