本文主要是介绍Java泛型的中庸之道,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!
代码组织和复用是所有计算机编程的基本手段:编写一次,多次使用,并在一个位置保存代码。—— 《Java编程思想》
面向对象的程序设计语言都有一种共有的复用方式——依赖继承体系的多态,来实现一种纵向复用。除此以外,诸如C++和C#还拥有不依赖继承体系的泛型,来实现一种横向复用。而Java,这个在其1.5版本才加入泛型特性的语言,为了平滑兼容已有的非泛型代码,其泛型依旧充满了继承结构的味道,只能说是一个瞻前顾后、遵循中庸之道的无奈的伪泛型。
以下我们只讲Java的泛型。
一、泛型的创建和使用
1.1、泛型创建和使用之间的矛盾
对于Java泛型,编写泛型类和泛型方法是相当简单的,但是编写出能够操作其泛型类型的泛化代码就需要额外的努力了,这些努力需要类创建者和类消费者共同付出,他们必须理解适配器设计模式的概念和实现。——《Java编程思想》
如上所言,创建泛型很容易:
class Holder<T> {T t;void set(T t){//...}T get(){return t;}
}
如上Holder类就是一个泛型类,但是因为类型擦除的原因,在set()方法的实现中,你只能将变量t当作Object来使用,只能调用Object类的方法,因此类创建者的设计将会无比受限。
在创建泛型时,可以设置一个泛型上界,从而在一定程序上减轻泛型实现时的限制:
class Holder<T extends Fruit> {T t;void set(T t){//...}T get(){return t;}
}
这时,泛型擦除到Fruit类型,在set()的方法体中你便可以将变量t当作Fruit来使用。但是,这个类的使用者,就需要让自己的类都继承于Fruit类,才能塞到Holder中去,这对类使用者来说是一定程度上的限制,该泛型的泛化程度也因此被限制了。
这就像一个天平,天平的左端是“泛化程度”,天平的右端是“泛型实现”,根据类的实际情况,两者取其平衡,是在Java的泛型中必须权衡的问题。
1.2、泛型类/泛型接口的创建和使用
简单的泛型类创建上一小节已举过例子,还有几种稍微复杂的情形:
//在继承时创建泛型
class MyHolder<T> extends Holder<T>{
}//在继承时创建泛型,并对泛型参数做进一步扩充
class MyHolder<T,U> extends Holder<T>{
}//泛型类型参数除了必须是Fruit的子类外,还必须实现了eatable接口和tasty接口
class Holder<T extends Fruit & eatable & tasty>{
}
而泛型类的使用有如下两种情形:
- 在new时,直接在<>中声明类型。 (若只在赋值语句的左侧的<>符号中有类型,说明用到了类型参数推断技术,该技术在1.7版本引入,在1.8版本进一步优化)
- 在继承已声明类型的泛型类时,如下:
//重写父类方法时,可直接使用已参数化的方法形参
class MyHolder extends Holder<Fruit> {@Overridevoid set(Fruit t){//...}
}//直接在类中调用父类已参数化的方法
class MyHolder extends Holder<Fruit> {void callFatherMedthod(){//get()是父类Holder的方法,并且具有了Fruit类的参数信息Fruit t = this.get();}
}
泛型接口的创建和使用和泛型类类似,不再赘言。
1.3、泛型方法的创建和使用
如果使用泛型方法可以取代将整个类泛型化,那么就应该只使用泛型方法,因为它可以使事情更清楚明白。另外,对于一个static的方法而言,无法访问泛型类的类型参数,所以,如果static方法需要使用泛型能力,就必须使其成为泛型方法。——《Java编程思想》
//泛型方法创建
class GenericMedthodWrapper{static <T> void genericMedthod(List<T> list){//...}
}//泛型方法使用:不必指明参数类型,会自动进行类型参数推断,就好像该方法被无限次的重载过
List<User> users = new ArrayList<>();
GenericMedthodWrapper.genericMedthod(users)
二、类型擦除和擦除补偿
2.1、类型擦除
在C#中,List<int>和List<String>就是两个不同的类型,它们在运行期生成,有自己的虚方发表和类型数据,是真实泛型。
Java泛型说到底只不过是编译期的一个语法糖,所谓语法糖就是让你少写点代码也能达到相同的效果,而这你偷懒少写的代码,编译器帮你补上了,它是编译器实现的一些小把戏。所以Java泛型语法只对编译器生效,故而也就只在你自己的源码中存在,在编译后的字节码中,已不存在任何的泛型语义信息,它已经被编译器擦除了。
在泛型中的所有动作都发生在边界处——对传递进来的值进行额外的编译期检查,并插入对传递出去的值的转型。有且仅有边界是发生动作的地方。
在泛型代码内部,无法获得任何有关泛型参数类型的信息
2.2、擦除补偿
在泛型代码内部确实无法直接获取泛型参数类型信息,但是可以通过一些额外的补偿手段,间接地获取类型信息。
1、在泛型类中保存一个对应泛型参数类型的Class对象,通过Class对象的isInstance()和newInstance()方法。
2、在泛型类中保存一个对应泛型参数类型的对象,获取该对象后从该对象身上挖掘类型信息。
3、通过java.lang.reflect.ParameterizedType接口。
三、通配符
Java的泛型通配符,实际上是在泛型不变性的前提下,解决了泛型的协变和逆变问题。
关于协变、逆变,以及涉及到的里氏替换原则,可参考下面两篇博文:
设计模式六大原则(2):里氏替换原则
Java中的逆变与协变
先厘清一点,泛型通配符是在使用泛型类的时候使用的,而不是在创建泛型时使用的。以下讨论的都是在使用泛型类时通配符的用法:
class Fruit {
}
class Apple extends Fruit {
}//对类来说,这样引用是没毛病的
Fruit fruit = new Apple();
//但这样引用就不行了,会报错
List<Fruit> fruitList = new ArrayList<Apple>()
3.1、<? extends Fruit> 协变
真正的问题是我们在谈论容器的类型,而不是容器持有的类型。但如果真的就想这样引用,该怎么做呢?如下这样就可以了:
List<? extends Fruit> fruitList = new ArrayList<Apple>();Fruit fruit = fruitList.get(i);
fruitList.add(new Apple()); //error
fruitList.add(new Object()); //error
但是,通配符引用的是明确的类型,但是这个 ? extends Fruit 意味着fruitList要持有某种具体的Fruit或其子类型,也就意味着编译器根本无法确定 ? extends Fruit 引用的是什么类型,所以编译器很决绝,直接不让你再通过fruitList.add()添加任何对象了,对,是任何,Object也不行。而通过fruitList.get()出来的对象即是Fruit类型。
3.2、<? super Fruit> 逆变
然而我就是像添加对象怎么办呢?可如下这样:
List<? super Fruit> fruitList = new ArrayList<Apple>();Object obj = fruitList.get(i);
fruitList.add(new Fruit());
fruitList.add(new Apple());
? super Fruit 意味着fruitList要持有某种具体的Fruit或其父类型,所以此时就可以通过fruitList.add()添加Fruit类型或其子类了,但是通过fruitList.get()出来的只能是Object类型。
以上 ? extends Fruit 和 ? super Fruit 对于add()和get()的限制,让人初看有点匪夷所思,但仔细想想还是有道理的,需要稍加品悟。
3.3、<?>
还有最后一个更让人匪夷所思的通配符 —— 无界通配符<?>。使用无界通配符好像等价于使用原生类型,但其实二者还有有所区别的。还是那句话:“通配符引用的是明确的类型”,List实际上表示“持有任何Object类型的原生List”,而List<?>表示“具有某种特定类型的非原生List,只是我们不知道那种类型是什么”。
List<?> list = new ArrayList<>();
list.add(new Object()); //error
不能向这个list加入除了null以外的任何对象,这个道理和之前的 ? extends Fruit 是类似的,? extends Fruit 都不能加入任何对象,就更别说 ? 了。
无界通配符有一个重要应用:当你在处理多个泛型参数时,有时允许一个参数可以是任何类型,同时为其他参数确定某种特定的类型。比如声明一个 Map<String, ?>
四、泛型的真正内涵
先厘清几个概念(个人见解,理性看待),且只抓核心要害:
第一个范畴:
- 静态语言: 又可称“静态编译语言”,指从源码转为目标代码/中间代码的动作发生在程序编译阶段。
- 动态语言:又可称“动态编译语言”,指从源码转为目标代码/中间代码的动作发生在程序运行阶段。
第二个范畴:
- 静态类型语言:类型的区分标志仅在于你赋予这个类型的独特名字,而跟这个类型中含有的属性和方法签名无关。
- 动态类型语言:所有类型的名字都一样,继而也就不会再关心类型的名字,类型的区分标志仅在于类型中的属性和方法签名。
第三个范畴:
- 强类型语言:变量需指定类型信息,只有确是该类型的值才可赋给该变量。
- 弱类型语言:变量无需指定类型信息,任何类型的值都可赋给该变量。
不同范畴之间的概念,没有直接关联,比如动态类型语言不一定是动态语言,也不一定是弱类型语言。但是动态类型语言通过运行期再编译的方式确实更易实现且合理,所以大多数的动态类型语言都是动态语言,比如javascript、groovy,所以人们常把动态类型语言说成动态语言。但是,C++的泛型实现却是在编译期间,就通过动态类型语言特性完成的。
一门语言设计成动态语言还是静态语言,没有必然的因果逻辑关系,仅仅是语言规范中的人为规定,除去性能因素,动态语言和静态语言在最终的使用效果上可以说没有区别。但是一门语言是动态类型语言还是静态类型语言,对于程序员的日常编程的设计和实施都会影响甚大。
4.1、泛型的本质
所谓泛型,就是在静态类型语言中去渴求动态类型语言的特性。你也会发现真正的动态类型语言中,不会有,也没有必要有泛型这种东西,泛型只会出现在静态类型中。
泛型的目的就是:
(1)通过某种途径来放宽对我们的代码将要作用的类型所作的限制。
(2)同时不丢失静态类型检查的好处。
4.2、Java对缺乏动态类型的补偿
(1)Java中没有动态类型的特性,但是可以通过反射,达到和动态类型相似的效果。可以调用Class类的getMethod()方法和Method类的invoke方法。
(2)迎合基于继承的类型擦除,让我们的类来继承泛型上界,或者通过适配器设计模式来继承泛型上界。
4.3、Java的泛型究竟带来了什么
Java的泛型带来了一定程度的泛化效果,但更多地是带来了程序语言的可表达性,提升了语义准确性,以及将曾经的运行期检查提前到了编译期。
五、参考书籍
《Java编程思想》—— Bruce Eckel
第15章 泛型
《深入理解Java虚拟机》—— 周志明
第10章 10.3 Java语法糖的味道
《Groovy程序设计》—— Venkat Subramaniam
第3章 动态类型
这篇关于Java泛型的中庸之道的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!