Dieser Artikel bietet Ihnen eine detaillierte Analyse (Code) von Platzhaltern in Java. Ich hoffe, dass er für Freunde hilfreich ist.
Die Irrelevanz von Subtypen generischer Typen wurde im vorherigen Teil dieses Artikels erwähnt. Aber manchmal möchten wir in der Lage sein, generische Typen wie gewöhnliche Typen zu verwenden:
◆ Upcasting einer Referenz auf ein generisches Objekt
◆ Downcasting einer Referenz auf ein generisches Objekt
Upcast einen Verweis auf ein generisches Objekt
Angenommen, wir haben viele Kisten, die jeweils unterschiedliche Früchte enthalten, und wir müssen einen universellen Weg finden, um jede Kiste mit Obst zu handhaben. Allgemeiner gesagt ist A ein Untertyp von B, und wir müssen einen Weg finden, eine Instanz vom Typ C einer Deklaration vom Typ C zuzuweisen.
Um dies zu erreichen, müssen wir eine Erweiterungsdeklaration mit Platzhaltern verwenden, wie im folgenden Beispiel:
List<Apple> apples = new ArrayList<Apple>(); List<? extends Fruit> fruits = apples;
„? erweitert“ ist eine Subtypabhängigkeit eines generischen Typs. In die Realität umgesetzt: Apple ist ein Untertyp von Fruit und List
Einen Verweis auf ein generisches Objekt ablehnen
Jetzt möchte ich einen weiteren Platzhalter einführen: ? Super. Wenn Typ B ein Supertyp (übergeordneter Typ) von Typ A ist, dann ist C ein Subtyp von C<:>List<Fruit> fruits = new ArrayList<Fruit>();
List<? super Apple> = fruits;
Warum funktioniert die Verwendung von Platzhaltermarkierungen?
Das Prinzip ist jetzt klar: Wie nutzen wir diese neue grammatikalische Struktur?
?extends
Lassen Sie uns ein in diesem zweiten Teil verwendetes Beispiel noch einmal betrachten, in dem es um Subtypabhängigkeiten von Java-Arrays geht:
Apple[] apples = new Apple[ 1 ]; Fruit[] fruits = apples; fruits[ 0 ] = new Strawberry();
Wie wir sehen können Wenn Sie ein Strawberry-Objekt zu einem als Fruit-Array deklarierten Apple-Objektarray hinzufügen, kann der Code kompiliert werden, es wird jedoch zur Laufzeit eine Ausnahme ausgelöst.
Jetzt können wir Platzhalter verwenden, um den relevanten Code in Generika umzuwandeln: Da Apple eine Unterklasse von Fruit ist, verwenden wir den Platzhalter ? erweitert, damit die Definition eines List
List<Apple> apples = new ArrayList<Apple>(); List<? extends Fruit> fruits = apples; fruits.add( new Strawberry());
Dieses Mal kann der Code nicht kompiliert werden! Der Java-Compiler verhindert, dass Sie Erdbeeren zu einer Obstliste hinzufügen. Wir können den Fehler zur Kompilierungszeit erkennen und müssen zur Laufzeit nicht prüfen, um sicherzustellen, dass inkompatible Typen zur Liste hinzugefügt werden. Selbst wenn Sie Fruit-Objekte zur Liste hinzufügen, funktioniert es nicht:
fruits.add( new Fruit());
Das ist nicht möglich. Tatsächlich können Sie keine Werte in eine Datenstruktur schreiben, die ?extends verwendet.
Der Grund ist ganz einfach, Sie können es sich so vorstellen: Dieser Platzhalter „? erweitert T“ teilt dem Compiler mit, dass wir es mit einem Subtyp vom Typ T zu tun haben, aber wir wissen nicht, um welchen Subtyp es sich handelt. Da es keine Möglichkeit gibt, sicher zu sein, dürfen wir zur Gewährleistung der Typsicherheit keine Daten dieses Typs hinzufügen. Andererseits wissen wir, dass es sich, egal um welchen Typ es handelt, immer um einen Untertyp vom Typ T handelt. Wenn wir Daten lesen, können wir sicherstellen, dass die Daten, die wir erhalten, eine Instanz vom Typ T sind:
Fruit get = fruits.get( 0 );
? super
Was ist die allgemeine Verwendung von Super? Schauen wir uns das zuerst an:
List<Fruit> fruits = new ArrayList<Fruit>(); List<? super Apple> = fruits;
Wir sehen, dass Fruits auf eine Liste verweist, die einige der Supertypen von Apple enthält. Auch hier wissen wir nicht genau, was die Superklasse ist, aber wir wissen, dass Apple und alle Unterklassen von Apple mit diesem Typ kompatibel sind. Da es sich bei diesem unbekannten Typ um Apple und die Superklasse von GreenApple handelt, können wir schreiben:
fruits.add( new Apple()); fruits.add( new GreenApple());
Wenn wir die Superklasse von Apple hinzufügen möchten, warnt Sie der Compiler:
fruits.add( new Fruit()); fruits.add( new Object());
Da wir nicht Da ich nicht weiß, um welche Art von Superklasse es sich handelt, dürfen nicht alle derartigen Instanzen hinzugefügt werden.
Wie wäre es mit dem Abrufen von Daten dieses Typs? Es stellt sich heraus, dass Sie nur eine Object-Instanz herausnehmen können: Da wir nicht wissen, was die Superklasse ist, kann der Compiler nur garantieren, dass es sich um ein Object handelt, da Object die Superklasse jedes Java-Typs ist.
Zugriffsprinzip und PECS-Gesetz
Wenn wir die Eigenschaften von ?extends und dem ?Super-Wildcard zusammenfassen, können wir folgende Schlussfolgerungen ziehen:
◆ Wenn Wenn Sie Daten aus einem Datentyp abrufen möchten, verwenden Sie den Platzhalter „?“
◆ Wenn Sie das Objekt in eine Datenstruktur schreiben möchten, verwenden Sie den Platzhalter „?“. Um beides zu speichern, verwenden Sie keine Platzhalter, wenn Sie es dennoch übernehmen möchten.
So nennt Maurice Naftalin in seinem Buch „Java Generics and Collections“ das Zugriffsprinzip und Joshua Bloch in seinem Buch „Effective Java“ die PECS-Regel.
Bloch erinnerte daran, dass PECS für „Producer Extends, Consumer Super“ steht, was einfacher zu merken und zu verwenden ist.
Das Obige folgt von unten:
Das Java-Tutorial
Java Generics and Collections, von Maurice Naftalin und Philip Wadler
Effektive chinesische Java-Version ( 2. Auflage), von 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
例子
你既可以使用Java类库里提供的泛型类,也可以使用自己的泛型类。
类型安全的写入数据…
下面的这段代码是个例子,我们创建了一个List
List<String> str = new ArrayList<String>(); str.add( "Hello " ); str.add( "World." );
如果我们试图在List
str.add( 1 ); // 不能编译
类型安全的读取数据…
当我们在使用List
String myString = str.get( 0 );
遍历
类库中的很多类,诸如Iterator
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中,类型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
答案会出乎你的意料:没有任何关系。用更通俗的话,泛型类型跟其是否子类型没有任何关系。
这意味着下面的这段代码是无效的:
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);
泛型出现后,数组的这个个性已经不再有使用上的必要了(下面一部分我们会谈到这个),实际上是应该避免使用
相关推荐:
Das obige ist der detaillierte Inhalt vonDetaillierte Analyse von Platzhaltern in Java (Code). Für weitere Informationen folgen Sie bitte anderen verwandten Artikeln auf der PHP chinesischen Website!