>  기사  >  Java  >  Java Generics 요약(3) - 와일드카드 사용에 대한 자세한 설명

Java Generics 요약(3) - 와일드카드 사용에 대한 자세한 설명

黄舟
黄舟원래의
2017-03-22 10:25:101478검색

제네릭 사용에는 와일드카드라는 중요한 것이 또 있습니다. 이 글에서는 와일드카드 사용에 대해 소개합니다. 매우 좋은 참조 값을 가지고 있습니다. 아래 에디터로 살펴보겠습니다

소개

처음 두 글에서는 제네릭, 타입 삭제, 제네릭 배열의 기본 사용법을 소개했습니다. 제네릭 사용에는 와일드카드라는 또 다른 중요한 사항이 있습니다. 이 기사에서는 와일드카드 사용에 대해 소개합니다.

배열의 공분산

와일드카드를 이해하기 전에 먼저 배열을 이해해 봅시다. Java의 배열은 공변적입니다. 이는 무엇을 의미합니까? 다음 예를 살펴보십시오.

class Fruit {}
class Apple extends Fruit {}
class Jonathan extends Apple {}
class Orange extends Fruit {}
public class CovariantArrays {
 public static void main(String[] args) { 
 Fruit[] fruit = new Apple[10];
 fruit[0] = new Apple(); // OK
 fruit[1] = new Jonathan(); // OK
 // Runtime type is Apple[], not Fruit[] or Orange[]:
 try {
  // Compiler allows you to add Fruit:
  fruit[0] = new Fruit(); // ArrayStoreException
 } catch(Exception e) { System.out.println(e); }
 try {
  // Compiler allows you to add Oranges:
  fruit[0] = new Orange(); // ArrayStoreException
 } catch(Exception e) { System.out.println(e); }
 }
} /* Output:
java.lang.ArrayStoreException: Fruit
java.lang.ArrayStoreException: Orange
*///:~

기본 메서드의 첫 번째 줄은 Apple 배열을 생성하고 이를 Fruit 배열에 대한 참조에 할당합니다. Apple은 Fruit의 하위 클래스이고 Apple 객체는 Fruit 객체이기도 하므로 Apple 배열도 Fruit의 배열입니다. 이것을 배열의 공분산이라고 합니다. Java는 배열을 공분산하도록 설계했는데, 이는 논란의 여지가 있습니다.

Apple[]을 Fruit[]로 "업캐스트"할 수 있지만 배열 요소의 실제 유형은 여전히 ​​Apple이므로 Apple 또는 Apple의 하위 클래스만 배열에 넣을 수 있습니다. 위 코드에서는 Fruit 객체와 Orange 객체가 배열에 배치됩니다. 컴파일러의 경우 이를 컴파일할 수 있지만 런타임 시 JVM은 배열의 실제 유형이 Apple[]임을 알 수 있으므로 배열에 다른 객체가 추가되면 예외가 발생합니다.

일반 디자인의 목적 중 하나는 컴파일 타임에 이러한 런타임 오류를 발견할 수 있도록 하는 것입니다. 배열 대신 일반 컨테이너 클래스를 사용할 때 어떤 일이 발생하는지 살펴보세요.

// Compile Error: incompatible types:
ArrayList<Fruit> flist = new ArrayList<Apple>();

위 코드는 단순히 컴파일되지 않습니다. 제네릭의 경우 Apple은 Fruit의 하위 유형이지만 ArrayList은 ArrayList의 하위 유형이 아니며 제네릭은 공분산을 지원하지 않습니다.

와일드카드 사용

위에서 우리는 Integer가 Number의 하위 유형이더라도

과 같은 문은 컴파일할 수 없다는 것을 알고 있습니다. 그렇다면 우리가 이러한 "상향적 변화" 관계를 가질 필요가 있다면 어떻게 될까요? 여기서 와일드카드가 사용됩니다. Listc8f01a3f8889dcf657849dd45bc0fc4c list = ArrayListc0f559cc8d56b43654fcbe4aa9df7b4a

상위 와일드카드

641c1dd1b20e9979a0cb70e3109a8bc6 형식의 와일드카드를 사용하여 일반적인 상향 변환을 수행합니다.

public class GenericsAndCovariance {
 public static void main(String[] args) {
 // Wildcards allow covariance:
 List<? extends Fruit> flist = new ArrayList<Apple>();
 // Compile Error: can&#39;t add any type of object:
 // flist.add(new Apple());
 // flist.add(new Fruit());
 // flist.add(new Object());
 flist.add(null); // Legal but uninteresting
 // We know that it returns at least Fruit:
 Fruit f = flist.get(0);
 }
}

위의 예에서 Flist 유형은

입니다. List 유형으로 읽을 수 있습니다. 이 유형은 List57019040ccef885c8e3bd8f9deb31922 Fruit을 상속하는 유형일 수 있습니다. 이 목록이 모든 유형의 과일을 담을 수 있다는 의미는 아닙니다. 와일드카드는 특정 유형을 나타냅니다. 즉, "특정 유형이지만 Flist가 지정하지 않음"을 의미합니다. 이 예에 대한 구체적인 설명은 유형이 Fruit에서 상속되는 한 Flist 참조가 특정 유형의 목록을 가리킬 수 있다는 것입니다(예: ). 그러나 flist로 상향 변환하기 위해 flist는 특정 유형이 무엇인지 상관하지 않습니다. new ArrayList463277d9ebc274bcf30ecc27cb72790a

위에서 언급했듯이 와일드카드 문자

는 특정 유형(Fruit 또는 해당 하위 클래스)의 목록을 나타내지만 실제 유형이 무엇인지는 상관하지 않습니다. 과일은 그 상한선이다. 그렇다면 우리는 그러한 목록으로 무엇을 할 수 있을까요? 사실, 이 List가 어떤 유형을 가지고 있는지 모른다면 어떻게 객체를 안전하게 추가할 수 있을까요? 위 코드에서 Apple, Orange, Fruit 객체 등 어떤 객체든 Flist에 추가하면 컴파일러는 이를 허용하지 않으며 추가할 수 있는 유일한 항목은 null입니다. 따라서 일반적인 상향 형변환(List57019040ccef885c8e3bd8f9deb31922)을 수행하면 이 목록에 개체를 추가하는 기능이 손실됩니다. List57019040ccef885c8e3bd8f9deb31922 flist = new ArrayList463277d9ebc274bcf30ecc27cb72790a()

반면에 Fruit을 반환하는 메소드를 호출하면 안전합니다. 우리는 이 목록에서 실제 유형이 무엇이든 확실히 Fruit로 변환될 수 있다는 것을 알고 있으므로 컴파일러는 Fruit의 반환을 허용합니다.

와일드카드의 기능과 한계를 이해하고 나면 매개변수를 받는 메소드를 호출할 수 없는 것 같습니다. 실제로는 그렇지 않습니다. 다음 예를 살펴보세요.

public class CompilerIntelligence {
 public static void main(String[] args) {
 List<? extends Fruit> flist =
 Arrays.asList(new Apple());
 Apple a = (Apple)flist.get(0); // No warning
 flist.contains(new Apple()); // Argument is ‘Object&#39;
 flist.indexOf(new Apple()); // Argument is ‘Object&#39;
 //flist.add(new Apple()); 无法编译
 }
}

위 예에서 Flist 유형은

이고 일반 매개변수는 제한된 와일드카드를 사용하므로 유형을 추가하는 기능을 잃게 됩니다. 객체 예제에서는 코드의 마지막 줄을 컴파일할 수 없습니다. List57019040ccef885c8e3bd8f9deb31922

但是 flist 却可以调用 contains 和 indexOf 方法,它们都接受了一个 Apple 对象做参数。如果查看 ArrayList 的源代码,可以发现 add() 接受一个泛型类型作为参数,但是 contains 和 indexOf 接受一个 Object 类型的参数,下面是它们的方法签名:

public boolean add(E e)
public boolean contains(Object o)
public int indexOf(Object o)

所以如果我们指定泛型参数为 57019040ccef885c8e3bd8f9deb31922 时,add() 方法的参数变为 ? extends Fruit,编译器无法判断这个参数接受的到底是 Fruit 的哪种类型,所以它不会接受任何类型。

然而,contains 和 indexOf 的类型是 Object,并没有涉及到通配符,所以编译器允许调用这两个方法。这意味着一切取决于泛型类的编写者来决定那些调用是 “安全” 的,并且用 Object 作为这些安全方法的参数。如果某些方法不允许类型参数是通配符时的调用,这些方法的参数应该用类型参数,比如 add(E e)。

当我们自己编写泛型类时,上面介绍的就有用了。下面编写一个 Holder 类:

public class Holder<T> {
 private T value;
 public Holder() {}
 public Holder(T val) { value = val; }
 public void set(T val) { value = val; }
 public T get() { return value; }
 public boolean equals(Object obj) {
 return value.equals(obj);
 }
 public static void main(String[] args) {
 Holder<Apple> Apple = new Holder<Apple>(new Apple());
 Apple d = Apple.get();
 Apple.set(d);
 // Holder<Fruit> Fruit = Apple; // Cannot upcast
 Holder<? extends Fruit> fruit = Apple; // OK
 Fruit p = fruit.get();
 d = (Apple)fruit.get(); // Returns ‘Object&#39;
 try {
  Orange c = (Orange)fruit.get(); // No warning
 } catch(Exception e) { System.out.println(e); }
 // fruit.set(new Apple()); // Cannot call set()
 // fruit.set(new Fruit()); // Cannot call set()
 System.out.println(fruit.equals(d)); // OK
 }
} /* Output: (Sample)
java.lang.ClassCastException: Apple cannot be cast to Orange
true
*///:~

在 Holer 类中,set() 方法接受类型参数 T 的对象作为参数,get() 返回一个 T 类型,而 equals() 接受一个 Object 作为参数。fruit 的类型是 Holder57019040ccef885c8e3bd8f9deb31922,所以set()方法不会接受任何对象的添加,但是 equals() 可以正常工作。

下边界限定通配符

通配符的另一个方向是 “超类型的通配符“: ? super TT是类型参数的下界。使用这种形式的通配符,我们就可以 ”传递对象” 了。还是用例子解释:

public class SuperTypeWildcards {
 static void writeTo(List<? super Apple> apples) {
 apples.add(new Apple());
 apples.add(new Jonathan());
 // apples.add(new Fruit()); // Error
 }
}

writeTo 方法的参数 apples 的类型是 List72b4226105aa1d07ec3b6e98f565c59e  它表示某种类型的 List,这个类型是 Apple 的基类型。也就是说,我们不知道实际类型是什么,但是这个类型肯定是 Apple 的父类型。因此,我们可以知道向这个 List 添加一个 Apple 或者其子类型的对象是安全的,这些对象都可以向上转型为 Apple。但是我们不知道加入 Fruit 对象是否安全,因为那样会使得这个 List 添加跟 Apple 无关的类型。

在了解了子类型边界和超类型边界之后,我们就可以知道如何向泛型类型中 “写入” ( 传递对象给方法参数) 以及如何从泛型类型中 “读取” ( 从方法中返回对象 )。下面是一个例子:

public class Collections { 
 public static <T> void copy(List<? super T> dest, List<? extends T> src) 
 {
 for (int i=0; i<src.size(); i++) 
 dest.set(i,src.get(i)); 
 } 
}

src 是原始数据的 List,因为要从这里面读取数据,所以用了上边界限定通配符:d203bb1ae585225d4838a2b7e3d0503e,取出的元素转型为 T。dest 是要写入的目标 List,所以用了下边界限定通配符:117c5a0bdb71ea9a9d0c2b99b03abe3e,可以写入的元素类型是 T 及其子类型。

无边界通配符

还有一种通配符是无边界通配符,它的使用形式是一个单独的问号:List6b3d0130bba23ae47fe2b8e8cddf0195,也就是没有任何限定。不做任何限制,跟不用类型参数的 List 有什么区别呢?

List6b3d0130bba23ae47fe2b8e8cddf0195 list表示 list 是持有某种特定类型的 List,但是不知道具体是哪种类型。那么我们可以向其中添加对象吗?当然不可以,因为并不知道实际是哪种类型,所以不能添加任何类型,这是不安全的。而单独的 List list ,也就是没有传入泛型参数,表示这个 list 持有的元素的类型是 Object,因此可以添加任何类型的对象,只不过编译器会有警告信息。

总结

通配符的使用可以对泛型参数做出某些限制,使代码更安全,对于上边界和下边界限定的通配符总结如下:

  • 使用 Listb75ffcf7a0002c20418af7f7df067052 list 这种形式,表示 list 可以引用一个 ArrayList ( 或者其它 List 的 子类 ) 的对象,这个对象包含的元素类型是 C 的子类型 ( 包含 C 本身)的一种。

  • 使用 List41a5f90752881873ec48ff3be0e4f0a1 list 这种形式,表示 list 可以引用一个 ArrayList ( 或者其它 List 的 子类 ) 的对象,这个对象包含的元素就类型是 C 的超类型 ( 包含 C 本身 ) 的一种。

大多数情况下泛型的使用比较简单,但是如果自己编写支持泛型的代码需要对泛型有深入的了解。这几篇文章介绍了泛型的基本用法、类型擦除、泛型数组以及通配符的使用,涵盖了最常用的要点,泛型的总结就写到这里。

위 내용은 Java Generics 요약(3) - 와일드카드 사용에 대한 자세한 설명의 상세 내용입니다. 자세한 내용은 PHP 중국어 웹사이트의 기타 관련 기사를 참조하세요!

성명:
본 글의 내용은 네티즌들의 자발적인 기여로 작성되었으며, 저작권은 원저작자에게 있습니다. 본 사이트는 이에 상응하는 법적 책임을 지지 않습니다. 표절이나 침해가 의심되는 콘텐츠를 발견한 경우 admin@php.cn으로 문의하세요.