JavaRush /Java 博客 /Random-ZH /Java 泛型中的通配符

Java 泛型中的通配符

已在 Random-ZH 群组中发布
你好!我们继续研究泛型这个话题。从之前的讲座中,您已经对它们有了很多了解(关于在使用泛型时使用可变参数以及关于类型擦除),但我们还没有讨论一个重要的主题:通配符。这是泛型的一个非常重要的特性。以至于我们专门为此专门举办了一场讲座!然而,通配符并不复杂,你现在就会明白了:)泛型中的通配符 - 1让我们看一个例子:
public class Main {

   public static void main(String[] args) {

       String str = new String("Test!");
       // ниHowих проблем
       Object obj = str;

       List<String> strings = new ArrayList<String>();
       // ошибка компиляции!
       List<Object> objects = strings;
   }
}
这里发生了什么?我们看到两种非常相似的情况。在第一个中,我们尝试将对象强制转换String为类型Object。这没有任何问题,一切都按其应有的方式进行。但在第二种情况下,编译器会抛出错误。尽管看起来我们在做同样的事情。只是现在我们使用的是几个对象的集合。但为什么会出现错误呢?String我们将一个对象转换为一种类型还是 20 个对象,本质上有什么区别Object对象和对象集合之间存在重要区别如果一个类B是某个类的后继者А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> object对字符串集合的引用strings,因此我们可以添加的strings不是字符串,而是任何对象Object!因此,我们失去了只有泛型中指定的对象才在我们的集合中的保证String。也就是说,我们失去了泛型的主要优点——类型安全。而且由于编译器允许我们执行所有这些操作,这意味着我们只会在程序执行期间遇到错误,这总是比编译错误糟糕得多。为了防止这种情况,编译器给我们一个错误:
// ошибка компиляции
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()");
   }
}
处于等级制度顶端的只是动物:宠物是从它们继承的。宠物分为两种 - 狗和猫。现在想象我们需要创建一个简单的方法iterateAnimals()。该方法应该接受任何动物的集合(AnimalPetCatDog),迭代所有元素,并每次向控制台输出一些内容。我们来尝试写一下这个方法:
public static void iterateAnimals(Collection<Animal> animals) {

   for(Animal animal: animals) {

       System.out.println("Еще один шаг в цикле пройден!");
   }
}
看来问题已经解决了!然而,正如我们最近发现的那样,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("Еще один шаг в цикле пройден!");
       }
   }

   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());

       //ошибка компилятора!
       iterateAnimals(cats);
   }
}
看来形势对我们来说不太好!事实证明,我们必须编写单独的方法来枚举所有类型的动物?事实上,不,你不必:)通配符会帮助我们解决这个问题!我们将使用以下结构通过一种简单的方法解决该问题:
public static void iterateAnimals(Collection<? extends Animal> animals) {

   for(Animal animal: animals) {

       System.out.println("Еще один шаг в цикле пройден!");
   }
}
这是通配符。更准确地说,这是几种通配符中的第一种 - “ extends ”(另一个名称是Upper Bounded Wildcards)。这个设计告诉我们什么?Animal这意味着该方法将类对象或任何后代类的对象的集合作为输入Animal (? extends Animal)Animal换句话说,该方法可以接受集合、PetDog或作为输入Cat- 这没有什么区别。让我们确保这有效:
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);
}
控制台输出:

Еще один шаг в цикле пройден!
Еще один шаг в цикле пройден!
Еще один шаг в цикле пройден!
Еще один шаг в цикле пройден!
Еще один шаг в цикле пройден!
Еще один шаг в цикле пройден!
Еще один шаг в цикле пройден!
Еще один шаг в цикле пройден!
我们总共创建了4个集合和8个对象,控制台中正好有8个条目。一切都很好!:) 通配符使我们能够轻松地将必要的逻辑与特定类型的绑定整合到一个方法中。我们不再需要为每种类型的动物编写单独的方法。想象一下,如果我们的应用程序用于动物园或兽医诊所,我们会有多少种方法:) 现在让我们看看不同的情况。我们的继承层次结构将保持不变:顶级类是Animal,下面是类 Pets Pet,下一级是CatDog。现在您需要重写该方法,iretateAnimals()以便它可以适用于除狗之外的任何类型的动物。Collection<Animal>也就是说,它应该接受, Collection<Pet>or作为输入Collection<Cat>,但不应该与 一起使用Collection<Dog>。我们怎样才能做到这一点?似乎为每种类型编写单独的方法的前景再次浮现在我们面前:/我们还能如何向编译器解释我们的逻辑?这可以非常简单地完成!在这里,通配符将再次为我们提供帮助。但这次我们将使用不同的类型 - “ super ”(另一个名称是Lower Bounded Wildcards)。
public static void iterateAnimals(Collection<? super Cat> animals) {

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

       System.out.println("Еще один шаг в цикле пройден!");
   }
}
这里的原理是类似的。该构造<? super Cat>告诉编译器该方法可以将该类或任何其他祖先类iterateAnimals()的对象集合作为输入。在我们的例子中,类本身、它的祖先以及祖先的祖先都符合这个描述。该类不符合此约束,因此尝试使用带有列表的方法将导致编译错误: CatCatCatPetsAnimalDogList<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);

   //ошибка компиляции!
   iterateAnimals(dogs);
}
我们的问题得到了解决,通配符再次变得非常有用:) 讲座到此结束。现在您知道了,学习 Java 时泛型主题是多么重要 - 我们花了 4 个讲座来讨论它!但现在你已经很好地掌握了这个话题,并且能够在面试中证明自己:) 现在是时候回到任务上了!祝你学习顺利!:)
评论
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION