Maison > Java > javaDidacticiel > Cours d'apprentissage sur les pensées de programmation Java (3) Chapitre 15 - Génériques

Cours d'apprentissage sur les pensées de programmation Java (3) Chapitre 15 - Génériques

php是最好的语言
Libérer: 2018-08-09 14:39:02
original
2368 Les gens l'ont consulté

Le concept de génériques (génériques) est l'un des changements majeurs de Java SE5. Les génériques implémentent le concept de types paramétrés (types paramétrés), permettant d'appliquer du code à plusieurs types. Le terme « générique » signifie : « applicable à de très nombreux types ».

1 Méthode générique

  Cela n'a rien à voir avec le fait que la classe dans laquelle se trouve la méthode générique soit générique, c'est-à-dire que la classe dans laquelle se trouve la méthode générique peut être un type de classe générique ou non.

  • Les méthodes génériques permettent à la méthode de changer indépendamment de la classe.

  • Un principe directeur de base : chaque fois que vous le pouvez, vous devriez essayer d'utiliser des méthodes génériques. Autrement dit, si l’utilisation d’une méthode générique peut remplacer la généralisation de la classe entière, vous ne devez utiliser que la méthode générique car elle rend les choses plus claires.

  • Pour une méthode static, les paramètres de type de la classe générique ne sont pas accessibles, donc si la méthode static doit utiliser des capacités génériques, elle doit être rendue méthode générique.

  • Pour définir une méthode générique, il suffit de mettre la liste des paramètres génériques avant la valeur de retour.

1.1 Inférence de paramètre de type

  Lors de l'utilisation de méthodes génériques, il n'est généralement pas nécessaire de spécifier le type de paramètre, car le compilateur découvrira le type spécifique pour nous. C’est ce qu’on appelle l’inférence d’argument de type.

L'inférence de type n'est valable que pour les opérations d'affectation
    .
  • Si le résultat d'un appel de méthode générique est transmis en tant que

    paramètre
  • à une autre méthode, le compilateur
  • n'effectuera pas d'inférence de type

    . 1.1.1 Spécification de type explicite

  •   Insérez des crochets angulaires entre l'opérateur point et le nom de la méthode, puis placez le type entre les crochets angulaires, c'est-à-dire
Spécification de type explicite

.

2 Le mystère de l'effacement

Selon la documentation du JDK,

"renverra un

Class.getTypeParameters() Tableau de objets représentant des paramètres de type avec une déclaration générique..." Cela semble impliquer que vous pouvez trouver des informations sur les types de paramètres, mais, comme vous pouvez le voir sur la sortie, tout ce que vous pouvez trouver est utilisé comme Paramètre L'identifiant TypeVariablede l'espace réservé n'est pas une information utile. La dure réalité est donc la suivante :

À l'intérieur du code générique, il n'y a aucun moyen d'obtenir des informations sur le type du paramètre générique

. Ainsi, vous pouvez connaître des choses comme les identifiants de paramètres génériques et les

limites de type génériques

- mais vous ne pouvez pas savoir comment créer un type réel spécifique. paramètres de l'instance . …, c'est le problème le plus fondamental auquel vous devez faire face lorsque vous travaillez avec des génériques Java. Les génériques Java sont implémentés en utilisant erasure, ce qui signifie que lorsque vous utilisez des génériques, toute information de type spécifique est effacée,

la seule chose que vous savez est que vous utilisez un objet

. Ainsi, et sont en fait List<String> du même type au moment de l'exécution. Les deux formulaires sont effacés vers leurs "List<Integer>types natifs, c'est-à-dire .2.1 Méthode C++ List 2.1.1 L'exemple de modèle C++ suivant :

Comment sait-il que la méthode

existe pour le paramètre de type T Lorsque vous

instanciez
ce modèle, le compilateur C++ ? va vérifier, donc pour le moment
est instancié par

, il voit que f() a une méthode Si ce n'est pas le cas, vous obtiendrez un Manipulator<HasF>. erreur de compilation, afin que la sécurité du type soit garantie 2.1.2 Traduit en Java, il ne sera pas compilé HasFf()En raison de l'effacement, le. Le compilateur Java ne peut pas mapper l'exigence selon laquelle manipul() doit pouvoir appeler f() sur obj à HasF. Le fait que f() > Classe générique, étant donné les limites de la classe générique <.>, cela indique au compilateur qu'il ne peut accepter que les types qui suivent cette limite En raison des limites, le code suivant sera compilé

.
package net.mrliuli.generics.erase;/**
 * Created by li.liu on 2017/12/7.
 *//**
 * 由于有了擦除,Java编译器无法将manipulate()必须能够在obj上调用f()这一需求映射到HasF拥有f()这一事实上。
 * @param <T>
 */class Manipulator<T>{    private T obj;    public Manipulator(T x){ obj = x; }    // Error: Cannot resolve method &#39;f()&#39;
    //public void manipulate(){ obj.f(); }}/**
 * 为了调用f(),我们必须协助泛型类,给定泛型类的边界,以此告知编译器只能接受遵循这个边界的类型。由于有了边界,下面的代码就可以编译了。
 * @param <T>
 */class Manipulator2<T extends HasF>{    private T obj;    public Manipulator2(T x){ obj = x; }    public void manipulate(){ obj.f(); }
}public class Manipulation {
    public static void main(String[] args){
        HasF hf = new HasF();
        Manipulator<HasF> manipulator = new Manipulator<>(hf);        //manipulator.manipulate();
        Manipulator2<HasF> manipulator2 = new Manipulator2<>(hf);
        manipulator2.manipulate();
    }
}
Copier après la connexion

2.3 擦除

我们说泛型类型参数将擦除到它的第一个边界(它可能会有多个边界),我们还提到了类型参数的擦除。编译器实际上会把类型参数替换为它的擦除,就像上面的示例一样。T 擦除到HasF,就好像在类的声明中用 HasF 替换T 一样。

2.4 擦除的问题

擦除的核心动机是它使得泛化的客户端可以用非泛化的类库来使用,反之亦然,这经常被称为迁移兼容性

因此,擦除主要的正当理由是从非泛化的代码到泛化的代码的转变过程,以及在不破坏现有类库的情况下,将泛型融入Java语言。

擦除的代码是显著的。

  如果编写了下面这样的代码:

class Foo<T>{ T var; }
Copier après la connexion

  那么看起来当你在创建Foo的实例时:

Foo<Cat> f = new Foo<Cat>();
Copier après la connexion
  • class Foo中的代码应该知道现在工作于Cat之上,而泛型语法也强烈暗示:在整个类中的各个地方,类型T都在被替换。但是事实上并非如此,无论何时,当你在编写这个类的代码时,必须提醒自己:“不,它只是一个Object。”

  • 擦除和迁移兼容性意味着,使用泛型并不是强制的。

class GenericBase<T>{}class Derived1<T> extends GenericBase<T>{}class Derived2 extends GenericBase{} // No warning
Copier après la connexion

2.5 边界处的动作

  • 即使擦除在方法或类内部移除了有关实际类型的信息,编译器仍旧可以确保在方法或类中使用的类型的内部一致性

  • 因为擦除在方法体中移除了类型信息,所以在运行时的问题就是边界即对象进入和离开方法的地点。这些正是编译器在编译期执行类型检查插入转型代码的地点。

  • 在泛型中的所有动作都发生在边界处——对传递进来的值进行额外的编译期检查,并插入对传递出去的值的转型。这有助于澄清对擦除的混淆,记住,“边界就是发生动作的地方。”

3 擦除的补偿(Compensating for erasure)

有时必须通过引入类型标签(type tag)来对擦除进行补偿(compensating)。这意味着你需要显示地传递你的类型的Class对象,以便你可以在类型表达式中使用它。

  • 创建类型实例

  • 泛型数组

4 边界(bound)

  • 边界使得你可以在用于泛型的参数类型上设置限制条件。尽管这使得你可以强制规定泛型可以应用的类型,但是其潜在的一个更重要的效果是你可以按照自己的边界类型来调用方法

  • 因为擦除移除了类型信息,所以,可以用无界泛型参数调用的方法只是那些可以用Object调用的方法。

  • 但是,如果能够将这个参数限制为某个类型子集,那么你就可以用这些类型子集来调用方法。

  • 通配符被限制为单一边界

5 通配符(wildcards)

  • 数组的一种特殊行为
      可以将子类型的数组赋给基类型的数组引用。然后编译期数组元素可以放置基类型及其子类型的元素,即编译时不报错,但运行时的数组机制知道实际的数组类型是子类,因此会在运行时检查放置的类型是否是实际类型及其再导出的子类型,不是则抛出java.lang.ArrayStoreException异常。

  • 容器的类型与容器持有的类型

// Compile Error: incompatible types:List<Fruit> list = new ArrayList<Apple>();
Copier après la connexion

  与数组不同,泛型没有内建的协变类型。即*协变性对泛型不起作用

package net.mrliuli.generics.wildcards;import java.util.*;/**
 * Created by leon on 2017/12/8.
 */public class GenericsAndCovariance {
    public static void main(String[] args){        // Compile Error: incompatible types:
        //List<Fruit> list = new ArrayList<Apple>();

        // Wildcards allow covariance:
        List<? extends Fruit> flists = new ArrayList<Apple>();        // But, 编译器并不知道flists持有什么类型对象。实际上上面语句使得向上转型,丢失掉了向List中传递任何对象的能力,甚至是传递Object也不行。
        //flists.add(new Apple());
        //flists.add(new Fruit());
        //flists.add(new Object());

        flists.add(null);   // legal but uninteresting
        // We know that it returns at least Fruit:
        Fruit f = flists.get(0);
    }
}
Copier après la connexion

5.1 编译器有多聪明

  • 对于 List<? extends Fruit>set() 方法不能工作于 AppleFruit,因为 set() 的参数也是 ? extends Furit,这意味着它可以是任何事物,而编译器无法验证“任何事物”的类型安全性。

  • 但是,equals() 方法工作良好,因为它将接受Object类型而并非T类型的参数。因此,编译器只关注传递进来和要返回的对象类型,它并不会分析代码,以查看是否执行了任何实际的写入和读取操作。

5.2 逆变(Contravariance)

  • 使用超类型通配符。声明通配符是由某个特定类的任何基类界定的,方法是指定<? super MyClass>,甚至或者使用类型参数:<? super T>。这使得你可以安全地传递一个类型对象到泛型类型中。

  • 参数apples是Apple的某种基类型的List,这样你就知道向其中添加Apple或Apple的子类型是安全的。

package net.mrliuli.generics.wildcards;import java.util.*;public class SuperTypeWildcards {
    /**
     * 超类型通配符使得可以向泛型容器写入。超类型边界放松了在可以向方法传递的参数上所作的限制。
     * @param apples    参数apples是Apple的某种基类型的List,这样你就知道向其中添加Apple或Apple的子类型是安全的。
     */
    static void writeTo(List<? super Apple> apples){
        apples.add(new Apple());
        apples.add(new Jonathan());        //apples.add(new Fruit());    // Error
    }
}
Copier après la connexion
  • GenericWriting.java 中 writeExact(fruitList, new Apple()); 在JDK1.7中没有报错,说明进入泛型方法 writeExact()T 被识别为 Fruit,书中说报错,可能JDK1.5将 T 识别为 Apple

package net.mrliuli.generics.wildcards;import java.util.*;/**
 * Created by li.liu on 2017/12/8.
 */public class GenericWriting {
    static <T> void writeExact(List<T> list, T item){
        list.add(item);
    }    static List<Apple> appleList = new ArrayList<Apple>();    static List<Fruit> fruitList = new ArrayList<Fruit>();    static void f1(){
        writeExact(appleList, new Apple());
        writeExact(fruitList, new Apple());
    }    static <T> void writeWithWildcard(List<? super T> list, T item){
        list.add(item);
    }    static void f2(){
        writeWithWildcard(appleList, new Apple());
        writeWithWildcard(fruitList, new Apple());
    }    public static void main(String[] args){
        f1();
        f2();
    }
}
Copier après la connexion

5.3 无界通配符(Unbounded wildcards)

  原生泛型HolderHolder<?>

原生Holder将持有任何类型的组合,而Holder<?>将持有具有某种具体类型同构集合,因此不能只是向其中传递Object。

5.4 捕获转换

以下示例,被称为捕获转换,因为未指定的通配符类型被捕获,并被转换为确切类型。参数类型在调用f2()的过程中被捕获,因此它可以在对f1()的调用中被使用。

package net.mrliuli.generics.wildcards;/**
 * Created by leon on 2017/12/9.
 */public class CaptureConversion {
    static <T> void f1(Holder<T> holder){
        T t = holder.get();
        System.out.println(t.getClass().getSimpleName());
    }    static void f2(Holder<?> holder){
        f1(holder);     // Call with captured type
    }    public static void main(String[] args){
        Holder raw = new Holder<Integer>(1);
        f1(raw);
        f2(raw);
        Holder rawBasic = new Holder();
        rawBasic.set(new Object());
        f2(rawBasic);
        Holder<?> wildcarded = new Holder<Double>(1.0);
        f2(wildcarded);
    }
}
Copier après la connexion

6 问题

  • 基本类型不能作为类型参数

  • 由于探险,一个类不能实现同一个泛型接口的两种变体

  • 由于擦除,通过泛型来重载方法将产生相同的签名,编译出错,不能实现重载

  • 基类劫持了接口

7 总结

我相信被称为泛型的通用语言特性(并非必须是其在Java中的特定实现)的目的在于可表达性,而不仅仅是为了创建类型安全的容器。类型安全的容器是能够创建更通用代码这一能力所带来的副作用。

泛型正如其名称所暗示的:它是一种方法,通过它可以编写出更“泛化”的代码,这些代码对于它们能够作用的类型有更少的限制,因此单个的代码段能够应用到更多的类型上

相关文章:

Java编程思想学习课时(一):第1~13、16章

Java编程思想学习课时(二)第14章-类型信息

Ce qui précède est le contenu détaillé de. pour plus d'informations, suivez d'autres articles connexes sur le site Web de PHP en chinois!

Étiquettes associées:
source:php.cn
Déclaration de ce site Web
Le contenu de cet article est volontairement contribué par les internautes et les droits d'auteur appartiennent à l'auteur original. Ce site n'assume aucune responsabilité légale correspondante. Si vous trouvez un contenu suspecté de plagiat ou de contrefaçon, veuillez contacter admin@php.cn
Tutoriels populaires
Plus>
Derniers téléchargements
Plus>
effets Web
Code source du site Web
Matériel du site Web
Modèle frontal