/java-11

Java 11 OCP study notes.

Java 11

Java SE 11 Programmer I

Creating a Simple Java Program

  1. A Java class is compiled using the javac program. Compilation results in one or more class files depending on the contents of the Java source file. An example is shown below:

    javac TestClass.java
  2. A Java class is executed using the java program. An example is shown below:

    java TestClass a b c
  3. The Java Virtual Machine (JVM) is an executable. When the JVM runs, it loads the given class and looks for the main method of that class to run. The executable for the JVM is named java.

  4. Every Java class belongs to a package. There can be at most one package statement in the source file, and it must be the first statement in the file. If there is no package statement, then the classes defined in that file belong to an unnamed package which is known as the default package. Classes from other packages can be imported so that they can be referred to without using the Fully Qualified Class Name (FQCN).

  5. Java code is made up of expressions, statements, and blocks. An expression is made up of variables, operators, and method invocations. An expression evaluates to a single value. An expression is something which evaluates a value, while a statement is a line of code that does something. Some expressions can be made into a statement by terminating it with a semicolon, such as assignment expressions, usage of ++ or --, method invocation and object creation expressions. A block is a group of zero or more statements between balanced branches and can be used wherever a single statement is allowed.

  6. The stack is used for storing local variables, function calls, and references to objects. The heap is used for storing objects.

Working with Java Primitive Data Types and String APIs

  1. Java is a statically typed language. This means that the data type of a variable is defined at compile time and cannot change during run time.

  2. Java has primitive and reference variables. The Java compiler and JVM inherently know what primitive types mean, while reference data types are built by combining primitive data types and other reference data types.

  3. Primitive data types are shown below:

    Date Type Size Description Sample
    byte 1 byte -128 to 127 -1, 0, 1
    short 2 bytes -32,768 to 32,767 -1, 0, 1
    int 4 bytes -2,147,483,648 to 2,147,483,647 -1, 0, 1
    long 8 bytes -9,223,372,036,854,775,808 to 9,223,372,036,854,775,807 -1, 0, 1
    float 4 bytes Stores fractional numbers. Sufficient for storing 6 to 7 decimal digits 1.1f, 2.0f
    double 8 bytes Stores fractional numbers. Sufficient for storing 15 decimal digits 1.1d, 2.0d
    boolean 1 bit Stores true or false values true, false
    char 2 bytes Stores a single character '\u0000', 'a'
  4. Wrapper classes exist for Byte, Short, Integer, Long, Float and Double.

  5. Reference types include:

    • Classes
    • Interfaces
    • Enums
  6. A variable contains a raw number and assigning one variable to another simply copies that number from one variable to another. Java uses pass by value semantics.

  7. Java initialises all static and instance variables of a class automatically if you do not initialise them explicitly. You must initialise local variables explicitly before they are used.

  8. Assigning a smaller type to a larger type is known as implicit widening conversion as no cast is required. To assign a larger type to a smaller type, for a compile time constant (i.e., final) the compiler will automatically narrow the value. If the source variable is not a constant then a cast is required, this is known as explicit narrowing. Determining the value that will be assigned when doing an explicit can be complicated.

  9. A String is an object of class java.lang.String. String is a final class and implements java.lang.CharSequence. String is immutable so the value cannot be changed after instantiation.

  10. A String can be instantiated using:

    String myString = new String();
    String myString = new String("example");
    String myString = "example";
    String myString = new String(StringBuilder sb);
    String myString = new String(byte[] bytes);
    String myString = new String(char[] value);
    String myString = s1 + "add";
  11. String concatenation can occur with the += operator if one of the operands is a String.

  12. Strings created without using the new keyword are kept in the string pool. If a String is created without the new keyword and the same string already exists in the string pool, then a reference to the existing String will be returned.

  13. Useful String methods include:

    int length();
    char charAt(int index);
    int indexOf(int ch);
    String substring(int beginIndex, int endIndex);
    String substring(int beginIndex);
    String concat(String str);
    String toLowerCase();
    String toUpperCase();
    String replace(char oldChar, char newChar);
    String strip();
    String stripLeading();
    String stripTrailing();
    String trim();
  14. Note that in the substring methods the ending index is exclusive, and the starting index is inclusive.

  15. Useful methods for inspecting a String include:

    boolean isBlank();
    boolean isEmpty();
    boolean startsWith(String prefix);
    boolean endsWith(String suffix);
    boolean contains(CharSequence s);
    boolean equals(Object anObject);
    boolean equalsIgnoreCase(String anotherString);
  16. java.lang.StringBuilder is the mutable twin of java.lang.String. StringBuilder is also a final class and implements java.lang.CharSequence. StringBuilder is better suited for creating temporary strings that have no use once a method ends.

  17. StringBuilder provides several constructors:

    StringBuilder();
    StringBuilder(CharSequence seq);
    StringBuilder(int capacity);
    StringBuilder(String str);
  18. Note that the default size for the no argument constructor is 16 characters.

  19. Useful StringBuilder methods include:

    insert(int position, <X> value);
    append(<X> value);
    reverse();
    delete(int start, int end);
    deleteCharAt(int index);
    replace(int start, int end, String replacement);
    int capacity();
    char charAt(int index);
    int length();
    int indexOf(String str);
    int indexOf(String str, int startIndex);
    void setLength(int len);
    String substring(int start);
    String substring(int start, int end);
    String toString();
  20. The value to append is typically determined by String.valueOf().

Using Operators and Decision Constructs

  1. Arithmetic operators are used to perform standard mathematical operations on all primitive variables (and wrapper objects for numeric types) except Boolean.

  2. Operators and their precedence shown below:

    Operator Type Category Precedence
    Unary postfix expr++ expr--
    Unary prefix ++expr --expr +expr -expr ~ !
    Arithmetic multiplicative * / %
    Arithmetic additive + -
    Shift shift << >> >>>
    Relational comparison < > <= >= instanceof
    Relational equality == !=
    Bitwise bitwise AND &
    Bitwise bitwise exclusive OR ^
    Bitwise bitwise inclusive OR |
    Logical logical AND &&
    Logical logical OR ||
    Ternary ternary ? :
    Assignment assignment = += -= *= /= %= &= ^= |= <<= >>= >>>=
  3. Java applies the rules of numeric promotion while working with operators that deal with numeric values. Unary numeric promotion occurs if an operand is smaller than int and causes the operand to be automatically promoted to int. Binary numeric promotion occurs if one of the promoted operands is larger than int, and causes both operands to be promoted to the larger operand.

  4. In the case of a Dangling Else statement, the Else is associated to the nearest if statement.

  5. In a switch statement the expression must be:

    • Integral types (byte, char, short, int) and their wrapper classes. Note that this excludes long.
    • enum type.
    • A String.
  6. In a switch statement case labels are optional, but if provided they must be compile time constants. If an enum is used the unqualified enum constant (e.g., VALUE_A and not Class.MyEnum.VALUE_A) must be used.

  7. In a switch statement the default block is optional, but if provided there can only be 1.

Working with Java Arrays

  1. Examples for array instantiation:

    int[] ia = new int[10];
    int[] ia = {0, 1, 2, 3, 4, 5};
    int[][] iaa = new int[2][3];
    int[][] iaa = new int[3][];
    int[][] iaa = new int[][]{new int[]{1,2}};
    int[][] iaa = {{1,2}};
  2. Members of an array object include:

    • length
    • clone
  3. Arrays have two interesting runtime properties:

    • They are reified meaning that type checking is done at runtime by the JVM and not the compiler.
    • They are covariant meaning that a subclass object can be stored in an array that is declared to be the type of its superclass (i.e., you can store an integer in a number array).
  4. The compare method compares 2 int arrays and returns 0 if they are equal, a value less than 0 if the first array is lexicographically less than the second array, and a value greater than 0 if the first array is lexicographically greater than the second array.

  5. The mismatch method takes two int arrays and returns the index of the first mismatch, otherwise it returns -1 if no mismatch is found.

Describing and Using Objects and Classes

  1. When an object is instantiated the JVM allocates the necessary heap space to contain the various fields defined in the class. An example of instantiation is shown below:

    new java.lang.Object();
  2. In Java if you do not specify any reference variable explicitly within any instance method, the JVM assumes that you mean to access the same object for which the method has been invoked. You can make this explicit with the this keyword.

  3. The structure of a Java source file is:

    • At most one package statement.
    • Zero or more import statements.
    • One or more reference type (i.e., class, interface, or enum) definitions.
  4. Members of a class definition include field declarations, methods, constructors, and initialisers. Members can be static or non-static.

  5. The code for a top-level public reference type must be written inside a Java file with the same name.

  6. Java has three visibility scopes for variables - class, method, and block. Java has five lifespan scopes for variables - class, instance, method, for loop, and block.

  7. From Java 10 you can use a var declaration to infer types in a local method. An example is shown below:

    var str = "Java 11";
  8. If there are no more references to an Object, the JVM concludes that the object is not required anymore and is marked as garbage. The object is then removed using garbage collection.

Creating and Using Methods

  1. The basic structure of a method is shown below:

    returnType methodName(parameters){
        methodBody
    }
  2. The method must return a value of the type specified, with the following exceptions:

    • If the type is numeric then the return value can be one of any other numeric type as long as the type of the return value is smaller than the declared type.
    • Wrapper classes and primitives are interchangeable.
    • A subtype of the declared type can be returned (referred to as covariant return).
  3. The method signature includes the method name and its ordered list of parameter types. Where multiple methods exist with the same name but different parameter types, it is said that the class has overloaded the method name.

  4. If the type but not exact number of parameters is known, the varargs syntax can be used. This is represented by 3 dots after a data type. Restrictions on the usage of varargs are:

    • A method cannot have more than 1 varargs parameter.
    • The varargs parameter must be the last parameter in the parameter list.
  5. The following rules used by the compiler to disambiguate method calls to overloaded methods:

    • A compilation error will occur if the compiler is not able to successfully disambiguate a particular method call.
    • If the compiler finds a method whose parameter list is an exact match to the argument list of the method call, that method is selected.
    • If more than one method is capable of accepting a method call and none of them are an exact match, the one that is most specific is chosen by the compiler.
    • Higher priority is given to primitive versions if the argument can be widened to the method parameter type, this occurs before autoboxing is considered.
    • Autoboxing is considered before varargs.
  6. When a new instance of a class is created, the JVM does four things:

    • Checks if the class has been initialised, if not, loads and initialises the class.
    • Allocates the memory to hold the instance variables of the class in the heap space.
    • Initialises the instance variables to their default values.
    • Gives the instance an opportunity to set the values of the instance variables as per the instance initialisers and constructors.
  7. An instance initialiser is shown below:

    class TestClass{
        {
            System.out.println("In instance initialiser");
        }
    }
  8. An instance variable is not allowed to use the value of a variable if that variable is declared below the initialiser. It can assign a value to such a variable. An instance initialiser is not expected to throw any exceptions. Instance initialisers should generally be avoided, and a well-designed class should not need to use them.

  9. A constructor is a method that always has the same name as the class and does not have a return type.

  10. A no argument constructor is defined by default only if no constructors are provided explicitly.

  11. Constructors can be overloaded through constructor overloading. When a constructor calls another constructor of the same class, this is done with the this keyword and is called constructor chaining. The call to another constructor must be the first line of code in a constructor.

  12. Java forces the programmer to assign a value to a final variable explicitly. A static variable can be used by other classes only after the class is loaded made ready to use. You can assign a value to a final static variable at the time of declaration or in any one of the static initialisers.

  13. Access to static members is decided by the compiler at compile time by checking the declared type of the variable. This is referred to as static binding. Static binding uses type information to bind a method to a method call, as opposed to dynamic binding which considers the actual object.

  14. The first step in creating an instance of a class is to initialise the class itself. Whenever the JVM encounters the usage of a class for the first time, it allocates and initialises space for the static fields of that class. The rules for static blocks:

    • A class can have any number of static blocks. They are executed in the order that they appear in the class.
    • A static block can access all static variables and static methods of the class. However, if the declaration of a static variable appears after the static block, then only the value can be set.
    • If the class has a superclass, and the superclass has not been initialised already, the JVM will initialise the superclass first.
    • There is no way to access or refer to a static block. It can only be invoked by the JVM, and only once.

Applying Encapsulation

  1. Encapsulation, Inheritance and Polymorphism are the three pillars of Object-Orientated Programming.

  2. Encapsulation is about restricting direct access to an objects data fields. This is achieved using of access modifiers. Access modifiers and their impact on accessibility are shown below:

    Modifier Class Package Subclass World
    public Y Y Y Y
    protected Y Y Y N
    no modifier Y Y N N
    private Y N N N
  3. A top-level class, interface or enum can only have a public or default access modifier.

  4. Members of an interface are always public, even if not declared that way. The compiler will generate an error if you define them as private or protected. From Java 9 an interface can have private methods.

  5. Enum constants are always public, even if not declared that way. The compiler will generate an error if you define them as private or protected. Enum constructors are always private.

  6. Encapsulation encourages loose coupling between classes. If the functionality of a class is exposed through methods, there are two advantages:

    • The implementation can be modified without users being aware (i.e., the implementation details of the functionality are hidden from the users).
    • The value of a variable can be ensured to be consistent with the business logic of the class. For example, you could restrict setting the age of a Person class instance from being a negative number.

Reusing Implementations Through Inheritance

  1. A class defines a type and contains a state (the instance fields) and the implementation (the methods). Thus, inheritance could be of state, implementation, or type.

  2. Java restricts a class from extending more than one class, so it is said that Java does not support multiple inheritance of state. A class can inherit implementation by extending a class and/or by implementing interfaces. As a class can implement multiple interfaces, Java supports multiple implementation inheritance. Java also supports multiple inheritance of type as an object can have multiple types: the type of its own class and the types of all the interfaces that the class implements.

  3. To inherit features from another class, that class is extended using the extends keyword. Constructors, static and instance initialisers of a class are not considered members of a class so are not inherited by a subclass. Only class members that are visible to another class as per the rules of access modifiers are inherited in a subclass.

  4. Note that when the JVM initialises a class, memory is allocated for all instance variables irrespective of whether they will be accessible.

  5. A subclass cannot be initialised before its parent. A call to super() is automatically inserted by the compiler in the first line of the constructor if no other call is provided.

  6. The order of initialisation for loading a class is summarised below:

    • If there is a super class, initialise static fields and execute static initialisers of the super class in the order of their appearance.
    • Initialise static fields and execute static initialisers of the class in the order of their appearance.
    • If there is a super class, initialise the instance fields and execute instance initialisers of the super class in the order of their appearance.
    • Execute the super class's constructor.
    • Initialise the instance fields and execute instance initialisers of the class in the order of their appearance.
    • Execute class constructor.
  7. An abstract class is used to capture common features of multiple related object types while knowing that no object that exhibits only the feature captured in the abstract class can exist.

  8. An abstract method allows you to capture declaration without providing implementation.

  9. A summary of the application of access modifiers, final, abstract, and static keywords is shown below:

    • An abstract class doesn't have to have an abstract method but if a class has an abstract method, it must be declared abstract. In other words, a concrete class cannot have an abstract method.
    • An abstract class cannot be instantiated irrespective of whether it has an abstract method or not.
    • A final class or a final method cannot be abstract.
    • A final class cannot contain an abstract method but an abstract class may contain a final method.
    • A private method is implicitly final.
    • A private method can never be abstract.
    • A static method can be final but can never be abstract.
  10. The conventional sequence of modifiers in a method declaration is shown below:

    <access modifier> <static> <final or abstract> <return type> methodName(<parameter list>)
  11. Polymorphism refers to the ability of an object to exhibit behaviour associated with different types. The objective of polymorphism is to enable classes to become standardised components that can be easily exchanged without any impact on other components.

  12. Polymorphism works because of dynamic binding of method calls. When you invoke an instance method using a reference variable, it is not the compiler, but the JVM that determines which code to execute based on the class of the actual object referenced by the variable. In Java, calls to non-private and non-final instance methods are dynamically bound. Everything else is statically bound at compile time by the compiler.

  13. Static methods, static variables, and instance variables are accessed as per the declared type of the variable through which they are accessed and not according to the actual type of the object to which the variable refers. Contrast this with methods, where the actual type of the object determines which instance method is used.

  14. A class can completely replace the behaviour of an instance method that it inherited by providing its own implementation of that method. This is known as overriding. The rules for overriding a method are shown below:

    • An overriding method must not be less accessible than the overridden method.
    • The return type of the overriding method must be a covariant return of the overridden method.
    • The types and order of the parameter list must be the same.
    • An overriding method cannot put a wider exception in its throws clause than the ones present in the throws clause of the overridden method.
  15. A class can hide static methods and variables and instance variables. Basically, static methods are hidden, non-static methods are overridden.

  16. The purpose of casting is to provide the compiler with the type information of the actual object to which a variable will be pointing at run time. When you cast a reference to another type, you are basically saying that the program does something that is not evident from the code itself. Ideally, you should never need to use casting.

  17. The instanceof operator can be used to check if an object is an instance of a particular reference type. An example is shown below:

    if(f instanceof Mango){
    }

Programming Abstractly Through Interfaces

  1. An interface is used to describe behaviour as captured by a group of objects. An example is shown below:

    interface Movable{
        void move(int x);
    }
  2. From an OOP perspective an interface should not contain any implementation. It should only contain method declarations. However, Java allows interfaces to contain static fields as well as default, static, and private methods.

  3. Key rules for interfaces are shown below:

    • Members of an interface are always public, even if not declared that way. The compiler will generate an error if you define them as private or protected. From Java 9 an interface can have private methods.
    • An interface is always abstract.
    • All variables are implicitly public, static, and final. Static methods are assumed to be public if not otherwise marked. Non-static methods must be explicitly marked private or default.
  4. Methods that interfaces can contain are shown below:

    • Abstract methods: Contain only declarations. An example is shown below:

      interface Movable{
          void move(int x); // implicitly abstract
          abstract void move2(int x); // explicitly abstract
      }
    • Default methods: The opposite of abstract, a method cannot be both default and abstract. An example is shown below:

      interface Movable{
          default move(int x){
              System.out.println("Moving by "+x+" points");  
          }
      }
    • Static methods: Can be public or private but not protected or default. This is because static methods are not inherited or overridden in any meaningful way, and because protected methods can be seen by classes in the same package, and which extend the class containing the method. However, a static method cannot be inherited so the package-private access modifier should be used instead of protected, as the only remaining intention is for it to be accessible within the same package. If no access modifier is specified, they are implicitly public. An example is shown below:

      interface Movable{
          static void sayHello(){
              System.out.println("Hello!");  
          }
      }
    • Private methods: Helpful when methods get too big and need to be refactored into smaller internal methods. An example is shown below:

      interface Movable{
          private void moveInternal(){
              System.out.println("in moveInternal");  
          }
          default void move(int n){
              while(n-->0) moveInternal();
          }
      }
  5. An empty interface is known as a marker interface. The most common marker interface used in Java is java.io.Serializable. It signifies to the JVM that objects of classes implementing this interface can be serialised and deserialised.

  6. A class can implement any number of interfaces. Once a class declares that it implements an interface, it must have an implementation for all the abstract methods declared in that interface. Implementation could be inherited from an ancestor class. If the class does not have implementation for even one of the methods, the class must be declared abstract. An example is shown below:

    interface Movable{
        void Move();
    }
    
    interface Readable{
        void read();
    }
    
    class Price implements Movable, Readable{
        public void move(){
            System.out.println("Moving...");
        }
        public void read(){
            System.out.println("Reading...");
        }
    }
  7. Unlike the static methods of a class, the static methods of an interface cannot be inherited. Multiple fields with the same name can be inherited if they are not used ambiguously.

  8. An interface can extend any number of interfaces. The extending interface inherits all members except static methods of each of the extended interfaces.

  9. An interface defines behaviour. An interface tells you nothing about the actual object other than how it behaves. An abstract class defines an object which in turn drives the behaviour.

  10. An ArrayList is a class that implements java.util.List, which in turn extends java.util.Collection.

  11. A parametrised class uses a type parameter instead of the data type. An example is shown below:

    public class DataHolder<E>{
        E data;
        public E getData() { return data; }
        public void setData(E e){ data = e; }
    }
    
    public class SomeClass<E>{
        public static void consumeData(DataHolder<String> stringData){
            String s = stringData.getData(); // no cast required
        }
    }
  12. This allows a class to be written in a generic fashion, without hardcoding it to any type. This also allows that class to be typed to any type as per the requirement at the time of use.

  13. A parametrised method is like a parametrised class. The only difference is that the type parameter is valid only for that method instead of the whole class.

  14. All generic information is stripped at run time, this is known as type erasure. This means that the presence of generics can throw up complicated situations with respect to Overloading and Overriding.

  15. The java.util.Collection interface is the root interface in the collection hierarchy. The java.util.List interface defines the behaviour of collections that keep objects in an order. The functionality is implemented by classes such as ArrayList and LinkedList.

  16. Useful List methods are shown below:

    E get(int index);
    E set(int index, E e);
    boolean add(E e);
    void add(int index, E e);
    boolean addAll(Collection<? extends E> c);
    void addAll(int index, Collection<? extends E> c);
    E remove(int index);
    boolean remove(Object obj);
    boolean removeAll(Collection<?> c);
    boolean retainAll(Collection c);
    void clear();
    int size();
    default void forEach(Consume<? super T> action);
    boolean isEmpty();
    boolean contains(Object o);
    boolean containsAll(Collection c);
    List subList(int fromIndex, int toIndex);
    int indexOf(Object o);
    int lastIndexOf(Object o);
    Object[] toArray();
    <T> T[] toArray(T[] a);
  17. The of and copyOf methods take 0 to 10 parameters and return an unmodifiable list.

  18. ArrayList constructors are shown below:

    ArrayList();
    ArrayList(Collection c);
    ArrayList(int initialCapacity);
  19. Advantages of ArrayList over an array include dynamic sizing, type safety, and readymade features. Disadvantages of ArrayList over an array include higher memory usage, no type safety, and no support for primitive values.

  20. The java.util.HashMap class implements the java.util.Map interface. Map does not extend the Collection interface. Useful Map methods are shown below:

    V get(Object key);
    V put(K key, V value);
    V remove(Object key);
    Set<K> keySet();
    Collection<V> values();
    void clear();
    int size();
    default void forEach(BiConsumer<? super K, ? super V> action);
  21. A lambda expression is a shortcut for the compiler that defines a class with a method and instantiates that class. The below is an example of a class that implements a car shop:

    class Car {
      String company;
      int year;
      double price;
      String type;
    
      Car(String c, int y, double p, String t) {
        this.company = c;
        this.year = y;
        this.price = p;
        this.type = t;
      }
    
      public String toString() {
        return "(" + company + "" + year + ")";
      }
    }
    
    class CarMall {
      List<Car> cars = new ArrayList<>();
    
      CarMall() {
        cars.add(new Car("Honda", 2012, 9000.0, "HATCH"));
        cars.add(new Car("Honda", 2018, 17000.0, "SEDAN"));
        cars.add(new Car("Toyota", 2014, 19000.0, "SUV"));
        cars.add(new Car("Ford", 2014, 13000.0, "SPORTS"));
        cars.add(new Car("Nissan", 2017, 8000.0, "SUV"));
      }
    
      List<Car> showCars(CarFilter cf) {
        ArrayList<Car> carsToShow = new ArrayList<>();
        for (Car c : cars) {
          if (cf.showCar(c))
            carsToShow.add(c);
        }
        return carsToShow;
      }
    }
    
    interface CarFilter {
      boolean showCar(Car c);
    }
  22. The showCars method returns a list of cars based on any given criteria. By accepting an interface as an argument, the showCars method lets the caller decide the criteria for the search. TestClass below represents a third party class that uses CarMall:

    public class TestClass {
        public static void main(String[] args) {
        CarMall cm = new CarMall();
        CarFilter cf = new CompanyFilter("Honda");
        List<Car> carsByCompany = cm.showCars(cf);
        System.out.println(carsByCompany);
        }
    }
    
    class CompanyFilter implements CarFilter {
      private String company;
    
      public CompanyFilter(String c) {
        this.company = c;
      }
    
      public boolean showCar(Car c) {
        return company.contentEquals(c.company);
      }
    }
  23. Observe that the above implementation of CarFilter can be replaced with the below:

    public class TestClass {
        public static void main(String[] args) {
            CarMall cm = new CarMall();
            List<Car> carsByCompany = cm.showCars(c -> c.company.contentEquals("Honda"));
            System.out.println(carsByCompany);
        }
    }
  24. The compiler knows that the showCars method must pass an object of a class that implements CarFilter. As a result it is easy to generate the below:

    class XYZ implements CarFilter{
        public boolean showCar(Car <<parameterName>>){
            return <<an expression that returns a boolean>>;
    }
  25. The parameterName and expression are contained within the lambda expression.

  26. A lambda expression can be written only where the target type is an interface with exactly one abstract method. Such an interface is known as a functional interface.

  27. The syntax for a lambda expression is the variable declarations on the left side of the arrow operator and the right side for the code that you want execution.

  28. Multiple lines of code must be written within curly braces. Parameter options are shown below:

    () -> true // no parameter
    a -> a*a // 1 parameter
    (a) -> a*a // 1 parameter
    (int a) -> a*a // 1 parameter
    (a, b, c) -> a + b + c // multiple parameters
    (var a) -> a*a // var is also allowed
  29. Filtering a list of objects is so common that Java provides a generic interface java.util.function.Predicate for this purpose. This is shown below:

    interface Predicate<T>{
        boolean test(T t);
    }
  30. This allows us to remove the CarFilter interface in the above example:

    List<Car> showCars(Predicate<Car> cp) {
      ArrayList<Car> carsToShow = new ArrayList<>();
      for (Car c : cars) {
        if (cp.test(c)){
          carsToShow.add(c);
        }
      }
      return carsToShow;
    }
  31. Other methods for the Predicate interface are shown below:

    default Predicate<T> and(Predicate<? super T> other);
    default Predicate<T> negate();
    default Predicate<T> or(Predicate<? super T> other);
    static <T> Predicate<T> isEqual(Object targetRef);   
  32. The Predicate interface is an example of a functional interface. They are called functional interfaces because they represent a single function and are for doing exactly one thing.

Handling Exceptions

  1. Java exceptions are designed to help you write code that covers all possible execution paths. This includes normal operations, exceptional situations, and unknown exceptional situations. This is shown below:

    try{    
        // code for normal course of action
    } catch(SecurityException se) {
        // code for known exceptional situation
        System.out.println("No permission!");
    }
    catch(Throwable t) {
        // code for unknown exceptional situations
        System.out.println("Some problem in copying: "+t.getMessage());
        t.printStackTrace();
    }
  2. When developing code there is always the provider and the client. Exceptions are a means for the provider to let the client know about any exceptional events and allow the client to determine how they want to deal with them. The mechanism to let the client know is to throw an exception. An example is shown below:

    public void copyFile(String inputPath, String outputPath) throws IOException {
        // code to copy file
    }
  3. If the client can resolve the situation a catch statement can be used. If the client cannot handle the exceptional situation either, the exception can be propagated to the client's client. An exception is considered handled when it is caught in a catch block.

  4. The throw statement is used to explicitly throw an exception. Throwing an exception implies that the code has encountered an unexpected situation with which it does not want to deal. Java requires that you list the exceptions that a method might throw in the throws clause of that method. This is shown below:

    public double computeSimpleInterest (double p, double r, double t) throws Exception{
        if(t<0) {
            throw new IllegalArgumentException("time is less than 0");
        }
        // other code
    }
  5. A try statement provides an opportunity to recover from an exceptional situation that arises while executing a block of code.

    try {
        // code that might throw exceptions
    } catch(<ExceptionClass1> e1){
        // code to execute if code in try throws exception 1
    } catch(<ExceptionClass2> e2){
        // code to execute if code in try throws exception 2
    } catch(<ExceptionClassN> en){
        // code to execute if code in try throws exception N
    } finally{
        // code to execute after try and catch blocks finish execution
    }
  6. The java.lang.Throwable class is the root of all exceptions. A Throwable object includes the chain of the method calls that led to the exception (known as the "stack trace") and any informational message specified by the programmer.

  7. The Throwable hierarchy is shown below:

  8. Generally checked exceptions are those that extend java.lang.Throwable but do not extend java.lang.RuntimeException or java.lang.Error. The remaining exceptions are checked exceptions. Checked exceptions must be declared in the throws clause of the method or caught in a catch block within the method.

Understanding Modules

  1. JAR files are used to package multiple classes into a file and deliver the file as an application to users. A JAR file is a zip file with a special directory named META-INF. This directory contains one or more files with MAINIFEST.MF always being one of them. The manifest contains extra information about the JAR file such as the version of Java used to build the file, and the class with the main() method. Each line in the manifest is a key/value pair separated by a colon.

  2. Packaging classes into a JAR file without a well thought out plan can give rise to unwieldy applications that are difficult to update and/or reuse. JAR files also make it difficult to use part of an application without having the whole set of JAR files. Various Java community projects such as Ant, Maven, and Graven have attempted to provide a way of managing these issues.

  3. In Java 9 the Java Module System was introduced to assist with these problems. The idea of Modules is that classes are mixed and matched at run time to form an application.

  4. A module is declared using the module-info.java file. By convention, this file is placed in a folder with the same name as the module name. An example of the contents of this file is shown below:

    module simpleinterest{
    }
  5. An example of the file structure for a module shown below:

  6. The module is compiled using the below:

    javac -d out --module-source-path src --module simpleinterest
  7. The above command used 3 switches. The -d switch directed the output to a particular directory, the --module-source-path switch provided the location of the source module definition, and the --module switch specified the name of the module to compile. Although not used here, the --module-path (or -p for short) switch would be used to specify the location of modules required by the module that is being compiled. The -m switch specifies a name of the nodule.

  8. A valid module name consists of Java identifiers separated by ".". A Java identifier cannot start with a number or contain a dash (-).

  9. The file structure after compilation is shown below:

  10. The module can be run using the below:

    java --module-path out --module simpleinterest/simpleinterest.SimpleInterestCalculator
  11. Note that the format of the --module switch argument is <module name>/<main class name>.

  12. The module is compiled into a JAR using the below:

    jar --create --file simpleinterest.jar --main-class simpleinterest.SimpleInterestCalculator -C out\simpleinterest
  13. The above command used 4 switches. The --create switch tells the JAR tool to create, the --file switch specifies the name of the file, the --main-class switch adds a Main-Class entry in the JAR file manifest, and the -C switch makes the JAR tool change working directories so that the structure inside the JAR file is the same as the structure inside out\simpleinterest.

  14. The module can now be run using the below:

    java --module-path . --module simpleinterest
  15. It is a good design practise to define functionality in the form of an interface and let the actual implementation implement that interface. Separating the interface and the implementation into separate modules allows us to build an application by mixing and matching modules without the need to bundle classes that are not required for the application. An interface is added as shown below:

    package calculators;
    public interface InterestCalculator{
        public double calculate(double principle, double rate, double time);
    }
  16. The exports clause allows the public types within a package be eligible to be accessible by other modules. A module can only export packages, and not individual types. The contents of module-info.java for the calculators module is shown below:

    module calculators{
        exports calculators;
    }
  17. The requires clause is the counterpart of the exports clause. The purpose of having a requires class is to make the dependencies of a module explicitly clear to the users. The contents of module-info.java for the simpleinterest module is shown below:

    module simpleinterest{
        requires calculators;
    }
  18. A simplified SimpleInterestCalculator.java is shown below:

    package simpleinterest;
    import calculators.InterestCalculator;
    public class SimpleInterestCalculator implements InterestCalculator{
        public double calculate(double principle, double rate, double time){
            return principle*rate*time;
        }
        public static void main(String[] args){
            InterestCalculator ic = new SimpleInterestCalculator();
            System.out.println(ic.calculate(100, .05, 2));
        }
    }
  19. The directory structure after compilation is shown below:

  20. The exports clause allows any other module to require it. The Java module systems allows you to fine tune access to a module only to specific modules using a variation of the exports clause. This is shown below:

    module <modulename>{
        exports <packagename> to <modulename(s)>;
    }
  21. If a module A reads another module B, and module B reads another module C, module A does not read module C. That is to say that dependencies are not transitive. An example is shown below:

    module ui{
        requires hr;
    }
    
    module hr{
        requires valueobjects;
        exports hrservice;
    }
    
    // code appearing in a class in the ui module
    HRService hrService = new HRService(); // HRService is defined in hr module
    Employee e = hrService.getEmployee(employeeId); // Employee is defined in valueObjects module
  22. In this case the ui module does not have a requires valueobjects, so the ui module cannot access the Employee class from the valueobjects module. This code will fail to compile. A requires valueobjects could be added to the ui module, but there could be many requires clauses in the hr module. Only multiple compilation failures can make this information known to the ui module.

  23. A requires transitive clause allows you to specify that if a module depends on another module, any module that depends on it should also depend on the other module. An example is shown below

    module hr{
        requires transitive valueobjects;
        exports hrservice;
    }
  24. This has the added advantage of not requiring a requires valueobjects clause in the ui module. The requires hr clause in the ui module automatically makes all the modules transitively required by the hr module, readable to the ui module. This is called implied readability.

  25. A modular JAR file can be ran using the -classpath or -jar options, however the JVM will not enforce the access rules specified in the module descriptor.

  26. A common use case is wanting to develop a module that depends on a third-party non-modular JAR. If you put a non-modular JAR on the module-path, Java will consider the non-modular JAR to be a module. Such a module is known as an automatic module or a named module and the name of the module is created automatically using the name of the JAR file.

  27. As there is no module-info.class in a non-modular JAR, an automatic module exports all its packages and can read all exported packages of modules on the module-path and classes available on the classpath.

  28. If a module depends on a non-modular third-party JAR, you need to add a requires clause in module-info and put the third-party JAR in the --module-path. If additionally, the automatic module requires a class from another non-modular JAR, that JAR needs to be included on the classpath.

  29. The specification of Standard modules is governed by the Java Community Process (JCP). Standard modules have names starting with "java". All other modules are part of the JDK and have names starting with "jdk". A standard module may contain both standard and non-standard API packages, however if the standard module exports a non-standard package then the export must be qualified. A standard module must not grant implied readability to any non-standard module. A non-standard module must not export any standard API packages.

Understanding Java Technology and Environment

  1. Java code is compiled into Java bytecode, which is interpreted by the JVM. A class file produced on one platform will run on any platform that has a JVM.

  2. Java is a separate application installed on top of an Operating System.

  3. The Java Runtime Environment (JRE) includes the class libraries and executables that are required to run a Java program while the Java Development Kit (JDK) includes tools such as the Java compiler and the Java debugger.

Java SE 11 Programmer II

Java Fundamentals

  1. A final variable does not need to be assigned when it is declared, only before it is used. A variable reference being marked as final does not mean the associated object cannot be modified. If an instance variable is final, then it must be assigned a value when it is declared or when the object is instantiated. Similarly, static variables must be assigned a value when declared or in a static initialiser.

  2. Methods marked final cannot be overridden by a subclass. This essentially prevents any polymorphic behaviour on the method call and ensures that a specific version of the method is always called. The opposite of a final method is an abstract method as an abstract method must be implemented.

  3. A final class cannot be extended. A class cannot be both abstract and final.

  4. An enum can be used to specify a fixed set of constants. Using an enum is better than using constants because it provides type-safe checking. Another advantage of an enum is the enum value can be used in a switch statement. An enum can contain methods but the first line must be the list of values. Note that if an enum has a body (e.g. default value) then the semicolon becomes mandatory.

    public enum Season{
        WINTER, SPRING, SUMMER, FALL
    }
  5. A nested class is one that is defined within another class. There are 4 types:

    • Inner class: A non-static type defined at the member level.
    • Static nested class: A static typed defined at the member level.
    • Local class: A class defined within a method body.
    • Anonymous class: A special case of a local class that does not have a name.
  6. An inner class cannot declare static fields or methods, except for static final fields. It can also access members of the outer class including private methods. An inner classes will result in an Outer$Inner.class file being created by the compiler. As inner class can have the same variable names as outer classes, a call to this is prefixed with the class name.

  7. A static nested class can be instantiated without an instance of the enclosing class. However, it cannot access the instance variables or methods in the outer class directly. It requires an explicit reference to the outer class variable. The nesting creates a namespace because the enclosing class name must be used to refer to it.

  8. A local class is declared in a method, constructor, or initialiser. A local class does not have any access modifiers and cannot be declared static or declare static fields unless they are static final. When defined in an instance method, they have access to all fields and methods of the enclosing class. They can access local variables only if the variables are final or effectively final. An effectively final variable is one whose value does not change after it is set.

  9. An anonymous class is a special form of a local class that does not have a name. It is declared and instantiated in one statement using the new keyword, a type name with parentheses, and a set of braces. Anonymous classes are required to extend an existing class or implement an existing interface.

  10. The rules for modifiers in nested classes are summarised below:

  11. The rules for members in nested classes are summarised below:

  12. The rules for access in nested classes are summarised below:

  13. When Java was first released, there were only two types of members an interface declaration could include: abstract methods and static final variables. Since Java 8 and 9, new method types have been added. The interface member types are summarised below:

  14. A default method may be declared within an interface to provide a default implementation. The default method is assumed to be public and cannot be marked abstract, final, or static. It may also be overridden by a class that implements the interface. If a class inherits two or more default methods with the same method signature, then the class must override the method.

  15. To call a default method from a class which overrides the implementation:

    interfaceName.super.methodName();
  16. A static interface method must include a method body and is assumed to be public. It cannot be marked abstract or final and cannot be referenced without using the interface name. Static interface methods are not inherited by a class implementing the interface.

  17. A private interface method is used to avoid code duplication in instance methods. It must be marked private and include a method body. It may only be called by default and private (non-static) methods within the interface definition.

  18. A private static method is used to avoid code duplication in static methods. It must be marked private and static and may only be called by other methods within the interface definition.

  19. The rules for interface member access are summarised below:

  20. A functional interface is an interface that contains a single abstract method. A lambda expression is like an anonymous class that defines one method. Any functional interface can be implemented as a lambda expression.

  21. Note that if a functional method includes an abstract method with the same signature as a public method found in Object, then those methods do not count towards the single abstract method test. This includes:

    String toString()
    boolean equals(Object)
    int hashCode()
  22. A lambda expression contains a parameter name, arrow, and body. The parameters list the variables, which must be compatible with the type and number of input parameters of the functional interface's single abstract method. The body must also be compatible with the return type of the functional interface's abstract method. Example lambda expressions are shown below:

    a -> a.canHop()
    (Animal a) -> {return a.canHop();}
  23. A var parameter can be used in the parameter list, but then all parameters must use var. If the type is specified for one parameter, then it must be specified for all parameters. A semicolon is mandatory in the body if there is only a single expression. An expression-based lambda body is not terminated with a semicolon as it is an expression not a statement. However, a statement-based lambda with multiple lines requires that each statement be terminated with a semicolon.

Annotations

  1. Annotations are all about metadata. They let you assign metadata attributes to classes, methods, variables, and other Java types. An example is shown below:

    public class Mammal{}
    public class Bird{}
    
    @ZooAnimal public class Lion extends Mammal{}
    @ZooAnimal public class Peacock extends Bird{}
  2. The above could have been achieved by extending a ZooAnimal class but that would require the class hierarchy to be changed. Annotations are like interfaces. While interface can be applied to classes, annotations can be applied to classes, methods, expressions, and even other annotations. Annotations also allow a set of values to be passed. An example is shown below:

    public class Veterinarian{
        @ZooAnimal(habitat="Infirmary") private Lion sickLion;
        @ZooAnimal(habitat="Safari") private Lion healthyLion;
        @ZooAnimal(habitat="Special Enclosure") private Lion blindLion;
    }
  3. The values are part of the type declaration and not of the variable. Without annotations, a new Lion type for each habitat value would need to be defined. This would become difficult in large applications.

  4. To declare an annotation the @interface annotation is used. An example is shown below:

    public @interface Exercise{}
    }
  5. To apply the annotation to other code we simply use the @Exercise annotation. A parenthesis is required if there are elements specified, and optional otherwise. Examples are shown below:

    @Exercise() public class Cheetah{}
    @Exercise public class Sloth{}
    @Exercise
    public class ZooEmployee{}
  6. To declare an annotation with elements the elements need to be available in the annotation declaration. An example is shown below:

    public @interface Exercise{}
        int hoursPerDay();
    }
  7. This changes how the annotation is used. An example is shown below:

    @Exercise(hoursPerDay=3) public class Cheetah{}
  8. When declaring an annotation, any element without a default value is considered required. A default value must be a non-null constant expression. An example including a default value is shown below:

    public @interface Exercise{}
        int hoursPerDay();
        int startHour() default 6;
    }
  9. The element type must be a primitive type, a String, a Class, an enum, another annotation, or an array of any of these types. Note that this excludes wrapper classes and arrays of arrays.

  10. Like abstract interface methods, annotation elements are implicitly abstract and public. Declaring elements protected, private or final will result in a compilation failure.

  11. Like interface variables, annotation variables are implicitly public, static, and final. A constant variable can be declared in an annotation but are not considered elements.

  12. A shorthand format exists for using annotations. This can occur if the annotation declaration contains an element named value(), the usage of the annotation provides no values for other elements, and the declaration does not contain any elements that are required. An example is shown below:

    public @interface Injured{
        String veterinarian() default "unassigned";
        String value() default "foot";
        int age() default 1;
    }
    
    @Injured("Legs") public void fallDown() {}
  13. A shorthand format also exists for providing an array that contains a single element. An example is shown below:

    public @interface Music{
        String[] genres();
    }
    
    public class Giraffe{
        @Music(genres={"Rock and roll"}) String mostDisliked;
        @Music(genres="Classical") String favorite;
    }
  14. An annotation can be applied to an annotation to specify what types the annotation can be applied to. This is done by specifying the ElementType using @Target. An example is shown below:

    @Target({ElementType.METHOD,ElementType.CONSTRUCTOR})
    public @interface ZooAttraction{}
  15. The options for ElementType are shown below:

  16. The TYPE_USE value covers nearly all other values. One exception is that it can only be used on a method that returns a value, a void method would still need METHOD defined in the annotation. TYPE_USE is typically used for cast operations, object creation with new and inside type declarations.

  17. The compiler discards certain types of information when converting source code into a .class file. Annotations may be discarded by the compiler at runtime. The @Retention annotation can be used to specify. The options for @Retention are shown below:

  18. Javadoc is a built-in standard within Java that generates documentation for a class or API. If the @Documented annotation is present, then the generated Javadoc will include annotation information defined on Java types. An example is shown below:

    // Hunter.java
    import java.lang.annotation.Documented;
    @Documented public @interface Hunter{}
    
    // Lion.java
    @Hunter public class Lion{}
  19. In the above example @Hunter would be published with the Lion Javadoc information because it is marked with @Documented.

  20. The @Inherited annotation is used to allow subclasses to inherit the annotation information found in the parent class.

    // Vertebrate.java
    import java.lang.annotation.Inherited;
    @Inherited public @interface Vertebrate{}
    
    // Mammal.java
    @Vertebrate public class Mammal{}
    
    // Dolphin.java
    public class Dolphin extends Mammal{}
  21. In the above example the @Vertebrate annotation will be applied to both Mammal and Dolphin.

  22. The @Repeatable annotation can be used to apply an annotation more than once. To declare a @Repeatable annotation, a containing annotation with type value must be defined.

    // Containing annotation type
    public @interface Risks{
        Risk[] value();
    }
    
    // Containing annotation class
    @Repeatable(Risks.class)
    public @interface Risk{
        String danger();
        int level() default 1;
    }
    
    public class Zoo{
        public static class Monkey{}
        @Risk(danger="Silly")
        @Risk(danger="Aggressive",level=5)
        @Risk(danger="Violent",level=10)
        private Monkey monkey;
    }
  23. Commonly used built-in annotations are shown below:

  24. Example applications of commonly used annotations are shown below:

Generics and Collections

  1. Method references can make code easier to read. An example is shown below:

    @FunctionalInterface
    public interface LearnToSpeak{
        void speak(String sound);
    }
    
    public class DuckHelper{
        public static void teacher(String name, LearnToSpeak trainer){
            trainer.speak(name);
        }
    }
    
    // long version
    public class Duckling{
        public static void makeSound(String sound){
            LearnToSpeak learner = s -> System.out.println(s);
            DuckHelper.teacher(sound, learner);
        }
    }
    
    // short version
    public class Ducking{
        public static void makeSound(String sound){
            LearnToSpeak learner = System.out::println;
            DuckHelper.teacher(sound, learner);
        }
    }
  2. There are four formats for method references. Examples are shown below:

    // Static Methods
    Consumer<List<Integer>> methodRef = Collections::sort;
    Consumer<List<Integer>> lambda = x -> Collections.sort(x);
    
    // Instance Methods on a Particular Object
    var str = "abc";
    Predicate<String> methodRef = str::startsWith;
    Predicate<String> lambda = s -> str.startsWith(s);
    
    var random = new Random();
    Supplier<Integer> methodRef = random::nextInt;
    Supplier<Integer> lambda = () -> random.nextInt();
    
    // Instance Methods on a Parameter
    Predicate<String> methodRef = String::isEmpty;
    Predicate<String> lambda = s -> s.isEmpty();
    
    // Constructors
    Supplier<List<String>> methodRef = ArrayList::new;
    Supplier<List<String>> lambda = () -> new ArrayList();
  3. Each Java primitive has a corresponding wrapper class. A null value can be assigned to a wrapper class as a null value can be assigned to any reference variable. Attempting to unbox a wrapper class with a null value will cause a NullPointerException.

  4. The Diamond Operator is a shorthand notation that allows you to omit the generic type from the right side of a statement when the type can be inferred. An example is shown below:

    List<Integer> list = new ArrayList<Integer>();
    List<Integer> list = new ArrayList<>();
  5. A collection is a group of objects contained in a single object. The Java Collection Framework is a set of classes in java.util for storing collections. The common

    • List: Ordered collection of elements that can contain duplicates. Accessed by an int index.
    • Set: A collection that does not allow duplicate entries.
    • Queue: A collection that orders its elements in a specific order. A typical queue is FIFO.
    • Map: A collection that maps keys to values, with no duplicate keys allowed. The elements are key/value pairs.
  6. The Collection interface and its sub interfaces as well as some implementing classes are shown below. Interfaces are shown in rectangles, with classes in rounded boxes:

  7. The Collection interface contains useful convenience methods. These are shown below:

    boolean add(E element);
    boolean remove(Object object);
    boolean isEmpty();
    int size();
    void clear();
    boolean contains(Object object);
    boolean removeIf(Predicate<? super E> filter);
    void forEach(Consumer<? super T> action);
  8. The Collections.sort() method is commonly used when working with collections. To sort objects that you create yourself, Java provides an interface called Comparable. This is shown below:

    public interface Comparable<T>{
        int compareTo(T o);
    }
  9. The compareTo() method returns:

    • The number 0 when the current object is equivalent to the argument to compareTo().
    • A negative number when the current object is smaller than the argument to compareTo().
    • A positive number when the current object is larger than the argument to compareTo().
  10. An example is shown below:

    public class MissingDuck implements Comparable<MissingDuck>{
        private String name;
        public int compareTo(MissingDuck quack){
            if(quack == null)
                throw new IllegalArgumentException("Poorly formed duck!");
            if(this.name == null && quack.name == null)
                return 0;
            else if(this.name == null) return -1;
            else if(quack.name == null) return 1;
            else return name.compareTo(quack.name);
        }
    }
  11. Note that only one compareTo() method can be implemented for a class. If we want to sort by something else, a Comparator can be used. Comparator is a functional interface. An example is shown below:

    public static void main(String[] args){
        Comparator<Duck> byWeight = new Comparator<Duck>(){
            public int compare(Duck d1, Duck d2){
                return d1.getWeight()-d2.getWeight();
            }
        }
    };
    
    // alternate implementation with lambda
    Comparator<Duck> byWeight = (d1,d2) -> d1.getWeight()-d2.getWeight();
    
    // alternate implementation with method reference
    Comparator<Duck> byWeight = Comparator.comparing(Duck::getWeight);
    
    Collection.sort(ducks, byWeight);
  12. A summary of the differences between Comparable and Comparator are shown below:

  13. When building a comparator there are several helper methods that can be used. These are shown below:

    reversed();
    thenComparing(function);
    thenComparingDouble(function);
    thenComparingInt(function);
    thenComparing(function);
  14. Generics allow you to write and use parametrised types. This allows the compiler to detect issues rather than a ClassCastException exception being thrown.

  15. Generics can be introduced into classes using angle brackets. An example is shown below:

    public class Crate<T>{
        private T contents;
        public T emptyCrate(){
            return contents;
        }
        public void packCrate(T contents){
            this.contents = contents;
        }
    }
  16. A type parameter can have any name. By convention, the below letters are used:

    • E for an element
    • K for a map key
    • V for a map value
    • N for a number
    • T for a generic data type
    • S, U, V etc. for multiple generic types
  17. Generics can also be introduced into methods using angle brackets. An example is shown below:

    public class Handler{
        public static <T> Crate<T> ship(T t){
            System.out.println("Shipping " + t);
            return new Crate<T>();
        }
    }
  18. A bounded parameter type is a generic type that specifies a bound for the generic. A wildcard generic type is an unknown generic type represented with a question mark.

  19. An unbounded wildcard is used when any type is okay. An example is shown below:

    public static void printList(List<?> list){
        for (Object x:list)
            System.out.println(x);
    }
  20. Note that a generic type cannot use a subclass. An example that will not compile is shown below:

    ArrayList<Number> list = new ArrayList<Integer>();
  21. An upper-bounded wildcard can be used to say that any class that extends a class or that class itself can be the parameter type. An example is shown below:

    public static long total(List<? extends Number> list){
        long count = 0;
        for(Number number:list)
            count += number.longValue();
        return count;
    }
  22. Note that due to type erasure the above code is converted to something like:

    public static long total(List list){
        long count = 0;
        for(Object obj:list)
            Number number = (Number) obj;
            count += number.longValue();
        }
        return count;
    }
  23. When upper bounds or unbounded wildcards are used in such a way the list becomes immutable and cannot be modified.

  24. A lower-bounded wildcard can be used to say that any instance of a class or an instance of a superclass can be the parameter type. An example is shown below:

    public static void addSound(List<? super String> list){
        list.add("quack");
    }
    
    List<String> strings = new ArrayList<String>();
    strings.add("tweet");
    
    List<Object> objects = new ArrayList<Object>(strings);
    addSound(strings);
    addSound(objects);
  25. A useful mnemonic is PECS: Producer Extends, Consumer Super. If you need a List to produce T values (you want to read Ts from the list), you need to declare it using extends. If you need a list to consume T values (you want to write Ts into the list), you need to declare it using super. If you need to both read and write to a list, you need to declare it exactly with no wildcards.

  26. In the below example, if you want to write elements into the list, you cannot add a Number, Integer or a Double because each one is not compatible with all types. A Number can be read because any of the lists will contain a Number or a subclass of Number. When using extends like this you can only read, and not write.

    List<? extends Number> foo = new ArrayList<>();
    
    // The foo list could be one of these
    List<? super IOException> foo = new ArrayList<Number>();
    List<? super IOException> foo = new ArrayList<Integer>();
    List<? super IOException> foo = new ArrayList<Double();
  27. In the below example, if you want to write elements into the list, you can add an IOException or a FileNotFoundException but not an Exception. This is because an Exception cannot be added to a list of a more specific subclass. An Object can be read from this list but you won't know which type.

    List<? super IOException> foo = new ArrayList<>();
    
    // The foo list could be one of these
    List<? super IOException> foo = new ArrayList<Exception>();
    List<? super IOException> foo = new ArrayList<IOException>();
    List<? super IOException> foo = new ArrayList<Object>();

Functional Programming

  1. The functional interfaces shown below are provided as built-in functional interfaces in the java.util.function package:

  2. A Supplier is used when you want to generate or supply values without taking any input. A supplier is often used to construct new objects. The definition is shown below:

    @FunctionalInterface
    public interface Supplier<T>{
        T get();
    }
  3. An example for Supplier is shown below:

    Supplier<LocalDate> s1 = LocalDate::now;
    Supplier<LocalDate> s2 = () -> LocalDate.now();
    
    LocalDate d1 = s1.get();
    LocalDate d2 = s2.get();
  4. A Consumer is used when you want to do something with a parameter but not return anything. A BiConsumer is the same but takes two parameters. The definitions are shown below:

    @FunctionalInterface
    public interface Consumer<T>{
        void accept(T t);
        // default method omitted
    }
    
    @FunctionalInterface
    public interface BiConsumer<T, U>{
        void accept(T t, U u);
        // default method omitted
    }
  5. An example for Consumer is shown below:

    Consumer<String> c1 = System.out::println;
    Consumer<String> c2 = x-> System.out.println(x);
    
    c1.accept("Hi");
    c2.accept("Hi");
  6. An example for BiConsumer is shown below:

    var map = new HashMap<String, Integer>();
    BiConsumer<String, Integer> b1 = map::put;
    BiConsumer<String, Integer> b2 = (k, v) -> map.put(k, v);
    
    b1.accept("chicken", 7);
    b2.accept("chick", 1);
  7. A Predicate is often used when filtering or matching. A BiPredicate is the same but takes two parameters. The definitions are shown below:

    @FunctionalInterface
    public interface Predicate<T>{
        boolean test(T t);
        // default and static methods omitted
    }
    
    @FunctionalInterface
    public interface BiPredicate<T, U>{
        boolean test(T t, U t);
        // default methods omitted
    }
  8. An example for Predicate is shown below:

    Predicate<String> p1 = String::isEmpty;
    Predicate<String> p2 = x -> x.isEmpty();
    System.out.println(p1,test(""); // true
    System.out.println(p2,test(""); // true
  9. An example for BiPredicate is shown below:

    BiPredicate<String, String> b1 = String::startsWith;
    BiPredicate<String, String> b2 = (string, suffix) -> string.startsWith(prefix);
    
    System.out.println(b1.test("chicken", "chick")); // true
    System.out.println(b2.test("chicken", "chick")); // true
  10. A Function turns one parameter into a value of a potentially different type and returns it. A BiFunction turns two parameters into a value and returns it. The definitions are shown below:

    @FunctionalInterface
    public interface Function<T, R>{
        R apply(T t);
        // default and static methods omitted
    }
    
    @FunctionalInterface
    public interface BiFunction<T, U, R>{
        R apply(T t, U u);
        // default method omitted
    }
  11. An example for Function is shown below:

    Function<String, Integer> f1 = String::length;
    Function<String, Integer> f2 = x -> x.length();
    
    System.out.println(f1.apply("cat")); // 3
    System.out.println(f2.apply("cat")); // 3
  12. An example for BiFunction is shown below:

    BiFunction<String, <String, String> b1 = String::concat;
    BiFunction<String, <String, String> b2 = (string, toAdd) -> string.concat(toAdd);
    
    System.out.println(b1.apply("cat ", "dog")); // cat dog
    System.out.println(b2.apply("cat ", "dog")); // cat dog
  13. A UnaryOperator is a special case of a Function where all the type parameters are the same type. A BinaryOperator merges two values into one of the same type. The definitions are shown below:

    @FunctionalInterface
    public interface UnaryOperator<T> extends Function<T, T>{}
    
    @FunctionalInterface
    public interface BinaryOperator<T> extends BiFunction<T,T,T>{
    // omitted static methods
    }
  14. An example for UnaryOperator is shown below:

    UnaryOperator<String> u1 = String::toUpperCase;
    UnaryOperator<String> u2 = x -> x.toUpperCase();
    
    System.out.println(u1.apply("hi")); // HI
    System.out.println(u2.apply("hi")); // HI
  15. An example for BinaryOperator is shown below:

    BinaryOperator<String> b1 = String::concat;
    BinaryOperator<String> b2 = (string, toAdd) -> string.concat(toAdd);
    
    System.out.println(u1.apply("hi ", "there")); // hi there
    System.out.println(u2.apply("hi ", "there")); // hi there
  16. The built-in functional interfaces contain various helpful default methods. An example for the Predicate helper methods is shown below with the two statements being equivalent:

    Predicate<String> combination = s -> s.contains("cat") && ! s.contains("brown");
    Predicate<String> combination = cat.and(brown.negate());
  17. An example for the Consumer helper methods is shown below:

    Consumer<String> c1 = x -> System.out.print("1:" + x);
    Consumer<String> c2 = x -> System.out.print(",2:" + x);
    
    Consumer<String> combined = c1.andThen(c2);
    combined.accept("hi"); // 1:hi,2:hi
  18. An example for the Function helper methods is shown below:

    Function<Integer, Integer> before = x -> x + 1;
    Function<Integer, Integer> after = x -> x * 2;
    
    Function<Integer, Integer> combined = after.compose(before);
    System.out.println(combined.apply(3)); // 8
  19. An Optional type is used to express a result that could be "not applicable" without using null references. The Optional instance methods are summarised below:

  20. An example using the isPresent() and get() methods is shown below:

    public static Optional<Double> average(int... scores){
        if(scores.length == 0) return Optional.empty();
        int sum = 0;
        for(int score:scores) sum += score;
        return Optional.of((double) sum/scores.length);
    }
    
    Optional<Double> opt = average(90, 100);
    if(opt.isPresent())
        System.out.println(opt.get()); // 95.0
  21. The ofNullable() method can be used to return an empty Optional if the value is null:

    Optional o (value == null) ? Optional.empty() : Optional.of(value);
    Optional o = Optional.ofNullable(value);
  22. The ifPresent() method can be used to specify a Consumer to be run when there is a value inside of an Optional:

    Optional<Double> opt = average(90, 100);
    opt.ifPresent(System.out::println);
  23. There are multiple methods that can be used to handle an empty Optional. Note that if the value does exist then the value will just be printed. Examples are shown below:

    Optional<Double> opt = average();
    System.out.println(opt.orElse(Double.NaN)); // NaN
    System.out.println(opt.orElseGet(() -> Math.random())); // random number
    System.out.println(opt.orElseThrow()); // NoSuchElementException
    System.out.println(opt.orElseThrow(
        () -> new IllegalStateException())); // Can throw something else
  24. A stream in Java is a sequence of data. A stream pipeline consists of the operations that run on a stream to produce a result. A stream can be finite or infinite.

  25. A stream pipeline consists of:

    • Source: Where the stream comes from.
    • Intermediate operations: Transforms the stream into another one. There can be many intermediate operations. They do not run until the terminal operation runs.
    • Terminal operations: Produces a result. A stream can only be used once, the stream is no longer valid after a terminal operation completes.
  26. The Stream interface is defined in the java.util.stream package.

  27. Examples for creating a finite stream are shown below:

    Stream<String> empty = Stream.empty(); // 0
    Stream<Integer> singleElement = Stream.of(1); // 1
    Stream<Integer> fromArray = Stream.of(1,2,3); // 3
    
    var list = List.of("a","b","c");
    Stream<String> fromList = list.stream();
    Stream<String> fromListParallel = list.parallelStream();
  28. Examples for creating an infinite stream are shown below:

    Stream<Double> randoms = Stream.generate(Math::random);
    Stream<Integer> oddNumbers = Stream.iterate(1, n -> n + 2);
    Stream<Integer> oddNumbersUnder100 = Stream.iterate{
        1, // seed
        n -> n < 100; // Predicate to specify when done
        n -> n + 2; // UnaryOperator to get next value
    }
  29. A terminal operation can be performed without an intermediate operation. Reductions are a special type of terminal operation where the contents of the stream are combined into a single primitive or Object. Terminal stream operation method signatures and examples are shown below:

    // long count()
    Stream<String> s = Stream.of("monkey", "gorilla", "bonobo");
    System.out.println(s.count()); // 3
    
    // Optional<T> min(Comparator<? super T> comparator)
    // Optional<T> max(Comparator<? super T> comparator)
    Stream<String> s = Stream.of("monkey", "ape", "bonobo");
    Optional<String> min = s.min((s1, s2) -> s1.length()-s2.length());
    min.ifPresent(System.out.println); // ape
    
    Optional<?> minEmpty = Stream.empty().min((s1, s2) -> 0);
    System.out.println(minEmpty.isPresent()); // false
    
    // Optional<T> finndAny()
    // Optional<T> findFirst()
    Stream<String> s = Stream.of("monkey", "gorilla", "bonobo");
    Stream<String> infinite = Stream.generate(() -> "chimp");
    s.findAny().ifPresent(System.out:prinln); // monkey (usually)
    infinite.findAny().ifPresent(System.out::println); // chimp
    
    // boolean anyMatch(Predicate<? super T> predicate)
    // boolean allMatch(Predicate<? super T> predicate)
    // boolean noneMatch(Predicate<? super T> predicate)
    
    var list = List.of("monkey", "2", "chimp");
    Stream<String> infinite = Stream.generate(() -> "chimp");
    Predicate<String> pred = x -> Character.isLetter(x.charAt(0));
    
    System.out.println(list.stream().anyMatch(pred)); // true
    System.out.println(list.stream().allMatch(pred)); // false
    System.out.println(list.stream().noneMatch(pred)); // false
    System.out.println(infinite.anyMatch(pred)); // true
    
    // void forEach(Consumer<? super T> action)
    
    Stream<String> s = Stream.of("Monkey", "Gorilla", "Bonobo");
    s.forEach(System.out::print); // MonkeyGorillaBonobo
    
    // T reduce(T identity, BinaryOperator<T> accumulator)
    // Optional<T> reduce(BinaryOperator<T> accumulator)
    // <U> reduce(U identity,
    // BiFunction<U, ? super T, U> accumulator,
    // BinaryOperator<U> combiner)
    
    Stream<String> stream = Stream.of{"w", "o", "l", "f"};
    String word = stream.reduce("", (s,c) -> s + c);
    System.out.println(word); // wolf;
    
    // <R> R collect(Supplier<R> supplier,
    // BiConsumer(<R, ? super T> accumulator,
    // BiConsumer<R, R> combiner)
    // <R, A> R collect(Collector<? super T, A, R> collector)
    
    Stream<String> stream = Stream.of("w", "o", "l", "f");
    TreeSet<String> set = stream.collect(
        Treeset::new,
        Treeset::add,
        Treeset::addAll);
    System.out.println(set); // [f, l, o, w]
  30. An intermediate operation produces a stream as its result. Intermediate operation method signatures and examples are shown below:

    // Stream<T> filter(Predicate<? super T> predicate)
    
    Stream<String> s = Stream.of("monkey", "gorilla", "bonobo");
    s.filter(x -> x.startsWith("m"))
        .forEach(System.out::print); // monkey
    
    // Stream<T> distinct()
    
    Stream<String> s = Stream.of("duck", "duck", "duck", "goose");
    s.distinct()
        .forEach(System.out::print); // duckgoose
    
    // Stream<T> limit(long maxSize)
    // Stream<T> skip(long n)
    
    Stream<Integer> s = Stream.iterate(1, n -> n + 1);
    s.skip(5)
        .limit(2)
        .forEach(System.out::print); // 67
    
    // <R> Stream<R> map(Function<? super T>, ? extends R> mapper)
    Stream<String> s = Stream.of("monkey", "gorilla", "bonobo");
    s.map(String::length)
        .forEach(System.out::print); // 676
    
    // <R> Stream<R> flatMap(
        Function<? super T, ? extends Stream<? extends R>> mapper)
    
    List<String> zero = List.of();
    var one = List.of("Bonobo");
    var two = List.of("Mama Gorilla", "Baby Gorilla");
    Stream<List<String>> animals = Stream.of(zero, one, two);
    
    animals.flatMap(m -> m.stream())
        .forEach(System.out::println);
    
    // Bonobo
    // Mama Gorilla
    // Baby Gorilla
    
    // Stream<T> sorted()
    // Stream<T> sorted(Comparator<? super T> comparator>
    
    Stream<String> s = Stream.of("brown-", "bear-"):
    s.sorted()
        .forEach(System.out:print); // bear-brown-
    
    Stream<String> s = Stream.of("brown bear-", "grizzly-");
    s.sorted(Comparator.reverseOrder())
        .forEach(System.out::print); // grizzly-brown bear-
    
    // Stream<T> peek(Consumer<? super T> action)
    
    var stream = Stream.of("black bear", "brown bear", "grizzly");
    long count = stream.filter(s -> s.startsWith("g"))
        .peek(System.out::println).count() // grizzly
    System.out.println(count); // 1
  31. Intermediate and a terminal operation can be chained in a pipeline. An example is shown below:

    var list = List.of("Toby", "Anna", "Leroy", "Alex");
    list.stream()
        .filter(n -> n.length() == 4)
        .sorted()
        .limit(2)
        .forEach(System.out::println); // AnnaAlex
  32. Primitive Streams allow you to work with the int, double and long primitives. They include specialised methods for working with numeric data. The primitive streams are intStream, longStream and doubleStream.

  33. Primitive streams can be created from other streams. An example is shown below:

    Stream<String> objStream = Stream.of("penguin", "fish");
    IntStream intStream = objStream.mapToInt(s -> s.length());
  34. To create a primitive stream from another stream the below methods are used:

  35. The function parameters used when mapping streams are shown below:

  36. Methods can return OptionalDouble, OptionalInt and OptionalLong types when dealing with streams of primitives. A summary of the Optional types for primitives is shown below:

  37. To use multiple terminal operations to produce a result from a stream summary statistic can be used. Summary statistics includes the getMin(), getMax(), getAverage(), getSum() and getCount() methods. An example is shown below:

    private static int range(intStream ints){
        IntSummaryStatistics stats = ints.summaryStatistics();
        if(stats.getCount() == 0) throw new RuntimeException();
        return stats.getMax()-stats.getMin();
    }
  38. There are special functional interfaces for primitives. The BooleanSupplier functional interface is shown below:

    // boolean getAsBoolean()
    
    BooleanSupplier b1 = () -> true;
    BooleanSupplier b2 = () -> Math.random() > 0.5;
    System.out.println(b1.getAsBoolean()); // true
    System.out.println(b2.getAsBoolean()); // true or false
  39. Common functional interfaces for other primitives are shown below:

  40. Common functional interfaces for other primitives are shown below:

  41. Predefined collectors are available via static methods on the Collectors interface. These are shown below:

  42. Examples for using collectors are shown below:

    var ohMy = Stream.of("lions", "tigers", "bears");
    String result = ohMy.collect(Collectors.joining(", "));
    System.out.println(result); // lions, tigers, bears
    
    var ohMy = Stream.of("lions", "tigers", "bears");
    String result = ohMy.collect(Collectors.averagingInt(String::length));
    System.out.println(result); // 5.333333333333
    
    var ohMy = Stream.of("lions", "tigers", "bears");
    TreeSet<String> result = ohMy
        .filter(s -> s.startsWith("t))
        .collect(Collectors.toCollection(TreeSet::new));
    System.out.println(result); // [tigers]
    
    var ohMy = Stream.of("lions", "tigers", "bears");
    Map<String, Integer> map = ohMy.collect(
        Collectors.toMap(s -> s, String::length));
    Systemout.println(map); // {lions=5, bears=5, tigers=6}
    
    var ohMy = Stream.of("lions", "tigers", "bears");
    Map<Integer, String> map = ohMy.collect(Collectors.toMap(
        String::length,
        k -> k,
        (s1, s2) -> s1 + "," + s2));
    System.out.println(map); // {5=lions,bears, 6=tigers}
    System.out.println(map.getClass()); // class java.util.HashMap
    
    var ohMy = Stream.of("lions", "tigers", "bears");
    Map<Integer, List<String>> map = ohMy.collect(
        Collectors.groupingBy(String::length));
    System.out.println(map); // {5=[lions, bears], 6=[tigers]}
    
    var ohMy = Stream.of("lions", "tigers", "bears");
    Map<Boolean, List<String> map = ohMy.collect(
        Collectors.partitioningBy(s -> s.length() <= 5));
    System.out.println(map); // {false=[tigers], true=[lions, bears]}

Exceptions, Assertions, and Localization

  1. A custom exception class can be created by extending Exception (for a checked exception), or RuntimeException (for an unchecked exception).

  2. A try-with-resources statement ensures that any resources declared in the try block are automatically closed at the conclusion of the try block. A resource is typically a file or a database that requires a stream or connection to read or write data.

  3. To be used in a try-with-resources statement the resource is required to implement the AutoClosable interface. Inheriting AutoClosable requires implementing a close() method. If multiple resources are included in a try-with-resources statement they are closed in the reverse order in which they are declared.

  4. It is possible to use resources declared prior to a try-with-resources statement, provided they are marked final or are effectively final. The syntax is to use the resource name in place of the resource declaration, separated by a semicolon.

  5. An assertion is a Boolean expression that you place where you expect something to be true. An assert statement contains this statement along with an optional message. An assertion allows for detecting defects in the code. You can turn on assertions for testing and debugging while leaving them off when your program is in production. Unit tests are most frequently used to verify behaviour, whereas assertions are commonly used to verify the internal state of a program.

  6. Assertions should never alter outcomes. Assertions should be turned off in a production environment.

  7. The syntax for an assertion is shown below:

    assert test_value;
    assert test_value: message;
  8. An assertion evaluating to false will result in an AssertionError being thrown at runtime if assertions are enabled. To enable assertions a flag can be passed as per the below:

    java -enableassertions Rectangle
    java -ea Rectangle
  9. Assertions can be enabled or disabled for specific classes or packages.

    java -ea:com.demos... my.programs.Main // enable for classes in the com.demos package and any subpackages
    java -ea:com.demos... -da:com.demos.TestColors my.programs.Main // enable for com.demos but disables in TestColors class
  10. Java includes numerous classes for dates and times:

  11. Each of these types contains a now() method to get the current date or time and an of() method to instantiate an object. Various get methods are also provided.

  12. The format() method can take a DateTimeFormatter to display standard or custom formats. Note that enclosing values in single quotes escapes the values. Examples are shown below:

    LocalTime time = LocalTime.of(11, 12, 34);
    LocalDate date = LocalDate.of(2020, Month.OCTOBER, 20);
    System.out.println(time.format(DateTimeFormatter.ISO_LOCAL_TIME)); // standard format example
    var f = DateTimeFormatter.ofPattern("MMMM dd, yyyy 'at' hh:mm");
    System.out.println(dt.format(f)); // October 20, 2020 at 11:12
  13. Supported symbols for each date and time class are shown below:

  14. Internationalisation is the process of designing a program so that it can be adapted. Localisation means supporting multiple locales or geographic regions. Examples for working with locales are shown below:

    Locale locale = Locate.getDefault();
    System.out.println(locale); // en_US
    System.out.println(Locate.GERMAN); // de
    System.out.println(Locale.GERMANY); // de_DE
  15. Formatting or parsing currency and number values can change depending on the locale. Methods to get a number format based on a locale are shown below:

  16. An example of their usage is shown below:

    int attendeesPerYear = 3_200_000;
    int attendeesPerMonth = attendeesPerYear / 12; 
    var us = NumberFormat.getInstance(Locale.US);
    System.out.println(us.format(attendeesPerMonth)); // 266,666
    var gr = NumberFormat.getInstance(Locale.GERMANY);
    System.out.println(gr.format(attendeesPerMonth)); /// 266.666
  17. The DecimalFormat class can be used to express currency. A # is used to omit the position if no digit exists, and a 0 is used to place a 0 in the position if no digit exists. Examples are shown below:

    double d = 1234567.467;
    NumberFormat f1 = new DecimalFormat("###,###,###.0");
    System.out.println(f1.format(d)); // 1,234,567.5
  18. Date formats can also vary by locale. Methods used to retrieve an instance of DateTimeFormatter using the default locale are shown below:

  19. A resource bundle contains locale-specific objects used by a program. It is commonly stored in a properties file. A properties file is a text file in a specific format with key/value pairs.

  20. An example using two property files is shown below:

    Zoo_en.properties // name of file
    hello=Hello
    open=The zoo is open
    
    Zoo_fr.properties // name of file
    hello=Bonjour
    open=Le zoo est ouvert
    
    public static void printWelcomeMessage(Locale locale){
        var rb = ResourceBundle.getBundle("Zoo", locale);
        System.out.println(rb.getString("hello") + "," + rb.getString("open"));
    }
    
    public static void main(String[] args){
        var us = new Locale("en", "US");
        var france = new Locale("fr", "FR");
        printWelcomeMessage(us);
        printWelcomeMessage(france);
    }
  21. To find a resource bundle to use Java looks for the language/country in the filename, followed by just the language. The default resource bundle is used if no matching locale can be found.

Modular Applications

  1. There are three types of modules:

    • Named Modules: Contains a module-info file which appears in the root of the JAR alongside one or more packages. Unless otherwise specified, a module is a named module. Named modules appear on the module path rather than the classpath.
    • Automatic Module: Appears on the module path but does not contain a module-info file. It is a regular JAR file that is placed on the module path and gets treated as a module. The code referencing an automatic module treats it as if there is a module-info present, and automatically exports all packages. If an Automatic-Module-Name is specified in the manifest, then that name is used. Otherwise, a module name is automatically determined based on the JAR filename. To determine the name the file extension is removed, then the version number, and then special characters are replaced with a period. If a period is the first or last character it is also removed.
    • Unnamed Module: Like an automatic module, it is a regular JAR file. An unnamed module is on the classpath rather than the module path. An unnamed module does not usually contain a module-info file, and if it does, it is ignored since it is on the classpath. Unnamed modules do not export any packages to named or automatic modules, and an unnamed module can read from any JARs on the classpath or module path.
  2. Modules prefixed with java (standard modules) are shown below:

  3. Modules prefixed with jdk (part of the JDK) are shown below:

  4. The jdeps command provides information about dependencies. An example is shown below:

    // Animatronic.java
    
    import java.time.*;
    import java.util.*;
    import sun.misc.Unsafe;
    
    public class Animatronic {
        private List<String> names;
        private LocalDate visitDate;
    
        public Animatronic(List<String> names, LocalDate visitDate){
            this.names = names;
            this.visitDate = visitDate;
        }
    
        public void unsafeMethod(){
            Unsafe unsafe = Unsafe.getUnsafe();
        }
    }
    javac *.java
    jar -cvf zoo.dino.jar .
    jdeps zoo.dino.jar
  5. Before older applications can be migrated to use modules, the structure of the packages and libraries in the existing application need to be determined. In the below diagram style, the arrows point from the project that will require the dependency to the one that makes it available. Projects that do not have any dependencies are at the bottom.

  6. A bottom-up migration is the easiest migration approach. It works when you have the power to convert any JAR files that are not already modules. The approach is:

    • Pick the lowest-level project that has not yet been migrated.
    • Add a module-info.java file to that project. Be sure to add any exports to expose any package used by higher level JAR files. Also, add the requires directive for any modules it depends on.
    • Move this newly migrated named module from the classpath to the module path.
    • Ensure any projects that have not yet been migrated stay as unnamed modules on the classpath.
    • Repeat with the next-lowest-level project until you are done.
  7. A top-down migration is most useful when you do not have control of every JAR file used by your application. The approach is:

    • Place all projects on the module path.
    • Pick the highest-level project that has not yet been migrated.
    • Add a module-info file to that project to convert the automatic module into a named module. Remember to add any exports or requires directives. Automatic module names can be used when writing the requires directive since most of the projects on the module path do not have names yet.
    • Repeat with the next-highest-level project until you are done.
  8. An example of the bottom-up migration approach (left) and top-down migration approach (right) is shown below:

  9. When splitting up a project into modules, a problem with cyclic dependencies may arise. A cyclic dependency occurs when 2 or more things have dependencies on each other. Modules that have cyclic dependencies will not compile. A common technique to resolve this issue is to introduce another module containing all the code that the modules share. Note that a cyclic dependency can still exists between packages with a module.

  10. Although not recommended, is possible to customise what packages a module exports from the command line.

    javac --add-reads moduleA=moduleB --add-exports moduleB/com.modB.package1=moduleA ...
    java --add-reads moduleA=moduleB --add-exports moduleB/com.modB.package1=moduleA ...
    // --add-reads moduleA=moduleB implies that moduleA wants to read all exported packages of moduleB
    // add-exports moduleB/com.modB.package1=moduleA implies that moduleB exports package com.modB.package1 to moduleA
    // add-open is used ot provide access to privat members of classes through reflection (not required for exam)
  11. The previous section discussed modules in terms of dependencies, with one module exporting its public types and another module requiring them. This is a very tight coupling. A looser coupling can be if one module requires an implementation of an interface from another module, and the other module provides that implementation.

  12. A service is composed of an interface, classes referenced by the interface references, and a way to look up the implementations of the interface. A sample tours application will be used to introduce this concept. The 4 modules within this application are shown below:

  13. A service provider interface specifies the behaviour that the service will have. The service provider interface is exported for other modules to use. For the tours application this is shown below:

    // Souvenir.java
    package zoo.tours.api;
    
    public class Souvenir{
        private String description;
    
        public String getDescription(){
            return description;
        }
    
        public void setDescription(String description){
            this.description = description;
        }
    }
    
    // Tour.java
    package zoo.tours.api;
    
    public interface Tour {
        String name();
        int length();
        Souvenir getSouvenir();    
    }
    
    // module-info.java
    module zoo.tours.api{
        exports zoo.tours.api;
    }
  14. A service locator can find any classes that implement a service provider interface. At runtime, there may be many service providers (or none) that are found by the service locator. The service locator requires the service provider interface package, uses the Tour class to lookup classes that implement a service provider interface, and exports the package with the lookup for other modules to use. For the tours application this is shown below:

    // TourFinder.java
    package zoo.tours.reservations;
    
    import java.util.*;
    import zoo.tours.api.*;
    
    public class TourFinder{
        
        public static Tour findSingleTour(){
            ServiceLoader<Tour> loader = ServiceLoader.load(Tour.class);
            for(Tour tour : loader)
                return tour;
            return null;
        }
    
        public static List<Tour> findAllTours(){
            List<Tour> tours = new ArrayList<>();
            ServiceLoader<Tour> loader = ServiceLoader.load(Tour.class);
            for(Tour tour : loader)
                tours.add(tour);
            return tours;
        }
    }
    
    // module-info.java
    module zoo.tours.reservations {
        exports zoo.tours.reservations;
        requires zoo.tours.api;
        uses zoo.tours.api.Tour;
    }
  15. A consumer refers to a module that obtains and uses a service. Once the consumer has acquired a service via the service locator, it is able to invoke the methods provided by the service provider interface. The consumer requires service provider interface and the service locator. For the tours application this is shown below:

    // Tourist.java
    package zoo.visitor;
    
    import java.util.*;
    import zoo.tours.api.*;
    import zoo.tours.reservations.*;
    
    public class Tourist{
        public static void main(String[] args){
            Tour tour = TourFinder.findSingleTour();
            System.out.println("Single tour: " + tour);
        
            List<Tour> tours = TourFinder.findAllTours();
            System.out.println("# tours: " + tours.size());
        }
    }
    
    // module-info.java
    module zoo.visitor{
        requires zoo.tours.api;
        requires zoo.tours.reservations;
    }
  16. A service provider is the implementation of a service provider interface. The service provider requires the service provider interface and provides an implementation of the behaviour specified in the service provider interface. Note that the export directive is not used as we do not want consumers referring to the service provider directly. For the tours application this is shown below:

    // TourImpl.java
    package zoo.tours.agency;
    
    import zoo.tours.api.*;
    
    public class TourImpl implements Tour{
        public String name(){
            return "Behind the Scenes";
        }
    
        public int length(){
            return 120;
        }
    
        public Souvenir getSouvenir(){
            Souvenir gift = new Souvenir();
            gift.setDescription("stuffed animal");
            return gift;
        }
    }
    
    // module-info.java
    module zoo.visitor{
        requires zoo.tours.api;
        provides zoo.tours.api.Tour with zoo.tours.agency.TourImpl;
    }
  17. If a service provider declares a provider method, then the service loader invokes that method to obtain an instance of the service provider. A provider method is a public static method named "provider" with no formal parameters and a return type that is assignable to the service's interface or class. In this case, the service provider need not be assignable to the service's interface or class.

  18. If a service provider does not declare a provider method, then the service provider is instantiated directly, via its constructor. There must be a service provider constructor that takes no arguments and is assignable to the service's interface or class. The provides directive in a service provider cannot specify the same service more than once.

  19. If the used directive occurs in a class, the ServiceLoader.load() method returns a ServiceLoader object that can provide instances of the service type. The module system automatically discovers provider modules at start up by scanning modules in the Java runtime image and modular jars in the module path. As there can be multiple implementations of the service, multiple instances of the service type can be returned. The service type should offer enough descriptor methods for a consumer to select the best implementation.

  20. The requires directive takes an object name, the exports directive takes a package name, the uses directive takes a type name, and the provides directive takes a service type and provider class. A consumer module will contain requires/uses, while a provider module will contain requires/provides.

  21. A summary of the directives required for reach service artefact is shown below:

Concurrency

  1. Disk and network operations are extremely slow compared to CPU operations. Multithreaded processing is used by modern operating systems to allow applications to execute multiple tasks at the same time, which allows tasks waiting for resources to give way to other processing requests. Java has traditionally supported multithreaded programming using the Thread class. The Concurrency API has grown over time to provide numerous classes for performing complex thread-based tasks.

  2. A thread is the smallest unit of execution that can be scheduled by the operating system. A process is a group of associated threads that execute in the same, shared environment. A task is a single unit of work performed by a thread.

  3. A process model is shown below:

  4. Thread types include system threads threads created by the JVM, and user-defined threads which are created by the application developer. Operating systems use a thread scheduler to determine which threads should be executing. A context switch is the process of storing a thread's current state and later restoring the state of the thread to continue executing. A thread can interrupt or supersede another thread if it has a higher thread priority.

  5. The java.lang.Runnable interface is a functional interface that takes no arguments and returns no data. It is commonly used to define the task or work that a thread will execute, separate from the main application thread. The definition of runnable is shown below:

    @FunctionalInterface 
    public interface Runnable {
        void run();
    }
  6. To execute a thread first you define an instance of java.lang.Thread, and then you start the task using the Thread.start() method. Examples of defining a thread are shown below:

    // Providing a Runnable object to the Thread constructor
    public class PrintData implements Runnable{
        @Override public void run() {
            for(int i = 0; i < 3; i++)
                System.out.println("Printing record: "+i);
        }
    
        public static void main(String[] args){
            (new Thread(new PrintData())).start();
    }
    
    // Creating a class that extends Thread and overrides the run() method
    public class ReadInventoryThread extends Thread{
        @Override public void run() {
            System.out.println("Printing zoo inventory");
        }
    
        public static void main(String[] args){
            (new ReadInventoryThread()).start();
        }
    }
  7. While threads operate asynchronously, one thread may need to wait for the results of another thread. In such a case the Thread.sleep() method can be used to make a thread pause until results are ready. An example is shown below:

    class CheckResults {
        private static int counter = 0;
    
        public static void main(String[] a) throws InterruptedException {
        new Thread(() -> {
            for (int i = 0; i < 500; i++) {
            CheckResults.counter++;
            }
        }).start();
        while (CheckResults.counter < 100) {
            System.out.println("Not reached yet");
            Thread.sleep(1000); // 1 SECOND
        }
        System.out.println("Reached!");
        }
    }
  8. To improve on the above and to assist with creating and managing threads, the ExecutorService interface in the Concurrency API can be used.

  9. An example of using newSingleThreadExecutor() is shown below:

    import java.util.concurrent.*;
    public class ZooInfo{
        public static void main(String[] args){
            ExecutorService service = null;
            Runnable task1 = () ->
                System.out.println("Printing zoo inventory");
            Runnable task2 = () -> {for(int i = 0; i < 3; i++)
                System.out.println("Printing record: "+i);};
    
            try{
                service = Executors.newSingleThreadExecutor();
                System.out.println("begin");
                service.execute(task1);
                service.execute(task2);
                service.execute(task1);
                System.out.println("end");
            } finally {
                if(service != null) service.shutdown();
            }
        }
    }
  10. If the shutdown() method is not called then the application will never terminate. As part of shutdown the thread executor rejects any new tasks submitted to the thread executor while continuing to execute any previously submitted tasks. The isShutdown() and isTerminated() methods can be used to check the status of the thread. The shutdownNow() method attempts to stop all running tasks immediately.

  11. Tasks can be submitted to an ExecutorService in multiple ways. The execute() method is inherited from the Executor interface, which the ExecutorService interface extends. It is considered a "fire-and-forget" method as once it is submitted the result is not directly available to the calling thread. The submit() method returns a Future instance that can be used to determine whether the task is complete.

  12. Useful ExecutorService methods are shown below:

    void execute(Runnable command);
    Future<?> submit(Runnable task);
    <T> Future<T> submit(Callable<T> task);
    <T> List<Future<T>> invokeAll(Collections<? extends Callable<T>> tasks) throws InterruptedException;
    <T> T invokeAny(Collection<? extends Callable<T>> tasks) throws InterruptedException, ExecutionException;
  13. To improve on the previous CheckResults implementation and avoid managing threads directly, the below implementation using submit() to return a Future object can be used:

    public class CheckResults {
        private static int counter = 0;
    
        public static void main(String[] unused) throws Exception {
        ExecutorService service = null;
        try {
            service = Executors.newSingleThreadExecutor();
            Future<?> result = service.submit(() -> {
            for (int i = 0; i < 500; i++) {
                CheckResults.counter++;
            }
            });
            result.get(10, TimeUnit.SECONDS);
            System.out.println("Reached!");
        } catch (TimeoutException e) {
            System.out.println("Not reached in time");
        } finally {
            if (service != null) {
            service.shutdown();
            }
        }
        }
    }
  14. The java.util.concurrent.Callable functional interface is similar to Runnable except that its call() method returns a value and can throw a checked exception. The definition of the Callable interface is shown below:

    @FunctionalInterface
    public interface Callable<V> {
        V call() throws Exception;
    }
  15. The Callable interface is often preferable over Runnable since it allows more details to be retrieved easily from the task after it is completed.

  16. After submitting tasks to a thread executor, it is common to wait for the results. An example is shown below for a simple generic pattern where the result from the thread executor doesn't need to be retained:

    ExecutorService service = null;
    try {
        service = Executors.newSingleThreadExecutor();
        // Add tasks to the thread executor
    } finally {
        if (service != null) {
        service.shutdown();
        }
    }
    if (service != null) {
        service.awaitTermination(1, TimeUnit.MINUTES);
        // Check whether all tasks are finished
        if (service.isTerminated()) {
        System.out.println("Finished!");
        } else {
        System.out.println("At least one task is still running");
        }
    }
  17. The invokeAll() and invokeAny() methods can be used to execute task collections synchronously. The invokeAll() method will wait indefinitely until all tasks are complete, while the invokeAny() method will wait indefinitely until at least one task completes.

  18. The ScheduledExecutorService can be used to schedule a task to happen at some future time. Useful ScheduledExecutorService methods are shown below:

    schedule(Callable<V> callable, long delay, TimeUnit unit);
    schedule(Runnable command, long delay, TimeUnit unit);
    scheduleAtFixedRate(Runnable command, long initialDelay, long period, TimeUnit unit);
    scheduleWithFixedDelay(Runnable command, long initialDelay, long delay, TimeUnit unit);
  19. An example usage of ScheduledExecutorService is shown below:

    ScheduledExecutorService service = Executors.newSingleThreadScheduledExecutor();
    Runnable task1 = () -> System.out.println("Hello zoo");
    Callable<String> task2 = () -> "Monkey";
    ScheduledFuture<?> r1 = service.schedule(task1, 10, TimeUnit.SECONDS);
    ScheduledFuture<?> r2 = service.schedule(task2, 8, TimeUnit.MINUTES);
  20. Additional factory methods in the Executors class are available that use a pool of threads. A thread pool is a group of pre-instantiated reusable threads that are available to perform a set of tasks. Useful ScheduledExecutorService factory methods are shown below:

    ExecutorService.newSingleThreadExecutor();
    ScheduledExecutorService.newSingleThreadScheduledExecutor();
    ExecutorService.newCachedThreadPool();
    ExecutorService.newFixedThreadPool(int);
    ScheduledExecutorService.newScheduledThreadPool(int);
  21. An instance of a pooled-thread executor will execute concurrently if the number of tasks is less than the number of available threads. Calling newFixedThreadPool() with a value of 1 is equivalent to calling newSingleThreadExecutor(). The number of threads in the pool is often set to equal the number of available CPUs.

  22. Thread-safety is the property of an object that guarantees safe execution by multiple threads at the same time. As threads run in a shared environment and memory space, data must be organised so that we do not end up with unexpected results.

  23. An example of non-thread safe code is shown below:

    public class SheepManager {
        private int sheepCount = 0;
    
        private void incrementAndReport() {
            System.out.print((++sheepCount) + " ");
        }
    
        public static void main(String[] args) {
            ExecutorService service = null;
            try {
                service = Executors.newFixedThreadPool(20);
                SheepManager manager = new SheepManager();
                for (int i = 0; i < 10; i++) {
                    service.submit(() -> manager.incrementAndReport());
                }
            } finally {
                if (service != null) {
                    service.shutdown();
                }
            }
        }
    }
  24. Multiple threads read and write the sheepCount variable, with one of the threads overwriting the results of the others. When 2 or more threads execute the right side of the expression, only the result of one of the increment operations is stored. This is known as a race condition and means the output will be different each time.

  25. Atomic is the property of an operation to be carried out as a single unit of execution without any interference by another thread. The java.util.concurrent.atomic package contains the following useful Atomic classes:

    AtomicBoolean
    AtomicInteger
    AtomicLong
  26. Each class contains numerous methods that equivalent to many of the primitive built-in operators. Common atomic methods (e.g. for AtomicInteger) are shown below:

    int get();
    void set(int);
    int getAndSet(int);
    int incrementAndGet();
    int getAndIncrement();
    int decrementAndGet();
    int getAndDecrement()
  27. Replacing ++sheepCount and the int declaration with sheepCount.incrementAndGet() and an AtomicInteger declaration results in all of the numbers being printed, but the order is still not guaranteed. No increment operation is lost but we do not know which thread will return first.

  28. A monitor (or lock) is a structure that supports mutual exclusion, which is the property that at most one thread is executing a particular segment of code at a given time. As each thread arrives at a lock it checks if any threads are already in the block, and only a single thread can hold the lock. Adding the synchronized block as per the below will ensure the output of the above is sequential:

    private void incrementAndReport() {
        synchronized(this) {
            System.out.print((++sheepCount) + " ");
        }
    }
  29. The synchronized modifier can also be added to a method to achieve the same effect. A static synchronized block can also be used if thread access needs to be ordered across all instances, and not just a single instance.

  30. Correctly using the synchronised keyword can be challenging and has performance implication. Other classes within the Concurrency API that are easier to use are recommended.

  31. The Concurrency API includes the lock interface that is conceptually like using the synchronized keyword, but that has a lot more features. The below blocks are equivalent:

    // Implementation #1 with a synchronized block
    Object object = new Object();
    synchronized (object) {
        // protected code
    }
    
    // Implementation #2 with a Lock
    Lock lock = new ReentrantLock();
    try {
        lock.lock();
        // protected code
    } finally {
        lock.unlock();
    }
  32. Useful lock interface methods include:

    void lock();
    void unlock();
    boolean tryLock();
    boolean tryLock(long, TimeUnit);
  33. The tryLock() method attemps to acquire a lock and immediately returns a boolean result. It does not wait if another thread already holds the lock, it returns immediately whether a lock is available. The tryLock(long, TimeUnit) method is similar but will wait for a period of time to acquire the lock. An example for tryLock() is shown below:

    Lock lock = new ReentrantLock();
    new Thread(() -> printMessage(lock)).start();
    if (lock.tryLock()) {
        try {
            System.out.println("Lock obtained, entering protected code");
        } finally {
            lock.unlock();
        }
    } else {
        System.out.println("Unable to acquire lock, doing something else");
    }
  34. The CyclicBarrier class can be used to allow a set of threads to wait for each other to reach a common execution point, known as a barrier. This allows multiple threads to still run. An example is shown below:

       public class LionPenManager {
        private void removeLions() {
            System.out.println("Removing lions");
        }
    
        private void cleanPen() {
            System.out.println("Cleaning the pen");
        }
    
        private void addLions() {
            System.out.println("Adding lions");
        }
    
        public void performTask(CyclicBarrier c1, CyclicBarrier c2) {
            try {
                removeLions();
                c1.await();
                cleanPen();
                c2.await();
                addLions();
            } catch (InterruptedException | BrokenBarrierException e) {
                // handle
            }
        }
    
        public static void main(String[] args) {
            ExecutorService service = null;
            try {
                service = Executors.newFixedThreadPool(4);
                var manager = new LionPenManager();
                var c1 = new CyclicBarrier(4);
                var c2 = new CyclicBarrier(4, () -> System.out.println("*** Pen Cleaned!"));
                for (int i = 0; i < 4; i++) {
                    service.submit(() -> manager.performTask(c1, c2));
                }
            } finally {
                if (service != null) {
                    service.shutdown();
                }
            }
        }
    }
  35. The Concurrency API also includes interfaces and classes to help solve common memory consistency errors. A memory consistency error occurs when two threads have inconsistent views of what should be the same data. The JVM may throw a ConcurrentModificationException in such a scenario. An example is shown below:

    // ConcurrentModificationException thrown
    var foodData = new HashMap<String, Integer>();
    foodData.put("penguin", 1);
    foodData.put("flamingo", 2);
    for(String key: foodData.keySet()) {
        foodData.remove(key);
    }
    
    // No exception thrown
    var foodData = new ConcurrentHashMap<String, Integer>();
    foodData.put("penguin", 1);
    foodData.put("flamingo", 2);
    for(String key: foodData.keySet()) {
        foodData.remove(key);
    }
  36. In the above example the iterator on keySet() is not properly updated after the first element is removed, so an exception is thrown. Concurrent collection classes should be used any time multiple threads are going to modify a collections object outside of a synchronized block. Concurrent collection classes are shown below:

    ConcurrentHashMap;
    ConcurrentLinkedQueue;
    ConcurrentSkipListMap;
    ConcurrentSkipListSet;
    CopyOnWriteArrayList;
    CopyOnWriteArraySet;
    LinkedBlockingQueue;
  37. The SkipList classes, ConcurrentSkipListSet and ConcurrentSkipListMap, are concurrent versions of their sorted counterparts, Treeset and Treemap, respectively. They maintain their elements or keys in the natural ordering of their elements.

  38. The CopyOnWriteArrayList and CopyOnWriteArraySet classes copy all their elements to a new underlying structure anytime an element is added, modified, or removed from the collection. By modified element, we mean that the reference in the collection is changed. Modifying the actual contents of objects within the collection will not cause a new structure to be allocated. Although the data is copied to a new underlying structure, our reference to the Collection object does not change. This is particularly useful in multithreaded environments that need to iterate the collection. Any iterator established prior to modification will not see the changes, but instead it will iterate over the original elements prior to the modification.

  39. The CopyOnWriteArraySet class is used like a Hashset and has similiar properties as the CopyOnWriteArrayList class. The CopyOnWrite classes can use a lot of memory, since a new collection structure needs to be allocated anytime the collection is modified. They are commonly used in multithreaded environment situations where reads are far more common than writes.

  40. The LinkedBlockingQueue class implements the BlockingQueue interface. The BlockingQueue is just like the regular Queue, except that it includes methods that will wait a specific amount of time to complete an operation.

  41. The Concurrency API includes methods for obtaining synchronised versions of existing noncurrent collection objects. They operate on the inputted collection and return a reference that is the same type as the underlying collection. These are listed below:

    synchronizedCollection(Collection<T> c);
    synchronizedList(List<T> list);
    synchronizedMap(Map<K,V> m);
    synchronizedNavigableMap(NavigableMap<K,V> m);
    synchronizedNavigableSet(NavigableSet<T> s);
    synchronizedSet<Set<T> s);
    synchronizedSortedMap(SortedMap<K,V> m);
    synchronizedSortedSet(SortedSet<T> s);
  42. If you are given an existing collection that is not a concurrent class and need to access it among multiple threads, you can wrap it using the methods above.

  43. A threading problem can occur in multithreaded applications when two or more threads interact in an unexpected an undesirable way. For example, two threads may block each other from accessing a particular segment of code. As shown above, the Concurrency API creates threads and manages complex thread interactions for you. Although it reduces the potential for threading issues, it does not eliminate them.

  44. Liveness is the ability of an application to be able to execute in a timely manner. There are three types of liveness issues with which you should be familiar: deadlock, starvation, and livelock. Deadlock occurs when two or more threads are blocked forever, each waiting on each other. Livelock occurs when two or more threads are conceptually blocked forever, although they are each active and trying to complete their task. Starvation occurs when a single thread is perpetually denied access to a shared resource or lock. Livelock is a special case of resource starvation in which two or more threads actively try to acquire a set of locks, are unable to do so, and restart part of the process. In practise, livelock is often difficult to detect, as threads in this state appear active and able to respond to respond to requests.

  45. A race condition is an undesirable result that occurs when two tasks, which should be completed sequentially, are completed at the same time. Race conditions lead to invalid data if they are not properly handled. They tend to appear in highly concurrent applications.

  46. A parallel stream is a stream that is capable of processing results concurrently, using multiple threads. For example, you can use a parallel stream and the map() operation to operate concurrently on the elements in the stream, vastly improving performance over processing a single element at a time.

  47. A parallel stream can be created on an existing stream (note that a terminal operation on s2 makes s1 unavailable for further use):

    Stream<Integer> s1 = List.of(1,2).stream();
    Stream<Integer> s2 = s1.parallel();
  48. A parallel stream can also be created directly:

    Stream<Integer> s3 = List.of(1,2).parallelStream();
  49. A parallel decomposition is the process of taking a task, breaking it up into smaller pieces that can be performed concurrently, and then reassembling the results. As an example:

    private static int doWork(int input) {
        try {
            Thread.sleep(5000);
        } catch (InterruptedException e) {
        }
        return input;
    }
    
    // serial outputs 1 2 3 4 5 25 seconds
    long start = System.currentTimeMillis();
    List.of(1, 2, 3, 4, 5).stream().map(w -> doWork(w))
        .forEach(s -> System.out.print(s + " "));
    
    // parallel outputs 3 2 1 5 4 5 seconds
    long start = System.currentTimeMillis();
    List.of(1, 2, 3, 4, 5).parallelstream().map(w -> doWork(w))
        .forEach(s -> System.out.print(s + " "));
  50. The results are no longer ordered or predictable. In this case, our system had enough CPUs for all the tasks to be run concurrently.

I/O

  1. There are three primary java.io.File constructors:

    public File(String pathname);
    public file(File parent, String child);
    public File(String parent, String child);
  2. An instance of the File class only represents a path to the file. Unless operated upon, it is not connected to an actual file within the file system. Common methods of the File class include:

    boolean delete();
    boolean exists();
    String getAbsolutePath();
    String getName();
    String getParent();
    boolean isDirectory();
    boolean isFile();
    long lastModified();
    long length();
    File[] listFiles()
    boolean mkdir();
    boolean mkdirs();
    boolean renameTo(File dest);
  3. The contents of a file may be accessed or written via an I/O stream. The java.io API defines two sets of stream classes: byte streams and character streams. Byte streams read and write binary data and have class names that end in InputStream or OutputStream. Character streams read and write text data and have class names that end in Reader or Writer. The byte streams are primarily used to work with binary data, such as an image or executable file, while character streams are used to work with text files.

  4. Generally, most InputStream classes have a corresponding OutputStream class. Similiarly, Reader classes typically have a corresponding Writer class. Exceptions to this rule include PrintWriter (which has no accompanying PrintReader class), and PrintStream which has no corresponding InputStream.

  5. The java.io library defines four abstract classes that are the parents of all stream classes defined within the API: InputStream, OutputStream, Reader, and Writer. The java.io concrete stream classes are:

    FileInputStream;
    FileOutputStream;
    FileReader;
    FileWriter;
    BufferedInputStream;
    BufferedOutputStream;
    BufferedReader;
    BufferedWriter;
    ObjectInputStream;
    ObjectOutputStream;
    PrintStream;
    PrintWriter;
  6. An example of reading and writing from a stream (note that the int value returned will be -1 at the end of the stream) is shown below:

    // InputStream and Reader
    public int read() throws IOException;
    
    // OutputStream and Writer
    public void write(int b) throws IOException;
  7. Overloaded methods are also provided to read from an offset and based on a particular length:

    // InputStream
    public int read(byte[] b) throws IOException;
    public int read(byte[] b, int offset, int length) throws IOException;
    
    // OutputStream
    public void write(byte[] b) throws IOException;
    public void write(byte[] b, int offset, int length) throws IOException;
    
    // Reader
    public void write(char[] c) throws IOException;
    public void write(char[] c, int offset, int length) throws IOException;
    
    // Writer
    public void write(char[] c) throws IOException;
    public void write(char[] c, int offset, int length) throws IOException;
  8. All I/O streams include a close method to release any resources within the stream when they are no longer needed. It is imperative that all I/O streams are closed lest they lead to resource leaks. Since all I/O streams implement Closeable, the best way to do this is with a try-with-resources statement. Note that when working with wrapped streams, you only need to close the topmost objects:

    try (var fis = new FileInputStream("zoo-data.txt")) {
        System.out.print(fis.read());
    }
  9. All input stream classes can be manipulated with the following methods:

    // InputStream and Reader
    public boolean markSupported();
    public void mark(int readLimit);
    public reset() throws IOException;
    public long skip(long n) throws IOException;
  10. The mark() and reset() methods return a stream to an earlier position. Make sure to call markSupported() on the stream to confirm the stream supports mark(). The skip() method reads data from the stream but discards the contents.

  11. When data is written to an output stream, the underlying operating system does not guarantee that the data will make it to the file system immediately. The data may be cached in memory, with a write only occurring after a temporary cache is filled or after some amount of time has passed. If the application terminates unexpectedly, the data would be lost, because it was never written to the file system. To address this, all output stream classes provide a flush() method to request that all accumulated data be written immediately to disk. Note that each time it is used, it may cause a noticeable delay in the application, so it should only be used intermittently.

  12. Some common, concrete I/O stream classes are shown below:

    // byte streams
    public FileInputStream(File file) throws FileNotFoundException;
    public FileInputStream(String name) throws FileNotFoundException;
    public FileOutputStream(File file) throws FileNotFoundException;
    public FileOutputStream(String name) throws FileNotFoundException;
    
    // character streams
    public FileReader(File file) throws FileNotFoundException;
    public FileReader(String name) throws FileNotFoundException;
    public FileWriter(File file) throws FileNotFoundException;
    public FileWriter(String name) throws FileNotFoundException;
  13. Buffered streams contain a number of performance improvements for managing data in memory:

    // byte streams
    public BufferedInputStream(InputStream in);
    public FileInputStream(String name) throws FileNotFoundException;
    public FileOutputStream(File file) throws FileNotFoundException;
    public FileOutputStream(String name) throws FileNotFoundException;
    
    // character streams
    public BufferedReader(Reader in);
    public BufferedWriter(Writer out);
  14. The following examples copies a file using these streams. In the first example, instead of reading one byte at a time, we read and write up to 1024 bytes at a time. In the second example, we use a String instead of a buffer array, and inserting a newLine() on every iteration of the loop:

    // byte stream
    void copyFileWithBuffer(File src, File dest) throws IOException {
        try (var in = new BufferedInputStream(new FileInputStream(src));
                var out = new BufferedOutputStream(
                        new FileOutputStream(dest))) {
            var buffer = new byte[1024];
            int lengthRead;
            while ((lengthRead = in.read(buffer)) > 0) {
                out.write(buffer, 0, lengthRead);
                out.flush();
            }
        }
    }
    
    // character stream
    void copyTextFileWithBuffer(File src, File dest) throws IOException {
        try (var reader = new BufferedReader(new FileReader(src));
                var writer = new BufferedWriter(new FileWriter(dest))) {
            String s;
            while ((s = reader.readLine()) != null) {
                writer.write(s);
                writer.newLine();
            }
        }
    }
  15. Serialization is the process of converting an in-memory object to a byte stream. Likewise, deserialization is the process of converting from a byte stream into an object. Serialization often involves writing an object to a stored or transmittable format, while deserialization is the reciprocal process.

  16. To serialize an object using the I/O API, the object must implement the java.io.Serializable interface. Since Serializable is a marker interface, it does not have any methods. Generally speaking, you should only mark data-orientated classes serializable. The purpose of using this interface is to inform any process attempting to serialize the object that you have taken the proper steps to make the object serializable.

  17. Oftentimes, the transient modifier is used for sensitive data of the class, such as a password. There are other objects it does not make sense to serialize, like the state of an in-memory thread. If the object is part of a serializable object, we just mark it transient to ignore these select instance members. When making a class serializable, every instance member of the class must be serializable, marked transient, or having a null value at the time of serialization.

  18. The following classes are high level streams that operate on existing streams:

    // serialize an object to a stream
    public ObjectOutputStram(OutputStream out) throws IO Exception;
    
    // deserialize an object from a stream
    public ObjectInputStream(InputStream in) throws IO Exception;
  19. Two common methods using these methods and example usages are shown below:

    // ObjectInputStream
    public Object readObject() throws IOException, ClassNotFoundException
    
    // ObjectOutputStream
    public void writeObject(Object obj) throws IOException;
    
    // save an object to file
    void saveToFile(List<Gorilla> gorillas, File dataFile) throws IOException {
        try(var out = new ObjectOutputStream(new BufferedOutputStream(new FileOutputStream(dataFile)))) {
            for (Gorilla gorilla : gorillas)
                out.writeObject(gorilla;)
        }
    }
    
    // read an object from file
    List<Gorilla> readFromFile(File dataFile)
            throws IOException, ClassNotFoundException {
        var gorillas = new ArrayList<Gorilla>();
        try (var in = new ObjectInputStream(
                new BufferedInputStream(new FileInputStream(dataFile)))) {
            while (true) {
                var object = in.readObject();
                if (object instanceof Gorilla)
                    gorillas.add((Gorilla) object);
            }
    
        } catch (EOFException e) {
            // File end reached
        }
        return gorillas;
    }
  20. It should be noted that the constructor and any instance initializations defined in the serialized class are ignored during the deserialization process. Java only calls the constructor of the first non-serializable parent class in the class hierarchy.

  21. A diagram of I/O stream classes is shown below:

  22. The java.io.Console class is designed to handle user interactions. The below example will ask the user a series of questions and print the results based on this information:

    public void readConsole() {
        Console console = System.console();
        if (console == null) {
            throw new RuntimeException("Console not available");
        } else {
            String name = console.readLine("Please enter your name: ");
            console.writer().format("Hi %s", name);
            console.writer().println();
            console.format("What is your address?");
            String address = console.readLine();
            char[] password = console.readPassword(
                    "Enter a password " + "between %d and %d characters: ", 5,
                    10);
            char[] verify = console.readPassword("Enter the password again: ");
            console.printf(
                    "Passwords " + (Arrays.equals(password, verify) ? "match"
                            : "do not match"));
        }
    }

NIO.2

  1. NIO.2 is an acronym that stands for the second version of the Non-blocking Input/Output API, and it is sometimes referred to as the "New I/O". NIO.2 allows us to do a lot more with files and directories than the original java.io API. At its core NIO.2 is a replacement for the legacy java.io.File class that aims to provide a more intuitive, more feature-rich API for working with files and directories.

  2. The cornerstone of NIO.2 is the java.nio.file.Path interface. A Path represents a hierarchical path on the storage system to a file or directory. A Path can be thought of as a replacement for the java.io.File class, although it is used a bit differently. Unlike the java.io.File class, the Path interface contains support for symbolic links. NIO.2 includes full support for creating, detecting, and navigating symbolic links within the file system.

  3. Path is an interface, and the JVM returns a file system-specific implementation when a Path is created, such as a Windows or Unix Path class. Examples for creating a Path object are shown below:

    // Path factory method
    public static Path of(String first, String... more);
    
    // example 1
    Path path1 = Path.of("pandas/cuddly.png");
    Path path2 = Path.of("c:\\zooinfo\\November\\employees.txt");
    Path path3 = Path.of("/home/zoodirectory");
    
    // example 2
    Path path1 = Path.of("pandas", "cuddly.png");
    Path path2 = Path.of("c:", "zooinfo", "November", "employees.txt");
    Path path3 = Path.of("/", "home", "zoodirectory");
  4. Another way of obtaining a Path instance is from the java.nio.file.Paths factory class. Note the s at the end to distinguish it from the Path interface. Examples are shown below:

    // Paths factory method
    public static Path get(String first, String... more);
    
    Path path1 = Paths.get("pandas/cuddly.png");
    Path path2 = Paths.get("c:\\zooinfo\\November\\employees.txt");
    Path path3 = Paths.get("/", "home", "zoodirectory");
  5. A Path can also be obtained using the Paths class with a URI value. Examples are shown below:

    // URI Constructor
    public URI(String str) throws URISyntaxException;
    
    URI a = new URI("file://icecream.txt");
    Path b = Path.of(a);
    Path c = Paths.get(a);
    URI d = b.toUri();
  6. A Path can also be obtained using the FileSystems class. Examples are shown below:

    // FileSystems factory method
    public static FileSystem getDefault();
    
    // FileSystem instance method
    public Path getPath(String first, String... more);
    
    Path path1 = FileSystems.getDefault().getPath("pandas/cuddly.png");
    Path path2 = FileSystems.getDefault().getPath("c:\\zooinfo\\November\\employees.txt");
    Path path3 = FileSystems.getDefault().getPath("/home/zoodirectory");
  7. Finally, a Path instance can be obtained using the legacy java.io.File class. Examples are shown below:

    // Path to File, using Path instance method
    public default File toFile();
    
    // File to Path, using java.io.File instance method
    public Path toPath();
    
    File file = new File("husky.png");
    Path path = file.toPath();
    File backToFile = path.toFile();
  8. The relationships between NIO.2 classes and interfaces are shown below:

  9. Many NIO.2 methods include a varargs that takes an optional list of value. The table below shows the commonly used arguments:

  10. An example is shown below:

    void copy(Path source, Path target) throws IOException {
        Files.move(source, target, LinkOption.NOFOLLOW_LINKS,
                StandardCopyOption.ATOMIC_MOVE);
    }
  11. Common causes of an IOException include a loss of communication to the file system, inaccessibility of files or directories, an inability to overwrite a file, or a file or directory being required but not exist.

  12. Other common Path instance methods are shown below:

    Path of(String, String...);
    Path getParent();
    URI toURI();
    boolean isAbsolute();
    String toString();
    Path toAbsolutePath();
    int getNameCount();
    Path relativize();
    Path getName(int);
    Path resolve(Path);
    Path subpath(int, int);
    Path normalize();
    Path getFileName();
    Path toRealPath(LinkOption...);
  13. The Files helper class can interact with real files and directories within the file system. Common static methods in the Files class are shown below:

    boolean exists(Path, LinkOption...);
    Path move(Path, Path, CopyOption...);
    boolean isSameFile(Path, Path);
    void delete(Path);
    Path createDirectory(Path, FileAttribute<?>...);
    BufferedReader newBufferedReader(Path);
    Path copy(Path, Path, CopyOption...);
    BufferedWriter newBufferedWriter(Path, OpenOption...);
    long copy(InputStream, Path, CopyOption...);
    List<String> readAllLines(Path);
    long copy(Path, OutputStream);
  14. The Files attribute also provides methods for accessing file and directory metadata, referred to as file attributes. These methods include:

    public static boolean isDirectory(Path path, LinkOption... options);
    public static boolean isSymbolicLink(Path path);
    public static boolean isRegularFile(Path path, LinkOption... options);
    public static boolean isHidden(Path path) throws IOException;
    public static boolean isReadable(Path path);
    public static boolean isWriteable(Path path);
    public static boolean isExecutable(Path path);
    public static long size(Path path) throws IOException;
    public static FileTime getLastModifiedTime(Path path, LinkOption... options) throws IOException;
  15. NIO.2 includes two methods for working with attributes. For each method, you need to provide a file system type object. The BasicFileAttributes type is the most used. Examples of reading and updating using these methods are shown below:

    public static <A extends BasicFileAttributes> A readAttributes(Path path,
        Class<A> type, LinkOption... options) throws IOException;
    public static <V extends FileAttributeView> V getFileAttributeView(
        Path path, Class<V> type, LinkOption... options);
    
    // read
    var path = Paths.get("/turtles/sea.txt");
    BasicFileAttributes data = Files.readAttributes(path, BasicFileAttributes.class);
    
    // update
    BasicFileAttributes view = Files.readAttributes(path, BasicFileAttributes.class);
    BasicfileAttributes attributes = view.readAttributes();
    FileTime lastModifiedTime = FileTime.fromMi(attributes.lastModifiedTime().toMillis() + 10_000);
    view.setTimes(lastModifiedTime, null, null);
  16. The Files class contains useful Stream API methods that operate on files, directories, and directory trees. The example below performs a deep copy:

    public void copyPath(Path source, Path target) {
        try {
            Files.copy(source, target);
            if (Files.isDirectory(source)) {
                try (Stream<Path> s = Files.list(source)) {
                    s.forEach(
                            p -> copyPath(p, target.resolve(p.getFileName())));
                }
            }
        } catch (IOException e) {
            // handle exception
        }
    }
  17. The Files.lines() method is preferable to the Files.readAllLines() method as it does not read the entire file into memory, instead it lazily processes each line and prints it as it is read.

JDBC

  1. There are two main ways to access a relational database from Java. Java Database Connectivity Language (JDBC) accesses data as rows and columns. Java Persistence API (JPA) accesses data through Java objects using a concept called Object-Relational Mapping (ORM). Databases that store their data in a format other than tables, such as key/value, are known as NoSQL databases. NoSQL is out of scope for this exam.

  2. Create, Read, Update, Delete (CRUD) are the types of SQL statements. All database imports are in the java.sql package.

  3. The JDBC interfaces are declared in the JDK. The concrete classes come from the JDBC driver, and each database has a different JAR file with these classes. The interfaces and an example implementation are shown below:

  4. Driver establishes a connection to the database, Connection sends commands to the database, PreparedStatement executes a SQL query, CallableStatement executes a SQL query, and ResultSet reads the results of a query. The example below shows end to end JDBC code:

    public class MyFirstDatabaseConnection {
        public static void main(String[] args) throws SQLException {
            String url = "jdbc:derby:zoo";
            try (Connection conn = DriverManager.getConnection(url);
                    PreparedStatement ps = conn
                            .prepareStatement("SELECT name FROM animal");
                    ResultSet rs = ps.executeQuery()) {
                while (rs.next()) {
                    System.out.println(rs.getString(1));
                }
            }
        }
    }
  5. The JDBC URL format includes the protocol, subprotocol, and subname. Examples are shown below:

    jdbc:postgresql://localhost/zoo
    jdbc:oracle:thin@123,123,123,123:1521:zoo
    jdbc:mysql://localhost:3306
    jdbc:mysql://localhost:3306/zoo?profileSQL=true
  6. A Connection can be established with DriverManager or DataSource. DriverManager is the only one relevant in the exam, but in the real world DataSource is much more powerful as it can pool connection and store the database connection information outside the application.

  7. You have the choice of working with a Statement, PreparedStatement, or CallableStatement. Statement is an interface that both PreparedStatement and CallableStatement extend. A PreparedStatement takes parameters, while a Statement does not. A CallableStatement is for queries that are inside the database.

  8. PreparedStatement is preferred to Statement as it provides increased performance, security, readability, and makes future use easier. The ps.execute(), ps.executeQuery(), and ps.executeUpdate() methods are used to run SQL statements. The ps.execute() method returns a Boolean result type and true for all SELECT operations and false for all other operations, the ps.executeQuery() method returns a ResultSet and the relevent rows and columns for a SELECT operation and n/a for other operations, and the ps.executeUpdate() method returns n/a for a SELECT operation and the number of rows added/changed/removed for other operations.

  9. A PreparedStatement allows you to set parameters using the setBoolean(), setDouble(), setInt, setLong, setObject, and setString methods. It is important to note that the variables start counting from 1 and not from 0. An example is shown below:

    public static void register(Connection conn, int key, int type, String name)
            throws SQLException {
        String sql = "INSERT INTO names VALUES(?,?,?)";
        try (PreparedStatement ps = conn.prepareStatement(sql)) {
            ps.setInt(1, key);
            ps.setString(3, name);
            ps.setInt(2, type);
            ps.executeUpdate();
        }
    }
  10. The addBatch() and executeBatch() methods can be used to run multiple statements in fewer trips to the database. As the database is often on a different machine than the machine the Java code runs, reducing the number of network calls can improve performance significantly.

  11. A CallableStatement can be used to execute a stored procedure on a database. An example is shown below with the read_e_names() stored procedure:

    String sql = "{call read_e_names()}";
    try(CallableStatement cs = conn.prepareCall(sql);
            ResultSet rs = cs.executeQuery()) {
        while(rs.next()) {
            System.out.println(rs.getString(3));
        }
    }
  12. An example calling the read_names_by_letter() stored procedure which requires a prefix parameter is shown below:

    var sql = "{call read_names_by_letter(?)}";
    try (var cs = conn.prepareCall(sql)) {
        cs.setString("prefix", "Z");
        try (var rs = cs.executeQuery()) {
            while (rs.next()) {
                System.out.println(rs.getString(3));
            }
        }
    }
  13. An example calling the magic_number() stored procedure which requires a Num parameter and returns an OUT parameter is shown below:

    var sql = "?=call magic_number(?)}";
    try (var cs = conn.prepareCall(sql)) {
        cs.registerOutParameter(1, Types.INTEGER);
        cs.execute();
        System.out.println(cs.getInt("num"));
    }
  14. An example calling the double_number() stored procedure which requires a Num parameter and returns an INOUT parameter is shown below:

    var sql = "{? = call double_number(?)}";
    try (var cs = conn.prepareCall(sql)) {
        cs.setInt(1, 8);
        cs.registerOutParameter(1, Types.INTEGER);
        cs.execute();
        System.out.println(cs.getInt("num"));
    }
  15. JDBC resources, such as a Connection, are expensive to create. Not closing them creates a resource leak that will eventually slow down you program. The resources need to be closed in a specific order. The ResultSet is closed first, followed by the PreparedStatement (or CallableStatement), and then the Connection. Closing all three is not strictly necessary, as closing a JDBC resource should close any resources that it created. JDBC also automatically closes a ResultSet when you run another SQL statement from the same Statement, PreparedStatement, or CallableStatement.

Security

  1. A key security principle is to limit access as much as possible. This is known as the principle of least privilege. There are four levels of access control in Java, and the lowest level of accessible possible should be used.

  2. If subclassing is not required, classes should use the final modified to prevent subclassing.

  3. Object immutability should be used wherever possible. This can be achieved my marking the class final, marking all instance variables private, not defining any setter methods and marking parameters final, not allowing referenced mutable objects to be modified, and using a constructor to set all properties of the object (making a copy if needed).

  4. A copy can be returned easily using the clone() method if the class implements the Clonable interface. A shallow copy is returned if no custom implementation is provided. If required, a deep copy implementation can be written for the class.

  5. Injection is an attack where dangerous input runs in a program as part of a command. Sources of untrusted data include user input, reading from files, and retrieving data from a database. In the real world, any data that did not originate from your program should be considered suspect.

  6. Consider the following example:

    public int getOpening(Connection conn, String day) throws SQLException {
        String sql = "SELECT opens FROM hours WHERE day = " + day + "";
    
        try (var stmt = conn.createStatement();
                var rs = stmt.executeQuery(sql)) {
            if (rs.next()) {
                return rs.getInt("opens");
            }
        }
        return -1;
    }
    
    // good
    int opening = attack.getOpening(comm, "monday"); // 10
    
    // bad
    int evil = attack.getOpening(conn, "monday' OR day IS NOT NULL OR day = 'sunday"); // 9
  7. The second execution ran the following SQL statement which returns all rows (9 is returned as that happens to be the first result):

    SELECT opens FROM hours
    WHERE day = 'monday'
    OR day IS NOT NULL
    OR day = 'sunday'
  8. Consider second execution with the following example where a PreparedStatement is used:

    public int getOpening(Connection conn, String day) throws SQLException {
        String sql = "SELECT opens FROM hours WHERE day = ?";
    
        try (var ps = conn.prepareStatement(sql)) {
            ps.setString(1, day);
            try (var rs = ps.executeQuery()) {
                if (rs.next()) {
                    return rs.getInt("opens");
                }
            }
            return -1;
        }
    }
    
    // bad
    int evil = attack.getOpening(conn, "monday' OR day IS NOT NULL OR day = 'sunday"); // -1
  9. The entire string is matched against the day column, and since there is no match, no rows are returned.

  10. Command injection is another type that uses operating system commands to do something unexpected. Consider the following example:

    Console console = System.console();
    String dirName = console.readLine();
    Path path = Paths.get("c:/data/diets/" + dirName);
    try (Stream<Path> stream = Files.walk(path)) {
        stream.filter(p -> p.toString().endsWith(".txt"))
                .forEach(System.out::println);
    }
  11. When run with .. as the directory name, a secrets directory could be returned. A whitelist can be implemented to control what directories can be searched:

    Console console = System.console();
    String dirName = console.readLine();
    if (dirName.equals("mammal") || dirName.equals("birds")) {
        Path path = Paths.get("c:/data/diets/" + dirName);
        try (Stream<Path> stream = Files.walk(path)) {
            stream.filter(p -> p.toString().endsWith(".txt"))
                    .forEach(System.out::println);
        }
    }
  12. When working on a project, you will often encounter confidential or sensitive data. Confidential information should not be put into a toString() method, as it is likely to wind up logged somewhere that you did not intend. You should be careful what methods you call in sensitive contexts such as writing to a log file, printing an exception or stack trace, System.out and System.err messages, or writing to data files.

  13. You also need to be careful about what is in memory. If the application crashes, it may generate a dump file which contains the values of everything in memory. For example, when calling the readPassword() method on Console, it returns a char[] instead of a String. This is safer because it will not be placed in the String pool, where it could remain in memory after that code that used it is run, and you can null out the value of the array element yourself rather than waiting for the garbage collector to do it. The idea is to have confidential data in memory for as short a time as possible.

  14. Imagine you are storing data in an Employee record. We want to write this data to a file and read this data back into memory, but we want to do this without writing any potentially sensitive data to disk. This can be achieved with serialization. Recall that Java skips calling the constructor when deserializing an object. This means validation performed in the constructor cannot be relied on.

  15. Consider the following class:

    import java.io.*;
    
    public class Employee implements Serializable {
        private String name;
        private int age;
    
        // Constructors/getters/setters
    }
  16. Recall that marking a field as transient prevents it from being serialized. Serialized fields can also be whitelisted by including them in a ObjectStreamField [] serialPersistentFields object. Security requirements may require us to implement custom serialization. We have a requirement to store the Social Security number, and we do need to serialize this information. However, we do not want to store it in plain text, so we will need to write some custom code. Consider the following updated class which uses custom read and write methods to securely encrypt and decrypt the Social Security number:

    public class Employee {
        private String name;
        private String ssn;
        private int age;
    
        // Constructors/getters/setters
    
        private static final ObjectStreamField[] serialPersistentFields = {
                new ObjectStreamField("name", String.class),
                new ObjectStreamField("ssn", String.class) };
    
        private static String encrypt(String input) {
            // Implementation omitted
        }
    
        private static String decrypt(String input) {
            // Implementation omitted
        }
    
        private void writeObject(ObjectOutputStream s) throws Exception {
            ObjectOutputStream.PutField fields = s.putFields();
            fields.put("name", name);
            fields.put("ssn", encrypt(ssn));
            s.writeFields();
        }
    
        private void readObject(ObjectInputStream s) throws Exception {
            ObjectInputStream.GetField fields = s.readFields();
            this.name = (String) fields.get("name", null);
            this.ssn = decrypt((String) fields.get("ssn", null));
        }
    }
  17. Some fields are too sensitive even for custom serialization. A password should never be decryptable. When a password is set for a user, it should be converted to a String value using a salt (initial random value) using a one-way hashing algorithm. Databases of stored passwords can get stolen. Having them properly encrypted means the attacker cannot do much with them.

  18. Sometimes an object can have different contents in memory versus on disk. When present, the readResolve() method is run after the readObject() method and is capable of replacing the reference of the object returned by deserialization. Similarly, if we want to write an object to disk but do not completely trust the instance we are holding, we will want to write the object in memory instead of what is in the this instance. When present, the writeReplace() method is run before writeObject() and allows us to replace the object that gets serialized. This is shown below:

  19. When constructing sensitive objects, you need to ensure that subclasses can't change the behaviour. This can be done by making methods final, making classes final, or making the constructor private.

  20. The person running your program will have access to the bytecode (.class) files, typically bundled in a JAR file. With the bytecode, they can decompile your code and get source code. It is not as well written as the code you wrote but has equivalent information. Using an obfuscator makes your decompiled bytecode harder to read and therefore harder to reverse engineer, it doesn't provide any security.

  21. A Denial of Service (DoS) attack can exploit poorly written code. This could include code that leaks resources (does not close a resource), creates very large resources, doesn't handle overflowing numbers correctly, or tries to exploit the limitations of data structures.

Practise Tests

Working with Java Data Types

  1. What will the following code print when run?

    public class TestClass {
        public static Integer wiggler(Integer x) {
            Integer y = x + 10;
            x++;
            System.out.println(x);
            return y;
        }
    
        public static void main(String[] args) {
            Integer dataWrapper = new Integer(5);
            Integer value = wiggler(dataWrapper);
            System.out.println(dataWrapper + value);
        }
    }
    • The printed output is 6 20.
  2. What will the following code print when run?

    public class TestClass {
        public static void main(String args[]) {
            Object obj1 = new Object();
            Object obj2 = obj1;
            if (obj1.equals(obj2)) System.out.println("true");
            else System.out.println("false");
        }
    }
    • This code will not compile as there are no brackets around the if-else statement. If there was only an if statement, it would compile but brackets are recommended for code style. If there were brackets the code would output true.
  3. Give the below code, what are the types of the variables a and b?

    public class TestClass {
        public void myMethod(String... params) {
            var a = params;
            var b = params[0];
        }
    }
    • The type of a is String[] and the type of b is String.
  4. You want to find out whether two strings are equal or not, in terms of the actual characters within the strings. What is the best way to do this?

    • Use String's equals method.
  5. What will be the result of attempting to compile and run the following program?

    public static void main(String args[]) {
        StringBuilder sb = new StringBuilder("12345678");
        sb.setLength(5);
        sb.setLength(10);
        System.out.println(sb.length());
    }
    • The printed output is 10.
  6. What will be the result of attempting to compile and run the following program?

    public static void main(String args[]) {
        int k = 1;
        int[] a = { 1 };
        k += (k = 4) * (k + 2);
        a[0] += (a[0] = 4) * (a[0] + 2);
        System.out.println(k + " , " + a[0]);
    }
    • The printed output is 25 , 25.
  7. What will be the result of attempting to compile and run the following program?

    public class SM {
        public String checkIt(String s) {
            if (s.length() == 0 || s == null) {
                return "EMPTY";
            }
            else return "NOT EMPTY";
        }
    
        public static void main(String args[]) {
            SM a = new SM();
            System.out.println(a.checkIt(null));
        }
    }
    • A NullPointerException is thrown because s is operated upon before the null check.
  8. Given the following class, which statements can be inserted at line 1 without causing the code to fail compilation?

    public class TestClass {
        int a;
        int b = 0;
        static int c;
    
        public void m() {
            int d;
            int e = 0;
            // Line 1
        }
    }
    • Variables a, b, c, and e can be incremented at Line 1. The variable d cannot be incremented as it has not been initialised. Unlike instance or static variables, local variables are not initialised automatically, and must be initialised if they are used.
  9. Which of these are valid expressions to create a string of value "hello world"?

    • The following are valid:
    System.out.println(" hello world".trim());
    System.out.println("hello".concat(" world"));
    System.out.println(
            new StringBuilder("world").insert(0, "hello ").toString());
  10. Given the following class, what can be done to make this code compile and run?

    public class Square {
        private double side = 0; // Line 2
        public static void main(String[] args) { // Line 4
            Square sq = new Square(); // Line 5
            side = 10; // Line 6
        }
    }
    • Line 6 needs to be replaced with sq.side = 10.
  11. What will the following program print when run?

    public class Operators {
    
        public static int operators() {
            int x1 = -4;
            int x2 = x1--;
            int x3 = ++x2;
            if (x2 > x3) {
                --x3;
            } else {
                x1++;
            }
            return x1 + x2 + x3;
        }
    
        public static void main(String[] args) {
            System.out.println(operators());
        }
    }
    • The output is -10.
  12. What will be the result of attempting to compile or run the following class?

    public class TestClass {
        public static void main(String args[]) {
            int i, j, k;
            i = j = k = 9;
            System.out.println(i);
        }
    }
    • The code will compile and print a value of 9.
  13. What will the below program print if compiled and run using the java Switcher 1 2 3 command line?

    public class Switcher {
        public static void main(String[] args) {
            switch (Integer.parseInt(args[1])) // 1
            {
            case 0:
                var b = false; // 2
                break;
    
            case 1:
                b = true; // 3
                break;
            }
    
            if (b) { // 4
                System.out.println(args[2]);
            }
        }
    }
    • It will fail to compile because at line 4 b is not defined.
  14. Which of the following operators can be used in conjunction with a String object?

    • The +, +=, and . operators can be used with String objects. There are no ++ or * operators for String.
  15. What will the following code print?

    public static void main(String[] args) {
        int i = 0;
        int j = 1;
        if ((i++ == 0) & (j++ == 2)) {
            i = 12;
        }
        System.out.println(i + " " + j);
    }
    • It will print 1 2.
  16. What will the following code print?

    public class TrimTest {
        public static void main(String[] args) {
            String blank = " "; // one space
            String line = blank + "hello" + blank + blank;
            line.concat("world");
            String newLine = line.trim();
            System.out.println((int) (line.length() + newLine.length()));
        }
    }
    • It will print 13. The concatenation does not assign a new value to line, and trim takes spaces from the beginning and end of a String.
  17. Which of the following is not a primitive data value in Java?

    • Strings and the Object class are not primitive data types. The primitive data types are boolean, byte, char, int, long, float, and double.
  18. What will be the output of the following program?

    public class SubstringTest {
        public static void main(String args[]) {
            String String = "string isa string";
            System.out.println(String.substring(3, 6));
        }
    }
    • It will print ing (note there is no space as the 6th character is excluded from the upper bound).
  19. Consider the following lines of code, what variables can you put in place of ? to cause the expression to evaluate to 'true'?

    boolean greenLight = true;
    boolean pedestrian = false;
    boolean rightTurn = true;
    boolean otherLane = false;
    ((rightTurn && !pedestrian || otherLane) || (? && !pedestrian && greenLight)) == true;
    • Any value is alright as the first part of the expression evaluates to true.
  20. Which of the following expressions will evaluate to true if preceded by the following code?

    String a = "java";
    char[] b = { 'j', 'a', 'v', 'a' };
    String c = new String(b);
    String d = a;
    • The expressions a == d, a == "java", and a.equals(c) will all evaluate to true. The expression b == d" cannot be used as the objects are of different types.
  21. What will the following code print

    String abc = "";
    abc.concat("abc");
    abc.concat("def");
    System.out.print(abc);
    • An empty String will be printed as the values as abc is not assigned any new value.
  22. What will the following code print?

    String string = " hello java guru ".strip();
    System.out.print(string);
    • It will print hello java guru as strip() will remove the spaces from the beginning and end of string.
  23. What will the following code print?

    int i1 = 1, i2 = 2, i3 = 3;
    int i4 = i1 + (i2 = i3);
    System.out.println(i4);
    • It will print 4.
  24. What will the following code print?

    String str1 = "str1";
    String str2 = "str2";
    System.out.println(str1.concat(str2));
    System.out.println(str1);
    • It will print str1str2 and str1.
  25. What type can be inserted in the code above so that the above code compiles and runs as expected?

    Byte condition = 1;
    switch (condition) {
        case 1:
            System.out.println("1");
            break;
        case 2:
            System.out.println("2");
            break;
        case 3:
            System.out.println("3");
            break;
    }
    • The types var condition = new Integer("1") and Byte condition = 1 types would allow the code to compile and run. Only String, byte, char, short, int, and their wrapper classes, and enums can be used as types of a switch variable.
  26. What will the following lines of code print?

    System.out.println(1 + 5 < 3 + 7);
    System.out.println((2 + 2) >= 2 + 3);
    • It will print true and false.
  27. Which of the following options will empty the contents of the StringBuilder referred to by variable sb and method dumpLog()?

    public class Logger {
        private StringBuilder sb = new StringBuilder();
    
        public void logMsg(String location, String message) {
            sb.append(location);
            sb.append("-");
            sb.append(message);
        }
    
        public void dumpLog() {
            System.out.println(sb.toString());
            // Empty the contents of sb here
        }
    }
    • The sb.delete(0, sb.length()) method can be used.
  28. Which of the above variables will have the value 45?

    int expr1 = 3 + 5 * 9 - 7;
    int expr2 = 3 + (5 * 9) - 7;
    int expr3 = 3 + 5 * (9 - 7);
    int expr4 = (3 + 5) * 9 - 7;
    • None of these variables will have the value 45.
  29. What will the following program print?

    public class TestClass {
        static String str = "Hello World";
    
        public static void changeIt(String s) {
            s = "Good bye world";
        }
    
        public static void main(String[] args) {
            changeIt(str);
            System.out.println(str);
        }
    }
    • It will print Hello World as the value of s is not returned and assigned to str.
  30. What will be the output of the following code snippet?

    int a = 1;
    int[] ia = new int[10];
    int b = ia[a];
    int c = b + a;
    System.out.println(b = c);
    • The output will be 1.

Java Object Oriented Approach

Controlling Program Flow

Arrays and Collections

Exception Handlng

Concurrency

Java I/O + NIO

Modules

JDBC

Streams and Lambda

Localization

Annotations

Security