![JAVA Evil Edition](http://www.slingingsemicolons.com/files/articles/img/Java-Evil-Edition-orfjackal_net_thumb.png)
Итак, ниже представлен десяток наиболее, на мой взгляд, интересных задач по Java SE из более чем 1000, проработанных мной. Сложность варьируется от средней до ооооооочень сложной. Решение большинства задач практически не требует знания API, достаточно логики и фундаментальных основ Java.
К слову, сложность экзамена Oracle Certified Professional Java Programmer гораздо ниже чем сложность данного теста, поэтому все, кто правильно ответит хотя бы на половину этих вопросов, может смело сдавать этот экзамен без всякой подготовки.
На самом деле количество очень понравившихся мне вопросов в несколько раз больше, поэтому через недельку я планирую написать продолжение данной темы. Если вопросы показались вам слишком легкими или наоборот, слишком сложными — дайте мне знать, внесем коррективы.
ВНИМАНИЕ: во второй половине статьи — ответы и подробные пояснения по соответствующим нюансам JAVA.
Тест
Во всех вопросах правильный ответ только один.
Вопрос 1
Имеется следующий код:
public class Overload{
public void method(Object o) {
System.out.println("Object");
}
public void method(java.io.FileNotFoundException f) {
System.out.println("FileNotFoundException");
}
public void method(java.io.IOException i) {
System.out.println("IOException");
}
public static void main(String args[]) {
Overload test = new Overload();
test.method(null);
}
}
Результатом его компиляции и выполнения будет:
- Ошибка компиляции
- Ошибка времени выполнения
- «Object»
- «FileNotFoundException»
- «IOException»
Вопрос 2
Float f1 = new Float(Float.NaN);
Float f2 = new Float(Float.NaN);
System.out.println( ""+ (f1 == f2)+" "+f1.equals(f2)+ " "+(Float.NaN == Float.NaN) );
Что будет выведено в результате выполнения данного куска кода:
- false false false
- false true false
- true true false
- false true true
- true true true
Вопрос 3
class Mountain {
static String name = "Himalaya";
static Mountain getMountain() {
System.out.println("Getting Name ");
return null;
}
public static void main(String[ ] args) {
System.out.println( getMountain().name );
}
}
Что произойдет при попытке выполнения данного кода:
- Будет выведено «Himalaya» но НЕ будет выведено «Getting Name „
- Будет выведено “Getting Name » и «Himalaya»
- Ничего не будет выведено
- Будет выброшен NullPointerException
- Будет выведено «Getting Name », а потом выброшено NullPointerException
Вопрос 4
Integer a = 120;
Integer b = 120;
Integer c = 130;
Integer d = 130;
System.out.println(a==b);
System.out.println(c==d);
В результате выполнения данного кода будет выведено:
- true true
- false false
- false true
- true false
- произойдет ошибка времени выполнения
Вопрос 5
Прошлый вопрос уже когда-то поднимался на хабре, поэтому этот вопрос для тех, кому был не интересен предыдущий:
//In File Other.java
package other;
public class Other { public static String hello = "Hello"; }
//In File Test.java
package testPackage;
import other.*;
class Test{
public static void main(String[] args) {
String hello = "Hello", lo = "lo";
System.out.print((testPackage.Other.hello == hello) + " ");
System.out.print((other.Other.hello == hello) + " ");
System.out.print((hello == ("Hel"+"lo")) + " ");
System.out.print((hello == ("Hel"+lo)) + " ");
System.out.println(hello == ("Hel"+lo).intern());
}
}
class Other { static String hello = "Hello"; }
В результате мы получим:
- false true true false true
- false false true false true
- true true true true true
- true true true false true
- Все ответы неверны
Вопрос 6
Дана сигнатура метода:
public static <E extends CharSequence> List<? super E> doIt(List<E> nums)
Который вызывается как-то так:
result = doIt(in);
Какого типа должны быть result и in?
- ArrayList<String> in; List<CharSequence> result;
- List<String> in; List<Object> result;
- ArrayList<String> in; List result;
- List<CharSequence> in; List<CharSequence> result;
- ArrayList<Object> in; List<CharSequence> result;
Вопрос 7
public static void doIt(String String) { //1
int i = 10;
i : for (int k = 0 ; k< 10; k++) { //2
System.out.println( String + i); //3
if( k*k > 10) continue i; //4
}
}
Данный код:
- Не скомпилируется из-за строки 1
- Не скомпилируется из-за строки 2
- Не скомпилируется из-за строки 3
- Не скомпилируется из-за строки 4
- Скомпилируется и запустится без проблем
Вопрос 8
public class Main {
static void method(int... a) {
System.out.println("inside int...");
}
static void method(long a, long b) {
System.out.println("inside long");
}
static void method(Integer a, Integer b) {
System.out.println("inside INTEGER");
}
public static void main(String[] args) {
int a = 2;
int b = 3;
method(a,b);
}
}
В результате мы получим:
- Ошибку компиляции
- Ошибку времени выполнения
- «inside int...»
- «inside long»
- «inside INTEGER»
Вопрос 9
class Super { static String ID = "QBANK"; }
class Sub extends Super{
static { System.out.print("In Sub"); }
}
class Test{
public static void main(String[] args) {
System.out.println(Sub.ID);
}
}
В результате выполнения данного кода:
- Он даже не скомпилируется
- Результат зависит от реализации JVM
- Будет выведено «QBANK»
- Будет выведено «In Sub» и «QBANK»
- Все ответы неверны
Вопрос 10
Имеется два класса:
//in file A.java
package p1;
public class A{
protected int i = 10;
public int getI() { return i; }
}
//in file B.java
package p2;
import p1.*;
public class B extends A{
public void process(A a) {
a.i = a.i*2;
}
public static void main(String[] args) {
A a = new B();
B b = new B();
b.process(a);
System.out.println( a.getI() );
}
}
В результате выполнения класса В мы получим:
- Будет выведено «20»
- Будет выведено «10»
- Код не скомпилирутся
- Возникнет ошибка времени выполнения
- Все ответы неверны
Внимание — Ответы
Итак, сверим результаты:
- 4
- 2
- 2
- 4
- 4
- 3
- 5
- 4
- 3
- 3
Хочу поздравить тех, кто правильно ответил на большинство вопросов. Вы — круты.
Разбор полетов
Итак, начнем.
Вопрос 1
Имеется следующий код:
public class Overload{
public void method(Object o) {
System.out.println("Object");
}
public void method(java.io.FileNotFoundException f) {
System.out.println("FileNotFoundException");
}
public void method(java.io.IOException i) {
System.out.println("IOException");
}
public static void main(String args[]) {
Overload test = new Overload();
test.method(null);
}
}
Наверное, мой любимый вопрос. Большинство людей угадывает ответ с 5-го раза. FileNotFoundException — последнее, о чем думаешь глядя на этот код. Но на самом деле всё просто. Начнем с того, что в любой метод можно передать null. А дальше компилятор выбирает наиболее узкую версию метода из возможных. В данном случае FileNotFoundException это подкласс IOException, который, в свою очередь, подкласс Object. Поэтому компилятор выбирает именно данный метод.
Вот немного другая ситуация:
public class Overload{
public void method(Object o) {
System.out.println("Object");
}
public void method(String s) {
System.out.println("String");
}
public void method(StringBuffer sb) {
System.out.println("StringBuffer");
}
public static void main(String args[]) {
Overload test = new Overload();
test.method(null);
}
}
На первый взгляд код почти такой же, но он даже не скомпилируется, так как тут есть два метода на одном уровне иерархии, и компилятор не может выбрать наиболее специфичный из них.
Вопрос 2
Float f1 = new Float(Float.NaN);
Float f2 = new Float(Float.NaN);
System.out.println( ""+ (f1 == f2)+" "+f1.equals(f2)+ " "+(Float.NaN == Float.NaN) );
C первым выражением вроде всё ясно. Разные объекты, поэтому false. А вот дальше…
В Java NaN'ы несравнимы между собой. Но есть два исключения в работе класса Float:
- Если f1 и f2 оба представляют Float.NaN, тогда метод equals возвращает true, в то время как Float.NaN==Float.NaN принимает значение false.
- Если f1 содержит +0.0f в то время как f2 содержит -0.0f, метод equal возвращает false, в то время как 0.0f==-0.0f возвращает true.
Вопрос 3
class Mountain {
static String name = "Himalaya";
static Mountain getMountain() {
System.out.println("Getting Name ");
return null;
}
public static void main(String[ ] args) {
System.out.println( getMountain().name );
}
}
Тут всё просто: для всех static полей и методов компилятор заменяет название объекта на название класса, и во время выполнения уже не имеет никакого значения, была ссылка нулевой или нет.
То есть mountain.name меняется на Mountain.name.
Вообще рекомендуют при обращении к статик-членам класса всегда ссылаться на них именно через имя класса, а не объекта.
Вопрос 4
Integer a = 120;
Integer b = 120;
Integer c = 130;
Integer d = 130;
System.out.println(a==b);
System.out.println(c==d);
Многим данный вопрос знаком. Для остальных расскажу. Для более эффективного использования памяти, в джаве используются так называемые пулы. Есть строковый пул, Integer pool итд. Когда мы создаем объект не используя операцию new, объект помещается в пул, и в последствии, если мы захотим создать такой же объект (опять не используя new), новый объект создан не будет, а мы просто получим ссылку на наш объект из пула.
Особенность Integer-пула — он хранит только числа, которые помещаются в тип данных byte: от -128 до 127. Для остальных чисел пул не работает.
Вопрос 5
Таже тема, но сложнее:
//In File Other.java
package other;
public class Other { public static String hello = "Hello"; }
//In File Test.java
package testPackage;
import other.*;
class Test{
public static void main(String[] args) {
String hello = "Hello", lo = "lo";
System.out.print((testPackage.Other.hello == hello) + " ");
System.out.print((other.Other.hello == hello) + " ");
System.out.print((hello == ("Hel"+"lo")) + " ");
System.out.print((hello == ("Hel"+lo)) + " "); // runtime
System.out.println(hello == ("Hel"+lo).intern());
}
}
class Other { static String hello = "Hello"; }
Всё тот же строковый пул, но с нюансами.
Итак, 6 малоизвестных фактов о строках в Java:
1. Строковые литералы в одном классе представляют собой ссылки на один и тот же объект.
2. Строковые литералы в разных классах, но в одном пакете представляют собой ссылки на один и тот же объект.
3. Строковые литералы в разных классах и разных пакетах всё равно представляют собой ссылки на один и тот же объект )).
4. Строки, получающиеся сложением констант, вычисляются во время компиляции и далее смотри пункт первый.
5. Строки, создаваемые во время выполнения НЕ ссылаются на один и тот же объект. (поэтому четвертый вывод — false)
6. Метод intern в любом случае возвращает объект из пула, вне зависимости от того, когда создается строка, на этапе компиляции или выполнения. (Поэтому последний вывод — true).
Более подробно эта тема описана в пункте 3.10.5 спецификации Java.
Вопрос 6
Дана сигнатура метода:
public static <E extends CharSequence> List<? super E> doIt(List<E> nums)
Который вызывается как-то так:
result = doIt(in);
Какого типа должны быть result и in?
- ArrayList<String> in; List<CharSequence> result;
- List<String> in; List<Object> result;
- ArrayList<String> in; List result;
- List<CharSequence> in; List<CharSequence> result;
- ArrayList<Object> in; List<CharSequence> result;
Входной параметр был описан как List<E>, где E — это какой-то класс, наследующий CharSequence. Таким образом ArrayList<String>, List<String>, или List<CharSequence> — подходящие варианты для 'in'.
Данный метод возвращает List<? super E>, что значит, что это List, содержащий какой-то суперкласс E. Притом, E будет привязано к типу, который используется для 'in'. Например, если вы объявили ArrayList<String> in, E будет типа String.
Важный момент, который нужно понять, что невозможно заранее определить, что вернет метод. То есть вы не можете тут указать ни один из Java-классов, даже Object.
Единственный возможный вариант — использовать нетипизированный список, например: List result.
Данная тема достаточно сложна, и у меня есть мысли посвятить нюансам работы с коллекциями в Java отдельную статью. Там я постараюсь осветить эти моменты подробнее.
Вопрос 7
public static void doIt(String String) { //1
int i = 10;
i : for (int k = 0 ; k< 10; k++) { //2
System.out.println( String + i); //3
if( k*k > 10) continue i; //4
}
}
Этот код работает ).
Есть еще более забавный пример:
class X {
<X> X(X x) {
System.out.println(x);
}
}
Это тоже рабочий код, который печатает любой объект, который указать в конструкторе класса X.
Вернемся к нашей задаче. String это не ключевое слово, поэтому его можно использовать как идентификатор. Второй момент — джава хранит идентификаторы меток отдельно от всех остальных идентификаторов. Поэтому допускается использование одинаковых идентификаторов.
Вопрос 8
public class Main {
static void method(int... a) {
System.out.println("inside int...");
}
static void method(long a, long b) {
System.out.println("inside long");
}
static void method(Integer a, Integer b) {
System.out.println("inside INTEGER");
}
public static void main(String[] args) {
int a = 2;
int b = 3;
method(a,b);
}
}
В данном случае вызовется метод, принимающий тип long. Это сделано по той причине, что до 5й джавы не существовало ни методов с переменным количеством параметров, ни автоупаковки. Поэтому в старых версиях джавы вызвался бы метод long как единственный возможный. Поскольку нововведения не должны влиять на выполнение старого кода, вполне логично, что запускается метод long.
Если бы у нас было только два метода, «inside int...» и «inside INTEGER», запустился бы последний. Таким образом, приоритет автоупаковки в Java выше, чем приоритет vararg'ов.
Вопрос 9
class Super { static String ID = "QBANK"; }
class Sub extends Super{
static { System.out.print("In Sub"); }
}
class Test{
public static void main(String[] args) {
System.out.println(Sub.ID);
}
}
Статик-блоки запускаются во время инициализации класса. Весь вопрос в том, когда класс инициализируется, а это происходит в следующих случаях:
- Когда вызывается метод, непосредственно находящийся в данном классе (а не наследующийся от суперкласса)
- Когда вызывается конструктор данного класса, либо когда создается массив объектов данного класса
- Когда читается либо присваивается поле, которое не является константой
В данном случае, ни одно из этих трех условий не выполнялось, таким образом, класс Sub не инициализировался.
Вопрос 10
Имеется два класса:
//in file A.java
package p1;
public class A{
protected int i = 10;
public int getI() { return i; }
}
//in file B.java
package p2;
import p1.*;
public class B extends A{
public void process(A a) {
a.i = a.i*2;
}
public static void main(String[] args) {
A a = new B();
B b = new B();
b.process(a);
System.out.println( a.getI() );
}
}
Данный вопрос отражает важное свойство спецификатора доступа protected. Во всех книгах и статьях пишут, что члены с доступом protected видны в данном пакете и в подклассах других пакетов. Но мало кто уточняет, что за пределами пакета данные члены видны ТОЛЬКО ЧЕРЕЗ НАСЛЕДОВАНИЕ.
У самого класса B есть доступ к полю i, но он не может вызывать его на другом объекте, как это сделано в примере, что и приводит к ошибке компиляции.
Надеюсь вам понравились данные задачи и их решения. С скором времени я напишу продолжение данной статьи, где мы рассмотрим другие интересные аспекты языка JAVA.
Спасибо за внимание.