In diesem Artikel werden hauptsächlich die Verwendung von Java-Generika und Probleme im Zusammenhang mit der Typlöschung vorgestellt. Hat einen sehr guten Referenzwert. Werfen wir einen Blick mit dem Editor unten
Einführung
Java hat den generischen Mechanismus in 1.5 eingeführt, das heißt parametrisierte Typen Der Typ der Variablen ist ein Parameter, der bei Verwendung als spezifischer Typ angegeben wird. Generika können für Klassen, Schnittstellen und Methoden verwendet werden. Durch die Verwendung von Generika kann der Code einfacher und sicherer gemacht werden. Allerdings nutzen Generika in Java die Typlöschung, es handelt sich also lediglich um Pseudo-Generika. Dieser Artikel fasst die Verwendung von Generika und bestehende Probleme zusammen und bezieht sich hauptsächlich auf „Gedanken zur Java-Programmierung“.
Die anderen beiden Artikel in dieser Reihe:
Zusammenfassung von Java Generics (2): Generics and Arrays
Zusammenfassung von Java Generics (3): Verwendung von Platzhaltern
Grundlegende Verwendung
Generische Klassen
Wenn es einen Klassenhalter gibt, der zum Umschließen einer Variablen verwendet wird, kann der Typ dieser Variablen beliebig sein. Wie schreibe ich den Halter? Bevor es Generika gab, konnten Sie Folgendes tun:
public class Holder1 { private Object a; public Holder1(Object a) { this.a = a; } public void set(Object a) { this.a = a; } public Object get(){ return a; } public static void main(String[] args) { Holder1 holder1 = new Holder1("not Generic"); String s = (String) holder1.get(); holder1.set(1); Integer x = (Integer) holder1.get(); } }
In Holder1 gibt es eine Variable, auf die von Object verwiesen wird. Da jeder Typ in ein Objekt umgewandelt werden kann, kann dieser Halter jeden Typ akzeptieren. Beim Herausnehmen weiß Holder nur, dass es ein Objekt Objekt speichert, sodass es in den entsprechenden Typ gezwungen werden muss. In der Hauptmethode speichert Inhaber1 zunächst einen String, der ein String-Objekt ist, und speichert dann ein Integer-Objekt (Parameter 1 wird automatisch eingerahmt). Beim Herausnehmen von Variablen aus dem Holder ist das Casting bereits mühsam. Auch hier müssen Sie sich verschiedene Typen merken. Wenn Sie einen Fehler machen, kommt es zu einer Laufzeitausnahme.
Werfen wir einen Blick auf die generische Version von Holder:
public class Holder2<T> { private T a; public Holder2(T a) { this.a = a; } public T get() { return a; } public void set(T a) { this.a = a; } public static void main(String[] args) { Holder2<String> holder2 = new Holder2<>("Generic"); String s = holder2.get(); holder2.set("test"); holder2.set(1);//无法编译 参数 1 不是 String 类型 } }
In Holder2 ist die Variable a ein parametrisierter Typ T. T ist nur ein Bezeichner, und andere Buchstaben können es auch sein gebraucht. Beim Erstellen des Holder2-Objekts wird der Typ des Parameters T in spitzen Klammern übergeben. In diesem Objekt entsprechen dann alle Vorkommen von T dem Ersetzen durch String. Was get jetzt herausnimmt, ist kein Objekt, sondern ein String-Objekt, sodass keine Typkonvertierung erforderlich ist. Darüber hinaus kann beim Aufrufen von set nur der String-Typ übergeben werden, da sonst die Kompilierung nicht erfolgreich ist. Dies gewährleistet die Typensicherheit im Halter2 und verhindert, dass versehentlich ein falscher Typ eingegeben wird.
Anhand des obigen Beispiels können wir sehen, dass pan den Code einfacher und sicherer macht. Nach der Einführung von Generika wurden auch einige Klassen in der Java-Bibliothek neu geschrieben, um Generika zu unterstützen. Wenn wir sie verwenden, übergeben wir Parametertypen wie: ArrayListc0f559cc8d56b43654fcbe4aa9df7b4a ;>();.
Generische Methoden
Generika können nicht nur auf Klassen abzielen, sondern eine Methode auch individuell generisch machen. Zum Beispiel:
public class GenericMethod { public <K,V> void f(K k,V v) { System.out.println(k.getClass().getSimpleName()); System.out.println(v.getClass().getSimpleName()); } public static void main(String[] args) { GenericMethod gm = new GenericMethod(); gm.f(new Integer(0),new String("generic")); } } 代码输出: Integer String
Die GenericMethod Die Klasse selbst ist nicht generisch. Beim Erstellen ihres Objekts müssen keine generischen Parameter übergeben werden, aber ihre Methode f ist eine generische Methode. Vor dem Rückgabetyp steht seine Parameterkennung b77a8d9c3c319e50d4b02a976b347910. Beachten Sie, dass es hier zwei generische Parameter gibt, sodass es mehrere generische Parameter geben kann.
Beim Aufruf einer generischen Methode müssen Sie die generischen Parameter nicht explizit übergeben, was im obigen Aufruf nicht der Fall ist. Dies liegt daran, dass der Compiler die Parametertypinferenz verwendet, um die Typen von K und V basierend auf den Typen der übergebenen Argumente (hier Ganzzahl und String) abzuleiten.
Typlöschung
Was ist Typlöschung?
Java-Generika verwenden den Typlöschmechanismus löste viele Kontroversen aus, so sehr, dass die generischen Funktionen von Java eingeschränkt wurden und nur noch als „pseudo-generisch“ bezeichnet werden können. Was ist Typlöschung? Vereinfacht ausgedrückt existieren Typparameter nur zur Kompilierungszeit. Zur Laufzeit kennt die Java Virtual Machine (JVM) die Existenz von Generika nicht. Schauen wir uns zunächst ein Beispiel an:
public class ErasedTypeEquivalence { public static void main(String[] args) { Class c1 = new ArrayList<String>().getClass(); Class c2 = new ArrayList<Integer>().getClass(); System.out.println(c1 == c2); } }
Der obige Code hat zwei verschiedene ArrayLists: ArrayListc0f559cc8d56b43654fcbe4aa9df7b4a Unserer Meinung nach sind ihre parametrisierten Typen unterschiedlich, einer speichert Ganzzahlen und der andere speichert Zeichenfolgen. Durch den Vergleich ihrer Klassenobjekte ist die obige Codeausgabe jedoch wahr. Dies bedeutet, dass sie aus Sicht der JVM derselben Klasse angehören. In C++, C# und anderen Sprachen, die echte Generika unterstützen, handelt es sich um unterschiedliche Klassen.
Der generische Parameter wird bis zu seiner ersten Grenze gelöscht. Wenn der Parametertyp beispielsweise in der Klasse Holder2 ein einzelnes T ist, wird er in Object gelöscht, was allen Stellen entspricht, an denen T erscheint. Durch Objekt ersetzen. Aus Sicht der JVM ist die gespeicherte Variable a also immer noch vom Typ Object. Der Grund, warum es automatisch herausgenommen wird, ist der von uns übergebene Parametertyp. Dies liegt daran, dass der Compiler den Typkonvertierungscode in die kompilierte Bytecode-Datei einfügt und wir ihn nicht manuell konvertieren müssen. Wenn der Parametertyp Grenzen hat, löschen Sie ihn bis zu seiner ersten Grenze, was im nächsten Abschnitt besprochen wird.
擦除带来的问题
擦除会出现一些问题,下面是一个例子:
class HasF { public void f() { System.out.println("HasF.f()"); } } public class Manipulator<T> { private T obj; public Manipulator(T obj) { this.obj = obj; } public void manipulate() { obj.f(); //无法编译 找不到符号 f() } public static void main(String[] args) { HasF hasF = new HasF(); Manipulator<HasF> manipulator = new Manipulator<>(hasF); manipulator.manipulate(); } }
上面的 Manipulator 是一个泛型类,内部用一个泛型化的变量 obj,在 manipulate 方法中,调用了 obj 的方法 f(),但是这行代码无法编译。因为类型擦除,编译器不确定 obj 是否有 f() 方法。解决这个问题的方法是给 T 一个边界:
class Manipulator2<T extends HasF> { private T obj; public Manipulator2(T x) { obj = x; } public void manipulate() { obj.f(); } }
现在 T 的类型是 1f179c3e268e631bc7ed98c5289251b7,这表示 T 必须是 HasF 或者 HasF 的导出类型。这样,调用 f() 方法才安全。HasF 就是 T 的边界,因此通过类型擦除后,所有出现 T 的
地方都用 HasF 替换。这样编译器就知道 obj 是有方法 f() 的。
但是这样就抵消了泛型带来的好处,上面的类完全可以改成这样:
class Manipulator3 { private HasF obj; public Manipulator3(HasF x) { obj = x; } public void manipulate() { obj.f(); } }
所以泛型只有在比较复杂的类中才体现出作用。但是像 1f179c3e268e631bc7ed98c5289251b7 这种形式的东西不是完全没有意义的。如果类中有一个返回 T 类型的方法,泛型就有用了,因为这样会返回准确类型。比如下面的例子:
class ReturnGenericType<T extends HasF> { private T obj; public ReturnGenericType(T x) { obj = x; } public T get() { return obj; } }
这里的 get() 方法返回的是泛型参数的准确类型,而不是 HasF。
类型擦除的补偿
类型擦除导致泛型丧失了一些功能,任何在运行期需要知道确切类型的代码都无法工作。比如下面的例子:
public class Erased<T> { private final int SIZE = 100; public static void f(Object arg) { if(arg instanceof T) {} // Error T var = new T(); // Error T[] array = new T[SIZE]; // Error T[] array = (T)new Object[SIZE]; // Unchecked warning } }
通过 new T() 创建对象是不行的,一是由于类型擦除,二是由于编译器不知道 T 是否有默认的构造器。一种解决的办法是传递一个工厂对象并且通过它创建新的实例。
interface FactoryI<T> { T create(); } class Foo2<T> { private T x; public <F extends FactoryI<T>> Foo2(F factory) { x = factory.create(); } // ... } class IntegerFactory implements FactoryI<Integer> { public Integer create() { return new Integer(0); } } class Widget { public static class Factory implements FactoryI<Widget> { public Widget create() { return new Widget(); } } } public class FactoryConstraint { public static void main(String[] args) { new Foo2<Integer>(new IntegerFactory()); new Foo2<Widget>(new Widget.Factory()); } }
另一种解决的方法是利用模板设计模式:
abstract class GenericWithCreate<T> { final T element; GenericWithCreate() { element = create(); } abstract T create(); } class X {} class Creator extends GenericWithCreate<X> { X create() { return new X(); } void f() { System.out.println(element.getClass().getSimpleName()); } } public class CreatorGeneric { public static void main(String[] args) { Creator c = new Creator(); c.f(); } }
具体类型的创建放到了子类继承父类时,在 create 方法中创建实际的类型并返回。
总结
本文介绍了 Java 泛型的使用,以及类型擦除相关的问题。一般情况下泛型的使用比较简单,但是某些情况下,尤其是自己编写使用泛型的类或者方法时要注意类型擦除的问题。接下来会介绍数组与泛型的关系以及通配符的使用。
Das obige ist der detaillierte Inhalt vonZusammenfassung von Java Generics (1) – Detaillierte Erläuterung der grundlegenden Verwendung und Typlöschung. Für weitere Informationen folgen Sie bitte anderen verwandten Artikeln auf der PHP chinesischen Website!