【问题标题】:How do I make a generic List which can accept two different, unrelated types?如何制作一个可以接受两种不同的、不相关的类型的通用列表?
【发布时间】:2009-08-29 12:33:44
【问题描述】:

我必须定义一个列表,它有两种可能的值

  1. 字符串
  2. 一些用户定义的类

如何创建一个类型安全的列表,因为它只接受这两种类型?

我想避免使用原始列表。

【问题讨论】:

  • “类型安全”是什么意思?没有 ClastCastExceptions?
  • @Stephen 的意思是我不想使用 RAW LIST,我想定义一个列表,它只能接受两种类型的对象(以上定义)除了这两种类型之外的任何添加尝试都应该提示我“编译时间错误”
  • 那么您将需要创建自己的类似列表的类。它不会是 List,因为你想违反 List 的约定。

标签: java generics list


【解决方案1】:

我不会声称这是一个完美的解决方案,但我会建议您使用“持有人”类 - 也被一些作家称为“标记类”(包括 Joshua Bloch,他说标记类是“冗长、容易出错且效率低下”)。

但是,鉴于您的情况,我找不到更好的方法。以下解决方案提供:

  • 插入没有可能共同祖先的类型时的编译时类型安全
  • 无需实现和测试任何其他代码即可将当前列表替换为其他集合的能力
  • 在其他上下文中使用持有者类的能力

你可以这样定义你的持有者类:

类持有人{ 私人字符串 foo; 私人用户类栏; 布尔 isString; 布尔初始化=假; 持有人(字符串 str){ foo = str;是字符串=真; } Holder (UserClass bar) { this.bar = bar;是字符串=假; } 字符串 getStringVal () { if (! initialized) throw new IllegalStateException ("not initialized yet"); if (!isString) throw new IllegalStateException ("contents not string"); 返回富; } // 使用与 getUserClassVal() 类似的方法 ... }

另一种选择是使用enum 作为标签,而不是布尔值 isString - 这具有易于扩展到其他类型的价值。

那么你当然会有你的化合物列表:

列表 samsList = new ArrayList()

插入很简单,并且根据您的要求,编译时类型安全:

samsList.add (new Holder(stringVal)); samsList.add (new Holder(userClassVal));

从列表中检索值稍微复杂一些:在决定使用哪个 getter 之前,您必须检查标签 (holder.isString())。例如,列表上的 foreach 迭代如下所示:

for(持有人:samsList){ 如果(持有人.isString()) doYourStringProcessing (holder.getStringVal()); 别的 doYourUserClassProcessing (holder.getUserClassVal()); }

就像我说的,我并不是说这是完美的,但它满足您的要求,将满足您的需求并最大程度地减少调用者的负担。

然而,我想指出,我觉得这可能是考虑在某处进行重构/重新设计的原因。我遵循的指导方针之一是,每当我发现自己证明了合理练习的例外情况时,值得更多思考,而不仅仅是“我该怎么做?”。

原因如下:假设我是对的,在这种情况下例外是合理的,那么实际上只有两种可能性。一是健全的实践是不完整的(所以“Prefer X over Y”应该改写为“Prefer X over Y except in case Z”)。

但更有可能的是,底层子句的设计并不完美,我们应该认真考虑进行一些重新设计/重构。

【讨论】:

  • 我认为 Bloch 的第 20 项与实际上与层次相关的类有关,而不是与这里面临的问题有关(即您的免责声明对您的解决方案过于苛刻)。在我看来,您的解决方案比公认的答案更好。它具有在使用集合时返回类型化结果的优点(getter,迭代......)
【解决方案2】:

由于StringObject 的直接子类并且是最终的,因此在String 和Object 之外的用户定义类之间找不到公共超类型。所以List<Object>是你必须使用的。

从设计的角度来看,在集合中混合不相关的类是一个坏主意。想想你想要完成什么,你可能会想出更好的方法。

【讨论】:

  • @kdgregorythanx 回复..但是有没有办法将对象的类型限制为 1)字符串 2)用户定义的类
  • 不,你想要完成的事情是不可能的,而且有充分的理由。想象一下,您可以这样做,您会从列表中检索到什么样的参考资料?除此之外,你的类和 String 有什么样的通用接口?为什么不尝试创建另一个类,该类在后台存储 String 或您的类,并仅公开它们共同的和与您的列表相关的行为,并让您的列表仅存储该类的实例?
  • 虽然我同意您的观点,即设计可能需要重新考虑,但我不同意异构容器是一个“坏主意”。请参阅 Effective Java Item 29。我认为 OP 只是有点不清楚他的用例是什么……但异构容器并不总是一个坏主意。
  • 我为否决您的答案而道歉...实际上我不是故意的,直到您发布我的答案才意识到...现在我不允许更改它。如果您编辑答案,我可以撤消它。我只是想指出我不同意“在集合中混合不相关的类是一个坏主意”的笼统说法。大多数时候,你可能是对的,但有时它是有道理的。这就是我想要表达的全部内容。
【解决方案3】:

我建议使用类似“任何一种”类型的东西。任何一个都可以保存一种类型或另一种类型的值,但不能同时保存两者。然后,您的列表具有两者之一的类型。这是一个更明确的解决方案。

这里是用法:

final List<Either<Integer, String>> list = new ArrayList<>();

list.add(Either.left(1234));
list.add(Either.right("hello"));

for (final Either<Integer, String> i : list) {
    if (i.isLeft()) {
        System.out.println(i.left());
    } else {
        System.out.println(i.right());
    }
}

代码如下:

package com.stackoverflow.q1351335;

public interface Either<L, R> {

    boolean isLeft();

    L left();

    R right();

    static <L, R> Either<L, R> left(final L value) {
        return new EitherLeft<>(value);
    }

    static <L, R> Either<L, R> right(final R value) {
        return new EitherRight<>(value);
    }
}

package com.stackoverflow.q1351335;

import java.util.Objects;

public final class EitherLeft<L, R> implements Either<L, R> {
    private final L value;
    public boolean isLeft() {
        return true;
    }
    public L left() {
        return value;
    }
    public R right() {
        throw new IllegalStateException("Cannot get right value from an EitherLeft");
    }
    public EitherLeft(final L value) {
        super();
        this.value = value;
    }
    @Override
    public boolean equals(final Object obj) {
        if (obj instanceof EitherLeft) {
            Objects.equals(this.value, ((EitherLeft) obj).value);
        }
        return false;
    }
    @Override
    public int hashCode() {
        return Objects.hashCode(value);
    }
    @Override
    public String toString() {
        return "EitherLeft[" + Objects.toString(value) + "]";
    }
}

package com.stackoverflow.q1351335;

import java.util.Objects;

public final class EitherRight<L, R> implements Either<L, R> {
    private final R value;
    public boolean isLeft() {
        return false;
    }
    public L left() {
        throw new IllegalStateException("Cannot get left value from an EitherRight");
    }
    public R right() { return value; }
    public EitherRight(final R value) {
        super();
        this.value = value;
    }
    @Override
    public boolean equals(final Object obj) {
        if (obj instanceof EitherRight) {
            Objects.equals(this.value, ((EitherRight) obj).value);
        }
        return false;
    }
    @Override
    public int hashCode() {
        return Objects.hashCode(value);
    }
    @Override
    public String toString() {
        return "EitherRight[" + Objects.toString(value) + "]";
    }
}

更新

对这段代码的一个很好的改进是向Either添加一个“匹配”函数:

// Illustration of usage: 
myEither.match(
    leftValue -> {
        // Do stuff with left
    }, 
    rightValue -> {
        // Do stuff with right
    });

这种方法的优点是不可能意外访问EitherRight 的左侧或EitherLeft 的右侧。这是对更多功能(如在 FP)语言中使用的方法的模仿。

【讨论】:

    【解决方案4】:

    我想定义一个列表,它只能接受两种类型(以上定义)的对象,除了这两种类型之外的任何添加尝试都应该提示我“编译时间错误”

    这可以满足您的要求(出现运行时错误):

    public class MyList extends ArrayList<Object> {
    
        public MyList() {
            super();
        }
    
        public MyList(int initialSize) {
            super(initialSize);
        }
    
        @Override
        public void add(Object obj) {
            if ((obj instanceof String) || (obj instanceof SomeType)) {
                add(obj);
            } else {
                throw new IllegalArgumentException("not a String or SomeType");
            }
        }
    
        public void add(String s) {
            super.add(s);
        }
    
        public void add(SomeType s) {
            super.add(s);
        }
    }
    

    没有办法在 Java 中实现这一点,这会给您一个编译时错误,您将错误类型的元素(在您的意义上)添加到 List。但是,如果这不是List,您可以定义该类以重载add 方法等。在这里创建新的“加法器”方法对您没有帮助,因为现有的add(T) 方法仍将存在于接口中。无论您做什么(在 Java 中),调用它都不会是编译时错误。

    【讨论】:

    • 请注意,使用 instanceof 将允许添加 SomeType 的任何子类。你真的想检查 obj.getClass() == SomeType.class。我发帖之前没有看到你回答,但我们的想法有些相似。
    【解决方案5】:

    如果您的意思是“类型安全”,例如在编译时检查类型安全,那么尝试使用泛型来解决这个问题将会很困难。

    主要原因是因为String类是final,所以无法从String创建子类。

    如果可以将String 子类化,则可以将StringString 的用户定义子类都包含在声明为List&lt;? extends String&gt; 的列表中:

    // Not possible.
    List<? extends String> list = new ArrayList<? extends String>;
    list.add("A string");
    list.add(new UserDefinedSubclassOfString());   // There can be no such class.
    

    一种选择是创建一个包含与这两种类型交互的方法的类,它实际上包含Lists,参数化为需要存储的两种类型:

    class MyList {
    
        List<String> strings;
        List<UserDefined> objects;
    
        public void add(String s) {
            strings.add(s);
        }
    
        public void add(UserDefined o) {
            objects.add(o);
        }
    
        // And, so on.
    }
    

    然而,这种方法的问题是它不能使用List 接口,因为它期望参数是E 类型。因此,使用Object? 作为参数(即分别为List&lt;Object&gt;List&lt;?&gt;)将无法达到目的,因为无法在编译时检查类型,因为所有类都在Java 有 Object 作为它的祖先。

    要考虑的一件事是如何处理从这个假设的MyList 获取对象。如果只有一个 get 方法,则返回类型必须是 StringUserDefined 类的共同祖先。这将是Object

    解决这个问题的唯一方法是提供两个 getter,每个类型一个。例如,getStringgetUserDefined。在这一点上,显然不可能使用List 接口,这将需要在get 方法中返回E 类型。

    正如kdgregory's answer 所说,在解决方案中包含这些问题似乎表明它可能不是解决需要解决的问题的最佳方法。

    要了解泛型是什么以及使用泛型有什么可能和不可能,来自The Java TutorialsLesson: Generics 将是一个好的开始。

    【讨论】:

    • 很抱歉,但我无法理解您的解释,您能否详细说明解决方案?请
    • 这种方法的另一个细微差别是它不会维护添加到列表中的对象的顺序。
    【解决方案6】:

    正如 kdgregory 所说,您可能需要重新考虑您的方法。可能有两个列表,List&lt;String&gt;List&lt;Whatever&gt;。可能是一个List&lt;SomeContainer&gt;,包含一个字符串或一个用户对象。可能包含此列表的类希望变为class Blah&lt;T&gt; 而不是class Blah,其中T 可以是StringUserClass 或其他。

    【讨论】:

      【解决方案7】:

      或者可能是一个 List,它采用自定义类型 MyType,将您的 String 和您需要的任何其他内容封装到一个抽象中。

      如果您一直在考虑原语和数据结构,那么您需要提高自己的视野。面向对象是关于封装、抽象和信息隐藏的。在我看来,你做得还不够。

      【讨论】:

        【解决方案8】:

        我的问题是“用户定义的类”应该用于什么? 没有这些知识,很难给出好的建议。 用户不是在创建类,程序员是。您是否开发了一些通用框架?

        您的列表的商业目的是什么? 如果没有提供特定于用户的类,字符串是否应该用作某种默认类型? 在这种情况下,您可以设置 BaseUserDefinedClass,并使用如下列表:

        List<? extends BaseUserDefinedClass> = new ArrayList<DerivedFromBaseUserDefinedClass>();
        

        【讨论】:

          【解决方案9】:

          这取决于您将列表用于...

          如果你只想用它来获取字符串,你可以使用:

          List<String> stringList = new ArrayList<String>();
          // Add String
          stringList.add("myString");
          // add YourObject
          YourObject obj = new YourObject(...);
          stringList.add(obj.toString());
          // ...
          for(String s : stringList) {
              System.out.println(s);
          }
          

          如果您打算使用它来获取 YourObject 引用:

          List<YourObject> objList = new ArrayList<YourObject>();
          // Add String 
          objList.add(new YourObjectAdapter("myString"));
          // add YourObject
          YourObject obj = new YourObject(...);
          objList.add(obj)
          // ...
          for (YourObject y : objList) {
              System.out.println(y.toString());
              // Assuming YourObject defines the "doSomething() method"
              y.doSomething();
          }
          // ...
          class YourObjectAdapter extends YourObject {
              private String wrappedString;
              public YourObjectAdapter(String s) {
                  this.wrappedString = s;
              }
              @Override
              public void toString() {
                  return wrappedString();
              }
              @Override
              public void doSomething() {
                // provide some default implementation...
              }
          }
          

          【讨论】:

            【解决方案10】:

            请参阅《有效 Java,第 29 条:考虑类型安全的异构容器》一书。您也许可以根据您的特定用例调整该项目中的想法。

            我假设您想像List&lt;Object&gt; 一样使用您的List,但您只想允许插入Strings 和其他一些特定类型?这是你想要达到的目标吗?

            如果是这样,你可以这样做:

            import java.util.*;
            
            public class HetContainer {
              Set<Class<?>> allowableClasses;
            
              List<Object> items;
            
              public HetContainer(List<Class<?>> allowableClasses) {
                this.allowableClasses = new HashSet<Class<?>>(allowableClasses);
                items = new ArrayList<Object>();
              }
            
              public void add(Object o) {
                if (allowableClasses.contains(o.getClass())) {
                  items.add(o);
                } else {
                  throw new IllegalArgumentException("Object of type " + o.getClass() + " is not allowed.");
                }
              }
            
              public Object get(int i) {
                return items.get(i);
              }
            
              public static void main(String[] args) {
                List<Class<?>> classes = new ArrayList<Class<?>>();
                classes.add(String.class);
                classes.add(Integer.class);
                HetContainer h = new HetContainer(classes);
                h.add("hello");
                h.add(new Integer(5));
                try {
                  h.add(new Double(5.0));
                } catch (IllegalArgumentException e) {
                  System.out.println(e);
                }
              }
            }
            

            这只是为了向您展示您可以做的事情的简化。此外,一个警告是你不能将泛型类型放入容器中......你为什么问?因为不可能说List&lt;Integer&gt;.classList&lt;Double&gt;.class。原因是因为“擦除”......在运行时,两者都只是Lists。所以你可以在HetContainer 中加入一个原始的List,而不是一个通用的List。再次阅读 Effective Java,这样您就可以了解 Java 的所有限制并根据您的需要进行调整。

            【讨论】:

            • 您会注意到 OP 正在寻找编译时类型安全性。我应该因此而否决你的答案吗?
            • 你说得对,这不提供编译时类型安全......我没有注意到对问题的评论。我的回答的重点也是“阅读有效的java”。我认为它显示了语言的局限性。如果 OP 读到他会理解优缺点以及如何实现异构容器。为了获得编译时类型安全,他需要包装一个列表并且只提供添加支持对象的方法。
            猜你喜欢
            • 2017-09-11
            • 1970-01-01
            • 2011-10-20
            • 1970-01-01
            • 2012-06-02
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            • 2020-05-05
            相关资源
            最近更新 更多