泛型解读
看了java编程思想的泛型,我发现泛型是java后生的一个功能,所以他在加入的时候必须要兼容老代码,适应新代码。“泛型”这个术语的意思是:“适用于许多许多的类型”,让类或者方法具备最广泛的表达能力。
Object与泛型比较
泛型引人注目最主要的一个原因就是为了创建容器类,支持容器能够保存许多类型还可以限定边界,关于边界的概念在下面会讲到,我们先来介绍泛型的简单使用。
在使用泛型之前,如果我们希望类型多样化会使用Object,但是这样做的局限性也很明显:
1)Object缺失了边界
2)在实例化取值时也无法确定真正的类型,需要强制转化,无法由编译器来保证类型的正确性
public class Generics { private Object a; public Object getA() { return a; } public void setA(Object a) { this.a = a; } public static void main(String[] args) { Generics generics = new Generics(); String b = "hello"; AutoMobile autoMobile = new AutoMobile(); generics.setA(b); b = (String) generics.getA(); autoMobile = (AutoMobile)generics.getA(); } }上面的代码毫无违和感,在类型信息就解释过,类型转换的正确性校验是在运行时,而且当我们设置不同的值时也是完全可行的,这样就会让代码显得乱。所以使用泛型避免这样的事情发生。泛型的声明:用尖括号括住,放在类名后面。
public class Generics<T> { private T a; public T getA() { return a; } public void setA(T a) { this.a = a; } public static void main(String[] args) { Generics<AutoMobile> generics = new Generics(); String b = ""; //当我设置了Generics的参数类型为AutoMobile就规定了a属性的类型为AutoMobile其他没关联的类型是无法通过编译器的 generics.setA(b); //同理取值也是一样 b = generics.getA(); } }
运行上面代码编译器自然会报出不兼容的错误。
从类型上分,泛型可以定义在类、接口和内部类中,声明的时候只需要用尖括号括住,放在类名后面;从类的结构来分,泛型可以分别定义在类和方法上,要定义泛型方法只需要将泛型参数列表置于返回值之前,其实这也好理解,你在方法中使用泛型如果声明在方法名后面,那么前面的返回类型就无法获取到泛型,会带来不便;
下面是泛型在类、接口和内部类中使用 其实都是大同小异就不一一介绍了。其实从接口和类的使用来看,泛型的泛化很大程度上节省了重复代码,使得代码更加简洁灵活。
类中使用
public class LinkedStatck<T> { private Node<T> top = new Node<>(); public void push(T item){ top = new Node(item,top); } public T pop(){ T result = top.item; if(!top.end()) top = top.next; return result; } private static class Node<U>{ U item ; Node<U> next; public Node() { } public Node(U item, Node<U> next) { this.item = item; this.next = next; } //哨兵 boolean end(){ return item == null && next == null; } } }
接口中使用
public interface Generator<T> { T next(); }
public class Coffee implements Generator<Coffee> { @Override public Coffee next() { return null; } }
内部类中使用
public class Customer { public static Generator<Customer> generator(){ return new Generator<Customer>() { @Override public Customer next() { return new Customer(); } }; } }
泛型方法的使用
泛型方法使得该方法能够独立于类而产生变化。无论何时,只要你能够做到,你就应该尽量使用泛型方法。另外,对于一个static的方法而已,无法访问泛型类的类型参数,所以,如果static方法需要使用泛型能力,就必须使其成为泛型方法。
public <T> T f(T x){ System.out.println(x.getClass().getName()); return x; }
前面一部分讲的都是泛型的使用,下面讲讲泛型本身一些性质,理解下面这些才能得心应手的使用泛型。
泛型的擦除
public class ErasedTypeEquivalence { public static void main(String[] args) { Class class1 = new ArrayList<String>().getClass(); Class class2 = new ArrayList<Integer>().getClass(); System.out.println(class1==class2); } }
在不了解泛型擦除的性质之前,都会主观的以为class1和class2是不一样的,因为ArrayList存储的数据是不同类型的。但是在上面的代码来看不是这样的,jvm的输出结果是true就是说class1和class2是相同的。泛型只是占位符,在泛型代码内部是无法获取参数类型信息的比如ArrayList<String>会被擦除为原生类型ArrayList,下面的代码可以证明这一点:
public class ErasedTypeEquivalence { public static void main(String[] args) { Class class1 = new ArrayList<String>().getClass(); Class class2 = new ArrayList<Integer>().getClass(); System.out.println(class1==class2); System.out.println(Arrays.toString(class1.getTypeParameters())); System.out.println(Arrays.toString(class2.getTypeParameters())); } }
当时开发者为什么会这么处理呢,这点我在一开始也提到了,泛型是后生功能,为了兼容现有的代码所做出的牺牲。由于擦除,泛型不能用于显式的引用运行时类型的操作之中,例如转型、instanceof操作和new表达式。
上面这个例子即使设置了kind的具体类型,在调用kind创建数组的时候T也会被擦除,但是它仍旧可以在编译器确保方法或者类的一致性,这就是边界问题,即在进入或者离开方法的时候编译器会执行类型检查并插入转型代码。
擦除的补偿
java也提供了一些补偿的机制。
1)使用类型标签isInstance,这个方法可以判断当前类能否转化为传入的实际类型
public class ArrayMaker<T> { private Class<T> kind; public ArrayMaker(Class<T> kind) { this.kind = kind; } public boolean f(Object g){ return kind.isInstance(g); } public static void main(String[] args) { ArrayMaker<String> arrayMaker = new ArrayMaker<>(String.class); System.out.println(arrayMaker.f(new Integer(1))); System.out.println(arrayMaker.f(new String("1"))); } }
2)使用类型标签newInstance
这里的newInstance和上面ArrayMaker类里面的newInstance最大的区别就是,我们是在kind的内部进行创建操作,这样就在边界之内,当你确定T的值时,kind的内部就可以调用T的默认构造器进行对象的创建。下面我还举了一个意外情况就是当你传进来的class没有默认构造器的时候创建会失败。
public class ClassAsFactory<T> { T x; public ClassAsFactory(Class<T> kind) { try { this.x = kind.newInstance(); }catch (InstantiationException e){ throw new RuntimeException(); }catch ( IllegalAccessException e){ throw new RuntimeException(); } } public Class getX(){ return x.getClass(); } public static void main(String[] args) { ClassAsFactory<String> factory = new ClassAsFactory(String.class); Class x = factory.getX(); System.out.println("****************"); System.out.println(x.getName()); System.out.println("****************"); factory = new ClassAsFactory(Integer.class); x = factory.getX(); System.out.println(x.getName()); } }
但是我发现上面这种方式有一点不好就是当我声明参数类型为String时,我实际传入的一个Date类型竟然也能赋值成功,在编译器和运行时都不会校验这种错误。
public class ClassAsFactory<T> { T x; public ClassAsFactory(){} public ClassAsFactory(Class<T> kind) { try { this.x = kind.newInstance(); }catch (InstantiationException e){ System.out.println("初始化失败"); throw new RuntimeException(); }catch ( IllegalAccessException e){ throw new RuntimeException(); } } public Class getX(){ return x.getClass(); } public static void main(String[] args) { /*ClassAsFactory<String> factory = new ClassAsFactory(String.class); Class x = factory.getX(); System.out.println("****************"); System.out.println(x.getName()); System.out.println("****************");*/ ClassAsFactory<String> factory1 = new ClassAsFactory<>(); System.out.println(Arrays.toString(factory1.getClass().getTypeParameters())); ClassAsFactory<String> factory = new ClassAsFactory(Date.class); Class x = factory.getX(); System.out.println(x.getName()); } }
这个其实就是语法糖的概念理解,在你初始化的时候是根据你的右边构造器来的,在你实际操作对象的时候是根据左边限定的泛型来的,因为在没有运行之前是不产生具体对象的,我这么简单的解释可能不好理解,有兴趣的同学可以自己了解下这方面的知识。
public class ClassAsFactory<T> { T x; public ClassAsFactory(){} public ClassAsFactory(Class<T> kind) { try { this.x = kind.newInstance(); }catch (InstantiationException e){ System.out.println("初始化失败"); throw new RuntimeException(); }catch ( IllegalAccessException e){ throw new RuntimeException(); } } public Class getX(T s){ return x.getClass(); } public static void main(String[] args) { ClassAsFactory<String> factory1 = new ClassAsFactory<>(); System.out.println(Arrays.toString(factory1.getClass().getTypeParameters())); ClassAsFactory<Integer> factory = new ClassAsFactory(Date.class); Class x = factory.getX(); System.out.println(x.getName()); } }
泛型的边界
在上面的例子中,由于擦除所有定义的泛型都是只作为一个参数或者回参在流转,并没有使用到泛型里面的方法,当我们定义了泛型的边界,我们可以使用在类或者方法中使用边界的方法。这是我认为泛型边界最重要的一个效果。
public class Colored <T extends HasColor> { T item; Colored(T item){ this.item = item; } T getItem(){ return item; } java.awt.Color color(){ return item.getColor(); } }通过限定边界就可以使用边界的方法,同时缩小泛化的范围。这在实际使用上也是很常见方便的。当然限定的边界可以定义多个毕竟java是可以同时实现多接口的。
通配符
通配符和边界限定写法上其实很相似,他是应用在初始化对象上,而不是在类或者方法上,使用通配符可以解决向上转型的问题。在下面的例子中Red实现了HasColor接口但是还是无法直接在list中转换成HasColor,但是使用通配符就可以。
public class Red implements HasColor { @Override public Color getColor() { return null; } }
*通配符<?>
*通配符看起来意味着“任何事物”,但实际不是的List<Object>和List<?>的区别在于 LIstanbul<Object>属于原生List,而通配符表示为某种特定的类型但是我们暂时不知道而已。
泛型使用中的问题
1)任何基本类型都不能作为类型参数
2)一个类不能实现同一个泛型接口的两种变体
public interface Payable<T> { }
public class Employee implements Payable<Employee> { }
public class Hourly extends Employee implements Payable<Hourly> { }
3)转型和警告
当返回一个参数类型不确定的左边又是一个含有泛型的参数类型时,需要进行强制转换来标记类型。当注释掉SuppressWarnings时会被要求强制转型,但是又被告知不应该转型。通过javaSE5新增了一个特性通过泛型类来转型。
public class NeedCasting { public void f(String[] args) throws Exception{ ObjectInputStream objectInputStream = new ObjectInputStream(new FileInputStream(args[0])); @SuppressWarnings("unchecked") List<Colored> coloreds = (List<Colored>) objectInputStream.readObject(); List<Colored> colored = List.class.cast(objectInputStream.readObject()); } }
4)重载
由于擦除,重载的时候会忽略泛型的类型,所以List<T>和List<R>是同一个东西。
泛型我们就介绍到这,有什么疑问或者写的好不的地方欢迎留言。