代码之家  ›  专栏  ›  技术社区  ›  Lolly

方法中带有extends关键字的Java泛型[重复]

  •  -1
  • Lolly  · 技术社区  · 2 年前

    我正在学习Java泛型。我越过了下面的线。

    public <T extends Building> List<T> fromArrayToList(T[] t) {
    ...
    }
    

    在编译时,这将被转换为如下所示,

    public List<Building>  fromArrayToList(Building[] t) {
    ...
    }
    

    有了这样的理解,我可以在代码中使用下面这样的编译版本。

    public List<Building>  fromArrayToList(Building[] t) {
     ...
    }
    

    使用的原因是什么 List<? extends Building> 而不是 List<Building> ?

    1 回复  |  直到 2 年前
        1
  •  1
  •   rzwitserloot    2 年前

    泛型几乎完全是经过编译器检查的文档。在运行时( java.exe )不知道它们是什么——大多数泛型一开始就无法通过编译(“rasure”)。

    因此,在编译时将其“转换”为其他内容是无关紧要的。Generics 完全 让编译器检查你的工作。它在运行时不执行任何操作。

    那又怎样 是吗?它将事物联系在一起。它告诉编译器:有一些类型,我们不知道它是什么,但在这些地方(不止一个地方,将一件事链接到其他地方是没有用的),它是相同的类型,无论它是什么。这种情况的任何“用法”都可以重新定义它是什么类型,只要它们保持“链接”。

    例如,简单地想象一下这种方法:

    public Object print(Object o) {
      System.out.println(o);
      return o;
    }
    

    一种简单的方法,你可以在使用时使用它来动态打印东西。例如,您可能想要小写传入用户名,以便根据用户名表(不区分大小写)对其进行检查,如下所示: users.get(username.toLowerCase()) ,但也许您希望看到输入的原始用户名,因此您可以执行以下操作:

    users.get(print(username).toLowerCase());
    

    但那行不通!的返回类型 print 方法是 Object 对象 没有 .toLowerCase() 方法

    是的,你有眼睛。我也是:我们可以清楚地看到 打印 显然是一个字符串,所以它应该工作,但它不工作。这不是javac的疏忽——这是有意的行为: 稍后更改方法的主体 宣称是事实 关于您的API,以及在这个特定版本中会发生什么。 public Object print(Object o) 声明 今天和永远(或者至少,直到你说你的API不再向后兼容),取1个参数,可以是任何对象,这个方法返回一些东西。什么不,但是,它至少是一个对象(不是特别有用)。

    今天 它会把你传给它的东西还给它。但是明天呢?谁知道呢。

    所以,让我们解决这个问题,让它再次发挥作用。添加泛型。我们要 链接 具有返回类型的参数的类型:

    public <T> T print(T object) {
      System.out.println(object);
      return object;
    }
    

    现在 maps.get(print(username).toLowerCase()) 确实有效!太棒了

    它之所以有效,只是因为你链接了类型:你告诉编译器:嘿,这个打印方法?每次调用它时,都会有一些类型。对它没有任何限制(好吧,它必须是Object或其某个子类型,所以不是基元,除此之外,任何东西都可以)。返回类型和参数类型都是该类型。哪种类型在当时最方便。因此,给定该规则,编译器会选择 String 成为那种类型,现在代码可以工作了,太好了。

    这是 完全是编译器在做 .如果你对字节码进行反编译,你会注意到print方法只是 public Object print(Object object) ,而是 呼叫者 这有点改变-反编译 users.get(print(username).toLowerCase()) 行中,您注意到编译器已注入 铸造 尽管java行不包含强制转换。那是因为 编译器 意识到这样做是安全的, 因为 泛型的一部分。

    这同样适用于您的代码。你像第一个片段一样写它(用T),这样就可以了:

    class Highrise extends Building {}
    
    Highrise[] highrises = ....;
    List<Highrise> asList = fromArrayToList(highrises);
    

    如果你使用了代码中的第三个片段,那就不起作用了。事实上,第三个片段是 破碎的 。毕竟,我可以称之为:

    List<Building> highrises = ...;
    highrises.add(new LogCabin());
    

    毕竟,驾驶室就是一座建筑。考虑到我可以琐碎地写:

    Building b = new LogCabin();
    

    如果 highrises.add(new LogCabin()) 如果高层建筑是 List<Building> 但是你刚刚在高层建筑列表中添加了一个小木屋,你把它弄坏了。

    使用泛型要实现的第二件事是 方差 给定一个X的列表,它不能仅仅被视为一个Y的列表,其中Y是一个超类型。这完全有意义:

    Building b = new LogCabin(); // fine
    

    但这不起作用:

    List<Building> b = new ArrayList<LogCabin>(); // fails at compile time
    

    为什么?好吧,因为你可以调用 b.add(new Highrise()) 现在你的小木屋列表中有一座高层建筑:

    List<LogCabin> logCabins = new ArrayList<LogCabin>();
    List<Building> buildings = logCabins;
    buildings.add(new Highrise());
    LogCabin lc = logCabins.get(0);
    

    上面的代码证明了为什么不变性是必要的。 ? extends 允许您选择协方差,但是,为了解决上述问题,您不能调用 .add List<? extends> 任何东西即使只是 List<?> (是 List<? extends Object> (好吧,为了变得迂腐,你可以打电话 .add(null) -字面意思 null 文字,因为 无效的 literal同时是所有引用类型,但通常这是非常无用的)。

    通过禁用“添加”,它现在不再重要。让你将木屋列表分配给建筑列表的问题是,这意味着你可以将非木屋建筑添加到列表中,但如果 add 不起作用 完全 ,它不再重要。因此:

    List<LogCabin> logCabins = new ArrayList<LogCabin>(); // fine
    List<? extends Building> buildings = logCabin; // compiles fine
    buildings.add(new Highrise()); // compiler error
    

    现在你知道为什么了 extends 是有用的。

    最好记住两件事来理解泛型:

    • 类型变量以这种方式命名是有充分理由的:有一个Type,但你不知道它是什么 int x; 说“有一个整数,不知道是0、1、5、1238123,谁知道呢?它有一个用于任何特定调用的特定文件,但每次调用都可能不同”- <T> 说有某种类型,谁知道它是什么。
    • 它们将事物联系在一起。如果他们不把事情联系起来,那就没用,或者是语言黑客。类型var必须至少在两个位置使用。

    两个地方的用法示例:

    class ArrayList<T> { // first use
      void add(T elem) { ... } // also here
      T get(int idx) { ... } // and here
      void addAll(List<? extends T> elems) {} // and here
    }
    

    至少有4个地方使用了T,这很好。实现联动。