Java

Operatory relacyjne

Operatory relacyjne | Kurs Java

Operatory relacyjne – Codziennie zdarza nam się coś porównywać. Może chcemy wiedzieć, czy to jabłko 🍎 jest bardziej czerwone od tego jabłka obok, a może zastanawiamy się, czy szafa jest tak samo szeroka jak kanapa 🛋️. W Javie również często dokonuję się porównań  – dzięki operatorom relacyjnym.

W tym materiale chcę Ci opowiedzieć o operatorach relacyjnych. Pokażę Ci, także jak w Javie porównuje się ciągi znaków.

Java – Operatory relacyjne – wprowadzenie

Z tego materiału dowiesz się:

  • Czym są operatory relacyjne?
  • Jakie są rodzaje operatorów relacyjnych?
  • Jak porównuje się obiekty np. String’i?

Java – Operatory relacyjne

Operatory relacyjne służą do porównania ze sobą dwóch zmiennych lub literałów.
Java posiada operatory relacyjne takie jak:

  • operator równości,
  • operator nierówności,
  • operator większy, bądź równy,
  • operator mniejszy, bądź równy,
  • operator większy niż,
  • operator mniejszy niż.

⚠️Przy porównaniach należy jednak pamiętać o typie porównywanych danych. Powinien on być taki sam dla obu argumentów lub ich typy powinny być ze sobą zgodne, przykładowo można porównać liczbę 2.5 typu float z liczbą 2 typu int, ale nie da się już porównać ich ze zmienną typu boolean. Dostalibyśmy wtedy wyjątek kompilacji.⚠️

Operator równości

Operatora równości „==” używamy, aby porównać wartości po obu stronach. Jeśli są one równe, operacja zwraca wartość true.

boolean v1 = 1==2; // v1 = false
boolean v2 = 2==2; // v2 = true

⚠️ Należy pamiętać, że operator równości powinno się stosować do porównywania typów prostych lub enumów. Porównując obiekty operator „==” porównuje referencję obiektów (miejsce w pamięci w komputera), a nie same obiekty. Chcąc porównać obiekty, należy nadpisać metodę equals() z klasy Object. ⚠️

User user = new User("Tomek","Wolinski");
User user1 = new User("Tomek","Wolinski");
boolean isEqualUser = user == user1 // false

Porównywanie ciągów znaków

W przypadku klasy String odpowiedzialnej, za przechowywanie ciągu znaków, również należy zastosować metodę equals. Klasa String jest klasą obiektową i posiada nadpisaną już metodę equals, więc możemy ją od razu użyć, aby porównać string’i.

boolean v1 = "Hello World".equals("Ala ma kota"); // v1 = false

String text = "Hello World!";
boolean v2 = text.equals("Ala ma kota"); // v2 = false

➡ ZOBACZ 👉: String – najważniejszy typ danych

Operator nierówności

Operator nierówności to „!=” i wykonuje operację odwrotną do operatora równości. Jeśli wartości po obu stronach nie są równe, operacja zwraca wartość true.

boolean v3 = 1!=2; //true

Operator większy bądź równy

Operator większy lub równy „>=” porównuje wartości po obu stronach i zwraca true, jeśli lewa wartość jest większa lub równa prawej stronie.

boolean v5 = 1>=2; //false

Operator mniejszy bądź równy

Operator większy lub równy „<=” porównuje wartości po obu stronach i zwraca true, jeśli lewa wartość jest mniejsza lub równa prawej stronie.

boolean v4 = 1<=2; //true

Operator większy niż

Porównując dwie wartości za pomocą operatora „większy niż” „>”, otrzymujemy true, jeśli wartość po lewej stronie jest większa od wartości po prawej stronie.

boolean v6 = 1>2; //false

Operator mniejszy niż

Porównując natomiast dwie wartości za pomocą operatora „mniejszy niż” „<”, otrzymujemy true, jeśli wartość po lewej stronie jest mniejsza od wartości po prawej stronie.

boolean v6 = 1<2; //true

Java – Operatory relacyjne – podsumowanie

W ramach tego materiału dowiedzieliśmy się, czym są operatory relacyjne. Bliżej zapoznaliśmy się z rodzajami operatorów relacyjnych i poznaliśmy sposób na porównanie typów obiektowych. Jeżeli chcesz kontynuować swoją przygodę z Javą i poznać inne struktury, które oferuję ten język programowania – to zapraszam do kolejnego tematu z serii o Javie. Temat będzie rozszerzeniem zagadnienia operatorów matematycznych, ponieważ pomówimy bardziej szczegółowo o inkrementacji.

➡ ZOBACZ 👉: Inkrementacja

Kierunek Java

W serii o Javie zapoznajesz się z podstawowymi tematami o Javie. Jeżeli chcesz bardziej kompleksowo zagłębić się w temat Javy, poczytać, posłuchać o Javie, to zachęcam Cię do zapoznania się z moim kursem „Kierunek Java”:

➡ ZOBACZ 👉: Kierunek Java

kierunek java

No comments
Share:
String – konwertowanie i zamiana typów

String – konwertowanie i zamiana typów: Array, ArrayList, Char, Int, Integer

Konwertowanie i zamiana typów to fragment artykułu na temat klasy String. W tekście zebrane zostały fragmenty kodu pokazujące, w jaki sposób zamienić jeden typ danych na inny.

String to array

  1. Zamianę stringa na tablicę stringów można zrobić przy pomocy metody split.
    String str = "J a v a";
    String[] actualStringArray = str.split(" ");
    String[] expectedStringArray = {"J", "a", "v", "a"};
    assertArrayEquals(expectedStringArray, actualStringArray);
    
  2. Zamiana stringa na tablicę charów.
    str = "Java";
    char[] actualCharArray = str.toCharArray();
    char[] expectedCharArray = {'J', 'a', 'v', 'a'};
    assertArrayEquals(expectedCharArray, actualCharArray);

Array to String

  1. Zamiana przy pomocy metody: Arrays.toString
    String[] array = {"a", "b", "c"};
    String actualValue = Arrays.toString(array);
    String expectedValue = "[a, b, c]";
    assertEquals(expectedValue, actualValue);
  2. Zamiana przy pomocy metody: String.join
    actualValue = String.join(", ", array);
    expectedValue = "a, b, c";
    assertEquals(expectedValue, actualValue);
  3. Zamiana za pomocą Java Stream
    actualValue = Stream.of(array).collect(Collectors.joining(", "));
    expectedValue = "a, b, c";
    assertEquals(expectedValue, actualValue);
  4. Zamiana z wykorzystaniem zwykłej pętli
    actualValue = "";
    for (int i=0; i<array.length; i++) {
        actualValue += array[i];
        if(i!=array.length-1){
            actualValue += ", ";
        }
    }        
    expectedValue = "a, b, c";
    assertEquals(expectedValue, actualValue);
  5. Dla zagnieżdżonych tabel można wykorzystać metodę: Arrays.deepToString
    String[][] deepArray = {{"a"}, {"b"}, {"c"}};
    actualValue = Arrays.deepToString(deepArray);
    expectedValue = "[[a], [b], [c]]";
    assertEquals(expectedValue, actualValue);

Char array to String

  1. Zamiana tablicy znaków (charów) na stringa przez konstruktor klasy String
    char[] charArray = {'J', 'a', 'v', 'a'};
    
    String actualValue =  new String(charArray);
    String expectedValue = "Java";
    assertEquals(expectedValue, actualValue);
  2. Zamiana przy pomocy metody: String.copyValueOf
    actualValue = String.copyValueOf(charArray);
    expectedValue = "Java";
    assertEquals(expectedValue, actualValue);
  3. Zamiana przy pomocy metody: String.valueOf
    actualValue = String.valueOf(charArray);
    expectedValue = "Java";
    assertEquals(expectedValue, actualValue);
  4. Zamiana przy pomocy metody: Arrays.toString
    actualValue = Arrays.toString(charArray);
    expectedValue = "[J, a, v, a]";
    assertEquals(expectedValue, actualValue);

Array to ArrayList

  1. Zamiana za pomocą metody: Arrays.asList
    String[] array = {"J", "a", "v", "a"};
    List<String> actualValue = Arrays.asList(array);
    List<String> expectedValue = Arrays.asList(new String[]{"J", "a", "v", "a"});
  2. Zamiana za pomocą konstruktora i metody: Arrays.asList
    actualValue = new ArrayList<>(Arrays.asList(array));
    
  3. Zamiana przez dodanie wszystkich elementów przy pomocy metody: Collections.addAll
    actualValue = new ArrayList<>();
    Collections.addAll(actualValue, array);
  4. Zamiana przez Java stream I
    actualValue = Stream.of(array).collect(Collectors.toCollection(ArrayList::new));
  5. Zamiana przez Java stream II
    actualValue = Stream.of(array).collect(Collectors.toList());

ArrayList to array

  1. Zamiana przy pomocy metody: toArray tworzy tablicę z referencjami typu Object
    List<String> list = Arrays.asList("J", "a", "v", "a");
    String[] expectedValue = {"J", "a", "v", "a"};
    
    Object[] actualValue = list.toArray();
    Assert.assertArrayEquals(expectedValue, actualValue);
  2. Jeżeli do metody toArray przekażemy odpowiednią tablicę, zostanie ona uzupełniona wartościami z listy. W tym wypadku otrzymujemy tablicę referencji typu String.
    String[] actualValue = list.toArray(new String[list.size()]);
  3. Do metody można również przekazać pustą tablicę
    actualValue = list.toArray(new String[0]);
  4. Zamiana z wykorzystaniem Java stream
    actualValue = list.stream().toArray(String[]::new);
  5. Zamiana przy pomocy zwykłej pętli
    actualValue = new String[list.size()];
    for (int i = 0; i < list.size(); i++) {
     actualValue[i] = list.get(i);
    }

Int to String

  1. Zamiana przez statyczną metodę: Integer.toString
    Integer i = 123;
    String actualValue = Integer.toString(i);
    String expectedValue = "123";
    assertEquals(expectedValue, actualValue);
  2. Zamiana przez statyczną metodę: String.valueOf
    actualValue = String.valueOf(i);
  3. Zamiana przez metodę: toString na obiekcie typu Integer
    actualValue = i.toString();
  4. Zamiana przez dodanie stringa
    actualValue = "" + i;

String to int

  1. Parsując string przy pomocy statycznej metody: Integer.valueOf otrzymamy obiekt typu Integer. Dla nierozpoznanych liczb zostanie wyrzucony wyjątek: NumberFormatException.
    String str = "123";
    Integer valueOf = Integer.valueOf(str);
    assertEquals(Integer.valueOf(123), valueOf);
  2. Metoda Integer.decode działa bardzo podobnie do valueOf
    Integer decode = Integer.decode(str);
    assertEquals(Integer.valueOf(123), decode);
  3. Parsując stringa przy pomocy statycznej metody: Integer.parseInt otrzymamy typ prosty int. Podobnie jak w przypadku metody valueOf można otrzymać wyjątek: NumberFormatException.
    int parseInt = Integer.parseInt(str);
    assertEquals(123, parseInt);
  4. Podczas parsowania stringów należy uważać na wyjątek: NumberFormatException. Taką sytuację można jednak obsłużyć z wykorzystaniem klasy Optional
    Optional<Integer> maybeInteger;
    try {
        maybeInteger= Optional.of(Integer.valueOf(str));
    } catch (NumberFormatException e) {
        maybeInteger = Optional.empty();
    }

     

 

kierunek java

No comments
Share:
StringBuilder vs StringBuffer vs String

StringBuilder: czy zawsze taki szybki? | String vs StringBuilder vs StringBuffer

StringBuilder

Operacje na ciągach znaków występują praktycznie w każdej aplikacji, dlatego są jedną z pierwszych umiejętności, jakie zdobywają młodzi programiści.

Jednak co dobrze sprawdza się w prostej aplikacji w stylu: Hello World, w bardziej skomplikowanym projekcie, przy dużym obciążeniu, może już przysporzyć nam nie lada problemów.

Czas poszerzyć arsenał swoich umiejętności o nową klasę: StringBuilder, która jest uważana za panaceum na problemy wydajnościowe ze stringami. Tylko czy aby na pewno jest to zawsze najlepsze wyjście?

Niezmienność [Immutability]

Standardowa klasa String jest niemodyfikowalna, w efekcie czego każda zmiana stringa pociąga za sobą konieczność utworzenia nowego obiektu.

String value = "Java";
value = value + " Rocks";

Powyższy kod wymaga utworzenia nowego obiektu zawierającego połączone stringi i przypisania go do zmiennej: value . Przy tak małej skali nie stanowi to problemu, jednak gdyby kod był wykonywany wielokrotnie w jakimś newralgicznym punkcie aplikacji, mogłoby to znacząco wpłynąć na spadek wydajności.

➡ ZOBACZ 👉Immutable – niezmienne obiekty

Niezmienność obiektów sama w sobie nie jest zła, wręcz przeciwnie, nawet często zaleca się korzystanie z takich rozwiązań. Jednak zgodnie z zasadą złotego młotka, nie ma rozwiązań w 100% uniwersalnych i należy dobrać odpowiednie narzędzia do problemu, który chcemy rozwiązać.

Dzięki obiektom typu immutable możemy między innymi:

  • poprawić wydajność aplikacji, ponieważ łatwiej jest je keszować;
  • zwiększyć bezpieczeństwo aplikacji, ponieważ mamy pewność, że wykorzystywany obiekt nie ulegnie zmianie podczas działania aplikacji;
  • można je bezpiecznie wykorzystywać jako klucze np. w mapach.

StringBuilder

Klasa StringBuilder powstała jako rozszerzenie funkcjonalności oferowanych przez tradycyjnego stringa. W przeciwieństwie do pierwowzoru StringBuilder jest klasą modyfikowalną, czyli jej obiekty mogą się rozszerzać i zmieniać swoją zawartość. Dzięki temu można łączyć ze sobą różne fragmenty tekstu, bez konieczności generowania wielu niepotrzebnych obiektów.

Obiekty klasy StringBuilder (StringBuffer też, ale o tym za chwilę) to swego rodzaju bufory, które pozwalają na dynamiczną modyfikację ciągów znaków (napisów).

StringBuilder sb = new StringBuilder();
sb.append("Java");
sb.append(' ');
sb.append("Rocks").append("!").append(0);
String value = sb.toString(); // value => "Java Rocks!0"

Powyższy przykład demonstruje, w jaki sposób można korzystać ze StringBuildera.

  1. Pierwsza linijka to utworzenie pustego bufora. StringBuilder jako argument może też przyjąć tekst (wtedy zostanie automatycznie nim zainicjowany) lub liczbę (wtedy rozmiar bufora zostanie ustawiony na podaną wartość).
  2. Druga linijka to modyfikacja bufora przez metodę append. Metoda append modyfikuje obiekt, na którym jest wywoływana oraz zwraca dokładnie ten sam obiekt.
  3. Trzecia linijka dodaje pojedynczy znak spacji. Metoda append jest przeciążona, dzięki czemu można dodawać przy jej pomocy: stringi, pojedyncze znaki, zmienne boolean, liczby oraz dowolne inne obiekty – wtedy zostanie wywołana na nich metoda toString().
  4. W czwartej linii pokazane jest, w jaki sposób można tworzyć łańcuchy wywołań. Dzięki temu, że metoda append zwraca obiekt this, takie wywołanie jest jednoznaczne z rozbiciem tego na pojedyncze linie.
  5. Ostatnia linia pobiera zbuforowany tekst i zapisuje go w zmiennej typu String.

Najważniejsze metody klasy StringBuilder

  • StringBuilder append(String str) 
    Wykorzystywana jest do dodawania nowego elementu na końcu bufora. Dzięki przeciążeniu metody można przy jej pomocy dodać praktycznie dowolną wartość.

    String actualValue = sb.append("1").append('1').append(1).toString();
    String expectedValue = "111";
    
  • StringBuilder insert(int offset, String str)
    Przy jej pomocy można wstawić element w buforze na konkretnej pozycji. Wszystkie elementy znajdujące się dalej zostaną przesunięte. Metoda insert, podobnie jak append, jest przystosowana do obsługi wszystkich typów. Należy uważać, żeby podany indeks nie przekroczył zakresu istniejącego bufora, bo dostaniemy wyjątek: IndexOutOfBoundsException.

    sb.insert(0, "Java Rocks");
    String actualValue = sb.insert(4, 8).toString();
    String expectedValue = "Java8 Rocks";
    
  • StringBuilder replace(int start, int end, String str)
    Metoda wykorzystywana jest do zamiany wskazanego przedziału w buforze na inny ciąg znaków. W przypadku podania błędnego indeksu wyrzuci: IndexOutOfBoundsException.

    sb.replace(0, 0, "Java 8 Rocks");
    String actualValue = sb.replace(0, 6, "Java7").toString();
    String expectedValue = "Java7 Rocks";
    
  •  StringBuilder delete(int start, int end)
    Przy jej pomocy można usunąć z bufora ciąg znaków o podanym zakresie.

    StringBuilder sb = new StringBuilder("Java8 Rocks");
    String actualValue = sb.delete(4, 6).toString();
    String expectedValue = "JavaRocks";
    
  • StringBuilder reverse()
    Metoda reverse odwraca ciąg znaków znajdujący się w buforze.

    StringBuilder sb = new StringBuilder("Java Rocks"); 
    String actualValue = sb.reverse().toString();
    String expectedValue = "skcoR avaJ";
    
  • int capacity()
    Metoda capacity zwraca aktualny rozmiar bufora.
  • void ensureCapacity(int minimumCapacity)
    Przy jej pomocy można wymusić ustawienie minimalnego wymaganego rozmiaru bufora. Jeżeli nie zrobimy tego ręcznie, a skończy się aktualny rozmiar bufora, StringBuilder i tak go sam powiększy.
  • int length()
    Zwraca aktualny rozmiar przechowywanego ciągu znaków. Metoda length zawsze będzie zwracała mniejszą lub równą wartość dla metody capacity.
  • char charAt(int index)
    Metoda zwraca znak znajdujący się pod wskazanym indeksem w buforze.

    StringBuilder sb = new StringBuilder("Java Rocks");
    char actualValue = sb.charAt(1);
    char expectedValue = 'a';
    
  • String substring(int start, int end)
    Metoda substring zwraca wybrany fragment ciągu znaków z bufora.

    StringBuilder sb = new StringBuilder("Java Rocks");
    String actualValue = sb.substring(0, 4);
    String expectedValue = "Java";
    

Rozszerzalność bufora

Klasa StringBuilder opiera swoje działanie na implementacji wewnętrznego bufora. To właśnie dzięki niemu nie musimy się przejmować, czy dodawane przez nas znaki zmieszczą się, czy nie.

W celu poprawy wydajności możemy co prawda sterować rozmiarem bufora, np. dzięki metodzie ensureCapacity lub konstruktorowi klasy przyjmującemu początkowy rozmiar, jednak nie jest to konieczne.

Na przykładzie poniżej pokażę, w jaki sposób zwiększany jest rozmiar bufora podczas dodawania nowych elementów.

// 1
StringBuilder sb = new StringBuilder(3);
assertEquals(3, sb.capacity());
assertEquals(0, sb.length());
// 2
sb.append("1");
assertEquals(3, sb.capacity());
assertEquals(1, sb.length());
// 3
sb.append("23");
assertEquals(3, sb.capacity());
assertEquals(3, sb.length());
// 4
sb.append("4");
assertTrue(sb.capacity() > 4);
assertEquals(4, sb.length());
  1. Krok Pierwszy
    Na początku tworzymy nowy obiekt, ustawiając jednocześnie rozmiar bufora na 3. Bufor jest jeszcze pusty, dlatego metoda length zwraca 0.
    Zainicjowanie buildera bez podania argumentu lub podając ciąg znaków, ustawi rozmiar wewnętrznego bufora na długość podanego stringa + domyślny rozmiar bufora (w przypadku mojej implementacji Javy jest to 16).
  2. Krok Drugi
    W tym kroku dodajemy pierwszy element do bufora. Ponieważ ilość elementów bez problemu mieści się jeszcze w buforze, nie było potrzeby jego rozszerzania.
  3. Krok Trzeci
    W kolejnym kroku dodajemy 2 nowe elementy. Teraz bufor jest już pełny, czyli capacity i length są sobie równe.
  4. Krok Czwarty
    Dodanie kolejnego elementu do bufora wymusiło już jego powiększenie. Ilość elementów w buforze to 4, a jego rozmiar został tak zwiększony, by mógł przyjąć więcej nowych elementów.
    Rozszerzenie bufora polega na utworzeniu nowego bufora o rozmiarze: 2x[rozmiar aktualnego bufora]+2 oraz przekopiowaniu do niego zawartości starego bufora.

StringBuffer vs StringBuilder

Prawie wszystkie wnioski odnośnie klasy StringBuilder pasują również do klasy StringBuffer. Te dwie klasy różnią się przede wszystkim tym, że StringBuffer posiada metody synchronizowane a  StringBuilder nie.

Dzięki synchronizowanym metodom klasa StingBuffer nadaje się do programowania współbieżnego, gdzie niektóre metody mogą być wywoływane z równolegle pracujących wątków. Niesie to ze sobą pewne ograniczenia wydajnościowe, dlatego jeżeli nie piszemy aplikacji wykorzystującej równoległe wątki, lepiej zostać przy standardowej wersji bez synchronizacji.

Testy wydajnościowe: StringBuilder vs StringBuffer vs String

Testy wydajnościowe: StringBuilder vs StringBuffer vs String

Testy wydajnościowe
[StringBuffer vs StringBuilder vs String]

Wśród wielu programistów panuje przekonanie, że konkatenacja stringów jest wolna, zła i powinno się jej unikać na korzyść StringBuffera.

W poniższych testach postaram się pokazać, że nie zawsze jest to prawda. Łączenie stringów przy pomocy StringBuffera rzeczywiście może być wielokrotnie szybsze, jednak tylko przy zachowaniu konkretnych warunków.

Na potrzeby testu przygotowałem 3 fragmenty kodu odpowiedzialne za łączenie stringów za pomocą: standardowej konkatenacji stringów (znaczek +), StringBuilder oraz StringBuffera.

W celu symulacji rzeczywistych sytuacji będziemy sterować dwoma parametrami testów:

  • ilość ciągów znaków, które zostaną połączone w ramach jednego testu (zmienna 'n’ z przykładu) – czyli jak dużo jest konkatenacji w ramach jednego wywołania;
  • ilość wywołań całej metody – czyli jak dużo razy wywoływana jest metoda zawierająca konkatenacje.
String v = "";
for (int i = 0; i < n; i++) {
    v += "a";
}
StringBuilder sb = new StringBuilder();
for (int i = 0; i < n; i++) {
    sb.append("a");
}
StringBuffer sb = new StringBuffer();
for (int i = 0; i < n; i++) {
    sb.append("a");
}

Wyniki poszczególnych testów (w milisekundach) podane w tabeli poniżej.

Test 1: Łączenie małej ilości stringów (10)

Test polega na łączeniu ze sobą 10 prostych stringów.

Iteracje StringBuilder StringBuffer String
10 1 1 1
1000 1 1 1
10000 4 4 3

Przy tak małej ilości konkatenacji widać, że czasy dla wszystkich sposobów są bardzo zbliżone, a nawet standardowa klasa String wypadła minimalnie szybciej.

Test 2: Łączenie średniej ilości stringów (1000)

Test polega na łączeniu ze sobą 1000 prostych stringów.

Iteracje StringBuilder StringBuffer String
10 1 1 2
1000 9 13 128
10000 63 73 1317

Podczas łączenia 1000 stringów ze sobą StringBuilder wysunął się już na prowadzenie. Szczególnie dużą różnicę widać w porównaniu do zwykłego stringa.

Test 3: Łączenie dużej ilości stringów (10 000)

W teście z dużą ilością stringów różnice pogłębiły się jeszcze bardziej.

Dla tak dużej ilości konkatenacji wykorzystanie zwykłego stringa stanowi już poważny problem wydajnościowy.

Pojawiły się też różnice miedzy StringBuilderem a StringBufferem wynikające z synchronizowania metod. Stąd wniosek, że jeżeli nie wykorzystujemy programowania wielowątkowego, to poiwinniśmy zostać przy wykorzystaniu StringBuildera.

Iteracje StringBuilder StringBuffer String
10 1 1 149
1000 92 65 12328
10000 648 810 124743

Test 4: Wielokrotne uruchamianie prostych konkatenacji

Ten test polega na wielokrotnym uruchomieniu bardzo prostej konkatenacji. Może to symulować np. budowanie komunikatu do loga w bardzo często wykorzystywanej metodzie.

String value = "a"+"b";
StringBuilder sb = new StringBuilder();
sb.append("a").append("b");
Iteracje String StringBuilder
1000000 5 26
1000000000 7 10985

Dla tego przypadku klasa StringBuilder została bardzo daleko w tyle. Pokazuje to tylko tyle, że nie zawsze wykorzystanie „lepszych” mechanizmów przynosi zamierzony skutek.

Test 5: Wykorzystanie ensureCapacity dla StringBuildera

Ten test miał na celu pokazanie, jak duży wpływ na wydajność może mieć prawidłowe wykorzystanie metody ensureCapacity,  jeżeli z góry znamy, przynajmniej przybliżony, docelowy rozmiar wynikowego ciągu znaków.

Konkatenacje Iteracje Czas z ensureCapacity Czas z domyślnymi ustawieniami
10 10000 3 4
1000 10000 57 63
10000 10000 568 810

Dzięki wykorzystaniu metody ensureCapacity rzeczywiście udało się osiągnąć kilkudziesięciu procentową poprawę wydajności, jednak nie są to już tak spektakularne różnice, jak w przypadku poprzednich testów.

Dlaczego tak wolno?

W tym miejscu trzeba się przyjrzeć, co dokładnie Java robi podczas wywoływania standardowej konkatenacji stringów, bo jest to dosyć zaskakujące.

Java podczas kompilacji kodu źródłowego do bytecode zamienia konkatenację na wywołanie klasy StringBuilder!

Można to zweryfikować np. przy wykorzystaniu narzędzia: javap:

public class Concatenation {

    public void stringConcatenation() {
        String value = "a";
        value += "b";
    }

    public void StringBuilderConcatenation() {
        StringBuilder sb = new StringBuilder("a");
        sb.append("b");
        String value = sb.toString();
    }
}
# javap -c Concatenation.class 
Compiled from "Concatenation.java"
public class pl.stormit.stringbuilder.Concatenation {
  public pl.stormit.stringbuilder.Concatenation();
    Code:
       0: aload_0
       1: invokespecial #1                  // Method java/lang/Object.<init>":()V
       4: return

  public void stringConcatenation();
    Code:
       0: ldc           #2                  // String a
       2: astore_1
       3: new           #3                  // class java/lang/StringBuilder
       6: dup
       7: invokespecial #4                  // Method java/lang/StringBuilder."<init>":()V
      10: aload_1
      11: invokevirtual #5                  // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
      14: ldc           #6                  // String b
      16: invokevirtual #5                  // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
      19: invokevirtual #7                  // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
      22: astore_1                                                                                                                                                                                                   
      23: return                                                                                                                                                                                                     
                                                                                                                                                                                                                     
  public void StringBuilderConcatenation();                                                                                                                                                                          
    Code:                                                                                                                                                                                                            
       0: new           #3                  // class java/lang/StringBuilder                                                                                                                                         
       3: dup                                                                                                                                                                                                        
       4: ldc           #2                  // String a
       6: invokespecial #8                  // Method java/lang/StringBuilder."<init>":(Ljava/lang/String;)V
       9: astore_1
      10: aload_1
      11: ldc           #6                  // String b
      13: invokevirtual #5                  // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
      16: pop
      17: aload_1
      18: invokevirtual #7                  // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
      21: astore_2
      22: return
}

Niewinnie wyglądający '+’ wymusza utworzenie tymczasowego obiektu StringBuilder z nową wartością i przekonwertowanie go znowu do stringa przy pomocy metody toString().

Skoro pod spodem zawsze jest taki sam bufor, to o co to całe zamieszanie i skąd te różnice?
Różnice wynikają z drobnych, wydawałoby się, szczegółów, takich jak inicjalny rozmiar bufora oraz to, czy będzie on wykorzystywany między wywołaniami pętli, czy za każdym razem tworzony od nowa.

Wady klasy String

Konkatenacja przy pomocy stringa za każdym razem wymaga utworzenia tymczasowego obiektu StringBuildera i  ponownej jego zmiany do stringa. Dlatego to rozwiązanie absolutnie nie nadaje się, jeżeli chcemy łączyć ze sobą dużą ilość ciągów znaków.

Wady klasy StringBuilder

Domyślny rozmiar bufora nie zawsze jest odpowiedni, jednak jeżeli znamy docelowy rozmiar stringa, możemy dostosować go do naszych potrzeb.

Wnioski

W powyższym tekście pokazałem, w jaki sposób można zoptymalizować podstawowe operacje na ciągach znaków.

Jednak niezależnie od przedstawionych tutaj przykładów zachęcam do przeprowadzania testów samodzielnie i weryfikacji niektórych założeń. Może bowiem okazać się, że problem tkwi nie tam, gdzie się go pierwotnie spodziewaliśmy. Warto również pamiętać, że nawet drobne różnice, jeżeli trafią na podatny grunt (np. wielokrotne wywołanie metody), mogą przyczynić się do dużych problemów.

 

kierunek java

2 komentarze
Share:
Immutables

Immutables – niemodyfikowalne obiekty na sterydach

Immutables to wszechstronne narzędzie do pracy z klasami typu: value object. Biblioteka pozwala uniknąć pisania oraz utrzymywania powtarzalnego i niewiele wnoszącego kodu (boilerplate code). Jej głównym celem jest realizacja reguły DRY (ang. Don’t Repeat Yourself, pol. Nie powtarzaj się), co naprawdę całkiem fajnie zrealizuje.

Wprowadzenie do Immutables

Immutables działa jako standardowy procesor adnotacji w obrębie kompilatora javac. W celu wygenerowanie klasy Immutables wystarczy dodać adnotację: @Value.Immutable. Na podstawie informacji z bazowej klasy procesor adnotacji podczas kompilacji utworzy pełną implementację nowej klasy o nazwie: Immutable + [nazwa klasy bazowej].

➡ ZOBACZ 👉: Immutable – niezmienne obiekty

@Value.Immutable
public abstract class UserValueImmutable {

abstract String getName();
abstract Integer getAge();
}
UserValueImmutable user = ImmutableUserValueImmutable.builder()
.name("Tomasz")
.age(100)
.build();
//-no-import-rewrite
package UserValue;

import java.lang.Object;
import java.lang.String;
import java.lang.Float;
import java.lang.Double;

/**
* Immutable implementation of {@link UserValueImmutable}.
*

* Use the builder to create immutable instances:
* {@code ImmutableUserValueImmutable.builder()}.
*/
@SuppressWarnings({"all"})
@javax.annotation.Generated({"Immutables.generator", "UserValueImmutable"})
public final class ImmutableUserValueImmutable extends UserValue.UserValueImmutable {
private final java.lang.String name;
private final java.lang.Integer age;

private ImmutableUserValueImmutable(java.lang.String name, java.lang.Integer age) {
this.name = name;
this.age = age;
}

/**
* @return The value of the {@code name} attribute
*/
@Override
java.lang.String getName() {
return name;
}

/**
* @return The value of the {@code age} attribute
*/
@Override
java.lang.Integer getAge() {
return age;
}

/**
* Copy the current immutable object by setting a value for the {@link UserValueImmutable#getName() name} attribute.
* An equals check used to prevent copying of the same value by returning {@code this}.
* @param value A new value for name
* @return A modified copy of the {@code this} object
*/
public final ImmutableUserValueImmutable withName(java.lang.String value) {
if (this.name.equals(value)) return this;
java.lang.String newValue = java.util.Objects.requireNonNull(value, "name");
return new ImmutableUserValueImmutable(newValue, this.age);
}

/**
* Copy the current immutable object by setting a value for the {@link UserValueImmutable#getAge() age} attribute.
* An equals check used to prevent copying of the same value by returning {@code this}.
* @param value A new value for age
* @return A modified copy of the {@code this} object
*/
public final ImmutableUserValueImmutable withAge(java.lang.Integer value) {
if (this.age.equals(value)) return this;
java.lang.Integer newValue = java.util.Objects.requireNonNull(value, "age");
return new ImmutableUserValueImmutable(this.name, newValue);
}

/**
* This instance is equal to all instances of {@code ImmutableUserValueImmutable} that have equal attribute values.
* @return {@code true} if {@code this} is equal to {@code another} instance
*/
@Override
public boolean equals(Object another) {
if (this == another) return true;
return another instanceof ImmutableUserValueImmutable
&amp;amp;amp;amp;amp;&amp;amp;amp;amp;amp; equalTo((ImmutableUserValueImmutable) another);
}

private boolean equalTo(ImmutableUserValueImmutable another) {
return name.equals(another.name)
&amp;amp;amp;amp;amp;&amp;amp;amp;amp;amp; age.equals(another.age);
}

/**
* Computes a hash code from attributes: {@code name}, {@code age}.
* @return hashCode value
*/
@Override
public int hashCode() {
int h = 31;
h = h * 17 + name.hashCode();
h = h * 17 + age.hashCode();
return h;
}

/**
* Prints the immutable value {@code UserValueImmutable} with attribute values.
* @return A string representation of the value
*/
@Override
public String toString() {
return "UserValueImmutable{"
+ "name=" + name
+ ", age=" + age
+ "}";
}

/**
* Creates an immutable copy of a {@link UserValueImmutable} value.
* Uses accessors to get values to initialize the new immutable instance.
* If an instance is already immutable, it is returned as is.
* @param instance The instance to copy
* @return A copied immutable UserValueImmutable instance
*/
public static ImmutableUserValueImmutable copyOf(UserValueImmutable instance) {
if (instance instanceof ImmutableUserValueImmutable) {
return (ImmutableUserValueImmutable) instance;
}
return ImmutableUserValueImmutable.builder()
.from(instance)
.build();
}

/**
* Creates a builder for {@link ImmutableUserValueImmutable ImmutableUserValueImmutable}.
* @return A new ImmutableUserValueImmutable builder
*/
public static ImmutableUserValueImmutable.Builder builder() {
return new ImmutableUserValueImmutable.Builder();
}

/**
* Builds instances of type {@link ImmutableUserValueImmutable ImmutableUserValueImmutable}.
* Initialize attributes and then invoke the {@link #build()} method to create an
* immutable instance.
*

;{@code Builder} is not thread-safe and generally should not be stored in a field or collection,
* but instead used immediately to create instances.&amp;amp;amp;amp;lt;/em&amp;amp;amp;amp;gt;
*/
public static final class Builder {
private static final long INIT_BIT_NAME = 0x1L;
private static final long INIT_BIT_AGE = 0x2L;
private long initBits = 0x3L;

private java.lang.String name;
private java.lang.Integer age;

private Builder() {
}

/**
* Fill a builder with attribute values from the provided {@code UserValueImmutable} instance.
* Regular attribute values will be replaced with those from the given instance.
* Absent optional values will not replace present values.
* @param instance The instance from which to copy values
* @return {@code this} builder for use in a chained invocation
*/
public final Builder from(UserValueImmutable instance) {
java.util.Objects.requireNonNull(instance, "instance");
name(instance.getName());
age(instance.getAge());
return this;
}

/**
* Initializes the value for the {@link UserValueImmutable#getName() name} attribute.
* @param name The value for name
* @return {@code this} builder for use in a chained invocation
*/
public final Builder name(java.lang.String name) {
this.name = java.util.Objects.requireNonNull(name, "name");
initBits &amp;amp;amp;amp;amp;= ~INIT_BIT_NAME;
return this;
}

/**
* Initializes the value for the {@link UserValueImmutable#getAge() age} attribute.
* @param age The value for age
* @return {@code this} builder for use in a chained invocation
*/
public final Builder age(java.lang.Integer age) {
this.age = java.util.Objects.requireNonNull(age, "age");
initBits &amp;amp;amp;amp;amp;= ~INIT_BIT_AGE;
return this;
}

/**
* Builds a new {@link ImmutableUserValueImmutable ImmutableUserValueImmutable}.
* @return An immutable instance of UserValueImmutable
* @throws java.lang.IllegalStateException if any required attributes are missing
*/
public ImmutableUserValueImmutable build() {
if (initBits != 0) {
throw new java.lang.IllegalStateException(formatRequiredAttributesMessage());
}
return new ImmutableUserValueImmutable(name, age);
}

private String formatRequiredAttributesMessage() {
java.util.List<String> attributes = new java.util.ArrayList<>();
if ((initBits & INIT_BIT_NAME) != 0) attributes.add("name");
if ((initBits & INIT_BIT_AGE) != 0) attributes.add("age");
return "Cannot build UserValueImmutable, some of required attributes are not set " + attributes;
}
}
}

 

Immutables poza abstrakcyjnymi klasami obsługuje również zwykłe klasy, interfejsy i adnotacje. Zasada działania pozostaje jednak prawie taka sama.

Wnioski na podstawie wygenerowanego kodu

  • kompilator automatycznie dodał nową klasę, która dziedziczy po klasie bazowej napisanej przez użytkownika. Dzięki takiemu podejściu z klasy bazowej można korzystać jak z publicznego API, a cała reszta aplikacji może być nieświadoma, że klasa została wygenerowana z wykorzystaniem biblioteki Immutables.
  • biblioteka nie narzuca żadnych dodatkowych zależności podczas działania aplikacji. Cały potrzebny kod jest generowany już podczas kompilacji
  • kompilator sam utworzył pola klasy na podstawie abstrakcyjnych getterów
  • w powstałym kodzie nie ma setterów. Ponieważ wszystkie pola są oznaczona jako final nie można też dodać ich w klasie bazowej. Dzięki temu powstałe obiekty są niemodyfikowalne (immutables)
  • klasa ma wygenerowane metody equals, hashCode oraz toString uwzględniające wszystkie pola
  • nowa klasa posiada prywatny konstruktor, dlatego jedynym sposobem na jej utworzenie jest wykorzystanie również przygotowanego buildera
  • adnotacje z abstrakcyjnych getterów są przekopiowane do ich implementacji
  • kod zawiera wygenerowane metody kopiujące. Są to metody w formacie: ’with’ + [nazwa pola]. Za ich pomocą można otrzymać nowy obiekt immutable ze zmodyfikowanym tylko jednym polem

Walidacja oraz normalizacja danych

Dzięki wbudowanemu mechanizmowi można bardzo łatwo zaimplementować walidację oraz normalizację danych. Wystarczy w klasie bazowej dodać nieprywatną metodę z adnotacją: @Value.Check.

Metody zwracające void mogą obsługiwać tylko walidację.

@Value.Immutable
public abstract class UserValueCheck {

public abstract String getName();
public abstract Integer getAge();

@Value.Check
protected UserValueCheck validateAndNormalize() {
if (getAge() < 0) {
throw new IllegalStateException("Age variable must be positive.");
}

if (!getName().toUpperCase().equals(getName())) {
return ImmutableUserValueCheck.builder()
.from(this)
.name(getName().toUpperCase())
.build();
}
return this;
}
}

Można jednak rozszerzyć metodę o zwracanie walidowanego obiektu i przeprowadzić jego normalizację, tak jak zostało to zrobione w przykładzie. Trzeba jednak uważać by zawsze na końcu zwracany był obiekt this. W przeciwnym wypadku walidacja będzie odpalana bez końca i doprowadzi to do przepełnienia stosu.

lazy loading

lazy loading

Leniwe dociąganie danych

Kolejna ciekawą funkcjonalnością wprowadzoną przez framework jest leniwe ładowanie, czyli lazy loading. Dla wybranej metody wystarczy dodać adnotację: @Value.Lazy, żeby biblioteka wygenerowała resztę kodu.

Wynik takiej operacji zostanie wyliczony tylko raz i będzie przechowywany w lokalnej zmiennej.

@Value.Immutable(builder = false)
public class LazyLoadingExample {

@Value.Lazy
public String longRunningMethod() {
try {
Thread.sleep(1000);
} catch (InterruptedException ex) {
throw new RuntimeException(ex);
}

return "" + System.currentTimeMillis();
}
}

Powstały kod jest napisane z uwzględnieniem potencjalnych kłopotów z wyścigami wątków.

Atrybuty oznaczone w ten sposób nie są już uwzględniane w implementacjach metod equals oraz hashCode.

public String longRunningMethod() {
if ((lazyInitBitmap & LONG_RUNNING_METHOD_LAZY_INIT_BIT) == 0) {
synchronized (this) {
if ((lazyInitBitmap & LONG_RUNNING_METHOD_LAZY_INIT_BIT) == 0) {
this.longRunningMethod = Preconditions.checkNotNull(super.longRunningMethod(), "longRunningMethod");
lazyInitBitmap |= LONG_RUNNING_METHOD_LAZY_INIT_BIT;
}
}
}
return longRunningMethod;
}

Własna implementacja metod: hashCode(), equals() oraz toString()

W celu nadpisania powyższych metod wystarczy zaimplementować je w bazowej klasie użytkownika. Kompilator sam wtedy rozpozna, żeby nie nadpisywać ich implementacji.

Jednak zanim nadpisze się te metody, warto zastanowić się, czy na pewno jest to konieczne. W wielu wypadkach wystarczy wykluczyć niektóre pola z implementacji tych metod i nie bawić się w ręczne ich pisanie. Do tego celu służy adnotacja: @Value.Auxiliary, którą trzeba dodać na niechcianych polach (getterach, na podstawie których zostaną wygenerowane te pola).

Etapowy builder

Biblioteka daje możliwość zaimplementowania tak zwanego staged builder’a. Dla klasy Immutables trzeba dodać adnotację: @Value.Style(stagedBuilder = true). Pomysł polega na tym, że podczas tworzenia obiektu przy pomocy buildera już na poziomie kompilacji kodu wymuszona jest kolejność podawania wymaganych parametrów obiektu.

Funkcjonalność jest o tyle ciekawa, że nie trzeba nawet uruchamiać aplikacji, żeby zweryfikować czy wszystkie wymagane argumenty zostały podane.

//-no-import-rewrite
package UserValue;

import java.lang.Object;
import java.lang.String;
import java.lang.Float;
import java.lang.Double;

/**
* Immutable implementation of {@link UserValueImmutable}.
*

* Use the builder to create immutable instances:
* {@code ImmutableUserValueImmutable.builder()}.
*/
@SuppressWarnings({"all"})
@javax.annotation.Generated({"Immutables.generator", "UserValueImmutable"})
public final class ImmutableUserValueImmutable extends UserValue.UserValueImmutable {
private final java.lang.String name;
private final java.lang.Integer age;

private ImmutableUserValueImmutable(java.lang.String name, java.lang.Integer age) {
this.name = name;
this.age = age;
}

/**
* @return The value of the {@code name} attribute
*/
@Override
java.lang.String getName() {
return name;
}

/**
* @return The value of the {@code age} attribute
*/
@Override
java.lang.Integer getAge() {
return age;
}

/**
* Copy the current immutable object by setting a value for the {@link UserValueImmutable#getName() name} attribute.
* An equals check used to prevent copying of the same value by returning {@code this}.
* @param value A new value for name
* @return A modified copy of the {@code this} object
*/
public final ImmutableUserValueImmutable withName(java.lang.String value) {
if (this.name.equals(value)) return this;
java.lang.String newValue = java.util.Objects.requireNonNull(value, "name");
return new ImmutableUserValueImmutable(newValue, this.age);
}

/**
* Copy the current immutable object by setting a value for the {@link UserValueImmutable#getAge() age} attribute.
* An equals check used to prevent copying of the same value by returning {@code this}.
* @param value A new value for age
* @return A modified copy of the {@code this} object
*/
public final ImmutableUserValueImmutable withAge(java.lang.Integer value) {
if (this.age.equals(value)) return this;
java.lang.Integer newValue = java.util.Objects.requireNonNull(value, "age");
return new ImmutableUserValueImmutable(this.name, newValue);
}

/**
* This instance is equal to all instances of {@code ImmutableUserValueImmutable} that have equal attribute values.
* @return {@code true} if {@code this} is equal to {@code another} instance
*/
@Override
public boolean equals(Object another) {
if (this == another) return true;
return another instanceof ImmutableUserValueImmutable
&amp;amp;amp;amp;amp;&amp;amp;amp;amp;amp; equalTo((ImmutableUserValueImmutable) another);
}

private boolean equalTo(ImmutableUserValueImmutable another) {
return name.equals(another.name)
&amp;amp;amp;amp;amp;&amp;amp;amp;amp;amp; age.equals(another.age);
}

/**
* Computes a hash code from attributes: {@code name}, {@code age}.
* @return hashCode value
*/
@Override
public int hashCode() {
int h = 31;
h = h * 17 + name.hashCode();
h = h * 17 + age.hashCode();
return h;
}

/**
* Prints the immutable value {@code UserValueImmutable} with attribute values.
* @return A string representation of the value
*/
@Override
public String toString() {
return "UserValueImmutable{"
+ "name=" + name
+ ", age=" + age
+ "}";
}

/**
* Creates an immutable copy of a {@link UserValueImmutable} value.
* Uses accessors to get values to initialize the new immutable instance.
* If an instance is already immutable, it is returned as is.
* @param instance The instance to copy
* @return A copied immutable UserValueImmutable instance
*/
public static ImmutableUserValueImmutable copyOf(UserValueImmutable instance) {
if (instance instanceof ImmutableUserValueImmutable) {
return (ImmutableUserValueImmutable) instance;
}
return ImmutableUserValueImmutable.builder()
.from(instance)
.build();
}

/**
* Creates a builder for {@link ImmutableUserValueImmutable ImmutableUserValueImmutable}.
* @return A new ImmutableUserValueImmutable builder
*/
public static ImmutableUserValueImmutable.Builder builder() {
return new ImmutableUserValueImmutable.Builder();
}

/**
* Builds instances of type {@link ImmutableUserValueImmutable ImmutableUserValueImmutable}.
* Initialize attributes and then invoke the {@link #build()} method to create an
* immutable instance.
*

;{@code Builder} is not thread-safe and generally should not be stored in a field or collection,
* but instead used immediately to create instances.&amp;amp;amp;amp;lt;/em&amp;amp;amp;amp;gt;
*/
public static final class Builder {
private static final long INIT_BIT_NAME = 0x1L;
private static final long INIT_BIT_AGE = 0x2L;
private long initBits = 0x3L;

private java.lang.String name;
private java.lang.Integer age;

private Builder() {
}

/**
* Fill a builder with attribute values from the provided {@code UserValueImmutable} instance.
* Regular attribute values will be replaced with those from the given instance.
* Absent optional values will not replace present values.
* @param instance The instance from which to copy values
* @return {@code this} builder for use in a chained invocation
*/
public final Builder from(UserValueImmutable instance) {
java.util.Objects.requireNonNull(instance, "instance");
name(instance.getName());
age(instance.getAge());
return this;
}

/**
* Initializes the value for the {@link UserValueImmutable#getName() name} attribute.
* @param name The value for name
* @return {@code this} builder for use in a chained invocation
*/
public final Builder name(java.lang.String name) {
this.name = java.util.Objects.requireNonNull(name, "name");
initBits &amp;amp;amp;amp;amp;= ~INIT_BIT_NAME;
return this;
}

/**
* Initializes the value for the {@link UserValueImmutable#getAge() age} attribute.
* @param age The value for age
* @return {@code this} builder for use in a chained invocation
*/
public final Builder age(java.lang.Integer age) {
this.age = java.util.Objects.requireNonNull(age, "age");
initBits &amp;amp;amp;amp;amp;= ~INIT_BIT_AGE;
return this;
}

/**
* Builds a new {@link ImmutableUserValueImmutable ImmutableUserValueImmutable}.
* @return An immutable instance of UserValueImmutable
* @throws java.lang.IllegalStateException if any required attributes are missing
*/
public ImmutableUserValueImmutable build() {
if (initBits != 0) {
throw new java.lang.IllegalStateException(formatRequiredAttributesMessage());
}
return new ImmutableUserValueImmutable(name, age);
}

private String formatRequiredAttributesMessage() {
java.util.List<String> attributes = new java.util.ArrayList<>();
if ((initBits & INIT_BIT_NAME) != 0) attributes.add("name");
if ((initBits & INIT_BIT_AGE) != 0) attributes.add("age");
return "Cannot build UserValueImmutable, some of required attributes are not set " + attributes;
}
}
}

Minusem tego rozwiązania jest generowania przez implementację Immutables dodatkowych interfejsów oraz bardziej skomplikowanego kodu. Może to przełożyć się na odrobinę dłuższy czas budowania projektu oraz większe zużycie pamięci na wygenerowane klasy.

Serializacja

Immutables wspiera również standardową serializację Javy. Klasę należy oznaczyć adnotacją: @Serial.Version(1), gdzie 1 jest wersją klasy.

Klasa użytkownika nie musi implementować interfejsu Serializable, ponieważ zostanie on automatycznie dodany do klasy Immutables.

Dodatkowa konfiguracja generowanej klasy

W celu dopasowania generowanej implementacji do swoich potrzeb można posłużyć się opcjonalnymi parametrami adnotacji @Value.Immutable.

public @interface Immutable {

public boolean singleton() default false;
public boolean intern() default false;
public boolean copy() default true;
public boolean prehash() default false;
public boolean builder() default true;
}

Dzięki tym parametrom można:

  • singleton – oznaczyć klasę jako singleton. Warto jednocześnie wyłączyć opcję generowania buildera (builder=false). Wtedy obiekt singletonu będzie dostępny z metody: UserSingleton userSingleton = ImmutableUserSingleton.of();
  • intern – parametr optymalizacyjny. Wprowadza wewnętrzną pulę obiektów danego typu. Dzięki tej opcji obiekty o tych samych wartościach atrybutów będą przechowywane w tym samym miejscu w pamięci
  • copy – generowanie metod kopiujących. Dla przykładu użytkownika będą to metody withName i withAge
  • prehash – kolejna opcja optymalizacyjna. Dzięki niej hashe dla metody hashCode są wyliczane tylko raz
  • builder – generowanie klasy buildera

Modyfikowalne obiekty

Mimo iż nazwa biblioteki na to nie wskazuje, posiada ona również wsparcie dla obiektów modyfikowalnych. Nie są to funkcjonalności tak mocno rozbudowane, jak te z głównego nurtu biblioteki, ale mimo to stanową ciekawą alternatywą np. dla generowanie getterówsetterów przy pomocy Lomboka.

@Value.Modifiable
public abstract class UserModifiable {

abstract String getName();
abstract Integer getAge();
}

Podobnie jak w przypadku @Value.Immutables, na podstawie klasy użytkownika zostanie wygenerowana nowa klasa. W tym wypadku będzie to:

package pl.stormit.immutables;

import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
import javax.annotation.Generated;

/**
* A modifiable implementation of the {@link UserModifiable UserModifiable} type.
*

Use the {@link #create()} static factory methods to create new instances.
* Use the {@link #toImmutable()} method to convert to canonical immutable instances.
*

&lt;em&gt;ModifiableUserModifiable is not thread-safe&lt;/em&gt;
* @see ImmutableUserModifiable
*/
@SuppressWarnings({"all"})
@Generated({"Modifiables.generator", "UserModifiable"})
public final class ModifiableUserModifiable extends UserModifiable {
private static final long INIT_BIT_NAME = 0x1L;
private static final long INIT_BIT_AGE = 0x2L;
private long initBits = 0x3L;

private String name;
private Integer age;

private ModifiableUserModifiable() {}

/**
* Construct a modifiable instance of {@code UserModifiable}.
* @return A new modifiable instance
*/
public static ModifiableUserModifiable create() {
return new ModifiableUserModifiable();
}

/**
* @return value of {@code name} attribute
*/
@Override
final String getName() {
if (!nameIsSet()) {
checkRequiredAttributes();
}
return name;
}

/**
* @return value of {@code age} attribute
*/
@Override
final Integer getAge() {
if (!ageIsSet()) {
checkRequiredAttributes();
}
return age;
}

/**
* Clears the object by setting all attributes to their initial values.
* @return {@code this} for use in a chained invocation
*/
public ModifiableUserModifiable clear() {
initBits = 0x3L;
name = null;
age = null;
return this;
}

/**
* Fill this modifiable instance with attribute values from the provided {@link UserModifiable} instance.
* Regular attribute values will be overridden, i.e. replaced with ones of an instance.
* Any of the instance's absent optional values will not be copied (will not override current values).
* @param instance The instance from which to copy values
* @return {@code this} for use in a chained invocation
*/
public ModifiableUserModifiable from(UserModifiable instance) {
Objects.requireNonNull(instance, "instance");
setName(instance.getName());
setAge(instance.getAge());
return this;
}

/**
* Assigns a value to the {@link UserModifiable#getName() name} attribute.
* @param name The value for name
* @return {@code this} for use in a chained invocation
*/
public ModifiableUserModifiable setName(String name) {
this.name = Objects.requireNonNull(name, "name");
initBits &amp;= ~INIT_BIT_NAME;
return this;
}

/**
* Assigns a value to the {@link UserModifiable#getAge() age} attribute.
* @param age The value for age
* @return {@code this} for use in a chained invocation
*/
public ModifiableUserModifiable setAge(Integer age) {
this.age = Objects.requireNonNull(age, "age");
initBits &amp;= ~INIT_BIT_AGE;
return this;
}

/**
* Returns {@code true} if the required attribute {@link UserModifiable#getName() name} is set.
* @return {@code true} if set
*/
public final boolean nameIsSet() {
return (initBits &amp; INIT_BIT_NAME) == 0;
}

/**
* Returns {@code true} if the required attribute {@link UserModifiable#getAge() age} is set.
* @return {@code true} if set
*/
public final boolean ageIsSet() {
return (initBits &amp; INIT_BIT_AGE) == 0;
}

/**
* Reset an attribute to its initial value.
* @return {@code this} for use in a chained invocation
*/
public final ModifiableUserModifiable unsetName() {
initBits |= INIT_BIT_NAME;
name = null;
return this;
}

/**
* Reset an attribute to its initial value.
* @return {@code this} for use in a chained invocation
*/
public final ModifiableUserModifiable unsetAge() {
initBits |= INIT_BIT_AGE;
age = null;
return this;
}

/**
* Returns {@code true} if all required attributes are set, indicating that the object is initialized.
* @return {@code true} if set
*/
public final boolean isInitialized() {
return initBits == 0;
}

private void checkRequiredAttributes() {
if (!isInitialized()) {
throw new IllegalStateException(formatRequiredAttributesMessage());
}
}

private String formatRequiredAttributesMessage() {
List&lt;String&gt; attributes = new ArrayList&lt;String&gt;();
if (!nameIsSet()) attributes.add("name");
if (!ageIsSet()) attributes.add("age");
return "UserModifiable in not initialized, some of the required attributes are not set " + attributes;
}

/**
* Converts to {@link ImmutableUserModifiable ImmutableUserModifiable}.
* @return An immutable instance of UserModifiable
*/
public final ImmutableUserModifiable toImmutable() {
checkRequiredAttributes();
return ImmutableUserModifiable.copyOf(this);
}

/**
* This instance is equal to all instances of {@code ModifiableUserModifiable} that have equal attribute values.
* An uninitialized instance is equal only to itself.
* @return {@code true} if {@code this} is equal to {@code another} instance
*/
@Override
public boolean equals(Object another) {
if (this == another) return true;
if (!(another instanceof ModifiableUserModifiable)) return false;
ModifiableUserModifiable other = (ModifiableUserModifiable) another;
if (!isInitialized() || !other.isInitialized()) {
return false;
}
return equalTo(other);
}

private boolean equalTo(ModifiableUserModifiable another) {
return name.equals(another.name)
&amp;&amp; age.equals(another.age);
}

/**
* Computes a hash code from attributes: {@code name}, {@code age}.
* @return hashCode value
*/
@Override
public int hashCode() {
int h = 31;
h = h * 17 + name.hashCode();
h = h * 17 + age.hashCode();
return h;
}

/**
* Generates a string representation of this {@code UserModifiable}.
* If uninitialized, some attribute values may appear as question marks.
* @return A string representation
*/
@Override
public String toString() {
return "ModifiableUserModifiable{"
+ "name=" + (nameIsSet() ? getName() : "?")
+ ", age=" + (ageIsSet() ? getAge() : "?")
+ "}";
}
}

Wygenerowana klasa zawiera:

  • pola utworzone na podstawie abstrakcyjnych getterów
  • gettery
  • settery
  • metody equals oraz hashCode
  • metodę toString

Ciekawostką jest to, że klasa może zawierać jednocześnie dwie adnotacje: @Value.Modifiable oraz @Value.Immutable. Dla tej klasy zostaną wtedy wygenerowane dwie implementacje: ImmutableUserModifiable oraz ModifiableUserModifiable.

Programowanie funkcyjne

Biblioteka posiada również wsparcie dla programowania funkcyjnego. Funkcjonalności dostępne są w ramach osobnego modułu org.immutables:func.

@Value.Immutable
@Functional
abstract class UserFunctional {

public abstract String getName();
public abstract Integer getAge();
}
ImmutableUserFunctional user = ImmutableUserFunctional.builder()
.name("Tomasz")
.age(100).build();

List<UserFunctional> users = Arrays.asList(user.withAge(1), user.withAge(2),
user.withAge(3), user.withAge(4));

List<Integer> ages = Lists.transform(users, UserFunctionalFunctions.getAge());

assertEquals(Arrays.asList(1, 2, 3, 4), ages);

Wnioski

Biblioteka bazuje na bardzo podobnych założeniach jak opisywana wcześniej AutoValue. Dlatego bardzo wiele wniosków oraz funkcjonalności pokrywa się dla tych bibliotek.

Rozwiązania tego typu rozszerzają standardowe funkcjonalności oferowane przez Javę. Dzięki temu programista może poświęcić więcej swojego czasu i wysiłku na obsługę problemów typowo biznesowych, zamiast wymyślać koło na nowo.

kierunek java

No comments
Share:
Debugowanie

Debugowanie, jakiego jeszcze nie znałeś

Debugowanie aplikacji to proces związany z naprawą błędów w kodzie, przez co jest zazwyczaj bardzo nielubiany przez programistów. Postaram się jednak przybliżyć ten temat tak, by stał się bardziej zrozumiały oraz pokazać kilka sztuczek, które powinny sprawić, że będzie on dużo szybszy i bardziej efektywny.

Wprowadzenie

Zgodnie z definicją debugowanie to proces polegający na systematycznej redukcji liczby błędów w oprogramowaniu. Przeprowadzany jest zazwyczaj z kontrolowanym wykonaniem kodu aplikacji przy wykorzystaniu debuggera.

Samo słowo bug (z ang. robak), rozumiane jako błąd po raz pierwszy zostało użyte już w IX wieku przez Tomasza Edisona, który w jednym ze swoich listów określił nim usterki techniczne. Jednak jego popularyzację zawdzięczamy admirał Grace Hopper. Razem ze swoimi współpracownikami w latach 40-tych ubiegłego wieku w komputerze Mark II znalazła ćmę, która zaplątała się w przekaźnik, powodując błędy. Admirał nazwała pozbycie się robaka debugowaniem, czyli odrobaczaniem.

Przebieg debugowania

Każdy problem wymaga indywidualnego podejścia, jednak mimo to zazwyczaj proces od znalezienia błędu do jego naprawy można podzielić na 5 głównych etapów:

1. Reprodukcja błędu

To pierwszy i ważny etap naprawy błędu. Zdarza się bowiem, że zgłaszane problemy tak naprawdę nie są błędami i wynikają z braku zrozumienia działania aplikacji. Zdarza się również, że odtworzenia błędu na środowisku u programisty jest bardzo trudne.

Spowodowane jest to najczęściej inną konfiguracją środowiska niż środowisko produkcyjne lub brakiem odpowiednich danych. Żeby ułatwić sobie ten proces, najczęściej wykorzystuje się zrzuty bazy danych ze środowiska, na którym znaleziono błąd lub przeprowadza się zdalne debugowanie.

2. Wyizolowanie źródła błędu

Kiedy uda się już odtworzyć błąd, trzeba ustalić dokładnie, w jakich warunkach on zachodzi. Na tym etapie szukamy minimalnych warunków, jakie muszą być spełnione, żeby wywołać dany błąd.

3. Identyfikacja przyczyny awarii

Mając już wyizolowane źródło błędu, przystępujemy do jego szukania w kodzie. Najczęściej wykorzystuje się do tego debugger lub przegląda się szczegółowe logi.

4. Usunięcie defektu

Po znalezienie dokładnego miejsca powodującego błąd trzeba podjąć decyzję, jak go naprawić. Trzeba bardzo uważać, ponieważ naprawiając jeden fragment kodu, można jednocześnie wywołać błędy w innych, teoretycznie niepowiązanych, miejscach.

5. Weryfikacja powodzenia naprawy

Po usunięciu błędu trzeba jeszcze potwierdzić, czy system działa w pełni poprawnie oraz czy nie wywołało to innych niepożądanych skutków.

Techniki debugowania

Techniki debugowania

Techniki debugowania

Ze względu na przyjętą technikę oraz wykorzystane narzędzia, można wyróżnić kilka głównych podejść do debugowania problemu:

Printf debugging

Nazwa printf debugging pochodzi od wykorzystania funkcji printf z C. Jednak niezależnie od wyboru konkretnej metody, czy to System.out.println(), logger.debug(), czy języka programowania, zasada pozostaje taka sama. Chodzi o wyświetlanie przygotowanych komunikatów i ich przeglądania.
Powstałe w ten sposób logi można przeglądać w wersji online, jednocześnie korzystając z aplikacji i wywołując wybrane funkcjonalności, żeby spowodować błędy lub w formie zrzutu już po wystąpieniu błędu.

Z debuggerem

Najczęstsza i zazwyczaj najbardziej komfortowa forma debugowania. Dzięki wykorzystaniu debuggera podłączonego do aplikacji można sterować jej wykonaniem, wykonywać instrukcja programu krok po kroku oraz podglądać wartości poszczególnych zmiennych

Zdalne debugowanie

Jeżeli nie można odtworzyć błędu na lokalnym komputerze można spróbować połączyć się debuggerem do zdalnej maszyny. Takie działanie jest prawie identyczne jak w przypadku debugowania aplikacji lokalnej, jednak w tym wypadku aplikacja znajduje się na osobnej maszynie i debugger musi sterować jej wykonaniem przez sieć. Może to powodować dodatkowe problemy oraz narzuty wydajnościowe związane z samym wykorzystaniem sieci.

Poawaryjne debugowanie (post-mortem)

W tym wypadku, jeżeli już doszło do awarii, można próbować dojść, co dokładnie się stało, dzięki różnego rodzaju zrzutom danych, które zostały zrobione bezpośrednio przed lub już w trakcie awarii.

W tym celu najczęściej wykorzystane są zrzuty pamięci (heap dump) lub zrzut wątków z maszyny wirtualnej Javy (thread dump).

Debugowanie przez wykluczenie

Debugowanie przez wykluczenie polega na próbie izolacji przyczyny powodującej błąd. Można to przeprowadzić np. poprzez wyłączanie kolejnych modułów aplikacji i weryfikację, czy błąd nadal występuje.

Taki sposób debugowanie nazywany jest również debugowaniem z wykorzystaniem algorytmu: Wolf fence.

Debugger

Debugger

Debugger

Ponieważ debugowanie z wykorzystaniem debuggera daje największe możliwości, przyjrzymy mu się bliżej. W przykładach posłużę się wbudowanych debuggerem w Netbeans IDE.

Netbeans IDE

Debugger w Netbeans daje bardzo dużo możliwości i jest bardzo dobrze zintegrowany z IDE. Dzięki temu naprawdę rzadko potrzebne są jakieś zewnętrzne narzędzia.

W celu uruchomienia projektu w trybie debug można skorzystać z menu głównego Debug lub przycisku jak na grafice poniżej.

Netbeans debugger

Netbeans debugger

Netbeans daje możliwość debugowania aktualnie otwartego projektu lub połączenie się debuggerem do zdalnego procesu.

Netbeans debug test

Netbeans debug test

Bardzo przydatną opcją jest również debugowanie testów jednostkowych. W tym celu na wybranym teście można skorzystać z menu kontekstowego i wybrać opcję: Debug Test File.

Netbeans uruchomiony debugger

Netbeans uruchomiony debugger

Po uruchomieniu aplikacji w debuggerze będzie ona normalnie się wykonywała, aż natrafi na breakpoint. Na widoku powyżej pokazany jest screen z debuggera zatrzymanego na breakpoincie.

W tym momencie można podejrzeć, w jakim dokładnie stanie jest aplikacja. Poszczególne punkty widoku opisane są poniżej:

  1. Okno debugging – pokazuje stos wywołań do aktualnego miejsca wykonywania aplikacji. Można tu zweryfikować z jakiego miejsca została wywołana aktualnie wykonywana metoda.
  2. Aktualnie wykonywana linia – ta linia jest oznaczona na zielono i wskazuje na linijkę kodu aktualnie przetwarzaną przez debugger.
  3. Linia z breakpointem – linijka kodu oznaczona na czerwono wskazuje, że znajduje się przy niej breakpoint.
  4. Okno Variables – widok z aktualnie dostępnymi zmiennymi oraz ich wartościami.
  5. Ustawiona czujka – można w ten sposób podejrzeć wartość wygenerowaną przez całe wywołania.

Breakpoint

Breakpoint, czyli punkt wstrzymania pozwala na zatrzymanie wykonywania aplikacji we wskazanym przez nas miejscu. Aplikacja wtedy przekazuje sterowanie do debuggera, a my możemy podejrzeć aktualny stan aplikacji, czyli np. wszystkie dostępne zmienne.

Gdzie można postawić breakpoint?

Breakpointy ustawiamy w linijkach, które zawierają jakiś kod do wykonania. To znaczy, że nie zatrzymamy aplikacji na klamerkach, czy na pustej sygnaturze metody.

Warto zauważyć, że żeby w danym breakpoincie zatrzymała się nasza aplikacja, to wskazany przez niego kod musi się wykonać. Jeżeli więc postawimy breakpoint np. w if’ie i nie zostaną spełnione wymagane warunki, to nasz punkt zatrzymania zostanie pominięty.

Gdzie należy postawić breakpoint?

Głowna zasada jest taka, że breakpoint stawiamy przed miejscem, które chcemy podejrzeć. Ponieważ po zatrzymaniu działania programu nie można się już cofnąć, czasem nawet lepiej jest ustawić kilka nadmiarowych breakpointów lub trochę za wcześnie zatrzymać aplikację, żeby złapać ten właściwy moment.

Działanie programu wstrzymywane jest dokładnie przed wybraną linią, czyli linijka oznaczona breakpointem w momencie zatrzymania się, nie jest jeszcze wykonana.

Jak dodać breakpoint?

Nowy breakpoint możemy dodać, wskazując wybraną linijkę i wybierając z menu kontekstowego: Toggle Line Breakpoint. Alternatywnie można również kliknąć na numer z lewej strony widoku, oznaczający kolejną linijkę kodu.

Po dodaniu breakpointu linijka zostanie oznaczona na czerwono, a przy numerze linii pojawi się czerwony kwadrat.

Netbeans jak dodać breakpoint

Netbeans jak dodać breakpoint

W analogiczny sposób usuwamy wybrane breakpointy.

Netbeans breakpointy

Netbeans breakpointy

 

Wiele ciekawych możliwości daje również widok Breakpoints – można go włączyć przez menu główne Window -> Debugging -> Breakpoints. Z tego poziomu widać wszystkie dodane wcześniej breakpointy, można je usunąć, tymczasowo wyłączyć lub dodać nowe.

Sterowanie działaniem programu w trybie debugowanie

Podczas debugowania bardzo często poruszamy się po kodzie aplikacji w kontrolowany sposób. Poniżej dostępne opcje sterowania opisane wg przycisków, od lewej do prawej strony.

Netbeans sterowanie debugerem

Netbeans sterowanie debugerem

  • Finish Debugger Session (Shift + F5) – kończy działanie debuggera.
  • Pause – zatrzymuje wszystkie wątki z aktualnej sesji.
  • Continue (F5) – kontynuuje wykonanie aplikacji i zatrzymuje się dopiero na kolejnym breakpoincie, jeżeli taki będzie.
  • Step Over (F8) – przechodzi linijkę dalej i wykonuje jedną operację – jeżeli ta linijka jest wywołaniem funkcji, wykonuje ją i oddaje sterowanie bezpośrednio po niej.
  • Step Over Expression (Shift + F8) – ta opcja wykorzystywana jest, jeżeli w jednej linijce mamy wyrażenia składające się z kilku różnych wywołań. Dzięki niej można rozbić wywołanie takiej linijki na poszczególne kroki.
  • Step Into (F7) – przechodzi linijkę dalej i wykonuje jedną operację – jeżeli ta linijka jest wywołaniem funkcji, przechodzi do jej wnętrza. Jeżeli w danej linii będzie kilka wywołań metod, IDE pogrubi je i trzeba będzie wybrać, do wywołania której chcemy przejść.
  • Step Out (Ctrl + F7) – kontynuuje działanie aż do końca aktualnie wykonywanej metody. Oddaje sterowanie zaraz po zwróceniu przez nią wyniku.
  • Run to Cursor (F4) – kontynuuje wykonanie aplikacji i zatrzymuje się dopiero na linijce kodu oznaczonej kursorem.

W praktyce najczęściej wykorzystywane jest Continue, Step Over oraz Step Into – dlatego warto nauczyć się ich skrótów klawiszowych.

Sztuczki i kruczki debugowania

Dlaczego to tak wolno działa?

Uruchomienia aplikacji w trybie debugowania sprawia, że jest ona całkowicie inaczej traktowana przez maszynę wirtualną Javy. Debugowanie wymusza przekazywanie między nimi dużo większej ilości informacji, nie jest również przeprowadzana wewnętrznie standardowa optymalizacja. To wszystko przekłada się na wolniejsze wykonywanie się debugowanej aplikacji.

W celu przyspieszenia warto też usunąć wszystkie niepotrzebne breakpointy. Czasami zostawia się ustawione breakpointy w dawno już niedebugowanym kodzie, a to, w skrajnym wypadku, potrafi wręcz uniemożliwić normalną pracę z debuggerem.

Dodanie wyrażeń do obserwowania i modyfikacja zmiennych w locie

Przedstawiony wcześniej widok Variables umożliwia podglądanie nie tylko wartości zmiennych, ale również całych wyrażeń.

Dzięki temu można zatrzymać debugger w wybranym miejscu i testować różne fragmenty kodu dodając je do obserwowanych i dopiero jak taki kod zadziała, przenieść go do normalnego kodu w pliku źródłowym.

W ten sam sposób można również modyfikować zmienne w locie, bez konieczności restartowania całej aplikacji – dodajemy przykładowe wywołanie do obserwowanych, w celu modyfikacji zmiennej:

nazwaZmiennej = „wartość zmodyfikowana przez debugger”;

Takie rozwiązania są bardzo przydatne w większych projektach, gdzie zbudowanie na nowo i zrestartowanie całej aplikacji z nowym kodem zajmuje sporo czasu.

Rozproszone debugowanie

W środowisku rozproszonym debugowanie jest jeszcze trudniejsze. Czasami dochodzi wręcz do sytuacji, że uruchamia się aplikację w trybie debugowania na kilku różnych komputerach i biega między nimi, żeby puścić dalej wykonywanie debuggera 🙂

W takich sytuacjach można spróbować ograniczyć się do debugowania tylko na jednej maszynie, a na pozostałych zwiększyć poziom logowania np. na standardowe wyjście i przeglądać logi już po fakcie.

Breakpoint warunkowy [conditional breakpoint]

Większość debuggerów daje możliwość określenia specjalnych dodatkowych warunków dla breakpointów. Jest to niezwykle przydatne, jeżeli dany fragment kodu jest wykonywany wiele razy, a my chcemy złapać wywołanie aplikacji tylko w jednym przypadku.

Netbeans breakpoint warunkowy

Netbeans breakpoint warunkowy

Dla przykładu z obrazka, zamiast klikać 300 razy next, next… można ustawić właśnie takie warunki i poczekać, aż debugger zrobi resztę. W ten sam sposób można również np. zatrzymać wykonywanie debuggera w metodzie, dodając warunek na jej argumenty.

 

 

No comments
Share:

Klonowanie płytkie czy głębokie w Javie

Klonowanie jest to mechanizm polegający na duplikowaniu, czyli kopiowaniu jednego obiektu na drugi. Jednak w Javie na obiekty wskazują referencje i to właśnie przy ich pomocy można manipulować obiektami. Przypisanie obiektu do innej referencji duplikuje samą referencję, a nie obiekt, na który wskazuje. Mechanizm klonowania i metoda clone() jest jednym z potencjalnych rozwiązań dla brakującej funkcjonalności kopiowania obiektów.

Jak działa clone() i Clonable w Javie

Metoda clone() ma swoją implementację już w klasie Object. Jednak żeby móc sklonować obiekt danej klasy, trzeba jeszcze implementować interfejs Cloneable. W przeciwnym wypadku zostanie rzucony wyjątek: java.lang.CloneNotSupportedException.

 

protected native Object clone() throws CloneNotSupportedException;

public interface Cloneable { }

Domyślna deklaracja metody clone() oznaczona jest modyfikatorem dostępu protected, dlatego, żeby można było ją wywołać z poziomu innej klasy, trzeba ją jeszcze nadpisać i zmienić modyfikator na public. Opcjonalnie można dodatkowo rzutować zwrócony obiekt na aktualną klasę.

public class UserShallow implements Cloneable {

	@Override
	public UserShallow clone() throws CloneNotSupportedException {
		return (UserShallow) super.clone();
	}
}

Płytkie czy głębokie kopiowanie (deep copy vs shallow copy)

Kopiowanie płytkie (shallow copy)

Jest to podstawowa i najczęściej spotykana metoda kopiowania obiektów. W tym przypadku podczas klonowania jednego obiektu na drugi, wszystkie jego własności (typy proste i referencje) są również kopiowane.

W przypadku typów prostych wartości zostaną skopiowane i można je modyfikować niezależnie w oryginalnym obiekcie i w jego kopii. Jednak dla wszystkich pól przechowujących referencje (adresy w pamięci komputera), to właśnie referencje będą skopiowane i po skopiowaniu będą dalej wskazywały dokładnie na ten sam obiekt. W efekcie czego oba obiekty, oryginał i kopia, wskazują na ten sam obiekt zależny. Jego modyfikacja zmieni stan obu obiektów.

Kopiowanie płytkie (shallow copy)

Kopiowanie płytkie (shallow copy)

Kopiowanie głębokie (deep copy)

Alternatywą dla kopiowania płytkiego jest kopiowanie głębokie. W tym wypadku referencje zamiast być kopiowane, są tak modyfikowane, by wskazywały na obiekty zależne, które również są klonowane.

Takie podejście jest jednak zdecydowanie bardziej skomplikowane i kosztowne. Powoduje to konieczność tworzenia wielu nowych obiektów. Mogą również pojawić się problemy, jeżeli w grafie zależności obiektów będą cykle.

Kopiowanie głębokie (deep copy)

Kopiowanie głębokie (deep copy)

Powyższe trudności powodują, że bardzo często powstają rozwiązania hybrydowe, łączące kopiowanie płytkie i głębokie.

Klonowanie półautomatyczne

Poniższy kod jest przykładem klonowania półautomatycznego. W pierwszym kroku tworzona jest kopia aktualnego obiektu, zawierająca również kopię wszystkich jego trzech pól.

Referencje dla pól: name, age i interest prowadzą dokładnie do tych samych obiektów, co w oryginalnym obiekcie.

W kolejnym kroku tworzony jest nowy obiekt typu Interest i nadpisywana jest referencja prowadząca do niego. W tym wypadku do utworzenia został wykorzystany zwykły konstruktor, jednak gdyby klasa Interest obsługiwała klonowanie, mogłaby być to metoda clone().

public class UserSemiAutomatic implements Cloneable {

	private String name;
	private Integer age;
	private Interest interest;

	public UserSemiAutomatic(String name, Integer age, Interest interest) {
		this.name = name;
		this.age = age;
		this.interest = interest;
	}

	@Override
	public UserSemiAutomatic clone() throws CloneNotSupportedException {
		UserSemiAutomatic userCloned = (UserSemiAutomatic) super.clone();
		userCloned.interest = new Interest(interest.getName());

		return userCloned;
	}
}

Błędy architektoniczne, czyli dlaczego klonowanie jest ZŁE

Skoro Java posiada wbudowany mechanizm do obsługi klonowania obiektów, wydawałoby się, że będzie to mechanizm dopracowany i polecany przy rozwiązywaniu podobnych problemów. Niestety ta część API Javy powstała dość dawno i zawiera bardzo dużo błędów architektonicznych, uniemożliwiających wręcz wykorzystanie jej w wielu sytuacjach. Nawet sam Josh Bloch opisuje to rozwiązanie jako deeply broken.

Spośród błędów projektowych klonowania w Javie najważniejsze to:

  • Interfejs Cloneable nie ma metody clone()! – jest to największy błąd, który skutkuje licznymi negatywnymi konsekwencjami:
    • oznaczenie klasy jako Cloneable nic nie mówi, na temat tego, co można z danym obiektem zrobić. Wskazuje jedynie, że „coś” ma zadziać się lokalnie, w implementacji Javy;
    • przechowując np. kolekcję obiektów implementujących Cloneable, nie można na nich wykonać polimorficznej operacji clone;
    • nie można rzutować obiektu na Cloneable i wykonać na nim operacji clone();
    • również rzutowanie obiektu do klasy Object niewiele pomoże, ponieważ zadeklarowana w niej metoda clone jest chroniona (protected);
  • Przy powyższych ograniczeniach dla klienta klasy nie ma właściwie żadnych korzyści z takiego rozwiązania. Równie dobrze można utworzyć dowolną inną metodę (np. copy) i zrealizować w niej kopiowania/klonowania w inny, dowolny sposób;
  • Mechanizm klonowania może okazać się zdradliwy, ponieważ zamiast tworzyć na nowo pola obiektów, opiera swoje działanie na ich kopiowaniu. W praktyce oznacza to, że przy tworzeniu nowych obiektów nie jest wywoływany ich konstruktor i inicjalizacja stanu obiektu może przebiec w inny sposób, niż programista to pierwotnie zakładał;
  • Standardowa implementacja polega na wywołaniu metody rodzica: super.clone(), w całej strukturze dziedziczenia, aż do klasy Object. Takie rozwiązanie skutkuje tak zwanym płytkim kopiowaniem (shallow copy). W efekcie czego sklonowane obiekty mogą współdzielić między sobą stan. To z kolei może prowadzić do niespodziewanego modyfikowania obiektów zależnych i trudnych do wychwycenia błędów. Alternatywą tego rozwiązania jest kopiowanie głębokie (deep copy);
Java clone - inne sposoby

Java clone – inne sposoby

Skoro nie klonowanie, to co?

1. Copy constructor

Wzorzec projektowy copy constructor to jedna z częściej wybieranych alternatyw dla klonowania obiektów. W tym wzorcu jeden z konstruktorów klasy przyjmuje obiekt, na podstawie którego inicjowany jest wewnętrzny stan obiektu. Nowo tworzony obiekt i obiekt przekazywany jako argument są zazwyczaj tego samego typu, jednak nie jest to wymóg.

public class UserCopyConstructor {

	private String name;
	private Integer age;
	private List<Interest> interests;

	public UserCopyConstructor(UserCopyConstructor userCopyConstructor) {
		name = userCopyConstructor.name;
		age = userCopyConstructor.age;
		interests = userCopyConstructor.interests;
	}

	public UserCopyConstructor(String name, Integer age) {
		this.name = name;
		this.age = age;
	}
}

Powyższy przykład konstruktora kopiującego realizuje kopiowanie płytkie (shallow copy), jednak nic nie stoi na przeszkodzie, żeby wykorzystać ten wzorzec do kopiowania głębokiego (deep copy).

Copy constructor jest bardzo często wykorzystywany w operacjach na kolekcjach w Javie.

new ArrayList(new LinkedList());

new LinkedHashSet(new HashSet());

2. Statyczna metoda kopiująca

Jest to modyfikacja metody z copy constructor, w której konstruktor został zamieniony na statyczną metodę kopiującą.

public class UserCopyMethod {

	private String name;
	private Integer age;
	private List<Interest> interests;

	public static UserCopyMethod newInstance(UserCopyMethod userCopyConstructor) {
		UserCopyMethod newUserCopyMethod = new UserCopyMethod();
		newUserCopyMethod.name = userCopyConstructor.name;
		newUserCopyMethod.age = userCopyConstructor.age;
		newUserCopyMethod.interests = userCopyConstructor.interests;

		return newUserCopyMethod;
	}
}

3. Klonowanie przez serializację

Serializacja jest dość prostym sposobem na osiągnięcie kopiowania głębokiego.

Trzeba jednak pilnować, żeby klasa główna oraz wszystkie obiekty zależne były serializowane. W przeciwnym wypadku dostaniemy wyjątek: java.io.NotSerializableException.

class SerializationService {

	public static <T> T deepCopy(T object) {
		try {
			ByteArrayOutputStream baos = new ByteArrayOutputStream();
			ObjectOutputStream oos = new ObjectOutputStream(baos);
			oos.writeObject(object);
			ByteArrayInputStream bais = new ByteArrayInputStream(baos.toByteArray());
			ObjectInputStream ois = new ObjectInputStream(bais);
			return (T) ois.readObject();

		} catch (IOException | ClassNotFoundException e) {
			throw new RuntimeException(e);
		}
	}
}

public class UserSerialization implements Serializable {

	private String name;
	private Integer age;
	private List<Interest> interests;

	public UserSerialization deepCopy() {
		return SerializationService.deepCopy(this);
	}
}

4. Klonowanie z wykorzystaniem Apache Commons

Apache Commons udostępnia gotową funkcjonalność klonowania obiektów. Wykorzystanie jej jest wyjątkowo proste, wystarczy wywołać gotową metodę z klasy narzędziowej: SerializationUtils.

W tym wypadku klonowanie wewnętrznie zostało zaimplementowane z wykorzystaniem serializacji, dlatego wszystkie uwarunkowania są prawie identyczne jak w poprzednim przykładzie.

UserSerialization userCloned = (UserSerialization) SerializationUtils.clone(user);

Testy porównawcze

Test polegał na przygotowaniu i uruchomieniu 1 000 000 (milion) razy niezależnych funkcji kopiujących obiekty i zmierzeniu czasu ich wykonania.

Cały kod testu dostępny jest na repozytorium.

  • Utworzenie obiektu – test polegał na samym tworzeniu nowego obiektu przez operator new
  • Klonowanie płytkie – na początku został utworzony jeden obiekt, a następnie był klonowany przy każdej iteracji testu
  • Klonowanie głębokie – podobnie jak przykład wyżej, tylko dla klonowania głębokiego
  • Serializacja – uruchomienie klonowania głębokiego przez serializację
  • Apache Commons – klonowanie z wykorzystaniem biblioteki Apache Commons wewnętrznie zrealizowane jest przez serializację i deserializację obiektu
  • Copy constructor – kopiowanie głębokie z wykorzystaniem wzorca projektowego copy constructor
Utworzenie obiektu Klonowanie płytkie Klonowanie głębokie Serializacja Apache Commons Copy constructor
23ms 15ms 55ms 23485ms 24429ms 60ms

Wnioski

Kopiowanie obiektów jest bardzo przydatną  i dość często realizowaną funkcjonalnością, dlatego warto chwilę się zastanowić, by dobrze ją realizować.

Zważywszy na liczne wady wbudowanego rozwiązania do klonowania, warto poszukać alternatywnych rozwiązań.

Przy ręcznej realizacji tej funkcjonalności bardzo dobrze sprawuje się wzorzec projektowy copy constructor. Przy jego pomocy można bardzo precyzyjnie określić, które obiekty mają być kopiowane, a które referencje zachowane w obecnej formie.

W przypadku chęci zautomatyzowania całego procesu można posłużyć się serializacją. To rozwiązanie dużo łatwiej jest zrealizować, jednak nie daje tak rozbudowanych możliwości konfiguracji. Jest też zdecydowanie wolniejsze, jednak to nie zawsze ma znaczenie.

 

kierunek java

1 Comment
Share:
hashCode i equals

hashCode i equals – co grozi Ci za złamanie kontraktu między metodami equals i hashCode?

Metody hashCode i equals to jedne z podstawowych metod wykorzystywanych w Javie. Ich deklaracja znajduje się już w klasie Object. Mimo iż obie metody posiadają domyślną implementację, to wykorzystanie jej lub próba napisania własnej, może przysporzyć programistom nie lada problemów.

public boolean equals(Object obj)
public int hashCode()

Porównywanie typów prostych w Javie

Do porównywania typów prostych, takich jak liczby całkowite, liczby zmiennoprzecinkowe, czy wartości boolean przeznaczony jest standardowy operator porównania ==.

1 == 1

1.5 == 2.5

true == false

 

Porównywanie obiektów w Javie [equals]

W przypadku obiektów sprawa nie jest już tak prosta, jak w przypadku typów prostych. Operator porównania może okazać się zwodniczy.

Operator porównania ==

Java udostępnia standardowy operator porównania ==, który sprawdza, czy obiekty są „takie same – identyczne”. Co w praktyce oznacza, że muszą znajdować się dokładnie w tym samym miejscu w pamięci komputera. Jednak nawet jeżeli obiekty będą miały taką samą wartość, np. liczba 1 (typ Integer), to mogą znajdować się w innym miejscu w pamięci.

Metoda equals

Kolejny sposób porównywania obiektów to metoda equals, która w zamyśle wykorzystana jest do porównania obiektów wg ich wartości. Jednak jej ostateczne zachowanie zależne jest od implementacji. Przykładowo, domyślna implementacja to standardowy operator porównania, sprawdzający identyczność obiektów.

    public boolean equals(Object obj) {
        return (this == obj);
    }

Klasy typów opakowujących (np. Integer, Long itp.) oraz String nadpisują implementację metody equals, w celu porównywania obiektów po wartości.

Dla własnych klas taką implementację należy napisać samemu.

Metoda hashCode

Metoda hashCode powstała w celach optymalizacyjnych. Jej zadaniem jest wygenerowanie dla obiektu szybkiego skrótu (hashu) w formie liczby całkowitej.

Hash generowany jest na podstawie stanu obiektu – wartości pól obiektu, które wyróżniają go od innych obiektów tej samej klasy. Powstały kod powinien zapewniać jak największą unikatowość – tak by możliwie niewielka liczba obiektów mogła wygenerować identyczny kod.

HashCode jest wykorzystywana między innymi przez wbudowane w Javie struktury danych (np. HashMap). Najpierw obiekty są dzielone na tak zwane kubełki, wg wartości kodu hashCode. A dopiero następnie są między sobą porównywane z wykorzystaniem metody equals. Dzięki temu nie ma potrzeby porównywania ze sobą wszystkich obiektów.

Kontrakt hashCode i equals

Kontrakt hashCode i equals

Kontrakt między metodami hashCode() i equals()

O tych metodach zazwyczaj mówi się jednocześnie, ponieważ został zdefiniowany między nimi kontrakt. Jego wypełnienie chroni programistę przed trudnymi do zdiagnozowania błędami.

  1. Każde wywołanie metody hashCode na tym samym obiekcie musi kończyć się zwróceniem tej samej liczy całkowitej.
  2. Jeżeli dwa obiekty są sobie równe (wg metody equals), to ich hashCode również musi być równy.
  3. Jeżeli obiekty są różne (wg metody equals), to ich hashCode może być równy, jednak ze względów wydajnościowych powinno to być unikane.
  4. Relacja wyznaczona metodą equals musi być zwrotna, czyli dla każdej zmiennej x różnej od null wyrażenie x.equals(x) musi zwracać wartość = true.
  5. Relacja wyznaczona metodą equals musi być symetryczna, czyli dla każdej pary zmiennych x i y, wyrażenie x.equals(y) ma wartość true i wtedy i tylko wtedy gdy y.equals(x) = true.
  6. Relacja wyznaczona metodą equals musi być przechodnia, czyli dla dowolnych zmiennych z, y i z, jeżeli x.equals(y) = true oraz y.equals(z) = true, to x.equals(z) musi również = true.
  7. Relacja wyznaczona metodą equals musi być spójna, czyli każdorazowe wywołanie x.equals(y) (przy założeniu, że między wywołaniami obiekty nie były modyfikowane) zawsze musi zwracać tę samą wartość – zawsze true albo zawsze false.
  8. Każdy obiekt jest różny od null, czyli wywołanie x.equals(null) dla obiektu x różnego od null, zawsze musi zwrócić false.

Potencjalne konsekwencje niezachowania kontraktu między equals i hashCode

Brak zachowania kontraktu niestety nie powoduje błędów kompilacji. Może jednak przyczynić się do powstania trudnych do wyśledzenia błędów podczas działania aplikacji.

Jednym z częściej spotykanych problemów jest możliwość wielokrotnego dodania takich samych obiektów do zbioru danych (Set) lub „gubienie” się tych obiektów w secie. Może to przyczynić się do utraty wielu godzin na debugowaniu takiego błędu.

Implementacja metod hashCode i equals

Bardzo często wystarczy automatyczna implementacja wygenerowana przez IDE, Lombok, AutoValue lub inne podobne narzędzia. Tylko w bardzo specyficznych sytuacjach zachodzi potrzeba implementowania tych metod ręcznie.

We własnej implementacji warto zwrócić uwagę między innymi, by zmienne oznaczane jako transient lub których stan jest wyliczany wlocie, nie były porównywane. Tego typu pola, np. po deserializacji obiektu mogą mieć nieaktualne wartości.

Więcej na temat problemów z implementacją i utrzymaniem tych metod można przeczytać w artykule o boilerplate code.

public class User {

	private String name;

	private String username;

	private Integer age;

	@Override
	public int hashCode() {
		int hash = 5;
		hash = 37 * hash + Objects.hashCode(this.name);
		hash = 37 * hash + Objects.hashCode(this.username);
		hash = 37 * hash + Objects.hashCode(this.age);
		return hash;
	}

	@Override
	public boolean equals(Object obj) {
		if (this == obj) {
			return true;
		}
		if (obj == null) {
			return false;
		}
		if (getClass() != obj.getClass()) {
			return false;
		}
		final User other = (User) obj;
		if (!Objects.equals(this.name, other.name)) {
			return false;
		}
		if (!Objects.equals(this.username, other.username)) {
			return false;
		}
		if (!Objects.equals(this.age, other.age)) {
			return false;
		}
		return true;
	}
}
Testy hashCode i equals

Testy hashCode i equals

Jak testować hashCode i equals?

Bardzo często pojawia się pytanie o sensowność testowania implementacji hashCode i equals. Prawda jest jednak taka, że te metody również zawierają logikę. Rezygnacja z ich testowania może przyczynić się do powstania różnego rodzaju błędów, tak samo, jak w przypadku innych metod.

Sytuacja nie jest jednak taka zła, na jaką wygląda. Ponieważ  większość implementacji tych metod jest do siebie zbliżona, można z testów wyciągnąć część wspólną i je również, przynajmniej częściowo, zautomatyzować.

Poniżej przedstawiam listę przykładowych możliwości testowania metod hashCode oraz equals.

1. Własna klasa testująca

W ramach testów przygotowałem klasę testującą kontrakt hashCode i equals: ManualEqualsTester oraz jej przykładowe wykorzystanie w teście UserManualTest.

public class ManualEqualsTester {

	private static final int ITERATIONS = 10;

	private final Object reference, equalReference, secondEqualReference, notEqualReference;

	public ManualEqualsTester(Object reference, Object equalReference, Object secondEqualReference, Object notEqualReference) {
		this.reference = reference;
		this.equalReference = equalReference;
		this.notEqualReference = notEqualReference;
		this.secondEqualReference = secondEqualReference;
	}

	public void verify() {
		testConstancyHashCode();
		testEqualityOfHashCode();
		testIsReflexive();
		testIsSymmetric();
		testIsTransitive();
		testConstancyEquals();
		testNullIsNotEquals();
	}

	private void testConstancyHashCode() {
		int hashCode = reference.hashCode();
		for (int i = 0; i &lt; ITERATIONS; i++) {
			Assert.assertEquals(hashCode, reference.hashCode());
		}
	}

	private void testEqualityOfHashCode() {
		Assert.assertEquals(reference.hashCode(), equalReference.hashCode());
	}

	private void testIsReflexive() {
		Assert.assertTrue(reference.equals(reference));
		Assert.assertTrue(equalReference.equals(equalReference));
		Assert.assertTrue(notEqualReference.equals(notEqualReference));
	}

	private void testIsSymmetric() {
		Assert.assertTrue(reference.equals(equalReference));
		Assert.assertTrue(equalReference.equals(reference));

		Assert.assertFalse(reference.equals(notEqualReference));
		Assert.assertFalse(notEqualReference.equals(reference));
	}

	private void testIsTransitive() {
		Assert.assertTrue(reference.equals(equalReference));
		Assert.assertTrue(equalReference.equals(secondEqualReference));
		Assert.assertTrue(reference.equals(secondEqualReference));
	}

	private void testConstancyEquals() {
		for (int i = 0; i &lt; ITERATIONS; i++) {
			Assert.assertTrue(reference.equals(equalReference));
			Assert.assertTrue(reference.equals(secondEqualReference));
			Assert.assertFalse(reference.equals(notEqualReference));
		}
	}

	private void testNullIsNotEquals() {
		Assert.assertFalse(reference.equals(null));
		Assert.assertFalse(equalReference.equals(null));
		Assert.assertFalse(secondEqualReference.equals(null));
		Assert.assertFalse(notEqualReference.equals(null));
	}
}

 

public class UserManualTest {

	private static ManualEqualsTester manualEqualsTester;

	@BeforeClass
	public static void setUp() throws Exception {
		User user = new User("Tomasz", "Woliński", 100);
		User userEqual = new User("Tomasz", "Woliński", 100);
		User userSecondEqual = new User("Tomasz", "Woliński", 100);
		User userNotEqual = new User("", "", 1);
		manualEqualsTester = new ManualEqualsTester(user, userEqual, userSecondEqual, userNotEqual);
	}

	@Test
	public void testEquality() {
		manualEqualsTester.verify();
	}
}

Warto zrobić coś takiego w celu poznania warunków, jakie muszą spełniać te metody. Jednak w praktyce spokojnie swoje testy można opierać o gotowe rozwiązania, takie jak EqualsVerifier czy Guava, które są opisane w kolejnych punktach.

2. EqualsVerifier

EqualsVerifier to niewielka, ale bardzo pomocna przy testowaniu implementacji equals i hashCode biblioteka. Kod źródłowy oraz szczegółową dokumentację można znaleźć na stronie projektu.

public class UserEqualsVerifierTest {

	@Test
	public void testEquality() {
		EqualsVerifier.forClass(User.class).usingGetClass().verify();
	}
}

Jeżeli metoda equals jest napisana z wykorzystaniem getClass() zamiast instanceof, należy w wywołaniu dodać usingGetClass(), w przeciwnym wypadku jest to zbędne.

3. Guava Testing Library

Guava w swoim arsenale również posiada klasę EqualsTester, która może być bardzo pomocna przy automatycznym testowaniu poprawności metod equals i hashCode.

Biblioteka guava-testlib dostępna jest na repozytorium maven.

package pl.stormit.hashcodeequals;

import com.google.common.testing.EqualsTester;
import org.junit.BeforeClass;
import org.junit.Test;

public class UserGuavaTest {

	private static EqualsTester equalsTester;
	private static User user1, user2;

	@BeforeClass
	public static void setUp() throws Exception {
		equalsTester = new EqualsTester();
		user1 = new User("Tomasz", "Woliński", 1000);
		user2 = new User("Tomasz", "Woliński", 1000);
	}

	@Test
	public void testEquality() {
		equalsTester.addEqualityGroup(user1, user2);
		equalsTester.testEquals();
	}
}

 

Podsumowanie – hashCode i equals

Metody hashCode i equals są na tyle powszechne w Javie, że prędzej czy później każdy programista Java będzie musiał się z nimi zmierzyć. Warto oczywiście znać wszystkie warunki, jakie te metody muszą spełniać, jednak o ile to możliwe, warto unikać pisania ich ręcznie. Ich implementację lepiej powierzyć gotowym bibliotekom lub generatorom kodu. Dzięki czemu zaoszczędzimy nie tylko na samym czasie implementacji, ale również unikniemy konieczności ich testowania i ewentualnych błędów.

 

kierunek java

No comments
Share:
AutoValue

AutoValue

AutoValue to rozwijana przez Google na zasadach wolnej licencji (Apache 2.0) biblioteka pozwalająca łatwiej i przyjemniej obchodzić się z klasami typu value object. Dzięki niej w bardzo prosty sposób można uniknąć czasochłonnego i błędogennego pisania oraz utrzymywania metod equals, hashCode oraz toString.

Poniższy kod przedstawia prostą klasę napisaną z wykorzystaniem AutoValue.

@AutoValue
public abstract class UserWithFactoryMethod {

	static UserWithFactoryMethod create(String name, int age) {
		return new AutoValue_UserWithFactoryMethod(name, age);
	}

	abstract String getName();
	abstract int getAge();
}
UserWithFactoryMethod userWithFactoryMethod
= UserWithFactoryMethod.create("Tomasz", 100);

W celu wygenerowania w pełni funkcjonalnej klasy value object, wystarczy zdefiniować abstrakcyjną klasę (interfejsy nie są wspierane) i dodać dla niej adnotację @AutoValue oraz statyczną metodę fabryczną, która utworzy nowy obiekt. Całą resztą zajmie się AutoValue.

Metoda fabryczna create (nazwa jest dowolna, jednak ze względu na konwencję nazewnictwa zalecam zostawić create) tworzy nowy obiekt klasy AutoValue_User, która zostanie wygenerowana przez bibliotekę podczas kompilacji. Nazwa nowej klasy to przedrostek AutoValue_ oraz nazwa naszej klasy.

package pl.stormit.autovalue;

import javax.annotation.Generated;

@Generated("com.google.auto.value.processor.AutoValueProcessor")
final class AutoValue_UserWithFactoryMethod extends UserWithFactoryMethod {

	private final String name;
	private final int age;

	AutoValue_UserWithFactoryMethod(
			String name,
			int age) {
		if (name == null) {
			throw new NullPointerException("Null name");
		}
		this.name = name;
		this.age = age;
	}

	@Override
	String getName() {
		return name;
	}

	@Override
	int getAge() {
		return age;
	}

	@Override
	public String toString() {
		return "UserWithFactoryMethod{"
				+ "name=" + name + ", "
				+ "age=" + age
				+ "}";
	}

	@Override
	public boolean equals(Object o) {
		if (o == this) {
			return true;
		}
		if (o instanceof UserWithFactoryMethod) {
			UserWithFactoryMethod that = (UserWithFactoryMethod) o;
			return (this.name.equals(that.getName())) && (this.age == that.getAge());
		}
		return false;
	}

	@Override
	public int hashCode() {
		int h = 1;
		h *= 1000003;
		h ^= this.name.hashCode();
		h *= 1000003;
		h ^= this.age;
		return h;
	}
}

AutoValue_UserWithFactoryMethod jest to pełnoprawna klasa, której kod można podejrzeć i np. debugować. Nie powinno się jednak jej modyfikować, ponieważ zmiany zostaną nadpisane przy najbliższej kompilacji.

Uwagi i spostrzeżenia na temat wygenerowanego kodu:

  • powstała klasa dziedziczy po napisanej przez nas ręcznie. Jeżeli dodatkowo wszystkie odwołania w kodzie będą odnosić się do klasy bazowej, reszta aplikacji może być nieświadoma istnienia klas wygenerowanych przez bibliotekę
  • AutoValue samo utworzyło pola klasy na podstawie abstrakcyjnych getterów
  • ponieważ wszystkie obiekty zarządzane przez bibliotekę są z założenia niemodyfikowalne (immutable), w powstałym kodzie nie ma setterów. Nie można również dodać ich w klasie bazowej, ponieważ utworzone pola są oznaczone jako final.
  • klasa ma wygenerowane metody equals, hashCode oraz toString uwzględniające wszystkie pola
  • adnotacje z abstrakcyjnych getterów są przekopiowane do ich implementacji. Nie ma natomiast możliwości adnotowania utworzonych pól.

Przyczyna powstania biblioteki

Głównym powodem jest chęć automatyzacji wytwarzania oraz utrzymywania powtarzalnego kodu. Więcej na ten temat można przeczytać w artykule o boilerplate code, w tekście przedstawione są również alternatywne rozwiązania dla AutoValue.

AutoValue - wprowadzenie

AutoValue – wprowadzenie

Wprowadzenie do technologii

AutoValue działa jako standardowy procesor adnotacji w obrębie kompilatora javac. Nowy kod powstaje w obrębie tego samego pakietu na podstawie abstrakcyjnej klasy pisanej ręcznie przez programistę.

W celu nadpisania standardowej implementacji biblioteki dla metod hashCode, equals i toString wystarczy je zadeklarować w klasie abstrakcyjnej. Kompilator sam rozpozna, żeby nie generować dla nich implementacji w klasie dziedziczącej AutoValue. Dobrym zwyczajem, aczkolwiek niekoniecznym, jest również oznaczenie tych metod jako final, żeby wyraźnie zaznaczyć, że framework nie modyfikuje już ich zachowania.

Czasami zachodzi potrzeba wykluczenia jakiegoś pola z automatycznie generowanych metod, w tym celu wystarczy nie deklarować abstrakcyjnego gettera, a to pole zadeklarować ręcznie w swojej klasie. Dla AutoValue takie pole będzie niewidoczne.

Generowanie obiektów z wykorzystaniem buildera

Poza podstawowym sposobem generowania obiektów przez factory method, biblioteka oferuje również możliwość tworzenia nowych obiektów z wykorzystaniem wzorca projektowego builder. To rozwiązanie jest bardziej wyrafinowane i daje więcej możliwości. Poniżej zmodyfikowana klasa użytkownika, z podstawową implementacją buildera.

@AutoValue
public abstract class UserWithBuilder {

	abstract String getName();

	abstract int getAge();

	static Builder builder() {
		return new AutoValue_UserWithBuilder.Builder();
	}

	@AutoValue.Builder
	abstract static class Builder {

		abstract Builder setName(String value);

		abstract Builder setAge(int value);

		abstract UserWithBuilder build();
	}
}

 

Biblioteka narzuca wybranie jednego z dwóch sposobów generowania obiektów. Wybór buildera automatycznie generuje prywatny konstruktor w generowanej klasie, tym samym blokując możliwość skorzystania z factory method. W tej sytuacji, jeżeli zajdzie taka konieczność, w metodzie create można skorzystać z nowo utworzonego buildera zamiast konstruktora, w celu ominięcia tego ograniczenia.

UserWithBuilder userWithBuilder
= UserWithBuilder.builder().setName("Tomasz").setAge(100).build();

Deklaracja buildera powinna mieć adnotację @AutoValue.Builder oraz abstrakcyjną metodę zwracającą główny obiekt. Sama nazwa tej metody jest dowolna, jednak zaleca się wykorzystanie nazwy build.

Natomiast klasa buildera może zostać zadeklarowana poza główną klasą, jednak tu również zaleca się jej deklarację jako wewnętrzną statyczną klasę, w celu zachowania przyjętej konwencji.

Na podstawie tych informacji oraz abstrakcyjnych setterów kompilator powinien wygenerować pełną implementację buildera.

package pl.stormit.autovalue;

import javax.annotation.Generated;

@Generated("com.google.auto.value.processor.AutoValueProcessor")
final class AutoValue_UserWithBuilder extends UserWithBuilder {

	private final String name;
	private final int age;

	private AutoValue_UserWithBuilder(
			String name,
			int age) {
		this.name = name;
		this.age = age;
	}

	@Override
	String getName() {
		return name;
	}

	@Override
	int getAge() {
		return age;
	}

	@Override
	public String toString() {
		return "UserWithBuilder{"
				+ "name=" + name + ", "
				+ "age=" + age
				+ "}";
	}

	@Override
	public boolean equals(Object o) {
		if (o == this) {
			return true;
		}
		if (o instanceof UserWithBuilder) {
			UserWithBuilder that = (UserWithBuilder) o;
			return (this.name.equals(that.getName())) && (this.age == that.getAge());
		}
		return false;
	}

	@Override
	public int hashCode() {
		int h = 1;
		h *= 1000003;
		h ^= this.name.hashCode();
		h *= 1000003;
		h ^= this.age;
		return h;
	}

	static final class Builder extends UserWithBuilder.Builder {
		private String name;
		private Integer age;
		Builder() {
		}
		Builder(UserWithBuilder source) {
			this.name = source.getName();
			this.age = source.getAge();
		}
		@Override
		public UserWithBuilder.Builder setName(String name) {
			this.name = name;
			return this;
		}
		@Override
		public UserWithBuilder.Builder setAge(int age) {
			this.age = age;
			return this;
		}
		@Override
		public UserWithBuilder build() {
			String missing = "";
			if (name == null) {
				missing += " name";
			}
			if (age == null) {
				missing += " age";
			}
			if (!missing.isEmpty()) {
				throw new IllegalStateException("Missing required properties:" + missing);
			}
			return new AutoValue_UserWithBuilder(
					this.name,
					this.age);
		}
	}

}

Memoized – czyli keszowanie wyniku funkcji

Niektóre wyliczenia w metodach są bardzo skomplikowane lub pobierane z bazy danych, co w porównaniu do innych metod może zajmować sporo czasu. Dlatego w celach optymalizacyjnych wartości zwracane przez takie metody dość często przechowuje się w prywatnym polu i tylko za pierwszym razem są one wyliczane.

Taki kod, żeby był w pełni poprawny, powinien również uwzględniać wyścigi wątków oraz być dobrze testowany. Zazwyczaj jednak jest to pomijane.

W takiej sytuacji przychodzi z pomocą AutoValue. Wystarczy dodać adnotacja @Memoized, a całą resztę powtarzalnej logiki wygeneruje framework.

@Memoized
String longRunningMethod() {
	try {
		Thread.sleep(1000);
	} catch (InterruptedException ex) {
		throw new RuntimeException(ex);
	}

	return "" + System.currentTimeMillis();
}

Ponieważ ta funkcjonalność realizowana jest przez rozszerzenie biblioteki, tym razem framework wygeneruje aż 2 klasy:

  • $AutoValue_MemoizedExample – jest do standardowa klasa AutoValue dziedzicząca po głównej klasie użytkownika i zawierająca implementację metod: equals, hashCode i toString oraz factory method lub builder
  • AutoValue_MemoizedExample – klasa wygenerowana przez rozszerzenie z nadpisaną implementacją longRunningMethod
final class AutoValue_MemoizedExample extends $AutoValue_MemoizedExample {
	private volatile String longRunningMethod;

	AutoValue_MemoizedExample() {
		super();
	}

	@Override
	String longRunningMethod() {
		if (longRunningMethod == null) {
			synchronized (this) {
				if (longRunningMethod == null) {
					longRunningMethod = super.longRunningMethod();
					if (longRunningMethod == null) {
						throw new NullPointerException("longRunningMethod() cannot return null");
					}
				}
			}
		}
		return longRunningMethod;
	}
}

Dobre praktyki korzystanie z AutoValue

  • unikanie typów pozwalających modyfikować wartości obiektów – ponieważ value objects mają być w pełni niemodyfikowalne, oznacza to, że również wszystkie typy w nich przechowywane powinny być niemodyfikowalne. Nie oznacza to jednak, że fabryka lub builder może przyjmować tylko takie typy. Ważne jest, żeby po ich przyjęciu zapewnić im niezmienność, np. korzystając z ImmutableList, czy ImmutableSet z Guavy.
  • utrzymywanie prostych klas bez zewnętrznych zależności – wszystkie klasy tego typu powinny być możliwie jak najprostsze oraz nie posiadać dodatkowych zależności. Jeżeli logika staje się zbyt rozbudowana, to najwyższy czas, żeby przenieść ją do jakiegoś serwisu i odchudzić value object.
  • tylko jedna referencja do wygenerowanego kodu – mimo iż jest to technicznie możliwe, to do wygenerowanego kodu nie powinno prowadzić więcej niż jedno odwołanie w całej aplikacji. Będzie to wywołanie konstruktora w factory method lub metody build w builderze. Dzięki takiemu podejściu ewentualne zmiany będą dużo mniej problematyczne. Można również łatwiej wprowadzić specyficzne przetwarzanie przed lub po utworzeniu takiego obiektu.
  • oznaczenie wszystkich zaimplementowanych metod jako final – dzięki temu, czytając klasę napisaną przez użytkownika, wyraźnie widać, że te metody nie będą już modyfikowane przez framework. Ma to szczególne znaczenie w przypadku metod equals, hashCode i toString, które domyślnie są generowane przez AutoValue.

Więcej na temat dobrych praktyk można przeczytać w artykule na stronie biblioteki.

Dlaczego warto korzystać?

  • AutoValue nie przenika do API, dzięki czemu programista korzystający z utworzonych klas może być całkowicie nieświadomi, że powstały z pomocą tej biblioteki
  • brak zależności w runtime aplikacji
  • pomijalna różnica w wydajności
  • proste i przejrzyste rozwiązanie oparte na standardowych mechanizmach Javy

Instalacja

Do projektu wystarczy dodać jedną zależność, która jest tylko potrzebna podczas kompilacji aplikacji (scope: provided). Podczas działania aplikacja będzie korzystała z wcześniej wygenerowanych klas i nie potrzebuje już żadnych dodatkowych zależności.

Najnowszą wersję biblioteki można pobrać z repozytorium maven.

<dependency>
	<groupId>com.google.auto.value</groupId>
	<artifactId>auto-value</artifactId>
	<version>1.4-rc1</version>
	<scope>provided</scope>
</dependency>

Wnioski

Dzięki AutoValue można skupić się na logice biznesowej, zostawiając nudną i potencjalnie błędogenną implementację niskopoziomowych szczegółów dla wbudowanego generatora. Ten mechanizm działa podobnie do generowania kodu przez IDE, jednak jego główną przewagą jest automatyczne przegenerowanie kodu i trzymanie go zawsze aktualnym w stosunku do nowo dodanych pól klasy.

Dodatkowo AV jest bardzo ciekawym przykładem wykorzystania Java custom annotation processing, który jest standardowym mechanizmem Javy. Powoduje to, że biblioteka jest ciekawą alternatywą dla Lombok, który opiera swoje działanie na potencjalnie ryzykownym wykorzystaniu wewnętrznej implementacji JDK.

Ogólne wrażenia z wykorzystania biblioteki oceniam bardzo pozytywnie. Korzysta się z niej bardzo przyjemnie i stosunkowo prosto.

 

kierunek java

No comments
Share:
Protocol Buffers XML

Protocol Buffers, czyli 160 razy szybsza alternatywa dla XML’a

Protocol buffers (protobuf) to rozwijany przez Google, niezależny od języka programowania i platformy, rozszerzalny sposób na binarną serializację strukturalnych danych.

Z powodzeniem można go przedstawić jako jedną z alternatyw dla XML’a. Pracę z biblioteką rozpoczynamy od określenia struktury danych oraz utworzenia na jej podstawie kodu źródłowego klas w wybranych języku programowania.  Wygenerowany kod służy do przechowywania tych danych oraz ich obsługi.

Geneza powstania

Google w ramach swoich systemów przesyła dane w tysiącach różnych formatów. Dotychczasowo znane formaty, jak np. XML nie spisywały się dobrze w tak dużej skali ze względu na swoje ograniczenia i narzuty związane z wydajnością. Dzięki Procol Buffers znacząca część tych problemów została rozwiązana.

Obecna wersja proto3 wspiera większość popularnych języków: Java, C++, Python, Java Lite, Ruby, JavaScript, Objective-C, C# oraz PHP.

Od roku 2008 biblioteka jest dostępna publicznie na zasadach wolnej licencji BSD.

Wprowadzenie do technologii

Technologia opiera się na definicjach komunikatów w specjalnym języku pośrednim, który jest przechowywany w plikach  .proto. Poszczególne komunikaty protocol buffer to mała jednostka informacji zawierająca pary nazwa-wartość.

Poniżej plik .proto z definicją użytkownika oraz przykładowe wykorzystanie wygenerowanej klasy.

package stormit;

option java_package = "pl.stormit.protobuf";
option java_outer_classname = "UserProtos";

message User {
   required string name = 1;
   optional string surname = 2;
   required int32 age = 3;

   enum UserType {
      NORMAL = 0;
      ADMIN = 1;
   }

   message Interest {
      required string name = 1;
   }

   repeated Interest interests = 4;
   required UserType userType = 5;
}

 

UserProtos.User.Builder userBuilder = UserProtos.User.newBuilder();
UserProtos.User.Interest.Builder interestBuilder = UserProtos.User.Interest.newBuilder();

userBuilder.setAge(100).setName("Tomasz")
   .setUserType(UserProtos.User.UserType.ADMIN);
userBuilder.addInterests(interestBuilder.setName("Java").build())
   .addInterests(interestBuilder.setName("Protobuf").build());

UserProtos.User user = userBuilder.build();

Użytkownik ma zdefiniowane 3 typy proste: imię, nazwisko oraz wiek. Dodatkowo zdefiniowany jest typ użytkownika w postaci typu wyliczeniowego.  Jest również podana lista zainteresowań (repeated Interest interests = 4;).

Składnia na pierwszy rzut oka wygląda na podobną do Javy, więc podstawowe funkcjonalności nie powinny sprawić większej trudności. Typ UserType oraz Interest mogłyby być zdefiniowane poza typem głównym User, wtedy byłyby zmapowane na osobne klasy, a nie klasy zagnieżdżone.

Kolejną rzeczą, która wymaga wyjaśnienia są przypisane wartości w definicji komunikatu (” = 1″, ” = 2″). Nie są to jak w Javie domyślne wartości, a unikatowe tagi wykorzystywane przy binarnej serializacji. Na podstawie tego znacznika pole jest rozpoznawane podczas serializacji i deserializacji, dlatego po jego ustawieniu nie powinno być już zmieniane. W celach optymalizacyjnych do często występujących elementów (np. repeated) należy korzystać z tagów z przedziału 1-15, ponieważ do zapisania tych liczb potrzeba najmniej pamięci.

 

Protocol Buffer Diagram

Protocol Buffer Diagram

Na podstawie utworzonych definicji komunikatów podczas kompilacji powstają docelowe klasy.

Zalecenia odnośnie rozszerzania i modyfikacji plików proto

Komunikatu Protobuf są wstecznie kompatybilne, trzeba jednak przestrzegać pewnych reguł podczas ich modyfikacji:

  • nie wolno zmieniać raz przypisanych wartości tagów dla istniejących już pól
  • nie wolno dodawać ani usuwać wymaganych pól
  • dla nowo dodanych pól muszą zostać wykorzystane nowe, nieużywane wcześniej wartości tagów

Przestrzegając powyższych zaleceń, jedyne co trzeba zrobić, to na nowo przekompilować pliki .proto do Javy. Starsze klasy zwyczajnie zignorują nowe pole podczas obsługi komunikatu.

Dlaczego warto korzystać?

Protocol bufferts przewyższa XML’a w wielu kwestiach, między innymi:

  • jest prostszy
  • komunikaty zajmują znacząco mniej miejsca
  • jest wielokrotnie szybszy
  • generuje klasy do obsługi danych, które zazwyczaj są wygodniejsze w obsłudze niż korzystanie z XML’a

A może jednak XML lub JSON?

  • przekazywane dane są w formacie czytelnym dla człowieka, można również je w bardzo prosty sposób modyfikować (w przeciwieństwie do Protobuf, który realizuje binarną serializację)
  • dane z serwisu można bezpośrednio wyświetlić w przeglądarce
  • dużo lepsze wsparcie do obsługi danych w JavaScript
  • brak narzuconej struktury przesyłanych danych
  • nie ma potrzeby instalacji nowej biblioteki oraz dodatkowego kompilowania klas do jej obsługi
  • XML jest formatem samo opisującym się, to znaczy, że nie ma potrzeby korzystania z definicji komunikatu, żeby go zrozumieć

Wybrane funkcjonalności

Generowanie klas Javy

Generowanie gotowych klas Javy polega na uruchomieniu kompilatora Protobuf na plikach wejściowych .proto podając ścieżkę do źródeł oraz katalog na wygenerowany kod. Poniżej przykładowe wywołanie:

bin/protoc -I=./sources --java_out=./dist ./sources/UserProtos.proto

Zapis strumienia do pliku

UserProtos.User user = DataUtils.createUserProtobuf();

try (FileOutputStream fos = new FileOutputStream("User.proto.ser")) {
   user.writeTo(fos);
} catch (IOException ex) {
   throw new RuntimeException(ex);
}

Odczyt komunikatu z pliku

try (FileInputStream fos = new FileInputStream("User.proto.ser")) {
   UserProtos.User userFromFile = UserProtos.User.parseFrom(fos);

} catch (IOException ex) {
   throw new RuntimeException(ex);
}

Instalacja

Bibliotekę możemy skompilować sami ze źródeł lub skorzystać z już wcześniej skompilowanych wersji na wybrany system. Wszystkie wersje wraz ze źródłami dostępne są na github.

Ja posłużę się prekompilowaną wersją protoc-3.1.0-linux-x86_64.zip. Gotowy kompilator jest dostępny pod ścieżką bin/protoc .

Do projektu, poza skompilowanymi już klasami komunikatów Protocol Buffers, trzeba dodać też samą bibliotekę. Najnowsza jej wersja dostępna jest na maven.

<dependency>
<groupId>com.google.protobuf</groupId>
<artifactId>protobuf-java</artifactId>
<version>3.1.0</version>
</dependency>

Test porównawczy
Protocol Buffers vs Serializacja Java vs JAXB

W ramach testu uruchomiłem 1000 razy serializację użytkownika z przykładu wraz z zapisem do pliku oraz odczyt komunikatu z pliku i jego deserializację.

Test był powtarzany kilkukrotnie i wyniki za każdym razem nieznacznie się różniły, co jest zrozumiałe, ponieważ są to stosunkowo nieduże komunikaty. Dlatego nie należy porównywać wyników co do milisekund, a raczej skupić się porównaniu rządu wielkości.

Wszystkie przykłady oraz kod testów dostępny na github pod adresem: https://github.com/StormITpl/JavaExamples/tree/master/protobuf.

  • Protobuf – domyślna implementacja Protobuf
  • JAXB – domyślna implementacja JAXB
  • JAXB + formatowanie – JAXB z włączonym formatowaniem wyjścia
  • JAXB + contex – JAXB z raz utworzonym kontekstem wykorzystanym przy wszystkich operacjach
  • Serializacja Java – domyślna serializacja Java
  • Serializacja Java + klasy Protobuf – serializacja Java uruchomiona na klasach wygenerowanych przez Protobuf
Protobuf JAXB JAXB + formatowanie JAXB + context Serializacja Java Serializacja Java + klasy Protobuf
zapis 85ms 1936ms 2047ms 113ms 119ms 98ms
odczyt 17ms 2839ms 2419ms 87ms 99ms 161ms
rozmiar komunikatu 32 bytes 211 bytes 267 bytes 211 bytes 487 bytes 224 bytes

Wnioski

Przeprowadzony test potwierdza to, czym chwalą się autorzy biblioteki. Protobuf rzeczywiście okazał się najszybszy, jeżeli chodzi o serializację i deserializację, wygenerował również najmniejszy komunikat. Różnice są szczególnie widoczne, jeżeli zestawimy to z domyślnym serializowaniem do XML’a przez JAXB. W teście wykorzystano stosunkowo nieduży obiekt, przy większej ilości danych różnice byłyby jeszcze bardziej widoczne.

Czy, w związku z powyższym, Protobuf w przyszłości w pełni zastąpi XML i inne podobne formaty?

Zdecydowanie nie. Mimo swoich wielu zalet i rozbudowanej funkcjonalności nie jest to rozwiązanie idealne dla każdej sytuacji, głównie ze względu na wykorzystanie binarnego zapisu komunikatu.

Biblioteka natomiast wydaje się bardzo dobra dla niezależnej domeny integracyjnej w projektach o wysokich wymaganiach wydajnościowych. W mniejszych projektach, bez takich wymagań, koszt utrzymywania dodatkowej technologii prawdopodobnie przewyższyłby korzyści.

 

kierunek java

4 komentarze
Share:
Kurs Java

Kurs Java | Darmowy Kurs Programowania w Javie

Kurs Java

Pragniesz rozpocząć przygodę z programowaniem? A może zastanawiasz się, czy programowanie jest właśnie dla Ciebie? 🤔

Jesteś zdecydowanie w odpowiednim miejscu! Java to język programowania, który chcę Ci przedstawić i zachęcić Cię nim do przygody z programowaniem. Poniżej znajdziesz linki do darmowych materiałów, dzięki którym poznasz podstawy Javy i wejdziesz w świat programowania. 🤗

Pozdrawiam i zapraszam do lektury 📖.

  1. Historia Javy
  2. Instalacja JDK
  3. Pierwszy program „Witaj Świecie!”
  4. Zintegrowane środowisko programistyczne IDE
  5. Komentarze i samodokumentujący się kod
  6. Zmienne i typy danych
  7. Typy proste
  8. Typy obiektowe
  9. Klasa String – metody z przykładami
  10. String – najważniejszy typ danych
  11. Operatory logiczne
  12. Operator przypisania
  13. STAŁE – final
  14. Klasy osłonowe
  15. Operatory matematyczne
  16. Operatory relacyjne
  17. Inkrementacja i dekrementacja
  18. Konwersja i rzutowanie typów
  19. Tablice
  20. Pętle
  21. Instrukcja warunkowa IF ELSE
  22. Switch Case
  23. Enum

 

kierunek java

No comments
Share: