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
- java calculates by default, dependent on contents of type
- we can customize serialization processes
writeObject
called to serialize an object, recievesObjectOutputStream
readObject
called to deserialized an object, receivesObjectInputStream
- 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)
- use
- annotations can be accessed with reflection
- use
getAnnotation
method of target
- use
- 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
- each type has one instance of
- 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
- from type reference by calling
- 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
- must use
- 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
- use class
- use Constructor
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
- you override
ExecutorService
abstracts thread management details and can interact with thread poolsCallable
interface represents a task to be run on a thread- can return results and throw exceptions (unlike
Runnable
interface)
- can return results and throw exceptions (unlike
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 singleLogger
Formatters
format log info for publication and eachHandler
has 1Formatter
- 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
- set with
- 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
- support the
- 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
- construct with a value separator, eg.
- 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)
- required:
String
class supports format specifiersFormatter
class writes formatted content to any class that implements theAppendable
interface- Regular Expressions are a powerful pattern matching syntax
String
class supports RegExreplaceFirst / All()
creates a new stringsplit()
splits the string into an arraymatch()
checks for matching values
- There are dedicated regex classes
Pattern
class compiles regular expressionsMatcher
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
- resources implement
Path
instance locates a file system item and includes the file system- default or zip file system
Paths
instance is a factory forPath
instances for default file systemFiles
type gives us methods for interacting with filesFileSystem
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
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;
- found in JDK bin folder (IDEs often provide plugin)
- during serialization, any added uninitialized member will be given a default value
- characters given
null
, integer given0
- fields that are removed will be removed in serialization
- characters given
custom serialization
- can be added to class with
writeObject
andreadObject
methods to type- these methods are called through reflection, thus should make them
private
- these methods are called through reflection, thus should make them
- 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
- use
- return type of
- implementing
readObject
method- return type of
void
and includesthrows
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
- return type of
- 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) { ... }
}
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 spellstoString
wrong astoSring
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?
- when a type implicitly extends the Object class and overwrites the
- 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)
- we will take this class and expand its thread requirements:
- 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.)
- use the interface keywork preceeded with
- 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 inClass
of annotation - returns a reference to an annotation interface
- returns null if does not have annotation of requested type
- use
- 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
- accepts
- 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: exists only in source file, compiler throws away so not even
- 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
- set with
@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
}
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
- each type has a
- the fundamental type is the
- Example: Class that describes
BankAccount
- will contain a
simpleName
BankAccount
- has fields
id
andbalance
- constructors
BankAccount
andBankAccount
- methods
getId
,getBalance
,deposit
,withdrawal
- will contain a
- 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
- the type has a member that can access the
- 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 methodisInterface()
that returnstrue
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
- provides static fields for bit comparisons
- 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, TypeMethod
- Name, Return type, Parameter typesConstructor
- 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 theObject
class like.equals()
,.toString()
, etc.- to exclude unwanted class methods use
.getDeclaringClass()
and compare it to theClass
model m.getDeclaringClass() != Object.class
check that the class is notObject
- can use
!=
since there is only oneClass
reference of a type
- can use
- to exclude unwanted class methods use
- to access an individual member by signature:
getField()
: pass namegetMethod()
: pass name plus parameter typesgetConstructor()
: pass parameter types
- members have access to modifiers
- use
getModifiers()
and then interpret withModifier
class
- use
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 theBankAccount
type,Object
reference cannotObject
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 onBankAccount
- using the method information returned, then
- 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
- constructors can be executed with
- 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
- executes worker classes against targets
- 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
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
- returned by
- 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;
isnon-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 entergetBalance()
, 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
- does not protect against a thread entering and obtaining a lock prior to the code in
- 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 methodssynchronizedList()
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()
- can
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()
- only one app-wide
Making log calls
Logger
class provides logging methods- access
Logger
instances withLogManager
- use
.getLogger()
- each instance is named and requires known the name
- a global logger instance is available via the
Logger
class' static fieldGLOBAL_LOGGER_NAME
- use
- 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 log entry is associated with a 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
andlogp
- parameter substation indicators explicitly appear within message
- use positional substitution
- zero-based index within curly braces
{N}
entering
andexiting
- 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 callsHandler
: publishes logging info, a logger can have multipleFormatter
: Formats log info for publication, 1 perHandler
- 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
useLogger.getLogger
static method- pass in a string to name it
- once created, can be accessed in
LogManager
- add a
Handler
withLogger.addHandler
- java provides built-in Handlers
- add a
Formatter
withHandler.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 OutputStreamSocketHandler
: writes to a network socketFileHandler
: writes to 1 or more files
Built-in Formatters
- java provides two built-in formatters
- both inherit directly from
Formatter
- both inherit directly from
XMLFormatter
: formats contents as XML, root element is log, each entry is named recordSimpleFormatter
: 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
- pass value with
- 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
- each
- 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
- focus on capturing important information
- 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
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
- easiest way to pass startup info
- 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"
- recieved by app's
- 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 neededgetProperty()
returns current value for key- returns null if not found / no default
- optional default
- inherits from
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
andXML
Properties
persisted assimple 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
\
- key / value separated by
- start a line with
#
or!
for comments - blank lines are ignored
Properties persisted as XML
- use
storeToXML
andloadFromXML
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
- stored as element named entry
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 theProperties
you create- defaults will be searched if key is not found in current
Properties
- default properties take precedence over default value passed to
getProperty
- defaults will be searched if key is not found in current
- 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
- allows you to create
- 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
orthis.getClass()
- the Java resource system provides
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
- classes must be in
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
- windows: separate with
- 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
- completely controlled by the
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
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 :(
- statically sized
- 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
- concept known as
- 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
andremoveIf
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
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
- construct
- 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
- specified with
- 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 conversionString 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 conversionString s = String.format("The average age between each is %.1f years", avgDiff); // outputs 3.7 instead of a long float
- can have at minimum a conversion notation like
- 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 isString.format(%#o, 32)
returns040
instead of just40
without the#
String.format(%#o, 32)
returns0x20
instead of just20
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
into1,234,567
- can also use it with localization values
- turns
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
- Factory for
Matcher
class applies compiled expression to a string- example:
Pattern
andMatcher
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
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
- byte streams
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)
- base class is
- 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 createsvoid write(byte[] buff)
also accepts an array
- characters from class
Writer
void write(int ch)
void write(char[] buff)
void write(String str)
- binary from class
- 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
andOutputStream
are abstract classes that are often inherited from when dealing with bytesInputStream
:ByteArrayInputStream
PipedInputStream
- work well in a producer / consumer relationship with
PipedOutputStream
- keeps track of what's been read / written for you
- work well in a producer / consumer relationship with
FileInputStream
OutputStream
ByteArrayOutputStream
PipedOutputStream
FileOutputStream
Reader
andWriter
are abstract classes that are often inherited from when dealing with charactersReader
: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
- method
- Automating cleanup with
AutoCloseable
interface- method
close
- base interface of
Closeable
interface - provides support for
try-with-resources
- method
try-with-resources
- automates cleanup opf 1 or more resources
- a resource is anything that implements
AutoCloseable
- a resource is anything that implements
- 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 trytry (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
- if multiple exceptions are thrown within the close, only the initial one is caught by the
- automates cleanup opf 1 or more resources
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
- utilizes the reader behavior over
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 closeInputStream
- closing
- can create your own "high-level" streams
- most commonly chain similar streams
- chain a reader over a reader, etc.
- most commonly chain similar streams
- 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
- if you call close on a
- 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
- why? Direct file access can be inefficient
- 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
- Unix:
- buffered streams add line break support
- uses correct value for current platform
BufferedWriter
: generate line breaks withnewLine()
BufferedReader
: line based read withreadLine()
- line breaks vary accross platform
- 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
andFileWriter
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
andPaths
typePath
- 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
- way to get a
- 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
- once you have a
- 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 likezip
Path
instances are tied to a file systemPath
only works withdefault
system
- there is a
- 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()
- static
- identify file systems with URIs (universal resource identifiers, like https)
- specifics of URIs vary greatly
zip
file system usesjar: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)
}