Home  >  Article  >  Java  >  Java Programming Thoughts Learning Class (3) Chapter 15 - Generics

Java Programming Thoughts Learning Class (3) Chapter 15 - Generics

php是最好的语言
php是最好的语言Original
2018-08-09 14:39:022330browse

The concept of generics is one of the major changes in Java SE5. Generics implement the concept of parameterized types (parameterized types), allowing code to be applied to multiple types. The term "generic" means: "applicable to many, many types."

1 Generic method

 Generic method has nothing to do with whether the class in which it is located is generic. That is, the class in which the generic method is located can be a generic class or not. type type.

  • Generic methods allow the method to change independently of the class.

  • A basic guideline: Whenever you can, you should use generic methods. That is, if using a generic method can replace genericizing the entire class, then you should only use the generic method because it makes things clearer.

  • For a static method, the type parameters of the generic class cannot be accessed, so if the static method needs to use generic capabilities , you must make it a generic method.

  • To define a generic method, simply place the generic parameter list before the return value.

1.1 Type parameter inference

 When using generic methods, it is usually not necessary to specify the parameter type, because the compiler will find out the specific type for us. This is called type argument inference.

Type inference is only valid for assignment operations
    .
  • If the result of a generic method call is passed as a

    parameter
  • to another method, the compiler will not
  • perform type inference

    . 1.1.1 Explicit type specification

  • Insert angle brackets between the dot operator and the method name, and then place the type within the angle brackets, that is,
Explicit type specification

.

2 The mystery of erasure

According to the description of the JDK documentation,

Class.getTypeParameters()

will "return a TypeVariableObject array, representing type parameters with generic declaration...", this seems to imply that you may find parameter type information, but, as you can see from the output , all you can find is the identifier used as the parameter placeholder, which is not useful information. So, the harsh reality is:

Inside the generic code, no information about the generic parameter type

can be obtained. Thus, you can know information such as generic parameter identifiers and

generic type boundaries

- but you cannot know how to create a specific The actual type parameters of the instance. ..., it is the most fundamental issue that must be dealt with when working with Java generics. Java generics are implemented using erasure

, which means that when you use generics, any specific type information is erased ,

The only thing you know is that you are using an object. Therefore Listf7e83be87db5cd2d9a8a0b8117b38cd4 and Listc0f559cc8d56b43654fcbe4aa9df7b4a are actually the same type at runtime. Both forms are erased to their "native type, which is List.2.1 The C way 2.1.1 The following C template example:

How does it know that the

f()
method exists for the type parameter T? ?When you
instantiate
this template, the C compiler will check, so at the moment

Manipulatorda98a08537ace02ebe5efb4a0b237f4a is instantiated, it sees that HasF has a method f(). If this is not the case, you will get a compile-time error , so type safety is guaranteed.

// Templates.cpp#include <iostream>using namespace std;template<class T> class Manipulator{
    T obj;public:
    Manipulator(T x) { obj = x; }    void manipulate() { obj.f(); }
};class HasF{public:    void f() { cout << "HasF::f()" << endl; }
};int main(){
    HasF hf;
    Manipulator<HasF> manipulator(hf);
    manipulator.manipulate();
}
2.1.2 Translated into Java, it will not compile.

Due to erasure , the Java compiler cannot map the requirement that manipulate() must be able to call f() on obj to the fact that HasF has f().
2.2 Generic Boundaries

In order to call f(), we must

assist the

generic class, given the

boundaries of the generic class, This tells the compiler that it can only accept types that follow this boundary. Because of the boundary, the following code can be compiled.

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();
    }
}

2.3 擦除

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

2.4 擦除的问题

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

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

擦除的代码是显著的。

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

class Foo<T>{ T var; }

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

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

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

class GenericBase<T>{}class Derived1<T> extends GenericBase<T>{}class Derived2 extends GenericBase{} // No warning

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>();

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

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);
    }
}

5.1 编译器有多聪明

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

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

5.2 逆变(Contravariance)

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

  • 参数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
    }
}
  • 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();
    }
}

5.3 无界通配符(Unbounded wildcards)

  原生泛型HolderHolder6b3d0130bba23ae47fe2b8e8cddf0195

原生Holder将持有任何类型的组合,而Holder6b3d0130bba23ae47fe2b8e8cddf0195将持有具有某种具体类型同构集合,因此不能只是向其中传递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);
    }
}

6 问题

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

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

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

  • 基类劫持了接口

7 总结

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

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

相关文章:

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

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

The above is the detailed content of Java Programming Thoughts Learning Class (3) Chapter 15 - Generics. For more information, please follow other related articles on the PHP Chinese website!

Statement:
The content of this article is voluntarily contributed by netizens, and the copyright belongs to the original author. This site does not assume corresponding legal responsibility. If you find any content suspected of plagiarism or infringement, please contact admin@php.cn