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]个接口,如果有类,那得写在第一位,标记式接口要写在最后
    • 泛型在编译后,都会被擦除成原始类型,用限定类型替代类型参数,字节码和虚拟机中是没有泛型的
    • 编译的时候,还会根据需要,加入桥方法,以保持多态
  • 相关阅读:
    新概念第二册(1)--英语口语听力课1
    外企面试课程(一)---熟悉常见的缩略词
    公司 邮件 翻译 培训 长难句 结课
    workflow
    公司 邮件 翻译 培训 长难句 20
    公司 邮件 翻译 培训 长难句 19
    Engineering Management
    公司 邮件 翻译 培训 长难句 18
    公司 邮件 翻译 培训 长难句 17
    第14.5节 利用浏览器获取的http信息构造Python网页访问的http请求头
  • 原文地址:https://www.cnblogs.com/sonng/p/7202865.html
Copyright © 2011-2022 走看看