Поиск…


Вступление

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

Это включает в себя любые распространенные ошибки в использовании языка Java или понимание среды выполнения.

Ошибки, связанные с конкретными API-интерфейсами, могут быть описаны в разделах, относящихся к этим API. Строки - это особый случай; они описаны в Спецификации языка Java. Подробности, отличные от распространенных ошибок, можно описать в этом разделе на строках .

Pitfall: использование == для сравнения объектов примитивных оберток, таких как Integer

(Эта ошибка относится одинаково ко всем примитивным типам обертки, но мы проиллюстрируем ее для Integer и int .)

При работе с объектами Integer возникает соблазн использовать == для сравнения значений, потому что это то, что вы бы сделали с значениями int . И в некоторых случаях это будет работать:

Integer int1_1 = Integer.valueOf("1");
Integer int1_2 = Integer.valueOf(1);

System.out.println("int1_1 == int1_2: " + (int1_1 == int1_2));          // true
System.out.println("int1_1 equals int1_2: " + int1_1.equals(int1_2));   // true

Здесь мы создали два объекта Integer со значением 1 и сравним их (в этом случае мы создали один из String и один из int literal. Существуют и другие альтернативы). Кроме того, мы видим, что оба метода сравнения ( == и equals ) дают true .

Такое поведение меняется, когда мы выбираем разные значения:

Integer int2_1 = Integer.valueOf("1000");
Integer int2_2 = Integer.valueOf(1000);

System.out.println("int2_1 == int2_2: " + (int2_1 == int2_2));          // false
System.out.println("int2_1 equals int2_2: " + int2_1.equals(int2_2));   // true

В этом случае, только equals сравнение дает правильный результат.

Причиной этого различия в поведении является то, что JVM поддерживает кеш объектов Integer для диапазона от -128 до 127. (Верхнее значение может быть переопределено системным свойством «java.lang.Integer.IntegerCache.high» или JVM-аргумент "-XX: AutoBoxCacheMax = размер"). Для значений в этом диапазоне Integer.valueOf() вернет кешированное значение, а не создает новый.

Таким образом, в первом примере Integer.valueOf(1) и Integer.valueOf("1") возвращают тот же кешированный экземпляр Integer . Напротив, во втором примере Integer.valueOf(1000) и Integer.valueOf("1000") создали и вернули новые объекты Integer .

Оператор == для эталонных типов тестов для ссылочного равенства (т. Е. Одного и того же объекта). Поэтому в первом примере int1_1 == int1_2 true потому что ссылки одинаковы. Во втором примере int2_1 == int2_2 является ложным, потому что ссылки разные.

Pitfall: забыв о свободных ресурсах

Каждый раз, когда программа открывает ресурс, такой как файл или сетевое соединение, важно освободить ресурс, как только вы закончите его использование. Аналогичная осторожность должна быть предпринята, если во время операций на таких ресурсах следует выбросить какие-либо исключения. Можно утверждать, что FileInputStream имеет финализатор, который вызывает метод close() в событии сбора мусора; однако, поскольку мы не можем быть уверены, когда начнется цикл сбора мусора, поток ввода может потреблять компьютерные ресурсы в течение неопределенного периода времени. Ресурс должен быть закрыт в finally разделе блока try-catch:

Java SE 7
private static void printFileJava6() throws IOException {
    FileInputStream input;
    try {
        input = new FileInputStream("file.txt");
        int data = input.read();
        while (data != -1){
            System.out.print((char) data);
            data = input.read();
        }
    } finally {
        if (input != null) {
            input.close();
        }
    }
}

Так как Java 7 действительно полезный и аккуратный оператор, введенный в Java 7, особенно для этого случая, называемый try-with-resources:

Java SE 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();
        }
    }
}

Оператор try-with-resources может использоваться с любым объектом, который реализует интерфейс Closeable или AutoCloseable . Он гарантирует, что каждый ресурс будет закрыт до конца инструкции. Разница между двумя интерфейсами заключается в том, что метод close() Closeable вызывает Closeable IOException которое должно быть обработано каким-то образом.

В тех случаях, когда ресурс уже открыт, но после его использования он должен быть безопасно закрыт, его можно назначить локальной переменной внутри try-with-resources

Java SE 7
private static void printFileJava7(InputStream extResource) throws IOException {
    try (InputStream input = extResource) {
        ... //access resource
    }
}

Локальная переменная ресурса, созданная в конструкторе try-with-resources, фактически является окончательной.

Pitfall: утечки памяти

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

Тем не менее, вы можете предотвратить освобождение памяти, позволяя объектам быть доступными, которые больше не нужны. Если вы называете это утечкой памяти или упаковкой памяти, результат будет таким же - ненужное увеличение выделенной памяти.

Утечки памяти в Java могут происходить по-разному, но наиболее распространенной причиной являются вечные ссылки на объекты, поскольку сборщик мусора не может удалить объекты из кучи, пока есть ссылки на них.

Статические поля

Можно создать такую ​​ссылку путем определения класса со static полем, содержащим некоторую коллекцию объектов, и забыть установить это static поле в null после того, как сбор больше не нужен. static поля считаются корнями GC и никогда не собираются. Другой проблемой является утечка в памяти без кучи при использовании JNI .

Утечка класса загрузчика

Тем не менее, самым коварным типом утечки памяти является утечка загрузчика класса. Класс loader содержит ссылку на каждый класс, который он загрузил, и каждый класс содержит ссылку на свой загрузчик классов. У каждого объекта есть ссылка на его класс. Поэтому, если даже один объект класса, загружаемый загрузчиком классов, не является мусором, может быть собрано не один класс, загруженный загрузчиком этого класса. Поскольку каждый класс также ссылается на его статические поля, они также не могут быть собраны.

Утечка утечки. Пример утечки утечки может выглядеть следующим образом:

final ScheduledExecutorService scheduledExecutorService = Executors.newScheduledThreadPool(1);
final Deque<BigDecimal> 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();
}

В этом примере создаются две запланированные задачи. Первая задача берет последнее число из дека, называемого numbers , и, если число делится на 51, оно печатает число и размер дека. Вторая задача помещает числа в deque. Обе задачи запланированы с фиксированной скоростью, и они запускаются каждые 10 мс.

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

Чтобы предотвратить это при сохранении семантики этой программы, мы можем использовать другой метод для pollLast чисел из deque: pollLast . В отличие от метода peekLast , pollLast возвращает элемент и удаляет его из deque, в то время как peekLast возвращает только последний элемент.

Pitfall: использование == для сравнения строк

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

public class Hello {
    public static void main(String[] args) {
        if (args.length > 0) {
            if (args[0] == "hello") {
                System.out.println("Hello back to you");
            } else {
                System.out.println("Are you feeling grumpy today?");
            }
        }
    }
}

Вышеупомянутая программа должна проверять первый аргумент командной строки и печатать разные сообщения, когда она не является словом «привет». Но проблема в том, что это не сработает. Эта программа выведет «Вы чувствуете себя сердитой сегодня?» независимо от того, что первый аргумент командной строки.

В этом конкретном случае String «hello» помещается в пул строк, в то время как String args [0] находится в куче. Это означает, что есть два объекта, представляющих один и тот же литерал, каждый со своей ссылкой. Поскольку == тесты для ссылок, а не фактическое равенство, сравнение даст ложь большую часть времени. Это не означает, что это всегда будет так.

Когда вы используете == для тестирования строк, то, что вы на самом деле тестируете, - это два объекта String - один и тот же объект Java. К сожалению, это не то, что означает равенство строк в Java. Фактически, правильным способом тестирования строк является использование метода equals(Object) . Для пары строк мы обычно хотим проверить, состоят ли они из одних и тех же символов в том же порядке.

public class Hello2 {
    public static void main(String[] args) {
        if (args.length > 0) {
            if (args[0].equals("hello")) {
                System.out.println("Hello back to you");
            } else {
                System.out.println("Are you feeling grumpy today?");
            }
        }
    }
}

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

public class Test1 {
    public static void main(String[] args) {
        String s1 = "hello";
        String s2 = "hello";
        if (s1 == s2) {
            System.out.println("same");
        } else {
            System.out.println("different");
        }
    }
}

Интересно, что это напечатает «тот же», хотя мы тестируем строки неверным образом. Это почему? Поскольку спецификация языка Java (раздел 3.10.5: литералы строк) предусматривает, что любые две строки >> литералы <<, состоящие из одних и тех же символов, будут фактически представлены одним и тем же объектом Java. Следовательно, тест == даст истину для равных литералов. (Строковые литералы «интернированы» и добавляются в общий «пул строк», когда ваш код загружен, но это фактически деталь реализации.)

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

    public class Test1 {
    public static void main(String[] args) {
        String s1 = "hello";
        String s2 = "hel" + "lo";
        String s3 = " mum";
        if (s1 == s2) {
            System.out.println("1. same");
        } else {
            System.out.println("1. different");
        }
        if (s1 + s3 == "hello mum") {
            System.out.println("2. same");
        } else {
            System.out.println("2. different");
        }
    }
}

Это будет выводить «1. same» и «2. different». В первом случае выражение + оценивается во время компиляции, и мы сравниваем один объект String с самим собой. Во втором случае он оценивается во время выполнения, и мы сравниваем два разных объекта String

Таким образом, использование == для тестирования строк в Java почти всегда неверно, но не гарантированно дает неправильный ответ.

Pitfall: тестирование файла перед попыткой его открыть.

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

public static File getValidatedFile(String path) throws IOException {
    File f = new File(path);
    if (!f.exists()) throw new IOException("Error: not found: " + path);
    if (!f.isFile()) throw new IOException("Error: Is a directory: " + path);
    if (!f.canRead()) throw new IOException("Error: cannot read file: " + path);
    return f;
}

Вы можете использовать вышеупомянутый метод следующим образом:

File f = null;
try {
    f = getValidatedFile("somefile");
} catch (IOException ex) {
    System.err.println(ex.getMessage());
    return;
}
try (InputStream is = new FileInputStream(file)) {
    // Read data etc.
}

Первая проблема заключается в сигнатуре для FileInputStream(File) потому что компилятор по-прежнему настаивает на том, что мы поймаем IOException здесь или дальше по стеку.

Вторая проблема заключается в том, что проверки, выполняемые методом getValidatedFile , не гарантируют успеха FileInputStream .

  • Условия гонки: другой поток или отдельный процесс может переименовать файл, удалить файл или удалить доступ для чтения после возвращения getValidatedFile . Это приведет к « IOException » IOException без специального сообщения.

  • Есть те случаи, которые не охватываются этими тестами. Например, в системе с SELinux в режиме принудительного исполнения попытка чтения файла может завершиться неудачей, несмотря на то, что canRead() возвращает true .

Третья проблема заключается в том, что тесты неэффективны. Например, exists вызовы isFile и canRead , каждый из которых выполняет команду syscall для выполнения требуемой проверки. Затем открывается другой syscall, чтобы открыть файл, который повторяет те же проверки за кулисами.

Короче говоря, такие методы, как getValidatedFile , ошибочны. Лучше просто попытаться открыть файл и обработать исключение:

try (InputStream is = new FileInputStream("somefile")) {
    // Read data etc.
} catch (IOException ex) {
    System.err.println("IO Error processing 'somefile': " + ex.getMessage());
    return;
}

Если вы хотите различать ошибки ввода-вывода при открытии и чтении, вы можете использовать вложенный try / catch. Если вы хотите улучшить диагностику открытых сбоев, вы можете выполнить проверки exists , isFile и canRead в обработчике.

Pitfall: мышление переменных как объектов

Никакая переменная Java не представляет объект.

String foo;   // NOT AN OBJECT

Ни один Java-массив не содержит объектов.

String bar[] = new String[100];  // No member is an object.

Если вы ошибочно считаете переменные как объекты, то реальное поведение языка Java удивит вас.

  • Для переменных Java, которые имеют примитивный тип (например, int или float ), переменная содержит копию значения. Все копии примитивного значения неразличимы; т.е. для номера один существует только одно значение int . Примитивные значения не являются объектами, и они не ведут себя как объекты.

  • Для переменных Java, которые имеют ссылочный тип (либо класс, либо тип массива), переменная содержит ссылку. Все копии ссылки неразличимы. Ссылки могут указывать на объекты, или они могут быть null что означает, что они указывают на отсутствие объекта. Однако они не являются объектами, и они не ведут себя как объекты.

Переменные не являются объектами в любом случае, и они не содержат объектов в любом случае. Они могут содержать ссылки на объекты , но это говорит что-то другое.

Пример класса

В следующих примерах используется этот класс, который представляет точку в 2D пространстве.

public final class MutableLocation {
   public int x;
   public int y;

   public MutableLocation(int x, int y) {
       this.x = x;
       this.y = y;
   }

   public boolean equals(Object other) {
       if (!(other instanceof MutableLocation) {
           return false;
       }
       MutableLocation that = (MutableLocation) other;
       return this.x == that.x && this.y == that.y;
   }
}

Экземпляр этого класса представляет собой объект, который имеет два поля x и y которые имеют тип int .

У нас может быть много экземпляров класса MutableLocation . Некоторые из них будут представлять одинаковые местоположения в 2D-пространстве; т.е. соответствующие значения x и y будут совпадать. Другие будут представлять разные местоположения.

Несколько переменных могут указывать на один и тот же объект

 MutableLocation here = new MutableLocation(1, 2);
 MutableLocation there = here;
 MutableLocation elsewhere = new MutableLocation(1, 2);

В приведенном выше MutableLocation мы объявили here три переменные, there и в elsewhere , elsewhere могут храниться ссылки на объекты MutableLocation .

Если вы (неправильно) считаете эти переменные объектами, то вы, скорее всего, неправильно понимаете утверждения:

  1. Скопируйте местоположение «[1, 2]» here
  2. Скопируйте местоположение «[1, 2]» there
  3. Скопируйте местоположение «[1, 2]» в elsewhere

Из этого вы можете сделать вывод, что у нас есть три независимых объекта в трех переменных. Фактически, только два объекта созданы выше. Переменные here и there действительно относятся к одному и тому же объекту.

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

System.out.println("BEFORE: here.x is " + here.x + ", there.x is " + there.x +
                   "elsewhere.x is " + elsewhere.x);
here.x = 42;
System.out.println("AFTER: here.x is " + here.x + ", there.x is " + there.x +
                   "elsewhere.x is " + elsewhere.x);

Это выведет следующее:

BEFORE: here.x is 1, there.x is 1, elsewhere.x is 1
AFTER: here.x is 42, there.x is 42, elsewhere.x is 1

Мы присвоили новое значение here.x и изменили значение, которое мы видим через there.x . Они относятся к одному и тому же объекту. Но значение, которое мы видим через elsewhere.x , не изменилось, поэтому в elsewhere должно быть ссылка на другой объект.

Если переменная была объектом, тогда присваивание here.x = 42 не изменилось бы there.x . there.x

Оператор равенства НЕ проверяет, что два объекта равны

Применение оператора равенства ( == ) для сравнения значений значений, если значения относятся к одному и тому же объекту. Он не проверяет, являются ли два (разных) объекта «равными» в интуитивном смысле.

 MutableLocation here = new MutableLocation(1, 2);
 MutableLocation there = here;
 MutableLocation elsewhere = new MutableLocation(1, 2);

 if (here == there) {
     System.out.println("here is there");
 }
 if (here == elsewhere) {
     System.out.println("here is elsewhere");
 }

Это напечатает «здесь есть», но он не будет печатать «здесь где-то еще». (Ссылки here и в elsewhere предназначены для двух разных объектов.)

Напротив, если мы назовем метод equals(Object) который мы реализовали выше, мы будем тестировать, если два экземпляра MutableLocation имеют одинаковое расположение.

 if (here.equals(there)) {
     System.out.println("here equals there");
 }
 if (here.equals(elsewhere)) {
     System.out.println("here equals elsewhere");
 }

Это напечатает оба сообщения. В частности, здесь here.equals(elsewhere) возвращает true потому что семантические критерии, которые мы выбрали для равенства двух объектов MutableLocation , были выполнены.

Вызов метода НЕ пропускает объекты вообще

Вызов метода Java использует pass по значению 1 для передачи аргументов и возврата результата.

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

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

Однако вы не передаете объект по ссылке 2 . Различие заключается в том, что если копия ссылки на объект модифицирована, чтобы указать на другой объект, исходная ссылка на объект все равно укажет на исходный объект.

void f(MutableLocation foo) {  
    foo = new MutableLocation(3, 4);   // Point local foo at a different object.
}

void g() {
    MutableLocation foo = MutableLocation(1, 2);
    f(foo);
    System.out.println("foo.x is " + foo.x); // Prints "foo.x is 1".
}

Также вы не передаете копию объекта.

void f(MutableLocation foo) {  
    foo.x = 42;
}

void g() {
    MutableLocation foo = new MutableLocation(0, 0);
    f(foo);
    System.out.println("foo.x is " + foo.x); // Prints "foo.x is 42"
}

1 - В таких языках, как Python и Ruby, термин «pass by sharing» является предпочтительным для «pass by value» объекта / ссылки.

2 - Термин «передавать по ссылке» или «вызов по ссылке» имеет очень специфическое значение в терминологии языка программирования. Фактически это означает, что вы передаете адрес переменной или элемента массива , так что, когда вызываемый метод присваивает новое значение формальному аргументу, он изменяет значение в исходной переменной. Java не поддерживает это. Для более подробного описания различных механизмов передачи параметров см. Https://en.wikipedia.org/wiki/Evaluation_strategy .

Pitfall: объединение назначений и побочных эффектов

Иногда мы видим вопросы StackOverflow Java (и вопросы C или C ++), которые задают что-то вроде этого:

i += a[i++] + b[i--];

оценивает ... для некоторых известных начальных состояний i , a и b .

Вообще говоря:

  • для Java ответ всегда задается 1 , но неочевидно, и часто трудно понять
  • для C и C ++ ответ часто не указан.

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

Чтобы проиллюстрировать, следующий простой, казалось бы, простой пример появился несколько раз в вопросах StackOverflow (например, этот ). В некоторых случаях это кажется подлинной ошибкой в ​​чьем-то коде.

int a = 1;
a = a++;
System.out.println(a);    // What does this print.

Большинство программистов (включая экспертов Java), быстро читающих эти утверждения, скажут, что он выводит 2 . Фактически, он выводит 1 . Подробное объяснение причин, пожалуйста, прочитайте этот ответ .

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


1 - по модулю потенциальных проблем с моделью памяти Java, если переменные или объекты видны другим потокам.

Pitfall: Не понимая, что String является неизменным классом

Новые программисты Java часто забывают или не могут полностью понять, что класс Java String неизменен. Это приводит к таким проблемам, как в следующем примере:

public class Shout {
    public static void main(String[] args) {
        for (String s : args) {
            s.toUpperCase();
            System.out.print(s);
            System.out.print(" ");
        }
        System.out.println();
    }
}

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

s.toUpperCase();

Вы можете подумать, что вызов toUpperCase() изменит s на верхнюю строку. Это не так. Это не может! String объекты неизменяемы. Они не могут быть изменены.

На самом деле метод toUpperCase() возвращает объект String который является строчной версией String которую вы вызываете ее. Вероятно, это будет новый объект String , но если s уже был в верхнем регистре, результатом может быть существующая строка.

Поэтому, чтобы эффективно использовать этот метод, вам нужно использовать объект, возвращенный вызовом метода; например:

s = s.toUpperCase();

Фактически правило «строки никогда не изменяется» применяется ко всем методам String . Если вы помните это, тогда вы можете избежать ошибок целой категории начинающих.



Modified text is an extract of the original Stack Overflow Documentation
Лицензировано согласно CC BY-SA 3.0
Не связан с Stack Overflow