在Java中,我们可以如何使用泛型使其更好看?

6
我有一种方法可以将一个列表(List)转换成一个映射(Map),使用列表元素的属性之一进行转换:

简单来说,它看起来像这样:

private Map<String, List<Diagnostic<? extends JavaFileObject>>> toMap( List<Diagnostic<? extends JavaFileObject>> diagnostics ) {
    Map<String, List<Diagnostic<? extends JavaFileObject>>> result = new HashMap<String, List<Diagnostic<? extends JavaFileObject>>>();
    for ( Diagnostic<? extends JavaFileObject> d : diagnostics ) {
        List<Diagnostic<? extends JavaFileObject>> list = null;
        if ( !result.containsKey( d.getCode() ) ) {
            list = new ArrayList<Diagnostic<? extends JavaFileObject>>();
            result.put( d.getCode(), list );
        } else {
            list = result.get( d.getCode() );
        }
        assert list != null;
        list.add( d );
    }
    return result;
}

呀啊!...

我非常喜欢泛型,之前我使用的是Java,在使用泛型之前,我不想回到“强制转换一切”的时代,但是当一个泛型包含一个泛型元素时,事情变得混乱了。

我知道在Java1.7中我们将能够使用“钻石”运算符,但应该还有另一种方法。

这是非泛型版本的样子:

private Map toMap( List diagnostics ) { 
    Map result = new HashMap();
    for( Object o  : diagnostics ) {
        Diagnostic d = ( Diagnostic ) o; 
        List list = null;
        if( !result.containsKey( d.getCode() ) ) { 
            list = new ArrayList();
            result.put( d.getCode() , list );
         } else { 
            list = result.get( d.getCode() );
         }
         assert list != null;
         list.add( d );
     }
     return result;
}

大致上,我并没有尝试编译它。

其他语言是如何处理的?例如C#、Scala?我非常喜欢SML或Haskell的处理方式,但有些时候过多的“魔法”可能会有害(当然这是主观的)。

有没有解决办法呢?


1
你的程序需要 <? extends JavaFileObject> 吗?还是可以使用 <JavaFileObject> 或接口? - JustinKSU
我理解你的痛苦 - 你看过Lombok和它对于推断类型使用val的方法吗?(http://projectlombok.org/features/val.html)虽然不能完全解决问题,但可以缩减代码。C#中的`var`提供了类似的解决方案。 - Adrian Cox
一个明显的解决方法是在你的方法内部使用原始类型,如果你接受编译器警告和类型错误的风险,但我同意这种干净的代码很丑陋。 - JB Nizet
@JustinKSU 我需要 <? extends JavaFileObject>,我稍后会用到它。 - OscarRyz
7个回答

7
你需要定义一个名为T的类型参数。然后你可以在你的泛型中使用T,像这样:
private <T extends JavaFileObject> Map<String, List<Diagnostic<T>> toMap(List<Diagnostic<T> diagnostics) {
    Map<String, List<Diagnostic<T>> result = new HashMap<String, List<Diagnostic<T>>();
    for (Diagnostic<T> d : diagnostics ) {
        List<Diagnostic<T>> list = null;
        if ( !result.containsKey(d.getCode())) {
            list = new ArrayList<Diagnostic<T>>();
            result.put( d.getCode(), list );
        } else {
            list = result.get( d.getCode() );
        }
        assert list != null;
        list.add( d );
    }
    return result;
}

在上方,你会看到类型参数被定义为<T extends JavaFileObject>,并且你需要在每一个需要使用它的地方重复使用T。这将使代码更加简洁明了。

2
如果您想使其更加清晰,只需创建一个封装List<Diagnostic<T>>的类型即可。 - Mohamed Mansour
最好的方法是 <D extends Diagnostic> Map<String, List<D>> toMap( List<D> diagnostics ) - 该方法只关心它是一些诊断对象的列表,不关心其他任何内容。 - irreputable
不确定,那个方法被另一段代码使用,而那段代码关心 JavaFileObject 的内容。我可以进行强制类型转换,但是... :-/ - OscarRyz
这样做是行不通的,因为Diagnostic对于JavaFileObject一无所知,它怎么会知道它的类型呢?DiagnosticJavaFileObject是两个不相关的实体。唯一的关联是Diagnostic聚合了一个JavaFileObject - Mohamed Mansour
3
请使用 <D extends Diagnostic<?>> 进行泛型限定。 - Paŭlo Ebermann
类型推断将知道 D=Diagnostic<? extends JavaFileObject> - irreputable

4
在Scala中,代码可能如下所示:
// collections are immutable by default, but we want the mutable flavour
import collection.mutable

// An alias so we don't keep repeating ourself
type DiagMultiMap[T] = mutable.Map[String, mutable.Set[Diagnostic[T]]]

//pimp DiagMultiMap with the addDiagnostic method
class MapDiag[T](theMap: DiagMultiMap[T]) {
  def addDiagnostic(d: Diagnostic[T]): Unit = {
    val set = theMap.getOrElseUpdate(d.getCode) {mutable.Set.empty}
    set += d
  }
}

//an implicit conversion to enable the pimp
implicit def mapDiagPimp[T](theMap: DiagMultiMap[T]) = new MapDiag(theMap)

//This is how we make one
def mkDiagnosticMultiMap[T](entries: Seq[Diagnostic[T]]): DiagMultiMap[T] = {
  val theMap = new mutable.HashMap[String, mutable.Set[Diagnostic[T]]]()
  entries foreach { theMap addDiagnostic _ }
  theMap
}

由于我无法访问 Diagnostic 代码,因此它未经过测试。


更新

这会教我晚上晚睡的后果,实际上这要简单得多...

给定任何一个序列的 Diagnostic 对象:

val diags = List(new Diagnostic(...), new Diagnositic(...), ...)

他们可以通过单个方法轻松地分组:
val diagMap = diags.groupBy(_.getCode)

但实际情况比这要复杂一些!

更大的问题是,Diagnostic是Java标准库的一部分,因此您无法使用变异注释重写它(稍后在代码中会详细介绍)。不过,一个包装器就可以解决问题,而且幸运的是它并不太大:

class RichDiagnostic[S+](underlying: Diagnostic[S]) {
  def code: String = underlying.getCode
  def columnNumber: Long = underlying.getColumnNumber
  def endPosition: Long = underlying.getEndPosition
  def kind: Diagnostic.Kind = underlying.getKind
  def lineNumber: Long = underlying.getLineNumber
  def messageFor(locale: Locale): String = underlying.getMessage(locale) 
  def position: Long = underlying.getPosition
  def source: S = underlying.getSource
  def startPosition: Long = underlying.getStartPosition
  implicit def toUnderlying: Diagnostic[S] = underlying
}

[S+] 中的 + 标记了这个类是协变的,因此如果 AB 的子类,则将 RichDiagnostic[A] 视为 RichDiagnostic[B] 的子类。 这是避免恶劣泛型签名的关键,不再需要使用 <? extends T><? super T> !使用起来也很容易:
val richDiags = diags.map(d => new RichDiagnostic(d))
val diagMap = richDiags.groupBy(_.code)

如果诊断信息最初是作为Java列表提供的,那么像map这样的方法就不会自动提供给您,但转换非常简单:
import collection.JavaConverters._

//the toList isn't strictly necessary, but we get a mutable Buffer otherwise
val richDiags = diagsFromJava.asScala.toList.map(d => new RichDiagnostic(d))
val diagMap = richDiags.groupBy(_.code)

构建这个集合是一次性操作,如果底层列表中添加了条目,则必须重复此操作,但我认为这不会成为问题。

+1 谢谢答案,Java1.6中可以通过javax.tool.Diagnostic访问诊断工具。 - OscarRyz
好的,我会回去查看那个类并更新答案。 - Kevin Wright

3

很好的例子。在通用版本中,有19个类型参数;在原始版本中,只有1个转换。由于这只是一个私有方法,我会选择使用原始版本。即使它更公开,仍然可以保留原始方法体,但具备完整的通用签名。可能是这样的:

Map<String, List<Diagnostic<? extends JavaFileObject>>> 
toMap( List<Diagnostic<? extends JavaFileObject>> diagnostics )
{
    Map result = new HashMap();
    for( Diagnostic d  : diagnostics ) 
    {
        List list = (List)result.get( d.getCode() );
        if(list==null)
            result.put( d.getCode(), list=new ArrayList());
         list.add( d );
    }
    return result;
}

使用更通用的签名和Java 7,我们可以实现以下效果:

<D extends Diagnostic<?>>
Map<String, List<D>> toMap( List<D> diagnostics )
{
    Map<String, List<D>> result = new HashMap<>();
    for( D d  : diagnostics ) 
    {
        List<D> list = result.get( d.getCode() );
        if(list==null)
            result.put( d.getCode(), list=new ArrayList<>());
         list.add( d );
    }
    return result;
}

void test()
{
    List<Diagnostic<? extends JavaFileObject>> x = null;

    Map<String, List<Diagnostic<? extends JavaFileObject>>> map = toMap(x);
}

8个类型参数。


值得注意的是,即使我们不使用<>来保持与Java 6的兼容性,你的第二个示例仍然比问题中的代码有了显著的改进。 - meriton

2

个人建议尝试破解此类问题(Eclipse编译 - 未尝试运行)

private class MapDiag extends HashMap<String, List<Diagnostic<? extends JavaFileObject>>>{
    private static final long serialVersionUID = 1L;

    void add(Diagnostic<? extends JavaFileObject> d){
      List<Diagnostic<? extends JavaFileObject>> list = null;
      if (containsKey(d.getCode())){
        list = get(d.getCode());
      }
      else {
        list = new ArrayList<Diagnostic<? extends JavaFileObject>>();
        put( d.getCode(), list );
      }
      list.add(d);
    }
  }

  private MapDiag toMap2( List<Diagnostic<? extends JavaFileObject>> diagnostics ) {
    MapDiag result = new MapDiag();
    for ( Diagnostic<? extends JavaFileObject> d : diagnostics ) {
      result.add(d);
    }
    return result;
  }

信不信由你,我实际上正在进行这个过程:class DiagnosticList extends ArrayList<Diagnostic<? extends JavaFileObject>>{} :) - OscarRyz

1

我认为一些评论已经得出了“答案”,但迄今为止,我认为没有人给出规范的表述。

private <T extends Diagnostic<? extends JavaFileObject>>
        Map<String, List<T>> toMap(List<T> diagnostics) {
    Map<String, List<T>> result = new HashMap<String, List<T>>();
    for (T d : diagnostics) {
        List<T> list = null;
        if (!result.containsKey(d.getCode())) {
            list = new ArrayList<T>();
            result.put(d.getCode(), list);
        } else {
            list = result.get(d.getCode());
        }
        assert list != null;
        list.add(d);
    }
    return result;
}

类型参数的引入极大地简化了方法的内部结构,同时保持了签名的表达能力。

需要注意的是,这是一个不同于所提出问题的方法,但总体而言可能更加正确。区别在于此处给出的方法将确保诊断的参数化类型对于方法的输入和输出是相同的。

不幸的是,在这种情况下,两个构造函数的调用阻止了我们进一步利用类型参数(特别是对于 Map),尽管如果我们愿意进行强制转换,我们可以使方法更加简洁。


1

首先,你的方法不对吗?我的意思是,它应该更像

List<T> list = null;
if (!result.containsKey(d.getCode())) {
    list = newArrayList();          
} else {
    list = result.get(d.getCode());
}   
result.put(d.getCode(), list);

此外,您始终可以使用静态实用程序方法来模拟钻石操作符,从而获得某种类型推断。也就是说,您可以通过这种方式来简化代码。
public static <K, V> HashMap<K, V> newHashMap() {
    return new HashMap<K, V>();
}

public static <T> ArrayList<T> newArrayList() {
    return new ArrayList<T>();
}

然后你的方法将会是这样

private Map<String, List<Diagnostic<? extends JavaFileObject>>> toMap(List<Diagnostic<? extends JavaFileObject>> diagnostics) {
    Map<String, List<Diagnostic<? extends JavaFileObject>>> result = newHashMap();
    for (Diagnostic<? extends JavaFileObject> d : diagnostics) {
        List<Diagnostic<? extends JavaFileObject>> list = null;
        if (!result.containsKey(d.getCode())) {
            list = newArrayList();
            result.put(d.getCode(), list);
        } else {
            list = result.get(d.getCode());
        }
        assert list != null;
        list.add(d);
    }
    return result;
}

至少实例化将更小... 请注意,如果您正在使用Google Guava库,则可能已经拥有此实用程序方法。 如果您将其与Curtain Dog给出的答案结合起来,您将得到

    private <T extends Diagnostic<? extends JavaFileObject>> Map<String, List<T>> toMap(List<T> diagnostics) {
    Map<String, List<T>> result = newHashMap();
    for (T d : diagnostics) {
        List<T> list = null;
        if (!result.containsKey(d.getCode())) {
            list = newArrayList();
            result.put(d.getCode(), list);
        } else {
            list = result.get(d.getCode());
        }
        assert list != null;
        list.add(d);
    }
    return result;
}

0
混合了大家的建议,这就是我所做的:
我创建了一个新类DiagnosticList来包装ArrayList<Diagnostic<? extends JavaFileObject>> 它非常简单:
static final class DiagnosticList 
extends ArrayList<Diagnostic<? extends JavaFileObject>>{
    // no arg constructor 
    public DiagnosticList(){}
    // Using a list
    public DiagnosticList(List<Diagnostic<? extends JavaFileObject>> diagnostics){
        super( diagnostics);
    }
}

然后我可以更改方法签名。

private Map<String, DiagnosticList> toMap( DiagnosticList diagnostics ) {
    Map<String, DiagnosticList> result = new HashMap<String, DiagnosticList>();
    for ( Diagnostic<? extends JavaFileObject> d : diagnostics ) {
        DiagnosticList list = result.get(d.getCode());
        if( list == null ) {
          result.put( d.getCode(), (list = new DiagnosticList()));
        }
        list.add( d );
    }
    return result;
}

这样会更易读。

虽然我可能会改变原始程序的语义,但我认为这将有助于可维护性。


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