Develop code that uses Java SE 8 collection improvements, including Collection.removeIf(), List.replaceAll(), Map.computeIfAbsent(), and Map.computeIfPresent() methods.
Develop code that uses String objects in the switch statement, binary literals, and numeric literals, including underscores in literals.
Use Lock, ReadWriteLock, and ReentrantLock classes in the java.util.concurrent.locks.
Format dates, numbers, and currency values for localization with the NumberFormat and DateFormat classes, including number and date format patterns.
Recursively access a directory tree by using the DirectoryStream and FileVisitor interfaces
Find a file by using the PathMatcher interface.
Observe the changes in a directory by using the WatchService interface.
Java 8 brings some new helper methods for Collection
s to write less verbose code.
The first one is forEach()
(we talked about it earlier):
default void forEach(Consumer<? super T> action)
That calls the accept()
method on each element of the collection. So instead of having something like:
for(int i = 0; i < list.size(); i++) {
System.out.println(list.get(i));
}
Or:
for(String s : list) {
System.out.println(s);
}
Now we can have it as (without creating a stream):
list.forEach(System.out::println);
Another one is removeif()
:
default boolean removeIf(Predicate<? super E> filter)
That iterates over the elements of the collection and removes an element if it matches the given predicate (if the implementation of the Collection
doesn't support removal, an UnsupportedOperationException
is thrown). So instead of having something like:
Iterator<String> it = list.iterator();
while (it.hasNext()) {
String s = it.next();
if(s != null && s.length() < 3) {
it.remove();
}
}
Now we can have just:
list.removeIf( s -> s != null && s.length() < 3 );
We also have replaceAll()
:
default void replaceAll(UnaryOperator<E> operator)
That replaces each element of this list with the result of applying the operator to the element. So instead of having something like:
Iterator<String> it = list.iterator();
while (it.hasNext()) {
String s = it.next();
it.set(s.toUpperCase());
}
Now we just have:
list.replaceAll(s -> s.toUpperCase());
Java 8 also brings new helper methods to Map
s. There's two interesting methods in particular.
One is computeIfAbsent()
:
default V computeIfAbsent(
K key,
Function<? super K,? extends V> mappingFunction)
This method calculates the value of the given key, and adds it to the map only if:
null
value associatednull
For example:
Map<String, String> letters = new HashMap<>();
letters.put("a", "a");
letters.put("b", null);
Function<String, String> func = k -> {
if(k.startsWith("d")) return null;
return k.toUpperCase();
};
// Won't update,already in map
letters.computeIfAbsent("a", func);
// Will update
letters.computeIfAbsent("b", func);
// Will add
letters.computeIfAbsent("c", func);
// Won't add
letters.computeIfAbsent("d", func);
// Yes, there's also a forEach in Maps
letters.forEach( (key, value) ->
System.out.format("%s-%s;", key, value)
);
The output:
a-a;b-B;c-C;
The other method is computeIfPresent()
:
default V computeIfPresent(
K key,
BiFunction<? super K,? super V,? extends V> remappingFunction)
This method updates the value of a given key only if:
null
value associated and the function also returns a non-null
valueIf the key has a non-null
associated and the function returns null
, the key is removed from the map. For example:
Map<String, String> letters = new HashMap<>();
letters.put("a", "a");
letters.put("b", null);
letters.put("d", "d");
BiFunction<String, String, String> func = (k, v) -> {
if(k.startsWith("d")) return null;
return k.toUpperCase();
};
// Will update
letters.computeIfPresent("a", func);
// Won't update
letters.computeIfPresent("b", func);
// Won't add
letters.computeIfPresent("c", func);
// Will remove
letters.computeIfPresent("d", func);
letters.forEach( (key, value) ->
System.out.format("%s-%s;", key, value)
);
In this case, the output will be:
a-A;b-null;
A switch
statement is another way to represent an if-else
statement. This is its syntax:
switch(expression) {
case constant_value_1:
statements;
case constant_value_2:
statements;
default:
statements;
}
The switch expression must evaluate to one of the following types:
byte
, short
, char
, int
enum
String
Anything else, like a float
, will generate a compiler error.
Here's an example of a switch
statement:
String s = "Jack";
switch(s) {
case "Mike":
System.out.println("Good morning Mr. " + s);
break;
case "Laura":
System.out.println("Good morning Mrs. " + s);
break;
default:
System.out.println("Good morning " + s);
}
There are many things to take into account with a switch
statement.
First, each case needs either a constant value or a final
variable initialized at declaration. Otherwise, a compile time error will be generated:
final int goodFinal = 2;
final int badFinal;
badFinal = 3;
int var = 1;
switch(var) {
case 1: // OK
var *= 2;
break;
case goodFinal: // OK
var++;
break;
case badFinal: // Compiler error!
var--;
}
It's not valid to have more than case
with the same value:
final int goodFinal = 2;
int var = 1;
switch(var) {
case 1: // OK
var *= 2;
break;
case 2: // Compiler error!
var++;
break;
case goodFinal: // Compiler error!
var--;
}
Besides, case
labels are evaluated from top to down beginning from the first case
constant that matches the switch
expression. This means that all the subsequent statements will be executed until the end of the switch
statement, or a break
is found. For example:
int var = 1;
switch(var) {
case 1:
System.out.println("Shirt");
case 2:
System.out.println("Pants");
case 3:
System.out.println("Shoes");
}
Will output:
Shirt
Pants
Shoes
Because of this, we can have a switch statement like the following:
int var = 1;
switch(var) {
case 1:
case 2:
System.out.println("Pants");
break;
case 3:
System.out.println("Shoes");
}
Where, if var
is 1
or 2
, execution will fall through, and Pants
will be printed.
Also, notice that default
doesn't need a break
because it's the last element of the switch
statement. However, default
can appear in any position, so watch for it.
switch
statement can only test for equality. This is important when using a String
expression because we have to take into account the meaning of object equality. In other words, this won't match:
String s = "a";
switch(s) {
case "A": // Strings are case-sensitive, so this case won't match
...
}
In Java, integers numbers are by default of type int
and floating-point numbers are by default of type double
.
Integer numbers are the ones with types byte
, short
, int
, and long
and generally expressed in decimal base. However, they can also be expressed as hexadecimal or binary numbers.
Hexadecimal numbers consist of numbers 0
through 9
and letters A
through F
. To specify a hexadecimal number, you add the 0x
or 0X
prefix.
int hex = 0x2B; // The number 43
Binary numbers consist of the numbers 0
and 1
. To specify a binary number, you add the 0b
or 0B
prefix.
int bin = 0b0010110; // The number 22
These numbers can be cast to float
or double
types, but we can't express these types as hexadecimal or binary numbers. For example:
float goodFloat = 0xF2; // OK, number 242.0
double goodDouble = 0b11110110; ; // OK, number 246.0
float badFloat = 0xF2f; // Compilation error!
double badDouble = 0b11110110d; ; // Compilation error!
From Java 7, you can place any number of underscores between the digits of numbers to separate groups of digits and improve readability.
You cannot put underscores in the following places:
Here are some examples:
int i = 34_765; // OK
float f = 43.987_876; // OK
short s = 0b0000____0101; // OK
int i2 = 100_; // Compilation error!
int i3 = 0_x1D; // Compilation error!
int i4 = 0x_1D; // Compilation error!
float f2 = 8945.40_f; // Compilation error!
float f3 = 8945_.40f; // Compilation error!