Posts Tagged Java

Poziom izolacji transakcji w JDBC

Dostępny interfejs dostępu do baz danych w Javie (JDBC) udostępnia kilka różnych poziomów izolacji poszczególnych transakcji. Pozwala na określenie, jak bardzo poszczególne transakcje mają być oddzielone od siebie. Niezależnie od poziomu izolacji transakcji, operacje takie jak wstawianie (INSERT), usuwanie (DELETE) oraz modyfikowanie (UPDATE) rekordów zachowują się zawsze tak samo, jedynie zachowanie operacji pobierającej (SELECT) dane z tabeli może być rożne.

Poziom transakcji jest ustawiany w ramach pojedynczego połączenia z bazą danych. Można go ustawia się go przy użyciu metody Connection.setTransactionIsolation(). Im wyższy poziom izolacji transakcji tym generalnie mniejsza wydajność operacji na bazie danych oraz większe prawdopodobieństwo wystąpienia blokad w dostępie do bazy danych.

Anomalie występujące w transakcjach

Poniżej znajduje się zestawienie anomalii, jakie mogą wystąpić w transakcjach:

Anomalie występujące w transakcjach
Nazwa anomalii Przykład działania anomalii
Dirty Reads

Mamy z nią do czynienia wtedy, gdy następuje w danej transakcji A odczyt zmodyfikowanych w transakcji B danych, a transakcja ta (B) nie została jeszcze zatwierdzona.

Transakcja A rozpoczyna się, i wywołuje:

SELECT * FROM zamowienia

Transakcja B w tym samym czasie wykonuje:

UPDATE zamowienie
SET wartosc_zamowienia=100
WHERE nr_zamowienia=123

Istnieje możliwość, że w transakcji A zostanie odczytana nowa wartość pola wartosc_zamowienia, nawet jeżeli nie zostanie to zapytanie zatwierdzone.

Non-Repeatable Reads

Ze zdarzeniem takim mamy do czynienia wtedy, gdy zapytania A wykonane w różnych momentach jednej transakcji może zwrócić inne wyniki (to samo zapytanie nie daje tego samego rezultatu). Zdarzyć się tak może, gdy w trakcie działania transakcji dane zostaną zmodyfikowane przez inną operacje na bazie danych.

Transakcja A rozpoczyna się, i wywołuje:

SELECT * FROM zamowienia
WHERE nr_zamowienia=123

Transakcja B w tym samym czasie wykonuje:

UPDATE zamowienie
SET wartosc_zamowienia=100
WHERE nr_zamowienia=123;

COMMIT;

Wykonanie ponowne zapytania w transakcji A spowoduje, że zostanie tym razem zwrócony zmodyfikowany rekord.

Phantom Reads

Z taką sytuacją mamy do czynienia wtedy, gdy transakcja A odczytuje dane z bazy danych, w trakcie jej trwanie transakcja B umieści w bazie danych rekord, który spełnia warunki zapytania z transakcji A. Jeżeli teraz w transakcji ponownie zostanie wykonane zapytanie, nowy rekord także zostanie zwrócony, co powoduje niezgodność wyniku działania dwóch zapytań w ramach tej samej transakcji.

Transakcja A odczytuje dane:

SELECT * FROM zamowienia
WHERE nr_zamowienia > 123

Transakcja B wstawia nowy wiersz do tabeli:

INSERT INTO zamowienia
(nr_zamowienia, nazwa)
VALUES ('140', 'Babol złapany');

COMMIT;

Jeżeli teraz w transakcji A ponownie wykonamy zapytanie, to nowy rekord także zostanie pobrany.

Poziomy transakcji

Rozróżniane są następujące poziomy transakcji:

  • TRANSACTION_READ_UNCOMMITTED

    Brak izolacji. Wszelkie zmiany, także te które nie są zatwierdzone jeszcze, są widoczne dla wszystkich zapytań w bazie danych.

  • TRANSACTION_READ_COMMITTED

    Minimalna izolacja, transakcje widzą tylko takie dane, które już są zatwierdzone i zapisane w bazie danych.

  • TRANSACTION_REPEATABLE_READ

    Poziom ten zapewnia powtarzalność odczytu danych. Jeżeli jakiś rekord zostanie odczytany, to nawet jego zatwierdzona modyfikacja w innej transakcji nie zmieni jego wartości przy ponownym odczycie.

  • TRANSACTION_SERIALIZABLE

    Najwyższy poziom transakcji, zwany szeregowym. Zapewnia on najlepszą izolację poszczególnych transakcji, które nie widzą swoich wzajemnych działań i nie wpływają na siebie. W momencie rozpoczęcia transakcji, stan bazy danych jest „zamrażany” i tylko na takim stanie transakcja może działać.

Lista anomalii jakie występują przy określonych typach transakcji, w zależności od sposobu blokowania danych:
Nazwa transakcji Blokada tabeli Blokada wiersza Wydajność
TRANSACTION_READ_UNCOMMITTED Możliwe: dirty reads, non-repeatable reads, phantom reads Możliwe: dirty reads, non-repeatable reads, phantom reads najszybsza
TRANSACTION_READ_COMMITTED Możliwe: non-repeatable reads i phantom reads Możliwe: non-repeatable reads i phantom reads szybka
TRANSACTION_REPEATABLE_READ Ponieważ blokowana jest cała tabela, to phantom reads nie są możliwe Możliwe są phantom reads średnia
TRANSACTION_SERIALIZABLE Brak anomalii Brak anomalii wolna

Źródła

Tags: , , , ,

Użycie słowa kluczowego break

Słowo kluczowe break może występować w dwóch formach:

  • forma samodzielna, bez etykiety
  • wskazanie etykiety, która ma zostać wywołania

Słowo kluczowe break bez etykiety

Samodzielne słowo break może zostać użyte tylko w kontekście pętli (for, while, do-while) oraz konstrukcji switch.

W przypadku pętli powoduje natychmiastowe przerwanie jej wykonywania i przejście do wykonywania nasŧpnej instrukcji po zakończeniu pętli. W przypadku wywołania polecenia wewnątrz wewnętrznej pętli, zostanie przerwane wykonywanie tylko pętli wewnętrznej.

1
2
3
4
5
6
7
8
9
10
11
12
for (int i = 0; i < 10; i++) {
    System.out.print("i = " + i);

    for (int j = i; j < i + 5; j++) {
        System.out.print("  j = " + j);
        if (j < 4) {
            break;
        }
    }

    System.out.println();
}

Użycie break w linii 7 w powyższym kodzie spowoduje przejście aplikacji do wykonywania kody w linii 11 (czyli ciągle wewnątrz pierwszej pętli).

Także break jest niezastąpione podczas używania konstrukcji switch:

1
2
3
4
5
6
7
8
9
10
11
12
13
int test = 1;

switch (test) {
default:
    System.out.println("Wartość domyślna");
case 0:
    System.out.println("Zero");
case 1:
    System.out.println("Jeden");
    break;
case 2:
    System.out.println("Dwa");
}

Co zostanie wydrukowane po uruchomieniu powyższego fragmentu kodu? Zaskoczenia żadnego nie będzie:

Jeden

Czyli zostanie wywołana linia 9, a następnie nastąpi opuszczenie całej konstrukcji switch.

Ale oto co zostanie wypisane na ekranie, jeżeli zmienimy przypisanie int test = 4:

Wartość domyślna
Zero
Jeden

Ponieważ wartość 4 nie ma żadnego przypadku opisanego w switch więc jest wykonywany blok wejścia default. I następnie kod jest wykonywany aż do pierwszego napotkanego słowa break lub też końca konstrukcji switch.

Słowo kluczowe break z etykietą

Istnieje także możliwość podania etykiety po słowie kluczowym break. Konstrukcja to może zostać użyta do zakończenia każdego bloku oznaczonego daną etykietą. Powoduje przejście wykonywania kodu do pierwszej instrukcji następującym po danym bloku kodu oznaczonym etykietą:

1
2
3
4
5
6
7
wyjscie: {
    int j =2;
    if (j == 2)
        break wyjscie;
    System.out.println("Blok danych z etykietą");
}
System.out.println("Koniec");

Po wykonaniu powyższego kodu zostanie wydrukowana tylko pojedyncza linia:

Koniec

Tę wersję konstrukcji break często używa się w połączeniu z zagnieżdżonymi pętlami, pozwala na opuszczenie całej ich struktury bez kłopotliwych testów na poszczególnych jej poziomach.

Tags: , , ,

Inicjalizacja zmiennych w pętli for

Definicja pętli for jest następująca:

for (<inicjalizacja>; <warunek pętli>; <wyrażanie zwiększające wartość>)
     <instrukcje do wykonania>

Chyba najczęściej używana forma instrukcji for wygląda tak:

1
2
3
4
5
6
for (int i = 0; i < 10; i++) {
        int k = 0;
        for (k = 0; k < i; k++) {
                // zrób coś
        }
}

Widać, że inicjalizacja zmiennej w pętli może zostać połączona także z jej deklaracją. Należy pamiętać, że jeżeli deklarację przeprowadzimy w części związanej z inicjalizacją, to zmienna taka będzie widziana tylko w ramach instrukcji for.

Nie trzeba ograniczać się do inicjalizacji tylko jednej zmiennej, można zrobić coś takiego:

1
2
3
for (int i = 0, k = 0; i < 3 && k < 1; i++, k++) {
        System.out.println("Pętla : i = " + i + ", k = " + k);
}

Kolejne inicjalizacje zmiennych powinny być rozdzielone przecinkami i wszystkie są tego samego typu. Nie jest dozwolona taka konstrukcja:

1
2
3
for (int i = 0, long k = 0; i < 3 && k < 1; i++, k++) {
    System.out.println("Pętla : i = " + i + ", k = " + k);
}

Można to ominąć, przenoszą deklarację zmienny poza pętlę for:

1
2
3
4
5
int i;
long k;
for (i = 0, k = 0; i < 3 && k < 1; i++, k++) {
    System.out.println("Pętla : i = " + i + ", k = " + k);
}

Jest także możliwa tak egzotyczna konstrukcja:

1
2
3
4
int i, k;
for (i = 0, k = 0, System.out.println("Petla for"); i < 3; i++) {
    System.out.println("Pętla : i = " + i + ", k = " + k);
}

Oczywiście, nie jest to zalecany sposób komunikowania, że rozpoczyna się pętla :).

Źródła

Tags: , , , ,

Definicja interfejsu w Javie

Poniżej znajduje się przykładowa definicja interfejsu napisana w Javie. Pierwsze pytanie brzmi, które elementy tej definicji są poprawne? Drugie, jak poszczególne (poprawne) elementy zachowają się podczas implementacji interfejsu?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
public interface InterfaceTest {

    // definicje pól
   
    int x1 = 1;
   
    public int x2 = 2;
   
    protected int x3 = 3;
   
    private int x4 = 4;
   
    static int x5 = 5;
   
    public static final int x6 = 6;
   
    final int x7 = 7;
   
   
    // metody
   
    int f1();
   
    public int f2();
   
    private int f3();
   
    protected int f4();
}

Poprawnie są zdefiniowane następujące pola: x1, x2, x5, x6, x7 oraz metody f1() i f2().

Błędne natomiast są definicje dla x3, x4 oraz f3() i f4(). Dlaczego?

Interfejs w praktyce definiuje nam zestaw pewnych publicznych metod, które zapewniają nam możliwość wywoływania określonych akcji na obiekcie. Z tego powodu w definicji interfejsu są dopuszczalne jedynie użycie modyfikatora public lub też jego brak (wtedy definicja także staje się publiczna). Inne przypadki są niedozwolone.

A teraz drugie pytanie, czy są jakieś różnice w dostępie do poszczególnych pól zdefiniowanych w interfejsie? Oto przykładowa klasa implementująca powyższy interfejs:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class Test implements InterfaceTest {

    @Override
    public int f1() {
        x1 = 0;
        x2 = 0;
        x5 = 0;
        x6 = 0;
        x7 = 0;
       
        return 0;
    }

    @Override
    public int f2() {
        return 0;
    }
}

Jak widać w klasie Test są zaimplementowane dwie metody f1() oraz f2(). W metodzie f1() następuje przypisanie nowej wartości zmiennym z interfejsu. Które przypisanie jest poprawne?

Odpowiedź: żadne. Dlaczego? Ponieważ definicje pól w interfejsie zawszą są finalne i statyczne.

Podsumowując, zasady jakie panują podczas definicji poszczególnych elementu interfejsu:

Pola interfejsu

  • mogą być tylko publiczne i tak są domyślnie definiowane
  • zawsze są statyczne
  • zawsze są finalne

Metody

  • mogą być tylko publiczne
  • nie mogą byś statyczne
  • nie mogą być finalne

Źródła

Tags: , ,

Wielokrotne rzutowanie typów prostych

Poniżej znajduje się prosta aplikacja, która rzutuje jedną liczbę między różnymi typami, tak że na wejściu mamy typ int, na wyjściu także.

1
2
3
4
5
public class Multicast {
    public static void main(String[] args) {
        System.out.println((int) (char) (byte) -1);
    }
}

I pytanie, co zostanie wydrukowane?.

Program jest dosyć prosty. Konwersja rozpoczyna się od cyfry -1, będącej typu int (domyślny typ dla liczb całkowitych). Następnie zostaje dokonana konwersja zawężająca do typu byte (8 bitów), potem do char (16 bitów) a na końcu z powrotem do typu int (32 bity). A na końcu drukuje liczbę 65535. I teraz pytanie brzmi, dlaczego nie -1?

Java wykorzystuje do prowadzenia obliczeń tzw. kod uzupełnień do dwóch:

Kod uzupełnień do dwóch (w skrócie U2 lub ZU2) – system reprezentacji liczb całkowitych w dwójkowym systemie pozycyjnym. Jest obecnie najpopularniejszym sposobem zapisu liczb całkowitych w systemach cyfrowych. Jego popularność wynika z faktu, że operacje dodawania i odejmowania są w nim wykonywane tak samo jak dla liczb binarnych bez znaku.

Z tej reprezentacji wynika, że liczba -1 w reprezentacji dwójkowej składa się z 32 jedynek. Rzutowanie z typu int to typu byte jest prosta i nie sprawia problemów. W wyniku tej konwersji zostają usunięte wszystkie bity oprócz tych najmniej znaczących. Czyli koniec końców także otrzymujemy liczbę -1 zapisane w 8 bitach (8 jedynek).

Natomiast rzutowanie na tym char sprawia więcej problemów. Typ char jest typem, w kŧórym można zapisać tylko liczby dodatnie, więc nie można zapisać w nim wartości ujemnej. Z tego powodu konwersja z typu char do byte nie jest uważana za konwersję rozszerzającą, tylko jest konwersja zawężająca. To powoduje, że najpierw nasŧępuje niejawna konwersja do typu int a dopiero teraz do typu char.

Więc najpierw następuje konwersja z typu byte na int, co powoduje, że uzyskujemy liczbę -1, która w zapisie dwójkowym składa się z 32 jedynek. Następnie dokonana zostaje konwersja do typu char, czyli zostają zachowane najmniej znaczące 16 bitów, czyli same jedynki czyli 65535.

Źródła

Tags: ,