Рубрики
Без рубрики

Каждый Java Junior делает это: Распространенные ошибки Java, допускаемые новичками

Все совершают ошибки, не только новички, но даже профессионалы. В этой статье их более дюжины… С пометкой java, новички.

Все совершают ошибки, не только новички, но даже профессионалы. В этой статье рассматривается более десятка распространенных ошибок, которые совершают новички и новички в Java, и как их избежать. Допускали ли вы или ваши коллеги какие-либо из этих распространенных ошибок Java в начале вашей карьеры?

Ошибки совершают все, не только ученики или новички, но и профессионалы. Как курс программирования, команда Code Gym часто собирает ошибки новичков, чтобы улучшить наш автокалидатор. На этот раз мы решили взять интервью у опытных программистов об ошибках в Java, которые они допустили ближе к началу своей карьеры или заметили их среди своих молодых коллег.

Мы собрали их ответы и составили этот список из дюжины популярных ошибок, которые совершают новички Java. Порядок ошибок является случайным и не несет в себе никакого особого значения.

#1. ООП: Неправильное построение иерархии объектов. В частности, непонимание того, где применять интерфейс, а где абстрактный класс.

Абстрактный класс позволяет создавать функциональные возможности, которые подклассы могут реализовывать или переопределять. В интерфейсе вы просто определяете функциональность, но не реализуете ее. Хотя класс может расширять только один абстрактный класс, он может использовать несколько интерфейсов.

Выбор между интерфейсом и абстрактным классом зависит от многих факторов.

  • Java не поддерживает множественное наследование, поэтому каждый класс может наследовать только один суперкласс. Однако любой класс может реализовать несколько интерфейсов.
  • Если вы решите написать много методов, то абстрактный класс – лучшее решение для вас, потому что вы можете предоставить реализации по умолчанию для некоторых методов, которые являются общими для всех подклассов.
  • Если вам нужно добавить или удалить метод из интерфейса, это может быть проблемой, потому что вы не можете объявить дополнительные методы в интерфейсе, не изменив все классы, которые реализуют этот интерфейс.

Давайте посмотрим на приведенный ниже пример. Здесь мы имеем неправильное построение иерархии объектов. В частности, непонимание того, когда следует применять интерфейс, а когда должен быть абстрактный класс.

 
public interface BaseEntity {
    long getId();
    void setId(long id);
}

public interface NamedEntity extends BaseEntity {
    String getName();
    void setName(String name);
}
public class User implements NamedEntity {

    private long id;
    private String name;
    private String avatarUrl;

    @Override
    public long getId() {
        return id;
    }
    @Override
    public void setId(long id) {
        this.id = id;
    }
    @Override
    public String getName() {
        return name;
    }

    @Override
    public void setName(String name) {
        this.name = name;
    }

    public String getAvatarUrl() {
        return avatarUrl;
    }

    public void setAvatarUrl(String avatarUrl) {
        this.avatarUrl = avatarUrl;
    }
}

public class Group implements NamedEntity {

    private long id;
    private String name;
    private String description;

    @Override
    public long getId() {
        return id;
    }

    @Override
    public void setId(long id) {
        this.id = id;
    }

    @Override
    public String getName() {
        return name;
    }

    @Override
    public void setName(String name) {
        this.name = name;
    }

    public String getDescription() {
        return description;
    }

    public void setDescription(String description) {
        this.description = description;
    }
}

public class Comment implements BaseEntity {

    private long id;
    private String content;

    @Override
    public long getId() {
        return id;
    }

    @Override
    public void setId(long id) {
        this.id = id;
    }

    public String getContent() {
        return content;
    }

    public void setContent(String content) {
        this.content = content;
    }
}

Здесь новичок использует интерфейс, несмотря на то, что класс гораздо больше подходит для этой задачи. Причина в том, что вы можете сохранить дублированный код в классе. Если это необходимо, вы можете добавить интерфейсы поверх родительских классов. Итак, вот правильное решение (использование абстрактных классов вместо интерфейсов):

 public abstract class BaseEntity {

    private long id;

    public long getId() {
        return id;
    }

    public void setId(long id) {
        this.id = id;
    }
}

public abstract class NamedEntity extends BaseEntity {

    private String name;

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }
}
public class User extends NamedEntity {

    private String avatarUrl;

    public String getAvatarUrl() {
        return avatarUrl;
    }

    public void setAvatarUrl(String avatarUrl) {
        this.avatarUrl = avatarUrl;
    }
}

public class Group extends NamedEntity {

    private String description;

    public String getDescription() {
        return description;
    }
    public void setDescription(String description) {
        this.description = description;
    }
}
public class Comment extends BaseEntity {
    private String content;

    public String getContent() {
        return content;
    }
    public void setContent(String content) {
        this.content = content;
    }
}

#2. ООП: беспорядок с порядком вызова конструкторов

Новички часто забывают о порядке вызова конструкторов при создании объектов. Правило простое: конструкторы вызываются в порядке наследования. Когда вы думаете о логике, становится ясно, что выполнение конструкторов в порядке наследования имеет некоторый смысл.

Суперкласс ничего не знает о своих подклассах. Таким образом, любая инициализация должна выполняться в суперклассе полностью независимо от любой инициализации, выполняемой подклассом. Поэтому это следует сделать в первую очередь.

 
public class Animal {
   public Animal() {
       System.out.println("Animal constructor has worked off");
   }
}

public class Cat extends Animal {
   public Cat() {
       System.out.println("Cat constructor has worked off!");
   }
   public static void main(String[] args) {
       Cat cat = new Cat();
   }
}

The Output is: 
Animal constructor has worked off
Cat constructor has worked off!

Как вы можете видеть, при создании дочерних элементов сначала неявно вызываются конструкторы базового класса.

Кроме того, важно не забывать вызывать правильный родительский конструктор, в противном случае будет вызван родительский конструктор по умолчанию, как в следующем примере:

public abstract class AbstractToken {


    private final Collection authorities;


    public AbstractToken(Collection authorities) {

        if (authorities == null) {

            this.authorities = Collections.emptyList();

            return;

        }

        for (Object a : authorities) {

            if (a == null) {

                throw new IllegalArgumentException("Authorities collection cannot contain any null elements");

            }

        }

        ArrayList temp = new ArrayList<>(authorities.size());

        temp.addAll(authorities);

        this.authorities = Collections.unmodifiableList(temp);

    }

    public AbstractToken() {

        this.authorities = Collections.emptyList();

    }

}

public class MyToken extends AbstractToken {



    private int userId;



    public MyToken(User user, Object... authority) {

        this.userId = user.getId();

    }

}

public class User {

    private int id;

    public int getId() {
        return id;
    }

Конструктор родительского класса явно не вызывается в конструкторе Моего токена. Таким образом, будет вызван конструктор AbstractToken без параметров. В этом конструкторе отсутствует необходимая часть инициализации объекта. Не забудьте вызвать конкретный конструктор, который вам действительно нужен:

public class MyToken extends AbstractToken {


    private int userId;


    public MyToken(User user, Object... authority) {

        super(Arrays.asList(authority));

        this.userId = user.getId();

    }

}

#3. ООП: Путаница с переопределением и перегрузкой

Переопределение и перегрузка – это два очень важных понятия в Java. Они могут действительно сбить с толку начинающих программистов Java.

Проще говоря, переопределение позволяет вам взять метод родительского класса и написать собственную реализацию этого метода в каждом унаследованном классе. Новая реализация “заменит” родительский элемент в дочернем классе.

Вот пример. У нас есть класс Animal с методом voice().

public class Animal {

   public void voice() {

    System.out.println("Speak!");
   }
}

Допустим, нам нужно переопределить поведение метода в производных классах. Например, мы реализуем 4 класса-наследника, которые будут иметь свою собственную реализацию метода voice.

public class Bear extends Animal {
   @Override
   public void voice() {
       System.out.println("Grrr!");
   }
}
public class Cat extends Animal {

   @Override
   public void voice() {
       System.out.println("Meow!");
   }
}

public class Dog extends Animal {

   @Override
   public void voice() {
       System.out.println("Bow-wow!");
   }
}


public class Snake extends Animal {

   @Override
   public void voice() {
       System.out.println("Hiss-hiss!");
   }
}

Теперь давайте проверим, как это работает.

public class Main {

   public static void main(String[] args) {

       Animal animal1 = new Dog();
       Animal animal2 = new Cat();
       Animal animal3 = new Bear();
       Animal animal4 = new Snake();

       animal1.voice();
       animal2.voice();
       animal3.voice();
       animal4.voice();
   }
}

The output is: 
Bow-wow!
Meow!
Grrr! 
Hiss-hiss!

В дополнение к переопределению, в программе мы можем использовать методы с одинаковым именем, но с разными типами и/или количеством параметров. Этот механизм называется перегрузкой метода.

public class Program{

    public static void main(String[] args) {

        System.out.println(sum(2, 3));          // 5
        System.out.println(sum(4.5, 3.2));      // 7.7
        System.out.println(sum(4, 3, 7));       // 14
    }
    static int sum(int x, int y){

        return x + y;
    }
    static double sum(double x, double y){

        return x + y;
    }
    static int sum(int x, int y, int z){

        return ukx + y + z;
    }
}

Здесь определены три варианта или три перегрузки метода sum(), но при его вызове, в зависимости от типа и количества переданных параметров, система выберет наиболее подходящую версию.

Есть еще один вариант, когда новичок может ошибиться при переопределении. Вы можете просто запомнить, в каком случае вызывается какой метод:

public class MyParent {
    public void testPrint() {
        System.out.println("parent");
    }
}

public class MyChild extends MyParent {

    @Override

    public void testPrint() {

        System.out.println("child");

    }


    public static void main(String[] args) {

        MyParent o1 = new MyParent();

        MyParent o2 = new MyChild();

    MyChild o3 = new MyChild();

//      MyChild o4 = (MyChild) new MyParent(); // ClassCastException



        o1.testPrint();

        o2.testPrint();

        o3.testPrint();

    }

} 
The output is:
parent
child
child

#4. Неправильная работа с исключениями

Очень часто начинающие программисты не знают, как правильно работать с исключениями. Начнем с того, что они просто игнорируют их, особенно те, которые перешли с других языков программирования. Однако по какой-то причине возникают исключения, поэтому не игнорируйте их.

Есть также случаи небрежного обращения с исключениями. Например, новичок пишет код, и его IDE начинает подчеркивать его красным цветом и объясняет, что во время его выполнения могут возникать определенные исключения. В этом случае неопытный программист часто предпочитает обернуть весь код в try-catch и ничего не делать в блоке catch:

public static void main(String[] args) {
    try {
        String urlString = new Scanner(System.in).nextLine();
        URL url = new URL(urlString);
        String content = new Scanner(url.openStream(), StandardCharsets.UTF_8).useDelimiter("\\A").next();
        System.out.println(content);
    } catch (IOException ignored) {
    }
}

Проблема в том, что в этом случае мы не будем знать, что за исключение произошло, по какой причине и произошло ли оно вообще.

Существуют разные варианты того, как исправить ситуацию. Например:

  • отображение трассировки стека:
public static void main(String[] args) {

    try {

        String urlString = new Scanner(System.in).nextLine();

        URL url = new URL(urlString);

        String content = new Scanner(url.openStream(), StandardCharsets.UTF_8).useDelimiter("\\A").next();

        System.out.println(content);

    } catch (IOException e) {

        e.printStackTrace();

    }

}
  • выбросьте исключения выше:
public static void main(String[] args) throws IOException {

    String urlString = new Scanner(System.in).nextLine();

    URL url = new URL(urlString);

    String content = new Scanner(url.openStream(), StandardCharsets.UTF_8).useDelimiter("\\A").next();

    System.out.println(content);

}
  • обрабатывайте каждое исключение отдельно:
public static void main(String[] args) {

    URL url = getUrl();

    String content = getContent(url, 3);

    System.out.println(content);

}

private static String getContent(URL url, int attempts) {

    while (true) {

        try {

            return new Scanner(url.openStream(), StandardCharsets.UTF_8).useDelimiter("\\A").next();

        } catch (IOException e) {

            if (attempts == 0) {

                throw new RuntimeException(e);

            }

            attempts--;

        }

    }

}

private static URL getUrl() {

    while (true) {

        String urlString = new Scanner(System.in).nextLine();

        try {

            return new URL(urlString);

        } catch (MalformedURLException e) {

            System.out.println("URL is incorrect. Please try again.");

        }

    }

}

#5. Проблемы с выбором правильных коллекций

Новички часто путаются в выборе правильной коллекции. Иногда они выбирают ту или иную коллекцию по ошибке, потому что не очень хорошо разбираются в структуре данных. Неправильный выбор может повлиять на эффективность вашей программы. С другой стороны, если коллекция выбрана правильно, ваша программа будет выглядеть проще и логичнее, а решение будет более эффективным.

Чтобы выяснить, какой тип коллекции подходит для вашей задачи, выясните характеристики и поведение каждого из них, а также различия между ними. Вы должны четко представлять себе плюсы и минусы каждой конкретной реализации (ArrayList против LinkedList, TreeMap против HashMap и так далее).

Я рекомендую для первых шагов:

  1. Исследуйте не только структуру коллекций, но и теоретические структуры данных.
  2. Создайте таблицу со всеми коллекциями. Дайте краткое определение того, какая структура данных лежит в основе, каковы ее особенности.
  3. Задайте себе несколько основных вопросов. Должна ли ваша коллекция разрешать доступ к элементам по индексу? Принимается ли в нем значение null? Разрешено ли дублировать элементы? Важны ли для вашего решения быстрое добавление и быстрое удаление элементов? Должен ли он поддерживать параллелизм?

#6. Незнание библиотек Java, переосмысление колес

Для Java написано огромное количество библиотек, но новички часто не замечают всех этих жемчужин. Не пытайтесь изобретать велосипед заново, сначала изучите существующие разработки по интересующему вас вопросу. Многие библиотеки были усовершенствованы разработчиками на протяжении многих лет, и вы можете использовать их бесплатно. Например, Google Guava или Log4j.

Давайте приведем пример. Вот как выглядит код без использования библиотек:

private static Map getFrequencyMap(Set words, List wordsList) {
    Map result = new HashMap<>();
    for (String word : words) {
        int count = 0;
        for (String s : wordsList) {
            if (word.equals(s)) {
                count++;
            }
        }
        result.put(word, count);
    }
    return result;
}

Здесь мы используем Коллекции.библиотека частот():

private static Map getFrequencyMap(Set words, List wordsList) {

    Map result = new HashMap<>();

    for (String word : words) {

        result.put(word, Collections.frequency(wordsList, word));

    }

    return result;

}

Конечно, здесь у нас есть только краткий пример, поэтому сложность кода изменилась лишь незначительно. Однако читаемость кода значительно повысилась. В больших проектах с большим количеством классов использование встроенных и сторонних библиотек может значительно ускорить разработку, улучшить читаемость и тестируемость. Так что не забудьте изучить библиотеки Java.

#7. Игнорирование JUnit и неправильное тестирование вашего собственного кода

Очень часто начинающие программисты неправильно “тестируют” свой код. Например, используя System.out.println(), подставляя и выводя на консоль разные значения. Серьезно, с самых первых шагов вы должны научиться использовать отличную библиотеку JUnit и писать тесты для своих программ. Более того, вам это обязательно понадобится в вашей работе. Модульное тестирование вашего собственного кода – хорошая практика для разработчиков.

#8. Забыв освободить ресурсы

Каждый раз, когда ваша программа открывает файл или устанавливает сетевое подключение, вам необходимо освободить ресурсы, которые она использует. Это также верно для случаев исключений при работе с ресурсами. Конечно, FileInputStream имеет финализатор, который вызывает метод close() для сбора мусора. Однако вы не можете быть уверены в начале цикла сборки. Таким образом, существует риск того, что входной поток может потреблять ресурсы бесконечно.

Проблема для начинающих заключается в том, что отсутствие освобождения ресурсов не приводит к ошибкам во время компиляции или выполнения. Так что об этом легко забыть. Давайте приведем пример.

private void populateConfigs(Path propertiesPath) throws IOException {
    assert propertiesPath != null;
    DirectoryStream directoryStream = Files.newDirectoryStream(propertiesPath, "*.properties");
    for (Path entry : directoryStream) {
        Properties properties = new Properties();
        properties.load(Files.newBufferedReader(entry));
        validateAndSave(properties);
    }
}

Программа работает, но нам лучше сделать это правильно. Давайте закроем ресурс следующим образом:

private void populateConfigs(Path propertiesPath) throws IOException {

    assert propertiesPath != null;

    try (DirectoryStream directoryStream = Files.newDirectoryStream(propertiesPath, "*.properties")) {

        for (Path entry : directoryStream) {

            Properties properties = new Properties();

            properties.load(Files.newBufferedReader(entry));

            validateAndSave(properties);

        }

    }

}

#9. Проблемы с равными и хэш-кодом

Класс Object является родительским классом для всех объектов Java. Этот класс имеет методы equal() и hashCode (). Метод equals (), как следует из его названия, используется для простой проверки того, равны ли два объекта. Метод hashCode(), который позволяет получить уникальное целое число для данного объекта.

Часто новички не чувствуют, что эти методы нужно переопределять для их объектов.

Реализация метода equals() по умолчанию просто проверяет два объекта по ссылке, чтобы убедиться, что они эквивалентны.

Например, вам нужно сравнить две точки на координатной плоскости, давайте попробуем переопределить метод equals:

public class Point {
    private int x;
    private int y;
    public Point(int x, int y) {
        this.x = x;
        this.y = y;
    }

Теперь, не переопределяя метод equals() для класса Point, давайте попробуем сравнить точки. Для этого давайте создадим демонстрационный класс Point и в нем есть три точки, две по логике декартовых координат — равные (с одинаковыми абсциссой и ординатой) и третья, которая отличается от них.

public class PointDemo {
   public static void main(String[] args) {
       Point point1 = new Point(2, 3);
       Point point2 = new Point(2, 3);
       Point point3 = new Point(2, 5);

       if (point1.equals(point2))
           System.out.println("1 and 2 are equal");
       else
           System.out.println("1 and 2 are not equal");

       if (point1.equals(point3)) System.out.println("1 and 3 are equal");
       else System.out.println("1 and 3 are not equal");
   }
}
The output is: 
1 and 2 are not equal
1 and 3 are not equal

Чаще всего практикующий новичок забывает не переопределять equals(), а правильно переопределять его. Например, они забывают, что объект может быть нулевым или что его нужно проверить на эквивалентность самому себе. Давайте напишем правильный метод equals() для сравнения двух точек на плоскости:

public boolean equals(Object obj) {
        if (obj == null)   return false; // checking if the passed object is null
        if (obj == this)   return true; //checking if the passed object is equal to itself 
        if (obj.getClass() == this.getClass()) { // checking if the passed object has the same result of the getClass () method as the current one, on which the equals method was called
            Point point = (Point) obj;  // now we can definitely convert the passed object to type Point
            if (point.x == this.x && point.y == this.y) // if the coordinates match, then return true, otherwise false
                return true;
        }
        return false;
    }
}

Теперь, если мы запустим метод main() класса Point Demo(), мы получим следующий результат:

1 and 2 are equal
1 and 3 are not equal

#10. Работа с неинициализированными объектами

Эта ошибка новичка приводит к исключению Nullpointerexception, которое возникает, когда они пытаются использовать неинициализированный объект. Пусть вас не смущает объявление объектной переменной, это не означает, что она инициализирована.

Так что, если вы напишете что-то вроде:

private static String name;
    public static void main(String[] args) {
        System.out.println(name.length());
    }

вы получите исключение при попытке вызвать метод length, поскольку поле name равно null. Вы всегда должны инициализировать их перед работой с переменными.

#11. Неправильная работа с оболочками

Вы должны быть осторожны и помнить, что классы-оболочки, такие как Integer или Boolean, являются ссылочными типами данных, а тип переменной может быть null. В этом случае лучше избегать операций, в которых null работает плохо.

Часто новички работают с неинициализированными переменными типа Integer или Boolean где-то как с int или bool, что может вызвать ошибки NullPointerException. Например, если у нас есть какие-либо логические значения, которые по умолчанию равны нулю, и они пытаются вызвать его в некоторых if (s), мы получим сообщение об ошибке.

Кроме того, автобоксинг переменных примитивных типов требует точного соответствия типу исходного примитива — типу “класса-оболочки”. Попытка автоматической упаковки переменной типа byte в Short без предварительного явного приведения byte-> short приведет к ошибке компиляции.

Вот пример:

public static class Man {
        private String name;
        private Integer age;

        public String getName() {
            return name;
        }

        public int getAge() {
            return age;
        }
   public void setAge(Integer age) {
            this.age = age;
        } 
        public static void main(String[] args) {
            Man man = new Man();
            System.out.println(man.getAge());
        }
    }

В этом примере, когда мы запускаем метод main, мы получим ошибку NullPointerException. Это происходит потому, что мы возвращаем int вместо Integer в getAge getter. Как мы уже говорили ранее, оно не может быть null и происходит автоматическая распаковка, в таких случаях вам нужно быть осторожным и обратить внимание, чтобы поле не было null или метод getAge возвращал целое число.

#12. Работа с асинхронным кодом

Эксперты заявили, что наиболее распространенными ошибками среди разработчиков в целом является работа с асинхронным кодом (параллелизм, потоки и т.д.).

Если в реальном проекте возникает необходимость работать с кодом асинхронно, вам не нужно использовать низкоуровневое многопоточное программирование. Это может быть полезно для изучения проблем или каких-то экспериментов. Однако в реальном проекте это приводит к ненужной сложности и множеству потенциальных ошибок. Поэтому при работе с многопоточностью лучше использовать классы из пакета java.util.concurrent или другие готовые сторонние библиотеки (Guava и др.)

Вот пример кода:

public class MyTask implements Runnable {

    private int monitoringPeriod;
    private boolean active;

    @Override
    public void run() {
        // do work
    }

    public int getMonitoringPeriod() {
        return monitoringPeriod;
    }

    public void setMonitoringPeriod(int monitoringPeriod) {
        this.monitoringPeriod = monitoringPeriod;
    }

    public boolean isActive() {
        return active;
    }

    public void setActive(boolean active) {
        this.active = active;
    }
}

public class Solution {

    public static void main(String[] args) {

        ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(8);

        Collection tasks = generateTasks();

        for (MyTask task : tasks) {

            if (task.isActive()) {

               scheduler.scheduleWithFixedDelay(task, 0, task.getMonitoringPeriod(), TimeUnit.SECONDS);

            }

        }

    }

    private static Collection generateTasks() {

        Collection result = new HashSet<>();

        // tasks generation

        return result;

    }

}

Если вы попытаетесь сделать то же самое, но вручную запустите потоки, вы можете получить трудночитаемый и сложный в обслуживании код. Это также связано с изобретением колеса.

Вывод

Мы попытались описать самые популярные ошибки, допускаемые новичками Java, по мнению экспертов, опытными программистами. Многие примеры предназначены в статье для того, чтобы новичкам было удобно как можно скорее избавиться от этих ошибок.

На самом деле, на самом деле это не ошибки. Это определенные этапы практического использования языка, через которые проходит большинство начинающих Java-программистов. Ошибки самого первого этапа (например, неправильное размещение фигурных скобок или точек с запятой мы опустим) и начнем с тех, которые часто делают Java Trainee и Java Junior. Конечно, желательно не затягивать этот этап “популярных ошибок” надолго. Это именно то, чего мы желаем вам на вашем пути разработчика Java.

Впервые опубликовано на (JaxEnter) [ https://jaxenter.com/java-mistakes-174395.html ].

Оригинал: “https://dev.to/alexyelenevych/every-java-junior-does-it-common-java-mistakes-made-by-newcomers-12c1”