CodeGym /Blog Java /Random-PL /Top 50 pytań i odpowiedzi na rozmowie o pracę dla Java Co...
John Squirrels
Poziom 41
San Francisco

Top 50 pytań i odpowiedzi na rozmowie o pracę dla Java Core. Część 2

Opublikowano w grupie Random-PL
Top 50 pytań i odpowiedzi na rozmowie o pracę dla Java Core. Część 1Top 50 pytań i odpowiedzi na rozmowie o pracę dla Java Core.  Część 2 - 1

Wielowątkowość

24. Jak utworzyć nowy wątek w Javie?

Tak czy inaczej wątek jest tworzony przy użyciu klasy Thread. Ale są na to różne sposoby…
  1. Dziedzicz java.lang.Thread .
  2. Zaimplementuj interfejs java.lang.Runnable — konstruktor klasy Thread przyjmuje obiekt Runnable.
Porozmawiajmy o każdym z nich.

Dziedzicz klasę Thread

W tym przypadku sprawiamy, że nasza klasa dziedziczy java.lang.Thread . Ma metodę run() i właśnie tego potrzebujemy. Całe życie i logika nowego wątku będzie w tej metodzie. To trochę jak główna metoda dla nowego wątku. Następnie pozostaje już tylko stworzenie obiektu naszej klasy i wywołanie metody start() . Spowoduje to utworzenie nowego wątku i rozpoczęcie wykonywania jego logiki. Spójrzmy:

/**
* An example of how to create threads by inheriting the {@link Thread} class.
*/
class ThreadInheritance extends Thread {

   @Override
   public void run() {
       System.out.println(Thread.currentThread().getName());
   }

   public static void main(String[] args) {
       ThreadInheritance threadInheritance1 = new ThreadInheritance();
       ThreadInheritance threadInheritance2 = new ThreadInheritance();
       ThreadInheritance threadInheritance3 = new ThreadInheritance();
       threadInheritance1.start();
       threadInheritance2.start();
       threadInheritance3.start();
   }
}
Dane wyjściowe konsoli będą wyglądać mniej więcej tak:
Wątek-1 Wątek-0 Wątek-2
Oznacza to, że nawet tutaj widzimy, że wątki są wykonywane nie w kolejności, ale raczej tak, jak JVM uzna to za stosowne do ich uruchomienia :)

Zaimplementuj interfejs Runnable

Jeśli jesteś przeciwny dziedziczeniu i/lub już odziedziczyłeś inną klasę, możesz użyć interfejsu java.lang.Runnable . Tutaj sprawiamy, że nasza klasa implementuje ten interfejs, implementując metodę run() , tak jak w powyższym przykładzie. Pozostaje tylko stworzyć obiekty Thread . Mogłoby się wydawać, że im więcej linijek kodu, tym gorzej. Ale wiemy, jak zgubne jest dziedziczenie i że za wszelką cenę lepiej go unikać ;) Zerknijcie:

/**
* An example of how to create threads from the {@link Runnable} interface.
* It's easier than easy — we implement this interface and then pass an instance of our object
* to the constructor.
*/
class ThreadInheritance implements Runnable {

   @Override
   public void run() {
       System.out.println(Thread.currentThread().getName());
   }

   public static void main(String[] args) {
       ThreadInheritance runnable1 = new ThreadInheritance();
       ThreadInheritance runnable2 = new ThreadInheritance();
       ThreadInheritance runnable3 = new ThreadInheritance();

       Thread threadRunnable1 = new Thread(runnable1);
       Thread threadRunnable2 = new Thread(runnable2);
       Thread threadRunnable3 = new Thread(runnable3);

       threadRunnable1.start();
       threadRunnable2.start();
       threadRunnable3.start();
   }
}
A oto wynik:
Wątek-0 Wątek-1 Wątek-2

25. Jaka jest różnica między procesem a wątkiem?

Top 50 pytań i odpowiedzi na rozmowie o pracę dla Java Core.  Część 2 - 2Proces i wątek różnią się w następujący sposób:
  1. Działający program nazywany jest procesem, ale wątek jest fragmentem procesu.
  2. Procesy są niezależne, ale wątki są fragmentami procesu.
  3. Procesy mają różne przestrzenie adresowe w pamięci, ale wątki współużytkują wspólną przestrzeń adresową.
  4. Przełączanie kontekstu między wątkami jest szybsze niż przełączanie między procesami.
  5. Komunikacja między procesami jest wolniejsza i droższa niż komunikacja między wątkami.
  6. Wszelkie zmiany w procesie nadrzędnym nie wpływają na proces podrzędny, ale zmiany w wątku nadrzędnym mogą wpływać na wątek podrzędny.

26. Jakie są korzyści z wielowątkowości?

  1. Wielowątkowość pozwala aplikacji/programowi zawsze reagować na dane wejściowe, nawet jeśli już wykonuje jakieś zadania w tle;
  2. Wielowątkowość umożliwia szybsze wykonywanie zadań, ponieważ wątki działają niezależnie;
  3. Wielowątkowość zapewnia lepsze wykorzystanie pamięci podręcznej, ponieważ wątki mogą uzyskiwać dostęp do zasobów pamięci współdzielonej;
  4. Wielowątkowość zmniejsza liczbę wymaganych serwerów, ponieważ jeden serwer może obsługiwać wiele wątków jednocześnie.

27. Jakie są stany w cyklu życia wątku?

Top 50 pytań i odpowiedzi na rozmowie o pracę dla Java Core.  Część 2 - 3
  1. Nowy: W tym stanie obiekt Thread jest tworzony przy użyciu operatora new, ale nowy wątek jeszcze nie istnieje. Wątek nie zostanie uruchomiony, dopóki nie wywołamy metody start() .
  2. Runnable: W tym stanie wątek jest gotowy do działania po starcie () nazywa się metoda. Jednak nie został on jeszcze wybrany przez program planujący wątki.
  3. Uruchomiony: W tym stanie harmonogram wątków wybiera wątek ze stanu gotowości i uruchamia się.
  4. Oczekiwanie/Zablokowany: w tym stanie wątek nie jest uruchomiony, ale nadal żyje lub czeka na zakończenie innego wątku.
  5. Martwy/zakończony: gdy wątek opuszcza metodę run() , jest w stanie martwym lub zakończonym.

28. Czy można dwukrotnie uruchomić wątek?

Nie, nie możemy zrestartować wątku, ponieważ po uruchomieniu i uruchomieniu wątku przechodzi on w stan martwy. Jeśli spróbujemy uruchomić wątek dwukrotnie, zostanie zgłoszony wyjątek java.lang.IllegalThreadStateException . Spójrzmy:

class DoubleStartThreadExample extends Thread {

   /**
    * Simulate the work of a thread
    */
   public void run() {
	// Something happens. At this state, this is not essential.
   }

   /**
    * Start the thread twice
    */
   public static void main(String[] args) {
       DoubleStartThreadExample doubleStartThreadExample = new DoubleStartThreadExample();
       doubleStartThreadExample.start();
       doubleStartThreadExample.start();
   }
}
Wyjątek nastąpi, gdy tylko wykonanie dojdzie do drugiego uruchomienia tego samego wątku. Spróbujcie sami ;) Lepiej zobaczyć to raz, niż słyszeć o tym sto razy.

29. Co jeśli wywołasz run() bezpośrednio bez wywoływania start()?

Tak, z pewnością możesz wywołać metodę run() , ale nie zostanie utworzony nowy wątek, a metoda nie będzie działać w osobnym wątku. W tym przypadku mamy zwykły obiekt wywołujący zwykłą metodę. Jeśli mówimy o metodzie start() , to inna sprawa. Po wywołaniu tej metody JVM uruchamia nowy wątek. Ten wątek z kolei wywołuje naszą metodę ;) Nie wierzysz? Tutaj, spróbuj:

class ThreadCallRunExample extends Thread {

   public void run() {
       for (int i = 0; i < 5; i++) {
           System.out.print(i);
       }
   }

   public static void main(String args[]) {
       ThreadCallRunExample runExample1 = new ThreadCallRunExample();
       ThreadCallRunExample runExample2 = new ThreadCallRunExample();

       // Two ordinary methods will be called in the main thread, one after the other.
       runExample1.run();
       runExample2.run();
   }
}
A dane wyjściowe konsoli będą wyglądać tak:
0123401234
Jak widać, żaden wątek nie został utworzony. Wszystko działało tak, jak na zwykłej lekcji. Najpierw została wykonana metoda pierwszego obiektu, a następnie drugiego.

30. Co to jest wątek demona?

Wątek demona to wątek, który wykonuje zadania o niższym priorytecie niż inny wątek. Innymi słowy, jego zadaniem jest wykonywanie zadań pomocniczych, które muszą być wykonane tylko w połączeniu z innym (głównym) wątkiem. Istnieje wiele wątków demonów, które działają automatycznie, takich jak wyrzucanie elementów bezużytecznych, finalizator itp.

Dlaczego Java kończy wątek demona?

Jedynym celem wątku demona jest zapewnienie obsługi w tle wątku użytkownika. W związku z tym, jeśli główny wątek zostanie zakończony, JVM automatycznie zakończy wszystkie swoje wątki demona.

Metody klasy Thread

Klasa java.lang.Thread udostępnia dwie metody pracy z wątkiem demona:
  1. public void setDaemon(boolean status) — Ta metoda wskazuje, czy będzie to wątek demona. Wartość domyślna to fałsz . Oznacza to, że żadne wątki demona nie zostaną utworzone, chyba że wyraźnie to wyrazisz.
  2. public boolean isDaemon() — Ta metoda zasadniczo pobiera zmienną demona , którą ustawiliśmy przy użyciu poprzedniej metody.
Przykład:

class DaemonThreadExample extends Thread {

   public void run() {
       // Checks whether this thread is a daemon
       if (Thread.currentThread().isDaemon()) {
           System.out.println("daemon thread");
       } else {
           System.out.println("user thread");
       }
   }

   public static void main(String[] args) {
       DaemonThreadExample thread1 = new DaemonThreadExample();
       DaemonThreadExample thread2 = new DaemonThreadExample();
       DaemonThreadExample thread3 = new DaemonThreadExample();

       // Make thread1 a daemon thread.
       thread1.setDaemon(true);

       System.out.println("daemon? " + thread1.isDaemon());
       System.out.println("daemon? " + thread2.isDaemon());
       System.out.println("daemon? " + thread3.isDaemon());

       thread1.start();
       thread2.start();
       thread3.start();
   }
}
Wyjście konsoli:
demon? prawdziwy demon? fałszywy demon? fałszywy wątek demona wątek użytkownika wątek użytkownika
Z danych wyjściowych widzimy, że wewnątrz samego wątku możemy użyć metody static currentThread() , aby dowiedzieć się, który to wątek. Alternatywnie, jeśli mamy odniesienie do obiektu wątku, możemy również dowiedzieć się bezpośrednio z niego. Zapewnia to niezbędny poziom konfigurowalności.

31. Czy można uczynić wątek demonem po jego utworzeniu?

Nie. Jeśli spróbujesz to zrobić, otrzymasz wyjątek IllegalThreadStateException . Oznacza to, że możemy utworzyć wątek demona tylko przed jego uruchomieniem. Przykład:

class SetDaemonAfterStartExample extends Thread {

   public void run() {
       System.out.println("Working...");
   }

   public static void main(String[] args) {
       SetDaemonAfterStartExample afterStartExample = new SetDaemonAfterStartExample();
       afterStartExample.start();
      
       // An exception will be thrown here
       afterStartExample.setDaemon(true);
   }
}
Wyjście konsoli:
Praca... Wyjątek w wątku „main” java.lang.IllegalThreadStateException w java.lang.Thread.setDaemon(Thread.java:1359) w SetDaemonAfterStartExample.main(SetDaemonAfterStartExample.java:14)

32. Co to jest hak zamykający?

Hak zamykający to wątek, który jest niejawnie wywoływany przed zamknięciem wirtualnej maszyny Java (JVM). W ten sposób możemy go użyć do zwolnienia zasobu lub zapisania stanu, gdy wirtualna maszyna Java zostanie zamknięta normalnie lub nieprawidłowo. Możemy dodać hak zamykający za pomocą następującej metody:

Runtime.getRuntime().addShutdownHook(new ShutdownHookThreadExample());
Jak pokazano na przykładzie:

/**
* A program that shows how to start a shutdown hook thread,
* which will be executed right before the JVM shuts down
*/
class ShutdownHookThreadExample extends Thread {

   public void run() {
       System.out.println("shutdown hook executed");
   }

   public static void main(String[] args) {

       Runtime.getRuntime().addShutdownHook(new ShutdownHookThreadExample());

       System.out.println("Now the program is going to fall asleep. Press Ctrl+C to terminate it.");
       try {
           Thread.sleep(60000);
       } catch (InterruptedException e) {
           e.printStackTrace();
       }
   }
}
Wyjście konsoli:
Teraz program zasypia. Naciśnij Ctrl + C, aby go zakończyć. hak zamykający wykonany

33. Co to jest synchronizacja?

W Javie synchronizacja to możliwość kontrolowania dostępu wielu wątków do dowolnego udostępnionego zasobu. Gdy wiele wątków próbuje jednocześnie wykonać to samo zadanie, możesz uzyskać nieprawidłowy wynik. Aby rozwiązać ten problem, Java używa synchronizacji, która umożliwia uruchamianie tylko jednego wątku w danym momencie. Synchronizację można osiągnąć na trzy sposoby:
  • Synchronizacja metody
  • Synchronizacja określonego bloku
  • Synchronizacja statyczna

Synchronizacja metody

Zsynchronizowana metoda służy do blokowania obiektu dla dowolnego udostępnionego zasobu. Gdy wątek wywołuje zsynchronizowaną metodę, automatycznie uzyskuje blokadę obiektu i zwalnia ją, gdy wątek zakończy swoje zadanie. Aby to zadziałało, musisz dodać synchronized słowo kluczowe. Możemy zobaczyć, jak to działa, patrząc na przykład:

/**
* An example where we synchronize a method. That is, we add the synchronized keyword to it.
* There are two authors who want to use one printer. Each of them has composed their own poems
* And of course they don’t want their poems mixed up. Instead, they want work to be performed in * * * order for each of them
*/
class Printer {

   synchronized void print(List<String> wordsToPrint) {
       wordsToPrint.forEach(System.out::print);
       System.out.println();
   }

   public static void main(String args[]) {
       // One object for two threads
       Printer printer  = new Printer();

       // Create two threads
       Writer1 writer1 = new Writer1(printer);
       Writer2 writer2 = new Writer2(printer);

       // Start them
       writer1.start();
       writer2.start();
   }
}

/**
* Author No. 1, who writes an original poem.
*/
class Writer1 extends Thread {
   Printer printer;

   Writer1(Printer printer) {
       this.printer = printer;
   }

   public void run() {
       List<string> poem = Arrays.asList("I ", this.getName(), " Write", " A Letter");
       printer.print(poem);
   }

}

/**
* Author No. 2, who writes an original poem.
*/
class Writer2 extends Thread {
   Printer printer;

   Writer2(Printer printer) {
       this.printer = printer;
   }

   public void run() {
       List<String> poem = Arrays.asList("I Do Not ", this.getName(), " Not Write", " No Letter");
       printer.print(poem);
   }
}
A wyjście konsoli jest takie:
I Thread-0 Napisz list Nie wątpię-1 Nie piszę żadnego listu

Blok synchronizacji

Zsynchronizowany blok może służyć do przeprowadzania synchronizacji dowolnego konkretnego zasobu w metodzie. Powiedzmy, że w dużej metodzie (tak, nie należy ich pisać, ale czasem się zdarzają) z jakiegoś powodu trzeba zsynchronizować tylko małą sekcję. Jeśli umieścisz cały kod metody w zsynchronizowanym bloku, będzie ona działać tak samo jak metoda zsynchronizowana. Składnia wygląda następująco:

synchronized ("object to be locked") {
   // The code that must be protected
}
Aby uniknąć powtórzenia poprzedniego przykładu, utworzymy wątki z wykorzystaniem anonimowych klas, czyli od razu zaimplementujemy interfejs Runnable.

/**
* This is how a synchronization block is added.
* Inside the block, you need to specify which object's mutex will be acquired.
*/
class Printer {

   void print(List<String> wordsToPrint) {
       synchronized (this) {
           wordsToPrint.forEach(System.out::print);
       }
       System.out.println();
   }

   public static void main(String args[]) {
       // One object for two threads
       Printer printer = new Printer();

       // Create two threads
       Thread writer1 = new Thread(new Runnable() {
           @Override
           public void run() {
               List<String> poem = Arrays.asList("I ", "Writer1", " Write", " A Letter");
               printer.print(poem);
           }
       });
       Thread writer2 = new Thread(new Runnable() {
           @Override
           public void run() {
               List<String> poem = Arrays.asList("I Do Not ", "Writer2", " Not Write", " No Letter");
               printer.print(poem);
           }
       });

       // Start them
       writer1.start();
       writer2.start();
   }
}

}
A wyjście konsoli jest takie:
I Writer1 Napisz list Nie piszę Writer2 Nie piszę żadnego listu

Synchronizacja statyczna

Jeśli zsynchronizujesz metodę statyczną, blokowanie nastąpi w klasie, a nie w obiekcie. W tym przykładzie przeprowadzamy synchronizację statyczną, stosując słowo kluczowe synchronized do metody statycznej:

/**
* This is how a synchronization block is added.
* Inside the block, you need to specify which object's mutex will be acquired.
*/
class Printer {

   static synchronized void print(List<String> wordsToPrint) {
       wordsToPrint.forEach(System.out::print);
       System.out.println();
   }

   public static void main(String args[]) {

       // Create two threads
       Thread writer1 = new Thread(new Runnable() {
           @Override
           public void run() {
               List<String> poem = Arrays.asList("I ", "Writer1", " Write", " A Letter");
               Printer.print(poem);
           }
       });
       Thread writer2 = new Thread(new Runnable() {
           @Override
           public void run() {
               List<String> poem = Arrays.asList("I Do Not ", "Writer2", " Not Write", " No Letter");
               Printer.print(poem);
           }
       });

       // Start them
       writer1.start();
       writer2.start();
   }
}
A wyjście konsoli jest takie:
Nie jestem pisarzem2 Nie piszę żadnego listu. Ja pisarz1. Napisz list

34. Co to jest zmienna lotna?

W programowaniu wielowątkowym słowo kluczowe volatile służy do zabezpieczania wątków. Kiedy zmodyfikowana zmienna jest modyfikowana, zmiana jest widoczna dla wszystkich innych wątków, więc zmienna może być używana przez jeden wątek na raz. Używając słowa kluczowego volatile , możesz zagwarantować, że zmienna jest bezpieczna dla wątków i przechowywana we współdzielonej pamięci oraz że wątki nie będą przechowywać jej w swoich pamięciach podręcznych. Jak to wygląda?

private volatile AtomicInteger count;
Po prostu dodajemy volatile do zmiennej. Należy jednak pamiętać, że nie oznacza to całkowitego bezpieczeństwa wątków... W końcu operacje na zmiennej mogą nie być atomowe. To powiedziawszy, możesz używać klas Atomic , które wykonują operacje atomowo, tj. w pojedynczej instrukcji procesora. W pakiecie java.util.concurrent.atomic istnieje wiele takich klas .

35. Co to jest impas?

W Javie zakleszczenie jest czymś, co może się zdarzyć w ramach wielowątkowości. Zakleszczenie może wystąpić, gdy wątek czeka na blokadę obiektu uzyskaną przez inny wątek, a drugi wątek czeka na blokadę obiektu uzyskaną przez pierwszy wątek. Oznacza to, że dwa wątki czekają na siebie i wykonywanie ich kodu nie może być kontynuowane. Top 50 pytań i odpowiedzi na rozmowie o pracę dla Java Core.  Część 2 - 4Rozważmy przykład, który ma klasę implementującą Runnable. Jego konstruktor pobiera dwa zasoby. Metoda run() uzyskuje dla nich blokadę w kolejności. Jeśli utworzysz dwa obiekty tej klasy i przekażesz zasoby w innej kolejności, możesz łatwo wpaść w impas:

class DeadLock {

   public static void main(String[] args) {
       final Integer r1 = 10;
       final Integer r2 = 15;

       DeadlockThread threadR1R2 = new DeadlockThread(r1, r2);
       DeadlockThread threadR2R1 = new DeadlockThread(r2, r1);

       new Thread(threadR1R2).start();
       new Thread(threadR2R1).start();
   }
}

/**
* A class that accepts two resources.
*/
class DeadlockThread implements Runnable {

   private final Integer r1;
   private final Integer r2;

   public DeadlockThread(Integer r1, Integer r2) {
       this.r1 = r1;
       this.r2 = r2;
   }

   @Override
   public void run() {
       synchronized (r1) {
           System.out.println(Thread.currentThread().getName() + " acquired resource: " + r1);

           try {
               Thread.sleep(1000);
           } catch (InterruptedException e) {
               e.printStackTrace();
           }

           synchronized (r2) {
               System.out.println(Thread.currentThread().getName() + " acquired resource: " + r2);
           }
       }
   }
}
Wyjście konsoli:
Pierwszy wątek uzyskał pierwszy zasób Drugi wątek uzyskał drugi zasób

36. Jak uniknąć impasu?

Ponieważ wiemy, jak dochodzi do impasu, możemy wyciągnąć pewne wnioski...
  • W powyższym przykładzie zakleszczenie występuje ze względu na fakt, że mamy zagnieżdżone blokowanie. Oznacza to, że mamy zsynchronizowany blok wewnątrz zsynchronizowanego bloku. Aby tego uniknąć, zamiast zagnieżdżania, należy utworzyć nową wyższą warstwę abstrakcji, przenieść synchronizację na wyższy poziom i wyeliminować zagnieżdżone blokowanie.
  • Im więcej blokad wykonasz, tym większe prawdopodobieństwo zakleszczenia. Dlatego za każdym razem, gdy dodajesz zsynchronizowany blok, musisz zastanowić się, czy naprawdę go potrzebujesz i czy możesz uniknąć dodania nowego.
  • Używając Thread.join() . Możesz także wpaść w impas, gdy jeden wątek czeka na inny. Aby uniknąć tego problemu, można rozważyć ustawienie limitu czasu dla metody join() .
  • Jeśli mamy jeden wątek, to nie będzie impasu ;)

37. Co to jest sytuacja wyścigu?

Jeśli w prawdziwych wyścigach biorą udział samochody, to wyścigi wielowątkowe obejmują wątki. Ale dlaczego? :/ Istnieją dwa wątki, które są uruchomione i mogą uzyskać dostęp do tego samego obiektu. Mogą też próbować aktualizować stan obiektu udostępnionego w tym samym czasie. Jak na razie wszystko jasne, prawda? Wątki są wykonywane dosłownie równolegle (jeśli procesor ma więcej niż jeden rdzeń) lub sekwencyjnie, przy czym procesor przydziela przeplatane przedziały czasu. Nie możemy zarządzać tymi procesami. Oznacza to, że gdy jeden wątek odczytuje dane z obiektu, nie możemy zagwarantować, że będzie miał czas na zmianę obiektu, ZANIM zrobi to inny wątek. Takie problemy pojawiają się, gdy mamy takie kombinacje typu „sprawdź i działaj”. Co to znaczy? Załóżmy, że mamy instrukcję if , której ciało zmienia sam warunek if, na przykład:

int z = 0;

// Check
if (z < 5) {
// Act
   z = z + 5;
}
Dwa wątki mogłyby jednocześnie wejść do tego bloku kodu, gdy z nadal wynosi zero, a następnie oba wątki mogłyby zmienić jego wartość. W rezultacie nie otrzymamy oczekiwanej wartości 5. Zamiast tego otrzymamy 10. Jak tego uniknąć? Musisz zdobyć blokadę przed sprawdzeniem i działaniem, a następnie zwolnić blokadę. Oznacza to, że musisz wprowadzić pierwszy wątek do bloku if , wykonać wszystkie akcje, zmienić z , a dopiero potem dać następnemu wątkowi możliwość zrobienia tego samego. Ale następny wątek nie wejdzie do bloku if , ponieważ z będzie teraz równe 5:

// Acquire the lock for z
if (z < 5) {
   z = z + 5;
}
// Release z's lock
===================================================

Zamiast konkluzji

Chcę podziękować wszystkim, którzy doczytali do końca. To była długa droga, ale wytrzymaliście! Być może nie wszystko jest jasne. To normalne. Kiedy po raz pierwszy zacząłem studiować Javę, nie mogłem pojąć, czym jest zmienna statyczna. Ale nic wielkiego. Przespałem się z tym, przeczytałem jeszcze kilka źródeł i wtedy przyszło zrozumienie. Przygotowanie do rozmowy kwalifikacyjnej jest raczej kwestią akademicką niż praktyczną. W efekcie przed każdą rozmową warto przejrzeć i odświeżyć sobie w pamięci te rzeczy, z których być może nie korzystasz zbyt często.

I jak zawsze, oto kilka przydatnych linków:

Dziękuję wszystkim za przeczytanie. Do zobaczenia wkrótce :) Mój profil na GitHubieTop 50 pytań i odpowiedzi na rozmowie o pracę dla Java Core.  Część 2 - 5
Komentarze
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION