模板是泛型编程的基础,泛型编程即以一种独立于任何特定类型的方式编写代码 泛型擦除

C++ 模板

 2015-09-08

C++ 模板

模板是泛型编程的基础,泛型编程即以一种独立于任何特定类型的方式编写代码。

模板是创建泛型类或函数的蓝图或公式。库容器,比如迭代器和算法,都是泛型编程的例子,它们都使用了模板的概念。

每个容器都有一个单一的定义,比如 向量,我们可以定义许多不同类型的向量,比如 vector <int> 或 vector <string>

您可以使用模板来定义函数和类,接下来让我们一起来看看如何使用。

函数模板

模板函数定义的一般形式如下所示:

template <class type> ret-type func-name(parameter list)
{
   // 函数的主体
}  

在这里,type 是函数所使用的数据类型的占位符名称。这个名称可以在函数定义中使用。

下面是函数模板的实例,返回两个数中的最大值:

#include <iostream>
#include <string>

using namespace std;

template <typename T>
inline T const& Max (T const& a, T const& b) 
{ 
    return a < b ? b:a; 
} 
int main ()
{
 
    int i = 39;
    int j = 20;
    cout << "Max(i, j): " << Max(i, j) << endl; 

    double f1 = 13.5; 
    double f2 = 20.7; 
    cout << "Max(f1, f2): " << Max(f1, f2) << endl; 

    string s1 = "Hello"; 
    string s2 = "World"; 
    cout << "Max(s1, s2): " << Max(s1, s2) << endl; 

   return 0;
}

 

当上面的代码被编译和执行时,它会产生下列结果:

Max(i, j): 39
Max(f1, f2): 20.7
Max(s1, s2): World

类模板

正如我们定义函数模板一样,我们也可以定义类模板。泛型类声明的一般形式如下所示:

template <class type> class class-name {
.
.
.
}

在这里,type 是占位符类型名称,可以在类被实例化的时候进行指定。您可以使用一个逗号分隔的列表来定义多个泛型数据类型。

下面的实例定义了类 Stack<>,并实现了泛型方法来对元素进行入栈出栈操作:

#include <iostream>
#include <vector>
#include <cstdlib>
#include <string>
#include <stdexcept>

using namespace std;

template <class T>
class Stack { 
  private: 
    vector<T> elems;     // 元素 

  public: 
    void push(T const&);  // 入栈
    void pop();               // 出栈
    T top() const;            // 返回栈顶元素
    bool empty() const{       // 如果为空则返回真。
        return elems.empty(); 
    } 
}; 

template <class T>
void Stack<T>::push (T const& elem) 
{ 
    // 追加传入元素的副本
    elems.push_back(elem);    
} 

template <class T>
void Stack<T>::pop () 
{ 
    if (elems.empty()) { 
        throw out_of_range("Stack<>::pop(): empty stack"); 
    }
	// 删除最后一个元素
    elems.pop_back();         
} 

template <class T>
T Stack<T>::top () const 
{ 
    if (elems.empty()) { 
        throw out_of_range("Stack<>::top(): empty stack"); 
    }
	// 返回最后一个元素的副本 
    return elems.back();      
} 

int main() 
{ 
    try { 
        Stack<int>         intStack;  // int 类型的栈 
        Stack<string> stringStack;    // string 类型的栈 

        // 操作 int 类型的栈 
        intStack.push(7); 
        cout << intStack.top() <<endl; 

        // 操作 string 类型的栈 
        stringStack.push("hello"); 
        cout << stringStack.top() << std::endl; 
        stringStack.pop(); 
        stringStack.pop(); 
    } 
    catch (exception const& ex) { 
        cerr << "Exception: " << ex.what() <<endl; 
        return -1;
    } 
}  

 

当上面的代码被编译和执行时,它会产生下列结果:

7
hello
Exception: Stack<>::pop(): empty stack

 

 

 

《java编程思想 泛型》鸭子类型 - 简书 https://www.jianshu.com/p/db4274701be3

Java 泛型 | 菜鸟教程 https://www.runoob.com/java/java-generics.html

Java 重写(Override)与重载(Overload) | 菜鸟教程 https://www.runoob.com/java/java-override-overload.html

 

面试官问我:“泛型擦除是什么,会带来什么问题?” https://juejin.im/post/6844904083199918093

 https://mp.weixin.qq.com/s/hihVKLi_2wZ3dF7O9NMauw

泛型趣谈

ImportNew 2017-12-30
 

来源:四火的唠叨 ,

www.raychase.net/1932

 

Java中的泛型带来了什么好处?规约。就像接口定义一样,可以帮助对于泛型类型和对象的使用上,保证类型的正确性。如果没有泛型的约束,程序员大概需要在代码里面使用大量的类型强制转换语句,而且需要非常清楚没有标注的对象实际类型,这是容易出错的、恼人的。但是话说回来,泛型可不只有规约,还有很多有趣的用法,容我一一道来。

 

泛型擦除

 

Java的泛型在编译阶段实际上就已经被擦除了(这也是它和C#泛型最本质的区别),也就是说,对于使用泛型的定义,对于编译执行的过程,并没有任何的帮助(有谁能告诉我为什么按着泛型擦除来设计?)。所以,单纯利用泛型的不同来设计接口,会遇到预期之外的问题,比如说:

 

public interface Builder<K,V> {

    public void add(List<K> keyList);

    public void add(List<V> valueList);

}

 

想这样设计接口?仅仅靠泛型类型的不同来设计重载接口?那是痴人说梦。但是如果代码变成这样呢?

 

public class GenericTypes {

 

    public static String method(List<String> list) {

        System.out.println("invoke method(List<String> list)");

        return "";

    }

 

    public static int method(List<Integer> list) {

        System.out.println("invoke method(List<Integer> list)");

        return 1;

    }

 

    public static void main(String[] args) {

        method(new ArrayList<String>());

        method(new ArrayList<Integer>());

    }

}

 

这个情况就有点特殊了,Sun的Javac编译器居然可以通过编译,而其它不行,这个例子来自IcyFenix的文章,有兴趣不妨移步参阅IcyFenix的文章以及下面的讨论。

 

http://icyfenix.iteye.com/blog/1021949

 

方法泛型

 

在JDK的java.util.List接口里面,定义了这样一个方法:

 

public interface List<E> extends Collection<E> {

    <T> T[] toArray(T[] a);

}

 

事实上,这个方法泛型T表示的是任意类型,它可是和此例中的接口/类泛型E毫不相干啊。

 

如果我去设计方法,我可能写成这样:

 

<T> T[] toArray();

 

其实这个T[ ] a参数的作用也容易理解:

 

  1. 确定了数组类型;

  2. 如果给定的数组a能够容纳得下结果,就会把结果放进a里面(JDK的注释有说明“If the list fits in the specified array, it is returned therein.”),同时也把a返回。

 

如果没有这个T[ ] a参数的话,光光定义一个方法泛型<T>是没有任何意义的,因为这个T是什么类型完全是无法预料的,例如:

 

public class Builder {

    public <E> E call(){

        return null;

    }

 

    public static void main(String[] args) {

        String s = new Builder().call(); // ①

        Integer i = new Builder().call(); // ②

        new Builder().<String>call(); // ③

    }

}

 

可以看到,call()方法返回的是类型E,这个E其实没有任何约束,它可以表示任何对象,但是代码上不需要强制转换就可以赋给String类型的对象s(①),也可以赋给Integer的对象i(②),甚至,你可以主动告知编译器返回的对象类型(③)。

 

链式调用

 

看看如下示例代码:

 

public class Builder<S> {

    public <E> Builder<E> change(S left, E right){

        // 省略实现

    }

 

    public static void main(String[] args) {

        new Builder<String>().change("3", 3).change(3, 3.0f).change(3.0f, 3.0d);

    }

}

 

同样一个change方法,接收的参数变来变去的,上例中方法参数从String-int变到int-float,再变为float-double,这样的泛型魔法在设计链式调用的方法的时候,特别是定义DSL语法的时候特别有用。

 

使用问号 

 

其实问号帮助表示的是“通配符类型”,通配符类型 List<?> 与原始类型 List 和具体类型 List<String>都不相同,List<?>表示这个list内的每个元素的类型都相同,但是这种类型具体是什么我们却不知道。注意,List<?>和List<Object>可不相同,由于Object是最高层的超类,List<Object>表示元素可以是任何类型的对象,但是List<?>可不是这个意思。

 

来看一段有趣的代码:

 

class Wrapper<E> {

    private E e;

    public void put(E e) {

        this.e = e;

    }

 

    public E get(){

        return e;

    }

}

 

public class Builder {

    public void check(Wrapper<?> wrapper){

        System.out.println(wrapper.get()); // ①

        wrapper.put(new Object()); // ② wrong!

        wrapper.put(wrapper.get()); // ③ wrong!

        wrapper.put(null); // ④ right!

    }

}

 

Wrapper的类定义里面指定了它包装了一个类型为E的对象,但是在另一个使用它的类Builder里面,指定了Wrapper的泛型参数是?,这就意味着这个被包装的对象的类型是完全不可知的:

 

  • 现在我可以调用Wrapper的get方法把对象取出来看看(①),

  • 但是我不能放任意类型确定的对象进去,Object也不行(②),

  • 即便是从wrapper里面get出来也不行(编译器太不聪明了是吧?③)

  • 可奇葩的是,放一个null是可以被允许的,因为null根本就不是任何一个类型的对象(④,注意,不能放int这类的原语类型,虽然它不是对象,但因为它会被自动装箱成Integer,从而变成具体类型,所以是会失败的)。

 

现在思考一下,如果要表示这个未知对象是某个类的子类,上面代码的Wrapper定义不变,但是check方法写成:

 

public void check(Wrapper<? extends String> wrapper){

    wrapper.put(new String());

}

 

这样呢?

 

……

 

依然报错,因为new String()确实是String的子类(或它自己)的对象,一点都没错,但是它可不见得和同为String子类(或它自己)的“?”属于同一个类型啊!

 

如果写成这样呢(注意extends变成了super)?

 

public void check(Wrapper<? super String> wrapper){

    wrapper.put(new String());

}

 

这次对了,为什么呢?

 

……

 

因为wrapper要求put的参数“?”必须是String的父类(或它自己),而不管这个类型如何变化,它一定是new String()的父类(或它自己)啊!

 

泛型递归

 

啥,泛型还能递归?当然能。而且这也是一种好玩的泛型使用:

 

class Wrapper<E extends Wrapper<E>> implements Comparable<Wrapper<E>> {

 

    @Override

    public int compareTo(Wrapper<E> wrapper) {

        return 0;

    }

 

}

 

好玩吧?泛型也能递归。这个例子指的是,一个对象E由包装器Wrapper所包装,但是,E也必须是一个包装器,这正是包装器的递归;同时,包装器也实现了一个比较接口,使得两个包装器可以互相比较大小。

 

别晕!泛型只不过是一个普普通通的语言特性,但是也挺有趣的。

 

【2014-1-9】补充,来自kidneyball的回复:

 

为什么要按着类型擦除来设计。据我所知,Java1.5引入泛型的最大压力来自于没有泛型的容器API相比起C++的标准模板库来太难用,太多不必要的显式转型,完全违背了DRY原则也缺乏精细的类型检查。但Java与C++不同,C++的对象没有公共父类,不使用泛型根本无法建立一个能存放所有类型的容器,所以必须在费大力气在编译后的运行代码中支持泛型,保留泛型信息自然是顺水推舟。而Java所有对象都有一个共同父类Object,当时已有的容器实现已经在运行期表现良好。所以Sun的考虑是加入一层简单的编译期泛型语法糖进行自动转换和类型检查,而在编译后的字节码中则擦除掉泛型信息,仍然走Object容器的旧路。这种升级方案对jdk的改动是最小的,Runtime根本不用改,改编译器就行了。

 

 

 [2005.11710] Featherweight Go https://arxiv.org/abs/2005.11710

 

 We describe a design for generics in Go inspired by previous work on Featherweight Java by Igarashi, Pierce, and Wadler. Whereas subtyping in Java is nominal, in Go it is structural, and whereas generics in Java are defined via erasure, in Go we use monomorphisation. Although monomorphisation is widely used, we are one of the first to formalise it. Our design also supports a solution to The Expression Problem.

 

Comparing Java and C# Generics - Jonathan Pryor's web log http://www.jprl.com/Blog/archive/development/2007/Aug-31.html

 

秒懂Java泛型_ShuSheng007的程序人生-CSDN博客 https://blog.csdn.net/ShuSheng0007/article/details/80720406

 

Java泛型原理解析

为什么人们会说Java的泛型是伪泛型呢,就是因为Java在编译时擦除了所有的泛型信息,所以Java根本不会产生新的类型到字节码或者机器码中,所有的泛型类型最终都将是一种原始类型,那样在Java运行时根本就获取不到泛型信息。

擦除

Java编译器编译泛型的步骤: 
1.检查泛型的类型 ,获得目标类型 
2.擦除类型变量,并替换为限定类型(T为无限定的类型变量,用Object替换) 
3.调用相关函数,并将结果强制转换为目标类型。

 ArrayList<String> arrayString=new ArrayList<String>();     
 ArrayList<Integer> arrayInteger=new ArrayList<Integer>();     
 System.out.println(arrayString.getClass()==arrayInteger.getClass()); 
  • 1
  • 2
  • 3

上面代码输入结果为 true,可见通过运行时获取的类信息是完全一致的,泛型类型被擦除了!

如何擦除: 
当擦除泛型类型后,留下的就只有原始类型了,例如上面的代码,原始类型就是ArrayList。擦除类型变量,并替换为限定类型(T为无限定的类型变量,用Object替换),如下所示

擦除之前:

//泛型类型  
class Pair<T> {    
    private T value;    
    public T getValue() {    
        return value;    
    }    
    public void setValue(T  value) {    
        this.value = value;    
    }    
} 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10

擦除之后:

//原始类型  
class Pair {    
    private Object value;    
    public Object getValue() {    
        return value;    
    }    
    public void setValue(Object  value) {    
        this.value = value;    
    }    
}  

 

 

因为在Pair<T>中,T是一个无限定的类型变量,所以用Object替换。如果是Pair<T extends Number>,擦除后,类型变量用Number类型替换。

与其他语言相比较

相比较Java,其模仿者C#在泛型方面无疑做的更好,其是真泛型。

C#泛型类在编译时,先生成中间代码IL,通用类型T只是一个占位符。在实例化类时,根据用户指定的数据类型代替T并由即时编译器(JIT)生成本地代码,这个本地代码中已经使用了实际的数据类型,等同于用实际类型写的类,所以不同的封闭类的本地代码是不一样的。其可以在运行时通过反射获得泛型信息,而且C#的泛型大大提高了代码的执行效率。

那么Java为什么不采用类似C#的实现方式呢,答案是:要向下兼容!兼容害死人啊。关于C#与Java的泛型比较,可以查看这篇文章:Comparing Java and C# Generics

 

 

 

 

 

 
posted @ 2018-10-22 08:24  papering  阅读(246)  评论(0编辑  收藏  举报