Access level modifiers determine whether other classes can use a particular field or invoke a particular method.
Access Control
An exception is an event that occurs during the execution of a program that disrupts the normal flow of instructions.
The Java language uses exceptions to handle errors and other exceptional events.
When an error occurs within a method, the method creates an object and hands it off to the runtime system. This object contains information about the error, including its type and the state of the program when the error occurred.
When a method throws an exception, the JVM tries to find something to handle it.
The set of possible "somethings" to handle the exception is the list of methods called to get to the method where the error occurred. This list of methods is known as the call stack.
class MyClass {
public void myMethod() throws Exception {
if(error) {
throw new Exception();
}
}
}
class MyClass {
public void myMethod() throws Exception{
if(error) {
throw new Exception();
}
}
}
class Main {
public static void main(String[] args) {
MyClass myClass = new MyClass();
try{
myClass.myMethod();
} catch (Exception e) {
System.out.println(e.getMessage());
}
}
}
public class MyException extends Exception{
public MyException(String message) {
super(message);
}
public MyException (){
super("A custom exception.");
}
}
class MyClass {
public void myMethod() throws MyException{
if(error) {
throw new MyException();
}
}
}
class Main {
public static void main(String[] args) {
MyClass myClass = new MyClass();
try{
myClass.myMethod();
} catch (MyException e) {
System.out.println(e.getMessage());
} catch (Exception e){
System.out.println(e.getMessage());
}
}
}
class MyClass {
public void myMethod() throws MyException{
if(error) {
throw new MyException();
}
}
}
class Main {
public static void main(String[] args) {
MyClass myClass = new MyClass();
try{
myClass.myMethod();
} catch (MyException e) {
System.out.println(e.getMessage());
} catch (Exception e){
System.out.println(e.getMessage());
} finally {
System.out.println("This will always happen, regardless of an exception being thrown or not";
}
}
}
class MyClass {
public void myMethod(){
try {
myMethod2();
} catch (MyException e) {
System.out.println (e.getMessage());
}
}
private void myMethod2() throws MyException {
myMethod3();
}
private void myMethod3() throws MyException {
if(error) {
throw new MyException();
}
}
}
"Every input results in an output."
This is the fundamental idea in which computers are based on.
Input - signals or data received by the system
Output - signals or data sent from the system.
The Java platform has a package called java.io that provides system input and output through data streams, serialization and the file system.
An I/O stream represents an input source or an output destination. A Stream can represent many different kinds of sources and destinations, and support many different kinds of data.
A program uses an input stream to read data from a source and an output stream to write data to a destination. Both operations occur one item at a time.
Programs use byte streams to perform input and output of 8-bit bytes. All input byte stream classes are descended from InputStream.
// READING BYTES FROM A FILE
public static void main(String[] args) {
// OPEN AN INPUT STREAM WITH A FILE PATH AS THE SOURCE
FileInputStream fileInputStream = new FileInputStream("file_path");
// READ ONE BYTE; THIS IS A BLOCKING METHOD
int b = fileInputStream.read(); // returns the next byte read, or -1
// READ MULTIPLE BYTES
byte[] buffer = new byte[1024];
int num = fileInputStream.read(buffer); // returns the total number of bytes read, or -1
// ALWAYS CLOSE THE STREAMS
fileInputStream.close();
}
All output byte stream classes are descended from OutputStream interfaces.
// WRITE BYTES TO A FILE
public static void main(String[] args) {
FileInputStream fileInputStream = new FileInputStream("file_path");
// OPEN AN OUTPUT STREAM WITH A FILE PATH AS THE DESTINATION
FileOutputStream fileOutputStream = new FileOutputStream("file_path");
// WRITE ONE BYTE TO A FILE
fileOutputStream.write(fileInputStream.read());
// WRITE MULTIPLE BYTES TO A FILE
String message = "This is a very profound message";
fileOutputStream.write(message.getBytes());
// ALWAYS CLOSE THE STREAMS
fileOutputStream.close();
}
Write a program capable of writing the contents of a file into another file.
Byte Streams should only be used to perform very low-level I/O.
However, it's important to discuss them, since all the more complicated streams are built on top of byte streams.
The Java platform stores character values using Unicode conventions.
Computers are able to represent letters by associating each character to a number.
The lists of recognised characters are called encoding schemes.
Unfortunately for us, there are many different character encoding schemes, i.e., there are many ways to map between bytes, code points and characters.
A few of them include:
All input character stream classes are descendent from Reader.
// WRITE BYTES TO A FILE
public static void main(String[] args) {
FileReader fileReader = new FileReader("file_path");
char[] buffer = new char[1024];
// READ THE FIRST 1024 CHARACTERS FROM THE FILE; USING THE DEFAULT ENCODING SCHEME
int num = fileReader.read(buffer);
fileReader.close();
}
All output character stream classes are descendent from Writer.
// WRITE BYTES TO A FILE
public static void main(String[] args) {
FileWriter fileWriter = new FileWriter("file_path");
fileWriter.write("Some pretty message to write to a file.");
fileWriter.close();
}
The examples we've seen so far are handled directly by the OS. This can make a program much less efficient.
To reduce this kind of overhead, the Java platform implements buffered I/O streams.
public static void main(String[] args) throws IOException {
// THE BUFFERED READER WRAPS THE FILE READER
BufferedReader reader = new BufferedReader(new FileReader("file_path"));
String line = "";
String result = "";
while((line = reader.readLine()) != null) {
result += line + "\n";
}
reader.close();
}
public static void main(String[] args) throws IOException {
BufferedWriter writer = new BufferedWriter(new FileWriter("file_path"));
writer.write(text);
// IF THE BUFFER IS NOT FULL, FLUSH WILL FORCE WRITE
writer.flush();
// AUTO-FLUSH IS DONE ON CLOSE
writer.close();
}
Write a Java program capable of doing the following:
The Java programming language allows you to define a class within another class.
A nested class is a member of its enclosing class.
Non-static nested classes have access to other members of the enclosing class, even if they are declared private.
Static nested classes do not have access to other members of the enclosing class (unless they're also static).
A nested class can be declared private, public, protected, or package private, contrary to outer classes, which can only be declared as public or package private.
It's a way to logically group classes that are only used in one place: if a class is only useful to another class, then it makes sense to embed it inside that class.
It increases encapsulation.
It can lead to more readable and maintainable code, by reducing class file pollution.
Add the filtering functionality to our Directory Analyser.
It should now be able to list all the files that start with a combination of letters.
Check the FilenameFilter class and use an anonymous class for this exercise.
An Iterator is an object that can be used to loop through collections, like the LinkedList and ArrayList we've talked about before. Iterating is a technical term for looping.
Implementing this interface forces us to implement the iterator() method. This allows the object to be the target of the enhanced-for loop statement.
Iterating through our NodeContainer class is uglier than it needs to be. If we make it implement Iterable, that operation becomes a lot cleaner.
public interface Interable {
Iterator iterator();
}
public interface Iterator {
Object next();
boolean hasNext();
}
Making NodeContainer implement Iterable.
Storing Object objects in the NodeContainer: the cost of too much flexibility.
A generic type is a generic class or interface that is parameterised over types
public class Box {
private Object object;
public void set(Object object) {
this.object = object;
}
public Object get() {
return object;
}
}
public class Box<T> {
private T object;
public void set(T object) {
this.object = object;
}
public T get() {
return object;
}
}
The normal version
The generic version
T is a type parameter.
Type parameters go inside angled brackets.
T defines a generic type that will be specified at the time of creation of a Box object.
public class Main {
public static void main(String[] args){
// CREATING A BOX OF BALLS
Box<Ball> ballBox = new Box<>();
ballBox.set(new Ball("football"));
ballBox.set(new Ball("tennis"));
balBox.set(new Pencil()); // COMPILING ERROR
}
}
By convention, type parameter names are single, uppercase letters.
The most commonly used type parameter names are:
By convention, type parameter names are single, uppercase letters.
The most commonly used type parameter names are:
Turn the NodeContainer class type safe using generics.
A Collection represents a group of objects known as its elements.
It contains methods that perform basic operations.
public interface Collection<E> {
int size();
boolean isEmpty();
boolean contains(Object element);
boolean add(E element);
boolean remove(Object element);
Iterator<E> iterator();
(...)
}
It also contains methods that operate on entire collections. We call these bulk operations.
public interface Collection<E> {
(...)
boolean containsAll(Collection<?> c);
boolean addAll(Collection<? extends E> c);
boolean removeAll(Collection<?> c);
boolean retainAll(Collection<?> c);
void clear();
}
A List is an ordered Collection.
Lists may contain duplicate elements.
When we implemented the two dynamic containers on the last exercise, we were actually implementing very simple versions of two types of lists: LinkedList and ArrayList.
LinkedList - Sequencial implementation of the List interface. All of the operations performed will traverse the whole list from the beginning or the end (depending on how close the specified index is).
ArrayList - Resizable-array implementation of the List interface.
A Queue is a collection for holding elements prior to processing.
Queues inherit the methods of Collection, and add other insertion/extraction/inspection methods.
Typically, a queue orders elements in a FIFO manner.
Specific implementations include the PriorityQueue, LinkedList and BlockingQueue.
LinkedList - As seen before, LinkedList implements both List and Queue Interfaces.
Adding/accessing/removing elements can be performed from both ends.
public static void main(String[] args) {
Queue<Integer> integerQueue2 = new LinkedList<>();
integerQueue2.add(10);
integerQueue2.add(5);
integerQueue2.add(12);
Iterator it2 = integerQueue2.iterator();
while (it2.hasNext() != false) {
System.out.print(it2.next() + " ");
}
}
PriorityQueue - Elements of the priority queue are ordered according to their natural ordering.
Elements inserted into a PriorityQueue must implement the Comparable interface.
public static void main(String[] args) {
Queue<Integer> integerQueue1 = new PriorityQueue<>();
integerQueue1.add(10);
integerQueue1.add(5);
integerQueue1.add(12);
while (!integerQueue1.isEmpty()) {
System.out.print(integerQueue1.remove() + " ");
}
}
The To-do list
A Set is a collection that contains only the methods inherited from Collection, and it adds the restriction that duplicate elements are not allowed.
Specific implementations include HashSet, TreeSet, etc. Different implementations may use different storage methods, allow for navigation or sorting...
The ground rule for any of these implementations is that any two elements e1 and e2 may not be equal: e1.equals(e2).
equals() - The default implementation of this method in the Object class states that equality is the same as object identity. This method may be overriden to change an object's definition of equality.
This method should be reflexive, symmetrical, transitive and consistent.
public class Person {
private String name;
private int age;
public Person(String name, int age) {...}
@Override
public boolean equals(Object o) {
if (this == o) {
return true;
}
if (o == null || getClass() != o.getClass()) {
return false;
}
Person person = (Person) o;
return age == person.age && name.equals(person.name);
}
}
hashcode() - Objects that are equal to each other must return the same hashcode. However, the contrary isn't true: different objects might return the same hashcode. The value of hashcode may only change if a property that is used by equals() changes.
public class Person {
private String name;
private int age;
public Person(String name, int age) {
this.name = name;
this.age = age;
}
@Override
public int hashCode() {
return Objects.hash(name, age);
}
}
If you override the equals() method, you should also override the hashcode method.
The Dupe Finder
The Word Histogram
The Snake Game