带着问题阅读html
一、什么是Java泛型,有什么用处java
二、Java泛型的实现机制是什么编程
三、Java泛型有哪些局限和限制数组
引入泛型以前,试想编写一个加法器,为处理不一样数字类型,就须要对不一样类型参数进行重载,但其实现内容是彻底同样的,若是是一个更复杂的方法,无疑会形成重复。ide
public int add(int a, int b) {return a + b;} public float add(float a, float b) {return a + b;} public double add(double a, double b) {return a + b;}
通常的类和方法,只能使用具体的类型,要么是基本类型,要么是自定义的类。若是要编写能够应用于多种类型的代码,这种刻板的限制对代码的束缚就会很大。《Java编程思想》工具
Java在1.5版本引入泛型,经过泛型实现的加法代码可简化为:编码
public <T extends Number> double add(T a, T b) { return a.doubleValue() + b.doubleValue(); }
泛型的核心概念是参数化类型,使用参数指定方法类型,而非硬编码。泛型的出现带给咱们不少好处,其中最重要的莫过于对集合类的改进,避免了任意类型均可以丢到同一个集合里的不可靠问题。设计
然而Python和Go的集合能够容纳任意类型,这到底是进步仍是退步呢code
泛型通常有三种使用方式:泛型类、泛型接口和泛型方法。htm
public class GenericClass<T> { private T member; } ... // 初始化时指定泛型类型 GenericClass<String> instance = new GenericClass<String>();
public interface GenericInterface<T> { void test(T param); } // 实现类指定泛型类型 public class GenericClass implements GenericInterface<String> { @Override public void test(String param) {...} }
如前文中加法代码的实现就是泛型方法。
// 在方法前添加<T>,泛型类型可用于返回值也可用于参数 public <T> T function(T param); ... function("123"); // 编译器自动识别T为String
List<String> strList = new ArrayList<>(); List<Integer> intList = new ArrayList<>(); System.out.println(strList.getClass() == intList.getClass()); //true
对如上部分代码,相信多数人接触到泛型的第一时刻都认为这是两个不一样的类型,反编译其字节码得到代码以下:
ArrayList var1 = new ArrayList(); ArrayList var2 = new ArrayList(); System.out.println(var1.getClass() == var2.getClass());
咱们发现两个列表都变成ArrayList类型,若是你们对Jdk1.5以前的版本还有印象就能够看出,这一段反编译的代码就是Java集合最初的使用形式。所以,Java泛型的是经过编译期将泛型的实际类型擦除为原始类型(一般为Object)实现的伪泛型。
所谓伪泛型,是相对C++的"真泛型"(异构扩展,可见参考第三条),在Java中,因为编译后擦除了具体类型,在泛型代码内部,没法得到任何有关泛型参数类型的信息,在运行期代码所持有的也只是擦除后的原始类型,也就意味着在运行期能够经过反射的方式为泛型类传入任何原始类型的参数。
public class GenericTest { public List<Integer> ints = new ArrayList<>(); public static void main(String[] args) { GenericTest test = new GenericTest(); List<GenericTest> list = (List<GenericTest>) GenericTest.class.getDeclaredField("ints").get(test); list.add(new GenericTest()); System.out.println(test.ints.get(0)); // 打印GenericTest变量地址 int number = test.ints.get(0); // 类型转换抛出异常 } } // 泛型代码内部是指泛型类或泛型方法内部。 public class Generic<T> { public Class getTClass() { //没法获取 } } public <T> Class getParamClass(T param) { //没法获取 }
在泛型外部能够获取已指定的泛型参数类型,经过javap -v
查看Constant Pool
,可看到具体类型记录在Signature
。
public class Outer { private List<String> list = new ArrayList<>(); //能够获取list的具体类型 }
事实上在Java推出泛型时,C++的模板泛型已经至关成熟,设计者也并不是没有能力实现包含具体类型的泛型,使用类型擦除最重要的缘由仍是为了保持兼容性。假设ArrayList<String>
和ArrayList
编译后是不一样的class,那么为了兼容旧代码正常运行,必须平行的添加一套泛型集合并在后续版本中同时维护,而集合类做为大量使用的基础工具类,开发者不得不为此承担大量代码切换的风险(参考Vector
和HashTable
的带来的遗留问题),所以相较于兼容性的取舍,采用类型擦除实现泛型算是折中方案。
思考一下,下面的类能够编译经过吗
public class Test { void test(List<String> param) {} void test(List<Integer> param) {} }
前面说到泛型会被擦除为原始类型,通常是Object
。若是泛型声明为<? extends Number>
,就会被擦除为Number
。
List<Number> numbers = new ArrayList<>(); List<Integer> integers = new ArrayList<>(); numbers = integers; // compile error
考虑以上代码,numbers
能够增长Integer
类型的元素,直觉上integers
应该也能够赋值给numbers
。因为类型擦除,Java在编译期限定了只有相同类型的泛型实例才能够互相赋值,但这样就违背了Java的多态,为了解决泛型转换的问题,Java引入了上下限<? extends A>
和<? super B>
两种机制。
若是泛型声明为<? extends A>
,即声明该泛型的上界也即擦除后的原始类型为A
,同时该泛型类的实例能够引用A
子类的泛型实例。
// 上界保证取出来的元素必定是Number,但没法约束放入的类型 List<Integer> integers = new ArrayList<>(); List<Float> floats = new ArrayList<>(); List<? extends Number> numbers = integers; // numbers = floats; 也能够 numbers.get(0); // ok,总能保证取出的必定是Number numbers.put(1); // compile error,没法保证放入的是否符合约束
若是泛型声明为<? super B>
,即声明该泛型的下界为B
,原始类型仍为Object
,同时该泛型类的实力能够引用B
父类的泛型实例。
// 假设三个继承类 Child -> Father -> GrandFather // 下界保证写入的元素必定是Child,但没法肯定取出的类型 List<Father> fathers = new ArrayList<>(); List<GrandFather> grandFathers = new ArrayList<>(); List<? super Child> childs = fathers; // childs = grandFathers; 也能够 numbers.put(new Child()); //ok,总能保证明际容器可接受Child Child ele = (Child) numbers.get(0); // runtime error,没法肯定获得的具体类型
在Java中,根据里式替换原则,向上转型是默认合法的,向下转型则须要强制转换,如不能转换则报错。在extends
的get
和super
的put
场景中,必定能够保证读取/放入的元素是能够向上转型的,而在extends
的put
和super
的get
中,则没法确承认转的类型,所以extends
只能读取,super
只能写入。
固然若是使用super时,取出的对象以Object存放,也没有问题,由于super擦除后的原始类型为Object。
参考《Effective Java》中给出的PECS
使用建议。
为了得到最大限度的灵活性,要在表示生产者或消费者的输入参数上使用通配符类型。
若是参数化类型表示一个T生产者,就使用<? extends T>。 producer-extends
若是参数化类型表示一个T消费者,就使用<? super T>。consumer-super
若是某个输入参数便是生产者又是消费者,那么通配符类型对你就没什么好处了。
这一段话笔者认为有必定迷惑性,生产者是写入的,消费者是读取的,前文介绍过extends
用于读取,而super
用于写入,偏偏相反。
我的认为对这段话的正确理解是以泛型为第一视角切入,即当泛型类型自己做为生产者提供功能(被读取)时使用extends
,反之(被写入)使用super
。而很是规意义上生产者要写入的容器采用extends
,消费者读取的容器使用super
。
// producer,此时返回值做为生产后的结果提供给消费者 List<? extends A> writeBuffer(...); // consumer,此时返回值做为消费后的结果提供给生产者 List<? super B> readBuffer(...);
泛型类也能够被继承,泛型类主要有两种继承方式。
public class Father<T> { public void test(T param){} } // 泛型继承,Child依然是泛型类 public class Child<T> extends Father<T> { @Override public void test(T param){} } // 指定泛型类型,StringChild为具体类 public class StringChild extends Father<String> { @Override public void test(String param){} }
咱们知道@Override
是保持签名不变且重写父类方法,查看Father
类字节码,其中test方法被擦除为void test(Object param)
;在StringChild
中,方法签名为void test(String param)
。到此读者可能意识到,这根本不是重写而是重载(Overload
)。
查看StringChild
的字节码。
... #3 = Methodref ... public void test(java.lang.String); ... invokespecial #3 // Method StringChild.test:(Ljava/lang.Object;) V ... public void test(java.lang.Object);
能够看到其中实际包含了两个方法,一个参数是String
一个是Object
,后者才是对父类方法的重写,前者经过invoke转到对后者的调用。这个方法是JVM在编译时自动添加的,也叫作桥方法。同时还有一点须要说起,示例中的代码是以泛型参数做为入参,做为返回类型的话会产生Object test()
和String test()
两个方法,这两个方法在常规的编码中是没法编译经过的,但JVM为泛型多态的实现容许了这个不合规的存在。
Object
,所以范型不支持基本类型List<int> intList = new ArrayList<>(); // 没法编译
T instance = new T(); // 不能直接使用泛型初始化 if (t instanceOf T); // 不能判断泛型类型 T[] array = new T[]{}; // 不能建立泛型数组
// Error public class Generic<T> { public static T t; public static T test() {return t;} }
// 假设继承实现一个泛型异常 class SomeException<T> extends Exception... try { ... } catch(SomeException<Integer> | SomeException<String> ex) { //因为类型擦除,没法捕获多个泛型异常 ... }