JavaRush /Blog Java /Random-PL /Przerwa kawowa #146. 5 błędów, które popełnia 99% program...

Przerwa kawowa #146. 5 błędów, które popełnia 99% programistów Java. Ciągi znaków w Javie - widok od środka

Opublikowano w grupie Random-PL

5 błędów, które popełnia 99% programistów Java

Źródło: Medium W tym poście dowiesz się o najczęstszych błędach popełnianych przez wielu programistów Java. Przerwa kawowa #146.  5 błędów, które popełnia 99% programistów Java.  Ciągi znaków w Javie – widok od wewnątrz – 1Jako programista Java wiem, jak źle jest spędzać dużo czasu na naprawianiu błędów w kodzie. Czasami zajmuje to kilka godzin. Jednak wiele błędów pojawia się z powodu ignorowania przez programistę podstawowych zasad - czyli są to błędy bardzo niskiego poziomu. Dzisiaj przyjrzymy się niektórym typowym błędom w kodowaniu, a następnie wyjaśnimy, jak je naprawić. Mam nadzieję, że pomoże to Państwu uniknąć problemów w codziennej pracy.

Porównywanie obiektów za pomocą Objects.equals

Zakładam, że znasz tę metodę. Wielu programistów często z niego korzysta. Ta technika, wprowadzona w JDK 7, pomaga szybko porównać obiekty i skutecznie uniknąć irytującego sprawdzania wskaźnika zerowego. Ale ta metoda jest czasami używana nieprawidłowo. Oto co mam na myśli:
Long longValue = 123L;
System.out.println(longValue==123); //true
System.out.println(Objects.equals(longValue,123)); //false
Dlaczego zastąpienie == przez Objects.equals() spowodowałoby zły wynik? Dzieje się tak, ponieważ kompilator == uzyska podstawowy typ danych odpowiadający typowi opakowania longValue , a następnie porówna go z tym podstawowym typem danych. Jest to równoznaczne z automatyczną konwersją stałych przez kompilator na podstawowy typ danych porównawczych. Po użyciu metody Objects.equals() domyślnym podstawowym typem danych stałej kompilatora jest int . Poniżej znajduje się kod źródłowy metody Objects.equals() , gdzie a.equals(b) używa Long.equals() i określa typ obiektu. Dzieje się tak, ponieważ kompilator założył, że stała jest typu int , więc wynik porównania musi być fałszywy.
public static boolean equals(Object a, Object b) {
        return (a == b) || (a != null && a.equals(b));
    }

  public boolean equals(Object obj) {
        if (obj instanceof Long) {
            return value == ((Long)obj).longValue();
        }
        return false;
    }
Znając przyczynę, naprawienie błędu jest bardzo proste. Po prostu zadeklaruj typ danych stałych, np. Objects.equals(longValue,123L) . Powyższe problemy nie pojawią się, jeśli logika będzie ścisła. Musimy przestrzegać jasnych zasad programowania.

Nieprawidłowy format daty

W codziennym rozwoju często trzeba zmienić datę, ale wiele osób używa niewłaściwego formatu, co prowadzi do nieoczekiwanych rzeczy. Oto przykład:
Instant instant = Instant.parse("2021-12-31T00:00:00.00Z");
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("YYYY-MM-dd HH:mm:ss")
.withZone(ZoneId.systemDefault());
System.out.println(formatter.format(instant));//2022-12-31 08:00:00
Wykorzystuje format RRRR-MM-dd do zmiany daty z 2021 na 2022. Nie powinieneś tego robić. Dlaczego? Dzieje się tak, ponieważ wzorzec Java DateTimeFormatter „YYYY” jest oparty na standardzie ISO-8601, który definiuje rok jako czwartek każdego tygodnia. Ale 31 grudnia 2021 roku przypadł w piątek, więc program błędnie wskazuje rok 2022. Aby tego uniknąć, musisz użyć formatu rrrr-MM-dd, aby sformatować datę . Ten błąd występuje rzadko, dopiero wraz z nadejściem nowego roku. Ale w mojej firmie spowodowało to awarię produkcyjną.

Używanie ThreadLocal w ThreadPool

Jeśli utworzysz zmienną ThreadLocal , wątek uzyskujący dostęp do tej zmiennej utworzy zmienną lokalną wątku. W ten sposób można uniknąć problemów związanych z bezpieczeństwem wątków. Jeśli jednak używasz ThreadLocal w puli wątków , musisz zachować ostrożność. Twój kod może generować nieoczekiwane wyniki. Załóżmy prosty przykład, że mamy platformę e-commerce i użytkownicy muszą wysłać wiadomość e-mail, aby potwierdzić dokonanie zakupu produktów.
private ThreadLocal<User> currentUser = ThreadLocal.withInitial(() -> null);

    private ExecutorService executorService = Executors.newFixedThreadPool(4);

    public void executor() {
        executorService.submit(()->{
            User user = currentUser.get();
            Integer userId = user.getId();
            sendEmail(userId);
        });
    }
Jeśli do zapisania informacji o użytkowniku użyjemy ThreadLocal , pojawi się ukryty błąd. Ponieważ używana jest pula wątków i wątki można ponownie wykorzystać, użycie ThreadLocal do uzyskania informacji o użytkowniku może błędnie wyświetlić informacje innej osoby. Aby rozwiązać ten problem, należy użyć sesji.

Użyj HashSet, aby usunąć zduplikowane dane

Podczas kodowania często potrzebujemy deduplikacji. Kiedy myślisz o deduplikacji, pierwszą rzeczą, o której wiele osób myśli, jest użycie zestawu HashSet . Jednak nieostrożne użycie HashSet może spowodować niepowodzenie deduplikacji.
User user1 = new User();
user1.setUsername("test");

User user2 = new User();
user2.setUsername("test");

List<User> users = Arrays.asList(user1, user2);
HashSet<User> sets = new HashSet<>(users);
System.out.println(sets.size());// the size is 2
Niektórzy uważni czytelnicy powinni być w stanie odgadnąć przyczynę niepowodzenia. HashSet używa kodu skrótu, aby uzyskać dostęp do tablicy skrótów i używa metody równości w celu ustalenia, czy obiekty są równe. Jeśli obiekt zdefiniowany przez użytkownika nie przesłania metody hashcode i metody równa się , wówczas domyślnie zostaną użyte metody hashcode i metody równości obiektu nadrzędnego. Spowoduje to, że HashSet założy, że są to dwa różne obiekty, co spowoduje niepowodzenie deduplikacji.

Eliminacja „zjedzonego” wątku basenowego

ExecutorService executorService = Executors.newFixedThreadPool(1);
        executorService.submit(()->{
            //do something
            double result = 10/0;
        });
Powyższy kod symuluje scenariusz, w którym w puli wątków zostaje zgłoszony wyjątek. Kod biznesowy musi zakładać różne sytuacje, więc jest bardzo prawdopodobne, że z jakiegoś powodu wygeneruje wyjątek RuntimeException . Ale jeśli nie ma tutaj specjalnej obsługi, wówczas ten wyjątek zostanie „zjedzony” przez pulę wątków. I nie będziesz miał nawet możliwości sprawdzenia przyczyny wyjątku. Dlatego najlepiej jest wychwytywać wyjątki w puli procesów.

Stringi w Javie - widok od środka

Źródło: Medium Autor tego artykułu postanowił szczegółowo przyjrzeć się tworzeniu, funkcjonalności i cechom ciągów znaków w Javie. Przerwa kawowa #146.  5 błędów, które popełnia 99% programistów Java.  Ciągi znaków w Javie – widok od środka – 2

kreacja

Ciąg w Javie można utworzyć na dwa różne sposoby: niejawnie, jako literał ciągu i jawnie, używając słowa kluczowego new . Literały łańcuchowe to znaki ujęte w podwójne cudzysłowy.
String literal   = "Michael Jordan";
String object    = new String("Michael Jordan");
Chociaż obie deklaracje tworzą obiekt typu string, istnieje różnica w sposobie umieszczenia obu tych obiektów w pamięci sterty.

Reprezentacja wewnętrzna

Poprzednio ciągi znaków były przechowywane w postaci char[] , co oznaczało, że każdy znak był oddzielnym elementem tablicy znaków. Ponieważ były one reprezentowane w formacie kodowania znaków UTF-16 , oznaczało to, że każdy znak zajmował dwa bajty pamięci. Nie jest to zbyt poprawne, ponieważ statystyki użytkowania pokazują, że większość obiektów łańcuchowych składa się wyłącznie ze znaków Latin-1 . Znaki Latin-1 można reprezentować za pomocą jednego bajtu pamięci, co może znacznie zmniejszyć zużycie pamięci — nawet o 50%. W wydaniu JDK 9 opartym na JEP 254 zaimplementowano nową funkcję ciągów wewnętrznych, zwaną Compact Strings. W tej wersji char[] zmieniono na byte[] i dodano pole flagi kodera reprezentujące użyte kodowanie (Latin-1 lub UTF-16). Następnie następuje kodowanie na podstawie zawartości ciągu. Jeśli wartość zawiera tylko znaki Latin-1, wówczas stosowane jest kodowanie Latin-1 ( klasa StringLatin1 ) lub kodowanie UTF-16 ( klasa StringUTF16 ).

Alokacja pamięci

Jak wspomniano wcześniej, istnieje różnica w sposobie alokacji pamięci dla tych obiektów na stercie. Użycie jawnego słowa kluczowego new jest całkiem proste, ponieważ JVM tworzy i przydziela pamięć dla zmiennej na stercie. Dlatego użycie literału łańcuchowego następuje po procesie zwanym internowaniem. Internowanie ciągów to proces umieszczania ciągów w puli. Używa metody przechowywania tylko jednej kopii każdej indywidualnej wartości ciągu, która musi być niezmienna. Poszczególne wartości przechowywane są w puli String Intern. Ta pula to magazyn Hashtable , który przechowuje odniesienie do każdego obiektu ciągu utworzonego przy użyciu literałów i jego skrótu. Chociaż wartość ciągu znajduje się na stercie, jego odniesienie można znaleźć w wewnętrznej puli. Można to łatwo sprawdzić, wykonując poniższy eksperyment. Tutaj mamy dwie zmienne o tej samej wartości:
String firstName1   = "Michael";
String firstName2   = "Michael";
System.out.println(firstName1 == firstName2);             //true
Kiedy podczas wykonywania kodu maszyna JVM napotka imię_pierwsze1 , wyszukuje wartość ciągu znaków w wewnętrznej puli ciągów Michael . Jeśli nie może go znaleźć, dla obiektu w wewnętrznej puli tworzony jest nowy wpis. Gdy wykonanie osiągnie imię imię2 , proces powtarza się ponownie i tym razem wartość można znaleźć w puli na podstawie zmiennej imię1 . W ten sposób zamiast duplikować i tworzyć nowy wpis, zwracany jest ten sam link. Zatem warunek równości jest spełniony. Z drugiej strony, jeśli za pomocą słowa kluczowego new utworzona zostanie zmienna o wartości Michael , nie nastąpi internowanie i warunek równości nie zostanie spełniony.
String firstName3 = new String("Michael");
System.out.println(firstName3 == firstName2);           //false
Interningu można używać z metodą FirstName3 intern() , chociaż zwykle nie jest to preferowane.
firstName3 = firstName3.intern();                      //Interning
System.out.println(firstName3 == firstName2);          //true
Interning może również wystąpić podczas łączenia dwóch literałów łańcuchowych za pomocą operatora + .
String fullName = "Michael Jordan";
System.out.println(fullName == "Michael " + "Jordan");     //true
Widzimy tutaj, że w czasie kompilacji kompilator dodaje oba literały i usuwa operator + z wyrażenia, tworząc pojedynczy ciąg znaków, jak pokazano poniżej. W czasie wykonywania zarówno fullName , jak i „dodany literał” są internowane i warunek równości jest spełniony.
//After Compilation
System.out.println(fullName == "Michael Jordan");

Równość

Z powyższych eksperymentów widać, że domyślnie internowane są tylko literały łańcuchowe. Jednak aplikacja Java z pewnością nie będzie zawierać wyłącznie literałów łańcuchowych, ponieważ może otrzymywać ciągi znaków z różnych źródeł. Dlatego używanie operatora równości nie jest zalecane i może dawać niepożądane rezultaty. Testowanie równości powinno być przeprowadzane wyłącznie metodą równości . Wykonuje równość w oparciu o wartość ciągu, a nie adres pamięci, w którym jest przechowywany.
System.out.println(firstName1.equals(firstName2));       //true
System.out.println(firstName3.equals(firstName2));       //true
Istnieje również nieco zmodyfikowana wersja metody Equals o nazwie EqualsIgnoreCase . Może być przydatny do celów, w których wielkość liter nie jest uwzględniana.
String firstName4 = "miCHAEL";
System.out.println(firstName4.equalsIgnoreCase(firstName1));  //true

Niezmienność

Ciągi są niezmienne, co oznacza, że ​​ich stanu wewnętrznego nie można zmienić po ich utworzeniu. Można zmienić wartość zmiennej, ale nie wartość samego ciągu. Każda metoda klasy String zajmująca się manipulowaniem obiektem (na przykład concat , substring ) zwraca nową kopię wartości zamiast aktualizować istniejącą wartość.
String firstName  = "Michael";
String lastName   = "Jordan";
firstName.concat(lastName);

System.out.println(firstName);                       //Michael
System.out.println(lastName);                        //Jordan
Jak widać, w żadnej ze zmiennych nie zachodzą żadne zmiany: ani imię , ani nazwisko . Metody klasy String nie zmieniają stanu wewnętrznego, tworzą nową kopię wyniku i zwracają wynik, jak pokazano poniżej.
firstName = firstName.concat(lastName);

System.out.println(firstName);                      //MichaelJordan
Komentarze
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION