JavaRush /Java блог /Random UA /Кава-брейк #146. 5 помилок, які роблять 99% Java-розробни...

Кава-брейк #146. 5 помилок, які роблять 99% Java-розробників. Рядки в Java - вид зсередини

Стаття з групи Random UA

5 помилок, які роблять 99% Java-розробників

Джерело: Medium У цій публікації ви дізнаєтеся про найбільш поширені помилки, яких допускають багато Java-розробників. Кава-брейк #146.  5 помилок, які роблять 99% Java-розробників.  Рядки в Java - вид зсередини.Як програміст, що працює на Java, я знаю, як погано витрачати купу часу на усунення помилок у коді. Іноді на це йде кілька годин. При цьому багато помилок виникають через те, що розробник ігнорує базові правила, тобто це помилки дуже низького рівня. Сьогодні ми розглянемо деякі поширені помилки кодування, а потім пояснимо, як їх усунути. Сподіваюся, це допоможе вам уникнути проблем у повсякденній роботі.

Порівняння об'єктів із використанням Objects.equals

Я вважаю, що ви знайомі із цим методом. Багато розробників часто його використовують. Це метод, що з'явився в JDK 7, допомагає швидко порівнювати об'єкти та ефективно уникати набридливу перевірку нульового покажчика. Але це метод іноді використовується неправильно. Ось що я маю на увазі:
Long longValue = 123L;
System.out.println(longValue==123); //true
System.out.println(Objects.equals(longValue,123)); //false
Чому заміна == на Objects.equals() призведе до неправильного результату? Це з тим, що з допомогою компілятора == буде отримано базовий тип даних, відповідний типу упаковки longValue , та був порівняння його з цим базовим типом даних. Це еквівалентно тому, що компілятор автоматично перетворює константи базовий тип даних порівняння. Після використання методу Objects.equals() базовим типом даних константи компілятора за умовчанням є int . Нижче наведено вихідний код Objects.equals() , у якому a.equals(b) використовує Long.equals() та визначає тип об'єкта. Це тому, що компілятор вважав, що константа має тип int , тому результат порівняння має бути хибним.
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;
    }
Знаючи причину виправити помилку дуже просто. Просто оголошуйте тип даних констант, наприклад Objects.equals(longValue,123L) . Перераховані вище проблеми не виникнуть, якщо логіка буде суворою. Що нам потрібно зробити, так це слідувати чітким правилам програмування.

Неправильний формат дати

У повсякденній розробці часто потрібно змінювати дату, але багато людей використовують неправильний формат, що призводить до несподіваних речей. Ось приклад:
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
Тут використовується формат YYYY-MM-dd для зміни дати з 2021 до 2022 року. Так не варто робити. Чому? Це пов'язано з тим, що шаблон Java DateTimeFormatter "YYYY" заснований на стандарті ISO-8601, в якому рік визначається по четвер кожного тижня. Але 31 грудня 2021 довелося на п'ятницю, тому програма помилково вказує 2022 рік. Щоб уникнути такого, для форматування дати необхідно використовувати формат yyyy-MM-dd . Ця помилка трапляється нечасто, лише з приходом нового року. Але в моїй компанії це спричинило збій у виробництві.

Використання ThreadLocal у ThreadPool

Якщо ви створите змінну ThreadLocal , то потік, що звертається до цієї змінної, створить локальну змінну потоку. Так ви зможете уникнути проблем із безпекою потоків. Однак, якщо ви використовуєте ThreadLocal в кулі потоків , вам потрібно бути обережним. У коді може з'явитися несподіваний результат. Для простого прикладу припустимо, що ми маємо платформу електронної торгівлі, і користувачам необхідно надіслати електронний лист для підтвердження виконаної купівлі товарів.
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);
        });
    }
Якщо ми використовуємо ThreadLocal для збереження інформації про користувача, з'явиться прихована помилка. Оскільки використовується пул потоків, а потоки можна використовувати повторно, при використанні ThreadLocal для отримання інформації про користувача може помилково відображатися чужа інформація. Для вирішення цієї проблеми варто використати сесії.

Використовуйте HashSet для видалення даних, що повторюються

При кодуванні у нас часто виникає потреба у дедуплікації. При згадці дедуплікації багато хто насамперед думає використовувати HashSet . Однак необережне використання HashSet може призвести до дедуплікації.
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
Деякі уважні читачі мають здогадатися про причину збою. HashSet використовує хеш-код для звернення до хеш-таблиці та використовує метод equals, щоб визначити, чи рівні об'єкти. Якщо об'єкт, що визначається користувачем, не перевизначає метод хеш-коду та метод equals , то за замовчуванням будуть використовуватися метод хеш-коду та метод equals батьківського об'єкта. Таким чином, HashSet припустить, що це два різні об'єкти, що призведе до збою дедуплікації.

Виняток "з'їденого" потоку пулу

ExecutorService executorService = Executors.newFixedThreadPool(1);
        executorService.submit(()->{
            //do something
            double result = 10/0;
        });
Наведений вище код імітує сценарій, де у пулі потоків видається виняток. Бізнес-код повинен припускати різні ситуації, тому дуже ймовірно, що з деяких причин він викличе RuntimeException . Але якщо тут немає спеціальної обробки, то цей виняток буде з'їдено пулом потоків. І у вас навіть не буде способу перевірити причину виключення. Тому найкраще перехоплювати винятки у пулі процесів.

Рядки в Java - вид зсередини

Джерело: Medium Автор цієї статті вирішив докладно розглянути створення, функціонал та особливості роботи рядків у Java. Кава-брейк #146.  5 помилок, які роблять 99% Java-розробників.  Рядки в Java - вид зсередини.

створення

Рядок в Java може бути створена двома різними способами: неявно, як рядковий літерал, і явно з використанням ключового слова new . Рядкові літерали (String literals) - це символи, укладені в подвійні лапки.
String literal   = "Michael Jordan";
String object    = new String("Michael Jordan");
Хоча обидві декларації створюють рядковий об'єкт, є різниця в тому, як обидва ці об'єкти знаходяться в купі пам'яті.

Внутрішнє уявлення

Раніше рядки зберігалися у формі char[] , тобто кожен символ був окремим елементом у масиві символів. Оскільки вони були представлені у форматі кодування символів UTF-16 , це означало, що кожен символ займав два байти пам'яті. Це не дуже правильно, оскільки статистика використання показує, що більшість рядкових об'єктів складаються лише із символів Latin-1 . Символи Latin-1 можуть бути представлені з використанням одного байта пам'яті, що може значно скоротити використання пам'яті на цілих 50%. Нову внутрішню функцію рядків було реалізовано як частину випуску JDK 9 на основі JEP 254 під назвою Compact Strings. У цьому релізі char[] змінабо на byte[] і було додано поле прапора кодувальника для представлення кодування, що використовується (Latin-1 або UTF-16). Після цього кодування відбувається на основі вмісту рядка. Якщо значення містить лише символи Latin-1, то використовується кодування Latin-1 (клас StringLatin1 ) або застосовується кодування UTF-16 (клас StringUTF16 ).

Виділення пам'яті

Як зазначалося раніше, існує різниця у способі виділення пам'яті для цих об'єктів у купі. Використання явного ключового слова new досить прямолінійно, оскільки JVM створює та виділяє пам'ять для змінної у купі. Тому використання рядкового літералу слідує за процесом, званим інтернуванням. Інтернування рядків – це процес розміщення рядків у пул. Він використовує метод зберігання лише однієї копії кожного окремого рядкового значення, яке має бути незмінним. Окремі значення зберігаються у внутрішньому пулі (String Intern pool). Цей пул являє собою сховище Hashtable , в якому зберігається посилання на кожен рядковий об'єкт, створений за допомогою літералів, та його хеш. Хоча рядкове значення знаходиться у купі, його посилання можна знайти у внутрішньому пулі. У цьому легко переконатись за допомогою наведеного нижче експерименту. Тут у нас є дві змінні з однаковим значенням:
String firstName1   = "Michael";
String firstName2   = "Michael";
System.out.println(firstName1 == firstName2);             //true
Під час виконання коду, коли JVM стикається з firstName1 , він шукає рядкове значення у внутрішньому пулі рядків Michael . Якщо він не може знайти його, тоді для об'єкта створюється новий запис у внутрішньому пулі. Коли виконання досягає firstName2 процес знову повторюється, і цього разу значення може бути знайдено в пулі на основі змінної firstName1 . Таким чином, замість дублювання та створення нового запису повертається те саме посилання. Тому умова рівності виконується. З іншого боку, якщо змінна зі значенням Michael створюється за допомогою ключового слова new, інтернування не відбувається і умова рівності не виконується.
String firstName3 = new String("Michael");
System.out.println(firstName3 == firstName2);           //false
Інтернування може застосовуватися з firstName3 методу intern() , хоча це зазвичай не переважно.
firstName3 = firstName3.intern();                      //Interning
System.out.println(firstName3 == firstName2);          //true
Інтернування може відбуватися при об'єднанні двох рядкових літералів з допомогою оператора + .
String fullName = "Michael Jordan";
System.out.println(fullName == "Michael " + "Jordan");     //true
Тут ми бачимо, що під час компіляції компілятор додає обидва літерали і видаляє оператор + з виразу, щоб сформувати один рядок, як показано нижче. Під час виконання обидва значення, fullName та “доданий літерал”, інтернуються, і виконується умова рівності.
//After Compilation
System.out.println(fullName == "Michael Jordan");

Рівність

З наведених вище експериментів видно, що за умовчанням інтернуються лише рядкові літерали. Однак Java-додаток напевно не матиме лише рядкові літерали, оскільки він може отримувати рядки з різних джерел. Тому використання оператора рівності не рекомендується і може призвести до небажаних результатів. Перевірка рівності повинна виконуватися лише методом equals . Він виконує рівність на основі значення рядка, а не адресаи пам'яті, в якій зберігається.
System.out.println(firstName1.equals(firstName2));       //true
System.out.println(firstName3.equals(firstName2));       //true
Існує також трохи змінена версія викликуваного методу equals під назвою equals IgnoreCase . Вона може бути корисною для цілей, нечутливих до регістру.
String firstName4 = "miCHAEL";
System.out.println(firstName4.equalsIgnoreCase(firstName1));  //true

Незмінність

Рядки незмінні, тобто їх внутрішній стан неможливо змінити після створення. Можна змінити значення змінної, але не значення рядка. Кожен метод класу String , пов'язаний з керуванням об'єктом (наприклад, concat , substring ), повертає нову копію значення, а не оновлює існуюче значення.
String firstName  = "Michael";
String lastName   = "Jordan";
firstName.concat(lastName);

System.out.println(firstName);                       //Michael
System.out.println(lastName);                        //Jordan
Як бачите, жодних змін не відбувається ні з жодною змінною: ні firstName , ні lastName . Методи класу String не змінюють внутрішній стан, вони створюють нову копію результату та повертають результат, як показано нижче.
firstName = firstName.concat(lastName);

System.out.println(firstName);                      //MichaelJordan
Коментарі
ЩОБ ПОДИВИТИСЯ ВСІ КОМЕНТАРІ АБО ЗАЛИШИТИ КОМЕНТАР,
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ