Interfaces
Learning Objectives
Interfaces are fundamental to understanding Java’s design philosophy. Understanding what they are and when to use them will help future topics make sense.
Self Study
As you read through the resources below try to answer the following questions:
- Can you summarise in a sentence or two what an interface is in Java? How do they compare to the classes you’ve seen so far?
- How is an interface different from a class? What are the key features of each? Think about how an interface relates to a class and compare to how a class relates to an instance of that class.
- Why do interfaces exist? What key problems do they solve? Think about: coupling, polymorphism, testing
Reading Materials
- The Importance of Interfaces in Java: A Comprehensive Guide - This Medium article provides some motivation for why interfaces exist in Java and the problems they solve. It’s really useful to understand the why before learning the how. Note the article does use some quite abstract language, so don’t worry if it doesn’t all click right away; the aim here is just to give a bit of context before we jump in to the technical details.
- Baeldung - Interfaces in Java - Focus on the examples that show how to create an interface in Java
- W3Schools Java Interface - Have a look at the code snippets for more examples of basic interfaces
- GeeksforGeeks - Interfaces in Java - Read the summary of when to use classes vs interfaces, and have a look at the diagrams to see the relationship between classes and interfaces
Exercises
✍️Exercise 1.1
Imagine you are designing a system for a bank to send monthly statements to customers. Some customers prefer to receive physical letters in the post, whereas others prefer to receive emails.
- Create a
StatementSenderinterface with a method#sendStatementthat accepts aStringparameterstatementContent - What should the return type of this method be?
- What is the visibility of interface methods? Why?
- Make sure to write Javadoc on both the class and method
- Why is this Javadoc important? Think about who we are aiming this javadoc at (i.e. who will read it / benefit from it)?
- Create implementations of
LetterStatementSenderandEmailStatementSender - The implementations of
#sendStatementshould simply print to the console a success method to say that the statement has been sent via the given delivery method (in reality these methods would likely call out to some additional processing or third party email/postal service, but we don’t need to do that here) - Now create a class StatementDeliveryService with a method
#deliverStatement(String statementContent, StatementSender statementSender) - This method should send the content using the sender
- Now create a main method that creates a new
StatementDeliveryServiceand sends a statement via both letter and email
Reflections
Think about the following questions, make notes and be prepared to talk through your thoughts in the workshop.
- Look at the example you just created. Why was it useful to use an interface? How would it look if we had only used classes - can you think of any problems that might cause?
- How will the use of interfaces make the code more maintainable in future? What if we decide we also want the option to send statements via text message?
- How does using an interface enforce the contract that it sets out? Again think about writing a new text message implementation.
- Similarly, what if we decide in future that physical letters are no longer supported; which code do we need to update?
- Think about how you would test
StatementDeliveryService. In practice you wouldn’t want to send an actual email or a letter every time you ran the test, so how could theStatementSenderinterface help here? (NB our expectation here is for a testing-implementation as they won’t have covered mocking yet
✍️Exercise 1.2
- Create a
Statementinterface and add a new#sendStatementmethod toStatementSenderthat accepts the new interface - What is it called when you have two versions of the same method?
- What methods might
Statementhave? Think about the type of information you’d expect to see on a bank statement. - Create two implementations of
Statement:AllTransactionsMonthlyStatement- As the name suggests, includes all transactions, both ingoing and outgoing for the current monthOutgoingTransactionsMonthlyStatement- Only includes the outgoing transactions for the current month
- How should the implementation behaviour differ?
- Create a new class StatementService that:
- Has methods to create and send both types of statements
- Uses
StatementDeliveryServiceto send the statements
- Note we’ll need a new method on
StatementDeliveryServicethat accepts aStatement - Refactor your main method to use
StatementService - You’re now starting to build up a hierarchy of classes and interfaces. Have a look back through the reflection questions about and apply them to your refactored system - are the answers the same? Are interfaces even more important now? Imagine we increased the complexity even further by adding more services and functionality - do interfaces become more or less important?
Data Structures
Learning Objectives
Self Study
As you read through the resources below try to answer the following questions:
- What are some real life examples of when a
Map,ListandSetshould be used to store data?- As an example: which would be best for a) Storing the names of varieties of fruit sold by a greengrocer, b) A register of students first names in a school class, c) The names of the students and the grades they achieved
- Can you think of another example for when to use a
Map,ListandSet? - What are the properties of Maps, Lists, Sets that make each suitable for different types of data? Think about ordering, duplicates and key/values here.
- And how do they compare in terms of time complexity when inserting / removing / updating items? Use Big-O notation here.
- Is it possible to instantiate a
Map/List/Setdirectly? If not, why not? - How does this relate to interfaces? Where does the
Collectionsinterface fit in? - What are the most common implementations? Can you find a
Setimplementation that has a different ordering property to normal? - What is the
#equalsmethod and how does it differ to == ? - How do Sets and Maps make use of the equals method?
- What is the relationship between
#equalsand#hashcode? - Why can’t we always just use == ?
- What happens in the following cases: a)
x == foo, b)x.equals(foo), c)foo.equals(x)? (where in each case x is a null variable and foo is a non-null variable) - Why bother with
Collectionsat all, can’t we just use Arrays? What advantages do these pre-built data structures provide? - Bonus: Is it possible to create
Collectionsof primitive types? Why?
Reading Materials
Collections
- GeeksforGeeks - Collections Framework
- Read the motivation for why Collections are useful (remember, we always want to start with why!)
- Look at the sections on the main interfaces and implementations, and look at the class diagrams to see how they relate to one another
- Just focus on Sets, Lists, Maps for now; ignore queues and concurrency.
- Generics With Collections
- We aren’t focussed too much on generics at this stage, but a very basic understanding is needed to work with Collections, so focus mainly on how to enforce what types can be contained within a collection e.g. how do I create a List of Strings? What about a Set of Animals? Or a List of Sets of integers?!
- Ignore the section on generic methods
- Java Collections Complexity
- Worth reading the whole article to see how different types of Collection compare, and try to understand why the time complexity is what it is so that you can reason it yourself from first principles if necessary
- Don’t worry about memorizing all of the Big-O time complexity for every collection though, use this table instead:
Equals and hashCode
- Equality In Java: Operators, Methods And What To Use When
- This provides a good explanation and example as to why the equals method exists and how it differs to ==
- Java Hashcode
- Introduction and overview of what a hashcode is
- Why Use Hashcode In Java
- Provides an explanation of why #hashCode exists and how it relates to #equals
- There is some mention here of ‘buckets’ and other details of how HashSet and HashMap implementations work → if you are interested you can research in more detail but it is not required learning at this stage, though it may provide a good discussion topic in a workshop!
Data structures
- List Interface in Java- Show how to construct and modify each data structure → these will be useful to read when completing the exercises below
- Baeldung - Guide to Java HashSet
- Baeldung - Guide to Java HashMap
Exercises
✍️Exercise 2.1
You are given three arrays containing data relating to students and their grades: firstNames, lastNames and grades. Each student is represented by the same index in each array, so firstNames[i] has the surname lastNames[i] and grade grades[i]. Create a class (with an appropriate name) that takes the three arrays as constructor parameters and has the following methods to collect the data into more sensible structures (note you will need to choose the appropriate return type!):
getFirstNames(String[] firstNames, String[] lastNames, int[] grades)getDistinctFirstNames(String[] firstNames, String[] lastNames, int[] grades)getDistinctFirstNamesInAlphabeticalOrder(String[] firstNames, String[] lastNames, int[] grades)→ Hint: no for loops needed; consider an appropriate collection implementationgetLastNameToGrade(String[] firstNames, String[] lastNames, int[] grades)(Note you can assume there are no duplicate last names)
Create a main method that calls each method above and prints the output to the console. The data to use should be:
- firstNames = {Anuket, Eryn, Filip, Raquel, Eryn}
- lastNames = {Hassan, Doe, Pappas, Clarke, Jones}
- grades = {22, 45, 35, 22, 41}
Reflection
Suppose there are a set of twins in the class, so there are some duplicate lastNames. Can we still build a map of students to their grades?
✍️Exercise 2.2
- Create a
Studentclass with fieldsfirstNameandlastName - Implement the following method:
Map<Student, Integer> getStudentToGrade - What methods do you need to define on
Studentfor this map to work safely? - Update the data set to include the twins and print the result of
getStudentToGradeto the console
✍️Exercise 2.3
- Now let’s ditch the arrays completely and work only with collections: in your main method, call each of the methods you’ve already created and assign the results to new variables. This will be our starting data.
- Suppose a new student joins the class. Modify each of the collections you’ve created to include the new student and their grade
- Now suppose a student leaves the class. Modify each of the collections to remove that student.
- Now suppose that one of the students was found to have cheated in their exam and their grade needs to be modified. Update the collections to set the grade of one of the students to 0.
Reflections
Think about the following questions, make notes and be prepared to talk through your thoughts in the workshop.
- Would it have been possible to use Arrays to model the same data?
- Would it have been easier, harder or the same? Why?
- What was your thought process when deciding which data structure was most appropriate for modelling particular data? What things did you consider?
- Suppose the initial data set was input incorrectly and the same student’s name and grade was input twice. How would your
Map<Student, Grade>handle this? Why? - What is the difference between variables: a)
List firstNames, b)List<String> firstNames, c)ArrayList<String> firstNames? Which is preferable? Bonus: how aboutList<?> firstNames?
✍️Exercise 2.4
- Given the same data as above, without using for loops, find:
- The highest and lowest grades
- The first and last names of the student with the highest grade
- Any firstNames that belong to more than one student
Exceptions
Learning Objectives
Self Study
As you read through the resources below try to answer the following questions:
- How do Java exceptions compare to non-Java exceptions that you’ve seen before? Think about Javascript / HTTP errors etc - do they serve the same purpose?
- Why does Java have checked and unchecked exceptions? What are the features of each? Think of an example of when you’d use each
- What is the hierarchy of exceptions in Java?
- What are the options for handling checked exceptions? What are the Java keywords involved?
- What is a null pointer exception?
- What is the Closable interface and why do we need it? What are the options for closing closable resources?
Reading Materials
- Both of these pages provide a solid overview of exceptions in Java; read at least one of them:
- Checked vs Unchecked Exceptions
- A Summary Of Why The Two Exception Types Exist
- Explanation Of Nulls And Null Pointer Exceptions
Checked vs Unchecked Exceptions Quiz
Complete the short quiz here to check your understanding of checked vs unchecked exceptions
Exercises
✍️Exercise 3.1
Create a service that safely reads text files
- Create a
FileReaderServiceclass with a method#readFile(String filename)that:- Attempts to read a file using
BufferedReader - Returns the file contents as a
String - Properly handles
FileNotFoundExceptionandIOException - Uses try/finally to ensure the file is closed
- Safely handles the possibility of
fileNamebeing null - Create a custom checked exception called
InvalidFileFormatException - Throw this if the file is empty
- Attempts to read a file using
- From a main method, call the service with some dummy text files, including an empty file
✍️Exercise 3.2
- Refactor your above method to use try-with-resources instead of try/finally
Reflections
Think about the following questions, make notes and be prepared to talk through your thoughts in the workshop.
- Why are
FileNotFoundExceptionandIOExceptionchecked rather than unchecked exceptions? What advantages does this give developers and systems that use them? - In this example we had control over which strings we passed to the method, so in theory we could make sure they wouldn’t ever be null. When writing production code do we have this same level of control?
- When is it beneficial to include null checks, and when isn’t it?
- How does try-with-resources relate to a try/finally block? Think about flexibility / clean code etc
- If an exception is thrown, does a finally block get executed before or after the catch block? How could you prove this?
- What about if the catch block throws an unchecked exception? Does the finally block still execute?
✍️Exercise 3.3
Should try/catch blocks be used as part of ‘standard’ processing? e.g. say I have a payment processing system that accepts cash or card, and I first attempt to process a card payment, before throwing an error and processing cash if no card details are found:
void processPayment(Payment payment) {
try {
cardProcessor.process(payment);
} catch(NoCardDetailsFoundException e) {
cashProcessor.process(payment);
}
}- Will this work correctly? If we know some payments will definitely be made by cash, is there a better way to handle this? Why?
- What are some potential problems with throwing errors like this? Think about: when we want to find out about errors / unnecessary processing etc
- Update the above pseudocode to remove the try/catch block and handle the card/cash options more gracefully. Feel free to invent some methods on Payment to help you!
Streams and Optionals
Learning Objectives
Self Study
As you read through the resources below try to answer the following questions:
- What are the main advantages of using
Streamover for loops? Are there any cases where using aStreammay be worse? Think about: readability / performance / maintainability - What are the main ways to open a stream? Think about collections and arrays
- How are streams evaluated under the covers? i.e. are they executed line by line?
- What are intermediate and terminal operations?
- How does the way Java streams are evaluated compare to JS map, filter etc?
- What are the tradeoffs in terms of performance, memory etc?
- How do Java streams tie into interfaces? i.e. when streaming a set/list where is the #stream method defined?
- What about Maps, can they be streamed? How?
- How does using an
Optionalto handle ‘missing’ values compare to explicitly checking for nulls? Can you think of any advantages / disadvantages to usingOptionals? - Can an
Optionalitself be null (i.e.Optional<String) myOptionalString = null)? What problems does this bring? How does this impact your answer above - In general it is not recommended to use Optionals as parameters in (especially public) methods - why?
Reading Materials
- Lambdas and Method References → just scan through to learn what each is and the syntax
- Introduction to Streams and Examples of Key Methods → you don’t need to memorise each method, just use this to learn what streams are, when they are useful and to get a feel for the sorts of operations you can perform
- Introduction to optionals → again, use this to learn what they are, why they are used and don’t worry too much about the specific method names (that’s what your IDE is for!), but definitely do understand what you’d want to use an optional for
- Comparison of Optionals vs null handling
- An introduction to programming paradigms - you don’t need to memorise everything here, but having some familiarity with the imperative, declarative and functional paradigms, and being able to recognise examples of each will be useful
Exercises
Alfa Streams and Lambdas exercises - TODO when we move this to git: need the superclass and the imports, as well as the source for NameFinder, ShapeCounter, Remainders..
/**
* =======================
* Streams
* =======================
* <p>
* Time to venture into the world of functional programming and try using some Streams.
* <p>
* Let's go... Have a go at solving the following exercises first using loops, and then convert them to use streams. As you work through, think about how the two approaches compare in terms of readability, how easy the code would be to adapt in future (as we know in enterprise software, code never stays the same forever as new requirements and bugs are worked on!), and compare the trade-offs in efficiency.
*/
public class TestStreamsExercise extends ExerciseTest {
/**
* For this first exercise, we will implement the {@link NameFinder#printNamesStartingWithA(Collection)}
* method. It should take a collection of names and *use a stream* to find the names that start
* with the letter 'A' and print them. It should not be case-sensitive, so both "Anne" and "andy"
* would be printed.
* <p>
* This test will check your method's output.
*/
@Test
public void testPrintNamesStartingWithA() {
List<String> names = Lists.newArrayList("Antony", "John", "amy", "Ben", "Dan", "Ian", "Andrew", "Peter", "Oliver");
new NameFinder().printNamesStartingWithA(names);
checkPrintedOutput(format("Antony%namy%nAndrew%n"));
}
/**
* Now let's try something a bit more complicated. We need to implement the method
* {@link NameFinder#vowelNamesCapitalisedDescending(Collection)} so that it will take
* our list of names, find the ones that start with a vowel, capitalise them (in case the first
* letter isn't already capitalised) and then return them in a list in reverse-alphabetical
* order.
* <p>
* Got all that? Give it a go and the test below will check your result.
*/
@Test
public void testVowelNamesCapitalisedDescending() {
List<String> names = Lists.newArrayList("Antony", "John", "amy", "Ben", "Dan", "Ian", "Andrew", "Peter", "Oliver");
List<String> expectedResult = Lists.newArrayList("Oliver", "Ian", "Antony", "Andrew", "Amy");
assertEquals("Names should be as expected", expectedResult, new NameFinder().vowelNamesCapitalisedDescending(names));
}
/**
* Here's a problem involving a Map. Let's see if we can solve it using a stream.
* <p>
* The method {@link ShapeCounter#countShapesWithSides(Map, int)} should take a map of shapes to
* their number of sides and a number of sides and will tell us how many shapes in the map have
* that many sides.
* <p>
* Use the test below to check your method is working.
*/
@Test
public void testStreamingMapEntries() {
Map<String, Integer> shapesToSides = new HashMap<>();
shapesToSides.put("Right-Angled Triangle", 3);
shapesToSides.put("Isosceles Triangle", 3);
shapesToSides.put("Square", 4);
shapesToSides.put("Rhombus", 4);
shapesToSides.put("Parallelogram", 4);
shapesToSides.put("Hexagon", 6);
shapesToSides.put("Octagon", 8);
ShapeCounter shapeCounter = new ShapeCounter();
assertEquals("Should be two 3-sided shapes", 2, shapeCounter.countShapesWithSides(shapesToSides, 3));
assertEquals("Should be three 4-sided shapes", 3, shapeCounter.countShapesWithSides(shapesToSides, 4));
}
/**
* Maths time again. For this test we will need to implement the method
* {@link Remainders#printRemainders(int)} to print the remainders of the numbers
* 1-20 when divided by a given divisor. The remainders should all be on one line
* and should be separated by commas. There should be a line break at the end.
* <p>
* For example, for a divisor of 2 it would go:
* <pre>
* 1,0,1,0 ... and so on until we have 20 numbers
* </pre>
*/
@Test
public void testPrintingRemainders() {
Remainders remainders = new Remainders();
remainders.printRemainders(2);
remainders.printRemainders(5);
checkPrintedOutput(format("1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0%n1,2,3,4,0,1,2,3,4,0,1,2,3,4,0,1,2,3,4,0%n"));
}
/**
* Time to use one of the more 'interesting' terminal operations. In this exercise,
* you'll need to use {@code .groupingBy()}.
* <p>
* To pass this test, implement the {@link Remainders#remainderGroups(int)} method
* to return a {@code Map<Integer, List<Integer>>} with an entry for each different
* remainder. The key should be the remainder itself and the value should be a list
* of all the numbers between 1 and 20 that have that remainder.
*/
@Test
public void testGroupingRemainders() {
Remainders remainders = new Remainders();
Map<Integer, List<Integer>> expectedRemaindersFor5 = new HashMap<>();
expectedRemaindersFor5.put(0, List.of(5, 10, 15, 20));
expectedRemaindersFor5.put(1, List.of(1, 6, 11, 16));
expectedRemaindersFor5.put(2, List.of(2, 7, 12, 17));
expectedRemaindersFor5.put(3, List.of(3, 8, 13, 18));
expectedRemaindersFor5.put(4, List.of(4, 9, 14, 19));
assertEquals("Remainder groups for 5 should be correct", expectedRemaindersFor5, remainders.remainderGroups(5));
Map<Integer, List<Integer>> expectedRemaindersFor8 = new HashMap<>();
expectedRemaindersFor8.put(0, List.of(8, 16));
expectedRemaindersFor8.put(1, List.of(1, 9, 17));
expectedRemaindersFor8.put(2, List.of(2, 10, 18));
expectedRemaindersFor8.put(3, List.of(3, 11, 19));
expectedRemaindersFor8.put(4, List.of(4, 12, 20));
expectedRemaindersFor8.put(5, List.of(5, 13));
expectedRemaindersFor8.put(6, List.of(6, 14));
expectedRemaindersFor8.put(7, List.of(7, 15));
assertEquals("Remainder groups for 8 should be correct", expectedRemaindersFor8, remainders.remainderGroups(8));
}
/**
* What if we wanted to know 'how many times does each remainder appear?'. We'd need to get a map
* with an entry for each remainder where the value tells us the count as a Long.
* <p>
* That's exactly what the method {@link Remainders#remainderCounts(int)} should do. Let's
* implement it to give us the count for each remainder for a given divisor.
*/
@Test
public void testCountingRemainders() {
Remainders remainders = new Remainders();
Map<Integer, Long> expectedRemaindersFor5 = new HashMap<>();
expectedRemaindersFor5.put(0, 4L);
expectedRemaindersFor5.put(1, 4L);
expectedRemaindersFor5.put(2, 4L);
expectedRemaindersFor5.put(3, 4L);
expectedRemaindersFor5.put(4, 4L);
assertEquals("Remainder counts for 5 should be correct", expectedRemaindersFor5, remainders.remainderCounts(5));
Map<Integer, Long> expectedRemaindersFor8 = new HashMap<>();
expectedRemaindersFor8.put(0, 2L);
expectedRemaindersFor8.put(1, 3L);
expectedRemaindersFor8.put(2, 3L);
expectedRemaindersFor8.put(3, 3L);
expectedRemaindersFor8.put(4, 3L);
expectedRemaindersFor8.put(5, 2L);
expectedRemaindersFor8.put(6, 2L);
expectedRemaindersFor8.put(7, 2L);
assertEquals("Remainder counts for 8 should be correct", expectedRemaindersFor8, remainders.remainderCounts(8));
}
}Reflections
Think about the following questions, make notes and be prepared to talk through your thoughts in the workshop.
- In general, do you find streams more or less readable than loops? Think about trivial vs more complex data manipulation
- How about when writing code; do you find it easier to write using streams or loops?
- Are there any hard and fast rules about when and when not to use streams? What are some trade-offs to consider when deciding?
- Can you think of an example of when using an Optional would be useful? How about an example of when using an Optional may actually make things worse?
- Do you need to memorise all the available stream operations? What is a useful tool to find out which methods are available?