CodeGym /Java 博客 /随机的 /泛型中的通配符
John Squirrels
第 41 级
San Francisco

泛型中的通配符

已在 随机的 群组中发布
你好!让我们继续研究泛型。您已经从之前的课程中获得了大量关于它们的知识(关于在处理泛型时使用可变参数类型擦除),但是我们还没有考虑一个重要的主题——通配符。这是泛型非常重要的特性。如此之多,以至于我们专门为此开设了一堂课!也就是说,通配符并没有什么特别复杂的地方。你马上就会看到 :)泛型中的通配符 - 1让我们看一个例子:

public class Main {

   public static void main(String[] args) {
      
       String str = new String("Test!");
       // No problem
       Object obj = str;
      
       List<String> strings = new ArrayList<String>();
       // Compilation error!
       List<Object> objects = strings;
   }
}
这里发生了什么?我们看到两种非常相似的情况。在这种情况下,我们将一个String对象转换为一个Object对象。这里没有问题——一切都按预期进行。但在第二种情况下,编译器会产生错误。但我们正在做同样的事情,不是吗?这次我们只是使用几个对象的集合。但是为什么会出现这个错误呢?有什么不同?我们是将一个String对象投射到一个Object还是 20 个对象?对象对象集合之间有一个重要的区别。 如果B班级是班级的孩子A,则Collection<B>不是 的孩子Collection<A> 这就是为什么我们无法将我们的投射List<String>List<Object>. String是 的孩子Object,但List<String>不是 的孩子List<Object> 这可能看起来不是很直观。为什么语言的创造者会这样做呢?让我们假设编译器没有给我们一个错误:

List<String> strings = new ArrayList<String>();
List<Object> objects = strings;
在这种情况下,例如,我们可以执行以下操作:

objects.add(new Object());
String s = strings.get(0);
因为编译器没有给我们任何错误并允许我们创建一个List<Object>指向 的引用strings,所以我们可以将任何旧Object对象添加到strings集合中!因此,我们失去了我们的集合仅包含泛型类型调用中类型参数指定的对象的保证String。换句话说,我们失去了泛型的主要优势——类型安全。而且因为编译器没有阻止我们这样做,我们只会在运行时得到一个错误,这总是比编译错误糟糕得多。为了防止出现这种情况,编译器会报错:

// Compilation error
List<Object> objects = strings;
...并提醒我们那List<String>不是 . 的后代List<Object>。这是泛型的铁律,在使用泛型时必须牢记这一点。让我们继续。假设我们有一个小的类层次结构:

public class Animal {

   public void feed() {

       System.out.println("Animal.feed()");
   }
}

public class Pet extends Animal {

   public void call() {

       System.out.println("Pet.call()");
   }
}

public class Cat extends Pet {

   public void meow() {

       System.out.println("Cat.meow()");
   }
}
该层次结构顶部是一个简单的 Animal 类,该类由 Pet 继承。Pet 有 2 个子类:Dog 和 Cat。现在假设我们需要创建一个简单的iterateAnimals()方法。该方法应该采用任何动物的集合 ( Animal, Pet, Cat, Dog),迭代所有元素,并在每次迭代期间在控制台上显示一条消息。让我们尝试编写这样一个方法:

public static void iterateAnimals(Collection<Animal> animals) {

   for(Animal animal: animals) {

       System.out.println("Another iteration in the loop!");
   }
}
看来问题解决了!然而,正如我们最近了解到的,List<Cat>,List<Dog>List<Pet>不是List<Animal>! 这意味着当我们尝试使用iterateAnimals()猫列表调用该方法时,会出现编译错误:

import java.util.*;

public class Main3 {


   public static void iterateAnimals(Collection<Animal> animals) {

       for(Animal animal: animals) {

           System.out.println("Another iteration in the loop!");
       }
   }

   public static void main(String[] args) {


       List<Cat> cats = new ArrayList<>();
       cats.add(new Cat());
       cats.add(new Cat());
       cats.add(new Cat());
       cats.add(new Cat());

       // Compilation error!
       iterateAnimals(cats);
   }
}
情况对我们来说不太好!我们是否必须编写单独的方法来枚举每种动物?实际上,不,我们没有 :) 碰巧的是,通配符可以帮助我们解决这个问题!我们可以使用以下构造通过一种简单的方法解决问题:

public static void iterateAnimals(Collection<? extends Animal> animals) {

   for(Animal animal: animals) {

       System.out.println("Another iteration in the loop!");
   }
}
这是一个通配符。更准确地说,这是几种类型的通配符中的第一种。它被称为上限通配符,用? 延伸。这个结构告诉我们什么?这意味着该方法接受对象集合或从(? extends Animal)Animal派生的任何类的对象集合。Animal换句话说,该方法可以接受AnimalPetDogCat对象的集合——这没有区别。让我们说服自己它有效:

public static void main(String[] args) {

   List<Animal> animals = new ArrayList<>();
   animals.add(new Animal());
   animals.add(new Animal());

   List<Pet> pets = new ArrayList<>();
   pets.add(new Pet());
   pets.add(new Pet());

   List<Cat> cats = new ArrayList<>();
   cats.add(new Cat());
   cats.add(new Cat());

   List<Dog> dogs = new ArrayList<>();
   dogs.add(new Dog());
   dogs.add(new Dog());

   iterateAnimals(animals);
   iterateAnimals(pets);
   iterateAnimals(cats);
   iterateAnimals(dogs);
}
控制台输出:

Another iteration in the loop!
Another iteration in the loop!
Another iteration in the loop!
Another iteration in the loop!
Another iteration in the loop!
Another iteration in the loop!
Another iteration in the loop!
Another iteration in the loop!
我们一共创建了4个集合,8个对象,控制台上正好有8个入口。一切都很好!:) 通配符使我们能够轻松地将绑定到特定类型的必要逻辑放入一个方法中。我们消除了为每种动物编写单独方法的需要。想象一下,如果我们的应用程序被动物园或兽医办公室使用,我们需要多少方法 :) 但是现在让我们看看不同的情况。我们的继承层次结构保持不变:顶级类是AnimalPet下面是类,下一层是CatDog类。现在您需要重写该iterateAnimals()方法,以便可以处理任何类型的动物,狗除外。也就是说,它应该接受Collection<Animal>Collection<Pet>Collection<Car>,但它不应该与 一起使用Collection<Dog>。我们怎样才能做到这一点?似乎我们再次面临为每种类型编写单独方法的前景:/否则我们如何向编译器解释我们想要发生的事情?其实很简单!在这里,通配符再次为我们提供帮助。但这次我们将使用另一种类型的通配符——下界通配符,它​​使用super表示。

public static void iterateAnimals(Collection<? super Cat> animals) {

   for(int i = 0; i < animals.size(); i++) {

       System.out.println("Another iteration in the loop!");
   }
}
这里原理类似。该<? super Cat>构造告诉编译器该iterateAnimals()方法可以接受Cat对象集合或类的任何祖先Cat作为输入作为输入。在这种情况下,该类Cat、它的父级Pet及其父级的父级Animal都符合此描述。该类Dog不符合我们的限制,因此尝试使用带有List<Dog>参数的方法将导致编译错误:

public static void main(String[] args) {

   List<Animal> animals = new ArrayList<>();
   animals.add(new Animal());
   animals.add(new Animal());

   List<Pet> pets = new ArrayList<>();
   pets.add(new Pet());
   pets.add(new Pet());

   List<Cat> cats = new ArrayList<>();
   cats.add(new Cat());
   cats.add(new Cat());

   List<Dog> dogs = new ArrayList<>();
   dogs.add(new Dog());
   dogs.add(new Dog());

   iterateAnimals(animals);
   iterateAnimals(pets);
   iterateAnimals(cats);
  
   // Compilation error!
   iterateAnimals(dogs);
}
我们已经解决了我们的问题,通配符再次证明非常有用 :) 至此,本课结束。现在您知道泛型在您的 Java 学习中有多么重要了——我们已经学习了 4 节关于它们的完整课程!但是现在您已经精通该主题并且可以在求职面试中证明您的技能 :) 现在,是时候回到任务了!祝你学业成功!:)
评论
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION