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. Jako 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);
System.out.println(Objects.equals(longValue,123));
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));
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());
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(()->{
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.
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);
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);
Interningu można używać z metodą
FirstName3 intern() , chociaż zwykle nie jest to preferowane.
firstName3 = firstName3.intern();
System.out.println(firstName3 == firstName2);
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");
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.
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));
System.out.println(firstName3.equals(firstName2));
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));
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);
System.out.println(lastName);
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);
GO TO FULL VERSION