JavaRush /Blog Java /Random-PL /Historia jednego wywiadu: ciekawe pytania
GuitarFactor
Poziom 30
Санкт-Петербург

Historia jednego wywiadu: ciekawe pytania

Opublikowano w grupie Random-PL
Niedawno miałem okazję uczestniczyć w rozmowie kwalifikacyjnej na stanowisko stażysty w jednej z dużych firm IT. Historia jednego wywiadu: ciekawe pytania - 1To była moja pierwsza rozmowa w branży IT i moim zdaniem okazała się ciekawa. W sumie byłem „przesłuchiwany” ponad 3 godziny (poprzedzało to zadanie domowe i sprawdzian w biurze na komputerze). Chcę złożyć hołd ankieterowi, który nie poddał się, gdy odpowiedziałem błędnie na pytanie, ale za pomocą swoich pytań naprowadzających zmusił mnie do przemyślenia tego i dojścia do prawidłowej odpowiedzi. Poniżej przedstawię kilka „szkiców” – moim zdaniem całkiem ciekawych pytań, z których część pozwoliła mi na głębsze zrozumienie pewnych aspektów Javy. Być może dla niektórych te rzeczy będą wydawać się oczywiste, ale myślę, że znajdą się tacy, dla których będzie to przydatne. Poniżej frazy są wyróżnione następującą czcionką: Prowadzący wywiad – pogrubioną czcionką Wyjaśnienia lektora i moje przemyślenia – kursywą Moje odpowiedzi – zwykłą czcionką Tło mamy już za sobą, przejdźmy do rzeczy)

Szkic 1. „Pozornie prosta metoda”

Napisz, jak zaimplementowałbyś metodę zwracającą wynik dzielenia liczby a przez liczbę b. Osoba przeprowadzająca wywiad pisze na kartce papieru
int divide(int a, int b) {
}
*Z niedowierzaniem spojrzałem na kartkę papieru z podpisem metody. Gdzie jest haczyk?* Piszę:
int divide(int a, int b) {
    return a/b;
}
Czy są jakieś problemy związane z tą metodą? *Łapię naprawdę głupiego głupka* Najwyraźniej nie.. Następnie pojawia się uzasadnione pytanie: A co jeśli b=0? *Wow, wyrzucą mnie z tego biura, jeśli będę tak dalej postępował!* O tak, oczywiście. Tutaj mamy argumenty typu int, więc zostanie zgłoszony wyjątek arytmetyczny. Jeśli argumenty były typu float lub double, wynikiem byłaby Infinity. Co zamierzamy z tym zrobić? Zaczynam pisać try/catch
int divide(int a, int b) {
    try {
        return a/b;
    } catch (Exception e) {
        e.printStackTrace();
        return ... // ??? what the hack?
    }
}
*Mogę wrócić i zamrozić: coś musi zostać zwrócone w przypadku błędu. Ale jak odróżnić to „coś” od wyniku obliczeń?* Co zwrócimy? Hm... Zmieniłbym typ zwracanej zmiennej na Integer iw przypadku wyjątku zwróciłbym wartość null. Wyobraźmy sobie, że nie możemy zmienić typu. Czy możemy się jakoś wydostać? Może poza tym zrobimy coś innego? *Oto nadchodzi* Możemy również przekazać to do metody wywołującej! Prawidłowy. Jak to będzie wyglądać?
int divide(int a, int b) throws ArithmeticException{
    return a/b;
}

void callDivide(int a, int b) {
    try {
        divide(a, b);
    } catch (ArithmeticException e) {
        e.printStackTrace();
    }
}
Czy konieczna jest obsługa wyjątku? Tak, ponieważ jawnie przekazujemy to z metody dzielenia. (*Tutaj się myliłem! Poniżej znajdują się pytania prowadzące osobę przeprowadzającą rozmowę kwalifikacyjną, które pozwalają na uzyskanie prawidłowej odpowiedzi*) A wyjątek arytmetyczny — jakiego rodzaju jest to wyjątek — jest zaznaczony czy nie? Jest to wyjątek środowiska wykonawczego, co oznacza, że ​​jest niezaznaczony. *Nadchodzi zabójcze pytanie* Okazuje się, twoimi słowami, że jeśli w sygnaturze metody określiliśmy wyjątek arytmetyczny, to stał się on sprawdzonym wyjątkiem? *Ugh!* Prawdopodobnie... nie. Tak, nie. Jeśli w sygnaturze zaznaczymy rzuty /niezaznaczony wyjątek/, ostrzegamy jedynie, że metoda może zgłosić wyjątek, ale nie jest konieczna obsługa go w metodzie wywołującej. To załatwione. Czy jest coś jeszcze, co możemy zrobić, aby uniknąć błędów? *Po chwili namysłu* Tak, możemy również sprawdzić, czy (b==0). I zastosuj trochę logiki. Prawidłowy. Możemy więc pójść na 3 sposoby:
  • próbuj złapać
  • rzuty – przekazanie do metody wywołującej
  • sprawdzanie argumentów
W takim przypadku, dividektóra metoda jest Twoim zdaniem lepsza?
Zdecydowałbym się przekazać wyjątek do metody wywołującej, ponieważ ... w metodzie dzielenia nie jest jasne, jak przetworzyć ten wyjątek i jaki typ wyniku intma zostać zwrócony w przypadku błędu. W metodzie wywołującej użyłbym argumentu b, aby sprawdzić, czy jest on równy zero. Wydaje się, że ta odpowiedź zadowoliła rozmówcę, ale szczerze mówiąc, nie jestem pewien, czy ta odpowiedź jest jednoznaczna))

Szkic 2. „Kto jest szybszy?”

Po standardowym pytaniu, czym różni się ArrayList od LinkedList, pojawiło się następujące pytanie: Co stanie się szybciej - wstawienie elementu na środek ArrayListczy na środek LinkedList? *Tu podskoczyłem, przypomniało mi się, że wszędzie czytam coś w stylu „użyj LinkedList, aby wstawić lub usunąć elementy na środku listy”. W domu nawet dwukrotnie sprawdziłem wykłady JavaRush, jest tam takie zdanie: „jeśli zamierzasz wstawić (lub usunąć) wiele elementów w środku kolekcji, lepiej użyj LinkedList. We wszystkich innych przypadkach - ArrayList”. Odpowiedź automatyczna* Dzięki . będzie szybciej LinkedList. Wyjaśnij proszę
  1. Aby wstawić element w środku ArrayList, odnajdujemy go na liście w czasie stałym, a następnie przeliczamy indeksy elementów znajdujących się na prawo od wstawianego, w czasie liniowym.
  2. Dla LinkedList.. Najpierw dochodzimy do środka w czasie liniowym, a następnie wstawiamy element w czasie stałym, zmieniając połączenia dla elementów sąsiednich.
Okazuje się, co jest szybsze? Hmm... Okazuje się, że jest tak samo. Ale kiedy jest LinkedListszybciej? Okazuje się, że gdy wstawimy go do pierwszej połowy listy. Przykładowo, jeśli wstawisz go na samym początku, ArrayListbędziesz musiał przeliczyć wszystkie indeksy aż do samego końca, ale LinkedListkonieczna będzie jedynie zmiana odniesienia do pierwszego elementu. Morał: nie wierz dosłownie wszystkiemu, co jest napisane, nawet w JavaRush!)

Szkic 3. „Gdzie bylibyśmy bez równych sobie i hashcode!”

Rozmowa na temat równości i hashcode była bardzo długa - jak to zastąpić, jaka jest implementacja Object, co dzieje się pod maską, kiedy wstawia się element HashMapitp. Podam tylko kilka punktów, które moim zdaniem są interesujące* Wyobraź sobie, że stworzyliśmy klasę
public class A {
    int id;

    public A(int id) {
        this.id = id;
    }
}
I nie zastąpili equalsi hashcode. Opisz, co się stanie, gdy kod zostanie wykonany
A a1 = new A(1);
A a2 = new A(1);
Map<A, String> hash = new HashMap<>();
hash.put(a1, "1");
hash.get(a2);
*Dobrze, że przed rozmową poświęciłem kilka dni na zrozumienie podstawowych algorytmów, ich złożoności i struktur danych - bardzo mi to pomogło, dzięki CS50!*
  1. Utwórz dwie instancje klasy A

  2. Tworzymy pustą mapę, która domyślnie posiada 16 koszy. Klucz jest obiektem klasy A, w którym metody equalsi nie są nadpisywane hashcode.

  3. Umieść to a1na mapie. Aby to zrobić, najpierw obliczamy skrót a1.

    Ile będzie równy skrót?

    Adres komórki w pamięci jest implementacją metody z klasyObject

  4. Na podstawie hasha obliczamy indeks koszyka.

    Jak możemy to obliczyć?

    *Niestety, nie udzieliłem tutaj jasnej odpowiedzi. Masz długą liczbę - hash, a jest 16 segmentów - jak zdefiniować indeks, aby obiekty o różnych skrótach były równomiernie rozmieszczone w segmentach? Mogę sobie wyobrazić, że indeks jest obliczany w ten sposób:

    int index = hash % buckets.length

    Już w domu widziałem, że oryginalna implementacja w kodzie źródłowym jest nieco inna:

    static int indexFor(int h, int length)
    {
        return h & (length - 1);
    }
  5. Sprawdzamy, czy nie ma kolizji i wstawiamy a1.

  6. Przejdźmy do metody get. Instancje a1 i a2 na pewno mają inny hash(inny adres w pamięci), więc dla tego klucza nic nie znajdziemy

    A co jeśli zdefiniujemy to na nowo tylko hashcodew klasie A i spróbujemy wstawić do hashmapy najpierw parę z kluczem a1, a potem z a2?

    Następnie najpierw znajdziemy żądany koszyk hashcode- ta operacja zostanie wykonana poprawnie. Następnie zacznijmy przeglądać obiekty Entryna liście LinkedList dołączonej do koszyka i porównajmy klucze według equals. Ponieważ equalsnie zostanie przesłonięty, wówczas z klasy zostanie pobrana podstawowa implementacja Object- porównanie przez odniesienie. a1 i a2 mają gwarancję różnych łączy, więc „pominiemy” wstawiony element a1, a a2 zostanie umieszczone na liście LinkedList jako nowy węzeł.

    Jaki jest wniosek? Czy można go użyć jako klucza w HashMapobiekcie bez nadpisywania equalshashcode?

    Nie, nie możesz.

Szkic 4. „Złammy to celowo!”

Po pytaniach dotyczących błędu i wyjątku pojawiło się następujące pytanie: Napisz prosty przykład, w którym funkcja wyrzuci StackOverflow. *Wtedy przypomniałem sobie, jak nękał mnie ten błąd, gdy próbowałem napisać jakąś funkcję rekurencyjną.* Prawdopodobnie stanie się to w przypadku wywołania rekurencyjnego, jeśli warunek wyjścia z rekurencji zostanie podany niepoprawnie. *Potem zacząłem próbować czegoś sprytnego, w końcu ankieter pomógł, wszystko okazało się proste*
void sof() {
    sof();
}
Czym ten błąd różni się od OutOfMemory? *Nie odpowiedziałem tutaj, dopiero później zrozumiałem, że było to pytanie o znajomość Stackpamięci HeapJava (wywołania i referencje do obiektów przechowywane są w stosie, a same obiekty w pamięci sterty). W związku z tym StackOverflow jest wyrzucany, gdy w pamięci nie ma już miejsca Stackna następne wywołanie metody, a OutOfMemoryw pamięci skończyło się miejsce na obiekty Heap*
To są momenty z rozmowy, które pamiętam. Ostatecznie zostałem przyjęty na staż, więc przede mną 2,5 miesiąca szkolenia i jak wszystko pójdzie dobrze praca w firmie) W razie zainteresowania mogę napisać kolejny artykuł, tym razem mniejszy, z analiza prostego, ale ilustrującego problemu, że zostałem zaproszony na rozmowę kwalifikacyjną w innej firmie. To tyle, mam nadzieję, że ten artykuł pomoże komuś pogłębić lub uporządkować swoją wiedzę. Życzę wszystkim miłej nauki!
Komentarze
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION