07
-
7.0 Employees Application
- A business system has a collection of employee objects; employees are allocated a unique id on joining the company. Other information held includes the employee name, role, and length of service (in complete years).
-
public class Employee implements Comparable<Employee>,Serializable { private final int employeeId; private String employeeName; private String employeeRole; private int lengthOfService; private static int numberOfEmployees=0; public Employee() { this.employeeId = ++numberOfEmployees; this.employeeName = null; this.employeeRole = null; this.lengthOfService = 0; } public Employee(String employeeName, String employeeRole) { … } public Employee(int employeeId, String employeeName, String employeeRole, int lengthOfService) { … } @Override public int hashCode() { return getId()*31 + getEmployeeName().hashCode()*31 + getEmployeeRole().hashCode()*31 + getLengthOfService()*31; } @Override public boolean equals(Object o) { if (o instanceof Employee) { Employee e = (Employee)o; return e.getId() == getId() && e.getEmployeeName() == getEmployeeName() && e.getLengthOfService() == getLengthOfService() && e.getEmployeeRole().equals(getEmployeeRole()); } else { return false; } } @Override public int compareTo(Employee compareEmployee) { int empId = ((Employee) compareEmployee).getId(); //ascending order return this.employeeId - empId; //descending order //return employeeId - this.empId; } public static Comparator<Employee> EmployeeNameComparator = new Comparator<Employee>() { @Override public int compare(Employee emp1, Employee emp2) { String empName1 = emp1.getEmployeeName(); String empName2 = emp2.getEmployeeName(); //ascending order return empName1.compareTo(empName2); //descending order //return empName2.compareTo(empName1); } };
- Model Class Checklist:
- 1. fields (types)
- 2. constructor(s) (new, load)
- 3. getters and setters
- 4. toString()
- 5. additional operations
- e.g. add/remove from collection field
- 6. hashCode() and equals()
- 7. Comparable and compareTo()
- 8. Comparators
-
public class EmployeesController { private final Repository<Employee> repository; public EmployeesController() { … char c = inputHelper.readCharacter("Load File (Y/N)?"); if (c == 'Y' || c == 'y') { String fileName = inputHelper.readString("Enter filename"); this.repository = new Repository<>(fileName); } else { this.repository = new Repository<>(); } } private char displayEmployeesMenu() { InputHelper inputHelper = new InputHelper(); System.out.print("\nA. List Employees (Iterator)"); System.out.print("\tB. List Employees In Reverse (Iterator)"); System.out.print("\tC. List Employees With Role"); System.out.print("\nD. List Employees With Role And Service"); System.out.print("\tE. List Employees With Search Criteria"); System.out.print("\tF. List Employees Using Lambda Expressions"); System.out.print("\nG. List Employees Using Predicate"); System.out.print("\tH. Process Employees"); System.out.print("\tI. Process Employees With Function"); System.out.print("\nJ. Process Elements With Function"); System.out.print("\tK. Process Elements With Aggregate Operations"); System.out.print("\tQ. Quit\n"); return inputHelper.readCharacter("Enter choice", "ABCDEFGHIJKQ"); } public void run() { boolean finished = false; String role="Developer"; int service = 3; do { char choice = displayEmployeesMenu(); switch (choice) { case 'A': listEmployees(); break; case 'B': listEmployeesInReverse(); break; …
-
7.1 Collections and Retrieval
- This application requires different retrieval functionality as want to be able to collect and retrieve based on fields required, conditions to be met, and sort order. The simplest solution is a different method for each type of retrieval. However this leads to large number of methods and code changes for new forms of retrieval. Consequently our goal is to create generic method which will handle multiple types of query.
- List Employees
-
private void listEmployees() { System.out.format("\033[31m%s\033[0m%n", "Employees"); Iterator it = repository.getItems().listIterator(); while (it.hasNext()) { Employee e = (Employee) it.next(); System.out.println(e); } }
-
Employees ========= employee id: 1, employee name: Sergi Busquets, employee role: Project Leader, length of service: 5 employee id: 2, employee name: Sergi Roberts, employee role: Developer, length of service: 2 employee id: 3, employee name: Edgar Davids, employee role: Tester, length of service: 0 employee id: 4, employee name: Dani Alves, employee role: Developer, length of service: 9 employee id: 5, employee name: Jordi Alba, employee role: Developer, length of service: 4 employee id: 6, employee name: Ivan Rakitic, employee role: Developer, length of service: 1 employee id: 7, employee name: Luis Suarez, employee role: Tester, length of service: 2
- 🔗 Java Interface ListIterator
- List Employees In Reverse
-
private void listEmployeesInReverse() { ListIterator it = repository.getItems().listIterator(repository.getItems().size()); while (it.hasPrevious()) { Employee e = (Employee) it.previous(); System.out.println(e); } }
-
Employees ========= employee id: 7, employee name: Luis Suarez, employee role: Tester, length of service: 2 employee id: 6, employee name: Ivan Rakitic, employee role: Developer, length of service: 1 employee id: 5, employee name: Jordi Alba, employee role: Developer, length of service: 4 employee id: 4, employee name: Dani Alves, employee role: Developer, length of service: 9 employee id: 3, employee name: Edgar Davids, employee role: Tester, length of service: 0 employee id: 2, employee name: Sergi Roberts, employee role: Developer, length of service: 2 employee id: 1, employee name: Sergi Busquets, employee role: Project Leader, length of service: 5
- List Employees With Role
-
private void listEmployeesWithRole(String role) { Iterator it = repository.getItems().listIterator(); while (it.hasNext()) { Employee e = (Employee) it.next(); if (e.getEmployeeRole().equals(role)) System.out.println(e); } }
-
Employees : Developer ========= employee id: 2, employee name: Sergi Roberts, employee role: Developer, length of service: 2 employee id: 4, employee name: Dani Alves, employee role: Developer, length of service: 9 employee id: 5, employee name: Jordi Alba, employee role: Developer, length of service: 4 employee id: 6, employee name: Ivan Rakitic, employee role: Developer, length of service: 1
- List Employees With Role & Service
-
private void listEmployeesWithRoleAndService(String role, int service) { Iterator it = repository.getItems().listIterator(); while (it.hasNext()) { Employee e = (Employee) it.next(); if (e.getEmployeeRole().equals(role) && e.getLengthOfService() >= service) System.out.println(e); } }
-
Employees : Developer, Service >=: 3 ========= employee id: 4, employee name: Dani Alves, employee role: Developer, length of service: 9 employee id: 5, employee name: Jordi Alba, employee role: Developer, length of service: 4
-
7.2 Using a Functional Interface
- Recall from previous lectures that an interface specifies a set of abstract methods. A class implements an interface and supplies concrete methods for each abstract method defined in the interface. A functional interface has only one abstract method and to ‘instantiate’ a functional interface we must create an object and invoke the method on that object. A local class can be used to both provide a concrete method implementation of the abstract method and permit instantiation of an object to invoke the method on.
- A local class is defined where it is needed and we would require a local class for each type of query. However this make our program vulnerable to new demands and changes in class structure. Let’s consider the use of a functional interface to check an employee against some specified criteria. The interface requires a "test()" method. A local class can be used to implement the concrete "test()" code for a specific query and invoke that method on the Employee object itself returning true or false
-
interface CheckEmployee { boolean test(Employee e); } class CheckEmployeeForRoleAndService implements CheckEmployee { String role; int service; CheckEmployeeForRoleAndService(String role, int service) { this.role = role; this.service = service; } @Override public boolean test(Employee e) { return e.getEmployeeRole().equals(this.role) && e.getLengthOfService() >= this.service; } } public boolean matchRoleAndService(String role,int service) { CheckEmployeeForRoleAndService tester = new CheckEmployeeForRoleAndService(role, service); return tester.test(this); } private void listEmployeesWithSearchCriteria(String role, int service) { Iterator it = repository.getItems().listIterator(); while (it.hasNext()) { Employee e = (Employee) it.next(); if (e.matchRoleAndService(role, service)) System.out.println(e); } }
- Using An Anonymous Class
- Recall that a local class is only being used to instantiate an object that can invoke the overridden interface method. There is no need for a named class here and we can use an anonymous class. This reduces the amount of code however the syntax is still fairly unwieldy. Let’s revisit the "matchRoleAndService()" method and replace the local class with an anonymous class implementation.
-
public boolean matchRoleAndService(String role, int service) { return new CheckEmployee() { @Override public boolean test(Employee e) { return e.getEmployeeRole().equals(role) && e.getLengthOfService() >= service; } }.test(this); }
- Using Lambda Expressions
- Recall that a functional interface has only one method and therefore we shouldn’t need to specify that method. A lambda expression allows specification of functionality which can be passed into a method and used to ‘instantiate’ the functional interface, i.e. treat code as data. This would allow us to pass different types of functionality, queries, into a method, i.e. a more general retrieval method. Let’s implement a general match() method within the employee class with a CheckEmployee interface argument which can be instantiated with a lambda expression
-
Employee class: public boolean match(CheckEmployee tester) { return tester.test(this); } Controller class: private void listEmployeesUsingLambdaExpression(String role, int service) { while (it.hasNext()) { Employee e = (Employee) it.next(); if (e.match( emp -> emp.getEmployeeRole().equals(role) && emp.getLengthOfService() >= service )) System.out.println(e); } }
- The syntax of a lambda expression is:
- comma-separated list of formal parameters
- Enclosed within parentheses unless there is only one
- arrow token
- ->
- body
- single expression or block
- return value or void
- if void braces not needed
- Could be considered as an anonymous method
- Facilitates general methods where specific functionality is passed as an argument
- Lambda expressions are very popular in modern Java programming. They facilitate functional programming and can be used to defer execution as long as possible by passing functionality to place where it is actually needed. They can also be used to represent a functional interface as an expression which reduces code and provides flexibility. They provide better support for the DRY (Don’t Repeat Yourself) principle.
- 🔗 When to Use Nested Classes, Local Classes, Anonymous Classes, and Lambda Expressions
-
7.3 Standard Functional Interfaces
- Java 8 has recognized the commonality between functional interfaces and has created a number of standard functional interfaces which make the JDK work and are open to user code.
- 🔗 Package java.util.function
- The complier is responsible for determining the type for a lambda expression. It determines the type for the expression based on the context in which the expression appear. The types for lambda expressions are known as target types. Each type has a single abstract method called the functional method. Given that functional interfaces can only have a single abstract method lambda expressions can only be used if the type of lambda body and return type match. Lambda expression have four uses; assignment, invocation, return, and cast context. General-purpose (Generic) interfaces specify one or more parameter types, void or return value for abstract methods; used for common requirements:
- Supplier<T>
- get() method
- Supplies a value of type T
- Consumer<T>
- accept() method
- Consumes a value of type T
- Function<T, R>
- apply() method
- A function with argument of type T and return value of type R
- Predicate<T>
- test() method
- Boolean-valued function
-
7.4 Using a Predicate & Other Standard Functional Interfaces
- Using a Predicate
- Recall the CheckEmployee interface has only one "test()" method which we ‘instantiated’ using: local class, anonymous class, and a lambda expression. The Predicate<T> general purpose interface already supplies this specification so we can use it:
-
public boolean match(Predicate<Employee> tester) { return tester.test(this); }
- The Predicate<T> interface:
-
interface Predicate<T> { boolean test(T t); }
- is instantiated as:
-
interface Predicate<Employee> { boolean test(Employee t); }
- Using Other Standard Functional Interfaces
-
private void processEmployees(String role, int service) { while (it.hasNext()) { Employee e = (Employee) it.next(); if (e.match(emp -> emp.getEmployeeRole().equals(role) && emp.getLengthOfService() >= service)) processEmployee(e, emp -> System.out.println(emp)); } }
- Recall the "Consumer<T>" interface contains an "accept()" method which consumes an object of type T (recap: T is a generic type, i.e. somewhere in our code we will create a type that we substitute in for T and consumer will then be of and expect that type). Once the "Predicate<T>" has been used to select employees that match the criteria detailed in the lambda expression.
- We can ‘instantiate’ the Consumer<T> interface to process those employees:
-
private void processEmployee(Employee employee, Consumer<Employee> process) { process.accept(employee); }
- The concrete implementation of the accept() method is supplied as argument: "emp -> System.out.println(emp)". The Function<T, R> interface can be instantiated with a function which takes an argument of type T and returns an object of type R. Again we can use a lambda expression to ‘instantiate’ the interface: "emp -> emp.getEmployeeName()". We can extend the previous example to use a predicate for matching, a function for selecting fields and a consumer for processing.
-
private void processEmployeesWithFunction(String role, int service) { while (it.hasNext()) { Employee e = (Employee) it.next(); if (e.match(emp -> emp.getEmployeeRole().equals(role) && emp.getLengthOfService() >= service)) { processEmployeeWithFunction(e, emp -> emp.getEmployeeName(), str -> System.out.println(str)); } } } private void processEmployeeWithFunction(Employee employee, Function<Employee, String> mapper, Consumer<String> process) { String data = mapper.apply(employee); process.accept(data); }
-
7.5 Generics
- Generics enable types (classes & interface) to be parameterized, i.e. written in a generic manner with no specific types and types are supplied at time of instantiation. This facilitates reuse of code and extensibility giving us many advantages including stronger type checking at compile time. Common parameter names (enclosed by a diamond <>) are:
- E – Element (collections framework)
- K – Key
- N – Number
- T – Type
- V – Value
-
private <X, Y> void processElementsWithFunction( Iterable<X> source, Predicate<X> tester, Function<X, Y> mapper, Consumer<Y> process ) { for (X x : source) { if (tester.test(x)) { Y data = mapper.apply(x); process.accept(data); } } }
- In order to use a generic function we need to obtain a "source" of objects and filter objects that match "Predicate" object "tester". Then map each filtered object to a value as specified by the "Function" object "mapper". We perform actions on each mapped object using the "Consumer" "object" block.
-
processElementsWithFunction( repository.getItems(), (Employee e) -> e.getEmployeeRole().equals(role) && e.getLengthOfService() >= service, e -> e.getEmployeeName(), str -> System.out.println(str) );
-
Generic Function ================ Dani Alves Jordi Alba
-
processElementsWithFunction( projects, (Project p) -> p.getNumberOfAllocatedDays() >= 10 && p.getEmployees().size() >= 2, p -> Integer.toString(p.getProjectId()) + ": " + p.getProjectTitle(), str -> System.out.println(str) );
-
7.6 Streams
- A stream is a sequence of elements. Processing elements in a collection can be achieved by streaming the collection and then applying a pipeline of aggregate operations (operations that perform a function), typically: stream, filter, map and process. Streams are made up of a sequence of aggregate operations which are known as pipelines. Pipelines have a source, typically a collection and are divided into intermediate, e.g. filtering a collection by some criteria, and terminal operations, typically processing the matched (filtered) elements of the collection to produce output or a new value.
- 🔗 Package java.util.stream
- Aggregate Operations Overview
- Operations that perform a function
- Process elements from a stream
- Not the collection
- Pipeline is a sequence of aggregate operations
- Typically accept lambda expressions as parameters
-
repository.getItems() .stream() .filter(e -> e.getEmployeeRole().equals(role) && e.getLengthOfService() >= service) .map(e -> e.getEmployeeName()) .forEach(str ->System.out.println(str));
-
projects .stream() .filter(p -> p.getNumberOfAllocatedDays() >= 10 && p.getEmployees().size() >= 2) .map(p -> Integer.toString(p.getProjectId()) + ": " + p.getProjectTitle()) .forEach(str -> System.out.println(str));
-
run: Aggregate Operations ==================== Dani Alves Jordi Alba 1: Order Processing 2: Stock Control 4: CRM 5: Web
- 🔗 Aggregate Operations
processElements Action Aggregate Operation Obtain a source of objects Stream<E> stream() Filter objects that match a Predicate object Stream<T> filter(Predicate<? super T> predicate) Map objects to another value as specified by a Function object <R> Stream<R> map(function<? super T, ? extend R> mapper) Perform an action as specified by a Consumer object void forEach(consumer<? super T> action) -
7.7 Generic Repository Class
- Note we previously tried to minimize reference to the class of object stored in the Repository collection. However, we still had some reference to Customer, in this case Customer would be replaced by Employee :
-
public class Repository { private List<Customer> items;
- We are aiming to produce a generic Repository class where the collection can hold any suitable class of object. Therefore, we replace the references to the class of object with a generic class T. This allows us to specify the class of object to be stored in the repository at run time rather than development time.
-
public class Repository<T extends RepositoryObject> implements RepositoryInterface<T>, Serializable { private List<T> items; … @Override public List<T> getItems() { return this.items; } …
- Note T (and whatever replaces it at instantiation time) must implement a "getId()" method
-
public T getItem(int id) { for (T item:this.items) { if (item.getId() == id) return item; } return null; }
- Therefore we need to ensure that any class that replaces T has a "getId()" method. To do this we create a RepositoryObject abstract class which specifies an abstract getId() method. Any class which desires to hold objects in our generic Repository class must extend this abstract class and provide a concrete implementation of the getId() method.
-
public class Repository<T extends RepositoryObject> public abstract class RepositoryObject { public abstract int getId(); }
- Note we could specify other methods here that we might need in implementing additional methods in the generic Repository class. Similarly we could provide a default implementation of getId() which uses Java facilities to generate a unique id.
-
public class Employee extends RepositoryObject implements Comparable<Employee>, Serializable { private final int id; @Override public int getId() { return this.id; }
- The controller class defines the repository object and so has to specify the class of object to store in the repository:
-
private final Repository<Employee> repository;
- The DAOInterface has no reference to the class of object stored in the repository so need not change:
-
public Repository load(String filename); public void store(String filename, Repository repository);
- The DAOObjImpl class contains no reference to the class of object and will work provide the class implements the Serializable interface:
-
@Override public Repository load(String filename) { Repository repository = new Repository(); try { … repository = (Repository) ois.readObject(); } catch(ClassNotFoundException ex){ } return repository; }
- Note the text implementation still needs to know about the class of object as it has to create objects from text strings read from the text file