zoukankan      html  css  js  c++  java
  • 0072 Java中的泛型--泛型是什么--泛型类--泛型方法--擦除--桥方法

    什么是泛型,有什么用?

    先运行下面的代码:

    public class Test {
        public static void main(String[] args) {
            Comparable c=new Date();				//Date实现了Comparable接口的
            System.out.println(c.compareTo("red")); //这里可以通过编译,但在运行的时候会抛出异常。ClassCastException: java.lang.String cannot be cast to java.util.Date
        }
    }
    

    上面的代码稍微修改下:

    public class Test {
        public static void main(String[] args) {
            Comparable<Date> c=new Date();				//这行修改了,加入了泛型
            System.out.println(c.compareTo("red")); 	//这行会提示错误:compareTo (java.util.Date) in Comparable cannot be applied to (java.lang.String)
        }
    }
    

    对比上面的代码,没加入泛型的时候,在程序运行期才发现问题,而加入了泛型则在程序编译期就发现了,这就是泛型的优势所在。
    在第二段代码中,泛型就好象是在告诉编译器:这里声明的变量c只跟Date类型进行比较,如果跟别的类型比较,那么就不能通过编译。

    再用ArrayList的例子看下

    public class Test {
        public static void main(String[] args) {
            List list=new ArrayList();          //没有泛型
            list.add("HTTP");					//添加的是String类型
            list.add("FTP");					//添加的是String类型
            list.add("SMTP");					//添加的是String类型
            list.add(1024);						//不小心添加了个Integer类型,编译和运行期都不会报错
            Object http = list.get(0);			//取出第一个String元素,类型变成了Object
            String ftp = list.get(1);			//编译错误:取出第二个String元素,却不能直接赋值给String类型变量
            String smtp = (String) list.get(2); //取出第三个String元素,经强制类型转换赋值给String类型
        }
    }
    

    上面的代码就是在Java1.5泛型加入前的办法,list中可以加入的是Object类型,同时加入String和Integer都没问题,然后不管加进去的是什么类型,取出来的时候都成了Object,还得强制转换成自己的类型。但在实际编码中往往只是添加同类型的元素,得小心翼翼的保证不会加入其他类型,否则在取出来的时候会出意外。泛型加入后,如果不小心添加了非指定类型元素,那根本不能通过编译,在取出来的时候,直接就是指定的类型,而不再是Object。

    也就是说对类型的保证由程序员转到了编译器,将可能的错误从运行期转到了编译期。其实泛型只是一层皮,功能的实现还是Object+强制转换。

    泛型就是Java的语法糖,所谓语法糖就是:这种语法对功能没有影响,但更方便程序员使用,虚拟机并不支持这些语法,在编译阶段就被还原成了简单语法结构。Java中的其他语法糖还有变长参数、自动拆装箱、内部类等。

    自定义泛型类

    设想有这样的一个类:用来盛装两个对象,但其类型不确定,可能是String,可能是File,可能是自定义的。我们引入一个类型参数T来代替它可能盛装的类型:

    public class Pair<T> {  //这里的<T>就是类型参数,写在类名的后面
        private T first;
        private T second;
    
        public Pair() {
            first = null;
            second = null;
        }
    
        public Pair(T first, T second) {
            this.first = first;
            this.second = second;
        }
    
        public T getFirst() {
            return first;
        }
    
        public T getSecond() {
            return second;
        }
    
        public void setFirst(T first) {
            this.first = first;
        }
    
        public void setSecond(T second) {
            this.second=second;
        }
    
        public String toString(){
            return first.toString()+" "+second.toString(); //甚至可以调用T的方法
        }
    }
    

    类型参数:
    写在类名后面
    可以有多个,比如:<K,V>
    T一般代表任意类型,E一般用于集合类型中,KV一般用于键值类型
    类型参数T可以用在实例变量、局部变量、方法返回值中
    可以调用类型参数T的方法。既然不知道T具体是什么类型,那咋能调用其方法呢?后面再说

    使用Pair类:

    public class PairTest1 {
        public static void main(String[] args) {
            String[] words = {"Mary", "had", "a", "little", "las"};
            Pair<String> mm = ArrayAlg.minmax(words);     //指定mm中装的是String类型
            System.out.println("min= " + mm.getFirst());
            System.out.println("max= " + mm.getSecond());
        }
    }
    
    class ArrayAlg{
        public static Pair<String> minmax(String[] a) {  //计算一个字符串数组中的最大、最小值
            if (a == null || a.length == 0) {
                return null;
            }
            String min = a[0];
            String max = a[0];
            int len = a.length;
            for (int i = 1; i < len; i++) {
                if (min.compareTo(a[i]) > 0) {
                    min = a[i];
                }
                if (max.compareTo(a[i]) < 0) {
                    max = a[i];
                }
            }
            return new Pair(min, max);
        }
    }
    
    

    自定义泛型方法

    定义泛型接口跟定义泛型类是一样,另外还可以在一个非泛型类中定义泛型方法,当然也可以定义在泛型类中
    看代码:

    public class ArrayAlg {						//类中没有泛型参数,这不是个泛型类
        public static <T> T getMiddle(T[] a) {	//类型参数在方法中,这是个泛型方法,类型参数放在返回值前修饰符后。该方法返回一个数组中间那个元素
            return a[a.length/2];
        }
    }
    

    使用该泛型方法

    public class Test {
        public static void main(String[] args) {
            Integer[] a = {54, 42, 94, 23, 34};
            Integer middle = ArrayAlg.<Integer>getMiddle(a); //使用的时候在方法调用前指定类型实参,其实也可以省略,编译器能通过方法中的实参类型自动判定类型实参
            System.out.println(middle);
        }
    }
    

    对类型参数进行限定

    看下面的代码,计算一个数组中的最小元素:

    public class ArrayAlg {
        public static <T> T min(T[] a) {
            if (a == null || a.length == 0) {
                return null;
            }
            T smallest = a[0];
            for (int i=1;i<a.length;i++) {
                if (smallest.compareTo(a[i]) > 0) { //这里会有编译错误,提示smallest没有compareTo()方法
                    smallest = a[i];
                }
            }
            return smallest;
        }
    }
    

    上面代码中,需要调用类型参数T的compareTo()方法,但T有没有这个方法呢?不知道。但是如果能明确告知T实现了Comparable接口,那T类型就一定是具有compareTo()方法的,这就是类型限定。

    在声明类型参数处改为这样: public static <T extends Comparable> T min(T[] a) {

    类型限定:
    形式: <T extends BoundingType> ,关键字就是extends,没有别的比如implements
    可以进行多个限定:比如:<T extends Comparable & Serializable> .注意这是且的关系
    可以限定多个接口,但最多只能限定一个类,且这个类得排在第一位

    <T extends Comparable>,这里指定了T的上限是Comparable,那么<T>的上限是什么呢?是Object

    正是因为有了类型限定,才能调用T的方法。

    泛型的擦除

    前面说了,Java的泛型只是语法糖,仅仅存在于源码层面。
    编译后的字节码中是没有泛型的。
    虚拟机中更没有泛型类,所有的类都是普通类。
    编译器在编译的时候,会将泛型信息擦除,转换为原始类,用限定类型替换泛型T,再插入必要的强制类型转换或者桥方法。
    如果有多个限定类型,那就用第一个替换,因此标记式接口要放到泛型列表的最后。

    比如:

    public class Pair<T>{
    	private T first;
    	public T getFirst(){
    		return first;
    	}
    }
    

    擦除泛型变成这样了:

    public class Pair{
    	private Object first;
    	public Object getFirst(){
    		return first;
    	}
    }
    

    下面使用这个getFirst()方法,代码片段:

    Pair<Employee> buddies=....;
    Employee buddy=buddies.getFirst();
    

    上面这个代码片段中,buddies.getFirst()取出来的为啥直接就是Employee呢,不是Object呢?因为在编译的时候,编译器自动插入了强制类型转换,大概是这样子Employee buddy=(Employee) buddies.getFirst();

    桥方法--与多态的冲突

    前面的Pair类类型擦除后,大概是这样的:

    public class Pair{
    	...
    	public void setSecond(Object second){
    		this.second=second;
    	}
    }
    

    下面用一个DateInterval类继承Pair,并重写setSecond()方法,确保seconde一定大于first:

    public class DateInterval extends Pair<Date>{
    	...
    	@Override									//注意这个注解,没有报错,说明重写成功了的
    	public void setSecond(Date second){			//该方法确保了第二个日期一定大于第一个
    		if(second.compareTo(getFirst())>=0){
    			super.setSecond(second);
    		}
    	}
    }
    

    问题来了,setSecond(Date second)重写了setSecond(Object second),这不科学啊。
    实际上,DateInterval编译后新增加了一个桥方法

    public void setSecond(Object second){   //实际完成重写的是这个桥方法
    	setSecond((Date)Object);			//调用DateInterval的setSecond(Date second)方法
    }
    

    除了set外,还有get也存在问题:
    DateInterval中会存在这样的两个方法:

    public Object getSecond(){}
    public Date getSecond(){}
    

    这里两个get方法的方法签名是相同的,不能共存于一个类中。
    但其实它们就是能共存,因为虚拟机辨别方法的时候还加上了返回值类型。

    所以啊,泛型遇到编译和虚拟机有点特别了:
    虚拟机没有泛型,只有普通的类和方法
    类型参数要被其限定类型替换
    插入桥方法来保持多态
    插入了强制类型转换

    总结

    • 泛型就是语法糖,仅存在于源代码中,功能的实现还是Object(限定类型)+强制类型转换实现
    • 泛型能保证程序员少犯错误,将发现错误的时间点从运行期提到编译期
    • 可以定义泛型类、泛型接口、泛型方法。泛型类和泛型接口,把类型参数写在类名或接口名前面;泛型方法将类型参数写到方法名前面
    • 类型参数可以进行限定,且能限定多个,可以是[0,1]个类+[0,n]个接口,如果有类,那得写在第一位,标记式接口要写在最后
    • 泛型在编译后,都会被擦除成原始类型,用限定类型替代类型参数,字节码和虚拟机中是没有泛型的
    • 编译的时候,还会根据需要,加入桥方法,以保持多态
  • 相关阅读:
    VBA值列选取与复制,赋值
    Processing的条件式
    VBA之四给程序自动加行号
    自上而下的语法分析
    Processing绘制四边形
    Processing的代码编写流程
    Processing编程语言简介
    follow集的求解
    Processing函数与循环
    在UBUNTU下用ruby求得网卡地址IP地址和用户名
  • 原文地址:https://www.cnblogs.com/sonng/p/7202865.html
Copyright © 2011-2022 走看看