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

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

package online.javanese.basics.oop.tuples;
// воспользуемся кругом и точкой из урока про интерфейсы
import online.javanese.basics.oop.interfaces.Circle;
import online.javanese.basics.oop.interfaces.Point;
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 + ")";
    }
}

Воспользуемся:

package online.javanese.basics.oop.tuples;
import online.javanese.basics.oop.interfaces.Circle;
import online.javanese.basics.oop.interfaces.Point;
import java.util.Random;
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 (кортеж) и сделаем его универсальным, чтобы его же можно было использовать в похожих ситуациях:

package online.javanese.basics.oop.tuples;
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 + ")";
    }
}

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

package online.javanese.basics.oop.tuples;
import online.javanese.basics.oop.interfaces.Circle;
import online.javanese.basics.oop.interfaces.Point;
import java.util.Random;
public class TupleExample {
    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(). Уже только после этого можно вызвать метод, объявленный в классе Point: ((Point) mission.getSecond()).getX().

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

java.lang.ClassCastException: online.javanese.basics.oop.interfaces.Circle cannot be cast to online.javanese.basics.oop.interfaces.Point

Generics

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

package online.javanese.basics.oop.tuples;
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 — это так называемые параметры типа. Каждый из них может быть любым классом в каждом отдельном случае. Воспользуемся этим классом:

package online.javanese.basics.oop.tuples;
import online.javanese.basics.oop.interfaces.Circle;
import online.javanese.basics.oop.interfaces.Point;
import java.util.Random;
public class GenericTupleExample {
    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();
        // <Circle, Point> на месте <> писать не обязательно,
        // компилятор подставит их автоматически,
        // исходя из типа, возвращаемого методом
        return new GenericTuple<>(
                new Circle(random.nextDouble(),
                        random.nextDouble(),
                        random.nextDouble()),
                new Point(random.nextDouble(),
                        random.nextDouble()));
    }
}

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

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

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