Java е език за програмиране, който първоначално е разработен за интерактивна телевизия , но с течение на времето тя стана широко разпространена навсякъде, където може да се използва софтуер. Проектирана с идеята за обектно-ориентирано програмиране, премахване на сложността на други езици като C или C ++, събиране на боклук и архитектурно агностична виртуална машина, Java създаде нов начин на програмиране. Освен това, той има нежна крива на обучение и изглежда успешно се придържа към собственото си мото - „Пиши веднъж, тичай навсякъде“, което е почти винаги вярно; но проблемите с Java все още са налице. Ще разгледам десет Java проблеми, които според мен са най-често срещаните грешки.
Определено е грешка за Разработчици на Java да игнорира безбройните библиотеки, написани на Java. Преди да преоткриете колелото, опитайте се да потърсите наличните библиотеки - много от тях са били полирани през годините на своето съществуване и са свободни за използване. Това могат да бъдат регистриращи библиотеки, като logback и Log4j, или свързани с мрежата библиотеки, като Netty или Akka. Някои от библиотеките, като Joda-Time, се превърнаха в де факто стандарт.
Следва личен опит от един от предишните ми проекти. Частта от кода, отговорна за избягването на HTML, е написана от нулата. Той работеше добре в продължение на години, но в крайна сметка срещна потребителски вход, което го накара да се завърти в безкраен цикъл. Потребителят, намирайки услугата да не реагира, се опита да опита отново със същия вход. В крайна сметка всички процесори на сървъра, разпределени за това приложение, бяха заети от този безкраен цикъл. Ако авторът на този наивен HTML инструмент за бягство е решил да използва една от добре познатите библиотеки, налични за HTML избягване, като напр. HtmlEscapers от Google Гуава , това вероятно нямаше да се случи. Най-малкото, вярно за повечето популярни библиотеки с общност зад нея, грешката би била открита и коригирана по-рано от общността за тази библиотека.
Тези проблеми с Java могат да бъдат много неудобни и понякога остават неразкрити, докато не бъдат пуснати в производство. Поведението на пробив в операторите за превключване често е полезно; пропускането на ключова дума „break“, когато такова поведение не е желано, може да доведе до катастрофални резултати. Ако сте забравили да поставите „break“ в „case 0“ в примера по-долу, програмата ще напише „Zero“, последвано от „One“, тъй като контролният поток вътре ще премине през целия оператор „switch“, докато достига „почивка“. Например:
public static void switchCasePrimer() { int caseIndex = 0; switch (caseIndex) { case 0: System.out.println('Zero'); case 1: System.out.println('One'); break; case 2: System.out.println('Two'); break; default: System.out.println('Default'); } }
В повечето случаи по-чистото решение би било да се използва полиморфизъм и да се премести код със специфично поведение в отделни класове. Java грешки като тази могат да бъдат открити с помощта на статични анализатори на код, напр. FindBugs и PMD .
Всеки път, когато дадена програма отвори файл или мрежова връзка, за начинаещите Java е важно да освободят ресурса, след като приключите с използването му. Подобна предпазливост трябва да се вземе, ако по време на операции с такива ресурси се налага изключение. Може да се твърди, че FileInputStream има финализатор, който извиква метода close () при събиране на боклук; тъй като обаче не можем да сме сигурни кога ще започне цикълът за събиране на боклука, входният поток може да консумира компютърни ресурси за неопределен период от време. Всъщност има наистина полезно и изрядно изявление, въведено в Java 7, специално за този случай, наречено опитайте с ресурси :
private static void printFileJava7() throws IOException { try(FileInputStream input = new FileInputStream('file.txt')) { int data = input.read(); while(data != -1){ System.out.print((char) data); data = input.read(); } } }
Това изявление може да се използва с всеки обект, който реализира интерфейса AutoClosable. Той гарантира, че всеки ресурс е затворен до края на изявлението.
Свързани: 8 основни въпроса за интервю за JavaJava използва автоматично управление на паметта и макар да е облекчение да забравите за ръчното разпределяне и освобождаване на памет, това не означава, че начинаещият разработчик на Java не трябва да е наясно как се използва паметта в приложението. Все още са възможни проблеми с разпределението на паметта. Докато една програма създава препратки към обекти, които вече не са необходими, тя няма да бъде освободена. По някакъв начин все още можем да наречем това изтичане на памет. Изтичането на памет в Java може да се случи по различни начини, но най-честата причина са вечните препратки към обекти, тъй като събирачът на боклук не може да премахва обекти от купчината, докато все още има препратки към тях. Човек може да създаде такава препратка, като дефинира клас със статично поле, съдържащо някаква колекция от обекти, и забравяйки да зададе това статично поле на нула, след като колекцията вече не е необходима. Статичните полета се считат за GC корени и никога не се събират.
Друга потенциална причина за такива изтичания на памет е група обекти, които се позовават един на друг, причинявайки кръгови зависимости, така че събирачът на боклук да не може да реши дали тези обекти с референции за взаимна зависимост са необходими или не. Друг проблем са течовете в паметта без купчина, когато се използва JNI.
Примитивният пример за изтичане може да изглежда по следния начин:
final ScheduledExecutorService scheduledExecutorService = Executors.newScheduledThreadPool(1); final Deque numbers = new LinkedBlockingDeque(); final BigDecimal divisor = new BigDecimal(51); scheduledExecutorService.scheduleAtFixedRate(() -> { BigDecimal number = numbers.peekLast(); if (number != null && number.remainder(divisor).byteValue() == 0) { System.out.println('Number: ' + number); System.out.println('Deque size: ' + numbers.size()); } }, 10, 10, TimeUnit.MILLISECONDS); scheduledExecutorService.scheduleAtFixedRate(() -> { numbers.add(new BigDecimal(System.currentTimeMillis())); }, 10, 10, TimeUnit.MILLISECONDS); try { scheduledExecutorService.awaitTermination(1, TimeUnit.DAYS); } catch (InterruptedException e) { e.printStackTrace(); }
Този пример създава две планирани задачи. Първата задача взема последното число от дек, наречен „числа“, и отпечатва номера и размера на дека, в случай че числото се дели на 51. Втората задача поставя числа в дека. И двете задачи са планирани с фиксирана скорост и се изпълняват на всеки 10 ms. Ако кодът се изпълни, ще видите, че размерът на deque постоянно се увеличава. Това в крайна сметка ще доведе до запълване на deque с обекти, консумиращи цялата налична памет. За да предотвратим това, запазвайки семантиката на тази програма, можем да използваме различен метод за вземане на числа от deque: “pollLast”. Противно на метода “peekLast”, “pollLast” връща елемента и го премахва от deque, докато “peekLast” връща само последния елемент.
За да научите повече за изтичането на памет в Java, моля, вижте нашата статия, която демистифицира този проблем .
Прекомерното разпределение на боклука може да се случи, когато програмата създава много краткотрайни обекти. Събирачът на боклук работи непрекъснато, премахвайки ненужните обекти от паметта, което влияе негативно на работата на приложенията. Един прост пример:
String oneMillionHello = ''; for (int i = 0; i <1000000; i++) { oneMillionHello = oneMillionHello + 'Hello!'; } System.out.println(oneMillionHello.substring(0, 6));
В разработката на Java , низовете са неизменни. И така, при всяка итерация се създава нов низ. За да се справим с това, трябва да използваме променлив StringBuilder:
StringBuilder oneMillionHelloSB = new StringBuilder(); for (int i = 0; i <1000000; i++) { oneMillionHelloSB.append('Hello!'); } System.out.println(oneMillionHelloSB.toString().substring(0, 6));
Докато първата версия изисква доста време за изпълнение, версията, която използва StringBuilder, дава резултат за значително по-малко време.
Избягването на прекомерна употреба на null е добра практика. Например за предпочитане е да се връщат празни масиви или колекции от методи вместо nulls, тъй като това може да помогне за предотвратяване на NullPointerException.
Помислете за следния метод, който обхожда колекция, получена от друг метод, както е показано по-долу:
List accountIds = person.getAccountIds(); for (String accountId : accountIds) { processAccount(accountId); }
Ако getAccountIds () връща null, когато човек няма акаунт, тогава NullPointerException ще бъде издигнат. За да поправите това, ще е необходима нулева проверка. Ако обаче вместо нула връща празен списък, тогава NullPointerException вече не е проблем. Освен това кодът е по-чист, тъй като не е необходимо да проверяваме нулата на променливата accountIds.
За да се справят с други случаи, когато някой иска да избегне нули, могат да се използват различни стратегии. Една от тези стратегии е да се използва незадължителен тип, който може да бъде празен обект или обвивка с някаква стойност:
Optional optionalString = Optional.ofNullable(nullableString); if(optionalString.isPresent()) { System.out.println(optionalString.get()); }
Всъщност Java 8 предоставя по-кратко решение:
Optional optionalString = Optional.ofNullable(nullableString); optionalString.ifPresent(System.out::println);
Типът по избор е част от Java от версия 8, но е известен отдавна в света на функционалното програмиране. Преди това той беше достъпен в Google Guava за по-ранни версии на Java.
Често е изкушаващо да се оставят изключенията необработени. Най-добрата практика обаче както за начинаещи, така и за опитни разработчици на Java е да се справят с тях. Изключенията се изхвърлят нарочно, така че в повечето случаи трябва да се обърнем към проблемите, причиняващи тези изключения. Не пренебрегвайте тези събития. Ако е необходимо, можете да го върнете отново, да покажете диалогов прозорец за грешка на потребителя или да добавите съобщение към дневника. Най-малкото трябва да се обясни защо изключението е оставено необработено, за да могат другите разработчици да знаят причината.
selfie = person.shootASelfie(); try { selfie.show(); } catch (NullPointerException e) { // Maybe, invisible man. Who cares, anyway? }
По-ясен начин да се подчертае незначителността на изключенията е да се кодира това съобщение в името на променливата на изключенията, като това:
try { selfie.delete(); } catch (NullPointerException unimportant) { }
Това изключение възниква, когато колекция се модифицира по време на итерация върху нея, използвайки методи, различни от предоставените от обекта на итератора. Например имаме списък с шапки и искаме да премахнем всички, които имат клапи за уши:
List hats = new ArrayList(); hats.add(new Ushanka()); // that one has ear flaps hats.add(new Fedora()); hats.add(new Sombrero()); for (IHat hat : hats) { if (hat.hasEarFlaps()) { hats.remove(hat); } }
Ако стартираме този код, ще се изведе “ConcurrentModificationException”, тъй като кодът модифицира колекцията, докато я итерира. Същото изключение може да възникне, ако една от множеството нишки, работещи с един и същ списък, се опитва да модифицира колекцията, докато други итерират над нея. Едновременната модификация на колекции в множество нишки е нещо естествено, но трябва да се третира с обичайни инструменти от инструментариума за едновременно програмиране, като синхронизационни брави, специални колекции, приети за едновременна модификация и т.н. Има тънки разлики в това как този проблем на Java може да бъде разрешен в единични резбовани кутии и многонишкови кутии. По-долу е кратко обсъждане на някои начини, по които това може да бъде обработено в сценарий с една резба:
Събирането на шапки с клапи за уши в списък, за да ги премахнете по-късно от друг цикъл, е очевидно решение, но изисква допълнителна колекция за съхранение на шапките, които трябва да бъдат премахнати:
List hatsToRemove = new LinkedList(); for (IHat hat : hats) { if (hat.hasEarFlaps()) { hatsToRemove.add(hat); } } for (IHat hat : hatsToRemove) { hats.remove(hat); }
Този подход е по-кратък и не се нуждае от допълнителна колекция, за да бъде създадена:
Iterator hatIterator = hats.iterator(); while (hatIterator.hasNext()) { IHat hat = hatIterator.next(); if (hat.hasEarFlaps()) { hatIterator.remove(); } }
Използването на итератора на списък е подходящо, когато модифицираната колекция изпълнява списъчен интерфейс. Итераторите, които реализират интерфейса ListIterator, поддържат не само операции за премахване, но и операции за добавяне и задаване. ListIterator изпълнява интерфейса на Iterator, така че примерът ще изглежда почти същото като метода за премахване на Iterator. Единствената разлика е типът итератор на шапка и начинът, по който получаваме този итератор с метода “listIterator ()”. Фрагментът по-долу показва как да замените всяка шапка с клапи за уши със сомбреро, използвайки методите “ListIterator.remove” и “ListIterator.add”:
как да създадете rest api
IHat sombrero = new Sombrero(); ListIterator hatIterator = hats.listIterator(); while (hatIterator.hasNext()) { IHat hat = hatIterator.next(); if (hat.hasEarFlaps()) { hatIterator.remove(); hatIterator.add(sombrero); } }
С ListIterator извикванията на метода за премахване и добавяне могат да бъдат заменени с едно повикване за задаване:
IHat sombrero = new Sombrero(); ListIterator hatIterator = hats.listIterator(); while (hatIterator.hasNext()) { IHat hat = hatIterator.next(); if (hat.hasEarFlaps()) { hatIterator.set(sombrero); // set instead of remove and add } }
Използвайте поточни методи, въведени в Java 8 С Java 8, програмисти имат способността да трансформират колекция в поток и да филтрират този поток според някои критерии. Ето пример за това как api на потока може да ни помогне да филтрираме шапки и да избегнем „ConcurrentModificationException“.
hats = hats.stream().filter((hat -> !hat.hasEarFlaps())) .collect(Collectors.toCollection(ArrayList::new));
Методът „Collectors.toCollection“ ще създаде нов ArrayList с филтрирани шапки. Това може да е проблем, ако условието за филтриране трябва да бъде изпълнено от голям брой елементи, което води до голям ArrayList; по този начин трябва да се използва внимателно. Използвайте метода List.removeIf, представен в Java 8 Друго решение, налично в Java 8, и очевидно най-краткото, е използването на метода „removeIf“:
hats.removeIf(IHat::hasEarFlaps);
Това е. Под капака използва „Iterator.remove“, за да постигне поведението.
Ако в самото начало решихме да използваме “CopyOnWriteArrayList” вместо “ArrayList”, тогава изобщо нямаше да има проблем, тъй като “CopyOnWriteArrayList” предоставя методи за модификация (като задаване, добавяне и премахване), които не се променят подкрепящия масив на колекцията, а по-скоро създайте нова модифицирана версия на нея. Това позволява повторение на оригиналната версия на колекцията и модификации върху нея едновременно, без риск от “ConcurrentModificationException”. Недостатъкът на тази колекция е очевиден - генериране на нова колекция с всяка модификация.
Има и други колекции, настроени за различни случаи, напр. “CopyOnWriteSet” и “ConcurrentHashMap”.
Друга възможна грешка при едновременните модификации на колекцията е да се създаде поток от колекция и по време на итерацията на потока да се модифицира колекцията за архивиране. Общото правило за потоците е да се избягва модификация на основната колекция по време на заявка за поток. Следващият пример ще покаже неправилен начин за обработка на поток:
List filteredHats = hats.stream().peek(hat -> { if (hat.hasEarFlaps()) { hats.remove(hat); } }).collect(Collectors.toCollection(ArrayList::new));
Погледът на метода събира всички елементи и извършва предвиденото действие върху всеки един от тях. Тук действието се опитва да премахне елементи от основния списък, което е погрешно. За да избегнете това, опитайте някои от методите, описани по-горе.
Понякога кодът, предоставен от стандартната библиотека или от доставчик на трета страна, разчита на правила, които трябва да се спазват, за да накарат нещата да работят. Например, това може да бъде hashCode и е равно на договор, който, когато се следва, прави гарантирана работа за набор от колекции от рамката за събиране на Java и за други класове, които използват hashCode и equals методи. Неспазването на договорите не е вид грешка, която винаги води до изключения или нарушава компилацията на кода; по-сложно е, защото понякога променя поведението на приложението без никакви признаци на опасност. Грешният код може да се вмъкне в продуктовата версия и да предизвика цял куп нежелани ефекти. Това може да включва лошо поведение на потребителския интерфейс, грешни отчети с данни, лоша производителност на приложението, загуба на данни и др. За щастие тези катастрофални грешки не се случват много често. Вече споменах hashCode и е равно на договор. Използва се в колекции, които разчитат на хеширане и сравняване на обекти, като HashMap и HashSet. Най-просто казано, договорът съдържа две правила:
Нарушаването на първото правило на договора води до проблеми при опит за извличане на обекти от хеш-карта. Второто правило означава, че обектите с един и същ хеш код не са непременно равни. Нека разгледаме ефектите от нарушаването на първото правило:
public static class Boat { private String name; Boat(String name) { this.name = name; } @Override public boolean equals(Object o) getClass() != o.getClass()) return false; Boat boat = (Boat) o; return !(name != null ? !name.equals(boat.name) : boat.name != null); @Override public int hashCode() { return (int) (Math.random() * 5000); } }
Както можете да видите, клас Boat е заменил методите equals и hashCode. Той обаче е нарушил договора, защото hashCode връща произволни стойности за един и същ обект всеки път, когато е извикан. Следният код най-вероятно няма да намери лодка с име „Enterprise“ в хешсета, въпреки факта, че добавихме този вид лодка по-рано:
public static void main(String[] args) { Set boats = new HashSet(); boats.add(new Boat('Enterprise')); System.out.printf('We have a boat named 'Enterprise' : %b
', boats.contains(new Boat('Enterprise'))); }
Друг пример за договор включва метода на финализиране. Ето цитат от официалната документация за Java, описващ нейната функция:
Общият договор за финализиране е, че той се извиква, ако и когато JavaTM виртуалната машина е установила, че вече няма никакви средства, чрез които този обект може да бъде достъпен от която и да е нишка (която все още не е умряла), освен в резултат на действие, предприето от финализирането на някакъв друг обект или клас, който е готов за финализиране. Методът за финализиране може да предприеме всякакви действия, включително да направи този обект отново достъпен за други нишки; обичайната цел на финализирането обаче е да се извършат действия за почистване, преди обектът да бъде необратимо изхвърлен. Например методът за финализиране за обект, който представлява входна / изходна връзка, може да извършва явни I / O транзакции, за да прекъсне връзката, преди обектът да бъде окончателно изхвърлен.
Човек би могъл да реши да използва метода за финализиране за освобождаване на ресурси като обработчици на файлове, но това би било лоша идея. Това е така, защото няма гаранции за времето кога ще бъде извикано финализирането, тъй като то се извиква по време на събирането на боклука и времето на GC е неопределимо.
Суровите типове, съгласно спецификациите на Java, са типове, които или не са параметризирани, или нестатични членове на клас R, които не са наследени от суперкласа или суперинтерфейса на R. Няма алтернативи на суровите типове, докато в Java не са въведени родови типове . Той поддържа генерично програмиране от версия 1.5 и генериците несъмнено са значително подобрение. Поради причини, свързани с обратната съвместимост, е оставен подводен капан, който потенциално може да наруши системата на типа. Нека разгледаме следния пример:
List listOfNumbers = new ArrayList(); listOfNumbers.add(10); listOfNumbers.add('Twenty'); listOfNumbers.forEach(n -> System.out.println((int) n * 2));
Тук имаме списък с числа, дефинирани като суров ArrayList. Тъй като неговият тип не е посочен с параметър type, можем да добавим всеки обект в него. Но в последния ред ние хвърляме елементи към int, удвояваме го и отпечатваме удвоеното число на стандартен изход. Този код ще се компилира без грешки, но след като се стартира, ще се появи изключение по време на изпълнение, защото се опитахме да хвърлим низ към цяло число. Очевидно системата за шрифтове не може да ни помогне да напишем безопасен код, ако скрием необходимата информация от нея. За да отстраним проблема, трябва да посочим типа обекти, които ще съхраняваме в колекцията:
List listOfNumbers = new ArrayList(); listOfNumbers.add(10); listOfNumbers.add('Twenty'); listOfNumbers.forEach(n -> System.out.println((int) n * 2));
Единствената разлика от оригинала е редът, дефиниращ колекцията:
List listOfNumbers = new ArrayList();
Фиксираният код няма да се компилира, защото се опитваме да добавим низ в колекция, която се очаква да съхранява само цели числа. Компилаторът ще покаже грешка и ще посочи реда, където се опитваме да добавим низа „Двадесет“ към списъка. Винаги е добра идея да параметризирате родови типове. По този начин компилаторът е в състояние да извършва всички възможни проверки на типа и шансовете за изключения по време на изпълнение, причинени от несъответствия в системата на типа, са сведени до минимум.
Java като платформа опростява много неща при разработването на софтуер, разчитайки както на усъвършенстваната JVM, така и на самия език. Въпреки това, неговите функции, като премахване на ръчно управление на паметта или прилични OOP инструменти, не премахват всички проблеми и проблеми, пред които е изправен редовен разработчик на Java. Както винаги, познанията, практиката и Java уроци като този са най-доброто средство за избягване и отстраняване на грешки в приложенията - така че познавайте библиотеките си, четете java, четете JVM документация и пишете програми. Не забравяйте и за статичните анализатори на код, тъй като те могат да сочат към действителните грешки и да подчертават потенциални грешки.
Свързани: Урок за напреднали Java Class: Ръководство за презареждане на клас