泛型有啥用?
先看个经典的例子:
List arrayList = new ArrayList();
arrayList.add("aaaa");
arrayList.add(100);
for(int i = 0; i< arrayList.size();i++){
String item = (String)arrayList.get(i);
Log.d("泛型测试","item = " + item);
}
上面的代码是可以通过编译的,但是运行时会报java.lang.ClassCastException: java.lang.Integer cannot be cast to java.lang.String 的错误。
若使用泛型:
List<String> arrayList = new ArrayList<String>();
则在编译期间, arrayList.add(100); 就会报错。
一、为什么需要泛型?
泛型是JDK1.5后增加的属性,主要的目的是解决ClassCastException的问题。
我们知道Object可以接收任意类型的数据,例如整型、浮点型、字符串都可以用Object来接收,因为存在以下的装换关系:
- 基本数据类型--------->转换为包装类--------->自动向上转型为Object
以上是向上转型,没有任何问题,但是当向下转型时就会出现问题。例如:
@Data
class Ball {
private Object x;
private Object y;
}
public class Test {
public static void main (String[] args){
Ball ball = new Ball();
ball.setX(90);
ball.setY("字符串");
int a = (Integer) ball.getX();
int b = (Integer) ball.getY();
}
}
以上代码在编译时虽然不会报错,但是执行时就会报ClassCastException,如下:
尽管在set时没有问题,定义的y是Object类型,即使传入字符串也没问题,但是后面将Object类型强转为Integer类型就会报错。这样编译时正常但是执行时报错的错误很容易被忽略,有安全隐患,最好的做法是避免这种强制转换。泛型就是来解决这个安全隐患的,使用泛型以后,在编译阶段就能查找出这类的错误,从而避免运行时才报错的尴尬。
总体来说,使用泛型有什么好处:
- 1、类型安全。泛型的主要目标是提高Java程序的类型安全。通过知道使用泛型定义的变量的类型限制,编译期间即可验证。
- 2、消除强制类型转换。消除源代码中的许多强制类型转换。这使得代码更加可读,并且减少了出错机会。
- 3、高效率、潜在的性能收益。泛型集合一旦声明了是何种数据类型的集合,就只能添加何种数据类型。它是运行时动态的获取类型参数。也就是说没有装箱和拆箱这些操作。减少了处理器的资源浪费。
- 4、“泛型” 意味着编写的代码可以被不同类型的对象所重用。
二、泛型定义
“泛型” 意味着编写的代码可以被不同类型的对象所重用。
泛型用“<T>”来指定实例化对象的类型,T代表任意类型(可以用T也可以使用具体的类型),上面的代码可以用泛型改写如下:
在实例化Ball的对象时,指定了T为Integer类型,此时若再给y设置字符串值,即使没有运行但编译器已经报错。
所以泛型是在实例化对象时的指定类型,这样于此泛型对应的属性、变量、方法和返回值等都将绑定这个指定的类型,一旦与该类型不符就会出现错误提示,同时避免了强制向下转型,提高安全性。
三、特性
泛型只在编译阶段有效。
List<String> stringArrayList = new ArrayList<String>();
List<Integer> integerArrayList = new ArrayList<Integer>();
Class classStringArrayList = stringArrayList.getClass();
Class classIntegerArrayList = integerArrayList.getClass();
if(classStringArrayList.equals(classIntegerArrayList)){
Log.d("泛型测试","类型相同");
}
泛型测试: 类型相同
上面的例子可以证明,在编译之后程序会采取去泛型化的措施。在编译过程中,正确检验泛型结果后,会将泛型的相关信息擦除。
当编译器对带有泛型的java代码进行编译时,它会去执行类型检查和类型推断,然后生成普通的不带泛型的字节码,这种普通的字节码可以被一般的 Java 虚拟机接收并执行,这在就叫做 类型擦除(type erasure)。
也就是说Java中的泛型,只在编译阶段有效。
四、泛型擦除
泛型是通过擦除来实现的。
因此泛型只在编译时强化它的类型信息,而在运行时丢弃(或者擦除)它的元素类型信息。擦除使得使用泛型的代码可以和没有使用泛型的代码随意互用。
五、泛型通配符
先看以下代码:
class Ball<T> {
private T info;
public void setInfo(T info) {
this.info = info;
}
public T getInfo() {
return info;
}
}
public class Test {
public static void main(String[] args) {
Ball<String> ball = new Ball<>();
ball.setInfo("hahaha");
sout(ball);
}
public static void sout(Ball<String> tempBall){
System.out.println(tempBall.getInfo());
}
}
以上代码中,可以在实例化Ball类的对象时确定泛型T的类型,虽然实现了接收任意类型的Ball,但是sout()这个方法处的参数类型就写死了,只能是String,如果此时要换一种参数类型就会出错。此时你可能会想到重载一下sout()这个方法不就好了,如下:
当你想重载sout方法时,编译器直接报错,会报:“both methods have same erasure”,这是因为泛型类型在编译后,会做类型擦除,最终无论是String还是Integer都会被Object代替,因此认为这两个是同一个方法,不构成重载而报错。
“泛型” 意味着编写的代码可以被不同类型的对象所重用。这个怎么体现或者实现呢?
此时我们只需要在方法的参数处使用泛型通配符就可以实现接收任意类型数据且不用修改或重载方法,泛型通配符一般定义为:
类名<?> 变量名
以上代码可以用泛型通配符改写为:
class Ball<T> {
private T info;
public void setInfo(T info) {
this.info = info;
}
public T getInfo() {
return info;
}
}
public class Test {
public static void main(String[] args) {
Ball<String> ballA = new Ball<>();
Ball<Integer> ballB = new Ball<>();
ballA.setInfo("hahaha");
ballB.setInfo(100);
sout(ballA);
sout(ballB);
}
public static void sout(Ball<?> tempBall){
System.out.println(tempBall.getInfo());
}
}
此时在sout()方法的传参采用Ball类+泛型通配符的方式,就实现了接收任意类型数据的目的。
所以,泛型 + 通配符 '?' 可以实现代码复用。
六、泛型上限与下限
以上泛型通配符还提供两个小通配符:
- ?extends 类 :设置泛型上限
- ?super 类 :设置泛型下限
例如:
?extends Number 表示该泛型只允许设置Number及其子类
?super String 表示该泛型只能使用String及其父类
class Ball<T extends Number> {
private T info;
public void setInfo(T info) {
this.info = info;
}
public T getInfo() {
return info;
}
}
public class Test {
public static void main(String[] args) {
Ball<Integer> ball = new Ball<>();
ball.setInfo(100);
sout(ball);
}
public static void sout(Ball<? extends Number> tempBall){
System.out.println(tempBall.getInfo());
}
}
class Ball<T> {
private T info;
public void setInfo(T info) {
this.info = info;
}
public T getInfo() {
return info;
}
}
public class Test {
public static void main(String[] args) {
Ball<String> ball = new Ball<>();
ball.setInfo("100");
sout(ball);
}
public static void sout(Ball<? super String> tempBall){
System.out.println(tempBall.getInfo());
}
}
七、泛型接口
泛型还可以定义在接口中,泛型接口定义如下:
interface Ball<T> {
public String test(T t);
}
public class Test {
public static void main(String[] args) {
}
}
既然是接口就需要实现它,实现泛型接口有两种方式:
- (1)在实现类中继续设置泛型
interface Ball<T> {
public String test(T t);
}
class ImBall<S> implements Ball<S> {
@Override
public String test(S s) {
return "s== " + s;
}
}
public class Test {
public static void main(String[] args) {
Ball<String> ball = new ImBall<>();
System.out.println(ball.test("haha"));
}
}
- (2)在实现类中定义具体的泛型类型
interface Ball<T> {
public String test(T t);
}
class ImBall implements Ball<String> {
@Override
public String test(String s) {
return "s== " + s;
}
}
public class Test {
public static void main(String[] args) {
Ball<String> ball = new ImBall();
System.out.println(ball.test("haha"));
}
}
八、泛型方法
泛型可以定义在类、接口上,也可以定义在方法上。
public class Test22 {
public static void main(String[] args) {
// 传入了String类型,泛型就是String类型
String strs [] = info("haha", "gege", "didi");
for (String str:strs){
System.out.println(str);
}
}
public static <T> T[] info(T ... args) {
return args;
}
}
九、泛型注意事项
- 泛型中设置的类型必须是引用类型,因此若是操作基本数据类型就需要先转换成包装类;
- 在遇到强制转型时,可以考虑引入泛型设计。