Знаете ли вы Java как свои пять пальцев, или же вы не очень хорошо с ним знакомы, в любом случае вы можете допустить ошибку. Почему? Потому, что все мы люди, и нам свойственно ошибаться. Ниже подобран список из десяти самых популярных ошибок, которые в тот или иной момент может допустить Java программист.
10. Попытка доступа к не статическим переменным-членам из статических методов (например, из main)
Многие начинающие Java-программисты при первом знакомстве с Java допускают ошибки, связанные с доступом к не статическим переменным-членам класса из метода main. Сигнатура этого метода определена как static – это означает, что нет необходимости создавать экземпляр класса для вызова метода Main.
Например, виртуальная машина Java (JVM) может вызывать статический метод main из класса MyApplication таким образом:
MyApplication.main ( command_line_args );
Ошибочно думать, что при этом создается экземпляр класса MyApplication, и мы сможем получить доступ к не статическим переменным-членам этого класса.
Именно поэтому, нижеприведённый пример приведёт к генерации компилятором ошибки:
public class StaticDemo
{
public String my_member_variable = "somedata";
public static void main (String args[])
{
// Попытка доступа к не статическим членам из статического метода
System.out.println ("Это приведёт к ошибке компиляции" + my_member_variable );
}
}
Если вы хотите получить доступ к не статическим переменным-членам класса из статического метода, необходимо создать экземпляр этого класса. Ниже приведён простой пример, как писать правильный код для доступа к не статическим переменным-членам путём предварительного создания экземпляра класса:
public class NonStaticDemo
{
public String my_member_variable = "somedata";
public static void main (String args[])
{
NonStaticDemo demo = new NonStaticDemo();
// Доступ к нестатической переменной из статического метода
System.out.println ("Это правильный код, и он НЕ приведёт к ошибке компиляции: " +
demo.my_member_variable );
}
}
9. Опечатки в именах методов при переопределении (overriding)
Переопределение позволяет программисту заменить реализацию существующего метода новой. Переопределение – удобная функция, и большинство программистов, пишущих объектно-ориентированный код, используют её. Если вы используете модель обработки событий в AWT 1.1, вы будете часто переопределять реализацию слушателей событий (listeners) для обеспечения нужной функциональности.
При этом, не трудно ошибиться при наборе имени переопределяемого метода. В этом случае метод не будет переопределён, а будет создан совершенно новый метод, но с теми же параметрами и типом возвращаемого значения.
public class MyWindowListener extends WindowAdapter {
// Этот метод должен называться WindowClosed
public void WindowClose(WindowEvent e) {
// Выходим, когда пользователь закрыл окно
System.exit(0);
}
});
Естественно, компилятор не сможет обнаружить эту ошибку, и будет весьма неприятно, когда она обнаружится случайно.
Чтобы гарантировать, что мы не допустили ошибку при переопределении метода, необходимо убедиться, что новый (переопределённый) метод вызывается: для этого можно использовать оператор println, запись в log-файл, либо хороший трассирующий отладчик.
8. Ошибка использования оператора сравнения ( = вместо == )
Эту ошибку легко допустить, особенно если вы раньше программировали на другом языке, подобном языку Pascal. Там для присваивания используется оператор :=, а для сравнения используется оператор =.
К счастью, даже если вы не заметили эту ошибку в коде сразу, компилятор будет ругаться на неё. Чаще всего, это приводит к ошибке вида:
«Can't convert xxx to boolean», где xxx – это Java-тип, которому вы присваиваете значение, вместо операции сравнения.
7. Ошибка при сравнении двух объектов ( == вместо .equals)
При использовании оператора ==, фактически происходит сравнение двух ссылок на объект, для того, чтобы убедиться, что они указывают на один и тот же объект. Оператор == нельзя использовать для сравнения символов внутри строк, для этого необходимо использовать метод .equals, который наследуется всеми классами от java.lang.Object. Нижеприведённый пример отображает разницу между операторами == и .equals:
public static void main(String args[])
{
String strOld = "Habrahabr";
String strNew = new String(strOld);
System.out.println(strOld + " equals " + strNew + " -> " + strOld.equals(strNew));
System.out.println(strOld + " == " + strNew + " -> " + (strOld == strNew));
}
6. Путаница при использовании передачи параметра по значению и при передаче параметра по ссылке.
Java использует оба метода передачи параметров, поэтому важно понимать, когда параметр передаётся по значению, а когда – по ссылке.
При передаче простого типа данных, такого как char, int, float или double, он передаётся по значению. Это означает, что создаётся копия передаваемого типа данных, которое и передаётся в качестве параметра. Если внутри функции это значение будет изменено, то это изменение отразится только лишь на созданной копии объекта, а оригинальное значение не изменится. Поэтому, если необходимо, чтобы функция изменяла простой тип данных, который передаётся в неё, необходимо сделать его возвращаемым значением этой функции, либо поместить его внутрь объекта.
При передаче в качестве параметра объекта, такого как: массив, вектор или строка – происходит передача по ссылке. Это означает, что вы передаёте ссылку на объект, а не на его копию. Любые изменения, которые будут сделаны с этим объектом – отображаются и сохраняются сразу же.
Небольшое замечание: так как такой объект, как строка, не содержит методов для изменения его содержания, его также можно передавать по значению.
5. Создание пустых обработчиков ошибок.
Очень заманчиво писать пустые обработчики ошибок и просто игнорировать любые возникающие ошибки, но если произойдет какая-нибудь проблема, то будет очень трудно найти причину ошибки.
Поэтому, даже простейший обработчик ошибок – полезен.
Например, нижеприведённая обертка вашего кода отловит любое исключение и выведет сообщение о нём. Не обязательно писать свой отдельный обработчик для каждого вида исключения (хотя, это всё ещё остается хорошей практикой программирования).
public static void main(String args[])
{
try {
// Здесь должен быть ваш код
}
catch (Exception e)
{
System.out.println ("Ошибка: " + e );
}
}
4. В языке Java индексация начинается с нуля.
Если вы раньше программировали на Си-подобном языке, вы не столкнетесь с этой ошибкой, чего нельзя сказать о тех, кто ранее программировал на некоторых других языках.
Массивы в Java индексируются с нуля, т.е. первый элемент массива имеет индекс 0.
Небольшой пример:
public static void main(String args[])
{
// Создаём массив из трёх строк
String[] strArray = new String[3];
// Индекс первого элемента = 0
strArray[0] = "First string";
// Индекс второго элемента = 1
strArray[1] = "Second string";
// Индекс третьего элемента = 2
strArray[2] = "Third and final string";
}
В этом примере у нас есть массив, состоящий из трёх строк, но на самом деле, для доступа к нужному элементу массива, нужно вычесть единицу из его порядкового номера. Если мы забудем это, и попытаемся получить доступ к третьему элементу нашего массива, использовав strArray [3], это приведёт к возникновению ошибки ArrayOutOfBoundsException, так как таким образом мы попытались обратиться к несуществующему, четвёртому элементу нашего массива. Возникновение этой ошибки – возможный признак того, что вы забыли об индексации с нуля в Java.
Другое место, где проблема с индексацией с нуля также актуальна – это работа со строками, где позиция символа также начинается с нуля.
Так, для получения определённого символа строки по порядковому номеру — используется функция String.charAt (INT), где INT – это порядковый номер символа в строке: 0 – первый символ, 1 – второй символ, и т. д.
Не стоит об этом забывать, иначе это приведёт к сложным проблемам, особенно в приложениях с активной обработкой строк. При этом, вы будете работать не с теми символами, о которых предполагали, и это также может привести к появлению ошибки, подобной ArrayOutOfBoundsException: попытка доступа за пределы размерности строки приведёт к возникновению исключения StringIndexOutOfBoundsException.
Нижеприведённый пример иллюстрирует это:
public class StrDemo
{
public static void main (String args[])
{
String abc = "abc";
System.out.println ("Символ на позиции 0 : " + abc.charAt(0) );
System.out.println ("Символ на позиции 1 : " + abc.charAt(1) );
System.out.println ("Символ на позиции 2 : " + abc.charAt(2) );
// Эта строка будет причиной появления ошибки StringIndexOutOfBoundsException
System.out.println ("Символ на позиции 3 : " + abc.charAt(3) );
}
}
Стоит отметить, что индексация с нуля применяется в Java не только для массивов или для строк: в классах java.util.Date и java.util.Calendar нумерация месяцев начинается с 0, но дни нумеруются с 1.
Это демонстрируется в нижеприведённом примере:
import java.util.Date;
import java.util.Calendar;
public class ZeroIndexedDate
{
public static void main (String args[])
{
// Получим сегодняшнюю дату
Date today = new Date();
System.out.println ("Date.getMonth() returns : " + today.getMonth());
System.out.println ("Date.getDate() returns : " + today.getDate());
// Получим сегодняшнюю дату с использованием объекта Calendar
Calendar rightNow = Calendar.getInstance();
System.out.println ("Calendar.get (month) returns : " + rightNow.get ( Calendar.MONTH ));
System.out.println ("Calendar.get (day) returns : " + rightNow.get ( Calendar.DAY_OF_MONTH ));
}
}
Для того, чтобы избежать проблемы с индексацией с ноля в Java — всегда смотрите в документации, с какого элемента индексируется нужная вам сущность, если вы не знаете этого точно.
3. Одновременный доступ к общим переменным из потоков.
При написании многопоточных приложений многие программисты (включая автора оригинальной статьи) идут путём наименьшего сопротивления и оставляют свои программы уязвимыми для конфликта потоков.
Когда два потока (или более) одновременно обращаются к одной и той же переменной, существует возможность (по закону Мерфи – большая вероятность) того, что два потока попытаются одновременно изменить значение этой переменной.
Подобные проблемы не ограничиваются многопоточными приложениями или аплетами; если вы пишите код для Java API или JavaBeans, ваш код не может быть потокобезопасным.
Даже если вы ни разу не написали ни одного многопоточного приложения, люди которые будут использовать ваш код – напишут. Поэтому, вы всегда должны стараться предотвратить возможность одновременного доступа к общим данным.
Как решается эта проблема?
Есть несколько способов. Самый простой способ – сделать переменные закрытыми (private), и использовать синронизированные методы доступа (с ключевым словом synchronized). Такие методы доступа обеспечивают доступ к закрытым переменным с автоматическим использованием блокировок, и только один поток сможет изменить значение переменной в один и тот же момент времени.
Нижеприведённый пример использует такие методы и показывает безопасный способ изменения значения для счётчика:
public class MyCounter
{
private int count = 0; // счётчик начинается с 0
public synchronized void setCount(int amount)
{
count = amount;
}
public synchronized int getCount()
{
return count;
}
}
2. Игнорирование общепринятого стиля написания кода.
Это одна из самых частых ошибок, которую допускают многие. Это очень просто сделать, и иногда, смотря на название метода набранного в нижнем регистре, можно не увидеть ошибки. Придерживаться общепринятого стиля написания кода – хорошая практика написания кода.
Поэтому, просто запомните основные правила оформления имён переменных (и методов):
- названия всех методов и переменных-членов в Java API должны начинаться с маленьких букв
- в названиях всех методов и переменных-членов каждое новое последующее слово должно начинаться с большой буквы — например, — getDoubleValue() или methodForCreateTextFile()
1. Пустые указатели (null pointers)
Использование пустых указателей – это одна из самых распространенных ошибок начинающих Java программистов.
Компилятор не может отследить эту ошибку, поскольку она может возникнуть только лишь во время выполнения программы. Когда происходит попытка доступа к объекту, ссылка на который равна null, возникает ошибка NullPointerException.
Причины возникновения этой ошибки могут отличаться, но в общих случаях появление этой ошибки означает, что либо была попытка доступа к неинициализированному объекту, либо не было проверки значения, возвращаемого функцией.
Если функция может вернуть null, то перед использованием ссылки на объект проверяйте каждый раз возвращаемое значение.
Для примера посмотрите на следующий код и попытайтесь определить, в каком месте возникнет ошибка:
public static void main(String args[])
{
// Разрешим до 3 параметров
String[] list = new String[3];
int index = 0;
while ( (index < args.length) && ( index < 3 ) )
{
list[index++] = args[index];
}
// Проверим всепараметры
for (int i = 0; i < list.length; i++)
{
if (list[i].equals "-help")
{
// .........
}
else
if (list[i].equals "-cp")
{
// .........
}
// else .....
}
}
Когда пользователь вводит три и более параметра, этот код будет работать нормально. Если пользователь не введёт параметры, возникнет ошибка времени выполнения NullPointerException.
При определённых условиях, ваши переменные будут инициализированы, а при других – нет, поэтому есть одно простое решение – ПРЕЖДЕ, чем попытаться получить доступ к переменной в массиве, нужно проверить, что он не равен null.
Заключение.
Эти ошибки – лишь небольшой список тех ошибок, который допускают Java программисты.
Полностью избежать ошибок при программировании невозможно, но опыт и внимательность позволят избежать повторения одних и тех же ошибок.
Все Java программисты сталкиваются с подобными ошибками, и радуйтесь, что когда вы остались до поздна на работе в поисках причины ошибки, и уже нашли её – в это время кто-то, где-то (особенно, кто не читал этот пост) только наступает на те же самые грабли.
Автор: Sysman