Як зробити метод повернення методу загальним?


588

Розглянемо цей приклад (типовий для книг OOP):

У мене є Animalклас, де кожен Animalможе мати багато друзів.
І підкласи подобаються Dog, Duck, і Mouseт.д. , які додають певну поведінку , якbark() , і quack()т.д.

Ось Animalклас:

public class Animal {
    private Map<String,Animal> friends = new HashMap<>();

    public void addFriend(String name, Animal animal){
        friends.put(name,animal);
    }

    public Animal callFriend(String name){
        return friends.get(name);
    }
}

А ось фрагмент коду з великою кількістю наборів текстів:

Mouse jerry = new Mouse();
jerry.addFriend("spike", new Dog());
jerry.addFriend("quacker", new Duck());

((Dog) jerry.callFriend("spike")).bark();
((Duck) jerry.callFriend("quacker")).quack();

Чи можу я використати дженерики для типу повернення, щоб позбутися набору даних, щоб я міг сказати

jerry.callFriend("spike").bark();
jerry.callFriend("quacker").quack();

Ось якийсь початковий код із типом повернення, переданий методу як параметр, який ніколи не використовується.

public<T extends Animal> T callFriend(String name, T unusedTypeObj){
    return (T)friends.get(name);        
}

Чи є спосіб визначити тип повернення під час виконання без використання додаткового параметра instanceof? Або, принаймні, передавши клас типу замість манекена.
Я розумію, що дженерики призначені для перевірки типу компіляції, але чи існує це рішення?

Відповіді:


902

Ви можете визначити callFriendтак:

public <T extends Animal> T callFriend(String name, Class<T> type) {
    return type.cast(friends.get(name));
}

Потім назвіть це як таке:

jerry.callFriend("spike", Dog.class).bark();
jerry.callFriend("quacker", Duck.class).quack();

Цей код має перевагу тим, що не створює жодних попереджень компілятора. Звичайно, це справді лише оновлена ​​версія кастингу з попередніх днів і не додає додаткової безпеки.


29
... але досі немає перевірки типу компіляції часу між параметрами виклику callFriend ().
Девід Шмітт

2
Це найкраща відповідь поки що - але ви повинні змінити addFriend таким же чином. Це ускладнює написання помилок, оскільки вам потрібен буквальний клас в обох місцях.
Крейг П. Мотлін

@Jaider, не зовсім те саме, але це буде працювати: // Animal Class public T CallFriend <T> (ім'я рядка), де T: Animal {повернути друзів [ім'я] як T; } // Виклик класу jerry.CallFriend <Dog> ("шип"). Bark (); jerry.CallFriend <Duck> ("шарлатан"). Quack ();
Нестор Ледон

124

Ні. Компілятор не може знати, який тип jerry.callFriend("spike")повертається. Крім того, ваша реалізація просто приховує роль в методі без додаткової безпеки типу. Врахуйте це:

jerry.addFriend("quaker", new Duck());
jerry.callFriend("quaker", /* unused */ new Dog()); // dies with illegal cast

У цьому конкретному випадку створення абстрактного talk()методу та його належне переосмислення в підкласах буде служити вам набагато краще:

Mouse jerry = new Mouse();
jerry.addFriend("spike", new Dog());
jerry.addFriend("quacker", new Duck());

jerry.callFriend("spike").talk();
jerry.callFriend("quacker").talk();

10
Хоча метод mmyers може працювати, я вважаю, що цей метод є кращим програмуванням OO і позбавить вас від проблем.
Джеймс Макмахон

1
Це правильний спосіб отримати той же результат. Зауважте, мета полягає в тому, щоб отримати похідну специфічну поведінку класу під час виконання без чіткого написання коду самостійно, щоб зробити некрасиві перевірки типу та кастинг. Запропонований @laz метод працює, але викидає тип безпеки у вікно. Цей метод вимагає меншої кількості рядків коду (оскільки реалізація методу запізніла і все одно переглядається під час виконання), але все ж давайте легко визначити унікальну поведінку для кожного підкласу Animal.
dcow

Але оригінальне запитання не стосується безпеки типу. Те, як я прочитав його, запитуючий просто хоче знати, чи є спосіб використовувати генеричні матеріали, щоб уникнути необхідності.
лаз

2
@laz: так, оригінальне запитання - як поставлене - не стосується безпеки типу. Це не змінює того факту, що існує безпечний спосіб здійснити це, усуваючи відмови класів у класі. Дивіться також weblogs.asp.net/alex_papadimoulis/archive/2005/05/25/…
Девід Шмітт

3
Я не погоджуюся з цим, але ми маємо справу з Java та всіма її дизайнерськими рішеннями. Я вважаю це питання просто спробою дізнатись, що можливо в Java generics, а не як xyproblem ( meta.stackexchange.com/questions/66377/what-is-the-xy-problem ), яку потрібно переробити. Як і будь-яка закономірність або підхід, бувають випадки, коли наданий я код підходить, і періоди, коли потрібно щось зовсім інше (наприклад, те, що ви запропонували у цій відповіді).
лаз

114

Ви можете реалізувати це так:

@SuppressWarnings("unchecked")
public <T extends Animal> T callFriend(String name) {
    return (T)friends.get(name);
}

(Так, це юридичний код; див. Java Generics: Загальний тип, визначений лише як тип повернення .)

Тип повернення буде виведений з абонента. Однак зверніть увагу на @SuppressWarningsпримітку: це говорить про те, що цей код не є безпечним для введення тексту . Ви повинні перевірити це самостійно, або ви могли потрапити ClassCastExceptionsпід час виконання.

На жаль, спосіб, яким ви користуєтесь ним (без присвоєння значення повернення тимчасовій змінній), єдиний спосіб зробити компілятор щасливим - назвати його так:

jerry.<Dog>callFriend("spike").bark();

Хоча це може бути трохи приємніше, ніж кастинг, вам, мабуть, краще дати Animalкласу абстрактний talk()метод, як сказав Девід Шмітт.


Метод ланцюжка насправді не був наміром. Я не проти присвоювати значення змінної підтипу і використовувати його. Дякую за рішення.
Сатіш

це чудово працює, коли ви робите ланцюжок викликів методів!
Хартмут П.

Мені дуже подобається цей синтаксис. Я думаю, що в C # це те, на jerry.CallFriend<Dog>(...що я думаю, виглядає краще.
andho

Цікаво, що власна java.util.Collections.emptyList()функція JRE реалізована саме так, і її javadoc рекламує себе як безпечний тип.
Ti Strga

31

Це запитання дуже схоже на пункт 29 в Ефективній Java - "Розгляньте тип безпечних різнорідних контейнерів". Відповідь Лаза є найближчим до рішення Блоха. Однак і put, і get повинні використовувати клас Literal для безпеки. Підписи стануть:

public <T extends Animal> void addFriend(String name, Class<T> type, T animal);
public <T extends Animal> T callFriend(String name, Class<T> type);

Всередині обох методів слід перевірити правильність параметрів. Додаткову інформацію див. У розділі Ефективна Java та клас javadoc.


17

Крім того, ви можете попросити метод повернути значення в заданому типі таким чином

<T> T methodName(Class<T> var);

Більше прикладів тут у документації на Oracle Java


17

Ось простіша версія:

public <T> T callFriend(String name) {
    return (T) friends.get(name); //Casting to T not needed in this case but its a good practice to do
}

Повністю працюючий код:

    public class Test {
        public static class Animal {
            private Map<String,Animal> friends = new HashMap<>();

            public void addFriend(String name, Animal animal){
                friends.put(name,animal);
            }

            public <T> T callFriend(String name){
                return (T) friends.get(name);
            }
        }

        public static class Dog extends Animal {

            public void bark() {
                System.out.println("i am dog");
            }
        }

        public static class Duck extends Animal {

            public void quack() {
                System.out.println("i am duck");
            }
        }

        public static void main(String [] args) {
            Animal animals = new Animal();
            animals.addFriend("dog", new Dog());
            animals.addFriend("duck", new Duck());

            Dog dog = animals.callFriend("dog");
            dog.bark();

            Duck duck = animals.callFriend("duck");
            duck.quack();

        }
    }

1
Що робить Casting to T not needed in this case but it's a good practice to do. Я маю на увазі, якщо за цим виконувати належним чином під час виконання, що означає "хороша практика"?
Фарид

Я мав на увазі сказати явний кастинг (T) не потрібно, оскільки декларація типу повернення <T> у декларації методу має бути достатньою
webjockey

9

Як ви сказали, що проходження уроку буде нормально, ви можете написати це:

public <T extends Animal> T callFriend(String name, Class<T> clazz) {
   return (T) friends.get(name);
}

А потім використовуйте його так:

jerry.callFriend("spike", Dog.class).bark();
jerry.callFriend("quacker", Duck.class).quack();

Не ідеально, але це майже все, наскільки ви отримуєте з дженерики Java. Існує спосіб реалізувати гетерогенні контейнери Typesafe (THC) за допомогою маркерів Super Type , але це знову має свої проблеми.


Мені дуже шкода, але це та сама відповідь, яку має Лаз, тому ви або копіюєте його, або він копіює вас.
Джеймс Макмахон

Це акуратний спосіб пройти Тип. Але все-таки це безпечний тип, як сказав Шмітт. Я все-таки міг би пройти інший клас і бомбардування набоїв. mmyers 2-а відповідь, щоб встановити тип Type in Return, здається кращим
Sathish

4
Немо, якщо ви перевірите час публікації, ви побачите, що ми розмістили їх майже точно в той же момент. Також вони точно не однакові, лише два рядки.
Fabian Steeg

@Fabian Я опублікував подібну відповідь, але важлива різниця між слайдами Bloch та тими, що були опубліковані в «Ефективній Java». Він використовує Class <T> замість TypeRef <T>. Але це все-таки чудова відповідь.
Крейг П. Мотлін

8

На основі тієї ж ідеї, що і маркерів Super Type, ви можете створити введений ідентифікатор, який слід використовувати замість рядка:

public abstract class TypedID<T extends Animal> {
  public final Type type;
  public final String id;

  protected TypedID(String id) {
    this.id = id;
    Type superclass = getClass().getGenericSuperclass();
    if (superclass instanceof Class) {
      throw new RuntimeException("Missing type parameter.");
    }
    this.type = ((ParameterizedType) superclass).getActualTypeArguments()[0];
  }
}

Але я думаю, що це може перемогти мету, оскільки тепер потрібно створити нові об’єкти id для кожного рядка і утримувати їх (або реконструювати їх з правильною інформацією про тип).

Mouse jerry = new Mouse();
TypedID<Dog> spike = new TypedID<Dog>("spike") {};
TypedID<Duck> quacker = new TypedID<Duck>("quacker") {};

jerry.addFriend(spike, new Dog());
jerry.addFriend(quacker, new Duck());

Але тепер ви можете використовувати клас так, як ви спочатку хотіли, без викидів.

jerry.callFriend(spike).bark();
jerry.callFriend(quacker).quack();

Це просто приховує параметр типу всередині ідентифікатора, хоча це означає, що ви можете отримати тип з ідентифікатора пізніше, якщо хочете.

Вам також потрібно буде застосувати методи порівняння та хешування для TypedID, якщо ви хочете мати можливість порівняти два однакові екземпляри ідентифікатора.


8

"Чи існує спосіб визначити тип повернення під час виконання без додаткового параметра, використовуючи instanceof?"

Як альтернативне рішення ви можете використовувати зразок відвідувача, як це. Зробіть тварину абстрактною та зробіть її реалізацією Привабливою:

abstract public class Animal implements Visitable {
  private Map<String,Animal> friends = new HashMap<String,Animal>();

  public void addFriend(String name, Animal animal){
      friends.put(name,animal);
  }

  public Animal callFriend(String name){
      return friends.get(name);
  }
}

Видимий лише означає, що реалізація Тварин готова прийняти відвідувача:

public interface Visitable {
    void accept(Visitor v);
}

А реалізація відвідувачів здатна відвідати всі підкласи тварини:

public interface Visitor {
    void visit(Dog d);
    void visit(Duck d);
    void visit(Mouse m);
}

Так, наприклад, реалізація Dog тоді виглядатиме так:

public class Dog extends Animal {
    public void bark() {}

    @Override
    public void accept(Visitor v) { v.visit(this); }
}

Хитрість тут полягає в тому, що, оскільки Собака знає, що це за тип, вона може запустити відповідний метод перевантаження відвідувача v, передавши "це" як параметр. Інші підкласи реалізують accept () точно так само.

Потім клас, який хоче викликати конкретні методи підкласу, повинен реалізувати інтерфейс Visitor таким чином:

public class Example implements Visitor {

    public void main() {
        Mouse jerry = new Mouse();
        jerry.addFriend("spike", new Dog());
        jerry.addFriend("quacker", new Duck());

        // Used to be: ((Dog) jerry.callFriend("spike")).bark();
        jerry.callFriend("spike").accept(this);

        // Used to be: ((Duck) jerry.callFriend("quacker")).quack();
        jerry.callFriend("quacker").accept(this);
    }

    // This would fire on callFriend("spike").accept(this)
    @Override
    public void visit(Dog d) { d.bark(); }

    // This would fire on callFriend("quacker").accept(this)
    @Override
    public void visit(Duck d) { d.quack(); }

    @Override
    public void visit(Mouse m) { m.squeak(); }
}

Я знаю, що це набагато більше інтерфейсів і методів, ніж ви торгувались, але це стандартний спосіб отримати обробку для кожного конкретного підтипу з точно нульовими примірниками чеків і нульових типів. І все це робиться стандартним мовою агностики, тому це не тільки для Java, але будь-яка мова OO повинна працювати однаково.


6

Неможливо. Як Карта повинна знати, який підклас Animal збирається отримати, враховуючи лише рядок String?

Єдине, як це було б можливо, це було б, якби кожна тварина прийняла лише одного типу друга (тоді це може бути параметр класу Animal) або метод callFriend () отримав параметр типу. Але це справді виглядає так, що вам не вистачає точки успадкування: це те, що ви можете обробляти підкласи рівномірно лише при використанні виключно методів надкласу.


5

Я написав статтю, яка містить доказ поняття, класи підтримки та тестовий клас, який демонструє, як маркери Super Type можуть бути отримані вашими класами під час виконання. У двох словах, це дозволяє делегувати альтернативні реалізації залежно від фактичних загальних параметрів, переданих абонентом. Приклад:

  • TimeSeries<Double> делегує приватному внутрішньому класу, який використовує double[]
  • TimeSeries<OHLC> делегує приватному внутрішньому класу, який використовує ArrayList<OHLC>

Див.: Використання TypeTokens для отримання загальних параметрів

Дякую

Річард Гомес - блог


Дійсно, дякую за те, що поділився своєю думкою, ваша стаття справді все це пояснює!
Yann-Gaël Guéhéneuc

3

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

public <T extends MobilePage> T tapSignInButton(Class<T> type) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {
    //signInButton.click();
    return type.getConstructor(AppiumDriver.class).newInstance(appiumDriver);
}
  • MobilePage - це суперклас, який тип поширюється, тобто ви можете використовувати будь-якого з його дітей (да)
  • type.getConstructor (Param.class тощо) дозволяє взаємодіяти з конструктором типу. Цей конструктор повинен бути однаковим між усіма очікуваними класами.
  • newInstance приймає заявлену змінну, яку потрібно передати новому конструктору об'єктів

Якщо ви не хочете викидати помилки, ви можете їх зрозуміти так:

public <T extends MobilePage> T tapSignInButton(Class<T> type) {
    // signInButton.click();
    T returnValue = null;
    try {
       returnValue = type.getConstructor(AppiumDriver.class).newInstance(appiumDriver);
    } catch (Exception e) {
        e.printStackTrace();
    }
    return returnValue;
}

Наскільки я розумію, це найкращий і найелегантніший спосіб використання Generics.
cbaldan

2

Не дуже, тому що, як ви кажете, компілятор знає лише, що callFriend () повертає Тварину, а не Собаку чи Качку.

Ви не можете додати абстрактний метод makeNoise () до Animal, який би був реалізований як кора чи шарлатан підкласами?


1
що робити, якщо тварини мають кілька методів, які навіть не підпадають під загальну дію, яку можна абстрагувати? Мені це потрібно для спілкування між підкласами з різними діями, де добре з передачею Type, а не екземпляром.
Сатіш

2
Ви дійсно щойно відповіли на власне запитання - якщо тварина має унікальну дію, то вам доведеться звернутися до цієї конкретної тварини. Якщо тварина має дію, яку можна згрупувати з іншими тваринами, ви можете визначити абстрактний чи віртуальний метод у базовому класі та скористатися цим.
Метт Джордан

2

Що ви шукаєте тут - це абстракція. Код проти інтерфейсів більше, і вам доведеться робити менше кастингу.

Приклад нижче наведений у C #, але концепція залишається такою ж.

using System;
using System.Collections.Generic;
using System.Reflection;

namespace GenericsTest
{
class MainClass
{
    public static void Main (string[] args)
    {
        _HasFriends jerry = new Mouse();
        jerry.AddFriend("spike", new Dog());
        jerry.AddFriend("quacker", new Duck());

        jerry.CallFriend<_Animal>("spike").Speak();
        jerry.CallFriend<_Animal>("quacker").Speak();
    }
}

interface _HasFriends
{
    void AddFriend(string name, _Animal animal);

    T CallFriend<T>(string name) where T : _Animal;
}

interface _Animal
{
    void Speak();
}

abstract class AnimalBase : _Animal, _HasFriends
{
    private Dictionary<string, _Animal> friends = new Dictionary<string, _Animal>();


    public abstract void Speak();

    public void AddFriend(string name, _Animal animal)
    {
        friends.Add(name, animal);
    }   

    public T CallFriend<T>(string name) where T : _Animal
    {
        return (T) friends[name];
    }
}

class Mouse : AnimalBase
{
    public override void Speak() { Squeek(); }

    private void Squeek()
    {
        Console.WriteLine ("Squeek! Squeek!");
    }
}

class Dog : AnimalBase
{
    public override void Speak() { Bark(); }

    private void Bark()
    {
        Console.WriteLine ("Woof!");
    }
}

class Duck : AnimalBase
{
    public override void Speak() { Quack(); }

    private void Quack()
    {
        Console.WriteLine ("Quack! Quack!");
    }
}
}

Це питання стосується кодування, а не поняття.

2

Я зробив наступне у своєму контракті на ліб:

public class Actor<SELF extends Actor> {
    public SELF self() { return (SELF)_self; }
}

підкласифікація:

public class MyHttpAppSession extends Actor<MyHttpAppSession> {
   ...
}

принаймні, це працює всередині поточного класу та, коли має чітку набрану посилання. Багаторазове успадкування працює, але виходить справді складним :)


1

Я знаю, що це зовсім інша річ, яку запитував той. Ще один спосіб вирішення цього питання - це рефлексія. Я маю на увазі, що це не приносить користі від Generics, але це дозволяє вам певним чином наслідувати поведінку, яку ви хочете виконати (зробити собачий гавкіт, зробити качка квак тощо), не піклуючись про кастинг типів:

import java.lang.reflect.InvocationTargetException;
import java.util.HashMap;
import java.util.Map;

abstract class AnimalExample {
    private Map<String,Class<?>> friends = new HashMap<String,Class<?>>();
    private Map<String,Object> theFriends = new HashMap<String,Object>();

    public void addFriend(String name, Object friend){
        friends.put(name,friend.getClass());
        theFriends.put(name, friend);
    }

    public void makeMyFriendSpeak(String name){
        try {
            friends.get(name).getMethod("speak").invoke(theFriends.get(name));
        } catch (IllegalArgumentException e) {
            e.printStackTrace();
        } catch (SecurityException e) {
            e.printStackTrace();
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        } catch (InvocationTargetException e) {
            e.printStackTrace();
        } catch (NoSuchMethodException e) {
            e.printStackTrace();
        }
    } 

    public abstract void speak ();
};

class Dog extends Animal {
    public void speak () {
        System.out.println("woof!");
    }
}

class Duck extends Animal {
    public void speak () {
        System.out.println("quack!");
    }
}

class Cat extends Animal {
    public void speak () {
        System.out.println("miauu!");
    }
}

public class AnimalExample {

    public static void main (String [] args) {

        Cat felix = new Cat ();
        felix.addFriend("Spike", new Dog());
        felix.addFriend("Donald", new Duck());
        felix.makeMyFriendSpeak("Spike");
        felix.makeMyFriendSpeak("Donald");

    }

}

1

а як на рахунок

public class Animal {
private Map<String,<T extends Animal>> friends = new HashMap<String,<T extends Animal>>();

public <T extends Animal> void addFriend(String name, T animal){
    friends.put(name,animal);
}

public <T extends Animal> T callFriend(String name){
    return friends.get(name);
}

}


0

Існує інший підхід, ви можете звузити тип повернення, коли ви перекриєте метод. У кожному підкласі вам потрібно буде замінити callFriend, щоб повернути цей підклас. Вартість складе кілька декларацій callFriend, але ви можете виділити загальні частини до методу, який називається внутрішньо. Мені це здається набагато простішим, ніж рішення, зазначені вище, і не потрібен додатковий аргумент для визначення типу повернення.


Я не впевнений, що ви маєте на увазі під «звуженням типу повернення». Afaik, Java та більшість набраних мов не перевантажують методи та функції, засновані на типі повернення. Наприклад, public int getValue(String name){}це не відрізняється з public boolean getValue(String name){}точки зору компілятора. Вам потрібно буде або змінити тип параметра, або додати / видалити параметри, щоб перевантаження було розпізнано. Можливо, я просто нерозумію тебе ...
Єдиний справжній Колтер

у java ви можете замінити метод у підкласі та вказати більш "вузький" (тобто більш конкретний) тип повернення. Дивіться stackoverflow.com/questions/14694852/… .
FeralWhippet

-3
public <X,Y> X nextRow(Y cursor) {
    return (X) getRow(cursor);
}

private <T> Person getRow(T cursor) {
    Cursor c = (Cursor) cursor;
    Person s = null;
    if (!c.moveToNext()) {
        c.close();
    } else {
        String id = c.getString(c.getColumnIndex("id"));
        String name = c.getString(c.getColumnIndex("name"));
        s = new Person();
        s.setId(id);
        s.setName(name);
    }
    return s;
}

Ви можете повернути будь-який тип і отримати безпосередньо подібне. Не потрібно друкувати.

Person p = nextRow(cursor); // cursor is real database cursor.

Це найкраще, якщо ви хочете налаштувати будь-який інший тип записів замість реальних курсорів.


2
Ви кажете, що "не потрібно набирати текст", але, очевидно, є задіяний набір даних: (Cursor) cursorнаприклад.
Самі Лайн

Це абсолютно невідповідне використання дженериків. Цей код cursorповинен бути a Cursor, і завжди повертатиме Person(або null). Використання дженерики знімає перевірку цих обмежень, роблячи код небезпечним.
Енді Тернер
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.