Relevant Reading Material
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.
class A {
// code for A
static class B {
// code for B
}
}
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();
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
Previously we had DDList
implement Iterable<T>
and the code implemented Iterable
by using the instance of DDList
. This implementation choice made it difficult to iterate over a DDList
because the iterator was altering the underlying DDList
object. Here is the code again
public class CellR<E> implements DDList<E>, Iterator<E> {
private E val;
private CellR<E> previous;
private CellR<E> next;
public CellR() {}
public CellR(CellR<E> cell, E element) {}
@Override
public Iterator<E> iterator() {
return this;
}
@Override
public E next() {
E result = this.getVal();
this.remove(this.getVal());
return result;
}
@Override
public boolean hasNext() {
return !isEmpty();
}
public E getVal() {}
public void setVal(E val) {}
public CellR<E> getPrevious() {}
public void setPrevious(CellR<E> previous) {}
public CellR<E> getNext() {}
public void setNext(CellR<E> next) {}
@Override
public Boolean isEmpty() {}
@Override
public Integer size() {}
@Override
public void add(E element) {}
@Override
public void remove(E element) {}
private boolean hasPrevious() {}
private boolean hasNextCell() {}
@Override
public E getElementAt(Integer index) {}
@Override
public int hashCode() {}
@Override
public boolean equals(Object obj) {}
@Override
public String toString() {}
}
We could use an inner class to define a custom iterator implementation that will have access to CellR
data.
public class CellR<E> implements DDList<E>, Iterator<E> {
private E val;
private CellR<E> previous;
private CellR<E> next;
public CellR() {}
public CellR(CellR<E> cell, E element) {}
/**
* Inner Class Iterator
*/
private class DDListIterator implements Iterator<E> {
private Integer index;
public DDListIterator() {
this.index = 0;
}
/**
* {@inheritDoc}
*/
@Override
public boolean hasNext() {
return this.index != size();
}
/**
* {@inheritDoc}
*/
@Override
public E next() {
E result = getElementAt(this.index);
this.index = this.index + 1;
return result;
}
}
@Override
public Iterator<E> iterator() {
return this.new DDListIterator();
}
// For Iterator<E>
@Override
public E next() {
E result = this.getVal();
this.remove(this.getVal());
return result;
}
@Override
public boolean hasNext() {
return !isEmpty();
}
public E getVal() {}
public void setVal(E val) {}
public CellR<E> getPrevious() {}
public void setPrevious(CellR<E> previous) {}
public CellR<E> getNext() {}
public void setNext(CellR<E> next) {}
@Override
public Boolean isEmpty() {}
@Override
public Integer size() {}
@Override
public void add(E element) {}
@Override
public void remove(E element) {}
private boolean hasPrevious() {}
private boolean hasNextCell() {}
@Override
public E getElementAt(Integer index) {}
@Override
public int hashCode() {}
@Override
public boolean equals(Object obj) {}
@Override
public String toString() {}
}
We can now iterate over our list without altering the underlying DDList
object.
public class Main {
/**
* @param args
*/
public static void main(String[] args) {
DDList<Integer> l1 = DDList.Empty();
l1.add(1);
l1.add(2);
l1.add(3);
l1.add(4);
Iterator<Integer> it = l1.iterator();
while (it.hasNext()) {
System.out.println(it.next());
}
for (Integer ele : l1) {
System.out.println(ele);
}
}
}
Generates
1 2 3 4 1 2 3 4
Anonymous Classes
Another feature of the Java Language is anonymous classes; a class defined in place.
public class CellR<E> implements DDList<E>, Iterator<E> {
private E val;
private CellR<E> previous;
private CellR<E> next;
public CellR() {}
public CellR(CellR<E> cell, E element) {}
/**
* Inner Class Iterator
*/
private class DDListIterator implements Iterator<E> {
private Integer index;
public DDListIterator() {
this.index = 0;
}
/**
* {@inheritDoc}
*/
@Override
public boolean hasNext() {
return this.index != size();
}
/**
* {@inheritDoc}
*/
@Override
public E next() {
E result = getElementAt(this.index);
this.index = this.index + 1;
return result;
}
}
@Override
public Iterator<E> iterator() {
//return this.new DDListIterator();
return new Iterator<E>() {
Integer index = 0; // initializer, anonymous classes cannot have constructors
/**
* {@inheritDoc}
*/
@Override
public boolean hasNext() {
return this.index != size();
}
/**
* {@inheritDoc}
*/
@Override
public E next() {
E result = getElementAt(this.index);
this.index = this.index + 1;
return result;
}
}; // NOTE THE SEMICOLON HERE!
}
// For Iterator<E>
@Override
public E next() {
E result = this.getVal();
this.remove(this.getVal());
return result;
}
@Override
public boolean hasNext() {
return !isEmpty();
}
public E getVal() {}
public void setVal(E val) {}
public CellR<E> getPrevious() {}
public void setPrevious(CellR<E> previous) {}
public CellR<E> getNext() {}
public void setNext(CellR<E> next) {}
@Override
public Boolean isEmpty() {}
@Override
public Integer size() {}
@Override
public void add(E element) {}
@Override
public void remove(E element) {}
private boolean hasPrevious() {}
private boolean hasNextCell() {}
@Override
public E getElementAt(Integer index) {}
@Override
public int hashCode() {}
@Override
public boolean equals(Object obj) {}
@Override
public String toString() {}
}
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
Recall our example using List
and sorting methods from Collections
public class StudentMain {
public static void main(String[] args) {
Student s1 = new Student("A", "A", 98);
Student s2 = new Student("B", "A", 100);
Student s3 = new Student("C", "A", 72);
Student s4 = new Student("D", "A", 10);
Student s5 = new Student("E", "A", 66);
Student s6 = new Student("E", "X", 89);
Student s7 = new Student("A", "B", 90);
Student s8 = new Student("A", "C", 76);
Student s9 = new Student("A", "D", 67);
List<Student> course = new ArrayList<>();
course.add(s1);
course.add(s2);
course.add(s3);
course.add(s4);
course.add(s5);
course.add(s6);
course.add(s7);
course.add(s8);
course.add(s9);
System.out.println("Original Class");
System.out.println(course);
Collections.sort(course);
System.out.println("Sorted Class with compareTo");
System.out.println(course);
GradeComparator gradeComparator = new GradeComparator();
Collections.sort(course, gradeComparator);
System.out.println("Sorted Class with GradeComparator");
System.out.println(course);
SurnameComparator surnameComparator = new SurnameComparator();
Collections.sort(course, surnameComparator);
System.out.println("Sorted Class with SurnameComparator");
System.out.println(course);
}
}
We created an interface for each sorting criterion and implemented our interface appropriately.
We could have implemented Comparator
using an anonymous class as well.
Java 8 provides a new feature called lambda expressions. Comparator has the annotation @FunctionalInterface
. This annotation is not code but rather an informative annotation that indicates that this type has one (and only one) abstract method.
Types that have one and only one abstract method (default
methods do not count) are called functional interfaces in Java 8.
public class StudentMain {
public static void main(String[] args) {
Student s1 = new Student("A", "A", 98);
Student s2 = new Student("B", "A", 100);
Student s3 = new Student("C", "A", 72);
Student s4 = new Student("D", "A", 10);
Student s5 = new Student("E", "A", 66);
Student s6 = new Student("E", "X", 89);
Student s7 = new Student("A", "B", 90);
Student s8 = new Student("A", "C", 76);
Student s9 = new Student("A", "D", 67);
List<Student> course = new ArrayList<>();
course.add(s1);
course.add(s2);
course.add(s3);
course.add(s4);
course.add(s5);
course.add(s6);
course.add(s7);
course.add(s8);
course.add(s9);
System.out.println("Original Class");
System.out.println(course);
Collections.sort(course);
System.out.println("Sorted Class with compareTo");
System.out.println(course);
GradeComparator gradeComparator = new GradeComparator();
Collections.sort(course, gradeComparator);
System.out.println("Sorted Class with GradeComparator");
System.out.println(course);
SurnameComparator surnameComparator = new SurnameComparator();
Collections.sort(course, surnameComparator);
System.out.println("Sorted Class with SurnameComparator");
System.out.println(course);
Comparator<Student> gC = (Student o1, Student o2) -> {
return o2.getGrade().compareTo(o1.getGrade());
};
System.out.println("Lambda 1");
Collections.sort(course, gC);
System.out.println(course);
Collections.shuffle(course);
System.out.println("Lambda 2 Inline");
course.sort((o1, o2) -> {
return o1.getFirst().compareTo(o1.getFirst());
});
System.out.println(course);
System.out.println("Lambda 3 Inline");
Collections.shuffle(course);
Collections.sort(course, (o1, o2) -> {
return o2.getGrade().compareTo(o1.getGrade());
});
System.out.println(course);
}
}
Method references
Sometimes expression that we would like to capture as a lambda expression is 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
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.
System.out.println("Print all students with grade > 60");
course.stream().filter(s -> s.getGrade() > 60).forEach(s -> System.out.println(s));
System.out.println("Add 10 to all grades > 60");
course.stream().filter(s -> s.getGrade() > 60).map(s -> {
s.setGrade(s.getGrade() + 10);
return s;
}).forEach(s -> System.out.println(s.toString()));
Separation of Concerns
The best way to understand and implement a system is by breaking it into smaller pieces.
-
Each piece has a reason for its existence.
-
Each piece has a clear definition of how it can be used.
-
Each pieces does one job and it does it well.
-
Single responsibility principle - A class should have one and one job.
-
Open Close principle - open to extension closed for modification.
-
Liskov substitution principle - subtypes honor the properties set by their supertypes.
-
Interface segregation principle - clients should never be forced to implement/depend on interfaces/methods they do not use.
-
Dependency Inversion principle - depend on abstractions not concrete implementations when you can.
-
Design Patterns
A collection of well thought out and well documented solutions to commonly found problems.
-
Creational Patterns - deal with constructing new instances
-
Structural Patterns - deal with relationships between objects
-
Behavioural Patterns - deal with communication patterns between objects