MOOC.fi Java Programming I Part 10
Fundamentals of Java
Part 10
- Handling collections as streams
- Stream is a way of going through a collection of data such that the programmer determines the operation to be performed on each value. No record is kept of the index or the variable being processed at any given time. like lambda
long numbersDivisibleByThree = inputs.stream() .mapToInt(s -> Integer.valueOf(s)) .filter(number -> number % 3 == 0) .count();
-
A stream can be formed from any object that implements the Collection interface (e.g., ArrayList, HashSet, HashMap, …) with the stream() method. The string values are then converted (“mapped”) to integer form using the stream’s mapToInt(value -> conversion) method. The conversion is implemented by the valueOf method of the Integer class, which we’ve used in the past. We then use the filter (value -> filter condition) method to filter out only those numbers that are divisible by three for further processing. Finally, we call the stream’s count() method, which counts the number of elements in the stream and returns it as a long type variable.
-
Lambda expression: Stream values are handled by methods related to streams. Methods that handle values get a function as a parameter that determines what is done with each element. What the function does is specific to the method in question. For instance, the filter method used for filtering elements is provided a function which returns the a boolean true or false, depending on whether to keep the value in the stream or not. The mapToInt method used for transformation is, on the other hand, provided a function which converts the value to an integer, and so on.
-
Functions thats handle stream elements cannot change values of variables outside of the function. This has to do with how static methods behave - during a method call, there is no access to any variables outside of the method. On the other hand with functions, the values of variables outside the function can be read, assuming that those values of those variables do not change in the program.
-
Stream methods can be roughly divided into two categories: (1) intermediate operations intended for processing elements and (2) terminal operations that end the processing of elements.
-
Intermediate operations return a value that can be further processed - you could, in practice, have an infinite number of intermediate operations chained sequentially (& separated by a dot). E.g. filter(), mapToInt()
-
A terminal operation returns a value to be processed, which is formed from, for instance, stream elements. E.g. average()
-
4 terminal operations:
-
.count() counts number of values in a list
-
.forEach() goes through values in list. It defines what is done to each list value and terminated the stream processing.
-
.collect() gathers the list values into a data structure. The collect method is given as a parameter to the Collectors object to which the stream values are collected - for example, calling Collectors.toCollection(ArrayList::new) creates a new ArrayList object that holds the collected values.
-
.reduce() combines list items. reduce(initialState, (previous, *object) -> actions on the object)*
-
Intermediate operations:
-
.map() changes a stream containing person objects into a stream containing first names.
-
.filter()
-
.distinct() returns a stream that only contains unique values
-
Reading files with streams
- The Comparable Interface
-
The Comparable interface defines the compareTo method used to compare objects. If a class implements the Comparable interface, objects created from that class can be sorted using Java’s sorting algorithms.
-
The compareTo method required by the Comparable interface receives as its parameter the object to which the “this” object is compared. If the “this” object comes before the object received as a parameter in terms of sorting order, the method should return a negative number. If, on the other hand, the “this” object comes after the object received as a parameter, the method should return a positive number. Otherwise, 0 is returned. The sorting resulting from the compareTo method is called natural ordering.
-
The compareTo method required by the interface returns an integer that informs us of the order of comparison.
public class Member implements Comparable<Member> { private String name; private int height; public Member(String name, int height) { this.name = name; this.height = height; } public String getName() { return this.name; } public int getHeight() { return this.height; } @Override public String toString() { return this.getName() + " (" + this.getHeight() + ")"; } @Override public int compareTo(Member member) { if (this.height == member.getHeight()) { return 0; } else if (this.height > member.getHeight()) { return 1; } else { return -1; } } //same but different way @Override public int compareTo(Member member) { return this.height - member.getHeight(); } }
- Stream does not sort the original list - only the items in the stream are sorted.
List<Member> member = new ArrayList<>(); member.add(new Member("mikael", 182)); member.add(new Member("matti", 187)); member.add(new Member("ada", 184)); member.stream().forEach(m -> System.out.println(m)); System.out.println(); // sorting the stream that is to be printed using the sorted method member.stream().sorted().forEach(m -> System.out.println(m)); member.stream().forEach(m -> System.out.println(m)); // sorting a list with the sort-method of the Collections class Collections.sort(member); member.stream().forEach(m -> System.out.println(m));
mikael (182) matti (187) ada (184) mikael (182) ada (184) matti (187) mikael (182) matti (187) ada (184) mikael (182) ada (184) matti (187)
-
A class can implement multiple interfaces. Multiple interfaces are implemented by separating the implemented interfaces with commas (public class … implements FirstInterface, SecondInterface …). Implementing multiple interfaces requires us to implement all of the methods for which implementations are required by the interfaces.
-
Sorting Method as a Lambda Expression without using Comparable interface: Both the sort method of the Collections class and the stream’s sorted method accept a lambda expression as a parameter that defines the sorting criteria. More specifically, both methods can be provided with an object that implements the Comparator interface, which defines the desired order - the lambda expression is used to create this object.
ArrayList<Person> persons = new ArrayList<>(); persons.add(new Person("Ada Lovelace", 1815)); persons.add(new Person("Irma Wyman", 1928)); persons.add(new Person("Grace Hopper", 1906)); persons.add(new Person("Mary Coombs", 1929)); persons.stream().sorted((p1, p2) -> { return p1.getBirthYear() - p2.getBirthYear(); }).forEach(p -> System.out.println(p.getName())); System.out.println(); persons.stream().forEach(p -> System.out.println(p.getName())); System.out.println(); Collections.sort(persons, (p1, p2) -> p1.getBirthYear() - p2.getBirthYear()); persons.stream().forEach(p -> System.out.println(p.getName()));
Ada Lovelace Grace Hopper Irma Wyman Mary Coombs Ada Lovelace Irma Wyman Grace Hopper Mary Coombs Ada Lovelace Grace Hopper Irma Wyman Mary Coombs
- When comparing strings, we can use the compareTo method provided by the String class.
persons.stream().sorted((p1, p2) -> { return p1.getName().compareTo(p2.getName()); }).forEach(p -> System.out.println(p.getName()));
-
Sorting By Multiple Criteria: comparing and thenComparing. The comparing method is passed the value to be compared first, and the thenComparing method is the next value to be compared. The thenComparing method can be used many times by chaining methods, which allows virtually unlimited values to be used for comparison.
-
When we sort objects, the comparing and thenComparing methods are given a reference to the object’s type - the method is called in order and the values returned by the method are compared. The method reference is given as Class::method
Comparator<Film> comparator = Comparator .comparing(Film::getReleaseYear) .thenComparing(Film::getName); Collections.sort(films, comparator); for (Film e: films) { System.out.println(e); }
-
Implementations of methods defined in the interface must always have public as their visibility attribute.
-
When a class implements an interface, it signs an agreement. The agreement dictates that the class will implement the methods defined by the interface. If those methods are not implemented in the class, the program will not function.
- The interface defines only the names, parameters, and return values of the required methods. The interface, however, does not have a say on the internal implementation of its methods. It is the responsibility of the programmer to define the internal functionality for the methods.
-
- Other useful techniques (StringBuilder)
- StringBuilder:
public String toString() { if (elements.isEmpty()) { return MessageFormat.format("The collection {0} is empty.", name); } final StringBuilder elementsStrings = new StringBuilder(); final String elementSingularPlural = elements.size() > 1 ? "elements" : "element"; final String amountOfElements = MessageFormat.format("The collection {0} has {1} {2}:\n", name, elements.size(), elementSingularPlural); for (String element : elements) { elementsStrings.append(element); if (elements.size() > 1) { elementsStrings.append("\n"); } } return amountOfElements + elementsStrings; }
String numbers = ""; for (int i = 1; i < 5; i++) { numbers = numbers + i; } System.out.println(numbers);
-
Here 5 strings are created because calling numbers + i creates new string each time.
-
String creation is not a quick operation. Space is allocated in memory for each string where the string is then placed. If the string is only needed as part of creating a larger string, performance should be improved.
-
Java’s ready-made StringBuilder class provides a way to concatenate strings without the need to create them. A new StringBuilder object is created with a new StringBuilder() call, and content is added to the object using the overloaded append method, i.e., there are variations of it for different types of variables. Finally, the StringBuilder object provides a string using the toString method.
StringBuilder numbers = new StringBuilder(); for (int i = 1; i < 5; i++) { numbers.append(i); } System.out.println(numbers.toString());
-
Regular expression: defines a set of strings in a compact form and is used to check correctness of string.
-
E.g. check if a student number entered by the user is in the correct format. A student number should begin with “01” followed by 7 digits between 0–9. Can go through the character string representing the student number using the charAt method. Another way would be to check that the first character is “0” and call the Integer.valueOf method to convert the string to a number. You could then check that the number returned by the Integer.valueOf method is less than 20000000. Or use regex
System.out.print("Provide a student number: "); String number = scanner.nextLine(); if (number.matches("01[0-9]{7}")) { System.out.println("Correct format."); } else { System.out.println("Incorrect format."); }
-
A vertical line indicates that parts of a regular expressions are optional. For example, *00 111 0000* defines the strings 00, 111 and 0000. The respond method returnstrue if the string matches any one of the specified group of alternatives. -
Parentheses determines which part of a regular expression is affected by the rules inside the parentheses. Say we want to allow the strings 00000 and 00001. We can do that by placing a vertical bar in between them this way \00000 00001. Parentheses allow us to limit the option to a specific part of the string. The expression *\0000(0 1)* specifies the strings 00000 and 00001. *car( s )* defines the singular (car) and plural (cars) forms of the word car. - Quantifier * repeats 0 … times
String string = "trolololololo"; if (string.matches("trolo(lo)*")) { System.out.println("Correct form."); } else { System.out.println("Incorrect form."); } // prints correct form
- Quantifier + repeats 1 … times
String string = "trolololololo"; if (string.matches("tro(lo)+")) { System.out.println("Correct form."); } else { System.out.println("Incorrect form."); } // print correct form
-
quantifier ? repeats 0 or 1 times
-
quantifier {a} repeats a times
-
quantifier {a,b} repeats a … b times
-
quantifier {a,} repeats a … times
-
*5{3}(1 0)5{3} defines strings that begin and end with three fives. An unlimited number of ones and zeros are allowed in between. -
Character Classes (Square Brackets) specifies set of characters in a compact way. Characters are enclosed in square brackets, and a range is indicated with a dash. For example, [145] means (1 4 5) and [2-36-9] means (2 3 6 7 8 9). Similarly, the entry [a-c]* defines a regular expression that requires the string to contain only a, b and c. -
Enumerated Type - Enum: If we know the possible values of a variable in advance, we can use a class of type enum, i.e., enumerated type to represent the values. Enumerated types are their own type in addition to being normal classes and interfaces. An enumerated type is defined by the keyword enum.
- enum lists the constant values it declares, separated by a comma. Enum types, i.e., constants, are conventionally written with capital letters.
public enum Suit { DIAMOND, SPADE, CLUB, HEART }
public class Card { private int value; private Suit suit; public Card(int value, Suit suit) { this.value = value; this.suit = suit; } @Override public String toString() { return suit + " " + value; } public Suit getSuit() { return suit; } public int getValue() { return value; } }
Card first = new Card(10, Suit.HEART); System.out.println(first); if (first.getSuit() == Suit.SPADE) { System.out.println("is a spade"); } else { System.out.println("is not a spade"); }
- 2 enums were compared with equal signs because each enum field gets a unique number code. Just as other classes in Java, these values also inherit the Object class and its equals method. The equals method compares this numeric identifier in enum types too.
- StringBuilder:
«««< HEAD *
======= * The numeric identifier of an enum field value can be found with ordinal(). The method actually returns an order number - if the enum value is presented first, its order number is 0. If its second, the order number is 1, and so on.
d5855a6ecf82f734e6022e97ff6c5f680cf04fee
* Object references in enums: Enumerated types may contain object reference variables. The values of the reference variables should be set in an internal constructor of the class defining the enumerated type, i.e., within a constructor having a private access modifier. Enum type classes cannot have a public constructor.
```java
public enum Color {
// constructor parameters are defined as
// the constants are enumerated
RED("#FF0000"),
GREEN("#00FF00"),
BLUE("#0000FF");
private String code; // object reference variable
private Color(String code) { // constructor
this.code = code;
}
public String getCode() {
return this.code;
}
}
System.out.println(Color.GREEN.getCode());
// #00ff00
```
* Iterator:
```java
public class Hand {
private List<Card> cards;
public Hand() {
this.cards = new ArrayList<>();
}
public void add(Card card) {
this.cards.add(card);
}
public void print() {
this.cards.stream().forEach(card -> {
System.out.println(card);
});
}
}
```
* Print method of class prints each card.
* ArrayList and other "object containers" that implement the Collection interface implement the Iterable interface, and they can also be iterated over with the help of an iterator - an object specifically designed to go through a particular type of **object collection**.
```java
public void print() {
Iterator<Card> iterator = cards.iterator();
while (iterator.hasNext()) {
System.out.println(iterator.next());
}
}
```
* The iterator is requested from the cards list containing cards. The iterator can be thought of as a "finger" that always points to a particular object inside the list. Initially it points to the first item, then to the next, and so on... until all the objects have been gone through with the help of the "finger".
* hasNext() asks if there are any objects still to be iterated over. If there are, the next object in line can be requested from the iterator using the next() method.
* next() returns next object in line to be processed and moves the iterator/finger to point to following object in the collection
* The object reference returned by the iterator's next method can of course also be stored in a variable like this:
```java
public void print(){
Iterator<Card> iterator = cards.iterator();
while (iterator.hasNext()) {
Card nextInLine = iterator.next();
System.out.println(nextInLine);
}
}
```
* Why is iterator used? Let's see this example where we create a method that removes cards from a given stream with a value lower than the given value
```java
public class Hand {
// ...
public void removeWorst(int value) {
this.cards.stream().forEach(card -> {
if (card.getValue() < value) {
cards.remove(card);
}
});
}
}
```
* results in java.util.ConcurrentModificationException error. Because when a list is iterated over using the forEach method, it's assumed that the list is **not modified** during the traversal.
* If you want to remove some of the objects from the list during a traversal, you can do so using an iterator. Calling the *remove()*method of the iterator object neatly removes form the list the item returned by the iterator with the previous *next()* call.
```java
public class Hand {
// ...
public void removeWorst(int value) {
Iterator<Card> iterator = cards.iterator();
while (iterator.hasNext()) {
if (iterator.next().getValue() < value) {
// removing from the list the element returned by the previous next-method call
iterator.remove();
}
}
}
}
```