如何在Java中将类作为参数传递并返回一个通用集合?

52

我正在为我的Java应用程序设计一个简单的数据访问对象。我有一些类(记录),它们代表着诸如UserFruit的单行表。

我希望有一个用于获取指定类型所有记录的方法。

目前我的代码是这样的:

public List<User> getAllUsers() {
 ...
}

public List<Fruit> getAllFruits() {
 ...
}

....

但我希望有一个像这样的单一的多态方法(错误的):

public List<T> getAllRecords(Class<T> type) {
    if(type instanceof User) {
        // Use JDBC and SQL SELECT * FROM user
    } else if(type instanceof Fruit) {
        // Use JDBC and SQL SELECT * FROM fruit
    }
    return collection;
}

使用示例:

List<Fruit> fruits = myDataAccessObject.getAllRecrods(Fruit.class);
List<User> users = myDataAccessObject.getAllRecords(User.class);

我该如何在Java中实现这个?


3
Java 有效编程第二版,条款52:通过接口引用对象;你应该偏爱 List<Fruit> 而不是 LinkedList<Fruit> - polygenelubricants
@polygene:谢谢,我已经更新了我的代码并阅读了第52项 :) - Jonas
7个回答

39

看起来你想采用Josh Bloch所称的类型安全异构容器模式:你正在传递一个类型令牌Class<T>,并希望得到一个List<T>

普通的THC可以以类型安全的方式将Class<T>映射到T,但由于你实际上想要一个List<T>,所以你需要使用Neal Gafter所称的超级类型令牌

下面的片段改编自Crazy Bob Lee在Neal Gafter的博客中发布的代码:

public abstract class TypeReference<T> {
    private final Type type;

    protected TypeReference() {
        Type superclass = getClass().getGenericSuperclass();
        if (superclass instanceof Class<?>) {
            throw new RuntimeException("Missing type parameter.");
        }
        this.type = ((ParameterizedType) superclass).getActualTypeArguments()[0];
    }
    public Type getType() {
        return this.type;
    }
}

现在您可以创建像这样的超类型令牌:
    TypeReference<String> stringTypeRef =
            new TypeReference<String>(){};

    TypeReference<Integer> integerTypeRef =
            new TypeReference<Integer>(){};

    TypeReference<List<Boolean>> listBoolTypeRef =
            new TypeReference<List<Boolean>>(){};

基本上,你需要传递一个TypeReference<T>而不是一个Class<T>。区别在于没有List<String>.class,但是你可以创建一个TypeReference<List<String>>
因此,现在我们可以按照以下方式创建容器(以下内容改编自Josh Bloch的原始代码):
public class Favorites {
    private Map<Type, Object> favorites =
        new HashMap<Type, Object>();

    public <T> void setFavorite(TypeReference<T> ref, T thing) {
        favorites.put(ref.getType(), thing);
    }
    public <T> T getFavorite(TypeReference<T> ref) {
        @SuppressWarnings("unchecked")
        T ret = (T) favorites.get(ref.getType());
        return ret;
    }
}

现在我们可以将两者结合起来:
    Favorites f = new Favorites();
    f.setFavorite(stringTypeRef, "Java");
    f.setFavorite(integerTypeRef, 42);
    f.setFavorite(listBoolTypeRef, Arrays.asList(true, true));

    String s = f.getFavorite(stringTypeRef);
    int i = f.getFavorite(integerTypeRef);
    List<Boolean> list = f.getFavorite(listBoolTypeRef);

    System.out.println(s);    // "Java"
    System.out.println(i);    // "42"
    System.out.println(list); // "[true, true]"

Neal Gafter在他的博客中提出,通过一些额外的功能,TypeReference可以成为JDK中值得包含的超类型标记。

附件

参考资料


1
参见:http://gafter.blogspot.com/2007/05/limitation-of-super-type-tokens.html——关于如果您使用具有类型参数的“TypeReference”,则未经检查的强制转换实际上是不安全的。 - polygenelubricants
感谢您提供的好答案和参考资料。但是解决方案很复杂。似乎这是Java语言中非常薄弱的部分。我想跳过实现它,因为它太复杂了。 - Jonas
在Java教程中也有一个解决方案,但它也使用了Reflections,因此比较复杂:http://download-llnw.oracle.com/javase/tutorial/extra/generics/literals.html - Jonas
1
太棒了。有些语言不需要所有这些机制,但在Java中实现这一点是令人钦佩的。 - Carl Smotricz

32

既然你在anish的回答评论中说不想在不同类中使用数据访问方法,那么我想为什么不尝试这样做。

public class Records {

    public interface RecordFetcher<T>{
        public List<T> getRecords();
    }
    static RecordFetcher<Fruit> Fruit=new RecordFetcher<Fruit>(){
        public List<Fruit> getRecords() {
            ...
        }
    };


    static RecordFetcher<User> User=new RecordFetcher<User>(){
        public List<User> getRecords() {
            ...
        }   
    };

    public static void main(String[] args) {
        List<Fruit> fruitRecords=Records.Fruit.getRecords();
        List<User> userRecords=Records.User.getRecords();

    }
}

编辑:

我想再添加一个我的实现。

public class Test 
{ 
    public static void main(String[] args) 
    { 
       Test dataAccess=new Test();
       List<Fruit> FruitList=dataAccess.getAllRecords(Fruit.myType);
       List<User> UserList=dataAccess.getAllRecords(User.myType);
    } 
    <T> List<T> getAllRecords(T cl)
    {
        List<T> list=new ArrayList<T>();
        if(cl instanceof Fruit)
        {
             // Use JDBC and SQL SELECT * FROM fruit
        }
        else if(cl instanceof User)
        {
            // Use JDBC and SQL SELECT * FROM user
        }
        return list;
    }
}
class Fruit
{
    static final Fruit myType;
    static {myType=new Fruit();}
}
class User
{
    static final User myType;
    static {myType=new User();}
}

编辑:

我认为这个实现方式就是你所要求的。

public class Test 
{ 
    public static void main(String[] args) throws InstantiationException, IllegalAccessException 
    { 
       Test dataAccess=new Test();
      
       List<Fruit> FruitList=dataAccess.getAllRecords(Fruit.class);
      
       List<User> UserList=dataAccess.getAllRecords(User.class);
      
    } 
    <T> List<T> getAllRecords(Class<T> cl) throws InstantiationException, IllegalAccessException
    {
        T inst=cl.newInstance();
        List<T> list=new ArrayList<T>();
        if(inst instanceof Fruit)
        {
             // Use JDBC and SQL SELECT * FROM fruit
        }
        else if(inst instanceof User)
        {
            // Use JDBC and SQL SELECT * FROM user
        }
        return list;
    }
}

是的,这是一种替代方案。但是,这段代码与我已经拥有的代码相比有什么优势呢?请参阅我在问题中的第一段代码清单。 - Jonas
并没有太大的优势。我能想到的唯一优点是,你可以将所有数据获取代码隔离到一个单独的类中,并使用相同的函数调用,而不是为不同对象使用不同的函数名称。 - Emil
1
请查看此链接: http://download-llnw.oracle.com/javase/tutorial/extra/generics/literals.html - Emil
太好了!我喜欢你的第二个解决方案,第三个也不错!我已经阅读了那个链接,并且实际上在几天前就将其作为评论发布给了polygenelubricants的答案,但我没有意识到这个技巧。谢谢! - Jonas

15

你已经很接近了。

public <T> LinkedList<T> getAllRecords(List<T> list) {
 ...
}

这被称为泛型方法

您需要指定一个参数,如List<T>。然后,基于您传递的列表类型,Java将推断要返回的泛型类型。

编辑:

Poly的答案非常好。 您可以很容易地进行以下操作,而不必创建TypeReference类。

List<Fruit> fruit = myDataAccessObject.getAllRecrods(new LinkedList<Fruit>());
List<User> users = myDataAccessObject.getAllRecords(new LinkedList<User>());

谢谢,这是一个不错的解决方案。但我在使用instanceof来对LinkedList<Fruit>LinkedList<User>执行不同的操作时遇到了问题。 - Jonas
@Jonas,你需要使用反射来获取声明的泛型类型...我的解决方案可能不是最好的。 - jjnguy
@Justin,我喜欢你的解决方案,因为它很简单,但是Poly的可能更正确一些,但那个解决方案要复杂得多。我会看看如何使用Reflections实现这个。谢谢。 - Jonas
@ Jonas,我知道BalusC在Stack Overflow上已经回答了一个关于你需要的确切问题。 - jjnguy
1
@Jonas,请查看这个问题:https://dev59.com/RXI-5IYBdhLWcg3wQWCc#1942680 - jjnguy
显示剩余4条评论

5

根据您实际检索数据的方式,您可以执行以下操作:

private static <T> List<T> getAll(Class<T> cls){
  List<T> fromSql = (List<T>) sql.query("SELECT * FROM objects WHERE type="+cls.getName());
  return fromSql;
}

这需要您的sql对象返回正确类型的列表,像iBatis这样的O/R映射器可以做到这一点。
如果您需要区分传递的类型,仍然可以在cls上进行switch/case操作。

4

嗯,我不确定你是否需要这样做。

但是这里有一种多态方法。它可能在某个地方有所帮助。

为不同的表创建不同的对象,每个对象实现一个公共接口。这意味着你将每个表都表示为一个对象。

import java.util.LinkedList;

public class DataAccessTest 
{

    /**
     * @param args
     */
    public static void main(String[] args) 
    {
        DataAccess myDataAccessObject = new DataAccess();
        Type type1 = new Fruit();
        Type type2 = new User();
        LinkedList<Type> list1 = myDataAccessObject.getAllRecords(type1);
        LinkedList<Type> list2 = myDataAccessObject.getAllRecords(type2);
        LinkedList<Type> list3 = myDataAccessObject.getAllRecords(new Fruit());
        LinkedList<Type> list4 = myDataAccessObject.getAllRecords(new User());
    }
}

class DataAccess
{
    public LinkedList<Type> getAllRecords(Type type)
    {
        return type.getAllRecords();
    }
}

interface Type
{
    public LinkedList<Type> getAllRecords();
}

class Fruit implements Type
{
    public LinkedList<Type> getAllRecords()
    {
        LinkedList<Type> list = new LinkedList<Type>();
        list.add(new Fruit());
        return list;
    }
}

class User implements Type
{
    public LinkedList<Type> getAllRecords() 
    {
        LinkedList<Type> list = new LinkedList<Type>();
        list.add(new User());
        return list;
    }
}

1
+1:这让您可以根据数据类型指定如何精确加载数据。让我想起了策略模式。 - f1sh
谢谢,但是用这种解决方案,我最终会在许多类中拥有数据访问对象。这并不是我想要的。 - Jonas
@Jonas:只是出于好奇,不同的表会有不同的列,对吧? 如果有任何更改或添加列的情况,那么只需要修改上述代码中的特定对象。虽然它是松散耦合的,但每个表都将有一个类。但是,表的属性再次表示在类中。 - aNish

1

我相信通过一些泛型魔法,你正在尝试的事情是可能的。我刚刚也遇到了同样的问题,这是我所做的:

public class ListArrayUtils{
   @SuppressWarnings("unchecked") // It is checked. 
   public static <T,E> List<T> filterByType(List<E> aList, Class<T> aClass){
      List<T> ans = new ArrayList<>();
      for(E e: aList){
         if(aClass.isAssignableFrom(e.getClass())){
            ans.add((T)e);
         }
      }
      return ans;
   }       
}

还有单元测试:

public class ListArrayUtilsTest{
   interface IfA{/*nothing*/}
   interface IfB{/*nothing*/}
   class A implements IfA{/*nothing*/}
   class B implements IfB{/*nothing*/}
   class C extends A implements IfB{/*nothing*/}

   @Test
   public void testFilterByType(){
      List<Object> data = new ArrayList<>();
      A a = new A();
      B b = new B();
      C c = new C();
      data.add(a);
      data.add(b);
      data.add(c);

      List<IfB> ans = ListArrayUtils.filterByType(data, IfB.class);

      assertEquals(2, ans.size());
      assertSame(b, ans.get(0));
      assertSame(c, ans.get(1));
   }
}

0

我实际上已经在通用数据访问库中完成了这个。请参见 Norm。Github 上有完整的源代码。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接