Java 7/8/9
workshops beyond legacy code
Kamil Lolo

Czego możecie się spodziewać?

- Przypomnienia rzadziej używanych elementów Javy 7
- Omówienia nowości z Javy 8 i ich przećwiczenia
- Wspomnienie najważniejszych nowości Javy 9
- Omówienia teorii i dużej ilości ćwiczeń
- Testy jednostkowe
- 7 minut Przerwy co 53 minuty

Dlaczego warto o tym mówić i to ćwiczyć?
Dlaczego Java 8?
- zwiększenie naszych kompetencji
- unikanie "boilerplate code"
- skupienie się na tym co ma robić nasz kod a nie jak
- pisanie mniejszej ilości kodu w celu osiągnięcia tego samego efektu
- efektywnie korzystanie z języka
- możliwość używania nowych wzorców
- Java 8 jest szybsza od poprzednich wersji out-of-box
- nowe narzędzia do rozwiązywania problemów
- minimalizacja ilości błędów
- żeby nie pisać kodu Javy 1.4 korzystając z kompilatora Javy 1.8
Java 7
...czyli zapomniane elementy języka

try-resources
Podczas używania JDBC można spotkać się z problemem połączeń do bazy danych, które zawsze trzeba pamiętać zamknąć na końcu w bloku
finally. Java 7 wprowadza mechanizm który pozwala wyeliminować ten boilerplate.
Connection con = null;
try {
con = ConnectionUtil.getDBConnection();
callProcedure("FOO_PROCEDURE", con);
}
catch (SQLException e) {
e.printStackTrace();
}
finally {
if(con !=null) {
try {
con.close();
}
catch(Exception e) {
}
}
}
try-resources
Podczas używania JDBC można spotkać się z problemem połączeń do bazy danych, które zawsze trzeba pamiętać zamknąć na końcu w bloku
finally. Java 7 wprowadza mechanizm który pozwala wyeliminować ten boilerplate.
Connection con = null;
try {
con = ConnectionUtil.getDBConnection();
callProcedure("FOO_PROCEDURE", con);
}
catch (SQLException e) {
e.printStackTrace();
}
finally {
if(con !=null) {
try {
con.close();
}
catch(Exception e) {
}
}
}
try (final Connection con = ConnectionUtil.getDBConnection()) {
callProcedure("FOO_PROCEDURE", con);
}
catch (SQLException e) {
e.printStackTrace();
}
try-resources
mechanizm pozwala definiować dowolną liczbę zmiennych które mają zostać zamknięte. Oddziela się je średnikiem. Jeżeli metoda close ma throws to musimy wyjątek złapać i obsłużyć.
static class ClosableUnicorn implements AutoCloseable {
void magic() {
System.out.println("magic");
}
@Override
public void close() throws IOException {
System.out.println("on close");
}
}
public static void main(final String... args) {
try (final ClosableUnicorn unicorn = new ClosableUnicorn()) {
unicorn.magic();
}
catch (IOException e) {
e.printStackTrace();
}
}
static String readFirstLineFromFile(String path) throws IOException {
try (BufferedReader br =
new BufferedReader(new FileReader(path))) {
return br.readLine();
}
}
try-resources in Java 9
Java 9 wprowadza możliwość wykorzystywania zmiennych w bloku try-resources które są effective final.
void foo() throws IOException {
BufferedReader br = new BufferedReader(new FileReader(""));
try (br) {
br.readLine();
//br = null; ERROR
}
//br = null; ERROR
}
void doSomethingWith(Connection connection) throws Exception {
try(connection) {
connection.doSomething();
}
}
abstract method in enum
W enumach można definiować metody abstrakcyjne. (od jakiej wersji?)
W jaki sposób taką metodę można zaimplementować?
enum Employe {
JUNIOR,
REGULAR,
CHEF;
public abstract long getSalary();
}

abstract method in enum
enum Employe {
JUNIOR(500) {
public float getSalary() {
return baseSalary;
}
},
REGULAR(1000) {
public float getSalary() {
return baseSalary + (REGULAR_BONUS * baseSalary);
}
},
CHEF(2000) {
public float getSalary() {
return baseSalary + (CHEF_BONUS * baseSalary);
}
};
final static float REGULAR_BONUS = 0.3f;
final static float CHEF_BONUS = 0.5f;
final long baseSalary;
Employe(final long baseSalary) {
this.baseSalary = baseSalary;
}
public abstract float getSalary();
}
underscore in numbers
Java 7 wprowadza możliwość używania podkreśleń podczas wpisywania literałów liczb. Jednak od Javy 9 nie będzie można używać podkreślenia jako nazwy zmiennej.
long bigNumber = 1_000_000; // correct in Java 7+
long _ = 1000; // incorect in Java 9
diamond
Podczas deklaracji zmiennych generycznych przed Java 7 obowiązkowe było podanie po obu stronach znaku równa się typu jakiego ma być zmienna. Informacja ta jednak może być wywnioskowana na podstawie kontekstu przez kompilator i już nie jesteśmy zmuszeni do wpisywania tej nadmiarowej informacji, wystarczy wpisać "<>". Pominięcie operatora diamentu spowoduje że kompilator uzna zmienną za surowego typu i nie będziemy mogli skorzystać z dobrodziejstw generyków.
List<String> names = new ArrayList<String>();
Map<String, List<Integer>> map = new HashMap<String, List<Integer>>();
// można zastosować skrócony zapis
Map<String, List<Integer>> map = new HashMap<>();
multi-catch
Często w metodach realizujących logikę biznesową dochodzi do sytuacji gdzie nasz kod może wyrzucić kilka różnych wyjątków, a my je procesujemy w taki sam sposób. Wymaga to od nas napisania kilku bloków catch. Java 7 wprowadziła mechanizm który potrafi nam tutaj ułatwić życie.
try {
foo();
}
catch(final IOException e) {
processError(e);
}
catch(final MethodNotFoundException e) {
processError(e);
}
catch(final IllegalArgumentException e) {
processError(e);
}
multi-catch
Często w metodach realizujących logikę biznesową dochodzi do sytuacji gdzie nasz kod może wyrzucić kilka różnych wyjątków, a my je procesujemy w taki sam sposób. Wymaga to od nas napisania kilku bloków catch. Java 7 wprowadziła mechanizm który potrafi nam tutaj ułatwić życie.
try {
foo();
}
catch(final IOException e) {
processError(e);
}
catch(final MethodNotFoundException e) {
processError(e);
}
catch(final IllegalArgumentException e) {
processError(e);
}
try {
foo();
}
catch(final IOException | MethodNotFoundException | IllegalArgumentException e) {
processError(e);
}
string in switch
Od Javy 7 można używać w wyrażeniach switch typów String.
public String getTypeOfDayWithSwitchStatement(String dayOfWeekArg) {
String typeOfDay;
switch (dayOfWeekArg) {
case "Monday":
typeOfDay = "Start of work week";
break;
case "Tuesday":
case "Wednesday":
case "Thursday":
typeOfDay = "Midweek";
break;
case "Friday":
typeOfDay = "End of work week";
break;
case "Saturday":
case "Sunday":
typeOfDay = "Weekend";
break;
default:
throw new IllegalArgumentException("Invalid day of the week: " + dayOfWeekArg);
}
return typeOfDay;
}
Java 8

...czyli wszystko co jest teraz sexy
Lambda
i functional interface
Single abstract method interface (SAM interface)
- na pewno używaliście jakiegoś interfejsu z tylko jedną metodą abstrakcyjną, przykładowo: Runnable, Callable, Comparator
- problem klas anonimowych
- boilerplate
- czy kompilator nie może domyślić się pewnych rzeczy na nas?
public static void main(String... args) {
List<String> names = Arrays.asList("Mateusz", "Zosia", "Ania");
names.sort(new Comparator<String>() {
@Override
public int compare(final String s1, final String s2) {
return s1.compareTo(s2);
}
});
}
public interface Comparator<T> {
int compare(T o1, T o2);
}
Nadmiarowe informacje w implementacji tej klasy
public static void main(String... args) {
List<String> names = Arrays.asList("Mateusz", "Zosia", "Ania");
names.sort(new Comparator<String>() {
@Override
public int compare(final String s1, final String s2) {
return s1.compareTo(s2);
}
});
}
public interface Comparator<T> {
int compare(T o1, T o2);
}
lista zawiera obiekty typu string
i tylko takie mogę tutaj sortować
dostaje dwa parametry typu T
zwracam int
jedyna ważna linijka
Co zostanie kiedy je usunę?
public static void main(String... args) {
List<String> names = Arrays.asList("Mateusz", "Zosia", "Ania");
names.sort(new Comparator<String>() {
@Override
public int compare(final String s1, final String s2) {
return s1.compareTo(s2);
}
});
names.sort(
// nasza pierwsza lambda!
(final String s1, final String s2) -> {
return s1.compareTo(s2);
}
);
}


Dozwolone formy wyrażenia lambda
names.sort((s1, s2) -> { return s1.compareTo(s2); });
names.sort((s1, s2) -> s1.compareTo(s2));
new Thread(() -> System.out.println("hello from another thread")).start();
foo( param -> System.out.print(param));
Runnable r = () -> System.out.println();
// Lambda to tylko implementacja interfejsu z jedna metodą abstrakcyjna
Callable<String> callable = new Callable<String>() {
@Override
public String call() throws Exception {
return "some string";
}
};
Callable<String> callable = () -> "some string";
Difference between Lambda Expression and Anonymous class
One key difference between using Anonymous class and Lambda expression is the use of this keyword. For anonymous class ‘this’ keyword resolves to anonymous class, whereas for lambda expression ‘this’ keyword resolves to enclosing class where lambda is written.
Lambda i "this"
public class Demo {
public static void main(final String... args) {
final Demo demo = new Demo();
}
Demo() {
new Thread(new Runnable() {
@Override
public void run() {
System.out.println(this); // com.comarch.training.Demo$1@69991479
}
}).start();
new Thread(() -> System.out.println(this)).start(); // demo
}
@Override
public String toString() {
return "demo";
}
}
Functional interface
@FunctionalInterface
public interface Callable<V> {
/**
* Computes a result, or throws an exception if unable to do so.
*
* @return computed result
* @throws Exception if unable to compute a result
*/
V call() throws Exception;
}
@FunctionalInterface
interface MyFI {
// ERROR: Multiple non-overriding abstract methods found in interface
void method1();
void method2();
}
Ograniczenia: Interfejs to interfejs
static abstract class F {
public abstract void foo();
}
public static void main(String... args) {
//Error: Target type of lambda conversion must be an interface
F f = () -> {};
}
Effective final
static String lastName = "";
public static void main(String... args) throws InterruptedException {
String userName = "";
Runnable r1 = () -> lastName = "Henio";
// ERROR: local variables referenced from a lambda
// expression must be final or effectively final
Runnable r2 = () -> userName = "Henio";
new Thread(r).start();
Thread.sleep(100);
System.out.println(lastName);
}
Debuggowanie

Pułapki Javy 8
final String httpRequestMethod = httpRequest.getMethod();
Response response = httpRequest.getBody()
.map(body -> requestBuilder.method(
httpRequestMethod,
Entity.entity(body,MediaType.APPLICATION_JSON_TYPE))
)
.orElse(requestBuilder.method(httpRequestMethod));
if (body.isPresent()) {
return requestBuilder.method(httpRequestMethod,Entity.entity(body.get(),MediaType.APPLICATION_JSON_TYPE)
} else {
return requestBuilder.method(httpRequestMethod)
}
- Proste rozwiązania można łatwo skomplikować
- Clean code mówi o maksymalnej liczbie lini w metodzi 30, co jeżeli teraz możemy zapisać w jednej linijce więcej rzeczy? Łatwiej złamać SRP
- Nie ćwicz nowości na systemie produkcyjnym
Referencje do metod
private static interface IMagicAlgorithm {
long calculate(long x);
}
private static long calculate(long x) {
return x * x;
}
public static void main(String... args) throws InterruptedException {
// obliczenia zawarte w lambda - slabe do utrzymania
IMagicAlgorithm algorithm1 = x -> x * x;
// wywolanie istenijacej metody - nadal nadmiarowy kod
IMagicAlgorithm algorithm2 = x -> calculate(x);
// referencja do metody!
IMagicAlgorithm algorithm3 = Demo::calculate;
}
-lambda tworzy anonimową metodę, co w przypadku kiedy mam już metodę i chcemy ją wykorzystać?
-wisienka na torcie lambd
Referencje do metody z klasy zewnętrznej
public class Demo {
@FunctionalInterface
private static interface IMagicAlgorithm {
long calculate(long x);
}
private long calculate(long x) {
return x * x;
}
private class Inner {
public void foo() {
// obliczenia zawarte w lambda - slabe do utrzymania
IMagicAlgorithm algorithm1 = x -> x * x;
// wywolanie istenijacej metody - nadal nadmiarowy kod
IMagicAlgorithm algorithm2 = x -> calculate(x);
// referencja do metody!
IMagicAlgorithm algorithm3 = Demo.this::calculate;
}
}
}
Możliwe wersje referencji do metod
- Klasa::metodaInstancji
- Klasa::metodaStatyczna
- obiekt::metodaInstancji
- Klasa::new
- typ[]::new
public void foo() {
final char[] numbers = {'A', 'b', 'C'};
final String result = Stream.of(numbers)
.map(String::valueOf) // metoda statyczna
.map(String::toLowerCase) // metoda instancji ( s-> s.toLowerCase() )
.map(this::appendeSeparator) // metoda obiektu
.map(WorkshopTest::trimmer) // metoda obiektu
.collect(Collectors.joining());
System.out.println(result);
}
private static String trimmer(final String input) {
return input.trim();
}
private String appendeSeparator(final String input) {
return input + "-";
}
Instead of using
AN ANONYMOUS CLASS
you can use
A LAMBDA EXPRESSION
And if this just calls one method, you can use
A METHOD REFERENCE
Zmiany w interfejsach
Default
- Od Javy 8 można w interfejsach implementować metody za pomocą słowa kluczowego default
- Mechanizm nie powinien być nadużywany. Może prowadzić do problemu diamentu znanego z wielodziedziczenia
- Default pomaga rozszerzać interfejsy bez łamania kompatybilności z istniejącym kodem
public interface Vehicle {
default void print() {
System.out.println("I am a vehicle!");
}
}
public interface FourWheeler {
default void print() {
System.out.println("I am a four wheeler!");
}
}
public class Car implements Vehicle, FourWheeler {
public void print() { //bez tej implementacji kompilator zglosi błąd
Vehicle.super.print();
}
}
Statyczne metody i pola
W interfejsach można również deklarować metody statyczne oraz pola które z domysłu są finalne i statyczne. Odwołujemy się do tego jak do pól, metod statycznych w klasach
public interface Vehicle {
String broomSound = "pffff";
static void broomBroom() {
broomSound =""; // error
System.out.println(broomSound); // dostep w metodzie statycznej
}
}
Execute around method
następca wzorca template method
definicja problemu
- wzorzec szablon metody wymaga od nas utworzenia klasy bazowej oraz osobnej klasy dla każdej implementacji. Co czasami jest przerostem formy na treścią
- w kodzie mamy wiele bardzo podobnych metod, przykładowo:
public Properties foo(final Properties input) throws Exception {
logger.info("start, with params: {}", input);
final Connection dbConnection = getDBConnection();
Properties result = new Properties();
try {
validateInput(input);
result = callDbProcedure(input, dbConnection);
if(result.getProperty("error") != null) {
throw new Exception(result.getProperty("error"));
}
} catch (Exception e) {
System.out.println(e.getMessage());
System.out.println("Rolling back...");
dbConnection.rollback();
} finally {
dbConnection.close();
}
logger.info("end");
return result;
}
public void shoulDoSomething(final Properties input ) throws Exception {
runInTransaction( connection-> {
validateInput(input);
return callDbProcedure(input, connection);
});
}
public Properties runInTransaction(final Function<Connection, Properties> callback) throws Exception {
logger.info("start, with params: {}", input);
final Connection dbConnection = getDBConnection();
Properties result = new Properties();
try {
result = callback.apply(dbConnection);
if(result.getProperty("error") != null) {
throw new IllegalStateException(result.getProperty("error"));
}
} catch (Exception e) {
System.out.println(e.getMessage());
System.out.println("Rolling back...");
dbConnection.rollback();
} finally {
dbConnection.close();
}
logger.info("end");
return result;
}
Functional programming
...czyli wszystko co jest teraz sexy

Deklaratywne programowanie vs imperatywne
Deklaratywne
Zestaw operacji jest nich przedstawiany bez ujawniania tego jak są zaimplementowane i jak wygląda przepływ danych. Mówimy co ma być zrobione a nie jak.
Imperatywne
Kod zmienia stan programu, korzystamy z zmiennych. Musimy określić nie tylko co ma być zrobione ale również jak.
users.forEach(System.out::print);
// nie interesuje mnie czy
// w metodzie jest for,
// for-each czy while
// mowie co ma być zrobione
for (int i = 0; i < users.size(); i++) {
System.out.println(users);
}
Co to jest programowanie funkcyjne?
Czym jest programowanie funkcyjne?
Programowanie gdzie najważniejszą rolę pełnią funkcje oraz abstrakcyjne ujęcie przepływu sterowania i operacji na danych za pomocą funkcji aby uniknąć efektów ubocznych i ograniczyć modyfikację stanu aplikacji.
Czy się charakteryzuje FP?
- Method as first citizen
- Pure functions
- Function composition
- Lazy evaluation

Method as first citizen
Co byście przekazali jako parametr takiej metody?
static class UserFromService {
private final String userName;
public UserFromService(final String userName) {
this.userName = userName;
}
public String getUserName() {
return userName;
}
}
public static void main(final String... args) {
final UserFromService userFromService = new UserFromService("Kamil");
final Properties data = new Properties();
// funkcja jak pobrac pole userName z obiektu userFromService
// i jeżeli nie jest null to wrzucić do Properties
// pod podanym kluczem.
setIfNotNull(????);
}
Method as first citizen
W językach funkcyjnych funkcje występują na tych samych zasadach co obiekty innych typów.
public static void main(final String... args) {
final UserFromService userFromService = new UserFromService("Kamil");
final Properties data = new Properties();
setIfNotNull(userFromService::getUserName, data::put, "firstName");
}
public static <T> void setIfNotNull( final Supplier<T> getter,
final BiConsumer<String, T> setter,
final String key ) {
final T t = getter.get();
if (null != t) {
setter.accept(key, t);
}
}
Funkcje w programowaniu funkcyjnym są obywatelami pierwszej klasy a co za tym idzie:
- Funkcje można przekazywać jako parametr do innych funkcji
- Funkcje można przypisywać do zmiennych jak obiekty klas.
- Funkcje mogą tworzyć funkcje i je zwracać
private static final Function<String, String> initUserName = (param) -> "userName2:" + param;
private static String getLassName(final Function<String, String> callback) {
final Function<String, String> afterFn = element -> element.toLowerCase();
return callback
.andThen(afterFn)
.apply(" Kowalski");
}
Pure functions
Inpure functions depend on some form of external state

Given the same input, the function always result in the same output
4 najważniejsze korzyści ze stosowanie czystych funkcji




Function composition
Składanie funkcji pozwala nam budować z funkcji, które pełnią rolę cegiełek, pełnowartościowe aplikacje.
public class Demo {
private static final BigDecimal USDToPLNRate = new BigDecimal("3.86");
private static final BigDecimal amountOfDollar1 = new BigDecimal("23");
private static final BigDecimal amountOfDollar2 = new BigDecimal("43");
@FunctionalInterface
private interface ExchangeRateConverter extends BiFunction<BigDecimal, BigDecimal, BigDecimal> {
default Function<BigDecimal, BigDecimal> curryRate(BigDecimal t) {
return u -> apply(t, u);
}
}
public static void main(final String... args) {
final Function<BigDecimal, BigDecimal> converter = ((ExchangeRateConverter) BigDecimal::multiply)
.curryRate(USDToPLNRate) // currying - rozwijanie funkcji
.andThen(Demo::afterConversion)
.compose(Demo::beforeConversion);
converter.apply(amountOfDollar1);
converter.apply(amountOfDollar2);
}
private static BigDecimal beforeConversion(BigDecimal amount) {
System.out.println("Kwota przed konwersja:" + amount);
return amount;
}
private static BigDecimal afterConversion(BigDecimal amount) {
System.out.println("Po konwersji:" + amount);
return amount;
}
}
Lazy evaluation
Kiedy wykona się kod z poprzedniego przykładu?
Najważniejsze interfejsy funkcyjne
Interfejsy funkcyjne dostępne w JDK
interfejs | parametry | zwraca | opis | metoda | inne metody |
---|---|---|---|---|---|
Runnable | - | void | uruchamia logikę bez parametrów i wyniku | run | - |
Supplier | - | T | Dostarcza wartość typu T | get | - |
Consumer | T | void | Pobiera wartość typu T | accept | andThen |
BiConsumer | T,U | void | Pobiera wartość typu T i U | accept | andThen |
Function | T | R | Funkcja z parametrem typu T | apply | compose, andThen, identity |
BiFunction | T,U | R | Funkcja z parametrem typu T i U | apply | andThen |
UnaryOperator | T |
T | Operator jednoargumentowy dla typu T | apply | compose, andThen, identity |
BinaryOperator | T,T | T | Operator dwuargumentowy dla typu T | apply | andThen, maxBy, minBy |
Predicate | T | boolean | Funkcja zwracająca wartość logiczną | test | and, or, negate, isEqual |
BiPredicate | T,U | boolean | Dwuargumentowa funkcja zwracająca wartość logiczną | test | and, or, negate |
Stream API
jako rodzaj IoC

Czym jest strumień?
- sekwencja elementów danego typu na których wykonujemy operacje
- przetwarza dane na żądanie (lazy), nie przechowuje danych
- pozwala agregować wynik operacji
- operacje mogą być łączone w łańcuch
- pozwala na odwrócenie kontroli
- inspirowany konstrukcjami używanymi w językach funkcyjnych
Jak utworzyć strumień?
// 1
final int[] data = {1, 2, 3};
Arrays.stream(data).forEach(System.out::print);
// 2
final List<String> strings = Arrays.asList("a","b");
strings.stream().forEach(System.out::print);
// 3
Stream.of("a","b").forEach(System.out::print);
// 4
Stream.generate(strings.iterator()::next)
.limit(strings.size())
.forEach(System.out::print);
// 5
IntStream.range(0,3).forEach(System.out::print);
// 6
Stream.empty().forEach(System.out::print);
// 7
Stream<String> streamBuilder =
Stream.<String>builder().add("a").add("b").add("c").build();
Strumienie typów prostych
// tworzy strumień intów
IntStream intStream = IntStream.range(1, 3);
// strumień intów łącznie z liczbami krańcowymi
LongStream longStream = LongStream.rangeClosed(1, 3);
// Tworzy strumień DoubleStream
Random random = new Random();
DoubleStream doubleStream = random.doubles(3);
// dodatkowe metody arytmetyczne
int[] numbers = {2,4,23,21};
Arrays.stream(numbers).average().ifPresent(System.out::println); // 12.5
- Strumienie są generyczne, stąd też nie mogą przechowywać typów prostych, lecz ich otoczki
- Java posiada wbudowane strumienie opakowujące typów prostych w ich wrappery.
- Strumienie te są dużo bardziej wydajne niż używanie strumienia z wrapperami
- Obsługiwane są 3 typu takich strumieni: IntStream, LongStream, DoubleStream
- strumienie typów prostych mają również wbudowane operacje arytmetyczne np. sum, max, min, average etc.
Operacje terminujące i natychmiastowe
Wszystkie operacje jakie możemy wykonywać na strumieniu dzielą się na natychmiastowe zwracają one referencje do strumienia i nie wykonują żadnej logiki. Dzięki czemu strumienie są leniwe. Drugi rodzaj operacji jaki możemy wykonać na strumieniu to operacja terminująca strumień która zwraca najczęściej jakiś obiekt wynikowy i wymusza wykonanie wszystkich operacji w strumieniu.
// 1
holdings
.stream() // utworzenie strumienia
.sorted() // operacja natychmiastowa
.distinct() // operacja natychmiastowa
.count(); // operacja terminująca
// 2
final List<String> names = Arrays.asList("Anna", "Aneta", "Zosia", "Henia");
final Stream<String> userStream = names.stream();
userStream.forEach(System.out::println);
//java.lang.IllegalStateException: stream has already been operated upon or close
userStream.forEach(System.out::println);
interfejs Stream

forEach/forEachOrdered
- wykonuje podaną operacje (Consumer) na każdym elemencie strumienia
- operacja terminująca
- nie ma gwarancji zachowanie kolejności w wielowątkowym strumieniu
- metoda forEachOrdered działa analogicznie, jednak zachowuje kolejność przy wielowątkowym przetwarzaniu
final String[] balances = {"0,01", "19.99", "100 "};
// z wykorzystanie referncji do metody
Arrays.stream(balances).forEach(System.out::println);
// z wykorzystaniem wyrazenia Lambda
Arrays.stream(balances).forEach(current -> System.out.println(current) );
map
- Metoda konwertuje obiekt z jednego typu do innego.
- Ilość elementów w strumieniu po takiej operacji jest taka sama, jednak obiekty zostają zamienione na inny typ.
- Operacja natychmiastowa
- Jako parametr przyjmuje obiekt typu Function
final String[] balances = {"0,01", "19.99", "100 "};
Arrays.stream(balances)
.map(String::trim) // .map( current -> current.trim() )
.map(balance -> balance.replace(",", "."))
.map(BigDecimal::new) // .map( current -> new BigDecimal(current) )
.forEach(System.out::println);
<R> Stream<R> map(Function<? super T, ? extends R> mapper)
mapToInt/Long/Double
- Zamienia strumień Obiektów na strumień prymitywów
- Pozwala uniknąć nie potrzebnego boxingu
- Używamy wszędzie tam gdzie robimy mapowanie na typ prosty
final Human[] people = {new Human(18), new Human(25)};
Arrays.stream(people)
// .map(Human::getAge) uzycie tego spowoduje zbedny boxing to Integer!!
.mapToInt(Human::getAge)
.forEach(System.out::println);
class Human {
int age;
Human(int age) {
this.age = age;
}
public int getAge() {
return age;
}
}
flatMap
- zamienia każdy element strumienia na zero lub więcej elementów (dokładnie rzecz ujmując na strumień obiektów)
- operacja natychmiastowa
- dostępne również takie metody jak: flatMapToInteger, flatMapToDouble, flatMapToLong
- przyjmuje na wejściu: Stream<R> flatMap(Function<T, Stream<R> mapper)
class Book {
private String name;
public Book(final String name) {
this.name = name;
}
// getter/setter
}
class Library {
private String name;
private List<Book> books;
public Library(
final String name,
final List<Book> books ) {
this.name = name;
this.books = books;
}
// getter/setter
}
final List<Library> libraries = Arrays.asList(
new Library(
"publiczna",
Arrays.asList(new Book("Java for dummies"))
),
new Library(
"uniwersystecka",
Arrays.asList(new Book("Thinking in Java"))
)
);
libraries.stream()
.flatMap(library -> library.books.stream())
.forEach(System.out::println);
filter
- Pozwala odfiltrować elementy strumienia. Usuwając z nich te dla których wywołanie predicate ma wartość false.
- Pierwotna kolekcja nie ulega zmianie.
- Operacja natychmiastowa
final List<String> names = Arrays.asList("Anna", "Aneta", "Zosia", "Henia");
names.stream()
.filter(name -> name.startsWith("A"))
.forEach(System.out::println); // operacja terminująca
Stream<T> filter(Predicate<? super T> predicate)
distinct
- operacja natychmiastowa
- zwraca strumień z którego usunięto duplikaty
- wykorzystuje metodę equals do porównania obiektów
- mało wydajny w wielowątkowym przetwarzaniu. Lepiej przełaczyć się na jeden wątek za pomocą metody sequential
final List<Library> libraries = Arrays.asList(
new Library("publiczna",
Arrays.asList(
new Book("Java for dummies"),
new Book("Thinking in Java")
)
),
new Library("uniwersystecka",
Arrays.asList(
new Book("Thinking in Java"))
)
);
libraries.stream()
.flatMap(
library -> library.books.stream()
)
.distinct()
.forEach(System.out::println);
class Book {
private String name;
public Book(final String name) {
this.name = name;
}
@Override
public boolean equals(final Object o) {
if (this == o) return true;
if (o == null ||
getClass() != o.getClass())
return false;
final Book book = (Book) o;
return name != null ?
name.equals(book.name)
: book.name == null;
}
@Override
public int hashCode() {
return name != null ? name.hashCode() : 0;
}
}
peek
- wykonuje na każdym elemencie podaną logikę
- jest operacją natychmiastową
- jeżeli korzystamy z wielowątkowości należy zapewnić synchronizację
- ta metoda powstała z myślą o tym żeby ułatwić debuggowanie strumieni
- przyjmuje jako parametr obiekt typu Consumer
Stream
.of("one", "two", "three", "four")
.filter(e -> e.length() > 3)
.peek(e -> System.out.println("Filtered value: " + e))
.map(String::toUpperCase)
.peek(e -> System.out.println("Mapped value: " + e))
.collect(Collectors.toList());
sorted
- sortuje strumień zgodnie z naturalnym porządkiem, elementy strumienia muszą implementować Comperable
- w przypadku kiedy klasa nie implementuje Comperable możemy jako parametr przekazać Comparator
Arrays.asList("Hania","Marian", "Stefan", "Amadeusz", "Franek")
.stream()
.sorted()
.sorted(Comparator.reverseOrder())
.sorted((o1, o2) -> o1.compareTo(o2))
.forEach(System.out::println);
limit
- ogranicza liczbę elementów strumienia do podanej liczby
- operacja natychmiastowa
- dla strumienia wielowątkowego operacja jest kosztowna
- jeżeli chcemy szybszego działania wielowątkowego to musimy wyłączyć ograniczenie trzymania kolejności metodą: unordered
- przydatne w nieskończonych strumieniach
final Random random = new Random();
IntStream
.generate(random::nextInt)
.limit(3)
.forEach(System.out::println);
skip
- pomija pierwsze n elementów strumienia
- jeżeli elementów do pominięcia jest mniej niż elementów strumienia to zwracany jest pusty strumień
- operacja tania na sekwencyjnym strumieniu
- w wielowątkowych strumieniach należy użyć metody generate bądź wyłączyć constraint na kolejność obiektów (unordered)
String[] countries = {"poland", "russia", "france" , "germany"};
Arrays.stream(countries)
.skip(1)
.forEach(System.out::println);
collect
Jest to metoda terminująca pozwalająca przepakować dane ze strumienia w jakiś obiekt docelowy używając przy tym dodatkowej logiki. Na wejściu przyjmuje obiekt klasy Collector. Zbiór predefiniowanych Collectorów znajduje się w klasie Collectors.
toList() | Zamienia dane ze strumienia w liste |
---|---|
toSet() | Zamienia dane ze strumienia w set |
toCollection() | Pozwala zamienić dane na kolekcje danego typu. Jako parametr podajemy referencje do kontruktora |
toMap() | Pozwala zmapować strumień do mapy. Wymaga podania funkcji mapującej klucz (najczęściej wykorzystywana to Function.identity()) oraz mapującej wartość |
joining() | Pozwala na łączenie Stringów. Można podać separator, prefix, postfix |
counting() | Zlicza elementy jako long |
summarizingDouble()/Long/Int | Dostarcza takich statystyk jak: średnia, min, max, liczba, suma. Do każdych w tych danych są również osobne metody. |
groupingBy() | Grupuje dane do listy |
partitioningBy() | Grupuje dane po podanym predykacie |
collect - przykłady
private Stream<String> getFilteredCountryStream(final String[] countries) {
final Predicate<String> isEnoughtLenght = country -> country.length() > 6;
return Arrays.stream(countries).filter(isEnoughtLenght);
}
final String[] countries = {"Polska", "Malta", "Afganistan", "Turkmenistan"};
// 1
final List<String> filteredCountries1 = getFilteredCountryStream(countries).collect(toList());
System.out.println(filteredCountries1.getClass());// ArrayList
// 2
final List<String> filteredCountries2 = getFilteredCountryStream(countries)
.collect(toCollection(LinkedList::new));
// 3
final Set<String> countriesSet = getFilteredCountryStream(countries).collect(toSet());
// 4
final Map<String, String> counrtyMap = getFilteredCountryStream(countries)
.collect(toMap(Function.identity(), String::toUpperCase));
// 5
final String counrtyStr = getFilteredCountryStream(countries).collect(joining());
System.out.println(counrtyStr); // AfganistanTurkmenistan
// 6
final String counrtyStr2 = getFilteredCountryStream(countries).collect(joining(","));
System.out.println(counrtyStr2); // Afganistan,Turkmenistan
// 7
final String counrtyStr3 = getFilteredCountryStream(countries).collect(joining(",","To sa kraje:","."));
System.out.println(counrtyStr3); // To sa kraje:Afganistan,Turkmenistan.
reduce
- operacja redukcji łączy wszystkie elementy strumienia w jeden wyjściowy obiekt
- możliwe wersji operacji:
- z parametrem BiFunction (funkcja z dwoma parametrami)
- z dodatkowym parameterem początkowym
- z parametrem BinaryOperator - combiner używany w wielowątkowym przetwarzaniu, do scalania wyników. Nie używany przez sekwencyjny strumień.
myStream.reduce("", (s1, s2) -> s1 + s2); // brak kompilacji do StringBuilder
myStream.collect(Collectors.joining()); // korzysta z StringBuildera
myStream.collect(
StringBuilder::new,
StringBuilder::append,
StringBuilder::append
);
reduce vs collect
- obie metody na pierwszy rzut oka robią to samo
- służą do zamiany elementów strumienia w jeden obiekt wyjściowy
- reduce przeznaczony dla obiektów nie mutowalnych, redukcja dla każdego elementu strumienia tworzy nowy obiekt wyjściowy
- collect redukuje obiekty mutowalne, obiekt wyjściowy tworzony jest jednorazowo
groupingBy
Metoda pozwala wykonać operację grupowania elementów strumienia do mapy podobną do GROUP BY z SQL za pomocą funkcji klasyfikującej. Dodatkowo metoda występuje w wersji wielowątkowej: groupingByConcurrent
+----------+------------+-----------------+
| Name | City | Number of Sales |
+----------+------------+-----------------+
| Alice | London | 200 |
| Bob | London | 150 |
| Charles | New York | 160 |
| Dorothy | Hong Kong | 190 |
+----------+------------+-----------------+
// kod imperatywny:
Map<String, List<Employee>> result = new HashMap<>();
for (Employee e : employees) {
String city = e.getCity();
List<Employee> empsInCity = result.get(city);
if (empsInCity == null) {
empsInCity = new ArrayList<>();
result.put(city, empsInCity);
}
empsInCity.add(e);
}
// kod deklaratywny
Map<String, List<Employee>> employeesByCity = employees.stream().collect(groupingBy(Employee::getCity));
groupingBy
Metoda GroupingBy jest dostepna w 3 wersjach:
- z funkcją klasyfikującą
- z funkcją klasyfikującą i Collectorem
- z funkcją klasyfikującą, Collectorem, z metodą Supplier zwracającą mapę
// 1. Mapa z setem postow gdzie kluczem jest typ
Map<BlogPostType, Set<BlogPost>> postsPerType
= posts.stream().collect(groupingBy(BlogPost::getType, toSet()));
// 2. mapa postow danego autora gdzie posty sa jako mapa per typ
Map<String, Map<BlogPostType, List>> map
= posts.stream().collect(groupingBy(BlogPost::getAuthor, groupingBy(BlogPost::getType)));
// 3. mapa post gdzie wartoscia jest suma polubien
Map<BlogPostType, Integer> likesPerType
= posts.stream().collect(groupingBy(BlogPost::getType, summingInt(BlogPost::getLikes)));
// 4. Uzycie metody joining
Map<BlogPostType, String> postsPerType = posts.stream()
.collect(groupingBy(BlogPost::getType, mapping(BlogPost::getTitle, joining(", ", "Post titles: [", "]"))));
partycjonowanie
- pozwala rozdzielić elementy strumienia do dwóch grup na podstawie przekazanego predykata
- jako drugi parametr można podać Collector za pomocą które złączymy elementy tych dwóch grup
// 1
Map<Boolean, List<Person>> adult = persons
.stream()
.collect(
partitioningBy(
person -> person.getAge() > 18
)
);
// 2
Map<Boolean, Set<String>> adult = persons
.stream()
.collect(
partitioningBy(
person -> person.getAge() > 18,
mapping(Person::getName, toSet())
)
);
min/max
- znajduje w strumieniu najmniejszy/największy element korzystając z przekazanego komparatora
String[] countries = {"poland", "Albania", "france", "germany"};
Arrays.stream(countries)
.skip(1)
.min(String::compareTo) // Albania
.ifPresent(System.out::println); // prawidlowe uzycia optional
anyMatch/allMatch/noneMatch
- metody przyjmują Predicate jako parametr i sprawdzają kolejno czy jakikolwiek element strumienia pasuje do niego, czy wszystkie pasują oraz czy żaden nie pasuje
String[] countries = {"poland", "Albania", "france", "germany"};
Arrays.stream(countries)
.skip(1)
.anyMatch(item -> item.startsWith("Z")); // false
Parallel stream
- Java pozwala przetwarzać strumienie w wielu wątkach jednocześnie
- korzysta z ForkJoinPool
- pozwala szybciej procesować duże zbiory danych
- należy uważać na wydajność przy niektórych operacjach
- z zwykłego strumienia przechodzimy do równoleglego za pomocą metody parallel, wracamy o sekwencyjnego za pomocą sequential
- tam gdzie dostępne są metody do tworzenia strumienia (stream) są również metody do tworzenia strumienia wielowątkowego.
final String[] countries = {"Polska", "Malta", "Afganistan", "Turkmenistan"};
final Predicate<String> isEnoughtLenght = country -> country.length() > 6;
Arrays.stream(countries)
.parallel()
.peek(it -> System.out.println(it + " w watku: " + Thread.currentThread().getName()))
.filter(isEnoughtLenght)
.count();
-----------------------------------------------------------
Wypisze:
Polska w watku: ForkJoinPool.commonPool-worker-3
Turkmenistan w watku: ForkJoinPool.commonPool-worker-2
Malta w watku: ForkJoinPool.commonPool-worker-1
Afganistan w watku: main
ForkJoinPool
- domyślnie wykorzystywany jest ForkJoinPool który jest wspólny dla całego jvm
- domyślnie ForkJoinPool tworzy tyle wątków ile jest procesów na maszynie
- wprowadzony w Java 7
- ForkJoinPool.commonPool()
- zmiana liczby wątków poprzez parametr:
-Djava.util.concurrent.ForkJoinPool.common.parallelism=5
int count = 0;
public void bu() {
IntStream.range(0, 10_000)
.parallel()
.forEach(i -> count++);
System.out.println("\n count = " + count); // 7433, 9195
}
AtomicInteger count = new AtomicInteger(0);
@Test
public void bu() {
IntStream.range(0, 10_000)
.parallel()
.forEach(i -> count.incrementAndGet());
System.out.println("\n count = " + count); // 10000
}
Czytelność strumieni
Co robi ten kod legacy?
Czy dostaniemy ArrayIndexOutOfBoundsException?
public static String fooWithoutStream() {
final List<String> result = new LinkedList<>();
String nameList = "";
for (int i = 0; i < holdings.size(); i++) {
for (int j = 0; j < holdings.get(i).getCompanies().size(); ++i) {
for (int k = 0; k < holdings.get(i).getCompanies().get(j).getUsers().size(); k++) {
User user = holdings.get(i).getCompanies().get(j).getUsers().get(k);
if (!result.contains(user.getFirstName())) {
result.add(user.getFirstName());
}
}
}
}
result.sort(new Comparator<String>() {
@Override
public int compare(final String o1, final String o2) {
return o1.compareTo(o2);
}
});
for (String name : result) {
nameList += name + " ";
}
return nameList.trim();
}
Czytelność strumieni
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Morbi nec metus justo. Aliquam erat volutpat.
public static String fooWithStream() {
return holdings.stream()
.flatMap(holding -> holding.getCompanies().stream())
.flatMap(company -> company.getUsers().stream())
.map(User::getFirstName)
.distinct()
.sorted()
.collect(Collectors.joining(" "));
}
public static String fooWithoutStream() {
final List<String> result = new LinkedList<>();
String nameList = "";
for (int i = 0; i < holdings.size(); i++) {
for (int j = 0; j < holdings.get(i).getCompanies().size(); ++i /** blad */) {
for (int k = 0; k < holdings.get(i).getCompanies().get(j).getUsers().size(); k++) {
User user = holdings.get(i).getCompanies().get(j).getUsers().get(k);
if (!result.contains(user.getFirstName())) {
result.add(user.getFirstName());
}
}
}
}
result.sort(new Comparator<String>() {
@Override
public int compare(final String o1, final String o2) {
return o1.compareTo(o2);
}
});
for (String name : result) {
nameList += name + " ";
}
return nameList.trim();
}
Kiedy nie używać strumieni?
Kiedy nie używać strumieni?
- w krytycznych wydajnościowo miejscach gdzie parallel Stream nie sprawdzi się
- kiedy strumień to przerost formy nad treścią
- kiedy nasz kod nie działa do końca i trzeba go dokładnie debugować
List<Integer> list = Arrays.asList(1, 2, 3);
// Old school
for (Integer i : list)
for (int j = 0; j < i; j++)
System.out.println(i * j);
// "Modern"
list.forEach(i -> {
IntStream.range(0, i).forEach(j -> {
System.out.println(i * j);
});
});
Optional

Optional jako sposób na null pointer
Optional jest to klasa z pakietu java.util, która przechowuję jakąś wartość w środku. Tą wartością może być null, stąd też Optional z założenia ma pomagać unikać błędów typu NullPointerException.
Klasa jest generyczna, stąd też może przechowywać dowolnego typu zmienną.

Gdzie NIE używać Optionali?
- w polach klas
- w kolekcjach
- jako parametr metod
- w serializowanych polach
// 1
Map<String, Optional<List<Optional<User>>>> usersInCompany; // łot !?
// 2
class User {
private final String firstName;
User(final String firstName) {
this.firstName = firstName;
}
public Optional<String> getFirstName() {
return Optional.of(this.firstName); // OK!
}
}
Jak nie używać Optional?
// 1
Optional<String> optString = Optional.of(x);
// 2
if (optString.isPresent()) {
foo(optString.get());
}
// 3
optString.ifPresent(this::foo);
Metody klasy Optional
static Optional empty() | Metoda fabrykująca pustego optionala. |
---|---|
static Optional of(T value) | Metoda fabrykująca Optionala z podaną wartością. Jeżeli przekazany obiekt jest nullem to otrzymamy nullpointer. |
static Optional ofNullable(T value) | Jak wyżej, ale bez null pointera |
Optional(T value) | Konstruktor również wymaga żeby parametr nie był nullem |
T get() | Pobranie wartości przechowywanej przez Optional |
boolean isPresent() | Sprawdzenie czy obiekt w Optional ma wartośc null |
void ifPresent(Consumer consumer) | Wykonanie logiki w przypadku kiedy w Optional znajdują się dane. |
filter, map, flatMap | Metody analogiczne jak w stream |
<X extends Throwable> T orElseThrow(Supplier<? extends X> exceptionSupplier) | Wyrzuca wyjątek jeżeli pole w Optional ma wartość null |
T orElseGet(Supplier other) | Pobiera domyślną wartość z przekazanej metody |
T orElse(T other) | Przekazujemy domyślną wartość która zostanie zwrócona w przypadku kiedy Optional nie przechowuje danych |
Optional w akcji
public char firstChar(String s) {
if (s != null && !s.isEmpty())
return s.charAt(0);
else
throw new IllegalArgumentException();
}
Optional.ofNullable(s)
.filter(s -> !s.isEmpty())
.map(s -> s.charAt(0))
.orElseThrow(IllegalArgumentException::new);
Optional w akcji
private String foo(final Holding h) {
if (h != null) {
final Company c = h.getCompanies().get(0);
if (c != null && c.getUsers() != null) {
final User u = c.getUsers().get(0);
if (u != null && u.getFirstName() != null) {
final String result = u.getFirstName();
if (result.length() > 0) {
return result;
}
}
}
}
return "404-not-found";
}
Co robi ta funkcja?
Optional w akcji
private String getCompanyFirstUserName1(final Holding holding) {
if (holding != null) {
final Company company = holding.getCompanies().get(0);
if (company != null && company.getUsers() != null) {
final User user = company.getUsers().get(0);
if (user != null && user.getFirstName() != null) {
final String result = user.getFirstName();
if (result.length() > 0) {
return result;
}
}
}
}
return "not found";
}
private String getCompanyFirstUserName2(final Holding holding) {
return Optional.ofNullable(holding)
.map(Holding::getCompanies)
.map(Vector::firstElement)
.map(Company::getUsers)
.map(Vector::firstElement)
.map(User::getFirstName)
.filter(name -> name.length() > 0)
.orElse("not found");
}
Nashorn

...pomówmy chwilę o javascript
Czym jest Nashorn?
- Nashorn jest silnikiem js (podobnie jak V8 znany z Node.js ) który rozszerza możliwości Javy pod kątem uruchomienia kodu js przez jvm.
- Java posiada REPL (ang. read-eval-print loop) dla tego silnika o nazwie jjs dostępny w JDK
- Kod Javy można wywoływać z poziomu js oraz w drugą stronę
- Nashorn wspiera ECMAScript 5.1

Wywoływanie funkcji js w Java
Nashorn pozwala wywoływać funkcje js z poziomu kodu Javy. Do takich funkcji można nawet przekazać obiekty Javy jako parametry.
var fun1 = function(name) {
print('Hi there from Javascript, ' + name);
return "greetings from javascript";
};
var fun2 = function (object) {
print("JS Class Definition: " +
Object.prototype.toString.call(object));
};
ScriptEngine engine = new ScriptEngineManager().getEngineByName("nashorn");
engine.eval(new FileReader("script.js"));
Invocable invocable = (Invocable) engine;
Object result = invocable.invokeFunction("fun1", "Peter Parker");
System.out.println(result);
System.out.println(result.getClass());
// Hi there from Javascript, Peter Parker
// greetings from javascript
// class java.lang.String
Wywoływanie funkcji Java w js
Z poziomu kodu js możemy wywoływac kod javy. Do klas Javy odwołujemy się wtedy poprzez: Java.type.
var MyJavaClass = Java.type('my.package.MyJavaClass');
var result = MyJavaClass.fun1('John Doe');
print(result);
// Hi there from Java, John Doe
// greetings from java
static String fun1(String name) {
System.out.format("Hi there from Java, %s", name);
return "greetings from java";
}
Trochę mniejsze nowości
Zmiany w Date API


Java 9

...o tym co nas czeka za chwilę
Czy Java 9 jest gotowa
do używania na produkcji?
Not yet.
- problemy z Maven/Gradle
- dobre wsparcie IDE
- masa problemów na które jeszcze nie ma odpowiedzi w sieci
jshell
moduły
zmiany w interfejsach
Nowe metody w klasie Optional
Java 9 uzupełnia Optional o kilka przydatnych metod
List<String> userPermits = ???
List<String> defaultPermit = Arrays.asList("transfer","login","history");
// 1
Optional
.ofNullable(users)
.stream() // NOWOŚĆ!
.forEach(this::activatePermitOnDatabase)
// 2 - wykorzystanie domyslnej wartosci
Optional
.ofNullable(users)
.orElse(Arrays.asList("hania","ela"))
.stream() // stream z klasy List
.forEach(this::activatePermitOnDatabase)
public Stream<T> stream()
public void ifPresentOrElse(Consumer<? super T> action, Runnable emptyAction);
public Optional<T> or(Supplier<? extends Optional<? extends T>> supplier)
Ćwiczenia

wejdźmy w świat streamów
Zasady obowiązujące podczas ćwiczeń
- NIE używamy pętli, if-ów, switchy
- NIE używamy zmiennych, tylko stałe
- NIE wysyłamy kodu innym osobom, jeżeli chcemy komuś pomóc to siadamy obok i jesteśmy jego driverem (nie dotykamy klawiatury)
Bank system
:fill(transparent,1)/about/bank-vault-door-ajar-digital-10185347-5748d1015f9b58516518ae95.jpg)
in Java 8
KATA

programistyczne
Linkografia
- grafiki pochodzą z https://static.pexels.com
- przystępne materiały z Javy 8: http://winterbe.com/
- opis programowania funkcyjnego: http://wazniak.mimuw.edu.pl/index.php?title=Programowanie_funkcyjne/Wstęp
- http://www.deadcoderising.com/2017-06-13-why-pure-functions-4-benefits-to-embrace-2/
- http://2.bp.blogspot.com/-Mgh0WScPjW8/Td-8b0ha7UI/AAAAAAAAADg/rs9FJhh2jIY/s1600/karate_01.jpg
- https://fthmb.tqn.com/dApe-GPqGqxCLoha4Mi-lIjoODo=/4728x3549/filters:no_upscale():fill(transparent,1)/about/bank-vault-door-ajar-digital-10185347-5748d1015f9b58516518ae95.jpg
Get a Taste of Lambdas and Get Addicted to Streams
https://www.youtube.com/watch?v=1OpAgZvYXLQ
Design Patterns in the Light of Lambda Expressions
https://www.youtube.com/watch?v=e4MT_OguDKg
Refactoring to Functional Style with Java 8
https://www.youtube.com/watch?v=wjF1WqGhoQI&t=3131s
Przydatne linki video
Koniec
Czas na podsumowanie zdobytej wiedzy i wasze opinie.
Java 7/8/9
By Kamil Lolo
Java 7/8/9
- 1,893