Dzisiejszy wpis jest kontynuacją serii dotyczącej zawiłości języka Java. Jak już wiesz nieznajomość składni danego języka może prowadzić do wielu nieporozumień. Niektóre błędy są możliwe do szybkiego wyłapania, ale niektóre mogą prowadzić do długiej serii debugowania 😔.

Lista wszystkich wpisów dotyczących Zawiłości języka Java:
#1 - Zawiłości języka Java #1
#2 - Zawiłości języka Java #2
#3 - Zawiłości języka Java #3
#4 - Zawiłości języka Java #4

Tablice prymitywne traktowane są jak obiekty

Zacznijmy od przedstawienia problemu w kodzie źródłowym:

1
2
3
4
5
6
7
8
public class ArrayPrimitivesDemo {

  public static void main(String[] args) {
  int[][] intArray = new int[6][6];
  Object[] objectArray = intArray;
  objectArray[0] = "A to psiukus!";
  }
}

Na początku tworzona jest tablica dwuwymiarowa typu prymitywnego int. Potem następuje jej przypisanie do tablicy jednowymiarowej typu Object. Wynika to z faktu, że Java traktuje tablicę int jako obiekt 🤯! Na poniżej przedstawionym obrazku jest to dokładnie widoczne. Jak widać referencja Object wskazuje na to samo miejsce w pamięci co tablica int. Jednak to dopiero później dzieje się wielkie niebezpieczeństwo, ponieważ możliwe jest przypisanie obiektu typu String do pierwszego elementu naszej nowej tablicy Object! Przez bezpieczne rzutowanie pozbawiliśmy się ochrony przypisywania nieodpowiednich obiektów. Kompilator niestety w takiej sytuacji umożliwi nam takie rozwiązanie. Dopiero w trakcie uruchomienia programu otrzymamy wyjątek ArrayStoreException.

Odwzorowanie przypisania referencji
Odwzorowanie przypisania referencji do obiektów w pamięci

Na szczęście dzisiaj już nikt nie pisze kodu produkcyjnego w notatniku (przynajmniej mam taką nadzieję). Współczesne środowiska programistyczne od razu ostrzegą nas przed niebezpieczeństwem. Przykładem jest IDEA Intellij, który wyświetli nam taki komunikat:

Pomocne IDE
IDE potrafi wyłapać wiele nieprawidłowości za nas!

Z tego powodu nie należy się aż tak bać takiej sytuacji. Na pewno uda nam się takie nieporozumienie szybko wyłapać. Przestrzegam przed tego typu zawiłością na egzaminie OCA.

Nadpisywanie metod przez “dzieci”

W sytuacji, gdy rozszerzamy wybraną klasę to możliwe jest nadpisywanie (override) jej metod przez “dzieci” w następujących sytuacjach, gdy:

  • modyfikator dostępu nie będzie bardziej restrykcyjny niż w nadklasie (np. rodzic ma w sygnaturze metody public, a dziecko zmienia zmienia dostęp na protected)
  • typ zwracany nie będzie szerszy niż znajdują się w nadklasie (np. rodzic zwraca klasę Number to dziecko może zwrócić Integer, nie jest możliwy odwrotny układ)
  • typy i ilość parametrów musi się zgadzać (tutaj nie ma wyjątków jeśli chodzi o klasy typów, czyli nie może być to ani nadklasa ani podklasa danego parametru)
  • w sygnaturze metody rodzica znajduje się wyjątek. Wówczas dziecko nadpisując ją może:
    • zredukować liczbę wyjątków jeśli nie ma potrzeby komunikowania, że taki wyjątek może zostać rzucony
    • zmienić typ danego wyjątku na jego podklasę (np. rodzic komunikuje, że rzuca Exception natomiast dziecko może zmienić ten wyjątek na IOException)
    • dopisywać dowolną ilość nowych wyjątków tylko jeśli są one niekontrolowane (rozszerzają RuntimeException) - UWAGA: jest to tylko możliwe z racji tego, że nie muszą one być obsługiwane, równie dobrze może ich nie być w sygnaturze metody (zapraszam do tego artykułu dotyczącego wyjątków)

Mam dla Ciebie mały challenge 😎. Potrafisz wskazać poprawne sposoby nadpisania metod? Oczywiście podaj proszę uzasadnienie dlaczego akceptujesz lub odrzucasz dane rozwiązanie. Jestem bardzo ciekawy Twojej odpowiedzi!

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.LinkedList;
import java.util.List;

public class Superclass {

  public String methodA() {
    return "Superclass";
  }

  List<String> methodB() {
    return new LinkedList<>();
  }

  protected void methodC(String name, Number age) {
    System.out.println(name + " " + age);
  }

  public String methodD() throws Exception {
    return "methodD in Superclass";
  }
}

class Subclass extends Superclass {

  protected String methodA() {
    return "Subclass";
  }

  protected ArrayList<String> methodB() {
    return new ArrayList<>();
  }

  public void methodC(String name, Integer age) {
    System.out.println(name + " " + age);
  }

  public String methodD() throws SQLException, IllegalArgumentException {
    return "methodD in Subclass";
  }
}

Obsługa bloku try-catch nie do końca jest trywialna

Każdy z nas pewnie spotkał taki kod lub sam napisał podobny do tego przedstawionego poniżej:

1
2
3
4
5
6
7
8
9
10
11
12
try {
  Class.forName("org.sqlite.JDBC");
  Connection connection = DriverManager.getConnection("jdbc:sqlite:sample.db");
  Statement statement = connection.createStatement();
  ResultSet resultSet = statement.executeQuery("SELECT * FROM user;");

  while(resultSet.next()) {
    System.out.print(resultSet.getString(1));
  }
} catch(SQLException | ClassNotFoundException ex) {
  ex.printStackTrace();
}

No dobrze, ale to nie o tym chciałem napisać. Do bloku try-catch dodatkowo można dopisać sekcję finally, która wykonuje się zawsze niezależnie od tego czy wyjątek wystąpił czy nie. Jednak co się stanie w sytuacji, gdy w w bloku try albo catch wystąpi instrukcja return, a w bloku finally jeszcze będzie znajdował się kod do wykonania? Sprawdźmy to.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class SampleFinally {

  public String whoIsFirst() {
    try {
      return "I'm first!";
    } finally {
      System.out.println("Not this time!");
    }
  }

  public static void main(String[] args) {
    SampleFinally sampleFinally = new SampleFinally();
    System.out.println(sampleFinally.whoIsFirst());
  }
}

Poniżej zamieszam rezultat wykonania powyższego kodu:

Not this time!
I'm first!

Blok finally wykonał swoje instrukcje zanim metoda zwróciła wykonanie do metody wywołującej! W sumie nie sposób się temu dziwić, bo właśnie po to powstała taka możliwość, aby zamknąć wszystkie otwarte strumienie danych, które wykorzystywaliśmy. W przypadku, gdy wystąpi wyjątek podczas obsługiwania takiego strumienia mamy możliwość go bezpiecznie zamknąć choćby nie wiem co. Od Javy 8 istnieje możliwość ładniejszej obsługi tego typu sytuacji, więc odsyłam Cię do artykułu na ten temat.

Podsumowanie

Z chęcią dzielę się z Tobą ciekawostkami narzędzia, które używam i być może Ty także. Mam w zanadrzu jeszcze kilka ciekawostek ze świata Javy 8, więc mam nadzieję, że czerpiesz radość z ich poznawania tak jak i ja! Jeśli jednak wcześniej słyszałeś o nich to liczę, że w ten sposób mogę pomóc Ci odświeżyć Twoją wiedzę 🧠! Daj znać w komentarzu co myślisz o tej serii. Może sam również masz jakieś doświadczenia z niecodziennym zachowaniem Javy? Podziel się nimi, z chęcią posłucham!