Part 8! Getting more advanced in this sub-section now (part 3 is v hard)

  1. Short recap
    • recap of Java Programming I
  2. Hash Map
    • HashMap is used when data is stored as key-value pairs, where values can be added, retrieved, and deleted using keys.

    • .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"));
    
    • A constructor call returns a reference to an object. A reference is information about the location of object data.

    • Object as a method parameter: Since objects are reference variables, any type of object can be defined to be a method parameter.
     public AmusementParkRide(String name, int lowestHeight) {
         this.name = name;
         this.lowestHeight = lowestHeight;
     }
    
    • So the method allowedToRide of an AmusementParkRide object is given a Person object as a parameter. Like earlier, the value of the variable — in this case, a reference — is copied for the method to use. The method handles a copied reference, and it calls the getHeight method of the person passed as a parameter.

    • Review HealthStation and CardPayments exercise

    • Object as object variable (object containing references to other object) the thing I am struggling with

    • Abstraction is to conceptualize the programming code so that each concept has its own clear responsibilities.

     public class SimpleDate {
         private int day;
         private int month;
         private int year;
    
         public SimpleDate(int day, int month, int year) {
             this.day = day;
             this.month = month;
             this.year = year;
         }
    
         public String toString() {
             return this.day + "." + this.month + "." + this.year;
         }
    
         // used to check if this date object (`this`) is before
         // the date object given as the parameter (`compared`)
         public boolean before(SimpleDate compared) {
             // first compare years
             if (this.year < compared.year) {
                 return true;
             }
    
             if (this.year > compared.year) {
                 return false;
             }
         }
    
    • Even though the object variables year, month, and day are encapsulated (private) object variables, we can read their values by writing compared.variableName. This is because private variable can be accessed from all the methods contained by that class. Notice that the syntax here matches calling some object method. Unlike when calling a method, we refer to a field of an object, so the parentheses that indicate a method call are not written.
     public class Person {
         // ...
    
         public boolean olderThan(Person compared) {
             if (this.birthday.before(compared.getBirthday())) {
                 return true;
             }
    
             return false;
    
             // or return more directly:
             // return this.birthday.before(compared.getBirthday());
         }
     }
    
    • Why we use .equals() to compare strings and cannot use equal sign: With primitive variables such as int, comparing two variables can be done with two equality signs. This is because the value of a primitive variable is stored directly in the “variable’s box”. The value of reference variables, in contrast, is an address of the object that is referenced; so the “box” contains a reference to the memory location. Using two equality signs compares the equality of the values stored in the “boxes of the variables” — with reference variables, such comparisons would examine the equality of the memory references.

    • .equals() doesnt work really well for strings so need to implement design ourselves if want to compare 2 objects

     public class SimpleDate {
         private int day;
         private int month;
         private int year;
    
         public SimpleDate(int day, int month, int year) {
             this.day = day;
             this.month = month;
             this.year = year;
         }
    
         public int getDay() {
             return this.day;
         }
    
         public int getMonth() {
             return this.month;
         }
    
         public int getYear() {
             return this.year;
         }
    
         public boolean equals(Object compared) {
             // if the variables are located in the same position, they are equal
             if (this == compared) {
                 return true;
             }
    
             // if the type of the compared object is not SimpleDate, the objects are not equal
             if (!(compared instanceof SimpleDate)) {
                 return false;
             }
    
             // convert the Object type compared object
             // into a SimpleDate type object called comparedSimpleDate
             SimpleDate comparedSimpleDate = (SimpleDate) compared;
    
             // if the values of the object variables are the same, the objects are equal
             if (this.day == comparedSimpleDate.day &&
                 this.month == comparedSimpleDate.month &&
                 this.year == comparedSimpleDate.year) {
                 return true;
             }
             // or return this.day == comparedSimpleDate.day && this.month == comparedSimpleDate.month && this.year == comparedSimpleDate.year
    
             // otherwise the objects are not equal
             return false;
         }
    
         @Override
         public String toString() {
             return this.day + "." + this.month + "." + this.year;
         }
     }
    
    • The equals method is implemented in such a way that it can be used to compare the current object with any other object. The method receives an Object-type object as its single parameter — all objects are Object-type, in addition to their own type. The equals method first compares if the addresses are equal. After this, we examine if the types of the objects are the same. Next, the Object-type object passed as the parameter is converted to the type of the object that is being examined by using a type cast, so that the values of the object variables can be compared.

    • Object as a method’s return value: For example below, factory object could also be used to create and return new Car objects

     public class Factory {
         private String make;
    
         public Factory(String make) {
             this.make = make;
         }
    
         public Car procuceCar() {
             return new Car(this.make);
         }
     }
    
    • final keyword means values of these object variables cannot be modified after they have been set in the constructor. The objects of Money class are unchangeable so immutable — if we want to e.g. increase the amount of money, we must create a new object to represent that new amount of money.