Relevant Reading Material
File and Input/Output (I/O)
I/O Streams
I/O Streams is the basic abstraction used through the Java I/O package. You can conceptually think of I/O Streams as a pipe that we connect to a source and destination that allows us to create a flow of data from the pipe’s source to the pipe’s destination.
An I/O Stream has
-
an input source (a file, another program, a physical device etc.)
-
an output destination (a file, another program, a physical device etc.)
-
the kind of data streamed (bytes, ints, objects)
A stream is a sequence of data, an I/O Stream allows us to fetch (iterate) over the sequence of data.
So we talk about InputStream
and OutputStream
. The JDK provides some useful variants of streams
-
InputStream
can be one of-
FileInputStream
-
AudioInputStream
-
SequenceInputStream
-
and we can build our own as well!
-
-
OutputStream
can be one of-
FileOutpustStream
-
PipedOutputStream
-
and we can build our own as well!
-
We will start with an example for reading bytes and iteratively show some of the JDK classes that we can use to read
-
files
-
buffer our reads
-
improve our code on how
-
resources are handled
-
ensuring we properly deal with our resources
-
Reading Bytes
This example copies a file. This is the Unix command cp
in Java.
public class Main {
public static void main(String[] args) {
FileInputStream inputFile = null; (1)
FileOutputStream outputFile = null;
System.out.println(System.getProperty("user.dir")); (2)
try {
inputFile = new FileInputStream("roster.csv"); (3)
outputFile = new FileOutputStream("roster.out.csv"); (4)
int byteRead;
while ((byteRead = inputFile.read()) != -1) { (5)
System.out.println("Read : " + byteRead);
outputFile.write(byteRead);
}
} catch (FileNotFoundException fnfe) {
System.out.println("*** OUPS! A file was not found : " + fnfe.getMessage());
fnfe.printStackTrace();
} catch (IOException ioe) {
System.out.println("Something went wrong! : " + ioe.getMessage());
ioe.printStackTrace();
} finally { (6)
if (inputFile != null) {
try {
inputFile.close();
} catch (IOException e) { (7)
System.out.println("Failed to close input stream in finally block");
e.printStackTrace();
}
}
if (outputFile != null) {
try {
outputFile.close();
} catch (IOException e) {
System.out.println("Failed to close input stream in finally block");
e.printStackTrace();
}
}
}
}
}
1 | We create variables to hold our input and output streams. We are using FileInputStream and FileOutputStream since we are going to connect
out streams to files. We are using null here because we would like to have access to these variables outside in the try block as well as the finally block later. |
2 | System is a static class (recall System.out ) and we can use it to get system properties, like the user’s current directory "user.dir" . |
3 | We need to now initialize our stream’s endpoints to the appropriate files, source. |
4 | We need to now initialize our stream’s endpoints to the appropriate files, destination. |
5 | The while loop uses read() to read in a byte . We store the read result in an int . read() returns -1 when it reaches the end of the stream, in this case the end of our file "roster.csv" . |
6 | finally is a Java keyword and denotes the block of code that Java will run after our try block completes either with an exception or without an exception. If our try block completes with no exception, then Java will run the code in the finally block after try completes. If our try block throws an exception, Java will first run any catch blocks that match and will then run the code in the finally block. |
7 | Since we need to perform close() on our streams inside finally we have to again deal with any possible checked exceptions that might arise. |
There is a significant amount of code here to deal with
-
exceptional cases
-
cleaning up after our code so that we do not leave any resources open due to our code’s behaviour
-
code repetition
We will improve upon this code soon, but first let’s see how we can use this pattern to read in different kinds of data.
Reading characters
A character can be one or more bytes
depending on the computer we use and the language we use. Let’s read in one character at a time.
public class MainFileIO {
public static void main(String[] args) {
FileReader inputFile = null; (1)
FileWriter outputFile = null; (2)
System.out.println(System.getProperty("user.dir"));
try {
inputFile = new FileReader("roster.csv");
outputFile = new FileWriter("roster.out.csv");
int character;
while ((character = inputFile.read()) != -1) { (3)
System.out.println("Read : " + character);
outputFile.write(character);
}
} catch (FileNotFoundException fnfe) {
System.out.println("*** OUPS! A file was not found : " + fnfe.getMessage());
fnfe.printStackTrace();
} catch (IOException ioe) {
System.out.println("Something went wrong! : " + ioe.getMessage());
ioe.printStackTrace();
} finally {
if (inputFile != null) {
try {
inputFile.close();
} catch (IOException e) {
System.out.println("Failed to close input stream in finally block");
e.printStackTrace();
}
}
if (outputFile != null) {
try {
outputFile.close();
} catch (IOException e) {
System.out.println("Failed to close input stream in finally block");
e.printStackTrace();
}
}
}
}
}
1 | FileReader is provided by the JDK. A convenient class to read character files. |
2 | FileWriter is provided by the JDK. A convenient class to write character files. |
3 | Reads a whole character (we still store it in an int ). |
Buffering your read/write operations
Reading our stream one piece of data at a time works fine, but we would like to read a bit more at a time. For example when reading character files we would like to read lines of text instead of characters. A typical solution to is to provide an intermediate step that buffers our reads/writes. Think of it as "scratch paper" that we use to temporarily to store a sequence of reads/writes that we can then group together and return in bulk.
public class MainBufferedIO {
public static void main(String[] args) {
BufferedReader inputFile = null; (1)
BufferedWriter outputFile = null; (2)
System.out.println(System.getProperty("user.dir"));
try {
inputFile = new BufferedReader(new FileReader("roster.csv")); (3)
outputFile = new BufferedWriter(new FileWriter("roster.out.csv")); (4)
String line;
while ((line = inputFile.readLine()) != null) { (5)
System.out.println("Read : " + line);
outputFile.write(line);
}
} catch (FileNotFoundException fnfe) {
System.out.println("*** OUPS! A file was not found : " + fnfe.getMessage());
fnfe.printStackTrace();
} catch (IOException ioe) {
System.out.println("Something went wrong! : " + ioe.getMessage());
ioe.printStackTrace();
} finally {
if (inputFile != null) {
try {
inputFile.close();
} catch (IOException e) {
System.out.println("Failed to close input stream in finally block");
e.printStackTrace();
}
}
if (outputFile != null) {
try {
outputFile.close();
} catch (IOException e) {
System.out.println("Failed to close input stream in finally block");
e.printStackTrace();
}
}
}
}
}
1 | We use a BufferedReader to buffer our reads. |
2 | We use a BufferedWriter to buffer our writes. |
3 | We wrap our file reader with a buffered reader. |
4 | We wrap our file writer with a buffered writer. |
5 | Now we can request a whole line, which will cause our buffered reader to request a series of reads until it finds a new line. All these reads together will form the String that holds the line from our file. |
Lets improve on our code a little
Java (since version 7) provides a version of try
that understands about resources (files, streams etc.). try
-with-resources allows us to create
resources and makes sure that these resources are properly closed when our try
block exits (normally or abnormally).
Here is the syntax for try
-with-resources
try (<INITIALIZE-RESOURCES>) {
// code that uses initialized resources
} // closes and cleanup performed by try
Let’s re-implement our last example but this time use try
-with-resources
public class MainTryWithResources {
public static void main(String[] args) {
System.out.println(System.getProperty("user.dir"));
try (BufferedReader inputFile =new BufferedReader(new FileReader("roster.csv"));
BufferedWriter outputFile = new BufferedWriter(new FileWriter("roster.out.csv"));) {
String line;
while ((line = inputFile.readLine()) != null) {
System.out.println("Read : " + line);
outputFile.write(line);
}
} catch (IOException ioe) {
System.out.println("Something went wrong! : " + ioe.getMessage());
ioe.printStackTrace();
}
}
}
-
No
null
initializations -
No
finally
block with repetitive code and moretry-catch
blocks
Which version of our code do you prefer?
Regular Expressions
A regular expression is a sequence of characters used to describe a pattern. Typically we use a regular expression to capture a pattern of characters that we would like to search for in a document or another string.
For example the regular expression "class"
can be used inside a document to search for the word "class".
Regular expression (RE) can also use operators in order to allow users to express rich patterns
-
*
is a postfix operator, means 0 or more of the preceding RE, e.g.,"a*"
matches 0 or more "a"s in sequence. -
|
is an infix operator, referred to as union, matches if either the left or the right RE match, e.g.,"A|B"
matches an "A" or a "B" -
.
matches any single character, e.g,"a.b"
matches any string that contains 3 characters the first is an "a" and the third is a "b" with the second character being any character (including an "a" or "b") -
[ ]
matches a single character found within the square brackets, e.g.,"[abcd]"
matches a single character that is either "a" or "b" or "c" or "d". -
[^ ]
negation, match any character that is not in the set, e.g.,"[^abcd]"
matches a single character that is not "a" or "b" or "c" or "d".
Languages and libraries provide even more operators, consult your languages/library’s documentation.
Java provides Pattern
class that allows you to create your REs, while,
Matcher
is the class that runs the RE over an input and tells us if it matched or not. Matcher
can also provide the elements of the input that matched and over valuable information. Let’s try an example.
public class MainRE {
public static void main(String[] args) {
Pattern re1 = Pattern.compile("(a|b)b*");
Matcher matcher1 = re1.matcher("bbbbbbbb");
System.out.println(re1.pattern() + " on \"bbbbbbbb\" matched? " + matcher1.matches());
Matcher matcher2 = re1.matcher("abbbbbbb");
System.out.println(re1.pattern() + " on \"abbbbbbb\" matched? " + matcher2.matches());
System.out.println(re1.pattern() + " on \"a\" matched? " + re1.matcher("a").matches());
System.out.println(re1.pattern() + " on \"aaaaaaa\" matched? " + re1.matcher("aaaaaaa").matches());
}
}
We can also group parts of are RE using ()
to form a group
we can then use our matcher to get back each group.
// ...
Pattern re2 = Pattern.compile("(aa*)(bb*)(c)");
Matcher m = re2.matcher("abbbbbbc");
if (m.matches()) {
for (int i = 1; i <= 3; i++) {
String s = String.format(
"Found \"%s\" starting at index %d ending at index %d",
m.group(i),
m.start(i),
m.end(i));
System.out.println(s);
}
}
// ...
Nested Classes
The Java Language allows the definition of a class within a class. A nested class is a member of the enclosing class, and much like other kinds of members of a class there are static and non-static inner classes.
-
Static Nested Classes. Use
static
in their definition and have access to the static members of the enclosing class. Static Nested Classes interacts with its enclosing class in the same manner as a top-level class. -
Inner Classes. An Inner Class is associated with an instance of the enclosing class and has direct access to the enclosing class' fields and methods. Inner classes can not define any static members.
Static Nested Classes
Behave the same as top-level classes. It is a way to group related classes together.
|
|
To access static nested classes we need to use the name of the outer class, much like accessing a static member.
-
A.B b = new A.B();
The syntax is similar to how we would access a class within a package that we did not import, e.g., new java.util.ArrayList<Integer>();
Inner Classes
Objects of inner classes exist within an instance of the outer class.
class X {
// code for X
class Y {
// code for Y
}
}
-
An instance of
Y
can only exist within an instance ofX
. -
To access an inner class we need to do so through an instance of the outer class, much like accessing an objects members
X x = new X();
X.Y y = x.new Y();
Shadowing
When creating inner classes things get a little involved when we use the same variable names as the outer class.
-
Shadowing, inner most declaration hides the outer most declaration.
The Java Language provides syntax to allow developers to access variables at different scope levels.
public class ShadowTest {
public int x = 0;
class FirstLevel {
public int x = 1;
void methodInFirstLevel(int x) {
System.out.println("x = " + x); // argument x
System.out.println("this.x = " + this.x); // FirstLevel.x
System.out.println("ShadowTest.this.x = " + ShadowTest.this.x); // ShadowTest.x
}
}
public static void main(String args) {
ShadowTest st = new ShadowTest();
ShadowTest.FirstLevel fl = st.new FirstLevel();
fl.methodInFirstLevel(23);
}
}
Iterator example again
Recall our GenericListIterator
from last lecture. We could define GenericListIterator
as a private inner class.
public abstract class AList<X> implements List<X> {
@Override
public List<X> add(X element) {
return new Cons(element, this);
}
abstract X getFirst();
abstract AList<X> getRest();
abstract AList<X> remove();
@Override
public abstract <Y> AList<Y> applyToEach(Function<X, Y> f);
@Override
public Iterator<X> iterator() {
return this.new GenericListIterator<X>(this); (1)
}
private class GenericListIterator<X> implements Iterator<X> { (2)
private AList<X> list;
public GenericListIterator(AList<X> xes) {
this.list = xes;
}
@Override
public boolean hasNext() {
return !list.isEmpty();
}
@Override
public X next() {
try {
X element = list.getFirst();
list = list.getRest();
return element;
} catch (GenericListException glex) {
throw new NoSuchElementException(glex.getMessage());
}
}
@Override
public void remove() {
throw new UnsupportedOperationException("Don't even think about it!");
}
}
}
1 | Our code returns an instance of our inner class. |
2 | We basically cut and paste the source of GenericListIterator.java inside AList.java and delete GenericListIterator.java . |
In our previous implementation another developer could use the same package name as ours and extend our GenericListIterator
but now that
is a private inner class alterations to GenericListIterator
need to have access to our code.
Anonymous Classes
Another feature of the Java Language is anonymous classes; a class defined in place that does not get a name.
public abstract class AList<X> implements List<X> {
@Override
public List<X> add(X element) {
return new Cons(element, this);
}
abstract X getFirst();
abstract AList<X> getRest();
abstract AList<X> remove();
@Override
public abstract <Y> AList<Y> applyToEach(Function<X, Y> f);
@Override
public Iterator<X> iterator() {
return new Iterator<X> { (1)
private AList<X> list = AList.this; // outer (enclosing) instance
@Override
public boolean hasNext() {
return !list.isEmpty();
}
@Override
public X next() {
try {
X element = list.getFirst();
list = list.getRest();
return element;
} catch (GenericListException glex) {
throw new NoSuchElementException(glex.getMessage());
}
}
@Override
public void remove() {
throw new UnsupportedOperationException("Don't even think about it!");
}
}; // SEMI COLON HERE!! (2)
}
1 | After new instead of a call to a constructor, we have the class implementation of GenericListIterator in-place.
We are defining a class but we are not providing a name for it. |
2 | The inline class definition is found in a place in our code that must terminate with a ; . |
We went from
-
return new GenericListIterator()
, to, -
return new Iterator<X> { … };
Scoping
With inner and anonymous classes there are rules concerning what is accessible from the inner to the outer class.
In order to talk about what is captured and shadowed we need to first explain what final
and effectively final
means
Final
The Java Language provides a modifier key called final
. We can use final
on
-
classes - this class cannot be extended (subclassed).
-
methods - this method cannot be overridden or hidden in subclasses.
-
variable - this variable can only be initialized once and cannot be re-assigned.
Effectively final has no syntax. Java 8 calls a variable effectively final if
-
the variable is not declared
final
-
the code initializes the variable only once and cannot be re-assigned.
Scoping rules
-
Anonymous/Inner classes have access to the members of the enclosing class.
-
Anonymous/Inner classes have access local variables in its enclosing scope that are
final
or effectivelyfinal
-
Anonymous/Inner classes can shadow any other declaration in the enclosing scope that have the same name.
Lambda Expressions
Let’s see how the JDK allows us to sort using Comparator
public class StudentMain {
public static void main(String[] args) {
List<Author> authors = new ArrayList<>();
authors.add(new Author("a", "b", 80));
authors.add(new Author("Neil", "Gaiman", 70));
authors.add(new Author("Franz", "Kafka", 88));
authors.add(new Author("Albert", "Einstein", 80));
authors.add(new Author("Sylvia", "Plath", 50));
authors.add(new Author("Alan", "Moore", 100));
Comparator<Author> firstName = new Comparator<Author>() {
@Override
public int compare(Author o1, Author o2) {
return o1.getFirst().compareTo(o2.getFirst());
}
};
authors.sort(firstName);
System.out.println("\n Sort by first\n" + authors);
}
}
We created an anonymous class that implements Comparator
and the List
interface in java.util
has a method called sort
that given a
Comparator
will update our list to be sorted based on the Comparator
that we provided.
An even shorter syntactical way to obtain the same behaviour is to use a lambda expression instead of an anonymous class.
public class StudentMain {
public static void main(String[] args) {
List<Author> authors = new ArrayList<>();
authors.add(new Author("a", "b", 80));
authors.add(new Author("Neil", "Gaiman", 70));
authors.add(new Author("Franz", "Kafka", 88));
authors.add(new Author("Albert", "Einstein", 80));
authors.add(new Author("Sylvia", "Plath", 50));
authors.add(new Author("Alan", "Moore", 100));
Comparator<Author> firstName = new Comparator<Author>() {
@Override
public int compare(Author o1, Author o2) {
return o1.getFirst().compareTo(o2.getFirst());
}
};
authors.sort(firstName);
System.out.println("\n Sort by first\n" + authors);
authors.sort((Author o1, Author o2) -> { (1)
return o1.getLast().compareTo(o2.getLast());
});
System.out.println("\n Sort by last\n" + authors);
}
}
1 | Lambda expressions in Java specify their arguments in parenthesis (Author o1, Author o2) then the keyword → followed by the body of the lambda inside {} . |
In this example, Comparator
is Functional interface that contains one method. compare
that is not
-
static
-
has no
default
Java then allows us to place a lambda expression in the place where we would have provided an instance of Comparator
. Java takes the lambda expression and
makes it the implementation of compare
for an anonymous instance of Comparator
. This is yet another way to inline our code without the need to create
classes and instances of our new classes.
public class MainStudents {
private static String SEPARATOR = ",\n";
public static void main(String[] args) {
List<Integer> l1 = List.createEmpty();
l1 = l1.add(99).add(88).add(79);
List<Integer> l2 = List.createEmpty();
l2 = l2.add(89).add(78).add(79);
List<Integer> l3 = List.createEmpty();
l3 = l3.add(90).add(88).add(65);
List<Integer> l4 = List.createEmpty();
l4 = l4.add(90).add(88).add(65);
List<Integer> l5 = List.createEmpty();
l5 = l5.add(90).add(88).add(65);
Student s1 = new Student("john doe", "jd", l1);
Student s2 = new Student("mary eliz", "me", l2);
Student s3 = new Student("alex pratt", "ap", l3);
Student s4 = new Student("krista allen", "ka", l4);
Student s5 = new Student("andy howell", "ah", l5);
List<Student> course101 = List.createEmpty();
course101 = course101.add(s1).add(s2).add(s3).add(s4).add(s5);
System.out.println(course101.join(SEPARATOR));
Function<Student, Student> updateLogin = (Student s) -> { (1)
return new Student(s.getName(), s.getLogin() + "@github", s.getGrades());
};
List<Student> updatedCourse = course101.applyToEach(updateLogin);
System.out.println(updatedCourse.join(SEPARATOR));
System.out.println(updatedCourse.applyToEach(
(Student s) -> { (2)
return new Student(s.getName(), s.getLogin() + ".com", s.getGrades());
}).join(SEPARATOR));
System.out.println(updatedCourse.foldOver((Student s, String acc) -> {
return s.getLogin() + SEPARATOR + acc;
}, ""));
}
}
1 | We can use lambda expression to replace the right-hand-side of an anonymous class statement. |
2 | We can use lambda expressions inline |
Aggregate Operations
Java 8 uses Stream<T>
interface to create a sequence of elements for sequential and parallel processing.
Aggregate operations are operations that work on streams. A sequence of aggregate operations is called a pipeline.
public class MainStreams {
public static <R> void main(String[] args) {
List<Integer> lint = new ArrayList<>();
lint.add(1);
lint.add(2);
lint.add(3);
lint.add(4);
lint.add(5);
lint.add(6);
lint.stream()
.filter((Integer val) -> val % 2 == 0)
.forEach(System.out::println);
List<String> chars = new ArrayList<>();
chars.add("a");
chars.add("b");
chars.add("c");
chars.add("d");
List<String> result = chars
.stream()
.map(s -> s.toUpperCase())
.collect(Collectors.toList());
System.out.println(result);
System.out.println(result.stream().collect(Collectors.joining("|")));
List<Author> authors = new ArrayList<>();
authors.add(new Author("a", "b", 80));
authors.add(new Author("Neil", "Gaiman", 70));
authors.add(new Author("Franz", "Kafka", 88));
authors.add(new Author("Albert", "Einstein", 80));
authors.add(new Author("Sylvia", "Plath", 50));
authors.add(new Author("Alan", "Moore", 100));
Comparator<Author> firstName = new Comparator<Author>() {
@Override
public int compare(Author o1, Author o2) {
return o1.getFirst().compareTo(o2.getFirst());
}
};
authors.sort(firstName);
System.out.println("\n Sort by first\n" + authors);
authors.sort((Author o1, Author o2) -> {
return o1.getLast().compareTo(o2.getLast());
});
System.out.println("\n Sort by last\n" + authors);
authors.sort((Author o1, Author o2) -> {
return o1.getAge().compareTo(o2.getAge());
});
System.out.println("\n Sort by age\n" + authors);
System.out.println("\n First names \n" + authors.stream().map(Author::getFirst).collect
(Collectors.toList()));
System.out.println("\n Last names \n" + authors.stream().map(Author::getLast).collect
(Collectors.toList()));
System.out.println("\n Ages \n" + authors.stream().map(Author::getAge).collect
(Collectors.toList()));
}
}
Method references
Sometimes expressions that we would like to capture as a lambda expression are already defined as a named method in a class. Java 8 provides syntax that allows us to refer to a method whether is is a static or an instance method.
-
Class::staticMethod
- is a method reference to the static methodstaticMethod
in the classClass
-
o::m
- is a method reference to the methodm
found on the instanceo
-
String::compareToIgnoreCase)
- refers to an instance methodcompareToIgnoreCase
found inString
. Note that the methodcompareToIgnoreCase
takes 1 argument. The code will pass 2 arguments to the referenceString::compareToIgnoreCase
, e.g.,a
andb
and will then executea.compareToIgnoreCase(b)
. This is equivalent to(String a, String b) → { return a.compareToIgnoreCase(b);}
-
Student::new
- a reference to the constructor