Javanese Online

Пишем кортеж. Generics

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

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

public final class ParachuteMission {

    private final Circle target;
    private final Point droppingPoint;

    public ParachuteMission(Circle target, Point droppingPoint) {
        this.target = target;
        this.droppingPoint = droppingPoint;
    }

    public Circle getTarget() {
        return target;
    }

    public Point getDroppingPoint() {
        return droppingPoint;
    }

    @Override
    public String toString() {
        return "ParachuteMission(drop at " + droppingPoint + ", land to " + target + ")";
    }

}

Можно воспользовать этим классом так:

public class ParachuteMissionExample {

    public static void main(String[] args) {
    ParachuteMission mission = generateRandomMission();
    System.out.println(
            "Dropping point is at (" +
                    mission.getDroppingPoint().getX() + "; " +
                    mission.getDroppingPoint().getY() + ")");
    System.out.println("Must land in " + mission.getTarget());
    }

    private static ParachuteMission generateRandomMission() {
        Random random = new Random();
        return new ParachuteMission(
                new Circle(random.nextDouble(),
                        random.nextDouble(),
                        random.nextDouble()),
                new Point(random.nextDouble(),
                        random.nextDouble()));
    }

}
Dropping point is at (0.1559734552983515; 0.8449202669900885)
Must land in Circle(at (0.22848368817342946; 0.5060404193327808), r=0.14557853694639322)

Когда подобная задача возникает довольно часто, не всегда удобно писать новый класс для того, чтобы вернуть пару значений из метода. Переименуем ParachuteMission в Tuple (кортеж) и сделаем его универсальным, чтобы его же можно было использовать в похожих ситуациях, но с объектами любого типа:

public final class Tuple {

    private final Object first;
    private final Object second;

    public Tuple(Object first, Object second) {
        this.first = first;
        this.second = second;
    }

    public Object getFirst() {
        return first;
    }

    public Object getSecond() {
        return second;
    }

    @Override
    public String toString() {
        return "Tuple(first=" + first + ", second=" + second + ")";
    }
}

Попробуем воспользоваться:

public static void main(String[] args) {
    Tuple mission = generateRandomMission();
    System.out.println(
            "Dropping point is at (" +
                    ((Point) mission.getSecond()).getX() + "; " +
                    ((Point) mission.getSecond()).getY() + ")");
    System.out.println("Must land in " + mission.getFirst());
}
private static Tuple generateRandomMission() {
    Random random = new Random();
    return new Tuple(
            new Circle(random.nextDouble(),
                    random.nextDouble(),
                    random.nextDouble()),
            new Point(random.nextDouble(),
                    random.nextDouble()));
}

mission.getSecond() возвращает Object, и нам приходится явно приводить тип к Point: (Point) mission.getSecond(). Уже только после этого можно вызвать метод getX(), объявленный в классе Point.

Кроме того, что это создаёт визуальный мусор, высока вероятность ошибиться, например, вместо значения свойства second привести к типу Point значение свойства first: (Point) mission.getFirst(), что приведёт к ошибке во время выполнения:

java.lang.ClassCastException: Circle cannot be cast to Point

Generics

Другое название: обобщения. Шаблоны (templates) из C++ выполняют похожую задачу, но работают по-другому.

Для того чтобы позволить классу безопасно работать с объектами произвольного типа, разработали (позаимствовали) принцип Generics, который является реализацией так называемого параметрического полиморфизма.

public final class GenericTuple<F, S> {
    private final F first;
    private final S second;
    public GenericTuple(F first, S second) {
        this.first = first;
        this.second = second;
    }

    public F getFirst() {
        return first;
    }

    public S getSecond() {
        return second;
    }

    @Override
    public String toString() {
        return "Tuple(first=" + first + ", second=" + second + ")";
    }

}

F и S — это так называемые параметры типа. Каждый из них может быть любым ссылочным типом в каждом отдельном случае. Воспользуемся этим классом:

public static void main(String[] args) {
    GenericTuple<Circle, Point> mission = generateRandomMission();
    System.out.println(
            "Dropping point is at (" +
                    mission.getSecond().getX() + "; " +
                    mission.getSecond().getY() + ")");
    System.out.println("Must land in " + mission.getFirst());
}
private static GenericTuple<Circle, Point> generateRandomMission() {
    Random random = new Random();
    return new GenericTuple<>(
            new Circle(random.nextDouble(),
                    random.nextDouble(),
                    random.nextDouble()),
            new Point(random.nextDouble(),
                    random.nextDouble()));
    // <> называется 'diamond mark'.
    // <Circle, Point> на его месте писать не обязательно,
    // компилятор подставит типы автоматически,
    // исходя из типа возвращаемого методом значения.
    // Это называется выведением (выводом) типа,
    // type inference.
}

Приведения типа исчезают. Ошибочный код (Point) mission.getFirst() теперь не скомпилируется, т. к. компилятору известно, что mission.first в данном случае — Circle, а не Point.

Комментарии к уроку

Сообщить об ошибке

Javanese.Online в GitHub

Чаты и каналы в Telegram

RSS-лента