Java面试

Java泛型

2017-11-26  本文已影响75人  草帽小子J

抽着空闲的时间学习了一下自己对Java薄弱的方面,细细的体味了一下Java泛型和反射机制,并写了下总结,肯定会有理解不到位的地方,望勿喷指出!

Java泛型

啥叫泛型

我们直接看代码吧:

public class Test {

    public static void main(String[] args) {
        List list = new ArrayList();
        list.add("tjun");
        list.add("aswddads");
        list.add(99);

        for (int i = 0; i < list.size(); i++) {
            String name = (String) list.get(i); // 1
            System.out.println("name:" + name);
        }
    }
}

这段代码编译是会通过,但是在程序运行就会崩溃

java.lang.ClassCastException: java.lang.Integer cannot be cast to java.lang.String

为啥尼

我们知道对于Arraylist,它可以存入任何类型,代码中我们先add进了两个String类型,再add进了一个Integer类型,这是完全允许的;接下来,我们遍历取出list中的元素,这时候程序就崩了;异常信息如崩溃日志一致,而且此类错误可能稍不注意就会犯,因为在编译阶段能够通过;其实主要问题出现在以下两点:

1.将对象放入集合中,对于集合是不会记住此对象的类型,当再次从集合中取出对象时,该对象的编译类型变成了Object类型,但其运行时类型依然为其本身类型;
2.在代码//1处取出集合元素时需要人为的强制类型转化到具体的目标类型,且很容易出现“java.lang.ClassCastException”异常。

对于使集合记住元素类型,且编译时不出问题,运行时也不出现”java.lang.ClassCastException“,解决的办法就是利用泛型。
好吧,都说到这了,我们就进入概念时间吧:

泛型

泛型,即“参数化类型”,可能提到参数都会想到我们在写Java方法的时候都会有形参,实参;对于参数化类型我的理解就是,将类型由原来的具体类型参数化,类似于方法中的变量参数,可称之为类型形参,然后在使用的时候传入具体的类型。
看着这理论性的概念很头痛吧,来看看对于上个例子中的代码进行改进,你就会理解了:

public class Test {

    public static void main(String[] args) {
        /*
        List list = new ArrayList();
        list.add("qqyumidi");
        list.add("corn");
        list.add(100);
        */

        List<String> list = new ArrayList<String>();
        list.add("qqyumidi");
        list.add("corn");
        //list.add(100);   // 1  提示编译错误

        for (int i = 0; i < list.size(); i++) {
            String name = list.get(i); // 2
            System.out.println("name:" + name);
        }
    }
}

我们利用泛型的写法后,我们在编写代码的时候,//1处就会报出编译错误;因为我们在开始的时候就直接通过List<String>限定了在list集合中只能够含有String类型的元素,从而在//2处无须进行强制类型转换,因为此时,集合能够记住元素的类型信息,编译器已经能够确认它是String类型了。

泛型的特性

一句话,泛型只在编译阶段有效,上代码大餐:

public class Test {

    public static void main(String[] args) {
       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中的泛型,只在编译阶段有效。在编译过程中,正确检验泛型结果后,会将泛型的相关信息擦出,并且在对象进入和离开方法的边界处添加类型检查和类型转换的方法。也就是说,成功编译过后的class文件中是不包含任何泛型信息的,泛型信息不会进入到运行时阶段。

对此总结成一句话:泛型类型在逻辑上看以看成是多个不同的类型,实际上都是相同的基本类型

泛型使用

对于泛型的使用无非就是泛型类、泛型方法、泛型接口;
我们查看List、ArrayList源码,就可以看到泛型的三种使用.

但是下面我给出一个更简单的例子:

class Book<T> {

    private T data;

    public Book() {

    }

    public Book(T data) {
        this.data = data;
    }

    public T getData() {
        return data;
    }

}

我们常见的如T、E、K、V等形式的参数常用于表示泛型形参,由于接收来自外部使用时候传入的类型实参。

在使用泛型的时候如果传入泛型实参,则会根据传入的泛型实参做相应的限制,此时泛型才会起到本应起到的限制作用。如果不传入泛型类型实参的话,在泛型类中使用泛型的方法或成员变量定义的类型可以为任何的类型。
给你个例子看看:

Book b1 = new Book("111111");
Book b2 = new Book(4444);
Book b3 = new Book(55.55);
Book b4 = new Book(false);

Log.d("测试","key is " + b1.getData()); //测试key is 111111
Log.d("测试","key is " + b2.getData()); //测试key is 4444
Log.d("测试","key is " + b3.getData()); //测试key is 55.55
Log.d("测试","key is " + b4.getData()); //测试key is false

总结: 1.泛型的类型参数只能是类类型,不能是简单类型
2.不能对确切的泛型类型使用instanceof操作。如下面的操作是非法的,编译时会出错。

if(b4 instanceof Book<StoryBook>){
}

接口

接口常用在类产生器中:

//定义一个泛型接口
public interface Book<T> {
    public T next();
}

当实现泛型接口的类,未传入泛型实参时:

/**
 * 未传入泛型实参时,与泛型类的定义相同,在声明类的时候,需将泛型的声明也一起加到类中
 * 即:class  StoryBook<T> implements Book<T>{
 * 如果不声明泛型,如:class StoryBook implements Book<T>,编译器会报错:"Unknown class"
 */
class StoryBook<T> implements Book<T>{
    @Override
    public T next() {
        return null;
    }
}

当实现泛型接口的类,传入泛型实参时:

/**
 * 传入泛型实参时:
 * 定义一个生产器实现这个接口,虽然我们只创建了一个泛型接口Book<T>
 * 但是我们可以为T传入无数个实参,形成无数种类型的Book接口。
 * 在实现类实现泛型接口时,如已将泛型类型传入实参类型,则所有使用泛型的地方都要替换成传入的实参类型
 * 即:Book<T>,public T next();中的的T都要替换成传入的String类型。
 */
public class StoryBook implements Book<String> {

    private String[] books = new String[]{"b1", "b2", "b3"};

    @Override
    public String next() {
        Random rand = new Random();
        return books[rand.nextInt(3)];
    }
}

泛型方法

泛型类与泛型方法的区别在于:泛型类是在实例化类的时候指明泛型的具体类型,而泛型方法是在调用方法的时候指明泛型的具体类型,通过一段代码介绍一下泛型吧。

/**
 * 泛型方法的基本介绍
 * @param tClass 传入的泛型实参
 * @return T 返回值为T类型
 * 说明:
 *     1)public 与 返回值中间<T>非常重要,可以理解为声明此方法为泛型方法。
 *     2)只有声明了<T>的方法才是泛型方法,泛型类中的使用了泛型的成员方法并不是泛型方法。
 *     3)<T>表明该方法将使用泛型类型T,此时才可以在方法中使用泛型类型T。
 *     4)与泛型类的定义一样,此处T可以随便写为任意标识,常见的如T、E、K、V等形式的参数常用于表示泛型。
 */
public <T> T genericMethod(Class<T> tClass)throws InstantiationException ,
  IllegalAccessException{
        T instance = tClass.newInstance();
        return instance;
}

或许这样可能还是还是很迷惑,可以给看看我看的一个大神博客的代码演示一下:

public class BookTest {
   public class Book<T>{     
        private T key;

        public Book(T key) {
            this.key = key;
        }

        //虽然在方法中使用了泛型,但是这并不是一个泛型方法。
        //这只是类中一个普通的成员方法,只不过他的返回值是在声明泛型类已经声明过的泛型。
        //所以在这个方法中才可以继续使用 T 这个泛型,但他并不是泛型方法。
        public T getKey(){
            return key;
        }

        /**
         * 这个方法显然是有问题的,在编译器会给我们提示这样的错误信息"cannot reslove symbol E"
         * 因为在类的声明中并未声明泛型E,所以在使用E做形参和返回值类型时,编译器会无法识别。
        public E setKey(E key){
             this.key = keu
        }
        */
    }

    /** 
     * 这才是一个真正的泛型方法。
     * 首先在public与返回值之间的<T>必不可少,这表明这是一个泛型方法,并且声明了一个泛型T
     * 这个T可以出现在这个泛型方法的任意位置.
     * 泛型的数量也可以为任意多个 
     *    如:public <T> T showKeyName(Book<T> container){
     *        ...
     *        }
     */
    public <T> T showKeyName(Book<T> container){
        System.out.println("container key :" + container.getKey());
        //当然这个例子举的不太合适,只是为了说明泛型方法的特性。
        T test = container.getKey();
        return test;
    }

    //这也不是一个泛型方法,这就是一个普通的方法,只是使用了Book<Number>这个泛型类做形参而已。
    public void showKeyValue1(Book<Number> obj){
        Log.d("泛型测试","key value is " + obj.getKey());
    }

    //这也不是一个泛型方法,这也是一个普通的方法,只不过使用了泛型通配符?
    //同时这也印证了泛型通配符章节所描述的,?是一种类型实参,可以看做为Number等所有类的父类
    public void showKeyValue2(Book<?> obj){
        Log.d("泛型测试","key value is " + obj.getKey());
    }

     /**
     * 这个方法是有问题的,编译器会为我们提示错误信息:"UnKnown class 'E' "
     * 虽然我们声明了<T>,也表明了这是一个可以处理泛型的类型的泛型方法。
     * 但是只声明了泛型类型T,并未声明泛型类型E,因此编译器并不知道该如何处理E这个类型。
    public <T> T showKeyName(Book<E> container){
        ...
    }  
    */

    /**
     * 这个方法也是有问题的,编译器会为我们提示错误信息:"UnKnown class 'T' "
     * 对于编译器来说T这个类型并未项目中声明过,因此编译也不知道该如何编译这个类。
     * 所以这也不是一个正确的泛型方法声明。
    public void showkey(T genericObj){

    }
    */

    public static void main(String[] args) {


    }
}

通过上面这些例子,对于泛型方法的使用应该更得心应手了吧。
刚刚在里面看到有泛型通配符,那么在这里插讲讲泛型通配符。

泛型通配符

通过泛型的特性我们知道Book<Number>和Book<Integer>都是Book类型,那么请我们思考一下是否可以Book<Number>和Book<Integer>是存在继承关系的泛型?
我们来看一段代码:

public class BookTest {

    public static void main(String[] args) {

        Book<Number> name = new Book<Number>(99);
        Book<Integer> age = new Book<Integer>(712);

        getData(name);
 
        getData(age);  //1
        //The method getData(Book<Number>) in the type BookTest is 
        //not applicable for the arguments (Book<Integer>)

    }
    
    public static void getData(Book<Number> data){
        System.out.println("data :" + data.getData());
    }

}

代码在//1处出现了错误提示信息,表示Book<Number>和Book<Integer>在逻辑上不能视为继承关系。
我们再来看一段代码:

public class BookTest {

    public static void main(String[] args) {

        Book<Integer> b1 = new Book<Integer>(712);
        Book<Number> b2 = b1;  // 1
        Book<Float> f = new Book<Float>(3.14f);
        b2.setData(f);        // 2

    }

    public static void getData(Book<Number> data) {
        System.out.println("data :" + data.getData());
    }

}

class Book<T> {

    private T data;

    public Book() {

    }

    public Book(T data) {
        setData(data);
    }

    public T getData() {
        return data;
    }

    public void setData(T data) {
        this.data = data;
    }

}

这段代码,我们一眼就知道在//1和//2处肯定会提示错误;运用数学中的证明方法,我们不妨设Book<Number>在逻辑上可以视为Book<Integer>的父类,那么//1和//2处将不会有错误提示了;
那么问题就出来了,通过getData()方法取出数据时到底是什么类型呢?Integer? Float? 还是Number?
在编程过程中的顺序不可控性,导致在必要的时候必须要进行类型判断,且进行强制类型转换。显然,这与泛型的理念矛盾,因此,在逻辑上Book<Number>不能视为Book<Integer>的父类。

通过实践,我们知道其具体的错误提示的深层次原因了。那么如何解决呢?总不能再定义一个新的函数吧。这和Java中的多态理念显然是违背的,因此,我们需要一个在逻辑上可以用来表示同时是Book<Integer>和Book<Number>的父类的一个引用类型,由此,类型通配符(?)就该是我们考虑的了。

类型通配符使用代替具体的类型实参,那么Book<?>就可以在逻辑上成为Book<Number>和Book<Integer>的父类,因此对上面代码进行改进:

public class BookTest {

    public static void main(String[] args) {

        Book<String> string = new Book<String>("tjun");
        Book<Integer> integer = new Book<Integer>(99);
        Book<Number> number = new Book<Number>(100);

        getData(string);
        getData(integer);
        getData(number);
    }

    public static void getData(Book<?> data) {
        System.out.println("data :" + data.getData());
    }

}

使用类型通配符就解决了上面的问题。

通配符上下限

谈到类型通配符,那么就一定要说说类型通配符上限和类型通配符下限;
比如我们对上面的例子有了新需求,定义一个功能类似于getData()的方法,但对类型实参又有进一步的限制:只能是Number类及其子类。此时,需要用到类型通配符上限(Book<? extends Number> data)。看看代码:

public class BookTest {

    public static void main(String[] args) {

        Book<String> string = new Book<String>("tjun");
        Book<Integer> integer = new Book<Integer>(99);
        Book<Number> number = new Book<Number>(100);

        getData(string);
        getData(integer);
        getData(number);
        
        //getUpperNumberData(string); // 1  错误提示
        getUpperNumberData(integer);    // 2 正常
        getUpperNumberData(number); // 3 正常
    }

    public static void getData(Book<?> data) {
        System.out.println("data :" + data.getData());
    }
    
    public static void getUpperNumberData(Book<? extends Number> data){
        System.out.println("data :" + data.getData());
    }

}

类型通配符上限通过形如Book<? extends Number>形式定义,相对应的,类型通配符下限为Book<? super Number>形式,其含义与类型通配符上限正好相反.

我们继续回到泛型的方法来说几句。

类中的泛型方法

泛型方法可以出现杂任何地方和任何场景中使用。但是有一种情况是非常特殊的,当泛型方法出现在泛型类中时,我们再通过一个例子看一下:

public class Test {
    class Book{
        @Override
        public String toString() {
            return "book";
        }
    }

    class StoryBook extends Book{
        @Override
        public String toString() {
            return "storyBook";
        }
    }

    class Love{
        @Override
        public String toString() {
            return "love";
        }
    }

    class BookTest<T>{
        public void show_1(T t){
            System.out.println(t.toString());
        }

        //在泛型类中声明了一个泛型方法,使用泛型E,这种泛型E可以为任意类型。可以类型与T相同,也可以不同。
        //由于泛型方法在声明的时候会声明泛型<E>,因此即使在泛型类中并未声明泛型,编译器也能够正确识别泛型方法中识别的泛型。
        public <E> void show_3(E t){
            System.out.println(t.toString());
        }

        //在泛型类中声明了一个泛型方法,使用泛型T,注意这个T是一种全新的类型,可以与泛型类中声明的T不是同一种类型。
        public <T> void show_2(T t){
            System.out.println(t.toString());
        }
    }

    public static void main(String[] args) {
       StoryBook storyBook = new StoryBook();
       Love love = newLove();

        BookTest<Book> bookTest = new BookTest<Book>();
        //StoryBook是Book的子类,所以这里可以
        bookTest.show_1(storyBook);
        //编译器会报错,因为泛型类型实参指定的是Book,而传入的实参类是Love
        //bookTest.show_1(love);

        //使用这两个方法都可以成功
        bookTest.show_2(storyBook);
        bookTest.show_2(love);

        //使用这两个方法也都可以成功
        BookTest.show_3(storyBook);
        BookTest.show_3(love);
    }
}

泛型方法还有一个重要的知识点就是可变参数得提一下:

public <T> void printMsg( T... args){
    for(T t : args){
        Log.d("泛型测试","t is " + t);
    }
}

printMsg("111",222,"aaaa","2323.4",55.55);

泛型与静态方法

方法要使用泛型的话,必须将静态方法也定义成泛型方法 。

public class StaticTest<T> {
    ....
    /**
     * 如果在类中定义使用泛型的静态方法,需要添加额外的泛型声明(将这个方法定义成泛型方法)
     * 即使静态方法要使用泛型类中已经声明过的泛型也不可以。
     * 如:public static void show(T t){..},此时编译器会提示错误信息:
          "StaticTest cannot be refrenced from static context"
     */
    public static <T> void show(T t){

    }
}

即:如果静态方法要使用泛型的话,必须将静态方法也定义成泛型方法 ;静态方法无法访问类上定义的泛型;如果静态方法操作的引用数据类型不确定的时候,必须要将泛型定义在方法上。

泛型总结

泛型方法能使方法独立于类而产生变化,以下是一个基本的指导原则:

无论何时,如果你能做到,你就该尽量使用泛型方法。也就是说,如果使用泛型方法将整个类泛型化,那么就应该使用泛型方法。另外对于一个static的方法而已,无法访问泛型类型的参数。所以如果static方法要使用泛型能力,就必须使其成为泛型方法。

先就总结下泛型的学习吧,对于反射就接下来有时间就补充上。

最后,链上一个博客介绍泛型不错的文章

第一次写博客,望指正一起学习,轻喷,谢谢!

上一篇下一篇

猜你喜欢

热点阅读