*7
.HotSpot – maszyna wirtualna Javy, dostarczana przez firmę Oracle Corporation razem z pakietem Java Runtime Environment. Wykorzystuje między innymi takie technologie jak kompilacja w locie oraz optymalizacja adaptacyjna. Maszyna wirtualna HotSpot napisana jest w języku C++. Na stronie internetowej projektu OpenJDK znajduje się informacja, że kod źródłowy projektu to ok. 250 tys. linii kodu. Na program składają się m.in.:
JIT (just-in-time compilation) to metoda wykonywania programów polegająca na kompilacji do kodu maszynowego przed wykonaniem danego fragmentu kodu.
Cała procedura wygląda następująco: kod źródłowy jest kompilowany do kodu pośredniego (bajtowego), program jest rozpowszechniany w postaci kodu pośredniego, na maszynie, na której program zostaje uruchomiony, maszyna wirtualna przeprowadza kompilację kodu pośredniego do kodu maszynowego. Kompilacja może się odbywać w momencie pierwszego dostępu do kodu znajdującego się w pliku lub pierwszego wywołania funkcji (stąd nazwa just-in-time).
Kompilacja JiT pozwala na uruchomienie aplikacji Javy nawet gdy kod wygenerowany przez kompilator nie jest jeszcze zoptymalizowany. Teoretycznie JiT zaczyna działać jak tylko dana metoda jest wywołana i kompiluje kod bajtowy tej metody na odpowiedni kod binarny "just in time to execute").
Po kompilacji JVM uruchamia kod metody bezpośrednio, zamiast ją interpretować, co przyspiesza działanie aplikacji. Jednak, na początku działania czasem zachodzić konieczność kompilacji tysięcy metod, co może spowodować wolny start JVM. Wniosek: JVM bez JiT startuje szybko, ale działa wolniej; JVM z JiT startuje wolniej ale działa szybciej.
Poniższy diagram ilustruje różne poziomy JVM i ich współzależność.
new
. Z
drugiej strony dealokacja pamięci odbywa sie automatycznie (garbage collector).
Można manualnie wywołać funkcję System.gc();
, co sugeruje, że GC
powinien ruszyć (ale nie musi!). Można też przypisać nieużywanym referencjom
wartość null
, co ułatwia pracę GC.
Pierwszy program
class HelloWorld {
public static void main(String[] args) {
System.out.println();
System.out.println("Hello, World!\n");
}
}
Java jest językiem programowania z silnym systemem typów.
Oznacza to, że każda zmienna, atrybut czy parametr ma zadeklarowany typ.
Kompilator wylicza typy wszystkich wyrażeń w programie i sprawdza, czy
wszystkie operatory i metody są używane zgodnie z ich deklaracjami, czyli z
argumentami odpowiednich typów. Także elementy każdej instrukcji muszą mieć
właściwe typy, np. warunek w pętli while
musi być wyrażeniem o wartości typu logicznego.
Specyficzną cechą Javy jest to, że typy w tym języku są podzielone na dwie kategorie:
Typy pierwotne to grupa ośmiu typów zawierających wartości proste. Tymi typami są:
boolean
,
byte
, 8-bitshort
, 16-bitint
, 32-bitlong
, 64-bitchar
, 16-bit (unsigned)float
, 32-bitdouble
, 64-bitTypy referencyjne dzielą się z kolei na następujące kategorie:
Wartościami typów referencyjnych są referencje (w pewnym uproszczeniu można o nich
myśleć jako o wskaźnikach) do obiektów lub wartość null
.
Ponadto istnieje typ o pustej nazwie, będący typem wyrażenia null
. Ponieważ nie
ma on nazwy, nie da się nawet zadeklarować zmiennej tego typu. Wartość tego typu
można rzutować na dowolny typ referencyjny, więc można myśleć o wartości null
jako o wartości każdego typu referencyjnego i nie przejmować się istnieniem tego typu.
536230
0b10101010
012300
0xff, 0xab12
1e1f, 2.f, .3f, 0f
1e1, .3, 0.0, 2.d, 0d
true, false
'a'
'\t'
tab'\n'
linefeed'\f'
form feed'\r'
carriage return'\"'
double quote'\\'
backslash'\''
single quote'\uffff'
Unicode; 4 cyfry szestnastkowe'\177'
kod oktalny, dla spójności z COd Java SE 7, w laterałach numerycznych może pojawić się dowolna liczba znaków podkreślnika (_). Można w ten sposób np. grupować cyfry w dużej liczbie w celu poprawienia czytelności.
long creditCardNumber = 1234_5678_9012_3456L;
long hexBytes = 0xFF_EC_DE_5EL;
long hexWords = 0xCAFE_BABEL;
long bytes = 0b11010010_01101001_10010100_10010010L
Zmienne są (zwykle) nazwanymi pojemnikami na pojedyncze wartości typu z jakim zostały zadeklarowane. Zmienne typów pierwotnych przechowują wartości dokładnie tych typów, zmienne typów referencyjnych przechowują wartość null albo referencję do obiektu typu będącego zadeklarowanym typem zmiennej bądź jego podklasą.
Wyróżniamy siedem rodzajów zmiennych:
Przy deklaracji czterech rodzajów zmiennych nie będących parametrami można
jawnie podawać ich wartości początkowe. To bardzo dobra praktyka. Jeśli w
deklaracji zmiennych klasowych, egzemplarzowych lub tablic nie podano ich
wartość początkowej, to taka zmienna zostanie automatycznie zainicjalizowana
wartością domyślną. Ta wartość zależy od typu zmiennej, ogólnie można by
powiedzieć, że odpowiada wartości 0 (dla wartości logicznych będzie to
false
, a dla typów referencyjnych wartość
null
). W przypadku zmiennych lokalnych programista
musi sam zadbać o zainicjalizowanie zmiennej przed jej odczytaniem. W
przeciwnym przypadku kompilator zgłasza błąd.
boolean
):
<, <=, >, >=, ==, !=
&& || !
logiczne operatory warunkowe (tylko dla boolean
)+ −
unarny* ⁄ %
dwuargumentowe+ −
dwuargumentowe++ −−
inkrementacja i dekrementacja (pre i postfiksowa)?:
operator warunkowy+
operator konkatenacji stringów<< >> >>>
operatory przesunięcia (tylko dla typów całkowitych),boolean
)
~
operator dopełnienia bitowego& | ^
(and, or, xor)+= −= *= ⁄= &= |= ^= %= <<= >>=
>>>=
operatory modyfikacji ⁄
Dzielenie całkowite daje wynik całkowity, np. 5 ⁄ 2
daje w wyniku 2 (a nie 2.5)
%
(a ⁄ b) * b
+ (a % b)
jest równe a
a % b
daje wynik
f
taki, że f
ma ten sam znak co
a
oraz a = i * b + f
gdzie
i
jest liczbą całkowitą i |f| <
|b|
E1 ? E2 : E3
E1
musi być typu boolean
wartością wyrażenia jest E2
jeżeli E1
jest true
i
E3
jeżeli E1
jest false
np. c = (a > b) ? a : b
oblicza maximum liczb a, b
postfixowyn++, n−−
prefixowy++n, −−n
n = 5; m = n++;
daje m = 5
n = 5; m = ++n;
daje m = 6
Postać: E1 op= E2
jest równoważne:
E1 = (T)((E1) op (E2))
T
jest typem E1
, op
jest jednym z:
+, −, *, ⁄, %, &, |, ^, <<, >>, >>>
Typy E1
i E2
muszą być typami prostymi
(z wyjątkiem operatora +=
, kiedy E2
może być dowolnym typem gdy E1
jest typu String
byte −> short, int, long, float, double
short −> int, long, float, double
char −> int, long, float, double
int −> long, float, double
long −> float, double
float −> double
byte −> char
short −> byte, char
char −> byte, short
int −> byte, short, char
long −> byte, short, char, int
float −> byte, short, char, int, long
double −> byte, short, char, int, long, float
przypisanie wyrażenia do zmiennej jest mozliwe jeżeli:
int
byte, short, char
Przy wywoływaniu funkcji argumenty muszą być zgodne co do typu lub może zachodzić konwersja rozszerzająca. Konwersja zawężająca powoduje błąd kompilacji.
Unarne promocje numeryczne
Jeżeli argument jednego z operatorów unarnych
(+, −, ~
) jest byte, short, char
to zostaje
skonwertowany do typu int
. W przeciwnymprzypadku typ nie jest zmieniany.
Unarna promocja jest również dokonywana dla:
każdego z operandów operatorów <<, >>, >>>
osobno
(by drugi operand typu long
nie promował konwersji pierwszego operandu) oraz
wyrażenia określającego wymiar lub indeks tablicy.
Dokonywane dla operandów następujących operatorów dwuargumentowych:
+, −, *, ⁄, %
<, <=, >, >=, ==, !=
&, |, ^
? :
Zasady konwersji
double
drugi operand jest konwertowany do double
float
drugi operand jest konwertowany do float
long
drugi operand jest konwertowany do long
int
System.out.format()
Specifier | Applies to | Output |
---|---|---|
%a | floating point (except BigDecimal) | Hex output of floating point number |
%b | Any type | "true” if non-null, "false” if null |
%c | character | Unicode character |
%d | integer (incl. byte, short, int, long, bigint) | Decimal Integer |
%e | floating point | decimal number in scientific notation |
%f | floating point | decimal number |
%g | floating point | decimal number, possibly in scientific notation depending on the precision and value. |
%h | any type | Hex String of value from hashCode() method. |
%n | none | Platform-specific line separator. |
%o | integer (incl. byte, short, int, long, bigint) | Octal number |
%s | any type | String value |
%t | Date/Time (incl. long, Calendar, Date and TemporalAccessor) | %t is the prefix for Date/Time conversions. More formatting flags are needed after this. See Date/Time conversion below. |
%x | integer (incl. byte, short, int, long, bigint) | Hex string. |
Flag | Notes |
---|---|
%tA | Full name of the day of the week, e.g. "Sunday ", "Monday " |
%ta | Abbreviated name of the week day e.g. "Sun ", "Mon ", etc. |
%tB | Full name of the month e.g. "January ", "February ", etc. |
%tb | Abbreviated month name e.g. "Jan ", "Feb ", etc. |
%tC | Century part of year formatted with two digits e.g. "00” through "99”. |
%tc | Date and time formatted with "%ta %tb %td %tT %tZ %tY ” e.g. "Fri Feb 17 07:45:42 PST 2017 " |
%tD | Date formatted as "%tm/%td/%ty " |
%td | Day of the month formatted with two digits. e.g. "01 ” to "31 ". |
%te | Day of the month formatted without a leading 0 e.g. "1” to "31”. |
%tF | ISO 8601 formatted date with "%tY-%tm-%td ". |
%tH | Hour of the day for the 24-hour clock e.g. "00 ” to "23 ". |
%th | Same as %tb. |
%tI | Hour of the day for the 12-hour clock e.g. "01 ” – "12 ". |
%tj | Day of the year formatted with leading 0s e.g. "001 ” to "366 ". |
%tk | Hour of the day for the 24 hour clock without a leading 0 e.g. "0 ” to "23 ". |
%tl | Hour of the day for the 12-hour click without a leading 0 e.g. "1 ” to "12 ". |
%tM | Minute within the hour formatted a leading 0 e.g. "00 ” to "59 ". |
%tm | Month formatted with a leading 0 e.g. "01 ” to "12 ". |
%tN | Nanosecond formatted with 9 digits and leading 0s e.g. "000000000” to "999999999”. |
%tp | Locale specific "am” or "pm” marker. |
%tQ | Milliseconds since epoch Jan 1 , 1970 00:00:00 UTC. |
%tR | Time formatted as 24-hours e.g. "%tH:%tM ". |
%tr | Time formatted as 12-hours e.g. "%tI:%tM:%tS %Tp ". |
%tS | Seconds within the minute formatted with 2 digits e.g. "00” to "60”. "60” is required to support leap seconds. |
%ts | Seconds since the epoch Jan 1, 1970 00:00:00 UTC. |
%tT | Time formatted as 24-hours e.g. "%tH:%tM:%tS ". |
%tY | Year formatted with 4 digits e.g. "0000 ” to "9999 ". |
%ty | Year formatted with 2 digits e.g. "00 ” to "99 ". |
%tZ | Time zone abbreviation. e.g. "UTC ", "PST ", etc. |
%tz | Time Zone Offset from GMT e.g. "-0800 " |
Indeks argumentu jest podawany jako liczba zakończona znakiem "$" po znaku "%" i
oznacza numer argumentu na liście argumentów funkcji format()
.
Na przykład:
String.format("%2$s", 32, "Hello"); // prints: "Hello"
instrukcja - wyrażenie zakończone średnikiem;
blok - grupa instrukcji ujęta w nawiasy klamrowe {} (składniowo równoważna jednej instrukcji)
if (wyrażenie boolean)
instrukcja-1
else
instrukcja-2
E1 ? E2 : E3
E1
musi być typu boolean
wartością wyrażenia jest E2
jeżeli E1
jest true
i
E3
jeżeli E1
jest false
np. c = (a > b) ? a : b
oblicza maximum liczb a, b
switch (wyrażenie) {
case stała-1: instrukcja-1
case stała-2: instrukcja-2
...
case stała-n: instrukcja-n
default: instrukcja
}
Od Javy 12 możliwości switcha zostały poszerzone. Do tej pory w blokach case można było wykonać obliczenia, czy wywołać metodę. Od Javy 12 poszczególne bloki case mogą zwrócić wynik, który następnie można przypisać np.
String season = "Winter";
String translation = switch (season) {
case "Spring" -> "wiosna";
case "Summer" -> "lato";
case "Autumn" -> "jesień";
case "Winter" -> "zima";
default -> "nieznany";
};
W jednym bloku case można obsłużyć kilka przypadków na raz:
String season = "Winter";
String temperature = switch (season) {
case "Spring", "Summer" -> "ciepło";
case "Autumn", "Winter" -> "zimno";
default -> "nieznany";
};
while (wyrażenie)
instrukcja;
do {
instrukcja;
} while (warunek);
for (wyrażenie1; warunek; wyrażenie2)
instrukcja;
Równoważne:
wyrażenie1;
while (warunek) {
instrukcja;
wyrażenie2;
}
Deklaracja tablicy
int[] ia;
char[] ca;
Tworzenie tablicy:
new
int[] ia = new int[100];
char[] ca = new char[20];
int[] silnia = { 1, 1, 2, 6, 24};
char ac[] = {'N', 'i', 'e', ' ', 'S', 't', 'r', 'i', 'n', 'g'};
String[] aas = {"Tablica", "Stringów"};
Tablice muszą być indeksowane wyrażeniami całkowitymi (int, short,
byte, char
). Tablica o długości n
może być
indeksowana od 0
do n-1
.
Tablice wielowymiarowe są "tablicami tablic"
class Array2DNew {
public static void main (String[] args) {
final int nr = 3, nc = 4;
int[][] array2D = new int[nr][nc];
for (int i = 0; i < array2D.length; i++) {
for (int j = 0; j < array2D[i].length; j++)
array2D[i][j] = i + j;
}
System.out.println();
for (int[] row : array2D) {
for (int anInt : row) System.out.format("%4d", anInt);
System.out.println();
}
System.out.println();
}
}
Tablice o zmiennej długości wierszy
class Array2DCol {
public static void main (String[] args) {
int nr = args.length;
int[][] array2D = new int[nr][];
for (int i = 0; i < array2D.length; i++) {
array2D[i] = new int[Integer.parseInt(args[i])];
for (int j = 0; j < array2D[i].length; j++)
array2D[i][j] = i + j;
}
System.out.println();
for (int[] row : array2D) {
for (int anInt : row) System.out.format("%4d", anInt);
System.out.println();
}
System.out.println();
}
}
<modyfikatory> class nazwa <extends nazwa_superklasy> <interfaces> {
// zawartość klasy
}
public
- klasa jest dostępna spoza pakietu, w którym jest deklarowanaabstract
- klasa jest niekompletna (w sensie brakujących metod)final
- klasa "końcowa" - nie może mieć podklas
<modyfikatory> typ nazwy_pól;
<modyfikatory>: public protected private final static
Pola statyczne (static
): jedno pole dla wszystkich obiektów danej klasy
Pola dynamiczne: każdy obiekt ma swoją kopię
Pola final
: stałe (muszą być inicjowane w deklaracji)
<modyfikatory> typ nazwa(lista_parametrów) { ... }
<modyfikatory>: public protected private abstract static final synchronized native
Point p = new Point (1,2);
Point(...)
- konstruktor
Wywołania metod dynamicznych odnoszą się do konkretnego obiektu, np.
p.move (4,2);
q.move (1,3);
i są traktowane jako komunikaty do danego obiektu (więc muszą znać "adresata" tego komunikatu).
Często mamy do czynienia z klasami przeznaczonymi tylko do przechowywania danych (np. pobranych z bazy). W większości przypadków dane te są niezmiennicze, ponieważ to zapewnia ich spójność bez konieczności synchronizacji.
Aby to osiągnąć, tworzymy klasy zawierające:
private final
dla każdej danejequals()
zwracającą true
dla takich
samych wartości wszystkich pólhashCode()
zwracającą tę samą wartość gdy wszystkie
pola są takie sametoString()
zawierającą nazwę klasy oraz nazwy i
wartości pól
public class Person {
private final String name;
private final String address;
public Person(String name, String address) {
this.name = name;
this.address = address;
}
@Override
public int hashCode() {
return Objects.hash(name, address);
}
@Override
public boolean equals(Object obj) {
if (this == obj) {
return true;
} else if (!(obj instanceof Person)) {
return false;
} else {
Person other = (Person) obj;
return Objects.equals(name, other.name)
&& Objects.equals(address, other.address);
}
}
@Override
public String toString() {
return "Person [name=" + name + ", address=" + address + "]";
}
// standard getters
}
Powyższy kod jest poprawny, ale zawiera bardzo dużo "boilerplate code". Podobne metody musimy stworzyć dla wszystkich klas. Wiele IDE potrafi automatycznie generować za nas te metody, ale nie dopasowuje ich automatycznie w przypadku np. dodania nowego pola.
Java 14 wprowadza pojęcie rekordu jako klas niezmienniczych danych, które
wymagają tylko typu i nazw swoich pól składowych.
Metody equals(), hashCode(), toString()
oraz pola z atrybutami
private final
i publiczny konstruktor są generowane przez
kompilator.
Aby stworzyć rekord Person
jak w przykładzie powyżej wystarczy
napisać:
public record Person (String name, String address) {}
Możemy do naszego rekordu dopisać / nadpisać odpowiednie metody (np. kolejny
konstruktor, który sprawdza poprawność argumentów lub metodę
toString()
, która prezentuje dane w odpowiadającym nam
formacie).
Dziedziczenie jest procesem, w którym obiekt otrzymuje właściwości innego obiektu. Mechanizm ten wspiera tworzenie hierarchicznych klasyfikacji. Na przykład jabłko odmiany Złota Reneta należy do ogólniejszej klasyfikacji jabłka, która z kolei jest częścią klasy owoc, która należy do ogólniejszej klasy żywność. Klasa żywność posiada określone właściwości (np. wartość odżywcza), które odnoszą się do jej klas pochodnych, w tym klasy owoc. Oprócz właściwości dziedziczonych po klasie żywność klasa owoc może posiadać własne, specyficzne właściwości (soczystość, słodkość itd.), które odróżniają ją od innych podklas klasy żywność. Klasa jabłko definiuje z kolei właściwości specyficzne dla jabłek (rośnie na drzewach, w klimacie umiarkowanym itd.). Klasa Złota Reneta dziedziczy właściwości wszystkich wymienionych klas i dodaje własne, które czynią ją unikatową.
Dziedziczenie w językach obiektowych to tworzenie hierarchii klas. Kolejne klasy w hierarchii posiadają zarówno pola, jak i metody, klas, które je poprzedzają, oraz mogą zawierać nowe pola i metody.
Gdyby nie hierarchie dziedziczenia, każdy obiekt musiałby jawnie definiować całą swoją charakterystykę. Zastosowanie mechanizmu dziedziczenia powoduje, że obiekt definiuje jedynie te właściwości, które czynią go unikatowym w klasie. Pozostałe atrybuty może dziedziczyć po klasie nadrzędnej. W ten sposób mechanizm dziedziczenia w języku Java sprawia, że obiekt może być traktowany jako specyficzna instancja ogólniejszej klasy.
Gdy klasa dziedziczy po innej klasie, mówimy, że klasa ta rozszerza tę klasę. Taką klasę będziemy nazywać klasą podrzędną. Klasę rozszerzaną będziemy nazywać klasą bazową bądź klasą nadrzędną. W języku Java do zaznaczenia, że dana klasa ma dziedziczyć po innej klasie, używamy słowa kluczowego extends, po którym następuje nazwa klasy nadrzędnej.
Podklasy dziedziczą wszystkie pola i metody ze swojej bezpośredniej nadklasy i nadinterfejsów.
Konstruktory i statyczne inicjalizery nie są dziedziczone.
Dostęp do metod i pól klasy:
klasa | pakiet | podklasa | świat | |
---|---|---|---|---|
private |
X | |||
X | X | |||
protected |
X | X | X | |
public |
X | X | X | X |
Polimorfizm (z greckiego: wiele form) oznacza w programowaniu obiektowym możliwość posługiwania się pewnym zbiorem akcji za pomocą jednego interfejsu. Konkretna akcja zostaje wybrana w konkretnej sytuacji. Najprostszym przykładem polimorfizmu może być kierownica samochodu. Kierownica (czyli interfejs) wygląda zawsze tak samo niezależnie od tego, jaki mechanizm kierowania zastosowano w aucie. Kierownicy używa się zawsze w taki sam sposób niezależnie od tego czy samochód posiada zwykłą przekładnię kierowniczą, czy wyposażony jest we wspomaganie. Jeśli umiesz posługiwać się kierownicą, możesz jeździć dowolnym typem auta.
Polimorfizm to najważniejsza cecha, która umożliwia dostosowanie działania obiektów do własnych oczekiwań poprzez łączenie funkcjonalności zarówno dziedziczonej, jak i implementowanej samodzielnie. Idea polimorfizmu bazuje na tym, że użytkownik obiektu nie wie i nie musi wiedzieć, czy konkretne zachowanie wykorzystywanego obiektu zostało zrealizowane bezpośrednio w tym obiekcie czy też w tym, po którym dziedziczy on swoje właściwości. Ponadto może się okazać (i często tak się dzieje), że takie samo odwołanie do metody za każdym razem dotyczy innej akcji (inaczej zdefiniowanej). Może się też okazać, że w zależności od poziomu dziedziczenia pozornie ta sama metoda (nazywająca się tak samo) wykonuje inną akcję.
Koncepcję polimorfizmu można przekazać w najbardziej ogólny sposób jako "jeden interfejs, wiele metod". Oznacza to możliwość zaprojektowania ogólnego interfejsu dla grupy powiązanych akcji. Polimorfizm pozwala ograniczyć złożoność programu poprzez zastosowanie tego samego interfejsu dla określenia ogólnej klasy akcji. Zadaniem kompilatora jest wybranie określonej akcji (czyli metody), którą należy zastosować w konkretnej sytuacji. Programista nie musi dokonywać tego wyboru. Wystarczy, że używa ogólnego interfejsu.
Może się wydawać, że dziedziczenie jest dobrym sposobem na reużycie kodu, jednak nie zawsze jest to polecane rozwiązanie. Używane nieodpowiednio skutkuje oprogramowaniem słabej jakości. Dziedziczenie jest bezpieczne, jeśli jest używane tylko w obrębie pakietu, gdzie nadklasa i wszystkie podklasy są pod kontrolą.
Gdy klasa nie jest odpowiednio zaprojektowana do dziedziczenia, możemy natrafić na następujące problemy:
HashSet
, który będzie zliczał,
ile było prób dodania elementu. HashSet
zawiera dwie metody, które
są w stanie dodawać elementy: add()
i addAll()
, więc
możemy je obie nadpisać, używając dodatkowo licznika:
// Broken - Inappropriate use of inheritance!
class InstrumentedHashSet<E> extends HashSet<E> {
// The number of attempted element insertions
private int addCount = 0;
public InstrumentedHashSet() {
}
public InstrumentedHashSet(int initCap, float loadFactor) {
super(initCap, loadFactor);
}
@Override
public boolean add(E e) {
addCount++;
return super.add(e);
}
@Override
public boolean addAll(Collection<? extends E> c) {
addCount += c.size();
return super.addAll(c);
}
public int getAddCount() {
return addCount;
}
}
Jeżeli dodamy elementy używając metody addAll()
to wynik będzie
niepoprawny!!!
InstrumentedHashSet<String> s = new InstrumentedHashSet<>();
s.addAll(List.of("elem1", "elem2", "elem3"));
Metoda getAddCount()
zwróci 6 (choć powinna 3!!!), ponieważ
metoda addAll
w HashSet
pod spodem wykorzystuje metodę
add dla każdego elementu. Moglibyśmy to "naprawić" nie nadpisując metody
addAll
, ale wtedy nasz klasa zależałaby od tego szczegółu
implementacyjnego, który może się zmienić w kolejnej wersji i byłaby
niestabilna.
Podklasy nie są stabilne i bezpieczne w użyciu - załóżmy, że bezpieczeństwo programu zależy od tego, że wszystkie elementy dodane do kolekcji spełniają określony warunek. Można by to zaimplementować, rozszerzając klasę kolekcji i nadpisać wszystkie metody, które umożliwiają dodawanie elementów, tak by sprawdzały ten warunek. To będzie działać tylko do momentu, gdy do klasy w kolejnej wersji nie zostanie dodana nowa metoda, pozwalająca dodawać elementy. Kiedy to się stanie, będzie można dodać nielegalne elementy do kolekcji, używając nowej metody (dopóki dziura ta nie zostanie załatana).
Rozbudowane hierarchie dziedziczenia utrudniają również analizę kodu i testowanie.
Zamiast rozszerzać klasę, możemy zadeklarować ją jako pole prywatne w naszej nowej klasie i wywoływać jej odpowiadające metody. Jest to znane jako delegacja (forwarding). W rezultacie otrzymamy solidną klasę, która nie zależy od detalów implementacyjnych istniejącej klasy. Również dodawanie do niej nowych metod nie będzie miało wpływu na naszą klasę.
Dziedziczenie jest odpowiednie tylko wtedy, kiedy podklasa naprawdę jest podtypem danej klasy. Tzn. między nimi jest relacja "jest". Jeśli zastanawiamy się, czy klasa B powinna rozszerzać A, powinniśmy sobie zadać pytanie, czy na pewno B jest też A. Jeśli mamy ku temu wątpliwości to powinniśmy zastosować kompozycję.
Kolejną sprawą, nad którą warto się zastanowić jest to czy potrzebujemy rzeczywiście wszystkich metod z nadklasy?
Interface
jest typem referencyjnym, zawierającym definicje
stałych i abstrakcyjnych metod.
Cel: nadanie nie związanym ze sobą klasom pewnej wspólnej cechy.
Deklaracja:
interface nazwa <extends interface ...> {
deklaracje stałych
deklaracje metod
}
Pola mają domyślne atrybuty public static final
. Wszystkie
pola muszą być zainicjalizowane.
Metody mają domyślne atrybuty public abstract
. Nie może
być metod statycznych bo statyczna metoda nie może być
abstract
.
public abstract
static final
extends
lecz
implements
Kiedy klasa implementuje dany interfejs, można to przedstawić jako "kontrakt" zobowiązujący klasę do wykonania czynności określonych w interfejsie. Jeżeli klasa nie implementuje wszystkich metod zadeklarowanych w interfejsie, musi być zadeklarowana jako abstrakcyjna.
Nested classes are divided into two categories: static and non-static. Nested classes that are declared static are simply called static nested classes. Non-static nested classes are called inner classes.
Klasy w Javie mogą być zagnieżdżane na dowolną głębokość, tzn. Klasa
A
może zawierać klasę B
, która zawiera klasę
C
, itd. Jednak więcej niż jeden poziom zagnieżdżenia jest rzadko
spotykany i jest uważany za błąd projektowy.
Można wyróżnić kilka przyczyn, dla jakich stosuje się zagnieżdżanie klas:
Zobaczmy prosty przykład przedstawiający różne typy klas zagnieżdżonych:
class NestedTest {
// 1. Creating static class in Java
static class StaticNested {
public void name(){
System.out.println("static nested class");
}
}
// 2. Creating inner class in Java
class Inner {
public void name() {
System.out.println("inner class");
}
}
// 3. Creating local inner class inside class method
static void funStatic() {
class LocalInStatic {
public void name() {
System.out.println("local class in class method");
}
}
//creating instance of local inner class
LocalInStatic local = new LocalInStatic();
local.name(); // calling method from local inner class
}
// 4. Creating local inner class inside instance method
void fun() {
class Local {
public void name() {
System.out.println("local class in instance method");
}
}
//creating instance of local inner class
Local local = new Local();
local.name(); // calling method from local inner class
}
}
public class NestedClassTest {
public static void main(String args[]) {
// creating instance of static class (no instance of outer class needed)
NestedTest.StaticNested nested = new NestedTest.StaticNested();
nested.name();
// creating instance of inner class (outer class instance necessary)
NestedTest.Inner inner = new NestedTest().new Inner();
inner.name();
// creating instance of local class in class method
NestedTest.funStatic();
// creating instance of local class in instance method
new NestedTest().fun();
}
}
Klasy statyczne są najprostsze, ponieważ nie mają nic wspólnego z instancjami (obiektami) klasy zewnętrznej.
Klasa statyczna jest deklarowana jako składowa statyczna innej klasy. Podobnie jak inne składowe statyczne, taka klasa jest praktycznie doczepką, używającą przestrzeni nazw klasy otaczającej. Do stworzenia odpowiedniej instancji nie jest potrzebny obiekt klasy zewnętrznej.
Klasa wewnętrzna to klasa zadeklarowana jako niestatyczna składowa innej klasy
W odróżnieniu od klas statycznych, każda instancja klasy wewnętrznej jest
związana z instancją klasy zewnętrznej (dlatego jest ona konieczna). W powyższym
przykładzie obiekt Inner
jest związany z instancją
this
klasy NestedTest
.
Co nam daje taka konstrukcja? Instancja klasy wewnętrznej ma dostęp do
składowych klasy otaczającej, do których odnosimy się po prostu przez ich nazwy,
ale bez użycia referencji this
. Należy pamiętać, że
this
w klasie wewnętrznej odnosi się do instancji tej klasy a nie
instancji skojarzonej z nią klasy zewnętrznej.
Klasa lokalna jest klasą zadeklarowaną wewnątrz metody i jest znana tylko
wewnątrz tej metody. W związku z tym tylko tam mogą być tworzone jej instancje.
Zaletą jest fakt, że klasa lokalna ma dostęp do lokalnych zmiennych metody,
które są albo zdefiniowane z atrybutem final
bądź są
effectively final
(czyli nie zmieniają swojej wartości, ale nie
mają jawnej specyfikacji final
). Java zachowuje wartość takiej
zmiennej (taką, jaką miała w momencie tworzenia obiektu), nawet jeżeli zakres
życia zmiennej sie skończył.
Ponieważ klasa lokalna nie jest składową klasy czy pakietu, nie posiada atrybutów dostępu (ale jej składowe już tak).
Jeżeli klasa lokalna jest zadeklarowana w metodzie obiektowej
(niestatycznej), instancja tej klasy jest związana z referencją
this
tej metody. W związku z tym składowe klasy zewnętrznej są
dostępne tak jak w przypadku klas wewnętrznych.
Klasa anonimowa jest wygodnym w użyciu wariantem klasy lokalnej. W większości przypadków instancja klasy lokalnej jest tworzona raz, gdy uruchamiamy zawierającą ją metodę. Byłoby więc sensownie połączyć definicję klasy lokalnej i jej instancjację. Można też pominąć nazwę klasy lokalnej, która tak naprawdę nie jest potrzebna. Obie te funkcje spełniają klasy anonimowe.
Na ogół wykorzystuje się w tym przypadku interfejs, który klasa anonimowa powinna implementować.
Runnable action = new Runnable() {
public void run() {
...
}
};
Taka deklaracja tworzy nową instancję nienazwanej klasy, która implementuje
interfejs Runnable
(odpowiedzialny za tworzenie wątków; jego merodą
jest run()
).
Należy pamiętać, że klasa anonimowa jest uproszczoną wersją klasy lokalnej z
jedną instancją. Jeżeli potrzebna jest klasa lokalna, która implementuje kilka
interfejsów i dziedziczy po klasie innej niż Object
, to na ogół
musimy wrócić do koncepcji nazwanej klasy lokalnej.
Wyjątek powinien być generowany przez metodę gdy jest zmuszona do wykonania czegoś niemożliwego lub naruszającego stabilność systemu.
Wyjątek jest obiektem klasy Exception
lub
jej podklasy. By wygenerować wyjątek musimy stworzyć
nowy obiekt odpowiedniego typu i użyć instrukcji throw
.
throw new ArithmeticException();
lub
throw new ArrayIndexOutOfBoundsException("Integer array index out of bounds");
Wszystkie wbudowane wyjątki Javy mają 2 konstruktory:
jeden bez parametrów i drugi akceptujący String
(który może być wypisany, np.)
catch(ArrayIndexOutOfBoundsException e) {
System.out.println(e.toString());
}
Dlaczego Java wyróżnia trzy podstawowe typy wyjątków? Odpowiedź sprowadza się do tego, w jaki sposób program powinien obsłużyć (lub nie!) daną sytuację.
Exception
i klas pochodnych (ale nie
RuntimeException
) ogólnie reprezentują błędy, które są poza
bezpośrednią kontrolą programu i na ogół oznaczają problem z zasobami
zewnętrznymi. Na przykład błąd w połączeniu sieciowym, błąd systemu plików,
itp. Kompilator Javy zmusza nas do obsługi takich sytuacji (stąd nazwa "checked
exceptions"). Możemy "oszukać" kompilator, ale nie jest to dobra praktyka.Error
i klas pochodnych odpowiadają "poważnym"
błędom, których program raczej nie wyłapuje (ponieważ nie ma możliwości
sensownego wyjścia z tych sytuacji a kompilator tego nie wymaga). Na przykład:
brakujący plik class
, OutOfMemoryError
, itp.RuntimeExeptions
i klasy pochodne są nieco inne: reprezentują
wyjątki, które nie powinny się zdarzyć i na ogół są spowodowane błędami
programistycznymi. Na przykład: przekroczenie zakresu tablicy, dzielenie przez
zero, ujemna długość tablicy, itp. Wystąpienie wyjątków tego typu (unchecked
exceptions) nie jest sprawdzane przez kompilator, więc w tym sensie są one
podobne do wyjątków z klasy Error
.
Zasada ogólna: Jeżeli program ma szansę "podnieść się" po wystąpieniu wyjątku, powinien to być wyjątek typu "checked". Jeżeli nic sensownego nie da się zrobić - stosujemy wyjątek "unchecked".
ClassNotFoundException
CloneNotSupportedException
InstantiationException
InterruptedException
IOException
EOFException
FileNotFoundException
ArithmeticException
ClassCastException
IllegalArgumentException
NumberFormatException
IndexOutOfBoundsException
ArrayIndexOutOfBoundsException
StringIndexOutOfBoundsException
NegativeArraySizeException
NoSuchFieldException
NoSuchMethodException
NullPointerException
finally
try {
// instrukcje ktore moga potencjalnie zakonczyc sie wyjatkiem
} catch (TypWyjatku dowolnaNazwa) {
// instrukcje, gdy zajdzie wyjątek TypWyjatku
} finally {
// instrukcje, ktore maja byc wykonane niezaleznie od tego,
// czy wyjatek zostal zlapany, czy nie
}
Blok try-catch-finally
może zawierać:
try-catch
try-finally
W drugim z przypadków specyfikacja Javy gwarantuje, że blok
finally
będzie wykonany zawsze, nawet jeżeli blok try
spowoduje wygenerowanie (ale nie wyłapanie) wyjątku. W tym przypadku blok
finally
jest jedyną szansą na wykonanie clean-up'u (np. zamknięcie
plików). Kod po bloku finally
może nigdy nie zostać wykonany.
Jednak nawet jeżeli wyjątek zostanie wyłapany, częstą praktyką jest
umieszczenie w bloku catch
zdania return
(lub innego,
które zmienia sekwencyjne wykonanie instrukcji). W tym przypadku także blok
finally
zostanie wykonany. Jedynym wyjątkiem jest użycie funkcji
System.exit()
, która kończy program od razu (bez skoku do
finally
).
Umieszczenie kodu clean-up'u w bloku try-finally
jest zalecane
zawsze (nawet jeżeli nie przewidujemy wystąpienia wyjątków) ponieważ inaczej
możliwe jest przypadkowe ominięcie tego kodu (np. poprzez użycie
return
lub break
)
Konstrukcja try-with-resources
(od Java 8) wygląda jak
try/catch
z tym, że przez blokiem objętym try
możemy
zainicjalizować zmienne, które zostaną automatycznie zamknięte.
Konstrukcję try-with-resources
możemy nazwać także "menadżerem
kontekstu", automatycznie zarządza ona za nas kontekstem, w ramach którego
dostępne są zmienne zdefiniowane wewnątrz nawiasów (). Co więcej, wewnątrz tych
nawiasów możemy zainicjalizować więcej zmiennych, każda z nich zostanie
poprawnie zamknięta (zostanie na nich wywołana metoda close()
,
dlatego typy tu użyte muszą implementować interfejs AutoCloseable
).
W przykładzie poniżej odczytujemy kolejne linie z pliku wejściowego i zapisujemy
je do pliku wyjściowego.
try(BufferedReader fileReader = new BufferedReader(new FileReader("input.txt"));
BufferedWriter fileWriter = new BufferedWriter(new FileWriter("output.txt"))) {
String line;
while((line = fileReader.readLine()) != null) {
fileWriter.write(line);
fileWriter.newLine();
}
} catch(IOException exception) {
exception.printStackTrace();
}
Ad.1
readFile {
open the file;
determine its size;
allocate that much memory;
read the file into memory;
close the file;
}
Potencjalne problemy:
errorCodeType readFile {
initialize errorCode = 0;
open the file;
if (theFileIsOpen) {
determine the length of the file;
if (gotTheFileLength) {
allocate that much memory;
if (gotEnoughMemory) {
read the file into memory;
if (readFailed) {
errorCode = -1;
}
} else {
errorCode = -2;
}
} else {
errorCode = -3;
}
close the file;
if (FileDidntClose && errorCode == 0) {
errorCode = -4;
} else {
errorCode = errorCode and -4;
}
} else { errorCode = -5; }
return errorCode;
}
readFile {
try {
open the file;
determine its size;
allocate that much memory;
read the file into memory;
close the file;
} catch (fileOpenFailed) {
doSomething;
} catch (sizeDeterminationFailed) {
doSomething;
} catch (memoryAllocationFailed) {
doSomething;
} catch (readFailed) {
doSomething;
} catch (fileCloseFailed) {
doSomething;
}
}
Ad. 2. Propagacja błędów:
Tylko method1
jest zainteresowana w ewentualnej
obsłudze sytuacji awaryjnej
method1 {
call method2;
}
method2 {
call method3;
}
method3 {
call readFile;
}
method1 {
errorCodeType error;
error = call method2;
if (error)
doErrorProcessing;
else
proceed;
}
errorCodeType method2 {
errorCodeType error;
error = call method3;
if (error)
return error;
else
proceed;
}
errorCodeType method3 {
errorCodeType error;
error = call readFile;
if (error)
return error;
else
proceed;
}
method1 {
try {
call method2;
} catch (exception) {
doErrorProcessing;
}
}
method2 throws exception {
call method3;
}
method3 throws exception {
call readFile;
}
Możliwości:
catch (InvalidIndexException e) {
...
}
catch (ArrayException e) {
...
}
catch (Exception e) {
...
}
Thread
Thread()
Thread(Runnable target)
Thread(String name)
Thread(Runnable target, String name)
Thread
. Klasa ta powinna
zaimplementować metodę run()
klasy Thread
. Obiekt tej
klasy może być wtedy utworzony i uruchomiony.Runnable
. Klasa ta
musi zaimplementować metodę run()
. Wtedy obiekt tej klasy może być
utworzony i uruchomiony poprzez wywołanie metody start()
z klasy
Thread
.
simple = new Thread(this, str);
simple.start();
Not Runnable
sleep()
Thread.sleep((long)(Math.random()*1000));
wait()
Runnable
sleep()
musi upłynąć wyznaczony
czas (w milisec)wait()
inny obiekt musi zawiadomić wątek
o zmianie warunku oczekiwania wywołując notify()
lub
notifyAll()
Dead
). Zwykle
wątek zatrzymuje się gdy zakończy się jego metoda run()
System.out.println("DONE! " + simple.getName());
isAlive()
Metoda zwraca true
jeżeli wątek został wystartowany i jeszcze
nie został zatrzymany (jest w stanie Runnable
lub Not
Runnable
ale nie wiadomo w którym).
Metoda zwraca false
jeżeli jest to nowy wątek nie został jeszcze
wystartowany) lub wątek został już zatrzymany (jest w stanie
Dead
).
java.util.concurrent
Race conditions.
Pakiet java.util.concurrent
jest narzędziem do tworzenia aplikacji
współbieżnych. Do najistotniejszych klas pakietu należą:
Powyższy schemat przedstawia działanie egzekutora.
java.io
System.in
(typu InputStream
)System.out
(typu PrintStream
)System.err
Potoki proste (sink streams)
Nośnik | Potok znakowy | Potok bajtowy |
---|---|---|
CharArrayReader | ByteArrayInputStream | |
Pamięć | CharArrayWriter | ByteArrayOutputStream |
StringReader | StringBufferInputStream | |
StringWriter | ||
Pipe | PipedReader | PipedInputStream |
PipedWriter | PipedOutputStream | |
Plik | FileReader | FileInputStream |
FileWriter | FileOutputStream |
Potoki konwertujące (processing streams)
Funkcja | Potok znakowy | Potok bajtowy |
---|---|---|
Buforowanie | BufferedReader | BufferedInputStream |
BufferedWriter | BufferedOutputStream | |
Filtrowanie | FilterReader | FilterInputStream |
FilterWriter | FilterOutputStream | |
Konwersja między bajtami i znakami |
InputStreamReader OutputStreamWriter |
|
konkatenacja | SequenceInputStream | |
Konwersja danych | DataInputStream | |
DataOutputStream | ||
Cofanie znaku | PushbackReader | PushbackInputStream |
Drukowanie | PrintWriter | PrintStream |
Zachowują się jak duża tablica (zapisana na dysku). Indeksem tej tablicy
jest wskaźnik pliku (filepointer). Operacje odczytu zaczynają sie od
miejsca wskazywanego przez wskaźnik. Jeżeli plik jest otwarty także do zapisu to
możliwy jest zapis do pliku (podobnie od miejsca wskazywanego przez wskaźnik).
Operacje odczytu i zapisu modyfikują wskaźnik pliku. Wskażnik pliku może być
odczytany metodą getFilePointer()
i ustawiony metodą
seek()
.
RandomAccessFile (String name, String mode)
RandomAccessFile (File file, String mode)
public native long getFilePointer()
public native void seek(long pos)
public native long length()
Klasa RandomAccessFile
implementuje interfejsy DataInput,
DataOutput
, dostępne są więc metody odczytu/zapisu prostych typów danych
read(), readByte(), readInt, readLine(), ... write(), writeByte(),
writeInt() ...
write
operation takes data from a buffer and writes it to a
channel.
A read
operation reads data from a channel and writes it into a
buffer.
Once the data is in the buffer, we can read it and interpret it as raw bytes,
data types, objects, or characters as we need it.
A FileChannel can be mapped to a memory array for direct access. This allows for
much faster operation than accessing to the disk directly. It is built on native
features provides by the different operating system and this is the reason why
the concrete implementation of FileChannel are hidden just because they are
different depending on the machine we are working on.
Java NIO Channels are similar to streams with a few differences:
Here are the most important Channel implementations in Java NIO:
FileChannel
reads data from and to filesDatagramChannel
can read and write data over the network via UDPSocketChannel
can read and write data over the network via TCPServerSocketChannel
allows to listen for incoming TCP connections,
like a web server does. For each incoming connection a
SocketChannel
is createdUsing a Buffer to read and write data typically follows this little 4-step process:
buffer.flip()
,buffer.clear()
or buffer.compact()
,flip()
method call. In
reading mode the buffer lets you read all the data written into the buffer.
Once you have read all the data, you need to clear the buffer, to make it
ready for writing again. You can do this in two ways: By calling
clear()
or by calling compact()
. The
clear()
method clears the whole buffer. The compact()
method only clears the data which you have already read. Any unread data is
moved to the beginning of the buffer, and data will now be written into the
buffer after the unread data.
A Buffer has three properties you need to be familiar with, in order to understand how a Buffer works. These are:
In write mode the limit of a Buffer is the limit of how much data you can write into the buffer. In write mode the limit is equal to the capacity of the Buffer.
When flipping the Buffer into read mode, limit means the limit of how much data you can read from the Buffer. Therefore, when flipping a Buffer into read mode, limit is set to write position of the write mode. In other words, you can read as many bytes as were written (limit is set to the number of bytes written, which is marked by position).
Gather / scatter operations: one channel and many buffers, as below
Type erasure can be explained as the process of enforcing type constraints only at compile time and discarding the element type information at runtime.
For example:
public static<E> boolean containsElement(E[] elements, E element) {
for (E e : elements) {
if(e.equals(element)) {
return true;
}
}
return false;
}
When compiled, the unbound type E gets replaced with an actual type of
Object
:
public static boolean containsElement(Object[] elements, Object element) {
for (Object e : elements) {
if(e.equals(element)) {
return true;
}
}
return false;
}
The compiler ensures type safety of our code and prevents runtime errors.
At the class level, type parameters on the class are discarded during code
compilation and replaced with its first bound, or Object
if the
type parameter is unbound. Let’s implement a Stack using an array:
public class Stack<E> {
private E[] stackContent;
public Stack(int capacity) {
this.stackContent = (E[]) new Object[capacity];
}
public void push(E data) {
// ...
}
public E pop() {
// ...
}
}
Upon compilation, the unbound type parameter E
is replaced with
Object
:
public class Stack {
private Object[] stackContent;
public Stack(int capacity) {
this.stackContent = (Object[]) new Object[capacity];
}
public void push(Object data) {
// ...
}
public Object pop() {
// ...
}
}
In a case where the type parameter E
is bound:
public class BoundStack<E extends Comparable<E>> {
private E[] stackContent;
public BoundStack(int capacity) {
this.stackContent = (E[]) new Object[capacity];
}
public void push(E data) {
// ...
}
public E pop() {
// ...
}
}
When compiled, the bound type parameter E
is replaced with the first bound
class, Comparable
in this case:
public class BoundStack {
private Comparable [] stackContent;
public BoundStack(int capacity) {
this.stackContent = (Comparable[]) new Object[capacity];
}
public void push(Comparable data) {
// ...
}
public Comparable pop() {
// ...
}
}
For method-level type erasure, the method’s type parameter is not stored but
rather converted to its parent type Object
if it’s unbound or it’s
first bound class when it’s bound.
Let’s consider a method to display the contents of any given array:
public static<E> void printArray(E[] array) {
for (E element : array) {
System.out.printf("%s ", element);
}
}
Upon compilation, the type parameter E
is replaced with
Object
:
public static void printArray(Object[] array) {
for (Object element : array) {
System.out.printf("%s ", element);
}
}
For a bound method type parameter:
public static<E extends Comparable<E>> void printArray(E[] array) {
for (E element : array) {
System.out.printf("%s ", element);
}
}
We’ll have the type parameter E
erased and replaced with
Comparable
:
public static void printArray(Comparable[] array) {
for (Comparable element : array) {
System.out.printf("%s ", element);
}
}
The unbounded wildcard type is specified using the wildcard character (?),
for example, List<?>
. This is called a list of unknown type.
There are two scenarios where an unbounded wildcard is a useful approach:
Object
class.List.size()
or
List.clear()
. In fact, Class<?>
is so
often used because most of the methods in Class<T>
do
not depend on T
.You can use an upper bounded wildcard to relax the restrictions on a
variable. For example, say you want to write a method that works on
List<Integer>, List<Double>
, and
List<Number>
; you can achieve this by using an upper bounded
wildcard.
To declare an upper-bounded wildcard, use the wildcard character ('?'), followed by the extends keyword, followed by its upper bound. Note that, in this context, extends is used in a general sense to mean either "extends" (as in classes) or "implements" (as in interfaces).
To write the method that works on lists of Number
and the
subtypes of Number
, such as Integer, Double
, and
Float
, you would specify List<? extends
Number>
. The term List<Number>
is more
restrictive than List<? extends Number>
because the
former matches a list of type Number
only, whereas the latter
matches a list of type Number
or any of its subclasses.
A lower bounded wildcard is expressed using the wildcard character ('?'),
following by the super keyword, followed by its lower bound: <? super
A>
.
Note: You can specify an upper bound for a wildcard, or you can specify a lower bound, but you cannot specify both.
Say you want to write a method that puts Integer
objects into a
list. To maximize flexibility, you would like the method to work on
List<Integer>, List<Number>
, and
List<Object>
- anything that can hold Integer values.
To write the method that works on lists of Integer
and the
supertypes of Integer
, such as Integer, Number
, and
Object
, you would specify List<? super Integer>
.
The term List<Integer>
is more restrictive than
List<? super Integer>
because the former matches a list of
type Integer
only, whereas the latter matches a list of any type
that is a supertype of Integer
.
Wyrażenia lambda są pierwszym krokiem zbliżającym Javę do paradygmatu programowania funkcjonalnego. Lambda jest funkcją, która może być stworzona bez konieczności przynależenia do klasy, przekazywana jak obiekt i wykonywana na żądanie.
Wyrażenia lambda można także porównać do klas anonimowych. Mają one jednak dużo bardziej czytelną i zwięzłą składnię.
Na przykład wyrażenie lambda, które podnosi do kwadratu przekazaną liczbę wygląda następująco:
x -> x * x
Wyrażenie lambda ma następującą składnię
<lista parametrów> -> <ciało wyrażenia>
Lista parametrów zawiera wszystkie parametry przekazane do "ciała" wyrażenia
lambda. W szczególności lista ta może być pusta. Wyrażenie lambda poniżej nie
przyjmuje żadnych argumentów, zwraca natomiast instancję klasy
String
:
() -> "some return value"
Podawanie typów parametrów jest opcjonalne. Kompilator jest w stanie wydedukować te parametry z kontekstu w którym znajduje się dane wyrażenie lambda. W razie potrzeby można je także podać:
(Integer x, Long y) -> System.out.println(x*y)
Nawiasy otaczające listę parametrów są opcjonalne jeśli wyrażenie ma wyłącznie jeden parametr bez określonego typu.
W wielu przypadkach wyrażenia lambda składaja się z pojedynczego wyrażenia (jak w powyższych przykładach).
Może się jednak zdarzyć, że wyrażenie lambda będzie zawierało więcej linii. W
takim przypadku musi być otoczone nawiasami {}
jak w przykładzie
poniżej:
x -> {
if (x != null) {
return x * x;
} else {
return 0;
}
}
() -> System.out.println("Zero parameter lambda");
Puste nawiasy sygnalizują lambdę bez paramertów, podobnie jak w przypadku zwykłych metod.
(param) -> System.out.println("One parameter: " + param);
param -> System.out.println("One parameter(no parentheses): " + param);
Pojedynczy parametr można umieścić w nawiasach lub bez nich.
(p1, p2) -> System.out.println("Multiple parameters: " + p1 + ", " + p2);
W typ przypadku nawiasy są konieczne.
Jeżeli kompilator nie jest w stanie wywnioskować typu / typów parametrów, to podajemy je jawnie (jak w deklaracji metody):
(Car car) -> System.out.println("The car is: " + car.getName());
Pojedyncze wyrażenie:
(oldState, newState) -> System.out.println("State changed")
Wyrażenie złożone umieszczamy w nawiasach klamrowych { }
,
podobnie jak ciało funkcji:
(oldState, newState) -> {
System.out.println("Old state: " + oldState);
System.out.println("New state: " + newState);
}
Używamy instrukcji return
jak w funkcji:
(param) -> {
System.out.println("param: " + param);
return "return value";
}
(a1, a2) -> { return a1 > a2; }
W tym ostatnim przykładzie, kiedy lambda tylko oblicza i zwraca wartość w
pojedynczym wyrażeniu, zdanie return
można pominąć:
(a1, a2) -> a1 > a2;
Jeżeli odpowiedź na wszystkie trzy pytania jest pozytywna, to lambda jest dopasowywana do interfejsu.
Definiując klasę anonimową implementującą interfejs, musimy jawnie zdefiniować, który interfejs implementujemy. W przypadku wyrażeń lambda ten typ często jest wnioskowany (inferred) z kontekstu. Na przykład typ parametru funkcji można wywnioskować z jej deklaracji.
Podobnie kompilator może wywnioskować typy parametrów lambdy z deklaracji metody
Lambda jest praktycznie obiektem (klasy implementującej interfejs). Wyrażenie lambda można więc przypisać do zmiennej i przekazywać jak każdy inny obiekt:
public interface MyComparator {
public boolean compare(int a1, int a2);
}
MyComparator myComparator = (a1, a2) -> a1 > a2;
boolean result = myComparator.compare(2, 5);
Zamiana klasy anonimowej na wyrażenie lambda.
public interface Checker<T> {
boolean check(T object);
}
Checker<Integer> isOddAnonymous = new Checker<>() {
@Override
public boolean check(Integer object) {
return object % 2 != 0;
}
};
System.out.println(isOddAnonymous.check(123));
System.out.println(isOddAnonymous.check(124));
W przykładzie tym zdefiniowano interfejs Checker
, który posiada
jedną metodę check()
. Metoda ta zwraca wartość logiczną na
podstawie przekazanego argumentu.
Fragment kodu robiący to samo jednak przy użyciu składni wyrażeń lambda wygląda następująco:
Checker<Integer> isOddLambda = object -> object % 2 != 0;
System.out.println(isOddLambda.check(123));
System.out.println(isOddLambda.check(124));
Każde wyrażenie lambda jest instancją dowolnego interfejsu funkcyjnego.
Interfejs funkcyjny to interfejs, który ma dokładnie jedną abstrakcyjną metodę.
Wprowadzono adnotację @FunctionalInterface
, którą zaleca dodać do
interfejsów tego typu. Adnotacja ta zapewnia, że kompilator upewni się, że dany
interfejs jest interfejsem funkcyjnym. Jeśli nie, wówczas kompilacja się nie
powiedzie. Przykładem interfejsu funkcyjnego może być zdefiniowany wcześniej
interfejs Checker
. Zawiera on wyłącznie jedną metodę
check()
.
W biblotece Javy znajduje się zestaw interfejsów funkcyjnych do
wykorzystania. W większości przypadków w zupełności wystarczy ich użycie. Część
z nich znajduje się w pakiecie java.util.function
. Najważniejsze z
nich to:
Function<T, R>
zawiera metodę apply
, która
przyjmuje instancję klasy T
zwracając instancję klasy
R
,Consumer<T>
zawiera metodę accept
, która
przyjmuje instancję klasy T
,Predicate<T>
zawiera metodę test
, która
przyjmuje instancję klasy T
i zwraca wartość logiczną.
Interfejs ten może posłużyć do zastąpienia interfejsu Checker
,Supplier<T>
zawiera metodę get
, która nie
przyjmuje żadnych parametrów i zwraca instancję klasy T
,UnaryOperator<T>
jest specyficznym przypadkiem
interfejsu Function
. W tym przypadku typ argumentu i typ
zwracany są te same.Wyrażenia lambda zdefiniowane wcześniej można przypisać do tych właśnie interfejsów:
UnaryOperator square = x -> x * x;
Supplier someString = () -> "some return value";
BiConsumer multiplier = (Integer x, Long y) -> System.out.println(x * y);
Function multiline = x -> {
if (x != null) {
return (long) x * x;
} else {
return 0L;
}
};
W klasie Object
zdefiniowano metodę getClass
. Zastosowana wobec dowolnego
obiektu zwraca odnośnik do jego klasy, do obiektu klasy
java.lang.Class
.
Obiekty klasy Class
są klasami (to ważne: tu same klasy są
obiektami)
Możemy wobec takich obiektów stosować różne metody klasy Class
z
pakietu java.lang
, np.
getSuperClass()
- zwracającą obiekt klasy Class oznaczający
klasę bazową danej klasy,getInterfaces()
- zawracającą tablicę obiektów, zawierającą
interfejsy danej klasy,newInstance()
- tworzącą nowy obiekt danej klasy.Obiektów klasy Class
nie możemy tworzyć za pomocą wyrażenia new,
ponieważ ta klasa nie posiada publicznego konstruktora. Jedynie poprzez użycie
odpowiednich metod uzyskujemy referencje do tych obiektów, np.:
Class c = Class.forName("java.lang.Integer");
lub
Class c = java.lang.Integer.class;
uzyskujemy referencję do klasy Integer
i możemy się nim posłużyć
przy tworzeniu obiektu:
Integer i = (Integer) c.newInstance();
W statycznym przypadku, gdy wszystko jest ustalone "w źródle" programu, sens takich konstrukcji jest niewielki. Ale czasem warto odłożyć pewne ustalenia do fazy wykonania programu, zwiększając jego elastyczność i uniwersalność. Wtedy dynamiczna reprezentacja obiektów-klas bardzo się przydaje.
Podstawowy programistyczny interfejs refleksji (Core Reflection API)
realizowany jest przez klasy pakietu java.lang.reflect
oraz
rozbudowaną klasę Class
z pakietu java.lang
.
Refleksja oznacza, że w trakcie wykonania programu możliwe są następujące działania:
Użycie refleksji pozwala m.in. na:
Uzywając mechanizmów refleksji do metod i pól odwołujemy się poprzez ich nazwy, a nie identyfikatory. Na czym polega różnica wobec statycznego przypadku?
W statyce odwołania są skonkretyzowane na etapie kompilacji. Piszemy np.
b.getText()
. I tak już zostanie na zawsze. W dynamice konstrukcja
jest całkiem inna - właśnie posługująca się nazwą metody. Piszemy raczej tak:
b.invokeMethod("getText")
. Tu invokeMethod()
jest
naszą własną metodą. Jako argument podajemy nazwę metody klasy, a ponieważ jest
to String
, możemy go zmieniać w każdym momencie wykonania programu.
Np. możemy napisać: b.invokeMethod(s)
, a kolejne podstawienia pod s
różnych nazw metod będzie zmieniać znaczenie tej linii programu. Właśnie w
invokeMethod()
używamy środków refleksji.
Klasy pakietu java.lang.reflect
Klasa | Przeznaczenie |
---|---|
Array | Tworzenie tablic, uzyskiwanie i ustalanie wartości elementów |
Constructor | Informacja i dostęp do danego konstruktora danej klasy. W szczególności wykorzystanie dla tworzenia obiektu. |
Field | Informacja i dostęp do pola obiektu. Pobranie i zmiana wartości pola. |
Method | Informacja o danej metodzie danej klasy. Dynamiczne wywołanie metody na rzecz danego obiektu. |
Modifier | Uzyskiwanie informacji o modyfikatorach składowej obiektu lub klasy. |
Użyteczne metody klasy java.lang.Class
Metoda | Przeznaczenie |
---|---|
getClasses() getDeclaredClasses() |
Zwraca tablicę obiektów klasy Class, które są składowymi danej klasy. |
getConstructors() getDeclaredConstructors() |
Zwraca tablicę obiektów klasy Constructor; są to konstruktory danej klasy |
getConstructor(Class[]) getDeclaredConstructor(Class[]) |
Zwraca obiekt konstruktor (obiekt klasy konstruktor), który ma podane typy argumentów |
getMethods() getDeclaredMethods() |
Zwraca tablicę, zawierającą odnośniki do metod klasy. Metody są obiektami klasy Method. |
getMethod(String, Class[]) getDeclaredMethod(String, Class[]) |
Zwraca metodę o podanej nazwie i podanych argumentach jako obiekt klsy Method. |
Rozróżnienie pomiędzy metodami mającymi i nie mającymi w nazwie tekstu
"Declared"
jest następujące:
Ważne zastosowanie znajduje refleksja w budowie uniwersalnych modeli danych. Zobaczymy to na przykładzei modelu danych dla tabeli, który może być zastosowane dla dwolnych tabel dowolnych obiektów. Dane są reprezentowane jako lista obiektów - każdy obiekt-element listy jest pokazywany w tabeli jako wiersz, a wartościami kolumn są wartości przekazanych przy konstrukcji modelu pól obiektu.
Program w języku Java to, w pewnym uproszczeniu, zbiór klas. Nie jest to jednak zbiór bezładny. Klasy pogrupowane są w tzw. pakiety. Klasy dzielimy na pakiety by pogrupować je według ich znaczenia, analogicznie do tego, jak dzielimy pliki na katalogi. Pakiety, podobnie jak katalogi, mają strukturę hierarchiczną, tj. każdy z pakietów może zawierać kolejne pakiety, podobnie jak katalogi mogą zawierać podkatalogi. Każdy pakiet, oprócz dowolnej liczby innych pakietów może zawierać także dowolną liczbę klas, podobnie do katalogu, który oprócz innych katalogów może zawierać dowolnie wiele plików.
com.example.mypackage
dla pakietu
mypackage
stworzonego przez programistę w domenie
example.com
. Pakiety z biblioteki Javy zaczynają się od
java.
lub javax.
Jeśli implementujemy klasę, to musimy jej kod źródłowy umieścić w osobnym
pliku, który to plik nazywa się dokładnie tak samo jak ta klasa i posiada
rozszerzenie .java
. Podobnie, jeśli deklarujemy, że klasa należy do
pewnego pakietu, to plik zawierający implementację tej klasy musi znajdować się
w katalogu odpowiadającym temu pakietowi. Przykładowo, jeśli plik zawierający
definicję klasy HelloWorld
znajduje się w katalogu o nazwie
naukajavy
, który to katalog z kolei znajduje się w katalogu o
nazwie pl
, tj. plik o nazwie HelloWorld.java
znajduje
się w katalogu pl/naukajavy
, to klasa ta należy do pakietu
pl.naukajavy
.
Implementując klasę musimy określić w jakim znajduje się ona pakiecie. W tym celu, na samym początku pliku umieszczamy instrukcję:
package nazwa_pakietu;
gdzie zamiast ciągu nazwa_pakietu
wpisujemy nazwę pakietu.
Przykładowo, implementacja klasy HelloWorld
z pakietu
pl.naukajavy
, tj. klasy pl.naukajavy.HelloWorld
wygląda następująco:
package pl.naukajavy;
public class HelloWorld {
public static void main(String[] args) {
System.out.println("Hello World!");
}
}
Kod ten umieszczamy w pliku pl/naukajavy/HelloWorld.java
.
Jeśli klasę umieszczamy w pakiecie domyślnym (tj. poza jakimkolwiek pakietem),
to deklarację pakietu w pliku - tj. instrukcję package
-
pomijamy.
Zdanie import
ma dwie podstawowe formy:
// import the Vector class from java.util package.
import java.util.Vector;
// import all the classes from java.util package
import java.util.*;
Pierwsza wersja importuje pojedynczą klasę (w tym przypadku
Vector
) z pakietu (np. java.util
), druga udostępnia
wszystkie klasy z wymienionego pakietu (ale nie podpakietów, które muszą być
importowane osobno).
Aby używać statycznych składowych musimy odpowiednie nazwy kwalifikować nazwą klasy, która je zawiera.
double r = Math.cos(Math.PI * theta);
static import
pozwala na niekwalifikowany dostęp do składowych
statycznych albo poszczególnych
import static java.lang.Math.PI;
albo wszystkich
import static java.lang.Math.*;
Wtedy możemy napisać
double r = cos(PI * theta);
static import
nie powinien być nadużywany. Jest dopuszczalny przy
częstym dostępie do statycznych składowych jednej lub dwóch klas. Jeżeli
program importuje dużo składowych z wielu klas to staje się nieczytelny i
przestrzeń nazw jest zanieczyszczona przez wszystkie zaimportowane składowe.
Szczególnie szkodliwy może być import "globalny". Lepiej jest zaimportować tylko
potrzebne składowe.
Singleton
jest kreacyjnym wzorcem projektowym, który:
Wszystkie implementacje wzorca Singleton
współdzielą poniższe dwa etapy:
Singleton
.Adapter jest strukturalnym wzorcem projektowym pozwalającym na współdziałanie ze sobą obiektów o niekompatybilnych interfejsach.
Załózmy, że nasza aplikacja pobiera dane z wielu źródeł w formacie XML. Postanawiamy wzbogacić aplikację poprzez dodanie biblioteki graficznej prezentacji danych. Ale jest haczyk: biblioteka ta działa wyłącznie z danymi w formacie JSON.
Aby rozwiązać dylemat niezgodności formatów, można stworzyć adapter XML-do-JSON. Potem zaś możemy dostosować kod tak, aby komunikował się z biblioteką wyłącznie za pomocą adaptera. Gdy adapter otrzyma wywołanie, tłumaczy przychodzące dane XML na strukturę JSON i przekazuje wywołanie dalej, do odpowiedniej metody opakowywanego obiektu biblioteki.
Adapter można rozpoznać po konstruktorze przyjmującym instancję innego typu abstrakcji/interfejsu. Gdy adapter otrzymuje wywołanie kierowane do którejś z jego metod, tłumaczy parametry wywołania do stosownego formatu i przekazuje je do jednej lub wielu metod opakowanego obiektu.
Metoda szablonowa to behawioralny wzorzec projektowy według którego definiuje się szkielet algorytmu w klasie bazowej i pozwala klasom pochodnym nadpisać poszczególne jego etapy bez zmiany ogólnej struktury.
Wyobraź sobie, że tworzysz aplikację zbierającą dane, która analizuje dokumenty w korporacji. Użytkownicy przesyłają do niej dokumenty w różnych formatach (PDF, DOC, CSV), a ta próbuje wydobyć z nich istotne dane i przedstawić je w jednym formacie.
Wszystkie klasy odpowiedzialne za przetwarzanie różnych typów dokumentów mają sporo podobnego kodu. Fragmenty odpowiedzialne za pracę z różnymi formatami danych są odmienne, ale kod odpowiedzialny za obróbkę i analizę danych jest niemal identyczny. Chcemy się tych powtórzeń pozbyć nie naruszając przy tym struktury algorytmów.
Dodatkowo nasz kod miał sporo instrukcji warunkowych służących wybieraniu odpowiedniego sposobu działania zależnie od klasy obiektu przetwarzającego. Jeśli wszystkie trzy klasy przetwarzające miałyby jeden wspólny interfejs lub wspólną klasę bazową, byłoby możliwe usunięcie instrukcji warunkowych w kodzie klienckim i zastosowanie polimorfizmu.
Mediator to behawioralny wzorzec projektowy pozwalający zredukować chaos zależności pomiędzy obiektami. Wzorzec ten ogranicza bezpośrednią komunikację pomiędzy obiektami i zmusza je do współpracy wyłącznie za pośrednictwem obiektu mediatora.
Piloci statków powietrznych zbliżający się do lotniska lub je opuszczający nie rozmawiają ze sobą nawzajem, lecz za pośrednictwem kontrolera lotów, siedzącego w wieży z widokiem na lądowisko. Bez kontrolera, piloci musieliby wiedzieć o każdym samolocie lub śmigłowcu w okolicy lotniska, dyskutować na temat pierwszeństwa lądowania. Mogłoby to niekorzystnie wpłynąć na statystyki bezpieczeństwa.
Wieża nie musi kontrolować całego lotu. Jej funkcja służy nakładaniu ograniczeń w obszarze terminala aby żaden z pilotów nie był przytłoczony dużą ilością aktorów biorących udział w funkcjonowaniu lotniska.
Wstrzykiwanie zależności jest techniką, która pozwala nam unikać silnych powiązań pomiędzy klasami. W Javie istotną rolę odgrywa tutaj korzystanie z interfejsów oraz zalet jakie daje nam polimorfizm.
Zalety
MessageService
i
Client
) nic tak naprawdę o sobie nie wiedzą.
Client
nic nie wie o implementacji, zna tylko
interfejs.Dependency Injection w Javie wymaga następujących elementów
Jedna z zasad projektowania obiektowego mówi, że "Klasy powinny być otwarte na rozszerzenia, ale zamknięte na modyfikacje".
Co to oznacza w praktyce?
Aby rozszerzyć funkconalność o kolejny typ komunikacji (np. wysłanie
komunikatu przez messengera) wystarczy dodać nową klasę implementującą
MessageService
i odpowiadający jej
MessageServiceInjector
(tworzący odpowiedni serwis i klienta).
Implementacja klasy ClientApplication
zostaje niezmieniona.
Strumień reprezenuje sekwencję elementów i umożliwia wykonywanie szeregu operacji na tych elementach
Niech będzie dana następująca definicja klasy BoardGame
:
public class BoardGame {
public final String name;
public final double rating;
public final BigDecimal price;
public final int minPlayers;
public final int maxPlayers;
public BoardGame(String name, double rating, BigDecimal price, int minPlayers, int maxPlayers) {
this.name = name;
this.rating = rating;
this.price = price;
this.minPlayers = minPlayers;
this.maxPlayers = maxPlayers;
}
}
Tworzymy listę obiektów tej klasy:
List<BoardGame> games = Arrays.asList(
new BoardGame("Terraforming Mars", 8.38, new BigDecimal("123.49"), 1, 5),
new BoardGame("Codenames", 7.82, new BigDecimal("64.95"), 2, 8),
new BoardGame("Puerto Rico", 8.07, new BigDecimal("149.99"), 2, 5),
new BoardGame("Terra Mystica", 8.26, new BigDecimal("252.99"), 2, 5),
new BoardGame("Scythe", 8.3, new BigDecimal("314.95"), 1, 5),
new BoardGame("Power Grid", 7.92, new BigDecimal("145"), 2, 6),
new BoardGame("7 Wonders Duel", 8.15, new BigDecimal("109.95"), 2, 2),
new BoardGame("Dominion: Intrigue", 7.77, new BigDecimal("159.95"), 2, 4),
new BoardGame("Patchwork", 7.77, new BigDecimal("75"), 2, 2),
new BoardGame("The Castles of Burgundy", 8.12, new BigDecimal("129.95"), 2, 4)
);
Naszym zadaniem jest wybrać te gry, które spełniają następujące warunki:
Podejście klasyczne:
for (BoardGame game : games) {
if (game.maxPlayers > 4) {
if (game.rating > 8) {
if (new BigDecimal(150).compareTo(game.price) > 0) {
System.out.println(game.name.toUpperCase());
}
}
}
}
Taka struktura ma swoją nazwę: Arrow Anti-Pattern. Jednym ze sposobów uniknięcia tego antywzorca może być użycie strumieni:
games.stream()
.filter(g -> g.maxPlayers > 4)
.filter(g -> g.rating > 8)
.filter(g -> new BigDecimal(150).compareTo(g.price) > 0)
.map(g -> g.name.toUpperCase())
.forEach(System.out::println);
Operacje związane ze strumieniami można podzielić na trzy rozłączne grupy:
Każdy strumień ma dokładnie jedną metodę, która go tworzy na podstawie danych źródłowych. Następnie dane te są przetwarzane przez dowolną liczbę operacji. Każda z tych operacji tworzy nowy strumień danych wywodzący się z poprzedniego. Na samym końcu strumień może mieć dokładnie jedną metodę kończącą pracę ze strumieniem.
Większość operacji na strumieniach musi być nieinterferująca
(non-interfering) i bezstanowa (stateless).
Funkcja jest nieinterferująca gdy nie modyfikuje podstawowego źródła danych
strumienia (w naszym przypadku listy games
).
Funkcja jest bezstanowa jeżeli wykonanie operacji jest deterministyczne, czyli
nie zależy od zewnętrznych zmiennych lub stanów, które mogą się zmieniać w
czasie wykonania.
Stream<Integer> stream1 = new LinkedList<Integer>().stream();
Stream<Integer> stream2 = Arrays.stream(new Integer[]{});
Stream<String> stream3 = Pattern.compile(".").splitAsStream("some longer sentence");
DoubleStream doubles = DoubleStream.of(1, 2, 3);
IntStream ints = IntStream.range(0, 123);
LongStream longs = LongStream.generate(() -> 1L);
DoubleStream randomDoubles = new Random().doubles();
IntStream randomInts = new Random().ints();
LongStream randomLongs = new Random().longs();
Stream.empty();
try (Stream<String> lines = new BufferedReader(new FileReader("file.txt")).lines()) {
// do something
}
Istotną cechą operacji pośrednich jest "laziness". W następującym przykładzie nie ma operacji terminalnej.
Stream.of("d2", "a2", "b1", "b3", "c")
.filter(s -> {
System.out.println("filter: " + s);
return true;
});
Powyższy fragment programu nie drukuje niczego, ponieważ operacje pośrednie są
wykonywane tylko w obecności operacji terminalnej. Jeżeli dodamy operację
terminalną (forEach
):
Stream.of("d2", "a2", "b1", "b3", "c")
.filter(s -> {
System.out.println("filter: " + s);
return true;
})
.forEach(s -> System.out.println("forEach: " + s));
to uzyskamy następujący wynik
filter: d2 forEach: d2 filter: a2 forEach: a2 filter: b1 forEach: b1 filter: b3 forEach: b3 filter: c forEach: c
Kolejność linii wydruku może być zasakująca. Można się spodziewać wykonania
kolejnych operacji na każdym elemencie strumienia i dopiero wtedy przejścia do
następnej operacji. Okazuje się jednak, że każdy element przesuwa się wzdłuż
łańcucha. Dopiero kiedy "d2" przejdzie operację filter
i
forEach
, strumień zaczyna obsługę elementu "a2". Takie podejście
może zmniejszyć liczbę operacji wykonywanych na każdym elemencie
Stream.of("d2", "a2", "b1", "b3", "c")
.map(s -> {
System.out.println("map: " + s);
return s.toUpperCase();
})
.anyMatch(s -> {
System.out.println("anyMatch: " + s);
return s.startsWith("A");
});
// map: d2
// anyMatch: D2
// map: a2
// anyMatch: A2
Operacja anyMatch
zwraca true
jak tylko napotka
pierwszy element spełniający dane kryterium. Dzięki 'pionowemu' wykonywaniu
operacji w strumieniu, map
działa tylko dla dwóch pierwszych
elementów.
Stream.of("d2", "a2", "b1", "b3", "c")
.map(s -> {
System.out.println("map: " + s);
return s.toUpperCase();
})
.filter(s -> {
System.out.println("filter: " + s);
return s.startsWith("A");
})
.forEach(s -> System.out.println("forEach: " + s));
// map: d2
// filter: D2
// map: a2
// filter: A2
// forEach: A2
// map: b1
// filter: B1
// map: b3
// filter: B3
// map: c
// filter: C
Jak widać map
i filter
zostały wykonane pięć razy (dla
każdego obiektu kolekcji), natomiast forEach
tylko raz.
Jednakże możemy zredukować liczbę operacji, zmieniając ich kolejność
Stream.of("d2", "a2", "b1", "b3", "c")
.filter(s -> {
System.out.println("filter: " + s);
return s.startsWith("A");
})
.map(s -> {
System.out.println("map: " + s);
return s.toUpperCase();
})
.forEach(s -> System.out.println("forEach: " + s));
// filter: d2
// filter: a2
// map: a2
// forEach: A2
// filter: b1
// filter: b3
// filter: c
Jak widać map
jest teraz wykonywane tylko raz. Taka optymalizacja
może być bardzo istotna dla dużej liczby elementów w strumieniu.
Stream.of("d2", "a2", "b1", "b3", "c")
.sorted((s1, s2) -> {
System.out.printf("sort: %s; %s\n", s1, s2);
return s1.compareTo(s2);
})
.filter(s -> {
System.out.println("filter: " + s);
return s.startsWith("A");
})
.map(s -> {
System.out.println("map: " + s);
return s.toUpperCase();
})
.forEach(s -> System.out.println("forEach: " + s));
Sortowanie jest specjalnym rodzajem operacji pośredniej. Jest to tak zwana operacja ze stanem, ponieważ w czasie sortowania kolekcji elementów, musimy pamiętać aktualny stan. Wyniki powyższego programu są następujące
sort: a2; d2 sort: b1; a2 sort: b1; d2 sort: b1; a2 sort: b3; b1 sort: b3; d2 sort: c; b3 sort: c; d2 filter: a2 map: a2 forEach: A2 filter: b1 filter: b3 filter: c filter: d2
Operacja sorted
jest wykonywana "poziomo", na całej kolekcji.
Wprowadzamy jeszcze jedną modyfikację optymalizyjącą
Stream.of("d2", "a2", "b1", "b3", "c")
.filter(s -> {
System.out.println("filter: " + s);
return s.startsWith("A");
})
.sorted((s1, s2) -> {
System.out.printf("sort: %s; %s\n", s1, s2);
return s1.compareTo(s2);
})
.map(s -> {
System.out.println("map: " + s);
return s.toUpperCase();
})
.forEach(s -> System.out.println("forEach: " + s));
// filter: d2
// filter: a2
// filter: b1
// filter: b3
// filter: c
// map: a2
// forEach: A2
Tym razem sorted
nie zostało wykonana, ponieważ filter
ograniczył liczbę elementów do jednego.
Strumienie nie mogą być wykorzystywane wielokrotnie. Po wywołaniu dowolnej operacji terminalnej strumień jest zamykany. Aby obejść to ograniczenie tworzymy nowy strumień dla każdej operacji terminalnej.
Supplier<Stream<String>> streamSupplier =
() -> Stream.of("d2", "a2", "b1", "b3", "c")
.filter(s -> s.startsWith("a"));
streamSupplier.get().anyMatch(s -> true); // ok
streamSupplier.get().noneMatch(s -> true); // ok
Każde wywołanie get()
tworzy nowy strumień, na którym możemy
wywoływać operacje terminalne.
Java nie została zaprojektowana z myślą o programowaniu funkcyjnym. Dlatego przetwarzanie danych wykorzystując streamy w większości przypadków jest mniej wydajne. Ten fakt musimy mieć na uwadze zwłaszcza przetwarzając duże zestawy danych. Jednak korzyścią płynącą z programowania funkcyjnego jest mniejsze zaangażowanie pamięci.
main = do
-- map:
-- anonymous functions are created with a backslash followed by
-- all the arguments.
putStrLn "\nmap"
print $ map (* 5) [1..5] -- [ 5, 10, 15, 20, 25]
print $ map (\x -> x*x) [1..5] -- [ 1, 4, 9, 16, 25]
-- filter: (according to some predicate)
putStrLn "\nfilter"
-- print odd
print $ [ x | x <- [2, 3, 4, 5, 8, 10, 11], (\y -> y `mod` 2 /= 0) x] -- [3, 5, 11]
-- print greater than 5
print $ [ x | x <- [2, 3, 4, 5, 8, 10, 11], (\y -> y > 5) x] -- [8, 10, 11]
-- filter & map together:
putStrLn "\nfilter & map"
print $ map (\x -> x*x) (filter (\x -> x `mod` 2 == 0 && x > 0) [0..6]) -- [4, 16, 36]
-- collect:
-- using fold with an anonymous function. foldl1 means fold left,
-- and use the first value in the list as the initial value for the accumulator.
putStrLn "\ncollect"
print $ foldl1 (\acc x -> acc + x) [1..5] -- 15
putStrLn ""