Skip to content

Instantly share code, notes, and snippets.

@Theartbug
Last active April 5, 2019 16:23
Show Gist options
  • Save Theartbug/e8265dd9c32c66101dadca115a8199d0 to your computer and use it in GitHub Desktop.
Save Theartbug/e8265dd9c32c66101dadca115a8199d0 to your computer and use it in GitHub Desktop.
java-fundamentals: The Core Platform

In Summary

Persisting Objects with Serialization

  • serialization provides object persistence in files / databases
    • between processes / across networks
  • primitive types are implicitly serializable
  • Classes must implement Serializable
    • no methods to implement
  • ObjectOutputStream performs serializing / ObjectInputStream perofrms deserializing
  • Serial versoin unique identifier is used to determine version compatibility
    • java calculates by default, dependent on contents of type
      • changes to type will break compatibility
    • can explicitly set instead with serialVersionUID field and calculate initial value with serialver utility
      • value maintained across versions with manual control
  • we can customize serialization processes
    • writeObject called to serialize an object, recieves ObjectOutputStream
    • readObject called to deserialized an object, receives ObjectInputStream
    • the serialization process will use reflection to find these methods on your class
      • if they are present, serialization will use them
  • mark fields as transient to exclude fields from the serialization process
    • this is useful if the field can be derived
    • we can then manually set during the deserialization process

Adding type metadata with annotations

  • programs sometimes need metadata to capture context and intent
  • Annotations act as metadata and are a special kind of interface
    • do not change target behavior and must be interpreted
  • can create custom annotations in a similar way to declaring interfaces
    • use interface keyword proceeded by @
    • set retention to control availability in app
      • annotations do not live into runtime by default
    • set a target to narrow use (just types, methods, fields, etc)
  • annotations can be accessed with reflection
    • use getAnnotation method of target
  • annotations can optionally have elements that associate values with the annotation
    • these are declared as methods on the annotation
    • setting is similar to fields and can associate a default value
    • if the element name is value we do not have to include it when assigning a value, as long as it is the only element value we are setting

Runtime type information and reflection

  • every type is represented by instance of Class
    • each type has one instance of Class
  • you can access a type's Class instance multiple ways
    • from type reference by calling getClass()
    • from full string name by calling Class.forName()
    • from type literal by calling typename.class
  • All aspects of types are knowable
    • superclass and interfaces
    • fields, methods, constructors, paramater signatures
    • access modifiers (public, final, abstract)
  • working with modifiers is weird as they are returned as an int value
    • must use Modifier class to interpret
      • provides static fields for bit values and helper methods
  • reflection is not limited to describing types, can be used to access and invoke members
  • Objects can be created with reflection as constructors can be executed
    • use Constructor newInstance() method
    • handled even simpler for no-argument constructors
      • use class newInstance() method

Multithreading and Concurrency

  • Thread class represents a thread of execution
    • similar to most OS thread representations
    • responsible to handle most details
  • Runnable interface represents a task to run on a thread
    • you override run() method
    • cannot return results
    • exceptions are responsibility of the thread
  • ExecutorService abstracts thread management details and can interact with thread pools
  • Callable interface represents a task to be run on a thread
    • can return results and throw exceptions (unlike Runnable interface)
  • Future interface represents results of a thread task
    • can access results from task and can throw exceptions from thread task
  • All Java objects have a lock
    • can access with synchronized methods which acquire the lock of target instance of call
    • can only have one active method at a time on an object
    • can also manually acquire a lock by using a synchronized statement block
      • then available to any code referencing object

Capturing Application Activity with Java Log System

  • log system is centrally managed with one app-wide LogManager class
  • Logger class represents each individual logger and provides log methods
  • levels indicate relative importance of entry
    • each entry is recorded with a level
    • each Logger has a capture level
  • Loggers rely on other components:
    • Handlers publish log info and multiple can belong to a single Logger
    • Formatters format log info for publication and each Handler has 1 Formatter
  • Log configuration can be handled in code or within a file
    • filename is passed with system property
  • Loggers are heirarchical which is established through naming
    • Loggers can pass log entries to parents
    • Loggers can inherit parent log level
  • best set up:
    • manage setup primarily on parent loggers
    • make log call primarily on child loggers
    • have high level information recorded at the parent, finer detail recorded at child

Controlling App Execution and Environment

  • anything passed into the commandline when executing program is received to the main method
  • Properties class provides name / value pairs that persist across app executions
    • good for user preferences or simple app state
    • supports providing defaults which can be included in app package
  • class path controls where classes are found
    • set with CLASSPATH, but use sparingly since changing for one app can affect others
    • better to set using Java command -cp option, set specific to each app
  • execution environment info is available
    • Java provides system properties
    • OS environment variables are accessible which can provide app-specific variables

Working with Collections

  • Collections hold and organize values, they are iterable and tend to dynamically size
    • can provide optimations or sophistications
  • Collections can be type restricted
    • use Java generics to specify type
    • return values appropriately typed
    • typing enforced added values
  • you can convert between collections and arrays
    • toArray() and can get back a typed array / object
    • Arrays class' prvoes toList method
  • some collections provide sorting
    • support the Comparable interface
      • type defines own sort
    • support Comparator interface
      • specifies sort for another type
  • map collections store key / value pairs
    • keys are unique and some maps sort keys

String Formatting and Regular Expressions

  • StringJoiner simplifies combining sequence of values
    • construct with a value separator, eg. ", "
    • optionally specify start / end strings
  • Add the values and retrieve string
  • can specify a special value for empty (no values added)
    • "" (empty string) is considered something added
  • format specifiers describe the desired result in formatting
    • required: % (denotes format), conversion (octal, long, integer, etc.)
    • optional: precision (decimal places), flags (number grouping, left justify), argument index (which specifier goes with what value)
  • String class supports format specifiers
  • Formatter class writes formatted content to any class that implements the Appendable interface
  • Regular Expressions are a powerful pattern matching syntax
  • String class supports RegEx
    • replaceFirst / All() creates a new string
    • split() splits the string into an array
    • match() checks for matching values
  • There are dedicated regex classes
    • Pattern class compiles regular expressions
    • Matcher class applies pattern to a string

Input and Output with Streams and Files

  • the java.io package contains stream based I/O and legacy file/filesystem types
  • the java.nio.file package contains file/filesystem types (current)
  • Streams are a ordered sequence of data
    • unidirectional and can be binary or character-based
    • can be chained together
  • try-with-resources automates resource cleanup
    • resources implement AutoCloseable interface
  • Path instance locates a file system item and includes the file system
    • default or zip file system
  • Paths instance is a factory for Path instances for default file system
  • Files type gives us methods for interacting with files
  • FileSystem type represents a file system
    • can have specialized such as a zip
    • identified by URIs jar:file
  • FileSystems type gives methods for creating / opening file system

Persisting Objects With Serialization

OverView

  • Java can persist objects from runtime into a byte stream
    • can restore from byte stream into runtime
  • most cases are simple to accomplish
    • leverages reflection
    • operates only on instance members
    • customizable
  • saving objects opens possibilities
    • saving state to file system / database
    • pass across memory address boundaries / network
  • serialization will store the object you give it, as well as any other objects it is linked to (object-graph)
    • stores object-graph to byte stream
    • deserialization will restore an object-graph from byte stream
  • Serialization types:
    • Serializable
      • implemented by serializable types
      • indicates that type supports serialization
      • has no methods
    • ObjectOutputStream
      • serializes object-graph to stream
    • ObjectInputStream
      • Deserializes stream to object-graph

Being Serializable

  • implement Serializable interface
    • no methods, just a marker
    • type members must be serializable
      • Primitive types are serializable by default
      • Methods must implment Serializable
  • Example: Being Serializable
// implement Serializable once confirmed all fields can be serialized
public class BankAccount implements Serializable {
   // String is not a primitive type, is stored in an Object String reference
   // BankAccount Class will have reference to that String Object stored in it
   // String implements Serializable, so we good
   private final String id;
   // primitive type stored on the BankAccount reference itself
   private int balance = 0;

   public BankAccount(String id) {...}
   public BankAccount(String id, int balance) {...}
}
  • Example: Serializing and Object
void saveAccount(BankAccount ba, String filename) {
   try(ObjectOutputStream objectStream = new ObjectOutputStream(Files.newOutputStream(Paths.get(filename)))) {
      // write the object to a new stream into filename
      objectStream.writeObject(ba)
   } catch (IOException e) { ... }
}
  • Example: Deserialization
BankAccount loadAccount(String filename) {
   BankAccount ba = null;
   
   try(ObjectInputStream objectStream = new ObjectInputStream(Files.newInputStream(Paths.get(filename)))) {
      // type cast it
      ba = (BankAccount) objectStream.readObject();
   } catch (IOException e) {
      ...
   } catch (ClassNotFoundException e) {...}
   // return the ref
   return ba;
}

Class Version Incompatibility

  • will get an InvalidClassException if class was updated and saved class no longer matches
  • Java knows that the classes do not match because of the Serial Version Unique Identifier
    • Java stores this number with with class during serialization
    • Java compares this number with the current class' number (serializes the current class to compare)

Creating Class version compatability

  • SVUI (Serial version unique identifier) indicates version incompatibility
    • compatible versions will have same value
  • Java calculates SVUI at runtime and the value is affected by multiple factors
    • type name, implemented interfaces, members
  • can specify serial version as part of type definition
    • we can then determine compatibility
  • specify serial version with serialVersionUID field on type
    • must be a long number and static final
    • should be private
  • calculate for initial version of type using serialver utility then use the same value for future versions
    • how compatibility is maintained
  • serialver utility preforms the same calculation as Java runtime
    • found in JDK bin folder (IDEs often provide plugin)
      • uses class' class file
      • will search in local folder or you can specify -classpath
    • can pass class name on command line
      • displays value to console
    • can use -show option to use a GUI
    • private static final long serialVersionUID = -234562652345345634L;
  • during serialization, any added uninitialized member will be given a default value
    • characters given null, integer given 0
    • fields that are removed will be removed in serialization

custom serialization

  • can be added to class with writeObject and readObject methods to type
    • these methods are called through reflection, thus should make them private
  • implementing writeObject method
    • return type of void
    • include throws clause (possible IOException)
    • accepts ObjectOutputStream used to write values
      • use defaultWriteObject for default behavior
      • useful when you want your own code to run during serialization process, but you don't want to do anything special
  • implementing readObject method
    • return type of void and includes throws clause (IOException and ClassNotFoundException)
    • accepts ObjectInputStream used to read values
      • reads in order things were stored
      • can use readFields to get field name info, thus can access by name
    • defaultReadObject for default behavior
  • Example: Customizing serialization
// method inside class that implements Serializable
private void writeObject(ObjectOutputStream out) throws IOEception {
   // for standard serialization to happen
   out.defaultWriteObject();
}
private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException {
   // obtain fields
   ObjectInputStream.GetField fields = in.readFields();
   // access specific fields
   id = (String) fields.get("id", null);
   balance = fields.get("balance", 0);
   // access fields that are not in all versions
   // if they return nothing, assign an obvious value to demonstrate that 
   lastTxType = fields.get("lastTxType", 'u');
   lastTxAmount = fields.get("lastTxAmount", -1);
}

Transient Fields

  • sometimes we don't want / need fields serialized
    • when fields are derived from one another
    • avoids unnecessary storage
  • use transient keyword to exclude from serialization
    • then restore the field value manually during custom deserialization
  • Example: transient fields
public class AccountGroup implements Serializable {
   // HashMaps are serializable
   private Map<String, BankAccount> accountMap = new HashMap<>();
   // totalBalance can be calculated on deserialization
   private transient int totalBalance;
   public int getTotalBalance() { return totalBalance; }
   public void addAccount(BankAccount account) {
      totalBalance += account.getBalance();
      accountMap.put(account.getId(), account);
   }
   // need custom deserialization to manually re-create totalBalance
   void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException {
      in.defaultreadObject();
      for(BankAccount account : accountMap.values()) totalBalance += account.getBalance();
   }
   // no need for writeObject since it behaves as default
}

void saveGroup(AccountGroup g, String filename) {
   try(ObjectOutputStream objectStream = new ObjectOutputStream(Files.newOutputStream(Paths.get(fileName)))) {
      objectStream.writeObject(g);
   } catch(IOException e) { ... }
}

Adding Type Metadata with Annotations

The need to express context and intent

  • programs do not stand alone, they fit into a larger picture
    • assumptions are made by devs about the system type, toolset, execution environment
  • programs incorporate context and intent
    • type system solves much of this issue, but standard typing isn't enough
  • Example: when typing isn't enough to give context
    • when a type implicitly extends the Object class and overwrites the toString method, there is a possibility the dev spells toString wrong as toSring and the intent would not be known
      • there is little difference between overwritting a method vs declaring a new one
    • how to let intent be known?
  • need a way to extend the type system
    • often try to supplement manually with comments, documentation
    • need a more structured solution

Use Annotations

  • are special types that act as metadata and are applied to a specific target
  • have no direct impact on target / don't change target behavior
  • annotations must be interpreted by tools, exec environments, programs
  • annotations in code will have @ preceeding the name and placed directly before target
    • allowable targets vary with annotation
  • Example: Expressing intent with Override Annotation
@Override // place above toString 
public String toString() { ... }
  • compiler looks for methods marked with this annotation and verifies there is a method matching signature that can be overridden
  • will generate an error if there is no method with that signature on parent classes
  • Java provides a few annotations out-of-box
    • Override, Deprecated, SuppressWarnings
    • but also provides types to create annotations
  • Example: Depricated annotations
class Does {
   //gives a warning in the compilation
   @Depricated
   void doItThisWay() {...}
   // devs add this new way to do something and want other devs consuming this code to migrate over
   void doItThisNewWay() {...}
}

// if you want to supress those warnings and deal with migration later
// DANGEROUS to suppress at class level, since you could supress
// warnings you don't even know about
// instead supress as close to the methods as possible
@SuppressWarnings("deprecation")
class MyWorker { ... }

Declaring Annotations

  • you can create custom annotations that act as custom meta data and provides the same capabilities as built-in annotations
  • Example: Flexible work dispatch system
    • we will take this class and expand its thread requirements:
      • can create own thread
      • can be run on app's thread pool
      • preference indicated with annotation (NEW)
  • annotations are a special kind of interface, but useage is more restricted
    • cannot be explicitly implemented
    • all custom annotations will implicitly extend the Annotation interface
    • the interface behavior is not initially apparent
  • declare annotations similarly to interfaces
    • use the interface keywork preceeded with @ symbol
    • can have same type modifiers that interfaces can public , private
    • declarations are allowed in the same places as interfaces (inside the of classes, etc.)
  • annotations can optionally have elements
    • these associate values within an annotation
    • declared exactly like a method
    • setting elements is similar to how fields are set
  • Example: Declaring Annotations
// declare annotation
public @interface WorkHandler {
   boolean useThreadPool();
}

// add annotation to class
// set element of annotation like you would a field
@WorkHandler(useThreadPool = false)
public class AccountWorker implements Runnable, TaskWorker {
   BankAccount ba; 
   public void setTarget(object target) { ... }
   public void doWork() {
      // check to see if instance implements runnable, if so type cast it back into ba, else use this class
      Thread t = new Thread(
         Runnable.class.isInstance(ba) ? (Runnable)ba : this);
      t.start();
   }
   public void run() { ... }
}

Accessing Annotations

  • can access annotations through reflection, just like regular classes
    • use getAnnotation() on type / member, pass in Class of annotation
    • returns a reference to an annotation interface
      • returns null if does not have annotation of requested type
  • Example: accessing annotations
void startWork(String workerTypeName, Object workerTarget) throws Exception {
   try {
      Class<?> workerType = Class.forName(workerTypeName);
      TaskWorker worker = (TaskWorker) workerType.newInstance();
      worker.setTarget(workerTarget);
      // obtain reference to annotation
      // behaves similar to interface in this manner
      WorkHandler wh = workerType.getAnnotation(WorkHandler.class);
      if(wh == null) // throw exception
      // call it like a method, it will return the value that was set on the Class
      if(wh.useThreadPool())
         pool.submit(new Runnable() { // anonymous class Runnable
            public void run() {
               worker.doWork();
            }
         });
      else worker.doWork();
   } catch(Exception e) { ... }
}
  • this will not return the annotation since they are not available at runtime

Annotation Target and Retention

  • can specify availablility (at runtime vs ...)
    • this is part of annotation declaration
    • indicated with @Retention annotation
      • accepts RetentionPolicy value
  • Retention Policy Values:
    • Source: exists only in source file, compiler throws away so not even Class file can access it
    • Class: exists in class file, compile brings into Class file, discarded at runtime (IS DEFAULT)
    • Runtime: loaded in runtime, accessible with reflection
  • Source and Class are meant more for tool building (things that analyze source code or Class files)
  • Runtime is helpful to any application dev
@Retention(RetentionPolicy.RUNTIME)
public @interface WorkHandler {
   boolean useThreadPool();
}
  • right now our annotation can be applied to anything (Class, Field, Method, etc.)
  • Annotations can narrow allowable targets via @Target
    • part of annotation declaration
    • accepts ElementType value
    • can accept multiple targets with Array notation
  • Example: Annotation Retention
Target(ElementType.CONSTRUCTOR)
Target( { ElementType.TYPE, ElementType.METHOD } )

...
// order does not matter here
@Target(ElementType.TYPE);
@Retention(RetentionPolicy.RUNTIME)
public @interface WorkHandler {
   boolean useThreadPool();
}
  • will throw an error if we place annotation on field, method, etc.

A Closer Look at Elements

  • Elements can be setup to simplify setting
    • handle common cases / default values
    • there is assignment shorthand
  • Elements can be declared with a default value
    • set with default keyword
    • can still explicitly set if desired
@Target(ElementType.TYPE);
@Retention(RetentionPolicy.RUNTIME)
public @interface WorkHandler {
   // set default
   boolean useThreadPool() default true;
}

...
// utilize default
@WorkHandler
public class AccountWorker implements Runnable, TaskWorker { ... }

// set to something other than default
@WorkHandler(useThreadPool = false)
public class AccountWorker implements Runnable, TaskWorker { ... }
  • you can assign an element value without including the element name
    • must only set one element
    • that element must have a name of value
    @Target(ElementType.TYPE);
    @Retention(RetentionPolicy.RUNTIME)
    public @interface WorkHandler {
       // use value instead
       boolean value() default true;
    }
    
    ...
    // dont need to specify name of element
    @WorkHandler(false)
    public class AccountWorker implements Runnable, TaskWorker { ... }
    
  • can only use certain types for elements
    • Primitive (boolean)
    • String
    • Enum
    • Annotation (an annotation can have an element which is another annotation
    • Class<?> (can store type information inside of them)
    • Array (must be one of the types shown here)
  • Example: Annotation Class<?> Element
Bank Account acct1 = new BankAccount();
startWork("com.jwhh.utils.AccountWorker", acct1);
  • allows us to have workers of types we didn't know about at compile time
  • but if we know about what worker we want to use at compile time, can pair it to the class to be worked on
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface ProcessedBy {
   // no default
   Class<?> value();
}

// add the worker at complie time 
@ProcessedBy(AccountWorker.class)
public class BankAccount { ... }

...

BankAccount acct = new BankAccount();
// create a new function that will read the metadata (annotation)
// and work from there
startWorkSelfContained(acct);

void startWorkSelfContained(Object workerTarget) throws Exception {
   Class<?> targetType = workerTarget.getClass();
   // grab the target annotation (metadata)
   ProcessedBy pb = targetType.getAnnotation(ProcessedBy.class);
   Class<?> workerType = pb.value();
   // create instance and cast the workerType into TaskWorker
   TaskWorker worker = (TaskWorker) workerType.newInstance();
   // remainder of code like startWork method
}

Runtime Type Information and Reflection

Overview

  • Reflection allows us to examine types at runtime
    • can dynamically execute and access members
  • Apps do not always control the types used
    • common in advanced app designs, tools, frameworks
  • often need to dynamically load types
    • load types that we did not know about at compile time!
    • there is no type-specific source code available
  • we can examine objects as runtime
    • gain access to Type, Base types, Interfaces implemented, Members
    • variety of uses:
      • types capabilities, tools development (type inspector / browser / schema generation)

Dynamic Execution and Access

  • Can access full capability of type (construct instances, access fields, call methods)
  • variety of uses
    • configurable application designs
    • specifics tasks externally controlled
    • inversion of control application designs
      • app provides fundamental behavior
      • classes added to specialize behavior

Type as a Type

  • type is the foundation of any app solution
    • we use types to model business / tech issues
  • java uses types to model type issues
    • the fundamental type is the Class class
      • each type has a Class instance that describes the type in detail
  • Example: Class that describes BankAccount
    • will contain a simpleName
      • BankAccount
    • has fields
      • id and balance
    • constructors
      • BankAccount and BankAccount
    • methods
      • getId, getBalance, deposit, withdrawal
  • The Class instance of our type will act as a memory model at runtime
  • The relationship between Class and our type is not just for creation time
    • the type has a member that can access the Class that describes it
  • Each type instance will have access to the same Class instance that models our type
    • means we can access information about a type we don't have source code to!

Accessing a Types Class Instance

  • from type reference use getClass() method
  • from string name use Class.forName() static method
    • must be sure the name is a fully qualified type
  • from type literal typeName.class
  • Example: class instance access
void showName(Class<?> theClass) { // the Class is a generic type since we won't know the class going in
   // do something with class
}
// from type reference
void doWork(object obj) {
   Class<?> c = obj.getClass();
   showName(c);
}

// from String Name
Class<?> c = Class.forName("com.jwhh.finance.BankAccount"); // pass in full package name

// from Type Literal
Class<?> c = BankAccount.class; // returns type descriptor
Class<BankAccount> c = BankAccount.class // can be rewritten since we know the type we are working with
  • the way the Class is accessed does not change what is returned
    • remains one type descriptor across all type instances

Accessing Type Information

  • Once a Class type is accessed, every aspect of a type is knowable
    • Superclass, implmented interfaces, modifiers, members
  • Example: Accessing type information
public final class HighVolumeAccount extends BankAccount implements Runnable {
   public HighVolumeAccount(String id) { super(id); }
   public HighVolumeAccount(String id, int balance) { super(id, balance); }
   
   private int[] readDailyDeposits() { ... }
   private int[] readDailyWithdrawls() { ... }
   
   public String run() {
      for(int depositAmt: readDailyDeposits()) deposit(depositAmt);
      for(int withdrawalAmt:readDailyWithdrawls()) withdrawal(withdrawalAmt);
   }
}

...

void classInfo(Object obj) { // pass reference to HighVolumeAccount
   Class<?> theClass = obj.getClass(); // points to Class model
   System.out.println(theClass.getSimpleName()); // HighVolumeAccount
   Class<?> superclass = theClass.getSuperClass(); // returns BankAccount
   Class<?> interfaces = theClass.getInterfaces();
   for(Class<?> interface: interfaces) System.out.println(interface.getSimpleName()); // returns Runnable
}
  • the Class class has a method isInterface() that returns true if the type is an interface
  • to retrieve a type access modifier use getModifiers()
    • returns a single int value that represents all the modifiers
    • each modifier is a separate bit of that int
  • use Modifier class to interpret modifiers
    • provides static fields for bit comparisons
      • requires use of bitwise and / or (useful for deep level computing)
    • usually just want to know if that modifier is used
      • static helper methods exist, each check for a specific modifier
      • returns true / false
  • Example: retrieving type access modifiers
void typeModifiers(Object obj) {
   Class<?> theClass = obj.getClass();
   int modifiers = theClass.getModifiers(); // returns integer with bits set for all modifiers present
   
   if((modifiers & Modifier.FINAL) > 0) // bitwise check - final, less common
   if(Modifier.isFinal(modifiers)) // method check, more common
   
   if(Modifier.isPrivate(modifiers)) // method check, more common
   else if(Modifier.isProtected(modifiers)) // method check, more common
   else if(Modifier.isPublic(modifiers)) // method check, more common
}

Accessing Type Member information

  • In review:
    • Field - Name, Type
    • Method - Name, Return type, Parameter types
    • Constructor - Name, Parameter types
  • If we want members that are declared on the type itself, public, protected, and private will be returned
    • getDeclaredFields(), getDeclaredMethods(), getDeclaredConstructors()
  • if we want memebers that are declared and inherited, public will only be returned
    • getFields(), getMethods(), getConstructors()
    • constructors cannot be inherited, so getConstructors() only shows the public constructors of current type
  • when using .getMethods(), these will include all inherited public methods, even from the Object class like .equals(), .toString(), etc.
    • to exclude unwanted class methods use .getDeclaringClass() and compare it to the Class model
    • m.getDeclaringClass() != Object.class check that the class is not Object
      • can use != since there is only one Class reference of a type
  • to access an individual member by signature:
    • getField(): pass name
    • getMethod(): pass name plus parameter types
    • getConstructor(): pass parameter types
  • members have access to modifiers
    • use getModifiers() and then interpret with Modifier class

Interacting with Object Instances

  • Reflection is not limited to describing types
    • can access and invoke members
  • In order to call methods of an object instance, the reference variable has to be of the matching type
    • BankAccount reference can call methods on the BankAccount type, Object reference cannot
      • Object lacks the information necessary to make the call
    • can retrieve the proper type reference information by retrieving the Class model
      • using the method information returned, then Object can call methods on BankAccount
  • Example: method access with reflection
BankAccount acct1 = new BankAccount("1234");
callGetId(acct1);

void callGetId(Object obj) {
   try { // must handle errors (possibly not find method)
      Class<?> theClass = obj.getClass();
      Method m = theClass.getMethod("getId"); // getId has no paramaters, so name alone is fine
      Object result = m.invoke(obj); // store the result of the invocation
      // do something with result
   } catch(Exception e) { ... }
}

...

BankAccount acct1 = new BankAccount("1234", 500); // initialize with $500
callDeposit(acct1, 50); // deposit 50

void callDeposit(Object obj, int amt) {
   try {
      Class<?> theClass = obj.getClass(); // get the class model
      Method m = theClass.getMethod("deposit", int.class); // give method signature
      // int.class gives the type description of int
      m.invoke(obj, amt); // invoke the method
   } catch(Exception e) { ... }  
}

// since the method deposit was invoked on the same instance of acct1
// will have that deposit in the accout
System.out.println("Balance: " + acct.getBalance()); // 550
  • reflection or traditional method calls will point to the same object instance
  • reflection is slower than regular method calls

Instance creation with Reflection

  • Objects can be created with reflection
    • constructors can be executed with Constructor.newInstance() method
      • returns a reference to the new instance
    • simplified handling for no-arg constructor with Class.newInstance() method
      • no need to access constructor directly
    • fairly common way of creating classes
  • The desire is to create a flexible work dispatch system
    • executes worker classes against targets
      • build in a way so we don't have to know what all the worker classes are
    • can use any worker in classpath
      • just pass the type name of the worker class, and everything else should work from there
  • the method to start work accepts to arguments
    • name of worker type: String reference
    • target of work: Object reference
  • worker has type requirements
    • constructor that accepts target type
    • doWork method that takes no arguments
    • allows us to create an instance of the worker, passing in the target, and begin doing work
  • Example: Worker Class
public class AccountWorker implements Runnable {
   BankAccount ba;
   HighVolumneAccount hva;
   public AccountWorker(BankAccount ba) { ... } // assigns reference to field
   public AccountWorker (HighVolumeAccount hva) { ... } // assigns reference to field
   public void doWork() {
      // if the hva exists, pass that in directly since it already implements runnable
      // if not, have AccountWorker do the work for ba
      Thread t = new Thread(hva 1= null ? hva : this); 
      t.start();
   }
   public void run() {
      char txType = // read tx type
      int amt = // read tx amout
      if(txType == 'w') ba.withdrawal(amt);
      else ba.deposit(amt);
   }
}
   // fully qualified type name of worker and reference
   void startWork(String workerTypeName, Object workerTarget) {
      try {
         Class<?> workerType = Class.forName(workerTypeName); // obtain class model of worker
         Class<?> targetType = workerTarget.getClass(); // obtain class model of type to be created
         Constructor c = workerType.getConstructor(targetType); // obtain the constructor from worker, passing in the targetType reference
         Object worker = c.newInstance(workerTarget); // create instance of worker
         Method doWork = workerType.getMethod("doWork"); // get method
         doWork.invoke(worker); // invoke method passing in worker
      } catch(Exception e) { ... } // a number of exceptions could occur
   }

Instance creation with reflection

  • We can update our flexible work dispatch system (same requirements as before)
    • method to start work sill accepts same 2 arguments as before
  • Worker type requirements should change to be more flexible
    • now should have a no-argument constructor
    • implements our own TaskWorker interface:
    public interface TaskWorker {
       void setTarget(Object target);
       void doWork();
    }
    
  • Example: worker implementing interface
public class AccountWorker implements Runnable, TaskWorker {
   BankAccount ba; // since HighVolumeAccount inherits from BankAccount, don't need to place here
   public void setTarget(object target) { // could protect with generics, but will keep as object for simplicity
      if(BankAccount.class.isInstance(target)) ba = (BankAccount)target; // type cast
      else throw new IllegalArgumentException( ... );
   }
   public void doWork() {
      // check to see if instance implements runnable, if so type cast it back into ba, else use this class
      Thread t = new Thread(
         Runnable.class.isInstance(ba) ? (Runnable)ba : this);
      t.start();
   }
   public void run() { ... }
}

...

void startWork(String workerTypeName, Object workerTarget) {
   try {
      Class<?> workerType = Class.forName(workerTypeName); // get type information for worker
      // since we now have a no argument constructor, we don't have to get the constructor information
      // type cast it to the interface we know it has
      TaskWorker worker = (TaskWorker) workerType.newInstance();
      // treat the rest like regular programing, no need for reflection
      worker.setTarget(workerTarget);
      // since we know the interface methods, we can simply call them!
      worker.doWork();
   } catch(Exception e) { ... }
}
  • only use just as much reflection as needed

Multithreading and Concurrency

Basics

  • Process
    • is the instance of a program / application
    • has resources such as memory
    • has at least once thread, can have multiple
  • Thread
    • sequence of programmed instructions
    • the thing that executes a program's code
    • utilizes process resources
    • one thread can create another thread and so fourth
    • multiple threads are can have concurrency
      • working on tasks at the same time
    • issues happen when multiple threads attempt ot access the same resource
      • must coordinate so threads do not introduce errors

Move to Multithreading

  • Enables more complete CPU use
    • threads often wait on non-CPU tasks (interacting with storage, networks, etc.)
    • most computers have multiple CPU cores (allows things to run in parallel)
  • can reduce perceived execution times
    • less real-time passes for user
  • Example: adder class multithreading
class Adder {
   private String inFile, outFile;
   public Adder(String inFile, String outFile) { // assigns file names to member fields}
   public void doAdd() throws IOException {
      int total = 0;
      String line = null;
      // since working with storage, not currently using CPU
      try (BufferedReader reader = Files.newBufferedReader(Paths.get(inFile))) { 
         while ((line = reader.readLine()) != null) total += Integer.parseInt(inline);
      }
      try (BufferedWriter writer = Files.newBufferedWriter(Paths.get(outFile))) {
         writer.write("Total: " + total);
      }
   }
}

...

String[] inFiles = {"./file1.txt, ... , "./file6.txt"};
String[] outFiles = {"./file1.out.txt, ... , "./file6.out.txt"};
// single-threaded way
try {
   for(int i=0; i < inFiles.length; i++) { // loop over the files
      Adder adder = new Adder(inFiles[i], outFiles[i]);
      adder.doAdd();
   }
} catch(IOException e) {
   // do something
}
  • multithreading is an explicit choice
    • don't get it for free, must break the problem up into parts to utilize
    • must handoff the parts for processing
  • java provides different levels of abstraction
    • supports very direct handling with manual creation & coordination
    • supports higher level handling with simplified creation and coordination

Java Threading Foundation

  • limited threading abstraction
    • very close to standard OS behavior
    • each thread starts for a specific task and terminates at end of task
  • requires explicit management
  • exceptions are tied to a single thread, each must handle own exceptions
  • Runnable interface
    • represents a task to be run on a thread
    • run() is only member
  • Thread class represents a thread of execution
    • can interact with and effect thread state
    • start() begins thread running process
  • Example: Adder class with Threading support
class Adder implements Runnable { //implements Runnable interface
   private String inFile, outFile;
   public Adder(String inFile, String outFile) { // assigns file names to member fields}
   public void doAdd() throws IOException { ... }
   public void run() { // can now implement the run method from the Runnable interface
      try {
         doAdd(); // simply add the doAdd() method for the doAdder class to know how to be run on alternate threads
      } catch(IOException e) { ... }
   }
}

...

String[] inFiles = {"./file1.txt, ... , "./file6.txt"};
String[] outFiles = {"./file1.out.txt, ... , "./file6.out.txt"};
// multi-threaded way
// no longer need try-catch block here since its handled inside of Adder
   for(int i=0; i < inFiles.length; i++) { // loop over the files
      Adder adder = new Adder(inFiles[i], outFiles[i]);
      Thread thread = new Thread(adder); // pass the class into the Thread class
      thread.start(); // starts a new thread for each file with each loop!
   }

  • potential problem: if the main thread terminates after firing off multiple threads, those threads will also terminate in the main thread cleanup process
    • solution: have the main thread wait until all threads are finished
  • Example: make sure main thread waits to terminate
String[] inFiles = {"./file1.txt, ... , "./file6.txt"};
String[] outFiles = {"./file1.out.txt, ... , "./file6.out.txt"};
// multi-threaded way
Thread[] threads = new Thread[inFiles.length];
for(int i=0; i < inFiles.length; i++) { // loop over the files
   Adder adder = new Adder(inFiles[i], outFiles[i]);
   Thread[i] = new Thread(adder); // add to array of threads
   thread[i].start(); // starts a new thread for each file with each loop!
 }

for(Thread thread:threads) thread.join(); // allows for all threads to complete
  • this code could introduce problems if instead there were 1000 files or each files was very large
    • would choke the system

Thread Pools

  • Thread Class allows for direct control over thread startup, shutdown, & coordination
    • can be challenging to efficiently manage
    • can be easily misused
  • Thread Pools abstract thread management
    • creates a queue for tasks
    • assigns tasks into a pool of threads
    • Handles details of managing threads
  • ExecutorService interface
    • Models thread pool behavior
    • can submit tasks
    • request and wait for pool shutdown
  • Executor class
    • offer methods for creating thread pools
    • can create dynamically sized pools
    • can limit pool size
    • can schedule tasks in pools for later
  • Example: thread pool
String[] inFiles = {"./file1.txt, ... , "./file6.txt"};
String[] outFiles = {"./file1.out.txt, ... , "./file6.out.txt"};
// multi-threaded way
ExecutorService es = Executors.newFixedThreadPool(3); // does not allow more than 3 threads at a time to exist
for(int i=0; i < inFiles.length; i++) { // loop over the files
   Adder adder = new Adder(inFiles[i], outFiles[i]);
   es.submit(adder); // give adder to exectuor to manage threads
 }
try {
   es.shutdown(); // allows for all threads to complete, does not allow new work to be submitted
   es/awaitTermination(60, TimeUnit.SECONDS); // will wait up to 60 seconds for the pool to shutdown
} catch (Exception e) { ... }

Creating a closer relationship between thread tasks

  • Often times threading is coupled (main thread needs results from worker)
    • whether the background task succeeded
  • Working thread must place results / error exception in a place in memory where main thread can look and access
  • Callable interface represents a task to be run on a thread
    • can return results / throw exceptions
    • only member is the call() method
  • Future interface represents results from a thread task
    • returned by ExecutorService.submit
    • key method get()
      • will block until task completes
      • returns callable interface result
      • throws callable interface exception
  • Example: Adder method returns a value
class Adder implements Callable <Integer> { //implements Callable interface and returns Integer reference type
   private String inFile, outFile;
   public Adder(String inFile, String outFile) { // assigns file names to member fields}
   public int doAdd() throws IOException { // change return type to int
      int total = 0;
      String line = null;
      // since working with storage, not currently using CPU
      try (BufferedReader reader = Files.newBufferedReader(Paths.get(inFile))) { 
         while ((line = reader.readLine()) != null) total += Integer.parseInt(inline);
      }
      return total; // returns total instead of writing it elsewhere
   }
   public Integer call() throws IOException { // implements call
     return doAdd(); // do not need to handle doAdd() catch here
   }
}

...

String[] inFiles = {"./file1.txt, ... , "./file6.txt"};
ExecutorService es = Executors.newFixedThreadPool(3); // does not allow more than 3 threads at a time to exist
Future<Integer>[] results = new Future[inFiles.length]; // will collect the future results
for(int i=0; i < inFiles.length; i++) { // loop over the files
   Adder adder = new Adder(inFiles[i], outFiles[i]);
   results[i] = es.submit(adder); // place results into Future array
 }

...
// then retrieve results

for(Future<Integer> result:results) {
   try {
      int value = result.get(); // blocks until return value is available
      // do something with value
   } catch(ExecutionException e) { // catches exception raised in Adder
      Throwable adderEx = e.getCause(); // get the exception
      // do something with adderEx
   }
}

Concurrency issues

  • challenge: threads sometimes share resources
    • not a problem if resources are only read
    • changes must be coordinated
  • failure to coordinate can cause problems
    • receive wrong results / program crash
  • Example: introduction to concurrency
public class BankAccount {
   private int balance;
   public BankAccount(int startBalance) {
      balance = startBalance;
   }
   
   public int getBalance() {
      return balance;
   }
   
   public void deposit(int amount) {
      balance += amount;
   }
}

...

public class Worker implements Runnable {
   private BankAccount account;
   public Worker(BankAccount account) {
      this.account = account;
   }
   public void run() {
      for(int i=0; i < 10; i++ {
         int startBalance = account.getBalance();
         account.deposit(10);
         int endBalance = account.getBalance();
      }
   }
}

// Running on a single thread

ExecutorService es = Executors.newFixedThreadPool(5);
BankAccount account = new BankAccount(100);

for(int i = 0; i < 5; i++) { // start multiple workers at once
   Worker worker = new Worker(account);
   es.submit(worker);
}

// shutdown es and wait
  • the above code will loop through the thread pool and start 5 threads at once
    • each add $10 per loop
  • since they are all working on the same BankAccount class, who knows what the final accountBalance will be?
  • Workers may have stale state
  • reason why these multiple workers failed to always get the correct accountBalance is:
    • balance += amount; is non-automic processed (has multiple steps that do not happen at one time)
  • one thread might read a value prior to another thread writing an updated value (stale state)

Coordinating Method Access

  • synchronized methods help coordinate thread access to methods
    • acts as a method modifier and a class can have as many as needed
    • use to protect modification by multiple threads
    • use to protect reading a value that may be modified by another thread
  • synchronization is managed per instance of class
    • no more than one thread can be in any synchronized method at a time
  • do not want to always synchronize due to significant overhead
    • only use in multithreading scenarios
  • constructors are never synchronized
    • an object instance is always created on exactly one thread
  • Example: synchronize methods
public class BankAccount {
   private int balance;
   public BankAccount(int startBalance) {
      balance = startBalance;
   }
   
   public synchronized int getBalance() { // prevent multiple thread reading
      return balance;
   }
   
   public synchronized void deposit(int amount) { // prevent multiple thread access
      balance += amount;
   }
}
  • if one thread is in deposit(), do not allow another thread to enter getBalance(), visa versa
  • when a single thread is attempting to access object instance, will check to see if instance is locked
    • every instance has a single possible lock place
    • if not locked, will acquire the lock position and can move forward into synchronized methods
    • another thread comes along and checks that lock, if locked, waits
    • once previous thread exits the deposit() method, waiting thread will be alerted and will place new lock on instance

Manual synchronization

  • synchonized methods allow for automated concurrency management
    • use the lock of current object instance
  • all java objects have a lock
    • you can manually acquire that lock via a synchronized statement block
    • available to any code with a reference
  • Example: Synchronized statement block
class BankAccount {
   // ...
   public void deposit(int amount) { // remove synchronized
      balance += amount; 
   }
}

class Worker implements Runnable {
   private BankAcocunt account;
   // ...
   public void run() {
      for(int i=0; i<10; i++) {
         synchronized(account) { // add synchronize manually to the BankAccount working on
            account.deposit(10);
         }
      }
   }
}
  • synchronize statement blocks provide flexibility
    • enables use of non-thread safe classes
    • can protect complex blocks of code (don't need a method to do it)
      • right in the middle of another method

Manually Synchronized Code

  • Example: updated Bank Account
public class BankAccount {
   private int balance;
   public BankAccount(int startBalance) { balance = startBalance; }
   
   public synchronized int getBalance() { return balance; }
   
   public synchronized void deposit(int amount) { balance += amount; }
   
   public synchronized void withdrawl(int amount) { balance -= amount; }
}

...

public class TxWorker implements Runnable { // transaction worker
   protected BankAccount account;
   protected char txType; // 'w' = withdrawl, 'd' = deposit
   protected int amt;
   public TxWorker(BankAccount account, char txType, int amt) { ... }
   public void run() { // we know the methods are protected
      if (txType == 'w') account.withdrawl(amt);
      else if (txType == 'd') account.diposit(amt);
   }
}

...

ExecutorService es = Executors.newFixedThreadPool(5);
TxWorker[] workers = // retrieve TxWorker instances

for (TxWorker worker: workers) es.submit(worker);

// shutdown and wait
  • example: Transaction promo worker
public class TxPromoWorker extends TxWorker {
   public TxPromoWorker(BankAccount account, char txType, int amt) { super(...) }
   public void run() {
      if (txType == 'w') account.withdrawl(amt);
      else if (txType == 'd') {
         account.deposit(amt);
         if(account.getBalance() > 500) { // if balance is over $500
            int bonus = (int)((account.getBalance() - 500) *.01); // add 10% extra over that $500
            account.deposit(bonus)
         }
      }
   }
}
  • this class is flawed
    • does not protect against a thread entering and obtaining a lock prior to the code in else if() finishing
    • could lead to undesirable effects, such as the bonus amount actually being a negative number
  • Example: safe transaction promo worker run() method
   public void run() {
      if (txType == 'w') account.withdrawl(amt);
      else if (txType == 'd') {
         synchronized(account) { // will now block for the entire statement!
            account.deposit(amt);
            if(account.getBalance() > 500) { 
               int bonus = (int)((account.getBalance() - 500) *.01); 
               account.deposit(bonus)
            }
          }
      }
   }
  • since locks are associated with a thread, a thread that has a lock can continue to access methods within that locked object!

Concurrency-related Types

  • There are concurrency safe collections / blocking collections
  • most collections (List / Map) are not thread safe
    • if accessing with multiple threads, will eventually corrupt
  • can create a thread safe wrapper using Collection static methods
    • synchronizedList()
    • synchronizedMap()
  • these wrappers are a thread safe proxy
    • the actual work occurs in the OG object
  • blocking collections address the producer / consumer model
    • one or more threads produce content while one or more threads consume content
    • consumer thread must wait for production work to be available
  • java provides blocking queues
    • if a consumer reads an empty queue, will block until content is available
    • LinkedBlockingQueue, PriorityBlockingQueue, etc.
  • java.util.concurrent provides many types for managing concurrency
    • contains most of what we've talked about
    • contains Semaphores which coordinate access to multiple resources
  • java.util.concurrent.atomic provides atomic operations
    • can set(), get(), getAndAdd(), compareAndSet()

Capturing App activity with Java Log System

Log System Management

  • level of detail varies
  • java has a built-in solution java.util.logging
  • log system is centrally managed
    • only one app-wide LogManager class instance
    • manages log system configuration and object that do actual logging
    • accessed via LogManager.getLogManager()

Making log calls

  • Logger class provides logging methods
  • access Logger instances with LogManager
    • use .getLogger()
      • each instance is named and requires known the name
    • a global logger instance is available via the Logger class' static field GLOBAL_LOGGER_NAME
  • example: making log calls
public class Main {
  // create a logger than can be accessed anywhere in class
  static Logger logger = LogManager.getLogManager().getLogger(Logger.GLOBAL_LOGGER_NAME);
  public static void main(String[] args) {
    logger.log(Level.INFO, "My first log message");
  }
}

Log levels

  • Levels control logging detail
    • each log entry is associated with a level
      • included with each log call
    • each logger has a capture level with .setLevel()
      • ignores entries below capture level
  • each level has a numeric value
    • 7 basic levels, 2 special levels
    • can define custom levels, but should be avoided
Level Numeric Value Description
SEVERE 1000 serious failure
WARNING 900 potential problem
INFO 800 general info
CONFIG 700 config info
FINE 500 general dev info
FINER 400 detailed dev info
FINEST 300 specialized dev info
ALL Integer.MIN_VALUE logger capture everything
  • log level checking is very inexpensive

Types of Log Methods

  • several logging methods
    • simple, level convenience, precise, precise convenience, paramaterized
  • example: simple log method
logger.log(Level.SEVERE, "Uh Oh!");

// logs
date time com.ps.training.Main main // calling class name and method is inferred
SEVERE: Uh Oh!!
  • standar log methods infer the calling info for you
    • sometimes it gets it wrong
  • can use precise log methods to avoid regular log getting it wrong
    • logp
    • you pass in calling class and method name
  • Example: pass information to logger
logger.logp(Level.SEVERE, "class.name", "myMethod", "message to pass");
date time class.name myMethod 
SEVERE: message to pass
  • precise has convenience methods
    • simplify logging common method actions
    • logs a predefined message
    • these are always logged at Level.FINER | Method | Message | |--------|-----------| | entering | ENTRY | | exiting | RETURN |
  • by default, loggers are not set to include FINER level messages

Parameterised Message Methods

  • Some messages support message parameters
    • log and logp
      • parameter substation indicators explicitly appear within message
      • use positional substitution
      • zero-based index within curly braces {N}
    • entering and exiting
      • values appear after default message and are space separated
    • values are always passed as Object class
      • can accept individual or Object array
  • Example: parameterized variables logger.log(Level.INFO, "{0} is {1} days from {2}", new Object[]{"Sunday", "2", "Tuesday"})

Creating / Adding log components

  • log system is divided into components
    • each component handles a specific task
    • easy to set up common behaviors and provides flexibility
  • consists of 3 core components
    • Logger: accepts app calls
    • Handler: publishes logging info, a logger can have multiple
    • Formatter: Formats log info for publication, 1 per Handler
  • different log levels can be added to each Handler
    • allows logging to different areas to be fine tuned
    • Example: one handler sending logs to a file can have FINE whereas another handler sending logs to the terminal can have SEVERE
  • To create a Logger use Logger.getLogger static method
    • pass in a string to name it
    • once created, can be accessed in LogManager
  • add a Handler with Logger.addHandler
    • java provides built-in Handlers
  • add a Formatter with Handler.setFormatter
    • java provides built-in Formatters
  • Example: Creating / Adding log components
public class Main {
   static Logger logger = Logger.getLogger("com.pluralsight"); //create logger
   public static void main (String[] args) {
      Handler h = new ConsoleHandler(); // create handler
      Formatter f = new SimpleFormatter(); // create formatter
      h.setFormatter(f); 
      logger.addHandler(h);
      logger.setLevel(Level.INFO);
      logger.log(Level.INFO, "We're Logging!");
   }
}

Built-in Handlers

  • java provides several built-in handlers
    • inherit directly or indirectly from Handler
  • Commonly used built-in Handlers
    • ConsoleHandler: writes to System.err (console or output window)
    • StreamHandler: writes to specified OutputStream
    • SocketHandler: writes to a network socket
    • FileHandler: writes to 1 or more files

Built-in Formatters

  • java provides two built-in formatters
    • both inherit directly from Formatter
  • XMLFormatter: formats contents as XML, root element is log, each entry is named record
  • SimpleFormatter: format as simple text, formatting is customizable (standard formatting notation)
  • Example: SimpleFormatter Formatting
String.Format(format, date, source, logger, level, message, thrown);
// %1$tb %1$td %1$tY %1$tl:%1$tM:%1$tS %1$Tp %2$s %n %4$s: %5$s%6$s%n
// date is parsed into multiple parts
  • You can set format string with a system property
    • java.util.logging.SimpleFormatter.format
    • pass value with -D option
  • Example: setting SimpleString formatter
// in terminal
java -Djava.util.logging.SimpleFormatter.format=%5$s,%2$s,%4$s%n
// prints out message, class & method, log level
// %n denotes a new line break

Log config file

  • config info can be set in a file
    • follows standard properties file format
    • can replace or be used with code-based config
  • java.util.logging.config.file sets file name
    • pass value with -D
  • specific config values depend on classes
    • most code-based options available
  • Handlers and Formatters require a fully qualified class name followed by .someValueName
  • Loggers are named by passing the name to getLogger followed by .someValueName

Log system naming and hiearchy

  • naming implies parent-child relationship
    • LogManager links loggers in a hierarchy based on loggers name
  • Logger naming
    • should follow hiearchical naming
    • corresponds to type hierarchy
      • each . separates a level
    • generally tied to a class' full name
  • log entry information is bubbled up to parents
  • Example: log hierarchy
com.ps -> com.ps.accounting / com.ps.training -> com.ps.training.Student / com.ps.training.Main
// both Student and Main will pass log entries up to com.ps.training, which then passes it up to com.ps
  • done incorrectly could have duplicated logs
  • can instead harness this system
    • focus on capturing important information
      • option to obtain details if needed
    • manage log setup on parents
    • manage log calls at children
  • loggers do not require a level setting (can be null)
    • this allows children to inherit parent levels
    • primarily set levels on parents
      • make them restrictive, dont get excessive detail
    • when more detail is desired, can set log level of children to overwrite parent level
  • loggers do not require handlers
    • logger does not log if there is no handler, but does pass info up to parent logger
    • add handlers to parent loggers best practice
  • example: logger levels parent / child
// class
package com.ps.training;
public class MAin {
   static Logger pkgLogger = Logger.getLogger("com.ps.training"); // parent logger
   static Logger logger = Logger.getLogger("com.ps.training.Main"); // child logger
   public static Main {
      logger.entering("com.ps.training", "Main"); // FINER level logged to com.ps.training.Main
      logger.log(Level.INFO, "We're Logging!"); // logged to com.ps.training and com.ps.training.Main
      logger.exiting("com.ps.training", "Main"); // FINER level Logged to com.ps.training.Main
   }
}

// logger creds file
com.ps.training.hanlders=java.util.logging.ConsoleHandler
com.ps.training.level=INFO // parent logs INFO level
java.util.logging.FileHandler.level=ALL
java.util.logging.FileHandler.pattern=./main_%g.log
com.ps.training.Main.handlers=java.util.logging.FileHandler
com.ps.training.Main.level=ALL // child logs ALL levels

Controlling App Excecution and Environment

Command-line Arguments

  • Can pass info to app on command line
    • easiest way to pass startup info
      • target of app processing (input / output files, URLs, etc.
    • behavior options
  • arguments are passed as a String array
    • recieved by app's main function
    • each argument is a separate element
      • separated by OS's whitespace
      • honor OS's value quoting can have a file name " .txt"
  • IDE's allow for ease of testing
    • can set command line arguments
    • will automatically pass when run in IDE
    • different for each IDE
  • example: filenames with spaces
public class Main {
  public static void main(String[] args) {
    // escape if there are no arguments
    if(args.length === 0) {
      showUsage(); // print an error to the console
      return;
    }
    
    String filename = args[0];
    // check that the file even exists
    if(!Files.exists(Paths.get(filename))) {
      System.out.println("\n File not found: " + filename);
      return; // print error and escape
    }
    showFileLines(filename); // read the file back out to the console
  }
  
  private static void showFileLines(String filename) {
    System.out.println();
    
    try(BufferedReader reader = Files.newBufferedReader(Paths.get(filename))) {
      String line = null;
      while((line = reader.readLin()) !=null) System.out.println(line);
    } catch(Exception ex) {
      System.out.println(ex.getClass().getSimpleName() + " - " + ex.getMessage());
    }
  }
}
  • if file name has multiple whitespaces, use quotes like "multiple part name.txt"

managing persistable key / value pairs

  • Will often need persistent information
    • user preferences, app configuration, app state
  • need an easy way to manage this information
    • set / retrieve values, store / load between app executions, provide default value when not set
  • use the java.util.Properties class
    • inherits from HashTable class but keys and values must be Strings
    • setProperty() method sets current value for key / creates or updates key as needed
    • getProperty() returns current value for key
      • returns null if not found / no default
      • optional default

Store and load property values

  • Properties can be persisted
    • can be written to and read from a stream
    • optionally include comments
    • supports 2 formats: simple text and XML
  • Properties persisted as simple text
    • use store and load methods (OutputStream / InputStream / Reader / Writer)
    • normally name with .properties suffix
    • one key / value pair written per line
      • key / value separated by = or :
      • surrounding whitespace by = or : is ignored
      • whitespace acts as key / value separator if no = or :
      • can escape whitespace with \
    • start a line with # or ! for comments
    • blank lines are ignored

Properties persisted as XML

  • use storeToXML and loadFromXML methods
    • supports InputStream / OutputStream
  • normally name file with .xml suffix
  • one key / value pair per XML element
    • stored as element named entry
      • key stored as key attribute
      • value stored as element value
    • use comment element for comments

Providing default properties

  • you can provide default values when Properties is initially created
    • simplify configuration
    • initial values for user preferences
    • dont have to provide defaults for each getProperty() call
  • pass a default Properties to constructor of the Properties you create
    • defaults will be searched if key is not found in current Properties
    • default properties take precedence over default value passed to getProperty
  • examples: default properties
Properties defaults = new Properties();
defaults.setProperty("position", "1");
Properties. props = new Properties(defaults); // pass defaults in
String nextPos = props.getProperty("position"); // returns 1

int iPos = Integer.parseInt(nextPos);
// do something with iPos
props.setProperty("position", Integer.toString(++iPos)); // increments position
// do some other work
nextPos = props.getProperty("position"); // returns 2

including default properties in package

  • default property file can be part of a package
    • allows you to create .properties file at development time
    • the build process will include the file in package
  • you can then load the file from the package
    • the Java resource system provides getResourceAsStream()
    • this is accessed through any class in a package
      • ClassName.class or this.getClass()

Default class loading

  • most applications have dependencies from outside
  • JDK packages are located automatically, but need help locating others (3rd party dependencies, etc)
  • locating packages
    • dev time is IDE specific
    • run time, java provides number of options
  • java searches current directory
    • classes must be in .class files (not .jar)
    • must be under package directories

Specifying Class Path

  • can provide a list of paths to search
    • searched in the order they appear
    • current directory is used only if given in list
  • two options for specifying class path
    • environment variable
    • java command option
  • environmental variable CLASSPATH is used
    • will become the default path
    • used by all programs that dont provide specific path
    • use with caution
      • if one program changes it, all others will be affected

class path structure

  • use -cp in the console for class path
  • paths are provided as a delimited list
    • windows: separate with ;
    • mac: separate with :
    • searched in order they appear
  • to reference classes in .class files, use a path to folder containing package root
  • to reference classes in .jar files, use path to jar file location including jar file name
  • example: class path structure
psdir -> com -> pluralsight -> training -> Student.class / Main.class
libdir -> com -> jwhh -> support -> Other.class
mydir (coming from here)
...
// write into the console
java -cp /psdir:/libdir com.pluralsight.training.Main

// if a jar file
psdir -> training.jar -> [com -> pluralsight -> training -> Student.class / Main.class]

// write to console
java -cp /psdir/training.jar:/libdir com.pluralsight.training.Main

class loading with -jar option

  • the -jar command line option locks down class loading
    • completely controlled by the .jar file
    • no other loading source is used

Execution environment information

  • apps often need environment information
    • user / system info, java config, app specific
  • java provides two common solutions
    • system properties
    • environment variables
  • Java provides info about environment via System.getProperty()
    • includes user, java installation, OS config info
  • each can be accessed via a string name
  • Mac OS supports environment variables
    • provide config info
    • many are already set by the OS
    • can provide app-specific variables
  • apps can access these by System.getenv()
    • returns Map<String, String>
    • access one with System.getenv(name)
  • can set environment variables within directory of app
    • set VAR_NAME=var name

Working with Collections

Managing groups of data

  • Apps often need to manage data in commonly typed groups
    • most basic solution is to use arrays
  • Arrays are limiting
    • statically sized
      • size is determined upon creation
      • must create a new array if need more space
    • Requires explicit position management
      • need to specifically assign each item to a position, yuck
    • just a bunch of known values :(
  • collections provide more powerful options

Role of Collections

  • collections hold and organize values
    • iterable
    • can provide type safety
    • tend to dynamically size
  • a wide variety of collections are available
    • may be a simple list of values
    • can provide optimization or sophistication
      • ordering
      • prevent duplicates
      • manage data as a name / value pair (map / dictionary)
  • example: simple collection of objects
ArrayList list = new ArrayList(); // accepts objects
list.add("Foo"); 
list.add("Bar"); // grows with each addition

System.out.println(Elements: " + list.size()); // can access the size of the list

// to access things in list, must state type (strings were converted to their Object form when placed in)
for(Object o:list) System.out.println(o); // println will call .toString() method on object

String s = (String)list.get(0); // must type cast to convert back to String from Object

Collections and type saftey

  • collections hold Objects by default
    • you must convert return values to desired type
    • does not restrict types of value added
  • collections can be type restricted
    • concept known as generics
    • type is specified during creation of collection
    • limits what types the collection can work on
  • collection type restriction is restrictive
    • return values from collection will be the stated type
    • compiler will check to see that the values added are compatible with the type given
  • example: stringly typed collection
ArrayList<String> list = new ArrayList<>(); // can type the collection, do not need to place type again in the instance creation
list.add("Foo"); 
list.add("Bar");

System.out.println(Elements: " + list.size()); // can access the size of the list

// since returned as strings, do not need to type cast
for(String o:list) System.out.println(o);

String s = list.get(0);
SomeMadeUpClass c = new SomeMadeUpClass();
list.add(c); // will not be allowed since type is not String

Collection Interface

  • Each collection type has its own features
    • many are shared across types
  • The collection interface provides a common methods
    • implemented by most collection types (except the Map collection)
    • extends the iterable interface so can use for loops
  • common methods:
Method description
size returns # elements
clear removes all elements
isEmpty returns true if no elements
add add a single element
addAll add all memebers of another collection
  • common equality-based methods:
Method description
contains returns true if contains element
containsAll returns true if contains all members of another collection
remove remove element
removeAll remove all elements contained in another collection
retainAll remove all elements NOT contained in another collection
  • these tests all use the .equals() method
  • example: removing a memeber
public class MyClass {
  String label, value;
  
  public MyClass(String label, String value) {
    // assign label and value to member fields
  }
  
  public boolean equals(Object o) {
    MyClass other = (MyClass o) // typecast
    return value.equalsIgnoreCase(other.value); // only comparing the value, not label
  }
}
...

ArrayList<MyClass> list = new ArrayList<>();

MyClass v1 = new MyClass("v1", "abc");
MyClass v2 = new MyClass("v2", "abc");
MyClass v3 = new MyClass("v3", "abc");

list.add(v1).add(v2).add(v3);

// removes the first match it finds
list.remove(v3) // will remove v1 because .equals() only compares values

Java 8 Collection Features

  • Java 8 introduced lambda expressions
    • code is passed as arguments
  • collections methods leverage lambdas
    • forEach and removeIf
      • removeIf accepts a predicate which is a code block that results to a boolean, if true will remove
  • list.forEach(x -> System.out.println(m.getLabel()));
  • list.removeIf(x -> x.getValue().equals("abc");

Converting between Collections and Arrays

  • sometimes APIs require an Array when we are working with collections
    • often legacy code / libraries
  • Collection interface can return an array
    • .toArray() returns an Object array
    • .toArray(T[] array) returns array of type T
  • array content can be retrieved as collection
    • .asList(): Collection<MyClass> list = Arrays.asList(myArray);
  • example: retrieving an array
ArrayList<MyClass> list = new ArrayList<>();
// add items

Object[] objArray = list.toArray();

MyClass[] a1 = list.toArray(new MyClass[0]); // pass in an empty array to indicate the type of array returned
MyClass[] a2 = new MyClass[3]; // create an array with space
MyClass[] a3 = list.toArrray(a2); // if there is an array with space for the members, the returned array will be the one given
if(a2 == a3) // a2 and a3 reference the same array

Collection Types

  • there are a wide variety of collections
    • each have specific behaviors
  • collections interfaces provide contracts for collection behavior
  • collection classes provide collection implementation and implement 1 or more collection interfaces
  • common collection classes:
Interface description
Collection basic collection operations
List Collection that maintains a particular order
Queue Collection with concept of order and specific "next" element
Set Collection that contains no duplicates
SortedSet A Set whose memebers are sorted
  • collection classes continued:
Interface description
ArrayList A List backed by a resizeable array, effecient at random access but inefficient at random inserts
LinkedList A List and Queue backed by a doubly-linked list, effecient random insert but inefficient random access
HashSet A Set implemented as a hash table, Efficient general purpose usage at any size
TreeSet A SortedSet implemented as a balanced binary tree. Members accesible in order but less effecient to modify and search than a HashSet

Sorting

  • some collections rely on sorting
    • two ways to specify sort behavior
  • Comparable interface
    • implemented by the type to be sorted
    • type specifies own sort behavior
      • should be consistent with equals
  • Comparator interface
    • implemented by type that preforms sort
    • specifies the sort behavior of another type
    • useful for cases where you want to sort a type that does not implement the comparable interface
    • useful for when you want to provide an alternate sort than that provided by the type to be sorted
  • example: implementing Comparable
public class MyClass implements Comparable<MyClass> {
  String label, value;
  public String toString() { return label + " | " + value; }
  
  public boolean equals(Object o) {
    MyClass other = (MyClass) o;
    return value.equalsIgnoreCase(other.value);
  }
  
  public int compareTo(MyClass other) { // returns - , 0 , or +
    return value.equalsIgnoreCase(other.value);
  }
}

TreeSet<MyClass> tree = new TreeSet<>();
tree.add(new MyClass("2222", "ghi"));
tree.add(new MyClass("3333", "abc"));
tree.add(new MyClass("1111", "def"));
tree.forEach(m -> system.out.println(m)); 
// 3333 | abc
// 1111 | def
// 2222 | ghi
  • example: implementing Comparator
public class MyComparator implements Comparator<MyClass> {
  public int compare(MyClass x, MyClass y) { // returns - , 0 , or +
    return x.getLabel().compareToIgnoreCase(y.getLabel());
  }
}

TreeSet<MyClass> tree = new TreeSet<>(new MyComparator); // pass in comparator
tree.add(new MyClass("2222", "ghi"));
tree.add(new MyClass("3333", "abc"));
tree.add(new MyClass("1111", "def"));
tree.forEach(m -> system.out.println(m)); // now sorted by label!
// 1111 | def
// 2222 | ghi
// 3333 | abc

Map Collections

  • Maps store key / value pairs
    • each key is unique but values can be duplicates (like JS Set())
    • values can be null
  • common map types
Interface description
Map basic map operations
SortedMap map with keys sorted
Class description
HashMap Efficient general purpose Map implementation
TreeMap SortedMap implemented as a self-balancing tree
Supports Comparable and Comparator sorting
  • common map methods
Method description
put Add key and value
putIfAbsent Add key and value if key not contained or value null
get return value for key, if key not found return null
getOrDefault return value for key, if key not found return provided default
values return a Collection of the contained values
keySet return a Set of the contained keys
forEach Perform action for each entry
replaceAll Perfor action for each entry replacing each key's value with action result

Sorted Map Collections

Method description
firstKey Return first key
lastKey Return last key
headMap Return a map for all keys that are less than the specified key ( does not include given key)
tailMap Return a map for all keys that are greater than or equal to the specified key
subMap Return a map for all keys that are greater than or equal to the start key, but less than the ending key
  • example: Using SortedMap
SortedMap<STring, STring> map = new TreeMap<>();
tree.add(new MyClass("2222", "ghi"));
tree.add(new MyClass("3333", "abc"));
tree.add(new MyClass("1111", "def"));
tree.add(new MyClass("4444", "mno"));
tree.add(new MyClass("6666", "xyz"));
tree.add(new MyClass("5555", "pqr"));

SortedMap<String, String> hMap = map.headMap("3333"); // returns values less than the passed key
// 1111 | def
// 2222 | ghi

SortedMap<String, String> tMap = map.tailMap("3333"); // returns values greater than and including the passed key
// 3333 | abc
// 4444 | mno
// 5555 | pqr
// 6666 | xyz

String Formatting and Regular Expressions

More Powerful Solutions to Creating String Representations

  • Concatenating strings is often detail focused & awkward
    • StringBuilder is somewhat effecient, but still has same issues
  • StringJoiner - simplifies joining a sequence of values
  • String Formatting - can specify desired appearance without dealing with creation details

String Joiner

  • StringJoiner(separator, starter, ender)
  • purpose is to simplify composing a string comprised of a sequence of values
  • Steps
    • construct StringJoiner
    • specify string to separate values
    • optionally specify start / end strings
    • add values
    • retrieve resulting string
  • example: StringJoiner with Separator
StringJoiner sj = new StringJoiner(", "); // how the strings will be joined
sj.add("alpha");
sj.add("theta");
sj.add("gamma");
String theResult = sj.toString(); // priints: apha, theta, gamma

// can chain methods as well
StringJoiner sj = new StringJoiner(", "); 
sj.add("alpha").add("theta").add("gamma");
String theResult = sj.toString(); // priints: apha, theta, gamma
  • example: StringJoiner with Start and End values
StringJoiner sj = new StringJoiner(", ", "{", "}"); // denote starting and closing values 
sj.add("alpha").add("theta").add("gamma");
String theResult = sj.toString(); // priints: {apha, theta, gamma}
  • example: more complex separator
StringJoiner sj = new StringJoiner("], [", "[", "]");
sj.add("alpha").add("theta").add("gamma");
String theResult = sj.toString(); // priints: [apha], [theta], [gamma]}

String Joiner Edge Case Handler

  • when only one value is added
    • if constructed with only a separator, will return the value only
    • if constructed with start / end strings, will return the string wrapped with start / end values
  • when no values added
    • if constructed with only a separator, will get back an empty string
    • if constructed with start / end strings, will return a string with start / end values
    StringJoiner sj = new StringJoiner(", ", "{", "}");
    String theResult = sj.toString(); // returns "{}"
    
  • can customize a special string for empty case
    • specified with setEmptyValue()
    • triggered only when .add() method is not called
  • example: customizing empty handling:
StringJoiner sj = new StringJoiner(", ", "{", "}");
sj.setEmptyValue("EMPTY");
String theResult = sj.toString(); // returns "EMPTY"
  • even if you have start and end values, will not return with those
  • if an .add() is called with an empty string, the empty case is not triggered

Constructing Strings with Format specifiers

  • format specifiers are concerned with describing the desired result, not with how
    • can control many aspects of appearance
    • positioning, decimal places, representation
  • Some methods support format specifiers
    • String.format
    • System.out.printf
    • Formatter.format - allows other types to be formatted
  • all format specifiers start with % sign
    • can have at minimum a conversion notation like %d, which denotes conversion
      String s = String.format("My nephews are %d, %d, %d, and %d years old", variable1, variable2, variable3, variable4);
      
    • can become more complex like %.1f, which denotes precision and conversion
      String s = String.format("The average age between each is %.1f years", avgDiff); // outputs 3.7 instead of a long float
      
  • parts: %[argument index][flags][width][precisoin]conversion
    • precision: decimal places to display
    • width: minimum characters to display (space-padded, right-justified by default)

Common format conversions

format meaning type example result
d decimal integral 32 32
o octal integral 32 40
x X hex integral 32
f decimal float 123.0 123.0000000
e E scientific notation float 123.0
s string general "Hello" Hello
Implements Formattable return value of format method
Other Classes return value of toString method

Format Flags

  • # denotes radix, so integral values will be returned in a way that shows what kind of value it is
    • String.format(%#o, 32) returns 040 instead of just 40 without the #
    • String.format(%#o, 32) returns 0x20 instead of just 20
  • 0 denotes zero-padding when width is defined
  • - denotes left justify when width is defined
// with no flags
s1 = String.format("W:%d X:%d, 5, 235); // W:5 X:235
s2 = String.format("W:%d X:%d, 481, 12); // W:481 X:12

// with spaces
s1 = String.format("W:%4d X:%4d, 5, 235);  // W:   5 X: 235
s2 = String.format("W:%4d X:%4d, 481, 12); // W: 481 X:  12

// with zero spacing
s1 = String.format("W:%04d X:%04d, 5, 235);  // W:0005 X:0235
s2 = String.format("W:%04d X:%04d, 481, 12); // W:0481 X:0012

//with left align
s1 = String.format("W:%-4d X:%-4d, 5, 235);  // W:5    X:235 
s2 = String.format("W:%-4d X:%-4d, 481, 12); // W:481  X:12
  • , denotes group value in decimals and floats
    • turns 1234567 into 1,234,567
    • can also use it with localization values
  • space denotes leave a space for positive numbers so they will line up with negative numbers
  • + always show sign for pos / neg numbers
  • ( denotes enclosing negative values in parenthesis
  • can combine flags that work on the same numbers
    • % (d will leave a space for the positive numbers, but also wrap negatives in parens

String Matching with Regex

  • Regular expressions are powerful with pattern matching systems
    • \W+ match 1+ word characters (letter, digit, underscore)
    • /b match word breaks
  • Java supports regular expressions
    • Methods on the String class
    • Dedicated classes

String Class Support for Regular Expressions

  • replaceFirst, replaceAll methods
    • Returns a new updated string
    • pattern identifies which parts to change
  • split method - similar to JS
  • match() method
    • identifies if string matches the pattern
  • example: replaceAll()
String s = "apple, apple and orange please";

String s2 = s.replaceAll("ple", "ricot"); // apricot, apricot, orange ricotase
String s3 = s.replaceAll("ple\\b", "ricot"); // regular expression denotes only replace at end of word break
// must be double escaped 

Dedicated Regular Expression Classes

  • Regex considerations
    • Compilation is processing intensive
    • String methods repeat compoilation on every use
  • Pattern class compiles a regular expression
    • Factory for Matcher class instances
  • Matcher class applies compiled expression to a string
  • example: Pattern and Matcher classes
String value = "apple, apple and orange please";
Pattern pattern = Pattern.compile("\\W+"); 
Matcher matcher = pattern.matcher(value); // matcher is a method of the compiled Pattern class

// while there are things for matcher to find
while(matcher.find()) System.out.println(matcher.group());
// apple
// apple
// and
// orange
// please
  • regex101.com for regex testing

Input and Output with Streams and Files

Streams

  • an ordered sequence of data
  • provides a common I/O model (input / output)
  • abstracts details of source destination
  • are unidirectional, can only read from or write to per stream
  • two categories
    • byte streams
      • interact as binary data
    • text streams
      • interact as unicode characters
    • interaction is the same for both types

Read and Writing from streams

  • Read
    • base class is InputStream for reading from binary data (8 bits)
    • base class is Reader for reading from characters (16 bits)
    • both have int read() methods (return 32 bits)
    • both have a way of reading from an array
      • int read(byte[] buff)
      • int read(char[] buff)
  • example reading binary data:
InputStream input = // create input stream
int intVal;
while((intVal = input.read()) >= 0) { // returns a -1 when finished reading file
  byte byteVal = (byte) intVal; // must cast the integer value into a byte
  // do something with byteVal
}
  • example reading character data:
Reader reader = // create input stream
int intVal;
while((intVal = reader.read()) >= 0) { // returns a -1 when finished reading file
  char charVal = (char) intVal; // must cast the integer value into a char
  // do something with charVal
}
  • example binary array reading:
InputStream input = // create input stream
int length;
byte[] byteBuff = new byte[10];
while((length = input.read(byteBuff)) >= 0) {
  for(int i=0; i<length; i++) { // use the length value to see how much was actually read
    byte byteVal = byteBuff[i];
    // do something with byteVal
  }
}
  • example character array reading:
Reader reader = // create reader
int length;
char[] charBuff = new char[10];
while ((length = reader.read(charBuff)) >= 0) {
  for(int i=0; i<length; i++) {
    char charVal = charBuff[i];
    // do something wtih charVal
  }
}
  • write
    • binary from class OutputStream
      • void write(int b) can accept individual byte that a reader creates
      • void write(byte[] buff) also accepts an array
    • characters from class Writer
      • void write(int ch)
      • void write(char[] buff)
      • void write(String str)
  • example write binary:
OutputStream output = // create stream

byte byteVal = 100;
output.write(byteVal); // a widening type conversion, Java handles automatically

byte[] byteBuff = {0, 63, 127};
output.write(byteBuff);
  • example write character:
Writer write = // create writer

char charVal = 'a';
writer.write(charVal);

char[] charBuff = {'a', 'b', 'c'};
writer.write(charBuff);

String stringVal = "Hello World";
writer.write(stringVal);

Common stream classes

  • InputSteam and OutputStream are abstract classes that are often inherited from when dealing with bytes
  • InputStream:
    • ByteArrayInputStream
    • PipedInputStream
      • work well in a producer / consumer relationship with PipedOutputStream
      • keeps track of what's been read / written for you
    • FileInputStream
  • OutputStream
    • ByteArrayOutputStream
    • PipedOutputStream
    • FileOutputStream
  • Reader and Writer are abstract classes that are often inherited from when dealing with characters
  • Reader:
    • CharArrayReader
    • StringReader
    • PipedReader
    • InputStreamReader
      • can create a reader over an inputstream
      • allows for consumption of binary into character data
      • higher order functionality is achieved by layering as such
      • FilerReader inherits from
  • Writer:
    • CharArrayWriter
    • StringWriter
    • PipedWriter
    • OutputStreamWriter
      • can create a writer over an output stream
      • FileWriter inherits from

Stream Errors and Cleanup

  • Error Handling
    • Stream methods throw exceptions to indicate errors
  • Cleanup
    • Cannot rely on standard Java resource recovery
    • streams are backed by physical storage
      • often exist outside of Java runtime
      • Runtime may not reliably clean up
    • Reliable cleanup is provided through Closeable interface
      • method close
      • closing is not simple and it itself can fail
  • Automating cleanup with AutoCloseable interface
    • method close
    • base interface of Closeable interface
    • provides support for try-with-resources
  • try-with-resources
    • automates cleanup opf 1 or more resources
      • a resource is anything that implements AutoCloseable
    • syntax similar to traditional try statement
    • optionally includes catch blocks(s)
      • handle try body
      • handle close method call
    • try becomes a try-with-resources by stating the resource in parenthesis after try
      • try (Reader reader = Helper.openReader("file1.txt")) {...}
      • will allow the removal of a finally block that would otherwise be needed to handle the closing of a file
      • can declare multiple resources with a semicolon after each resource
        • try (Resource 1; Resource 2; Resource 3) {...}
    • if an exception is thrown within the close, catch will handle it
      • if multiple exceptions are thrown within the close, only the initial one is caught by the catch block
      • java still keeps track of the other exceptions, but they are known as suppressed exceptions
      • can be called with e.getSuppressed() from the caught exception, will return a collection of suppressed exceptions

chaining streams

  • often chained together
    • one stream instance levereages another
    • creates higher-level functionality
    • simplifies reusability
    • chain using constructor
  • InputStreamReader levereages chaining
    • utilizes the reader behavior over InputStream
    • Character behavior over binary
void doChain(InputStream in) throws IOException {
  int length;
  char[] charBuff = new char[128];
  try (InputStreamReader rdr = new InputStreamReader(in)) { // pass reference of InputStream into the InputStreamReader
    while((length = rdr.read(charBuff)) >= 0) {
      // do something with charBuff
    }
  }
}
  • closing a higher-level stream will close the containing stream as well
    • closing InputStreamReader will automatically close InputStream
  • can create your own "high-level" streams
    • most commonly chain similar streams
      • chain a reader over a reader, etc.
  • There are classes available to simplify customization
    • FilterReader, FilterWriter, FilterInputStream, FilterOutputStream
    • these are abstract
    • all the methods call to the contained stream methods
      • if you call close on a FilterReader class, it will automatically call close on whatever stream its wrapping
    • override only customized methods to do something different than the contained stream does!

File and Buffered Streams

  • Accessing files
    • often use streams for file-based I/O
    • there is a class for each stream type in java.io package
    • java.io classes are now deprecated
      • but still widely used in code
  • buffered streams
    • why? Direct file access can be inefficient
      • buffered streams can improve efficiency
      • buffers content in memory
      • performs reads / writes in large chunks
      • reduces underlying stream interaction
    • buffering is available for all 4 stream types
      • BufferedReader, BufferedWriter, BufferedInputStream, BufferedOutputStream
  • example buffered streams:
try(BufferedReader br = new BufferedReader(new FileReader("file1.txt")) {
  int intVal;
  while((intVal = br.read()) >= 0) { // while the buffered reader still has valid bytes to read
    char charVal = (char) intVal; // type casting
    // do something with charVal
  }
}
  • Buffered Steams and line breaks
    • line breaks vary accross platform
      • Unix: \n
      • Windows: \r\n
    • buffered streams add line break support
      • uses correct value for current platform
      • BufferedWriter: generate line breaks with newLine()
      • BufferedReader: line based read with readLine()
  • writing line breaks example:
// data.txt
String[] data = {
  "Line 1",
  "Line 2 2",
  "Line 3 3 3",
  "Line 4 4 4 4",
  "Line 5 5 5 5 5",
}

void writeData(String[] data) throws IOException {
  try (BifferedWriter bw = new BufferedWriter(new FileWriter("data.txt"))) {
    for(String d:data) {
      bw.write(d); // all strings would end up on single line if only used this without .newline()
      bw.newline(); // makes sure a new line is created no matter the system
    }
  }
}

Accessing Files with java.nio.file package

  • java.nio.file is the preferred package for files
    • will still come across the older types in code, but write with these newer ones
    • FileReader and FileWriter are depricated
    • general stream types are still in use like InputStream, OutputStream, Reader, Writer, BufferedReader, BufferedWriter
  • provide a number of benefits over java.io package
    • better exception handling
    • greater scalability
    • more file system feature support
    • simplifies common tasks
  • Path and Paths type
    • Path
      • used to lacate a file system item
      • can be file or directory (folder)
    • Paths
      • way to get a Path
      • Static Path factory methods
      • From string-based hierarchical path
      • from URI
    • can pass Paths a single string: Path p1 = Paths.get("\\documents\\data\\foo.text");
    • or multiple strings: Path p2 = Paths.get("\\documents", "data", "foo.txt");
      • represents the parts of the path
      • both ways above will reference the same file
  • Files type
    • once you have a Path you can interact with it using this type
    • contain static methods for interacting with files
      • create, copy, delete, etc.
    • can open file streams
      • newBufferedReader, newBufferedWriter, newInputStream, newOutputStream
    • read / write file contents
      • readAllLines
      • write
  • example reading lines with BufferedReader:
void readData() throws IOException {
  try (BufferedReader br = Files.newBufferedReader(Paths.get("data.txt"))) {
    String inValue;
    while ((inValue = br.readLine()) != null) {
      // do something
    }
  }
}
  • instead, can be much more efficient with Files methods
  • example readAllLines:
void readThemAll() throws IOException {
  List<String> lines = Files.readAllLines(Paths.get("data.txt"));
  
  for(String line: lines) // do something with each line;
}

Using Default File System and Zip file system

  • Files exist within a file system
    • there is a default file system and more specialized systems like zip
    • Path instances are tied to a file system
      • Path only works with default system
  • File system types
    • FileSystem
      • represents an individual file system
      • is a factory for Path instances!
    • FileSystems
      • static FileSystem factory methods
      • allow you to open or create a file system with method newFileSystem()
  • identify file systems with URIs (universal resource identifiers, like https)
    • specifics of URIs vary greatly
    • zip file system uses jar:file scheme: jar:file:/grace/data/bar.zip
  • file systems support custom properties
    • these properties vary for each file system type
    • Ex: whether to create Path if it does not exist in system or string encoding
  • example: create a zip file system to open a zip file and then copy a default file into zip system
String[] data = {
  "Line 1",
  "Line 2 2",
  "Line 3 3 3",
  "Line 4 4 4 4",
  "Line 5 5 5 5 5",
}

// call method with the zip file in the default file system (can use Paths)
try(FileSystem zipFs = openZip(Paths.get("myData.zip"))) { // wrap in try-with-resources to catch any errors
  copyToZip(zipFs);
  writeToFileInZip1(zipFs, data);
  writeToFileInZip2(zipFs, data);
} catch(Exception e) {
  // do something with exceptions
}

private static FileSystem openZip(Path zipPath) throws IOException, URISyntaxException {
  Map<String,String> providerProps = new HashMap<>(); // a way to store name : value pairs
  providerProps.put("create", "true"); // if it doesn't already exist, create it
  
  // when creating file systems need a URI
  // need to make sure the zipPath has the correct formatting for the URI, convert with .toUri()
  URI zipUri = new URI("jar:file", zipPath.toUri().getPath(), null);
  
  // can now pass in to file system
  FileSystem zipFs = FileSystems.newFileSystem(zipUri, providerProps);
  
  return zipFs;
}

private static void copyToZip(FileSystem zipFs) throws IOException { 
  // can use Paths in the default file system to get the file path
  Path sourcefile = Paths.get("file1.txt"); // shortcut version
  // Path sourceFile = FileSystems.getDefault().getPath("file1.txt"); same as above, longhand version
  Path destFile = zipFs.getPath("/file1copied.txt"); // you can name the desitation file whatever you want using the getPath method
  
  // third option dictates options to pass in, we will want to replace if something already exists
  Files.copy(sourceFile, destFile, StandardCopyOption.REPLACE_EXISTING);
}

private static void writeToFileInZip1(FileSystem zipFs, String[] data) throws IOException {
  // set up writer that points to a path within zip file system
  try(BufferedWriter writer = Files.newBufferedWriter(zipFs.getPath("/newFile1.txt"))) {
    for(String d:data) {
      writer.write(d);
      writer.newLine();
    }
  }
}

private static void writeToFileInZip2(FileSystem zipFs, Strigg[] data) throws IOException {
  // Files.write() does all the heafty lifting for us, along with closing the file for us when finished
  // create a new file location, pass in the data array that will be used, pass in the character set to use, and the option for create
  // Arrays.asList() turns an array into an iterable 
  Files.write(zipFs.getPath("/newFile2.txt"), Arrays.asList(data), Charset.defaultCharset(), StandarOpenOption.CREATE)
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment