【问题标题】:How to create a generic class that has a generic class as type?如何创建具有泛型类作为类型的泛型类?
【发布时间】:2015-07-08 14:57:51
【问题描述】:

在我的项目中,我有一个模型抽象类,它定义了一个 getId() 方法和一个扩展模型的不同类。 为了使每个类能够根据需要定义 ID 字段的类型,我将 Model 类设为通用:

public abstract class Model<T> {
    public abstract T getId();
}

所以我现在可以扩展它:

public class Person extends Model<String> {
    @Override
    public String getId() {
        return getName();
    }
}

没关系。

现在我想创建一个作为变更集的类,我可以在其中管理更新和删除的模型。在这个类中,我想使用映射来管理更新的模型,模型 id 是键,模型本身是值。 (变更集只有一种类型,例如 ChangeSet 将仅包含人,ChangeSet 将仅包含汽车等)。

我想像这样实例化它:

ChangeSet<Person> changeSet = new ChangeSet<Person>();

对我来说,可以推断出 ID 将是一个字符串,因为 Person 扩展了 Model。

但这似乎是不可能的。我能得到的最接近的是这两种选择:

public class ChangeSet<M extends Model, T>{
    private final Map<T, M> updated = new HashMap<>();
...
}

所以我可以将更新的模型存储在一组中。但是 Eclipse 正在考虑我应该指定模型的泛型类型,创建这样的签名:

public class ChangeSet<M extends Model<T>, T>{
    private final Map<T, M> updated = new HashMap<>();
...
}

Eclipse 没问题,但现在我需要这段代码来实例化一个对象。

ChangeSet<String, Person> changeSet = new ChangeSet<Person<String>, String>();

它不是那么优雅,重复的“字符串”很尴尬。

有什么方法可以(接近)我想要的声明?

【问题讨论】:

  • 你用的是什么版本的JDK?
  • 对于 1.7,我认为这是我们能得到的最接近的版本。在 Java 8 中,您将能够将其写为 ChangeSet&lt;Person,String&gt; changeSet = new ChangeSet&lt;Person, String&gt;(); 而不会重复。可以使用菱形运算符进一步写为ChangeSet&lt;Person,String&gt; changeSet = new ChangeSet&lt;&gt;();。我认为这可以通过类型推断来实现 - docs.oracle.com/javase/tutorial/java/generics/…

标签: java generics


【解决方案1】:

接口使用和泛型的良好组合肯定会产生一个简单且可扩展的更简洁的实现。

这是我的建议 - 我实际上实现了这个并且效果很好:

基于 OP 的 cmets 进行更新

模型接口可以被“生成”以限制返回类型:

package org.example;

/**
 * Created by prahaladd on 08/07/15.
 */
public interface Model<T extends  Identifier>
{
    T getIdentifier();
}

实现使用特定类型标识符的模型类:

package org.example;



/**
 * Created by prahaladd on 08/07/15.
 */
public class Person implements Model<StringIdentifier>
{

    private final String name;
    private final String id;

    public Person(String id, String name)
    {
        this.id = id;
        this.name = name;
    }
    @Override
    public StringIdentifier getIdentifier()
    {
        return new StringIdentifier(id);
    }

    public String getName()
    {
        return  name;
    }
}

ChangeSet 实现现在进行了一些更改以模仿 Map 接口,如下所示。事实上,它现在接受将作为键存储的标识符的类型:

package org.example;

import java.util.Map;

/**
 * Created by prahaladd on 08/07/15.
 */
public class ChangeSet<T extends Identifier, M extends  Model<T>>
{
    //Refer to PECS - http://stackoverflow.com/questions/2723397/java-generics-what-is-pecs

    private Map<? super Identifier, M> changeMap;

    public void addChange(M element)
    {
        changeMap.put(element.getIdentifier(),element);
    }

    public M getChangedElementForId(T id)
    {
        return changeMap.get(id);
    }
}

所有这些更改都还不错 - 您可以很容易地实例化 ChangeSet,如下所示:

package org.example;

public class Main {

    public static void main(String[] args)
    {
        Person p1 = new Person("1", "Tom");
        Person p2 = new Person("2", "Jerry");

         //change set is instantiated without any redundant generic parameters
        ChangeSet<StringIdentifier, Person> changes = new ChangeSet<StringIdentifier,Person>();

        //assume that there were some changes and you want to add them to the changeset.
        changes.addChange(p1);
        changes.addChange(p2);

        //retrieve element from the changeset for an id

        p1= changes.getChangedElementForId(new StringIdentifier("1"));
        p2 = changes.getChangedElementForId(new StringIdentifier("2"));


    }
}

替代解决方案

首先 - 定义一个接口来封装您的 ID。这不是矫枉过正。假设您有不同类型的 ID,使用接口来定义标识符的合同将大大有助于使您的代码干净和可扩展:

package org.example;

/**
 * Created by prahaladd on 08/07/15.
 */
public interface Identifier<T>
{
    T getIdentifier();
}

现在您已经定义了一个 Identifier 接口,您可以根据您的各种 ID 类型为其定义不同的实现。例如下面我提供了 StringIdentifier 的实现,它生成字符串类型的 ID:

package org.example;

/**
 * Created by prahaladd on 08/07/15.
 */
public class StringIdentifier implements Identifier<String>
{

    private final String identifier;

    public StringIdentifier(String id)
    {
        identifier = id;
    }

    @Override
    public String getIdentifier()
    {
        return "someId";
    }
}

现在定义模型接口。理想情况下,模型接口不应该处理任何 ID 类型,它应该只知道它必须返回一个标识符(就像您的用例一样)。

package org.example;

/**
 * Created by prahaladd on 08/07/15.
 */
public interface Model
{
    Identifier getIdentifier();
}

现在提供模型接口的实现。例如以下是您的查询中提到的 Person 类:

包 org.example;

/**
 * Created by prahaladd on 08/07/15.
 */
public class Person implements Model
{

    private final String name;
    private final String id;

    public Person(String id, String name)
    {
        this.id = id;
        this.name = name;
    }
    @Override
    public Identifier getIdentifier()
    {
        return new StringIdentifier(id);
    }

    public String getName()
    {
        return  name;
    }
}

现在定义变更集。 ChangeSet 应该只知道它存储了 ID 对象和对应的 Model 之间的映射。它并不真正了解 ID 对象的类型。这使得 ChangeSet 类非常灵活,甚至可以支持除了您想要的同质集合之外的异构集合。

package org.example;

import java.util.Map;

/**
 * Created by prahaladd on 08/07/15.
 */
public class ChangeSet<M extends  Model>
{
    //Refer to PECS - http://stackoverflow.com/questions/2723397/java-generics-what-is-pecs

    private Map<? super Identifier, M> changeMap;

    private Class identifierType;

    public void addChange(M element)
    {
        //prahaladd - update :  save the identifier type for a later check.
        if(identifierType != null)
        {
             identifierType = element.getIdentifier.getClass();
        } 
        changeMap.put(element.getIdentifier(),element);
    }

    public M getChangedElementForId(Identifier id)
    {
        //prahaladd updated - verify that the type of the passed in id
        //is the same as that of the changeset identifier type.
        if(!id.getClass().equals(identifierType))
        {
                 throw new IllegalArgumentException();
        }
        return changeMap.get(id);
    }
}

现在,努力得到了回报。看看下面的客户端实现:

package org.example;

public class Main {

    public static void main(String[] args)
    {
        Person p1 = new Person("1", "Tom");
        Person p2 = new Person("2", "Jerry");

        ChangeSet<Person> changes = new ChangeSet<Person>();

        //assume that there were some changes and you want to add them to the changeset.
        changes.addChange(p1);
        changes.addChange(p2);

        //retrieve element from the changeset for an id

        p1= changes.getChangedElementForId(new StringIdentifier("1"));
        p2 = changes.getChangedElementForId(new StringIdentifier("2"));
    }
}

和你想象的一样!!如您所见,这里没有什么花哨的东西。简单的面向对象的概念以及接口和泛型的深思熟虑的组合。

希望这会有所帮助!

【讨论】:

  • 嗨 Prahalad,非常周到的回应和很好的解释。但是你的班级模型是有代价的。它为 id 引入了一个包装类 Identifier 和一个附加的实现类(StringIdentifier)。要调用 ChangeSet#getChangedElementForId(),您需要一个临时 Identifier 对象。而且它不是类型安全的:给定一个名为 ChangeSet 类型的变量,您可以调用 changes.getChangedElementForId(new IntIdentifier(1)) 这当然没有意义(总是返回 null),但也没有被编译器(根据问题作者的要求)。
  • @Prahalad 这是一个非常好的解决方案,您对我们提出的关于“按 id 查找”的问题有什么建议吗?
  • @JSBach 我正在为我的解决方案提供更新,并将保留原始答案以供备用参考。为了解决 wero 的评论 - 一个不太好的方法是在 getChangedElementForId 的 ChangeSet 类中引入某种类型的验证,检查传入的标识符是否与地图期望的标识符类型相同(更新我以前的解决方案有评论)。变更集可以在不真正了解底层类型的情况下执行此操作。但是,更新后的解决方案自然地处理了这个问题,我更倾向于它
【解决方案2】:

如果更新后的地图是 ChangeSet 的一个实现细节,你可以简单地写:

public class ChangeSet<M extends Model<?>> {
    private final Map<Object,M> updated = new HashMap<>();

    public void add(M model) {
        updates.put(model.getId(), model);
    }
}

但如果 ChangeSet 应该有依赖于模型的 id 类型 T 的方法,这不是一个好的解决方案。

【讨论】:

  • 这正是问题所在:我有一个 addModel 方法来检查模型是否已经存在,如果存在则“合并”两者。这就是首先引发问题的原因:(
  • 你的 addModel-method 的参数是什么?
  • 抱歉耽搁了... public void addModel(final M model) { if (!map.contains(model.getId())) { ..... }
猜你喜欢
  • 1970-01-01
  • 2016-05-05
  • 2020-08-26
  • 1970-01-01
  • 2022-10-15
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多