>  기사  >  Java  >  Java(코드)의 와일드카드에 대한 자세한 분석

Java(코드)의 와일드카드에 대한 자세한 분석

不言
不言원래의
2018-09-08 16:46:062318검색

이 기사는 Java의 와일드카드에 대한 자세한 분석(코드)을 제공합니다. 필요한 친구가 참고할 수 있기를 바랍니다.

일반 유형의 하위 유형이 관련성이 없다는 점은 이 문서의 이전 부분에서 언급되었습니다. 하지만 때로는 일반 유형처럼 제네릭 유형을 사용할 수 있기를 원합니다.

◆ 일반 객체에 대한 참조 업캐스트

◆ 일반 객체에 대한 참조 다운캐스트

일반 객체 업캐스트 Quote

예를 들어, 상자가 많고 각 상자에 서로 다른 과일이 들어 있으며 과일 상자를 보편적으로 처리하는 방법을 찾아야 한다고 가정해 보겠습니다. 보다 일반적으로 A는 B의 하위 유형이므로 C 유형의 인스턴스를 C 유형의 선언에 할당하는 방법을 찾아야 합니다.

이를 달성하려면 다음 예와 같이 와일드카드가 있는 확장 선언을 사용해야 합니다.

List<Apple> apples = new ArrayList<Apple>();
List<? extends Fruit> fruits = apples;

"?extends"는 일반 유형의 하위 유형입니다. 종속성은 현실이 됩니다. Apple은 Fruit Type의 하위 유형입니다. List는 List의 하위 유형입니다.

일반 객체에 대한 참조를 다운캐스트

이제 또 다른 와일드카드를 소개하겠습니다: ? 유형 B가 유형 A의 상위 유형인 경우 C는 C super A>의 하위 유형입니다.

List<Fruit> fruits = new ArrayList<Fruit>();
List<? super Apple> = fruits;

와일드카드 표시를 사용하는 이유는 무엇인가요?

이제 원칙은 분명해졌습니다. 이 새로운 문법 구조를 어떻게 활용할까요?

? extends

Java 배열의 하위 유형 종속성에 대해 설명하는 두 번째 부분에서 사용된 예제를 다시 살펴보겠습니다.

Apple[] apples = new Apple[ 1 ];
Fruit[] fruits = apples;
fruits[ 0 ] = new Strawberry();

보시다시피, Fruit로 선언된 객체로 이동하면 Strawberry 객체를 추가한 후 배열의 Apple 객체 배열을 사용하면 코드를 컴파일할 수 있지만 런타임에 예외가 발생합니다.

이제 와일드카드를 사용하여 관련 코드를 일반 코드로 변환할 수 있습니다. Apple은 Fruit의 하위 클래스이므로 ? 확장 와일드카드를 사용하여 List 객체의 정의를 List ;님의 발언:

List<Apple> apples = new ArrayList<Apple>();
List<? extends Fruit> fruits = apples;
fruits.add( new Strawberry());

이번에는 코드를 컴파일할 수 없습니다! Java 컴파일러는 과일 목록에 딸기를 추가하는 것을 방지합니다. 컴파일 타임에 오류를 감지할 수 있으며, 호환되지 않는 유형이 목록에 추가되었는지 확인하기 위해 런타임에 확인할 필요가 없습니다. 목록에 과일 개체를 추가하더라도 작동하지 않습니다.

fruits.add( new Fruit());

이 작업은 수행할 수 없습니다. 실제로 ? 확장을 사용하는 데이터 구조에는 어떤 값도 쓸 수 없습니다.

이유는 매우 간단합니다. 이렇게 생각하면 됩니다. ? 확장 T 와일드카드는 컴파일러에게 T 유형의 하위 유형을 다루고 있지만 우리는 이 하위 유형이 무엇인지 모릅니다. 확신할 수 있는 방법이 없기 때문에 유형 안전성을 보장하기 위해 이 유형의 데이터를 추가할 수 없습니다. 반면, 어떤 유형이든지 항상 유형 T의 하위 유형이라는 것을 알고 있기 때문에 데이터를 읽을 때 우리가 얻는 데이터가 유형 T의 인스턴스인지 확인할 수 있습니다.

Fruit get = fruits.get( 0 );

? super

super 와일드카드를 사용할 때 일반적인 상황은 무엇인가요? 먼저 이것을 살펴보겠습니다.

List<Fruit> fruits = new ArrayList<Fruit>();
List<? super Apple> = fruits;

fruits가 Apple의 상위 유형 중 일부를 보유하는 List를 가리키는 것을 알 수 있습니다. 다시 말하지만, 우리는 슈퍼클래스가 무엇인지 정확히 알지 못하지만 Apple과 Apple의 모든 하위 클래스가 해당 유형과 호환된다는 것은 알고 있습니다. 이 알려지지 않은 유형은 Apple이고 GreenApple의 슈퍼클래스이므로 다음과 같이 작성할 수 있습니다.

fruits.add( new Apple());
fruits.add( new GreenApple());

Apple의 슈퍼클래스를 여기에 추가하려는 경우 컴파일러는 다음과 같이 경고합니다.

fruits.add( new Fruit());
fruits.add( new Object());

우리는 그것을 모르기 때문에 어떤 종류의 슈퍼 클래스인지 그런 경우는 모두 가입할 수 없습니다.

이 유형에서 데이터를 가져오는 것은 어떻습니까? Object 인스턴스만 꺼낼 수 있다는 것이 밝혀졌습니다. 우리는 슈퍼클래스가 무엇인지 모르기 때문에 컴파일러가 보장할 수 있는 유일한 것은 그것이 Object라는 것입니다. 왜냐하면 Object는 모든 Java 유형의 슈퍼클래스이기 때문입니다.

액세스 원칙 및 PECS 규칙

? 확장 및 ? 슈퍼 와일드카드의 특징을 요약하면 다음과 같은 결론을 내릴 수 있습니다.

◆ 데이터 유형에서 데이터를 얻으려면 ? 확장 와일드카드를 사용하세요.

◆ 객체를 데이터 구조에 쓰려면 ? 슈퍼 와일드카드를 사용하세요.

◆ 저장과 검색을 모두 원한다면 와일드카드를 사용하지 마세요.

이것은 Maurice Naftalin이 그의 저서 "Java Generics and Collections"에서 액세스 원칙이라고 부르는 것과 Joshua Bloch가 그의 저서 "Effective Java"에서 PECS 규칙이라고 부르는 것입니다.

Bloch는 PECS가 "Producer Extends, Consumer Super"의 약자로서 기억하고 사용하기 더 쉽다는 점을 상기시켰습니다.

위 내용은 맨 아래부터 이어집니다.

The Java Tutorial

java Generics and Collections, 작성자: Maurice Naftalin 및 Philip Wadler

Effective Java 중국어 버전(2판), 작성자: Joshua Bloch.

尽管有这么多丰富的资料,有时我感觉,有很多的程序员仍然不太明白Java泛型的功用和意义。这就是为什么我想使用一种最简单的形式来总结一下程序员需要知道的关于Java泛型的最基本的知识。

Java泛型由来的动机

理解Java泛型最简单的方法是把它看成一种便捷语法,能节省你某些Java类型转换(casting)上的操作:

List<Apple> box = ...;
Apple apple = box.get( 0 );

上面的代码自身已表达的很清楚:box是一个装有Apple对象的List。get方法返回一个Apple对象实例,这个过程不需要进行类型转换。没有泛型,上面的代码需要写成这样:

List box = ...;
Apple apple = (Apple) box.get( 0 );

很明显,泛型的主要好处就是让编译器保留参数的类型信息,执行类型检查,执行类型转换操作:编译器保证了这些类型转换的绝对无误。

相对于依赖程序员来记住对象类型、执行类型转换——这会导致程序运行时的失败,很难调试和解决,而编译器能够帮助程序员在编译时强制进行大量的类型检查,发现其中的错误。

泛型的构成

由泛型的构成引出了一个类型变量的概念。根据Java语言规范,类型变量是一种没有限制的标志符,产生于以下几种情况:

◆ 泛型类声明

◆ 泛型接口声明

◆ 泛型方法声明

◆ 泛型构造器(constructor)声明

泛型类和接口

如果一个类或接口上有一个或多个类型变量,那它就是泛型。类型变量由尖括号界定,放在类或接口名的后面:

public interface List<T> extends Collection<T> {
...
}

简单的说,类型变量扮演的角色就如同一个参数,它提供给编译器用来类型检查的信息。

Java类库里的很多类,例如整个Collection框架都做了泛型化的修改。例如,我们在上面的第一段代码里用到的List接口就是一个泛型类。在那段代码里,box是一个List对象,它是一个带有一个Apple类型变量的List接口的类实现的实例。编译器使用这个类型变量参数在get方法被调用、返回一个Apple对象时自动对其进行类型转换。

实际上,这新出现的泛型标记,或者说这个List接口里的get方法是这样的:

 T get( int index);

get方法实际返回的是一个类型为T的对象,T是在List声明中的类型变量。

泛型方法和构造器(Constructor)

非常的相似,如果方法和构造器上声明了一个或多个类型变量,它们也可以泛型化。

 public static <t> T getFirst(List<T> list)

这个方法将会接受一个List类型的参数,返回一个T类型的对象。

例子

你既可以使用Java类库里提供的泛型类,也可以使用自己的泛型类。

类型安全的写入数据…

下面的这段代码是个例子,我们创建了一个List实例,然后装入一些数据:

List<String> str = new ArrayList<String>();
str.add( "Hello " );
str.add( "World." );

如果我们试图在List装入另外一种对象,编译器就会提示错误:

 str.add( 1 ); // 不能编译

类型安全的读取数据…

当我们在使用List对象时,它总能保证我们得到的是一个String对象:

 String myString = str.get( 0 );

遍历

类库中的很多类,诸如Iterator,功能都有所增强,被泛型化。List接口里的iterator()方法现在返回的是Iterator,由它的T next()方法返回的对象不需要再进行类型转换,你直接得到正确的类型。

for (Iterator<String> iter = str.iterator(); iter.hasNext();) {
String s = iter.next();
System.out.print(s);
}

使用foreach

“for each”语法同样受益于泛型。前面的代码可以写出这样:

for (String s: str) {
System.out.print(s);
}

这样既容易阅读也容易维护。

自动封装(Autoboxing)和自动拆封(Autounboxing)

在使用Java泛型时,autoboxing/autounboxing这两个特征会被自动的用到,就像下面的这段代码:

List<Integer> ints = new ArrayList<Integer>();
ints.add( 0 );
ints.add( 1 );
int sum = 0 ;
for ( int i : ints) {
sum += i;
}

然而,你要明白的一点是,封装和解封会带来性能上的损失,所有,通用要谨慎的使用。

子类型

在Java中,跟其它具有面向对象类型的语言一样,类型的层级可以被设计成这样:

Java(코드)의 와일드카드에 대한 자세한 분석

在Java中,类型T的子类型既可以是类型T的一个扩展,也可以是类型T的一个直接或非直接实现(如果T是一个接口的话)。因为“成为某类型的子类型”是一个具有传递性质的关系,如果类型A是B的一个子类型,B是C的子类型,那么A也是C的子类型。在上面的图中:

◆ FujiApple(富士苹果)是Apple的子类型

◆ Apple是Fruit(水果)的子类型

◆ FujiApple(富士苹果)是Fruit(水果)的子类型

所有Java类型都是Object类型的子类型。

B类型的任何一个子类型A都可以被赋给一个类型B的声明:

Apple a = ...;
Fruit f = a;

泛型类型的子类型

如果一个Apple对象的实例可以被赋给一个Fruit对象的声明,就像上面看到的,那么,List 和 a List之间又是个什么关系呢?更通用些,如果类型A是类型B的子类型,那C 和 C之间是什么关系?

答案会出乎你的意料:没有任何关系。用更通俗的话,泛型类型跟其是否子类型没有任何关系。

这意味着下面的这段代码是无效的:

List<apple> apples = ...;
List<fruit> fruits = apples;</fruit></apple>

下面的同样也不允许:

List < Apple > apples;
List < Fruit > fruits = ...;
apples = fruits ;

为什么?一个苹果是一个水果,为什么一箱苹果不能是一箱水果?

在某些事情上,这种说法可以成立,但在类型(类)封装的状态和操作上不成立。如果把一箱苹果当成一箱水果会发生什么情况?

List<apple> apples = ...;
List<fruit> fruits = apples;
fruits.add( new Strawberry());</fruit></apple>

如果可以这样的话,我们就可以在list里装入各种不同的水果子类型,这是绝对不允许的。

另外一种方式会让你有更直观的理解:一箱水果不是一箱苹果,因为它有可能是一箱另外一种水果,比如草莓(子类型)。

这是一个需要注意的问题吗?

应该不是个大问题。而程序员对此感到意外的最大原因是数组和泛型类型上用法的不一致。对于泛型类型,它们和类型的子类型之间是没什么关系的。而对于数组,它们和子类型是相关的:如果类型A是类型B的子类型,那么A[]是B[]的子类型:

Apple[] apples = ...;
Fruit[] fruits = apples;

可是稍等一下!如果我们把前面的那个议论中暴露出的问题放在这里,我们仍然能够在一个apple类型的数组中加入strawberrie(草莓)对象:

Apple[] apples = new Apple[ 1 ];
Fruit[] fruits = apples;
fruits[ 0 ] = new Strawberry();

这样写真的可以编译,但是在运行时抛出ArrayStoreException异常。因为数组的这特点,在存储数据的操作上,Java运行时需要检查类型的兼容性。这种检查,很显然,会带来一定的性能问题,你需要明白这一点。

重申一下,泛型使用起来更安全,能“纠正”Java数组中这种类型上的缺陷。

现在估计你会感到很奇怪,为什么在数组上会有这种类型和子类型的关系,我来给你一个《Java Generics and Collections》这本书上给出的答案:如果它们不相关,你就没有办法把一个未知类型的对象数组传入一个方法里(不经过每次都封装成 Object[]),就像下面的:

void sort(Object[] o);

泛型出现后,数组的这个个性已经不再有使用上的必要了(下面一部分我们会谈到这个),实际上是应该避免使用

相关推荐:

Java中的访问修饰符详细解析

详解Java Reference源码分析代码

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

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